Skip to content

fix: clientside RCE — network part data could retarget command parts to other players (PlayerOwner injection) - #1477

Merged
Techbot121 merged 1 commit into
CapsAdmin:masterfrom
James159758:fix/part-settable-whitelist
Oct 11, 2026
Merged

Techbot121 merged 1 commit into
CapsAdmin:masterfrom
James159758:fix/part-settable-whitelist

Conversation

@James159758

@James159758 James159758 commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Severity: Critical — clientside RCE

PART:SetTable applied any existing Set<key> method from part tables received over the network, including internal ones. Because outfits are rebroadcast by the server to every client, a regular player could set the internal field PlayerOwner on parts worn by other players, retargeting the part's owner to the victim. Since command parts execute their owner's console commands/Lua when GetPlayerOwner() == LocalPlayer, the attacker's code executes on the victim's client.

Impact

  • Arbitrary console commands on the victim's client (works on default server settings, no sv_allowcslua needed): chat as the victim, screenshots, changing client convars, triggering any non-blocked console command.
  • Arbitrary Lua on the victim's client (requires sv_allowcslua 1, common on build/sandbox servers): file.* writes into the victim's DATA folder, outbound HTTP from the victim (IP/SteamID exfiltration), installing clientside hooks (input control via CreateMove, chat interception), freezing/crashing the client.
  • No admin rights, no consent mechanics involved; the victim only needs to receive the outfit (clients request outfits automatically after spawn).
  • The same field injection also lets the attacker hide parts from the victim's PAC editor (ShowInEditor) and attach outfits to other players.

Minimal PoC (conceptual)

The whole attack is one pac_submit whose part table contains an internal field:

local outfit = {
    part = {
        self = {
            ClassName     = "command",
            UniqueID      = "x",
            OwnerName     = "self",
            String        = "say this runs on the victim's client",
            ExecuteOnWear = true,
            PlayerOwner   = victim_player_entity, -- <-- internal field, applied by SetTable
        },
        children = {},
    },
}
-- serialized with pac3's StringStream and sent as net message "pac_submit".

On the victim's client PART:SetTable (base_part.lua) applies SetPlayerOwner(victim) from the network table. WearPartFromServer then calls part:CallRecursive("OnWorn") → command:OnWorn → Execute(), which checks GetPlayerOwner() == pac.LocalPlayer — now true for the victim — and runs ent:ConCommand(self.String) on their client (parts/command.lua). With UseLua = true and sv_allowcslua 1, the same field placement leads to CompileString/execution of arbitrary Lua on the victim.

A full weaponized PoC (including the wire serializer, persistence via outfit resync, editor invisibility, and a chat-controlled C2 payload) is available privately through GitHub security advisory on request.

Root cause

SetTable (base_part.lua) applies every existing Set<key> setter found in the network table — there is no restriction to declared (storable) properties, so internal setters such as SetPlayerOwner and SetShowInEditor are reachable from client data that the server rebroadcasts.

Fix

  • PART:SetTable now only applies keys declared in StorableVars. This is backward compatible: PART:ToTable() only serializes StorableVars, so all existing saved/worn outfits are unaffected. Internal setters are no longer reachable from network data.
  • Defense in depth: the server now strips internal keys (PlayerOwner, PlayerOwnerHash, Owner, ShowInEditor) from part.self in pace.HandleReceivedData before rebroadcasting, which also protects clients running older versions of the addon.

Verification

  • Existing outfits load, save and wear identically (only StorableVars keys are ever serialized).
  • Internal fields sent inside part.self over the network are ignored clientside and removed serverside.
  • Command parts still execute their owner's Lua/commands as before for the local player (sv_allowcslua permitting), but can no longer be retargeted to another player.

SetTable applied every existing Set<key> setter from network-provided
part tables, including internal ones (SetPlayerOwner, SetShowInEditor,
SetOwner). Part data crosses the server to other clients, so any client
could set internal state on parts worn by other players.

ToTable() only serializes StorableVars, so restricting SetTable to
StorableVars keys is fully backward compatible with existing outfits.

Also strip internal keys from part.self serverside before rebroadcast,
protecting clients that have not updated yet.
@James159758 James159758 changed the title fix: prevent network-provided part data from setting internal part state fix: clientside RCE — network part data could retarget command parts to other players (PlayerOwner injection) Oct 11, 2026
@pingu7867

Copy link
Copy Markdown
Collaborator

I'd also be inclined to apply punishment (a pac ban) or at least log it, since with your checks, these wrongful table keys are expected to only show up in a manually-injected data payload. As far as I know.

@Techbot121
Techbot121 merged commit 2f21b19 into CapsAdmin:master Oct 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants