├── BappDescription.html
├── BappManifest.bmf
├── LICENSE
├── README.md
└── FransLinkfinder.py
/BappDescription.html:
--------------------------------------------------------------------------------
1 | Burp Extension for a passively scanning JavaScript files for endpoint links.
2 |
3 | - Export results the text file
4 | - Exclude specific 'js' files e.g. jquery, google-analytics
5 |
6 |
--------------------------------------------------------------------------------
/BappManifest.bmf:
--------------------------------------------------------------------------------
1 | Uuid: 0e61c786db0c4ac787a08c4516d52ccf
2 | ExtensionType: 2
3 | Name: JS Link Finder
4 | RepoName: js-link-finder
5 | ScreenVersion: 1.0.0
6 | SerialVersion: 1
7 | MinPlatformVersion: 0
8 | ProOnly: True
9 | Author: InitRoot
10 | ShortDescription: Burp Extension for passively scanning JavaScript files for endpoint links.
11 | EntryPoint: FransLinkfinder.py
12 | BuildCommand:
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 InitRoot
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BurpJSLinkFinder - Find links within JS files.
2 | Burp Extension for a passive scanning JS files for endpoint links.
3 | - Export results the text file
4 | - Exclude specific 'js' files e.g. jquery, google-analytics
5 |
6 | Copyright (c) 2019 Frans Hendrik Botes
7 |
8 |
9 | Credit to https://github.com/GerbenJavado/LinkFinder for the idea and regex
10 |
11 | ## Setup
12 | For use with the professional version of Burp Suite. Ensure you have JPython loaded and setup
13 | before installing.
14 |
15 | You can modify the exclusion list by updating the strings on line 33.
16 | Currently any strings that include the included words will not be analysed.
17 |
18 | ```
19 | # Needed params
20 |
21 | JSExclusionList = ['jquery', 'google-analytics','gpt.js']
22 |
23 | ```
24 |
25 | ## Usage
26 |
27 | Once you've loaded the plugin there is some things to consider.
28 | Burp performs threading on passive scanning by itself. This can be controlled by looking at the Scanner options.
29 | For quick scanning I make use of the following settings with this plugin:
30 |
31 | Scanner --> Live Scanning
32 | - Live Active Scanning : Disabled
33 | - Live Passive Scanning : Use suite scope
34 |
35 | As with ALL the burp scanner items, you have to give it a minute or so to work through the data. You shouldn't be waiting several minutes for a result tho.
36 |
37 | If the links have been excluded monitor the OUTPUT of the extension under the Extender options to verify.
38 |
39 |
40 | ## Screenshot
41 | 
42 |
43 | ## Update
44 | - Added swing memory management (14/06/2019)
45 | - Added exclusion list on line 33 of code ['jquery', 'google-analytics','gpt.js'] (14/06/2019)
46 | - Added ability to export files (15/06/2019)
47 |
--------------------------------------------------------------------------------
/FransLinkfinder.py:
--------------------------------------------------------------------------------
1 | #
2 | # BurpLinkFinder - Find links within JS files.
3 | #
4 | # Copyright (c) 2019 Frans Hendrik Botes
5 | # Credit to https://github.com/GerbenJavado/LinkFinder for the idea and regex
6 | #
7 | from burp import IBurpExtender, IScannerCheck, IScanIssue, ITab
8 | from java.io import PrintWriter
9 | from java.net import URL
10 | from java.util import ArrayList, List
11 | from java.util.regex import Matcher, Pattern
12 | import binascii
13 | import base64
14 | import re
15 | from javax import swing
16 | from java.awt import Font, Color
17 | from threading import Thread
18 | from array import array
19 | from java.awt import EventQueue
20 | from java.lang import Runnable
21 | from thread import start_new_thread
22 | from javax.swing import JFileChooser
23 |
24 | # Using the Runnable class for thread-safety with Swing
25 | class Run(Runnable):
26 | def __init__(self, runner):
27 | self.runner = runner
28 |
29 | def run(self):
30 | self.runner()
31 |
32 | # Needed params
33 |
34 | JSExclusionList = ['jquery', 'google-analytics','gpt.js']
35 |
36 | class BurpExtender(IBurpExtender, IScannerCheck, ITab):
37 | def registerExtenderCallbacks(self, callbacks):
38 | self.callbacks = callbacks
39 | self.helpers = callbacks.getHelpers()
40 | callbacks.setExtensionName("BurpJSLinkFinder")
41 |
42 | callbacks.issueAlert("BurpJSLinkFinder Passive Scanner enabled")
43 |
44 | stdout = PrintWriter(callbacks.getStdout(), True)
45 | stderr = PrintWriter(callbacks.getStderr(), True)
46 | callbacks.registerScannerCheck(self)
47 | self.initUI()
48 | self.callbacks.addSuiteTab(self)
49 |
50 | print ("Burp JS LinkFinder loaded.")
51 | print ("Copyright (c) 2019 Frans Hendrik Botes")
52 | self.outputTxtArea.setText("Burp JS LinkFinder loaded." + "\n" + "Copyright (c) 2019 Frans Hendrik Botes" + "\n")
53 |
54 | def initUI(self):
55 | self.tab = swing.JPanel()
56 |
57 | # UI for Output
58 | self.outputLabel = swing.JLabel("LinkFinder Log:")
59 | self.outputLabel.setFont(Font("Tahoma", Font.BOLD, 14))
60 | self.outputLabel.setForeground(Color(255,102,52))
61 | self.logPane = swing.JScrollPane()
62 | self.outputTxtArea = swing.JTextArea()
63 | self.outputTxtArea.setFont(Font("Consolas", Font.PLAIN, 12))
64 | self.outputTxtArea.setLineWrap(True)
65 | self.logPane.setViewportView(self.outputTxtArea)
66 | self.clearBtn = swing.JButton("Clear Log", actionPerformed=self.clearLog)
67 | self.exportBtn = swing.JButton("Export Log", actionPerformed=self.exportLog)
68 | self.parentFrm = swing.JFileChooser()
69 |
70 |
71 |
72 | # Layout
73 | layout = swing.GroupLayout(self.tab)
74 | layout.setAutoCreateGaps(True)
75 | layout.setAutoCreateContainerGaps(True)
76 | self.tab.setLayout(layout)
77 |
78 | layout.setHorizontalGroup(
79 | layout.createParallelGroup()
80 | .addGroup(layout.createSequentialGroup()
81 | .addGroup(layout.createParallelGroup()
82 | .addComponent(self.outputLabel)
83 | .addComponent(self.logPane)
84 | .addComponent(self.clearBtn)
85 | .addComponent(self.exportBtn)
86 | )
87 | )
88 | )
89 |
90 | layout.setVerticalGroup(
91 | layout.createParallelGroup()
92 | .addGroup(layout.createParallelGroup()
93 | .addGroup(layout.createSequentialGroup()
94 | .addComponent(self.outputLabel)
95 | .addComponent(self.logPane)
96 | .addComponent(self.clearBtn)
97 | .addComponent(self.exportBtn)
98 | )
99 | )
100 | )
101 |
102 | def getTabCaption(self):
103 | return "BurpJSLinkFinder"
104 |
105 | def getUiComponent(self):
106 | return self.tab
107 |
108 | def clearLog(self, event):
109 | self.outputTxtArea.setText("Burp JS LinkFinder loaded." + "\n" + "Copyright (c) 2019 Frans Hendrik Botes" + "\n" )
110 |
111 | def exportLog(self, event):
112 | chooseFile = JFileChooser()
113 | ret = chooseFile.showDialog(self.logPane, "Choose file")
114 | filename = chooseFile.getSelectedFile().getCanonicalPath()
115 | print("\n" + "Export to : " + filename)
116 | open(filename, 'w', 0).write(self.outputTxtArea.text)
117 |
118 |
119 | def doPassiveScan(self, ihrr):
120 |
121 | try:
122 | urlReq = ihrr.getUrl()
123 | testString = str(urlReq)
124 | linkA = linkAnalyse(ihrr,self.helpers)
125 | # check if JS file
126 | if ".js" in str(urlReq):
127 | # Exclude casual JS files
128 | if any(x in testString for x in JSExclusionList):
129 | print("\n" + "[-] URL excluded " + str(urlReq))
130 | else:
131 | self.outputTxtArea.append("\n" + "[+] Valid URL found: " + str(urlReq))
132 | issueText = linkA.analyseURL()
133 | for counter, issueText in enumerate(issueText):
134 | #print("TEST Value returned SUCCESS")
135 | self.outputTxtArea.append("\n" + "\t" + str(counter)+' - ' +issueText['link'])
136 |
137 | issues = ArrayList()
138 | issues.add(SRI(ihrr, self.helpers))
139 | return issues
140 | except UnicodeEncodeError:
141 | print ("Error in URL decode.")
142 | return None
143 |
144 |
145 | def consolidateDuplicateIssues(self, isb, isa):
146 | return -1
147 |
148 | def extensionUnloaded(self):
149 | print "Burp JS LinkFinder unloaded"
150 | return
151 |
152 | class linkAnalyse():
153 |
154 | def __init__(self, reqres, helpers):
155 | self.helpers = helpers
156 | self.reqres = reqres
157 |
158 |
159 | regex_str = """
160 |
161 | (?:"|') # Start newline delimiter
162 |
163 | (
164 | ((?:[a-zA-Z]{1,10}://|//) # Match a scheme [a-Z]*1-10 or //
165 | [^"'/]{1,}\. # Match a domainname (any character + dot)
166 | [a-zA-Z]{2,}[^"']{0,}) # The domainextension and/or path
167 |
168 | |
169 |
170 | ((?:/|\.\./|\./) # Start with /,../,./
171 | [^"'><,;| *()(%%$^/\\\[\]] # Next character can't be...
172 | [^"'><,;|()]{1,}) # Rest of the characters can't be
173 |
174 | |
175 |
176 | ([a-zA-Z0-9_\-/]{1,}/ # Relative endpoint with /
177 | [a-zA-Z0-9_\-/]{1,} # Resource name
178 | \.(?:[a-zA-Z]{1,4}|action) # Rest + extension (length 1-4 or action)
179 | (?:[\?|/][^"|']{0,}|)) # ? mark with parameters
180 |
181 | |
182 |
183 | ([a-zA-Z0-9_\-]{1,} # filename
184 | \.(?:php|asp|aspx|jsp|json|
185 | action|html|js|txt|xml) # . + extension
186 | (?:\?[^"|']{0,}|)) # ? mark with parameters
187 |
188 | )
189 |
190 | (?:"|') # End newline delimiter
191 |
192 | """
193 |
194 | def parser_file(self, content, regex_str, mode=1, more_regex=None, no_dup=1):
195 | #print ("TEST parselfile #2")
196 | regex = re.compile(regex_str, re.VERBOSE)
197 | items = [{"link": m.group(1)} for m in re.finditer(regex, content)]
198 | if no_dup:
199 | # Remove duplication
200 | all_links = set()
201 | no_dup_items = []
202 | for item in items:
203 | if item["link"] not in all_links:
204 | all_links.add(item["link"])
205 | no_dup_items.append(item)
206 | items = no_dup_items
207 |
208 | # Match Regex
209 | filtered_items = []
210 | for item in items:
211 | # Remove other capture groups from regex results
212 | if more_regex:
213 | if re.search(more_regex, item["link"]):
214 | #print ("TEST parselfile #3")
215 | filtered_items.append(item)
216 | else:
217 | filtered_items.append(item)
218 | return filtered_items
219 |
220 | # Potential for use in the future...
221 | def threadAnalysis(self):
222 | thread = Thread(target=self.analyseURL(), args=(session,))
223 | thread.daemon = True
224 | thread.start()
225 |
226 | def analyseURL(self):
227 |
228 | endpoints = ""
229 | #print("TEST AnalyseURL #1")
230 | mime_type=self.helpers.analyzeResponse(self.reqres.getResponse()).getStatedMimeType()
231 | if mime_type.lower() == 'script':
232 | url = self.reqres.getUrl()
233 | encoded_resp=binascii.b2a_base64(self.reqres.getResponse())
234 | decoded_resp=base64.b64decode(encoded_resp)
235 | endpoints=self.parser_file(decoded_resp, self.regex_str)
236 | #print("TEST AnalyseURL #2")
237 | return endpoints
238 | return endpoints
239 |
240 |
241 | class SRI(IScanIssue,ITab):
242 | def __init__(self, reqres, helpers):
243 | self.helpers = helpers
244 | self.reqres = reqres
245 |
246 | def getHost(self):
247 | return self.reqres.getHost()
248 |
249 | def getPort(self):
250 | return self.reqres.getPort()
251 |
252 | def getProtocol(self):
253 | return self.reqres.getProtocol()
254 |
255 | def getUrl(self):
256 | return self.reqres.getUrl()
257 |
258 | def getIssueName(self):
259 | return "Linkfinder Analysed JS files"
260 |
261 | def getIssueType(self):
262 | return 0x08000000 # See http:#portswigger.net/burp/help/scanner_issuetypes.html
263 |
264 | def getSeverity(self):
265 | return "Information" # "High", "Medium", "Low", "Information" or "False positive"
266 |
267 | def getConfidence(self):
268 | return "Certain" # "Certain", "Firm" or "Tentative"
269 |
270 | def getIssueBackground(self):
271 | return str("JS files holds links to other parts of web applications. Refer to TAB for results.")
272 |
273 | def getRemediationBackground(self):
274 | return "This is an informational finding only.
"
275 |
276 | def getIssueDetail(self):
277 | return str("Burp Scanner has analysed the following JS file for links: "
278 | "%s
" % (self.reqres.getUrl().toString()))
279 |
280 | def getRemediationDetail(self):
281 | return None
282 |
283 | def getHttpMessages(self):
284 | #print ("................raising issue................")
285 | rra = [self.reqres]
286 | return rra
287 |
288 | def getHttpService(self):
289 | return self.reqres.getHttpService()
290 |
291 |
292 | if __name__ in ('__main__', 'main'):
293 | EventQueue.invokeLater(Run(BurpExtender))
294 |
--------------------------------------------------------------------------------