Introduction "120" is the second web challenge from the Codegate CTF Preliminary 2014. It has been solved by 100 teams, but our team was the fastest. The challenge contains two files: index.php and auth.php, where both are very similar:
Additionally, we are given the source code of index.php:<?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);?>
Recon We can see that the script is setting a random password for every 120 consecutive requests. The password is linked to our unique IP address and stored in the MySQL database. We are able to send password parameter via POST, which is tested on the blacklist:if (eregi("replace|load|information|union|select|from|where|limit|offset|order|by|ip|\.|#|-|/|\*",$_POST['password'])){ @mysql_close($link); exit("Wrong access"); }
Yes, this challenge is about SQL injection attack. Let's look at the vulnerable line:$query = "select * from rms_120_pw where (ip='$_SERVER[REMOTE_ADDR]') and (password='$_POST[password]')";
It's impossible to alter$_SERVER[REMOTE_ADDR]
, but$_POST[password]
is under our control! Magic quotes were disabled on that server, so there was no problem to close a single quote. Take a look at the rest of the code:$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"); }
Errors are stripped (@
before functions names), so our attack may be boolean blind injection based on True/False output. Functions like sleep or benchmark are not on the blacklist, so we can also do time-based attack. The second idea is more reasonable, because we have only 120 queries. ...is it? eregi - old and bad WAF is based on the eregi function. In the manual we can read:
Warning
This function has been
DEPRECATED as of PHP 5.3.0. Relying on this feature is
highly discouraged.
Why is that? For example because eregi stops reading strings on the first null byte. Remember that PHP allows us to send strings containing null bytes. By setting first byte of password to null we can bypass eregi and use all forbidden words from the blacklist! 120? - just think out of the box After every 120 requests new password linked to our IP address is generated. So logical is that we have to get our password in 120 queries. ...is it? Who said it must be our password? :) We just can use our first IP address to get password linked to a second IP address we control. For example: 1) Request index.php to generate new password and store it in the database. Use first IP to do it. 2) Do blind SQL injection attack to get password linked to the first IP. Use second IP to do it. 3) Login in auth.php and profit. Use first IP to do it. Payload The last missing thing is the payload. After bypassing the whole blacklist creating one is not difficult. %00') union select if((select 1 from rms_120_pw where ip='$IP_with_static_password
' and ord(mid(password,$i
,1))>$guess
limit 1),'$IP_with_exploit
',0),2-- x We can do fast binsearch boolean-based blind SQL injection using this. Congrats!
After all we solved it in very fast and smart way :)
Congrats! the key is DontHeartMeBaby*$#@! The challenge has been solved by Mawekl, member of the Dragon Sector.
nice writeup! Another way to solve web500 #codegate@2014: Bypass 120 requests per session limit check (without code optimization). Idea is so simple, generate the list of reusable sessions (from same ip) and brute pwd of the last. http://pastebin.com/8f66dt7h
ReplyDeleteAn other solution that recover the password in 30 requests only:
ReplyDeleteThe idea is to do a time based sql injection with a different delay for each letter:
a-> wait 1 sec
b-> wait 2 sec
...
http://pastebin.com/wew16zT7
TRL [Dulac]