近日,对某cmsV9.9版本进行代码审计,发现了4处漏洞。
这4处漏洞漏洞比较基础,也很经典。从这4处漏洞,可以反应了在程序开发过程中一些容易忽略的问题,下面分享下本次审计过程。
审计之旅
在开发程序时,如果没有正确的过滤单引号(’)、双引号(”)、反斜杠(\)等特殊字符,往往会产生代码/sql注入漏洞。
在针对这些特殊字符,开发者经常使用如下方式进行过滤:
- 使用addslashes进行过滤
- 使用str_replace对单引号等进行替换操作
- 使用is_numeric等方法对数字类型的输入进行判断与过滤
审计此cms时发现,以上3种方式,在此程序的开发过程中,都有使用。但是,不严谨的使用,使得注入漏洞仍然存在
首先分析下此cms,来看下\include\common.php文件
如上图,在common.php文件中require filter.inc.php
在filter.inc.php文件中,存在如下图代码
此cms使用伪全局变量的模式,使用foreach从’_GET’,’_POST’,’_COOKIE’中遍历数组中的值,然后再将获取到的数组键名作为变量,数组中的键值过滤后作为变量的值
跟进_FilterAll
可见,程序在此处使用addslashes对键值进行过滤
这样一来,我们可以通过’_GET’,’_POST’,’_COOKIE’为程序中的任意参数传入值,但是传入的值会被addslashes过滤
例如 http://www.testcms.com/index.php?grq=1’23
程序接收此url后,程序中的grq变量会为:$grq=”1'23”
开发者对这样的处理方式仍然不放心,于是,在\include\common.php文件中require_once(sea_INC.”/filter.inc.php”)进行过滤后,再次从’_GET’,’_POST’,’_COOKIE’中取键值对进行伪全局变量赋值与过滤
跟进_RunMagicQuotes方法
_RunMagicQuotes方法中对键值使用addslashes进行过滤与赋值变量
以上两段代码依次执行,对比下这两处代码
filter.inc.php文件中的过滤与赋值代码
common.php文件中的过滤与赋值代码
filter.inc.php文件中赋值的${$k}被后续common.php文件中的${$k}覆盖了,filter.inc.php文件中的过滤部分代码白写了。
虽说filter.inc.php过滤部分代码在做无用功,但是仍然可以看出开发者对特殊字符的防范意识很高
在了解完该程序之后,接下来漏洞:
1.未对拼接参数使用单引号闭合而导致的sql注入漏洞
位于上图143行处
经过上文对程序伪全局变量赋值方式的分析可知,这里$leftSelect 可由GET方法传递的来。当使用GET传入$leftSelect时,程序会使用addslashes对参数进行过滤
但是如上图143行拼接的sql语句来看,并未对$leftSelect变量使用单引号进行闭合,导致虽然使用addslashes函数进行过滤,但仍然存在sql注入漏洞
构造payload
&leftSelect=1 or updatexml(1,concat(0x7,user(),0x7e),1)
最终执行的sql语句是:
UPDATE sea_data
SET tid
= 1 where tid= 1 or updatexml(1,concat(0x7,user(),0x7e),1)
同样,在另一处文件中admin_tempvideo.php, 也存在相同的漏洞
如上图可见,$ids变量被拼接到sql语句中,并在上图25行被执行
$ids变量由$e_id通过implode方法拼接而来,而$e_id变量可以通过GET方法直接传入,$e_id变量可控,由此造成sql注入漏洞
构造的payload如下
[&e_id0]=1)%20or%20extractvalue(1,concat(0x7e,(SELECT%20CONCAT_WS(0x23,name,%20password)%20FROM%20sea_admin%20limit%200,1)))–%20&type=1 or extractvalue(1,concat(0x7e,(SELECT CONCAT_WS(0x23,name, password) FROM sea_admin limit 0,1)))– &type=1)
2.对键值进行过滤,忘记对键名进行处理
漏洞文件: admin_config.php
如上图,看到将$configstr变量写入文件中去
查看下写入文件的具体位置
该位置固定,即为/data/config.cache.inc.php
config.cache.inc.php
跟踪下$configstr变量
$configstr变量由$k与$v拼接而来,
在下图红框中所示,$$k的值经过str_replace方法过滤
在程序入口处,通过伪全局变量的方式,其实已经对$_POST中的$k进行变量赋值,所以$$k的值即为通过POST传入的变量的经过过滤的键值
例如POST中
&edit___grq=te’st
那么,此时的$k=“&edit___grq”,$&edit___grq=”te\’st”,$$k=” te\’st”
值得注意的是,上图中仅仅对$$k 进行str_replace处理,而$k并没有经过任何过滤,直接拼接到$configstr变量中,也就是说,可以通过POST提交的KEY值传递构造好的payload,该payload将会被写入文件中去,造成远程代码执行
回头看一下config.cache.inc.php文件
开发者在写过滤代码时,考虑到程序在处理配置文件经常会出现漏洞:即配置文件中变量值注入的问题
在以往的此类漏洞中,往往是未对配置文件中变量值进行合理的过滤,导致单引号等特殊字符被写入值部分,从而构造闭合结果导致注入的产生。
所以,开发者吸取了以往的经验,对写入配置文件中的值部分进行str_replace处理,对”’”与”\”进行转义。但是,由于这里的变量名同样可控,而且未对变量名进行任何过滤,因此,str_replace处理形同虚设
使用如下payload,直接注入
&edit___a;phpinfo();//=1
3.经过严格的过滤,报错日志文件中却存在利用点
漏洞触发点位于\comment\api\index.php
由于seacms采用伪全局变量的形式,$page $id等变量可以从GET请求参数中传递进来
程序使用is_numeric对$page进行限制,使得$page必须是数字
上图最下面一行,$h = ReadData($id,$page);
可见将GET请求传入的$id,$page 传递进ReadDate方法中
跟入ReadDate方法
可见GET请求传入的$id,$page 传递进Readmlist方法中
跟进Readmlist方法
在Readmlist方法中的88行,可见存在一处sql语句
可以看到,该sql语句中拼接了$page变量,而$page变量是由GET请求接收而来,可控
当我们通过GET请求传递一个负数值的page时(例如-1),此时执行的sql语句为
SELECT id,uid,username,dtime,reply,msg,agree,anti,pic,vote,ischeck FROM sea_comment WHERE m_type=1 AND v_id=666 ORDER BY id DESC limit -20,10
该sql语句会导致sql语法错误而报错,如下图
到此为止,程序仍然是安全的,并不存在sql注入或是代码执行漏洞。但是此cms对报错日志的处理方式,却很有意思:
当sql语法出现错误时,程序会将报错日志会被写入\data\mysqli_error_trace.php
跟进mysqli_error_trace.php文件,错误日志的格式如上图:
错误日志会被包裹
第一行(第一个红框)为触发错误的url
第二行(第二个红框)为sql语法错误信息
因此,当构造payload如下时
comment/api/index.php?page=-1&gid=666&payload=/phpinfo();/
执行成功后,可见成功写入mysqli_error_trace.php,如下图:
访问/data/mysqli_error_trace.php
即可执行phpinfo
结束语
从本次代码审计的结果来看,程序在使用addslashes等过滤方式对输入进行过滤后,仍然产生了两处代码执行,两处sql注入。因此使用addslashes等过滤方式在程序入口处对输入进行过滤并不能代表程序固若金汤。配合程序自身的逻辑,使用恰当的方式进行参数过滤才是最优解。