Challenge Description
We’ve created a simple contract to store a secret flag. But you currently are not the owner of the contract… Only the owner can access the flag. Can you figure out how to become the owner?
Approach
This is a classic smart contract access control vulnerability challenge. The contract stores a secret flag and restricts access to the “owner,” but the access control mechanism has a flaw that allows anyone to become the owner.
Common Blockchain Access Control Vulnerabilities
- Unprotected state-changing functions: A function that sets the owner lacks an access modifier (e.g.,
onlyOwner), allowing anyone to call it. - tx.origin vs msg.sender confusion: Using
tx.originfor authentication instead ofmsg.senderenables phishing attacks. - Public visibility on sensitive functions: Functions intended to be internal/private are accidentally left public.
- Initialization race condition: The constructor or initializer can be called by anyone, or there is a separate
initialize()function that was never called (or can be called again). - Storage slot reading: Even “private” variables in Solidity are readable on-chain — nothing on the blockchain is truly private.
Typical Contract Pattern
pragma solidity ^0.8.0;
contract AccessControl {
address public owner;
string private flag;
constructor(string memory _flag) {
owner = msg.sender;
flag = _flag;
}
// VULNERABILITY: No access control on this function!
function setOwner(address _newOwner) public {
owner = _newOwner;
}
// Or alternatively, a flawed modifier:
// function setOwner(address _newOwner) public {
// require(tx.origin == owner); // vulnerable to phishing
// owner = _newOwner;
// }
function getFlag() public view returns (string memory) {
require(msg.sender == owner, "Not the owner!");
return flag;
}
}
Attack Vectors
Approach A — Unprotected setOwner / changeOwner: Simply call the function that changes ownership with your own address.
Approach B — Read storage directly: Even if you cannot become the owner, the flag stored as a “private” variable can be read directly from the blockchain storage slot using web3.eth.getStorageAt().
Approach C — Unprotected initializer: Call an initialize() or init() function that was left unprotected.
Solution
- Connect to the provided RPC endpoint using the challenge credentials (typically an RPC URL, contract address, and a funded wallet private key).
- Read the contract ABI or source code (usually provided or discoverable via the challenge).
- Identify the vulnerability: Look for an unprotected function that modifies the
ownerstate variable. - Call the vulnerable function to set yourself as the owner.
- Call
getFlag()to retrieve the flag. - Alternatively, read the storage slot directly to bypass access control entirely.
Solution Script
python3 solve.py
Flag
picoCTF{...} (placeholder - actual flag varies per instance)