Skip to content

Commit 74f5012

Browse files
authored
Preserve custom Host headers for WebSocket requests (#6172)
Co-authored-by: cuteNBD <206630735+bingdongni@users.noreply.github.com>
1 parent bf8b995 commit 74f5012

3 files changed

Lines changed: 95 additions & 2 deletions

File tree

‎lib/request/connect.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,8 @@ def getPage(**kwargs):
521521

522522
# Prepare HTTP headers
523523
headers = forgeHeaders({HTTP_HEADER.COOKIE: cookie, HTTP_HEADER.USER_AGENT: ua, HTTP_HEADER.REFERER: referer, HTTP_HEADER.HOST: host or getHeader(dict(conf.httpHeaders), HTTP_HEADER.HOST) or getHostHeader(url)}, base=None if target else {})
524+
# WebSocket builds its own default Host, but must preserve explicit overrides.
525+
customWsHost = webSocket and bool(host or getHeader(dict(conf.httpHeaders), HTTP_HEADER.HOST) or getHeader(auxHeaders, HTTP_HEADER.HOST) is not None)
524526

525527
if HTTP_HEADER.COOKIE in headers:
526528
cookie = headers[HTTP_HEADER.COOKIE]
@@ -573,9 +575,13 @@ def getPage(**kwargs):
573575
if ':' in line:
574576
header, value = line.split(':', 1)
575577
headers[header] = value
578+
if webSocket and header.upper() == HTTP_HEADER.HOST.upper():
579+
headers[HTTP_HEADER.HOST] = value
580+
customWsHost = True
576581

577582
if conf.localhost:
578583
headers[HTTP_HEADER.HOST] = "localhost"
584+
customWsHost = True
579585

580586
for key, value in list(headers.items()):
581587
if key.upper() == HTTP_HEADER.ACCEPT_ENCODING.upper():
@@ -600,7 +606,7 @@ def getPage(**kwargs):
600606
try:
601607
ws.settimeout(WEBSOCKET_INITIAL_TIMEOUT if kb.webSocketRecvCount is None else timeout)
602608
wsHeaders = tuple("%s: %s" % (getUnicode(key), getUnicode(value)) for key, value in headers.items() if getUnicode(key).upper() != HTTP_HEADER.HOST.upper())
603-
ws.connect(url, header=wsHeaders, cookie=cookie) # WebSocket will add Host field of headers automatically
609+
ws.connect(url, header=wsHeaders, cookie=cookie, host_header=getHeader(headers, HTTP_HEADER.HOST) if customWsHost else None)
604610
ws.send(urldecode(post or ""))
605611

606612
_page = []

‎lib/request/websocket.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def settimeout(self, timeout):
5858
if self.sock is not None:
5959
self.sock.settimeout(timeout)
6060

61-
def connect(self, url, header=None, cookie=None):
61+
def connect(self, url, header=None, cookie=None, host_header=None):
6262
parts = _urllib.parse.urlsplit(url)
6363
secure = parts.scheme == "wss"
6464
host = parts.hostname
@@ -75,6 +75,8 @@ def connect(self, url, header=None, cookie=None):
7575
hostport = "[%s]" % host if ":" in host else host # bracket IPv6 literals
7676
if port != (443 if secure else 80): # Note: only the scheme's own default port gets omitted
7777
hostport = "%s:%d" % (hostport, port)
78+
if host_header is not None:
79+
hostport = getText(host_header)
7880

7981
key = getText(base64.b64encode(os.urandom(16)))
8082
lines = ["GET %s HTTP/1.1" % resource,

‎tests/test_websocket.py‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
OPCODE_PING,
3434
OPCODE_CLOSE,
3535
)
36+
from lib.request.connect import Connect
37+
38+
_GET_PAGE = Connect.__dict__["getPage"]
3639

3740

3841
class _FakeSock(object):
@@ -162,6 +165,88 @@ def sendall(self, data):
162165
finally:
163166
W.socket.create_connection = original
164167

168+
def test_getpage_preserves_host_header(self):
169+
import socket
170+
import tempfile
171+
import lib.request.websocket as W
172+
from lib.core.data import conf, kb
173+
from lib.core.option import _setKnowledgeBaseAttributes
174+
from lib.core.threads import getCurrentThreadData
175+
176+
class _HandshakeSock(_FakeSock):
177+
def __init__(self):
178+
_FakeSock.__init__(self)
179+
self.handshake = None
180+
self.response = b""
181+
182+
def settimeout(self, value):
183+
pass
184+
185+
def sendall(self, data):
186+
if self.handshake is None:
187+
self.handshake = data
188+
key = next(line.split(b":", 1)[1].strip() for line in data.split(b"\r\n") if line.lower().startswith(b"sec-websocket-key:"))
189+
accept = base64.b64encode(hashlib.sha1(key + _GUID.encode("ascii")).digest())
190+
self.response = b"HTTP/1.1 101 Switching Protocols\r\nSec-WebSocket-Accept: " + accept + b"\r\n\r\n" + _serverFrame("ok")
191+
192+
def recv(self, size):
193+
result, self.response = self.response[:size], self.response[size:]
194+
return result
195+
196+
sockets = []
197+
def create_socket(*args, **kwargs):
198+
sock = _HandshakeSock()
199+
sockets.append(sock)
200+
return sock
201+
202+
oldConf, oldKb = dict(conf), dict(kb)
203+
threadData = getCurrentThreadData()
204+
oldThreadData = dict(threadData.__dict__)
205+
oldTimeout = socket.getdefaulttimeout()
206+
original = W.socket.create_connection
207+
oldGetPage = Connect.__dict__["getPage"]
208+
fd, headersFile = tempfile.mkstemp(prefix="sqlmap-ws-headers-")
209+
try:
210+
with os.fdopen(fd, "w") as f:
211+
f.write("hOsT: file.example\n")
212+
threadData.__dict__.clear()
213+
threadData.reset()
214+
_setKnowledgeBaseAttributes(False)
215+
kb.wizardMode, kb.webSocketRecvCount = True, 1
216+
conf.url = "ws://example.test:8080/test"
217+
conf.hostname, conf.scheme, conf.port, conf.path = "example.test", "ws", 8080, "/test"
218+
conf.parameters, conf.data = {}, None
219+
conf.abortCode, conf.ignoreCode = [], []
220+
conf.timeout, conf.batch, conf.keepAlive = 3, True, False
221+
conf.retryOn, conf.ignoreRedirects, conf.scope = None, False, None
222+
W.socket.create_connection = create_socket
223+
Connect.getPage = _GET_PAGE
224+
cases = (
225+
("ws://example.test:8080/test", [], None, None, None, False, "example.test:8080"),
226+
("ws://user:pass@example.test:8080/test", [], None, None, None, False, "example.test:8080"),
227+
("ws://example.test:8080/test", [('hOsT', 'configured.example')], None, None, None, False, "configured.example"),
228+
("ws://example.test:8080/test", [('Host', 'configured.example')], "override.example:8443", None, None, False, "override.example:8443"),
229+
("ws://example.test:8080/test", [], None, {'Host': 'aux.example'}, None, False, "aux.example"),
230+
("ws://example.test:8080/test", [], None, None, headersFile, False, "file.example"),
231+
("ws://example.test:8080/test", [], None, None, None, True, "localhost"),
232+
)
233+
for url, headers, host, aux, path, localhost, expected in cases:
234+
conf.url = url
235+
conf.httpHeaders = headers
236+
conf.localhost, kb.headersFile = localhost, path
237+
self.assertEqual(Connect.getPage(url=url, host=host, auxHeaders=aux)[0], "ok")
238+
hostLines = [line for line in sockets[-1].handshake.split(b"\r\n") if line.lower().startswith(b"host:")]
239+
self.assertEqual(len(hostLines), 1)
240+
self.assertEqual(hostLines[0].split(b":", 1)[1].strip(), expected.encode("ascii"))
241+
finally:
242+
Connect.getPage = oldGetPage
243+
W.socket.create_connection = original
244+
socket.setdefaulttimeout(oldTimeout)
245+
conf.clear(); conf.update(oldConf)
246+
kb.clear(); kb.update(oldKb)
247+
threadData.__dict__.clear(); threadData.__dict__.update(oldThreadData)
248+
os.unlink(headersFile)
249+
165250

166251
if __name__ == "__main__":
167252
unittest.main()

0 commit comments

Comments
 (0)