ENS contest - GimelSec's results

Decentralised naming for wallets, websites, & more.

General Information

Platform: Code4rena

Start Date: 12/07/2022

Pot Size: $75,000 USDC

Total HM: 16

Participants: 100

Period: 7 days

Judge: LSDan

Total Solo HM: 7

Id: 145

League: ETH

ENS

Findings Distribution

Researcher Performance

Rank: 4/100

Findings: 7

Award: $4,497.99

🌟 Selected for report: 2

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: csanuragjain

Also found by: GimelSec, cccz

Labels

bug
duplicate
2 (Med Risk)
old-submission-method

Awards

937.2294 USDC - $937.23

External Links

Lines of code

https://github.com/code-423n4/2022-07-ens/blob/main/contracts/wrapper/NameWrapper.sol#L271-L285 https://github.com/code-423n4/2022-07-ens/blob/main/contracts/ethregistrar/ETHRegistrarController.sol#L189-L208

Vulnerability details

Impact

NameWrapper.renew() has an onlyController modifier. And ETHRegistrarController should be the controller of NameWrapper (Otherwise ETHRegistrarController.register cannot call NameWrapper.registerAndWrapETH2LD). Therefore, ETHRegistrarController.renew should call NameWrapper.renew. Otherwise, the second-level domain owners cannot renew their second-level domain in NameWrapper.

Proof of Concept

NameWrapper.renew has an onlyController modifier https://github.com/code-423n4/2022-07-ens/blob/main/contracts/wrapper/NameWrapper.sol#L271-L285

function renew( uint256 tokenId, uint256 duration, uint64 expiry ) external override onlyController returns (uint256 expires) { bytes32 node = _makeNode(ETH_NODE, bytes32(tokenId)); expires = registrar.renew(tokenId, duration); (address owner, uint32 fuses, uint64 oldExpiry) = getData( uint256(node) ); expiry = _normaliseExpiry(expiry, oldExpiry, uint64(expires)); _setData(node, owner, fuses, expiry); }

ETHRegistrarController should be the controller of NameWrapper. But ETHRegistrarController.renew doesn’t call NameWrapper.renew. https://github.com/code-423n4/2022-07-ens/blob/main/contracts/ethregistrar/ETHRegistrarController.sol#L189-L208

function renew(string calldata name, uint256 duration) external payable override { bytes32 label = keccak256(bytes(name)); IPriceOracle.Price memory price = rentPrice(name, duration); require( msg.value >= price.base, "ETHController: Not enough Ether provided for renewal" ); uint256 expires = base.renew(uint256(label), duration); if (msg.value > price.base) { payable(msg.sender).transfer(msg.value - price.base); } emit NameRenewed(name, label, msg.value, expires); }

Tools Used

None

ETHRegistrarController.renew should call NameWrapper.renew

function renew(string calldata name, uint256 duration, uint64 wrapperExpiry) external payable override { bytes32 label = keccak256(bytes(name)); IPriceOracle.Price memory price = rentPrice(name, duration); require( msg.value >= price.base, "ETHController: Not enough Ether provided for renewal" ); uint256 expires = NameWrapper.renew(uint256(label), duration, wrapperExpiry); if (msg.value > price.base) { payable(msg.sender).transfer(msg.value - price.base); } emit NameRenewed(name, label, msg.value, expires); }

#0 - Arachnid

2022-07-27T03:14:55Z

Duplicate of #223.

Awards

5.45 USDC - $5.45

Labels

bug
duplicate
2 (Med Risk)
old-submission-method

External Links

Lines of code

https://github.com/code-423n4/2022-07-ens/blob/main/contracts/ethregistrar/ETHRegistrarController.sol#L183-L185 https://github.com/code-423n4/2022-07-ens/blob/main/contracts/ethregistrar/ETHRegistrarController.sol#L211

Vulnerability details

Impact

Transfer ETH by using transfer() may cause this transaction to fail. In EIP-1884:

In many cases, a recipient of ether from a CALL will want to issue a LOG. The LOG operation costs 375 plus 375 per topic. If the LOG also wants to do an SLOAD, this change may make some such transfers fail.

Proof of Concept

https://github.com/code-423n4/2022-07-ens/blob/main/contracts/ethregistrar/ETHRegistrarController.sol#L183-L185

payable(msg.sender).transfer( msg.value - (price.base + price.premium) );

https://github.com/code-423n4/2022-07-ens/blob/main/contracts/ethregistrar/ETHRegistrarController.sol#L211

payable(owner()).transfer(address(this).balance);

Tools Used

None

Use call{value: amount}() instead of transfer:

(bool success, ) = payable(msg.sender).call{value: amount}(""); require(success, "Address: unable to send value, recipient may have reverted");

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol#L60

Note that call may suffer from reentrancy attack, it should follow Checks-Effects-Interactions.

#0 - jefflau

2022-07-22T09:52:09Z

Duplicate of #133

Findings Information

🌟 Selected for report: panprog

Also found by: GimelSec

Labels

bug
duplicate
2 (Med Risk)
old-submission-method

Awards

937.2294 USDC - $937.23

External Links

Lines of code

https://github.com/code-423n4/2022-07-ens/blob/main/contracts/dnssec-oracle/BytesUtils.sol#L44-L80

Vulnerability details

Impact

There are some implementation mistakes in dnssec-oracle/BytesUtils.compare.

  • There should be a sanity check for offsets and lens
  • if (shortest > 32) is not a correct condition. It should check the size of the last block. shortest is the total size of the bytes array.
  • Returned value disobey the dev comment https://github.com/code-423n4/2022-07-ens/blob/main/contracts/dnssec-oracle/BytesUtils.sol#L32. int diff = int(a & mask) - int(b & mask); and return int(len) - int(otherlen); give the opposite results.
  • Should be careful of int underflow/overflow in int diff = int(a & mask) - int(b & mask) (e.g., type(int).min - 1 will revert)

In conclusion, these mistakes may lead to serious problems.

Proof of Concept

https://github.com/code-423n4/2022-07-ens/blob/main/contracts/dnssec-oracle/BytesUtils.sol#L44-L80

/* * @dev Returns a positive number if `other` comes lexicographically after * `self`, a negative number if it comes before, or zero if the * contents of the two bytes are equal. Comparison is done per-rune, * on unicode codepoints. * @param self The first bytes to compare. * @param offset The offset of self. * @param len The length of self. * @param other The second bytes to compare. * @param otheroffset The offset of the other string. * @param otherlen The length of the other string. * @return The result of the comparison. */ function compare(bytes memory self, uint offset, uint len, bytes memory other, uint otheroffset, uint otherlen) internal pure returns (int) { uint shortest = len; if (otherlen < len) shortest = otherlen; uint selfptr; uint otherptr; assembly { selfptr := add(self, add(offset, 32)) otherptr := add(other, add(otheroffset, 32)) } for (uint idx = 0; idx < shortest; idx += 32) { uint a; uint b; assembly { a := mload(selfptr) b := mload(otherptr) } if (a != b) { // Mask out irrelevant bytes and check again uint mask; if (shortest > 32) { mask = type(uint256).max; } else { mask = ~(2 ** (8 * (32 - shortest + idx)) - 1); } int diff = int(a & mask) - int(b & mask); if (diff != 0) return diff; } selfptr += 32; otherptr += 32; } return int(len) - int(otherlen); }

Tools Used

None

function compare(bytes memory self, uint offset, uint len, bytes memory other, uint otheroffset, uint otherlen) internal pure returns (int) { require(offset + len <= self.length && otheroffset + otherlen <= other.length); //## sanity check uint shortest = len; if (otherlen < len) shortest = otherlen; uint selfptr; uint otherptr; assembly { selfptr := add(self, add(offset, 32)) otherptr := add(other, add(otheroffset, 32)) } for (int remain = int(shortest); remain > 0; remain -= 32) { //safely assume shortest does not overflow uint a; uint b; assembly { a := mload(selfptr) b := mload(otherptr) } if (a != b) { // Mask out irrelevant bytes and check again uint mask; if (remain >= 32) { //## mask = type(uint256).max; } else { mask = ~(2 ** (8 * (32 - remain)) - 1); } //int diff = int(b & mask) - int(a & mask); //## overflow while converting to int for non ascii character //if (diff != 0) //return diff; a &= mask; b &= mask; if (a < b) return 1; else if(a > b) return -1; } selfptr += 32; otherptr += 32; } return int(otherlen) - int(len); //reasonable to safely assume that length cannot overflow int }

#0 - Arachnid

2022-07-27T02:37:34Z

First two points are correct. RE third point, the function comments are incorrect. Fourth point is incorrect, as no overflow is possible.

As the submitter fails to establish any situation in which this can lead to a vulnerability with the current code, this should be triaged as QA.

#1 - Arachnid

2022-07-28T22:58:04Z

Marking this as a duplicate of #78.

Findings Information

🌟 Selected for report: GimelSec

Also found by: csanuragjain

Labels

bug
2 (Med Risk)
disagree with severity
old-submission-method

Awards

1562.0489 USDC - $1,562.05

External Links

Lines of code

https://github.com/code-423n4/2022-07-ens/blob/main/contracts/dnssec-oracle/DNSSECImpl.sol#L186-L190

Vulnerability details

Impact

DNSSEC allows parent zones to sign for its child zones. To check validity of a signature, RFC4034 3.1.7 requires the Signer's Name in any RRSIG RDATA to contain the zone of covered RRset. This requirement is reasonable since any child zone should be covered by its parent zone.

ENS tries to implement the concept of name coverage in DNSSECImpl.verifySignature, but unfortuantely does it wrong, resulting in possibiliy of coverage between two unrelated domains. In the worst case, an attacker can utilize this bug to forge malicious trust chains and authenticate invalid domains.

Proof of Concept

In DNSSECImpl.verifySignature, ENS tries to verify the name of RRSet zone (name) is contained by Signer's Name (rrset.signerName).

if(rrset.signerName.length > name.length || !rrset.signerName.equals(0, name, name.length - rrset.signerName.length)) //## This allows matches such as name="SubString.com" signerName="String.com", which is clearly incorrect, use label counts instead { revert InvalidSignerName(name, rrset.signerName); }

In DNS, for a parent zone to contain another child zone, we generally require the child zone to be a subdomain of the parent. For instance, example.eth. in considered to cover sub.example.eth., while xample.eth. should not be cover example.eth..

Unfortunately in the implementation shown above, both cases will path the check, and ample.eth. will be considered appropriate to sign for example.eth.. This is against the original design of DNS, and would result in breach of zone hierarchy.

In practice, the requirement to exploit this is a bit more complex. Since names are stored as a sequence of packed labels, example.eth. should be stored as \x06example\x03eth\x00, while xample.eth. is stored as \x05xample\x03eth\x00. Thus to successfully pull off the attack ,we have to make sure that the packed signer's name is actually a substring of child zone.

A simple (yet unrealistic) example can be like this xample.eth. can sign for e\x05xample.eth., since packed format of those two names are \x05xample\x03eth\x00 and \x07e\x05ample\x03eth\x00.

In general, it would require some effort for an attacker to find attackable zones, nevertheless, this should still be considered as a potential threat to the integrity of ENS.

Tools Used

None

Check label by label instead of comparing the entire name. To actually meet all requirements specified in RFC4034 and RFC4035, there are still a lot to do, but we will discuss that in a seperate issue for clarity.

#0 - csanuragjain

2022-07-20T05:29:03Z

https://github.com/code-423n4/2022-07-ens-findings/issues/61 seems to be similar and duplicate of this issue

#1 - Arachnid

2022-07-27T02:48:26Z

This is a valid issue, but unexploitable in the wild; RFC 1035 specifies labels are limited to 63 octets or less, and the ASCII code of lowercase 'a' is 97. As a result, no vulnerable names should exist.

Recommend that this be triaged as severity 2 as a result.

#2 - dmvt

2022-08-03T12:00:56Z

I agree with @Arachnid on this. The lack of real world likelihood makes this a Medium severity.

Findings Information

🌟 Selected for report: GimelSec

Also found by: Lambda, zzzitron

Labels

bug
2 (Med Risk)
disagree with severity
old-submission-method

Awards

937.2294 USDC - $937.23

External Links

Lines of code

https://github.com/code-423n4/2022-07-ens/blob/main/contracts/dnssec-oracle/RRUtils.sol#L266-L268

Vulnerability details

Impact

Comparing serial numbers should follow RFC1982 due to the possibility of numbers wrapping around. RRUtils.serialNumberGte tried to follow the RFC but failed to do so, leading to incorrect results in comparison.

Proof of Concept

For a serial number i1 to be greater than i2, the rules provided by RFC1982 is as follow ((i1 < i2) && ((i2 - i1) > (2**31))) || ((i1 > i2) && ((i1 - i2) < (2**31)))

ENS implements int32(i1) - int32(i2) > 0, which will suffer from revert in cases such as i1=0x80000000, i2=0x7fffffff

Tools Used

None

Use the naive implementation instead return (i1 == i2) || ((i1 < i2) && ((i2 - i1) > (2**31))) || ((i1 > i2) && ((i1 - i2) < (2**31)));

#0 - Arachnid

2022-07-27T02:56:17Z

This is intentional, see https://en.wikipedia.org/wiki/Serial_number_arithmetic#General_solution

Nevertheless, this should be documented. Recommend triaging as QA.

#1 - Arachnid

2022-08-03T20:49:51Z

Correction - while the operation is correct per the Wikipedia article, it should be in an unchecked block to allow overflow. Recommend triaging as 2.

#2 - dmvt

2022-08-04T21:10:56Z

Agree with the sponsor. This is a bug and it does potentially impact protocol functionality, but it will not occur until far in the future, making it fairly unlikely. Medium makes sense here.

Summary

We list 2 low-critical findings:

  • (Low) floating pragma
  • (Low) Open TODO

(Low) floating pragma

Impact

Floating pragma may cause unexpected compilation time behaviour and introduce unintended bugs.

Proof of Concept

dnssec-oracle/BytesUtils.sol 1:pragma solidity ^0.8.4; dnssec-oracle/SHA1.sol 1:pragma solidity >=0.8.4; dnssec-oracle/DNSSECImpl.sol 2:pragma solidity ^0.8.4; dnssec-oracle/DNSSEC.sol 2:pragma solidity ^0.8.4; dnssec-oracle/RRUtils.sol 1:pragma solidity ^0.8.4;

Don't use ^, lock pragma to ensure compiler version. e.g. pragma solidity 0.8.4;

(Low) Open TODO

Impact

Open TODO should be resolved.

Proof of Concept

https://github.com/code-423n4/2022-07-ens/blob/main/contracts/dnssec-oracle/DNSSECImpl.sol#L238

// TODO: Check key isn't expired, unless updating key itself

Resolve the TODO.

Lines of code

https://github.com/code-423n4/2022-07-ens/blob/main/contracts/ethregistrar/ETHRegistrarController.sol#L49-L65 https://github.com/code-423n4/2022-07-ens/blob/main/contracts/ethregistrar/BaseRegistrarImplementation.sol#L46-L49 https://github.com/code-423n4/2022-07-ens/blob/main/contracts/wrapper/NameWrapper.sol#L49-L74

Vulnerability details

Impact

ETHRegistrarController, BaseRegistrarImplementation and NameWrapper should have some sanity checks in their constructor, Otherwise it could lead to irreversible errors.

For instance, base and price are set in ETHRegistrarController.constructor. And they are both immutable. So if they are set to wrong addresses(e.g., address(0)). it is impossible to change them back to correct values.

Proof of Concept

https://github.com/code-423n4/2022-07-ens/blob/main/contracts/ethregistrar/ETHRegistrarController.sol#L49-L65

BaseRegistrarImplementation immutable base; IPriceOracle public immutable prices; … constructor( BaseRegistrarImplementation _base, IPriceOracle _prices, uint256 _minCommitmentAge, uint256 _maxCommitmentAge, ReverseRegistrar _reverseRegistrar, INameWrapper _nameWrapper ) { require(_maxCommitmentAge > _minCommitmentAge); base = _base; prices = _prices; minCommitmentAge = _minCommitmentAge; maxCommitmentAge = _maxCommitmentAge; reverseRegistrar = _reverseRegistrar; nameWrapper = _nameWrapper; }

Tools Used

None

Add sanity checks in those constructor.

#0 - jefflau

2022-07-21T18:05:37Z

I think the more likely scenario is that the address can get set to anything and we could paste the wrong address in, so checking for the 0 address probably doesn't help too much. The BaseRegistrarImplementation is out of scope since it's already locked. Both NameWrapper and ETHRegistrarController will need to be deployed and can be checked before hand, if anything goes wrong it can be redeployed.

#1 - jefflau

2022-07-27T08:16:44Z

Recommend severity 0. Will not fix.

#2 - dmvt

2022-08-03T23:54:58Z

I'm downgrading this to gas. The worst impact here is that the sponsor has to redeploy.

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter