YXcms漏洞分析

YXcms安装

在安装该cms之前,同样也是要为其创建数据库

1658411088830

继续常规操作在phpstudy中添加站点,最后我们访问yxcms.com即可进入安装界面,在数据安装界面的配置信息如下

1658411242092

最后安装完成主页面如下

1658411991467

接着我们访问后台页面,http://yxcms.com/index.php?r=admin,账号密码/admin:123456,我们就进入了后台

1658412040643

路由

拿到这个站点之后,我们就需要熟悉这个站点的路由方式

http://yxcms.com/index.php?r=default/column/index&col=demoshow

1658412238050

我们在其主控制器中编写如下测试代码

1658412638041

根据其之前的路由方式,我们这里访问http://yxcms.com/index.php?r=default/index/test1,输出我们的测试内容

1658412747341

参数传递

修改我们的test代码,使用$_GET函数接收参数,但是我们使用常规的参数传递方式显示方法不存在

1658482696540

这个框架中把参数传递的符号修改为了&

1658482777379

如果我们传递参数的是否传入了特殊字符,使用$_GET是没有过滤的

1658482908574

我们在使用$_GET这种原始的方法传递参数,然后使用框架中的in()函数即可进行过滤

1658483074957

我们简单分析一下in函数

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);//这里又遇到了和之前相同的问题,没有过滤key
        }
        return $data;
    } else {
        return $data;
    }    
}

这里我们还是发现了和之前一样的问题,对数组进行了处理,但是没有对key值进行过滤,我们可以看到如下的测试结果

1658483301818

增删改查

查询数据

在进行增删改查之前我们需要新建一个测试的数据库表yx_user并写入如下数据

1658483913129

接着我们在控制器中添加测试代码,但是我们在页面访问该功能点的时候提示userModel模型类不存在

1658891607890

原因是我们在操作的时候是对User数据库进行操作的,但是model文件夹中并没有对应的文件

1658892199246

这里我们就根据其他文件的格式针对user表创建一个model文件

1658892499214

我们再次访问该功能点,成功查询出数据

1658892530214

插入数据

我们再编写插入数据的代码进行测试,这里显示的乱码但是数据成功插入

1658904018516

1658904444780

修改数据

1658904983624

删除数据

1658905053912

内核分析

查询数据内核分析

首先我们对测试代码的查询部分下断点

1658906786786

model层都是一些复制操作,我们这里直接分析find函数,这里我们可以看到,他一次进入了table、field、where、order、find

1658907053438

首先我们进入table,这里进行了赋值操作,并根据情况添加表前缀yx

1658907547066

table走完之后进入了find,find是查询一条,所以限制了limit=1,然后还是调用了select语句

1658907923199

继续走到select语句中,获得表以及字段,然后进入了一个条件函数,这里我们跟进

1658908102344

继续F7跟进

1658908184263

这里我们可以看到这里的代码主要是对condition进行拼接,而且没有进行过滤

    //解析查询条件
    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;
    }

接着回到_parseCondition进行了赋值操作,然后返回了conditon,最终又回到了select函数中,我们继续跟进query

1658909050183

query具体操作

 public function query($sql, $params = array(), $is_query = false) {
        if ( empty($sql) ) return false;
        $sql = str_replace('{pre}', $this->pre, $sql);    //表前缀替换
        $this->sql = $sql;//sql语句赋值
        //判断当前的sql是否是查询语句
        if ( $is_query || stripos(trim($sql), 'select') === 0 ) {
            $data = $this->_readCache();
            if ( !empty($data) ) return $data;

            $query = $this->db->query($this->sql, $params);    //这里的query跟进如下    
            while($row = $this->db->fetchArray($query)) {
                $data[] = $row;
            }
            $this->_writeCache($data);
            return $data;                
        } else {
            return $this->db->execute($this->sql, $params); //不是查询条件,直接执行
        }
    }

在之前的query的操作中,又进行了db->query()操作,这里跟进结果如下,在这个操作中没有进行过滤直接进行了查询操作

1658913126603

插入数据内核分析

在测试代码中下断点F7跟进,首先我们会进入model层,这里我们之前已经见到,直接F8,

1658913837715

model层结束后,我们进入了insert函数,依次执行了table、data、insert我们F7跟进

1658913958312

首先我们会进入table函数,这里我们之前在分析查询的时候也已经遇到过了,过了table函数后我们会进行insert函数,这里我们进入_parseData进行分析

1658914336083

_parseData中有调用了parseData,我们继续F7跟进

1658914709110

在parseData中判断了数据是否为数组,根据type进行不同的操作,这里还调用了escape函数,我们跟进分析一下

//解析待添加或修改的数据
    public function parseData($options, $type) {//options中包含我们传入的数据,type是add
        //如果数据是字符串,直接返回
        if(is_string($options['data'])) {
            return $options['data'];
        }
        if( is_array($options) && !empty($options) ) {//判断是否为数组
            switch($type){
                case 'add':
                        $data = array();
                        $data['fields'] = array_keys($options['data']);
                        $data['values'] = $this->escape( array_values($options['data']) );//这里调用了escape函数
                        return " (`" . implode("`,`", $data['fields']) . "`) VALUES (" . implode(",", $data['values']) . ") ";
                case 'save':
                        $data = array();
                        foreach($options['data'] as $key => $value) {
                                $data[] = " `$key` = " . $this->escape($value);
                        }
                        return implode(',', $data);
            default:return false;
            }
        }
        return false;
    }

escape函数中有一处过滤,过滤了数组中的value值,我们一直F8,最后返回到了_parseData中,这时$data是拼凑过的sql语句

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 语句中使用的字符串中的特殊字符
        }
    }

继续走我们会回到insert函数中,在insert的后续操作中,首先进行了sql语句的拼接,完整的sql语句最后存储到了$this->sql中,接下来的代码就是执行sql语句,最后完成sql插入的操作,其中在execute执行了sql语句,依旧没有进行过滤

1658915897217

总结

如果我们传入数据的时候是数组类型的,其会过滤value值但是不会过滤key值,如果使用了in函数,也会对key值进行过滤。

数字型注入

首先我们编写测试代码,具体内容如下,关键点就是在使用find函数的时候,没有对id加单引号

1658924735986

首先我们进行正常请求

1658924939644

接着我们添加上and关键字,这里可以看到,其接收到id的内容为1 and 0,数据库也没有查询结果

1658925013338

修改请求语句为id=1 and 3,返回的结果为id=1的请求结果,说明存在数字型注入

1658925080615

原因就是in函数过滤的时候仅仅过滤了字符型没有过滤数字型

insert注入

首先还是编写测试代码

1658926668472

进行正常请求的测试

1658926757991

接着我们在请求数据中修改某个数据的键名,给其添加特殊字符,我们可以看到直接报错了

1658926917201

这里我们构造一个报错注入

data[username`,`password`)%20VALUES%20(%27xxx%27,1%20and%20updatexml(1,concat(0x3a,user()),1)%20);%23%23%23----)%20]=aaa&data[password]=999

1658927388136

我们就针对这个注入进行下断分析

1658927505799

首先进入in中,我们可以根据之前的代码分析知道in中对value值进行了过滤没有对key值进行过滤

1658927605380

接着进入insert->_parseData->parseData中仅仅对value进行了过滤,最终导致注入成功

任意文件删除漏洞

对应的文件为photoController.php,在protectedappsadmincontroller下。第355行的delpic()方法

1658928119885

这里方便测试我们在代码中输出路径信息

echo $path.$picname;
exit();

当我们传输如下post数据的时候,我们路径信息如下,可以完成目录穿越

1658928671233

接着我们删除exit();,并在C盘根目录下创建一个测试文件test.txt,当我们执行再次执行该命令后,C盘下文件就已经被删除

1658928844617

根本原因就是没有对我们传输的数据进行过滤和判断,利用这个漏洞删除如下文件会导致站点重装

1658928991064

picname=../../protected/apps/install/install.lock

任意文件写入漏洞

对应的文件为setController.php,在protectedappsadmincontroller下。tpadd方法在140行
其代码内容如下,经过简单的分析得知,我们可以写任意内容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();
           
       }
    }

首先我们添加部分测试用代码

echo $filepath;
exit();

1658929515299

接着在前台访问页面

1658929915230

我们输入文件名编写文件内容,创建并抓包,我们可以看到返回的文件路径,我们可以知道1是其创建的文件夹名称

1658930263396

通过返回的文件路径,且在view文件夹下包含两个文件夹default、mobile,我们在default目录下写一个phpinfo文件,如果我们不使用default,写入文件就会失败,因为他不会创建文件夹

1658931187125

1658931033502

访问我们写入的php文件

1658931294070

XSS漏洞

我们在留言本提交如下内容

1658931598188

在后台中我们可以看到已经被过滤了

1658931519543

但是我们使用如下代码进行测试

1658931677189

我们插入的内容如下,没有进行弹框

1658931745472

当我们点击编辑查看后,还是会弹框

1658978068945

在数据库中我们可以看到,他已经被过滤了

1658978147838

但是到再次编辑后,将其显示在前端页面后就解析为正常的标签

1658978216981

最后修改:2022 年 11 月 11 日
如果觉得我的文章对你有用,请随意赞赏