Attackathon _ Fuel Network 32965 - [Blockchain_DLT - Critical] Messages to L included even on revert
Submitted on Mon Jul 08 2024 15:33:31 GMT-0400 (Atlantic Standard Time) by @NinetyNineCrits for Attackathon | Fuel Network
Report ID: #32965
Report type: Blockchain/DLT
Report severity: Critical
Target: https://github.com/FuelLabs/fuel-core/tree/v0.31.0
Impacts:
Direct loss of funds
Description
Brief/Intro
Messages to the L1 bridge are included in the block even in case of reverts. This allows theft of all tokens from the bridge.
Vulnerability Details
The function executor/src/executor.rs::update_execution_data adds all message_ids from the MessageOut receipts of the current tx to the execution data even if the tx itself has reverted:
execution_data
.message_ids
.extend(receipts.iter().filter_map(|r| r.message_id()));
let status = if reverted {
TransactionExecutionResult::Failed {
result: Some(state),
receipts,
total_gas: used_gas,
total_fee: tx_fee,
}
} else {
// else tx was a success
TransactionExecutionResult::Success {
result: Some(state),
receipts,
total_gas: used_gas,
total_fee: tx_fee,
}
};This means its possible to send out messages to the L1 but revert the tx, which un-does the burn of the bridged tokens. This fake-withdraw can be repeated over and over again and messages can still be relayed on L1, allowing theft of all tokens from the bridge
Impact Details
Theft of all tokens from the bridge on L1
References
Not applicable
Proof of concept
Proof of Concept
Link to the gist: https://gist.github.com/99crits/3ee583a149bc41ca62e76ccfbf7c0e89
This poc is based on the tests/bridge_erc20.ts file in the fuel-bridge repository. It demonstrates the following flow:
Attacker and victim both deposit the same amount of the same token
Attacker withdraws 2 times, first time he reverts his tx at the end and the second time follows the regular flow. He can relay twice and will have double the amount of tokens he originally deposited.
Victim withdraws, L2 side will be successful and his tokens will be burned. The relay on L1 will fail, due to underflow (no tokens left)
This test requires a minor modification to the message_proof endpoint on the client. Currently this endpoint does an early return if the tx has not succeeded:
//@note query/message.rs
let message_block_height = match database
.transaction_status(&transaction_id)
.into_api_result::<TransactionStatus, StorageError>(
)? {
Some(TransactionStatus::Success { block_height, .. }) => block_height,
_ => return Ok(None),
};However any attacker can run his own client and modify the endpoint like this:
let message_block_height = match database
.transaction_status(&transaction_id)
.into_api_result::<TransactionStatus, StorageError>(
)? {
Some(TransactionStatus::Success { block_height, .. }) => block_height,
Some(TransactionStatus::Failed { block_height, .. }) => block_height,
_ => return Ok(None),
};All the necessary data for the proof is already on-chain, this modification just allows an easy retrieval of the necessary message-proof.
So the setup steps are the following:
Clone
fuel-corefrom commit c5b425e2b3e05899e83bed0090865a7f4ec30c78 (latest one as of 2024-07-08)Add the line
Some(TransactionStatus::Failed { block_height, .. }) => block_height,as additional matching branch toquery/message.rsas shown aboveRun
docker build -t 99crits-fuel-core . -f deployment/Dockerfile(command from the README with custom tag)Now in the
fuel-bridgerepository: Indocker/fuel-core/Dockerfilechange theFROMdirective toFROM 99crits-fuel-core:latestChange to
fuel-bridge/dockerdirectory and runmake cleanand thenmake upThen, go to the
fuel-bridge/packages/integration-tests/directory and runpnpm installFinally, paste the gist from above as
bridge_erc20_multi_withdrawal_using_reverts.tsintointegration-tests/testsand then runpnpm mocha -b -r ts-node/register 'tests/bridge_erc20_multi_withdrawal_using_reverts.ts'
Last updated
Was this helpful?