Skip to content

Commit 2630d98

Browse files
Merge pull request #192 from Yamato-Security/188-extract-credentials
feat: add `extract-credentials` command
2 parents dba7c2f + afe3250 commit 2630d98

File tree

5 files changed

+133
-4
lines changed

5 files changed

+133
-4
lines changed

CHANGELOG-Japanese.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## 2.7.0 [2024/10/21] SecTor Release
44

5+
**新機能:**
6+
7+
- `extract-credentials`コマンド: セキュリティ4688とSysmon 1イベントのコマンドライン情報から平文の認証情報を取り出す。例: `wmic``schtasks``net user``psexec`の使用。 (#192) (@fukusuket)
8+
59
**改善:**
610

711
- HTMLレポートのRule SummaryページのTotal DetectionsとUnique Detectionsの検出サマリが1つの表に統合された。(#182) (@nishikawaakira)

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## 2.7.0 [2024/10/21] SecTor Release
44

5+
**New Features:**
6+
7+
`extract-credentials` command: extract out plaintext credentials from the command line information in Security 4688 and Sysmon 1 events. Ex: `wmic`, `schtasks`, `net user`, `psexec` usage. (#192) (@fukusuket)
8+
59
**Enhancements:**
610

711
- Detection summary for Total Detections and Unique Detections in the Rule Summary page of the HTML report has been consolidated into one table. (#182) (@nishikawaakira)

src/takajo.nim

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import takajopkg/takajoCore
2626
import takajopkg/takajoTerminal
2727
import takajopkg/ttpResult
2828
import takajopkg/vtResult
29+
include takajopkg/extractCredentials
2930
include takajopkg/extractScriptblocks
3031
include takajopkg/listDomains
3132
include takajopkg/listIpAddresses
@@ -62,7 +63,8 @@ when isMainModule:
6263
clCfg.version = "2.7.0"
6364
const examples = "Examples:\p"
6465
const example_automagic = " automagic -t ../hayabusa/timeline.jsonl [--level low] [--displayTable] -o case-1\p"
65-
const example_extract_scriptblocks = " extract-scriptblocks -t ../hayabusa/timeline.jsonl [--level low] -o scriptblock-logs\p"
66+
const example_extract_credentials = " extract-credentials -t ../hayabusa/timeline.jsonl [--skipProgressBar] -o credentials.csv\p"
67+
const example_extract_scriptblocks = " extract-scriptblocks -t ../hayabusa/timeline.jsonl [--level low] [--skipProgressBar] -o scriptblock-logs\p"
6668
const example_list_domains = " list-domains -t ../hayabusa/timeline.jsonl [--skipProgressBar] -o domains.txt\p"
6769
const example_list_hashes = " list-hashes -t ../hayabusa/case-1.jsonl [--skipProgressBar] -o case-1\p"
6870
const example_list_ip_addresses = " list-ip-addresses -t ../hayabusa/timeline.jsonl [--skipProgressBar] -o ipAddresses.txt\p"
@@ -95,7 +97,7 @@ when isMainModule:
9597
clCfg.useMulti = "Version: 2.7.0 Dev Build\pUsage: takajo.exe <COMMAND>\p\pCommands:\p$subcmds\pCommand help: $command help <COMMAND>\p\p" &
9698
examples &
9799
example_automagic &
98-
example_extract_scriptblocks & example_html_report &
100+
example_extract_credentials & example_extract_scriptblocks & example_html_report &
99101
example_list_domains & example_list_hashes & example_list_ip_addresses & example_list_undetected_evtx & example_list_unused_rules &
100102
example_split_csv_timeline & example_split_json_timeline &
101103
example_stack_cmdlines & example_stack_computers & example_stack_dns & example_stack_ip_addresses & example_stack_logons & example_stack_processes & example_stack_services & example_stack_tasks & example_stack_users &
@@ -119,6 +121,16 @@ when isMainModule:
119121
"timeline": "Hayabusa JSONL timeline file or directory (profile: any)",
120122
}
121123
],
124+
[
125+
extractCredentials, cmdName = "extract-credentials",
126+
doc = "extract plaintext credentials from the command-line auditing",
127+
help = {
128+
"output": "save results to a csv file",
129+
"quiet": "do not display the launch banner (default: false)",
130+
"skipProgressBar": "do not display the progress bar (default: false)",
131+
"timeline": "Hayabusa JSONL timeline file or directory (profile: any)",
132+
}
133+
],
122134
[
123135
extractScriptblocks, cmdName = "extract-scriptblocks",
124136
doc = "extract and reassemble PowerShell EID 4104 script block logs",

src/takajopkg/extractCredentials.nim

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
const ExtractCredentialsMsg =
2+
"""This command extracts plaintext credentials from command-lines in Sysmon 1 and Security 4688 event logs."""
3+
4+
type
5+
ExtractCredentialsCmd* = ref object of AbstractCmd
6+
seqOfResultsTables*: seq[Table[string, string]]
7+
8+
method filter*(self: ExtractCredentialsCmd, x: HayabusaJson): bool =
9+
let eidMatched = (x.EventID == 1 and x.Channel == "Sysmon") or (x.EventID == 4688 and x.Channel == "Sec")
10+
if not eidMatched:
11+
return false
12+
let cmdline = x.Details["Cmdline"].getStr().toLower()
13+
let commands = ["net user", "schtasks", "wmic", "psexec"]
14+
let cmdMatched = any(commands, proc(x: string): bool = x in cmdline)
15+
if "net user" in cmdline:
16+
return true # net user does not have password parameter
17+
let passwordPram = ["-p", "-password", "-passwd", "–password", "–passwd", "/p", "/password", "/passwd"]
18+
let passwordMatched = any(passwordPram, proc(x: string): bool = x in cmdline)
19+
return cmdMatched and passwordMatched
20+
21+
proc extractUserPass(cmd: string): (string, string) =
22+
let patterns = [
23+
(re(r".*/user:([^\s/]+).*"), re(r".*/password:([^\s/]+).*")), # for wmic
24+
(re(r".*/U ([^\s/]+).*"), re(r".*/P ([^\s/]+).*")), # for schtasks
25+
(re(r".*net user ([^\s/]+)"), re(r".*?([^\s/]+) /add.*")), # for net user
26+
(re(r".*-u ([^\s/]+).*"), re(r".*-p ([^\s/]+).*")) # for psexec
27+
]
28+
29+
for (userPattern, passwordPattern) in patterns:
30+
var username = ""
31+
var password = ""
32+
33+
if cmd =~ userPattern:
34+
username = matches[0]
35+
36+
if cmd =~ passwordPattern:
37+
password = matches[0]
38+
39+
if username != "" and password != "":
40+
return (username, password)
41+
42+
return ("", "")
43+
44+
method analyze*(self: ExtractCredentialsCmd, x: HayabusaJson) =
45+
var singleResultTable = initTable[string, string]()
46+
singleResultTable["Timestamp"] = x.Timestamp
47+
singleResultTable["Computer"] = x.Computer
48+
singleResultTable["Event"] = x.Channel & "-" & $(x.EventID)
49+
let cmd = x.Details["Cmdline"].getStr()
50+
let (user, pass) = extractUserPass(cmd)
51+
if user == "" and pass == "":
52+
return # skip if no user or password found
53+
singleResultTable["User"] = user
54+
singleResultTable["Password"] = pass
55+
singleResultTable["Cmdline"] = cmd
56+
self.seqOfResultsTables.add(singleResultTable)
57+
58+
method resultOutput*(self: ExtractCredentialsCmd) =
59+
var savedFiles = "n/a"
60+
var results = "n/a"
61+
let header = ["Timestamp", "Computer", "Event", "User", "Password", "Cmdline"]
62+
if self.output != "":
63+
# Open file to save results
64+
var outputFile = open(self.output, fmWrite)
65+
66+
## Write CSV header
67+
outputFile.write(header.join(",") & "\p")
68+
69+
## Write contents
70+
for table in self.seqOfResultsTables:
71+
for i, key in enumerate(header):
72+
if table.hasKey(key):
73+
if i < header.len() - 1:
74+
outputFile.write(escapeCsvField(table[key]) & ",")
75+
else:
76+
outputFile.write(escapeCsvField(table[key]))
77+
else:
78+
outputFile.write(",")
79+
outputFile.write("\p")
80+
outputFile.close()
81+
let fileSize = getFileSize(self.output)
82+
savedFiles = self.output & " (" & formatFileSize(fileSize) & ")"
83+
results = "Events: " & intToStr(self.seqOfResultsTables.len).insertSep(',')
84+
if self.displayTable:
85+
echo ""
86+
echo "Saved results to " & savedFiles
87+
else:
88+
var table: TerminalTable
89+
table.add header
90+
for t in self.seqOfResultsTables:
91+
table.add t[header[0]], t[header[1]], t[header[2]], t[header[3]], t[header[4]], t[header[5]]
92+
if self.displayTable:
93+
echo ""
94+
table.echoTableSepsWithStyled(seps = boxSeps)
95+
echo ""
96+
self.cmdResult = CmdResult(results: results, savedFiles: savedFiles)
97+
98+
proc extractCredentials(output: string = "", quiet: bool = false, skipProgressBar: bool = false, timeline: string) =
99+
checkArgs(quiet, timeline, "informational")
100+
var filePaths = getTargetExtFileLists(timeline, ".jsonl", true)
101+
for timelinePath in filePaths:
102+
let cmd = ExtractCredentialsCmd(
103+
timeline: timelinePath,
104+
skipProgressBar: skipProgressBar,
105+
output: output,
106+
name: "extract-credentials",
107+
msg: ExtractCredentialsMsg)
108+
cmd.analyzeJSONLFile()

src/takajopkg/takajoCore.nim

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ proc analyzeJSONLFile*(self: AbstractCmd, cmds: seq[AbstractCmd] = newSeq[
6161
try:
6262
if cmd.filter(jsonLine):
6363
cmd.analyze(jsonLine)
64-
except CatchableError:
64+
except CatchableError as e:
65+
echo "An error occurred: ", e.msg
6566
continue
6667

6768
if not self.skipProgressBar:
@@ -81,4 +82,4 @@ proc analyzeJSONLFile*(self: AbstractCmd, cmds: seq[AbstractCmd] = newSeq[
8182
echo ""
8283
table.echoTableSepsWithStyled(seps = boxSeps)
8384

84-
outputElapsedTime(startTime)
85+
outputElapsedTime(startTime)

0 commit comments

Comments
 (0)