[Pkg-tigervnc-devel] Bug#971392: buster-pu: package tigervnc/1.9.0+dfsg-3+deb10u3

Joachim Falk joachim.falk at gmx.de
Tue Sep 29 20:32:13 BST 2020


Package: release.debian.org
Severity: normal
Tags: buster
User: release.debian.org at packages.debian.org
Usertags: pu

Security fix for CVE-2020-26117 as detailed in bug #971272.

-- System Information:
Debian Release: 10.6
  APT prefers stable-updates
  APT policy: (500, 'stable-updates'), (500, 'stable')
Architecture: amd64 (x86_64)

Kernel: Linux 4.19.0-9-amd64 (SMP w/16 CPU cores)
Kernel taint flags: TAINT_WARN
Locale: LANG=de_DE.UTF-8, LC_CTYPE=de_DE.UTF-8 (charmap=UTF-8), LANGUAGE=de_DE.UTF-8 (charmap=UTF-8)
Shell: /bin/sh linked to /bin/dash
Init: systemd (via /run/systemd/system)
LSM: AppArmor: enabled
-------------- next part --------------
diff -Nru tigervnc-1.9.0+dfsg/debian/changelog tigervnc-1.9.0+dfsg/debian/changelog
--- tigervnc-1.9.0+dfsg/debian/changelog	2020-06-16 21:36:31.000000000 +0200
+++ tigervnc-1.9.0+dfsg/debian/changelog	2020-09-29 20:21:20.000000000 +0200
@@ -1,3 +1,13 @@
+tigervnc (1.9.0+dfsg-3+deb10u3) buster; urgency=high
+
+  [ Joachim Falk ]
+  * Properly store certificate exceptions in native and java VNC viewer. The
+    VNC viewers stored the certificate exceptions as authorities, meaning that
+    the owner of a certificate could impersonate any server after a client had
+    added an exception. This is issue CVE-2020-26117 (Closes: #971272).
+
+ -- Joachim Falk <joachim.falk at gmx.de>  Tue, 29 Sep 2020 20:21:20 +0200
+
 tigervnc (1.9.0+dfsg-3+deb10u2) buster; urgency=medium

   [ Joachim Falk ]
diff -Nru tigervnc-1.9.0+dfsg/debian/patches/CVE-2020-26117.patch tigervnc-1.9.0+dfsg/debian/patches/CVE-2020-26117.patch
--- tigervnc-1.9.0+dfsg/debian/patches/CVE-2020-26117.patch	1970-01-01 01:00:00.000000000 +0100
+++ tigervnc-1.9.0+dfsg/debian/patches/CVE-2020-26117.patch	2020-09-29 20:21:20.000000000 +0200
@@ -0,0 +1,460 @@
+Description: Properly store certificate exceptions in native and java VNC viewer.
+ They stored the certificates as authorities, meaning that the owner of a
+ certificate could impersonate any server after a client had added an exception.
+Author: Pierre Ossman <ossman at cendio.se> and Brian P. Hinz <bphinz at users.sf.net>
+Abstract:
+ Properly store certificate exceptions
+ .
+ . git commit b30f10c681ec87720cff85d490f67098568a9cba
+ .
+ The previous method stored the certificates as authorities, meaning that
+ the owner of that certificate could impersonate any server it wanted
+ after a client had added an exception.
+ .
+ Handle this more properly by only storing exceptions for specific
+ hostname/certificate combinations, the same way browsers or SSH does
+ things.
+ .
+ . git commit f029745f63ac7d22fb91639b2cb5b3ab56134d6e
+ .
+ Like the native viewer, the Java viewer didn't store certificate
+ exceptions properly. Whilst not as bad as the native viewer, it still
+ failed to check that a stored certificate wouldn't be maliciously used
+ for another server. In practice this can in most cases be used to
+ impersonate another server.
+ .
+ Handle this like the native viewer by storing exceptions for a specific
+ hostname/certificate combination.
+ .
+ This issue is CVE-2020-26117.
+
+Index: pkg-tigervnc/common/rfb/CSecurityTLS.cxx
+===================================================================
+--- pkg-tigervnc.orig/common/rfb/CSecurityTLS.cxx
++++ pkg-tigervnc/common/rfb/CSecurityTLS.cxx
+@@ -232,22 +232,6 @@ void CSecurityTLS::setParam()
+     if (*cafile && gnutls_certificate_set_x509_trust_file(cert_cred,cafile,GNUTLS_X509_FMT_PEM) < 0)
+       throw AuthFailureException("load of CA cert failed");
+
+-    /* Load previously saved certs */
+-    char *homeDir = NULL;
+-    int err;
+-    if (getvnchomedir(&homeDir) == -1)
+-      vlog.error("Could not obtain VNC home directory path");
+-    else {
+-      CharArray caSave(strlen(homeDir) + 19 + 1);
+-      sprintf(caSave.buf, "%sx509_savedcerts.pem", homeDir);
+-      delete [] homeDir;
+-
+-      err = gnutls_certificate_set_x509_trust_file(cert_cred, caSave.buf,
+-                                                   GNUTLS_X509_FMT_PEM);
+-      if (err < 0)
+-        vlog.debug("Failed to load saved server certificates from %s", caSave.buf);
+-    }
+-
+     if (*crlfile && gnutls_certificate_set_x509_crl_file(cert_cred,crlfile,GNUTLS_X509_FMT_PEM) < 0)
+       throw AuthFailureException("load of CRL failed");
+
+@@ -272,7 +256,10 @@ void CSecurityTLS::checkSession()
+   const gnutls_datum_t *cert_list;
+   unsigned int cert_list_size = 0;
+   int err;
++
++  char *homeDir;
+   gnutls_datum_t info;
++  size_t len;
+
+   if (anon)
+     return;
+@@ -315,13 +302,13 @@ void CSecurityTLS::checkSession()
+     throw AuthFailureException("decoding of certificate failed");
+
+   if (gnutls_x509_crt_check_hostname(crt, client->getServerName()) == 0) {
+-    char buf[255];
++    CharArray text;
+     vlog.debug("hostname mismatch");
+-    snprintf(buf, sizeof(buf), "Hostname (%s) does not match any certificate, "
+-			       "do you want to continue?", client->getServerName());
+-    buf[sizeof(buf) - 1] = '\0';
+-    if (!msg->showMsgBox(UserMsgBox::M_YESNO, "hostname mismatch", buf))
+-      throw AuthFailureException("hostname mismatch");
++    text.format("Hostname (%s) does not match the server certificate, "
++                "do you want to continue?", client->getServerName());
++    if (!msg->showMsgBox(UserMsgBox::M_YESNO,
++                         "Certificate hostname mismatch", text.buf))
++      throw AuthFailureException("Certificate hostname mismatch");
+   }
+
+   if (status == 0) {
+@@ -348,87 +335,80 @@ void CSecurityTLS::checkSession()
+
+   vlog.debug("Saved server certificates don't match");
+
+-  if (gnutls_x509_crt_print(crt, GNUTLS_CRT_PRINT_ONELINE, &info)) {
+-    /*
+-     * GNUTLS doesn't correctly export gnutls_free symbol which is
+-     * a function pointer. Linking with Visual Studio 2008 Express will
+-     * fail when you call gnutls_free().
+-     */
+-#if WIN32
+-    free(info.data);
+-#else
+-    gnutls_free(info.data);
+-#endif
+-    throw AuthFailureException("Could not find certificate to display");
++  homeDir = NULL;
++  if (getvnchomedir(&homeDir) == -1) {
++    throw AuthFailureException("Could not obtain VNC home directory "
++                               "path for known hosts storage");
++  }
++
++  CharArray dbPath(strlen(homeDir) + 16 + 1);
++  sprintf(dbPath.buf, "%sx509_known_hosts", homeDir);
++  delete [] homeDir;
++
++  err = gnutls_verify_stored_pubkey(dbPath.buf, NULL,
++                                    client->getServerName(), NULL,
++                                    GNUTLS_CRT_X509, &cert_list[0], 0);
++
++  /* Previously known? */
++  if (err == GNUTLS_E_SUCCESS) {
++    vlog.debug("Server certificate found in known hosts file");
++    gnutls_x509_crt_deinit(crt);
++    return;
+   }
+
+-  size_t out_size = 0;
+-  char *out_buf = NULL;
+-  char *certinfo = NULL;
+-  int len = 0;
+-
+-  vlog.debug("certificate issuer unknown");
+-
+-  len = snprintf(NULL, 0, "This certificate has been signed by an unknown "
+-                          "authority:\n\n%s\n\nDo you want to save it and "
+-                          "continue?\n ", info.data);
+-  if (len < 0)
+-    AuthFailureException("certificate decoding error");
+-
+-  vlog.debug("%s", info.data);
+-
+-  certinfo = new char[len];
+-  if (certinfo == NULL)
+-    throw AuthFailureException("Out of memory");
+-
+-  snprintf(certinfo, len, "This certificate has been signed by an unknown "
+-                          "authority:\n\n%s\n\nDo you want to save it and "
+-                          "continue? ", info.data);
+-
+-  for (int i = 0; i < len - 1; i++)
+-    if (certinfo[i] == ',' && certinfo[i + 1] == ' ')
+-      certinfo[i] = '\n';
+-
+-  if (!msg->showMsgBox(UserMsgBox::M_YESNO, "certificate issuer unknown",
+-		       certinfo)) {
+-    delete [] certinfo;
+-    throw AuthFailureException("certificate issuer unknown");
+-  }
+-
+-  delete [] certinfo;
+-
+-  if (gnutls_x509_crt_export(crt, GNUTLS_X509_FMT_PEM, NULL, &out_size)
+-      == GNUTLS_E_SHORT_MEMORY_BUFFER)
+-    AuthFailureException("Out of memory");
+-
+-  // Save cert
+-  out_buf =  new char[out_size];
+-  if (out_buf == NULL)
+-    AuthFailureException("Out of memory");
+-
+-  if (gnutls_x509_crt_export(crt, GNUTLS_X509_FMT_PEM, out_buf, &out_size) < 0)
+-    AuthFailureException("certificate issuer unknown, and certificate "
+-			 "export failed");
+-
+-  char *homeDir = NULL;
+-  if (getvnchomedir(&homeDir) == -1)
+-    vlog.error("Could not obtain VNC home directory path");
+-  else {
+-    FILE *f;
+-    CharArray caSave(strlen(homeDir) + 1 + 19);
+-    sprintf(caSave.buf, "%sx509_savedcerts.pem", homeDir);
+-    delete [] homeDir;
+-    f = fopen(caSave.buf, "a+");
+-    if (!f)
+-      msg->showMsgBox(UserMsgBox::M_OK, "certificate save failed",
+-                      "Could not save the certificate");
+-    else {
+-      fprintf(f, "%s\n", out_buf);
+-      fclose(f);
+-    }
++  if ((err != GNUTLS_E_NO_CERTIFICATE_FOUND) &&
++      (err != GNUTLS_E_CERTIFICATE_KEY_MISMATCH)) {
++    throw AuthFailureException("Could not load known hosts database");
+   }
+
+-  delete [] out_buf;
++  if (gnutls_x509_crt_print(crt, GNUTLS_CRT_PRINT_ONELINE, &info))
++    throw AuthFailureException("Could not find certificate to display");
++
++  len = strlen((char*)info.data);
++  for (size_t i = 0; i < len - 1; i++) {
++    if (info.data[i] == ',' && info.data[i + 1] == ' ')
++      info.data[i] = '\n';
++  }
++
++  /* New host */
++  if (err == GNUTLS_E_NO_CERTIFICATE_FOUND) {
++    CharArray text;
++
++    vlog.debug("Server host not previously known");
++    vlog.debug("%s", info.data);
++
++    text.format("This certificate has been signed by an unknown "
++                "authority:\n\n%s\n\nSomeone could be trying to "
++                "impersonate the site and you should not "
++                "continue.\n\nDo you want to make an exception "
++                "for this server?", info.data);
++
++    if (!msg->showMsgBox(UserMsgBox::M_YESNO,
++                         "Unknown certificate issuer",
++                         text.buf))
++      throw AuthFailureException("Unknown certificate issuer");
++  } else if (err == GNUTLS_E_CERTIFICATE_KEY_MISMATCH) {
++    CharArray text;
++
++    vlog.debug("Server host key mismatch");
++    vlog.debug("%s", info.data);
++
++    text.format("This host is previously known with a different "
++                "certificate, and the new certificate has been "
++                "signed by an unknown authority:\n\n%s\n\nSomeone "
++                "could be trying to impersonate the site and you "
++                "should not continue.\n\nDo you want to make an "
++                "exception for this server?", info.data);
++
++    if (!msg->showMsgBox(UserMsgBox::M_YESNO,
++                         "Unexpected server certificate",
++                         text.buf))
++      throw AuthFailureException("Unexpected server certificate");
++  }
++
++  if (gnutls_store_pubkey(dbPath.buf, NULL, client->getServerName(),
++                          NULL, GNUTLS_CRT_X509, &cert_list[0], 0, 0))
++    vlog.error("Failed to store server certificate to known hosts database");
+
+   gnutls_x509_crt_deinit(crt);
+   /*
+Index: pkg-tigervnc/java/com/tigervnc/rfb/CSecurityTLS.java
+===================================================================
+--- pkg-tigervnc.orig/java/com/tigervnc/rfb/CSecurityTLS.java
++++ pkg-tigervnc/java/com/tigervnc/rfb/CSecurityTLS.java
+@@ -107,12 +107,6 @@ public class CSecurityTLS extends CSecur
+       X509CRL.setDefaultStr(getDefaultCRL());
+   }
+
+-// FIXME:
+-// Need to shutdown the connection cleanly
+-
+-// FIXME?
+-// add a finalizer method that calls shutdown
+-
+   public boolean processMsg(CConnection cc) {
+     is = (FdInStream)cc.getInStream();
+     os = (FdOutStream)cc.getOutStream();
+@@ -257,8 +251,13 @@ public class CSecurityTLS extends CSecur
+     {
+       Collection<? extends Certificate> certs = null;
+       X509Certificate cert = chain[0];
++      String pk =
++        Base64.getEncoder().encodeToString(cert.getPublicKey().getEncoded());
+       try {
+         cert.checkValidity();
++        verifyHostname(cert);
++      } catch(CertificateParsingException e) {
++        throw new SystemException(e.getMessage());
+       } catch(CertificateNotYetValidException e) {
+         throw new AuthFailureException("server certificate has not been activated");
+       } catch(CertificateExpiredException e) {
+@@ -267,73 +266,111 @@ public class CSecurityTLS extends CSecur
+ 			      "do you want to continue?"))
+           throw new AuthFailureException("server certificate has expired");
+       }
+-      String thumbprint = getThumbprint(cert);
+       File vncDir = new File(FileUtils.getVncHomeDir());
+-      File certFile = new File(vncDir, "x509_savedcerts.pem");
+-      CertificateFactory cf = CertificateFactory.getInstance("X.509");
+-      if (vncDir.exists() && certFile.exists() && certFile.canRead()) {
+-        InputStream certStream = new MyFileInputStream(certFile);
+-        certs = cf.generateCertificates(certStream);
+-        for (Certificate c : certs)
+-          if (thumbprint.equals(getThumbprint((X509Certificate)c)))
+-            return;
+-      }
++      if (!vncDir.exists())
++        throw new AuthFailureException("Could not obtain VNC home directory "+
++                                       "path for known hosts storage");
++      File dbPath = new File(vncDir, "x509_known_hosts");
++      String info =
++        "  Subject: "+cert.getSubjectX500Principal().getName()+"\n"+
++        "  Issuer: "+cert.getIssuerX500Principal().getName()+"\n"+
++        "  Serial Number: "+cert.getSerialNumber()+"\n"+
++        "  Version: "+cert.getVersion()+"\n"+
++        "  Signature Algorithm: "+cert.getPublicKey().getAlgorithm()+"\n"+
++        "  Not Valid Before: "+cert.getNotBefore()+"\n"+
++        "  Not Valid After: "+cert.getNotAfter()+"\n"+
++        "  SHA-1 Fingerprint: "+getThumbprint(cert)+"\n";
+       try {
+-        verifyHostname(cert);
++        if (dbPath.exists()) {
++          FileReader db = new FileReader(dbPath);
++          BufferedReader dbBuf = new BufferedReader(db);
++          String line;
++          String server = client.getServerName().toLowerCase();
++          while ((line = dbBuf.readLine())!=null) {
++            String fields[] = line.split("\\|");
++            if (fields.length==6) {
++              if (server.equals(fields[2]) && pk.equals(fields[5])) {
++                vlog.debug("Server certificate found in known hosts file");
++                dbBuf.close();
++                return;
++              } else if (server.equals(fields[2]) && !pk.equals(fields[5]) ||
++                         !server.equals(fields[2]) && pk.equals(fields[5])) {
++                throw new CertStoreException();
++              }
++            }
++          }
++          dbBuf.close();
++        }
+         tm.checkServerTrusted(chain, authType);
++      } catch (IOException e) {
++        throw new AuthFailureException("Could not load known hosts database");
++      } catch (CertStoreException e) {
++        vlog.debug("Server host key mismatch");
++        vlog.debug(info);
++        String text =
++          "This host is previously known with a different "+
++          "certificate, and the new certificate has been "+
++          "signed by an unknown authority\n"+
++          "\n"+info+"\n"+
++          "Someone could be trying to impersonate the site and you should not continue.\n"+
++          "\n"+
++          "Do you want to make an exception for this server?";
++        if (!msg.showMsgBox(YES_NO_OPTION, "Unexpected certificate issuer", text))
++          throw new AuthFailureException("Unexpected certificate issuer");
++        store_pubkey(dbPath, client.getServerName().toLowerCase(), pk);
+       } catch (java.lang.Exception e) {
+         if (e.getCause() instanceof CertPathBuilderException) {
+-          String certinfo =
++          vlog.debug("Server host not previously known");
++          vlog.debug(info);
++          String text =
+             "This certificate has been signed by an unknown authority\n"+
++            "\n"+info+"\n"+
++            "Someone could be trying to impersonate the site and you should not continue.\n"+
+             "\n"+
+-            "  Subject: "+cert.getSubjectX500Principal().getName()+"\n"+
+-            "  Issuer: "+cert.getIssuerX500Principal().getName()+"\n"+
+-            "  Serial Number: "+cert.getSerialNumber()+"\n"+
+-            "  Version: "+cert.getVersion()+"\n"+
+-            "  Signature Algorithm: "+cert.getPublicKey().getAlgorithm()+"\n"+
+-            "  Not Valid Before: "+cert.getNotBefore()+"\n"+
+-            "  Not Valid After: "+cert.getNotAfter()+"\n"+
+-            "  SHA1 Fingerprint: "+getThumbprint(cert)+"\n"+
+-            "\n"+
+-            "Do you want to save it and continue?";
+-          if (!msg.showMsgBox(YES_NO_OPTION, "certificate issuer unknown",
+-                certinfo)) {
+-            throw new AuthFailureException("certificate issuer unknown");
+-          }
+-          if (certs == null || !certs.contains(cert)) {
+-            byte[] der = cert.getEncoded();
+-            String pem = DatatypeConverter.printBase64Binary(der);
+-            pem = pem.replaceAll("(.{64})", "$1\n");
+-            FileWriter fw = null;
+-            try {
+-              if (!vncDir.exists())
+-                vncDir.mkdir();
+-              if (!certFile.exists() && !certFile.createNewFile()) {
+-                vlog.error("Certificate save failed.");
+-              } else {
+-                fw = new FileWriter(certFile.getAbsolutePath(), true);
+-                fw.write("-----BEGIN CERTIFICATE-----\n");
+-                fw.write(pem+"\n");
+-                fw.write("-----END CERTIFICATE-----\n");
+-              }
+-            } catch (IOException ioe) {
+-              msg.showMsgBox(OK_OPTION, "certificate save failed",
+-                             "Could not save the certificate");
+-            } finally {
+-              try {
+-                if (fw != null)
+-                  fw.close();
+-              } catch(IOException ioe2) {
+-                throw new Exception(ioe2.getMessage());
+-              }
+-            }
+-          }
++            "Do you want to make an exception for this server?";
++          if (!msg.showMsgBox(YES_NO_OPTION, "Unknown certificate issuer", text))
++            throw new AuthFailureException("Unknown certificate issuer");
++          store_pubkey(dbPath, client.getServerName().toLowerCase(), pk);
+         } else {
+           throw new SystemException(e.getMessage());
+         }
+       }
+     }
+
++    private void store_pubkey(File dbPath, String serverName, String pk)
++    {
++      ArrayList<String> lines = new ArrayList<String>();
++      File vncDir = new File(FileUtils.getVncHomeDir());
++      try {
++        if (dbPath.exists()) {
++          FileReader db = new FileReader(dbPath);
++          BufferedReader dbBuf = new BufferedReader(db);
++          String line;
++          while ((line = dbBuf.readLine())!=null) {
++            String fields[] = line.split("\\|");
++            if (fields.length==6)
++              if (!serverName.equals(fields[2]) && !pk.equals(fields[5]))
++                lines.add(line);
++          }
++          dbBuf.close();
++        }
++      } catch (IOException e) {
++        throw new AuthFailureException("Could not load known hosts database");
++      }
++      try {
++        if (!dbPath.exists())
++          dbPath.createNewFile();
++        FileWriter fw = new FileWriter(dbPath.getAbsolutePath(), false);
++        Iterator i = lines.iterator();
++        while (i.hasNext())
++          fw.write((String)i.next()+"\n");
++        fw.write("|g0|"+serverName+"|*|0|"+pk+"\n");
++        fw.close();
++      } catch (IOException e) {
++        vlog.error("Failed to store server certificate to known hosts database");
++      }
++    }
++
+     public X509Certificate[] getAcceptedIssuers ()
+     {
+       return tm.getAcceptedIssuers();
+@@ -389,12 +426,13 @@ public class CSecurityTLS extends CSecur
+         }
+         Object[] answer = {"YES", "NO"};
+         int ret = JOptionPane.showOptionDialog(null,
+-          "Hostname verification failed. Do you want to continue?",
+-          "Hostname Verification Failure",
++          "Hostname ("+client.getServerName()+") does not match the"+
++          " server certificate, do you want to continue?",
++          "Certificate hostname mismatch",
+           JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE,
+           null, answer, answer[0]);
+         if (ret != JOptionPane.YES_OPTION)
+-          throw new WarningException("Hostname verification failed.");
++          throw new WarningException("Certificate hostname mismatch.");
+       } catch (CertificateParsingException e) {
+         throw new SystemException(e.getMessage());
+       } catch (InvalidNameException e) {
diff -Nru tigervnc-1.9.0+dfsg/debian/patches/series tigervnc-1.9.0+dfsg/debian/patches/series
--- tigervnc-1.9.0+dfsg/debian/patches/series	2020-06-16 21:34:14.000000000 +0200
+++ tigervnc-1.9.0+dfsg/debian/patches/series	2020-09-29 20:21:20.000000000 +0200
@@ -40,3 +40,4 @@
 CVE-2019-15693.patch
 CVE-2019-15694.patch
 CVE-2019-15695.patch
+CVE-2020-26117.patch


More information about the Pkg-tigervnc-devel mailing list