In this part 2 of the series, we’ll examine how to exploit the “private” vulnerability in Solidity.
As the name suggests, private means not accessible to anyone outside.
- Can we expect the same in Solidity smart contracts?
- Is any variable declared as ” private” in Solidity is not accessible from the outside world?
If the answer is yes, then the assumption may be wrong. It is possible to access the private variables of smart contracts from the outside world.
For the same, the key is to understand the storage structure and its arrangement in Solidity.
The sections below detail the storage structure, followed by the exploit, and ultimately the conclusion.
Let’s begin!
Storage Structure
In Solidity, the variables are stored in either storage or memory.
The storage section can be viewed as writing to the hard disk i.e. permanent storage and writing to memory can be viewed as writing to RAM i.e. temporary storage.
The storage is arranged as slots. The total size of the storage is 2^256 bytes and each slot occupies 32 bytes each.
The data will be stored sequentially in the order in which they are declared.
To save space, the storage performs optimizations to accommodate neighboring variables if they fit within the 32 bytes, and in such cases, they are packed into the same slot.
The storage arrangement is given below.
As shown storage slot starts from 0 up to 2^256 and each storage slot is 32 bytes.
For an example contract, see how the variables get stored in the slots.
contract StorageArrangment { uint256 public a; // gets stored in Slot 0 = 32 bytes bytes32 public b; // gets stored in Slot 1 = 32 bytes address public addr; // gets stored in Slot 2 = 20 bytes bool public mybool; // gets stored in Slot 2 = 1 byte uint16 public c; // gets stored in Slot 2 = 2 bytes }
Slot0 is 32 bytes, Slot1 is 32 bytes, and Slot2 is 23 bytes. Thus as can be seen Slot2 accommodates the neighbors to save space.
In the case of inheritance, the storage variables of the base contracts take the first slot of the storage and afterward the derived contract storage variables.
Exploit
To exploit the private variables of the contract consider a simple contract named privateExploit.sol
that performs a deposit and withdrawal as follows.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.9; import "@openzeppelin/contracts/access/Ownable.sol"; contract PrivateExploit is Ownable{ uint256 public customerid; bytes32 private _password; constructor(uint256 id, bytes32 password){ customerid = id; _password = password; } modifier verifyPassword(bytes32 password) { require(password == _password, "Password does not match"); _; } function depositFunds(uint256 _amount) payable public onlyOwner { // deposit amount to the contract } function withdrawFunds(bytes32 password) public verifyPassword(password){ (bool os, ) = payable(msg.sender).call{value:address(this).balance}(""); require(os, "Failed to withdraw funds!"); } }
The PrivateExploit
contract is described in brief.
- The contract is inheriting
Ownable
fromOpenZeppelin
to ensure that only the owner can operate on certain functions. - The constructor initializes the customer id and a password.
depositFunds()
for depositing the amount. It is defined but not needed for our exploit.withdrawFunds()
which takes the password as input, verifies the password using the modifierverifyPassword
. If the password matches it will transfer the balance to themsg.sender
.
Using Truffle or (any other tool such as Hardhat) deploy the contract. For the same on the terminal.
$ truffle develop
Next would be to migrate so as to deploy the contract
???? Note: You need to write the migration file. If you are new to Truffle please follow the guide here for writing deployment scripts.
During deployment, the constructor params need to be passed. The deployment script is given below.
const privateexploit = artifacts.require("PrivateExploit"); module.exports = function (deployer) { deployer.deploy(privateexploit, 1, "0x1234567890123456789012345678901234567890123456789012345678903132"); };
As the password is bytes32, I have set the password to.
"0x1234567890123456789012345678901234567890123456789012345678903132"
Feel free to use any customer id and the password.
$truffle migrate
After the deployment is successful, you can now interact with the contract using the web3 API.
To exploit the private variables we make use of the web3 API:
web3.eth.getStorageAt(addressHexString, position [, defaultBlock] [, callback])
Each contract is made up of EVM bytecodes that handle the execution and storage of the contract’s state. This is a low-level function that returns the storage state of the contract. The data is stored in a key/value store.
The getStorageAt()
function returns the value of the contract’s storage at a specific point.
Let’s use this and make the exploit. As seen from the above fig, the contract is deployed at the address “0xcFb962032Ba7A16eb7e5e1059BB3531E7d38CcfE
”.
In the above figure, the first param is the contract address, the second param is the slot number (0,1,2,..etc), and a callback function (console.log
).
Due to inheritance, the Slot0
is occupied by the storage variables of the base contract, in this case, Ownable
. Slot1
returns 0x01
(the customer id) and lastly Slot2
returns the password “0x1234567890123456789012345678901234567890123456789012345678903132
”.
The password can then easily be used to withdraw the funds by the attacker.
Conclusion
In this part we used the web3 API getStorageAt()
to exploit the private variables of the contract.
Ensure that you don’t store any sensitive information such as passwords, user names, private keys on the blockchain.
As the keyword is private, the user is tempted to assume that the variable is not visible outside the contract, but as we saw in this post this is untrue.
Happy Hacking — i.e., preventing the same! ????