Introduction
OK Jail was a Python jail challenge at BearcatCTF 2025. The solution involved numerical manipulation with a limited character set, as well as abusing Python builtin and integer functions.
Reviewing the Source Code
This is the source code for the OK Jail challenge, provided during the CTF:
1 | import json |
Let’s break this down step by step.
Our first call in the sequence is checkin()
to populate the ok
variable.
Looking at this function, we can see that it first checks if all characters in the string are in the set ~(0)|<
. Wow, that is a very limited character set. Then, it evaluates the code and make sure it runs - if so, its output becomes the ok
variable.
Then comes the name
, which is simply just parsed JSON. inmate
can be any function on the ok
object’s type, and cell
is any builtin function.
After gathering all that information, our jail function is called: builtins.{cell}(ok.{inmate}(*{name}))
. Let’s break this down too:
*{name}
- Expands thename
object into a function’s arguments.ok.{inmate}(*{name})
- Calls the function name stored ininmate
on theok
object with the expanded value ofname
as the arguments. If you recall correctly,inmate
is the function name we specified earlier.builtins.{cell}(...)
- Finally, we call our specified builtin.
Approaching Exploitation
If we look at Python’s list of built-in functions, we can see eval
is one of them. This means if we can get an arbitrary string from a function call on ok
, we can automatically win it.
First, we need to look in a bit more detail at what ok
actually is, though. We know that we have 6 characters we can work with, and those are ~(0)|<
. Let’s take a closer look at what we can do here:
- Left shifts, with
<<
- Less-than comparisons, with
<
- Inversions, with
~
- Bitwise OR, with
|
- Of course, the number 0
Hmm, doesn’t look like we can really get anything other than an int
out of this. Wait, don’t give up yet. Let’s take a quick look at the standard int functions…
Aha! Turns out, the int
class has a to_bytes function, that turns the integer into a byte string! This might be exactly what we need. I tested real quick to make sure eval
could accept a bytes argument:
1 | >>> eval(b'1') |
This means, if we can get a number representing some sort of Python code, we can win it!
The Number Conundrum
But, wait. With such a limited character set, how are we going to get any number besides zero? Let’s take a look at our operations. Inversions, bitwise OR, comparisons, left shifts…
Considering we have left shifts and bitwise OR, if we can somehow get a number 1 of any kind, it all falls together: 1<<1
is 2, 1<<1<<1
is 4, and 1|1<<1|1<<1<<1
is 7! We can simply construct the number bit by bit until we get the full number.
But the problem is, how do we get a 1? Let’s take a look at our other operations. We see we have an invert operation, but the only thing to run it on is 0
, so let’s try that:
1 | >>> ~0 |
We have -1
. Now, what can we do with this? We can’t really do anything with -1. Unless…
Since <
is in our character set, we can do anything with a <
. That means left shift, but that also means a less-than operation. We know -1 is less than 0, so:
1 | >>> ~0<0 |
That’s even better! But, again, what in the world are we going to do with it? True is equivalent to 1, but it’s not like we can shift a boolean, right?
1 | >>> (~0<0)<<0 |
Oh. And this is where the security of this application all falls apart. We can just pass in a number that evaluates to some sort of payload, and then call to_bytes
on that number, and then call eval
on that bytes object, and now we have code execution.
Note: (~0<0)|0
also works and is a character shorter, if you’re itching to save some bytes.
Generating the Payload
We, ideally, want a short payload. Every byte has the potential to increase our payload by several kilobytes.
The shortest payload I could find was the 13-character eval(input())
, which simply just runs whatever we type in.
Now, we have to generate the payload. First, we wanna get our payload into a numeric form:
1 | >>> import binascii |
There we go!
My conversion code looked something like:
1 | # eval(input()) |
Let’s explain this mess. First, we convert the number to a binary string to make it a bit easier to manipulate individual bits. Then, we create our array of bit calculations. We also go ahead and return 0 if our number is 0.
Then, for each bit set to 1, we shift (~0<0)<<0
(1) left as many times as we need to get it in the correct place. We don’t need to do anything with zero bits. Then, we join it all with a bitwise OR.
This code generates the integer we need to convert into bytes. Then, it’s as simple as passing to_bytes
as our inmate, eval
as our cell, and the arguments to to_bytes
: [13, "big"]
(length 13, in big-endian) as the name. Then, we get to run whatever Python code we want.
Here’s a local run (since the servers are now offline):
1 | Welcome to the OK Jail! |
Conclusion
Sometimes, even with code restrictions, it is possible to “jailbreak” from these restrictions and execute arbitrary code. The OK Jail challenge emphasizes this because, even though we only get to work with a limited function set and character set, we are still able to abuse the functionalities of the Python programming language to break free from the restrictions and execute our own code.