SSH用戶枚舉漏洞(CVE-2018-15473)原理學習


一、漏洞簡介



1、漏洞編號和類型


CVE-2018-15473 SSH 用戶名(USERNAME)暴力枚舉漏洞

2、漏洞影響范圍


OpenSSH 7.7及其以前版本

3、漏洞利用方式


由於SSH本身的認證機制存在缺陷,導致攻擊者可以使用字典,暴力枚舉SSH存在的用戶名(Username)

4、漏洞修復方式


升級openssh

二、漏洞原理及其利用分析



1、漏洞原理


參考國外文獻:http://www.openwall.com/lists/oss-security/2018/08/15/5
觀察下列openssh代碼

  87 static int 88 userauth_pubkey(struct ssh *ssh) 89 { ... 101 if (!authctxt->valid) { 102 debug2("%s: disabled because of invalid user", __func__); 103 return 0; 104 } 105 if ((r = sshpkt_get_u8(ssh, &have_sig)) != 0 || 106 (r = sshpkt_get_cstring(ssh, &pkalg, NULL)) != 0 || 107 (r = sshpkt_get_string(ssh, &pkblob, &blen)) != 0) 108 fatal("%s: parse request failed: %s", __func__, ssh_err(r)); 

可以看出來,當用戶不可用時,連接userauth_pubkey會直接返回,如果用戶可用,則會進入下一個條件判斷,調用fatal函數。所以在username可用於不可用兩種情況下,可以看出來這個函數的返回是不同的

2、PoC原理


PoC地址:https://github.com/Rhynorater/CVE-2018-15473-Exploit
可以看下這段代碼,這就是判斷username是否可用的原理

try: transport.auth_publickey(username, paramiko.RSAKey.generate(1024)) except BadUsername: return (username, False) except paramiko.ssh_exception.AuthenticationException: return (username, True) 

由此可見,一切就在paramiko這個庫的transport.auth_publickey這個函數中

def auth_publickey(self, username, key, event=None):
        """
        Authenticate to the server using a private key.  The key is used to
        sign data from the server, so it must include the private part.

        If an ``event`` is passed in, this method will return immediately, and
        the event will be triggered once authentication succeeds or fails.  On
        success, `is_authenticated` will return ``True``.  On failure, you may
        use `get_exception` to get more detailed error information. Since 1.1, if no event is passed, this method will block until the authentication succeeds or fails. On failure, an exception is raised. Otherwise, the method simply returns. If the server requires multi-step authentication (which is very rare), this method will return a list of auth types permissible for the next step. Otherwise, in the normal case, an empty list is returned. :param str username: the username to authenticate as :param .PKey key: the private key to authenticate with :param .threading.Event event: an event to trigger when the authentication attempt is complete (whether it was successful or not) :return: list of auth types permissible for the next stage of authentication (normally empty) :raises: `.BadAuthenticationType` -- if public-key authentication isn't allowed by the server for this user (and no event was passed in) :raises: `.AuthenticationException` -- if the authentication failed (and no event was passed in) :raises: `.SSHException` -- if there was a network error """ if (not self.active) or (not self.initial_kex_done): # we should never try to authenticate unless we're on a secure link raise SSHException('No existing session') if event is None: my_event = threading.Event() else: my_event = event self.auth_handler = AuthHandler(self) self.auth_handler.auth_publickey(username, key, my_event) if event is not None: # caller wants to wait for event themselves return [] return self.auth_handler.wait_for_response(my_event) 

根據PoC的代碼,在username可用時,auth_publickey的函數會拋出異常,但是拋出的類型AuthenticationException,通過閱讀這個函數的代碼,返現只有self.auth_handler = AuthHandler(self)、self.auth_handler.auth_publickey(username, key, my_event), return self.auth_handler.wait_for_response(my_event)三條語句有可能會拋出這個異常,運行PoC測試發現,在最后一句話中拋出了異常(測試方法很簡單的點燈法,節點前后print信息即可判斷),跟蹤進入這個函數wait_for_response。

def wait_for_response(self, event): max_ts = None if self.transport.auth_timeout is not None: max_ts = time.time() + self.transport.auth_timeout while True: event.wait(0.1) #print self.transport.is_active() if not self.transport.is_active(): e = self.transport.get_exception() #print "e:",e if (e is None) or issubclass(e.__class__, EOFError): e = AuthenticationException('Authentication failed.') raise e if event.is_set(): break if max_ts is not None and max_ts <= time.time(): raise AuthenticationException('Authentication timeout.') if not self.is_authenticated(): e = self.transport.get_exception() if e is None: e = AuthenticationException('Authentication failed.') # this is horrible. Python Exception isn't yet descended from # object, so type(e) won't work. :( if issubclass(e.__class__, PartialAuthentication): return e.allowed_types raise e return [] 

這里有三個點可以拋出Authentication異常,經過修改打印信息獲取到,異常拋出在下面的這個地方。

    if (e is None) or issubclass(e.__class__, EOFError): e = AuthenticationException('Authentication failed.') raise e 

當用戶不可用時,也是在這個點位拋出異常,但是沒有進上文那個判斷,所以e應該不是None,也不是EOFerror, 我們屏蔽掉BadUsername,回歸到最近本的Python異常,發現返回的是這個Authencation Failed2異常,

def wait_for_response(self, event): max_ts = None if self.transport.auth_timeout is not None: max_ts = time.time() + self.transport.auth_timeout while True: event.wait(0.1) #print self.transport.is_active() if not self.transport.is_active(): e = self.transport.get_exception() #print "e:",e if (e is None) or issubclass(e.__class__, EOFError): e = AuthenticationException('Authentication failed1.') raise e if event.is_set(): break if max_ts is not None and max_ts <= time.time(): raise AuthenticationException('Authentication timeout.') if not self.is_authenticated(): e = self.transport.get_exception() if e is None: e = AuthenticationException('Authentication failed2.') # this is horrible. Python Exception isn't yet descended from # object, so type(e) won't work. :( if issubclass(e.__class__, PartialAuthentication): return e.allowed_types raise e return [] 

徹掉后event事件被置位了,可以看出,而且根據下文中auth_publickey函數的注釋部分可以看到的確如此,當身份驗證完成時觸發事件,根據漏洞原來描述,當username不可用時,openssh的函數就返回了,身份驗證完成,觸發了事件,因而跳出了while循環,又因為身份驗證失敗,所以進入了下一個判斷,exception的對象e是None,於是就成了一個新的AuthenticationException異常。而username可用時,並沒有完成身份認證,event沒有觸發,所以在while循環中因為認證失敗跑出了異常。當然PoC中定義自定義異常,來區別這兩個異常,從而做到判斷,但是從程序過程中可以看到兩個地方的的確在網絡通信上是有差別的。

"""
        Authenticate to the server using a private key.  The key is used to
        sign data from the server, so it must include the private part.

        If an ``event`` is passed in, this method will return immediately, and
        the event will be triggered once authentication succeeds or fails.  On
        success, `is_authenticated` will return ``True``.  On failure, you may
        use `get_exception` to get more detailed error information. Since 1.1, if no event is passed, this method will block until the authentication succeeds or fails. On failure, an exception is raised. Otherwise, the method simply returns. If the server requires multi-step authentication (which is very rare), this method will return a list of auth types permissible for the next step. Otherwise, in the normal case, an empty list is returned. :param str username: the username to authenticate as :param .PKey key: the private key to authenticate with :param .threading.Event event: an event to trigger when the authentication attempt is complete (whether it was successful or not) :return: list of auth types permissible for the next stage of authentication (normally empty) :raises: `.BadAuthenticationType` -- if public-key authentication isn't allowed by the server for this user (and no event was passed in) :raises: `.AuthenticationException` -- if the authentication failed (and no event was passed in) :raises: `.SSHException` -- if there was a network error """


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM