Last active
August 27, 2024 14:01
-
-
Save jborean93/ca63f50ecaa9be5b517df5ad3433d461 to your computer and use it in GitHub Desktop.
Generates a Win32 Access Token using S4U (no password required)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Copyright: (c) 2023, Jordan Borean (@jborean93) <[email protected]> | |
# MIT License (see LICENSE or https://opensource.org/licenses/MIT) | |
#Requires -Module Ctypes | |
Function New-S4UAccessToken { | |
<# | |
.SYNOPSIS | |
Generates an S4U access token. | |
.DESCRIPTION | |
Generates an S4U access token for the user specified. This token can be | |
used for things like spawning a new process or impersonating on the thread. | |
.PARAMETER UserName | |
The username to generate the S4U access token. This accepts both the | |
Netbios HOST\username or UPN [email protected] format. If neither form | |
is used the username is treated as a local user name. The values | |
'.\username' "$env:COMPUTERNAME\username", or 'username' are all treated | |
as local users. The values 'DOMAIN\username' or '[email protected]' are | |
treated as domain users. | |
.PARAMETER LogonType | |
The logon type of the access token to generate. Defaults to Batch but can | |
be set to Network if the $UserName specified does not have batch logon | |
rights. Can also be set to Interactive to create a Batch logon but also | |
add the BUILTIN\INTERACTIVE group associated with an interactive logon. | |
This is more of a hack to try and replicate an actual interactive token so | |
it might have some unexpected side effects. | |
.PARAMETER Group | |
Add the specified groups to the new access token. This parameter accepts a | |
list of hashtables with the keys: | |
Sid - The group name or SID as a string or IdentiyReference object | |
Attributes - Attributes for the group, See TokenGroupAttributes | |
.EXAMPLE | |
Generates an S4U token and uses Start-ProcessWith from the ProcessEx module | |
to start a new process with this token | |
$token = New-S4UAccessToken local-user | |
Start-ProcessWith cmd.exe -Token $token | |
.EXAMPLE | |
Generate an S4U token with extra groups | |
$token = New-S4UAccessToken local-user -Group @( | |
@{ Sid = "$env:COMPUTERNAME\LocalGroup"; Attributes = 'SE_GROUP_MANDATORY, SE_GROUP_ENABLED_BY_DEFAULT, SE_GROUP_ENABLED' } | |
) | |
.NOTES | |
The token is only valid for use on the current host. Attempting to use the | |
S4U token for outbound authentication will fail as the user appears as an | |
anonymous user. | |
The caller must have the SeTcbPrivilege present to work. An easy way to | |
get this privilege is to run as SYSTEM. | |
This cmdlet requires the Ctypes PowerShell module to work. | |
#> | |
[CmdletBinding()] | |
param ( | |
[Parameter(Mandatory, Position = 0)] | |
[string] | |
$UserName, | |
[Parameter()] | |
[ValidateSet('Batch', 'Network', 'Interactive')] | |
[string] | |
$LogonType = 'Batch', | |
[Parameter()] | |
[System.Collections.IDictionary[]] | |
$Group | |
) | |
[Flags()] enum TokenGroupAttributes { | |
SE_GROUP_MANDATORY = 0x00000001 | |
SE_GROUP_ENABLED_BY_DEFAULT = 0x00000002 | |
SE_GROUP_ENABLED = 0x00000004 | |
SE_GROUP_OWNER = 0x00000008 | |
SE_GROUP_USE_FOR_DENY_ONLY = 0x00000010 | |
SE_GROUP_INTEGRITY = 0x00000020 | |
SE_GROUP_INTEGRITY_ENABLED = 0x00000040 | |
SE_GROUP_RESOURCE = 0x20000000 | |
SE_GROUP_LOGON_ID = 0xC0000000 | |
} | |
ctypes_struct LUID { | |
[int]$LowPart | |
[int]$HightPart | |
} | |
ctypes_struct UNMANAGED_STRING { | |
[int16]$Length | |
[int16]$MaximumLength | |
[IntPtr]$Buffer | |
} | |
ctypes_struct KERB_S4U_LOGON { | |
[int]$MessageType | |
[int]$Flags | |
[UNMANAGED_STRING]$ClientUpn | |
[UNMANAGED_STRING]$ClientRealm | |
} | |
ctypes_struct SID_AND_ATTRIBUTES { | |
[IntPtr]$Sid | |
[int]$Attributes | |
} | |
ctypes_struct TOKEN_SOURCE { | |
[MarshalAs('ByValArray', SizeConst = 8)][char[]]$SourceName | |
[LUID]$SourceIdentifier | |
} | |
ctypes_struct TOKEN_GROUPS { | |
[int]$GroupCount | |
[MarshalAs('ByValArray', SizeConst = 1)][SID_AND_ATTRIBUTES[]]$Groups | |
} | |
ctypes_struct QUOTA_LIMITS { | |
[IntPtr]$PagedPoolLimit | |
[IntPtr]$NonPagedPoolLimit | |
[IntPtr]$MinimumWorkingSetSize | |
[IntPtr]$MaximumWorkingSetSize | |
[IntPtr]$PagefileLimit | |
[Int64]$TimeLimit | |
} | |
$advapi32 = New-CtypesLib Advapi32.dll | |
$secur32 = New-CtypesLib Secur32.dll | |
$userPart = $UserName | |
$domainPart = '' | |
if ($UserName.Contains('\')) { | |
$domainPart, $userPart = $UserName.Split([char[]]@('\'), 2) | |
} | |
if ($domainPart -eq '.') { | |
$domainPart = $env:COMPUTERNAME | |
} | |
if ( | |
($domainPart -and $domainPart -ne $env:COMPUTERNAME ) -or | |
(-not $domainPart -and $UserName.Contains('@')) | |
) { | |
$packageName = 'Kerberos' | |
} | |
else { | |
$packageName = 'MICROSOFT_AUTHENTICATION_PACKAGE_V1_0' | |
$domainPart = $env:COMPUTERNAME | |
} | |
$logonTypeId = if ($LogonType -eq 'Batch') { | |
4 | |
} | |
elseif ($LogonType -eq 'Interactive') { | |
4 | |
$Group = @( | |
$Group | |
@{ | |
Sid = [System.Security.Principal.SecurityIdentifier]::new("S-1-5-4") # NT AUTHORITY\INTERACTIVE | |
Attributes = [TokenGroupAttributes]'SE_GROUP_MANDATORY, SE_GROUP_ENABLED_BY_DEFAULT, SE_GROUP_ENABLED' | |
} | |
) | |
} | |
else { | |
3 | |
} | |
Write-Verbose "Calling LsaLogonUser with PackageName: '$packageName', LogonType: $logonTypeId, UserName: '$userPart', Domain: '$domainpart'" | |
$logonProcessPtr = $lsaHandle = $packageNamePtr = $authInfoPtr = $localGroupsPtr = $profileBuffer = [IntPtr]::Zero | |
try { | |
# Get the LSA handle used for later operations. This is where the | |
# SeTcbPrivilege check happens. | |
$logonProcess = "ctypes" | |
$logonProcessBytes = [System.Text.Encoding]::ASCII.GetBytes($logonProcess) | |
$logonProcessPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($logonProcessBytes.Length) | |
[System.Runtime.InteropServices.Marshal]::Copy($logonProcessBytes, 0, $logonProcessPtr, $logonProcessBytes.Length) | |
$logonProcessString = [UNMANAGED_STRING]@{ | |
Length = $logonProcessBytes.Length | |
MaximumLength = $logonProcessBytes.Length | |
Buffer = $logonProcessPtr | |
} | |
$securityMode = 0 | |
$status = $secur32.LsaRegisterLogonProcess( | |
[ref]$logonProcessString, | |
[ref]$lsaHandle, | |
[ref]$securityMode | |
) | |
if ($status) { | |
$err = $advapi32.LsaNtStatusToWinError($status) | |
throw [System.ComponentModel.Win32Exception]$err | |
} | |
# Determine the auth package id that needs to be used | |
$packageNameBytes = [System.Text.Encoding]::ASCII.GetBytes($packageName) | |
$packageNamePtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($packageNameBytes.Length) | |
[System.Runtime.InteropServices.Marshal]::Copy($packageNameBytes, 0, $packageNamePtr, $packageNameBytes.Length) | |
$packageNameString = [UNMANAGED_STRING]@{ | |
Length = $packageNameBytes.Length | |
MaximumLength = $packageNameBytes.Length | |
Buffer = $packageNamePtr | |
} | |
$authPackageId = 0 | |
$status = $secur32.LsaLookupAuthenticationPackage( | |
$lsaHandle, | |
[ref]$packageNameString, | |
[ref]$authPackageId | |
) | |
if ($status) { | |
$err = $advapi32.LsaNtStatusToWinError($status) | |
throw [System.ComponentModel.Win32Exception]$err | |
} | |
# Build the authentication information buffer | |
$s4uLength = [System.Runtime.InteropServices.Marshal]::SizeOf([type][KERB_S4U_LOGON]) | |
$usernameBytes = [System.Text.Encoding]::Unicode.GetBytes($userPart) | |
$domainBytes = [System.Text.Encoding]::Unicode.GetBytes($domainPart) | |
$authInfoLength = $s4uLength + $usernameBytes.Length + $domainBytes.Length | |
$authInfoPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($authInfoLength) | |
$usernamePtr = [IntPtr]::Add($authInfoPtr, $s4uLength) | |
$domainPtr = [IntPtr]::Add($usernamePtr, $usernameBytes.Length) | |
$authInfo = [KERB_S4U_LOGON]@{ | |
MessageType = 12 # KerbS4ULogon | |
Flags = 0 | |
ClientUpn = [UNMANAGED_STRING]@{ | |
Length = $usernameBytes.Length | |
MaximumLength = $usernameBytes.Length | |
Buffer = $usernamePtr | |
} | |
ClientRealm = [UNMANAGED_STRING]@{ | |
Length = $domainBytes.Length | |
MaximumLength = $domainBytes.Length | |
Buffer = $domainPtr | |
} | |
} | |
[System.Runtime.InteropServices.Marshal]::StructureToPtr($authInfo, $authInfoPtr, $false) | |
[System.Runtime.InteropServices.Marshal]::Copy($usernameBytes, 0, $usernamePtr, $usernameBytes.Length) | |
[System.Runtime.InteropServices.Marshal]::Copy($domainBytes, 0, $domainPtr, $domainBytes.Length) | |
if ($Group) { | |
$tokenGroupsSize = [System.Runtime.InteropServices.Marshal]::SizeOf([type][TOKEN_GROUPS]) | |
$saaSize = [System.Runtime.InteropServices.Marshal]::SizeOf([type][SID_AND_ATTRIBUTES]) | |
$sidSize = 0 | |
$groupSAA = @( | |
foreach ($g in $Group) { | |
try { | |
$gSid = if ($g.Sid -is [string]) { | |
try { | |
[System.Security.Principal.SecurityIdentifier]::new($g.Sid) | |
} | |
catch { | |
[System.Security.Principal.NTAccount]::new($g.Sid).Translate([System.Security.Principal.SecurityIdentifier]) | |
} | |
} | |
elseif ($g.Sid -is [System.Security.Principal.NTAccount]) { | |
$g.Sid.Translate([System.Security.Principal.SecurityIdentifier]) | |
} | |
elseif ($g.Sid -is [System.Security.Principal.SecurityIdentifier]) { | |
$g.Sid | |
} | |
else { | |
throw "Group Sid value '$($g.Sid)' must be a string or IdentityReference" | |
} | |
$sidBytes = [byte[]]::new($gSid.BinaryLength) | |
$gSid.GetBinaryForm($sidBytes, 0) | |
$sidSize += $sidBytes.Length | |
@{ | |
Sid = $sidBytes | |
Attributes = [TokenGroupAttributes]$g.Attributes | |
} | |
} | |
catch { | |
$_.ErrorDetails = "Group entry must contain a Sid and Attributes key with the Sid being a string or Identity reference of a value group, error when building group object: $($_)" | |
$PSCmdlet.WriteError($_) | |
continue | |
} | |
} | |
) | |
if ($groupSAA.Count) { | |
# tokenGroupsSize includes 1 SID_AND_ATTRIBUTES value so exclude that from the final count | |
$tokenGroupsSize = $tokenGroupsSize + ($saaSize * ($Group.Count - 1)) + $sidSize | |
$localGroupsPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($tokenGroupsSize) | |
$saaOffset = [System.Runtime.InteropServices.Marshal]::OffsetOf([type][TOKEN_GROUPS], "Groups") | |
$saaPtr = [IntPtr]::Add($localGroupsPtr, [int64]$saaOffset) | |
$sidPtr = [IntPtr]::Add($localGroupsPtr, $tokenGroupsSize - $sidSize) | |
$tokenGroups = [TOKEN_GROUPS]@{ | |
GroupCount = $groupSAA.Count | |
} | |
[System.Runtime.InteropServices.Marshal]::StructureToPtr($tokenGroups, $localGroupsPtr, $false) | |
foreach ($saa in $groupSAA) { | |
[System.Runtime.InteropServices.Marshal]::Copy($saa.Sid, 0, $sidPtr, $saa.Sid.Length) | |
$saaValue = [SID_AND_ATTRIBUTES]@{ | |
Sid = $sidPtr | |
Attributes = $saa.Attributes | |
} | |
[System.Runtime.InteropServices.Marshal]::StructureToPtr($saaValue, $saaPtr, $false) | |
$saaPtr = [IntPtr]::Add($saaPtr, $saaSize) | |
$sidPtr = [IntPtr]::Add($sidPtr, $saa.Sid.Length) | |
} | |
} | |
} | |
# Generate a LUID that is used to identify the source of the logon (us) | |
$sourceLuid = [LUID]::new() | |
if (-not $advapi32.Returns([bool]).SetLastError().AllocateLocallyUniqueId([ref]$sourceLuid)) { | |
throw [System.ComponentModel.Win32Exception]::new() | |
} | |
$sourceContext = [TOKEN_SOURCE]@{ | |
SourceName = "ctypes".PadRight(8, [char]0).ToCharArray() | |
SourceIdentifier = $sourceLuid | |
} | |
$logonId = [LUID]::new() | |
$profileBufferLength = 0 | |
$quotas = [QUOTA_LIMITS]::new() | |
$token = [IntPtr]::Zero | |
$subStatus = 0 | |
$status = $secur32.LsaLogonUser( | |
$lsaHandle, | |
[ref]$logonProcessString, | |
$logonTypeId, | |
$authPackageId, | |
$authInfoPtr, | |
$authInfoLength, | |
$localGroupsPtr, | |
[ref]$sourceContext, | |
[ref]$profileBuffer, | |
[ref]$profileBufferLength, | |
[ref]$logonId, | |
[ref]$token, | |
[ref]$quotas, | |
[ref]$subStatus | |
) | |
if ($status) { | |
$err = $advapi32.LsaNtStatusToWinError($status) | |
$errMsg = [System.ComponentModel.Win32Exception]::new($err).Message | |
throw ('LsaLogonUser failed 0x{0:X8} - SubStatus 0x{1:X8}: {2}' -f $status, $subStatus, $errMsg) | |
} | |
[Microsoft.Win32.SafeHandles.SafeAccessTokenHandle]::new($token) | |
} | |
finally { | |
if ($logonProcessPtr -ne [IntPtr]::Zero) { | |
[System.Runtime.InteropServices.Marshal]::FreeHGlobal($logonProcessPtr) | |
} | |
if ($packageNamePtr -ne [IntPtr]::Zero) { | |
[System.Runtime.InteropServices.Marshal]::FreeHGlobal($packageNamePtr) | |
} | |
if ($authInfoPtr -ne [IntPtr]::Zero) { | |
[System.Runtime.InteropServices.Marshal]::FreeHGlobal($authInfoPtr) | |
} | |
if ($localGroupsPtr -ne [IntPtr]::Zero) { | |
[System.Runtime.InteropServices.Marshal]::FreeHGlobal($localGroupsPtr) | |
} | |
if ($profileBuffer -ne [IntPtr]::Zero) { | |
$secur32.Returns([void]).LsaFreeReturnBuffer($profileBuffer) | |
} | |
if ($lsaHandle -ne [IntPtr]::Zero) { | |
$secur32.Returns([void]).LsaDeregisterLogonProcess($lsaHandle) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment