// HackTheBox  ·  writeup

OpTinselTrace-2

HackTheBox Easy Pythongrepgit
DFIRCloud

Scenario

It seems our precious technology has been leaked to the threat actor. Our head Elf, PixelPepermint, seems to think that there were some hard-coded sensitive URLs within the technology sent. Please audit our Sparky Cloud logs and confirm if anything was stolen! PS - Santa likes his answers in UTC…

Executive Summary

A binary carrying a hard-coded S3 location led the threat actor to a publicly readable bucket of internal scripts. The bucket was emptied in seconds by a scripted Python client, and the repository’s git history gave up AWS access keys that had been committed and later removed. Those keys belonged to an IAM user, and a second external address used them to list and read a private bucket, retrieving a VPN profile. Everything after the first lead is visible in CloudTrail and in the repository the actor downloaded.

Artifacts

Timestamp note: CloudTrail eventTime is already UTC, which is what Santa asks for. Reformat to YYYY-MM-DD hh:mm:ss.

Discovery

Q1 — What is the MD5 sum of the binary the Threat Actor found the S3 bucket location in?

Nothing in this case’s evidence answers this: CloudTrail records API calls, not the file that told the actor where to look, and a search for hashes in the merged log comes back empty. The binary is the malware from the phishing emails in the preceding case, OpTinselTrace-1. Its strings contain the papa-noel bucket URL, which is how the actor learned the bucket’s location, and its MD5 is the answer.

grep -i md5 all.json

Gotcha: This one is a cross-case pivot. Hunting for the hash inside the Sparky Cloud logs goes nowhere, so carry the sample over from the earlier Sherlock.

Click flag to reveal 62d5c1f1f9020c98f97d8085b9456b05

Collection

Q2 — What time did the Threat Actor begin their automated retrieval of the contents of our exposed S3 bucket?

Merge the CloudTrail files into a single JSON document, then filter GetObject events against the papa-noel bucket. One external IP, 191.101.31.57, issues them back to back with a python-requests user agent. The earliest event in that run marks the start of the automated pull.

python3 -I work/all.py
grep GetObject all.json | grep papa-noel
Click to reveal answer 2023-11-29 08:24:07
Q3 — What time did the Threat Actor complete their automated retrieval of the contents of our exposed S3 bucket?

Same filter as Q2, read from the other end of the run. The last event is the GetObject for santa-list.csv, nine seconds after the first.

grep GetObject all.json | grep papa-noel
Click flag to reveal 2023-11-29 08:24:16
Q4 — Based on the Threat Actor's user agent - what scripting language did the TA likely utilise to retrieve the files?

The userAgent on every bulk GetObject is python-requests/2.25.1, the default header of the Python requests library. A person in a browser or the AWS CLI would present a different agent, which is why the uniform value fits a script.

grep -o 'python-requests/[0-9.]*' all.json | sort | uniq -c

Hint: The format mask *****n is the language name, six letters ending in n.

Click to reveal answer python

Credential Access

Q5 — Which file did the Threat Actor locate some hard coded credentials within?

Search the downloaded NPoleScripts repository for AWS access key IDs, then walk the history. The credentials were removed from one script in a later commit, so the old values are still readable in the diff.

grep -ri 'AKIA' NPoleScripts/
git log -p

Gotcha: The working tree is clean. The keys only show up in the git history, where the commit that removed them still holds the old values.

Click flag to reveal claus.py

Exfiltration

Q6 — Please detail all confirmed malicious IP addresses. (Ascending Order)

Pivot on the IAM user whose keys leaked and group its events by source address. Two addresses issue ListObjects and GetObject calls as elfadmin, and they are the answer. The logs hold more external IPs, but they are scanners that touched the public bucket and have no tie to the stolen keys or the bulk download.

python3 -I -c "import json,collections; d=json.load(open('all.json')); print(collections.Counter(e['sourceIPAddress'] for e in d if e.get('userIdentity',{}).get('userName')=='elfadmin'))"

Gotcha: The mask shows exactly two addresses. Padding the list with scanner IPs, or with any other external address in the logs, is wrong however suspicious it looks.

Click to reveal answer 45.133.193.41, 191.101.31.57
Q7 — We are extremely concerned that the threat actor managed to compromise our private S3 bucket, which contains an important VPN file. Confirm the name of this VPN file and the time it was retrieved by the threat actor.

From Q6, filter GetObject against north-pole-private. The call comes from 45.133.193.41 as elfadmin and names the VPN profile in the request parameters. Its eventTime is the retrieval time.

grep GetObject all.json | grep north-pole-private
Click flag to reveal bytesparkle.ovpn, 2023-11-29 10:16:53
Q8 — Please confirm the username of the compromised AWS account?

The userIdentity.userName field on the private bucket calls from Q7 names the IAM user whose keys leaked in Q5. Counting the field across the log shows it is the only named user behind those calls.

grep -o '"userName": "[a-z]*"' all.json | sort | uniq -c

Hint: The mask *******n is eight letters ending in n.

Click to reveal answer elfadmin

Remediation

Q9 — Based on the analysis completed Santa Claus has asked for some advice. What is the ARN of the S3 Bucket that requires locking down?

The bucket to lock down is the public one that held the git repository and started the chain, not the private bucket, which only fell to the leaked keys. Take the name from the Q2 events and write it as an S3 ARN. This was reasoned from the attack path rather than read from an artifact, and HTB accepted it.

grep GetObject all.json | grep papa-noel

Hint: An S3 bucket ARN has no region or account ID, which matches the 22-character mask.

Click flag to reveal arn:aws:s3:::papa-noel

Attack Summary

PhaseAction
DiscoveryActor learns the bucket location from strings in a binary
CollectionScripted Python client downloads the whole public bucket in nine seconds
Credential AccessAWS keys recovered from the git history of one script
DiscoveryStolen IAM user lists objects in the private bucket from a second IP
CollectionVPN profile retrieved from the private bucket

IOCs

TypeValue
IP45.133.193[.]41
IP191.101.31[.]57
User agentpython-requests/2.25.1
IAM userelfadmin
Filebytesparkle.ovpn
Hash (MD5)62d5c1f1f9020c98f97d8085b9456b05

MITRE ATT&CK

TechniqueIDDescription
Data from Cloud StorageT1530Bulk download of the public bucket and the VPN profile
Cloud Storage Object DiscoveryT1619ListObjects against the private bucket
Unsecured Credentials: Credentials In FilesT1552.001AWS keys committed in a script
Valid Accounts: Cloud AccountsT1078.004Leaked IAM keys used from a second IP
Automated CollectionT1119Scripted retrieval of every object in the bucket

Defender Takeaways

A public bucket should never hold source. The repository was readable by anyone and was fetched in nine seconds. Block public access at the account level and alert on GetObject bursts from a single external IP.

Deleting a secret from a script does not remove it. The AWS keys were gone from the current file but intact in git history. Rotate any key that was ever committed and scan history with a secret scanner before publishing a repo.

Scope IAM keys tightly. One leaked user reached a private bucket from an unfamiliar IP. Restrict keys by source IP or role, and alert on a long-lived user key used from a new address.

Hard-coded locations in binaries leak your cloud layout. The bucket name sat in the strings of a malicious binary, so anything shipped or received carries it. Keep bucket names out of distributed tooling, and keep samples alongside CloudTrail so the first lead can be closed.

Conclusion

A public bucket leaked a repository, the repository leaked keys, and the keys opened a private bucket holding a VPN profile. One exposed bucket was enough to start the whole chain.