skipfish/src/checks.c

2035 lines
58 KiB
C

/*
skipfish - injection tests
---------------------------
Author: Niels Heinen <heinenn@google.com>,
Michal Zalewski <lcamtuf@google.com>
Copyright 2009 - 2012 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.
*/
#define _VIA_CHECKS_C
#include "crawler.h"
#include "analysis.h"
#include "http_client.h"
#include "checks.h"
#include "auth.h"
static u8 inject_prologue_tests(struct pivot_desc* pivot);
static u8 inject_prologue_check(struct http_request*, struct http_response*);
static u8 dir_ips_tests(struct pivot_desc* pivot);
static u8 dir_ips_check(struct http_request*, struct http_response*);
static u8 inject_xml_tests(struct pivot_desc* pivot);
static u8 inject_xml_check(struct http_request*, struct http_response*);
static u8 inject_xss_tests(struct pivot_desc* pivot);
static u8 inject_xss_check(struct http_request*, struct http_response*);
static u8 inject_shell_tests(struct pivot_desc* pivot);
static u8 inject_shell_check(struct http_request*, struct http_response*);
static u8 inject_diff_shell_tests(struct pivot_desc* pivot);
static u8 inject_diff_shell_check(struct http_request*, struct http_response*);
static u8 inject_dir_listing_tests(struct pivot_desc* pivot);
static u8 inject_dir_listing_check(struct http_request*, struct http_response*);
static u8 inject_inclusion_tests(struct pivot_desc* pivot);
static u8 inject_inclusion_check(struct http_request*, struct http_response*);
static u8 inject_split_tests(struct pivot_desc* pivot);
static u8 inject_split_check(struct http_request*, struct http_response*);
static u8 inject_redir_tests(struct pivot_desc* pivot);
static u8 inject_redir_check(struct http_request*, struct http_response*);
static u8 inject_sql_tests(struct pivot_desc* pivot);
static u8 inject_sql_check(struct http_request*, struct http_response*);
static u8 inject_format_tests(struct pivot_desc* pivot);
static u8 inject_format_check(struct http_request*, struct http_response*);
static u8 inject_integer_tests(struct pivot_desc* pivot);
static u8 inject_integer_check(struct http_request*, struct http_response*);
static u8 put_upload_tests(struct pivot_desc* pivot);
static u8 put_upload_check(struct http_request*, struct http_response*);
static u8 inject_behavior_tests(struct pivot_desc* pivot);
static u8 inject_behavior_check(struct http_request*, struct http_response*);
static u8 param_behavior_tests(struct pivot_desc* pivot);
static u8 param_behavior_check(struct http_request*, struct http_response*);
static u8 param_ognl_tests(struct pivot_desc* pivot);
static u8 param_ognl_check(struct http_request*, struct http_response*);
static u8 xssi_tests(struct pivot_desc* pivot);
static u8 xssi_check(struct http_request*, struct http_response*);
/* The crawl structure defines the tests by combining function pointers and
flags. The values given indicate the following:
1- Amount of responses expected
2- Whether to keep requests and responses before calling the check
3- Whether the check accepted pivots with res_varies set
4- Whether the check is time sensitive
5- Whether we should scrape the response for links.
6- The type of PIVOT that the test/check accepts
7- Pointer to the function that scheduled the test(s) requests
8- Pointer to the function that checks the result
9- Whether to skip this test
At the end, inject_done() is called:
- we move on with additional tests (e.g. parameter)
- or continue with the next pivot
Point 8 allows command-line flags to toggle enabled/disabled
tests. For example, shell injection tests are not so relevant on
Windows environments so this allow them to be disabled.
*/
u32 cb_handle_cnt = 19; /* Total of checks */
u32 cb_handle_off = 4; /* Checks after the offset are optional */
static struct cb_handle cb_handles[] = {
/* Authentication check */
{ 2, 0, 0, 0, 0, 0,
CHK_SESSION, (u8*)"session check",
auth_verify_tests, auth_verify_checks, 0 },
/* Behavior checks for dirs/params */
{ BH_CHECKS, 1, 0, 0, 2, PIVOT_PARAM,
CHK_BEHAVE, (u8*)"param behavior",
param_behavior_tests, param_behavior_check, 0 },
{ 2, 1, 0, 0, 2, PIVOT_PARAM,
CHK_OGNL, (u8*)"param OGNL",
param_ognl_tests, param_ognl_check, 0 },
{ BH_CHECKS, 1, 0, 0, 2, PIVOT_DIR|PIVOT_FILE,
CHK_BEHAVE, (u8*)"inject behavior",
inject_behavior_tests, inject_behavior_check, 0 },
/* All the injection tests */
{ 2, 1, 0, 0, 2, PIVOT_DIR,
CHK_IPS, (u8*)"IPS check",
dir_ips_tests, dir_ips_check, 0 },
{ 2, 1, 0, 0, 0, PIVOT_DIR|PIVOT_SERV,
CHK_PUT, (u8*)"PUT upload",
put_upload_tests, put_upload_check, 0 },
{ 4, 1, 0, 0, 1, PIVOT_DIR|PIVOT_PARAM,
CHK_DIR_LIST, (u8*)"dir traversal",
inject_dir_listing_tests, inject_dir_listing_check, 0 },
#ifdef RFI_SUPPORT
{ 12, 1, 1, 0, 1, 0,
CHK_FI, (u8*)"file inclusion",
inject_inclusion_tests, inject_inclusion_check, 0 },
#else
{ 11, 1, 1, 0, 1, 0,
CHK_FI, (u8*)"file inclusion",
inject_inclusion_tests, inject_inclusion_check, 0 },
#endif
{ 4, 0, 1, 0, 1, 0,
CHK_XSS, (u8*)"XSS injection",
inject_xss_tests, inject_xss_check, 0 },
{ 1, 1, 1, 0, 1, 0,
CHK_XSSI, (u8*)"XSSI protection",
xssi_tests, xssi_check, 0 },
{ 0, 0, 1, 0, 1, 0,
CHK_PROLOG, (u8*)"prologue injection",
inject_prologue_tests, inject_prologue_check, 0 },
{ 2, 1, 1, 0, 1, 0,
CHK_RSPLIT, (u8*)"Header injection",
inject_split_tests, inject_split_check, 0 },
{ 5, 1, 1, 0, 1, PIVOT_PARAM,
CHK_REDIR, (u8*)"Redirect injection",
inject_redir_tests, inject_redir_check, 0 },
{ 10, 1, 0, 0, 1, 0,
CHK_SQL, (u8*)"SQL injection",
inject_sql_tests, inject_sql_check, 0 },
{ 2, 1, 0, 0, 1, 0,
CHK_XML, (u8*)"XML injection",
inject_xml_tests, inject_xml_check, 0 },
{ 12, 1, 0, 0, 1, 0,
CHK_SHELL_DIFF, (u8*)"Shell injection (diff)",
inject_diff_shell_tests, inject_diff_shell_check, 0 },
{ 12, 1, 1, 1, 1, 0,
CHK_SHELL_SPEC, (u8*)"Shell injection (spec)",
inject_shell_tests, inject_shell_check, 0 },
{ 2, 1, 0, 0, 1, 0,
CHK_FORMAT, (u8*)"format string",
inject_format_tests, inject_format_check, 1 },
{ 9, 1, 0, 0, 1, 0,
CHK_INTEGER, (u8*)"integer handling",
inject_integer_tests, inject_integer_check, 1 }
};
/* Dump the checks to stdout */
void display_injection_checks(void) {
u32 i;
SAY("\n[*] Available injection tests:\n\n");
for (i=cb_handle_off; i<cb_handle_cnt; i++) {
SAY(" -- [%2d] %-25s %s\n", i-cb_handle_off, cb_handles[i].name,
cb_handles[i].skip ? "(disabled)" : "");
}
SAY("\n");
}
/* Disable tests by parsing a comma separated list which we received
from the command-line. */
void toggle_injection_checks(u8* str, u32 enable, u8 user) {
u32 tnr;
u8* ptr;
/* If this is user input, we only allow check manipulation. Else,
we also allow other tests, such as for stability to be toggled */
u32 offset = user ? cb_handle_off : 0;
/* Copy the string for manipulation */
u8* ids = ck_strdup(str);
ptr = (u8*)strtok((char*)ids, ",");
for (; ptr != NULL ;){
tnr = atoi((char*)ptr);
if (tnr > cb_handle_cnt)
FATAL("Unable to parse checks toggle string");
tnr += offset;
/* User values are array index nr + 1 */
if (enable && cb_handles[tnr].skip) {
cb_handles[tnr].skip = 0;
DEBUG(" Enabled test: %d : %s\n", tnr, cb_handles[tnr].name);
} else {
cb_handles[tnr].skip = 1;
DEBUG(" Disabled test: %d : %s\n", tnr, cb_handles[tnr].name);
}
ptr = (u8*)strtok(NULL, ",");
}
ck_free(ids);
}
/* The inject state manager which uses the list ot check structs to
decide what test to schedule next */
u8 inject_state_manager(struct http_request* req, struct http_response* res) {
u32 i;
s32 check = req->pivot->check_idx;
DEBUG_CALLBACK(req, res);
/* If we are in crawler only more, jump to inject_done to effectively
disable all checks for the pivot */
if(no_checks) goto inject_done;
/* First test that gets us in the loop? This means we'll immediately go and
schedule some tests */
if (check == -1) goto schedule_tests;
/* Safety check */
if (check > cb_handle_cnt)
FATAL("Check number %d exceeds handle count %d!",check,cb_handle_cnt);
/* If requests failed for a test than we might have chosen to not
proceed with it by adding the check to i_skip. Here we check if this
is the case. */
if (req->pivot->i_skip[check])
return 0;
/* For simple injection tests, we do not abort at 503, 504's. But for
differential tests, we have to. */
if (res->state != STATE_OK || (!cb_handles[check].allow_varies &&
(res->code == 503 || res->code == 504))) {
handle_error(req, res, (u8*)cb_handles[check].name, 0);
content_checks(req, res);
req->pivot->i_skip[check] = 1;
return 0;
}
/* Store req/res which is used by checks that like to have multiple req/res
pairs before getting called. */
if (cb_handles[check].res_keep) {
req->pivot->misc_req[req->user_val] = req;
req->pivot->misc_res[req->user_val] = res;
req->pivot->misc_cnt++;
/* Check and return if we need more responses. */
if (cb_handles[check].res_num &&
req->pivot->misc_cnt != cb_handles[check].res_num)
return 1;
}
/* Check the results of previously scheduled tests and, if that goes
well, schedule new tests. When the callback returns 1, this means
more requests are needed before we can can do the final checks. */
DEBUG_STATE_CALLBACK(req, cb_handles[check].name, 1);
/* Check if we got all responses to avoid handing over NULL poiners to the
* checks() functions */
if (cb_handles[check].res_keep) {
for (i=0; i<req->pivot->misc_cnt; i++) {
if (!MREQ(i) || !MRES(i)) {
problem(PROB_FETCH_FAIL, req, res, (u8*)"During injection testing", req->pivot, 0);
/* Today, we'll give up on this test. In the next release: reschedule */
goto content_checks;
}
}
}
if (cb_handles[check].checks(req,res))
return 1;
if (!cb_handles[check].res_keep &&
(cb_handles[check].res_num && ++req->pivot->misc_cnt != cb_handles[check].res_num))
return 0;
content_checks:
/* If we get here, we're done and can move on. First make sure that
all responses have been checked. Than free memory and schedule the
next test */
if (cb_handles[check].res_keep && req->pivot->misc_cnt) {
for (i=0; i<req->pivot->misc_cnt; i++) {
/* Only check content once */
if (!MRES(i) || !MREQ(i) || MRES(i)->stuff_checked)
continue;
/* Only scrape for checks that want it
0 = don't scrape
1 = check content
2 = check content and extract links */
if (cb_handles[check].scrape > 0) {
content_checks(MREQ(i), MRES(i));
if (cb_handles[check].scrape == 2)
scrape_response(MREQ(i), MRES(i));
}
}
}
schedule_tests:
destroy_misc_data(req->pivot, req);
check = ++req->pivot->check_idx;
if (check < cb_handle_cnt) {
/* Move to the next test in case it's marked... */
if (cb_handles[check].skip) goto schedule_tests;
/* Move to the next test in case the page is unstable and the test doesn't want it. */
if ((req->pivot->res_varies && !cb_handles[check].allow_varies) ||
(req->pivot->res_time_exceeds && cb_handles[check].time_sensitive))
goto schedule_tests;
/* Move to the next test in case of pivot type mismatch */
if (cb_handles[check].pv_flag > 0 && !(cb_handles[check].pv_flag & req->pivot->type))
goto schedule_tests;
DEBUG_STATE_CALLBACK(req, cb_handles[check].name, 0);
/* Do the tests and return upon success or move on to the next upon
a return value of 1. We store the ID of the check in the pivot to
allow other functions, that use the pivot, to find out the current
injection test */
req->pivot->check_id = cb_handles[check].id;
if (cb_handles[check].tests(req->pivot) == 1)
goto schedule_tests;
return 0;
}
inject_done:
/* All injection tests done. Reset the counter and call inject_done()
to finish (or proceed with param tests */
DEBUG_STATE_CALLBACK(req, "inject_done", 1);
req->pivot->check_idx = -1;
inject_done(req->pivot);
return 0;
}
static u8 xssi_tests(struct pivot_desc* pv) {
struct http_request* n;
DEBUG_HELPER(pv);
/* We only want Javascript that does not have inclusion protection. This
* test, should be moved to the injection manager whenever we have more
* content specific tests (e.g. css ones) */
if(pv->res->js_type != 2 || pv->res->json_safe)
return 1;
n = req_copy(pv->req, pv, 1);
n->callback = inject_state_manager;
n->no_cookies = 1;
async_request(n);
return 0;
}
static u8 xssi_check(struct http_request* req,
struct http_response* res) {
DEBUG_MISC_CALLBACK(req, res);
/* When the response with cookie is different from the cookie-less response,
* than the content is session depended. In case of Javascript without XSSI
* protection, this is more than likely an issue. */
if (!same_page(&RPRES(req)->sig, &MRES(0)->sig)) {
/* Responses that do not contain the term "function", "if", "for", "while", etc,
are much more likely to be dynamic JSON than just static scripts. Let's
try to highlight these. */
if ((!req->method || !strcmp((char*)req->method, "GET")) &&
!inl_findstr(res->payload, (u8*)"if (", 2048) &&
!inl_findstr(res->payload, (u8*)"if(", 2048) &&
!inl_findstr(res->payload, (u8*)"for (", 2048) &&
!inl_findstr(res->payload, (u8*)"for(", 2048) &&
!inl_findstr(res->payload, (u8*)"while (", 2048) &&
!inl_findstr(res->payload, (u8*)"while(", 2048) &&
!inl_findstr(res->payload, (u8*)"function ", 2048) &&
!inl_findstr(res->payload, (u8*)"function(", 2048)) {
problem(PROB_JS_XSSI, req, res, (u8*)"Cookie-less JSON is different", req->pivot, 0);
} else {
problem(PROB_JS_XSSI, req, res, (u8*)"Cookie-less Javascript response is different", req->pivot, 0);
}
}
/* Now this is interesting. We can lookup the issues in the pivot and if
* analysis.c thinks this page has an XSSI, we can kill that assumption */
remove_issue(req->pivot, PROB_JS_XSSI);
return 0;
}
static u8 inject_behavior_tests(struct pivot_desc* pv) {
struct http_request* n;
u32 i;
DEBUG_HELPER(pv);
for (i=0;i<BH_CHECKS;i++) {
n = req_copy(pv->req, pv, 1);
n->callback = inject_state_manager;
n->user_val = i;
async_request(n);
}
return 0;
}
static u8 inject_behavior_check(struct http_request* req,
struct http_response* res) {
u32 i;
/* pv->state may change after async_request() calls in
insta-fail mode, so we should cache accordingly. */
DEBUG_CALLBACK(req, res);
for (i=0; i<req->pivot->misc_cnt; i++) {
if (!same_page(&RPRES(req)->sig, &MRES(i)->sig)) {
problem(PROB_VARIES, MREQ(i), MRES(i), 0, MREQ(i)->pivot, 0);
return 0;
}
}
return 0;
}
static u8 put_upload_tests(struct pivot_desc* pv) {
struct http_request* n;
DEBUG_HELPER(pv);
/* First a PUT request */
n = req_copy(pv->req, pv, 1);
if (n->method) ck_free(n->method);
n->method = ck_strdup((u8*)"PUT");
n->user_val = 0;
n->callback = inject_state_manager;
replace_slash(n, (u8*)("PUT-" BOGUS_FILE));
async_request(n);
/* Second a FOO for false positives */
n = req_copy(pv->req, pv, 1);
if (n->method) ck_free(n->method);
n->method = ck_strdup((u8*)"FOO");
n->user_val = 1;
n->callback = inject_state_manager;
replace_slash(n, (u8*)("FOO-" BOGUS_FILE));
async_request(n);
return 0;
}
static u8 put_upload_check(struct http_request* req,
struct http_response* res) {
DEBUG_MISC_CALLBACK(req, res);
/* If PUT and FOO of the page does not give the same result. And if
additionally we get a 2xx code, than we'll mark the issue as detected */
if (!same_page(&MRES(0)->sig, &MRES(1)->sig) &&
MRES(0)->code >= 200 && MRES(1)->code < 300)
problem(PROB_PUT_DIR, MREQ(0), MRES(0), 0, req->pivot, 0);
return 0;
}
/* The prologue test checks whether it is possible to inject a string in the
first bytes of the response because this can lead to utf-7 or third party
browser plugin attacks */
static u8 inject_prologue_tests(struct pivot_desc* pivot) {
u32 orig_state = pivot->state;
struct http_request* n;
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, (u8*)"+/skipfish-bom");
n->callback = inject_state_manager;
async_request(n);
return 0;
}
static u8 inject_prologue_check(struct http_request* req,
struct http_response* res) {
DEBUG_CALLBACK(req, res);
if (res->pay_len && !prefix(res->payload, (u8*)"+/skipfish-bom") &&
!GET_HDR((u8*)"Content-Disposition", &res->hdr))
problem(PROB_PROLOGUE, req, res, NULL, req->pivot, 0);
return 0;
}
/* XML injection checks evaluates multiple server responses and determined
whether the injected string caused a difference in behavior/reponse */
static u8 inject_xml_tests(struct pivot_desc* pivot) {
/* Backend XML injection - 2 requests. */
u32 orig_state = pivot->state;
struct http_request* n;
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, "sfish>'>\"><sfish></sfish>");
n->callback = inject_state_manager;
n->user_val = 0;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, "sfish>'>\"></sfish><sfish>");
n->callback = inject_state_manager;
n->user_val = 1;
async_request(n);
return 0;
}
static u8 inject_xml_check(struct http_request* req,
struct http_response* res) {
DEBUG_MISC_CALLBACK(req, res);
/* Got all responses:
misc[0] = valid XML
misc[1] = bad XML
If misc[0] != misc[1], we probably have XML injection on backend side. */
if (!same_page(&MRES(0)->sig, &MRES(1)->sig)) {
problem(PROB_XML_INJECT, MREQ(0), MRES(0),
(u8*)"responses for <sfish></sfish> and </sfish><sfish> look different",
req->pivot, 0);
RESP_CHECKS(MREQ(1), MRES(1));
}
return 0;
}
static u8 inject_shell_tests(struct pivot_desc* pivot) {
/* Shell command injection - 12 requests. */
u32 orig_state = pivot->state;
u8* tmp;
struct http_request* n;
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, (u8*)"`echo skip12``echo 34fish`");
n->callback = inject_state_manager;
n->user_val = 0;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n, (u8*)"`echo skip12``echo 34fish`");
n->callback = inject_state_manager;
n->user_val = 1;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, (u8*)"`echo${IFS}skip12``echo${IFS}34fish`");
n->callback = inject_state_manager;
n->user_val = 2;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n, (u8*)"`echo${IFS}skip12``echo${IFS}34fish`");
n->callback = inject_state_manager;
n->user_val = 3;
async_request(n);
/* We use the measured time_base as an offset for the sleep test. The
value is limited to MAX_RES_DURATION and the result is < 10 */
tmp = ck_alloc(10);
sprintf((char*)tmp, (char*)"`sleep %d`", pivot->res_time_base + SLEEP_TEST_ONE);
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n,tmp );
n->callback = inject_state_manager;
n->user_val = 4;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, tmp);
n->callback = inject_state_manager;
n->user_val = 5;
async_request(n);
sprintf((char*)tmp, (char*)"`sleep %d`", pivot->res_time_base + SLEEP_TEST_TWO);
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n, tmp);
n->callback = inject_state_manager;
n->user_val = 6;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, tmp);
n->callback = inject_state_manager;
n->user_val = 7;
async_request(n);
tmp = ck_realloc(tmp, 15);
sprintf((char*)tmp, (char*)"`sleep${IFS}%d`", pivot->res_time_base + SLEEP_TEST_ONE);
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n, tmp);
n->callback = inject_state_manager;
n->user_val = 8;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, tmp);
n->callback = inject_state_manager;
n->user_val = 9;
async_request(n);
sprintf((char*)tmp, (char*)"`sleep${IFS}%d`", pivot->res_time_base + SLEEP_TEST_TWO);
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n, tmp);
n->callback = inject_state_manager;
n->user_val = 10;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, tmp);
n->callback = inject_state_manager;
n->user_val = 11;
async_request(n);
ck_free(tmp);
return 0;
}
static u8 inject_shell_check(struct http_request* req,
struct http_response* res) {
u32 i;
DEBUG_MISC_CALLBACK(req, res);
/* Look in the first 4 requests to find our concatenated string */
for (i = 0; i < 3; i++) {
if (inl_findstr(MRES(i)->payload, (u8*)"skip1234fish", 1024))
problem(PROB_SH_INJECT, MREQ(i), MRES(i),
(u8*)"Confirmed shell injection (echo test)", req->pivot, 0);
}
/* Check that the request was delayed by our sleep. The sleep delay is
calculated by using the time_base in order to avoid FPs */
u32 test_one = req->pivot->res_time_base + SLEEP_TEST_ONE;
u32 test_two = req->pivot->res_time_base + SLEEP_TEST_TWO;
/* Now we check if the request duration was influenced by the sleep. We
do this by testing if the total request time was longer (or equal)
to: the average request time + the sleep time (3 or 5 seconds).
We allow the `sleep` request to take 1 second longer than
expected which is the final measure to reduce FPs.
*/
if ((RTIME(4) >= test_one && RTIME(4) < test_one + 1) &&
(RTIME(6) >= test_two && RTIME(6) < test_two + 1)) {
problem(PROB_SH_INJECT, MREQ(4), MRES(4),
(u8*)"Confirmed shell injection (sleep test)", req->pivot, 0);
}
if ((RTIME(5) >= test_one && RTIME(5) < test_one + 1) &&
(RTIME(7) >= test_two && RTIME(7) < test_two + 1)) {
problem(PROB_SH_INJECT, MREQ(5), MRES(5),
(u8*)"Confirmed shell injection (sleep test)", req->pivot, 0);
}
if ((RTIME(8) >= test_one && RTIME(8) < test_one + 1) &&
(RTIME(10) >= test_two && RTIME(10) < test_two + 1)) {
problem(PROB_SH_INJECT, MREQ(8), MRES(8),
(u8*)"Confirmed shell injection (sleep test)", req->pivot, 0);
}
if ((RTIME(9) >= test_one && RTIME(9) < test_one + 1) &&
(RTIME(11) >= test_two && RTIME(11) < test_two + 1)) {
problem(PROB_SH_INJECT, MREQ(9), MRES(9),
(u8*)"Confirmed shell injection (sleep test)", req->pivot, 0);
}
return 0;
}
static u8 inject_diff_shell_tests(struct pivot_desc* pivot) {
/* Shell command injection - 12 requests. */
u32 orig_state = pivot->state;
struct http_request* n;
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n, "`true`");
n->callback = inject_state_manager;
n->user_val = 0;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n, "`false`");
n->callback = inject_state_manager;
n->user_val = 1;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n, "`uname`");
n->callback = inject_state_manager;
n->user_val = 2;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n, "\"`true`\"");
n->callback = inject_state_manager;
n->user_val = 3;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n, "\"`false`\"");
n->callback = inject_state_manager;
n->user_val = 4;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n, "\"`uname`\"");
n->callback = inject_state_manager;
n->user_val = 5;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n, "'`true`'");
n->callback = inject_state_manager;
n->user_val = 6;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n, "'`false`'");
n->callback = inject_state_manager;
n->user_val = 7;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n, "'`uname`'");
n->callback = inject_state_manager;
n->user_val = 8;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, "`true`");
n->callback = inject_state_manager;
n->user_val = 9;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, "`false`");
n->callback = inject_state_manager;
n->user_val = 10;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, "`uname`");
n->callback = inject_state_manager;
n->user_val = 11;
async_request(n);
return 0;
}
static u8 inject_diff_shell_check(struct http_request* req,
struct http_response* res) {
DEBUG_MISC_CALLBACK(req, res);
/* Got all responses:
misc[0] = `true`
misc[1] = `false`
misc[2] = `uname`
misc[3] = "`true`"
misc[4] = "`false`"
misc[5] = "`uname`"
misc[6] = '`true`'
misc[7] = "`false`"
misc[8] = '`uname`'
And a variant that replaces the original values (instead of appending)
misc[9] = `true`
misc[10] = `false`
misc[11] = `uname`
If misc[0] == misc[1], but misc[0] != misc[2], we probably have shell
injection. Ditto for the remaining triplets. We use the `false` case
to avoid errors on search fields, etc. */
if (same_page(&MRES(0)->sig, &MRES(1)->sig) &&
!same_page(&MRES(1)->sig, &MRES(2)->sig)) {
problem(PROB_SH_INJECT, MREQ(1), MRES(1),
(u8*)"responses to `true` and `false` different than to `uname`",
req->pivot, 0);
}
if (same_page(&MRES(3)->sig, &MRES(4)->sig) &&
!same_page(&MRES(4)->sig, &MRES(5)->sig)) {
problem(PROB_SH_INJECT, MREQ(3), MRES(3),
(u8*)"responses to `true` and `false` different than to `uname`",
req->pivot, 0);
}
if (same_page(&MRES(6)->sig, &MRES(7)->sig) &&
!same_page(&MRES(6)->sig, &MRES(8)->sig)) {
problem(PROB_SH_INJECT, MREQ(6), MRES(6),
(u8*)"responses to `true` and `false` different than to `uname`",
req->pivot, 0);
}
if (same_page(&MRES(9)->sig, &MRES(10)->sig) &&
!same_page(&MRES(10)->sig, &MRES(11)->sig)) {
problem(PROB_SH_INJECT, MREQ(9), MRES(9),
(u8*)"responses to `true` and `false` different than to `uname`",
req->pivot, 0);
}
return 0;
}
static u8 inject_xss_tests(struct pivot_desc* pivot) {
/* Cross-site scripting - three requests (also test common
"special" error pages). */
struct http_request* n;
u32 orig_state = pivot->state;
u32 i, uval;
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n, new_xss_tag(NULL));
set_value(PARAM_HEADER, (u8*)"Referer", new_xss_tag(NULL), 0, &n->par);
register_xss_tag(n);
n->callback = inject_state_manager;
n->user_val = 0;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, new_xss_tag((u8*)".htaccess.aspx"));
register_xss_tag(n);
n->callback = inject_state_manager;
n->user_val = 1;
async_request(n);
/* A last ones with only header injections. The User-Agent injection
doesn't seems to be very useful for reflective XSS scenario's
but could reveal persistant XSS problems (i.e. in log / backend
interfaces) */
n = req_copy(pivot->req, pivot, 1);
set_value(PARAM_HEADER, (u8*)"Referer", new_xss_tag(NULL), 0, &n->par);
set_value(PARAM_HEADER, (u8*)"User-Agent", new_xss_tag(NULL), 0, &n->par);
register_xss_tag(n);
n->callback = inject_state_manager;
n->user_val = 2;
async_request(n);
/* One for testing HTTP_HOST XSS types which are somewhat unlikely
but still have abuse potential (e.g. stored XSS') */
n = req_copy(pivot->req, pivot, 1);
set_value(PARAM_HEADER, (u8*)"Host", new_xss_tag(NULL), 0, &n->par);
register_xss_tag(n);
n->callback = inject_state_manager;
n->user_val = 3;
async_request(n);
/* Finally we tests the cookies, one by one to avoid breaking the
session */
uval = 3;
for (i=0;i<global_http_par.c;i++) {
if (global_http_par.t[i] != PARAM_COOKIE)
continue;
n = req_copy(pivot->req, pivot, 1);
set_value(PARAM_COOKIE, global_http_par.n[i],
new_xss_tag(NULL), 0, &n->par);
register_xss_tag(n);
n->callback = inject_xss_check;
n->user_val = ++uval;
async_request(n);
}
return 0;
}
static u8 inject_xss_check(struct http_request* req,
struct http_response* res) {
DEBUG_CALLBACK(req, res);
if (!req || !res || FETCH_FAIL(res))
return 0;
/* Content checks do automatic HTML parsing and XSS detection.
scrape_page() is generally not advisable here. This is not a very
exiting check and we'll be able to get rid of it in future updated. */
content_checks(req, res);
return 0;
}
static u8 inject_dir_listing_tests(struct pivot_desc* pivot) {
struct http_request* n;
u8* tmp = NULL;
u32 orig_state = pivot->state;
/* Directory listing - 4 requests. The logic here is a bit
different for parametric targets (which are easy to examine with
a ./ trick) and directories (which require a more complex
comparison). */
pivot->misc_cnt = 0;
n = req_copy(pivot->req, pivot, 1);
if (orig_state == PSTATE_CHILD_INJECT) {
replace_slash(n, (u8*)".");
set_value(PARAM_PATH, NULL, (u8*)"", -1, &n->par);
} else {
tmp = ck_alloc(strlen((char*)TPAR(n)) + 5);
sprintf((char*)tmp, ".../%s", TPAR(n));
ck_free(TPAR(n));
TPAR(n) = ck_strdup(tmp);
}
n->callback = inject_state_manager;
n->user_val = 0;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
if (orig_state == PSTATE_CHILD_INJECT) {
replace_slash(n, (u8*)".sf");
set_value(PARAM_PATH, NULL, (u8*)"", -1, &n->par);
} else {
ck_free(TPAR(n));
TPAR(n) = ck_strdup(tmp + 2);
}
n->callback = inject_state_manager;
n->user_val = 1;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
if (orig_state == PSTATE_CHILD_INJECT) {
replace_slash(n, (u8*)"\\.\\");
} else {
tmp[3] = '\\';
ck_free(TPAR(n));
TPAR(n) = ck_strdup(tmp);
}
n->callback = inject_state_manager;
n->user_val = 2;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
if (orig_state == PSTATE_CHILD_INJECT) {
replace_slash(n, (u8*)"\\.sf\\");
} else {
ck_free(TPAR(n));
TPAR(n) = ck_strdup(tmp + 2);
ck_free(tmp);
}
n->callback = inject_state_manager;
n->user_val = 3;
async_request(n);
return 0;
}
static u8 inject_dir_listing_check(struct http_request* req,
struct http_response* res) {
u32 orig_state = req->pivot->state;
DEBUG_MISC_CALLBACK(req, res);
/* Got all responses. For directories, this is:
pivot = /
misc[0] = /./
misc[1] = /.sf/
misc[2] = \.\
misc[3] = \.sf\
Here, if pivot != misc[0], and misc[0] != misc[1], we probably
managed to list a hidden dir. The same test is carried out for
misc[2] and misc[3].
For parameters, this is:
misc[0] = .../known_val
misc[1] = ./known_val
misc[2] = ...\known_val
misc[3] = .\known_val
Here, the test is simpler: if misc[1] != misc[0], or misc[3] !=
misc[2], we probably have a bug. The same if misc[4] or misc[5]
contain magic strings, but misc[0] doesn't.
*/
if (orig_state == PSTATE_CHILD_INJECT) {
if (MRES(0)->code < 300 &&
!same_page(&MRES(0)->sig, &RPRES(req)->sig) &&
!same_page(&MRES(0)->sig, &MRES(1)->sig)) {
problem(PROB_DIR_LIST_BYPASS, MREQ(0), MRES(0),
(u8*)"unique response for /./",
req->pivot, 0);
/* Use pivot's request, rather than MREQ(0), for link scraping;
MREQ(0) contains an "illegal" manually constructed path. */
RESP_CHECKS(RPREQ(req), MRES(0));
}
if (MRES(2)->code < 300 &&
!same_page(&MRES(2)->sig, &RPRES(req)->sig) &&
!same_page(&MRES(2)->sig, &MRES(3)->sig)) {
problem(PROB_DIR_LIST_BYPASS, MREQ(2), MRES(2),
(u8*)"unique response for \\.\\",
req->pivot, 0);
RESP_CHECKS(MREQ(2), MRES(2));
}
} else {
if (!same_page(&MRES(0)->sig, &MRES(1)->sig)) {
problem(PROB_DIR_TRAVERSAL, MREQ(1), MRES(1),
(u8*)"responses for ./val and .../val look different",
req->pivot, 0);
RESP_CHECKS(MREQ(0), MRES(0));
}
if (!same_page(&MRES(2)->sig, &MRES(3)->sig)) {
problem(PROB_DIR_TRAVERSAL, MREQ(3), MRES(3),
(u8*)"responses for .\\val and ...\\val look different",
req->pivot, 0);
RESP_CHECKS(MREQ(2), MRES(2));
}
}
return 0;
}
static u8 inject_inclusion_tests(struct pivot_desc* pivot) {
struct http_request* n;
u32 i;
/* Perhaps do this in state manager ?*/
if (pivot->state == PSTATE_CHILD_INJECT)
return 1;
/* We combine the traversal and file disclosure attacks here since
the checks are almost identical */
i = 0;
while (disclosure_tests[i]) {
n = req_copy(pivot->req, pivot, 1);
/* No % encoding for these requests */
n->fuzz_par_enc = (u8*)ENC_NULL;
ck_free(TPAR(n));
TPAR(n) = ck_strdup((u8*)disclosure_tests[i]);
n->callback = inject_state_manager;
n->user_val = i;
async_request(n);
i++;
}
#ifdef RFI_SUPPORT
/* Optionally try RFI */
n = req_copy(pivot->req, pivot, 1);
ck_free(TPAR(n));
TPAR(n) = ck_strdup((u8*)RFI_HOST);
n->callback = inject_state_manager;
n->user_val = i;
async_request(n);
#endif
return 0;
}
static u8 inject_inclusion_check(struct http_request* req,
struct http_response* res) {
DEBUG_MISC_CALLBACK(req, res);
u32 not_found = 0;
/*
Perform directory traveral and file inclusion tests.
misc[1] = ../../../../../../../../etc/hosts
misc[2] = ../../../../../../../../etc/hosts\0
misc[3] = ../../../../../../../../etc/passwd
misc[4] = ../../../../../../../../etc/passwd\0
misc[5] = ..\..\..\..\..\..\..\..\boot.ini
misc[6] = ..\..\..\..\..\..\..\..\boot.ini\0
misc[7] = ../../../../../../../../WEB-INF/web.xml
misc[8] = ../../../../../../../../WEB-INF/web.xml\0
misc[9] = file:///etc/hosts
misc[10] = file:///etc/passwd
misc[11] = file:///boot.ini
misc[12] = RFI (optional)
*/
/* Check on the /etc/hosts file disclosure */
if (!inl_findstr(RPRES(req)->payload, (u8*)"127.0.0.1", 1024)) {
if (inl_findstr(MRES(0)->payload, (u8*)"127.0.0.1", 1024)) {
problem(PROB_FI_LOCAL, MREQ(0), MRES(0),
(u8*)"response resembles /etc/hosts (traversal)", req->pivot, 0);
} else if (inl_findstr(MRES(1)->payload, (u8*)"127.0.0.1", 1024)) {
problem(PROB_FI_LOCAL, MREQ(1), MRES(1),
(u8*)"response resembles /etc/hosts (traversal with NULL byte)", req->pivot, 0);
} else if (inl_findstr(MRES(4)->payload, (u8*)"127.0.0.1", 1024)) {
problem(PROB_FI_LOCAL, MREQ(4), MRES(4),
(u8*)"response resembles /etc/hosts (via file://)", req->pivot, 0);
} else not_found++;
}
/* Check on the /etc/passwd file disclosure */
if (!inl_findstr(RPRES(req)->payload, (u8*)"root:x:0:0:root", 1024)) {
if (inl_findstr(MRES(2)->payload, (u8*)"root:x:0:0:root", 1024)) {
problem(PROB_FI_LOCAL, MREQ(2), MRES(2),
(u8*)"response resembles /etc/passwd (via traversal)", req->pivot, 0);
} else if (inl_findstr(MRES(3)->payload, (u8*)"root:x:0:0:root", 1024)) {
problem(PROB_FI_LOCAL, MREQ(3), MRES(3),
(u8*)"response resembles /etc/passwd (via traversal)", req->pivot, 0);
} else if (inl_findstr(MRES(9)->payload, (u8*)"root:x:0:0:root", 1024)) {
problem(PROB_FI_LOCAL, MREQ(9), MRES(9),
(u8*)"response resembles /etc/passwd (via file://)", req->pivot, 0);
} else not_found++;
}
/* Windows boot.ini disclosure */
if (!inl_findstr(RPRES(req)->payload, (u8*)"[boot loader]", 1024)) {
if (inl_findstr(MRES(4)->payload, (u8*)"[boot loader]", 1024)) {
problem(PROB_FI_LOCAL, MREQ(4), MRES(4),
(u8*)"response resembles c:\\boot.ini (via traversal)", req->pivot, 0);
} else if (inl_findstr(MRES(5)->payload, (u8*)"[boot loader]", 1024)) {
problem(PROB_FI_LOCAL, MREQ(5), MRES(5),
(u8*)"response resembles c:\\boot.ini (via traversal)", req->pivot, 0);
} else if (inl_findstr(MRES(10)->payload, (u8*)"[boot loader]", 1024)) {
problem(PROB_FI_LOCAL, MREQ(10), MRES(10),
(u8*)"response resembles c:\\boot.ini (via file://)", req->pivot, 0);
} else not_found++;
}
/* Check the web.xml disclosure */
if (!inl_findstr(RPRES(req)->payload, (u8*)"<servlet-mapping>", 1024)) {
if (inl_findstr(MRES(6)->payload, (u8*)"<servlet-mapping>", 1024)) {
problem(PROB_FI_LOCAL, MREQ(6), MRES(10),
(u8*)"response resembles ./WEB-INF/web.xml (via traversal)", req->pivot, 0);
} else if (inl_findstr(MRES(7)->payload, (u8*)"<servlet-mapping>", 1024)){
problem(PROB_FI_LOCAL, MREQ(7), MRES(7),
(u8*)"response resembles ./WEB-INF/web.xml (via traversal)", req->pivot, 0);
} else not_found++;
}
/* If we disclosed a file, than we can remove any present traversal
warnings, which in that case are just duplicate/noise */
if (not_found != 4)
remove_issue(req->pivot, PROB_DIR_TRAVERSAL);
#ifdef RFI_SUPPORT
if (!inl_findstr(RPRES(req)->payload, (u8*)RFI_STRING, 1024) &&
inl_findstr(MRES(11)->payload, (u8*)RFI_STRING, 1024)) {
problem(PROB_FI_REMOTE, MREQ(11), MRES(11),
(u8*)"remote file inclusion", req->pivot, 0);
}
#endif
return 0;
}
static u8 inject_redir_tests(struct pivot_desc* pivot) {
struct http_request* n;
u32 orig_state = pivot->state;
/* XSS checks - 5 requests */
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, "http://skipfish.invalid/;?");
n->callback = inject_state_manager;
n->user_val = 0;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, "//skipfish.invalid/;?");
n->callback = inject_state_manager;
n->user_val = 1;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, "skipfish://invalid/;?");
n->callback = inject_state_manager;
n->user_val = 2;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, "'skip'''\"fish\"\"\"");
n->callback = inject_state_manager;
n->user_val = 3;
async_request(n);
/* Finally an encoded version which is aimed to detect injection
problems in JS handlers, such as onclick, which executes HTML encoded
strings. */
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, "&apos;skip&apos;&apos;&apos;&quot;fish&quot;&quot;&quot;");
n->callback = inject_state_manager;
n->user_val = 4;
async_request(n);
return 0;
}
static u8 inject_redir_check(struct http_request* req,
struct http_response* res) {
u8* val;
u32 i;
DEBUG_MISC_CALLBACK(req, res);
/* Check Location, Refresh headers. */
for (i=0; i < req->pivot->misc_cnt; i++) {
val = GET_HDR((u8*)"Location", &MRES(i)->hdr);
if (val) {
if (!case_prefix(val, "http://skipfish.invalid/") ||
!case_prefix(val, "//skipfish.invalid/"))
problem(PROB_URL_REDIR, MREQ(i), MRES(i), (u8*)"injected URL in 'Location' header",
req->pivot, 0);
if (!case_prefix(val, "skipfish:"))
problem(PROB_URL_XSS, MREQ(i), MRES(i), (u8*)"injected URL in 'Location' header",
req->pivot, 0);
}
val = GET_HDR((u8*)"Refresh", &MRES(i)->hdr);
if (val && (val = (u8*)strchr((char*)val, '=')) && val++) {
u8 semi_safe = 0;
if (*val == '\'' || *val == '"') { val++; semi_safe++; }
if (!case_prefix(val, "http://skipfish.invalid/") ||
!case_prefix(val, "//skipfish.invalid/"))
problem(PROB_URL_REDIR, MREQ(i), MRES(i), (u8*)"injected URL in 'Refresh' header",
req->pivot, 0);
/* Unescaped semicolon in Refresh headers is unsafe with MSIE6. */
if (!case_prefix(val, "skipfish:") ||
(!semi_safe && strchr((char*)val, ';')))
problem(PROB_URL_XSS, MREQ(i), MRES(i), (u8*)"injected URL in 'Refresh' header",
req->pivot, 0);
}
/* META tags and JS will be checked by content_checks(). We're not
calling scrape_page(), because we don't want to accumulate bogus,
injected links. */
content_checks(MREQ(i), MRES(i));
}
return 0;
}
static u8 inject_split_tests(struct pivot_desc* pivot) {
struct http_request* n;
u32 orig_state = pivot->state;
/* Header splitting - 2 requests */
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n, "bogus\nSkipfish-Inject:bogus");
n->callback = inject_state_manager;
n->user_val = 0;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n, "bogus\rSkipfish-Inject:bogus");
n->callback = inject_state_manager;
n->user_val = 1;
async_request(n);
return 0;
}
static u8 inject_split_check(struct http_request* req,
struct http_response* res) {
DEBUG_MISC_CALLBACK(req, res);
/* Not differential. */
if (res->state != STATE_OK) {
handle_error(req, res, (u8*)"during header injection attacks", 0);
return 0;
}
/* Check headers - that's all! */
if (GET_HDR((u8*)"Skipfish-Inject", &MRES(0)->hdr))
problem(PROB_HTTP_INJECT, MREQ(0), MRES(0),
(u8*)"successfully injected 'Skipfish-Inject' header into response",
req->pivot, 0);
if (GET_HDR((u8*)"Skipfish-Inject", &MRES(1)->hdr))
problem(PROB_HTTP_INJECT, MREQ(1), MRES(1),
(u8*)"successfully injected 'Skipfish-Inject' header into response",
req->pivot, 0);
return 0;
}
static u8 inject_sql_tests(struct pivot_desc* pivot) {
struct http_request* n;
u32 orig_state = pivot->state;
u8 is_num = 0;
/* SQL injection - 10 requests */
if (orig_state != PSTATE_CHILD_INJECT) {
u8* pstr = TPAR(pivot->req);
u32 c = strspn((char*)pstr, "01234567890.+-");
if (pstr[0] && !pstr[c]) is_num = 1;
}
n = req_copy(pivot->req, pivot, 1);
if (!is_num) SET_VECTOR(orig_state, n, "9-8");
else APPEND_VECTOR(orig_state, n, "-0");
n->callback = inject_state_manager;
n->user_val = 0;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
if (!is_num) SET_VECTOR(orig_state, n, "8-7");
else APPEND_VECTOR(orig_state, n, "-0-0");
n->callback = inject_state_manager;
n->user_val = 1;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
if (!is_num) SET_VECTOR(orig_state, n, "9-1");
else APPEND_VECTOR(orig_state, n, "-0-9");
n->callback = inject_state_manager;
n->user_val = 2;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n, "\\\'\\\"");
set_value(PARAM_HEADER, (u8*)"User-Agent", (u8*)"sfish\\\'\\\"", 0, &n->par);
set_value(PARAM_HEADER, (u8*)"Referer", (u8*)"sfish\\\'\\\"", 0, &n->par);
set_value(PARAM_HEADER, (u8*)"Accept-Language", (u8*)"sfish\\\'\\\",en", 0,
&n->par);
n->callback = inject_state_manager;
n->user_val = 3;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n, "\'\"");
set_value(PARAM_HEADER, (u8*)"User-Agent", (u8*)"sfish\'\"", 0, &n->par);
set_value(PARAM_HEADER, (u8*)"Referer", (u8*)"sfish\'\"", 0, &n->par);
set_value(PARAM_HEADER, (u8*)"Accept-Language", (u8*)"sfish\'\",en", 0,
&n->par);
n->callback = inject_state_manager;
n->user_val = 4;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n, "\\\\\'\\\\\"");
set_value(PARAM_HEADER, (u8*)"User-Agent", (u8*)"sfish\\\\\'\\\\\"", 0, &n->par);
set_value(PARAM_HEADER, (u8*)"Referer", (u8*)"sfish\\\\\'\\\\\"", 0, &n->par);
set_value(PARAM_HEADER, (u8*)"Accept-Language", (u8*)"sfish\\\\\'\\\\\",en", 0,
&n->par);
n->callback = inject_state_manager;
n->user_val = 5;
async_request(n);
/* This is a special case to trigger fault on blind numerical injection. */
n = req_copy(pivot->req, pivot, 1);
if (!is_num) SET_VECTOR(orig_state, n, "9 - 1");
else APPEND_VECTOR(orig_state, n, " - 0 - 0");
n->callback = inject_state_manager;
n->user_val = 6;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
if (!is_num) SET_VECTOR(orig_state, n, "9 1 -");
else APPEND_VECTOR(orig_state, n, " 0 0 - -");
n->callback = inject_state_manager;
n->user_val = 7;
async_request(n);
/* Another round of SQL injection checks for a different escaping style. */
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n, "''''\"\"\"\"");
set_value(PARAM_HEADER, (u8*)"User-Agent", (u8*)"sfish''''\"\"\"\"", 0,
&n->par);
set_value(PARAM_HEADER, (u8*)"Referer", (u8*)"sfish''''\"\"\"\"", 0, &n->par);
set_value(PARAM_HEADER, (u8*)"Accept-Language", (u8*)"sfish''''\"\"\"\",en",
0, &n->par);
n->callback = inject_state_manager;
n->user_val = 8;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
APPEND_VECTOR(orig_state, n, "'\"'\"'\"'\"");
set_value(PARAM_HEADER, (u8*)"User-Agent", (u8*)"sfish'\"'\"'\"'\"", 0,
&n->par);
set_value(PARAM_HEADER, (u8*)"Referer", (u8*)"sfish'\"'\"'\"'\"", 0,
&n->par);
set_value(PARAM_HEADER, (u8*)"Accept-Language",
(u8*)"sfish'\"'\"'\"'\",en", 0, &n->par);
n->callback = inject_state_manager;
n->user_val = 9;
async_request(n);
/* Todo: cookies */
return 0;
}
static u8 inject_sql_check(struct http_request* req,
struct http_response* res) {
DEBUG_MISC_CALLBACK(req, res);
/* Got all data:
misc[0] = 9-8 (or orig-0)
misc[1] = 8-7 (or orig-0-0)
misc[2] = 9-1 (or orig-0-9)
misc[3] = [orig]\'\"
misc[4] = [orig]'"
misc[5] = [orig]\\'\\"
misc[6] = 9 - 1 (or orig - 0 - 0)
misc[7] = 9 1 - (or orig 0 0 - -)
misc[8] == [orig]''''""""
misc[9] == [orig]'"'"'"'"
If misc[0] == misc[1], but misc[0] != misc[2], probable (numeric) SQL
injection. Ditto for misc[1] == misc[6], but misc[6] != misc[7].
If misc[3] != misc[4] and misc[3] != misc[5], probable text SQL
injection.
If misc[4] == misc[9], and misc[8] != misc[9], probable text SQL
injection.
*/
if (same_page(&MRES(0)->sig, &MRES(1)->sig) &&
!same_page(&MRES(1)->sig, &MRES(2)->sig)) {
problem(PROB_SQL_INJECT, MREQ(0), MRES(0),
(u8*)"response suggests arithmetic evaluation on server side (type 1)",
req->pivot, 0);
}
if (same_page(&MRES(1)->sig, &MRES(6)->sig) &&
!same_page(&MRES(6)->sig, &MRES(7)->sig)) {
problem(PROB_SQL_INJECT, MREQ(7), MRES(7),
(u8*)"response suggests arithmetic evaluation on server side (type 2)",
req->pivot, 0);
}
if (same_page(&MRES(3)->sig, &MRES(4)->sig) &&
!same_page(&MRES(4)->sig, &MRES(5)->sig)) {
problem(PROB_SQL_INJECT, MREQ(4), MRES(4),
(u8*)"response to '\" different than to \\'\\\"", req->pivot, 0);
}
if (same_page(&MRES(4)->sig, &MRES(9)->sig) &&
!same_page(&MRES(8)->sig, &MRES(9)->sig)) {
problem(PROB_SQL_INJECT, MREQ(4), MRES(4),
(u8*)"response to ''''\"\"\"\" different than to '\"'\"'\"'\"", req->pivot, 0);
}
return 0;
}
static u8 inject_format_tests(struct pivot_desc* pivot) {
struct http_request* n;
u32 orig_state = pivot->state;
/* Format string attacks - 2 requests. */
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, "sfish%dn%dn%dn%dn%dn%dn%dn%dn");
n->callback = inject_state_manager;
n->user_val = 0;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, "sfish%nd%nd%nd%nd%nd%nd%nd%nd");
n->callback = inject_state_manager;
n->user_val = 1;
async_request(n);
return 0;
}
static u8 inject_format_check(struct http_request* req,
struct http_response* res) {
DEBUG_MISC_CALLBACK(req, res);
/* Got all data:
misc[0] = %dn... (harmless)
misc[1] = %nd... (crashy)
If misc[0] != misc[1], probable format string vuln.
*/
if (!same_page(&MRES(0)->sig, &MRES(1)->sig)) {
problem(PROB_FMT_STRING, MREQ(1), MRES(1),
(u8*)"response to %dn%dn%dn... different than to %nd%nd%nd...",
req->pivot, 0);
RESP_CHECKS(MREQ(1), MRES(1));
}
return 0;
}
static u8 inject_integer_tests(struct pivot_desc* pivot) {
struct http_request* n;
u32 orig_state = pivot->state;
/* Integer overflow bugs - 9 requests. */
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, "-0000012345");
n->callback = inject_state_manager;
n->user_val = 0;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, "-2147483649");
n->callback = inject_state_manager;
n->user_val = 1;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, "-2147483648");
n->callback = inject_state_manager;
n->user_val = 2;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, "0000012345");
n->callback = inject_state_manager;
n->user_val = 3;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, "2147483647");
n->callback = inject_state_manager;
n->user_val = 4;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, "2147483648");
n->callback = inject_state_manager;
n->user_val = 5;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, "4294967295");
n->callback = inject_state_manager;
n->user_val = 6;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, "4294967296");
n->callback = inject_state_manager;
n->user_val = 7;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
SET_VECTOR(orig_state, n, "0000023456");
n->callback = inject_state_manager;
n->user_val = 8;
async_request(n);
return 0;
}
static u8 inject_integer_check(struct http_request* req,
struct http_response* res) {
DEBUG_MISC_CALLBACK(req, res);
/* Got all data:
misc[0] = -12345 (baseline)
misc[1] = -(2^31-1)
misc[2] = -2^31
misc[3] = 12345 (baseline)
misc[4] = 2^31-1
misc[5] = 2^31
misc[6] = 2^32-1
misc[7] = 2^32
misc[8] = 23456 (validation)
If misc[3] != misc[8], skip tests - we're likely dealing with a
search field instead.
If misc[0] != misc[1] or misc[2], probable integer overflow;
ditto for 3 vs 4, 5, 6, 7.
*/
if (!same_page(&MRES(3)->sig, &MRES(8)->sig))
return 0;
if (!same_page(&MRES(0)->sig, &MRES(1)->sig)) {
problem(PROB_INT_OVER, MREQ(1), MRES(1),
(u8*)"response to -(2^31-1) different than to -12345",
req->pivot, 0);
RESP_CHECKS(MREQ(1), MRES(1));
}
if (!same_page(&MRES(0)->sig, &MRES(2)->sig)) {
problem(PROB_INT_OVER, MREQ(2), MRES(2),
(u8*)"response to -2^31 different than to -12345",
req->pivot, 0);
RESP_CHECKS(MREQ(2), MRES(2));
}
if (!same_page(&MRES(3)->sig, &MRES(4)->sig)) {
problem(PROB_INT_OVER, MREQ(4), MRES(4),
(u8*)"response to 2^31-1 different than to 12345",
req->pivot, 0);
RESP_CHECKS(MREQ(4), MRES(4));
}
if (!same_page(&MRES(3)->sig, &MRES(5)->sig)) {
problem(PROB_INT_OVER, MREQ(5), MRES(5),
(u8*)"response to 2^31 different than to 12345",
req->pivot, 0);
RESP_CHECKS(MREQ(5), MRES(5));
}
if (!same_page(&MRES(3)->sig, &MRES(6)->sig)) {
problem(PROB_INT_OVER, MREQ(6), MRES(6),
(u8*)"response to 2^32-1 different than to 12345",
req->pivot, 0);
RESP_CHECKS(MREQ(6), MRES(6));
}
if (!same_page(&MRES(3)->sig, &MRES(7)->sig)) {
problem(PROB_INT_OVER, MREQ(7), MRES(7),
(u8*)"response to 2^32 different than to 12345",
req->pivot, 0);
RESP_CHECKS(MREQ(7), MRES(7));
}
return 0;
}
static u8 param_behavior_tests(struct pivot_desc* pivot) {
struct http_request* n;
u32 i;
if (pivot->fuzz_par < 0 || !url_allowed(pivot->req) || !param_allowed(pivot->name)) {
pivot->state = PSTATE_DONE;
if (delete_bin) maybe_delete_payload(pivot);
return 0;
}
DEBUG_HELPER(pivot);
/* Parameter behavior. */
for (i=0;i<BH_CHECKS;i++) {
n = req_copy(pivot->req, pivot, 1);
ck_free(TPAR(n));
TPAR(n) = ck_strdup((u8*)BOGUS_PARAM);
n->callback = inject_state_manager;
n->user_val = i;
async_request(n);
}
return 0;
}
static u8 param_behavior_check(struct http_request* req,
struct http_response* res) {
u32 i;
u32 res_diff;
u32 page_diff = 0;
DEBUG_MISC_CALLBACK(req, res);
req->pivot->state = PSTATE_PAR_CHECK;
for (i=0; i<req->pivot->misc_cnt; i++) {
/* Store the biggest response time */
res_diff = MREQ(i)->end_time - MREQ(i)->start_time;
if(res_diff > req->pivot->res_time_base)
req->pivot->res_time_base = res_diff;
/* Compare the page responses */
if (!page_diff && !same_page(&MRES(i)->sig, &RPRES(req)->sig))
page_diff = i;
}
/* If the largest response time exceeded our threshold, we'll skip
the timing related tests */
if(req->pivot->res_time_base > MAX_RES_DURATION) {
problem(PROB_VARIES, req, res, (u8*)"Responses too slow for time sensitive tests", req->pivot, 0);
req->pivot->res_time_exceeds = 1;
}
if (page_diff == req->pivot->misc_cnt) {
DEBUG("* Parameter seems to have no effect.\n");
req->pivot->bogus_par = 1;
return 0;
}
DEBUG("* Parameter seems to have some effect:\n");
debug_same_page(&res->sig, &RPRES(req)->sig);
if (req->pivot->bogus_par) {
DEBUG("* We already classified it as having no effect, whoops.\n");
req->pivot->res_varies = 1;
problem(PROB_VARIES, req, res, 0, req->pivot, 0);
return 0;
}
/* If we do not have a signature yet, record it. Otherwise, make sure
it did not change. */
if (!req->pivot->r404_cnt) {
DEBUG("* New signature, recorded.\n");
memcpy(&req->pivot->r404[0], &res->sig, sizeof(struct http_sig));
req->pivot->r404_cnt = 1;
} else {
if (!same_page(&res->sig, &req->pivot->r404[0])) {
DEBUG("* Signature does not match previous responses, whoops.\n");
req->pivot->res_varies = 1;
problem(PROB_VARIES, req, res, 0, req->pivot, 0);
}
}
return 0;
}
static u8 param_ognl_tests(struct pivot_desc* pivot) {
struct http_request* n;
u32 ret = 1;
u8* tmp;
/* All probes failed? Assume bogus parameter, what else to do... */
if (!pivot->r404_cnt)
pivot->bogus_par = 1;
/* If the parameter has an effect, schedule OGNL checks. */
if (!pivot->bogus_par && !pivot->res_varies &&
pivot->req->par.n[pivot->fuzz_par]) {
n = req_copy(pivot->req, pivot, 1);
tmp = ck_alloc(strlen((char*)n->par.n[pivot->fuzz_par]) + 8);
sprintf((char*)tmp, "[0]['%s']", n->par.n[pivot->fuzz_par]);
ck_free(n->par.n[pivot->fuzz_par]);
n->par.n[pivot->fuzz_par] = tmp;
n->callback = inject_state_manager;
n->user_val = 0;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
ck_free(n->par.n[pivot->fuzz_par]);
n->par.n[pivot->fuzz_par] = ck_strdup((u8*)"[0]['sfish']");
n->callback = inject_state_manager;
n->user_val = 1;
async_request(n);
ret = 0;
}
/* Injection attacks should be carried out even if we think this
parameter has no visible effect; but injection checks will not proceed
to dictionary fuzzing if bogus_par or res_varies is set. */
pivot->state = PSTATE_PAR_INJECT;
return ret;
}
static u8 param_ognl_check(struct http_request* req,
struct http_response* res) {
DEBUG_MISC_CALLBACK(req, res);
/* First response is meant to give the same result. Second
is meant to give a different one. */
if (same_page(&MREQ(0)->pivot->res->sig, &MRES(0)->sig) &&
!same_page(&MREQ(1)->pivot->res->sig, &MRES(1)->sig)) {
problem(PROB_OGNL, req, res,
(u8*)"response to [0]['name']=... identical to name=...",
req->pivot, 0);
}
return 0;
}
static u8 dir_ips_tests(struct pivot_desc* pivot) {
struct http_request* n;
pivot->state = PSTATE_IPS_CHECK;
n = req_copy(pivot->req, pivot, 1);
tokenize_path((u8*)IPS_TEST, n, 0);
n->callback = inject_state_manager;
n->user_val = 0;
async_request(n);
n = req_copy(pivot->req, pivot, 1);
tokenize_path((u8*)IPS_SAFE, n, 0);
n->callback = inject_state_manager;
n->user_val = 1;
async_request(n);
return 0;
}
static u8 dir_ips_check(struct http_request* req,
struct http_response* res) {
struct pivot_desc* par;
DEBUG_MISC_CALLBACK(req, res);
par = dir_parent(req->pivot);
if (!par || !par->uses_ips) {
if (MRES(0)->state != STATE_OK)
problem(PROB_IPS_FILTER, MREQ(0), MRES(0),
(u8*)"request timed out (could also be a flaky server)",
req->pivot, 0);
else if (!same_page(&MRES(0)->sig, &MRES(1)->sig))
problem(PROB_IPS_FILTER, MREQ(0), MRES(0), NULL, req->pivot, 0);
} else {
if (MRES(0)->state == STATE_OK && same_page(&MRES(0)->sig, &MRES(1)->sig))
problem(PROB_IPS_FILTER_OFF, MREQ(0), MRES(0), NULL, req->pivot, 0);
}
destroy_misc_data(req->pivot, req);
req->pivot->state = PSTATE_CHILD_INJECT;
return 0;
}