Access_Control picoCTF 2026 Solution

Published: March 20, 2026

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 of the contract should be able to access it. Contract: here

Download and read AccessControl.sol.

Set up Foundry or cast/web3 to interact with the deployed contract.

bash
cat AccessControl.sol
  1. Step 1Read AccessControl.sol and find the exposed function
    Open the source and look for any externally callable function that writes to owner. In this contract there's a public claimOwnership() with no msg.sender check, so anyone can call it. See smart contract CTF bugs for the full taxonomy of access control flaws.
    bash
    cat AccessControl.sol
    Learn more

    Access control in Solidity smart contracts relies on the developer correctly checking who is calling a function. The canonical pattern is an onlyOwner modifier that compares msg.sender to a stored owner address. When this check is missing, broken, or bypassable, any caller can claim ownership.

    The four flaws to scan for in a CTF contract:

    1. The owner variable is public and set in a function anyone can call (this challenge).
    2. tx.origin is used instead of msg.sender. tx.origin is always the EOA that signed the outermost transaction, so a victim calling an attacker contract still authenticates as the victim.
    3. The initialize() function lacks a flag preventing re-initialization, letting anyone replay it after deployment.
    4. A pre-0.5 contract uses a same-named function as a constructor (typo or deliberate), making it permanently callable.

    The Foundry toolkit's cast command-line tool is invaluable for interacting with deployed contracts without writing a full script. cast call reads state, cast send sends transactions. Read the Solidity source first, always.

  2. Step 2Become the owner
    Send a transaction to the exposed function with your funded private key. Replace CONTRACT, YOUR_ADDR, RPC_URL, and PRIVATE_KEY with the values from the instance page.
    bash
    YOUR_ADDR=$(cast wallet address --private-key $PRIVATE_KEY)
    bash
    cast send $CONTRACT 'claimOwnership()' --from $YOUR_ADDR --rpc-url $RPC_URL --private-key $PRIVATE_KEY
    bash
    # Alternate flaw: setOwner(address) is public
    bash
    cast send $CONTRACT 'setOwner(address)' $YOUR_ADDR --from $YOUR_ADDR --rpc-url $RPC_URL --private-key $PRIVATE_KEY
    Learn more

    In Ethereum, all state-changing contract calls are transactions that must be signed by a private key. The msg.sender global variable always holds the address that signed the current transaction. If a contract allows anyone to call a function that sets owner = msg.sender, you simply call it and you become the owner.

    The Uninitialized Storage Pattern is a related vulnerability: if a proxy contract's storage slot for the implementation address overlaps with the owner slot, an attacker can set the implementation to their own contract and upgrade themselves to owner. This affected several real DeFi protocols.

    For CTF challenges, the challenge typically provides a funded private key (shown on the instance page). Use cast wallet address --private-key KEY to derive your address, then sign transactions with --private-key. Always confirm state changes with cast call after each cast send.

  3. Step 3Verify ownership transferred
    Before reading the flag, confirm the previous step actually worked. cast call is free and quick.
    bash
    cast call $CONTRACT 'owner()(address)' --rpc-url $RPC_URL
    bash
    # Output should match $YOUR_ADDR
    Learn more

    This is the cheapest sanity check available. cast call queries state without a transaction (no gas, instant), so use it after every state-changing call to confirm the chain saw what you expected. If the returned address doesn't match yours, your claimOwnership() reverted silently or you sent it from the wrong key.

  4. Step 4Read the flag
    With ownership in hand, call getFlag() and decode the returned string.
    bash
    cast call $CONTRACT 'getFlag()(string)' --rpc-url $RPC_URL --from $YOUR_ADDR
    Learn more

    cast call executes a read-only call (no transaction, no gas cost) against a deployed contract. The function signature format 'getFlag()(string)' tells cast both the input types (none) and output types (string) so it can ABI-decode the return value for you.

    In production Solidity, sensitive data is rarely returned directly even to the owner - instead, events are emitted or data is stored off-chain. However, CTF contracts intentionally expose flags through a guarded function, which is why bypassing the access control directly yields the flag.

    The broader lesson is that blockchain data is public: even "private" Solidity state variables can be read by anyone via eth_getStorageAt RPC calls. True secrets should never be stored on-chain unencrypted - but in this CTF challenge the flag is gated behind an access-controlled function rather than encrypted storage, making ownership the only protection.

Flag

picoCTF{4cc3ss_c0ntr0l_byp4ss_...}

The contract's owner check is bypassable - the ownership can be claimed by anyone due to a missing or broken access modifier.

How to prevent this

Smart contract access control is mostly about not trusting tx.origin and not leaving init functions reachable.

  • Use OpenZeppelin's Ownable or AccessControl rather than rolling your own. Both ship audited onlyOwner / onlyRole modifiers and constructor-time owner assignment.
  • Always compare against msg.sender, never tx.origin. tx.origin can be manipulated by routing the call through an attacker-controlled proxy contract.
  • For upgradeable contracts, use OpenZeppelin's Initializable with the initializer modifier. Under the hood it sets a boolean storage flag (_initialized) on first call and checks it on entry, so any subsequent call reverts with InvalidInitialization(). Pair with Slither and a Foundry invariant test that asserts the owner can never change unexpectedly.

Want more picoCTF 2026 writeups?

Useful tools for Blockchain

Related reading

What to try next