
2025/12/19 0:00
Using TypeScript to obtain one of the rarest license plates
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
著者は、車両登録局(DMV)から免許証の更新を促すリマインダーを受け取った後、フロリダ州のユニークなバニティプレートを追い求め始めました。フロリダ州はユーザーがカスタムプレートの空き状況を確認できる公開ASP.NET Webフォームを提供しています。このフォームは隠しフィールド(
__VIEWSTATE、__VIEWSTATEGENERATOR、__EVENTVALIDATION)を露出しますが、CAPTCHAやIPレート制限はありません。著者はフォームを一度取得し、正規表現でこれらの隠しフィールドを抽出し、一回のリクエストで最大5つのプレート組み合わせを投稿することで、自動化された毎時チェックを実行しました。結果は「AVAILABLE」または「NOT AVAILABLE」のラベル(lblOutPutRowOne–Five)として表示され、彼はそれらをプログラム的に解析しPostgreSQLに保存しました。
このマイクロサービスを使用して、著者は「EO」のような珍しい2文字組み合わせを監視しました。11月26日に「EO」が利用可能になりましたが、取引完了前に別のユーザーに主張されてしまいました。その後、「HY」を追跡し、最初の試行で失敗した後に再び出現するのを確認しました。税務署で長時間待った後、彼は「HY」の予約を成功させ、支払いも完了しました。フロリダ州のカスタムプレートには60日間の配送期間があるため、著者はまだ物理的なプレートを提示できませんが、予約が有効であることを確認しています。
この記事では、この脆弱性―保護策なしに高頻度で自動チェックを行う点―が州にCAPTCHAやより厳格なレートリミットの追加を促す可能性があり、オンラインシステムのギャップが望ましいバニティプレートを確保するために悪用される方法を示しています。
本文
ほとんどの人は、DMV(車両管理局)が割り当てる文字と数字のランダムな組み合わせについて二度考えることさえありません。
私はそうした人ではありません。
オンラインではずっとクリーンで覚えやすいデジタルアイデンティティを追求してきました。長年にわたり、Instagram には最初の名前+苗字(
@jlaf)と、各種プラットフォームでフルワード(@explain、@discontinue)などのハンドルを取得してきました。そのため DMV が車両登録更新の三回目のリマインダーを郵送した際、その直感が再び働きました。なぜ自分だけのユニークなナンバープレート組み合わせを考えていなかったのかと。
ナンバープレートの希少性階層
| 種類 | 可能な組み合わせ数 |
|---|---|
| 単一数字プレート | 10 |
| 繰り返し数字プレート | 10 |
| 単一文字プレート | 26 |
| 繰り返し文字組み合わせ | ? |
| 二文字プレート組み合わせ | 676 |
これらのレアなプレートを調べた結果、好奇心が勝ってしまいました。実際にどれほど希少になるのでしょうか?また、州の公開検索ツールを使えばどこまで掘り下げられるのでしょうか?
PlateRadar と独占権
現在、ナンバープレートの在庫情報を大量に取得できる唯一のリソースは PlateRadar です。賢いサイトなら、このデータが誰かにとって価値あるものだと理解し、その希少情報を $20/月 の有料壁の裏側に隠します。また、24時間ごとにデータを更新しており、レアなアイテムを手に入れる際にはタイミングが極めて重要です。24時間で十分とは言えません。
フロリダ州の Vanity Plate チェッカー
フロリダ州は他の多くの州と違い、車両登録オフィスに並ぶ前に「カスタム文字列」(プレートに印刷したい文字・数字の組み合わせ)が利用可能かどうかを確認できるウェブサイトを提供しています。このツールは、どのプレートタイプがその組み合わせをサポートしているかも教えてくれます。なぜなら、各プレートには文字数制限が異なるためです(例:5文字のみ許可されているものと7文字まで可能なもの)。
サイトには便利な機能があります。複数の組み合わせを同時にチェックでき、リクエストごとの遅延は発生しません。私は手動で送信していた際に、実際に高速でリクエストできることに気づきました――もし全プロセスを自動化したらどうでしょう?
制限のないレート
Burp Suite を起動し、サービスへのリクエストをプロキシしました。
POST https://services.flhsmv.gov/mvcheckpersonalplate/ __VIEWSTATE=… __VIEWSTATEGENERATOR=0719FE0A __EVENTVALIDATION=… ctl00$MainContent$txtInputRowOne=MYPLATE … (rows two–five は空) ctl00$MainContent$btnSubmit=Submit
__VIEWSTATE、__VIEWSTATEGENERATOR、__EVENTVALIDATION のフィールドは即座にこれは ASP.NET Web フォームであることを示しました。政府のウェブサイトということで、他に何が期待できるかは想像しづらいです。
EVENTVALIDATION(2006年導入)は偽造フォーム送信を防止するものですが、実際にはスクレイピングを難しくすべきです。各リクエストごとに新しい値を取得すれば、システムを簡単に圧倒しレート制限に引っかかるはずでした。
しかし全くレート制限がありませんでした。
CAPTCHA も IP 制限も WAF(Web アプリケーションファイアウォール)もなく、Burp Repeater は一連の空ペイロードリクエストすべてに
200 OK を返しました。
プロセスの自動化
ワークフローは次の通りです:
- ページを 1 回取得。実際のブラウザヘッダーでフォームを読み込み、3 つの隠しフィールドを取得します。
- 値を抽出。正規表現ヘルパーを使用。
function extractFormFields(html: string) { const viewStateMatch = html.match(/id="__VIEWSTATE"\s+value="([^"]+)"/); const viewStateGeneratorMatch = html.match( /id="__VIEWSTATEGENERATOR"\s+value="([^"]+)"/ ); const eventValidationMatch = html.match( /id="__EVENTVALIDATION"\s+value="([^"]+)"/ ); if (!viewStateMatch || !viewStateGeneratorMatch || !eventValidationMatch) { throw new Error("Failed to extract required form fields from page"); } return { viewState: viewStateMatch[1], viewStateGenerator: viewStateGeneratorMatch[1], eventValidation: eventValidationMatch[1], }; }
- POST データを構築。プレート組み合わせは
(XXX は 1〜5)で送信します。5 枚同時にチェックすると処理速度が大幅に向上します。ctl00$MainContent$txtInputRowXXX
function buildFormData( plates: string[], viewState: string, viewStateGenerator: string, eventValidation: string ) { const params = new URLSearchParams(); params.append("__VIEWSTATE", viewState); params.append("__VIEWSTATEGENERATOR", viewStateGenerator); params.append("__EVENTVALIDATION", eventValidation); const fieldNames = [ "ctl00$MainContent$txtInputRowOne", "ctl00$MainContent$txtInputRowTwo", "ctl00$MainContent$txtInputRowThree", "ctl00$MainContent$txtInputRowFour", "ctl00$MainContent$txtInputRowFive", ]; for (let i = 0; i < 5; i++) { params.append( fieldNames[i], i < plates.length ? plates[i].toUpperCase() : "" ); } params.append("ctl00$MainContent$btnSubmit", "Submit"); return params.toString(); }
- POST を送信し、レスポンスを解析。
function extractPlateStatuses( html: string, plates: string[] ): PlateCheckResult[] { const results: PlateCheckResult[] = []; const labelIds = [ "MainContent_lblOutPutRowOne", "MainContent_lblOutPutRowTwo", "MainContent_lblOutputRowThree", "MainContent_lblOutputRowFour", "MainContent_lblOutputRowFive", ]; for (let i = 0; i < plates.length; i++) { const labelId = labelIds[i]; const regex = new RegExp(`id="${labelId}"[^>]*>([^<]*)<`, "i"); const match = html.match(regex); const status = match ? match[1].trim() : ""; const available = status.toUpperCase() === "AVAILABLE"; results.push({ plate: plates[i], available, status: status || "UNKNOWN", }); } return results; }
’25 年のプレート戦争
スクリプトが順調に動いたので、結果を PostgreSQL に保存し、最後にチェックしたタイムスタンプも記録する小さなマイクロサービスを構築しました。高価値組み合わせ(単一または二文字)については 1〜2 時間ごとにポーリングしました。知らずにシステムはリアルタイムで更新されていました:誰かがプレートを予約すると、次の検索時点でバックエンドに反映されるのです。
データ可視化には Next.js のフロントエンドを作り、結果を閲覧・フィルタリングし、テキストファイルからプレートリストを一括アップロードして高速チェックできるようにしました。
WEBSITE、SITE、CAPTCHA などのクールな組み合わせも見つけましたが、唯一残っていた二文字組み合わせ EO を発見したときほど心躍らせるものはありませんでした。
- EO は 11 月 26 日に利用可能でした。
- 12 月 1 日には既に使用不可という通知を受けました。誰かが開店時点で取得したようです。
- 二文字プレート全体を再チェックし、別の組み合わせがまだプールに戻っていることを確認しました。
- 税務署へ直行し、1 時間待機後、自分の事情を説明して HY を予約しました。
フロリダ州ではカスタムプレートは 60 日以内に配送されるので、実際のプレートはまだ届いていませんが、存在し、支払済みであることは確かです。TypeScript と決意さえあれば、ほぼ何でも手に入れられるという証拠ですね。