为什么想要审计这套源码呐?之前看到某大佬在做反钓鱼网站的时候,发现钓鱼网站的后台用的就是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就确定存在了,可以打到管理员的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数据库监控得到如下结果:
这里做了转义,再看看代码!
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 即可。
就这样吧~