Skip to content

Instantly share code, notes, and snippets.

@jborean93
Last active August 27, 2024 14:01
Show Gist options
  • Save jborean93/ca63f50ecaa9be5b517df5ad3433d461 to your computer and use it in GitHub Desktop.
Save jborean93/ca63f50ecaa9be5b517df5ad3433d461 to your computer and use it in GitHub Desktop.
Generates a Win32 Access Token using S4U (no password required)
# 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