说明
关键的漏洞点是在于Carrot中的transReward函数。代码如下:
1 | contract token is ERC20 { |
其中的pool.functionCall(data)中没有对pool进行限制,理论上就可以调用任意的pool方法。
这个漏洞也可以换一个名字,取名为滥用functionCall导致的漏洞。functionCall是openzeppelin中的address库的方法。具体的使用方法如下:
Performs a Solidity function call using a low level
call. A plaincallis an unsafe replacement for a function call: use this function instead.If
targetreverts with a revert reason, it is bubbled up by this function (like regular Solidity function calls).Returns the raw returned data. To convert to the expected return value, use
abi.decode.Requirements:
target
must be a contract.
callingtarget withdatamust not revert.
简单点说,functionCall底层就是封装了call()函数的调用。
漏洞分析
攻击分析
根据blocksec披露的信息info,分析攻击交易的调用栈。0xa624660c29ee97f3f4ebd36232d8199e7c97533c9db711fa4027994aa11e01b9

攻击步骤大致分为四步:
- 攻击合约调用目标Token(Carrot)中的漏洞方法
transReward()方法 - 调用pool合约的
0xbf699b4b方法 - 之后测试一个空的转账
- 确认第三步空的转账成功成功,成功将受害者的Token转移到目标合约
关于这几个步骤中还是有疑问,为什么需要进行第二步,第二步的作用什么。下面结合Token合约进行分析。
合约分析
1 | contract token is ERC20 { |
与转账有关的两个函数是:transferFrom和_beforeTransfer。
_beforeTransfer,就是判断msg.sender是不是pool.owner()。如果是就将_isExcludedFromFee[from] = true。
1 | function _beforeTransfer( address from,address to,uint256 amount) private{ |
在transferFrom()函数内部通过_isExcludedFromFee[_msgSender()]的判断语句,会存在一个分支执行。结合_beforeTransfer()的实现,transferFrom()的逻辑就是,如果判断是pool.owner直接转账。如果不是,转账之后重新授权。
如果我们直接调用transferFrom()将受害者的Token转移到我们的账户中,最终就会在_approve(sender,_msgSender(),_allowances[sender][_msgSender()].sub(amount,"ERC20: transfer amount exceeds allowance"));失败。
所以,关键的问题就是要让我们的msg.sender成为pool.owner()
综合0xa624660c29ee97f3f4ebd36232d8199e7c97533c9db711fa4027994aa11e01b9攻击分析,
Carrot.transReward(0xbf699b4b0000000000000000000000005575406ef6b15eec1986c412b9fbe144522c45ae)
Carrot.transReward的调用就是为了改变pool.owner()
1 | 0x6863b549bf730863157318df4496ed111adfa64f.owner() |
最后的输出结果是0x5575406ef6b15eec1986c412b9fbe144522c45ae
漏洞复现
结合我们的分析,根据0xa624660c29ee97f3f4ebd36232d8199e7c97533c9db711fa4027994aa11e01b9,我们就可以写出对应的Poc。
1 |
调用结果如下: