<?php
/**
* TCP 連接埠掃描與 Banner Grabbing
* @param string $host 目標主機(IP 或域名)
* @param int $port 連接埠號
* @param float $timeout 連線逾時(秒)
* @return array ['success' => bool, 'banner' => string, 'error' => string]
*/
function scanPort(string $host, int $port, float $timeout = 3.0): array {
$socket = @fsockopen($host, $port, $errno, $errstr, $timeout);
if (!$socket) {
return [
'success' => false,
'banner' => '',
'error' => "Connection failed ($errno): $errstr"
];
}
// 設定讀取逾時
stream_set_timeout($socket, $timeout);
// 針對常見協議發送適當請求以誘導 Banner 回應
$request = match ($port) {
21 => "USER anonymous\r\n", // FTP
22 => "", // SSH 會主動發送 Banner
25 => "EHLO scanner\r\n", // SMTP
80 => "HEAD / HTTP/1.0\r\n\r\n", // HTTP
110 => "USER test\r\n", // POP3
143 => "A001 CAPABILITY\r\n", // IMAP
3306 => "\x0a", // MySQL 握手包觸發
6379 => "PING\r\n", // Redis
default => "\r\n", // 通用換行嘗試
};
if ($request) {
fwrite($socket, $request);
}
// 讀取回應(限制大小防止記憶體耗盡)
$banner = '';
while (!feof($socket) && strlen($banner) < 4096) {
$banner .= fread($socket, 1024);
$meta = stream_get_meta_data($socket);
if ($meta['timed_out']) break;
}
fclose($socket);
return [
'success' => true,
'banner' => trim($banner),
'error' => ''
];
}
// 批次掃描範例
$targets = [
['example.com', 80],
['example.com', 443],
['192.168.1.1', 22],
['db.internal', 3306],
];
foreach ($targets as [$host, $port]) {
$result = scanPort($host, $port);
$status = $result['success'] ? '✅ 開放' : '❌ 關閉/過濾';
echo "{$host}:{$port} {$status}\n";
if ($result['banner']) {
echo " Banner: " . str_replace(["\r", "\n"], ['\\r', '\\n'], $result['banner']) . "\n";
}
}
// 概念示意:使用 Swoole 協程並發
use Swoole\Coroutine;
Coroutine\run(function () {
$hosts = ['192.168.1.1', '192.168.1.2', '192.168.1.3'];
$ports = [22, 80, 443, 3306];
$channel = new Swoole\Coroutine\Channel(count($hosts) * count($ports));
foreach ($hosts as $host) {
foreach ($ports as $port) {
Coroutine::create(function () use ($host, $port, $channel) {
$result = scanPort($host, $port, 1.0);
$channel->push([$host, $port, $result]);
});
}
}
for ($i = 0; $i < count($hosts) * count($ports); $i++) {
[$host, $port, $result] = $channel->pop();
// 處理結果...
}
});
function identifyService(string $banner, int $port): string {
$signatures = [
'SSH-' => 'SSH',
'220 ' => 'FTP',
'220-' => 'FTP',
'HTTP/' => 'HTTP',
'Server:' => 'HTTP',
'MySQL' => 'MySQL',
'Redis' => 'Redis',
'PostgreSQL' => 'PostgreSQL',
'Microsoft-IIS' => 'IIS',
'Apache' => 'Apache',
'nginx' => 'Nginx',
];
foreach ($signatures as $sig => $name) {
if (stripos($banner, $sig) !== false) {
return $name;
}
}
return 'Unknown';
}
function outputResults(array $results, string $format = 'table'): void {
match ($format) {
'json' => print(json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)),
'csv' => {
echo "Host,Port,Status,Service,Banner\n";
foreach ($results as $r) {
echo "{$r['host']},{$r['port']}," .
($r['success'] ? 'Open' : 'Closed') . "," .
$r['service'] . "," .
'"' . str_replace('"', '""', $r['banner']) . "\"\n";
}
},
default => {
// 表格輸出
printf("%-20s %-6s %-8s %-15s %s\n", 'Host', 'Port', 'Status', 'Service', 'Banner');
foreach ($results as $r) {
printf("%-20s %-6d %-8s %-15s %s\n",
$r['host'], $r['port'],
$r['success'] ? 'Open' : 'Closed',
$r['service'],
mb_strimwidth($r['banner'], 0, 60, '...')
);
}
}
};
}
PHP 的 fsockopen/stream_socket_client 提供了輕量級的 TCP 連線能力,配合協議特定請求可實作基礎的連接埠掃描與 Banner Grabbing。實務部署時應注意:非同步並發提升效率、服務指紋識別增加價值、嚴格遵守法律授權範圍。