Skip to content

Commit 47597e7

Browse files
authored
Merge pull request #110 from outflanknl/cs4.2
Cobalt Strike 4.2 support
2 parents f4dca28 + faba889 commit 47597e7

6 files changed

Lines changed: 164 additions & 115 deletions

File tree

c2servers/filebeat/filebeat_cobaltstrike.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,27 @@ filebeat.inputs:
101101
log:
102102
type: keystrokes
103103

104+
- type: log
105+
scan_frequency: 5s
106+
enabled: true
107+
fields_under_root: true
108+
paths:
109+
- /root/cobaltstrike/logs/*/*/screenshots.log
110+
# 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
111+
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 ["
112+
multiline.negate: true
113+
multiline.match: after
114+
multiline.max_lines: 100000
115+
fields:
116+
infra:
117+
log:
118+
type: rtops
119+
c2:
120+
program: cobaltstrike
121+
log:
122+
type: screenshots
123+
124+
104125
filebeat.config.modules:
105126
path: ${path.config}/modules.d/*.yml
106127
reload.enabled: false

elkserver/docker/redelk-logstash/redelkinstalldata/redelk-main/conf.d/51-filter-c2-cobaltstrike_logstash.conf

Lines changed: 91 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,23 @@ filter {
1616
}
1717
}
1818

19-
if [c2][log][type] == "events" {
20-
# Get the timestamp from the log line, and get the rest of the log line
21-
# Since CS version 3.14 the logging changed to include the UTC keyword
22-
if " UTC " not in [message] { # check for legacy CS version, will be removed in the future
23-
grok {
24-
match => { "message" => "(?<[c2][timestamp]>%{MONTHNUM}\/%{MONTHDAY} %{HOUR}\:%{MINUTE}) %{GREEDYDATA:[c2][message]}" }
25-
}
26-
# Set the timestamp from the log to @timestamp
27-
date {
28-
match => [ "[c2][timestamp]", "MM/dd HH:mm" ]
29-
target => "@timestamp"
30-
timezone => "Etc/UTC"
31-
}
32-
} else { # check for newer version of CS, contains "UTC" in time logging lines
33-
grok {
34-
match => { "message" => "(?<[c2][timestamp]>%{MONTHNUM}\/%{MONTHDAY} %{HOUR}\:%{MINUTE}\:%{SECOND}) UTC %{GREEDYDATA:[c2][message]}" }
35-
}
36-
# Set the timestamp from the log to @timestamp
37-
date {
38-
match => [ "[c2][timestamp]", "MM/dd HH:mm:ss" ]
39-
target => "@timestamp"
40-
timezone => "Etc/UTC"
41-
}
42-
} # end of legacy CS version check
19+
# Get the timestamp from the log line, and get the rest of the log line to c2.message
20+
grok {
21+
match => { "message" => "(?<[c2][timestamp]>%{MONTHNUM}\/%{MONTHDAY} %{TIME}) UTC( |\t)%{GREEDYDATA:[c2][message]}" }
22+
}
23+
# Set the timestamp from the log to @timestamp
24+
date {
25+
match => [ "[c2][timestamp]", "MM/dd HH:mm:ss" ]
26+
target => "@timestamp"
27+
timezone => "Etc/UTC"
28+
}
4329

4430

31+
#
32+
# Cobalt Strike event log
33+
# Parsed by filebeat as c2.log.type:events
34+
#
35+
if [c2][log][type] == "events" {
4536
# matching lines like: *** initial beacon from username@ip (hostname)
4637
if " initial beacon from " in [c2][message] {
4738
mutate {
@@ -58,34 +49,22 @@ filter {
5849
mutate {
5950
replace => { "[c2][log][type]" => "events_joinleave" }
6051
}
61-
}
62-
}
6352

64-
if [c2][log][type] == "beacon" {
65-
# Get the timestamp from the log line, and get the rest of the log line
66-
# Since CS version 3.14 the logging changed to include the UTC keyword
67-
if " UTC " not in [message] { # check for legacy CS version, will be removed in the future
6853
grok {
69-
match => { "message" => "(?<[c2][timestamp]>%{MONTHNUM}\/%{MONTHDAY} %{TIME}) %{GREEDYDATA:[c2][message]}" }
70-
}
71-
# Set the timestamp from the log to @timestamp
72-
date {
73-
match => [ "[c2][timestamp]", "MM/dd HH:mm" ]
74-
target => "@timestamp"
75-
timezone => "Etc/UTC"
76-
}
77-
} else { # check for newer version of CS, contains "UTC" in time logging lines
78-
grok {
79-
match => { "message" => "(?<[c2][timestamp]>%{MONTHNUM}\/%{MONTHDAY} %{TIME}) UTC %{GREEDYDATA:[c2][message]}" }
80-
}
81-
# Set the timestamp from the log to @timestamp
82-
date {
83-
match => [ "[c2][timestamp]", "MM/dd HH:mm:ss" ]
84-
target => "@timestamp"
85-
timezone => "Etc/UTC"
54+
match => { "[c2][message]" => [
55+
"\*\*\* (?<[c2][operator]>([^\()]*)) \(%{IP:[c2][operator_ip]}\) joined",
56+
"\*\*\* %{GREEDYDATA:[c2][operator]} quit"
57+
]}
8658
}
87-
} # end of legacy CS version check
59+
}
60+
}
8861

62+
63+
#
64+
# Cobalt Strike beacon log
65+
# Parsed by filebeat as c2.log.type:beacon
66+
#
67+
if [c2][log][type] == "beacon" {
8968
# Add path/URI value to the full beacon.log file
9069
ruby {
9170
path => "/usr/share/logstash/redelk-main/scripts/cs_makebeaconlogpath.rb"
@@ -101,8 +80,7 @@ filter {
10180
grok {
10281
match => { "[log][file][path]" => [
10382
"/cobaltstrike/logs/((\d{6}))/unknown/(beacon|ssh)_(?<[implant][id]>(\d{1,10}))",
104-
"/cobaltstrike/logs/((\d{6}))/%{IPORHOST:[host][ip_int]}/beacon_(?<[implant][id]>(\d{1,10}))",
105-
"/cobaltstrike/logs/((\d{6}))/%{IPORHOST:[host][ip_int]}/ssh_(?<[implant][id]>(\d{1,10}))"
83+
"/cobaltstrike/logs/((\d{6}))/%{IPORHOST:[host][ip_int]}/(beacon|ssh)_(?<[implant][id]>(\d{1,10}))"
10684
] }
10785
}
10886

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

196-
# Since Cobalt Strike v3.14 the task log line contains MITRE ATT&CK numbers of the task that is about to be performed.
174+
# The task log line can contain MITRE ATT&CK numbers of the task that is about to be performed.
197175
# Example: [task] <T1113, T1093> Tasked beacon to take screenshot
198-
# 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.
176+
# Here we check if '<T' and '>' are in c2.message. If so, we parse the field.
199177
# We also check if there are multiple values, and if so split them up
200178
if "<T" in [implant][task] and ">" in [implant][task] {
201179
grok {
@@ -272,10 +250,11 @@ filter {
272250
}
273251
}
274252

253+
# Leaving this in here for legacy as screenshot logging changed in CS4.2.
275254
# check for received screenshots and add a path value to the screenshot
276255
if "received screenshot (" in [implant][output] {
277256
ruby {
278-
path => "/usr/share/logstash/redelk-main/scripts/cs_makescreenshotpath.rb"
257+
path => "/usr/share/logstash/redelk-main/scripts/cs_makescreenshotpath_beforecs4.2.rb"
279258
}
280259
}
281260
}
@@ -290,34 +269,35 @@ filter {
290269
match => { "[c2][message]" => "]%{GREEDYDATA:[implant][output]}" }
291270
}
292271
}
293-
294272
}
295273

296-
if [c2][log][type] == "keystrokes" {
297-
# Get the timestamp from the log line, and get the rest of the log line
298-
# Since CS version 3.14 the logging changed to include the UTC keyword
299-
if " UTC " not in [message] { # check for legacy CS version, will be removed in the future
300-
grok {
301-
match => { "message" => "(?<[c2][timestamp]>%{MONTHNUM}\/%{MONTHDAY} %{TIME}) %{GREEDYDATA:[c2][message]}" }
302-
}
303-
# Set the timestamp from the log to @timestamp
304-
date {
305-
match => [ "[c2][timestamp]", "MM/dd HH:mm:ss" ]
306-
target => "@timestamp"
307-
timezone => "Etc/UTC"
308-
}
309-
} else { # check for newer version of CS, contains "UTC" in time logging lines
310-
grok {
311-
match => { "message" => "(?<[c2][timestamp]>%{MONTHNUM}\/%{MONTHDAY} %{TIME}) UTC %{GREEDYDATA:[c2][message]}" }
312-
}
313-
# Set the timestamp from the log to @timestamp
314-
date {
315-
match => [ "[c2][timestamp]", "MM/dd HH:mm:ss" ]
316-
target => "@timestamp"
317-
timezone => "Etc/UTC"
318-
}
274+
275+
#
276+
# Cobalt Strike screenshots log
277+
# Parsed by filebeat as c2.log.type:screenshots
278+
#
279+
# 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
280+
if [c2][log][type] == "screenshots" {
281+
# Matching lines like: 11/06 21:07:30 UTC MARCS-TEST 1 marcs screen_30efde80_1518442534.jpg
282+
grok {
283+
match => { "[c2][message]" => "(?<[host][name]>([^\s]*))\s(?<[screenshot][desktop_session]>([^\t]*))\t(?<[user][name]>([^\t]*))\t(?<[screenshot][file_name]>([^\t]*))\t(%{GREEDYDATA:[screenshot][title]})" }
284+
}
285+
grok {
286+
match => { "[screenshot][file_name]" => "screen_([^_]*)_(?<[implant][id]>(\d{1,10}))"}
319287
}
288+
289+
# add url to screenshot files (full and thumb)
290+
ruby {
291+
path => "/usr/share/logstash/redelk-main/scripts/cs_makescreenshotpath.rb"
292+
}
293+
}
294+
320295

296+
#
297+
# Cobalt Strike keystrokes log
298+
# Parsed by filebeat as c2.log.type:keystrokes
299+
#
300+
if [c2][log][type] == "keystrokes" {
321301
# Set the beacon id from the file name
322302
# 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'.
323303
# It is expected that the logs are in the default subdirectory of the folder cobaltstrike: /cobaltstrike/logs/
@@ -328,45 +308,46 @@ filter {
328308
]}
329309
}
330310

331-
# add url to full keystrokes file
332-
ruby {
333-
path => "/usr/share/logstash/redelk-main/scripts/cs_makekeystrokespath.rb"
334-
}
335-
}
336-
337-
if [c2][log][type] == "downloads" {
338-
if " UTC " not in [message] { # check for legacy CS version, will be removed in the future
339-
# Matching lines like: #1546505606424 10.202.1.11 12654 7 /root/cobaltstrike/downloads/9ce6fbfb1 testdoc.txt C:\Users\Administrator\Desktop\
311+
# In CS 4.2 the log line inside the keystroke file changed. We now have two possible matches:
312+
# 1. 11/13 10:15:32 UTC Received keystrokes from marc in desktop 2
313+
# 2. 10/02 11:17:31 UTC Received keystrokes - pre CS 4.2
314+
if " from " in [c2][message] and " in desktop " in [c2][message] {
340315
grok {
341-
# TODO: the large int is a timestamp (in ms)
342-
# 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.
343-
match => { "message" => "%{WORD}(\t)%{GREEDYDATA:[c2][message]}" }
316+
match => { "[c2][message]" => "Received keystrokes from %{GREEDYDATA:[keystrokes][user]} in desktop %{INT:[keystrokes][desktop_session]}" }
344317
}
345-
grok {
346-
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]}" }
318+
ruby {
319+
path => "/usr/share/logstash/redelk-main/scripts/cs_makekeystrokespath.rb"
347320
}
348-
} else { # check for newer version of CS, contains "UTC" in time logging lines
349-
# 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\
350-
grok {
351-
match => { "message" => "(?<[c2][timestamp]>%{MONTHNUM}\/%{MONTHDAY} %{HOUR}\:%{MINUTE}\:%{SECOND}) UTC(\t)%{GREEDYDATA:[c2][message]}" }
352-
}
353-
grok {
354-
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]}" }
355-
}
356-
# Set the timestamp from the log to @timestamp
357-
date {
358-
match => [ "[c2][timestamp]", "MM/dd HH:mm:ss" ]
359-
target => "@timestamp"
360-
timezone => "Etc/UTC"
321+
} else {
322+
ruby {
323+
path => "/usr/share/logstash/redelk-main/scripts/cs_makekeystrokespath_beforecs4.2.rb"
361324
}
362-
} # end of legacy CS version check
325+
}
326+
}
327+
363328

364-
# add url to full keystrokes file
329+
#
330+
# Cobalt Strike downloads log
331+
# Parsed by filebeat as c2.log.type:downloads
332+
#
333+
if [c2][log][type] == "downloads" {
334+
# 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\
335+
grok {
336+
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]}" }
337+
}
338+
339+
# add url to full downloads file
365340
ruby {
366341
path => "/usr/share/logstash/redelk-main/scripts/cs_makedownloadspath.rb"
367342
}
368343
}
369344

345+
346+
347+
#
348+
# Cobalt Strike credentials log
349+
# Parsed by filebeat as c2.log.type:credentials
350+
#
370351
if [c2][log][type] == "credentials" {
371352
# Drop the first line with headers
372353
if "#User" in [message] {
@@ -379,6 +360,10 @@ filter {
379360
}
380361
}
381362

363+
364+
#
365+
# Generic tidy up things below
366+
#
382367
# Add data about OS for nice display
383368
if [host][os][kernel] and [c2][log][type] != "credentials" {
384369
mutate {
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#
22
# Part of RedELK
33
# Script to have logstash insert an extra field pointing to the full TXT file of a Cobalt Strike keystrokes file
4+
# Cobalt Strike 4.2 and higher
45
#
56
# Author: Outflank B.V. / Marc Smeets
67
#
@@ -9,10 +10,11 @@ def filter(event)
910
host = event.get("[agent][name]")
1011
logpath = event.get("[log][file][path]")
1112
implant_id = event.get("[implant][id]")
13+
desktop_session = event.get("[keystrokes][desktop_session]")
1214
temppath = logpath.split('/cobaltstrike')
1315
temppath2 = temppath[1].split(/\/([^\/]*)$/)
14-
keystrokespath = "/c2logs/" + "#{host}" + "#{temppath2[0]}" + "/keystrokes_" + "#{implant_id}" + ".txt"
16+
keystrokespath = "/c2logs/" + "#{host}" + "#{temppath2[0]}" + "/keystrokes_" + "#{implant_id}" + "." + "#{desktop_session}" + ".txt"
1517
event.tag("_rubyparseok")
16-
event.set("[keystrokes][url]", keystrokespath)
18+
event.set("[keystrokes][url]", keystrokespath)
1719
return [event]
1820
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#
2+
# Part of RedELK
3+
# Script to have logstash insert an extra field pointing to the full TXT file of a Cobalt Strike keystrokes file
4+
# Before Cobalt Strike 4.2
5+
#
6+
# Author: Outflank B.V. / Marc Smeets
7+
#
8+
9+
def filter(event)
10+
host = event.get("[agent][name]")
11+
logpath = event.get("[log][file][path]")
12+
implant_id = event.get("[implant][id]")
13+
temppath = logpath.split('/cobaltstrike')
14+
temppath2 = temppath[1].split(/\/([^\/]*)$/)
15+
keystrokespath = "/c2logs/" + "#{host}" + "#{temppath2[0]}" + "/keystrokes_" + "#{implant_id}" + ".txt"
16+
event.tag("_rubyparseok")
17+
event.set("[keystrokes][url]", keystrokespath)
18+
return [event]
19+
end
Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
11
#
22
# Part of RedELK
33
# Script to have logstash insert extra fields pointing to the Cobalt Strike screenshots
4+
# Cobalt Strike 4.2 and higher
45
#
56
# Author: Outflank B.V. / Marc Smeets
67
#
78

89
def filter(event)
9-
require 'time'
1010
host = event.get("[agent][name]")
1111
logpath = event.get("[log][file][path]")
12-
implant_id = event.get("[implant][id]")
13-
timefromcs = event.get("[c2][timestamp]") + " UTC"
14-
timestring = Time.parse(timefromcs).strftime("%I%M%S")
12+
filename = event.get("[screenshot][file_name]")
1513
temppath = logpath.split('/cobaltstrike')
1614
temppath2 = temppath[1].split(/\/([^\/]*)$/)
17-
screenshoturl = "/c2logs/" + "#{host}" + "#{temppath2[0]}" + "/screenshots/screen_" + "#{timestring}" + "_" + "#{implant_id}" + ".jpg"
18-
thumburl = "/c2logs/" + "#{host}" + "#{temppath2[0]}" + "/screenshots/screen_" + "#{timestring}" + "_" + "#{implant_id}" + ".jpg.thumb.jpg"
15+
screenshoturl = "/c2logs/" + "#{host}" + "#{temppath2[0]}" + "/screenshots/"+ "#{filename}"
16+
thumburl = "/c2logs/" + "#{host}" + "#{temppath2[0]}" + "/screenshots/"+ "#{filename}" + ".thumb.jpg"
1917
event.tag("_rubyparseok")
2018
event.set("[screenshot][full]", screenshoturl)
2119
event.set("[screenshot][thumb]", thumburl)
2220
return [event]
23-
end
21+
end

0 commit comments

Comments
 (0)