Thursday, May 24, 2018

WindScribe VPN Privilege Escalation

The VPN component in Windscribe 1.81 uses the OpenVPN client for connections. Also, it creates a WindScribeService.exe system process that establishes a \\.\pipe\WindscribeService named pipe endpoint that allows the Windscribe VPN process to connect and execute an OpenVPN process or other processes (like taskkill, etc.). There is no validation of the program name before constructing the lpCommandLine argument for a CreateProcess call. An attacker can run any malicious process with SYSTEM privileges through this named pipe.


Named Pipe function:
HANDLE createNamedPipe()
{
  HANDLE result; // rax@2
  struct _SECURITY_ATTRIBUTES SecurityAttributes; // [sp+40h] [bp-28h]@1

  SecurityAttributes.lpSecurityDescriptor = malloc(0x28ui64);
  if ( InitializeSecurityDescriptor(SecurityAttributes.lpSecurityDescriptor, 1u)
    && SetSecurityDescriptorDacl(SecurityAttributes.lpSecurityDescriptor, 1, 0i64, 0) )
  {
    SecurityAttributes.nLength = 24;
    SecurityAttributes.bInheritHandle = 1;
    result = CreateNamedPipeW(
               L"\\\\.\\pipe\\WindscribeService",
               0x40000003u,
               6u,
               0xFFu,
               0x7D05u,
               0x7D05u,
               0,
               &SecurityAttributes);
  }
  else
  {
    result = (HANDLE)-1;
  }
  return result;
}
First NamedPipe is created and then information is waited in WindscribeService (Running in SYSTEM Privilege) from WindScribe to execute openvpn client or other commands.

Main function for creating NamedPipe:
  v5 = createNamedPipe();  < === NamedPipe is created by this function.
  if ( v5 != (HANDLE)-1 )
  {
    v6 = CreateEventW(0i64, 1, 1, 0i64);
    if ( v6 )
    {
      ServiceStatus.dwControlsAccepted = 5;
      ServiceStatus.dwCurrentState = 4;
      ServiceStatus.dwWin32ExitCode = 0;
      ServiceStatus.dwCheckPoint = 0;
      SetServiceStatus(hServiceStatus, &ServiceStatus);
      Overlapped.InternalHigh = (ULONG_PTR)v6;
      Handles = hEvent;
      v19 = v6;
      ConnectNamedPipe(v5, &Overlapped);
      for ( i = WaitForMultipleObjects(2u, &Handles, 0, 0xFFFFFFFF);
            i;
            i = WaitForMultipleObjects(2u, &Handles, 0, 0xFFFFFFFF) )
      {
        if ( i == 1 )
        {
          v8 = 32005;
          v9 = (char *)&Overlapped.hEvent;
          LODWORD(v17) = 0;
          while ( ReadFile(v5, v9, v8, (LPDWORD)&v17, 0i64) ) // Information (shellcode) sent by attacker is read and written to buffer.
          {
            v9 += (unsigned int)v17;
            v8 -= (unsigned int)v17;
            if ( !v8 ) < If data sent by attacker is 32005 byte, sendLog_and_CreateProcess functions are called and data is sent to this function.
            {
              v11 = 1;
              goto LABEL_21;
            }
          }
          v11 = 0;
LABEL_21:
          if ( v11 )
          {
            sendLog_and_CreateProcess(
              (__int64)&Buffer,
              (__int64)&Overlapped.hEvent,
              v10,
              &Uuid,
              (__int64)engineHandle,
              (__int64)&v26);
There are multiple conditions in sendLog_and_CreateProcess functions and in this functions there is validation for COMMAND_EXECUTE(to run openvpn.exe process) in which attacker should sent a byte to buffer.
Then this command line parameter(which has been sent to create process) is sent to send_log_file function for logging.

sendLog_and_CreateProcess function:
send_log_file(&File, L"AA_COMMAND_EXECUTE, blocking=%d, cmd=%s"); < === is sent to log file
if ( *(_BYTE *)(v7 + 4) )
{
  sub_13FF6E7C0(v13, v12, v14);
  v16 = create_priviliged_process(v15, (__int64)&Memory, v7); < == Information recieved by buffer is sent to CreateProcess function.
}
Then attacker information which has been sent to buffer by Named Pipe, is sent to CreateProcess function in create_priviliged_process and process is executed.

MSDN CreateProcess documentation:
CreateProcess(
  _In_opt_    LPCTSTR               lpApplicationName,
  _Inout_opt_ LPTSTR                lpCommandLine, < ===== "calc.exe"
  _In_opt_    LPSECURITY_ATTRIBUTES lpProcessAttributes,
  _In_opt_    LPSECURITY_ATTRIBUTES lpThreadAttributes,
  _In_        BOOL                  bInheritHandles,
  _In_        DWORD                 dwCreationFlags,
  _In_opt_    LPVOID                lpEnvironment,
  _In_opt_    LPCTSTR               lpCurrentDirectory,
  _In_        LPSTARTUPINFO         lpStartupInfo,
  _Out_       LPPROCESS_INFORMATION lpProcessInformation
);
create_priviliged_process function:
  if ( CreatePipe(&hReadPipe, &hWritePipe, &PipeAttributes, 0) )
  {
    sub_13FF78490(&StartupInfo, 0i64, 104i64);
    StartupInfo.cb = 104;
    StartupInfo.dwFlags |= 0x100u;
    StartupInfo.hStdInput = 0i64;
    StartupInfo.hStdError = hWritePipe;
    StartupInfo.hStdOutput = hWritePipe;
    hWritePipe = 0i64;
    hObject = 0i64;
    v15 = 0i64;
    if ( CreateProcessW(
           0i64,
           (LPWSTR)(v3 + 261), < ==== commandline parameter is sent by attacker.
           0i64,
           0i64,
           1,
           0x8000020u,
           0i64,
           0i64,
           &StartupInfo,
           (LPPROCESS_INFORMATION)&hWritePipe) )
    {
      WaitForSingleObject(hWritePipe, 0xFFFFFFFF);
      GetExitCodeProcess(hWritePipe, (LPDWORD)&hReadPipe);
      CloseHandle(hWritePipe);
      sub_13FF6E370(&Memory, hReadPipe);
      CloseHandle(hReadPipe);
      CloseHandle(hWritePipe);
      CloseHandle(hObject);
      v11[8] = 1;
      *(_DWORD *)&v11[9] = (_DWORD)hReadPipe;
      *(_DWORD *)&v11[18] = Size;
      if ( Size )
      {
        v8 = operator new(Size);
        *(_QWORD *)&v11[22] = v8;
        v9 = &Memory;
        if ( v21 >= 0x10 )
          v9 = Memory;
        sub_13FF700B0(v8, v9, Size);
      }
      if ( v21 >= 0x10 )
        j_free(Memory);
    }



Exploit code:

https://pastebin.com/eLG3dpYK

CVE Assigned:
CVE-2018-11479

Sunday, April 29, 2018

PHDays HackQuest 2018 Rubik WriteUP (Kubernetes Exploitation)

Bir neçə gün öncə başlayan Wallarm tərəfindən təşkil edilən PHDays HackQuest-də Rubik taskı haqqında yazacam.

İlk öncə tasklar belədir:

event0
mnogorock
k3y
sincity
wowsuchchain
audio.mp3
malwaremustlive
board
eNgeeks
Rubik
CryptoApocalypse


Telegram kanalında belə bir xatırlatma var idi:

Just want to remind you that /tasks is sorted by complexity, simple at first

Sonuncu tasklardan olan Rubik taskına baxdığımız zaman Wordpress üzərində qurulmuş bir blog var.
Daha sonra kanalda bir hint göndərildi. Hint isə belə idi:

Hint for Rubik: Notice the "print" button in news. Veeery strange! Can I print my article?

Source code-a baxdığımızda print button-ın yanında belə bir comment ilə qarşılaşırıq.

<!-- UPD 25.04.2018, bold button for html2pdf --><b><a href='?print'>[Print]</a></b>


html2pdf haqqında oxuduğda isə bu library html səhifənin pdf formatına çevrilməsi üçün istifadə edildiyini görmək olar. Daha sonra bu blogda qeydiyyatdan keçərək post əlavə etməyi yoxladım. Post əlavə etdikdən sonra review üçün administratora göndərmək lazım olur postu ancaq Review edərək biz kontenti görə bilirik. Daha sonra review hissəsində Print button-dan istifadə edərək yazdığım html kontenti görə bildim. Test üçün istifadə etdiyim payload belə idi:

<script>document.write("can you pwn me?")</script>


Artıq hər hansı bir JS kodunun html2pdf library tərəfindən handle edildiyini bilirik. Daha sonra Ajax kod ilə file:// scheması istifadə edərək hər hansı bir faylın oxunmasını yoxladım və /etc/passwd faylını yoxlayaraq oxuya bildim. Ancaq gecə saatlarında əməliyyat sistemində flag axtararkən Telegram kanalda bu task haqqında növbəti hint gəldi.

Hint for Rubik: There is no need to read local files.

The blog is not the only service on the host.


Artıq əmin oldum ki, hər hansı bir fayl oxunmasına ehtiyyac yoxdur. Sonuncu məqalədə qeyd edildiyi kimi iframe vasitəsi ilə daxili portları yoxlamağa başladım.
Sistemdə digər servisləri yoxlamaq üçün aşağıdakı kod ilə digər portları yoxlamağa başladım. 

<iframe src="http://127.0.0.1:8080"></iframe>

Bu iframe kodunu yoxladığdan sonra sistemdə 8080 portu üzərində Kubernetes Container Orchestration idarə edilməsi üçün Dashboard olduğunu gördüm.


Həmçinin DevOps environmentlərlə çalışdığım üçündə Kubernetesi artıq bir neçə ay əvvəldən istifadə edib öyrənmişdim Kubernetes ilə nələr edə biləciyimi düşünəndə kubernetes dashboard yeni api-də shell execution enviromentının feature olaraq əlavə edildiyini bilirdim. Daha sonra düşündüm ki, default namespace-dəki podların siyahısına baxmalıyam. Kubernetes dashboardın frontend texnologiya olaraq AngularJS istifadə etdiyini bilərək artıq Dashboard-ın istifadə etdiyi APİ üzərində araşdırmalar aparmağa başladım.
Kubernetesin current namespace-dəki podların siyahısını çəkmək üçün /api/v1/pod parametrini istifadə etdiyini gördüm. Burada isə Dashboard üçün APİ kommunikasiya üçün Golang ilə Rest APİ routing interfeysi hazırlanıb. Daha sonra bütün podların siyahısını görmək üçün sorğu göndərdikdə mənə qayıdan nəticə belə oldu:

Response-da göründüyü kimi mənə lazım olan flag kontaynerinin yerləşdiyi pod-u artıq bilirəm. Artıq növbəti addımda bu kontayner üzərində shell environment almaq idi mənə lazım olan proses. Bundan əvvəl kubelet API-a məxsus 10255 və 10250 portlarını yoxlamışdım əvvəlcədən ancaq bir nəticə ala bilməmişdim. Sonra Kubernetes Dashboardı source kodlarını analiz etməyə başladım həmçinin dashboard üzərdinə research aparmağa başladım. Brauzer üzərində bəzi araşdırmalar apardığda Kubernetes Dashboard üzərində interaktiv shell execution environmenti üçün Websocket protokolonu istifadə etdiyini gördüm daha sonra source kod üzərində initialization proseslərinə baxmağa başladım. Kod üzərində ilkin initialization stage-də dashboard APİ üzərində Session İD dəyəri generasiya edilir və bütün sessiya boyu bu session id istifadə edilir.

API for generation session id:
"api/v1/pod/default/flagggg-v233-923pra/shell/flag"

Browser vasitəsi ilə session id generasiya olunmağı üçün request göndərdikdə:

Request URL: http://0xdeadbeef/api/v1/pod/default/flagggg-v233-923pra/shell/flag

Response olaraq bizə session id göndərilir:

id:"5e1b1fde44803fe4c93e3d628b4f8114"

Source kod üzərində baxdığda isə:




handleExecShell funksiyası vasitəsi ilə terminal session id generasiya edilir:

// Handles execute shell API call
func (apiHandler *APIHandler) handleExecShell(request *restful.Request, response *restful.Response) {
 sessionId, err := genTerminalSessionId()
 if err != nil {
  kdErrors.HandleInternalError(response, err)
  return
 }

 k8sClient, err := apiHandler.cManager.Client(request)
 if err != nil {
  kdErrors.HandleInternalError(response, err)
  return
 }

 cfg, err := apiHandler.cManager.Configrequest)
 if err != nil {
  kdErrors.HandleInternalError(response, err)
  return
 }

 terminalSessions[sessionId] = TerminalSession{
  id:       sessionId,
  bound:    make(chan error),
  sizeChan: make(chan remotecommand.TerminalSize),
 }
 go WaitForTerminal(k8sClient, cfg, request, sessionId)
 response.WriteHeaderAndEntity(http.StatusOK, TerminalResponse{Id: sessionId})
}



Sonra isə SockJS adında javascript library ilə yaradılan terminal websocket sessiyasına qoşulma edərək interaktiv direktivləri göndərir. Qoşulma üçün istifadə edilən JS koduna baxaq:

  /**
   * Attached to hterm.onTerminalReady
   * @private
   */
  onTerminalReady() {
    this.io = this.term.io.push();
    this.resource_(`api/v1/pod/${this.stateParams_.objectNamespace}/${this.podName}/shell/${
                       this.container}`)
        .get({}, this.onTerminalResponseReceived.bind(this));
  }

  /**
   * Called when .../shell/... resource is fetched
   * @private
   */
  onTerminalResponseReceived(terminalResponse) {
    // https://github.com/sockjs/sockjs-client
    this.conn = new SockJS(`api/sockjs?${terminalResponse.id}`);
    this.conn.onopen = this.onConnectionOpen.bind(this, terminalResponse);
    this.conn.onmessage = this.onConnectionMessage.bind(this);
    this.conn.onclose = this.onConnectionClose.bind(this);
  }


Generasiya edilmiş session id alındığdan sonra açılmış websocket-ə göndərilir:

  /**
   * Attached to SockJS.onopen
   * @private
   */
  onConnectionOpen(terminalResponse) {
    this.conn.send(JSON.stringify({'Op': 'bind', 'SessionID': terminalResponse.id}));

    // Send at at least one resize event after attach so pty has the initial size
    this.onTerminalResize(this.term.screenSize.width, this.term.screenSize.height);

    this.io.onVTKeystroke = this.onTerminalVTKeystroke.bind(this);
    this.io.sendString = this.onTerminalSendString.bind(this);
    this.io.onTerminalResize = this.onTerminalResize.bind(this);
  }


Brauzer üzərində stabil websocket connection sonrasında göndərilən məlumatlara baxaq:




İlk olaraq terminal initilization üçün bu iki direktiv ard arda göndərilir:

"{"Op":"bind","SessionID":"5e1b1fde44803fe4c93e3d628b4f8114"}"

"{"Op":"resize","Cols":x,"Rows":y}"

Sonra is response olaraq shell prompt gəldiyini görürük:
"{"Op":"stdout","Data":"root@youcanpwnme:/myapp# ","SessionID":"","Rows":0,"Cols":0}"

Artıq burada shell environmentin işləmə prinsipi anlayandan sonra WebSocket obyektini istifadə edərək terminal session id generasiya edib command prompt almağa çalışdım. İlk öncə hackquestdə olan kubernetes dashboard api üzərində session id yaratmaq üçün bir iframe ilə request göndərdim:

<iframe src="http://127.0.0.1:8080/api/v1/pod/default/flagggg-d54f5c54f-nvb26/shell/flag"></iframe>



Terminal Session İd generasiya edildi:

Bu id dəyərini alaraq websocket yaradaraq yuxarıda göstərilən direktivləri göndəririk API adresinə. Bunun üçün isə mən kiçik bir payload hazırladım:



Ardından convert edilən pdf kontentində alınan shell prompt məlumatını görürük:


Response olaraq isə bizə Flag dəyərinin gəldiyini görürük :)
Flag: md5(Hallo_from_bykva)