Artifact Triage
Grabbed the malicious artifacts in an isolated sandbox. Safety first.
I feel like a kid on Halloween with a bag full of unknown delights.
Two files:
Powershell-Operational.evtx: Windows event log, likely containing Jim’s malicious scripttraffic.pcapng: the motherload. Network capture with C2 traffic, payload delivery, and encrypted commands
Wrestling python-evtx into Submission
Before I could read the log, I had to actually get python-evtx to work. Three tries, three different failures:
python3 -m evtx→ module not foundpython-evtxvia pip → broken binary from package conflictevtxpackage via pip →PyEvtxParserdoesn’t support the context manager protocol
Determined. We will figure this out.
Two conflicting packages had both installed an evtx_dump binary and they were stepping on each other’s toes. Bypassed them with direct Python, but the first attempt used a with statement that PyEvtxParser can’t handle:
python3 -c "
import evtx
with evtx.PyEvtxParser('/home/jenn/Masquerade/attachments/dist/Powershell-Operational.evtx') as p:
for r in p.records_json():
print(r)
" > ps_log.txt
Now, we flip the evtx dumpster over. Why? Because it looked at me the wrong way… or that’s just proper handling protocol.
Dropped the with statement, called the parser directly:
python3 -c "
import evtx
p = evtx.PyEvtxParser('/home/jenn/Masquerade/attachments/dist/Powershell-Operational.evtx')
for r in p.records_json():
print(r)
" > ps_log.txt
Quick verification that this messochism tango was over:
wc -l ps_log.txt
Log dumped. Now we rage. Onwards.
Deobfuscating the PowerShell
A quick grep for ScriptBlockText unveiled malicious PowerShell in event_record_id 6. The attacker split strings to evade detection, but jokes on you, Jim. They rejoin cleanly.
Q1. External domain. Join 'api-edg' + 'e' + 'cl' + 'oud.xy' + 'z' → api-edgecloud.xyz
Q2. Encryption algorithm. Looking at the loop structure, my memory got a ping from something I learned in my Sec+ 1 course. $s = 0..255 KSA with $j = ($j + $s[$i] + $k[$i % $k.Count]) % 256, then PRGA and XOR. That’s literally textbook RC4.
Q3. Decryption key. Join 'X9vT3pL' + '2QwE' + '8xR6' + 'ZkYhC4' + 's' → X9vT3pL2QwE8xR6ZkYhC4s
What the Event Log Told Us
Event ID 4104 is script block logging. It fires when PowerShell executes a block and logs the content. A SIEM rule detecting 4104 events containing DownloadString, WebClient, and -join in the same block would have caught this. Can’t save the world.
The re-joined string that answered Q3 was the giveaway. Too bad, Jim.
The script downloads a payload, RC4-decrypts it in memory, writes it to $env:TEMP, then launches it via Start-Process. Writing to TEMP and immediately executing is a high-fidelity behavioral indicator. If there had been an EDR in place, it would have been making itself heard.
.xyz domains are typically another red-alert warning, and api-edgecloud.xyz has no legitimate business being in this context. Shame on the DNS reputation feed or proxy that didn’t block and alert on it.
A huge round of applause for whomever enabled script block logging. Without it, the evtx would be empty and we’d have only the PCAP to work with.
But I digress. On to the PCAP.
Pivoting to the PCAP
An HTTP-only filter came back inconclusive. A mixed HTTP/TLS protocol dump did the trick:
tshark -r traffic.pcapng -T fields -e ip.dst -e tcp.dstport -e _ws.col.Protocol 2>/dev/null | sort -u
Two IPs on Port 80 had C2 auras: 34.174.57.99 and 34.174.85.91. The 443s were likely regular Windows telemetry.
tshark -r traffic.pcapng -Y "ip.addr == 34.174.57.99 || ip.addr == 34.174.85.91" 2>/dev/null
Aha! Got ya. Two separate C2 roles:
34.174.85.91: deliveredamd.bin(the payload) → Q4 and Q534.174.57.99: C2 server receiving beacons viaGET /images?guid=<base64>→ Q6, Q7, and Q8
That giant base64 blob in the guid parameter is the encrypted C2 traffic.
Timestamp and Payload Extraction
Q4. Server response timestamp. Pulled the HTTP Date header off the 200 OK response from 34.174.85.91:
tshark -r traffic.pcapng -Y "ip.src == 34.174.85.91 && http" -T fields -e http.date 2>/dev/null
Q4 Answer: Fri, 10 Apr 2026 05:28:23 GMT
Q5. SHA-256 of the decrypted payload. Exported HTTP objects:
tshark -r traffic.pcapng -Y "ip.src == 34.174.85.91" --export-objects "http,/tmp/payload_extract" 2>/dev/null
amd.bin confirmed as the payload. The %2f files are fragments from C2 beacon traffic. Ignore those.
RC4 is a symmetric stream cipher. Same key encrypts and decrypts. OpenSSL should do this in one step:
openssl enc -rc4 -nosalt -nopad -K $(echo -n "X9vT3pL2QwE8xR6ZkYhC4s" | xxd -p) \
-in /tmp/payload_extract/amd.bin -out /tmp/payload_decrypted.bin
Or not, but that’s okay. We have other tools in the box. OpenSSL’s RC4 implementation truncates keys shorter than my 22-character key.
Python RC4 on the raw binary:
python3 -c "
key = b'X9vT3pL2QwE8xR6ZkYhC4s'
data = open('/tmp/payload_extract/amd.bin','rb').read()
s = list(range(256)); j = 0
for i in range(256):
j = (j + s[i] + key[i % len(key)]) % 256
s[i], s[j] = s[j], s[i]
i = j = 0; out = []
for byte in data:
i = (i + 1) % 256
j = (j + s[i]) % 256
s[i], s[j] = s[j], s[i]
out.append(byte ^ s[(s[i] + s[j]) % 256])
open('/tmp/payload_decrypted.bin','wb').write(bytes(out))
"
Foiled myself again. Something still off. Back to the script block.
Third attempt. xxd revealed the file was hex-encoded ASCII, not raw encrypted bytes:
xxd /tmp/payload_extract/amd.bin | head -3
Printable ASCII hex. Hex-decode first, then RC4 decrypt:
python3 -c "
raw = open('/tmp/payload_extract/amd.bin','rb').read().decode().strip()
data = bytes.fromhex(raw)
key = b'X9vT3pL2QwE8xR6ZkYhC4s'
s = list(range(256)); j = 0
for i in range(256):
j = (j + s[i] + key[i % len(key)]) % 256
s[i], s[j] = s[j], s[i]
i = j = 0; out = []
for byte in data:
i = (i + 1) % 256
j = (j + s[i]) % 256
s[i], s[j] = s[j], s[i]
out.append(byte ^ s[(s[i] + s[j]) % 256])
open('/tmp/payload_decrypted.bin','wb').write(bytes(out))
"
file /tmp/payload_decrypted.bin
sha256sum /tmp/payload_decrypted.bin
Valid PE32 .NET executable. Good job, Jenn.
Q5 Answer: e3d39d42df63c6874780737244370ba517820f598fd2443e47ff6580f10c17cb
Decompiling the .NET Client
Q6. Remote URL. Strings with UTF-16LE encoding on the decrypted PE32:
strings -e l /tmp/payload_decrypted.bin | grep -i "http\|images\|34.174"
Q6 Answer: http://34.174.57.99/images?guid=
Q7. Encryption key and algorithm. Same approach, different grep:
strings -e l /tmp/payload_decrypted.bin | grep -iv "http\|system\|microsoft\|object\|string"
Key surfaced: M4squ3r4d3Th3P4ck3tSt34lthM0d31337. The key name itself is basically tech-pig-Latin for “Masquerade The Packet Stealth Mode 1337,” giving custom XOR or AES vibes.
Algorithm confirmation needed a proper .NET decompile. Installed dnfile:
pip install dnfile --break-system-packages
python3 -c "
import dnfile
pe = dnfile.dnPE('/tmp/payload_decrypted.bin')
for s in pe.net.mdtables.MethodDef:
print(s.Name)
"
AES confirmed. CreateAesKey right there in the method list. My spidey-senses said the key string was almost certainly either MD5-hashed into a 16-byte AES-128 key, or SHA-256-hashed into a 32-byte AES-256 key.
Grabbed RVAs and peeked at the bytecode. The CIL was too dense AF to read manually. Pivot: extract the C2 response bodies from the PCAP and brute the AES mode.
Decrypting the C2 Traffic
The tell-tale <!-- {0} --> template means ciphertext lives inside HTML comments. Not so sneaky.
Raw stream was also dense AF, so I exported HTTP objects instead:
tshark -r traffic.pcapng -Y "ip.src == 34.174.57.99" --export-objects "http,/tmp/c2_extract" 2>/dev/null
grep -o '<!--.*-->' /tmp/c2_extract/%2f
Response bodies wrapped in <!-- oldcss=<base64> -->. “Feeling lucky,” indeed. First encrypted command: LQPZY0C4ZPwZD8K0sFRzQKtP8l0NE35v/EzXkc0lU0Q=
First attempt. MD5 hash of the key string, extract IV from first 16 bytes of ciphertext, AES-CBC decrypt:
python3 -c "
import hashlib, base64
from Crypto.Cipher import AES
key = hashlib.md5(b'M4squ3r4d3Th3P4ck3tSt34lthM0d31337').digest()
ct = base64.b64decode('LQPZY0C4ZPwZD8K0sFRzQKtP8l0NE35v/EzXkc0lU0Q=')
iv = ct[:16]
cipher = AES.new(key, AES.MODE_CBC, iv)
print(cipher.decrypt(ct[16:]))
"
Nada. MD5 produced garbage. The first beacon response was just nothing anyway. The attacker hadn’t issued a command yet. The actual commands lived in the numbered files.
Pivot to SHA-256 with a zero IV, looped across every %2f* file:
for f in /tmp/c2_extract/%2f*; do
echo "=== $f ===";
grep -o '<!-- oldcss=.*-->' "$f" | grep -o 'oldcss=.*-->' | sed 's/oldcss=//;s/ -->//' | python3 -c "
import hashlib, base64, sys
from Crypto.Cipher import AES
key = hashlib.sha256(b'M4squ3r4d3Th3P4ck3tSt34lthM0d31337').digest()
ct = base64.b64decode(sys.stdin.read().strip())
iv = b'\x00'*16
cipher = AES.new(key, AES.MODE_CBC, iv)
print(cipher.decrypt(ct))
" 2>/dev/null
done
WIN. Flag in %2f(9).
Let’s rehash (pun intended) the threat actor’s command sequence:
whoami /all: reconnaissance and privilege checkipconfig /all: network enumerationecho THM{...}: flag, aka buried treasure
The DESKTOP-I6C5C7M:::: prefix is the hostname beacon the client prepended to each response before encrypting and sending back. Tsk, tsk. The nothing entries are idle beacons from before any commands had been issued.
Final derivation: SHA-256 of the key string → 32-byte AES-256 key. Base64-decode the ciphertext, AES-256-CBC decrypt with a zero IV. Plaintext command falls out.
Q8 Answer: THM{m45k3d_tr4ff1c_0v3r_c0v3rt_ch4nn3lz}
Full Attack Chain
Spearphish → Jim runs malicious .ps1 → obfuscated PowerShell
→ split-string reconstruction reveals api-edgecloud.xyz + RC4 key
→ HTTP GET pulls hex-encoded RC4-encrypted payload from 34.174.85.91
→ Hex-decode → RC4 decrypt → PE32 .NET executable (amdfendrsr.exe in TEMP)
→ .NET C2 client beacons to 34.174.57.99 via GET /images?guid=<base64>
→ CreateAesKey: SHA-256 of hardcoded string = 32-byte AES-256 key
→ Commands returned in <!-- oldcss=<base64> --> HTML comment template
→ AES-256-CBC decrypt with zero IV → whoami /all, ipconfig /all, flag