LDAP 密码加密方式初探

最近在登录公司内部的一个管理系统时,总是弹出对话框提示证书过期、是否继续云云,今天突发奇想,这个登录过程该不会没有加密传输吧?

先用 Wireshark 抓包看看,果然是未加密的 HTTP 连接,在其中一个访问 /LogicService.asmx 的 POST 请求中,可以看到如下的 XML 内容:

<?xml version="1.0" encoding="utf-8" ?>
<soap:Envelope
    xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"
    xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"
    xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">
    <soap:Body>
        <EncryptLogin xmlns=\"http://tempuri.org/\">
            <UserID>philip_ye</UserID>
            <password>4r4g8OovaDRiiijWlEC5AQ==</password>
            <ip>192.1.1.110.11.151.164</ip>
        </EncryptLogin>
    </soap:Body>
</soap:Envelope>

其中的 password 字段是我用「admin」作为密码输入登录界面再抓包得到的值,密码的密文 4r4g8OovaDRiiijWlEC5AQ== 最后有两个等号,应该是 base64 编码后的结果,尝试着解码看看:

$ printf 4r4g8OovaDRiiijWlEC5AQ== | base64 -d | hexdump -C
00000000  e2 be 20 f0 ea 2f 68 34  62 8a 28 d6 94 40 b9 01  |.. ../h4b.(..@..|

得到一个 128bit 的值:e2be20f0ea2f6834628a28d69440b901,这个值看着又像是 MD5 的结果,而 MD5 运算在通常的应用中是不可逆的,因此对「admin」做 MD5 运算来验证:

$ printf admin | md5sum
21232f297a57a5a743894a0e4a801fc3  -

得到的结果是 21232f297a57a5a743894a0e4a801fc3,与 e2be20f0ea2f6834628a28d69440b901 不相等,显然没有这么简单。

根据公司的这个管理系统客户端的默认图标来看,应该是 .NET Framework 写的。根据 StackOverflow 上这个问答找来 .NET Reflector 对 EXE 文件反编译,整个管理系统客户端的源代码赫然眼前。当年通过反编译学习某些功能实现原理的日子再次上演了。

找到「登录」按钮的点击事件处理函数,其中有这么一条语句:

bool flag = manage.EncryptLogin(userID, SingleEncode.Encode(text), ip);

其中的 text 就是从密码框中获取到的明文密码,也就是我之前输入的「admin」,而 SingleEncode.Encode() 就是对密码加密的接口了。

继续查看 SingleEncode.Encode() 的实现:

public static string Encode(string s) => 
    Convert.ToBase64String(GetBytesFromStream(GetEncodeStream(Encoding.ASCII.GetBytes(s))));

可以看出,整个加密过程确实是先加密之后再使用 base64 编码的。继续看 GetEncodeStream() 的实现(Encoding.ASCII.GetBytes()GetBytesFromStream() 都是系统接口):

private static MemoryStream GetEncodeStream(byte[] bytes)
{
    MemoryStream stream = new MemoryStream();
    CryptoStream stream2 = new CryptoStream(stream, GetEncoder(), CryptoStreamMode.Write);
    stream2.Write(bytes, 0, bytes.Length);
    stream2.FlushFinalBlock();
    return stream;
}

重点在 GetEncoder()

private static ICryptoTransform GetEncoder()
{
    RijndaelManaged managed = new RijndaelManaged();
    return managed.CreateEncryptor(Key, IV);
}

至此可以看出,采用的是 Rijndael 加密算法,也就是大名鼎鼎的 AES 加密算法。Rijndael 与 AES 的区别可参考 The Differences Between Rijndael and AES

RijndaelManaged 是系统类,入参 KeyIV 可在 MSDN 上 RijndaelManaged 类的说明文档中找到:

  • IV: Gets or sets the initialization vector (IV) for the symmetric algorithm.(Inherited from SymmetricAlgorithm.)
  • Key: Gets or sets the secret key for the symmetric algorithm.(Inherited from SymmetricAlgorithm.)

GetEncoder() 所在的 SingleEncode 类的构造函数中,对 KeyIV 进行初始化赋值:

static SingleEncode()
{
    Key = new byte[] {
        0xd8, 0xd5, 0x19, 0x48, 0xea, 0xae, 0x74, 0x84,
        0xe1, 0x15, 0x5e, 0xd0, 0x54, 0xf7, 0x69, 0x9b,
        0x77, 0x00, 0xc4, 0x35, 0x78, 0xbb, 0x99, 0x62,
        0x20, 0xb9, 0xed, 0x7f, 0xf9, 0x76, 0x6d, 0x16
    };
    IV = new byte[] {
        0x46, 0x3e, 0xa9, 0x70, 0x1f, 0xb5, 0xaa, 0x7a,
        0x0f, 0xa6, 0x59, 0xc1, 0x62, 0xef, 0x0a, 0xf6
    };
}

为避免我司信息安全事故,上述 Key 和 IV 已经过修改,并非实际使用的值。

可以看到 Key 为 32 字节(256-bit),IV 为 16 字节。由 mcrypt(3) 得知,需要 IV 的只有 CBC 模式和 CFB 模式,而从明文「admin」长度 5 字节、密文长度 32 字节、Key 长度 32 字节这点来判断,使用的应该是 CBC 模式(CBC 模式的密文长度与 Key 长度相等,而 CFB 模式的密文长度与明文长度相等)。也就是说,具体加密算法应该是 AES-256-CBC。

将上述的 KeyIV 拼接后传入 openssl 命令验证一下(openssl 命令行使用说明详见 OpenSSL Wiki 页面):

$ printf admin | openssl enc -aes-256-cbc -K d8d51948eaae7484e1155ed054f7699b7700c43578bb996220b9ed7ff9766d16 -iv 463ea9701fb5aa7a0fa659c162ef0af6 | hexdump -C
00000000  e2 be 20 f0 ea 2f 68 34  62 8a 28 d6 94 40 b9 01  |.. ../h4b.(..@..|

Bingo!对「admin」使用 AES-256-CBC 加密得到的值正是对抓包看到的 4r4g8OovaDRiiijWlEC5AQ== 进行 base64 解码之后的值: e2be20f0ea2f6834628a28d69440b901

把整个加密过程串起来:

$ printf admin | openssl enc -aes-256-cbc -K d8d51948eaae7484e1155ed054f7699b7700c43578bb996220b9ed7ff9766d16 -iv 463ea9701fb5aa7a0fa659c162ef0af6 | base64
4r4g8OovaDRiiijWlEC5AQ==

要知道,AES 是对称加密算法,也就是说,知道了 KeyIV,是可以将密文解密成明文的:

$ printf 4r4g8OovaDRiiijWlEC5AQ== | base64 -d | openssl enc -d -aes-256-cbc -K d8d51948eaae7484e1155ed054f7699b7700c43578bb996220b9ed7ff9766d16 -iv 463ea9701fb5aa7a0fa659c162ef0af6
admin

最后,把加密和解密的过程写成一个简单的脚本,命名为 ldap-password.sh:

#!/bin/sh

KEY=d8d51948eaae7484e1155ed054f7699b7700c43578bb996220b9ed7ff9766d16
IV=463ea9701fb5aa7a0fa659c162ef0af6

help() {
    echo "Encryption: $0 enc [PASSWORD_IN_PLAIN_TEXT]"
    echo "Decryption: $0 dec [PASSWORD_IN_CIPHER]"
}

if [ $# -ne "2" ]
then
    help
elif [ "$1" == "enc" ]
then
    printf $2 | openssl enc -aes-256-cbc -K $KEY -iv $IV | base64
elif [ "$1" == "dec" ]
then
    printf $2 | base64 -d | openssl enc -d -aes-256-cbc -K $KEY -iv $IV
    printf "\n"
else
    help
fi

这样加解密就方便多了:

$ ./ldap-password.sh enc admin
4r4g8OovaDRiiijWlEC5AQ==
$ ./ldap-password.sh dec 4r4g8OovaDRiiijWlEC5AQ==
admin

小结

  • 登录时与服务器的传输是使用 HTTP 明文传输的。
  • 密码字段是加密的,先对密码的明文使用 AES-256-CBC 加密,再使用 base64 编码生成密文。
  • 使用 AES 加密算法时的初始化向量(IV)和密钥(Key)是硬编码的值,安全性大大折扣。

以上。

comments powered by Disqus