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.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/evil-winrm.rb +692 -28
  3. metadata +44 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 415e9194d397ebfd14843c3dd62d3a5db61f79eed2329a579cef3c15cf30495a
4
- data.tar.gz: 01e3d0accc75bafeb73724438e463cd6123caea3c36f1f6daeea1be3076c6673
3
+ metadata.gz: b563b633495483fcaa8b94925479d4adb6da542a87cbde37f11c93f5fa068188
4
+ data.tar.gz: 3b131febdc2255e94df04e5c80049fb7e200da32b2af59c62d9532453c514cc9
5
5
  SHA512:
6
- metadata.gz: 64d925c647e495df4cb60104c349be1b225a48d6706a1f22fade92cca8a9fcf284f7bca08a6c4e45fb5d598dbb54344b42a734382f290236fc3d691991445843
7
- data.tar.gz: 6e000628cc0bd7b8d426e81573611a57869192ab7a511d5057d75110e535420dca15cd6657750700bfd9e41c5e84c5f7a6b0ff023f2c380ab3bb8c6cf3460bb8
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.7'
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
- exit(exit_code)
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
- paths = self.paths(str)
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
- pwd = shell.run('(get-location).path').output.strip
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
- unless source_s.match(Dir.pwd) then
662
- if source_s.match(/^\.[\\\/]/)
663
- source_s = source_s.gsub(/^\.[\\\/]/, "")
664
- end
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
- output = shell.run(command) do |stdout, stderr|
888
- stdout&.each_line do |line|
889
- $stdout.puts(line.rstrip)
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
- next unless !$logger.nil? && !command.empty?
895
- output_logger = ''
896
- output.output.each_line do |line|
897
- output_logger += "#{line.rstrip!}\n"
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
- print_message("An error of type #{e.class} happened, message is #{e.message}", TYPE_ERROR, true, $logger)
923
- custom_exit(1)
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.7'
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: 2024-10-14 00:00:00.000000000 Z
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