前言

在之前分析CVE-2018-7600时,对drupal的代码不熟悉,本想完整的分析整个流程,无奈水平不够,只能在漏洞出现的位置打断点调试。对一个ajax请求为什么会由那段代码处理完全不了解。后来复现drupal其他漏洞时,看代码也很吃力,所以对drupal的代码结构和运行流程进行一次分析,消除知识盲点。

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
drupal865/
|-- core/ #drupal核心代码
|-- modules/ #存放自定义和下载的模块
|-- profiles/ #存放自定义配置文件
|-- sites/ #存放数据库配置等站点相关文件
|-- themes/ #存放自定义和下载的主题
|-- vendor/ #代码依赖库
|-- example.gitignore
|-- index.php #入口文件
|-- INSTALL.txt
|-- LICENSE.txt
|-- autoload.php
|-- update.php
|-- composer.json
|-- composer.lock
|-- README.txt
|-- robots.txt
`-- web.config

其中core目录下存放了drupal的核心代码,看一下core目录的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
drupal865/core/
|-- assets #存放第三方js,css等文件
|-- config #存放核心配置文件
|-- includes #功能性函数库
|-- lib #drupal原生核心类
|-- misc #drupal运行所需的前端文件
|-- modules #drupal自带的模块,应用逻辑大部分在这里面
| |-- example
| | | `-- example.info.yml #模块详细信息
| | | `-- example.route.yml #模块路由信息
|-- profiles #配置文件
|-- scripts #开发人员使用的命令行脚本
|-- tests #测试文件
`-- themes #drupal自带主题

每个模块的目录中都会有一个module_name.route.yml,用来记录该模块所有的路由信息,可以通过阅读这个文件迅速建立url与代码的对应关系。

这是文档:https://www.drupal.org/docs/8/api/routing-system/structure-of-routes

运行流程

drupal官方提供了一个非常详细的运行流程图,由于图太大,这里附上链接:

https://www.drupal.org/files/d8_render_pipeline_0.pdf

接下来我们在代码层面看一下drupal到底是如何运行的。

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

use Drupal\Core\DrupalKernel;
use Symfony\Component\HttpFoundation\Request;

$autoloader = require_once 'autoload.php'; //类自动加载

$kernel = new DrupalKernel('prod', $autoloader); //实例化一个drupal内核对象用于处理请求

$request = Request::createFromGlobals(); //将发送来的请求封装成Request对象
$response = $kernel->handle($request); //**将请求转化为响应**
$response->send(); //将响应返回给用户

$kernel->terminate($request, $response); //结束整个过程

如果了解过symfony的http-kernel组件,就会发现drupal入口文件的内容与http-kernel官方文档给出的demo非常像,文档在这:https://symfony.com/doc/2.7/components/http_kernel.html

drupal处理请求是以symfony的http-kernel组件为基础的,并对处理流程进行了细化。

阅读文档,我们可以得知symfony的http-kernel的设计理念是将请求转化为响应。它是以事件来驱动运行的,会创建一个event dispatcher用来分发各种event,再由event listener来处理event,所有的操作都通过这种形式来完成。

请求预处理

下面跟进handle方法:

core/lib/Drupal/Core/StackMiddleware/KernelPreHandle.php

在处理之前会进入preHandle方法进行过滤。

core/lib/Drupal/Core/DrupalKernel.php

将request对象传入RequestSanitizer类进行过滤。

core/lib/Drupal/Core/Security/RequestSanitizer.php

对用户传入的信息进行解析,并在processParameterBag方法中调用了stripDangerousValues方法。

该方法对输入进行递归过滤,将#开头的参数全部过滤。这正是CVE-2018-7600的修补方法,在正式开始处理请求之前,就对传入的参数进行了过滤。

core/lib/Drupal/Core/DrupalKernel.php

在过滤完之后,又进行一系列的准备工作,下面就正式开始处理请求。

请求处理

vendor/symfony/http-kernel/HttpKernel.php

这里派发了一个REQUEST事件,开始处理请求。

首先根据event_name获取到对应的event_listener,然后遍历listener中对event的操作,开始处理event。

这里的操作包括处理OPTION请求、处理url中/的问题、权限认证等等,由于篇幅问题,没办法一一展示,有兴趣的可以每个跟进去看一下。这里想着重看一下根据url匹配路由的过程,因为在个人看来,理清一个框架很重要的一步就是了解它的路由,这样才能在代码与页面之间建立一种对应关系。

vendor/symfony/http-kernel/EventListener/RouterListener.php

这里对request对象进行解析,获取到了路由相关的信息,跟进去看一下:

经过一系列调用来到core/lib/Drupal/Core/Routing/RouteProvider.php

首先获取路由信息对应的缓存id,如果信息已经缓存,就直接取出返回。如果没有缓存,就继续查找路由信息,并在得到路由信息后存入缓存,减少查询次数提高效率。继续跟进getRoutesByPath方法:

可以看到路由信息是存在数据库中的,详细信息经过序列化。

调用对应的控制器

vendor/symfony/http-kernel/HttpKernel.php

回到HttpKernel.php,REQUEST事件处理完之后,分发CONTROLLER事件找到对应的controller,分发CONTROLLER_ARGUMENTS事件获取controller函数。处理完之后,调用controller,完成request到response的转化。跟进去看一下:

core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php

这里用闭包调用的方式对controller进行了一次包装,继续跟进:

这里才真正调用了路由对应的控制器,在控制器中有多种情况,大致处理流程如下:

controller的返回值有两种:a renderable array或者一个Symfony\Component\HttpFoundation\Response类型的对象。

请求转化为响应

这里我们选择一种较为复杂的情况进行分析,以CVE-2018-7600中的图片上传流程为例进行分析:

core/lib/Drupal/Core/Controller/FormController.php

首先根据路由和form参数生成一个form对象,接着实例化一个FormState对象,用于存储表单的各种状态。

这里介绍一下drupal的form api,使用它可以方便的创建和管理表单。一个form对象由这么几个基本方法:

1
2
3
4
getFormId: 必须定义的方法,返回表单机器名。
buildForm: 必须定义的,它返回一个渲染数组,类似 hook_form。
validateForm: 是可选的,进行校验,类似 hook_form_validate。
submitForm: 执行提交处理,类似 hook_form_submit。

文档在这:https://www.drupal.org/docs/8/api/form-api/introduction-to-form-api

表单处理

跟进buildForm方法:

core/lib/Drupal/Core/Form/FormBuilder.php

首先会对表单进行一些预处理,如果表单中存在元素需要渲染(即解析名字前有`#的元素),会在retrieveForm方法中进行,表单中的元素最终会进入doRender方法进行解析渲染。

由于发送的请求不同,调用栈也会有不同。

接下来是重点,由processForm方法来处理表单操作,跟进看一下:


首先在doBuildForm方法中递归处理form中的元素,接下来验证各元素的合法性,最后提交构建好的表单。

由doSubmitForm调用相关的代码进行操作,如数据库操作、缓存清理等。

ajax请求处理

在processForm执行完毕之后,如果是ajax请求,会抛出一个FormAjaxException,要进一步处理表单。

vendor/symfony/http-kernel/HttpKernel.php

一路跟进,可以看到这里派发了一个EXCEPTION事件。

派发事件之后会遍历所有的异常类型,然后匹配到正确的异常处理函数。

core/lib/Drupal/Core/Form/EventSubscriber/FormAjaxSubscriber.php

根据form构造response,跟进:

core/lib/Drupal/Core/Form/FormAjaxResponseBuilder.php

根据form_state获取对应的回调函数。

core/modules/file/src/Element/MangedFile.php

如果之前调试过CVE-2018-7600,会觉得这里非常熟悉。

在对应的callback函数中生成response。到这里,request到response的转化已经完成。

页面展示

vendor/symfony/http-kernel/HttpKernel.php

上面提到过,controller的返回结果有两种,如果返回的是renderable array,则派发VIEW事件,渲染模版;如果是Response对象,则直接发送给客户端。

流程结束

接下来就会派发RESPONSE事件和FINISH_REQUEST事件,将当前请求从栈中移出。

最后派发TERMINATE事件,整个流程结束。

参考链接

https://www.drupal.org/files/d8_render_pipeline_0.pdf

https://www.drupal.org/docs/8/api/form-api/introduction-to-form-api

https://www.drupal.org/docs/8/api/routing-system/structure-of-routes

http://nowicode.com/bookpage/145