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:
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!
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!