rex-powershell 0.1.0

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.
@@ -0,0 +1,61 @@
1
+ # -*- coding: binary -*-
2
+
3
+ module Rex
4
+ module Powershell
5
+ class Function
6
+ FUNCTION_REGEX = Regexp.new(/\[(\w+\[\])\]\$(\w+)\s?=|\[(\w+)\]\$(\w+)\s?=|\[(\w+\[\])\]\s+?\$(\w+)\s+=|\[(\w+)\]\s+\$(\w+)\s?=/i)
7
+ PARAMETER_REGEX = Regexp.new(/param\s+\(|param\(/im)
8
+ attr_accessor :code, :name, :params
9
+
10
+ include Output
11
+ include Parser
12
+ include Obfu
13
+
14
+ def initialize(name, code)
15
+ @name = name
16
+ @code = code
17
+ populate_params
18
+ end
19
+
20
+ #
21
+ # To String
22
+ #
23
+ # @return [String] Powershell function
24
+ def to_s
25
+ "function #{name} #{code}"
26
+ end
27
+
28
+ #
29
+ # Identify the parameters from the code and
30
+ # store as Param in @params
31
+ #
32
+ def populate_params
33
+ @params = []
34
+ start = code.index(PARAMETER_REGEX)
35
+ return unless start
36
+ # Get start of our block
37
+ idx = scan_with_index('(', code[start..-1]).first.last + start
38
+ pclause = block_extract(idx)
39
+
40
+ matches = pclause.scan(FUNCTION_REGEX)
41
+
42
+ # Ignore assignment, create params with class and variable names
43
+ matches.each do |param|
44
+ klass = nil
45
+ name = nil
46
+ param.each do |value|
47
+ if value
48
+ if klass
49
+ name = value
50
+ @params << Param.new(klass, name)
51
+ break
52
+ else
53
+ klass = value
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,96 @@
1
+ # -*- coding: binary -*-
2
+
3
+ require 'rex/text'
4
+
5
+ module Rex
6
+ module Powershell
7
+ module Obfu
8
+ MULTI_LINE_COMMENTS_REGEX = Regexp.new(/<#(.*?)#>/m)
9
+ SINGLE_LINE_COMMENTS_REGEX = Regexp.new(/^\s*#(?!.*region)(.*$)/i)
10
+ WINDOWS_EOL_REGEX = Regexp.new(/[\r\n]+/)
11
+ UNIX_EOL_REGEX = Regexp.new(/[\n]+/)
12
+ WHITESPACE_REGEX = Regexp.new(/\s+/)
13
+ EMPTY_LINE_REGEX = Regexp.new(/^$|^\s+$/)
14
+
15
+ #
16
+ # Remove comments
17
+ #
18
+ # @return [String] code without comments
19
+ def strip_comments
20
+ # Multi line
21
+ code.gsub!(MULTI_LINE_COMMENTS_REGEX, '')
22
+ # Single line
23
+ code.gsub!(SINGLE_LINE_COMMENTS_REGEX, '')
24
+
25
+ code
26
+ end
27
+
28
+ #
29
+ # Remove empty lines
30
+ #
31
+ # @return [String] code without empty lines
32
+ def strip_empty_lines
33
+ # Windows EOL
34
+ code.gsub!(WINDOWS_EOL_REGEX, "\r\n")
35
+ # UNIX EOL
36
+ code.gsub!(UNIX_EOL_REGEX, "\n")
37
+
38
+ code
39
+ end
40
+
41
+ #
42
+ # Remove whitespace
43
+ # This can break some codes using inline .NET
44
+ #
45
+ # @return [String] code with whitespace stripped
46
+ def strip_whitespace
47
+ code.gsub!(WHITESPACE_REGEX, ' ')
48
+
49
+ code
50
+ end
51
+
52
+ #
53
+ # Identify variables and replace them
54
+ #
55
+ # @return [String] code with variable names replaced with unique values
56
+ def sub_vars
57
+ # Get list of variables, remove reserved
58
+ get_var_names.each do |var, _sub|
59
+ code.gsub!(var, "$#{@rig.init_var(var)}")
60
+ end
61
+
62
+ code
63
+ end
64
+
65
+ #
66
+ # Identify function names and replace them
67
+ #
68
+ # @return [String] code with function names replaced with unique
69
+ # values
70
+ def sub_funcs
71
+ # Find out function names, make map
72
+ get_func_names.each do |var, _sub|
73
+ code.gsub!(var, @rig.init_var(var))
74
+ end
75
+
76
+ code
77
+ end
78
+
79
+ #
80
+ # Perform standard substitutions
81
+ #
82
+ # @return [String] code with standard substitution methods applied
83
+ def standard_subs(subs = %w(strip_comments strip_whitespace sub_funcs sub_vars))
84
+ # Save us the trouble of breaking injected .NET and such
85
+ subs.delete('strip_whitespace') unless get_string_literals.empty?
86
+ # Run selected modifiers
87
+ subs.each do |modifier|
88
+ send(modifier)
89
+ end
90
+ code.gsub!(EMPTY_LINE_REGEX, '')
91
+
92
+ code
93
+ end
94
+ end # Obfu
95
+ end
96
+ end
@@ -0,0 +1,157 @@
1
+ # -*- coding: binary -*-
2
+
3
+ require 'zlib'
4
+ require 'rex/text'
5
+
6
+ module Rex
7
+ module Powershell
8
+ module Output
9
+ #
10
+ # To String
11
+ #
12
+ # @return [String] Code
13
+ def to_s
14
+ code
15
+ end
16
+
17
+ #
18
+ # Returns code size
19
+ #
20
+ # @return [Integer] Code size
21
+ def size
22
+ code.size
23
+ end
24
+
25
+ #
26
+ # Return code with numbered lines
27
+ #
28
+ # @return [String] Powershell code with line numbers
29
+ def to_s_lineno
30
+ numbered = ''
31
+ code.split(/\r\n|\n/).each_with_index do |line, idx|
32
+ numbered << "#{idx}: #{line}"
33
+ end
34
+
35
+ numbered
36
+ end
37
+
38
+ #
39
+ # Return a zlib compressed powershell code wrapped in decode stub
40
+ #
41
+ # @param eof [String] End of file identifier to append to code
42
+ #
43
+ # @return [String] Zlib compressed powershell code wrapped in
44
+ # decompression stub
45
+ def deflate_code(eof = nil)
46
+ # Compress using the Deflate algorithm
47
+ compressed_stream = ::Zlib::Deflate.deflate(code,
48
+ ::Zlib::BEST_COMPRESSION)
49
+
50
+ # Base64 encode the compressed file contents
51
+ encoded_stream = Rex::Text.encode_base64(compressed_stream)
52
+
53
+ # Build the powershell expression
54
+ # Decode base64 encoded command and create a stream object
55
+ psh_expression = "$s=New-Object IO.MemoryStream(,"
56
+ psh_expression << "[Convert]::FromBase64String('#{encoded_stream}'));"
57
+ # Read & delete the first two bytes due to incompatibility with MS
58
+ psh_expression << '$s.ReadByte();'
59
+ psh_expression << '$s.ReadByte();'
60
+ # Uncompress and invoke the expression (execute)
61
+ psh_expression << 'IEX (New-Object IO.StreamReader('
62
+ psh_expression << 'New-Object IO.Compression.DeflateStream('
63
+ psh_expression << '$s,'
64
+ psh_expression << '[IO.Compression.CompressionMode]::Decompress)'
65
+ psh_expression << ')).ReadToEnd();'
66
+
67
+ # If eof is set, add a marker to signify end of code output
68
+ # if (eof && eof.length == 8) then psh_expression += "'#{eof}'" end
69
+ psh_expression << "echo '#{eof}';" if eof
70
+
71
+ @code = psh_expression
72
+ end
73
+
74
+ #
75
+ # Return Base64 encoded powershell code
76
+ #
77
+ # @return [String] Base64 encoded powershell code
78
+ def encode_code(eof = nil)
79
+ @code = Rex::Text.encode_base64(Rex::Text.to_unicode(code))
80
+ end
81
+
82
+ #
83
+ # Return ASCII powershell code from base64/unicode
84
+ #
85
+ # @return [String] ASCII powershell code
86
+ def decode_code
87
+ @code = Rex::Text.to_ascii(Rex::Text.decode_base64(code))
88
+ end
89
+
90
+ #
91
+ # Return a gzip compressed powershell code wrapped in decoder stub
92
+ #
93
+ # @param eof [String] End of file identifier to append to code
94
+ #
95
+ # @return [String] Gzip compressed powershell code wrapped in
96
+ # decompression stub
97
+ def gzip_code(eof = nil)
98
+ # Compress using the Deflate algorithm
99
+ compressed_stream = Rex::Text.gzip(code)
100
+
101
+ # Base64 encode the compressed file contents
102
+ encoded_stream = Rex::Text.encode_base64(compressed_stream)
103
+
104
+ # Build the powershell expression
105
+ # Decode base64 encoded command and create a stream object
106
+ psh_expression = "$s=New-Object IO.MemoryStream(,"
107
+ psh_expression << "[Convert]::FromBase64String('#{encoded_stream}'));"
108
+ # Uncompress and invoke the expression (execute)
109
+ psh_expression << 'IEX (New-Object IO.StreamReader('
110
+ psh_expression << 'New-Object IO.Compression.GzipStream('
111
+ psh_expression << '$s,'
112
+ psh_expression << '[IO.Compression.CompressionMode]::Decompress)'
113
+ psh_expression << ')).ReadToEnd();'
114
+
115
+ # If eof is set, add a marker to signify end of code output
116
+ # if (eof && eof.length == 8) then psh_expression += "'#{eof}'" end
117
+ psh_expression << "echo '#{eof}';" if eof
118
+
119
+ @code = psh_expression
120
+ end
121
+
122
+ #
123
+ # Compresses script contents with gzip (default) or deflate
124
+ #
125
+ # @param eof [String] End of file identifier to append to code
126
+ # @param gzip [Boolean] Whether to use gzip compression or deflate
127
+ #
128
+ # @return [String] Compressed code wrapped in decompression stub
129
+ def compress_code(eof = nil, gzip = true)
130
+ @code = gzip ? gzip_code(eof) : deflate_code(eof)
131
+ end
132
+
133
+ #
134
+ # Reverse the compression process
135
+ # Try gzip, inflate if that fails
136
+ #
137
+ # @return [String] Decompressed powershell code
138
+ def decompress_code
139
+ # Extract substring with payload
140
+ encoded_stream = @code.scan(/FromBase64String\('(.*)'/).flatten.first
141
+ # Decode and decompress the string
142
+ unencoded = Rex::Text.decode_base64(encoded_stream)
143
+ begin
144
+ @code = Rex::Text.ungzip(unencoded) || Rex::Text.zlib_inflate(unencoded)
145
+ rescue Zlib::GzipFile::Error
146
+ begin
147
+ @code = Rex::Text.zlib_inflate(unencoded)
148
+ rescue Zlib::DataError => e
149
+ raise RuntimeError, 'Invalid compression'
150
+ end
151
+ end
152
+
153
+ @code
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,21 @@
1
+ # -*- coding: binary -*-
2
+
3
+ module Rex
4
+ module Powershell
5
+ class Param
6
+ attr_accessor :klass, :name
7
+ def initialize(klass, name)
8
+ @klass = klass.strip
9
+ @name = name.strip.gsub(/\s|,/, '')
10
+ end
11
+
12
+ #
13
+ # To String
14
+ #
15
+ # @return [String] Powershell param
16
+ def to_s
17
+ "[#{klass}]$#{name}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,182 @@
1
+ # -*- coding: binary -*-
2
+
3
+ module Rex
4
+ module Powershell
5
+ module Parser
6
+ # Reserved special variables
7
+ # Acquired with: Get-Variable | Format-Table name, value -auto
8
+ RESERVED_VARIABLE_NAMES = [
9
+ '$$',
10
+ '$?',
11
+ '$^',
12
+ '$_',
13
+ '$args',
14
+ '$ConfirmPreference',
15
+ '$ConsoleFileName',
16
+ '$DebugPreference',
17
+ '$Env',
18
+ '$Error',
19
+ '$ErrorActionPreference',
20
+ '$ErrorView',
21
+ '$ExecutionContext',
22
+ '$false',
23
+ '$FormatEnumerationLimit',
24
+ '$HOME',
25
+ '$Host',
26
+ '$input',
27
+ '$LASTEXITCODE',
28
+ '$MaximumAliasCount',
29
+ '$MaximumDriveCount',
30
+ '$MaximumErrorCount',
31
+ '$MaximumFunctionCount',
32
+ '$MaximumHistoryCount',
33
+ '$MaximumVariableCount',
34
+ '$MyInvocation',
35
+ '$NestedPromptLevel',
36
+ '$null',
37
+ '$OutputEncoding',
38
+ '$PID',
39
+ '$PROFILE',
40
+ '$ProgressPreference',
41
+ '$PSBoundParameters',
42
+ '$PSCulture',
43
+ '$PSEmailServer',
44
+ '$PSHOME',
45
+ '$PSSessionApplicationName',
46
+ '$PSSessionConfigurationName',
47
+ '$PSSessionOption',
48
+ '$PSUICulture',
49
+ '$PSVersionTable',
50
+ '$PWD',
51
+ '$ReportErrorShowExceptionClass',
52
+ '$ReportErrorShowInnerException',
53
+ '$ReportErrorShowSource',
54
+ '$ReportErrorShowStackTrace',
55
+ '$ShellId',
56
+ '$StackTrace',
57
+ '$true',
58
+ '$VerbosePreference',
59
+ '$WarningPreference',
60
+ '$WhatIfPreference'
61
+ ].map(&:downcase).freeze
62
+
63
+ #
64
+ # Get variable names from code, removes reserved names from return
65
+ #
66
+ # @return [Array] variable names
67
+ def get_var_names
68
+ our_vars = code.scan(/\$[a-zA-Z\-\_0-9]+/).uniq.flatten.map(&:strip)
69
+ our_vars.select { |v| !RESERVED_VARIABLE_NAMES.include?(v.downcase) }
70
+ end
71
+
72
+ #
73
+ # Get function names from code
74
+ #
75
+ # @return [Array] function names
76
+ def get_func_names
77
+ code.scan(/function\s([a-zA-Z\-\_0-9]+)/).uniq.flatten
78
+ end
79
+
80
+ #
81
+ # Attempt to find string literals in PSH expression
82
+ #
83
+ # @return [Array] string literals
84
+ def get_string_literals
85
+ code.scan(/@"(.+?)"@|@'(.+?)'@/m)
86
+ end
87
+
88
+ #
89
+ # Scan code and return matches with index
90
+ #
91
+ # @param str [String] string to match in code
92
+ # @param source [String] source code to match, defaults to @code
93
+ #
94
+ # @return [Array[String,Integer]] matched items with index
95
+ def scan_with_index(str, source = code)
96
+ ::Enumerator.new do |y|
97
+ source.scan(str) do
98
+ y << ::Regexp.last_match
99
+ end
100
+ end.map { |m| [m.to_s, m.offset(0)[0]] }
101
+ end
102
+
103
+ #
104
+ # Return matching bracket type
105
+ #
106
+ # @param char [String] opening bracket character
107
+ #
108
+ # @return [String] matching closing bracket
109
+ def match_start(char)
110
+ case char
111
+ when '{'
112
+ '}'
113
+ when '('
114
+ ')'
115
+ when '['
116
+ ']'
117
+ when '<'
118
+ '>'
119
+ else
120
+ fail ArgumentError, 'Unknown starting bracket'
121
+ end
122
+ end
123
+
124
+ #
125
+ # Extract block of code inside brackets/parenthesis
126
+ #
127
+ # Attempts to match the bracket at idx, handling nesting manually
128
+ # Once the balanced matching bracket is found, all script content
129
+ # between idx and the index of the matching bracket is returned
130
+ #
131
+ # @param idx [Integer] index of opening bracket
132
+ #
133
+ # @return [String] content between matching brackets
134
+ def block_extract(idx)
135
+ fail ArgumentError unless idx
136
+
137
+ if idx < 0 || idx >= code.length
138
+ fail ArgumentError, 'Invalid index'
139
+ end
140
+
141
+ start = code[idx]
142
+ stop = match_start(start)
143
+ delims = scan_with_index(/#{Regexp.escape(start)}|#{Regexp.escape(stop)}/, code[idx + 1..-1])
144
+ delims.map { |x| x[1] = x[1] + idx + 1 }
145
+ c = 1
146
+ sidx = nil
147
+ # Go through delims till we balance, get idx
148
+ while (c != 0) && (x = delims.shift)
149
+ sidx = x[1]
150
+ x[0] == stop ? c -= 1 : c += 1
151
+ end
152
+
153
+ code[idx..sidx]
154
+ end
155
+
156
+ #
157
+ # Extract a block of function code
158
+ #
159
+ # @param func_name [String] function name
160
+ # @param delete [Boolean] delete the function from the code
161
+ #
162
+ # @return [String] function block
163
+ def get_func(func_name, delete = false)
164
+ start = code.index(func_name)
165
+
166
+ return nil unless start
167
+
168
+ idx = code[start..-1].index('{') + start
169
+ func_txt = block_extract(idx)
170
+
171
+ if delete
172
+ delete_code = code[0..idx]
173
+ delete_code << code[(idx + func_txt.length)..-1]
174
+ @code = delete_code
175
+ end
176
+
177
+ Function.new(func_name, func_txt)
178
+ end
179
+ end # Parser
180
+ end
181
+ end
182
+