区块链安全

低版本 Solidity 错误构造函数导致鉴权绕过漏洞分析

0x01 前言

在读了这篇文章 一些智能合约存在笔误,一个字母可造成代币千万市值蒸发! 之后,我决定自己写个实验复现一遍这种漏洞。

漏洞的原理很简单:在低版本的 Solidity 中,构造函数是和合约名同名的函数,这样会导致如果我们本意是写一个构造函数,但是因为大小写等笔误导致这个「构造函数」跟合约名不一致,那么如果这个构造函数是 public 的,外部账户就都可以对其进行调用。如果这个构造函数里面实际实现的是鉴权功能,那么就会导致通过外部调用,攻击者绕过鉴权,进行一些恶意调用。总之关键点就是:虚假的构造器+鉴权绕过 = 恶意调用。

在下面我会自己编写一段漏洞合约来验证这种恶意调用。

0x02 实验逻辑

首先这个实验中合约都会部署在 Rinkeby 测试网络。为什么选这个呢?因为他的水龙头是比较好用的。Ropsten 测试网络的水龙头好像有什么问题,近期我经常乞讨失败。

Rinkeby 测试网络的乞讨方式比较有意思:你要先发条 twitter,内容是你的钱包的 address。然后将这条推文的 url 贴到水龙头验证处,你的测试币就会即刻到账:

然后开始在 remix 上面将合约代码编写、编译、部署至测试网络。这个漏洞的合约代码由2个文件组成:

代码分析:

  • MorphToken 继承自 Owned。Owned 合约实现了一个虚假的构造函数 owned(),因为跟合约名不一致,导致其实并不是创建时候只被调用一次的构造函数。因为其 public 的属性,导致外部账户都可以调用这个函数。
  • owned() 函数实现的功能是:将 owner 状态变量的值赋值为调用的外部账户。也就是说:谁调用这个合约,谁就变成了是 owner。因为继承关系,MorphToken 合约也有 owned() 函数,可以通过调用这个函数改变状态变量 owner 的值。
  • 修饰器 onlyOwner 进行了鉴权,本意是判断调用者是否为创建合约的外部账户(因为构造函数只在合约创建时候被运行一次)。
  • 子合约里面的 withdraw() 函数使用了 onlyOwner 修饰器,这个函数的功能是从合约中转出以太币。但如果攻击者先通过调用 owned() 函数改变了 owner,onlyOwner 修饰器的鉴权就会失效,导致可见性为 public 的 withdraw() 函数可以被恶意利用,盗取合约中的以太币。

所以,在实验中,首先我们有两个账户:

  • 合约创建者账户:0xcDF1b08Fb6DC74b50c4ce6eEFA0c3AC134874144
  • 攻击者账户:0x0930DE9b7d359b337a21C78629dCc58bCa472A21

实验流程为:

  1. 创建合约,此时 owner 为 合约创建者账户:0xcDF1b08Fb6DC74b50c4ce6eEFA0c3AC134874144
  2. 使用 payable 函数 getBalance 给合约打点钱,不然一会儿没法转钱。
  3. 此时攻击者账户调用 MorphToken 合约的 withdraw() 函数从合约取钱,因为 msg.sender != owner,所以这个交易会失败。
  4. 然后攻击者调用 owned() 函数,改变 owner 为自身。
  5. 然后攻击者再次调用 withdraw() 函数,此时 onlyOwner 修饰器鉴权失效,就可以成功从合约中取到钱。

0x03 实验过程

首先,deploy 并 publish 了 2.sol:

然后调用 getBalance() 给这个合约的余额转入 1 ether:

然后攻击者尝试调用 withdraw() 函数取出 0.1ether:

交易失败:

因为此时合约的 owner 仍然是创建者:

然后攻击者尝试调用继承来的 owned() 函数:

调用成功:

再次查看 owner 状态变量,可以看到此时 owner 变了,变为了攻击者:

然后再来尝试调用 withdraw() 函数取钱:

可以看到取钱成功:

整个是这样一个流程,可以看到最终成功通过 withdraw 函数从漏洞合约取走了 0.1 ether:

漏洞合约地址:https://rinkeby.etherscan.io/address/0xe62933d7028d0a34Bd889360a5F24F810cfd5F94

0x04 漏洞危害分析

因为这个漏洞主要是因为构造函数和合约名不一致导致的。但是较高版本 Solidity 中已经不再允许这样写构造函数:

而是使用 constructor 关键字:

contract A {
	uint x;
	constructor(uint a) public {
	    x = a ;
	}
}

所以在新开发的项目中这种漏洞应该不会多见。但是在存量项目中或者使用低版本 Solidity 开发的项目中仍会存在。因此依然值得注意。

0x05 参考文档