上传文件至「/」

This commit is contained in:
2026-08-18 07:54:05 +00:00
parent c394f9c902
commit 6b78281783
+704
View File
@@ -0,0 +1,704 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import requests
import time
import urllib.parse
import sys
import io
import uuid
import subprocess
import concurrent.futures
import re
import threading
import json
import os
from io import BytesIO
if sys.platform == 'win32':
try:
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', line_buffering=True)
except Exception:
pass
if getattr(sys, 'frozen', False):
BASE_DIR = os.path.dirname(sys.executable)
else:
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
CONFIG_FILE = os.path.join(BASE_DIR, "config.json")
STATE_FILE = os.path.join(BASE_DIR, ".eportal_state.json")
# 模块级默认值,main() 会从 config.json 覆盖
PORTAL_HOST = "http://192.168.200.100"
PORTAL_BASE_URL = PORTAL_HOST + "/eportal"
USERNAME = ""
PASSWORD = ""
MAX_ATTEMPTS = 60
SLEEP_SECONDS = 3
PING_HOST = "www.dufe.edu.cn"
PUBLIC_KEY_FALLBACK = "10001&94dd2a8675fb779e6b9f7103698634cd400f27a154afa67af6166a43fc26417222a79506d34cacc7641946abda1785b7acf9910ad6a0978c91ec84d40b71d2891379af19ffb333e7517e390bd26ac312fe940c340466b4a5d4af1d65c3b5944078f96a1a51a5a53e4bc302818b7c9f63c4a1b07bd7d874cef1c3d4b2f5eb7871"
DEFAULT_CONFIG = {
"username": "",
"password": "",
"portal_host": "http://192.168.200.100",
"max_attempts": 60,
"sleep_seconds": 3,
"ping_host": "www.dufe.edu.cn",
"public_key_fallback": "10001&94dd2a8675fb779e6b9f7103698634cd400f27a154afa67af6166a43fc26417222a79506d34cacc7641946abda1785b7acf9910ad6a0978c91ec84d40b71d2891379af19ffb333e7517e390bd26ac312fe940c340466b4a5d4af1d65c3b5944078f96a1a51a5a53e4bc302818b7c9f63c4a1b07bd7d874cef1c3d4b2f5eb7871",
}
def load_config():
def _write_default():
try:
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(DEFAULT_CONFIG, f, ensure_ascii=False, indent=4)
_print(f"已创建默认配置文件: {CONFIG_FILE}")
except Exception:
pass
if not os.path.exists(CONFIG_FILE):
_write_default()
return dict(DEFAULT_CONFIG)
# 检测空文件或内容为空
try:
if os.path.getsize(CONFIG_FILE) == 0:
_print("config.json 为空,重新生成默认配置", "ERROR")
_write_default()
return dict(DEFAULT_CONFIG)
except Exception:
pass
cfg = dict(DEFAULT_CONFIG)
try:
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, dict):
_print("config.json 格式错误,使用默认配置", "ERROR")
return dict(DEFAULT_CONFIG)
cfg.update(data)
except json.JSONDecodeError as e:
_print(f"config.json 解析失败: {e},使用默认配置", "ERROR")
except Exception as e:
_print(f"读取 config.json 失败: {e}", "ERROR")
return cfg
def _print(msg, level="INFO"):
print(f"[{level}] {msg}", flush=True)
_skip_wait = False # Ctrl+C 退出时跳过等待
def _wait_exit():
global _skip_wait
if _skip_wait:
return
try:
input("按任意键退出...")
except (EOFError, KeyboardInterrupt):
pass
def save_state(mac, user_index):
try:
with open(STATE_FILE, "w") as f:
json.dump({"mac": mac, "userIndex": user_index}, f)
except Exception:
pass
def load_state():
try:
if os.path.exists(STATE_FILE):
with open(STATE_FILE, "r") as f:
return json.load(f)
except Exception:
pass
return None
def detect_portal():
try:
requests.get(PORTAL_HOST + "/", timeout=(2, 2), allow_redirects=False)
return None, None
except Exception:
pass
for url in [
"http://www.baidu.com",
"http://detectportal.firefox.com/canonical.html",
"http://www.msftconnecttest.com/redirect",
]:
try:
resp = requests.get(url, timeout=(2, 2), allow_redirects=False)
if resp.status_code in (301, 302, 303, 307, 308):
loc = resp.headers.get("Location", "")
m = re.match(r'(https?://[^/]+(?:/eportal)?)', loc)
if m:
host = re.match(r'https?://[^/]+', m.group(1)).group(0)
base = m.group(1)
return host, base
except Exception:
pass
return None, None
_page_info_cache = None
def _fetch_page_info():
global _page_info_cache
if _page_info_cache:
return _page_info_cache
info = {"public_key": None, "query_string": "", "portal_mac": None}
def parse_page(resp, extract_qs=True):
html = resp.text
if "?" in resp.url:
info["query_string"] = resp.url.split("?", 1)[1]
m = re.search(r'[?&]mac=([^&]+)', resp.url)
if m:
info["portal_mac"] = urllib.parse.unquote(m.group(1)).replace(':', '').replace('-', '').lower()
# 从 HTML 提取 publicKey
for pid in ['publicKey', 'publicKeyModulus', 'publicKeyExponent']:
m = re.search(rf'(?:id|name)=["\']{pid}["\'][^>]*value=["\']([^"\']+)', html)
if m:
if pid == 'publicKey' and '&' in m.group(1):
info["public_key"] = m.group(1)
break
if not info["public_key"]:
exp_match = re.search(r'(?:id|name)=["\']publicKeyExponent["\'][^>]*value=["\']([^"\']+)', html)
mod_match = re.search(r'(?:id|name)=["\']publicKeyModulus["\'][^>]*value=["\']([^"\']+)', html)
if exp_match and mod_match:
info["public_key"] = exp_match.group(1) + "&" + mod_match.group(1)
if not extract_qs:
return
# 从 HTML 提取 queryString(JS 重定向: location.href='...?wlanuserip=...')
if not info["query_string"]:
m = re.search(r"location\.href\s*=\s*['\"]([^'\"]*\?[^'\"]+)", html)
if m:
qs_url = m.group(1)
if "?" in qs_url:
info["query_string"] = qs_url.split("?", 1)[1]
if not info["query_string"]:
params = []
for key in ['wlanuserip', 'wlanacname', 'wlanacip', 'wlanusermac', 'wlanuserfirsturl',
'ssid', 'nasip', 'snmpagentip', 'mac', 't', 'url']:
m = re.search(rf'(?:id|name)=["\']{key}["\'][^>]*value=["\']([^"\']+)', html)
if m:
params.append(f"{key}={m.group(1)}")
if params:
info["query_string"] = "&".join(params)
# 方法1: 跟随 portal 根路径重定向链,提取 JS 重定向中的 queryString
try:
resp = requests.get(PORTAL_HOST + "/", timeout=10, allow_redirects=True)
parse_page(resp)
except Exception:
pass
# 方法2: 访问 eportal 页面获取 publicKey(同时尝试提取 queryString)
try:
resp = requests.get(PORTAL_BASE_URL + "/", timeout=10, allow_redirects=True)
parse_page(resp, extract_qs=not bool(info["query_string"]))
except Exception:
pass
# 方法3: 对 HTTP 外部 URL 发起请求触发 DNS 劫持,从返回的 HTML 提取 queryString
if not info["query_string"]:
for test_url in ["http://www.baidu.com", "http://detectportal.firefox.com/canonical.html"]:
try:
resp = requests.get(test_url, timeout=5, allow_redirects=False)
if resp.status_code in (301, 302, 303, 307, 308):
loc = resp.headers.get("Location", "")
if "?" in loc:
info["query_string"] = loc.split("?", 1)[1]
m = re.search(r'[?&]mac=([^&]+)', loc)
if m:
info["portal_mac"] = urllib.parse.unquote(m.group(1)).replace(':', '').replace('-', '').lower()
break
elif resp.status_code == 200:
parse_page(resp)
if info["query_string"]:
break
except Exception:
pass
_page_info_cache = info
return info
def get_public_key():
info = _fetch_page_info()
if info["public_key"]:
return info["public_key"]
return PUBLIC_KEY_FALLBACK
def get_query_string():
return _fetch_page_info()["query_string"]
def get_portal_mac():
return _fetch_page_info()["portal_mac"]
def js_rsa_encrypt(data_str, modulus_hex, exponent_hex):
# 模拟 JS RSAUtils.encryptedString:原始RSA,零填充,每2个charCode打包为1个16bit digit
modulus = int(modulus_hex, 16)
exponent = int(exponent_hex, 16)
modulus_bytes = (modulus.bit_length() + 7) // 8
js_digits = (modulus_bytes + 1) // 2
chunk_size = 2 * (js_digits - 1)
a = [ord(ch) for ch in data_str]
while len(a) % chunk_size != 0:
a.append(0)
parts = []
for i in range(0, len(a), chunk_size):
val = 0
j = 0
for k in range(i, i + chunk_size, 2):
val += (a[k] + (a[k + 1] << 8)) << (16 * j)
j += 1
parts.append(hex(pow(val, exponent, modulus))[2:])
return " ".join(parts)
def double_encode(s):
return urllib.parse.quote(urllib.parse.quote(s, safe=''), safe='')
def _ocr_single(img_bytes):
try:
from PIL import Image
import pytesseract
img = Image.open(BytesIO(img_bytes))
gray = img.convert("L")
binary = gray.point(lambda x: 0 if x < 200 else 255)
return pytesseract.image_to_string(
binary,
config="--psm 7 -c tessedit_char_whitelist=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
).strip()
except (ImportError, Exception):
return ""
def _fetch_captcha():
import random
url = f"{PORTAL_BASE_URL}/validcode?rnd={random.random()}"
resp = requests.get(url, timeout=10)
return resp.content, url
def recognize_captcha(first_url):
try:
if not first_url.startswith("http"):
first_url = urllib.parse.urljoin(PORTAL_BASE_URL, first_url)
r = requests.get(first_url, timeout=10)
text = _ocr_single(r.content)
if text:
_print(f"验证码 OCR 识别: {text}")
return text
except Exception as e:
_print(f"验证码下载失败: {e}", "ERROR")
for i in range(2, 4):
_print(f"OCR未识别,刷新验证码重试({i}/3)...")
try:
img_bytes, _ = _fetch_captcha()
text = _ocr_single(img_bytes)
if text:
_print(f"OCR第{i}次: {text}")
return text
except Exception as e:
_print(f"验证码刷新失败: {e}", "ERROR")
_print("OCR 3次均失败,切换为手动输入...", "ERROR")
try:
from PIL import Image
img_bytes, _ = _fetch_captcha()
Image.open(BytesIO(img_bytes)).show()
except Exception:
pass
return input(" 请手动输入验证码: ").strip()
def get_all_macs():
macs = []
mac_raw = set()
def add_mac(m):
if not m:
return
m = m.replace(':', '').replace('-', '').replace('.', '').lower().strip()
if len(m) == 12 and all(c in '0123456789abcdef' for c in m):
if m not in mac_raw and m != '000000000000':
mac_raw.add(m)
macs.append(m)
# ipconfig /all(Windows 最可靠)
try:
output = subprocess.run(
['ipconfig', '/all'],
capture_output=True, text=True, timeout=5
).stdout
for line in output.split('\n'):
if '物理地址' in line or 'Physical' in line:
parts = line.split(':')
if len(parts) >= 2:
add_mac(parts[-1])
except Exception:
pass
# getmac 库
try:
from getmac import get_mac_address
m = get_mac_address()
if m:
add_mac(m)
except Exception:
pass
# uuid(不可靠,放最后)
try:
m = format(uuid.getnode(), '012x')
if m != '000000000000':
add_mac(m)
except Exception:
pass
# PowerShell 备用
if not macs:
try:
output = subprocess.run(
['powershell', '-Command',
'Get-NetAdapter | Select-Object -ExpandProperty MacAddress'],
capture_output=True, text=True, timeout=5
).stdout
for line in output.strip().split('\n'):
add_mac(line)
except Exception:
pass
return macs
def is_authenticated():
test_urls = [
("https://detectportal.firefox.com/canonical.html", "success"),
("https://www.msftconnecttest.com/connecttest.txt", "Microsoft"),
("https://www.baidu.com", None),
]
passed = 0
for url, keyword in test_urls:
try:
r = requests.get(url, allow_redirects=False, timeout=3)
if r.status_code == 200:
if keyword:
if keyword in r.text:
passed += 1
else:
passed += 1
except Exception:
continue
return passed >= 2
def do_login(mac, query_string=""):
exponent_hex, modulus_hex = get_public_key().split('&')
plain = f"{PASSWORD}>{mac}"
encrypted = js_rsa_encrypt(plain[::-1], modulus_hex, exponent_hex)
def build_body(validcode=""):
return "&".join([
f"userId={double_encode(USERNAME)}",
f"password={double_encode(encrypted)}",
f"service={double_encode('')}",
f"queryString={double_encode(query_string)}",
"operatorPwd=",
"operatorUserId=",
f"validcode={double_encode(validcode)}",
f"passwordEncrypt={double_encode('true')}",
])
def parse_result(result):
ok = result.get("result") == "success"
ui = result.get("userIndex", "")
ki = result.get("keepaliveInterval", 0)
return ok, result.get("message", ""), ui, ki
try:
resp = requests.post(
f"{PORTAL_BASE_URL}/InterFace.do?method=login",
data=build_body(),
headers={"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"},
timeout=10,
)
resp.encoding = 'utf-8'
result = resp.json()
if result.get("result") == "fail" and result.get("validCodeUrl"):
captcha_url = result["validCodeUrl"]
if captcha_url and captcha_url.strip():
_print(f"需要验证码 ({captcha_url})")
code = recognize_captcha(captcha_url)
if code:
_print(f"输入验证码: {code},重试...")
resp2 = requests.post(
f"{PORTAL_BASE_URL}/InterFace.do?method=login",
data=build_body(code),
headers={"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"},
timeout=10,
)
resp2.encoding = 'utf-8'
result = resp2.json()
return parse_result(result)
except Exception as e:
return False, str(e), "", 0
def ping_test(host=None, count=3):
if host is None:
host = PING_HOST
try:
cmd = ["ping", "-n", str(count), host] if sys.platform == "win32" else ["ping", "-c", str(count), host]
output = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if output.returncode == 0:
# 提取平均延迟
m = re.search(r'平均\s*=\s*(\d+)\s*ms|avg.*?(\d+\.?\d*).*?ms', output.stdout)
if m:
avg = m.group(1) or m.group(2)
_print(f"Ping {host} 平均延迟: {avg}ms")
else:
_print(f"Ping {host} 成功(延迟未解析)")
else:
_print(f"Ping {host} 失败: {output.stderr.strip()}", "ERROR")
except Exception as e:
_print(f"Ping {host} 异常: {e}", "ERROR")
def register_mac(mac, user_index):
try:
resp = requests.post(
f"{PORTAL_BASE_URL}/InterFace.do?method=registerMac",
data=f"mac={mac}&userIndex={user_index}",
headers={"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"},
timeout=10,
)
resp.encoding = 'utf-8'
result = resp.json()
if result.get("result") == "success":
_print(f"MAC快速认证已注册: {mac}")
else:
_print(f"MAC注册: {result.get('message', result)}", "ERROR")
except Exception as e:
_print(f"MAC注册异常: {e}", "ERROR")
def start_keepalive(user_index, interval_seconds):
interval = max(60, min(600, int(interval_seconds) if interval_seconds else 180))
def _loop():
count = 0
while True:
time.sleep(interval)
count += 1
try:
resp = requests.post(
f"{PORTAL_BASE_URL}/InterFace.do?method=keepalive",
data=f"userIndex={user_index}",
headers={"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"},
timeout=10,
)
resp.encoding = 'utf-8'
result = resp.json()
_print(f"保活 #{count} → {result.get('result', '?')}")
except Exception as e:
_print(f"保活 #{count} 异常: {e}", "ERROR")
t = threading.Thread(target=_loop, daemon=True)
t.start()
_print(f"保活线程已启动(间隔 {interval}s)")
def main():
global PORTAL_HOST, PORTAL_BASE_URL, _page_info_cache
global USERNAME, PASSWORD, MAX_ATTEMPTS, SLEEP_SECONDS
global PING_HOST, PUBLIC_KEY_FALLBACK
cfg = load_config()
if not cfg["username"] or not cfg["password"]:
_print("请编辑 config.json 填入账号密码后重新运行", "ERROR")
return False
PORTAL_HOST = cfg["portal_host"]
PORTAL_BASE_URL = PORTAL_HOST + "/eportal"
USERNAME = cfg["username"]
PASSWORD = cfg["password"]
MAX_ATTEMPTS = int(cfg["max_attempts"])
SLEEP_SECONDS = int(cfg["sleep_seconds"])
PING_HOST = cfg["ping_host"]
PUBLIC_KEY_FALLBACK = cfg["public_key_fallback"]
_print(f"校园网认证登录")
_print(f"目标: {PORTAL_HOST} 用户: {USERNAME}")
print("[INFO] 自动检测 Portal 地址...", end=" ", flush=True)
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
future = executor.submit(detect_portal)
try:
host, base = future.result(timeout=5)
except concurrent.futures.TimeoutError:
host, base = None, None
if host and base:
PORTAL_HOST = host
PORTAL_BASE_URL = base
_page_info_cache = None
print(f"→ {base}")
else:
print("→ 使用默认")
if is_authenticated():
_print("已认证,无需登录!")
return True
saved = load_state()
if saved and saved.get("userIndex"):
try:
resp = requests.post(
f"{PORTAL_BASE_URL}/InterFace.do?method=getOnlineUserInfo",
data=f"userIndex={saved['userIndex']}",
headers={"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"},
timeout=10,
)
resp.encoding = 'utf-8'
info = resp.json()
if info.get("result") == "success":
_print(f"恢复上次会话 userIndex={saved['userIndex']}")
ping_test()
start_keepalive(saved["userIndex"], info.get("keepaliveInterval", 180))
_print("按 Ctrl+C 退出...")
try:
while True:
time.sleep(60)
except KeyboardInterrupt:
_print("退出保活")
return True
else:
_print("上次会话已失效,重新登录...")
except Exception:
pass
all_macs = []
if saved and saved.get("mac"):
all_macs.append(saved["mac"])
_print(f"[上次MAC] {saved['mac']}")
portal_mac = get_portal_mac()
if portal_mac:
all_macs.append(portal_mac)
_print(f"[Portal MAC] {portal_mac}")
local_macs = get_all_macs()
for m in local_macs:
if m not in all_macs:
all_macs.append(m)
_print(f"[扫描] 发现 {len(local_macs)} 个本地网卡MAC:")
for i, m in enumerate(local_macs):
print(f" {i+1}. {m}")
qs = get_query_string()
if qs:
_print(f"[queryString] {qs[:80]}...")
fallbacks = ["111111111111", "000000000000", "ffffffffffff",
"111111111", "123456789abc", "abcdef123456",
"000000000001", "deadbeef0000"]
for m in fallbacks:
if m not in all_macs:
all_macs.append(m)
_print(f"共 {len(all_macs)} 种MAC方案,最多尝试 {MAX_ATTEMPTS} 次\n")
last_msg = ""
mac_index = 0
delay = SLEEP_SECONDS
consecutive_fails = 0
for attempt in range(1, MAX_ATTEMPTS + 1):
mac = all_macs[mac_index % len(all_macs)]
mac_index += 1
if attempt % (len(all_macs) + 1) == 0:
fresh = get_all_macs()
for m in fresh:
if m not in all_macs:
all_macs.append(m)
_print(f"[新增MAC] {m}")
print(f"[{attempt:02d}/{MAX_ATTEMPTS}] MAC={mac:14s}", end=" ")
ok, msg, user_idx, keep_interval = do_login(mac, query_string=qs)
print(f"→ {msg}")
if ok:
_print(f"登录成功!MAC={mac}")
ping_test()
save_state(mac, user_idx)
if user_idx:
_print(f"userIndex: {user_idx}")
register_mac(mac, user_idx)
start_keepalive(user_idx, keep_interval)
_print("按 Ctrl+C 退出保活...")
try:
while True:
time.sleep(60)
except KeyboardInterrupt:
global _skip_wait
_skip_wait = True
_print("退出保活")
return True
consecutive_fails += 1
if msg != last_msg:
delay = SLEEP_SECONDS
consecutive_fails = 0
else:
delay = min(SLEEP_SECONDS * (1.5 ** (consecutive_fails // 3)), 60)
last_msg = msg
if attempt % 5 == 0 and is_authenticated():
_print("检测到已认证重定向!")
return True
time.sleep(delay)
if is_authenticated():
_print("最终检查:已认证")
return True
_print(f"{MAX_ATTEMPTS} 次尝试失败。最后: {last_msg}", "ERROR")
return False
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
pass
except Exception as e:
_print(f"异常退出: {e}", "ERROR")
_wait_exit()