First thing I’d do is create a simple script to summarize what the bad guy did and when: $ ls tcp*s* | sort -k 2 -g -t- | while read line; do id=`echo $line | awk -F- '{ print $2 }'`; timestamp=`echo $line | awk -F- '{ print $4 }'`; date=`date -d @$timestamp`; action=`head -n 1 $line | awk '{ print $1" "$2}'`; echo "$id $date $action"; done 1 Sat Feb 20 10:12:01 EST 2010 GET /wp-login.php 2 Sat Feb 20 10:12:02 EST 2010 POST /wp-login.php 3 Sat Feb 20 10:12:03 EST 2010 GET /wp-login.php 4 Sat Feb 20 10:12:04 EST 2010 POST /wp-login.php … 91 Sat Feb 20 10:13:22 EST 2010 GET /wp-login.php 92 Sat Feb 20 10:13:23 EST 2010 POST /wp-login.php So from this it appears the bad guys was doing both GETS and POSTS to the wordpress login page for this site. Let’s look a little deeper into what was going on by starting on the first request: $ cat tcp-1-1266678719-1266678721-c-2916-193.226.51.2\:16118s66.173.221.158\:80 GET /wp-login.php HTTP/1.1 Referer: http://www.google.com/ User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; MRA 5.1 (build 02228); .NET CLR 1.1.4322; InfoPath.2; .NET CLR 2.0.50727) Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/x-silverlight, */* Host: www.elderhaskell.com Connection: Keep-Alive Overall this is pretty straightforward. Nothing explicitly malicious here. I’m curious, are all the GETs the same? Let’s add to our summary script (first 6 chars of md5 hash) and see: $ ls tcp*s* | sort -k 2 -g -t- | while read line; do id=`echo $line | awk -F- '{ print $2 }'`; timestamp=`echo $line | awk -F- '{ print $4 }'`; date=`date -d @$timestamp`; action=`head -n 1 $line | awk '{ print $1" "$2}'`; digest=`md5sum $line | head -c 6`; echo "$id $date $action $digest"; done 1 Sat Feb 20 10:12:01 EST 2010 GET /wp-login.php 17beda 2 Sat Feb 20 10:12:02 EST 2010 POST /wp-login.php 0394ce 3 Sat Feb 20 10:12:03 EST 2010 GET /wp-login.php 43ab32 4 Sat Feb 20 10:12:04 EST 2010 POST /wp-login.php 80c2de 5 Sat Feb 20 10:12:05 EST 2010 GET /wp-login.php 43ab32 6 Sat Feb 20 10:12:06 EST 2010 POST /wp-login.php bc19cd … 89 Sat Feb 20 10:13:20 EST 2010 GET /wp-login.php 43ab32 90 Sat Feb 20 10:13:21 EST 2010 POST /wp-login.php dfb19b 91 Sat Feb 20 10:13:22 EST 2010 GET /wp-login.php 43ab32 92 Sat Feb 20 10:13:23 EST 2010 POST /wp-login.php 2aa8dd Curiously, all the GETs are the same except for the first. What’s the diff: diff tcp-1-*s* tcp-3-*s* 2c2 < Referer: http://www.google.com/ --- > Referer: http://www.elderhaskell.com/wp-login.php 6c6 < Connection: Keep-Alive --- > Cookie: wordpress_test_cookie=WP+Cookie+check Alright, so the first time bad guy visits site he claims a referrer of google, from then on he has a cookie set. Nothing explicitly malicious there, but the referrer from google without search parameters looks a little artificial. A quick analysis of the responses for these requests shows “normal” activity. Ok, that was easy, we just analyzed 46 web requests by looking at 2. Now, for the interesting part, the POSTs. We already know they are all different. Let’s look at a couple: $ cat tcp-2-1266678721-1266678722-c-3518-193.226.51.2\:16222s66.173.221.158\:80 POST /wp-login.php HTTP/1.1 Referer: http://www.elderhaskell.com/wp-login.php User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; MRA 5.1 (build 02228); .NET CLR 1.1.4322; InfoPath.2; .NET CLR 2.0.50727) Accept: text/html, application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8, image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/x-silverlight, */* Content-Type: application/x-www-form-urlencoded Host: www.elderhaskell.com Cookie: wordpress_test_cookie=WP+Cookie+check Content-Length: 109 Expect: 100-continue log=admin&pwd=admin&wp-submit=Log+In&redirect_to=http%3a%2f%2fwww.elderhaskell.com%2fwp-admin%2f&testcookie=1 Looking at stream #4, it appears they are all the same except for the actual post data (login credentials) and the Content-Length header. Let’s make sure by modifying our digest to ignore those two lines: $ ls tcp*s* | sort -k 2 -g -t- | while read line; do id=`echo $line | awk -F- '{ print $2 }'`; timestamp=`echo $line | awk -F- '{ print $4 }'`; date=`date -d @$timestamp`; action=`head -n 1 $line | awk '{ print $1" "$2}'`; digest=`grep -v -E "^(Content-Length|log=)" $line | md5sum | head -c 6`; echo "$id $date $action $digest"; done 1 Sat Feb 20 10:12:01 EST 2010 GET /wp-login.php 17beda 2 Sat Feb 20 10:12:02 EST 2010 POST /wp-login.php 3f8d0a 3 Sat Feb 20 10:12:03 EST 2010 GET /wp-login.php 43ab32 4 Sat Feb 20 10:12:04 EST 2010 POST /wp-login.php 3f8d0a 5 Sat Feb 20 10:12:05 EST 2010 GET /wp-login.php 43ab32 6 Sat Feb 20 10:12:06 EST 2010 POST /wp-login.php 3f8d0a … 91 Sat Feb 20 10:13:22 EST 2010 GET /wp-login.php 43ab32 92 Sat Feb 20 10:13:23 EST 2010 POST /wp-login.php 3f8d0a Bingo. All the POSTs are the same too except for the credential provided. Now we’re getting to the interesting part. Let’s look at the credentials used: ls tcp*s* | sort -k 2 -g -t- | while read line; do id=`echo $line | awk -F- '{ print $2 }'`; timestamp=`echo $line | awk -F- '{ print $4 }'`; date=`date +%H:%M:%S -d @$timestamp`; action=`head -n 1 $line | awk '{ print $1" "$2}'`; digest=`grep -v -E "^(Content-Length|log=)" $line | md5sum | head -c 6`; creds=`grep -E "^log=" $line | awk -F'&' '{ print $1" "$2 }' | sed -r 's/(log=|pwd=)//g'`; echo "$id $date $action $digest $creds"; done | grep POST 2 10:12:02 POST /wp-login.php 3f8d0a admin admin 4 10:12:04 POST /wp-login.php 3f8d0a admin simple1 6 10:12:06 POST /wp-login.php 3f8d0a admin password 8 10:12:08 POST /wp-login.php 3f8d0a admin 123456 10 10:12:09 POST /wp-login.php 3f8d0a admin qwerty 12 10:12:11 POST /wp-login.php 3f8d0a admin abc123 14 10:12:13 POST /wp-login.php 3f8d0a admin letmein 16 10:12:15 POST /wp-login.php 3f8d0a admin monkey 18 10:12:17 POST /wp-login.php 3f8d0a admin myspace1 20 10:12:18 POST /wp-login.php 3f8d0a admin password1 22 10:12:20 POST /wp-login.php 3f8d0a admin blink182 24 10:12:22 POST /wp-login.php 3f8d0a admin John 26 10:12:24 POST /wp-login.php 3f8d0a admin john 28 10:12:26 POST /wp-login.php 3f8d0a admin love 30 10:12:27 POST /wp-login.php 3f8d0a admin god 32 10:12:29 POST /wp-login.php 3f8d0a admin sex 34 10:12:31 POST /wp-login.php 3f8d0a admin 12345 36 10:12:33 POST /wp-login.php 3f8d0a admin qwerty123 38 10:12:35 POST /wp-login.php 3f8d0a admin 12345678 40 10:12:36 POST /wp-login.php 3f8d0a admin wordpress 42 10:12:38 POST /wp-login.php 3f8d0a admin wp_admin 44 10:12:40 POST /wp-login.php 3f8d0a admin wp_password 46 10:12:42 POST /wp-login.php 3f8d0a admin wpadmin 48 10:12:44 POST /wp-login.php 3f8d0a wp-admin admin 50 10:12:45 POST /wp-login.php 3f8d0a wp-admin simple1 … 90 10:13:21 POST /wp-login.php 3f8d0a wp-admin wp_password 92 10:13:23 POST /wp-login.php 3f8d0a wp-admin wpadmin My first comment is that I’m never going to use blink182 as a password again :) So bad guy tried password list with username of admin then tried the same list with username of wp-admin. Classic. Was bad guy ever successful in getting in? Manual inspection of the first POST response indicates login was a failure. Let’s use the same digest method to verify that all POST responses were login failures: ls tcp*s* | sort -k 2 -g -t- | while read line; do id=`echo $line | awk -F- '{ print $2 }'`; timestamp=`echo $line | awk -F- '{ print $4 }'`; date=`date +%H:%M:%S -d @$timestamp`; action=`head -n 1 $line | awk '{ print $1" "$2}'`; digest=`grep -v -E "^(Content-Length|log=)" $line | md5sum | head -c 6`; creds=`grep -E "^log=" $line | awk -F'&' '{ print $1" "$2 }' | sed -r 's/(log=|pwd=)//g'`; digest2=`echo $line | sed s/s/c/ | xargs grep -v -E "^(Date|Last-Modified)" | md5sum | head -c 6`; echo "$id $date $action $digest $digest2 $creds"; done | grep POST 2 10:12:02 POST /wp-login.php 3f8d0a 0f713b admin admin 4 10:12:04 POST /wp-login.php 3f8d0a 0f713b admin simple1 … 44 10:12:40 POST /wp-login.php 3f8d0a 0f713b admin wp_password 46 10:12:42 POST /wp-login.php 3f8d0a 0f713b admin wpadmin 48 10:12:44 POST /wp-login.php 3f8d0a 98ee8f wp-admin admin 50 10:12:45 POST /wp-login.php 3f8d0a 98ee8f wp-admin simple1 … 90 10:13:21 POST /wp-login.php 3f8d0a 98ee8f wp-admin wp_password 92 10:13:23 POST /wp-login.php 3f8d0a 98ee8f wp-admin wpadmin Whoa, not so fast. Looks like response changed when bad guy went from admin to wp-admin. While I’m certain not all the tries where successes, I am curious what is different: $ diff tcp-2-*c*c* tcp-92-*c*c* 4c4 < Date: Sat, 20 Feb 2010 15:12:01 GMT --- > Date: Sat, 20 Feb 2010 15:13:22 GMT 8c8 < Last-Modified: Sat, 20 Feb 2010 15:12:02 GMT --- > Last-Modified: Sat, 20 Feb 2010 15:13:23 GMT 12c12 < Content-Length: 2316 --- > Content-Length: 2253 28c28 <
ERROR: Incorrect password. Lost your password?
--- >
ERROR: Invalid username. Lost your password?
34c34 < --- > 57,62c57 < setTimeout( function(){ try{ < d = document.getElementById('user_pass'); < d.value = ''; < d.focus(); < } catch(e){} < }, 200); --- > try{document.getElementById('user_login').focus();}catch(e){} Whoa. Apparently this installation of wordpress responds differently if your login fails because of username check or password check. That’s definitely a security No-No, so we’ll point this out to the remediation folk. Regardless, bad guy did not get in. Again, that was easy, we just analyzed and summarized 46 web request/responses by looking at a couple of each. The last thing we need to do is build a reasonable report of the activity. We make a simile timeplot event XML file. $ echo "" > timeplot.xml; ls tcp*s* | sort -k 2 -g -t- | while read line; do id=`echo $line | awk -F- '{ print $2 }'`; timestamp=`echo $line | awk -F- '{ print $4 }'`; date=`date -d @$timestamp`; action=`head -n 1 $line | awk '{ print $1" "$2}'`; digest=`grep -v -E "^(Content-Length|log=)" $line | md5sum | awk '{ print $1 }'`; creds=`grep -E "^log=" $line | awk -F'&' '{ print $1" "$2 }' | sed -r 's/(log=|pwd=)//g'`; digest2=`echo $line | sed s/s/c/ | xargs grep -v -E "^(Date|Last-Modified)" | md5sum | awk '{ print $1 }'`; method=`echo $action | awk '{ print $1 }'`; conn_info=`echo $line | awk -F- '{ print $NF }' | sed 's/s/ /'`; echo "$conn_info $digest $digest2"; done >> timeplot.xml; echo "" >> timeplot.xml