mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-07-03 06:51:08 +00:00
Adding support for import sqlmap as a library (#2083)
This commit is contained in:
parent
e1126a2a4e
commit
d2ead9dcda
5 changed files with 312 additions and 3 deletions
190
lib/utils/library.py
Normal file
190
lib/utils/library.py
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
#!/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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue