// HackTheBox  ·  writeup

WordpressWebForensics

HackTheBox Very Easy Splunk
DFIR

Scenario

Allow 1-2 minutes after spawning for the services to start and then connect to the SIEM. WordPress web server logs ingested into Splunk.

Executive Summary

One external host scanned a WordPress site with WPScan, then switched to the login form and brute-forced it until a request finally returned a redirect, which marks a successful login. With valid credentials the attacker opened the built-in theme editor and modified a PHP file in the active default theme, turning it into a web shell. Minutes later the same address began sending POST requests to that file a few seconds apart, the rhythm of an operator typing commands, using an ordinary desktop browser user-agent rather than a scanner string. Everything is visible in a single access_combined source in Splunk.

Artifacts

Timestamp note: Splunk shows _time as ingested. Answers use the YYYY-MM-DD hh:mm:ss form straight from strftime or the Time column, with no timezone conversion.

Reconnaissance

Q1 — What is the attacker’s IP address from which the WPScan enumeration originated?

Start with a census of every client and user-agent, ordered by first appearance. One address carries a WPScan v3.8.25 user-agent, which is the scanner’s default header. That is the enumeration source.

index=main sourcetype=access_combined | stats count min(_time) as first by clientip, useragent | sort first
Click flag to reveal 203.101.190.116
Q3 — What is the name and version of the tool used by the attacker?

Pivot on the Q1 address and group by user-agent. The scanner announces itself in the useragent field, with name and version.

index=main clientip=203.101.190.116 | stats count min(_time) as f max(_time) as l by useragent | eval f=strftime(f,"%F %T"), l=strftime(l,"%F %T")
Click flag to reveal WPScan v3.8.25
Q2 — When did the attacker begin reconnaissance activity?

The same grouped output gives the first and last time per user-agent. The first request carrying the WPScan header from Q3 is the start of the scan.

index=main wpscan | head 3 | table _time clientip useragent

Gotcha: The same IP also made an earlier, benign-looking request with a normal Firefox user-agent (03:09:59). That is a manual browse, not scanner traffic. Reconnaissance starts at the first WPScan request.

Click to reveal answer 2024-08-28 03:15:23

Credential Access

Q4 — The attacker performed a brute-force attack on the WordPress login form. How many seconds did this activity last for before successfully obtaining valid credentials?

Filter the attacker to POSTs against wp-login.php and group by status. A failed WordPress login re-renders the form with a 200, while a successful one answers with a 302 redirect. The burst starts at 03:50:33 and the first 302 arrives at 03:50:40, so the brute force lasted the difference between the two.

index=main clientip=203.101.190.116 method=POST uri_path="*wp-login.php" | stats count min(_time) as f max(_time) as l by status | eval f=strftime(f,"%F %T"), l=strftime(l,"%F %T")

To see the transition request by request, list the login traffic in time order.

index=main clientip=203.101.190.116 "wp-login" | sort _time | table _time method status uri_path bytes
Click to reveal answer 7

Persistence

Q5 — The attacker manipulated a WordPress theme file to maintain persistent access by uploading a web shell. Which WordPress theme was targeted?

After the login succeeds, look for the admin theme editor. Requests to theme-editor from the attacker address show the edit, and the POSTs that follow target files under one theme directory.

index=main uri="*theme-editor*" clientip=203.101.190.116 | sort _time | table _time method uri status
Click flag to reveal twentytwentyfour
Q6 — What is the name of the PHP theme file the attacker edited and used as a web shell entry point?

From Q5 the theme is known. Within its directory the attacker’s POSTs after the edit all land on one file under patterns/. A theme pattern template that normally gets only GETs, now receiving POSTs, is the web shell.

index=main uri_path="*hidden-404.php*" | sort _time | table _time clientip method status bytes
Click to reveal answer hidden-404.php

Execution

Q7 — We see few POST requests to the suspected PHP file that the attacker supposedly edited. The evidence suggests that this is a webshell because the access to the URI is done with few seconds between each attempt, which could be the attacker running commands. When did the attacker first run a command? Provide an answer based on the patterns in the log.

Sort the requests to the Q6 file by time. A GET at 04:03:46 comes first, which is the attacker loading the file to confirm it responds. The first POST, at 04:03:51, is the first command sent to the shell, and later POSTs follow a few seconds apart.

index=main uri_path="*hidden-404.php*" | sort _time | table _time clientip method status bytes

Gotcha: The earlier GET is not a command. The question asks for the first POST in the pattern.

Click flag to reveal 2024-08-28 04:03:51
Q8 — List the user-agent used to access the webshell.

Restrict the attacker’s traffic to browser-style user-agents and POST requests, and read the useragent on the web shell hits. It is a desktop Firefox string, so the operator drove the shell from a browser, not the scanner.

index=main clientip=203.101.190.116 useragent="Mozilla*" method=POST | sort _time | table _time method uri status useragent

Tip: The format hint is the full header, not just the browser name and version. Copy the whole string from the useragent field.

Click to reveal answer Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0

Attack Summary

PhaseAction
ReconnaissanceManual browse, then a WPScan v3.8.25 sweep from one external IP
Credential AccessBurst of POSTs to wp-login.php ends in a 302 about seven seconds later
PersistenceTheme editor used to modify a PHP file in the twentytwentyfour theme
ExecutionPOSTs to hidden-404.php a few seconds apart, from a Firefox user-agent

Forensic Timeline

TimestampEvent
2024-08-28 03:09:59First request from the attacker IP, a normal Firefox browse
2024-08-28 03:15:23First request with the WPScan user-agent
2024-08-28 03:50:33Login brute force begins against wp-login.php
2024-08-28 03:50:40First 302 on wp-login.php, valid credentials obtained
2024-08-28 04:03:46First GET to hidden-404.php
2024-08-28 04:03:51First POST to hidden-404.php, first command run

IOCs

TypeValue
IP203.101.190[.]116
User-agent (scanner)WPScan v3.8.25
User-agent (web shell)Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0
File (web shell)patterns/hidden-404.php in the twentytwentyfour theme

MITRE ATT&CK

TechniqueIDDescription
Active Scanning: Vulnerability ScanningT1595.002WPScan enumeration of the WordPress site
Brute Force: Password GuessingT1110.001Rapid POSTs to wp-login.php until a 302
Valid AccountsT1078Authenticated session used to reach the theme editor
Server Software Component: Web ShellT1505.003Theme PHP file edited into a shell and driven with POSTs

Defender Takeaways

A login brute force that succeeds in seconds means there was no throttling. A run of failed wp-login.php POSTs ended in a valid login about seven seconds after it started. Rate limiting, a lockout plugin or MFA on administrator accounts would have broken the chain here. Alert on a burst of 200s from one IP followed by a 302.

The built-in theme editor turns one stolen admin login into code execution. The attacker never uploaded a file; they edited an existing theme file through theme-editor. Setting DISALLOW_FILE_EDIT in wp-config.php removes that path, and any request to theme-editor from an external address is worth an alert.

Scanner strings are free detection, and the attacker dropped them as soon as it mattered. The WPScan header was visible from the first request and could have been blocked at the edge. The shell phase used an ordinary Firefox string, so detection there has to rely on behaviour: POSTs to a theme template that is normally only fetched with GET, repeating every few seconds.

Watch for file-integrity changes in theme directories. A template under patterns/ started answering POSTs minutes after the editor request. File integrity monitoring on wp-content/themes would flag the modification at the moment of the edit.

Conclusion

The attacker went from a scanner banner to a working web shell in under an hour, using only a guessable login, the stock theme editor and a browser. The decisive detail is the pattern, not the content: failed logins into a redirect, an editor request, then rhythmic POSTs to a file that should never take them.