• DocCms_2016 代码审计


    0x00 结构浏览

      按照代码审计的惯例,拿到这款cms之后首先浏览了一下目录结构,在基本了解之后,首先进入/index.php,这里包含了两个文件:/admini/config/qd-config.php和/loader/load.php,简单看了一下代码,没什么用,程序入口文件没有太大价值。转到后台/admini目录下,发现还有个/admin/index.php,打开阅读一下源码,没什么用,但是明确了这是一款比较硬核的cms,注释极少,程序可读性不是很高。/admin目录下还有一个/admin/controllers,从命名上就能看出这是一个比较重要的业务逻辑包,打开之后里面还有一个/admin/controllers/index.php,里面只有两个函数,还是没什么用。在这个目录下,还有一个文件夹为system,打开之后居然还有一个/admin/controllers/system/index.php....里面就俩函数,也没有有价值的信息。

    0x01 任意文件下载

      虽然这一大堆index都没有用到,但是也帮助我们了解了这款cms的结构,所以我们直接从后台业务逻辑核心部分,也就是/admini/controllers/system入手,首先读一下bakup.php的源码,这个文件主要作用是管理数据库,里面有一个download函数疑似存在任意文件下载:

     1 function download()
     2 {
     3     global $request;
     4     if(!empty($request['filename']))
     5     {
     6         file_down(ABSPATH.'/temp/data/'.$request['filename']);
     7     }
     8     else
     9     {
    10          echo '<script>alert("文件名不能为空!");window.history.go(-1);</script>';
    11     }
    12 }

      为了进一步确认可以下载的范围,跟读file_down函数:

     1 function file_down($file,$filename='')
     2 {
     3     if(is_file($file))
     4     {
     5         $filename = $filename ? $filename : basename($file);
     6         $filetype = fileext($filename);
     7         $filesize = filesize($file);
     8         header('Cache-control: max-age=31536000');
     9         header('Expires: '.gmdate('D, d M Y H:i:s', time() + 31536000).' GMT');
    10         header('Content-Encoding: none');
    11         //header('Content-Length: '.$filesize);
    12         header('Content-Disposition: attachment; filename='.$filename);
    13         header('Content-Type: '.$filetype);
    14         readfile($file);
    15     }
    16     else
    17     {
    18         echo '<script>alert("文件不存在!");window.history.go(-1);</script>';
    19     }
    20     exit;
    21 }

      可以看出这里没有做任何的过滤,构造payload:

    http://127.0.0.1/doccms-2016/admini/index.php?m=system&s=bakup&a=download&filename=index.php

    0x02 CSRF

      在这个文件里还有一个export函数:

     1 function export()
     2 {
     3     global $db,$request,$sizelimit,$startrow;
     4     $tables=$request['tables'];
     5     $sizelimit=$request['sizelimit'];
     6     if($request['dosubmit'])
     7     {
     8         $fileid = isset($request['fileid']) ? $request['fileid'] : 1;
     9         if($fileid==1 && $tables) 
    10         {
    11             if(!isset($tables) || !is_array($tables)) 
    12                 echo "<script>alert('请选择要备份的数据表!');window.history.go(0);</script>";
    13             $random = mt_rand(100000, 999999);
    14             cache_write('bakup_tables.php', $tables);
    15         }
    16         else
    17         {
    18             if(!$tables = cache_read('bakup_tables.php')) 
    19                 echo "<script>alert('请选择要备份的数据表!');window.history.go(-1);</script>";
    20         }
    21         $sqldump = '';
    22         $tableid = isset($request['tableid']) ? $request['tableid'] - 1 : 0;
    23         $startfrom = isset($request['startfrom']) ? intval($request['startfrom']) : 0;
    24         $tablenumber = count($tables);
    25         for($i = $tableid; $i < $tablenumber && strlen($sqldump) < $sizelimit * 1024; $i++)
    26         {
    27             $sqldump .= sql_dumptable($tables[$i], $startfrom, strlen($sqldump));
    28             $startfrom = 0;
    29         }
    30         
    31         if(trim($sqldump))
    32         {
    33             $sqldump = "#Realure.cn Created
    # --------------------------------------------------------
    
    
    ".$sqldump;
    34             $tableid = $i;
    35             $random = isset($request['random']) ? $request['random'] : $random;
    36             $filename = DB_DBNAME.'_'.date('Ymd').'_'.$random.'_'.$fileid.'.sql';
    37             $fileid++;
    38 
    39             $bakfile = '../temp/data/'.$filename;
    40             if(!is_writable('../temp/data/')) 
    41                 message('数据无法备份到服务器!请检查 ./data 目录是否可写。', $forward);
    42             file_put_contents($bakfile, $sqldump);
    43             //echo 'wer';
    44             //exit;
    45             echo '<script>alert("备份文件'.$filename.'写入成功!");window.location.href="?m=system&s=bakup&a=export&sizelimit='.$sizelimit.'&tableid='.$tableid.'&fileid='.$fileid.'&startfrom='.$startrow.'&random='.$random.'&dosubmit=1";</script>';
    46         }
    47         else
    48         {
    49            cache_delete('bakup_tables.php');
    50            echo '<script>alert("数据库备份完毕!");window.location.href="?m=system&s=bakup&a=export";</script>';
    51             //message('数据库备份完毕!');
    52         }
    53         exit;
    54     }
    55 }

      不用看代码,光看输出就能看出来这是备份数据库的函数,但是代码审计总还是要看代码的,这个函数接收了用户提交的两个参数:tables、sizelimit,依据这两个参数导出数据库备份文件,没有验证referer和token,因此会存在CSRF漏洞,诱导管理员备份数据,结合之前的任意文件下载漏洞就可以轻松拿到数据库信息。payload:

    http://127.0.0.1/doccms-2016/admini/index.php?m=system&s=bakup&a=export&tables[]=doc_user&sizelimit=2048&dosubmit=开始备份数据

    0x03 可执行sql文件的上传

      这个文件的问题很多,几乎每个函数都可以利用,后面还有一个上传sql文件的函数:

     1 function uploadsql()
     2 {
     3     global $request;
     4     $uploadfile=basename($_FILES['uploadfile']['name']);
     5     if($_FILES['userfile']['size']>$request['max_file_size'])
     6         echo '<script>alert("您上传的文件超出了2M的限制!");window.history.go(-1);</script>';
     7     if(fileext($uploadfile)!='sql') 
     8         echo '<script>alert("只允许上传sql格式文件!");window.history.go(-1);</script>';
     9     $savepath = ABSPATH.'/temp/data/'.$uploadfile;
    10     if(move_uploaded_file($_FILES['uploadfile']['tmp_name'], $savepath))
    11     {
    12          echo '<script>alert("数据库SQL脚本文件上传成功!");window.history.go(-1);</script>';
    13     }
    14     else
    15     {
    16         echo '<script>alert("数据库SQL脚本文件上传失败!");window.history.go(-1);</script>';
    17     }
    18 }

      这里有一个代码逻辑漏洞,在代码的第7行和fileext()函数中希望验证后缀名,只有.sql文件才能上传,但是后面并没有exit,而是弹窗提示只允许上传sql格式文件,程序继续运行,这样我们就可以上传任意文件了,新建一个phpinfo,在数据恢复页面上传。

      除此之外,这里还有第二种利用方式,有一个数据库导入函数import:

     1 function import()
     2 {
     3     global $db,$request;
     4     $pre=$request['pre'];
     5     if($request['dosubmit'])
     6      {
     7         if($request['filename'] && fileext($request['filename'])=='sql')
     8         {
     9             $filepath = ABSPATH.'/temp/data/'.$filename;
    10             if(!is_file($filepath)) 
    11                   echo '<script>alert("文件不存在!");window.history.go(-1);</script>';
    12             $sql = file_get_contents($filepath);
    13             sql_execute($sql);
    14               echo '<script>alert("'.$filename.'中的数据已经成功导入到数据库!");window.history.go(-1);</script>';
    15         }
    16         else
    17         {
    18             $fileid = isset($request['fileid']) ? $request['fileid'] : 1;
    19             $filename = $request['pre'].$fileid.'.sql';
    20             $filepath = ABSPATH.'/temp/data/'.$filename;
    21             if(is_file($filepath))
    22             {
    23                 $sql = file_get_contents($filepath);//将整个文件读入一个字符串
    24                 sql_execute($sql);
    25                 $fileid++;
    26                 echo '<script>alert("数据文件'.$filename.'导入成功!");window.location.href="?m=system&s=bakup&a=import&pre='.$pre.'&fileid='.$fileid.'&dosubmit=1";</script>';        
    27                         
    28             }
    29             else
    30             {
    31                 echo '<script>alert("数据库恢复成功!");window.location.href="?m=system&s=bakup&a=import";</script>';
    32             }
    33         }
    34      }
    35 }

      代码的18-28行读取.sql文件并且执行,如果我们自己上传的sql脚本带有一句话木马,在权限足够的情况下可以直接拿shell。

    0x04 同目录任意文件删除

      这个文件搞完了,下一个文件是changeskin.php,里面有一个deleteFile函数:

     1 function deleteFile(){
     2     global $request;
     3     $dirPath = get_abs_skin_root().filter_submitpath( $request['dirPath'] );
     4     if(is_file($dirPath)){
     5         @unlink($dirPath);
     6         exit('1::delete ok');
     7     }else{
     8         exit('0::Forbidden');
     9     }
    10 }

      里面调用了两个函数,继续跟读:

     1 function filter_submitpath($path)
     2 {
     3      $path= preg_replace('/[.]{2,}/', '', $path);//去除 .. 禁止提交访问上级目录的路径
     4      return preg_replace('/[/]{2,}/', '/', $path);//校正路径 
     5 }
     6 
     7 
     8 function get_abs_skin_root()
     9 {
    10     return ABSPATH.'/'.SKINROOT.'/'.STYLENAME.'/';
    11 }

      可以看出这里没有进行任何的过滤,可以删除同目录下的所有文件。

  • 相关阅读:
    [Elasticsearch] 向已存在的索引中加入自己定义filter/analyzer
    Hibernate持久化对象的状态
    android 利用cmdline,将參数从preloader传递到kernel
    HDOJ的题目分类
    activiti自己定义流程之Spring整合activiti-modeler实例(七):任务列表展示
    十大广泛使用的Linux发行版
    Windows下配置Git服务器和客户端 超全
    适当使用enum做数据字典 ( .net c# winform csharp asp.net webform )
    System.Drawing.Image在Save之后Type变了
    winform ListView应用之分组、重绘图标、网格线 (c# .net winform)
  • 原文地址:https://www.cnblogs.com/richardlee97/p/10669027.html
Copyright © 2020-2023  润新知