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:
- Where is this function withdrawing funds from if you don’t have a balance associated with your address?
- 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
> 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})
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
This comment has been removed by a blog administrator.
ReplyDelete