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
|
|
@ -189,7 +189,7 @@ b14628a6c9327d110afe50b01f3171f64f61823343b8de89596e854b00b74928 lib/core/dump.
|
|||
9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py
|
||||
0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py
|
||||
888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py
|
||||
906d17d317ef11f67d52b30cf6bbcfd67c3af35af0942f697a13c55d9aa89816 lib/core/settings.py
|
||||
1d609263088c5767b4f92ead270f84cd218d9602007b75b3fd45c1169f183265 lib/core/settings.py
|
||||
c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py
|
||||
a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py
|
||||
15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py
|
||||
|
|
@ -266,6 +266,7 @@ bd9267d94390ba87d6c5a35c90f2406d6a4135a7c8ea01db76dd9e6519eee2ed lib/utils/dial
|
|||
71a66ff766a2921106770b26acff380de469222dc893816a7b970b384c927666 lib/utils/hash.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/utils/__init__.py
|
||||
1bbf57e43f921d4132e6e5a336ff39454a9506b36de94ebcc45879d0abcac56a lib/utils/keysetdump.py
|
||||
b57aa20b7a6fd8afd07bae773fd03f8acb05655ee605362b220e65a0664dc38d lib/utils/library.py
|
||||
dd30ef67da30b666c53013ee32253cd9396ed0e5d0a44d509680742e06ebcd23 lib/utils/pivotdumptable.py
|
||||
c1dfc3bed0fed9b181f612d1d747955dd2b506dbe99bc9fd481495602371473a lib/utils/progress.py
|
||||
c442e9ef8324fd6fdf7bc334d765f0a6ce4037397eb3d79d59b5ce3e9a043855 lib/utils/prove.py
|
||||
|
|
@ -509,7 +510,7 @@ cedf45d33461bd7e5400d06611a63c8a4ffae1a4510030c5696b9d46ed6a9883 plugins/generi
|
|||
46517f1444c202710e388873960130850ed092e17bd6f4dd5f2fedea3dbb8ffc sqlmapapi.py
|
||||
f09d1b06901e7e02d0dbf4de607f6a4a9889acc322ae9353b98ea9101fb9548a sqlmapapi.yaml
|
||||
627d90f1194335b800cbc9cc78db6697cf9e02e193a83598e0d4d0abb55b63b8 sqlmap.conf
|
||||
d375c77f1f4270ec0967e67963fe410f14b5d2e51ed6483593dc1aaa4e8e106e sqlmap.py
|
||||
80d66407453d34d672c389f6d9ab059d925528615429f2e6e9f286ce03d2c5d6 sqlmap.py
|
||||
eb37a88357522fd7ad00d90cdc5da6b57442b4fec49366aadb2944c4fbf8b804 tamper/0eunion.py
|
||||
a9785a4c111d6fee2e6d26466ba5efb3b229c00520b26e8024b041553b53efba tamper/apostrophemask.py
|
||||
cf26bc8006519bd25ce06d347f72770cd75b61575cf65e5812274e8ab9392eb4 tamper/apostrophenullencode.py
|
||||
|
|
@ -626,6 +627,7 @@ b23bf934dafe54c241761517a7b8c139159aa4b941db10832a626a51fea81e35 tests/test_htt
|
|||
d539d0ae758b5bb91e314ab82ab4fe03d6fb2f8b377d16aefa6d7d1d77a7d5a9 tests/test_identifiers_output.py
|
||||
5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py
|
||||
0fc7bd9bae4fbd09f51027780b7a8e72eab73810dccdfdf87ed9e489e6e671c9 tests/test_ldap.py
|
||||
7780bbd53f4ef48b01b689f3989c62822ee7f326dfc3b4110522c9af93a61482 tests/test_library.py
|
||||
caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py
|
||||
790b78c600b61eb0bdd6e07e14b1db3eb2ddd5fc5d4edb9e975f85ced38558c7 tests/test_nosql.py
|
||||
88a8c7ce0ba0ca721dffbcf9351cd07f7e471ad2fe667a10608c18952b09868d tests/test_openapi_drift.py
|
||||
|
|
|
|||
|
|
@ -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.12"
|
||||
VERSION = "1.10.7.13"
|
||||
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)
|
||||
|
|
|
|||
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)
|
||||
|
|
@ -664,3 +664,9 @@ if __name__ == "__main__":
|
|||
else:
|
||||
# cancelling postponed imports (because of CI/CD checks)
|
||||
__import__("lib.controller.controller")
|
||||
|
||||
# exposing the programmatic library facade as 'sqlmap.scan()' / 'sqlmap.scanFromRequest()'
|
||||
from lib.utils.library import scan, scanFromRequest, SqlmapError
|
||||
|
||||
# public library API (also marks the re-exported names above as intentional for pyflakes)
|
||||
__all__ = ["scan", "scanFromRequest", "SqlmapError"]
|
||||
|
|
|
|||
111
tests/test_library.py
Normal file
111
tests/test_library.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Unit coverage for the library facade (import sqlmap; sqlmap.scan(...)).
|
||||
|
||||
The facade drives the engine out-of-process through a generated configuration file (the same '-c'
|
||||
mechanism the REST API uses) and reads back a '--report-json' report. These tests stub
|
||||
subprocess.Popen to (a) capture the argv/config sqlmap.scan() builds from its keyword options and
|
||||
(b) feed back a canned report - keeping the test fast, offline and network-free (no real scan runs).
|
||||
|
||||
stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import sqlmap
|
||||
|
||||
|
||||
class _FakePopen(object):
|
||||
"""Stub that records argv/config and writes a canned report to the config's 'reportJson' path."""
|
||||
|
||||
captured = {}
|
||||
returncode = 0
|
||||
|
||||
def __init__(self, argv, **kwargs):
|
||||
_FakePopen.captured["argv"] = argv
|
||||
_FakePopen.captured["kwargs"] = kwargs
|
||||
with open(argv[argv.index("-c") + 1]) as f:
|
||||
config = f.read()
|
||||
_FakePopen.captured["config"] = config
|
||||
report = re.search(r"(?im)^reportjson\s*=\s*(.+)$", config).group(1).strip()
|
||||
with open(report, "w") as f:
|
||||
json.dump({"success": True, "data": [{"type_name": "BANNER", "value": "3.45.1"}], "error": []}, f)
|
||||
|
||||
def wait(self, timeout=None):
|
||||
return 0
|
||||
|
||||
def poll(self):
|
||||
return 0
|
||||
|
||||
def kill(self):
|
||||
pass
|
||||
|
||||
|
||||
class TestLibraryFacade(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._realPopen = subprocess.Popen
|
||||
subprocess.Popen = _FakePopen
|
||||
_FakePopen.captured = {}
|
||||
|
||||
def tearDown(self):
|
||||
subprocess.Popen = self._realPopen
|
||||
|
||||
def test_requires_a_target(self):
|
||||
subprocess.Popen = self._realPopen # never reached; guard fires first
|
||||
self.assertRaises(sqlmap.SqlmapError, sqlmap.scan)
|
||||
|
||||
def test_rejects_unknown_option(self):
|
||||
# a command line switch spelling (rather than a conf option name) must be rejected loudly
|
||||
self.assertRaises(sqlmap.SqlmapError, sqlmap.scan, "http://target/?id=1", current_user=True)
|
||||
|
||||
def test_options_go_through_config(self):
|
||||
result = sqlmap.scan("http://target/vuln.php?id=1", technique="BEU", dumpTable=True,
|
||||
tbl="users", level=3, getBanner=True, raw=["--fresh-queries"])
|
||||
argv = _FakePopen.captured["argv"]
|
||||
config = _FakePopen.captured["config"]
|
||||
# driven via a generated config file, stdin ignored, engine plumbing set - no arg escaping
|
||||
self.assertIn("-c", argv)
|
||||
self.assertIn("--ignore-stdin", argv)
|
||||
self.assertIn("--fresh-queries", argv) # raw escape hatch stays on the CLI
|
||||
# options land in the config using sqlmap's own (conf) names (ConfigParser lowercases keys)
|
||||
self.assertTrue(re.search(r"(?im)^url\s*=\s*http://target/vuln.php\?id=1$", config))
|
||||
self.assertTrue(re.search(r"(?im)^technique\s*=\s*BEU$", config))
|
||||
self.assertTrue(re.search(r"(?im)^tbl\s*=\s*users$", config))
|
||||
self.assertTrue(re.search(r"(?im)^level\s*=\s*3$", config))
|
||||
self.assertTrue(re.search(r"(?im)^dumptable\s*=\s*True$", config))
|
||||
self.assertTrue(re.search(r"(?im)^getbanner\s*=\s*True$", config))
|
||||
self.assertTrue(re.search(r"(?im)^batch\s*=\s*True$", config))
|
||||
self.assertTrue(re.search(r"(?im)^outputdir\s*=", config)) # each run isolated on disk
|
||||
# file descriptors are not leaked to the engine (matches the REST API subprocess)
|
||||
self.assertFalse(_FakePopen.captured["kwargs"].get("close_fds") and os.name == "nt")
|
||||
# canned report is returned verbatim
|
||||
self.assertTrue(result["success"])
|
||||
self.assertEqual(result["data"][0]["value"], "3.45.1")
|
||||
|
||||
def test_scan_from_request_uses_request_file(self):
|
||||
sqlmap.scanFromRequest("/tmp/req.txt", technique="U")
|
||||
config = _FakePopen.captured["config"]
|
||||
self.assertTrue(re.search(r"(?im)^requestfile\s*=\s*/tmp/req.txt$", config))
|
||||
self.assertTrue(re.search(r"(?im)^technique\s*=\s*U$", config))
|
||||
|
||||
def test_missing_report_raises(self):
|
||||
class _NoReportPopen(_FakePopen):
|
||||
def __init__(self, argv, **kwargs):
|
||||
_FakePopen.captured["argv"] = argv # write nothing -> no report file
|
||||
subprocess.Popen = _NoReportPopen
|
||||
self.assertRaises(sqlmap.SqlmapError, sqlmap.scan, "http://target/?id=1")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Loading…
Add table
Add a link
Reference in a new issue