Cisco Japan Blog

22 件の脆弱性と一連の攻撃チェーン:VPN 経由で Milesight UR32L ルータが乗っ取られる危険性

1 min read



  • Cisco Talos は、Milesight 社の UR32L ルータに 17 件の脆弱性(63 件の CVE)、MilesightVPN リモートアクセス ソリューション ソフトウェアに 5 件の脆弱性(6 件の CVE)を発見しました。
  • 発見された脆弱性を攻撃者がエクスプロイトし、UR32L と MilesightVPN を完全に侵害する可能性があります。
  • この投稿では、MilesightVPN リモートアクセス ソリューションを介してのみ UR32L に到達できるという状況での攻撃シナリオを取り上げ、攻撃者がどのように MilesightVPN をエクスプロイトし、UR32L を完全に侵害する可能性があるかを説明します。

Cisco Talos は最近、Milesight 社の UR32L(ARMv7 Linux ベースの産業用セルラールータ)と MilesightVPN(Milesight デバイス用のリモートアクセス ソリューション)に複数の脆弱性を発見しました。

Cisco Talos は本日、全部で 22 件のセキュリティアドバイザリを発表しました。そのうち 9 件は CVSS スコアが 8 を超えており、関連する CVE は 69 件に上ります。Milesight 社からは公式の修正プログラムは提供されていませんが、シスコの脆弱性開示ポリシーに従って、Talos はこれらの脆弱性を開示しています。このポリシーでは 90 日間の開示期限が設けられていますpopup_iconが、この間に Milesight 社による適切な対応は見られませんでした。

MilesightVPN は、インターネットに直接接続されていない Milesight デバイス用のリモートアクセス ソリューションです。MilesightVPN では VPN システムを使用してデバイスにリモートアクセスします。管理者は、簡単にデバイスの VPN 設定を行えます。MilesightVPN では HTTP の管理ページを使用して各デバイスから MilesightVPN へのトンネル接続をモニタリングします。また、この管理ページで、各デバイスの VPN に入るための VPN 設定を生成します。デフォルトでは、MilesightVPN が HTTP サーバーポートとあらゆるインターフェイスの VPN ポートをバインドします。このソフトウェアは、管理者とデバイスがアクセスできるサーバーマシンにインストールする必要があるため、管理者にとって最も一般的な設定は、MilesightVPN をインターネットに公開することです。

Milesight UR32L はセルラー機能を備えた産業用ルータです。権限が異なる複数のユーザーをサポートしており、シェルを利用できます(機能へのアクセスには制限があります)。また、多くの一般的な産業用ルータの特徴と機能を備えています。Milesight UR32L には、ルート権限を取得し、root で実行する機能はありません。

攻撃シナリオの概要

このブログ記事では、Milesight UR32L が攻撃の標的にされるシナリオを見ていきます。この攻撃シナリオでは、UR32L ルータはインターネットに公開されておらず、内部ネットワークとルータ自体へのアクセスには VPN トンネルを使用するものとします。Talos は、このデバイスを MilesightVPN を使用して管理するシナリオを考えました。このソフトウェアは、UR32L ルータと管理者がアクセスできるマシンにインストールする必要があります。この記事の目的を考慮し、MilesightVPN サーバーはインターネットに公開されている前提とします。

MilesightVPN は、VPN システムの基盤として OpenVPN を使用しています。定評のある VPN システムを使用するのは優れた選択です。OpenVPN はよく知られたテスト済みの VPN 技術なので、セキュリティ上の大きな問題が発生する可能性は高くありません。ただし MilesightVPN では、OpenVPN トンネルに関連したサービス(HTTP サーバーなど)を作成して接続をモニタリングするほか、各デバイスの VPN に入るための OpenVPN 設定を生成します。デフォルトでは、MilesightVPN は HTTP サーバーポートとあらゆるインターフェイスの OpenVPN ポートをバインドします。つまり、インターネットに公開されている MilesightVPN にアクセスできれば、そうしたサービスにもデフォルトでアクセスできるということになります。

UR32L には独自の HTTP サーバーがあり、ユーザーがルータの設定を管理できます。また、このシナリオの前提として、UR32L の HTTP サーバーには VPN を介してのみアクセスでき、MilesightVPN サーバーは VPN 設定を生成できる唯一のノードであるとします。

想定する攻撃シナリオを図で表したもの

下図には、想定する攻撃シナリオに関連する脆弱性のみを記載しています。この図は、攻撃者が Milesight UR32L のルート権限を取得するためにたどる可能性のあるステップを示しています。

攻撃の解説

攻撃者が MilesightVPN サーバーの IP アドレスを特定しても、有効なログイン情報がないため HTTP の管理ページにログインすることはできません。MilesightVPN の HTTP サーバーのログインページは以下のように表示されます。

この状態から、攻撃者が TALOS-2023-1701popup_icon(CVE-2023-22319)をエクスプロイトする可能性があり、成功すればログインをバイパスし、MilesightVPN の管理用 Web ページにアクセスできるようになります。これは、提供されたログイン情報をチェックする LoginAuth 関数における SQL インジェクションの脆弱性です。このチェックでは、SQL クエリでプリペアドステートメントが使用されていません(サニタイズ処理が行われません)。LoginAuth のコードは以下のとおりです。

function LoginAuth(res,postdata,connection){

console.info('#######log.node:loginauth start');

var sha512=crypto.createHash('sha512');

sha512.update(postdata.pwd);

var pwd=sha512.digest('hex');

[1]     $sql="select * from user where user='"+postdata.user+"' and passwd='"+pwd+"'";

[2]     connection.query($sql).then(function(data){

var result={};

if(data['error'])

{

[...]

}

else

{

if(data['result'].length>0)

{

var dt=data['result'];

result['status']=1;

var token=generateToken(dt[0]['user']);

var exp=new Date(

new Date().getTime()+

expiretime*1000).toUTCString();

res.setHeader('Set-Cookie',['token='+token]);

console.info('#######log.node:loginauth success');

res.write(JSON.stringify(result));

res.end();

}

else

{

[...]

}

}

});

}

この関数では、[1] 提供されたユーザー名とパスワードが既存のユーザーのものと一致するかどうかをチェックするための SQL クエリを作成しています。次に、[2] でこのクエリが実行され、結果のテーブルが空でなければ、最初に一致したユーザーに対応する JWT が作成され、Set-Cookie の値としてレスポンスヘッダーに置かれます。クエリ文字列がプリペアドステートメントではなく文字列の連結によって「準備」されているので、この関数には SQL インジェクションの脆弱性があります。この SQL インジェクションにより、攻撃者が HTTP サーバーの認証をバイパスし、管理者のアクセス権を得ることができます。

ログインページをバイパスすると、以下のようなインターフェイスが表示されます。この例では、複数のデバイスが登録されていて、1 台のデバイスが実際に接続されていることがわかります。

ここでは、サーバーに接続されているデバイスとその IP に関する情報を収集できます。さらに、OpenVPN の設定ファイルを入手することも可能です。これは、VPN トンネルに入り、それまでは到達できなかったデバイスと通信するのに必要なものです。以上の流れで、VPN 内のすべてのデバイスと通信できるようになります。下の画像は、Milesight UR32L ルータの HTTP サーバーのログインページです。

この先は、複数の攻撃経路が考えられます。TALOS-2023-1697popup_icon(CVE-2023-23902)は、認証前のスタックベースのバッファオーバーフローの脆弱性で、任意のリモートコード実行(RCE)につながる可能性があります。

攻撃者に求められる唯一の要件は、ルータの HTTP サーバーと通信できることです。これは、uhttpd バイナリ(UR32L の HTTP サーバーのバイナリ)の decrypt_string 関数における、認証前のスタックベースのバッファオーバーフローの脆弱性です。関数は、Web ページへのログイン時に提供されるログインパスワードを復号する役割を担っています。ブラウザから送信されたパスワードは、まず AES で暗号化され、次に Base64 でエンコードされます。decrypt_string は、そのパスワードを Base64 でデコードしてから AES で復号します。

uhttpd バイナリは位置独立実行形式(PIE)ではないので常に同じ仮想メモリアドレスにロードされますが、このバイナリのライブラリは位置独立コード(PIC)です。このため、ライブラリのコードの一部を再利用する必要がない限り、バイナリのコードを使用してコード再利用攻撃を実行することができ、情報が洩れることもありません。さらに、バイナリではスタックカナリアが使用されていないため、コード再利用はエクスプロイトの成功が最も見込める攻撃シナリオとなります。

バイナリは root として実行され、system 関数や popen 関数といった関数を利用します。そのため、OS コマンドの実行を狙った攻撃を仕掛けることも選択肢の 1 つとなります。

以下は、uhttpd decrypt_string のコードスニペットです。

void decrypt_string(

char *b64_encrypted_password,

char *decrypted_password,

size_t size_decrypted_password)

{

[...]

uchar stack_decrypted_string [72];

[... init the AES_key variable]

[... init the AES_IV variable]

[... calculate the __size variable value ...]

base64_decoded_string_start = (uchar *)malloc(__size);

[...]

memset(base64_decoded_string_start,0,__size);

processed_len = 0;

base64_decode_string_cursor = base64_decoded_string_start;

do {

// this check allow to base64 decode the string before decrypting it

if ((password_len_ - padding) <= processed_len) {

*base64_decode_string_cursor = '\0';

ctx = EVP_CIPHER_CTX_new();

[... check error ...]

cipher_type = EVP_aes_128_cbc();

processed_len = EVP_DecryptInit_ex(

ctx,

cipher_type,

(ENGINE *)0x0,

AES_key,

AES_IV);

[... check error ...]

[1]         processed_len = EVP_DecryptUpdate(

ctx,

stack_decrypted_string,

&output_len,

base64_decoded_string_start,

(base64_decode_string_cursor +

(-1 - base64_decoded_string_start)));

[... check error ...]

processed_len = output_len;

iVar3 = EVP_DecryptFinal_ex(

ctx,

stack_decrypted_string + output_len,&output_len);

[... check error ...]

processed_len = processed_len + output_len;

EVP_CIPHER_CTX_free(ctx);

stack_decrypted_string[processed_len] = '\0';

EVP_cleanup();

ERR_free_strings();

free(base64_decoded_string_start);

[2]         strncpy(

decrypted_password,

(char*)stack_decrypted_string,

size_decrypted_password);

return;

}

[...]

[3]            [... base64 decode ...]

} while( true );

}

この関数には 3 つのパラメータがあります。b64_encrypted_password パラメータは、AES で暗号化され Base64 でエンコードされたパスワード文字列であり、Base64 でデコードされた後に AES で復号されます。decrypted_password パラメータは、デコードされ復号されたパスワードがコピーされる宛先バッファです。size_decrypted_password パラメータは、decrypted_password バッファのサイズです。[3] b64_encrypted_password 文字列が Base64 でデコードされ、[1] で復号された値が AES で復号され、72 バイト長のスタックバッファ stack_decrypted_string に格納されます。最終的に、[2]size_decrypted_password バイトが stack_decrypted_string バッファから decrypted_password バッファにコピーされます。

decrypted_password では、size_decrypted_password バイトだけがコピーされるため、復号された decrypted_password バッファのあらゆる種類のバッファオーバーフローを防ぐことができます。

[1] では、OpenSSL EVP_DecryptUpdate 関数が、Base64 でデコードされたユーザー制御データの AES 復号を実行し、stack_decrypted_string スタックバッファに格納します。stack_decrypted_string スタックバッファは固定サイズであり、ユーザーから提供された文字列は、復号されるとスタックバッファよりも長くなる場合があります。これが原因で、stack_decrypted_string バッファでオーバーフローが発生する可能性があります。

ログイン API は基本的に、AES で暗号化され、Base64 でエンコードされたパスワードを受け取ります。この関数は、Base64 の文字列をデコードし、それを AES で復号するために使用されます。これにより、実際のパスワードの値が得られます。この脆弱性は、パスワードがスタックバッファをオーバーフローし、保存されている戻り先のアドレスを含むスタックの内容を上書きしてしまうというものです。

この関数で最後に処理される以下のコードが、脆弱性のエクスプロイトを容易にしています。

strncpy(

decrypted_password,

(char*)stack_decrypted_string,

size_decrypted_password);

return;

decrypted_password は、この関数の呼び出し元のスタックスペースにあるバッファで、デコードされ復号されたパスワードが保存されます。アセンブリでは次のようになります。

ldr r0, [sp,#0xc]

bl strncpy

add sp, sp, #0x84

pop {r4,r5,r6,r7,r8,r9,r10,r11,pc}

平文のパスワードを decrypted_password 配列にコピーする関数が strncpy であり、これは呼び出し元に戻る前の最後の関数呼び出しでもあります。関数の第 1 引数を渡すために、r0 レジスタが使用されます。strncpy の場合、r0 はコピー先のバッファを指します。strncpy の関数呼び出しの後は、r0 は平文のパスワードの先頭を指します。

スタックバッファ オーバーフローの脆弱性であること、バイナリがスタックカナリアを持たないこと、バイナリが PIE ではなく system 関数が使用されていることに加え、r0 が指す内容を制御できることが原因で、脆弱性のエクスプロイトが容易になっています。下の gdb のスクリーンショットで示すように、実行するコマンドを平文パスワードのペイロードの先頭に配置することによって、攻撃者が戻り先のアドレスを上書きし、その関数を system 関数に戻して、実行されるシェルコマンドを制御する可能性があります。

decrypt_string 関数の最後の命令が pc です。バイナリが PIE ではないため、戻り先のアドレスが system の plt エントリで上書きされており、r0 レジストリは制御可能な文字列(controllable)を指しています。そのため、decrypt_string が戻ると system(“controllable”) が実行されます。

脆弱性の詳細

以下の脆弱性は、Milesight UR32L におけるコマンドインジェクションの問題です。これらの脆弱性は、このルータのさまざまな機能に存在します。細工されたネットワークパケットを標的のデバイスに送信することによって、攻撃者がこれらの脆弱性をエクスプロイトする可能性があります。

UR32L には、バッファオーバーフローにつながる脆弱性も複数存在します。攻撃者が、個別の脆弱性に応じて細工した HTTP リクエストまたはネットワークリクエストを標的のデバイスに送信することにより、これらの脆弱性を引き起こす可能性があります。

TALOS-2023-1705popup_icon(CVE-2023-23546)は、Milesight UR32L の設定ミスの脆弱性であり、攻撃者がデバイス上で高い権限を取得する可能性があります。この場合、攻撃者がこの脆弱性をエクスプロイトするには中間者攻撃を行う必要があります。

TALOS-2023-1696popup_icon(CVE-2023-23571)というアクセス違反の脆弱性もあり、細工されたネットワークリクエストを攻撃者がデバイスに送信することで、サービス拒否につながる危険性があります。TALOS-2023-1695popup_icon(CVE-2023-23547)も、細工されたネットワークリクエストによってエクスプロイトされる可能性がありますが、この場合は任意のファイルが読み取られてしまいます。

Talos はそのほか、MilesightVPN に以下の 5 件の脆弱性を発見しました。

  • TALOS-2023-1700popup_icon(CVE-2023-22844):認証バイパスの脆弱性
  • TALOS-2023-1701popup_icon(CVE-2023-22319):SQL インジェクションの脆弱性
  • TALOS-2023-1702popup_icon(CVE-2023-23907):ディレクトリトラバーサルの脆弱性
  • TALOS-2023-1703popup_icon(CVE-2023-22371):コマンドインジェクションの脆弱性
  • TALOS-2023-1704popup_icon(CVE-2023-24496、CVE-2023-24497):クロスサイトスクリプティングの脆弱性

カバレッジ

これらの脆弱性に対するエクスプロイトを検出する Snort ルールは、61206 ~ 61208、61212、61255 ~ 61258、61266 ~ 61269、61395 ~ 61397 です。今後、脆弱性に関する新たな情報が追加されるまでの間は、ルールが追加されたり、現行のルールが変更されたりする場合がありますのでご注意ください。最新のルールの詳細については、Cisco Secure Firewall または Snort.org を参照してください。

 

本稿は 2023 年 07 月 06 日に Talos Grouppopup_icon のブログに投稿された「Taking over Milesight UR32L routers behind a VPN: 22 vulnerabilities and a full chainpopup_icon」の抄訳です。

 

コメントを書く