yeg
2 天以前 d8eaa16682cf180e9563d6be05fb43057daf6c07
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
#!/usr/bin/env python3
# 重置后台平台管理员 admin 的口令。
#
# 算法与应用完全一致(见 MerchantPasswordHasher):
#   pbkdf2$210000$<base64 salt(16B)>$<base64 PBKDF2WithHmacSHA256(password, salt, 210000, 256bit)>
#
# 明文只写到 /root/admin-password.txt(600),**不打印到 stdout**。
# 输出只有:长度/字符类别的摘要 + 自检结果 + 端到端验证结果。
#
# 顺带做两件卫生工作:
#   * login_failed_count 清零、locked_until 置 NULL(等价于应用里的 resetStaffPassword)
#   * 吊销所有未过期的 admin_session —— 服务商那次告警的报文里带出过 accessToken,
#     换口令时把在用的会话一并作废,避免旧 token 继续可用;端到端验证自己产生的那个也会被吊销。
#
# 用法:python3 /root/reset-admin-password.py
 
import base64
import hashlib
import json
import os
import secrets
import subprocess
import sys
import urllib.request
 
LENGTH = 20
# 不含单引号/双引号/反斜杠/空格/`$`:既能满足服务商要求的四类组合,
# 又不会破坏 SQL 字符串字面量与 shell 引用。
CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789@%^*+_.-"
SPECIALS = "@%^*+_.-"
OUT_PATH = "/root/admin-password.txt"
LOGIN_URL = "http://127.0.0.1:8080/api/admin/auth/login"
 
 
def generate():
    while True:
        value = "".join(secrets.choice(CHARS) for _ in range(LENGTH))
        if (any(c.isupper() for c in value)
                and any(c.islower() for c in value)
                and any(c.isdigit() for c in value)
                and any(c in SPECIALS for c in value)):
            return value
 
 
def derive(password, salt, iterations):
    return hashlib.pbkdf2_hmac("sha256", password.encode(), salt, iterations, dklen=32)
 
 
def matches(password, encoded):
    parts = encoded.split("$")
    if len(parts) != 4 or parts[0] != "pbkdf2":
        return False
    iterations = int(parts[1])
    salt = base64.b64decode(parts[2])
    expected = base64.b64decode(parts[3])
    return derive(password, salt, iterations) == expected
 
 
def mysql(sql):
    result = subprocess.run(["mysql", "-N", "-B", "-D", "talent_card", "-e", sql],
                            capture_output=True, text=True)
    if result.returncode != 0:
        print("mysql 执行失败: " + result.stderr.strip())
        sys.exit(1)
    return result.stdout.strip()
 
 
def revoke_sessions():
    mysql("update admin_session set revoked_at = now() where revoked_at is null;")
 
 
def login_works(password):
    """拿新口令真调一次登录接口。只回布尔值,绝不打印 token。"""
    request = urllib.request.Request(
        LOGIN_URL,
        data=json.dumps({"account": "admin", "password": password}).encode(),
        headers={"Content-Type": "application/json"},
        method="POST")
    try:
        with urllib.request.urlopen(request, timeout=10) as response:
            payload = json.loads(response.read().decode())
            token = (payload.get("data") or {}).get("accessToken")
            return response.status == 200 and bool(token)
    except Exception as exception:
        print("    登录接口返回异常: " + type(exception).__name__ + " " + str(exception))
        return False
 
 
def main():
    password = generate()
 
    salt = os.urandom(16)
    encoded = ("pbkdf2$210000$"
               + base64.b64encode(salt).decode()
               + "$"
               + base64.b64encode(derive(password, salt, 210000)).decode())
 
    # 口令取自上面的字符集(无引号),所以内联进 SQL 字面量是安全的
    mysql("update admin_operator set password_hash = '" + encoded
          + "', login_failed_count = 0, locked_until = null, updated_at = now()"
          + " where account = 'admin' and deleted = 0;")
    revoke_sessions()
 
    stored = mysql("select password_hash from admin_operator where account = 'admin' and deleted = 0")
    if not stored:
        print("!!! 没读到 admin 的哈希,中止")
        sys.exit(1)
 
    fd = os.open(OUT_PATH, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    with os.fdopen(fd, "w") as handle:
        handle.write("account: admin\npassword: " + password + "\n")
    os.chmod(OUT_PATH, 0o600)
 
    print("已重置 admin 的口令。")
    print("  明文写入 " + OUT_PATH + "(权限 600),未打印到 stdout。")
    print("  长度 " + str(len(password)) + ";含大写/小写/数字/符号四类。")
    print("  失败计数与锁定已清零;未过期的 admin_session 已全部吊销。")
    print("  自检(用库里存的哈希重新算一遍该口令): "
          + ("匹配 ✓" if matches(password, stored) else "不匹配 ✗"))
 
    passed = login_works(password)
    print("  端到端(真调一次登录接口,只看成功与否): " + ("成功 ✓" if passed else "失败 ✗"))
    revoke_sessions()  # 把这次验证产生的会话也作废
 
    remaining = mysql("select count(*) from admin_session where revoked_at is null")
    print("  吊销后仍有效的 admin_session 数: " + remaining)
    return 0 if passed else 1
 
 
if __name__ == "__main__":
    sys.exit(main())