root-me의 XML-External-Entity 문제를 풀다가 XEE가 발생되는 취약한 소스코드는 어떻게 이루어져 있는지, 그렇다면 어떻게 대응해야하는지 연구해보았다.
일단, 해당 문제의 소스코드는 다음과 같이 이루어져 있다. 이 중 처음보거나 헷갈리는 코드에 대해서 상세히 알아보자.
1. 코드분석
<?php
echo '<html>';
echo '<header><title>XXE</title></header>';
echo '<body>';
echo '<h3><a href="?action=checker">checker</a> | <a href="?action=auth">login</a></h3><hr />';
if ( ! isset($_GET['action']) ) $_GET['action']="checker";
if($_GET['action'] == "checker"){
libxml_disable_entity_loader(false);
libxml_use_internal_errors(true);
echo '<h2>RSS Validity Checker</h2>
<form method="post" action="index.php">
<input type="text" name="url" placeholder="http://host.tld/rss" />
<input type="submit" />
</form>';
if(isset($_POST["url"]) && !(empty($_POST["url"]))) {
$url = $_POST["url"];
echo "<p>URL : ".htmlentities($url)."</p>";
try {
$ch = curl_init("$url");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT ,0);
$inject = curl_exec( $ch );
curl_close($ch);
$string = simplexml_load_string($inject, null, LIBXML_NOENT);
if ( ! is_object($string)) throw new Exception("error");
foreach($string->channel->item as $row){
print "<br />";
print "===================================================";
print "<br />";
print htmlentities($row->title);
print "<br />";
print "===================================================";
print "<br />";
print "<h4 style='color: green;'>XML document is valid</h4>";
}
} catch (Exception $e) {
print "<h4 style='color: red;'>XML document is not valid</h4>";
}
}
}
if($_GET['action'] == "auth"){
echo '<strong>Login</strong><br /><form METHOD="POST">
<input type="text" name="username" />
<br />
<input type="password" name="password" />
<br />
<input type="submit" />
</form>
';
if(isset($_POST['username'], $_POST['password']) && !empty($_POST['username']) && !empty($_POST['password']))
{
$user=$_POST["username"];
$pass=$_POST["password"];
if($user === "admin" && $pass === "[FLAG]"){
print "Flag : [FLAG]<br />";
}
}
}
echo '</body></html>';
?>
(1) libxml 함수
libxml_disable_entity_loader(false);
libxml_use_internal_errors(true);
- libxml_disable_entity_loader 함수는 외부 entities를 로그하는 능력을 disable 할 것인지에 대한 설정이다. 여기서는 false로 하였기 때문에 외부의 entities를 가져올 수 있으며 이로 인해 XEE가 가능하다. 보안을 위해서라면 true로 해야한다.
- libxml_use_internal_errors 함수는 기본 libxml error 처리를 비활성화하고 사용자가 에러를 핸들링할지를 정하는 함수이다. 여기서는 true로 하였으므로 코드 상에서 try~catch 문을 이용하여 에러를 핸들링해주었다.
(2) curl 함수
$ch = curl_init("$url");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT ,0);
$inject = curl_exec( $ch );
curl_close($ch);
- curl_init 함수는 cURL 세션을 초기화하는 함수이다. 해당 함수curl_setopt(), curl_exec(), curl_close() 함수에서 사용할 수 있는 cURL handle이 리턴된다.
- curl_setopt 함수는 cURL transfer을 위한 옵션을 설정해주는 함수이다. 여기서 설정하는 옵션은 다수가 있지만, 소스 내에 존재하는 옵션들에 대해서만 알아보았다.
CURLOPT_RETURNTRANSFER |
TRUE일 경우 curl_exec()의 결과를 문자열로 리턴해준다. |
CURLOPT_TIMEOUT |
cURL 함수가 실행될 최대 초단위의 시간이다. |
CONNECTTIMEOUT |
연결을 시도하기 위해 대기하는 시간이다. 0일 경우 즉시 시도한다. |
- curl_exec 함수는 주어진 cURL 세션을 실행하는 함수이다. 기본적으로는 결과가 성공일 땐 TRUE, 실패일 땐 FALSE를 출력하지만 CURLOPT_RETURNTRANSFER 옵션이 설정되어 있을 경우, 성공일 때 그 결과가 출력된다.
- curl_close 함수는 cURL 세션과 모든 자원들을 닫는다. cURL handle인 ch 또한 삭제된다.
(3) simplexml_load_string 함수
$string = simplexml_load_string($inject, null, LIBXML_NOENT);
- simplexml_load_string 함수는 XML 형식의 문자열을 object로 변환해주는 함수이다. XML 형식의 문자열은 $inject로 들어가며 LIBXML_NOENT FLAG를 통해 &var;와 같은 ENTITY reference를 모두 실행 결과로 대체한다. 만약 해당 FLAG를 주지 않으면 external entity가 노출되지 않는다.
2. 결론
※ XXE 취약점이 trigger되기 위한 조건
1. simplexml_load_string에 LIBXML_NOENT FLAG가 설정되어 있어야 한다.
2. libxml_disable_entity_loader가 true로 설정되어 있어야 한다. 이는 1의 설정이 되어있어도 false로 되어있다면 trigger되지 않는다.
참고
- https://umbum.dev/475
- https://security.stackexchange.com/questions/133906/is-php-loadxml-vulnerable-to-xxe-attack-and-to-other-attacks-is-there-a-list