Project Breakdown: BruteExpose

Project inspired by Brute.Fail 

What is BruteExpose? (Now Known as Live Security Monitor)

chomnr/live-security-monitor  is an application that, whenever someone attempts to log in to a server that uses OpenSSH, will log the credentials used, the origin (IP & country), the attack protocol, and the date.

Why I chose Java?

The only reason I chose Java was that I didn't have a project written in it. If I could go back and rewrite this project, I would probably write it entirely in C or C++. It's also a language I'm highly familiar with because I use to write Minecraft plugins in Java.

Metrics & Analytics System

Metrics:

To collect the actual metrics, I ended up going with IPInfo; I ended up using the .mmdb (MaxMind database), which I regret using because that meant that I would have to constantly update the .mmdb to avoid getting weird errors in my code. Instead, I should have used the IPInfo API. 

Analytics:

To interpret the metrics data, I wrote a modular analytics system. You can easily add or remove different stats from the code, and they will be reflected accordingly in the JSON (where the stats are located/tracked).

Currently supported as of 6/1/2024
  • NumberOfAttemptsOverTime
  • AttackTotalByDayOfWeek
  • DistributionOfAttackProtocols
  • AttackOriginByCountry
  • AttackOriginByIp
  • CommonlyTargetedByCredential

Integrating your own analytics here's a snippet from the ProtocolBasedMetrics

ProtocolBasedMetrics.java

This file will actually help populate the value inside the .json file that keeps track of all these analytics. Many of these metrics can be grouped together into one function because when one of the values is affected, all of them are affected as well. Therefore, we just need a simple populate() function that covers them all. If you need another example when multiple stats are being tracked look at the TimeBasedMetrics folder.

   Private DistributionOfAttackProtocols distributionOfAttackProtocols
   public enum ProtocolBasedType {
      SSH,
      UNKNOWN
    }
    public ProtocolBasedMetrics() {
      distributionOfAttackProtocols = new DistributionOfAttackProtocols();
    }
    public DistributionOfAttackProtocols getDistributionOfAttackProtocols() {
      return distributionOfAttackProtocols;
    }
    public void populate(String name, int amount) {
      getDistributionOfAttackProtocols().insert(name, amount);
    }
    public void populate(ProtocolBasedType type, int amount) {
      getDistributionOfAttackProtocols().insert(type, amount);
    }
    public void populate(String type) {
      getDistributionOfAttackProtocols().insert(type, 1);
    }
    public void populate(ProtocolBasedType type) {
      getDistributionOfAttackProtocols().insert(type, 1);
    }
 

DistributionOfAttackProtocols.java

This is actual stat that will be tracked. All we do is a make simple hashmap and our Json Object Mapper will handle the rest.

    private HashMap protocols = new HashMap<>();
    public DistributionOfAttackProtocols() {}
    public void insert(String type, int amount) {
        ProtocolBasedType protocolType = getProtocolByName(type);
        addAttempts(protocolType, amount);
    }
    public void insert(ProtocolBasedType type, int amount) {
        addAttempts(type, amount);
    }
    private void addAttempts(ProtocolBasedType type, int amount) {
        String protocolName = getNameOfProtocol(type);

        if (protocols.get(protocolName) == null) {
            protocols.put(protocolName, amount);
        } else {
            protocols.put(protocolName, getAttempts(type)+amount);
        }
    }
    private Integer getAttempts(ProtocolBasedType type) {
        return protocols.get(getNameOfProtocol(type));
    }
    public ProtocolBasedType getProtocolByName(String protocol) {
        if (protocol.equalsIgnoreCase("sshd")) {
            return ProtocolBasedType.SSH;
        }
        if (protocol.equalsIgnoreCase("ssh")) {
            return ProtocolBasedType.SSH;
        }
        // or return UNKNOWN
        return ProtocolBasedType.UNKNOWN;
    }
    private String getNameOfProtocol(ProtocolBasedType type) {
        return type.name();
    }

BruteMetricData.java

This is where we will instantiate our analytic/metric.
    private TimeBasedMetrics timeBasedMetrics;
    private GeographicMetrics geographicMetrics;
    private ProtocolBasedMetrics protocolBasedMetrics;
    private CredentialBasedMetrics credentialBasedMetrics;

    public BruteMetricData() {
        timeBasedMetrics = new TimeBasedMetrics();
        geographicMetrics = new GeographicMetrics();
        protocolBasedMetrics = new ProtocolBasedMetrics();
        credentialBasedMetrics = new CredentialBasedMetrics();
    }

    public TimeBasedMetrics getTimeBasedMetrics() {
        return timeBasedMetrics;
    }
    public GeographicMetrics getGeographicMetrics() { return geographicMetrics; }
    public ProtocolBasedMetrics getProtocolBasedMetrics() { return protocolBasedMetrics; }
    public CredentialBasedMetrics getCredentialBasedMetrics() { return credentialBasedMetrics; }


Forking OpenSSH

If you use regular OpenSSH you will notice after trying to dump the password, you will get something like this
^M^?INCORRECT^@"
This is a safety mechanism built into OpenSSH in order to avoid leaking via timing. So, in order to circumvent this, you will need to disable it by removing this line of code. https://github.com/openssh/openssh-portable/blob/df56a8035d429b2184ee94aaa7e580c1ff67f73a/auth-pam.c#L1198

Now the bad password will not be overrided by OpenSSH.

Dumping:

Now we need to dump the credentials to a .txt file named brute_tracker.txt it will dump the username, password, host and the protocol.

It's a VERY simple script and our Java application will listen to brute_tracker.txt and whenever it is edited it will automatically read the latest entry and store the data.

#include "library.h"
#include <security/pam_appl.h>
#include <security/pam_modules.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BE_LOG_FILE "/var/log/brute_tracker.txt"
#define BE_DELAY 700

PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) {
    char    *username,
            *password,
            *protocol,
            *hostname;

    pam_get_item(pamh, PAM_USER, (void*)&username);
    pam_get_item(pamh, PAM_AUTHTOK, (void*)&password);
    pam_get_item(pamh, PAM_RHOST, (void*)&hostname);
    pam_get_item(pamh, PAM_SERVICE, (void*)&protocol);

    // Added a delay to ensure that BruteExpose gets to read the entry.
    // In terms of practicality, I should have wrote the entire program in C,
    // but I am not familiar with the language.
    usleep(BE_DELAY);

    FILE *fd = fopen(BE_LOG_FILE, "a");
    if (fd != NULL) {
        fprintf(fd, "%s %s %s %s \n", username, password, hostname, protocol);
        fclose(fd);
    }

    return PAM_SUCCESS;
}

After compiling the script, you need to do the following:
  • Drop the .pam file here /lib/x86_64-linux-gnu/security/
  • Nano into common-auth  sudo nano /etc/pam.d/common-auth

Now you need add libbe_pam.so right before the password gets denied.

# here are the per-package modules (the "Primary" block)
auth    [success=2 default=ignore]      pam_unix.so nullok
# enable BruteExpose.
auth    optional                        libbe_pam.so
# here's the fallback if no module succeeds 
auth requisite pam_deny.so
What's happening? If pam.unix.so is successful it will skip the next 2 lines. If not it will hit our pam module then the pam_deny module.

What would I have done differently?

1. I wouldn't have written a modular analytical system; instead, I would have just hardcoded the analytics for simplicity. This project is not really practical for real-world use cases because we need to introduce a vulnerability to OpenSSH in order to get it working.

2. I would not have used Java; instead, I would probably written the entire thing in C. Or I would have written the whole thing as a simple PAM module using C. Using two separate languages introduces much more complexity and makes it hard to maintain.

3. Using IPInfo API instead of their .mmdb. I really should have just used their API for simplicity. I'm not sure why I was so adamant about using their .mmdb. Boy, I probably regret this most because I have to constantly update the .mmdb file manually. with an API, I wouldn't have to do that. Oh, well, you live, you learn.

4. Use SQLite instead of JSON. JSON is great but not great for a database. I can write so much data, but I can only read so much data. After my .json file reaches a certain point, I can't read any more data from it. But on the other hand, SQLite is great for reading and writing, so yeah, I should have used SQLite instead of JSON.

This is just a brief project breakdown not as technical as my Ark breakdown but technical enough.