├── Burp-SessionAuthTool.py ├── README.md ├── TODO └── test ├── TestApp.py └── TestServer.py /Burp-SessionAuthTool.py: -------------------------------------------------------------------------------- 1 | # Burp SessionAuthTool Extension 2 | # Copyright 2017 Thomas Patzke 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from burp import (IBurpExtender, ITab, IScannerCheck, IScanIssue, IContextMenuFactory, IContextMenuInvocation, IParameter, IIntruderPayloadGeneratorFactory, IIntruderPayloadGenerator) 18 | from javax.swing import (JPanel, JTable, JButton, JTextField, JLabel, JScrollPane, JMenuItem) 19 | from javax.swing.table import AbstractTableModel 20 | from java.awt import (GridBagLayout, GridBagConstraints) 21 | from array import array 22 | import pickle 23 | 24 | class BurpExtender(IBurpExtender, ITab, IScannerCheck, IContextMenuFactory, IParameter, IIntruderPayloadGeneratorFactory): 25 | def registerExtenderCallbacks(self, callbacks): 26 | self.callbacks = callbacks 27 | self.helpers = callbacks.getHelpers() 28 | callbacks.setExtensionName("Session Authentication Tool") 29 | self.out = callbacks.getStdout() 30 | 31 | # definition of suite tab 32 | self.tab = JPanel(GridBagLayout()) 33 | self.tabledata = MappingTableModel(callbacks) 34 | self.table = JTable(self.tabledata) 35 | #self.table.getColumnModel().getColumn(0).setPreferredWidth(50); 36 | #self.table.getColumnModel().getColumn(1).setPreferredWidth(100); 37 | self.tablecont = JScrollPane(self.table) 38 | c = GridBagConstraints() 39 | c.fill = GridBagConstraints.HORIZONTAL 40 | c.anchor = GridBagConstraints.FIRST_LINE_START 41 | c.gridx = 0 42 | c.gridy = 0 43 | c.gridheight = 6 44 | c.weightx = 0.3 45 | c.weighty = 0.5 46 | self.tab.add(self.tablecont, c) 47 | 48 | c = GridBagConstraints() 49 | c.weightx = 0.1 50 | c.anchor = GridBagConstraints.FIRST_LINE_START 51 | c.gridx = 1 52 | 53 | c.gridy = 0 54 | label_id = JLabel("Identifier:") 55 | self.tab.add(label_id, c) 56 | self.input_id = JTextField(20) 57 | self.input_id.setToolTipText("Enter the identifier which is used by the application to identifiy a particular test user account, e.g. a numerical user id or a user name.") 58 | c.gridy = 1 59 | self.tab.add(self.input_id, c) 60 | 61 | c.gridy = 2 62 | label_content = JLabel("Content:") 63 | self.tab.add(label_content, c) 64 | self.input_content = JTextField(20, actionPerformed=self.btn_add_id) 65 | self.input_content.setToolTipText("Enter some content which is displayed in responses of the application and shows that the current session belongs to a particular user, e.g. the full name of the user.") 66 | c.gridy = 3 67 | self.tab.add(self.input_content, c) 68 | 69 | self.btn_add = JButton("Add/Edit Identity", actionPerformed=self.btn_add_id) 70 | c.gridy = 4 71 | self.tab.add(self.btn_add, c) 72 | 73 | self.btn_del = JButton("Delete Identity", actionPerformed=self.btn_del_id) 74 | c.gridy = 5 75 | self.tab.add(self.btn_del, c) 76 | 77 | callbacks.customizeUiComponent(self.tab) 78 | callbacks.customizeUiComponent(self.table) 79 | callbacks.customizeUiComponent(self.tablecont) 80 | callbacks.customizeUiComponent(self.btn_add) 81 | callbacks.customizeUiComponent(self.btn_del) 82 | callbacks.customizeUiComponent(label_id) 83 | callbacks.customizeUiComponent(self.input_id) 84 | callbacks.addSuiteTab(self) 85 | callbacks.registerScannerCheck(self) 86 | callbacks.registerIntruderPayloadGeneratorFactory(self) 87 | callbacks.registerContextMenuFactory(self) 88 | 89 | def btn_add_id(self, e): 90 | ident = self.input_id.text 91 | self.input_id.text = "" 92 | content = self.input_content.text 93 | self.input_content.text = "" 94 | self.tabledata.add_mapping(ident, content) 95 | self.input_id.requestFocusInWindow() 96 | 97 | def btn_del_id(self, e): 98 | rows = self.table.getSelectedRows().tolist() 99 | self.tabledata.del_rows(rows) 100 | 101 | ### ITab ### 102 | def getTabCaption(self): 103 | return("SessionAuth") 104 | 105 | def getUiComponent(self): 106 | return self.tab 107 | 108 | ### IContextMenuFactory ### 109 | def createMenuItems(self, invocation): 110 | menuitems = list() 111 | msgs = invocation.getSelectedMessages() 112 | if msgs != None: 113 | if len(msgs) == 1: # "add as object id/as content to last id" context menu items 114 | bounds = invocation.getSelectionBounds() 115 | if bounds != None and bounds[0] != bounds[1]: 116 | msg = None 117 | if invocation.getInvocationContext() == IContextMenuInvocation.CONTEXT_MESSAGE_EDITOR_REQUEST or invocation.getInvocationContext() == IContextMenuInvocation.CONTEXT_MESSAGE_VIEWER_REQUEST: 118 | msg = msgs[0].getRequest().tostring() 119 | if invocation.getInvocationContext() == IContextMenuInvocation.CONTEXT_MESSAGE_EDITOR_RESPONSE or invocation.getInvocationContext() == IContextMenuInvocation.CONTEXT_MESSAGE_VIEWER_RESPONSE: 120 | msg = msgs[0].getResponse().tostring() 121 | if msg != None: 122 | selection = msg[bounds[0]:bounds[1]] 123 | shortSelection = selection[:20] 124 | if len(selection) > len(shortSelection): 125 | shortSelection += "..." 126 | menuitems.append(JMenuItem("Add '" + shortSelection + "' as object id", actionPerformed=self.gen_menu_add_id(selection))) 127 | if self.tabledata.lastadded != None: 128 | menuitems.append(JMenuItem("Add '" + shortSelection + "' as content to last added id", actionPerformed=self.gen_menu_add_content(selection))) 129 | if len(msgs) > 0: # "Send to Intruder" context menu items 130 | requestsWithIds = list() 131 | for msg in msgs: 132 | if isinstance(msg.getRequest(), array) and self.tabledata.containsId(msg.getRequest().tostring()): 133 | requestsWithIds.append(msg) 134 | if len(requestsWithIds) > 0: 135 | menuitems.append(JMenuItem("Send to Intruder and preconfigure id injection points", actionPerformed=self.gen_menu_send_intruder(requestsWithIds))) 136 | 137 | return menuitems 138 | 139 | def gen_menu_add_id(self, ident): 140 | def menu_add_id(e): 141 | self.tabledata.add_mapping(ident, "") 142 | return menu_add_id 143 | 144 | def gen_menu_add_content(self, content): 145 | def menu_add_content(e): 146 | self.tabledata.set_lastadded_content(content) 147 | return menu_add_content 148 | 149 | def gen_menu_send_intruder(self, requestResponses): 150 | def menu_send_intruder(e): 151 | for requestResponse in requestResponses: 152 | httpService = requestResponse.getHttpService() 153 | request = requestResponse.getRequest() 154 | injectionPoints = list() 155 | for ident in self.tabledata.getIds(): 156 | newInjectionPoints = findAll(request.tostring(), ident) 157 | if newInjectionPoints != None: 158 | injectionPoints += newInjectionPoints 159 | if len(injectionPoints) > 0: 160 | self.callbacks.sendToIntruder(httpService.getHost(), httpService.getPort(), httpService.getProtocol() == "https", request, injectionPoints) 161 | return menu_send_intruder 162 | 163 | ### IIntruderPayloadGeneratorFactory ### 164 | def getGeneratorName(self): 165 | return "SessionAuth Identifiers" 166 | 167 | def createNewInstance(self, attack): 168 | return IdentifiersPayloadGenerator(self.tabledata) 169 | 170 | ### IScannerCheck ### 171 | def doPassiveScan(self, baseRequestResponse): 172 | analyzedRequest = self.helpers.analyzeRequest(baseRequestResponse) 173 | params = analyzedRequest.getParameters() 174 | ids = self.tabledata.getIds() 175 | issues = list() 176 | 177 | for param in params: 178 | value = param.getValue() 179 | for ident in ids: 180 | if value == ident: 181 | issues.append(SessionAuthPassiveScanIssue( 182 | analyzedRequest.getUrl(), 183 | baseRequestResponse, 184 | param, 185 | ident, 186 | self.tabledata.getValue(ident), 187 | SessionAuthPassiveScanIssue.foundEqual, 188 | self.callbacks 189 | )) 190 | elif value.find(ident) >= 0: 191 | issues.append(SessionAuthPassiveScanIssue( 192 | analyzedRequest.getUrl(), 193 | baseRequestResponse, 194 | param, 195 | ident, 196 | self.tabledata.getValue(ident), 197 | SessionAuthPassiveScanIssue.foundInside, 198 | self.callbacks 199 | )) 200 | if len(issues) > 0: 201 | return issues 202 | else: 203 | return None 204 | 205 | def doActiveScan(self, baseRequestResponse, insertionPoint): 206 | ids = self.tabledata.getIds() 207 | if len(ids) <= 1: # active check only possible if multiple ids were given 208 | return None 209 | baseVal = insertionPoint.getBaseValue() 210 | url = baseRequestResponse.getUrl() 211 | 212 | idFound = list() 213 | for ident in ids: # find all identifiers in base value 214 | if baseVal.find(ident) >= 0: 215 | idFound.append(ident) 216 | if len(idFound) == 0: # no given identifier found, nothing to do 217 | return None 218 | 219 | baseResponse = baseRequestResponse.getResponse().tostring() 220 | baseResponseBody = baseResponse[self.helpers.analyzeResponse(baseResponse).getBodyOffset():] 221 | issues = list() 222 | scannedCombos = list() 223 | for replaceId in idFound: # scanner checks: replace found id by other given ids 224 | for scanId in ids: 225 | if replaceId == scanId or set([replaceId, scanId]) in scannedCombos: 226 | continue 227 | scannedCombos.append(set([replaceId, scanId])) 228 | 229 | scanPayload = baseVal.replace(replaceId, scanId) 230 | scanRequest = insertionPoint.buildRequest(scanPayload) 231 | scanRequestResponse = self.callbacks.makeHttpRequest(baseRequestResponse.getHttpService(), scanRequest) 232 | scanResponse = scanRequestResponse.getResponse().tostring() 233 | scanResponseBody = scanResponse[self.helpers.analyzeResponse(scanResponse).getBodyOffset():] 234 | 235 | if baseResponseBody == scanResponseBody: # response hasn't changed - no issue 236 | continue 237 | 238 | # Analyze responses 239 | replaceValue = self.tabledata.getValue(replaceId) 240 | scanValue = self.tabledata.getValue(scanId) 241 | # naming convention: 242 | # first word: base || scan (response) 243 | # second word: Replace || Scan (value) 244 | if replaceValue != "": 245 | baseReplaceValueCount = len(baseResponseBody.split(replaceValue)) - 1 246 | scanReplaceValueCount = len(scanResponseBody.split(replaceValue)) - 1 247 | else: 248 | baseReplaceValueCount = 0 249 | scanReplaceValueCount = 0 250 | 251 | if scanValue != "": 252 | baseScanValueCount = len(baseResponseBody.split(scanValue)) - 1 253 | scanScanValueCount = len(scanResponseBody.split(scanValue)) - 1 254 | else: 255 | baseScanValueCount = 0 256 | scanScanValueCount = 0 257 | 258 | if scanScanValueCount == 0: 259 | # Scan identifier content value doesn't appears, but responses differ 260 | issueCase = SessionAuthActiveScanIssue.caseScanValueNotFound 261 | elif baseReplaceValueCount > 0 and baseScanValueCount == 0 and scanReplaceValueCount == 0 and scanScanValueCount == baseReplaceValueCount: 262 | # Scan identifier replaces all occurrences of the original identifier in the response 263 | issueCase = SessionAuthActiveScanIssue.caseScanValueAppearsExactly 264 | elif baseReplaceValueCount > 0 and baseScanValueCount == 0 and scanReplaceValueCount == 0 and scanScanValueCount > 0: 265 | # Scan identfiers value appears, replaced ids value disappears 266 | issueCase = SessionAuthActiveScanIssue.caseScanValueAppearsFuzzy 267 | elif baseReplaceValueCount > scanReplaceValueCount and baseScanValueCount < scanScanValueCount: 268 | # Occurence count of replaced id value decreases, scan id value increases 269 | issueCase = SessionAuthActiveScanIssue.caseDecreaseIncrease 270 | elif baseScanValueCount < scanScanValueCount: 271 | # Occurence count of scan id value increases 272 | issueCase = SessionAuthActiveScanIssue.caseScanValueIncrease 273 | else: 274 | # Remainingg cases 275 | issueCase = SessionAuthActiveScanIssue.caseOther 276 | 277 | issues.append(SessionAuthActiveScanIssue( 278 | url, 279 | baseRequestResponse, 280 | insertionPoint, 281 | scanPayload, 282 | scanRequestResponse, 283 | replaceId, 284 | replaceValue, 285 | scanId, 286 | scanValue, 287 | issueCase, 288 | self.callbacks 289 | )) 290 | 291 | if len(issues) > 0: 292 | return issues 293 | else: 294 | return None 295 | 296 | def consolidateDuplicateIssues(self, existingIssue, newIssue): 297 | if existingIssue.getIssueDetail() == newIssue.getIssueDetail(): 298 | return 1 299 | else: 300 | return 0 301 | 302 | 303 | class SessionAuthPassiveScanIssue(IScanIssue): 304 | foundEqual = 1 # parameter value equals identifier 305 | foundInside = 2 # identifier was found inside parameter value 306 | 307 | def __init__(self, url, httpmsgs, param, ident, value, foundtype, callbacks): 308 | self.callbacks = callbacks 309 | self.service = httpmsgs.getHttpService() 310 | self.findingurl = url 311 | requestMatch = [array('i', [param.getValueStart(), param.getValueEnd()])] 312 | responseMatches = findAll(httpmsgs.getResponse().tostring(), value) 313 | self.httpmsgs = [callbacks.applyMarkers(httpmsgs, requestMatch, responseMatches)] 314 | if responseMatches: 315 | self.foundInResponse = True 316 | else: 317 | self.foundInResponse = False 318 | self.param = param 319 | self.ident = ident 320 | self.value = value 321 | self.foundtype = foundtype 322 | if self.foundInResponse: 323 | self.issueSeverity = "Low" 324 | else: 325 | self.issueSeverity = "Information" 326 | 327 | def __eq__(self, other): 328 | return self.param.getType() == other.param.getType() and self.param.getName() == other.param.getName() and self.param.getValue() == other.param.getValue() 329 | 330 | def __ne__(self, other): 331 | return not self == other 332 | 333 | def __repr__(self): 334 | return "SessionAuthPassiveScanIssue(" + self.getUrl() + "," + self.param.getType() + "," + self.param.getName + "," + self.param.getValue() + ")\n" 335 | 336 | def getUrl(self): 337 | return self.findingurl 338 | 339 | def getIssueName(self): 340 | return "Object Identifier found in Parameter Value" 341 | 342 | def getIssueType(self): 343 | return 1 344 | 345 | def getSeverity(self): 346 | return self.issueSeverity 347 | 348 | def getConfidence(self): 349 | if self.foundtype == self.foundEqual: 350 | return "Certain" 351 | elif self.foundtype == self.foundInside: 352 | return "Tentative" 353 | 354 | def getIssueDetail(self): 355 | msg = "The " + getParamTypeStr(self) + " " + self.param.getName() + " contains the user identifier " + self.ident + "." 356 | if self.foundInResponse: 357 | msg += "\nThe value " + self.value + " associated with the identifier was found in the request. The request is \ 358 | probably suitable for active scan detection of privilege escalation vulnerabilities." 359 | return msg 360 | 361 | def getRemediationDetail(self): 362 | return "A web application should generally perform access control checks to prevent privilege escalation vulnerabilities. The checks must not trust any \ 363 | data which is sent by the client because it is potentially manipulated. There must not be a static URL which allows to access a protected resource." 364 | 365 | def getIssueBackground(self): 366 | return "User identifiers submitted in requests are potential targets for parameter tampering attacks. An attacker could try to impersonate other users by \ 367 | replacement of his own user identifier by the id from a different user. This issue was reported because the user identifier previously entered was found in \ 368 | the request." 369 | 370 | def getRemediationBackground(self): 371 | return "The reaction to request manipulation and access attempts should be analyzed manually. This scan issue just gives you a pointer for potential interesting \ 372 | requests. It is important to understand if the replacement of an object identifier in the request gives an unprivileged user access to data he shouldn't be able \ 373 | to access." 374 | 375 | def getHttpMessages(self): 376 | return self.httpmsgs 377 | 378 | def getHttpService(self): 379 | return self.service 380 | 381 | 382 | class SessionAuthActiveScanIssue(IScanIssue): 383 | caseOther = 0 384 | caseScanValueNotFound = 1 385 | caseScanValueAppearsExactly = 2 386 | caseScanValueAppearsFuzzy = 3 387 | caseDecreaseIncrease = 4 388 | caseScanValueIncrease = 5 389 | 390 | def __init__(self, url, baseRequestResponse, insertionPoint, scanPayload, scanRequestResponse, replaceId, replaceValue, scanId, scanValue, issueCase, callbacks): 391 | self.callbacks = callbacks 392 | self.service = baseRequestResponse.getHttpService() 393 | self.findingUrl = url 394 | self.insertionPoint = insertionPoint 395 | self.scanPayload = scanPayload 396 | baseResponseMatches = findAll(baseRequestResponse.getResponse().tostring(), replaceValue) 397 | self.baseRequestResponse = callbacks.applyMarkers(baseRequestResponse, None, baseResponseMatches) 398 | scanPayloadOffsets = insertionPoint.getPayloadOffsets(scanPayload) 399 | scanRequestMatch = None 400 | if scanPayloadOffsets != None: 401 | scanRequestMatch = [array('i', scanPayloadOffsets)] 402 | scanResponseMatches = findAll(scanRequestResponse.getResponse().tostring(), scanValue) 403 | self.scanRequestResponse = callbacks.applyMarkers(scanRequestResponse, scanRequestMatch, scanResponseMatches) 404 | self.replaceId = replaceId 405 | self.replaceValue = replaceValue 406 | self.scanId = scanId 407 | self.scanValue = scanValue 408 | self.issueCase = issueCase 409 | 410 | def getUrl(self): 411 | return self.findingUrl 412 | 413 | def getIssueName(self): 414 | if self.issueCase in [self.caseScanValueAppearsExactly, self.caseScanValueAppearsFuzzy, self.caseDecreaseIncrease, self.caseScanValueIncrease]: 415 | return "Potential Privilege Escalation Vulnerability" 416 | else: 417 | return "Replacement of Identifier causes different Responses" 418 | 419 | def getIssueType(self): 420 | if self.issueCase in [self.caseScanValueAppearsExactly, self.caseScanValueAppearsFuzzy, self.caseDecreaseIncrease, self.caseScanValueIncrease]: 421 | return 2 422 | else: 423 | return 3 424 | 425 | def getSeverity(self): 426 | if self.issueCase in [self.caseScanValueAppearsExactly, self.caseScanValueAppearsFuzzy, self.caseDecreaseIncrease, self.caseScanValueIncrease]: 427 | return "High" 428 | else: 429 | return "Information" 430 | 431 | def getConfidence(self): 432 | if self.issueCase == self.caseScanValueAppearsExactly: 433 | return "Certain" 434 | elif self.issueCase in [self.caseScanValueAppearsFuzzy, self.caseDecreaseIncrease]: 435 | return "Firm" 436 | else: 437 | return "Tentative" 438 | 439 | def getIssueDetail(self): 440 | msg = "

The replaced identifier was " + self.replaceId + " and replaced by " + self.scanId + " in the response. \ 441 | The response was watched for decreasing occurrences of the content value " + self.replaceValue + " and increasing \ 442 | occurrences of " + self.scanValue + "." 443 | if self.issueCase in [self.caseOther, self.caseScanValueNotFound]: 444 | msg += "

The replacement of the identifier has caused a different response, but the occurence of the content value never changed \ 445 | or the content value of the replacement identifier even doesn't appeared. The differences should be verified manually.

" 446 | elif self.issueCase == self.caseScanValueAppearsExactly: 447 | msg += "

The content value associated with the replaced identifier disappeared completely. Instead the content value of \ 448 | the replacement identifier appeared with the same count. This is a quite strong indication of a possible privilege escalation \ 449 | vulnerability.

" 450 | elif self.issueCase == self.caseScanValueAppearsFuzzy: 451 | msg += "

The content value associated with the replaced identifier disappeared completely. Instead the content value of \ 452 | the replacement identifier appeared with a different count. This is an indication of a possible privilege escalation \ 453 | vulnerability.

" 454 | elif self.issueCase == self.caseDecreaseIncrease: 455 | msg += "

The appearance count of the content value associated with the replaced identifier decreased, while the appearance count of \ 456 | the replacement identifier increased. This is an indication of a possible privilege escalation vulnerability.

" 457 | elif self.issueCase == self.caseDecreaseIncrease: 458 | msg += "

The appearance count of the content value associated with the replaced identifier decreased, while the appearance count of \ 459 | the replacement identifier increased. This is an indication of a possible privilege escalation vulnerability.

" 460 | elif self.issueCase == self.caseScanValueIncrease: 461 | msg += "

The appearance appearance count of the replacement identifier increased. This is an weak indication of a possible privilege \ 462 | escalation vulnerability.

" 463 | 464 | return msg 465 | 466 | def getRemediationDetail(self): 467 | return "A web application should generally perform access control checks to prevent privilege escalation vulnerabilities. The checks must not trust any \ 468 | data which is sent by the client because it is potentially manipulated. There must not be a static URL which allows to access a protected resource." 469 | 470 | def getIssueBackground(self): 471 | msg = "The given request/response pair was automatically scanned for privilege escalation vulnerabilities by replacement \ 472 | of identifiers in the request and comparing the resulting responses. This issue was reported " 473 | if self.issueCase in [self.caseScanValueAppearsExactly, self.caseScanValueAppearsFuzzy, self.caseDecreaseIncrease, self.caseScanValueIncrease]: 474 | msg += "because the occurrence count of the content values associated with the changed identifiers have changed (see issue details)." 475 | else: 476 | msg += "for informational purposes because the responses differ. There is no direct indication for a privilege escalation issue." 477 | return msg 478 | 479 | def getRemediationBackground(self): 480 | return "The reaction to request manipulation and access attempts should be analyzed manually. This scan issue just gives you a pointer for potential interesting \ 481 | requests. It is important to understand if the replacement of an object identifier in the request gives an unprivileged user access to data he shouldn't be able \ 482 | to access." 483 | 484 | def getHttpMessages(self): 485 | return [self.baseRequestResponse, self.scanRequestResponse] 486 | 487 | def getHttpService(self): 488 | return self.service 489 | 490 | 491 | class IdentifiersPayloadGenerator(IIntruderPayloadGenerator): 492 | def __init__(self, source): 493 | self.ids = source.getIds() 494 | self.reset() 495 | 496 | def reset(self): 497 | self.workIds = list(self.ids) 498 | self.workIds.reverse() 499 | 500 | def hasMorePayloads(self): 501 | return len(self.workIds) > 0 502 | 503 | def getNextPayload(self, baseValue): 504 | try: 505 | return self.workIds.pop() 506 | except IndexError: 507 | return 508 | 509 | 510 | class MappingTableModel(AbstractTableModel): 511 | def __init__(self, callbacks): 512 | AbstractTableModel.__init__(self) 513 | self.columnnames = ["User/Object Identifier", "Content"] 514 | self.mappings = dict() 515 | self.idorder = list() 516 | self.lastadded = None 517 | self.callbacks = callbacks 518 | self.loadMapping() 519 | 520 | def getColumnCount(self): 521 | return len(self.columnnames) 522 | 523 | def getRowCount(self): 524 | return len(self.mappings) 525 | 526 | def getColumnName(self, col): 527 | return self.columnnames[col] 528 | 529 | def getValueAt(self, row, col): 530 | if col == 0: 531 | return self.idorder[row] 532 | else: 533 | return self.mappings[self.idorder[row]] 534 | 535 | def getColumnClass(self, idx): 536 | return str 537 | 538 | def isCellEditable(self, row, col): 539 | if col < 1: 540 | return False 541 | else: 542 | return True 543 | 544 | def add_mapping(self, ident, content): 545 | if ident not in self.mappings: 546 | self.idorder.append(ident) 547 | self.mappings[ident] = content 548 | self.lastadded = ident 549 | self.fireTableDataChanged() 550 | self.saveMapping() 551 | 552 | def set_lastadded_content(self, content): 553 | self.mappings[self.lastadded] = content 554 | self.fireTableDataChanged() 555 | 556 | def del_rows(self, rows): 557 | rows.sort() 558 | deleted = 0 559 | for row in rows: 560 | delkey = self.idorder[row - deleted] 561 | del self.mappings[delkey] 562 | if delkey == self.lastadded: 563 | self.lastadded = None 564 | if row - deleted > 0: 565 | self.idorder = self.idorder[:row - deleted] + self.idorder[row + 1 - deleted:] 566 | else: 567 | self.idorder = self.idorder[1:] 568 | self.fireTableRowsDeleted(row - deleted, row - deleted) 569 | deleted = deleted + 1 570 | self.saveMapping() 571 | 572 | def setValueAt(self, val, row, col): 573 | if col == 1: 574 | self.mappings[self.idorder[row]] = val 575 | self.fireTableCellUpdated(row, col) 576 | self.saveMapping() 577 | 578 | def getIds(self): 579 | return self.idorder 580 | 581 | def getValue(self, ident): 582 | return self.mappings[ident] 583 | 584 | def containsId(self, msg): 585 | for ident in self.idorder: 586 | if msg.find(ident) >= 0: 587 | return True 588 | return False 589 | 590 | def saveMapping(self): 591 | self.callbacks.saveExtensionSetting("mappings", pickle.dumps(self.mappings)) 592 | self.callbacks.saveExtensionSetting("idorder", pickle.dumps(self.idorder)) 593 | self.callbacks.saveExtensionSetting("lastadded", pickle.dumps(self.lastadded)) 594 | 595 | def loadMapping(self): 596 | storedMappings = self.callbacks.loadExtensionSetting("mappings") 597 | if isinstance(storedMappings, str): 598 | try: 599 | self.mappings = pickle.loads(storedMappings) or dict() 600 | except: 601 | self.mappings = dict() 602 | 603 | storedIdorder = self.callbacks.loadExtensionSetting("idorder") 604 | if isinstance(storedIdorder, str): 605 | try: 606 | self.idorder = pickle.loads(storedIdorder) or list() 607 | except: 608 | self.idorder = list() 609 | 610 | storedLastAdded = self.callbacks.loadExtensionSetting("lastadded") 611 | if isinstance(storedLastAdded, str): 612 | try: 613 | self.lastadded = pickle.loads(storedLastAdded) 614 | except: 615 | self.lastadded = None 616 | 617 | ### Global Functions ### 618 | 619 | # Find all occurrences of a string in a string 620 | # Input: two strings 621 | # Output: list of integer arrays (suitable as burp markers) 622 | def findAll(searchIn, searchVal): 623 | if searchVal == None or len(searchVal) == 0: 624 | return None 625 | found = list() 626 | length = len(searchVal) 627 | continueSearch = True 628 | offset = 0 629 | while continueSearch: 630 | pos = searchIn.find(searchVal) 631 | if pos >= 0: 632 | found.append(array('i', [pos + offset, pos + length + offset])) 633 | searchIn = searchIn[pos + length:] 634 | offset = offset + pos + length 635 | else: 636 | continueSearch = False 637 | if len(found) > 0: 638 | return found 639 | else: 640 | return None 641 | 642 | def getParamTypeStr(scanIssue): 643 | paramtype = scanIssue.param.getType() 644 | if paramtype == IParameter.PARAM_URL: 645 | return "URL parameter" 646 | elif paramtype == IParameter.PARAM_BODY: 647 | return "body parameter" 648 | elif paramtype == IParameter.PARAM_COOKIE: 649 | return "cookie" 650 | elif paramtype == IParameter.PARAM_XML: 651 | return "XML parameter" 652 | elif paramtype == IParameter.PARAM_XML_ATTR: 653 | return "XML attribute parameter" 654 | elif paramtype == IParameter.PARAM_MULTIPART_ATTR: 655 | return "multipart attribute parameter" 656 | elif paramtype == IParameter.PARAM_JSON: 657 | return "JSON parameter" 658 | else: 659 | return "parameter" 660 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ARCHIVED 2 | 3 | This project isn't maintained anymore by me. Feel free to fork! 4 | 5 | See [this blog article](https://patzke.org/the-burp-sessionauth-extension.html) for a full description of what this is. 6 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | * Multiple values per id 2 | 3 | * Generalize extension from recognition of user ids to object ids 4 | (e.g. documents, downloads etc.). User should be able to define 5 | multiple object groups. The active scanner module only exchanges 6 | the found id by ids from the same group. 7 | 8 | * Case insensitive match of identifiers and values as option 9 | 10 | * RegExp matches of identifiers and values 11 | 12 | * Add blacklist of URLs for login requets 13 | In login requests it is intended to submit an user id. No issue 14 | should be reported in these cases. 15 | 16 | * Disable detection for particular parameter types 17 | Some web applications store session data client-side, e.g. in 18 | cookies, and transmit the user name in such requests. In such cases 19 | the extensions is quite useless in current state because it would 20 | report all requests. 21 | 22 | * Add some heuristics to identify typical parameter names 23 | There are parameter names like uid, userId etc. which are typical 24 | suspicious to vulnerailities targeted by this extension. 25 | 26 | * Detection and automatic addition of user ids 27 | User identifiers and possibly values could be automatically 28 | recognized by usage of heuristics and suggested to the user for 29 | addition into the list. 30 | 31 | -------------------------------------------------------------------------------- /test/TestApp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import cgi 4 | import html 5 | 6 | testcases = { 7 | 'ScanValueAppearsExactly': { 8 | '123': 'AccessibleContent xxx AccessibleContent', 9 | '456': 'ProtectedContent xxx ProtectedContent' 10 | }, 11 | 'ScanValueAppearsFuzzy': { 12 | '123': 'AccessibleContent xxx AccessibleContent', 13 | '456': 'ProtectedContent xxx' 14 | }, 15 | 'DecreaseIncrease': { 16 | '123': 'AccessibleContent xxx AccessibleContent xxx ProtectedContent xxx ProtectedContent', 17 | '456': 'AccessibleContent xxx ProtectedContent xxx ProtectedContent xxx ProtectedContent' 18 | }, 19 | 'ScanValueIncrease': { 20 | '123': 'AccessibleContent xxx AccessibleContent xxx ProtectedContent xxx ProtectedContent', 21 | '456': 'AccessibleContent xxx AccessibleContent xxx ProtectedContent xxx ProtectedContent xxx ProtectedContent' 22 | }, 23 | 'ScanValueNotFound': { 24 | '123': 'xxx xxx xxx', 25 | '456': 'xxx yyy xxx' 26 | }, 27 | 'Other': { 28 | '123': 'AccessibleContent xxx AccessibleContent', 29 | '456': 'AccessibleContent xxx AccessibleContent xxx' 30 | }, 31 | 'NoDiff': { 32 | '123': 'AccessibleContent xxx AccessibleContent', 33 | '456': 'AccessibleContent xxx AccessibleContent' 34 | } 35 | } 36 | 37 | print("Content-Type: text/html") 38 | print("") 39 | 40 | param = cgi.FieldStorage() 41 | testtype = param.getfirst('type') or "" 42 | ident = param.getfirst('id') or "" 43 | 44 | if testtype in testcases: 45 | if ident in testcases[testtype]: 46 | print(testcases[testtype][ident]) 47 | else: 48 | print("

Burp SessionAuth Plugin Tests

") 49 | print("type=" + html.escape(testtype) + ", id=" + html.escape(ident) + "
") 50 | print("

IDs and Content Values

"); 51 | print("Configure the following identifiers and content values in Burp SessionAuth extension:") 52 | print("
  • id=123: AccessibleContent
  • id=456: ProtectedContent
") 53 | print("

Test Types

") 54 | print(""); 58 | -------------------------------------------------------------------------------- /test/TestServer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import BaseHTTPServer 4 | import CGIHTTPServer 5 | 6 | server = BaseHTTPServer.HTTPServer 7 | handler = CGIHTTPServer.CGIHTTPRequestHandler 8 | handler.cgi_directories = ["/"] 9 | 10 | httpd = BaseHTTPServer.HTTPServer(("", 8000), handler) 11 | httpd.serve_forever() 12 | --------------------------------------------------------------------------------