From 50fd6ce7f71ab6b14e41dce5edabef4e7b6c4f98 Mon Sep 17 00:00:00 2001 From: ricterz Date: Tue, 24 Mar 2015 10:30:38 +0800 Subject: [PATCH 1/5] add websocket support for parse url #1198 --- lib/core/common.py | 3 ++- lib/core/target.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/core/common.py b/lib/core/common.py index 62d2bb664..c6b57846d 100755 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -1248,7 +1248,8 @@ def parseTargetUrl(): errMsg += "on this platform" raise SqlmapGenericException(errMsg) - if not re.search("^http[s]*://", conf.url, re.I): + if not re.search("^http[s]*://", conf.url, re.I) and \ + not re.search("^ws[s]*://", conf.url, re.I): if ":443/" in conf.url: conf.url = "https://" + conf.url else: diff --git a/lib/core/target.py b/lib/core/target.py index bd6379e14..90d2a68bd 100644 --- a/lib/core/target.py +++ b/lib/core/target.py @@ -211,7 +211,7 @@ def _setRequestParams(): kb.processUserMarks = True if (kb.postHint and CUSTOM_INJECTION_MARK_CHAR in conf.data) else kb.processUserMarks - if re.search(URI_INJECTABLE_REGEX, conf.url, re.I) and not any(place in conf.parameters for place in (PLACE.GET, PLACE.POST)) and not kb.postHint and not CUSTOM_INJECTION_MARK_CHAR in (conf.data or ""): + if re.search(URI_INJECTABLE_REGEX, conf.url, re.I) and not any(place in conf.parameters for place in (PLACE.GET, PLACE.POST)) and not kb.postHint and not CUSTOM_INJECTION_MARK_CHAR in (conf.data or "") and conf.url.startswith("http"): warnMsg = "you've provided target URL without any GET " warnMsg += "parameters (e.g. www.site.com/article.php?id=1) " warnMsg += "and without providing any POST parameters " From 78dbe080d70f5d9236e9e47c5b4f49f215a15232 Mon Sep 17 00:00:00 2001 From: ricterz Date: Tue, 24 Mar 2015 17:19:37 +0800 Subject: [PATCH 2/5] determine whether it's websocket when connect #1198 --- lib/request/connect.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/request/connect.py b/lib/request/connect.py index 677decdc5..f556ab674 100644 --- a/lib/request/connect.py +++ b/lib/request/connect.py @@ -18,6 +18,7 @@ import time import traceback import urllib2 import urlparse +import websocket from extra.safe2bin.safe2bin import safecharencode from lib.core.agent import agent @@ -232,6 +233,7 @@ class Connect(object): retrying = kwargs.get("retrying", False) crawling = kwargs.get("crawling", False) skipRead = kwargs.get("skipRead", False) + is_websocket = conf.url.startswith("ws") if not urlparse.urlsplit(url).netloc: url = urlparse.urljoin(conf.url, url) @@ -364,7 +366,24 @@ class Connect(object): url = unicodeencode(url) post = unicodeencode(post, kb.pageEncoding) - if method and method not in (HTTPMETHOD.GET, HTTPMETHOD.POST): + if is_websocket: + try: + ws = websocket.WebSocket() + ws.connect(url) + ws.send(urldecode(post) if post else '') + response = ws.recv() + ws.close() + return response, {}, 101 + + except websocket.WebSocketConnectionClosedException: + # TODO: more exception to handle + warnMsg = "connection was forcibly closed by the target URL" + logger.critical(warnMsg) + return Connect._retryProxy(**kwargs) + except Exception: + return None, None, None + + elif method and method not in (HTTPMETHOD.GET, HTTPMETHOD.POST): method = unicodeencode(method) req = MethodRequest(url, post, headers) req.set_method(method) From 9b5dcbbbb2f0bc39338fe37bff81510c1a3988e1 Mon Sep 17 00:00:00 2001 From: ricterz Date: Tue, 24 Mar 2015 18:21:50 +0800 Subject: [PATCH 3/5] modified error handle #1198 --- lib/request/connect.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/lib/request/connect.py b/lib/request/connect.py index f556ab674..9b3ae3da9 100644 --- a/lib/request/connect.py +++ b/lib/request/connect.py @@ -367,21 +367,12 @@ class Connect(object): post = unicodeencode(post, kb.pageEncoding) if is_websocket: - try: - ws = websocket.WebSocket() - ws.connect(url) - ws.send(urldecode(post) if post else '') - response = ws.recv() - ws.close() - return response, {}, 101 - - except websocket.WebSocketConnectionClosedException: - # TODO: more exception to handle - warnMsg = "connection was forcibly closed by the target URL" - logger.critical(warnMsg) - return Connect._retryProxy(**kwargs) - except Exception: - return None, None, None + ws = websocket.WebSocket() + ws.connect(url) + ws.send(urldecode(post) if post else '') + response = ws.recv() + ws.close() + return response, {}, 101 elif method and method not in (HTTPMETHOD.GET, HTTPMETHOD.POST): method = unicodeencode(method) @@ -557,7 +548,7 @@ class Connect(object): debugMsg = "got HTTP error code: %d (%s)" % (code, status) logger.debug(debugMsg) - except (urllib2.URLError, socket.error, socket.timeout, httplib.BadStatusLine, httplib.IncompleteRead, struct.error, ProxyError, SqlmapCompressionException), e: + except (urllib2.URLError, socket.error, socket.timeout, httplib.BadStatusLine, httplib.IncompleteRead, struct.error, ProxyError, SqlmapCompressionException, websocket.WebSocketException), e: tbMsg = traceback.format_exc() if "no host given" in tbMsg: @@ -582,6 +573,10 @@ class Connect(object): elif "IncompleteRead" in tbMsg: warnMsg = "there was an incomplete read error while retrieving data " warnMsg += "from the target URL" + elif "Handshake status" in tbMsg: + status = re.search("Handshake status ([\d]{3})", tbMsg) + errMsg = "websocket handshake status %s" % status.group(1) if status else 'unknown' + raise SqlmapConnectionException(errMsg) else: warnMsg = "unable to connect to the target URL" From 811f5c11c6cb6292deb0a3e28435fce1c622d575 Mon Sep 17 00:00:00 2001 From: ricterz Date: Tue, 24 Mar 2015 18:50:57 +0800 Subject: [PATCH 4/5] remove Host header field and add cookie support #1198 --- lib/request/connect.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/request/connect.py b/lib/request/connect.py index 9b3ae3da9..3adb88bd5 100644 --- a/lib/request/connect.py +++ b/lib/request/connect.py @@ -367,11 +367,14 @@ class Connect(object): post = unicodeencode(post, kb.pageEncoding) if is_websocket: + # WebSocket will add Host field of headers automatically + disallowed_headers = ['Host'] ws = websocket.WebSocket() - ws.connect(url) + ws.connect(url, header=["%s: %s" % _ for _ in headers.items() if _[0] not in disallowed_headers], cookie=cookie) ws.send(urldecode(post) if post else '') response = ws.recv() ws.close() + # WebSocket class does not have response headers return response, {}, 101 elif method and method not in (HTTPMETHOD.GET, HTTPMETHOD.POST): @@ -554,7 +557,7 @@ class Connect(object): if "no host given" in tbMsg: warnMsg = "invalid URL address used (%s)" % repr(url) raise SqlmapSyntaxException(warnMsg) - elif "forcibly closed" in tbMsg: + elif "forcibly closed" in tbMsg or "Connection is already closed" in tbMsg: warnMsg = "connection was forcibly closed by the target URL" elif "timed out" in tbMsg: if kb.testMode and kb.testType not in (None, PAYLOAD.TECHNIQUE.TIME, PAYLOAD.TECHNIQUE.STACKED): From bbfdb02a0ef0577ab2312861448a68a373c58921 Mon Sep 17 00:00:00 2001 From: ricterz Date: Tue, 24 Mar 2015 22:25:16 +0800 Subject: [PATCH 5/5] fix mandatorily depend of websocket #1198 --- lib/core/option.py | 11 +++++++++++ lib/request/connect.py | 10 ++++++++-- lib/utils/deps.py | 11 +++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/core/option.py b/lib/core/option.py index 0eafd1b20..6bfc91760 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -2137,6 +2137,16 @@ def _setTorSocksProxySettings(): socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5 if conf.torType == PROXY_TYPE.SOCKS5 else socks.PROXY_TYPE_SOCKS4, LOCALHOST, conf.torPort or DEFAULT_TOR_SOCKS_PORT) socks.wrapmodule(urllib2) +def _checkWebSocket(): + infoMsg = "checking URL is WebSocket or not" + logger.debug(infoMsg) + if conf.url and (conf.url.startswith("ws:/") or conf.url.startswith("wss:/")): + try: + from websocket import ABNF + except ImportError: + errMsg = "it seems that python 'websocket-client' third-party library not be installed. " + raise SqlmapMissingDependence(errMsg) + def _checkTor(): if not conf.checkTor: return @@ -2383,6 +2393,7 @@ def init(): _setWafFunctions() _setTrafficOutputFP() _resolveCrossReferences() + _checkWebSocket() parseTargetUrl() parseTargetDirect() diff --git a/lib/request/connect.py b/lib/request/connect.py index 3adb88bd5..ba2e7909b 100644 --- a/lib/request/connect.py +++ b/lib/request/connect.py @@ -18,7 +18,13 @@ import time import traceback import urllib2 import urlparse -import websocket + +try: + import websocket + from websocket import WebSocketException +except ImportError: + class WebSocketException(Exception): + pass from extra.safe2bin.safe2bin import safecharencode from lib.core.agent import agent @@ -551,7 +557,7 @@ class Connect(object): debugMsg = "got HTTP error code: %d (%s)" % (code, status) logger.debug(debugMsg) - except (urllib2.URLError, socket.error, socket.timeout, httplib.BadStatusLine, httplib.IncompleteRead, struct.error, ProxyError, SqlmapCompressionException, websocket.WebSocketException), e: + except (urllib2.URLError, socket.error, socket.timeout, httplib.BadStatusLine, httplib.IncompleteRead, struct.error, ProxyError, SqlmapCompressionException, WebSocketException), e: tbMsg = traceback.format_exc() if "no host given" in tbMsg: diff --git a/lib/utils/deps.py b/lib/utils/deps.py index ea2f661e9..d25c8ea94 100644 --- a/lib/utils/deps.py +++ b/lib/utils/deps.py @@ -78,6 +78,17 @@ def checkDependencies(): logger.warn(warnMsg) missing_libraries.add('python-ntlm') + try: + from websocket import ABNF + debugMsg = "'python websocket-client' library is found" + logger.debug(debugMsg) + except ImportError: + warnMsg = "sqlmap requires 'python websocket-client' third-party library for " + warnMsg += "if you plan to attack a web application behind websocket. " + warnMsg += "Download from https://pypi.python.org/pypi/websocket-client/" + logger.warn(warnMsg) + missing_libraries.add('websocket-client') + if IS_WIN: try: import pyreadline