2
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?

CTF初心者によるSECCON Beginners CTF 2025 writeup

Posted at

結果

スクリーンショット 2025-08-05 141818.png

スクリーンショット 2025-08-05 145609.png

スコア800で、880チーム中528位でした。目標は3問正解だったので、かなり満足いく結果になりました。そのうち4問のwriteupを書きたいと思います。

環境は以下のとおりでした。
windows
・VSCode ソースコード閲覧
・Chrome 課題ページへのアクセスと調べもの
kali-linux
・コマンドでのアクセス

Writeup

skipping(web) [Beginner]

示されたURLにアクセスすると、以下の画面になります。そのまま/flagをクリックすると、403 forbiddenを返されてしまいます。

スクリーンショット 2025-07-26 172126.png

index.jsに以下の記述を見つけました。

index.js
const check = (req, res, next) => {
    if (!req.headers['x-ctf4b-request'] || req.headers['x-ctf4b-request'] !== 'ctf4b') {
        return res.status(403).send('403 Forbidden');
    }

    next();
}

これを見ると、x-ctf4b-requestヘッダにctf4bという値がセットされていないと403 Forbiddenが返されてしまうことがわかります。
問題文にも「curlなどを利用して」と書かれていたので、curlでヘッダを設定しアクセスします。

スクリーンショット 2025-08-06 224740.png
ctf4b{y0ur_5k1pp1n6_15_v3ry_n1c3}
フラグが取得できました。

log-viewer(web) [Easy]

示されたURLにアクセスすると、access.logdebug.logが確認できるサイトに飛びます。?file=にどちらかのファイル名を指定して閲覧するようです。
http://log-viewer.challenges.beginners.seccon.jp:9999/?file=access.log

スクリーンショット 2025-07-26 172451.png

access.logの下から五行目に、?file=access.logでも、debug.logでもないのに200 OKを返しているログを見つけました。

[21/June/2025:10:42:53 +0900] "GET /?file=../../proc/self/environ HTTP/1.1" 200

../を使ってディレクトリトラバーサルを成功させた記録だと考えられるので、自分でもアクセスしてみます。

スクリーンショット 2025-07-26 173841.png

指定したファイルの内容が確認できる状態になっているようです。
ここから少し詰まったのですが、debug.logの方にもヒントがありました。(2行目)

2025/06/21 10:40:02 DEBUG Parsed command line arguments flag=ctf4b{this_is_dummy_flag} port=8000

このログから、コマンドライン引数にフラグがありそうです。
?file=../../proc/self/cmdlineとすると、フラグが確認できました。

スクリーンショット 2025-08-06 170324.png

ctf4b{h1dd1ng_1n_cmdl1n3_m4y_b3_r34d4bl3}

url-checker(misc) [Beginner]

指定されたホストにncコマンドでアクセスしてみます。
allowed_hostname = "example.com"とあるので、http://example.com/と入力すると、以下の画面になりました。

スクリーンショット 2025-08-06 172930.png

入力されたURLが適切かチェックするツールのようです。ソースコードのmain.pyを見てみます。

main.py(前半)
allowed_hostname = "example.com"
user_input = input("Enter a URL: ").strip()
parsed = urlparse(user_input)

urlparse()は、引数のURLを部分文字列に分割するメソッドです。(参考: https://ja.pymotw.com/2/urlparse/index.html)
parsed = urlparse('http://netloc/path;parameters?query=argument#fragment')とすると、以下に分割してくれます。

parsed.scheme http
parsed.netloc netloc
parsed.hostname netloc
parsed.path /path
parsed.params parameters
parsed.query query=argument
parsed.fragment fragment
main.py(後半)
if parsed.hostname == allowed_hostname:
        print("You entered the allowed URL :)")
    elif parsed.hostname and parsed.hostname.startswith(allowed_hostname):
        print(f"Valid URL :)")
        print("Flag: ctf4b{dummy_flag}")
    else:
        print(f"Invalid URL x_x, expected hostname {allowed_hostname}, got {parsed.hostname if parsed.hostname else 'None'}")

elifブロックに入ればフラグを取得できそうです。elifの条件は、parsed.hostname == allowed_hostnameではないが、parsed.hostnameallowed_hostnameで始まるということなので、http://example.commmmm/と入力すると、フラグが確認できました。

スクリーンショット 2025-08-06 223559.png

ctf4b{574r75w17h_50m371m35_n07_53cur37}

url-checker2(misc) [Easy]

url-checkerの改良版のようです。中盤にコードが追加され、後半も改良されています。

main.py
#中盤(追加部分)
input_hostname = None
if ':' in parsed.netloc:
    input_hostname = parsed.netloc.split(':')[0]

#後半
if parsed.hostname == allowed_hostname:
        print("You entered the allowed URL :)")
    elif input_hostname and input_hostname == allowed_hostname and parsed.hostname and parsed.hostname.startswith(allowed_hostname):
        print(f"Valid URL :)")
        print("Flag: ctf4b{dummy_flag}")
    else:
        print(f"Invalid URL x_x, expected hostname {allowed_hostname}, got {parsed.hostname if parsed.hostname else 'None'}")

またしてもelifブロックに入るのを目指すようです。当たり前ですがurl-checkerで成功したURLはもう通らなくなっています。
改めて調べてみると、urlparse()メソッドはRFC3986のURL構文要素に基づいて分割を行っているようです。RFC3986ドキュメントを確認します。(参考:https://datatracker.ietf.org/doc/html/rfc3986.html#section-3.2)

RFC3986
authority   = [ userinfo "@" ] host [ ":" port ]

urlparse()parsed.netlocauthorityに対応し、parsed.hostnamehostに対応しています。つまり、実はparsed.netlocはさらに分割されており、そのうち@の右側かつ:の左側の部分がparsed.hostnameということですね。(@:はなくてもいい)

parsed=urlparse('http://user:pass@NetLoc:80/path;parameters?query=argument#fragment')だと、以下に分割されます。(参考: https://ja.pymotw.com/2/urlparse/index.html)

parsed.scheme http
parsed.netloc user:pass@NetLoc:80
parsed.hostname netloc
parsed.path /path
parsed.params parameters
parsed.query query=argument
parsed.fragment fragment

ソースコード中盤を見ると、parsed.netlocのうち:で分割した一番左側をinput_hostnameとしています。
ソースコード後半にある目標のelif条件を箇条書きにしてみます。

parsed.hostname != allowed_hostname
input_hostname == allowed_hostname
parsed.hostname.startswith(allowed_hostname)

これらの条件より、http://example.com:@example.commmmm/と入力すると、フラグを確認できました。

スクリーンショット 2025-08-07 003615.png
ctf4b{cu570m_pr0c3551n6_0f_url5_15_d4n63r0u5}

感想

初参加ながら、とても楽しく問題に取り組めました。
来年はwebに絞って全完を目指したいです!

2
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
2
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?