最近在登录公司内部的一个管理系统时,总是弹出对话框提示证书过期、是否继续云云,今天突发奇想,这个登录过程该不会没有加密传输吧?
先用 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
)是硬编码的值,安全性大大折扣。
以上。