此漏洞(CVE-2017-2641)允许攻击者远程在Moodle服务器上执行PHP代码。它是由Moodle系统中几个组件中的不同漏洞组成的;我将在这篇文章里详细讲解每个漏洞的细节。
Moodle是个非常流行的学习管理系统,部署在世界各地的许多学术机构,包括麻省理工、斯坦福、剑桥和牛津等著名大学。我选择检测Moodle主要就是因为它很受欢迎并存储大量敏感信息,如成绩、测试结果等学生的私人数据。
该漏洞只需任何高于游客的用户权限来利用(如学生,教师等),并影响目前部署的几乎所有 Moodle版本。我强烈建议所有Moodle管理员立刻安装最新的安全补丁以解决这个问题。
受该漏洞影响的版本:
3.2 到 3.2.1、 3.1 至 3.1.4、 3.0 到 3.0.8,2.7.0 到 2.7.18 和其他不受支持的版本。
Moodle的系统极为庞大,包含数千个文件,数百个不同的组件,和约200万行的PHP代码。很明显,虽然它的各块代码可以彼此交互,也定是由很多不同的开发人员所写。
在这个文章中,我将示范为何太多代码,太多开发人员和缺乏规范的注释及文档会导致关键的逻辑漏洞。逻辑漏洞存在于几乎每一个代码库庞大的系统中,所以我所指出的这些安全问题影响和重要性远超Moodle系统本身。
我们可以从Moodle系统Ajax机制的实现中观察到“相同功能,不同代码”问题的清晰事例。 Moodle通过使用“外部函数”实现了一个动态的Ajax系统,允许不同的组件共同使用系统内置的Ajax接口。每个想使用Ajax机制的组件都需要注册一个独立的“外部函数”,注明被调用的组件、函数名和调用所需的权限。当这些组件需要使用Ajax接口时,它们只需调用“service.php”文件并提供已经注册了的外部函数名就可以了。如此一来,各个组件的开发者都可以使用Moodle内置的Ajax接口,省去了自己开发Ajax接口的麻烦。但是,当Moodle核心部分的开发人员也开始使用这个接口时,问题就出现了。
不久之前,如果一个部件需要通过Ajax请求更改某个用户的首选项,则需调用“setuserpref.php”文件,并提供要更改的属性名称和新的值。
相关代码如下:
从代码第二段落我们可以看到,Moodle在检查需要被更改的首选项是否存在于“ajax_updatable_user_prefs”数组内,因为这个数组定义了哪些首选项可以通过Ajax接口更改。这很合理,因为开发人员不希望用户通过Ajax接口恶意更改任何可能影响系统运作的关键首选项。
虽然大多数用户首选项就算不能通过Ajax接口也可通过其他方法更改,Moodle的开发者们还是尽力在降低首选项机制被恶意利用的可能。
事实证明,他们对于首选项的细心防护的确很有效,直到某个马虎的开发者添加了一个新的外部函数“update_user_preferences”。添加这个新函数的目的是取代一个旧的函数“update_users”。“update_users”的用途与“setuserpref.php”文件类似,也被用来更改用户首选项,但属于系统内部函数,不能通过Ajax接口调用。根据更新记录里的注释,“update_users”函数“可被用于更改任何用户属性”;这很显然存在安全隐患,因为一个只用来更改用户首选项的函数完全不需要有更改任何属性这么危险的功能。新的函数解决了这个问题– 它仅可以更改用户首选项。这么看来,用“update_user_preferences” 替代“update_users”似乎是件有利无害的事。
但是,旧函数和新函数还有几个很重要的区别。由于其危险性,旧函数不能通过Ajax接口调用;但新函数可以,因为开发人员认为旧函数的危险功能(更改任何用户属性)已经在新函数中被移除了,所以允许它被其他组件调用。
为了安全,开发者还在新函数中增加了一道权限检查,防止当前用户更改其他用户的首选项。只可惜,他没能看到深层更大的安全隐患。如果这些首选项的值被用在eval或exec函数中,那能否更改其他用户的首选项就根本无所谓了,因为攻击者可以直接在系统上执行自己的代码。
但讨论这些有些过早了,咱们先退几步分析一下这个函数是如何实现的:
通过阅读这段代码我们可以看到,由于有权限检查,我们只能更改自己用户的首选项 。
即便如此,只要和之前提到的实现同样功能的“setuserpref.php”文件仔细对比一下,就不难发现这个新的函数漏了点什么。虽然新函数确保用户只能更改自己的首选项,但却没有检查用户更改的首选项是否应该允许通过Ajax更改。这和之前通过Ajax更改用户首选项的方法有着关键的区别。
这是不同开发者在不同的时间,有着不同目的的情况下写出实现同一个功能的不同代码的经典案例。
总结下来,在“update_user_preferences()”外部函数被添加,取代“update_users()”函数前,若是任何组件想通过Ajax更改用户首选项则需调用“setuserpref.php”文件 。“update_user_preferences()”外部函数被添加后,组件也可以通过Ajax接口调用它来更改用户首选项 。但是,与“setuserpref.php”不同,“update_user_preferences()”并不会检查所更改的首选项是否应该允许远程更改。这导致了本来密不透风的Ajax更改用户首选项机制悄然破了个大洞,但开发人员还错误地自认为用户首选项无法被攻击者恶意利用 。
用户首选项被开发者判定为无法被恶意利用是有一定道理的,不仅是因为之前更改它们的方法“setuserpref.php”密不透风。这些首选项本身似乎对于系统的运作影响甚微– 它们不被包括任何数据库查询中,也不定义任何组件;它们仅能够使图形用户界面(GUI)产生些许变化。
那么,我们到底能利用它做什么?
首先我们需要了解系统的几个GUI部分是怎么运作的。Moodle的GUI将用户界面上各个组件信息分为独立的区块,并让用户以区块作为单元来自定义自己的界面。用户可以根据自己所需增加相关组件的区块,或移除不需要的区块。
其中一个名为“course_overview”(课程概述)的区块可以显示当前用户登记选修的课程。这些课程的排序存储于一个名为“course_overview_course_sortorder”的用户首选项中。这个首选项里存有所有当前用户登记的课程的课程编号。编号依时间排序,共同存储在一个由逗号分隔的字符串里。当课程概述区块需要显示课程排序的时候,会先执行这段代码将首选项里的课程编号从字符串拆分为数组,然后再按顺序显示给用户:
但如果首选项内容为空呢?这种情况下,课程概述区块会试着通过一个遗留首选项,“course_overview_course_order”,来读取课程编号。这个首选项存有所有课程的编号,但储存和读取它们的方式与“course_overview_course_sortorder”不同。用来读取这个首选项的代码如下:
居然调用了unserialize()函数,我们中大奖了。
【译者注:对于不熟悉PHP和对象注入的朋友这里稍微做一下讲解,因为这个漏洞不算太常见。 unserialize()(反序列化)是个方便但危险的PHP函数,常与serialize(序列化)配合使用。serialize()可以把一个任意类型的对象转换为字符串以方便储存,unserialize()则把由serialize()转换的字符串转回PHP对象;对象的值、数据类型和结构在转换中都会保留。若是被反序列化的字符串中存在未经正规过滤和编码的用户输入,则会导致对象注入漏洞,因为攻击者可以将任意数据注入进unserialize()函数返回的对象里。想要更好地理解对象注入可以参考:http://blog.csdn.net/qq_19876131/article/details/50926210】
这又是个因开发人员和代码缺乏规范性而导致安全漏洞的经典案例。它充分展现了遗留代码和向后兼容性带来的安全隐患,也再次反映了不同开发者会用不同方式来实现同样的功能这个现象。
对于我们来说,这意味着我们可以利用这个函数进行对象注入攻击。
由于Moodle对用户输入进行统一过滤,我们的注入受到些局限:
1. 我们不能注入任何空字节 。这也说明我们不能设置任何受保护或私有的对象属性,因为在PHP序列化它们时会在它们的序列化声明中加入空字节。
2. 虽然Moodle的代码库中有很多类,但绝大多数都是我们不可达的。它们不是在我们进行对象注入的作用域中没有被包含,就是无法通过自动加载功能来访问。
这些限制使得对象注入的难度提高了不少。不过别担心,我标题里都说了可以执行远程代码。咱们继续研究。
趋于上述限制,我们只能使用已被包含的类的公共属性。我们也不能用任何依赖于受保护或私有属性的代码,因为它们被初始化为它们的默认值或着空。
这大大缩减了系统的可攻击面。几乎所有的类都会在某些情况下使用受保护的属性,而且大多数类根本不会有任何公共属性。
进攻的第一步是要先弄清我们具体能调用哪些PHP魔术方法。我们当然可以调用__wakeup()(当一个对象被反序列化时会调用)和__destruct() (当一个对象被摧毁时会调用),但我们能否调用另一个很常用的方法__toString()?
我们再次回到代码寻找一下线索。我们可以看到,我们注入的输入在被反序列化后成为了一个数组:
上面这个函数在试图把我们反序列化的数组的成员用逗号连接为一条长的字符串。但是,如果这个数组的某个成员是个对象,那它的类的__toString()魔术方法就会被调用。因为我们完全控制这个数组的值,所以想执行几个对象的__toString()方法就能执行几个。
但用__toString()又能做什么呢?我们先来看看“attribute_format”这个抽象类是如何实现它的__toString()方法的:
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课