|
15 | 15 |
|
16 | 16 | from lib.core.agent import agent |
17 | 17 | from lib.core.common import beep |
| 18 | +from lib.core.common import calculateDeltaSeconds |
18 | 19 | from lib.core.common import getUnicode |
19 | 20 | from lib.core.common import randomInt |
20 | 21 | from lib.core.common import randomStr |
|
26 | 27 | from lib.core.data import kb |
27 | 28 | from lib.core.data import logger |
28 | 29 | from lib.core.data import paths |
| 30 | +from lib.core.datatype import advancedDict |
| 31 | +from lib.core.datatype import injectionDict |
29 | 32 | from lib.core.enums import HTTPMETHOD |
30 | 33 | from lib.core.enums import NULLCONNECTION |
| 34 | +from lib.core.enums import PAYLOAD |
31 | 35 | from lib.core.exception import sqlmapConnectionException |
32 | 36 | from lib.core.exception import sqlmapGenericException |
33 | 37 | from lib.core.exception import sqlmapNoneDataException |
34 | 38 | from lib.core.exception import sqlmapSiteTooDynamic |
35 | 39 | from lib.core.exception import sqlmapUserQuitException |
36 | 40 | from lib.core.session import setString |
37 | 41 | from lib.core.session import setRegexp |
| 42 | +from lib.core.settings import ERROR_SPACE |
| 43 | +from lib.core.settings import ERROR_EMPTY_CHAR |
38 | 44 | from lib.request.connect import Connect as Request |
| 45 | +from plugins.dbms.firebird.syntax import Syntax as Firebird |
| 46 | +from plugins.dbms.postgresql.syntax import Syntax as PostgreSQL |
| 47 | +from plugins.dbms.mssqlserver.syntax import Syntax as MSSQLServer |
| 48 | +from plugins.dbms.oracle.syntax import Syntax as Oracle |
| 49 | +from plugins.dbms.mysql.syntax import Syntax as MySQL |
| 50 | +from plugins.dbms.access.syntax import Syntax as Access |
| 51 | +from plugins.dbms.sybase.syntax import Syntax as Sybase |
| 52 | +from plugins.dbms.sqlite.syntax import Syntax as SQLite |
| 53 | +from plugins.dbms.maxdb.syntax import Syntax as MaxDB |
| 54 | + |
| 55 | + |
| 56 | +def unescape(string, dbms): |
| 57 | + unescaper = { |
| 58 | + "Access": Access.unescape, |
| 59 | + "Firebird": Firebird.unescape, |
| 60 | + "MaxDB": MaxDB.unescape, |
| 61 | + "Microsoft SQL Server": MSSQLServer.unescape, |
| 62 | + "MySQL": MySQL.unescape, |
| 63 | + "Oracle": Oracle.unescape, |
| 64 | + "PostgreSQL": PostgreSQL.unescape, |
| 65 | + "SQLite": SQLite.unescape, |
| 66 | + "Sybase": Sybase.unescape |
| 67 | + } |
| 68 | + |
| 69 | + if isinstance(dbms, list): |
| 70 | + dbmsunescaper = unescaper[dbms[0]] |
| 71 | + else: |
| 72 | + dbmsunescaper = unescaper[dbms] |
39 | 73 |
|
40 | | -def checkSqlInjection(place, parameter, value, parenthesis): |
41 | | - """ |
42 | | - This function checks if the GET, POST, Cookie, User-Agent |
43 | | - parameters are affected by a SQL injection vulnerability and |
44 | | - identifies the type of SQL injection: |
| 74 | + return dbmsunescaper(string) |
45 | 75 |
|
46 | | - * Unescaped numeric injection |
47 | | - * Single quoted string injection |
48 | | - * Double quoted string injection |
49 | | - """ |
| 76 | +def checkSqlInjection(place, parameter, value): |
| 77 | + # Store here the details about boundaries and payload used to |
| 78 | + # successfully inject |
| 79 | + injection = injectionDict() |
50 | 80 |
|
51 | | - logic = conf.logic |
52 | | - randInt = randomInt() |
53 | | - randStr = randomStr() |
54 | | - prefix = "" |
55 | | - suffix = "" |
56 | | - retVal = None |
| 81 | + for test in conf.tests: |
| 82 | + title = test.title |
| 83 | + stype = test.stype |
| 84 | + proceed = True |
57 | 85 |
|
58 | | - if conf.prefix or conf.suffix: |
59 | | - if conf.prefix: |
60 | | - prefix = conf.prefix |
| 86 | + # Parse test's <risk> |
| 87 | + if test.risk > conf.risk: |
| 88 | + debugMsg = "skipping test '%s' because the risk " % title |
| 89 | + debugMsg += "is higher than the provided" |
| 90 | + logger.debug(debugMsg) |
| 91 | + continue |
61 | 92 |
|
62 | | - if conf.suffix: |
63 | | - suffix = conf.suffix |
| 93 | + # Parse test's <level> |
| 94 | + if test.level > conf.level: |
| 95 | + debugMsg = "skipping test '%s' because the level " % title |
| 96 | + debugMsg += "is higher than the provided" |
| 97 | + logger.debug(debugMsg) |
| 98 | + continue |
| 99 | + |
| 100 | + if "details" in test and "dbms" in test.details: |
| 101 | + dbms = test.details.dbms |
| 102 | + else: |
| 103 | + dbms = None |
64 | 104 |
|
65 | | - for case in kb.injections.root.case: |
66 | | - conf.matchRatio = None |
| 105 | + # Skip current test if it is the same SQL injection type |
| 106 | + # already identified by another test |
| 107 | + if injection.data and stype in injection.data: |
| 108 | + debugMsg = "skipping test '%s' because " % title |
| 109 | + debugMsg += "we have already the payload for %s" % PAYLOAD.SQLINJECTION[stype] |
| 110 | + logger.debug(debugMsg) |
67 | 111 |
|
68 | | - positive = case.test.positive |
69 | | - negative = case.test.negative |
| 112 | + continue |
| 113 | + |
| 114 | + # Skip DBMS-specific tests if they do not match the DBMS |
| 115 | + # identified |
| 116 | + if injection.dbms is not None and injection.dbms != dbms: |
| 117 | + debugMsg = "skipping test '%s' because " % title |
| 118 | + debugMsg += "the back-end DBMS is %s" % injection.dbms |
| 119 | + logger.debug(debugMsg) |
70 | 120 |
|
71 | | - if not prefix and not suffix and case.name == "custom": |
72 | 121 | continue |
73 | 122 |
|
74 | | - infoMsg = "testing %s (%s) injection " % (case.desc, logic) |
75 | | - infoMsg += "on %s parameter '%s'" % (place, parameter) |
| 123 | + infoMsg = "testing '%s'" % title |
76 | 124 | logger.info(infoMsg) |
77 | 125 |
|
78 | | - payload = agent.payload(place, parameter, value, negative.format % eval(negative.params)) |
79 | | - _ = Request.queryPage(payload, place) |
| 126 | + # Parse test's <request> |
| 127 | + payload = agent.cleanupPayload(test.request.payload) |
80 | 128 |
|
81 | | - payload = agent.payload(place, parameter, value, positive.format % eval(positive.params)) |
82 | | - trueResult = Request.queryPage(payload, place) |
| 129 | + if dbms: |
| 130 | + payload = unescape(payload, dbms) |
83 | 131 |
|
84 | | - if trueResult: |
85 | | - infoMsg = "confirming %s (%s) injection " % (case.desc, logic) |
86 | | - infoMsg += "on %s parameter '%s'" % (place, parameter) |
87 | | - logger.info(infoMsg) |
| 132 | + if "comment" in test.request: |
| 133 | + comment = test.request.comment |
| 134 | + else: |
| 135 | + comment = "" |
| 136 | + testPayload = "%s%s" % (payload, comment) |
| 137 | + |
| 138 | + if conf.prefix is not None and conf.suffix is not None: |
| 139 | + boundary = advancedDict() |
| 140 | + |
| 141 | + boundary.level = 1 |
| 142 | + boundary.clause = [ 0 ] |
| 143 | + boundary.where = [ 1, 2, 3 ] |
| 144 | + # TODO: inspect the conf.prefix and conf.suffix to set |
| 145 | + # proper ptype |
| 146 | + boundary.ptype = 1 |
| 147 | + boundary.prefix = conf.prefix |
| 148 | + boundary.suffix = conf.suffix |
| 149 | + |
| 150 | + conf.boundaries.insert(0, boundary) |
| 151 | + |
| 152 | + for boundary in conf.boundaries: |
| 153 | + # Parse boundary's <level> |
| 154 | + if boundary.level > conf.level: |
| 155 | + # NOTE: shall we report every single skipped boundary too? |
| 156 | + continue |
88 | 157 |
|
89 | | - payload = agent.payload(place, parameter, value, negative.format % eval(negative.params)) |
| 158 | + # Parse test's <clause> and boundary's <clause> |
| 159 | + # Skip boundary if it does not match against test's <clause> |
| 160 | + clauseMatch = False |
90 | 161 |
|
91 | | - randInt = randomInt() |
92 | | - randStr = randomStr() |
| 162 | + for clauseTest in test.clause: |
| 163 | + if clauseTest in boundary.clause: |
| 164 | + clauseMatch = True |
| 165 | + break |
93 | 166 |
|
94 | | - falseResult = Request.queryPage(payload, place) |
| 167 | + if test.clause != [ 0 ] and boundary.clause != [ 0 ] and not clauseMatch: |
| 168 | + continue |
95 | 169 |
|
96 | | - if not falseResult: |
97 | | - infoMsg = "%s parameter '%s' is %s (%s) injectable " % (place, parameter, case.desc, logic) |
98 | | - infoMsg += "with %d parenthesis" % parenthesis |
99 | | - logger.info(infoMsg) |
| 170 | + # Parse test's <where> and boundary's <where> |
| 171 | + # Skip boundary if it does not match against test's <where> |
| 172 | + whereMatch = False |
100 | 173 |
|
101 | | - if conf.beep: |
102 | | - beep() |
| 174 | + for where in test.where: |
| 175 | + if where in boundary.where: |
| 176 | + whereMatch = True |
| 177 | + break |
103 | 178 |
|
104 | | - retVal = case.name |
105 | | - break |
| 179 | + if not whereMatch: |
| 180 | + continue |
| 181 | + |
| 182 | + # Parse boundary's <prefix>, <suffix> and <ptype> |
| 183 | + prefix = boundary.prefix if boundary.prefix else "" |
| 184 | + suffix = boundary.suffix if boundary.suffix else "" |
| 185 | + ptype = boundary.ptype |
| 186 | + injectable = False |
106 | 187 |
|
107 | | - kb.paramMatchRatio[(place, parameter)] = conf.matchRatio |
| 188 | + # If the previous injections succeeded, we know which prefix, |
| 189 | + # postfix and parameter type to use for further tests, no |
| 190 | + # need to cycle through all of the boundaries anymore |
| 191 | + condBound = (injection.prefix is not None and injection.suffix is not None) |
| 192 | + condBound &= (injection.prefix != prefix or injection.suffix != suffix) |
| 193 | + condType = injection.ptype is not None and injection.ptype != ptype |
| 194 | + |
| 195 | + if condBound or condType: |
| 196 | + continue |
| 197 | + |
| 198 | + # For each test's <where> |
| 199 | + for where in test.where: |
| 200 | + # The <where> tag defines where to add our injection |
| 201 | + # string to the parameter under assessment. |
| 202 | + if where == 1: |
| 203 | + origValue = value |
| 204 | + elif where == 2: |
| 205 | + origValue = "-%s" % value |
| 206 | + elif where == 3: |
| 207 | + origValue = "" |
| 208 | + |
| 209 | + # Forge payload by prepending with boundary's prefix and |
| 210 | + # appending with boundary's suffix the test's |
| 211 | + # ' <payload><command> ' string |
| 212 | + boundPayload = "%s%s %s %s" % (origValue, prefix, testPayload, suffix) |
| 213 | + boundPayload = boundPayload.strip() |
| 214 | + boundPayload = agent.cleanupPayload(boundPayload) |
| 215 | + reqPayload = agent.payload(place, parameter, value, boundPayload) |
| 216 | + |
| 217 | + # Parse test's <response> |
| 218 | + # Check wheather or not the payload was successful |
| 219 | + for method, check in test.response.items(): |
| 220 | + check = agent.cleanupPayload(check) |
| 221 | + |
| 222 | + # In case of boolean-based blind SQL injection |
| 223 | + if method == "comparison": |
| 224 | + sndPayload = agent.cleanupPayload(test.response.comparison) |
| 225 | + |
| 226 | + if dbms: |
| 227 | + sndPayload = unescape(sndPayload, dbms) |
| 228 | + |
| 229 | + if "comment" in test.response: |
| 230 | + sndComment = test.response.comment |
| 231 | + else: |
| 232 | + sndComment = "" |
| 233 | + |
| 234 | + sndPayload = "%s%s" % (sndPayload, sndComment) |
| 235 | + boundPayload = "%s%s %s %s" % (origValue, prefix, sndPayload, suffix) |
| 236 | + boundPayload = boundPayload.strip() |
| 237 | + boundPayload = agent.cleanupPayload(boundPayload) |
| 238 | + cmpPayload = agent.payload(place, parameter, value, boundPayload) |
| 239 | + |
| 240 | + # Useful to set conf.matchRatio at first |
| 241 | + conf.matchRatio = None |
| 242 | + _ = Request.queryPage(cmpPayload, place) |
| 243 | + |
| 244 | + trueResult = Request.queryPage(reqPayload, place) |
| 245 | + |
| 246 | + if trueResult: |
| 247 | + falseResult = Request.queryPage(cmpPayload, place) |
| 248 | + |
| 249 | + if not falseResult: |
| 250 | + infoMsg = "%s parameter '%s' is '%s' injectable " % (place, parameter, title) |
| 251 | + logger.info(infoMsg) |
| 252 | + |
| 253 | + kb.paramMatchRatio[(place, parameter)] = conf.matchRatio |
| 254 | + injectable = True |
| 255 | + |
| 256 | + kb.paramMatchRatio[(place, parameter)] = conf.matchRatio |
| 257 | + |
| 258 | + # In case of error-based or UNION query SQL injections |
| 259 | + elif method == "grep": |
| 260 | + reqBody, _ = Request.queryPage(reqPayload, place, content=True) |
| 261 | + match = re.search(check, reqBody, re.DOTALL | re.IGNORECASE) |
| 262 | + |
| 263 | + if not match: |
| 264 | + continue |
| 265 | + |
| 266 | + output = match.group('result') |
| 267 | + |
| 268 | + if output: |
| 269 | + output = output.replace(ERROR_SPACE, " ").replace(ERROR_EMPTY_CHAR, "") |
| 270 | + |
| 271 | + if output == "1": |
| 272 | + infoMsg = "%s parameter '%s' is '%s' injectable " % (place, parameter, title) |
| 273 | + logger.info(infoMsg) |
| 274 | + |
| 275 | + injectable = True |
| 276 | + |
| 277 | + # In case of time-based blind or stacked queries SQL injections |
| 278 | + elif method == "time": |
| 279 | + start = time.time() |
| 280 | + _ = Request.queryPage(reqPayload, place) |
| 281 | + duration = calculateDeltaSeconds(start) |
| 282 | + |
| 283 | + if duration >= conf.timeSec: |
| 284 | + infoMsg = "%s parameter '%s' is '%s' injectable " % (place, parameter, title) |
| 285 | + logger.info(infoMsg) |
| 286 | + |
| 287 | + injectable = True |
| 288 | + |
| 289 | + if injectable is True: |
| 290 | + injection.place = place |
| 291 | + injection.parameter = parameter |
| 292 | + injection.ptype = ptype |
| 293 | + injection.prefix = prefix |
| 294 | + injection.suffix = suffix |
| 295 | + |
| 296 | + injection.data[stype] = (title, where, comment, boundPayload) |
| 297 | + |
| 298 | + if "details" in test: |
| 299 | + for detailKey, detailValue in test.details.items(): |
| 300 | + if detailKey == "dbms" and injection.dbms is None: |
| 301 | + injection.dbms = detailValue |
| 302 | + elif detailKey == "dbms_version" and injection.dbms_version is None: |
| 303 | + injection.dbms_version = detailValue |
| 304 | + elif detailKey == "os" and injection.os is None: |
| 305 | + injection.os = detailValue |
| 306 | + |
| 307 | + break |
108 | 308 |
|
109 | | - return retVal |
| 309 | + return injection |
110 | 310 |
|
111 | 311 | def heuristicCheckSqlInjection(place, parameter, value): |
112 | 312 | if kb.nullConnection: |
|
0 commit comments