Mybb 18.20 From Stored XSS to RCE 分析

作者:LoRexxar’@知道创宇404实验室
日期:2019年6月12日

2019年6月11日,RIPS团队在团队博客中分享了一篇MyBB <= 1.8.20: From Stored XSS to RCE,文章中主要提到了一个Mybb18.20中存在的存储型xss以及一个后台的文件上传绕过。

其实漏洞本身来说,毕竟是需要通过XSS来触发的,哪怕是储存型XSS可以通过私信等方式隐藏,但漏洞的影响再怎么严重也有限,但漏洞点却意外的精巧,下面就让我们一起来详细聊聊看…

漏洞要求

储存型xss

拥有可以发布信息的账号权限
服务端开启视频解析
<=18.20

管理员后台文件创建漏洞

拥有后台管理员权限(换言之就是需要有管理员权限的账号触发xss)
<=18.20

漏洞分析

在原文的描述中,把多个漏洞构建成一个利用链来解释,但从漏洞分析的角度来看,我们没必要这么强行,我们分别聊聊这两个单独的漏洞:储存型xss、后台任意文件创建。

储存型xss

在Mybb乃至大部分的论坛类CMS中,一般无论是文章还是评论又或是的什么东西,都会需要在内容中插入图片、链接、视频等等等,而其中大部分都是选择使用一套所谓的“伪”标签的解析方式。

也就是说用户们通过在内容中加入[url]、[img]等“伪”标签,后台就会在保存文章或者解析文章的时候,把这类“伪”标签转化为相应的、,然后输出到文章内容中,而这种方式会以事先规定好的方式解析和处理内容以及标签,也就是所谓的白名单防御,而这种语法被称之为bbcode。

这样一来攻击者就很难构造储存型xss了,因为除了这些标签以外,其他的标签都不会被解析(所有的左右尖括号以及双引号都会被转义)。

function htmlspecialchars_uni($message)
{
$message = preg_replace("#&(?!#[0-9]+;)#si", “&”, $message); // Fix & but allow unicode
$message = str_replace("<", “<”, $message);
$message = str_replace(">", “>”, $message);
$message = str_replace(""", “”", $message);
return $message;
}
正所谓,有人的地方就会有漏洞。

在这看似很绝对的防御方式下,我们不如重新梳理下Mybb中的处理过程。

在/inc/class_parse.php line 435 的 parse_mycode函数中就是主要负责处理这个问题的地方。

function parse_mycode($message, $options=array())
{
    global $lang, $mybb;

    if(empty($this->options))
    {
        $this->options = $options;
    }

    // Cache the MyCode globally if needed.
    if($this->mycode_cache == 0)
    {
        $this->cache_mycode();
    }

    // Parse quotes first
    $message = $this->mycode_parse_quotes($message);

    // Convert images when allowed.
    if(!empty($this->options['allow_imgcode']))
    {
        $message = preg_replace_callback("#\[img\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_callback1'), $message);
        $message = preg_replace_callback("#\[img=([1-9][0-9]*)x([1-9][0-9]*)\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_callback2'), $message);
        $message = preg_replace_callback("#\[img align=(left|right)\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_callback3'), $message);
        $message = preg_replace_callback("#\[img=([1-9][0-9]*)x([1-9][0-9]*) align=(left|right)\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_callback4'), $message);
    }
    else
    {
        $message = preg_replace_callback("#\[img\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_disabled_callback1'), $message);
        $message = preg_replace_callback("#\[img=([1-9][0-9]*)x([1-9][0-9]*)\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_disabled_callback2'), $message);
        $message = preg_replace_callback("#\[img align=(left|right)\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_disabled_callback3'), $message);
        $message = preg_replace_callback("#\[img=([1-9][0-9]*)x([1-9][0-9]*) align=(left|right)\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_disabled_callback4'), $message);
    }

    // Convert videos when allow.
    if(!empty($this->options['allow_videocode']))
    {
        $message = preg_replace_callback("#\[video=(.*?)\](.*?)\[/video\]#i", array($this, 'mycode_parse_video_callback'), $message);
    }
    else
    {
        $message = preg_replace_callback("#\[video=(.*?)\](.*?)\[/video\]#i", array($this, 'mycode_parse_video_disabled_callback'), $message);
    }

    $message = str_replace('$', '&#36;', $message);

    // Replace the rest
    if($this->mycode_cache['standard_count'] > 0)
    {
        $message = preg_replace($this->mycode_cache['standard']['find'], $this->mycode_cache['standard']['replacement'], $message);
    }

    if($this->mycode_cache['callback_count'] > 0)
    {
        foreach($this->mycode_cache['callback'] as $replace)
        {
            $message = preg_replace_callback($replace['find'], $replace['replacement'], $message);
        }
    }

    // Replace the nestable mycode's
    if($this->mycode_cache['nestable_count'] > 0)
    {
        foreach($this->mycode_cache['nestable'] as $mycode)
        {
            while(preg_match($mycode['find'], $message))
            {
                $message = preg_replace($mycode['find'], $mycode['replacement'], $message);
            }
        }
    }

    // Reset list cache
    if($mybb->settings['allowlistmycode'] == 1)
    {
        $this->list_elements = array();
        $this->list_count = 0;

        // Find all lists
        $message = preg_replace_callback("#(\[list(=(a|A|i|I|1))?\]|\[/list\])#si", array($this, 'mycode_prepare_list'), $message);

        // Replace all lists
        for($i = $this->list_count; $i > 0; $i--)
        {
            // Ignores missing end tags
            $message = preg_replace_callback("#\s?\[list(=(a|A|i|I|1))?&{$i}\](.*?)(\[/list&{$i}\]|$)(\r\n?|\n?)#si", array($this, 'mycode_parse_list_callback'), $message, 1);
        }
    }

    $message = $this->mycode_auto_url($message);

    return $message;
}

当服务端接收到你发送的内容时,首先会处理解析[img]相关的标签语法,然后如果开启了$this->options[‘allow_videocode’](默认开启),那么开始解析[video]相关的语法,然后是[list]标签。在488行开始,会对[url]等标签做相应的处理。

if(KaTeX parse error: Expected '}', got 'EOF' at end of input: … foreach(this->mycode_cache[‘callback’] as $replace)
{
m e s s a g e = p r e g r e p l a c e c a l l b a c k ( message = preg_replace_callback( message=pregreplacecallback(replace[‘find’], $replace[‘replacement’], $message);
}
}
我们把上面的流程简单的具象化,假设我们在内容中输入了

[video=youtube]youtube.com/test[/video][url]test.com[/url]
后台会首先处理[video],然后内容就变成了

[url]test.com[/url] 然后会处理[url]标签,最后内容变成 乍一看好像没什么问题,每个标签内容都会被拼接到标签相应的属性内,还会被htmlspecialchars_uni处理,也没办法逃逸双引号的包裹。

但假如我们输入这样的内容呢?

[video=youtube]http://test/test#[url]οnlοad=alert();//[/url]&1=1[/video]
首先跟入到函数/inc/class_parse.php line 1385行 mycode_parse_video中
在这里插入图片描述

链接经过parse_url处理被分解为

array (size=4)
‘scheme’ => string ‘http’ (length=4)
‘host’ => string ‘test’ (length=4)
‘path’ => string ‘/test’ (length=5)
‘fragment’ => string ‘[url]οnmοusemοve=alert();//[/url]&1=1’ (length=41)
然后在1420行,各个参数会被做相应的处理,由于我们必须保留=号以及/ 号,所以这里我们选择把内容放在fragment中。

在这里插入图片描述

在1501行case youtube中,被拼接到id上

case “youtube”:
if($fragments[0])
{
$id = str_replace(’!v=’, ‘’, KaTeX parse error: Expected 'EOF', got '#' at position 47: …utube.com/watch#̲!v=fds123 }…input[‘v’])
{
$id = $input[‘v’]; // http://www.youtube.com/watch?v=fds123
}
else
{
$id = $path[1]; // http://www.youtu.be/fds123
}
break;
最后id会经过一次htmlspecialchars_uni,然后生成模板。

i d = h t m l s p e c i a l c h a r s u n i ( id = htmlspecialchars_uni( id=htmlspecialcharsuni(id);

eval("$video_code = “”.KaTeX parse error: Expected '}', got 'EOF' at end of input: …s->get("video_{video}_embed", 1, 0)."";");
return $video_code;
当然这并不影响到我们上面的内容。

到此为止我们的内容变成了

紧接着再经过对[url]的处理,上面的内容变为

[video=youtube]http://test/test#[url]οnlοad=alert();//[/url]&1=1[/video]
变成了

最后浏览器会做简单的解析分割处理,最后生成了相应的标签,当url中的链接加载完毕,标签的动作属性就可以被触发了。

在这里插入图片描述

管理员后台文件创建漏洞

在Mybb的管理员后台中,管理员可以自定义论坛的模板和主题,除了普通的导入主题以外,他们允许管理员直接创建新的css文件,当然,服务端限制了管理员的这种行为,它要求管理员只能创建文件结尾为.css的文件。

/admin/inc/functions_themes.php line 264

function import_theme_xml($xml, KaTeX parse error: Expected '}', got 'EOF' at end of input: ….. foreach(theme[‘stylesheets’][‘stylesheet’] as KaTeX parse error: Expected '}', got 'EOF' at end of input: … if(substr(stylesheet[‘attributes’][‘name’], -4) != “.css”)
{
continue;
}

看上去好像并没有什么办法绕过,但值得注意的是,代码中先将文件名先写入了数据库中。

在这里插入图片描述

紧接着我们看看数据库结构

在这里插入图片描述

我们可以很明显的看到name的类型为varchar且长度只有30位。

如果我们在上传的xml文件中构造name为tttttttttttttttttttttttttt.php.css时,name在存入数据库时会被截断,并只保留前30位,也就是tttttttttttttttttttttttttt.php.

<?xml version="1.0" encoding="UTF-8"?> test 紧接着我们需要寻找一个获取name并创建文件的地方。

在/admin/modules/style/themes.php 的1252行,这个变量被从数据库中提取出来。

在这里插入图片描述

theme_stylesheet 的name作为字典的键被写入相关的数据。

当$mybb->input[‘do’] == "save_orders"时,当前主题会被修改。

在这里插入图片描述

在保存了当前主题之后,后台会检查每个文件是否存在,如果不存在,则会获取name并写入相应的内容。

在这里插入图片描述

可以看到我们成功的写入了php文件

完成的漏洞复现过程

储存型xss

找到任意一个发送信息的地方,如发表文章、发送私信等…

在这里插入图片描述

发送下面这些信息

[video=youtube]http://test/test#[url]οnlοad=alert();//[/url]&amp;1=1[/video]
然后阅读就可以触发

在这里插入图片描述

管理员后台文件创建漏洞

找到后台加载theme的地方

在这里插入图片描述

构造上传文件test.xml

<?xml version="1.0" encoding="UTF-8"?> test 需要注意要勾选 Ignore Version Compatibility。

然后查看Theme列表,找到新添加的theme

在这里插入图片描述

然后保存并访问相应tid地址的文件即可

在这里插入图片描述

补丁

https://github.com/mybb/mybb/commit/44fc01f723b122be1bc8daaca324e29b690901d6
储存型xss
在这里插入图片描述

这里的iframe标签的链接被encode_url重新处理,一旦被转义,那么[url]就不会被继续解析,则不会存在问题。

管理员后台文件创建漏洞

在这里插入图片描述

在判断文件名后缀之前,加入了字符数的截断,这样一来就无法通过数据库字符截断来构造特殊的name了。

写在最后

整个漏洞其实说到实际利用来说,其实不算太苛刻,基本上来说只要能注册这个论坛的账号就可以构造xss,由于是储存型xss,所以无论是发送私信还是广而告之都有很大的概率被管理员点击,当管理员触发之后,之后的js构造exp就只是代码复杂度的问题了。

抛开实际的利用不谈,这个漏洞的普适性才更加的特殊,bbcode是现在主流的论坛复杂环境的解决方案,事实上,可能会有不少cms会忽略和mybb一样的问题,毕竟人才是最大的安全问题,当人自以为是理解了机器的一切想法时,就会理所当然得忽略那些还没被发掘的问题,安全问题,也就在这种情况下悄然诞生了…

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
#include<stdio.h> #include<math.h> #include<stdlib.h> #define dx 100 struct bb { int m; int n; int hl[dx][dx]; int jk[dx][dx]; }; double det(struct bb *A, int n); double algebraic_cofactor(struct bb *A, struct bb *B, int row, int col); void adjoint(struct bb *A, struct bb *B); void inverse(struct bb *A,double inv[][dx],int N); int main() { struct bb A; int m,n; printf("输入几行几列:\n"); scanf("%d %d",&m,&n); A.m = m; A.n = n; printf("请输入矩阵:\n"); for(int i = 0; i < A.m; i++) { for(int j = 0; j < A.n; j++) { scanf("%d", &A.hl[i][j]); } } double inv[dx][dx]; int N = A.m; // Assuming square matrix // 计算逆矩阵 inverse(&A, inv, N); // 输出结果 printf("逆矩阵:\n"); for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { printf("%.2f ", A.hl[i][j]); } printf("\n"); } return 0; } double det(struct bb *A, int n) { double sum=0; if(n==1) { return A->hl[0][0]; } else if(n==2) { return A->hl[0][0]*A->hl[1][1]-A->hl[0][1]*A->hl[1][0]; } int i,j,k; struct bb *mybb = (struct bb *)malloc(sizeof(struct bb)); for(k=0;k<n;k++) { double b[dx][dx]; for(i=1;i<n;i++) { for(j=0;j<n;j++) { if(j<k) { b[i-1][j]=A->hl[i][j]; } else if(j>k) { b[i-1][j-1]=A->hl[i][j]; } } } mybb->m = n - 1; mybb->n = n - 1; for(i=0;i<mybb->m;i++) { for(j=0;j<mybb->n;j++) { mybb->hl[i][j] = b[i][j]; } } double detb=det(mybb,n-1); sum+=A->hl[0][k]*pow(-1,k)*detb; } free(mybb); return sum; } double algebraic_cofactor(struct bb *A, struct bb B, int row, int col) { int i,j,m=0,n=0,M=A->m; double sign; if((row+col)%2==0) { sign=1; } else { sign=-1; } for(i=0;i<M;i++) { if(i!=row) { for(j=0;j<M;j++) { if(j!=col) { B->jk[m][n]=A->hl[i][j]; n++; } } m++; n=0; } } double detb=det(B,M-1); return signdetb; } void adjoint(struct bb *A, struct bb *B) { int i,j,M=A->m; for(i=0;i<M;i++) { for(j=0;j<M;j++) { B->hl[j][i]=algebraic_cofactor(A,B,i,j); //注意这里是 hl[j][i] 而不是 hl[i][j] } } } void inverse(struct bb *A,double inv[][dx],int N) { // 构造伴随矩阵 struct bb B; B.m = N; B.n = N; adjoint(A, &B); // 计算行列式的值 double dets=det(A,N); // 判断行列式是否为零 if(dets == 0) { printf("该矩阵不可逆!\n"); return; } // 计算逆矩阵 for(int i=0;i<N;i++) { for(int j=0;j<N;j++) { inv[i][j] = B.hl[i][j] / dets; } } }修改这个代码找出错误并改正
最新发布
05-29
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值