├── thresholds.txt ├── dangerous_headers.txt ├── error1.png ├── gear_2.png ├── UI_theme_dark.txt ├── UI_theme_light.txt ├── template ├── template.docx ├── template.py └── info.txt ├── cookie_flags.txt ├── output └── .gitignore ├── __pycache__ ├── docx.cpython-38.pyc ├── docx.cpython-39.pyc └── template.cpython-39.pyc ├── potentially_dangerous_headers.txt ├── config.txt ├── security_headers.txt ├── LICENSE ├── .github └── workflows │ └── codeql.yml ├── request_headers.txt ├── response_headers.txt ├── README.md └── headers_window.py /thresholds.txt: -------------------------------------------------------------------------------- 1 | 1 3 2 | 0 3 3 | 0 2 -------------------------------------------------------------------------------- /dangerous_headers.txt: -------------------------------------------------------------------------------- 1 | 1 x-powered-by 2 | 1 server -------------------------------------------------------------------------------- /error1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antlarac/Headers/HEAD/error1.png -------------------------------------------------------------------------------- /gear_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antlarac/Headers/HEAD/gear_2.png -------------------------------------------------------------------------------- /UI_theme_dark.txt: -------------------------------------------------------------------------------- 1 | #F07338 2 | #00FF00 3 | #FF0000 4 | #4FC3F7 5 | #707070 6 | #FFC745 -------------------------------------------------------------------------------- /UI_theme_light.txt: -------------------------------------------------------------------------------- 1 | #5B4FFF 2 | #00FF00 3 | #FF0000 4 | #4FC3F7 5 | #a0a0a0 6 | #447EAA -------------------------------------------------------------------------------- /template/template.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antlarac/Headers/HEAD/template/template.docx -------------------------------------------------------------------------------- /cookie_flags.txt: -------------------------------------------------------------------------------- 1 | 1 httponly 2 | 1 secure 3 | 0 samesite 4 | 0 expire 5 | 0 max-age 6 | 0 path 7 | 0 domain -------------------------------------------------------------------------------- /output/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /__pycache__/docx.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antlarac/Headers/HEAD/__pycache__/docx.cpython-38.pyc -------------------------------------------------------------------------------- /__pycache__/docx.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antlarac/Headers/HEAD/__pycache__/docx.cpython-39.pyc -------------------------------------------------------------------------------- /potentially_dangerous_headers.txt: -------------------------------------------------------------------------------- 1 | 1 access-control-allow-origin 2 | 1 access-control-allow-credentials 3 | 0 set-cookie -------------------------------------------------------------------------------- /__pycache__/template.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antlarac/Headers/HEAD/__pycache__/template.cpython-39.pyc -------------------------------------------------------------------------------- /config.txt: -------------------------------------------------------------------------------- 1 | UI_theme -- dark 2 | last_save_type -- JSON: Header -> Host 3 | last_filter_type -- Request + Response + 4 | last_output_file -- Enter your output file path 5 | -------------------------------------------------------------------------------- /security_headers.txt: -------------------------------------------------------------------------------- 1 | 1 Cache-Control 2 | 1 Content-Security-Policy 3 | 1 Strict-Transport-Security 4 | 0 Public-Key-Pins 5 | 1 X-XSS-Protection 6 | 1 X-Frame-Options 7 | 1 X-Content-Type-Options 8 | 0 Feature-Policy 9 | 1 Referrer-Policy 10 | 0 Permissions-Policy -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 dh0ck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '20 13 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Use only 'java' to analyze code written in Java, Kotlin or both 38 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 39 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v3 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v2 48 | with: 49 | languages: ${{ matrix.language }} 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | 54 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 55 | # queries: security-extended,security-and-quality 56 | 57 | 58 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 59 | # If this step fails, then you should remove it and run the build manually (see below) 60 | - name: Autobuild 61 | uses: github/codeql-action/autobuild@v2 62 | 63 | # ℹ️ Command-line programs to run using the OS shell. 64 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 65 | 66 | # If the Autobuild fails above, remove it and uncomment the following three lines. 67 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 68 | 69 | # - run: | 70 | # echo "Run, Build Application using script" 71 | # ./location_of_script_within_repo/buildscript.sh 72 | 73 | - name: Perform CodeQL Analysis 74 | uses: github/codeql-action/analyze@v2 75 | with: 76 | category: "/language:${{matrix.language}}" 77 | -------------------------------------------------------------------------------- /template/template.py: -------------------------------------------------------------------------------- 1 | from docx.oxml.ns import nsdecls 2 | from docx.oxml import parse_xml 3 | import os, sys 4 | from docxtpl import DocxTemplate 5 | from docxtpl import InlineImage 6 | from docx.shared import Mm 7 | 8 | f = open('info.txt') 9 | infos = f.readlines() 10 | f.close() 11 | descriptions = {} 12 | solutions = [] 13 | # info.txt file cannot contain empty lines!!! all lines must be of the format: 14 | # header ::: description --- solution 15 | for info in infos: 16 | header = info.split(' ::: ')[0] 17 | contents = info.split(' ::: ')[1] 18 | descriptions[header] = {} 19 | descriptions[header]["description"] = contents.split('---')[0] 20 | descriptions[header]["solution"] = contents.split('---')[1] 21 | 22 | #breakpoint() 23 | vulns = [ 24 | ('Missing HTTP-Strict-Transport-Security header','Missing Security Header','MEDIUM','6.5','CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N'), 25 | ('Missing Cache-Control header','Missing Security Header', 'LOW','3.1','CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N'), 26 | ('Missing X-Frame-Options header','Missing Security Header','LOW','3.1','CVSS:3.0/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:L/A:N'), 27 | ('Missing Content-Security-Policy header','Missing Security Header','LOW','3.1','CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N'), 28 | ('Missing X-Content-Type-Options header','Missing Security Header','LOW','3.1','CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N'), 29 | ('Missing X-XSS-Protection header','Missing Security Header','MEDIUM','5.4','CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N'), 30 | ('Missing Referrer-Policy header','Missing Security Header','MEDIUM','3.1','CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N'), 31 | ('Missing Cookie Secure flag','Cookies without flags','LOW','3.1','CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N'), 32 | ('Missing Cookie HttpOnly flag','Cookies without flags','LOW','3.1','CVSS:3.0/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N'), 33 | ('Dangerous Server header','Dangerous header','LOW','3.1','CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N'), 34 | ('Dangerous X-Powered-By header','Dangerous header','LOW','3.1','CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N'), 35 | ('Potentially Dangerous Access-Control-Allow-Origin header','Potentially Dangerous Header','LOW','3.1','CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N'), 36 | ('Potentially Dangerous Access-Control-Allow-Credentials header','Potentially Dangerous Header','LOW','3.1','CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N') 37 | ] 38 | 39 | 40 | cvss_images={'3.1':'3,1.png','6.5':'6,5.png','0':'0.png','5.4':'5,4.png'} 41 | 42 | def get_issues(): 43 | """ Generates output dictionary form output file generated by burp extension 44 | in the order Host > Issue > Issue details > URL""" 45 | dic = {'Host':{}} 46 | def fill_dic(title, variable): 47 | if title not in dic['Host'][host]['Issue'].keys(): 48 | dic['Host'][host]['Issue'][issue] = {title:{variable:[url]}} 49 | else: 50 | if variable not in dic['Host'][host]['Issue'][issue][title].keys(): 51 | dic['Host'][host]['Issue'][issue][title][variable] = [] 52 | dic['Host'][host]['Issue'][issue][title][variable].append(url) 53 | 54 | f = open('../output/selected_output.txt','r') 55 | for line in f.readlines(): 56 | issue = line.split(';')[0].split(': ')[1] 57 | host = line.split(';')[1].split(': ')[1] 58 | url = line.split(';')[2].split('- URL: ')[1].split('- Port')[0] 59 | port = line.split(';')[2].split('- Port: ')[1].rstrip('\n') 60 | 61 | host = host + ' [' + port + ']' 62 | 63 | if host not in dic['Host'].keys(): 64 | dic['Host'][host] = {'Issue':{}} 65 | 66 | if issue == "Missing Security Header": 67 | """for missing securit headers use the structure of the dictionary: 68 | host -> issue -> missing header -> url""" 69 | missing_header = line.split(';')[2].split('Missing "')[1].split('" header')[0] 70 | fill_dic("Missing Security Header", missing_header) 71 | 72 | if issue == "Dangerous header": 73 | dangerous_header = line.split(';')[2].split('" header')[0].split('Detail: ')[1].lstrip('"') 74 | fill_dic("Dangerous header", dangerous_header) 75 | 76 | if issue == "Potentially Dangerous Header": 77 | potentially_dangerous_header = line.split(';')[2].split('" header')[0].split('Detail: ')[1].lstrip('"') 78 | fill_dic("Potentially Dangerous Header", potentially_dangerous_header) 79 | 80 | if issue == "Cookies without flags": 81 | missing_flag = line.split('Missing "')[1].split('" flag -')[0] 82 | fill_dic("Cookies without flags", missing_flag) 83 | 84 | f.close() 85 | return dic 86 | 87 | fill_dic = get_issues() 88 | doc = DocxTemplate("template.docx") 89 | 90 | def build_item(IP,host,port,vuln,cvss,urls): 91 | dic = { 92 | "Name":vuln[0], 93 | "Port":port, 94 | "Protocol":"TCP", 95 | "Description":descriptions[vuln[0]]["description"], 96 | "Severity":vuln[2], 97 | "Code":'', 98 | "Host":host, 99 | "IP":IP, 100 | "State":"OPEN", 101 | "Solution":descriptions[vuln[0]]["solution"], 102 | "CVSS":vuln[3], 103 | "CVSS_image":InlineImage(doc, cvss_images[vuln[3]], width=Mm(160)), 104 | "URLs":urls 105 | } 106 | return dic 107 | 108 | headers = [] 109 | headers1 = [] 110 | for host in fill_dic['Host'].keys(): 111 | for issue in fill_dic['Host'][host]['Issue'].keys(): 112 | for detail in fill_dic['Host'][host]['Issue'][issue][issue].keys(): 113 | urls = fill_dic['Host'][host]['Issue'][issue][issue][detail] 114 | IP = '-' 115 | # loop to retrieve the appropriate description and solution of a certain issue type 116 | for vuln in vulns: 117 | if detail.lower() in vuln[0].lower(): 118 | break 119 | cvss = vuln[3] 120 | if IP != '-': 121 | IP = ' - (' + IP + ')' 122 | else: 123 | IP = '' 124 | to_append = build_item(IP,host.split(' [')[0],host.split(' [')[1].split(']')[0],vuln,cvss,urls) 125 | headers.append(to_append) 126 | headers1.append({"headers":to_append})#, "urls":urls}) 127 | 128 | '''las access-control-allow-origin mirar si las ponian en el informe anterior 129 | si no estaban. si no las ponen, quitarlas de aqui, solo habria que ponerla 130 | si esta mal configurada, no si no esta (en principio), puedo crearla pero poniendo 131 | un aviso de revisarla a mano''' 132 | 133 | context1 = { 134 | "headers" : headers1 135 | } 136 | doc.render(context1) 137 | # Add colors to severity cell 138 | colors = {"CRITICAL":"C857C9","HIGH":"FF0000","MEDIUM":"ffff00","LOW":"00B050"} 139 | for table in doc.tables: 140 | severity = table.cell(1,3).text 141 | shading_elm_1 = parse_xml(r''.format(nsdecls('w'),colors[severity])) 142 | 143 | 144 | table.rows[1].cells[3]._tc.get_or_add_tcPr().append(shading_elm_1) 145 | 146 | if len(sys.argv) != 2: 147 | filename = "output.docx" 148 | else: 149 | filename = sys.argv[1] 150 | try: 151 | os.remove(filename) 152 | except: 153 | pass 154 | doc.save(filename) 155 | 156 | -------------------------------------------------------------------------------- /template/info.txt: -------------------------------------------------------------------------------- 1 | Missing HTTP-Strict-Transport-Security header ::: The HTTP protocol by itself is clear text, meaning that any data that is transmitted via HTTP can be captured and the contents viewed. To keep data private and prevent it from being intercepted, HTTP is often tunnelled through either Secure Sockets Layer (SSL) or Transport Layer Security (TLS). When either of these encryption standards are used, it is referred to as HTTPS.HTTP Strict Transport Security (HSTS) is an optional response header that can be configured on the server to instruct the browser to only communicate via HTTPS. This will be enforced by the browser even if the user requests a HTTP resource on the same server.Cyber-criminals will often attempt to compromise sensitive information passed from the client to the server using HTTP. This can be conducted via various Man-in-The-Middle (MiTM) attacks or through network packet captures.---Depending on the framework being used the implementation methods will vary, however it is advised that the `Strict-Transport-Security` header be configured on the server.One of the options for this header is `max-age`, which is a representation (in milliseconds) determining the time in which the client's browser will adhere to the header policy.Depending on the environment and the application this time period could be from as low as minutes to as long as days. 2 | Missing Cache-Control header ::: The HTTP 'Cache-Control' header is used to specify directives for caching mechanisms.The server did not return or returned an invalid 'Cache-Control' header which means page containing sensitive information (password, credit card, personal data, social security number, etc) could be stored on client side disk and then be exposed to unauthorised persons.---Configure your web server to include a 'Cache-Control' header with appropriate directives. If page contains sensitive information 'Cache-Control' value should be 'no-store' and 'Pragma' header value should be 'no-cache'. 3 | Missing X-Frame-Options header ::: Clickjacking (User Interface redress attack, UI redress attack, UI redressing) is a malicious technique of tricking a Web user into clicking on something different from what the user perceives they are clicking on, thus potentially revealing confidential information or taking control of their computer while clicking on seemingly innocuous web pages.The server didn't return an `X-Frame-Options` header which means that this website could be at risk of a clickjacking attack.The `X-Frame-Options` HTTP response header can be used to indicate whether or not a browser should be allowed to render a page inside a frame or iframe. Sites can use this to avoid clickjacking attacks, by ensuring that their content is not embedded into other sites.---Configure your web server to include an `X-Frame-Options` header. 4 | Missing Content-Security-Policy header ::: Content Security Policy (CSP) is a web security standard that helps to mitigate attacks like cross-site scripting (XSS), clickjacking or mixed content issues. CSP provides mechanisms to websites to restrict content that browsers will be allowed to load.---Configure Content Security Policy on your website by adding 'Content-Security-Policy' HTTP header or meta tag http-equiv='Content-Security-Policy'. 5 | Missing X-Content-Type-Options header ::: The HTTP 'X-Content-Type-Options' response header prevents the browser from MIME-sniffing a response away from the declared content-type.The server did not return a correct 'X-Content-Type-Options' header, which means that this website could be at risk of a Cross-Site Scripting (XSS) attack.---Configure your web server to include an 'X-Content-Type-Options' header with a value of 'nosniff'. 6 | Missing X-XSS-Protection header ::: The HTTP 'X-XSS-Protection' response header is a feature of modern browsers that allows websites to control their XSS auditors.The server is not configured to return a 'X-XSS-Protection' header which means that any pages on this website could be at risk of a Cross-Site Scripting (XSS) attack. This URL is flagged as a specific example.If legacy browsers support is not needed, it is recommended to use Content-Security-Policy without allowing unsafe-inline scripts instead.---Configure your web server to include an 'X-XSS-Protection' header with a value of '1; mode=block' on all pages.:web 7 | Missing Referrer-Policy header ::: Referrer Policy provides mechanisms to websites to restrict referrer information (sent in the referer header) that browsers will be allowed to add.No Referrer Policy header or metatag configuration has been detected.---Configure Referrer Policy on your website by adding Referrer-Policy HTTP header or meta tag referrer in HTML. 8 | Missing Cookie Secure flag ::: When the Secure flag is set on a cookie, the browser will prevent it from being sent over a clear text channel (HTTP) and only allow it to be sent when an encrypted channel is used (HTTPS)It was detected that a cookie is being set without the Secure flag. Note that if the cookie does not contain sensitive information, the risk of this vulnerability is mitigated.---If the cookie contains sensitive information, then the server should ensure that the cookie has the `secure` flag set. 9 | Missing Cookie HttpOnly flag ::: The HttpOnly flag assists in the prevention of client side-scripts (such as JavaScript) from accessing and using the cookie.This can help prevent XSS attacks from targeting the cookies holding the client's session token (setting the HttpOnly flag does not prevent, nor safeguard against XSS vulnerabilities themselves).---The initial step to remedy this would be to determine whether any client-side scripts (such as JavaScript) need to access the cookie and if not, set the HttpOnly flag.It should be noted that some older browsers are not compatible with the HttpOnly flag; therefore, setting this flag will not protect those clients against this form of attack. 10 | Dangerous Server header ::: The Server header sent by the remote web server disclose information that can aid an attacker, such as the server type of version.---It is advised to remove the Server header of the web server to not disclose detailed information about the underlying web server. 11 | Dangerous X-Powered-By header ::: The X-Powered-By header sent by the remote web server disclose information that can aid an attacker, such as languages and/or frameworks or technologies used by the web server.---It is advised to remove the X-Powered-By headers of the web server to not disclose detailed information about the underlying technologies. 12 | Potentially Dangerous Access-Control-Allow-Origin header ::: The CORS policy allows the application to specify exceptions to the protections implemented by the browser, and enables the developer to specify allowlisted domains for which external JavaScript is permitted to execute and interact with the page.The 'Access-Control-Allow-Origin' header is insecure when set to '*' or null, as it allows any domain to perform cross-domain requests and read responses. An attacker could abuse this configuration to retrieve private content from an application which does not use standard authentication mechanisms (for example, an Intranet allowing access from the internal network only).---Unless the target application is specifically designed to serve public content to any domain, the 'Access-Control-Allow-Origin' should be configured with an allowlist including only known and trusted domains to perform cross-domain requests if needed, or should be disabled. 13 | Potentially Dangerous Access-Control-Allow-Credentials header ::: The default behaviour of cross-origin resource requests is for requests to be passed without credentials like cookies and the Authorization header. However, the cross-domain server can permit reading of the response when credentials are passed to it by setting the CORS Access-Control-Allow-Credentials header to true.If the value is set to truethen the browser will send credentials (cookies, authorization headers or TLS client certificates). A wrong Access-Control-Allow-Origin configuration can allow external sites to steal credentials.---Set the Access-Control-Allow-Credentials header to true only when required and after having validated that the Access-Control-Allow-Origin header is properly set. -------------------------------------------------------------------------------- /request_headers.txt: -------------------------------------------------------------------------------- 1 | A-IM&&Acceptable instance-manipulations for the request.&&A-IM: feed&& && 2 | Accept&&Media type(s) that is/are acceptable for the response. See Content negotiation.&&Accept: text/html&& && 3 | Accept-Charset&&Character sets that are acceptable.&&Accept-Charset: utf-8&& && 4 | Accept-Datetime&&Acceptable version in time.&&Accept-Datetime: Thu, 31 May 2007 20:35:00 GMT&& && 5 | Accept-Encoding&&List of acceptable encodings. See HTTP compression.&&Accept-Encoding: gzip, deflate&& && 6 | Accept-Language&&List of acceptable human languages for response. See Content negotiation.&&Accept-Language: en-US&& && 7 | Access-Control-Request-Method&& && && && 8 | Access-Control-Request-Headers&&Initiates a request for cross-origin resource sharing with Origin (below).&&Access-Control-Request-Method: GET&& && 9 | Authorization&&Authentication credentials for HTTP authentication.&&Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==&& && 10 | Cache-Control&&Used to specify directives that must be obeyed by all caching mechanisms along the request-response chain.&&Cache-Control: no-cache&& && 11 | Connection&&Control options for the current connection and list of hop-by-hop request fields. Must not be used with HTTP/2.&&Connection: keep-alive&& && 12 | Content-Encoding&&The type of encoding used on the data. See HTTP compression.&&Content-Encoding: gzip&& && 13 | Content-Length&&The length of the request body in octets (8-bit bytes).&&Content-Length: 348&& && 14 | Content-MD5&&A Base64-encoded binary MD5 sum of the content of the request body.&&Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ==&& && 15 | Content-Type&&The Media type of the body of the request (used with POST and PUT requests).&&Content-Type: application/x-www-form-urlencoded&& && 16 | Cookie&&An HTTP cookie previously sent by the server with Set-Cookie (below).&&Cookie: $Version=1; Skin=new;&& && 17 | Date&&The date and time at which the message was originated (in "HTTP-date" format as defined by RFC 7231 Date/Time Formats).&&Date: Tue, 15 Nov 1994 08:12:31 GMT&& && 18 | Expect&&Indicates that particular server behaviors are required by the client.&&Expect: 100-continue&& && 19 | Forwarded&&Disclose original information of a client connecting to a web server through an HTTP proxy.&&Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43 Forwarded: for=192.0.2.43, for=198.51.100.17&& && 20 | From&&The email address of the user making the request.&&From: user@example.com&& && 21 | Host&&The domain name of the server (for virtual hosting), and the TCP port number on which the server is listening. The port number may be omitted if the port is the standard port for the service requested. Mandatory since HTTP/1.1. If the request is generated directly in HTTP/2, it should not be used.&&Host: en.wikipedia.org:8080&& && 22 | HTTP2-Settings&&A request that upgrades from HTTP/1.1 to HTTP/2 MUST include exactly one HTTP2-Setting header field. The HTTP2-Settings header field is a connection-specific header field that includes parameters that govern the HTTP/2 connection, provided in anticipation of the server accepting the request to upgrade.&&HTTP2-Settings: token64&& && 23 | If-Match&&Only perform the action if the client supplied entity matches the same entity on the server. This is mainly for methods like PUT to only update a resource if it has not been modified since the user last updated it.&&If-Match: "737060cd8c284d8af7ad3082f209582d"&& && 24 | If-Modified-Since&&Allows a 304 Not Modified to be returned if content is unchanged.&&If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT&& && 25 | If-None-Match&&Allows a 304 Not Modified to be returned if content is unchanged, see HTTP ETag.&&If-None-Match: "737060cd8c284d8af7ad3082f209582d"&& && 26 | If-Range&&If the entity is unchanged, send me the part(s) that I am missing; otherwise, send me the entire new entity.&&If-Range: "737060cd8c284d8af7ad3082f209582d"&& && 27 | If-Unmodified-Since&&Only send the response if the entity has not been modified since a specific time.&&If-Unmodified-Since: Sat, 29 Oct 1994 19:43:31 GMT&& && 28 | Max-Forwards&&Limit the number of times the message can be forwarded through proxies or gateways.&&Max-Forwards: 10&& && 29 | Origin&&Initiates a request for cross-origin resource sharing (asks server for Access-Control-* response fields).&&Origin: http://www.example-social-network.com&& && 30 | Pragma&&Implementation-specific fields that may have various effects anywhere along the request-response chain.&&Pragma: no-cache&& && 31 | Prefer&&Allows client to request that certain behaviors be employed by a server while processing a request.&&Prefer: return=representation&& && 32 | Proxy-Authorization&&Authorization credentials for connecting to a proxy.&&Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==&& && 33 | Range&&Request only part of an entity. Bytes are numbered from 0. See Byte serving.&&Range: bytes=500-999&& && 34 | TE&&The transfer encodings the user agent is willing to accept: the same values as for the response header field Transfer-Encoding can be used, plus the "trailers" value (related to the "chunked" transfer method) to notify the server it expects to receive additional fields in the trailer after the last, zero-sized, chunk.Only trailers is supported in HTTP/2.&&TE: trailers, deflate&& && 35 | Trailer&&The Trailer general field value indicates that the given set of header fields is present in the trailer of a message encoded with chunked transfer coding.&&Trailer: Max-Forwards&& && 36 | Transfer-Encoding&&The form of encoding used to safely transfer the entity to the user. Currently defined methods are: chunked, compress, deflate, gzip, identity. Must not be used with HTTP/2.&&Transfer-Encoding: chunked&& && 37 | User-Agent&&The user agent string of the user agent.&&User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/12.0&& && 38 | Upgrade&&Ask the server to upgrade to another protocol. Must not be used in HTTP/2.&& Upgrade: h2c, HTTPS/1.3, IRC/6.9, RTA/x11, websocket&& && 39 | Via&&Informs the server of proxies through which the request was sent.&&Via: 1.0 fred, 1.1 example.com (Apache/1.1)&& && 40 | Warning&&A general warning about possible problems with the entity body.&&Warning: 199 Miscellaneous warning && && 41 | Upgrade-Insecure-Requests&&Tells a server which (presumably in the middle of a HTTP -> HTTPS migration) hosts mixed content that the client would prefer redirection to HTTPS and can handle Content-Security-Policy: upgrade-insecure-requests&&Upgrade-Insecure-Requests: 1 && && 42 | X-Requested-With&&Mainly used to identify Ajax requests (most JavaScript frameworks send this field with value of XMLHttpRequest); also identifies Android apps using WebView&&X-Requested-With: XMLHttpRequest && && 43 | DNT&&Requests a web application to disable their tracking of a user. This is Mozilla's version of the X-Do-Not-Track header field (since Firefox 4.0 Beta 11). Safari and IE9 also have support for this field. &&DNT: 1 (Do Not Track Enabled), DNT: 0 (Do Not Track Disabled) && && 44 | X-Forwarded-For&&A de facto standard for identifying the originating IP address of a client connecting to a web server through an HTTP proxy or load balancer. Superseded by Forwarded header.&&X-Forwarded-For: client1, proxy1, proxy2; X-Forwarded-For: 129.78.138.66, 129.78.64.103 && && 45 | X-Forwarded-Host&&A de facto standard for identifying the original host requested by the client in the Host HTTP request header, since the host name and/or port of the reverse proxy (load balancer) may differ from the origin server handling the request. Superseded by Forwarded header.&&X-Forwarded-Host: en.wikipedia.org:8080; X-Forwarded-Host: en.wikipedia.org && && 46 | X-Forwarded-Proto&&A de facto standard for identifying the originating protocol of an HTTP request, since a reverse proxy (or a load balancer) may communicate with a web server using HTTP even if the request to the reverse proxy is HTTPS. An alternative form of the header (X-ProxyUser-Ip) is used by Google clients talking to Google servers. Superseded by Forwarded header.&&X-Forwarded-Proto: https && && 47 | Front-End-Https&&Non-standard header field used by Microsoft applications and load-balancers&&Front-End-Https: on && && 48 | X-Http-Method-Override&&Requests a web application to override the method specified in the request (typically POST) with the method given in the header field (typically PUT or DELETE). This can be used when a user agent or firewall prevents PUT or DELETE methods from being sent directly.&&X-HTTP-Method-Override: DELETE && && 49 | X-ATT-DeviceId&&Allows easier parsing of the MakeModel/Firmware that is usually found in the User-Agent String of AT&T Devices&&X-Att-Deviceid: GT-P7320/P7320XXLPG && && 50 | X-Wap-Profile&&Links to an XML file on the Internet with a full description and details about the device currently connecting. In the example to the right is an XML file for an AT&T Samsung Galaxy S2.&&x-wap-profile: http://wap.samsungmobile.com/uaprof/SGH-I777.xml && && 51 | Proxy-Connection&&Implemented as a misunderstanding of the HTTP specifications. Common because of mistakes in implementations of early HTTP versions. Has exactly the same functionality as standard Connection field.&&Proxy-Connection: keep-alive && && 52 | X-UIDH&&Server-side deep packet insertion of a unique ID identifying customers of Verizon Wireless; also known as "perma-cookie" or "supercookie"&&X-UIDH: ... && && 53 | X-Csrf-Token&&Used to prevent cross-site request forgery. Alternative header names are: X-CSRFToken and X-XSRF-TOKEN&&X-Csrf-Token: i8XNjC4b8KVok4uw5RftR38Wgp2BFwql && && 54 | X-Request-ID&& && && && 55 | X-Correlation-ID&&Correlates HTTP requests between a client and server.&&X-Request-ID: f058ebd6-02f7-4d3f-942e-904344e8cde5 && && -------------------------------------------------------------------------------- /response_headers.txt: -------------------------------------------------------------------------------- 1 | Accept-CH&&Requests HTTP Client Hints&&Accept-CH: UA, Platform&& && 2 | Access-Control-Allow-Origin&&Specifies which web sites can participate in cross-origin resource sharing&& && && 3 | Access-Control-Allow-Credentials&&Specifies which web sites can participate in cross-origin resource sharing&& && && 4 | Access-Control-Expose-Headers&&Specifies which web sites can participate in cross-origin resource sharing&& && && 5 | Access-Control-Max-Age&&Specifies which web sites can participate in cross-origin resource sharing&& && && 6 | Access-Control-Allow-Methods&&Specifies which web sites can participate in cross-origin resource sharing&& && && 7 | Access-Control-Allow-Headers&&Specifies which web sites can participate in cross-origin resource sharing&& && && 8 | Accept-Patch&&Specifies which patch document formats this server supports&&Accept-Patch: text/example;charset=utf-8&& && 9 | Accept-Ranges&&What partial content range types this server supports via byte serving&&Accept-Ranges: bytes&& && 10 | Age&&The age the object has been in a proxy cache in seconds&&Age: 12&& && 11 | Allow&&Valid methods for a specified resource. To be used for a 405 Method not allowed&&Allow: GET, HEAD&& && 12 | Alt-Svc&&A server uses "Alt-Svc" header (meaning Alternative Services) to indicate that its resources can also be accessed at a different network location (host or port) or using a different protocol When using HTTP/2, servers should instead send an ALTSVC frame.&&Alt-Svc: http/1.1="http2.example.com:8001"; ma=7200&& && 13 | Cache-Control&&Tells all caching mechanisms from server to client whether they may cache this object. It is measured in seconds&&Cache-Control: max-age=3600&& && 14 | Connection&&Control options for the current connection and list of hop-by-hop response fields.&&Connection: close&& && 15 | Content-Disposition&&An opportunity to raise a "File Download" dialogue box for a known MIME type with binary format or suggest a filename for dynamic content. Quotes are necessary with special characters.&&Content-Disposition: attachment; filename="fname.ext"&& && 16 | Content-Encoding&&The type of encoding used on the data. See HTTP compression.&&Content-Encoding: gzip&& && 17 | Content-Language&&The natural language or languages of the intended audience for the enclosed content&&Content-Language: da&& && 18 | Content-Length&&The length of the response body in octets (8-bit bytes)&&Content-Length: 348&& && 19 | Content-Location&&An alternate location for the returned data&&Content-Location: /index.htm&& && 20 | Content-MD5&&A Base64-encoded binary MD5 sum of the content of the response&&Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ==&& && 21 | Content-Range&&Where in a full body message this partial message belongs&&Content-Range: bytes 21010-47021/47022&& && 22 | Content-Type&&The MIME type of this content&&Content-Type: text/html; charset=utf-8&& && 23 | Date&&The date and time that the message was sent (in "HTTP-date" format as defined by RFC 7231)&&Date: Tue, 15 Nov 1994 08:12:31 GMT&& && 24 | Delta-Base&&Specifies the delta-encoding entity tag of the response.&&Delta-Base: "abc"&& && 25 | ETag&&An identifier for a specific version of a resource, often a message digest&&ETag: "737060cd8c284d8af7ad3082f209582d"&& && 26 | Expires&&Gives the date/time after which the response is considered stale (in "HTTP-date" format as defined by RFC 7231)&&Expires: Thu, 01 Dec 1994 16:00:00 GMT&& && 27 | IM&&Instance-manipulations applied to the response.&&IM: feed&& && 28 | Last-Modified&&The last modified date for the requested object (in "HTTP-date" format as defined by RFC 7231)&&Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT&& && 29 | Link&&Used to express a typed relationship with another resource, where the relation type is defined by RFC 5988&&Link: ; rel="alternate"&& && 30 | Location&&Used in redirection, or when a new resource has been created.&&Location: http://www.w3.org/pub/WWW/People.html&& && 31 | P3P&&This field is supposed to set P3P policy, in the form of P3P:CP="your_compact_policy". However, P3P did not take off, most browsers have never fully implemented it, a lot of websites set this field with fake policy text, that was enough to fool browsers the existence of P3P policy and grant permissions for third party cookies.&&P3P: CP="This is not a P3P policy! See https://en.wikipedia.org/wiki/Special:CentralAutoLogin/P3P for more info."&& && 32 | Pragma&&Implementation-specific fields that may have various effects anywhere along the request-response chain.&&Pragma: no-cache&& && 33 | Preference-Applied&&Indicates which Prefer tokens were honored by the server and applied to the processing of the request.&&Preference-Applied: return=representation&& &&RFC 7240 34 | Proxy-Authenticate&&Request authentication to access the proxy.&&Proxy-Authenticate: Basic&& && 35 | Public-Key-Pins&&HTTP Public Key Pinning, announces hash of website's authentic TLS certificate&&Public-Key-Pins: max-age=2592000; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=";&& && 36 | Retry-After&&If an entity is temporarily unavailable, this instructs the client to try again later. Value could be a specified period of time (in seconds) or a HTTP-date.&&Retry-After: 120&& && 37 | Server&&A name for the server&&Server: Apache/2.4.1 (Unix)&& && 38 | Set-Cookie&&An HTTP cookie&&Set-Cookie: UserID=JohnDoe; Max-Age=3600; Version=1&& && 39 | Strict-Transport-Security&&A HSTS Policy informing the HTTP client how long to cache the HTTPS only policy and whether this applies to subdomains.&&Strict-Transport-Security: max-age=16070400; includeSubDomains&& && 40 | Trailer&&The Trailer general field value indicates that the given set of header fields is present in the trailer of a message encoded with chunked transfer coding.&&Trailer: Max-Forwards&& && 41 | Transfer-Encoding&&The form of encoding used to safely transfer the entity to the user. Currently defined methods are: chunked, compress, deflate, gzip, identity.&&Transfer-Encoding: chunked&& && 42 | Tk&&Tracking Status header, value suggested to be sent in response to a DNT(do-not-track), possible values: "!" — under construction, "?" — dynamic, "G" — gateway to multiple parties, "N" — not tracking, "T" — tracking, "C" — tracking with consent, "P" — tracking only if consented, "D" — disregarding DNT, "U" — updated&&Tk: ?Upgrade: Ask the client to upgrade to another protocol. Upgrade: h2c, HTTPS/1.3, IRC/6.9, RTA/x11, websocket&& && 43 | Vary&&Tells downstream proxies how to match future request headers to decide whether the cached response can be used rather than requesting a fresh one from the origin server.&&Example Vary: *, Example 2: Vary: Accept-Language&& && 44 | Via&&Informs the client of proxies through which the response was sent.&&Via: 1.0 fred, 1.1 example.com (Apache/1.1)&& && 45 | Warning&&A general warning about possible problems with the entity body.&&Warning: 199 Miscellaneous warning&& && 46 | WWW-Authenticate&&Indicates the authentication scheme that should be used to access the requested entity.&&WWW-Authenticate: Basic&& && 47 | X-Frame-Options&&Clickjacking protection: deny - no rendering within a frame, sameorigin - no rendering if origin mismatch, allow-from - allow from specified location, allowall - non-standard, allow from any location&&X-Frame-Options: deny&& && 48 | Content-Security-Policy&&Content Security Policy definition.&& && && 49 | X-Content-Security-Policy&&Content Security Policy definition.&& && && 50 | X-WebKit-CSP&&Content Security Policy definition.&&X-WebKit-CSP: default-src 'self'&& && 51 | Expect-CT&&Notify to prefer to enforce Certificate Transparency.&&Expect-CT: max-age=604800, enforce, report-uri="https://example.example/report"&& && 52 | NEL&&Used to configure network request logging.&&NEL: { "report_to": "name_of_reporting_group", "max_age": 12345, "include_subdomains": false, "success_fraction": 0.0, "failure_fraction": 1.0 }&& && 53 | Permissions-Policy&&To allow or disable different features or APIs of the browser.&&Permissions-Policy: fullscreen=(), camera=(), microphone=(), geolocation=(), interest-cohort=()&& && 54 | Refresh&&Used in redirection, or when a new resource has been created. This refresh redirects after 5 seconds. Header extension introduced by Netscape and supported by most web browsers. Defined by HTML Standard&&Refresh: 5; url=http://www.w3.org/pub/WWW/People.html&& && 55 | Report-To&&Instructs the user agent to store reporting endpoints for an origin.&&Report-To: { "group": "csp-endpoint", "max_age": 10886400, "endpoints": [ { "url": "https-url-of-site-which-collects-reports" } ] }&& && 56 | Status&&CGI header field specifying the status of the HTTP response. Normal HTTP responses use a separate "Status-Line" instead, defined by RFC 7230.&&Status: 200 OK&& && 57 | Timing-Allow-Origin&&The Timing-Allow-Origin response header specifies origins that are allowed to see values of attributes retrieved via features of the Resource Timing API, which would otherwise be reported as zero due to cross-origin restrictions.&&Timing-Allow-Origin: *&& && 58 | X-Content-Duration&&Provide the duration of the audio or video in seconds; only supported by Gecko browsers&&X-Content-Duration: 42.666&& && 59 | X-Content-Type-Options&&The only defined value, "nosniff", prevents Internet Explorer from MIME-sniffing a response away from the declared content-type. This also applies to Google Chrome, when downloading extensions.&&X-Content-Type-Options: nosniff&& && 60 | X-Powered-By&&Specifies the technology (e.g. ASP.NET, PHP, JBoss) supporting the web application (version details are often in X-Runtime, X-Version, or X-AspNet-Version)&&X-Powered-By: PHP/5.4.0&& && 61 | X-Redirect-By&&Specifies the component that is responsible for a particular redirect.&&X-Redirect-By: WordPress&& && 62 | X-Request-ID&&Correlates HTTP requests between a client and server.&& && && 63 | X-Correlation-ID&&Correlates HTTP requests between a client and server.&&X-Request-ID: f058ebd6-02f7-4d3f-942e-904344e8cde5&& && 64 | X-UA-Compatible&&Recommends the preferred rendering engine (often a backward-compatibility mode) to use to display the content. Also used to activate Chrome Frame in Internet Explorer. In HTML Standard, only the IE=edge value is defined.&&X-UA-Compatible: IE=edge&& && 65 | X-XSS-Protection&&Cross-site scripting (XSS) filter&&X-XSS-Protection: 1; mode=block&& && 66 | X-Azure-OriginStatusCode&&This header contains the HTTP status code returned by the backend&&X-Azure-OriginStatusCode: 503&& && 67 | X-Azure-ExternalError&&This header shows the error code that Front Door servers come across while establishing connectivity to the backend server to process a request.&&X-Azure-ExternalError: 0x830c1011, The certificate authority is unfamiliar&& && -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Headers Burp Extension 2 | 3 | ## Introduction 4 | Let me ask you two questions: 5 | 6 | 1- Are you a pentester or bug bounty hunter? 7 | 8 | 2- Do you hate reporting? 9 | 10 | If you answered yes to both (and I assume that if you answered "yes" to the first one, you necessarily answered "yes" to the second one), check out this extension. It removes the hassle of reporting missing security headers in your pentest reports. Here is what the extension does (the last part is the best, so read until the end!): 11 | 12 | - It gets all your Burp Proxy History, element by element. 13 | - It identifies all headers, both for the requests and the responses. 14 | - From all these URLs and Hosts you have visited, it retains only the unique ones, discarding duplicates. 15 | - It gives you a list of all the headers available, and if you click on them it will tell you at which Hosts and URLs you can find them. 16 | - It also identifies the meta headers and shows you their contents in an unified way, so you don't need to scrape websites or do other dirty and annoying tricks. 17 | - You can select headers you are interested in, or define new ones, from different categories (missing security headers, dangerous or potentially dangerous headers). A large preset of headers is included, but you can add your own if you miss any (feel free to send pull requests with headers you would like to include) 18 | - It gives you some nice formatting of headers information for every request, as well as color effects to let you know which requests contain more of the headers you are looking for. 19 | - You can output to text files the headers detected in CSV or JSON format. More options will be included in the future, if they make sense. 20 | - But, best of all, you can export to MS Word custom templates the results, so that you can autofill your own reports with a couple of mouse clicks, or generate a Word file from which you can just copy and paste to your own report individual tables, graphs, and use the included vulnerable headers or cookie flags definitions, or replace them with your own. 21 | 22 | No more hours wasted reporting missing security headers, server headers, etc etc etc :) 23 | A couple of clicks will do that for you!!! 24 | 25 | ## Disclaimer 26 | As far as I have used and tested this extension, both while developing it and for my personal and professional use, it works as it should, but there may be unexpected cases where it doesn't find some header, doesn't properly find unique endpoints or some other minor issues. In any case I don't think you will find many of these outliers, but complicated URLs could cause some problems. If you detect any, feel free to let me know (check the How to contribute section further down below). Also, I advise that you check manually the results provided by this extension, at least a couple of times to get used to how this extension works, and to make sure that you don't report to your client anything that is not actually there. By now I don't check it anymore for my own pentests because I know it works, but use it at your own risk! 27 | 28 | 29 | 30 | 31 | ## Requirements 32 | This extension is written in Python, therefore you will need the Jython .jar file set on your Burp Suite configuration. If you don't know how to do that, check my course on Burp Extensions creation (https://medium.com/system-weakness/burp-extensions-creation-1-7ddeb61efb33) or google it, it shouldn't be difficult :) 33 | 34 | Also, one of the main components of this extension requires that you install Python 3 on your system (on most linux distros should be included by default, on Windows use the installer from https://www.python.org/downloads/). If everything happens with no problems, this extension should detect your installation of Python and install the docxtpl library, to be able to create the .docx reports. In any case, you can install the library yourself with ``pip install docxtpl``. If the extension cannot find your Python binary path you can set it manually, read item 5 on the Instructions or, create an Issue if it doesn't work for you. 35 | 36 | In any case, the extension generates an auxiliary file that you can copy to another machine, and generate it there. 37 | 38 | ## Instructions 39 | 1- After you have captured some proxy traffic, switch to the Headers tab and click the Update button. That will fill the left panel with the headers that appear in the requests from your captured traffic, sorted by alphabetical order. If you change to the Response tab, in that same panel, you will see the sorted headers from the captured responses. In both cases, under each header you can see the hosts where that header is present. 40 | 41 | ![gif1](https://user-images.githubusercontent.com/34309036/210148566-c0a36726-a80f-4a4b-817c-458a6da441d0.gif) 42 | 43 | 2- If there are many results, you can use the two filters at the top to filter hosts and endpoints. In the case of endpoints filter, you can use different words separated by a comma. The filter will show all results matching any of these words. Press enter after writing filters, or click again the Update button to filter results. 44 | 45 | ![gif2](https://user-images.githubusercontent.com/34309036/210148657-02da7b3f-9e72-4521-be54-9ad4235eefa4.gif) 46 | 47 | 48 | 3- On the left panel as well, you can click on the Meta tab, to get the ```` HTML tags for each host that has any. 49 | 50 | ![gif3](https://user-images.githubusercontent.com/34309036/210148686-5dd130de-69d2-460d-ab01-e29a9b7b80d2.gif) 51 | 52 | 4- If you click in any row of the left panel, either for Requests, Responses or Meta, the central panel will fill with the URLs (endpoint) where those headers or meta tags can be found. The central panel has two tabs, "Unique endpoints" and "All endpoints". If you have browsed the same endpoints during your tests, you will have different requests to and responses from the same endpoint. Or you may have requests to an endpoint, with slight variations in parameters or routes in the URLs. That can be confusing, and seem bulkier than it really is. In the "All endpoints" tab you will see all requests where a certain header, in a certain Host, appears. In the "Unique endpoints" tab, some preprocessing is done, to show you only unique endpoints. Also, query string parameters and things like numbers in the URL have been grepped and replaced by a star symbol. This will remove duplicated URLs, and only show you unique endpoints, which is specially useful when your history has grown pretty big. Addittionaly, the Unique endpoints tab shows you the number of detected headers belonging to three categories: "Security headers", "Dangerous or verbose headers" and "Potentially dangerous headers". Along with the number, you will see a color which becomes brighter, the more headers you want to be taken into account in these categories. You can select these headers from the configuration window (click on the gear icon to open it, and don't forget to click on the "Apply changes" button when you are done!!!). The brightest color will be when all headers for that category are present. 53 | 54 | 5- Both for "Unique endpoints" and "All endpoints", when you click them (in the case of unique endpoints it will show the first occurrence), the panel on the right will show you a summary of the request and response headers for that endpoint, highlighting in orange and bold font the currently selected header on the left panel, in bold the Host header, for easier visibility, and color symbols for any detected Security header (green [+]), Dangerous or verbose header (red [-]), or potentially dangerous header (blue [?]). This summary also includes a list of missing security headers, compared to all the security headers you have chosen in the configuration window. 55 | 56 | ![gif4](https://user-images.githubusercontent.com/34309036/210148727-39a5eca3-b876-40bd-8e78-23a829d369d6.gif) 57 | 58 | 6- Click on the blue Summary button on the lower right corner to open an additional window. This is where the magic happens, and you can export a Word report based on a custom template all the headers findings. First, click on the "Update Hosts" button on the upper left corner to populate the left panel with all hosts detected from the Proxy History. Then, check the checkboxes next to every host that you want to include in the report. Take into account that there will probably be a lot of undesired hosts in this list, from analytic sites and other third parties. Also, make sure that you select all subdomains or desired hosts. Next, make sure that the categories of vulnerability types that you desire to report are checked (by default, all): "Missing security headers", "Potentially dangerous headers", "dangerous or verbose headers" and "Cookies without flags". Lastly, click the "Update for selected hosts" button to populate the main panel with all the headers that will be reported. Again, uncheck all the checkboxes of the entries that you don't want to include in the report. 59 | 60 | I have noticed during my tests that on many occasions a host uses different headers for different endpoints. This makes it very easy to miss problematic headers when you test manually, but not anymore thanks to this extension. Change the value of "Depth" on the upper right corner in the Summary window to an integer value, to scan for different depths on the URL structure (for example, depth=1 means the endpoint "/x", depth=2 means "/.../x", depth=3 means "/.../.../x" and so on (where "x" would be the last portion of the endpoint), and depth=0 will give you the full URL). Of course, the lower the depth (which, by default is 1, the lowest possible) the less "granular" the results will be, and the fewer entries you will have. So if you want to report more headers, or "dig" deeper, increase the depth and click again on the "Update for selected hosts" button. 61 | 62 | Finally, click on the "Choose output file" on the lower left corner to choose the output .docx file to export the results, or write the full path directly on the textbox next to the button. And click on the ".docx report" button on the lower right corner to generate the report. To the left of this button there is a gear icon that you can click to open the configuration for generating Word reports. As mentioned earlier, you need to have Python3 installed on your computer (Jython is not enough, since the module to generate word reports doesn't work in Jython). If you install it using standard methods (the Python installer for Windows, or apt install python on linux) everything should work. However, you can provide the path to the Python executable on the upper textbox in this window. Then, click the button to make sure you are running the "docxtpl" Python library, which is used to create the reports. You can install it yourself, but this extension will install it for you (at least I didn't have problems installing it, but you never know...). 63 | 64 | ![gif5](https://user-images.githubusercontent.com/34309036/210148737-ed530567-875d-4590-a500-0b21c0d968d2.gif) 65 | 66 | ![234px-Warning svg](https://user-images.githubusercontent.com/34309036/210150493-a925991c-7ff7-440f-9275-7075357b6131.png) 67 | 68 | WARNING!I have noticed that some strange urls, with a lot of symbols can cause problems. If the report is not appearing in the target path, uncheck the longest and weirdest looking URLs, to see if the report appears now. Then, decide if you can skip that URL in your report, or add it manually. It's hard to predict every type of URLs out there, so some cannot be easily parsed, sorry! As an example, this URL caused me problems when generating the report, so if you see something similar, please uncheck it from the Summary window and try again: ``/rum.js;rAraT1IazsN9CNL_S+tTtSCsOVg=``. In this case the problem was the semicolon between "js" and what comes next, that messed up some string splitting that the code does 69 | 70 | 6- If you use the example template, or modify it, don't forget to update the figure numbers for the evidences that you can paste later into the report. For that, you can select all text on Word (Ctrl-A, or some other key combination depending on your keyboard language) and then right click on the selection and click on "Update field" (for some keyboard languages, the F9 key also works). 71 | 72 | ![gif6](https://user-images.githubusercontent.com/34309036/210150343-dc988795-ff39-41d5-8583-c323fa7bfaf2.gif) 73 | 74 | NOTE: You don't need to create the report on the same machine where you have Burp installed. The extension generates the file called output/selected_output.txt file when you click the generate report button. You can copy this file over to another machine that has Python and Docxtpl, and call the script in /template/template.py. To do that, copy the selected_output.txt file to another machine where you have downloaded the repo, and place it in the output/ directory. Then go to the template directory and run: ``python template.py ``, where ```` is the absolute or relative path where the report will be saved. If, for example, you do ``python template.py out.docx``, a report called out.docx will be saved in the very same template folder, next to the template.py script. 75 | 76 | 7- Enjoy the many hours of manual work that you just saved yourself. Preferably, use these hours to go for a walk or do some exercise instead of gaming, so that the electricity that you save won't pollute the planet, I guess that typing in Word requires less electricity than games to run :) 77 | 78 | ## Word template and other customization 79 | 1- This extension recalls the configurations that you use, but make sure to "save changes" or "apply changes" in the different config windows, whenever you see such buttons. Doing so, next time you load the extension it will have the same settings you last used. There are also buttons to revert to factory settings. 80 | 81 | 2- You will probably want to use a custom template for the word reports. First, look for the provided template. It is in the /template/template.docx file. If you installed this extension via the Portswigger BApp store, you will find the directory of this extension under ``C:\Users\\AppData\Roaming\BurpSuite\bapps``. Then to find it (folders are given weird names, look for some file belonging to this extension, for example, "headers_window.py". Then look for the template). 82 | 83 | The template contains Jinja2 instructions to replace in each generated table the appropriate values. Notice that the provided template contains a for loop instruction surrounding the table. This means that a different table will be generated for every issue that will be reported. You can skip some of this information if you don't need it. or you can place it in other arrangements. Next is a description of the different fields you can use (include the "{ " before and " }}" after, they are Jinja2 syntax and are necessary.): 84 | 85 | - ``{{ host[‘headers’]["Name"] }}``: Vulnerability name 86 | - ``{{ host[‘headers’]["Host"] }}``: Host name 87 | - ``{{ host[‘headers’]["IP"] }}``: Host IP, in case it's available 88 | - ``{{ host[‘headers’]["Port"] }}``: Port where the endpoint is 89 | - ``{{ host[‘headers’]["Protocol"] }}``: Protocol 90 | - ``{{ host[‘headers’]["Severity"] }}``: Severity (Critical, High, Medium or Low) 91 | - ``{{ host[‘headers’]["Code"] }}``: In case you use some code to identify vulnerabilities. Not implemented right now. 92 | - ``{{ host[‘headers’][“CVSS”] }}``: CVSS value (from 0 to 10) 93 | - ``{{ host[‘headers’][“CVSS_image”] }}``: Any image that you want to include to graphically represent the CVSS score 94 | - ``{{ host[‘headers’][“Description”] }}``: Vulnerability description 95 | - ``{{ host[‘headers’][“Solution”] }}``: Remediation instructions for the vulnerability 96 | - in case you want to include a list of endpoints where the vulnerability is found 97 | ``` 98 | {% for url in host[‘headers’][“URLs”] %} 99 | - {{ url }} 100 | {%- endfor %}``` 101 | 102 | 103 | Feel free to modify the template.py file to include any new functionality that is currently not implemented. This script already contains CVSS scores and vectors for the different vulnerabilities, but keep in mind that they could vary for specific conditions. However I think they are a valid approximation in most cases. 104 | 105 | Also, the info.txt file, also in the template directory, contains the description and remediation for every vulnerability. Feel free to modify them, to adapt them to your needs or style of reporting. Every line is in the following format: 106 | `` ::: --- `` 107 | So if you add new, or modify the current ones, make sure they are in this format. Also, avoid empty lines, just in case. 108 | 109 | ## How to contribute 110 | If you enjoy this extension, please give a star to the repo: https://github.com/dh0ck/Headers 111 | I have dedicated many hours to create it, not just to save myself time in the long run, but especially to free pentesters around the world from something that I know we all hate, so that we can dedicate more time to actually doing what we like, and finding more vulns. 112 | 113 | If you want to contribute, feel free to add pull requests, or if you have ideas that you would like this extension to include, feel free to create an Issue on this repo, write to me on Telegram (@dh0ck) or via LinkedIn (https://www.linkedin.com/in/dh0ck). If I find your idea is feasible I will add it to my to-do list! Let's make this tool save us even more hours of stupid reporting! 114 | 115 | Thanks again for taking the time to try this extension, hope you find it useful ;) 116 | -------------------------------------------------------------------------------- /headers_window.py: -------------------------------------------------------------------------------- 1 | from burp import IBurpExtender, ITab 2 | from burp import IContextMenuFactory 3 | 4 | import threading 5 | #import java 6 | import shutil, glob, re, sys, os, subprocess 7 | #from time import sleep 8 | from javax.swing import JFrame, JProgressBar, JSplitPane, JTable, JScrollPane, JPanel, BoxLayout, WindowConstants, JLabel, JMenuItem, JTabbedPane, JButton, JTextField, JTextArea, SwingConstants, JEditorPane, JComboBox, DefaultComboBoxModel, JFileChooser, ImageIcon, JCheckBox, JRadioButton, ButtonGroup, KeyStroke 9 | from javax.swing.table import DefaultTableModel, DefaultTableCellRenderer, TableCellRenderer 10 | from java.awt import BorderLayout, Dimension, FlowLayout, GridLayout, GridBagLayout, GridBagConstraints, Point, Component, Color # quitar los layout que no utilice 11 | from java.util import List, ArrayList 12 | from java.lang import Boolean, String, Integer 13 | #from java.awt.event.MouseEvent import getPoint 14 | from java.awt.event import MouseListener, FocusListener 15 | 16 | 17 | burp_extender_instance = "" # variable global que sera el instance de bupr extender, para acceder a los valores de la instancia de IBurpExtender que burp crea, pero desde fuera, sobre todo para cambiar con clicks la tabla de endpoints 18 | history1 = [] 19 | host_endpoint = [] #se rellena al darle a filter en la tab, pero habra que arreglar que no haya duplicados cuando cambian los valores de los query string rameters (o puedo dejar que se repitan y ponerlos todos.) lo bueno seria poner tambien el index del history y en el text area poner los headers de la req los de la resp, separados por una =========, etc 20 | endpoint_table = [] 21 | endpoint_table_meta = [] 22 | selected_header_name = "" 23 | 24 | 25 | class RawHtmlRenderer(DefaultTableCellRenderer): 26 | def __init__(self): 27 | self.result = JLabel() 28 | self.DTCR = DefaultTableCellRenderer() 29 | 30 | def getTableCellRendererComponent( 31 | self, 32 | table, # JTable - table containing value 33 | value, # Object - value being rendered 34 | isSelected, # boolean - Is value selected? 35 | hasFocus, # boolean - Does this cell have focus? 36 | row, # int - Row # (0..N) 37 | col # int - Col # (0..N) 38 | ) : 39 | comp = self.DTCR.getTableCellRendererComponent( 40 | table, value, isSelected, hasFocus, row, col 41 | ) 42 | 43 | 44 | 45 | result = self.result 46 | 47 | ############################################################################ 48 | ###### something is not right, clicking on cells doesn't change selected background 49 | result.setBorder( comp.getBorder() ) 50 | if (isSelected): 51 | result.setBackground(table.getSelectionBackground()) 52 | #result.setBackground(Color.blue)) 53 | result.setForeground(table.getSelectionForeground()) 54 | 55 | else: 56 | result.setBackground(table.getBackground()) 57 | result.setForeground(table.getForeground()) 58 | ################################################################################ 59 | result.setText(value) 60 | result.putClientProperty("html.disable", None) 61 | 62 | return result 63 | 64 | class ConfigTableModel(DefaultTableModel): 65 | def __init__(self, data, headings): 66 | DefaultTableModel.__init__(self, data, headings) 67 | 68 | def getColumnClass(self, col): 69 | return [Boolean, String][col] 70 | 71 | class IssueTableModel(DefaultTableModel): 72 | """Extends the DefaultTableModel to make it readonly.""" 73 | def __init__(self, data, headings): 74 | # call the DefaultTableModel constructor to populate the table 75 | DefaultTableModel.__init__(self, data, headings) 76 | 77 | def isCellEditable(self, row, column): 78 | """Returns True if cells are editable.""" 79 | canEdit = [False, False, False] 80 | return canEdit[column] 81 | 82 | class IssueTableMouseListener(MouseListener): 83 | """Some necessary entries that must be present on all mouse listeners, so this is a parent class that is inherited by the other specific mouse listener clases below.""" 84 | 85 | def getClickedIndex(self, event): 86 | """Returns the value of the first column of the table row that was 87 | clicked. This is not the same as the row index because the table 88 | can be sorted.""" 89 | # get the event source, the table in this case. 90 | tbl = event.getSource() 91 | # get the clicked row 92 | row = tbl.getSelectedRow() 93 | # get the first value of clicked row 94 | return tbl.getValueAt(row, 0) 95 | # return event.getSource.getValueAt(event.getSource().getSelectedRow(), 0) 96 | 97 | def getClickedRow(self, event): 98 | """Returns the complete clicked row.""" 99 | tbl = event.getSource() 100 | return [tbl.getModel().getDataVector().elementAt(tbl.getSelectedRow()), tbl.getSelectedRow()] 101 | 102 | def mousePressed(self, event): 103 | pass 104 | 105 | def mouseReleased(self, event): 106 | pass 107 | 108 | # the following two are necessary, although they are empty, otherwise the extension crashes when the mouse cursor enters or exits the table 109 | def mouseEntered(self, event): 110 | pass 111 | 112 | def mouseExited(self, event): 113 | pass 114 | 115 | class IssueTableMouseListener_Window(IssueTableMouseListener): 116 | """Adds values to the extra information panel when an entry in the floating window is double clicked. The extra info panel is always there, double clicking elements of the floating window only makes it visible.""" 117 | def mouseClicked(self, event): 118 | if event.getClickCount() == 1: # single click on table elements 119 | header = self.getClickedRow(event)[0] 120 | 121 | header = header[0].split(''.format(burp_extender_instance.color1))[1].split('')[0] #debe haber mas abajo al reves el orden de las closing tabs b y font 122 | burp_extender_instance.extra_info_textarea1.setText(header) 123 | if header in list(burp_extender_instance.dict_req_headers.keys()) and header not in list(burp_extender_instance.dict_resp_headers.keys()): 124 | burp_extender_instance.extra_info_textarea2.setText(burp_extender_instance.dict_req_headers[header][0]) 125 | burp_extender_instance.extra_info_textarea3.setText(burp_extender_instance.dict_req_headers[header][1]) 126 | burp_extender_instance.extra_info_textarea4.setText(burp_extender_instance.dict_req_headers[header][2]) 127 | burp_extender_instance.extra_info_textarea5.setText(burp_extender_instance.dict_req_headers[header][3]) 128 | if header not in (list(burp_extender_instance.dict_req_headers.keys())) and header not in list(burp_extender_instance.dict_resp_headers.keys()): 129 | burp_extender_instance.extra_info_textarea2.setText('Description unavailable for header: ' + header) 130 | burp_extender_instance.extra_info_textarea3.setText('Example unavailable for header: ' + header) 131 | burp_extender_instance.extra_info_textarea4.setText('URL unavailable for header: ' + header) 132 | burp_extender_instance.extra_info_textarea5.setText('Potential risks unavailable for header: ' + header) 133 | if header in list(burp_extender_instance.dict_resp_headers.keys()): 134 | burp_extender_instance.extra_info_textarea2.setText(burp_extender_instance.dict_resp_headers[header][0]) 135 | burp_extender_instance.extra_info_textarea3.setText(burp_extender_instance.dict_resp_headers[header][1]) 136 | burp_extender_instance.extra_info_textarea4.setText(burp_extender_instance.dict_resp_headers[header][2]) 137 | burp_extender_instance.extra_info_textarea5.setText(burp_extender_instance.dict_resp_headers[header][3]) 138 | if event.getClickCount() == 2: # double click to make extra info panel visible 139 | burp_extender_instance.extra_info.setVisible(True) 140 | 141 | class IssueTableMouseListener_Meta(IssueTableMouseListener): 142 | """Adds functionality to the Header-host table (to the tab) when its elements are clicked.""" 143 | def mouseClicked(self, event): 144 | burp_extender_instance.is_meta = True 145 | if event.getClickCount() == 1: 146 | tbl = event.getSource() 147 | val = tbl.getModel().getDataVector().elementAt(tbl.getSelectedRow()) 148 | 149 | identifier = val[0] 150 | clicked_host = val[1] 151 | 152 | k = tbl.getSelectedRow() 153 | if identifier == '': 154 | while identifier == '': 155 | k -= 1 156 | identifier = tbl.getModel().getDataVector().elementAt(k)[0] 157 | 158 | global endpoint_table_meta # igual no tiene que ser gobal 159 | endpoint_table_meta = [] 160 | 161 | # meta_table tiene columnas: host | url | meta tag, una para cada tag, repitiendo host y url si hay mas de una tag en una url 162 | for (host, endpoint, meta) in burp_extender_instance.meta_table: 163 | #if identifier not in meta and clicked_host != host: 164 | if clicked_host == host: 165 | spl = endpoint.split(' ') 166 | line = spl[0] + " :: " + host + " :: " + " ".join(spl[1:]) 167 | endpoint_table_meta.append([endpoint]) #poner el host antes de la url pero despues del method 168 | #endpoint_table_meta.append([line]) #poner el host antes de la url pero despues del method 169 | 170 | #ESTA COGIENDO ENPOINTS QUE NO CORRESPONDEN, VER SI ES QUE COINCIDEN CON UN HOST DIFERENTE. TAMBIEN TENGO QUE APLICAR REGEX EN LOS UNIQUE ENDPOINTS 171 | 172 | burp_extender_instance.selected_meta_header = identifier#header # este lo settea ok para la de endpoints 173 | burp_extender_instance.selected_host = clicked_host 174 | burp_extender_instance.update_meta_endpoints(endpoint_table_meta) 175 | 176 | class IssueTableMouseListener_Tab(IssueTableMouseListener): 177 | """Adds functionality to the Header-host table when its elements are clicked.""" 178 | def mouseClicked(self, event): 179 | burp_extender_instance.is_meta = False 180 | if event.getClickCount() == 1: 181 | 182 | tbl = event.getSource() 183 | val = tbl.getModel().getDataVector().elementAt(tbl.getSelectedRow()) 184 | 185 | header = val[0] 186 | clicked_host = val[1] 187 | 188 | k = tbl.getSelectedRow() 189 | if header == '': 190 | while header == '': 191 | k -= 1 192 | header = tbl.getModel().getDataVector().elementAt(k)[0] 193 | header_value = header.split(''.format(burp_extender_instance.color1))[1].split('')[0] 194 | #hasta aqui ok, header_value es el header que se ha marcado (primera columna), creo que este solo lo uso para subrayado en el textarea 195 | 196 | global host_endpoint 197 | global endpoint_table 198 | endpoint_table = [] 199 | for (host, endpoint) in host_endpoint: 200 | 201 | if clicked_host == host:# and endpoint not in endpoint_table: 202 | endpoint_table.append([endpoint]) 203 | 204 | 205 | ###global burp_extender_instance #variable global que representa la instancia de IBurpExtender que se crea al cargar la extension. se usa para acceder desde fuera (especialmente desde el mouse event handler para actualizar la endpoint_table) a propiedades y metodos de la instancia "principal" de la extension. el valor se lo doy dentro de la intancia, igualando esta variable a self 206 | global selected_header_name 207 | selected_header_name = header_value 208 | 209 | burp_extender_instance.selected_host = clicked_host 210 | burp_extender_instance.selected_header = header_value#header # este lo settea ok para la de endpoints 211 | burp_extender_instance.endpoint_table1 = endpoint_table 212 | burp_extender_instance.update_endpoints(endpoint_table) 213 | 214 | class IssueTableMouseListener_Endpoints(IssueTableMouseListener): 215 | """Adds functionality to the click actions on rows of the "Unique endpoints" and "All endpoints" tables.""" 216 | def extra_symbol(self, head): 217 | 218 | if head.split(": ")[0].lower() in self.security_headers: 219 | extra_symbol = ' [ + ] ' 220 | elif head.split(": ")[0].lower() in self.dangerous_headers: 221 | extra_symbol = ' [ X ] ' 222 | elif head.split(": ")[0].lower() in self.potentially_dangerous_headers: 223 | extra_symbol = ' [ ? ] ' 224 | else: 225 | extra_symbol = "" 226 | return extra_symbol 227 | 228 | 229 | def mouseClicked(self, event): 230 | if event.getClickCount() == 1: 231 | tbl = event.getSource() 232 | 233 | burp_extender_instance.clicked_endpoint(tbl, True) 234 | 235 | class summary_unique_mouse_listener(IssueTableMouseListener): 236 | def mouseClicked(self, event): 237 | print('summary table clicked') 238 | 239 | class summary_all_mouse_listener(IssueTableMouseListener): 240 | def mouseClicked(self, event): 241 | print('summary all clicked, go to history and show request/response') 242 | 243 | class SummaryTableModel_left(DefaultTableModel): 244 | def __init__(self, data, headings): 245 | DefaultTableModel.__init__(self, data, headings) 246 | 247 | def getColumnClass(self, col): 248 | # columnas: add to report?, Host 249 | return [Boolean, String][col] 250 | 251 | def isCellEditable(self, row, column): 252 | """Returns True if cells are editable.""" 253 | canEdit = [True, False] 254 | return canEdit[column] 255 | 256 | class SummaryTableModel_right(DefaultTableModel): 257 | def __init__(self, data, headings): 258 | DefaultTableModel.__init__(self, data, headings) 259 | 260 | def getColumnClass(self, col): 261 | # columnas: history index, add to report?, issue type, host, unique endpoint 262 | # issue types: 263 | # - missing security headers 264 | # - dangerous 265 | # - potentially dangerous 266 | # - http verbs 267 | # - cookies without flags 268 | 269 | return [Boolean, String, String, String][col] 270 | #return [Integer, Boolean, String, String, String][col] 271 | 272 | def isCellEditable(self, row, column): 273 | """Returns True if cells are editable.""" 274 | canEdit = [True, False, False, False, False] 275 | return canEdit[column] 276 | 277 | class IssueTable(JTable): 278 | """Table class for the tables used in the extension. Needed to give the capacity to tables to perform actions when their rows are clicked.""" 279 | def __init__(self, model, table_type): 280 | self.setModel(model) 281 | self.getTableHeader().setReorderingAllowed(False) 282 | if table_type == "tab": 283 | self.addMouseListener(IssueTableMouseListener_Tab()) 284 | elif table_type == "meta": 285 | self.addMouseListener(IssueTableMouseListener_Meta()) 286 | elif table_type == "window": 287 | self.addMouseListener(IssueTableMouseListener_Window()) 288 | elif table_type == "endpoints": 289 | self.addMouseListener(IssueTableMouseListener_Endpoints()) 290 | elif table_type == "config_headers": 291 | pass 292 | elif table_type == "summary_unique_endpoints": 293 | self.addMouseListener(summary_unique_mouse_listener()) 294 | elif table_type == "summary_all_endpoints": 295 | self.addMouseListener(summary_all_mouse_listener()) 296 | 297 | '''def getTableCellRendererComponent( 298 | self, 299 | table, # JTable - table containing value 300 | value, # Object - value being rendered 301 | isSelected, # boolean - Is value selected? 302 | hasFocus, # boolean - Does this cell have focus? 303 | row, # int - Row # (0..N) 304 | col # int - Col # (0..N) 305 | ) : 306 | comp = self.DTCR.getTableCellRendererComponent( 307 | table, value, isSelected, hasFocus, row, col 308 | ) 309 | result = self.result 310 | result.setText(value) 311 | result.putClientProperty("html.disable", None) 312 | 313 | return result''' 314 | 315 | 316 | 317 | 318 | 319 | class BurpExtender(IBurpExtender, IContextMenuFactory, ITab): 320 | """Main class of the Headers extension, instantiated by Burp.""" 321 | 322 | def apply_config(self): 323 | """Read the configuration file and load the configurations. It is run when the extension loads.""" 324 | print("Applying config...") 325 | f = open("config.txt","r") 326 | for line in f.readlines(): 327 | feature = line.split(' -- ')[0] 328 | value = line.split(' -- ')[1].strip('\n') 329 | if feature == "last_save_type": 330 | self.save_format.setSelectedItem(value) 331 | self.config_dict[feature] = value 332 | elif feature == "last_output_file": 333 | self.save_path.setText(value) 334 | self.config_dict[feature] = value 335 | elif feature == "last_filter_type": 336 | self.preset_filters.setSelectedItem(value) 337 | self.config_dict[feature] = value 338 | elif feature == "UI_theme": 339 | self.UI_theme = value 340 | self.config_dict[feature] = value 341 | f.close() 342 | f = open('security_headers.txt','r') 343 | self.total_security_headers = len(f.readlines()) 344 | f.close() 345 | f = open('potentially_dangerous_headers.txt','r') 346 | self.total_potential_headers = len(f.readlines()) 347 | f.close() 348 | f = open('dangerous_headers.txt','r') 349 | self.total_dangerous_headers = len(f.readlines()) 350 | f.close() 351 | f = open('cookie_flags.txt','r') 352 | self.total_cookie_flags = len(f.readlines()) 353 | f.close() 354 | 355 | # UI colors 356 | if self.UI_theme == "dark": 357 | f = open('UI_theme_dark.txt', 'r') 358 | elif self.UI_theme == "light": 359 | f = open('UI_theme_light.txt', 'r') 360 | for k, line in enumerate(f.readlines()): 361 | if k == 0: 362 | self.color1 = line.strip('\n') # for titles and dashed lines 363 | elif k == 1: 364 | self.color2 = line.strip('\n') # for security headers 365 | elif k == 2: 366 | self.color3 = line.strip('\n') # for potentially dangerous headers 367 | elif k == 3: 368 | self.color4 = line.strip('\n') # for dangerous or verbose headers 369 | elif k == 4: 370 | self.color5 = line.strip('\n') # for the rest of headers 371 | elif k == 5: 372 | self.color6 = line.strip('\n') # for the wildcards in the unique endpoints URL 373 | f.close() 374 | 375 | def update_config(self):#, feature, value): 376 | """Update the configuration file with values supplied in the configuration panel of the extension""" 377 | f = open("config.txt", "w") 378 | for key in list(self.config_dict.keys()): 379 | f.write(key + " -- " + self.config_dict[key] + "\n") #comprobar 380 | f.close() 381 | 382 | def read_headers(self): 383 | """ Read the values currently checked in the advanced config tables and use them in the future """ 384 | 385 | self.table_config_security = [] 386 | self.table_config_dangerous = [] 387 | self.table_config_potentially_dangerous = [] 388 | self.table_config_cookie_flags = [] 389 | 390 | 391 | for i in range(self.initial_count_security_headers): 392 | if self.model_tab_config_security.getValueAt(i,0): 393 | self.table_config_security.append([True, self.model_tab_config_security.getValueAt(i,1)]) 394 | 395 | for i in range(self.initial_count_dangerous_headers): 396 | if self.model_tab_config_dangerous.getValueAt(i,0): 397 | self.table_config_dangerous.append([True, self.model_tab_config_dangerous.getValueAt(i,1)]) 398 | 399 | for i in range(self.initial_count_potentially_dangerous_headers): 400 | if self.model_tab_config_potentially_dangerous.getValueAt(i,0): 401 | self.table_config_potentially_dangerous.append([True, self.model_tab_config_potentially_dangerous.getValueAt(i,1)]) 402 | 403 | for i in range(self.initial_count_cookie_flags): 404 | if self.model_tab_config_cookie_flags.getValueAt(i,0): 405 | self.table_config_cookie_flags.append([True, self.model_tab_config_cookie_flags.getValueAt(i,1)]) 406 | 407 | self.dangerous_headers = [] 408 | self.security_headers = [] 409 | self.potentially_dangerous_headers = [] 410 | self.cookie_flags = [] 411 | 412 | for line in self.table_config_security: 413 | self.security_headers.append(line[1].strip('\n').lower()) 414 | 415 | for line in self.table_config_dangerous: 416 | self.dangerous_headers.append(line[1].strip('\n')) 417 | 418 | for line in self.table_config_potentially_dangerous: 419 | self.potentially_dangerous_headers.append(line[1].strip('\n')) 420 | 421 | for line in self.table_config_cookie_flags: 422 | self.cookie_flags.append(line[1].strip('\n')) 423 | 424 | def make_chosen_headers_permanent(self, event): 425 | self.security_headers = [] 426 | f = open("security_headers.txt","w") 427 | for i in range(self.initial_count_security_headers): 428 | #print(str(i) + '/' + str(self.initial_count_security_headers)) 429 | if i < self.initial_count_security_headers - 1: 430 | if self.model_tab_config_security.getValueAt(i,0): 431 | f.write("1 " + self.model_tab_config_security.getValueAt(i,1) + '\n') 432 | self.security_headers.append(self.model_tab_config_security.getValueAt(i,1)) 433 | else: 434 | f.write("0 " + self.model_tab_config_security.getValueAt(i,1) + '\n') 435 | else: 436 | if self.model_tab_config_security.getValueAt(i,0): 437 | f.write("1 " + self.model_tab_config_security.getValueAt(i,1)) 438 | self.security_headers.append(self.model_tab_config_security.getValueAt(i,1)) 439 | else: 440 | f.write("0 " + self.model_tab_config_security.getValueAt(i,1)) 441 | 442 | f.close() 443 | 444 | f = open("dangerous_headers.txt","w") 445 | self.dangerous_headers = [] 446 | for i in range(self.initial_count_dangerous_headers): 447 | if i < self.initial_count_dangerous_headers - 1: 448 | if self.model_tab_config_dangerous.getValueAt(i,0): 449 | f.write("1 " + self.model_tab_config_dangerous.getValueAt(i,1) + '\n') 450 | self.dangerous_headers.append(self.model_tab_config_dangerous.getValueAt(i,1)) 451 | else: 452 | f.write("0 " + self.model_tab_config_dangerous.getValueAt(i,1) + '\n') 453 | else: 454 | if self.model_tab_config_dangerous.getValueAt(i,0): 455 | f.write("1 " + self.model_tab_config_dangerous.getValueAt(i,1)) 456 | self.dangerous_headers.append(self.model_tab_config_dangerous.getValueAt(i,1)) 457 | else: 458 | f.write("0 " + self.model_tab_config_dangerous.getValueAt(i,1)) 459 | 460 | f.close() 461 | 462 | f = open("potentially_dangerous_headers.txt","w") 463 | self.potentially_dangerous_headers = [] 464 | for i in range(self.initial_count_potentially_dangerous_headers): 465 | if i < self.initial_count_potentially_dangerous_headers - 1: 466 | if self.model_tab_config_potentially_dangerous.getValueAt(i,0): 467 | f.write("1 " + self.model_tab_config_potentially_dangerous.getValueAt(i,1) + '\n') 468 | self.potentially_dangerous_headers.append(self.model_tab_config_potentially_dangerous.getValueAt(i,1)) 469 | else: 470 | f.write("0 " + self.model_tab_config_potentially_dangerous.getValueAt(i,1) + '\n') 471 | else: 472 | if self.model_tab_config_potentially_dangerous.getValueAt(i,0): 473 | f.write("1 " + self.model_tab_config_potentially_dangerous.getValueAt(i,1)) 474 | self.potentially_dangerous_headers.append(self.model_tab_config_potentially_dangerous.getValueAt(i,1)) 475 | else: 476 | f.write("0 " + self.model_tab_config_potentially_dangerous.getValueAt(i,1)) 477 | f.close() 478 | 479 | f = open("cookie_flags.txt","w") 480 | self.cookie_flags = [] 481 | for i in range(self.initial_count_cookie_flags): 482 | if i < self.initial_count_cookie_flags - 1: 483 | if self.model_tab_config_cookie_flags.getValueAt(i,0): 484 | f.write("1 " + self.model_tab_config_cookie_flags.getValueAt(i,1) + '\n') 485 | self.cookie_flags.append(self.model_tab_config_cookie_flags.getValueAt(i,1)) 486 | else: 487 | f.write("0 " + self.model_tab_config_cookie_flags.getValueAt(i,1) + '\n') 488 | else: 489 | if self.model_tab_config_cookie_flags.getValueAt(i,0): 490 | f.write("1 " + self.model_tab_config_cookie_flags.getValueAt(i,1)) 491 | self.cookie_flags.append(self.model_tab_config_cookie_flags.getValueAt(i,1)) 492 | else: 493 | f.write("0 " + self.model_tab_config_cookie_flags.getValueAt(i,1)) 494 | f.close() 495 | 496 | def create_extra_info_window(self): 497 | # el extra info window lo defino aqui fuera para que exista desde un principio y al hacer doble click en las tablas solamente se haga visible, pero no se ee un nuevo frame por cada doble click 498 | self.extra_info = JFrame("Extended header info") 499 | self.extra_info_panel = JPanel() 500 | self.extra_info_panel.setLayout(BoxLayout(self.extra_info_panel, BoxLayout.Y_AXIS ) ) 501 | self.extra_info.setSize(400, 350) 502 | self.extra_info.setLocation(840, 0) 503 | self.extra_info.toFront() 504 | self.extra_info.setAlwaysOnTop(True) 505 | 506 | self.extra_info_label1 = JLabel("Header Name:") 507 | self.extra_info_label1.putClientProperty("html.disable", None) 508 | #extra_info_label1 = JLabel("Header Name:".format(self.color1)) 509 | self.extra_info_label1.setAlignmentX(JLabel.LEFT_ALIGNMENT) 510 | self.extra_info_textarea1 = JTextArea("Header Name", rows=1, editable=False) 511 | self.extra_info_textarea1.setLineWrap(True) 512 | self.scrollPane_1 = JScrollPane(self.extra_info_textarea1) 513 | self.scrollPane_1.setAlignmentX(JScrollPane.LEFT_ALIGNMENT) 514 | 515 | self.extra_info_label2 = JLabel("Header Description:") 516 | self.extra_info_label2.putClientProperty("html.disable", None) 517 | #extra_info_label2 = JLabel("Header Description:".format(self.color1)) 518 | self.extra_info_label2.setAlignmentX(JLabel.LEFT_ALIGNMENT) 519 | self.extra_info_textarea2 = JTextArea("Description",rows=5, editable=False) 520 | self.extra_info_textarea2.setLineWrap(True) 521 | self.scrollPane_2 = JScrollPane(self.extra_info_textarea2) 522 | self.scrollPane_2.setAlignmentX(JScrollPane.LEFT_ALIGNMENT) 523 | 524 | self.extra_info_label3 = JLabel("Usage example:") 525 | self.extra_info_label3.putClientProperty("html.disable", None) 526 | #extra_info_label3 = JLabel("Usage example:".format(self.color1)) 527 | self.extra_info_label3.setAlignmentX(JLabel.LEFT_ALIGNMENT) 528 | self.extra_info_textarea3 = JTextArea("Example",rows=3, editable=False) 529 | self.extra_info_textarea3.setLineWrap(True) 530 | self.scrollPane_3 = JScrollPane(self.extra_info_textarea3) 531 | self.scrollPane_3.setAlignmentX(JScrollPane.LEFT_ALIGNMENT) 532 | 533 | self.extra_info_label4 = JLabel("URL describing header:") 534 | self.extra_info_label4.putClientProperty("html.disable", None) 535 | #extra_info_label4 = JLabel("URL describing header:".format(self.color1)) 536 | self.extra_info_label4.setAlignmentX(JLabel.LEFT_ALIGNMENT) 537 | self.extra_info_textarea4 = JTextArea("URL2",rows=2, editable=False) 538 | self.extra_info_textarea4.setLineWrap(True) 539 | self.scrollPane_4 = JScrollPane(self.extra_info_textarea4) 540 | self.scrollPane_4.setAlignmentX(JScrollPane.LEFT_ALIGNMENT) 541 | 542 | self.extra_info_label5 = JLabel("Potential risks associated with header:") 543 | self.extra_info_label5.putClientProperty("html.disable", None) 544 | #extra_info_label5 = JLabel("Potential risks associated with header:".format(self.color1)) 545 | self.extra_info_label5.setAlignmentX(JLabel.LEFT_ALIGNMENT) 546 | self.extra_info_textarea5 = JTextArea("There are no potential risks associated with this header",rows=3, editable=False) 547 | self.extra_info_textarea5.setLineWrap(True) 548 | self.scrollPane_5 = JScrollPane(self.extra_info_textarea5) 549 | self.scrollPane_5.setAlignmentX(JScrollPane.LEFT_ALIGNMENT) 550 | 551 | for element in [self.extra_info_label1, self.scrollPane_1, self.extra_info_label2, self.scrollPane_2, self.extra_info_label3, self.scrollPane_3, self.extra_info_label4, self.scrollPane_4, self.extra_info_label5, self.scrollPane_5]: 552 | self.extra_info_panel.add(element) 553 | 554 | self.extra_info.add(self.extra_info_panel) 555 | self.dict_req_headers = {} 556 | self.req_headers_description = open('request_headers.txt','r') 557 | for line in self.req_headers_description.readlines(): 558 | line_split = line.split('&&') 559 | header_name = line_split[0] 560 | 561 | header_description = line_split[1] 562 | if header_description.rstrip() == '': 563 | header_description = 'Description unavailable for header: ' + header_name 564 | 565 | header_example = line_split[2] 566 | if header_example.rstrip() == '': 567 | header_example = 'Example unavailable for header: ' + header_name 568 | 569 | header_url = line_split[3] 570 | if header_url.rstrip() == '': 571 | header_url = 'URL unavailable for header ' + header_name 572 | 573 | header_risk = line_split[4] 574 | if header_risk.rstrip() == '': 575 | header_risk = 'Potential risks information unavailable for header ' + header_name 576 | self.dict_req_headers[header_name] = (header_description, header_example, header_url, header_risk) 577 | self.req_headers_description.close() 578 | 579 | self.dict_resp_headers = {} 580 | self.resp_headers_description = open('response_headers.txt','r') 581 | for line in self.resp_headers_description.readlines(): 582 | line_split = line.split('&&') 583 | header_name = line_split[0] 584 | 585 | header_description = line_split[1] 586 | if header_description.rstrip() == '': 587 | header_description = 'Description unavailable for header: ' + header_name 588 | 589 | header_example = line_split[2] 590 | if header_example.rstrip() == '': 591 | header_example = 'Example unavailable for header: ' + header_name 592 | 593 | header_url = line_split[3] 594 | if header_url.rstrip() == '': 595 | header_url = 'URL unavailable for header ' + header_name 596 | 597 | header_risk = line_split[4] 598 | if header_risk.rstrip() == '': 599 | header_risk = 'Potential risks information unavailable for header ' + header_name 600 | self.dict_resp_headers[header_name] = (header_description, header_example, header_url, header_risk) 601 | self.resp_headers_description.close() 602 | 603 | def compile_regex(self): 604 | """Compile regular expressions that will be used later by the extension to match URL parameters""" 605 | #matchea lo que haya entre = y & o entre = y ' ', para el ultimo parametro de la linea 606 | self.query_params = re.compile('=.*?&|=.*? ') 607 | 608 | # matchea numeros en la url tipo /asdf/1234/qwe/1234, matchearia los dos 1234 y secuencias de letras, numeros y guiones o puntos. igual algun caso raro se cuela, pero por lo que he visto pilla todo 609 | self.number_between_forwardslash = re.compile('\/[a-zA-Z]*\d+[a-zA-Z0-9-_\.]*') 610 | 611 | # match de headers 612 | self.meta = re.compile('') 613 | 614 | def find_host(self, req_headers): 615 | """Given a request-response object, find the Host to which it was requested""" 616 | for req_head in req_headers[1:]: 617 | if 'Host: ' in req_head: 618 | host = req_head.split(': ')[1] 619 | break 620 | return host 621 | 622 | def restore_save_thresholds_func(self, event): 623 | f = open('thresholds.txt', 'r') 624 | thresholds = f.readlines() 625 | f.close() 626 | 627 | check_box_security_value = thresholds[0].split(' ')[0] 628 | check_box_potentially_dangerous_value = thresholds[1].split(' ')[0] 629 | check_box_dangerous_value = thresholds[2].split(' ')[0] 630 | 631 | threshold_security_value = thresholds[0].split(' ')[1] 632 | threshold_potentially_dangerous_value = thresholds[1].split(' ')[1] 633 | threshold_dangerous_value = thresholds[2].split(' ')[1] 634 | 635 | security_checkbox_state = True if check_box_security_value == '1' else False 636 | potentially_dangerous_checkbox_state = True if check_box_potentially_dangerous_value == '1' else False 637 | dangerous_checkbox_state = True if check_box_dangerous_value == '1' else False 638 | 639 | self.check_box_security.setSelected(security_checkbox_state) 640 | self.check_box_potentially_dangerous.setSelected(potentially_dangerous_checkbox_state) 641 | self.check_box_dangerous.setSelected(dangerous_checkbox_state) 642 | 643 | self.threshold_count_security.setText(threshold_security_value) 644 | self.threshold_count_potentially_dangerous.setText(threshold_potentially_dangerous_value) 645 | self.threshold_count_dangerous.setText(threshold_dangerous_value) 646 | 647 | def save_threshold_config_func(self, event): 648 | f = open('thresholds.txt', 'w') 649 | selected_security = '1' if self.check_box_security.isSelected() == True else '0' 650 | selected_potentially_dangerous = '1' if self.check_box_potentially_dangerous.isSelected() == True else '0' 651 | selected_dangerous = '1' if self.check_box_dangerous.isSelected() == True else '0' 652 | 653 | f.write(selected_security + ' ' + self.threshold_count_security.getText() + '\n') 654 | f.write(selected_potentially_dangerous + ' ' + self.threshold_count_potentially_dangerous.getText() + '\n') 655 | f.write(selected_dangerous + ' ' + self.threshold_count_dangerous.getText()) 656 | 657 | f.close() 658 | return 659 | 660 | def reset_threshold_config_func(self, event): 661 | self.threshold_count_security.setText(str(self.initial_count_security_headers)) 662 | self.threshold_count_potentially_dangerous.setText("{}".format(self.initial_count_potentially_dangerous_headers)) 663 | self.threshold_count_dangerous.setText("{}".format(self.initial_count_dangerous_headers)) 664 | 665 | self.check_box_security.setSelected(False) 666 | self.check_box_dangerous.setSelected(False) 667 | self.check_box_potentially_dangerous.setSelected(False) 668 | return 669 | 670 | def create_advanced_config_frame(self): 671 | self.advanced_config_panel = JFrame("Advanced configuration") 672 | self.advanced_config_panel.setLayout(BorderLayout()) 673 | self.advanced_config_panel.toFront() 674 | self.advanced_config_panel.setAlwaysOnTop(True) 675 | self.advanced_config_panel.setSize(800, 600) 676 | self.advanced_config_panel.setLocationRelativeTo(None) 677 | 678 | # --------------------- Theme selection ------------------------# 679 | self.theme_model = DefaultComboBoxModel() 680 | self.theme_model.addElement("Dark") 681 | self.theme_model.addElement("Light") 682 | theme_selector = JComboBox(self.theme_model) 683 | # ----------------------------------------------------------------# 684 | 685 | 686 | 687 | # --------------------- Headers selection ------------------------# 688 | self.table_config_security = [] 689 | self.table_config_potentially_dangerous = [] 690 | self.table_config_dangerous = [] 691 | self.table_config_cookie_flags = [] 692 | 693 | f = open('security_headers.txt','r') 694 | for line in f.readlines(): 695 | active = line.split(' ')[0] 696 | if active == '1': 697 | self.table_config_security.append([True, line.split(' ')[1].strip('\n')]) 698 | else: 699 | self.table_config_security.append([False, line.split(' ')[1].strip('\n')]) 700 | f.close() 701 | 702 | f = open('potentially_dangerous_headers.txt','r') 703 | for line in f.readlines(): 704 | active = line.split(' ')[0] 705 | if active == '1': 706 | self.table_config_potentially_dangerous.append([True, line.split(' ')[1].strip('\n')]) 707 | else: 708 | self.table_config_potentially_dangerous.append([False, line.split(' ')[1].strip('\n')]) 709 | f.close() 710 | 711 | f = open('dangerous_headers.txt','r') 712 | for line in f.readlines(): 713 | active = line.split(' ')[0] 714 | if active == '1': 715 | self.table_config_dangerous.append([True, line.split(' ')[1].strip('\n')]) 716 | else: 717 | self.table_config_dangerous.append([False, line.split(' ')[1].strip('\n')]) 718 | f.close() 719 | 720 | f = open('cookie_flags.txt','r') 721 | for line in f.readlines(): 722 | active = line.split(' ')[0] 723 | if active == '1': 724 | self.table_config_cookie_flags.append([True, line.split(' ')[1].strip('\n')]) 725 | else: 726 | self.table_config_cookie_flags.append([False, line.split(' ')[1].strip('\n')]) 727 | f.close() 728 | 729 | self.config_column_names = ("Use?", "Header name") 730 | self.config_column_names_flags = ("Use?", "Flag name") 731 | 732 | self.model_tab_config_security = ConfigTableModel(self.table_config_security, self.config_column_names) 733 | self.table_tab_config_security = JTable(self.model_tab_config_security) 734 | 735 | self.model_tab_config_potentially_dangerous = ConfigTableModel(self.table_config_potentially_dangerous, self.config_column_names) 736 | self.table_tab_config_potentially_dangerous = JTable(self.model_tab_config_potentially_dangerous) 737 | 738 | self.model_tab_config_dangerous = ConfigTableModel(self.table_config_dangerous, self.config_column_names) 739 | self.table_tab_config_dangerous = JTable(self.model_tab_config_dangerous) 740 | 741 | self.model_tab_config_cookie_flags = ConfigTableModel(self.table_config_cookie_flags, self.config_column_names_flags) 742 | self.table_tab_config_cookie_flags = JTable(self.model_tab_config_cookie_flags) 743 | 744 | self.table_tab_config_security.getColumnModel().getColumn(0).setMaxWidth(50) 745 | self.table_tab_config_security.getColumnModel().getColumn(1).setPreferredWidth(400) 746 | 747 | self.table_tab_config_potentially_dangerous.getColumnModel().getColumn(0).setMaxWidth(50) 748 | self.table_tab_config_potentially_dangerous.getColumnModel().getColumn(1).setPreferredWidth(400) 749 | 750 | self.table_tab_config_dangerous.getColumnModel().getColumn(0).setMaxWidth(50) 751 | self.table_tab_config_dangerous.getColumnModel().getColumn(1).setPreferredWidth(400) 752 | 753 | self.table_tab_config_cookie_flags.getColumnModel().getColumn(0).setMaxWidth(50) 754 | self.table_tab_config_cookie_flags.getColumnModel().getColumn(1).setPreferredWidth(400) 755 | 756 | c = GridBagConstraints() 757 | c.fill = GridBagConstraints.HORIZONTAL 758 | 759 | security_headers_tab = JPanel(GridBagLayout()) 760 | security_headers_tab.add(JScrollPane(self.table_tab_config_security), c) 761 | 762 | dangerous_headers_tab = JPanel(GridBagLayout()) 763 | dangerous_headers_tab.add(JScrollPane(self.table_tab_config_dangerous), c) 764 | 765 | dangerous_headers_tab = JPanel(GridBagLayout()) 766 | dangerous_headers_tab.add(JScrollPane(self.table_tab_config_cookie_flags), c) 767 | 768 | potentially_dangerous_headers_tab = JPanel(GridBagLayout()) 769 | potentially_dangerous_headers_tab.add(JScrollPane(self.table_tab_config_potentially_dangerous), c) 770 | # ----------------------------------------------------------------# 771 | 772 | 773 | # ------------------ Add contents to main tabs -------------------# 774 | aux_panel = JPanel(BorderLayout()) 775 | theme_panel = JPanel(GridBagLayout()) 776 | 777 | # Add buttons at the bottom inside a panel 778 | add_header_to_category_button = JButton("Add header to category", actionPerformed = self.add_headers_to_categories) 779 | add_header_to_category_button.putClientProperty("html.disable", None) 780 | add_header_to_category_button.setForeground(Color.WHITE) 781 | add_header_to_category_button.setBackground(Color(10,101,247)) 782 | 783 | remove_header_from_category_button = JButton("Remove header from category", actionPerformed = self.remove_headers_from_categories) 784 | remove_header_from_category_button.putClientProperty("html.disable", None) 785 | remove_header_from_category_button.setForeground(Color.WHITE) 786 | remove_header_from_category_button.setBackground(Color(210,101,47))#Color(10,101,247)) Color(210,101,47) 787 | 788 | make_curr_selection_permanent_button = JButton("Apply changes", actionPerformed = self.make_chosen_headers_permanent) 789 | make_curr_selection_permanent_button.putClientProperty("html.disable", None) 790 | make_curr_selection_permanent_button.setForeground(Color.WHITE) 791 | make_curr_selection_permanent_button.setBackground(Color(10,101,247)) 792 | 793 | button_panel = JPanel(GridBagLayout()) 794 | e = GridBagConstraints() 795 | e.fill = GridBagConstraints.HORIZONTAL 796 | e.gridx = 0 797 | e.weightx = 1 798 | e.gridy = 0 799 | button_panel.add(add_header_to_category_button, e) 800 | e.gridx += 1 801 | button_panel.add(remove_header_from_category_button, e) 802 | e.gridx += 1 803 | button_panel.add(make_curr_selection_permanent_button, e) 804 | 805 | 806 | # Fill the tables for each category 807 | self.categories_tabs = JTabbedPane() 808 | self.categories_tabs.add("Security headers", JScrollPane(self.table_tab_config_security)) 809 | self.categories_tabs.add("Potentially dangerous headers", JScrollPane(self.table_tab_config_potentially_dangerous)) 810 | self.categories_tabs.add("Dangerous headers", JScrollPane(self.table_tab_config_dangerous)) 811 | self.categories_tabs.add("Cookie Flags", JScrollPane(self.table_tab_config_cookie_flags)) 812 | aux_panel.add(self.categories_tabs, BorderLayout.CENTER) 813 | aux_panel.add(button_panel, BorderLayout.SOUTH) 814 | 815 | 816 | #--------------- threshold panel --------------------- 817 | threshold_panel = JPanel(GridBagLayout()) 818 | c = GridBagConstraints() 819 | c.anchor = GridBagConstraints.WEST 820 | c.gridx = 0 821 | c.gridy = 0 822 | c.weightx = 1 823 | c.fill = GridBagConstraints.HORIZONTAL 824 | threshold_panel.add(JLabel("Use threshold for Security headers?"), c) 825 | c.gridy += 1 826 | threshold_panel.add(JLabel("Use threshold for Potentially Dangerous headers?"), c) 827 | c.gridy += 1 828 | threshold_panel.add(JLabel("Use threshold for Dangerous headers?"), c) 829 | 830 | c.gridx = 1 831 | c.gridy = 0 832 | self.check_box_security = JCheckBox() 833 | threshold_panel.add(self.check_box_security, c) 834 | c.gridy += 1 835 | self.check_box_potentially_dangerous = JCheckBox() 836 | threshold_panel.add(self.check_box_potentially_dangerous, c) 837 | c.gridy += 1 838 | self.check_box_dangerous = JCheckBox() 839 | threshold_panel.add(self.check_box_dangerous, c) 840 | 841 | c.gridx = 2 842 | c.gridy = 0 843 | self.threshold_count_security = JTextField("{}".format(len(self.table_config_security))) 844 | threshold_panel.add(self.threshold_count_security, c) 845 | c.gridy += 1 846 | self.threshold_count_potentially_dangerous = JTextField("{}".format(len(self.table_config_potentially_dangerous))) 847 | threshold_panel.add(self.threshold_count_potentially_dangerous, c) 848 | 849 | c.gridy += 1 850 | self.threshold_count_dangerous = JTextField("{}".format(len(self.table_config_dangerous))) 851 | threshold_panel.add(self.threshold_count_dangerous, c) 852 | 853 | c.gridx = 0 854 | c.gridy += 1 855 | save_threshold_config = JButton("Save thresholds", actionPerformed = self.save_threshold_config_func) 856 | threshold_panel.add(save_threshold_config, c) 857 | 858 | c.gridx += 1 859 | restore_threshold_config = JButton("Restore saved thresholds", actionPerformed = self.restore_save_thresholds_func) 860 | threshold_panel.add(restore_threshold_config, c) 861 | 862 | c.gridx += 1 863 | reset_threshold_config = JButton("Reset to default", actionPerformed = self.reset_threshold_config_func) 864 | threshold_panel.add(reset_threshold_config, c) 865 | 866 | 867 | 868 | # Theme panel contents 869 | d = GridBagConstraints() 870 | d.fill = GridBagConstraints.HORIZONTAL 871 | d.gridx = 0 872 | d.gridy = 0 873 | theme_panel.add(JLabel("If you use Burp's dark theme, you will probably see better this extension by selecting 'dark', and vice versa."), d) 874 | d.gridy += 1 875 | theme_panel.add(theme_selector, d) 876 | 877 | 878 | # add the main tabs 879 | self.main_tabs = JTabbedPane() 880 | self.main_tabs.addTab('Configure headers criteria', aux_panel) 881 | self.main_tabs.addTab('Configure thresholds', threshold_panel) 882 | self.main_tabs.addTab('Theme', theme_panel) 883 | 884 | self.advanced_config_panel.add(self.main_tabs, BorderLayout.CENTER) 885 | # ----------------------------------------------------------------# 886 | 887 | return 888 | 889 | def show_advanced_config(self, event): 890 | """Show the advanced configuration window when clicking the gear button""" 891 | self.advanced_config_panel.setVisible(True) 892 | 893 | def get_categories_headers_length(self): 894 | """ Get how many headers are in each category when the extension is loaded. Used in read_headers() 895 | to tell it how many times it must loop to generate arrays of each category of headers""" 896 | 897 | f = open('security_headers.txt','r') 898 | # the filter in the next line removes all occurences of '', i.e. doesnt consider empty lines for counting the number of headers 899 | self.initial_count_security_headers = len(list(filter(('').__ne__, f.readlines()))) 900 | #self.initial_count_security_headers = len(f.readlines()) 901 | f.close() 902 | 903 | f = open('dangerous_headers.txt','r') 904 | self.initial_count_dangerous_headers= len(list(filter(('').__ne__, f.readlines()))) 905 | #self.initial_count_dangerous_headers= len(f.readlines()) 906 | f.close() 907 | 908 | f = open('potentially_dangerous_headers.txt','r') 909 | self.initial_count_potentially_dangerous_headers = len(list(filter(('').__ne__, f.readlines()))) 910 | #self.initial_count_potentially_dangerous_headers = len(f.readlines()) 911 | f.close() 912 | 913 | f = open('cookie_flags.txt','r') 914 | self.initial_count_cookie_flags = len(list(filter(('').__ne__, f.readlines()))) 915 | #self.initial_count_dangerous_headers= len(f.readlines()) 916 | f.close() 917 | 918 | def check_python_modules(self, event): 919 | # subprocess que guarde en una variable output de python -c import ... 920 | 921 | python_path = self.python_path_textfield.getText() 922 | os_type = sys.platform.getshadow() 923 | win_python_command = "py --version" 924 | linux_python_command = 'python3 --version' 925 | win_python_command = 'py docx.py' 926 | if 'win' in os_type: 927 | #proc = subprocess.Popen(win_python_command, stdout=subprocess.PIPE) 928 | if python_path != '': 929 | proc = subprocess.Popen("{} --version".format(python_path), stdout=subprocess.PIPE) 930 | else: 931 | proc = subprocess.Popen(["py","--version"], stdout=subprocess.PIPE) 932 | elif 'linux' in os_type: 933 | #proc = subprocess.Popen(linux_python_command, stdout=subprocess.PIPE) 934 | if python_path != '': 935 | proc = subprocess.Popen(["{}".format(python_path),"--version"], stdout=subprocess.PIPE) 936 | else: 937 | proc = subprocess.Popen(["python3","--version"], stdout=subprocess.PIPE) 938 | else: 939 | raise("Error identifying operating system type. Provide path to Python3 binary.") 940 | 941 | 942 | if 'docxtpl' not in sys.modules.keys(): 943 | os.system('pip install docxtpl') 944 | 945 | if python_path != '': 946 | docxtpl_version = subprocess.Popen(["{}".format(python_path),"-c","import docxtpl; print('Docxtpl version:',docxtpl.__version__)"],stdout=subprocess.PIPE) 947 | else: 948 | docxtpl_version = subprocess.Popen(["python3","-c","import docxtpl; print('Docxtpl version:',docxtpl.__version__)"],stdout=subprocess.PIPE) 949 | 950 | print(python_path) 951 | output = proc.stdout.read() 952 | self.python_msg.setText(output.strip('\r\n') + '; ' + docxtpl_version.stdout.read()) 953 | 954 | def create_docx_frame(self): 955 | self.docx_frame = JFrame("Configure .docx report") 956 | self.docx_frame.setLayout(GridBagLayout()) 957 | self.docx_frame.setAlwaysOnTop(True) 958 | self.docx_frame.setSize(800, 600) 959 | self.docx_frame.setLocationRelativeTo(None) 960 | self.docx_frame.toFront() 961 | self.docx_frame.setAlwaysOnTop(True) 962 | 963 | c = GridBagConstraints() 964 | c.gridx = 0 965 | c.weightx = 0 966 | c.gridy = 0 967 | self.docx_frame.add(JLabel('Make sure your Python3 installation runs on windows by typing "py" on Powershell or python3 or bash\n'), c) 968 | c.gridy += 1 969 | self.docx_frame.add(JLabel('If your python executable is in a custom path, please write it in the next textbox'), c) 970 | c.gridy += 1 971 | c.weightx = 1 972 | c.fill = GridBagConstraints.HORIZONTAL 973 | self.python_path_textfield = JTextField() 974 | self.docx_frame.add(self.python_path_textfield, c) 975 | 976 | c.gridy += 1 977 | self.docx_frame.add(JLabel(' ')) 978 | self.check_python_docx_modules = JButton("Check docx modules", actionPerformed = self.check_python_modules) 979 | self.docx_frame.add(self.check_python_docx_modules, c) 980 | 981 | c.gridy += 1 982 | self.export_docx = JButton("Export docx", actionPerformed = self.output_selected_summary) 983 | self.docx_frame.add(self.check_python_docx_modules, c) 984 | 985 | c.gridy += 1 986 | self.python_msg = JTextArea() 987 | self.docx_frame.add(self.python_msg, c) 988 | 989 | def registerExtenderCallbacks(self, callbacks): 990 | """Import Burp Extender callbacks and execute some preliminary functions for setting up the extension when it's loaded with proper configurations""" 991 | self._callbacks = callbacks 992 | self._helpers = callbacks.helpers 993 | callbacks.setExtensionName("Headers") 994 | callbacks.registerContextMenuFactory(self) 995 | callbacks.addSuiteTab(self) 996 | self.req_header_dict = {} 997 | self.resp_header_dict = {} 998 | self.for_table = [] # Items in this table will be shown in the Header-Host table (left side of the screen) for Requests and Responses headers 999 | self.header_host_table = [] # This holds data in three columns with Headers, Unique headers and Hosts and it's used for saving data to a file 1000 | self.for_req_table = [] 1001 | self.for_resp_table = [] 1002 | self.headers_already_in_table = [] 1003 | self.meta_headers_already_in_table = [] 1004 | self.last_len = 0 1005 | self.last_len_meta = 0 1006 | self.last_row = 0 1007 | self.last_row_meta = 0 1008 | ### global history1 # variable global con el history de requests 1009 | history1 = self._callbacks.getProxyHistory() 1010 | global burp_extender_instance 1011 | burp_extender_instance = self 1012 | self.config_dict = {} 1013 | self.apply_config() 1014 | self.compile_regex() 1015 | self.create_advanced_config_frame() 1016 | self.create_extra_info_window() 1017 | self.get_categories_headers_length() 1018 | self.read_headers() 1019 | self.create_docx_frame() 1020 | self.selected_host = "" 1021 | self.selected_header = "" 1022 | self.is_meta = False 1023 | self.dic_host_unique_endpoint = {} 1024 | 1025 | 1026 | return 1027 | 1028 | def getTabCaption(self): 1029 | """Name that will be shown in the extension's tab in the Burp interface""" 1030 | return "Headers" 1031 | 1032 | 1033 | 1034 | def ColorScore(self, value, total, type): 1035 | """ Make a color more intense if more headers of that type are present. To be used in the unique endpoints table.""" 1036 | 1037 | if type == "security": 1038 | if total != 0: #this is done to avoid dividing by zero. if there are no selected headers for a category, return brown color 1039 | score = value * 255.0 / total 1040 | if self.check_box_security.isSelected() and value >= int(self.threshold_count_security.getText()): 1041 | score = 255.0 1042 | elif self.check_box_security.isSelected() and value < int(self.threshold_count_security.getText()): 1043 | score = 0.0 1044 | elif score > 255.0: 1045 | score = 255.0 1046 | color = "#00{}00".format(hex(int(score)).split('0x')[1].zfill(2)) 1047 | # if there are no headers of this type show the symbol as brown, looks better 1048 | return color.replace("#000000", "#707070") 1049 | else: 1050 | return "#707070" 1051 | 1052 | elif type == "dangerous": 1053 | if total != 0: 1054 | score = value * 255.0 / total 1055 | if self.check_box_dangerous.isSelected() and value >= int(self.threshold_count_dangerous.getText()): 1056 | score = 255.0 1057 | elif self.check_box_dangerous.isSelected() and value < int(self.threshold_count_dangerous.getText()): 1058 | score = 0.0 1059 | elif score > 255.0: 1060 | score = 255.0 1061 | color = "#{}0000".format(hex(int(score)).split('0x')[1].zfill(2)) 1062 | return color.replace("#000000", "#707070") 1063 | else: 1064 | return "#707070" 1065 | 1066 | elif type == "potential": 1067 | if total != 0: 1068 | score = value * 255.0 / total 1069 | if self.check_box_potentially_dangerous.isSelected() and value >= int(self.threshold_count_potentially_dangerous.getText()): 1070 | score = 255.0 1071 | elif self.check_box_potentially_dangerous.isSelected() and value < int(self.threshold_count_potentially_dangerous.getText()): 1072 | score = 0.0 1073 | elif score > 255.0: 1074 | score = 255.0 1075 | R_factor = hex(int(int(0x4F) * score)).split('0x')[1] 1076 | G_factor = hex(int(int(0xC3) * score)).split('0x')[1] 1077 | B_factor = hex(int(int(0xF7) * score)).split('0x')[1] 1078 | color = "#{0}{1}{2}".format(R_factor.zfill(2), G_factor.zfill(2), B_factor.zfill(2)) 1079 | return color.replace("#000000", "#707070") 1080 | else: 1081 | return "#707070" 1082 | 1083 | 1084 | def extra_symbol(self, head): 1085 | """Creates the extra symbols for security [+], dangerous [X] and potentially dangrous [?] headers that are shown in the request header summary at the right side of the screen.""" 1086 | 1087 | 1088 | if head.split(": ")[0].lower() in self.security_headers: 1089 | extra_symbol = ' [ + ] ' 1090 | elif head.split(": ")[0].lower() in self.dangerous_headers: 1091 | extra_symbol = ' [ X ] ' 1092 | elif head.split(": ")[0].lower() in self.potentially_dangerous_headers: 1093 | extra_symbol = ' [ ? ] ' 1094 | else: 1095 | extra_symbol = "" 1096 | return extra_symbol 1097 | 1098 | def to_get_colors(self, url, host, from_click): 1099 | """Get the count of each symbol for the unique endpoints table. That number will be transformed into color strings by the self.ColorScore function""" 1100 | # get_colors == True -> just get how many symbols for categories there are. get_colors == False -> fill the summary 1101 | count_colors = {"dangerous":0, "security":0, "potential":0} #count how many of each categories are for a certain request/response pair. this is used in the unique endpoints table to add brighter or fainter color symbols 1102 | 1103 | if from_click: 1104 | val = url#val = tbl.getModel().getDataVector().elementAt(tbl.getSelectedRow()) 1105 | #else: 1106 | # val = tbl.getModel().getDataVector().elementAt(0) 1107 | 1108 | for item in history1: 1109 | request = burp_extender_instance._helpers.bytesToString(item.getRequest()).split('\r\n\r\n')[0] 1110 | req_headers = request.split('\r\n') 1111 | endpoint = req_headers[0] 1112 | 1113 | endpoint = self.apply_regex(endpoint) 1114 | 1115 | if endpoint == val: # si coincide un endpoint del history con el que hemos seleccionado 1116 | 1117 | host = self.find_host(req_headers) 1118 | 1119 | if host == burp_extender_instance.selected_host: # si coincide el host del history con el que clickamos en la tabla de headers. este if no es inutil?? bueno, creo que valdria si dos host distintos tienen un mismo endpoint 1120 | 1121 | for req_head in sorted(req_headers[1:]): # este for encuentra el Host header 1122 | 1123 | extra_symbol = self.extra_symbol(req_head) 1124 | if "[ + ]" in extra_symbol: 1125 | count_colors["security"] += 1 1126 | elif "[ X ]" in extra_symbol: 1127 | count_colors["dangerous"] += 1 1128 | elif "[ ? ]" in extra_symbol: 1129 | count_colors["potential"] += 1 1130 | 1131 | response = burp_extender_instance._helpers.bytesToString(item.getResponse()).split('\r\n\r\n')[0] 1132 | resp_headers = response.split('\r\n') 1133 | for resp_head in sorted(resp_headers[1:]): 1134 | 1135 | extra_symbol = self.extra_symbol(resp_head) 1136 | if "[ + ]" in extra_symbol: 1137 | count_colors["security"] += 1 1138 | elif "[ X ]" in extra_symbol: 1139 | count_colors["dangerous"] += 1 1140 | elif "[ ? ]" in extra_symbol: 1141 | count_colors["potential"] += 1 1142 | 1143 | break 1144 | 1145 | return count_colors 1146 | 1147 | def replace_symbol(self, replace_here): 1148 | """Replace the wildcard symbol with an html formatted one for colors. Implemented as a separate function becaused it is called several times, to avoid code duplication.""" 1149 | # it's important that the new symbol has spaces, some endpoints are so goddamn long that without spaces they don't fit and are completely hidden 1150 | replace_here = replace_here.replace('[*]',' * '.format(self.color6)) 1151 | #replace_here = replace_here.replace('[*]','[ * ]'.format(self.color6)) 1152 | return replace_here 1153 | 1154 | def clicked_endpoint(self, tbl, from_click): 1155 | """Fill the summary (panel at the right side of the extension tab) when an endpoint (either from the "Unique endpoints" table or from the "All endpoints table") is clicked. The summary contains the request and response headers, marked with symbols if they are security headers, dangerous headers, or potentially dangerous headers.""" 1156 | 1157 | # update the headers categories to be considered, by looking at the checkboxes in the advanced config table 1158 | self.read_headers() 1159 | 1160 | if from_click: 1161 | val = tbl.getModel().getDataVector().elementAt(tbl.getSelectedRow()) 1162 | else: 1163 | val = tbl.getModel().getDataVector().elementAt(0) 1164 | tbl.setRowSelectionInterval(0, 0) 1165 | 1166 | # lo siguiente es porque se selecciona (en principio) un endpoint de la lista de uniques, que han sido modificados, y hay que encontrar el bueno 1167 | for item in history1: 1168 | request = self._helpers.bytesToString(item.getRequest()).split('\r\n\r\n')[0] 1169 | req_headers = request.split('\r\n') 1170 | iter_host = self.find_host(req_headers) 1171 | endpoint = req_headers[0] 1172 | match = False 1173 | 1174 | 1175 | # the next three ifs are for matching to elements in the history if we choose from the unique endpoints or from all endpoints, must be processed differently for the comparison 1176 | if tbl.getModel() == self.model_unique_endpoints and self.is_meta == False and iter_host == self.selected_host: 1177 | match = self.replace_symbol(self.apply_regex(endpoint)) == val[0].split(' - ')[1].strip('').strip('') 1178 | 1179 | if tbl.getModel() == self.model_unique_endpoints and self.is_meta == True and iter_host == self.selected_host: 1180 | match = self.replace_symbol(self.apply_regex(endpoint)) == val[0] 1181 | 1182 | if tbl.getModel() == self.model_all_endpoints and iter_host == self.selected_host: 1183 | match = endpoint == val[0].strip('').strip('') 1184 | 1185 | if match: 1186 | host = self.find_host(req_headers) #del loop del history 1187 | clicked_header = self.selected_header 1188 | 1189 | # Run this block and exit this function if an item from the Meta headers tab was clicked 1190 | if self.is_meta: 1191 | metas = [] 1192 | request = self._helpers.bytesToString(item.getRequest()).split('\r\n\r\n')[0] 1193 | host = self.find_host(req_headers) #del loop del history 1194 | response = self._helpers.bytesToString(item.getResponse()).split('\r\n\r\n')[0] 1195 | resp_headers = response.split('\r\n') 1196 | for k, resp_head in enumerate(resp_headers[1:]): 1197 | if "Content-Type: text/html" in resp_head: 1198 | resp_html_head = self._helpers.bytesToString(item.getResponse()).split('\r\n\r\n')[1].split('')[0]#.encode('utf-8') 1199 | metas = self.meta.findall(resp_html_head) 1200 | 1201 | buffer = "

Meta headers

".format(self.color1) 1202 | buffer += "Host: {}
".format(host) 1203 | buffer += "Endpoint: {}
".format(endpoint) 1204 | for meta in metas: 1205 | meta_line = meta.encode('utf-8').replace('<','<').replace('>','>') 1206 | # Add colors to the meta fields in the summary pannel 1207 | meta_line = meta_line.replace('<meta', '<meta'.format(self.color2)) 1208 | meta_line = meta_line.replace(' charset=', ' charset='.format(self.color3)) 1209 | meta_line = meta_line.replace(' name=', ' name='.format(self.color3)) 1210 | meta_line = meta_line.replace(' property=', ' property='.format(self.color3)) 1211 | meta_line = meta_line.replace(' http-equiv=', ' http-equiv='.format(self.color3)) 1212 | meta_line = meta_line.replace(' content=', ' content='.format(self.color3)) 1213 | meta_line = meta_line.replace('>', '>'.format(self.color2)) 1214 | 1215 | buffer += '
  • ' + meta_line + '
  • \n' 1216 | buffer += '' 1217 | 1218 | self.header_summary.setText(buffer) 1219 | return 1220 | 1221 | 1222 | if host == self.selected_host: # si coincide el host del history con el que clickamos en la tabla de headers. 1223 | buffer = "" 1224 | 1225 | self.header_summary.setText("") 1226 | buffer += '

    Request headers:

    '.format(self.color1) + "\n" 1227 | buffer += '' + req_headers[0] + '' 1228 | buffer += '
      ' 1229 | for req_head in sorted(req_headers[1:]): # este for encuentra el Host header 1230 | 1231 | extra_symbol = self.extra_symbol(req_head) 1232 | 1233 | req_head = req_head.replace('<','< ') 1234 | req_head_name = req_head.split(': ')[0] 1235 | req_head_value = req_head.split(': ')[1] 1236 | 1237 | if req_head.split(": ")[0] != "Host" and req_head.split(": ")[0] == clicked_header: 1238 | buffer += '
    • ' + ''.format(self.color1) + req_head_name + "" + extra_symbol + ': '.format(self.color1) + req_head_value + "
    • " 1239 | 1240 | elif req_head.split(": ")[0] == "Host": 1241 | buffer += '
    • ' + extra_symbol + '' + req_head + "
    • " 1242 | else: 1243 | buffer += '
    • ' + req_head_name + extra_symbol + ": " + req_head_value + "
    • " 1244 | 1245 | buffer += "

    " * 2 + "
    " + "
    " 1246 | buffer += '

    Response headers:

    '.format(self.color1) 1247 | buffer += '
      ' 1248 | 1249 | response = self._helpers.bytesToString(item.getResponse()).split('\r\n\r\n')[0] 1250 | resp_headers = response.split('\r\n') 1251 | 1252 | self.missing_header_array = [] 1253 | for i in range(self.model_tab_config_security.getRowCount()): 1254 | if self.model_tab_config_security.getValueAt(i,0): 1255 | self.missing_header_array.append(self.model_tab_config_security.getValueAt(i,1)) 1256 | 1257 | self.missing_header_array = list(map(lambda x: x.lower(), self.missing_header_array)) 1258 | for resp_head in sorted(resp_headers[1:]): 1259 | #print(resp_head) 1260 | try: 1261 | self.missing_header_array.remove(resp_head.split(":")[0].lower()) 1262 | except: 1263 | pass 1264 | extra_symbol = self.extra_symbol(resp_head) 1265 | 1266 | resp_head = resp_head.replace('<','< ') #este es porque algunos headers tenian links en html y se renderizaba en cosas raras 1267 | resp_head_name = resp_head.split(': ')[0] 1268 | resp_head_value = resp_head.split(': ')[1] 1269 | 1270 | # highlight the clicked header on the summary 1271 | if resp_head.split(":")[0] == clicked_header: 1272 | buffer += '
    • ' + ''.format(self.color1) + resp_head_name + "" + extra_symbol + ': '.format(self.color1) + resp_head_value + "
    • " 1273 | else: 1274 | buffer += '
    • ' + resp_head_name + extra_symbol + ": " + resp_head_value + "
    • " 1275 | 1276 | buffer += '

    Missing security headers:
      ' 1277 | for missing_header in self.missing_header_array: 1278 | buffer += '
    • [ - ] {}'.format(missing_header.title()) 1279 | 1280 | buffer += '
    ' 1281 | buffer += '

    *Note: Some enpoints don\'t return some headers sometimes. If you can\'t find the header you selected on the table to the left, please select other endpoint, perhaps from the \"All Endpoints\" tab.

    ' 1282 | buffer += '
    Color legend for headers names (check yourself if the value is correct):' 1283 | buffer += '
      ' 1284 | buffer += '
    • [ + ] : Security header
    • ' 1285 | buffer += '
    • [ - ] : Missing security header
    • ' 1286 | buffer += '
    • [ X ] : Dangerous or too verbose header
    • ' 1287 | buffer += '
    • [ ? ] : Potentially dangerous header
    • ' 1288 | self.header_summary.setText(buffer + "") 1289 | 1290 | # para que el summary no haga scroll down hasta el final al actualizarlo 1291 | self.header_summary.setSelectionStart(0) 1292 | self.header_summary.setSelectionEnd(0) 1293 | break 1294 | 1295 | def choose_output_file(self, event): 1296 | """File dialogue to choose the path where the output file will be written""" 1297 | fc = JFileChooser() 1298 | result = fc.showOpenDialog( None ) 1299 | if result == JFileChooser.APPROVE_OPTION : 1300 | self.save_path.setText(str(fc.getSelectedFile())) 1301 | #print(str(fc.getSelectedFile())) 1302 | 1303 | return 1304 | 1305 | def choose_output_docx_file(self, event): 1306 | """File dialogue to choose the path where the output docx file will be written""" 1307 | fc = JFileChooser() 1308 | result = fc.showOpenDialog( None ) 1309 | if result == JFileChooser.APPROVE_OPTION : 1310 | self.out_docx_path.setText(str(fc.getSelectedFile())) 1311 | #print(str(fc.getSelectedFile())) 1312 | 1313 | return 1314 | 1315 | def save_json(self,event): 1316 | """Save data to an output file, either in JSON format or in plain text. Multiple output formats are available.""" 1317 | out_type = self.save_ComboBox.getSelectedItem() 1318 | out_file_name = self.save_path.getText() 1319 | 1320 | 1321 | hosts = [] 1322 | headers = [] 1323 | unique_headers = [] 1324 | for line in self.header_host_table: 1325 | hosts.append(line[2]) 1326 | headers.append(line[0]) 1327 | unique_headers.append(line[1]) 1328 | 1329 | unique_hosts = sorted(list(set(hosts))) 1330 | try: 1331 | unique_hosts.remove('\n') 1332 | except: 1333 | pass 1334 | 1335 | self.host_header_table = [] 1336 | 1337 | for unique_host in unique_hosts: 1338 | k = 0 1339 | for line in self.header_host_table: 1340 | if line[2] == unique_host: 1341 | if k == 0: 1342 | self.host_header_table.append([unique_host , unique_host , line[0]]) 1343 | else: 1344 | self.host_header_table.append([unique_host , "" , line[0]]) 1345 | k += 1 1346 | 1347 | Error_frame3 = JFrame()#FlowLayout()) 1348 | Error_frame3.setLayout(FlowLayout()) 1349 | Error_frame3.setSize(260, 90) 1350 | Error_frame3.setLocationRelativeTo(None) 1351 | a=os.getcwd() + '\\error1.png' 1352 | image_path=a.encode('string-escape') #ver si esto falla al coger en linux el icono 1353 | Error_frame3.add(JLabel(ImageIcon(image_path))) 1354 | Error_frame3.add(JLabel(" Wrong output file.")) 1355 | Error_frame3.toFront() 1356 | Error_frame3.setAlwaysOnTop(True) 1357 | 1358 | if "Save headers to..." in out_file_name or out_file_name == "": 1359 | Error_frame1 = JFrame() 1360 | Error_frame1.setLayout(FlowLayout()) 1361 | Error_frame1.setSize(260, 90) 1362 | Error_frame1.setLocationRelativeTo(None) 1363 | a=os.getcwd() + '\\error1.png' 1364 | image_path=a.encode('string-escape') #ver si esto falla al coger en linux el icono 1365 | Error_frame1.add(JLabel(ImageIcon(image_path))) 1366 | Error_frame1.add(JLabel(" Please, enter output file path.")) 1367 | Error_frame1.setVisible(True) 1368 | Error_frame1.toFront() 1369 | Error_frame1.setAlwaysOnTop(True) 1370 | 1371 | 1372 | return 1373 | 1374 | if out_type == "Choose output format": 1375 | Error_frame2 = JFrame()#FlowLayout()) 1376 | Error_frame2.setLayout(FlowLayout()) 1377 | Error_frame2.setSize(260, 90) 1378 | Error_frame2.setLocationRelativeTo(None) 1379 | a=os.getcwd() + '\\error1.png' 1380 | image_path=a.encode('string-escape') #ver si esto falla al coger en linux el icono 1381 | Error_frame2.add(JLabel(ImageIcon(image_path))) 1382 | Error_frame2.add(JLabel(" Please, select output format.")) 1383 | Error_frame2.setVisible(True) 1384 | Error_frame2.toFront() 1385 | Error_frame2.setAlwaysOnTop(True) 1386 | 1387 | return 1388 | 1389 | elif out_type == "TXT: Host -> Header": 1390 | try: 1391 | f = open(out_file_name, 'w') 1392 | f.write("Columns:\n") 1393 | f.write("Host; Unique Host; Header\n\n") 1394 | 1395 | for line in self.host_header_table: 1396 | f.write("; ".join(line) + "\n") 1397 | f.close() 1398 | except: 1399 | Error_frame3.setVisible(True) 1400 | 1401 | elif out_type == "TXT: Header -> Host": 1402 | try: 1403 | f = open(out_file_name, 'w') 1404 | f.write("Columns:\n") 1405 | f.write("Header; Unique Header; Host Name\n\n") 1406 | for line in self.header_host_table: 1407 | if "----------------" in line[1]: 1408 | f.write("".join(line) + "\n") 1409 | else: 1410 | f.write("; ".join(line) + "\n") 1411 | f.close() 1412 | except: 1413 | Error_frame3.setVisible(True) 1414 | 1415 | 1416 | elif out_type == "JSON: Host -> Header": 1417 | try: 1418 | first = True 1419 | k = 0 1420 | f = open(out_file_name, 'w') 1421 | 1422 | f.write("{\n") 1423 | all_hosts = [self.host_header_table[i][1] for i in range(len(self.host_header_table))] 1424 | #all_hosts.remove('') #for some reason this is not removing the empty element '', so I leave it commented and subtract 2 instead of 1 from the lenght in the if k < len... a few lines below 1425 | #print(set(all_hosts)) 1426 | for line in self.host_header_table: 1427 | 1428 | [host1, unique_host1, header] = line 1429 | if unique_host1 != "": 1430 | if not first: 1431 | arr = str(arr_headers) 1432 | arr = arr.replace("u'", "'") 1433 | arr = arr.replace("'", '"') 1434 | 1435 | if k < len(set(all_hosts)) - 2: 1436 | f.write(' "' + host1 + '":' + arr + ',\n' ) 1437 | else: 1438 | f.write(' "' + host1 + '":' + arr + '\n' ) 1439 | 1440 | arr_headers = [] 1441 | arr_headers.append(header) 1442 | k += 1 1443 | 1444 | else: 1445 | arr_headers.append(header) 1446 | first = False 1447 | 1448 | f.write("}") 1449 | f.close() 1450 | 1451 | except: 1452 | Error_frame3.setVisible(True) 1453 | 1454 | 1455 | 1456 | else: 1457 | return 1458 | 1459 | 1460 | 1461 | print("save json!") 1462 | 1463 | update_now = False 1464 | if self.save_format.getSelectedItem() != self.config_dict["last_save_type"]: 1465 | self.config_dict["last_save_type"] = self.save_format.getSelectedItem() 1466 | update_now = True 1467 | if self.save_path.getText() != self.config_dict["last_output_file"]: 1468 | self.config_dict["last_output_file"] = self.save_path.getText() 1469 | update_now = True 1470 | if update_now: 1471 | self.update_config()#"last_filter_type", self.preset_filters.getSelectedItem()) 1472 | 1473 | return 1474 | 1475 | def apply_regex(self, string_for_regex): 1476 | """Applies regular expressions to URLs in order to replace query string parameters values with wildcards. This is used later to remove redundant endpoints in the "Unique endpoints" table, and keep only the unique ones.""" 1477 | #matchea lo que haya entre = y & o entre = y ' ', para el ultimo parametro de la linea 1478 | #query_params = re.compile('=.*?&|=.*? ') 1479 | 1480 | # matchea numeros en la url tipo /asdf/1234/qwe/1234, matchearia los dos 1234 y secuencias de letras, numeros y guiones o puntos. igual algun caso raro se cuela, pero por lo que he visto pilla todo 1481 | #number_between_forwardslash = re.compile('\/[a-zA-Z]*\d+[a-zA-Z0-9-_\.]*') 1482 | 1483 | matches = self.query_params.findall(string_for_regex.split('HTTP/')[0]) 1484 | for match in matches: 1485 | try: 1486 | string_for_regex = string_for_regex.replace(match[1:], '[*]' + match[-1]) 1487 | except: 1488 | print('Error matching first regex when computing unique endpoints.') 1489 | matches1 = self.number_between_forwardslash.findall(string_for_regex.split('HTTP/')[0]) 1490 | for match1 in matches1: 1491 | try: 1492 | string_for_regex = string_for_regex.replace(match1[1:], '[*]' ) 1493 | except: 1494 | print('Error matching second regex when computing unique endpoints.') 1495 | return string_for_regex 1496 | 1497 | def update_meta_endpoints(self, endpoint_table): 1498 | self.model_unique_endpoints.setRowCount(0) 1499 | self.model_all_endpoints.setRowCount(0) 1500 | self.unique_entries = [] 1501 | 1502 | for entry in endpoint_table: 1503 | self.model_all_endpoints.addRow(entry) 1504 | 1505 | for entry in endpoint_table: 1506 | entry[0] = self.apply_regex(entry[0]) 1507 | 1508 | if entry not in self.unique_entries: 1509 | self.unique_entries.append(entry) 1510 | #coger el host del elemento clickado en la tabla de la izda 1511 | #host = self.table_tab_req.getModel().getDataVector().elementAt(self.table_tab_req.getSelectedRow())[1] 1512 | self.model_unique_endpoints.addRow( [ self.replace_symbol(entry[0]) ]) 1513 | 1514 | 1515 | self.table_unique_endpoints.setRowSelectionInterval(0,0) 1516 | self.clicked_endpoint(self.table_unique_endpoints, False) 1517 | return 1518 | 1519 | def call_filter_endpoints(self, event): 1520 | self.update_endpoints(self.endpoint_table1) 1521 | return 1522 | 1523 | def determine_progress(self): 1524 | #self.progressBar.setIndeterminate(1) 1525 | self.progressBar.setIndeterminate(1) 1526 | self.framewait.setLocationRelativeTo(None) 1527 | self.framewait.setVisible(True) 1528 | self.framewait.toFront() 1529 | self.framewait.setAlwaysOnTop(True) 1530 | 1531 | def update_endpoints_worker(self, endpoint_table): 1532 | """Update the "Unique endpoints" table and the "All endpoints" table when a row in the Header-Host table (at the left side of the extension tab) is clicked. The endpoint tables show all the endpoints that exist in the Burp history for which the Host request header is the one clicked on the Header-Host table.""" 1533 | 1534 | self.model_unique_endpoints.setRowCount(0) 1535 | self.model_all_endpoints.setRowCount(0) 1536 | self.unique_entries = [] 1537 | self.progressBar.setIndeterminate(0) 1538 | self.progressBar.setValue(0) 1539 | 1540 | 1541 | total_security = 0 1542 | total_dangerous = 0 1543 | total_potentially_dangerous = 0 1544 | for i in range(self.model_tab_config_security.getRowCount()): 1545 | total_security += self.model_tab_config_security.getValueAt(i,0) 1546 | for i in range(self.model_tab_config_potentially_dangerous.getRowCount()): 1547 | total_potentially_dangerous += self.model_tab_config_potentially_dangerous.getValueAt(i,0) 1548 | for i in range(self.model_tab_config_dangerous.getRowCount()): 1549 | total_dangerous += self.model_tab_config_dangerous.getValueAt(i,0) 1550 | 1551 | #meter filtro de endpoints 1552 | keywords = self.filter_endpoints.getText().lower().split(',') 1553 | 1554 | 1555 | for entry in endpoint_table: 1556 | for keyword in keywords: 1557 | #print('----') 1558 | #print (keyword.lower().strip()) 1559 | #print(entry[0].lower()) 1560 | if keyword.lower().strip() in entry[0].lower() or self.filter_endpoints.getText() == "To filter endpoints enter keywords (separated by a comma)" or self.filter_endpoints.getText() == "": 1561 | self.model_all_endpoints.addRow(entry) 1562 | 1563 | for k_progress, entry in enumerate(endpoint_table): 1564 | if k_progress % 5 == 0: 1565 | self.progressBar.setValue(100 * k_progress // len(endpoint_table)) 1566 | 1567 | entry[0] = self.apply_regex(entry[0]) 1568 | for keyword in keywords: 1569 | if keyword.lower().strip() in entry[0].lower() or self.filter_endpoints.getText() == "To filter endpoints enter keywords (separated by a comma)" or self.filter_endpoints.getText() == "": 1570 | 1571 | if entry not in self.unique_entries: 1572 | 1573 | self.unique_entries.append(entry) 1574 | #coger el host del elemento clickado en la tabla de la izda 1575 | try: 1576 | host = self.table_tab_req.getModel().getDataVector().elementAt(self.table_tab_req.getSelectedRow())[1] 1577 | except: 1578 | host = self.table_tab_resp.getModel().getDataVector().elementAt(self.table_tab_resp.getSelectedRow())[1] 1579 | colors = self.to_get_colors(entry[0], host, True) #colors es un dict 1580 | 1581 | 1582 | symbols_color = {} 1583 | for color in colors.keys(): 1584 | 1585 | if color == "security": 1586 | total = total_security 1587 | #total = self.total_security_headers 1588 | elif color == "potential": 1589 | total = total_potentially_dangerous 1590 | #total = self.total_potential_headers 1591 | elif color == "dangerous": 1592 | total = total_dangerous 1593 | #total = self.total_dangerous_headers 1594 | symbols_color[color] = self.ColorScore(colors[color], total, color) 1595 | 1596 | symbols_string = '[{1}+][{3}?][{5}X] - '.format(symbols_color ["security"], colors["security"], symbols_color["potential"], colors["potential"], symbols_color["dangerous"], colors["dangerous"]) 1597 | #symbols_string = '[+][?][X] - '.format(symbols_color["security"], symbols_color["potential"], symbols_color["dangerous"]) 1598 | #print(symbols_string +entry[0]+'') 1599 | 1600 | ################################### 1601 | #self.model_unique_endpoints.addRow( [symbols_string + entry[0] + '']) 1602 | #print('' + entry[0].replace('[*]', '[*]'.format(self.color1)) + '') 1603 | self.model_unique_endpoints.addRow( [ '' + symbols_string + self.replace_symbol(entry[0]) + '' ]) 1604 | '''if host in self.dic_host_unique_endpoint.keys(): 1605 | self.dic_host_unique_endpoint[host] = [entry[0]] 1606 | else: 1607 | self.dic_host_unique_endpoint[host].append(entry[0])''' 1608 | 1609 | 1610 | #print( [ '' + symbols_string + self.replace_symbol(entry[0]) + '' ]) 1611 | #self.model_unique_endpoints.addRow( [ '' + entry[0].replace('[*]', '[*]'.format(self.color1)) + '']) 1612 | #self.model_unique_endpoints.addRow( [ entry[0] ]) 1613 | 1614 | 1615 | self.table_unique_endpoints.setRowSelectionInterval(0,0) 1616 | self.clicked_endpoint(self.table_unique_endpoints, False) 1617 | self.framewait.setVisible(False) 1618 | return 1619 | 1620 | def update_endpoints(self, event): 1621 | thread_show_progress = threading.Thread(target=self.determine_progress) 1622 | update_endpoints_worker = threading.Thread(target=self.update_endpoints_worker, args=(endpoint_table,)) 1623 | thread_show_progress.start() 1624 | update_endpoints_worker.start() 1625 | 1626 | def addRB( self, pane, bg, text ): 1627 | """Add radio buttons""" 1628 | button = JRadioButton(text,itemStateChanged = self.toggle) 1629 | index = self.categories_tabs.getSelectedIndex() 1630 | if index == 0 and text == 'Security headers': 1631 | button.doClick() 1632 | self.toggle 1633 | if index == 1 and text == 'Potentially dangerous headers': 1634 | button.doClick() 1635 | self.toggle 1636 | if index == 2 and text == 'Dangerous or verbose headers': 1637 | button.doClick() 1638 | self.toggle 1639 | 1640 | bg.add(pane.add(button)) 1641 | 1642 | return 1643 | 1644 | def toggle( self, event ) : 1645 | """Sets the file to which the user will add new headers of a certain category. If these headers are found in requests or responses in the history, the appropriate symbols will be applied to them in the "Unique endpoints" table and in the Summary.""" 1646 | text = event.getItem().getText() 1647 | if text == "Security headers": 1648 | self.file_to_add_headers = "security_headers.txt" 1649 | elif text == "Potentially dangerous headers": 1650 | self.file_to_add_headers = "potentially_dangerous_headers.txt" 1651 | elif text == "Dangerous or verbose headers": 1652 | self.file_to_add_headers = "dangerous_headers.txt" 1653 | return 1654 | 1655 | def add_header_to_file(self, event): 1656 | """Adds a new header supplied by the user to the corresponding category (security, dangerous, potentially dangerous). Not saved to file here, that is done with the button for persisting changes.""" 1657 | filename = self.file_to_add_headers 1658 | text = self.header_to_add.getText() 1659 | 1660 | if filename == "security_headers.txt": 1661 | self.table_config_security.append([True, text]) 1662 | self.model_tab_config_security.addRow([True, text]) 1663 | self.initial_count_security_headers += 1 1664 | 1665 | elif filename == "dangerous_headers.txt": 1666 | self.table_config_dangerous.append([True, text]) 1667 | self.model_tab_config_dangerous.addRow([True, text]) 1668 | self.initial_count_dangerous_headers += 1 1669 | 1670 | elif filename == "potentially_dangerous_headers.txt": 1671 | self.table_config_potentially_dangerous.append([True, text]) 1672 | self.model_tab_config_potentially_dangerous.addRow([True, text]) 1673 | self.initial_count_potentially_dangerous_headers += 1 1674 | 1675 | self.added_header_info.setText('Header "{0}" added to {1}'.format(text, filename)) 1676 | 1677 | def remove_headers_from_categories(self, event): 1678 | selected_tab = self.categories_tabs.getSelectedIndex() 1679 | 1680 | if selected_tab == 0: 1681 | selected = self.table_tab_config_security.getSelectedRows() 1682 | # we need to remove from the last selected to the first selected to avoid confusions when the table model resizes every time we remove an element 1683 | for i in selected[::-1]: 1684 | self.initial_count_security_headers -= 1 1685 | #print(self.initial_count_security_headers) 1686 | self.model_tab_config_security.removeRow(i) 1687 | 1688 | if selected_tab == 1: 1689 | selected = self.table_tab_config_potentially_dangerous.getSelectedRows() 1690 | for i in selected[::-1]: 1691 | self.model_tab_config_potentially_dangerous.removeRow(i) 1692 | self.initial_count_potentially_dangerous_headers -= 1 1693 | 1694 | if selected_tab == 2: 1695 | selected = self.table_tab_config_dangerous.getSelectedRows() 1696 | for i in selected[::-1]: 1697 | self.model_tab_config_dangerous.removeRow(i) 1698 | self.initial_count_dangerous_headers -= 1 1699 | 1700 | def add_headers_to_categories(self, event): 1701 | """Configuration panel which the user can use to add new headers to category files.""" 1702 | self.file_to_add_headers = "" 1703 | add_headers = JFrame("Add new header to categories") 1704 | add_headers_panel = JPanel() 1705 | add_headers_panel.setLayout(BoxLayout(add_headers_panel, BoxLayout.Y_AXIS ) ) 1706 | 1707 | bg = ButtonGroup() 1708 | add_headers_panel.add( JLabel( 'Select category to add header:' ) ) 1709 | self.addRB( add_headers_panel, bg, 'Security headers' ) 1710 | self.addRB( add_headers_panel, bg, 'Potentially dangerous headers' ) 1711 | self.addRB( add_headers_panel, bg, 'Dangerous or verbose headers' ) 1712 | add_headers_panel.add( JLabel( ' ' ) ) 1713 | add_headers_panel.add( JLabel( 'New header to be added:' ) ) 1714 | 1715 | new_header_textfield = JTextField("New header") 1716 | new_header_textfield.addActionListener(self.add_header_to_file) 1717 | self.header_to_add = add_headers_panel.add(new_header_textfield) 1718 | 1719 | 1720 | self.add_header_button = add_headers_panel.add(JButton('Add new header', actionPerformed = self.add_header_to_file)) 1721 | self.add_header_button.setForeground(Color.WHITE) 1722 | self.add_header_button.setBackground(Color(10,101,247)) 1723 | 1724 | self.added_header_info = JTextArea("", rows=2, editable=False) 1725 | self.added_header_info.setLineWrap(True) 1726 | add_headers_panel.add(JScrollPane(self.added_header_info)) 1727 | 1728 | add_headers.setSize(400, 220) 1729 | add_headers.add(add_headers_panel) 1730 | add_headers.setLocationRelativeTo(None) 1731 | add_headers.setVisible( True ) 1732 | add_headers.toFront() 1733 | add_headers.setAlwaysOnTop(True) 1734 | 1735 | self.threshold_count_security.setText(str(len(self.table_config_security))) 1736 | self.threshold_count_potentially_dangerous.setText(str(len(self.table_config_potentially_dangerous))) 1737 | self.threshold_count_dangerous.setText(str(len(self.table_config_dangerous))) 1738 | 1739 | return 1740 | 1741 | def show_docx(self, event): 1742 | self.docx_frame.setVisible(True) 1743 | self.docx_frame.toFront() 1744 | self.docx_frame.setAlwaysOnTop(True) 1745 | 1746 | def summary_update_hosts(self, event): 1747 | unique_hosts = set([x[2] for x in self.header_host_table]) 1748 | self.output_hosts_summary_model.setRowCount(0) 1749 | for host in sorted(unique_hosts): 1750 | # para test los pongo todos a true, normalmente iran a false, pero intentar poner una forma de seleccionar todos con un boton 1751 | #self.output_hosts_summary_model.addRow([True, host]) 1752 | self.output_hosts_summary_model.addRow([False, host]) 1753 | 1754 | def data_from_request(self, item): 1755 | """ Returns header data from a request-response object """ 1756 | """ sometimes this function has included https:// in the url for some reason""" 1757 | request = self._helpers.bytesToString(item.getRequest()).split('\r\n\r\n')[0] 1758 | req_headers = request.split('\r\n') 1759 | #iter_host = self.find_host(req_headers) 1760 | endpoint = req_headers[0] 1761 | unique_endpoint = self.apply_regex(endpoint.split(' ')[1]) 1762 | try: #some responses return None and the next split fails, for that reason the try-except is used 1763 | response = self._helpers.bytesToString(item.getResponse()).split('\r\n\r\n')[0] 1764 | except: 1765 | response = '' 1766 | resp_headers = response.split('\r\n') 1767 | return req_headers, endpoint, response, resp_headers, unique_endpoint 1768 | 1769 | def check_depth(self, url, depth): 1770 | """takes a URL (example: /dev/admin) and returns the url of only the given depth (for example, /dev for depth==1). 1771 | If depth==0 returns the whole URL. Depth can be useful if different directories in the webapp apply different headers 1772 | (for example using nested .htaccess files???)""" 1773 | url = url.split('?')[0] 1774 | if depth == 0: 1775 | return url 1776 | url = url.split('/') 1777 | return '/' + '/'.join(url[0:depth]).rstrip('/').lstrip('/') 1778 | 1779 | def output_selected_summary(self, event): 1780 | f = open('output/selected_output.txt','w') 1781 | 1782 | for i in range(self.unique_endpoints_summary_model.getRowCount()): 1783 | if self.unique_endpoints_summary_model.getValueAt(i,0): 1784 | issue = self.unique_endpoints_summary_model.getValueAt(i,1) 1785 | host = self.unique_endpoints_summary_model.getValueAt(i,2) 1786 | detail = self.unique_endpoints_summary_model.getValueAt(i,3) 1787 | f.write('Issue: ' + issue + '; Host: ' + host + '; Detail: ' + detail + '\n') 1788 | f.close() 1789 | os.chdir('template') 1790 | if self.out_docx_path.getText() != "Output file": 1791 | optional_name = " " + self.out_docx_path.getText() 1792 | else: 1793 | optional_name = "" 1794 | 1795 | python_path = self.python_path_textfield.getText() 1796 | if python_path != '': 1797 | print("{0} template.py{1}".format(python_path, optional_name)) 1798 | os.system("{1} template.py{}".format(python_path, optional_name)) 1799 | else: 1800 | # igual esto falla por no poner el py o python3? 1801 | print("python3 template.py{}".format(optional_name)) 1802 | os.system("python3 template.py{}".format(optional_name)) 1803 | os.chdir('..') 1804 | 1805 | 1806 | 1807 | 1808 | def summary_update_endpoints_worker(self): 1809 | self.selected_output_hosts = [] 1810 | self.unique_endpoints_summary_model.setRowCount(0) 1811 | self.progressBar.setIndeterminate(0) 1812 | self.progressBar.setValue(0) 1813 | if self.depth_textbox.getText() == "Depth (0=all; Default=1)": 1814 | self.depth = 1 1815 | else: 1816 | if self.depth_textbox.getText().isnumeric(): 1817 | self.depth = int(self.depth_textbox.getText()) 1818 | else: 1819 | self.depth_textbox.setText('Enter a number') 1820 | 1821 | 1822 | #meter puerto de alguna forma en el output dic, junto al host 1823 | 1824 | '''self.dic_summary es un diccionario de diccionarios, que contiene como keys los issue_types, y como 1825 | valor para cada key otra diccionario, cuyas keys son los host y para cada host el value es una lista 1826 | de las unique urls afectadas''' 1827 | self.dic_summary = { 1828 | "Missing Security Headers":{}, 1829 | "Dangerous Headers":{}, 1830 | "Potentially Dangerous Headers":{}, 1831 | "Bad HTTP Methods":{}, 1832 | "Cookies Without Flags":{} 1833 | } 1834 | 1835 | # adds the selected hosts to a new array, works well 1836 | for i in range(self.output_hosts_summary_model.getRowCount()): 1837 | if self.output_hosts_summary_model.getValueAt(i,0) == True: 1838 | to_add = self.output_hosts_summary_model.getValueAt(i,1) 1839 | self.selected_output_hosts.append(to_add) 1840 | 1841 | 1842 | 1843 | for k_progress, item in enumerate(history1): 1844 | if k_progress % 10 == 0: 1845 | self.progressBar.setValue(100 * k_progress // len(history1)) 1846 | 1847 | host = item.getHost() 1848 | port = item.getPort() 1849 | 1850 | req_headers, endpoint, response, resp_headers, unique_endpoint = self.data_from_request(item) 1851 | 1852 | 1853 | if host in self.selected_output_hosts: 1854 | req_headers, endpoint, response, resp_headers, unique_endpoint = self.data_from_request(item) 1855 | 1856 | ### - bad http methods 1857 | '''http_method = endpoint.split(' ')[0] 1858 | if http_method in ['PUT', 'TRACE', 'DELETE']: 1859 | self.unique_endpoints_summary_model.addRow([True, "Bad HTTP methods", host, self.apply_regex(url)]) 1860 | if host not in self.dic_summary["Bad HTTP Methods"].keys() : #si no exite el host, anadelo 1861 | self.dic_summary["Bad HTTP Methods"][host] = [] 1862 | if self.apply_regex(url) not in self.dic_summary["Bad HTTP Methods"][host]: # si para ese host no esta la url unique, anadela 1863 | self.dic_summary["Bad HTTP Methods"][host].append("Method: " + http_method + "; URL: " + self.appl_regex(url))''' 1864 | 1865 | # present security headers will be removed from here to create a list of missing security headers for every endpoint 1866 | #needed to be like this, to avoid copy by reference, which removes elements from self.security_headers as well 1867 | remaining_headers = list(self.security_headers) 1868 | removed_headers = [] 1869 | 1870 | for header in resp_headers: 1871 | 1872 | ### - cookies without flags 1873 | #self.flags= ['secure','httponly'] 1874 | for flag in self.cookie_flags: 1875 | if self.checkbox_cookies.isSelected(): 1876 | if 'set-cookie' in header.lower(): 1877 | if flag not in header.lower(): 1878 | if host not in self.dic_summary["Cookies Without Flags"].keys(): 1879 | self.dic_summary["Cookies Without Flags"][host] = [] 1880 | 1881 | #string_to_add = 'Missing "{}" header - URL: '.format(flag.title()) + self.check_depth(unique_endpoint, self.depth) + ' - Port: ' + port 1882 | string_to_add = 'Missing "{0}" flag - URL: {1} - Port: {2}'.format(flag.title(), self.check_depth(unique_endpoint, self.depth), port) 1883 | if string_to_add not in self.dic_summary["Cookies Without Flags"][host]: 1884 | self.unique_endpoints_summary_model.addRow([True, "Cookies without flags", host, string_to_add]) 1885 | self.dic_summary["Cookies Without Flags"][host].append(string_to_add) 1886 | 1887 | 1888 | 1889 | ### - missing security headers (continues after the end of this loop) 1890 | if self.checkbox_missing_security.isSelected(): 1891 | check_header = header.split(':')[0].lower().rstrip().lstrip() 1892 | if check_header in self.security_headers and check_header not in removed_headers: 1893 | remaining_headers.remove(check_header) 1894 | removed_headers.append(check_header) 1895 | 1896 | 1897 | ### - dangerous header 1898 | if self.checkbox_dangerous.isSelected(): 1899 | for dangerous_header in self.dangerous_headers: 1900 | if dangerous_header in header.lower(): 1901 | if host not in self.dic_summary["Dangerous Headers"].keys(): 1902 | self.dic_summary["Dangerous Headers"][host] = [] 1903 | 1904 | string_to_add = '"{0}" header - URL: {1} - Port: {2}'.format(dangerous_header.title(), self.check_depth(unique_endpoint, self.depth), port) 1905 | if string_to_add not in self.dic_summary["Dangerous Headers"][host]: 1906 | self.unique_endpoints_summary_model.addRow([True, "Dangerous header", host, string_to_add]) 1907 | self.dic_summary["Dangerous Headers"][host].append(string_to_add) 1908 | 1909 | ### - potentially dangerous 1910 | if self.checkbox_potentially_dangerous.isSelected(): 1911 | for potentially_dangerous_header in self.potentially_dangerous_headers: 1912 | if potentially_dangerous_header in header.lower(): 1913 | if host not in self.dic_summary["Potentially Dangerous Headers"].keys(): 1914 | self.dic_summary["Potentially Dangerous Headers"][host] = [] 1915 | 1916 | string_to_add = '"{0}" header - URL: {1} - Port: {2}'.format(potentially_dangerous_header.title(), self.check_depth(unique_endpoint, self.depth), port) 1917 | if string_to_add not in self.dic_summary["Potentially Dangerous Headers"][host]: 1918 | self.unique_endpoints_summary_model.addRow([True, "Potentially Dangerous Header", host, string_to_add]) 1919 | self.dic_summary["Potentially Dangerous Headers"][host].append(string_to_add) 1920 | 1921 | ### - continuation of missing security headers, after knowing which headers are not in a response 1922 | if self.checkbox_missing_security.isSelected(): 1923 | for header in remaining_headers: 1924 | if host not in self.dic_summary["Missing Security Headers"].keys(): 1925 | self.dic_summary["Missing Security Headers"][host] = [] 1926 | string_to_add = 'Missing "{0}" header - URL: {1} - Port: {2}'.format(header.title(), self.check_depth(unique_endpoint, self.depth), port) 1927 | if string_to_add not in self.dic_summary["Missing Security Headers"][host]: 1928 | self.unique_endpoints_summary_model.addRow([True, "Missing Security Header", host, string_to_add]) 1929 | self.dic_summary["Missing Security Headers"][host].append(string_to_add) 1930 | 1931 | 1932 | self.framewait.setVisible(False) 1933 | 1934 | def summary_update_endpoints(self, event): 1935 | thread_show_progress = threading.Thread(target=self.determine_progress) 1936 | summary_update_endpoints_worker = threading.Thread(target=self.summary_update_endpoints_worker) 1937 | thread_show_progress.start() 1938 | summary_update_endpoints_worker.start() 1939 | 1940 | def create_summary(self): 1941 | self.summary_frame = JFrame("Summary") 1942 | self.summary_frame.setLayout(BorderLayout()) 1943 | self.summary_frame.setSize(1600, 600) 1944 | self.summary_frame.setLocationRelativeTo(None) 1945 | 1946 | colNames_left = ("Include?", "Host" ) 1947 | 1948 | self.depth = 0 1949 | 1950 | c = GridBagConstraints() 1951 | c.gridx = 0 1952 | c.gridy = 0 1953 | c.weightx = 1 1954 | c.weighty = 1 1955 | c.fill = GridBagConstraints.BOTH 1956 | 1957 | self.output_hosts_summary_model = SummaryTableModel_left([], colNames_left) 1958 | self.output_hosts_summary_table = JTable(self.output_hosts_summary_model) 1959 | self.output_hosts_summary_table.getColumnModel().getColumn(0).setPreferredWidth(60) 1960 | self.output_hosts_summary_table.getColumnModel().getColumn(0).setMaxWidth(60) 1961 | 1962 | c.fill = GridBagConstraints.HORIZONTAL 1963 | c.gridy += 1 1964 | 1965 | 1966 | c.fill = GridBagConstraints.BOTH 1967 | c.gridy += 1 1968 | colNames_right = ("Report?", "Issue type", "Host", "Details" ) 1969 | self.unique_endpoints_summary_model = SummaryTableModel_right([], colNames_right) 1970 | 1971 | summary_all_table = JTable(self.unique_endpoints_summary_model) 1972 | 1973 | summary_all_table.getColumnModel().getColumn(0).setPreferredWidth(60) 1974 | summary_all_table.getColumnModel().getColumn(0).setMaxWidth(60) 1975 | summary_all_table.getColumnModel().getColumn(1).setPreferredWidth(150) 1976 | summary_all_table.getColumnModel().getColumn(1).setMaxWidth(180) 1977 | summary_all_table.getColumnModel().getColumn(2).setPreferredWidth(250) 1978 | summary_all_table.getColumnModel().getColumn(3).setPreferredWidth(300) 1979 | 1980 | left_panel = JPanel(GridBagLayout()) 1981 | c = GridBagConstraints() 1982 | c.anchor = GridBagConstraints.WEST 1983 | 1984 | button_update_hosts = JButton('Update Hosts',actionPerformed=self.summary_update_hosts) 1985 | button_update_for_selected_hosts = JButton('Update for selected Hosts', actionPerformed=self.summary_update_endpoints) 1986 | 1987 | button_update_hosts.putClientProperty("html.disable", None) 1988 | button_update_for_selected_hosts.putClientProperty("html.disable", None) 1989 | 1990 | button_update_hosts.setBackground(Color(10,101,247)) 1991 | button_update_for_selected_hosts.setBackground(Color(10,101,247)) 1992 | 1993 | left_panel.add(button_update_hosts,c) 1994 | left_panel.add(button_update_for_selected_hosts,c) 1995 | 1996 | c.weightx = 1 1997 | self.checkbox_missing_security = JCheckBox("Missing security headers") 1998 | self.checkbox_potentially_dangerous = JCheckBox("Potentially Dangerous headers") 1999 | self.checkbox_dangerous = JCheckBox("Dangerous or verbose headers") 2000 | self.checkbox_cookies = JCheckBox("Cookies without flags") 2001 | #self.depth_label = JLabel("Depth (0 = all)\0") 2002 | #self.depth_label.putClientProperty("html.disable", None) 2003 | self.depth_textbox = JTextField("Depth (0=all; Default=1)") 2004 | 2005 | self.checkbox_missing_security.setSelected(True) 2006 | self.checkbox_potentially_dangerous.setSelected(True) 2007 | self.checkbox_dangerous.setSelected(True) 2008 | self.checkbox_cookies.setSelected(True) 2009 | 2010 | left_panel.add(self.checkbox_missing_security, c) 2011 | left_panel.add(self.checkbox_potentially_dangerous, c) 2012 | left_panel.add(self.checkbox_dangerous, c) 2013 | left_panel.add(self.checkbox_cookies, c) 2014 | #c.anchor = GridBagConstraints.EAST 2015 | #left_panel.add(self.depth_label, c) 2016 | #c.anchor = GridBagConstraints.WEST 2017 | c.weightx = 2 2018 | c.fill = GridBagConstraints.HORIZONTAL 2019 | left_panel.add(self.depth_textbox, c) 2020 | 2021 | self.summary_frame.add(left_panel, BorderLayout.NORTH) 2022 | 2023 | split = JSplitPane(JSplitPane.HORIZONTAL_SPLIT, JScrollPane(self.output_hosts_summary_table), JScrollPane(summary_all_table)) 2024 | split.setDividerLocation(200) 2025 | self.summary_frame.add(split, BorderLayout.CENTER) 2026 | 2027 | right_panel = JPanel(GridBagLayout()) 2028 | c = GridBagConstraints() 2029 | c.fill = GridBagConstraints.HORIZONTAL 2030 | c.anchor = GridBagConstraints.WEST 2031 | c.weightx = 0 2032 | right_panel.add(JButton("Choose output file", actionPerformed = self.choose_output_docx_file), c) 2033 | c.weightx = 1 2034 | self.out_docx_path = JTextField("Output file") 2035 | right_panel.add(self.out_docx_path, c) 2036 | 2037 | 2038 | c.anchor = GridBagConstraints.WEST 2039 | c.weightx = 0 2040 | #c.gridx += 1 2041 | #c.gridy = y_pos 2042 | a=os.getcwd() + '\\gear_2.png' 2043 | image_path=a.encode('string-escape') #ver si esto falla al coger en linux el icono 2044 | self.advanced_config_button = JButton(ImageIcon(image_path)) 2045 | self.advanced_config_button.addActionListener(self.show_docx) 2046 | self.advanced_config_button.setPreferredSize(Dimension(23, 23)) 2047 | right_panel.add(self.advanced_config_button, c) 2048 | 2049 | 2050 | c.weightx = 0 2051 | right_panel.add(JButton(".docx report", actionPerformed = self.output_selected_summary), c) 2052 | self.summary_frame.add(right_panel, BorderLayout.SOUTH) 2053 | 2054 | def show_summary(self, event): 2055 | self.summary_frame.setVisible(True) 2056 | self.summary_frame.toFront() 2057 | 2058 | def getUiComponent(self): 2059 | """Builds the interface of the extension tab.""" 2060 | 2061 | self.framewait = JFrame() 2062 | self.panelwait = JPanel() 2063 | self.panelwait.setLayout(BoxLayout(self.panelwait, BoxLayout.Y_AXIS)) 2064 | self.framewait.setSize(350, 100) 2065 | self.panelwait.add(JLabel("Please wait, may take some time...")) 2066 | self.panelwait.add(JLabel(" ")) 2067 | self.progressBar = JProgressBar() 2068 | self.progressBar.setMaximum(100) 2069 | self.progressBar.setMinimum(1) 2070 | self.panelwait.add(self.progressBar) 2071 | self.framewait.add(self.panelwait) 2072 | 2073 | 2074 | self.create_summary() 2075 | 2076 | panel = JPanel(GridBagLayout()) 2077 | 2078 | # ================== Add button and filter ===================== # 2079 | JPanel1 = JPanel(GridBagLayout()) 2080 | 2081 | c = GridBagConstraints() 2082 | c.gridx = 0 2083 | y_pos = 0 2084 | c.gridy = y_pos 2085 | c.anchor = GridBagConstraints.WEST 2086 | #self.filter_but = JButton('Update table', actionPerformed = self.filter_entries) 2087 | self.filter_but = JButton('Update', actionPerformed = self.filter_entries) 2088 | self.filter_but.putClientProperty("html.disable", None) 2089 | self.filter_but.setBackground(Color(210,101,47)) 2090 | JPanel1.add( self.filter_but, c ) 2091 | 2092 | 2093 | self.preset_filters = DefaultComboBoxModel() 2094 | self.preset_filters.addElement("Request + Response + ") 2095 | 2096 | c.fill = GridBagConstraints.HORIZONTAL 2097 | c.weightx = 1 2098 | c.gridx += 1 2099 | self.filterComboBox = JComboBox(self.preset_filters) 2100 | JPanel1.add(self.filterComboBox , c ) 2101 | 2102 | c.weightx = 8 2103 | c.gridx += 1 2104 | self.filter = JTextField('To filter headers enter keywords (separated by a comma)') 2105 | dim = Dimension(500,23) 2106 | self.filter.setPreferredSize(dim) 2107 | self.filter.addActionListener(self.filter_entries) 2108 | JPanel1.add(self.filter , c ) 2109 | 2110 | c.weightx = 8 2111 | c.gridx += 1 2112 | # en este y el anterior intentaba meter las hints en la textbox con la clase de arriba, pero no va y es complicado, la converti de java y seguro que falta algo 2113 | #self.filter_endpoints = HintTextField('To filter endpoints enter keywords (separated by a comma)', True) 2114 | self.filter_endpoints = JTextField('To filter endpoints enter keywords (separated by a comma)') 2115 | dim = Dimension(500,23) 2116 | self.filter_endpoints.setPreferredSize(dim) 2117 | self.filter_endpoints.addActionListener(self.call_filter_endpoints) 2118 | JPanel1.add(self.filter_endpoints , c ) 2119 | 2120 | c.weightx = 0 2121 | c.gridx += 1 2122 | c.gridy = y_pos 2123 | a=os.getcwd() + '\\gear_2.png' 2124 | image_path=a.encode('string-escape') #ver si esto falla al coger en linux el icono 2125 | self.advanced_config_button = JButton(ImageIcon(image_path)) 2126 | self.advanced_config_button.addActionListener(self.show_advanced_config) 2127 | self.advanced_config_button.setPreferredSize(Dimension(23, 23)) 2128 | JPanel1.add(self.advanced_config_button, c) 2129 | 2130 | c = GridBagConstraints() 2131 | y_pos =0 2132 | c.gridy = y_pos 2133 | c.fill = GridBagConstraints.HORIZONTAL 2134 | c.anchor = GridBagConstraints.WEST 2135 | panel.add(JPanel1 , c) 2136 | 2137 | # ================== Add small separation between filter and tables (Consider removing) ===================== # 2138 | 2139 | c = GridBagConstraints() 2140 | y_pos += 1 2141 | c.gridy = y_pos 2142 | c.fill = GridBagConstraints.HORIZONTAL 2143 | c.anchor = GridBagConstraints.WEST 2144 | text1 = JLabel("
      ") 2145 | text1.putClientProperty("html.disable", None) 2146 | panel.add( text1 , c) 2147 | 2148 | # ================== Add table ===================== # 2149 | 2150 | c = GridBagConstraints() 2151 | y_pos += 1 2152 | c.gridy = y_pos 2153 | c.weighty = 2 2154 | c.weightx = 2 2155 | c.fill = GridBagConstraints.BOTH 2156 | 2157 | #todas las columnas del archivo: header name && description && example && (permanent, no se que es esto) && 2158 | self.colNames = ('Header name','Appears in Host') 2159 | self.colNames_meta = ('Meta header identifier','Meta header content') 2160 | 2161 | self.model_tab_req = IssueTableModel([["",""]], self.colNames) 2162 | self.table_tab_req = IssueTable(self.model_tab_req, "tab") 2163 | self.table_tab_req.getColumnModel().getColumn(0).setCellRenderer(RawHtmlRenderer()) 2164 | self.table_tab_req.getColumnModel().getColumn(1).setCellRenderer(RawHtmlRenderer()) 2165 | self.table_tab_req.putClientProperty("html.disable", None) 2166 | #im = self.table_tab_req.getInputMap(JTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) 2167 | #im.put(KeyStroke.getKeyStroke("DOWN"), self.printxx) 2168 | #im.put(KeyStroke.getKeyStroke("DOWN", 0), self.printxx) 2169 | 2170 | self.table_tab_req.getColumnModel().getColumn(0).setPreferredWidth(130) 2171 | self.table_tab_req.getColumnModel().getColumn(0).setMaxWidth(130) 2172 | self.table_tab_req.getColumnModel().getColumn(1).setPreferredWidth(100) 2173 | 2174 | self.model_tab_resp = IssueTableModel([["",""]], self.colNames) 2175 | self.table_tab_resp = IssueTable(self.model_tab_resp, "tab") 2176 | self.table_tab_resp.getColumnModel().getColumn(0).setCellRenderer(RawHtmlRenderer()) 2177 | self.table_tab_resp.getColumnModel().getColumn(1).setCellRenderer(RawHtmlRenderer()) 2178 | 2179 | self.table_tab_resp.getColumnModel().getColumn(0).setPreferredWidth(100) 2180 | self.table_tab_resp.getColumnModel().getColumn(1).setPreferredWidth(100) 2181 | 2182 | self.model_tab_meta = IssueTableModel([["",""]], self.colNames_meta) 2183 | self.table_tab_meta = IssueTable(self.model_tab_meta, "meta") 2184 | self.table_tab_meta.getColumnModel().getColumn(0).setCellRenderer(RawHtmlRenderer()) 2185 | self.table_tab_meta.getColumnModel().getColumn(1).setCellRenderer(RawHtmlRenderer()) 2186 | 2187 | self.table_tab_meta.getColumnModel().getColumn(0).setPreferredWidth(100) 2188 | self.table_tab_meta.getColumnModel().getColumn(1).setPreferredWidth(100) 2189 | 2190 | 2191 | # IMPORTANT: tables must be inside a JScrollPane so that the Table headers (that is, the columns names) are visible!!! 2192 | panelTab_req = JPanel(BorderLayout()) 2193 | panelTab_req.add(JScrollPane(self.table_tab_req)) 2194 | panelTab_resp = JPanel(BorderLayout()) 2195 | panelTab_resp.add(JScrollPane(self.table_tab_resp)) 2196 | panelTab_meta = JPanel(BorderLayout()) 2197 | panelTab_meta.add(JScrollPane(self.table_tab_meta)) 2198 | 2199 | 2200 | self.tab_tabs = JTabbedPane() 2201 | self.tab_tabs.addTab('Requests', panelTab_req) 2202 | self.tab_tabs.addTab('Responses', panelTab_resp) 2203 | self.tab_tabs.addTab('', panelTab_meta) 2204 | 2205 | 2206 | # ================== Add endpoints table ===================== # 2207 | 2208 | 2209 | self.model_unique_endpoints = IssueTableModel([[""]], ["Unique endpoints for selected host"]) 2210 | self.table_unique_endpoints = IssueTable(self.model_unique_endpoints, "endpoints") 2211 | self.table_unique_endpoints.getColumnModel().getColumn(0).setCellRenderer(RawHtmlRenderer()) 2212 | 2213 | self.model_all_endpoints = IssueTableModel([[""]], ["All endpoints for selected host"]) 2214 | self.table_all_endpoints = IssueTable(self.model_all_endpoints, "endpoints") 2215 | 2216 | 2217 | self.endpoint_tabs = JTabbedPane() 2218 | self.endpoint_tabs.addTab('Unique endpoints', JScrollPane(self.table_unique_endpoints)) 2219 | self.endpoint_tabs.addTab('All endpoints', JScrollPane(self.table_all_endpoints)) 2220 | 2221 | 2222 | self.header_summary = JEditorPane("text/html", "") 2223 | self.header_summary.putClientProperty("html.disable", None) 2224 | self.scroll_summary = JScrollPane(self.header_summary) 2225 | 2226 | self.summary_summary = JEditorPane("text/html", "") 2227 | self.scroll_summary_summary = JScrollPane(self.summary_summary) 2228 | 2229 | self.summary_panel = JPanel() 2230 | self.summary_panel.setLayout(BorderLayout()) 2231 | self.summary_panel.add(self.scroll_summary, BorderLayout.CENTER) 2232 | 2233 | self.splt_2 = JSplitPane(JSplitPane.HORIZONTAL_SPLIT,self.endpoint_tabs, self.summary_panel) 2234 | 2235 | self.splt_1 = JSplitPane(JSplitPane.HORIZONTAL_SPLIT,JScrollPane(self.tab_tabs), self.splt_2) 2236 | #self.splt_1.setDividerLocation(300) 2237 | panel.add(self.splt_1, c) 2238 | 2239 | # ================== Add saving to file ===================== # 2240 | JPanel2 = JPanel(GridBagLayout()) 2241 | 2242 | c = GridBagConstraints() 2243 | c.gridx = 0 2244 | c.gridy = y_pos 2245 | c.anchor = GridBagConstraints.WEST 2246 | self.save_but = JButton('Save headers', actionPerformed = self.save_json) 2247 | self.save_but.putClientProperty("html.disable", None) 2248 | self.save_but.setBackground(Color(10,101,247)) 2249 | JPanel2.add( self.save_but, c ) 2250 | 2251 | 2252 | 2253 | c.gridx += 1 2254 | c.gridy = y_pos 2255 | c.anchor = GridBagConstraints.WEST 2256 | self.save_format = DefaultComboBoxModel() 2257 | self.save_format.addElement("Choose output format") 2258 | self.save_format.addElement("TXT: Host -> Header") 2259 | self.save_format.addElement("TXT: Header -> Host") 2260 | self.save_format.addElement("JSON: Host -> Header") 2261 | self.save_ComboBox = JComboBox(self.save_format) 2262 | JPanel2.add( self.save_ComboBox, c ) 2263 | 2264 | c.gridx += 1 2265 | c.gridy = y_pos 2266 | self.choose_file_but = JButton('Choose output file', actionPerformed = self.choose_output_file) 2267 | self.choose_file_but.putClientProperty("html.disable", None) 2268 | JPanel2.add( self.choose_file_but, c ) 2269 | 2270 | c.fill = GridBagConstraints.HORIZONTAL 2271 | c.weightx = 1 2272 | c.gridx += 1 2273 | c.gridy = y_pos 2274 | self.save_path = JTextField('Save headers to... (write full path or click "Choose output file". The file will be created)') 2275 | JPanel2.add(self.save_path , c ) 2276 | 2277 | c.gridx += 1 2278 | c.weightx = 0 2279 | c.gridy = y_pos 2280 | c.anchor = GridBagConstraints.EAST 2281 | self.save_but = JButton('Summary', actionPerformed = self.show_summary) 2282 | self.save_but.putClientProperty("html.disable", None) 2283 | self.save_but.setBackground(Color(10,101,247)) 2284 | JPanel2.add( self.save_but, c ) 2285 | 2286 | c = GridBagConstraints() 2287 | y_pos += 1 2288 | c.gridy = y_pos 2289 | c.fill = GridBagConstraints.HORIZONTAL 2290 | c.anchor = GridBagConstraints.WEST 2291 | panel.add( JPanel2 , c) 2292 | 2293 | return panel 2294 | 2295 | def clear_table(self): 2296 | """Clears the Header-Host table. It is also called every time a filter is applied.""" 2297 | self.model_tab_req.setRowCount(0) 2298 | self.model_tab_resp.setRowCount(0) 2299 | self.model_tab_meta.setRowCount(0) 2300 | self.for_table = [] 2301 | self.header_host_table = [] 2302 | self.for_req_table = [] 2303 | self.for_resp_table = [] 2304 | self.for_table_meta = [] 2305 | self.req_header_dict = {} 2306 | self.resp_header_dict = {} 2307 | self.headers_already_in_table = [] 2308 | self.meta_headers_already_in_table = [] 2309 | self.last_row = 0 2310 | self.last_len = 0 2311 | self.last_row_meta = 0 2312 | self.last_len_meta = 0 2313 | return 2314 | 2315 | def get_meta_tags(self): 2316 | global history2 2317 | history2 = [] 2318 | history2 = self._callbacks.getProxyHistory() 2319 | self.meta_table = [] 2320 | 2321 | keywords = self.filter.getText().lower().split(',') 2322 | for item in history2: 2323 | ''' This try / except is because for some reason some entries fail somewhere here. also happens for the normal request responses, not only metas''' 2324 | try: 2325 | response = self._helpers.bytesToString(item.getResponse()).split('\r\n\r\n')[0] 2326 | resp_headers = response.split('\r\n') 2327 | for resp_head in resp_headers[1:]: 2328 | if "Content-Type: text/html" in resp_head: 2329 | resp_html_head = self._helpers.bytesToString(item.getResponse()).split('\r\n\r\n')[1].split('')[0] 2330 | metas = self.meta.findall(resp_html_head) 2331 | for meta in metas: 2332 | if meta not in self.meta_table: 2333 | request = self._helpers.bytesToString(item.getRequest()).split('\r\n\r\n')[0] 2334 | req_headers = request.split('\r\n') 2335 | host = self.find_host(req_headers) 2336 | endpoint = req_headers[0] 2337 | self.meta_table.append([host, endpoint, meta]) 2338 | break 2339 | except: 2340 | pass 2341 | 2342 | self.for_table_meta = [] # the two columns that appear on the meta tag in the left table 2343 | meta_header_item = [] 2344 | for metax in self.meta_table: 2345 | meta_values = metax[2].split(" ") 2346 | host = metax[0] 2347 | if len(meta_values[1:]) == 1: # if the meta tag has two items. 2348 | val = [meta_values[1], ""] 2349 | if val not in self.for_table_meta: 2350 | self.for_table_meta.append(val) 2351 | meta_header_item.append(meta_values[1]) #only meta header first item, used below 2352 | else: # if the meta tag has more than two items 2353 | val = [meta_values[1], host] 2354 | if val not in self.for_table_meta: 2355 | self.for_table_meta.append(val) 2356 | meta_header_item.append(meta_values[1]) 2357 | 2358 | 2359 | self.for_table_meta_uniques = sorted([list(x) for x in list({tuple(i) for i in self.for_table_meta})]) 2360 | 2361 | self.meta_headers_already_in_table = [] 2362 | 2363 | #self.model_tab_meta.putClientProperty("html.disable", None) 2364 | last_meta = '' 2365 | k = 0 2366 | for table_entry_meta in self.for_table_meta_uniques: 2367 | for keyword in keywords: 2368 | # Apply filter to meta headers 2369 | if keyword.lower().strip() in table_entry_meta[0] or keyword.lower().strip() in table_entry_meta[1] or self.filter.getText() == "To filter headers enter keywords (separated by a comma)" or self.filter.getText() == "": 2370 | 2371 | if last_meta != table_entry_meta[0] and k > 0: 2372 | self.model_tab_meta.insertRow(self.last_row_meta, [''.format(self.color1) + '-' * 300 + '', ''.format(self.color1) + '-' * 300 + '' * 300]) 2373 | self.last_row_meta += 1 2374 | 2375 | if table_entry_meta[0] not in self.meta_headers_already_in_table: 2376 | self.meta_headers_already_in_table.append(table_entry_meta[0]) 2377 | self.model_tab_meta.insertRow(self.last_row_meta, table_entry_meta) 2378 | self.last_row_meta += 1 2379 | else: 2380 | self.model_tab_meta.insertRow(self.last_row_meta, ["",table_entry_meta[1]]) 2381 | self.last_row_meta += 1 2382 | last_meta = table_entry_meta[0] 2383 | k += 1 2384 | 2385 | self.last_len_meta = len(history2) 2386 | return 2387 | 2388 | def filter_entries_worker(self): 2389 | """Applies the supplied filter(s) to the Header-Host table. If no filters are applied, all available entries are shown.""" 2390 | self.clear_table() 2391 | self.read_headers() 2392 | 2393 | #if True: 2394 | if self.preset_filters.getSelectedItem() == "Request + Response + ": 2395 | self.get_meta_tags() 2396 | 2397 | global history1 2398 | history1 = [] 2399 | history1 = self._callbacks.getProxyHistory() 2400 | self.progressBar.setIndeterminate(0) 2401 | for k_progress, item in enumerate(history1): # ver si puedo coger el index de la request para ponerlo luego en la endpoint table 2402 | if k_progress % 20 == 0: 2403 | self.progressBar.setValue(100 * k_progress // len(history1)) 2404 | 2405 | # Sometimes some strange errors happen for some requests, with this we just skip them. 2406 | '''algunas fallan en algun punto de aqui dentro''' 2407 | try: 2408 | request = self._helpers.bytesToString(item.getRequest()).split('\r\n\r\n')[0] 2409 | req_headers = request.split('\r\n') 2410 | 2411 | # -------- find the host for every request --------# 2412 | host = self.find_host(req_headers) 2413 | 2414 | if (host, req_headers[0]) not in host_endpoint: #si encuentro el index del history meterlo en la siguiente linea 2415 | host_endpoint.append((host, req_headers[0])) 2416 | # -------------------- requests -------------------# 2417 | 2418 | for req_head in req_headers[1:]: 2419 | req_head_name = req_head.split(': ')[0] 2420 | # mira si ya existe ese header en el dict 2421 | if req_head_name in self.req_header_dict: 2422 | if host not in self.req_header_dict[req_head_name]: 2423 | self.req_header_dict[req_head_name].append(host) 2424 | # si no existe el header crea la primera entrada 2425 | else: 2426 | self.req_header_dict[req_head_name] = [host] 2427 | 2428 | # anade a otra table las lineas que iran en la extension, poniendo celdas vacias en el header name para no repetir cuando hay varios host con el mismo header 2429 | # ----------------- responses ---------------# 2430 | response = self._helpers.bytesToString(item.getResponse()).split('\r\n\r\n')[0] 2431 | resp_headers = response.split('\r\n') 2432 | for resp_head in resp_headers[1:]: 2433 | resp_head_name = resp_head.split(': ')[0] 2434 | if resp_head_name in self.resp_header_dict: 2435 | if host not in self.resp_header_dict[resp_head_name]: 2436 | self.resp_header_dict[resp_head_name].append(host) 2437 | else: 2438 | self.resp_header_dict[resp_head_name] = [host] 2439 | except: 2440 | request = self._helpers.bytesToString(item.getRequest()).split('\r\n\r\n')[0] 2441 | req_headers = request.split('\r\n') 2442 | # el siguiente ya contiene toda la history, no pensar en que vienen otras request luego, ya estan todas 2443 | 2444 | req_keys = sorted(list(self.req_header_dict.keys())) 2445 | resp_keys = sorted(list(self.resp_header_dict.keys())) 2446 | 2447 | 2448 | k2 = 0 # se usa para meter en el output file los titulos de request y response 2449 | for keys in [req_keys, resp_keys]: # seguro que esto hace lo que debe? es un array de 2 arrays, no uno solo con todas las keys, ok, creo que esto lo puse asi para no duplicar el bloque de abajo y hacer lo mismo para requests y responses con este for sin duplicar codigo, era por eso, 100% seguro 2450 | 2451 | if keys == req_keys: 2452 | self.for_table = self.for_req_table 2453 | self.header_dict = self.req_header_dict 2454 | self.dataModel_tab = self.model_tab_req 2455 | else: 2456 | self.for_table = self.for_resp_table 2457 | self.header_dict = self.resp_header_dict 2458 | self.dataModel_tab = self.model_tab_resp 2459 | 2460 | 2461 | for key in keys: 2462 | added_something = False #used at the end of the loop to determine if a dash line should be added 2463 | k2 += 1 2464 | k1 = 0 2465 | if k2 == 1: 2466 | self.header_host_table.append(["", "//---------------- REQUEST HEADERS -----------------//", "\n"]) # used for saving data to file in disk 2467 | 2468 | if k2 == len(req_keys) + 1: 2469 | self.header_host_table.append(["\n", "//---------------- RESPONSE HEADERS -----------------//", "\n"]) # used for saving data to file in disk 2470 | 2471 | 2472 | for host in self.header_dict[key]: 2473 | # Apply the filter: 2474 | keywords = self.filter.getText().lower().split(',') 2475 | 2476 | for keyword in keywords: 2477 | if keyword.lower().strip() in host.lower() or keyword.lower() in key.lower() or self.filter.getText() == "To filter headers enter keywords (separated by a comma)": 2478 | if [key, host] not in self.for_table: 2479 | if k1 == 0 and key not in self.headers_already_in_table: 2480 | self.for_table.append([''.format(self.color1) + key + '', host]) # used for displaying data in Host-Header table 2481 | ############## self.for_table.putClientProperty("html.disable", None) 2482 | added_something = True 2483 | self.header_host_table.append([key, key, host]) # used for saving data to file in disk 2484 | if key not in self.headers_already_in_table: 2485 | self.headers_already_in_table.append(key) 2486 | k1 = 1 2487 | else: 2488 | self.for_table.append(["", host]) 2489 | self.header_host_table.append([key, "", host]) 2490 | if key not in self.headers_already_in_table: 2491 | self.headers_already_in_table.append(key) 2492 | 2493 | 2494 | 2495 | # if some line was added to the header - host table, add a dashed line at the end. If not, don't add it 2496 | if added_something: 2497 | self.for_table.append([''.format(self.color1) + '-' * 300 + '', ''.format(self.color1) + '-' * 300 + '' * 300]) 2498 | 2499 | # enter only new rows in for_table, dont reload all the table every time (probably there should be something to check if some entries were deleted form history. create a variable that counts up to 5 every time the button is clicked and then compares the history with the stored history, to check for missing entries that should be removed from the history1 variable or from self.for_table?) 2500 | for table_entry in self.for_table[self.last_len:]: 2501 | self.dataModel_tab.insertRow(self.last_row, table_entry) 2502 | self.last_row += 1 2503 | self.last_row = 0 2504 | self.for_table = [] 2505 | self.last_len = len(history1) 2506 | 2507 | # update config file with last filter type used 2508 | if self.preset_filters.getSelectedItem() != self.config_dict["last_filter_type"]: 2509 | self.config_dict["last_filter_type"] = self.preset_filters.getSelectedItem() 2510 | self.update_config() 2511 | 2512 | self.framewait.setVisible(False) 2513 | return 2514 | 2515 | def filter_entries(self, event): 2516 | thread_show_progress = threading.Thread(target=self.determine_progress) 2517 | filter_entries_worker = threading.Thread(target=self.filter_entries_worker) 2518 | thread_show_progress.start() 2519 | filter_entries_worker.start() 2520 | 2521 | def createMenuItems(self, context_menu): 2522 | """Adds an entry to Burp's context menu, when it is clicked the floating window with headers information of the selected item(s) in Burp history is shown""" 2523 | self.context = context_menu 2524 | menu_list = ArrayList() 2525 | menu_list.add(JMenuItem("Headers", actionPerformed=self.show_window)) 2526 | return menu_list 2527 | 2528 | def pullRequest(self, event): 2529 | """Creates the string to be submitted for contributing with new headers information""" 2530 | final_text = self.new_header_name.getText() + "&&" + \ 2531 | self.new_header_description.getText() + "&&" + \ 2532 | self.new_header_example.getText() + "&&" + \ 2533 | self.new_header_url.getText() + "&&" + \ 2534 | self.new_header_risks.getText() 2535 | self.to_submit_text.setLineWrap(True) 2536 | self.to_submit_text.setText(final_text) 2537 | return 2538 | 2539 | def show_window(self, event): 2540 | """Creates the floating window with the information about the headers present in the selected items of Burp's history""" 2541 | # ----------------------------------------- crear diccionarios ---------------------------------------------# 2542 | dict_req_headers = {} 2543 | req_headers_description = open('request_headers.txt','r') 2544 | for line in req_headers_description.readlines(): 2545 | line_split = line.split('&&') 2546 | header_name = line_split[0] 2547 | header_description = line_split[1] 2548 | dict_req_headers[header_name] = header_description 2549 | req_headers_description.close() 2550 | 2551 | dict_resp_headers = {} 2552 | resp_headers_description = open('response_headers.txt','r') 2553 | for line in resp_headers_description.readlines(): 2554 | line_split = line.split('&&') 2555 | header_name = line_split[0] 2556 | header_description = line_split[1] 2557 | dict_resp_headers[header_name] = header_description 2558 | resp_headers_description.close() 2559 | 2560 | # ------------- create tablas ------------------# 2561 | 2562 | http_traffic = self.context.getSelectedMessages() 2563 | self.tableDataReq = [] 2564 | self.tableDataResp = [] 2565 | self.aux_names_req = [] 2566 | self.aux_names_resp = [] 2567 | 2568 | for traffic in http_traffic: 2569 | request = self._helpers.bytesToString(traffic.getRequest()).split('\r\n\r\n')[0] 2570 | req_headers = request.split('\r\n') 2571 | 2572 | # -------- find the host for every request --------# 2573 | host = self.find_host(req_headers) 2574 | 2575 | # -------------------- requests -------------------# 2576 | for req_head in req_headers[1:]: 2577 | req_head_name = req_head.split(': ')[0] 2578 | try: 2579 | description = dict_req_headers[req_head_name] 2580 | except: 2581 | description = " --- Description unavailable --- " 2582 | if req_head_name not in self.aux_names_req: 2583 | self.tableDataReq.append([req_head_name, description, host]) 2584 | self.aux_names_req.append(req_head_name) 2585 | 2586 | 2587 | # ----------------- responses ---------------# 2588 | response = self._helpers.bytesToString(traffic.getResponse()).split('\r\n\r\n')[0] 2589 | resp_headers = response.split('\r\n') 2590 | for resp_head in resp_headers[1:]: 2591 | resp_head_name = resp_head.split(': ')[0] 2592 | try: 2593 | description = dict_resp_headers[resp_head_name] 2594 | except: 2595 | description = " --- Description unavailable --- " 2596 | if resp_head_name not in self.aux_names_resp: 2597 | self.tableDataResp.append([resp_head_name, description, host]) 2598 | self.aux_names_resp.append(resp_head_name) 2599 | 2600 | 2601 | self.tableDataReq.sort() 2602 | self.tableDataResp.sort() 2603 | '''el numero en bytes es el valor binario de ascii, por ej N=78, y la newline que separa header y body es 0D 0A 0D 0A = 13 10 13 10 = \r\n\r\n''' 2604 | 2605 | # --------------- create tabs and place JTables inside -------------# 2606 | #tab1 = JPanel() 2607 | #tab2 = JPanel() 2608 | 2609 | frame = JFrame("Headers") 2610 | frame.setSize(850, 350) 2611 | #colNames = ('Header name','Header description') 2612 | #todas las columnas del archivo: header name && description && example && (permanent, no se que es esto) && 2613 | 2614 | 2615 | c=[x[0:2] for x in self.tableDataReq] 2616 | self.model_window_req = IssueTableModel(c, self.colNames) 2617 | self.tableReq = IssueTable(self.model_window_req, "window") 2618 | self.tableReq.getColumnModel().getColumn(0).setPreferredWidth(200) 2619 | self.tableReq.getColumnModel().getColumn(1).setPreferredWidth(800) 2620 | 2621 | descriptionColumnReq = self.tableReq.getColumnModel().getColumn(1) 2622 | 2623 | """ 2624 | #Set up tool tips for the description cells. 2625 | renderer = DefaultTableCellRenderer(); 2626 | renderer.setToolTipText(self.getToolTipText(event)); 2627 | #renderer.setToolTipText("Click for combo box"); 2628 | descriptionColumnReq.setCellRenderer(renderer); 2629 | #} 2630 | """ 2631 | 2632 | 2633 | d=[x[0:2] for x in self.tableDataResp] 2634 | self.model_window_resp = IssueTableModel(d, self.colNames) 2635 | self.tableResp = IssueTable(self.model_window_resp, "window") 2636 | self.tableResp.getColumnModel().getColumn(0).setPreferredWidth(200) 2637 | self.tableResp.getColumnModel().getColumn(1).setPreferredWidth(800) 2638 | 2639 | # It's necessary to place JScrollPane insde a JPanel for it to resize and show the scrollbar: 2640 | panelTab1 = JPanel(BorderLayout()) 2641 | panelTab1.add(JScrollPane(self.tableReq)) 2642 | panelTab2 = JPanel(BorderLayout()) 2643 | panelTab2.add(JScrollPane(self.tableResp)) 2644 | panelTab3 = JPanel(GridBagLayout()) 2645 | 2646 | # ======================================================== # 2647 | c = GridBagConstraints() 2648 | c.gridx = 1 2649 | y_pos = 0 2650 | c.gridy = y_pos 2651 | c.anchor = GridBagConstraints.WEST 2652 | text1 = JLabel(" ") 2653 | panelTab3.add(text1 ,c) 2654 | 2655 | c = GridBagConstraints() 2656 | c.gridx = 1 2657 | y_pos += 1 2658 | c.gridy = y_pos 2659 | c.anchor = GridBagConstraints.WEST 2660 | text1 = JLabel("Web technologies evolve fast and new headers constantly pop up. Please, contribute information about undocumented headers!") 2661 | panelTab3.add( text1 , c) 2662 | 2663 | c = GridBagConstraints() 2664 | c.gridx = 1 2665 | y_pos += 1 2666 | c.gridy = y_pos 2667 | c.anchor = GridBagConstraints.WEST 2668 | text1 = JLabel( "To do it, fill in the fields below and press the button to create a new entry. Then create a pull request to:") 2669 | panelTab3.add(text1 ,c) 2670 | 2671 | c = GridBagConstraints() 2672 | c.gridx = 1 2673 | y_pos += 1 2674 | c.gridy = y_pos 2675 | c.anchor = GridBagConstraints.WEST 2676 | text1 = JLabel("www.github.com/dh0ck/Headers with the generated text, or send it to @dh0ck via telegram.") 2677 | panelTab3.add(text1 ,c) 2678 | 2679 | c = GridBagConstraints() 2680 | c.gridx = 1 2681 | y_pos += 1 2682 | c.gridy = y_pos 2683 | c.anchor = GridBagConstraints.WEST 2684 | text1 = JLabel("Alternatively, add these lines to your local file request_headers.txt and response_headers.txt") 2685 | panelTab3.add(text1 ,c) 2686 | 2687 | 2688 | c = GridBagConstraints() 2689 | c.gridx = 1 2690 | y_pos += 1 2691 | c.gridy = y_pos 2692 | c.anchor = GridBagConstraints.WEST 2693 | text1 = JLabel(" ") 2694 | panelTab3.add(text1 ,c) 2695 | 2696 | # ========================== add text fields ============================== # 2697 | fields_names = [' Header Name: ', ' Header Description: ', ' Example: ', ' URL explaining header: ', ' Potential header risks: '] 2698 | 2699 | self.new_header_name = JTextField('') 2700 | self.new_header_description = JTextField('') 2701 | self.new_header_example = JTextField('') 2702 | self.new_header_url = JTextField('') 2703 | self.new_header_risks = JTextField('') 2704 | 2705 | fields = [ self.new_header_name, self.new_header_description, self.new_header_example, self.new_header_url, self.new_header_risks ] 2706 | 2707 | for k, field in enumerate(fields): 2708 | c = GridBagConstraints() 2709 | c.gridx = 0 2710 | y_pos += 1 2711 | c.gridy = y_pos 2712 | c.anchor = GridBagConstraints.EAST 2713 | panelTab3.add(JLabel(fields_names[k]), c) 2714 | 2715 | c = GridBagConstraints() 2716 | c.fill = GridBagConstraints.HORIZONTAL 2717 | c.weightx = 1 2718 | c.gridx = 1 2719 | c.gridy = y_pos 2720 | panelTab3.add(fields[k] , c) 2721 | 2722 | # ======================= add button ================================= # 2723 | 2724 | c = GridBagConstraints() 2725 | c.fill = GridBagConstraints.HORIZONTAL 2726 | c.weightx = 1 2727 | c.gridx = 1 2728 | y_pos += 1 2729 | c.gridy = y_pos 2730 | but = JButton("submit", actionPerformed = self.pullRequest) 2731 | but.setToolTipText("Click to generate a new entry. Please, submit it to @dh0ck or create a pull request. It will be reviewed before approval. Thanks for contributing!!!") 2732 | panelTab3.add(but, c) 2733 | 2734 | # ========================== show entry to be submitted ============================== # 2735 | c = GridBagConstraints() 2736 | c.fill = GridBagConstraints.HORIZONTAL 2737 | c.weightx = 1 2738 | c.weighty = 2 2739 | c.gridx = 1 2740 | y_pos += 1 2741 | c.gridy = y_pos 2742 | self.to_submit_text = JTextArea( '' , editable = 0, rows = 3) 2743 | panelTab3.add(JScrollPane(self.to_submit_text), c) 2744 | 2745 | # ========================= about panel =============================== # 2746 | panelTab4 = JPanel() 2747 | panelTab4.putClientProperty("html.disable", None) 2748 | panelTab4.setLayout(BoxLayout(panelTab4, BoxLayout.Y_AXIS ) ) 2749 | 2750 | a1 = " Thank you for using Headers" 2751 | a2 = " For tutorials, please visit:" 2752 | a3 = " https://github.com/dh0ck/Headers" 2753 | a4 = " " 2754 | a5 = " " 2755 | a6 = " If you have requests or suggestions please let me know via telegram (@dh0ck) or send pull requests to the GitHub repo." 2756 | a7 = " " 2757 | a8 = " " 2758 | 2759 | for label in [a1, a2, a3, a4, a5, a6, a7, a8]: 2760 | panelTab4.add(JLabel(label)) 2761 | 2762 | 2763 | tabs = JTabbedPane() 2764 | tabs.addTab('Requests', panelTab1) 2765 | tabs.addTab('Responses', panelTab2) 2766 | tabs.addTab('Add new headers', panelTab3) 2767 | tabs.addTab('About', panelTab4) 2768 | frame.add(tabs) 2769 | 2770 | frame.setVisible(True) 2771 | frame.toFront() 2772 | frame.setAlwaysOnTop(True) 2773 | 2774 | 2775 | 2776 | 2777 | --------------------------------------------------------------------------------