255 lines
12 KiB
Python
255 lines
12 KiB
Python
|
'''
|
||
|
Executes an arbitrary github gist on GCloudShell in ephimeral mode, no traces left in the docker containers
|
||
|
Every restart of the Google Cloud Shell the machine appears to be a completely different one (ip, mac)
|
||
|
Specs:
|
||
|
2x vCPU Intel Xeon @ 2.6 GHz
|
||
|
1GB Ram
|
||
|
|
||
|
'''
|
||
|
# text.get_attribute("value") return the text content of a text field
|
||
|
|
||
|
from qwiklabs.get_google_account import get_google_account
|
||
|
from utils import global_vars, browser_manager
|
||
|
|
||
|
import random
|
||
|
import time
|
||
|
|
||
|
from selenium.webdriver.common.by import By
|
||
|
from selenium.webdriver.common.keys import Keys
|
||
|
from selenium import webdriver
|
||
|
|
||
|
class Shell():
|
||
|
def __init__(self):
|
||
|
print("[GCShell] Created a new GShell instance ")
|
||
|
Shell.TEXT_AREA_XPATH = '/html/body/cloud-shell-root/div/docked/stacked-layout/div[1]/div/devshell/terminal-container/div/xterm-terminal-tab/div/xterm-terminal/div/div/div/div[2]/div[1]/textarea'
|
||
|
Shell.XTERM_SCREEN_XPATH = '/html/body/cloud-shell-root/div/docked/stacked-layout/div[1]/devshell/terminal-container/div/xterm-terminal-tab/div/xterm-terminal/div/div/div/div[2]'
|
||
|
|
||
|
self.account_specs = get_google_account()
|
||
|
|
||
|
self.driver = self.account_specs[3]
|
||
|
self.account = (self.account_specs[0], self.account_specs[1])
|
||
|
self.gcs_link = self.account_specs[2]
|
||
|
|
||
|
self.started = False
|
||
|
|
||
|
def start(self):
|
||
|
if self.account_specs is False:
|
||
|
print("[GCShell] No account was available, can't start!")
|
||
|
return
|
||
|
|
||
|
# If using a test account, use the provided gsc link
|
||
|
if global_vars.TEST_ACCOUNT is False:
|
||
|
while (browser_manager.waitForElement(self.driver, By.XPATH, '/html/body/main/div/ql-drawer-container/ql-drawer-content/ql-drawer-container/ql-drawer[1]/ql-lab-control-panel') is False):
|
||
|
print("[GCShell] Page not ready, waiting...")
|
||
|
|
||
|
# get the shadow_dom element and click it
|
||
|
panel = self.driver.find_element_by_xpath('/html/body/main/div/ql-drawer-container/ql-drawer-content/ql-drawer-container/ql-drawer[1]/ql-lab-control-panel')
|
||
|
shadow1 = browser_manager.expand_shadow_element(self.driver, panel)
|
||
|
open_shell = shadow1.find_element_by_css_selector('div:nth-child(2) > div > ql-button')
|
||
|
print("[GCShell] Opening shell")
|
||
|
open_shell.click()
|
||
|
|
||
|
time.sleep(5)
|
||
|
|
||
|
# Now switch to the second tab
|
||
|
self.driver.switch_to.window(self.driver.window_handles[1]) #apparently clicking in selenium doesn't switch window
|
||
|
else:
|
||
|
print("[GCShell] Using Google Test Account")
|
||
|
self.driver.get(self.gcs_link)
|
||
|
|
||
|
time.sleep(5)
|
||
|
|
||
|
oldPage = False
|
||
|
#The email may be already filled in, just in case delete it and type it again
|
||
|
while browser_manager.waitForElement(self.driver, By.CSS_SELECTOR, '#identifierId') is False:
|
||
|
if browser_manager.waitForElement(self.driver, By.CSS_SELECTOR, '#Email') is not False:
|
||
|
oldPage = True
|
||
|
break
|
||
|
|
||
|
print("[GCShell] Page not ready, waiting...")
|
||
|
time.sleep(5)
|
||
|
|
||
|
# print("Got old page?", oldPage)
|
||
|
|
||
|
print("[GCShell] Inserting credentials")
|
||
|
if oldPage:
|
||
|
email = self.driver.find_element_by_css_selector('#Email')
|
||
|
email.send_keys(Keys.CONTROL, 'a')
|
||
|
email.send_keys(Keys.BACKSPACE)
|
||
|
browser_manager.inputText(self.driver, By.CSS_SELECTOR, '#Email', self.account[0])
|
||
|
browser_manager.clickButton(self.driver, By.CSS_SELECTOR, '#next')
|
||
|
|
||
|
time.sleep(5)
|
||
|
|
||
|
#password is always empty, no need to empty the textbox first
|
||
|
browser_manager.inputText(self.driver, By.CSS_SELECTOR, '#password', self.account[1])
|
||
|
browser_manager.clickButton(self.driver, By.CSS_SELECTOR, '#submit')
|
||
|
else:
|
||
|
email = self.driver.find_element_by_css_selector('#identifierId')
|
||
|
email.send_keys(Keys.CONTROL, 'a')
|
||
|
email.send_keys(Keys.BACKSPACE)
|
||
|
browser_manager.inputText(self.driver, By.CSS_SELECTOR, '#identifierId', self.account[0])
|
||
|
browser_manager.clickButton(self.driver, By.XPATH, '/html/body/div[1]/div[1]/div[2]/div/div[2]/div/div/div[2]/div/div[2]/div/div[1]/div/div/button')
|
||
|
|
||
|
time.sleep(5)
|
||
|
|
||
|
#password is always empty, no need to empty the textbox first
|
||
|
browser_manager.inputText(self.driver, By.XPATH, '//*[@id="password"]/div[1]/div/div[1]/input', self.account[1])
|
||
|
browser_manager.clickButton(self.driver, By.XPATH, '/html/body/div[1]/div[1]/div[2]/div/div[2]/div/div/div[2]/div/div[2]/div/div[1]/div/div/button')
|
||
|
|
||
|
# Login Done: Accept the EULA
|
||
|
browser_manager.clickButton(self.driver, By.XPATH, '//*[@id="accept"]')
|
||
|
|
||
|
time.sleep(5)
|
||
|
|
||
|
# Now we are in GCP. We need to accept some stuff (country, terms of use)
|
||
|
# The form accepts keyboard input and anticipates it proposing a country. Make up a random string, send it. It will be out country
|
||
|
|
||
|
# Select country button
|
||
|
#there's no country starting with an 'x'
|
||
|
alphabet = 'abcdefghijklmnopqrstuwyz'
|
||
|
letters = 3
|
||
|
country = ""
|
||
|
for i in range(0, letters):
|
||
|
country += alphabet[random.randint(0, len(alphabet)-1)]
|
||
|
#country += '\n'
|
||
|
# print(country)
|
||
|
|
||
|
browser_manager.inputText(self.driver, By.XPATH, '/html/body/div[3]/div[3]/div/mat-dialog-container/xap-deferred-loader-outlet/ng-component/mat-dialog-content/form/cfc-tos-checkboxes/form/div[1]/cfc-loader/div/mat-form-field/div/div[1]/div[3]/ace-select', country, interval=0, after_delay=2)
|
||
|
|
||
|
#accept terms of use
|
||
|
browser_manager.clickButton(self.driver, By.XPATH, '/html/body/div[3]/div[3]/div/mat-dialog-container/xap-deferred-loader-outlet/ng-component/mat-dialog-content/form/cfc-tos-checkboxes/form/div[2]/mat-checkbox/label/span[1]')
|
||
|
#finally agree
|
||
|
browser_manager.clickButton(self.driver, By.XPATH, '/html/body/div[3]/div[3]/div/mat-dialog-container/xap-deferred-loader-outlet/ng-component/mat-dialog-actions/ace-progress-button/div[1]/button')
|
||
|
#wait for the page to load
|
||
|
time.sleep(20)
|
||
|
|
||
|
# Now open GCS, clicking the button
|
||
|
browser_manager.clickButton(self.driver, By.XPATH, '//*[@id="pcc-devshell-container"]/xap-deferred-loader-outlet/pcc-platform-bar-devshell-button/pcc-platform-bar-button/button')
|
||
|
time.sleep(10)
|
||
|
|
||
|
# Sometimes another accept pops out
|
||
|
browser_manager.clickButton(self.driver, By.XPATH, '/html/body/div[3]/div[3]/div/mat-dialog-container/xap-deferred-loader-outlet/ng-component/mat-dialog-content/form/cfc-tos-checkboxes/form/div[1]/cfc-loader/div/mat-form-field/div/div[1]/div[3]/ace-select')
|
||
|
|
||
|
# Sometimes there's even an accept button inside the iframe, where the command area should be
|
||
|
self.switch_to_shell_iframe()
|
||
|
browser_manager.clickButton(self.driver, By.XPATH, '/html/body/div/div[2]/div/mat-dialog-container/dialog-overlay/div[5]/modal-action/button')
|
||
|
|
||
|
#wait for the machine to boot
|
||
|
time.sleep(20)
|
||
|
|
||
|
self.started = True
|
||
|
# Done. This is ours to command now!
|
||
|
|
||
|
# reboot to get a fresh machine (we are in ephimeral mode)
|
||
|
def reboot(self):
|
||
|
print("[GCShell] Going down for reboot NOW!")
|
||
|
self.switch_to_shell_iframe()
|
||
|
|
||
|
browser_manager.clickButton(self.driver, By.XPATH, '/html/body/cloud-shell-root/div/docked/stacked-layout/div[1]/devshell/devshell-toolbar/csh-header/mat-toolbar/csh-header-buttons/more-button/button')
|
||
|
browser_manager.clickButton(self.driver, By.XPATH, '/html/body/div[3]/div[2]/div/div/div/button[1]')
|
||
|
browser_manager.clickButton(self.driver, By.XPATH, '/html/body/div[3]/div[2]/div/mat-dialog-container/dialog-overlay/div[5]/modal-action[1]/button')
|
||
|
|
||
|
rebooted = False
|
||
|
t = 0
|
||
|
#wait for it to actually reboot
|
||
|
while not rebooted:
|
||
|
time.sleep(5)
|
||
|
t += 5
|
||
|
try:
|
||
|
self.get_text_area()
|
||
|
rebooted = True
|
||
|
except:
|
||
|
print("[GCShell]{} seconds passed, VM still hasn't fully rebooted".format(t))
|
||
|
|
||
|
print("[GCShell] VM fully rebooted")
|
||
|
|
||
|
def toggle_ephimeral(self):
|
||
|
print("[GCShell] Toggling ephimeral mode...")
|
||
|
#open menu
|
||
|
browser_manager.clickButton(self.driver, By.XPATH, '/html/body/cloud-shell-root/div/docked/stacked-layout/div[1]/devshell/devshell-toolbar/csh-header/mat-toolbar/csh-header-buttons/more-button/button')
|
||
|
#click ephimeral mode option
|
||
|
browser_manager.clickButton(self.driver, By.XPATH, '//*[@id="mat-menu-panel-10"]/div/button[6]')
|
||
|
|
||
|
#this only appears when enabling. Click to enable
|
||
|
browser_manager.clickButton(self.driver, By.XPATH, '//*[@id="mat-slide-toggle-1"]')
|
||
|
|
||
|
#This confirms enabling or disabling, depending if it's already enabled or not
|
||
|
browser_manager.clickButton(self.driver, By.XPATH, '//*[@id="mat-dialog-0"]/dialog-overlay/div[5]/modal-action[1]/button')
|
||
|
print("[GCShell] Toggled ephimeral mode...")
|
||
|
|
||
|
|
||
|
# select the text area. This is where we can input commands
|
||
|
# (assumes the window is still open)
|
||
|
def switch_to_shell_iframe(self):
|
||
|
#sometimes we're still in the iframe, so switching to main content is needed
|
||
|
self.driver.switch_to.default_content()
|
||
|
|
||
|
#switch to shell iframe
|
||
|
frame = self.driver.find_element_by_xpath('/html/body/pan-shell/pcc-shell/cfc-panel-container/div/div/cfc-panel[1]/div[1]/div/div[3]/cfc-panel-container/div/div/cfc-panel/div/div/cfc-panel-container/div/div/cfc-panel[2]/div/div[1]/pcc-cloud-shell-wrapper/xap-deferred-loader-outlet/pcc-cloud-shell/div/div[2]/iframe')
|
||
|
self.driver.switch_to.frame(frame)
|
||
|
return frame
|
||
|
|
||
|
def get_text_area(self):
|
||
|
if self.started:
|
||
|
self.switch_to_shell_iframe()
|
||
|
return self.driver.find_element_by_xpath(Shell.TEXT_AREA_XPATH)
|
||
|
|
||
|
return False
|
||
|
|
||
|
def execute_command(self, command, stop_current=False):
|
||
|
text_area = self.get_text_area()
|
||
|
|
||
|
if text_area is False:
|
||
|
print("[GCShell] No account were available, can't execute cmd!")
|
||
|
return
|
||
|
|
||
|
if stop_current:
|
||
|
text_area.send_keys(Keys.CONTROL, 'c')
|
||
|
time.sleep(5) #wait in case it takes a bit of time to stop
|
||
|
|
||
|
browser_manager.inputText_no_find(self.driver, text_area, command + '\n', interval=0)
|
||
|
|
||
|
def execute_python_custom_script(self, gist):
|
||
|
gist.execute(self)
|
||
|
|
||
|
# we use our own "language" to add information to the script (e.g. how much to wait before a command is complete)
|
||
|
def execute_script_from_local(self, gist_path):
|
||
|
print("[GCShell] Executing gist from", gist_path)
|
||
|
with open(gist_path, "r") as file:
|
||
|
for line in file.readlines():
|
||
|
line = line.strip().rstrip()
|
||
|
print("[GCShell] Executing specific line:", line)
|
||
|
|
||
|
# that's a comment, ignore by skipping to the next line
|
||
|
if( line.startswith("#") ):
|
||
|
print("[GCShell] Line was a comment, skipping")
|
||
|
else:
|
||
|
i1 = line.find('.')
|
||
|
i2 = line.find('.',1)
|
||
|
sleeptime = int(line[i1+1:i2])
|
||
|
|
||
|
command = line[i2+1:]
|
||
|
|
||
|
print("[GCShell] Executing command:", line, "for", sleeptime, "seconds")
|
||
|
|
||
|
self.execute_command(command, False)
|
||
|
time.sleep(sleeptime)
|
||
|
|
||
|
# take a raw text file from the url (github, pastebin possibly), run it using curl
|
||
|
def execute_raw_gist(self, gist_url, arguments):
|
||
|
arguments_str = ""
|
||
|
for a in arguments:
|
||
|
arguments_str += " "
|
||
|
arguments_str += a
|
||
|
command = gist_url + " " + arguments_str
|
||
|
|
||
|
self.execute_command(command, stop_current=True)
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|