Writeup de "Funds Secured" | Blockchain
Writeup de Funds Secured | Blockchain
Descripcion del Challenge:
En Arodor, un programa de crowdfunding de última generación impulsó investigaciones innovadoras. Impulsado por un smart contract, el programa tenía como objetivo recaudar fondos. Un consejo directivo supervisaba esta campaña, siendo responsable de finalizar el programa mediante un esquema de wallet multi-firma. Tu objetivo es explotar el contrato y robar los fondos, representando una amenaza para la noble misión científica de Arodor.
Info:
1 | Private key : 0x8c5791a5eedf0f28562d0e863116e4a1a19825c32606ca5a1efeabfbe3958cc5 |
Seteo variables para trabajar comodamente:
1 | Private='0x8c5791a5eedf0f28562d0e863116e4a1a19825c32606ca5a1efeabfbe3958cc5' |
Con eso configurado, descargue el challenge y dejé todo listo para empezar. El árbol de archivos en mi carpeta quedó así:
Acá es donde voy a empezar a leer y entender bien la lógica del sistema de votación y la función crítica closeCampaign()
que es, básicamente, donde va a estar el bug que nos permite vaciar los fondos.
Campaign.sol
1 | // SPDX-License-Identifier: UNLICENSED |
Setup.sol
1 | // SPDX-License-Identifier: UNLICENSED |
Entendiendo el objetivo y su lógica
El objetivo basicamente es: vaciar el contrato Crowdfunding para que isSolved() de true.
Esto se evalua en SETUP.SOL:
Ese TARGET es simplemente una instancia del contrato de Crowdfunding a la que se le transfirieron 1100 ETH cuando se deployea el SETUP. Osea, hay platita y hay que robarla.
¿Podemos interactuar directo con Crowdfunding
?
Nop. El contrato Crowdfunding (que está definido dentro del archivo Campaign.sol
), tiene una funcion publica llamada closeCampaign(address to)
pero esta protegida como “Only owner”. Y el único que puede pasar esa verificación es el CouncilWallet
, que fue seteado como owner durante el deployment.
El Fallo
Cuando miraba la función closeCampaign
dentro de CouncilWallet
, en un principio parece segura. Te pide una lista de firmas, chequea que cada firma sea de un miembro del consejo, que no esten duplicadas, etc.
Sin embargo hay un pequeño detalle interesante y es que en ningun momento se valida que haya al menos 6 firmas en el array!.
Ese if (i > 5)
corta el loop si ya se tiene 6 firmas, pero no exige que las haya!. Esto valida que podriamos enviar una lista totalmente vacia haciendo que no se chequee nada.. luego se terminaria llamando a:
1 | Crowdfunding(crowdfundingContract).closeCampaign(to); |
Y como CouncilWallet
es el owner del contrato Crowdfunding
, la llamada pasa el require(msg.sender == owner)
, y se ejecuta:
1 | selfdestruct(payable(to)); |
Por ende, se destruye el contrato y manda los 1100 ETH a donde le digamos.
Explotacion
Entonces, la solucion es mas simple de lo que parece.. Solo hay que hacer una llamada a closeCampaign(...)
desde nuestro address con los siguientes requisitos:
Un array vacío (
[]
) como firmaLa dirección
CouncilWallet
como destino (para simplificar, se podria usar nuestro address tambien si queres recuperar los fondos, pero como el reto nos pide que el balance deCrowdfunding
sea 0, con CouncilWallet alcanza)Y por ultimo la address del contrato
Crowdfunding
1 | cast send $Wallet 'closeCampaign(bytes[] memory, address, address)' '[]' $Wallet $Crowdfunding --private-key $Private |
Si verificamos la funcion isSolved():
1 | cast call $Setup 'isSolved() (bool)' |
Nos da TRUE, solo resta ir por nuestra flag :)
Conclusion..
A veces no se necesita una falla cripto si la logica esta mal implementada. Aunque el contrato use ECDSA correctamente, sino se valida la cantidad minima de firmas el chequeo no sirve absolutamente de nada.