forked from modzero/mod0BurpUploadScanner
-
Notifications
You must be signed in to change notification settings - Fork 62
/
UploadScanner.py
executable file
·9615 lines (8663 loc) · 699 KB
/
UploadScanner.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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
Upload Scanner extension for the Burp Suite Proxy
Adds various security checks that can be used for
web applications that allow file upload
Copyright (C) 2017 floyd
Created on Feb 24, 2017
@author: floyd, http://floyd.ch, @floyd_ch, modzero AG, https://www.modzero.ch, @mod0
"""
# Developed when using Firefox, but short tests showed it works fine with IE, Chrome and Edge
# Tested on OSX primarily, but worked fine on Windows (including tests with exiftool.exe)
# Rules for unicode support in this extension: when Java APIs are used, everything is converted straight away to str
# with FloydsHelpers.u2s, str works best for me as "bytes" in python2. If we get byte[] from Java, we use the
# FloydsHelpers.jb2ps helper. Take care when we get back more complex objects from Java, make sure attributes of
# those objects are encoded with these two methods before usage.
# Burp imports
from burp import IBurpExtender
from burp import IScannerInsertionPoint
from burp import IScannerCheck
from burp import IScanIssue
from burp import IHttpRequestResponse
from burp import IHttpListener
from burp import ITab
from burp import IMessageEditorController
from burp import IScannerInsertionPointProvider
from burp import IHttpService
from burp import IContextMenuFactory
from burp import IExtensionStateListener
# Java stdlib imports
from java.util import ArrayList
from javax.swing import JLabel
from javax.swing import JScrollPane
from javax.swing import JButton
from javax.swing import JSplitPane
from javax.swing import JTextField
from javax.swing import JTabbedPane
from javax.swing import JTable
from javax.swing import JPanel
from javax.swing import JTextPane
from javax.swing import JFileChooser
from javax.swing import JCheckBox
from javax.swing import JOptionPane
from javax.swing import JMenuItem
from javax.swing import AbstractAction
from javax.swing import BorderFactory
from javax.swing import SwingConstants
from javax.swing.table import AbstractTableModel
from javax.swing.event import DocumentListener
from java.awt import Font
from java.awt import Color
from java.awt import Insets
from java.awt import GridBagLayout
from java.awt import GridBagConstraints
from java.awt import Image
from java.awt import Desktop
from java.awt import Dimension
from java.awt import RenderingHints
from java.awt.event import ActionListener
from java.awt.image import BufferedImage
from java.io import ByteArrayOutputStream
from java.io import ByteArrayInputStream
from javax.imageio import ImageIO
from java.net import URI
from java.net import URL
from java.nio.file import Files
from java.lang import Thread
from java.lang import IllegalStateException
from java.lang import System
# python stdlib imports
from io import BytesIO # to mimic file IO but do it in-memory
import tempfile # to make temporary files for exiftool to process
import subprocess # to call exiftool
import re # to check if exiftool name only consist of alphanum.- and to detect passwd files in downloads
import random # to chose randomly
import string # ascii letters to chose random file name from
import urllib # URL encode etc.
import time # detect timeouts and sleep for Threads
import os # local paths parsing etc.
import stat # To make exiftool executable executable
import copy # copying str/lists if a duplicate is necessary
import struct # Little/Big endian attack strings
import imghdr # Detecting mime types
import mimetypes # Detecting mime types
import cgi # for HTML escaping
import urlparse # urlparser for custom HTTP services
import zipfile # to create evil zip files in memory
import sys # to show detailed exception traces
import traceback # to show detailed exception traces
import textwrap # to wrap request texts after a certain amount of chars
import binascii # for the fingerping module
import zlib # for the fingerping module
import itertools # for the fingerping module
import threading # to make stuff thread safe
import pickle # persisting object serialization between extension reloads
import ast # to parse ${PYTHONSTR:'abc\ndef'} into a python str
from jarray import array # to go from python list to Java array
# Developer debug mode
global DEBUG_MODE
DEBUG_MODE = False
if DEBUG_MODE:
# Hint: Module "gc" garbage collector is not fully implemented in Jython as it uses the Java garbage collector
# see https://answers.launchpad.net/sikuli/+question/160893
import profile # For profiling to fix performance problems
import pdb # For debugging
# Use this to do debugging on command line:
# if DEBUG_MODE:
# pdb.set_trace()
# Glossary to read this code
# brr: abbrevation for BaseRequestRespnse, it's of class IHttpRequestResponse
# urr: abbrevation UploadRequestsResponses, see class UploadRequestsResponses,
# has three members of type IHttpRequestResponse (upload, preflight, redownload)
# *_types: specifies filename prefix, suffic (extension) and content_type for a test
# these are cut down in the function get_types, eg. when we detect that the content
# type is not sent at all in the request
class BurpExtender(IBurpExtender, IScannerCheck,
AbstractTableModel, ITab, IScannerInsertionPointProvider,
IHttpListener, IContextMenuFactory, IExtensionStateListener):
# Internal constants/read-only:
DOWNLOAD_ME = "Dwld"
MARKER_URL_CONTENT = "A_FILENAME_PLACEHOLDER_FOR_THE_DESCRIPTION_NeVeR_OcCuRs_iN_ReAl_WoRlD_DaTa"
MARKER_ORIG_EXT = 'ORIG_EXT'
MARKER_COLLAB_URL = "http://example.org/"
MARKER_CACHE_DEFEAT_URL = "https://example.org/cachedefeat/"
NEWLINE = "\r\n"
REGEX_PASSWD = re.compile("[^:]{3,20}:[^:]{1,100}:\d{0,20}:\d{0,20}:[^:]{0,100}:[^:]{0,100}:[^:]*$")
# TODO: If we just add \\ the extension uploads *a lot more* files... worth doing?
PROTOCOLS_HTTP = (
# 'ftp://',
# 'smtp://',
# 'mailto://',
# The following is \\ for Windows servers...
# '\\\\',
'http://',
'https://',
)
MAX_SERIALIZED_DOWNLOAD_MATCHERS = 500
MAX_RESPONSE_SIZE = 300000 # 300kb
# ReDownloader constants/read-only:
REDL_URL_BAD_HEADERS = ("content-length:", "accept:", "content-type:", "referer:")
REDL_FILENAME_MARKER = "${FILENAME}"
PYTHON_STR_MARKER_START = "${PYTHONSTR:"
PYTHON_STR_MARKER_END = "}"
# Implement IBurpExtender
def registerExtenderCallbacks(self, callbacks):
print "Extension loaded"
self._callbacks = callbacks
self._helpers = callbacks.getHelpers()
if DEBUG_MODE:
sys.stdout = callbacks.getStdout()
sys.stderr = callbacks.getStderr()
callbacks.setExtensionName("Upload Scanner")
# A lock to make things thread safe that access extension level globals
# Attention: use wisely! On MacOS it seems to be fine that a thread has the lock
# and acquires it again, that's fine. However, on Windows acquiring the same lock
# in the same thread twice will result in a thread lock and everything will halt!
self.globals_write_lock = threading.Lock()
# only set here at the beginning once, then constant
self.FILE_START = ''.join(random.sample(string.ascii_letters, 4))
# Internal vars/read-write:
self._log = ArrayList()
# The functions of DownloadMatcherCollection are thread safe
self.dl_matchers = DownloadMatcherCollection(self._helpers)
# TODO Burp API limitation: IBurpCollaboratorClientContext persistence
# Find out if CollaboratorMonitorThread is already running.
# Although this works and we can find our not-killed Thread, it will not have the
# functions of CollaboratorMonitorThread, so for example the "add" function
# isn't there anymore.
# for thread in Thread.getAllStackTraces().keySet():
# print thread.getName()
# if thread.name == CollaboratorMonitorThread.NAME:
# print "Found running CollaboratorMonitorThread, reusing"
# self.collab_monitor_thread = thread
# self.collab_monitor_thread.resume(self)
# break
# else:
# # No break occured on the for loop
# # Create a new thread
# print "No CollaboratorMonitorThread found, starting a new one"
# self.collab_monitor_thread = CollaboratorMonitorThread(self)
# self.collab_monitor_thread.start()
self.collab_monitor_thread = CollaboratorMonitorThread(self)
self.collab_monitor_thread.start()
self._warned_flexiinjector = False
self._no_of_errors = 0
self._ui_tab_index = 1
self._option_panels = {}
# Internal vars fuzzer (read only)
self.KNOWN_FUZZ_STRINGS = [
"A" * 256,
"A" * 1024,
"A" * 4096,
"A" * 20000,
"A" * 65535,
"%x" * 256,
"%n" * 256,
"%s" * 256,
"%s%n%x%d" * 256,
"%s" * 256,
"%.1024d",
"%.2048d",
"%.4096d",
"%.8200d",
"%99999999999s",
"%99999999999d",
"%99999999999x",
"%99999999999n",
"%99999999999s" * 200,
"%99999999999d" * 200,
"%99999999999x" * 200,
"%99999999999n" * 200,
"%08x" * 100,
"%%20s" * 200,
"%%20x" * 200,
"%%20n" * 200,
"%%20d" * 200,
"%#0123456x%08x%x%s%p%n%d%o%u%c%h%l%q%j%z%Z%t%i%e%g%f%a%C%S%08x%%#0123456x%%x%%s%%p%%n%%d%%o%%u%%c%%h%%l%%q%%j%%z%%Z%%t%%i%%e%%g%%f%%a%%C%%S%%08x",
"'",
"\\",
"<",
"+",
"%",
"$",
"`"
]
# End internal vars
# The "*_types" variables define which prefix, file extension
# and mime type is sent for the tests:
# prefix, file extension, mime type
# empty prefix = don't use prefix in front of filename
# empty file extension = don't use/cut the filename's file extension
# file extension == self._magick_original_extension, don't change whatever was there
# empty mime type = use default mime type found in the original base request
# The different extensions can vary in several ways:
# - the original extension the file had that was uploaded in the base request, self._marker_orig_ext, eg. .png
# - the payload extension, for example if we upload php code it would be .php
# - the real file extension, for example .gif if we produced a gif file that has php code in the comment
# TODO feature: Go through all TYPES and decide if .ORIG%00.EVIL makes sense as well as .EVIL%00.ORIG
# TODO feature: Additionally: maybe randomize casing, eg. .PdF?
# TODO feature: Reasoning about what _TYPES we should use. Make a big table that show what combinations we
# can send and which checks on the server side could be present. For each combination, note if the upload
# would succeed. Then rate the server side checks for likelihood to be implemented on a server (biased). In
# a next step, take real world samples and check manually to confirm rough likelihood... There are so many
# factors:
# CT whitelist (often in place)
# EXT whitelist (often in place but surprisingly often not as well...)
# CONTENT whitelist (eg. is it a PNG?)
# CONTENT transformation (convert PNG to PNG with software X)
# Checks CT matches EXT -> I get the impression this is rarely done
# Checks CT matches CONTENT -> I get the impression this is rarely done
# Checks EXT matches CONTENT
# etc.
# The following var is a special case when we detect that the request doesn't include
# the filename or content-type (e.g. Vimeo image avatar upload), so we don't do 30
# identical requests with the exact same content. See the get_types function.
self.NO_TYPES = {'', '', ''}
# ImageTragick types
self.IM_SVG_TYPES = {
# ('', '', ''),
('', BurpExtender.MARKER_ORIG_EXT, ''),
('', '', 'image/png'),
('', '.svg', 'image/svg+xml'),
# ('', '.svg', 'text/xml'),
('', '.png', 'image/png'),
# ('', '.jpeg', 'image/jpeg')
}
# Interesting fact: image/jpeg is not the only jpeg mime type sent by browsers::
# image/pjpeg
# image/x-citrix-pjpeg
# And also:
# image/x-citrix-gif
self.IM_MVG_TYPES = {
# ('', '', ''),
('', BurpExtender.MARKER_ORIG_EXT, ''),
('', '', 'image/png'),
('', '.mvg', ''),
('', '.mvg', 'image/svg+xml'),
('', '.png', 'image/png'),
# ('', '.jpeg', 'image/jpeg'),
('mvg:', '.mvg', ''),
# ('mvg:', '.mvg', 'image/svg+xml'),
}
# Xbm black/white pictures
self.XBM_TYPES = {
# ('', '', ''),
('', BurpExtender.MARKER_ORIG_EXT, ''),
('', '.xbm', ''),
('', '.xbm', 'image/x-xbm'),
('', '.xbm', 'image/png'),
('xbm:', BurpExtender.MARKER_ORIG_EXT, ''),
}
# Ghostscript types
self.GS_TYPES = {
('', BurpExtender.MARKER_ORIG_EXT, ''),
('', '.gs', ''),
('', '.eps', ''),
('', BurpExtender.MARKER_ORIG_EXT, 'text/plain'),
('', '.jpeg', 'image/jpeg'),
('', '.png', 'image/png'),
}
# LibAvFormat types
self.AV_TYPES = {
# ('', '', ''),
('', BurpExtender.MARKER_ORIG_EXT, ''),
('', BurpExtender.MARKER_ORIG_EXT, 'audio/mpegurl'),
('', BurpExtender.MARKER_ORIG_EXT, 'video/x-msvideo'),
# ('', '.m3u8', 'application/vnd.apple.mpegurl'),
('', '.m3u8', 'application/mpegurl'),
# ('', '.m3u8', 'application/x-mpegurl'),
('', '.m3u8', 'audio/mpegurl'),
# ('', '.m3u8', 'audio/x-mpegurl'),
('', '.avi', 'video/x-msvideo'),
('', '.avi', ''),
}
self.EICAR_TYPES = {
# ('', '', ''),
('', BurpExtender.MARKER_ORIG_EXT, ''),
('', '.exe', ''),
('', '.exe', 'application/x-msdownload'),
# ('', '.exe', 'application/octet-stream'),
# ('', '.exe', 'application/exe'),
# ('', '.exe', 'application/x-exe'),
# ('', '.exe', 'application/dos-exe'),
# ('', '.exe', 'application/msdos-windows'),
# ('', '.exe', 'application/x-msdos-program'),
('', BurpExtender.MARKER_ORIG_EXT, ''),
('', BurpExtender.MARKER_ORIG_EXT, 'application/x-msdownload'),
# ('', self._magick_original_extension, 'application/octet-stream'),
# ('', self._magick_original_extension, 'application/exe'),
# ('', self._magick_original_extension, 'application/x-exe'),
# ('', self._magick_original_extension, 'application/dos-exe'),
# ('', self._magick_original_extension, 'application/msdos-windows'),
# ('', self._magick_original_extension, 'application/x-msdos-program'),
}
self.PL_TYPES = {
#('', BurpExtender.MARKER_ORIG_EXT, ''),
('', BurpExtender.MARKER_ORIG_EXT, 'text/x-perl-script'),
('', '.pl', ''),
('', '.pl', 'text/x-perl-script'),
('', '.cgi', ''),
#('', '.cgi', 'text/x-perl-script'),
}
self.PY_TYPES = {
#('', BurpExtender.MARKER_ORIG_EXT, ''),
('', BurpExtender.MARKER_ORIG_EXT, 'text/x-python-script'),
('', '.py', ''),
('', '.py', 'text/x-python-script'),
('', '.cgi', '')
}
self.RB_TYPES = {
#('', BurpExtender.MARKER_ORIG_EXT, ''),
('', BurpExtender.MARKER_ORIG_EXT, 'text/x-ruby-script'),
('', '.rb', ''),
('', '.rb', 'text/x-ruby-script'),
}
# .htaccess types
self.HTACCESS_TYPES = {
('', '', ''),
('', '%00' + BurpExtender.MARKER_ORIG_EXT, ''),
('', '\x00' + BurpExtender.MARKER_ORIG_EXT, ''),
('', '', 'text/plain'),
('', '%00' + BurpExtender.MARKER_ORIG_EXT, 'text/plain'),
('', '\x00' + BurpExtender.MARKER_ORIG_EXT, 'text/plain'),
}
self.PDF_TYPES = {
('', BurpExtender.MARKER_ORIG_EXT, ''),
('', BurpExtender.MARKER_ORIG_EXT, 'application/pdf'),
('', '.pdf', ''),
('', '.pdf', 'application/pdf'),
}
self.URL_TYPES = {
#('', BurpExtender.MARKER_ORIG_EXT, ''),
#('', BurpExtender.MARKER_ORIG_EXT, 'application/octet-stream'),
('', '.URL', ''),
#('', '.URL', 'application/octet-stream'),
}
self.INI_TYPES = {
#('', BurpExtender.MARKER_ORIG_EXT, ''),
#('', BurpExtender.MARKER_ORIG_EXT, 'application/octet-stream'),
('', '.ini', ''),
#('', '.URL', 'application/octet-stream'),
}
self.ZIP_TYPES = {
('', BurpExtender.MARKER_ORIG_EXT, ''),
('', BurpExtender.MARKER_ORIG_EXT, 'application/zip'),
('', '.zip', ''),
('', '.zip', 'application/zip'),
}
self.CSV_TYPES = {
# ('', '', ''),
('', BurpExtender.MARKER_ORIG_EXT, ''),
('', '.csv', ''),
('', '.csv', 'text/csv'),
# ('', self._marker_orig_ext, ''),
# ('', self._marker_orig_ext, 'text/csv'),
}
self.EXCEL_TYPES = {
# ('', '', ''),
('', BurpExtender.MARKER_ORIG_EXT, ''),
('', '.xls', ''),
('', '.xls', 'application/vnd.ms-excel'),
# ('', BurpExtender.MARKER_ORIG_EXT, ''),
# ('', BurpExtender.MARKER_ORIG_EXT, 'text/application/vnd.ms-excel'),
}
self.IQY_TYPES = {
('', BurpExtender.MARKER_ORIG_EXT, ''),
('', '.iqy', ''),
('', '.iqy', 'application/vnd.ms-excel'),
}
# Server Side Include types
# See also what file extensions the .htaccess module would enable!
# It is unlikely that a server accepts content type text/html...
self.SSI_TYPES = {
#('', '.shtml', 'text/plain'),
('', '.shtml', 'text/html'),
#('', '.stm', 'text/html'),
#('', '.shtm', 'text/html'),
#('', '.html', 'text/html'),
#('', BurpExtender.MARKER_ORIG_EXT, 'text/html'),
('', '.shtml', ''),
('', '.stm', ''),
('', '.shtm', ''),
('', '.html', ''),
('', BurpExtender.MARKER_ORIG_EXT, ''),
}
self.ESI_TYPES = {
('', '.txt', 'text/plain'),
#('', '.txt', ''),
('', BurpExtender.MARKER_ORIG_EXT, ''),
}
self.SVG_TYPES = {
('', BurpExtender.MARKER_ORIG_EXT, ''), # Server doesn't check file contents
('', '.svg', 'image/svg+xml'), # Server enforces matching of file ext and content type
('', '.svg', ''), # Server doesn't check file ext
('', BurpExtender.MARKER_ORIG_EXT, 'image/svg+xml'), # Server doesn't check content-type
}
self.XML_TYPES = {
('', BurpExtender.MARKER_ORIG_EXT, ''),
('', '.xml', 'application/xml'),
('', '.xml', 'text/xml'),
#('', '.xml', 'text/plain'),
('', '.xml', ''),
('', BurpExtender.MARKER_ORIG_EXT, 'text/xml'),
}
self.SWF_TYPES = {
('', BurpExtender.MARKER_ORIG_EXT, ''),
('', '.swf', 'application/x-shockwave-flash'),
('', '.swf', ''),
('', BurpExtender.MARKER_ORIG_EXT, 'application/x-shockwave-flash'),
}
self.HTML_TYPES = {
('', BurpExtender.MARKER_ORIG_EXT, ''),
('', '.htm', ''),
('', '.html', ''),
('', '.htm', 'text/html'),
#('', '.html', 'text/html'),
('', '.html', 'text/plain'),
('', '.xhtml', ''),
#('', BurpExtender.MARKER_ORIG_EXT, 'text/html'),
}
print "Creating UI..."
self._create_ui()
with self.globals_write_lock:
print "Deserializing settings..."
self.deserialize_settings()
# It is important these registrations are done at the end, so the global_lock is freed.
# Otherwise when still deserializing and using the context menu at the same time there
# has been a global Burp thread-lock where I had to force quit Burp :(
# Automatic active scanning of multipart forms
callbacks.registerScannerCheck(self)
# Automatic active scanning of non-multipart messages (method called once per actively scanned request)
callbacks.registerScannerInsertionPointProvider(self)
# Automatic issue detection when file is downloaded
callbacks.registerHttpListener(self)
# Context menu to send requests to this extension
callbacks.registerContextMenuFactory(self)
# Get notified when extension is unloaded
callbacks.registerExtensionStateListener(self)
print "Extension fully registered and ready"
def _create_ui(self):
self._main_jtabedpane = JTabbedPane()
# The split pane with the log and request/response details
self._splitpane = JSplitPane(JSplitPane.VERTICAL_SPLIT)
# table of log entries
logTable = Table(self)
scrollPane = JScrollPane(logTable)
self._splitpane.setLeftComponent(scrollPane)
# tabs with request/response viewers
tabs = JTabbedPane()
self._requestViewer = self._callbacks.createMessageEditor(logTable, False)
self._responseViewer = self._callbacks.createMessageEditor(logTable, False)
tabs.addTab("Request", self._requestViewer.getComponent())
tabs.addTab("Response", self._responseViewer.getComponent())
self._splitpane.setRightComponent(tabs)
# OPTIONS
self._global_opts = OptionsPanel(self, self._callbacks, self._helpers, global_options=True)
# README
self._aboutJLabel = JLabel(Readme.get_readme(), SwingConstants.CENTER)
self._aboutJLabel.putClientProperty("html.disable", None)
self._callbacks.customizeUiComponent(self._main_jtabedpane)
self._callbacks.customizeUiComponent(self._splitpane)
self._callbacks.customizeUiComponent(self._global_opts)
self._callbacks.customizeUiComponent(self._aboutJLabel)
self._main_jtabedpane.addTab("Global & Active Scanning configuration", None, JScrollPane(self._global_opts), None)
self._main_jtabedpane.addTab("Done uploads", None, self._splitpane, None)
self._main_jtabedpane.addTab("About", None, JScrollPane(self._aboutJLabel), None)
self._callbacks.addSuiteTab(self)
# UI END
# Implement IExtensionStateListener
def extensionUnloaded(self):
self.collab_monitor_thread.extensionUnloaded()
for index in self._option_panels:
self._option_panels[index].stop_scan(None)
self.serialize_settings()
print "Extension unloaded"
def serialize_settings(self):
self.save_project_setting("UploadScanner_dl_matchers", "")
# TODO Burp API limitation: IBurpCollaboratorClientContext persistence
#self.save_project_setting("UploadScanner_collab_monitor", None)
self.save_project_setting("UploadScanner_tabs", "")
self._callbacks.saveExtensionSetting('UploadScanner_global_opts', "")
if not self._global_opts.cb_delete_settings.isSelected():
self._callbacks.saveExtensionSetting('UploadScanner_global_opts', pickle.dumps(self._global_opts.serialize()).encode("base64"))
self.save_project_setting('UploadScanner_dl_matchers',
pickle.dumps(self.dl_matchers.serialize()).encode("base64"))
# TODO Burp API limitation: IBurpCollaboratorClientContext persistence
# what a pity, IBurpCollaboratorClientContext objects can also not be serialized... :(
#self.save_project_setting('UploadScanner_collab_monitor',
# pickle.dumps(self.collab_monitor.serialize()).encode("base64"))
self.save_project_setting('UploadScanner_tabs',
pickle.dumps([self._option_panels[x].serialize() for x in self._option_panels]).encode("base64"))
print "Saved settings..."
else:
print "Deleted all settings..."
def deserialize_settings(self):
try:
k = self.load_project_setting("UploadScanner_dl_matchers")
if k:
dm = pickle.loads(k.decode("base64"))
if dm:
self.dl_matchers.deserialize(dm)
# TODO Burp API limitation: IBurpCollaboratorClientContext persistence
#k = self.load_project_setting("UploadScanner_collab_monitor")
#if k:
# cm = pickle.loads(k.decode("base64"))
# self.collab_monitor.deserialize(cm)
k = self.load_project_setting("UploadScanner_tabs")
if k:
tabs = pickle.loads(k.decode("base64"))
if tabs:
for option_panel in tabs:
# right part, create with dummy request first first
sc = ScanController(CustomRequestResponse('', '', CustomHttpService('https://example.org'), '', ''), self._callbacks)
# left part, options
# add a reference to the ScanController to the options
options = OptionsPanel(self, self._callbacks, self._helpers, scan_controler=sc)
# Take all settings from the serialized object (also recursively changes ScanController)
options.deserialize(option_panel)
self.create_tab(options, sc)
k = self._callbacks.loadExtensionSetting("UploadScanner_global_opts")
if k:
cm = pickle.loads(k.decode("base64"))
if cm:
self._global_opts.deserialize(cm)
print "Restored settings..."
except:
e = traceback.format_exc()
print "An error occured when deserializing settings. We just ignore the serialized data therefore."
print e
try:
self.save_project_setting("UploadScanner_dl_matchers", "")
# TODO Burp API limitation: IBurpCollaboratorClientContext persistence
#self.save_project_setting("UploadScanner_collab_monitor", None)
self.save_project_setting("UploadScanner_tabs", "")
except:
e = traceback.format_exc()
print "An error occured when storing empty serialize data We just ignore it for now."
print e
def save_project_setting(self, name, value):
request = """GET /"""+name+""" HTTP/1.0
# You can ignore this item in the site map. It was created by the UploadScanner extension.
# The reason is that the Burp API is missing a certain functionality to save settings.
# TODO Burp API limitation: This is a hackish way to be able to store project-scope settings
# We don't want to restore requests/responses of tabs in a totally different Burp project
# However, unfortunately there is no saveExtensionProjectSetting in the Burp API :(
# So we have to abuse the addToSiteMap API to store project-specific things
# Even when using this hack we currently cannot persist Collaborator interaction checks
# (IBurpCollaboratorClientContext is not serializable and Threads loose their Python class
# functionality when unloaded) due to Burp API limitations.
"""
response = None
if value:
response = "HTTP/1.1 200 OK\r\n" + value
rr = CustomRequestResponse(name, '', CustomHttpService('http://uploadscannerextension.local/'), request, response)
self._callbacks.addToSiteMap(rr)
def load_project_setting(self, name):
rrs = self._callbacks.getSiteMap('http://uploadscannerextension.local/'+name)
if rrs:
rr = rrs[0]
if rr.getResponse():
return "\r\n".join(FloydsHelpers.jb2ps(rr.getResponse()).split("\r\n")[1:])
else:
return None
else:
return None
# Implement IContextMenuFactory
def createMenuItems(self, invocation): #IContextMenuInvocation
action = MenuItemAction(invocation, self)
menu_item = JMenuItem(action)
menu_item.setText("Send to Upload Scanner")
return [menu_item, ]
# interaction from context menu
def new_request_response(self, invocation):
brr = invocation.getSelectedMessages()[0]
# We can only work with requests that also have a response:
if not brr.getRequest() or not brr.getResponse():
print "Tried to send a request where no response came back via context menu to the UploadScanner. Ignoring."
else:
with self.globals_write_lock:
# right part
sc = ScanController(brr, self._callbacks)
# left part, options
# add a reference to the ScanController to the options
options = OptionsPanel(self, self._callbacks, self._helpers, scan_controler=sc)
# Take all settings from global options:
options.deserialize(self._global_opts.serialize(), global_to_tab=True)
self.create_tab(options, sc)
def create_tab(self, options, sc):
# main split view
splitpane = JSplitPane(JSplitPane.HORIZONTAL_SPLIT)
splitpane.setLeftComponent(JScrollPane(options))
splitpane.setRightComponent(JScrollPane(sc))
# The CloseableTab will add itself to its parent
CloseableTab(str(self._ui_tab_index), self._main_jtabedpane, splitpane,
self._callbacks.customizeUiComponent, self.tab_closed, self._ui_tab_index)
self._option_panels[self._ui_tab_index] = options
self._ui_tab_index += 1
def tab_closed(self, index):
# What happens when a tab closes? Things we could take care of:
# DownloadMatchers created in this tab (remove them?) -> No, we want to persist them
# ColabMonitor urls added (remove them?) -> No, keep them
# For now we just don't do anything, meaning memory consumption never decreases...
# The only thing we do is remove them from _option_panels so they don't show again when
# serialized and deserialized, however, their DownloadMatchers will survive
should_close = False
if self._option_panels[index].scan_controler.scan_running:
should_close = self.show_tab_close_popup()
else:
should_close = True
if should_close:
with self.globals_write_lock:
print "Closing tab", index
del self._option_panels[index]
return should_close
# Implement ITab
def getTabCaption(self):
return "Upload Scanner"
def getUiComponent(self):
return self._main_jtabedpane
def show_error_popup(self, error_details, location, brr):
if "OutOfMemoryError: java.lang.OutOfMemoryError" in error_details:
full_msg = "Your Burp ran out of memory (RAM). This is a fatal issue and was detected by the UploadScanner " \
"extension. As there is no way to recover from this, UploadScanner is now going to unload " \
"itself, hopefully freeing up some memory for you. Please restart Burp with more memory " \
"allocated as described under '9. Burp runs out of memory.' on " \
"https://support.portswigger.net/customer/portal/articles/1965913-troubleshooting . Basically " \
"you have to start Burp with a larger -Xmx argument. Other strategies might be starting a new " \
"Burp project, loading less extensions or processing less requests in general. Press 'OK' to " \
"unload the UploadScanner extension."
response = JOptshow_error_popuionPane.showConfirmDialog(self._global_opts, full_msg, "Out of memory",
JOptionPane.OK_CANCEL_OPTION)
if response == JOptionPane.OK_OPTION:
self._callbacks.unloadExtension()
return
try:
f = file("BappManifest.bmf", "rb").readlines()
for line in f:
if line.startswith("ScreenVersion: "):
error_details += "\n" + line.replace("ScreenVersion", "Upload Scanner Version")
break
error_details += "\nExtension code location: " + location
except:
print "Could not find plugin version..."
try:
error_details += "\nJython version: " + sys.version
error_details += "\nJava version: " + System.getProperty("java.version")
except:
print "Could not find Jython/Java version..."
try:
error_details += "\nBurp version: " + " ".join([x for x in self._callbacks.getBurpVersion()])
error_details += "\nCommand line arguments: " + " ".join([x for x in self._callbacks.getCommandLineArguments()])
error_details += "\nWas loaded from BApp: " + str(self._callbacks.isExtensionBapp())
except:
print "Could not find Burp details..."
self._no_of_errors += 1
if self._no_of_errors < 2:
full_msg = 'The Burp extension "Upload Scanner" just crashed. The details of the issue are at the bottom. \n' \
'Please let the maintainer of the extension know. No automatic reporting is present, but if you could \n' \
'report the issue on github http://github.com/floyd-fuh/burp-UploadScanner \n' \
'or send an Email to burpplugins' + 'QGZsb3lkLmNo'.decode("base64") + ' this would \n' \
'be appreciated. The details of the error below can also be found in the "Extender" tab.\n' \
'Do you want to open a github issue with the details below now? \n' \
'Details: \n{}\n'.format(FloydsHelpers.u2s(error_details))
response = JOptionPane.showConfirmDialog(self._global_opts, full_msg, full_msg,
JOptionPane.YES_NO_OPTION)
if response == JOptionPane.YES_OPTION:
# Ask if it would also be OK to send the request
request_msg = "Is it OK to send along the following request? If you click 'No' this request will not \n" \
"be sent, but please consider submitting an anonymized/redacted version of the request \n" \
"along with the bug report, as otherwise a root cause analysis is likely not possible. \n" \
"You can also find this request in the Extender tab in the UploadScanner Output tab. \n\n"
request_content = textwrap.fill(repr(FloydsHelpers.jb2ps(brr.getRequest())), 100)
print request_content
if len(request_content) > 1000:
request_content = request_content[:1000] + "..."
request_msg += request_content
response = JOptionPane.showConfirmDialog(self._global_opts, request_msg, request_msg,
JOptionPane.YES_NO_OPTION)
if response == JOptionPane.YES_OPTION:
error_details += "\nRequest: " + request_content
else:
error_details += "\nRequest: None"
if Desktop.isDesktopSupported():
desktop = Desktop.getDesktop()
if desktop.isSupported(Desktop.Action.BROWSE):
github = "https://github.com/modzero/mod0BurpUploadScanner/issues/new?title=Bug" \
"&body=" + urllib.quote("```\n" + error_details + "\n```")
desktop.browse(URI(github))
#if desktop.isSupported(Desktop.Action.MAIL):
# mailto = "mailto:burpplugins" + 'QGZsb3lkLmNo'.decode("base64") + "?subject=UploadScanner%20bug"
# mailto += "&body=" + urllib.quote(error_details)
# desktop.mail(URI(mailto))
def show_tab_close_popup(self):
full_msg = 'Scan still running. Burp Collaborator interactions might get lost. Are you sure you want to close the tab? \n'
response = JOptionPane.showConfirmDialog(self._global_opts, full_msg, full_msg, JOptionPane.YES_NO_OPTION)
if response == JOptionPane.YES_OPTION:
return True
else:
return False
# Implement AbstractTableModel
def getRowCount(self):
try:
return self._log.size()
except:
return 0
def getColumnCount(self):
return 2
def getColumnName(self, columnIndex):
if columnIndex == 0:
return "Status"
if columnIndex == 1:
return "URL"
return ""
def getValueAt(self, rowIndex, columnIndex):
logEntry = self._log.get(rowIndex)
if columnIndex == 0:
return logEntry._status
if columnIndex == 1:
return logEntry._url
return ""
# Helper function to easily add an entry to the log:
def add_log_entry(self, rr):
with self.globals_write_lock:
row = self._log.size()
status = self._helpers.analyzeResponse(rr.getResponse()).getStatusCode()
self._log.add(LogEntry(status, self._callbacks.saveBuffersToTempFiles(rr),
self._helpers.analyzeRequest(rr).getUrl()))
self.fireTableRowsInserted(row, row)
# Implement IHttpListener
def processHttpMessage(self, _, messageIsRequest, base_request_response):
try:
# This can get computationally expensive if there are a lot of files that were uploaded...
# ... make sure we only scan responses and if we have any matcher rules
if not messageIsRequest:
resp = base_request_response.getResponse()
if not resp:
print "processHttpMessage called with BaseRequestResponse with no response. Ignoring."
return
if len(resp) >= BurpExtender.MAX_RESPONSE_SIZE:
# Don't look at responses longer than MAX_RESPONSE_SIZE
return
req = base_request_response.getRequest()
if not req:
print "processHttpMessage called with BaseRequestResponse with no request. Ignoring."
return
iRequestInfo = self._helpers.analyzeRequest(base_request_response)
#print type(iRequestInfo.getUrl().toString()), repr(iRequestInfo.getUrl().toString())
url = iRequestInfo.getUrl()
if url:
url = FloydsHelpers.u2s(url.toString())
else:
# Indeed the url might be None... according to https://github.com/modzero/mod0BurpUploadScanner/issues/17
return
# ... do not scan things that are not "in scope" (see DownloadMatcherCollection class)
# means we only check if we uploaded stuff to that host or the user configured
# another host in the ReDownloader options that is therefore also "in scope"
matchers = self.dl_matchers.get_matchers_for_url(url)
if not matchers:
#We hit this for all not "in scope" requests
#we also hit it for URLs that can not be parsed by urlparse such as https://github.com/modzero/mod0BurpUploadScanner/issues/12
return
iResponseInfo = self._helpers.analyzeResponse(base_request_response.getResponse())
headers = [FloydsHelpers.u2s(x) for x in iResponseInfo.getHeaders()]
body = FloydsHelpers.jb2ps(base_request_response.getResponse())[iResponseInfo.getBodyOffset():]
# We do a small hack here: we iterate in reverse order (denoted with [::-1]) over the passive checks. We do that as
# the thumbnail check that is done for image metadata is a little tricky: as the thumbnail image itself
# is used as a regular test and another image file includes the contents of the thumbnail image as well, the wrong issue
# would be shown. In other words: the entire thumbnail file is included in one of the image files. When we iterate
# reversed, we hit the correct issue definition first.
for matcher in list(matchers)[::-1]:
if matcher.matches(url, headers, body):
issue_copy = matcher.issue.create_copy()
if BurpExtender.MARKER_URL_CONTENT in issue_copy.detail:
if matcher.url_content:
issue_copy.detail = issue_copy.detail.replace(BurpExtender.MARKER_URL_CONTENT,
matcher.url_content)
elif matcher.filename_content_disposition:
issue_copy.detail = issue_copy.detail.replace(BurpExtender.MARKER_URL_CONTENT,
matcher.filename_content_disposition)
elif matcher.filecontent:
issue_copy.detail = issue_copy.detail.replace(BurpExtender.MARKER_URL_CONTENT,
matcher.filecontent)
else:
issue_copy.detail = issue_copy.detail.replace(BurpExtender.MARKER_URL_CONTENT,
"UNKNOWN")
self._create_download_scan_issue(base_request_response, issue_copy)
# As the matcher was now triggered, we can remove it as it should not trigger again,
# because every attack defines its own matcher
self.dl_matchers.remove_reported(url, matcher)
# At maximum there will be 1 scan issue per message, as it is unlikely that there is more than 1
# download in a HTTP message. Therefore we can use "return" after adding a scan issue.
return
except:
# I had enough of being the exception collector of processHttpMessage and python lib quirks...
# no alerting of the user in this case anymore
#self.show_error_popup(traceback.format_exc(), "processHttpMessage", base_request_response)
raise sys.exc_info()[1], None, sys.exc_info()[2]
def _create_download_scan_issue(self, base_request_response, issue):
# For unknown reasons (probably Jython Vodoo) this doesn't work:
# issue.servicePy = base_request_response.getHttpService()
# issue.urlPy = self._helpers.analyzeRequest(base_request_response).getUrl()
# Therefore we do this:
issue.setHttpService(base_request_response.getHttpService())
issue.setUrl(self._helpers.analyzeRequest(base_request_response).getUrl())
issue.httpMessagesPy.append(base_request_response)
self._add_scan_issue(issue)
def _add_scan_issue(self, issue):
print "Reporting", issue.name
#print issue.toString()
self._callbacks.addScanIssue(issue)
# implement IScannerCheck
def doPassiveScan(self, base_request_response):
# see processHttpMessage which is a more general case of passive scan
pass
def consolidateDuplicateIssues(self, existingIssue, newIssue):
#if existingIssue.getUrl() == newIssue.getUrl() and \
# existingIssue.getIssueName() == newIssue.getIssueName():
# return -1
#else:
return 0
def doActiveScan(self, base_request_response, insertionPoint, options=None):
try:
# Also see getInsertionPoints
if insertionPoint.getInsertionPointType() == IScannerInsertionPoint.INS_PARAM_MULTIPART_ATTR:
if insertionPoint.getInsertionPointName() == "filename":
req = base_request_response.getRequest()
if not req:
print "doActiveScan called with BaseRequestResponse with no request. Ignoring."
return
print "Multipart filename found!"
if not options:
options = self._global_opts
injector = MultipartInjector(base_request_response, options, insertionPoint, self._helpers, BurpExtender.NEWLINE)
self.do_checks(injector)
else:
print "This is not a type file but something else in a multipart message:", insertionPoint.getInsertionPointName()
except:
self.show_error_popup(traceback.format_exc(), "doActiveScan", base_request_response)
if options and options.redl_enabled:
options.scan_was_stopped()
raise sys.exc_info()[1], None, sys.exc_info()[2]
# Implement IScannerInsertionPointProvider
def getInsertionPoints(self, base_request_response):
try:
# TODO Burp API limitation: Is there another way to simply say "each active scanned HTTP request once"?
# it seems not: https://support.portswigger.net/customer/en/portal/questions/16776337-confusion-on-insertionpoints-active-scan-module?new=16776337
# So we are going to abuse a functionality of Burp called IScannerInsertionPoint
# which is by coincidence always called once per request for every actively scanned item (with base_request_response)
# this is an ugly hack...
req = base_request_response.getRequest()
if not req: