0


攻防世界-WEB-WEIPHP(记一次有趣的代码审计)

前言

其实很多时候,在面对一个站时,确认了站的特征(CMS,框架等等)就能够从github等平台上找到源码进行审计,从而发现问题并加以利用


正文

这套cms其实挺出名的//DOGE,20年的时候我就听说了,当时就有CTF比赛用这套系统写了一道题

先用常规的思路做一次测试好了

首先进行信息收集

尝试git

能够发现,git不下来

那么上科技找源码去好了

实战的时候也可以利用这样进行测试

其实这套框架编排做的还是挺舒服的

接下来进行审计

没错,就是那种苦逼的一个一个看过去,然后摸清楚原理,又或者是自己搭个环境尝试

通常来说做一次审计会花费极长的时间,由于我知道这套源码大致情况,所以这里就拉出几个有意思的地方着重讲解


代码审计

phpinfo(题外小插曲)

首先是我觉得最nb的一点

/public/info.php

太有石粒了

虽然phpinfo本身会暴露大量的服务器信息,但是这个玩法在这题本身没有很大的作用,

flag没有写在里面

这里讲一下正常情况下phpinfo()暴露的信息有哪些可以着重关注的

首先是php版本号

还有就是一些配置信息

等等等等

还有就是环境变量信息

这个不难找,懒得截图//DOGE

扩展模块

等等等等

错误报告

为on时我们可以根据详细报错信息进行分析程序的内部结构和潜在的开发模式

哦吼,没关debug

即使display_errors被设置为Off,错误信息仍然可以被记录下来,这对于问题的排查和监控非常重要。log_errors设置为On表示错误信息会被记录

内存限制

佬们很喜欢,但是CTFer考虑的可就多了

暂时就说这些


思路1(未成功)

weiphp5.0-master\application\home\controller\file.php

这个页面的源码里面其实有包含File.php upload功能的路径

**upload_picture **

 public function upload_picture()
    {
        // TODO: 用户登录检测
        if (!is_login()){
            $return = array(
                'status' => 0,
                'info' => '上传失败,请先登录',
                'data' => ''
            );
            return json($return);
        }
        /* 返回标准数据 */
        $return = array(
            'status' => 1,
            'info' => '上传成功',
            'data' => ''
        );
        /* 调用文件上传组件上传文件 */
        $Picture = D('home/Picture');
        $pic_driver = strtolower(config('picture_upload_driver'));
        
        $info = $Picture->upload(config('picture_upload'), config('picture_upload_driver'), config("upload_{$pic_driver}_config")); // TODO:上传到远程服务器
        /* 记录图片信息 */
        if ($info) {
            $return = array_merge($info['download'], $return);
        } else {
            $return['status'] = 0;
            $return['info'] = $Picture->getError();
        }
        /* 返回JSON数据 */
        return json($return);
    }

这个地方做了验证,所以无法利用

upload_file

无意义,且无法利用

upload_dialog

    function upload_dialog()
    {
        return $this->fetch();
    }

这里跳转到了图片选择器(别的文件定义的fetch方法)中,但是fetch有做验证判断是否已经登陆

不过分析源码你可以发现这里还有一个**upload_root **

  /* 文件上传 到根目录 */
    public function upload_root() {
        $return = array(
                'status' => 1,
                'info' => '上传成功',
                'data' => ''
        );
        /* 调用文件上传组件上传文件 */
        $File = D('home/File');
        $file_driver = strtolower(config('picture_upload_driver'));
        $setting = array (
                'rootPath' => './' ,
        );
        $info = $File->upload($setting, config('picture_upload_driver'), config("upload_{$file_driver}_config"));
//         $info = $File->upload(config('download_upload'), config('picture_upload_driver'), config("upload_{$file_driver}_config"));
        /* 记录附件信息 */
        if ($info) {
            $return['status'] = 1;
            $return = array_merge($info['download'], $return);
        } else {
            $return['status'] = 0;
            $return['info'] = $File->getError();
        }
        /* 返回JSON数据 */
        return json_encode($return);
        
    }
    
}

好家伙,并未做限制,没有判断用户是否登录

我们再查看源码发现虽然对传入的文件做了限制

function upload_files($setting = '', $driver = '', $config = '', $type = 'picture', $isTest = false)
{
    $return['msg'] = '';

    $files = request()->file();
    // dump($_FILES);
    // dump($files);//dump($rr);
    if (empty($files) || count($files) <= 0) {
        $return['msg'] = '找不到上传文件';
    }
    if ($return['msg'] != '') {
        return $return;
    }

    $key = key($files);
    $file = isset($files[$key]) ? $files[$key] : [];
    $rootpath = './uploads/' . $type . '/';
    $saveName = time_format(time(), 'Ymd') . '/' . uniqid();

    if (isset($setting['rootPath'])) {
        unset($setting['rootPath']);
    }

    // 检测上传根目录
    if (empty($return['msg'])) {
        if (!is_dir($rootpath) && function_exists('mkdirs')) {
            mkdirs($rootpath);
        }

        if (!(is_dir($rootpath) && is_writable($rootpath))) {
            $return['msg'] = '上传根目录不存在!请尝试手动创建:' . $rootpath;
        }
    }
    if (empty($return['msg'])) {
        //判断扩展名是不是php,不支持上传php文件
        $info = $file->getInfo();
        $info = pathinfo($info['name']);
        if (strtolower($info['extension']) == 'php') {
            $return['msg'] = '不支持上传该文件类型';
            $return['code'] = 0;
            $redata[$key] = $return;
            return $redata;
        }
        $checkRule = [];
        if ($type == 'picture') {
            //图片扩展名验证 ,图片大小不超过20M
            $checkRule['ext'] = 'gif,jpg,jpeg,png,bmp';
            $checkRule['size'] = 20971520;
        } else {
            $allowExt = input('allow_file_ext', '');
            if ($allowExt != '') {
                $checkRule['ext'] = $allowExt;
            }
            $allowSize = input('allow_file_maxsize', '');
            if ($allowSize > 0) {
                $checkRule['size'] = $allowSize;
            }
        }
        $info = $file->isTest($isTest)
            ->rule('uniqid')
            ->validate($checkRule)
            ->move($rootpath, DIRECTORY_SEPARATOR . $saveName);
        if ($info) {
            $return['mime'] = $info->getMime();
            $return['name'] = $info->getFilename();
            $return['key'] = $key;
            $return['ext'] = $info->getExtension();
            $return['savename'] = str_replace('\\', '/', $info->getSaveName());
            $return['md5'] = $info->md5();
            $return['sha1'] = $info->sha1();
            $return['code'] = 1;
            $of = $info->getInfo();
            isset($of['name']) || $of['name'] = $return['name'];
            $return['old_name'] = $of['name'];
            $return['size'] = isset($of['size']) ? $of['size'] : 0;
            $return['rootPath'] = $rootpath;
        } else {
            $return['msg'] = $file->getError();
            $return['code'] = 0;
        }
    }
    $redata[$key] = $return;
    return $redata;
}

但不多,正常CMS谁开这个啊

     $allowExt = input('allow_file_ext', '');
            if ($allowExt != '') {
                $checkRule['ext'] = $allowExt;
            }

我的思路就是抓包修改后缀,把原本被改成jpg的php文件改成phtml,从上面可以看出他并没有对图片里面的内容进行识别过滤,所以简单利用这一点即可

我们根据前面所给upload路径的规律,找到绝对路径(应该是)

嗯.....


思路2(未成功)

不能气馁,虽然说失败了,但是不能气馁

因为upload只是这套框架的其中之一

还有sql注入

weiphp5.0-master\application\material\controller\material.php

 $title = I('title');
        // dump($title);exit;
        if (!empty($title)) {//太简单了不解释
            $map['title'] = array(
                'like',
                "%$title%"
            );
            $where .= " and `title` like '%$title%'";
        }
        $count = M()->query("SELECT COUNT( distinct `group_id`) AS tp_count FROM `wp_material_news` WHERE {$where} LIMIT 1");//可以看到title没有任何过滤拼接到了sql语句里
        $count = isset($count[0]['tp_count']) ? $count[0]['tp_count'] : 0;

所以count是关键,因为我们可控是title,估计只能使用盲注来做判断

恰巧我不行

所以果断放弃


思路3(成功)

weiphp5.0-master\application\common\controller\base.php

一些基础配置,不过有一个地方很有意思

    // 与post_data函数相比,多了错误判断,省得在业务里重复判断
    public function post_data($url, $param, $type = 'json', $return_array = true, $useCert = [])
    {
        $res = post_data($url, $param, $type, $return_array, $useCert);

        // 各种常见错误判断
        if (isset($res['curl_erron'])) {
            $this->error($res['curl_erron'] . ': ' . $res['curl_error']);
        }
        if ($return_array) {
            if (isset($res['errcode']) && $res['errcode'] != 0) {
                $this->error(error_msg($res));
            } elseif (isset($res['return_code']) && $res['return_code'] == 'FAIL' && isset($res['return_msg'])) {
                $this->error($res['return_msg']);
            } elseif (isset($res['result_code']) && $res['result_code'] == 'FAIL' && isset($res['err_code']) && isset($res['err_code_des'])) {
                $this->error($res['err_code'] . ': ' . $res['err_code_des']);
            }
        }
        return $res;
    }

追踪post_data

发现存在于weiphp5.0-master\application\common.php中


// 以POST方式提交数据
function post_data($url, $param = [], $type = 'json', $return_array = true, $useCert = [])
{
    $has_json = false;
    if ($type == 'json' && is_array($param)) {
        $has_json = true;
        $param = json_encode($param, JSON_UNESCAPED_UNICODE);
    } elseif ($type == 'xml' && is_array($param)) {
        $param = ToXml($param);
    }
    add_debug_log($url, 'post_data');

    // 初始化curl
    $ch = curl_init();
    if ($type != 'file') {
        add_debug_log($param, 'post_data');
        // 设置超时
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    } else {
        // 设置超时
        curl_setopt($ch, CURLOPT_TIMEOUT, 180);
    }

    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

    // 设置header
    if ($type == 'file') {
        $header[] = "content-type: multipart/form-data; charset=UTF-8";
        curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
    } elseif ($type == 'xml') {
        curl_setopt($ch, CURLOPT_HEADER, false);
    } elseif ($has_json) {
        $header[] = "content-type: application/json; charset=UTF-8";
        curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
    }

    // curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)');
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
    curl_setopt($ch, CURLOPT_AUTOREFERER, 1);
    // dump($param);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $param);
    // 要求结果为字符串且输出到屏幕上
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    // 使用证书:cert 与 key 分别属于两个.pem文件
    if (isset($useCert['certPath']) && isset($useCert['keyPath'])) {
        curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
        curl_setopt($ch, CURLOPT_SSLCERT, $useCert['certPath']);
        curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
        curl_setopt($ch, CURLOPT_SSLKEY, $useCert['keyPath']);
    }

    $res = curl_exec($ch);
    if ($type != 'file') {
        add_debug_log($res, 'post_data');
    }
    // echo $res;die;
    $flat = curl_errno($ch);

    $msg = '';
    if ($flat) {
        $msg = curl_error($ch);
    }
    // add_request_log($url, $param, $res, $flat, $msg);
    if ($flat) {
        return [
            'curl_erron' => $flat,
            'curl_error' => $msg
        ];
    } else {
        if ($return_array && !empty($res)) {
            $res = $type == 'json' ? json_decode($res, true) : FromXml($res);
        }

        return $res;
    }
}

好的,既然CURLOPT_SSL_VERIFYPEER

CURLOPT_SSL_VERIFYHOST选项为false

并且**$url**参数并没有做任何过滤

由此判断可能存在ssrf

我们进行尝试

首先根据base.php我们明白

需要传入3个参数:url param type

url处用于访问文件,预设flag为/flag

param为数组,其实不动他就行

type处需要着重讲解,如果$type为json且$param是数组,则使用json_encode将其转换为JSON字符串,那么我把type=file,那么就无法被转换    而且很nb的是,源码没有强制,只能说太有实力了

所以我们简单进行构造

?url=file:///flag&param=111&type=file

整套分析下来可以发现操作在61.147.171.105:63287/weiphp5.0/public/index.php/home/user/处进行

比如说login

所以在此调用**post_data **

这作者能处,有东西是真给

搞定


结尾

也不能完全怪作者其实,毕竟tp框架+自己开发的CMS

不摸透=肉鸡

正常来说看一套源码需要花费很多时间,这次算是有目的的乱翻

所以我不是完全理解他的运作模式

感兴趣的可以自己扒下来玩玩

GitHub - weiphpdev/weiphp5.0: WeiPHP5.0,公众号与小程序结合的最佳开发框架,,它实现一个后台同时管理和运营多个客户端(公众号,微信小程序,后续将支持支付宝小程序,百度小程序等)

然后一起讨论 ,毕竟学海无涯 //DOGE

最后,求赞求关注,感谢

作者的其他文章

攻防世界-WEB-catcat-new-CSDN博客

BugKu-WEB-unserialize-Noteasy-CSDN博客

攻防世界-WEB-filemanager-CSDN博客

BugKu-new_php_bugku newphp-CSDN博客

标签: 前端 php 网络安全

本文转载自: https://blog.csdn.net/m0_52061428/article/details/140083110
版权归原作者 MQ4 所有, 如有侵权,请联系我们删除。

“攻防世界-WEB-WEIPHP(记一次有趣的代码审计)”的评论:

还没有评论