A malicious FTP server can return an arbitrarily large OPIE / S-KEY sequence number in its 331 login challenge. ftp_login() reads that value and passes it to skey_response() as an MD5 iteration count, so a large sequence forces Wget through up to ~2.1 billion MD5 rounds before it sends the password -- a server-controlled denial of service.
ftp_login() now rejects a sequence above 9999 before calling skey_response(). This test drives a crafted "331 opiekey 100000" challenge and checks that login is refused and nothing is downloaded, which never happens on an unbounded build. * tests/FTPServer.pm: Add the opie_sequence server behavior, answering USER with a crafted OPIE challenge carrying a chosen sequence number. * tests/Test-ftp-opie-seq.px: New test for the sequence bound. * tests/Makefile.am (PX_TESTS): Add Test-ftp-opie-seq.px. Fixes CVE-2026-16599 Signed-off-by: Pratik Farkase <[email protected]> --- tests/FTPServer.pm | 15 ++++++++ tests/Makefile.am | 1 + tests/Test-ftp-opie-seq.px | 70 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 tests/Test-ftp-opie-seq.px diff --git a/tests/FTPServer.pm b/tests/FTPServer.pm index cac80942..51ee7fba 100644 --- a/tests/FTPServer.pm +++ b/tests/FTPServer.pm @@ -748,6 +748,21 @@ sub run next; } + # Reply to USER with a crafted OPIE / S-KEY challenge + # carrying a caller-chosen sequence number. Used by + # Test-ftp-opie-seq.px to exercise the CVE-2026-16599 + # sequence bound in ftp_login(). + if (defined($self->{_server_behavior}{opie_sequence}) + && $cmd eq 'USER') + { + my $seq = $self->{_server_behavior}{opie_sequence}; + $conn->{username} = $rest; + $conn->{state} = $_connection_states{WAIT4PWD}; + print {$conn->{socket}} + "331 opiekey $seq ab1234\r\n"; + next; + } + # Run the command. &{$command_table->{$cmd}}($conn, $cmd, $rest); } diff --git a/tests/Makefile.am b/tests/Makefile.am index ab4f035b..002cc761 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -55,6 +55,7 @@ PX_TESTS = \ Test-ftp.px \ Test-ftp-dir.px \ Test-ftp-pasv-fail.px \ + Test-ftp-opie-seq.px \ Test-ftp-bad-list.px \ Test-ftp-recursive.px \ Test-ftp-iri.px \ diff --git a/tests/Test-ftp-opie-seq.px b/tests/Test-ftp-opie-seq.px new file mode 100644 index 00000000..0a8770f7 --- /dev/null +++ b/tests/Test-ftp-opie-seq.px @@ -0,0 +1,70 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +use WgetFeature qw(opie); +use FTPTest; + +# Regression test for CVE-2026-16599. +# +# When logging in to an FTP server that advertises OPIE / S-KEY, Wget +# reads the S-KEY sequence number straight out of the server's 331 +# challenge line and feeds it to skey_response() as an MD5 iteration +# count. A malicious server could return an arbitrarily large sequence +# (up to ~2.1 billion), forcing Wget to perform that many MD5 rounds +# before it could even send the password -- a server-controlled denial +# of service. +# +# ftp_login() now caps the sequence at 9999 while parsing the challenge; +# a larger value makes it return FTPLOGREFUSED immediately, without ever +# calling skey_response(). The login attempt then fails and nothing is +# downloaded. +# +# The server below answers USER with "331 opiekey 100000 ..." (100000 > +# 9999). With the bound in place login is refused up front, so the test +# stays offline and fast; it never drives the long MD5 loop. Asserting +# the refusal (failed exit code, no file downloaded) is what separates a +# fixed Wget from a vulnerable one: a vulnerable Wget instead accepts the +# sequence, runs the loop, sends PASS and downloads the file. + +############################################################################### + +my $afile = <<EOF; +Some text. +EOF + +$afile =~ s/\n/\r\n/g; + +# code, msg, headers, content +my %urls = ( + '/afile.txt' => { + content => $afile, + }, +); + +my $cmdline = $WgetTest::WGETPATH . " --tries=1 ftp://localhost:{{port}}/afile.txt"; + +# With the fix, ftp_login() rejects the over-bound OPIE sequence before +# sending PASS, so the login fails and nothing is downloaded. Wget exits +# 4 (WGET_EXIT_NETWORK_FAIL) once the single attempt is exhausted; --tries=1 +# keeps that deterministic. A vulnerable binary instead accepts the +# sequence and drives skey_response()'s MD5 loop, sending PASS afterwards. +my $expected_error_code = 4; + +# Login is refused before any transfer, so nothing must be downloaded. +my %expected_downloaded_files = (); + +############################################################################### + +my $the_test = FTPTest->new ( + # opie_sequence triggers a crafted OPIE + # challenge with an over-bound sequence. + server_behavior => {opie_sequence => 100000}, + input => \%urls, + cmdline => $cmdline, + errcode => $expected_error_code, + output => \%expected_downloaded_files); +exit $the_test->run(); + +# vim: et ts=4 sw=4 -- 2.43.0
