DEV Community

Daniel
Daniel

Posted on

How Unsolicited EtcdCluster Packets Could Stall or Redirect WEMIX Mining Coordination

A node that asks one peer for a cluster response should not accept the first answer broadcast by somebody else.

WEMIX etcdJoin did exactly that.

The function selected a specific peer, sent admin_requestEtcdAddMember for that peer’s ID, and then waited for a response through a global feed:

ch := make(
    chan string,
    16,
)

sub :=
    wemixapi.SubscribeToEtcdCluster(
        ch,
    )
Enter fullscreen mode Exit fullscreen mode

That feed carried only:

Cluster string
Enter fullscreen mode Exit fullscreen mode

It did not carry:

Sending peer ID

Request ID

Pending join identity
Enter fullscreen mode Exit fullscreen mode

Meanwhile, every accepted partner EtcdCluster packet was published into the same feed:

go wemixapi.GotEtcdCluster(
    cluster,
)
Enter fullscreen mode Exit fullscreen mode

As a result, another WEMIX partner peer could send an unsolicited cluster string before the legitimate response arrived.

The target would consume that value as though it answered the pending join.

The proof of concept demonstrated the real etcdJoin requesting:

legit-id
Enter fullscreen mode Exit fullscreen mode

and then consuming an attacker value first.

The join returned:

not found
Enter fullscreen mode Exit fullscreen mode

before the legitimate response was released.

A separate control showed that an attacker-influenced cluster containing the local node endpoint passed etcdFixCluster and became a valid InitialCluster candidate.

I submitted this report as Major through the WEMIX bug bounty program hosted on CertiK Skynet.

The program classified it as Low and paid a $100 bounty.

That classification does not match the demonstrated impact.

This was not a harmless unsolicited packet.

It was request confusion in a privileged P2P control plane used for mining coordination.

The complete public report and proof of concept are available in the GitHub Gist.

The security invariant

WEMIX uses etcd as part of the coordination layer for mining nodes.

A node joining that cluster needs an InitialCluster configuration describing the expected members and endpoints.

The request-response invariant should be simple:

A join request sent for peer A must be completed only by a response correlated with peer A and that pending request.

Partner status alone is not enough.

A peer may be authorized to participate in the partner protocol without being authorized to answer every join request currently pending on the target.

EtcdJoin selected a legitimate peer

The request side knew which peer it wanted:

func (
    ma *wemixAdmin,
) etcdJoin(
    name string,
) error {
    var node *wemixNode

    ma.lock.Lock()

    for _, i :=
        range ma.nodes {
        if i.Name == name ||
            i.Enode == name ||
            i.Id == name ||
            i.Ip == name {
            node = i
            break
        }
    }

    ma.lock.Unlock()

    if node == nil {
        return ethereum.NotFound
    }
Enter fullscreen mode Exit fullscreen mode

The function later issued:

err :=
    admin.rpcCli.CallContext(
        ctx,
        nil,
        "admin_requestEtcdAddMember",
        &node.Id,
    )
Enter fullscreen mode Exit fullscreen mode

In the PoC, the selected peer was:

Name
legit

ID
legit-id
Enter fullscreen mode Exit fullscreen mode

The RPC boundary confirmed that etcdJoin requested the correct ID.

The failure was not incorrect peer selection.

The failure was losing that identity while waiting for the response.

The response path discarded correlation

The subscription API exposed a process-wide feed of strings:

func SubscribeToEtcdCluster(
    ch chan string,
) event.Subscription {
    return etcdClusterFeed.Subscribe(
        ch,
    )
}

func GotEtcdCluster(
    cluster string,
) {
    etcdClusterFeed.Send(
        cluster,
    )
}
Enter fullscreen mode Exit fullscreen mode

The consumer received no origin information.

Once a value entered the feed, etcdJoin could not determine:

Which peer sent it

Which request it answered

Whether it was solicited

Whether it arrived for another concurrent operation
Enter fullscreen mode Exit fullscreen mode

This is the root cause.

A broadcast notification channel was used as though it were a correlated response channel.

Any accepted partner packet entered that feed

The P2P handler checked whether both the local node and the sending peer were WEMIX partners:

func handleEtcdCluster(
    backend Backend,
    msg Decoder,
    peer *Peer,
) error {
    if !wemixminer.AmPartner() ||
        !wemixminer.IsPartner(
            peer.ID(),
        ) {
        return nil
    }

    var cluster string

    if err :=
        msg.Decode(
            &cluster,
        );
        err != nil {
        return fmt.Errorf(
            "%w: message %v: %v",
            errDecode,
            msg,
            err,
        )
    }

    go wemixapi.GotEtcdCluster(
        cluster,
    )

    return nil
}
Enter fullscreen mode Exit fullscreen mode

The partner check is a real exploitation precondition.

The report does not claim that an arbitrary public peer can reach this path.

But after the check, peer.ID() was discarded.

Only the cluster string survived.

The code transformed:

A message from partner X
Enter fullscreen mode Exit fullscreen mode

into:

An originless global event
Enter fullscreen mode Exit fullscreen mode

That allowed one partner to satisfy a request intended for another.

First response wins

The relevant response branch inside etcdJoin was:

case cluster :=
    <-ch:

    cluster, err :=
        ma.etcdFixCluster(
            cluster,
        )

    if err != nil {
        log.Error(
            "etcd failed to join",
            "error",
            err,
        )

        return err
    }

    cfg :=
        ma.etcdNewConfig(
            false,
        )

    cfg.InitialCluster =
        cluster

    etcd, err :=
        embed.StartEtcd(
            cfg,
        )
Enter fullscreen mode Exit fullscreen mode

The first delivered value controlled the attempt.

If etcdFixCluster rejected it, etcdJoin returned immediately.

It did not ignore the unrelated packet and continue waiting for the expected peer.

If the value passed parsing, the result became:

cfg.InitialCluster =
    cluster
Enter fullscreen mode Exit fullscreen mode

The code never compared the response against:

node.Id

Expected partner

Pending request ID
Enter fullscreen mode Exit fullscreen mode

The parser could reject or preserve attacker input

etcdFixCluster required the local node to appear in the candidate cluster.

If the local entry was absent, it returned:

ethereum.NotFound
Enter fullscreen mode Exit fullscreen mode

If the local endpoint was present, the function could normalize that entry while preserving other supplied members.

The PoC used two attacker strings.

Deterministic stall input

evil=https://203.0.113.10:2380
Enter fullscreen mode Exit fullscreen mode

This omitted the local node.

The real parser rejected it.

Because etcdJoin treated the packet as the awaited response, the whole join attempt returned before the legitimate response arrived.

Redirect candidate input

evil=https://203.0.113.10:2380,=https://127.0.0.1:50001
Enter fullscreen mode Exit fullscreen mode

This included:

An attacker-controlled member

The expected local endpoint
Enter fullscreen mode Exit fullscreen mode

The parser accepted it and returned a fixed cluster candidate.

The proof deliberately did not start a persistent etcd process with that value.

That kept the test deterministic and bounded.

The directly demonstrated result is therefore:

The wrong peer can terminate the real join
Enter fullscreen mode Exit fullscreen mode

and:

An attacker-influenced redirect candidate can pass the real parser
Enter fullscreen mode Exit fullscreen mode

The article does not claim that the PoC maintained a live malicious etcd cluster.

The complete stalled-join sequence

The proof executed this flow:

1. The target called etcdJoin("legit")

2. EtcdJoin selected legit-id

3. It subscribed to the global EtcdCluster feed

4. It sent admin_requestEtcdAddMember for legit-id

5. The legitimate response was delayed

6. The attacker cluster was emitted first through GotEtcdCluster

7. The global feed delivered that value to the real etcdJoin

8. EtcdFixCluster returned not found

9. EtcdJoin returned before the legitimate response was released

10. The later legitimate response could not complete the finished attempt
Enter fullscreen mode Exit fullscreen mode

The recorded result was:

RPC requested peer ID
legit-id

Malicious cluster emitted first
True

EtcdJoin consumed attacker cluster first
True

EtcdJoin returned before legitimate response
True

Join error
not found
Enter fullscreen mode Exit fullscreen mode

That is deterministic request confusion.

The legitimate response arrived too late

The in-process RPC boundary delayed the legitimate cluster until after the attacker value had already caused etcdJoin to return.

The test then released:

wemixapi.GotEtcdCluster(
    r.legit,
)
Enter fullscreen mode Exit fullscreen mode

The legitimate cluster itself was valid.

The real parser accepted it in the direct control.

But the request had already been completed by the wrong value.

The bug was therefore not:

The legitimate peer returned malformed data
Enter fullscreen mode Exit fullscreen mode

It was:

The consumer accepted an unrelated response first
Enter fullscreen mode Exit fullscreen mode

The protocol already recognized request IDs

The P2P layer contained request-tracking machinery:

func (
    p *Peer,
) RequestEtcdAddMember() error {
    id :=
        rand.Uint64()

    requestTracker.Track(
        p.id,
        p.version,
        EtcdAddMemberMsg,
        EtcdClusterMsg,
        id,
    )

    return p2p.Send(
        p.rw,
        EtcdAddMemberMsg,
        common.Big1,
    )
}
Enter fullscreen mode Exit fullscreen mode

The eth66 response shape also contained:

type EtcdClusterPacket66 struct {
    RequestId uint64
    EtcdClusterPacket
}
Enter fullscreen mode Exit fullscreen mode

The problem was not the complete absence of a correlation concept in the codebase.

The problem was that etcdJoin ultimately consumed only:

chan string
Enter fullscreen mode Exit fullscreen mode

By the time the cluster reached the waiting join, peer and request context were gone.

What the PoC used

The proof used the real:

wemixAdmin.etcdJoin

SubscribeToEtcdCluster

GotEtcdCluster

etcdFixCluster

Node-selection logic

First-response consumption behavior
Enter fullscreen mode Exit fullscreen mode

An in-process RPC boundary was used only to:

Record admin_requestEtcdAddMember

Confirm legit-id was requested

Delay the legitimate response
Enter fullscreen mode Exit fullscreen mode

It did not publish the malicious value.

It did not decide the final error.

The attacker value entered through the same GotEtcdCluster sink used by the real P2P handler after partner validation.

The real etcdJoin consumed it.

The real etcdFixCluster returned ethereum.NotFound.

The command was:

go test ./wemix \
    -run TestEtcd \
    -count=1 \
    -v
Enter fullscreen mode Exit fullscreen mode

The test passed.

The controls isolated the issue

The PoC verified:

Real EtcdJoin used
True

Real EtcdFixCluster used
True

Real global feed used
True

Requested peer
legit-id

Feed contains peer identity
False

Feed contains request ID
False

Join consumed attacker first
True

Join returned before legitimate response
True

Legitimate cluster passes parser
True

Invalid attacker cluster fails parser
True

Attacker redirect candidate passes parser
True

Persistent etcd process started in primary path
False
Enter fullscreen mode Exit fullscreen mode

These controls matter because they separate three questions:

Was the correct peer requested?
Yes

Did the consumer know who answered?
No

Could the attacker value control the result?
Yes
Enter fullscreen mode Exit fullscreen mode

What the proof establishes

The proof demonstrates:

EtcdJoin selected a specific legitimate peer

The RPC request contained legit-id

The response feed had no peer binding

The response feed had no request binding

An unsolicited attacker value arrived first

The real join consumed it

The real join returned before the legitimate response

A malformed attacker value caused deterministic join failure

An attacker-influenced cluster passed the real parser
Enter fullscreen mode Exit fullscreen mode

The proof does not demonstrate:

Direct fund theft

A completed live-network exploit

Total network shutdown

Permanent consensus failure

A persistent attacker-controlled etcd process

Exploitation by a non-partner peer
Enter fullscreen mode Exit fullscreen mode

Those limits are why the report was submitted as Major rather than Critical.

Why this is Major, not Low

The impact is a privileged P2P control-plane failure affecting mining coordination.

The request was completed by the wrong peer

The target requested legit-id.

The consumer accepted an originless value that could have come from another partner.

That is a failure of request-response integrity.

The join failed through the real path

The attacker value was not rejected as unrelated.

It reached etcdFixCluster.

The function returned not found.

The real etcdJoin terminated before the legitimate response.

The attacker could influence the cluster candidate

The redirect control passed the real parser while preserving the attacker-controlled member.

The PoC did not start etcd with that candidate, but it proved the value crossed the validation step that feeds InitialCluster.

The affected path coordinates mining nodes

This was not a UI or monitoring endpoint.

The function joins the etcd cluster used by mining nodes.

A failed or attacker-influenced join can prevent the target from coordinating as intended and can contribute to missed production opportunities.

The attack is privileged but not administrative

A real exploit requires a WEMIX partner peer.

It does not require:

Governance control

Administrator credentials

Validator private keys

Public RPC manipulation
Enter fullscreen mode Exit fullscreen mode

Partner membership should permit protocol participation.

It should not permit one peer to impersonate another peer’s response.

The failure can be raced again

A failed attempt requires another join.

Without correlation, the same class of unsolicited partner packet can race the next attempt as well.

Low does not fit the demonstrated result

Low would fit:

An unsolicited packet ignored before use

A stale status update

A harmless parser discrepancy

A message with no effect on a pending operation

A UI-only failure
Enter fullscreen mode Exit fullscreen mode

This report showed the real mining-coordination join being terminated by the wrong response.

That is Major.

Why the $100 outcome understates the evidence

The final Low classification produced a $100 bounty.

That records the program decision.

It does not change the result:

Requested peer
legit-id

Consumed response
Attacker cluster

Peer identity in feed
None

Request ID in feed
None

Join returned before legitimate response
True

Join result
not found

Attacker-influenced redirect candidate accepted
True
Enter fullscreen mode Exit fullscreen mode

The strongest directly demonstrated impact was a malicious partner controlling the result of another peer’s pending mining-coordination join.

That is a Major control-plane and liveness issue.

Why I describe the WEMIX handling as systematic downgrading

Severity disagreements are normal in bug bounty programs.

My concern is the recurring outcome across my own valid WEMIX submissions.

Reports supported by reproducible proofs and concrete protocol behavior were repeatedly assigned substantially lower final severities.

This report was submitted as Major.

The proof showed the real etcdJoin requesting legit-id, consuming an unsolicited attacker value, and returning before the legitimate response.

It also showed that the consumer received neither peer identity nor request ID and that an attacker-influenced redirect candidate passed etcdFixCluster.

The report was classified as Low.

I cannot infer intent from that decision.

I can document the repeated outcome and compare the final classification with the technical evidence.

Calling it systematic downgrading describes that recurring pattern across my WEMIX submissions without claiming a motive.

A technically convincing Low justification would need to explain why these effects are minor:

One partner satisfying another peer’s pending request

No peer binding

No request-ID binding

The real join consuming the attacker value

The join returning before the legitimate response

Attacker-controlled membership surviving parser validation

Mining coordination for the target being stalled or influenced
Enter fullscreen mode Exit fullscreen mode

Without addressing those facts, the Low classification does not match the proof.

The correct fix

The response consumed by etcdJoin must preserve correlation context.

A structured response can carry:

type EtcdClusterResponse struct {
    PeerID    string
    RequestID uint64
    Cluster   string
}
Enter fullscreen mode Exit fullscreen mode

The pending join should record:

Expected peer ID

Expected request ID
Enter fullscreen mode Exit fullscreen mode

The handler should preserve:

Actual sending peer ID

Received request ID

Cluster string
Enter fullscreen mode Exit fullscreen mode

The waiting join must ignore any response where:

PeerID does not match
Enter fullscreen mode Exit fullscreen mode

or:

RequestID does not match
Enter fullscreen mode Exit fullscreen mode

The existing global string feed may remain for telemetry or legacy observers.

It must not control a correlated join operation.

Safer lifecycle

The corrected flow should be:

1. EtcdJoin selects legit-id

2. The request receives a unique request ID

3. The pending operation stores both values

4. A partner response arrives

5. The handler preserves peer and request identity

6. EtcdJoin rejects unrelated peers

7. EtcdJoin rejects mismatched request IDs

8. Only the matching cluster reaches etcdFixCluster

9. Only the validated matching result becomes InitialCluster
Enter fullscreen mode Exit fullscreen mode

An unsolicited packet can then be logged or penalized without completing the join.

Regression tests

A complete correction should verify:

  1. The expected peer with the correct request ID completes the join

  2. Another partner’s response is ignored

  3. A mismatched request ID is ignored

  4. An unsolicited packet arriving first cannot terminate the operation

  5. An unrelated malformed cluster cannot make etcdJoin return

  6. An attacker-influenced cluster from the wrong peer cannot become InitialCluster

  7. Concurrent joins cannot consume each other’s responses

  8. Timeout behavior remains bounded when no matching response arrives

The central invariant is:

Only a response matching both the intended peer and the pending request may complete an etcd join.

Broader lessons

Partner status is not response correlation

A partner can be authorized to use a privileged protocol without being authorized to answer every pending request.

Global feeds are wrong for correlated responses

Broadcast feeds are useful for notifications.

They are unsafe when exactly one response must complete exactly one request.

First response wins is not authentication

Timing decided which cluster value etcdJoin trusted.

A peer that arrived first controlled the attempt.

Control-plane messages need context

A cluster string is not enough.

The consumer also needs:

Who sent it

Which request it answers
Enter fullscreen mode Exit fullscreen mode

Targeted liveness failures still matter

A vulnerability does not need to stop the entire network to affect blockchain operations.

Disrupting one mining node’s coordination path can still cost production opportunities and degrade liveness for that target.

Conclusion

The target selected:

Peer
legit

Peer ID
legit-id
Enter fullscreen mode Exit fullscreen mode

But it waited on a global feed carrying only:

Cluster string
Enter fullscreen mode Exit fullscreen mode

A malicious partner emitted an unsolicited value first.

The real etcdJoin consumed it.

The feed contained no peer identity.

It contained no request ID.

The join returned not found before the legitimate response arrived.

A separate control proved that an attacker-influenced cluster containing the local endpoint passed etcdFixCluster.

The primary PoC did not start a persistent malicious etcd process.

It proved the underlying defect:

The first accepted partner cluster string could complete a pending join regardless of which peer or request it belonged to.

The program classified the report as Low and paid $100.

The demonstrated impact was Major.

The report did not claim theft, total network shutdown, or permanent consensus failure.

It demonstrated a narrower and directly proven failure:

A malicious partner peer could terminate another node’s mining-coordination join and could supply an attacker-influenced cluster candidate by winning an uncorrelated response race.

A privileged P2P response-confusion bug that controls a mining node’s join attempt is not a Low-severity issue.

Top comments (0)