apk-redteam-pipeline について
APK の取得、静的解析、実行時テストを段階的に進める Android アプリ向けレッドチームスキルです。対象として許可されたモバイルアプリの評価を想定し、アプリ一覧から API やコンポーネントの調査まで扱います。
主な機能
- 01組織が保有するアプリの一覧化
- 02APK と分割 XAPK の取得
- 03DEX の逆コンパイルとシークレット検索
- 04証明書とエクスポートコンポーネントの検査
- 05Frida による計装と通信キャプチャ
- 06Firebase 設定の確認
活用シーン
- 01許可された Android アプリの評価
- 02APK からの API ホストと認証情報の発見
- 03エクスポートコンポーネントとアプリ設定の確認
- 04証明書ピンニング回避後の実行時リクエスト調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ uphiago/ recon-skills --skill apk-redteam-pipelineインストール後、Claude Code で/apk-redteam-pipelineを入力して読み込まれたか確認します。
詳細
このスキルを使用する場面
次の場合に実行します。
- 調査で、対象の開発者名(Play ストアのデベロッパーページ)に紐づくモバイルアプリが1つ以上見つかった場合
- ウェブアプリが
*.apkファイルを直接ホストしている場合(例:ある案件でサブドメインからRecruitz.apkが見つかった) - インフォスティーラーログから APK のパッケージ ID が漏えいしている場合(例:インフォスティーラーダンプ形式に含まれる
com.<brand>.app、com.<brand>.<sub-brand>のパターン) - 顧客向けアプリ、ディーラー/パートナー向けポータル、または従業員向けモバイルコンパニオンアプリがスコープ内にある場合
- バグバウンティプログラムで Android がスコープ内に指定されている場合
次の場合は使用しないでください。
- iOS のみが対象の場合(別のパイプラインを使用します。IPA のリバース、MobSF、frida-ios-dump)
- JavaScript バンドル分析で既にカバーされている React Native/Flutter のウェブアプリ
- サーバーサイドのみの評価
ステージ 0 — 組織が所有するすべてのアプリの一覧作成
Play ストアのデベロッパーページのスクレイピング
# Find developer page from the target's brand name
curl --max-time 30 --connect-timeout 10 -sk -A "Mozilla/5.0" "https://play.google.com/store/apps/developer?id=<Brand+Name>" -o /tmp/dev.html
# Extract package IDs
grep -oE 'id=[a-zA-Z0-9._]+' /tmp/dev.html | sort -u
出力例(匿名化済み — 複数ブランドを持つ複合企業で一般的な7つのパッケージ):
com.events.<brand>build
com.<corp>.<sub-brand-1>
com.<corp>.<sub-brand-2>
com.<corp>.<flagship>
com.<corp>.<product-line-1>
com.<corp>.<product-line-2>
com.<corp>.<sub-brand-3>
インフォスティーラーログとの相互照合
インフォスティーラーログには *@com.<corp>.<app> のようなパッケージ名が含まれます。漏えいしたダンプがある場合は、creds_userpass.txt からこれらを抽出します。
ブランド名の組み合わせを推測(複数ブランドを持つ複合企業のパターン)
com.<brand>.app
com.<brand>.mobile
com.<brand>.android
com.<brand>connect.app
in.<brand>.dealer
in.co.<brand>.app
ステージ 1 — APK の取得
第一候補:APKPure から直接取得(認証不要)
# Follow 302 redirects to actual download
curl -sk -L --max-time 60 --connect-timeout 10 \
"https://d.apkpure.net/b/APK/<package_id>?version=latest" \
-o "<package_id>.apk"
# Or via the legacy d-XX.app-mirror.example.com mirror chain (we saw this work)
第二候補:APKMirror で検索
curl --max-time 30 --connect-timeout 10 -sk -A "Mozilla/5.0" "https://www.apkmirror.com/?post_type=app_release&searchtype=apk&s=<brand>" \
| grep -oE 'href="[^"]+\.apk[^"]*"' | sort -u
第三候補:APKPure のウェブ検索
curl --max-time 30 --connect-timeout 10 -sk "https://apkpure.com/search?q=<brand>" | grep -oE 'data-dt-app="[^"]+"'
XAPK と APK
.xapkは、複数の分割 APK(base + config.armeabi-v7a + config.en + など)を含む zip です- まず外側のファイルを解凍し、次に内側の
base.apkまたは<package>.apkを解凍します - apkpure のダウンロードでは、EOCD 署名が欠落した不完全な XAPK が返されることがあります。これは CDN のレート制限が原因です。IP を切り替えて再試行するか、
unzipより寛容な7z xを使用します
# Standard unzip (works for clean APK)
unzip -o <package>.apk -d extracted_<package>/
# For truncated/repaired XAPK
7z x -y <package>.apk -o"extracted_<package>"
# For nested XAPK
for inner in extracted_<package>/*.apk; do
mkdir -p "extracted_<package>/$(basename "$inner" .apk)"
unzip -o "$inner" -d "extracted_<package>/$(basename "$inner" .apk)"
done
ステージ 2 — DEX の逆コンパイル(jadx)
# Install
brew install jadx # macOS
# or
wget https://github.com/skylot/jadx/releases/latest/download/jadx-1.5.x.zip
# Decompile
jadx -d decompiled_<package>/ <package>.apk
# For XAPK that contains multiple APKs
for inner in extracted_<package>/*.apk; do
jadx -d decompiled_<package>_$(basename "$inner" .apk)/ "$inner"
done
完全な逆コンパイルを行わず、「文字列のみ」をすばやく確認する場合:
find extracted_<package> -name "classes*.dex" -exec strings -8 {} \; > strings_<package>.txt
ステージ 3 — シークレットの grep(60パターンのカタログ)
# URL grep — owned-domain references
grep -oE 'https?://[a-zA-Z0-9.-]+\.(target1|target2|target3)\.(com|io|net|in)[a-zA-Z0-9./_?=&%-]*' strings_<package>.txt | sort -u
# Internal IP / port URLs
grep -oE 'https?://(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|127\.)[0-9.]+(:[0-9]+)?[a-zA-Z0-9./_?=&-]*' strings_<package>.txt
# Cloud credentials
grep -oE 'AKIA[A-Z0-9]{16}' # AWS Access Key
grep -oE 'aws_secret_access_key[\s:=]+[A-Za-z0-9/+=]{40}' # AWS Secret
grep -oE 'AIza[A-Za-z0-9_-]{35}' # Google API key
grep -oE 'ya29\.[A-Za-z0-9_-]+' # Google OAuth refresh token
grep -oE 'gh[ps]_[A-Za-z0-9]{36}' # GitHub PAT
grep -oE 'glpat-[A-Za-z0-9_-]{20}' # GitLab PAT
grep -oE 'xox[pbar]-[A-Za-z0-9-]+' # Slack token
grep -oE 'sk-[A-Za-z0-9]{48}' # OpenAI API key
grep -oE 'sk-ant-[A-Za-z0-9_-]{90,}' # Anthropic API key
grep -oE 'AC[a-f0-9]{32}' # Twilio Account SID
grep -oE 'sk_live_[A-Za-z0-9]{24}' # Stripe live key
grep -oE 'pk_live_[A-Za-z0-9]{24}' # Stripe publishable
grep -oE 'mailgun-[A-Za-z0-9-]{40}' # Mailgun
grep -oE 'SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}' # SendGrid
# JWT (any algorithm)
grep -oE 'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*' strings_<package>.txt
# Firebase
grep -oE '"(api_key|project_id|database_url|storage_bucket|client_id|mobilesdk_app_id|google_app_id|gcm_defaultSenderId)"\s*:\s*"[^"]+"' \
extracted_<package>/res/values/strings.xml \
extracted_<package>/google-services.json \
decompiled_<package>/resources/AndroidManifest.xml 2>/dev/null
# OAuth client secrets
grep -oE 'client_secret["\s:=]+[A-Za-z0-9_-]{24,}' strings_<package>.txt
# Hardcoded passwords (heuristic — many false positives, manual review)
grep -oE '"password"\s*:\s*"[^"]+"|password\s*=\s*"[^"]+"' decompiled_<package>/sources/**/*.java 2>/dev/null
実際の調査での発見例(匿名化済み — 許可を得た案件から)
# Customer-facing APK shipped a hardcoded URL of this shape:
https://api.<client>.example/<path-token>/<resource-token>?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.<payload>.<sig>
# Decoded JWT payload: {"sid":<int>,"iat":<unix-ts>,"exp":<unix-ts>}
# Expired ~8 years earlier — but path tokens + 30 /v1/* endpoints still useful intel
ステージ 4 — ピン留めされた証明書の抽出
# Find .cer / .der / .pem files in assets/
find extracted_<package>/assets -iname "*.cer" -o -iname "*.der" -o -iname "*.pem" -o -iname "*.crt" 2>/dev/null
# Or in network_security_config.xml
find extracted_<package> -name "network_security_config.xml" -exec cat {} \;
# For each cert, extract subject + SAN (might reveal new internal API hosts)
for cert in $(find extracted_<package>/assets -iname "*.cer"); do
echo "=== $cert ==="
openssl x509 -in "$cert" -noout -subject -issuer -dates 2>/dev/null
openssl x509 -in "$cert" -noout -text 2>/dev/null | grep -E 'Subject:|DNS:|Issuer:|Validity'
done
実際の調査例
許可を得た案件で顧客向け APK を調べたところ、assets/api_<service>_<domain>_com.cer が含まれていました。これにより、受動的な調査では見つかっていなかった api.<service>.<domain>.example というアセットの存在が明らかになりました。
ステージ 5 — エクスポートされたコンポーネントの列挙
AndroidManifest.xml にはコンポーネントが記載されています。エクスポートされたコンポーネント(特に android:exported="true" が設定されているものや、intent-filter によって暗黙的にエクスポートされているもの)は、他のアプリから起動できるため、インテントインジェクションの潜在的な攻撃対象領域となります。
# Decode binary AndroidManifest if needed
apktool d <package>.apk -o decoded_<package>/ # apktool decodes binary manifest
# Or read directly from jadx output
cat decompiled_<package>/resources/AndroidManifest.xml | grep -E '<(activity|service|receiver|provider)' | head -50
# Filter exported
grep -E 'android:exported="true"' decompiled_<package>/resources/AndroidManifest.xml
エクスポートされた各コンポーネントについて、次を確認します。
- WebView に渡される extras を受け付けるか(インテント → WebView → XSS/file://)
- URI extras を受け付けるか(ディープリンクを介した SSRF の可能性)
- extras を他の Activity に渡すか(インテントのリダイレクト)
ステージ 6 — Firebase/クラウドサービスの設定を調査
# google-services.json — full Firebase config
find extracted_<package> -name "google-services.json" -exec cat {} \; | python3 -m json.tool
# Look for:
# project_id → can guess Firestore / RTDB URL: https://<project_id>.firebaseio.com/.json
# storage_bucket → can guess GCS bucket: gs://<bucket>
# web_api_key → can use to enumerate Firebase tenant config
# Test if Firestore is publicly readable
curl --max-time 30 --connect-timeout 10 -s "https://firestore.googleapis.com/v1/projects/<project_id>/databases/(default)/documents/users"
# Test if Realtime DB is publicly readable
curl --max-time 30 --connect-timeout 10 -s "https://<project_id>.firebaseio.com/.json"
# Test if Storage Bucket is publicly listable
curl --max-time 30 --connect-timeout 10 -s "https://firebasestorage.googleapis.com/v0/b/<bucket>/o"
ステージ 7 — ランタイム計測(Frida)
静的解析だけでは不十分な場合に使用します。実行時にトークンをダンプする、証明書ピンニングを回避する、API 呼び出しを追跡するといった操作ができます。
セットアップ
pip install --break-system-packages frida-tools objection
adb devices # ensure device/emulator connected
# Push frida-server to device (root required, or use rooted emulator like Genymotion / x86_64 AVD)
証明書ピンニングの回避(汎用)
// frida-script-pinning-bypass.js
Java.perform(function() {
// OkHttp
try {
var CertificatePinner = Java.use('okhttp3.CertificatePinner');
CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function() {
console.log('[+] OkHttp pinning bypassed for: ' + arguments[0]);
return;
};
} catch (e) {}
// HttpsURLConnection
try {
var TrustManagerImpl = Java.use('com.android.org.conscrypt.TrustManagerImpl');
TrustManagerImpl.verifyChain.implementation = function(chain) {
console.log('[+] TrustManagerImpl verifyChain bypassed');
return chain;
};
} catch (e) {}
});
frida -U -l frida-script-pinning-bypass.js -f <package_id> --no-pause
HTTP リクエストをフック
Java.perform(function() {
var OkHttpClient = Java.use('okhttp3.OkHttpClient');
var Request = Java.use('okhttp3.Request');
var Call = Java.use('okhttp3.Call');
OkHttpClient.newCall.implementation = function(req) {
var url = req.url().toString();
var headers = req.headers().toString();
console.log('[REQ] ' + url);
console.log('[HDRS] ' + headers);
return this.newCall(req);
};
});
objection によるトークンのすばやい抽出
objection --gadget com.target.app explore
# Inside:
android hooking watch class_method <class>.<method> --dump-args --dump-return
ステージ 8 — ネットワークトラフィックのキャプチャ(mitmproxy を使用)
ピンニングを回避した後、API をさらに詳しく特定する場合に使用します。
# Run mitmproxy on host
mitmproxy --listen-port 8080
# Configure Android device proxy to host:8080
# Install mitmproxy CA cert on device:
# - Pull from http://mitm.it on the device, or
# - Push to /system/etc/security/cacerts/ on rooted device
# Use the Frida pinning bypass script while traffic flows through mitmproxy
# All API calls visible in mitmproxy UI
判断ツリー — 見つかったものに応じて次に行うこと
| 見つかったもの | 次の手順 |
|---|---|
| 有効なJWT(有効期限内) | APIホストに対してテストし、アクセス権があるか確認します。sidの操作も試します |
| 期限切れのJWT | パストークン/APIエンドポイントの構造を調べます — VPN接続後に役立つ情報です |
| AWSアクセスキー | awscli でテストします:aws sts get-caller-identity — 漏えいしたキーの多くは、今も権限を持っています |
| Firebase の project_id と web_api_key | 公開されている Firestore/RTDB/Storage の読み取りをテストします |
| Google API キー(AIza*) | https://www.googleapis.com/customsearch/v1 などに対してテストし、どの API が有効になるか確認します |
| ハードコードされた HTTP URL(http://) | 証明書ピンニングがない場合、ダウングレードによる MITM の可能性があります |
| 内部ホスト用のピン留めされた証明書 | 新たなアセットの発見です — そのホストは実在します |
| WebView を持つエクスポート済み Activity | インテントインジェクション → URL 読み込みの悪用をテストします |
| スタックトレースの痕跡(Stack Overflow の URL) | 開発者の質問を特定し、アーキテクチャを推測します |
| ハードコードされた認証情報 | ただちにスプレー攻撃を行います(関連スキルの上限があれば、それを守ってください) |
注意点
- 「password」だけを grep しないでください — ほとんどのシークレットには、固有の高シグナルパターン(AKIA、AIza、eyJ など)があります。一般的な単語で grep すると、ノイズが多くなります。
- XAPK の分割 APK を見落とさないでください — config.armeabi の分割や config. の分割には、異なるコードパスが含まれていることがあります。
- 期限切れの JWT を「使えない情報」と見なさないでください — パス構造、エンドポイント一覧、署名アルゴリズムは、今も有用です。上記の8年前に期限切れになった JWT の例がそれを示しています。
- 最新バージョンだけをリバースしないでください — APKMirror のバージョン履歴から取得できる古い APK には、新しいバージョンでは削除されていても、サーバー側では有効なシークレットが残っていることがあります。
- アプリが「シンプル」に見えても Firebase を無視しないでください — Firebase ルールの設定ミス(Firestore の公開読み取り)は非常によくあります。
- 本番環境のデバイスで Frida を実行しないでください — ルート化済みのエミュレーターまたはテスト用デバイスのみを使用してください。
- APKPure CDN のレート制限 — EOCD が欠落した不完全なダウンロードはよくあります。IP を切り替えて(VPN を切り替えて)再試行してください。不完全にダウンロードされたアーカイブでは、
7z xのほうがunzipより寛容です。 - 大きな APK は jadx でメモリ不足になることがあります — 100 MB を超える APK では、
-Xmx4gを指定して jadx のヒープを増やすか、まずstringsのみの処理に切り替えてください。 - assets/ 内の Keystore/SQLite ファイル — 正規表現によるシークレットパターンに一致しないため、見落とされがちです。
assets/ディレクトリツリーで.db、.sqlite、.jks、.bks、.p12ファイルを手動で確認してください。 - ProGuard/R8 による難読化 — ほとんどの APK は難読化されています。シークレットのパターンは名前の変更後も残りますが、クラス名は残りません。メソッド名が
a.b.c.dだからといって、APK を見落とさないでください。 - マルチ dex APK — 一部のシークレットは
classes2.dex以降に存在します。classes.dexだけを調べて終わりにしないでください。 - Play ストアのデベロッパーページには、非アクティブなアプリやベータ版アプリが掲載されていることがあります — ダウンロード数や最終更新日と照合してください。古いアプリはアーカイブ済みでも、シークレットが漏えいしたままの場合があります。
検証
このセルフテストを実行し、パイプラインが最初から最後まで正常に動作することを確認します。
-
取得テスト — 公開されている APK をダウンロードし、ファイルが有効な ZIP であることを確認します:
download-apk() { local pkg="$1" curl -sk -L --max-time 60 --connect-timeout 10 "https://d.apkpure.net/b/APK/$pkg?version=latest" -o "/tmp/$pkg.apk" file "/tmp/$pkg.apk" # should report: Zip archive data / Java archive data } download-apk org.example.testapp -
逆コンパイルテスト — apktool がバイナリのマニフェストをデコードすることを確認します:
apktool d /tmp/org.example.testapp.apk -o /tmp/testapp_decoded/ ls /tmp/testapp_decoded/AndroidManifest.xml 2>/dev/null && echo "PASS: manifest decoded" || echo "FAIL: apktool not installed or APK corrupted" -
シークレット grep テスト — 既知のテスト値にパターンが一致することを確認します:
echo "AKIA1234567890ABCDEF" | grep -oE 'AKIA[A-Z0-9]{16}' && echo "PASS: AWS key pattern" || echo "FAIL" echo "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U" | grep -oE 'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*' && echo "PASS: JWT pattern" || echo "FAIL" echo "AIzaSyD4iE2xVh2G7kLpQmN3jR8tUwXyZ1aBcDeF" | grep -oE 'AIza[A-Za-z0-9_-]{35}' && echo "PASS: Google API key pattern" || echo "FAIL" -
XAPK 処理テスト — 分割 APK 用に 7z が利用可能であることを確認します:
7z --help >/dev/null 2>&1 && echo "PASS: 7z available" || echo "FAIL: install p7zip-full" -
Firebase テスト — google-services.json の解析が動作することを確認します:
echo '{"project_info":{"project_id":"test-project"}}' > /tmp/test-gs.json python3 -c "import json; d=json.load(open('/tmp/test-gs.json')); print(d['project_info']['project_id'])" && echo "PASS" || echo "FAIL" rm -f /tmp/test-gs.json
5つすべてのテストに合格すれば、パイプラインは稼働しています。テスト 1(取得)は APKPure CDN によるブロックのため失敗する場合がありますが、残りのワークフローには影響しません。APKMirror または APK の直接 URL に切り替えてください。
ツールのインストール(初回のみ)
brew install jadx p7zip
pip install --break-system-packages frida-tools objection
# Genymotion or AVD for rooted Android emulator
APK のダウンロードを簡単にするため、download-apk シェル関数を追加できます:
download-apk() {
local pkg="$1"
curl -sk -L --max-time 60 --connect-timeout 10 "https://d.apkpure.net/b/APK/$pkg?version=latest" -o "$pkg.apk"
echo "Downloaded $(wc -c < "$pkg.apk") bytes"
file "$pkg.apk"
}
隣接するスキルとの連携
offensive-osint— Play Store/APKMirror の列挙は、より広範な OSINT の一部ですhunt-cloud-misconfig— Firebase の公開読み取りは、発見後のこのスキルの専門領域ですhunt-api-misconfig— アルゴリズムとサンプルトークンが手に入った後の JWT 操作ですevidence-hygiene— 抽出したシークレットは、レポートに含める前にマスキングする必要がありますredteam-mindset— APK のリバースエンジニアリングは、エンゲージメント初日に行うものであり、後回しにするものではありません
実際の調査結果のテンプレート(匿名化済み — 許可を得たエンゲージメント)
調査結果:顧客向け APK にハードコードされた JWT と 30 件以上の内部 API エンドポイント
- 対象:レガシーなモバイルビルドにハードコードされた情報から、内部 API の範囲が明らかになりました
- 観察事項:
com.<client>.<app>を逆コンパイルしたところ、埋め込まれた JWT(約 8 年前に有効期限切れ)と、外部からはアクセスできないapi2.<client>.exampleに対する 30 件以上の/v1/*エンドポイントパスが見つかりました - 説明:APK に開発者向けビルドの成果物が含まれています。パストークンはセキュリティ・バイ・オブスキュリティであり、API エンドポイント一覧は偵察に利用できる情報です。
- 影響:VPN の侵入口を確保した後(認証情報の侵害による)、攻撃者はバイナリをリバースエンジニアリングせずに API の全体像を把握できます。HS256 シークレットを復元できた場合(SSRF/LFI 経由)、任意のトークンを偽造できます。
- 推奨事項:HS256 シークレットをローテーションし、RS256 に移行し、ビルドからハードコードされた URL を削除し、組織内のすべての APK を監査してください。
- 証拠:抽出した文字列、デコードした JWT、エンドポイント一覧
関連スキルと攻撃チェーン
cloud-iam-deep— APK からのシークレット抽出によって、使用可能な AWS/GCP/Azure 認証情報が見つかることがよくあります。チェーンの起点:jadx による文字列検索で AWS Access Key ID と Secret を取得 →cloud-iam-deepのaws sts get-caller-identity→ ロール/ポリシーの列挙 → IAM 権限昇格経路(文書化されている 24 種類の AWS 権限昇格パターンのいずれか)→ クラウドプレーンの掌握。同じ流れは、APK リソースから抽出した GCP サービスアカウント JSON や Azure の共有アクセス署名トークンにも当てはまります。hunt-api-misconfig— APK のエンドポイント一覧から API の全体像をすぐに把握でき、マスアサインメント、JWT、CORS の不具合がよく見つかります。チェーンの起点:APK から/v1/users/meと/v1/admin/usersが判明 →/v1/users/meに対するhunt-api-misconfigのマスアサインメント検査({is_admin:true})→ 管理者ロールへの昇格 →/v1/admin/usersへのアクセス。hunt-rce— APK から抽出したハードコード済みの JWT 署名シークレット(HS256)を使うと、任意のトークンを偽造できます。チェーンの起点:APK の文字列から HS256 シークレットを取得 → 管理者トークンを偽造 → 管理 API にアクセス → API に eval/テンプレート/シンクが存在する場合は、hunt-rceでサーバーに到達します。また、エクスポートされたコンポーネントにインテントインジェクションのシンクがある場合、アプリがローカルにインストールされていればRuntime.execに到達できます。offensive-osint— APK は、より広範な組織の偵察グラフを構成するノードの 1 つです。漏えいデータのコーパスや証明書の透明性情報と組み合わせます。チェーンの起点:APK から内部 API のホスト名api2.example.comが判明 →offensive-osintで証明書の透明性情報を検索 → 関連するサブドメインを発見 → 攻撃対象領域を拡大。redteam-report-template— クライアントのエンジニアはモバイルアプリの静的解析結果を「難読化の問題」として退けることが多いため、APK の調査結果では「バイナリから何が漏えいするか」を明確に示す必要があります。チェーンの起点:検証済みの調査結果(抽出したトークン/URL/シークレット)→triage-validationの 7-Question Gate(特に「この認証情報は現在も認証に使えるか?」)→ バイナリのバージョンと抽出手順の再現方法を明記してredteam-report-templateにまとめます。