Help in identifying data obfuscation(protocol impersonation using cookie)

Hi guys,

I’m trying to identify potential data obfuscation (protocol impersonation using cookie). However, i’m stumbling upon some issue committing a detection. Not sure if someone can kindly advise on it.

My objective is to identify any traffic where there are more than 3x different cookie header data found from a single source IP towards the same domain within 5 minutes.

So i attempted to use session table to store the data then use the key_name as an identifier for the session before storing the cookie data from the session into an array to do a unique count, but somehow was not able to commit a detection for my test traffic.

Debug output logs unfortunately also does not seems to be in sequence, making it even harder to troubleshoot any issues.

// session table options, if >2 occurences, in 5 minutes then flag a detection
let opts = {
expire: 300, // 5 mins to expire.
notify: true // notify on expiration
};

// Our threshold. If we see this many or more hash counts
// in a 5 minute of period of time, let's fire a detection.
let threshold = 3;

if (event == "HTTP_RESPONSE" || event == "HTTP_REQUEST") {

//For testing, return if the network traffic does not contain any cookie to reduce noise. 
if(HTTP.headers.cookie === null)
    return;

let sess = {};

// Get metrics and build our session table object
sess['src_ip'] = Flow.client.ipaddr.toString();
sess['host'] = HTTP.host;
sess['cookie'] = HTTP.headers.cookies;

// Now, concat, then hash.
let md5_sum = md5(sess.src_ip + sess.host);
let key_name = "zzzzz:" + md5_sum;

// Let's add to the Session table.
var sess_id = Session.add(key_name, sess, opts); // store this info to the table 

if (sess_id.hasOwnProperty('count')) {
    sess_id.count = ++sess_id.count;
    Session.modify(key_name, sess_id);
} else { 
    sess_id.count = 1;
    Session.modify(key_name, sess_id);
}
}

if (event == "SESSION_EXPIRE") {

let expiredKeys = Session.expiredKeys;
var cookie_counter;
var cookie_array = [];

for (var i = 0; i < expiredKeys.length; i++) {

    //reduce noise by returning undefined key names
    if(expiredKeys[i].name === undefined)
        return;

    if(expiredKeys[i].name.startsWith("zzzz")){
        //loop through session and add into cookie array.
        for (var x = 0; x < expiredKeys[x].value.count; x++)
        cookie_array[x] = expiredKeys[x].value.cookie;
    }

    //get number of unique cookies as we are only interested in unique cookie data
    cookie_array = [...new Set(cookie_array)];
    cookie_counter = cookie_array.length;

    if ((cookie_counter > threshold)) {

        // Ok, let's commit the detecion
        commitDetection('Potential_PIC', {
            categories: ['sec.command'],
            title: "Potential_PIC",
            participants: [
                { role: 'offender', object: expiredKeys[i].value.src_ip }
            ],
            description:
                "Potential PIC test: " + "\n" +
                "- **Client IP:** " + expiredKeys[i].value.src_ip + "\n" +
                "- **HTTP Host:** " + expiredKeys[i].value.host,
            identityKey: [
                expiredKeys[i].value.src_ip,
                expiredKeys[i].value.host,
            ].join('!!!!'),
            identityTtl: "hour",
            riskScore: 5               
        })
    }
}
}

Reading over this, a couple things jumped out at me. Since the forums don’t provide line numbers, I’ve pulled out the relevant snippets below.

//For testing, return if the network traffic does not contain any cookie to reduce noise. 
if(HTTP.headers.cookie === null)
    return;

Does this ever return non-null values? I think you were trying to access HTTP.cookies here. The HTTP header name is Cookie, rather than cookie, so I’d be surprised if the trigger ever gets past this check.

Then later the trigger says:

sess['cookie'] = HTTP.headers.cookies;

Here again, I think you were looking to access HTTP.cookies. The expression HTTP.headers.cookies will return the first header with the literal name cookies, which I would never expect to be present.


Later on, you have this piece:

    // reduce noise by returning undefined key names
    if(expiredKeys[i].name === undefined)
        return;

Have you seen name be undefined? Note that this would exit the entire trigger rather than advancing the loop - I don’t think that’s intended.


Starting at the line below, I’m unable to follow the desired behavior:

//loop through session and add into cookie array.
        for (var x = 0; x < expiredKeys[x].value.count; x++)
        cookie_array[x] = expiredKeys[x].value.cookie;

cookie_array is initialized once, outside the outer for loop. However, the counter x is declared inside the loop, meaning it starts at 0 each time.

For the input [{ value: { count: 3, cookie: 'A' }}, { value: { count: 2, cookie: 'B' }}], the value of cookie_array will evolve as follows:

[]
// for expiring key...

// for x in 0..count ...
  ['A']
  ['A', 'A']
  ['A', 'A', 'A']
// end of for x

// cookie_array = [...new Set(cookie_array)]
['A']

// outer loop iterates

// for x in 0..count ...
  ['B']
  ['B', 'B']
// end of inner loop

// cookie_array = [...new Set(cookie_array)]
['B']

What’s the desired behavior here?

Also worth noting that this behavior depends on the data type of cookie; using new Set(...) only does value equality checks on primitive values, e.g. strings. The data type of HTTP.cookies is an object, so I think the use of Set won’t have the expected outcome.


Edit 2: I noticed one more issue in the commitDetection call:

The trigger adds a participant here:

{ role: 'offender', object: expiredKeys[i].value.src_ip }

However, the data type of src_ip was string when the IP was saved into the session table:

sess['src_ip'] = Flow.client.ipaddr.toString();

I would expect commitDetection to fail at runtime when given a participant object of the wrong data type. You should be able to fix this by simply removing the toString call.

Hi,

Really appreciate your feedback.

I tried to retrieve cookie data using HTTP.cookies but it does not return the information i need. However, HTTP.headers.cookie did return what i was checking out for. Not sure if it is supposed to be that way.

I do also often see undefined expiredkeys for some reason, hence, i returned them to reduce noise (it only produce more debug logs).

As my objective is to identify constant change in the cookie header for any http hostname from any workstation, I tried to add several counts ofsess object (with source IP and http host as keyname and cookie as my ‘data check’) into the expiredKeys to identify if there are >3 counts of different cookie data for a single domain within 5 minutes. Hence, the attempt to loop through all cookie data and filter out the unique values.

However, i can’t seems to properly store/retrieve the cookie data, hence the entire structure of the code seems a little off.

Would appreciate if you could kindly advise what is the most “efficient and appropriate” way to store, retrieve each HTTP host’s cookie data and to identify unique cookie data.

As for the commit detection, i did a test run and it seems to be working as expected.

I think something like the below would do what you need.

const KEY_NAMESPACE = "zzzzz:";

const MIN_OCCURRENCES = 3;

const PERIOD_SEC = 300;

// This type annotation makes sure we are reading and writing the same properties
// when we interact with our objects from the session table. It's understood by
// the trigger editor, and has no effect at runtime.
/**
 * @typedef SessionEntry IP-host pair and the list of observed cookie values.
 * @property src_ip {IPAddress}
 * @property host {string}
 * @property cookies {string[]}
 */

/**
 * Check a session table entry to see if a detection should be fired.
 * 
 * @param {SessionEntry} entry
 */
function processEntry(entry) {
    if (new Set(entry.cookies).size <= MIN_OCCURRENCES) return;

    commitDetection('Potential_PIC', {
        categories: ['sec', 'sec.command'],
        title: 'Potential PIC',
        description: [
            "Potential PIC test: ",
            "- **Client IP:** " + entry.src_ip,
            "- **HTTP Host:** " + entry.host,
        ].join('\n'),
        participants: [
            { role: 'offender', object: entry.src_ip }
        ],
        identityKey: [
            entry.src_ip.toString(),
            entry.host
        ].join('!!!!'),
        identityTtl: 'hour',
        riskScore: 5,
    });
}

/**
 * Update the session table with the newly-seen cookie value for the given IP-host pair.
 * 
 * @param {IPAddress} src_ip
 * @param {string} host
 * @param {string} cookie
 * 
 * @returns {SessionEntry} The latest value written to the session table for this key.
 */
function updateSessionEntry(src_ip, host, cookie) {
    /** 
     * A session entry for this IP-host pair with no cookies. We unconditionally
     * add the cookie value below, so adding it here would create duplicates in the array.
     * 
     * @type SessionEntry 
     */
    const blankEntry = {
        src_ip: src_ip,
        host: host,
        cookies: [],
    };

    const key_name = KEY_NAMESPACE + md5(blankEntry.src_ip + blankEntry.host);

    /** @type SessionEntry */
    const sessionEntry = Session.add(key_name, blankEntry);

    sessionEntry.cookies.push(cookie);

    Session.replace(key_name, sessionEntry, {
        // Set expiration on update so that new cookies prolong the entry's life.
        expire: PERIOD_SEC
    });

    return sessionEntry;
}

const cookieValue = HTTP.headers.cookie;

// Early-exit logic
if(cookieValue === null || cookieValue === undefined) return;

const liveEntry = updateSessionEntry(Flow.client.ipaddr, HTTP.host, cookieValue);
processEntry(liveEntry);

Rather than wait for SESSION_EXPIRE, we check the number of unique cookie values whenever we add one to the session entry.

This version is set to only fire on HTTP_REQUEST; my expectation is that for impersonation we care about the cookie the client is sending, though maybe that’s incorrect?

Note: The above code’s session entry will keep all cookie values for a given src/host pair until that pair is idle for PERIOD_SEC. If it’s important to expire individual cookie values without waiting for that idle, change the type of cookies to be { value: string; seen_at: number }[] and then in updateSessionEntry manually remove expired entries before writing a new value to the array.

Hi,

Thanks a lot for the help and suggestion. Really appreciate it.

I’m trying to validate the data passing through the trigger script below but I realized after initializing and assigning session data into sessionEntry variable, the IP address seems to return 1.0.0.0. I have checked that the IP is correctly parsed into blankEntry correctly but somehow it becomes otherwise after that which is quite unusual to me.

That sounds like it might be a bug when IP addresses are written to/read from the session table; I’d suggest reaching out to Support on that.

Hi Bryan,

We’ve investigated this behavior and there is indeed a bug here that we’ll be fixing in an upcoming release. Thanks for calling this out. In the meantime, you can work around this by creating a new IPAddress: new IPAddress(Flow.client.ipaddr.toString()) before adding it to the session table. Sorry about that, and hope the workaround is helpful.

1 Like

Hi guys,

Thanks again for all the help.

As advised, have tried to create a new IP below and the IP seems to be parsed properly,

const liveEntry = updateSessionEntry(new IPAddress(Flow.client.ipaddr.toString()), HTTP.host, cookie_Value);

However, i think i might have found another bug with the session table. Please do let me know if i’m wrong. The code below,

Session.replace(key_name, sessionEntry, {
    // Set expiration on update so that new cookies prolong the entry's life.
    expire: PERIOD_SEC
});

is suppose to replace the sessionEntry object with the updated cookie value until the session key expires as long as the keyname matches. However, the session does not seems to be lasting till the assigned expiry time (I extended it to 3600 and changed the min occurrence to 7 after baselining in my environment). I have also checked that the src_ip and host remains consisted (based on the output from the debug logs).

Debug logs also shows that the number of cookies in the session’s blankentry cookie array does not seems to append which maybe indicate the expiry time did not take effect.

Would appreciate if you could kindly advise on this.

EDIT: After continuously running my test traffic for approx. 2 hours, i’m starting to see the cookie data append into session table properly. However, occasionally, it will “break” out of the session table and “start” another new session table despite having the same keyname.

Is it possible for you to pause adding/updating things to the session table with same key name for while and let things expire out? If you have been continuously updating for a long while, some old values could be sticking around.

Hi @ryanc,

Apparnately, after running the trigger script for an extended period of time after the intended expiry period, the session table got flushed out and then appended with the correct data. Will take note of this in future.

Thanks for the information!

2 Likes