🪷Girls in CTF 2025

Writeup for Girls in CTF 2025. This year, my team under the name Hacker Pencen managed to place 12th!

This writeup is all my solves (which is not much but it's all I can do given my condition at the time). Special shoutout to Sarah for being the hard carry fr ✨

Category: Web

Basic Login

Upon launching an instance, we can see that there is a column to input username. Input any username of your choice (mine is admin) and it will automatically generate the JWT token for you.

Upon successful token generation, there is a button to check/request for admin privileges. Clicking the button revealed that we indeed do not have the admin privileges.

The main goal is to perform JWT cracking by manipulating the token to gain admin privileges. With the help of chatgpt, I created a python script to help me uncover the password that is used to sign the JSON Web Token (JWT).

# check_secret_on_token.py
# Usage: python check_secret_on_token.py wordlist.txt
import jwt, sys
 
# ORIGINAL TOKEN (from you)
TOKEN =  <insert the token here>
 
if len(sys.argv) < 2:
    print("Usage: python check_secret_on_token.py wordlist.txt")
    sys.exit(1)
 
wordlist = sys.argv[1]
 
print("[*] Trying to verify original token using", wordlist)
with open(wordlist, errors="ignore") as f:
    for i, line in enumerate(f):
        s = line.strip()
        if not s:
            continue
        try:
            # we skip expiration verification to allow testing old tokens
            payload = jwt.decode(TOKEN, s, algorithms=["HS256"], options={"verify_exp": False})
            print("[+] FOUND secret:", s)
            print("[+] payload:", payload)
            break
        except jwt.exceptions.InvalidSignatureError:
            if i % 500 == 0:
                print(f"    tried {i} entries... still no")
        except jwt.exceptions.ExpiredSignatureError:
            print("[!] signature valid but token expired! secret:", s)
            try:
                payload = jwt.decode(TOKEN, s, algorithms=["HS256"], options={"verify_exp": False})
                print("payload:", payload)
            except Exception:
                pass
            break
        except Exception as e:
            print("[!] error for", s, type(e).__name__, e)
            break

We found the password used to sign the token, but the role is still a user. I use jwt.io to rebuild the token and sign it used the obtained secret passphrase princess.

Replace the modified token to the column and you’ll obtain the flag.

Flag: GCTF25{w34K_5ecr3t_1S_@_WEAK_prInC3S5_Mu@h!_21843aef0f58}

Nyan Nyan PDF Genereator

Upon launching an instance, we are directed to a webpage that generates a PDF upon input. The description hinted at secret so I assumed that the directory is as such.

Download and open the pdf file to get the flag

Flag: GCTF25{n07_a_9o0D_w@y_t0_geN3Ra73_PdF_nY@NN~_1c77db107556}

Nyeow Nyeow PDF Generator

Create an instance to access the webpage.

The challenge desc hinted at /nyan-nyan, hence I tried that one. <path /nyan-nyan>

Download and open .pdf to obtain the flag.

Flag: GCTF25{NY4N_ny@n_5hou1d_noT_AI1#w_r3D1r3Ct_WIuu~_b06f05422318}

Forensics

NHAT 1

First log tells us about History-journal

SourceFile

SourceFileSha1

Reason

C:\Users\zach\AppData\Local\Google\Chrome\User Data\Default\History-journal

DA39A3EE5E6B4B0D3255BFEF95601890AFD80709

Deduped

├── Local
│   └── Google
│       └── Chrome
│           └── User Data
│               └── Default
│                   ├── Favicons
│                   ├── Favicons-journal
│                   ├── History
│                   ├── Login Data
│                   ├── Network
│                   │   └── Cookies
│                   ├── Network Action Predictor
│                   ├── Preferences
│                   ├── Sessions
│                   │   ├── Session_13402043187619092
│                   │   ├── Session_13402043477686091
│                   │   ├── Tabs_13402043343841306
│                   │   └── Tabs_13402043477756296
│                   ├── Shortcuts
│                   ├── Top Sites
│                   └── Web Data
└── Roaming
    └── Microsoft
        └── Protect
            └── S-1-5-21-1013411303-2420397385-2210645690-1001
                ├── 2a226c54-7078-4263-aaed-c0bf57c31504
                └── Preferred

Reading the history:

strings History | grep "gctf"
gctf{w4Rm!ng_Up_WItH_BROw$3r_HIsToRY}gctf{w4rm!ng_up_with_brow$3r_history}Q
gctf{w4Rm!ng_Up_WItH_BROw$3r_HIsToRY}gctf{w4rm!ng_up_with_brow$3r_history}
gctf{w4rm!ng_up_with_brow$3r_history}
gctf{w4rm!ng_up_with_brow$3r_history}
gctf{w4Rm!ng_Up_WItH_BROw$3r_HIsToRY}
W       gctf{w4Rm!ng_Up_WItH_BROw$3r_HIsToRY}
<https://www.google.com/search?q=gctf%7Bw4rm!ng_up_with_brow%243r_history%7D&sourceid=chrome&ie=UTF-8gctf{w4rm!ng_up_with_brow$3r_history}>
<https://www.google.com/search?q=gctf%7Bw4rm!ng_up_with_brow%243r_history%7D&sourceid=chrome&ie=UTF-8gctf{w4rm!ng_up_with_brow$3r_history}>
<https://www.google.com/search?q=gctf%7Bw4Rm!ng_Up_WItH_BROw%243r_HIsToRY%7D&oq=gctf%7Bw4Rm!ng_Up_WItH_BROw%243r_HIsToRY%7D&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIHCAEQABjvBTIHCAIQABjvBTIKCAMQABiABBiiBDIHCAQQABjvBTIHCAUQABjvBdIBBzc5MmowajSoAgCwAgE&sourceid=chrome&ie=UTF-8https://www.google.com/>
<https://www.google.com/search?q=gctf%7Bw4Rm!ng_Up_WItH_BROw%243r_HIsToRY%7D&oq=gctf%7Bw4Rm!ng_Up_WItH_BROw%243r_HIsToRY%7D&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIHCAEQABjvBTIHCAIQABjvBTIKCAMQABiABBiiBDIHCAQQABjvBTIHCAUQABjvBdIBBzc5MmowajSoAgCwAgE&sourceid=chrome&ie=UTF-8https://www.google.com/>
<https://www.google.com/search?q=gctf%7Bw4Rm!ng_Up_WItH_BROw%243r_HIsToRY%7D&oq=gctf%7Bw4Rm!ng_Up_WItH_BROw%243r_HIsToRY%7D&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIHCAEQABjvBTIHCAIQABjvBTIKCAMQABiABBiiBDIHCAQQABjvBTIHCAUQABjvBdIBBzc5MmowajSoAgCwAgE&sourceid=chrome&ie=UTF-8&sei=XmLCaIryIpm64-EP7cTPyAU>
<https://www.google.com/search?q=gctf%7Bw4Rm!ng_Up_WItH_BROw%243r_HIsToRY%7D&oq=gctf%7Bw4Rm!ng_Up_WItH_BROw%243r_HIsToRY%7D&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIHCAEQABjvBTIHCAIQABjvBTIKCAMQABiABBiiBDIHCAQQABjvBTIHCAUQABjvBdIBBzc5MmowajSoAgCwAgE&sourceid=chrome&ie=UTF-8>
<https://www.google.com/search?q=gctf%7Bw4Rm!ng_Up_WItH_BROw%243r_HIsToRY%7D&oq=gctf%7Bw4Rm!ng_Up_WItH_BROw%243r_HIsToRY%7D&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIHCAEQABjvBTIHCAIQABjvBTIKCAMQABiABBiiBDIHCAQQABjvBTIHCAUQABjvBdIBBzc5MmowajSoAgCwAgE&sourceid=chrome&ie=UTF-8gctf{w4Rm!ng_Up_WItH_BROw$3r_HIsToRY}> - Google Search
<https://www.google.com/search?q=gctf%7Bw4Rm!ng_Up_WItH_BROw%243r_HIsToRY%7D&oq=gctf%7Bw4Rm!ng_Up_WItH_BROw%243r_HIsToRY%7D&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIHCAEQABjvBTIHCAIQABjvBTIKCAMQABiABBiiBDIHCAQQABjvBTIHCAUQABjvBdIBBzc5MmowajSoAgCwAgE&sourceid=chrome&ie=UTF-8&sei=XmLCaIryIpm64-EP7cTPyAUgctf{w4Rm!ng_Up_WItH_BROw$3r_HIsToRY}> - Google Search

Flag: gctf{w4rm!ng_up_with_brow$3r_history}

NHAT 2

From the shortcut. we can see the previous flag:

After few checks, there is one URL that stood out: https://github.com/ghostpack/sharpdpapi

After few checks, there is another URL that stood out: https://github.com/ourokronii-hololive/sharpup

We got some similar files:

Flag: gctf{57E4LINg_froM_Gh0STPACk}

Reverse Engineering

Invitation to Java

Decompiled the .jar file, the files given are:

  • Main.java — provides the encoded byte array and the key.

  • Validator.java — abstract class implementing a decode() that XORs every byte with the provided key and returns a UTF-8 string.

  • InviteValidator.java — concrete validator that checks whether the user input equals the decoded string.

  • Encoded bytes in Main:

byte[] encoded = new byte[]{61,57,46,60,33,16,59,44,27,5,107,41,5,44,105,40,3,5,60,47,20,5,24,15,46,5,20,106,46,5,21,106,42,39};
byte ke
y = 90;
decode() behavior in Validator:
for (int i = 0; i < encoded.length; i++) {
    result[i] = (byte)(encoded[i] ^ key);
}
return new String(result, StandardCharsets.UTF_8);

The program uses a simple XOR cipher where each byte in the encoded array is XOR'd with the single-byte key 90. Because XOR is reversible and the key is present in Main, we can recover the original plaintext by XOR'ing each encoded byte with the same key.

Apply the same XOR operation to the byte array and decode as UTF-8.

In this case, I use a short Java snippet to XOR each byte with 90 to reveal the flag:

import java.nio.charset.StandardCharsets;
public class Solve {
    public static void main(String[] args) {
        byte[] encoded = new byte[]{
            61, 57, 46, 60, 33, 16, 59, 44, 27, 5,
            107, 41, 5, 44, 105, 40, 3, 5, 60, 47,
            20, 5, 24, 15, 46, 5, 20, 106, 46, 5,
            21, 106, 42, 39
        };
        byte key = 90;
 
        byte[] decoded = new byte[encoded.length];
        for (int i = 0; i < encoded.length; i++) {
            decoded[i] = (byte)(encoded[i] ^ key);
        }
 
        String flag = new String(decoded, StandardCharsets.UTF_8);
        System.out.println(flag);
    }
}
Flag: gctf{JavA_1s_v3rY_fuN_BUt_N0t_O0p}

That's all! Yet another great CTF from the organizer, hope that I get to join the next one with a cleaner bill of health~

Last updated