FileSystem
前置知识
软链接
ln -s
zip -y
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.js1
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
75const 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 });
// Command: unzip temp/file.zip -d target_dir
execFile('unzip', [zipPath, '-d', userDir], (err, stdout, stderr) => {
fs.unlinkSync(zipPath); // Clean up temp file
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);
// Prevent path traversal
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.js1
2
3
4
5
6module.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.sh1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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在根目录随机文件夹下
查看dockerfile1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17FROM 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 | ln -s /app/.env 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 | const cookieSignature = require('cookie-signature'); |
1 | PS C:\Users\link\Desktop\CTF_Code_Tool> node express_session.js |






