Skip to content

FTP mixin: Check for complete response#21574

Open
g0tmi1k wants to merge 1 commit into
rapid7:masterfrom
g0tmi1k:ftp_mixin2
Open

FTP mixin: Check for complete response#21574
g0tmi1k wants to merge 1 commit into
rapid7:masterfrom
g0tmi1k:ftp_mixin2

Conversation

@g0tmi1k

@g0tmi1k g0tmi1k commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

This has come from: #21416

Some FTP server/configurations may write multiple complete responses into a single TCP segment.
recv_ftp_resp() stores the response in @ftpbuff, and then returns the first response.
On the next call to recv_ftp_resp(), it empty's @ftpbuff into left, but then called get_once unconditionally before inspecting left.
However, if the socket was idle at that point, get_once blocked until ftp_timeout expired and returned nil, thus dropping the buffered response.

This behaviour was seen with VSFTPD 2.3.4 in Metasploitable 2.

1.) connect() -> recv_ftp_resp() reads the 220 banner.
The 331 response may arrive in the same TCP segment and is saved to @ftpbuff.
2.) send_user() sends USER anonymous\r\n -> recv_ftp_resp()
@ftpbuff drains to left.
Socket is idle (server awaits PASS).
get_once times out -> nil -> connect_login() fails on /^(331|2)/ check.

Example:

$ ruby -r socket -e '
  [21, 2121].each do |port|
    begin
      s = TCPSocket.new("10.0.0.10", port)
      s.write("USER anonymous\r\n")
      sleep 0.1
      data = s.recv(4096)
      puts "Port #{port}: #{data.inspect}"
      s.close
    rescue => e
      puts "Port #{port}: #{e}"
    end
  end'
Port 21: "220 (vsFTPd 2.3.4)\r\n331 Please specify the password.\r\n"
Port 2121: "220 ProFTPD 1.3.1 Server (Debian) [::ffff:10.0.0.10]\r\n331 Password required for anonymous\r\n"
$

Demo

Setup

Will need to re-run this each time, isn't loop'd.

$ ruby -r socket -e '
    s = TCPServer.new("127.0.0.1", 2121)
    puts "[i] Listening"
    c = s.accept
    c.write("220 vsFTPd 2.3.4\r\n331 Please specify the password.\r\n")
    puts "[+] Burst sent"
    sleep 5
    c.close'
[i] Listening

Before

  • Never sends PASS [...]
$ git status
On branch master
Your branch is up to date with 'origin/master'.

nothing to commit, working tree clean
$
$  ./msfconsole -q -x 'db_status; workspace -D; setg VERBOSE true;
use auxiliary/scanner/ftp/ftp_anonymous;
set RHOSTS 127.0.0.1;
set RPORT 2121;
set FTPDEBUG true;
run'
[*] Connected to msf. Connection type: postgresql.
[*] Deleted workspace: default
[*] Recreated the default workspace
VERBOSE => true
RHOSTS => 127.0.0.1
RPORT => 2121
FTPDEBUG => true
[*] 127.0.0.1:2121        - FTP recv: "220 vsFTPd 2.3.4\r\n"
[*] 127.0.0.1:2121        - FTP send: "USER anonymous\r\n"
[*] 127.0.0.1:2121        - Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed
msf auxiliary(scanner/ftp/ftp_anonymous) >

After

  • Can see 331 [...] & PASS [...]
$ git status
On branch ftp_mixin2
Your branch is up to date with 'origin/ftp_mixin2'.

nothing to commit, working tree clean
$
$ ./msfconsole -q -x 'db_status; workspace -D; setg VERBOSE true;
use auxiliary/scanner/ftp/ftp_anonymous;
set RHOSTS 127.0.0.1;
set RPORT 2121;
set FTPDEBUG true;
run'
[*] Connected to msf. Connection type: postgresql.
[*] Deleted workspace: default
[*] Recreated the default workspace
VERBOSE => true
RHOSTS => 127.0.0.1
RPORT => 2121
FTPDEBUG => true
[*] 127.0.0.1:2121        - FTP recv: "220 vsFTPd 2.3.4\r\n"
[*] 127.0.0.1:2121        - FTP buff: "331 Please specify the password.\r\n"
[*] 127.0.0.1:2121        - FTP send: "USER anonymous\r\n"
[*] 127.0.0.1:2121        - FTP recv (buff): "331 Please specify the password.\r\n"
[*] 127.0.0.1:2121        - FTP send: "PASS mozilla@example.com\r\n"
[*] 127.0.0.1:2121        - Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed
msf auxiliary(scanner/ftp/ftp_anonymous) >

e.g. two responses in one TCP burst

LLM was used for the spec
rarr.each do |ln|
if !found_end
resp << ln << "\r\n"
if ln.length > 3 && ln[3, 1] == ' ' && ln[0, 3] =~ /\A\d{3}\z/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like too cryptic to me. Could we simplify this line?

Suggested change
if ln.length > 3 && ln[3, 1] == ' ' && ln[0, 3] =~ /\A\d{3}\z/
if ln.match?(/\A\d{3} /)

Also, I would recommend looking at how the net/ftp handles this too: https://github.com/ruby/net-ftp/blob/master/lib/net/ftp.rb#L691

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rn-enhancement release notes enhancement

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

4 participants