Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
beefproject
GitHub Repository: beefproject/beef
Path: blob/master/extensions/proxy/proxy.rb
1154 views
1
#
2
# Copyright (c) 2006-2025 Wade Alcorn - [email protected]
3
# Browser Exploitation Framework (BeEF) - https://beefproject.com
4
# See the file 'doc/COPYING' for copying permission
5
#
6
require 'openssl'
7
8
module BeEF
9
module Extension
10
module Proxy
11
class Proxy
12
HB = BeEF::Core::Models::HookedBrowser
13
H = BeEF::Core::Models::Http
14
@response = nil
15
16
# Multi-threaded Tunneling Proxy: listens on host:port configured in extensions/proxy/config.yaml
17
# and forwards requests to the hooked browser using the Requester component.
18
def initialize
19
@conf = BeEF::Core::Configuration.instance
20
@proxy_server = TCPServer.new(@conf.get('beef.extension.proxy.address'), @conf.get('beef.extension.proxy.port'))
21
22
# setup proxy for SSL/TLS
23
ssl_context = OpenSSL::SSL::SSLContext.new
24
# ssl_context.ssl_version = :TLSv1_2
25
26
# load certificate
27
begin
28
cert_file = @conf.get('beef.extension.proxy.cert')
29
cert = File.read(cert_file)
30
ssl_context.cert = OpenSSL::X509::Certificate.new(cert)
31
rescue StandardError
32
print_error "[Proxy] Could not load SSL certificate '#{cert_file}'"
33
end
34
35
# load key
36
begin
37
key_file = @conf.get('beef.extension.proxy.key')
38
key = File.read(key_file)
39
ssl_context.key = OpenSSL::PKey::RSA.new(key)
40
rescue StandardError
41
print_error "[Proxy] Could not load SSL key '#{key_file}'"
42
end
43
44
ssl_server = OpenSSL::SSL::SSLServer.new(@proxy_server, ssl_context)
45
ssl_server.start_immediately = false
46
47
loop do
48
ssl_socket = ssl_server.accept
49
Thread.new ssl_socket, &method(:handle_request)
50
end
51
end
52
53
def handle_request(socket)
54
request_line = socket.readline
55
56
# HTTP method # defaults to GET
57
method = request_line[/^\w+/]
58
59
# Handle SSL requests
60
url_prefix = ''
61
if method == 'CONNECT'
62
# request_line is something like:
63
# CONNECT example.com:443 HTTP/1.1
64
host_port = request_line.split[1]
65
proto = 'https'
66
url_prefix = "#{proto}://#{host_port}"
67
loop do
68
line = socket.readline
69
break if line.strip.empty?
70
end
71
socket.puts("HTTP/1.0 200 Connection established\r\n\r\n")
72
socket.accept
73
print_debug("[PROXY] Handled CONNECT to #{host_port}")
74
request_line = socket.readline
75
end
76
77
method, _path, version = request_line.split
78
79
# HTTP scheme/protocol # defaults to http
80
proto = 'http' unless proto.eql?('https')
81
82
# HTTP version # defaults to 1.0
83
version = 'HTTP/1.0' if version !~ %r{\AHTTP/\d\.\d\z}
84
85
# HTTP request path
86
path = request_line[/^\w+\s+(\S+)/, 1]
87
88
# url # proto://host:port + path
89
url = url_prefix + path
90
91
# We're overwriting the URI::Parser UNRESERVED regex to prevent BAD URI errors
92
# when sending attack vectors (see tolerant_parser)
93
# anti: somehow the config below was removed, have a look into this
94
tolerant_parser = URI::Parser.new(UNRESERVED: BeEF::Core::Configuration.instance.get('beef.extension.requester.uri_unreserved_chars'))
95
uri = tolerant_parser.parse(url.to_s)
96
97
uri_path_and_qs = uri.query.nil? ? uri.path : "#{uri.path}?#{uri.query}"
98
99
# extensions/requester/api/hook.rb parses raw_request to find port and path
100
raw_request = "#{[method, uri_path_and_qs, version].join(' ')}\r\n"
101
content_length = 0
102
103
loop do
104
line = socket.readline
105
106
content_length = Regexp.last_match(1).to_i if line =~ /^Content-Length:\s+(\d+)\s*$/
107
108
if line.strip.empty?
109
# read data still in the socket, exactly <content_length> bytes
110
raw_request += "\r\n#{socket.read(content_length)}" if content_length >= 0
111
break
112
else
113
raw_request += line
114
end
115
end
116
117
# Saves the new HTTP request to the db. It will be processed by the PreHookCallback of the requester component.
118
# IDs are created and incremented automatically by DataMapper.
119
http = H.new(
120
request: raw_request,
121
method: method,
122
proto: proto,
123
domain: uri.host,
124
port: uri.port,
125
path: uri_path_and_qs,
126
request_date: Time.now,
127
hooked_browser_id: get_tunneling_proxy,
128
allow_cross_origin: 'true'
129
)
130
http.save
131
print_debug(
132
"[PROXY] --> Forwarding request ##{http.id}: " \
133
"domain[#{http.domain}:#{http.port}], " \
134
"method[#{http.method}], " \
135
"path[#{http.path}], " \
136
"cross origin[#{http.allow_cross_origin}]"
137
)
138
139
# Wait for the HTTP response to be stored in the db.
140
# TODO: re-implement this with EventMachine or with the Observer pattern.
141
sleep 0.5 while H.find(http.id).has_ran != 'complete'
142
@response = H.find(http.id)
143
print_debug "[PROXY] <-- Response for request ##{@response.id} to [#{@response.path}] on domain [#{@response.domain}:#{@response.port}] correctly processed"
144
145
response_body = @response['response_data']
146
response_status = @response['response_status_code']
147
headers = @response['response_headers']
148
149
# The following is needed to forward back some of the original HTTP response headers obtained via XHR calls.
150
# Original XHR response headers are stored in extension_requester_http table (response_headers column),
151
# but we are forwarding back only some of them (Server, X-.. - like X-Powered-By -, Content-Type, ... ).
152
# Some of the original response headers need to be removed, like encoding and cache related: for example
153
# about encoding, the original response headers says that the content-length is 1000 as the response is gzipped,
154
# but the final content-length forwarded back by the proxy is clearly bigger. Date header follows the same way.
155
response_headers = ''
156
if response_status != -1 && response_status != 0
157
ignore_headers = %w[
158
Content-Encoding
159
Keep-Alive
160
Cache-Control
161
Vary
162
Pragma
163
Connection
164
Expires
165
Accept-Ranges
166
Transfer-Encoding
167
Date
168
]
169
headers.each_line do |line|
170
# stripping the Encoding, Cache and other headers
171
header_key = line.split(': ')[0]
172
header_value = line.split(': ')[1]
173
next if header_key.nil?
174
next if ignore_headers.any? { |h| h.casecmp(header_key).zero? }
175
176
# ignore headers with no value (@todo: why?)
177
next if header_value.nil?
178
179
unless header_key == 'Content-Length'
180
response_headers += line
181
next
182
end
183
184
# update Content-Length with the valid one
185
response_headers += "Content-Length: #{response_body.size}\r\n"
186
end
187
end
188
189
res = "#{version} #{response_status}\r\n#{response_headers}\r\n#{response_body}"
190
socket.write(res)
191
socket.close
192
end
193
194
def get_tunneling_proxy
195
proxy_browser = HB.where(is_proxy: true).first
196
return proxy_browser.session.to_s unless proxy_browser.nil?
197
198
hooked_browser = HB.first
199
unless hooked_browser.nil?
200
print_debug "[Proxy] Proxy browser not set. Defaulting to first hooked browser [id: #{hooked_browser.session}]"
201
return hooked_browser.session
202
end
203
204
print_error '[Proxy] No hooked browsers'
205
nil
206
end
207
end
208
end
209
end
210
end
211
212