Nessus-Scanner漏洞扫描及安全防范

2/2/2022 Nessus-ScannerRSA及AES加密算法漏洞扫描系统安全源码加密打包

# 1. Nessus Scanner扫描安全漏洞

# 1.1 Nessus Scanner简介

Nessus号称是世界上最流行的漏洞扫描程序,全世界有超过75000个组织在使用它。该工具提供完整的电脑漏洞扫描服务,并随时更新其漏洞数据库。Nessus不同于传统的漏洞扫描软件,Nessus可同时在本机或远端上遥控,进行系统的漏洞分析扫描。

Nessus

# 1.2 搭建破解版Nessus Scanner服务

# 1.2.1 搭建服务

官方的Nessus Scanner需要破解,我这里在Github上找了一个docker部署的免破解项目。

项目地址:https://github.com/elliot-bia/nessus (opens new window)

$ docker run -itd --name nessus -p 8834:8834 --restart=always ramisec/nessus
1

这时候我们用http访问服务地址,发现是被限制访问的。

Nessus禁止HTTP访问

下面我们有两种方式去访问。

  • 方法一:直接把http改成https(临时方案)

    直接把http改成https,但由于证书是无效的,Chrome会爆出“您的连接不是私密连接”的错误,这时候在旁边空白处鼠标左键单击一下,然后输入thisisunsafe,即可刷新成功访问。

  • 方法二:配置域名申请证书并开启https(永久方案)

    配置过程这里就不赘述了,详见我的另一篇博客:VPS基本部署环境的搭建与配置 (opens new window),需要注意的一点是,配置proxy_pass的时候,要写https地址。

# 1.2.2 重置密码

现在可以访问页面了,但登录密码处作者留了个彩蛋,没有公开,真的调皮。

nessus密码彩蛋

这里我们也不去破解了,直接去重置密码,然后用新密码登录即可。

$ docker exec -it nessus /bin/bash   # 进入容器内部
$ cd /opt/nessus/sbin/               # 切换到到Nessus目录
$ ./nessuscli lsuser                 # 列出所有用户(默认只有admin)
$ ./nessuscli chpasswd admin         # 更新admin用户的密码,需要输入两次
1
2
3
4

注:如果重置一遍没生效的话,就再去重置一遍,不清楚为什么我重置了两次才生效。

# 1.2.3 破解更新

这时候,打开 Nessus Scanner,想要新建一个扫描任务,会出现500内部服务器错误,原因是我们还没有破解更新,执行如下命令即可。

$ docker exec -it nessus /bin/bash /nessus/update.sh
1

Nessus-Scanner破解更新

破解更新成功后,再去新建一个扫描任务,就可以正常显示了。

# 1.3 Nessus Scanner基本使用

# 1.3.1 扫描服务器漏洞

点击New Scan——选择Basic Network Scan、Advanced Scan、Malware Scan等,其中Name随便填,Targets填你要扫描的主机,Description可填可不填,然后在Credentials处提供服务器的连接信息。

Nessus-Scanner新建扫描任务

配置完毕后,在列表页点击三角符号开始扫描,会实时显示扫描出的安全漏洞,扫描耗时比较长,耐心等待扫描结束。

Nessus-Scanner扫描过程

# 1.3.2 扫描Web应用漏洞

点击New Scan——选择Web Application Tests,其中Name随便填,Targets填你要扫描的主机,Description可填可不填。

配置Web-Application-Tests扫描

选中 DISCOVERY,将 Scan Type 选择为 Custom,再在 Port Scanning 处配置要扫描的端口号。

配置扫描的端口号

选择 Credentials 选项卡,新建HTTP认证,认证方式如何配置与具体Web应用有关,支持Automatic authentication、Basic/Digest authentication、HTTP login form、HTTP cookies import这4中认证方式。

配置登录认证方式

# 1.3.3 查看漏洞报告并针对性修复

Vulnerabilities处可以查看到所有扫描出的安全漏洞列表,点开即可查看详情,会给出漏洞描述、漏洞修复建议、漏洞端口等相关描述信息,可以根据这个对安全漏洞进行针对性的修复。

Nessus-Scanner查看漏洞报告

# 1.4 公开的安全漏洞检索

[1] 漏洞情报中心:https://vip.riskivy.com (opens new window)

漏洞情报中心

[2] 国家信息安全漏洞库:https://www.cnnvd.org.cn/home/childHome (opens new window)

国家信息安全漏洞库

# 1.5 Log4J严重安全漏洞

2021年11月在 Apache Log4J 日志记录库发现的一个严重漏洞。它本质上可以让黑客完全控制运行未打补丁 Log4J 版本的设备。恶意行为者可以利用该漏洞在易受攻击的系统中运行自己想要的几乎任何代码。

研究人员认为它是个“灾难性”安全漏洞,因为 Log4J 是全球部署最广泛的开源程序之一,而且该漏洞很容易利用。美国网络安全和基础设施安全局主任称“这是我整个职业生涯中见过的最严重的漏洞之一,即使不是唯一”。虽然该漏洞在被发现后不久就得到修补,但仍会带来多年风险,因为 Log4J 已深植于软件供应链。

Log4J严重安全漏洞

# 2. 数据加密解密算法

# 2.1 加密解密算法概述

AES加密为最常见的对称加密算法,RSA加密为最常见的非对称加密算法。平时开发的时候使用这两种算法进行加密,不要使用MD5,这是个摘要算法,不是用来加密的,加不加盐也都早就可以被完全破解了。

# 2.1.1 对称加密与非对称加密

对称加密的通信双方使用相同的密钥,如果一方的密钥遭泄露,那么整个通信就会被破解。 而非对称加密使用一对密钥,一个用来加密,一个用来解密,而且公钥是公开的,秘钥是自己保存的,不需要像对称加密那样在通信之前要先同步密钥。 非对称加密相比于对称加密,其安全性更好。

# 2.1.2 AES对称加密算法

AES加密为最常见的对称加密算法(微信小程序的加密传输就是用的这个加密算法)。对称加密算法也就是加密和解密用相同的密钥,具体的加密流程如下图:

AES加密传输流程

# 2.1.3 RSA非对称加密算法

RSA加密算法是一种非对称加密算法,所谓非对称,就是指该算法加密和解密使用不同的密钥,即使用加密密钥进行加密、解密密钥进行解密。在RSA算法中,加密密钥PK是公开信息,而解密密钥SK是需要保密的。加密算法E和解密算法D也都是公开的。虽然解密密钥SK是由公开密钥PK决定的,由于无法计算出大数n的欧拉函数phi(N),所以不能根据PK计算出SK。

# 2.1.4 MD5摘要算法

MD5信息摘要算法,是一种被广泛使用的密码散列函数,可以产生出一个128位的散列值,用于确保信息传输完整一致。MD5由美国密码学家Ronald Linn Rivest设计,于1992年公开,用以取代MD4算法。1996年后该算法被证实存在弱点,可以被加以破解,对于需要高度安全性的数据,专家一般建议改用其他算法。2004年,证实MD5算法无法防止碰撞,因此不适用于安全性认证,如SSL公开密钥认证或是数字签名等用途。

# 2.2 RSA加密解密

# 2.2.1 Java封装

import org.apache.commons.codec.binary.Base64;

import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

/**
 * RSA工具类
 */
public class RsaUtility {

    /**
     * 使用公钥字符串加密
     *
     * @param plainText       明文
     * @param publicKeyString 公钥字符串
     * @return 密文
     * @throws Exception 异常
     */
    public static String encryptByPublicString(String plainText, String publicKeyString) throws Exception {
        PublicKey publicKey = getPublicKeyByString(publicKeyString);
        return encrypt(plainText, publicKey);
    }

    /**
     * 使用私钥字符串解密
     *
     * @param cipherText       密文
     * @param privateKeyString 私钥字符串
     * @return 明文
     * @throws Exception 异常
     */
    public static String decryptByPrivateString(String cipherText, String privateKeyString) throws Exception {
        PrivateKey privateKey = getPrivateKeyByString(privateKeyString);
        return decrypt(cipherText, privateKey);
    }

    /**
     * 使用私钥字符串签名
     *
     * @param plainText        明文
     * @param privateKeyString 私钥字符串
     * @return 签名
     * @throws Exception 异常
     */
    public static String signByPrivateString(String plainText, String privateKeyString) throws Exception {
        PrivateKey privateKey = getPrivateKeyByString(privateKeyString);
        return sign(plainText, privateKey);
    }

    /**
     * 使用公钥字符串验签
     *
     * @param plainText       明文
     * @param signature       签名
     * @param publicKeyString 公钥字符串
     * @return 是否通过签名验证
     * @throws Exception 异常
     */
    public static boolean verifyByPublicString(String plainText, String signature, String publicKeyString) throws Exception {
        PublicKey publicKey = getPublicKeyByString(publicKeyString);
        return verify(plainText, signature, publicKey);
    }

    /**
     * 生成2048位的RSA密钥对
     *
     * @return 密钥对
     * @throws Exception 异常
     */
    public static KeyPair generateKeyPair() throws Exception {
        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
        generator.initialize(2048, new SecureRandom());
        return generator.generateKeyPair();
    }

    /**
     * 从RSA密钥对中获取私钥字符串
     *
     * @param keyPair RSA密钥对
     * @return 私钥字符串
     */
    public static String getPrivateKeyString(KeyPair keyPair) {
        return getPrivateKeyString(keyPair.getPrivate());
    }

    public static String getPrivateKeyString(PrivateKey privateKey) {
        return Base64.encodeBase64String(privateKey.getEncoded());
    }

    /**
     * 从RSA密钥对中获取公钥字符串
     *
     * @param keyPair RSA密钥对
     * @return 公钥字符串
     */
    public static String getPublicKeyString(KeyPair keyPair) {
        return Base64.encodeBase64String(keyPair.getPublic().getEncoded());
    }

    /**
     * 将私钥字符串还原为私钥
     *
     * @param privateKeyString 私钥字符串
     * @return 私钥
     * @throws Exception 异常
     */
    public static PrivateKey getPrivateKeyByString(String privateKeyString) throws Exception {
        byte[] keyBytes = Base64.decodeBase64(privateKeyString);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        return keyFactory.generatePrivate(keySpec);
    }

    /**
     * 将公钥字符串还原为公钥
     *
     * @param publicKeyString 公钥字符串
     * @return 公钥
     * @throws Exception 异常
     */
    public static PublicKey getPublicKeyByString(String publicKeyString) throws Exception {
        byte[] keyBytes = Base64.decodeBase64(publicKeyString);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        return keyFactory.generatePublic(keySpec);
    }

    /**
     * RSA加密
     *
     * @param plainText 明文
     * @param publicKey 公钥
     * @return 密文
     * @throws Exception 异常
     */
    public static String encrypt(String plainText, PublicKey publicKey) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        byte[] cipherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
        return Base64.encodeBase64String(cipherText);
    }

    /**
     * RSA解密
     *
     * @param cipherText 密文
     * @param privateKey 私钥
     * @return 明文
     * @throws Exception 异常
     */
    public static String decrypt(String cipherText, PrivateKey privateKey) throws Exception {
        byte[] bytes = Base64.decodeBase64(cipherText);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        return new String(cipher.doFinal(bytes), StandardCharsets.UTF_8);
    }

    /**
     * RSA签名
     *
     * @param plainText  明文
     * @param privateKey 私钥
     * @return 签名
     * @throws Exception 异常
     */
    public static String sign(String plainText, PrivateKey privateKey) throws Exception {
        Signature privateSignature = Signature.getInstance("SHA256withRSA");
        privateSignature.initSign(privateKey);
        privateSignature.update(plainText.getBytes(StandardCharsets.UTF_8));
        byte[] signedBytes = privateSignature.sign();
        return Base64.encodeBase64String(signedBytes);
    }

    /**
     * RSA验签
     *
     * @param plainText 明文
     * @param signature 签名
     * @param publicKey 公钥
     * @return 是否通过验证
     * @throws Exception 异常
     */
    public static boolean verify(String plainText, String signature, PublicKey publicKey) throws Exception {
        Signature publicSignature = Signature.getInstance("SHA256withRSA");
        publicSignature.initVerify(publicKey);
        publicSignature.update(plainText.getBytes(StandardCharsets.UTF_8));
        byte[] signedBytes = Base64.decodeBase64(signature);
        return publicSignature.verify(signedBytes);
    }

    public static void main(String[] args) throws Exception {

        // 生成2048位密钥对
        KeyPair keyPair = generateKeyPair();
        System.out.println("=====公钥=====");
        System.out.println(keyPair.getPublic());
        System.out.println("=====私钥=====");
        System.out.println(keyPair.getPrivate());

        // RSA公钥加密
        System.out.println("=====测试RSA公钥加密=====");
        String testText = "测试文本";
        String encryptText = encrypt(testText, keyPair.getPublic());
        System.out.println(encryptText);

        // RSA私钥解密
        System.out.println("=====测试RSA私钥解密=====");
        String decryptText = decrypt(encryptText, keyPair.getPrivate());
        System.out.println(decryptText);

    }

}
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

# 2.2.2 Python封装

# -*- coding: utf-8 -*-

from Crypto.Cipher import PKCS1_OAEP, AES
from Crypto.PublicKey import RSA
from Crypto.Random import get_random_bytes


def create_rsa_keys(code):
    """
    生成RSA私钥和公钥
    :param code: 密码
    :return:
    """
    # 生成 2048 位的 RSA 密钥
    key = RSA.generate(2048)
    encrypted_key = key.exportKey(passphrase=code, pkcs=8, protection="scryptAndAES128-CBC")
    # 生成私钥
    with open('private_rsa_key.bin', 'wb') as f:
        f.write(encrypted_key)
    # 生成公钥
    with open('rsa_public.pem', 'wb') as f:
        f.write(key.publickey().exportKey())


def file_encryption(file_name, public_key):
    """
    文件加密
    :param file_name: 文件路径名
    :param public_key: 公钥
    :return:
    """
    # 二进制只读打开文件,读取文件数据
    with open(file_name, 'rb') as f:
        data = f.read()
    file_name_new = file_name + '.rsa'
    with open(file_name_new, 'wb') as out_file:
        # 收件人秘钥 - 公钥
        recipient_key = RSA.import_key(open(public_key).read())
        # 一个 16 字节的会话密钥
        session_key = get_random_bytes(16)
        # Encrypt the session key with the public RSA key
        cipher_rsa = PKCS1_OAEP.new(recipient_key)
        out_file.write(cipher_rsa.encrypt(session_key))
        # Encrypt the data with the AES session key
        cipher_aes = AES.new(session_key, AES.MODE_EAX)
        cipher_text, tag = cipher_aes.encrypt_and_digest(data)
        out_file.write(cipher_aes.nonce)
        out_file.write(tag)
        out_file.write(cipher_text)
    return file_name_new


def file_decryption(file_name, code, private_key):
    """
    文件解密
    :param file_name: 文件路径名
    :param code: 密码
    :param private_key: 私钥
    :return:
    """
    with open(file_name, 'rb') as f_in:
        # 导入私钥
        private_key = RSA.import_key(open(private_key).read(), passphrase=code)
        # 会话密钥, 随机数, 消息认证码, 机密的数据
        enc_session_key, nonce, tag, cipher_text = [f_in.read(x) for x in (private_key.size_in_bytes(), 16, 16, -1)]
        cipher_rsa = PKCS1_OAEP.new(private_key)
        session_key = cipher_rsa.decrypt(enc_session_key)
        cipher_aes = AES.new(session_key, AES.MODE_EAX, nonce)
        # 解密
        data = cipher_aes.decrypt_and_verify(cipher_text, tag)
    # 文件重命名
    out_file_name = file_name.replace('.rsa', '')
    with open(out_file_name, 'wb') as f_out:
        f_out.write(data)
    return out_file_name


if __name__ == '__main__':
    create_rsa_keys("test_rsa_key")
    file_encryption("test.txt", "rsa_public.pem")
    file_decryption("test.txt.rsa", "test_rsa_key", "private_rsa_key.bin")
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

# 2.3 AES加密解密

# 2.3.1 Java封装

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.Base64;

/**
 * AES加密工具类
 */
public class AesUtility {

    /**
     * 偏移量  AES 为16bytes. DES
     */
    public static final String VIPARA = "0845762876543456";

    /**
     * 编码方式
     */
    public static final String CODE_TYPE = "UTF-8";

    /**
     * 填充类型
     */
    public static final String AES_TYPE = "AES/ECB/PKCS5Padding";

    /**
     * 字符补全
     */
    private static final String[] CONSULT = new String[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B",
            "C", "D", "E", "F", "G"};

    /**
     * AES加密
     *
     * @param cleartext 明文
     * @param aesKey    密钥
     * @return 密文
     */
    public static String encrypt(String cleartext, String aesKey) {
        // 加密方式: AES128(CBC/PKCS5Padding) + Base64
        try {
            if ("AES/ECB/NoPadding".equals(AES_TYPE)) {
                cleartext = completionCodeFor16Bytes(cleartext);
            }
            aesKey = md5(aesKey);
            System.out.println(aesKey);
            // 两个参数,第一个为私钥字节数组, 第二个为加密方式 AES或者DES
            SecretKeySpec key = new SecretKeySpec(aesKey.getBytes(), "AES");
            // 实例化加密类,参数为加密方式,要写全
            // PKCS5Padding比PKCS7Padding效率高,PKCS7Padding可支持IOS加解密
            Cipher cipher = Cipher.getInstance(AES_TYPE);
            // 初始化,此方法可以采用三种方式,按加密算法要求来添加。
            // (1)无第三个参数
            // (2)第三个参数为SecureRandom random = new SecureRandom();中random对象,随机数。(AES不可采用这种方法)
            // (3)采用此代码中的IVParameterSpec 加密时使用:ENCRYPT_MODE; 解密时使用:DECRYPT_MODE;
            // CBC类型的可以在第三个参数传递偏移量zeroIv,ECB没有偏移量
            cipher.init(Cipher.ENCRYPT_MODE, key);
            // 加密操作,返回加密后的字节数组,然后需要编码。主要编解码方式有Base64, HEX,
            // UUE,7bit等等。此处看服务器需要什么编码方式
            byte[] encryptedData = cipher.doFinal(cleartext.getBytes(CODE_TYPE));
            Base64.Encoder encoder = Base64.getMimeEncoder();
            return encoder.encodeToString(encryptedData);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 解密
     *
     * @param encrypted
     * @return
     */
    public static String decrypt(String encrypted, String aesKey) {
        try {
            aesKey = md5(aesKey);
            Base64.Decoder decoder = Base64.getMimeDecoder();
            byte[] byteMi = decoder.decode(encrypted);
            SecretKeySpec key = new SecretKeySpec(aesKey.getBytes(), "AES");
            Cipher cipher = Cipher.getInstance(AES_TYPE);
            // 与加密时不同MODE:Cipher.DECRYPT_MODE
            // CBC类型的可以在第三个参数传递偏移量zeroIv,ECB没有偏移量
            cipher.init(Cipher.DECRYPT_MODE, key);
            byte[] decryptedData = cipher.doFinal(byteMi);
            String content = new String(decryptedData, CODE_TYPE);
            if ("AES/ECB/NoPadding".equals(AES_TYPE)) {
                content = resumeCodeOf16Bytes(content);
            }
            return content;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 补全字符
     * @param str
     * @return
     * @throws UnsupportedEncodingException
     */
    public static String completionCodeFor16Bytes(String str) throws UnsupportedEncodingException {
        int num = str.getBytes(CODE_TYPE).length;
        int index = num % 16;
        // 进行加密内容补全操作, 加密内容应该为 16字节的倍数, 当不足16*n字节是进行补全, 差一位时 补全16+1位
        // 补全字符 以 $ 开始,$后一位代表$后补全字符位数,之后全部以0进行补全;
        if (index != 0) {
            StringBuilder stringBuilder = new StringBuilder(str);
            if (16 - index == 1) {
                stringBuilder.append("$").append(CONSULT[16 - 1]).append(addStr(16 - 1 - 1));
            } else {
                stringBuilder.append("$").append(CONSULT[16 - index - 1]).append(addStr(16 - index - 1 - 1));
            }
            str = stringBuilder.toString();
        }
        return str;
    }

    /**
     * 追加字符
     * @param num
     * @return
     */
    public static String addStr(int num) {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < num; i++) {
            stringBuilder.append("0");
        }
        return stringBuilder.toString();
    }

    /**
     * 还原字符(进行字符判断)
     * @param str
     * @return
     */
    public static String resumeCodeOf16Bytes(String str) {
        int indexOf = str.lastIndexOf("$");
        if (indexOf == -1) {
            return str;
        }
        String trim = str.substring(indexOf + 1, indexOf + 2).trim();
        int num = 0;
        for (int i = 0; i < CONSULT.length; i++) {
            if (trim.equals(CONSULT[i])) {
                num = i;
            }
        }
        if (num == 0) {
            return str;
        }
        return str.substring(0, indexOf).trim();
    }

    /**
     * md5
     * @param dateString
     * @return
     * @throws Exception
     */
    public static String md5(String dateString) throws Exception {
        byte[] digest = MessageDigest.getInstance("md5").digest(dateString.getBytes(CODE_TYPE));
        StringBuilder md5code = new StringBuilder(new BigInteger(1, digest).toString(16));
        // 如果生成数字未满32位,需要前面补0
        for (int i = 0; i < 32 - md5code.length(); i++) {
            md5code.insert(0, "0");
        }
        return md5code.toString();
    }

    public static String sampleEncrypt(String clearText, String aesKey) {
        try {
            KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
            keyGenerator.init(128);
            Cipher cipher = Cipher.getInstance(AES_TYPE);
            cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(aesKey.getBytes(CODE_TYPE), "AES"));
            byte[] b = cipher.doFinal(clearText.getBytes(CODE_TYPE));
            return Base64.getMimeEncoder().encodeToString(b);
        } catch (Exception ex) {
            ex.printStackTrace();
            return null;
        }
    }

    public static String sampleDecrypt(String encrypted, String aesKey) {
        try {
            KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
            keyGenerator.init(128);
            Cipher cipher = Cipher.getInstance(AES_TYPE);
            cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(aesKey.getBytes(CODE_TYPE), "AES"));
            byte[] b = cipher.doFinal(Base64.getMimeDecoder().decode(encrypted));
            return new String(b, CODE_TYPE);
        } catch (Exception ex) {
            ex.printStackTrace();
            return null;
        }
    }

    public static void main(String[] args) {
        // AES加密算法
        String encrypt = sampleEncrypt("{\"username\":\"admin\",\"password\":\"12345678\"}", "bbbvccc22cabdcbaf399dffff48604fv");
        System.out.println("result:" + encrypt);
        String decrypt = sampleDecrypt("/C3gUT4IKEBdf70O7sjqupwZSRhNKiArQAynSu3+LoCKmiFIb963ZIV2QlbzuQWW", SessionHolder.getWebAesKey());
        System.out.println(decrypt);
    }

}
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

注意:AES的256位密钥加解密可能会报 java.security.InvalidKeyException: Illegal key size or default parameters 异常

报错原因:

在我们安装JRE的目录下有这样一个文件夹:%JAVE_HOME%\jre\lib\security,其中包含有两个.jar文件:“local_policy.jar ”和“US_export_policy.jar”。JRE中自带的“local_policy.jar ”和“US_export_policy.jar”是支持128位密钥的加密算法,而当我们要使用256位密钥算法的时候,已经超出它的范围,无法支持,所以才会报:“java.security.InvalidKeyException: Illegal key size or default parameters”的异常。
1

解决办法:

方法一:下载单独策略文件,替换原来的文件。

方法二:将jdk升级到JDK1.8.0-161以上版本,就不需要单独策略文件了。

# 2.3.2 Python封装

# -*- coding: utf-8 -*-

import base64
from Crypto.Cipher import AES

'''
采用AES对称加密算法
'''

# str不是16的倍数那就补足为16的倍数
def add_to_16(value):
    while len(value) % 16 != 0:
        value += '\0'
    return str.encode(value)  

# 加密方法
def encrypt_file(key, input_file_path, encoding, output_file_path):
    # 一次性读取文本内容
    with open(input_file_path, 'r', encoding=encoding) as f:
        # print(text) 测试打印读取的数据
        # 待加密文本
        mystr = f.read()
    text = base64.b64encode(mystr.encode('utf-8')).decode('ascii')
    # 初始化加密器
    aes = AES.new(add_to_16(key), AES.MODE_ECB)
    # 先进行aes加密
    encrypt_aes = aes.encrypt(add_to_16(text))
    # 用base64转成字符串形式
    encrypted_text = str(base64.encodebytes(encrypt_aes), encoding='utf-8')  # 执行加密并转码返回bytes
    # print(encrypted_text) 测试打印加密数据
    # 写入加密数据到文件
    with open(output_file_path, "w") as bankdata:
        bankdata.write(encrypted_text)

# 解密方法
def decrypt_file(key, file_path, encoding):
    # 密文
    with open(file_path, 'r', encoding=encoding) as f:
        # print(text) 测试打印读取的加密数据
        # 待解密文本
        text = f.read()
    # 初始化加密器
    aes = AES.new(add_to_16(key), AES.MODE_ECB)
    # 优先逆向解密base64成bytes
    base64_decrypted = base64.decodebytes(text.encode(encoding='utf-8'))
    # bytes解密
    decrypted_text = str(aes.decrypt(base64_decrypted),encoding='utf-8')
    decrypted_text = base64.b64decode(decrypted_text.encode('utf-8')).decode('utf-8')
    print(decrypted_text)
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

# 2.4 数据传输加密方案

# 2.4.1 约定密钥使用AES加密

这种方案要求服务端和客户端约定好加密的AES密钥,在两端分别保留,在处理数据时就用约定好的AES密钥对数据进行加解密。

该方案能应付一些对安全要求不是很高的项目。由于AES密钥是分别放在两端的,客户端并不可靠,别有用心之人是能够获取到AES密钥的,这时就能通过抓包软件篡改传输的数据然后再次加密发送给服务端了,而此时,服务端是不知道数据是被篡改过的。

# 2.4.2 使用AES结合RSA进行加密

RSA加密虽然可靠,但其加密速度较慢,并且不适合对大量数据进行加密,因此单纯的使用RSA加密肯定是不可取的。而AES的加密速度快且对加密数据没有大小限制,但是AES密钥的传输及管理是个问题。可以将二者结合起来使用,用AES加密数据包,用RSA加密随机的AES密钥,这样就可以取长补短了。

以客户端给服务端传输数据为例:

  • 服务端生成一对RSA密钥,其中RSA私钥放在服务端(务必不可泄露),RSA公钥下发给客户端。
  • 客户端使用随机函数生成AES加密要用的AES密钥。
  • 客户端使用随机的AES密钥对要传输的数据进行AES加密。
  • 使用服务端给的RSA公钥对客户端随机生成的AES密钥进行加密。
  • 客户端将使用AES加密的数据以及使用RSA公钥加密的随机AES密钥一起发送给服务端。
  • 服务端拿到数据后,先使用RSA私钥对加密的随机AES密钥进行解密,解密成功即可确定是客户端发来的数据,没有经过他人修改,然后使用解密成功的AES密钥对使用AES加密的数据进行解密,获取最终的数据。

该方案由于服务端的RSA私钥被人是拿不到的,也就没办法解密你要传给服务端的数据,想篡改数据后再提交给服务端就也就做不到了。这种方案实际上已经很安全了,实现了对客户端身份的单向认证以及对传输数据的加密,基本上可以应付绝大部分的项目了。如果还想要加强安全性,可以采用“RSA结合AES实现双向验证”的方案。

# 3. 密码及敏感信息相关

有关登录认证及应用开发的安全防范,详见我的另一篇博客:安全框架Spring-Security基本使用 (opens new window)

# 3.1 弱密码验证及强密码生成

# 3.1.1 弱密码验证

常见场景:系统超级管理员账号初始化、用户注册账号、用户修改密码等

解决方案:在前后端使用正则验证

校验弱密码的正则表达式如下:

^(?=.*\d)(?=.*[a-zA-Z])(?=.*[~!@#$%^&*])[\da-zA-Z~!@#$%^&*]{8,16}$
1

含义解释:必须8-16位,必须带有特殊字符,必须带有数字,必须带有英文(大写或者小写均可)

# 3.1.2 强密码生成

常见场景:给中间件及数据库设置密码,AES密钥的生成,系统超级管理员账号的初始化等

解决方案:生成强密码

第一种方式:使用系统自带的命令生成(支持MacOS、Linux)

$ openssl rand -hex 32
1

第二种方式:使用Java代码生成

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * 生成指定位数的随机密码,密码中需要包含大写字母、小写字母、数字和特殊字符。
 */
public class RandPassword {
    public static final char[] allowedSpecialCharactors = {
            '`', '~', '@', '#', '$', '%', '^', '&',
            '*', '(', ')', '-', '_', '=', '+', '[',
            '{', '}', ']', '\\', '|', ';', ':', '"',
            '\'', ',', '<', '.', '>', '/', '?'};//密码能包含的特殊字符
    private static final int letterRange = 26;
    private static final int numberRange = 10;
    private static final int spCharactorRange = allowedSpecialCharactors.length;
    private static final SecureRandom random = new SecureRandom();
    private int passwordLength;//密码的长度
    private int minVariousType;//密码包含字符的最少种类

    public RandPassword(int passwordLength, int minVariousType) {
        if (minVariousType > CharactorType.values().length) minVariousType = CharactorType.values().length;
        if (minVariousType > passwordLength) minVariousType = passwordLength;
        this.passwordLength = passwordLength;
        this.minVariousType = minVariousType;
    }

    public String generateRandomPassword() {
        char[] password = new char[passwordLength];
        List<Integer> pwCharsIndex = new ArrayList();
        for (int i = 0; i < password.length; i++) {
            pwCharsIndex.add(i);
        }
        List<CharactorType> takeTypes = new ArrayList(Arrays.asList(CharactorType.values()));
        List<CharactorType> fixedTypes = Arrays.asList(CharactorType.values());
        int typeCount = 0;
        while (pwCharsIndex.size() > 0) {
            int pwIndex = pwCharsIndex.remove(random.nextInt(pwCharsIndex.size()));//随机填充一位密码
            Character c;
            if (typeCount < minVariousType) {//生成不同种类字符
                c = generateCharacter(takeTypes.remove(random.nextInt(takeTypes.size())));
                typeCount++;
            } else {//随机生成所有种类密码
                c = generateCharacter(fixedTypes.get(random.nextInt(fixedTypes.size())));
            }
            password[pwIndex] = c.charValue();
        }
        return String.valueOf(password);
    }

    private Character generateCharacter(CharactorType type) {
        Character c = null;
        int rand;
        switch (type) {
            case LOWERCASE://随机小写字母
                rand = random.nextInt(letterRange);
                rand += 97;
                c = new Character((char) rand);
                break;
            case UPPERCASE://随机大写字母
                rand = random.nextInt(letterRange);
                rand += 65;
                c = new Character((char) rand);
                break;
            case NUMBER://随机数字
                rand = random.nextInt(numberRange);
                rand += 48;
                c = new Character((char) rand);
                break;
            case SPECIAL_CHARACTOR://随机特殊字符
                rand = random.nextInt(spCharactorRange);
                c = new Character(allowedSpecialCharactors[rand]);
                break;
        }
        return c;
    }

    public static void main(String[] args) {
        System.out.println(new RandPassword(32, 4).generateRandomPassword());
    }
}

enum CharactorType {
    LOWERCASE,
    UPPERCASE,
    NUMBER,
    SPECIAL_CHARACTOR
}
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

# 3.2 禁止密码明文传输

常见场景:用户注册、用户登录、修改密码等

解决方案:使用加密算法进行加密

在进行包含密码的操作时,前后端必须对数据进行加密,通常在业务系统里我们选用AES对称加密算法,前端将请求包整个加密成一个字符串,以body的形式传输,后端进行解密及验证。入库的时候还要对密码单独进行加密存储。

# 3.3 中间件服务设置密码

对于ElasticSearch、Redis这类默认不要求设置密码的中间件,经常会有人在部署的时候不设置密码,这样非常不安全,务必要设置上。

# 3.3.1 ElasticSearch设置密码

部署ElasticSearch

$ docker pull elasticsearch:7.16.2
$ docker run -d --name es \
-p 9200:9200 -p 9300:9300 \
-v /root/docker/es/data:/usr/share/elasticsearch/data \
-v /root/docker/es/config:/usr/share/elasticsearch/config \
-v /root/docker/es/plugins:/usr/share/elasticsearch/plugins \
-e "discovery.type=single-node" -e ES_JAVA_OPTS="-Xms1g -Xmx1g" \
elasticsearch:7.16.2
$ docker update es --restart=always
1
2
3
4
5
6
7
8
9

注意事项:

1)Elasticsearch请选择7.16.0之后的版本,之前的所有版本都使用了易受攻击的 Log4j2版本,存在严重安全漏洞。

2)ES_JAVA_OPTS="-Xms1g -Xmx1g"只是一个示例,内存设置的少了会导致数据查询速度变慢,具体设置多少要根据业务需求来定,一般而言公司的实际项目要设置8g内存以上。

开启xpack并设置密码

进入容器进行配置

$ docker exec -it es /bin/bash 
$ cd config
$ chmod o+w elasticsearch.yml
$ vi elasticsearch.yml
1
2
3
4

其中,在 elasticsearch.yml 文件的末尾添加如下配置

xpack.security.enabled: true    
1

然后把权限修改回来,重启容器,设置账号密码,浏览器访问http://IP:9200地址即可(用 elastic账号 和自己设置的密码登录即可)

$ chmod o-w elasticsearch.yml
$ exit
$ docker restart es
$ docker exec -it es /bin/bash 
$ ./bin/elasticsearch-setup-passwords interactive   // 然后设置账号密码
1
2
3
4
5

# 3.3.2 Redis设置密码

$ docker pull redis:3.2.8
$ docker run --name redis -p 6379:6379 -d redis:3.2.8 --requirepass "your_password"
$ docker update redis --restart=always
1
2
3

# 3.4 敏感信息脱敏

常见场景:用户注册、用户信息修改等

解决方案:敏感信息数据脱敏

针对身份证号、姓名、手机号、邮箱、车牌号等等敏感信息,入库之前要对数据进行脱敏,以防一旦敏感泄露数据造成不可挽回的后果。敏感信息脱敏可以使用如下工具类来实现:

import org.apache.commons.lang3.StringUtils;

/**
 * 字段脱敏工具类
 */
public class SensitiveFieldUtils {

    /**
     * @param userName 名字
     * @return 脱敏结果
     */
    public static String chineseName(String userName) {
        if (StringUtils.isEmpty(userName)) {
            return "";
        }
        String name = StringUtils.left(userName, 1);
        return StringUtils.rightPad(name, StringUtils.length(userName), "*");
    }

    /**
     * @param idCard 身份证号
     * @return 脱敏结果
     */
    public static String idCard(String idCard) {
        if (StringUtils.isEmpty(idCard)) {
            return "";
        }
        String id = StringUtils.right(idCard, 4);
        return StringUtils.leftPad(id, StringUtils.length(idCard), "*");
    }

    /**
     * @param phone 手机号
     * @return 脱敏结果
     */
    public static String telephone(String phone) {
        if (StringUtils.isEmpty(phone)) {
            return "";
        }
        return StringUtils.left(phone, 3).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(phone, 4), StringUtils.length(phone), "*"), "***"));
    }

    /**
     * @param address 地址信息
     * @param sensitiveSize 敏感信息长度
     * @return 脱敏结果
     */
    public static String address(String address, int sensitiveSize) {
        if (StringUtils.isBlank(address)) {
            return "";
        }
        int length = StringUtils.length(address);
        return StringUtils.rightPad(StringUtils.left(address, length - sensitiveSize), length, "*");
    }

    /**
     * @param email 邮箱
     * @return 脱敏结果
     */
    public static String email(String email) {
        if (StringUtils.isBlank(email)) {
            return "";
        }
        int index = StringUtils.indexOf(email, "@");
        if (index <= 1) {
            return email;
        } else {
            return StringUtils.rightPad(StringUtils.left(email, 1), index, "*").concat(StringUtils.mid(email, index, StringUtils.length(email)));
        }
    }


    /**
     * @param cardNum 银行卡号
     * @return 脱敏结果
     */
    public static String bankCard(String cardNum) {
        if (StringUtils.isBlank(cardNum)) {
            return "";
        }
        return StringUtils.left(cardNum, 6).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(cardNum, 4), StringUtils.length(cardNum), "*"), "******"));
    }

    public static void main(String[] args) {

        // 测试姓名脱敏(张**)
        String name = chineseName("张三丰");
        System.out.println("name = " + name);

        // 测试身份证号脱敏(**************7812)
        String idCard = idCard("123456781234567812");
        System.out.println("idCard = " + idCard);

        // 测试手机号脱敏(186****0000)
        String telephone = telephone("18600000000");
        System.out.println("telephone = " + telephone);

        // 测试地址脱敏(天津市滨海新区*********)
        String address = address("天津市滨海新区经济开发区第三大街", 9);
        System.out.println("address = " + address);

        // 测试邮箱脱敏(t***@163.com)
        String email = email("[email protected]");
        System.out.println("email = " + email);

        // 测试银行卡号脱敏(622316******6887)
        String bankCard = bankCard("6223165905596887");
        System.out.println("bankCard = " + bankCard);
        
    }

}
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

# 4. Java和Python加密源码打包

需求情景:有些项目交付时,只是让对方可以私有化部署,但不交付源码。该情形下,打包时可以对其进行加密混淆,或者将其进行编译,可避免源码泄漏或字节码被反编译。

# 4.1 Jar包加密打包

ClassFinal是一款java class文件加密工具,支持直接加密jar包或war包,无需修改任何项目代码,兼容spring-framework,可避免源码泄漏或字节码被反编译。

使用方法: 使用如下命令对jar包进行加密即可,生成的 yourproject-encrypted.jar 就是加密后的jar文件,加密后的文件不可直接执行,需要配置javaagent。

$ java -jar classfinal-fatjar.jar -file yourproject.jar -libjars a.jar,b.jar -packages com.yourpackage,com.yourpackage2 -exclude com.yourpackage.Main -pwd 123456 -Y

参数说明
-file        加密的jar/war完整路径
-packages    加密的包名(可为空,多个用","分割)
-libjars     jar/war包lib下要加密jar文件名(可为空,多个用","分割)
-cfgfiles    需要加密的配置文件,一般是classes目录下的yml或properties文件(可为空,多个用","分割)
-exclude     排除的类名(可为空,多个用","分割)
-classpath   外部依赖的jar目录,例如/tomcat/lib(可为空,多个用","分割)
-pwd         加密密码,如果是#号,则使用无密码模式加密
-code        机器码,在绑定的机器生成,加密后只可在此机器上运行
-Y           无需确认,不加此参数会提示确认以上信息
1
2
3
4
5
6
7
8
9
10
11
12

使用实例:加密后的项目需要设置javaagent来启动,项目在启动过程中解密class,完全内存解密,不留下任何解密后的文件。

$ java -jar classfinal-fatjar-1.2.1.jar -file web_manage-0.0.1.jar -packages com.yoyo.admin.web_manage -pwd 123456 -Y // 对Jar加密
$ java -javaagent:web_manage-0.0.1-encrypted.jar='-pwd 123456’ -jar web_manage-0.0.1-encrypted.jar   // 启动加密Jar
1
2

如果将jar包解压出来查看源码,会发现里面的具体实现全都没了,拿不到多少有用的信息。

ClassFinal加密后的效果

# 4.2 Python源码加密打包

编译动态链接库的方式,不会被逆向破解,但是由于Cython的编译要求严格,可能需要改源码,而且有些复杂的项目编译后的引用会出问题。源码混淆的方式,容易被逆向破解,但复杂项目里不太会出问题(命名替换什么的就算了,一堆文件之间相互关联,都没换成同一个,到处都是爆红),也更容易适配系统差异。

# 4.2.1 Python编译动态链接库

可以将 Python 代码编译成动态链接库,这样就不会被逆向破解拿到源码了,需要安装Cython库。

$ pip3 install Cython
1

compile.py

# -*- coding: utf-8 -*-

import os
import shutil
import tempfile
from setuptools import setup, Extension
from Cython.Build import cythonize

# 指定你的源文件目录
source_dir = './fast-text-rank'
output_dir = source_dir + '_build'

# 创建输出目录
os.makedirs(output_dir, exist_ok=True)

# 创建临时目录
with tempfile.TemporaryDirectory() as temp_dir:
    temp_source_dir = os.path.join(temp_dir, 'src')

    # 递归复制文件夹并保留目录结构
    shutil.copytree(source_dir, temp_source_dir, ignore=shutil.ignore_patterns('*.pyc', '__pycache__'))

    # 找到所有的 .py 文件并创建一个扩展对象列表
    extensions = []
    for root, dirs, files in os.walk(temp_source_dir):
        for filename in files:
            if filename.endswith('.py'):
                abs_file_path = os.path.join(root, filename)
                relative_file_path = os.path.relpath(abs_file_path, temp_source_dir)
                module_name = os.path.splitext(relative_file_path.replace(os.path.sep, '.'))[0]
                pyx_file_path = abs_file_path + 'x'  # Append 'x' to make .pyx extension
                os.rename(abs_file_path, pyx_file_path)
                extensions.append(Extension(module_name, [pyx_file_path]))

    # 编写 setup.py 文件内容并构建
    setup(
        ext_modules=cythonize(extensions, compiler_directives={'language_level': "3"}),
        script_args=["build_ext", "--build-lib", output_dir],
    )

    # 复制临时目录中编译生成的文件到输出目录
    for root, dirs, files in os.walk(temp_dir):
        for filename in files:
            if filename.endswith('.so') or filename.endswith('.pyd'):
                # 相对于临时源目录的路径
                relative_path = os.path.relpath(root, temp_source_dir)
                # 构建目标目录
                dest_dir = os.path.join(output_dir, relative_path)
                os.makedirs(dest_dir, exist_ok=True)
                shutil.move(os.path.join(root, filename), dest_dir)

# 拷贝源目录下的非 .py 文件到输出目录,并保留目录结构
for root, dirs, files in os.walk(source_dir):
    for item in dirs + files:
        src_path = os.path.join(root, item)
        dst_path = src_path.replace(source_dir, output_dir, 1)
        if os.path.isdir(src_path):
            os.makedirs(dst_path, exist_ok=True)
        elif not item.endswith('.py') and not item.endswith('.pyc') and not os.path.exists(dst_path):
            shutil.copy2(src_path, dst_path)
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

适用情形:编译成动态链接库的 Python 代码不能直接执行,因为它们不是独立的可执行程序,因此需要一个主程序去调用。在原来主程序的同级目录新建一个main.py,引用一下原来主程序模块,写一下if __name__ == '__main__':即可,内容如下:

# -*- coding: utf-8 -*-

# 导入编译后的server模块
import server

# 从server模块中获取Flask实例并运行它
if __name__ == '__main__':
    server.app.run(host='0.0.0.0', port=4999, debug=False, threaded=True)
1
2
3
4
5
6
7
8

注意事项:如果原来主程序的if __name__ == '__main__':里写了需要外部依赖的代码,可以把原来这里的东西都写到 def main():里,统一成这种格式。

Python编译成动态链接库打包

# 4.2.2 Python混淆源码

Python混淆源码的库有很多,我几乎都试遍了,基本都是些玩具项目。简单项目用起来还行,项目复杂了之后,各个文件之前相互关联,混淆后就跑不起来了。

最后为了实现这个复杂项目的源码混淆,手写了一个脚本,主要包含压缩代码内容、添加垃圾代码、转换成一行显示等操作。这个脚本的混淆程度不太够,有点儿技术背景的人可以轻易还原回来。

$ pip3 install python-minifier      //  Python源码压缩库:https://github.com/dflook/python-minifier
1

code_mixture.py

# -*- coding: utf-8 -*-

import os
import shutil
import logging
import re
import random
from python_minifier import minify

# 日志记录
logging.basicConfig(filename='code_minifier.log', level=logging.INFO,
                    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)


# 文件编码转换并添加运行时解码器
def encode_with_runtime_decoder(content):
    # 将代码转换为一个长字符串
    encoded_content = repr(content)
    # 创建一个字符串,其中包含了执行原始代码的逻辑
    decoder = f"exec({encoded_content})\n"
    return decoder


# 生成随机垃圾代码
def generate_garbage_code():
    patterns = [
        # 列表推导式生成随机列表
        f"[{random.randint(0, 100)} for _ in range({random.randint(1, 3)})]\n",
        # 随机生成一个整数,但不使用它
        f"_unused_var_{random.randint(100, 999)} = {random.randint(100, 999)}\n",
        # 使用条件语句,确保有一个有效的代码块
        f"if {random.randint(0, 1)} == 0: pass\n",
        # 生成一个随机的函数定义
        f"def _useless_function_{random.randint(100, 999)}(): pass\n",
        # 生成一个随机的 try-except 语句
        f"try:\n    pass\nexcept Exception as e:\n    pass\n",
        # 随机生成一个 lambda 表达式
        f"_lambda_{random.randint(100, 999)} = lambda x: x + {random.randint(0, 100)}\n",
        # 生成一个随机的字符串赋值
        f"_random_string_{random.randint(100, 999)} = 'string{random.randint(0, 100)}'\n",
        # 生成一个随机的字典赋值
        f"_random_dict_{random.randint(100, 999)} = {{'key': {random.randint(0, 100)}}}\n",
    ]
    # 生成多行垃圾代码,每行随机选择一种模式
    lines = [random.choice(patterns) for _ in range(random.randint(10, 100))]
    return ''.join(lines)


# 在源代码末尾插入垃圾代码
def insert_garbage_code_at_end(source_code):
    garbage_code = generate_garbage_code()
    return source_code + '\n' + garbage_code


def process_directory(source_dir, target_dir, whitelist=None, skip_patterns=None):
    logging.info("开始混淆处理过程")

    # 确保目标目录存在
    if not os.path.exists(target_dir):
        os.makedirs(target_dir)

    # 将白名单中的路径转换为绝对路径
    if whitelist is not None:
        whitelist = [os.path.abspath(path) for path in whitelist]

    # 编译跳过的正则表达式
    if skip_patterns is not None:
        skip_patterns = [re.compile(pattern) for pattern in skip_patterns]

    # 获取总文件数量
    total_files = sum([len(files) for r, d, files in os.walk(source_dir)])
    processed_files = 0

    # 遍历原始目录中的所有文件
    for root, dirs, files in os.walk(source_dir):
        for file in files:
            source_file = os.path.join(root, file)
            target_file = os.path.join(target_dir, os.path.relpath(source_file, source_dir))
            absolute_source_file = os.path.abspath(source_file)

            # 检查是否应该跳过文件
            if any(pattern.search(absolute_source_file) for pattern in skip_patterns):
                continue
            # 确保目标文件的目录存在
            os.makedirs(os.path.dirname(target_file), exist_ok=True)

            # 更新处理进度
            processed_files += 1
            logger.info(f"处理文件 {source_file} ({processed_files}/{total_files})")

            # 检查文件是否在白名单中或是否为Python文件
            if absolute_source_file in whitelist or not file.endswith(".py"):
                # 白名单中的文件或非Python文件,直接拷贝
                shutil.copy2(source_file, target_file)
            else:
                try:
                    with open(source_file, 'r') as f:
                        original_content = f.read()
                    # 添加垃圾代码
                    source_code_with_garbage = insert_garbage_code_at_end(original_content)
                    # 文件编码转换
                    content_with_decoder = encode_with_runtime_decoder(source_code_with_garbage)
                    # 代码内容压缩
                    obfuscated_code = minify(content_with_decoder, remove_literal_statements=True)
                    with open(target_file, 'w') as f:
                        f.write(obfuscated_code)
                except Exception as e:
                    # 混淆过程中出现异常,直接拷贝文件
                    shutil.copy2(source_file, target_file)
                    logger.error(e)
                    logger.error(f"Failed to obfuscate {source_file}. File copied without obfuscation.")

    logger.info("混淆处理过程完成。")


if __name__ == '__main__':
    source_dir = "/root/your_project"
    target_dir = "/root/your_project_mixture"
    # 白名单,跳过混淆处理,拷贝到target_dir
    whitelist = [
        "{}{}".format(source_dir, "/test.py")
    ]
    # 放弃拷贝到target_dir,正则格式
    skip_patterns = [
        r"requirements(-.*)?\.txt",
        r".*__pycache__.*",
        r".*\.idea",
    ]
    process_directory(source_dir, target_dir, whitelist, skip_patterns)
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

# 5. 系统部署安全注意事项

本节主要是与系统部署有关的安全防范,此处仅简要介绍,具体见我的其他博客:VPS基本部署环境的搭建与配置 (opens new window)Docker容器化及项目环境管理 (opens new window)

# 5.1 服务器安全

# 5.1.1 开启防火墙并设置合理的规则

开启防火墙,并配置合理的iptables,ufw防火墙规则来保证系统层面的安全,主要注意的是docker自带防火墙,可能会与系统防火墙冲突。除此之外,不放行非必要的端口,必要时要设置IP白名单与IP黑名单来进行限制也是非常有必要的。

ufw防火墙常用命令

$ sudo ufw enable      // 启动ufw防火墙
$ sudo ufw disable     // 关闭ufw防火墙
$ sudo ufw status      // 查看ufw防火墙状态
$ sudo ufw allow 443   // 允许外部访问443端口
$ sudo ufw deny 443    // 禁止外部访问443端口
1
2
3
4
5

# 5.1.2 设置IP白名单限制访问

对于一些面向B端的保密项目,可以使用IP白名单策略限制访问。如果客户的IP不是固定的,可以考虑提供VPN的方式给他们连接。

# 5.2 中间件安全

# 5.2.1 使用安全版本的中间件

有些版本的中间件有安全漏洞,比如7.16.0之前的ES、1.20.0之前的Nginx,在部署中间件的时候要使用当下安全的版本,后续如果曝出安全漏洞,要及时升级,但也要注意不同版本的兼容性问题。

# 5.2.2 使用Shell脚本对数据定期备份

对存储在MySQL、Oracle、ElasticSearch等数据库及中间件的业务数据,为了防止意外发生,应该在服务器上配置shell脚本,定期对数据进行备份。

具体详见我的另一篇博客:常用服务的数据备份及同步专题 (opens new window)

# 5.3 应用服务安全

# 5.3.1 设置Docker Network

默认docker之间的网络不互通,如果需要其互相连接,则需要配置docker network。配置完之后,这些docker之间便可使用hostname直接本地访问,不对外提供服务,保证系统的安全性。

$ docker network create [network_name]                            // 创建网络
$ docker network ls                                               // 查看已创建的网络列表
$ docker network inspect [network_name]                           // 查看具体的网络详情
$ docker network connect [network_name] [CONTAINER ID/NAMES]      // 将容器加入网络,或者 docker run 时加 --network 进行指定
$ docker network disconnect [network_name] [CONTAINER ID/NAMES]   // 将容器移除网络
$ docker network rm [network_name]                                // 删除具体的网络
1
2
3
4
5
6

# 5.3.2 申请SSL证书开启HTTPS

SSL证书是一种数字证书,用于加密从用户的浏览器发送到Web服务器的数据。 通过这种方式,发送的数据对于使用Wireshark等数据包嗅探器来拦截和窃听您的通信的黑客来说是安全的。

Chrome一直在推动https,所有的http协议网站被标记为不安全,如果再不对网站进行https改造的话,那么可能会对信任度造成一定的影响,所以说对一个面向用户的网站来说,开启https是非常有必要的。

# 5.3.3 开启CDN服务

CDN的全称是Content Delivery Network,即内容分发网络。 CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN的关键技术主要有内容存储和分发技术。

通过使用 CDN 服务提供的全球节点,一方面可以提高网站响应速度和性能,节省源站资源;另一方面也可以保护站点抵御攻击,保证网站长期稳定在线。

# 6. 参考资料

[1] docker搭建elasticsearch6.8.7并开启x-pack认证 from 程序员宅基地 (opens new window)

[2] SpringBoot 关闭druid的页面 from CSDN (opens new window)

[3] AES的256位密钥加解密报 java.security.InvalidKeyException: Illegal key size or default parameters from CSDN (opens new window)

[4] Java字段脱敏处理 from 稀土掘金 (opens new window)

[5] Nessus+AWVS---Docker破解版安装 from Mari0er's Blog (opens new window)

[6] Nessus忘记用户名密码怎么办 from CSDN (opens new window)

[7] nessus新建扫描任务后,显示500内部服务器错误 from Github Issues (opens new window)

[8] 使用Nessus进行漏洞扫描的过程 from Chris Chan's BLOG (opens new window)

[9] Nessus 主机漏洞扫描器安装、配置、使用 from 51CTO (opens new window)

[10] 网络传输数据加解密方案选择(RSA+AES)from CSDN (opens new window)

[11] Nesses v8.x永久破解方法 from Jok3r's Blog (opens new window)

[12] 什么是 Log4j 漏洞 from IBM (opens new window)

[13] 保护您的Python脚本,将其加密为.pye并在导入时解密 from Github (opens new window)

[14] 将Python源代码转换为其最紧凑的表示形式 from Github (opens new window)

[15] Nessus 扫描web服务 from 稀土掘金 (opens new window)

Last Updated: 10/6/2024, 4:56:30 PM