你以為你請求的就是你想請求的嗎?

zsea發表於2021-11-04

在當今SPA應用流行的情況下,頁面上的所有東西都是通過javascript進行載入,本文將帶你一步一步截獲使用者請求,並修改請求地址。

我們主要使用的方法為Hook原生介面進行介面呼叫攔截;在攔截前,先定義一個URL修改的函式,統一將URL請求中的before修改為after,你在你的實際處理中可能會更加複雜。

function srcHook(url) {
    let nUrl = url.replace("hook-before", "hook-after");
    return nUrl;
}

Ajax請求

在前端中,一般是通過Ajax向後臺請求資料,所以首要需要攔截的就是Ajax的請求。

先來看一下如何發出一個Ajax請求:

var xhr = new XMLHttpRequest();
xhr.timeout = 3000;
xhr.ontimeout = function (event) {
    alert("請求超時!");
}
xhr.open('GET', '/data/hook-before.txt');
xhr.send();
xhr.onreadystatechange = function () {
    if (xhr.readyState == 4 && xhr.status == 200) {

         resolve(xhr.responseText);
         WriteLogs("====響應 " + xhr.responseText);
    }
}

可以看到,傳入URL引數的方法是xhr.open,所以我們重寫XMLHttpRequestopen方法進行攔截。重寫前,需要先儲存一下原生方法。

好了,現在開始正式Hook了:

var $open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function () {
  if (srcHook) {
    var src = srcHook(arguments[1]);
    if (src === false) return;
    if (src) {
      arguments[1] = src;
    }
  }
  return $open.apply(this, arguments);
}

是的,就是這麼簡單,在重寫的open方法中,對URL引數進行修改,然後呼叫原生方法。

通過我們的日誌資訊,可以看到,訪問修改已經成功:

image-20211103164438126

沒問題,Hook成功。但是,你看下面,還有一個fetch型別的請求沒有Hook到,彆著急,馬上處理它。

仍然首先來看一下fetch的呼叫方法:

fetch("/data/hook-before.txt")
.then(function(response){
    return response.text();
}).then(function(text){
    alert(text);
})

fetch是一個全域性函式,第一個引數為需要請求的網址。我們只需要重寫window物件上的fetch函式即可。

var $fetch = window.fetch;
window.fetch = function () {
  if (srcHook) {
    var src = srcHook(arguments[0]);
    if (src === false) return;
    if (src) {
      arguments[0] = src;
    }
  }
  return $fetch.apply(window, arguments);
}

這下沒問題了,兩種請求方式都攔截了。

DOM請求

對於正常的Ajax請求,我們已經進行了處理,但在有些情況下,會在頁面中使用JSONP來進行跨域請求。

我們仍然是先來看一下JSONP的實現:

var url="/data/hook-before.js";
var script = document.createElement('script');
script.setAttribute('src', url);
document.getElementsByTagName('head')[0].appendChild(script);

可以看出,JSONP的本質是向DOM中插入一個SCRIPT的Element。從程式碼中,我輕鬆的找到的Hook點,Element例項的setAttribute方法。

var $setAttribute = Element.prototype.setAttribute;
Element.prototype.setAttribute = function () {
  if (this.tagName=="SCRIPT"&&arguments[0]=="src"&&srcHook) {
    var src = srcHook(arguments[1]);
    if (src === false) return;
    if (src) {
      arguments[1] = src;
    }
  }
  return $setAttribute.apply(this, arguments);
}

和xhr的hook完全一樣。

通過同樣的方法,也可以把imglinkiframea給hook掉。

然而,上面的hook好像也差了點啥。請看下面的程式碼:

var url="/data/hook-before.js";
var script = document.createElement('script');
script.src=url;
document.getElementsByTagName('head')[0].appendChild(script);

沒錯,不呼叫setAttribute方法一樣可以設定src

先看一看src在原型鏈上的定義:

{get: ƒ, set: ƒ, enumerable: true, configurable: true}

通過定義可以知道src的屬性描述符(property descriptor)就可以重寫的,這下好辦了,我們重寫一下srcsetter

var descriptor=Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype, "src");
var setter=descriptor["set"];
descriptor["set"]=function(value){
  if (srcHook) {
    var src = srcHook(arguments[0]);
    if (src === false) return;
    if (src) {
      arguments[0] = src;
    }
  }
  return setter.apply(this, arguments);
}
descriptor["configurable"]=false;
//由於src的set有可能會被其它指令碼修改回去,此處通過設定configurable=false來強行禁止修改
Object.defineProperty(HTMLScriptElement.prototype, "src", descriptor);

通過同樣的方法,也可以把imglinkiframeastyle中和URL相關的屬性處理掉。

提示:innerHTML也是通過這種方法進行處理。

CSS中的請求

要發起一個請求,除了上面描述的方法外,也可以通過css中的background-image屬性發起。

document.getElementById("#id").style.background="url(/data/hook-before.jpg)";

CSS屬性屬於CSSStyleDeclaration物件,該物件的原型上有以下屬性可以發起請求:

  • cssText
  • background-image
  • background
  • border-image
  • borderImage
  • border-image-source
  • borderImageSource

使用程式碼示例中的方法設定CSS屬性,會直接發起請求,我們無法攔截。但是,我們可以通過呼叫CSSStyleDeclarationsetProperty方法進行屬性設定,所以我們需要在CSSStyleDeclaration的原型鏈上定義上面的屬性,通過設定settergetter,然後呼叫setProperty方法進行實際設定。程式碼示例如下:

    Object.defineProperty(CSSStyleDeclaration.prototype, "background",
        {
            get: function () {
                return this.getPropertyValue("background");
            },
            set: function (v) {
                v=srcHook(v);
                this.setProperty("background", v);
            }
        }
    );
    Object.defineProperty(CSSStyleDeclaration.prototype, "background-image",
        {
            get: function () {
                return this.getPropertyValue("background-image");
            },
            set: function (v) {
                v=srcHook(v);
                this.setProperty("background-image", v);
            }
        }
    );
    var descriptor = Object.getOwnPropertyDescriptor(CSSStyleDeclaration.prototype, "setProperty");
    var valuer = descriptor["value"];
    descriptor["value"] = function () {
        if (srcHook) {
            var src = srcHook(arguments[1]);
            if (src === false) return;
            if (src) {
                arguments[1] = src;
            }
        }
        return valuer.apply(this, arguments);
    }
    descriptor["configurable"] = false;
    //由於src的set有可能會被其它指令碼修改回去,此處通過設定configurable=false來強行禁止修改
    Object.defineProperty(CSSStyleDeclaration.prototype, "setProperty", descriptor);

由於在對background-imagebackground等屬性進行hook時,呼叫了setProperty方法進行設定,若原始碼中直接就呼叫的setProperty方法進行設定,則需要對setProperty的屬性描述符(property descriptor)進行重寫。

HTML中的請求

HTML中的請求,我們無法進行攔截,但可以使用MutationObserver監聽DOM物件的建立,對於其中的a標籤,可以修改href屬性。對於imgsrc屬性也可以修改,但無法阻止請求的發出,修改後的請求也會正常發出。

我們先在HTML中新增一個圖片顯示的DOM

<img src="/data/hook-before.jpg" />

在沒有監聽和修改前,頁面顯示的是HOOK前的圖片,如下:

然後,我們在JS中新增監聽和修改的程式碼,我們僅用IMG進行測試:

function DomWatch() {
    // part 1
    var observer = new MutationObserver(function(mutationsList, mutationObserver){
        mutationsList.forEach(function(mutation){
            if(!mutation.addedNodes) return;
            mutation.addedNodes.forEach(function(node){
                if(node.tagName!=="IMG") return;
                node.src=srcHook(node.src);
            })
        })
    });
    // part 2
    observer.observe(document, {childList:true,attributes:true,subtree:true});
}
DomWatch();

儲存,然後重新整理一下頁面,可以發現顯示的圖片已經發生了改變。

image-20211103094141533

在這裡,雖然我們看到的圖片已經發生了變化,但實際是在HTML中指定的圖片依然會發出請求。

image-20211103094342364

在Developer Tools的網路標籤中,可以看到,發出了兩次圖片請求。

關於MutationObserver的具體用法,請可以參考

在HTML中的DOM,也可以通過遍歷的方式進行修改,但是如果用innerHTML建立的DOM,處理上就會比較麻煩。

WebSocket中的請求

WebSocket中的請求是在new的時候指定的,如下:

new WebSocket("ws://121.40.165.18:8800")

我們需要攔截WebSocket的new操作,並將連線地址修改為我們需要的地址,對於new的攔截,這裡使用ES6的Proxy進行處理。在這裡,我們統一將地址修改為ws://119.29.3.36:6700/

const __WebSocket = new Proxy(window.WebSocket, {
   construct(target, args) {
       args[0]="ws://119.29.3.36:6700/";
       return new target(...args);
   }
});
window.WebSocket = __WebSocket;

image-20211103161320432

未解決的問題

如果在網頁的指令碼中,有通過locationlocation.href來重定向頁面地址,則無法對這個動作進行攔截,location物件已經被瀏覽器定義為了不可偽造,目前沒有找到好的辦法,只能通過服務端代理,將呼叫該屬性的js程式碼進行替換。

通過屬性描述可以看到,window上的locationlocation.href均設定了不可修改。

{enumerable: true, configurable: false, get: ƒ, set: ƒ}

寫在最後

本文是以前在將淘寶手機頁面搬進微信裡顯示的時候的研究結果,但後來這個也沒有用起來,現在基於互聯互通的政策要求,也就更沒有使用場景了。

本文的相關程式碼已上傳:

相關文章