Skip to content

Commit 2893fd5

Browse files
committed
Adding support for NoSQL injection
1 parent 0a331f2 commit 2893fd5

11 files changed

Lines changed: 1535 additions & 9 deletions

File tree

data/txt/sha256sums.txt

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,10 @@ ca86d61d3349ed2d94a6b164d4648cff9701199b5e32378c3f40fca0f517b128 extra/shutils/
160160
df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/recloak.sh
161161
1972990a67caf2d0231eacf60e211acf545d9d0beeb3c145a49ba33d5d491b3f extra/shutils/strip.sh
162162
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py
163-
63657c00a046ca0fb28fd069407ab6305bd7b95c42f26a96ed083fd05b152252 extra/vulnserver/vulnserver.py
163+
43214ecb0101bce72eb243c91b90db34693ebfd485d6c111a4ae22591ff7800b extra/vulnserver/vulnserver.py
164164
a2bf70d7f87c3a4e0675c0bad54119a4e04efa6ea2730a8338d5aebcd995630e lib/controller/action.py
165-
9387fb775b694156a71b336a2a9638ef24c577aa38746f391ac040ff05306d95 lib/controller/checks.py
166-
96463b969312bd4fd29452b5fc739f33e5a73f81fdc1ef80ac27debbe9926e42 lib/controller/controller.py
165+
0c6433b289094d37f295238699042a34a6ab950bb3d11f74fe9a83d30bb7f4bd lib/controller/checks.py
166+
ea0fdf6bcda59aae4d093bada965654a0cd940227c2dbdf62b6ded79baa8dfad lib/controller/controller.py
167167
d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py
168168
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py
169169
9c5764c92ce536d1f0f96200359ee5ef1f37f9128769bf990cb77f1d1f8e17b1 lib/core/agent.py
@@ -181,26 +181,26 @@ f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decor
181181
5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py
182182
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py
183183
914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py
184-
8b260bff7f24947ece55727277d526c88a91f7cb9ffe059c4b9c190bf85f80e1 lib/core/optiondict.py
184+
056930fba3cf9827f97d280bc38ac785c93108eb84c922f5f39723bb04dcf403 lib/core/optiondict.py
185185
4e7f2ad3d2866093aa195616a0e93de1687406edc0b9038fbfa76bf1c9c174b2 lib/core/option.py
186186
ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch.py
187187
49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py
188188
03db48f02c3d07a047ddb8fe33a757b6238867352d8ddda2a83e4fec09a98d04 lib/core/readlineng.py
189189
9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py
190190
0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py
191191
888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py
192-
2db950a79f3f8a4bbb0f35731d4e2eef220150961be55d8ba4b1f9565bdd483a lib/core/settings.py
192+
ca14e55b4d49a9b9f4e547180828030e4fcc51176dc9036879dbdae05919dd02 lib/core/settings.py
193193
c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py
194194
a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py
195195
19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py
196-
c1392cda2f202fa3c628f74533c8d9379d1cf7e754ac165e39021bbc2bbc4a22 lib/core/testing.py
196+
e453904a50372216b09146ad9f11cdced2323c10f49c3d866238cc044dcb2cce lib/core/testing.py
197197
95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py
198198
b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py
199199
53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py
200200
2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py
201201
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py
202202
54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py
203-
6060d2d11fab39796b87ace30a872302f365dea3b14d24670915fdb9edc86011 lib/parse/cmdline.py
203+
223badcfd102cdf3313411b63d09b6c59599d58dfc40d27409b1bfa2efc1aa8f lib/parse/cmdline.py
204204
02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py
205205
c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py
206206
5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py
@@ -240,6 +240,8 @@ a66a4b9df6207dce722c9b71d290ea426723cb4b697b416065dc7dd5db96fe8e lib/techniques
240240
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/error/__init__.py
241241
5bbef46c16e34fd80e3f9f0e9aa255ce2e39be0d0e57479e25890b041c7efc7d lib/techniques/error/use.py
242242
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/__init__.py
243+
44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 lib/techniques/nosql/__init__.py
244+
d62b28bf9f1544e65a1017994402f484166f4d64a1efb724351b15e27b851990 lib/techniques/nosql/inject.py
243245
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/union/__init__.py
244246
ceec65f8cb7c3254c4671351c837418c76ac5bc55ccbc40779f67231b54d7085 lib/techniques/union/test.py
245247
c65766f71e285fc85cdf58e7448c4c1d015af2a9dbb44fa3b665a9f13362fbcc lib/techniques/union/use.py
@@ -597,6 +599,7 @@ c04e8358fb6df45f69f2f26435c971acde280535bf304e84d30cf2681158c6a7 tests/test_has
597599
d539d0ae758b5bb91e314ab82ab4fe03d6fb2f8b377d16aefa6d7d1d77a7d5a9 tests/test_identifiers_output.py
598600
5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py
599601
caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py
602+
790b78c600b61eb0bdd6e07e14b1db3eb2ddd5fc5d4edb9e975f85ced38558c7 tests/test_nosql.py
600603
57fa9713a3186020be8bcc3f06399e92bf9ce82ec6d3413c76babe19606bb698 tests/test_openapi_drift.py
601604
cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pagecontent.py
602605
4bac34af2abddce003756d6776e89b2fda220bb7603ef3761f4f37ee29f9c369 tests/test_payload_marking.py

extra/vulnserver/vulnserver.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,46 @@
122122
LISTEN_ADDRESS = "localhost"
123123
LISTEN_PORT = 8440
124124

125+
# Minimal MongoDB-style collection backing the NoSQL operator-injection endpoint ('/nosql'). The
126+
# 'password' field is the blind-extraction target, constrained by a sibling 'name' equality match.
127+
NOSQL_USERS = {
128+
"luther": "s3cr3t",
129+
"fluffy": "carrot",
130+
"wu": "shanghai",
131+
}
132+
133+
def nosql_match(params):
134+
"""Emulates a MongoDB find() on NOSQL_USERS: reconstructs the operator object for the 'password'
135+
field (from bracket-notation 'password[$ne]=...' or a JSON sub-document) and evaluates it against
136+
the record selected by 'name'. An invalid $regex raises re.error (surfaced as a driver error)."""
137+
138+
record = NOSQL_USERS.get(params.get("name"))
139+
140+
spec = params.get("password")
141+
if isinstance(spec, dict):
142+
op, value = next(iter(spec.items()), ("$eq", None))
143+
else:
144+
op, value = "$eq", spec
145+
for key in params:
146+
match = re.match(r"^password\[(\$\w+)\](?:\[\])?$", key)
147+
if match:
148+
op, value = match.group(1), params[key]
149+
break
150+
151+
if isinstance(value, (tuple, list)):
152+
value = value[-1] if value else None
153+
154+
if record is None:
155+
return False
156+
elif op == "$ne":
157+
return record != value
158+
elif op == "$gt":
159+
return record > (value or "")
160+
elif op == "$regex":
161+
return re.search(value, record) is not None
162+
else: # $eq, $in (single-valued here) and any literal equality
163+
return record == value
164+
125165
_conn = None
126166
_cursor = None
127167
_lock = None
@@ -285,6 +325,20 @@ def do_REQUEST(self):
285325
self.wfile.write(form.encode(UNICODE_ENCODING))
286326
return
287327

328+
if self.url == "/nosql":
329+
self.send_response(OK)
330+
self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING)
331+
self.send_header("Connection", "close")
332+
self.end_headers()
333+
334+
try:
335+
output = "<html><body><b>Welcome %s</b></body></html>" % self.params.get("name") if nosql_match(self.params) else "<html><body><b>Invalid credentials</b></body></html>"
336+
except re.error: # invalid $regex -> emulate a MongoDB driver error (drives fingerprinting)
337+
output = "<html><body>MongoServerError: Regular expression is invalid: missing terminating ] for character class</body></html>"
338+
339+
self.wfile.write(output.encode(UNICODE_ENCODING))
340+
return
341+
288342
if self.url == '/':
289343
if not any(_ in self.params for _ in ("id", "query")):
290344
self.send_response(OK)

lib/controller/checks.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
from lib.core.settings import MAX_DIFFLIB_SEQUENCE_LENGTH
8888
from lib.core.settings import MAX_STABILITY_DELAY
8989
from lib.core.settings import NON_SQLI_CHECK_PREFIX_SUFFIX_LENGTH
90+
from lib.core.settings import NOSQL_ERROR_REGEX
9091
from lib.core.settings import PRECONNECT_INCOMPATIBLE_SERVERS
9192
from lib.core.settings import SINGLE_QUOTE_MARKER
9293
from lib.core.settings import SLEEP_TIME_MARKER
@@ -1170,6 +1171,13 @@ def _(page):
11701171
except (SystemError, RuntimeError) as ex:
11711172
logger.debug("Skipping FI heuristic due to regex failure: %s", getSafeExString(ex))
11721173

1174+
if not conf.nosql and re.search(NOSQL_ERROR_REGEX, page or ""):
1175+
infoMsg = "heuristic (NoSQL) test shows that %sparameter '%s' might be vulnerable to NoSQL injection attacks (rerun with switch '--nosql')" % ("%s " % paramType if paramType != parameter else "", parameter)
1176+
logger.info(infoMsg)
1177+
1178+
if conf.beep:
1179+
beep()
1180+
11731181
kb.disableHtmlDecoding = False
11741182
kb.heuristicMode = False
11751183

lib/controller/controller.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,11 @@ def start():
520520

521521
checkWaf()
522522

523+
if conf.nosql:
524+
from lib.techniques.nosql.inject import nosqlScan
525+
nosqlScan()
526+
continue
527+
523528
if conf.nullConnection:
524529
checkNullConnection()
525530

lib/core/optiondict.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118

119119
"Techniques": {
120120
"technique": "string",
121+
"nosql": "boolean",
121122
"timeSec": "integer",
122123
"uCols": "string",
123124
"uChar": "string",

lib/core/settings.py

Lines changed: 32 additions & 2 deletions
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.6.159"
23+
VERSION = "1.10.6.160"
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)
@@ -466,7 +466,8 @@
466466
r"error '[0-9a-f]{8}'((<[^>]+>)|\s)+(?P<result>[^<>]+)",
467467
r"\[[^\n\]]{1,100}(ODBC|JDBC)[^\n\]]+\](\[[^\]]+\])?(?P<result>[^\n]+(in query expression|\(SQL| at /[^ ]+pdo)[^\n<]+)",
468468
r"(?P<result>query error: SELECT[^<>]+)",
469-
r"(?P<result>(?:(?:ORA|PLS)-[0-9]{5}:|SQLCODE[ =:]+-?[0-9]+|SQLSTATE[ =:]+[0-9A-Z]{5}|Dynamic SQL Error|DB2 SQL error:|SAP DBTech JDBC:|SQLiteException:|You have an error in your SQL syntax;|Incorrect syntax near |Unclosed quotation mark after the character string|near \"[^\"]+\": syntax error)[^\n<]*)"
469+
r"(?P<result>(?:(?:ORA|PLS)-[0-9]{5}:|SQLCODE[ =:]+-?[0-9]+|SQLSTATE[ =:]+[0-9A-Z]{5}|Dynamic SQL Error|DB2 SQL error:|SAP DBTech JDBC:|SQLiteException:|You have an error in your SQL syntax;|Incorrect syntax near |Unclosed quotation mark after the character string|near \"[^\"]+\": syntax error)[^\n<]*)",
470+
r'"(?:errmsg|errorMessage|reason|msg)"\s*:\s*"(?P<result>[^"]+)"' # generic JSON error-message field (NoSQL document/REST back-ends)
470471
)
471472

472473
# Regular expression used for parsing charset info from meta html headers
@@ -847,6 +848,35 @@
847848
# Regular expression used for recognition of file inclusion errors
848849
FI_ERROR_REGEX = r"(?i)[^\n]{0,100}(no such file|failed (to )?open)[^\n]{0,100}"
849850

851+
# Regular expressions (per back-end, anchored to actual error-message structure - not product names) used for heuristic recognition of NoSQL injection
852+
NOSQL_ERRORS = (
853+
("MongoDB", r"Mongo(?:Server|Parse|Network|Runtime|Bulk|WriteConcern)?Error\b|\bBSON(?:Type)?Error\b|\bMongooseError\b|CastError: Cast to|unknown (?:top.level )?operator: ?\$|\$(?:regex|where|expr|in|nin|ne|gt|lt|elemMatch) (?:has to be|is not allowed|must be|not supported|requires)|Regular expression is invalid"),
854+
("CouchDB", r'"error"\s*:\s*"(?:bad_request|query_parse_error|missing_named_query)"|invalid operator: ?\$'),
855+
("Elasticsearch", r'"type"\s*:\s*"[a-z_]*?(?:query_shard|x_content_parse|parsing|search_phase_execution|illegal_argument|too_many_clauses|number_format|script)_exception"|Failed to parse query \['),
856+
("Solr", r"org\.apache\.solr\.[\w.]*(?:SyntaxError|SolrException)"),
857+
("Neo4j", r"Neo\.(?:ClientError|DatabaseError|TransientError|ClientNotification)\.|\bNeo4jError\b|even number of non-escaped quotes|Failed to parse string literal|expected an expression|'(?:UNWIND|OPTIONAL|DETACH|FOREACH|MERGE|LOAD CSV)'"),
858+
("ArangoDB", r"\bArangoError\b|AQL: (?:syntax|parse) error"),
859+
("Cassandra", r"line \d+:\d+ (?:no viable alternative at input|(?:mismatched|extraneous) input '.*?' expecting)|org\.apache\.cassandra|com\.datastax|\bInvalid(?:Request|Query)Exception\b"),
860+
("Redis", r"\bWRONGTYPE\b|ERR Error (?:compiling|running) script|@user_script|\bReplyError\b"),
861+
("Memcached", r"CLIENT_ERROR bad|SERVER_ERROR object too large"),
862+
("InfluxDB", r"error parsing query|unable to parse '[^']*': found"),
863+
("HBase/Phoenix", r"org\.apache\.phoenix|PhoenixParserException|org\.apache\.hadoop\.hbase"),
864+
)
865+
NOSQL_ERROR_REGEX = "(?:%s)" % '|'.join(regex for _, regex in NOSQL_ERRORS)
866+
867+
# Printable-ASCII codepoint bounds bisected (via regexp character-class ranges) during NoSQL blind extraction
868+
NOSQL_CHAR_MIN = 0x20
869+
NOSQL_CHAR_MAX = 0x7e
870+
871+
# Maximum number of document fields enumerated during a NoSQL ($where server-side JavaScript) document dump
872+
NOSQL_MAX_FIELDS = 64
873+
874+
# Maximum number of records walked during a NoSQL blind multi-record (ordered key paging) collection dump
875+
NOSQL_MAX_RECORDS = 100
876+
877+
# Upper bound for the length search during NoSQL blind extraction
878+
NOSQL_MAX_LENGTH = 1024
879+
850880
# Length of prefix and suffix used in non-SQLI heuristic checks
851881
NON_SQLI_CHECK_PREFIX_SUFFIX_LENGTH = 6
852882

lib/core/testing.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def vulnTest():
8888
("-u <url> --flush-session --technique=B --keyset --dump -T users", ("using keyset (seek) pagination", "30 entries", "luther", "nameisnull")), # keyset/seek dump via the SQLite rowid cursor
8989
("-u <url> -z \"tec=B\" --hex --fresh-queries --threads=4 --sql-query=\"SELECT * FROM users\"", ("SELECT * FROM users [30]", "nameisnull")),
9090
("-u \"<url>&echo=foobar*\" --flush-session", ("might be vulnerable to cross-site scripting",)),
91+
("-u \"<base>nosql?name=luther&password=x\" -p password --nosql --flush-session", ("is vulnerable to NoSQL injection", "back-end: 'MongoDB'", "NoSQL: GET parameter 'password'", "s3cr3t")), # NoSQL (MongoDB) operator-injection detection + blind regexp extraction
9192
("-u \"<url>&query=*\" --flush-session --technique=Q --banner", ("Title: SQLite inline queries", "banner: '3.")),
9293
("-d \"<direct>\" --flush-session --dump -T creds --dump-format=SQLITE --binary-fields=password_hash --where \"user_id=5\"", ("3137396164343563366365326362393763663130323965323132303436653831", "dumped to SQLITE database")),
9394
("-d \"<direct>\" --flush-session --banner --schema --sql-query=\"UPDATE users SET name='foobar' WHERE id=4; SELECT * FROM users; SELECT 987654321\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "4,foobar,nameisnull", "'987654321'",)),

lib/parse/cmdline.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,9 @@ def cmdLineParser(argv=None):
415415
techniques.add_argument("--technique", dest="technique",
416416
help="SQL injection techniques to use (default \"%s\")" % defaults.technique)
417417

418+
techniques.add_argument("--nosql", dest="nosql", action="store_true",
419+
help="Test for NoSQL injection (e.g. MongoDB, CouchDB, Neo4j)")
420+
418421
techniques.add_argument("--time-sec", dest="timeSec", type=int,
419422
help="Seconds to delay the DBMS response (default %d)" % defaults.timeSec)
420423

lib/techniques/nosql/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
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+
"""

0 commit comments

Comments
 (0)