diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4d82cbe --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 FourCore + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..407ef09 --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +# wintoken +Windows Token Manipulation GOlang Library + +Wintoken abstracts away windows token manipulation functions with functions you are more likely to use. The library exposes easy-to-use functions to steal tokens, enable/disable privileges, and grab interactive and linked tokens. + +## Install + +- Go + - Requires Go to be installed on system. Tested on Go1.16+. + - `go get github.com/fourcorelabs/wintoken` + +## Usage +- To steal a token from a process, you can use OpenProcessToken and supply the PID and the type of token that you want + +```go +package main + +import ( + "os/exec" + "syscall" + + "github.com/fourcorelabs/wintoken" +) + +func main() { + token, err := wintoken.OpenProcessToken(1234, wintoken.TokenPrimary) //pass 0 for own process + if err != nil { + panic(err) + } + defer token.Close() + + //Now you can use the token anywhere you would like + cmd := exec.Command("/path/to/binary") + cmd.SysProcAttr = &syscall.SysProcAttr{Token: syscall.Token(token.Token())} +} +``` + +- If you want the elevated interactive token for the currently logged in user, you can call GetInteractiveToken with TokenLinked as parameter + +```go +package main + +import ( + "os/exec" + "syscall" + + "github.com/fourcorelabs/wintoken" +) + +func main() { + //You can get an interactive token(if you are running as a service) + //and specify that you want the linked token(elevated) in the same line + token, err := wintoken.GetInteractiveToken(wintoken.TokenLinked) + if err != nil { + panic(err) + } + defer token.Close() + + //Now you can use the token anywhere you would like + cmd := exec.Command("/path/to/binary") + cmd.SysProcAttr = &syscall.SysProcAttr{Token: syscall.Token(token.Token())} +} +``` + +- Once you have a token, you can query information from this token such as its privileges, integrity levels, associated user details, etc. + +```go +package main + +import ( + "fmt" + + "github.com/fourcorelabs/wintoken" +) + +func main() { + token, err := wintoken.OpenProcessToken(1234, wintoken.TokenPrimary) + if err != nil { + panic(err) + } + defer token.Close() + + fmt.Println(token.GetPrivileges()) + fmt.Println(token.GetIntegrityLevel()) + fmt.Println(token.UserDetails()) +} +``` + +- You can Enable, Disable, and Remove privileges in a simple manner + +```go +package main + +import( + "github.com/fourcorelabs/wintoken" +) + +func main(){ + token, err := wintoken.OpenProcessToken(1234, wintoken.TokenPrimary) + if err != nil { + panic(err) + } + //Enable, Disable, or Remove privileges in one line + token.EnableAllPrivileges() + token.DisableTokenPrivileges([]string{"SeShutdownPrivilege", "SeTimeZonePrivilege"}) + token.RemoveTokenPrivilege("SeUndockPrivilege") +} +``` diff --git a/gettoken.go b/gettoken.go new file mode 100644 index 0000000..451ced9 --- /dev/null +++ b/gettoken.go @@ -0,0 +1,135 @@ +package wintoken + +import ( + "fmt" + "unsafe" + + "golang.org/x/sys/windows" +) + +const ( + WTS_CURRENT_SERVER_HANDLE windows.Handle = 0 +) + +//Open Process Token using PID, pass 0 as PID for self token +func OpenProcessToken(pid int, tokenType tokenType) (*Token, error) { + var ( + t windows.Token + duplicatedToken windows.Token + procHandle windows.Handle + err error + ) + + if pid == 0 { + procHandle = windows.CurrentProcess() + } else { + procHandle, err = windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, false, uint32(pid)) + } + if err != nil { + return nil, err + } + + if err = windows.OpenProcessToken(procHandle, windows.TOKEN_ALL_ACCESS, &t); err != nil { + return nil, err + } + + defer windows.CloseHandle(windows.Handle(t)) + + switch tokenType { + case TokenPrimary: + if err := windows.DuplicateTokenEx(t, windows.MAXIMUM_ALLOWED, nil, windows.SecurityDelegation, windows.TokenPrimary, &duplicatedToken); err != nil { + return nil, fmt.Errorf("error while DuplicateTokenEx: %w", err) + } + case TokenImpersonation: + if err := windows.DuplicateTokenEx(t, windows.MAXIMUM_ALLOWED, nil, windows.SecurityImpersonation, windows.TokenImpersonation, &duplicatedToken); err != nil { + return nil, fmt.Errorf("error while DuplicateTokenEx: %w", err) + } + + case TokenLinked: + if err := windows.DuplicateTokenEx(t, windows.MAXIMUM_ALLOWED, nil, windows.SecurityDelegation, windows.TokenPrimary, &duplicatedToken); err != nil { + return nil, fmt.Errorf("error while DuplicateTokenEx: %w", err) + } + dt, err := duplicatedToken.GetLinkedToken() + windows.CloseHandle(windows.Handle(duplicatedToken)) + if err != nil { + return nil, fmt.Errorf("error while getting LinkedToken: %w", err) + } + duplicatedToken = dt + } + + return &Token{token: duplicatedToken, typ: tokenType}, nil +} + +// Get the Interactive token associated with current logged in user +func GetInteractiveToken(tokenType tokenType) (*Token, error) { + + switch tokenType { + case TokenPrimary, TokenImpersonation, TokenLinked: + default: + return nil, ErrOnlyPrimaryImpersonationTokenAllowed + } + + var ( + sessionPointer uintptr + sessionCount uint32 + interactiveToken windows.Token + duplicatedToken windows.Token + sessionID uint32 + ) + + err := windows.WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, (**windows.WTS_SESSION_INFO)(unsafe.Pointer(&sessionPointer)), &sessionCount) + if err != nil { + return nil, fmt.Errorf("error while enumerating sessions: %v", err) + } + defer windows.WTSFreeMemory(sessionPointer) + + sessions := make([]*windows.WTS_SESSION_INFO, sessionCount) + size := unsafe.Sizeof(windows.WTS_SESSION_INFO{}) + + for i := range sessions { + sessions[i] = (*windows.WTS_SESSION_INFO)(unsafe.Pointer(sessionPointer + (size * uintptr(i)))) + } + + for i := range sessions { + if sessions[i].State == windows.WTSActive { + sessionID = sessions[i].SessionID + break + } + } + if sessionID == 0 { + return nil, ErrNoActiveSession + } + + if err := windows.WTSQueryUserToken(sessionID, &interactiveToken); err != nil { + return nil, fmt.Errorf("error while WTSQueryUserToken: %w", err) + } + + defer windows.CloseHandle(windows.Handle(interactiveToken)) + + switch tokenType { + case TokenPrimary: + if err := windows.DuplicateTokenEx(interactiveToken, windows.MAXIMUM_ALLOWED, nil, windows.SecurityDelegation, windows.TokenPrimary, &duplicatedToken); err != nil { + return nil, fmt.Errorf("error while DuplicateTokenEx: %w", err) + } + case TokenImpersonation: + if err := windows.DuplicateTokenEx(interactiveToken, windows.MAXIMUM_ALLOWED, nil, windows.SecurityImpersonation, windows.TokenImpersonation, &duplicatedToken); err != nil { + return nil, fmt.Errorf("error while DuplicateTokenEx: %w", err) + } + case TokenLinked: + if err := windows.DuplicateTokenEx(interactiveToken, windows.MAXIMUM_ALLOWED, nil, windows.SecurityDelegation, windows.TokenPrimary, &duplicatedToken); err != nil { + return nil, fmt.Errorf("error while DuplicateTokenEx: %w", err) + } + dt, err := duplicatedToken.GetLinkedToken() + windows.CloseHandle(windows.Handle(duplicatedToken)) + if err != nil { + return nil, fmt.Errorf("error while getting LinkedToken: %w", err) + } + duplicatedToken = dt + } + + if windows.Handle(duplicatedToken) == windows.InvalidHandle { + return nil, ErrInvalidDuplicatedToken + } + + return &Token{typ: tokenType, token: duplicatedToken}, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9143c4f --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/fourcorelabs/wintoken + +go 1.16 + +require golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e8a0352 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/vars.go b/vars.go new file mode 100644 index 0000000..add57b9 --- /dev/null +++ b/vars.go @@ -0,0 +1,11 @@ +package wintoken + +import "fmt" + +var ( + ErrNoActiveSession error = fmt.Errorf("no active session found") + ErrInvalidDuplicatedToken error = fmt.Errorf("invalid duplicated token") + ErrOnlyPrimaryImpersonationTokenAllowed error = fmt.Errorf("only primary or impersonation token types allowed") + ErrNoPrivilegesSpecified error = fmt.Errorf("no privileges specified") + ErrTokenClosed error = fmt.Errorf("token has been closed") +) diff --git a/wintoken.go b/wintoken.go new file mode 100644 index 0000000..6a2cb65 --- /dev/null +++ b/wintoken.go @@ -0,0 +1,401 @@ +package wintoken + +import ( + "bytes" + "encoding/binary" + "fmt" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") + procLookupPrivilegeName = modadvapi32.NewProc("LookupPrivilegeNameW") + procLookupPrivilegeDisplayName = modadvapi32.NewProc("LookupPrivilegeDisplayNameW") +) + +type ( + tokenType int + privModType int +) + +const ( + PrivDisable privModType = iota + PrivEnable + PrivRemove +) + +type Token struct { + typ tokenType + token windows.Token +} + +type TokenUserDetail struct { + Username string + Domain string + AccountType uint32 + UserProfileDir string + Environ []string +} + +func (t TokenUserDetail) String() string { + return fmt.Sprintf("Username: %s, Domain: %s, Account Type: %d, UserProfileDir: %s", t.Username, t.Domain, t.AccountType, t.UserProfileDir) +} + +type Privilege struct { + Name string + Description string + Enabled bool + EnabledByDefault bool + Removed bool + UsedForAccess bool +} + +func (p Privilege) String() string { + status := "Disabled" + if p.Removed { + status = "Removed" + } else if p.Enabled { + status = "Enabled" + } + return fmt.Sprintf("%s: %s", p.Name, status) +} + +const ( + TokenUnknown tokenType = iota + TokenPrimary + TokenImpersonation + TokenLinked +) + +func NewToken(token windows.Token, typ tokenType) *Token { + return &Token{ + token: token, + typ: typ, + } +} + +// Return the underlying token for use +func (t *Token) Token() windows.Token { + return t.token +} + +// Close the underlying token +func (t *Token) Close() { + windows.Close(windows.Handle(t.token)) + t.token = 0 +} + +func (t *Token) errIfTokenClosed() error { + if t.token == 0 { + return ErrTokenClosed + } + return nil +} + +func lookupPrivilegeNameByLUID(luid uint64) (string, string, error) { + nameBuffer := make([]uint16, 256) + nameBufferSize := uint32(len(nameBuffer)) + displayNameBuffer := make([]uint16, 256) + displayNameBufferSize := uint32(len(displayNameBuffer)) + + sysName, err := windows.UTF16PtrFromString("") + if err != nil { + return "", "", err + } + + if r1, _, err := procLookupPrivilegeName.Call(uintptr(unsafe.Pointer(sysName)), uintptr(unsafe.Pointer(&luid)), uintptr(unsafe.Pointer(&nameBuffer[0])), uintptr(unsafe.Pointer(&nameBufferSize))); r1 == 0 { + return "", "", err + } + + var langID uint32 + if r1, _, err := procLookupPrivilegeDisplayName.Call(uintptr(unsafe.Pointer(sysName)), uintptr(unsafe.Pointer(&nameBuffer[0])), uintptr(unsafe.Pointer(&displayNameBuffer[0])), uintptr(unsafe.Pointer(&displayNameBufferSize)), uintptr(unsafe.Pointer(&langID))); r1 == 0 { + return "", "", err + } + + return windows.UTF16ToString(nameBuffer), windows.UTF16ToString(displayNameBuffer), nil +} + +// Get User details associated with token +func (t *Token) UserDetails() (TokenUserDetail, error) { + uSid, err := t.token.GetTokenUser() + if err != nil { + return TokenUserDetail{}, err + } + user, domain, typ, err := uSid.User.Sid.LookupAccount("") + if err != nil { + return TokenUserDetail{}, err + } + uProfDir, err := t.token.GetUserProfileDirectory() + if err != nil { + return TokenUserDetail{}, err + } + env, err := t.token.Environ(false) + if err != nil { + return TokenUserDetail{}, err + } + return TokenUserDetail{Username: user, Domain: domain, AccountType: typ, UserProfileDir: uProfDir, Environ: env}, nil +} + +// List all Privileges from the token +func (t *Token) GetPrivileges() ([]Privilege, error) { + if err := t.errIfTokenClosed(); err != nil { + return nil, err + } + + n := uint32(0) + windows.GetTokenInformation(t.token, windows.TokenPrivileges, nil, 0, &n) + + b := make([]byte, n) + if err := windows.GetTokenInformation(t.token, windows.TokenPrivileges, &b[0], uint32(len(b)), &n); err != nil { + return nil, err + } + + privBuff := bytes.NewBuffer(b) + + var nPrivs uint32 + if err := binary.Read(privBuff, binary.LittleEndian, &nPrivs); err != nil { + return nil, fmt.Errorf("cannot read number of privileges: %w", err) + } + + privDetails := make([]Privilege, int(nPrivs)) + + for i := 0; i < int(nPrivs); i++ { + + var ( + luid uint64 + attributes uint32 + currentPrivInfo Privilege + err error + ) + + if err := binary.Read(privBuff, binary.LittleEndian, &luid); err != nil { + return nil, fmt.Errorf("cannot read LUID from buffer: %w", err) + } + + if err := binary.Read(privBuff, binary.LittleEndian, &attributes); err != nil { + return nil, fmt.Errorf("cannot read attributes from buffer: %w", err) + } + + currentPrivInfo.Name, currentPrivInfo.Description, err = lookupPrivilegeNameByLUID(luid) + if err != nil { + return nil, fmt.Errorf("cannot get privilege info based on the LUID: %w", err) + } + + currentPrivInfo.EnabledByDefault = (attributes & windows.SE_PRIVILEGE_ENABLED_BY_DEFAULT) > 0 + currentPrivInfo.UsedForAccess = (attributes & windows.SE_PRIVILEGE_USED_FOR_ACCESS) > 0 + currentPrivInfo.Enabled = (attributes & windows.SE_PRIVILEGE_ENABLED) > 0 + currentPrivInfo.Removed = (attributes & windows.SE_PRIVILEGE_REMOVED) > 0 + + privDetails[i] = currentPrivInfo + } + + return privDetails, nil +} + +// Enable All Privileges from Token +func (t *Token) EnableAllPrivileges() error { + if err := t.errIfTokenClosed(); err != nil { + return err + } + + privs, err := t.GetPrivileges() + if err != nil { + return err + } + + var toBeEnabled []string + + for _, p := range privs { + if !p.Removed && !p.Enabled { + toBeEnabled = append(toBeEnabled, p.Name) + } + } + return t.modifyTokenPrivileges(toBeEnabled, PrivEnable) +} + +// Disable All Privileges from Token +func (t *Token) DisableAllPrivileges() error { + if err := t.errIfTokenClosed(); err != nil { + return err + } + + privs, err := t.GetPrivileges() + if err != nil { + return err + } + + var toBeDisabled []string + + for _, p := range privs { + if !p.Removed && p.Enabled { + toBeDisabled = append(toBeDisabled, p.Name) + } + } + return t.modifyTokenPrivileges(toBeDisabled, PrivDisable) +} + +// Remove All Privileges from Token +func (t *Token) RemoveAllPrivileges() error { + if err := t.errIfTokenClosed(); err != nil { + return err + } + + privs, err := t.GetPrivileges() + if err != nil { + return err + } + + var toBeRemoved []string + + for _, p := range privs { + if !p.Removed { + toBeRemoved = append(toBeRemoved, p.Name) + } + } + return t.modifyTokenPrivileges(toBeRemoved, PrivRemove) +} + +// Enable token privileges by name +func (t *Token) EnableTokenPrivileges(privs []string) error { + return t.modifyTokenPrivileges(privs, PrivEnable) +} + +// Disable token privileges by name +func (t *Token) DisableTokenPrivileges(privs []string) error { + return t.modifyTokenPrivileges(privs, PrivDisable) +} + +// Remove token privileges by name +func (t *Token) RemoveTokenPrivileges(privs []string) error { + return t.modifyTokenPrivileges(privs, PrivRemove) +} + +// Enable token privilege by name +func (t *Token) EnableTokenPrivilege(priv string) error { + return t.modifyTokenPrivilege(priv, PrivEnable) +} + +// Disable token privilege by name +func (t *Token) DisableTokenPrivilege(priv string) error { + return t.modifyTokenPrivilege(priv, PrivDisable) +} + +// Remove token privilege by name +func (t *Token) RemoveTokenPrivilege(priv string) error { + return t.modifyTokenPrivilege(priv, PrivRemove) +} + +func (t *Token) modifyTokenPrivileges(privs []string, mode privModType) error { + if err := t.errIfTokenClosed(); err != nil { + return err + } + + if len(privs) == 0 { + return ErrNoPrivilegesSpecified + } + + errMsgConst := "" + switch mode { + case PrivDisable: + errMsgConst = "disabling" + case PrivEnable: + errMsgConst = "enabling" + case PrivRemove: + errMsgConst = "removing" + } + var errMsg string + for _, p := range privs { + err := t.modifyTokenPrivilege(p, mode) + if err != nil { + if len(errMsg) != 0 { + errMsg += "\n" + } + errMsg += fmt.Sprintf("%s privilege for %s failed: %s", errMsgConst, p, err) + } + } + + if len(errMsg) != 0 { + return fmt.Errorf(errMsg) + } + return nil +} + +func (t *Token) modifyTokenPrivilege(priv string, mode privModType) error { + if err := t.errIfTokenClosed(); err != nil { + return err + } + + var luid windows.LUID + + if err := windows.LookupPrivilegeValue(nil, windows.StringToUTF16Ptr(priv), &luid); err != nil { + return fmt.Errorf("LookupPrivilegeValueW failed: %w", err) + } + + ap := windows.Tokenprivileges{ + PrivilegeCount: 1, + } + ap.Privileges[0].Luid = luid + + switch mode { + case PrivEnable: + ap.Privileges[0].Attributes = windows.SE_PRIVILEGE_ENABLED + case PrivRemove: + ap.Privileges[0].Attributes = windows.SE_PRIVILEGE_REMOVED + } + + if err := windows.AdjustTokenPrivileges(t.token, false, &ap, 0, nil, nil); err != nil { + return fmt.Errorf("AdjustTokenPrivileges failed: %w", err) + } + + return nil +} + +// GetIntegrityLevel is used to get integrity level of the token +func (t *Token) GetIntegrityLevel() (string, error) { + if err := t.errIfTokenClosed(); err != nil { + return "", err + } + + n := uint32(0) + windows.GetTokenInformation(t.token, windows.TokenIntegrityLevel, nil, 0, &n) + + b := make([]byte, n) + if err := windows.GetTokenInformation(t.token, windows.TokenIntegrityLevel, &b[0], uint32(len(b)), &n); err != nil { + return "", err + } + + tml := (*windows.Tokenmandatorylabel)(unsafe.Pointer(&b[0])) + sid := (*windows.SID)(unsafe.Pointer(tml.Label.Sid)) + switch sid.String() { + case "S-1-16-4096": + return "Low", nil + + case "S-1-16-8192": + return "Medium", nil + + case "S-1-16-12288": + return "High", nil + + case "S-1-16-16384": + return "System", nil + + default: + return "Unknown", nil + } +} + +// GetLinkedToken is used to get the linked token in +func (t *Token) GetLinkedToken() (*Token, error) { + + lt, err := t.token.GetLinkedToken() + if err != nil { + return nil, err + } + + return &Token{ + typ: TokenLinked, + token: lt, + }, nil +}