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 是系统类,入参 Key 和 IV 可在 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 类的构造函数中,对 Key 和 IV 进行初始化赋值:

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。

将上述的 Key 和 IV 拼接后传入 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 是对称加密算法,也就是说,知道了 Key 和 IV,是可以将密文解密成明文的:

$ 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)是硬编码的值,安全性大大折扣。

以上。

One Comment

  1. […] 书接上回。在《LDAP 密码加密方式初探》一文中,使用 OpenSSL 命令 AES 算法加密解密时,都用到了 Key 和 IV 参数,那么这两个参数是如何生成的呢? […]

Comments are Disabled