Spawning in items using map pings in Don't Starve Together
Don't Starve Together#
Don't Starve Together (DST) is small survival game I've played for quite some time now. It's something I'd pick up every year or two and play with a few friends or on a public multiplayer server. According to my Steam stats I've poured over 1000 hours into this game, which is honestly more than I had expected.
DST modding#
Something I've not messed around with too much is the modding the game has. It's all done in Lua (yuck!), but you can make some pretty cool stuff with it. A mod I really liked is Epic Healthbar. It shows the health of the bosses and mobs you're fighting, but in a really nice way with more information such as the phase of the boss.
Unfortunately, this is a mod which requires both the server and the client to install it (since by default health is not sent to clients) and the servers I was playing on didn't have it installed. They had Show Me (Origin) instead. This is a pretty simple mod which would just show values of a mob when you hovered on them, like their health. I wanted to combine these two, since it's pretty annoying to hover the mob constantly and I wanted to have the convenience of Epic Healthbar.
After reading the source code of Show Me, I've figured out that the mods (and the game itself) have a nice RPC system to communicate between the client and the server. The devs made a nice forum post on how it works, but something that stuck out to me was:
In your mod, you should make sure to add appropriate checks so that a client cannot abuse your RPC, whether this is checking the prefab via player.prefab to limit it to a specific character, or check if they are a server admin with player.Network:IsServerAdmin(), purely depending on what its supposed to do.
Anyway, I decided to continue working on my mod, which would be hacking Epic Healthbar to work clientside and would read the RPC messages that Show Me sends to the client. This ended up being a little fun challenge, but after reading more examples on how DST modding works I was able to make something that I was decently happy with. You can find the source code of it here. Also, I have to give credit to Tykvesh, the creator of Epic Healthbar for helping me with the hacky loading this mod does.
Global Positions#
One really popular mod I personally use and most servers use is Global Positions. According to Steam it currently has over 7.5M subscribers, which is quite a lot for DST mods. This mod lets you share your map with other players and adds a nice pinging system so you can show your friends where stuff is on the map (similar to how you can add waypoints in shooter games). Looking at the source code, the way the mod handles pings looks like this:
ReceivePing = function(player, pingtype, x, y, z)
if pingtype == "delete" then
--Find the nearest ping and delete it (if it was actually somewhat close)
mindistsq, minping = math.huge, nil
for _,ping in pairs(pings) do
local px, py, pz = ping.Transform:GetWorldPosition()
dq = GLOBAL.distsq(x, z, px, pz)
if dq < mindistsq then
mindistsq = dq
minping = ping
end
end
-- Check that their mouse is actually somewhat close to it first, ~20
if mindistsq < 400 then
pings[minping.GUID] = nil
minping:Remove()
end
elseif pingtype == "clear" then
for _,ping in pairs(pings) do
ping:Remove()
end
else
local ping = GLOBAL.SpawnPrefab("ping_"..pingtype)
ping.OnRemoveEntity = function(inst) pings[inst.GUID] = nil end
ping.parentuserid = player.userid
ping.Transform:SetPosition(x,y,z)
pings[ping.GUID] = ping
end
end
AddModRPCHandler(modname, "Ping", ReceivePing)
This seemed pretty fine, but it actually wasn't. Remembering the blog post the devs said this data needed to be validated, so let's try sending a malformed pingtype:
SendModRPCToServer(
MOD_RPC["workshop-378160973"]["Ping"],
"hi",
ThePlayer.Transform:GetWorldPosition()
)
...and that just crashed the server.
GLOBAL.SpawnPrefab("ping_"..pingtype) attempts to spawn a ping_hi, which does not exist and returns nil,
which then crashes the server.
Digging deeper into the source#
Looking more at this function everything else seems fine,
but it would turn out later that the SpawnPrefab function it calls has some unexpected side effects.
It attempts to spawn an entity which starts with ping_, so it doesn't seem like you can spawn anything else.
I guess if some other mod (or the game itself) created a custom entity which also begins with ping_,
you'd be able to spawn it, but that's not too serious.
I decided to dig deeper, so I wanted to see how SpawnPrefab is implemented internally.
Luckily, I didn't have to search too deep, as the game is not obfuscated and you can easily extract the lua scripts.
You can even find a public mirror of the game scripts here.
Here's how SpawnPrefab is implemented:
function SpawnPrefab(name, skin, skin_id, creator)
name = string.sub(name, string.find(name, "[^/]*$"))
name = renames[name] or name
if skin and not IsItemId(skin) then
print("Unknown skin", skin)
skin = nil
end
local guid = TheSim:SpawnPrefab(name, skin, skin_id, creator)
return Ents[guid]
end
Now the moment I saw this I was very excited, since by just looking at the first line you can already see that
something funky is happening here. The first line runs a regex on the prefab we want to spawn and takes everything after the /.
So, what happens when we send something like /deerclops as the pingtype?
SendModRPCToServer(
MOD_RPC["workshop-378160973"]["Ping"],
"/deerclops",
ThePlayer.Transform:GetWorldPosition()
)
First, the string gets concatinated so it attempts spawning a ping_/deerclops, which is then passed to SpawnPrefab.
SpawnPrefab then removes everything before the slash, meaning that instead of it spawning the ping,
it actually spawns deerclops, which is the ID of one of the bosses in the game.
This works on every prefab/entity ID in the game, meaning we can basically spawn in anything on the map
by just sending one RPC message, from items, mobs and even bosses!
It turns out this exploit has been a thing for over 7 years (!!) and until now from what I can tell nobody noticed.
The game isn't known for having the best documentation so this is quite an easy thing to miss.
The patch#
This mod is used on quite a lot of servers, so I wanted to make sure it was properly patched. I contacted the author, created a PR and an issue which eventually ended up getting merged. Huge credits to rezecib for maintaining this mod for almost 10 years now and for patching it.
I ended up exploring other mods to see if they had any similar issues and it turned out they did! Show Me also had a similar bug where you could crash the server. While I was waiting for everything to get merged, I wrote a little mod which would inject and validate the RPC messages and sent it to a few of my friends who ran servers so that they could deploy it. You can find the source code of it here.