
VTiger オープンソースCRM v7.5における認証済みSQLインジェクションの脆弱性
発見者: Jacob Elliott
2023/07/13
VTiger CRM v7.5.0 のレポートモジュールでは、レポートの選択フィールドに対するチェックが不十分であり、それらが保存され、後でレポート実行時に二次SQLインジェクションとして再導入されます。これにより、攻撃者はデータベースから任意のフィールド(ユーザーパスワードハッシュ、WebサービスAPIアクセスキー、その他の機密データ)を漏洩させることができます。
CRMで認証した後、ユーザーはレポートモジュールに移動し、新しいレポートを作成できます。

テーブルの結合方法の都合上、レコードが存在するモジュールをプライマリモジュールとして選択するのが最適なようです。私は1件のレコードが含まれていた連絡先(Contacts)を選択しました。

次に、ユーザーはプライマリモジュールから正当なフィールドを選択し、レポート作成プロセスを続行できます。

最後に、ユーザーはBurpSuiteのようなプロキシツールで接続を傍受しながら、最終レポートを保存するボタンをクリックできます。selected_fieldsパラメーターでは、以前に選択されたフィールドが次の形式で保存関数に渡されます:
sql_table:sql_column:label:field_name
この時点で、ユーザーはsql_tableとsql_columnを、データベースから漏洩させたい任意の値に変更できます。このPOCでは、以下を使用しました:
vtiger_users:user_name:Contacts_Salutation:salutationtype
および
vtiger_users:user_password:Contacts_First_Name:firstname
修正されたリクエストを転送した後、最終レポートが表示され、データベースから目的の列が含まれており、管理者ユーザーのユーザー名とパスワードハッシュが明らかになります。

適切なチェックの欠如は、modules/Reports/ReportRun.php(394-398行)に導入されています。提供された各列名は「:」で分割されます。
$selectedfields = explode(":", $fieldcolname);
そして、ユーザーが管理者でない場合、スクリプトはそのフィールドがレポート用に選択されたプライマリモジュールから生成された許可フィールドの配列に含まれているかどうかをチェックします:
!in_array($selectedfields[3], $permitted_fields[$module])
しかし、入力された内容を思い出してください:
vtiger_users:user_name:Contacts_Salutation:salutationtype
「許可フィールド」は配列のインデックス3の要素に対してチェックされるため、チェックされるフィールドは連絡先モジュールのsalutationtypeとなり、これは機密とは見なされないため、エクスポートが許可されます。しかし、配列の最初の2つの要素で提供されたテーブルと列はそのような検証を受けず、データ漏洩につながります。
この問題は、このコミットで修正されました。選択されたフィールドの検証を変更し、各モジュールにハードコードされた許可フィールドに対してチェックするようにしました。
public function checkPermission(Vtiger_Request $request) {
parent::checkPermission($request);
$record = $request->get('record');
if ($record) {
$reportModel = Reports_Record_Model::getCleanInstance($record);
if (!$reportModel->isEditable()) {
throw new AppException(vtranslate('LBL_PERMISSION_DENIED'));
}
}
$selectedFields = $request->get('selected_fields');
$groupbyfields = $request->get('groupbyfield');
$fieldsData = array($selectedFields, $groupbyfields);
foreach ($fieldsData as $selectedField){
foreach ($selectedField as $field) {
list($tablename, $colname, $module_field, $fieldname, $single) = split(":", $field);
list($module, $fieldName) = split("_", $module_field, 2);
$moduleModel = Vtiger_Module_Model::getInstance($module);
$fieldModel = Vtiger_Field_Model::getInstance($fieldname, $moduleModel);
if (($fieldModel->table !== $tablename) || ($fieldModel->column !== $colname)) {
throw new AppException(vtranslate('LBL_PERMISSION_DENIED'));
}
}
}
return true;
}