constcrypto=require('crypto');classDatabase{constructor(){this.notes=[];this.secret=`secret-${crypto.randomUUID}`;}createNote({data}){constid=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){returncrypto.createHmac('sha256',this.secret).update(id.toString()).digest('hex');}}constdb=newDatabase();db.createNote({data:process.env.FLAG});constexpress=require('express');constapp=express();app.use(express.urlencoded({extended:false}));app.use(express.static('public'));app.post('/create',(req,res)=>{constdata=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;constnote=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/
We were given a binary file and libc file. Using Ghidra, we can see the decompiled code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
undefined8main(undefined4param_1,undefined8param_2){charlocal_22[10];undefined8local_18;undefined4local_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);return0;}
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).
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
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.
fromCrypto.Util.numberimportgetPrime,bytes_to_long,long_to_bytesdefgetAnnoyingPrime(nbits,e):whileTrue:p=getPrime(nbits)if(p-1)%e**2==0:returnpnbits=128e=17p=getAnnoyingPrime(nbits,e)q=getAnnoyingPrime(nbits,e)flag=b"dice{???????????????????????}"N=p*qcipher=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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
frompwnimport*fromCrypto.Util.numberimport*n=57996511214023134147551927572747727074259762800050285360155793732008227782157e=17c=19441066986971115501070184268860318480501957407683654861466353590162062492971# n is small, so it is easy to factor itp=172036442175296373253148927105725488217q=337117592532677714973555912658569668821phi=(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 flagforminMod(c,n).nth_root(gcd(e,phi),all=True):flag=long_to_bytes(m)ifb'dice'inflag:print(b'Flag: {flag.decode()}')exit()