Skip to content

Instantly share code, notes, and snippets.

@x4fx77x4f
Last active April 5, 2021 18:55
Show Gist options
  • Save x4fx77x4f/e997c64a57389349726e783b8c351554 to your computer and use it in GitHub Desktop.
Save x4fx77x4f/e997c64a57389349726e783b8c351554 to your computer and use it in GitHub Desktop.
Script to check for problems with getfenv/setfenv
--@shared
local env = {}
env.getfenv = getfenv
local function func(...)
local fenv = getfenv(...)
return fenv
end
setfenv(func, env)
local labels = {
[_G] = "_G",
[env] = "env"
}
print(getfenv(func))
-- 0 should be getfenv's environment
-- 1 should be the caller's environment
-- 2 should be the caller's caller's environment
-- etc...
-- Once you exhaust the stack, you get "bad argument #1 to 'getfenv' (invalid level)"
for i=0, 10 do
local fenv = func(i)
print(CLIENT and "CLIENT" or "SERVER", i, labels[fenv] or fenv and "???" or "nil")
end
assert(func() == env)
--@name getfenv/setfenv pentest
--@shared
local print = print
local pcall = pcall
local getfenv = getfenv
local setfenv = setfenv
local _G = _G
dummy = true
local passed, total = 0, 0
local function test(desc, cond)
total = total+1
passed = passed+(cond and 1 or 0)
print((cond and "pass. " or "FAIL! ")..desc)
end
print("-----starting "..(CLIENT and "client" or "server").."side test-----")
local workedEver = false
local proxyResult
local function proxy(i)
proxyResult = getfenv(i)
end
for i=-2, 10 do
local success, err = pcall(proxy, i)
--print(i, success, err, proxyResult)
test("positive getfenv("..i..") doesn't leak", proxyResult == _G or proxyResult == nil)
--if proxyResult == _G then print(i) end
workedEver = workedEver or proxyResult == _G
proxyResult = nil
end
if not workedEver then
print("warning: getfenv(n) never returned _G. This is not a vuln, but it should be looked into")
end
--print(getfenv(), _G)
test("getfenv() == _G", getfenv() == _G)
local failEnv = setmetatable({}, {
__index = function(self, k)
error("If you are reading this, shit has hit the fan!")
end,
__newindex = function(self, k, v)
error("If you are reading this, shit has hit the fan!")
end
})
local workedEver = false
for i=-2, 10 do
local oldSuccess, oldErr = pcall(getfenv, i)
local success, err = pcall(setfenv, i, failEnv)
--print(i, success, err)
if err ~= _G then
test("out of bounds setfenv("..i..") fails", not success)
else
workedEver = true
test("in bounds setfenv("..i..") works", success)
test("in bounds setfenv("..i..") doesn't leak", err == _G or err == nil)
end
if oldSuccess then
pcall(setfenv, i, oldErr)
end
end
if not workedEver then
print("warning: setfenv(n) never succeeded. This is not a vuln, but it should be looked into")
end
local dofile = dofile
local success, err = pcall(setfenv, dofile, failEnv)
test("setfenv(out of bounds function) fails", not success)
local function func() print(dummy) end
local success, err = pcall(setfenv, func, failEnv)
test("setfenv(in bounds function) works", success)
test("setfenv(in bounds function) doesn't leak", err == func)
-- I've heard from somewhere that if a metamethod is called from outside
-- of the sandbox, there is extra potential for exploitation.
local tested = false
local magic = setmetatable({}, {
__add = function(a, b)
if not tested then
tested = true
for i=-10, 10 do
local success, env = pcall(getfenv, i)
test("metamethod getfenv("..i..") doesn't leak", env == _G or not env or not success)
end
end
return 0
end
})
local a, b = Color(), Color()
a[1] = magic
b[1] = magic
local c = a+b
assert(tested, "failed to test metamethod")
print("-----end of "..(CLIENT and "client" or "server").."side test-----")
print((passed == total and "passed all tests. " or "FAILED SOME TESTS! ").."passed "..passed.." out of "..total)
--@name getfenv suicide
--@shared
-- This is a test to make sure modifying whitelistedEnvs externally works.
-- I can't think of any good reason WHY an external script should be
-- able to modify whitelistedEnvs, but extra options probably won't
-- hurt anything -- as long as no one does something stupid like:
-- Player(2):GetEyeTrace().Entity.instance.whitelistedEnvs[_G] = true
timer.create('', 1, 0, function()
local env = getfenv(0) -- the environment of getfenv itself, according to lua docs
if env == nil then
print(SERVER and "SERVER" or "CLIENT", "no changes detected...")
else
print(SERVER and "SERVER" or "CLIENT", "CHANGE SUCCESS! WHY WOULD YOU EVER DO THIS?")
timer.remove('')
end
end)
--@name setfenv black magic
--@server
local code = [[
local location = 1
local env = getfenv(location)
setfenv(location, {
PASS = true,
FAIL = true
})
return PASS, env
]]
local func, err = (CompileString or loadstring)(code)
assert(type(func) == 'function', err or func)
local _G = _G
local print = print
local pass, env = func()
local fail = FAIL
if pass and not fail then
print("pass")
elseif not pass and fail then
print("FAIL: setfenv set its caller")
elseif pass and fail then
print("FAIL: setfenv set both itself and its caller???")
elseif not pass and not fail then
print("FAIL: setfenv did absolutely nothing")
end
print(env == _G and "env == _G" or "env ~= _G")
--@name setfenv RAM eater
--@client
local math_random = math.random
local pcall = pcall
local quotaUsed = quotaUsed
local string_char = string.char
local table_concat = table.concat
local function dummy() end
local threshold = quotaMax()*0.5
hook.add('think', '', function()
local str = {}
local i = 1
while quotaUsed() < threshold do
str[i] = string_char(math_random(0, 255))
i = i+1
end
str = table_concat(str, '')
pcall(setfenv, dummy, str)
end)
--@shared
local prefix = SERVER and "SERVER" or "CLIENT"
local env = {
FAIL = true,
print = print
}
local lazy = setmetatable({
env
}, {
__mode = 'v'
})
local function guinea()
print(prefix, FAIL and "FAILED TWICE", "didn't fail twice wtf?")
end
setfenv(guinea, env)
env = nil
--guinea = nil
hook.add('think', '', function()
if #lazy ~= 1 then
print(prefix, "FAILED ONCE")
guinea()
hook.remove('think', '')
end
end)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment