#!/usr/bin/ruby
#
# This script leverages couple of methods in order to validate that passed
# domain is a S3 bucket indeed.
#
# Mariusz B., 2019, <mb@binary-offensive.com>
#

require 'resolv'
require 'uri'
require 'net/http'

DEBUG = false

$cached_responses = {}
$dns_records = {}
$random_resource = (0...32).map { ('a'..'z').to_a[rand(26)] }.join


class Resp
	attr_accessor :body
	attr_accessor :headers

	def to_s
		return @body
	end

	def to_str
		return @body
	end
end

def dbg(x)
	if DEBUG
		puts "[dbg] #{x}"
	end
end

def checkDnsRecords(bucket)
	begin
		Resolv::DNS.open do |dns|
			$dns_records['ip'] = dns.getaddress(bucket).to_s
			$dns_records['rev-dns'] = dns.getnames($dns_records['ip']).pop.to_s
		end
	rescue Resolv::ResolvError
		dbg "\tCould not resolve name #{bucket}."
		return false
	end

	if $dns_records['rev-dns'].end_with? '.amazonaws.com' and $dns_records['rev-dns'].include? 's3'
		dbg "\tReverse-DNS record for IP (#{$dns_records['ip']}) points to AWS S3: #{$dns_records['rev-dns']}"
		return true
	end

	return false
end

def fetch(url)
	unless $cached_responses.key? url
		begin
			uri = URI.parse(url)
			response = Net::HTTP.get_response uri

			resp = Resp.new
			resp.body = response.body
			resp.headers = response.each_header.to_h

			$cached_responses[url] = resp

		rescue Exception => e
			#puts "\tHTTP Request (#{url}) failed: #{e}"
			$cached_responses[url] = nil
		end
	end

	return $cached_responses[url]
end

def checkServerHeader(bucket)
	['http', 'https'].each do |scheme|
		out = fetch "#{scheme}://#{bucket}"
		if not out.nil? and out.headers.include? 'server' and out.headers['server'].downcase == 'amazons3'
			dbg "\tAmazon S3 bucket found by 'Server' HTTP response header contents."
			return true
		end
	end

	['http', 'https'].each do |scheme|
		out = fetch "#{scheme}://#{bucket}.s3.amazonaws.com"
		if not out.nil? and out.headers.include? 'server' and out.headers['server'].downcase == 'amazons3'
			dbg "\tAmazon S3 bucket found by 'Server' HTTP response header contents."
			return true
		end
	end

	['http', 'https'].each do |scheme|
		out = fetch "#{scheme}://s3.amazonaws.com/#{bucket}"
		if not out.nil? and out.headers.include? 'server' and out.headers['server'].downcase == 'amazons3'
			dbg "\tAmazon S3 bucket found by 'Server' HTTP response header contents."
			return true
		end
	end

	return false
end

def checkAmzHeaders(bucket)
	out = fetch "http://#{bucket}.s3.amazonaws.com"
	if not out.nil? and out.headers.include? 'x-amz-request-id' and out.headers.include? 'x-amz-id-2'
		dbg "\tAmazon S3 found by 'x-amz-request-id' and 'x-amz-id-2' HTTP response headers existence."
		return true
	end

	out = fetch "http://s3.amazonaws.com/#{bucket}"
	if not out.nil? and out.headers.include? 'x-amz-request-id' and out.headers.include? 'x-amz-id-2'
		dbg "\tAmazon S3 found by 'x-amz-request-id' and 'x-amz-id-2' HTTP response headers existence."
		return true
	end

	return false
end

def checkBucketRegionHeader(bucket)
	out = fetch "http://#{bucket}.s3.amazonaws.com"
	if not out.nil? and out.headers.include? 'x-amz-bucket-region'
		dbg "\tAmazon S3 bucket region found in 'x-amz-bucket-region' HTTP response header"
		return true
	end

	out = fetch "http://s3.amazonaws.com/#{bucket}"
	if not out.nil? and out.headers.include? 'x-amz-bucket-region'
		dbg "\tAmazon S3 bucket region found in 'x-amz-bucket-region' HTTP response header"
		return true
	end

	return false
end

def checkBucketResponse(bucket)
	traces = [
		'ListBucketResult xmlns=',
		'</Contents></ListBucketResult>',
		'<Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>',
		'<Error><Code>AllAccessDisabled</Code>',
		'<Error><Code>PermanentRedirect</Code><Message>',
		'</Endpoint><Bucket>',
		'<Error><Code>TemporaryRedirect</Code>',
		"<Name>#{bucket}</Name>",
		"<Bucket>#{bucket}</Bucket>"
	]

	out = fetch "http://#{bucket}.s3.amazonaws.com"
	if not out.nil? and out.headers.include? 'content-type' and out.headers['content-type'].downcase == 'application/xml'
		traces.each do |trace| 
			if out.body.include? trace
				dbg "\tAmazon S3 bucket identified by trace in body: '#{trace}'"
				return true
			end
		end
	end

	out = fetch "http://s3.amazonaws.com/#{bucket}"
	if not out.nil? and out.headers.include? 'content-type' and out.headers['content-type'].downcase == 'application/xml'
		traces.each do |trace| 
			if out.body.include? trace
				dbg "\tAmazon S3 bucket identified by trace in body: '#{trace}'"
				return true
			end
		end
	end

	return false
end

def checkNonExistentResourceBucketResponse(bucket)
	traces = [
		'<li>Code: NoSuchKey</li>',
		'<Error><Code>NoSuchKey</Code><Message>',
	]

	out = fetch "http://#{bucket}.s3.amazonaws.com/#{$random_resource}"
	unless out.nil?
		traces.each do |trace| 
			if out.body.include? trace
				dbg "\tAmazon S3 bucket identified by trace in body of a non-existent resource: '#{trace}'"
				return true
			end
		end
	end

	out = fetch "http://s3.amazonaws.com/#{bucket}/#{$random_resource}"
	unless out.nil?
		traces.each do |trace| 
			if out.body.include? trace
				dbg "\tAmazon S3 bucket identified by trace in body of a non-existent resource: '#{trace}'"
				return true
			end
		end
	end

	return false
end

def checkIfBucketExists(bucket)
	traces = [
		'<Error><Code>NoSuchBucket</Code>',
		'<Message>The specified bucket does not exist</Message>',
		'<BucketName>flaws.cloudfsdsdfsdfdsf</BucketName>'
	]

	found = 0

	out = fetch "http://#{bucket}.s3.amazonaws.com"
	if not out.nil? and out.headers.include? 'content-type' and out.headers['content-type'].downcase == 'application/xml'
		traces.each do |trace| 
			if out.body.include? trace
				found += 1
			end
		end
	end

	if found == traces.length
		dbg("Bucket verified to be non-existent.")
		return false
	end

	out = fetch "http://s3.amazonaws.com/#{bucket}"
	if not out.nil? and out.headers.include? 'content-type' and out.headers['content-type'].downcase == 'application/xml'
		traces.each do |trace| 
			if out.body.include? trace
				found += 1
			end
		end
	end

	if found == traces.length
		dbg("Bucket verified to be non-existent.")
		return false
	end

	return true
end

def main(args)

	puts %{
	:: Identifies AWS S3 Buckets via couple of methods
	Mariusz B. 19', <mb@binary-offensive.com>
	}

	if ARGV.length != 1
		puts "Usage: ./identifyS3Bucket.rb <name|domain>"
		exit
	end

	points = 0

	bucket = ARGV.pop
	puts "[.] Examining bucket with name: #{bucket}"

	unless checkIfBucketExists bucket
		puts "[-] There is no such bucket."
		exit 1
	end

	
	if checkDnsRecords bucket
		puts "[+] S3 bucket identified via DNS records."
		points += 1
	end

	if checkServerHeader bucket
		puts "[+] S3 Bucket identified by HTTP header 'Server' in response."
		points += 1
	end

	if checkAmzHeaders bucket
		puts "[+] S3 Bucket identified by HTTP amz headers."
		points += 1
	end

	if checkBucketResponse bucket
		puts "[+] S3 Bucket identified via traces in HTTP response body."
		points += 1
	end

	if checkNonExistentResourceBucketResponse bucket
		puts "[+] S3 Bucket identified via traces in HTTP response of a non-existent resource."
		points += 1
	end

	return 0 if points > 0
	return 1

end

if __FILE__ == $0
	main(ARGV)
end