Ncat HTTP proxy Digest: honor "algorithm" param

David Fifield <[email protected]> Fri, 1 Feb 2019 13:06:23 -0700
Newsgroups gmane.comp.security.nmap.devel
Message-ID <[email protected]>
I noticed a copy-and-paste error in the Ncat HTTP proxy Digest
authentication. An unknown algorithm in credentials like
algorithm="foobar" was still being treated as ALGORITHM_MD5.

That led me to find that the server was not even checking the algorithm
param, and always acting as if it were ALGORITHM_MD5.

This patch fixes the copy-and-paste error and makes it so that
Proxy-Authenticate and Proxy-Authorization headers that have an unknown
algorithm are ignored.

_______________________________________________
Sent through the dev mailing list
https://nmap.org/mailman/listinfo/dev
Archived at http://seclists.org/nmap-dev/
0001-Ncat-HTTP-proxy-Digest-check-the-algorithm-auth-para.patch (text/x-diff, 8.2 KB)
From b4f2467272127bacd5a156f967458c89e45f796c Mon Sep 17 00:00:00 2001
From: David Fifield <[email protected]>
Date: Fri, 1 Feb 2019 11:47:24 -0700
Subject: [PATCH] Ncat HTTP proxy Digest: check the "algorithm" auth-param.

There were two bugs here: a copy-paste error caused an unknown algorithm
in credentials to be treated as "MD5"; and a missing check that
algorithm=="MD5" before doing MD5 digests.
---
 ncat/http.c            | 33 +++++++++++++++------
 ncat/http_digest.c     |  3 +-
 ncat/test/ncat-test.pl | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 105 insertions(+), 10 deletions(-)

diff --git a/ncat/http.c b/ncat/http.c
index a57b8f155..1729cba0d 100644
--- a/ncat/http.c
+++ b/ncat/http.c
@@ -1524,7 +1524,7 @@ static const char *http_read_credentials(const char *s,
                 if (str_equal_i(value, "MD5"))
                     credentials->u.digest.algorithm = ALGORITHM_MD5;
                 else
-                    credentials->u.digest.algorithm = ALGORITHM_MD5;
+                    credentials->u.digest.algorithm = ALGORITHM_UNKNOWN;
             } else if (str_equal_i(name, "qop")) {
                 if (str_equal_i(value, "auth"))
                     credentials->u.digest.qop = QOP_AUTH;
@@ -1605,12 +1605,21 @@ struct http_challenge *http_header_get_proxy_challenge(const struct http_header
                 http_challenge_free(challenge);
                 return NULL;
             }
-            if (auth_scheme_is_better(tmp_info.scheme, challenge->scheme)) {
-                http_challenge_free(challenge);
-                *challenge = tmp_info;
-            } else {
+#if HAVE_HTTP_DIGEST
+            // RFC 2617, section 3.2.1: "If the algorithm is not understood, the
+            // challenge should be ignored (and a different one used, if there
+            // is more than one)."
+            if (tmp_info.scheme == AUTH_DIGEST && tmp_info.digest.algorithm == ALGORITHM_UNKNOWN) {
+                http_challenge_free(&tmp_info);
+                continue;
+            }
+#endif
+            if (!auth_scheme_is_better(tmp_info.scheme, challenge->scheme)) {
                 http_challenge_free(&tmp_info);
+		continue;
             }
+            http_challenge_free(challenge);
+            *challenge = tmp_info;
         }
     }
 
@@ -1636,12 +1645,18 @@ struct http_credentials *http_header_get_proxy_credentials(const struct http_hea
                 http_credentials_free(credentials);
                 return NULL;
             }
-            if (auth_scheme_is_better(tmp_info.scheme, credentials->scheme)) {
-                http_credentials_free(credentials);
-                *credentials = tmp_info;
-            } else {
+#if HAVE_HTTP_DIGEST
+            if (tmp_info.scheme == AUTH_DIGEST && tmp_info.u.digest.algorithm == ALGORITHM_UNKNOWN) {
+                http_credentials_free(&tmp_info);
+                continue;
+            }
+#endif
+            if (!auth_scheme_is_better(tmp_info.scheme, credentials->scheme)) {
                 http_credentials_free(&tmp_info);
+                continue;
             }
+            http_credentials_free(credentials);
+            *credentials = tmp_info;
         }
     }
 
diff --git a/ncat/http_digest.c b/ncat/http_digest.c
index eb54f2796..f8f43f744 100644
--- a/ncat/http_digest.c
+++ b/ncat/http_digest.c
@@ -398,7 +398,8 @@ int http_digest_check_credentials(const char *username, const char *realm,
         || credentials->u.digest.realm == NULL
         || credentials->u.digest.nonce == NULL
         || credentials->u.digest.uri == NULL
-        || credentials->u.digest.response == NULL) {
+        || credentials->u.digest.response == NULL
+        || credentials->u.digest.algorithm != ALGORITHM_MD5) {
         return 0;
     }
     if (credentials->u.digest.qop != QOP_NONE && credentials->u.digest.qop != QOP_AUTH)
diff --git a/ncat/test/ncat-test.pl b/ncat/test/ncat-test.pl
index 904478dee..15551b458 100755
--- a/ncat/test/ncat-test.pl
+++ b/ncat/test/ncat-test.pl
@@ -1889,6 +1889,28 @@ Proxy-Authenticate: Basic realm=\"$realm\", Digest realm=\"$realm\", nonce=\"$no
 	return 1;
 };
 
+server_client_test "HTTP proxy client ignores Digest with unknown algorithm",
+["-k"], ["--proxy", "$HOST:$PORT", "--proxy-auth", "user:pass", "--proxy-type", "http"],
+sub {
+	my $nonce = "0123456789abcdef";
+	my $realm = "realm";
+	my $req = timeout_read($s_out);
+	$req or die "No initial request from client";
+	syswrite($s_in, "HTTP/1.0 407 Authentication Required\r\
+Proxy-Authenticate: Basic realm=\"$realm\"\r\
+Proxy-Authenticate: Digest algorithm=\"foobar\" realm=\"$realm\", nonce=\"$nonce\", qop=\"auth\"\r\n\r\n");
+	$req = timeout_read($s_out);
+	$req or die "No followup request from client";
+	$req = HTTP::Request->parse($req);
+	foreach my $hdr ($req->header("Proxy-Authorization")) {
+		my ($scheme, %attrs) = parse_proxy_header($hdr);
+		if ($scheme eq "Digest") {
+			die "Client used Digtest auth with an unknown algorithm";
+		}
+	}
+	return 1;
+};
+
 server_client_test "HTTP proxy Digest client auth",
 ["-k"], ["--proxy", "$HOST:$PORT", "--proxy-auth", "user:pass", "--proxy-type", "http"],
 sub {
@@ -2457,6 +2479,63 @@ Proxy-Authorization: Digest username=\"user\", realm=\"$attrs{realm}\", nonce=\"
 kill_children;
 
 ($s_pid, $s_out, $s_in) = ncat_server("--proxy-type", "http", "--proxy-auth", "user:pass");
+test "HTTP proxy Digest credentials unknown algorithm",
+sub {
+	my ($c_pid, $c_out, $c_in) = ncat_client();
+	syswrite($c_in, "CONNECT $HOST:$PORT HTTP/1.0\r\n\r\n");
+	my $resp = timeout_read($c_out);
+	$resp or die "No response from server";
+	$resp = HTTP::Response->parse($resp);
+	foreach my $hdr ($resp->header("Proxy-Authenticate")) {
+		my ($scheme, %attrs) = parse_proxy_header($hdr);
+		next if $scheme ne "Digest";
+		die "no nonce" if not $attrs{"nonce"};
+		die "no realm" if not $attrs{"realm"};
+		my ($c_pid, $c_out, $c_in) = ncat_client();
+		my $response = digest_response("user", "pass", $attrs{"realm"}, "CONNECT", "$HOST:$PORT", $attrs{"nonce"}, undef, undef, undef);
+		syswrite($c_in, "CONNECT $HOST:$PORT HTTP/1.0\r\
+Proxy-Authorization: Digest algorithm=\"foobar\", username=\"user\", realm=\"$attrs{realm}\", nonce=\"$attrs{nonce}\", uri=\"$HOST:$PORT\", response=\"$response\"\r\n\r\n");
+		$resp = timeout_read($c_out);
+		$resp or die "No response from server";
+		$resp = HTTP::Response->parse($resp);
+		my $code = $resp->code;
+		$resp->code == 407 or die "Expected response code 407, got $code";
+		return 1;
+	}
+	die "No Proxy-Authenticate: Digest in server response";
+};
+kill_children;
+
+($s_pid, $s_out, $s_in) = ncat_server("--proxy-type", "http", "--proxy-auth", "user:pass");
+test "HTTP proxy Digest credentials unknown algorithm followed by known algorithm",
+sub {
+	my ($c_pid, $c_out, $c_in) = ncat_client();
+	syswrite($c_in, "CONNECT $HOST:$PORT HTTP/1.0\r\n\r\n");
+	my $resp = timeout_read($c_out);
+	$resp or die "No response from server";
+	$resp = HTTP::Response->parse($resp);
+	foreach my $hdr ($resp->header("Proxy-Authenticate")) {
+		my ($scheme, %attrs) = parse_proxy_header($hdr);
+		next if $scheme ne "Digest";
+		die "no nonce" if not $attrs{"nonce"};
+		die "no realm" if not $attrs{"realm"};
+		my ($c_pid, $c_out, $c_in) = ncat_client();
+		my $response = digest_response("user", "pass", $attrs{"realm"}, "CONNECT", "$HOST:$PORT", $attrs{"nonce"}, undef, undef, undef);
+		syswrite($c_in, "CONNECT $HOST:$PORT HTTP/1.0\r\
+Proxy-Authorization: Digest algorithm=\"foobar\", username=\"user\", realm=\"$attrs{realm}\", nonce=\"$attrs{nonce}\", uri=\"$HOST:$PORT\", response=\"$response\"\r\
+Proxy-Authorization: Digest algorithm=\"MD5\", username=\"user\", realm=\"$attrs{realm}\", nonce=\"$attrs{nonce}\", uri=\"$HOST:$PORT\", response=\"$response\"\r\n\r\n");
+		$resp = timeout_read($c_out);
+		$resp or die "No response from server";
+		$resp = HTTP::Response->parse($resp);
+		my $code = $resp->code;
+		$resp->code == 200 or die "Expected response code 200, got $code";
+		return 1;
+	}
+	die "No Proxy-Authenticate: Digest in server response";
+};
+kill_children;
+
+($s_pid, $s_out, $s_in) = ncat_server("--proxy-type", "http", "--proxy-auth", "user:pass");
 test "HTTP proxy Digest missing fields",
 sub {
 	my ($c_pid, $c_out, $c_in) = ncat_client();
-- 
2.11.0