-
Notifications
You must be signed in to change notification settings - Fork 2
/
antiscan.module
461 lines (412 loc) · 12.5 KB
/
antiscan.module
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
<?php
/**
* @file antiscan.module
*/
define('MODULE_UID', 10001); // reasonable big uid for use in DB records
/**
* Implements hook_config_info().
*/
function antiscan_config_info() {
$prefixes['antiscan.settings'] = array(
'label' => t('Antiscan'),
'group' => t('Configuration'),
);
return $prefixes;
}
/**
* Implements hook_permission().
* Same as IP address blocking module
*
*/
function antiscan_permission() {
return array(
'antiscan settings' => array(
'title' => t('Administer Antiscan settings')
),
);
}
/**
* Implements hook_menu().
*
*/
function antiscan_menu() {
$items = array();
$items['admin/config/people/antiscan'] = array(
'type' => MENU_NORMAL_ITEM,
'title' => 'Antiscan',
'description' => 'Manage IP blocking for restricted paths.',
'page callback' => 'backdrop_get_form',
'page arguments' => array('antiscan_form'),
'access arguments' => array('antiscan settings'),
'file' => 'antiscan.admin.inc',
);
return $items;
}
/**
* Implements hook_boot().
*
*/
function antiscan_boot() {
$request_uri = isset($_SERVER['REQUEST_URI']) ? htmlspecialchars($_SERVER['REQUEST_URI']) : '';
$ua_string = isset($_SERVER['HTTP_USER_AGENT']) ? htmlspecialchars($_SERVER['HTTP_USER_AGENT']) : '';
$referrer = isset($_SERVER['HTTP_REFERER']) ? htmlspecialchars($_SERVER['HTTP_REFERER']) : '';
$ip = check_plain(ip_address());
$config = config('antiscan.settings');
$test_mode = $config->get('test_mode');
if ($test_mode || (antiscan_check_ip($ip) && !antiscan_ip_blocked($ip))) {
if (antiscan_path_match($request_uri)) {
antiscan_action($ip, 'path', $request_uri);
}
elseif (antiscan_blocked_ua($ua_string)) {
antiscan_action($ip, 'ua', $ua_string);
}
elseif (antiscan_blocked_referrer($referrer)) {
antiscan_action($ip, 'referrer', $referrer);
}
}
}
/**
* Check if path contain restricted pattern.
* @param string $path
* @return boolean
*/
function antiscan_path_match($path = '') {
$match = FALSE;
$config = config('antiscan.settings');
$path_patterns = $config->get('path_patterns');
$patterns = _textarea_to_array($path_patterns);
foreach ($patterns as $pattern) {
if (substr($pattern, -1) == '*') {
$pattern = substr($pattern, 0, -1);
}
if (strpos(strtolower($path), $pattern) !== FALSE) {
$match = TRUE;
break;
}
}
return $match;
}
/**
* Check if UA string listed in the list blocked.
* @param string $ua
* @return boolean
*/
function antiscan_blocked_ua($ua = '') {
$match = FALSE;
$config = config('antiscan.settings');
$blocked_ua = $config->get('blocked_ua');
$u_agents = _textarea_to_array($blocked_ua);
foreach ($u_agents as $u_agent) {
if (substr($u_agent, -1) == '*') {
$u_agent = substr($u_agent, 0, -1);
}
if (strpos($ua, $u_agent) !== FALSE) {
$match = TRUE;
break;
}
}
return $match;
}
/**
* Check if referrer listed in the list blocked.
* @param string $referrer
* @return boolean
*/
function antiscan_blocked_referrer($referrer = '') {
$match = FALSE;
$config = config('antiscan.settings');
$blocked_referrer = $config->get('blocked_referrer');
$blocked_refs = _textarea_to_array($blocked_referrer);
foreach ($blocked_refs as $blocked_ref) {
if (strpos(strtolower($referrer), $blocked_ref) !== FALSE) {
$match = TRUE;
break;
}
}
return $match;
}
/**
* Check IP then do required action.
* @param string $ip
* @param string $type
* @param string $subject
*/
function antiscan_action($ip, $type, $subject) {
$config = config('antiscan.settings');
$test_mode = $config->get('test_mode');
$log_enabled = $config->get('log_enabled');
$threshold_enabled = $config->get('threshold_enabled');
$window = intval($config->get('threshold_window'));
$limit = intval($config->get('threshold_limit'));
$pre_ban_message = '<h1>Suspicious activity detected. Your IP address ' . $ip . ' will be blocked if you continue such attempts.</h1>';
$ban_message = '<h1>Suspicious activity detected, your IP address ' . $ip . ' has been blocked.</h1>';
$ban_test_message = '<p>Blocking reason: ' . $type . ' is ' . $subject . '</p>'
. '<h2>This is not a real blocking - the test mode of the "Antiscan" module is on!</h2>';
if ($test_mode) {
header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
print $ban_message . $ban_test_message;
exit();
}
if (!antiscan_logged_in_ip($ip)) {
header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
$reason = 'Other';
switch ($type) {
case 'path':
// Check if threshold is enabled
if ($threshold_enabled) {
include_once BACKDROP_ROOT . '/core/includes/common.inc';
flood_register_event('antiscan', $window);
// When flood_is_allowed return false, the user will be blocked.
if (flood_is_allowed('antiscan', $limit, $window)) {
if ($log_enabled) {
watchdog(
'antiscan',
'IP %ip attempting to use restricted path "%subject".',
array('%ip' => $ip, '%subject' => $subject),
WATCHDOG_WARNING
);
print $pre_ban_message;
exit();
}
}
}
if (strlen($subject) > 45) {
$reason = 'Request: ' . substr($subject, 0, 45) . '...';
}
else {
$reason = 'Request: ' . $subject;
}
break;
case 'ua':
$reason = 'Blocked User-Agent: ' . $subject;
break;
case 'referrer':
$reason = 'Spam referrer: ' . $subject;
break;
}
print $ban_message;
antiscan_block_ip($ip, $reason, $type);
exit();
}
}
/**
* Check if IP is current logged-in user IP.
* @global object $user
* @param string $ip
* @return TRUE if IP is current logged-in user IP
*/
function antiscan_logged_in_ip($ip = '') {
global $user;
if ($user->uid > 0 && $user->hostname == $ip) {
return TRUE;
}
else {
return FALSE;
}
}
/**
* Block an IP address.
*
* @param string $ip IP address to block.
* @param string $reason Reason for blocking.
* @param string $type
*/
function antiscan_block_ip($ip, $reason, $type) {
$config = config('antiscan.settings');
$log_enabled = $config->get('log_enabled');
if (mb_strlen($reason,'UTF-8') > 230) {
$reason_dec = substr($reason, 0, 200) . ' ... ';
}
else {
$reason_dec = $reason;
}
if (db_field_exists('blocked_ips', 'type')) {
$fields = array('ip' => $ip, 'reason' => $reason_dec, 'time' => time(), 'uid' => MODULE_UID, 'type' => $type);
}
else {
$fields = array('ip' => $ip, 'reason' => $reason_dec, 'time' => time(), 'uid' => MODULE_UID,);
}
// Insert the record to DB.
try {
db_insert('blocked_ips')->fields($fields)->execute();
}
catch (PDOException|FieldsOverlapException|NoFieldsException $e) {
watchdog_exception('antiscan', $e);
}
if ($log_enabled) {
watchdog(
'antiscan',
'IP %ip blocked. %reason.',
array('%ip' => $ip, '%reason' => $reason_dec),
WATCHDOG_WARNING
);
}
}
/**
* Check if IP is valid
* @param string $ip IP address
* @return boolean TRUE if $ip is valid IP address
*/
function antiscan_check_ip($ip) {
return (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE) !== FALSE);
}
/**
* Checks to see if an IP address already blocked.
* Blocked IP addresses are stored in the database.
* @param $ip IP address to check.
* @return bool TRUE if access is denied, FALSE if access is allowed.
*/
function antiscan_ip_blocked($ip) {
$blocked = FALSE;
if (class_exists('Database', FALSE)) {
$blocked = (bool) db_select('blocked_ips', 'bi')
->fields('bi', array('ip'))
->condition('ip', $ip)
->execute()
->fetchField();
}
return $blocked;
}
/**
* Implementation of hook_cron().
*/
function antiscan_cron() {
$config = config('antiscan.settings');
$abuseipdb_report = $config->get('abuseipdb_report');
$unblock = $config->get('unblock');
$unblock_after = $config->get('unblock_after');
$time_expired = time() - $unblock_after;
antiscan_fix_duplicated_ips();
if ($unblock) {
antiscan_unblock($time_expired);
}
if ($abuseipdb_report) {
antiscan_abuseipdb_report();
}
}
/**
* Remove duplicated blocked IP records from DB.
* Also helps to avoid repeating the reports to AbuseIPDB.
* There can be several such records with the same IP and timestamp,
* as a result of the attack of bots.
*/
function antiscan_fix_duplicated_ips() {
$query = 'DELETE i1 FROM {blocked_ips} i1 INNER JOIN {blocked_ips} i2 WHERE i1.iid < i2.iid AND i1.ip = i2.ip';
$result = db_query($query);
if ($result->rowCount()) {
$duplicated = $result->rowCount();
watchdog('antiscan', 'Removed %duplicated duplicate(s) of blocked IP.', array('%duplicated' => $duplicated));
}
}
/**
* Remove blocked IP records from DB
* @param int $time_expired
*/
function antiscan_unblock($time_expired) {
$unblocked = db_delete('blocked_ips')
->condition('uid', MODULE_UID)
->condition('time', $time_expired,'<')
->execute();
if ($unblocked > 0) {
watchdog('antiscan', 'Unblocked %unblocked IP(s). Blocking period expired.', array('%unblocked' => $unblocked));
}
}
/**
* Report blocked IP to AbuseIPDB.
*/
function antiscan_abuseipdb_report() {
if (module_exists('abuseipdb_report')) {
$config = config('antiscan.settings');
$abuseipdb_report = $config->get('abuseipdb_report');
if ($abuseipdb_report) {
$last_report_date = state_get('antiscan_abuseipdb_report_last_date', 0);
$with_type = db_field_exists('blocked_ips', 'type');
$fields = $with_type ? array('ip', 'reason', 'type') : array('ip', 'reason');
$result = db_select('blocked_ips', 'bi')
->fields('bi', $fields)
->condition('uid', MODULE_UID, '=')
->condition('time', $last_report_date, '>')
->execute()
->fetchAll();
if ($result) {
// Most relevant categories: Bad Web Bot, Web App Attack
$categories = array(19, 21);
foreach ($result as $record => $value) {
$ip = $value->ip;
$request = $with_type ? $value->type : $value->reason;
// Replacing information that may be sensitive.
switch ($request) {
case 'path':
$message = 'malicious activity detected';
break;
case 'ua':
$message = 'forbidden user agent';
break;
case 'referrer':
$message = 'forbidden referrer';
break;
default:
$message = $request;
}
abuseipdb_report_ip($ip, $message, '"Antiscan" module', $categories);
sleep(1);
}
state_set('antiscan_abuseipdb_report_last_date', time());
}
}
}
}
/**
* Utility function
* @param string $raw_string
* @param boolean $use_comma If TRUE comma used as divider too, otherwise return/new line only.
* @return array of strings saved in the configuration file string via textarea field
*/
function _textarea_to_array($raw_string = '', $use_comma = TRUE) {
$array_of_strings = array();
if (!empty($raw_string)) {
if ($use_comma) {
$raw_array = preg_split("/\\r\\n|\\r|\\n|,/", $raw_string);
}
else {
$raw_array = preg_split("/\\r\\n|\\r|\\n/", $raw_string);
}
$array_of_strings = array_filter(array_map('trim', $raw_array));
}
return $array_of_strings;
}
/**
* Implements hook_block_info().
*/
function antiscan_block_info() {
$blocks['blocked_ips'] = array(
'info' => t('Blocked IPs'),
'description' => t('Number of blocked IPs.'),
);
return $blocks;
}
/**
* Implements hook_block_view().
*
* Generates a block with number of blocked IPs.
*/
function antiscan_block_view($delta = '') {
$block = array();
$ip_num = '';
include_once BACKDROP_ROOT . '/core/includes/install.inc';
$requirements = system_get_requirements();
foreach ($requirements as $requirement) {
if (isset($requirement['title']) && $requirement['title'] === 'IP address blocking') {
$ip_num = $requirement['value'];
}
}
if (user_access('antiscan settings') && ($delta === 'blocked_ips')) {
if (!empty($ip_num)) {
$block['subject'] = t('Blocked IPs');
$block['content'] = array(
'#markup' => 'Total ' . $ip_num,
);
return $block;
}
}
}