Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- /*
- ratproxy
- --------
- A simple HTTP proxy to use for code audits of rich web 2.0 applications.
- Meant to detect JSON-related and other script-accessible content problems as
- you interact with the tested application and otherwise just mind your business.
- Please use this tool responsibly and in good faith. Thanks.
- Author: Michal Zalewski <lcamtuf@google.com>
- Copyright 2007, 2008 by Google Inc. All Rights Reserved.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <signal.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <string.h>
- #include <sys/wait.h>
- #include <ctype.h>
- #include <netdb.h>
- #include <openssl/md5.h>
- #include <time.h>
- #include "config.h"
- #include "types.h"
- #include "debug.h"
- #include "nlist.h"
- #include "http.h"
- #include "mime.h"
- #include "ssl.h"
- #include "string-inl.h"
- static struct naive_list domains; /* Domains to keep track of */
- static _u8 check_png, /* Check all PNG files? */
- dump_urls, /* Dump all visited URLs? */
- all_files, /* Report all file inclusions */
- all_flash, /* Report all Flash documents */
- get_xsrf, /* Report GET XSRF status */
- bad_js, /* Report risky Javascript */
- all_post, /* Report all POST requests */
- all_cookie, /* Report all cookie URLs */
- picky_cache, /* Be picky about chdrs */
- use_double, /* Make 2, not 1 extra req */
- try_attacks, /* Validate XSRF/XSS suspects */
- fix_attacks, /* Correct XSRF/XSS fallout */
- log_active, /* Log cross-domain content */
- log_mixed, /* Log mixed content */
- use_any, /* Listen on any address */
- all_xss; /* Report all XSS suspects */
- static _u32 use_port = DEFAULT_PORT; /* Proxy port to listen on */
- _u8* use_proxy; /* Upstream proxy */
- _u8* trace_dir; /* Trace directory */
- _u32 proxy_port = 8080; /* Upstream proxy port */
- _u8 use_len; /* Use length, not cksum */
- static FILE* outfile; /* Output file descriptor */
- /* Display usage information */
- static void usage(_u8* argv0) {
- debug("Usage: %s [ -w logfile ] [ -v logdir ] [ -p port ] [ -d domain ] [ -P host:port ] "
- "[ -xtifkgmjscael2XCr ]\n"
- " -w logfile - write results to a specified file (default: stdout)\n"
- " -v logdir - write HTTP traces to a specified directory (default: none)\n"
- " -p port - listen on a custom TCP port (default: 8080)\n"
- " -d domain - analyze requests to specified domains only (default: all)\n"
- " -P host:port - use upstream proxy for all requests (format host:port)\n"
- " -r - accept remote connections (default: 127.0.0.1 only)\n"
- " -l - use response length, not checksum, for identity check\n"
- " -2 - perform two, not one, page identity check\n"
- " -e - perform pedantic caching headers checks\n"
- " -x - log all XSS candidates\n"
- " -t - log all directory traversal candidates\n"
- " -i - log all PNG files served inline\n"
- " -f - log all Flash applications for analysis (add -v to decompile)\n"
- " -s - log all POST requests for analysis\n"
- " -c - log all cookie setting URLs for analysis\n"
- " -g - perform XSRF token checks on all GET requests\n"
- " -j - report on risky Javascript constructions\n"
- " -m - log all active content referenced across domains\n"
- " -X - disruptively validate XSRF, XSS protections\n"
- " -C - try to auto-correct persistent side effects of -X\n"
- " -k - flag HTTP requests as bad (for HTTPS-only applications)\n"
- " -a - indiscriminately report all visited URLs\n\n"
- "Example settings suitable for most tests:\n"
- " 1) Low verbosity : -v <outdir> -w <outfile> -d <domain> -lfscm\n"
- " 2) High verbosity : -v <outdir> -w <outfile> -d <domain> -lextifscgjm\n"
- " 3) Active testing : -v <outdir> -w <outfile> -d <domain> -XClfscm\n\n"
- "Multiple -d options are allowed. Consult the documentation for more.\n", argv0);
- exit(1);
- }
- #define sayf(x...) fprintf(outfile,x)
- /* Check hostname against a list of tracked ones. */
- static _u8 host_ok(_u8* hname) {
- _u32 i, hlen;
- /* If no domains defined, accept all. */
- if (!domains.c) return 1;
- hlen = strlen(hname);
- for (i=0;i<domains.c;i++) {
- _u32 dlen = strlen(domains.v[i]);
- if (dlen > hlen) continue;
- if (!strcmp(hname + (hlen - dlen), domains.v[i])) return 1;
- }
- return 0;
- }
- /* Test for XSSable payload */
- static _u8 xss_field(_u8* value, _u8 head) {
- _u32 c = 0;
- if (strlen(value) < (head ? MIN_XSS_HEAD : MIN_XSS_LEN)) return 0;
- while (no_xss_text[c]) {
- if (!strncasecmp(value,no_xss_text[c],strlen(no_xss_text[c]))) return 0;
- c++;
- }
- return 1;
- }
- #define MOD_PRED 1
- #define MOD_AUTH 2
- #define MOD_ECHO 4
- #define NOECHO(_x) ((_x) & ~MOD_ECHO)
- #define ECHO(_x) ((_x) & MOD_ECHO)
- /* Check if the page has a predictable URL, user-specific content, echoed parameters. */
- static _u8 get_modifiers(struct http_request* req, struct http_response* res) {
- FILE *server;
- static struct http_response* mini = 0;
- _u32 ret = 0;
- _u32 fno = 0;
- _u32 i;
- /* Test for echoed query parameters in response body... */
- if (res->is_text && res->payload_len) {
- #ifdef CHECK_ECHO_PATH
- if (req->path && strstr(res->payload,req->path)) ret = MOD_ECHO;
- #endif /* CHECK_ECHO_PATH */
- for (i=0;!ret && i<req->p.c;i++)
- if (!req->p.fn[i][0] && xss_field(req->p.v2[i],0) && strstr(res->payload,req->p.v2[i]))
- { ret = MOD_ECHO; break; }
- }
- /* ...and in HTTP header values. */
- for (i=0;!ret && i<req->p.c;i++)
- if (!req->p.fn[i][0] && xss_field(req->p.v2[i],1)) {
- _u32 j;
- for (j=0;j<res->h.c;j++)
- if (strstr(res->h.v2[j],req->p.v2[i])) { ret = MOD_ECHO; break; }
- }
- /* Check for predictable URLs. */
- if (!req->xsrf_safe) ret |= MOD_PRED;
- /* Check for authentication. */
- /* Some field names may override our checks. */
- while (auth_fields[fno]) {
- _u32 i;
- for (i=0;i<req->p.c;i++) {
- if (auth_fields[fno][0] == '=') {
- if (!strcasecmp(req->p.v1[i],auth_fields[fno] + 1)) return ret | MOD_AUTH;
- } else {
- if (rp_strcasestr(req->p.v1[i],auth_fields[fno])) return ret | MOD_AUTH;
- }
- }
- fno++;
- }
- /* No cookies? Then do not resend. */
- if (!req->cookies.c) return ret;
- /* Try to verify that the request requires authentication by replaying it with
- no cookies. This should have no side effects in sanely written applications. */
- /* TODO: We should continue also if custom HTTP headers or HTTP auth is detected;
- we currently bail out on this, however. */
- if (!mini) {
- server = open_server_complete(0,req);
- if (req->from_ssl) {
- ssl_setup();
- ssl_start(fileno(server),-1);
- fclose(server);
- server = fdopen(ssl_srv_tap,"w+");
- }
- mini = send_request(0,server,req,1);
- if (req->from_ssl) ssl_shutdown();
- checksum_response(mini);
- if (use_double) {
- _u64 temp = mini->cksum;
- /* ...and do it again! */
- server = open_server_complete(0,req);
- if (req->from_ssl) {
- ssl_setup();
- ssl_start(fileno(server),-1);
- fclose(server);
- server = fdopen(ssl_srv_tap,"w+");
- }
- mini = send_request(0,server,req,1);
- if (req->from_ssl) ssl_shutdown();
- checksum_response(mini);
- /* If checksum changes over time, give up. */
- if (temp != mini->cksum) mini->cksum = res->cksum;
- }
- }
- if (mini->cksum != res->cksum) ret |= MOD_AUTH;
- return ret;
- }
- /* DISRUPTIVE CHECK: Try removing XSRF protection, see what happens. */
- static void try_replay_xsrf(struct http_request* req, struct http_response* res) {
- FILE *server;
- struct http_response* not;
- struct http_request r2;
- _u32 i;
- _u8 got_token = 0;
- if (!req->xsrf_safe || req->authsub) return;
- memcpy(&r2,req,sizeof(struct http_request));
- /* Duplicate parameter value pointer array, so that we may modify it at will. */
- r2.p.v2 = malloc(r2.p.c * sizeof(_u8*));
- if (!r2.p.v2) fatal("out of memory");
- memcpy(r2.p.v2,req->p.v2,r2.p.c * sizeof(_u8*));
- /* Do not run contains_token() checks on file fields. */
- for (i=0;i<req->p.c;i++)
- if (!req->p.fn[i][0] && contains_token(req->p.v1[i],req->p.v2[i])) {
- got_token = 1;
- r2.p.v2[i] = "0"; /* Clobber value. */
- }
- /* Ooops! */
- if (!got_token) return;
- /* Rebuild query / payload strings. */
- reconstruct_request(&r2);
- server = open_server_complete(0,req);
- if (req->from_ssl) {
- ssl_setup();
- ssl_start(fileno(server),-1);
- fclose(server);
- server = fdopen(ssl_srv_tap,"w+");
- }
- not = send_request(0,server,&r2,0);
- if (req->from_ssl) ssl_shutdown();
- /* Fix potential side effects of our request. */
- if (fix_attacks) {
- server = open_server_complete(0,req);
- if (req->from_ssl) {
- ssl_setup();
- ssl_start(fileno(server),-1);
- fclose(server);
- server = fdopen(ssl_srv_tap,"w+");
- }
- send_request(0,server,req,0); /* sink response */
- if (req->from_ssl) ssl_shutdown();
- }
- checksum_response(not);
- /* Clobbering all XSRF-ish tokens caused no change? */
- if (not->cksum == res->cksum) req->xsrf_safe = 0;
- }
- /* DISRUPTIVE CHECK: Try injecting XSS payload, see what happens. */
- static _u8 try_replay_xss(struct http_request* req, struct http_response* res) {
- FILE *server;
- struct http_response* not;
- struct http_request r2;
- _u32 i;
- _u8 got_candidate = 0;
- _u8* cur;
- _u8 htmlstate = 0, htmlurl = 0;
- if (!res->is_text) return 0;
- memcpy(&r2,req,sizeof(struct http_request));
- /* Duplicate parameter value pointer array, so that we may modify it at will. */
- r2.p.v2 = malloc(r2.p.c * sizeof(_u8*));
- if (!r2.p.v2) fatal("out of memory");
- memcpy(r2.p.v2,req->p.v2,r2.p.c * sizeof(_u8*));
- for (i=0;i<req->p.c;i++)
- if (!req->p.fn[i][0] && xss_field(req->p.v2[i],0) && strstr(res->payload,req->p.v2[i])
- #ifndef XSS_XSRF_TOKENS
- && !contains_token(req->p.v1[i],req->p.v2[i])
- #endif /* !XSS_XSRF_TOKENS */
- ) {
- /* This does not account for all scenarios possible XSS scenarios, but is a
- pretty good all-around string. Since we want to minimize the number of
- requests generated, it will have to do. */
- r2.p.v2[i] = "qg:qg qg=-->qg\"qg>qg'qg>qg+qg<qg>";
- got_candidate = 1;
- }
- if (!got_candidate) return 0;
- /* Rebuild query / payload strings. */
- reconstruct_request(&r2);
- server = open_server_complete(0,req);
- if (req->from_ssl) {
- ssl_setup();
- ssl_start(fileno(server),-1);
- fclose(server);
- server = fdopen(ssl_srv_tap,"w+");
- }
- not = send_request(0,server,&r2,0);
- if (req->from_ssl) ssl_shutdown();
- /* Fix potential side effects of our request. */
- if (fix_attacks) {
- server = open_server_complete(0,req);
- if (req->from_ssl) {
- ssl_setup();
- ssl_start(fileno(server),-1);
- fclose(server);
- server = fdopen(ssl_srv_tap,"w+");
- }
- send_request(0,server,req,0); /* sink response */
- if (req->from_ssl) ssl_shutdown();
- }
- if (!not->payload_len) return 0;
- detect_mime(not);
- if (not->is_text)
- detect_charset(not);
- /* Do some minimal and dumbed down HTML parsing on the response to detect q9g
- strings in dangerous configurations. */
- #define HS_IN_TAG 1
- #define HS_IN_DBLQ 2
- #define HS_IN_SNGQ 4
- #define HS_IN_COMM 8
- #define HS_IN_CDATA 16
- cur = not->payload;
- while (*cur) {
- /* Detect successful XSS attempts... */
- if (!strncasecmp(cur,"qg",2)) {
- /* <tag foo=bar onload=...> */
- if (htmlstate == HS_IN_TAG && !strncasecmp(cur+2," qg=",4)) return 1;
- /* <tag src=foo:bar...> */
- if (htmlurl && !strncasecmp(cur+2,":qg",3)) return 1;
- /* <tag><script>... */
- if (htmlstate == 0 && !strncasecmp(cur+2,"<qg",3)) return 1;
- /* <tag>+ADw-script+AD4-... */
- if (htmlstate == 0 && (!not->charset || not->bad_cset) && !strncasecmp(cur+2,"+qg",3)) return 1;
- /* <tag foo="bar"onload=...> */
- if (htmlstate == (HS_IN_TAG|HS_IN_DBLQ) && !strncasecmp(cur+2,"\"qg",3)) return 1;
- /* <tag foo='bar'onload=...> */
- if (htmlstate == (HS_IN_TAG|HS_IN_SNGQ) && !strncasecmp(cur+2,"'qg",3)) return 1;
- } else {
- /* Handle CDATA blocks */
- if (htmlstate == 0 && !strncasecmp(cur,"<![CDATA[",9)) { htmlstate = HS_IN_CDATA; cur += 9; continue; }
- if (htmlstate == HS_IN_CDATA && !strncmp(cur,"]]>",3)) { htmlstate = 0; cur += 3; continue; }
- /* Handle <!-- --> blocks (this depends on rendering mode, but hey). */
- if (htmlstate == 0 && !strncmp(cur,"<!--",4)) { htmlstate = HS_IN_COMM; cur += 4; continue; }
- if (htmlstate == HS_IN_COMM && !strncmp(cur,"-->",3)) { htmlstate = 0; cur += 3; continue; }
- /* Detect what could pass for tag opening / closure... */
- if (htmlstate == 0 && *cur == '<' && (isalpha(cur[1]) || cur[1] == '!' || cur[1] == '?')) { htmlstate = HS_IN_TAG; cur++; continue; }
- if (htmlstate == HS_IN_TAG && *cur == '>') { htmlstate = 0; htmlurl = 0; cur++; continue; }
- /* Handle double quotes around HTML parameters */
- if (htmlstate == HS_IN_TAG && cur[-1] == '=' && *cur == '"') { htmlstate |= HS_IN_DBLQ; cur++; continue; }
- if (htmlstate == (HS_IN_TAG|HS_IN_DBLQ) && *cur == '"') { htmlstate = HS_IN_TAG; cur++; continue; }
- /* Handle single quotes around HTML parameters */
- if (htmlstate == HS_IN_TAG && cur[-1] == '=' && *cur == '\'') { htmlstate |= HS_IN_SNGQ; cur++; continue; }
- if (htmlstate == (HS_IN_TAG|HS_IN_SNGQ) && *cur == '\'') { htmlstate = HS_IN_TAG; cur++; continue; }
- /* Special handling for SRC= and HREF= locations. */
- if (htmlstate == HS_IN_TAG && isspace(cur[-1]) && !strncasecmp(cur,"href=",5)) {
- htmlurl = 1; cur += 5; continue;
- }
- if (htmlstate == HS_IN_TAG && isspace(cur[-1]) && !strncasecmp(cur,"src=",4)) {
- htmlurl = 1; cur += 4; continue;
- }
- /* Cancel mode if any character other than ", ', or qg: URL is encountered. */
- if (htmlurl) htmlurl = 0;
- }
- cur++;
- }
- /* So, no XSS? Bummer. */
- return 0;
- }
- /* Check for publicly cacheable documents. Returns 0 if not public,
- 1 if apparently meant to be public, 2 if partly protected. */
- static _u8 is_public(struct http_request* req, struct http_response* res) {
- _u8 http10intent;
- /* "Expires" and "Pragma" should say the same. */
- if (res->pr10intent && res->ex10intent && res->pr10intent != res->ex10intent) return 2;
- http10intent = res->ex10intent ? res->ex10intent : res->pr10intent;
- /* HTTP/1.0 and HTTP/1.1 intents should say the same. */
- if (http10intent && res->cc11intent && http10intent != res->cc11intent) return 2;
- /* [Picky] HTTP/1.0 and HTTP/1.1 intents should not appear at all, or appear at once */
- if (picky_cache && (http10intent ^ res->cc11intent)) {
- if (strcmp(req->method,"GET")) return 0; /* Non-GET requests won't be cached. */
- return 2;
- }
- if (res->cc11intent == INTENT_PRIV || http10intent == INTENT_PRIV) return 0;
- /* No interest in making this document private was expressed... */
- if (strcmp(req->method,"GET")) return 0; /* Non-GET requests won't be cached. */
- return 1;
- }
- static _u8 dump_fn[1024];
- static _u8 dumped_already;
- /* Save trace data to file, if requested. */
- static _u8* save_trace(struct http_request* req, struct http_response* res) {
- _s32 f;
- _u32 i;
- FILE* out;
- if (!trace_dir) return "-";
- /* Do not save the same request twice. */
- if (dumped_already) return dump_fn;
- dumped_already = 1;
- sprintf(dump_fn,"%.512s/%08x-%04x.trace",trace_dir,(_u32)time(0),getpid());
- f = open(dump_fn, O_WRONLY | O_CREAT | O_EXCL, 0600);
- if (f < 0) {
- debug(">>> Unable to open trace file '%s'! <<<\n",dump_fn);
- return "-";
- }
- out = fdopen(f,"w");
- fprintf(out,"== REQUEST TO %s:%u (%u headers, %u byte payload) ==\n\n%s /%s%s%s HTTP/1.0\n",
- req->host, req->port, req->h.c, req->payload_len,
- req->method, req->path, req->query ? "?" : "", req->query ? req->query : (_u8*)"");
- for (i=0;i<req->h.c;i++)
- fprintf(out,"%s: %s\n", req->h.v1[i], req->h.v2[i]);
- fprintf(out,"\n");
- if (req->payload_len)
- fwrite(req->payload,req->payload_len > MAXTRACEITEM ? MAXTRACEITEM : req->payload_len,1,out);
- if (req->payload_len > MAXTRACEITEM)
- fprintf(out,"\n*** DATA TRUNCATED DUE TO SIZE LIMITS ***");
- fprintf(out,"\n\n== SERVER RESPONSE (%u headers, %u byte payload, detected MIME %s) ==\n\n"
- "HTTP/1.0 %u \n",
- res->h.c, res->payload_len, res->mime_type ? res->mime_type : (_u8*)"(none)",
- res->code);
- for (i=0;i<res->h.c;i++)
- fprintf(out,"%s: %s\n", res->h.v1[i], res->h.v2[i]);
- fprintf(out,"\n");
- if (res->payload_len)
- fwrite(res->payload,res->payload_len > MAXTRACEITEM ? MAXTRACEITEM : res->payload_len,1,out);
- if (res->payload_len > MAXTRACEITEM)
- fprintf(out,"\n*** DATA TRUNCATED DUE TO SIZE LIMITS ***");
- fprintf(out,"\n\n== END OF TRANSACTION ==\n");
- fclose(out);
- close(f);
- return dump_fn;
- }
- /* Use Flare to decode Flash file, if available. */
- static void decode_flash(struct http_response* res) {
- _s32 f, pid;
- _u8 tmp[1024];
- struct stat st;
- if (!dumped_already || !res->payload_len) return; /* ? */
- sprintf(tmp,"%s.swf",dump_fn);
- f = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0600);
- if (f < 0) return;
- write(f, res->payload, res->payload_len);
- close(f);
- if (!(pid = fork())) {
- /* Flare is way too noisy, let's close stderr. */
- close(2);
- execl("./flare","flare",tmp,NULL);
- execlp("flare","flare",tmp,NULL);
- exit(1);
- }
- if (pid > 0) waitpid(pid, (int*)&f, 0);
- unlink(tmp);
- sprintf(tmp,"%s.flr",dump_fn);
- if (stat(tmp,&st) || !st.st_size) unlink(tmp);
- /* So we should have a non-zero length .flr file next to a trace file
- now; ratproxy-report.sh will detect this. */
- }
- /* A "fuzzy" comparator to avoid reporting "refresher" cookies where some minor parameters
- were changed as new cookie arrivals; but to detect blanking or other major overwrites. */
- static _u8 unique_cookies(struct naive_list2* reqc, struct naive_list2* resc) {
- _u32 i,j;
- if (!resc->c) return 0; /* No cookies set at all. */
- if (!reqc->c) return 1; /* All set cookies must be new. */
- for (i=0;i<resc->c;i++) {
- for (j=0;j<reqc->c;j++) {
- if (!strcasecmp(resc->v1[i],reqc->v1[j]) && /* Same name */
- strlen(resc->v2[i]) == strlen(reqc->v2[j])) /* Same length */
- break; /* ...must be a refresher cookie. */
- }
- /* No refresher cookie matches for one cookie? Good enough. */
- if (j == reqc->c) return 1;
- }
- /* All cookies were refreshers. */
- return 0;
- }
- /* Cookie renderer, for reporting purposes. */
- static _u8* make_cookies(struct naive_list2* reqc, struct naive_list2* resc) {
- _u8* ret = 0;
- _u32 i,j;
- _u8 had_some = 0;
- if (!resc->c) return "-";
- #define ALLOC_STRCAT(dest,src) do { \
- _u32 _sl = strlen(src); \
- _u32 _dl = 0; \
- if (dest) _dl = strlen(dest); \
- dest = realloc(dest,_sl + _dl + 1); \
- if (!dest) fatal("out of memory"); \
- strcpy(dest + _dl, src); \
- } while (0)
- for (i=0;i<resc->c;i++) {
- /* Render only newly set cookies! */
- for (j=0;j<reqc->c;j++) {
- if (!strcasecmp(resc->v1[i],reqc->v1[j]) && /* Same name */
- strlen(resc->v2[i]) == strlen(reqc->v2[j])) /* Same length */
- break; /* ...must be a refresher cookie. */
- }
- if (j == reqc->c) {
- if (!had_some) had_some = 1; else ALLOC_STRCAT(ret,"; ");
- ALLOC_STRCAT(ret,resc->v1[i]);
- ALLOC_STRCAT(ret,"=");
- ALLOC_STRCAT(ret,resc->v2[i]);
- }
- }
- return ret ? ret : (_u8*)"-";
- }
- /* Check for safe JSON prologues. */
- static _u8 is_json_safe(_u8* str) {
- _u32 i = 0;
- while (json_safe[i]) {
- if (!strncmp(str,json_safe[i],strlen(json_safe[i]))) return 1;
- i++;
- }
- return 0;
- }
- /* Check for scripts that appear to be standalone or empty (as opposed to
- JSON-like dynamically generated response snippets for on-page execution). */
- static _u8 standalone_script(_u8* str) {
- if (!str) return 1; /* Empty */
- skip_more:
- while (*str && isspace(*str)) str++;
- if (!strncmp(str,"/*",2)) {
- str = strstr(str+2, "*/");
- if (!str) return 1; /* Empty */
- goto skip_more;
- }
- if (!strncmp(str,"//",2)) {
- str += 2;
- while (*str && strchr("\r\n",*str)) str++;
- goto skip_more;
- }
- if (*str == '(') { str++; goto skip_more; }
- if (!*str) return 1; /* Empty */
- /* This is not very scientific - in fact, there is no good way to
- settle this - but should be a pretty good predictor in most cases. */
- if (!strncasecmp(str,"var",3) && isspace(str[3])) return 1; /* Script */
- if (!strncasecmp(str,"function",8) && isspace(str[8])) return 1; /* Script */
- return 0; /* Probably JSON */
- }
- /* The main request handling and routing routine. */
- static void handle_client(FILE* client) {
- FILE *server;
- struct http_request* req;
- struct http_response* res;
- _u8 m;
- _u32 i;
- _u8 got_xss = 0;
- #define BEST_MIME (res->sniffed_mime ? res->sniffed_mime : \
- (res->mime_type ? res->mime_type : (_u8*)""))
- /* TODO: Ideally, S() shouldn't do HTML escaping in machine
- output (just filter | and control chars); but this requires
- ratproxy-report.sh to be reworked. */
- // Request printer macros - since most of the data does not change.
- #define SHOW_REF_MSG(warn,mesg,mod) \
- sayf("%u|%u|%s|-|%u|%u|%s|http%s://%s:%u/%s%s%s|-|%s|-|%s|-|-|-\n", \
- warn, mod, mesg, res->code, res->payload_len, res->mime_type ? \
- res->mime_type : (_u8*)"-", req->from_ssl ? "s" : "", S(req->host,0), req->port, \
- S(req->path,0), req->query ? "?" : "", req->query ? \
- S(req->query,0) : (_u8*)"", save_trace(req,res), S(req->referer,0))
- #define SHOW_MSG(warn,mesg,off_par,mod) \
- sayf("%u|%u|%s|%s|%u|%u|%s|%s|%s|%s|%s|http%s://%s:%u/%s%s%s|%s|%s|%s\n", \
- warn, mod ,mesg, off_par ? S(off_par,0) : (_u8*)"-", \
- res->code, res->payload_len, \
- res->mime_type ? S(res->mime_type,0) : (_u8*)"-", \
- res->sniffed_mime ? S(res->sniffed_mime,0) : (_u8*)"-", \
- res->charset ? S(res->charset,0) : (_u8*)"-", \
- save_trace(req,res), \
- S(req->method,0), req->from_ssl ? "s" : "", S(req->host,0), \
- req->port, S(req->path,0), req->query ? "?" : "", \
- req->query ? S(req->query,0) : (_u8*)"", \
- S(make_cookies(&req->cookies,&res->cookies),0), \
- req->payload_len ? S(stringify_payload(req),0) : (_u8*)"-", \
- res->payload_len ? S(res->payload,0) : (_u8*)"-")
- /* First, let's collect and complete the request */
- req = collect_request(client,0,0);
- server = open_server_complete(client, req);
- if (req->is_connect) {
- ssl_setup();
- ssl_start(fileno(server),fileno(client));
- fclose(client); fclose(server);
- client = fdopen(ssl_cli_tap,"w+");
- server = fdopen(ssl_srv_tap,"w+");
- if (!client || !server) fatal("out of memory");
- req = collect_request(client, req->host, req->port);
- req->is_connect = 1;
- }
- res = send_request(client, server, req, 0);
- send_response(client,res);
- if (req->from_ssl) ssl_shutdown();
- /* Now, if the target is not within the set of tested domains,
- there are several things we want to check if it originated
- from within the tested locations. */
- if (!host_ok(req->host)) {
- _u8 *refq;
- if (!req->ref_host) goto skip_tests;
- /* Requests between non-analyzed sites do not concern us. */
- if (!host_ok(req->ref_host)) goto skip_tests;
- /* Referer token leakage test: contains_token() succeeds on "Referer" query */
- if ((refq=strchr(req->referer,'?'))) {
- struct naive_list_p p = { 0, 0, 0, 0, 0 };
- _u32 i;
- parse_urlencoded(&p,refq + 1);
- for (i=0;i<p.c;i++)
- if (contains_token(p.v1[i],p.v2[i])) break;
- if (i != p.c)
- SHOW_REF_MSG(3,"Referer may leak session tokens",1);
- }
- /* Cross-domain script inclusion check */
- detect_mime(res);
- if (rp_strcasestr(BEST_MIME,"script") ||
- !strcasecmp(BEST_MIME,"application/json")|| !strcasecmp(BEST_MIME,"text/css"))
- SHOW_REF_MSG(3,"External code inclusion",1);
- /* POST requests between domains - outgoing. */
- if (strcmp(req->method,"GET")) {
- SHOW_REF_MSG(2,"Cross-domain POST requests",0);
- } else if (log_active) {
- i = 0;
- while (active_mime[i]) {
- if (!strcasecmp(BEST_MIME,active_mime[i])) {
- SHOW_REF_MSG(1,"References to external active content",1);
- break;
- }
- i++;
- }
- }
- goto skip_tests;
- }
- /* All right, everything below pertains to checks on URLs within
- the tested domain. Let's do some basic information gathering first. */
- checksum_response(res);
- detect_mime(res);
- if (res->is_text)
- detect_charset(res);
- if (dump_urls) SHOW_MSG(0,"!All visited URLs",0,0);
- /* If requested to do so, we need to log non-HTTPS traffic and
- prioritize it depending on document type. */
- if (log_mixed && !req->from_ssl) {
- m = get_modifiers(req,res);
- if (!strcasecmp(BEST_MIME,"text/html") || rp_strcasestr(BEST_MIME,"script") ||
- !strcasecmp(BEST_MIME,"application/json") ||
- !strcasecmp(BEST_MIME,"text/css") || !strcasecmp(BEST_MIME,"application/xhtml+xml"))
- SHOW_MSG(2,"Potential mixed content",0,m);
- else SHOW_MSG(0,"Potential mixed content",0,m);
- }
- /* If instructed to do so, adjust XSRF "safety" rating based on packet
- replay now. */
- if (try_attacks) try_replay_xsrf(req,res);
- /***********************
- * HEADER BASED CHECKS *
- ***********************/
- if (res->code < 200 || res->code >= 400) {
- switch (NOECHO(m = get_modifiers(req,res))) {
- /* No big deal, but warrants an investigation; more important if
- the content is user-specific. */
- case 0:
- case MOD_PRED:
- SHOW_MSG(0,"HTTP errors",0,m); break;
- case MOD_AUTH:
- case MOD_PRED | MOD_AUTH:
- SHOW_MSG(1,"HTTP errors",0,m); break;
- }
- }
- /* Detect 302 with Location: that contains req->query or req->payload,
- and lacks XSRF token? */
- if (res->location && (req->query || req->payload) && !req->xsrf_safe) {
- _u8* hname = strdup(res->location), *y;
- if (!hname) fatal("out of memory");
- if (!strncasecmp(hname,"http://",7)) hname += 7; else
- if (!strncasecmp(hname,"https://",8)) hname += 8;
- y = hname;
- while (isalnum(*y) || *y == '-' || *y == '.') y++;
- *y = 0;
- if (hname[0] && ((req->query && rp_strcasestr(req->query,hname)) ||
- (req->payload && rp_strcasestr(req->payload,hname)))) {
- SHOW_MSG(3,"HTTP redirector",0,1);
- }
- }
- /* If not a HTTP redirector, examine for HTML redirection anyway */
- if (!res->location && (req->query || req->payload) && res->payload && !req->xsrf_safe) {
- _u8* mref=rp_strcasestr(res->payload,"HTTP-EQUIV=\"Refresh\"");
- _u8* hname = mref ? rp_strcasestr(mref + 20, ";URL=") : 0;
- if (hname) {
- _u8* mrefend = strchr(mref + 20,'>'), *y;
- if (mrefend && hname < mrefend) {
- hname = strdup(hname + 5);
- if (!hname) fatal("out of memory");
- if (!strncasecmp(hname,"http://",7)) hname += 7; else
- if (!strncasecmp(hname,"https://",8)) hname += 8;
- y = hname;
- while (isalnum(*y) || *y == '-' || *y == '.') y++;
- *y = 0;
- if (hname[0] && ((req->query && rp_strcasestr(req->query,hname)) ||
- (req->payload && rp_strcasestr(req->payload,hname)))) {
- SHOW_MSG(3,"HTML META redirector",0,1);
- }
- }
- }
- }
- if (req->multipart) {
- m = get_modifiers(req,res);
- SHOW_MSG(0,"File upload forms",0,m);
- }
- if (all_post && req->payload && strcasecmp(req->method,"GET")) {
- m = get_modifiers(req,res);
- SHOW_MSG(0,"All POST requests",0,m);
- }
- if (unique_cookies(&req->cookies,&res->cookies) && !req->xsrf_safe && (req->payload || req->query)) {
- m = get_modifiers(req,res);
- SHOW_MSG(2,"Cookie issuer with no XSRF protection",0,m);
- /* TODO: Maybe check if query data copied over to cookies. */
- }
- if (all_cookie && unique_cookies(&req->cookies,&res->cookies)) {
- m = get_modifiers(req,res);
- SHOW_MSG(0,"All cookie setting URLs",0,m);
- }
- /* If there's a request that requires authentication and accept parameters,
- it should probably employ anti-XSRF protection of some sort. */
- if (!req->xsrf_safe && (req->payload || req->query)) {
- m = get_modifiers(req,res);
- if (m & MOD_AUTH) {
- if (!strcasecmp(req->method,"GET")) {
- if (get_xsrf)
- SHOW_MSG(0,"GET query with no XSRF protection",0,m);
- } else
- SHOW_MSG(3,"POST query with no XSRF protection",0,m);
- } else {
- /* POST requests that do not require authentication are interesting,
- though not necessarily very troubling. */
- if (strcasecmp(req->method,"GET"))
- SHOW_MSG(1,"POST query with no XSRF protection",0,m);
- }
- }
- if (res->has_multiple) {
- /* Duplicate Content-Type or Content-Disposition headers are a sure
- way to get into trouble. */
- switch (NOECHO(m = get_modifiers(req,res))) {
- case 0:
- SHOW_MSG(1,"Ambiguous HTTP content headers",0,m); break;
- case MOD_PRED:
- case MOD_AUTH:
- SHOW_MSG(2,"Ambiguous HTTP content headers",0,m); break;
- case MOD_PRED | MOD_AUTH:
- SHOW_MSG(3,"Ambiguous HTTP content headers",0,m); break;
- }
- }
- /* Unusual, but hey, let's report it because we can. */
- if (res->has_badclen)
- SHOW_MSG(3,"Misstated Content-Length",0,0);
- /* POST requests that pass auth tokens between domains. If coming
- from an excluded domain, this is more important. */
- if (req->ref_host && strcmp(req->method,"GET") &&
- strcasecmp(req->host,req->ref_host)) {
- if (!host_ok(req->ref_host))
- SHOW_REF_MSG(2,"Cross-domain POST requests",0);
- else
- SHOW_REF_MSG(1,"Cross-domain POST requests",0);
- }
- /* Report caching headers issues (but only once!) */
- if (!req->from_ssl && unique_cookies(&req->cookies,&res->cookies) && is_public(req,res)) {
- switch (NOECHO(m = get_modifiers(req,res))) {
- case 0:
- case MOD_AUTH:
- SHOW_MSG(1,"Bad caching headers","cacheable SetCookie",m);
- break;
- case MOD_PRED:
- case MOD_AUTH | MOD_PRED:
- SHOW_MSG(3,"Bad caching headers","cacheable SetCookie",m);
- break;
- }
- } else if (!req->from_ssl && is_public(req,res) == 2 && res->payload_len && res->code < 300) {
- m = get_modifiers(req,res);
- if (NOECHO(m) == (MOD_AUTH | MOD_PRED)) SHOW_MSG(3,"Bad caching headers","Expires/Date/Cache-Control mismatch",m);
- else if (NOECHO(m) == MOD_AUTH) SHOW_MSG(2,"Bad caching headers","Expires/Date/Cache-Control mismatch",m);
- }
- /************************
- * PAYLOAD BASED CHECKS *
- ************************/
- /* If the document is empty, bail out (everything below relies on non-NULL res->payload). */
- if (!res->payload_len) goto skip_tests;
- /* Cross-domain files are always worth a look. */
- if ((strstr(req->path,"crossdomain.xml") || strstr(req->path,"clientaccesspolicy.xml")) &&
- (strstr(res->payload,"<cross-domain-policy>") || strstr(res->payload,"<access-policy>"))) {
- m = get_modifiers(req,res);
- SHOW_MSG(1,"Cross-domain access policy",0,m);
- }
- if (res->is_text && (!res->charset || res->bad_cset)) {
- /* Missing charsets and typos lead to UTF-7 cross-site scripting. */
- if (strcasecmp(BEST_MIME,"text/css")) {
- /* Cases where content is echoed back are higher risk, but we care
- about stored attacks too. */
- switch (NOECHO(m = get_modifiers(req,res))) {
- case 0:
- SHOW_MSG(ECHO(m) ? 3 : 1,"Bad or no charset declared for renderable file",0,m); break;
- case MOD_PRED:
- case MOD_AUTH:
- SHOW_MSG(ECHO(m) ? 3 : 1,"Bad or no charset declared for renderable file",0,m); break;
- case MOD_PRED | MOD_AUTH:
- SHOW_MSG(ECHO(m) ? 3 : 2,"Bad or no charset declared for renderable file",0,m); break;
- }
- }
- }
- if (res->mime_type && !strcasecmp(res->mime_type,"text/plain")) {
- /* Modern interactive websites have very few reasons to serve
- text/plain documents, and if these documents are user-controlled,
- content sniffing can lead to XSS. */
- /* Let's just ignore text/css; the next check will catch it anyway,
- and it's nearly guaranteed to be harmless. */
- if (strcasecmp(BEST_MIME,"text/css"))
- switch (NOECHO(m = get_modifiers(req,res))) {
- case 0:
- SHOW_MSG(1,"MIME type set to text/plain",0,m); break;
- case MOD_AUTH:
- case MOD_PRED:
- SHOW_MSG(ECHO(m) ? 2 : 1,"MIME type set to text/plain",0,m); break;
- case MOD_PRED | MOD_AUTH:
- SHOW_MSG(ECHO(m) ? 3 : 2,"MIME type set to text/plain",0,m); break;
- }
- }
- if (!res->mime_type) {
- /* Having no MIME type almost always warrants scrutiny, as content
- sniffing runs rampant and may have a browser-specific outcome. */
- switch (NOECHO(m = get_modifiers(req,res))) {
- case 0:
- SHOW_MSG(1,"MIME type missing",0,m); break;
- case MOD_PRED:
- case MOD_AUTH:
- SHOW_MSG(ECHO(m) ? 2 : 1,"MIME type missing",0,m); break;
- case MOD_PRED | MOD_AUTH:
- SHOW_MSG(ECHO(m) ? 3 : 2,"MIME type missing",0,m); break;
- }
- }
- /* Let's be annoying here for initial betas, why not?. */
- if (res->payload_len > 10 && res->mime_type && !res->sniffed_mime)
- debug(">>> Failed to detect MIME type '%s' (%s:%u/%s?%s), tell lcamtuf@google.com <<<\n",
- S(res->mime_type,0), S(req->host,0), req->port, S(req->path,0), req->query ?
- S(req->query,0) : (_u8*)"");
- if (res->sniffed_mime && res->mime_type &&
- strcasecmp(res->mime_type, res->sniffed_mime)) {
- if (res->is_text) {
- /* MIME mismatch on text formats that are rendered by the browser
- is usually a major problem and may lead to XSS. */
- /* Do not be too picky about HTML - XHTML mismatches, though... */
- if (res->mime_type && res->sniffed_mime &&
- !strcasecmp(res->mime_type,"text/html") &&
- !strcasecmp(res->sniffed_mime,"application/xhtml+xml"))
- goto ignore_mime_mismatch;
- if (strcasecmp(BEST_MIME,"text/css"))
- switch (NOECHO(m = get_modifiers(req,res))) {
- case 0:
- SHOW_MSG(1,"MIME type mismatch on renderable file",0,m);
- break;
- case MOD_AUTH:
- case MOD_PRED:
- SHOW_MSG(ECHO(m) ? 2 : 1,"MIME type mismatch on renderable file",0,m); break;
- case MOD_PRED | MOD_AUTH:
- SHOW_MSG(ECHO(m) ? 3 : 2,"MIME type mismatch on renderable file",0,m); break;
- }
- } else if (!strncasecmp(BEST_MIME,"image/",6)) {
- /* Subtle mismatches with images may have disastrous effects as
- content sniffing inevitably kicks in and may lead to HTML
- parsing in EXIF or comment data.*/
- switch (NOECHO(m = get_modifiers(req,res))) {
- case 0:
- SHOW_MSG(1,"MIME type mismatch on image file",0,m); break;
- case MOD_AUTH:
- case MOD_PRED:
- SHOW_MSG(2,"MIME type mismatch on image file",0,m); break;
- case MOD_PRED | MOD_AUTH:
- SHOW_MSG(3,"MIME type mismatch on image file",0,m); break;
- }
- } else {
- if (!strcasecmp(res->mime_type,"application/octet-stream")) {
- /* Defaulting to application/octet-stream may trigger content
- sniffing. */
- switch (NOECHO(m = get_modifiers(req,res))) {
- case 0:
- SHOW_MSG(1,"Generic MIME type used",0,m); break;
- case MOD_AUTH:
- case MOD_PRED:
- SHOW_MSG(ECHO(m) ? 2 : 1,"Generic MIME type used",0,m); break;
- case MOD_PRED | MOD_AUTH:
- SHOW_MSG(ECHO(m) ? 3 : 2,"Generic MIME type used",0,m); break;
- }
- } else {
- /* Other MIME type mismatches still warrant attention, as this
- might be a result of a typo or the like. */
- switch (NOECHO(m = get_modifiers(req,res))) {
- case 0:
- case MOD_AUTH:
- case MOD_PRED:
- SHOW_MSG(1,"MIME type mismatch on binary file",0,m); break;
- case MOD_PRED | MOD_AUTH:
- SHOW_MSG(2,"MIME type mismatch on binary file",0,m); break;
- }
- }
- }
- }
- ignore_mime_mismatch:
- if ((rp_strcasestr(BEST_MIME,"script") || !strcasecmp(BEST_MIME,"application/json"))) {
- /* JSON is almost always worth inspecting - doubly so if not secured against XSRF. */
- switch (NOECHO(m = get_modifiers(req,res))) {
- case 0:
- case MOD_PRED:
- break;
- case MOD_AUTH:
- SHOW_MSG(standalone_script(res->payload) ? 0 : 1,
- "Dynamic Javascript for direct inclusion",0,m); break;
- case MOD_PRED | MOD_AUTH:
- /* TODO: Move this to a proper Javascript analyzer instead. */
- if (standalone_script(res->payload)) {
- SHOW_MSG(0,"Dynamic Javascript for direct inclusion",0,m);
- } else if (is_json_safe(res->payload)) {
- SHOW_MSG(ECHO(m) ? 1 : 0,"Dynamic Javascript for direct inclusion",0,m);
- } else {
- SHOW_MSG(ECHO(m) ? 3 : 2,"Dynamic Javascript for direct inclusion",0,m);
- }
- break;
- }
- }
- if (!strcasecmp(BEST_MIME,"image/png") && !res->is_attach) {
- switch (NOECHO(m = get_modifiers(req,res))) {
- case 0:
- case MOD_PRED:
- if (check_png) SHOW_MSG(2,"Inline PNG image",0,m); break;
- case MOD_AUTH:
- SHOW_MSG(2,"Inline PNG image",0,m); break;
- case MOD_PRED | MOD_AUTH:
- SHOW_MSG(3,"Inline PNG image",0,m); break;
- }
- }
- /* Echoed markup in a query is bad. */
- for (i=0;i<req->p.c;i++)
- if (!req->p.fn[i][0] && strchr(req->p.v2[i],'<') && strstr(res->payload,req->p.v2[i])) {
- switch (NOECHO(m = get_modifiers(req,res))) {
- case 0:
- case MOD_AUTH:
- SHOW_MSG(2,"Direct markup echoed back",req->p.v2[i],m); break;
- case MOD_PRED:
- case MOD_PRED | MOD_AUTH:
- SHOW_MSG(3,"Direct markup echoed back",req->p.v2[i],m); break;
- }
- break;
- }
- /* Non-echoed paths in query are often bad, though there are some common patterns
- of false psoitives. */
- for (i=0;i<req->p.c;i++) if (!req->p.fn[i][0] && strlen(req->p.v2[i]) < MAX_FPATH &&
- strcmp(req->p.v1[i],"utmp") /* Analytics-specific. */ ) {
- _u8* x = strchr(req->p.v2[i],'/');
- _u8* y = strchr(req->p.v2[i],'.');
- if (!x) continue; /* No slash - no problem */
- if (y && y <= x) continue; /* "www.foo.com/bar/baz.jpg" */
- if (x[1] == '/') continue; /* "http://www.foo.com/" */
- if (isdigit(x[1]) && isdigit(x[2]) && x[3] == '/') continue; /* 01/02/2007 */
- if (isdigit(x[1]) && isdigit(x[3]) && x[2] == '/') continue; /* 01/2/2007 */
- do { x++; } while (isalnum(*x) || *x == '_');
- if (*x != '/') continue; /* "text/plain" */
- if (strstr(res->payload,req->p.v2[i])) /* Text simply echoed back? */
- continue;
- switch (NOECHO(m = get_modifiers(req,res))) {
- case 0:
- case MOD_AUTH:
- SHOW_MSG(2,"File path in query parameters",req->p.v2[i],m); break;
- case MOD_PRED:
- case MOD_PRED | MOD_AUTH:
- SHOW_MSG(3,"File path in query parameters",req->p.v2[i],m); break;
- }
- /* Report only once per URL. */
- goto no_more_paths;
- }
- /* Non-echoed filenames are not necessarily evil, but worth examining. */
- if (all_files)
- for (i=0;i<req->p.c;i++) if (!req->p.fn[i][0] && strlen(req->p.v2[i]) < MAX_FPATH &&
- strcmp(req->p.v1[i],"utmp") /* Analytics-specific again. */ ) {
- _u8* x = req->p.v2[i];
- while (isalnum(*x) || *x == '_' || *x == '/') x++;
- if (*x == '.' && isalpha(x[1]) && isalpha(x[2]) && strlen(x+1) <= 5 &&
- !strstr(res->payload,req->p.v2[i])) {
- m = get_modifiers(req,res);
- SHOW_MSG(1,"File name in query parameters",req->p.v2[i],m);
- break;
- }
- }
- no_more_paths:
- /* Java method names in a query are bad. */
- for (i=0;i<req->p.c;i++) if (!req->p.fn[i][0]) {
- _u8* x = strstr(req->p.v2[i],"com.");
- if (x && isalpha(x[4]) && strchr(x+4,'.') && !strstr(res->payload,req->p.v2[i]) &&
- !strchr(x,'/')) {
- switch (NOECHO(m = get_modifiers(req,res))) {
- case 0:
- case MOD_AUTH:
- SHOW_MSG(2,"Java method call in query parameters",req->p.v2[i],m); break;
- case MOD_PRED:
- case MOD_PRED | MOD_AUTH:
- SHOW_MSG(3,"Java method call in query parameters",req->p.v2[i],m); break;
- }
- break;
- }
- }
- /* Javascript code in a query is bad; ignore alert(...) though, as this is almost
- always a sign of manual XSS testing, not a legitimate functionality. */
- for (i=0;i<req->p.c;i++) if (!req->p.fn[i][0]) {
- _u8* x = strchr(req->p.v2[i],'(');
- if (x && (x == req->p.v2[i] || isalpha(x[-1])) && strchr(x+1,')') &&
- !rp_strcasestr(req->p.v2[i],"alert(") &&
- strstr(res->payload,req->p.v2[i])) {
- switch (NOECHO(m = get_modifiers(req,res))) {
- case 0:
- case MOD_AUTH:
- SHOW_MSG(2,"Javascript code echoed back",req->p.v2[i],m); break;
- case MOD_PRED:
- case MOD_PRED | MOD_AUTH:
- SHOW_MSG(3,"Javascript code echoed back",req->p.v2[i],m); break;
- }
- break;
- }
- }
- /* SQL statement in a query is bad. */
- for (i=0;i<req->p.c;i++) if (!req->p.fn[i][0]) {
- _u8* x = rp_strcasestr(req->p.v2[i],"SELECT");
- if (x && rp_strcasestr(x+1,"FROM") && !strstr(res->payload,req->p.v2[i])) {
- switch (NOECHO(m = get_modifiers(req,res))) {
- case 0:
- case MOD_AUTH:
- SHOW_MSG(2,"SQL code in query parameters",req->p.v2[i],m); break;
- case MOD_PRED:
- case MOD_PRED | MOD_AUTH:
- SHOW_MSG(3,"SQL code in query parameters",req->p.v2[i],m); break;
- }
- break;
- }
- }
- /* Check for OGNL-style parameter names. */
- if (!req->non_param)
- for (i=0;i<req->p.c;i++) {
- if (!req->p.fn[i][0] && req->p.v1[i][0] && req->p.v2[i][0]) {
- _u8* x = strchr(req->p.v1[i] + 1, '.'); // 'user.lname'
- _u8* x1 = x ? strchr(x + 1, '.') : 0; // 'user.lname.foo'
- _u8* y = strchr(req->p.v1[i] + 1, '['); // 'users[0].lname'
- if (((x && x1) || y) && req->p.v1[i][0] != '[') {
- switch (NOECHO(m = get_modifiers(req,res))) {
- case 0:
- case MOD_AUTH:
- SHOW_MSG(1,"Suspicious parameter passing scheme",req->p.v1[i],m); break;
- case MOD_PRED:
- case MOD_PRED | MOD_AUTH:
- SHOW_MSG(2,"Suspicious parameter passing scheme",req->p.v1[i],m); break;
- }
- break;
- }
- }
- }
- /* Locate generic XSS candidates. */
- if (try_attacks)
- if (try_replay_xss(req,res)) {
- m = get_modifiers(req,res);
- SHOW_MSG(3,"Confirmed XSS vectors",0,m);
- got_xss = 1;
- }
- if (all_xss && !got_xss && res->is_text)
- for (i=0;i<req->p.c;i++)
- if (!req->p.fn[i][0] && xss_field(req->p.v2[i],0) && strstr(res->payload,req->p.v2[i])) {
- m = get_modifiers(req,res);
- if (!rp_strcasestr(BEST_MIME,"script") && strcasecmp(BEST_MIME,"application/json"))
- SHOW_MSG(0,"XSS candidates",req->p.v1[i],m);
- else SHOW_MSG(1,"XSS candidates (script)",req->p.v1[i],m);
- break;
- }
- for (i=0;i<req->p.c;i++)
- if (!req->p.fn[i][0] && xss_field(req->p.v2[i],1)) {
- _u32 j;
- for (j=0;j<res->h.c;j++)
- if (strstr(res->h.v2[j],req->p.v2[i])) {
- m = get_modifiers(req,res);
- SHOW_MSG(0,"Request splitting candidates",req->p.v1[i],m);
- goto xss_done;
- }
- }
- xss_done:
- /* Check for what looks like JSON with inline HTML (we skip standalone scripts,
- as they often contain static HTML to be rendered). We do some basic quote
- state tracking not to get confused by regular arithmetic. No comment
- tracking, but that shouldn't break easily. */
- if ((rp_strcasestr(BEST_MIME,"script") || !strcasecmp(BEST_MIME,"application/json")) &&
- !standalone_script(res->payload)) {
- _u8* p = res->payload, qstate = 0, got_html = 0, esc_next = 0, pv = ' ';
- do {
- if (esc_next) { esc_next = 0; continue; }
- /* TODO: This should be replaced with a proper Javascript analyzer. */
- switch (*p) {
- case '\\': esc_next = 1; break;
- case '\'': case '"':
- if (qstate == *p) qstate = 0; else if (!qstate) qstate = *p;
- break;
- case '<': if (qstate) got_html = 1; break;
- case '>': if (qstate && got_html) got_html = 2; break;
- }
- } while (got_html < 2 && (pv=*(p++)));
- if (got_html == 2) {
- switch (NOECHO(m = get_modifiers(req,res))) {
- case 0: case MOD_PRED: case MOD_AUTH:
- SHOW_MSG(1,"Markup in dynamic Javascript",0,m); break;
- case MOD_AUTH | MOD_PRED:
- SHOW_MSG(ECHO(m) ? 2 : 1,"Markup in dynamic Javascript",0,m); break;
- }
- }
- }
- if (all_flash && !strcasecmp(BEST_MIME,"application/x-shockwave-flash")) {
- m = get_modifiers(req,res);
- SHOW_MSG(0,"All Flash applications",0,m);
- if (trace_dir) decode_flash(res);
- }
- /* TODO: Add more index checks and other troubling server responses. */
- if (strstr(res->payload,">[To Parent Directory]<") ||
- strstr(res->payload,"<title>Index of /")) {
- m = get_modifiers(req,res);
- SHOW_MSG(0,"Directory indexes",0,m);
- }
- /* TODO: This should be replaced with a proper Javascript analyzer. */
- if (bad_js && res->is_text && (
- rp_strcasestr(res->payload,".write(") ||
- rp_strcasestr(res->payload,".writeln("))) {
- m = get_modifiers(req,res);
- SHOW_MSG(1,"Risky Javascript code","document.write",m);
- }
- if (bad_js && res->is_text &&
- (rp_strcasestr(res->payload,".innerHtml") ||
- rp_strcasestr(res->payload,".outerHtml"))) {
- m = get_modifiers(req,res);
- SHOW_MSG(1,"Risky Javascript code","innerHTML",m);
- }
- if (bad_js && res->is_text &&
- rp_strcasestr(res->payload,"document.referrer")) {
- m = get_modifiers(req,res);
- SHOW_MSG(2,"Risky Javascript code","document.referrer",m);
- }
- if (bad_js && res->is_text &&
- rp_strcasestr(res->payload,"document.domain")) {
- m = get_modifiers(req,res);
- SHOW_MSG(2,"Risky Javascript code","document.domain",m);
- }
- skip_tests:
- fflush(outfile);
- exit(0);
- }
- static void listen_loop(void) {
- _s32 lsock, csock, on = 1;
- _u32 x;
- static struct sockaddr_in saddr;
- lsock=socket(AF_INET, SOCK_STREAM, 0);
- if (lsock < 0) pfatal("cannot create socket");
- if (setsockopt(lsock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(_s32)) == -1)
- pfatal("cannot setsockopt()");
- saddr.sin_family = AF_INET;
- if (!use_any) {
- saddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
- } else {
- saddr.sin_addr.s_addr = htonl(INADDR_ANY);
- }
- saddr.sin_port = htons(use_port);
- x = sizeof(saddr);
- if (bind(lsock, (struct sockaddr*)&saddr, x)) pfatal("cannot bind to port");
- if (listen(lsock, 10)) pfatal("listen() failed");
- debug("[*] Proxy configured successfully. Have fun, and please do not be evil.\n");
- if (use_proxy)
- debug(" Upstream proxy is %s:%u\n",use_proxy,proxy_port);
- if (try_attacks)
- debug(" WARNING: Disruptive tests enabled. use with care.\n");
- debug("[+] Accepting connections on port %u/tcp (%s)...\n", use_port,
- use_any ? "any source" : "local only");
- while ((csock = accept(lsock, (struct sockaddr*)&saddr, &x)) >= 0) {
- /* Bury zombies */
- while (waitpid(-1,&x,WNOHANG) > 0);
- if (!fork()) {
- FILE* client;
- close(lsock);
- client = fdopen(csock,"w+");
- if (!client) fatal("fdopen() failed");
- handle_client(client);
- /* Not reached */
- exit(0);
- }
- close(csock);
- }
- pfatal("accept() failed");
- }
- int main(int argc, char** argv) {
- _s32 opt;
- _u8* x;
- signal(SIGPIPE, SIG_IGN);
- debug("ratproxy version " VERSION " by <lcamtuf@google.com>\n");
- while ((opt = getopt(argc,argv,"+w:v:p:d:P:itxgjmafske2clXCr")) > 0)
- switch (opt) {
- case 'w': {
- _s32 f;
- if (outfile) fatal("multiple -w options make no sense");
- unlink(optarg); /* Ignore errors */
- f = open(optarg,O_WRONLY|O_CREAT|O_EXCL,0600);
- if (f < 0) pfatal("cannot open log file");
- outfile = fdopen(f,"w");
- if (!outfile) pfatal("fdopen failed");
- }
- break;
- case 'v': {
- if (trace_dir) fatal("multiple -v options make no sense");
- trace_dir = optarg;
- mkdir(trace_dir,0700); /* Ignore errors */
- if (access(trace_dir,X_OK)) pfatal("cannot create -v directory");
- }
- break;
- case 'p':
- use_port = atoi(optarg);
- if (!use_port || use_port > 65535) fatal("invalid -p value");
- break;
- case 'P':
- use_proxy = optarg;
- x = strchr(optarg,':');
- if (!x) break;
- *(x++) = 0;
- proxy_port = atoi(x);
- if (!proxy_port || proxy_port > 65535) fatal("invalid proxy port");
- break;
- case '2':
- use_double = 1;
- break;
- case 'd':
- ADD(domains,optarg);
- break;
- case 'i':
- check_png = 1;
- break;
- case 'e':
- picky_cache = 1;
- break;
- case 't':
- all_files = 1;
- break;
- case 'f':
- all_flash = 1;
- break;
- case 'x':
- all_xss = 1;
- break;
- case 'g':
- get_xsrf = 1;
- break;
- case 'j':
- bad_js = 1;
- break;
- case 'l':
- use_len = 1;
- break;
- case 's':
- all_post = 1;
- break;
- case 'a':
- dump_urls = 1;
- break;
- case 'c':
- all_cookie = 1;
- break;
- case 'X':
- try_attacks = 1;
- break;
- case 'm':
- log_active = 1;
- break;
- case 'C':
- fix_attacks = 1;
- break;
- case 'k':
- log_mixed = 1;
- break;
- case 'r':
- use_any = 1;
- break;
- default:
- usage(argv[0]);
- }
- if (optind != argc) usage(argv[0]);
- if (optind == 1)
- debug("\n[!] WARNING: Running with no command-line config options specified. This is\n"
- " almost certainly not what you want, as most checks are disabled. Please\n"
- " consult the documentation or use --help for more information.\n\n");
- else if (!domains.c)
- debug("\n[!] WARNING: Running with no 'friendly' domains specified. Many cross-domain\n"
- " checks will not work. Please consult the documentation for advice.\n\n");
- if (!outfile) outfile = stdout;
- listen_loop();
- /* Not reached */
- return 0;
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement