// HackTheBox  ·  writeup

Python_Stealer

HackTheBox Hard unzipcatgrep
Malware Analysis

Scenario

You are a security analyst tasked with analyzing a Python script that is suspected to be a malicious stealer.

Executive Summary

The sample was a single plaintext Python stealer, so the whole analysis was a static read of the source with nothing executed. The script decrypts the browser’s master key through the Windows DPAPI and uses it with AES to open stored secrets, then harvests Discord tokens by regex from local storage files. It also looks up the victim’s public IP, and it fetches its exfiltration webhook at runtime from an externally hosted page rather than hardcoding it, parsing the HTML with a spoofed browser User-Agent. Stolen data leaves as a Discord webhook post that carries a custom avatar image and the author’s signature.

Artifacts

Format note: Each task shows a masked format hint. For the User-Agent, URL and regex answers the mask matches the literal string in the source, so copy it exactly as it appears.

Resource Development

Q3 — What is the name of the author of the code?

Extract the archive and read the header of stealer.py. The author signs the file with a credit line in a comment and the same handle comes back in the embedded webhook payload, so a grep for Made by lands on it directly. The format mask is six characters.

unzip -P infected danger.zip -d work/sample
grep -n "Made by" work/sample/stealer.py
Click flag to reveal Astraa

Credential Access

Q1 — What is the function that is used to decrypt the master key?

Read the decrypt() routine in stealer.py. The browser’s Local State key is stored DPAPI-protected, so the script calls the Windows API that unwraps it for the current user. That call is the master-key decryption step, and the 18-character mask fits the Windows API name.

cat work/sample/stealer.py
Click to reveal answer CryptUnprotectData
Q2 — What is the mode that is used by the AES algorithm?

From Q1 the master key is the AES key, so look at where decrypt() builds the cipher object. The AES.new call passes a mode constant, which is the authenticated mode used by Chromium for its encrypted values. The three-character mask matches the mode name.

grep -n "AES" work/sample/stealer.py
Click flag to reveal GCM
Q6 — What is the name of the function that is used to collect data from the browser?

Follow the call graph from the main routine. One function walks the browser and Discord storage paths and returns the harvested values, and its name is a verb and noun joined by an underscore (mask ***_*****). That is the collection entry point that feeds the exfiltration step.

grep -n "def " work/sample/stealer.py
Click to reveal answer get_token
Q9 — What is the regex that is used to dump tokens?

Inside the Q6 function the script scans the leveldb and log files and runs re.findall on the contents. Encrypted tokens start with a fixed prefix followed by base64 text, and the pattern also carries odd bracket and anchor characters. The mask ***********:[^.*\['(.*)'\].*$][^\"]* already shows the tail, so copy the whole pattern from the findall call.

grep -n "findall" work/sample/stealer.py

Tip: The pattern looks mangled because it is a character class, not a real capture group. Copy it byte for byte rather than retyping it.

Click flag to reveal dQw4w9WgXcQ:[^.*\['(.*)'\].*$][^\"]*

Discovery

Q8 — What is the function name that is used to retrieve IP address?

The payload sent to the webhook includes the victim’s public IP, so a small helper requests it from an external service. It is the five-character function defined near the top of the script.

grep -n "def " work/sample/stealer.py
Click to reveal answer getip

Command and Control

Q5 — What is the hooked URL?

The script does not hardcode its webhook. The get_webhook_url routine requests a page on an externally hosted site and reads the webhook out of it. The hooked URL is the address of that page, a randomly named subdomain on a free static-hosting platform (mask *****://******************.*******.***).

grep -n -A8 "def get_webhook_url" work/sample/stealer.py
Click flag to reveal https://fhrkvjbdhfdobdkjvh.netlify.app
Q7 — What is the ID paragraph element that allows an attacker to parse HTML?

From Q5 the page is fetched and parsed with BeautifulSoup. The webhook sits in a <p> element selected with soup.find("p", id=...), so the ID passed there is the answer. It is a nine-character random string, which means the operator can rotate the webhook by editing the page and never touch the deployed script.

grep -n "soup.find" work/sample/stealer.py
Click flag to reveal djhjvsdbk
Q4 — What is the UserAgent in the code?

The request in get_webhook_url sends a header dictionary (headers2) with a hardcoded User-Agent. It is an old Chrome 23 string on Linux, which does not match the Windows victims the rest of the script targets. That mismatch is a cheap network indicator for the page fetch.

grep -n -i "user-agent" work/sample/stealer.py
Click to reveal answer Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11

Exfiltration

Q10 — What is the avatar_url ?

Find the JSON payload that the script posts to the webhook. Besides the content, it sets a custom avatar_url so the messages show up in the Discord channel with the author’s branding. The value is a Discord CDN attachment link ending in an image filename.

grep -n "avatar_url" work/sample/stealer.py
Click to reveal answer https://cdn.discordapp.com/attachments/826581697436581919/982374264604864572/atio.jpg

Attack Summary

PhaseAction
Resource DevelopmentOperator ships a single Python stealer signed with a handle
Command and ControlScript fetches an externally hosted page with a spoofed User-Agent and parses the webhook from a <p> element
Discoverygetip looks up the victim’s public IP
Credential AccessBrowser master key unwrapped with DPAPI, then AES-GCM decryption of stored secrets
Credential AccessDiscord tokens harvested from local files with a regex
ExfiltrationData posted to the Discord webhook with a custom avatar

IOCs

TypeValue
URL (webhook resolver)hxxps[://]fhrkvjbdhfdobdkjvh[.]netlify[.]app
HTML element IDdjhjvsdbk
User-AgentMozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11
URL (avatar)hxxps[://]cdn[.]discordapp[.]com/attachments/826581697436581919/982374264604864572/atio[.]jpg
Function namesdecrypt, get_token, getip, get_webhook_url
Author handleAstraa

MITRE ATT&CK

TechniqueIDDescription
Credentials from Web BrowsersT1555.003Browser master key unwrapped with CryptUnprotectData, secrets decrypted with AES-GCM
Steal Application Access TokenT1528Discord tokens pulled from local storage by regex
System Network Configuration DiscoveryT1016getip retrieves the victim’s public IP
Web Service: Dead Drop ResolverT1102.001Webhook fetched at runtime from a page on a static hosting platform
Application Layer Protocol: Web ProtocolsT1071.001HTTP fetch with a spoofed Linux Chrome User-Agent
Exfiltration Over Web Service: Exfiltration Over WebhookT1567.004Stolen data posted to a Discord webhook

Defender Takeaways

Stealers that resolve their webhook at runtime defeat static blocklists. The Discord webhook is not in the script, only the hosting page is. Block or alert on requests to throwaway subdomains on free static-hosting platforms from non-browser processes, and treat the page URL as the pivot to find the webhook.

A User-Agent that does not match the host OS is a free detection. The script claims Chrome 23 on Linux while running on Windows desktops. Proxy logs with a User-Agent versus endpoint OS check will surface it.

Discord webhook traffic from a Python process is not normal. Alert on discord.com/api/webhooks and cdn.discordapp.com connections from python.exe or from unsigned binaries, particularly right after a read of browser profile directories.

DPAPI-protected browser keys fall to any code running as the user. CryptUnprotectData needs no privilege beyond the logged-in account, so the control is keeping untrusted scripts from running at all and moving credentials out of browser stores and into a managed vault.

Conclusion

This stealer is small and unobfuscated, yet it covers the whole theft chain: unwrap the browser key, harvest tokens, geolocate the victim, and exfiltrate over a trusted chat service. The one design choice that matters for defenders is the externally hosted webhook, because it turns one hosting-page URL into the thread that unravels the operation.