testssl/testssl.sh

[BUG] Using a port suffix for the URI causes some file descriptor errors

Closed this issue · 17 comments

I am running version commit a719c46bcb49caa32fd08e4a1301da48f6b013ae.

I couldn't find any issues or PRs mentioning this exact problem. When using a URI like host:port some errors occur for certain tests, when it tries to set up a file descriptor. For demonstration purposes I'll just use a public domain and the default port of 443, in which case the problem also occurs merely because the port is present. The relevant section from the output:

 Testing vulnerabilities 

 Heartbleed (CVE-2014-0160)                not vulnerable (OK), no heartbeat extension
 CCS (CVE-2014-0224)                       not vulnerable (OK)
 Ticketbleed (CVE-2016-9244), experiment.  not vulnerable (OK), no session ticket extension
 Opossum (CVE-2025-49812)                  /usr/local/src/testssl.sh/testssl.sh: line 1940: github.com:443: Name or service not known
/usr/local/src/testssl.sh/testssl.sh: line 1940: /dev/tcp/github.com:443/80: Invalid argument

Line 1940 is exec 33<>/dev/tcp/$node/80, but $node is github.com:443 so that won't work. :> I think the same problem might also occur on line 1849, given that it's exactly the same but other code uses $nodeip instead. I'm not sure under what circumstances that might trigger though.

Command line / docker command to reproduce

/usr/local/src/testssl.sh/testssl.sh --parallel github.com:443

Expected behavior

Looking at the CVE for Opossum it seems to require an initial plaintext connection. In that case I'm actually not too sure if this particular test is very useful when running on a non-standard port, since there's a good chance port 80 won't be in use anyway (or it even belongs to a different web server on the same IP). Trying to guess the port is likely to be incorrect, and the specific host I actually want to test doesn't even use any plaintext ports.

Instead of causing a "strange" looking error, perhaps testssl.sh could simply detect the port suffix and emit a warning of its own? Then users could either skip the test, or provide an extra argument for the plaintext port. I'm just running with the default tests though so I'm not sure how to skip just the Opossum test, besides manually specifying all the other tests (requiring a lot of maintenance).

Your system

  • OS: Debian GNU/Linux 12 (bookworm)
  • Platform: Linux 6.1.0-37-amd64 x86_64
  • OpenSSL + bash: Using OpenSSL 3.0.16 (Apr 15 2025) [~94 ciphers] and Using bash 5.2.15

Thanks, @GottemHams ! L1940 works when just specifying $NODEIP instead.

For the lines more above, that seems to be a bug, too. It's supposed to check the revocation and phones out to an external service. Because everyone noways have either curl or wget installed this probably was never executed. That needs to be fixed in 3.2 as well, which I do later.

Mind to fix the first one with a PR?

Looking at the CVE for Opossum it seems to require an initial plaintext connection. In that case I'm actually not too sure if this particular test is very useful when running on a non-standard port, since there's a good chance port 80 won't be in use anyway (or it even belongs to a different web server on the same IP). Trying to guess the port is likely to be incorrect, and the specific host I actually want to test doesn't even use any plaintext ports

It's a check like it's recommended from the authors. And, as in real life, all checks require a human to interpret it (“a fool with a tool is still a fool”).

though so I'm not sure how to skip just the Opossum test

There were/are plans to provide a --no-\<OPTION\>, like for rating, see around L24560 but due to time constraints...

It's a check like it's recommended from the authors. And, as in real life, all checks require a human to interpret it (“a fool with a tool is still a fool”).

Fair enough. :>

Unfortunately just changing $node to $NODEIP doesn't seem to be sufficient just yet, as now it takes about 2 minutes longer to run the test because it keeps waiting for a connection, which eventually fails with Connection timed out. I see in other places that the exec is wrapped in a subshell combined with timeout (which does require you to specify a --connect-timeout). So I think it would have to be something like this:

diff --git a/testssl.sh b/testssl.sh
index b4cf2118..6237cf25 100755
--- a/testssl.sh
+++ b/testssl.sh
@@ -1937,7 +1937,12 @@ http_header_printf() {
      [[ $DEBUG -eq 0 ]] && errfile=/dev/null
 
      IFS=/ read -r proto foo node query <<< "$1"
-     exec 33<>/dev/tcp/$node/80
+
+     if [[ -n "$CONNECT_TIMEOUT" ]] && ! $TIMEOUT_CMD $CONNECT_TIMEOUT bash -c "exec 33<>/dev/tcp/$NODEIP/80"; then
+          return 1
+     fi
+
+     exec 33<>/dev/tcp/$NODEIP/80
      printf -- "%b" "HEAD ${proto}//${node}/${query} HTTP/1.1\r\nUser-Agent: ${useragent}\r\nHost: ${node}\r\n${request_header}\r\nAccept: */*\r\n\r\n\r\n" >&33 2>$errfile &
      wait_kill $! $HEADER_MAXSLEEP
      if [[ $? -ne 0 ]]; then

Although I'm not sure about the return value. Perhaps it should use the same logic as what happens after wait_kill $! $HEADER_MAXSLEEP and potentially return 3 as well?

Hmm, I thought I've tested this (port 80 not reachable). But maybe I confused that with some other endeavors I am working on.

The TIMEOUT_CMD is kind of a legacy thing, it works on Linux but there's no such command on Mac per default. Maybe other BSDs need to have that installed too. The user's getting an error message though when specified --connect-timeout.

If the top of my head it should be better then to put the exec in the background with a subshell and use wait_kill $! $HEADER_MAXSLEEP *) . If that is the point where connection is being made, there should be no reason for putting the HEAD command in the background, TBC.

*) At some point of time we may want to populate CONNECT_TIMEOUT with a default value. So that we can use better wait_kill $! $CONNECT_TIMEOUT. Now it's not set. Setting it though would require more changes.

If that is the point where connection is being made, there should be no reason for putting the HEAD command in the background, TBC.

I think both may actually need to be backgrounded, since the line with the HEAD request is a separate interaction with its own timeout: setting up the socket might work, but the actual request can still time out. Anyway, I've come up with something I think is the most reliable: #2850.

addressing that via $node is actually not that bad here. If is strace bash -c "exec 33<>/dev/tcp/$node/80" as it uses first IPv6 and then IPv4

connect(3, {sa_family=AF_INET6, sin6_port=htons(80), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::1", &sin6_addr), sin6_scope_id=0}, 28) = 0
getsockname(3, {sa_family=AF_INET6, sin6_port=htons(43554), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::1", &sin6_addr), sin6_scope_id=0}, [28]) = 0
close(3)                                = 0
socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP) = 3
connect(3, {sa_family=AF_INET6, sin6_port=htons(80), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::1", &sin6_addr), sin6_scope_id=0}, 28) = -1 ECONNREFUSED (Connection refused)
close(3)                                = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 ECONNREFUSED (Connection refused)

seems we're working on the same thing now ;-(

You can just push your changes to my PR I suppose. :> Or is that not what allow edits by maintainers means?

addressing that via $node is actually not that bad here.

So what we really need then, is the original hostname without a port?

So what we really need then, is the original hostname without a port?

It seems like the global $NODE variable might be exactly that, couldn't we just use that?

setting up the socket might work, but the actual request can still time out

hmmmm, ok. Maybe it's safe to assume that if the connection to the IP works well the application layer will work too. Maybe not. But I haven't seen a case yet

Other than that:

So what we really need then, is the original hostname without a port?

For sockets you always need a port or what do you mean? $node is equal $NODE --> here. The URI (including) is passed from the cmd line to the function. If we want this function more generic I believe it's better to use not the global here.

I meant that instead of github.com:443 we'd need to determine just the github.com part, resulting in /dev/tcp/github.com/80. I printed both $node and $NODE in this function and it seems the latter does get its port stripped: https://github.com/testssl/testssl.sh/blob/3.3dev/testssl.sh#L21934.

I'll propose my PR in a sec I've been working on... but I didn't manage to test it against a host where port 80 is dropped/rejected as I have none right now

Sure. :> I just ran your PR against the same hosts I was testing for mine, but if the connection is actively refused then you'll get a bunch of errors (I'm running with --debug 1 now by the way):

 Opossum (CVE-2025-49812)                  /usr/local/src/testssl.sh/testssl.sh: connect: Connection refused
/usr/local/src/testssl.sh/testssl.sh: line 1946: /dev/tcp/my.host/80: Connection refused
/usr/local/src/testssl.sh/testssl.sh: line 1948: 33: Bad file descriptor
/usr/local/src/testssl.sh/testssl.sh: line 1953: 33: Bad file descriptor
cat: /tmp/testssl.JiEg5k/my.host.133.3.3.7.http_head_printf.log: No such file or directory

See also: https://github.com/testssl/testssl.sh/pull/2850/files#diff-50f5a2161a3b2a3d875fabd6fd7f9465bd3c1e823b0ecc44fa3be7f9dff6174eR1942-R1950

Ok, thanks for the feedback. Need to check then myself

You're right, at least when e.g. the connection is reset. wait_kill works only for dropped connections.

Updated the PR. Now it works fine for me . Let me know whether it still doesn't work for you

That seems to work fine for me too:

 Opossum (CVE-2025-49812)                  connection to port 80 failed