Capture The Flag ️Writeups — AwesomWasm 2023 Pt. 1

JCsec
Oak Security
Published in
9 min readSep 19, 2023

Welcome to our official solutions for all the Oak Security CosmWasm CTF challenges! This post will be the first part of two, enjoy our writeups for challenges 1 to 5.

Photo by Gabriel Crismariu on Unsplash

Challenge 01: Mjolnir

Purpose

The main purpose of this contract is to allow users to lock their funds for a predetermined period. Once the period elapses, users can then unlock them and retrieve their funds.

Entry points

  • Deposit message: allows users to deposit funds to create a lockup.
  • Withdraw message:
    - Allows users to withdraw funds from their lockups.
    - Responsible for ensuring the lockup period has ended and the lockup ID belongs to the caller.

Base scenario

  1. The contract is instantiated with initial funds, see /ctf-01/src/integration_tests.rs#L40-L45
  2. A user will deposit funds into the contract, see /ctf-01/src/integration_tests.rs#L50-L59

Winning condition

Demonstrate how an unprivileged user can drain all funds inside the contract.

Solution

One of the features of the Withdraw message is that it allows users to withdraw multiple lockup ids within a single transaction. This is facilitated by the ids parameter, a vector composed of unsigned integers representing the lockup ID.

However, the function does not verify the absence of duplicate lockup IDs in the parameter. If a user includes the same lockup ID multiple times in the ids parameter, the function will inaccurately calculate the locked value more than once, leading to an overpayment to the user.

Consequently, an attacker can steal funds from the contract by performing a withdrawal with a duplicated lockup ID in the ids parameter, causing a loss of funds to other users.

Proof of concept

For the proof of concept that demonstrates the attack scenario mentioned above see /ctf-01/src/exploit.rs.

Fix commit

The withdrawal function is fixed by ensuring no duplicate lockup IDs exist within the provided IDs vector. Please check this commit of the “fixed” branch.

Challenge 02: Gungnir

Purpose

The main purpose of this contract is to allow users to deposit funds and stake them in exchange for voting power. The user can unstake them and withdraw the funds after a predefined time.

Entry points

  • Deposit message:
    - Allows users to deposit funds, increasing their total_tokens balance.
  • Withdraw message:
    - Allows users to withdraw funds, decreasing their total_tokens balance.
    - Responsible for ensuring the withdrawal amount does not exceed the staked voting power.
  • Stake message:
    - Allows users to stake deposited funds in return for voting power.
    - Voting power received is directly proportional to the staked token amount.
    - Once staked, tokens can only be unstaked after the end of the LOCK_PERIOD.
  • Unstake Message:
    - Allows users to unstake tokens, reducing their voting power.
    - Responsible for ensuring the tokens can only be unstaked after the LOCK_PERIOD has elapsed.

Base scenario

The contract is instantiated with zero funds, see /ctf-02/src/integration_tests.rs#L23-L41.

Winning condition

Demonstrate how an unprivileged user can achieve an unfair amount of voting power.

Solution

When the user unstakes their tokens, their voting power will be deducted by the value specified in the unlock_amount parameter. See /ctf-02/src/contract.rs#L137 and /ctf-02/src/state.rs#L10-L11.

In the state.rs file, the voting_power variable is configured as u128, which is Rust’s native unsigned integer. If the user’s voting power is less than the unlock_amount value, an overflow error will occur and revert the transaction when overflow-checks are enabled.

However, overflow-checks are disabled intentionally for this challenge, as seen in the Cargo.toml file. This implies that Rust will not check for overflows during arithmetic operations and return the overflowed value.

Consequently, an attacker can use the Unstake message with an unlock_amount larger than their actual voting power. This would cause the subtraction to overflow, resulting in their voting power being erroneously large without requiring them to stake an equal amount of tokens.

Proof of concept

The test case needs to be executed with release mode to reproduce the issue correctly.

cargo test — release — exploit::tests::exploit — exact — nocapture

Without specifying the release mode explicitly, the test case defaults to debug mode. Since debug mode captures all overflow errors regardless of the overflow-checks configuration, the test case will not work properly.

By specifying the mode to release, we reproduce how the contract will behave in the mainnet, as deployed contracts will be compiled in release mode instead of debug mode.

For the proof of concept that demonstrates the attack scenario mentioned above see /ctf-02/src/exploit.rs.

Further reference.

Fix commit

The fix to this challenge is to enable overflow-checks in the Cargo.toml file. Alternatively, it is possible to prevent this issue by using checked_sub (reference) to return an error if an overflow occurs.

Please check this commit of the “fixed” branch.

Challenge 03: Laevateinn

Purpose

The main purpose of the contracts is to allow users to request a flash loan for arbitrage purposes. Users are expected to interact with the proxy contract to request flash loans, which calls the underlying flash loan contract that handles most of the core logic.

Reference on flash loans.

Entry points

Proxy contract:

  • RequestFlashLoan message:
    - The entry point for users to request a flash loan.
    - The recipient parameter indicates the contract to be executed with the flash loaned funds.
    - The msg parameter represents the message to execute.
    - Several validations are performed, such as: 1) ensures the recipient is not the flash loan contract address, 2) ensures the proxy contract is authorized to call the flash loan contract and 3) ensures the borrowed funds are returned in the same transaction.

Flash loan contract:

  • SetProxyAddr message:
    - Sets the proxy contract address.
    - Ensures the caller is the current contract owner.
  • FlashLoan message:
    - The entry point for the proxy contract to initiate a flash loan.
    - Performs a state check to store flash loan progress.
    - Sends all funds to the proxy contract address.
  • SettleLoan message:
    - The entry point for the proxy contract to complete a flash loan.
    - Performs a state check to load flash loan progress.
    - Ensures the amount received is not lower than the requested amount.
  • WithdrawFunds message:
    - Sends all funds inside the contract to the recipient’s address.
    - Ensures the caller is the current contract owner.
  • TransferOwner message:
    - Transfers the contract owner to the new_owner address.
    - Ensures the caller is the current contract owner or the proxy contract address.

Base scenario

  1. The proxy contract is instantiated with the flash loan contract address, see /ctf-03/contracts/proxy/src/integration_tests.rs#L64-L77.
  2. The flash loan contract is instantiated with initial funds, see /ctf-03/contracts/proxy/src/integration_tests.rs#L92-L93.
  3. The proxy contract address is configured to the flash loan contract using the SetProxyAddr message, see /ctf-03/contracts/proxy/src/integration_tests.rs#L95-L104

Winning condition

Demonstrate how an unprivileged user can drain all funds from the flash loan contract.

Solution

When requesting a flash loan, the CallToFlashLoan error will revert the transaction if the recipient address is the flash loan contract address. This validation is important to prevent the user from controlling the proxy contract to execute authenticated messages in the flash loan contract, such as the TransferOwner message. See /ctf-03/contracts/proxy/src/contract.rs#L58-L61.

Notice that when the proxy contract is instantiated, the flash loan contract address is validated with the addr_validate function. This ensures that the flash loan contract address is in lowercase format.

An error will occur if an uppercase address is validated with the addr_validate function. This is demonstrated in the test case below:

#[test]
fn test_address_validation() {
use cosmwasm_std::testing::mock_dependencies;
use cosmwasm_std::Api;

let deps = mock_dependencies();

let lowercase_addr = "cosmos100000v3fpv4qg2a9ea6sj70gykxpt63wgjen2p";

// no error
deps.api.addr_validate(lowercase_addr).unwrap();

let uppercase_addr = lowercase_addr.to_ascii_uppercase();

// error
deps.api.addr_validate(&uppercase_addr).unwrap();

}

The lowercase validation is mainly due to the Bech32 address format used by the Cosmos blockchain. Bech32 format treats both uppercase and lowercase formats as the same address.

As the flash loan contract address is validated with the addr_validate function, this means the address format will be lowercase. By specifying the recipient parameter as the uppercase flash loan contract address, we can bypass the validation and control the proxy contract to execute authenticated messages in the flash loan contract.

To drain all funds in the flash loan contract, we can specify the msg parameter as TransferOwner message with the new_owner address set to our address. Once we obtain ownership of the flash loan contract, we call the WithdrawFunds message to complete our exploit flow.

Proof of concept

For the proof of concept that demonstrates the attack scenario mentioned above see /ctf-03/contracts/proxy/src/exploit.rs.

Fix commit

The fix to this challenge is to validate the recipient’s address to ensure uppercase addresses are not allowed.

Please check this commit of the “fixed” branch.

Challenge 04: Gram

Purpose

The main purpose of this contract is to allow users to deposit funds and receive shares in return. Conversely, users can redeem their shares for the underlying assets. After yields are received through simple transfers to the contract from third parties, the total number of underlying assets increases while total shares remain constant, enabling users to redeem shares for their initial deposit with distributed rewards.

Entry points

  • Mint message:
    - The entry point for users to mint shares by depositing funds.
    - Ensures the mint amount is not zero.
    - Increases the total supply value.
    - Increases the user’s shares balance.
  • Burn message:
    - The entry point for users to burn shares in return for funds.
    - Ensures the asset amount to return is not zero.
    - Decreases the total supply value.
    - Decreases the user’s shares balance.

Base scenario

The contract is instantiated with zero funds, see /ctf-04/src/integration_tests.rs#L24-L42.

Winning condition

Demonstrate how an unprivileged user can withdraw more funds than deposited.

Solution

When there are no deposits in the contract, the shares minted will be proportional to the deposit amount as shown in lines 58 and 59. This is problematic as it allows the attacker to carry out a “share inflation attack”.

The attack requires several circumstances to succeed. Firstly, the attacker needs to be the first depositor and deposit a small amount of funds in exchange for a share. This step allows the attacker to influence the mint amount later.

When a legitimate user deposits next, the attacker will front-run their transaction to send funds directly to the contract. The attacker’s transaction will be processed first, and the contract’s underlying asset balance will increase.

The mint amount calculation will have been manipulated when the user’s transaction gets processed. As the attacker artificially increases the total_assets value, the resulting mint_amount value will decrease.

Consequently, users will receive less funds than their deposit amount after burning all their shares, causing a loss of funds scenario.

Proof of concept

For the proof of concept that demonstrates the attack scenario mentioned above see /ctf-04/src/exploit.rs

Fix commit

The fix to this challenge is to implement virtual assets protection, similar to the ERC4626 approach.

Please check this commit of the “fixed” branch.

Challenge 05: Draupnir

Purpose

The main purpose of this contract is to allow users to deposit and withdraw their tokens, which will be internally accounted for. The vault’s owner can perform arbitrary actions through the OwnerAction entry point. In addition, a two-step address transfer is implemented for the owner role.

Entry points

  • Deposit message:
    - Requires the user to deposit DENOM.
    - Increases the user balance.
  • Withdraw message:
    - Deducts the user balance.
    - Transfers the withdrawn amount of DENOM to the sender.
  • OwnerAction message:
    - Privileged entry point that restricts the caller to the owner.
    - Executes an arbitrary CosmosMsg.
  • ProposeNewOwner message:
    - A privileged entry point that limits the caller to the owner.
    - Saves a proposed validated address into the storage.
  • AcceptOwnership message:
    - A privileged entry point that restricts the caller to proposed_owner in storage.
    - Sets the proposed owner as the current owner.
  • DropOwnershipProposal message:
    - Privileged entry point that restricts the caller to the owner.
    - Clears the contents of the current proposed_owner from storage.

Base scenario

  1. The contract has been instantiated with zero funds.
  2. USER1 and USER2 deposit 10_000 tokens each.
  3. The owner role is assigned to the ADMIN address.

Winning condition

Demonstrate how an unprivileged user can drain all the funds inside the contract.

Solution

The accept_owner function of the contract checks if the caller’s address is that of the proposed_owner in line 129. If not, it should return an Unauthorized error. However, instead of returning the error, it just declares the ContractError struct’s element Unauthorized, resulting in a statement without effect that allows any attacker to bypass the condition and claim the contract’s ownership.

At this point, the attacker can call the OwnerAction message to transfer all the contract’s funds to their address.

Proof of concept

For the proof of concept that demonstrates the attack scenario mentioned above see /ctf-05/src/exploit.rs

Fix commit

The fix to this challenge is to change line 130 such that it returns the error through a return statement.

Please check this commit of the “fixed” branch.

The second part is ready and will be released soon, stay tuned!

If you are looking for support for your project’s security, please get in touch, and we can schedule a call to discuss your needs.

Stay in touch: Website | Twitter | LinkedIn

This content was prepared by the two Oak Security auditors who designed the CTF challenges: JC and Richie.

--

--

JCsec
Oak Security

Smart Contract security auditor specialized in CosmWasm. Follow me on Twitter @jcsec_audits and Github https://github.com/jcsec-security