Skip to content

Commit 52142f2

Browse files
committed
MS-9454 Redis Scanner: Support versions
Updating the Redis Login Scanner to properly support all versions of Redis and their implementations to handle the `AUTH` command.
1 parent e691f72 commit 52142f2

File tree

8 files changed

+151
-43
lines changed

8 files changed

+151
-43
lines changed

documentation/modules/auxiliary/scanner/redis/redis_login.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,19 @@ database with optional durability. Redis supports different kinds of abstract da
44
such as strings, lists, maps, sets, sorted sets, HyperLogLogs, bitmaps, streams, and spatial indexes.
55

66
This module is login utility to find the password of the Redis server by bruteforcing the login portal.
7-
Note that Redis does not require a username to log in; login is done purely via supplying a valid password.
87

98
A complete installation guide for Redis can be found [here](https://redis.io/topics/quickstart)
109

10+
### Redis Authentication
11+
12+
Redis has several ways to support secure connections to the in-memory database:
13+
14+
* Prior to Redis 6, the `requirepass` directive could be set, setting a master password for all connections.
15+
This requires the usage of the `AUTH <password>` command before executing any commands on the cluster.
16+
* After Redis 6, the `requirepass` directive sets a password for the default user `default`
17+
* The `AUTH` command now takes two arguments instead of one: `AUTH <username> <password>`
18+
* The `AUTH` command still accepts a single arguments, but defaults to the user `default`
19+
1120
## Setup
1221

1322
Run redis in docker without auth:

lib/metasploit/framework/login_scanner/redis.rb

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
require 'metasploit/framework/login_scanner/base'
22
require 'metasploit/framework/login_scanner/rex_socket'
33
require 'metasploit/framework/tcp/client'
4+
require 'rex/proto/redis'
45

56
module Metasploit
67
module Framework
@@ -19,11 +20,6 @@ class Redis
1920
LIKELY_SERVICE_NAMES = [ 'redis' ]
2021
PRIVATE_TYPES = [ :password ]
2122
REALM_KEY = nil
22-
OLD_PASSWORD_NOT_SET = /but no password is set/i
23-
PASSWORD_NOT_SET = /without any password configured/i
24-
WRONG_PASSWORD_SET = /^-WRONGPASS/i
25-
INVALID_PASSWORD_SET = /^-ERR invalid password/i
26-
OK = /^\+OK/
2723

2824
# This method can create redis command which can be read by redis server
2925
def redis_proto(command_parts)
@@ -84,15 +80,23 @@ def attempt_login(credential)
8480
def validate_login(data)
8581
return if data.nil?
8682

87-
return Metasploit::Model::Login::Status::NO_AUTH_REQUIRED if data =~ OLD_PASSWORD_NOT_SET
88-
return Metasploit::Model::Login::Status::NO_AUTH_REQUIRED if data =~ PASSWORD_NOT_SET
89-
return Metasploit::Model::Login::Status::INCORRECT if (data =~ INVALID_PASSWORD_SET) == 0
90-
return Metasploit::Model::Login::Status::INCORRECT if (data =~ WRONG_PASSWORD_SET) == 0
91-
return Metasploit::Model::Login::Status::SUCCESSFUL if (data =~ OK) == 0
83+
return Metasploit::Model::Login::Status::NO_AUTH_REQUIRED if no_password_set?(data)
84+
return Metasploit::Model::Login::Status::INCORRECT if invalid_password?(data)
85+
return Metasploit::Model::Login::Status::SUCCESSFUL if data.match(::Rex::Proto::Redis::Base::Constants::OKAY)
9286

9387
nil
9488
end
9589

90+
def no_password_set?(data)
91+
data.match(::Rex::Proto::Redis::Base::Constants::NO_PASSWORD_SET) ||
92+
data.match(::Rex::Proto::Redis::Version6::Constants::NO_PASSWORD_SET)
93+
end
94+
95+
def invalid_password?(data)
96+
data.match(::Rex::Proto::Redis::Base::Constants::WRONG_PASSWORD) ||
97+
data.match(::Rex::Proto::Redis::Version6::Constants::WRONG_PASSWORD)
98+
end
99+
96100
# (see Base#set_sane_defaults)
97101
def set_sane_defaults
98102
self.connection_timeout ||= 30

lib/msf/core/auxiliary/redis.rb

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,9 @@ module Auxiliary::Redis
1010
include Auxiliary::Scanner
1111
include Auxiliary::Report
1212

13-
REDIS_UNAUTHORIZED_RESPONSE = /(?<auth_response>ERR operation not permitted|NOAUTH Authentication required)/i
14-
1513
#
1614
# Initializes an instance of an auxiliary module that interacts with Redis
1715
#
18-
1916
def initialize(info = {})
2017
super
2118
register_options(
@@ -52,12 +49,14 @@ def redis_command(*commands)
5249
vprint_error("No response to '#{command_string}'")
5350
return
5451
end
55-
if match = command_response.match(REDIS_UNAUTHORIZED_RESPONSE)
52+
if (match = authentication_required?(command_response))
5653
auth_response = match[:auth_response]
54+
5755
fail_with(::Msf::Module::Failure::BadConfig, "#{peer} requires authentication but Password unset") unless datastore['Password']
5856
vprint_status("Requires authentication (#{printable_redis_response(auth_response, false)})")
57+
5958
if (auth_response = send_redis_command('AUTH', datastore['PASSWORD']))
60-
unless auth_response =~ /\+OK/
59+
unless auth_response =~ Rex::Proto::Redis::Base::Constants::OKAY
6160
vprint_error("Authentication failure: #{printable_redis_response(auth_response)}")
6261
return
6362
end
@@ -87,6 +86,13 @@ def printable_redis_response(response_data, convert_whitespace = true)
8786

8887
private
8988

89+
# Verifies whether the response indicates if authentication is required
90+
# @return [RESPParser] Returns a matched response if a hit is there; otherwise nil.
91+
def authentication_required?(response)
92+
response.match(Rex::Proto::Redis::Base::Constants::AUTHENTICATION_REQUIRED) ||
93+
response.match(Rex::Proto::Redis::Version6::Constants::AUTHENTICATION_REQUIRED)
94+
end
95+
9096
def redis_proto(command_parts)
9197
return if command_parts.blank?
9298
command = "*#{command_parts.length}\r\n"

lib/rex/proto/redis.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
require 'rex/proto/redis/base'
2+
require 'rex/proto/redis/version6'
3+
4+
module Rex
5+
module Proto
6+
# Protocol module inside the Rex namespace to support Redis.
7+
# Because the behavior of Redis changes between certain versions,
8+
# dedicated submodules exist for each version
9+
module Redis
10+
11+
end
12+
end
13+
end

lib/rex/proto/redis/base.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module Rex
2+
module Proto
3+
module Redis
4+
# Module containing the constants and functionality for any Redis version.
5+
# When a behavior changes, check whether a more recent version module exits
6+
# and include the constants from there, or use the functionality defined there.
7+
module Base
8+
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
13+
end
14+
end
15+
end
16+
end
17+
end

lib/rex/proto/redis/version6.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module Rex
2+
module Proto
3+
module Redis
4+
# Module containing the required constants and functionality
5+
# specifically for Redis 6 and newer.
6+
module Version6
7+
module Constants
8+
AUTHENTICATION_REQUIRED = /(?<auth_response>NOAUTH Authentication required)/i
9+
NO_PASSWORD_SET = /(?<auth_response>ERR AUTH <password> called without any password configured for the default user. Are you sure your configuration is correct?)/i
10+
WRONG_PASSWORD = /(?<auth_response>WRONGPASS invalid username-password pair or user is disabled)/i
11+
end
12+
end
13+
end
14+
end
15+
end

modules/auxiliary/scanner/redis/redis_login.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
require 'metasploit/framework/login_scanner/redis'
77
require 'metasploit/framework/credential_collection'
88

9+
# Metasploit Module - Redis Login Scanner
10+
#
11+
# @example
12+
# use auxiliary/scanner/redis/login
13+
#
914
class MetasploitModule < Msf::Auxiliary
1015
include Msf::Exploit::Remote::Tcp
1116
include Msf::Auxiliary::Scanner
@@ -45,7 +50,12 @@ def initialize(info = {})
4550
def requires_password?(_ip)
4651
connect
4752
command_response = send_redis_command('INFO')
48-
!(command_response && REDIS_UNAUTHORIZED_RESPONSE !~ command_response)
53+
54+
## Check against the old and new password required response to support all Redis versions
55+
!(
56+
(command_response && Rex::Proto::Redis::Base::Constants::AUTHENTICATION_REQUIRED !~ command_response) ||
57+
(command_response && Rex::Proto::Redis::Version6::Constants::AUTHENTICATION_REQUIRED !~ command_response)
58+
)
4959
end
5060

5161
def run_host(ip)

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

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
require 'metasploit/framework/login_scanner/redis'
33

44
RSpec.describe Metasploit::Framework::LoginScanner::Redis do
5+
let(:socket) { double('Socket') }
56

67
def update_socket_res(res)
7-
socket = double('Socket')
88
allow(socket).to receive(:put)
99
allow(socket).to receive(:get_once).and_return(res)
1010
allow(subject).to receive(:sock).and_return(socket)
@@ -39,45 +39,79 @@ def update_socket_res(res)
3939
allow(subject).to receive(:select)
4040
end
4141

42-
context 'when server returns no password is set' do
43-
let(:res) do
44-
'but no password is set'
45-
end
42+
context 'with Redis version < 6' do
43+
context 'when server returns no password is set' do
44+
let(:res) { '-ERR Client sent AUTH, but no password is set' }
4645

47-
before do
48-
update_socket_res(res)
49-
end
46+
before do
47+
update_socket_res(res)
48+
end
5049

51-
it 'returns NO_AUTH_REQUIRED' do
52-
expect(subject.attempt_login(credential).status).to eq(Metasploit::Model::Login::Status::NO_AUTH_REQUIRED)
50+
it 'returns NO_AUTH_REQUIRED' do
51+
expect(subject.attempt_login(credential).status).to eq(Metasploit::Model::Login::Status::NO_AUTH_REQUIRED)
52+
end
5353
end
54-
end
5554

56-
context 'when server returns invalid password' do
57-
let(:res) do
58-
'-ERR invalid password'
59-
end
55+
context 'when server returns invalid password' do
56+
let(:res) { '-ERR invalid password' }
6057

61-
before do
62-
update_socket_res(res)
58+
before do
59+
update_socket_res(res)
60+
end
61+
62+
it 'returns INCORRECT' do
63+
expect(subject.attempt_login(credential).status).to eq(Metasploit::Model::Login::Status::INCORRECT)
64+
end
6365
end
6466

65-
it 'returns INCORRECT' do
66-
expect(subject.attempt_login(credential).status).to eq(Metasploit::Model::Login::Status::INCORRECT)
67+
context 'when server returns OK' do
68+
let(:res) { '+OK' }
69+
70+
before do
71+
update_socket_res(res)
72+
end
73+
74+
it 'returns SUCCESSFUL' do
75+
expect(subject.attempt_login(credential).status).to eq(Metasploit::Model::Login::Status::SUCCESSFUL)
76+
end
6777
end
6878
end
6979

70-
context 'when server returns OK' do
71-
let(:res) do
72-
'+OK'
80+
context 'with Redis version > 6' do
81+
context 'when server returns no password is set' do
82+
let(:res) { '-ERR AUTH <password> called without any password configured for the default user. Are you sure your configuration is correct?' }
83+
84+
before do
85+
update_socket_res(res)
86+
end
87+
88+
it 'returns NO_AUTH_REQUIRED' do
89+
expect(subject.attempt_login(credential).status).to eq(Metasploit::Model::Login::Status::NO_AUTH_REQUIRED)
90+
end
7391
end
7492

75-
before do
76-
update_socket_res(res)
93+
context 'when server returns invalid password' do
94+
let(:res) { '-WRONGPASS invalid username-password pair or user is disabled' }
95+
96+
before do
97+
update_socket_res(res)
98+
end
99+
100+
it 'returns INCORRECT' do
101+
expect(subject.attempt_login(credential).status).to eq(Metasploit::Model::Login::Status::INCORRECT)
102+
end
77103
end
78104

79-
it 'returns SUCCESSFUL' do
80-
expect(subject.attempt_login(credential).status).to eq(Metasploit::Model::Login::Status::SUCCESSFUL)
105+
context 'when server returns OK' do
106+
let(:res) { '+OK' }
107+
108+
before do
109+
update_socket_res(res)
110+
end
111+
112+
it 'returns SUCCESSFUL' do
113+
expect(subject.attempt_login(credential).status).to eq(Metasploit::Model::Login::Status::SUCCESSFUL)
114+
end
81115
end
82116
end
83117
end

0 commit comments

Comments
 (0)