Ethernaut Series - 00

·

6 min read

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:

  1. The variable infoNum should be declared as uint256 instead of uint8 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, like uint256. 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 a uint8, 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.

  1. Initialize a foundry project using forge init. My current folder structure looks like this:

  2. Configure your foundry.toml to your preferred testnet along with your API keys. In my case, the testnet will be Sepolia.

     [profile.default]
     src = "src"
     out = "out"
     libs = ["lib"]
    
     eth_rpc_url = "https://eth-mainnet.g.alchemy.com/v2/<api>"
     etherscan_api_key = "<ethereum_api>"
    
  3. 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:

  4. 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:

  5. Now we will write a test script which will be stored in the test 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.

  6. 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
    
  7. 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:

  8. 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:

  1. Create a new folder for your PoC and type forge init to initialize a foundry project there.

  2. 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.

  1. 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:

    1. 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();
        }

    }
  1. We can access the contract instance using the variable level0. Here we simply call the password() function to verify the password and then pass it in authenticate() 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 the test prefix to know which test functions to run.

  1. 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 with Script.sol in the import statement and the function name has been changed to run(). 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:

  1. Aditya Dixit's Blog

Did you find this article valuable?

Support Mrigendra's Blog by becoming a sponsor. Any amount is appreciated!