Golang in Windows: Execute command as another user

In Windows, there are some (admittedly limited) use cases where you could be interested in executing a command as another user. I came across one such use case during the development of Tutela, but this could be extended to generic uses. Under Windows, most services would need to run using SYSTEM credentials. This is true for the Tutela agent was well, which has a service component that runs using SYSTEM privileges. However, some operations require being run within the context of the user that is currently logged into the computer. Anything to do with the windows manager (explorer.exe) falls under this category.

So how can a golang program, running under SYSTEM privileges, execute a command under USER privileges?

Tokens

Each process runs using a set of “tokens”. These tokens describe the security context of a running process. Searching across the golang windows codebase, there’s an interesting system call:

https://github.com/golang/sys/blob/4f61da869c0cac4042c389774ce3a1627c48b555/windows/security_windows.go#L628

//sys OpenProcessToken(process Handle, access uint32, token *Token) (err error) = advapi32.OpenProcessToken

The documentation for the OpenProcessToken says:

The OpenProcessToken function opens the access token associated with a process.

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocesstoken

The high level idea

  1. Find a process which is running with user tokens
  2. Call OpenProcessToken to capture those tokens
  3. Somehow use those tokens to run a command

Step 1 is easy, as mentioned before you can typically find “explorer.exe” running with user tokens

Step 2 is easy too if you are familiar with syscalls:

  • Use “OpenProcess” to get a handle to the process ID
  • Use “OpenProcessToken” to get the token

For example:

func getToken(pid int) (syscall.Token, error) {
	var err error
	var token syscall.Token

	handle, err := syscall.OpenProcess(syscall.PROCESS_QUERY_INFORMATION, false, uint32(pid))
	if err != nil {
		Logger.Warnw("Token Process", "err", err)
	}
	defer syscall.CloseHandle(handle)

	// Find process token via win32
	err = syscall.OpenProcessToken(handle, syscall.TOKEN_ALL_ACCESS, &token)

	if err != nil {
		Log.Warn("Open Token Process", "err", err)
	}
	return token, err
}

The hidden gem

Now that we have the user tokens, we’re left trying to figure out how to use them in step 3. It turns out that Golang’s own exec package has a semi-hidden parameter:

https://pkg.go.dev/os/exec#:~:text=//%20SysProcAttr%20holds%20optional%2C%20operating%20system%2Dspecific%20attributes.%0A%09//%20Run%20passes%20it%20to%20os.StartProcess%20as%20the%20os.ProcAttr%27s%20Sys%20field.%0A%09SysProcAttr%20*syscall.SysProcAttr

Tapping SysProcAttr shows some interesting parameters, but not exactly what we are after. However, opening this in an IDE does show something interesting:

The description says it all… it’s exactly what we’re after.

Bringing this together into code:

func runAsUser(cmdPath string) (string, string) {

	token, err := GetUserToken() //function we defined above
	if err != nil {
		Log.Warn("Get Token", "err", err)
	}

	defer token.Close()

	cmd := exec.Command(cmdPath)

	var stdout bytes.Buffer
	var stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

    // this is the important bit!
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Token: token,
	}

	err = cmd.Run()
	return stdout.String(), stderr.String()

}

TL;DR

The golang exec package allows you to pass through windows security tokens to a command struct via the SysProcAttr field, which in windows allows for storing tokens.