这周末本来在赶报告ddl,写报告写累了准备去看看题休息一下,没想到一道题看了六七个小时卡住,浪费了很多时间.. 记录下这道题。
Amazing Crypto WAF
题目分析
给了docker,浏览一遍,两个container,开放服务的container是个代理,转发请求到内部container。proxy代码如下:
1 |
|
这里有个waf,过滤所有的get参数和post参数,waf 是这样的:
这个代理还有一个加密功能,会把post的部分参数值在请求前使用AES加密:
可以看到一些参数是不加密。加密结果是ENCRYPT:xxx
格式,在得到内层container的响应后,这个代理会正则匹配ENCRYPT:xxx
格式的数据,进行解密替换密文再返回给我们。
而内层container,sqlite 数据库,有登陆/注册、增删notes、查看notes功能,具体代码如下:
1 |
|
初始化题目的时候会创建一个flag用户,密码未知,然后插入一条body为flag的notes:
不过经过前面的分析,body和title字段进入数据库的是加密过的密文。
各种尝试
分析完题目逻辑后,审计源码。很明显后端/notes
处order是唯一的注入点,order by 后面可以直接用 asc,(case when 1 then randomblob(5000000000000) else 1 end)
,观察是否返回error来进行布尔盲注。
于是开始注入,因为过滤了select,起初我的payload是这样的:
1 | desc,(case when (substr(body,1,1)=char(100)) then randomblob(5000000000000) else 1 end) |
但是发现注不出flag的密文, 只能注出我自己的notes… 仔细看下sql语句:
1 | query_db(f'select * from notes where user = ? order by timestamp {order}', [g.user['uuid']]) |
order by后面受到 where 的限制,所以我们只能注出自己用户的内容。
这里卡了很久,尝试绕过waf,不过waf有个递归的urldecode,感觉绕不过去。翻了n遍文档寻找如何不用select的情况下构造一个新的查询逻辑,但是无果,感觉这条路是无解的。
于是我想到了读文件,要是能读文件直接读取 /tmp/secret,再把 sqlite.db 给拖下来,可以直接伪造 flag 用户。
本地起个环境,试试:
看起来可以!注入试一下,好像8太行,看下log:
没事了,python sqlite3 看起来不支持 readfile…
柳暗花明
经队里师傅的尝试,竟然能绕过WAF使用select,看看怎么利用的:
1 | /notes%3Forder=xxx |
回过头看下源码:
waf 是通过 request.args取的,而把?进行urlencode,后面的数据都被当成了path,自然可以通过waf。而请求后端的时候,path会自动进行一次urldecode拼接到url后面,%3F变成了?,后端就可以正常获取到order参数了。不过注意后面还多一个?,在我们注入的时候payload后面要加个注释,把这个问号注释掉,否则会报错。
是我菜了… 学到了。
绕过waf ,那就好办了,字符串也不需要用CHAR拼接了,构造payload:
1 | desc,(case when (select substr(body,%s,1)="%s" from notes where user!={userid}) then randomblob(5000000000000) else 1 end)-- |
注入出flag的密文,需要我们进行解密。很明显可以看到删除note处有一处很可疑:
/notes路由根本就没有处理 deleted 参数的逻辑,并且这里的note_uuid是我们可控的。redirect函数:
可以看到返回的内容里包含location,所以/delete_note路由的uuid参数就是一个用于解密的功能,把密文通过uuid参数发过去就可以了。
完整exp:
1 | import requests |
远程环境交互非常慢(还是https),本地跑是完全没有问题的。