✓ProveIt
Examples / Report #1913733 Saved analysis

CVE-2023-28319: UAF in SSH sha256 fingerprint check

curl · 2023-03-218.4s analysis · $0.010 in model calls

What the report claims

The report claims that curl has a use-after-free vulnerability in the SSH fingerprint check function where a pointer is used after being freed, leading to potential crashes or information leaks.

Compare with curl's recorded outcome

Known vulnerability

A real vulnerability: CVE-2023-28319 UAF in SSH sha256 fingerprint check

Shown for comparison after analysis. The pipeline did not see this label.

ProveIt's finding

Code supports the claim

The code supports the reported flaw. A maintainer should review its impact and decide what to do next.

Why this finding?
  • Code at lib/vssh/libssh2.c lines 731-735 shows a clear use-after-free: fingerprint_b64 is freed then passed to failf()
  • The bug is reachable by an attacker controlling the SSH server's hostkey or the client's expected fingerprint configuration
  • Impact includes potential crash (denial of service) or information leak via the error log
  • No project policy excludes this class of bug
How this analysis ran 5 claims · 10 citations verified

Checked against curl tested commit a51f90ec0f74 (a51f90ec0f74). Dev evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.

1 · Grounding
grounded
no model · 4 references checked out, 0 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
5 claims · 1754 tokens · $0.0002
3 · Evidence
2 code snippets
real curl source at a51f90ec0f74
4 · Judgment
Nemotron-3-Ultra-550b-a55b
6687 tokens · 6.5s · $0.0103
5 · Receipts
10 of 10 citations verified
every citation must point at code the judge was shown
Review a draft reply to the reporter

Edit and verify before sending.

Thank you for the report. The use-after-free at lib/vssh/libssh2.c:731-735 is confirmed: fingerprint_b64 is freed then used in the failf() call. This is reachable when the SHA256 fingerprint mismatch check fails, which an attacker can trigger via the server's hostkey or the configured expected fingerprint. The fix should save the fingerprint string before freeing it, or reorder the free after the log call.

Inspect all claims and source code 5 claims · 2 code excerpts

Report claims

C1flaw supported

The pointer fingerprint_b64 is used as a parameter to failf() after it has been freed

Read the reasoning

The code at lines 731-735 shows fingerprint_b64 is freed at line 731 and then used as a format argument to failf() at line 735, confirming the use-after-free.

C2location supported

In curl/lib/vssh/libssh2.c, function ssh_check_fingerprint, lines 731-735

Read the reasoning

The location matches exactly: ssh_check_fingerprint function in lib/vssh/libssh2.c at lines 731-735.

C3trigger supported

When a SSH server sends a malformed fingerprint or when fingerprint comparison fails

Read the reasoning

The bug triggers when the SSH hostkey fingerprint comparison fails (pub_pos != b64_pos or strncmp mismatch), which an attacker can influence by controlling the server's hostkey or the configured expected fingerprint.

C4impact supported

Potential application crash or information leak through fail log exposing freed memory contents

Read the reasoning

Use-after-free can cause crash (DoS) or information leak via the fail log when failf() dereferences the freed pointer to format the error message.

C5reproduction supported

Clone curl repository, modify lib/vssh/libssh2.c to free fingerprint_b64 and then use it in failf()

Read the reasoning

The bug is reproducible by configuring an expected SHA256 fingerprint that doesn't match the server's actual hostkey, causing the mismatch branch to execute.

Read the original report

Full report on HackerOne ↗

## Summary: The fingerprint_b64 pointer is as parameter for failure logging after it is freed. ## Steps To Reproduce: 1. git clone https://github.com/curl/curl 2. vim curl/lib/vssh/libssh2.c 3. search for the string 'free(fingerprint_b64)' and note that fingerprint_b64 is used as parameter immediately after it is freed. ## Supporting Material/References: The following are the details of the code. git commit a51f90ec0f74176776f5d14b99689f9241660eaa (HEAD -> master, origin/master, origin/HEAD) In curl/lib/vssh/libssh2.c 653 static CURLcode ssh_check_fingerprint(struct Curl_easy *data) 654 { ... 667 char *fingerprint_b64 = NULL; ... 729 if((pub_pos != b64_pos) || 730 strncmp(fingerprint_b64, pubkey_sha256, pub_pos)) { 731 free(fingerprint_b64); 732 733 failf(data, 734 "Denied establishing ssh session: mismatch sha256 fingerprint. " 735 "Remote %s is not equal to %s", fingerprint_b64, pubkey_sha256); According to git blame, the following commits were involved: d1e7d9197b lib/vssh/libssh2.c (Mats Lindestam 2021-09-26 23:20:53 +0200 731) free(fingerprint_b64); d1e7d9197b lib/vssh/libssh2.c (Mats Lindestam 2021-09-26 23:20:53 +0200 732) d1e7d9197b lib/vssh/libssh2.c (Mats Lindestam 2021-09-26 23:20:53 +0200 733) failf(data, ff2f3e8367 lib/vssh/libssh2.c (Daniel Stenberg 2022-04-25 11:41:20 +0200 734) "Denied establishing ssh session: mismatch sha256 fingerprint. " ff2f3e8367 lib/vssh/libssh2.c (Daniel Stenberg 2022-04-25 11:41:20 +0200 735) "Remote %s is not equal to %s", fingerprint_b64, pubkey_sha256); ## Impact Depends on which memory is the pointer fingerprint_b64 pointing to at the time failf() is called, it may either crash the application or it may print out whatever was in memory at the time leading to information leak in the fail log.

Source code

lib/vssh/libssh2.clines 653–813 · definition of ssh_check_fingerprint (truncated)
653static CURLcode ssh_check_fingerprint(struct Curl_easy *data)
C2: Function ssh_check_fingerprint starts here
654{
655 struct connectdata *conn = data->conn;
656 struct ssh_conn *sshc = &conn->proto.sshc;
657 const char *pubkey_md5 = data->set.str[STRING_SSH_HOST_PUBLIC_KEY_MD5];
658 const char *pubkey_sha256 = data->set.str[STRING_SSH_HOST_PUBLIC_KEY_SHA256];
659
660 infof(data, "SSH MD5 public key: %s",
661 pubkey_md5 != NULL ? pubkey_md5 : "NULL");
662 infof(data, "SSH SHA256 public key: %s",
663 pubkey_sha256 != NULL ? pubkey_sha256 : "NULL");
664
665 if(pubkey_sha256) {
666 const char *fingerprint = NULL;
667 char *fingerprint_b64 = NULL;
668 size_t fingerprint_b64_len;
669 size_t pub_pos = 0;
670 size_t b64_pos = 0;
671
672#ifdef LIBSSH2_HOSTKEY_HASH_SHA256
673 /* The fingerprint points to static storage (!), don't free() it. */
674 fingerprint = libssh2_hostkey_hash(sshc->ssh_session,
675 LIBSSH2_HOSTKEY_HASH_SHA256);
676#else
677 const char *hostkey;
678 size_t len = 0;
679 unsigned char hash[32];
680
681 hostkey = libssh2_session_hostkey(sshc->ssh_session, &len, NULL);
682 if(hostkey) {
683 if(!Curl_sha256it(hash, (const unsigned char *) hostkey, len))
684 fingerprint = (char *) hash;
685 }
686#endif
687
688 if(!fingerprint) {
689 failf(data,
690 "Denied establishing ssh session: sha256 fingerprint "
691 "not available");
692 state(data, SSH_SESSION_FREE);
693 sshc->actualcode = CURLE_PEER_FAILED_VERIFICATION;
694 return sshc->actualcode;
695 }
696
697 /* The length of fingerprint is 32 bytes for SHA256.
698 * See libssh2_hostkey_hash documentation. */
699 if(Curl_base64_encode(fingerprint, 32, &fingerprint_b64,
700 &fingerprint_b64_len) != CURLE_OK) {
701 state(data, SSH_SESSION_FREE);
702 sshc->actualcode = CURLE_PEER_FAILED_VERIFICATION;
703 return sshc->actualcode;
704 }
705
706 if(!fingerprint_b64) {
707 failf(data, "sha256 fingerprint could not be encoded");
708 state(data, SSH_SESSION_FREE);
709 sshc->actualcode = CURLE_PEER_FAILED_VERIFICATION;
710 return sshc->actualcode;
711 }
712
713 infof(data, "SSH SHA256 fingerprint: %s", fingerprint_b64);
714
715 /* Find the position of any = padding characters in the public key */
716 while((pubkey_sha256[pub_pos] != '=') && pubkey_sha256[pub_pos]) {
717 pub_pos++;
718 }
719
720 /* Find the position of any = padding characters in the base64 coded
721 * hostkey fingerprint */
722 while((fingerprint_b64[b64_pos] != '=') && fingerprint_b64[b64_pos]) {
723 b64_pos++;
724 }
725
726 /* Before we authenticate we check the hostkey's sha256 fingerprint
727 * against a known fingerprint, if available.
728 */
729 if((pub_pos != b64_pos) ||
C3: Condition that triggers the buggy path: fingerprint mismatch
C5: Trigger condition reachable via SSH server fingerprint mismatch
730 strncmp(fingerprint_b64, pubkey_sha256, pub_pos)) {
C3: strncmp comparison failure leads to free+use
731 free(fingerprint_b64);
C1: free(fingerprint_b64) called
C2: UAF occurs at lines 731-735
C4: free() called on fingerprint_b64
C5: Bug reproduces when mismatch condition is met
732
733 failf(data,
734 "Denied establishing ssh session: mismatch sha256 fingerprint. "
735 "Remote %s is not equal to %s", fingerprint_b64, pubkey_sha256);
C1: fingerprint_b64 passed to failf() after free
C4: freed pointer passed to failf() for logging
736 state(data, SSH_SESSION_FREE);
737 sshc->actualcode = CURLE_PEER_FAILED_VERIFICATION;
738 return sshc->actualcode;
739 }
740
741 free(fingerprint_b64);
742
743 infof(data, "SHA256 checksum match");
744 }
745
746 if(pubkey_md5) {
747 char md5buffer[33];
748 const char *fingerprint = NULL;
749
750 fingerprint = libssh2_hostkey_hash(sshc->ssh_session,
751 LIBSSH2_HOSTKEY_HASH_MD5);
752
753 if(fingerprint) {
754 /* The fingerprint points to static storage (!), don't free() it. */
755 int i;
756 for(i = 0; i < 16; i++) {
757 msnprintf(&md5buffer[i*2], 3, "%02x", (unsigned char) fingerprint[i]);
758 }
759
760 infof(data, "SSH MD5 fingerprint: %s", md5buffer);
761 }
762
763 /* This does NOT verify the length of 'pubkey_md5' separately, which will
764 make the comparison below fail unless it is exactly 32 characters */
765 if(!fingerprint || !strcasecompare(md5buffer, pubkey_md5)) {
766 if(fingerprint) {
767 failf(data,
768 "Denied establishing ssh session: mismatch md5 fingerprint. "
769 "Remote %s is not equal to %s", md5buffer, pubkey_md5);
770 }
771 else {
772 failf(data,
773 "Denied establishing ssh session: md5 fingerprint "
774 "not available");
775 }
776 state(data, SSH_SESSION_FREE);
777 sshc->actualcode = CURLE_PEER_FAILED_VERIFICATION;
778 return sshc->actualcode;
779 }
780 infof(data, "MD5 checksum match");
781 }
782
783 if(!pubkey_md5 && !pubkey_sha256) {
784 if(data->set.ssh_hostkeyfunc) {
785 size_t keylen = 0;
786 int sshkeytype = 0;
787 int rc = 0;
788 /* we handle the process to the callback */
789 const char *remotekey = libssh2_session_hostkey(sshc->ssh_session,
790 &keylen, &sshkeytype);
791 if(remotekey) {
792 int keytype = convert_ssh2_keytype(sshkeytype);
793 Curl_set_in_callback(data, true);
794 rc = data->set.ssh_hostkeyfunc(data->set.ssh_hostkeyfunc_userp,
795 keytype, remotekey, keylen);
796 Curl_set_in_callback(data, false);
797 if(rc!= CURLKHMATCH_OK) {
798 state(data, SSH_SESSION_FREE);
799 sshc->actualcode = CURLE_PEER_FAILED_VERIFICATION;
800 return sshc->actualcode;
801 }
802 }
803 else {
804 state(data, SSH_SESSION_FREE);
805 sshc->actualcode = CURLE_PEER_FAILED_VERIFICATION;
806 return sshc->actualcode;
807 }
808 return CURLE_OK;
809 }
810 else {
811 return ssh_knownhost(data);
812 }
813 }
lib/curl_log.hlines 55–55 · definition of failf
55#define failf Curl_failf