CVE-2023-37271: RestrictedPython Remote Code Execution

UIUCTF 2023 had a challenge called Rattler Read, in which the goal was to exploit a Python sandbox, which ran user-provided code under RestrictedPython.

RestrictedPython is a library which can be used to create safer subsets of Python, in order to execute untrusted user code within a larger process. For example, it can be used to provide a limited set of Python builtins (removing e.g. __import__), restrict attribute access via . and getattr/setattr to eliminate dangerous attributes (such as banning any attribute starting with an underscore), and block certain language features. RestrictedPython is used in several applications, such as Zope (to provide safe evaluation of templates) and Redash (to execute queries written in Python). Without RestrictedPython, untrusted code uploaded to these platforms could execute arbitrary code with the permissions of the server process.

In Rattler Read, the problem uses an older version of RestrictedPython (5.0), and the intended solution seems to have been to exploit a since-patched bug (CVE-2021-32807) which exposes string.Formatter to untrusted code. See an example exploit here:

However, Laurence Lee and I found a different approach, which turned out to be a previously-unknown zero-day vulnerability in RestrictedPython (!). We responsibly disclosed our bug, which was tagged CVE-2023-37271 and given a CVSS score of 9.9/10 (Critical), as it allows for remote code execution in certain environments, such as Zope and Redash.

The bug is that RestrictedPython does not adequately restrict access to the gi_frame attribute of generator objects. This attribute provides access to a generator’s stack frame, which includes the local variables and builtin objects for that frame, as well as a reference to the parent stack, all with attributes that do not start with an underscore (and are thereby allowed as attribute names by RestrictedPython).

While a generator is running, it can access its own stack frame (provided there’s a reference to the generator), then use the f_back attribute to walk the stack backwards beyond the RestrictedPython invocation boundary (i.e. to get the stack frame of the function which called RestrictedPython). From there, you can access the frame’s builtins to obtain functions such as __import__, leading to remote code execution.

The exploit looks like this:

g = (g.gi_frame.f_back for x in [1])
[x for x in g][0].f_back.f_back.f_builtins["__import__"]("os").system("cat /flag.txt")

The bug has now been fixed, so if you depend on RestrictedPython to execute sandboxed Python, be sure to upgrade!

Previous Article
Next Article