You are given a URI (all happens in the http://58.229.183.24/5a520b6b783866fd93f9dcdaf753af08/ route) that leads to index.php, the same but ends with index.phps and is an alleged source code of the former, finally, index.php contains a link to auth.php
index.phps listing below
<?php
session_start();
$link = @mysql_connect('localhost', '', '');
@mysql_select_db('', $link);
function RandomString()
{
$filename = "smash.txt";
$f = fopen($filename, "r");
$len = filesize($filename);
$contents = fread($f, $len);
$randstring = '';
while( strlen($randstring)<30 ){
$t = $contents[rand(0, $len-1)];
if(ctype_lower($t)){
$randstring .= $t;
}
}
return $randstring;
}
$max_times = 120;
if ($_SESSION['cnt'] > $max_times){
unset($_SESSION['cnt']);
}
if ( !isset($_SESSION['cnt'])){
$_SESSION['cnt']=0;
$_SESSION['password']=RandomString();
$query = "delete from rms_120_pw where ip='$_SERVER[REMOTE_ADDR]'";
@mysql_query($query);
$query = "insert into rms_120_pw values('$_SERVER[REMOTE_ADDR]', '$_SESSION[password]')";
@mysql_query($query);
}
$left_count = $max_times-$_SESSION['cnt'];
$_SESSION['cnt']++;
if ( $_POST['password'] ){
if (eregi("replace|load|information|union|select|from|where|limit|offset|order|by|ip|\.|#|-|/|\*",$_POST['password'])){
@mysql_close($link);
exit("Wrong access");
}
$query = "select * from rms_120_pw where (ip='$_SERVER[REMOTE_ADDR]') and (password='$_POST[password]')";
$q = @mysql_query($query);
$res = @mysql_fetch_array($q);
if($res['ip']==$_SERVER['REMOTE_ADDR']){
@mysql_close($link);
exit("True");
}
else{
@mysql_close($link);
exit("False");
}
}
@mysql_close($link);
?>
<head>
<link rel="stylesheet" type="text/css" href="black.css">
</head>
<form method=post action=index.php>
<h1> <?= $left_count ?> times left </h1>
<div class="inset">
<p>
<label for="password">PASSWORD</label>
<input type="password" name="password" id="password" >
</p>
</div>
<p class="p-container">
<span onclick=location.href="auth.php"> Auth </span>
<input type="submit" value="Check">
</p>
</form>
That's all that's given. Going thru the index.phps:
-
If you just hit the page, the script will create a PHP session with a counter
$_SESSION['cnt']. Every time you hit the page using the same session (meaning the same PHPSESSID - that's how PHP "knows" your client), you get the counter incremented by one till it goes up to120. -
On the first visit to the page, your client IP is written to the DB via
$_SERVER[REMOTE_ADDR]while any other record with the sameipdatabase field gets removed (bydelete from rms_120_pw where ip='$_SERVER[REMOTE_ADDR]'). Along with the IP, a new "random" password is written to the same table. -
The password is generated by
RandomString(), which reads publicly accessiblesmash.txt(article of the decade!) and fetches lowercase ASCII string 30 characters in length. That's the password that gets written to the DB. -
If counter is overflown (120 requests), the session gets renewed: a new password is being generated and written to the DB together with the same client IP.
-
If there's a
POSTrequest to the script with apasswordform parameter, then the page runseregi()against$_REQUEST['password'], with an attempt to prevent SQL-injection, however does it poorly as you will see in the Solution section.
- If you do a
POSTrequest toindex.phpwith the following payload:password='+or+password+like+'%
then the resulting SQL query is "select * from rms_120_pw where (ip='$_SERVER[REMOTE_ADDR]') and (password='' or password like '%')" and will bring "True" as the result of the script's processing. This bypasses eregi() but leaves you with the question "What's next?".
-
Recall, there's another script
auth.phpwhich doesn't seem to be prone to the same SQL attacks. Chances are, you're supposed to retrieve the password from therms_120_pwtable and submit it toauth.phpfor it will query the same table. -
Claim: we can bruteforce the password, based upon responses from
index.php"True" or "False" when doing SQL-injection. The payload below will tell if the first characted of the password string is "a":
password='+or+password+like+'a%
-
If the script returns "True" then the guess is correct and you can proceed to the next character (and so up to 30, which is the password length), otherwise "False" which means you have to try the next character in this position.
-
Recall, each request eats up one attempt. If we are allowed a handful of 120 attempts, the alphabet is lowercase ASCII (according to
RandomString()) then there120/26 = 4characters you can find in the worst case, having consumed all the attempts. This is a no-go.
There are various search algorithms that differentiate in their running time. However, this is not really needed here. Although, the number of attempts is limited to 120, you have to look at what is limiting it: $_SESSION['cnt']. If you carefully look at the source, you might see that what's written to the database is the remote IP, not the PHP session id, per se. That is, every time you request a page without a PHPSESSID cookie being sent to the server, but via the same IP, that IP still goes to the DB. In other words, you may request index.php even a thousand times (in theory), every time without a cookie, but storing the PHPSESSID that the server gave you. While doing this, the secret password is being regenerated every single time, until the very last request. With the last request, you stop raping the server, store all the cookies you fetched (esp. the last one) and of course, the same $_SERVER['REMOTE_ADDR'] is stored in the DB.
What this gives us, is the ability to consume attempts while bruteforcing the password, without being worried of the password getting overwritten. As long as you don't deplete 120 attempts, the password remains the same stored in the table. With that in mind the algorithm becomes simple:
-
Procure cookies (for the winter) enough to make
26*30requests at worst. Thanks toLIKEstatement it's not26^30. That's just7cookies. Again, there are ways to minimize the effort to many less attempts, but I want to consider the worst case to know the margin. -
Start bruteforcing
index.phpusing payloads likepassword='+or+password+like+'abcdefg% -
If the number of attempts is close to
120, supply another cookie and go from there.
That simple.
http://git.io/codegate2014-web-120-poc
-
If you are to use Python with
python-requestsmodule, then be informed that python-requests always URL-encodes the payload (dataparameter) which bit me again. If you know how to "officially" tell it not to - let us know in the comments. -
Usual one: I noticed the CTF server to go up in ping time by 2 seconds (!) after this challenge opened
-
Please share your password search algorithm if any
Read the manual if unsure, post comment(s) if unclear.