GitHub Enterprise Remote Code Execution

Everyone uses GitHub. If you have huge amount of green paper or you are
very paranoid about your code, you can run your own GitHub. For $2,500
USD per 10 user years you get GitHub Enterprise: A virtual machine
containing a fully-featured GitHub instance. Despite a few edge cases
that are handled with an occasional GitHub.enterprise? invocation, it
runs the same code base as the original.

So let’s hack it.

Deobfuscating the code

When you download GitHub Enterprise, you will get a VirtualBox image
which you can deploy on your own box. I booted some random recovery
image to take a look inside the machine. Inside in the /data
directory, there is the GitHub code:

Turns out that there is a ruby module named ruby_concealer.so that
just runs Zlib::Inflate::inflate on the binary string and then and
XORs with the key "This obfuscation is intended to discourage GitHub
Enterprise customers from making modifications to the VM. We know this
'encryption' is easily broken. ". They are right. The following tool
deobfuscates the code:

#!/usr/bin/ruby## This tool is only used to "decrypt" the github enterprise source code.## Run in the /data directory of the instance.require"zlib"require"byebug"KEY="This obfuscation is intended to discourage GitHub Enterprise customers "+"from making modifications to the VM. We know this 'encryption' is easily broken. "classStringdefunescapebuffer=[]mode=0tmp=""# https://github.com/ruby/ruby/blob/trunk/doc/syntax/literals.rdoc#stringssequences={"a"=>7,"b"=>8,"t"=>9,"n"=>10,"v"=>11,"f"=>12,"r"=>13,"e"=>27,"s"=>32,"\""=>34,"#"=>35,"\\"=>92,"{"=>123,"}"=>125,}self.chars.eachdo|c|ifmode==0ifc=="\\"mode=1tmp=""elsebuffer<<c.ordendelsetmp<<ciftmp[0]=="x"iftmp.length==3buffer<<tmp[1..2].hexmode=0tmp=""nextelsenextendendiftmp.length==1&&sequences[tmp]buffer<<sequences[tmp]mode=0tmp=""nextendraise"Unknown sequences: \"\\#{tmp}\""endendbuffer.pack("C*")enddefdecrypti,plaintext=0,''Zlib::Inflate.inflate(self).each_bytedo|c|plaintext<<(c^KEY[i%KEY.length].ord).chri+=1endplaintextendendDir.glob("**/*.rb").eachdo|file|header="require \"ruby_concealer.so\"\n__ruby_concealer__ \""len=header.lengthFile.open(file,"r+")do|fh|iffh.read(len)==headerputsfileciphertext=fh.read[0..-1].unescapeplaintext=ciphertext.decryptfh.truncate(0)fh.rewindfh.write(plaintext)endendend

The enterprise management interface

Now that I got my hands on the code, I started looking for
vulnerabilities. I thought the management console would be a promising
target. If you are the admin, you can add SSH keys (for root access),
shut down services, etc. To the mere mortal however it looks like this:

Not suprisingly, the code can be found in /data/enterprise-manage/current/.

Session Management

Since the management interface is a rack app, the first thing I did was
to look into the config.ru file to learn more about the architecture
of the application, I noticed that the it uses Rack::Session::Cookie.
As you may have guessed from the name, that is the rack middleware that
dumps session data into a cookie.

The vulnerability

There are two problems with the code above.

ENV["ENTERPRISE_SESSION_SECRET"] is never set, so the secret
defaults to the value above. You can sign arbitrary cookies and set your
session ID as you like. However this does not help you, since the
session ID is 32 random bytes.

But you can now feed arbitrary data into Marshal.load, since
you can forge a valid signature. Unlike JSON, the Marshal format does
not only allow hashes, arrays and static types, but also ruby objects.
This allows remote code execution, as you will see now.

Crafting the exploit code

To run arbitrary code, I needed to generate input to Marshal.load that
runs my code upon deserialization. To achieve this, I need to construct
code that runs on access to the object. This is composed of two stages:

A malicious ERb template

The way .erb templates are parsed is that Erubis reads them and
generates a Erubis::Eruby object which contains the code in the
template in the @src instance variable. So if I put my code there,
I just need something to call object.result and my code will be run.

erubis=Erubis::Eruby.allocateerubis.instance_variable_set:@src,"%x{id > /tmp/pwned}; 1"# erubis.result would run the code

An evil InstanceVariableProxy

In ActiveSupport there is a convient way to tell users that things
have changed. It’s called
ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy. You can
use it to deprecate an instance variable. If you run a method on that
instance variable, it will call the new one for you and give a warning.
That was exactly what I needed. See for example this session: