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:
- Send a user a link to a website, perhaps a game
they can play on Ethereum etc
- 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.
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
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.