The Ethereum Name Service (ENS) contract recently suffered from a critical bug that prompted a security advisory and a migration to a new contract (CVE-2020-5232). ENS allows users to associate online resources with human-readable names. As you might expect, it allows you to transfer and sell domain names.
Specific details about the bug were in scant supply. We heard about the forthcoming fix and wondered: Could Manticore have found this bug?
In short, if a person transferred an ENS name in a specific way, they would be capable of later claiming it back away from the new owner. This would not have worked if a person transferred a name in the normal way. In other words, to make use of this bug, the person doing the transferring had to have been intentionally setting themselves up from the beginning to claim it back.
We decided to dive in and try Manticore on the original contract and discover the bug.
- Go to etherscan.io and dig out the contract code.
- Scratch our heads while observing the strange Solidity dialect.
- Realize it’s not Solidity at all. It was written in LLL.
Luckily, Manticore does not rely on any high-level language and can inspect code at the EVM level. So we backpedaled a bit and found the creation transaction that gave birth to the ENS contract. After some clever use of etherscan.io magic, we found the transaction and extracted the initialization bytecode:
From the advisory, we inferred that the bug could be exploited through four transactions:
- The attacker buys a name or ENS node
- The attacker does some unknown exploitation preparatives
- The attacker sells the name/node/subnode to the victim
- The attacker expropriates the name/node and regains ownership over the node
The unknown bits occur during steps 2 and 4. If we setup the scenario appropriately, then Manticore should discover the precise actions required for these steps on its own.
We reviewed the exported functions by inspecting the contract code:
;; Precomputed function IDs. (def 'get-node-owner 0x02571be3) ; owner(bytes32) (def 'get-node-resolver 0x0178b8bf) ; resolver(bytes32) (def 'get-node-ttl 0x16a25cbd) ; ttl(bytes32) (def 'set-node-owner 0x5b0fc9c3) ; setOwner(bytes32,address) (def 'set-subnode-owner 0x06ab5923) ; setSubnodeOwner(bytes32,bytes32,address) (def 'set-node-resolver 0x1896f70a) ; setResolver(bytes32,address) (def 'set-node-ttl 0x14ab9038) ; setTTL(bytes32,uint64)
This information was enough to set up the preconditions for the vulnerability in a Manticore script and let its symbolic execution produce the exploit for us, automatically:
In just a few minutes, Manticore found two ways to expropriate back the subnode and, therefore, exploit this vulnerability.
If you inspect the generated exploits, you can see the attacker needs to send a setTTL or setResolver transaction before she sells the bait node to the victim. Here are the two complete exploit traces:
[+] Accounts in the emulated ethereum world: The contract address: 3c90ec8304b1da72f2e336d19336e9046d71e981 The owner address: d77e14a2801273ab0a1da75f43585d3e32f0bd1d The attacker address: 911c639393f0ca8eed3a1dbebf740053b7fb8ce8 The victim address: a21337d4001af93c16ee19b8ebb210b714ed92bb [+] ENS root owner gives the attacker 'tob' sub node [+] Let the attacker prepare the attack. Manticore AEG. [+] The attacker `sells` the node to a victim (and transfer it) [+] Now lets the attacker finalize the exploit somehow. Manticore AEG. [+] Check if the subnode owner is victim in all correct final states. [*] Exploit found! (The owner of subnode is again the attacker) setSubnodeOwner(0x0, 0x2bcc18f608e191ae31db40a291c23d2c4b0c6a9998174955eaa14044d6677c8b, 0x911c639393f0ca8eed3a1dbebf740053b7fb8ce8) setTTL(0xbb6346a9c6ed45f95a4faaf4c0e9859d34e43a3a342e2e8345efd8a72c57b1fc, 0x911c639393f0ca8eed3a1dbebf740053b7fb8ce8) setOwner(0xbb6346a9c6ed45f95a4faaf4c0e9859d34e43a3a342e2e8345efd8a72c57b1fc, 0xa21337d4001af93c16ee19b8ebb210b714ed92bb) setResolver(0xbb6346a9c6ed45f95a4faaf4c0e9859d34e43a3a342e2e8345efd8a72c57b21c, 0x911c639393f0ca8eed3a1dbebf740053b7fb8ce8) owner(0xbb6346a9c6ed45f95a4faaf4c0e9859d34e43a3a342e2e8345efd8a72c57b1fc) [*] Exploit found! (The owner of subnode is again the attacker) setSubnodeOwner(0x0, 0x2bcc18f608e191ae31db40a291c23d2c4b0c6a9998174955eaa14044d6677c8b, 0x911c639393f0ca8eed3a1dbebf740053b7fb8ce8) setResolver(0xbb6346a9c6ed45f95a4faaf4c0e9859d34e43a3a342e2e8345efd8a72c57b1fc, 0x911c639393f0ca8eed3a1dbebf740053b7fb8ce8) setOwner(0xbb6346a9c6ed45f95a4faaf4c0e9859d34e43a3a342e2e8345efd8a72c57b1fc, 0xa21337d4001af93c16ee19b8ebb210b714ed92bb) setTTL(0xbb6346a9c6ed45f95a4faaf4c0e9859d34e43a3a342e2e8345efd8a72c57b1dc, 0x911c639393f0ca8eed3a1dbebf740053b7fb8ce8) owner(0xbb6346a9c6ed45f95a4faaf4c0e9859d34e43a3a342e2e8345efd8a72c57b1fc)
The API of the new ENS implementation has changed significantly and these exploits are no longer applicable. This new code has been reviewed by other parties, however, contract owners should always build tests for important security properties into their development process. It is left as an exercise for the reader to write a Manticore script that verifies the new contract is safe from similar issues.
Manticore helps you reason about code, test security properties, and generate exploits with very little knowledge of the contract’s inner workings. I personally find this example with ENS interesting because the contract is not written in Solidity and it highlights Manticore’s ability to handle low-level EVM.
Review our “Building Secure Contracts” to learn more about using Manticore. It includes tutorials on symbolic execution, instructions for using Manticore, and techniques to maximize its bug finding capabilities. We’re also available to help you integrate our tools into your development process: Contact us or join the Empire Hacking Slack.
As of March 3rd, ENS finished their contract migration and published a port-mortem of this incident.
Excellent post! It’s worth noting that while the new contract is implemented in Solidity and has some new functionality, the same functions are still available – so you should still be able to run this script against the new version.