Skip to content

Commit fc1ae69

Browse files
committed
Adding native websocket support
1 parent 4c12dda commit fc1ae69

6 files changed

Lines changed: 371 additions & 27 deletions

File tree

‎lib/core/option.py‎

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2698,15 +2698,6 @@ def putheader(self, header, *values):
26982698
_http_client.HTTPConnection._http_vsn = 10
26992699
_http_client.HTTPConnection._http_vsn_str = 'HTTP/1.0'
27002700

2701-
if conf.url and (conf.url.startswith("ws:/") or conf.url.startswith("wss:/")):
2702-
try:
2703-
from websocket import ABNF
2704-
ABNF # require websocket-client, not any 'websocket' module
2705-
except ImportError:
2706-
errMsg = "sqlmap requires third-party module 'websocket-client' "
2707-
errMsg += "in order to use WebSocket functionality"
2708-
raise SqlmapMissingDependence(errMsg)
2709-
27102701
def _checkTor():
27112702
if not conf.checkTor:
27122703
return

‎lib/core/settings.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from thirdparty import six
2121

2222
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
23-
VERSION = "1.10.7.56"
23+
VERSION = "1.10.7.57"
2424
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
2525
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
2626
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)

‎lib/request/connect.py‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,8 @@
1919
import time
2020
import traceback
2121

22-
try:
23-
import websocket
24-
from websocket import WebSocketException
25-
except ImportError:
26-
class WebSocketException(Exception):
27-
pass
22+
from lib.request import websocket
23+
from lib.request.websocket import WebSocketException
2824

2925
from lib.core.agent import agent
3026
from lib.core.common import asciifyUrl

‎lib/request/websocket.py‎

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
#!/usr/bin/env python
2+
3+
"""
4+
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
5+
See the file 'LICENSE' for copying permission
6+
"""
7+
8+
# Native, dependency-free WebSocket client (RFC 6455), replacing the 'websocket-client' third-party
9+
# library. Covers exactly what sqlmap needs: the client handshake, masked text framing, wss (TLS) and
10+
# the recv/timeout surface. It also removes the long-standing ambiguity with the unrelated 'websocket'
11+
# PyPI package (both expose a top-level 'websocket' module). Pure standard library, Python 2.7 / 3.x.
12+
13+
import base64
14+
import hashlib
15+
import os
16+
import socket
17+
import ssl
18+
import struct
19+
20+
from lib.core.convert import getBytes
21+
from lib.core.convert import getText
22+
from thirdparty.six.moves import urllib as _urllib
23+
24+
_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" # RFC 6455 magic value for the accept key
25+
26+
OPCODE_CONTINUATION = 0x0
27+
OPCODE_TEXT = 0x1
28+
OPCODE_BINARY = 0x2
29+
OPCODE_CLOSE = 0x8
30+
OPCODE_PING = 0x9
31+
OPCODE_PONG = 0xA
32+
33+
class WebSocketException(Exception):
34+
pass
35+
36+
class WebSocketTimeoutException(WebSocketException):
37+
pass
38+
39+
class WebSocketConnectionClosedException(WebSocketException):
40+
pass
41+
42+
class WebSocket(object):
43+
"""
44+
Minimal RFC 6455 client exposing the websocket-client subset used by sqlmap: settimeout(), connect(),
45+
send(), recv(), close(), the handshake .status and getheaders()
46+
"""
47+
48+
def __init__(self):
49+
self.sock = None
50+
self.status = None
51+
self._headers = {}
52+
self._timeout = None
53+
self._buffer = b""
54+
self._closed = False
55+
56+
def settimeout(self, timeout):
57+
self._timeout = timeout
58+
if self.sock is not None:
59+
self.sock.settimeout(timeout)
60+
61+
def connect(self, url, header=None, cookie=None):
62+
parts = _urllib.parse.urlsplit(url)
63+
secure = parts.scheme == "wss"
64+
host = parts.hostname
65+
port = parts.port or (443 if secure else 80)
66+
resource = parts.path or "/"
67+
if parts.query:
68+
resource += "?%s" % parts.query
69+
70+
self.sock = socket.create_connection((host, port), timeout=self._timeout)
71+
if secure:
72+
self.sock = ssl._create_unverified_context().wrap_socket(self.sock, server_hostname=host)
73+
self.sock.settimeout(self._timeout)
74+
75+
hostport = "[%s]" % host if ":" in host else host # bracket IPv6 literals
76+
if port not in (80, 443):
77+
hostport = "%s:%d" % (hostport, port)
78+
79+
key = getText(base64.b64encode(os.urandom(16)))
80+
lines = ["GET %s HTTP/1.1" % resource,
81+
"Host: %s" % hostport,
82+
"Upgrade: websocket",
83+
"Connection: Upgrade",
84+
"Sec-WebSocket-Key: %s" % key,
85+
"Sec-WebSocket-Version: 13"]
86+
for _ in (header or ()):
87+
lines.append(_)
88+
if cookie:
89+
lines.append("Cookie: %s" % cookie)
90+
lines.extend(("", ""))
91+
self.sock.sendall(getBytes("\r\n".join(lines)))
92+
93+
self._readHandshake(key)
94+
95+
def _readHandshake(self, key):
96+
while b"\r\n\r\n" not in self._buffer:
97+
chunk = self._recvSome()
98+
if not chunk:
99+
raise WebSocketException("incomplete WebSocket handshake response")
100+
self._buffer += chunk
101+
102+
head, _, self._buffer = self._buffer.partition(b"\r\n\r\n")
103+
lines = getText(head).split("\r\n")
104+
105+
try:
106+
self.status = int(lines[0].split(" ", 2)[1])
107+
except (IndexError, ValueError):
108+
raise WebSocketException("malformed WebSocket handshake response")
109+
110+
for line in lines[1:]:
111+
name, _, value = line.partition(":")
112+
self._headers[name.strip().lower()] = value.strip()
113+
114+
if self.status != 101:
115+
raise WebSocketException("Handshake status %d" % self.status) # 'Handshake status' is matched in connect.py
116+
117+
accept = getText(self._headers.get("sec-websocket-accept", ""))
118+
expected = getText(base64.b64encode(hashlib.sha1(getBytes(key + _GUID)).digest()))
119+
if accept != expected:
120+
raise WebSocketException("invalid 'Sec-WebSocket-Accept' in handshake response")
121+
122+
def getheaders(self):
123+
return dict(self._headers)
124+
125+
def send(self, payload, opcode=OPCODE_TEXT):
126+
self._sendFrame(getBytes(payload), opcode)
127+
128+
def _sendFrame(self, data, opcode):
129+
length = len(data)
130+
frame = bytearray()
131+
frame.append(0x80 | opcode) # FIN set, single (unfragmented) frame
132+
133+
if length < 126:
134+
frame.append(0x80 | length) # client frames must set the mask bit
135+
elif length < 65536:
136+
frame.append(0x80 | 126)
137+
frame += struct.pack("!H", length)
138+
else:
139+
frame.append(0x80 | 127)
140+
frame += struct.pack("!Q", length)
141+
142+
mask = bytearray(os.urandom(4))
143+
frame += mask
144+
payload = bytearray(data)
145+
for i in range(length):
146+
payload[i] ^= mask[i % 4]
147+
frame += payload
148+
149+
try:
150+
self.sock.sendall(bytes(frame))
151+
except socket.timeout:
152+
raise WebSocketTimeoutException("timed out while sending data")
153+
except ssl.SSLError as ex:
154+
if "timed out" in str(ex).lower():
155+
raise WebSocketTimeoutException("timed out while sending data")
156+
raise
157+
158+
def recv(self):
159+
data = bytearray()
160+
while True:
161+
fin, opcode, payload = self._recvFrame()
162+
if opcode == OPCODE_CLOSE:
163+
self._closed = True
164+
raise WebSocketConnectionClosedException("WebSocket connection closed")
165+
elif opcode == OPCODE_PING:
166+
self._sendFrame(bytes(payload), OPCODE_PONG)
167+
continue
168+
elif opcode == OPCODE_PONG:
169+
continue
170+
171+
data += payload
172+
if fin:
173+
break
174+
175+
return getText(bytes(data))
176+
177+
def _recvFrame(self):
178+
header = bytearray(self._readStrict(2))
179+
fin = (header[0] >> 7) & 1
180+
opcode = header[0] & 0x0F
181+
masked = (header[1] >> 7) & 1
182+
length = header[1] & 0x7F
183+
184+
if length == 126:
185+
length = struct.unpack("!H", self._readStrict(2))[0]
186+
elif length == 127:
187+
length = struct.unpack("!Q", self._readStrict(8))[0]
188+
189+
mask = bytearray(self._readStrict(4)) if masked else None
190+
payload = bytearray(self._readStrict(length))
191+
if mask is not None: # servers must not mask, but unmask defensively
192+
for i in range(length):
193+
payload[i] ^= mask[i % 4]
194+
195+
return fin, opcode, payload
196+
197+
def _readStrict(self, count):
198+
while len(self._buffer) < count:
199+
chunk = self._recvSome()
200+
if not chunk:
201+
raise WebSocketConnectionClosedException("WebSocket connection closed")
202+
self._buffer += chunk
203+
204+
result, self._buffer = self._buffer[:count], self._buffer[count:]
205+
return result
206+
207+
def _recvSome(self):
208+
try:
209+
return self.sock.recv(8192)
210+
except socket.timeout:
211+
raise WebSocketTimeoutException("timed out while receiving data")
212+
except ssl.SSLError as ex:
213+
# Python 2 raises ssl.SSLError('The read operation timed out') instead of socket.timeout on a
214+
# TLS read timeout - which is exactly sqlmap's normal "read frames until timeout" path over wss
215+
if "timed out" in str(ex).lower():
216+
raise WebSocketTimeoutException("timed out while receiving data")
217+
raise
218+
219+
def close(self):
220+
if self.sock is not None:
221+
try:
222+
if not self._closed:
223+
self._sendFrame(struct.pack("!H", 1000), OPCODE_CLOSE) # normal closure
224+
except (socket.error, WebSocketException):
225+
pass
226+
try:
227+
self.sock.close()
228+
except socket.error:
229+
pass
230+
self.sock = None

‎lib/utils/deps.py‎

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,6 @@ def checkDependencies():
8383
logger.warning(warnMsg)
8484
missing_libraries.add('python-impacket')
8585

86-
try:
87-
__import__("websocket._abnf")
88-
debugMsg = "'websocket-client' library is found"
89-
logger.debug(debugMsg)
90-
except ImportError:
91-
warnMsg = "sqlmap requires 'websocket-client' third-party library "
92-
warnMsg += "if you plan to attack a web application using WebSocket. "
93-
warnMsg += "Download from 'https://pypi.python.org/pypi/websocket-client/'"
94-
logger.warning(warnMsg)
95-
missing_libraries.add('websocket-client')
96-
9786
try:
9887
__import__("tkinter")
9988
debugMsg = "'tkinter' library is found"

0 commit comments

Comments
 (0)