这周末本来有报告ddl,说好的不打,结果还是忍不住去看了题(ROIS的Web每年评价都不错)… 打了两天,时间大部分浪费在了两个sql题目上。队里也没啥人打,一共打比赛的就四个人。
CandyShop
首先是Mongodb 注入,可以注入出密码。
后面pug模版渲染,address处可以拼接代码,但是没有require,构造成下面这种:
注意要有缩进,不然会报错很坑。我本地起了个环境,调试了一会才可以。
1 | import requests |
easyphp
需要绕过nginx对 /admin的过滤,以及路径中要包含 login 的检查。
这个题比较坑,我本地起环境直接报错,必须docker起,所以我就在源码里用 var_dump 调试。
跟flight路由源码,https://segmentfault.com/a/1190000017289717?sort=newest
最终用正则匹配url的时候在net/Route.php
:
pattern是/admin的时候,这个正则是这样的:
1 | #^/admin/?(?:\\?.*)?$#i |
并且默认不区分大小写,所以使用/Admin%3flogin
可以绕过。
后面还需要bypass对 ../ 的过滤,看看这个$request->query 怎么得到的:
这里因为前面%3f问号绕过,可能有点问题,%3f解码成了问号,后面的变成了参数:
可以看到login被识别成了一个GET参数,所以后面就可以拼接&来传入data来bypass WAF 对$_GET的过滤了。
但是 /ADMIN%3flogin=1&data=..%2f
不成功,因为%2f解码后变成了/,中间的又变成路径了。这里继续跟源码,发现 self::parseQuery 里有 parse_url,parse_url会进行一次urldecode:
所以二次编码可以绕过。
payload:/ADMIN%3flogin=1&data=..%252f..%252f..%252fflag
EasySQLi
这个题做了好久,最后还好终于出了,进行了很多的尝试。不过这题目从做下来到赛后跟出题人交流,学到了不少。
题目逻辑很简单,user()就是flag,需要注入出user()的数据。问题在于 where 的条件是假的,导致order by 的语句不会执行(其实where条件是真的也不能sleep,因为只有一行数据不需要order by)
失败的尝试
pdo 默认是可以堆叠的,但是这个题没给配置,就非常难受。开始的想法是order by 后面的语句根本就不会被执行,不堆叠不能做,所以测试了好久题目能不能堆叠。
本地搭建了一个非常近似的环境:
1 |
|
起个 8.0.21 的mysql:
1 | docker run --name sql -e MYSQL_ROOT_PASSWORD=123456 -p 2333:3306 -d mysql:8.0.21 |
默认pdo是可以堆叠的,所以本地这个环境堆叠没什么问题。
这个题代码里有一些对时间的操作,这个 set_time_limit(1);
加上最后的 usleep,我本来以为是让脚本运行时间恒定为一秒左右,防止时间盲注。但是本地测试发现直接order设置成 1;select sleep(4)
就可以sleep 4s。google 一下:
1 | The set_time_limit() function and the configuration directive max_execution_time only affect the execution time of the script itself. Any time spent on activity that happens outside the execution of the script such as system calls using system(), stream operations, database queries, etc. is not included when determining the maximum time that the script has been running. This is not true on Windows where the measured time is real. |
看起来mysql操作不受这个函数的影响。但是远程就是不延迟,这让我对远程是否可以堆叠产生了怀疑。
本地测试还发现一个有趣的现象,即使sleep,但是运行php脚本,马上就 echo 了行数。所以第二条语句是在后台执行的,而不是在执行阶段卡住。以为这是考点,但是没什么卵用(不能堆叠)。
经过了大量的本地测试和远程尝试,可以基本确定远程不能堆叠注入….
感觉不是时间盲注,但是题目对时间的操作确实奇怪,一直stuck,就去看另一个sql了。
直到第二天晚上放了hint:time-based。继续硬着头皮看,想到了一个问题,为什么用prepare和execute而不是直接query,莫非 prepare 的时候有延迟的方法?
然后就是各种尝试,使用mysql client 终端的 prepare 进行测试,测试很多发现后面还是不会执行…
发现updatexml
另一个 ns_shift_sql 题目是考报错的,我随便在这个题直接扔一个 updatexml 报错注入的payload,没想到居然在prepare阶段被执行了:
瞬间狂喜,感觉可以搞,但是不能用 sleep。要想办法让mysql去做一些费时的操作造成延迟,所以尝试用一大堆hex,因为这个计算量是成倍增加。
1 | prepare a from "select 1 where 0 order by updatexml(1,concat('~',(if(1,(select length(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex('11111111'))))))))))))))))))))))))))))))))))))))))))))))))))),'a'),1)),1)"; |
结果mysql这个容器直接挂了,而if后面改成0,不会挂。看样子可以盲注了。
我直接试了一下打远程,结果远程环境 mysql 直接挂了。减少点hex,经过测试,大约30个hex,里面数据是 ‘1’,不会挂掉,并且远程环境能延时3秒多,非常不错。
1 | import requests |
不过后面我尝试打挂题目,奇怪的是打不挂了。还以为出题人限制了,赛后问出题人没改题,应该是设置了 restart always。
赛后交流
与Nu1L的@wupco师傅交流了一下,他说prepare优先处理updatexml是老问题了,之前见过orz… 他用的是repeat函数来延迟,不过好像效果没有hex好,他跑了好几次的样子,时间都是精确到毫秒级别的。
与出题人也交流了一下,出题的想法是对抗mysql的优化,mysql 会提前计算一些表达式之类的能造成延迟。我在比赛中用的是mysql client 的prepare,没想到这跟pdo的prepare还有些不同。在命令行里updatexml prepare可以造成延时,但是在pdo却不可以:
我在prepare前,prepare和execute中间以及execute后都输出了一下时间,可以看到是在execute的时候才延时。所以这两个prepare还是有些不同的。
除了updatexml以外,出题人还提供了一种方式:
1 | ( |
利用 json_table 凭空捏一个表。
但是基于 json_table 和 updatexml 都无法注数据,也可以说是无法执行 from table_name
,一旦出现这个逻辑就没有时间差了,这与prepare的处理有关系(现在做注入题都需要看mysql源码了么orz)。
另外一个有趣的点是,做题的时候因为搞prepare,所以就去翻prepare的文档,发现了这个:
8.0.22版本后prepare在确定参数类型这块有些修改。比赛的时候题目描述里写明了mysql版本是8.0.21,当时就感觉这块会不会就是考点,但是联系不上。赛后跟出题人交流,prepare里用 updatexml 可以造成延迟触发的地方就是确定类型这里(出题人扒源码的图):
其中if (args[1]->const_item()
就是限制了不用访问其他表。
我启了一个8.0.22的docker测试,发现常字符串还可以延时,但是user()这种就不行了:
mysql8.0.22后就给修了。
ns_shaft_sql
题目大致意思是有46关,每一关传入一句sql,要让mysql报错,并且报错信息含有特定字符串(需要用select查询)。每过一关,会把你这关的sql语句里用到的所有函数提取出来,再去一个名为$mysql_function的数组里查找,如果存在就加入到黑名单,你下一关就不能用这个函数了。有个例外,unhex和concat两个函数可以随便使用,相当于在白名单里。
经过测试常见的报错注入函数,像 updatexml,extractvalue都在这个列表里,猜测这应该是个mysql所有函数的列表?为了搞出这个列表,我把mysql文档上的function列表拖了下来,然后写了个脚本去测试:
1 | import requests |
发现很多都在列表里,所以把没在列表里的函数打出来:
1 | AND |
这些都是随意使用没有次数限制的,发现了一些ST开头的没被ban,尝试了几下不行。赛后问0ops师傅的做法,就是利用ST_LineStringFromText
这个函数,我的姿势不对…
只有传入一些特定参数才能触发。
Nu1L的wupco师傅用的是这个exp:set @@sql_mode:=(select concat(0x22,v) from s where k='xxx');
,很巧妙。
与出题人交流,出题人表示预期就是用46种不同的方式来报错,这两个解法都属于非预期。那个mysql function list 原意是所有的mysql函数,但是不全导致了 0ops 师傅的非预期。
预期解是看mysql源码,理解报错原因,反向定位批量找。编译一个debug版本的mysql,然后用vscode +gdb动态调试… 具体看官方wp吧。
其他题目有时间再复现吧… verysafe听说类似2020巅峰极客初赛一道题,利用pearcmd.php,再结合 caddy 一个目录穿越。