├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── azure_enum.gemspec ├── bin ├── console └── setup ├── discovery.xml.erb ├── exe └── azure_enum ├── lib ├── azure_enum.rb └── azure_enum │ └── version.rb └── test ├── azure_enum_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Style/StringLiterals: 2 | EnforcedStyle: double_quotes 3 | Metrics/LineLength: 4 | Max: 100 5 | Metrics/MethodLength: 6 | Max: 25 7 | 8 | Naming/AccessorMethodName: 9 | Exclude: 10 | - 'lib/azure_enum.rb' 11 | 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.5.1 5 | before_install: gem install bundler -v 1.16.1 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in azure_enum.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Technion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AzureEnum 2 | 3 | This Ruby Gem assists in enumeration of Office 365 or Exchange on-premise federated domains. This can allow you to identify domains associated with a business, not easily identified through traditional means. The examples below demonstrate how output can be interesting. 4 | 5 | The time this process takes can vary from a few seconds to a few minutes depending on the hosting server. 6 | ## Installation 7 | 8 | Add this line to your application's Gemfile: 9 | 10 | ```ruby 11 | gem 'azure_enum' 12 | ``` 13 | 14 | And then execute: 15 | 16 | $ bundle 17 | 18 | Or install it yourself as: 19 | 20 | $ gem install azure_enum 21 | 22 | ## Usage 23 | 24 | You can use this gem from within an application: 25 | 26 | ``` 27 | require "azure_enum" 28 | x = AzureEnum.federated("lolware.net") 29 | => ["lolzware.onmicrosoft.com", "lolware.net"] 30 | ``` 31 | 32 | Or by installing and running the binary: 33 | ``` 34 | $ azure_enum lolware.net 35 | Please wait while the given domain is enumerated. 36 | lolzware.onmicrosoft.com 37 | lolware.net 38 | ``` 39 | 40 | ## Examples 41 | 42 | The following examples against some random domains demonstrate the tools capabilities. 43 | 44 | ``` 45 | $ azure_enum afl.com.au 46 | Please wait while the given domain is enumerated. 47 | afl.com.au 48 | aflnt.com.au 49 | ntthunder.com.au 50 | aflgoulburnmurray.com.au 51 | aflwesterndistrict.com.au 52 | aflgippsland.com.au 53 | aflyarraranges.com.au 54 | 55 | $ azure_enum kmart.com.au 56 | Please wait while the given domain is enumerated. 57 | kmart.com.au 58 | KASAsia.com 59 | 60 | $ azure_enum microsoft.com 61 | Please wait while the given domain is enumerated. 62 | corp.webtv.net 63 | microsoft.onmicrosoft.com 64 | surface.com 65 | bungie.com 66 | navic.tv 67 | middleeast.corp.microsoft.com 68 | wingroup.windeploy.ntdev.microsoft.com 69 | exchangecalendarsharing.com 70 | redmond.corp.microsoft.com 71 | northamerica.corp.microsoft.com 72 | bing.com 73 | corp.microsoft.com 74 | placeware.com 75 | (snip large list) 76 | ``` 77 | 78 | ## Development 79 | 80 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 81 | 82 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 83 | 84 | ## Contributing 85 | 86 | Bug reports and pull requests are welcome on GitHub at https://github.com/technion/azure_enum. 87 | Sometimes you get this output: 88 | 89 | Unknown key: Max-Age = 31536000 90 | 91 | It seems to be a known bug in HTTPClient. 92 | 93 | ## License 94 | 95 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 96 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList["test/**/*_test.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /azure_enum.gemspec: -------------------------------------------------------------------------------- 1 | 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "azure_enum/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "azure_enum" 8 | spec.version = AzureEnum::VERSION 9 | spec.authors = ["Technion"] 10 | spec.email = ["technion@lolware.net"] 11 | 12 | spec.summary = %q{Enumerate Office 365 tenancies for federated domains.} 13 | spec.description = %q{External enumeration toolkit to identify organisation relationships in Office 365.} 14 | spec.homepage = "https://github.com/technion/azure_enum" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 18 | f.match(%r{^(test|spec|features)/}) 19 | end 20 | spec.bindir = "exe" 21 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 22 | spec.require_paths = ["lib"] 23 | 24 | spec.add_development_dependency "bundler", "~> 2.3.0" 25 | spec.add_development_dependency "rake", "~> 10.0" 26 | spec.add_development_dependency "minitest", "~> 5.0" 27 | spec.add_dependency "httpclient", "~> 2.8.0" 28 | spec.add_dependency "nokogiri", "~> 1.15.4" 29 | end 30 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "azure_enum" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /discovery.xml.erb: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | urn:uuid:6389558d-9e05-465e-ade9-aae14c4bcd10 10 | http://schemas.microsoft.com/exchange/2010/Autodiscover/Autodiscover/GetFederationInformation 11 | 12 | <%= @url %> 13 | 14 | http://www.w3.org/2005/08/addressing/anonymous 15 | 16 | 17 | 18 | 20 | 21 | <%= @domain %> 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /exe/azure_enum: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "azure_enum" 4 | 5 | if (ARGV.length == 0) 6 | warn "Please provide domain to enumerate on command line. eg azure_enum lolware.net" 7 | exit 8 | end 9 | 10 | puts "Please wait while the given domain is enumerated." 11 | domains = AzureEnum.federated(ARGV[0]) 12 | if (!domains || domains == []) 13 | puts "Unfortunately this domain cannot be enumerated" 14 | exit 15 | end 16 | 17 | puts domains 18 | -------------------------------------------------------------------------------- /lib/azure_enum.rb: -------------------------------------------------------------------------------- 1 | require "azure_enum/version" 2 | require "erb" 3 | require "httpclient" 4 | require "nokogiri" 5 | 6 | # Azure and Exchange federated domain enumerator 7 | module AzureEnum 8 | # Class initializes with a domain name, and provides methods to interact with MS Autodiscover 9 | class Federation 10 | def initialize(domain) 11 | @domain = domain 12 | @xml_text = nil 13 | @redirect = nil 14 | end 15 | 16 | # This will identify if the http:// redirect exists for the domain, usually per Office 365 17 | def check_redirect 18 | url = "http://autodiscover.#{@domain}/autodiscover/autodiscover.svc" 19 | begin 20 | res = HTTPClient.head(url) 21 | rescue 22 | return nil 23 | end 24 | return nil unless res.status_code == 302 25 | @redirect = res.header["Location"][0] 26 | end 27 | 28 | def autodisc 29 | # These are the default autodiscover domains. 30 | [ 31 | "https://autodiscover.#{@domain}/autodiscover/autodiscover.svc", 32 | "https://#{@domain}/autodiscover/autodiscover.svc" 33 | ] 34 | end 35 | 36 | def enumerate_autodisc(httpsdomains = autodisc) 37 | httpsdomains.unshift @redirect if @redirect 38 | @redirect = nil 39 | httpsdomains.each do |url| 40 | xml = get_xml(@domain, url) 41 | begin 42 | http = HTTPClient.new 43 | content = { "Content-Type" => "text/xml; charset=utf-8" } 44 | res = http.post(url, xml, content) 45 | # In the event of a second redirect we are on the right path 46 | # Recurse this function for the correct result 47 | if res.status_code == 302 48 | return enumerate_autodisc [res.header['Location'][0]] 49 | end 50 | @xml_text = res.content 51 | return true 52 | # It is bad style to rescue "all" errors. However, it turns out there is a practically 53 | # never ending list of ways this can fail. And "any" failure is reason to rule out the address 54 | rescue 55 | next 56 | end 57 | end 58 | return false 59 | end 60 | 61 | def getdomains 62 | raise "enumumerate_autodisc not called yet" unless @xml_text 63 | tree = Nokogiri.parse(@xml_text) 64 | tree.xpath( 65 | "//ad:GetFederationInformationResponseMessage/ad:Response/ad:Domains/ad:Domain", 66 | ad: "http://schemas.microsoft.com/exchange/2010/Autodiscover" 67 | ).map(&:text) 68 | end 69 | 70 | private 71 | 72 | # This is an internal class just to pass the correct structure to ERB in get_xml 73 | class Discovery 74 | def initialize(domain, url) 75 | @domain = domain 76 | @url = url 77 | end 78 | 79 | def get_binding 80 | binding 81 | end 82 | end 83 | 84 | def get_xml(domain, url) 85 | path = File.dirname __dir__ 86 | template = File.read(File.join(path, "discovery.xml.erb")) 87 | renderer = ERB.new(template) 88 | discovery = Discovery.new(domain, url) 89 | renderer.result(discovery.get_binding) 90 | end 91 | end 92 | 93 | # This is the intended API: runs each step of the enumeration process and returns a result 94 | class << self 95 | def federated(domain) 96 | e = Federation.new(domain) 97 | e.check_redirect 98 | if e.enumerate_autodisc 99 | e.getdomains 100 | else 101 | nil 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/azure_enum/version.rb: -------------------------------------------------------------------------------- 1 | module AzureEnum 2 | VERSION = "0.2.2".freeze 3 | end 4 | -------------------------------------------------------------------------------- /test/azure_enum_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class AzureEnumTest < Minitest::Test 4 | def test_that_it_has_a_version_number 5 | refute_nil ::AzureEnum::VERSION 6 | end 7 | 8 | def test_lolware_lookup 9 | assert_equal(["lolzware.onmicrosoft.com", "lolware.net"], AzureEnum.federated("lolware.net")) 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 2 | require "azure_enum" 3 | 4 | require "minitest/autorun" 5 | --------------------------------------------------------------------------------