返回
顶部

前言

最近的工作中遇到了一个使用MODX搭建的网站,而这个站碰巧licong师傅也看过,在他的指导下开始了对MODX源代码的审计

版本探测

MODX的管理路径为xxx.com/manager

1593878549600

直接google搜索MODX history release,从搜索结果中可以找到两个网站提供历史发行版本下载:

实际上MODX有两套CMS,一套叫做evolution,另一套叫做revolution

从MODX的github仓库中我们可以找到相关链接

1593880048935

通过下载这两套CMS并进行本地搭建之后与网站进行对比,确定网站所使用的的版本:

1593880141929

搜索相应版本是否存在漏洞

在官网上找到一个<=1.1的RCE漏洞

1593880381525

网站上的描述是说MODX的Ajaxsearch, eForm和evoGallery组件存在远程命令执行漏洞,就只有这么一句话,下面我们就根据这条线索对源代码进行审计

环境搭建

MODX安装

下载完对应版本的源代码之后解压到根目录,访问进行安装,我这里使用的是php5.3 nts,跟着安装向导走,在创建数据表的地方会报错

1593881426811

老版本的MySQL使用TYPE而不是ENGINE,我使用的mysql版本为5.5.53,已经不支持TYPE,因此需要全局替换

1593881785665

出现如下页面即代表环境搭建成功

1593881885599

调试环境的搭建

php调试选择xdebug,这里我使用的是开发工具是IDEA,IDEA会自动检测到php文件并提示安装php相关插件,php运行环境的配置如下,xdebug的安装方式可以通过点击进行官网查看

1593882100592

由于审计过程中需要使用postman构造POST请求,为了能够正常调试,需要在请求的URL后面加上xdebug的session:?XDEBUG_SESSION_START=14759

注意每次调试XDEBUG_SESSION_START的值都会改变

在调试代码的过程中,我们可能会想要输出中间变量,这里有一个小技巧,就是使用print_r输出数组的时候可以使用如下方式进行输出,方便我们查看数组结构:

1593882447769

审计代码

根据官网的描述,我们先从Ajaxsearch组件入手,使用Seay源代码审计系统自动审计,先筛选出存在远程命令执行漏洞风险且与Ajaxsearch组件相关的文件

1593882642508

在MODX 1.0.5的源代码中Ajaxsearch组件的入口文件只有一个evolution-1.0.5\assets\snippets\ajaxSearch\ajaxSearchPopup.php

直接在该文件的入口下断点,单步调试,调试完改文件之后得出的结论就是,payload中必须包含searchas_version,其中前者只要值不为空即可,后者值必须为1.9.2

该文件中除了run方法之外,不存在能够出发代码执行的操作,因此跟入run方法

run方法是AjaxSearch类的方法,它负责完成ajax搜索,通读该类文件发现需要再跟入到AjaxSearchCtrl类的run方法中

跟入run方法,执行AjaxSearchResults类的getSearchResults方法,根据上面自动审计的结果,该类文件的_doFilter方法可能存在命令执行漏洞

$this->_filterValue = eval(substr($filterArray[1], 5));

_doFilter方法处下断点进行调试,发现我们根本没有进入该方法,直接返回了结果

1593883520237

我们回到AjaxSearch类文件中,在调用AjaxSearchCtrl类的run方法处下断点跟入

1593883676977

发现$valid的值为false,无法进入可能存在漏洞的代码,因此我们跟入AjaxSearchInput类的display方法一探究竟

问题出在AjaxSearchInput类文件的165行,由于我们的AjaxSearchConfig类对象的成员变量cfg值为null,导致$this->asCfg->cfg['maxWords']为一个不存在的值,任意一个有值的变量都满足该if条件,最终导致valid变量为false

1593884247483

asCfg成员变量是一个引用,在AjaxSearchInput类的init方法中初始化,在此方法体中下断点

1593884992433

查看调用栈,找到$asCfg变量初始化的地方:AjaxSearch类文件,if (!$asCfg->initConfig($msgErr)) return $msgErr;

1593884983993

下断点跟入initConfig方法,发现cfg成员变量是dcfg和ucfg合并之后的结果,dcfg是evolution-1.0.5\assets\snippets\ajaxSearch\configs\default.config.php定义的一个关联数组,而ucfg来自此处$this->ucfg = $this->parseUserConfig(strip_tags($_POST['ucfg']));

他是我们通过POST请求传入的参数,跟入parseUserConfig方法(我做了一些变量的输出,重点关注parseUserConfig方法体):

<?php
//对传入的ucfg参数进行处理
function parseUserConfig($strUcfg) {
    $ucfg = array();
    //  &一串不包含=的字符串=`一串不包含`的字符串`
    $pattern = '/&([^=]*)=`([^`]*)`/';
    //要匹配的内容在两个括号里,$out最终会变成一个长度为3的数组,第一个存储的是匹配出的所有模式,
    //第二个存储的是第一个括号里的内容,第三个存储的是第二个括号里的内容
    //根据输入的字符串,可能会匹配出多组,第一个括号里的模式存在out[1],第二个括号里的模式存在out[2]中
    preg_match_all($pattern, $strUcfg, $out);

    echo "<pre>";
    print_r($out);
    echo "<pre>";

    foreach ($out[1] as $key => $values) $ucfg[$out[1][$key]] = $out[2][$key];
    //处理完成后$ucfg会变成一个关联数组
    //如果我们的$$strUcfg的值是&a=`aksdajldasjds`
    //则$ucfg的值为[a] => aksdajldasjds
    return $ucfg;
}

$strUcfg = '&a=`24yhgr5w3bsf`&b=`24wrgsfgesdg`';

$ret = parseUserConfig($strUcfg);

echo "<br />";
echo "<hr>";
echo "<br />";
echo "<pre>";
print_r($ret);
echo "<pre>"; 

运行结果:

1593885841779

现在我们的POST参数如下:

1593885922797

现在让我们回到这个断点:$this->_filterValue = eval(substr($filterArray[1], 5));

1593886050409

在前面的分析中,我们知道了AjaxSearchConfig对象的cfg成员变量是由dcfg和ucfg使用array_merge方法合并之后形成的,dcfg我们不可控,可控的变量是ucfg,ucfg是一个关联数组,我们开始的payload是:

&a=`aksdajldasjds`

形成的关联数组是a => aksdajldasjds ,如果我们把ucfg参数的值构造为下面的样子:

&a=`aksdajldasjds`&filter=`6986758` 

则生成的关联数组是a => aksdajldasjds filter => 6986758 ,所谓后来者居上,array_merge方法会让后面的关联数组的键覆盖已经存在的键,如此一来,$this->asCfg->cfg['filter']就是可控的变量了

现在我们直接将断点下在代码执行的地方

1593886461579

发送POST请求,未进入else分支,分析if分支的代码,回溯$filterArray变量,发现该变量是由我们可控的变量$this->asCfg->cfg['filter']使用,分割生成的数组,我们要想进入else分支,就要满足substr($filterArray[1], 0, 5) == "@EVAL",这就要求我们构造的filter键的值包含,不然数组长度为1,$filterArray[1]根本就不存在,且,后面的前5个字符应该为@EVAL,后面为我们要执行的命令,最终构造出来的ucfg的值应该为:

&a=`aksdajldasjds`&filter=`6986758,@EVALCODE` 

1593886820179

至此,代码审计完成,成功触发RCE漏洞

后记

本次代码审计的整体过程还是比较简单的,因为源代码并未对用户的恶意输入进行任何过滤

再次感谢licong师傅在此次审计过程中的指导

这里有一个地方需要说一下,在调试过程中我更改了ajaxSearchPopup.php的代码(这是很严重的错误操作,代码审计是绝对不允许更改源代码的

1593967064949

因为在调试过程中总是出现某些常量没有被定义的情况,因此直接将配置文件手动包含了进来,后来从官网论坛中了解到,必须要通过根目录下的index-ajax.php进入ajaxSearchPopup.php,一些变量的定义也是在该文件中进行的,不通过index-ajax.php是无法使用$modx变量的

index-ajax.php中进行了配置文件的引入

1593967324354

因此直接按照上面截图中的POST请求是无法触发RCE的,其实只需要分析index-ajax.php对POST请求做一下修改即可,详情不再赘述

这里学到的一个经验就是,如果出现常量未定义的错误,可以去找一下常量是在哪个文件中定义的,然后找出所有引用了该文件的代码,分析和漏洞代码有关联的代码,最终确定入口