In this Sherlock, you will analyze web server logs to identify suspicious access to the development environment and review network traffic to identify unauthorized Hardhat transactions. This investigation will reveal how an insider exploited Hardhat to steal customer funds without directly attacking production systems.
An insider abused a path traversal in the ReachKart development server to read arbitrary files, starting with /etc/passwd and moving on to the application’s own source and package.json. The leaked source exposed the JWT signing secret, which let the attacker forge an admin token and walk into the admin panel. The panel’s WebSocket terminal gave them a shell, and from it they served the SQLite customer database over a throwaway Python web server and pulled it down with wget. The database identified every seller and their wallet, and the attacker then drained all of them to a single address through the Hardhat JSON-RPC node, finishing with a balance check on their own wallet.
grep and awktsharkTimestamp note: The capture’s frame times carry a +11 offset. The access log is UTC, and every answer here is UTC. Convert before submitting.
Triage the log for traversal sequences. A single route accepts a template parameter, and the attacker’s requests against it all carry ../ chains. The pcap agrees: the HTTP responses to that route contain file contents instead of an order bill.
grep -F 'template=../' access.log | awk '{print $7}' | sort | uniq -c | sort -rn
From Q1 we know which route to filter on. Early traversal attempts fail, so the first one that counts is the first with a 200 response: a read of /etc/passwd using a long ../ chain.
grep -F 'template=../' access.log | grep ' 200 ' | head -1
Gotcha: Take the first
200, not the first traversal request. Earlier attempts hit the endpoint but did not return the file.
The attacker used the traversal to read the project’s package.json, and the response is in the capture. List the HTTP conversations, then follow the stream that answers the getOrderBill request for it (stream 42) and read the dependencies block.
tshark -r network-traffic.pcap -q -z follow,tcp,ascii,42
Note: The file lists a caret range (
^). The answer is the version number inside it.
The same package.json from Q3 lists the dev chain package. The packet capture never contains a Hardhat clientVersion response, so the version comes from the dependency range rather than a banner. This answer was reasoned from the file, not read from a running node. Grepping the 8545 JSON for HardhatNetwork confirms that Hardhat is the node answering.
tshark -r network-traffic.pcap -q -z follow,tcp,ascii,42 | grep -i hardhat
The attacker kept traversing and read the application source. Follow the streams that return the server files (53 to 56); one is rk-logging.js, which defines the SECRET_KEY the server uses for JWT signing.
tshark -r network-traffic.pcap -q -z follow,tcp,ascii,53 | grep -i secret
With the secret from Q5 the attacker could mint a token. Filter for requests carrying a cookie starting eyJ and look at the GET /admin request (frame 1235), where the token is presented as the admin cookie.
tshark -r network-traffic.pcap -Y 'http.cookie contains "eyJ"' -T fields -e frame.number -e http.request.uri -e http.cookie
Base64-decode the middle segment of the Q6 token. The payload carries name, email, account_type set to admin, and iat/exp claims.
echo 'eyJuYW1lIjoiRGFydGggVmFkZXIiLCJlbWFpbCI6ImRhcnRodmFkZXJAZW1waXJlLmNvbSIsImFjY291bnRfdHlwZSI6ImFkbWluIiwiaWF0IjoxNzQwODAyNzM0LCJleHAiOjE3NDA4ODkxMzR9' | base64 -d
Two sources agree. The leaked rk-server.js creates its WebSocket.Server on one port, and the capture shows the terminal traffic on that same TCP port. The WebSocket text payloads are the attacker’s keystrokes and the shell’s replies.
tshark -r network-traffic.pcap -Y websocket -T fields -e tcp.dstport -e websocket.payload.text
The terminal session starts a Python http.server on port 6969, and a wget request for /data/reachkart.db follows. Read the request time of that GET in the capture and remove the +11 offset to get UTC.
tshark -r network-traffic.pcap -Y 'tcp.port==6969 && http.request' -T fields -e frame.time -e http.request.uri
Gotcha: The capture’s clock is +11 from UTC, while the log is UTC. Submitting the raw frame time gives the wrong hour.
Export Objects does not carve the file, because port 6969 is not dissected as HTTP. Follow the raw stream (57), strip the HTTP header by hand, and hash the body. The body length matches the Content-Length of 69632, which proves the strip was clean.
tshark -r network-traffic.pcap -q -z follow,tcp,raw,57
sha256sum reachkart.db
Open the carved database from Q10 and count users rows whose account_type is seller.
sqlite3 reachkart.db "select count(*) from users where account_type='seller';"
Hardhat’s JSON-RPC runs on 8545 in clear text. Dump the JSON for traffic after the download and find the first eth_sendRawTransaction from a seller wallet (frame 1512). The hash is in the response’s result.
tshark -r network-traffic.pcap -Y 'tcp.port==8545 && json' -T json > rpc.json
grep -n 'eth_sendRawTransaction' rpc.json | head
Resolve each transaction with eth_getTransactionByHash and sum the value fields. Eight transfers from the sellers go to the attacker’s address (0x82b0...). A smaller 0.00023 ETH transaction is the attacker funding gas, not theft, so leave it out.
python3 -I parse_rpc.py rpc.json
Gotcha: Including the 0.00023 funding transaction gives the wrong total. It reappears in Q15 instead.
The blockNumber of the last theft transaction from Q13 is 0x12. Convert it to decimal.
python3 -I -c 'print(int("0x12",16))'
The attacker finished with an eth_getBalance call on their own address (frame 2079). Convert the hex wei result to ETH. The balance is the stolen total from Q13 plus the 0.00023 ETH of gas funding that was already in the wallet.
grep -n 'eth_getBalance' rpc.json | tail -1
| Phase | Action |
|---|---|
| Initial Access | Path traversal on /user/getOrderBill reads /etc/passwd |
| Discovery | package.json leaked, giving Express and Hardhat versions |
| Credential Access | Application source leaked, exposing the JWT signing secret |
| Credential Access | Admin JWT forged and presented on GET /admin |
| Execution | Admin WebSocket terminal on port 8888 used for shell commands |
| Collection | Python http.server on 6969 serves the customer database, fetched with wget |
| Discovery | Database queried for seller accounts and wallets |
| Impact | Eight seller wallets drained to one address over Hardhat JSON-RPC |
| Impact | Attacker checks their own balance |
| Type | Value |
|---|---|
| Vulnerable endpoint | /user/getOrderBill (template parameter) |
| JWT signing secret | SuperSecretPassword |
| Attacker email (JWT) | darthvader@empire[.]com |
| WebSocket port | 8888 |
| Staging port | 6969 (Python http.server) |
| File | reachkart.db, SHA-256 fabe3234bb709ee5e5c5c2789c891a9a49368ffa520b23d60f6be2f2ca81bac6 |
| First theft tx | 0x7b7ded2d51f0dcb1bf3fc5cc9598b81a7a622aac15d3841d377c548986e0a7c3 |
| Attacker wallet | 0x82b0... (as seen in the transfers) |
| Technique | ID | Description |
|---|---|---|
| Exploit Public-Facing Application | T1190 | Path traversal in the template parameter of /user/getOrderBill |
| Data from Local System | T1005 | Traversal reads of /etc/passwd, package.json and source files, then the customer database |
| Unsecured Credentials: Credentials In Files | T1552.001 | JWT signing secret hardcoded in rk-logging.js |
| Forge Web Credentials: Web Cookies | T1606.001 | Admin JWT minted with the leaked secret |
| Command and Scripting Interpreter | T1059 | Shell commands through the admin panel’s WebSocket terminal |
| Financial Theft | T1657 | Eight seller wallets drained to one address |
A template parameter must never reach the filesystem unchecked. One ../ chain in /user/getOrderBill exposed every file the Node process could read. Resolve the path, enforce a base directory, and use an allowlist of template names instead.
Secrets do not belong in source. The JWT key sat in a source file that the traversal could read, and a weak, guessable value made the forged token trivial. Load secrets from the environment or a vault, rotate them after any file disclosure, and use a high-entropy key.
An admin panel with a shell behind it needs more than a cookie. A forged token was enough to reach a WebSocket terminal. Put the panel behind network controls and MFA, and alert on admin sessions that spawn a shell or start listeners such as a Python web server on an unusual port.
A dev blockchain node should never be reachable with real wallets. Hardhat’s JSON-RPC accepted signed transfers over plain HTTP with no authentication, and it held funds belonging to production sellers. Keep dev environments isolated from production data and keys, and bind the RPC port to localhost.
The attacker never touched production directly: one traversal in a dev endpoint led to a hardcoded secret, a forged admin token, a shell, and a drained wallet set. The single lesson is that a development environment holding real credentials and funds is a production system.