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
Slither
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.
Phalcon
- https://explorer.phalcon.xyz/tx/eth/0xd4fafa1261f6e4f9c8543228a67caf9d02811e4ad3058a2714323964a8db61f6
- Trace + Source + Debug + Params etc
- Can Simulate transactions
Sam’s Transaction Viewer
- https://openchain.xyz/trace/ethereum/0xd4fafa1261f6e4f9c8543228a67caf9d02811e4ad3058a2714323964a8db61f6
- Gives the function inputs raw and decoded
Tenderly
- https://dashboard.tenderly.co/tx/mainnet/0xd4fafa1261f6e4f9c8543228a67caf9d02811e4ad3058a2714323964a8db61f6
- Similar to both but worse debugger
- However, the advantage is that you can view the code and the conversion process of Input data while Debugging
- Debug -> select call -> re-simulate
EVM Bytecode Decompiler
https://library.dedaub.com/decompile
Asset Flow
https://metasleuth.io/result/eth/0x6bfd9e286e37061ed279e4f139fbc03c8bd707a2cdd15f7260549052cbba79b7
- Like Phantom but can be better flow chart
VSCode
Solidity Metrics
https://marketplace.visualstudio.com/items?itemName=tintinweb.solidity-metrics
Solidity Visual Developer
https://marketplace.visualstudio.com/items?itemName=tintinweb.solidity-visual-auditor
Inline Bookmarks
https://marketplace.visualstudio.com/items?itemName=tintinweb.vscode-inline-bookmarks
Static Analysis
- Mythx - https://mythx.io/
- Slither - https://github.com/crytic/slither
- Mythril - https://github.com/ConsenSys/mythril
- 4naly3er - https://github.com/ConsenSys/mythril
Fuzzing
- Echidna - https://github.com/crytic/echidna
- Foundry Fuzz - https://book.getfoundry.sh/forge/fuzz-testing
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 | // SPDX-License-Identifier: AGPL-3.0 |
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 | def ondofinance(): |
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.
- get_account fetches the account
- 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
- the ABI json file gets parsed.
- the web3 python library initializes the provider, which uses my environment variable containing my infura project ID.
- provider is confirmed as connected
- 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.
- 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.
- the TrancheToken ABI is saved as
abi2
- 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
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
It didn’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.
Videos
Secureum and ethernaut content
https://youtu.be/b6zEWJwMcGc?t=300
Andy Li - code4rena and real hunting
https://www.youtube.com/watch?v=nVucSDlcDFE