mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-07-03 06:51:08 +00:00
190 lines
7.7 KiB
Python
190 lines
7.7 KiB
Python
#!/usr/bin/env python
|
|
|
|
"""
|
|
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
|
See the file 'LICENSE' for copying permission
|
|
"""
|
|
|
|
# Library facade for programmatic (in-code) usage: 'import sqlmap; sqlmap.scan(...)'.
|
|
#
|
|
# This is the code-level sibling of the REST API (lib/utils/api.py): both drive the engine as an
|
|
# isolated subprocess for programmatic callers. The public names here are re-exported by sqlmap.py so
|
|
# that they are reachable as 'sqlmap.scan', 'sqlmap.scanFromRequest' and 'sqlmap.SqlmapError'.
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
|
|
__all__ = ["scan", "scanFromRequest", "SqlmapError"]
|
|
|
|
# Absolute path of the engine entry point (this module lives at <root>/lib/utils/library.py)
|
|
SQLMAP_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "sqlmap.py")
|
|
|
|
class SqlmapError(Exception):
|
|
"""
|
|
Raised by the library facade (scan/scanFromRequest) when a scan can not produce a result report
|
|
"""
|
|
|
|
pass
|
|
|
|
def _terminateProcess(process):
|
|
"""
|
|
Best-effort hard teardown of a scan subprocess together with its whole process group, so a
|
|
timed-out scan never leaves orphaned sqlmap workers behind (POSIX kills the group, others fall
|
|
back to killing the process itself)
|
|
"""
|
|
|
|
import signal
|
|
|
|
try:
|
|
if os.name != "nt" and hasattr(os, "killpg"):
|
|
os.killpg(os.getpgid(process.pid), getattr(signal, "SIGKILL", signal.SIGTERM))
|
|
else:
|
|
process.kill()
|
|
except (OSError, AttributeError):
|
|
try:
|
|
process.kill()
|
|
except (OSError, AttributeError):
|
|
pass
|
|
|
|
def scan(url=None, requestFile=None, timeout=None, outputDir=None, raw=None, **options):
|
|
"""
|
|
Runs a sqlmap scan in a dedicated subprocess and returns its structured result (library usage).
|
|
|
|
Keyword options are plain sqlmap option names - exactly the names used in a sqlmap configuration
|
|
file (data/sqlmap.conf) and by the REST API, i.e. the 'conf' names, NOT command line switches. So
|
|
scan(url, technique="BEU", getBanner=True, dumpTable=True, tbl="users", level=3) is equivalent to
|
|
the config file lines 'technique = BEU', 'getBanner = True', 'dumpTable = True', 'tbl = users',
|
|
'level = 3'. Unknown names are rejected. The scan is driven through a generated config file passed
|
|
with '-c' (the same mechanism the REST API uses), so there is a single option namespace and no
|
|
argument escaping. 'raw' takes a list of extra raw command line switches for the rare thing not
|
|
expressible as a config option (e.g. raw=["--fresh-queries"]).
|
|
|
|
The engine runs fully out-of-process, so a scan can never affect the calling process (no shared
|
|
global state, no HTTP-stack patching, no risk of the host being exited). The return value is the
|
|
parsed '--report-json' report - the same structure as the REST API '/scan/<id>/data' response: a
|
|
dict with keys 'success', 'data' (a list of {'type_name', 'value'} entries: TARGET, TECHNIQUES,
|
|
BANNER, DUMP_TABLE, ...), 'error' and 'meta'.
|
|
|
|
scan() is blocking and thread-safe, so it is both thread- and asyncio-ready: run several at once
|
|
in threads, or from an event loop with 'await loop.run_in_executor(None, functools.partial(scan,
|
|
url, dumpTable=True))'. For unattended/concurrent use the run is hardened like the REST API
|
|
subprocess: batch mode (never prompts) with stdin closed, isolated file descriptors, its own
|
|
output directory (so parallel scans of the same target can not collide on session/dump files and
|
|
nothing accumulates on disk), engine output streamed to a temporary file rather than buffered in
|
|
memory, and - when 'timeout' is set - the whole subprocess group is torn down on expiry. Pass
|
|
'outputDir' to keep the run's files.
|
|
|
|
Example:
|
|
import sqlmap
|
|
result = sqlmap.scan("http://target/vuln.php?id=1", dumpTable=True, tbl="users")
|
|
"""
|
|
|
|
import shutil
|
|
import subprocess
|
|
import time
|
|
|
|
from lib.core.common import saveConfig
|
|
from lib.core.optiondict import optDict
|
|
|
|
if not (url or requestFile):
|
|
raise SqlmapError("scan() requires either 'url' or 'requestFile'")
|
|
|
|
if not os.path.isfile(SQLMAP_FILE):
|
|
raise SqlmapError("could not locate the sqlmap engine ('%s')" % SQLMAP_FILE)
|
|
|
|
knownOptions = set()
|
|
for family in optDict.values():
|
|
knownOptions.update(family)
|
|
|
|
config = {}
|
|
if url:
|
|
config["url"] = url
|
|
if requestFile:
|
|
config["requestFile"] = requestFile
|
|
config.update(options)
|
|
|
|
unknown = [_ for _ in config if _ not in knownOptions]
|
|
if unknown:
|
|
raise SqlmapError("unknown option(s) %s - scan() expects sqlmap option names as used in a configuration file (e.g. getBanner, dumpTable, tbl, technique, level), not command line switches" % ", ".join(repr(_) for _ in sorted(unknown)))
|
|
|
|
handle, report = tempfile.mkstemp(prefix="sqlmap-", suffix=".json")
|
|
os.close(handle)
|
|
|
|
# Each run gets its own output directory so concurrent scans can not collide on session/dump files
|
|
# and no scan state piles up on disk. A caller-provided 'outputDir' is respected and left in place.
|
|
ownOutput = not outputDir
|
|
if ownOutput:
|
|
outputDir = tempfile.mkdtemp(prefix="sqlmap-output-")
|
|
|
|
# engine plumbing goes through the very same option namespace
|
|
config["batch"] = True
|
|
config["disableColoring"] = True
|
|
config["outputDir"] = outputDir
|
|
config["reportJson"] = report
|
|
|
|
handle, configFile = tempfile.mkstemp(prefix="sqlmap-", suffix=".conf")
|
|
os.close(handle)
|
|
saveConfig(config, configFile)
|
|
|
|
argv = [sys.executable or "python", SQLMAP_FILE, "-c", configFile, "--ignore-stdin"]
|
|
if raw:
|
|
argv += list(raw)
|
|
|
|
logHandle, logFile = tempfile.mkstemp(prefix="sqlmap-", suffix=".log")
|
|
devnull = open(os.devnull, "rb")
|
|
|
|
kwargs = {"shell": False, "close_fds": os.name != "nt", "cwd": os.path.dirname(SQLMAP_FILE) or '.', "stdin": devnull, "stdout": logHandle, "stderr": subprocess.STDOUT}
|
|
if os.name == "nt":
|
|
kwargs["creationflags"] = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
|
|
elif sys.version_info >= (3, 2):
|
|
kwargs["start_new_session"] = True # own process group -> clean group teardown
|
|
else:
|
|
kwargs["preexec_fn"] = os.setsid
|
|
|
|
process = None
|
|
try:
|
|
process = subprocess.Popen(argv, **kwargs)
|
|
|
|
if timeout is None:
|
|
process.wait()
|
|
else:
|
|
end = time.time() + timeout
|
|
while process.poll() is None:
|
|
if time.time() > end:
|
|
_terminateProcess(process)
|
|
process.wait()
|
|
raise SqlmapError("scan timed out after %s second(s)" % timeout)
|
|
time.sleep(0.5)
|
|
|
|
try:
|
|
with open(report, "rb") as f:
|
|
return json.loads(f.read().decode("utf-8", "replace"))
|
|
except (IOError, OSError, ValueError):
|
|
try:
|
|
with open(logFile, "rb") as f:
|
|
tail = f.read().decode("utf-8", "replace").strip()
|
|
except (IOError, OSError):
|
|
tail = ""
|
|
raise SqlmapError("scan did not produce a valid report (exit code %s)\n%s" % (getattr(process, "returncode", None), tail[-1000:]))
|
|
finally:
|
|
try:
|
|
os.close(logHandle)
|
|
except OSError:
|
|
pass
|
|
devnull.close()
|
|
for path in (report, logFile, configFile):
|
|
try:
|
|
os.remove(path)
|
|
except OSError:
|
|
pass
|
|
if ownOutput:
|
|
shutil.rmtree(outputDir, ignore_errors=True)
|
|
|
|
def scanFromRequest(requestFile, **options):
|
|
"""
|
|
Convenience wrapper for scan(requestFile=...) - runs a scan from a saved HTTP request file ('-r')
|
|
"""
|
|
|
|
return scan(requestFile=requestFile, **options)
|