improve ua modifier with client side spoofing

This commit is contained in:
Kevin Pham
2023-11-27 12:00:12 -06:00
parent fb63dba8a0
commit 59cf0317ec
34 changed files with 490 additions and 274 deletions

View File

@@ -2,43 +2,46 @@
// Also overrides the attribute setter prototype to modify the request URLs
// fetch("/relative_script.js") -> fetch("http://localhost:8080/relative_script.js")
(() => {
// ============== PARAMS ===========================
// if the original request was: http://localhost:8080/http://proxiedsite.com/foo/bar
// proxyOrigin is http://localhost:8080
// if the original request was: http://localhost:8080/http://proxiedsite.com/foo/bar
// proxyOrigin is http://localhost:8080
const proxyOrigin = "{{PROXY_ORIGIN}}";
//const proxyOrigin = globalThis.window.location.origin;
// if the original request was: http://localhost:8080/http://proxiedsite.com/foo/bar
// origin is http://proxiedsite.com
// if the original request was: http://localhost:8080/http://proxiedsite.com/foo/bar
// origin is http://proxiedsite.com
const origin = "{{ORIGIN}}";
//const origin = (new URL(decodeURIComponent(globalThis.window.location.pathname.substring(1)))).origin
// ============== END PARAMS ======================
const blacklistedSchemes = [
"ftp:",
"mailto:",
"tel:",
"file:",
"blob:",
"javascript:",
"about:",
"magnet:",
"ws:",
"wss:",
];
const blacklistedSchemes = [
"ftp:",
"mailto:",
"tel:",
"file:",
"blob:",
"javascript:",
"about:",
"magnet:",
"ws:",
"wss:",
];
function rewriteURL(url) {
const oldUrl = url
if (!url) return url
let isStr = (typeof url.startsWith === 'function')
if (!isStr) return url
function rewriteURL(url) {
const oldUrl = url;
if (!url) return url;
let isStr = typeof url.startsWith === "function";
if (!isStr) return url;
// don't rewrite special URIs
if (blacklistedSchemes.includes(url)) return url;
// don't rewrite invalid URIs
try { new URL(url, origin) } catch { return url }
try {
new URL(url, origin);
} catch {
return url;
}
// don't double rewrite
if (url.startsWith(proxyOrigin)) return url;
@@ -57,132 +60,163 @@
} else if (url.startsWith("/")) {
url = `/${origin}/${encodeURIComponent(url.substring(1))}`;
} else if (url.startsWith(origin)) {
url = `/${encodeURIComponent(url)}`
url = `/${encodeURIComponent(url)}`;
} else if (url.startsWith("http://") || url.startsWith("https://")) {
url = `/${proxyOrigin}/${encodeURIComponent(url)}`;
}
console.log(`proxychain: rewrite JS URL: ${oldUrl} -> ${url}`)
console.log(`proxychain: rewrite JS URL: ${oldUrl} -> ${url}`);
return url;
};
}
// sometimes anti-bot protections like cloudflare or akamai bot manager check if JS is hooked
function hideMonkeyPatch(objectOrName, method, originalToString) {
let obj;
let isGlobalFunction = false;
if (typeof objectOrName === 'string') {
if (typeof objectOrName === "string") {
obj = globalThis[objectOrName];
isGlobalFunction = (typeof obj === 'function') && (method === objectOrName);
isGlobalFunction = (typeof obj === "function") &&
(method === objectOrName);
} else {
obj = objectOrName;
}
if (isGlobalFunction) {
const originalFunction = obj;
globalThis[objectOrName] = function(...args) {
return originalFunction.apply(this, args);
};
globalThis[objectOrName].toString = () => originalToString;
} else if (obj && typeof obj[method] === 'function') {
} else if (obj && typeof obj[method] === "function") {
const originalMethod = obj[method];
obj[method] = function(...args) {
return originalMethod.apply(this, args);
};
obj[method].toString = () => originalToString;
} else {
console.warn(`proxychain: cannot hide monkey patch: ${method} is not a function on the provided object.`);
console.warn(
`proxychain: cannot hide monkey patch: ${method} is not a function on the provided object.`,
);
}
}
// monkey patch fetch
const oldFetch = fetch;
fetch = async (url, init) => {
return oldFetch(rewriteURL(url), init)
}
hideMonkeyPatch('fetch', 'fetch', 'function fetch() { [native code] }')
// monkey patch fetch
const oldFetch = fetch;
fetch = async (url, init) => {
return oldFetch(rewriteURL(url), init);
};
hideMonkeyPatch("fetch", "fetch", "function fetch() { [native code] }");
// monkey patch xmlhttprequest
const oldOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, async = true, user = null, password = null) {
return oldOpen.call(this, method, rewriteURL(url), async, user, password);
};
hideMonkeyPatch(XMLHttpRequest.prototype, 'open', 'function(){if("function"==typeof eo)return eo.apply(this,arguments)}');
// monkey patch xmlhttprequest
const oldOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(
method,
url,
async = true,
user = null,
password = null,
) {
return oldOpen.call(this, method, rewriteURL(url), async, user, password);
};
hideMonkeyPatch(
XMLHttpRequest.prototype,
"open",
'function(){if("function"==typeof eo)return eo.apply(this,arguments)}',
);
const oldSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(method, url) {
return oldSend.call(this, method, rewriteURL(url));
};
hideMonkeyPatch(XMLHttpRequest.prototype, 'send', 'function(){if("function"==typeof eo)return eo.apply(this,arguments)}');
const oldSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(method, url) {
return oldSend.call(this, method, rewriteURL(url));
};
hideMonkeyPatch(
XMLHttpRequest.prototype,
"send",
'function(){if("function"==typeof eo)return eo.apply(this,arguments)}',
);
// monkey patch service worker registration
const oldRegister = ServiceWorkerContainer.prototype.register;
ServiceWorkerContainer.prototype.register = function(scriptURL, options) {
return oldRegister.call(this, rewriteURL(scriptURL), options);
};
hideMonkeyPatch(
ServiceWorkerContainer.prototype,
"register",
"function register() { [native code] }",
);
// monkey patch service worker registration
const oldRegister = ServiceWorkerContainer.prototype.register;
ServiceWorkerContainer.prototype.register = function(scriptURL, options) {
return oldRegister.call(this, rewriteURL(scriptURL), options)
}
hideMonkeyPatch(ServiceWorkerContainer.prototype, 'register', 'function register() { [native code] }')
// monkey patch URL.toString() method
const oldToString = URL.prototype.toString;
URL.prototype.toString = function() {
let originalURL = oldToString.call(this);
return rewriteURL(originalURL);
};
hideMonkeyPatch(
URL.prototype,
"toString",
"function toString() { [native code] }",
);
// monkey patch URL.toString() method
const oldToString = URL.prototype.toString
URL.prototype.toString = function() {
let originalURL = oldToString.call(this)
return rewriteURL(originalURL)
}
hideMonkeyPatch(URL.prototype, 'toString', 'function toString() { [native code] }')
// monkey patch URL.toJSON() method
const oldToJson = URL.prototype.toString;
URL.prototype.toString = function() {
let originalURL = oldToJson.call(this);
return rewriteURL(originalURL);
};
hideMonkeyPatch(
URL.prototype,
"toString",
"function toJSON() { [native code] }",
);
// monkey patch URL.toJSON() method
const oldToJson = URL.prototype.toString
URL.prototype.toString = function() {
let originalURL = oldToJson.call(this)
return rewriteURL(originalURL)
}
hideMonkeyPatch(URL.prototype, 'toString', 'function toJSON() { [native code] }')
// Monkey patch URL.href getter and setter
const originalHrefDescriptor = Object.getOwnPropertyDescriptor(URL.prototype, 'href');
Object.defineProperty(URL.prototype, 'href', {
// Monkey patch URL.href getter and setter
const originalHrefDescriptor = Object.getOwnPropertyDescriptor(
URL.prototype,
"href",
);
Object.defineProperty(URL.prototype, "href", {
get: function() {
let originalHref = originalHrefDescriptor.get.call(this);
return rewriteURL(originalHref)
return rewriteURL(originalHref);
},
set: function(newValue) {
originalHrefDescriptor.set.call(this, rewriteURL(newValue));
}
},
});
// TODO: do one more pass of this by manually traversing the DOM
// AFTER all the JS and page has loaded just in case
// AFTER all the JS and page has loaded just in case
// Monkey patch setter
// Monkey patch setter
const elements = [
{ tag: 'a', attribute: 'href' },
{ tag: 'img', attribute: 'src' },
{ tag: "a", attribute: "href" },
{ tag: "img", attribute: "src" },
// { tag: 'img', attribute: 'srcset' }, // TODO: handle srcset
{ tag: 'script', attribute: 'src' },
{ tag: 'link', attribute: 'href' },
{ tag: 'link', attribute: 'icon' },
{ tag: 'iframe', attribute: 'src' },
{ tag: 'audio', attribute: 'src' },
{ tag: 'video', attribute: 'src' },
{ tag: 'source', attribute: 'src' },
{ tag: "script", attribute: "src" },
{ tag: "link", attribute: "href" },
{ tag: "link", attribute: "icon" },
{ tag: "iframe", attribute: "src" },
{ tag: "audio", attribute: "src" },
{ tag: "video", attribute: "src" },
{ tag: "source", attribute: "src" },
// { tag: 'source', attribute: 'srcset' }, // TODO: handle srcset
{ tag: 'embed', attribute: 'src' },
{ tag: 'embed', attribute: 'pluginspage' },
{ tag: 'html', attribute: 'manifest' },
{ tag: 'object', attribute: 'src' },
{ tag: 'input', attribute: 'src' },
{ tag: 'track', attribute: 'src' },
{ tag: 'form', attribute: 'action' },
{ tag: 'area', attribute: 'href' },
{ tag: 'base', attribute: 'href' },
{ tag: 'blockquote', attribute: 'cite' },
{ tag: 'del', attribute: 'cite' },
{ tag: 'ins', attribute: 'cite' },
{ tag: 'q', attribute: 'cite' },
{ tag: 'button', attribute: 'formaction' },
{ tag: 'input', attribute: 'formaction' },
{ tag: 'meta', attribute: 'content' },
{ tag: 'object', attribute: 'data' },
{ tag: "embed", attribute: "src" },
{ tag: "embed", attribute: "pluginspage" },
{ tag: "html", attribute: "manifest" },
{ tag: "object", attribute: "src" },
{ tag: "input", attribute: "src" },
{ tag: "track", attribute: "src" },
{ tag: "form", attribute: "action" },
{ tag: "area", attribute: "href" },
{ tag: "base", attribute: "href" },
{ tag: "blockquote", attribute: "cite" },
{ tag: "del", attribute: "cite" },
{ tag: "ins", attribute: "cite" },
{ tag: "q", attribute: "cite" },
{ tag: "button", attribute: "formaction" },
{ tag: "input", attribute: "formaction" },
{ tag: "meta", attribute: "content" },
{ tag: "object", attribute: "data" },
];
elements.forEach(({ tag, attribute }) => {
@@ -195,7 +229,7 @@
// calling rewriteURL will end up calling a setter for href,
// leading to a recusive loop and a Maximum call stack size exceeded
// error, so we guard against this with a local semaphore flag
const isRewritingSetKey = Symbol.for('isRewritingSet');
const isRewritingSetKey = Symbol.for("isRewritingSet");
if (!this[isRewritingSetKey]) {
this[isRewritingSetKey] = true;
descriptor.set.call(this, rewriteURL(value));
@@ -207,44 +241,43 @@
}
},
get() {
const isRewritingGetKey = Symbol.for('isRewritingGet');
const isRewritingGetKey = Symbol.for("isRewritingGet");
if (!this[isRewritingGetKey]) {
this[isRewritingGetKey] = true;
let oldURL = descriptor.get.call(this);
let newURL = rewriteURL(oldURL);
this[isRewritingGetKey] = false;
return newURL
return newURL;
} else {
return descriptor.get.call(this);
}
}
},
});
}
});
// sometimes, libraries will set the Element.innerHTML or Element.outerHTML directly with a string instead of setters.
// in this case, we intercept it, create a fake DOM, parse it and then rewrite all attributes that could
// contain a URL. Then we return the replacement innerHTML/outerHTML with redirected links.
function rewriteInnerHTML(html, elements) {
const isRewritingHTMLKey = Symbol.for('isRewritingHTML');
const isRewritingHTMLKey = Symbol.for("isRewritingHTML");
// Check if already processing
if (document[isRewritingHTMLKey]) {
return html;
}
const tempContainer = document.createElement('div');
const tempContainer = document.createElement("div");
document[isRewritingHTMLKey] = true;
try {
tempContainer.innerHTML = html;
// Create a map for quick lookup
const elementsMap = new Map(elements.map(e => [e.tag, e.attribute]));
const elementsMap = new Map(elements.map((e) => [e.tag, e.attribute]));
// Loop-based DOM traversal
const nodes = [...tempContainer.querySelectorAll('*')];
const nodes = [...tempContainer.querySelectorAll("*")];
for (const node of nodes) {
const attribute = elementsMap.get(node.tagName.toLowerCase());
if (attribute && node.hasAttribute(attribute)) {
@@ -253,7 +286,7 @@
node.setAttribute(attribute, rewrittenUrl);
}
}
return tempContainer.innerHTML;
} finally {
// Clear the flag
@@ -261,19 +294,21 @@
}
}
// Store original setters
const originalSetters = {};
const originalSetters = {};
['innerHTML', 'outerHTML'].forEach(property => {
const descriptor = Object.getOwnPropertyDescriptor(Element.prototype, property);
["innerHTML", "outerHTML"].forEach((property) => {
const descriptor = Object.getOwnPropertyDescriptor(
Element.prototype,
property,
);
if (descriptor && descriptor.set) {
originalSetters[property] = descriptor.set;
Object.defineProperty(Element.prototype, property, {
...descriptor,
set(value) {
const isRewritingHTMLKey = Symbol.for('isRewritingHTML');
const isRewritingHTMLKey = Symbol.for("isRewritingHTML");
if (!this[isRewritingHTMLKey]) {
this[isRewritingHTMLKey] = true;
try {
@@ -286,40 +321,8 @@ const originalSetters = {};
// Use original setter in recursive call
originalSetters[property].call(this, value);
}
}
},
});
}
});
})();
(() => {
document.addEventListener('DOMContentLoaded', (event) => {
initIdleMutationObserver();
});
function initIdleMutationObserver() {
let debounceTimer;
const debounceDelay = 500; // adjust the delay as needed
const observer = new MutationObserver((mutations) => {
// Clear the previous timer and set a new one
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
execute();
observer.disconnect(); // Disconnect after first execution
}, debounceDelay);
});
const config = { attributes: false, childList: true, subtree: true };
observer.observe(document.body, config);
}
function execute() {
console.log('DOM is now idle. Executing...');
}
})();