Skip to content

7. OPSEC Considerations

HuskyHacks edited this page Apr 20, 2022 · 21 revisions

This section contains a few OPSEC considerations when using OffensiveNotion. This information should be equally useful to defenders and operators.

Network Signatures

The OffensiveNotion binary uses the legitimate Notion Developer API for all C2. This means that it is encrypted traffic that will reach out to one of the many Notion Developer API IP addresses that exist.

If one were to inspect packets on the endpoint, one would see a normal TLS handshake followed by encrypted traffic heading towards the Notion API:

image

Without SSL stripping/proxying, this traffic is indecipherable. We can try to follow the TCP stream, but to no avail:

image

One potential indicator comes from the Client Hello during the TLS handshake, where the bytes of the SNI (Server Name Indication) are visible in plain text as api.notion.com. This is the TLS equivalent of looking at the Host Header in cleartext HTTP. Network protocol parsers like Zeek and IDS like Suricata may be able to key in on this. Be advised that if the Notion API is used in your environment, this method lends itself to a lot of false positives:

image

Host-based Signatures

Notion.exe Icon

We can start with the obvious. If you look closely at the icon of the compiled binary, you may notice that it is the Notion icon. But it also has a slight red tint to it.

Legitimate Notion.exe:

image

OffensiveNotion binary:

image

Notion.so App Mode

One of the primary strengths of OffensiveNotion is its chameleon-like ability to blend in as a normal Notion.exe application. It does this by co-opting the Notion icon and has the additional option to launch the Notion.exe application on startup.

Or does it?

If the app is configured to launch Notion when executed, look closely at the Parent-Child process relationship when this occurs:

image

The notion.exe process, in turn, starts msedge.exe as a child process. Examine the arguments for this process:

image

The app=https://notion.so argument here launches Edge in application mode. This presents the Notion home page in a web browser without the URL bar and other UI components:

image

To the untrained eye, this appears to be the actual Notion application. This process is independent from the actual OffensiveNotion agent. That is to say, you can close this Notion window without killing the agent.

Note that it is up to the discretion of the operator to enable or disable the app from launching this way. It is possible to create an OffensiveNotion agent that executes with no visible indicators at all.

Post-exploitation Parent-Child Process Relationships

Signatures can be created for post-exploitation activities. Let's use the Windows agent as an example.

The Windows OffensiveNotion agent executes all shell commands by invoking the native Windows shell. This spawns cmd.exe as a child process of the OffensiveNotion agent:

image

The result of a shell whoami 🎯 command is pictured above. Notion.exe spawns cmd.exe, which handles and processes the whoami.exe call. Keen defenders will take note of this.

Any processes identified this way can/should be investigated for more detail:

image

It is left as an exercise to the reader to identify the other post-exploitation methods that OffensiveNotion uses. TL;DR: read the code and they are in there!

Running Process Memory

🔴 Warning

Major OPSEC consideration ahead!

If defenders are able to identify the OffensiveNotion process and can get to the endpoint and dump the running process memory, it is possible to see the plain-text bytes of a ton of important data.

For example, you can see the API key:

image

The current/previous commands that the agent has executed:

image

image

And even things in the agent's session page that are not commands! Like:

image

image

Why does this happen? The Agent's basic program flow is to make a request to the API using the secret key and parent-page ID, check if there are any new command blocks, and execute them. It then returns the result of that command, sleeps for a pre-defined duration, and starts the loop over.

This means that for every check-in, the agent reads everything that is on its listener page and evaluates if it is a command block or not. This means that the strings associated with the content on the page are in the running memory of the process.

A defender with access to memory analysis tools can extract these strings from the running process. If the process is still running, a tool like Process Hacker can pull strings from the memory as well (this was used in the examples above).

Though this is an edge-case risk, this means an operator should not put anything on the listener page they wouldn't want a defender to see!

This also means that technically speaking, it would be possible for a defender to extract your API key and parent page ID and make a request to your agent's listener page. From there, it is possible to inject arbitrary commands into the agent listener page itself to be interpreted by the agent (imagine a defender using the API key and parent-page ID to inject a shutdown command!).

YARA Rule

YARA signatures are available in the main code repository here.

Guardrails

Red team responsibly, friends! The OffensiveNotion agent supports execution guardrails. You can code these in the source manually or set them during the Docker main.py build (preferred).

The agent supports three different guardrail checks. Each can be used individually or combined. If multiple guardrails are used, the agent must pass ALL guardrail checks to execute.

For the following, assume I am making a Windows Debug agent:

image

Hostname

Keys the payload with a given hostname. This is case insensitive.

[!] Guardrails!
[!] Enter a username to key off. [Leave blank for no keying to username] > 
[!] Enter a hostname to key off. [Leave blank for no keying to hostname] > slothco-dev
[!] Enter the domain name to key off. [Leave blank for no keying to domain name] > 
[*] Your configs are: 
    [*] SLEEP: 5
    [*] JITTER: 0
    [*] API_KEY: [API key]
    [*] PARENT_PAGE_ID: [parent page ID]
    [*] LOG_LEVEL: 5
    [*] LITCRYPT_KEY: offensivenotion
    [*] ENV_CHECKS: [{'Hostname': 'slothco-dev'}]
[!] Do these look good? [yes/no] [default is yes] > 

If the hostname matches, the payload executes:

image

If the hostname does not match, the payload does not execute.

Username

Key the payload execution with a username. This is case insensitive:

[!] Guardrails!
[!] Enter a username to key off. [Leave blank for no keying to username] > a.user
[!] Enter a hostname to key off. [Leave blank for no keying to hostname] > 
[!] Enter the domain name to key off. [Leave blank for no keying to domain name] > 
[*] Your configs are: 
    [*] SLEEP: 5
    [*] JITTER: 0
    [*] API_KEY: [API key]
    [*] PARENT_PAGE_ID: [parent page ID]
    [*] LOG_LEVEL: 5
    [*] LITCRYPT_KEY: offensivenotion
    [*] ENV_CHECKS: [{'Username': 'a.user'}]
[!] Do these look good? [yes/no] [default is yes] > 

Now, when the agent checks in, it checks the current username against the keyed value:

image

If there is a match, the agent executes.

If there is not a match, the agent does not execute:

image

Domain Name (Windows only)

Keys the payload to a Windows active directory domain name. Follows the same pattern as the two guardrails listed above. Is case insensitive.

Multiple Guardrails

Guardrail checks can be combined:

[!] Guardrails!
[!] Enter a username to key off. [Leave blank for no keying to username] > a.user
[!] Enter a hostname to key off. [Leave blank for no keying to hostname] > slothco-dev
[!] Enter the domain name to key off. [Leave blank for no keying to domain name] > slothco.lan
[*] Your configs are: 
    [*] SLEEP: 5
    [*] JITTER: 0
    [*] API_KEY: [API key]
    [*] PARENT_PAGE_ID: [parent page ID]
    [*] LOG_LEVEL: 5
    [*] LITCRYPT_KEY: offensivenotion
    [*] ENV_CHECKS: [{'Username': 'a.user'}, {'Hostname': 'slothco-dev'}, {'Domain': 'slothco.lan'}]
[!] Do these look good? [yes/no] [default is yes] > yes

image

Raw JSON

To implement these checks when using the config command or in the source code, follow this format:

"ENV_CHECKS": [{"Username": "a.user"}, {"Hostname": "slothco-dev"}, {"Domain": "slothco.lan"}]}