Friday, January 26, 2024

Smart Contract Hacking Chapter 5 - Understanding And Attacking Authentication & Authorization On The Ethereum Blockchain

 

In this chapter we will take a look at bypassing UI restrictions using Indirect Object Reference (IDOR) vulnerabilities to bypass unprotected functionality. We will then take a look at various authorization schemes and how to implement them so you can easily spot authorization issues when attacking contracts. We will take a look at both simple authorization and role-based authorization.

Contact Info:  

Twitter: @ficti0n

Penetration Testing: http://cclabs.io


Understanding Smart Contract Authorization and Visibility

Smart contracts function in much the same way as an API that uses endpoints as interfaces to its functionality. You can code DApps for various platforms and access needed functionality within smart contracts for value transfers with functional logic. A common issue in the past was that smart contract functions had public visibility by default, meaning that they were accessible by anyone knew how to interact with them. If you didn't explicitly define the access level of the function it would automatically default to public, allowing anyone to call the function and perform actions using the contracts ABI. 

In newer versions of solidity, the compiler will complain and refuse to compile if you do not explicitly define the visibility of a function as one of the following:

Visibility:

ü  External – Is accessible to other contracts but cannot be accessed internally to the contract.

ü  Public - Is accessible to other contracts and can be accessed internally.

ü  Internal – Can only be accessed within the current contract or contracts deriving from it

ü  Private – These are only visible by the contract that defined them.

 

 

A quick example of a pubic vs a private method is as follows:

Action Steps:

ü  Open up remix in your browser

ü  Create a new solidity file named visibility.sol

ü  Type the following code into the new document and compile/deploy the contract.

ü  Play with the resulting functionality taking note of the visibility definitions above.

 

Simple Visibility Example:

 

1.    pragma solidity ^0.6.6;
2.   
3.    contract visibility {
4.   
5.      function add(uint _a, uint _b) private pure returns (uint){
6.           return _a + _b; 
7.      }
8.      
9.      function get_add_result(uint a, uint b) public pure returns (uint){
10.        return add(a, b);
11.  }   
12.}

 

The visibility.sol contract has two functions at lines 5 and 9. The add function at line 9 is set to private which means that you cannot call it directly from an external call with the contracts ABI, nor with another contract using an external interface to this contract. However, it is called via another function within the same contract at line 10. This is because a function can call private functions within its own contract. Visibility limits certain functions you can call directly. 

If we take a look at a screenshot of the deployed contract you will see that you only have a button to call the public function get_add_result and not the private add function. Note when submitting of 3 + 4 the get_add_result function is easily able to access the private functionality even if you cannot directly and 7 is returned.

 


Visibility is the first part of the equation and determines where the function is accessible from. There is also the matter of actual authorization to access functionality within the smart contract regardless of its visibility.  This is not something that is built in by default and usually managed by the reviewing the address of the caller and making a decision.  The address of the caller is generally going to be the msg.sender unless coded in alternative ways. We will use those other ways in upcoming chapters to bypass authorization in unique ways but for now we will focus on msg.sender.


Video WalkThrough of Visibility Code



 

Implementing Authorization:

Our functions are properly using private and public variables where appropriate, call it a day we are good to go right?  Nope not even close, this just means we have a proper flow to our program and we have limited the visibility of functions that have no need to have direct interaction with a user.  This does not stop a malicious hacker from directly accessing all of our public functions. Many of these public functions are bound to have sensitive functionality tied to financial transactions or interact with private functions that have the functionality you are trying to manipulate.

In a smart contract we need a way to actually tell who has access to a public function in order to setup authorized transactions, for example a bank transfer. Otherwise you would create an account and everyone would be able to access its funds and transfer the funds out to themselves. An attacker can call any public function within the contract, even those meant for administrators only.

Some examples of administrative functionality you would not want exposed would be a self-destruct function to render a contract useless or adding a new administrative account that does have authorization to sensitive functions.

To illustrate this point let's take a look at the following contract that has a few sensitive functions but no protection against unauthorized users. Before you read what the code below does, try the following steps and take a guess at what it's doing yourself and where it should have protections.

Action Steps:

ü  Open your browser and go to remix.ethereum.org

ü  Create a new file named noAuth.sol and type in the following code

ü  Deploy this contract and play with its deposit and withdraw functionality

ü  Do you see any potential issues in authorization?

ü  Do you see any potential issues with the business logic, etc?

 

Example Walkthrough of No Authorization

1.    pragma solidity ^0.6.6;
2.   
3.    contract noAuth {
4.      mapping (address =>uint) balances;
5.   
6.      function deposit() public payable{
7.            balances[msg.sender] = balances[msg.sender]+msg.value;       
8.      }
9.      
10.   function withdraw(uint amount) public payable {
11.         msg.sender.transfer(amount);
12.   }
13.    
14.    function kill() public {
15.        selfdestruct(msg.sender);
16.    }
17.}

 

The noAuth contract above is setup like a mini bank account, where you have the ability to deposit your funds and withdraw your funds. The funds are mapped to your msg.sender address on line 4.  However, there are a few flaws with the way this contract is setup, both in authorization as well as business logic. 

Let's go through the code and look about how it is setup.  First, we have a deposit function on line 6 which accepts a value transfer via the "payable" keyword and applies the value to your current balance associated with your address.  This function seems ok.

Next, we have a withdraw function which receives an amount and transfers that amount to the address which calls the function. But.

ü  The withdraw function never actually checks if you have a balance associated with your address

ü  It also doesn't validate if you have enough in your balance to send the amount you're asking for.

 

That poses a few interesting questions:

  1. Where is this function withdrawing funds from if you don't have a balance associated with your address?
  2. Can you simply liquidate the funds from the account as a whole?

 

Is this a potential business logic / authorization issue?

Finally, we have a kill function on line 14, which simply calls the built-in solidity self-destruct function and transfers all of the contract's funds to the caller of the function. This function will terminate the contracts functionality permanently and liquidate the contracts funds into the account address which ran the kill function. Much like the other two functions the kill function has no authorization, poses a risk to everyone's funds, and leaves the whole contract vulnerable to termination.

Let's play around with this functionality and determine if this is true within the Remix UI.

Action Steps:

ü  Deposit 10 Ether via the deposit function with the value field using account one.

ü  Switch accounts to account two which has no funds and try to withdraw funds. Did it work?

ü  Now call the kill function from account two. What happened?

ü  Try to withdraw funds again with either account. What happened? 

 

Vulnerable Authorization Code WalkThrough: 




Thinking about Smart Contracts as unpublished API's for DApps

There are multiple critical issues with the above smart contract:

ü  It's not validating the logic that users need to have funds associated with their account to make withdrawals. 

ü  It's not stopping a user from killing the contract and liquidating all of the funds of other accounts.

But I have UI mitigation's!!

What if a developer mitigates the issues via a Web or Mobile DApp simply by not providing a way for a user to execute the Kill functionality unless that user is the administrator in the DApp.  Also, what if the UI manages your funds on the DApp's business logic. For example, restricting you from withdrawing funds if the address using the DApp does not have an appropriate balance.  So, we are safe right?

No, not really, much like an API we can call these directly without ever accessing the UI.  By directly calling the public functions of the smart contract, we do not have UI or middleware restrictions. In the web app world this would be equivalent to Indirect object reference (IDOR).  You often see this with video games or web applications where the application from the front end looks good with solid restrictions. But then you start doing some enumeration you realize that all of the functionality comes from an API.

If you start poking around that API enumerating endpoints and fuzzing keywords you often will start finding API endpoints with interesting names that do things intended only for developers and administrators. This can lead to sensitive information disclosure or the ability to change and modify sensitive data. This is a very typical occurrence in web applications and Smart Contracts are no different.

Case of the Video Game Heist

For example, I was performing a penetration test against a large video game development shop whose primary fear was the ability to bypass the in-app purchases functionality.

I first started playing the video game and getting a feel for the game play and sequence of events. For example, the gameplay, how money transfers worked and how in-app purchases were processed. Everything seemed pretty good from the perspective of the mobile and web application UI parameters.  I noted all of the calls were to external APIs and decided to take a look at those.

I setup both a local TCP sniffer on the mobile application, a TCP proxy and captured all of the web requests using a web proxy while playing the game.  When reviewing the output, I noticed some interesting calls which exposed a list of every API endpoint in the application.

I started looking at the returned API endpoints and noted many functions which were not available to me from within the mobile application. Most notably for the client was functions named something similar to Get_Gold, and Get_All_Items. These endpoint names seemed interesting to me so I coded up a python loop which called the API for Get_Gold 100 times. At this point my Gold within the game increased 100-fold. Next, I called the Get_All_items endpoint and received every single item in the game for free.

At this point I didn't even need the gold which I just stole as I owned every single item in the game.   Apparently, these were created by developers and never removed from the API endpoints. Instead they were just restricted by not having the functionality available on the UI of the game.

Yes, sometimes it is just that easy!!!  But how do we do this with a smart contract?

 

Enumerating functions in a contract

So how does this story relate to your Smart Contracts?  Well we have a few options available to us when trying to enumerate public functionality so we can make direct calls.  The most useful resources for enumerating these issues is both the sour
ce code and the Application Binary Interface (ABI).

First, we can take a look at the source code, if you are performing the penetration test the client should provide the source code. If the client does not provide the source code, most Ethereum projects tend to be open source, so you should find a GitHub with the source code. A third option for retrieving the source code would be pulling it from etherscan.io at the address where the contract is deployed. This should be located under the contract tab.  For example, try the following steps to illustrate this point:


Go to etherscan.io and type chainlink into the search field at the top right and click the result shown below that pops up while your typing: 




Next under the profile summary click the contract address: 


You will then see a contract tab on the page that loads. Click that:

 


4.       This will provide the source code for the application if it's available and it will provide the ABI:

ol


Secondly you will want the ABI for the contract in order to interact with it. The ABI is a JSON file which describes the functionality of the smart contract and how to interact with its functions.  You can also generally obtain this exactly as you did above from the contract tab of etherscan.io shown below.

 


Another option if you were provided a contract from the client is to deploy a contract to Remix and grab the ABI that is created. You can grab this in Remix under the compiler section under compiler details. Just click the ABI text and it will copy it to your clipboard.

 


An ABI file for our noAuth contract will look something like the following Snippet.

___________________________________________________________________________________

                [{

                                "inputs": [],

                                "name": "deposit",

                                "outputs": [],

                                "stateMutability": "payable",

                                "type": "function"

                },

                {

                                "inputs": [],

                                "name": "kill",

                                "outputs": [],

                                "stateMutability": "nonpayable",

                                "type": "function"

                },

                {

                                "inputs": [

                                                {

                                                   "internalType": "uint256",

                                                   "name": "amount",

                                                   "type": "uint256"

                                                }

                                ],

                                "name": "withdraw",

                                "outputs": [],

                                "stateMutability": "payable",

                                "type": "function"

                }]

___________________________________________________________________________________

 

Notice that the ABI above is simply just a JSON file that describes the functions in the contract for example the last function in the ABI shows the withdraw function with the following elements:

ü  It takes an amount with the type uint256

ü  It says it has no outputs

ü  It is payable meaning it can send and receive transactions

ü  It also notes that it is a function

 

So, the question is, how we can call these public functions directly if they were not programmed into the UI? The answer is we can use Web3 and programmatically interact with the contract via its ABI to bypass any front-end restrictions. 

Let's directly interact with the noAuth contract and then let's implement authorization and requirement checks. This way you understand how to access public functions but also ways to properly prevent authorization issues with standard security libraries. This also helps with knowing what to look for when reviewing contract source code. 


Directly Calling Public Functions with Web3.js

Steps for setting up the lab:

(Follow the video in the below reference section if you want a walkthrough of the setup)

1.       Open up your browser, and in Remix and create the noAuth.sol file

2.       Start Ganache-Cli on in your terminal

3.       Set the provider in Remix Deploy section to Web3 Provider

4.       Deploy the noAuth.sol contract, which will now deploy to your local ganache blockchain

5.       Copy the address for noAuth.sol. You will need it.

6.       Copy the address of the second account

7.       Deposit 10 Ether via the Deposit function and the Value field (don't forget to change the value type to Ether from Wei)

 

Since not all of the public functions are accessible or may contain restrictions from our UI, we will attack the contract from the command line by directly calling the functions via Web3 using the contracts ABI. 

We will need the ABI for this and we can get the ABI by going to the compilation section in Remix and clicking the ABI link shown below. 

 


 

Note that as Web3 updates and ABI contract formats update you will need to update your web3 commands, I have had this happen to me frequently as this is a newer technology and the formats are always updating so, if this gives you issues feel free to steal the ABI from above to work with the Web3 commands below.

Now open up a terminal and install web3 followed by opening a node terminal:

$ npm install web3

$ node

Once node is running you will see a blank line with a > meaning you are in the node interactive console.  We will now setup a direct connection and attack both the withdraw and kill functions to liquidate the contracts funds and terminate its functionality.  The first thing we will need to do is setup our web3 import using the localhost target where our ganache-cli is running our blockchain transactions.  Note with the commands below the output will usually say "undefined", you can ignore this output.

 

> const Web3 = require('web3')

> const URL = "http://localhost:8545"

> const web3 = new Web3(URL)

 

These lines of input simply create an instance of web3 and set its target network URL. If this were a bug bounty or pentest on another network you would supply that target URL for the target network, we can do this with Infura URL's to the test nets and mainnet on ethereum. We cover how to do this in other labs, but for this lab we are using our local targets.

 

Next lets setup our accounts so that we are using the 2nd account we selected in our remix account dropdown which was imported from ganache-cli. Note accounts start with 0 so the second account is actually labeled as account 1. And also note we deployed our contract with account 0.

 

> accounts = web3.eth.getAccounts();

> var account;

> accounts.then((v) => {(this.account = v[1])})

 

We setup our account in web3 simply by grabbing all of the accounts and then setting the value of account (singular) to 1 with the commands above. Syntax in node / JavaScript is a bit cryptic at times so the commands may look a bit odd but you can easily look them up in the web3 documentation. 

 

Now we need to setup our target contract address from the proxy contract. We also need to paste in the full ABI and then connect the address and the ABI with a contract variable to reference in our calls to the contract. We can do that with the input below.

 

> const address = "ADD CONTRACT ADDRESS HERE"

> const abi = ADD ABI HERE

> const contract = new web3.eth.Contract(abi, address)


Now we are ready to make a call to the contract with the contract connection variable we just created. We will first withdraw funds to our second account which never deposited any funds. We do this using the command below that calls the withdraw function using our account variable. We also specify sending a default gas value since we need to send gas with transactions that make changes on the blockchain.

Before using the command below, first note your account balance in remix on your second account. This should be 100 ether at this point as it was not used in any transactions and it also holds no balance to withdraw in the contract.  Then send the following command which requests 1 ether in Wei. Wei is denominated as the following 1 Ether = 1,000,000,000,000,000,000 Wei (10^18)


> contract.methods.withdraw("1000000000000000000").send({gas: 3000000,from: account})

 

After a few moments you should see your balance increase in the second account on Remix.  Now let's kill the contract so no one else can use it which will additionally send the remaining ether in the contract to our address per the msg.sender value in the source code call to self-destruct.


> contract.methods.kill().send({gas: 3000000,from: account})

 

Video WalkThrough Attacking Authorization with Web3.js: 





Example Fix with Simple Authorization

So obviously it's easy to understand we have functions we don't want directly called. To prevent this we need to implement some kind of protection scheme. Whether that is a require statements for accounts or more elaborate role-based designs.  There are various ways we can implement authorization. We will cover a few common things you will see while auditing solidity smart contract code.  While this is not a book about how to securely code your applications, in this case it is appropriate to understand what you might see while analyzing a contract you are trying to exploit.

The first example we will review is a simple authorization scheme using a contract owner and require statements.

Important Reminder:

Make sure to type out each of these contracts and test what they are doing for yourself before reading the descriptions below the code. The muscle memory of typing all of this code and trying to understand what you typed out will help you in spotting issues when you are auditing code. Also learning how to code will help you write exploits against contracts quickly and understand when it is or is not working and how to fix it.

 

1.    pragma solidity ^0.6.6;
2.   
3.    contract simpleAuth {
4.      address owner;
5.      mapping (address =>uint) balances;
6.      
7.      constructor() public {
8.           owner = msg.sender;
9.      }
10. 
11.   function deposit() public payable{
12.  balances[msg.sender] = balances[msg.sender]+msg.value;       
13.                 }
14.    
15.   function withdraw(uint amount) public payable {
16.         require (balances[msg.sender] >= amount);
17.        msg.sender.transfer(amount);
18.   }
19.    
20.   function kill() public {
21.        require(msg.sender == owner);
22.        selfdestruct(msg.sender);
23.   }
24. }

 

You will notice two changes to this contract from the original. The first change is on line 7 where a constructor sets the owner of the contract to the address of the user who deployed the contract.  This constructor is only run one time when the contract is deployed. Meaning the owner cannot change.  You will notice the initialization of the owner variable was also added on line 4.

 

The second change is the usage of require statements on lines 16 and 21. The require statement on line 16 is not associated to the owner but does add a check to make sure the user requesting a withdrawal has an amount in their balances mapping which is higher than the balance they are requesting to withdraw. This fixes the issue with users withdrawing funds they do not actually have.

 

The next require statement on line 21 makes sure to check that the user calling the Self-Destruct functionality is the owner of the contract. This prevents anyone from just killing the contract and stealing the funds from the account.

 

Exit Scam Warning

Something still smells bad regarding this contract!! The kill function is highly suspect as it removes all of the funds in the contract and could be indicative of an "exit scheme". Whereby a malicious developer creates a contract that handles funds, for example in a game, or an online exchange. But the malicious contract is created for the sole purpose of exiting with all of the user's funds when the balance reaches a desired balance.

These types of issues are something you should always take note of when you see them, and flag them during your assessment. The client might not like that you flagged their intended functionality but that is not your problem. They should know better than to have sketchy functionality and it should be called out.  Even if they did not intend to use the function maliciously, it opens the door for someone else to do so.

 

Example Fix-2 Using Modifiers for Simple Authentication

Another popular authorization pattern is using an onlyOwner modifier. This is often coupled with Openzeppelin security libraries, which we will take a look at in our role-based example. However, in the example below we use a modifier in a simple way to illustrate what you may see in a contract. 

 

1.    pragma solidity ^0.6.6;
2.   
3.    contract simpleAuth2 {
4.      address owner;
5.      mapping (address =>uint) balances;
6.      
7.      constructor() public {
8.           owner = msg.sender;
9.      }
10.   modifier onlyOwner() {
11.        require(msg.sender == owner);
12.         _;
13.   }
14. 
15.   function deposit() public payable{
16.  balances[msg.sender] = balances[msg.sender]+msg.value;       
17.                 }
18.    
19.    function withdraw(uint amount) public payable {
20.         require (balances[msg.sender] >= amount);
21.         msg.sender.transfer(amount);
22.   }
23.   
24.    function kill() public  onlyOwner{
25.         selfdestruct(msg.sender);
26.   }
27.}

 

 

This contract is also very similar to the simpleAuth contract above with a few small modifications to make it more extendable when there are a ton of functions that need authorization restrictions. These changes will also make the authorization simpler and more readable within your code.  Changes in this contract are on lines 10 and 24.

On line 10 we define a modifier named onlyOwner which we can apply to any function. This modifier code will run prior to the original functions execution. In this example the modifier simply checks that the user calling the function is the owner of the contract. You will also note the use of _; which simply signals contract to continue running the function after this modifier code is finished.

You can apply this onlyOwner modifier to any function you wish to have authorization restrictions by simply adding onlyOwner in the function definition. You will see this on line 24. If modifiers requirement is not met the function will not be run. If the requirement is met it transfers control back to the function to continue execution.


WalkThrough of Fixing Authorization Issues With Modifiers: 

 


Example Using Openzeppelin for Role Based Access Control:

The best way to cover your security needs as always is with well-audited, open source security libraries. One option we have for a bit more complex authorization is the Openzeppelin libraries located at:

https://github.com/OpenZeppelin/openzeppelin-contracts

For the previous examples you could have replicated the simple authorization with the ownable contract by OpenZeppelin by importing its functionality in the same way you would import library functionality in any other language.

Since we already looked at a simple example without OpenZeppelin, lets instead take a look at role-based authorization using OpenZeppelin. Role based authorization a bit more involved, but not complicated.  Let's take a look at a simple example.

Before you read the descriptions type out the role-based code below in remix and try to figure out what's happening on your own by deploying this contract and playing with its functionality and see if you can understand how it works.

 

Action Steps to deploy:

ü  Open up remix in your browser

ü  Type out the following code and the import will import all of the OpenZeppelin files in a directory within remix automatically

ü  With your first account, make sure to compile this with the newest version of Solidity that OpenZepplin files are using at the time of writing this was 0.6.2. I used version 0.6.6 without any issues. If versions change in the future you will get an error. Review the error and update the compiler version and pragma version in the code appropriately. But always use the latest version of OpenZepplin files.

ü  Take a look at the created users and make assumptions as to what each user has access to

ü  Play with each function under both the admin and the user context with the first account and another account of your choice.

 

1.    pragma solidity ^0.6.6;
2.    import "https://github.com/OpenZeppelin/openzeppelin-
3.    contracts/blob/master/contracts/access/AccessControl.sol";
4.   
5.    contract roleBased is AccessControl {
6.      bytes32 public constant admin = keccak256("admin");
7.      bytes32 public constant user = keccak256("user");
8.      mapping (address =>uint) balances;
9.      
10.  constructor() public {
11.         _setupRole(admin, msg.sender);
12.  }
13.   
14.  function deposit() public payable{
15.        if (!(hasRole(admin, msg.sender))){
16.            _setupRole(user, msg.sender);
17.        }
18.  balances[msg.sender] = balances[msg.sender]+msg.value;
19.         
20.  }
21.  function withdraw(uint amount) public payable {
22.        require(hasRole(user, msg.sender), "Not a user of this bank");
23.         require (balances[msg.sender] >= amount);
24.        
25.         msg.sender.transfer(amount);
26.  }
27.    
28.  function kill() public {
29.        require(hasRole(admin, msg.sender), "Not an administrator");
30.        selfdestruct(msg.sender);
31.  }
32.}

 

Once you have the roleBased contract deployed you will notice a few changes from the simpleAuth version. First, we are importing the OpenZeppelin libraries which imports all of the prerequisite needs for the role-based access control into Remix.

Secondly, on lines 6-7 we are creating both a user and admin role identifiers. If you take a look at the documentation link from the references at the end of this section it states that the role identifier must be created as a bytes32 hash. We create these as a bytes32 type and hash them with keccak256 which is essentially the equivalent of a sha3 hash function. This type of hashing is standard on Ethereum's consensus engine for producing blocks. Keccak256 is often seen as the hashing function within Solidity smart contracts.

The constructor was updated to execute the _setupRole function from OpenZeppelin. This sets the admin user as the user who initially deployed the contract. In this case we used our first account, so our first account is our admin user. 

The user account is then setup within the deposit function on line 16 for every user who deposits funds and is not already an administrator, as we don't want to overwrite the admin role with the user role. This would be a business logic error that eliminated all admin accounts, which would be bad.  When you deposit funds as the second account your address will be associated with a regular user role.

As an example of how authorization is handled with role identifiers take a look at lines 22 and 29.  On line 22 if you have not already deposited funds you will not have a user role so you cannot withdraw funds. You will be given an error when checking the hasRole requirement.

Try this out with a user who has not deposited funds yet. 

Finally, within the kill function on line 29 you will see a check for an admin role identifier. If the account address calling kill does not have this associated role identifier, an error is displayed and the transaction will not process.

Try the kill function with your second user and take a look at your output window. It should turn red and show that error.  Now if you switch back to your admin user on the first account you can successfully kill the contract.

Note that you can also enumerate, grant and revoke user roles. Check out the references section below for more information if you are interested in that functionality.

 

Authorization Summary:

I hope this chapter was enlightening on how authorization is handled on the blockchain and the dangers of not having authorization on sensitive functions. In the lab package for the certification and on the final CTF exam, there will be many occurrences of authorization which you can further test your business logic and authorization bypass attacking skills.

 

Authorization References

https://docs.openzeppelin.com/contracts/3.x/access-control

https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/access

Related links


No comments:

Post a Comment