commit
5a7ac8c29a
|
@ -0,0 +1,662 @@
|
||||||
|
load Metasploit::Framework.root.join('tools/msu_finder.rb').to_path
|
||||||
|
|
||||||
|
require 'nokogiri'
|
||||||
|
require 'uri'
|
||||||
|
|
||||||
|
describe MicrosoftPatchFinder do
|
||||||
|
|
||||||
|
before(:each) do
|
||||||
|
cli = Rex::Proto::Http::Client.new('127.0.0.1')
|
||||||
|
allow(cli).to receive(:connect)
|
||||||
|
allow(cli).to receive(:request_cgi)
|
||||||
|
allow(cli).to receive(:send_recv).and_return(Rex::Proto::Http::Response.new)
|
||||||
|
allow(Rex::Proto::Http::Client).to receive(:new).and_return(cli)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:technet) do
|
||||||
|
MicrosoftPatchFinder::SiteInfo::TECHNET
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:microsoft) do
|
||||||
|
MicrosoftPatchFinder::SiteInfo::MICROSOFT
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:googleapis) do
|
||||||
|
MicrosoftPatchFinder::SiteInfo::GOOGLEAPIS
|
||||||
|
end
|
||||||
|
|
||||||
|
describe MicrosoftPatchFinder::SiteInfo do
|
||||||
|
context 'Constants' do
|
||||||
|
context 'TECHNET' do
|
||||||
|
it 'returns 157.56.148.23 as the IP' do
|
||||||
|
expect(technet[:ip]).to eq('157.56.148.23')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns technet.microsoft.com as the vhost' do
|
||||||
|
expect(technet[:vhost]).to eq('technet.microsoft.com')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'MICROSOFT' do
|
||||||
|
it 'returns 104.72.230.162 as the IP' do
|
||||||
|
expect(microsoft[:ip]).to eq('104.72.230.162')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns www.microsoft.com as the vhost' do
|
||||||
|
expect(microsoft[:vhost]).to eq('www.microsoft.com')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'GOOGLEAPIS' do
|
||||||
|
it 'returns 74.125.28.95 as the IP' do
|
||||||
|
expect(googleapis[:ip]).to eq('74.125.28.95')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns www.googleapis.com as the vhost' do
|
||||||
|
expect(googleapis[:vhost]).to eq('www.googleapis.com')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe MicrosoftPatchFinder::Helper do
|
||||||
|
|
||||||
|
def get_stdout(&block)
|
||||||
|
out = $stdout
|
||||||
|
$stdout = fake = StringIO.new
|
||||||
|
begin
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
$stdout = out
|
||||||
|
end
|
||||||
|
fake.string
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_stderr(&block)
|
||||||
|
out = $stderr
|
||||||
|
$stderr = fake = StringIO.new
|
||||||
|
begin
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
$stderr = out
|
||||||
|
end
|
||||||
|
fake.string
|
||||||
|
end
|
||||||
|
|
||||||
|
subject(:object_helper) do
|
||||||
|
mod = Object.new
|
||||||
|
mod.extend MicrosoftPatchFinder::Helper
|
||||||
|
mod
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#print_debug' do
|
||||||
|
it 'prints a [DEBUG] message' do
|
||||||
|
output = get_stderr { object_helper.print_debug }
|
||||||
|
expect(output).to include('[DEBUG]')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#print_status' do
|
||||||
|
it 'prints a [*] message' do
|
||||||
|
output = get_stderr { object_helper.print_status }
|
||||||
|
expect(output).to include('[*]')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#print_error' do
|
||||||
|
it 'prints an [ERROR] message' do
|
||||||
|
output = get_stderr { object_helper.print_error }
|
||||||
|
expect(output).to include('[ERROR]')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#print_line' do
|
||||||
|
it 'prints a regular message' do
|
||||||
|
msg = 'TEST'
|
||||||
|
output = get_stdout { object_helper.print_line(msg) }
|
||||||
|
expect(output).to eq("#{msg}\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#send_http_request' do
|
||||||
|
it 'returns a Rex::Proto::Http::Response object' do
|
||||||
|
allow(object_helper).to receive(:print_debug)
|
||||||
|
res = object_helper.send_http_request(MicrosoftPatchFinder::SiteInfo::TECHNET)
|
||||||
|
expect(res).to be_kind_of(Rex::Proto::Http::Response)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe MicrosoftPatchFinder::PatchLinkCollector do
|
||||||
|
|
||||||
|
let(:ms15_100_html) do
|
||||||
|
%Q|
|
||||||
|
<html>
|
||||||
|
<div id="mainBody">
|
||||||
|
<div>
|
||||||
|
<h2>
|
||||||
|
<div>
|
||||||
|
<span>Affected Software</span>
|
||||||
|
<div class="sectionblock">
|
||||||
|
<table>
|
||||||
|
<tr><td><a href="https://www.microsoft.com/downloads/details.aspx?familyid=1">fake link</a></td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</html>
|
||||||
|
|
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:ms07_029_html) do
|
||||||
|
%Q|
|
||||||
|
<html>
|
||||||
|
<div id="mainBody">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="http://technet.microsoft.com">Download the update</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</html>
|
||||||
|
|
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:ms03_039_html) do
|
||||||
|
%Q|
|
||||||
|
<html>
|
||||||
|
<div id="mainBody">
|
||||||
|
<div>
|
||||||
|
<div class="sectionblock">
|
||||||
|
<p>
|
||||||
|
<strong>Download locations</strong>
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="http://technet.microsoft.com">Download</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</html>
|
||||||
|
|
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:ms07_030_html) do
|
||||||
|
%Q|
|
||||||
|
<html>
|
||||||
|
<div id="mainBody">
|
||||||
|
<p>
|
||||||
|
<strong>Affected Software</strong>
|
||||||
|
</p>
|
||||||
|
<table>
|
||||||
|
<tr><td><a href="http://technet.microsoft.com">Download</a></td></tr>
|
||||||
|
</div>
|
||||||
|
</html>
|
||||||
|
|
|
||||||
|
end
|
||||||
|
|
||||||
|
subject(:patch_link_collector) do
|
||||||
|
MicrosoftPatchFinder::PatchLinkCollector.new
|
||||||
|
end
|
||||||
|
|
||||||
|
before(:each) do
|
||||||
|
allow(patch_link_collector).to receive(:print_debug)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#download_advisory' do
|
||||||
|
it 'returns a Rex::Proto::Http::Response object' do
|
||||||
|
res = patch_link_collector.download_advisory('ms15-100')
|
||||||
|
expect(res).to be_kind_of(Rex::Proto::Http::Response)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#get_appropriate_pattern' do
|
||||||
|
|
||||||
|
it 'returns a pattern for ms15-100' do
|
||||||
|
expected_pattern = '//div[@id="mainBody"]//div//div[@class="sectionblock"]//table//a'
|
||||||
|
p = patch_link_collector.get_appropriate_pattern(::Nokogiri::HTML(ms15_100_html))
|
||||||
|
expect(p).to eq(expected_pattern)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a pattern for ms07-029' do
|
||||||
|
expected_pattern = '//div[@id="mainBody"]//ul//li//a[contains(text(), "Download the update")]'
|
||||||
|
p = patch_link_collector.get_appropriate_pattern(::Nokogiri::HTML(ms07_029_html))
|
||||||
|
expect(p).to eq(expected_pattern)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a pattern for ms03-039' do
|
||||||
|
expected_pattern = '//div[@id="mainBody"]//div//div[@class="sectionblock"]//ul//li//a'
|
||||||
|
p = patch_link_collector.get_appropriate_pattern(::Nokogiri::HTML(ms03_039_html))
|
||||||
|
expect(p).to eq(expected_pattern)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a pattern for ms07-030' do
|
||||||
|
expected_pattern = '//div[@id="mainBody"]//table//a'
|
||||||
|
p = patch_link_collector.get_appropriate_pattern(::Nokogiri::HTML(ms07_030_html))
|
||||||
|
expect(p).to eq(expected_pattern)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#get_details_aspx' do
|
||||||
|
let(:details_aspx) do
|
||||||
|
res = Rex::Proto::Http::Response.new
|
||||||
|
allow(res).to receive(:body).and_return(ms15_100_html)
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an URI object to a details aspx' do
|
||||||
|
links = patch_link_collector.get_details_aspx(details_aspx)
|
||||||
|
expected_uri = 'https://www.microsoft.com/downloads/details.aspx?familyid=1'
|
||||||
|
expect(links.length).to eq(1)
|
||||||
|
expect(links.first).to be_kind_of(URI)
|
||||||
|
expect(links.first.to_s).to eq(expected_uri)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#follow_redirect' do
|
||||||
|
let(:expected_header) do
|
||||||
|
{ 'Location' => 'http://example.com/' }
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:http_res) do
|
||||||
|
res = Rex::Proto::Http::Response.new
|
||||||
|
allow(res).to receive(:headers).and_return(expected_header)
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'goes to a location based on the Location HTTP header' do
|
||||||
|
cli = Rex::Proto::Http::Client.new('127.0.0.1')
|
||||||
|
allow(cli).to receive(:connect)
|
||||||
|
allow(cli).to receive(:request_cgi)
|
||||||
|
allow(cli).to receive(:send_recv).and_return(http_res)
|
||||||
|
allow(Rex::Proto::Http::Client).to receive(:new).and_return(cli)
|
||||||
|
|
||||||
|
expect(patch_link_collector.follow_redirect(technet, http_res).headers).to eq(expected_header)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#get_download_page' do
|
||||||
|
it 'returns a Rex::Proto::Http::Response object' do
|
||||||
|
uri = URI('http://www.example.com/')
|
||||||
|
expect(patch_link_collector.get_download_page(uri)).to be_kind_of(Rex::Proto::Http::Response)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#get_download_links' do
|
||||||
|
let(:confirm_aspx) do
|
||||||
|
%Q|
|
||||||
|
<html>
|
||||||
|
<a href="https://www.microsoft.com/en-us/download/confirmation.aspx?id=1">Download</a>
|
||||||
|
</html>
|
||||||
|
|
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:expected_link) do
|
||||||
|
'http://download.microsoft.com/download/9/0/6/906BC7A4-7DF7-4C24-9F9D-3E801AA36ED3/Windows6.0-KB3087918-x86.msu'
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:download_html_res) do
|
||||||
|
html = %Q|
|
||||||
|
<html>
|
||||||
|
<a href="#{expected_link}">Click here</a>
|
||||||
|
</html>
|
||||||
|
|
|
||||||
|
|
||||||
|
res = Rex::Proto::Http::Response
|
||||||
|
allow(res).to receive(:body).and_return(html)
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an array of links' do
|
||||||
|
cli = Rex::Proto::Http::Client.new('127.0.0.1')
|
||||||
|
allow(cli).to receive(:connect)
|
||||||
|
allow(cli).to receive(:request_cgi)
|
||||||
|
allow(cli).to receive(:send_recv).and_return(download_html_res)
|
||||||
|
allow(Rex::Proto::Http::Client).to receive(:new).and_return(cli)
|
||||||
|
|
||||||
|
expect(patch_link_collector.get_download_links(confirm_aspx).first).to eq(expected_link)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#has_advisory?' do
|
||||||
|
it 'returns true if the page is found' do
|
||||||
|
res = Rex::Proto::Http::Response.new
|
||||||
|
expect(patch_link_collector.has_advisory?(res)).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false if the page is not found' do
|
||||||
|
html = %Q|
|
||||||
|
<html>
|
||||||
|
We are sorry. The page you requested cannot be found
|
||||||
|
</html>
|
||||||
|
|
|
||||||
|
|
||||||
|
res = Rex::Proto::Http::Response.new
|
||||||
|
allow(res).to receive(:body).and_return(html)
|
||||||
|
expect(patch_link_collector.has_advisory?(res)).to be_falsey
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#is_valid_msb?' do
|
||||||
|
let(:good_msb) do
|
||||||
|
'MS15-100'
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:bad_msb) do
|
||||||
|
'MS15-01'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true if the MSB format is correct' do
|
||||||
|
expect(patch_link_collector.is_valid_msb?(good_msb)).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false if the MSB format is incorrect' do
|
||||||
|
expect(patch_link_collector.is_valid_msb?(bad_msb)).to be_falsey
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe MicrosoftPatchFinder::TechnetMsbSearch do
|
||||||
|
|
||||||
|
subject(:technet_msb_search) do
|
||||||
|
MicrosoftPatchFinder::TechnetMsbSearch.new
|
||||||
|
end
|
||||||
|
|
||||||
|
before(:each) do
|
||||||
|
allow_any_instance_of(MicrosoftPatchFinder::TechnetMsbSearch).to receive(:print_debug)
|
||||||
|
allow_any_instance_of(MicrosoftPatchFinder::TechnetMsbSearch).to receive(:send_http_request) { |info_obj, info_opts, opts|
|
||||||
|
case opts['uri']
|
||||||
|
when /\/en\-us\/security\/bulletin\/dn602597\.aspx/
|
||||||
|
html = %Q|
|
||||||
|
<div class="sb-search">
|
||||||
|
<div class="SearchBox">
|
||||||
|
<input type="text" id="txtSearch" title="Search Security Bulletins" value="Search Security Bulletins" />
|
||||||
|
<input type="button" id="btnSearch" />
|
||||||
|
</div>
|
||||||
|
<select id="productDropdown">
|
||||||
|
<option value="-1">All</option>
|
||||||
|
<option value="10175">Active Directory</option>
|
||||||
|
<option value="10401">Windows Internet Explorer 10</option>
|
||||||
|
<option value="10486">Windows Internet Explorer 11</option>
|
||||||
|
<option value="1282">Windows Internet Explorer 7</option>
|
||||||
|
<option value="1233">Windows Internet Explorer 8</option>
|
||||||
|
<option value="10054">Windows Internet Explorer 9</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
|
||||||
|
when /\/security\/bulletin\/services\/GetBulletins/
|
||||||
|
html = %Q|{
|
||||||
|
"l":1,
|
||||||
|
"b":[
|
||||||
|
{
|
||||||
|
"d":"9/8/2015",
|
||||||
|
"Id":"MS15-100",
|
||||||
|
"KB":"3087918",
|
||||||
|
"Title":"Vulnerability in Windows Media Center Could Allow Remote Code Execution",
|
||||||
|
"Rating":"Important"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
|
||||||
|
else
|
||||||
|
html = ''
|
||||||
|
end
|
||||||
|
|
||||||
|
res = Rex::Proto::Http::Response.new
|
||||||
|
allow(res).to receive(:body).and_return(html)
|
||||||
|
res
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:ie10) do
|
||||||
|
'Windows Internet Explorer 10'
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:ie10_id) do
|
||||||
|
10401
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#find_msb_numbers' do
|
||||||
|
it 'returns an array of found MSB numbers' do
|
||||||
|
msb = technet_msb_search.find_msb_numbers(ie10)
|
||||||
|
expect(msb).to be_kind_of(Array)
|
||||||
|
expect(msb.first).to eq('ms15-100')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#search' do
|
||||||
|
it 'returns search results in JSON format' do
|
||||||
|
results = technet_msb_search.search(ie10)
|
||||||
|
expect(results).to be_kind_of(Hash)
|
||||||
|
expect(results['b'].first['Id']).to eq('MS15-100')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#search_by_product_ids' do
|
||||||
|
it 'returns an array of found MSB numbers' do
|
||||||
|
results = technet_msb_search.search_by_product_ids([ie10_id])
|
||||||
|
expect(results).to be_kind_of(Array)
|
||||||
|
expect(results.first).to eq('ms15-100')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#search_by_keyword' do
|
||||||
|
it 'returns an array of found MSB numbers' do
|
||||||
|
results = technet_msb_search.search_by_keyword('ms15-100')
|
||||||
|
expect(results).to be_kind_of(Array)
|
||||||
|
expect(results.first).to eq('ms15-100')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#get_product_dropdown_list' do
|
||||||
|
it 'returns an array of products' do
|
||||||
|
results = technet_msb_search.get_product_dropdown_list
|
||||||
|
expect(results).to be_kind_of(Array)
|
||||||
|
expect(results.first).to be_kind_of(Hash)
|
||||||
|
expected_hash = {:option_value=>"10175", :option_text=>"Active Directory"}
|
||||||
|
expect(results.first).to eq(expected_hash)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe MicrosoftPatchFinder::GoogleMsbSearch do
|
||||||
|
|
||||||
|
subject(:google_msb_search) do
|
||||||
|
MicrosoftPatchFinder::GoogleMsbSearch.new
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:json_data) do
|
||||||
|
%Q|{
|
||||||
|
"kind": "customsearch#search",
|
||||||
|
"url": {
|
||||||
|
"type": "application/json",
|
||||||
|
"template": ""
|
||||||
|
},
|
||||||
|
"queries": {
|
||||||
|
"request": [
|
||||||
|
{
|
||||||
|
"title": "Google Custom Search - internet",
|
||||||
|
"totalResults": "1",
|
||||||
|
"searchTerms": "internet",
|
||||||
|
"count": 10,
|
||||||
|
"startIndex": 1,
|
||||||
|
"inputEncoding": "utf8",
|
||||||
|
"outputEncoding": "utf8",
|
||||||
|
"safe": "off",
|
||||||
|
"cx": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
"title": "Technet.microsoft"
|
||||||
|
},
|
||||||
|
"searchInformation": {
|
||||||
|
"searchTime": 0.413407,
|
||||||
|
"formattedSearchTime": "0.41",
|
||||||
|
"totalResults": "1",
|
||||||
|
"formattedTotalResults": "1"
|
||||||
|
},
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"kind": "customsearch#result",
|
||||||
|
"title": "Microsoft Security Bulletin MS15-093 - Critical",
|
||||||
|
"htmlTitle": "Microsoft Security Bulletin MS15-093 - Critical",
|
||||||
|
"link": "https://technet.microsoft.com/en-us/library/security/ms15-093.aspx",
|
||||||
|
"displayLink": "technet.microsoft.com",
|
||||||
|
"snippet": "",
|
||||||
|
"htmlSnippet": "",
|
||||||
|
"cacheId": "2xDJB6zqL_sJ",
|
||||||
|
"formattedUrl": "https://technet.microsoft.com/en-us/library/security/ms15-093.aspx",
|
||||||
|
"htmlFormattedUrl": "https://technet.microsoft.com/en-us/library/security/ms15-093.aspx",
|
||||||
|
"pagemap": {
|
||||||
|
"metatags": [
|
||||||
|
{
|
||||||
|
"search.mshkeyworda": "ms15-093",
|
||||||
|
"search.mshattr.assetid": "ms15-093",
|
||||||
|
"search.mshattr.docset": "bulletin",
|
||||||
|
"search.mshattr.sarticletype": "bulletin",
|
||||||
|
"search.mshattr.sarticleid": "MS15-093",
|
||||||
|
"search.mshattr.sarticletitle": "Security Update for Internet Explorer",
|
||||||
|
"search.mshattr.sarticledate": "2015-08-20",
|
||||||
|
"search.mshattr.sarticleseverity": "Critical",
|
||||||
|
"search.mshattr.sarticleversion": "1.1",
|
||||||
|
"search.mshattr.sarticlerevisionnote": "",
|
||||||
|
"search.mshattr.sarticleseosummary": "",
|
||||||
|
"search.mshattr.skbnumber": "3088903",
|
||||||
|
"search.mshattr.prefix": "MSRC",
|
||||||
|
"search.mshattr.topictype": "kbOrient",
|
||||||
|
"search.mshattr.preferredlib": "/library/security",
|
||||||
|
"search.mshattr.preferredsitename": "TechNet",
|
||||||
|
"search.mshattr.docsettitle": "MSRC Document",
|
||||||
|
"search.mshattr.docsetroot": "Mt404691",
|
||||||
|
"search.save": "history",
|
||||||
|
"search.microsoft.help.id": "ms15-093",
|
||||||
|
"search.description": "",
|
||||||
|
"search.mscategory": "dn567670",
|
||||||
|
"search.mscategoryv": "dn567670Security10",
|
||||||
|
"search.tocnodeid": "mt404691",
|
||||||
|
"mshkeyworda": "ms15-093",
|
||||||
|
"mshattr": "AssetID:ms15-093",
|
||||||
|
"save": "history",
|
||||||
|
"microsoft.help.id": "ms15-093"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
|
||||||
|
end
|
||||||
|
|
||||||
|
before(:each) do
|
||||||
|
allow_any_instance_of(MicrosoftPatchFinder::GoogleMsbSearch).to receive(:print_debug)
|
||||||
|
allow_any_instance_of(MicrosoftPatchFinder::GoogleMsbSearch).to receive(:send_http_request) { |info_obj, info_opts, opts|
|
||||||
|
res = Rex::Proto::Http::Response.new
|
||||||
|
allow(res).to receive(:body).and_return(json_data)
|
||||||
|
res
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:expected_msb) do
|
||||||
|
'ms15-093'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#find_msb_numbers' do
|
||||||
|
it 'returns an array of msb numbers' do
|
||||||
|
results = google_msb_search.find_msb_numbers(expected_msb)
|
||||||
|
expect(results).to be_kind_of(Array)
|
||||||
|
expect(results).to eq([expected_msb])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#search' do
|
||||||
|
it 'returns a hash (json data)' do
|
||||||
|
results = google_msb_search.search(starting_index: 1)
|
||||||
|
expect(results).to be_kind_of(Hash)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#parse_results' do
|
||||||
|
it 'returns a hash (json data)' do
|
||||||
|
res = Rex::Proto::Http::Response.new
|
||||||
|
allow(res).to receive(:body).and_return(json_data)
|
||||||
|
|
||||||
|
results = google_msb_search.parse_results(res)
|
||||||
|
expect(results).to be_kind_of(Hash)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#get_total_results' do
|
||||||
|
it 'returns a fixnum' do
|
||||||
|
total = google_msb_search.get_total_results(JSON.parse(json_data))
|
||||||
|
expect(total).to be_kind_of(Fixnum)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#get_next_index' do
|
||||||
|
it 'returns a fixnum' do
|
||||||
|
i = google_msb_search.get_next_index(JSON.parse(json_data))
|
||||||
|
expect(i).to be_kind_of(Fixnum)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe MicrosoftPatchFinder::Driver do
|
||||||
|
|
||||||
|
let(:msb) do
|
||||||
|
'ms15-100'
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:expected_link) do
|
||||||
|
'http://download.microsoft.com/download/9/0/6/906BC7A4-7DF7-4C24-9F9D-3E801AA36ED3/Windows6.0-KB3087918-x86.msu'
|
||||||
|
end
|
||||||
|
|
||||||
|
before(:each) do
|
||||||
|
opts = { keyword: msb }
|
||||||
|
allow(MicrosoftPatchFinder::OptsConsole).to receive(:get_parsed_options).and_return(opts)
|
||||||
|
allow_any_instance_of(MicrosoftPatchFinder::PatchLinkCollector).to receive(:download_advisory).and_return(Rex::Proto::Http::Response.new)
|
||||||
|
allow_any_instance_of(MicrosoftPatchFinder::PatchLinkCollector).to receive(:get_details_aspx).and_return([expected_link])
|
||||||
|
allow_any_instance_of(MicrosoftPatchFinder::PatchLinkCollector).to receive(:get_download_page).and_return(Rex::Proto::Http::Response.new)
|
||||||
|
allow_any_instance_of(MicrosoftPatchFinder::PatchLinkCollector).to receive(:get_download_links).and_return([expected_link])
|
||||||
|
allow_any_instance_of(MicrosoftPatchFinder::Driver).to receive(:print_debug)
|
||||||
|
allow_any_instance_of(MicrosoftPatchFinder::Driver).to receive(:print_error)
|
||||||
|
allow_any_instance_of(MicrosoftPatchFinder::PatchLinkCollector).to receive(:print_debug)
|
||||||
|
allow_any_instance_of(MicrosoftPatchFinder::PatchLinkCollector).to receive(:print_error)
|
||||||
|
end
|
||||||
|
|
||||||
|
subject(:driver) do
|
||||||
|
MicrosoftPatchFinder::Driver.new
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#get_download_links' do
|
||||||
|
it 'returns an array of links' do
|
||||||
|
results = driver.get_download_links(msb)
|
||||||
|
expect(results).to be_kind_of(Array)
|
||||||
|
expect(results.first).to eq(expected_link)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#google_search' do
|
||||||
|
it 'returns search results' do
|
||||||
|
skip('See rspec for MicrosoftPatchFinder::GoogleMsbSearch#find_msb_numbers')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#technet_search' do
|
||||||
|
it 'returns search results' do
|
||||||
|
skip('See rspec for MicrosoftPatchFinder::TechnetMsbSearch#find_msb_numbers')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -0,0 +1,771 @@
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
###
|
||||||
|
#
|
||||||
|
# This sceript will enumerate download links for Microsoft patches.
|
||||||
|
#
|
||||||
|
# Author:
|
||||||
|
# * sinn3r
|
||||||
|
#
|
||||||
|
###
|
||||||
|
|
||||||
|
|
||||||
|
msfbase = __FILE__
|
||||||
|
while File.symlink?(msfbase)
|
||||||
|
msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase))
|
||||||
|
end
|
||||||
|
$:.unshift(File.expand_path(File.join(File.dirname(msfbase), '..', 'lib')))
|
||||||
|
require 'rex'
|
||||||
|
require 'nokogiri'
|
||||||
|
require 'uri'
|
||||||
|
require 'json'
|
||||||
|
require 'optparse'
|
||||||
|
|
||||||
|
module MicrosoftPatchFinder
|
||||||
|
|
||||||
|
module SiteInfo
|
||||||
|
TECHNET = {
|
||||||
|
ip: '157.56.148.23',
|
||||||
|
vhost: 'technet.microsoft.com'
|
||||||
|
}
|
||||||
|
|
||||||
|
MICROSOFT = {
|
||||||
|
ip: '104.72.230.162',
|
||||||
|
vhost: 'www.microsoft.com'
|
||||||
|
}
|
||||||
|
|
||||||
|
GOOGLEAPIS = {
|
||||||
|
ip: '74.125.28.95',
|
||||||
|
vhost: 'www.googleapis.com'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# This provides whatever other classes need.
|
||||||
|
module Helper
|
||||||
|
|
||||||
|
# Prints a debug message.
|
||||||
|
#
|
||||||
|
# @param msg [String] The message to print.
|
||||||
|
# @return [void]
|
||||||
|
def print_debug(msg='')
|
||||||
|
$stderr.puts "[DEBUG] #{msg}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Prints a status message.
|
||||||
|
#
|
||||||
|
# @param msg [String] The message to print.
|
||||||
|
# @return [void]
|
||||||
|
def print_status(msg='')
|
||||||
|
$stderr.puts "[*] #{msg}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Prints an error message.
|
||||||
|
#
|
||||||
|
# @param msg [String] The message to print.
|
||||||
|
# @return [void]
|
||||||
|
def print_error(msg='')
|
||||||
|
$stderr.puts "[ERROR] #{msg}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Prints a regular message.
|
||||||
|
#
|
||||||
|
# @param msg [String] The message to print.
|
||||||
|
# @return pvoid
|
||||||
|
def print_line(msg='')
|
||||||
|
$stdout.puts msg
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sends an HTTP request with Rex.
|
||||||
|
#
|
||||||
|
# @param rhost [Hash] Information about the target host. Use MicrosoftPatchFinder::SiteInfo.
|
||||||
|
# @option rhost [String] :vhost
|
||||||
|
# @option rhost [String] :ip IPv4 address
|
||||||
|
# @param opts [Hash] Information about the Rex request.
|
||||||
|
# @raise [RuntimeError] Failure to make a request.
|
||||||
|
# @return [Rex::Proto::Http::Response]
|
||||||
|
def send_http_request(rhost, opts={})
|
||||||
|
res = nil
|
||||||
|
|
||||||
|
opts.merge!({'vhost'=>rhost[:vhost]})
|
||||||
|
|
||||||
|
print_debug("Requesting: #{opts['uri']}")
|
||||||
|
|
||||||
|
cli = Rex::Proto::Http::Client.new(rhost[:ip], 443, {}, true, 'TLS1')
|
||||||
|
tries = 1
|
||||||
|
begin
|
||||||
|
cli.connect
|
||||||
|
req = cli.request_cgi(opts)
|
||||||
|
res = cli.send_recv(req)
|
||||||
|
rescue ::EOFError, Errno::ETIMEDOUT ,Errno::ECONNRESET, Rex::ConnectionError, OpenSSL::SSL::SSLError, ::Timeout::Error => e
|
||||||
|
if tries < 3
|
||||||
|
print_error("Failed to make a request, but will try again in 5 seconds...")
|
||||||
|
sleep(5)
|
||||||
|
tries += 1
|
||||||
|
retry
|
||||||
|
else
|
||||||
|
raise "[x] Unable to make a request: #{e.class} #{e.message}\n#{e.backtrace * "\n"}"
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
cli.close
|
||||||
|
end
|
||||||
|
|
||||||
|
res
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Collects MSU download links from Technet.
|
||||||
|
class PatchLinkCollector
|
||||||
|
include MicrosoftPatchFinder::Helper
|
||||||
|
|
||||||
|
# Returns a response of an advisory page.
|
||||||
|
#
|
||||||
|
# @param msb [String] MSB number in this format: msxx-xxx
|
||||||
|
# @return [Rex::Proto::Http::Response]
|
||||||
|
def download_advisory(msb)
|
||||||
|
send_http_request(SiteInfo::TECHNET, {
|
||||||
|
'uri' => "/en-us/library/security/#{msb}.aspx"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Returns the most appropriate pattern that could be used to parse and extract links from an advisory.
|
||||||
|
#
|
||||||
|
# @param n [Nokogiri::HTML::Document] The advisory page parsed by Nokogiri
|
||||||
|
# @return [Hash]
|
||||||
|
def get_appropriate_pattern(n)
|
||||||
|
# These pattern checks need to be in this order.
|
||||||
|
patterns = [
|
||||||
|
# This works from MS14-001 until the most recent
|
||||||
|
{
|
||||||
|
check: '//div[@id="mainBody"]//div//h2//div//span[contains(text(), "Affected Software")]',
|
||||||
|
pattern: '//div[@id="mainBody"]//div//div[@class="sectionblock"]//table//a'
|
||||||
|
},
|
||||||
|
# This works from ms03-040 until MS07-029
|
||||||
|
{
|
||||||
|
check: '//div[@id="mainBody"]//ul//li//a[contains(text(), "Download the update")]',
|
||||||
|
pattern: '//div[@id="mainBody"]//ul//li//a[contains(text(), "Download the update")]'
|
||||||
|
},
|
||||||
|
# This works from sometime until ms03-039
|
||||||
|
{
|
||||||
|
check: '//div[@id="mainBody"]//div//div[@class="sectionblock"]//p//strong[contains(text(), "Download locations")]',
|
||||||
|
pattern: '//div[@id="mainBody"]//div//div[@class="sectionblock"]//ul//li//a'
|
||||||
|
},
|
||||||
|
# This works from MS07-030 until MS13-106 (the last update in 2013)
|
||||||
|
# The check is pretty short so if it kicks in too early, it tends to create false positives.
|
||||||
|
# So it goes last.
|
||||||
|
{
|
||||||
|
check: '//div[@id="mainBody"]//p//strong[contains(text(), "Affected Software")]',
|
||||||
|
pattern: '//div[@id="mainBody"]//table//a'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
patterns.each do |pattern|
|
||||||
|
if n.at_xpath(pattern[:check])
|
||||||
|
return pattern[:pattern]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Returns the details page for an advisory.
|
||||||
|
#
|
||||||
|
# @param res [Rex::Proto::Http::Response]
|
||||||
|
# @return [Array<URI::HTTP>] An array of URI objects.
|
||||||
|
def get_details_aspx(res)
|
||||||
|
links = []
|
||||||
|
|
||||||
|
page = res.body
|
||||||
|
n = ::Nokogiri::HTML(page)
|
||||||
|
|
||||||
|
appropriate_pattern = get_appropriate_pattern(n)
|
||||||
|
|
||||||
|
n.search(appropriate_pattern).each do |anchor|
|
||||||
|
found_link = anchor.attributes['href'].value
|
||||||
|
if /https:\/\/www\.microsoft\.com\/downloads\/details\.aspx\?familyid=/i === found_link
|
||||||
|
begin
|
||||||
|
links << URI(found_link)
|
||||||
|
rescue ::URI::InvalidURIError
|
||||||
|
print_error "Unable to parse URI: #{found_link}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
links
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Returns the redirected page.
|
||||||
|
#
|
||||||
|
# @param rhost [Hash] From MicrosoftPatchFinder::SiteInfo
|
||||||
|
# @param res [Rex::Proto::Http::Response]
|
||||||
|
# @return [Rex::Proto::Http::Response]
|
||||||
|
def follow_redirect(rhost, res)
|
||||||
|
opts = {
|
||||||
|
'method' => 'GET',
|
||||||
|
'uri' => res.headers['Location']
|
||||||
|
}
|
||||||
|
|
||||||
|
send_http_request(rhost, opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Returns the download page of an advisory.
|
||||||
|
#
|
||||||
|
# @param uri [URI::HTTP]
|
||||||
|
# @return [Rex::Proto::Http::Response]
|
||||||
|
def get_download_page(uri)
|
||||||
|
opts = {
|
||||||
|
'method' => 'GET',
|
||||||
|
'uri' => uri.request_uri
|
||||||
|
}
|
||||||
|
|
||||||
|
res = send_http_request(SiteInfo::MICROSOFT, opts)
|
||||||
|
|
||||||
|
if res.headers['Location']
|
||||||
|
return follow_redirect(SiteInfo::MICROSOFT, res)
|
||||||
|
end
|
||||||
|
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Returns a collection of found MSU download links from an advisory.
|
||||||
|
#
|
||||||
|
# @param page [String] The HTML page of the advisory.
|
||||||
|
# @return [Array<String>] An array of links
|
||||||
|
def get_download_links(page)
|
||||||
|
page = ::Nokogiri::HTML(page)
|
||||||
|
|
||||||
|
relative_uri = page.search('a').select { |a|
|
||||||
|
a.attributes['href'] && a.attributes['href'].value.include?('confirmation.aspx?id=')
|
||||||
|
}.first
|
||||||
|
|
||||||
|
return [] unless relative_uri
|
||||||
|
relative_uri = relative_uri.attributes['href'].value
|
||||||
|
|
||||||
|
absolute_uri = URI("https://www.microsoft.com/en-us/download/#{relative_uri}")
|
||||||
|
opts = {
|
||||||
|
'method' => 'GET',
|
||||||
|
'uri' => absolute_uri.request_uri
|
||||||
|
}
|
||||||
|
res = send_http_request(SiteInfo::MICROSOFT, opts)
|
||||||
|
n = ::Nokogiri::HTML(res.body)
|
||||||
|
|
||||||
|
n.search('a').select { |a|
|
||||||
|
a.attributes['href'] && a.attributes['href'].value.include?('http://download.microsoft.com/download/')
|
||||||
|
}.map! { |a| a.attributes['href'].value }.uniq
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Returns whether the page is an advisory or not.
|
||||||
|
#
|
||||||
|
# @param res [Rex::Proto::Http::Response]
|
||||||
|
# @return [Boolean] true if the page is an advisory, otherwise false.
|
||||||
|
def has_advisory?(res)
|
||||||
|
!res.body.include?('We are sorry. The page you requested cannot be found')
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Returns whether the number is in valid MSB format or not.
|
||||||
|
#
|
||||||
|
# @param msb [String] The number to check.
|
||||||
|
# @return [Boolean] true if the number is in MSB format, otherwise false.
|
||||||
|
def is_valid_msb?(msb)
|
||||||
|
/^ms\d\d\-\d\d\d$/i === msb
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# A class that searches advisories from Technet.
|
||||||
|
class TechnetMsbSearch
|
||||||
|
include MicrosoftPatchFinder::Helper
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
opts = {
|
||||||
|
'method' => 'GET',
|
||||||
|
'uri' => '/en-us/security/bulletin/dn602597.aspx'
|
||||||
|
}
|
||||||
|
res = send_http_request(SiteInfo::TECHNET, opts)
|
||||||
|
@firstpage ||= res.body
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Returns a collection of found MSB numbers either from the product list, or generic search.
|
||||||
|
#
|
||||||
|
# @param keyword [String] The product to look for.
|
||||||
|
# @return [Array<String>]
|
||||||
|
def find_msb_numbers(keyword)
|
||||||
|
product_list_matches = get_product_dropdown_list.select { |p| Regexp.new(keyword) === p[:option_text] }
|
||||||
|
if product_list_matches.empty?
|
||||||
|
print_debug("Did not find a match from the product list, attempting a generic search")
|
||||||
|
search_by_keyword(keyword)
|
||||||
|
else
|
||||||
|
product_names = []
|
||||||
|
ids = []
|
||||||
|
product_list_matches.each do |e|
|
||||||
|
ids << e[:option_value]
|
||||||
|
product_names << e[:option_text]
|
||||||
|
end
|
||||||
|
print_debug("Matches from the product list (#{product_names.length}): #{ product_names * ', ' }")
|
||||||
|
search_by_product_ids(ids)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Returns the search results in JSON format.
|
||||||
|
#
|
||||||
|
# @param keyword [String] The keyword to search.
|
||||||
|
# @return [Hash] JSON data.
|
||||||
|
def search(keyword)
|
||||||
|
opts = {
|
||||||
|
'method' => 'GET',
|
||||||
|
'uri' => '/security/bulletin/services/GetBulletins',
|
||||||
|
'vars_get' => {
|
||||||
|
'searchText' => keyword,
|
||||||
|
'sortField' => '0',
|
||||||
|
'sortOrder' => '1',
|
||||||
|
'currentPage' => '1',
|
||||||
|
'bulletinsPerPage' => '9999',
|
||||||
|
'locale' => 'en-us'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res = send_http_request(SiteInfo::TECHNET, opts)
|
||||||
|
begin
|
||||||
|
return JSON.parse(res.body)
|
||||||
|
rescue JSON::ParserError
|
||||||
|
end
|
||||||
|
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Performs a search based on product IDs
|
||||||
|
#
|
||||||
|
# @param ids [Array<Fixnum>] An array of product IDs.
|
||||||
|
# @return [Array<String>] An array of found MSB numbers.
|
||||||
|
def search_by_product_ids(ids)
|
||||||
|
msb_numbers = []
|
||||||
|
|
||||||
|
ids.each do |id|
|
||||||
|
j = search(id)
|
||||||
|
msb = j['b'].collect { |e| e['Id']}.map{ |e| e.downcase}
|
||||||
|
msb_numbers.concat(msb)
|
||||||
|
end
|
||||||
|
|
||||||
|
msb_numbers
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Performs a search based on a keyword
|
||||||
|
#
|
||||||
|
# @param keyword [String]
|
||||||
|
# @return [Array<String>] An array of found MSB numbers
|
||||||
|
def search_by_keyword(keyword)
|
||||||
|
j = search(keyword)
|
||||||
|
j['b'].collect { |e| e['Id']}.map{ |e| e.downcase }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Returns the product list that Technet currently supports for searching.
|
||||||
|
#
|
||||||
|
# @return [Array<Hash>]
|
||||||
|
def get_product_dropdown_list
|
||||||
|
@product_dropdown_list ||= lambda {
|
||||||
|
list = []
|
||||||
|
|
||||||
|
page = ::Nokogiri::HTML(firstpage)
|
||||||
|
page.search('//div[@class="sb-search"]//select[@id="productDropdown"]//option').each do |product|
|
||||||
|
option_value = product.attributes['value'].value
|
||||||
|
option_text = product.text
|
||||||
|
next if option_value == '-1' # This is the ALL option
|
||||||
|
list << { option_value: option_value, option_text: option_text }
|
||||||
|
end
|
||||||
|
|
||||||
|
list
|
||||||
|
}.call
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :firstpage
|
||||||
|
end
|
||||||
|
|
||||||
|
class GoogleMsbSearch
|
||||||
|
include MicrosoftPatchFinder::Helper
|
||||||
|
|
||||||
|
# API Doc:
|
||||||
|
# https://developers.google.com/custom-search/json-api/v1/using_rest
|
||||||
|
# Known bug:
|
||||||
|
# * Always gets 20 MSB results. Weird.
|
||||||
|
|
||||||
|
def initialize(opts={})
|
||||||
|
@api_key = opts[:api_key]
|
||||||
|
@search_engine_id = opts[:search_engine_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Returns the MSB numbers associated with the keyword.
|
||||||
|
#
|
||||||
|
# @param keyword [String] The keyword to search for in an advisory.
|
||||||
|
# @return [Array<String>] MSB numbers
|
||||||
|
def find_msb_numbers(keyword)
|
||||||
|
msb_numbers = []
|
||||||
|
next_starting_index = 1
|
||||||
|
|
||||||
|
begin
|
||||||
|
while
|
||||||
|
results = search(keyword: keyword, starting_index: next_starting_index)
|
||||||
|
items = results['items']
|
||||||
|
items.each do |item|
|
||||||
|
title = item['title']
|
||||||
|
msb = title.scan(/Microsoft Security Bulletin (MS\d\d\-\d\d\d)/).flatten.first
|
||||||
|
if msb
|
||||||
|
msb_numbers << msb.downcase
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
next_starting_index = get_next_index(results)
|
||||||
|
next_page = results['queries']['nextPage']
|
||||||
|
|
||||||
|
# Google API Documentation:
|
||||||
|
# https://developers.google.com/custom-search/json-api/v1/using_rest
|
||||||
|
# "This role is not present if the current results are the last page.
|
||||||
|
# Note: This API returns up to the first 100 results only."
|
||||||
|
break if next_page.nil? || next_starting_index > 100
|
||||||
|
end
|
||||||
|
rescue RuntimeError => e
|
||||||
|
print_error(e.message)
|
||||||
|
return msb_numbers.uniq
|
||||||
|
end
|
||||||
|
|
||||||
|
msb_numbers.uniq
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Performs a search using Google API
|
||||||
|
#
|
||||||
|
# @param opts [Hash]
|
||||||
|
# @options opts [String] :keyword The keyword to search
|
||||||
|
# @return [Hash] JSON data
|
||||||
|
def search(opts={})
|
||||||
|
starting_index = opts[:starting_index]
|
||||||
|
|
||||||
|
search_string = [
|
||||||
|
opts[:keyword],
|
||||||
|
'intitle:"Microsoft Security Bulletin"',
|
||||||
|
'-"Microsoft Security Bulletin Summary"'
|
||||||
|
].join(' ')
|
||||||
|
|
||||||
|
opts = {
|
||||||
|
'method' => 'GET',
|
||||||
|
'uri' => '/customsearch/v1',
|
||||||
|
'vars_get' => {
|
||||||
|
'key' => api_key,
|
||||||
|
'cx' => search_engine_id,
|
||||||
|
'q' => search_string,
|
||||||
|
'start' => starting_index.to_s,
|
||||||
|
'num' => '10', # 10 is max
|
||||||
|
'c2coff' => '1' # 1 = Disabled, 0 = Enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res = send_http_request(SiteInfo::GOOGLEAPIS, opts)
|
||||||
|
results = parse_results(res)
|
||||||
|
if starting_index == 1
|
||||||
|
print_debug("Number of search results: #{get_total_results(results)}")
|
||||||
|
end
|
||||||
|
|
||||||
|
results
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Parse Google API search results
|
||||||
|
#
|
||||||
|
# @param res [Rex::Proto::Http::Response]
|
||||||
|
# @raise [RuntimeError] If Google returns an error
|
||||||
|
# @return [Hash]
|
||||||
|
def parse_results(res)
|
||||||
|
j = JSON.parse(res.body)
|
||||||
|
|
||||||
|
if j['error']
|
||||||
|
message = j['error']['errors'].first['message']
|
||||||
|
reason = j['error']['errors'].first['reason']
|
||||||
|
raise "Google Search failed. #{message} (#{reason})"
|
||||||
|
end
|
||||||
|
|
||||||
|
j
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Returns the total results.
|
||||||
|
#
|
||||||
|
# @param j [Hash] JSON data from Google.
|
||||||
|
# @return [Fixnum]
|
||||||
|
def get_total_results(j)
|
||||||
|
j['queries']['request'].first['totalResults'].to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Returns the next index.
|
||||||
|
#
|
||||||
|
# @param j [Hash] JSON data from Google.
|
||||||
|
# @return [Fixnum]
|
||||||
|
def get_next_index(j)
|
||||||
|
j['queries']['nextPage'] ? j['queries']['nextPage'].first['startIndex'] : 0
|
||||||
|
end
|
||||||
|
|
||||||
|
# @!attribute api_key
|
||||||
|
# @return [String] The Google API key
|
||||||
|
attr_reader :api_key
|
||||||
|
|
||||||
|
# @!attribute search_engine_id
|
||||||
|
# @return [String] The Google Custom Search Engine ID
|
||||||
|
attr_reader :search_engine_id
|
||||||
|
end
|
||||||
|
|
||||||
|
class OptsConsole
|
||||||
|
def self.banner
|
||||||
|
%Q|
|
||||||
|
Usage: #{__FILE__} [options]
|
||||||
|
|
||||||
|
The following example will download all IE update links:
|
||||||
|
#{__FILE__} -q "Internet Explorer"
|
||||||
|
|
||||||
|
Searching advisories via Technet:
|
||||||
|
When you submit a query, the Technet search engine will first look it up from a product list,
|
||||||
|
and then return all the advisories that include the keyword you are looking for. If there's
|
||||||
|
no match from the product list, then the script will try a generic search. The generic method
|
||||||
|
also means you can search by MSB, KB, or even the CVE number.
|
||||||
|
|
||||||
|
Searching advisories via Google:
|
||||||
|
Searching via Google requires an API key and an Search Engine ID from Google. To obtain these,
|
||||||
|
make sure you have a Google account (such as Gmail), and then do the following:
|
||||||
|
1. Go to Google Developer's Console
|
||||||
|
1. Enable Custom Search API
|
||||||
|
2. Create a browser type credential. The credential is the API key.
|
||||||
|
2. Go to Custom Search
|
||||||
|
1. Create a new search engine
|
||||||
|
2. Under Sites to Search, set: technet.microsoft.com
|
||||||
|
3. In your search site, get the Search Engine ID under the Basics tab.
|
||||||
|
By default, Google has a quota limit of 1000 queries per day. You can raise this limit with
|
||||||
|
a fee.
|
||||||
|
|
||||||
|
The way this tool uses Google to find advisories is the same as doing the following manually:
|
||||||
|
[Query] site:technet.microsoft.com intitle:"Microsoft Security Bulletin" -"Microsoft Security Bulletin Summary"
|
||||||
|
|
||||||
|
Dryrun:
|
||||||
|
If you'd like to double check on false positives, you can use the -d flag and manually verify
|
||||||
|
the accuracy of the search results before actually collecting the download links.
|
||||||
|
|
||||||
|
Download:
|
||||||
|
The following trick demonstrates how you can automatically download the updates:
|
||||||
|
ruby #{__FILE__} -q "ms15-100" -r x86 > /tmp/list.txt && wget -i /tmp/list.txt
|
||||||
|
|
||||||
|
Patch Extraction:
|
||||||
|
After downloading the patch, you can use the extract_msu.bat tool to automatically extract
|
||||||
|
Microsoft patches.
|
||||||
|
|
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.get_parsed_options
|
||||||
|
options = {}
|
||||||
|
|
||||||
|
parser = OptionParser.new do |opt|
|
||||||
|
opt.banner = banner.strip.gsub(/^[[:blank:]]{4}/, '')
|
||||||
|
opt.separator ''
|
||||||
|
opt.separator 'Specific options:'
|
||||||
|
|
||||||
|
opt.on('-q', '--query <keyword>', 'Find advisories that include this keyword') do |v|
|
||||||
|
options[:keyword] = v
|
||||||
|
end
|
||||||
|
|
||||||
|
opt.on('-s', '--search-engine <engine>', '(Optional) The type of search engine to use (Technet or Google). Default: Technet') do |v|
|
||||||
|
case v.to_s
|
||||||
|
when /^google$/i
|
||||||
|
options[:search_engine] = :google
|
||||||
|
when /^technet$/i
|
||||||
|
options[:search_engine] = :technet
|
||||||
|
else
|
||||||
|
raise OptionParser::MissingArgument, "Invalid search engine: #{v}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
opt.on('-r', '--regex <string>', '(Optional) Specify what type of links you want') do |v|
|
||||||
|
options[:regex] = v
|
||||||
|
end
|
||||||
|
|
||||||
|
opt.on('--apikey <key>', '(Optional) Google API key. Set this if the search engine is Google') do |v|
|
||||||
|
options[:google_api_key] = v
|
||||||
|
end
|
||||||
|
|
||||||
|
opt.on('--cx <id>', '(Optional) Google search engine ID. Set this if the search engine is Google') do |v|
|
||||||
|
options[:google_search_engine_id] = v
|
||||||
|
end
|
||||||
|
|
||||||
|
opt.on('-d', '--dryrun', '(Optional) Perform a search, but do not fetch download links. Default: no') do |v|
|
||||||
|
options[:dryrun] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
opt.on_tail('-h', '--help', 'Show this message') do
|
||||||
|
$stderr.puts opt
|
||||||
|
exit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
parser.parse!
|
||||||
|
|
||||||
|
if options.empty?
|
||||||
|
raise OptionParser::MissingArgument, 'No options set, try -h for usage'
|
||||||
|
elsif options[:keyword].nil? || options[:keyword].empty?
|
||||||
|
raise OptionParser::MissingArgument, '-q is required'
|
||||||
|
end
|
||||||
|
|
||||||
|
unless options[:search_engine]
|
||||||
|
options[:search_engine] = :technet
|
||||||
|
end
|
||||||
|
|
||||||
|
if options[:search_engine] == :google
|
||||||
|
if options[:google_api_key].nil? || options[:google_search_engine_id].empty?
|
||||||
|
raise OptionParser::MissingArgument, 'Search engine is Google, but no API key specified'
|
||||||
|
elsif options[:google_search_engine_id].nil? || options[:google_search_engine_id].empty?
|
||||||
|
raise OptionParser::MissingArgument, 'Search engine is Google, but no search engine ID specified'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
options
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Driver
|
||||||
|
include MicrosoftPatchFinder::Helper
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
begin
|
||||||
|
@args = MicrosoftPatchFinder::OptsConsole.get_parsed_options
|
||||||
|
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
||||||
|
print_error(e.message)
|
||||||
|
exit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns download links.
|
||||||
|
#
|
||||||
|
# @param msb [String] MSB number.
|
||||||
|
# @param regex [String] The regex pattern to use to collect specific download URLs.
|
||||||
|
# @return [Array<String>] Download links
|
||||||
|
def get_download_links(msb, regex=nil)
|
||||||
|
msft = MicrosoftPatchFinder::PatchLinkCollector.new
|
||||||
|
|
||||||
|
unless msft.is_valid_msb?(msb)
|
||||||
|
print_error "Not a valid MSB format."
|
||||||
|
print_error "Example of a correct one: ms15-100"
|
||||||
|
return []
|
||||||
|
end
|
||||||
|
|
||||||
|
res = msft.download_advisory(msb)
|
||||||
|
|
||||||
|
if !msft.has_advisory?(res)
|
||||||
|
print_error "The advisory cannot be found"
|
||||||
|
return []
|
||||||
|
end
|
||||||
|
|
||||||
|
links = msft.get_details_aspx(res)
|
||||||
|
if links.length == 0
|
||||||
|
print_error "Unable to find download.microsoft.com links. Please manually navigate to the page."
|
||||||
|
return []
|
||||||
|
else
|
||||||
|
print_debug("Found #{links.length} affected products for this advisory.")
|
||||||
|
end
|
||||||
|
|
||||||
|
link_collector = []
|
||||||
|
|
||||||
|
links.each do |link|
|
||||||
|
download_page = msft.get_download_page(link)
|
||||||
|
download_links = msft.get_download_links(download_page.body)
|
||||||
|
if regex
|
||||||
|
filtered_links = download_links.select { |l| Regexp.new(regex) === l }
|
||||||
|
link_collector.concat(filtered_links)
|
||||||
|
else
|
||||||
|
link_collector.concat(download_links)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
link_collector
|
||||||
|
end
|
||||||
|
|
||||||
|
# Performs a search via Google
|
||||||
|
#
|
||||||
|
# @param keyword [String] The keyword to search
|
||||||
|
# @param api_key [String] Google API key
|
||||||
|
# @param cx [String] Google Search Engine Key
|
||||||
|
# @return [Array<String>] See MicrosoftPatchFinder::GoogleMsbSearch#find_msb_numbers
|
||||||
|
def google_search(keyword, api_key, cx)
|
||||||
|
search = MicrosoftPatchFinder::GoogleMsbSearch.new(api_key: api_key, search_engine_id: cx)
|
||||||
|
search.find_msb_numbers(keyword)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Performs a search via Technet
|
||||||
|
#
|
||||||
|
# @param keyword [String] The keyword to search
|
||||||
|
# @return [Array<String>] See MicrosoftPatchFinder::TechnetMsbSearch#find_msb_numbers
|
||||||
|
def technet_search(keyword)
|
||||||
|
search = MicrosoftPatchFinder::TechnetMsbSearch.new
|
||||||
|
search.find_msb_numbers(keyword)
|
||||||
|
end
|
||||||
|
|
||||||
|
def run
|
||||||
|
links = []
|
||||||
|
msb_numbers = []
|
||||||
|
keyword = args[:keyword]
|
||||||
|
regex = args[:regex] ? args[:regex] : nil
|
||||||
|
api_key = args[:google_api_key]
|
||||||
|
cx = args[:google_search_engine_id]
|
||||||
|
|
||||||
|
case args[:search_engine]
|
||||||
|
when :technet
|
||||||
|
print_debug("Searching advisories that include #{keyword} via Technet")
|
||||||
|
msb_numbers = technet_search(keyword)
|
||||||
|
when :google
|
||||||
|
print_debug("Searching advisories that include #{keyword} via Google")
|
||||||
|
msb_numbers = google_search(keyword, api_key, cx)
|
||||||
|
end
|
||||||
|
|
||||||
|
print_debug("Advisories found (#{msb_numbers.length}): #{msb_numbers * ', '}") unless msb_numbers.empty?
|
||||||
|
|
||||||
|
return if args[:dryrun]
|
||||||
|
|
||||||
|
msb_numbers.each do |msb|
|
||||||
|
print_debug("Finding download links for #{msb}")
|
||||||
|
links.concat(get_download_links(msb, regex))
|
||||||
|
end
|
||||||
|
|
||||||
|
unless links.empty?
|
||||||
|
print_status "Found these links:"
|
||||||
|
print_line links * "\n"
|
||||||
|
print_status "Total downloadable updates found: #{links.length}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :args
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
if __FILE__ == $PROGRAM_NAME
|
||||||
|
mod = MicrosoftPatchFinder::Driver.new
|
||||||
|
begin
|
||||||
|
mod.run
|
||||||
|
rescue Interrupt
|
||||||
|
$stdout.puts
|
||||||
|
$stdout.puts "Good bye"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
TODO:
|
||||||
|
* Make a gem
|
||||||
|
* Make it generic in order to manage different kind of patches and providers
|
||||||
|
* Multithreading
|
||||||
|
=end
|
Loading…
Reference in New Issue