Skip to content

Commit 2b6cf16

Browse files
authored
Land rapid7#19297, improve redis scanner logic to handle no auth scenario
2 parents 27a63aa + c5717d4 commit 2b6cf16

File tree

3 files changed

+86
-11
lines changed

3 files changed

+86
-11
lines changed

lib/metasploit/framework/login_scanner/redis.rb

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,40 @@ class Redis
1515
include Metasploit::Framework::LoginScanner::RexSocket
1616
include Metasploit::Framework::Tcp::Client
1717

18+
# Required to be able to invoke the scan! method from the included Base module.
19+
# We do not use inheritance, so overwriting a method and relying on super does
20+
# not work in this case.
21+
alias parent_scan! scan!
22+
1823
DEFAULT_PORT = 6379
1924
LIKELY_PORTS = [ DEFAULT_PORT ]
2025
LIKELY_SERVICE_NAMES = [ 'redis' ]
2126
PRIVATE_TYPES = [ :password ]
2227
REALM_KEY = nil
2328

29+
# Attempt to login with every {Credential credential} in
30+
# {#cred_details}, by calling {#attempt_login} once for each.
31+
#
32+
# If a successful login is found for a user, no more attempts
33+
# will be made for that user. If the scanner detects that no
34+
# authentication is required, no further attempts will be made
35+
# at all.
36+
#
37+
# @yieldparam result [Result] The {Result} object for each attempt
38+
# @yieldreturn [void]
39+
# @return [void]
40+
def scan!(&block)
41+
first_credential = to_enum(:each_credential).first
42+
result = attempt_login(first_credential)
43+
result.freeze
44+
45+
if result.status == Metasploit::Model::Login::Status::NO_AUTH_REQUIRED
46+
yield result if block_given?
47+
else
48+
parent_scan!(&block)
49+
end
50+
end
51+
2452
# This method can create redis command which can be read by redis server
2553
def redis_proto(command_parts)
2654
return if command_parts.blank?
@@ -45,17 +73,25 @@ def attempt_login(credential)
4573
service_name: 'redis'
4674
}
4775

48-
disconnect if self.sock
76+
disconnect if sock
4977

5078
begin
5179
connect
5280
select([sock], nil, nil, 0.4)
5381

54-
command = redis_proto(['AUTH', credential.private.to_s])
82+
# Skip this call if we're dealing with an older redis version.
83+
response = authenticate(credential.public.to_s, credential.private.to_s) unless @older_redis
5584

56-
sock.put(command)
85+
# If we're dealing with an older redis version or the previous call failed,
86+
# try the backwards compatibility call instead.
87+
# We also set the @older_redis to true if we haven't as we might be entering this
88+
# block from the match response.
89+
if @older_redis || (response && response.match(::Rex::Proto::Redis::Base::Constants::WRONG_ARGUMENTS_FOR_AUTH))
90+
@older_redis ||= true
91+
response = authenticate_pre_v6(credential.private.to_s)
92+
end
5793

58-
result_options[:proof] = sock.get_once
94+
result_options[:proof] = response
5995
result_options[:status] = validate_login(result_options[:proof])
6096
rescue Rex::ConnectionError, EOFError, Timeout::Error, Errno::EPIPE => e
6197
result_options.merge!(
@@ -64,13 +100,36 @@ def attempt_login(credential)
64100
)
65101
end
66102

67-
disconnect if self.sock
103+
disconnect if sock
68104

69105
::Metasploit::Framework::LoginScanner::Result.new(result_options)
70106
end
71107

72108
private
73109

110+
# Authenticates against Redis using the provided credentials arguments.
111+
# Takes either a password, or a username and password combination.
112+
#
113+
# @param [String] username The username to authenticate with, defaults to 'default'
114+
# @param [String] password The password to authenticate with.
115+
# @return [String] The response from Redis for the AUTH command.
116+
def authenticate(username, password)
117+
command = redis_proto(['AUTH', username.blank? ? 'default' : username, password])
118+
sock.put(command)
119+
sock.get_once
120+
end
121+
122+
# Authenticates against Redis using the provided password.
123+
# This method is for older Redis instances of backwards compatibility.
124+
#
125+
# @param [String] password The password to authenticate with.
126+
# @return [String] The response from Redis for the AUTH command.
127+
def authenticate_pre_v6(password)
128+
command = redis_proto(['AUTH', password])
129+
sock.put(command)
130+
sock.get_once
131+
end
132+
74133
# Validates the login data received from Redis and returns the correct Login status
75134
# based upon the contents Redis sent back:
76135
#

lib/rex/proto/redis/base.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ module Redis
66
# and include the constants from there, or use the functionality defined there.
77
module Base
88
module Constants
9-
AUTHENTICATION_REQUIRED = /(?<auth_response>NOAUTH Authentication required)/i
10-
NO_PASSWORD_SET = /(?<auth_response>ERR Client sent AUTH, but no password is set)/i
11-
WRONG_PASSWORD = /(?<auth_response>ERR invalid password)/i
12-
OKAY = /\+OK/i
9+
AUTHENTICATION_REQUIRED = /(?<auth_response>NOAUTH Authentication required)/i
10+
NO_PASSWORD_SET = /(?<auth_response>ERR Client sent AUTH, but no password is set)/i
11+
WRONG_PASSWORD = /(?<auth_response>ERR invalid password)/i
12+
WRONG_ARGUMENTS_FOR_AUTH = /(?<auth_response>ERR wrong number of arguments for 'auth' command)/i
13+
OKAY = /\+OK/i
1314
end
1415
end
1516
end

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
RSpec.describe Metasploit::Framework::LoginScanner::Redis do
55
let(:socket) { double('Socket') }
66

7-
def update_socket_res(res)
7+
def update_socket_res(*res)
88
allow(socket).to receive(:put)
9-
allow(socket).to receive(:get_once).and_return(res)
9+
allow(socket).to receive(:get_once).and_return(*res)
1010
allow(subject).to receive(:sock).and_return(socket)
1111
end
1212

@@ -40,6 +40,21 @@ def update_socket_res(res)
4040
end
4141

4242
context 'with Redis version < 6' do
43+
context 'when server returns incorrect arguments' do
44+
let(:res) do
45+
[
46+
"-ERR wrong number of arguments for 'auth' command",
47+
'+OK'
48+
]
49+
end
50+
51+
before { update_socket_res(*res) }
52+
53+
it 'successfully retries and gets the correct response' do
54+
expect(subject.attempt_login(credential).status).to eq(Metasploit::Model::Login::Status::SUCCESSFUL)
55+
end
56+
end
57+
4358
context 'when server returns no password is set' do
4459
let(:res) { '-ERR Client sent AUTH, but no password is set' }
4560

0 commit comments

Comments
 (0)