evil-winrm 3.7 → 3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/evil-winrm.rb +692 -28
- metadata +44 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b563b633495483fcaa8b94925479d4adb6da542a87cbde37f11c93f5fa068188
|
|
4
|
+
data.tar.gz: 3b131febdc2255e94df04e5c80049fb7e200da32b2af59c62d9532453c514cc9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2679808f81c6db0e8c63a795259789c30baacc3a7b9207f5025bfbbd0181a2371362fc8073070c7253a2e6260343a654bf7a147ec72ef34d21eeb7a27a759db4
|
|
7
|
+
data.tar.gz: 592d254c61760ded537f5844972479cbfbf0a640706cd233510265c39de7de9e3e616250154fce29a909076f7c07b4cba8c93e087a813bf768e1e0cb32a22ce1
|
data/evil-winrm.rb
CHANGED
|
@@ -22,7 +22,7 @@ require 'shellwords'
|
|
|
22
22
|
# Constants
|
|
23
23
|
|
|
24
24
|
# Version
|
|
25
|
-
VERSION = '3.
|
|
25
|
+
VERSION = '3.8'
|
|
26
26
|
|
|
27
27
|
# Msg types
|
|
28
28
|
TYPE_INFO = 0
|
|
@@ -34,7 +34,7 @@ TYPE_SUCCESS = 4
|
|
|
34
34
|
# Global vars
|
|
35
35
|
|
|
36
36
|
# Available commands
|
|
37
|
-
$LIST = %w[Bypass-4MSI services upload download menu exit]
|
|
37
|
+
$LIST = %w[Bypass-4MSI services upload download clear cls menu exit]
|
|
38
38
|
$COMMANDS = $LIST.dup
|
|
39
39
|
$CMDS = $COMMANDS.clone
|
|
40
40
|
$LISTASSEM = [''].sort
|
|
@@ -107,6 +107,9 @@ $url = 'wsman'
|
|
|
107
107
|
$default_service = 'HTTP'
|
|
108
108
|
$full_logging_path = "#{Dir.home}/evil-winrm-logs"
|
|
109
109
|
$user_agent = "Microsoft WinRM Client"
|
|
110
|
+
$ccache_file = nil
|
|
111
|
+
$original_krb5ccname = nil
|
|
112
|
+
$kerberos_cleanup_registered = false
|
|
110
113
|
|
|
111
114
|
# Redefine download method from winrm-fs
|
|
112
115
|
module WinRM
|
|
@@ -194,14 +197,11 @@ class EvilWinRM
|
|
|
194
197
|
def arguments
|
|
195
198
|
options = { port: $port, url: $url, service: $service, user_agent: $user_agent }
|
|
196
199
|
optparse = OptionParser.new do |opts|
|
|
197
|
-
opts.banner = 'Usage: evil-winrm -i IP -u USER [-s SCRIPTS_PATH] [-e EXES_PATH] [-P PORT] [-a USERAGENT] [-p PASS] [-H HASH] [-U URL] [-S] [-c PUBLIC_KEY_PATH ] [-k PRIVATE_KEY_PATH ] [-r REALM] [--spn SPN_PREFIX] [-l]'
|
|
200
|
+
opts.banner = 'Usage: evil-winrm -i IP -u USER [-s SCRIPTS_PATH] [-e EXES_PATH] [-P PORT] [-a USERAGENT] [-p PASS] [-H HASH] [-U URL] [-S] [-c PUBLIC_KEY_PATH ] [-k PRIVATE_KEY_PATH ] [-r REALM] [-K TICKET_FILE] [--spn SPN_PREFIX] [-l]'
|
|
198
201
|
opts.on('-S', '--ssl', 'Enable ssl') do |_val|
|
|
199
202
|
$ssl = true
|
|
200
203
|
options[:port] = '5986'
|
|
201
204
|
end
|
|
202
|
-
opts.on('-a', '--user-agent USERAGENT', 'Specify connection user-agent (default Microsoft WinRM Client)') do |val|
|
|
203
|
-
options[:user_agent] = val
|
|
204
|
-
end
|
|
205
205
|
opts.on('-c', '--pub-key PUBLIC_KEY_PATH', 'Local path to public key certificate') do |val|
|
|
206
206
|
options[:pub_key] = val
|
|
207
207
|
end
|
|
@@ -216,6 +216,7 @@ class EvilWinRM
|
|
|
216
216
|
options[:scripts] = val
|
|
217
217
|
end
|
|
218
218
|
opts.on('--spn SPN_PREFIX', 'SPN prefix for Kerberos auth (default HTTP)') { |val| options[:service] = val }
|
|
219
|
+
opts.on('-K', '--ccache TICKET_FILE', 'Path to Kerberos ticket file (ccache or kirbi format, auto-detected)') { |val| options[:ccache] = val }
|
|
219
220
|
opts.on('-e', '--executables EXES_PATH', 'C# executables local path') { |val| options[:executables] = val }
|
|
220
221
|
opts.on('-i', '--ip IP', 'Remote host IP or hostname. FQDN for Kerberos auth (required)') do |val|
|
|
221
222
|
options[:ip] = val
|
|
@@ -237,6 +238,9 @@ class EvilWinRM
|
|
|
237
238
|
options[:password] = "00000000000000000000000000000000:#{val}"
|
|
238
239
|
end
|
|
239
240
|
opts.on('-P', '--port PORT', 'Remote host port (default 5985)') { |val| options[:port] = val }
|
|
241
|
+
opts.on('-a', '--user-agent USERAGENT', 'Specify connection user-agent (default Microsoft WinRM Client)') do |val|
|
|
242
|
+
options[:user_agent] = val
|
|
243
|
+
end
|
|
240
244
|
opts.on('-V', '--version', 'Show version') do |_val|
|
|
241
245
|
puts("v#{VERSION}")
|
|
242
246
|
custom_exit(0, false)
|
|
@@ -294,6 +298,7 @@ class EvilWinRM
|
|
|
294
298
|
$realm = options[:realm]
|
|
295
299
|
$service = options[:service]
|
|
296
300
|
$user_agent = options[:user_agent]
|
|
301
|
+
$ccache_file = options[:ccache]
|
|
297
302
|
unless $log.nil?
|
|
298
303
|
|
|
299
304
|
FileUtils.mkdir_p $full_logging_path
|
|
@@ -321,6 +326,98 @@ class EvilWinRM
|
|
|
321
326
|
|
|
322
327
|
# Generate connection object
|
|
323
328
|
def connection_initialization
|
|
329
|
+
# If using Kerberos and host is an IP, ask user if they want to resolve it to FQDN
|
|
330
|
+
if (!$ccache_file.nil? || !$realm.nil?) && is_ip_address?($host)
|
|
331
|
+
puts
|
|
332
|
+
print_message("IP address detected (#{$host}). Kerberos requires FQDN. Do you want to attempt reverse DNS lookup?", TYPE_WARNING, true, $logger)
|
|
333
|
+
print_message('Press "y" to attempt DNS resolution, press any other key to cancel', TYPE_WARNING, true, $logger)
|
|
334
|
+
response = $stdin.getch.downcase
|
|
335
|
+
puts
|
|
336
|
+
|
|
337
|
+
if response == 'y'
|
|
338
|
+
print_message("Attempting reverse DNS lookup to get FQDN for Kerberos...", TYPE_INFO, true, $logger)
|
|
339
|
+
fqdn = resolve_ip_to_fqdn($host, $realm)
|
|
340
|
+
if fqdn
|
|
341
|
+
print_message("[+] Resolved IP #{$host} to FQDN: #{fqdn}", TYPE_SUCCESS, true, $logger)
|
|
342
|
+
$host = fqdn
|
|
343
|
+
else
|
|
344
|
+
print_message("Could not resolve IP #{$host} to FQDN.", TYPE_ERROR, true, $logger)
|
|
345
|
+
print_message("When using Kerberos tickets, you must provide an FQDN instead of an IP address.", TYPE_ERROR, true, $logger)
|
|
346
|
+
custom_exit(1, false)
|
|
347
|
+
end
|
|
348
|
+
else
|
|
349
|
+
print_message("DNS resolution cancelled by user.", TYPE_ERROR, true, $logger)
|
|
350
|
+
print_message("When using Kerberos tickets, you must provide an FQDN instead of an IP address.", TYPE_ERROR, true, $logger)
|
|
351
|
+
custom_exit(1, false)
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Configure Kerberos ticket file if provided (supports both ccache and kirbi)
|
|
356
|
+
if !$ccache_file.nil?
|
|
357
|
+
expanded_path = File.expand_path($ccache_file)
|
|
358
|
+
|
|
359
|
+
unless File.exist?(expanded_path)
|
|
360
|
+
print_message("Kerberos ticket file not found: #{expanded_path}", TYPE_ERROR, true, $logger)
|
|
361
|
+
custom_exit(1, false)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
unless File.readable?(expanded_path)
|
|
365
|
+
print_message("Kerberos ticket file is not readable: #{expanded_path}", TYPE_ERROR, true, $logger)
|
|
366
|
+
custom_exit(1, false)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Check if file is not empty
|
|
370
|
+
if File.size(expanded_path) == 0
|
|
371
|
+
print_message("Kerberos ticket file is empty: #{expanded_path}", TYPE_ERROR, true, $logger)
|
|
372
|
+
custom_exit(1, false)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Detect ticket type
|
|
376
|
+
ticket_type = detect_ticket_type(expanded_path)
|
|
377
|
+
|
|
378
|
+
# Convert kirbi to ccache if needed
|
|
379
|
+
if ticket_type == :kirbi
|
|
380
|
+
ccache_path = convert_kirbi_to_ccache(expanded_path)
|
|
381
|
+
ticket_type_name = "kirbi"
|
|
382
|
+
else
|
|
383
|
+
# Already ccache format
|
|
384
|
+
ccache_path = expanded_path
|
|
385
|
+
ticket_type_name = "ccache"
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Only modify ENV if it's not already set to avoid memory issues
|
|
389
|
+
# If user has already set KRB5CCNAME, we'll use that instead
|
|
390
|
+
if ENV['KRB5CCNAME'].nil? || ENV['KRB5CCNAME'].empty?
|
|
391
|
+
# Save original (nil) value
|
|
392
|
+
$original_krb5ccname = ENV['KRB5CCNAME']
|
|
393
|
+
# Set KRB5CCNAME environment variable
|
|
394
|
+
ENV['KRB5CCNAME'] = ccache_path
|
|
395
|
+
print_message("Using #{ticket_type_name} Kerberos ticket file: #{expanded_path}", TYPE_INFO, true, $logger)
|
|
396
|
+
else
|
|
397
|
+
# User already has KRB5CCNAME set, save original and warn them
|
|
398
|
+
$original_krb5ccname = ENV['KRB5CCNAME']
|
|
399
|
+
print_message("KRB5CCNAME is already set to: #{ENV['KRB5CCNAME']}. Using existing value instead of #{expanded_path}", TYPE_WARNING, true, $logger)
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Register at_exit handler to clean up KRB5CCNAME before any automatic cleanup
|
|
403
|
+
# This prevents malloc errors when the process exits (especially when shell is idle)
|
|
404
|
+
unless $kerberos_cleanup_registered
|
|
405
|
+
at_exit do
|
|
406
|
+
begin
|
|
407
|
+
if defined?($original_krb5ccname) && !$original_krb5ccname.nil?
|
|
408
|
+
ENV['KRB5CCNAME'] = $original_krb5ccname
|
|
409
|
+
elsif defined?($original_krb5ccname) && $original_krb5ccname.nil?
|
|
410
|
+
# Only delete if we set it (if original was nil)
|
|
411
|
+
ENV.delete('KRB5CCNAME') if ENV.key?('KRB5CCNAME')
|
|
412
|
+
end
|
|
413
|
+
rescue => e
|
|
414
|
+
# Ignore errors during cleanup
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
$kerberos_cleanup_registered = true
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
324
421
|
if $ssl
|
|
325
422
|
$conn = if $pub_key && $priv_key
|
|
326
423
|
WinRM::Connection.new(
|
|
@@ -333,6 +430,16 @@ class EvilWinRM
|
|
|
333
430
|
client_key: $priv_key,
|
|
334
431
|
user_agent: $user_agent
|
|
335
432
|
)
|
|
433
|
+
elsif !$realm.nil?
|
|
434
|
+
WinRM::Connection.new(
|
|
435
|
+
endpoint: "https://#{$host}:#{$port}/#{$url}",
|
|
436
|
+
user: '',
|
|
437
|
+
password: '',
|
|
438
|
+
transport: :kerberos,
|
|
439
|
+
realm: $realm,
|
|
440
|
+
no_ssl_peer_verification: true,
|
|
441
|
+
user_agent: $user_agent
|
|
442
|
+
)
|
|
336
443
|
else
|
|
337
444
|
WinRM::Connection.new(
|
|
338
445
|
endpoint: "https://#{$host}:#{$port}/#{$url}",
|
|
@@ -517,7 +624,32 @@ class EvilWinRM
|
|
|
517
624
|
print_message("Exiting with code #{exit_code}", TYPE_ERROR, true, $logger)
|
|
518
625
|
end
|
|
519
626
|
end
|
|
520
|
-
|
|
627
|
+
|
|
628
|
+
# Restore KRB5CCNAME environment variable before exiting to avoid memory issues
|
|
629
|
+
begin
|
|
630
|
+
if defined?($original_krb5ccname) && !$original_krb5ccname.nil?
|
|
631
|
+
ENV['KRB5CCNAME'] = $original_krb5ccname
|
|
632
|
+
elsif defined?($original_krb5ccname) && $original_krb5ccname.nil?
|
|
633
|
+
# Only delete if we set it (if original was nil)
|
|
634
|
+
ENV.delete('KRB5CCNAME') if ENV.key?('KRB5CCNAME')
|
|
635
|
+
end
|
|
636
|
+
rescue => e
|
|
637
|
+
# Ignore errors during cleanup
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
# Close connection explicitly before exiting to avoid memory issues with Kerberos
|
|
641
|
+
begin
|
|
642
|
+
if defined?($conn) && !$conn.nil?
|
|
643
|
+
# Try to close the connection gracefully
|
|
644
|
+
$conn = nil
|
|
645
|
+
end
|
|
646
|
+
rescue => e
|
|
647
|
+
# Ignore errors during cleanup
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
# Use exit! to bypass at_exit handlers that might cause memory issues
|
|
651
|
+
# This prevents the malloc error when using Kerberos
|
|
652
|
+
exit!(exit_code)
|
|
521
653
|
end
|
|
522
654
|
|
|
523
655
|
# Progress bar
|
|
@@ -535,12 +667,418 @@ class EvilWinRM
|
|
|
535
667
|
shell.run("(get-item '#{path}').length").output.strip.to_i
|
|
536
668
|
end
|
|
537
669
|
|
|
670
|
+
# Clear screen
|
|
671
|
+
def clear_screen
|
|
672
|
+
system('clear') || system('cls') || puts("\033[2J\033[H")
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
# Get history file path based on host and user
|
|
676
|
+
def get_history_file_path
|
|
677
|
+
history_dir = File.join(Dir.home, '.evil-winrm', 'history')
|
|
678
|
+
FileUtils.mkdir_p(history_dir) unless Dir.exist?(history_dir)
|
|
679
|
+
|
|
680
|
+
# Create a safe filename from host and user
|
|
681
|
+
safe_host = ($host || 'unknown').gsub(/[^a-zA-Z0-9._-]/, '_')
|
|
682
|
+
safe_user = ($user || 'unknown').gsub(/[^a-zA-Z0-9._-]/, '_')
|
|
683
|
+
history_filename = "#{safe_host}_#{safe_user}.hist"
|
|
684
|
+
|
|
685
|
+
File.join(history_dir, history_filename)
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
# Load history from file
|
|
689
|
+
def load_history
|
|
690
|
+
history_file = get_history_file_path
|
|
691
|
+
return unless File.exist?(history_file)
|
|
692
|
+
|
|
693
|
+
begin
|
|
694
|
+
File.readlines(history_file).each do |line|
|
|
695
|
+
line = line.chomp
|
|
696
|
+
Readline::HISTORY.push(line) unless line.empty?
|
|
697
|
+
end
|
|
698
|
+
rescue => e
|
|
699
|
+
# Silently fail if history can't be loaded
|
|
700
|
+
end
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
# Save command to history file
|
|
704
|
+
def save_to_history(command)
|
|
705
|
+
return if command.nil? || command.strip.empty? || command.strip == 'exit'
|
|
706
|
+
|
|
707
|
+
history_file = get_history_file_path
|
|
708
|
+
begin
|
|
709
|
+
File.open(history_file, 'a') do |f|
|
|
710
|
+
f.puts(command)
|
|
711
|
+
end
|
|
712
|
+
rescue => e
|
|
713
|
+
# Silently fail if history can't be saved
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
# Resolve IP address to FQDN using reverse DNS lookup
|
|
718
|
+
# Returns the best FQDN when multiple PTR records exist (prioritizes server FQDN over domain name)
|
|
719
|
+
# If only domain is found, attempts to construct and verify server FQDN using forward DNS
|
|
720
|
+
# Also checks /etc/hosts for manual entries
|
|
721
|
+
def resolve_ip_to_fqdn(ip_address, realm = nil)
|
|
722
|
+
require 'socket'
|
|
723
|
+
require 'resolv'
|
|
724
|
+
begin
|
|
725
|
+
resolver = Resolv::DNS.new
|
|
726
|
+
hostnames = []
|
|
727
|
+
|
|
728
|
+
# Step 0: Check /etc/hosts for manual entries (highest priority)
|
|
729
|
+
if File.exist?('/etc/hosts') && File.readable?('/etc/hosts')
|
|
730
|
+
begin
|
|
731
|
+
File.readlines('/etc/hosts').each do |line|
|
|
732
|
+
# Skip comments and empty lines
|
|
733
|
+
next if line.strip.empty? || line.strip.start_with?('#')
|
|
734
|
+
|
|
735
|
+
# Parse line: IP hostname1 hostname2 ...
|
|
736
|
+
parts = line.split
|
|
737
|
+
next if parts.empty?
|
|
738
|
+
|
|
739
|
+
# Check if first part matches our IP
|
|
740
|
+
if parts[0] == ip_address
|
|
741
|
+
# Add all hostnames from this line
|
|
742
|
+
parts[1..-1].each do |hostname|
|
|
743
|
+
# Only consider FQDNs (contain at least one dot)
|
|
744
|
+
if hostname && hostname.include?('.')
|
|
745
|
+
hostnames << hostname unless hostnames.include?(hostname)
|
|
746
|
+
end
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
end
|
|
750
|
+
if !hostnames.empty?
|
|
751
|
+
print_message("Found FQDN(s) in /etc/hosts: #{hostnames.join(', ')}", TYPE_INFO, true, $logger)
|
|
752
|
+
end
|
|
753
|
+
rescue => e
|
|
754
|
+
# If we can't read /etc/hosts, continue with DNS lookup
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
# Step 1: Get all PTR records (reverse DNS)
|
|
759
|
+
begin
|
|
760
|
+
ptr_name = Resolv::IPv4.create(ip_address).to_name
|
|
761
|
+
ptr_records = resolver.getresources(ptr_name, Resolv::DNS::Resource::IN::PTR)
|
|
762
|
+
|
|
763
|
+
ptr_records.each do |ptr|
|
|
764
|
+
hostname = ptr.name.to_s
|
|
765
|
+
if hostname && hostname.include?('.')
|
|
766
|
+
hostnames << hostname unless hostnames.include?(hostname)
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
rescue Resolv::ResolvError, Resolv::ResolvTimeout
|
|
770
|
+
# If Resolv::DNS fails, try Resolv.getname as fallback
|
|
771
|
+
begin
|
|
772
|
+
hostname = Resolv.getname(ip_address)
|
|
773
|
+
if hostname && hostname.include?('.')
|
|
774
|
+
hostnames << hostname unless hostnames.include?(hostname)
|
|
775
|
+
end
|
|
776
|
+
rescue Resolv::ResolvError
|
|
777
|
+
# Continue to Socket fallback
|
|
778
|
+
end
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
# If no results from Resolv, try Socket.getnameinfo
|
|
782
|
+
if hostnames.empty?
|
|
783
|
+
begin
|
|
784
|
+
hostname = Socket.getnameinfo([Socket::AF_INET, nil, ip_address], Socket::NI_NAMEREQD)[0]
|
|
785
|
+
if hostname && hostname.include?('.')
|
|
786
|
+
hostnames << hostname unless hostnames.include?(hostname)
|
|
787
|
+
end
|
|
788
|
+
rescue SocketError
|
|
789
|
+
# All methods failed
|
|
790
|
+
end
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
# Step 2: If we only got the domain name, try to find the server FQDN
|
|
794
|
+
# Remove duplicates before checking
|
|
795
|
+
hostnames.uniq!
|
|
796
|
+
|
|
797
|
+
# Only do this if we don't already have a server FQDN (3+ parts) from /etc/hosts or DNS
|
|
798
|
+
has_server_fqdn = hostnames.any? { |h| h.split('.').length >= 3 }
|
|
799
|
+
domain_only = hostnames.find { |h| h.split('.').length == 2 }
|
|
800
|
+
|
|
801
|
+
# Only attempt forward DNS lookup if:
|
|
802
|
+
# 1. We don't already have a server FQDN
|
|
803
|
+
# 2. We have a domain-only result
|
|
804
|
+
# 3. We have a realm to work with
|
|
805
|
+
if !has_server_fqdn && domain_only && realm
|
|
806
|
+
# Try common DC hostname patterns
|
|
807
|
+
domain = domain_only.downcase
|
|
808
|
+
realm_domain = realm.downcase
|
|
809
|
+
|
|
810
|
+
# Common DC naming patterns
|
|
811
|
+
candidates = [
|
|
812
|
+
"dc01.#{domain}",
|
|
813
|
+
"dc1.#{domain}",
|
|
814
|
+
"dc.#{domain}",
|
|
815
|
+
"dc01.#{realm_domain}",
|
|
816
|
+
"dc1.#{realm_domain}",
|
|
817
|
+
"dc.#{realm_domain}",
|
|
818
|
+
"ad.#{domain}",
|
|
819
|
+
"ad.#{realm_domain}",
|
|
820
|
+
"ad01.#{domain}",
|
|
821
|
+
"ad01.#{realm_domain}"
|
|
822
|
+
]
|
|
823
|
+
|
|
824
|
+
# Remove duplicates from candidates (in case we already have it)
|
|
825
|
+
candidates.reject! { |c| hostnames.include?(c) }
|
|
826
|
+
|
|
827
|
+
# Verify each candidate with forward DNS lookup
|
|
828
|
+
candidates.each do |candidate|
|
|
829
|
+
begin
|
|
830
|
+
addresses = resolver.getaddresses(candidate)
|
|
831
|
+
# Check if any of the resolved addresses match our IP
|
|
832
|
+
if addresses.any? { |addr| addr.to_s == ip_address }
|
|
833
|
+
hostnames << candidate unless hostnames.include?(candidate)
|
|
834
|
+
print_message("Found server FQDN via forward DNS lookup: #{candidate}", TYPE_INFO, true, $logger)
|
|
835
|
+
# Stop after finding first valid server FQDN
|
|
836
|
+
break
|
|
837
|
+
end
|
|
838
|
+
rescue Resolv::ResolvError
|
|
839
|
+
# This candidate doesn't resolve, skip it
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
return nil if hostnames.empty?
|
|
845
|
+
|
|
846
|
+
# Step 3: Select the best FQDN
|
|
847
|
+
# If we have multiple results, prioritize the server FQDN over domain name
|
|
848
|
+
if hostnames.length > 1
|
|
849
|
+
# Sort by: more dots first, then by length (longer = more specific)
|
|
850
|
+
sorted = hostnames.sort_by { |h| [-h.count('.'), -h.length] }
|
|
851
|
+
|
|
852
|
+
# Prefer hostnames that look like server names (have a hostname prefix before the domain)
|
|
853
|
+
# e.g., "dc01.futuristic.tech" over "futuristic.tech"
|
|
854
|
+
best = sorted.find { |h| h.split('.').length >= 3 } || sorted.first
|
|
855
|
+
|
|
856
|
+
print_message("Multiple DNS names found: #{hostnames.join(', ')}. Selected: #{best}", TYPE_INFO, true, $logger)
|
|
857
|
+
return best
|
|
858
|
+
else
|
|
859
|
+
result = hostnames.first
|
|
860
|
+
# If we only have domain, warn the user
|
|
861
|
+
if result.split('.').length == 2
|
|
862
|
+
print_message("Only domain name found (#{result}). Server FQDN not detected. Kerberos may still work.", TYPE_WARNING, true, $logger)
|
|
863
|
+
end
|
|
864
|
+
return result
|
|
865
|
+
end
|
|
866
|
+
rescue => e
|
|
867
|
+
# Any other error
|
|
868
|
+
return nil
|
|
869
|
+
end
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
# Check if a string is an IP address
|
|
873
|
+
def is_ip_address?(str)
|
|
874
|
+
# Match IPv4 address pattern
|
|
875
|
+
ipv4_pattern = /^(\d{1,3}\.){3}\d{1,3}$/
|
|
876
|
+
return true if str.match?(ipv4_pattern)
|
|
877
|
+
|
|
878
|
+
# Match IPv6 address pattern (simplified)
|
|
879
|
+
ipv6_pattern = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/
|
|
880
|
+
return true if str.match?(ipv6_pattern)
|
|
881
|
+
|
|
882
|
+
false
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
# Detect ticket file type (kirbi or ccache)
|
|
886
|
+
def detect_ticket_type(file_path)
|
|
887
|
+
# Check by extension first
|
|
888
|
+
ext = File.extname(file_path).downcase
|
|
889
|
+
return :kirbi if ext == '.kirbi'
|
|
890
|
+
return :ccache if ext == '.ccache'
|
|
891
|
+
|
|
892
|
+
# If no extension or unknown, try to detect by file content
|
|
893
|
+
# Kirbi files typically start with specific ASN.1 structures
|
|
894
|
+
# CCache files have a different structure
|
|
895
|
+
begin
|
|
896
|
+
first_bytes = File.binread(file_path, 4)
|
|
897
|
+
# Kirbi files often start with specific ASN.1 tags
|
|
898
|
+
# This is a heuristic - not 100% reliable but works for most cases
|
|
899
|
+
if first_bytes[0] == 0x76 || first_bytes[0] == 0x6a || first_bytes[0] == 0x61
|
|
900
|
+
return :kirbi
|
|
901
|
+
end
|
|
902
|
+
# CCache files have a different structure
|
|
903
|
+
return :ccache
|
|
904
|
+
rescue => e
|
|
905
|
+
# If we can't read, default to ccache
|
|
906
|
+
return :ccache
|
|
907
|
+
end
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
# Convert kirbi ticket to ccache format
|
|
911
|
+
def convert_kirbi_to_ccache(kirbi_path)
|
|
912
|
+
# Validate input file first
|
|
913
|
+
expanded_kirbi = File.expand_path(kirbi_path)
|
|
914
|
+
|
|
915
|
+
unless File.exist?(expanded_kirbi)
|
|
916
|
+
print_message("Kirbi ticket file not found: #{expanded_kirbi}", TYPE_ERROR, true, $logger)
|
|
917
|
+
custom_exit(1, false)
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
unless File.readable?(expanded_kirbi)
|
|
921
|
+
print_message("Kirbi ticket file is not readable: #{expanded_kirbi}", TYPE_ERROR, true, $logger)
|
|
922
|
+
custom_exit(1, false)
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
# Check if file is not empty
|
|
926
|
+
if File.size(expanded_kirbi) == 0
|
|
927
|
+
print_message("Kirbi ticket file is empty: #{expanded_kirbi}", TYPE_ERROR, true, $logger)
|
|
928
|
+
custom_exit(1, false)
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
# Generate output path (same directory, change extension)
|
|
932
|
+
output_dir = File.dirname(expanded_kirbi)
|
|
933
|
+
output_name = File.basename(expanded_kirbi, '.kirbi') + '.ccache'
|
|
934
|
+
ccache_path = File.join(output_dir, output_name)
|
|
935
|
+
|
|
936
|
+
# Try to find ticket converter (multiple possible names)
|
|
937
|
+
converter_names = [
|
|
938
|
+
'ticket_converter.py',
|
|
939
|
+
'impacket-ticketConverter',
|
|
940
|
+
'impacket-ticketConverter.py',
|
|
941
|
+
'ticketConverter.py',
|
|
942
|
+
'ticketConverter'
|
|
943
|
+
]
|
|
944
|
+
|
|
945
|
+
converter_paths = []
|
|
946
|
+
|
|
947
|
+
# Check in PATH for each name
|
|
948
|
+
converter_names.each do |name|
|
|
949
|
+
cmd = `which #{name} 2>/dev/null`.strip
|
|
950
|
+
converter_paths << cmd unless cmd.empty?
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
# Also check common installation paths
|
|
954
|
+
converter_names.each do |name|
|
|
955
|
+
converter_paths << name # Current directory
|
|
956
|
+
converter_paths << "/usr/local/bin/#{name}"
|
|
957
|
+
converter_paths << "/usr/bin/#{name}"
|
|
958
|
+
converter_paths << File.join(Dir.home, '.local', 'bin', name)
|
|
959
|
+
converter_paths << File.join(Dir.home, name)
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
# Remove duplicates and empty strings
|
|
963
|
+
converter_paths.uniq!
|
|
964
|
+
converter_paths.reject!(&:empty?)
|
|
965
|
+
|
|
966
|
+
converter_found = nil
|
|
967
|
+
converter_paths.each do |path|
|
|
968
|
+
if File.exist?(path) && File.executable?(path)
|
|
969
|
+
converter_found = path
|
|
970
|
+
break
|
|
971
|
+
end
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
unless converter_found
|
|
975
|
+
print_message("Ticket converter not found. Please install one of: ticket_converter.py, impacket-ticketConverter, or impacket-ticketConverter.py.", TYPE_ERROR, true, $logger)
|
|
976
|
+
print_message("Sources: https://github.com/Zer1t0/ticket_converter or https://github.com/SecureAuthCorp/impacket", TYPE_INFO, true, $logger)
|
|
977
|
+
custom_exit(1, false)
|
|
978
|
+
end
|
|
979
|
+
|
|
980
|
+
# Check if it's a Python script or shell script
|
|
981
|
+
is_python = false
|
|
982
|
+
begin
|
|
983
|
+
first_line = File.readlines(converter_found).first
|
|
984
|
+
if first_line
|
|
985
|
+
# Check for Python shebang
|
|
986
|
+
if first_line.match?(/^#!.*python/)
|
|
987
|
+
is_python = true
|
|
988
|
+
# Check for shell shebang (bash, sh, etc.) - if it's shell, it's not Python
|
|
989
|
+
elsif first_line.match?(/^#!.*\/(bin\/)?(bash|sh|zsh)/)
|
|
990
|
+
is_python = false
|
|
991
|
+
# Check extension
|
|
992
|
+
elsif File.extname(converter_found) == '.py'
|
|
993
|
+
is_python = true
|
|
994
|
+
end
|
|
995
|
+
elsif File.extname(converter_found) == '.py'
|
|
996
|
+
is_python = true
|
|
997
|
+
end
|
|
998
|
+
rescue => e
|
|
999
|
+
# If we can't read, check extension or assume it's executable and try directly
|
|
1000
|
+
is_python = (File.extname(converter_found) == '.py')
|
|
1001
|
+
end
|
|
1002
|
+
|
|
1003
|
+
if is_python
|
|
1004
|
+
# It's a Python script, need to run with python/python3
|
|
1005
|
+
python_cmd = nil
|
|
1006
|
+
['python3', 'python'].each do |py|
|
|
1007
|
+
if system("which #{py} > /dev/null 2>&1")
|
|
1008
|
+
python_cmd = py
|
|
1009
|
+
break
|
|
1010
|
+
end
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
unless python_cmd
|
|
1014
|
+
print_message("Python not found. Please install Python 3 to convert kirbi tickets.", TYPE_ERROR, true, $logger)
|
|
1015
|
+
custom_exit(1, false)
|
|
1016
|
+
end
|
|
1017
|
+
|
|
1018
|
+
cmd = "#{python_cmd} #{converter_found} #{expanded_kirbi} #{ccache_path} 2>&1"
|
|
1019
|
+
else
|
|
1020
|
+
# It's a shell script or executable, run it directly
|
|
1021
|
+
cmd = "#{converter_found} #{expanded_kirbi} #{ccache_path} 2>&1"
|
|
1022
|
+
end
|
|
1023
|
+
|
|
1024
|
+
# Run conversion
|
|
1025
|
+
print_message("Converting kirbi ticket to ccache format...", TYPE_INFO, true, $logger)
|
|
1026
|
+
result = `#{cmd}`
|
|
1027
|
+
|
|
1028
|
+
unless $?.success?
|
|
1029
|
+
# Parse error output to provide a clearer message
|
|
1030
|
+
error_lines = result.split("\n")
|
|
1031
|
+
|
|
1032
|
+
# Check for common Python errors
|
|
1033
|
+
if result.include?('ModuleNotFoundError') || result.include?('No module named')
|
|
1034
|
+
module_match = result.match(/No module named ['"]([^'"]+)['"]/)
|
|
1035
|
+
module_name = module_match ? module_match[1] : 'unknown'
|
|
1036
|
+
if module_name == 'impacket'
|
|
1037
|
+
print_message("The ticket converter requires impacket module which is not installed.", TYPE_ERROR, true, $logger)
|
|
1038
|
+
print_message("Please install it with: pip3 install impacket", TYPE_INFO, true, $logger)
|
|
1039
|
+
custom_exit(1, false)
|
|
1040
|
+
else
|
|
1041
|
+
print_message("The ticket converter requires Python module '#{module_name}' which is not installed.", TYPE_ERROR, true, $logger)
|
|
1042
|
+
print_message("Please install required dependencies.", TYPE_INFO, true, $logger)
|
|
1043
|
+
custom_exit(1, false)
|
|
1044
|
+
end
|
|
1045
|
+
elsif result.include?('ImportError')
|
|
1046
|
+
print_message("The ticket converter has import errors. Please ensure all required Python dependencies are installed.", TYPE_ERROR, true, $logger)
|
|
1047
|
+
print_message("For impacket scripts, run: pip3 install impacket", TYPE_INFO, true, $logger)
|
|
1048
|
+
custom_exit(1, false)
|
|
1049
|
+
elsif result.include?('Permission denied') || result.match?(/permission denied/i)
|
|
1050
|
+
print_message("Permission denied when executing ticket converter. Please check file permissions: #{converter_found}", TYPE_ERROR, true, $logger)
|
|
1051
|
+
custom_exit(1, false)
|
|
1052
|
+
else
|
|
1053
|
+
# Extract the most relevant error message (usually the last non-empty line)
|
|
1054
|
+
error_msg = error_lines.reverse.find { |line| !line.strip.empty? && !line.strip.match?(/^Traceback|File "/) }
|
|
1055
|
+
error_msg ||= error_lines.last || result.strip
|
|
1056
|
+
error_msg = error_msg.strip
|
|
1057
|
+
|
|
1058
|
+
# Limit error message length
|
|
1059
|
+
error_msg = error_msg[0..200] + '...' if error_msg.length > 200
|
|
1060
|
+
|
|
1061
|
+
print_message("Failed to convert kirbi to ccache using #{File.basename(converter_found)}.", TYPE_ERROR, true, $logger)
|
|
1062
|
+
print_message("Error: #{error_msg}", TYPE_ERROR, true, $logger)
|
|
1063
|
+
custom_exit(1, false)
|
|
1064
|
+
end
|
|
1065
|
+
end
|
|
1066
|
+
|
|
1067
|
+
unless File.exist?(ccache_path)
|
|
1068
|
+
print_message("Conversion completed but output file not found: #{ccache_path}", TYPE_ERROR, true, $logger)
|
|
1069
|
+
custom_exit(1, false)
|
|
1070
|
+
end
|
|
1071
|
+
|
|
1072
|
+
print_message("[+] Successfully converted to: #{ccache_path}", TYPE_SUCCESS, true, $logger)
|
|
1073
|
+
ccache_path
|
|
1074
|
+
end
|
|
1075
|
+
|
|
538
1076
|
# Main function
|
|
539
1077
|
def main
|
|
540
1078
|
arguments
|
|
1079
|
+
print_header
|
|
541
1080
|
connection_initialization
|
|
542
1081
|
file_manager = WinRM::FS::FileManager.new($conn)
|
|
543
|
-
print_header
|
|
544
1082
|
completion_check
|
|
545
1083
|
|
|
546
1084
|
# Log check
|
|
@@ -562,6 +1100,12 @@ class EvilWinRM
|
|
|
562
1100
|
print_message('Useless spn provided, only used for Kerberos auth', TYPE_WARNING, true, $logger)
|
|
563
1101
|
end
|
|
564
1102
|
|
|
1103
|
+
# Kerberos checks
|
|
1104
|
+
if !$ccache_file.nil? && $realm.nil?
|
|
1105
|
+
print_message("Realm (-r) is required when using ccache file (-K)", TYPE_ERROR, true, $logger)
|
|
1106
|
+
custom_exit(1, false)
|
|
1107
|
+
end
|
|
1108
|
+
|
|
565
1109
|
unless $scripts_path.nil?
|
|
566
1110
|
check_directories($scripts_path, 'scripts')
|
|
567
1111
|
@functions = read_scripts($scripts_path)
|
|
@@ -615,7 +1159,7 @@ class EvilWinRM
|
|
|
615
1159
|
if test_s.count(' ') < 2
|
|
616
1160
|
complete_path(str, shell) || []
|
|
617
1161
|
else
|
|
618
|
-
|
|
1162
|
+
self.paths(str) || []
|
|
619
1163
|
end
|
|
620
1164
|
when (Readline.line_buffer.empty? || !(Readline.line_buffer.include?(' ') || Readline.line_buffer =~ %r{^"?(\./|\.\./|[a-z,A-Z]:/|~/|/)}))
|
|
621
1165
|
result = $COMMANDS.grep(/^#{Regexp.escape(str)}/i) || []
|
|
@@ -633,13 +1177,72 @@ class EvilWinRM
|
|
|
633
1177
|
Readline.completion_case_fold = true
|
|
634
1178
|
Readline.completer_quote_characters = '"'
|
|
635
1179
|
|
|
1180
|
+
# Configure Ctrl+L to clear screen
|
|
1181
|
+
if Readline.respond_to?(:emacs_editing_mode)
|
|
1182
|
+
Readline.emacs_editing_mode
|
|
1183
|
+
end
|
|
1184
|
+
|
|
1185
|
+
# Set up Ctrl+L binding to clear screen
|
|
1186
|
+
begin
|
|
1187
|
+
if Readline.respond_to?(:bind_key)
|
|
1188
|
+
Readline.bind_key("\C-l") do
|
|
1189
|
+
clear_screen
|
|
1190
|
+
Readline.refresh_line
|
|
1191
|
+
nil
|
|
1192
|
+
end
|
|
1193
|
+
end
|
|
1194
|
+
rescue => e
|
|
1195
|
+
# If binding fails, Ctrl+L will work at terminal level
|
|
1196
|
+
end
|
|
1197
|
+
|
|
1198
|
+
# Load history for this host/user
|
|
1199
|
+
load_history
|
|
1200
|
+
|
|
636
1201
|
until command == 'exit' do
|
|
637
|
-
|
|
1202
|
+
begin
|
|
1203
|
+
pwd = shell.run('(get-location).path').output.strip
|
|
1204
|
+
rescue => e
|
|
1205
|
+
# Handle connection/timeout errors when getting pwd
|
|
1206
|
+
error_msg = e.message.to_s.downcase
|
|
1207
|
+
if error_msg.include?('timeout') || error_msg.include?('connection') ||
|
|
1208
|
+
error_msg.include?('closed') || error_msg.include?('broken') ||
|
|
1209
|
+
e.class.to_s.include?('Timeout') || e.class.to_s.include?('Connection')
|
|
1210
|
+
puts
|
|
1211
|
+
print_message("Connection timeout or error occurred: #{e.class} - #{e.message}", TYPE_ERROR, true, $logger)
|
|
1212
|
+
print_message("Cleaning up and exiting...", TYPE_WARNING, true, $logger)
|
|
1213
|
+
# Clean up KRB5CCNAME before exiting
|
|
1214
|
+
begin
|
|
1215
|
+
if defined?($original_krb5ccname) && !$original_krb5ccname.nil?
|
|
1216
|
+
ENV['KRB5CCNAME'] = $original_krb5ccname
|
|
1217
|
+
elsif defined?($original_krb5ccname) && $original_krb5ccname.nil?
|
|
1218
|
+
ENV.delete('KRB5CCNAME') if ENV.key?('KRB5CCNAME')
|
|
1219
|
+
end
|
|
1220
|
+
rescue => cleanup_error
|
|
1221
|
+
# Ignore cleanup errors
|
|
1222
|
+
end
|
|
1223
|
+
custom_exit(1, false)
|
|
1224
|
+
else
|
|
1225
|
+
# For other errors, try to continue with a default pwd
|
|
1226
|
+
pwd = "C:\\"
|
|
1227
|
+
end
|
|
1228
|
+
end
|
|
1229
|
+
|
|
638
1230
|
if $colors_enabled
|
|
639
1231
|
command = Readline.readline( "#{colorize('*Evil-WinRM*', 'red')}#{colorize(' PS ', 'yellow')}#{pwd}> ", true)
|
|
640
1232
|
else
|
|
641
1233
|
command = Readline.readline("*Evil-WinRM* PS #{pwd}> ", true)
|
|
642
1234
|
end
|
|
1235
|
+
|
|
1236
|
+
# Handle Ctrl+L if it returns as empty or special character
|
|
1237
|
+
if command == "\f" || (command.nil? && Readline.line_buffer.empty?)
|
|
1238
|
+
clear_screen
|
|
1239
|
+
command = ''
|
|
1240
|
+
next
|
|
1241
|
+
end
|
|
1242
|
+
|
|
1243
|
+
# Save command to history file
|
|
1244
|
+
save_to_history(command) if command && !command.strip.empty?
|
|
1245
|
+
|
|
643
1246
|
$logger&.info("*Evil-WinRM* PS #{pwd} > #{command}")
|
|
644
1247
|
|
|
645
1248
|
if command.start_with?('upload')
|
|
@@ -658,11 +1261,10 @@ class EvilWinRM
|
|
|
658
1261
|
source_s = paths.pop
|
|
659
1262
|
end
|
|
660
1263
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
source_s = Dir.pwd + '/' + source_s
|
|
1264
|
+
# Resolve relative paths correctly, including paths with ../
|
|
1265
|
+
unless source_s.match(/^[a-zA-Z]:[\\\/]/) || source_s.match(/^\/\//)
|
|
1266
|
+
# If it's a relative path, expand it from current directory
|
|
1267
|
+
source_s = File.expand_path(source_s, Dir.pwd)
|
|
666
1268
|
end
|
|
667
1269
|
|
|
668
1270
|
source_expr_i = source_s.index(/(\*\.|\*\*|\.\*|\*)/) || -1
|
|
@@ -688,10 +1290,17 @@ class EvilWinRM
|
|
|
688
1290
|
sources = []
|
|
689
1291
|
|
|
690
1292
|
if source_expr_i == -1
|
|
1293
|
+
# Validate file exists and is readable before upload
|
|
1294
|
+
unless File.exist?(source_s)
|
|
1295
|
+
raise "Source file does not exist: #{source_s}"
|
|
1296
|
+
end
|
|
1297
|
+
unless File.readable?(source_s)
|
|
1298
|
+
raise "Source file is not readable: #{source_s}"
|
|
1299
|
+
end
|
|
691
1300
|
sources.push(source_s)
|
|
692
1301
|
else
|
|
693
1302
|
Dir[source_s].each do |filename|
|
|
694
|
-
sources.push(filename)
|
|
1303
|
+
sources.push(filename) if File.exist?(filename) && File.readable?(filename)
|
|
695
1304
|
end
|
|
696
1305
|
if sources.length > 0
|
|
697
1306
|
shell.run("mkdir #{dest_s} -ErrorAction SilentlyContinue")
|
|
@@ -882,21 +1491,50 @@ class EvilWinRM
|
|
|
882
1491
|
load_ETW_patch(shell)
|
|
883
1492
|
@Bypass_4MSI_loaded = true
|
|
884
1493
|
end
|
|
1494
|
+
elsif command.strip.downcase == 'clear' || command.strip.downcase == 'cls'
|
|
1495
|
+
command = ''
|
|
1496
|
+
clear_screen
|
|
885
1497
|
end
|
|
886
1498
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1499
|
+
begin
|
|
1500
|
+
output = shell.run(command) do |stdout, stderr|
|
|
1501
|
+
stdout&.each_line do |line|
|
|
1502
|
+
$stdout.puts(line.rstrip)
|
|
1503
|
+
end
|
|
1504
|
+
$stderr.print(stderr)
|
|
890
1505
|
end
|
|
891
|
-
$stderr.print(stderr)
|
|
892
|
-
end
|
|
893
1506
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1507
|
+
next unless !$logger.nil? && !command.empty?
|
|
1508
|
+
output_logger = ''
|
|
1509
|
+
output.output.each_line do |line|
|
|
1510
|
+
output_logger += "#{line.rstrip!}\n"
|
|
1511
|
+
end
|
|
1512
|
+
$logger.info(output_logger)
|
|
1513
|
+
rescue => e
|
|
1514
|
+
# Handle connection/timeout errors gracefully
|
|
1515
|
+
error_msg = e.message.to_s.downcase
|
|
1516
|
+
if error_msg.include?('timeout') || error_msg.include?('connection') ||
|
|
1517
|
+
error_msg.include?('closed') || error_msg.include?('broken') ||
|
|
1518
|
+
e.class.to_s.include?('Timeout') || e.class.to_s.include?('Connection')
|
|
1519
|
+
puts
|
|
1520
|
+
print_message("Connection timeout or error occurred: #{e.class} - #{e.message}", TYPE_ERROR, true, $logger)
|
|
1521
|
+
print_message("Cleaning up and exiting...", TYPE_WARNING, true, $logger)
|
|
1522
|
+
# Clean up KRB5CCNAME before exiting
|
|
1523
|
+
begin
|
|
1524
|
+
if defined?($original_krb5ccname) && !$original_krb5ccname.nil?
|
|
1525
|
+
ENV['KRB5CCNAME'] = $original_krb5ccname
|
|
1526
|
+
elsif defined?($original_krb5ccname) && $original_krb5ccname.nil?
|
|
1527
|
+
ENV.delete('KRB5CCNAME') if ENV.key?('KRB5CCNAME')
|
|
1528
|
+
end
|
|
1529
|
+
rescue => cleanup_error
|
|
1530
|
+
# Ignore cleanup errors
|
|
1531
|
+
end
|
|
1532
|
+
custom_exit(1, false)
|
|
1533
|
+
else
|
|
1534
|
+
# Re-raise other errors
|
|
1535
|
+
raise
|
|
1536
|
+
end
|
|
898
1537
|
end
|
|
899
|
-
$logger.info(output_logger)
|
|
900
1538
|
end
|
|
901
1539
|
rescue Errno::EACCES => e
|
|
902
1540
|
puts
|
|
@@ -919,8 +1557,34 @@ class EvilWinRM
|
|
|
919
1557
|
print_message("Check your /etc/hosts file to ensure you can resolve #{$host}", TYPE_ERROR, true, $logger)
|
|
920
1558
|
custom_exit(1)
|
|
921
1559
|
rescue Exception => e
|
|
922
|
-
|
|
923
|
-
|
|
1560
|
+
# Check if it's a Kerberos ticket expired error
|
|
1561
|
+
error_class = e.class.to_s
|
|
1562
|
+
error_message = e.message.to_s
|
|
1563
|
+
|
|
1564
|
+
# Detect GSSAPI/GSS errors related to expired tickets
|
|
1565
|
+
error_message_lower = error_message.downcase
|
|
1566
|
+
is_gss_error = (error_class.include?('GSSAPI') || error_class.include?('GssApi') || error_class.include?('GSS'))
|
|
1567
|
+
is_expired_error = (error_message_lower.include?('ticket expired') ||
|
|
1568
|
+
(error_message_lower.include?('expired') && error_message_lower.include?('ticket')) ||
|
|
1569
|
+
(error_message_lower.include?('kerberos') && error_message_lower.include?('expired')))
|
|
1570
|
+
|
|
1571
|
+
if is_gss_error && is_expired_error
|
|
1572
|
+
print_message("Kerberos ticket expired. The ticket file provided is no longer valid. Please generate a new Kerberos ticket and try again.", TYPE_ERROR, true, $logger)
|
|
1573
|
+
# Clean up KRB5CCNAME before exiting
|
|
1574
|
+
begin
|
|
1575
|
+
if defined?($original_krb5ccname) && !$original_krb5ccname.nil?
|
|
1576
|
+
ENV['KRB5CCNAME'] = $original_krb5ccname
|
|
1577
|
+
elsif defined?($original_krb5ccname) && $original_krb5ccname.nil?
|
|
1578
|
+
ENV.delete('KRB5CCNAME') if ENV.key?('KRB5CCNAME')
|
|
1579
|
+
end
|
|
1580
|
+
rescue => cleanup_error
|
|
1581
|
+
# Ignore cleanup errors
|
|
1582
|
+
end
|
|
1583
|
+
custom_exit(1, false)
|
|
1584
|
+
else
|
|
1585
|
+
print_message("An error of type #{e.class} happened, message is #{e.message}", TYPE_ERROR, true, $logger)
|
|
1586
|
+
custom_exit(1)
|
|
1587
|
+
end
|
|
924
1588
|
end
|
|
925
1589
|
end
|
|
926
1590
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: evil-winrm
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: '3.
|
|
4
|
+
version: '3.8'
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- CyberVaca
|
|
@@ -11,8 +11,36 @@ authors:
|
|
|
11
11
|
autorequire:
|
|
12
12
|
bindir: bin
|
|
13
13
|
cert_chain: []
|
|
14
|
-
date:
|
|
14
|
+
date: 2025-12-07 00:00:00.000000000 Z
|
|
15
15
|
dependencies:
|
|
16
|
+
- !ruby/object:Gem::Dependency
|
|
17
|
+
name: benchmark
|
|
18
|
+
requirement: !ruby/object:Gem::Requirement
|
|
19
|
+
requirements:
|
|
20
|
+
- - ">="
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: 0.1.0
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: 0.1.0
|
|
30
|
+
- !ruby/object:Gem::Dependency
|
|
31
|
+
name: csv
|
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
|
33
|
+
requirements:
|
|
34
|
+
- - ">="
|
|
35
|
+
- !ruby/object:Gem::Version
|
|
36
|
+
version: 2.4.8
|
|
37
|
+
type: :runtime
|
|
38
|
+
prerelease: false
|
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: 2.4.8
|
|
16
44
|
- !ruby/object:Gem::Dependency
|
|
17
45
|
name: fileutils
|
|
18
46
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -61,6 +89,20 @@ dependencies:
|
|
|
61
89
|
- - "~>"
|
|
62
90
|
- !ruby/object:Gem::Version
|
|
63
91
|
version: '3.0'
|
|
92
|
+
- !ruby/object:Gem::Dependency
|
|
93
|
+
name: syslog
|
|
94
|
+
requirement: !ruby/object:Gem::Requirement
|
|
95
|
+
requirements:
|
|
96
|
+
- - ">="
|
|
97
|
+
- !ruby/object:Gem::Version
|
|
98
|
+
version: 2.1.0
|
|
99
|
+
type: :runtime
|
|
100
|
+
prerelease: false
|
|
101
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
102
|
+
requirements:
|
|
103
|
+
- - ">="
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: 2.1.0
|
|
64
106
|
- !ruby/object:Gem::Dependency
|
|
65
107
|
name: winrm
|
|
66
108
|
requirement: !ruby/object:Gem::Requirement
|