iTerm2 CVE-2019-9535 分析(待续)

最近iterm2被曝了一个CVE-2019-9535,据说可以通过cat xxx达到命令执行的效果。随后我便开始对该漏洞进行分析,走了一趟歪路,不过之后回到正轨,却不知道该漏洞应该如何利用。先把目前分析的结果写一篇文章,如果后续能利用了,再来补充该文章。

iTerm2与Tmux的关系

经过几天的研究,终于搞明白iTerm2和Tmux的关系了。

tmux可以分为客户端与服务端,客户端的指令就是我们平常使用tmux用到的指令,比如:set -g set-titles on

服务端指令一般以百分号%开头。

此外有固定格式来回应客户端的指令,%begin timestamp number init开头,%end timestamp number init结尾,中间的值就是服务端对于客户端的响应请求。

比如:

1
2
%begin 1571109491 0 0
%end 1571109491 0 0

表示让客户端进行初始化,又比如,当客户端像服务端发送指令:show-options -v -g set-titles,服务端的响应为:

1
2
3
%begin 1571109491 1 1
on
%end 1571109491 1 1

我们可以使用命令:tmux -C来和服务端直接进行通信。

那iTerm2又是如何内置tmux的呢?为啥使用tmux -CC的时候,iTerm2会新建窗口?

先来看看当我们运行tmux -CC命令后,产生的现象:

1
2
3
4
5
6
7
8
9
$ tmux -CC
** tmux mode started **

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

当前终端得到如上所示的输出,然后弹出一个新的iterm2终端。但是在其他终端软件中却不会有这种情况。这就是iTerm2内置的tmux功能。

经过iTerm2源码的审计和调试发现,iTerm2会检测终端上的输出的字符,当检测到\033P1000p的时候,将会进入tmux模式。iTerm2变成tmux客户端,与服务端进行通信。

可以使用如下命令进行简单的测试:

1
2
3
4
5
6
7
8
9
$ echo -en "\033P1000p"
** tmux mode started **

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

误入歧途的研究

因为之前看了一篇文章说,使用run 'xxxx'来达到命令执行的效果,所以我就开始研究如果注入该命令。

审计源码,发现如下几处可能存在命令注入的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# iTermTmuxStatusBarMonitor.m
- (void)handleStatusLeftResponse:(NSString *)response {
if (!response) {
return;
}
NSString *command = [NSString stringWithFormat:@"display-message -p \"%@\"", [self escapedString:response]];
[_gateway sendCommand:command responseTarget:self responseSelector:@selector(handleStatusLeftValueExpansionResponse:)];
}

- (void)handleStatusRightResponse:(NSString *)response {
if (!response) {
return;
}
NSString *command = [NSString stringWithFormat:@"display-message -p \"%@\"", [self escapedString:response]];
[_gateway sendCommand:command responseTarget:self responseSelector:@selector(handleStatusRightValueExpansionResponse:)];
}

# iTermInitialDirectory+Tmux.m
- (void)tmuxNewWindowCommandInSession:(NSString *)session
recyclingSupported:(BOOL)recyclingSupported
scope:(iTermVariableScope *)scope
completion:(void (^)(NSString *))completion {
NSArray *args = @[ @"new-window", @"-PF '#{window_id}'" ];

if (session) {
NSString *targetSessionArg = [NSString stringWithFormat:@"\"%@:+\"", [session stringByEscapingQuotes]];
NSArray *insertionArguments = @[ @"-a",
@"-t",
targetSessionArg ];
args = [args arrayByAddingObjectsFromArray:insertionArguments];
}
[self tmuxCommandByAddingCustomDirectoryWithArgs:args
recyclingSupported:recyclingSupported
scope:scope
completion:completion];
}

# iTermTmuxOptionMonitor.m
- (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];
}

一个一个来说,我们要如何触发上面三处注入。

首先说明一下,我是用的是iTerm版本是Build 3.3.5,使用Xcode对源码进行的调试。

status bar注入点

handleStatusLeftResponse函数和handleStatusRightResponse其实代码一样,也就是一个左一个右的区别。对该函数进行回溯:

1
2
3
4
5
handleStatusLeftResponse
-> requestUpdates
-> setActive
-> _tmuxStatusBarMonitor.active = [iTermProfilePreferences boolForKey:KEY_SHOW_STATUS_BAR inProfile:aDict];
-> _tmuxStatusBarMonitor.active = [iTermProfilePreferences boolForKey:KEY_SHOW_STATUS_BAR inProfile:self.profile];

触发该点的注入,首先需要开启KEY_SHOW_STATUS_BAR配置,没找到通过输出字符串就能控制配置的方法,所以只能手工修改配置,该配置如图所示,选中图中选项:

iterm20

勾选后,iTerm2客户端就会向tmux服务端发送命令:display-message -p "#{status-left}",服务端将会有如下返回:

1
2
3
%begin 1571109491 4 1
{xxxx}
%end 1571109491 4 1

接收到返回后,iTerm2客户端会把接受到的字符串拼接到下一个发送到服务端的命令中:

1
display-message -p "{xxxx}"

escapedString函数只过滤了双引号和反斜杠,所以可以通过\n之类的符号控制命令。

new-window

第二个存在注入点的函数是tmuxNewWindowCommandInSession,通过审计追踪发现,该函数是点击事件的函数:

执行命令$ tmux -CC后,会弹出新窗口,这个时候可以打开Shell -> tmux -> Dashboard
iterm21

通过点击该+按钮,表示new window,可以触发tmuxNewWindowCommandInSession函数,该window的session_name将会被注入到该命令中:

1
2
3
4
5
6
7
8
9
NSArray *args = @[ @"new-window", @"-PF '#{window_id}'" ];

if (session) {
NSString *targetSessionArg = [NSString stringWithFormat:@"\"%@:+\"", [session stringByEscapingQuotes]];
NSArray *insertionArguments = @[ @"-a",
@"-t",
targetSessionArg ];
args = [args arrayByAddingObjectsFromArray:insertionArguments];
}

同样也是过滤了双引号和反斜杆,思路也同上。

title string

第三个函数为title string的注入,当服务端向客户端发送初始化指令的时候:

1
2
%begin 1571109491 0 0
%end 1571109491 0 0

iTerm2客户端会进入tmuxInitialCommandDidCompleteSuccessfully进行一些初始化工作,关键在loadTitleFormat函数中:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)loadTitleFormat {
[gateway_ sendCommandList:@[ [gateway_ dictionaryForCommand:@"show-options -v -g set-titles"
responseTarget:self
responseSelector:@selector(handleShowSetTitles:)
responseObject:nil
flags:0],
[gateway_ dictionaryForCommand:@"show-options -v -g set-titles-string"
responseTarget:self
responseSelector:@selector(handleShowSetTitlesString:)
responseObject:nil
flags:0] ]];
}

该函数将会向服务端发送下面两句命令:

1
2
show-options -v -g set-titles
show-options -v -g set-titles-string

相关的回调函数:

1
2
3
4
5
6
7
8
9
- (void)handleShowSetTitles:(NSString *)result {
_shouldSetTitles = [result isEqualToString:@"on"];
[[NSNotificationCenter defaultCenter] postNotificationName:kTmuxControllerDidFetchSetTitlesStringOption
object:self];
}

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

set-titles == on的情况下,会执行下面逻辑:

1
2
3
4
tmuxDidFetchSetTitlesStringOption
-> updateTmuxTitleMonitor
-> installTmuxTitleMonitor
-> initWithGateway

传入initWithGateway函数的format参数为set-titles-string命令的返回值,用户可以控制。

接下来将会触发updateOnce函数:

1
2
3
4
5
6
7
8
9
_format = set-titles-string

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

- (void)updateOnce {
......
NSString *command = [NSString stringWithFormat:@"display-message -t '%@' -p '%@'", _target, self.escapedFormat];

这个时候我们就能控制命令的参数了,这次过滤的是单引号和反斜杆,同样思路也同上。

回归正途

根据上面的分析,我们发现如果是手动的话,可以有很多方法执行系统命令,但是这个思路正确吗?

我们回头看看最初公布该漏洞的描述和利用视频。总结一下该漏洞被认为是高危的原因:

  1. curl/tail就能触发,也就是只要输出相应的字符串(payload)就能触发该漏洞。
  2. 连上ssh,也能触发本地的系统命令。

根据这两点,我们回头看看上面的利用思路,除了自己日自己,根本没任何用处。

这个时候研究进度又回到了起点,接下来我按照本地,通过只要输出相应就能执行命令的思路进行思考。

现在已知如下几点:

  1. iTerm作为tmux客户端
  2. 输出\033P1000p可以激活iTerm客户端

可以得出结论,攻击者应该是作为tmux服务端,这么一看,之前的研究思路完全是错误的。因为run command是tmux服务端进行执行的,我们要攻击的是iTerm2,所以攻击的是客户端。

经过研究发现iTerm2 客户端和tmux 服务端通信的方法就是简单的字符输入输出,所以就算我们可以注入iTerm2发给服务端的命令,但是iTerm2并不会执行该命令,只是对该命令进行字符输出。

到此为止,研究陷入僵局……

文章目录
  1. 1. iTerm2与Tmux的关系
  2. 2. 误入歧途的研究
    1. 2.1. status bar注入点
    2. 2.2. new-window
    3. 2.3. title string
  3. 3. 回归正途