Google CTF 2022
My writeups for Google CTF 2022.
A couple days ago, a few friends and I competed in the Google CTF. It was our first time doing a CTF, so we weren’t sure how well we were going to do. Although we were only able to find four flags, I had a great time; there’s something unrivaled about the feeling of finally cracking a puzzle you’ve been working on for hours.
We ended the competition in 76th place, putting us in the top 20% of teams that found at least one flag. All things considered, I’m very happy with that result and I’m excited to play more CTFs in the future.
Treebox is a simple sandbox escape challenge in Python. The flag is in the
flag file in the working directory, and we had to escape the sandbox to read it.
The attacker can execute arbitrary Python programs, but only if they meet a condition checked by
verify_secure, which just traverses the AST and blocks execution if the program contains any
For example, the following code would not be allowed because it contains
So we needed a way to write function calls which don’t look like function calls from the parser’s perspective.
There are lots of ways to do this, but the first one we thought of is to use operator overloading: in Python, objects can have special methods which override special language behavior, including arithmetic operations. For example, the
__div__ methods are called when you do arithmetic with an object.
So we could define a class like this:
class Functions: __add__ = open funs = Functions() fun + "flag" # returns a file descriptor
But we still needed an instance of the class to call the methods. This is where the second interesting trick comes in:
raise statements can instantiate classes. If you call
raise MyException, where
MyException is a class, it will instantiate that class with no arguments. But in the AST, this is a
Raise node, not a
Call node like regular class instantiation.
class Functions: __add__ = print __sub__ = iter __mul__ = open try: raise Functions except Functions as funs: # equivalent to `for block in iter(open("flag")):` for block in funs - (funs * "flag"): funs + block # equivalent to `print(block)`
And this successfully grabs the flag.
By the way, the iterator is just a way to read the file using only builtin functions. We couldn’t call
f.read() using operator overloading because
read is a method on the file descriptor.
In Legit, the attacker has access to a CLI utility which downloads Git repositories and lists and displays files from them. Since the attacker controls the argument passed to
git clone, they can completely control the contents of the Git repository.
The object of the challenge is to trick the CLI into reading a file outside of the Git repository you downloaded.
Before actually printing the file, it normalizes and resolves the path to make sure it’s actually inside the Git repository:
def show_file(): filepath = input(">>> Path of the file to display: ") real_filepath = os.path.realpath(os.path.join(_REPO_DIR, filepath)) if _REPO_DIR != os.path.commonpath((_REPO_DIR, real_filepath)): print("Hacker detected!") return result = subprocess.run(["cat", real_filepath], capture_output=True) print(result.stdout.decode())
So just making a symlink pointing to the flag in the Git repository doesn’t work, because
show_file would resolve it with
os.path.realpath before checking if the file is in the repository.
Also, the fact that it uses
cat to read the file made us very suspicious that symlinks are involved, since
cat will resolve them a second time. We decided to look at the documentation for
os.path.realpath to see if they have any unintuitive behavior.
And, as it turns out, they do! By default,
os.path.realpath tries to resolve symlinks, but if resolution fails, it gives up and stops resolving symlinks in the rest of the path. There’s a
strict mode which raises the error up to the caller, but it’s not used here.
Symlink resolution fails if a cycle is detected, so we created a repository with the following symlinks:
cycle => ./cycle flag => /flag
Then, we told the CLI to read the file at the path
./cycle/../flag. While normalizing the path, it tried to resolve the
/cycle segment, but failed and gave up. Then, it saw the
/.. and removed the previous segment,
/cycle. Finally, it added on
/flag to get the path
REPO_DIR/flag. That got passed to
cat, which resolved the symlink and read the flag.
If that explanation doesn’t satisfy you, you can read the source code of CPython’s
_joinrealpath, which is the meat of
Engraver is a challenge in the hardware category involving a robot arm holding a laser pointer. The attachment contains three things:
- A picture of a robot arm holding a laser pointer.
- A long-exposure shot of the robot arm drawing letters with the laser pointer.
- A dump of USB traffic to the arm containing movements which draw the flag.
Some research on the bot shown in the picture yielded1 a GitHub repository with a reference for the protocol used by the arm’s servos. The protocol appeared to match HID data in packets sent to the arm, so we exported the relevant packets to JSON from Wireshark and wrote a simple parser to get the data. We put it in a Google Sheet and graphed it to see if the data looked reasonable. Three servos were moving, two of them in about the same way. By guess-and-check, we figured out that the two similar servos were controlling the X and Y axes, and the third servo was switching the laser on and off.
At that point, we had all the data we needed to find the flag, but it wasn’t really possible to read it in that format. We wrote a program in Rust to generate an animated GIF of the lines the arm would draw.
It required a bit of frame-by-frame analysis to read what the arm was writing, but we did figure it out eventually.
Annoyingly, we also found a GitHub repository which used a different kind of servo with a different, but similar, control protocol. That ended up wasting us a couple hours of time. ↩