mirror of
https://github.com/hotyue/IP-Sentinel.git
synced 2026-09-04 23:18:17 +08:00
security(core): 引入时间戳与动态 HMAC-SHA256 签名,彻底斩断明文 PSK 泄露与重放攻击隐患 (v3.0.4)
This commit is contained in:
+57
-24
@@ -55,18 +55,24 @@ if [ -n "$AGENT_IP" ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 3. 启动轻量级 Python3 Webhook 监听服务 (带 403 权限校验路由)
|
# 3. 启动轻量级 Python3 Webhook 监听服务 (v3.0.4 动态 HMAC 签名防重放)
|
||||||
cat > "${INSTALL_DIR}/core/webhook.py" << 'EOF'
|
cat > "${INSTALL_DIR}/core/webhook.py" << 'EOF'
|
||||||
import http.server
|
import http.server
|
||||||
import socketserver
|
import socketserver
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import html # [v3.0.2+ 修复] 用于安全转义日志中的特殊字符
|
import html
|
||||||
|
# ================== [v3.0.4 新增密码学与解析依赖] ==================
|
||||||
|
import urllib.parse
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
import time
|
||||||
|
# ====================================================================
|
||||||
|
|
||||||
PORT = int(sys.argv[1])
|
PORT = int(sys.argv[1])
|
||||||
|
|
||||||
# 🛡️ [v3.0.2 紧急加固] 提取全局鉴权 Token (利用 CHAT_ID 作为 PSK 预共享密钥)
|
# 🛡️ 提取全局鉴权 Token (利用 CHAT_ID 作为 PSK 预共享密钥)
|
||||||
AUTH_TOKEN = ""
|
AUTH_TOKEN = ""
|
||||||
if os.path.exists('/opt/ip_sentinel/config.conf'):
|
if os.path.exists('/opt/ip_sentinel/config.conf'):
|
||||||
with open('/opt/ip_sentinel/config.conf', 'r') as f:
|
with open('/opt/ip_sentinel/config.conf', 'r') as f:
|
||||||
@@ -78,16 +84,49 @@ if os.path.exists('/opt/ip_sentinel/config.conf'):
|
|||||||
|
|
||||||
class AgentHandler(http.server.BaseHTTPRequestHandler):
|
class AgentHandler(http.server.BaseHTTPRequestHandler):
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
# 🛡️ 鉴权拦截器:防非法扫描与 DDoS 资源耗尽
|
# 🛡️ [v3.0.4 核心] URL 解析与动态 HMAC-SHA256 签名校验
|
||||||
if AUTH_TOKEN and f"auth={AUTH_TOKEN}" not in self.path:
|
parsed = urllib.parse.urlparse(self.path)
|
||||||
self.send_response(401)
|
req_path = parsed.path
|
||||||
self.send_header("Content-type", "text/plain")
|
|
||||||
self.end_headers()
|
if AUTH_TOKEN:
|
||||||
self.wfile.write(b"401 Unauthorized: Access Denied\n")
|
query = urllib.parse.parse_qs(parsed.query)
|
||||||
return
|
req_t = query.get('t', [''])[0]
|
||||||
|
req_sign = query.get('sign', [''])[0]
|
||||||
|
|
||||||
|
# 校验 1:参数是否齐全
|
||||||
|
if not req_t or not req_sign:
|
||||||
|
self.send_response(401)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(b"401 Unauthorized: Missing Signature\n")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 校验 2:时间戳防重放 (误差 ±60秒 内有效,拒绝隔夜抓包重放)
|
||||||
|
if abs(int(time.time()) - int(req_t)) > 60:
|
||||||
|
self.send_response(401)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(b"401 Unauthorized: Request Expired\n")
|
||||||
|
return
|
||||||
|
except ValueError:
|
||||||
|
self.send_response(401)
|
||||||
|
self.end_headers()
|
||||||
|
return
|
||||||
|
|
||||||
|
# 校验 3:HMAC 数据完整性与身份合法性校验
|
||||||
|
msg = f"{req_path}:{req_t}".encode('utf-8')
|
||||||
|
expected_sign = hmac.new(AUTH_TOKEN.encode('utf-8'), msg, hashlib.sha256).hexdigest()
|
||||||
|
|
||||||
|
# 使用 compare_digest 防御时序攻击
|
||||||
|
if not hmac.compare_digest(expected_sign, req_sign):
|
||||||
|
self.send_response(401)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(b"401 Unauthorized: Signature Mismatch\n")
|
||||||
|
return
|
||||||
|
|
||||||
# 路由 1: Google 区域纠偏 (由于 URL 带有 auth 参数,必须由 == 改为 startswith)
|
# ================== 路由分发 (恢复为安全的精确匹配) ==================
|
||||||
if self.path.startswith('/trigger_google') or self.path.startswith('/trigger_run'):
|
|
||||||
|
# 路由 1: Google 区域纠偏
|
||||||
|
if req_path == '/trigger_google' or req_path == '/trigger_run':
|
||||||
if os.path.exists('/opt/ip_sentinel/core/mod_google.sh'):
|
if os.path.exists('/opt/ip_sentinel/core/mod_google.sh'):
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-type", "text/plain")
|
self.send_header("Content-type", "text/plain")
|
||||||
@@ -101,7 +140,7 @@ class AgentHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
self.wfile.write(b"403 Forbidden: Google Module Disabled\n")
|
self.wfile.write(b"403 Forbidden: Google Module Disabled\n")
|
||||||
|
|
||||||
# 路由 2: IP 信用净化
|
# 路由 2: IP 信用净化
|
||||||
elif self.path.startswith('/trigger_trust'):
|
elif req_path == '/trigger_trust':
|
||||||
if os.path.exists('/opt/ip_sentinel/core/mod_trust.sh'):
|
if os.path.exists('/opt/ip_sentinel/core/mod_trust.sh'):
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-type", "text/plain")
|
self.send_header("Content-type", "text/plain")
|
||||||
@@ -115,22 +154,21 @@ class AgentHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
self.wfile.write(b"403 Forbidden: Trust Module Disabled\n")
|
self.wfile.write(b"403 Forbidden: Trust Module Disabled\n")
|
||||||
|
|
||||||
# 路由 3: 触发战报推送
|
# 路由 3: 触发战报推送
|
||||||
elif self.path.startswith('/trigger_report'):
|
elif req_path == '/trigger_report':
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-type", "text/plain")
|
self.send_header("Content-type", "text/plain")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(b"Action Accepted: tg_report\n")
|
self.wfile.write(b"Action Accepted: tg_report\n")
|
||||||
subprocess.Popen(['bash', '/opt/ip_sentinel/core/tg_report.sh'])
|
subprocess.Popen(['bash', '/opt/ip_sentinel/core/tg_report.sh'])
|
||||||
|
|
||||||
# 路由 4: 抓取并回传实时日志 (v3.0.2 鲁棒性增强版)
|
# 路由 4: 抓取并回传实时日志
|
||||||
elif self.path.startswith('/trigger_log'):
|
elif req_path == '/trigger_log':
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-type", "text/plain")
|
self.send_header("Content-type", "text/plain")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(b"Action Accepted: fetch_log\n")
|
self.wfile.write(b"Action Accepted: fetch_log\n")
|
||||||
|
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.parse
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config = {}
|
config = {}
|
||||||
@@ -142,36 +180,31 @@ class AgentHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
key, val = line.split('=', 1)
|
key, val = line.split('=', 1)
|
||||||
config[key] = val.strip('"\'')
|
config[key] = val.strip('"\'')
|
||||||
|
|
||||||
# 🛡️ 核心修复:HTML 转义防止 Telegram 报错
|
|
||||||
log_data = "日志文件不存在或为空"
|
log_data = "日志文件不存在或为空"
|
||||||
log_path = '/opt/ip_sentinel/logs/sentinel.log'
|
log_path = '/opt/ip_sentinel/logs/sentinel.log'
|
||||||
if os.path.exists(log_path):
|
if os.path.exists(log_path):
|
||||||
with open(log_path, 'r', errors='ignore') as f:
|
with open(log_path, 'r', errors='ignore') as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
if lines:
|
if lines:
|
||||||
# 抓取最后 15 行并进行转义,确保 [ ] & < > 不会破坏消息
|
|
||||||
log_data = html.escape("".join(lines[-15:]))
|
log_data = html.escape("".join(lines[-15:]))
|
||||||
|
|
||||||
node_name = subprocess.check_output(['hostname']).decode('utf-8').strip()[:15]
|
node_name = subprocess.check_output(['hostname']).decode('utf-8').strip()[:15]
|
||||||
|
|
||||||
# 🛡️ 核心修复:使用 HTML 模式,日志显示更整齐且稳定
|
|
||||||
text_msg = f"📄 <b>[{node_name}] 实时运行日志:</b>\n<pre><code>{log_data}</code></pre>"
|
text_msg = f"📄 <b>[{node_name}] 实时运行日志:</b>\n<pre><code>{log_data}</code></pre>"
|
||||||
|
|
||||||
data = urllib.parse.urlencode({
|
data = urllib.parse.urlencode({
|
||||||
'chat_id': config.get('CHAT_ID', ''),
|
'chat_id': config.get('CHAT_ID', ''),
|
||||||
'text': text_msg,
|
'text': text_msg,
|
||||||
'parse_mode': 'HTML'
|
'parse_mode': 'HTML'
|
||||||
}).encode('utf-8')
|
}).encode('utf-8')
|
||||||
|
|
||||||
# 🛡️ 核心修复:补全 UA 头,通过安全网关校验
|
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
config.get('TG_API_URL', ''),
|
config.get('TG_API_URL', ''),
|
||||||
data=data,
|
data=data,
|
||||||
headers={'User-Agent': 'IP-Sentinel-Agent/3.0.2'}
|
headers={'User-Agent': 'IP-Sentinel-Agent/3.0.4'}
|
||||||
)
|
)
|
||||||
urllib.request.urlopen(req, timeout=10)
|
urllib.request.urlopen(req, timeout=10)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 发生错误时在本地打印,便于长官排查
|
|
||||||
print(f"Log transmission failed: {e}")
|
print(f"Log transmission failed: {e}")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
+42
-5
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# 脚本名称: tg_master.sh (Master 端调度枢纽 V2.0 模块化适配版)
|
# 脚本名称: tg_master.sh (Master 端调度枢纽 V3.0.4 动态签名版)
|
||||||
# 核心功能: 监听 TG、操作 SQLite、Webhook 精准调度、403权限拦截、僵尸节点清理
|
# 核心功能: 监听 TG、操作 SQLite、Webhook 精准调度、403权限拦截、僵尸节点清理
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
@@ -35,6 +35,25 @@ db_exec() {
|
|||||||
sqlite3 "$DB_FILE" "$1"
|
sqlite3 "$DB_FILE" "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ================== [v3.0.4 核心: 动态 HMAC 签名生成器] ==================
|
||||||
|
# 用法: generate_signed_url <IP> <PORT> <PATH>
|
||||||
|
generate_signed_url() {
|
||||||
|
local target_ip=$1
|
||||||
|
local target_port=$2
|
||||||
|
local action_path=$3
|
||||||
|
local current_t=$(date +%s)
|
||||||
|
|
||||||
|
# 构建加密载荷: "路径:时间戳"
|
||||||
|
local payload="${action_path}:${current_t}"
|
||||||
|
|
||||||
|
# 使用 CHAT_ID 作为密钥,生成 SHA256 HMAC 签名
|
||||||
|
local signature=$(echo -n "$payload" | openssl dgst -sha256 -hmac "$CHAT_ID" | awk '{print $NF}')
|
||||||
|
|
||||||
|
# 返回最终带签名的 URL
|
||||||
|
echo "http://${target_ip}:${target_port}${action_path}?t=${current_t}&sign=${signature}"
|
||||||
|
}
|
||||||
|
# ========================================================================
|
||||||
|
|
||||||
# --- 核心轮询循环 ---
|
# --- 核心轮询循环 ---
|
||||||
while true; do
|
while true; do
|
||||||
OFFSET=$(cat $OFFSET_FILE)
|
OFFSET=$(cat $OFFSET_FILE)
|
||||||
@@ -106,12 +125,29 @@ while true; do
|
|||||||
else
|
else
|
||||||
send_msg "$CHAT_ID" "📢 **司令部指令下达:正在召唤所有哨兵回传简报...**"
|
send_msg "$CHAT_ID" "📢 **司令部指令下达:正在召唤所有哨兵回传简报...**"
|
||||||
echo "$NODE_DATA" | while IFS='|' read -r NNAME AIP APORT; do
|
echo "$NODE_DATA" | while IFS='|' read -r NNAME AIP APORT; do
|
||||||
# [v3.0.2 紧急加固] 批量下发战报时,必须同步追加 ?auth 鉴权令牌,防止被 Agent 拒绝
|
# 🛡️ [v3.0.4] 动态签名防重放批量下发
|
||||||
curl -s -m 5 "http://${AIP}:${APORT}/trigger_report?auth=${CHAT_ID}" > /dev/null &
|
TARGET_URL=$(generate_signed_url "$AIP" "$APORT" "/trigger_report")
|
||||||
|
curl -s -m 5 "$TARGET_URL" > /dev/null &
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
# ================== [补充缺失的全节点一键维护功能] ==================
|
||||||
|
"all_run")
|
||||||
|
NODE_DATA=$(db_exec "SELECT node_name, agent_ip, agent_port FROM nodes WHERE chat_id='$CHAT_ID';")
|
||||||
|
if [ -z "$NODE_DATA" ]; then
|
||||||
|
send_msg "$CHAT_ID" "⚠️ 您名下暂无在线节点。"
|
||||||
|
else
|
||||||
|
send_msg "$CHAT_ID" "📢 **司令部指令下达:正在唤醒所有哨兵执行系统维护...**"
|
||||||
|
echo "$NODE_DATA" | while IFS='|' read -r NNAME AIP APORT; do
|
||||||
|
# 🛡️ [v3.0.4] 动态签名防重放批量下发 (维护模块)
|
||||||
|
TARGET_URL=$(generate_signed_url "$AIP" "$APORT" "/trigger_run")
|
||||||
|
curl -s -m 5 "$TARGET_URL" > /dev/null &
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
# ====================================================================
|
||||||
|
|
||||||
"list_nodes")
|
"list_nodes")
|
||||||
NODE_LIST=$(db_exec "SELECT node_name FROM nodes WHERE chat_id='$CHAT_ID';")
|
NODE_LIST=$(db_exec "SELECT node_name FROM nodes WHERE chat_id='$CHAT_ID';")
|
||||||
if [ -z "$NODE_LIST" ]; then
|
if [ -z "$NODE_LIST" ]; then
|
||||||
@@ -174,8 +210,9 @@ while true; do
|
|||||||
send_msg "$CHAT_ID" "⏳ 正在向 \`$TARGET_NODE\` ($AGENT_IP) 下发 [$ACTION_TYPE] 指令,请稍候..."
|
send_msg "$CHAT_ID" "⏳ 正在向 \`$TARGET_NODE\` ($AGENT_IP) 下发 [$ACTION_TYPE] 指令,请稍候..."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 触发 Webhook(v3.0.2 避免DDoS攻击加固)
|
# 🛡️ [v3.0.4] 动态签名生成与触发 (防重放与防篡改)
|
||||||
RESPONSE=$(curl -s -m 5 "http://${AGENT_IP}:${AGENT_PORT}/trigger_${ACTION_TYPE}?auth=${CHAT_ID}" || echo "FAILED")
|
TARGET_URL=$(generate_signed_url "$AGENT_IP" "$AGENT_PORT" "/trigger_${ACTION_TYPE}")
|
||||||
|
RESPONSE=$(curl -s -m 5 "$TARGET_URL" || echo "FAILED")
|
||||||
|
|
||||||
# 结果判定
|
# 结果判定
|
||||||
if [ "$RESPONSE" == "FAILED" ]; then
|
if [ "$RESPONSE" == "FAILED" ]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user