bscan 1.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CONFIG.rdoc +131 -0
- data/README.rdoc +140 -0
- data/Rakefile +54 -0
- data/VERSION +1 -0
- data/bin/bscan +79 -0
- data/bscan.gemspec +63 -0
- data/lib/bscan.rb +324 -0
- data/lib/bscan/modules/injector.rb +142 -0
- data/lib/bscan/modules/kill_apache.rb +201 -0
- data/lib/bscan/modules/many_threads.rb +52 -0
- data/lib/bscan/modules/slowloris.rb +263 -0
- data/lib/bscan/utils/bscan_helper.rb +133 -0
- data/release_notes.txt +25 -0
- data/samples/config/big_request.txt +12 -0
- data/samples/config/conf +58 -0
- data/samples/config/injector.txt +514 -0
- data/samples/config/request.txt +12 -0
- data/samples/headless-bscan.sh +3 -0
- data/test.sh +3 -0
- data/test/bscan_test.rb +4 -0
- metadata +91 -0
data/lib/bscan.rb
ADDED
@@ -0,0 +1,324 @@
|
|
1
|
+
#!/usr/bin/env jruby
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
require 'buby'
|
5
|
+
require 'getoptlong'
|
6
|
+
require 'json'
|
7
|
+
require 'bscan/utils/bscan_helper'
|
8
|
+
require 'java'
|
9
|
+
|
10
|
+
class String
|
11
|
+
def camelize
|
12
|
+
self.split(/[^a-z0-9]/i).map{|w| w.capitalize}.join
|
13
|
+
end
|
14
|
+
def camelize!
|
15
|
+
self.replace(self.split(/[^a-z0-9]/i).map{|w| w.capitalize}.join)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module BScan
|
20
|
+
|
21
|
+
include BscanHelper
|
22
|
+
|
23
|
+
attr_accessor :activity
|
24
|
+
attr_reader :modules_only
|
25
|
+
attr_reader :bscan_config
|
26
|
+
|
27
|
+
def Log (mtype, *msgs)
|
28
|
+
pr = "Unknown:"
|
29
|
+
case mtype
|
30
|
+
when 0
|
31
|
+
pr = "ERROR:"
|
32
|
+
when 1
|
33
|
+
pr = "WARN:"
|
34
|
+
when 2
|
35
|
+
pr = "INFO:"
|
36
|
+
when 3
|
37
|
+
pr = "DEBUG:"
|
38
|
+
end
|
39
|
+
msgs.each do |msg|
|
40
|
+
if (@ll >= mtype)
|
41
|
+
dt = Time.now.strftime("%y%m%d %H%M%S")
|
42
|
+
@log.println "#{dt} #{pr} #{msg}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
@log.flush
|
46
|
+
end
|
47
|
+
|
48
|
+
def evt_commandline_args args
|
49
|
+
@cmd_params ||= JSON.parse args[0]
|
50
|
+
@ll = @cmd_params['loglevel'].to_i
|
51
|
+
|
52
|
+
lfile = @cmd_params['logfile']
|
53
|
+
if lfile != nil
|
54
|
+
begin
|
55
|
+
@log = java.io.PrintStream.new(lfile)
|
56
|
+
rescue Exception => e
|
57
|
+
$stderr.puts("BScan.evt_register_callbacks Error: can't open log file '#{lfile}', exception: #{e.message}")
|
58
|
+
$stderr.puts(e.backtrace.join("\n"))
|
59
|
+
Process.exit!(2)
|
60
|
+
end
|
61
|
+
else
|
62
|
+
@log = $stdout
|
63
|
+
end
|
64
|
+
|
65
|
+
Log 2, "BScan.evt_commandline_args CMD_PARAMS: #{@cmd_params}"
|
66
|
+
@cmd_params.each_pair do |k,v|
|
67
|
+
Log 2,"BScan.evt_commandline_args #{k}:#{v}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
|
73
|
+
def evt_http_message(tool_name, is_request, message_info)
|
74
|
+
super(tool_name, is_request, message_info)
|
75
|
+
if tool_name == 'Scanner'
|
76
|
+
if is_request
|
77
|
+
Log 2, "#"*70, "# BScan.evt_http_message REQUEST: #{message_info.url.toString}, tool: #{tool_name}", "#"*70
|
78
|
+
Log 3, "BScan.evt_http_message #{message_info.req_str}", ""
|
79
|
+
else
|
80
|
+
Log 2, "# BScan.evt_http_message RESPONSE CODE: #{message_info.statusCode}", ""
|
81
|
+
Log 3, "BScan.evt_http_message #{message_info.rsp_str}", ""
|
82
|
+
end
|
83
|
+
end
|
84
|
+
if tool_name == 'Spider'
|
85
|
+
if not is_request
|
86
|
+
@activity[0] = true
|
87
|
+
https = message_info.getProtocol() == "https" ? true : false
|
88
|
+
Log 2, "BScan.evt_http_message Passively scanning: #{message_info.url.to_string}"
|
89
|
+
do_passive_scan(message_info.getHost(), message_info.getPort(), https, message_info.getRequest(), message_info.getResponse())
|
90
|
+
if (is_in_scope(message_info.url))
|
91
|
+
Log 2, "BScan.evt_http_message Actively scanning: #{message_info.url.to_string}"
|
92
|
+
isqi = do_active_scan(message_info.getHost(), message_info.getPort(), https, message_info.getRequest(), [])
|
93
|
+
@queue.push(isqi)
|
94
|
+
run_modules message_info
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def is_module_static n,p
|
101
|
+
pref = 'bscan.' + n + '.'
|
102
|
+
pref += p + '.' if p and p.length > 0
|
103
|
+
@bscan_config[pref + 'static_request'] == 'true'
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
def run_modules msg=nil
|
108
|
+
if @modules
|
109
|
+
@modules.each do |m|
|
110
|
+
begin
|
111
|
+
mod,prop=m.split(':',2)
|
112
|
+
prop ||=''
|
113
|
+
mn = File.basename(mod,".rb")
|
114
|
+
is_static = is_module_static(mn,prop)
|
115
|
+
Log 2, "BScan.run_modules executing module #{mod}:#{prop} #{is_static}"
|
116
|
+
mn.camelize!
|
117
|
+
if (is_static && !msg) || (!is_static && msg)
|
118
|
+
eval("
|
119
|
+
# puts '=====================MODULE PATH: ' + $:.join(':')
|
120
|
+
require '#{mod}'
|
121
|
+
require 'bscan/utils/bscan_helper.rb'
|
122
|
+
|
123
|
+
class #{mn}#{prop}Class
|
124
|
+
include #{mn}
|
125
|
+
include BscanHelper
|
126
|
+
end
|
127
|
+
#{mn}#{prop}Class.new.run(self, msg, prop)
|
128
|
+
")
|
129
|
+
end
|
130
|
+
rescue Exception => e
|
131
|
+
Log 1, "BScan.run_modules Can't exceute module #{mod}, Exception: #{e.message}"
|
132
|
+
Log 1, e.backtrace.join("\n")
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def evt_scan_issue issue
|
139
|
+
super(issue)
|
140
|
+
# Buby::HttpRequestResponseHelper.implant(issue.http_messages)
|
141
|
+
write_issue_state issue
|
142
|
+
end
|
143
|
+
|
144
|
+
def sync_save_state issue
|
145
|
+
@sync_state_mutex ||= Mutex.new
|
146
|
+
@sync_state_mutex.synchronize {
|
147
|
+
save_state(@sstream) if @sstream and issue.severity =~ /(High)/
|
148
|
+
}
|
149
|
+
end
|
150
|
+
|
151
|
+
def write_issue_state issue
|
152
|
+
# Log 2,"INSPECT: #{issue.http_messages[0].methods} #{issue.http_messages[0].inspect} #{issue.http_messages[0].to_s} "
|
153
|
+
|
154
|
+
Log 2,"BScan.write_issue_state #{not @istream} #{issue.http_messages[0].methods} #{issue.http_messages[0].to_s} "
|
155
|
+
@istream or return
|
156
|
+
begin
|
157
|
+
@istream.puts '#'*70,"#{issue.issue_name} : #{issue.url}",
|
158
|
+
"Severity: #{issue.severity}(#{issue.confidence})",
|
159
|
+
"Background: #{issue.issue_background}",
|
160
|
+
"Details: #{issue.issue_detail}",
|
161
|
+
"Remediation: #{issue.remediation_background}",
|
162
|
+
"Request: #{issue.http_messages[0].req_str}",
|
163
|
+
"Response: #{issue.http_messages[0].rsp_str}"
|
164
|
+
# sync_save_state issue throws exceptions
|
165
|
+
@istream.flush
|
166
|
+
rescue Exception => e
|
167
|
+
Log 0, "BScan.write_issue_state Can't write issue #{issue.issue_name}, Exception: #{e.message}"
|
168
|
+
Log 0, e.backtrace.join("\n")
|
169
|
+
end
|
170
|
+
end
|
171
|
+
def evt_application_closing
|
172
|
+
Log 2,"BScan.evt_application_closing"
|
173
|
+
@istream.close if @istream
|
174
|
+
@log.close if @log
|
175
|
+
end
|
176
|
+
|
177
|
+
def evt_register_callbacks cb
|
178
|
+
super(cb)
|
179
|
+
begin
|
180
|
+
|
181
|
+
Log 2, "="*30, "BScan.evt_register_callbacks registring, log = #{@cmd_params['logfile']} ", "="*30
|
182
|
+
@bscan_config = @cmd_params['bscan_config']
|
183
|
+
@burp_config = @cmd_params['burp_config']
|
184
|
+
@issues = @bscan_config['bscan.issues']
|
185
|
+
@modules_only = (@bscan_config['bscan.modules_only'] and @bscan_config['bscan.modules_only'] == 'true')
|
186
|
+
|
187
|
+
Log 1, "BScan.evt_register_callbacks No issues dir provided. Issues will not be logged." if not @issues
|
188
|
+
if (@issues)
|
189
|
+
begin
|
190
|
+
dt = Time.now.strftime("%y%m%d_%H%M%S")
|
191
|
+
File.directory? @issues or %x{mkdir -p "#{@issues}"}
|
192
|
+
@sstream = "#{@issues}/session.#{dt}.zip"
|
193
|
+
@istream = File.open("#{@issues}/issues.#{dt}.txt","w")
|
194
|
+
rescue Exception => e
|
195
|
+
Log 0, "BScan.evt_register_callbacks Can't create issues or session files, Exception: #{e.message}"
|
196
|
+
Log 0, e.backtrace.join("\n")
|
197
|
+
|
198
|
+
exit_suite
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
@queue ||= []
|
203
|
+
@activity = [false]
|
204
|
+
@inactivity_to = @bscan_config['bscan.inactivity_to']
|
205
|
+
@inactivity_to ||= '30'
|
206
|
+
@inactivity_to = @inactivity_to.to_i
|
207
|
+
# Will exit if @activity = 0 and @queue is empty two times
|
208
|
+
if @monitor == nil
|
209
|
+
Log 2, "="*30, "BScan.evt_register_callbacks Starting Monitor", "="*30
|
210
|
+
@monitor = Thread.new(@queue, @activity) {|q,a|
|
211
|
+
cnt=0;
|
212
|
+
while (true)
|
213
|
+
sleep(@inactivity_to/2)
|
214
|
+
q.delete_if {|e| e.getPercentageComplete() == 100}
|
215
|
+
if q.length == 0 and not a[0]
|
216
|
+
cnt += 1
|
217
|
+
else
|
218
|
+
cnt = 0
|
219
|
+
# Log 2, "BScan.evt_register_callbacks QUEUE: #{q.length}, Activity: #{a[0]}"
|
220
|
+
end
|
221
|
+
if cnt > 1
|
222
|
+
# Log 2, "BScan.evt_register_callbacks Scanning complete"
|
223
|
+
exit_suite
|
224
|
+
break
|
225
|
+
end
|
226
|
+
a[0] = false
|
227
|
+
end
|
228
|
+
}
|
229
|
+
end
|
230
|
+
|
231
|
+
Log 2, '='*30, 'BScan.evt_register_callbacks Params', '='*30
|
232
|
+
params = save_config
|
233
|
+
params['target.scopeinclude0']='**empty**' # burp can store the previous scope somehow, thus need to clean
|
234
|
+
@burp_config.each_pair do |k,v|
|
235
|
+
params[k] = v
|
236
|
+
end
|
237
|
+
load_config params
|
238
|
+
params.each_pair {|k,v| Log 2,"#{k}:#{v}"} # if k =~ /^(scanner|spider)/}
|
239
|
+
|
240
|
+
@modules ||= @bscan_config['bscan.modules']
|
241
|
+
@modules = [@modules] if not @modules.kind_of?(Array)
|
242
|
+
|
243
|
+
mods = []
|
244
|
+
@modules.each do |m|
|
245
|
+
mods << m.split(',')
|
246
|
+
end
|
247
|
+
|
248
|
+
@modules = mods.flatten
|
249
|
+
|
250
|
+
urls = @bscan_config['bscan.url']
|
251
|
+
Log 2, "BScan.evt_register_callbacks urls: #{@modules_only} #{urls.join('|')}"
|
252
|
+
|
253
|
+
run_modules # run_modules without 'msg' will do static reqs only
|
254
|
+
return if @modules_only
|
255
|
+
|
256
|
+
if not urls
|
257
|
+
Log 0, "BScan.evt_register_callbacks No URL's provided in config. Use bscan.url param. Multiple entries are OK"
|
258
|
+
exit_suite
|
259
|
+
end
|
260
|
+
|
261
|
+
urls = [urls] if not urls.kind_of?(Array)
|
262
|
+
|
263
|
+
urls.each do |u|
|
264
|
+
Log 2, "BScan.evt_register_callbacks checking url: #{u}"
|
265
|
+
if not is_in_scope(u)
|
266
|
+
Log 2, "BScan.evt_register_callbacks including url: #{u}"
|
267
|
+
include_in_scope(u)
|
268
|
+
end
|
269
|
+
Log 2, "BScan.evt_register_callbacks Sending to spider: #{u}"
|
270
|
+
send_to_spider(u)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
rescue Exception => e
|
274
|
+
Log 0, "BScan.evt_register_callbacks Exception: #{e.message}"
|
275
|
+
Log 0, e.backtrace.join("\n")
|
276
|
+
exit_suite
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
|
281
|
+
def log ll, stream
|
282
|
+
stream.puts if true
|
283
|
+
end
|
284
|
+
|
285
|
+
def add_multi map,k,v
|
286
|
+
if (map[k])
|
287
|
+
ov = map[k];
|
288
|
+
if (ov.kind_of?(Array))
|
289
|
+
map[k] << v
|
290
|
+
else
|
291
|
+
map[k] = [ov,v]
|
292
|
+
end
|
293
|
+
else
|
294
|
+
map[k] = v
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
def read_config file
|
299
|
+
|
300
|
+
burp_config = {}
|
301
|
+
bscan_config = {}
|
302
|
+
|
303
|
+
begin
|
304
|
+
open_in_path(file).each_line do |line|
|
305
|
+
line.chomp!
|
306
|
+
line.strip!
|
307
|
+
next if (line =~ /^#/ or line.length < 1)
|
308
|
+
data = line.split(/=/)
|
309
|
+
val = nil
|
310
|
+
val = data[1..-1].join('=') if data.size > 1
|
311
|
+
if data[0] =~ /^bscan./
|
312
|
+
add_multi(bscan_config,data[0],val)
|
313
|
+
else
|
314
|
+
burp_config[data[0]] = val
|
315
|
+
end
|
316
|
+
end
|
317
|
+
rescue Exception => e
|
318
|
+
$stderr.puts("BScan.read_config Error: can't read config file '#{file}', exception: #{e.message}")
|
319
|
+
$stderr.puts(e.backtrace.join("\n"))
|
320
|
+
Process.exit(3)
|
321
|
+
end
|
322
|
+
[burp_config, bscan_config]
|
323
|
+
end
|
324
|
+
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'bscan/utils/bscan_helper.rb'
|
2
|
+
|
3
|
+
module Injector
|
4
|
+
|
5
|
+
COMMENT_START='# '
|
6
|
+
|
7
|
+
def run *args
|
8
|
+
|
9
|
+
@bscan = args[0]
|
10
|
+
@config ||= @bscan.instance_variable_get("@bscan_config")
|
11
|
+
@bscan.activity[0]=true
|
12
|
+
|
13
|
+
@prop_pref = 'bscan.injector.'
|
14
|
+
@prop_pref += args[2] + '.' if args[2] && args[2].length > 0
|
15
|
+
@mid = args[2]?"Injector.#{args[2]}.":'Injector.'
|
16
|
+
msg = args[1]
|
17
|
+
|
18
|
+
if not msg
|
19
|
+
inject_to_pattern
|
20
|
+
return
|
21
|
+
end
|
22
|
+
|
23
|
+
url = msg.url.dup.to_s
|
24
|
+
@bscan.Log 2, "#{@mid}run for #{url}"
|
25
|
+
begin
|
26
|
+
if (url =~ /([^?]+)\?(.+)/)
|
27
|
+
beg = "#{$1}?"
|
28
|
+
params = $2
|
29
|
+
@bscan.Log 2, "#{@mid}run BEG: #{beg} PARAMS: #{params} FILE: #{@config[prop('file')]}"
|
30
|
+
injs = open_in_path(@config[prop('file')])
|
31
|
+
injs.each_line do |l|
|
32
|
+
l.chomp!
|
33
|
+
next if (l =~ /^#{COMMENT_START}/ or l.length < 1)
|
34
|
+
@bscan.Log 2, "#{@mid}run injecting: #{l}"
|
35
|
+
|
36
|
+
@bscan.activity[0]=true
|
37
|
+
|
38
|
+
do_scan(msg, beg + l, l) # in parameter name
|
39
|
+
do_scan(msg, beg.chop + l, l) # in URL
|
40
|
+
|
41
|
+
pos=0
|
42
|
+
while (m=params.match(/([^&]+)=([^&]+)/,pos))
|
43
|
+
trg = beg + params[0..m.begin(2)-1] + l + params[m.end(2)..-1]
|
44
|
+
@bscan.activity[0]=true
|
45
|
+
do_scan(msg, trg, l)
|
46
|
+
pos=m.end(1)+1
|
47
|
+
end
|
48
|
+
end
|
49
|
+
injs.close
|
50
|
+
end
|
51
|
+
|
52
|
+
inject_to_body msg if @config['bscan.injector.one.inject_to_body'] == 'true'
|
53
|
+
|
54
|
+
rescue Exception => e
|
55
|
+
@bscan.Log 0, "#{@mid}run Exception: #{e.message}"
|
56
|
+
@bscan.Log 0, e.backtrace.join("\n")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def inject_to_pattern
|
61
|
+
param = @config[prop('inject_instead_of')]
|
62
|
+
a=[]
|
63
|
+
if (not param or (a=param.split(':',3)).size < 3)
|
64
|
+
@bscan.Log 0, "#{@mid}inject_to_pattern: 'inject_instead_of' parameter is not valid #{param}"
|
65
|
+
return
|
66
|
+
end
|
67
|
+
@bscan.Log 2, "#{@mid}inject_to_pattern input: #{a.join('|')}"
|
68
|
+
begin
|
69
|
+
p,f,proto = Regexp.escape(a[0]), a[1], a[2]
|
70
|
+
file = open_in_path(f)
|
71
|
+
req = file.read
|
72
|
+
req.gsub!(/\^M\n/,"\r\n")
|
73
|
+
file.close
|
74
|
+
|
75
|
+
injs = open_in_path(@config[prop('file')])
|
76
|
+
injs.each_line do |l|
|
77
|
+
l.chomp!
|
78
|
+
next if (l =~ /^#{COMMENT_START}/ or l.length < 1)
|
79
|
+
@bscan.Log 2, "#{@mid}inject_to_pattern injecting: #{l}"
|
80
|
+
|
81
|
+
pos = 0
|
82
|
+
while (m=req.match(/(#{p}).*?(#{p})/,pos))
|
83
|
+
r = (req[0..m.begin(1)-1] + l + req[m.end(2)..-1]).gsub /#{p}(.*?)#{p}/,'\1'
|
84
|
+
|
85
|
+
@bscan.Log 2, "#{@mid}inject_to_pattern new req:\n#{r}"
|
86
|
+
set_len r
|
87
|
+
@bscan.activity[0]=true
|
88
|
+
send_req r,proto,l
|
89
|
+
pos=m.end(1)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
injs.close
|
93
|
+
|
94
|
+
rescue Exception => e
|
95
|
+
@bscan.Log 0, "#{@mid}inject_to_pattern Exception: #{e.message}"
|
96
|
+
@bscan.Log 0, e.backtrace.join("\n")
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
def inject_to_body msg
|
102
|
+
scanf = false
|
103
|
+
@bscan.Log 2, "#{@mid}inject_to_body req: #{msg.req_str}"
|
104
|
+
msg.request_headers.each do |a|
|
105
|
+
@bscan.Log 2, "#{@mid}inject_to_body hdr: #{a[0]} #{a[1]}"
|
106
|
+
if a.size > 1 and a[0] =~ /content-type/i and a[1] =~ /application\/x-www-form-urlencoded/i
|
107
|
+
scanf = true
|
108
|
+
break
|
109
|
+
end
|
110
|
+
end
|
111
|
+
return if not scanf
|
112
|
+
m=msg.req_str.match(/\r?\n\r?\n/)
|
113
|
+
return if m.size < 1
|
114
|
+
start_pos = m.end(0)
|
115
|
+
|
116
|
+
begin
|
117
|
+
injs = open_in_path(@config[prop('file')])
|
118
|
+
injs.each_line do |l|
|
119
|
+
l.chomp!
|
120
|
+
next if (l =~ /^#{COMMENT_START}/ or l.length < 1)
|
121
|
+
@bscan.Log 2, "#{@mid}inject_to_body injecting: #{l}"
|
122
|
+
pos=start_pos
|
123
|
+
while (m=msg.req_str.match(/([^=]+)=([^=]+)/,pos))
|
124
|
+
req = msg.req_str[0..m.begin(2)-1] + l + msg.req_str[m.end(2)..-1]
|
125
|
+
req.sub!(/content-length\s*:\s*\d+/i, "Content-Length: "+(req.length-start_pos).to_s)
|
126
|
+
@bscan.Log 2, "#{@mid}inject_to_body #{pos} #{req}"
|
127
|
+
@bscan.activity[0]=true
|
128
|
+
send_req req, msg.getProtocol, l
|
129
|
+
pos=m.end(1)+1
|
130
|
+
end
|
131
|
+
end
|
132
|
+
injs.close
|
133
|
+
rescue Exception => e
|
134
|
+
@bscan.Log 0, "#{@mid}inject_to_body Exception: #{e.message}"
|
135
|
+
@bscan.Log 0, e.backtrace.join("\n")
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
|
141
|
+
|
142
|
+
end
|