# -*- coding: utf-8 -*- from __future__ import print_function import base64, os, socket, subprocess, tempfile, sys, binascii import re from datetime import datetime # ---- Compatibility for CentOS 6 (Py2.6/2.7) ---- # Safe print (UTF-8, hỗ trợ stream=file) def _p(msg, color=None, stream=None, **kwargs): # Back-compat cho calls kiểu file=... if stream is None and 'file' in kwargs: stream = kwargs['file'] if stream is None: stream = sys.stdout try: unicode # Py2 check b = msg.encode('utf-8') if isinstance(msg, unicode) else msg except NameError: b = msg try: if color: stream.write(color) stream.write(b) if color: try: stream.write(Fore.RESET) except Exception: pass stream.write("\n") except Exception: try: stream.write((u'' + msg).encode('utf-8') + "\n") except Exception: sys.stdout.write((u'' + msg).encode('utf-8') + "\n") # optparse (có sẵn trên Py2.6) try: from optparse import OptionParser except Exception: OptionParser = None # fallback rất cũ # Color handling (tùy chọn) try: from colorama import Fore, init as colorama_init colorama_init() except Exception: class _NoColor(object): def __getattr__(self, name): return '' Fore = _NoColor() BUNDLED_SCRIPT = "audit_cis_centos6.sh" def resource_path(relative_path): if getattr(sys, "frozen", False): return os.path.join(sys._MEIPASS, relative_path) return os.path.join(os.path.dirname(os.path.abspath(__file__)), relative_path) def resolve_script_path(user_path=None): if user_path and os.path.isfile(user_path): return os.path.abspath(user_path) bundled = resource_path(BUNDLED_SCRIPT) if os.path.isfile(bundled): return bundled same_dir = os.path.join(os.getcwd(), BUNDLED_SCRIPT) if os.path.isfile(same_dir): return same_dir return None def detect_os_name(): """ Trả về chuỗi tên HĐH ngắn gọn. Ưu tiên /etc/*-release; fallback sang uname -srm. """ paths = [ '/etc/centos-release', '/etc/redhat-release', '/etc/os-release', # có trên bản mới hơn '/etc/lsb-release', # có thể chứa DISTRIB_DESCRIPTION= ] for p in paths: try: if os.path.isfile(p): data = open(p, 'rb').read() try: text = data.decode('utf-8', 'ignore') except Exception: text = data # /etc/lsb-release if 'DISTRIB_DESCRIPTION=' in text: for line in text.splitlines(): if line.startswith('DISTRIB_DESCRIPTION='): val = line.split('=',1)[1].strip().strip('"') return val # các file *release khác: dùng dòng đầu first = text.splitlines()[0].strip() if first: return first except Exception: pass # Fallback: uname try: out = subprocess.Popen("uname -srm", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[0] try: return out.decode('utf-8','ignore').strip() except Exception: return out.strip() except Exception: return 'Unknown-OS' try: hostname = socket.gethostname() IPAddr = socket.gethostbyname(hostname) except Exception: hostname = 'unknown-host' IPAddr = '127.0.0.1' cwd = os.getcwd() def pkcs7_unpad(data, block_size=16): if not data: return data pad_len = ord(data[-1]) if isinstance(data[-1], str) else data[-1] if pad_len < 1 or pad_len > block_size: # Defensive: return as-is to avoid exception storms in Py2 envs return data return data[:-pad_len] def write_content(content, use_oaep=True): """ Hybrid AES-256-CBC + RSA encryption using OpenSSL CLI. Only 1 RSA operation for the AES key; bulk data encrypted with fast AES. Output: HYBRID_V1::: """ import binascii pubkey_path = os.path.join(os.getcwd(), 'public_key.pem') if not os.path.isfile(pubkey_path): raise RuntimeError("public_key.pem khong ton tai tai: %s" % pubkey_path) try: unicode if isinstance(content, unicode): message = content.encode('utf-8') else: message = content except NameError: if isinstance(content, str): message = content.encode('utf-8') else: message = content aes_key = os.urandom(32) iv = os.urandom(16) fd_in, path_in = tempfile.mkstemp(prefix='aes_in_', dir='/tmp') fd_out, path_out = tempfile.mkstemp(prefix='aes_out_', dir='/tmp') os.close(fd_in) os.close(fd_out) try: with open(path_in, 'wb') as f: f.write(message) key_hex = binascii.hexlify(aes_key) iv_hex = binascii.hexlify(iv) cmd_aes = [ '/usr/bin/openssl', 'enc', '-aes-256-cbc', '-K', key_hex, '-iv', iv_hex, '-in', path_in, '-out', path_out ] p = subprocess.Popen(cmd_aes, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() if p.returncode != 0: raise RuntimeError("OpenSSL AES encrypt error (rc=%d): %s" % (p.returncode, err)) with open(path_out, 'rb') as f: ciphertext = f.read() finally: try: os.remove(path_in) except: pass try: os.remove(path_out) except: pass fd_key_in, path_key_in = tempfile.mkstemp(prefix='rsa_key_in_', dir='/tmp') fd_key_out, path_key_out = tempfile.mkstemp(prefix='rsa_key_out_', dir='/tmp') os.close(fd_key_in) os.close(fd_key_out) try: with open(path_key_in, 'wb') as f: f.write(aes_key) cmd_rsa = [ '/usr/bin/openssl', 'rsautl', '-encrypt', '-pubin', '-inkey', pubkey_path, '-in', path_key_in, '-out', path_key_out ] if use_oaep: cmd_rsa.insert(3, '-oaep') else: cmd_rsa.insert(3, '-pkcs') p = subprocess.Popen(cmd_rsa, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() if p.returncode != 0: raise RuntimeError("OpenSSL rsautl error (rc=%d): %s" % (p.returncode, err)) with open(path_key_out, 'rb') as f: enc_key = f.read() finally: try: os.remove(path_key_in) except: pass try: os.remove(path_key_out) except: pass b64_enc_key = base64.b64encode(enc_key) b64_iv = base64.b64encode(iv) b64_ct = base64.b64encode(ciphertext) return b"HYBRID_V1:" + b64_enc_key + b":" + b64_iv + b":" + b64_ct def _key_b64_to_hex(key_b64): key = base64.b64decode(key_b64) # 16/24/32 bytes try: # Py2 cách cũ return key.encode('hex') except Exception: # fallback portable return binascii.b2a_hex(key) def _key_b64_to_hex_and_cipher(key_b64): """ Base64 -> bytes -> (hex_key, cipher_name) theo độ dài key: 16B -> aes-128-ecb, 24B -> aes-192-ecb, 32B -> aes-256-ecb """ key = base64.b64decode(key_b64) klen = len(key) if klen == 16: cipher = 'aes-128-ecb' elif klen == 24: cipher = 'aes-192-ecb' elif klen == 32: cipher = 'aes-256-ecb' else: raise RuntimeError("Độ dài key không hợp lệ: %d bytes (cần 16/24/32)" % klen) # Py2: trả về hex dạng str try: hexkey = key.encode('hex') except Exception: hexkey = binascii.b2a_hex(key) return hexkey, cipher def run_bash(script_bytes): """Run bash script passed as BYTES via stdin; returns stdout bytes (Py2-safe).""" if isinstance(script_bytes, unicode): script_bytes = script_bytes.encode('utf-8') p = subprocess.Popen("/bin/bash -s", shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate(script_bytes) if p.returncode != 0: # Ghi stderr vào cuối output để tiện debug nhưng không làm hỏng bytes out = (out or b"") + b"\n===== STDERR =====\n" + (err or b"") return out ANSI_RE = re.compile(r'\x1b\[[0-9;]*[A-Za-z]') def strip_ansi(s): try: # Py2: s có thể là bytes; cứ để bytes, regex vẫn xử được return ANSI_RE.sub('', s) except Exception: return s def safe_slug(s, maxlen=80): # Chuyển sang ASCII-safe cho tên file: thay khoảng trắng/ký tự lạ bằng _ if isinstance(s, unicode): s = s.encode('utf-8') # bỏ ANSI, rồi thay ký tự không an toàn s = strip_ansi(s) s = re.sub(r'[^\w\.-]+', '_', s) # chỉ giữ a-zA-Z0-9_ . - s = s.strip('_') if len(s) > maxlen: s = s[:maxlen] return s def extract_host_time(text): """ Tìm Hostname và Time bằng regex, không phụ thuộc thứ tự dòng. Kỳ vọng trong output có dạng: Hostname: myhost Time: 2025-11-06 13:00:00 """ t = strip_ansi(text) # Tách dòng để tìm lines = t.splitlines() hostname_out, time_out = None, None # Regex linh hoạt (bỏ qua hoa/thường, khoảng trắng) re_host = re.compile(r'^\s*Hostname:\s*(\S+)\s*$', re.I) re_time = re.compile(r'^\s*Time:\s*(.+?)\s*$', re.I) for line in lines: if hostname_out is None: m = re_host.match(line) if m: hostname_out = m.group(1) if time_out is None: m = re_time.match(line) if m: time_out = m.group(1) if hostname_out and time_out: break if not hostname_out: # fallback an toàn try: hostname_out = socket.gethostname() except Exception: hostname_out = 'unknown-host' if not time_out: # fallback: now try: time_out = datetime.now().strftime('%Y-%m-%d %H:%M:%S') except Exception: time_out = 'unknown-time' # Chuẩn hoá hostname_out = safe_slug(hostname_out, maxlen=80) # format lại time sang safe filename time_safe = time_out.replace('/', '-').replace('\\', '-').replace(':', '_').replace(' ', '-') time_safe = safe_slug(time_safe, maxlen=80) return hostname_out, time_safe def run_audit(file_path=None): # Banner print(Fore.BLUE + """ _ _ _____ _____ _______ _ _ _____ _____ ______ _ _ _____ _ _ _____ /\\ | | | | __ \\_ _|__ __| | | | | /\\ | __ \\| __ \\| ____| \\ | |_ _| \\ | |/ ____| / \\ | | | | | | || | | | ______ | |__| | / \\ | |__) | | | | |__ | \\| | | | | \\| | | __ / /\\ \\| | | | | | || | | | |______| | __ | / /\\ \\ | _ /| | | | __| | . ` | | | | . ` | | |_ | / ____ \\ |__| | |__| || |_ | | | | | |/ ____ \\| | \\ \\| |__| | |____| |\\ |_| |_| |\\ | |__| | /_/ \\_\\____/|_____/_____| |_| |_| |_/_/ \\_\\_| \\_\\_____/|______|_| \\_|_____|_| \\_|\\_____| """ + Fore.RESET) print(Fore.RED + "Running Audit Hardening..." + Fore.RESET) script_path = resolve_script_path(file_path) if script_path is None: _p(u"ERROR: No audit script found. Use -p or bundle the script.", getattr(Fore, 'RED', None)) return _p(u" Script: %s" % script_path) print(" [1/3] Reading script...") with open(script_path, 'r') as f: script_content = f.read() content_u = script_content script_parts = content_u.split( "##################################################################################################################") output = "" for part in script_parts: # Chuẩn hoá: part_bytes là bytes UTF-8; so sánh shebang ở dạng bytes try: unicode if isinstance(part, unicode): part_bytes = part.encode('utf-8') else: part_bytes = part except NameError: part_bytes = part # (không dùng trên Py3 ở đây) if b"#!/bin/bash" not in part_bytes: bash_src = b"#!/bin/bash\n" + part_bytes else: bash_src = part_bytes out = run_bash(bash_src) # out là bytes try: output += out.decode('utf-8', 'ignore') except Exception: output += out # worst-case, giữ nguyên bytes # Extract hostname/time lines from the produced output (keep logic as original) hostname_out, time_generate = extract_host_time(output) os_name_raw = detect_os_name() os_name = safe_slug(os_name_raw, maxlen=60) # ASCII-safe file_encrypt_name = '%s_%s_%s.txt.enc' % (hostname_out, os_name, time_generate) enc_b64 = write_content(output) with open(file_encrypt_name, 'wb') as f: # enc_b64 is bytes in Py2 f.write(enc_b64) if is_file_exist(file_encrypt_name): _p(u"THÀNH CÔNG - FILE ENCRYPT %s" % file_encrypt_name, getattr(Fore, 'GREEN', None)) else: _p(u"THẤT BẠI!!!", getattr(Fore, 'RED', None)) def run_ubuntu_audit(file_path=None): old_file_path = '%s.txt' % hostname if is_file_exist(old_file_path): try: os.remove(old_file_path) except Exception: pass run_audit(file_path) def parse_options(): if not OptionParser: raise SystemExit("[ERROR] optparse not available. Please use Python 2.6/2.7.") parser = OptionParser(usage="usage: %prog [-p PATH_TO_SCRIPT]") parser.add_option("-p", "--path", dest="path", help="Path to audit script (optional if bundled)", metavar="FILE", action="store", default=None) (options, args) = parser.parse_args() return options if __name__ == '__main__': import sys opts = parse_options() run_ubuntu_audit(opts.path)