This repository has been archived by the owner on Jul 20, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 13
/
SinkLogger.py
173 lines (139 loc) · 7.88 KB
/
SinkLogger.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
from burp import IBurpExtender, IProxyListener, ITab
from java.awt import FlowLayout
from java.awt.event import ItemEvent, ItemListener
from java.io import PrintWriter
from javax.swing import JCheckBox, JPanel
import re
class BurpExtender(IBurpExtender, IProxyListener, ITab):
# Implement IBurpExtender
def registerExtenderCallbacks(self, callbacks):
# Keep some useful references
self._callbacks = callbacks
self._helpers = callbacks.getHelpers()
# Set the extension name
callbacks.setExtensionName("Sink Logger")
# Obtain the output stream
self._stdout = PrintWriter(callbacks.getStdout(), True)
# Register as a Proxy listener
callbacks.registerProxyListener(self)
# Initialize and register UI
self.initUserInterface()
callbacks.addSuiteTab(self)
# Extension specific stuff
self.loggingEnabled = False
self.scriptContentTypes = ["javascript", "ecmascript", "jscript", "json"]
self.excludedContentTypes = ['text/css', "image/", "text/plain", "application/x-octet-stream"]
self.commonHijackingProtections = ["for (;;);", ")]}'", "{} &&", "while(1);" ]
# In Chrome console.warn shows an extremely useful stack trace and it's hidden by default so the output is not a mess (i.e. console.trace())
# Minified version of this script is injected into responses
"""
var QF9iYXlvdG9w = QF9iYXlvdG9w || new Proxy({}, {
set: function(target, key, value, receiver) {
if (value != undefined && value !== "") {
if ((value + "").startsWith("[object")) {
try {
var svalue = JSON.stringify(value);
} catch(error) {}
}
console.warn(`Sink log (${key}): ${svalue !== undefined ? svalue : value}`);
}
return Reflect.set(target, key, value, receiver);
}
});
"""
self.proxyInitialization = "var QF9iYXlvdG9w=QF9iYXlvdG9w||new Proxy({},{set:function set(a,b,c,d){if(c!=void 0&&\"\"!==c){if((c+\"\").startsWith(\"[object\"))try{var e=JSON.stringify(c)}catch(f){}console.warn(\"Sink log (\"+b+\"): \"+(e===void 0?c:e))}return Reflect.set(a,b,c,d)}});"
self.proxyInitializationHTML = "<script>%s</script>" % self.proxyInitialization
# pattern: replacement passed into re.sub()
self.sinkPatterns = {
r'\.innerHTML\s*=([^=])': r'.innerHTML=QF9iYXlvdG9w.innerHTML=\1',
r'eval\(([^)])': r'eval(QF9iYXlvdG9w.eval=\1',
r'document\.write\(([^)])': r'document.write(QF9iYXlvdG9w.write=\1',
r'(document|window)\.location(?:\.href)?\s*=([^=])': r'\1.location=QF9iYXlvdG9w.location=\2',
r'(window|document)\.location\.replace\(([^)])': r'\1.location.replace(QF9iYXlvdG9w.location=\2',
# Other jQuery sinks will eventually pass data to innerHTML or eval() (globalEval uses "indirect" as eval alias)
r'(\$|jQuery)\.(globalEval|parseHTML)\(([^)])': r'\1.\2(QF9iYXlvdG9w.jQuery_\2=\3'
}
# CSP / SRI
self.metaHeaderPattern = re.compile("<meta.*Content-Security-Policy.*>", flags=re.MULTILINE|re.IGNORECASE)
self.integrityAttributesPattern = re.compile("integrity(.{1,3})sha", flags=re.IGNORECASE)
self._stdout.println("GitHub: https://github.com/bayotop/sink-logger")
self._stdout.println("Contact: https://twitter.com/_bayotop")
self._stdout.println("")
self._stdout.println("Successfully initialized Sink Logger...")
# Implement IProxyListener
def processProxyMessage(self, messageIsRequest, message):
if messageIsRequest or not self.loggingEnabled:
return
try:
response = message.getMessageInfo().getResponse()
except:
return
# Process the response and inject the proxy initialization script if HTML / JS
responseHeaders = self._helpers.analyzeResponse(response).getHeaders()
responseBody = self.processResponse(response)
if not responseBody:
return
# Get rid of CSP and SRI to ensure that the injected script executes
editedResponseHeaders = [x for x in responseHeaders if not "content-security-policy" in x.lower()]
responseBody = self.integrityAttributesPattern.sub(r'integrityx\1sha', responseBody)
responseBody = self.metaHeaderPattern.sub("", responseBody)
# "Proxify" sinks in response
for key,value in self.sinkPatterns.items():
responseBody = re.sub(key, value, responseBody)
editedResponse = self._helpers.buildHttpMessage(editedResponseHeaders, responseBody)
message.getMessageInfo().setResponse(editedResponse)
# Let users review the response for modification in UI
message.setInterceptAction(message.ACTION_FOLLOW_RULES)
def processResponse(self, response):
analyzedResponse = self._helpers.analyzeResponse(response)
responseBody = response[analyzedResponse.getBodyOffset():]
if not responseBody or self.checkContentType(analyzedResponse, self.excludedContentTypes):
return False
responsePeek = responseBody[0:100].tostring().lstrip()
mimeType = analyzedResponse.getStatedMimeType().split(';')[0]
inferredMimeType = analyzedResponse.getInferredMimeType().split(';')[0]
# Inject script into HTML and JS (and exclude JSON)
if responsePeek.lower().startswith("<html") or responsePeek.lower().startswith("<!doctype html"):
return self.processHtml(responseBody.tostring())
elif (all(x not in responsePeek for x in self.commonHijackingProtections) and
(responsePeek[0] not in ["{","["]) and
("script" in mimeType or "script" in inferredMimeType or self.checkContentType(analyzedResponse, self.scriptContentTypes))):
return self.proxyInitialization + responseBody
else:
# All unrecognized responses will be "proxified"
return responseBody
def checkContentType(self, analyzedResponse, patterns):
headers = analyzedResponse.getHeaders()
contentTypeHeaders = [x for x in headers if "content-type:" in x.lower()]
for cth in contentTypeHeaders:
if any(c in cth.lower() for c in patterns):
return True
return False
def processHtml(self, response):
# Browsers do weird stuff if a script precedes the doctype / root html element
if "<head>" in response:
return response.replace("<head>", "<head>" + self.proxyInitializationHTML, 1)
if "</title>" in response:
return response.replace("</title>", "</title>" + self.proxyInitializationHTML, 1)
if "<body>" in response:
return response.replace("<body>", "<body>" + self.proxyInitializationHTML, 1)
return self.proxyInitializationHTML + response
# Implement ITab
def getTabCaption(self):
return "Sink Logger"
def getUiComponent(self):
return self.configurationPanel
def initUserInterface(self):
class TriggerLoggingListener(ItemListener):
def __init__(self, extender):
self.extender = extender
def itemStateChanged(self, e):
if e.getStateChange() == ItemEvent.SELECTED:
self.extender.loggingEnabled = True
else:
self.extender.loggingEnabled = False
loggingTrigger = JCheckBox("Enable logging (clearing browser cache might be needed to reflect the change)")
loggingTrigger.addItemListener(TriggerLoggingListener(self))
self.configurationPanel = JPanel()
self.configurationPanel.setLayout(FlowLayout(FlowLayout.LEFT))
self.configurationPanel.add(loggingTrigger)