前言

Moodle是一个广泛使用的开源电子学习软件,有超过1.27亿用户,允许教师和学生数字化管理课程活动和交换学习材料,通常由大学部署。本文我们将检查RIPsCodeAnalysis检测到的上一个Moodle版本中的关键漏洞的技术本质。它位于Moodle的Quiz组件中,可以通过教师角色成功地利用它来执行远程代码执行。如果你使用的是早于Moodle 3.5.0的版本,我们强烈建议立即更新到最新版本。

 

影响-谁能利用什么?

在使用默认配置运行的最新Moodle(早于3.5.0)课程中,必须为攻击者分配教师角色。通过另一个漏洞(如XSS)升级到此角色也是可能的。考虑到这些需求和漏洞的知识,攻击者将能够在运行Moodle的服务器的底层操作系统上执行任意命令。通过使用由Moodle计算的巧尽心思构建的数学公式,攻击者绕过了阻止执行恶意命令的内部安全机制。接下来我们将研究该漏洞的技术细节。

 

Quiz组件中的数学公式

Moodle允许教师设置一个包含多种类型问题的测试。其中包括计算问题,允许教师输入一个数学公式,由Moodle对随机输入变量进行动态评估。这样可以防止学生作弊,并简单地分享他们的成绩。例如,教师可以键入什么是{x}添加到{y}?的答案公式是{x}+{y}。然后Moodle将生成两个随机数,并为问答文本中的占位符{x}和{y}插入它们(例如3.9+2.1)。最后,它将通过对公式输入调用对安全性敏感的PHP函数eval()来计算答案6.0,该函数以其被恶意利用的风险而闻名,因为它允许执行任意PHP代码。

question/type/calculated/questiontype.php public function substitute_variables_and_eval($str, $dataset) { // substitues {x} and {y} for numbers like 1.2 with str_replace(): $formula = $this->substitute_variables($str, $dataset); if ($error = qtype_calculated_find_formula_errors($formula)) { return $error; // formula security mechanism }
        $str=null; eval('$str = '.$formula.';'); // dangerous eval()-call return $str;
} 

为了强制只使用无害的PHP代码,Moodle的开发人员引入了一个验证器函数qtype_calculated_find_formula_errors(),该函数在危险的eval()调用之前被调用,目的是检测教师提供的公式中的非法和恶意代码。

question/type/calculated/questiontype.php function qtype_calculated_find_formula_errors($formula) { // Returns false if everything is alright // otherwise it constructs an error message. // Strip away dataset names. while (preg_match('~\{[[:alpha:]][^>} <{"']*\}~', $formula, $regs)){
        $formula = str_replace($regs[0], '1', $formula);
    }
    // Strip away empty space and lowercase it.
    $formula = strtolower(str_replace(' ', '', $formula));
    $safeoperatorchar = '-+/*%>:^~<?=&|!'; /* */ $operatorornumber = "[{$safeoperatorchar}.0-9eE]"; // [...] if (preg_match("~[^{$safeoperatorchar}.0-9eE]+~", $formula, $regs)) { return get_string('illegalformulasyntax','qtype_calculated',$regs[0]);
    } else { // Formula just might be valid. return false;
    }
} 

 

开发一个绕过方法

正如在上面的源代码中所看到的,在最后一个preg_match()调用非常严格,除了公式中的-+/*%:^<?=&lx!.0-9EE之外,不允许使用任何字符。但是,在嵌套在一个临时循环中的str_replace()将递归地替换公式中的所有占位符,类似于{x}中的占位符。相应的正则表达式表明,考虑到{system(Ls)}是一个有效的占位符,占位符名称在其字符集中几乎不受限制,并且也将被$formula = str_replace($regs[0], ‘1’, $formula)中的1替换。这存在一个风险,因为在函数返回False之前,它将隐藏所有潜在的恶意字符,使其不受保护的preg_match()调用的影响,从而指示一个有效的公式。使用此技术隐藏恶意代码并将其与嵌套占位符结合,会出现可利用的漏洞。第一个恶意公式被验证器qtype_calculated_find_formula_errors()拒绝。如果我们将其作为占位符,并将其嵌入到花括号中(如第二个payload所示),验证器将不会检测到我们的攻击,但Moodle将简单地将我们的占位符替换为一个随机数1.2,然后才能到达eval()。但是,如果我们引入另一个占位符并将其直接嵌套到我们已有的占位符中,Moodle将只替换内部占位符,而一个危险的占位符将到达eval(),如表的第三行所示。此时,我们的payload将抛出一个PHP语法错误,这是因为eval()的输入是无效的PHP代码。因此,我们只需更正PHP语法,通过使用PHP注释从PHP解析器中排除无效部分,就可以在第4行使用最终的有效公式,该公式最终允许通过GET参数0执行代码。

视频地址:https://blog.ripstech.com/videos/moodle-passwd_CONV.mp4

 

改进不足的补丁

在向Moodle报告了这个问题后,他们立即做出了回应,并提出了一个补丁,以迅速解决这个问题。然而,在使用RIP重新扫描应用程序之后,我们的SAST解决方案仍然检测到指向新引入的修补程序的绕过方法相同的漏洞。在检查了相关的源代码和扫描结果之后,我们能够更准确地绕过补丁程序,实现与以前相同的效果。这是可能的前三个提议的补丁,我会在下一小节解释每个绕过方法。

第一个补丁:黑名单

Moodle开发人员提出的第一个补丁是基于拒绝包含在利用payload中使用的PHP注释的公式的想法。正如您在代码中所看到的,修补程序在foreach循环前面加上了一个前缀,用于检查公式是否包含特定的字符串。

question/type/calculated/questiontype.php function qtype_calculated_find_formula_errors($formula) { foreach (['//', '/*', '#'] as $commentstart) { if (strpos($formula, $commentstart) !== false) { return get_string('illegalformulasyntax', 'qtype_calculated', 
                           $commentstart);
        }
    } 

这个补丁使我们当前的payload变得毫无用处,因为验证函数qtype_calculated_find_formula_errors()检测到启动PHP注释/、/*、#的字符串,这些字符串在我们当前的利用payload中使用。这个补丁实现了黑名单方法,并基于这样的假设:没有攻击者能够在不使用注释的情况下将上述表中行和列3的无效PHP语法更正为有效的PHP语法。但是还是不够,更复杂的payload仍然可以绕过。

第二个补丁:拒绝嵌套占位符

第二个补丁的想法是通过在检测占位符时删除“递归”来防止嵌套在我们的payload中使用的占位符。但是,使用RIPs重新扫描应用程序仍然报告了同样的漏洞,这使得我们更精确地查看了下面的新代码。

question/type/calculated/questiontype.php public function find_dataset_names($text) { // Returns the possible dataset names found in the text as an array. // The array has the dataset name for both key and value. if (preg_match_all('~\{([[:alpha:]][^>} <{"']*)\}~',$text,$regs)) {
        $datasetnames = array_unique($regs[1]);
            return array_combine($datasetnames, $datasetnames);
        } else {
            return [];
        }
     }
// [...]
function qtype_calculated_find_formula_errors($formula) {
    $datasetnames = find_dataset_names($formula);
    foreach ($datasetnames as $datasetname) {
        $formula = str_replace('{'.$datasetname.'}', '1', $formula);
     } 

每当我们输入嵌套占位符{a{b}时,方法qtype_calculated_find_formula_errors()现在只将{b}替换为占位符,剩下的公式{A1}被检测为非法。但是,如果我们将公式更改为{b}{a1}{a{b},那么函数find_dataset_names()恰好检测到并返回两个占位符{b}和{a1}。每个占位符在foreach循环中都被一个接着一个替换,从我们的{b}开始,剩下的公式是1{a1}{a1}。最后,在替换{A1}之后,公式等于111,验证器批准嵌套占位符,从而破坏此修补程序的意图。考虑到这一技巧,我们只需适当地调整最后的payload就可以获得与以前相同的效果:

第三个补丁:黑名单和线性替换

第三个补丁结合了前两种方法,在防止嵌套占位符方面看起来非常好。但是,如果攻击者以Quiz组件的导入特性为目标,并重新导入恶意破坏的XML问题文件,则攻击者能够控制substitute_variables()(请参阅此处)的$DataSet参数,并使占位符替换无效。

<quiz> <question type="calculated"> [...] <answer fraction="100"> <text>log(1){system($_GET[0])}</text> </answer> </question> <dataset_definitions> <dataset_definition> <name><text>x</text></name> </dataset_definition> </dataset_definitions> </quiz> 

<name><text>x</text></name>定义了占位符{x}的名称。这个占位符在<text>log(1){system($_GET[0])}</text>公式中从未使用过。这将使危险占位符{system($_get[0])}的替换无效,并导致与以前的补丁相同的代码注入漏洞。

第四个补丁

不幸的是,由于时间限制,我们无法完全验证第四个补丁的完整性。如果有变化我们将更新这篇博客文章,当然会事先通知开发人员。

 

时间线

  • 2018.5.01 与供应商的第一次接触不足补丁#1拟议
  • 2018.5.01 补丁#1提议
  • 2018.5.02 报告绕过方法#1并确认
  • 2018.5.07 补丁#2提议
  • 2018.5.08 报告绕过方法#2并确认
  • 2018.5.12 补丁#3提议
  • 2018.5.15 报告绕过方法#3并确认
  • 2018.5.16 补丁#4提议
  • 2018.5.17 发布最终补丁

 

总结

在这篇文章中,我们研究了Moodle中的一个关键漏洞。Moodle通常被集成到更大的系统中,加入WebMailer、eLearning平台和其他技术,形成一个具有共享帐户凭据的单一体系结构,该体系结构跨越一个巨大的攻击面,供未经身份验证的攻击者钓鱼或提取教师帐户的凭据。在某些情况下,有一种自动请求Moodle课程的服务,它可以利用学生的权利,使他能够执行自己选择的恶意软件,并在他参加的大学课程中给自己评分。

在自动安全分析的帮助下,在10分钟内,不仅报告漏洞本身,而且报告补丁的不足,可以节省很多时间。我们要感谢Moodle团队在修补这个问题上的快速反应和合作。我们建议更新到最新的Moodle版本。