Creating a PoC for Smart Contract Bug Bounties

After gaining some practice hacking smart contract on ethernaut, it’s time to try hacking on a real bug bounty program. In this post I’ll walk through my process.

1 - Identify a bounty program

The first thing that must be done is picking a bug bounty program. Head over to Immunify and pick a random program that you’d like to hunt.

Scroll down to ‘Assets in Scope’ and you’ll find a number of etherscan addresses.

In this post I’ll be testing the ondofinance bounty program

2 - Prepare the development environment

As discussed in other blog posts, we want to do our testing either locally or via mainnet fork. Testing locally works for simple projects but sometimes there’s problems compiling and configuring everything if a project uses, say, hardhat, but we want to use brownie.

We’ll create a new brownie project manually by copying our Ethernaut project files to a new directory.

  • Ethernaut project is available on my GitHub

3 - Audit

At this point I want to begin the discovery and recon phase where we try to identify interesting, or vulnerable functionality of the smart contracts. The scope of the bounty includes several etherscan addresses - for our purposes we’ll choose the first one and start reading the source code on etherscan.

In this guide we won’t go into hacking smart contracts via bytecode (symbolic analysis), though it can be done. When etherscan publishes the solidity source code, this means the ABI is available for us to use, which contains all of the read and write functions/variables that we care about. For now we only want to audit smart contracts that have the source (ABI) published.

Tools

Top of the list is going to be slither which will scan for common vulnerabilities. It interprets solidity code and converts it into an intermediate language, which is then used to perform vulnerability checks. There will be false positives, there will be results that we don’t care about. Being an effective analyst is all about evaluating the results.

slither 0x2BB8de958134AFd7543d4063CaFAD0b7c6de08BC

As simple as that, slither will scan the smart contract files that are associated with the address using etherscan.

Findings

Let’s look at a snippet of the slither output

1
TrancheToken (contracts/TrancheToken.sol#16-120) is an upgradeable contract that does not protect its initiliaze functions: TrancheToken.initialize(uint256,string,string,address) (contracts/TrancheToken.sol#33-43). Anyone can delete the contract with: TrancheToken.destroy(address) (contracts/TrancheToken.sol#94-101)Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#unprotected-upgradeable-contract

Notice that the slither output will be color coded, I assume based on the level of severity for each respective finding.

Looking at this snippet we can see that the tool identified a destroy() function that anyone can call. Interesting - lets investigate whether that’s true.

Solidity Analysis

Going back to the etherscan page, we can see that there’s a file named TrancheToken.sol which is what slither was complaining about.
There were multiple files within the etherscan link though, and only one of them is the “main” file. In this case the Contract Name, shown at the top of the etherscan page, is AllPairVault.

Below is a snippet of that file:

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

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
//<other imports>

/**
* @title A container for all Vaults
* <Other information etc etc>*
*/
contract AllPairVault is OndoRegistryClient, IPairVault {
using OLib for OLib.Investor;
using SafeERC20 for IERC20;
using Address for address;
using EnumerableSet for EnumerableSet.UintSet;

// <Other stuff within the file...>

address public immutable trancheTokenImpl;

// <Other stuff within the file...>

So here we see that the TancheToken that we are looking for is declared as an immutable address. This is how we can access the TancheToken instance. Instead of manually looking for the public or external keyword within the code, you could have also selected the “Read” and “Write” buttons on etherscan to see what’s available for use.

Exploitation

Within our brownie project I created the following function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def ondofinance():

account = get_account()

ondoInstanceAddress = config["networks"][network.show_active()][f"ondo_first_address"]
with open("./AllPairVault.json") as f:
info_json = json.load(f)

abi = info_json["result"]

w3 = Web3(Web3.HTTPProvider(f"https://mainnet.infura.io/v3/{os.getenv('WEB3_INFURA_PROJECT_ID')}"))
print(f"Connected? {w3.isConnected()}")
ondo = w3.eth.contract(address=ondoInstanceAddress, abi=abi)

trancheToken=ondo.functions.trancheTokenImpl().call()

print('trancheToken :', trancheToken)

with open("./TrancheToken.json") as f:
abi2 = json.load(f)


tranche = w3.eth.contract(address=trancheToken, abi=abi2)
tranche.functions.initialize(0, "test", "test", ondoInstanceAddress).call({'from':account.address})

This is different than how solidity classes are instantiated with brownie since we’re not using the contract interfaces within the project. Web3.py can be used to interact with existing contracts using only the address and ABI. There’s a lot to dissect here so I will explain each piece.

  1. get_account fetches the account
  2. within our config we’ve identified the address of the etherscan address specified in the bounty program. this gets defined as ondoInstanceAddress.

Next I headed over to etherscan and scrolled down the page until I reached a textbox containing the ABI. After saving that ABI to disk, I named the file AllPairVault.json

  1. the ABI json file gets parsed.
  2. the web3 python library initializes the provider, which uses my environment variable containing my infura project ID.
  3. provider is confirmed as connected
  4. now that the ABI has been parsed and we have the smart contract address, we create the contract object

Now that we know where the TrancheToken instance is defined, lets grab it.

  1. the TrancheToken instance address is saved as trancheToken

At this point we need to initialize the TrancheToken class finally. Again we’ll grab the ABI from etherscan, save the JSON to disk, and reference that.

  1. the TrancheToken ABI is saved as abi2
  2. the address that we discovered, and the abi, are used to create a contract object

Now we could try to call the initialize function that slither mentioned to us

  1. intialize is called using random parameters.
    • Remember that address checksums get checked for validity, so you must pass a real address otherwise solidity will interpret it as a string. Here I’m using the ondoInstanceAddress but any address could be used.

Finally we can test it out. We want to do our testing using a fork of mainnet, so I call the script like this:
brownie run scripts/deploy.py --network mainnet-fork-dev which will use my fork.

  • My github has context on how to set this up in your local environment.

So can we call initialize ?

1
ContractLogicError: execution reverted: Initializable: contract is already initialized

Nope! We can’t. Looking back at the original finding, slither said:

Anyone can delete the contract with: TrancheToken.destroy(address)

Maybe we can call it! Let’s try by replacing the last line:

1
tranche.functions.destroy(account.address).call({'from':account.address})

Kicking off the script again, we get the following output:
ContractLogicError: execution reverted: Invalid access: Only Registry can call

Dang! It doesn’t work, but that’s ok - there’s lots of other tests that we’ll create when auditing smart contracts and this is just the first of many.
What’s important is that we understand the methodology of basic solidity testing.

  • Copyrights © 2021-2024 blindCyber

请我喝杯咖啡吧~

支付宝
微信