443 lines
14 KiB
Python
443 lines
14 KiB
Python
# -*- 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:<b64_enc_key>:<b64_iv>:<b64_ciphertext>
|
|
"""
|
|
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 <script.sh> 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)
|