class JiraScan

Constants

VERSION

Public Class Methods

detectJiraDashboard(url) click to toggle source

Check if URL is running Jira using Dashboard page

@param [String] URL

@return [Boolean]

# File lib/jira_scan.rb, line 56
def self.detectJiraDashboard(url)
  url += '/' unless url.to_s.end_with? '/'
  res = sendHttpRequest("#{url}secure/Dashboard.jspa")

  return false unless res
  return false unless res.code.to_i == 200

  res.body.to_s.include?('JIRA')
end
detectJiraLogin(url) click to toggle source

Check if URL is running Jira using Login page

@param [String] URL

@return [Boolean]

# File lib/jira_scan.rb, line 39
def self.detectJiraLogin(url)
  url += '/' unless url.to_s.end_with? '/'
  res = sendHttpRequest("#{url}login.jsp")

  return false unless res
  return false unless res.code.to_i == 200

  res.body.to_s.include?('JIRA')
end
devMode(url) click to toggle source

Check if dev mode is enabled

@param [String] URL

@return [Boolean]

# File lib/jira_scan.rb, line 145
def self.devMode(url)
  url += '/' unless url.to_s.end_with? '/'
  res = sendHttpRequest(url)

  return false unless res
  return false unless res.code.to_i == 200

  res.body.to_s.include?('<meta name="ajs-dev-mode" content="true">')
end
getDashboards(url) click to toggle source

Retrieve list of dashboards

@param [String] URL

@return [Array] list of dashboards

# File lib/jira_scan.rb, line 346
def self.getDashboards(url)
  url += '/' unless url.to_s.end_with? '/'
  max = 1_000
  res = sendHttpRequest("#{url}rest/api/2/dashboard?maxResults=#{max}")

  return [] unless res
  return [] unless res.code.to_i == 200
  return [] unless res.body.to_s.start_with?('{"startAt"')
  return [] unless res.body.to_s.include?('id')
  return [] unless res.body.to_s.include?('name')

  JSON.parse(res.body.to_s, symbolize_names: true)[:dashboards].map { |d| [d[:id], d[:name]] }
rescue
  []
end
getFieldNamesQueryComponentDefault(url) click to toggle source

Retrieve list of field names from QueryComponent!Default.jspa (CVE-2020-14179) jira.atlassian.com/browse/JRASERVER-71536 jira.atlassian.com/browse/JRACLOUD-75661

@param [String] URL

@return [Array] list of field names

# File lib/jira_scan.rb, line 466
def self.getFieldNamesQueryComponentDefault(url)
  url += '/' unless url.to_s.end_with? '/'
  res = sendHttpRequest("#{url}secure/QueryComponent!Default.jspa")

  return [] unless res
  return [] unless res.code.to_i == 200
  return [] unless res.body.to_s.start_with?('{"searchers"')

  searchers = JSON.parse(res.body.to_s)['searchers']
  return [] if searchers.empty?

  groups = searchers['groups']
  return [] if groups.empty?

  field_names = []
  groups.each do |g|
    g['searchers'].each do |s|
      field_names << s
    end
  end

  JSON.parse(field_names.to_json, symbolize_names: true).map { |f| [f[:name], f[:id], f[:key], f[:isShown].to_s, f[:lastViewed]] }
rescue
  []
end
getFieldNamesQueryComponentJql(url) click to toggle source

Retrieve list of field names from QueryComponent!Jql.jspa (CVE-2020-14179) jira.atlassian.com/browse/JRASERVER-71536

@param [String] URL

@return [Array] list of field names

# File lib/jira_scan.rb, line 500
def self.getFieldNamesQueryComponentJql(url)
  url += '/' unless url.to_s.end_with? '/'
  res = sendHttpRequest("#{url}secure/QueryComponent!Jql.jspa?jql=")

  return [] unless res
  return [] unless res.code.to_i == 200
  return [] unless res.body.to_s.start_with?('{"searchers"')

  searchers = JSON.parse(res.body.to_s)['searchers']
  return [] if searchers.empty?

  groups = searchers['groups']
  return [] if groups.empty?

  field_names = []
  groups.each do |g|
    g['searchers'].each do |s|
      field_names << s
    end
  end

  JSON.parse(field_names.to_json, symbolize_names: true).map { |f| [f[:name], f[:id], f[:key], f[:isShown].to_s, f[:lastViewed]] }
rescue
  []
end
getGadgets(url) click to toggle source

Retrieve list of installed gadgets jira.atlassian.com/browse/JRASERVER-72613

@param [String] URL

@return [Array] list of installed gadgets

# File lib/jira_scan.rb, line 263
def self.getGadgets(url)
  url += '/' unless url.to_s.end_with? '/'
  res = sendHttpRequest("#{url}rest/config/1.0/directory.json")

  return [] unless res
  return [] unless res.code.to_i == 200
  return [] unless res.body.to_s.start_with?('{"categories"')

  gadgets = JSON.parse(res.body.to_s)['gadgets']
  return [] if gadgets.empty?

  JSON.parse(gadgets.to_json, symbolize_names: true).map { |g| [g[:title], g[:authorName], g[:authorEmail], g[:description]] }
rescue
  []
end
getLinkedApps(url) click to toggle source

Retrieve list of linked applications jira.atlassian.com/browse/JRASERVER-64963 jira.atlassian.com/browse/JRACLOUD-64963

@param [String] URL

@return [Array] list of linked applications

# File lib/jira_scan.rb, line 441
def self.getLinkedApps(url)
  url += '/' unless url.to_s.end_with? '/'
  res = sendHttpRequest("#{url}rest/menu/latest/admin")

  return [] unless res
  return [] unless res.code.to_i == 200
  return [] unless res.body.to_s.start_with?('[{"key"')
  return [] unless res.body.to_s.include?('link')
  return [] unless res.body.to_s.include?('label')
  return [] unless res.body.to_s.include?('applicationType')

  JSON.parse(res.body.to_s, symbolize_names: true).map { |r| [r[:link], r[:label], r[:applicationType]] }
rescue
  []
end
getPopularFilters(url) click to toggle source

Retrieve list of popular filters jira.atlassian.com/browse/JRASERVER-23255

@param [String] URL

@return [Array] list of popular filters

# File lib/jira_scan.rb, line 323
def self.getPopularFilters(url)
  url += '/' unless url.to_s.end_with? '/'
  res = sendHttpRequest("#{url}secure/ManageFilters.jspa?filter=popular&filterView=popular")

  return [] unless res
  return [] unless res.code.to_i == 200
  return [] unless res.body.to_s.include?('<h1>Manage Filters</h1>')

  return res.body.to_s.scan(%r{requestId=\d+">(.+?)</a>}) if res.body.to_s =~ /requestId=\d/
  return res.body.to_s.scan(%r{filter=\d+">(.+?)</a>}) if res.body.to_s =~ /filter=\d/

  []
rescue
  []
end
getProjectCategories(url) click to toggle source

Retrieve list of project categories

@param [String] URL

@return [Array] list of project categories

# File lib/jira_scan.rb, line 416
def self.getProjectCategories(url)
  url += '/' unless url.to_s.end_with? '/'
  res = sendHttpRequest("#{url}rest/api/2/projectCategory")

  return [] unless res
  return [] unless res.code.to_i == 200
  return [] unless res.body.to_s.start_with?('[{"self"')
  return [] unless res.body.to_s.include?('id')
  return [] unless res.body.to_s.include?('name')
  return [] unless res.body.to_s.include?('description')

  JSON.parse(res.body.to_s, symbolize_names: true).map { |r| [r[:id], r[:name], r[:description]] }
rescue
  []
end
getProjects(url) click to toggle source

Retrieve list of projects

@param [String] URL

@return [Array] list of projects

# File lib/jira_scan.rb, line 392
def self.getProjects(url)
  url += '/' unless url.to_s.end_with? '/'
  max = 1_000
  res = sendHttpRequest("#{url}rest/api/2/project?maxResults=#{max}")

  return [] unless res
  return [] unless res.code.to_i == 200
  return [] unless res.body.to_s.start_with?('[{"expand"')
  return [] unless res.body.to_s.include?('id')
  return [] unless res.body.to_s.include?('key')
  return [] unless res.body.to_s.include?('name')

  JSON.parse(res.body.to_s, symbolize_names: true).map { |r| [r[:id], r[:key], r[:name]] }
rescue
  []
end
getResolutions(url) click to toggle source

Retrieve list of resolutions

@param [String] URL

@return [Array] list of resolutions

# File lib/jira_scan.rb, line 369
def self.getResolutions(url)
  url += '/' unless url.to_s.end_with? '/'
  res = sendHttpRequest("#{url}rest/api/2/resolution")

  return [] unless res
  return [] unless res.code.to_i == 200
  return [] unless res.body.to_s.start_with?('[{"self"')
  return [] unless res.body.to_s.include?('id')
  return [] unless res.body.to_s.include?('name')
  return [] unless res.body.to_s.include?('description')

  JSON.parse(res.body.to_s, symbolize_names: true).map { |r| [r[:id], r[:name], r[:description]] }
rescue
  []
end
getServerInfo(url) click to toggle source

Retrieve Jira software information

@param [String] URL

@return [Array] Jira software information

# File lib/jira_scan.rb, line 125
def self.getServerInfo(url)
  url += '/' unless url.to_s.end_with? '/'
  res = sendHttpRequest("#{url}rest/api/latest/serverInfo")

  return [] unless res
  return [] unless res.code.to_i == 200
  return [] unless res.body.to_s.start_with?('{"baseUrl"')

  JSON.parse(res.body.to_s, symbolize_names: true)
rescue
  []
end
getUsersFromUserPickerBrowser(url) click to toggle source

Retrieve list of users from UserPickerBrowser

@param [String] URL

@return [Array] list of first 1,000 users

# File lib/jira_scan.rb, line 196
def self.getUsersFromUserPickerBrowser(url)
  url += '/' unless url.to_s.end_with? '/'
  max = 1_000
  res = sendHttpRequest("#{url}secure/popups/UserPickerBrowser.jspa?max=#{max}")

  return [] unless res && res.code.to_i == 200 && res.body.to_s.include?('<h1>User Picker</h1>')

  users = []
  if res.body.to_s.include? 'cell-type-email'
    res.body.to_s.scan(%r{<td data-cell-type="name" class="user-name">(.*?)</td>\s+<td data-cell-type="fullname" >(.*?)</td>\s+<td data-cell-type="email" class="cell-type-email">(.*?)</td>}m).each do |u|
      users << u
    end
  else
    res.body.to_s.scan(%r{<td data-cell-type="name" class="user-name">(.*?)</td>\s+<td data-cell-type="fullname" >(.*?)</td>}m).each do |u|
      users << u
    end
  end

  users
rescue
  []
end
getVersionFromDashboard(url) click to toggle source

Get Jira version from Dashboard page

@param [String] URL

@return [String] Jira version

# File lib/jira_scan.rb, line 73
def self.getVersionFromDashboard(url)
  url += '/' unless url.to_s.end_with? '/'
  res = sendHttpRequest("#{url}secure/Dashboard.jspa")

  return unless res
  return unless res.code.to_i == 200

  version = res.body.to_s.scan(%r{<meta name="ajs-version-number" content="([\d\.]+)">}).flatten.first
  build = res.body.to_s.scan(%r{<meta name="ajs-build-number" content="(\d+)">}).flatten.first

  unless version && build
    return unless res.body.to_s =~ /Version: ([\d\.]+)-#(\d+)/
    version = Regexp.last_match(1)
    build = Regexp.last_match(2)
  end

  "#{version}-##{build}"
end
getVersionFromLogin(url) click to toggle source

Get Jira version from Login page

@param [String] URL

@return [String] Jira version

# File lib/jira_scan.rb, line 99
def self.getVersionFromLogin(url)
  url += '/' unless url.to_s.end_with? '/'
  res = sendHttpRequest("#{url}login.jsp")

  return unless res
  return unless res.code.to_i == 200

  version = res.body.to_s.scan(%r{<meta name="ajs-version-number" content="([\d\.]+)">}).flatten.first
  build = res.body.to_s.scan(%r{<meta name="ajs-build-number" content="(\d+)">}).flatten.first

  unless version && build
    return unless res.body.to_s =~ /Version: ([\d\.]+)-#(\d+)/
    version = Regexp.last_match(1)
    build = Regexp.last_match(2)
  end

  "#{version}-##{build}"
end
insecure() click to toggle source
# File lib/jira_scan.rb, line 24
def self.insecure
  @insecure ||= false
end
insecure=(insecure) click to toggle source
# File lib/jira_scan.rb, line 28
def self.insecure=(insecure)
  @insecure = insecure
end
logger() click to toggle source
# File lib/jira_scan.rb, line 16
def self.logger
  @logger
end
logger=(logger) click to toggle source
# File lib/jira_scan.rb, line 20
def self.logger=(logger)
  @logger = logger
end
metaInf(url) click to toggle source

Check if META-INF contents are accessible (CVE-2019-8442) jira.atlassian.com/browse/JRASERVER-69241

@param [String] URL

@return [Boolean]

# File lib/jira_scan.rb, line 305
def self.metaInf(url)
  url += '/' unless url.to_s.end_with? '/'
  res = sendHttpRequest("#{url}s/#{rand(36**6).to_s(36)}/_/META-INF/maven/com.atlassian.jira/atlassian-jira-webapp/pom.xml")

  return false unless res
  return false unless res.code.to_i == 200

  res.body.to_s.start_with?('<project')
end
restGroupUserPicker(url) click to toggle source

Check if unauthenticated access to REST GroupUserPicker is allowed (CVE-2019-8449) jira.atlassian.com/browse/JRASERVER-69796

@param [String] URL

@return [Boolean]

# File lib/jira_scan.rb, line 245
def self.restGroupUserPicker(url)
  url += '/' unless url.to_s.end_with? '/'
  res = sendHttpRequest("#{url}rest/api/2/groupuserpicker")

  return false unless res
  return false unless res.code.to_i == 400

  res.body.to_s.include?('The username query parameter was not provided')
end
restUserPicker(url) click to toggle source

Check if unauthenticated access to REST UserPicker is allowed (CVE-2019-3403) jira.atlassian.com/browse/JRASERVER-69242

@param [String] URL

@return [Boolean]

# File lib/jira_scan.rb, line 227
def self.restUserPicker(url)
  url += '/' unless url.to_s.end_with? '/'
  res = sendHttpRequest("#{url}rest/api/2/user/picker")

  return false unless res
  return false unless res.code.to_i == 400

  res.body.to_s.include?('The username query parameter was not provided')
end
sendHttpRequest(url) click to toggle source

Fetch URL

@param [String] URL

@return [Net::HTTPResponse] HTTP response

# File lib/jira_scan.rb, line 533
def self.sendHttpRequest(url)
  target = URI.parse(url)
  @logger.info("Fetching #{target}")

  http = Net::HTTP.new(target.host, target.port)
  if target.scheme.to_s.eql?('https')
    http.use_ssl = true
    http.verify_mode = @insecure ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
  end
  http.open_timeout = 20
  http.read_timeout = 20
  headers = {}
  headers['User-Agent'] = "JiraScan/#{VERSION}"
  headers['Accept-Encoding'] = 'gzip,deflate'

  begin
    res = http.request(Net::HTTP::Get.new(target, headers.to_hash))
    if res.body && res['Content-Encoding'].eql?('gzip')
      sio = StringIO.new(res.body)
      gz = Zlib::GzipReader.new(sio)
      res.body = gz.read
    end
  rescue Timeout::Error, Errno::ETIMEDOUT
    @logger.error("Could not retrieve URL #{target}: Timeout")
    return nil
  rescue => e
    @logger.error("Could not retrieve URL #{target}: #{e}")
    return nil
  end
  @logger.info("Received reply (#{res.body.length} bytes)")
  res
end
userPickerBrowser(url) click to toggle source

Check if unauthenticated access to UserPickerBrowser.jspa is allowed

@param [String] URL

@return [Boolean]

# File lib/jira_scan.rb, line 179
def self.userPickerBrowser(url)
  url += '/' unless url.to_s.end_with? '/'
  res = sendHttpRequest("#{url}secure/popups/UserPickerBrowser.jspa")

  return false unless res
  return false unless res.code.to_i == 200

  res.body.to_s.include?('<h1>User Picker</h1>')
end
userRegistration(url) click to toggle source

Check if account registration is enabled

@param [String] URL

@return [Boolean]

# File lib/jira_scan.rb, line 162
def self.userRegistration(url)
  url += '/' unless url.to_s.end_with? '/'
  res = sendHttpRequest("#{url}secure/Signup!default.jspa")

  return false unless res
  return false unless res.code.to_i == 200

  res.body.to_s.include?('<h1>Sign up</h1>')
end
viewUserHover(url) click to toggle source

Check if unauthenticated access to ViewUserHover.jspa is allowed (CVE-2020-14181) jira.atlassian.com/browse/JRASERVER-71560

@param [String] URL

@return [Boolean]

# File lib/jira_scan.rb, line 287
def self.viewUserHover(url)
  url += '/' unless url.to_s.end_with? '/'
  res = sendHttpRequest("#{url}secure/ViewUserHover.jspa")

  return false unless res
  return false unless res.code.to_i == 200

  res.body.to_s.include?('User does not exist')
end