Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions c2servers/filebeat/filebeat_cobaltstrike.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,27 @@ filebeat.inputs:
log:
type: keystrokes

- type: log
scan_frequency: 5s
enabled: true
fields_under_root: true
paths:
- /root/cobaltstrike/logs/*/*/screenshots.log
# Since Cobalt Strike version 3.14 the time format in the logs is changed. Here we use regex 'or' function (expr1)|(expr2) to match new or old format
multiline.pattern: '(^\d\d\/\d\d\s\d\d\:\d\d\:\d\d\sUTC\s\[)|(^\d\d\/\d\d\s\d\d\:\d\d\:\d\d\s\[)' # match "06/19 12:32:56 UTC [" or "06/19 12:32:56 ["
multiline.negate: true
multiline.match: after
multiline.max_lines: 100000
fields:
infra:
log:
type: rtops
c2:
program: cobaltstrike
log:
type: screenshots


filebeat.config.modules:
path: ${path.config}/modules.d/*.yml
reload.enabled: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,23 @@ filter {
}
}

if [c2][log][type] == "events" {
# Get the timestamp from the log line, and get the rest of the log line
# Since CS version 3.14 the logging changed to include the UTC keyword
if " UTC " not in [message] { # check for legacy CS version, will be removed in the future
grok {
match => { "message" => "(?<[c2][timestamp]>%{MONTHNUM}\/%{MONTHDAY} %{HOUR}\:%{MINUTE}) %{GREEDYDATA:[c2][message]}" }
}
# Set the timestamp from the log to @timestamp
date {
match => [ "[c2][timestamp]", "MM/dd HH:mm" ]
target => "@timestamp"
timezone => "Etc/UTC"
}
} else { # check for newer version of CS, contains "UTC" in time logging lines
grok {
match => { "message" => "(?<[c2][timestamp]>%{MONTHNUM}\/%{MONTHDAY} %{HOUR}\:%{MINUTE}\:%{SECOND}) UTC %{GREEDYDATA:[c2][message]}" }
}
# Set the timestamp from the log to @timestamp
date {
match => [ "[c2][timestamp]", "MM/dd HH:mm:ss" ]
target => "@timestamp"
timezone => "Etc/UTC"
}
} # end of legacy CS version check
# Get the timestamp from the log line, and get the rest of the log line to c2.message
grok {
match => { "message" => "(?<[c2][timestamp]>%{MONTHNUM}\/%{MONTHDAY} %{TIME}) UTC( |\t)%{GREEDYDATA:[c2][message]}" }
}
# Set the timestamp from the log to @timestamp
date {
match => [ "[c2][timestamp]", "MM/dd HH:mm:ss" ]
target => "@timestamp"
timezone => "Etc/UTC"
}


#
# Cobalt Strike event log
# Parsed by filebeat as c2.log.type:events
#
if [c2][log][type] == "events" {
# matching lines like: *** initial beacon from username@ip (hostname)
if " initial beacon from " in [c2][message] {
mutate {
Expand All @@ -58,34 +49,22 @@ filter {
mutate {
replace => { "[c2][log][type]" => "events_joinleave" }
}
}
}

if [c2][log][type] == "beacon" {
# Get the timestamp from the log line, and get the rest of the log line
# Since CS version 3.14 the logging changed to include the UTC keyword
if " UTC " not in [message] { # check for legacy CS version, will be removed in the future
grok {
match => { "message" => "(?<[c2][timestamp]>%{MONTHNUM}\/%{MONTHDAY} %{TIME}) %{GREEDYDATA:[c2][message]}" }
}
# Set the timestamp from the log to @timestamp
date {
match => [ "[c2][timestamp]", "MM/dd HH:mm" ]
target => "@timestamp"
timezone => "Etc/UTC"
}
} else { # check for newer version of CS, contains "UTC" in time logging lines
grok {
match => { "message" => "(?<[c2][timestamp]>%{MONTHNUM}\/%{MONTHDAY} %{TIME}) UTC %{GREEDYDATA:[c2][message]}" }
}
# Set the timestamp from the log to @timestamp
date {
match => [ "[c2][timestamp]", "MM/dd HH:mm:ss" ]
target => "@timestamp"
timezone => "Etc/UTC"
match => { "[c2][message]" => [
"\*\*\* (?<[c2][operator]>([^\()]*)) \(%{IP:[c2][operator_ip]}\) joined",
"\*\*\* %{GREEDYDATA:[c2][operator]} quit"
]}
}
} # end of legacy CS version check
}
}


#
# Cobalt Strike beacon log
# Parsed by filebeat as c2.log.type:beacon
#
if [c2][log][type] == "beacon" {
# Add path/URI value to the full beacon.log file
ruby {
path => "/usr/share/logstash/redelk-main/scripts/cs_makebeaconlogpath.rb"
Expand All @@ -101,8 +80,7 @@ filter {
grok {
match => { "[log][file][path]" => [
"/cobaltstrike/logs/((\d{6}))/unknown/(beacon|ssh)_(?<[implant][id]>(\d{1,10}))",
"/cobaltstrike/logs/((\d{6}))/%{IPORHOST:[host][ip_int]}/beacon_(?<[implant][id]>(\d{1,10}))",
"/cobaltstrike/logs/((\d{6}))/%{IPORHOST:[host][ip_int]}/ssh_(?<[implant][id]>(\d{1,10}))"
"/cobaltstrike/logs/((\d{6}))/%{IPORHOST:[host][ip_int]}/(beacon|ssh)_(?<[implant][id]>(\d{1,10}))"
] }
}

Expand Down Expand Up @@ -193,9 +171,9 @@ filter {
match => { "[c2][message]" => "(([^\s]*)) %{GREEDYDATA:[implant][task]}" }
}

# Since Cobalt Strike v3.14 the task log line contains MITRE ATT&CK numbers of the task that is about to be performed.
# The task log line can contain MITRE ATT&CK numbers of the task that is about to be performed.
# Example: [task] <T1113, T1093> Tasked beacon to take screenshot
# Here we check if '<T' and '>' are in c2.message. If so, we parse the field. If not, we assume its an old CS version and skip the creation of the ATT&CK Technique field.
# Here we check if '<T' and '>' are in c2.message. If so, we parse the field.
# We also check if there are multiple values, and if so split them up
if "<T" in [implant][task] and ">" in [implant][task] {
grok {
Expand Down Expand Up @@ -272,10 +250,11 @@ filter {
}
}

# Leaving this in here for legacy as screenshot logging changed in CS4.2.
# check for received screenshots and add a path value to the screenshot
if "received screenshot (" in [implant][output] {
ruby {
path => "/usr/share/logstash/redelk-main/scripts/cs_makescreenshotpath.rb"
path => "/usr/share/logstash/redelk-main/scripts/cs_makescreenshotpath_beforecs4.2.rb"
}
}
}
Expand All @@ -290,34 +269,35 @@ filter {
match => { "[c2][message]" => "]%{GREEDYDATA:[implant][output]}" }
}
}

}

if [c2][log][type] == "keystrokes" {
# Get the timestamp from the log line, and get the rest of the log line
# Since CS version 3.14 the logging changed to include the UTC keyword
if " UTC " not in [message] { # check for legacy CS version, will be removed in the future
grok {
match => { "message" => "(?<[c2][timestamp]>%{MONTHNUM}\/%{MONTHDAY} %{TIME}) %{GREEDYDATA:[c2][message]}" }
}
# Set the timestamp from the log to @timestamp
date {
match => [ "[c2][timestamp]", "MM/dd HH:mm:ss" ]
target => "@timestamp"
timezone => "Etc/UTC"
}
} else { # check for newer version of CS, contains "UTC" in time logging lines
grok {
match => { "message" => "(?<[c2][timestamp]>%{MONTHNUM}\/%{MONTHDAY} %{TIME}) UTC %{GREEDYDATA:[c2][message]}" }
}
# Set the timestamp from the log to @timestamp
date {
match => [ "[c2][timestamp]", "MM/dd HH:mm:ss" ]
target => "@timestamp"
timezone => "Etc/UTC"
}

#
# Cobalt Strike screenshots log
# Parsed by filebeat as c2.log.type:screenshots
#
# This is for CS4.2 and later parsing of screenshot data. Since CS4.2 there is a dedicated screenshots.log file. Before CS4.2 it was parsed from regular beacon log
if [c2][log][type] == "screenshots" {
# Matching lines like: 11/06 21:07:30 UTC MARCS-TEST 1 marcs screen_30efde80_1518442534.jpg
grok {
match => { "[c2][message]" => "(?<[host][name]>([^\s]*))\s(?<[screenshot][desktop_session]>([^\t]*))\t(?<[user][name]>([^\t]*))\t(?<[screenshot][file_name]>([^\t]*))\t(%{GREEDYDATA:[screenshot][title]})" }
}
grok {
match => { "[screenshot][file_name]" => "screen_([^_]*)_(?<[implant][id]>(\d{1,10}))"}
}

# add url to screenshot files (full and thumb)
ruby {
path => "/usr/share/logstash/redelk-main/scripts/cs_makescreenshotpath.rb"
}
}


#
# Cobalt Strike keystrokes log
# Parsed by filebeat as c2.log.type:keystrokes
#
if [c2][log][type] == "keystrokes" {
# Set the beacon id from the file name
# Need to match for 2 different occurence, one where the IP address is known based on the file name, and one where it states 'unknown'.
# It is expected that the logs are in the default subdirectory of the folder cobaltstrike: /cobaltstrike/logs/
Expand All @@ -328,45 +308,46 @@ filter {
]}
}

# add url to full keystrokes file
ruby {
path => "/usr/share/logstash/redelk-main/scripts/cs_makekeystrokespath.rb"
}
}

if [c2][log][type] == "downloads" {
if " UTC " not in [message] { # check for legacy CS version, will be removed in the future
# Matching lines like: #1546505606424 10.202.1.11 12654 7 /root/cobaltstrike/downloads/9ce6fbfb1 testdoc.txt C:\Users\Administrator\Desktop\
# In CS 4.2 the log line inside the keystroke file changed. We now have two possible matches:
# 1. 11/13 10:15:32 UTC Received keystrokes from marc in desktop 2
# 2. 10/02 11:17:31 UTC Received keystrokes - pre CS 4.2
if " from " in [c2][message] and " in desktop " in [c2][message] {
grok {
# TODO: the large int is a timestamp (in ms)
# This type of log does not have a regular timestamp, but it does have a large int at the beginning. Lets throw that away as we have no use for it now.
match => { "message" => "%{WORD}(\t)%{GREEDYDATA:[c2][message]}" }
match => { "[c2][message]" => "Received keystrokes from %{GREEDYDATA:[keystrokes][user]} in desktop %{INT:[keystrokes][desktop_session]}" }
}
grok {
match => { "[c2][message]" => "%{IP:[host][ip_int]}(\t)(?<[implant][id]>(\d{0,10}))(\t)%{INT}(\t)%{NOTSPACE:[file][directory_local]}(\t)(?<[file][name]>([^\t]*))(\t)%{GREEDYDATA:[file][directory]}" }
ruby {
path => "/usr/share/logstash/redelk-main/scripts/cs_makekeystrokespath.rb"
}
} else { # check for newer version of CS, contains "UTC" in time logging lines
# matching lines like: 05/25 13:29:44 UTC 192.168.217.131 93439 70 /root/cobaltstrike/downloads/2914cdfa8 helloworld.ps1 C:\users\marcs\Desktop\
grok {
match => { "message" => "(?<[c2][timestamp]>%{MONTHNUM}\/%{MONTHDAY} %{HOUR}\:%{MINUTE}\:%{SECOND}) UTC(\t)%{GREEDYDATA:[c2][message]}" }
}
grok {
match => { "[c2][message]" => "%{IP:[host][ip_int]}(\t)(?<[implant][id]>(\d{0,10}))(\t)%{INT}(\t)%{NOTSPACE:[file][directory_local]}(\t)(?<[file][name]>([^\t]*))(\t)%{GREEDYDATA:[file][directory]}" }
}
# Set the timestamp from the log to @timestamp
date {
match => [ "[c2][timestamp]", "MM/dd HH:mm:ss" ]
target => "@timestamp"
timezone => "Etc/UTC"
} else {
ruby {
path => "/usr/share/logstash/redelk-main/scripts/cs_makekeystrokespath_beforecs4.2.rb"
}
} # end of legacy CS version check
}
}


# add url to full keystrokes file
#
# Cobalt Strike downloads log
# Parsed by filebeat as c2.log.type:downloads
#
if [c2][log][type] == "downloads" {
# matching lines like: 05/25 13:29:44 UTC 192.168.217.131 93439 70 /root/cobaltstrike/downloads/2914cdfa8 helloworld.ps1 C:\users\marcs\Desktop\
grok {
match => { "[c2][message]" => "%{IP:[host][ip_int]}(\t)(?<[implant][id]>(\d{0,10}))(\t)%{INT}(\t)%{NOTSPACE:[file][directory_local]}(\t)(?<[file][name]>([^\t]*))(\t)%{GREEDYDATA:[file][directory]}" }
}

# add url to full downloads file
ruby {
path => "/usr/share/logstash/redelk-main/scripts/cs_makedownloadspath.rb"
}
}



#
# Cobalt Strike credentials log
# Parsed by filebeat as c2.log.type:credentials
#
if [c2][log][type] == "credentials" {
# Drop the first line with headers
if "#User" in [message] {
Expand All @@ -379,6 +360,10 @@ filter {
}
}


#
# Generic tidy up things below
#
# Add data about OS for nice display
if [host][os][kernel] and [c2][log][type] != "credentials" {
mutate {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#
# Part of RedELK
# Script to have logstash insert an extra field pointing to the full TXT file of a Cobalt Strike keystrokes file
# Cobalt Strike 4.2 and higher
#
# Author: Outflank B.V. / Marc Smeets
#
Expand All @@ -9,10 +10,11 @@ def filter(event)
host = event.get("[agent][name]")
logpath = event.get("[log][file][path]")
implant_id = event.get("[implant][id]")
desktop_session = event.get("[keystrokes][desktop_session]")
temppath = logpath.split('/cobaltstrike')
temppath2 = temppath[1].split(/\/([^\/]*)$/)
keystrokespath = "/c2logs/" + "#{host}" + "#{temppath2[0]}" + "/keystrokes_" + "#{implant_id}" + ".txt"
keystrokespath = "/c2logs/" + "#{host}" + "#{temppath2[0]}" + "/keystrokes_" + "#{implant_id}" + "." + "#{desktop_session}" + ".txt"
event.tag("_rubyparseok")
event.set("[keystrokes][url]", keystrokespath)
event.set("[keystrokes][url]", keystrokespath)
return [event]
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#
# Part of RedELK
# Script to have logstash insert an extra field pointing to the full TXT file of a Cobalt Strike keystrokes file
# Before Cobalt Strike 4.2
#
# Author: Outflank B.V. / Marc Smeets
#

def filter(event)
host = event.get("[agent][name]")
logpath = event.get("[log][file][path]")
implant_id = event.get("[implant][id]")
temppath = logpath.split('/cobaltstrike')
temppath2 = temppath[1].split(/\/([^\/]*)$/)
keystrokespath = "/c2logs/" + "#{host}" + "#{temppath2[0]}" + "/keystrokes_" + "#{implant_id}" + ".txt"
event.tag("_rubyparseok")
event.set("[keystrokes][url]", keystrokespath)
return [event]
end
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
#
# Part of RedELK
# Script to have logstash insert extra fields pointing to the Cobalt Strike screenshots
# Cobalt Strike 4.2 and higher
#
# Author: Outflank B.V. / Marc Smeets
#

def filter(event)
require 'time'
host = event.get("[agent][name]")
logpath = event.get("[log][file][path]")
implant_id = event.get("[implant][id]")
timefromcs = event.get("[c2][timestamp]") + " UTC"
timestring = Time.parse(timefromcs).strftime("%I%M%S")
filename = event.get("[screenshot][file_name]")
temppath = logpath.split('/cobaltstrike')
temppath2 = temppath[1].split(/\/([^\/]*)$/)
screenshoturl = "/c2logs/" + "#{host}" + "#{temppath2[0]}" + "/screenshots/screen_" + "#{timestring}" + "_" + "#{implant_id}" + ".jpg"
thumburl = "/c2logs/" + "#{host}" + "#{temppath2[0]}" + "/screenshots/screen_" + "#{timestring}" + "_" + "#{implant_id}" + ".jpg.thumb.jpg"
screenshoturl = "/c2logs/" + "#{host}" + "#{temppath2[0]}" + "/screenshots/"+ "#{filename}"
thumburl = "/c2logs/" + "#{host}" + "#{temppath2[0]}" + "/screenshots/"+ "#{filename}" + ".thumb.jpg"
event.tag("_rubyparseok")
event.set("[screenshot][full]", screenshoturl)
event.set("[screenshot][thumb]", thumburl)
return [event]
end
end
Loading