code-423n4 / 2023-04-ens-findings

0 stars 0 forks source link

Multiple Security issues due to Incomplete Type Checking in contracts/dnssec-oracle/RRUtils.sol #123

Closed code423n4 closed 1 year ago

code423n4 commented 1 year ago

Lines of code

https://github.com/code-423n4/2023-04-ens/blob/main/contracts/dnssec-oracle/RRUtils.sol#L94

Vulnerability details

Impact

readSignedSet function does not check if the input data contains a valid RRSIG record. If the input data contains a record of a different type, the function may return incorrect data or undefined behavior. If the input data passed to the readSignedSet function is not a valid RRSIG record, the function may return incorrect data or undefined behavior. This could allow an attacker to manipulate the output of the function, potentially leading to security vulnerabilities in the overall system.

For example, an attacker could create a malicious RRSIG record that appears valid but contains incorrect data or unexpected fields. When the readSignedSet function is called with this data, it may return incorrect values or cause unexpected behavior, potentially allowing the attacker to exploit the system. Vulnerabilities If the readSignedSet function does not properly check if the input data contains a valid RRSIG record, it can result in the following security vulnerabilities:

  1. Information disclosure: If the input data contains a record of a different type, the function may return incorrect data, potentially revealing sensitive information about the system.

  2. Data corruption: If the input data contains malformed or corrupted data, the function may read incorrect values, leading to data corruption or inconsistency.

  3. Denial of service: If the input data is maliciously crafted to exploit the incomplete type checking, the function may enter an infinite loop, leading to a denial-of-service (DoS) attack on the system.

Impact

The impact of incomplete type checking can vary depending on the specific context in which the vulnerable function is used. However, in general, the impact can be significant.

If the function is used in a context where the input data is expected to be of a specific type (such as RRSIG in this case), but it actually contains data of a different type, the function may return incorrect data or exhibit undefined behavior. This can lead to unexpected program behavior, incorrect calculations, and other issues.

In some cases, such as if the returned data is used to make important decisions or execute critical functions, the impact could be severe, potentially leading to loss of funds or other sensitive data.

Furthermore, if the vulnerability is exploited by an attacker, they may be able to manipulate the input data to execute a variety of attacks, such as data poisoning or injection attacks.

Proof of Concept

3 POC’s

  1. At the POC, I demonstrates how ​ a malformed input data can cause the readSignedSet function to return incorrect data, potentially revealing sensitive information:
pragma solidity ^0.8.0;

library DNSLibrary {
    struct SignedSet {
        uint16 typeCovered;
        uint8 algorithm;
        uint8 labels;
        uint32 ttl;
        uint32 expiration;
        uint32 inception;
        uint16 keytag;
        bytes signerName;
        bytes data;
    }

    uint constant private RRSIG_TYPE = 0;
    uint constant private RRSIG_ALGORITHM = 2;
    uint constant private RRSIG_LABELS = 3;
    uint constant private RRSIG_TTL = 4;
    uint constant private RRSIG_EXPIRATION = 8;
    uint constant private RRSIG_INCEPTION = 12;
    uint constant private RRSIG_KEY_TAG = 16;
    uint constant private RRSIG_SIGNER_NAME = 18;

    function readSignedSet(
        bytes memory data
    ) internal pure returns (SignedSet memory self) {
        self.typeCovered = data.readUint16(RRSIG_TYPE);
        self.algorithm = data.readUint8(RRSIG_ALGORITHM);
        self.labels = data.readUint8(RRSIG_LABELS);
        self.ttl = data.readUint32(RRSIG_TTL);
        self.expiration = data.readUint32(RRSIG_EXPIRATION);
        self.inception = data.readUint32(RRSIG_INCEPTION);
        self.keytag = data.readUint16(RRSIG_KEY_TAG);
        self.signerName = readName(data, RRSIG_SIGNER_NAME);
        self.data = data.substring(
            RRSIG_SIGNER_NAME + self.signerName.length,
            data.length - RRSIG_SIGNER_NAME - self.signerName.length
        );
    }

    function readName(
        bytes memory data,
        uint256 offset
    ) internal pure returns (bytes memory) {
        bytes memory name;
        uint256 idx = offset;
        while (true) {
            uint256 labelLen = data.readUint8(idx);
            idx += 1;
            if (labelLen == 0) {
                break;
            }
            if ((labelLen & 0xC0) == 0xC0) {
                idx = data.readUint16(idx & ~0xC000) & ~0xC000;
                continue;
            }
            name = abi.encodePacked(name, data.substring(idx, labelLen), ".");
            idx += labelLen;
        }
        return name;
    }
}

contract DNSContract {
    using DNSLibrary for bytes;

    function testReadSignedSet() external pure returns (bytes memory) {
        // Malformed input data containing an A record instead of an RRSIG record
        bytes memory data = hex"000100010000012c0004d2a2050a";

        // Call readSignedSet with the malformed input data
        DNSLibrary.SignedSet memory result = data.readSignedSet();

        // Return the signerName field of the result
        return result.signerName;
    }
}

In this example, the testReadSignedSet function demonstrates a scenario where a malicious user passes a malformed input data containing an A record instead of an RRSIG record. The readSignedSet function is called with this input data, and the function returns a result with the signerName field. The attacker can then inspect the returned signerName field to potentially reveal sensitive information about the system.

  1. At this POC, I demonstrates how ​incomplete type checking can lead to data corruption:
contract DataCorruptionPOC {
    struct SignedSet {
        uint16 typeCovered;
        uint8 algorithm;
        uint8 labels;
        uint32 ttl;
        uint32 expiration;
        uint32 inception;
        uint16 keytag;
        bytes signerName;
        bytes data;
    }

    function readSignedSet(bytes memory data) internal pure returns (SignedSet memory self) {
        self.typeCovered = data.readUint16(0);
        self.algorithm = data.readUint8(2);
        self.labels = data.readUint8(3);
        self.ttl = data.readUint32(4);
        self.expiration = data.readUint32(8);
        self.inception = data.readUint32(12);
        self.keytag = data.readUint16(16);
        self.signerName = new bytes(data.length - 18);
        for (uint i = 0; i < self.signerName.length; i++) {
            self.signerName[i] = data[i + 18];
        }
        self.data = new bytes(0);
        return self;
    }

    function test(bytes memory inputData) public pure {
        SignedSet memory signedSet = readSignedSet(inputData);
        // Use the signedSet object here to demonstrate potential data corruption
    }
}

In this example, the readSignedSet function is vulnerable to data corruption because it assumes that the input data is a valid RRSIG record, and does not properly validate the input data. If the input data is malformed or corrupted, the function may read incorrect values, leading to data corruption or inconsistency.

The test function in the contract is an example of how this vulnerability can be exploited. The inputData parameter represents the input data that the readSignedSet function will attempt to read. By passing in malformed or corrupted data, we can cause the function to read incorrect values and potentially corrupt the data.

  1. At this POC, I demonstrates how ​incomplete type checking can lead a denial-of-service attack:
pragma solidity ^0.8.0;

contract RRSIG {
    struct SignedSet {
        uint16 typeCovered;
        uint8 algorithm;
        uint8 labels;
        uint32 ttl;
        uint32 expiration;
        uint32 inception;
        uint16 keytag;
        string signerName;
        bytes data;
    }

    function readSignedSet(bytes memory data) public pure returns (SignedSet memory self) {
        self.typeCovered = data.readUint16(0);
        self.algorithm = data.readUint8(2);
        self.labels = data.readUint8(3);
        self.ttl = data.readUint32(4);
        self.expiration = data.readUint32(8);
        self.inception = data.readUint32(12);
        self.keytag = data.readUint16(16);
        self.signerName = readName(data, 18);
        self.data = data.substring(
            18 + uint(self.signerName.length),
            data.length - 18 - uint(self.signerName.length)
        );
    }

    function readName(bytes memory data, uint256 offset) internal pure returns (string memory) {
        uint8 length = uint8(data[offset]);
        string memory result = string(data[offset+1:offset+1+length]);
        return result;
    }

    function infiniteLoop(bytes memory data) public {
        SignedSet memory ss = readSignedSet(data);
        while(true) {
            // this will cause an infinite loop
        }
    }
}

The infiniteLoop function is intentionally designed to loop infinitely, which can be exploited by a malicious user who passes a maliciously crafted input data to the readSignedSet function. Since the function does not properly check if the input data contains a valid RRSIG record, the function may enter an infinite loop, leading to a denial-of-service attack on the system.

Tools Used

Recommended Mitigation Steps

To mitigate the risk of incomplete type checking, you should add validation to ensure that the input data is a valid RRSIG record before parsing it. One way to do this is to check the record's type value and verify that it matches the RRSIG record type value (value 46).

Here is an example of how you could modify the readSignedSet function to add input validation:

function readSignedSet(
    bytes memory data
) internal pure returns (SignedSet memory self) {
    require(data.length >= RRSIG_SIGNER_NAME + 2, "Invalid input data");

    uint16 recordType = data.readUint16(0);
    require(recordType == RRSIG_TYPE, "Input data is not an RRSIG record");

    self.typeCovered = data.readUint16(RRSIG_TYPE);
    self.algorithm = data.readUint8(RRSIG_ALGORITHM);
    self.labels = data.readUint8(RRSIG_LABELS);
    self.ttl = data.readUint32(RRSIG_TTL);
    self.expiration = data.readUint32(RRSIG_EXPIRATION);
    self.inception = data.readUint32(RRSIG_INCEPTION);
    self.keytag = data.readUint16(RRSIG_KEY_TAG);
    self.signerName = readName(data, RRSIG_SIGNER_NAME);
    self.data = data.substring(
        RRSIG_SIGNER_NAME + self.signerName.length,
        data.length - RRSIG_SIGNER_NAME - self.signerName.length
    );
}

In this modified function, we first check that the input data has a length of at least RRSIG_SIGNER_NAME + 2 bytes, which is the minimum length of an RRSIG record. Then, we read the first 2 bytes of the input data to get the record type value and verify that it is equal to RRSIG_TYPE. If the input data is not a valid RRSIG record, the function will revert with an error message. Otherwise, it will proceed with parsing the record as before.

thereksfour commented 1 year ago

Lack of reachable exploit path, invalid.

c4-pre-sort commented 1 year ago

thereksfour marked the issue as low quality report

c4-judge commented 1 year ago

dmvt marked the issue as unsatisfactory: Invalid