Eldorion

Personal Rating: Easy

This blockchain challenge was quite fun and illustrated the risks of relying on (inaccurate) block timestamps for verification. This is the contract we are presented with:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract Eldorion {
    uint256 public health = 300;
    uint256 public lastAttackTimestamp;
    uint256 private constant MAX_HEALTH = 300;
    
    event EldorionDefeated(address slayer);
    
    modifier eternalResilience() {
        if (block.timestamp > lastAttackTimestamp) {
            health = MAX_HEALTH;
            lastAttackTimestamp = block.timestamp;
        }
        _;
    }
    
    function attack(uint256 damage) external eternalResilience {
        require(damage <= 100, "Mortals cannot strike harder than 100");
        require(health >= damage, "Overkill is wasteful");
        health -= damage;
        
        if (health == 0) {
            emit EldorionDefeated(msg.sender);
        }
    }

    function isDefeated() external view returns (bool) {
        return health == 0;
    }
}

As with most blockchain challenges, we have an rpc port to interact with the contract and a control port where we can fetch our private key, target contract address etc. So I started with fetching these details:

Inspecting the contract, this is how it seems to work:

There is a boss with 300 health that has to be defeated to win the challenge. Each hit can make a maximum of 100 damage, but if the timestamp increased when striking again, the health will be set to 300 again by the EternalResilience modifier. This command worked to make 100 damage:

cast send --rpc-url=http://83.136.251.145:36755 --private-key=0xf7daa5e11be210259f7a57643a2aaf95e75c36f9cf02992e91ba3728e1f2b8b0 0x73E69769AD010a24603cB3B18dc5688c7eFC6576 "attack(uint256)" 100

As expected, consecutive hits do not work to reduce Eldorion's health by 300 due to the modifier.

If there is a time gap (block.timestamp > lastAttackTimestamp), the health is reset at the start of each function call of attack().

By researching I found out that block.timestamp is accurate to about the second, but should not be trusted for millisecond precision. The attack vector seems to be that you have to send 3 attack requests within the smallest amount of time possible, so that the timestamp does not change during the three hits.

I wrote a script to execute three consecutive attacks, followed by the isDefeated function. The script checks for the flag afterwards. After a few attempts, it worked.

Rapid Attack Script
./sender.sh && echo 3 | nc 83.136.251.145 30791

HTB{w0w_tr1pl3_hit_c0mbo_ggs_y0u_defe4ted_Eld0r10n}

Last updated