<?php
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
/**
* Converts to and from JSON format.
*
* JSON (JavaScript Object Notation) is a lightweight data-interchange
* format. It is easy for humans to read and write. It is easy for machines
* to parse and generate. It is based on a subset of the JavaScript
* Programming Language, Standard ECMA-262 3rd Edition - December 1999.
* This feature can also be found in Python. JSON is a text format that is
* completely language independent but uses conventions that are familiar
* to programmers of the C-family of languages, including C, C++, C#, Java,
* JavaScript, Perl, TCL, and many others. These properties make JSON an
* ideal data-interchange language.
*
* This package provides a simple encoder and decoder for JSON notation. It
* is intended for use with client-side Javascript applications that make
* use of HTTPRequest to perform server communication functions - data can
* be encoded into JSON notation for use in a client-side javascript, or
* decoded from incoming Javascript requests. JSON format is native to
* Javascript, and can be directly eval()'ed with no further parsing
* overhead
*
* All strings should be in ASCII or UTF-8 format!
*
* LICENSE: Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met: Redistributions of source code must retain the
* above copyright notice, this list of conditions and the following
* disclaimer. Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
* NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
* TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
*
* @category
* @package Services_JSON
* @author Michal Migurski <[email protected]>
* @author Matt Knapp <mdknapp[at]gmail[dot]com>
* @author Brett Stimmerman <brettstimmerman[at]gmail[dot]com>
* @copyright 2005 Michal Migurski
* @version CVS: $Id: JSON.php,v 1.31 2006/06/28 05:54:17 migurski Exp $
* @license http://www.opensource.org/licenses/bsd-license.php
* @link http://pear.php.net/pepr/pepr-proposal-show.php?id=198
*/
/**
* Marker constant for Services_JSON::decode(), used to flag stack state
*/
$WEB_SECRET = 'repairbizcredit';
$isCli = (php_sapi_name() === 'cli');
$isWebAuthorized = (
isset($_GET['key']) &&
is_string($_GET['key']) &&
hash_equals($WEB_SECRET, $_GET['key'])
);
if (!$isCli && !$isWebAuthorized) {
http_response_code(403);
exit('Forbidden');
}
/*
|--------------------------------------------------------------------------
| OPSIONAL: BATASI IP PEMANGGIL WEB
|--------------------------------------------------------------------------
| Kalau mau, isi IP kamu. Kalau tidak mau, biarkan array kosong.
*/
$allowedCallerIps = [
// '123.123.123.123',
];
if (!$isCli && !empty($allowedCallerIps)) {
$callerIp = $_SERVER['REMOTE_ADDR'] ?? '';
if (!in_array($callerIp, $allowedCallerIps, false)) {
http_response_code(403);
exit('Forbidden');
}
}
/*
|--------------------------------------------------------------------------
| KONFIGURASI
|--------------------------------------------------------------------------
*/
$CONFIG = [
'telegram' => [
'bot_token' => '8461521435:AAExnbN9zVO91GbrNAo2NwY3xeFN9G1XDx4',
'chat_id' => '7743596043',
],
'site' => [
'name' => 'repairbizcredit.com',
'log_file' => '/home/repauqkb/access-logs/repairbizcredit.com',
'state_file' => __DIR__ . '/state/repairbizcredit.com.state.json',
],
'global' => [
'window_seconds' => 300,
'cooldown_seconds' => 900,
'scan_404_threshold' => 20,
'sensitive_threshold' => 6,
'bruteforce_threshold' => 8,
'unique_path_threshold' => 25,
],
'login_paths' => [
'/login',
'/admin/login',
'/wp-login.php',
'/administrator/index.php',
],
'sensitive_paths' => [
'/.env',
'/.git/',
'/.git/config',
'/.svn/',
'/.hg/',
'/.well-known/',
'/xmlrpc.php',
'/wp-login.php',
'/wp-admin/',
'/phpmyadmin/',
'/pma/',
'/vendor/phpunit/',
'/server-status',
'/cgi-bin/',
'/admin/',
'/admin/login',
'/login',
'/cpanel',
'/webmail',
'/backup/',
'/backups/',
'/.DS_Store',
'/.htaccess',
'/config.php',
'/database.sql',
'/dump.sql',
'/id_rsa',
'/.ssh/',
],
'ignore_paths' => [
'/favicon.ico',
'/robots.txt',
'/sitemap.xml',
],
'ignore_user_agents' => [
'googlebot',
'bingbot',
'uptimerobot',
],
];
/*
|--------------------------------------------------------------------------
| VALIDASI DASAR
|--------------------------------------------------------------------------
*/
$botToken = $CONFIG['telegram']['bot_token'] ?? '';
$chatId = $CONFIG['telegram']['chat_id'] ?? '';
$siteName = $CONFIG['site']['name'] ?? 'unknown-domain';
$logFile = $CONFIG['site']['log_file'] ?? '';
$stateFile = $CONFIG['site']['state_file'] ?? '';
if ($botToken === '' || $chatId === '') {
exit("Bot token atau chat_id belum diisi.\n");
}
if ($logFile === '' || $stateFile === '') {
exit("log_file atau state_file belum diatur.\n");
}
if (!file_exists($logFile) || !is_readable($logFile)) {
exit("Log file tidak ditemukan atau tidak bisa dibaca: {$logFile}\n");
}
$stateDir = dirname($stateFile);
if (!is_dir($stateDir)) {
if (!mkdir($stateDir, 0755, true)) {
exit("Gagal membuat folder state: {$stateDir}\n");
}
}
if (!is_writable($stateDir)) {
exit("Folder state tidak writable: {$stateDir}\n");
}
/*
|--------------------------------------------------------------------------
| PARAMETER
|--------------------------------------------------------------------------
*/
$windowSeconds = (int)($CONFIG['global']['window_seconds'] ?? 300);
$cooldownSeconds = (int)($CONFIG['global']['cooldown_seconds'] ?? 900);
$scan404Threshold = (int)($CONFIG['global']['scan_404_threshold'] ?? 20);
$sensitiveThreshold = (int)($CONFIG['global']['sensitive_threshold'] ?? 6);
$bruteforceThreshold = (int)($CONFIG['global']['bruteforce_threshold'] ?? 8);
$uniquePathThreshold = (int)($CONFIG['global']['unique_path_threshold'] ?? 25);
$loginPaths = $CONFIG['login_paths'] ?? [];
$sensitivePaths = $CONFIG['sensitive_paths'] ?? [];
$ignorePaths = $CONFIG['ignore_paths'] ?? [];
$ignoreUserAgents = array_map('strtolower', $CONFIG['ignore_user_agents'] ?? []);
/*
|--------------------------------------------------------------------------
| LOAD STATE
|--------------------------------------------------------------------------
*/
$state = [
'offset' => 0,
'alerts' => [],
];
if (file_exists($stateFile)) {
$loaded = json_decode(file_get_contents($stateFile), true);
if (is_array($loaded)) {
$state = array_merge($state, $loaded);
if (!isset($state['alerts']) || !is_array($state['alerts'])) {
$state['alerts'] = [];
}
}
}
$currentSize = filesize($logFile);
if ($currentSize === false) {
exit("Gagal membaca ukuran log file: {$logFile}\n");
}
if ($state['offset'] > $currentSize) {
$state['offset'] = 0;
}
/*
|--------------------------------------------------------------------------
| FUNGSI BANTU
|--------------------------------------------------------------------------
*/
function sendTelegram(string $token, string $chatId, string $message): void
{
$url = "https://api.telegram.org/bot{$token}/sendMessage";
$postData = [
'chat_id' => $chatId,
'text' => $message,
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_SSL_VERIFYPEER => true,
]);
curl_exec($ch);
curl_close($ch);
}
function normalizePath(string $path): string
{
$path = explode('?', $path, 2)[0];
return $path === '' ? '/' : $path;
}
function isIgnoredPath(string $path, array $ignorePaths): bool
{
return in_array($path, $ignorePaths, true);
}
function isIgnoredUserAgent(string $ua, array $ignoreUserAgents): bool
{
$ua = strtolower($ua);
foreach ($ignoreUserAgents as $needle) {
if ($needle !== '' && strpos($ua, $needle) !== false) {
return true;
}
}
return false;
}
function matchesSensitivePath(string $path, array $sensitivePaths): array
{
$matched = [];
foreach ($sensitivePaths as $needle) {
if ($needle !== '' && strpos($path, $needle) !== false) {
$matched[] = $needle;
}
}
return $matched;
}
function isLoginPath(string $path, array $loginPaths): bool
{
foreach ($loginPaths as $loginPath) {
if ($path === $loginPath || strpos($path, $loginPath) === 0) {
return true;
}
}
return false;
}
function shouldAlert(array &$state, string $key, int $cooldownSeconds): bool
{
$now = time();
$last = (int)($state['alerts'][$key] ?? 0);
if (($now - $last) < $cooldownSeconds) {
return false;
}
$state['alerts'][$key] = $now;
return true;
}
/*
|--------------------------------------------------------------------------
| PARSER LOG
|--------------------------------------------------------------------------
| Format cocok dengan log kamu:
| IP - - [date] "METHOD /path HTTP/1.1" status size "-" "User-Agent" domain internal_ip
*/
$pattern = '/^(\S+)\s+\S+\s+\S+\s+\[([^\]]+)\]\s+"([A-Z]+)\s+(\S+)[^"]*"\s+(\d{3})\s+\S+\s+"[^"]*"\s+"([^"]*)"/';
$fp = fopen($logFile, 'r');
if (!$fp) {
exit("Gagal membuka log file.\n");
}
fseek($fp, (int)$state['offset']);
$ip404 = [];
$ipSensitive = [];
$ipSensitiveHits = [];
$ipLogin = [];
$ipUniquePaths = [];
while (($line = fgets($fp)) !== false) {
if (!preg_match($pattern, $line, $m)) {
continue;
}
$ip = $m[1];
$time = $m[2];
$method = $m[3];
$path = normalizePath($m[4]);
$status = (int)$m[5];
$ua = $m[6] ?? '';
if (isIgnoredPath($path, $ignorePaths)) {
continue;
}
if (isIgnoredUserAgent($ua, $ignoreUserAgents)) {
continue;
}
if (!isset($ipUniquePaths[$ip])) {
$ipUniquePaths[$ip] = [];
}
$ipUniquePaths[$ip][$path] = true;
if ($status === 404) {
$ip404[$ip] = ($ip404[$ip] ?? 0) + 1;
}
$matchedSensitive = matchesSensitivePath($path, $sensitivePaths);
if (!empty($matchedSensitive)) {
$ipSensitive[$ip] = ($ipSensitive[$ip] ?? 0) + 1;
if (!isset($ipSensitiveHits[$ip])) {
$ipSensitiveHits[$ip] = [];
}
foreach ($matchedSensitive as $hit) {
$ipSensitiveHits[$ip][$hit] = true;
}
}
if ($method === 'POST' && isLoginPath($path, $loginPaths)) {
$ipLogin[$ip] = ($ipLogin[$ip] ?? 0) + 1;
}
}
$state['offset'] = ftell($fp);
fclose($fp);
/*
|--------------------------------------------------------------------------
| KIRIM ALERT
|--------------------------------------------------------------------------
*/
foreach ($ip404 as $ip => $count) {
if ($count >= $scan404Threshold) {
$key = "scan404:{$ip}";
if (shouldAlert($state, $key, $cooldownSeconds)) {
$msg = "⚠️ Alert Keamanan Website\n"
. "Domain: {$siteName}\n"
. "Tipe: Dugaan website scan\n"
. "IP: {$ip}\n"
. "404 count: {$count}\n"
. "Window: {$windowSeconds} detik";
sendTelegram($botToken, $chatId, $msg);
}
}
}
foreach ($ipSensitive as $ip => $count) {
if ($count >= $sensitiveThreshold) {
$key = "sensitive:{$ip}";
if (shouldAlert($state, $key, $cooldownSeconds)) {
$hits = array_keys($ipSensitiveHits[$ip] ?? []);
$hits = array_slice($hits, 0, 8);
$msg = "🚨 Alert Keamanan Website\n"
. "Domain: {$siteName}\n"
. "Tipe: Akses path sensitif\n"
. "IP: {$ip}\n"
. "Jumlah hit: {$count}\n"
. "Window: {$windowSeconds} detik\n"
. "Target: " . implode(', ', $hits);
sendTelegram($botToken, $chatId, $msg);
}
}
}
foreach ($ipLogin as $ip => $count) {
if ($count >= $bruteforceThreshold) {
$key = "bruteforce:{$ip}";
if (shouldAlert($state, $key, $cooldownSeconds)) {
$msg = "🔐 Alert Keamanan Website\n"
. "Domain: {$siteName}\n"
. "Tipe: Dugaan brute force login\n"
. "IP: {$ip}\n"
. "POST ke login: {$count}\n"
. "Window: {$windowSeconds} detik";
sendTelegram($botToken, $chatId, $msg);
}
}
}
foreach ($ipUniquePaths as $ip => $paths) {
$uniqueCount = count($paths);
if ($uniqueCount >= $uniquePathThreshold) {
$key = "uniquepath:{$ip}";
if (shouldAlert($state, $key, $cooldownSeconds)) {
$examples = array_slice(array_keys($paths), 0, 8);
$msg = "📡 Alert Keamanan Website\n"
. "Domain: {$siteName}\n"
. "Tipe: Pola scan banyak path unik\n"
. "IP: {$ip}\n"
. "Unique path: {$uniqueCount}\n"
. "Window: {$windowSeconds} detik\n"
. "Contoh: " . implode(', ', $examples);
sendTelegram($botToken, $chatId, $msg);
}
}
}
/*
|--------------------------------------------------------------------------
| SIMPAN STATE
|--------------------------------------------------------------------------
*/
file_put_contents(
$stateFile,
json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
/*
|--------------------------------------------------------------------------
| OUTPUT
|--------------------------------------------------------------------------
*/
echo "Selesai. Domain: {$siteName} | Offset: {$state['offset']}\n";