Adding switch --mine-params

This commit is contained in:
Miroslav Štampar 2026-07-24 14:27:05 +02:00
parent ffe124f3e0
commit 075d009bb7
8 changed files with 457 additions and 1 deletions

243
data/txt/common-params.txt Normal file
View file

@ -0,0 +1,243 @@
# Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
# See the file 'LICENSE' for copying permission
id
page
q
query
search
s
keyword
keywords
name
username
user
uid
userid
email
mail
pass
password
pwd
token
key
apikey
api_key
access_token
auth
session
sid
sessionid
lang
language
locale
country
region
city
zip
sort
order
orderby
dir
direction
filter
category
cat
type
kind
class
group
tag
tags
status
state
action
act
cmd
command
op
operation
mode
method
func
function
callback
jsonp
format
output
view
tab
step
stage
debug
test
dev
admin
role
level
priv
file
filename
path
dir
folder
doc
document
url
uri
link
href
redirect
redirect_uri
return
returnurl
return_url
next
target
dest
destination
goto
continue
ref
referer
referrer
source
src
from
to
date
year
month
day
time
timestamp
start
end
begin
finish
limit
offset
count
num
number
size
length
width
height
amount
price
qty
quantity
value
val
data
input
content
text
msg
message
body
title
subject
description
desc
comment
note
code
hash
sig
signature
csrf
csrf_token
csrftoken
nonce
salt
enc
encrypt
decrypt
base64
json
xml
raw
echo
reflect
show
hide
display
render
template
tpl
theme
skin
style
color
font
image
img
photo
avatar
icon
banner
video
audio
media
attachment
upload
download
export
import
backup
restore
sync
refresh
reload
reset
clear
flush
purge
enable
disable
active
enabled
visible
public
private
locked
verified
confirmed
approved
product
item
sku
model
brand
vendor
seller
store
shop
cart
basket
checkout
payment
invoice
receipt
transaction
account
profile
member
customer
client
company
organization
org
department
team
project
task
job
event
booking
reservation
appointment
schedule
calendar

View file

@ -529,6 +529,10 @@ def start():
checkWaf()
if conf.mineParams and not any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti, conf.xxe, conf.hql)):
from lib.utils.paraminer import mineParameters
mineParameters()
if any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti, conf.xxe, conf.hql)) and (conf.reportJson or conf.resultsFile):
singleTimeWarnMessage("'--report-json'/'--results-file' do not (yet) capture non-SQL technique (--graphql/--nosql/--ldap/--xpath/--ssti/--xxe/--hql) findings; these are reported on the console only")

View file

@ -1589,6 +1589,7 @@ def setPaths(rootPath):
paths.COMMON_COLUMNS = os.path.join(paths.SQLMAP_TXT_PATH, "common-columns.txt")
paths.COMMON_FILES = os.path.join(paths.SQLMAP_TXT_PATH, "common-files.txt")
paths.COMMON_TABLES = os.path.join(paths.SQLMAP_TXT_PATH, "common-tables.txt")
paths.COMMON_PARAMETERS = os.path.join(paths.SQLMAP_TXT_PATH, "common-params.txt")
paths.SQL_KEYWORDS = os.path.join(paths.SQLMAP_TXT_PATH, "keywords.txt")
paths.SMALL_DICT = os.path.join(paths.SQLMAP_TXT_PATH, "smalldict.txt")
paths.USER_AGENTS = os.path.join(paths.SQLMAP_TXT_PATH, "user-agents.txt")

View file

@ -241,6 +241,7 @@ optDict = {
"eta": "boolean",
"flushSession": "boolean",
"forms": "boolean",
"mineParams": "boolean",
"freshQueries": "boolean",
"googlePage": "integer",
"harFile": "string",

View file

@ -20,7 +20,7 @@ from lib.core.enums import OS
from thirdparty import six
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.7.180"
VERSION = "1.10.7.181"
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
@ -92,6 +92,9 @@ LIVE_COOKIES_TIMEOUT = 120
LOWER_RATIO_BOUND = 0.02
UPPER_RATIO_BOUND = 0.98
# Number of candidate names probed per request while mining for hidden parameters ('--mine-params')
PARAMETER_MINING_BUCKET_SIZE = 25
# For filling in case of dumb push updates
DUMMY_JUNK = "Phah5jue"

View file

@ -66,6 +66,7 @@ def vulnTest(tests=None, label="vuln"):
("-u <url> --data=\"security_level=4\" -p id --flush-session --technique=B --banner", ("random (non-scanner) User-Agent and browser-like headers to bypass the WAF/IPS", "Type: boolean-based blind", "banner: '3.")), # automatic WAF-bypass against a libinjection-class WAF: tampers cannot help, only the non-scanner User-Agent does
("-u <url> --data=\"security_level=5\" -p id --flush-session --technique=B", ("unable to automatically bypass the WAF/IPS", "does not seem to be injectable")), # automatic WAF-bypass honest bail: a libinjection-class WAF that no User-Agent or tamper can defeat
("-u <url> -p id --flush-session --technique=B --proof", ("sqlmap proved exploitation of the following injection point", "Parameter: id (GET)", "Technique: boolean-based blind", "TRUE (5/5)", "repeatably", "Retrieved: back-end DBMS banner '3.")), # --proof: report-grade proof in the injection-point style - forces the boolean technique (so a multi-technique point still proves), and actively reads a value out as the strongest proof
("-u <base> --mine-params --flush-session --technique=B", ("mining for hidden GET parameters", "found hidden parameter 'id'", "held back parameter(s) that break the base request", "Parameter: id (GET)", "Type: boolean-based blind")), # --mine-params: discover an injectable parameter absent from a bare URL, hold back the raw-SQL sink that would shadow it, then confirm the injection on the mined 'id'
("-r <request> --flush-session -v 5 --test-skip=\"heavy\" --save=<config>", ("CloudFlare", "web application technology: Express", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind", "saved command line options to the configuration file")),
("-c <config>", ("CloudFlare", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind")),
("-l <log> --flush-session --skip-waf -vvvvv --technique=U --union-from=users --banner --parse-errors", ("banner: '3.", "ORDER BY term out of range", "~xp_cmdshell", "Connection: keep-alive")),

View file

@ -721,6 +721,9 @@ def cmdLineParser(argv=None):
general.add_argument("--forms", dest="forms", action="store_true",
help="Parse and test forms on target URL")
general.add_argument("--mine-params", dest="mineParams", action="store_true",
help="Mine for hidden (unlinked) GET parameters to test")
general.add_argument("--fresh-queries", dest="freshQueries", action="store_true",
help="Ignore query results stored in session file")

200
lib/utils/paraminer.py Normal file
View file

@ -0,0 +1,200 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
import difflib
from lib.core.compat import xrange
from lib.core.common import getFileItems
from lib.core.common import paramToDict
from lib.core.common import randomStr
from lib.core.common import singleTimeWarnMessage
from lib.core.data import conf
from lib.core.data import logger
from lib.core.data import paths
from lib.core.enums import HTTPMETHOD
from lib.core.enums import PLACE
from lib.core.settings import DIFF_TOLERANCE
from lib.core.settings import MAX_DIFFLIB_SEQUENCE_LENGTH
from lib.core.settings import PARAMETER_MINING_BUCKET_SIZE
from lib.request.connect import Connect as Request
# Benign, broadly-stable value used both to check whether a discovered parameter is safe to add and
# as its seed during testing (a random value would error out an integer/id context and defeat inference)
PROBE_VALUE = "1"
def _canary():
return randomStr(10, lowercase=True)
def _fetch(get):
"""
Requests the target with the given GET query string, returning a (page, HTTP code) pair.
"""
try:
page, _, code = Request.getPage(get=get or None, silent=True, raise404=False)
except Exception:
page, code = None, None
return (page or ""), code
def _ratio(first, second):
"""
Similarity of two response bodies, mirroring the core page comparison (see comparison.py): an
exact match, a length ratio for oversized bodies, otherwise difflib's quick_ratio().
"""
if not first or not second:
return 0.0
if first == second:
return 1.0
if any(len(_) > MAX_DIFFLIB_SEQUENCE_LENGTH for _ in (first, second)):
ratio = 1.0 * len(first) / len(second)
return ratio if ratio <= 1 else 1.0 / ratio
return difflib.SequenceMatcher(None, first, second).quick_ratio()
def _differs(page, base, floor):
"""
True when 'page' departs from the baseline by more than the target's own dynamic jitter ('floor').
"""
return bool(page) and _ratio(page, base) < floor - DIFF_TOLERANCE
def _confirm(baseGet, name, base, floor):
"""
Confirms a single candidate in isolation with two differently-valued probes. Returns 'reflected'
when a value is echoed back (caught here even if a shared bucket hid it behind a preceding
parameter), 'behavioral' when both probes resemble each other yet depart from the baseline (its
presence, not its value, matters), otherwise None.
"""
def _get(value):
pair = "%s=%s" % (name, value)
return "%s&%s" % (baseGet, pair) if baseGet else pair
canaries = (_canary(), _canary())
first, second = _fetch(_get(canaries[0]))[0], _fetch(_get(canaries[1]))[0]
if not (first and second):
return None
if (canaries[0] in first or canaries[1] in second) and not any(_ in base for _ in canaries):
return "reflected"
if _ratio(first, second) < floor - DIFF_TOLERANCE: # value-dependent yet not reflected -> unreliable
return None
if _differs(first, base, floor) and _differs(second, base, floor):
return "behavioral"
return None
def _chunks(sequence, size):
for i in xrange(0, len(sequence), size):
yield sequence[i:i + size]
def _discover(candidates, baseGet, base, floor):
"""
Probes candidate names in buckets (one shared request per bucket, each name carrying its own
random canary) and returns the confirmed ones as (name, reason) pairs. Reflection is resolved
straight from the bucket response; the rest are confirmed individually only when the bucket
actually moved the response, so a target that ignores every candidate stays cheap.
"""
found = []
for chunk in _chunks(candidates, PARAMETER_MINING_BUCKET_SIZE):
canaries = dict((name, _canary()) for name in chunk)
query = "&".join("%s=%s" % (name, canaries[name]) for name in chunk)
page = _fetch("%s&%s" % (baseGet, query) if baseGet else query)[0]
if not page:
continue
pending = []
for name in chunk:
if canaries[name] in page and canaries[name] not in base:
found.append((name, "reflected"))
else:
pending.append(name)
if pending and _differs(page, base, floor):
for name in pending:
reason = _confirm(baseGet, name, base, floor)
if reason:
found.append((name, reason))
return found
def _commit(found, baseGet, baseCode):
"""
Adds the discovered parameters (seeded with PROBE_VALUE) to the GET test scope. One that turns
the request into a server error the baseline did not have would shadow and corrupt the testing
of every sibling parameter, so it is reported and held back rather than degrading detection.
"""
safe, disruptive = [], []
for name, _ in found:
pair = "%s=%s" % (name, PROBE_VALUE)
code = _fetch("%s&%s" % (baseGet, pair) if baseGet else pair)[1]
if code is not None and code >= 500 and not (baseCode is not None and baseCode >= 500):
disruptive.append(name)
else:
safe.append(name)
if disruptive:
logger.warning("held back parameter(s) that break the base request with a test value (test them explicitly with '-p'): %s" % ", ".join("'%s'" % _ for _ in disruptive))
if not safe:
return
logger.info("adding %d discovered parameter(s) to the test scope: %s" % (len(safe), ", ".join("'%s'" % _ for _ in safe)))
additions = "&".join("%s=%s" % (name, PROBE_VALUE) for name in safe)
conf.parameters[PLACE.GET] = "%s&%s" % (baseGet, additions) if baseGet else additions
conf.paramDict[PLACE.GET] = paramToDict(PLACE.GET, conf.parameters[PLACE.GET])
def mineParameters():
"""
Discovers hidden (unlinked) GET parameters the target still processes and queues the confirmed
ones for the regular injection tests, using two independent oracles (value reflection and a
behavioral side effect on the response).
"""
if conf.data or (conf.method and conf.method != HTTPMETHOD.GET):
singleTimeWarnMessage("'--mine-params' currently supports GET parameters only")
return
baseGet = conf.parameters.get(PLACE.GET) or ""
existing = set(conf.paramDict.get(PLACE.GET) or {})
candidates = [_ for _ in getFileItems(paths.COMMON_PARAMETERS, unique=True) if _ and _ not in existing]
if not candidates:
return
logger.info("mining for hidden GET parameters (%d candidate name(s))" % len(candidates))
base, baseCode = _fetch(baseGet)
if not base:
singleTimeWarnMessage("could not obtain a baseline response, skipping parameter mining")
return
floor = _ratio(base, _fetch(baseGet)[0]) # the target's own between-request jitter
found = _discover(candidates, baseGet, base, floor)
for name, reason in found:
logger.info("found hidden parameter '%s' (%s)" % (name, reason))
if not found:
logger.info("no hidden parameters found")
return
_commit(found, baseGet, baseCode)