├── AuthMatrix.py ├── CHANGES ├── JsonState.md ├── LICENSE ├── README.md └── images ├── img1.png ├── img2.png ├── img3.png ├── img4.png ├── img5.png └── img6.png /AuthMatrix.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 Mick Ayzenberg - Security Innovation 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from burp import IBurpExtender 22 | from burp import ITab 23 | from burp import IMessageEditorController 24 | from burp import IContextMenuFactory 25 | from burp import IHttpRequestResponse 26 | 27 | from java.awt import Component; 28 | from java.awt import GridBagLayout; 29 | from java.awt import GridBagConstraints; 30 | from java.awt import Dimension; 31 | from java.util import ArrayList; 32 | from java.lang import Boolean; 33 | from javax.swing import JScrollPane; 34 | from javax.swing import JSplitPane; 35 | from javax.swing import JTabbedPane; 36 | from javax.swing import JPanel; 37 | from javax.swing import JButton; 38 | from javax.swing import JTable; 39 | from javax.swing import JOptionPane; 40 | from javax.swing import JMenuItem; 41 | from javax.swing import JCheckBox; 42 | from javax.swing import JComboBox; 43 | from javax.swing import DefaultCellEditor; 44 | from javax.swing import JLabel; 45 | from javax.swing import JFileChooser; 46 | from javax.swing import JPopupMenu; 47 | from javax.swing import JTextField; 48 | from javax.swing import TransferHandler; 49 | from javax.swing import DropMode; 50 | from javax.swing import JSeparator; 51 | from javax.swing import SwingConstants; 52 | from javax.swing import JList 53 | from javax.swing import AbstractCellEditor 54 | from javax.swing import Timer 55 | from java.awt.datatransfer import StringSelection; 56 | from java.awt.datatransfer import DataFlavor; 57 | from javax.swing.table import AbstractTableModel; 58 | from javax.swing.table import TableCellRenderer; 59 | from javax.swing.table import JTableHeader; 60 | from javax.swing.table import TableCellEditor 61 | from java.awt import Color; 62 | from java.awt import Font; 63 | from java.awt.event import MouseAdapter; 64 | from java.awt.event import ActionListener; 65 | from java.awt.event import ItemListener; 66 | from java.awt.event import ItemEvent; 67 | from javax.swing.event import DocumentListener; 68 | from javax.swing.event import ChangeListener; 69 | import java.lang; 70 | 71 | from org.python.core.util import StringUtil 72 | from threading import Lock 73 | from threading import Thread 74 | import traceback 75 | import re 76 | import urllib 77 | import hashlib 78 | import json 79 | import base64 80 | import random 81 | import string 82 | 83 | 84 | AUTHMATRIX_VERSION = "0.8.2" 85 | 86 | 87 | class BurpExtender(IBurpExtender, ITab, IMessageEditorController, IContextMenuFactory): 88 | 89 | # 90 | # implement IBurpExtender 91 | # 92 | 93 | def registerExtenderCallbacks(self, callbacks): 94 | 95 | # keep a reference to our Burp callbacks object 96 | self._callbacks = callbacks 97 | # obtain an Burp extension helpers object 98 | self._helpers = callbacks.getHelpers() 99 | # set our extension name 100 | callbacks.setExtensionName("AuthMatrix - v"+AUTHMATRIX_VERSION) 101 | 102 | # DB that holds everything users, roles, and messages 103 | self._db = MatrixDB() 104 | 105 | # For saving/loading config 106 | self._fc = JFileChooser() 107 | 108 | # Used by inner classes 109 | selfExtender = self 110 | self._selectedColumn = -1 111 | self._selectedRow = -1 112 | 113 | # Table of Chain entries (NOTE: must be instantiated before userTable since its referenced) 114 | self._chainTable = ChainTable(model = ChainTableModel(self)) 115 | chainScrollPane = JScrollPane(self._chainTable) 116 | self._chainTable.redrawTable() 117 | 118 | # Table of User entries 119 | self._userTable = UserTable(model = UserTableModel(self)) 120 | roleScrollPane = JScrollPane(self._userTable) 121 | self._userTable.redrawTable() 122 | 123 | # Table of Request (AKA Message) entries 124 | self._messageTable = MessageTable(model = MessageTableModel(self)) 125 | messageScrollPane = JScrollPane(self._messageTable) 126 | self._messageTable.redrawTable() 127 | 128 | # Set Messages to reorderable 129 | self._messageTable.setDragEnabled(True) 130 | self._messageTable.setDropMode(DropMode.INSERT_ROWS) 131 | self._messageTable.setTransferHandler(RowTransferHandler(self._messageTable)) 132 | 133 | # Set Users to reorderable 134 | self._userTable.setDragEnabled(True) 135 | self._userTable.setDropMode(DropMode.INSERT_ROWS) 136 | self._userTable.setTransferHandler(RowTransferHandler(self._userTable)) 137 | 138 | 139 | 140 | 141 | # Semi-Generic Popup stuff 142 | def addPopup(component, popup): 143 | class genericMouseListener(MouseAdapter): 144 | def mousePressed(self, e): 145 | if e.isPopupTrigger(): 146 | self.showMenu(e) 147 | def mouseReleased(self, e): 148 | if e.isPopupTrigger(): 149 | self.showMenu(e) 150 | def showMenu(self, e): 151 | # NOTE: testing if .locked is ok here since its a manual operation 152 | if selfExtender._db.lock.locked(): 153 | return 154 | 155 | if type(component) is JTableHeader: 156 | table = component.getTable() 157 | column = component.columnAtPoint(e.getPoint()) 158 | if (type(table) is MessageTable 159 | and column >= selfExtender._db.STATIC_MESSAGE_TABLE_COLUMN_COUNT 160 | or type(table) is UserTable 161 | and column >= selfExtender._db.STATIC_USER_TABLE_COLUMN_COUNT): 162 | selfExtender._selectedColumn = column 163 | else: 164 | return 165 | else: 166 | selfExtender._selectedRow = component.rowAtPoint(e.getPoint()) 167 | popup.show(e.getComponent(), e.getX(), e.getY()) 168 | component.addMouseListener(genericMouseListener()) 169 | 170 | class actionRunMessage(ActionListener): 171 | def actionPerformed(self,e): 172 | if selfExtender._selectedRow >= 0: 173 | if selfExtender._selectedRow not in selfExtender._messageTable.getSelectedRows(): 174 | indexes = [selfExtender._db.getMessageByRow(selfExtender._selectedRow)._index] 175 | else: 176 | indexes = [selfExtender._db.getMessageByRow(rowNum)._index for rowNum in selfExtender._messageTable.getSelectedRows()] 177 | t = Thread(target=selfExtender.runMessagesThread, args = [indexes]) 178 | t.start() 179 | selfExtender._selectedColumn = -1 180 | # Redrawing the table happens in colorcode within the thread 181 | 182 | class actionToggleEnableUser(ActionListener): 183 | def actionPerformed(self,e): 184 | if selfExtender._selectedRow >= 0: 185 | if selfExtender._selectedRow not in selfExtender._userTable.getSelectedRows(): 186 | usersArray = [selfExtender._db.getUserByRow(selfExtender._selectedRow)] 187 | else: 188 | usersArray = [selfExtender._db.getUserByRow(rowNum) for rowNum in selfExtender._userTable.getSelectedRows()] 189 | for userEntry in usersArray: 190 | userEntry.toggleEnabled() 191 | selfExtender._selectedColumn = -1 192 | selfExtender._userTable.redrawTable() 193 | 194 | class actionToggleEnableMessage(ActionListener): 195 | def actionPerformed(self,e): 196 | if selfExtender._selectedRow >= 0: 197 | if selfExtender._selectedRow not in selfExtender._messageTable.getSelectedRows(): 198 | messagesArray = [selfExtender._db.getMessageByRow(selfExtender._selectedRow)] 199 | else: 200 | messagesArray = [selfExtender._db.getMessageByRow(rowNum) for rowNum in selfExtender._messageTable.getSelectedRows()] 201 | for messageEntry in messagesArray: 202 | messageEntry.toggleEnabled() 203 | selfExtender._selectedColumn = -1 204 | selfExtender._messageTable.redrawTable() 205 | 206 | class actionToggleEnableChain(ActionListener): 207 | def actionPerformed(self,e): 208 | if selfExtender._selectedRow >= 0: 209 | if selfExtender._selectedRow not in selfExtender._chainTable.getSelectedRows(): 210 | chainArray = [selfExtender._db.getChainByRow(selfExtender._selectedRow)] 211 | else: 212 | chainArray = [selfExtender._db.getChainByRow(rowNum) for rowNum in selfExtender._chainTable.getSelectedRows()] 213 | for chainEntry in chainArray: 214 | chainEntry.toggleEnabled() 215 | selfExtender._selectedColumn = -1 216 | selfExtender._chainTable.redrawTable() 217 | 218 | 219 | 220 | class actionRemoveMessage(ActionListener): 221 | def actionPerformed(self,e): 222 | if selfExtender._selectedRow >= 0: 223 | if selfExtender._selectedRow not in selfExtender._messageTable.getSelectedRows(): 224 | indexes = [selfExtender._db.getMessageByRow(selfExtender._selectedRow)._index] 225 | else: 226 | indexes = [selfExtender._db.getMessageByRow(rowNum)._index for rowNum in selfExtender._messageTable.getSelectedRows()] 227 | for i in indexes: 228 | selfExtender._db.deleteMessage(i) 229 | selfExtender._selectedColumn = -1 230 | selfExtender._messageTable.redrawTable() 231 | selfExtender._chainTable.redrawTable() 232 | 233 | class actionRemoveUser(ActionListener): 234 | def actionPerformed(self,e): 235 | if selfExtender._selectedRow >= 0: 236 | if selfExtender._selectedRow not in selfExtender._userTable.getSelectedRows(): 237 | indexes = [selfExtender._db.getUserByRow(selfExtender._selectedRow)._index] 238 | else: 239 | indexes = [selfExtender._db.getUserByRow(rowNum)._index for rowNum in selfExtender._userTable.getSelectedRows()] 240 | for i in indexes: 241 | selfExtender._db.deleteUser(i) 242 | selfExtender._selectedColumn = -1 243 | selfExtender._userTable.redrawTable() 244 | selfExtender._chainTable.redrawTable() 245 | 246 | class actionRemoveChain(ActionListener): 247 | def actionPerformed(self,e): 248 | if selfExtender._selectedRow >= 0: 249 | if selfExtender._selectedRow not in selfExtender._chainTable.getSelectedRows(): 250 | indexes = [selfExtender._db.getChainByRow(selfExtender._selectedRow)._index] 251 | else: 252 | indexes = [selfExtender._db.getChainByRow(rowNum)._index for rowNum in selfExtender._chainTable.getSelectedRows()] 253 | for i in indexes: 254 | selfExtender._db.deleteChain(i) 255 | selfExtender._selectedColumn = -1 256 | selfExtender._chainTable.redrawTable() 257 | 258 | class actionRemoveColumn(ActionListener): 259 | 260 | def __init__(self, table): 261 | self._table = table 262 | 263 | def actionPerformed(self,e): 264 | if selfExtender._selectedColumn >= 0: 265 | if self._table == "u": 266 | # Delete Role 267 | if selfExtender._selectedColumn >= selfExtender._db.STATIC_USER_TABLE_COLUMN_COUNT + selfExtender._db.headerCount + selfExtender._db.arrayOfSVs.size(): 268 | selfExtender._db.deleteRole(selfExtender._db.getRoleByColumn( 269 | selfExtender._selectedColumn, self._table)._index) 270 | 271 | # Delete SV 272 | elif selfExtender._selectedColumn >= selfExtender._db.STATIC_USER_TABLE_COLUMN_COUNT + selfExtender._db.headerCount: 273 | selfExtender._db.deleteSV(selfExtender._selectedColumn-(selfExtender._db.STATIC_USER_TABLE_COLUMN_COUNT+selfExtender._db.headerCount)) 274 | 275 | # Delete Header 276 | elif selfExtender._selectedColumn >= selfExtender._db.STATIC_USER_TABLE_COLUMN_COUNT: 277 | selfExtender._db.deleteHeader(selfExtender._selectedColumn-selfExtender._db.STATIC_USER_TABLE_COLUMN_COUNT) 278 | 279 | elif self._table == "m": 280 | # Delete Role 281 | selfExtender._db.deleteRole(selfExtender._db.getRoleByColumn( 282 | selfExtender._selectedColumn, self._table)._index) 283 | 284 | selfExtender._selectedColumn = -1 285 | selfExtender._userTable.redrawTable() 286 | selfExtender._messageTable.redrawTable() 287 | selfExtender._chainTable.redrawTable() 288 | 289 | class actionToggleRegex(ActionListener): 290 | def actionPerformed(self,e): 291 | if selfExtender._selectedRow >= 0: 292 | if selfExtender._selectedRow not in selfExtender._messageTable.getSelectedRows(): 293 | messages = [selfExtender._db.getMessageByRow(selfExtender._selectedRow)] 294 | else: 295 | messages = [selfExtender._db.getMessageByRow(rowNum) for rowNum in selfExtender._messageTable.getSelectedRows()] 296 | for m in messages: 297 | m.setFailureRegex(not m.isFailureRegex()) 298 | m.clearResults() 299 | selfExtender._selectedColumn = -1 300 | selfExtender._messageTable.redrawTable() 301 | 302 | class actionChangeRegexes(ActionListener): 303 | def actionPerformed(self,e): 304 | if selfExtender._selectedRow >= 0: 305 | if selfExtender._selectedRow not in selfExtender._messageTable.getSelectedRows(): 306 | messages = [selfExtender._db.getMessageByRow(selfExtender._selectedRow)] 307 | else: 308 | messages = [selfExtender._db.getMessageByRow(rowNum) for rowNum in selfExtender._messageTable.getSelectedRows()] 309 | 310 | newRegex,failureRegex = selfExtender.changeRegexPopup() 311 | if newRegex: 312 | for message in messages: 313 | message._regex = newRegex 314 | message.setFailureRegex(failureRegex) 315 | # Add to list of regexes if its not already there 316 | if newRegex not in selfExtender._db.arrayOfRegexes: 317 | selfExtender._db.arrayOfRegexes.append(newRegex) 318 | 319 | selfExtender._selectedColumn = -1 320 | selfExtender._messageTable.redrawTable() 321 | 322 | 323 | class actionChangeDomain(ActionListener): 324 | def replaceDomain(self, requestResponse, newDomain): 325 | requestInfo = selfExtender._helpers.analyzeRequest(requestResponse) 326 | reqBody = requestResponse.getRequest()[requestInfo.getBodyOffset():] 327 | newHeaders = ModifyMessage.getNewHeaders(requestInfo, None, ["Host: "+newDomain]) 328 | newreq = selfExtender._helpers.buildHttpMessage(newHeaders, reqBody) 329 | return newreq 330 | 331 | def actionPerformed(self,e): 332 | if selfExtender._selectedRow >= 0: 333 | if selfExtender._selectedRow not in selfExtender._messageTable.getSelectedRows(): 334 | messages = [selfExtender._db.getMessageByRow(selfExtender._selectedRow)] 335 | else: 336 | messages = [selfExtender._db.getMessageByRow(rowNum) for rowNum in selfExtender._messageTable.getSelectedRows()] 337 | 338 | # Autofill the service values if they are all the same 339 | uniqueServices = [(message._requestResponse.getHttpService().getHost(), 340 | message._requestResponse.getHttpService().getPort(), 341 | message._requestResponse.getHttpService().getProtocol()) for message in messages] 342 | service = None if len(set(uniqueServices)) != 1 else messages[0]._requestResponse.getHttpService() 343 | 344 | ok, host, port, tls, replaceHost = selfExtender.changeDomainPopup(service) 345 | if ok and host: 346 | if not port or not port.isdigit(): 347 | port = 443 if tls else 80 348 | for m in messages: 349 | if replaceHost: 350 | request = self.replaceDomain(m._requestResponse, host) 351 | else: 352 | request = m._requestResponse.getRequest() 353 | # TODO save the Response? 354 | m._requestResponse = RequestResponseStored(selfExtender, host, int(port), "https" if tls else "http", request) 355 | m.clearResults() 356 | selfExtender._selectedColumn = -1 357 | selfExtender._messageTable.redrawTable() 358 | 359 | class actionSetToggleForRole(ActionListener): 360 | def __init__(self, enabled): 361 | self._enabled = enabled 362 | 363 | def actionPerformed(self, e): 364 | if selfExtender._selectedColumn >= 0: 365 | messageIndexes = [selfExtender._db.getMessageByRow(rowNum)._index for rowNum in selfExtender._messageTable.getSelectedRows()] 366 | for messageIndex in messageIndexes: 367 | roleIndex = selfExtender._db.getRoleByColumn(selfExtender._selectedColumn, "m")._index 368 | selfExtender._db.setToggleForRole(messageIndex, roleIndex, self._enabled) 369 | selfExtender._selectedColumn = -1 370 | selfExtender._messageTable.redrawTable() 371 | 372 | # Message Table popups 373 | messagePopup = JPopupMenu() 374 | addPopup(self._messageTable,messagePopup) 375 | toggleEnabled = JMenuItem("Disable/Enable Request(s)") 376 | toggleEnabled.addActionListener(actionToggleEnableMessage()) 377 | messagePopup.add(toggleEnabled) 378 | messageRun = JMenuItem("Run Request(s)") 379 | messageRun.addActionListener(actionRunMessage()) 380 | messagePopup.add(messageRun) 381 | toggleRegex = JMenuItem("Toggle Regex Mode (Success/Failure)") 382 | toggleRegex.addActionListener(actionToggleRegex()) 383 | messagePopup.add(toggleRegex) 384 | changeRegex = JMenuItem("Change Regexes") 385 | changeRegex.addActionListener(actionChangeRegexes()) 386 | messagePopup.add(changeRegex) 387 | changeDomain = JMenuItem("Change Target Domain") 388 | changeDomain.addActionListener(actionChangeDomain()) 389 | messagePopup.add(changeDomain) 390 | messageRemove = JMenuItem("Remove Request(s)") 391 | messageRemove.addActionListener(actionRemoveMessage()) 392 | messagePopup.add(messageRemove) 393 | 394 | 395 | 396 | messageHeaderPopup = JPopupMenu() 397 | addPopup(self._messageTable.getTableHeader(),messageHeaderPopup) 398 | roleRemoveFromMessageTable = JMenuItem("Remove Role") 399 | roleRemoveFromMessageTable.addActionListener(actionRemoveColumn("m")) 400 | messageHeaderPopup.add(roleRemoveFromMessageTable) 401 | enableToggle = JMenuItem("Bulk Select Checkboxes") 402 | enableToggle.addActionListener(actionSetToggleForRole(True)) 403 | messageHeaderPopup.add(enableToggle) 404 | disableToggle = JMenuItem("Bulk Unselect Checkboxes") 405 | disableToggle.addActionListener(actionSetToggleForRole(False)) 406 | messageHeaderPopup.add(disableToggle) 407 | 408 | # User Table popup 409 | userPopup = JPopupMenu() 410 | addPopup(self._userTable,userPopup) 411 | toggleEnabled = JMenuItem("Disable/Enable User(s)") 412 | toggleEnabled.addActionListener(actionToggleEnableUser()) 413 | userPopup.add(toggleEnabled) 414 | userRemove = JMenuItem("Remove Users(s)") 415 | userRemove.addActionListener(actionRemoveUser()) 416 | userPopup.add(userRemove) 417 | 418 | 419 | userHeaderPopup = JPopupMenu() 420 | addPopup(self._userTable.getTableHeader(),userHeaderPopup) 421 | removeColumnFromUserTable = JMenuItem("Remove") 422 | removeColumnFromUserTable.addActionListener(actionRemoveColumn("u")) 423 | userHeaderPopup.add(removeColumnFromUserTable) 424 | 425 | # Chain Table popup 426 | chainPopup = JPopupMenu() 427 | addPopup(self._chainTable,chainPopup) 428 | toggleEnabled = JMenuItem("Disable/Enable Chain(s)") 429 | toggleEnabled.addActionListener(actionToggleEnableChain()) 430 | chainPopup.add(toggleEnabled) 431 | chainRemove = JMenuItem("Remove Chain(s)") 432 | chainRemove.addActionListener(actionRemoveChain()) 433 | chainPopup.add(chainRemove) 434 | 435 | 436 | # request tabs added to this tab on click in message table 437 | self._tabs = JTabbedPane() 438 | # Add change listener to set currentDisplayedItem 439 | class TabChangeListener(ChangeListener): 440 | def stateChanged(self, e): 441 | if type(e.getSource()) == JTabbedPane and e.getSource().getSelectedIndex()>=0: 442 | selfExtender._currentlyDisplayedItem = e.getSource().getSelectedComponent()._requestResponse 443 | self._tabs.addChangeListener(TabChangeListener()) 444 | 445 | 446 | # Button pannel 447 | buttons = JPanel() 448 | self._runButton = JButton("Run", actionPerformed=self.runClick) 449 | self._cancelButton = JButton("Cancel", actionPerformed=self.cancelClick) 450 | self._newUserButton = JButton("New User", actionPerformed=self.getInputUserClick) 451 | self._newRoleButton = JButton("New Role", actionPerformed=self.getInputRoleClick) 452 | self._newHeaderButton = JButton("New Header", actionPerformed=self.newHeaderClick) 453 | self._newChainButton = JButton("New Chain", actionPerformed=self.newChainClick) 454 | self._newStaticValueButton = JButton("New Chain Source", actionPerformed=self.newStaticValueClick) 455 | self._saveButton = JButton("Save", actionPerformed=self.saveClick) 456 | self._loadButton = JButton("Load", actionPerformed=self.loadClick) 457 | self._clearButton = JButton("Clear", actionPerformed=self.clearClick) 458 | 459 | buttons.add(self._runButton) 460 | buttons.add(self._cancelButton) 461 | self._cancelButton.setEnabled(False) 462 | separator1 = JSeparator(SwingConstants.VERTICAL) 463 | separator1.setPreferredSize(Dimension(25,0)) 464 | buttons.add(separator1) 465 | buttons.add(self._newUserButton) 466 | buttons.add(self._newRoleButton) 467 | buttons.add(self._newHeaderButton) 468 | separator2 = JSeparator(SwingConstants.VERTICAL) 469 | separator2.setPreferredSize(Dimension(25,0)) 470 | buttons.add(separator2) 471 | buttons.add(self._newChainButton) 472 | buttons.add(self._newStaticValueButton) 473 | separator3 = JSeparator(SwingConstants.VERTICAL) 474 | separator3.setPreferredSize(Dimension(25,0)) 475 | buttons.add(separator3) 476 | buttons.add(self._saveButton) 477 | buttons.add(self._loadButton) 478 | buttons.add(self._clearButton) 479 | 480 | # Top pane 481 | firstPane = JSplitPane(JSplitPane.VERTICAL_SPLIT,roleScrollPane,messageScrollPane) 482 | self._topPane = JSplitPane(JSplitPane.VERTICAL_SPLIT, firstPane, chainScrollPane) 483 | bottomPane = JSplitPane(JSplitPane.VERTICAL_SPLIT, self._tabs, buttons) 484 | 485 | # Main Pane 486 | self._splitpane = JSplitPane(JSplitPane.VERTICAL_SPLIT, self._topPane, bottomPane) 487 | 488 | 489 | 490 | # customize our UI components 491 | callbacks.customizeUiComponent(self._splitpane) 492 | callbacks.customizeUiComponent(firstPane) 493 | callbacks.customizeUiComponent(self._topPane) 494 | callbacks.customizeUiComponent(bottomPane) 495 | callbacks.customizeUiComponent(messageScrollPane) 496 | callbacks.customizeUiComponent(roleScrollPane) 497 | callbacks.customizeUiComponent(chainScrollPane) 498 | callbacks.customizeUiComponent(self._messageTable) 499 | callbacks.customizeUiComponent(self._userTable) 500 | callbacks.customizeUiComponent(self._chainTable) 501 | callbacks.customizeUiComponent(self._tabs) 502 | callbacks.customizeUiComponent(buttons) 503 | 504 | self._splitpane.setResizeWeight(0.5) 505 | firstPane.setResizeWeight(0.35) 506 | self._topPane.setResizeWeight(0.85) 507 | bottomPane.setResizeWeight(0.95) 508 | 509 | # Handles checkbox, regex, and enabled coloring 510 | # Must be bellow the customizeUiComponent calls 511 | self._messageTable.setDefaultRenderer(Boolean, SuccessBooleanRenderer(self._messageTable.getDefaultRenderer(Boolean), self._db)) 512 | self._messageTable.setDefaultRenderer(str, RegexRenderer(self._messageTable.getDefaultRenderer(str), self._db)) 513 | self._userTable.setDefaultRenderer(str, UserEnabledRenderer(self._userTable.getDefaultRenderer(str), self._db)) 514 | self._userTable.setDefaultRenderer(Boolean, UserEnabledRenderer(self._userTable.getDefaultRenderer(Boolean), self._db)) 515 | self._chainTable.setDefaultRenderer(str, ChainEnabledRenderer(self._chainTable.getDefaultRenderer(str), self._db)) 516 | 517 | 518 | # add the custom tab to Burp's UI 519 | callbacks.addSuiteTab(self) 520 | # register SendTo option 521 | callbacks.registerContextMenuFactory(self) 522 | 523 | return 524 | 525 | ## 526 | ## implement ITab 527 | ## 528 | 529 | 530 | def getTabCaption(self): 531 | return "AuthMatrix" 532 | 533 | def getUiComponent(self): 534 | return self._splitpane 535 | 536 | def highlightTab(self): 537 | currentPane = self._splitpane 538 | previousPane = currentPane 539 | while currentPane and not isinstance(currentPane, JTabbedPane): 540 | previousPane = currentPane 541 | currentPane = currentPane.getParent() 542 | if currentPane: 543 | index = currentPane.indexOfComponent(previousPane) 544 | # TODO use old background instead of black (currently doesnt work) 545 | #oldBackground = currentPane.getBackgroundAt(index) 546 | currentPane.setBackgroundAt(index,self._db.BURP_ORANGE) 547 | 548 | class setColorBackActionListener(ActionListener): 549 | def actionPerformed(self, e): 550 | currentPane.setBackgroundAt(index,Color.BLACK) 551 | 552 | timer = Timer(5000, setColorBackActionListener()) 553 | timer.setRepeats(False) 554 | timer.start() 555 | 556 | 557 | 558 | ## 559 | ## Creates the sendto tab in other areas of Burp 560 | ## 561 | 562 | def createMenuItems(self, invocation): 563 | 564 | 565 | 566 | 567 | def addRequestsToTab(e): 568 | for messageInfo in messages: 569 | requestInfo = self._helpers.analyzeRequest(messageInfo) 570 | name = str(requestInfo.getMethod()).ljust(8) + requestInfo.getUrl().getPath() 571 | # Grab regex from response 572 | regex = "^HTTP/1\\.1 200 OK" 573 | response = messageInfo.getResponse() 574 | if response: 575 | responseInfo=self._helpers.analyzeResponse(response) 576 | if len(responseInfo.getHeaders()): 577 | responseCodeHeader = responseInfo.getHeaders()[0] 578 | regex = "^"+re.escape(responseCodeHeader) 579 | # Must create a new RequestResponseStored object since modifying the original messageInfo 580 | # from its source (such as Repeater) changes this saved object. MessageInfo is a reference, not a copy 581 | messageIndex = self._db.createNewMessage(RequestResponseStored(self,requestResponse=messageInfo), name, regex) 582 | self._messageTable.redrawTable() 583 | self._chainTable.redrawTable() 584 | self.highlightTab() 585 | 586 | 587 | class UserCookiesActionListener(ActionListener): 588 | def __init__(self, currentUser, extender): 589 | self.currentUser=currentUser 590 | self.extender = extender 591 | 592 | def actionPerformed(self, e): 593 | for messageInfo in messages: 594 | cookieVal = "" 595 | requestInfo = self.extender._helpers.analyzeRequest(messageInfo) 596 | for header in requestInfo.getHeaders(): 597 | cookieStr = "Cookie: " 598 | if header.startswith(cookieStr): 599 | cookieVal = header[len(cookieStr):] 600 | 601 | # Grab Set-Cookie headers from the responses as well 602 | response = messageInfo.getResponse() 603 | if response: 604 | responseInfo = self.extender._helpers.analyzeResponse(response) 605 | responseCookies = responseInfo.getCookies() 606 | newCookies = "; ".join([x.getName()+"="+x.getValue() for x in responseCookies]) 607 | cookieVal = ModifyMessage.cookieReplace(cookieVal,newCookies) 608 | 609 | self.currentUser._cookies = cookieVal 610 | 611 | self.extender._userTable.redrawTable() 612 | self.extender.highlightTab() 613 | 614 | ret = [] 615 | messages = invocation.getSelectedMessages() 616 | 617 | # Check if the messages in the target tree have a response 618 | valid = True 619 | if invocation.getInvocationContext() == invocation.CONTEXT_TARGET_SITE_MAP_TREE: 620 | for selected in messages: 621 | if not selected.getResponse(): 622 | valid = False 623 | 624 | if valid: 625 | menuItem = JMenuItem("Send request(s) to AuthMatrix"); 626 | menuItem.addActionListener(addRequestsToTab) 627 | ret.append(menuItem) 628 | 629 | if len(messages)==1: 630 | # Send cookies to user: 631 | for user in self._db.getUsersInOrderByRow(): 632 | menuItem = JMenuItem("Send cookies to AuthMatrix user: "+user._name); 633 | menuItem.addActionListener(UserCookiesActionListener(user, self)) 634 | ret.append(menuItem) 635 | 636 | return ret 637 | 638 | ## 639 | ## implement IMessageEditorController 640 | ## this allows our request/response viewers to obtain details about the messages being displayed 641 | ## 642 | 643 | def getHttpService(self): 644 | return self._currentlyDisplayedItem.getHttpService() 645 | 646 | def getRequest(self): 647 | return self._currentlyDisplayedItem.getRequest() 648 | 649 | def getResponse(self): 650 | return self._currentlyDisplayedItem.getResponse() 651 | 652 | ## 653 | ## Actions on Bottom Row Button Clicks 654 | ## 655 | 656 | def getInputUserClick(self, e): 657 | newUser = JOptionPane.showInputDialog(self._splitpane,"Enter New User:") 658 | if newUser: 659 | self._db.getOrCreateUser(newUser) 660 | self._userTable.redrawTable() 661 | # redraw Message Table since it adds a new SingleUser Role 662 | self._messageTable.redrawTable() 663 | self._chainTable.redrawTable() 664 | 665 | def getInputRoleClick(self, e): 666 | newRole = JOptionPane.showInputDialog(self._splitpane,"Enter New Role:") 667 | if newRole: 668 | self._db.getOrCreateRole(newRole) 669 | self._userTable.redrawTable() 670 | self._messageTable.redrawTable() 671 | 672 | def newChainClick(self,e): 673 | self._db.createNewChain() 674 | self._chainTable.redrawTable() 675 | 676 | def newHeaderClick(self, e): 677 | self._db.addNewHeader() 678 | self._userTable.redrawTable() 679 | 680 | def newStaticValueClick(self, e): 681 | newSV = JOptionPane.showInputDialog(self._splitpane,"Enter a label for the new Chain Source:") 682 | if newSV: 683 | self._db.addNewSV(newSV) 684 | self._userTable.redrawTable() 685 | self._chainTable.redrawTable() 686 | 687 | def cancelClick(self,e): 688 | self._runCancelled = True 689 | self._cancelButton.setEnabled(False) 690 | 691 | def saveClick(self, e): 692 | # Update original requests with any user changes 693 | self._messageTable.updateMessages() 694 | 695 | returnVal = self._fc.showSaveDialog(self._splitpane) 696 | if returnVal == JFileChooser.APPROVE_OPTION: 697 | f = self._fc.getSelectedFile() 698 | if f.exists(): 699 | result = JOptionPane.showConfirmDialog(self._splitpane, "The file exists, overwrite?", "Existing File", JOptionPane.YES_NO_OPTION) 700 | if result != JOptionPane.YES_OPTION: 701 | return 702 | fileName = f.getPath() 703 | # TODO Potential bug here. Check if the value being written is 0 before opening 704 | # TODO add a try catch here? 705 | jsonValue = self._db.getSaveableJson() 706 | if jsonValue: 707 | fileout = open(fileName,'w') 708 | fileout.write(jsonValue) 709 | fileout.close() 710 | else: 711 | # TODO popup errors instead of prints 712 | print "Error: Save Failed. JSON empty." 713 | # TODO currently this will save the config to burp, but not to a specific project 714 | # Will also need an export and loadFromFile feature if this is ever implemented 715 | # self._callbacks.saveExtensionSetting("AUTHMATRIX", self._db.getSaveableJson()) 716 | 717 | def loadClick(self,e): 718 | returnVal = self._fc.showOpenDialog(self._splitpane) 719 | if returnVal == JFileChooser.APPROVE_OPTION: 720 | f = self._fc.getSelectedFile() 721 | fileName = f.getPath() 722 | 723 | filein = open(fileName,'r') 724 | jsonText = filein.read() 725 | filein.close() 726 | # Check if using on older state file compatible with v0.5.2 or greater 727 | if not jsonText or jsonText[0] !="{": 728 | warning = """ 729 | CAUTION: 730 | 731 | Loading a saved configuration prior to v0.6.3 deserializes data into Jython objects. 732 | This action may pose a security threat to the application. 733 | Only proceed when the source and contents of this file is trusted. 734 | 735 | Load Selected File? 736 | """ 737 | result = JOptionPane.showOptionDialog(self._splitpane, 738 | warning, "Caution", 739 | JOptionPane.YES_NO_OPTION, 740 | JOptionPane.WARNING_MESSAGE, 741 | None, 742 | ["OK", "Cancel"], 743 | "OK") 744 | 745 | if result != JOptionPane.YES_OPTION: 746 | return 747 | self._db.loadLegacy(fileName,self) 748 | else: 749 | self._db.loadJson(jsonText,self) 750 | # TODO currently can load exention settings, but this is saved for Burp and not for the Project specifically 751 | # self._db.loadJson(self._callbacks.loadExtensionSetting("AUTHMATRIX"),self) 752 | 753 | self._userTable.redrawTable() 754 | self._messageTable.redrawTable() 755 | self._chainTable.redrawTable() 756 | 757 | def clearClick(self,e): 758 | result = JOptionPane.showConfirmDialog(self._splitpane, "Clear AuthMatrix Configuration?", "Clear Config", JOptionPane.YES_NO_OPTION) 759 | if result == JOptionPane.YES_OPTION: 760 | self._db.clear() 761 | self._tabs.removeAll() 762 | self._userTable.redrawTable() 763 | self._messageTable.redrawTable() 764 | self._chainTable.redrawTable() 765 | 766 | def runClick(self,e): 767 | t = Thread(target=self.runMessagesThread) 768 | self._tabs.removeAll() 769 | t.start() 770 | 771 | def changeRegexPopup(self): 772 | regexComboBox = JComboBox(self._db.arrayOfRegexes) 773 | regexComboBox.setEditable(True) 774 | failureModeCheckbox = JCheckBox() 775 | 776 | panel = JPanel(GridBagLayout()) 777 | gbc = GridBagConstraints() 778 | gbc.anchor = GridBagConstraints.WEST 779 | firstline = JPanel() 780 | firstline.add(JLabel("Select a Regex for all selected Requests:")) 781 | secondline = JPanel() 782 | secondline.add(regexComboBox) 783 | thirdline = JPanel() 784 | thirdline.add(failureModeCheckbox) 785 | thirdline.add(JLabel("Regex Detects Unauthorized Requests (Failure Mode)")) 786 | 787 | 788 | gbc.gridy = 0 789 | panel.add(firstline,gbc) 790 | gbc.gridy = 1 791 | panel.add(secondline, gbc) 792 | gbc.gridy = 2 793 | panel.add(thirdline, gbc) 794 | 795 | 796 | result = JOptionPane.showConfirmDialog(self._splitpane, panel, "Select Response Regex", JOptionPane.OK_CANCEL_OPTION) 797 | value = regexComboBox.getSelectedItem() 798 | if result == JOptionPane.CANCEL_OPTION or not value: 799 | return None, None 800 | return value, failureModeCheckbox.isSelected() 801 | 802 | 803 | 804 | 805 | def changeDomainPopup(self, service): 806 | hostField = JTextField(25) 807 | portField = JTextField(25) 808 | checkbox = JCheckBox() 809 | 810 | replaceHostCheckbox = JCheckBox() 811 | replaceHostCheckbox.setSelected(True) 812 | 813 | errorField = JLabel("\n") 814 | errorField.setForeground(Color.orange); 815 | errorField.setFont 816 | 817 | def isValidDomain(domain): 818 | return re.match(r'^[a-zA-Z0-9-\.]+$', domain) 819 | 820 | if service: 821 | hostField.setText(service.getHost()) 822 | portField.setText(str(service.getPort())) 823 | if service.getProtocol()=="https": 824 | checkbox.setSelected(True) 825 | 826 | class HttpsItemListener(ItemListener): 827 | def itemStateChanged(self, e): 828 | if e.getStateChange() == ItemEvent.SELECTED and portField.getText() == "80": 829 | portField.setText("443") 830 | elif e.getStateChange() == ItemEvent.DESELECTED and portField.getText() == "443": 831 | portField.setText("80") 832 | checkbox.addItemListener(HttpsItemListener()) 833 | 834 | class HostDocumentListener(DocumentListener): 835 | def changeUpdate(self, e): 836 | self.testHost() 837 | def removeUpdate(self, e): 838 | self.testHost() 839 | def insertUpdate(self, e): 840 | self.testHost() 841 | 842 | def testHost(self): 843 | domain = hostField.getText() 844 | matches = isValidDomain(domain) 845 | if not matches: 846 | # NOTE Hacky way to fix layout when host is long 847 | if len(domain)>40: 848 | domain = domain[:40]+"..." 849 | errorField.setText("Invalid host: "+domain) 850 | else: 851 | errorField.setText("\n") 852 | hostField.getDocument().addDocumentListener(HostDocumentListener()) 853 | 854 | domainPanel = JPanel(GridBagLayout()) 855 | gbc = GridBagConstraints() 856 | gbc.anchor = GridBagConstraints.WEST 857 | 858 | firstline = JPanel() 859 | firstline.add(JLabel("Specify the details of the server to which the request will be sent.")) 860 | secondline = JPanel() 861 | secondline.add(JLabel("Host: ")) 862 | secondline.add(hostField) 863 | thirdline = JPanel() 864 | thirdline.add(JLabel("Port: ")) 865 | thirdline.add(portField) 866 | fourthline = JPanel() 867 | fourthline.add(checkbox) 868 | fourthline.add(JLabel("Use HTTPS")) 869 | fifthline = JPanel() 870 | fifthline.add(replaceHostCheckbox) 871 | fifthline.add(JLabel("Replace Host in HTTP header")) 872 | sixthline = JPanel() 873 | sixthline.add(errorField) 874 | 875 | gbc.gridy = 0 876 | domainPanel.add(firstline,gbc) 877 | gbc.gridy = 1 878 | domainPanel.add(secondline, gbc) 879 | gbc.gridy = 2 880 | domainPanel.add(thirdline, gbc) 881 | gbc.gridy = 3 882 | domainPanel.add(fourthline, gbc) 883 | gbc.gridy = 4 884 | domainPanel.add(fifthline, gbc) 885 | gbc.gridy = 5 886 | domainPanel.add(sixthline, gbc) 887 | 888 | 889 | result = JOptionPane.showConfirmDialog( 890 | self._splitpane,domainPanel, "Configure target details", JOptionPane.OK_CANCEL_OPTION) 891 | cancelled = (result == JOptionPane.CANCEL_OPTION) 892 | if cancelled or not isValidDomain(hostField.getText()): 893 | return (False, None, None, False, False) 894 | return (True, hostField.getText(), portField.getText(), checkbox.isSelected(), replaceHostCheckbox.isSelected()) 895 | 896 | ## 897 | ## Methods for running messages and analyzing results 898 | ## 899 | 900 | def lockButtons(self, running=True): 901 | # Disable run button, enable cancel button 902 | self._runButton.setEnabled(not running) 903 | self._newUserButton.setEnabled(not running) 904 | self._newRoleButton.setEnabled(not running) 905 | self._newHeaderButton.setEnabled(not running) 906 | self._newChainButton.setEnabled(not running) 907 | self._newStaticValueButton.setEnabled(not running) 908 | self._saveButton.setEnabled(not running) 909 | self._loadButton.setEnabled(not running) 910 | self._clearButton.setEnabled(not running) 911 | self._cancelButton.setEnabled(running) 912 | 913 | 914 | def runMessagesThread(self, messageIndexes=None): 915 | self._db.lock.acquire() 916 | try: 917 | self.lockButtons() 918 | self._runCancelled=False 919 | # Update original requests with any user changes 920 | self._messageTable.updateMessages() 921 | self._db.clearAllChainResults() 922 | 923 | indexes = messageIndexes 924 | if not indexes: 925 | indexes = self._db.getActiveMessageIndexes() 926 | self.clearColorResults(indexes) 927 | # Run in order of row, not by index 928 | messagesThatHaveRun = [] 929 | for message in self._db.getMessagesInOrderByRow(): 930 | # Only run if message is in the selected indexes (NOTE: dependencies will be run even if not selected) 931 | if message._index in indexes: 932 | messagesThatHaveRun = self.runMessageAndDependencies(message._index, messagesThatHaveRun, []) 933 | 934 | except: 935 | traceback.print_exc(file=self._callbacks.getStderr()) 936 | finally: 937 | self.lockButtons(False) 938 | self._db.lock.release() 939 | self._messageTable.redrawTable() 940 | 941 | def runMessageAndDependencies(self, messageIndex, messagesThatHaveRun, recursionCheckArray): 942 | messageEntry = self._db.arrayOfMessages[messageIndex] 943 | updatedMessagesThatHaveRun = messagesThatHaveRun[:] 944 | updatedRecursionCheckArray = recursionCheckArray[:] 945 | 946 | if messageIndex in updatedRecursionCheckArray: 947 | print "Error: Recursion detected in message chains: "+"->".join([str(i) for i in updatedRecursionCheckArray])+"->"+str(messageIndex) 948 | 949 | elif (messageIndex not in updatedMessagesThatHaveRun 950 | and messageEntry.isEnabled() 951 | and messageIndex in self._db.getActiveMessageIndexes()): 952 | updatedRecursionCheckArray.append(messageIndex) 953 | for chainIndex in self._db.getActiveChainIndexes(): 954 | # Run any dependencies first 955 | chainEntry = self._db.arrayOfChains[chainIndex] 956 | if (messageIndex in chainEntry.getToIDRange() 957 | and chainEntry.isEnabled() 958 | and str(chainEntry._fromID).isdigit() 959 | and int(chainEntry._fromID) >= 0): 960 | updatedMessagesThatHaveRun = self.runMessageAndDependencies(int(chainEntry._fromID), updatedMessagesThatHaveRun, updatedRecursionCheckArray) 961 | self.runMessage(messageIndex) 962 | # print messageIndex 963 | updatedMessagesThatHaveRun.append(messageIndex) 964 | 965 | return updatedMessagesThatHaveRun 966 | 967 | 968 | def runMessage(self, messageIndex): 969 | 970 | # NOTE: this uses hacky threading tricks for handling timeouts 971 | tempRequestResponse = [] 972 | index = 0 973 | def loadRequestResponse(index, service, message): 974 | # NOTE: tempRequestResponse is an array because of a threading issue, 975 | # where if this thread times out, it will still update temprequestresponse later on.. 976 | try: 977 | tempRequestResponse[index] = self._callbacks.makeHttpRequest(service,message) 978 | except java.lang.RuntimeException: 979 | # Catches if there is a bad host 980 | # TODO there may sometimes be an unhandled exception thrown in the stack trace here? 981 | print "Runtime Exception" 982 | return 983 | except: 984 | traceback.print_exc(file=callbacks.getStderr()) 985 | 986 | messageEntry = self._db.arrayOfMessages[messageIndex] 987 | messageEntry.clearResults() 988 | 989 | messageInfo = messageEntry._requestResponse 990 | requestInfo = self._helpers.analyzeRequest(messageInfo) 991 | reqBody = messageInfo.getRequest()[requestInfo.getBodyOffset():] 992 | 993 | 994 | for userIndex in [userEntry._index for userEntry in self._db.getUsersInOrderByRow()]: 995 | # Handle cancel button early exit here 996 | if self._runCancelled: 997 | return 998 | 999 | userEntry = self._db.arrayOfUsers[userIndex] 1000 | # Only run if the user is enabled 1001 | if userEntry.isEnabled(): 1002 | newHeaders = ModifyMessage.getNewHeaders(requestInfo, userEntry._cookies, userEntry._headers) 1003 | newBody = reqBody 1004 | 1005 | # Replace with Chain 1006 | for toValue, chainIndex in userEntry.getChainResultByMessageIndex(messageIndex): 1007 | # Add transformers 1008 | chain = self._db.arrayOfChains[chainIndex] 1009 | toValue = chain.transform(toValue, self._callbacks) 1010 | toRegex = chain._toRegex 1011 | newBody = StringUtil.toBytes(ModifyMessage.chainReplace(toRegex,toValue,[StringUtil.fromBytes(newBody)])[0]) 1012 | newHeaders = ModifyMessage.chainReplace(toRegex,toValue,newHeaders) 1013 | 1014 | # Replace with SV 1015 | # toValue = SV, toRegex = toRegex 1016 | for chain in [self._db.arrayOfChains[i] for i in self._db.getActiveChainIndexes()]: 1017 | svName = chain.getSVName() 1018 | # If the Chain Source exists, and this message is affected, and the chain is enabled 1019 | if svName and messageIndex in chain.getToIDRange() and chain.isEnabled(): 1020 | # get toValue for correct source 1021 | sourceUser = chain._sourceUser if chain._sourceUser>=0 else userIndex 1022 | # Check that sourceUser is active 1023 | if sourceUser in self._db.getActiveUserIndexes(): 1024 | toValue = self._db.getSVByName(svName).getValueForUserIndex(sourceUser) 1025 | # Add transformers 1026 | toValue = chain.transform(toValue, self._callbacks) 1027 | toRegex = chain._toRegex 1028 | newBody = StringUtil.toBytes(ModifyMessage.chainReplace(toRegex,toValue,[StringUtil.fromBytes(newBody)])[0]) 1029 | newHeaders = ModifyMessage.chainReplace(toRegex,toValue,newHeaders) 1030 | 1031 | # Replace Custom Special Types (i.e. Random) 1032 | newBody = StringUtil.toBytes(ModifyMessage.customReplace([StringUtil.fromBytes(newBody)])[0]) 1033 | newHeaders = ModifyMessage.customReplace(newHeaders) 1034 | 1035 | # Construct and send a message with the new headers 1036 | message = self._helpers.buildHttpMessage(newHeaders, newBody) 1037 | 1038 | 1039 | # Run with threading to timeout correctly 1040 | tempRequestResponse.append(None) 1041 | t = Thread(target=loadRequestResponse, args = [index,messageInfo.getHttpService(),message]) 1042 | t.start() 1043 | t.join(self._db.LOAD_TIMEOUT) 1044 | 1045 | # Create default requestResponse without response 1046 | requestResponse = RequestResponseStored(self, 1047 | request=message, 1048 | httpService=messageInfo.getHttpService()) 1049 | 1050 | if t.isAlive(): 1051 | print "ERROR: Request Timeout for Request #"+str(messageIndex)+" and User #"+str(userIndex) 1052 | elif tempRequestResponse[index]: 1053 | requestResponse = RequestResponseStored(self,requestResponse=tempRequestResponse[index]) 1054 | 1055 | messageEntry.addRunByUserIndex(userIndex, requestResponse) 1056 | 1057 | # Get Chain Result 1058 | response = requestResponse.getResponse() 1059 | if not response: 1060 | print "ERROR: No HTTP Response for Request #"+str(messageIndex)+" and User #"+str(userIndex) 1061 | else: 1062 | response = StringUtil.fromBytes(response) 1063 | for chain in [self._db.arrayOfChains[c] for c in self._db.getActiveChainIndexes()]: 1064 | # This wont have issues with SV because of the prefix never matching the index 1065 | if str(chain._fromID) == str(messageIndex) and chain.isEnabled(): 1066 | # If a sourceUser is set, replace for all users' chain results 1067 | # Else, replace each user's chain results individually 1068 | replace = True 1069 | affectedUsers = [userEntry] 1070 | if str(chain._sourceUser).isdigit() and chain._sourceUser >= 0: # TODO (0.9): why .isdigit()? Can this line just be removed 1071 | if str(chain._sourceUser) == str(userIndex): 1072 | affectedUsers = self._db.getUsersInOrderByRow() 1073 | else: 1074 | replace = False 1075 | 1076 | if replace: 1077 | result = "" 1078 | if chain._fromRegex: 1079 | match = re.search(chain._fromRegex, response, re.DOTALL) 1080 | if match and len(match.groups()): 1081 | result = match.group(1) 1082 | 1083 | for toID in chain.getToIDRange(): 1084 | for affectedUser in affectedUsers: 1085 | affectedUser.addChainResultByMessageIndex(toID, result, chain._index) 1086 | index +=1 1087 | 1088 | # Grab all active roleIndexes that are checkboxed 1089 | activeCheckBoxedRoles = [index for index in messageEntry._roles.keys() if messageEntry._roles[index] and not self._db.arrayOfRoles[index].isDeleted()] 1090 | # Check Role Results of message 1091 | for roleIndex in self._db.getActiveRoleIndexes(): 1092 | expectedResult = self.checkResult(messageEntry, roleIndex, activeCheckBoxedRoles) 1093 | messageEntry.setRoleResultByRoleIndex(roleIndex, expectedResult) 1094 | 1095 | 1096 | def clearColorResults(self, messageIndexArray = None): 1097 | if not messageIndexArray: 1098 | messageIndexes = self._db.getActiveMessageIndexes() 1099 | else: 1100 | messageIndexes = messageIndexArray 1101 | for messageIndex in messageIndexes: 1102 | messageEntry = self._db.arrayOfMessages[messageIndex] 1103 | messageEntry.clearResults() 1104 | self._messageTable.redrawTable() 1105 | 1106 | def checkResult(self, messageEntry, roleIndex, activeCheckBoxedRoles): 1107 | for userEntry in self._db.getUsersInOrderByRow(): 1108 | 1109 | ignoreUser = False 1110 | 1111 | # NOTE: When using failure regex, all users not in a checked role must see that regex 1112 | 1113 | # if user is not in this role, ignore it 1114 | if not userEntry._roles[roleIndex]: 1115 | ignoreUser = True 1116 | elif not userEntry.isEnabled(): 1117 | ignoreUser = True 1118 | else: 1119 | # If user is in any other checked role, then ignore it 1120 | for index in self._db.getActiveRoleIndexes(): 1121 | if not index == roleIndex and userEntry._roles[index]: 1122 | if index in activeCheckBoxedRoles: 1123 | ignoreUser = True 1124 | 1125 | if not ignoreUser: 1126 | if not userEntry._index in messageEntry._userRuns: 1127 | print ("Unexpected Error: Results not found for Request #" 1128 | + str(messageEntry._index) + " and User #" + str(userEntry._index)) 1129 | return False 1130 | requestResponse = messageEntry._userRuns[userEntry._index] 1131 | response = requestResponse.getResponse() 1132 | if not response: 1133 | # No Response: default to failed 1134 | return False 1135 | 1136 | resp = StringUtil.fromBytes(response) 1137 | found = re.search(messageEntry._regex, resp, re.DOTALL) 1138 | 1139 | roleChecked = roleIndex in activeCheckBoxedRoles 1140 | shouldSucceed = not roleChecked if messageEntry.isFailureRegex() else roleChecked 1141 | succeeds = found if shouldSucceed else not found 1142 | 1143 | 1144 | if not succeeds: 1145 | return False 1146 | 1147 | return True 1148 | 1149 | 1150 | ## 1151 | ## Static methods to modify requests during runs 1152 | ## 1153 | class ModifyMessage(): 1154 | 1155 | @staticmethod 1156 | def cookieReplace(oldCookieStr, newCookieStr): 1157 | previousCookies = oldCookieStr.replace(" ","").split(";") 1158 | 1159 | newCookies = newCookieStr.replace(" ","").split(";") 1160 | newCookieVariableNames = [] 1161 | for newCookie in newCookies: 1162 | # If its a valid cookie 1163 | equalsToken = newCookie.find("=") 1164 | if equalsToken >= 0: 1165 | newCookieVariableNames.append(newCookie[0:equalsToken+1]) 1166 | 1167 | # Add all the old unchanged cookies 1168 | for previousCookie in previousCookies: 1169 | # If its a valid cookie 1170 | equalsToken = previousCookie.find("=") 1171 | if equalsToken >= 0: 1172 | if previousCookie[0:equalsToken+1] not in newCookieVariableNames: 1173 | newCookies.append(previousCookie) 1174 | 1175 | # Remove whitespace 1176 | newCookies = [x for x in newCookies if x] 1177 | return "; ".join(newCookies) 1178 | 1179 | 1180 | # Replaces headers/cookies with user's token 1181 | @staticmethod 1182 | def getNewHeaders(requestInfo, newCookieStr, newHeaders): 1183 | ret = requestInfo.getHeaders() 1184 | headers = requestInfo.getHeaders() 1185 | 1186 | # Handle Cookies 1187 | if newCookieStr: 1188 | replaceIndex = -1 1189 | cookieHeader = "Cookie:" 1190 | oldCookieStr = "" 1191 | # Find existing cookie header 1192 | for i in range(headers.size()): 1193 | header = headers[i] 1194 | if str(header).startswith(cookieHeader): 1195 | replaceIndex = i 1196 | oldCookieStr = str(header)[len(cookieHeader):] 1197 | 1198 | newCookiesHeader = cookieHeader+" "+ModifyMessage.cookieReplace(oldCookieStr,newCookieStr) 1199 | 1200 | if replaceIndex >= 0: 1201 | ret.set(replaceIndex, newCookiesHeader) 1202 | else: 1203 | ret.add(newCookiesHeader) 1204 | 1205 | # Handle Custom Header 1206 | for newHeader in [x for x in newHeaders if x]: 1207 | replaceIndex = -1 1208 | colon = newHeader.find(":") 1209 | if colon >= 0: 1210 | for i in range(headers.size()): 1211 | header = headers[i] 1212 | # If the header already exists, remove it 1213 | if str(header).startswith(newHeader[0:colon+1]): 1214 | replaceIndex = i 1215 | if replaceIndex >= 0: 1216 | ret.set(replaceIndex, newHeader) 1217 | else: 1218 | ret.add(newHeader) 1219 | 1220 | return ret 1221 | 1222 | @staticmethod 1223 | def chainReplace(toRegex, toValue, toArray): 1224 | # TODO clean up so that the input is headers+body and its called only once 1225 | isBody = len(toArray)==1 1226 | if toRegex: 1227 | # BUG FIX: Geoff reported that if the regex ends at the newline on the last header, 1228 | # the regex fails. Hacky solution is to add an extra newlines before the regex search 1229 | # and remove it after. 1230 | to = "\r\n".join(toArray)+"\r\n\r\n" 1231 | match = re.search(toRegex, to, re.DOTALL) 1232 | if match and len(match.groups()): 1233 | ret = (to[0:match.start(1)]+toValue+to[match.end(1):]) 1234 | if ret[-4:] == "\r\n\r\n": 1235 | ret = ret[:-4] 1236 | if isBody: 1237 | return [ret] 1238 | else: 1239 | return ret.split("\r\n") 1240 | return toArray 1241 | 1242 | ## Method to replace custom special types in messages 1243 | @staticmethod 1244 | def customReplace(toArray): 1245 | ret = ArrayList() 1246 | customPrefix = "#{AUTHMATRIX:" 1247 | for to in toArray: 1248 | toNew = to 1249 | if customPrefix in to: 1250 | if customPrefix+"RANDOM}" in to: 1251 | # This will produce a random 4 char numeric string 1252 | # Most common use case is for APIs that reject requests that are identical to a previous request 1253 | randomString = ''.join(random.choice(string.digits) for _ in range(4)) 1254 | toNew = to.replace(customPrefix+"RANDOM}",randomString) 1255 | ret.add(toNew) 1256 | 1257 | return ret 1258 | 1259 | 1260 | 1261 | 1262 | ## 1263 | ## DB Class that holds all configuration data 1264 | ## 1265 | 1266 | class MatrixDB(): 1267 | 1268 | def __init__(self): 1269 | # Holds all custom data 1270 | # NOTE: consider moving these constants to a different class 1271 | self.STATIC_USER_TABLE_COLUMN_COUNT = 2 1272 | self.STATIC_MESSAGE_TABLE_COLUMN_COUNT = 3 1273 | self.STATIC_CHAIN_TABLE_COLUMN_COUNT = 7 1274 | self.LOAD_TIMEOUT = 10.0 1275 | self.BURP_ORANGE = Color(0xff6633) 1276 | 1277 | self.lock = Lock() 1278 | self.arrayOfMessages = ArrayList() 1279 | self.arrayOfRoles = ArrayList() 1280 | self.arrayOfUsers = ArrayList() 1281 | self.arrayOfChains = ArrayList() 1282 | self.deletedUserCount = 0 1283 | self.deletedRoleCount = 0 1284 | self.deletedMessageCount = 0 1285 | self.deletedChainCount = 0 1286 | self.arrayOfSVs = ArrayList() 1287 | self.headerCount = 0 1288 | self.arrayOfRegexes = [] 1289 | 1290 | 1291 | # Returns the index of the user, whether its new or not 1292 | def getOrCreateUser(self, name): 1293 | self.lock.acquire() 1294 | userIndex = -1 1295 | # Check if User already exits 1296 | for i in self.getActiveUserIndexes(): 1297 | if self.arrayOfUsers[i]._name == name: 1298 | userIndex = i 1299 | # Add new User 1300 | if userIndex < 0: 1301 | userIndex = self.arrayOfUsers.size() 1302 | self.arrayOfUsers.add(UserEntry(userIndex, 1303 | userIndex - self.deletedUserCount, 1304 | name, 1305 | headers=[""]*self.headerCount)) 1306 | 1307 | # Add SingleUser Role 1308 | self.lock.release() 1309 | singleRoleIndex = self.getOrCreateRole(name, True) 1310 | self.lock.acquire() 1311 | # Check Role for user 1312 | 1313 | # Add all existing roles as unchecked except the singleUser 1314 | for roleIndex in self.getActiveRoleIndexes(): 1315 | prechecked=False 1316 | if roleIndex == singleRoleIndex: 1317 | prechecked=True 1318 | self.arrayOfUsers[userIndex].addRoleByIndex(roleIndex,prechecked) 1319 | 1320 | self.lock.release() 1321 | return userIndex 1322 | 1323 | # Returns the index of the role, whether its new or not 1324 | def getOrCreateRole(self, role, newSingleUser=False): 1325 | self.lock.acquire() 1326 | roleIndex = -1 1327 | 1328 | suffix = " (only)" 1329 | name = role+suffix if newSingleUser else role 1330 | if newSingleUser or name.endswith(suffix): 1331 | singleUser = True 1332 | else: 1333 | singleUser = False 1334 | 1335 | # Check if Role already exists 1336 | for i in self.getActiveRoleIndexes(): 1337 | if self.arrayOfRoles[i]._name == name: 1338 | roleIndex = i 1339 | # Add new Role 1340 | if roleIndex < 0: 1341 | roleIndex = self.arrayOfRoles.size() 1342 | newColumn = roleIndex-self.deletedRoleCount 1343 | 1344 | # Insert column if not singleuser and increment singleuser columns 1345 | if not singleUser: 1346 | newColumn -= self.getActiveSingleUserRoleCount() 1347 | for i in self.getActiveSingleUserRoleIndexes(): 1348 | # NOTE this must be changed if reordering of roles is added 1349 | curColumn = self.arrayOfRoles[i].getColumn() 1350 | assert(curColumn >= newColumn) 1351 | self.arrayOfRoles[i].setColumn(curColumn+1) 1352 | 1353 | self.arrayOfRoles.add(RoleEntry(roleIndex, 1354 | newColumn, 1355 | name, 1356 | singleUser=singleUser)) 1357 | 1358 | # Add new role to each existing user as unchecked except the singleUser 1359 | for userIndex in self.getActiveUserIndexes(): 1360 | prechecked = False 1361 | if singleUser and self.arrayOfUsers[userIndex]._name == name[:-len(suffix)]: 1362 | prechecked=True 1363 | self.arrayOfUsers[userIndex].addRoleByIndex(roleIndex, prechecked) 1364 | 1365 | # Add new role to each existing message as unchecked 1366 | for messageIndex in self.getActiveMessageIndexes(): 1367 | self.arrayOfMessages[messageIndex].addRoleByIndex(roleIndex) 1368 | 1369 | self.lock.release() 1370 | return roleIndex 1371 | 1372 | # Returns the Row of the new message 1373 | # Unlike Users and Roles, allow duplicate messages 1374 | def createNewMessage(self,messagebuffer,name,regex): 1375 | self.lock.acquire() 1376 | messageIndex = self.arrayOfMessages.size() 1377 | self.arrayOfMessages.add(MessageEntry(messageIndex, messageIndex - self.deletedMessageCount, messagebuffer, name, regex=regex)) 1378 | 1379 | # Add all existing roles as unchecked 1380 | for roleIndex in self.getActiveRoleIndexes(): 1381 | self.arrayOfMessages[messageIndex].addRoleByIndex(roleIndex) 1382 | 1383 | # Add regex to array if its new 1384 | if regex and regex not in self.arrayOfRegexes: 1385 | self.arrayOfRegexes.append(regex) 1386 | 1387 | 1388 | self.lock.release() 1389 | return messageIndex 1390 | 1391 | def createNewChain(self): 1392 | self.lock.acquire() 1393 | chainIndex = self.arrayOfChains.size() 1394 | # Handle Example 1395 | if chainIndex == 0: 1396 | self.arrayOfChains.add(ChainEntry( 1397 | chainIndex, 1398 | chainIndex - self.deletedChainCount, 1399 | "Example", 1400 | "", 1401 | "StartAfter(.*?)EndAt", 1402 | "", 1403 | "StartAfter(.*?)EndAt")) 1404 | else: 1405 | self.arrayOfChains.add(ChainEntry(chainIndex, chainIndex - self.deletedChainCount)) 1406 | 1407 | self.lock.release() 1408 | return chainIndex 1409 | 1410 | def clear(self): 1411 | self.lock.acquire() 1412 | self.arrayOfMessages = ArrayList() 1413 | self.arrayOfRoles = ArrayList() 1414 | self.arrayOfUsers = ArrayList() 1415 | self.arrayOfChains = ArrayList() 1416 | self.deletedUserCount = 0 1417 | self.deletedRoleCount = 0 1418 | self.deletedMessageCount = 0 1419 | self.deletedChainCount = 0 1420 | self.arrayOfSVs = ArrayList() 1421 | self.headerCount = 0 1422 | self.arrayOfRegexes = [] 1423 | self.lock.release() 1424 | 1425 | def loadLegacy(self, fileName, extender): 1426 | from java.io import ObjectOutputStream; 1427 | from java.io import FileOutputStream; 1428 | from java.io import ObjectInputStream; 1429 | from java.io import FileInputStream; 1430 | 1431 | FAILURE_REGEX_SERIALIZE_CODE = "|AUTHMATRIXFAILUREREGEXPREFIX|" 1432 | AUTHMATRIX_SERIALIZE_CODE = "|AUTHMATRIXCOOKIEHEADERSERIALIZECODE|" 1433 | 1434 | ins = ObjectInputStream(FileInputStream(fileName)) 1435 | db=ins.readObject() 1436 | ins.close() 1437 | 1438 | self.lock.acquire() 1439 | self.arrayOfUsers = ArrayList() 1440 | self.arrayOfRoles = ArrayList() 1441 | self.arrayOfMessages = ArrayList() 1442 | self.arrayOfChains = ArrayList() 1443 | self.deletedUserCount = db.deletedUserCount 1444 | self.deletedRoleCount = db.deletedRoleCount 1445 | self.deletedMessageCount = db.deletedMessageCount 1446 | self.deletedChainCount = 0 # Updated with chain entries below in arrayOfUsers 1447 | self.arrayOfSVs = ArrayList() 1448 | self.headerCount = 1 # Legacy states had one header only 1449 | self.arrayOfRegexes = [] 1450 | 1451 | for message in db.arrayOfMessages: 1452 | if message._successRegex.startswith(FAILURE_REGEX_SERIALIZE_CODE): 1453 | regex = message._successRegex[len(FAILURE_REGEX_SERIALIZE_CODE):] 1454 | failureRegexMode=True 1455 | else: 1456 | regex = message._successRegex 1457 | failureRegexMode=False 1458 | messageEntry = RequestResponseStored(extender, message._host, message._port, message._protocol, message._requestData) 1459 | self.arrayOfMessages.add(MessageEntry( 1460 | message._index, 1461 | message._tableRow, 1462 | messageEntry, 1463 | message._name, message._roles, regex, message._deleted, failureRegexMode)) 1464 | 1465 | for role in db.arrayOfRoles: 1466 | self.arrayOfRoles.add(RoleEntry( 1467 | role._index, 1468 | role._mTableColumn-3, # NOTE this is done to preserve compatability with older state files 1469 | role._name, 1470 | role._deleted)) 1471 | 1472 | for user in db.arrayOfUsers: 1473 | # NOTE to preserve backwords compatability, chains are stored here in a really hacky way 1474 | if type(user._roles) == int: 1475 | # Chain 1476 | self.deletedChainCount = user._roles 1477 | 1478 | name="" 1479 | sourceUser="" 1480 | if user._name: 1481 | namesplit = user._name.split(AUTHMATRIX_SERIALIZE_CODE) 1482 | name=namesplit[0] 1483 | if len(namesplit)>1: 1484 | sourceUser=namesplit[1] 1485 | 1486 | token = user._token.split(AUTHMATRIX_SERIALIZE_CODE) 1487 | assert(len(token)==2) 1488 | fromID = token[0] 1489 | fromRegex = token[1] 1490 | staticcsrf = user._staticcsrf.split(AUTHMATRIX_SERIALIZE_CODE) 1491 | assert(len(staticcsrf)==2) 1492 | toID = staticcsrf[0] 1493 | toRegex = staticcsrf[1] 1494 | self.arrayOfChains.add(ChainEntry( 1495 | int(user._index), 1496 | int(user._tableRow), 1497 | name, 1498 | fromID, 1499 | fromRegex, 1500 | toID, 1501 | toRegex, 1502 | user._deleted, 1503 | sourceUser 1504 | )) 1505 | else: 1506 | # Normal User 1507 | token = [""] if not user._token else user._token.split(AUTHMATRIX_SERIALIZE_CODE) 1508 | cookies = token[0] 1509 | header = "" if len(token)==1 else token[1] 1510 | name = "" if not user._name else user._name 1511 | self.arrayOfUsers.add(UserEntry( 1512 | int(user._index), 1513 | int(user._tableRow), 1514 | name, 1515 | user._roles, 1516 | user._deleted, 1517 | cookies, 1518 | headers=[header])) 1519 | 1520 | self.lock.release() 1521 | 1522 | def loadJson(self, jsonText, extender): 1523 | # NOTE: Weird issue where saving serialized json for configs loaded from old states (pre v0.6.3) 1524 | # doesn't use correct capitalization on bools. 1525 | # This replacement might have weird results, but most are mitigated by using base64 encoding 1526 | jsonFixed = jsonText.replace(": False",": false").replace(": True",": true") 1527 | 1528 | # Get Rid of comments 1529 | jsonFixed = re.sub(r"[/][*]([^*]|([*][^/]))*[*][/]", "", jsonFixed, 0, re.MULTILINE) 1530 | 1531 | try: 1532 | stateDict = json.loads(jsonFixed) 1533 | except: 1534 | print jsonFixed 1535 | traceback.print_exc(file=extender._callbacks.getStderr()) 1536 | return 1537 | 1538 | version = stateDict["version"] 1539 | if version > AUTHMATRIX_VERSION: 1540 | print "Invalid Version in State File ("+version+")" 1541 | return 1542 | 1543 | backupState = self.getSaveableJson() 1544 | 1545 | self.lock.acquire() 1546 | 1547 | try: 1548 | 1549 | # NOTE: As of 0.8, If the state file is missing an element, then it assumes it is 1550 | # the intention of the user to not modify that array, so that just small bits can be updated. 1551 | 1552 | # NOTE: As of 0.8 the deleted counts and header counts are filled in using the values found in each array 1553 | 1554 | # TODO (0.9): every field that has "{int(x" is using an ID in the state that is not obvious to the user 1555 | 1556 | if "arrayOfRoles" in stateDict: 1557 | self.arrayOfRoles = ArrayList() 1558 | self.deletedRoleCount = 0 1559 | 1560 | # (self,index,columnIndex,name,deleted=False,singleUser=False): 1561 | for roleEntry in stateDict["arrayOfRoles"]: 1562 | deleted = False if "deleted" not in roleEntry else roleEntry["deleted"] 1563 | if deleted: 1564 | self.deletedRoleCount += 1 1565 | 1566 | self.arrayOfRoles.add(RoleEntry( 1567 | roleEntry["index"], 1568 | roleEntry["column"], 1569 | roleEntry["name"], 1570 | deleted = deleted, 1571 | singleUser = False if version < "0.7" or "singleUser" not in roleEntry else roleEntry["singleUser"] 1572 | )) 1573 | 1574 | if "arrayOfUsers" in stateDict: 1575 | self.arrayOfUsers = ArrayList() 1576 | self.deletedUserCount = 0 1577 | self.headerCount = 0 1578 | self.arrayOfSVs = ArrayList() 1579 | 1580 | # NOTE: leaving out chainResults 1581 | # (self, index, tableRow, name, roles = {}, deleted=False, cookies="", headers = [], enabled = True): 1582 | for userEntry in stateDict["arrayOfUsers"]: 1583 | deleted = False if "deleted" not in userEntry else userEntry["deleted"] 1584 | if deleted: 1585 | self.deletedUserCount += 1 1586 | 1587 | 1588 | 1589 | # Suppport old and new header versions 1590 | if "headersBase64" in userEntry: 1591 | headers = [base64.b64decode(x) for x in userEntry["headersBase64"]] 1592 | # Grab the number of headers. Sanity check will later confirm that each user has the right number of headers 1593 | if self.headerCount == 0: 1594 | self.headerCount = len(headers) 1595 | elif "headerBase64" in userEntry: 1596 | self.headerCount = 1 1597 | headers = [base64.b64decode(userEntry["headerBase64"])] 1598 | else: 1599 | headers = [""]*self.headerCount 1600 | 1601 | 1602 | self.arrayOfUsers.add(UserEntry( 1603 | userEntry["index"], 1604 | userEntry["tableRow"], 1605 | userEntry["name"], 1606 | {int(x): userEntry["roles"][x] for x in userEntry["roles"].keys()}, # convert keys to ints 1607 | deleted = deleted, 1608 | cookies = "" if "cookiesBase64" not in userEntry else base64.b64decode(userEntry["cookiesBase64"]), 1609 | headers = headers, 1610 | enabled = True if "enabled" not in userEntry else userEntry["enabled"] 1611 | )) 1612 | 1613 | # Update Static Values 1614 | keyword = "arrayOfChainSources" if version >= "0.8" else "arrayOfSVs" 1615 | if keyword in stateDict: 1616 | for svEntry in stateDict[keyword]: 1617 | # If the index does not match an active users, do not include it 1618 | self.arrayOfSVs.add(SVEntry( 1619 | svEntry["name"], 1620 | {int(x): svEntry["userValues"][x] for x in svEntry["userValues"].keys() if int(x) in self.getActiveUserIndexes()}, # convert keys to ints 1621 | )) 1622 | 1623 | 1624 | if "arrayOfMessages" in stateDict: 1625 | self.arrayOfMessages = ArrayList() 1626 | self.deletedMessageCount = 0 1627 | self.arrayOfRegexes = [] 1628 | 1629 | # NOTE leaving out roleResults and userRuns (need to convert keys) 1630 | # (self, index, tableRow, requestResponse, name = "", roles = {}, regex = "", deleted = False, failureRegexMode = False, enabled = True): 1631 | for messageEntry in stateDict["arrayOfMessages"]: 1632 | deleted = False if "deleted" not in messageEntry else messageEntry["deleted"] 1633 | if deleted: 1634 | self.deletedMessageCount += 1 1635 | 1636 | regex = "" if "regexBase64" not in messageEntry else base64.b64decode(messageEntry["regexBase64"]).decode("utf-8") 1637 | 1638 | if regex and regex not in self.arrayOfRegexes: 1639 | self.arrayOfRegexes.append(regex) 1640 | 1641 | requestResponse = None if deleted else RequestResponseStored( 1642 | extender, 1643 | messageEntry["host"], 1644 | messageEntry["port"], 1645 | messageEntry["protocol"], 1646 | StringUtil.toBytes((base64.b64decode(messageEntry["requestBase64"])).decode("utf-8"))) 1647 | 1648 | self.arrayOfMessages.add(MessageEntry( 1649 | messageEntry["index"], 1650 | messageEntry["tableRow"], 1651 | requestResponse, 1652 | messageEntry["name"], 1653 | {int(x): messageEntry["roles"][x] for x in messageEntry["roles"].keys()}, # convert keys to ints 1654 | regex = regex, 1655 | deleted = deleted, 1656 | failureRegexMode = False if "failureRegexMode" not in messageEntry else messageEntry["failureRegexMode"], 1657 | enabled = True if "enabled" not in messageEntry else messageEntry["enabled"] 1658 | )) 1659 | 1660 | 1661 | if "arrayOfChains" in stateDict: 1662 | self.arrayOfChains = ArrayList() 1663 | self.deletedChainCount = 0 1664 | 1665 | # NOTE: leaving out fromStart, fromEnd, toStart, toEnd 1666 | for chainEntry in stateDict["arrayOfChains"]: 1667 | deleted = False if "deleted" not in chainEntry else chainEntry["deleted"] 1668 | if deleted: 1669 | self.deletedChainCount += 1 1670 | 1671 | self.arrayOfChains.add(ChainEntry( 1672 | chainEntry["index"], 1673 | chainEntry["tableRow"], 1674 | name = "" if "name" not in chainEntry else chainEntry["name"], 1675 | fromID = "" if "fromID" not in chainEntry else chainEntry["fromID"], 1676 | fromRegex = "" if "fromRegexBase64" not in chainEntry else base64.b64decode(chainEntry["fromRegexBase64"]).decode("utf-8"), 1677 | toID = "" if "toID" not in chainEntry else chainEntry["toID"], 1678 | toRegex = "" if "toRegexBase64" not in chainEntry else base64.b64decode(chainEntry["toRegexBase64"]).decode("utf-8"), 1679 | deleted = deleted, 1680 | sourceUser = -1 if "sourceUser" not in chainEntry else chainEntry["sourceUser"], 1681 | enabled = True if "enabled" not in chainEntry else chainEntry["enabled"], 1682 | transformers = [] if "transformers" not in chainEntry else chainEntry["transformers"] 1683 | )) 1684 | 1685 | except: 1686 | self.lock.release() 1687 | print "Corrupt State File: Reverting back to original. (See stderr for more detail)" 1688 | traceback.print_exc(file=extender._callbacks.getStderr()) 1689 | self.loadJson(backupState,extender) 1690 | return 1691 | 1692 | self.lock.release() 1693 | 1694 | # Sanity checks 1695 | sanityResult = self.sanityCheck(extender) 1696 | if sanityResult: 1697 | print "Error parsing state file: "+sanityResult 1698 | # Revert to the backup state 1699 | self.loadJson(backupState,extender) 1700 | 1701 | 1702 | 1703 | def sanityCheck(self, extender): 1704 | try: 1705 | # Returns an error string if the DB is in a corrupt state, else returns None 1706 | userIndexes = self.getActiveUserIndexes() 1707 | roleIndexes = self.getActiveRoleIndexes() 1708 | messageIndexes = self.getActiveMessageIndexes() 1709 | chainIndexes = self.getActiveChainIndexes() 1710 | 1711 | # Index Checks 1712 | for indexes, currentArray, deletedCount in [ 1713 | (userIndexes, self.arrayOfUsers, self.deletedUserCount), 1714 | (roleIndexes, self.arrayOfRoles, self.deletedRoleCount), 1715 | (messageIndexes, self.arrayOfMessages, self.deletedMessageCount), 1716 | (chainIndexes, self.arrayOfChains, self.deletedChainCount)]: 1717 | # Check that indexes are all unique 1718 | if len(indexes) > len(set(indexes)): 1719 | return "Not All Indexes are Unique." 1720 | # Check that the DB array has the correct number of items 1721 | if len(currentArray) != len(indexes) + deletedCount: 1722 | return "Array found with incorrect number of items." 1723 | for currentIndex in indexes: 1724 | if currentIndex < 0: 1725 | return "Negative Index Found." 1726 | # Check that all index values are below the length of active+deleted 1727 | if currentIndex >= len(indexes)+deletedCount: 1728 | return "Index Higher than Total Active + Deleted." 1729 | # Check that the indexes within the array match the index of the Entry 1730 | if currentIndex != currentArray[currentIndex]._index: 1731 | return "Entries in the State File Arrays must be in order by index" 1732 | 1733 | # Row Checks 1734 | for indexes, currentArray in [ 1735 | (userIndexes, self.arrayOfUsers), 1736 | (messageIndexes, self.arrayOfMessages), 1737 | (chainIndexes, self.arrayOfChains)]: 1738 | rowList = [currentArray[currentIndex].getTableRow() for currentIndex in indexes] 1739 | # Check that the rows for a given table are all unique 1740 | if len(rowList) > len(set(rowList)): 1741 | return "Not all rows for a given table are unique." 1742 | for row in rowList: 1743 | # Check that rows are within appropriate bounds 1744 | if row >= len(indexes) or row <0: 1745 | return "Row out of bounds." 1746 | 1747 | # Column Checks 1748 | columnList = [self.arrayOfRoles[currentIndex].getColumn() for currentIndex in roleIndexes] 1749 | if len(columnList) > len(set(columnList)): 1750 | return "Not all columns for Roles array are unique." 1751 | for column in columnList: 1752 | if column < 0 or column >= len(roleIndexes): 1753 | return "Column out of bounds." 1754 | 1755 | # Custom Headers checks 1756 | for userIndex in userIndexes: 1757 | if len(self.arrayOfUsers[userIndex]._headers) != self.headerCount: 1758 | return "Incorrect Number of Headers for a User. Must be "+str(self.headerCount) 1759 | 1760 | # Role Assignment Checks 1761 | for indexes, currentArray in [ 1762 | (userIndexes, self.arrayOfUsers), 1763 | (messageIndexes, self.arrayOfMessages)]: 1764 | for index in indexes: 1765 | # Check that all keys are unique (might be redundant) 1766 | roleKeys = currentArray[index]._roles.keys() 1767 | if len(roleKeys) > len(set(roleKeys)): 1768 | return "Duplicate Keys on Roles Map" 1769 | # Check that all active roles are covered in that items map 1770 | for roleIndex in roleIndexes: 1771 | if roleIndex not in roleKeys: 1772 | return "Missing a Role Value in a Message or User" 1773 | 1774 | # NOTE: Skipping Static Value check because a missing SV is handled gracefully 1775 | 1776 | # TODO (0.9): check fromID and sourceUser in Chain 1777 | 1778 | except: 1779 | traceback.print_exc(file=extender._callbacks.getStderr()) 1780 | return "Unidentified" 1781 | return None 1782 | 1783 | 1784 | def getSaveableJson(self): 1785 | stateDict = {"version":AUTHMATRIX_VERSION} 1786 | 1787 | stateDict["arrayOfRoles"] = [] 1788 | for roleEntry in self.arrayOfRoles: 1789 | deleted = roleEntry._deleted 1790 | stateDict["arrayOfRoles"].append({ 1791 | "index":roleEntry._index, 1792 | "name":roleEntry._name if not deleted else None, 1793 | "deleted":deleted, 1794 | "column":roleEntry._column if not deleted else None, 1795 | "singleUser":roleEntry._singleUser if not deleted else None 1796 | }) 1797 | 1798 | stateDict["arrayOfUsers"] = [] 1799 | for userEntry in self.arrayOfUsers: 1800 | deleted = userEntry._deleted 1801 | stateDict["arrayOfUsers"].append({ 1802 | "index":userEntry._index, 1803 | "name":userEntry._name if not deleted else None, 1804 | "roles":userEntry._roles if not deleted else {}, 1805 | "deleted":deleted, 1806 | "enabled":userEntry._enabled, 1807 | "tableRow":userEntry._tableRow if not deleted else None, 1808 | "cookiesBase64":base64.b64encode(userEntry._cookies.encode("utf-8")) if userEntry._cookies and not deleted else "", 1809 | "headersBase64":[base64.b64encode(x.encode("utf-8")) if x else "" for x in userEntry._headers] if not deleted else [], 1810 | "chainResults":userEntry._chainResults if not deleted else {} 1811 | }) 1812 | 1813 | stateDict["arrayOfMessages"] = [] 1814 | for messageEntry in self.arrayOfMessages: 1815 | deleted = messageEntry._deleted 1816 | stateDict["arrayOfMessages"].append({ 1817 | "index":messageEntry._index, 1818 | "tableRow":messageEntry._tableRow if not deleted else None, 1819 | "requestBase64":base64.b64encode(StringUtil.fromBytes(messageEntry._requestResponse.getRequest()).encode("utf-8")) if not deleted else None, 1820 | "host":messageEntry._requestResponse.getHttpService().getHost() if not deleted else None, 1821 | "port":messageEntry._requestResponse.getHttpService().getPort() if not deleted else None, 1822 | "protocol":messageEntry._requestResponse.getHttpService().getProtocol() if not deleted else None, 1823 | "name":messageEntry._name if not deleted else None, 1824 | "roles":messageEntry._roles if not deleted else {}, 1825 | "regexBase64":base64.b64encode(messageEntry._regex.encode("utf-8")) if messageEntry._regex and not deleted else "", 1826 | "deleted":deleted, 1827 | "enabled":messageEntry._enabled, 1828 | "failureRegexMode":messageEntry._failureRegexMode if not deleted else None, 1829 | "runBase64ForUserID":{int(x): { 1830 | "request": None if not messageEntry._userRuns[x] or not messageEntry._userRuns[x].getRequest() else base64.b64encode(StringUtil.fromBytes(messageEntry._userRuns[x].getRequest()).encode("utf-8")), 1831 | "response": None if not messageEntry._userRuns[x] or not messageEntry._userRuns[x].getResponse() else base64.b64encode(StringUtil.fromBytes(messageEntry._userRuns[x].getResponse()).encode("utf-8"))} 1832 | for x in messageEntry._userRuns.keys()} if not deleted else {}, 1833 | "runResultForRoleID":messageEntry._roleResults if not deleted else {} 1834 | }) 1835 | 1836 | stateDict["arrayOfChains"] = [] 1837 | for chainEntry in self.arrayOfChains: 1838 | deleted = chainEntry._deleted 1839 | stateDict["arrayOfChains"].append({ 1840 | "index":chainEntry._index, 1841 | "fromID":chainEntry._fromID if not deleted else None, 1842 | "fromRegexBase64":base64.b64encode(chainEntry._fromRegex.encode("utf-8")) if chainEntry._fromRegex and not deleted else "", 1843 | "toID":chainEntry._toID if not deleted else None, 1844 | "toRegexBase64":base64.b64encode(chainEntry._toRegex.encode("utf-8")) if chainEntry._toRegex and not deleted else "", 1845 | "deleted":deleted, 1846 | "enabled":chainEntry._enabled, 1847 | "tableRow":chainEntry._tableRow if not deleted else None, 1848 | "name":chainEntry._name if not deleted else None, 1849 | "sourceUser":chainEntry._sourceUser if not deleted else None, 1850 | "fromStart":chainEntry._fromStart if not deleted else None, 1851 | "fromEnd":chainEntry._fromEnd if not deleted else None, 1852 | "toStart":chainEntry._toStart if not deleted else None, 1853 | "toEnd":chainEntry._toEnd if not deleted else None, 1854 | "transformers":chainEntry._transformers if not deleted else [] 1855 | }) 1856 | 1857 | stateDict["arrayOfChainSources"] = [] 1858 | for SVEntry in self.arrayOfSVs: 1859 | stateDict["arrayOfChainSources"].append({ 1860 | "name":SVEntry._name, 1861 | "userValues":SVEntry._userValues 1862 | }) 1863 | 1864 | # BUG: this is not using the correct capitalization on booleans after loading legacy states 1865 | return json.dumps(stateDict) 1866 | 1867 | def getActiveUserIndexes(self): 1868 | return [x._index for x in self.arrayOfUsers if not x.isDeleted()] 1869 | 1870 | def getActiveRoleIndexes(self): 1871 | return [x._index for x in self.arrayOfRoles if not x.isDeleted()] 1872 | 1873 | def getActiveSingleUserRoleIndexes(self): 1874 | return [x._index for x in self.arrayOfRoles if x.isSingleUser() and not x.isDeleted()] 1875 | 1876 | def getActiveMessageIndexes(self): 1877 | return [x._index for x in self.arrayOfMessages if not x.isDeleted()] 1878 | 1879 | def getActiveChainIndexes(self): 1880 | return [x._index for x in self.arrayOfChains if not x.isDeleted()] 1881 | 1882 | def getActiveUserCount(self): 1883 | ret = self.arrayOfUsers.size()-self.deletedUserCount 1884 | assert(ret == len(self.getActiveUserIndexes())) 1885 | return ret 1886 | 1887 | def getActiveRoleCount(self): 1888 | ret = self.arrayOfRoles.size()-self.deletedRoleCount 1889 | assert(ret == len(self.getActiveRoleIndexes())) 1890 | return ret 1891 | 1892 | def getActiveMessageCount(self): 1893 | ret = self.arrayOfMessages.size()-self.deletedMessageCount 1894 | assert(ret == len(self.getActiveMessageIndexes())) 1895 | return ret 1896 | 1897 | 1898 | def getActiveSingleUserRoleCount(self): 1899 | return len(self.getActiveSingleUserRoleIndexes()) 1900 | 1901 | 1902 | def getActiveChainCount(self): 1903 | return self.arrayOfChains.size()-self.deletedChainCount 1904 | 1905 | def getMessageByRow(self, row): 1906 | for messageEntry in [self.arrayOfMessages[i] for i in self.getActiveMessageIndexes()]: 1907 | if messageEntry.getTableRow() == row: 1908 | return messageEntry 1909 | 1910 | def getUserByRow(self, row): 1911 | for userEntry in [self.arrayOfUsers[i] for i in self.getActiveUserIndexes()]: 1912 | if userEntry.getTableRow() == row: 1913 | return userEntry 1914 | 1915 | def getRoleByColumn(self,column, table): 1916 | startingIndex = self.STATIC_MESSAGE_TABLE_COLUMN_COUNT if table == "m" else self.STATIC_USER_TABLE_COLUMN_COUNT+self.headerCount+self.arrayOfSVs.size() 1917 | for roleEntry in [self.arrayOfRoles[i] for i in self.getActiveRoleIndexes()]: 1918 | if roleEntry.getColumn()+startingIndex == column: 1919 | return roleEntry 1920 | 1921 | def getChainByRow(self, row): 1922 | for chainEntry in [self.arrayOfChains[i] for i in self.getActiveChainIndexes()]: 1923 | if chainEntry.getTableRow() == row: 1924 | return chainEntry 1925 | 1926 | def deleteUser(self,userIndex): 1927 | self.lock.acquire() 1928 | userEntry = self.arrayOfUsers[userIndex] 1929 | if userEntry: 1930 | userEntry.setDeleted() 1931 | self.deletedUserCount += 1 1932 | 1933 | previousRow = userEntry.getTableRow() 1934 | for user in [self.arrayOfUsers[i] for i in self.getActiveUserIndexes()]: 1935 | if user.getTableRow()>previousRow: 1936 | user.setTableRow(user.getTableRow()-1) 1937 | 1938 | # TODO maybe delete SingleUser role too (though it might be worth leaving if the user has boxes checked) 1939 | 1940 | self.lock.release() 1941 | 1942 | def deleteRole(self,roleIndex): 1943 | self.lock.acquire() 1944 | roleEntry = self.arrayOfRoles[roleIndex] 1945 | if roleEntry: 1946 | roleEntry.setDeleted() 1947 | self.deletedRoleCount += 1 1948 | 1949 | previousColumn = roleEntry.getColumn() 1950 | for role in [self.arrayOfRoles[i] for i in self.getActiveRoleIndexes()]: 1951 | if role.getColumn()>previousColumn: 1952 | role.setColumn(role.getColumn()-1) 1953 | 1954 | self.lock.release() 1955 | 1956 | def deleteMessage(self,messageIndex): 1957 | self.lock.acquire() 1958 | messageEntry = self.arrayOfMessages[messageIndex] 1959 | if messageEntry: 1960 | messageEntry.setDeleted() 1961 | self.deletedMessageCount += 1 1962 | 1963 | previousRow = messageEntry.getTableRow() 1964 | for message in [self.arrayOfMessages[i] for i in self.getActiveMessageIndexes()]: 1965 | if message.getTableRow()>previousRow: 1966 | message.setTableRow(message.getTableRow()-1) 1967 | 1968 | self.lock.release() 1969 | 1970 | def setToggleForRole(self, messageIndex, roleIndex, enabled): 1971 | self.lock.acquire() 1972 | messageEntry = self.arrayOfMessages[messageIndex] 1973 | messageEntry.setToggleForRoleByIndex(roleIndex, enabled) 1974 | self.lock.release() 1975 | 1976 | def deleteChain(self,chainIndex): 1977 | self.lock.acquire() 1978 | chainEntry = self.arrayOfChains[chainIndex] 1979 | if chainEntry: 1980 | chainEntry.setDeleted() 1981 | self.deletedChainCount += 1 1982 | 1983 | previousRow = chainEntry.getTableRow() 1984 | for chain in [self.arrayOfChains[i] for i in self.getActiveChainIndexes()]: 1985 | if chain.getTableRow()>previousRow: 1986 | chain.setTableRow(chain.getTableRow()-1) 1987 | 1988 | self.lock.release() 1989 | 1990 | def getMessagesInOrderByRow(self): 1991 | messages = [] 1992 | for i in range(self.getActiveMessageCount()): 1993 | messages.append(self.getMessageByRow(i)) 1994 | return messages 1995 | 1996 | def getUsersInOrderByRow(self): 1997 | users = [] 1998 | for i in range(self.getActiveUserCount()): 1999 | users.append(self.getUserByRow(i)) 2000 | return users 2001 | 2002 | def moveMessageToRow(self, fromRow, toRow): 2003 | self.lock.acquire() 2004 | messages = self.getMessagesInOrderByRow() 2005 | if fromRow > toRow: 2006 | messages[fromRow].setTableRow(toRow) 2007 | for i in range(toRow,fromRow): 2008 | messages[i].setTableRow(i+1) 2009 | 2010 | elif toRow > fromRow: 2011 | messages[fromRow].setTableRow(toRow-1) 2012 | for i in range(fromRow+1,toRow): 2013 | messages[i].setTableRow(i-1) 2014 | self.lock.release() 2015 | 2016 | def moveUserToRow(self, fromRow, toRow): 2017 | self.lock.acquire() 2018 | users = self.getUsersInOrderByRow() 2019 | if fromRow > toRow: 2020 | users[fromRow].setTableRow(toRow) 2021 | for i in range(toRow,fromRow): 2022 | users[i].setTableRow(i+1) 2023 | 2024 | elif toRow > fromRow: 2025 | users[fromRow].setTableRow(toRow-1) 2026 | for i in range(fromRow+1,toRow): 2027 | users[i].setTableRow(i-1) 2028 | self.lock.release() 2029 | 2030 | 2031 | def clearAllChainResults(self): 2032 | for i in self.getActiveUserIndexes(): 2033 | self.arrayOfUsers[i].clearChainResults() 2034 | 2035 | def getUserByName(self, name): 2036 | for i in self.getActiveUserIndexes(): 2037 | if self.arrayOfUsers[i]._name == name: 2038 | return self.arrayOfUsers[i] 2039 | 2040 | def getRoleByName(self, name): 2041 | for i in self.getActiveRoleIndexes(): 2042 | if self.arrayOfRoles[i]._name == name: 2043 | return self.arrayOfRoles[i] 2044 | 2045 | def addNewSV(self, name): 2046 | if not self.getSVByName(name): 2047 | self.lock.acquire() 2048 | newSVEntry = SVEntry(name) 2049 | self.arrayOfSVs.add(newSVEntry) 2050 | self.lock.release() 2051 | return newSVEntry 2052 | 2053 | def getSVByName(self, name): 2054 | for sv in self.arrayOfSVs: 2055 | if sv._name == name: 2056 | return sv 2057 | return None 2058 | 2059 | def deleteSV(self, index): 2060 | if index >=0 and index=0 and index =0 and headerIndex= 0 and svIndex < self._db.arrayOfSVs.size(): 2111 | return self._db.arrayOfSVs[svIndex]._name 2112 | else: 2113 | roleEntry = self._db.getRoleByColumn(columnIndex, 'u') 2114 | if roleEntry: 2115 | return roleEntry._name 2116 | return "" 2117 | 2118 | def getValueAt(self, rowIndex, columnIndex): 2119 | userEntry = self._db.getUserByRow(rowIndex) 2120 | headerIndex = columnIndex-self._db.STATIC_USER_TABLE_COLUMN_COUNT 2121 | svIndex = headerIndex - self._db.headerCount 2122 | if userEntry: 2123 | if columnIndex == 0: 2124 | return userEntry._name 2125 | elif columnIndex == 1: 2126 | return userEntry._cookies 2127 | elif headerIndex >=0 and headerIndex= 0 and svIndex < self._db.arrayOfSVs.size(): 2130 | return self._db.arrayOfSVs[svIndex].getValueForUserIndex(userEntry._index) 2131 | else: 2132 | roleEntry = self._db.getRoleByColumn(columnIndex, 'u') 2133 | if roleEntry: 2134 | roleIndex = roleEntry._index 2135 | return roleIndex in userEntry._roles and userEntry._roles[roleIndex] 2136 | return "" 2137 | 2138 | def addRow(self, row): 2139 | self.fireTableRowsInserted(row,row) 2140 | 2141 | def setValueAt(self, val, row, col): 2142 | # NOTE: testing if .locked is ok here since its a manual operation 2143 | if self._db.lock.locked(): 2144 | return 2145 | userEntry = self._db.getUserByRow(row) 2146 | headerIndex = col-self._db.STATIC_USER_TABLE_COLUMN_COUNT 2147 | svIndex = headerIndex - self._db.headerCount 2148 | if userEntry: 2149 | if col == 0: 2150 | # Verify user name does not already exist 2151 | if not self._db.getUserByName(val): 2152 | # Rename SingleUser role too 2153 | roleEntry = self._db.getRoleByName(userEntry._name+" (only)") 2154 | if roleEntry: 2155 | roleEntry._name = val+" (only)" 2156 | userEntry._name = val 2157 | elif col == 1: 2158 | userEntry._cookies = val 2159 | elif headerIndex >=0 and headerIndex= 0 and svIndex < self._db.arrayOfSVs.size(): 2162 | self._db.arrayOfSVs[svIndex].setValueForUserIndex(userEntry._index, val) 2163 | else: 2164 | roleIndex = self._db.getRoleByColumn(col, 'u')._index 2165 | userEntry.addRoleByIndex(roleIndex, val) 2166 | 2167 | self.fireTableCellUpdated(row,col) 2168 | # Refresh dropdown menu for Chains and SingleUser Role names for Messages 2169 | self._extender._chainTable.redrawTable() 2170 | self._extender._messageTable.redrawTable() 2171 | 2172 | # Set checkboxes and role editable 2173 | def isCellEditable(self, row, col): 2174 | return True 2175 | 2176 | # Create checkboxes 2177 | def getColumnClass(self, columnIndex): 2178 | if columnIndex < self._db.STATIC_USER_TABLE_COLUMN_COUNT+self._db.headerCount+self._db.arrayOfSVs.size(): 2179 | return str 2180 | else: 2181 | return Boolean 2182 | 2183 | 2184 | class UserTable(JTable): 2185 | 2186 | def __init__(self, model): 2187 | self.setModel(model) 2188 | return 2189 | 2190 | def redrawTable(self): 2191 | # NOTE: this is prob ineffecient but it should catchall for changes to the table 2192 | self.getModel().fireTableStructureChanged() 2193 | self.getModel().fireTableDataChanged() 2194 | 2195 | # User Name 2196 | self.getColumnModel().getColumn(0).setMinWidth(150); 2197 | self.getColumnModel().getColumn(0).setMaxWidth(1000); 2198 | 2199 | # Cookie 2200 | self.getColumnModel().getColumn(1).setMinWidth(150); 2201 | self.getColumnModel().getColumn(1).setMaxWidth(1500); 2202 | 2203 | self.getTableHeader().getDefaultRenderer().setHorizontalAlignment(JLabel.CENTER) 2204 | 2205 | 2206 | class MessageTableModel(AbstractTableModel): 2207 | 2208 | def __init__(self, extender): 2209 | self._extender = extender 2210 | self._db = extender._db 2211 | 2212 | def getRowCount(self): 2213 | return self._db.getActiveMessageCount() 2214 | 2215 | def getColumnCount(self): 2216 | return self._db.getActiveRoleCount()+self._db.STATIC_MESSAGE_TABLE_COLUMN_COUNT 2217 | 2218 | def getColumnName(self, columnIndex): 2219 | if columnIndex == 0: 2220 | return "ID" 2221 | elif columnIndex == 1: 2222 | return "Request Name" 2223 | elif columnIndex == 2: 2224 | return "Response Regex" 2225 | else: 2226 | roleEntry = self._db.getRoleByColumn(columnIndex, 'm') 2227 | if roleEntry: 2228 | return roleEntry._name 2229 | # TODO (0.9): Maybe show the index here to help with constructing state files? 2230 | #return roleEntry._name+" (#"+str(roleEntry._index)+")" 2231 | return "" 2232 | 2233 | def getValueAt(self, rowIndex, columnIndex): 2234 | messageEntry = self._db.getMessageByRow(rowIndex) 2235 | if messageEntry: 2236 | if columnIndex == 0: 2237 | return str(messageEntry._index) 2238 | elif columnIndex == 1: 2239 | return messageEntry._name 2240 | elif columnIndex == 2: 2241 | return messageEntry._regex 2242 | else: 2243 | roleEntry = self._db.getRoleByColumn(columnIndex, 'm') 2244 | if roleEntry: 2245 | roleIndex = roleEntry._index 2246 | return roleIndex in messageEntry._roles and messageEntry._roles[roleIndex] 2247 | return "" 2248 | 2249 | def addRow(self, row): 2250 | self.fireTableRowsInserted(row,row) 2251 | 2252 | def setValueAt(self, val, row, col): 2253 | # NOTE: testing if .locked is ok here since its a manual operation 2254 | if self._db.lock.locked(): 2255 | return 2256 | messageEntry = self._db.getMessageByRow(row) 2257 | if messageEntry: 2258 | if col == self._db.STATIC_MESSAGE_TABLE_COLUMN_COUNT-2: 2259 | messageEntry._name = val 2260 | elif col == self._db.STATIC_MESSAGE_TABLE_COLUMN_COUNT-1: 2261 | messageEntry._regex = val 2262 | # Add this value to the array 2263 | if val and val not in self._db.arrayOfRegexes: 2264 | self._db.arrayOfRegexes.append(val) 2265 | # TODO (0.9): Remove unused Regexes from that list 2266 | else: 2267 | roleIndex = self._db.getRoleByColumn(col, 'm')._index 2268 | messageEntry.addRoleByIndex(roleIndex,val) 2269 | 2270 | self.fireTableCellUpdated(row,col) 2271 | # Update the checkbox result colors since there was a change 2272 | if col >= self._db.STATIC_MESSAGE_TABLE_COLUMN_COUNT-1: 2273 | messageEntry.clearResults() 2274 | for i in range(self._db.STATIC_MESSAGE_TABLE_COLUMN_COUNT, self.getColumnCount()): 2275 | self.fireTableCellUpdated(row,i) 2276 | # Backup option 2277 | # Update entire table since it affects color 2278 | # self.fireTableDataChanged() 2279 | 2280 | # Refresh table so that combobox updates 2281 | self._extender._messageTable.redrawTable() 2282 | 2283 | 2284 | # Set checkboxes editable 2285 | def isCellEditable(self, row, col): 2286 | # Include regex 2287 | if col >= self._db.STATIC_MESSAGE_TABLE_COLUMN_COUNT-2: 2288 | return True 2289 | return False 2290 | 2291 | # Create checkboxes 2292 | def getColumnClass(self, columnIndex): 2293 | if columnIndex < self._db.STATIC_MESSAGE_TABLE_COLUMN_COUNT: 2294 | return str 2295 | else: 2296 | return Boolean 2297 | 2298 | 2299 | 2300 | class MessageTable(JTable): 2301 | 2302 | def __init__(self, model): 2303 | self.setModel(model) 2304 | self._extender = model._extender 2305 | self._viewerMap = {} 2306 | return 2307 | 2308 | def changeSelection(self, row, col, toggle, extend): 2309 | # show the message entry for the selected row 2310 | selectedMessage = self.getModel()._db.getMessageByRow(row) 2311 | 2312 | # Update messages with any user edits to original requests: 2313 | self.updateMessages() 2314 | self._extender._tabs.removeAll() 2315 | 2316 | # NOTE: testing if .locked is ok here since its a manual operation 2317 | if self.getModel()._db.lock.locked(): 2318 | # Provide some feedback on a click 2319 | self.redrawTable() 2320 | return 2321 | 2322 | # Create original Request tab and set default tab to Request 2323 | # Then Create test tabs and set the default tab to Response for easy analysis 2324 | originalTab = self.createRequestTabs(selectedMessage._requestResponse, True, selectedMessage._index) 2325 | originalTab.setSelectedIndex(0) 2326 | self._extender._tabs.addTab("Original",originalTab) 2327 | 2328 | for userEntry in self.getModel()._db.getUsersInOrderByRow(): 2329 | if userEntry._index in selectedMessage._userRuns.keys(): 2330 | tabname = str(userEntry._name) 2331 | self._extender._tabs.addTab(tabname,self.createRequestTabs(selectedMessage._userRuns[userEntry._index])) 2332 | 2333 | JTable.changeSelection(self, row, col, toggle, extend) 2334 | return 2335 | 2336 | def createRequestTabs(self, requestResponse, original=False, index=-1): 2337 | 2338 | class RequestResponseTabbedPane(JTabbedPane): 2339 | def __init__(self, requestResponse): 2340 | self._requestResponse=requestResponse 2341 | 2342 | requestTabs = RequestResponseTabbedPane(requestResponse) 2343 | requestViewer = self._extender._callbacks.createMessageEditor(self._extender, original) 2344 | responseViewer = self._extender._callbacks.createMessageEditor(self._extender, False) 2345 | requestTabs.addTab("Request", requestViewer.getComponent()) 2346 | requestTabs.addTab("Response", responseViewer.getComponent()) 2347 | self._extender._callbacks.customizeUiComponent(requestTabs) 2348 | requestViewer.setMessage(requestResponse.getRequest(), True) 2349 | if requestResponse.getResponse(): 2350 | responseViewer.setMessage(requestResponse.getResponse(), False) 2351 | if not original: 2352 | requestTabs.setSelectedIndex(1) 2353 | 2354 | if original and index>=0: 2355 | self._viewerMap[index] = requestViewer 2356 | 2357 | return requestTabs 2358 | 2359 | def redrawTable(self): 2360 | # NOTE: this is prob ineffecient but it should catchall for changes to the table 2361 | self.getModel().fireTableStructureChanged() 2362 | self.getModel().fireTableDataChanged() 2363 | 2364 | db = self.getModel()._db 2365 | 2366 | # Regex comboboxes 2367 | regexComboBox = JComboBox(db.arrayOfRegexes) 2368 | regexComboBox.setEditable(True) 2369 | regexComboBoxEditor = DefaultCellEditor(regexComboBox) 2370 | self.getColumnModel().getColumn(2).setCellEditor(regexComboBoxEditor) 2371 | 2372 | 2373 | 2374 | # Resize 2375 | self.getColumnModel().getColumn(0).setMinWidth(30); 2376 | self.getColumnModel().getColumn(0).setMaxWidth(45); 2377 | self.getColumnModel().getColumn(1).setMinWidth(300); 2378 | self.getColumnModel().getColumn(2).setMinWidth(150); 2379 | 2380 | def updateMessages(self): 2381 | # For now it sounds like this does not need to be locked, since its only manual operations 2382 | for messageIndex in self._viewerMap: 2383 | requestViewer = self._viewerMap[messageIndex] 2384 | if requestViewer and requestViewer.isMessageModified(): 2385 | messageEntry = self.getModel()._db.arrayOfMessages[messageIndex] 2386 | newMessage = requestViewer.getMessage() 2387 | # TODO save the response too? Downside is that the original may not match the response anymore 2388 | messageEntry._requestResponse = RequestResponseStored(self._extender, 2389 | request=newMessage, 2390 | httpService=messageEntry._requestResponse.getHttpService()) 2391 | self._viewerMap = {} 2392 | 2393 | 2394 | 2395 | 2396 | ### 2397 | ### Chain Tables 2398 | ### 2399 | 2400 | class ChainTableModel(AbstractTableModel): 2401 | 2402 | def __init__(self, extender): 2403 | self._extender = extender 2404 | self._db = extender._db 2405 | self.chainFromDefault = "All Users (Default)" 2406 | self.requestPrefix = "Request: " 2407 | self.svPrefix = "SV_" 2408 | self.destPrefix = "Request(s): " 2409 | 2410 | def getRowCount(self): 2411 | return self._db.getActiveChainCount() 2412 | 2413 | def getColumnCount(self): 2414 | # Disable if there arent any chains 2415 | if not self._db.getActiveChainCount(): 2416 | return 1 2417 | return self._db.STATIC_CHAIN_TABLE_COLUMN_COUNT 2418 | 2419 | def getColumnName(self, columnIndex): 2420 | if self.getColumnCount() == 1: 2421 | return "" 2422 | 2423 | if columnIndex == 0: 2424 | return "Chain Name" 2425 | elif columnIndex == 1: 2426 | return "Source" 2427 | elif columnIndex == 2: 2428 | return "Regex - Extract from HTTP Response" 2429 | elif columnIndex == 3: 2430 | return "Destination(s)" 2431 | elif columnIndex == 4: 2432 | return "Regex - Replace into HTTP Request" 2433 | elif columnIndex == 5: 2434 | return "Use Values From:" 2435 | elif columnIndex == 6: 2436 | return "Transformers" 2437 | return "" 2438 | 2439 | def getValueAt(self, rowIndex, columnIndex): 2440 | if self.getColumnCount() == 1: 2441 | return "" 2442 | 2443 | chainEntry = self._db.getChainByRow(rowIndex) 2444 | if chainEntry: 2445 | if columnIndex == 0: 2446 | return chainEntry._name 2447 | elif columnIndex == 1: 2448 | if chainEntry._fromID.isdigit() and int(chainEntry._fromID) in self._db.getActiveMessageIndexes(): 2449 | return self.requestPrefix+chainEntry._fromID 2450 | elif chainEntry._fromID.startswith(self.svPrefix): 2451 | # If it's a string, check if its a SV 2452 | svEntry = self._db.getSVByName(chainEntry._fromID[len(self.svPrefix):]) 2453 | if svEntry: 2454 | return svEntry._name 2455 | else: 2456 | return "" 2457 | else: 2458 | return "" 2459 | elif columnIndex == 2: 2460 | return chainEntry._fromRegex 2461 | elif columnIndex == 3: 2462 | return "" if not chainEntry._toID else self.destPrefix+chainEntry._toID 2463 | elif columnIndex == 4: 2464 | return chainEntry._toRegex 2465 | elif columnIndex == 5: 2466 | if chainEntry._sourceUser in self._db.getActiveUserIndexes(): 2467 | return self._db.arrayOfUsers[chainEntry._sourceUser]._name 2468 | elif chainEntry._sourceUser == -1: 2469 | return self.chainFromDefault 2470 | else: 2471 | return "" 2472 | elif columnIndex == 6: 2473 | ret = "x" 2474 | for transformer in chainEntry._transformers: 2475 | ret = transformer+"("+ret+")" 2476 | return "" if ret == "x" else ret 2477 | return "" 2478 | 2479 | def addRow(self, row): 2480 | self.fireTableRowsInserted(row,row) 2481 | 2482 | def setValueAt(self, val, row, col): 2483 | # NOTE: testing if .locked is ok here since its a manual operation 2484 | if self._db.lock.locked(): 2485 | return 2486 | chainEntry = self._db.getChainByRow(row) 2487 | if chainEntry: 2488 | if col == 0: 2489 | chainEntry._name = val 2490 | elif col == 1: 2491 | if val and self.requestPrefix in val and val[len(self.requestPrefix):].isdigit(): 2492 | chainEntry._fromID = val[len(self.requestPrefix):] 2493 | else: 2494 | # If it's a string, check if its a SV 2495 | svEntry = self._db.getSVByName(val) 2496 | if svEntry: 2497 | chainEntry._fromID = self.svPrefix+svEntry._name 2498 | # Clear fromRegex since its unused 2499 | chainEntry._fromRegex = "" 2500 | self.fireTableCellUpdated(row,col+1) 2501 | else: 2502 | chainEntry._fromID = "" 2503 | elif col == 2: 2504 | chainEntry._fromRegex = val 2505 | elif col == 3: 2506 | chainEntry._toID = val 2507 | elif col == 4: 2508 | chainEntry._toRegex = val 2509 | elif col == 5: 2510 | user = self._db.getUserByName(val) 2511 | if user: 2512 | chainEntry._sourceUser = user._index 2513 | else: 2514 | chainEntry._sourceUser = -1 2515 | elif col == 6: 2516 | if val == "(clear)": 2517 | chainEntry.clearTransformers() 2518 | else: 2519 | chainEntry.addTransformer(val) 2520 | 2521 | self.fireTableCellUpdated(row,col) 2522 | 2523 | 2524 | def isCellEditable(self, row, col): 2525 | if col >= 0: 2526 | # Disable Regex when SV 2527 | if col == 2 and self._db.getChainByRow(row).getSVName(): 2528 | return False 2529 | else: 2530 | return True 2531 | return False 2532 | 2533 | def getColumnClass(self, columnIndex): 2534 | return str 2535 | 2536 | 2537 | 2538 | class ChainTable(JTable): 2539 | 2540 | def __init__(self, model): 2541 | self.setModel(model) 2542 | return 2543 | 2544 | def redrawTable(self): 2545 | # NOTE: this is prob ineffecient but it should catchall for changes to the table 2546 | self.getModel().fireTableStructureChanged() 2547 | self.getModel().fireTableDataChanged() 2548 | 2549 | 2550 | 2551 | 2552 | if self.getModel().getColumnCount() > 1: 2553 | 2554 | db = self.getModel()._db 2555 | 2556 | # Chain Use Value From comboboxes 2557 | users = [self.getModel().chainFromDefault]+[userEntry._name for userEntry in db.getUsersInOrderByRow()] 2558 | usersComboBox = JComboBox(users) 2559 | usersComboBoxEditor = DefaultCellEditor(usersComboBox) 2560 | self.getColumnModel().getColumn(5).setCellEditor(usersComboBoxEditor) 2561 | 2562 | # Tranformers Combobox 2563 | transformers = ["(clear)"]+ChainEntry.TransformerList 2564 | transformerComboBox = JComboBox(transformers) 2565 | transformerComboBoxEditor = DefaultCellEditor(transformerComboBox) 2566 | self.getColumnModel().getColumn(6).setCellEditor(transformerComboBoxEditor) 2567 | 2568 | # Source ID comboboxes 2569 | sources = [sv._name for sv in db.arrayOfSVs] + [self.getModel().requestPrefix+str(x) for x in db.getActiveMessageIndexes()] 2570 | sourcesComboBox = JComboBox(sources) 2571 | sourcesComboBoxEditor = DefaultCellEditor(sourcesComboBox) 2572 | self.getColumnModel().getColumn(1).setCellEditor(sourcesComboBoxEditor) 2573 | 2574 | 2575 | destPrefix = self.getModel().destPrefix 2576 | # Popup editor for DEST IDs 2577 | class DestinationCellEditor(AbstractCellEditor, TableCellEditor, ActionListener): 2578 | # https://stackoverflow.com/questions/14153544/jtable-how-to-update-cell-using-custom-editor-by-pop-up-input-dialog-box 2579 | self.scrollablePane = JScrollPane() 2580 | self.destList = JList() 2581 | self.button = JButton() 2582 | self.oldVal = "" 2583 | 2584 | def actionPerformed(self,e): 2585 | JOptionPane.showMessageDialog(self.button,self.scrollablePane,"Select All Request IDs",JOptionPane.PLAIN_MESSAGE) 2586 | self.fireEditingStopped() 2587 | 2588 | def getTableCellEditorComponent(self,table,value,isSelected,rowIndex,vColIndex): 2589 | self.oldVal = value if destPrefix not in value else value[len(destPrefix):] 2590 | dests = db.getActiveMessageIndexes() 2591 | if dests: 2592 | self.destList = JList(dests) 2593 | self.destList.setVisibleRowCount(10) 2594 | self.scrollablePane = JScrollPane(self.destList) 2595 | 2596 | self.button = JButton() 2597 | self.button.setBorderPainted(False) 2598 | self.button.setOpaque(False) 2599 | self.button.setContentAreaFilled(False) 2600 | 2601 | self.button.addActionListener(self) 2602 | return self.button 2603 | 2604 | 2605 | def getCellEditorValue(self): 2606 | newValues = self.destList.getSelectedValuesList() 2607 | if not newValues: 2608 | return self.oldVal 2609 | return self.listToRanges(newValues) 2610 | 2611 | # Convert a list of ints into a range string 2612 | def listToRanges(self, intList): 2613 | ret = [] 2614 | for val in sorted(intList): 2615 | if not ret or ret[-1][-1]+1 != val: 2616 | ret.append([val]) 2617 | else: 2618 | ret[-1].append(val) 2619 | return ",".join([str(x[0]) if len(x)==1 else str(x[0])+"-"+str(x[-1]) for x in ret]) 2620 | 2621 | self.getColumnModel().getColumn(3).setCellEditor(DestinationCellEditor()) 2622 | 2623 | # Resize 2624 | self.getColumnModel().getColumn(0).setMinWidth(180); 2625 | self.getColumnModel().getColumn(0).setMaxWidth(300); 2626 | self.getColumnModel().getColumn(1).setMinWidth(115); 2627 | self.getColumnModel().getColumn(1).setMaxWidth(175); 2628 | self.getColumnModel().getColumn(2).setMinWidth(180); 2629 | self.getColumnModel().getColumn(3).setMinWidth(160); 2630 | self.getColumnModel().getColumn(3).setMaxWidth(320); 2631 | self.getColumnModel().getColumn(4).setMinWidth(180); 2632 | self.getColumnModel().getColumn(5).setMinWidth(150); 2633 | self.getColumnModel().getColumn(5).setMaxWidth(270); 2634 | self.getColumnModel().getColumn(6).setMinWidth(100); 2635 | 2636 | 2637 | 2638 | # For color-coding checkboxes in the message table 2639 | # Also Grey when not enabled 2640 | class SuccessBooleanRenderer(JCheckBox,TableCellRenderer): 2641 | 2642 | def __init__(self, defaultCellRender, db): 2643 | self.setOpaque(True) 2644 | self.setHorizontalAlignment(JLabel.CENTER) 2645 | self._defaultCellRender = defaultCellRender 2646 | self._db = db 2647 | 2648 | def getTableCellRendererComponent(self, table, value, isSelected, hasFocus, row, column): 2649 | cell = self._defaultCellRender.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column) 2650 | if value: 2651 | cell.setSelected(True) 2652 | else: 2653 | cell.setSelected(False) 2654 | if isSelected: 2655 | cell.setForeground(table.getSelectionForeground()) 2656 | cell.setBackground(table.getSelectionBackground()) 2657 | else: 2658 | cell.setForeground(table.getForeground()) 2659 | cell.setBackground(table.getBackground()) 2660 | 2661 | # Color based on results 2662 | if column >= self._db.STATIC_MESSAGE_TABLE_COLUMN_COUNT: 2663 | messageEntry = self._db.getMessageByRow(row) 2664 | if messageEntry: 2665 | if messageEntry.isEnabled(): 2666 | roleEntry = self._db.getRoleByColumn(column, 'm') 2667 | if roleEntry: 2668 | roleIndex = roleEntry._index 2669 | if not roleIndex in messageEntry._roleResults: 2670 | if isSelected: 2671 | cell.setBackground(table.getSelectionBackground()) 2672 | else: 2673 | cell.setBackground(table.getBackground()) 2674 | else: 2675 | # This site was used for generating color blends when selected (option 6 of 12) 2676 | # http://meyerweb.com/eric/tools/color-blend/#FFCD81:00CCFF:10:hex 2677 | sawExpectedResults = messageEntry._roleResults[roleIndex] 2678 | checkboxChecked = messageEntry._roles[roleIndex] 2679 | 2680 | # NOTE: currently no way to detect false positive in failure mode 2681 | # failureRegexMode = messageEntry.isFailureRegex() 2682 | 2683 | if sawExpectedResults: 2684 | # Set Green if success 2685 | if isSelected: 2686 | cell.setBackground(Color(0xC8,0xE0,0x51)) 2687 | else: 2688 | cell.setBackground(Color(0x87,0xf7,0x17)) 2689 | elif checkboxChecked: 2690 | # Set Blue if its probably a false positive 2691 | if isSelected: 2692 | cell.setBackground(Color(0x8B, 0xCD, 0xBA)) 2693 | else: 2694 | cell.setBackground(Color(0x00,0xCC,0xFF)) 2695 | else: 2696 | # Set Red if fail 2697 | if isSelected: 2698 | cell.setBackground(Color(0xFF, 0x87, 0x51)) 2699 | else: 2700 | cell.setBackground(Color(0xFF, 0x32, 0x17)) 2701 | else: 2702 | if isSelected: 2703 | cell.setBackground(Color(0xD1,0xB5,0xA3)) 2704 | else: 2705 | cell.setBackground(Color.GRAY) 2706 | 2707 | return cell 2708 | 2709 | 2710 | # For color-coding successregex in the message table 2711 | # Also Grey when not enabled 2712 | class RegexRenderer(JLabel, TableCellRenderer): 2713 | 2714 | def __init__(self, defaultCellRender, db): 2715 | self._defaultCellRender = defaultCellRender 2716 | self._db = db 2717 | 2718 | def getTableCellRendererComponent(self, table, value, isSelected, hasFocus, row, column): 2719 | # Regex color 2720 | cell = self._defaultCellRender.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column) 2721 | messageEntry = self._db.getMessageByRow(row) 2722 | 2723 | if column == self._db.STATIC_MESSAGE_TABLE_COLUMN_COUNT-1: 2724 | if messageEntry: 2725 | if messageEntry.isFailureRegex(): 2726 | # Set Grey if failure mode 2727 | if isSelected: 2728 | cell.setBackground(Color(0xD1,0xB5,0xA3)) 2729 | else: 2730 | cell.setBackground(Color(0x99,0x99,0xCC)) 2731 | else: 2732 | if isSelected: 2733 | cell.setBackground(table.getSelectionBackground()) 2734 | else: 2735 | cell.setBackground(table.getBackground()) 2736 | else: 2737 | if isSelected: 2738 | cell.setBackground(table.getSelectionBackground()) 2739 | else: 2740 | cell.setBackground(table.getBackground()) 2741 | # Set grey if disabled 2742 | if messageEntry and not messageEntry.isEnabled(): 2743 | if isSelected: 2744 | cell.setBackground(Color(0xD1,0xB5,0xA3)) 2745 | else: 2746 | cell.setBackground(Color.GRAY) 2747 | return cell 2748 | 2749 | # Default Renderer checking User Table for Enabled 2750 | class UserEnabledRenderer(TableCellRenderer): 2751 | def __init__(self, defaultCellRender, db): 2752 | self._defaultCellRender = defaultCellRender 2753 | self._db = db 2754 | 2755 | def getTableCellRendererComponent(self, table, value, isSelected, hasFocus, row, column): 2756 | # Regex color 2757 | cell = self._defaultCellRender.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column) 2758 | userEntry = self._db.getUserByRow(row) 2759 | if userEntry and not userEntry.isEnabled(): 2760 | if isSelected: 2761 | cell.setBackground(Color(0xD1,0xB5,0xA3)) 2762 | else: 2763 | cell.setBackground(Color.GRAY) 2764 | elif isSelected: 2765 | cell.setBackground(table.getSelectionBackground()) 2766 | else: 2767 | cell.setBackground(table.getBackground()) 2768 | return cell 2769 | 2770 | # TODO (0.9): combine these classes 2771 | # Default Renderer checking Chain Table for Enabled 2772 | class ChainEnabledRenderer(TableCellRenderer): 2773 | def __init__(self, defaultCellRender, db): 2774 | self._defaultCellRender = defaultCellRender 2775 | self._db = db 2776 | 2777 | def getTableCellRendererComponent(self, table, value, isSelected, hasFocus, row, column): 2778 | # Regex color 2779 | cell = self._defaultCellRender.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column) 2780 | chainEntry = self._db.getChainByRow(row) 2781 | if chainEntry and not chainEntry.isEnabled(): 2782 | if isSelected: 2783 | cell.setBackground(Color(0xD1,0xB5,0xA3)) 2784 | else: 2785 | cell.setBackground(Color.GRAY) 2786 | elif isSelected: 2787 | cell.setBackground(table.getSelectionBackground()) 2788 | else: 2789 | cell.setBackground(table.getBackground()) 2790 | return cell 2791 | 2792 | 2793 | 2794 | ## 2795 | ## Classes for Messages, Roles, and Users 2796 | ## 2797 | 2798 | class MessageEntry: 2799 | 2800 | def __init__(self, index, tableRow, requestResponse, name = "", roles = {}, regex = "", deleted = False, failureRegexMode = False, enabled = True): 2801 | self._index = index 2802 | self._tableRow = tableRow 2803 | self._requestResponse = requestResponse 2804 | self._name = name 2805 | self._roles = roles.copy() 2806 | self._failureRegexMode = failureRegexMode 2807 | self._regex = regex 2808 | self._deleted = deleted 2809 | self._userRuns = {} 2810 | self._roleResults = {} 2811 | self._enabled = enabled 2812 | return 2813 | 2814 | # Role are the index of the db Role array and a bool for whether the checkbox is default enabled or not 2815 | def addRoleByIndex(self,roleIndex,enabled=False): 2816 | self._roles[roleIndex] = enabled; 2817 | 2818 | def setToggleForRoleByIndex(self, roleIndex, enabled): 2819 | self._roles[roleIndex] = enabled; 2820 | 2821 | # Add one Run Result of user x message 2822 | def addRunByUserIndex(self,userIndex,requestResponse): 2823 | self._userRuns[userIndex] = requestResponse 2824 | 2825 | def setRoleResultByRoleIndex(self, roleIndex, roleResult): 2826 | self._roleResults[roleIndex] = roleResult 2827 | 2828 | def setDeleted(self): 2829 | self._deleted = True 2830 | 2831 | def isDeleted(self): 2832 | return self._deleted 2833 | 2834 | def setTableRow(self, row): 2835 | self._tableRow = row 2836 | 2837 | def getTableRow(self): 2838 | return self._tableRow 2839 | 2840 | def isFailureRegex(self): 2841 | return self._failureRegexMode 2842 | 2843 | def setFailureRegex(self, enabled=True): 2844 | self._failureRegexMode = enabled 2845 | 2846 | def clearResults(self): 2847 | # Clear Previous Results: 2848 | self._roleResults = {} 2849 | self._userRuns = {} 2850 | 2851 | def isEnabled(self): 2852 | return self._enabled 2853 | 2854 | def toggleEnabled(self): 2855 | self._enabled = not self._enabled 2856 | 2857 | 2858 | class UserEntry: 2859 | 2860 | def __init__(self, index, tableRow, name, roles = {}, deleted=False, cookies="", headers = [], enabled = True): 2861 | self._index = index 2862 | self._name = name 2863 | self._roles = roles.copy() 2864 | self._deleted = deleted 2865 | self._tableRow = tableRow 2866 | self._cookies = cookies 2867 | self._headers = headers[:] 2868 | self._chainResults = {} 2869 | self._enabled = enabled 2870 | return 2871 | 2872 | # Roles are the index of the db role array and a bool for whether the checkbox is default enabled or not 2873 | def addRoleByIndex(self, roleIndex, enabled=False): 2874 | self._roles[roleIndex] = enabled 2875 | 2876 | def addChainResultByMessageIndex(self, toID, toValue, chainIndex): 2877 | if not toID in self._chainResults: 2878 | self._chainResults[toID] = [(toValue, chainIndex)] 2879 | else: 2880 | self._chainResults[toID].append((toValue, chainIndex)) 2881 | 2882 | def getChainResultByMessageIndex(self, toID): 2883 | if toID in self._chainResults: 2884 | return self._chainResults[toID] 2885 | return [] 2886 | 2887 | def clearChainResults(self): 2888 | self._chainResults = {} 2889 | 2890 | def setDeleted(self): 2891 | self._deleted = True 2892 | 2893 | def isDeleted(self): 2894 | return self._deleted 2895 | 2896 | def setTableRow(self, row): 2897 | self._tableRow = row 2898 | 2899 | def getTableRow(self): 2900 | return self._tableRow 2901 | 2902 | def isEnabled(self): 2903 | return self._enabled 2904 | 2905 | def toggleEnabled(self): 2906 | self._enabled = not self._enabled 2907 | 2908 | 2909 | 2910 | class RoleEntry: 2911 | 2912 | def __init__(self,index,columnIndex,name,deleted=False,singleUser=False): 2913 | self._index = index 2914 | self._name = name 2915 | self._deleted = deleted 2916 | self._column = columnIndex 2917 | self._singleUser = singleUser 2918 | return 2919 | 2920 | def setDeleted(self): 2921 | self._deleted = True 2922 | 2923 | def isDeleted(self): 2924 | return self._deleted 2925 | 2926 | # NOTE: in v0.6 this value was changed to index into the dynamic columns only 2927 | def setColumn(self, column): 2928 | self._column = column 2929 | 2930 | def getColumn(self): 2931 | return self._column 2932 | 2933 | def isSingleUser(self): 2934 | return self._singleUser 2935 | 2936 | class ChainEntry: 2937 | 2938 | TransformerList = ["base64","url","hex","sha1","sha256","sha512","md5"] 2939 | 2940 | def __init__(self, index, tableRow, name="", fromID="", fromRegex="", toID="", toRegex="", deleted=False, sourceUser=-1, enabled=True, transformers=[]): 2941 | self._index = index 2942 | self._fromID = fromID 2943 | self._fromRegex = fromRegex 2944 | self._toID = toID 2945 | self._toRegex = toRegex 2946 | self._deleted = deleted 2947 | self._tableRow = tableRow 2948 | self._name = name 2949 | self._sourceUser = sourceUser 2950 | self._fromStart = "" 2951 | self._fromEnd = "" 2952 | self._toStart = "" 2953 | self._toEnd = "" 2954 | self._enabled = enabled 2955 | self._transformers = transformers[:] 2956 | 2957 | return 2958 | 2959 | def setDeleted(self): 2960 | self._deleted = True 2961 | 2962 | def isDeleted(self): 2963 | return self._deleted 2964 | 2965 | def setTableRow(self, row): 2966 | self._tableRow = row 2967 | 2968 | def getTableRow(self): 2969 | return self._tableRow 2970 | 2971 | def getFromStart(self): 2972 | return self._fromStart 2973 | 2974 | def getFromEnd(self): 2975 | return self._fromEnd 2976 | 2977 | def getToStart(self): 2978 | return self._toStart 2979 | 2980 | def getToEnd(self): 2981 | return self._toEnd 2982 | 2983 | def setFromStart(self, fromStart): 2984 | self._fromStart = fromStart 2985 | if self._fromEnd: 2986 | self._fromRegex = self.getRexegFromStartAndEnd(self._fromStart,self._fromEnd) 2987 | 2988 | def setFromEnd(self, fromEnd): 2989 | self._fromEnd = fromEnd 2990 | if self._fromStart: 2991 | self._fromRegex = self.getRexegFromStartAndEnd(self._fromStart,self._fromEnd) 2992 | 2993 | def setToStart(self, toStart): 2994 | self._toStart = toStart 2995 | if self._toEnd: 2996 | self._toRegex = self.getRexegFromStartAndEnd(self._toStart,self._toEnd) 2997 | 2998 | def setToEnd(self, toEnd): 2999 | self._toEnd = toEnd 3000 | if self._toStart: 3001 | self._toRegex = self.getRexegFromStartAndEnd(self._toStart,self._toEnd) 3002 | 3003 | def getRexegFromStartAndEnd(self,start,end): 3004 | # TODO add this to the UI, perhaps with a right click option to change the table rows? 3005 | return re.escape(start)+"(.*?)"+re.escape(end) 3006 | 3007 | def getToIDRange(self): 3008 | result = [] 3009 | for part in self._toID.split(','): 3010 | if '-' in part: 3011 | a,b = part.split('-') 3012 | if a.isdigit() and b.isdigit(): 3013 | a,b = int(a),int(b) 3014 | result.extend(range(a,b+1)) 3015 | else: 3016 | if part.isdigit(): 3017 | a = int(part) 3018 | result.append(a) 3019 | return result 3020 | 3021 | def getSVName(self): 3022 | # TODO access svPrefix from above 3023 | if self._fromID.startswith("SV_"): 3024 | return self._fromID[3:] 3025 | return None 3026 | 3027 | def isEnabled(self): 3028 | return self._enabled 3029 | 3030 | def toggleEnabled(self): 3031 | self._enabled = not self._enabled 3032 | 3033 | def addTransformer(self, value): 3034 | self._transformers.append(value) 3035 | 3036 | def clearTransformers(self): 3037 | self._transformers=[] 3038 | 3039 | def transform(self, value, callbacks): 3040 | ret = value 3041 | if not ret: 3042 | return "" 3043 | try: 3044 | for transformer in self._transformers: 3045 | #self._transformerList = ["base64encode","urlencode","hexencode","sha1","sha256","sha512","md5"] 3046 | if transformer == self.TransformerList[0]: 3047 | ret = base64.b64encode(ret.encode('utf-8')) 3048 | elif transformer == self.TransformerList[1]: 3049 | ret = urllib.quote_plus(ret) 3050 | elif transformer == self.TransformerList[2]: 3051 | ret = base64.b16encode(ret) 3052 | elif transformer == self.TransformerList[3]: 3053 | ret = hashlib.sha1(ret).hexdigest() 3054 | elif transformer == self.TransformerList[4]: 3055 | ret = hashlib.sha256(ret).hexdigest() 3056 | elif transformer == self.TransformerList[5]: 3057 | ret = hashlib.sha512(ret).hexdigest() 3058 | elif transformer == self.TransformerList[6]: 3059 | ret = hashlib.md5(ret).hexdigest() 3060 | except: 3061 | traceback.print_exc(file=callbacks.getStderr()) 3062 | return value 3063 | return ret 3064 | 3065 | 3066 | 3067 | class SVEntry: 3068 | 3069 | def __init__(self, name, userValues = {}): 3070 | self._name=name 3071 | self._userValues = userValues.copy() 3072 | 3073 | def setValueForUserIndex(self, userIndex, val): 3074 | self._userValues[userIndex] = val 3075 | 3076 | def getValueForUserIndex(self, userIndex): 3077 | if userIndex in self._userValues: 3078 | return self._userValues[userIndex] 3079 | return "" 3080 | 3081 | 3082 | 3083 | ## 3084 | ## RequestResponse Implementation 3085 | ## 3086 | 3087 | class RequestResponseStored(IHttpRequestResponse): 3088 | 3089 | def __init__(self, extender, host=None, port=None, protocol=None, request=None, response=None, comment=None, highlight=None, httpService=None, requestResponse=None): 3090 | self._extender=extender 3091 | self._host=host 3092 | self._port=port 3093 | self._protocol=protocol 3094 | self._request=request 3095 | self._response=response 3096 | self._comment=comment 3097 | self._highlight=highlight 3098 | if httpService: 3099 | self.setHttpService(httpService) 3100 | if requestResponse: 3101 | self.cast(requestResponse) 3102 | return 3103 | 3104 | def getComment(self): 3105 | return self._comment 3106 | 3107 | def getHighlight(self): 3108 | return self._highlight 3109 | 3110 | def getHttpService(self): 3111 | service = self._extender._helpers.buildHttpService(self._host, self._port, self._protocol) 3112 | if service: 3113 | return service 3114 | return None 3115 | 3116 | def getRequest(self): 3117 | return self._request 3118 | 3119 | def getResponse(self): 3120 | return self._response 3121 | 3122 | def setComment(self, comment): 3123 | self._comment = comment 3124 | return 3125 | 3126 | def setHighlight(self, color): 3127 | self._highlight = color 3128 | return 3129 | 3130 | def setHttpService(self, httpService): 3131 | self._host=httpService.getHost() 3132 | self._port=httpService.getPort() 3133 | self._protocol=httpService.getProtocol() 3134 | return 3135 | 3136 | def setRequest(self, message): 3137 | self._request = message 3138 | return 3139 | 3140 | def setResponse(self, message): 3141 | self._response = message 3142 | return 3143 | 3144 | def cast(self, requestResponse): 3145 | self.setComment(requestResponse.getComment()) 3146 | self.setHighlight(requestResponse.getHighlight()) 3147 | self.setHttpService(requestResponse.getHttpService()) 3148 | self.setRequest(requestResponse.getRequest()) 3149 | self.setResponse(requestResponse.getResponse()) 3150 | 3151 | 3152 | ## 3153 | ## Drag and Drop 3154 | ## 3155 | 3156 | class RowTransferHandler(TransferHandler): 3157 | 3158 | def __init__(self, table): 3159 | self._table = table 3160 | 3161 | def createTransferable(self, c): 3162 | assert(c == self._table) 3163 | return StringSelection(str(c.getSelectedRow())) 3164 | 3165 | def getSourceActions(self, c): 3166 | return TransferHandler.COPY_OR_MOVE 3167 | 3168 | def exportDone(self, c, t, act): 3169 | if act == TransferHandler.MOVE or act == TransferHandler.NONE: 3170 | self._table.redrawTable() 3171 | 3172 | 3173 | def canImport(self, info): 3174 | b = info.getComponent() == self._table and info.isDrop() and info.isDataFlavorSupported(DataFlavor.stringFlavor) 3175 | return b 3176 | 3177 | def importData(self, info): 3178 | target = info.getComponent() 3179 | dl = info.getDropLocation() 3180 | index = dl.getRow() 3181 | tablemax = self._table.getModel().getRowCount() 3182 | if index < 0 or index > tablemax: 3183 | index = tablemax 3184 | 3185 | rowFrom = info.getTransferable().getTransferData(DataFlavor.stringFlavor) 3186 | 3187 | if isinstance(self._table, MessageTable): 3188 | self._table.getModel()._db.moveMessageToRow(int(rowFrom), int(index)) 3189 | elif isinstance(self._table, UserTable): 3190 | self._table.getModel()._db.moveUserToRow(int(rowFrom), int(index)) 3191 | 3192 | 3193 | return True 3194 | 3195 | 3196 | 3197 | ## 3198 | ## LEGACY SERIALIZABLE CLASSES 3199 | ## 3200 | 3201 | # Serializable DB 3202 | # Used to store Database to Disk on Save and Load 3203 | class MatrixDBData(): 3204 | 3205 | def __init__(self, arrayOfMessages, arrayOfRoles, arrayOfUsers, deletedUserCount, deletedRoleCount, deletedMessageCount): 3206 | 3207 | self.arrayOfMessages = arrayOfMessages 3208 | self.arrayOfRoles = arrayOfRoles 3209 | self.arrayOfUsers = arrayOfUsers 3210 | self.deletedUserCount = deletedUserCount 3211 | self.deletedRoleCount = deletedRoleCount 3212 | self.deletedMessageCount = deletedMessageCount 3213 | 3214 | # Serializable MessageEntry 3215 | # Used since the Burp RequestResponse object can not be serialized 3216 | class MessageEntryData: 3217 | 3218 | def __init__(self, index, tableRow, requestData, host, port, protocol, name, roles, successRegex, deleted): 3219 | self._index = index 3220 | self._tableRow = tableRow 3221 | self._requestData = requestData 3222 | self._host = host 3223 | self._port = port 3224 | self._protocol = protocol 3225 | self._url = "" # NOTE obsolete, kept for backwords compatability 3226 | self._name = name 3227 | self._roles = roles 3228 | # NOTE: to preserve backwords compatability, successregex will have a specific prefix "|AMFAILURE|" to indicate FailureRegexMode 3229 | self._successRegex = successRegex 3230 | self._deleted = deleted 3231 | return 3232 | 3233 | class RoleEntryData: 3234 | 3235 | def __init__(self,index,mTableColumnIndex,uTableColumnIndex,name,deleted): 3236 | self._index = index 3237 | self._name = name 3238 | self._deleted = deleted 3239 | # NOTE: to preserve backwords compatibility, these will be the dynamic column +3 3240 | self._mTableColumn = mTableColumnIndex 3241 | self._uTableColumn = uTableColumnIndex 3242 | return 3243 | 3244 | class UserEntryData: 3245 | 3246 | def __init__(self, index, tableRow, name, roles, deleted, token, staticcsrf): 3247 | self._index = index 3248 | self._name = name 3249 | self._roles = roles 3250 | self._deleted = deleted 3251 | self._tableRow = tableRow 3252 | self._token = token 3253 | self._staticcsrf = staticcsrf 3254 | return 3255 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | v0.8 - 11/26/2018 2 | 3 | STATE FILE COMPATIBILITY - v0.5.2 and greater 4 | 5 | * Major release containing several updates. 6 | 7 | * AuthMatrix Tab will now highlight when a request is sent to it from another tab. 8 | 9 | * Chains, Messages, and Users can now all be disabled using a right click menu option. 10 | 11 | * Color scheme has been updated to reflect Burp's new palette. 12 | 13 | * Users can now be Reordered via drag-and-drop. This affects which order the requests are run. (Useful for DELETE requests) 14 | 15 | * AuthMatrix now checks for chains with message dependencies and will now run requests in the correct order to satisfy those dependencies. 16 | 17 | * State File loading has been simplified to be more permissive of missing elements. Additionally, users can now load "partial states" containing only certain table data in order to update just those tables. More information describing the expected JSON structure linked in the README. 18 | 19 | * Success Regexes are now a dropdown box containing all previous regexes for fast switching 20 | 21 | * New right click menu when selecting multiple requests that allows for updating Success Regexes for all at once. 22 | 23 | * Chain Transformers: new column in chains allowing encoding of input date before it is used in a request. Current encodings supported include base64, url, hex, and several hashing functions. 24 | 25 | * Various UI updates 26 | 27 | * Bug Fixes 28 | 29 | v0.7.1 - 11/21/2017 30 | 31 | STATE FILE COMPATIBILITY - v0.5.2 and greater 32 | 33 | * Bug Fix - Issue in chain regex logic when grabbing the last HTTP Header before the body 34 | 35 | * Bug Fix - Issue with saving state returning a file of size zero if the state included any time'd out requests 36 | 37 | * Minor UI Changes 38 | 39 | v0.7 - 04/24/2017 40 | 41 | STATE FILE COMPATIBILITY - v0.5.2 and greater 42 | 43 | * Single-user Roles - Automatic roles made for each new user. Will be useful for horizontal (cross-user) auth testing. 44 | 45 | * Deprecated Post Arg field in User Table - same functionality can be accomplished with Chains 46 | 47 | * Support multiple Header fields in User Table (default changed to 0) 48 | 49 | * Added Chain Sources - configurable strings per-user that can be used as the source of a chain. 50 | 51 | * Removed most data for deleted items from the state file 52 | 53 | * Infer default Response Regex from the Response Code if available 54 | 55 | * Changed Chains Table to use dropdowns when possible 56 | 57 | * Various UI Changes and Bug Fixes 58 | 59 | v0.6.3 - 04/15/2017 60 | 61 | STATE FILE COMPATIBILITY - v0.5.2 and greater 62 | 63 | * Updated Serialization format to JSON. Loading legacy state files is still supported for now, however, all new state files will be saved to the new format. Additionally, the results of a run are now exported as well in the RunResultForRoleID field of the saved json state. 64 | 65 | NOTE: For RunResultForRoleID in the JSON state file, the key of the array matches the RoleID and the value is a boolean indicating true (green) or false (red or blue). 66 | 67 | * New right-click context options for sending cookies to users. Sends both current cookies and cookies set in the message's reponse. 68 | 69 | * Fixed bug in chaining where content-length wouldn't update appropriately 70 | 71 | * Added randomness generation. Set the string #{AUTHMATRIX:RANDOM} anywhere in the original message's header or body and the run will replace it with a random 4 char numeric string. Most common use case is for APIs that reject requests that are identical to a previous request. 72 | 73 | v0.6.2 - 10/31/2016 74 | 75 | STATE FILE COMPATIBILITY - v0.5.2 and greater 76 | 77 | * Fixed a bug when sending a result RequestResponse to another area of Burp, such as Repeater 78 | 79 | * Add "Cancel" button to end runs early 80 | 81 | * Increase default run timeout 82 | 83 | * Updated UI: Add HTTP verb to default request name, Allow shrinking of unused columns in User Table, Improved locking UI during a run 84 | 85 | v0.6.1 - 10/13/2016 86 | 87 | STATE FILE COMPATIBILITY - v0.5.2 and greater 88 | 89 | * Bug fix with Failure Regex - Image in README now shows correct usage example 90 | 91 | v0.6.0 - MAJOR UPDATE - 10/11/2016 92 | 93 | STATE FILE COMPATIBILITY - v0.5.2 and greater 94 | 95 | * Chains: 96 | - New advanced configuration table for chaining several messages together 97 | - Can be used for Advanced CSRF Token handling or propagating IDs/GUIDs to later messages 98 | - See README for instructions and examples 99 | 100 | * Added Drag-and-Drop Reorder for Requests in Message Table: 101 | - Required to support Chains 102 | 103 | * New Columns in User and Message Table: 104 | - Separate columns for cookies, headers, and POST args for clarity 105 | - Supports use-case where user requires both a header and cookies 106 | - Both tables now use index identifier instead of row number 107 | 108 | * Update Color Scheme: 109 | - Blue for False Positive and purple for Failure Regex 110 | - Colored highlighting of selected rows 111 | 112 | * Allow Editing base requests from AuthMatrix 113 | - Only works when the message has been altered with a keyboard (i.e. not "Change request method" or "Change body encoding") 114 | - https://support.portswigger.net/customer/en/portal/questions/16705314-imessageeditor-ismessagemodified-does-not-detect-modification 115 | 116 | * Change Target Domain: 117 | - You can now right click selected messages and change the target domain/port/tls configuration 118 | 119 | * Bug Fixes: 120 | - no longer sends each base request on Load State 121 | - correctly times out during Run when host is unreachable 122 | - handle empty session tokens correctly 123 | - check if the HTTP Response is empty in checkResults() and handle appropriately 124 | - run messages in order by row number, not by index 125 | - when deleting items, decrement row if the row is affected by the change, not the index 126 | 127 | 128 | v0.5.4 - 09/26/2016 129 | 130 | STATE FILE COMPATIBILITY - v0.5.2 and greater 131 | 132 | * Added support for Failure Regexes (i.e. if anonymous role is checked and it sees an HTTP 200 regex, flag it) 133 | 134 | * Bug fix: Fixed issue when session token is left blank 135 | 136 | * A more appealing color scheme 137 | 138 | v0.5.3 - 06/29/2016 139 | 140 | STATE FILE COMPATIBILITY - v0.5.2 and greater 141 | 142 | * Modify color coding of results to be nicer colors and include orange for false positives/config errors. Orange will generally imply that the session token is invalid or the success regex is incorrect. 143 | 144 | * Update readme images and describe orange color 145 | 146 | v0.5.2 - 04/13/2016 147 | 148 | STATE FILE COMPATIBILITY - v0.5.2 and greater 149 | 150 | * MAJOR BUG FIX: Issue when adding new roles after messages have already been sent to AuthMatrix. Could lead to invalid results and incorrect UI display of the Message table's checkboxes. Invalidates some previous saved states. 151 | * Re-enabled "Send from Target Map Tree" when the selected messages all have responses 152 | * Refactored runMessage code and added support for multiple cookies in the session tokens 153 | 154 | v0.5.1 - 03/21/2016 155 | 156 | STATE FILE COMPATIBILITY - v0.4 and greater 157 | 158 | * Small Bug Fix: replace .append with .add on Java ArrayList types. Not sure why .append ever worked on certain systems to begin with... 159 | 160 | v0.5 - 02/23/2016 161 | 162 | STATE FILE COMPATIBILITY - v0.4 and greater 163 | 164 | * FEATURE: Changing host domains on load when they are unaccessible 165 | * Cleaned up variable names 166 | * Removed "Send to AuthMatrix" for Target Trees due to bugginess 167 | * Removed unused libraries from code 168 | * Add confirmation for Clear 169 | * Changed load timeout to 3.0 seconds 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /JsonState.md: -------------------------------------------------------------------------------- 1 | ## State Files 2 | 3 | State files are saved as JSON strings. 4 | 5 | This document describes each field of this file so that power users can automate tasks by modifying state files directly. 6 | 7 | Note that several fields are only used internally and can safely be ignored when constructing state by hand. 8 | 9 | Additionally, several fields are base64 encoded. This is done to simplify the process of parsing JSON (such as removing comments) 10 | 11 | Several fields reference identifiers of other elements (i.e. Users reference Roles by their index). For these cases, it may not be possible to locate the index from the UI. It may be necessary to parse an existing state to determine what existing elements map to which identifiers. 12 | 13 | ## JSON Format 14 | 15 | { 16 | /** 17 | * String indicating version of AuthMatrix this state file corresponds with 18 | * @required 19 | * @type String 20 | */ 21 | "version": "0.8", 22 | 23 | /** 24 | * Array containing 0 or more Role objects 25 | * @optional: if omitted, the existing roles are unchanged 26 | * @type Array 27 | */ 28 | "arrayOfRoles": [ 29 | { 30 | /** 31 | * Internal ID of this Role 32 | * @required 33 | * @condition All IDs for a given type must be unique 34 | * @condition IDs for a given type must start from 0 35 | * @condition All elements must be listed in order by index and increment by 1 36 | * @type int 37 | */ 38 | "index": 0, 39 | 40 | /** 41 | * Which column this role is displayed in 42 | * @required 43 | * @condition All non-deleted columns for a given type must be unique 44 | * @condition Non-deleted columns must be between 0 and the amount of non-deleted elements 45 | * @type int 46 | */ 47 | "column": 0, 48 | 49 | /** 50 | * Name of the Role 51 | * @required 52 | * @condition All non-deleted elements in arrayOfRoles must have different names 53 | * @type String 54 | */ 55 | "name": "Admins", 56 | 57 | /** 58 | * Indicates whether this Role has been deleted 59 | * @optional default=false 60 | * @type Bool 61 | */ 62 | "deleted": false, 63 | 64 | /** 65 | * Indicates whether this is a Single User role 66 | * @optional default=false 67 | * @type Bool 68 | */ 69 | "singleUser": false 70 | } 71 | ], 72 | /** 73 | * Array containing 0 or more Users 74 | * @optional: if omitted, the existing Users Table is unchanged 75 | * @type Array 76 | */ 77 | "arrayOfUsers": [ 78 | { 79 | /** 80 | * Internal ID of this User 81 | * @required 82 | * @condition All IDs for a given type must be unique 83 | * @condition IDs for a given type must start from 0 84 | * @condition All elements must be listed in order by index and increment by 1 85 | * @type int 86 | */ 87 | "index": 0, 88 | 89 | /** 90 | * Which row of the table this User is displayed in 91 | * @required 92 | * @condition All non-deleted rows for a given type must be unique 93 | * @condition Non-deleted row must be between 0 and the amount of non-deleted elements 94 | * @type int 95 | */ 96 | "tableRow": 0, 97 | 98 | /** 99 | * Name of the User 100 | * @required 101 | * @condition All non-deleted elements in arrayOfUsers must have different names 102 | * @type String 103 | */ 104 | "name": "admin123", 105 | 106 | /** 107 | * Map of keys and values, keys = Role Index, value = Bool indicating whether this User belonds to the role 108 | * @required 109 | * @condition All Role IDs must be included in this map 110 | * @type map(str, bool) 111 | */ 112 | "roles": { 113 | "0": true 114 | }, 115 | 116 | /** 117 | * Indicates whether this User has been deleted 118 | * @optional default=false 119 | * @type Bool 120 | */ 121 | "deleted": false, 122 | 123 | /** 124 | * Indicates whether this User is enabled 125 | * @optional default=true 126 | * @type Bool 127 | */ 128 | "enabled": true, 129 | 130 | /** 131 | * Value of the Cookies field for this User 132 | * @optional default="" 133 | * @type Base64EncodedString 134 | */ 135 | "cookiesBase64": "c2Vzc2lvbl9pZD1hc2Rm", 136 | 137 | /** 138 | * Array of custom headers for this User 139 | * @optional default = [] 140 | * @condition All users must have the same length array of headersBase64 items 141 | * @type ArrayOfBase64EncodedStrings 142 | */ 143 | "headersBase64": [ 144 | "QXV0aG9yaXphdGlvbjogYXNkZg==" 145 | ], 146 | 147 | /** 148 | * Internal field 149 | * Ignored during Load 150 | * @optional 151 | */ 152 | "chainResults": {} 153 | } 154 | ], 155 | /** 156 | * Array containing 0 or more Requests (Messages) 157 | * @optional: if omitted, the existing Message Table is unchanged 158 | * @type Array 159 | */ 160 | "arrayOfMessages": [ 161 | { 162 | /** 163 | * Internal ID of this Message 164 | * @required 165 | * @condition All IDs for a given type must be unique 166 | * @condition IDs for a given type must start from 0 167 | * @condition All elements must be listed in order by index and increment by 1 168 | * @type int 169 | */ 170 | "index": 0, 171 | 172 | /** 173 | * Which row of the table this Message is displayed in 174 | * @required 175 | * @condition All non-deleted rows for a given type must be unique 176 | * @condition Non-deleted row must be between 0 and the amount of non-deleted elements 177 | * @type int 178 | */ 179 | "tableRow": 0, 180 | 181 | /** 182 | * Friendly Name of the Message 183 | * @required 184 | * @condition All non-deleted elements in arrayOfMessages must have different names 185 | * @type String 186 | */ 187 | "name": "GET /HumanResources/admin/users", 188 | 189 | /** 190 | * Map of keys and values, keys = Role Index, value = Bool indicating whether this Message should succeed for this Role 191 | * @required 192 | * @condition All Role IDs must be included in this map 193 | * @type map(str,bool) 194 | */ 195 | "roles": { 196 | "0": true 197 | }, 198 | 199 | /** 200 | * Message Body 201 | * @required 202 | * @type Base64EncodedString 203 | */ 204 | "requestBase64": "R0VUIC9IdW1hblJlc291cmNlcy9hZG1pbi91c2VycyBIVFRQLzEuMQ0KSG9zdDogd3d3LnNlY3VyaXR5aW5ub3ZhdGlvbi5jb20KQWNjZXB0OiB0ZXh0L2h0bWwsYXBwbGljYXRpb24veGh0bWwreG1sLGFwcGxpY2F0aW9uL3htbDtxPTAuOSwqLyo7cT0wLjgNCkFjY2VwdC1MYW5ndWFnZTogZW4tVVMsZW47cT0wLjUNCkFjY2VwdC1FbmNvZGluZzogZ3ppcCwgZGVmbGF0ZQ0KQ29ubmVjdGlvbjogY2xvc2UNCgo==", 205 | 206 | /** 207 | * HTTP Protocol 208 | * @required 209 | * @type string[http|https] 210 | */ 211 | "protocol": "http", 212 | 213 | /** 214 | * TCP Port of target 215 | * @required 216 | * @type int[1-65535] 217 | */ 218 | "port": 80, 219 | 220 | /** 221 | * Domain of target 222 | * @required 223 | * @type string 224 | */ 225 | "host": "www.securityinnovation.com", 226 | 227 | /** 228 | * Indicates whether this Message has been deleted 229 | * @optional default=false 230 | * @type Bool 231 | */ 232 | "deleted": false, 233 | 234 | /** 235 | * Indicates whether this Message is enabled 236 | * @optional default=true 237 | * @type Bool 238 | */ 239 | "enabled": true, 240 | 241 | /** 242 | * Success Regex Field for Message 243 | * @optional default="" 244 | * @type Base64EncodedString 245 | */ 246 | "regexBase64": "XkhUVFAvMVwuMSAyMDAgT0s=", 247 | 248 | /** 249 | * Indicates whether this Message is using Failure Regex Mode 250 | * @optional default=false 251 | * @type Bool 252 | */ 253 | "failureRegexMode": false, 254 | 255 | /** 256 | * Internal field 257 | * Ignored during Load 258 | * @optional 259 | * @note This can be queried from Saved States to programatically retrieve the results of a run 260 | */ 261 | "runResultForRoleID": {}, 262 | 263 | /** 264 | * Internal field 265 | * Ignored during Load 266 | * @optional 267 | */ 268 | "runBase64ForUserID": {} 269 | } 270 | ], 271 | /** 272 | * Array containing 0 or more Chains 273 | * @optional: if omitted, the existing Chain Table is unchanged 274 | * @type Array 275 | */ 276 | "arrayOfChains": [ 277 | { 278 | /** 279 | * Internal ID of this Chain 280 | * @required 281 | * @condition All IDs for a given type must be unique 282 | * @condition IDs for a given type must start from 0 283 | * @condition All elements must be listed in order by index and increment by 1 284 | * @type int 285 | */ 286 | "index": 0, 287 | 288 | /** 289 | * Which row of the table this Chain is displayed in 290 | * @required 291 | * @condition All non-deleted rows for a given type must be unique 292 | * @condition Non-deleted row must be between 0 and the amount of non-deleted elements 293 | * @type int 294 | */ 295 | "tableRow": 0, 296 | 297 | /** 298 | * Friendly Name of the Chain 299 | * @optional default="" 300 | * @type String 301 | */ 302 | "name": "CSRF", 303 | 304 | /** 305 | * Indicates whether this Chain has been deleted 306 | * @optional default=false 307 | * @type Bool 308 | */ 309 | "deleted": false, 310 | 311 | /** 312 | * Indicates whether this Chain is enabled 313 | * @optional default=true 314 | * @type Bool 315 | */ 316 | "enabled": true, 317 | 318 | /** 319 | * Source of the Chain 320 | * Either the Message ID or the name of the chain prefixed with "SV_" 321 | * @optional default="" 322 | * @type String 323 | */ 324 | "fromID": "SV_CsrfToken", 325 | 326 | /** 327 | * Source Regex 328 | * @optional default="" 329 | * @type Base64EncodedString 330 | */ 331 | "fromRegexBase64": "PG5hbWU9ImNzcmZUb2tlbiIgdmFsdWU9IiguKj8pIiAvPg==", 332 | 333 | /** 334 | * Range of Message IDs that represent Destinations 335 | * @optional default="" 336 | * @type String 337 | */ 338 | "toID": "0-2", 339 | 340 | /** 341 | * Destination Regex 342 | * @optional default="" 343 | * @type Base64EncodedString 344 | */ 345 | "toRegexBase64": "Y3NyZlRva2VuPSguKj8pJg==", 346 | 347 | /** 348 | * User Values From field of Chain table 349 | * Either the index of the User, or -1 to indicate All Users 350 | * Optional default=-1 351 | * @type int 352 | */ 353 | "sourceUser": -1, 354 | 355 | /** 356 | * Ordered array of transformers applied to chain 357 | * @optional default=[] 358 | * @type ArrayOfStrings 359 | */ 360 | "transformers": ["base64","url"], 361 | 362 | /** 363 | * Internal field 364 | * Ignored during Load 365 | * @optional 366 | */ 367 | "fromEnd": "", 368 | 369 | /** 370 | * Internal field 371 | * Ignored during Load 372 | * @optional 373 | */ 374 | "fromStart": "", 375 | 376 | /** 377 | * Internal field 378 | * Ignored during Load 379 | * @optional 380 | */ 381 | "toStart": "", 382 | 383 | /** 384 | * Internal field 385 | * Ignored during Load 386 | * @optional 387 | */ 388 | "toEnd": "" 389 | } 390 | ], 391 | /** 392 | * Array containing 0 or more ChainSources 393 | * This is only loaded when an "arrayOfUsers" is also present 394 | * @optional: if omitted and "arrayOfUsers" is present, defaults to [] 395 | * @type Array 396 | */ 397 | "arrayOfChainSources": [ 398 | { 399 | /** 400 | * Name of the ChainSource 401 | * @required 402 | * @condition All ChainSource elements must have different names 403 | * @type String 404 | */ 405 | "name": "CsrfToken", 406 | 407 | /** 408 | * Map of keys and values, keys = User Index, value = Chain Source value 409 | * @required 410 | * @type map(str,str) 411 | */ 412 | "userValues": { 413 | "0": "asdf" 414 | } 415 | } 416 | ] 417 | } 418 | 419 | ## Partial States 420 | 421 | If any of the top-level arrays are missing, the associated tables will be unchanged during load. 422 | 423 | This is to enable users who want to update just one table of a configuration to do so with a minimized state file. 424 | 425 | ## Example: Update User Cookies 426 | 427 | { 428 | "version": "0.8", 429 | "arrayOfUsers": [ 430 | { 431 | "name": "user123", 432 | "index": 0, 433 | "tableRow": 0, 434 | "cookiesBase64": "c2Vzc2lvbl9pZD1hc2Rm", 435 | "headersBase64": [ 436 | "QXV0aG9yaXphdGlvbjogYXNkZg==" 437 | ], 438 | "roles": { 439 | "0": true, 440 | "1": false 441 | } 442 | }, 443 | { 444 | "name": "admin123", 445 | "index": 1, 446 | "tableRow": 1, 447 | "cookiesBase64": "c2Vzc2lvbl9pZD1hc2Rm", 448 | "headersBase64": [ 449 | "QXV0aG9yaXphdGlvbjogYXNkZg==" 450 | ], 451 | "roles": { 452 | "0": false, 453 | "1": true 454 | } 455 | } 456 | ] 457 | } 458 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 Mick Ayzenberg - Security Innovation 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AuthMatrix v0.8.2 2 | 3 | AuthMatrix is an extension to Burp Suite that provides a simple way to test authorization in web applications and web services. With AuthMatrix, testers focus on thoroughly defining tables of users, roles, and requests for their specific target application upfront. These tables are structured in a similar format to that of an access control matrix common in various threat modeling methodologies. 4 | 5 | Once the tables have been assembled, testers can use the simple click-to-run interface to kick off all combinations of roles and requests. The results can be confirmed with an easy to read, color-coded interface indicating any authorization vulnerabilities detected in the system. Additionally, the extension provides the ability to save and load target configurations for simple regression testing. 6 | 7 | # Installation 8 | 9 | AuthMatrix can be installed through the Burp Suite BApp Store. From within Burp Suite, select the Extender tab, select the BApp Store, select AuthMatrix, and click install. 10 | 11 | For Manual installation, download AuthMatrix.py from this repository. Then from within Burp Suite, select the Extender tab, click the Add button, change the Extension type to Python, and select the AuthMatrix python file. 12 | 13 | ### Note 14 | 15 | AuthMatrix requires configuring Burp Suite to use Jython. Easy instructions for this are located at the following URL. 16 | 17 | https://portswigger.net/burp/help/extender.html#options_pythonenv 18 | 19 | Be sure to use Jython version 2.7.0 or greater to ensure compatibility. 20 | 21 | # Basic Usage 22 | 23 | 1. Create roles for all privilege levels within the target application. (Common roles may include User, Admin, and Anonymous) 24 | 25 | 2. Create enough users to fit these various roles and select the checkboxes for all roles that the user belongs to. "Single-User" roles containing just the one user will be configured automatically to assist in cross-user resource testing. If these are not needed, feel free to delete these roles by right-clicking the column in the Request Table. 26 | 27 | 3. Generate session tokens for each user from the Repeater tab and enter them into the relevant column within the Users Table. Cookies can be sent directly to the users via the right click menu available in Repeater. AuthMatrix will intelligently parse the cookie string from the table and substitute/add them to the requests where applicable. 28 | 29 | * NOTE: The Cookies field is optional. If the target uses HTTP headers instead, these can be added by clicking the "New Header" button. 30 | 31 | * For more advanced configurations, including automated refreshing of credentials, see the "Chains for Authenticating Users" example below. 32 | 33 | 4. From another area of Burp Suite (i.e. Target tab, Repeater Tab, etc) right click a request and select "Send to AuthMatrix." 34 | 35 | 5. In the Request Table of AuthMatrix, select the checkboxes for all roles that are authorized to make each HTTP request. 36 | 37 | 6. Customize a Response Regex based on the expected response behavior of the request to determine if the action has succeeded. 38 | 39 | * Common regexes include HTTP Response headers, success messages within the body, or other variations within the body of the page. 40 | 41 | * NOTE: Requests can be configured to use a Failure Regex instead through the right-click menu (i.e. Authenticated users should never receive an HTTP 303) 42 | 43 | 44 | 7. Click Run at the bottom to run all requests or right click several requests and select run. Observe that the adjacent table will show color-coded results. 45 | 46 | * Green indicates no vulnerability detected 47 | 48 | * Red indicates the request may contain a vulnerability 49 | 50 | * Blue indicates that the result may be a false positive. (This generally means there is an invalid/expired session token or an incorrect regex) 51 | 52 | ## Sample AuthMatrix Configuration 53 | 54 | ![Sample AuthMatrix Configuration](images/img1.png) 55 | 56 | 57 | ## False Positives Detected (Invalid Session Tokens) 58 | 59 | ![Invalid Session Tokens](images/img2.png) 60 | 61 | # Advanced Usage 62 | 63 | ## Chains 64 | 65 | Chains provide a way to copy a static or dynamic value into the body of a request. These values can be pulled from the response of a previously run request (using a regex) or by specifing user-specific static string values. 66 | 67 | The most common use cases for Chains are: 68 | 69 | 1. Populating requests with valid CSRF Tokens 70 | 71 | 2. Testing newly created IDs/GUIDs for cross-user authorization issues 72 | 73 | 3. Automating authentication and session refreshing 74 | 75 | A Chain entry has the following values: 76 | 77 | * __Chain Name:__ a descriptive name 78 | 79 | * __Source:__ a static user string defined in the User Table or the ID of the source request in the Request table 80 | 81 | * __Extraction Regex:__ a regex used to extract a value from the response of the source request. This field is only used when a Request is specified in the previous field. If used, this must contain one parenthesis grouping that is to be extracted [i.e. (.*)] 82 | 83 | * __Destinations:__ a list of Request IDs that the source value will be replaced into. 84 | 85 | * __Replacement Regex:__ a regex used to determine where the source value is to be inserted. This must contain one parenthesis grouping to be replaced [i.e. (.*)] 86 | 87 | * __Use Values From:__ specify whether to use the source value obtained from one selected user (useful for cross-user resource tests) or to use the values from all users and place them into their corresponding user's destination requests (useful for automation tasks like CSRF token retrieval) 88 | 89 | __NOTE:__ Requests are run in order of row, however, if a chain dependency is detected, AuthMatrix will run the requests in the required order. 90 | 91 | 92 | ## Chains for CSRF 93 | 94 | ![Chain for CSRF](images/img4.png) 95 | 96 | ## Chains for Cross-User Resource Tests 97 | 98 | ![Chain Cross-User](images/img5.png) 99 | 100 | ## Chains for Authenticating Users 101 | 102 | ![Chain Authentication](images/img6.png) 103 | 104 | ## Failure Regex Mode 105 | 106 | For certain targets, it may be easier to configure AuthMatrix to detect the response condition of when a request has failed. For example, if a target site returns unique data on successful requests, but always returns an HTTP 303 when an unauthorized action is performed. 107 | 108 | In this mode, AuthMatrix will validate this regex for all users not part of a succeeding role. 109 | 110 | To do this, right click the request and select "Toggle Regex Mode". The regex field will be highlighted in purple to indicate that AuthMatrix will run the request in Failure Regex Mode. 111 | 112 | __NOTE:__ False positive detection and highlighting may not work in Failure Regex Mode 113 | 114 | ## Sample Configuration with Failure Regex Mode 115 | 116 | ![Sample Configuration with Failure Regex Mode](images/img3.png) 117 | 118 | ## JSON State File 119 | 120 | [Refer to the JsonState document for details regarding the structure of state files](JsonState.md) 121 | 122 | -------------------------------------------------------------------------------- /images/img1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SecurityInnovation/AuthMatrix/1b0fa45b548dc79e290ef74dd347da52dbacef91/images/img1.png -------------------------------------------------------------------------------- /images/img2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SecurityInnovation/AuthMatrix/1b0fa45b548dc79e290ef74dd347da52dbacef91/images/img2.png -------------------------------------------------------------------------------- /images/img3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SecurityInnovation/AuthMatrix/1b0fa45b548dc79e290ef74dd347da52dbacef91/images/img3.png -------------------------------------------------------------------------------- /images/img4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SecurityInnovation/AuthMatrix/1b0fa45b548dc79e290ef74dd347da52dbacef91/images/img4.png -------------------------------------------------------------------------------- /images/img5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SecurityInnovation/AuthMatrix/1b0fa45b548dc79e290ef74dd347da52dbacef91/images/img5.png -------------------------------------------------------------------------------- /images/img6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SecurityInnovation/AuthMatrix/1b0fa45b548dc79e290ef74dd347da52dbacef91/images/img6.png --------------------------------------------------------------------------------