import re import requests import base64 import os from pathlib import Path from .peasLoaded import PEASLoaded from .peassRecord import PEASRecord from .fileRecord import FileRecord from .yamlGlobals import ( TEMPORARY_LINPEAS_BASE_PATH, PEAS_FINDS_MARKUP, PEAS_FINDS_CUSTOM_MARKUP, PEAS_STORAGES_MARKUP, INT_HIDDEN_FILES_MARKUP, ROOT_FOLDER, STORAGE_TEMPLATE, FIND_TEMPLATE, FIND_LINE_MARKUP, STORAGE_LINE_MARKUP, STORAGE_LINE_EXTRA_MARKUP, EXTRASECTIONS_MARKUP, PEAS_VARIABLES_MARKUP, YAML_VARIABLES, SUIDVB1_MARKUP, SUIDVB2_MARKUP, SUDOVB1_MARKUP, SUDOVB2_MARKUP, CAP_SETUID_MARKUP, CAP_SETGID_MARKUP, LES_MARKUP, LES2_MARKUP, REGEXES_LOADED, REGEXES_MARKUP, FAT_LINPEAS_AMICONTAINED_MARKUP, FAT_LINPEAS_GITLEAKS_LINUX_MARKUP, FAT_LINPEAS_GITLEAKS_MACOS_MARKUP ) class LinpeasBuilder: def __init__(self, ploaded:PEASLoaded): self.ploaded = ploaded self.hidden_files = set() self.bash_find_f_vars, self.bash_find_d_vars = set(), set() self.bash_storages = set() self.__get_files_to_search() with open(TEMPORARY_LINPEAS_BASE_PATH, 'r') as file: self.linpeas_sh = file.read() def build(self): print("[+] Building variables...") variables = self.__generate_variables() self.__replace_mark(PEAS_VARIABLES_MARKUP, variables, "") print("[+] Building finds...") find_calls, find_custom_calls = self.__generate_finds() self.__replace_mark(PEAS_FINDS_MARKUP, find_calls, " ") self.__replace_mark(PEAS_FINDS_CUSTOM_MARKUP, find_custom_calls, " ") print("[+] Building storages...") storage_vars = self.__generate_storages() self.__replace_mark(PEAS_STORAGES_MARKUP, storage_vars, " ") #Check all the expected STORAGES in linpeas have been created for s in re.findall(r'PSTORAGE_[\w]*', self.linpeas_sh): assert s in self.bash_storages, f"{s} isn't created" #Replace interesting hidden files markup for a list of all the searched hidden files self.__replace_mark(INT_HIDDEN_FILES_MARKUP, sorted(self.hidden_files), "|") print("[+] Checking duplicates...") peass_marks = self.__get_peass_marks() for i,mark in enumerate(peass_marks): for j in range(i+1,len(peass_marks)): assert mark != peass_marks[j], f"Found repeated peass mark: {mark}" print("[+] Building autocheck sections...") sections = self.__generate_sections() for section_name, bash_lines in sections.items(): mark = "peass{"+section_name+"}" if mark in peass_marks: self.__replace_mark(mark, list(bash_lines), "") else: self.__replace_mark(EXTRASECTIONS_MARKUP, [bash_lines, EXTRASECTIONS_MARKUP], "\n\n") self.__replace_mark(EXTRASECTIONS_MARKUP, list(""), "") #Delete extra markup print("[+] Building regexes searches...") section = self.__generate_regexes_search() self.__replace_mark(REGEXES_MARKUP, list(section), "") print("[+] Building linux exploit suggesters...") les_b64, les2_b64 = self.__get_linux_exploit_suggesters() assert len(les_b64) > 100 assert len(les2_b64) > 100 self.__replace_mark(LES_MARKUP, list(les_b64), "") self.__replace_mark(LES2_MARKUP, list(les2_b64), "") print("[+] Downloading Fat Linpeas binaries...") aimcont_b64 = self.__get_bin("https://github.com/genuinetools/amicontained/releases/latest/download/amicontained-linux-amd64") self.__replace_mark(FAT_LINPEAS_AMICONTAINED_MARKUP, list(aimcont_b64), "") gitleaks_b64 = self.__get_bin("https://github.com/zricethezav/gitleaks/releases/download/v8.8.7/gitleaks_8.8.7_linux_x64.tar.gz", tar_gz="gitleaks") self.__replace_mark(FAT_LINPEAS_GITLEAKS_LINUX_MARKUP, list(gitleaks_b64), "") gitleaks_b64_macos = self.__get_bin("https://github.com/zricethezav/gitleaks/releases/download/v8.8.7/gitleaks_8.8.7_darwin_x64.tar.gz", tar_gz="gitleaks") self.__replace_mark(FAT_LINPEAS_GITLEAKS_MACOS_MARKUP, list(gitleaks_b64_macos), "") print("[+] Building GTFOBins lists...") suidVB, sudoVB, capsVB = self.__get_gtfobins_lists() assert len(suidVB) > 185, f"Len suidVB is {len(suidVB)}" assert len(sudoVB) > 250, f"Len sudo is {len(sudoVB)}" assert len(capsVB) > 10, f"Len suidVB is {len(capsVB)}" self.__replace_mark(SUIDVB1_MARKUP, suidVB[:int(len(suidVB)/2)], "|") self.__replace_mark(SUIDVB2_MARKUP, suidVB[int(len(suidVB)/2):], "|") self.__replace_mark(SUDOVB1_MARKUP, sudoVB[:int(len(sudoVB)/2)], "|") self.__replace_mark(SUDOVB2_MARKUP, sudoVB[int(len(sudoVB)/2):], "|") self.__replace_mark(CAP_SETUID_MARKUP, capsVB, "|") self.__replace_mark(CAP_SETGID_MARKUP, capsVB, "|") print("[+] Final sanity checks...") #Check that there arent peass marks left in linpeas peass_marks = self.__get_peass_marks() assert len(peass_marks) == 0, f"There are peass marks left: {', '.join(peass_marks)}" #Check for empty seds assert 'sed -${E} "s,,' not in self.linpeas_sh def __get_peass_marks(self): return re.findall(r'peass\{[\w\-\._ ]*\}', self.linpeas_sh) def __generate_variables(self): """Generate the variables from the yaml to set into linpeas bash script""" variables_bash = "" for var in YAML_VARIABLES: variables_bash += f"{var['name']}=\"{var['value']}\"\n" return variables_bash def __get_files_to_search(self): """Given a PEASLoaded and find the files that need to be searched on each root folder""" self.dict_to_search = {"d": {}, "f": {}} self.dict_to_search["d"] = {r: set() for r in ROOT_FOLDER} self.dict_to_search["f"] = {r: set() for r in ROOT_FOLDER} for precord in self.ploaded.peasrecords: for frecord in precord.filerecords: for folder in frecord.search_in: self.dict_to_search[frecord.type][folder].add(frecord.regex) if frecord.regex[0] == "." or frecord.regex[:2] == "*.": self.hidden_files.add(frecord.regex.replace("*","")) def __generate_finds(self) -> list: """Given the regexes to search on each root folder, generate the find command""" finds = [] finds_custom = [] all_folder_regexes = [] all_file_regexes = [] for type,searches in self.dict_to_search.items(): for r,regexes in searches.items(): if regexes: find_line = f"{r} " if type == "d": find_line += "-type d " bash_find_var = f"FIND_DIR_{r[1:].replace('.','').replace('-','_').replace('{ROOT_FOLDER}','').upper()}" self.bash_find_d_vars.add(bash_find_var) all_folder_regexes += regexes else: bash_find_var = f"FIND_{r[1:].replace('.','').replace('-','_').replace('{ROOT_FOLDER}','').upper()}" self.bash_find_f_vars.add(bash_find_var) all_file_regexes += regexes find_line += '-name \\"' + '\\" -o -name \\"'.join(regexes) + '\\"' find_line = FIND_TEMPLATE.replace(FIND_LINE_MARKUP, find_line) find_line = f"{bash_find_var}={find_line}" finds.append(find_line) # Buid folder and files finds when searching in a custom folder all_folder_regexes = list(set(all_folder_regexes)) find_line = '$SEARCH_IN_FOLDER -type d -name \\"' + '\\" -o -name \\"'.join(all_folder_regexes) + '\\"' find_line = FIND_TEMPLATE.replace(FIND_LINE_MARKUP, find_line) find_line = f"FIND_DIR_CUSTOM={find_line}" finds_custom.append(find_line) all_file_regexes = list(set(all_file_regexes)) find_line = '$SEARCH_IN_FOLDER -name \\"' + '\\" -o -name \\"'.join(all_file_regexes) + '\\"' find_line = FIND_TEMPLATE.replace(FIND_LINE_MARKUP, find_line) find_line = f"FIND_CUSTOM={find_line}" finds_custom.append(find_line) return finds, finds_custom def __generate_storages(self) -> list: """Generate the storages to save the results per entry""" storages = [] custom_storages = ["FIND_CUSTOM", "FIND_DIR_CUSTOM"] all_f_finds = "$" + "\\n$".join(list(self.bash_find_f_vars) + custom_storages) all_d_finds = "$" + "\\n$".join(list(self.bash_find_d_vars) + custom_storages) all_finds = "$" + "\\n$".join(list(self.bash_find_f_vars) + list(self.bash_find_d_vars) + custom_storages) for precord in self.ploaded.peasrecords: bash_storage_var = f"PSTORAGE_{precord.bash_name}" self.bash_storages.add(bash_storage_var) #Select the FIND_ variables to search on depending on the type files if all(frecord.type == "f" for frecord in precord.filerecords): storage_line = STORAGE_TEMPLATE.replace(STORAGE_LINE_MARKUP, all_f_finds) elif all(frecord.type == "d" for frecord in precord.filerecords): storage_line = STORAGE_TEMPLATE.replace(STORAGE_LINE_MARKUP, all_d_finds) else: storage_line = STORAGE_TEMPLATE.replace(STORAGE_LINE_MARKUP, all_finds) #Grep by filename regex (ended in '$') bsp = '\\.' #A 'f' expression cannot contain a backslash, so we generate here the bs need in the line below grep_names = f" | grep -E \"{'|'.join([frecord.regex.replace('.',bsp).replace('*', '.*')+'$' for frecord in precord.filerecords])}\"" #Grep by searched folders grep_folders_searched = f" | grep -E \"^{'|^'.join(list(set([d for frecord in precord.filerecords for d in frecord.search_in])))}\"".replace("HOMESEARCH","GREPHOMESEARCH") #Grep extra paths. They are accumulative between files of the same PEASRecord grep_extra_paths = "" if any(True for frecord in precord.filerecords if frecord.check_extra_path): grep_extra_paths = f" | grep -E '{'|'.join(list(set([frecord.check_extra_path for frecord in precord.filerecords if frecord.check_extra_path])))}'" #Grep to remove paths. They are accumulative between files of the same PEASRecord grep_remove_path = "" if any(True for frecord in precord.filerecords if frecord.remove_path): grep_remove_path = f" | grep -v -E '{'|'.join(list(set([frecord.remove_path for frecord in precord.filerecords if frecord.remove_path])))}'" #Construct the final line like: STORAGE_MYSQL=$(echo "$FIND_DIR_ETC\n$FIND_DIR_USR\n$FIND_DIR_VAR\n$FIND_DIR_MNT" | grep -E '^/etc/.*mysql|/usr/var/lib/.*mysql|/var/lib/.*mysql' | grep -v "mysql/mysql") storage_line = storage_line.replace(STORAGE_LINE_EXTRA_MARKUP, f"{grep_remove_path}{grep_extra_paths}{grep_folders_searched}{grep_names}") storage_line = f"{bash_storage_var}={storage_line}" storages.append(storage_line) return storages def __generate_sections(self) -> dict: """Generate sections for records with auto_check to True""" sections = {} for precord in self.ploaded.peasrecords: if precord.auto_check: section = f'if [ "$PSTORAGE_{precord.bash_name}" ] || [ "$DEBUG" ]; then\n' section += f' print_2title "Analyzing {precord.name.replace("_"," ")} Files (limit 70)"\n' for exec_line in precord.exec: if exec_line: section += " " + exec_line + "\n" for frecord in precord.filerecords: section += " " + self.__construct_file_line(precord, frecord) + "\n" section += "fi\n" sections[precord.name] = section return sections def __construct_file_line(self, precord: PEASRecord, frecord: FileRecord, init: bool = True) -> str: real_regex = frecord.regex[1:] if frecord.regex.startswith("*") and len(frecord.regex) > 1 else frecord.regex real_regex = real_regex.replace(".","\\.").replace("*",".*") real_regex += "$" analise_line = "" if init: analise_line = 'if ! [ "`echo \\\"$PSTORAGE_'+precord.bash_name+'\\\" | grep -E \\\"'+real_regex+'\\\"`" ]; then if [ "$DEBUG" ]; then echo_not_found "'+frecord.regex+'"; fi; fi; ' analise_line += 'printf "%s" "$PSTORAGE_'+precord.bash_name+'" | grep -E "'+real_regex+'" | while read f; do ls -ld "$f" 2>/dev/null | sed -${E} "s,'+real_regex+',${SED_RED},"; ' #If just list, just list the file/directory if frecord.just_list_file: if frecord.type == "d": analise_line += 'ls -lRA "$f";' analise_line += 'done; echo "";' return analise_line if frecord.type == "f": grep_empty_lines = ' | grep -IEv "^$"' grep_line_grep = f' | grep -E {frecord.line_grep}' if frecord.line_grep else "" grep_only_bad_lines = f' | grep -E "{frecord.bad_regex}"' if frecord.bad_regex else "" grep_remove_regex = f' | grep -Ev "{frecord.remove_regex}"' if frecord.remove_regex else "" sed_bad_regex = ' | sed -${E} "s,'+frecord.bad_regex+',${SED_RED},g"' if frecord.bad_regex else "" sed_very_bad_regex = ' | sed -${E} "s,'+frecord.very_bad_regex+',${SED_RED_YELLOW},g"' if frecord.very_bad_regex else "" sed_good_regex = ' | sed -${E} "s,'+frecord.good_regex+',${SED_GOOD},g"' if frecord.good_regex else "" if init: analise_line += 'cat "$f" 2>/dev/null' else: analise_line += 'cat "$ff" 2>/dev/null' if grep_empty_lines: analise_line += grep_empty_lines if grep_line_grep: analise_line += grep_line_grep if frecord.only_bad_lines and not grep_line_grep: analise_line += grep_only_bad_lines if grep_remove_regex: analise_line += grep_remove_regex if sed_bad_regex: analise_line += sed_bad_regex if sed_very_bad_regex: analise_line += sed_very_bad_regex if sed_good_regex: analise_line += sed_good_regex analise_line += '; done; echo "";' return analise_line #In case file is type "d" if frecord.files: for ffrecord in frecord.files: ff_real_regex = ffrecord.regex[1:] if ffrecord.regex.startswith("*") and ffrecord.regex != "*" else ffrecord.regex ff_real_regex = ff_real_regex.replace("*",".*") #analise_line += 'for ff in $(find "$f" -name "'+ffrecord.regex+'"); do ls -ld "$ff" | sed -${E} "s,'+ff_real_regex+',${SED_RED},"; ' + self.__construct_file_line(precord, ffrecord, init=False) analise_line += 'find "$f" -name "'+ffrecord.regex+'" | while read ff; do ls -ld "$ff" | sed -${E} "s,'+ff_real_regex+',${SED_RED},"; ' + self.__construct_file_line(precord, ffrecord, init=False) analise_line += 'done; echo "";' return analise_line def __get_linux_exploit_suggesters(self) -> tuple: r1 = requests.get("https://raw.githubusercontent.com/mzet-/linux-exploit-suggester/master/linux-exploit-suggester.sh") r2 = requests.get("https://raw.githubusercontent.com/jondonas/linux-exploit-suggester-2/master/linux-exploit-suggester-2.pl") return(base64.b64encode(bytes(r1.text, 'utf-8')).decode("utf-8"), base64.b64encode(bytes(r2.text, 'utf-8')).decode("utf-8")) def __get_bin(self, url, tar_gz="") -> str: os.system(f"wget -q '{url}' -O /tmp/bin_builder") if tar_gz: os.system(f"cd /tmp; tar -xvzf /tmp/bin_builder; rm /tmp/bin_builder; mv {tar_gz} /tmp/bin_builder") os.system("base64 /tmp/bin_builder | tr -d '\n' > /tmp/binb64; rm /tmp/bin_builder") b64bin = "" with open("/tmp/binb64", "r") as f: b64bin = f.read() os.system("rm /tmp/binb64") return b64bin def __get_gtfobins_lists(self) -> tuple: r = requests.get("https://github.com/GTFOBins/GTFOBins.github.io/tree/master/_gtfobins") bins = re.findall(r'_gtfobins/([\w_ \-]+).md', r.text) sudoVB = [] suidVB = [] capsVB = [] for b in bins: rb = requests.get(f"https://raw.githubusercontent.com/GTFOBins/GTFOBins.github.io/master/_gtfobins/{b}.md") if "sudo:" in rb.text: sudoVB.append(b+"$") if "suid:" in rb.text: suidVB.append("/"+b+"$") if "capabilities:" in rb.text: capsVB.append(b) return (suidVB, sudoVB, capsVB) def __generate_regexes_search(self) -> str: regexes = REGEXES_LOADED["regular_expresions"] regexes_search_section = "" for values in regexes: section_name = values["name"] regexes_search_section += f' print_2title "Searching {section_name}"\n' for entry in values["regexes"]: name = entry["name"] caseinsensitive = entry.get("caseinsensitive", False) regex = entry["regex"] regex = regex.replace('"', '\\"').strip() falsePositives = entry.get("falsePositives", False) if falsePositives: continue regexes_search_section += f" search_for_regex \"{name}\" \"{regex}\" {'1' if caseinsensitive else ''}\n" regexes_search_section += " echo ''\n\n" return regexes_search_section def __replace_mark(self, mark: str, find_calls: list, join_char: str): """Substitude the markup with the actual code""" self.linpeas_sh = self.linpeas_sh.replace(mark, join_char.join(find_calls)) #New line char is't needed def write_linpeas(self, path, rm_startswith=""): """Write on disk the final linpeas""" with open(path, "w") as f: if not rm_startswith: f.write(self.linpeas_sh) else: tmp_linpeas = "" for line in self.linpeas_sh.splitlines(): if not line.startswith(rm_startswith): tmp_linpeas += line + "\n" f.write(tmp_linpeas)