33637 - [BC - Critical] In get_tx_timestamp a prototype pollution bri...
In " get_tx_timestamp " a prototype pollution bricks validators
Submitted on Jul 25th 2024 at 13:08:56 UTC by @infosec_us_team for Boost | Shardeum: Core
Report ID: #33637
Report type: Blockchain/DLT
Report severity: Critical
Target: https://github.com/shardeum/shardus-core/tree/dev
Impacts:
Network not being able to confirm new transactions (total network shutdown)
Description
About The Scope
Where is the bug?
The issue is an insecure assignment in line #1049 of TransactionConsensus.ts
.
https://github.com/shardeum/shardus-core/blob/dev/src/state-manager/TransactionConsensus.ts#L1049
What are the scope rules of TransactionConsensus.ts?
@Mehdi from Shardeum team publicly said in Immunefi's Discord about TransactionConsensus.ts
:
"that was originally out of scope, but it's more accurate to say large parts of TransactionConsensus are out of scope. Large parts of it hold old code that is not active with the default config, namely: code paths that are behind useNewPOQ flag."
https://discord.com/channels/787092485969150012/1256211020482084987/1263282057769914368
They also confirmed that:
"Bugs in code from
TransactionConsensu.ts
that are not guarded by any configurable flag at all (like useNewPOQ and others), and that can be directly exploited by anyone, are in scope.https://discord.com/channels/787092485969150012/1256211020482084987/1264362678550265877
This report is in scope
The insecure assignment can be exploited by sending a request to an entry point that isn't guarded by any configurable flag.
Report Description
Location of the insecure assignment
The function generateTimestampReceipt(...)
inside TransactionConsensus.ts
contains the assignment below, where the value of signedTsReceipt.cycleCounter
and txId
are strings controlled by an attacker, and signedTsReceipt
is an object:
Line of code from: https://github.com/shardeum/shardus-core/blob/dev/src/state-manager/TransactionConsensus.ts#L1049
Sending '__proto__'
as the value for signedTsReceipt.cycleCounter
and sending 'somethingHere'
as the value for the variable txId
, the assignment becomes:
If the reader has experience exploiting server-side prototype pollution, the next section can be skipped, but we recommend reading it as a quick recap.
Understanding Prototype Pollution
Before diving deep into the report, is important to understand what the vulnerable assignment in Shardus Core does.
Let's start by mentioning the following line:
Produces the same outcome as the following line:
Executing any of them adds by default a field something
with value 1
to all new and previously created javascript objects during runtime.
Runnable Example:
In Typescript create an empty object out of JSON named person:
Pollute the prototype by adding a field whitehat
with the value true
:
Now the object person.whitehat
returns true:
But also does all other objects in the entire codebase, whether new or existing. For example, create a completely new object and log its whitehat
field:
Here's the full snippet of code for the example if you want to play around:
Exploiting the attack vector in Shardus Core
Active nodes can gossip other validators using the internal route get_tx_timestamp
to ask for or store the timestamp of a tx.
Below is the code that handles this request:
Snippet of code from: https://github.com/shardeum/shardus-core/blob/dev/src/state-manager/TransactionConsensus.ts#L242-L262
First, the server checks if the TX is in the cache. If that's the case, it returns its value, if not, it saves to the cache the values given in the request.
The function generateTimestampReceipt(...)
is the one that stores the received value in the cache.
Snippet of code from: https://github.com/shardeum/shardus-core/blob/dev/src/state-manager/TransactionConsensus.ts#L1029-L1051
There, we can see the vulnerable assignment once again:
this.txTimestampCache[signedTsReceipt.cycleCounter][txId] = signedTsReceipt
Adding fields to all Objects in the server
If the gossipped message to the get_tx_timestamp
route contains the following payload:
Where "bsdsdsdsd" is random gibberish.
Then, the vulnerable assignment becomes:
As a consequence, all objects in existence and new objects created in the future will contain a field . bsdsdsdsd
and the value will be the content of the signedTsReceipt
object.
A chain of crashes in Shardus Core
Many core functions in Shardus Core read and process the keys and values of an object.
When an attacker adds an unexpected key and value to ALL objects in the server, these functions crash, when they crash the server tries to exit by calling the function exitUncleanly(..)
, but even exitUncleanly(..)
crashes.
An infinite loop of crash -> try to exit -> repeat begins.
Let's analyze one of the functions that starts the crashing loop: Take a look at these lines of code in the _takeSnapshot()
function (which is executed repeatedly, with a timer):
Code snippet from: https://github.com/shardeum/shardus-core/blob/dev/src/statistics/index.ts#L337-L362
One of the keys returned by the object this.counters after receiving the malicious payload, is bsdsdsdsd, which is the gibberish we used in the attack to pollute ALL objects in the server.
The function will crash when trying to execute: this.counters[ 'bsdsdsdsd' ].snapshot()
because the value of bsdsdsdsd does not contain a function named "snapshot()".
The following error is printed to
./instances/shardus-instance-PORT_OF_VICTIM/logs/out.log
The following error is printed to the same path, but file
fatal.log
When that line crashes, the exception handler registered here https://github.com/shardeum/shardus-core/blob/dev/src/shardus/index.ts#L2856-L2887 will try to shut down the server uncleanly.
Forcing nodes to keep running under invalid states
But the exit attempt throws an exception as well, with the following stack trace:
It throws an exception because it tries to execute this.powGenerators[generator].kill
where generator
is a key read from an object. All objects now include an additional key named "bsdsdsdsd", and the value of that key does not contain a function named kill
, therefore it throws an exception when calling this.powGenerators['bsdsdsdsd'].kill()
Quick recap before moving to the next part of the exploit
What we can achieve so far:
1- A victim node can be moved to a state where some functions are constantly crashing and others will crash on demand.
2- 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.
Forcing the victim node to crash all internal gossip
When a node is selected to become active, he can send and receive messages to other active nodes using an "internal gossip route
".
The application is notified of nodes that are lost or unresponsive and can choose to slash them.
This section of the report focuses on making a victim node slashable: The node will sync data as usual, but will never respond to internal gossip.
When receiving a gossiped message, the code attempts to extract the payload in the following lines:
Snippet of code from: https://github.com/shardeum/shardus-core/blob/dev/src/p2p/Comms.ts#L183-L184
The field error
in the payload is optional, and a check is made to ensure that if it exists, it has to be of type string. If not, it throws an exception.
To proceed with the exploit, we gossip a message to the internal route that is vulnerable to prototype pollution ("get_tx_timestamp
") with the following payload:
Then, the vulnerable assignment becomes:
Which pollutes all existing objects with a field
error
that contains a value of type object (signedTsReceipt is an object).
Now, when the victim node receives any internal gossip message, the code will try to unwrap the payload and realize a field error
exists. When checking if it is a string, it will fail and throw the following error:
Impact of this report
A malicious node can either target a single node or a group of nodes with a prototype pollution attack that:
1- Moves nodes into an invalid state that constantly crashes.
2- Forces these nodes to continue working under any invalid state, by crashing all automatic attempts to exit.
3- Makes the victim nodes susceptible to slashing, by allowing them to operate basic actions as usual but to throw an exception in all internal gossip routes.
A slashing event creates a loss of funds for a node.
Continuously exploiting legit active nodes with this attack vector, every time a malicious node is selected as an active validator, leads to a bricked network.
In the worst-case scenario 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.
Press Enter on your keyboard to execute the code.
State of the victim node after sending the payload
If the steps were followed correctly:
The browser console will output:
The Network Monitor running at port 3000 will paint in red the victim node and later remove it from the screen:
A screenshot here: https://ibb.co/zJN4qGK
The output of running
shardus pm2 list
shows that all nodes are online:
The victim node starts to throw exceptions:
Below is the first one - we explain in the report exactly why it throws:
Verify this in the log file located at
./instances/shardus-instance-9003/logs/fatal.log
Right after firing this exception, the victim node will attempt to exit uncleanly, but that process throws another exception:
Verify this in the log file located at
./instances/shardus-instance-9003/logs/fatal.log
Then it keeps encountering exceptions at runtime and logging them in the same file.
More details about the payload the victim received and the chain of crashes can be read at ./instances/shardus-instance-9003/logs/main.log
:
Prove of victim node rejecting all internal gossips
Go back to the developer's console and run the following:
This time, we are sending a normal payload, without exploiting the prototype pollution in any way. The payload we are sending is { txId: "test", cycleCounter: "test", cycleMarker: "test" }
After sending, quickly go to ./instances/shardus-instance-9003/logs/p2p.log
and you will see the following error being logged:
The relevant segment is Comms: extractPayload: bad wrappedPayload: error must be, string.
We have polluted the value error
of all objects in the victim node, and now everywhere in Shardus Core and Shardeum that is enforced this value to be a string in case it exists, will throw an exception.
Last updated