杭工e家登录逆向
城南花已开 Lv6

前言

由于暑期实习期间每天乘坐地铁,实习工资少的可怜,发现杭工e家可以薅波羊毛,但苦于放券减量了,手动抢完全抢不到,遂尝试逆向,自动化薅羊毛,哈哈哈🤣

请求体解密

查壳

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
D:\IDM\Programs>ApkCheckPack_windows_amd64.exe -f 杭工e_3.1.4.apk
APK检测工具 - 扫描配置:
- 文件路径: 杭工e_3.1.4.apk
- 检测类型: ROOT(true) 模拟器(true) 反调试(true) 代理(true) SDK(true) 硬编码(false) 证书(true)
- 最大文件大小: 500 MB
- 递归扫描: true
---------------------------------------------------
正在扫描APK文件: 杭工e_3.1.4.apk

===================== 加固特征扫描结果 =====================

未发现加固特征

===================== 安全检测特征扫描结果 =====================

[ROOT检测特征]
classes.dex -> /data/local/bin/su (SU二进制文件常见路径)
classes.dex -> /data/local/su (SU二进制备用路径)
classes.dex -> /data/local/xbin/su (Xposed框架SU路径)
classes.dex -> /sbin/su (系统分区SU文件)
classes.dex -> /su/bin/su (Systemless SU路径)
classes.dex -> /system/app/Superuser.apk (Superuser安装包)
classes.dex -> /system/bin/failsafe/su (故障安全模式SU)
classes.dex -> /system/bin/su (系统内置SU)
classes.dex -> /system/sd/xbin/su (SD卡扩展SU路径)
classes.dex -> /system/xbin/su (常见SU存放路径)
classes.dex -> Superuser.apk (Superuser安装包(分段检测))
classes.dex -> /system/xbin/ (常见Root工具目录(分段检测))
classes.dex -> /vendor/bin/ (厂商Root工具目录(分段检测))
classes2.dex -> /data/local/bin/su (SU二进制文件常见路径)
classes2.dex -> /data/local/su (SU二进制备用路径)
classes2.dex -> /data/local/xbin/su (Xposed框架SU路径)
classes2.dex -> /sbin/su (系统分区SU文件)
classes2.dex -> /su/bin/su (Systemless SU路径)
classes2.dex -> /system/app/Kinguser.apk (Kingroot安装包)
classes2.dex -> /system/app/Superuser.apk (Superuser安装包)
classes2.dex -> /system/bin/.ext/su (隐藏的SU文件)
classes2.dex -> /system/bin/failsafe/su (故障安全模式SU)
classes2.dex -> /system/bin/su (系统内置SU)
classes2.dex -> /system/sd/xbin/su (SD卡扩展SU路径)
classes2.dex -> /system/usr/we-need-root/su (特殊目录SU文件)
classes2.dex -> /system/xbin/su (常见SU存放路径)
classes2.dex -> Kinguser.apk (Kingroot安装包(分段检测))
classes2.dex -> Superuser.apk (Superuser安装包(分段检测))
classes2.dex -> /system/xbin/ (常见Root工具目录(分段检测))
classes2.dex -> /vendor/bin/ (厂商Root工具目录(分段检测))

[模拟器检测特征]
classes.dex -> test-keys (测试版系统特征)
classes.dex -> goldfish (Android模拟器内核标识)
classes.dex -> 000000000000000 (模拟器默认IMEI)
classes.dex -> /dev/socket/qemud (QEMU守护进程socket)
classes.dex -> /dev/qemu_pipe (QEMU管道通信接口)
classes.dex -> ro.kernel.qemu (QEMU内核属性标识)
classes2.dex -> test-keys (测试版系统特征)
classes2.dex -> goldfish (Android模拟器内核标识)
classes2.dex -> 000000000000000 (模拟器默认IMEI)
classes2.dex -> emulator (模拟器标识)
classes2.dex -> eth0 (模拟器网络接口)

[反调试检测特征]
classes.dex -> /proc/self/status (TracerPid状态检测)
classes2.dex -> ptrace (Ptrace调试检测)
classes2.dex -> /proc/self/status (TracerPid状态检测)

[代理检测特征]
classes.dex -> Ljavax/net/ssl/X509TrustManager; (自定义证书信任管理器)
classes2.dex -> Ljavax/net/ssl/X509TrustManager; (自定义证书信任管理器)
classes3.dex -> Ljavax/net/ssl/X509TrustManager; (自定义证书信任管理器)

===================== 第三方SDK特征扫描结果 =====================

[Alibaba]
岳鹰全景监控 -> lib/arm64-v8a/libcrashsdk.so
岳鹰全景监控 -> lib/armeabi-v7a/libcrashsdk.so

[GNU]
iconv -> lib/arm64-v8a/libiconv.so
iconv -> lib/armeabi-v7a/libiconv.so

[SQLite]
SQLite -> lib/arm64-v8a/libsqlite.so
SQLite -> lib/armeabi-v7a/libsqlite.so

[Tencent]
Bugly -> lib/arm64-v8a/libBugly_Native.so
Bugly -> lib/armeabi-v7a/libBugly_Native.so
腾讯优图 SDK -> lib/arm64-v8a/libYTCommonLiveness.so
腾讯优图 SDK -> lib/armeabi-v7a/libYTCommonLiveness.so

[Umeng]
移动统计分析 -> lib/arm64-v8a/libumeng-spy.so
移动统计分析 -> lib/armeabi-v7a/libumeng-spy.so

[devilsen]
CZXing -> lib/arm64-v8a/libczxing.so
CZXing -> lib/armeabi-v7a/libczxing.so

[koral--]
android-gif-drawable -> lib/arm64-v8a/libpl_droidsonroids_gif.so
android-gif-drawable -> lib/armeabi-v7a/libpl_droidsonroids_gif.so

[spadix]
ZBar -> lib/arm64-v8a/libzbarjni.so
ZBar -> lib/armeabi-v7a/libzbarjni.so

[环信]
环信 IM -> lib/arm64-v8a/libhyphenate.so
环信 IM -> lib/arm64-v8a/libhyphenate_av.so
环信 IM -> lib/armeabi-v7a/libhyphenate.so
环信 IM -> lib/armeabi-v7a/libhyphenate_av.so

===================== 证书扫描结果 =====================

[证书文件: META-INF/CERT.RSA]
解析证书失败: x509: malformed tbs certificate

积分明细

  • 请求包

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    POST /unionApp/interf/front/U/U044 HTTP/2
    Host: app.hzgh.org.cn
    Content-Type: text/plain;charset=utf-8
    Content-Length: 654
    Connection: Keep-Alive
    Accept-Encoding: gzip, deflate, br
    Cookie: JSESSIONID=226A6497327FB39CE27D4E214D50526D
    User-Agent: okhttp/3.4.2

    {
    "channel":"02",
    "timestamp":"1754281415163",
    "app_ver_no":"3.1.4",
    "dec_key":"gS3JdOc+0KEqXmIkDk8L9SN5BLIaDN43OFGPiCwkz3READ5fPcLTzyTenVOmm64AONPefGgA\/ZPA4yC6Jfzm6DHnzEMrQ\/6a+CZubVXEo3aLOEQ+mVClebyIWbNJ\/GmaXu+wjj4iQMEHdOFF9m5BRhkRVgbR7g4LwU6+ayb3fYM=",
    "login_name":"y4UcBzOLq2Txj1gvjRga3qEDKo+WzCI9R8ep4PFNinA=",
    "page_num":"1",
    "ses_id":"df006b2bc4a84c149ce3814696161c14",
    "page_size":"20",
    "key":"channel,timestamp,app_ver_no,dec_key,login_name,page_num,ses_id,page_size",
    "sign":"AjZY+Q8aGfZDAnPiRUUqAb+z08Df5+Rd5OMftslW8LuCmGGJUbW0BgAkKKqpIl4HMlMccZeGWlMK0lZg+Wu5vSwba6l\/BWazLctzOl5eIcdKOW4zmvQqTlmHB8l9vkATGq1zFO5nilWlwziICN2Xmra8O48AMikqPvx6e9N+P20="
    }
  • 加密参数

    1
    2
    3
    "dec_key":"gS3JdOc+0KEqXmIkDk8L9SN5BLIaDN43OFGPiCwkz3READ5fPcLTzyTenVOmm64AONPefGgA\/ZPA4yC6Jfzm6DHnzEMrQ\/6a+CZubVXEo3aLOEQ+mVClebyIWbNJ\/GmaXu+wjj4iQMEHdOFF9m5BRhkRVgbR7g4LwU6+ayb3fYM=",
    "login_name":"y4UcBzOLq2Txj1gvjRga3qEDKo+WzCI9R8ep4PFNinA=",
    "sign":"AjZY+Q8aGfZDAnPiRUUqAb+z08Df5+Rd5OMftslW8LuCmGGJUbW0BgAkKKqpIl4HMlMccZeGWlMK0lZg+Wu5vSwba6l\/BWazLctzOl5eIcdKOW4zmvQqTlmHB8l9vkATGq1zFO5nilWlwziICN2Xmra8O48AMikqPvx6e9N+P20="

使用jadx-gui进行逆行apk文件

搜索相关关键字,得到加密逻辑

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

public a c(Map<String, String> map) throws JSONException {
String strC = k.c(24);
JSONObject jSONObject = new JSONObject();
try {
jSONObject.put("channel", "02");
jSONObject.put("app_ver_no", t7.b.t());
jSONObject.put("timestamp", String.valueOf(System.currentTimeMillis()));
if (this.f5735c.startsWith(com.zjte.hanggongefamily.base.a.f27579a)) {
jSONObject.put("dec_key", lf.c0.c(strC));
} else {
jSONObject.put("dec_key", lf.c0.a(strC));
}
if (map != null) {
Set<String> setKeySet = map.keySet();
for (String str : setKeySet) {
jSONObject.put(str, map.get(str));
}
for (String str2 : setKeySet) {
int i10 = 0;
while (true) {
String[] strArr = this.f5734b;
if (i10 < strArr.length) {
if (str2.equals(strArr[i10])) {
jSONObject.put(str2, k.b(map.get(str2).getBytes(), strC.getBytes()));
}
i10++;
}
}
}
}
JSONObject jSONObjectU = this.f5735c.startsWith(com.zjte.hanggongefamily.base.a.f27579a) ? bf.d.u(jSONObject, com.zjte.hanggongefamily.base.a.f27603m) : bf.d.t(jSONObject.toString(), com.zjte.hanggongefamily.base.a.f27605n);
String string = jSONObjectU.getString("key");
String string2 = jSONObjectU.getString("sign");
jSONObject.put("key", string);
jSONObject.put("sign", string2);
f(jSONObject.toString());
} catch (Exception e10) {
e10.printStackTrace();
}
return this;
}

其中channelapp_ver_no基本上是硬编码的

  • channel:02

  • app_ver_no:3.1.4

  • timestamp:当前系统时间戳

  • dec_key的逻辑时通过k.c(24)来随机生成24字符全大写的字符串作为strC

    再判断是否访问https://zhgh.hzgh.org/来选择采用lf.c0.c加密还是lf.c0.a加密,两者加密只有公钥的不同

    • k.c函数实现
1
2
3
4
5
6
7
8
9
10
11
12
13
 public static final String f38860a = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
---------------------
public static String c(int i10) {
StringBuffer stringBuffer = new StringBuffer();
Random random = new Random();
for (int i11 = 0; i11 < i10; i11++) {
stringBuffer.append(f38860a.charAt(random.nextInt(62)));
}
return stringBuffer.toString().toUpperCase();
}
----------------------
public static final String f27579a = "https://zhgh.hzgh.org/";

  • lf.c0类中的相关加密逻辑

    采用RSA/ECB/PKCS1Padding算法

    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
    public class c0 {

    /* renamed from: a, reason: collision with root package name */
    public static final String f38811a = "RSA";

    /* renamed from: b, reason: collision with root package name */
    public static final String f38812b = "RSA/ECB/PKCS1Padding";

    /* renamed from: c, reason: collision with root package name */
    public static final String f38813c = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVXsxrrMcxNwFNYt0wMTdqc5WMa4gr7nMbWbcQCpJ2XNBMTQetknYNzCr8MMRdHBKFKjdCJE40u6UDBXQx13z7OSKyvQBwtLj5n8eIQXRtpMIjvqfR1xRuNBi5147ZXJDbKxWGRm0kjLN5UuqnDe6zu8v6MKU7KNDzHUrWqsj2LwIDAQAB";

    /* renamed from: d, reason: collision with root package name */
    public static final String f38814d = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC7yWoQaojBBqKI2H0j4e8ZeX/n1yip6hxrxSVth5F5n1JJ/B3liPMdz6K1chNLFTAcbI7hTL9KkphP9yQ+bPYD68Ajrt/DFrW679Zi1CoeetHVrM4sF68lYarGXwnSlKloaPWnI4Ch9cSqIvIOInlpeJqYPlJ8ZJvGCmbQoM6bewIDAQAB";

    public static String a(String str) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IOException {
    byte[] bArr;
    try {
    Cipher cipher = Cipher.getInstance(f38812b);
    cipher.init(1, e(f38814d));
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(str.getBytes());
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    byte[] bArr2 = new byte[117];
    while (true) {
    int i10 = byteArrayInputStream.read(bArr2);
    if (i10 == -1) {
    return c.b(byteArrayOutputStream.toByteArray());
    }
    if (117 == i10) {
    bArr = bArr2;
    } else {
    bArr = new byte[i10];
    for (int i11 = 0; i11 < i10; i11++) {
    bArr[i11] = bArr2[i11];
    }
    }
    byteArrayOutputStream.write(cipher.doFinal(bArr));
    }
    } catch (InvalidKeyException e10) {
    e10.printStackTrace();
    return null;
    } catch (BadPaddingException e11) {
    e11.printStackTrace();
    return null;
    } catch (IllegalBlockSizeException e12) {
    e12.printStackTrace();
    return null;
    } catch (Exception e13) {
    e13.printStackTrace();
    return null;
    }
    }

    public static String c(String str) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IOException {
    byte[] bArr;
    try {
    Cipher cipher = Cipher.getInstance(f38812b);
    cipher.init(1, e(f38813c));
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(str.getBytes());
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    byte[] bArr2 = new byte[117];
    while (true) {
    int i10 = byteArrayInputStream.read(bArr2);
    if (i10 == -1) {
    return c.b(byteArrayOutputStream.toByteArray());
    }
    if (117 == i10) {
    bArr = bArr2;
    } else {
    bArr = new byte[i10];
    for (int i11 = 0; i11 < i10; i11++) {
    bArr[i11] = bArr2[i11];
    }
    }
    byteArrayOutputStream.write(cipher.doFinal(bArr));
    }
    } catch (InvalidKeyException e10) {
    e10.printStackTrace();
    return null;
    } catch (BadPaddingException e11) {
    e11.printStackTrace();
    return null;
    } catch (IllegalBlockSizeException e12) {
    e12.printStackTrace();
    return null;
    } catch (Exception e13) {
    e13.printStackTrace();
    return null;
    }
    }

    public static String b(String str) throws Exception {
    byte[] bArrA = c.a(str);
    Cipher cipher = Cipher.getInstance(f38812b);
    cipher.init(2, d(com.zjte.hanggongefamily.base.a.f27609p));
    return new String(cipher.doFinal(bArrA));
    }
    --------------------------------
    public static String f27609p = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAIOBMtf2AIYQlrNy/lVPHx4R/LKI+Vtk3bKmzID8vdVnh/4WA3lczqfejM10Xfy3sNe4l5EeQTvnDgUHbIFK8FyJRpvypAmS9oyW6uwGTjZEu5Y6hsSxiGAOG5ZOlH8vOSfuaAkZ+iUlqifPE3ZOmHkqGzmukg4wCRaPLx5ioq8zAgMBAAECgYAgLOVmx677HmXxBCrMbq57agU9HZx9SyGfS4Zv7Ob5pvo0Jei1sgpyMlabEmTIp50iOu0CubdWU8MvYdCfldlXQLW7cjk8N1NyGQLFd2fJ03a7gGWnwwEdPoNTpSHnB+mDL9l7MVjion5fLojzq9Pz1gMKL01I2TfZBDL4m6EbgQJBAMfgrMKtj7f40GA3qp/y/9/eBCAu8PbtFmtATLMQRf4tGhjvn349x1b6FZj8RiaRBSrq0Owjrdo5TUxgfS7dz3MCQQCobdWk2SQhRlqEHfFEro/8ab6gn3GhBDzzKvNjhKr2MO6JWqs+Vr+/P9uYpA+G+rv74uVIGWhjuNtI5+/69DFBAkAJOQS/tuJ6yrBSwD7PQpcr7UKjeYcE3cu7ByyC1q1kHRCnNedWG+Omz8NPW9Sg0vA6GrupKbxL5Xj7nTgpgXKhAkBIVlvioAvfaqrngUClAd//RZ9EtxYDVKGkwnaj8E/Iyr04KsPPU0ypJBD5XsT4cOmZxho5PAhUhAlSJ6MvAf/BAkA64ieVhtQA1KV0pSSEJMnbPlZe+yBYGTWLMaG2zL0kKEhIs2fIHbVhLFQ8TkO5oH+mhxuuXI5+nVU2G0dqUl6D";

    回到原始加密逻辑

    遍历 map 中的键,如果键名在 f5734b 列表中,则将对应的值使用 k.b(value.getBytes(), strC.getBytes()) 进行 DESede/ECB/PKCS5Padding 加密,密钥为 strC,然后 Base64 编码

    • f5734b列表
1
public String[] f5734b = {"login_name", "login_auth_code", "auth_code", "pwd", "password", "newpwd", "amt", "tr_amt", "sms_code", "total_amount", "account_no", "mob_data", "order_amt", "before_amt", "txn_amt", "tel", "mobile", "new_mobile", "code", "cert_no", "card_no", "reserve_mobile", "reply_tel", "card_bal", "bank_card_no", "car_no", SocializeConstants.TENCENT_UID, "invite_code", "auth_code", "imgAuthCode", "imgUniCode"};
  • k.b方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 public static final String f38864e = "DESede";

public static String a(String str, String str2) throws Exception {
SecretKey secretKeyGenerateSecret = SecretKeyFactory.getInstance(f38864e).generateSecret(new DESedeKeySpec(("HTt0Hzsu" + str2).getBytes()));
Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS7Padding");
cipher.init(2, secretKeyGenerateSecret, new IvParameterSpec(str2.substring(0, 8).getBytes()));
return new String(cipher.doFinal(Base64.decode(str, 0)), "utf-8");
}

public static String b(byte[] bArr, byte[] bArr2) throws Exception {
SecretKeySpec secretKeySpec = new SecretKeySpec(bArr2, f38864e);
Cipher cipher = Cipher.getInstance(f38864e);
cipher.init(1, secretKeySpec);
return Base64.encodeToString(cipher.doFinal(bArr), 2);
}

后面将所有原始的(非加密的)map 字段、RSA 加密后的 dec_key、以及 DESede 加密后的敏感字段放入一个 JSONObject

1
2
3
4
5
6
7
8
9
10
 public static String f27603m = "qwerqaz.-*";
public static String f27605n = "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAJ+C8Z9awsGU8DeBpq47p+pVBgIxWr9epYE5lTrVwoTvOv7dOBTsNgYPgDqFLbU8eZsV26DOvgd4TC5tZUWF7WbAleOcxvwA143XTBpZEeDx6who8KiW1WBKUwkeEfXZvOWhN2d+8GlCjvJu2J4yNGEXScQEIWb+ofE4Pd4yPkkzAgMBAAECgYB0Tzu18a0vEFX0c1JBm3g98w81jB1aiz3tMzqwMuvqmLIQ4uegwfhGhQkAItoIW/dj8RU7dWS096+87sG4ZwaKCv/SmT1CibqmSATrX6YNIFU4uXsZzMREJxmZi+V5AllT9DWBG5YjKgrGfWjL0Rq10ZvxYMTdjO+SbqDIjVoc+QJBAOrMXRO6G349NpLvo1QPevxIykKNKhr5Qkjv4oVydoVoHW6iMU30PhrBqBYla+K8W+xyeqrjd9ucDQFW/Z2+hD8CQQCt6jz4o7qadQM0gikoBsgWwp7teyZI/8ZH5htrKZwDJzUe6LuM9xjDeXAqqjNjQrDL7M+6T7ZwMmK3UN3boe4NAkEA6ioGabYh1TSXSNNVwG/v58twbA78/wm34aXb89rD+Shssflv0p7TkTuxtuR7RBU2WAmT7PoOfyaSkdN/++IVYQJBAJ/klCvQc/YfkFPNO0N2gK0UP4N8zmUc6tIdh6XNeocXm+oP9KaUYusMkghXtKkUnnDOBul28fdTC5kYOvD7fl0CQQDLIYfo8MSMgcFkBH1wRUbhjVv31bk8+4G9a+h7UkLdLtch5qPsS7bsFCyszqEYjhYtQ278Q20lSzaIsom0Q3ai";
--------------------

JSONObject jSONObjectU = this.f5735c.startsWith(com.zjte.hanggongefamily.base.a.f27579a) ? bf.d.u(jSONObject, com.zjte.hanggongefamily.base.a.f27603m) : bf.d.t(jSONObject.toString(), com.zjte.hanggongefamily.base.a.f27605n);
String string = jSONObjectU.getString("key");
String string2 = jSONObjectU.getString("sign");
jSONObject.put("key", string);
jSONObject.put("sign", string2);

对于key和sign的值会从jSONObjectU提取出来

至于jSONObjectU中的keysign的生成是判断url是否以https://zhgh.hzgh.org/开头的

如果是则执行bf.d.u方法,如果不是则执行bf.d.t

  • bf.d类的相关逻辑

    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
    public static JSONObject u(JSONObject jSONObject, String str) throws JSONException {
    JSONObject jSONObject2 = new JSONObject();
    Iterator<String> itKeys = jSONObject.keys();
    String strSubstring = "";
    String str2 = "";
    while (itKeys.hasNext()) {
    String string = itKeys.next().toString();
    if (!(jSONObject.get(string) instanceof JSONArray)) {
    if (itKeys.hasNext()) {
    strSubstring = strSubstring + string + ",";
    } else {
    strSubstring = strSubstring + string;
    }
    str2 = str2 + jSONObject.get(string).toString();
    } else if (!itKeys.hasNext() && strSubstring.endsWith(",")) {
    strSubstring = strSubstring.substring(0, strSubstring.length() - 1);
    }
    }
    jSONObject2.put("key", strSubstring);
    try {
    jSONObject2.put("sign", b(o(str2 + str).toUpperCase()).toUpperCase());
    } catch (Exception e10) {
    e10.printStackTrace();
    }
    return jSONObject2;
    }
    ------------------------------
    public static JSONObject t(String str, String str2) throws JSONException {
    JSONObject jSONObject = new JSONObject(str);
    JSONObject jSONObject2 = new JSONObject();
    Iterator<String> itKeys = jSONObject.keys();
    String strSubstring = "";
    String str3 = "";
    while (itKeys.hasNext()) {
    String string = itKeys.next().toString();
    if (!TextUtils.equals("content", string) && !TextUtils.equals("link_url", string) && !TextUtils.equals("url", string) && !TextUtils.equals("pic_cont", string) && !TextUtils.equals("advice_img1", string) && !TextUtils.equals("advice_img2", string) && !TextUtils.equals("advice_img3", string) && !TextUtils.equals("photo_one", string) && !TextUtils.equals("photo_two", string) && !TextUtils.equals("photo_three", string) && !TextUtils.equals("book_img", string) && !TextUtils.equals("pimge", string)) {
    if (!(jSONObject.get(string) instanceof JSONArray)) {
    if (itKeys.hasNext()) {
    strSubstring = strSubstring + string + ",";
    } else {
    strSubstring = strSubstring + string;
    }
    str3 = str3 + jSONObject.get(string).toString();
    } else if (!itKeys.hasNext() && strSubstring.endsWith(",")) {
    strSubstring = strSubstring.substring(0, strSubstring.length() - 1);
    }
    }
    }
    if (strSubstring.endsWith(",")) {
    try {
    strSubstring = strSubstring.substring(0, strSubstring.length() - 1);
    } catch (Exception unused) {
    }
    }
    jSONObject2.put("key", strSubstring);
    try {
    jSONObject2.put("sign", u.e(str3 + "zSw3MLRV7VuwT!*G", str2));
    } catch (Exception e10) {
    e10.printStackTrace();
    }
    return jSONObject2;
    }

    其实key也相当于是硬编码了,它是参与签名的所有字段名称

    "key":"channel,timestamp,app_ver_no,dec_key,login_name,page_num,ses_id,page_size"

如果走的是bf.d.u方法的话,那么sign的生成即b(o(str2 + str)

str2jSONObject 中所有非 JSONArray 类型的键的值的直接拼接

str则是qwerqaz.-*,先进行o方法再b方法,最后全部转换为大写

bf.d.o()一眼MD5加密然后小写的十六进制字符串

bf.d.b()是一个 SHA-1 哈希函数转换为小写的十六进制字符串

即最后的生成逻辑如下

sign = SHA1(MD5(str2 + "qwerqaz.-*").toUpperCase()).toUpperCase()

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

public static String o(String str) throws Exception {
byte[] bArrDigest = MessageDigest.getInstance("MD5").digest(str.getBytes("UTF-8"));
StringBuilder sb2 = new StringBuilder(bArrDigest.length * 2);
for (byte b10 : bArrDigest) {
int i10 = b10 & 255;
if (i10 < 16) {
sb2.append("0");
}
sb2.append(Integer.toHexString(i10));
}
return sb2.toString();
}
-----------
public static final String f38931c = "SHA256WithRSA";


public static String b(String str) throws NoSuchAlgorithmException {
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA");
messageDigest.update(str.getBytes());
byte[] bArrDigest = messageDigest.digest();
StringBuffer stringBuffer = new StringBuffer();
for (byte b10 : bArrDigest) {
String hexString = Integer.toHexString(b10 & 255);
if (hexString.length() < 2) {
stringBuffer.append(0);
}
stringBuffer.append(hexString);
}
return stringBuffer.toString();
} catch (Exception e10) {
e10.printStackTrace();
return "";
}
}

另外走bf.d.t的话,sign=u.e(str3 + "zSw3MLRV7VuwT!*G", str2))

str2即是f27605n里面的私钥内容

str3jSONObject 中所有参与签名字段的值的直接拼接

u.e方法是RSA 签名函数,签名算法为SHA256WithRSA

至此加密流程如下

  1. 生成会话密钥 (strC):

    • 客户端通过 k.c(24) 生成一个 24 字符的全大写随机字符串。
    • 字符集:"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    • 用作 DESede (Triple DES) 对称加密的密钥。
  2. 加密会话密钥 (dec_key):

    • 根据请求基础 URL (this.f5735c.startsWith("https://zhgh.hzgh.org/")) 判断。
      • 如果匹配: lf.c0.c(strC),使用硬编码的 RSA 公钥 f38813c 进行 RSA/ECB/PKCS1Padding 加密。
      • 如果不匹配: lf.c0.a(strC),使用硬编码的 RSA 公钥 f38814d 进行 RSA/ECB/PKCS1Padding 加密。
    • 加密结果再通过 c.b() 进行 Base64 编码。
  3. 敏感数据加密:

    • 请求 map 中包含在 f5734b 列表(如 login_name, password, sms_code, bank_card_no 等)的字段值,会使用 strC 作为密钥进行 DESede/ECB/PKCS5Padding 对称加密。
    • 加密结果再通过 Base64.encodeToString(..., 2) 进行 Base64 编码。
  4. 请求 JSON 构建:

    • channel, app_ver_no, timestamp, dec_key (RSA加密后的 strC),以及其他原始 map 字段和 DESede 加密后的敏感字段,组装成一个 JSONObject
  5. 签名生成 (keysign):

    • 条件分支:

      同样根据this.f5735c.startsWith("https://zhgh.hzgh.org/")判断。

      • 如果匹配 (使用 bf.d.u):
        • key 字段:逗号分隔的参与签名的所有键名(排除 JSONArray)。
        • 签名原始数据:所有参与签名键的值的拼接 (str2) + 硬编码字符串 f27603m ("qwerqaz.-*")。
        • sign 字段:SHA1(MD5((str2 + "qwerqaz.-*").toUpperCase()).toUpperCase()),结果再 toUpperCase()这是一个双重哈希,不是私钥签名。
      • 如果不匹配 (使用 bf.d.t):
        • key 字段:逗号分隔的参与签名的所有键名(排除特定图片/内容字段和 JSONArray)。
        • 签名原始数据:所有参与签名键的值的拼接 (str3) + 硬编码字符串 "zSw3MLRV7VuwT!*G"
        • sign 字段:使用 RSA 私钥 (f27605n,PKCS#8 格式的 Base64 字符串) 对签名原始数据进行 SHA256withRSA 签名,然后通过 c.b() 进行 Base64 编码。

返回体解密

解密逻辑

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
public void a(dj.e eVar, d0 d0Var) throws JSONException, IOException {
y5.f fVarA = bf.a.b().a();
try {
if (d0Var.R() > 599 || d0Var.R() < 400) {
String strString = d0Var.N().string();
if (strString == null || strString.isEmpty()) {
bf.c cVar = this.f5177a;
Type type = cVar.f5195a;
if (type != String.class) {
b.this.k(cVar, fVarA.m(strString, type));
} else {
b.this.k(cVar, strString);
}
} else {
try {
String string = new JSONObject(strString).getString("data2");
String strA = k.a(string.substring(172), c0.b(string.substring(0, 172)));
bf.c cVar2 = this.f5177a;
Type type2 = cVar2.f5195a;
if (type2 != String.class) {
b.this.k(cVar2, fVarA.m(strA, type2));
} else {
b.this.k(cVar2, strA);
}
} catch (Exception unused) {
bf.c cVar3 = this.f5177a;
Type type3 = cVar3.f5195a;
if (type3 != String.class) {
b.this.k(cVar3, fVarA.m(strString, type3));
} else {
b.this.k(cVar3, strString);
}
}
}
} else {
b.this.i(this.f5177a, d0Var.N().string());
}
} catch (Exception e10) {
b.this.i(this.f5177a, e10.getMessage());
}
b.this.g(this.f5177a, this.f5178b);
}
  1. 从 JSON 对象中提取 data2 字段的值,得到 string (即 data2 的 Base64 字符串)。
  2. data2 字符串分割成两部分:
    • 密文部分: string.substring(172) (从索引 172 到字符串末尾)。
    • 密钥/IV 材料部分: string.substring(0, 172) (从索引 0 到索引 171)。

其实也和加密流程大差不差,但少了很多步骤,在密钥部分即data2字符串中截取前172个字符,使用c0.b方法作为RSA的密文

f27609p私钥文件进行RSA 解密(PKCS1_v1_5 填充),得到str2

image

1
2
3
4
5
6
7
8
9
public static final String f38812b = "RSA/ECB/PKCS1Padding";
public static String f27609p = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAIOBMtf2AIYQlrNy/lVPHx4R/LKI+Vtk3bKmzID8vdVnh/4WA3lczqfejM10Xfy3sNe4l5EeQTvnDgUHbIFK8FyJRpvypAmS9oyW6uwGTjZEu5Y6hsSxiGAOG5ZOlH8vOSfuaAkZ+iUlqifPE3ZOmHkqGzmukg4wCRaPLx5ioq8zAgMBAAECgYAgLOVmx677HmXxBCrMbq57agU9HZx9SyGfS4Zv7Ob5pvo0Jei1sgpyMlabEmTIp50iOu0CubdWU8MvYdCfldlXQLW7cjk8N1NyGQLFd2fJ03a7gGWnwwEdPoNTpSHnB+mDL9l7MVjion5fLojzq9Pz1gMKL01I2TfZBDL4m6EbgQJBAMfgrMKtj7f40GA3qp/y/9/eBCAu8PbtFmtATLMQRf4tGhjvn349x1b6FZj8RiaRBSrq0Owjrdo5TUxgfS7dz3MCQQCobdWk2SQhRlqEHfFEro/8ab6gn3GhBDzzKvNjhKr2MO6JWqs+Vr+/P9uYpA+G+rv74uVIGWhjuNtI5+/69DFBAkAJOQS/tuJ6yrBSwD7PQpcr7UKjeYcE3cu7ByyC1q1kHRCnNedWG+Omz8NPW9Sg0vA6GrupKbxL5Xj7nTgpgXKhAkBIVlvioAvfaqrngUClAd//RZ9EtxYDVKGkwnaj8E/Iyr04KsPPU0ypJBD5XsT4cOmZxho5PAhUhAlSJ6MvAf/BAkA64ieVhtQA1KV0pSSEJMnbPlZe+yBYGTWLMaG2zL0kKEhIs2fIHbVhLFQ8TkO5oH+mhxuuXI5+nVU2G0dqUl6D";

public static String b(String str) throws Exception {
byte[] bArrA = c.a(str);
Cipher cipher = Cipher.getInstance(f38812b);
cipher.init(2, d(com.zjte.hanggongefamily.base.a.f27609p));
return new String(cipher.doFinal(bArrA));
}

使用str2来生成DESede 密钥和 IV,然后解密 data2 的剩余部分str,得到最终的明文

1
2
3
4
5
6
7
 public static final String f38864e = "DESede";
public static String a(String str, String str2) throws Exception {
SecretKey secretKeyGenerateSecret = SecretKeyFactory.getInstance(f38864e).generateSecret(new DESedeKeySpec(("HTt0Hzsu" + str2).getBytes()));
Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS7Padding");
cipher.init(2, secretKeyGenerateSecret, new IvParameterSpec(str2.substring(0, 8).getBytes()));
return new String(cipher.doFinal(Base64.decode(str, 0)), "utf-8");
}

根据HTt0Hzsu+str2作为DESede的密钥,拼接后取24字节

再将str2取前8字节来生成IV,最后对 Base64 解码后的字节串进行 DESede 解密(CBC 模式,PKCS5Padding/PKCS7Padding)

image

拷打Gemini给出解密脚本,可以再优化一下,这样更鲁棒🤣

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
import base64
from Crypto.Cipher import DES3, PKCS1_v1_5
from Crypto.PublicKey import RSA
from Crypto.Util.Padding import unpad

# 1. RSA 私钥 (从 Java 代码中的 f27609p 复制)
# 注意:Java 代码中的 f27609p 是一个 Base64 编码的 PKCS#8 私钥,PEM 格式
# Python RSA 库通常可以直接加载这种格式。
# 由于它是 PKCS#1Padding,我们应该使用 PKCS1_v1_5。
RSA_PRIVATE_KEY_B64 = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAIOBMtf2AIYQlrNy/lVPHx4R/LKI+Vtk3bKmzID8vdVnh/4WA3lczqfejM10Xfy3sNe4l5EeQTvnDgUHbIFK8FyJRpvypAmS9oyW6uwGTjZEu5Y6hsSxiGAOG5ZOlH8vOSfuaAkZ+iUlqifPE3ZOmHkqGzmukg4wCRaPLx5ioq8zAgMBAAECgYAgLOVmx677HmXxBCrMbq57agU9HZx9SyGfS4Zv7Ob5pvo0Jei1sgpyMlabEmTIp50iOu0CubdWU8MvYdCfldlXQLW7cjk8N1NyGQLFd2fJ03a7gGWnwwEdPoNTpSHnB+mDL9l7MVjion5fLojzq9Pz1gMKL01I2TfZBDL4m6EbgQJBAMfgrMKtj7f40GA3qp/y/9/eBCAu8PbtFmtATLMQRf4tGhjvn349x1b6FZj8RiaRBSrq0Owjrdo5TUxgfS7dz3MCQQCobdWk2SQhRlqEHfFEro/8ab6gn3GhBDzzKvNjhKr2MO6JWqs+Vr+/P9uYpA+G+rv74uVIGWhjuNtI5+/69DFBAkAJOQS/tuJ6yrBSwD7PQpcr7UKjeYcE3cu7ByyC1q1kHRCnNedWG+Omz8NPW9Sg0vA6GrupKbxL5Xj7nTgpgXKhAkBIVlvioAvfaqrngUClAd//RZ9EtxYDVKGkwnaj8E/Iyr04KsPPU0ypJBD5XsT4cOmZxho5PAhUhAlSJ6MvAf/BAkA64ieVhtQA1KV0pSSEJMnbPlZe+yBYGTWLMaG2zL0kKEhIs2fIHbVhLFQ8TkO5oH+mhxuuXI5+nVU2G0dqUl6D"

# 固定的 DESede 密钥前缀
DESEDE_KEY_PREFIX = "HTt0Hzsu".encode('utf-8')

data2 = input("请输入待解密的 data2 字符串: ")
if not data2:
print("错误:未输入data2字符串,解密终止。")
exit() # 或者 raise ValueError("data2 is empty")

def decrypt_data2(data2_full_base64: str) -> str:
"""
解密响应包中的 data2 字段。

Args:
data2_full_base64: 响应包中完整的 data2 字段值 (Base64 编码)。

Returns:
解密后的明文字符串。
"""
if len(data2_full_base64) < 172:
raise ValueError("data2 字符串长度不足 172,无法提取密钥材料。")

# --- 第一步:RSA 解密密钥材料 ---
# 1. 提取 data2 的前 172 个字符作为 RSA 加密数据 (Base64 编码)
rsa_encrypted_material_b64 = data2_full_base64[:172]

# 2. Base64 解码得到原始 RSA 密文
rsa_encrypted_bytes = base64.b64decode(rsa_encrypted_material_b64)
print(f"RSA 加密密钥材料 (Base64解码后 HEX): {rsa_encrypted_bytes.hex()} (长度: {len(rsa_encrypted_bytes)} 字节)")

# 3. 加载 RSA 私钥 (PKCS#8 PEM 格式)
# pycryptodome 要求 PEM 格式。我们将 Base64 编码的裸私钥字符串转换为 PEM 格式。
# Java f27609p 看起来是一个裸的 Base64 编码的私钥,通常是 PKCS#8 格式。
# 因此我们需要手动添加 PEM 头尾。
rsa_private_key_pem = (
"-----BEGIN PRIVATE KEY-----\n"
+ RSA_PRIVATE_KEY_B64 + "\n"
+ "-----END PRIVATE KEY-----"
)
private_key = RSA.import_key(rsa_private_key_pem)

# 4. RSA 解密 (使用 PKCS1_v1_5 填充)
# Java 代码是 PKCS1Padding,对应 Python 的 PKCS1_v1_5
rsa_cipher = PKCS1_v1_5.new(
private_key) # OAEP is generally preferred, but Java uses PKCS1Padding, which is PKCS1_v1_5
try:
# For PKCS1Padding, use PKCS1_v1_5.new()
rsa_cipher = PKCS1_v1_5.new(private_key)
# The result is the DESede key material string
rsa_decrypted_bytes = rsa_cipher.decrypt(rsa_encrypted_bytes,
None) # None for salt in case of OAEP, but v1_5 does not use it.
rsa_decrypted_str = rsa_decrypted_bytes.decode('utf-8')
print(f"RSA 解密后的字符串: {rsa_decrypted_str} (长度: {len(rsa_decrypted_str)} 字符)")
except ValueError as e:
print(f"RSA 解密失败,可能填充错误或密文无效: {e}")
raise

# --- 第二步:DESede 解密实际数据 ---
# 1. 提取 data2 的后半部分作为 DESede 密文 (Base64 编码)
desede_encrypted_data_b64 = data2_full_base64[172:]

# 2. 生成 DESede 密钥 (K1K2K3 模式,取前 24 字节)
# Java: ("HTt0Hzsu" + str2).getBytes()
# 这里的 str2 就是 rsa_decrypted_str
full_desede_key_string = DESEDE_KEY_PREFIX + rsa_decrypted_str.encode('utf-8')
desede_key = full_desede_key_string[:24] # Java DESedeKeySpec takes first 24 bytes if input is longer

if len(desede_key) != 24:
raise ValueError(f"生成的 DESede 密钥长度 {len(desede_key)} 不足 24 字节,无法用于三重 DES。")
print(f"DESede 密钥 (HEX): {desede_key.hex()} (长度: {len(desede_key)} 字节)")

# 3. 生成 IV (Initialization Vector)
# Java: str2.substring(0, 8).getBytes()
# 这里的 str2 还是 rsa_decrypted_str
iv_str = rsa_decrypted_str[:8]
iv = iv_str.encode('utf-8')

if len(iv) != 8:
raise ValueError(f"生成的 IV 长度 {len(iv)} 不符合 8 字节要求。")
print(f"IV (HEX): {iv.hex()} (长度: {len(iv)} 字节)")

# 4. Base64 解码 DESede 密文
desede_encrypted_bytes = base64.b64decode(desede_encrypted_data_b64)
print(f"DESede 密文 (Base64解码后 HEX): {desede_encrypted_bytes.hex()} (长度: {len(desede_encrypted_bytes)} 字节)")

# 5. 创建 DES3 Cipher 对象并解密
desede_cipher = DES3.new(desede_key, DES3.MODE_CBC, iv)

# 6. 解密并移除 PKCS7 填充
decrypted_padded_data = desede_cipher.decrypt(desede_encrypted_bytes)
decrypted_data = unpad(decrypted_padded_data, DES3.block_size, style='pkcs7')

# 7. 返回 UTF-8 编码的明文字符串
return decrypted_data.decode('utf-8')


try:
final_decrypted_content = decrypt_data2(data2)
print("\n=============================")
print("最终解密后的 data2 内容:")
print(final_decrypted_content)
print("=============================")
except Exception as e:
print(f"\n解密过程中发生错误: {e}")
print("请检查:data2内容是否正确?")

image

实际测试

青龙面板

同时为了排除本地网络环境过差,可以放到阿里云上,自动抢

whyour/qinglong: 支持 Python3、JavaScript、Shell、Typescript 的定时任务管理平台(Timed task management platform supporting Python3, JavaScript, Shell, Typescript)

配置一下钉钉bot,实现执行完任务后自动推送

image

为了能够更快速的发请求包,遂采用JavaScript脚本执行

让Gemini写了一下,注意主程序依赖钉钉配置文件、加密脚本以及工作流的配置问,文件名需要一一对应

  • AutoTicket.js
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
#!/usr/bin/env node
/**
* 测试快速脚本 - 调试版本
*/

const https = require('https');
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
const { encryptRequest, isSlowResponse } = require('./encrypt_rsa.js');
const DINGTALK_CONFIG = require('./dingtalk_config.js');
const WORKFLOW_CONFIG = require('./workflow_config.js');

// 构建请求参数
function buildExchangeParams() {
return {
...WORKFLOW_CONFIG.commonFields,
timestamp: Date.now().toString(),
...WORKFLOW_CONFIG.functions.exchange
};
}

// 构建完整URL
function buildExchangeUrl() {
return `${WORKFLOW_CONFIG.baseUrl}${WORKFLOW_CONFIG.endpoints.exchange}`;
}

/**
* 快速发送HTTP请求
*/
function sendRequestFast(url, data) {
return new Promise((resolve, reject) => {
const urlObj = new URL(url);
const options = {
hostname: urlObj.hostname,
port: urlObj.port || 443,
path: urlObj.pathname,
method: 'POST',
headers: {
'Host': 'app.hzgh.org.cn',
'Content-Type': 'application/json;charset=UTF-8',
'User-Agent': 'Mozilla/5.0 (Linux; Android 14; GM1910 Build/AP2A.240905.003.F1; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/139.0.7258.158 Mobile Safari/537.36;unionApp;HZGH',
'Accept': 'application/json, text/plain, */*',
'Sec-Ch-Ua': '"Not;A=Brand";v="99", "Android WebView";v="139", "Chromium";v="139"',
'Sec-Ch-Ua-Mobile': '?1',
'Sec-Ch-Ua-Platform': '"Android"',
'Origin': 'https://app.hzgh.org.cn:8123',
'X-Requested-With': 'com.zjte.hanggongefamily',
'Sec-Fetch-Site': 'same-site',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Dest': 'empty',
'Referer': 'https://app.hzgh.org.cn:8123/',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
'Priority': 'u=1, i',
'Pragma': 'no-cache',
'Cache-Control': 'no-cache',
'Content-Length': Buffer.byteLength(data)
},
rejectUnauthorized: false,
timeout: WORKFLOW_CONFIG.request.timeout
};

console.log('发送请求到:', url);

const req = https.request(options, (res) => {
let responseData = '';
res.on('data', (chunk) => {
responseData += chunk;
});
res.on('end', () => {
resolve({
statusCode: res.statusCode,
data: responseData
});
});
});

req.on('error', (error) => {
console.error('请求错误:', error);
reject(error);
});
req.on('timeout', () => {
console.error('请求超时');
req.destroy();
reject(new Error('Request timeout'));
});

req.write(data);
req.end();
});
}

/**
* 发送钉钉通知
*/
function sendDingTalkMessage(message) {
return new Promise((resolve, reject) => {
// 检查是否启用钉钉推送
if (!DINGTALK_CONFIG.enabled) {
console.log('📱 钉钉推送已禁用,跳过发送');
resolve(true);
return;
}

// 检查配置是否完整
if (DINGTALK_CONFIG.webhook.includes('YOUR_ACCESS_TOKEN') ||
DINGTALK_CONFIG.secret === 'YOUR_SECRET') {
console.log('⚠️ 钉钉配置未完成,请先配置 dingtalk_config.js 文件');
resolve(false);
return;
}

const timestamp = Date.now();
const sign = require('crypto')
.createHmac('sha256', DINGTALK_CONFIG.secret)
.update(`${timestamp}\n${DINGTALK_CONFIG.secret}`)
.digest('base64');

const webhook = `${DINGTALK_CONFIG.webhook}&timestamp=${timestamp}&sign=${encodeURIComponent(sign)}`;

const data = JSON.stringify({
msgtype: 'text',
text: {
content: message
}
});

const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(data)
}
};

const req = https.request(webhook, options, (res) => {
let responseData = '';
res.on('data', (chunk) => {
responseData += chunk;
});
res.on('end', () => {
if (res.statusCode === 200) {
console.log('✅ 钉钉通知发送成功');
resolve(true);
} else {
console.error('❌ 钉钉通知发送失败:', res.statusCode, responseData);
resolve(false);
}
});
});

req.on('error', (error) => {
console.error('❌ 钉钉通知发送错误:', error.message);
resolve(false);
});

req.write(data);
req.end();
});
}

/**
* 快速解密响应
*/
async function decryptResponseFast(responseData) {
try {
console.log('开始解密响应...');

// 构造完整的响应格式用于解密
const parsedData = JSON.parse(responseData);
const fullResponse = {
result: "000000",
msg: "处理中",
data2: parsedData.data2
};

// 使用临时文件方式传递数据
const fs = require('fs');
const path = require('path');
const tempFile = path.join(__dirname, 'temp_response.json');

// 写入临时文件
fs.writeFileSync(tempFile, JSON.stringify(fullResponse), 'utf8');

// 使用临时文件进行解密
const command = `node --security-revert=CVE-2023-46809 decrypt.js < "${tempFile}"`;
const { stdout, stderr } = await execAsync(command, {
timeout: 5000,
shell: true
});

// 清理临时文件
try {
fs.unlinkSync(tempFile);
} catch (e) {
// 忽略清理错误
}

if (stderr && !stderr.includes('SECURITY WARNING')) {
console.error('解密错误:', stderr);
return null;
}

if (!stdout) {
console.error('解密返回空结果');
return null;
}

// 过滤掉安全警告信息,提取JSON部分
const lines = stdout.split('\n');
let jsonContent = '';
let inJson = false;

for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine.startsWith('{')) {
inJson = true;
jsonContent = trimmedLine;
} else if (inJson) {
jsonContent += ' ' + trimmedLine;
if (trimmedLine.endsWith('}')) {
break;
}
}
}

if (!jsonContent) {
console.error('解密输出中未找到有效JSON');
return null;
}

console.log('解密成功,解析JSON...');
return JSON.parse(jsonContent);
} catch (error) {
console.error('解密失败:', error.message);
return null;
}
}

/**
* 执行单次请求
*/
async function executeRequest(params, attempt = 1) {
const url = buildExchangeUrl();
const startTime = Date.now();

try {
console.log(`\n🚀 第 ${attempt} 次尝试...`);

// 生成加密请求
const encryptStartTime = Date.now();
const encryptedData = encryptRequest(params, WORKFLOW_CONFIG.baseUrl);
const encryptTime = Date.now() - encryptStartTime;
console.log(`🔐 加密完成,耗时: ${encryptTime}ms,数据长度: ${encryptedData.length}`);

// 发送请求
const requestStartTime = Date.now();
const response = await sendRequestFast(url, encryptedData);
const requestTime = Date.now() - requestStartTime;

if (response.statusCode !== 200) {
console.error('❌ 请求失败,状态码:', response.statusCode);

// 根据状态码返回不同的错误信息
let errorType = 'UNKNOWN_ERROR';
let shouldRetry = true;
let retryDelay = 1000; // 默认1秒延迟

if (response.statusCode >= 500) {
// 5xx 服务器错误,延迟更长时间
errorType = 'SERVER_ERROR';
retryDelay = 3000; // 3秒延迟
console.log('⚠️ 服务器错误,将延迟重试');
} else if (response.statusCode === 404) {
// 404 错误,延迟中等时间
errorType = 'NOT_FOUND';
retryDelay = 2000; // 2秒延迟
console.log('⚠️ 资源未找到,将延迟重试');
} else if (response.statusCode === 429) {
// 429 请求过多,延迟更长时间
errorType = 'TOO_MANY_REQUESTS';
retryDelay = 5000; // 5秒延迟
console.log('⚠️ 请求过于频繁,将延迟重试');
} else if (response.statusCode >= 400 && response.statusCode < 500) {
// 4xx 客户端错误,延迟较短时间
errorType = 'CLIENT_ERROR';
retryDelay = 1500; // 1.5秒延迟
console.log('⚠️ 客户端错误,将延迟重试');
}

const totalTime = Date.now() - startTime;
return {
isSlow: false,
isError: true,
isHttpError: true,
error: `HTTP ${response.statusCode}`,
errorType: errorType,
retryDelay: retryDelay,
data: {
result: "ERROR",
msg: `HTTP错误: ${response.statusCode}`,
trcode: errorType
},
timing: {
encryptTime: 0,
requestTime: totalTime,
decryptTime: 0,
totalTime: totalTime
}
};
}

console.log(`📡 请求完成,耗时: ${requestTime}ms`);

// 解密响应
const decryptStartTime = Date.now();
console.log('🔓 开始解密响应...');

// 解析响应
const responseJson = JSON.parse(response.data);

if (responseJson.data2) {
const decryptedResponse = await decryptResponseFast(response.data);
const decryptTime = Date.now() - decryptStartTime;

if (decryptedResponse && decryptedResponse.data2) {
const data2Json = JSON.parse(decryptedResponse.data2);
const totalTime = Date.now() - startTime;

console.log(`🔓 解密完成,耗时: ${decryptTime}ms`);
console.log(`⏱️ 总耗时: ${totalTime}ms`);
console.log('📦 解密结果:', JSON.stringify(data2Json, null, 2));

// 检查是否是"手慢"响应
if (isSlowResponse(data2Json)) {
console.log('⚠️ 检测到"手慢"响应,准备重试...');
return {
isSlow: true,
data: data2Json,
timing: {
encryptTime,
requestTime,
decryptTime,
totalTime
}
};
} else {
console.log('🎉 成功!非"手慢"响应');
return {
isSlow: false,
data: data2Json,
timing: {
encryptTime,
requestTime,
decryptTime,
totalTime
}
};
}
} else {
console.log('❌ 解密失败');
return null;
}
} else {
console.log('❌ 响应中没有data2字段');
return null;
}
} catch (error) {
const totalTime = Date.now() - startTime;
console.error(`❌ 请求失败,总耗时: ${totalTime}ms,错误:`, error.message);

// 返回错误信息而不是null,这样主循环可以继续
return {
isSlow: false,
isError: true,
error: error.message,
data: {
result: "ERROR",
msg: `请求失败: ${error.message}`,
trcode: "NETWORK_ERROR"
},
timing: {
encryptTime: 0,
requestTime: totalTime,
decryptTime: 0,
totalTime: totalTime
}
};
}
}

/**
* 计算性能统计
*/
function calculatePerformanceStats(timings) {
if (timings.length === 0) return null;

const encryptTimes = timings.map(t => t.encryptTime);
const requestTimes = timings.map(t => t.requestTime);
const decryptTimes = timings.map(t => t.decryptTime);
const totalTimes = timings.map(t => t.totalTime);

const stats = {
attempts: timings.length,
encrypt: {
min: Math.min(...encryptTimes),
max: Math.max(...encryptTimes),
avg: Math.round(encryptTimes.reduce((a, b) => a + b, 0) / encryptTimes.length)
},
request: {
min: Math.min(...requestTimes),
max: Math.max(...requestTimes),
avg: Math.round(requestTimes.reduce((a, b) => a + b, 0) / requestTimes.length)
},
decrypt: {
min: Math.min(...decryptTimes),
max: Math.max(...decryptTimes),
avg: Math.round(decryptTimes.reduce((a, b) => a + b, 0) / decryptTimes.length)
},
total: {
min: Math.min(...totalTimes),
max: Math.max(...totalTimes),
avg: Math.round(totalTimes.reduce((a, b) => a + b, 0) / totalTimes.length)
}
};

return stats;
}

/**
* 生成请求耗时详情
*/
function generateRequestTimingDetails(requestDetails) {
if (requestDetails.length === 0) return '';

let details = '\n📊 每次请求耗时:\n';
details += '┌─────────┬─────┬─────────────┐\n';
details += '│ 尝试 │ 请求耗时 │ 结果 │\n';
details += '├─────────┼─────┼─────────────┤\n';

requestDetails.forEach(detail => {
const timing = detail.timing;
const result = detail.result;
const msg = detail.msg.length > 8 ? detail.msg.substring(0, 8) + '...' : detail.msg;

details += `│ 第${detail.attempt}次 │ ${timing.requestTime.toString().padStart(3)}ms │ ${msg.substring(0, 3)} │\n`;
});

details += '└─────────┴─────┴─────────────┘';
return details;
}

/**
* 显示性能统计
*/
function displayPerformanceStats(stats) {
if (!stats) return;

console.log('\n📊 性能统计报告');
console.log('='.repeat(50));
console.log(`🔄 总尝试次数: ${stats.attempts}`);
console.log('\n⏱️ 各阶段耗时统计 (毫秒):');
console.log('┌─────────┬─────┬─────┬─────┐');
console.log('│ 阶段 │ 最小 │ 最大 │ 平均 │');
console.log('├─────────┼─────┼─────┼─────┤');
console.log(`│ 加密 │ ${stats.encrypt.min.toString().padStart(3)}${stats.encrypt.max.toString().padStart(3)}${stats.encrypt.avg.toString().padStart(3)} │`);
console.log(`│ 请求 │ ${stats.request.min.toString().padStart(3)}${stats.request.max.toString().padStart(3)}${stats.request.avg.toString().padStart(3)} │`);
console.log(`│ 解密 │ ${stats.decrypt.min.toString().padStart(3)}${stats.decrypt.max.toString().padStart(3)}${stats.decrypt.avg.toString().padStart(3)} │`);
console.log(`│ 总计 │ ${stats.total.min.toString().padStart(3)}${stats.total.max.toString().padStart(3)}${stats.total.avg.toString().padStart(3)} │`);
console.log('└─────────┴─────┴─────┴─────┘');
}

/**
* 主测试函数 - 带重试和钉钉推送
*/
async function main() {
const scriptStartTime = Date.now();
console.log('🧪 测试快速脚本 - 带重试和钉钉推送');
console.log('='.repeat(60));

// 构建请求参数
const params = buildExchangeParams();

let attempt = 1;
let lastResult = null;
const allTimings = []; // 收集所有请求的耗时信息
const requestDetails = []; // 收集每次请求的详细信息
let consecutiveErrors = 0; // 连续错误次数

while (attempt <= DINGTALK_CONFIG.maxRetries) {
lastResult = await executeRequest(params, attempt);

if (!lastResult) {
console.log('❌ 请求失败,继续重试...');
// 不停止,继续重试
if (attempt < DINGTALK_CONFIG.maxRetries) {
console.log(`⏳ 等待 ${DINGTALK_CONFIG.retryDelay}ms 后重试...`);
await new Promise(resolve => setTimeout(resolve, DINGTALK_CONFIG.retryDelay));
// 重新构建参数以更新时间戳
Object.assign(params, buildExchangeParams());
}
attempt++;
continue;
}

// 收集耗时信息和请求详情
if (lastResult.timing) {
allTimings.push(lastResult.timing);
requestDetails.push({
attempt: attempt,
timing: lastResult.timing,
result: lastResult.data.result,
msg: lastResult.data.msg
});
}

// 检查是否是错误响应
if (lastResult.isError) {
consecutiveErrors++;

if (lastResult.isHttpError) {
// HTTP错误,使用智能延迟 + 动态调整
let baseDelay = lastResult.retryDelay || DINGTALK_CONFIG.retryDelay;
// 根据连续错误次数增加延迟(指数退避)
const dynamicDelay = Math.min(baseDelay * Math.pow(1.5, consecutiveErrors - 1), 10000);
console.log(`⚠️ HTTP错误 (${lastResult.errorType}),连续错误${consecutiveErrors}次,等待 ${Math.round(dynamicDelay)}ms 后重试...`);

if (attempt < DINGTALK_CONFIG.maxRetries) {
await new Promise(resolve => setTimeout(resolve, dynamicDelay));
// 重新构建参数以更新时间戳
Object.assign(params, buildExchangeParams());
}
} else {
// 网络错误,使用默认延迟 + 动态调整
const dynamicDelay = Math.min(DINGTALK_CONFIG.retryDelay * Math.pow(1.5, consecutiveErrors - 1), 8000);
console.log(`⚠️ 网络错误,连续错误${consecutiveErrors}次,等待 ${Math.round(dynamicDelay)}ms 后重试...`);

if (attempt < DINGTALK_CONFIG.maxRetries) {
await new Promise(resolve => setTimeout(resolve, dynamicDelay));
// 重新构建参数以更新时间戳
Object.assign(params, buildExchangeParams());
}
}
attempt++;
continue;
} else {
// 成功请求,重置连续错误计数
consecutiveErrors = 0;
}

if (!lastResult.isSlow) {
console.log('🎉 成功获取到非"手慢"响应!');

// 生成请求耗时详情
const timingDetails = generateRequestTimingDetails(requestDetails);

// 发送成功通知到钉钉
const successMessage = `🎉 积分兑换成功!\n\n` +
`📊 响应内容:\n${JSON.stringify(lastResult.data, null, 2)}\n\n` +
`⏰ 时间:${new Date().toLocaleString()}\n` +
`🔄 尝试次数:${attempt}\n` +
`⏱️ 最后耗时:${lastResult.timing ? lastResult.timing.totalTime : 0}ms` +
timingDetails;

await sendDingTalkMessage(successMessage);
break;
}

// 如果是"手慢"响应,准备重试
if (attempt < DINGTALK_CONFIG.maxRetries) {
console.log(`⏳ 等待 ${DINGTALK_CONFIG.retryDelay}ms 后重试...`);
await new Promise(resolve => setTimeout(resolve, DINGTALK_CONFIG.retryDelay));

// 重新构建参数以更新时间戳
Object.assign(params, buildExchangeParams());
}

attempt++;
}

// 如果所有重试都失败,发送失败通知
if (lastResult) {
if (lastResult.isSlow) {
console.log('😞 所有重试都返回"手慢"响应');

// 生成请求耗时详情
const timingDetails = generateRequestTimingDetails(requestDetails);

const failMessage = `😞 积分兑换失败 - 手慢啦!\n\n` +
`📊 最后响应内容:\n${JSON.stringify(lastResult.data, null, 2)}\n\n` +
`⏰ 时间:${new Date().toLocaleString()}\n` +
`🔄 总尝试次数:${DINGTALK_CONFIG.maxRetries}\n` +
`⏱️ 最后耗时:${lastResult.timing ? lastResult.timing.totalTime : 0}ms` +
timingDetails;

await sendDingTalkMessage(failMessage);
} else if (lastResult.isError) {
if (lastResult.isHttpError) {
console.log('😞 所有重试都遇到HTTP错误');

// 生成请求耗时详情
const timingDetails = generateRequestTimingDetails(requestDetails);

const errorMessage = `😞 积分兑换失败 - HTTP错误!\n\n` +
`📊 错误类型:${lastResult.errorType}\n` +
`📊 状态码:${lastResult.error}\n` +
`📊 连续错误:${consecutiveErrors}次\n\n` +
`⏰ 时间:${new Date().toLocaleString()}\n` +
`🔄 总尝试次数:${DINGTALK_CONFIG.maxRetries}\n` +
`⏱️ 最后耗时:${lastResult.timing ? lastResult.timing.totalTime : 0}ms` +
timingDetails;

await sendDingTalkMessage(errorMessage);
} else {
console.log('😞 所有重试都遇到网络错误');

// 生成请求耗时详情
const timingDetails = generateRequestTimingDetails(requestDetails);

const errorMessage = `😞 积分兑换失败 - 网络错误!\n\n` +
`📊 错误信息:${lastResult.error}\n` +
`📊 连续错误:${consecutiveErrors}次\n\n` +
`⏰ 时间:${new Date().toLocaleString()}\n` +
`🔄 总尝试次数:${DINGTALK_CONFIG.maxRetries}\n` +
`⏱️ 最后耗时:${lastResult.timing ? lastResult.timing.totalTime : 0}ms` +
timingDetails;

await sendDingTalkMessage(errorMessage);
}
}
} else {
console.log('😞 所有重试都失败,无响应');

// 生成请求耗时详情
const timingDetails = generateRequestTimingDetails(requestDetails);

const failMessage = `😞 积分兑换失败 - 无响应!\n\n` +
`⏰ 时间:${new Date().toLocaleString()}\n` +
`🔄 总尝试次数:${DINGTALK_CONFIG.maxRetries}` +
timingDetails;

await sendDingTalkMessage(failMessage);
}

// 显示性能统计
const performanceStats = calculatePerformanceStats(allTimings);
displayPerformanceStats(performanceStats);

const totalScriptTime = Date.now() - scriptStartTime;
console.log(`\n🏁 脚本执行完成,总耗时: ${totalScriptTime}ms`);
}

// 运行测试
if (require.main === module) {
main().catch(console.error);
}

module.exports = { main };
  • encyrpt_rsa.js
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
#!/usr/bin/env node
/**
* 完整RSA签名加密模块
* 基于encrypto.py重写,包含正确的RSA签名
*/

const crypto = require('crypto');
const NodeRSA = require('node-rsa');

// 常量定义
const F38860A = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const F38813C_PUB_KEY_BASE64 = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVXsxrrMcxNwFNYt0wMTdqc5WMa4gr7nMbWbcQCpJ2XNBMTQetknYNzCr8MMRdHBKFKjdCJE40u6UDBXQx13z7OSKyvQBwtLj5n8eIQXRtpMIjvqfR1xRuNBi5147ZXJDbKxWGRm0kjLN5UuqnDe6zu8v6MKU7KNDzHUrWqsj2LwIDAQAB";
const F38814D_PUB_KEY_B_BASE64 = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC7yWoQaojBBqKI2H0j4e8ZeX/n1yip6hxrxSVth5F5n1JJ/B3liPMdz6K1chNLFTAcbI7hTL9KkphP9yQ+bPYD68Ajrt/DFrW679Zi1CoeetHVrM4sF68lYarGXwnSlKloaPWnI4Ch9cSqIvIOInlpeJqYPlJ8ZJvGCmbQoM6bewIDAQAB";
const F27605N_PRIV_KEY_BASE64 = "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAJ+C8Z9awsGU8DeBpq47p+pVBgIxWr9epYE5lTrVwoTvOv7dOBTsNgYPgDqFLbU8eZsV26DOvgd4TC5tZUWF7WbAleOcxvwA143XTBpZEeDx6who8KiW1WBKUwkeEfXZvOWhN2d+8GlCjvJu2J4yNGEXScQEIWb+ofE4Pd4yPkkzAgMBAAECgYB0Tzu18a0vEFX0c1JBm3g98w81jB1aiz3tMzqwMuvqmLIQ4uegwfhGhQkAItoIW/dj8RU7dWS096+87sG4ZwaKCv/SmT1CibqmSATrX6YNIFU4uXsZzMREJxmZi+V5AllT9DWBG5YjKgrGfWjL0Rq10ZvxYMTdjO+SbqDIjVoc+QJBAOrMXRO6G349NpLvo1QPevxIykKNKhr5Qkjv4oVydoVoHW6iMU30PhrBqBYla+K8W+xyeqrjd9ucDQFW/Z2+hD8CQQCt6jz4o7qadQM0gikoBsgWwp7teyZI/8ZH5htrKZwDJzUe6LuM9xjDeXAqqjNjQrDL7M+6T7ZwMmK3UN3boe4NAkEA6ioGabYh1TSXSNNVwG/v58twbA78/wm34aXb89rD+Shssflv0p7TkTuxtuR7RBU2WAmT7PoOfyaSkdN/++IVYQJBAJ/klCvQc/YfkFPNO0N2gK0UP4N8zmUc6tIdh6XNeocXm+oP9KaUYusMkghXtKkUnnDOBul28fdTC5kYOvD7fl0CQQDLIYfo8MSMgcFkBH1wRUbhjVv31bk8+4G9a+h7UkLdLtch5qPsS7bsFCyszqEYjhYtQ278Q20lSzaIsom0Q3ai"
const F5734B = new Set(["login_name", "login_auth_code", "auth_code", "pwd", "password", "newpwd", "amt", "tr_amt", "sms_code", "total_amount", "account_no", "mob_data", "order_amt", "before_amt", "txn_amt", "tel", "mobile", "new_mobile", "code", "cert_no", "card_no", "reserve_mobile", "reply_tel", "card_bal", "bank_card_no", "car_no", "user_id", "invite_code", "auth_code", "imgAuthCode", "imgUniCode"]);
const F27603M = "qwerqaz.-*";
const F27579A = "https://zhgh.hzgh.org/";
const EXCLUDE_SIGN_FIELDS = new Set(["content", "link_url", "url", "pic_cont", "advice_img1", "advice_img2", "advice_img3", "photo_one", "photo_two", "photo_three", "book_img", "pimge"]);
const SIGN_SALT_FOR_ELSE_BRANCH = "zSw3MLRV7VuwT!*G";

// 预加载RSA密钥
let rsaKeys = {
f38813c: null,
f38814d: null,
f27605n: null
};

// 初始化RSA密钥
function initRSAKeys() {
if (!rsaKeys.f38813c) {
try {
rsaKeys.f38813c = new NodeRSA();
rsaKeys.f38813c.importKey(Buffer.from(F38813C_PUB_KEY_BASE64, 'base64'), 'public-der');
} catch (error) {
console.error('Failed to load f38813c key:', error.message);
}
}
if (!rsaKeys.f38814d) {
try {
rsaKeys.f38814d = new NodeRSA();
rsaKeys.f38814d.importKey(Buffer.from(F38814D_PUB_KEY_B_BASE64, 'base64'), 'public-der');
} catch (error) {
console.error('Failed to load f38814d key:', error.message);
}
}
if (!rsaKeys.f27605n) {
try {
// 尝试PKCS#1格式
const privateKeyPem = `-----BEGIN RSA PRIVATE KEY-----\n${F27605N_PRIV_KEY_BASE64}\n-----END RSA PRIVATE KEY-----`;
rsaKeys.f27605n = new NodeRSA();
rsaKeys.f27605n.importKey(privateKeyPem, 'private-pem');
} catch (error) {
try {
// 尝试PKCS#8格式
const privateKeyPem = `-----BEGIN PRIVATE KEY-----\n${F27605N_PRIV_KEY_BASE64}\n-----END PRIVATE KEY-----`;
rsaKeys.f27605n = new NodeRSA();
rsaKeys.f27605n.importKey(privateKeyPem, 'private-pem');
} catch (error2) {
try {
// 尝试直接导入Base64
rsaKeys.f27605n = new NodeRSA();
rsaKeys.f27605n.importKey(Buffer.from(F27605N_PRIV_KEY_BASE64, 'base64'), 'private-der');
} catch (error3) {
// 静默处理错误,else分支不需要私钥
// console.error('Failed to load f27605n key:', error3.message);
}
}
}
}
}

// 快速生成随机字符串
function generateRandomStrC(length = 24) {
let result = '';
for (let i = 0; i < length; i++) {
result += F38860A[Math.floor(Math.random() * F38860A.length)];
}
return result.toUpperCase();
}

// 快速DESede加密
function desedeEncrypt(data, key) {
try {
const keyBuffer = Buffer.from(key, 'utf8');
if (keyBuffer.length !== 24) {
throw new Error("DESede key must be 24 bytes");
}

// 使用ECB模式,手动PKCS7填充
const cipher = crypto.createCipheriv('des-ede3-ecb', keyBuffer, null);
cipher.setAutoPadding(false);

// 手动PKCS7填充
const blockSize = 8;
const dataBuffer = Buffer.from(data, 'utf8');
const padding = blockSize - (dataBuffer.length % blockSize);
const paddedData = Buffer.concat([dataBuffer, Buffer.alloc(padding, padding)]);

const encrypted = Buffer.concat([cipher.update(paddedData), cipher.final()]);
return encrypted.toString('base64');
} catch (error) {
console.error('DESede encryption error:', error);
return null;
}
}

// 快速MD5哈希
function md5Hash(text) {
return crypto.createHash('md5').update(text, 'utf8').digest('hex');
}

// 快速SHA1哈希
function sha1Hash(text) {
return crypto.createHash('sha1').update(text, 'utf8').digest('hex');
}

// 快速RSA加密 - 使用Node.js原生crypto模块匹配Python版本
function rsaEncryptStrC(strCKey, useZhghKey = false) {
try {
const keyBase64 = useZhghKey ? F38813C_PUB_KEY_BASE64 : F38814D_PUB_KEY_B_BASE64;

// 将base64密钥转换为PEM格式
const publicKeyPem = `-----BEGIN PUBLIC KEY-----\n${keyBase64}\n-----END PUBLIC KEY-----`;

// 使用RSA公钥加密
const encrypted = crypto.publicEncrypt({
key: publicKeyPem,
padding: crypto.constants.RSA_PKCS1_PADDING
}, Buffer.from(strCKey, 'utf8'));

return encrypted.toString('base64');
} catch (error) {
console.error('RSA encryption error:', error);
return null;
}
}

// RSA SHA256签名 - 使用Node.js crypto模块,与Python版本一致
function rsaSha256Sign(message, privateKeyBase64) {
try {
// 使用正确的私钥
const privateKeyPem = `-----BEGIN PRIVATE KEY-----\n${F27605N_PRIV_KEY_BASE64}\n-----END PRIVATE KEY-----`;

// 使用crypto.createSign,这与Python的PKCS1_v1_5.new()等效
const sign = crypto.createSign('RSA-SHA256');
sign.update(message, 'utf8');
const signature = sign.sign(privateKeyPem, 'base64');

return signature;
} catch (error) {
console.error('RSA SHA256 signing error:', error);
return null;
}
}

// zhgh分支加密
function encryptRequestZhghBranch(originalParams) {
const finalJsonObj = { ...originalParams };

// 1. 生成会话密钥
const strC = generateRandomStrC();

// 2. 加密会话密钥
const decKey = rsaEncryptStrC(strC, true);
if (!decKey) {
throw new Error("Failed to encrypt strC to dec_key.");
}
finalJsonObj["dec_key"] = decKey;

// 3. 加密敏感字段
for (const field of F5734B) {
if (field in finalJsonObj && typeof finalJsonObj[field] === 'string') {
const encryptedValue = desedeEncrypt(finalJsonObj[field], strC);
if (encryptedValue) {
finalJsonObj[field] = encryptedValue;
}
}
}

// 4. 生成签名
const sortedKeys = Object.keys(finalJsonObj).sort();
const str2Parts = [];
const keyParts = [];

for (const k of sortedKeys) {
const v = finalJsonObj[k];
if (Array.isArray(v)) continue; // 跳过JSONArray

keyParts.push(k);
str2Parts.push(String(v));
}

const str2 = str2Parts.join('');
const keyStr = keyParts.join(',');

// 双重哈希签名
const signMessage = str2 + F27603M;
const md5Result = md5Hash(signMessage.toUpperCase());
const sha1Result = sha1Hash(md5Result.toUpperCase());
const sign = sha1Result.toUpperCase();

finalJsonObj["key"] = keyStr;
finalJsonObj["sign"] = sign;

return JSON.stringify(finalJsonObj);
}

// else分支加密
function encryptRequestElseBranch(originalParams) {
const finalJsonObj = { ...originalParams };

// 1. 生成会话密钥
const strC = generateRandomStrC();

// 2. 加密会话密钥
const decKey = rsaEncryptStrC(strC, false);
if (!decKey) {
throw new Error("Failed to encrypt strC to dec_key.");
}
finalJsonObj["dec_key"] = decKey;

// 3. 加密敏感字段
for (const field of F5734B) {
if (field in finalJsonObj && typeof finalJsonObj[field] === 'string') {
const encryptedValue = desedeEncrypt(finalJsonObj[field], strC);
if (encryptedValue) {
finalJsonObj[field] = encryptedValue;
}
}
}

// 4. 生成签名
const sortedKeys = Object.keys(finalJsonObj).sort();
const str3Parts = [];
const keyParts = [];

for (const k of sortedKeys) {
const v = finalJsonObj[k];

if (EXCLUDE_SIGN_FIELDS.has(k)) continue;
if (Array.isArray(v)) continue; // 跳过JSONArray

keyParts.push(k);
str3Parts.push(String(v));
}

const str3 = str3Parts.join('');
const keyStr = keyParts.join(',');

// RSA签名
const signMessage = str3 + SIGN_SALT_FOR_ELSE_BRANCH;
const sign = rsaSha256Sign(signMessage, F27605N_PRIV_KEY_BASE64);
if (!sign) {
throw new Error("Failed to generate signature.");
}

finalJsonObj["key"] = keyStr;
finalJsonObj["sign"] = sign;

return JSON.stringify(finalJsonObj);
}

// 主加密函数
function encryptRequest(originalParams, baseUrl = "") {
// 预加载密钥
initRSAKeys();

if (baseUrl.startsWith(F27579A)) {
return encryptRequestZhghBranch(originalParams);
} else {
return encryptRequestElseBranch(originalParams);
}
}

// 检查是否为手慢响应
function isSlowResponse(data2Json) {
return (data2Json.result === '999992' &&
data2Json.msg && data2Json.msg.includes('手慢') &&
data2Json.trcode === 'OL41');
}

module.exports = {
encryptRequest,
isSlowResponse,
desedeEncrypt,
rsaEncryptStrC,
rsaSha256Sign,
md5Hash,
sha1Hash
};
  • decrypt.js
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// decrypt.js

const crypto = require('crypto');

// RSA 私钥 (直接嵌入代码中)
const RSA_PRIVATE_KEY_PEM = `-----BEGIN PRIVATE KEY-----
MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAIOBMtf2AIYQlrNy/lVPHx4R/LKI+Vtk3bKmzID8vdVnh/4WA3lczqfejM10Xfy3sNe4l5EeQTvnDgUHbIFK8FyJRpvypAmS9oyW6uwGTjZEu5Y6hsSxiGAOG5ZOlH8vOSfuaAkZ+iUlqifPE3ZOmHkqGzmukg4wCRaPLx5ioq8zAgMBAAECgYAgLOVmx677HmXxBCrMbq57agU9HZx9SyGfS4Zv7Ob5pvo0Jei1sgpyMlabEmTIp50iOu0CubdWU8MvYdCfldlXQLW7cjk8N1NyGQLFd2fJ03a7gGWnwwEdPoNTpSHnB+mDL9l7MVjion5fLojzq9Pz1gMKL01I2TfZBDL4m6EbgQJBAMfgrMKtj7f40GA3qp/y/9/eBCAu8PbtFmtATLMQRf4tGhjvn349x1b6FZj8RiaRBSrq0Owjrdo5TUxgfS7dz3MCQQCobdWk2SQhRlqEHfFEro/8ab6gn3GhBDzzKvNjhKr2MO6JWqs+Vr+/P9uYpA+G+rv74uVIGWhjuNtI5+/69DFBAkAJOQS/tuJ6yrBSwD7PQpcr7UKjeYcE3cu7ByyC1q1kHRCnNedWG+Omz8NPW9Sg0vA6GrupKbxL5Xj7nTgpgXKhAkBIVlvioAvfaqrngUClAd//RZ9EtxYDVKGkwnaj8E/Iyr04KsPPU0ypJBD5XsT4cOmZxho5PAhUhAlSJ6MvAf/BAkA64ieVhtQA1KV0pSSEJMnbPlZe+yBYGTWLMaG2zL0kKEhIs2fIHbVhLFQ8TkO5oH+mhxuuXI5+nVU2G0dqUl6D
-----END PRIVATE KEY-----`;

// 固定的 DESede 密钥前缀
const DESEDE_KEY_PREFIX = Buffer.from("HTt0Hzsu", 'utf-8');

// 从标准输入获取JSON响应
function getStdin() {
return new Promise((resolve, reject) => {
let data = '';
process.stdin.on('data', chunk => {
data += chunk;
});
process.stdin.on('end', () => {
resolve(data);
});
process.stdin.on('error', err => {
reject(err);
});
});
}

async function decryptData2FromStdin() {
let responseData;
try {
const responseText = (await getStdin()).trim();
if (!responseText) {
console.error("错误:未输入响应数据,解密终止。");
process.exit(1);
}

responseData = JSON.parse(responseText);
const data2 = responseData.data2;

if (!data2) {
console.error("错误:响应中没有data2字段,解密终止。");
process.exit(1);
}

const finalDecryptedContent = decryptData2(data2);

// 创建包含解密后data2的完整响应
responseData.data2 = finalDecryptedContent;

// 输出完整的JSON响应
console.log(JSON.stringify(responseData, null, 2)); // null, 2 用于格式化输出

} catch (e) {
console.error(`解密过程中发生错误: ${e.message}`);
if (responseData) {
console.error("原始响应数据:");
console.error(JSON.stringify(responseData, null, 2));
} else {
console.error("请检查输入的JSON数据是否有效。");
}
process.exit(1);
}
}

function decryptData2(data2_full_base64) {
if (data2_full_base64.length < 172) {
throw new Error("data2 字符串长度不足 172,无法提取密钥材料。");
}

// --- 第一步:RSA 解密密钥材料 ---
// 1. 提取 data2 的前 172 个字符作为 RSA 加密数据 (Base64 编码)
const rsa_encrypted_material_b64 = data2_full_base64.substring(0, 172);

// 2. Base64 解码得到原始 RSA 密文
const rsa_encrypted_buffer = Buffer.from(rsa_encrypted_material_b64, 'base64');


// 3. RSA 解密 (使用 PKCS #1 v1.5 填充)
// Node.js crypto.privateDecrypt 支持 PKCS#1_OAEP 和 PKCS#1_V1_5 填充
// 对应 Java 的 "RSA/ECB/PKCS1Padding"
const rsa_decrypted_buffer = crypto.privateDecrypt(
{
key: RSA_PRIVATE_KEY_PEM,
padding: crypto.constants.RSA_PKCS1_PADDING, // 对应 PKCS1_v1_5 填充
},
rsa_encrypted_buffer
);
const rsa_decrypted_str = rsa_decrypted_buffer.toString('utf-8');


// --- 第二步:DESede 解密实际数据 ---
// 1. 提取 data2 的后半部分作为 DESede 密文 (Base64 编码)
const desede_encrypted_data_b64 = data2_full_base64.substring(172);

// 2. 生成 DESede 密钥 (K1K2K3 模式,取前 24 字节)
// Java: ("HTt0Hzsu" + str2).getBytes()
// 这里的 str2 就是 rsa_decrypted_str
const full_desede_key_buffer = Buffer.concat([
DESEDE_KEY_PREFIX,
Buffer.from(rsa_decrypted_str, 'utf-8')
]);
const desede_key = full_desede_key_buffer.subarray(0, 24); // 使用 subarray 兼容旧 Node.js (slice 也行)

if (desede_key.length !== 24) {
throw new Error(`生成的 DESede 密钥长度 ${desede_key.length} 不足 24 字节,无法用于三重 DES。`);
}


// 3. 生成 IV (Initialization Vector)
// Java: str2.substring(0, 8).getBytes()
// 这里的 str2 还是 rsa_decrypted_str
const iv_str = rsa_decrypted_str.substring(0, 8);
const iv = Buffer.from(iv_str, 'utf-8');

if (iv.length !== 8) {
throw new Error(`生成的 IV 长度 ${iv.length} 不符合 8 字节要求。`);
}


// 4. Base64 解码 DESede 密文
const desede_encrypted_buffer = Buffer.from(desede_encrypted_data_b64, 'base64');


// 5. 创建 DES3 Cipher 对象并解密
// 对应 Java 的 "DESede/CBC/PKCS5Padding"
const decipher = crypto.createDecipheriv('des-ede3-cbc', desede_key, iv);

// 6. 解密并移除 PKCS7 填充 (Node.js 的 PKCS7 填充就是 PKCS5Padding)
decipher.setAutoPadding(true); // 默认启用 PKCS7/PKCS5 padding

let decrypted = decipher.update(desede_encrypted_buffer);
decrypted = Buffer.concat([decrypted, decipher.final()]);

// 7. 返回 UTF-8 编码的明文字符串
return decrypted.toString('utf-8');
}


// 如果脚本被直接执行,则从标准输入读取数据并执行解密
if (require.main === module) {
decryptData2FromStdin();
}

// 可选:如果需要其他 JS 脚本导入这个功能
module.exports = {
decryptData2
};
  • dingtalk_config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 钉钉机器人配置文件
* 请根据您的钉钉机器人信息修改以下配置
*/

module.exports = {
// 钉钉机器人webhook地址
// 获取方式:在钉钉群中添加自定义机器人,复制webhook地址
webhook: 'https://oapi.dingtalk.com/robot/send?access_token=xxxxxx',

// 钉钉机器人加签密钥
// 获取方式:在创建机器人时选择"加签"安全设置,复制密钥
secret: '',

// 重试配置
maxRetries: 20, // 最大重试次数
retryDelay: 1000, // 重试间隔(毫秒)

// 是否启用钉钉推送(设为false可禁用推送进行测试)
enabled: true
};
  • workflow_config.js
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
/**
* 工作流配置文件
* 包含所有请求的公共字段和配置
*/

module.exports = {
// 基础配置
baseUrl: 'https://app.hzgh.org.cn',

// 公共请求字段
commonFields: {
channel: "02",
app_ver_no: "3.1.4",
login_name: "此处填写加密后的用户名",
ses_id: "此处填写会话ID"
},

// Cookie配置
cookies: {
'JSESSIONID': '406CD683439BDFEC843C6C3A5C928504',
'SERVERID': '12240fdeb12bd990da3bbf786eeeee2f|1758383252|1758382894'
},

// HTTP请求头配置
headers: {
'Host': 'app.hzgh.org.cn',
'Content-Type': 'text/plain;charset=utf-8',
'Connection': 'Keep-Alive',
'User-Agent': 'okhttp/3.4.2'
},

// API端点配置
endpoints: {
login: '/unionApp/interf/front/U/U042',
signin: '/unionApp/interf/front/U/U042',
comment: '/unionApp/interf/front/AC/AC08',
query: '/unionApp/interf/front/U/U005',
exchange: '/unionApp/interf/front/OL/OL41' // 兑换优惠券接口
},

// 功能特定参数
functions: {
// 登录签到参数
login: {
type: "1"
},

// 日常签到参数
signin: {
type: "5"
},

// 评论参数,默认评论是"好",如需修改,请修改content字段
comment: {
related_id: "1232",
content_type: "1",
oper_type: "0",
suffix: "png",
content: "好"
},

// 查询积分参数(无额外参数)
query: {},

// 兑换优惠券参数,默认兑换4块的优惠券,如需修改,请修改exchange_id字段,9是2块,10是4块,11是6块
exchange: {
user_id: "此处填写加密后的用户ID",
exchange_id: "10"
}
},

// 请求配置
request: {
timeout: 8000,
retryDelay: 1000
}
};

另外自动签到的脚本也放上来吧,说实话AI写的代码是又臭又长,简直一坨

  • workflow_sigin.js
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
#!/usr/bin/env node
/**
* 工作流版本:3次签到 → 1次评论 → 1次查询积分
* 每次执行都推送到钉钉
* JavaScript版本,避免语言差距带来的性能差异
*/

const https = require('https');
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { encryptRequest } = require('./encrypt_rsa.js');
const DINGTALK_CONFIG = require('./dingtalk_config.js');
const WORKFLOW_CONFIG = require('./workflow_config.js');

/**
* 构建请求参数
* @param {string} functionName - 功能名称 (login, signin, comment, query)
* @param {Object} extraParams - 额外参数
* @returns {Object} 完整的请求参数
*/
function buildRequestParams(functionName, extraParams = {}) {
const params = {
...WORKFLOW_CONFIG.commonFields,
timestamp: Date.now().toString(),
...WORKFLOW_CONFIG.functions[functionName],
...extraParams
};
return params;
}

/**
* 构建完整URL
* @param {string} functionName - 功能名称
* @returns {string} 完整URL
*/
function buildUrl(functionName) {
return `${WORKFLOW_CONFIG.baseUrl}${WORKFLOW_CONFIG.endpoints[functionName]}`;
}

/**
* 通用执行函数
* @param {string} functionName - 功能名称
* @param {string} displayName - 显示名称
* @param {Object} extraParams - 额外参数
* @returns {Promise<boolean>} 执行结果
*/
async function executeFunction(functionName, displayName, extraParams = {}) {
console.log(`\n${'='.repeat(60)}`);
console.log(`🚀 ${displayName}`);
console.log(`⏰ 开始时间: ${new Date().toLocaleString()}`);

const params = buildRequestParams(functionName, extraParams);
const url = buildUrl(functionName);

try {
// 生成加密请求
const encryptedData = encryptRequest(params, url);
console.log('🔐 加密请求体:', JSON.stringify(JSON.parse(encryptedData), null, 2));
console.log('-'.repeat(50));

// 发送请求
const response = await sendRequestFast(url, encryptedData);

console.log(`✅ 请求成功! 状态码: ${response.statusCode}`);
console.log(`📄 原始响应: ${response.data}`);
console.log('-'.repeat(50));

// 解析响应
try {
const responseJson = JSON.parse(response.data);

if (responseJson.data2) {
console.log('🔍 发现data2字段,调用解密...');
const decryptedResponse = await callDecryptScript(responseJson);

if (decryptedResponse && decryptedResponse.data2) {
try {
const data2Json = JSON.parse(decryptedResponse.data2);
const msg = JSON.stringify(data2Json, null, 2);
console.log(`📋 解密结果: ${msg}`);

// 发送到钉钉
const dingtalkMessage = `🚀 杭工e家${displayName}执行完成

⏰ 执行时间: ${new Date().toLocaleString()}
📋 结果: ${data2Json.msg || '未知'}

详细信息:
${msg}`;

console.log('📤 发送钉钉通知...');
await sendDingTalkMessage(dingtalkMessage);

return true;
} catch (parseError) {
console.log(`⚠️ data2内容解析失败: ${parseError.message}`);
return false;
}
} else {
console.log('❌ 解密失败');
return false;
}
} else {
console.log('⚠️ 响应中没有data2字段');
return false;
}
} catch (jsonError) {
console.log('响应不是JSON格式');
return false;
}
} catch (error) {
console.log(`❌ 请求失败: ${error.message}`);
return false;
}
}

/**
* 快速发送HTTP请求
*/
function sendRequestFast(url, data) {
return new Promise((resolve, reject) => {
const urlObj = new URL(url);
const options = {
hostname: urlObj.hostname,
port: urlObj.port || 443,
path: urlObj.pathname,
method: 'POST',
headers: {
...WORKFLOW_CONFIG.headers,
'Content-Length': Buffer.byteLength(data)
},
rejectUnauthorized: false,
timeout: WORKFLOW_CONFIG.request.timeout
};

console.log('发送请求到:', url);

const req = https.request(options, (res) => {
let responseData = '';
res.on('data', (chunk) => {
responseData += chunk;
});
res.on('end', () => {
resolve({
statusCode: res.statusCode,
data: responseData
});
});
});

req.on('error', (error) => {
console.error('请求错误:', error);
reject(error);
});
req.on('timeout', () => {
console.error('请求超时');
req.destroy();
reject(new Error('Request timeout'));
});

req.write(data);
req.end();
});
}

/**
* 调用decrypt.js脚本解密响应
*/
async function callDecryptScript(responseJson) {
try {
console.log('开始解密响应...');

// 使用临时文件传递数据,避免shell转义问题
const tempFile = path.join(__dirname, 'temp_response.json');

// 写入临时文件
fs.writeFileSync(tempFile, JSON.stringify(responseJson), 'utf8');

try {
// 使用临时文件进行解密
const command = `node --security-revert=CVE-2023-46809 decrypt.js < "${tempFile}"`;
const { stdout, stderr } = await execAsync(command, {
timeout: 5000,
shell: true
});

if (stderr && !stderr.includes('SECURITY WARNING')) {
console.error('解密错误:', stderr);
return null;
}

if (!stdout) {
console.error('解密返回空结果');
return null;
}

// 过滤掉安全警告信息,提取JSON部分
const lines = stdout.split('\n');
let jsonContent = '';
let inJson = false;

for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine.startsWith('{')) {
inJson = true;
jsonContent = trimmedLine;
} else if (inJson) {
jsonContent += ' ' + trimmedLine;
if (trimmedLine.endsWith('}')) {
break;
}
}
}

if (!jsonContent) {
console.error('解密输出中未找到有效JSON');
return null;
}

console.log('解密成功,解析JSON...');
return JSON.parse(jsonContent);
} finally {
// 清理临时文件
try {
fs.unlinkSync(tempFile);
} catch (e) {
// 忽略清理错误
}
}
} catch (error) {
console.error('解密失败:', error.message);
return null;
}
}

/**
* 发送钉钉通知
*/
function sendDingTalkMessage(message) {
return new Promise((resolve, reject) => {
// 检查是否启用钉钉推送
if (!DINGTALK_CONFIG.enabled) {
console.log('📱 钉钉推送已禁用,跳过发送');
resolve(true);
return;
}

// 检查配置是否完整
if (DINGTALK_CONFIG.webhook.includes('YOUR_ACCESS_TOKEN') ||
DINGTALK_CONFIG.secret === 'YOUR_SECRET') {
console.log('⚠️ 钉钉配置未完成,请先配置 dingtalk_config.js 文件');
resolve(false);
return;
}

const timestamp = Date.now();
const sign = crypto
.createHmac('sha256', DINGTALK_CONFIG.secret)
.update(`${timestamp}\n${DINGTALK_CONFIG.secret}`)
.digest('base64');

const webhook = `${DINGTALK_CONFIG.webhook}&timestamp=${timestamp}&sign=${encodeURIComponent(sign)}`;

const data = JSON.stringify({
msgtype: 'text',
text: {
content: message
}
});

const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(data)
}
};

const req = https.request(webhook, options, (res) => {
let responseData = '';
res.on('data', (chunk) => {
responseData += chunk;
});
res.on('end', () => {
if (res.statusCode === 200) {
console.log('✅ 钉钉通知发送成功');
resolve(true);
} else {
console.error('❌ 钉钉通知发送失败:', res.statusCode, responseData);
resolve(false);
}
});
});

req.on('error', (error) => {
console.error('❌ 钉钉通知发送错误:', error.message);
resolve(false);
});

req.write(data);
req.end();
});
}

/**
* 执行登录签到功能
*/
async function loginFirst() {
return await executeFunction('login', '执行登录签到功能');
}

/**
* 执行签到功能
*/
async function executeSignin(attempt) {
return await executeFunction('signin', `第 ${attempt} 次签到`);
}

/**
* 执行评论功能
*/
async function executeComment() {
return await executeFunction('comment', '执行评论功能');
}

/**
* 执行查询积分功能
*/
async function executeQuery() {
return await executeFunction('query', '执行查询积分功能');
}

/**
* 主工作流函数
*/
async function main() {
console.log('🎯 杭工e家工作流脚本 (JavaScript版本)');
console.log('='.repeat(60));
console.log('工作流: 3次签到 → 1次评论 → 1次查询积分');
console.log('每次执行都推送到钉钉');
console.log('='.repeat(60));

const startTime = Date.now();

try {
// 执行登录签到
console.log('\n🔄 开始执行登录签到...');
await loginFirst();

// 执行3次签到
console.log('\n🔄 开始执行3次签到...');
for (let i = 1; i <= 3; i++) {
console.log(`\n第 ${i} 次签到:`);
await executeSignin(i);
await new Promise(resolve => setTimeout(resolve, WORKFLOW_CONFIG.request.retryDelay)); // 等待配置的延迟时间
}

// 执行1次评论
console.log('\n🔄 开始执行评论...');
await executeComment();

// 执行1次查询积分
console.log('\n🔄 开始查询积分...');
await executeQuery();

const totalTime = Date.now() - startTime;
console.log('\n🎉 工作流执行完成!');
console.log(`⏱️ 总耗时: ${totalTime}ms`);
console.log('='.repeat(60));

} catch (error) {
console.error('❌ 工作流执行失败:', error.message);
}
}

// 运行工作流
if (require.main === module) {
main().catch(console.error);
}

module.exports = { main, loginFirst, executeSignin, executeComment, executeQuery };

贴一下实际运行后的效果

青龙面板可以配置定时抢,每天的上午场和下午场,其实上午场就够抢两张的了,自己够用就行😅

image

  • 签到推送

image

  • 留言推送

image

  • 兑换优惠券推送

image

后记

当时在8月初的时候有了逆向杭工e家的想法,研究了两天还没搞定,卡在login_name那块,这是个二次加密的值,我以为还需要再次解密,遂去学习了一下frida,又是配环境又是hook生成函数,搁置了

后半个月出差加上公司各种事情,一直拖到九月中旬才开始,直到前几天我发现Github上有现成的仓库

发现login_name就是二次加密的base64值,直接再次加密签名即可😅

因为有现成的仓库,我就不重复造轮子新开一个仓库,利用大模型写了个JS版本的,性能开销小点,速度会快很多

已经PR了,等待作者合并中。。。😊

总字数 694.8k
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务