CTF Writeup: 2023 DeadSec CTF: Trailblazer

Summary

One of the things that I love about CTFs is when they provide challenges that don't require knowledge of weird language quirks or obscure exploits or (ugh) guesswork but instead just a clear head and some common sense. Kudos to the designer of the DeadSec 2023 CTF Trailblazer challenge, which offered exactly this type of problem.

Recon

The Trailblazer challenge provided exactly one page to the site and no source code was provided. Visiting the home page of the site provided the following text content:

1[0-9 a-z A-Z / " \+ , ( ) . # \[ \] =]

Visiting any other page of the site would result in the following 404 page:

404 Page

Interesting to note that the image appearing on the page is generated from the endpoint /images/now and appears to contain a timestamp.

One other observation is that the server is running the (Python) waitress framework, which we can see from the server headers:

1Server: waitress

Now that we have a sense for the server and related software, let's solve this challenge!

Analysis

The endpoint now, combined with the contents of the generated image, should be familiar to anyone who is at all familiar with the Python language, as being the default format of a datetime object, and the Python library function datetime.now can be used to return the current timestamp:

1Python 3.8.10 (default, Mar 13 2023, 10:26:41)
2[GCC 9.4.0] on linux
3Type "help", "copyright", "credits" or "license" for more information.
4>>> import datetime
5>>> str(datetime.datetime.now())
6'2023-05-21 12:26:57.831648'

We note that this matches what we see in the generated image. This allows us to conclude that the solution path here is a sort of RCE which will lead to us changing the contents of the image. We can easily verify this by picking some other class-level functions in the datetime class such as utcnow or today, which will generate the same image.

Leading to a Solution

So at this point we know:

  • The image is being generated by Python code probably eval'ed from a string like this: datetime.**last-path-segment**()
  • Certain characters are not allowed in the path segment (we assume this from the list of characters shown on the home page)
  • Our goal is to read the content of flag.txt (this was later provided as a hint although I solved the challenge prior to this hint being available)

Let's see if we can chain a simple method call first, since the injection point ends with parens we know that the last part of our injection has to be a function that takes no parameters. So the following works:

1/images/now().toordinal

This results in an image with the following content 738861. So we've confirmed we can chain function calls as we had hoped, and the contents of the image will reflect the return value of the last function call (toordinal is a function on a datetime object as documented here).

If you don't want further spoilers, you can safely stop here and try to build the exploit chain yourself 😁

Reading a File

Finally, I had to come up with a way to read the content of the flag file and ensure the contents of the file fed into the method chain, since we can't inject carriage returns and other control structures due to the character set limitations. Also, we can't use a typical __globals__ type injection because the _ character is prohibited.

The path I took was to inject a Python lambda function, which allows for arbitrary / simple inline code to be used to process typically an iterator such as an array or string. Typically these are used to perform some sort of processing on the input i.e. a transformation, but in this case we're just using it as a vehicle to inject arbitrary code.

Lambda functions can be used in many Python library functions, but map seemed like a logcal choice. Since map requires an iterable parameter, and we are starting from a datetime object, I decided to figure out how to get a string from the datetime and then pass the string to the map function. I built up the payload like so:

1/images/now().strftime(%22aaa%22).title --> AAA

Remember we still have to end the injection with a parameterless function invocation, there are many on string. strftime on the datetime class was useful because it allows us to provide any arbitrary string as output. We pass this lambda result to strftime to get the string value added into the method chain. We iterate on a dummy array [1] so that the lambda function is executed exactly once:

1/images/now().strftime(str(map(lambda a: a, [1]))).title --> '<Map Object At 0X7Fd1789Fd9A0>'

Oops! From this we can see our basic premise works, but we need to convert the map object (with a single element) to a printable string so we can see the result in the image output, we do this by wrapping it with the str(list(...)) built-in functions:

1/images/now().strftime(str(list(map(lambda a: a, [1])))).title --> '[1]`

Now we simply put an open('flag.txt').read(100) in the lambda and we should have our flag:

1/images/now().strftime(str(list(map(lambda a: open("flag.txt").read(100), [1])))).title

And we see the flag is (partially) revealed!

Partial Flag

Further work was required to see the whole flag, this was made a little more painful because the font used in the image did not clearly indicate uppercase and lowercase letters. We'll leave this as an exercise to the reader, try reproducing this in your local Python CLI and see how you might iterate through the characters 😄

Overall a super fun challenge that required no brute force or guesswork but just putting the pieces together. Thanks DeadSec!