Issues with Authorization Using tx.origin

7 min read

Understanding the smart contract vulnerability caused due to the tx.origin for user authorization.

The objective of this blog post is to provide a detailed understanding of the tx.origin vulnerability in smart contracts and ways to mitigate it. This is an addition to the series from our earlier blog, where we shared the details of Integer Overflow and UnderflowFront Running Attack, and Understanding Block Timestamp Manipulation

Introduction#

Solidity includes a global variable called tx.origin that can be used to retrieve the address of the account that initiated the first call in a given transaction. However, it is not the same as msg.sender, which returns the address of the immediate caller of the function. The tx.origin variable can be manipulated in certain scenarios where malicious actors use phishing attacks to trick users into conducting authenticated tasks on vulnerable contracts that rely on tx.origin for user authentication and access control.

This can allow the attacker to perform actions on the vulnerable contract that they would not otherwise be able to do, such as withdrawing funds or changing the state of the contract.

Smart contracts that use tx.origin for authorization are vulnerable to phishing attacks, in which a malicious contract can fool the contract's owner into running a function that only the contract's owner is able to call. In this attack, tx.origin is also going to be the owner of the contract that is being attacked. In order for the attack to succeed, the attacker needs to trick the actual contract owner into calling a function on a different smart contract so that the attack can be proxied to the main contract that holds funds. To visualize the attack carried out using tx.origin, consider the below attack scenario.

Attack Setup#

In this hypothetical scenario, we have two Ethereum accounts: "Owner" and "Attacker". The owner has created a smart contract called "Vault", which includes a "withdraw" function that validates whether the "tx.origin" matches the contract owner instead of using "msg.sender" for verification. This is meant to prevent unauthorized withdrawals and ensure that only the contract owner can withdraw funds.

The attacker discovers the security loophole and creates a phishing smart contract called "Airdrop". The Airdrop contract has a deceitful function called "claimAirdrop" that calls the "withdraw" function of the "Vault" contract.

Attack Execution#

The attacker then reaches out to the owner about an exciting airdrop and convinces them to claim it. The owner, thinking they have nothing to lose, uses their account to call the "claimAirdrop" function. However, the owner soon realizes that the balance of the "Vault" contract has been drained.

The attack is possible only when the transaction is initiated by the owner and goes through the Airdrop contract, which acts as an intermediary. As a result, the "tx.origin" value shows up as the owner's address in the Vault's withdrawal function, while the "msg.sender" value is the Airdrop contract's address. This allows the attacker to deceive the owner into draining the balance of the Vault contract.

To prevent such attacks, the owner should have used "msg.sender" instead of "tx.origin" to verify the caller. The "msg.sender" value always represents the address of the account that directly invoked the function. If the "withdraw" function had been programmed to check the "msg.sender" value instead of "tx.origin", it would have rejected the request from the Airdrop contract and prevented the funds from being drained.

Attack Explained#

Consider the below smart contract Vault.sol,



Line 5:

In line 5 of the contract, we have declared a public state variable owner.

Line 7 - 9:

In line 7 of the contract, we have declared a payable constructor.

A function that is used to set up an object is called a "constructor." When a contract or an object of the contract  is created, this function is called right away. Functions or addresses that are listed as "payable" in the contract can receive ether. So, this contract can receive ether when it is deployed.

In line 8 of the contract, we assign the address of the contract deployer to the earlier declared owner variable.

Line 11 - 16:

In line 11 of the contract, we declare a public function called withdraw, that takes two parameters for its function call: an address payable “to”, and a uint “amount”. With the public modifier, the function is accessible from outside the contract.

In line 12 of the contract, we use a ‘require’ statement to check if the transaction initiator is the same as the owner of the contract. In Solidity, ‘tx.origin’ is a global variable which is used to return the address of the account that sent the transaction. If the checks fail, it will throw an error message, “Access denied”.

If the check is passed, we move on to line 14 for the next set of instructions execution. This line will send Ether to the address ‘to’ passed to the transfer function, with the amount specified in ‘amount’ parameter. The call function is used to invoke the fallback function of the target contract, and the value field is used to specify the amount of Ether to send. The sent variable will be assigned a value of ‘true’ if the transfer is successful, otherwise it will be assigned to ‘false’ by default.

In line 15 of the contract, we use another require statement to check if the transfer of Ether was successful. If not, it will throw an error message, ‘Failed to transfer Ether!’.

Attack Scenario#

Let us consider that a user Alice deploys the Vault contract with 100 Ether. A malicious actor Eve deploys the below attack contract Airdrop.sol with the address of the Vault contract.


Line 5 - 6:

The line 5 of the Airdrop contract declares a public payable variable ‘owner’ which is an address payable type. On line 6, there is a declaration of a state variable called vault, which is an instance of the Vault contract.

Line 8 - 11:

The constructor function for the Airdrop contract takes an argument _vault of type Vault, and assigns it to the vault variable.

On line 10 of the Airdrop contract, the address of the contract deployer is set to the owner variable. The payable modifier indicates that the address can receive Ether.

Line 13 - 15:

Using the public function claimAirdrop, the contract attempts to call the withdraw function of the Vault contract, using the vault instance. Here, the balance of the vault instance address is transferred to the owner, which is the deployer of the Airdrop contract.

Thus, the Vault contract deployer was tricked into calling the claimAirdrop function of the Airdrop contract. Inside this function, it requested a transfer of all the funds in the Vault contract to the attacker’s controlled address. Since tx.origin in the withdraw function of the Vault contract remained equal to Alice's address, the contract successfully authorized the transfer. The malicious actor was able to take all of the Ether for their profits.

Exploit Impact#

THORChain

On July 22, 2021, the ETH Router Contract of the Thorchain Bifrost component was exploited by an attacker, resulting in the loss of approximately $8 million.

The attacker created a fake router contract, then a deposit event was emitted when they called a function by transferring a small amount of ether. The router is defined as an Asgard vault, and on the Thorchain router, it forwarded ether to the fake Asgard. This created a fake deposit event with a malicious memo. The Bifrost component intercepts this request as a normal deposit, but refunds the same to an attacker due to the bad memo definition.

The hacker targeted a refund logic that was eventually exploited due to a lack of multi-event handling. Many addresses began to receive an unknown UniH token. Users who tried to sell this token had their full RUNE balances drained after approving its use. This was possible because the transfer function of RUNE used tx.origin, instead of msg.sender for contract authorization. 

Prevention#

It is advisable to avoid using tx.origin in smart contracts to prevent vulnerabilities. However, in some cases, such as when implementing a contract that requires user authentication, it may be necessary to use tx.origin or other similar mechanisms. In such cases, it is important to carefully consider the security implications and potential attack vectors and to implement appropriate countermeasures to prevent exploitation of the vulnerability.

Here are some ways to prevent the tx.origin vulnerability in smart contracts:

  • Use msg.sender instead of tx.origin to identify the initiator of the transaction. Unlike tx.origin, msg.sender is immune to manipulation by attackers.
  • Implement access control mechanisms to restrict certain actions to authorized users. For example, we can use the require statement to ensure that only the contract owner or specific authorized users can execute certain functionality of the smart contract.
  • Avoid sending ether to untrusted contracts, especially those that rely on tx.origin. Instead, use established and audited contracts that follow best practices and have a good reputation.

Conclusion#

We hope that these references help you understand the basics of tx.origin, its usage, and the issues originating due to this implementation.

Reference Sources Stack ExchangeImmune Bytes 

By

Tags