1
0
mirror of https://github.com/golang/go synced 2024-11-25 21:37:58 -07:00

os/user: support calling Current on impersonated threads

The syscall.OpenCurrentProcessToken call in user.Current fails
when called from an impersonated thread, as the process token is
normally in that case.

This change ensures that the current thread is not impersonated
when calling OpenCurrentProcessToken, and then restores the
impersonation state, if any.

Fixes #68647

Change-Id: I3197535dd8355d21029a42f7aa3936d8fb021202
Reviewed-on: https://go-review.googlesource.com/c/go/+/602415
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
Reviewed-by: Alex Brainman <alex.brainman@gmail.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
qmuntal 2024-08-01 16:26:00 +02:00 committed by Quim Muntal
parent db0b6a85c2
commit fe392d0dff
5 changed files with 290 additions and 27 deletions

View File

@ -18,6 +18,8 @@ const (
//sys ImpersonateSelf(impersonationlevel uint32) (err error) = advapi32.ImpersonateSelf
//sys RevertToSelf() (err error) = advapi32.RevertToSelf
//sys ImpersonateLoggedOnUser(token syscall.Token) (err error) = advapi32.ImpersonateLoggedOnUser
//sys LogonUser(username *uint16, domain *uint16, password *uint16, logonType uint32, logonProvider uint32, token *syscall.Token) (err error) = advapi32.LogonUserW
const (
TOKEN_ADJUST_PRIVILEGES = 0x0020
@ -93,6 +95,26 @@ type LocalGroupUserInfo0 struct {
Name *uint16
}
const (
NERR_UserNotFound syscall.Errno = 2221
NERR_UserExists syscall.Errno = 2224
)
const (
USER_PRIV_USER = 1
)
type UserInfo1 struct {
Name *uint16
Password *uint16
PasswordAge uint32
Priv uint32
HomeDir *uint16
Comment *uint16
Flags uint32
ScriptPath *uint16
}
type UserInfo4 struct {
Name *uint16
Password *uint16
@ -125,6 +147,8 @@ type UserInfo4 struct {
PasswordExpired uint32
}
//sys NetUserAdd(serverName *uint16, level uint32, buf *byte, parmErr *uint32) (neterr error) = netapi32.NetUserAdd
//sys NetUserDel(serverName *uint16, userName *uint16) (neterr error) = netapi32.NetUserDel
//sys NetUserGetLocalGroups(serverName *uint16, userName *uint16, level uint32, flags uint32, buf **byte, prefMaxLen uint32, entriesRead *uint32, totalEntries *uint32) (neterr error) = netapi32.NetUserGetLocalGroups
// GetSystemDirectory retrieves the path to current location of the system

View File

@ -39,6 +39,7 @@ const (
ERROR_CALL_NOT_IMPLEMENTED syscall.Errno = 120
ERROR_INVALID_NAME syscall.Errno = 123
ERROR_LOCK_FAILED syscall.Errno = 167
ERROR_NO_TOKEN syscall.Errno = 1008
ERROR_NO_UNICODE_TRANSLATION syscall.Errno = 1113
)

View File

@ -49,7 +49,9 @@ var (
procAdjustTokenPrivileges = modadvapi32.NewProc("AdjustTokenPrivileges")
procDuplicateTokenEx = modadvapi32.NewProc("DuplicateTokenEx")
procImpersonateLoggedOnUser = modadvapi32.NewProc("ImpersonateLoggedOnUser")
procImpersonateSelf = modadvapi32.NewProc("ImpersonateSelf")
procLogonUserW = modadvapi32.NewProc("LogonUserW")
procLookupPrivilegeValueW = modadvapi32.NewProc("LookupPrivilegeValueW")
procOpenSCManagerW = modadvapi32.NewProc("OpenSCManagerW")
procOpenServiceW = modadvapi32.NewProc("OpenServiceW")
@ -82,6 +84,8 @@ var (
procVirtualQuery = modkernel32.NewProc("VirtualQuery")
procNetShareAdd = modnetapi32.NewProc("NetShareAdd")
procNetShareDel = modnetapi32.NewProc("NetShareDel")
procNetUserAdd = modnetapi32.NewProc("NetUserAdd")
procNetUserDel = modnetapi32.NewProc("NetUserDel")
procNetUserGetLocalGroups = modnetapi32.NewProc("NetUserGetLocalGroups")
procRtlGetVersion = modntdll.NewProc("RtlGetVersion")
procGetProcessMemoryInfo = modpsapi.NewProc("GetProcessMemoryInfo")
@ -113,6 +117,14 @@ func DuplicateTokenEx(hExistingToken syscall.Token, dwDesiredAccess uint32, lpTo
return
}
func ImpersonateLoggedOnUser(token syscall.Token) (err error) {
r1, _, e1 := syscall.Syscall(procImpersonateLoggedOnUser.Addr(), 1, uintptr(token), 0, 0)
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func ImpersonateSelf(impersonationlevel uint32) (err error) {
r1, _, e1 := syscall.Syscall(procImpersonateSelf.Addr(), 1, uintptr(impersonationlevel), 0, 0)
if r1 == 0 {
@ -121,6 +133,14 @@ func ImpersonateSelf(impersonationlevel uint32) (err error) {
return
}
func LogonUser(username *uint16, domain *uint16, password *uint16, logonType uint32, logonProvider uint32, token *syscall.Token) (err error) {
r1, _, e1 := syscall.Syscall6(procLogonUserW.Addr(), 6, uintptr(unsafe.Pointer(username)), uintptr(unsafe.Pointer(domain)), uintptr(unsafe.Pointer(password)), uintptr(logonType), uintptr(logonProvider), uintptr(unsafe.Pointer(token)))
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func LookupPrivilegeValue(systemname *uint16, name *uint16, luid *LUID) (err error) {
r1, _, e1 := syscall.Syscall(procLookupPrivilegeValueW.Addr(), 3, uintptr(unsafe.Pointer(systemname)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(luid)))
if r1 == 0 {
@ -385,6 +405,22 @@ func NetShareDel(serverName *uint16, netName *uint16, reserved uint32) (neterr e
return
}
func NetUserAdd(serverName *uint16, level uint32, buf *byte, parmErr *uint32) (neterr error) {
r0, _, _ := syscall.Syscall6(procNetUserAdd.Addr(), 4, uintptr(unsafe.Pointer(serverName)), uintptr(level), uintptr(unsafe.Pointer(buf)), uintptr(unsafe.Pointer(parmErr)), 0, 0)
if r0 != 0 {
neterr = syscall.Errno(r0)
}
return
}
func NetUserDel(serverName *uint16, userName *uint16) (neterr error) {
r0, _, _ := syscall.Syscall(procNetUserDel.Addr(), 2, uintptr(unsafe.Pointer(serverName)), uintptr(unsafe.Pointer(userName)), 0)
if r0 != 0 {
neterr = syscall.Errno(r0)
}
return
}
func NetUserGetLocalGroups(serverName *uint16, userName *uint16, level uint32, flags uint32, buf **byte, prefMaxLen uint32, entriesRead *uint32, totalEntries *uint32) (neterr error) {
r0, _, _ := syscall.Syscall9(procNetUserGetLocalGroups.Addr(), 8, uintptr(unsafe.Pointer(serverName)), uintptr(unsafe.Pointer(userName)), uintptr(level), uintptr(flags), uintptr(unsafe.Pointer(buf)), uintptr(prefMaxLen), uintptr(unsafe.Pointer(entriesRead)), uintptr(unsafe.Pointer(totalEntries)), 0)
if r0 != 0 {

View File

@ -5,9 +5,11 @@
package user
import (
"errors"
"fmt"
"internal/syscall/windows"
"internal/syscall/windows/registry"
"runtime"
"syscall"
"unsafe"
)
@ -200,36 +202,91 @@ var (
)
func current() (*User, error) {
t, e := syscall.OpenCurrentProcessToken()
if e != nil {
return nil, e
// Use runAsProcessOwner to ensure that we can access the process token
// when calling syscall.OpenCurrentProcessToken if the current thread
// is impersonating a different user. See https://go.dev/issue/68647.
var usr *User
err := runAsProcessOwner(func() error {
t, e := syscall.OpenCurrentProcessToken()
if e != nil {
return e
}
defer t.Close()
u, e := t.GetTokenUser()
if e != nil {
return e
}
pg, e := t.GetTokenPrimaryGroup()
if e != nil {
return e
}
uid, e := u.User.Sid.String()
if e != nil {
return e
}
gid, e := pg.PrimaryGroup.String()
if e != nil {
return e
}
dir, e := t.GetUserProfileDirectory()
if e != nil {
return e
}
username, domain, e := lookupUsernameAndDomain(u.User.Sid)
if e != nil {
return e
}
usr, e = newUser(uid, gid, dir, username, domain)
return e
})
return usr, err
}
// runAsProcessOwner runs f in the context of the current process owner,
// that is, removing any impersonation that may be in effect before calling f,
// and restoring the impersonation afterwards.
func runAsProcessOwner(f func() error) error {
var impersonationRollbackErr error
runtime.LockOSThread()
defer func() {
// If impersonation failed, the thread is running with the wrong token,
// so it's better to terminate it.
// This is achieved by not calling runtime.UnlockOSThread.
if impersonationRollbackErr != nil {
println("os/user: failed to revert to previous token:", impersonationRollbackErr.Error())
runtime.Goexit()
} else {
runtime.UnlockOSThread()
}
}()
prevToken, isProcessToken, err := getCurrentToken()
if err != nil {
return fmt.Errorf("os/user: failed to get current token: %w", err)
}
defer t.Close()
u, e := t.GetTokenUser()
if e != nil {
return nil, e
defer prevToken.Close()
if !isProcessToken {
if err = windows.RevertToSelf(); err != nil {
return fmt.Errorf("os/user: failed to revert to self: %w", err)
}
defer func() {
impersonationRollbackErr = windows.ImpersonateLoggedOnUser(prevToken)
}()
}
pg, e := t.GetTokenPrimaryGroup()
if e != nil {
return nil, e
return f()
}
// getCurrentToken returns the current thread token, or
// the process token if the thread doesn't have a token.
func getCurrentToken() (t syscall.Token, isProcessToken bool, err error) {
thread, _ := windows.GetCurrentThread()
// Need TOKEN_DUPLICATE and TOKEN_IMPERSONATE to use the token in ImpersonateLoggedOnUser.
err = windows.OpenThreadToken(thread, syscall.TOKEN_QUERY|syscall.TOKEN_DUPLICATE|syscall.TOKEN_IMPERSONATE, true, &t)
if errors.Is(err, windows.ERROR_NO_TOKEN) {
// Not impersonating, use the process token.
isProcessToken = true
t, err = syscall.OpenCurrentProcessToken()
}
uid, e := u.User.Sid.String()
if e != nil {
return nil, e
}
gid, e := pg.PrimaryGroup.String()
if e != nil {
return nil, e
}
dir, e := t.GetUserProfileDirectory()
if e != nil {
return nil, e
}
username, domain, e := lookupUsernameAndDomain(u.User.Sid)
if e != nil {
return nil, e
}
return newUser(uid, gid, dir, username, domain)
return t, isProcessToken, err
}
// lookupUserPrimaryGroup obtains the primary group SID for a user using this method:

View File

@ -0,0 +1,145 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package user
import (
"crypto/rand"
"encoding/base64"
"errors"
"internal/syscall/windows"
"runtime"
"strconv"
"syscall"
"testing"
"unsafe"
)
// windowsTestAcount creates a test user and returns a token for that user.
// If the user already exists, it will be deleted and recreated.
// The caller is responsible for closing the token.
func windowsTestAcount(t *testing.T) syscall.Token {
var password [33]byte
rand.Read(password[:])
// Add special chars to ensure it satisfies password requirements.
pwd := base64.StdEncoding.EncodeToString(password[:]) + "_-As@!%*(1)4#2"
name, err := syscall.UTF16PtrFromString("GoStdTestUser01")
if err != nil {
t.Fatal(err)
}
pwd16, err := syscall.UTF16PtrFromString(pwd)
if err != nil {
t.Fatal(err)
}
userInfo := windows.UserInfo1{
Name: name,
Password: pwd16,
Priv: windows.USER_PRIV_USER,
}
// Create user.
err = windows.NetUserAdd(nil, 1, (*byte)(unsafe.Pointer(&userInfo)), nil)
if errors.Is(err, syscall.ERROR_ACCESS_DENIED) {
t.Skip("skipping test; don't have permission to create user")
}
if errors.Is(err, windows.NERR_UserExists) {
// User already exists, delete and recreate.
if err = windows.NetUserDel(nil, name); err != nil {
t.Fatal(err)
}
if err = windows.NetUserAdd(nil, 1, (*byte)(unsafe.Pointer(&userInfo)), nil); err != nil {
t.Fatal(err)
}
} else if err != nil {
t.Fatal(err)
}
domain, err := syscall.UTF16PtrFromString(".")
if err != nil {
t.Fatal(err)
}
const LOGON32_PROVIDER_DEFAULT = 0
const LOGON32_LOGON_INTERACTIVE = 2
var token syscall.Token
if err = windows.LogonUser(name, domain, pwd16, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &token); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
token.Close()
if err = windows.NetUserDel(nil, name); err != nil {
if !errors.Is(err, windows.NERR_UserNotFound) {
t.Fatal(err)
}
}
})
return token
}
func TestImpersonatedSelf(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
want, err := current()
if err != nil {
t.Fatal(err)
}
levels := []uint32{
windows.SecurityAnonymous,
windows.SecurityIdentification,
windows.SecurityImpersonation,
windows.SecurityDelegation,
}
for _, level := range levels {
t.Run(strconv.Itoa(int(level)), func(t *testing.T) {
if err = windows.ImpersonateSelf(level); err != nil {
t.Fatal(err)
}
defer windows.RevertToSelf()
got, err := current()
if level == windows.SecurityAnonymous {
// We can't get the process token when using an anonymous token,
// so we expect an error here.
if err == nil {
t.Fatal("expected error")
}
return
}
if err != nil {
t.Fatal(err)
}
compare(t, want, got)
})
}
}
func TestImpersonated(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
want, err := current()
if err != nil {
t.Fatal(err)
}
// Create a test user and log in as that user.
token := windowsTestAcount(t)
// Impersonate the test user.
if err = windows.ImpersonateLoggedOnUser(token); err != nil {
t.Fatal(err)
}
defer func() {
err = windows.RevertToSelf()
if err != nil {
// If we can't revert to self, we can't continue testing.
panic(err)
}
}()
got, err := current()
if err != nil {
t.Fatal(err)
}
compare(t, want, got)
}