A CTF by the ISSS, a student-run organization in the Department of Computer Science at the University of Texas at Austin. They work on teaching students practical hacking skills, spreading awareness about security, and running hands-on demos and challenges for students to practice their skills.

March 8 - March 10 2019

UTCTF 2019

The UTCTF provided a nice range of challenges to introduce our club to the college-level CTF scene. Not all of these are full solutions - there were plenty that I worked on that we didn't solve. Plenty of lessons learned, though!

[basics] re (Reverse Engineering)

I know there's a string in this binary somewhere.... Now where did I leave it?

Opening the provided binary, the flag jumps right out in plain text:


[basics] (crypto)

Can you make sense of this file?

The file is full of binary. Converted to ascii it contains:

Uh-oh, looks like we have another block of text, with some sort of special encoding. Can you figure out what this encoding is? (hint: if you look carefully, you'll notice that there only characters present are A-Z, a-z, 0-9, and sometimes / and +. See if you can find an encoding that looks like this one.)

The Base64 decodes to:

New challenge! Can you figure out what's going on here? It looks like the letters are shifted by some constant. (hint: you might want to start looking up Roman people).
kvbsqrd, iye'bo kvwycd drobo! Xyg pyb dro psxkv (kxn wkilo dro rkbnocd...) zkbd: k celcdsdedsyx mszrob. Sx dro pyvvygsxq dohd, S'fo dkuox wi wocckqo kxn bozvkmon ofobi kvzrklodsm mrkbkmdob gsdr k mybboczyxnoxmo dy k nsppoboxd mrkbkmdob - uxygx kc k celcdsdedsyx mszrob. Mkx iye psxn dro psxkv pvkq? rsxd: Go uxyg drkd dro pvkq sc qysxq dy lo yp dro pybwkd edpvkq{...} - grsmr wokxc drkd sp iye coo drkd zkddobx, iye uxyg grkd dro mybboczyxnoxmoc pyb e, d, p, v k, kxn q kbo. Iye mkx zbylklvi gybu yed dro bowksxsxq mrkbkmdobc li bozvkmsxq drow kxn sxpobbsxq mywwyx gybnc sx dro Oxqvscr vkxqekqo. Kxydrob qbokd wodryn sc dy eco pboaeoxmi kxkvicsc: go uxyg drkd 'o' crygc ez wycd ypdox sx dro kvzrklod, cy drkd'c zbylklvi dro wycd mywwyx mrkbkmdob sx dro dohd, pyvvygon li 'd', kxn cy yx. Yxmo iye uxyg k pog mrkbkmdobc, iye mkx sxpob dro bocd yp dro gybnc lkcon yx mywwyx gybnc drkd cryg ez sx dro Oxqvscr vkxqekqo.
rghnxsdfysdtghu! qgf isak cthtuike dik zknthhkx rxqldgnxsliq risyykhnk. ikxk tu s cysn cgx syy qgfx isxe kccgxdu: fdcysn{3hrxqld10h_15_r00y}. qgf vtyy cthe disd s ygd gc rxqldgnxsliq tu pfud zftyethn gcc ditu ugxd gc zsutr bhgvykenk, she td xksyyq tu hgd ug zse scdkx syy. iglk qgf khpgqke dik risyykhnk!

Running this through a Caesar shift (ROT 16):

alright, you're almost there! Now for the final (and maybe the hardest...) part: a substitution cipher. In the following text, I've taken my message and replaced every alphabetic character with a correspondence to a different character - known as a substitution cipher. Can you find the final flag? hint: We know that the flag is going to be of the format utflag{...} - which means that if you see that pattern, you know what the correspondences for u, t, f, l a, and g are. You can probably work out the remaining characters by replacing them and inferring common words in the English language. Another great method is to use frequency analysis: we know that 'e' shows up most often in the alphabet, so that's probably the most common character in the text, followed by 't', and so on. Once you know a few characters, you can infer the rest of the words based on common words that show up in the English language.
hwxdnitvoitjwxk! gwv yiqa sjxjkyau tya padjxxan hngbtwdnibyg hyiooaxda. yana jk i soid swn ioo gwvn yinu asswntk: vtsoid{3xhngbt10x_15_h00o}. gwv ljoo sjxu tyit i owt ws hngbtwdnibyg jk fvkt pvjoujxd wss tyjk kwnt ws pikjh rxwloauda, ixu jt naioog jk xwt kw piu istan ioo. ywba gwv axfwgau tya hyiooaxda!

And some work with a substitution cipher:

congratulations! you have finished the beginner cryptography challenge. here is a flag for all your hard efforts: utflag{3ncrypt10n_15_c00l}. you will find that a lot of cryptography is just building off this sort of basic knowledge, and it really is not so bad after all. hope you enjoyed the challenge!

Low Sodium Bagel (Forensics)

I brought you a bagel, see if you can find the secret ingredient.

OK, so stego it is…

It’s a jpeg file, and has the appropriate FFD8 header and FFD9 footer. There’s a 456789:CDEFGHIJSTUVWXYZcdefghijstuvw string in the header, so we’re probably looking at standard encoded stego that needs a password.
No love from strings…and that’s a lot of 5 character garbage to look through…

What if there’s no password…let’s try it in https://futureboy.us/stegano/decode.pl with no password.


Lesson learned…try it without a password before spending the time looking for a password that isn’t there.

HabbyDabby's Secret Stash (Web)

HabbyDabby's hidden some stuff away on his web server that he created and wrote from scratch on his Mac. See if you can find out what he's hidden and where he's hidden it!

We just get a plain page with :

Welcome to HabbyDabby's Secret Stash
You'll never get our secrets!

Looking in the web inspector, we can see a hidden form using GET to select “file” from two options english.html or french.html. Formatting a GET request for those files:


just gets us the original English text


just gets the same text in French

But, it means we can grab files from the server. How about index.php?

if ( isset( $_GET['file'] ) ) {
  $file = $_GET['file'];
  if( !file_exists($file) ) die("File not found");
  if ($file === "english.html" || $file === "french.html"){
    echo file_get_contents( $_GET['file'] );
    // Force the download
    header("Content-Disposition: attachment; filename=" .  basename($file));
    header("Content-Length: " . filesize($file));
    header("Content-Type: application/octet-stream;");

  echo file_get_contents("index.html");

So, if the file we ask for is English.html or french.html, it serves those up. Otherwise, it forces a download of the requested file…just as it did for this one. And if nothing is requested, we just get the plain English version default in index.html.

Let’s try http://a.goodsecurity.fail/?file=/etc/passwd

And it does dump the passwd file:

list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin

But we don’t have permissions for the /etc/shadow file:

Warning: readfile(/etc/shadow): failed to open stream: Permission denied in /var/www/site/index.php on line 13

dirb shows a directory /a/ and navigating there shows a directory d which contains fakeflag.txt

It also shows a directory /e/ and navigating there shows a directory d which contains a directory e, which contains flag.txt:


This flag didn’t make a lot of sense, until I checked out another file that dirb found, called script:

mv dsstore.txt .DS_Store
mv e/dsstore.txt e/.DS_Store
mv e/d/dsstore.txt e/d/.DS_Store
mv e/d/e/dsstore.txt e/d/e/.DS_Store

FaceSafe (Misc)

The world's most elegant authentication platform

FaceSafe (BETA) is a secret-keeping platform that uses state-of-the-art AI to authenticate you. FaceSafe lets you store text secrets and gain access to them just based off of your picture!

Current users of FaceSafe (bold indicates VIP user):
• Mr. Airplane
• Ms. Automobile
• Ms. Bird
• Mr. Cat
• Mr. Deer [VIP]
• Mr. Dog
• Mr. Frog
• Mrs. Horse
• Mrs. Ship
• Ms. Truck

Support for more users will be added soon!

Current users: In order to gain access to your secret text, just upload a photo of yourself (32x32px):

So, possibly some file upload RCE. Looking in the web inspector, there is a comment:

<!-- TODO: cleanup metadata txt (robots, humans, etc) -->

Let’s look at robots.txt. Lots to work with here:

Disallow: /api/model/auth
Disallow: /api/model/check
Disallow: /api/model/expose
Disallow: /api/model/infer
Disallow: /api/model/model_metadata.json
Disallow: /api/model/model.model
Disallow: /static/event.png
Disallow: /static/find.png
Disallow: /static/bad.png

Or not…none of these is accessible (i.e. URL not found on the server). But it does tell us they are using some json data.

Tried uploading a PNG, but finding the site very slow to respond – high traffic or slow processing?

Tried a 32x32 pixel PNG file specifically (safari_32x32.png) and FaceSafe has identified me as Ms Bird:

FaceSafe identified you as Ms. Bird. Welcome, Ms. Bird!
Here is the secret you stored:

I’m pretty sure I want bold-face VIP Mr. Deer. But how to get that…?

Going back to the inspector, here’s the send.js that’s reading the input image:

  $("#image-upload").change(function () {
    console.log("Sending image over...");
    // Read the input image
    var reader = new FileReader();
    reader.onload = function() {
      var b64 = reader.result;
      // Send it over to the server
      var fd = new FormData();
      fd.append('image', document.querySelector('#image-upload').files[0]);
        url: '/api/check',
        data: fd,
        type: 'POST',
        processData: false,
        contentType: false,
        success: function(result) {
          result = JSON.parse(result);
      $("#greeting").html("FaceSafe identified you as " + result["user"] + ". Welcome, " + result["user"] + "!");
      $("#authenticated-secret").css("visibility", "visible");

There’s reference to an /api/check URL, so maybe all of the /api/model/ URLs were changed to /api/. Browsing over to /api/check, we now get a Method Not Allowed message…but the rest of the /api/ URLs are still not found.

Let’s grab a deer image and make it a 32x32 PNG....and it identifies us as Mr. Airplane: VROOM VROOM AIRPLANE SECRET

So, we’re making progress – it doesn’t just default to Ms. Bird, and my deer picture was black and white…maybe it’s cuing on the colours…blue Safari logo get’s us Ms. Bird, white deer gets us Mr. Airplane. Let’s try a more fawn coloured deer this time, and hope we don’t get Mr Dog or Horse.


Well, let’s see if it reacts the same way to the same image. Yes! Consistency is good.

OK, let’s hit it with some random 32x32 PNGs from iconfinder.com. My Cancel Icon, even if I do name it deer.png, comes back as Ms. Bird…so some shape identification (it’s circular like the Safari icon).

A grey disk icon gives us Mrs. Ship: HONK HONK

A red X gives us Ms Truck: VROOM VROOM

A team mate got Dog and Cat with greyscale images of her math homework.

A red error triangle gives us Ms Automobile: SKRRT SKRRT I AM A CAR

But a plain black and white triangle gives us Ms. Bird again.

Let’s go back to /api/check. Changing the method to POST in Burp, we get a message:

{“error”: “No image provided”}

And sending an image normally shows that we’re using the /api/check POST

But now we get an updated description on the site:

New Description: Can you get the secret? http://facesafe.xyz
Like any startup nowadays, FaceSafe had to get on the MACHINELEARNING™ train. Also, like any other startup, they may have been too careless about exposing their website metadata...
Hint: MACHINELEARNING™ logic: if it looks like noise, swims like noise, and quacks like noise, then it's probably... a deer?

White noise image gives us Cat: MEOWWWW

Different white noise gives us Horse: NEIGGGHHHH

And trying a dozen other variations on white noise and noisy images without any new successes, that’s as far as I got on this one.

Regular Zips (Forensics)

^ 7 y RU[A-Z]KKx2 R4\d[a-z]B N$

And they provide a RegularZips.zip file, which requires a password to open.

Well, the title and the text would suggest that they are providing a Regex clue to the password. Time to decompose this:

^ is an anchor, so the password starts with the rest of the expression
_7_y_RU these are just characters, so starts with “ 7 y RU”
[A-Z] – some letter between capital A and capital Z
KKx2_R4 another character string “KKx2 R4”
\d any single digit (0-9)
[a-z] some letter between lower case a and lower case z
B_N more string “B N“
$ the end anchor, so we’ve got a set length

Time to write a script to automate testing of passwords that meet these criteria

from zipfile import ZipFile
import string

zip_file = 'RegularZips.zip'

password1 = " 7 y RU"
AtoZ = string.ascii_uppercase
password2 = "KKx2 R4"
d = 0
atoz = string.ascii_lowercase
password3 = "B N"

for AZ in AtoZ:
  password = password1 + AZ + password2 + str(d) + az + password3
  # block raising an exception
    with ZipFile(zip_file) as zf:
    print (password)
    pass # doing nothing on exception
  for d in range(0,9):
    password = password1 + AZ + password2 + str(d) + az + password3
    # block raising an exception
      with ZipFile(zip_file) as zf:
      print (password)
      pass # doing nothing on exception
      for az in atoz:
        password = password1 + AZ + password2 + str(d) + az + password3
        # block raising an exception
          with ZipFile(zip_file) as zf:
          print (password)
          pass # doing nothing on exception

OK. So, the script got me the password (“ 7 y RUVKKx2 R48aB N”) and that extracts hint.txt, which is empty…hrm,,,

Looking at the original ZIP file in Hex Fiend, I can see that there is a PKZIP file hint.txt in there and a PKZIP archive.zip file a little further in. Let’s isolate the latter…

That works, and also requires a password, and my little script isn’t having any luck opening it.

I separated out both ZIP files and binwalk shows nothing unexpected. My script as no effect

Oh..this is weird. I'm trying the password manually, and I'm being prompted for a second password for archive.zip. It's not accepting the hint.txt password. So, there's something more complicated in here. I think I need to look at differently passworded files in the same ZIP

I tried it in a different extractor (RAR) and got hint with some garbled characters:

Po*û âKeÃÖz©‰∫<‹áµˇfWŸ54√¶

Trying it using unzip on the CLI, I get:

bad CRC 333b74b4 (should be ca7a1299)
(may instead be incorrect password)

Oooh. Tricksie. I think it's a password that's beyond that iteration in the script. This is just a bad password.

Yeah. I started my loop past the current password and got another hint.txt (also blank, but I'll keep trying)

Nevermind, I don’t think I’m getting the password at all, just close ones that produce a broken hints.txt file.

Maybe it’s the way I’m supplying the passwords – extractall might not be the way to go.

Oh…the man page has something interesting to add:

The correct password will always check out against the header, but there is a 1-in-256 chance that an incorrect password will as well. (This is a security feature of the PKWARE zipfile format; it helps prevent brute-force attacks that might otherwise gain a large speed advantage by testing only the header.) In the case that an incorrect password is given but it passes the header test anyway, either an incorrect CRC will be generated for the extracted data or else unzip will fail during the extraction because the ''decrypted'' bytes do not constitute a valid compressed data stream.

And it turns out that there’s a display error on the CTF page. The regex should be:

^ 7 y RU[A-Z]KKx2 R4\d[a-z]B N$

with tabs, not spaces

Running my script again, it returns a working password,

7 y RUHKKx2 R47gB N

but the hint.txt file is still broken.

I tried using this by hand on the CLI using unzip for a single member:

unzip -p RegularZips.zip hint.txt

And now I get the hint.txt

^\d00 2[a-z]F\s3u8J 1NzvA3l$

Another REGEX!

\d is a digit
00 2
[a-z] is some lowercase
\s is a whtespace character (space, tab, linefeed, return, formfeed, vertical tab)

So, I revised the original script to tackle this regex, and this version refused to open the second zip automatically or with me changing up the whitespace characters manually, nor did it return a password to test manually.

Tale of Two Cities (Cryptography)

Looks like this book got a little messed up... there are some weird characters in there.
Hint: hOpEfully thIS hint will help you!

The attached file is a text file of the Guttenberg Project’s copy of A Tale of Two Cities. Scanning quickly through this copy, there are random Chinese characters in the text.

Running a diff against the original Guttenberg copy, shows these characters have been inserted:

㐾�㐻㐌㐟㐀㐏㑖㐄㐓㐀㐴㐀㐄㐻㐉㐴㐷㐻㐾㐇㑎㑟Offset: 0x3400

And they replace the following letters:

ow " me i wi as a-m Li wo to es des fa go ter nd ote th nes an brance of the occ

Google translate doesn’t give up the goods.

The Unicode for the Chinese characters is:


Maybe they just carry the hex values?
3e fd 3b 0c 1f 00 0f 56 04 13 00 34 00 04 3b 09 34 37 3b 3e 07 4e 5f 20

That isn’t giving anything sensible in ASCII.

Here are the decimal values:
62 253 59 12 31 0 15 86 4 19 0 52 0 4 59 9 52 55 59 62 7 78 95 32

Looking back at the hint:
hOpEfully thIS hint will help you!

It is likely a reference to https://oeis.org/A000788, which an entry about the “Total number of 1’s in binary expressions of 0, …, n.”

Converting the Unicode characters to binary, we get:
00110100 00111110
11111111 11111101
00110100 00111011
00110100 00001100
00110100 00011111
00110100 00000000
00110100 00001111
00110100 01010110
00110100 00000100
00110100 00010011
00110100 00000000
00110100 00110100
00110100 00000000
00110100 00000100
00110100 00111011
00110100 00001001
00110100 00110100
00110100 00110111
00110100 00111011
00110100 00111110
00110100 00000111
00110100 01001110
00110100 01011111

This was as far as we got.

Rogue Leader (Forensics)

Our once-venerable president has committed the unspeakable crime of dine-and-dashing the pizza during our own club meetings. He's on the run as we speak, but we're not sure where he's headed.

Luckily, he forgot that we had planted a packet sniffer on his laptop, and we were able to retrieve the following capture when we raided his apartment:


He's too smart to email his plans to himself, but I'm certain he took them with him somehow. Can you help us figure out which country he's fleeing to?

Opening the pcap in Wireshark, we can see an enormous amount of USB communications between 2.6.4, 2.6.1, and host. Filtering out those using:

((!(usb.dst == "host")) && !(usb.src == "host"))

there are communications between and:

ec2-52-89-40-59.us-west-2.compute.amazonaws.com (
ec2-52-35-187-130.us-west-2.compute.amazonaws.com (
ec2-54-201-41-167.us-west-2.compute.amazonaws.com (
ec2-54-187-196-47.us-west-2.compute.amazonaws.com (
video-edge-c2b2f4.dfw02.abs.hls.ttvnw.net (
video-weaver.dfw02.hls.ttvnw.net ( ( (
client-event-reporter-production-854057631.us-west-2.elb.amazon (
twitch.map.fastly.net (
a23-77-88-53.deploy.static.akamaitechnologies.com (

DNS queries including:
None of the sites except incoherency.co.uk is accessible, and it is just a blog.

The video-edge and video-weaver connections and a connection with twitch.map.fastly.net sound promising. I don't know the first thing about Twitch streaming, but maybe that's where we should be looking?

One of my teammates also noticed that the USB communications are mouse and other controllers and such, and the mouse is giving it’s movements in hex. They were trying to plot the movements.

We tried to map out the hex movements, but ended without any results.

VisageNovel (Web)

After becoming king, Shrek decides to create a social network for all citizens of Duloc, using the most modern web technologies such as React and Express. Become an admin and gain access to the exclusive admins-only flag portal.


Note: please make up new passwords to use on this site. It's probably safe, but I make no guarantees!

We’re presented with a social networking site where we can login or register. Registering with a new account, we are shown our new profile, including a share link /userProfile/username

First Name Hi
Last Name There
Email hi@there.com
UserName hithere
ShareLink /userProfile/hithere

There are also buttons to:
Get Flag – clicking on this right now gets a message saying we’re not an admin
Update User – lets you update profile items, like name, email, and status – changing status to admin does not get us the flag
Update Password – lets us change the password without providing the current password.
Logout – logs us out

So, I think we need to find Shrek’s account, and get the flag from there.

Well, they saw that one coming…the Shrek account does not have any buttons, just a message:


Which is actually a button. Clicking on it tells you that this is being reported to an admin, and is replaced with the message:

Thank you for reporting! An admin is looking into this and should make a decision within 10 minutes.

King Shrek’s email is shrek@shrek.fr, username is shrek, and share link is /userProfile/shrek. So, maybe some information for changing passwords later, and we can see it’s not case sensitive on the usernames.

OK, how about userProfile/admin?

Names Garret Gu, email fake email, username admin, Status: Hello I am the admin. And that inappropriate button again.

I wonder if bold admin in the status helps…

Well, it does accept <b>admin</b>, but it doesn’t get the flag
The whole string “Hello I am the admin.” doesn’t do the trick either

Looking at other profiles (test. Test1, etc) also shows profile information and the INAPPROPRIATE button…so, it knows who I am and that I’m not that user…maybe there’s a cookie to manipulate.

Yes, my local storage cookie shows me logged_in_user: hithere.
I changed that to admin and now my page is INAPPROPRIATE

I navigated to admin’s page, and now I can see the buttons!
Not so fast, clicking the Get Flag button, still gets me the “You are not an admin” message.

There is a second local cookie, JWT, but it’s a long random string of characters:


Perhaps I can update his password and log in as him to establish the correct JWT cookie…

Well, I’m getting “An error has occurred. Please go login again.”

And the new password isn’t recognised when I try to log in.
Logging back in as myself, I can see that the JWT cookie has changed:


Trying the same in Shrek’s account, shows that the logged_in_user cookie is case sensitive. And I’m still not an admin

Still looking around in the inspector, I can see a findUser function
Its header shows that JWT cookie as the entry for Authorization

URL: http://visagenovel.ga:3003/findUser?username=hithere
Status: 304 Not Modified
Source: Memory Cache

GET /findUser HTTP/1.1
Accept: application/json, text/plain, */*
Origin: http://visagenovel.ga
Authorization: JWT
Referer: http://visagenovel.ga/userProfile/hithere
DNT: 1
If-None-Match: W/"c6-V/90oITZgcTdGI1Wo4jeVQLzeX4"
Host: visagenovel.ga:3003
Accept-Language: en-us
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.3 Safari/605.1.15
Accept-Encoding: gzip, deflate
Connection: keep-alive

HTTP/1.1 304 Not Modified
Access-Control-Allow-Origin: *
ETag: W/"c6-V/90oITZgcTdGI1Wo4jeVQLzeX4"
Connection: keep-alive
Date: Sun, 10 Mar 2019 00:17:55 GMT
X-Powered-By: Express

Query String Parameters
username: hithere

Maybe I can see admin’s JWT cookie this way? No, I need the auth token to run this against username=admin

A preview of my findUser results shows:

  "auth": true,
  "first_name": "Hi",
  "last_name": "There",
  "email": "hi@there.com",
  "username": "hithere",
  "is_admin": false,
  "status": "Hello I am the <b>admin.</b>",
  "reported": false,
  "message": "user found in db"

So, can I just change “is_admin” to true somehow?

The find user is a report from the server. Maybe I can change this field by modifying my Update User or Update Password requests…

Changing each of my user profile items and capturing the steps in Burp:

GET /sanitize?content=1Hello+I+am+the+%3Cb%3Eadmin.%3C%2Fb%3E HTTP/1.1
Host: visagenovel.ga:3003
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:65.0) Gecko/20100101 Firefox/65.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://visagenovel.ga/updateUser/hithere
Origin: http://visagenovel.ga
Connection: close

OPTIONS /updateUser HTTP/1.1
Host: visagenovel.ga:3003
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:65.0) Gecko/20100101 Firefox/65.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization,content-type
Referer: http://visagenovel.ga/updateUser/hithere
Origin: http://visagenovel.ga
Connection: close

PUT /updateUser HTTP/1.1
Host: visagenovel.ga:3003
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:65.0) Gecko/20100101 Firefox/65.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://visagenovel.ga/updateUser/hithere
Content-Type: application/json;charset=utf-8
Authorization: JWT
Content-Length: 192
Origin: http://visagenovel.ga
Connection: close

{"first_name":"Hi","last_name":"There","email":"hi@there.com","status":"MUhlbGxvIEkgYW0gdGhlIDxiPmFkbWluLjwvYj4=", "checksum":"05cef879f90931200f361c1d50ca679cd4af15c8","username":"hithere"}

The content of my Status is being sanitised, but nothing else is. Not sure where it’s getting that checksum, or what it’s doing with it.

Let’s change that to:

{"first_name":"Hi1","last_name":"There1","email":"hi1@there.com", "is_admin":true,"status":"MUhlbGxvIEkgYW0gdGhlIDxiPmFkbWluLjwvYj4=", "checksum":"05cef879f90931200f361c1d50ca679cd4af15c8","username":"hithere"}

That didn’t do it. I tried removing authorization from the Access-Control-Request-Headers: authorization,content-type

Which gave me a debugger exception for main.3a75770c.js.Looking a bit closer at that, there is some code about “promoteUser”, but no apparent way to trigger the promotion.

Let’s look closer at the JWT cookie. My first cookie was:

Base64 decoding gives us:

My current cookie is:


So, it looks like we’ve got an id#, probably user-specific, and an IAT and some padding

According to https://openid.net/specs/draft-jones-json-web-token-07.html the IAT in a JWT (Json web token) is just the time the token was issued, and is optional.

Changing the id to 1 and b64 encoding we get:

Replacing my JWT with this and changing my logged_in_user to admin, and browsing to the admin’s profile, we get an error. Maybe the length is an issue, so playing with the padding we get a cookie without b64 padding.


Nope, still erroring…

OK, so we can get to the admin’s page by browsing to /userProfile/admin. We can see the Get Flag button by changing the logged_in_user cookie to admin. We need to customise the JWT cookie to have the Get Flag button work.

The JWT is made up of three parts:

A header showing the encryption type: {"alg":"HS256","typ":"JWT"}
A payload showing the data we’re passing: {"id":1,"iat":1552229779}
A signature made up of base64(header) + '.' + base64(payload) encoded using the algorithm set in the header (HS256)

Header’s b64 is:

Payload’s b64 is:

Now to HS256 encode

Actually, we can do the whole thing from the plain text header and payload at JWT.io:


Nope, error…hmm…it’s triggering the debugger at the transformResponse function, and showing an invalid signature.

There is an option to b64 encode the secret, too:


That’s no good either. It looks like I need a key for the HS256 encryption. And there’s a tool for that: https://github.com/brendan-rius/c-jwt-cracker

The cracker’s test JWT with a trivially small secret (Sn1f) cracked in just a few seconds. My first JWT on this site is taking a bit longer…we’ll have to let that sit for a while (i.e. “secret” should take about 3000 seconds, or 50 minutes). If it’s done properly, though, it will be a 256-bit secret…sure hope not.

Been a couple of hours and still running…not looking good for finding the secret and formatting a JWT of my own. And I ran out of time to go any further. I think a closer look at trying to change my is_admin to true in the Update User are and figuring our that checksum might have been worthwhile.

DragonScim Workshops (Web)

DragonScim is holding it's PKing workshop again! Word on the street is the admins get into the console via the Contact. They thought it might be clever and crafty if they also just created their name with inspiration from fish that collide with themselves. Oh, and lastly, they've left a joke for us. Here it is:

How do you kill a circus?
You go for the juggler.

Also, the admins love Maryland a lot... They've been there 5 times.


The website is for the Dragon Scimitar Conference 2019 in Gielinor, World 325 – something Runescape something something… There’s an About page (about.php), a Buy Tickets page (buy-tickets.php), and a Contact page (contact.php). Only the last of these has an interactive field. The contact page has a single field called Name, and a Send Message submit button. Providing a name, the page calls:


Trying some obvious names like admin, Daga (the contact on the page), ‘ OR 1=1 all turn up nothing.

Dirb’ing the site gets us:

==> DIRECTORY: http://dragonscim.xyz/css/
==> DIRECTORY: http://dragonscim.xyz/fonts/
==> DIRECTORY: http://dragonscim.xyz/images/
+ http://dragonscim.xyz/index.php (CODE:200|SIZE:7285)
==> DIRECTORY: http://dragonscim.xyz/js/
+ http://dragonscim.xyz/server-status (CODE:403|SIZE:302)

Nothing interesting in /css/

/fonts/ has a backup.txt that is something to do with team icons when base64 decoded:

{"1":{"ID":1,"name":"My icons collection","bookmark_id":"59no1thgiy900000","created":null, "updated":1548320570,"active":1,"source":"local","order":0,"color":"000000","status":1}, "59no1thgiy900000":[{"id":46053,"team":0,"name":"sea-ship-with-containers","color":"#000000","premium":0,"sort":2}, {"id":1420796,"team":0,"name":"car","color":"#000000","premium":0,"sort":3},{"id":1388831,"team":0, "name":"platform","color":"#000000","premium":0,"sort":4},{"id":45896,"team":0,"name":"travel", "color":"#000000","premium":0,"sort":5},{"id":46028,"team":0,"name":"barn","color":"#000000","premium":0, "sort":6},{"id":45882,"team":0,"name":"frontal-truck","color":"#000000","premium":0,"sort":1}]}

/images/ is full of stock photos

/js/ seems to be full of standard js libraries rather than site-specific ones.

From all of these listable directories, though, we can see that the site is running on:

Apache/2.4.25 (Debian) Server at dragonscim.xyz Port 80

Trying to escape the field using ; doesn’t work – the input is being sanitised to %3B

No more progress made on this one.

Alice sends Bob a meme (Cryptography)

Eve is an Apple Employee who has access to the iMessage keystore (because there is nothing stopping them). They know Alice and Bob use iMessage instead of Signal, therefore they decrypted their messages and see that Alice has sent Bob a meme. Eve suspects more is going on. Can you confirm their suspicions? We included a screenshot of the message, and the actual files sent in the iMessage chat.

OK. Looking at the files in Hex Fiend, I can immediately see a PKZIP containing alice.txt near the end of the meme.png. Isolating and unzipping this file we get:

M = 108453893951105886914206677306984937223705600011149354906282902016584483568647
n < 84442469965344
P = (88610873236405736097813831550942828314268128800347374801890968111325912062058, 76792255969188554519144464321650537182337412449605253325780015124365585152539)

Bob has included a PKZIP called bob.txt in his bobresponse.png file, too. Isolating again and unzipping we get:

Q = (27543889954945113502256551007964501073506795938025836235838339960818915950890, 75922969573987021583641685217441284832467954055295272505357185824478295962572)

Now, what kind of encryption uses these sorts of parameters?

Likely, P and Q are messages, since that is all that Bob sent back. Both messages appear to consist of two parts, each with 77 numbers

M is likely a key, and at 78 characters long, would be suitable for XOR’ing against the message sections.

I don’t know what n would be that it should be less than 84442469965344 rather than equal to something.

A hint in the CTF chat referenced the Montgomery Curve, which is central to Ellptic Curve Cryptography.

So, this starting to make more sense:

Public Key: Starting Point A, Ending Point E – the two values in P
Private Key: Number of hops from A to E – the value for n

But that would mean that M is the message and Bob is just replying with his own public key?

I’ll keep picking at this one after the CTF, and see what I come up with.

Scrambled (Forensics)

B2 R U F' R' L' B B2 L F D D' R' F2 D' R R D2 B' L R
L' L B F2 R2 F2 R' L F' B' R D' D' F U2 B' U U D' U2 F'
L F' F2 R B R R F2 F' R2 D F' U L U' U' U F D F2 U R U' F U B2 B U2 D B F2 D2 L2 L2 B' F' D' L2 D U2 U2 D2 U B' F D R2 U2 R' B' F2 D' D B' U B' D B' F' U' R U U' L' L' U2 F2 R R F L2 B2 L2 B B' D R R' U L
Have fun!

Hint: rubik steganography

Right, Googling the text provided aligns with the clue. These are Rubiks Cube Notations:

A single letter by itself refers to a clockwise face rotation in 90 degrees (quarter turn):
   F R U L B D

A letter followed by an apostrophe means to turn that face counterclockwise 90 degrees:
   F' R' U' L' B' D'

A letter with the number 2 after it marks a double turn (180 degrees):
   F2 R2 U2 L2 B2 D2

Let’s start with a Baconian, using the ‘ to indicate 1

First two lines come out to:


Let’s try the 2 character entries as 1s
First two lines come out to:


So, no, not Baconian.

Ahh, the clue has been updated to “Rubikstega”, which is a specific encoding using Rubik’s cube rotations, which is where I was headed next, but it’s nice to have a reference to the specific article discussing the technique involved:


First header – containing permutation information variable P
B2 R U F' R' L' B B2 L F D D' R' F2 D' R R D2 B' L R

Second header – containing length information (len)
L' L B F2 R2 F2 R' L F' B' R D' D' F U2 B' U U D' U2 F'

Secret message – may contain some padding at the end
L F' F2 R B R R F2 F' R2 D F' U L U' U' U F D F2 U R U' F U B2 B U2 D B F2 D2 L2 L2 B' F' D' L2 D U2 U2 D2 U B' F D R2 U2 R' B' F2 D' D B' U B' D B' F' U' R U U' L' L' U2 F2 R R F L2 B2 L2 B B' D R R' U L

Using the default base-9 table:

Base-9 DigitNotation (axis) 1Notation (axis) 2
0L (X)F (Y)
1R (X)B (Y)
2U (Z)L2 (X)
3D (Z)R2 (X)
4F2 (Y)U2 (Z)
5B2 (Y)D2 (Z)
6L’ (X)F’ (Y)
7R’ (X)U’ (Z)
8B’ (Y)D’ (Z)

First scramble:

Converted to decimal (There are a lot of super-unreliable base-0 converters out there! This one was working properly: https://www.translatorscafe.com/unit-converter/en-us/numbers/3-12/decimal-base-9/. In future, I may use the wide array of converters built into CyberChef):

So, the i value for permuting the encoding table is 6, the first digit of the decimal value of the first header, which means we get the new base-9 table values starting 6 digits after that.
6 255367 346187025 8607

Base-9 DigitNotation (axis) 1Notation (axis) 2
3L (X)F (Y)
4R (X)B (Y)
6U (Z)L2 (X)
1D (Z)R2 (X)
8F2 (Y)U2 (Z)
7B2 (Y)D2 (Z)
0L’ (X)F’ (Y)
2R’ (X)U’ (Z)
5B’ (Y)D’ (Z)

Second scramble:

Converted to decimal, retaining the leading zero:

The first digit is j (0), and the second digit is k (4). So from the digit at 2+j+1 (digit 3) to the digit at 2+j+k (digit 6),

0 4 7899 04849256020841

Which would make the message length 7899?

Let’s just assume that all of the remaining rotations are part of the secret message

Base-9 DigitNotation (axis) 1Notation (axis) 2
3L (X)F (Y)
4R (X)B (Y)
6U (Z)L2 (X)
1D (Z)R2 (X)
8F2 (Y)U2 (Z)
7B2 (Y)D2 (Z)
0L’ (X)F’ (Y)
2R’ (X)U’ (Z)
5B’ (Y)D’ (Z)

Message in base-9:

In binary:

Padded to make multiples of 8 and partitioned to 8-bit chunks:
00010000 10101001 11101010 10100101 10101010 01100100 01001010 11101100 01100000 10110000 00101000 10001100 00001010 00001111 11011110 00011001 10011011 11011101 11100101 10010001 11101111 10001110 10011010 00000100 00100100 00000110 11000111 11101010 01100100 11111001 11001100 10101100

And converted to text:
�ꥪdJ�`�(� ���� $��d�̬

Well, that’s anti-climactic…

I was working on this one in the final minutes of the CTF, and ran out of time, but zzzanderw posted a Python script that solves this ( https://github.com/zzzanderw/ctf-writeups/tree/master/utctf2019/scrambled) , so going back over their results, let’s see where I went wrong…

We diverge starting at the second scramble:
I got: 034818230545538566580
They got: 263101562434461477412

Why is their permuted table different from mine if we came out with the same first scramble and permutation sequence?

Aha…I used the default table and my permuted values instead of the modified decoding table and my permuted values…

beforenowNotation (axis) 1Notation (axis) 2
30D (Z)R2 (X)
41B’ (Y)D’ (Z)
62L (X)F (Y)
13R’ (X)U’ (Z)
84R (X)B (Y)
75L’ (X)F’ (Y)
06F2 (Y)U2 (Z)
27U (Z)L2 (X)
58B2 (Y)D2 (Z)

So, now the second scramble should be:

Converted to decimal:

The first digit is j (3), and the second digit is k (2). So from the digit at 2+j+1 (digit 6) to the digit at 2+j+k (digit 7),
3 2 887738540626863753

Which would make the message length 73

Carrying on with the permuted table, let’s see if we finally get a secret message out of the first 73 digits of the secret message section:

Converting that to binary and padding to make octets:
01110101 01110100 01100110 01101100 01100001 01100111 01111011 01101101 01111001 01011111 01100010 01110010 01100001 00110001 01101110 01011111 00110001 01110011 01011111 01110011 01100011 01110010 01100001 01101101 01100010 01101100 00110011 01100100 01111101

And we finally get the flag:

Baby Pwn (Pwnable)

nc stack.overflow.fail 9000

We get just the file babypwn and a server running the program.

This is my first attempt at a proper binary pwn; I’m in a bit over my head…

The program prompts the user for their name, says hello to the user, prompts for an operation (one of +, -, or *), then prompts for two operands. Any unexpected character in the operation returns the “That’s not a valid operation!” error, but any character after the allowed operation characters just gets ignored. Characters after the operand numbers are also ignored. Negative numbers are accepted normally.

Opening the file in Hex Fiend, there aren’t any obvious flags, but I can see that it is an ELF binary (compiled on an Ubuntu 5.4.0 system) and I can see the messages it produces:

Goodbye %s
Welcome to the UT calculator service
What is your name?
Hello %s
Enter an operation(+ - *):
That’s not a valid operation!
Enter the first operand:
Enter the second operand:
The product is: %ld
The sum is: %ld
The difference is: %ld
How did I get here?

So far, I’ve made it to all but the “How did I get here?” and “Exiting” messages.

Let’s look at it in IDA:

Main looks like so (my ## comments):

int __cdecl main(int argc, const char **argv, const char **envp)
public main
main proc near
push rbp ## push top of stack (currently at 0x400686)
mov rbp, rsp
mov eax, 0
call welcome ## call the welcome subroutine
mov eax, 0
call do_calc ## call the do_calc subroutine
mov esi, offset name
mov edi, offset format ; "Goodbye %s\n"
mov eax, 0
call _printf ## call the _printf subroutine to print Goodbye message
pop rbp
main endp

Welcome prompts for the user’s name, stores it in name (at 0x601080 where there appears to be about 100 addresses for name characters), formats it with a Hello, and prints the Hello username message, then returns to main.

push rbp
mov rbp, rsp
mov edi, offset s ; "Welcome to the UT calculator service"
call _puts
mov edi, offset aWhatIsYourName ; "What is your name?"
call _puts
mov edi, offset name
mov eax, 0
call _gets
mov esi, offset name
mov edi, offset aHelloS ; "Hello %s\n"
mov eax, 0
call _printf
pop rbp
welcome endp

do_calc is a bit more complicated. It accepts an operand, which has only one address location

public _IO_stdin_used
.rodata:00000000004008E0 _IO_stdin_used db 1
.rodata:00000000004008E1 db 0
.rodata:00000000004008E2 db 2
.rodata:00000000004008E3 db 0
.rodata:00000000004008E4 db 0
.rodata:00000000004008E5 db 0
.rodata:00000000004008E6 db 0
.rodata:00000000004008E7 db 0
.rodata:00000000004008E8 ; char format[]
.rodata:00000000004008E8 format db 'Goodbye %s',0Ah,0 ; DATA XREF: main+1D↑o
.rodata:00000000004008F4 align 8
.rodata:00000000004008F8 ; char s[]
.rodata:00000000004008F8 s db 'Welcome to the UT calculator service',0
.rodata:00000000004008F8 ; DATA XREF: welcome+4↑o
.rodata:000000000040091D ; char aWhatIsYourName[]
.rodata:000000000040091D aWhatIsYourName db 'What is your name?',0
.rodata:000000000040091D ; DATA XREF: welcome+E↑o
.rodata:0000000000400930 ; char aHelloS[]
.rodata:0000000000400930 aHelloS db 'Hello %s',0Ah,0 ; DATA XREF: welcome+2C↑o
.rodata:000000000040093A ; char aEnterAnOperati[]
.rodata:000000000040093A aEnterAnOperati db 'Enter an operation (+ - *): ',0
.rodata:000000000040093A ; DATA XREF: do_calc+B↑o
.rodata:0000000000400957 ; char aThatSNotAValid[]
.rodata:0000000000400957 aThatSNotAValid db 'That',27h,'s not a valid operation!',0
.rodata:0000000000400957 ; DATA XREF: do_calc+3E↑o
.rodata:0000000000400975 ; char aEnterTheFirstO[]
.rodata:0000000000400975 aEnterTheFirstO db 'Enter the first operand: ',0
.rodata:0000000000400975 ; DATA XREF: do_calc:loc_400745↑o
.rodata:000000000040098F ; char aEnterTheSecond[]
.rodata:000000000040098F aEnterTheSecond db 'Enter the second operand: ',0
.rodata:000000000040098F ; DATA XREF: do_calc+82↑o
.rodata:00000000004009AA ; char aTheProductIsLd[]
.rodata:00000000004009AA aTheProductIsLd db 'The product is: %ld',0Ah,0
.rodata:00000000004009AA ; DATA XREF: do_calc+D7↑o
.rodata:00000000004009BF ; char aTheSumIsLd[]
.rodata:00000000004009BF aTheSumIsLd db 'The sum is: %ld',0Ah,0
.rodata:00000000004009BF ; DATA XREF: do_calc+F6↑o
.rodata:00000000004009D0 ; char aTheDifferenceI[]
.rodata:00000000004009D0 aTheDifferenceI db 'The difference is: %ld',0Ah,0
.rodata:00000000004009D0 ; DATA XREF: do_calc+112↑o
.rodata:00000000004009E8 ; char aHowDidIGetHere[]
.rodata:00000000004009E8 aHowDidIGetHere db 'How did I get here?',0
.rodata:00000000004009E8 ; DATA XREF: do_calc:loc_400816↑o
.rodata:00000000004009FC ; char aExiting[]
.rodata:00000000004009FC aExiting db 'Exiting..',0 ; DATA XREF: do_calc+12D↑o

It looks like the operands are quite limited in length, too. Testing with a very large pair of operands causes the “How did I get here?” and “Exiting” messages to return, but no flag to speak of.

Getting back to do_calc, it accepts an operator as var_1 (1 byte) and checks to see that it is valid by comparing var_1 with 2A (*), 2B (+), and 2D (-) . If it is, then it accepts two operands var_10 (qword) and var_18 (qword).

At first it seems to store them in nptr (byte ptr -50h) and var_90 (byte ptr -90h), though using atoll, which I see is a C/C++ function that takes a number, discards whitespace, and returns a long int.

Again it compares var_1 with 2B (+), 2D (-), and 2A (*). If it’s a +, it proceeds to sum var_10 and var_18. If it’s a -, it differences var_10 and var_18. If it’s a *, it multiplies var_10 and var_18. If its anything else, like when it gets overwritten with another value by an overly long operand, we get the How did I get here? message.

So, the program itself appears to run as expected. What else is in here?

There are some random single characters represented in the eh_frame and eh_frame_hdr sections.

CTF Discord chatter is repeatedly mentioning leaking libc version information…I have no idea. Some Googling shows me that I can get a shell if I can leak the libc base, and compute the offset to the system’s /bin/sh string.

From: https://www.reddit.com/r/LiveOverflow/comments/7w6xec/leaking_libc/ Let's say your binary uses printf and another random libc function such as fgets, these will have a GOT entry. When your program will run, the entries will be overwritten with the offset to the functions in memory. Here if you want a shell you will have to leak libc base, compute offset to system and /bin/sh string. Now, the leaking part. If you have the challenge binary you can get the relative offset to the libc base (address 0 in the libc binary file, use the physical address ). Let's say your fuction is at offset 0x400, the libc base will be <addr of function>-0x400. Now you have access to puts, and you know were the GOT entry of fgets is. You can call in your exploit puts(<addr of GOT entry>), parse the output (extract the first bytes which will be the address in the GOT slot) and turn this to a number. We now know where fgets is in memory, and we saw how to get the base address of libc. We can thus compute its base in memory : libc_base = fgets_got_addr - 0x400 From here you can compute the address of any function/data of the libc in memory using this base. If /bin/sh is at offset 0x200 in the physical binary your can get a pointer to it like this : /bin/sh_mem = libc_base + /bin/sh_physical_offset

This requires knowing the version of libc, and I think I might already have part of that information from the hex: Ubuntu 5.4.0.

Where is the libc base? __libc_start_main is at 0x601028

And right now I have no idea how to exploit this information even if I do find it. Have to shelf this one until I get some more experience with this sort of challenge…still, for now I’m glad I was able to parse the machine code to follow the program.

RIP (Forensics)

My friend John sent me this password protected ZIP, but forgot the password. I’m sure you can still find a way in, though.

I started out by running the let-me-in.zip provided through John the Ripper, but after a day and a half, I had no results. Time to learn more about fine-tuning John…


 Score: 1350/27500
 Rank: 166/581

 Score: 950/27500 (+ 400 completed independently of teammates, but not first)
 Rank: 260/1266