// HackTheBox  ·  writeup

Silent Drain

HackTheBox Medium WiresharktsharkxxdPython
DFIR

Scenario

SilentDrain is a network forensics lab where the evidence lives in a single PCAP: DNS queries, JSON-RPC calls, and a short chain of transactions that reveal how the leak happened and how the drain was executed. Players must reconstruct the timeline, identify the key entities involved, and validate what happened using only network and on-chain artifacts using tools such as Wireshark, Tshark, and a log analyzer.

Executive Summary

A development secret left the environment as base32 labels in DNS queries to an external domain. Shortly afterwards, plain-HTTP JSON-RPC traffic to a node on TCP 8545 showed a wallet being sized up with balance and nonce queries, then a signed raw transaction that moved almost its entire balance to a second address. A second transaction reusing the same nonce was submitted as a replacement attempt, but the first one was mined, and the node’s receipt fixes the block, gas and final balances. A separate legacy transaction carried a short marker in its call data. Everything was recovered from the PCAP alone.

Artifacts

Format note: There is no JSON-RPC dissector for this traffic, so the Ethereum fields have to be pulled from the TCP payload with tshark -T fields -e tcp.payload and xxd -r -p. Every RPC number is a hex quantity. Convert it before answering, and use integer or Decimal arithmetic for wei to ETH so no digits are lost.

Exfiltration

Q13 — What secret was exfiltrated via DNS queries (decoded plaintext)?

List every unique query name first. One parent domain, mmoxguard.com, receives queries with long random-looking subdomain labels, and the leftmost labels are numbered c0, c1 and so on: chunks of one base32 string. Concatenate the chunks in order, pad to a multiple of 8, and base32-decode. The plaintext is a key name and its value, matching the NAME:value shape of the answer mask.

tshark -r SilentDrain.pcap -Y dns -T fields -e dns.qry.name | sort -u
python3 -I -c "import base64,sys; s=sys.argv[1]+sys.argv[2]; print(base64.b32decode(s+'='*(-len(s)%8)).decode())" <c0-label> <c1-label>

Tip: Base32 on the wire is usually unpadded and case-insensitive. Restore the = padding or the decoder rejects it.

Click flag to reveal DEV_PRIV_KEY:zerotrust_secret_key

Impact

Q1 — What is the victim address?

Dump the TCP 8545 payloads and read the JSON-RPC calls in order. The recon calls (eth_getBalance, eth_getTransactionCount) all target one address, and the receipt for the mined transaction names the same address as its sender. That is the wallet being drained.

tshark -r SilentDrain.pcap -Y "tcp.port==8545 && tcp.len>0" -T fields -e tcp.payload
Click flag to reveal 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
Q2 — What is the attacker address (recipient of the drain)?

The drain travels in eth_sendRawTransaction. Decode the RLP of the EIP-1559 (type 0x02) transaction: chain ID 0x0539, nonce 0x00, the two fee caps, gas limit, then the to field. That recipient is the attacker. The raw hex is lower-case, so take the checksummed form of the address from how it appears elsewhere in the RPC traffic.

tshark -r SilentDrain.pcap -Y "http.request.method==POST && tcp.port==8545" -T fields -e tcp.payload | xxd -r -p | grep -a sendRawTransaction
Click to reveal answer 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
Q3 — What was the victim's balance before the drain (in ETH)?

The eth_getBalance response for the victim, taken before the send, is a hex wei quantity. Divide by 10^18 to get ETH.

python3 -I -c "print(0x3635c9adc5dea00000 / 10**18)"
Click flag to reveal 1000
Q6 — What nonce did the victim's draining transaction use?

Two places agree: the eth_getTransactionCount result for the victim before the send, and the nonce field in the decoded raw transaction, the single byte after the chain ID. It is the wallet’s first outgoing transaction.

tshark -r SilentDrain.pcap -Y "tcp.port==8545 && tcp.len>0" -T fields -e tcp.payload | xxd -r -p | grep -a getTransactionCount
Click to reveal answer 0
Q5 — Paste the full raw hex of the drain transaction.

The params array of the eth_sendRawTransaction request is the signed transaction. It starts 0x02f8, the type-2 envelope, so a regex on that prefix pulls it straight out of the decoded payload.

tshark -r SilentDrain.pcap -Y "tcp.port==8545 && tcp.len>0" -T fields -e tcp.payload | xxd -r -p | grep -aoE '0x02f8[0-9a-f]+'

Tip: Copy it from the command output. Retyping a 200-character hex string is where this goes wrong.

Click flag to reveal 0x02f87582053980843b9aca008506fc23ac00825208943c44cdddb6a900fa2b585dd299e03d12fa4293bc893635c36d0fefd2000080c001a0498272e16215b2ce611a17472b78019e5a555cac70064d3e6572d2341623dcf69fb357bcbb6d75272a1fd19c304bd3fc40081421425fc0253420933263793cae
Q4 — Provide the transaction hash of the drain operation.

The node answers eth_sendRawTransaction with the transaction hash. A later eth_getTransactionReceipt for that hash returns status 0x1, so the transfer was mined successfully. Filter the responses for 32-byte hex results.

tshark -r SilentDrain.pcap -Y "tcp.port==8545 && tcp.len>0" -T fields -e tcp.payload | xxd -r -p | grep -aE '"result":"0x[0-9a-f]{64}"'
Click to reveal answer 0x1a6ff040221eb211e55a20a033c25a77d21e3a3f2fa917f7eb37722dc6c7c2b4
Q7 — In which block number was the drain transaction mined?

The receipt returned for the Q4 hash carries blockNumber as a hex quantity.

tshark -r SilentDrain.pcap -Y "tcp.port==8545 && tcp.len>0" -T fields -e tcp.payload | xxd -r -p | grep -a blockNumber
Click flag to reveal 4
Q8 — What was gasUsed for the drain transaction (in gas units)?

Same receipt, gasUsed is 0x5208, the fixed cost of a plain value transfer.

python3 -I -c "print(0x5208)"
Click to reveal answer 21000
Q9 — What was the effectiveGasPrice (in wei)?

Read effectiveGasPrice from the receipt, not the fee fields in the signed transaction.

python3 -I -c "print(0x5e8b3609)"

Gotcha: The raw transaction carries a max fee and a priority fee, but the price actually paid depends on the block’s base fee. Only the receipt has it, and it is lower than the cap.

Click flag to reveal 1586181641
Q10 — Exactly how many ETH were transferred to the attacker?

The value field of the raw transaction from Q5 is 0x3635c36d0fefd20000 wei, just under the full balance. Convert with Decimal so no digits are rounded away.

python3 -I -c "from decimal import Decimal; print(Decimal(0x3635c36d0fefd20000) / Decimal(10**18))"
Click to reveal answer 999.99824
Q11 — What is the victim's balance after the drain (in ETH)?

A second eth_getBalance call after the block is mined returns 0x6226a63256db8 wei. It reconciles with the earlier answers: starting balance, minus the transferred value (Q10), minus gas used times effective gas price (Q8 and Q9).

python3 -I -c "from decimal import Decimal; print(Decimal(0x6226a63256db8) / Decimal(10**18))"
Click flag to reveal 0.001726690185539
Q12 — Give the transaction hash of the replacement (same-nonce) attempt and the hash of the mined transaction.

Two eth_sendRawTransaction calls use the same nonce. Add the frame number to the dump to see their order: the one sent second is the replacement attempt, and the first is the mined transaction from Q4. Give the replacement hash first, then the mined one.

tshark -r SilentDrain.pcap -Y "tcp.port==8545 && tcp.len>0" -T fields -e frame.number -e tcp.payload | xxd -r -p | grep -a sendRawTransaction

Gotcha: The format mask lists the replacement first. The mined hash is the one with a receipt, so it is easy to put it first by reflex.

Click to reveal answer 0xce871cd24d018b6baa8dd12e127f91aefb80134c7580cc2bc16602b97bb47f78,0x1a6ff040221eb211e55a20a033c25a77d21e3a3f2fa917f7eb37722dc6c7c2b4
Q14 — What 4-byte marker appears in the call calldata (exact hex string)?

Besides the type-2 transfer, the capture holds a legacy transaction sent to an address beginning 0x663f. Its data field is only four bytes: a marker, not a function call with arguments. Search the decoded payload for that prefix and read the data straight from the transaction.

tshark -r SilentDrain.pcap -Y "tcp.port==8545 && tcp.len>0" -T fields -e tcp.payload | xxd -r -p | grep -ai 663f
Click to reveal answer deadbeef

Attack Summary

PhaseAction
ExfiltrationDevelopment private key sent out as numbered base32 labels in DNS queries to mmoxguard[.]com
ImpactVictim balance and nonce queried over unauthenticated JSON-RPC on TCP 8545
ImpactSigned type-2 transaction sent with eth_sendRawTransaction, moving nearly the full balance to the attacker address
ImpactSecond transaction with the same nonce submitted as a replacement attempt
ImpactFirst transaction mined in block 4 as a 21000 gas transfer
ImpactPost-drain balance query shows only dust left
ImpactLegacy transaction with a four-byte marker in its call data

IOCs

TypeValue
Domainmmoxguard[.]com
Wallet (victim)0x70997970C51812dc3A010C7d01b50e0d17dc79C8
Wallet (attacker)0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
Tx hash (mined drain)0x1a6ff040221eb211e55a20a033c25a77d21e3a3f2fa917f7eb37722dc6c7c2b4
Tx hash (replacement)0xce871cd24d018b6baa8dd12e127f91aefb80134c7580cc2bc16602b97bb47f78
Calldata markerdeadbeef

MITRE ATT&CK

TechniqueIDDescription
Exfiltration Over Alternative Protocol: Exfiltration Over Unencrypted Non-C2 ProtocolT1048.003Secret chunked into base32 DNS labels
Financial TheftT1657Wallet drained to an attacker address with a signed raw transaction

Defender Takeaways

DNS is an exfiltration channel, and the labels give it away. Long high-entropy subdomains to one unfamiliar parent domain, split into numbered chunks, is a clear signature. Alert on label length and entropy per parent domain, and sinkhole unknown domains from any system that holds keys.

A development key should never control real funds. The leaked secret was named as a dev key yet unlocked a wallet holding 1000 ETH. Keep dev and production keys separate, and put large balances behind multi-signature or hardware-backed signing.

Unauthenticated JSON-RPC on 8545 exposes everything. The balance checks, the send and the receipts all crossed the network as clear HTTP. Bind nodes to localhost or require authentication and TLS, and log the source of every eth_sendRawTransaction.

Same-nonce replacements are worth an alert. A second transaction reusing the first one’s nonce is an attempt to override a pending transfer. Any monitoring that sees mempool or RPC traffic can flag it.

Conclusion

The attacker leaked a development key through DNS and used it to move almost the whole 1000 ETH balance in a single mined transaction, all visible in one PCAP. The sharpest lesson is that a “dev” secret with production funds behind it turns a quiet DNS leak into a total loss.