Fixes #1285
This commit is contained in:
@@ -6,18 +6,27 @@ module WPScan
|
|||||||
# Since 4.7 - Need more investigation as it seems WP 4.7.1 reduces the exposure, see https://github.com/wpscanteam/wpscan/issues/1038)
|
# Since 4.7 - Need more investigation as it seems WP 4.7.1 reduces the exposure, see https://github.com/wpscanteam/wpscan/issues/1038)
|
||||||
#
|
#
|
||||||
class WpJsonApi < CMSScanner::Finders::Finder
|
class WpJsonApi < CMSScanner::Finders::Finder
|
||||||
|
MAX_PER_PAGE = 100 # See https://developer.wordpress.org/rest-api/using-the-rest-api/pagination/
|
||||||
|
|
||||||
# @param [ Hash ] opts
|
# @param [ Hash ] opts
|
||||||
#
|
#
|
||||||
# @return [ Array<User> ]
|
# @return [ Array<User> ]
|
||||||
def aggressive(_opts = {})
|
def aggressive(_opts = {})
|
||||||
found = []
|
found = []
|
||||||
|
current_page = 0
|
||||||
|
|
||||||
JSON.parse(Browser.get(api_url).body)&.each do |user|
|
loop do
|
||||||
found << CMSScanner::User.new(user['slug'],
|
current_page += 1
|
||||||
id: user['id'],
|
|
||||||
found_by: found_by,
|
res = Typhoeus.get(api_url,
|
||||||
confidence: 100,
|
params: { per_page: MAX_PER_PAGE, page: current_page })
|
||||||
interesting_entries: [api_url])
|
|
||||||
|
total_pages ||= res.headers['X-WP-TotalPages'].to_i
|
||||||
|
|
||||||
|
users_in_page = users_from_response(res)
|
||||||
|
found += users_in_page
|
||||||
|
|
||||||
|
break if current_page >= total_pages || users_in_page.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
found
|
found
|
||||||
@@ -25,6 +34,23 @@ module WPScan
|
|||||||
found
|
found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param [ Typhoeus::Response ] response
|
||||||
|
#
|
||||||
|
# @return [ Array<User> ] The users from the response
|
||||||
|
def users_from_response(response)
|
||||||
|
found = []
|
||||||
|
|
||||||
|
JSON.parse(response.body)&.each do |user|
|
||||||
|
found << CMSScanner::User.new(user['slug'],
|
||||||
|
id: user['id'],
|
||||||
|
found_by: found_by,
|
||||||
|
confidence: 100,
|
||||||
|
interesting_entries: [response.effective_url])
|
||||||
|
end
|
||||||
|
|
||||||
|
found
|
||||||
|
end
|
||||||
|
|
||||||
# @return [ String ] The URL of the API listing the Users
|
# @return [ String ] The URL of the API listing the Users
|
||||||
def api_url
|
def api_url
|
||||||
@api_url ||= target.url('wp-json/wp/v2/users/')
|
@api_url ||= target.url('wp-json/wp/v2/users/')
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ describe WPScan::Finders::Users::WpJsonApi do
|
|||||||
subject(:finder) { described_class.new(target) }
|
subject(:finder) { described_class.new(target) }
|
||||||
let(:target) { WPScan::Target.new(url) }
|
let(:target) { WPScan::Target.new(url) }
|
||||||
let(:url) { 'http://wp.lab/' }
|
let(:url) { 'http://wp.lab/' }
|
||||||
let(:fixtures) { File.join(FINDERS_FIXTURES, 'users', 'wp_json_api') }
|
let(:fixtures) { FINDERS_FIXTURES.join('users', 'wp_json_api') }
|
||||||
|
|
||||||
describe '#aggressive' do
|
describe '#aggressive' do
|
||||||
|
before { allow(target).to receive(:sub_dir).and_return(false) }
|
||||||
|
|
||||||
|
context 'when only one page of results' do
|
||||||
before do
|
before do
|
||||||
allow(target).to receive(:sub_dir).and_return(false)
|
stub_request(:get, finder.api_url)
|
||||||
stub_request(:get, finder.api_url).to_return(body: body)
|
.with(query: { page: 1, per_page: 100 })
|
||||||
|
.to_return(body: body, headers: {})
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when not a JSON response' do
|
context 'when not a JSON response' do
|
||||||
@@ -18,13 +22,13 @@ describe WPScan::Finders::Users::WpJsonApi do
|
|||||||
|
|
||||||
context 'when a JSON response' do
|
context 'when a JSON response' do
|
||||||
context 'when unauthorised' do
|
context 'when unauthorised' do
|
||||||
let(:body) { File.read(File.join(fixtures, '401.json')) }
|
let(:body) { File.read(fixtures.join('401.json')) }
|
||||||
|
|
||||||
its(:aggressive) { should eql([]) }
|
its(:aggressive) { should eql([]) }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when limited exposure (WP >= 4.7.1)' do
|
context 'when limited exposure (WP >= 4.7.1)' do
|
||||||
let(:body) { File.read(File.join(fixtures, '4.7.2.json')) }
|
let(:body) { File.read(fixtures.join('4.7.2.json')) }
|
||||||
|
|
||||||
it 'returns the expected array of users' do
|
it 'returns the expected array of users' do
|
||||||
users = finder.aggressive
|
users = finder.aggressive
|
||||||
@@ -36,9 +40,42 @@ describe WPScan::Finders::Users::WpJsonApi do
|
|||||||
expect(user.id).to eql 1
|
expect(user.id).to eql 1
|
||||||
expect(user.username).to eql 'admin'
|
expect(user.username).to eql 'admin'
|
||||||
expect(user.confidence).to eql 100
|
expect(user.confidence).to eql 100
|
||||||
expect(user.interesting_entries).to eql ['http://wp.lab/wp-json/wp/v2/users/']
|
expect(user.interesting_entries).to eql ['http://wp.lab/wp-json/wp/v2/users/?page=1&per_page=100']
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when multiple pages of results' do
|
||||||
|
before do
|
||||||
|
stub_request(:get, finder.api_url)
|
||||||
|
.with(query: { page: 1, per_page: 100 })
|
||||||
|
.to_return(body: File.read(fixtures.join('4.7.2.json')), headers: { 'X-WP-TotalPages' => 2 })
|
||||||
|
|
||||||
|
stub_request(:get, finder.api_url)
|
||||||
|
.with(query: { page: 2, per_page: 100 })
|
||||||
|
.to_return(body: File.read(fixtures.join('4.7.2-2.json')), headers: { 'X-WP-TotalPages' => 2 })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the expected array of users' do
|
||||||
|
users = finder.aggressive
|
||||||
|
|
||||||
|
expect(users.size).to eql 2
|
||||||
|
|
||||||
|
user = users.first
|
||||||
|
|
||||||
|
expect(user.id).to eql 1
|
||||||
|
expect(user.username).to eql 'admin'
|
||||||
|
expect(user.confidence).to eql 100
|
||||||
|
expect(user.interesting_entries).to eql ['http://wp.lab/wp-json/wp/v2/users/?page=1&per_page=100']
|
||||||
|
|
||||||
|
user = users.second
|
||||||
|
|
||||||
|
expect(user.id).to eql 20
|
||||||
|
expect(user.username).to eql 'user'
|
||||||
|
expect(user.confidence).to eql 100
|
||||||
|
expect(user.interesting_entries).to eql ['http://wp.lab/wp-json/wp/v2/users/?page=2&per_page=100']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
28
spec/fixtures/finders/users/wp_json_api/4.7.2-2.json
vendored
Normal file
28
spec/fixtures/finders/users/wp_json_api/4.7.2-2.json
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 20,
|
||||||
|
"name": "user",
|
||||||
|
"url": "",
|
||||||
|
"description": "",
|
||||||
|
"link": "http://wp.lab/wordpress-4.7/author/user/",
|
||||||
|
"slug": "user",
|
||||||
|
"avatar_urls": {
|
||||||
|
"24": "http://1.gravatar.com/avatar/473fe256a0c7b9e907b55b2f492f8686?s=24&d=mm&r=g",
|
||||||
|
"48": "http://1.gravatar.com/avatar/473fe256a0c7b9e907b55b2f492f8686?s=48&d=mm&r=g",
|
||||||
|
"96": "http://1.gravatar.com/avatar/473fe256a0c7b9e907b55b2f492f8686?s=96&d=mm&r=g"
|
||||||
|
},
|
||||||
|
"meta": [],
|
||||||
|
"_links": {
|
||||||
|
"self": [
|
||||||
|
{
|
||||||
|
"href": "http://wp.lab/wordpress-4.7/wp-json/wp/v2/users/20"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"collection": [
|
||||||
|
{
|
||||||
|
"href": "http://wp.lab/wordpress-4.7/wp-json/wp/v2/users"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user