Solidity 中有一个全局变量,tx.origin
,它遍历整个调用栈并返回最初发送调用(或交易)的帐户的地址。在智能合约中使用此变量进行身份验证会使合约容易受到类似网络钓鱼的攻击。
一、漏洞
授权用户使用tx.origin
变量的合约通常容易受到网络钓鱼攻击的攻击,这可能会诱骗用户在有漏洞的合约上执行身份验证操作。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract Wallet {
address public owner;
constructor() payable {
owner = msg.sender;
}
function transfer(address payable _to, uint _amount) public {
require(tx.origin == owner, "Not owner");
(bool sent, ) = _to.call{value: _amount}("");
require(sent, "Failed to send Ether");
}
}
这个Wallet合约,核心函数transfer,可以指定数目进行转账,其中的第一行代码:
require(tx.origin == owner, "Not owner");
这里使用了tx.origin
来表示调用者。
假设使用者Bob发布了这样的一个Wallet合约,并且存入了10 Ether,它也是这个钱包的owner。
钓鱼者可以发布一个这样的合约:
contract Attack {
address payable public owner;
Wallet wallet;
constructor(Wallet _wallet) {
wallet = Wallet(_wallet);
owner = payable(msg.sender);
}
function attack() public {
wallet.transfer(owner, address(wallet).balance);
}
}
此时Attack合约的owner就是钓鱼者的地址。
当钓鱼者诱骗Bob点击了这个Attack合约的attack
方法后,通过合约间调用,transfer内的第一句验证,由于tx.origin
会追溯整个调用栈的第一个调用者,即Bob,那么这个验证可以通过,于是继续执行下去,Bob原来存储的Ether就全部传递给了钓鱼者。
其实很多时候不仅仅是诱骗点击attack方法这么简单,也可以诱骗别人发送Ether到Attack合约上,然后这个Attack合约上的receive()或者fallback()方法中可以嵌入这么一条跨合约transfer调用,这样也会被攻击,而且更隐蔽,因为钓鱼者可能把这个Attack合约伪装成他们自己的私人地址。
二、预防手段
tx.origin
是一种比较危险的用法,可以用msg.sender
来代替:
function transfer(address payable _to, uint256 _amount) public {
require(msg.sender == owner, "Not owner");
(bool sent, ) = _to.call{ value: _amount }("");
require(sent, "Failed to send Ether");
}
msg.sender
是最近的调用者,即使通过Attack合约进行跨合约调用,所以msg.sender
指的就是Attack合约地址,这样就无法通过第一行的owner验证。