Format presentasi PowerPoint (.pptx) mengacu pada standar Open Packaging Conventions (OPC) berbasis Open Office XML (OOXML). Struktur berkas PPTX secara fisik berupa arsip ZIP yang memuat struktur hirarki berkas XML dan aset biner. Generator dokumen backend yang memproses template dari pengguna rentan terhadap tiga vektor serangan utama: Zip Slip melalui manipulasi path arsip, XML External Entity (XXE) pada slide parser, dan Server-Side Request Forgery (SSRF) melalui deklarasi target relasi.
Vektor Serangan pada Parsing OOXML
Pemeriksaan keamanan PPTX melibatkan tiga area rentan dalam siklus pemrosesan:
- Zip Slip (Path Traversal): Header arsip ZIP dapat dimanipulasi dengan karakter traversal (seperti
../../etc/cron.d/exploit). Pemanggilan dekompresi naif menulis file di luar target direktori kerja. - XML External Entity (XXE): Slide XML (contoh:
ppt/slides/slide1.xml) dapat disisipi DOCTYPE eksternal. Parser default yang mengevaluasi entitas akan membaca file sistem host (LFI) atau membekukan CPU server (Billion Laughs / XML Bomb). - External Target SSRF: Skema relasi internal (
ppt/slides/_rels/slide1.xml.rels) mendukung atributTargetMode="External". Parser dokumen otomatis yang mem-fetch URL ini mengekspos endpoint metadata server lokal atau subnet internal.
Implementasi Rentan vs Hardened
Kode di bawah menunjukkan perbedaan antara pendekatan naif dengan arsitektur terlindungi saat memproses file template PPTX di Python.
1. Kode Rentan (Vulnerable Pipeline)
import zipfile
from xml.etree import ElementTree as ET
def unpack_and_parse_unsafe(pptx_path: str, extract_dir: str):
# VULNERABLE: Zip Slip via extractall
with zipfile.ZipFile(pptx_path, 'r') as z:
z.extractall(extract_dir)
# VULNERABLE: Standar parser rentan terhadap payload DTD/XXE jika lib xml diganti
slide_xml = f"{extract_dir}/ppt/slides/slide1.xml"
tree = ET.parse(slide_xml)
return tree.getroot()2. Kode Hardened (Secure Pipeline)
Gunakan resolusi path kanonikal via os.path.commonpath untuk sanitasi arsip, tolak symbolic link, nonaktifkan entitas eksternal pada lxml.etree.XMLParser, dan validasi atribut TargetMode pada schema relasi.
import os
import zipfile
from pathlib import Path
from lxml import etree
class SecurityError(Exception):
"""Dilempar saat ditemukan anomali keamanan pada OOXML."""
pass
def safe_extract_pptx(zip_path: str, extract_to: str) -> None:
destination = Path(extract_to).resolve()
with zipfile.ZipFile(zip_path, 'r') as zf:
for member in zf.infolist():
# Cegah eksploitasi symlink file system
if (member.external_attr >> 16) & 0o120000 == 0o120000:
raise SecurityError(f"Symlink terdeteksi: {member.filename}")
target_path = (destination / member.filename).resolve()
# Mitigasi Zip Slip dengan perbandingan canonical path
if os.path.commonpath([str(destination), str(target_path)]) != str(destination):
raise SecurityError(f"Path traversal terdeteksi: {member.filename}")
zf.extract(member, destination)
def get_secure_xml_parser() -> etree.XMLParser:
# Nonaktifkan resolusi entitas, jaringan, dan deklarasi DTD
return etree.XMLParser(
resolve_entities=False,
no_network=True,
load_dtd=False,
dtd_validation=False,
huge_tree=False
)
def parse_secure_slide(file_path: str) -> etree._Element:
parser = get_secure_xml_parser()
with open(file_path, 'rb') as f:
return etree.parse(f, parser=parser).getroot()
def validate_relationships(rels_path: str) -> None:
"""Tolak relasi yang memanggil target eksternal (SSRF)."""
if not os.path.exists(rels_path):
return
parser = get_secure_xml_parser()
tree = etree.parse(rels_path, parser=parser)
ns = {'r': 'http://schemas.openxmlformats.org/package/2006/relationships'}
for rel in tree.xpath('//r:Relationship', namespaces=ns):
mode = rel.get('TargetMode', '')
if mode.strip().lower() == 'external':
target = rel.get('Target', '')
raise SecurityError(f"Target relasi eksternal dilarang (SSRF risk): {target}")
# ponytail: Skema validasi parser terbatas pada skema relasi dasar. Tambahkan validator XSD ketat jika membutuhkan integritas data penuh.Pengujian Deterministik
Verifikasi pipeline pengamanan menggunakan script pengujian berbasis assert bawaan tanpa dependensi test-runner tambahan.
import io
import tempfile
import zipfile
def test_zip_slip_rejection():
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w') as zf:
zf.writestr("../../evil.txt", "payload")
zip_buffer.seek(0)
with tempfile.TemporaryDirectory() as temp_dir:
zip_path = os.path.join(temp_dir, "exploit.pptx")
with open(zip_path, "wb") as f:
f.write(zip_buffer.read())
try:
safe_extract_pptx(zip_path, temp_dir)
assert False, "Gagal: Zip Slip tidak diblokir"
except SecurityError:
pass # Berhasil dicegah
def test_xxe_rejection():
xxe_payload = b"""<?xml version="1.0"?>
<!DOCTYPE test [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<text>&xxe;</text>
</p:sld>"""
parser = get_secure_xml_parser()
try:
root = etree.fromstring(xxe_payload, parser=parser)
# Pastikan entity tidak terevaluasi menjadi isi file
text_val = root.findtext("text")
assert text_val is None or text_val == "", f"Gagal: XXE terevaluasi -> {text_val}"
except etree.XMLSyntaxError:
pass # Entity expansion diblokir oleh parser configuration
def test_ssrf_rejection():
rels_payload = b"""<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
Target="http://169.254.169.254/latest/meta-data/" TargetMode="External"/>
</Relationships>"""
with tempfile.NamedTemporaryFile(suffix=".rels", delete=False) as f:
f.write(rels_payload)
tmp_rels = f.name
try:
validate_relationships(tmp_rels)
assert False, "Gagal: SSRF External Target tidak diblokir"
except SecurityError:
pass # Berhasil dicegah
finally:
os.remove(tmp_rels)
if __name__ == "__main__":
test_zip_slip_rejection()
test_xxe_rejection()
test_ssrf_rejection()
print("Semua pengujian keamanan OOXML berhasil (assert passed).")Skipped: scanning resource binary/image hashing. Tambahkan integrasi ClamAV atau Pillow MIME verification saat generator mengizinkan embedding SVG/PNG.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!