Hacking vulnerable smart contract: CTF by ciphershastra.com

Photo by Scott Webb on Unsplash

Note: Code that we are using in this article is “Damn Vulnerable” and not audited, not to use in production.

Details about ctf: Minion is CTF challenge posted on ciphershastra.com

CTF challenge link: https://ciphershastra.com/minion.html

Tools/Techs. we are using: Remix online ide, metamask wallet, solidity programming.

Without wasting time lets take a look into Minion.sol file:

pragma solidity ^0.8.0;

contract Minion{

mapping(address => uint256) private contributionAmount;
mapping(address => bool) private pwned;
address public owner;
uint256 private constant MINIMUM_CONTRIBUTION = (1 ether)/10;
uint256 private constant MAXIMUM_CONTRIBUTION = (1 ether)/5;

constructor(){
owner = msg.sender;
}

function isContract(address account) internal view returns(bool){
uint256 size;
assembly {
size := extcodesize(account)
}
return size > 0;
}
function pwn() external payable{
require(tx.origin != msg.sender, "Well we are not allowing EOAs, sorry");
require(!isContract(msg.sender), "Well we don't allow Contracts either");
require(msg.value >= MINIMUM_CONTRIBUTION, "Minimum Contribution needed is 0.1 ether");
require(msg.value <= MAXIMUM_CONTRIBUTION, "How did you get so much money? Max allowed is 0.2 ether");
require(block.timestamp % 120 >= 0 && block.timestamp % 120 < 60, "Not the right time");
contributionAmount[msg.sender] += msg.value;

if(contributionAmount[msg.sender] >= 1 ether){
pwned[msg.sender] = true;

}
}

function verify(address account) external view returns(bool){
require(account != address(0), "You trynna trick me?");
return pwned[account];
}

function retrieve() external{
require(msg.sender == owner, "Are you the owner?");
require(address(this).balance > 0, "No balance, you greedy hooman");
payable(owner).transfer(address(this).balance);
}

function timeVal() external view returns(uint256){
return block.timestamp;
}
}

The challenge starts with “Call Me Anytime! Wait… Can You Even Call Me?” title. challenge is to call pwn() function so that we can claim that we have exploited pwn() function and edited “pwned” mapping which maps address of caller to to bool values , that means “true” if attacker was able to exploit by calling and false if not.

Lets take a look of some smart contract functions and variable they using:

pwn() : This function has some require statements which will revert the execution if condition become false. in order to execute function code we need to bypass all require statements. lets take a look at all code/ require statements in functions one by one.

  1. First line checks for caller is calling pwn() with Externally owned account.
  2. Second checks for if we are using smart contract for exploiting minion contract . “wait… what ? both contract and EOAs are not allowed to make request to this contract how we will be able to call contract functions?” Don’t worry we will find the way , its a CTF. Lets continue.
  3. Now third security check is about eth value (msg.value) sent during calling pwn function is greater than 0.1 ether.
  4. Fourth check statement is checking that msg.value is less than 0.2 (you can check it by calculating (1*(10**18))/5), “ remember its a CTF so don’t blindly trust on error messages written by required statement”.
  5. last check statement which reverts at odd minutes and passes on even :-
require(block.timestamp % 120 >= 0 && block.timestamp % 120 < 60, “Not the right time”);

it returns “Not the right time” message if condition fails , but we will take care of this check while calling pwn() function.

6. Then we have this follwoing code in pwn()

contributionAmount[msg.sender] += msg.value;

updates balance in contributeAmount mapping.

if(contributionAmount[msg.sender] >= 1 ether){
pwned[msg.sender] = true;

}

which sets true for our calling address in pwned mapping. and “thats what we want!”

verify() function : which returns boolean value if it returns true then we were able to pwn / compromise that contract. And for compromising that function we need to bypass those require function checks in pwn()

Let’s hack it…

  • Let’s say we are going to use smart contract to exploit Minion contract , so now we are able to bypass first check which checks for if msg.sender is EOA(Externally Owned Accounts).
  • Now next we have require statement which checks if msg.sender or caller is smart contract , for bypassing this check lets see how the condition in require() is working :-

So this checking statement is using isContract function which is using extcodesize opcode to get size of code at address, if it is more than 0 it’s a smart contract with something coded in it.

So that means we can’t call another smart contract function within our caller/attacker smart contract function and if we do so the transaction will revert to initial state.

But here’s a trick ,when contract is being created, code size (extcodesize) is 0. So we can call pwn function in attacker contract’s constructor and it will return zero. That’s how we can bypass isContract function check.

  • Now third security check is about eth value (msg.value) sent during calling pwn function is greater than 0.1 ether .
  • And Fourth check statement is checking that msg.value is less than 0.2 .

Bypassing 3rd and 4rth security checks:

we can try out sending 0.2 ether 5 times which will be 1 ether and that’s how we can execute statement in “if” block (pwn() > if {}).

We are very close…

Lets write caller contract from which we will call pwn() from Minion contract.

First of all we are going to use Remix online editor which creates its own version of EVM(Ethereum Virtual Machine) called JavaScript VM . Using this for checking if our attack works on local blockchain would be good decision because we don’t want others to see what we are trying to achieve on public blockchain if our attack fails.

Lets name it “destructor”. in following code you can see our contract has public variable named success of boolean type which we will use later to know if attack was successful.

contract destructor{
bool public successs=false;
constructor(address _addr) payable{
require(block.timestamp % 120 >= 0 && block.timestamp % 120 < 60,"Not the right time!");
Minion(_addr).pwn{value:0.2 ether}();
Minion(_addr).pwn{value:0.2 ether}();
Minion(_addr).pwn{value:0.2 ether}();
Minion(_addr).pwn{value:0.2 ether}();
Minion(_addr).pwn{value:0.2 ether}();

successs=true;
}
function balanceOf(address _addr) public view returns(uint256){
return _addr.balance;
}
}
  • Then we have constructor which is taking target contract address as a argument make sure constructor to make payable because we are sending eth to constructor.
  • Then in constructor we have require check for fulfilling that time condition in Minion contract which will revert the execution. And if this is right time to call that function its time to send ether to smart contract , but question arises how much value to send and how much value is allowed by pwn() ?.

Lets find the answer :- first of all we are sending ethers to pwn function for assigning our address to true in pwned mapping (which basically maps address to uint256) and how we can make it ? answer is if we look at “if” block in pwn() we need to send 1 or greater than 1 eth value.

But what about line 3 and 4 (in pwn())which checks for msg.value is less than equal to 0.2 and greater than equal to 0.1 ether , for sending 1 ether (1* 10**18 ) ether we can try out sending 0.2 ether 5 times which will be 1 ether and that’s how we can execute statement in “if” block.

  • After writing these statements lets make success to true , so this will let you know the attack was successful.(But even we don’t use this variable, creation of destructor contract is indication of successful attack)
  • Bellow the constructor we have simple balanceOf function which returns the balance of the entered address.

Okay we are done with writing attacker contract.

Its time to give it a shot!

Before moving copy paste Minion contract to remix > .sol file that you are using for writing attacker contract so we have one file name it whatever you want and then we have two contracts in it Minion which is target and destructor which is attacker contract.

Press ctrl+s so remix will compile contract or you can go to solidity compiler tab at left side and click on compile “filename.sol”(filename.sol is the file in which we have minion and destructor contracts). After compiling you can see green tick on tab that it the contract is successfully compile remember to set right compiler version.

Go to “deploy and run transaction” tab now select contract from contract list first select minion to deploy minion on local JS VM, once deployed its time to start attack by deploying destructor contract, now while deploying it set value to 1 ether (select ether instead wei in list front of value field) copy address of deployed minion contract by pressing “copy button front of deployed minion contract” and paste It for destructor constructor argument. We are ready to launch the attack.

deployment of Minion and destructor smart contract on remix local blockchain.

Press Deploy button to deploy contract . if you are seeing transaction reverted “Not the right time!” you need to wait for condition to be true so wait for some seconds and deploy again with 1 ether. After successful deployment you can expand destruct contract and check balance of minion contract . it should be 1000000000000000000(1 ether in wei)

Now its time to check if verify function return true and if returns true then yes we have successfully compromised pwn() function with bypassing all present require statements.

Launching Attack on live network…

Now its time to launch attack on live rinkeby testnet where minion contract is deployed for CTF.

To do this change environment in “Run and deploy” tab to “injected web3” this will make Metamask wallet popup confirmation make sure to select rinkeby testnet in metamask and make sure you have some test ethers in address you are going to use.

Now copy address of Minion contract from rinkeby network to remix > deploy and run transaction > At address field. press “At address” button. this will show deployed contract in remix(while doing this make sure you have compiled minion contract and then pressed on At address button)

Lets launch attack on minion contract deployed on rinkeby testnet by choosing destruct contract from list and then entering 1 ether to value and by pressing deploy after that you can see metamask popup for contract creation confirmation and then see transaction happening in remix terminal pane if that shows green tick the attack was successful and we have successfully captured the flag.

deployment of destructor contract on rinkeby blockchain.

So what just happened is we sent 1 ether to destructor ‘s constructor and destructor contract sent them to minion contract one by one by dividing 1 ether to 0.2 ethers and sending 5 times. Now lets verify that we were able to update pwned mapping and assign true to our address.

checking updated balance of minion and calling verify function with destructor’s address to check we captured the flag!

If you can see verify function returning true for destructor’s address then congrats!🎉 we captured the flag

Content of this article is for learning about smart contract security and for educational use only …

Hope you enjoyed this article . stay tuned.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Prajwal More

Prajwal More

Smart contract security and Blockchain ⛓