Skip to content

Commit

Permalink
add exploit for cosmwasm ctf 05
Browse files Browse the repository at this point in the history
  • Loading branch information
minaminao committed Jun 17, 2024
1 parent d0e4a48 commit 543e63b
Show file tree
Hide file tree
Showing 12 changed files with 710 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ Note
| [Oak Security CosmWasm CTF: 2. Gungnir](src/OakSecurityCosmWasmCTF/02-Gungnir/) | integer overflow |
| [Oak Security CosmWasm CTF: 3. Laevateinn](src/OakSecurityCosmWasmCTF/03-Laevateinn/) | address validation, uppercase |
| [Oak Security CosmWasm CTF: 4. Gram](src/OakSecurityCosmWasmCTF/04-Gram/) | invariant, rounding error |
| Oak Security CosmWasm CTF: 5. Draupnir | |
| [Oak Security CosmWasm CTF: 5. Draupnir](src/OakSecurityCosmWasmCTF/05-Draupnir/) | missing return |
| Oak Security CosmWasm CTF: 6. Hofund | |
| Oak Security CosmWasm CTF: 7. Tyrfing | |
| Oak Security CosmWasm CTF: 8. Gjallarhorn | |
Expand Down
4 changes: 4 additions & 0 deletions src/OakSecurityCosmWasmCTF/05-Draupnir/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[alias]
wasm = "build --release --lib --target wasm32-unknown-unknown"
unit-test = "test --lib"
schema = "run --bin schema"
54 changes: 54 additions & 0 deletions src/OakSecurityCosmWasmCTF/05-Draupnir/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
[package]
name = "oaksecurity-cosmwasm-ctf-05"
version = "0.1.0"
authors = ["Oak Security <info@oaksecurity.io>"]
edition = "2021"

exclude = [
# Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication.
"contract.wasm",
"hash.txt",
]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
crate-type = ["cdylib", "rlib"]

[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
debug-assertions = false
codegen-units = 1
panic = 'abort'
incremental = false
overflow-checks = true

[features]
# for more explicit tests, cargo test --features=backtraces
backtraces = ["cosmwasm-std/backtraces"]
# use library feature to disable all instantiate/execute/query exports
library = []

[package.metadata.scripts]
optimize = """docker run --rm -v "$(pwd)":/code \
--mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
cosmwasm/rust-optimizer:0.12.10
"""

[dependencies]
cosmwasm-schema = "1.1.3"
cosmwasm-std = "1.1.3"
cosmwasm-storage = "1.1.3"
cw-storage-plus = "1.0.1"
cw2 = "1.0.1"
cw-utils = "1.0.1"
schemars = "0.8.10"
serde = { version = "1.0.145", default-features = false, features = ["derive"] }
thiserror = { version = "1.0.31" }

[dev-dependencies]
cw-multi-test = "0.16.2"
30 changes: 30 additions & 0 deletions src/OakSecurityCosmWasmCTF/05-Draupnir/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Awesomwasm 2023 CTF

## Challenge 05: *Draupnir*

Simplified vault where users can deposit and withdraw their tokens which will be internally accounted.
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.

### Execute entry points:
```rust
pub enum ExecuteMsg {
Deposit {},
Withdraw { amount: Uint128 },
OwnerAction { msg: CosmosMsg },
ProposeNewOwner { new_owner: String },
AcceptOwnership {},
DropOwnershipProposal {},
}
```

Please check the challenge's [integration_tests](./src/integration_tests.rs) for expected usage examples.
You can use these tests as a base to create your exploit Proof of Concept.

**:house: Base scenario:**
- The contract has been instantiated with zero funds.
- `USER1` and `USER2` deposit `10_000` tokens each.
- The owner role is assigned to the `ADMIN` address.

**:star: Goal for the challenge:**
- Demonstrate how an unprivileged user can drain all the funds inside the contract.
1 change: 1 addition & 0 deletions src/OakSecurityCosmWasmCTF/05-Draupnir/src/bin/schema.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fn main() {}
173 changes: 173 additions & 0 deletions src/OakSecurityCosmWasmCTF/05-Draupnir/src/contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
#[cfg(not(feature = "library"))]
use cosmwasm_std::{
coin, entry_point, to_binary, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo,
Response, StdResult, Uint128,
};

use crate::error::ContractError;
use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
use crate::state::{assert_owner, State, BALANCES, STATE};
use cw_utils::must_pay;

pub const DENOM: &str = "uawesome";

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
let state = State {
current_owner: deps.api.addr_validate(&msg.owner)?,
proposed_owner: None,
};
STATE.save(deps.storage, &state)?;

Ok(Response::new()
.add_attribute("action", "instantiate")
.add_attribute("owner", msg.owner))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::Deposit {} => deposit(deps, info),
ExecuteMsg::Withdraw { amount } => withdraw(deps, info, amount),
ExecuteMsg::OwnerAction { msg } => owner_action(deps, info, msg),
ExecuteMsg::ProposeNewOwner { new_owner } => propose_owner(deps, info, new_owner),
ExecuteMsg::AcceptOwnership {} => accept_owner(deps, info),
ExecuteMsg::DropOwnershipProposal {} => drop_owner(deps, info),
}
}

/// Deposit entry point for user
pub fn deposit(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
// validate denom
let amount = must_pay(&info, DENOM).unwrap();

// increase total stake
let mut user_balance = BALANCES
.load(deps.storage, &info.sender)
.unwrap_or_default();
user_balance += amount;

BALANCES.save(deps.storage, &info.sender, &user_balance)?;

Ok(Response::new()
.add_attribute("action", "deposit")
.add_attribute("user", info.sender)
.add_attribute("amount", amount))
}

/// Withdrawal entry point for user
pub fn withdraw(
deps: DepsMut,
info: MessageInfo,
amount: Uint128,
) -> Result<Response, ContractError> {
// decrease total stake
let mut user_balance = BALANCES.load(deps.storage, &info.sender)?;

// Cosmwasm's Uint128 checks math operations
user_balance -= amount;

BALANCES.save(deps.storage, &info.sender, &user_balance)?;

let msg = BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![coin(amount.u128(), DENOM)],
};

Ok(Response::new()
.add_attribute("action", "withdraw")
.add_attribute("user", info.sender)
.add_attribute("amount", amount)
.add_message(msg))
}

/// Entry point for owner to execute arbitrary Cosmos messages
pub fn owner_action(
deps: DepsMut,
info: MessageInfo,
msg: CosmosMsg,
) -> Result<Response, ContractError> {
assert_owner(deps.storage, info.sender)?;

Ok(Response::new()
.add_attribute("action", "owner_action")
.add_message(msg))
}

/// Entry point for current owner to propose a new owner
pub fn propose_owner(
deps: DepsMut,
info: MessageInfo,
new_owner: String,
) -> Result<Response, ContractError> {
assert_owner(deps.storage, info.sender)?;

STATE.update(deps.storage, |mut state| -> StdResult<_> {
state.proposed_owner = Some(deps.api.addr_validate(&new_owner)?);
Ok(state)
})?;

Ok(Response::new()
.add_attribute("action", "propose_owner")
.add_attribute("new proposal", new_owner))
}

/// Entry point for new owner to accept a pending ownership transfer
pub fn accept_owner(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
let state = STATE.load(deps.storage)?;

if state.proposed_owner != Some(info.sender.clone()) {
ContractError::Unauthorized {};
}

STATE.update(deps.storage, |mut state| -> StdResult<_> {
state.current_owner = info.sender.clone();
state.proposed_owner = None;
Ok(state)
})?;

Ok(Response::new()
.add_attribute("action", "accept_owner")
.add_attribute("new owner", info.sender))
}

/// Entry point for current owner to drop pending ownership
pub fn drop_owner(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
assert_owner(deps.storage, info.sender)?;

STATE.update(deps.storage, |mut state| -> StdResult<_> {
state.proposed_owner = None;
Ok(state)
})?;

Ok(Response::new().add_attribute("action", "drop_owner"))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::State {} => to_binary(&query_state(deps)?),
QueryMsg::UserBalance { address } => to_binary(&query_balance(deps, address)?),
}
}

/// Returns user balance
pub fn query_balance(deps: Deps, address: String) -> StdResult<Uint128> {
let address = deps.api.addr_validate(&address)?;
BALANCES.load(deps.storage, &address)
}

/// Returns contract state
pub fn query_state(deps: Deps) -> StdResult<State> {
STATE.load(deps.storage)
}
11 changes: 11 additions & 0 deletions src/OakSecurityCosmWasmCTF/05-Draupnir/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use cosmwasm_std::StdError;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),

#[error("Unauthorized")]
Unauthorized {},
}
Loading

0 comments on commit 543e63b

Please sign in to comment.