// HackTheBox  ·  writeup

ReachKart

HackTheBox Medium tsharksqlite3Pythonsha256sumgrep
DFIR

Scenario

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.

Executive Summary

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.

Artifacts

Timestamp note: The capture’s frame times carry a +11 offset. The access log is UTC, and every answer here is UTC. Convert before submitting.

Initial Access

Q1 — What was the vulnerable endpoint that allowed the attacker to leak files?

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
Click flag to reveal /user/getOrderBill
Q2 — When was the first successful exploitation of the vulnerable endpoint by the attacker (time in UTC)?

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.

Click to reveal answer 2025-03-01 04:09:22

Discovery

Q3 — Which version of Express is currently being used on the server?

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.

Click flag to reveal 4.21.2
Q4 — Which Ethereum compatible development smart contract network is running on the server? (Format: name@version)

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
Click to reveal answer [email protected]

Credential Access

Q5 — What is the signing key used by the server to sign JSON Web Tokens (JWT)?

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
Click flag to reveal SuperSecretPassword
Q6 — The attacker was able to generate a JWT from the signing key and log in to the admin panel. What is the JWT value?

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
Click to reveal answer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiRGFydGggVmFkZXIiLCJlbWFpbCI6ImRhcnRodmFkZXJAZW1waXJlLmNvbSIsImFjY291bnRfdHlwZSI6ImFkbWluIiwiaWF0IjoxNzQwODAyNzM0LCJleHAiOjE3NDA4ODkxMzR9.RBmMEC7IYmkzz1LsT-kP_JkLCUb7-hsJKX4IdjE91TE
Q7 — Decode the token and find the email used by the attacker to log in to the admin panel.

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
Click flag to reveal [email protected]

Execution

Q8 — The admin panel uses WebSocket to send and receive terminal input. What port is being used?

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
Click to reveal answer 8888

Collection

Q9 — The attacker then was able to retrieve a sensitive file. When did the attacker get the file (UTC)?

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.

Click flag to reveal 2025-03-01 04:22:01
Q10 — What is the SHA-256 hash of the file that the attacker downloaded ?

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
Click to reveal answer fabe3234bb709ee5e5c5c2789c891a9a49368ffa520b23d60f6be2f2ca81bac6
Q11 — How many sellers are there in the e-commerce website?

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';"
Click flag to reveal 8

Impact

Q12 — The attacker started sending Ether from all identified sellers' wallets. What is the hash of the first transaction?

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
Click to reveal answer 0x7b7ded2d51f0dcb1bf3fc5cc9598b81a7a622aac15d3841d377c548986e0a7c3
Q13 — What was the total amount of Ether stolen by the attacker? (1 Eth = 10^18 wei)

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.

Click flag to reveal 24.4
Q14 — What is the block number of the last transaction in which Ether was stolen? (Decimal)

The blockNumber of the last theft transaction from Q13 is 0x12. Convert it to decimal.

python3 -I -c 'print(int("0x12",16))'
Click to reveal answer 18
Q15 — After the attacker stole the Ether, what was the balance in their wallet? (Ignore the trailing zeros)

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
Click flag to reveal 24.40023

Attack Summary

PhaseAction
Initial AccessPath traversal on /user/getOrderBill reads /etc/passwd
Discoverypackage.json leaked, giving Express and Hardhat versions
Credential AccessApplication source leaked, exposing the JWT signing secret
Credential AccessAdmin JWT forged and presented on GET /admin
ExecutionAdmin WebSocket terminal on port 8888 used for shell commands
CollectionPython http.server on 6969 serves the customer database, fetched with wget
DiscoveryDatabase queried for seller accounts and wallets
ImpactEight seller wallets drained to one address over Hardhat JSON-RPC
ImpactAttacker checks their own balance

IOCs

TypeValue
Vulnerable endpoint/user/getOrderBill (template parameter)
JWT signing secretSuperSecretPassword
Attacker email (JWT)darthvader@empire[.]com
WebSocket port8888
Staging port6969 (Python http.server)
Filereachkart.db, SHA-256 fabe3234bb709ee5e5c5c2789c891a9a49368ffa520b23d60f6be2f2ca81bac6
First theft tx0x7b7ded2d51f0dcb1bf3fc5cc9598b81a7a622aac15d3841d377c548986e0a7c3
Attacker wallet0x82b0... (as seen in the transfers)

MITRE ATT&CK

TechniqueIDDescription
Exploit Public-Facing ApplicationT1190Path traversal in the template parameter of /user/getOrderBill
Data from Local SystemT1005Traversal reads of /etc/passwd, package.json and source files, then the customer database
Unsecured Credentials: Credentials In FilesT1552.001JWT signing secret hardcoded in rk-logging.js
Forge Web Credentials: Web CookiesT1606.001Admin JWT minted with the leaked secret
Command and Scripting InterpreterT1059Shell commands through the admin panel’s WebSocket terminal
Financial TheftT1657Eight seller wallets drained to one address

Defender Takeaways

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.

Conclusion

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.