Jailpy3 (LIT 2025)
I usually don't solve rev challenges (games are an exception) but I have no idea why a python jail was also put into the rev category...
Made with a burning passion for pyjails (i.e. creating insane payloads just to bypass some random condition), I turned everything in this python script into pyjail tech! Here's a program that's suppose to print a flag. But something seems to be getting in the way...
We were provided with a 11.5MB python file... calling this a jail challenge would be wrong instead it is just obfuscated python code.
# The starting of obfuscated python file provided
print({}.__class__.__subclasses__()[2].copy.__builtins__[{}.__class__.__subclasses__()[2].copy.__builtins__[chr(1^2^32^64)+chr(8^32^64)+chr(2^16^32^64)]({}.__class__.__subclasses__()[2].copy.__builtins__[chr(1^2^4^8^16^64)+chr(1^2^4^8^16^64)+chr(1^8^32^64)+chr(1^4^8^32^64)+chr(16^32^64)+chr(1^2^4^8^32^64)+chr(2^16^32^64)+chr(4^16^32^64)+chr(1^2^4^8^16^64)+chr(1^2^4^8^16^64)](chr(1^2^16^32^64)+chr(1^4^16^32^64)+chr(2^32^64)+chr(16^32^64)+chr(2^16^32^64)+chr(1^2^4^8^32^64)+chr(1^2^32^64)+chr(1^4^32^64)+chr(1^2^16^32^64)+chr(1^2^16^32^64)).select.POLLIN^{}.__class__.__subclasses__()[2].copy.__builtins__[chr(1^2^4^8^16^64)+chr(1^2^4^8^16^64)+chr(1^8^32^64)+chr(1^4^8^32^64)+chr(16^32^64)+chr(1^2^4^8^32^64)+chr(2^16^32^64)+chr(4^16^32^64)+chr(1^2^4^8^16^64)+chr(1^2^4^8^16^64)](chr(1^2^16^32^64)+chr(1^4^16^32^64)+chr(2^32^64)+chr(16^32^64)+chr(2^16^32^64)+chr(1^2^4^8^32^64)+chr(1^2^32^64)+chr(1^4^32^64)+chr(1^2^16^32^64)+chr(1^2^16^32^64)).select.POLLPRI^{}...Seeing this code the first thing that we should do is replace chr(...) with the actual contents. The ^ operator represents a XOR operation (it is not exponent). So I asked ChatGPT to quickly write a script for me which performs these operations and hopefuly deobfuscates this.
import ast
import operator
import re
import sys
from pathlib import Path
ALLOWED_BINOPS = {
ast.BitXor: operator.xor,
ast.BitOr: operator.or_,
ast.BitAnd: operator.and_,
ast.LShift: operator.lshift,
ast.RShift: operator.rshift,
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.FloorDiv: operator.floordiv,
ast.Mod: operator.mod,
}
ALLOWED_UNARY = {ast.Invert: operator.invert, ast.UAdd: operator.pos, ast.USub: operator.neg}
def safe_eval_int(expr: str) -> int:
"""Evaluate a small integer expression safely."""
node = ast.parse(expr, mode="eval")
def _eval(n):
if isinstance(n, ast.Expression):
return _eval(n.body)
if isinstance(n, ast.Constant) and isinstance(n.value, int):
return n.value
if isinstance(n, ast.UnaryOp) and type(n.op) in ALLOWED_UNARY:
return ALLOWED_UNARY[type(n.op)](_eval(n.operand))
if isinstance(n, ast.BinOp) and type(n.op) in ALLOWED_BINOPS:
return ALLOWED_BINOPS[type(n.op)](_eval(n.left), _eval(n.right))
# Allow nested parentheses via Tuple with one elt in some Python versions? Not needed here.
raise ValueError(f"disallowed expression: {ast.dump(n, include_attributes=False)}")
val = _eval(node)
if not isinstance(val, int):
raise ValueError("non-integer expression")
return val
def decode_chr_sequence_to_str(exprs):
chars = []
for e in exprs:
code = safe_eval_int(e)
try:
chars.append(chr(code))
except ValueError:
raise ValueError(f"chr() out of range: {code}")
return "".join(chars)
def scan_and_replace_chr_concats(src: str):
"""
Find spans like: chr(<e1>) + chr(<e2>) + ... and replace with a quoted string.
Handles whitespace and newlines.
"""
i, n = 0, len(src)
replacements = []
while i < n:
if src.startswith("chr(", i):
start = i
exprs = []
def read_paren(idx):
# idx at start of '('
depth, j = 1, idx + 1
while j < n and depth:
c = src[j]
if c == "(":
depth += 1
elif c == ")":
depth -= 1
j += 1
if depth != 0:
raise ValueError("unbalanced parentheses in chr(...) sequence")
return j # position after ')'
# first chr(
if src.startswith("chr(", i):
# read first expr
open_idx = i + 3 # at '('
end_after_paren = read_paren(open_idx)
exprs.append(src[open_idx + 1 : end_after_paren - 1])
k = end_after_paren
# consume sequences of + chr(
while True:
# skip whitespace
while k < n and src[k].isspace():
k += 1
if k < n and src[k] == "+":
k += 1
while k < n and src[k].isspace():
k += 1
if k < n and src.startswith("chr(", k):
open2 = k + 3
end2 = read_paren(open2)
exprs.append(src[open2 + 1 : end2 - 1])
k = end2
continue
break
if len(exprs) >= 2: # only replace concatenations, not single chr(...)
try:
decoded = decode_chr_sequence_to_str(exprs)
replacements.append((start, k, repr(decoded)))
i = k
continue
except Exception:
# fall through and advance one char if evaluation fails
pass
i += 1
# Apply replacements from end to start.
if not replacements:
return src, 0
out = []
last = 0
for s, e, rep in sorted(replacements, key=lambda t: t[0]):
out.append(src[last:s])
out.append(rep)
last = e
out.append(src[last:])
new_src = "".join(out)
return new_src, len(replacements)
def deobfuscate_text(src: str, max_passes: int = 8):
"""Run multiple passes to catch cascaded patterns."""
total = 0
for _ in range(max_passes):
src, cnt = scan_and_replace_chr_concats(src)
total += cnt
if cnt == 0:
break
return src, total
def main():
if len(sys.argv) < 2:
print("usage: python deobfuscate_chr_concat.py <input.py> [output.py]")
sys.exit(2)
inp = Path(sys.argv[1])
outp = Path(sys.argv[2]) if len(sys.argv) >= 3 else None
text = inp.read_text(encoding="utf-8")
new_text, num = deobfuscate_text(text)
if outp:
outp.write_text(new_text, encoding="utf-8")
print(f"replaced {num} chr-concat sequences -> {outp}")
else:
print(new_text)
if __name__ == "__main__":
main()
After running this script the output looked like (note that it has been truncated... it was still 2.9MB):
Now we can notice that there is use of long unnecessary paths to call Python builtin functions and imports. For example the following snippet:
Is equivalent to just using chr(97) , both would return the sample output of a . So now once again I gave these instructions to GPT and it produced a functional script.
Finally, after executing this deobfuscation script we got our flag:
The challenge did look hard at start, probably I got scared because of the large file size, but by making two simple observations and using any LLM to write the scripts the challenge was easy solvable.