Skip to content

Commit 0866ab8

Browse files
committed
ftp_login: Expand login scanner specs
LLM was used here.
1 parent 74da02f commit 0866ab8

2 files changed

Lines changed: 260 additions & 8 deletions

File tree

spec/lib/metasploit/framework/login_scanner/ftp_spec.rb

Lines changed: 124 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,17 @@
100100
expect(ftp_scanner.errors[:ftp_timeout]).to be_empty
101101
end
102102
end
103+
end
103104

104-
105+
describe '#banner' do
106+
it 'is a public method' do
107+
expect(described_class.public_method_defined?(:banner)).to be true
108+
end
105109
end
106110

107111
context '#attempt_login' do
112+
let(:mock_socket) { double('socket') }
113+
108114
before(:example) do
109115
ftp_scanner.host = '127.0.0.1'
110116
ftp_scanner.port = 21
@@ -114,34 +120,144 @@
114120
ftp_scanner.cred_details = detail_group
115121
end
116122

123+
context 'when the connection fails' do
117124

118-
context 'when it fails' do
119-
120-
it 'returns Metasploit::Model::Login::Status::UNABLE_TO_CONNECT for a Rex::ConnectionError' do
125+
it 'returns UNABLE_TO_CONNECT for a Rex::ConnectionError' do
121126
expect(Rex::Socket::Tcp).to receive(:create) { raise Rex::ConnectionError }
122127
expect(ftp_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
123128
end
124129

125-
it 'returns Metasploit::Model::Login::Status::UNABLE_TO_CONNECT for a Rex::AddressInUse' do
130+
it 'returns UNABLE_TO_CONNECT for a Rex::AddressInUse' do
126131
expect(Rex::Socket::Tcp).to receive(:create) { raise Rex::AddressInUse }
127132
expect(ftp_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
128133
end
129134

130-
it 'returns :connection_disconnect for a ::EOFError' do
135+
it 'returns UNABLE_TO_CONNECT for a ::EOFError' do
131136
expect(Rex::Socket::Tcp).to receive(:create) { raise ::EOFError }
132137
expect(ftp_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
133138
end
134139

135-
it 'returns :connection_disconnect for a ::Timeout::Error' do
140+
it 'returns UNABLE_TO_CONNECT for a ::Timeout::Error' do
136141
expect(Rex::Socket::Tcp).to receive(:create) { raise ::Timeout::Error }
137142
expect(ftp_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
138143
end
139144

145+
it 'returns UNABLE_TO_CONNECT for a Errno::ECONNRESET' do
146+
expect(Rex::Socket::Tcp).to receive(:create) { raise Errno::ECONNRESET }
147+
expect(ftp_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
148+
end
149+
150+
it 'returns UNABLE_TO_CONNECT for a Rex::ConnectionTimeout' do
151+
expect(Rex::Socket::Tcp).to receive(:create) { raise Rex::ConnectionTimeout }
152+
expect(ftp_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
153+
end
154+
155+
it 'sets proof to the exception message' do
156+
expect(Rex::Socket::Tcp).to receive(:create) { raise ::Timeout::Error, 'connection timed out' }
157+
expect(ftp_scanner.attempt_login(pub_pri).proof).to eq 'connection timed out'
158+
end
159+
140160
end
141161

142-
context 'when it succeeds' do
162+
context 'when the connection succeeds' do
163+
before(:example) do
164+
allow(ftp_scanner).to receive(:connect).and_return(mock_socket)
165+
allow(ftp_scanner).to receive(:disconnect)
166+
end
167+
168+
context 'and the login fails' do
169+
170+
it 'returns UNABLE_TO_CONNECT when send_user returns nil' do
171+
allow(ftp_scanner).to receive(:send_user).and_return(nil)
172+
expect(ftp_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
173+
end
174+
175+
it 'sets proof to the expected string when the response is nil' do
176+
allow(ftp_scanner).to receive(:send_user).and_return(nil)
177+
expect(ftp_scanner.attempt_login(pub_pri).proof).to eq 'No response to login command'
178+
end
179+
180+
it 'returns INCORRECT when send_user returns a non-2xx non-331 response' do
181+
allow(ftp_scanner).to receive(:send_user).and_return("530 Login incorrect\r\n")
182+
expect(ftp_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::INCORRECT
183+
end
143184

185+
it 'sets proof to the stripped server response when INCORRECT' do
186+
allow(ftp_scanner).to receive(:send_user).and_return("530 Login incorrect\r\n")
187+
expect(ftp_scanner.attempt_login(pub_pri).proof).to eq '530 Login incorrect'
188+
end
144189

190+
it 'does not call send_pass when send_user response does not match 331 or 2xx' do
191+
allow(ftp_scanner).to receive(:send_user).and_return("530 Login incorrect\r\n")
192+
expect(ftp_scanner).not_to receive(:send_pass)
193+
ftp_scanner.attempt_login(pub_pri)
194+
end
195+
196+
it 'returns INCORRECT when send_pass returns a non-2xx response' do
197+
allow(ftp_scanner).to receive(:send_user).and_return("331 Password required\r\n")
198+
allow(ftp_scanner).to receive(:send_pass).and_return("530 Login incorrect\r\n")
199+
expect(ftp_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::INCORRECT
200+
end
201+
202+
it 'returns INCORRECT when send_user returns 2xx and send_pass returns a non-2xx response' do
203+
allow(ftp_scanner).to receive(:send_user).and_return("230 Login successful\r\n")
204+
allow(ftp_scanner).to receive(:send_pass).and_return("530 Login incorrect\r\n")
205+
expect(ftp_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::INCORRECT
206+
end
207+
208+
it 'returns UNABLE_TO_CONNECT when send_pass returns nil' do
209+
allow(ftp_scanner).to receive(:send_user).and_return("331 Password required\r\n")
210+
allow(ftp_scanner).to receive(:send_pass).and_return(nil)
211+
expect(ftp_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
212+
end
213+
214+
end
215+
216+
context 'and the login succeeds' do
217+
218+
it 'returns SUCCESSFUL when send_user returns 331 and send_pass returns 2xx' do
219+
allow(ftp_scanner).to receive(:send_user).and_return("331 Password required\r\n")
220+
allow(ftp_scanner).to receive(:send_pass).and_return("230 Login successful\r\n")
221+
expect(ftp_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::SUCCESSFUL
222+
end
223+
224+
it 'returns SUCCESSFUL when send_user returns 2xx and send_pass returns 2xx' do
225+
allow(ftp_scanner).to receive(:send_user).and_return("230 Login successful\r\n")
226+
allow(ftp_scanner).to receive(:send_pass).and_return("230 Already logged in\r\n")
227+
expect(ftp_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::SUCCESSFUL
228+
end
229+
230+
it 'sets proof to the stripped server response' do
231+
allow(ftp_scanner).to receive(:send_user).and_return("331 Password required\r\n")
232+
allow(ftp_scanner).to receive(:send_pass).and_return("230 Login successful\r\n")
233+
expect(ftp_scanner.attempt_login(pub_pri).proof).to eq "230 Login successful"
234+
end
235+
236+
end
237+
238+
context 'result metadata' do
239+
before(:example) do
240+
allow(ftp_scanner).to receive(:send_user).and_return("331 Password required\r\n")
241+
allow(ftp_scanner).to receive(:send_pass).and_return("230 Login successful\r\n")
242+
end
243+
244+
it 'sets result.host to the scanner host' do
245+
expect(ftp_scanner.attempt_login(pub_pri).host).to eq '127.0.0.1'
246+
end
247+
248+
it 'sets result.port to the scanner port' do
249+
expect(ftp_scanner.attempt_login(pub_pri).port).to eq 21
250+
end
251+
252+
it 'sets result.protocol to tcp' do
253+
expect(ftp_scanner.attempt_login(pub_pri).protocol).to eq 'tcp'
254+
end
255+
256+
it 'sets result.service_name to ftp' do
257+
expect(ftp_scanner.attempt_login(pub_pri).service_name).to eq 'ftp'
258+
end
259+
260+
end
145261
end
146262
end
147263

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
require 'rspec'
2+
3+
RSpec.describe 'FTP Login Scanner' do
4+
include_context 'Msf::Simple::Framework#modules loading'
5+
6+
subject do
7+
load_and_create_module(
8+
module_type: 'auxiliary',
9+
reference_name: 'scanner/ftp/ftp_login'
10+
)
11+
end
12+
13+
let(:ip) { '127.0.0.1' }
14+
15+
describe '#check_host' do
16+
before(:each) do
17+
allow(subject).to receive(:connect)
18+
allow(subject).to receive(:disconnect)
19+
allow(subject).to receive(:report_ftp_service_and_banner)
20+
allow(subject).to receive(:rport).and_return(21)
21+
end
22+
23+
context 'when an FTP banner is present' do
24+
before(:each) do
25+
allow(subject).to receive(:banner).and_return('220 vsftpd 3.0.3')
26+
allow(subject).to receive(:banner_version).and_return('vsftpd 3.0.3')
27+
end
28+
29+
it 'returns CheckCode::Appears' do
30+
expect(subject.check_host(ip).code).to eq 'appears'
31+
end
32+
33+
it 'includes the banner version in the message' do
34+
expect(subject.check_host(ip).reason).to include('vsftpd 3.0.3')
35+
end
36+
37+
it 'reports the FTP service and banner' do
38+
expect(subject).to receive(:report_ftp_service_and_banner).with(ip)
39+
subject.check_host(ip)
40+
end
41+
end
42+
43+
context 'when a 120 banner is present' do
44+
before(:each) do
45+
allow(subject).to receive(:banner).and_return('120 Service ready in 2 minutes')
46+
allow(subject).to receive(:banner_version).and_return('Service ready in 2 minutes')
47+
end
48+
49+
it 'returns CheckCode::Appears' do
50+
expect(subject.check_host(ip).code).to eq 'appears'
51+
end
52+
end
53+
54+
context 'when the banner is nil' do
55+
before(:each) do
56+
allow(subject).to receive(:banner).and_return(nil)
57+
end
58+
59+
it 'returns CheckCode::Unknown' do
60+
expect(subject.check_host(ip).code).to eq 'unknown'
61+
end
62+
63+
it 'reports no valid banner' do
64+
expect(subject.check_host(ip).reason).to include('No valid FTP banner received')
65+
end
66+
end
67+
68+
context 'when the banner does not match a valid FTP code' do
69+
before(:each) do
70+
allow(subject).to receive(:banner).and_return('500 Not FTP')
71+
end
72+
73+
it 'returns CheckCode::Unknown' do
74+
expect(subject.check_host(ip).code).to eq 'unknown'
75+
end
76+
77+
it 'reports no valid banner' do
78+
expect(subject.check_host(ip).reason).to include('No valid FTP banner received')
79+
end
80+
end
81+
82+
context 'when the connection fails' do
83+
it 'returns CheckCode::Unknown for Rex::ConnectionError' do
84+
allow(subject).to receive(:connect).and_raise(Rex::ConnectionError)
85+
expect(subject.check_host(ip).code).to eq 'unknown'
86+
end
87+
88+
it 'returns CheckCode::Unknown for Errno::ECONNRESET' do
89+
allow(subject).to receive(:connect).and_raise(Errno::ECONNRESET)
90+
expect(subject.check_host(ip).code).to eq 'unknown'
91+
end
92+
93+
it 'reports port closed for Rex::ConnectionError' do
94+
allow(subject).to receive(:connect).and_raise(Rex::ConnectionError)
95+
expect(subject.check_host(ip).reason).to include('Port closed or connection refused')
96+
end
97+
end
98+
99+
context 'when the connection times out' do
100+
it 'returns CheckCode::Unknown for Rex::ConnectionTimeout' do
101+
allow(subject).to receive(:connect).and_raise(Rex::ConnectionTimeout, 'timed out')
102+
expect(subject.check_host(ip).code).to eq 'unknown'
103+
end
104+
105+
it 'returns CheckCode::Unknown for Timeout::Error' do
106+
allow(subject).to receive(:connect).and_raise(::Timeout::Error, 'timed out')
107+
expect(subject.check_host(ip).code).to eq 'unknown'
108+
end
109+
110+
it 'returns CheckCode::Unknown for EOFError' do
111+
allow(subject).to receive(:connect).and_raise(::EOFError, 'end of file')
112+
expect(subject.check_host(ip).code).to eq 'unknown'
113+
end
114+
115+
it 'includes the exception message' do
116+
allow(subject).to receive(:connect).and_raise(::Timeout::Error, 'timed out')
117+
expect(subject.check_host(ip).reason).to include('timed out')
118+
end
119+
end
120+
121+
context 'disconnect behaviour' do
122+
it 'always calls disconnect on success' do
123+
allow(subject).to receive(:banner).and_return('220 FTP ready')
124+
allow(subject).to receive(:banner_version).and_return('FTP ready')
125+
expect(subject).to receive(:disconnect)
126+
subject.check_host(ip)
127+
end
128+
129+
it 'always calls disconnect on connection failure' do
130+
allow(subject).to receive(:connect).and_raise(Rex::ConnectionError)
131+
expect(subject).to receive(:disconnect)
132+
subject.check_host(ip)
133+
end
134+
end
135+
end
136+
end

0 commit comments

Comments
 (0)