Exposure Discovery with Intrigue Core

Exposure discovery is a fundamental capability of Attack Surface Management. While it’s important to find all the assets – it’s simply not enough to stop there, particularly with data breaches increasing rapidly and with more attackers driving time to exploit down across the board. In this post, we’ll talk about our contribution to improving blue team capability around exposure discovery, and how vulnerabilities, misconfigurations, and other exposures can be easily discovered with intrigue Core.

Today, we’re releasing a new version of our Open Core engine which makes vulnerability and misconfiguration discovery a first-class citizen inside Intrigue Core. While we’ve had the concept of exposure discovery and tracking for some time, this feature solidifies the capability within Core and makes it easier than ever to use and develop. Even more importantly, this new feature drives intelligence into Core which automatically kicks off checks based on inelligence provided by intrigue-ident.

This new implementation of checks as a first-class citizen underscores our commitment to our users, and our approach to the problem. Whether a check is fired due to software version comparison, custom logic provided by a user, or due to an external program such as a nuclei template, checks are bring to bear the full power of the Intrigue Core engine to identify exposures and focus the attention of security teams to what matters most.

Our singular goal is to enable security teams with the intelligence they need to find their assets, their exposures, and to protect themselves. Whether you’re an open source or a commercial user, this capability is available to you, effective immediately. Below, we’ll dive into the details of how to use this new feature and how to develop your own exposure checks.

What are exposure checks and how do i use them?

Expsure checks are now unified in the codebase, part metadata, part code. All new checks live in the lib/checks/ directory of the Core codebase. They are comprised of two parts: an issue (metadata) part and a check (code) part.

issue – The metadata about the vulnerability. Information such as the CVE (if any), name, severity, references go here. Ultimately, this is the information presented to users in Intrigue.io as well as the Intrigue Core engine.
check – The actual code that decides if a given entity is vulnerable or not. If the check returns a true (or otherwise truthy value… like a Hash), Core will create a corresponding Issue which uses the check metadata to show information about the results of the check Core provides helper methods to make writing checks simple, detailed in the next section.

There are two ways to run a check against an entity today: (1) Manually through the web ui by selecting the check name and providing the details of a target OR (2) Automatically when the affected software is detected via an intrigue ident fingerprint.

What if i want to write an exposure check?

There are a lot of vulnerabilities out there, and we welcome your help in writing checks. If you’re interested in writing checks, jump into our slack community. Once you understand the structure, writing checks can be very simple. Core provides a lot of flexibility for developers, therefore, it is useful to know whats going on under the hood as you get started. The easiest way to get going is by copying one of the existing checks and modifying the data. Here is an example that can be used as a template. We’ll describe it in detail below.

1. module Intrigue
2.  module Issue

3.    class ExampleCve20301337 < BaseIssue
4.      def self.generate(instance_details={})
5.      {
6.        added: "2020-11-19",
7.        name: "example_cve_2030_1337",
8.        pretty_name: "Off by many RCE CVE-2030-1337",
9.        severity: 1,
10.       category: "vulnerability",
11.       status: "confirmed",
12.       description: "This vulnerability breaks all the Bitcoin",
13.       affected_software: [ 
14.         { :vendor => "Vulnerable Software Corp", :product => "Not secure" }
15.       ],
16.       references: [
17.         { type: "description", uri: "https://nvd.nist.gov/vuln/detail/CVE-2030-1337" }
18.       ],
19.       authors: ["shpendk"]
20.      }.merge!(instance_details)
21.      end
22.    end
23.  end

24.  module Task
25.    class ExampleCve20301337 < BaseCheck
26.      def self.check_metadata
27.      {
28.        allowed_types: ["Uri"]
29.      }
30.      end

31.      def check
32.      {
33.        # get version for product
34.        version = get_version_for_vendor_product(@entity, "Vulnerable Software Corp", "Not secure")
35.        return false unless version

36.        return compare_versions_by_operator(version, "6.2.5" , "<")
37.      } 
38.      end

39.    end
40.  end
41. end

The check part runs code which will verify the existence of the vulnerability. Since the check is a function, any code can be run to perform complex actions. For example, one might want to make an HTTP request and grep in the response. The helper functions http_get_body or http_post can be used for this purpose. For a complete list of helper functions, check out the files in lib/tasks/helpers in the Core codebase. Thera are many many helpers and we’d encourage you to join the Slack group if you’re interested to learn more.’

In this particular check method, the compare_versions_by_operator helper function provided by Intrigue Core actually checks the version we were provided with a known vulnerable version. This function accepts two versions and an operator as a parameter. It then performs the comparison as shown, returning a true/false response. Here are some example uses

compare_versions_by_operator("5.1.2", "6.2.5" , "<") -> returns true
compare_versions_by_operator("5.1.2", "6.2.5" , ">=") -> returns false
compare_versions_by_operator("1.1", "1.1" , "<=") -> returns true
compare_versions_by_operator("2.2", "2.2" , "=") -> returns true

You’ll notice that get_version_for_vendor_product is also used, which returns the version (as a string) of the checked entity. Note that a version might not be available, so if that function returns nil, we should fail, as we do in the example above.

Finally and probably the most important aspect of all, the code that performs the exposure checking must return a truthy value for Intrigue Core to raise an issue. You can simply return true, however, we recommended to return data that proves the vulnerability exists. If the exposure check has succeeded (meaning, it returns a truthy value), it will automatically create an issue with all the metadata. If the check returns false, no issue will be created.

A few procedural notes to save you time are below. Exposure checks must fulfill these criteria to be successfully loaded by Intrigue Core:

1. The class name (lines 3 and 25) must be unique in regard to other checks (but identical for both the check code and the metadata).
2. The name of the check (defined in line 7) must be unique in the system
3. (Convention) Use camel-case for class names and snake case for the check name.

With that aside, many of the fields in the issue part are self-explanatory and pertain to metadata about the vulnerability. These fields are used to create the issue within Intrigue Core.

Metadata INSIDE the check like that on line 26 is primarily helpful for users who run the check manually. There are three keys, though you can largely ignore this when writing a first check, provided allowed_types remains a list of the types you want this check to be runnable on.

* allowed_types: an array of entities which can be passed to this check
* example_entities: an array of example entities, which serves as a placeholder in the web ui
* allowed_options: an array of possible options for the check (see details below)

Specifying exposure check options via allowed_options

This check metadata field is more complex than others, therefore, it gets its own section. In most situations, an exposure check will be self-contained and do the right thing. However, Intrigue Core allows to set and use options on the check. This is best explained via an example:

:allowed_options => [
          {:name => "threads", regex: "integer", :default => 1 },

In this example, an option named threads has been provided for the check. The default value has been set to 1, and the regex field to “integer”. The regex field ensures that the given value for the option passes the validation of that regex. Possible values are: integer, alpha_numeric, alpha_numeric_list, boolean, filename etc. A full list of possible regexes can be found in lib/tasks/helpers/regex.rb

To access the value passed as an option to the check, use the _get_option(“threads”) function, replacing the name of the option in the parameter. There can be arbitrary number of options given to a check, and the values can be used to control the exposure check flow.

This is it! A new exposure check has been born. If you place your check in the lib/checks/* directory and restart Intrigue Core, the new check will be available in the list of tasks and will be available for both manual and automatic start.

Exposure Check Automation

A key aspect to be re-emphasized is the affected_software field in Check metadata. While Intrigue Core is collecting entities, it uses the vendor/production combination specified in this field to determine which exposure checks automatically run on a given entity. This enables automatic exposure detection with no work required by the user beyond specifying the affected software.

Automated exposure checks can be disabled at any time in Intrigue Core by selecting Config -> Project Config, and unchecking the ” Exposure Checks Enabled?” feature.