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 providedprint({}.__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 astimport operatorimport reimport sysfrom pathlib import PathALLOWED_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}defsafe_eval_int(expr:str)->int:"""Evaluate a small integer expression safely.""" node = ast.parse(expr,mode="eval")def_eval(n):ifisinstance(n, ast.Expression):return_eval(n.body)ifisinstance(n, ast.Constant)andisinstance(n.value,int):return n.valueifisinstance(n, ast.UnaryOp)andtype(n.op)inALLOWED_UNARY:returnALLOWED_UNARY[type(n.op)](_eval(n.operand))ifisinstance(n, ast.BinOp)andtype(n.op)inALLOWED_BINOPS:returnALLOWED_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.raiseValueError(f"disallowed expression: {ast.dump(n,include_attributes=False)}") val =_eval(node)ifnotisinstance(val,int):raiseValueError("non-integer expression")return valdefdecode_chr_sequence_to_str(exprs): chars =[]for e in exprs: code =safe_eval_int(e)try: chars.append(chr(code))exceptValueError:raiseValueError(f"chr() out of range: {code}")return"".join(chars)defscan_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 =[]defread_paren(idx):# idx at start of '(' depth, j =1, idx +1while j < n and depth: c = src[j]if c =="(": depth +=1elif c ==")": depth -=1 j +=1if depth !=0:raiseValueError("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(whileTrue:# skip whitespacewhile k < n and src[k].isspace(): k +=1if k < n and src[k]=="+": k +=1while k < n and src[k].isspace(): k +=1if k < n and src.startswith("chr(", k): open2 = k +3 end2 =read_paren(open2) exprs.append(src[open2 +1: end2 -1]) k = end2continuebreakiflen(exprs)>=2:# only replace concatenations, not single chr(...)try: decoded =decode_chr_sequence_to_str(exprs) replacements.append((start, k, repr(decoded))) i = kcontinueexceptException:# fall through and advance one char if evaluation failspass i +=1# Apply replacements from end to start.ifnot replacements:return src,0 out =[] last =0for s, e, rep insorted(replacements,key=lambdat: 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)defdeobfuscate_text(src:str,max_passes:int=8):"""Run multiple passes to catch cascaded patterns.""" total =0for _ inrange(max_passes): src, cnt =scan_and_replace_chr_concats(src) total += cntif cnt ==0:breakreturn src, totaldefmain():iflen(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])iflen(sys.argv)>=3elseNone 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.
import ast, operator, re, sys
from pathlib import Path
import subprocess as _subprocess # for select.POLL* constants
# ---------- safe integer evaluator ----------
_BINOPS = {
ast.BitXor: operator.xor, ast.BitOr: operator.or_, ast.BitAnd: operator.and_,
ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
ast.LShift: operator.lshift, ast.RShift: operator.rshift,
ast.FloorDiv: operator.floordiv, ast.Mod: operator.mod,
}
_UNARY = {ast.Invert: operator.invert, ast.UAdd: operator.pos, ast.USub: operator.neg}
# expose only allowed names (after normalization we keep POLL* names)
_ALLOWED_NAMES = {name: getattr(_subprocess.select, name)
for name in dir(_subprocess.select) if name.startswith("POLL")}
def _safe_eval_int(expr: str) -> int:
node = ast.parse(expr, mode="eval")
def ev(n):
if isinstance(n, ast.Expression): return ev(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 _UNARY: return _UNARY[type(n.op)](ev(n.operand))
if isinstance(n, ast.BinOp) and type(n.op) in _BINOPS: return _BINOPS[type(n.op)](ev(n.left), ev(n.right))
if isinstance(n, ast.Name) and n.id in _ALLOWED_NAMES: return _ALLOWED_NAMES[n.id]
raise ValueError(f"disallowed expression: {ast.dump(n, include_attributes=False)}")
v = ev(node)
if not isinstance(v, int): raise ValueError("non-integer")
return v
# ---------- decoding helpers ----------
def _decode_chr_concat(src: str):
"""Replace chr(expr)+chr(expr)+... with quoted string."""
i, n = 0, len(src); reps = []
def read_paren(j):
depth, k = 1, j+1
while k < n and depth:
c = src[k]
if c == "(": depth += 1
elif c == ")": depth -= 1
k += 1
if depth: raise ValueError("unbalanced parens")
return k
while i < n:
if src.startswith("chr(", i):
start = i
aopen = i + 3
aend = read_paren(aopen)
exprs = [src[aopen+1:aend-1]]
k = aend
while True:
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 src.startswith("chr(", k):
bopen = k + 3
bend = read_paren(bopen)
exprs.append(src[bopen+1:bend-1])
k = bend
continue
break
if len(exprs) >= 2:
try:
s = "".join(chr(_safe_eval_int(e)) for e in exprs)
reps.append((start, k, repr(s)))
i = k; continue
except Exception:
pass
i += 1
if not reps: return src, 0
out, last = [], 0
for s, e, rep in reps:
out.append(src[last:s]); out.append(rep); last = e
out.append(src[last:])
return "".join(out), len(reps)
def _decode_single_chr(src: str):
"""Replace chr(<int-expr>) with a quoted one-character string where safe."""
# Avoid touching when followed/preceded by + which _decode_chr_concat handles.
pattern = re.compile(r"chr\(\s*([^\(\)]*?)\s*\)")
def repl(m):
expr = m.group(1)
# skip if contains quotes or disallowed chars to be safe
if any(c in expr for c in "'\"[]{}"): return m.group(0)
try:
ch = chr(_safe_eval_int(expr))
return repr(ch)
except Exception:
return m.group(0)
new = pattern.sub(repl, src)
changed = int(new != src)
return new, changed
# ---------- normalization passes ----------
_BUILTINS_JAIL = re.compile(
r"\{\}\.__class__\.__subclasses__\(\)\[\d+\]\.copy\.__builtins__"
)
def _normalize(src: str):
s = src
# 1) Collapse jail path to __builtins__
s = _BUILTINS_JAIL.sub("__builtins__", s)
# 2) Replace __builtins__['chr']( … ) -> chr( … )
s = re.sub(r"__builtins__\s*\[\s*['\"]chr['\"]\s*\]\s*\(", "chr(", s)
# 3) Replace __builtins__['__import__']('subprocess') -> subprocess
s = re.sub(
r"__builtins__\s*\[\s*['\"]__import__['\"]\s*\]\s*\(\s*['\"]subprocess['\"]\s*\)",
"subprocess", s)
# 4) Replace subprocess.select.POLLXXX -> POLLXXX
s = re.sub(r"subprocess\s*\.\s*select\s*\.\s*(POLL[A-Z_]+)", r"\1", s)
return s
def simplify_text(src: str, max_passes: int = 10):
s = _normalize(src)
total = 0
for _ in range(max_passes):
s2, n1 = _decode_chr_concat(s)
s3, n2 = _decode_single_chr(s2)
s4 = _normalize(s3) # new opportunities may appear
total += n1 + n2
if s4 == s:
break
s = s4
return s, total
def main():
if len(sys.argv) < 2:
print("usage: python simplify_ctf_obfuscation.py <input.py> [output.py]")
sys.exit(2)
inp = Path(sys.argv[1]); text = inp.read_text(encoding="utf-8")
out, n = simplify_text(text)
if len(sys.argv) >= 3:
Path(sys.argv[2]).write_text(out, encoding="utf-8")
print(f"simplified with {n} chr-rewrites -> {sys.argv[2]}")
else:
print(out)
if __name__ == "__main__":
main()