前言
在之前分析CVE-2018-7600时,对drupal的代码不熟悉,本想完整的分析整个流程,无奈水平不够,只能在漏洞出现的位置打断点调试。对一个ajax请求为什么会由那段代码处理完全不了解。后来复现drupal其他漏洞时,看代码也很吃力,所以对drupal的代码结构和运行流程进行一次分析,消除知识盲点。
目录结构
1 | drupal865/ |
其中core目录下存放了drupal的核心代码,看一下core目录的结构:
1 | drupal865/core/ |
每个模块的目录中都会有一个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 |
|
如果了解过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 | getFormId: 必须定义的方法,返回表单机器名。 |
文档在这: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