skipped 10 lines 11 11 import tempfile 12 12 import traceback 13 13 import StringIO 14 + import re 14 15 import gzip 15 16 import array 16 17 import platform 18 + from urllib import unquote, quote_plus 17 19 18 20 # Patch dir this file was loaded from into the path 19 21 # (Burp doesn't do it automatically) skipped 3 lines 23 25 from burp import IBurpExtender, IMessageEditorTab, IMessageEditorTabFactory, ITab, \ 24 26 IExtensionStateListener 25 27 26 - from google.protobuf.reflection import ParseMessage as parse_message 28 + # Deprecated, replaced by an implementation based on message_factory 29 + #from google.protobuf.reflection import ParseMessage as parse_message 30 + 27 31 from google.protobuf.text_format import Merge as merge_message 32 + from google.protobuf import message_factory 28 33 29 34 from java.awt.event import ActionListener, MouseAdapter 30 35 from java.lang import Boolean, RuntimeException 31 - from java.io import FileFilter 36 + from java.io import FileFilter, File 32 37 from javax.swing import JButton, JFileChooser, JMenu, JMenuItem, JOptionPane, JPanel, JPopupMenu 33 38 from javax.swing.filechooser import FileNameExtensionFilter 34 39 from java.lang import System 35 40 36 41 from ui import ParameterProcessingRulesTable 37 - 42 + from ui import decode_url_and_base64, encode_base64_and_url 38 43 39 44 CONTENT_PROTOBUF = ['application/protobuf', 'application/x-protobuf', 'application/x-protobuffer', 'application/x-protobuffer; charset=utf-8', 'application/octet-stream', 'application/grpc-web+proto'] 40 45 41 46 PROTO_FILENAME_EXTENSION_FILTER = FileNameExtensionFilter("*.proto, *.py", 42 47 ["proto", "py"]) 43 48 CONTENT_GZIP = ('gzip') 49 + 50 + PYTHON2_BINARY = 'python2' 44 51 45 52 def detectProtocBinaryLocation(): 46 53 system = System.getProperty('os.name') skipped 134 lines 181 188 class ProtobufEditorTab(IMessageEditorTab): 182 189 TAB_CAPTION = "Protobuf Decoder" 183 190 191 + 184 192 def __init__(self, extender, controller, editable): 185 193 self.extender = extender 186 194 self.callbacks = extender.callbacks skipped 11 lines 198 206 self.editor = extender.callbacks.createTextEditor() 199 207 self.editor.setEditable(editable) 200 208 209 + self.filter_search = None 210 + 201 211 mouseListener = LoadProtoMenuMouseListener(self) 202 212 self.getUiComponent().addMouseListener(mouseListener) 213 + 214 + self.last_proto = None 203 215 204 216 def getTabCaption(self): 205 217 return self.TAB_CAPTION skipped 6 lines 212 224 return False 213 225 214 226 if isRequest: 227 + 228 + # Necessary sometimes when content-type is not set 229 + #return True 230 + 215 231 info = self.helpers.analyzeRequest(content) 216 232 217 233 # check if request contains a specific parameter skipped 3 lines 221 237 return True 222 238 223 239 headers = info.getHeaders() 240 + 224 241 else: 242 + 243 + # Necessary sometimes when content-type is not set 244 + #return True 245 + 225 246 headers = self.helpers.analyzeResponse(content).getHeaders() 226 247 227 248 # first header is the request/response line skipped 54 lines 282 303 283 304 break 284 305 285 - #set message 286 - rawBytes = (content[info.getBodyOffset():]) 287 - global oldPadding 288 - global hasPadding 289 - hasPadding = False 306 + if parameter is None: 307 + #set message 308 + rawBytes = (content[info.getBodyOffset():]) 309 + global oldPadding 310 + global hasPadding 311 + hasPadding = False 312 + 313 + oldPadding= rawBytes[0:4] 314 + if rawBytes[0] == 0 and rawBytes[1] == 0 and rawBytes[2] == 0 and rawBytes[3] == 0: 315 + rawBytes = rawBytes[5:rawBytes[4]+5] 316 + hasPadding = True 317 + body = rawBytes.tostring() 318 + 319 + 320 + # If we already selected a proto for this specific tab, continue to use that very proto 321 + 322 + if(self.last_proto is not None): 323 + 324 + factory = message_factory.MessageFactory() 325 + klass = factory.GetPrototype(self.descriptor) 326 + klass_instance = klass() 327 + klass_instance.ParseFromString(body) 290 328 291 - oldPadding= rawBytes[0:4] 292 - if rawBytes[0] == 0 and rawBytes[1] == 0 and rawBytes[2] == 0 and rawBytes[3] == 0: 293 - rawBytes = rawBytes[5:rawBytes[4]+5] 294 - hasPadding = True 295 - body = rawBytes.tostring() 329 + message = klass_instance 296 330 297 - # Loop through all proto descriptors loaded 331 + self.tab.editor.setText(str(klass_instance)) 332 + self.tab.editor.setEditable(True) 333 + self.tab._current = (content, message, info, parameter) 334 + return 335 + 336 + # 1 - Loop through all proto descriptors loaded and use the first that matches 337 + ''' 298 338 for package, descriptors in self.descriptors.iteritems(): 299 339 for name, descriptor in descriptors.iteritems(): 300 340 try: skipped 22 lines 323 363 self.editor.setEditable(True) 324 364 self._current = (content, message, info, parameter) 325 365 return 366 + ''' 326 367 327 - # If we get to this point, then no loaded protos could deserialize 328 - # the message. Shelling out to protoc should be a last resort. 368 + # 2 - This implementation prints the results of all the protos that matches 369 + ''' 370 + content_pane = "" 371 + 372 + # Loop through all proto descriptors loaded 373 + for package, descriptors in self.descriptors.iteritems(): 374 + for name, descriptor in descriptors.iteritems(): 375 + try: 376 + print "Parsing message with proto descriptor %s (auto)." % (name) 377 + message = parse_message(descriptor.Request, body) 378 + except Exception: 379 + print "(exception parsing message... - continue)" 380 + continue 381 + 382 + # Stop parsing on the first valid message we encounter 383 + # this may result in a false positive, so we should still 384 + # allow users to specify a proto manually (select from a 385 + # context menu). 386 + 387 + if message.IsInitialized(): 388 + # The message is initialized if all of its 389 + # required fields are set. 390 + #print "Message: [%s]" % (message) 391 + 392 + if str(message) == "": 393 + # parse_message() returned an empty message, but no 394 + # error or exception: continue to the next proto descriptor 395 + print "(message is empty, trying other proto descriptors...)" 396 + else: 329 397 398 + content_pane = content_pane + "***** " + str(name) + " - " + str(package) + "\n" 399 + content_pane = content_pane + str(message) + "\n" 400 + 401 + if(content_pane != ""): 402 + self.editor.setText(str(content_pane)) 403 + self.editor.setEditable(False) 404 + self._current = (content, content_pane, info, parameter) 405 + return 406 + ''' 407 + 408 + # 3 - This implementation (the one that I prefer) decodes without protos with protoc if no proto is selected 330 409 process = subprocess.Popen([PROTOC_BINARY_LOCATION, '--decode_raw'], 331 410 stdin=subprocess.PIPE, 332 411 stdout=subprocess.PIPE, skipped 31 lines 364 443 365 444 try: 366 445 merge_message(self.editor.getText().tostring(), message) 446 + 367 447 headers = info.getHeaders() 368 448 serialized = message.SerializeToString() 369 449 370 - if hasPadding: 450 + if parameter is None and hasPadding: 371 451 oldPadding.append(len(serialized)) 372 452 serialized = oldPadding.tostring() + serialized 373 453 skipped 12 lines 386 466 387 467 except Exception as error: 388 468 JOptionPane.showMessageDialog(self.getUiComponent(), 389 - error.message, 'Error parsing message!', 469 + error.message + str ( traceback . format_exc ( ) ) , 'Error parsing message!', 390 470 JOptionPane.ERROR_MESSAGE) 391 471 392 472 # an error occurred while re-serializing the message, skipped 21 lines 414 494 def mouseReleased(self, event): 415 495 return self.handleMouseEvent(event) 416 496 497 + # Recursive method necessary to add also subtimes (a message can have inside other message definitions...) 498 + def populate_menu_recursive(self, descriptors, fatherMenu, father_match): 499 + 500 + iter_found = False 501 + 502 + single_father_match = father_match 503 + total_father_match = father_match 504 + 505 + # if self.tab.filter_search is None: 506 + 507 + for name, descriptor in descriptors.iteritems(): 508 + 509 + current_nested_dict = descriptor.nested_types_by_name 510 + 511 + if self.tab.filter_search is not None: 512 + single_father_match = father_match or re.search(self.tab.filter_search, name, re.IGNORECASE) 513 + total_father_match = total_father_match or re.search(self.tab.filter_search, name, re.IGNORECASE) 514 + 515 + if(len(current_nested_dict) > 0): 516 + 517 + protoMenu = JMenu(name) 518 + 519 + # The father is the messsage that encloses other messages 520 + enclosureOBject = JMenuItem("* Father") 521 + enclosureOBject.addActionListener(DeserializeProtoActionListener(self.tab, descriptor)) 522 + protoMenu.add(enclosureOBject) 523 + 524 + if self.populate_menu_recursive(current_nested_dict, protoMenu, single_father_match) or self.tab.filter_search is None: 525 + fatherMenu.add(protoMenu) 526 + iter_found = True 527 + 528 + else: 529 + 530 + if self.tab.filter_search is None or re.search(self.tab.filter_search, name, re.IGNORECASE) or single_father_match: 531 + 532 + protoMenu = JMenuItem(name) 533 + protoMenu.addActionListener(DeserializeProtoActionListener(self.tab, descriptor)) 534 + fatherMenu.add(protoMenu) 535 + 536 + iter_found = True 537 + 538 + return iter_found or total_father_match 539 + 540 + 417 541 def handleMouseEvent(self, event): 418 542 if event.isPopupTrigger(): 419 543 loadMenu = JMenuItem("Load .proto") skipped 1 lines 421 545 422 546 popup = JPopupMenu() 423 547 popup.add(loadMenu) 548 + 549 + filterMenu = JMenuItem("Filter .proto") 550 + filterMenu.addActionListener(SearchProtoActionListener(self.tab, event.getComponent())) 551 + popup.add(filterMenu) 424 552 425 553 if self.tab.descriptors: 426 554 skipped 1 lines 428 556 429 557 popup.addSeparator() 430 558 popup.add(deserializeAsMenu) 559 + 560 + # Raw deserialize using protoc without proto (it cannot be serialized if modified) 561 + rawMenu = JMenuItem("Raw") 562 + deserializeAsMenu.add(rawMenu) 563 + rawMenu.addActionListener(DeserializeProtoActionListener(self.tab, "raw")) 431 564 432 565 for pb2, descriptors in self.tab.descriptors.iteritems(): 433 - subMenu = JMenu(pb2) 434 - deserializeAsMenu.add(subMenu) 435 566 436 - for name, descriptor in descriptors.iteritems(): 437 - protoMenu = JMenuItem(name) 438 - protoMenu.addActionListener( 439 - DeserializeProtoActionListener(self.tab, descriptor)) 567 + subMenu = JMenu(pb2) 440 568 441 - subMenu.add(protoMenu) 569 + if self.populate_menu_recursive(descriptors, subMenu, self.tab.filter_search is None): 570 + deserializeAsMenu.add(subMenu) 442 571 443 572 popup.show(event.getComponent(), event.getX(), event.getY()) 444 573 skipped 24 lines 469 598 self.updateDescriptors(name, module_) 470 599 471 600 return 601 + 602 + # Method created to handle the situation in which a proto depends on another proto. In this situation the dependencies are imported recursively. 603 + def importProtoFileRecusive(self, proto): 604 + 605 + try: 606 + module = compile_and_import_proto(proto) 607 + if module: 608 + return module 609 + 610 + except (Exception, RuntimeException) as error: 611 + 612 + missing_module_regex = re.search('No module named (.*)_pb2', str(error)) 613 + compilation_regex = re.search('.*Module or method too large in `(.*)`.*', str(error)) # 614 + 615 + if(missing_module_regex): 616 + missing_module = missing_module_regex.group(1) + ".proto" 617 + self.tab.callbacks.getStdout().write('*** %s depends on %s. Trying to import it...!\n' % (proto, missing_module, )) 618 + if self.importProtoFileRecusive(File(proto.getParent(),missing_module)): 619 + return self.importProtoFileRecusive(proto) 620 + else: 621 + self.tab.callbacks.getStderr().write('*** ERROR, infinite recursion with proto %s!\n' % (str(proto.getAbsolutePath()), )) 622 + 623 + # This compile with python pb2 too large 624 + elif(compilation_regex): 625 + subprocess.check_call([PYTHON2_BINARY, '-m', 'py_compile', compilation_regex.group(1)]) 626 + return self.importProtoFileRecusive(proto) 627 + 628 + else: 629 + self.tab.callbacks.getStderr().write('*** ERROR in recursive import: %s!\n' % (str(error), )) 630 + tb = traceback.format_exc() 631 + self.tab.callbacks.getStderr().write('Traceback: %s!\n' % (str(tb), )) 632 + return None 472 633 473 634 def importProtoFiles(self, selectedFiles): 474 635 for selectedFile in selectedFiles: skipped 2 lines 477 638 self.importProtoFiles(selectedFile.listFiles(ListProtoFileFilter())) 478 639 else: 479 640 self.chooser.setCurrentDirectory(selectedFile.getParentFile()) 480 - 481 - try: 482 - module = compile_and_import_proto(selectedFile) 483 - if module: 484 - yield module 641 + yield self.importProtoFileRecusive(selectedFile) 485 642 486 - except (Exception, RuntimeException) as error: 487 - self.tab.callbacks.getStderr().write( 488 - 'Error importing proto %s!\n' % (selectedFile, )) 489 - 490 - traceback.print_exc(file=self.tab.callbacks.getStderr()) 491 - 492 - JOptionPane.showMessageDialog(None, 493 - '%s: %s' % (error.message, selectedFile), 494 - 'Error importing proto!', JOptionPane.ERROR_MESSAGE) 495 643 496 644 def actionPerformed(self, event): 497 645 if self.chooser.showOpenDialog(None) == JFileChooser.APPROVE_OPTION: skipped 3 lines 501 649 return 502 650 503 651 652 + # The purpose is being able to search for protos, if we have tons of proto 653 + class SearchProtoActionListener(ActionListener): 654 + def __init__(self, tab, component): 655 + self.tab = tab 656 + self.component = component 657 + 658 + def actionPerformed(self, event): 659 + self.tab.filter_search = JOptionPane.showInputDialog(self.component, "Search: ", "Search", 1); 660 + 661 + 504 662 class DeserializeProtoActionListener(ActionListener): 505 663 def __init__(self, tab, descriptor): 506 664 self.tab = tab skipped 11 lines 518 676 body = gUnzip(content[info.getBodyOffset():].tostring()) 519 677 else: 520 678 body = content[info.getBodyOffset():].tostring() 521 - 679 + #body = content[info.getBodyOffset():] 522 680 523 681 if parameter is not None: 524 682 param = self.tab.helpers.getRequestParameter( skipped 6 lines 531 689 for rule in rules.get('before', []): 532 690 body = rule(body) 533 691 692 + if parameter is None: 534 693 694 + # cut 5 bytes for grpc web 695 + rawBytes = (content[info.getBodyOffset():]) 696 + global oldPadding 697 + global hasPadding 698 + hasPadding = False 535 699 536 - # cut 5 bytes for grpc web 537 - rawBytes = (content[info.getBodyOffset():]) 538 - global oldPadding 539 - global hasPadding 540 - hasPadding = False 700 + oldPadding= rawBytes[0:4] 701 + if rawBytes[0] == 0 and rawBytes[1] == 0 and rawBytes[2] == 0 and rawBytes[3] == 0: 702 + rawBytes = rawBytes[5:] 703 + hasPadding = True 541 704 542 - oldPadding= rawBytes[0:4] 543 - if rawBytes[0] == 0 and rawBytes[1] == 0 and rawBytes[2] == 0 and rawBytes[3] == 0: 544 - rawBytes = rawBytes[5:] 545 - hasPadding = True 705 + body = rawBytes.tostring() 706 + #body = rawBytes 546 707 547 - body = rawBytes.tostring() 708 + if self.descriptor != "raw": 548 709 710 + print "Parsing message with proto descriptor %s (by user)." % (self.descriptor.name) 549 711 550 - print "Parsing message with proto descriptor %s (by user)." % (self.descriptor.name) 551 - message = parse_message(self.descriptor, body) 712 + # Deprecated method 713 + # message = parse_message(self.descriptor, body) 552 714 553 - self.tab.editor.setText(str(message)) 554 - self.tab.editor.setEditable(True) 555 - self.tab._current = (content, message, info, parameter) 715 + factory = message_factory.MessageFactory() 716 + klass = factory.GetPrototype(self.descriptor) 717 + klass_instance = klass() 718 + klass_instance.ParseFromString(body) 719 + 720 + message = klass_instance 721 + 722 + self.tab.editor.setText(str(klass_instance)) 723 + self.tab.editor.setEditable(True) 724 + self.tab._current = (content, message, info, parameter) 725 + self.tab.last_proto = self.descriptor 726 + 727 + else: 728 + 729 + print "Parsing message without any proto" 730 + 731 + process = subprocess.Popen([PROTOC_BINARY_LOCATION, '--decode_raw'], 732 + stdin=subprocess.PIPE, 733 + stdout=subprocess.PIPE, 734 + stderr=subprocess.PIPE) 735 + 736 + 737 + output = error = None 738 + try: 739 + output, error = process.communicate(body) 740 + except OSError: 741 + pass 742 + finally: 743 + if process.poll() != 0: 744 + process.wait() 745 + 746 + if error: 747 + #print "protoc displaying message - error..." 748 + self.tab.editor.setText(error) 749 + else: 750 + #print "protoc displaying message - output..." 751 + self.tab.editor.setText(output) 752 + 753 + self.tab.editor.setEditable(False) 754 + self.tab._current = (content, None, info, parameter) 556 755 557 756 except Exception as error: 558 - title = "Error parsing message as %s!" % (self.descriptor.name, ) 757 + 758 + if self.descriptor != "raw": 759 + title = "Error parsing message as %s!" % (self.descriptor.name, ) 760 + else: 761 + title = "Error parsing message as without any proto" 559 762 JOptionPane.showMessageDialog(self.tab.getUiComponent(), 560 - error.message, title, JOptionPane.ERROR_MESSAGE) 763 + error.message + str ( traceback . format_exc ( ) ) , title, JOptionPane.ERROR_MESSAGE) 764 + 765 + #print(str(error)) 766 + #print(str(error.message)) 767 + #print(str(traceback.format_exc())) 561 768 562 769 return 563 770 skipped 2 lines 566 773 curdir = os.path.abspath(os.curdir) 567 774 tempdir = tempfile.mkdtemp() 568 775 776 + print(str(tempdir)) 777 + 569 778 is_proto = os.path.splitext(proto.getName())[-1] == '.proto' 570 779 571 780 if is_proto: skipped 3 lines 575 784 tempdir, proto.getName()]) 576 785 module = proto.getName().replace('.proto', '_pb2') 577 786 578 - except subprocess.CalledProcessError: 787 + except subprocess.CalledProcessError as e : 788 + print("*** ERROR COMPILING") 789 + print(e) 579 790 shutil.rmtree(tempdir) 580 791 return None 581 792 skipped 10 lines 592 803 os.chdir(proto.getParent()) 593 804 594 805 sys.path.append(os.path.abspath(os.curdir)) 806 + 807 + # Added compilation in order to avoid error with big proto files 808 + subprocess.check_call([PYTHON2_BINARY, '-m', 'py_compile', str(module) + ".py"]) 809 + 595 810 return importlib.import_module(module) 811 + 812 + except subprocess.CalledProcessError as e: 813 + #except Exception as e: 814 + print("*** ERROR COMPILING") 815 + print(e) 816 + print(str(traceback.format_exc())) 596 817 597 818 finally: 598 819 sys.path.pop() skipped 4 lines