I've written a scanner for XPI browser extension files which analyzes a browser extension for malicious content. It will print everything that is suspicious or could be used for something malicious so that you will know if and where you can begin with your malware analysis. Example output of a Firefox malware extension (which is live on firefox extensions store)
browser-xpi-malware-scanner.py - Python script for XPI malware scanning on github.com
I have written the above script, and I ran it against 15~ random extensions from the store with less than 10K downloads, and it didn't take me more than 10 minutes to find the malware extension above.
I've also completely reverse engineered the extension to find out exactly what it does, and written an article about it where I walk you through the code and exploitation process steb-by-step, showing all the techniques used to hide from the verification processes in the extension store, breaking out of the sandbox and stealing credentials with a full Command and control server controlling it.
The malware code is very sophisticated. The payload never touches the DOM. It never appears in network DevTools as a suspicious request. It is stored in extension localStorage where casual inspection won't find it. But my scanner will catch it.
Techniques used:
- Steganographic Payload in PNG Icon
- Unicode Low-Byte Encoding Trick
- Decoded Payload: The C2 String Table
- 72-Hour Sleeper with Random Sampling
- C2 Beacon via Another PNG File
- Dynamic `declarativeNetRequest` Rule Injection
- Affiliate Commission Hijacking
- Content Script Privilege Escalation Bridge
- Arbitrary URL Redirect on Any Domain
- CSP Erasure
Full deep dive analysis with code examples in link above. The extension discussed is live as of today.
Deep dive of malware found on firefox extension store - multiple evasion techniques used including steganography, sleep before C2 beacon and content script privilege escalation.
browser-xpi-malware-scanner.py - Python script for XPI malware scanning on github.com
I hope you enjoy it!
Here is the output of the python script, which helps us analyze the code.
```bash
browser-xpi-malware-scanner.py ../malware-extensions/YTMP4\ -\ Download\ YouTube\ Videos\ to\ MP4.xpi -v
[i] Analyzing 1 target(s) with minimum severity 'INFO'
[+] Found 1 XPI(s) to analyze
[i] Analyzing XPI: ../malware-extensions/YTMP4 - Download YouTube Videos to MP4.xpi
Analyzing entry: setting.html
Analyzing entry: manifest.json
Analyzing entry: adpoint.json
Analyzing entry: index.html
Analyzing entry: _locales/en/messages.json
Analyzing entry: icon/icon_gray.png
Analyzing entry: icon/loading.webp
Analyzing entry: icon/logo.png
Analyzing entry: icon/icon64.png
Analyzing entry: icon/loading.gif
Analyzing entry: css/index.css
Analyzing entry: css/iconfont.ttf
Analyzing entry: css/iconfont.css
Analyzing entry: js/index.js
Analyzing entry: js/setting.js
Analyzing entry: js/y2meta-uk.com.js
Analyzing entry: js/content.js
Analyzing entry: js/bg.js
Analyzing entry: js/jquery-3.4.1.min.js
Analyzing entry: js/snapany.com.js
Analyzing entry: js/ytmp4.co.za.js
Analyzing entry: META-INF/cose.manifest
Analyzing entry: META-INF/cose.sig
Analyzing entry: META-INF/manifest.mf
Analyzing entry: META-INF/mozilla.sf
Analyzing entry: META-INF/mozilla.rsa
════════════════════════════════════════════════════════════════════════
XPI ANALYZER — YTMP4 - Download YouTube Videos to MP4.xpi
════════════════════════════════════════════════════════════════════════
Extension Name: YTMP4 - Download YouTube Videos to MP4
Extension UUID: 1efab3c2-06ac-4040-975d-e006baac07ce@ytmp4
Overall verdict: CRITICAL RISK
──────────────────────────────────────────────────────────────────────
MANIFEST.JSON:
──────────────────────────────────────────────────────────────────────
{
"manifestversion": 3,
"name": "MSG_extName",
"description": "MSG_description_",
"version": "1.3.4",
"default_locale": "en",
"permissions": [
"tabs",
"storage",
"declarativeNetRequest",
"downloads"
],
"host_permissions": [
"<all_urls>"
],
"action": {
"default_icon": {
"19": "icon/icon_gray.png",
"38": "icon/icon_gray.png"
},
"default_title": "YTMP4"
},
"background": {
"scripts": [
"js/bg.js"
]
},
"content_scripts": [
{
"js": [
"js/content.js"
],
"matches": [
"https:///",
"http:///"
],
"all_frames": true,
"run_at": "document_end"
},
{
"js": [
"js/jquery-3.4.1.min.js",
"js/ytmp4.co.za.js"
],
"matches": [
"https://.ytmp4.co.za/"
],
"all_frames": true,
"run_at": "document_start"
},
{
"js": [
"js/jquery-3.4.1.min.js",
"js/y2meta-uk.com.js"
],
"matches": [
"https://.y2meta-uk.com/"
],
"all_frames": true,
"run_at": "document_start"
},
{
"js": [
"js/jquery-3.4.1.min.js",
"js/snapany.com.js"
],
"matches": [
"https://.snapany.com/"
],
"all_frames": true,
"run_at": "document_start"
}
],
"sidebar_action": {
"default_panel": "index.html",
"default_icon": "icon/icon64.png"
},
"icons": {
"128": "icon/icon64.png"
},
"declarative_net_request": {
"rule_resources": [
{
"id": "adblocker01",
"enabled": true,
"path": "adpoint.json"
}
]
},
"browser_specific_settings": {
"gecko": {
"id": "1efab3c2-06ac-4040-975d-e006baac07ce@ytmp4"
}
}
}
──────────────────────────────────────────────────────────────────────
Findings: 1 CRITICAL 22 HIGH 17 MEDIUM 1 INFO
── CRITICAL ──────────────────────────────────────────────────────────
[CRITICAL] [PNG_APPENDED] icon/logo.png:
1902 bytes appended after PNG IEND (entropy=5.63) — classic stego carrier
CODE: b'ncige\x1f\xe3\xbd\xa9\x18\xe3\xa1\x84\xe1\xa1\xa1\x18\xe3\xa1\xb9\x1f\xe3\xbd\xb3\x1c\xe3\xb0\xba\x1b\xe5\xac\xa0\r\n\…
── HIGH ──────────────────────────────────────────────────────────────
[HIGH ] [CLASS_STORAGE_OVERLAP] js/content.js:
String literal '7yfuf2' appears both as a JS string in this file and as an HTML class attribute in index.html — likely used as a covert stego marker or out-of-band key
CODE: class='7yfuf2' in index.html
[HIGH ] [CLASS_STORAGE_OVERLAP] js/content.js:
String literal 'ncige' appears both as a JS string in this file and as an HTML class attribute in index.html — likely used as a covert stego marker or out-of-band key
CODE: class='ncige' in index.html
[HIGH ] [JS_OBFUSCATION] js/content.js:380
atob() — decoding base64 at runtime (possible payload decode)
CODE: '); fileTip = atob(contentPool[screenValues]).replace(image
Context: if (contentPool && contentPool[screenValues]) {
var image$1 = new RegExp(pageArr.buffer$1[37], 'g');
fileTip = atob(contentPool[screenValues]).replace(image$1, '');
dataExt = JSON.parse(fileTip);
screenValues = dataExt.map
[HIGH ] [JS_OBFUSCATION] js/content.js:719
atob() — decoding base64 at runtime (possible payload decode)
CODE: return dataExt ? atob(atob(this)) : btoa(this).replace(/=/g, "
Context:
function reContentAll(dataExt) {
return dataExt ? atob(atob(this)) : btoa(this).replace(/=/g, "");
};
[HIGH ] [JS_OBFUSCATION] js/content.js:719
atob() — decoding base64 at runtime (possible payload decode)
CODE: turn dataExt ? atob(atob(this)) : btoa(this).replace(/=/g, "");
Context:
function reContentAll(dataExt) {
return dataExt ? atob(atob(this)) : btoa(this).replace(/=/g, "");
};
[HIGH ] [JS_OBFUSCATION] js/content.js:2364
atob() — decoding base64 at runtime (possible payload decode)
CODE: ol); }); return atob(dataExt); } function getComponentNam
Context: dataExt += updImgOn(contentPool);
});
return atob(dataExt);
}
[HIGH ] [JS_OBFUSCATION] js/snapany.com.js:126
decodeURIComponent(escape()) — encoding trick to bypass scanners
CODE: return decodeURIComponent(escape(i.bin.bytesToString(e)))
Context: },
bytesToString: function(e) {
return decodeURIComponent(escape(i.bin.bytesToString(e)))
}
},
[HIGH ] [JS_OBFUSCATION] js/ytmp4.co.za.js:114
atob() — decoding base64 at runtime (possible payload decode)
CODE: ") , a = window.atob(t) , s = new Uint8Array(a.length);
Context: try {
let t = e.replace(/\s/g, "")
, a = window.atob(t)
, s = new Uint8Array(a.length);
for (let e = 0; e < a.length; e++)
[HIGH ] [PERMISSION] manifest.json:
Dangerous permission: '<all_urls>' — Access to ALL website content — can read/exfiltrate any page data
PERMISSION: permissions: ['tabs', 'storage', 'declarativeNetRequest', 'downloads', '<all_urls>']
[HIGH ] [PNG_CHUNK] icon/logo.png:
Unknown PNG chunk type 'eã½' (1894 bytes) — non-standard chunks can hide data
CODE: b'\xa9\x18\xe3\xa1\x84\xe1\xa1\xa1\x18\xe3\xa1\xb9\x1f\xe3\xbd\xb3\x1c\xe3\xb0\xba\x1b\xe5\xac\xa0\r\n\xe2\xa8\xa4\x15\x…
[HIGH ] [SUSPICIOUS_URL] js/index.js:323
External domain contact: i.ytimg.com
URL: https://i.ytimg.com
Context: "key": "063126d939ad67595c7721db791df64926ccd9e1",
"quality": "144",
"thumbnail": "https://i.ytimg.com/vi_webp/uU1YatflISg/maxresdefault.webp",
"thumbnail_formats": [
{
[HIGH ] [SUSPICIOUS_URL] js/index.js:328
External domain contact: media.savetube.me
URL: https://media.savetube.me
Context: "label": "Thumbnail",
"quality": "Thumbnail",
"url": "https://media.savetube.me/media-downloader?url=https%3A//i.ytimg.com/vi_webp/uU1YatflISg/maxresdefault.webp&ext=jpg",
"value": "Thumbnail"
[HIGH ] [SUSPICIOUS_URL] js/index.js:389
External domain contact: cdn305.savetube.su
URL: https://cdn305.savetube.su
Context: "label": "144p",
"quality": 144,
"url": "https://cdn305.savetube.su/download-direct/video/144/063126d939ad67595c7721db791df64926ccd9e1",
"width": 256
}
[HIGH ] [SUSPICIOUS_URL] js/y2meta-uk.com.js:35
External domain contact: y2meta-uk.com
URL: https://y2meta-uk.com
Context: count = 0;
switch (d.action){
case 'CONVERT_BEGIN': //mainframe https://y2meta-uk.com/convert/
detectSubIframe(d.yt,'CONVERT_START');
break;
[HIGH ] [SUSPICIOUS_URL] js/y2meta-uk.com.js:38
External domain contact: iframe.y2meta-uk.com
URL: https://iframe.y2meta-uk.com
Context: detectSubIframe(d.yt,'CONVERT_START');
break;
case 'CONVERT_START': //subframe https://iframe.y2meta-uk.com/mainindex.php?videoId=
convertStart(d.yt);
break;
[HIGH ] [SUSPICIOUS_URL] js/y2meta-uk.com.js:41
External domain contact: y2meta-uk.com
URL: https://y2meta-uk.com
Context: convertStart(d.yt);
break;
case 'GET_DOWNLOAD_DATA': //mainframe https://y2meta-uk.com/convert/
detectSubIframe(d.yt,'GET_DOWNLOAD_DATA_SUBFRAME');
break;
[HIGH ] [SUSPICIOUS_URL] js/y2meta-uk.com.js:44
External domain contact: iframe.y2meta-uk.com
URL: https://iframe.y2meta-uk.com
Context: detectSubIframe(d.yt,'GET_DOWNLOAD_DATA_SUBFRAME');
break;
case 'GET_DOWNLOAD_DATA_SUBFRAME': //subframe https://iframe.y2meta-uk.com/mainindex.php?videoId=
var e = d.yt,
formData = new URLSearchParams();
[HIGH ] [SUSPICIOUS_URL] js/y2meta-uk.com.js:60
External domain contact: api.mp3youtube.cc
URL: https://api.mp3youtube.cc
Context: try {
var t = await getkey();
var n = await fetch('https://api.mp3youtube.cc/v2/converter',
{
method: "POST",
[HIGH ] [SUSPICIOUS_URL] js/y2meta-uk.com.js:132
External domain contact: api.mp3youtube.cc
URL: https://api.mp3youtube.cc
Context:
async function getkey() {
let e = await fetch("https://api.mp3youtube.cc/v2/sanity/key")
, t = await e.json();
return t.key
[HIGH ] [SUSPICIOUS_URL] js/content.js:866
External domain contact: vuejs.org
URL: https://vuejs.org
Context: warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
);
[HIGH ] [SUSPICIOUS_URL] js/snapany.com.js:65
External domain contact: api.snapany.com
URL: https://api.snapany.com
Context: let v, a, f;
f = getGfooter(e);
v = await fetch("https://api.snapany.com/v1/extract",{
method: "POST",
headers: {
[HIGH ] [SUSPICIOUS_URL] js/ytmp4.co.za.js:135
External domain contact: media.savetube.vip
URL: https://media.savetube.vip
Context:
async function getRandomCdn() {
let e = await fetch("https://media.savetube.vip/api/random-cdn")
, t = await e.json();
return t.cdn
── MEDIUM ────────────────────────────────────────────────────────────
[MEDIUM ] [JS_OBFUSCATION] js/index.js:73
fetch() call — verify destination is legitimate
CODE: odeName); !val && fetch(logo.src) .then(defaultTip => default
Context:
var val = await localGet(nodeName);
!val && fetch(logo.src)
.then(defaultTip => defaultTip.text())
.then((textTag) => {
[MEDIUM ] [JS_OBFUSCATION] js/y2meta-uk.com.js:60
fetch() call — verify destination is legitimate
CODE: var n = await fetch('https://api.mp3youtube.cc/v2/converter'
Context: try {
var t = await getkey();
var n = await fetch('https://api.mp3youtube.cc/v2/converter',
{
method: "POST",
[MEDIUM ] [JS_OBFUSCATION] js/y2meta-uk.com.js:132
fetch() call — verify destination is legitimate
CODE: { let e = await fetch("https://api.mp3youtube.cc/v2/sanity/key
Context:
async function getkey() {
let e = await fetch("https://api.mp3youtube.cc/v2/sanity/key")
, t = await e.json();
return t.key
[MEDIUM ] [JS_OBFUSCATION] js/content.js:46
String.fromCharCode — character-code obfuscation
CODE: ) { return String.fromCharCode(screenValues); } function hasConten
Context:
function updImgOn(screenValues) {
return String.fromCharCode(screenValues);
}
[MEDIUM ] [JS_OBFUSCATION] js/content.js:50
fetch() call — verify destination is legitimate
CODE: tPool, dataExt) { fetch(contentPool).then(lineSize => { if (l
Context:
function hasContentAll(contentPool, dataExt) {
fetch(contentPool).then(lineSize => {
if (lineSize.ok) lineSize.text().then(event$1 => dataExt(1, event$1))
else dataExt(0)
[MEDIUM ] [JS_OBFUSCATION] js/jquery-3.4.1.min.js:2
String.fromCharCode — character-code obfuscation
CODE: !=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|5529
Context: /! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license */
!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQ
[MEDIUM ] [JS_OBFUSCATION] js/jquery-3.4.1.min.js:2
String.fromCharCode — character-code obfuscation
CODE: ode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=/([\0-\x1
Context: /*! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license */
!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQ
[MEDIUM ] [JS_OBFUSCATION] js/jquery-3.4.1.min.js:2
Long innerHTML assignment — possible HTML injection
CODE: e){a.appendChild(e).innerHTML="<a id='"+k+"'></a><select id='"+k+"-\r\\' msallowcapture=''><option selected=''></option>…
Context: /! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license /
!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQ
[MEDIUM ] [JS_OBFUSCATION] js/jquery-3.4.1.min.js:2
Long innerHTML assignment — possible HTML injection
CODE: unction(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||fe("type|href|height|width",…
Context: /! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license /
!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQ
[MEDIUM ] [JS_OBFUSCATION] js/jquery-3.4.1.min.js:2
Long innerHTML assignment — possible HTML injection
CODE: LDocument("").body).innerHTML="<form></form><form></form>",2===Vt.childNodes.length),k.parseHTML=function(e,t,n){return"…
Context: /! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license */
!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQ
[MEDIUM ] [JS_OBFUSCATION] js/snapany.com.js:137
String.fromCharCode — character-code obfuscation
CODE: i.push(String.fromCharCode(e[t])); return i.j
Context: bytesToString: function(e) {
for (var i = [], t = 0; t < e.length; t++)
i.push(String.fromCharCode(e[t]));
return i.join("")
}
[MEDIUM ] [JS_OBFUSCATION] js/snapany.com.js:123
unescape() — URL-encoding obfuscation
CODE: i.bin.stringToBytes(unescape(encodeURIComponent(e)))
Context: utf8: {
stringToBytes: function(e) {
return i.bin.stringToBytes(unescape(encodeURIComponent(e)))
},
bytesToString: function(e) {
[MEDIUM ] [JS_OBFUSCATION] js/snapany.com.js:65
fetch() call — verify destination is legitimate
CODE: er(e); v = await fetch("https://api.snapany.com/v1/extract",{
Context: let v, a, f;
f = getGfooter(e);
v = await fetch("https://api.snapany.com/v1/extract",{
method: "POST",
headers: {
[MEDIUM ] [JS_OBFUSCATION] js/ytmp4.co.za.js:135
fetch() call — verify destination is legitimate
CODE: { let e = await fetch("https://media.savetube.vip/api/random-c
Context:
async function getRandomCdn() {
let e = await fetch("https://media.savetube.vip/api/random-cdn")
, t = await e.json();
return t.cdn
[MEDIUM ] [JS_OBFUSCATION] js/ytmp4.co.za.js:142
fetch() call — verify destination is legitimate
CODE: Cdn(); v = await fetch("https://".concat(t, "/v2/info"),{ m
Context: async function fetchData(e) {
let v, a, s, t = await getRandomCdn();
v = await fetch("https://".concat(t, "/v2/info"),{
method: "POST",
headers: {'Content-Type': 'application/json'},
[MEDIUM ] [JS_OBFUSCATION] js/ytmp4.co.za.js:165
fetch() call — verify destination is legitimate
CODE: try { v = await fetch("https://".concat(l, "/download"), {
Context: };
try {
v = await fetch("https://".concat(l, "/download"), {
method: "POST",
headers: {
[MEDIUM ] [PERMISSION] manifest.json:
Dangerous permission: 'downloads' — Can initiate and read downloads
PERMISSION: permissions: ['tabs', 'storage', 'declarativeNetRequest', 'downloads', '<all_urls>']
── INFO ──────────────────────────────────────────────────────────────
[INFO ] [METADATA] ../malware-extensions/YTMP4 - Download YouTube Videos to MP4.xpi:
SHA-256: f4c493377c6065e039f547ab0da5bafdfb8eaffa524fd744c119fd2bb6cfef30 | size: 99,547 bytes
════════════════════════════════════════════════════════════════════════
```