Skip to content

Commit 5b55342

Browse files
committed
See changelog for 0.6
modified: CHANGELOG.md modified: DetectDynamicJS.py modified: README.md
1 parent 2afca80 commit 5b55342

3 files changed

Lines changed: 104 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
<a name="0.6"></a>
4+
## v0.6 (Marsellus Wallace)
5+
- issue a third request to reduce false positives
6+
- report authentication-based findings as (Medium, Firm)
7+
- report generic dynamic as (Information, Certain), might be removed in future version
8+
39
<a name="0.5"></a>
410
## v0.5 (Jules Winnfield)
511
- Bug fix

DetectDynamicJS.py

Lines changed: 91 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@
2525
from burp import IHttpRequestResponse
2626
from burp import IScanIssue
2727
from array import array
28+
from time import sleep
2829
import difflib
2930
except ImportError:
3031
print "Failed to load dependencies. This issue maybe caused by using an unstable Jython version."
3132

32-
VERSION = '0.5'
33-
VERSIONNAME = 'Jules Winnfield'
33+
VERSION = '0.6'
34+
VERSIONNAME = 'Marsellus Wallace'
3435

3536

3637
class BurpExtender(IBurpExtender, IScannerCheck, IExtensionStateListener, IHttpRequestResponse):
@@ -57,11 +58,12 @@ def extensionUnloaded(self):
5758
def doActiveScan(self, baseRequestResponse, insertionPoint):
5859
return []
5960

60-
# Burp Scanner invokes this method for each base request/response that is
61-
# passively scanned
6261
def doPassiveScan(self, baseRequestResponse):
6362
# WARNING: NOT REALLY A PASSIVE SCAN!
64-
# doPassiveScan issues always one request per request scanned
63+
# doPassiveScan issues always at least one, if not two requests,
64+
# per request scanned
65+
# This is, because the insertionPoint idea doesn't work well
66+
# for this test.
6567
scan_issues = []
6668
possibleFileEndings = ["js", "jsp", "json"]
6769
possibleContentTypes = ["javascript", "ecmascript", "jscript", "json"]
@@ -107,16 +109,55 @@ def doPassiveScan(self, baseRequestResponse):
107109
requestHeaders = request.tostring()[:requestBodyOffset].split('\r\n')
108110
requestBody = request.tostring()[requestBodyOffset:]
109111
modified_headers = "\n".join(header for header in requestHeaders if "Cookie" not in header)
110-
newResponse = self._callbacks.makeHttpRequest(self._requestResponse.getHttpService(), self._helpers.stringToBytes(modified_headers+body))
112+
newResponse = self._callbacks.makeHttpRequest(self._requestResponse.getHttpService(), self._helpers.stringToBytes(modified_headers+requestBody))
111113
issue = self.compareResponses(newResponse, self._requestResponse)
112114
if issue:
115+
# If response is script, check if script is dynamic
116+
if self.isScript(newResponse):
117+
# sleep, in case this is a generically time stamped script
118+
sleep(1)
119+
secondResponse = self._callbacks.makeHttpRequest(self._requestResponse.getHttpService(), self._helpers.stringToBytes(modified_headers+requestBody))
120+
isDynamic = self.compareResponses(secondResponse, newResponse)
121+
if isDynamic:
122+
issue = self.reportDynamicOnly(newResponse, self._requestResponse, secondResponse)
113123
scan_issues.append(issue)
114-
115124
if len(scan_issues) > 0:
116125
return scan_issues
117126
else:
118127
return None
119-
128+
129+
130+
def isScript(self, requestResponse):
131+
"""Determine if the response is a script"""
132+
possibleContentTypes = ["javascript", "ecmascript", "jscript", "json"]
133+
self._helpers = self._callbacks.getHelpers()
134+
135+
url = self._helpers.analyzeRequest(requestResponse).getUrl()
136+
url = str(url).split("?")[0]
137+
138+
response = requestResponse.getResponse()
139+
responseInfo = self._helpers.analyzeResponse(response)
140+
mimeType = responseInfo.getStatedMimeType().split(';')[0]
141+
inferredMimeType = responseInfo.getInferredMimeType().split(';')[0]
142+
bodyOffset = responseInfo.getBodyOffset()
143+
headers = response.tostring()[:bodyOffset].split('\r\n')
144+
145+
contentLengthL = [x for x in headers if "content-length:" in x.lower()]
146+
if len(contentLengthL) >= 1:
147+
contentLength = int(contentLengthL[0].split(':')[1].strip())
148+
else:
149+
contentLength = 0
150+
151+
if contentLength > 0:
152+
contentType = ""
153+
contentTypeL = [x for x in headers if "content-type:" in x.lower()]
154+
if len(contentTypeL) == 1:
155+
contentType = contentTypeL[0].lower()
156+
statusCode = responseInfo.getStatusCode()
157+
if (any(content in contentType for content in possibleContentTypes) or "script" in inferredMimeType or "script" in mimeType) and (int(statusCode) < 300 or int(statusCode) > 399):
158+
return True
159+
return False
160+
120161

121162
def compareResponses(self, newResponse, oldResponse):
122163
"""Compare two responses in respect to their body contents"""
@@ -133,25 +174,58 @@ def compareResponses(self, newResponse, oldResponse):
133174
result = None
134175
if str(nBody) != str(oBody):
135176
issuename = "Dynamic JavaScript Code Detected"
136-
issuelevel = "Information"
177+
issuelevel = "Medium"
137178
issuedetail = "These two files contain differing contents. Check the contents of the files to ensure that they don't contain sensitive information."
138179
issuebackground = "Dynamically generated JavaScript might contain session or user relevant information. Contrary to regular content that is protected by Same-Origin Policy, scripts can be included by third parties. This can lead to leakage of user/session relevant information."
139180
issueremediation = "Applications should not store user/session relevant data in JavaScript files with known URLs. If strict separation of data and code is not possible, CSRF tokens should be used."
140-
181+
issueconfidence = "Firm"
141182
oOffsets = self.calculateHighlights(nBody, oBody, oBodyOffset)
142183
nOffsets = self.calculateHighlights(oBody, nBody, nBodyOffset)
143184
result = ScanIssue(self._requestResponse.getHttpService(),
144185
self._helpers.analyzeRequest(self._requestResponse).getUrl(),
145-
issuename, issuelevel, issuedetail, issuebackground, issueremediation,
186+
issuename, issuelevel, issuedetail, issuebackground, issueremediation, issueconfidence,
146187
[self._callbacks.applyMarkers(oldResponse, None, oOffsets), self._callbacks.applyMarkers(newResponse, None, nOffsets)])
147188
else:
148189
url = self._helpers.analyzeRequest(newResponse).getUrl()
149190
url = str(url)
150-
print "File "+url+" is the same with and without cookies"
151191

152192
return result
153193

194+
def reportDynamicOnly(self, firstResponse, originalResponse, secondResponse):
195+
"""Report Situation as Dynamic Only"""
196+
issuename = "Dynamic JavaScript Code Detected"
197+
issuelevel = "Information"
198+
issueconfidence = "Certain"
199+
issuedetail = "These files contain differing contents. Check the contents of the files to ensure that they don't contain sensitive information."
200+
issuebackground = "Dynamically generated JavaScript might contain session or user relevant information. Contrary to regular content that is protected by Same-Origin Policy, scripts can be included by third parties. This can lead to leakage of user/session relevant information."
201+
issueremediation = "Applications should not store user/session relevant data in JavaScript files with known URLs. If strict separation of data and code is not possible, CSRF tokens should be used."
154202

203+
nResponse = firstResponse.getResponse()
204+
nResponseInfo = self._helpers.analyzeResponse(nResponse)
205+
nBodyOffset = nResponseInfo.getBodyOffset()
206+
nBody = nResponse.tostring()[nBodyOffset:]
207+
208+
oResponse = originalResponse.getResponse()
209+
oResponseInfo = self._helpers.analyzeResponse(oResponse)
210+
oBodyOffset = oResponseInfo.getBodyOffset()
211+
oBody = oResponse.tostring()[oBodyOffset:]
212+
213+
sResponse = secondResponse.getResponse()
214+
sResponseInfo = self._helpers.analyzeResponse(sResponse)
215+
sBodyOffset = sResponseInfo.getBodyOffset()
216+
sBody = sResponse.tostring()[sBodyOffset:]
217+
218+
oOffsets = self.calculateHighlights(nBody, oBody, oBodyOffset)
219+
nOffsets = self.calculateHighlights(oBody, nBody, nBodyOffset)
220+
sOffsets = self.calculateHighlights(oBody, sBody, sBodyOffset)
221+
result = ScanIssue(self._requestResponse.getHttpService(),
222+
self._helpers.analyzeRequest(self._requestResponse).getUrl(),
223+
issuename, issuelevel, issuedetail, issuebackground, issueremediation, issueconfidence,
224+
[self._callbacks.applyMarkers(originalResponse, None, oOffsets),
225+
self._callbacks.applyMarkers(firstResponse, None, nOffsets),
226+
self._callbacks.applyMarkers(secondResponse, None, sOffsets)])
227+
return result
228+
155229
def calculateHighlights(self, newBody, oldBody, bodyOffset):
156230
"""find the exact points for highlighting the responses"""
157231
s = difflib.SequenceMatcher(None, oldBody, newBody)
@@ -186,14 +260,14 @@ def consolidateDuplicateIssues(self, existingIssue, newIssue):
186260
nResponseInfo = self._helpers.analyzeResponse(nResponse)
187261
nBodyOffset = nResponseInfo.getBodyOffset()
188262
nBody = nResponse.tostring()[nBodyOffset:]
189-
newIssueResponses.append(nBody)
263+
# newIssueResponses.append(nBody)
190264

191265
for oldResponse in existingIssue.getHttpMessages():
192266
oResponse = oldResponse.getResponse()
193267
oResponseInfo = self._helpers.analyzeResponse(oResponse)
194268
oBodyOffset = oResponseInfo.getBodyOffset()
195269
oBody = oResponse.tostring()[oBodyOffset:]
196-
existingIssueResponses.append(oBody)
270+
# existingIssueResponses.append(oBody)
197271

198272
for newIssueResp in newIssueResponses:
199273
for existingIssueResp in existingIssueResponses:
@@ -206,14 +280,15 @@ def consolidateDuplicateIssues(self, existingIssue, newIssue):
206280
return 0
207281

208282
class ScanIssue(IScanIssue):
209-
def __init__(self, httpservice, url, name, severity, detailmsg, background, remediation, requests):
283+
def __init__(self, httpservice, url, name, severity, detailmsg, background, remediation, confidence, requests):
210284
self._url = url
211285
self._httpservice = httpservice
212286
self._name = name
213287
self._severity = severity
214288
self._detailmsg = detailmsg
215289
self._issuebackground = background
216290
self._issueremediation = remediation
291+
self._confidence = confidence
217292
self._httpmsgs = requests
218293

219294
def getUrl(self):
@@ -247,4 +322,4 @@ def getSeverity(self):
247322
return self._severity
248323

249324
def getConfidence(self):
250-
return "Certain"
325+
return self._confidence

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
The DetectDynamicJS Burp Extension provides an additional passive scanner that tries to find differing content in JavaScript files and aid in finding user/session data.
44

5-
Dynamically Generated JavaScript occasionally contains *data* in addition to code. Since, by default, scripts need to be able to be included by third parties, this can lead to leakage. The whole process of how to exploit this behavior is detailed in the paper [The Unexpected Dangers of Dynamic JavaScript](https://www.kittenpics.org/wp-content/uploads/2015/05/script-leakage.pdf) by Sebastian Lekies, Ben Stock, Martin Wentzel and Martin Johns. The paper inspired this extension. I hope this extension will ease the hunt for vulnerabilities described in the aforementioned paper. Release statement with additional information about the extension can be found on the official website [http://www.scip.ch/en/?labs.20151215](http://www.scip.ch/en/?labs.20151215).
5+
Dynamically Generated JavaScript occasionally contains *data* in addition to code. Since, by default, scripts need to be able to be included by third parties, this can lead to leakage. For more information about the reasons, the ways to find or how to exploit this issue, see [Cross-Site Script Inclusion](http://www.scip.ch/en/?labs.20160414).
6+
7+
## Note on Release 0.6 (Marsellus Wallace)
8+
If necessary, the extension will now issue two requests to reduce false positives. Also, depending on how the issue was discovered, it might be rated as Information and not as Medium.
69

710
## Note on Release 0.3 (Mia Wallace)
811
We decided to improve the usability by not requiring the user to request both the non-authenticated version and the authenticated version of the script. Instead, when calling a passive scan of the authenticated version of the script, the extension requests the non-authenticated version by itself. This has proven to be more comfortable. It should be noted that the extension is still a passive scanner module, despite the fact that it issues a request per scanned file.
@@ -16,13 +19,14 @@ Some default installations of Python might not install difflib. In that case you
1619
![Screenshot of Issue](https://github.com/luh2/DetectDynamicJS/blob/master/screenshots/generic.png)
1720
![Marked Difference in compared JS](https://github.com/luh2/DetectDynamicJS/blob/master/screenshots/secret.png)
1821

22+
## Contributions
23+
If you want to improve the extension, please send me a pull request or open an issue. To ease accepting pull requests, if you send a pull request, please make sure it addresses only one change and not multiple ones at the same time.
24+
1925
## Various
2026
The extension has been tested with Kali Linux, Burp version 1.6.32 and newer, Jython installation (not stand-alone) 2.7rc1.
2127

2228
If you test under Windows or use a different Burp version, please share if you experience problems.
2329

24-
If you want to improve the extension, please send me a pull request or leave a comment.
25-
2630
If you identify XSSI because of this extension, feel free to share!
2731

2832
## License

0 commit comments

Comments
 (0)