PageRenderTime 39ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/HTTP/Fuzzer.pm

https://code.google.com/
Perl | 376 lines | 223 code | 41 blank | 112 comment | 29 complexity | 43595dbefdf9375037f958aad4cc2e97 MD5 | raw file
  1. package HTTP::Fuzzer;
  2. =pod
  3. =head1 NAME
  4. HTTP::Fuzzer - An extensible HTTP Fuzzer
  5. =head1 SYNOPSIS
  6. use HTTP::Fuzzer;
  7. use HTTP::Fuzzer::WordListGenerator;
  8. sub handleHttpRequest($$$$) {
  9. my $params = shift;
  10. my $request = shift;
  11. my $responseCode = shift;
  12. my $responseContent = shift;
  13. if ($responseCode == 200) {
  14. while (my ($k,$v) = each %$params) {
  15. print "$k: $v\n";
  16. }
  17. }
  18. }
  19. my $fuzzer = new HTTP::Fuzzer();
  20. # simple use case
  21. $fuzzer->addService(
  22. host => 'http://127.0.0.1',
  23. url => '/{path}',
  24. filter_response_codes => '404',
  25. handler => \&handleHttpRequest );
  26. $fuzzer->addFuzzingValue('path', 'admin');
  27. $fuzzer->addFuzzingValue('path', 'install');
  28. $fuzzer->run();
  29. # more complicated test case
  30. $fuzzer->addService(
  31. method => 'POST',
  32. host => 'http://ws.contoso.com',
  33. url => '/books',
  34. content => '<book><isbn>{isbn}</isbn><title>{title}</title></book>'
  35. filter_response_codes => '404',
  36. handler => \&handleHttpRequest );
  37. $fuzzer->setHttpProxy('http://192.168.1.1:8080');
  38. my $limit = 1000;
  39. $fuzzer->addFuzzingValue('isbn', '<a>'x$limit . 'haha' . '</a>'x$limit);
  40. $fuzzer->addFuzzingValue('isbn', new HTTP::Fuzzer::WordListGenerator(file => 'D:\Tools\Wordlists\books_isbn.txt'));
  41. $fuzzer->addFuzzingValue('title', '<a>'x$limit . 'haha' . '</a>'x$limit);
  42. $fuzzer->addFuzzingValue('title', new HTTP::Fuzzer::WordListGenerator(file => 'D:\Tools\Wordlists\books_title.txt'));
  43. $fuzzer->run();
  44. =head2 FUZZING
  45. Fuzzed parameters are to be surrounded by curly brackets, such as C<{isbn}> or
  46. C<{title}>. For every parameter you have to specify at least one value or
  47. a way of generating values, such as word lists, regular expressions, ...
  48. It is not allowed to encapsulate parameters.
  49. =head2 ENCODINGS
  50. Any part of the request can be encoded as needed:
  51. my $headers = [
  52. Authorization => 'BASIC BASE64{{username}:{password}}'
  53. ];
  54. $fuzzer->addService(
  55. method => 'GET',
  56. host => 'http://ws.contoso.com',
  57. url => '/',
  58. filter_response_codes => '403',
  59. headers => $headers,
  60. handler => \&handleHttpRequest );
  61. All encodings process exactly one argument (eg. C<'{username}:{password}'>,
  62. after substituting the value for C<username> and C<password>).
  63. Currently, the following encodings are available:
  64. =over
  65. =item URLENCODE
  66. =item HTMLENCODE
  67. =item BASE64
  68. =item MD5
  69. =item SHA1
  70. =item XML
  71. You may, if this makes sense to you, encapsulate encodings, such as
  72. my $url = 'login.php?username=URLENCODE{{username}}&password=URLENCODE{SHA1{{username}:{password}}}';
  73. =cut
  74. use strict;
  75. use REST::Client;
  76. use HTTP::Fuzzer::Encoder;
  77. use Data::Dumper;
  78. use Carp::Assert;
  79. use constant {
  80. E_XML => \&escape_xml,
  81. E_URI => \&uri_encode,
  82. E_HTML => \&htmlentities
  83. };
  84. use constant {
  85. FUZZER_DEBUG => 0
  86. };
  87. =pod
  88. Running mode:
  89. OFFLINE => the callback function is invocated with all values despite responseCode and responseContent;
  90. the request is not sent
  91. ONLINE => send a request and invoke the callback function with request and response parameters
  92. =cut
  93. use constant {
  94. OFFLINE => 'offline',
  95. ONLINE => 'online'
  96. };
  97. sub TRACE {
  98. print (@_) if(FUZZER_DEBUG);
  99. }
  100. sub extract_params($$) {
  101. my $self = shift;
  102. my $svc = shift;
  103. my @params = ();
  104. # find last parameter and remove it from the string
  105. while ($svc =~ s/(.*)PARAMETER\{(\w+)\}(.*)/$1$3/) {
  106. my $param = $2;
  107. $param =~ m/[a-zA-Z0-9]+/ or die "invalid character in parameter '$param'";
  108. unshift @params, $param;
  109. }
  110. return @params
  111. }
  112. sub combine_values($$$$) {
  113. my $self = shift;
  114. my $svc = shift;
  115. my $dst = shift;
  116. my $src = shift;
  117. if (@$src == 0) {
  118. $self->sendRequest($svc, $dst);
  119. return;
  120. }
  121. my $param = shift @$src;
  122. #print STDERR "BEGIN handle $param\n";
  123. my $values = $self->{values}->{$param};
  124. # add all generators as default values
  125. if (! defined($values)) {
  126. die "no default values for {$param} found ...\n";
  127. }
  128. # create combinations of values
  129. foreach my $value (@$values) {
  130. if (ref($value) && $value->isa('HTTP::Fuzzer::AbstractGenerator')) {
  131. my $generator = $value;
  132. # acquire required parameters, to pass it to the generator function
  133. my %reqs = ();
  134. for my $req (@{$self->{requirements}->{$param}}) {
  135. $reqs{$req} = $dst->{$req}
  136. or die "unresolved requirement: $param requires $req\n";
  137. }
  138. $generator->init(%reqs);
  139. while ($generator->hasNext()) {
  140. $dst->{$param} = $generator->next();
  141. assert(defined($dst->{$param}));
  142. $self->combine_values($svc, $dst, $src);
  143. }
  144. } else {
  145. $dst->{$param} = $value;
  146. $self->combine_values($svc, $dst, $src);
  147. }
  148. }
  149. #print STDERR "END handle $param\n";
  150. unshift @$src, $param;
  151. }
  152. sub mask_parameters($$) {
  153. my $self = shift;
  154. my $template = shift || return '';
  155. if (ref($template) eq 'HASH') {
  156. while(my($k,$v) = each %$template) {
  157. $template->{$k} = $self->mask_parameters($v);
  158. }
  159. return $template;
  160. }
  161. my $encodings = join('|', @{$self->{encoder}->get_encodings()}, 'PARAMETER');
  162. $template = reverse $template;
  163. $encodings = reverse $encodings;
  164. $template =~ s/(\}\w+\{)(?!$encodings)/$1RETEMARAP/g;
  165. $template = reverse $template;
  166. return $template;
  167. }
  168. sub sendRequest($$$) {
  169. my $self = shift;
  170. my $svc = shift;
  171. my $params = shift;
  172. my @request_params = ();
  173. my $content = undef;
  174. my %response = ();
  175. my $new_svc = $self->{encoder}->apply_all_parameters($svc, $params);
  176. $self->{request_count}++;
  177. my $client = REST::Client->new(
  178. host => $svc->{host},
  179. cert => $svc->{cert},
  180. key => $svc->{key},
  181. ca => $svc->{ca},
  182. follow => 1
  183. );
  184. $client->getUseragent()->ssl_opts('verify_hostname' => 0);
  185. if (defined($self->{http_proxy}) && $self->{http_proxy}) {
  186. $client->getUseragent()->proxy(['http', 'https'], $self->{http_proxy});
  187. }
  188. if ($self->{mode} eq ONLINE) {
  189. $client->request($new_svc->{method},
  190. $new_svc->{url},
  191. $new_svc->{content},
  192. $new_svc->{headers});
  193. # filter unwanted response codes
  194. if (defined $svc->{filter_response_codes}) {
  195. return if ($client->responseCode() =~ m/$svc->{filter_response_codes}/o);
  196. }
  197. $response{responseCode} = $client->responseCode();
  198. $response{responseContent} = $client->responseContent();
  199. $response{responseHeader} = $client->responseHeader();
  200. }
  201. $svc->{handler}->(
  202. params => $params,
  203. method => $new_svc->{method},
  204. host => $new_svc->{host},
  205. url => $new_svc->{url},
  206. content => $new_svc->{content},
  207. headers => $new_svc->{headers},
  208. %response);
  209. }
  210. sub sort_parameters($$) {
  211. my $self = shift;
  212. my $parameters = shift;
  213. my @sorted_parameters = ();
  214. my %dependants = ();
  215. my %handled = ();
  216. my $param_count = 0;
  217. foreach my $param (@$parameters) {
  218. $dependants{$param} = $self->{requirements}->{$param};
  219. $param_count++;
  220. }
  221. # sort topologically
  222. while ($param_count > 0) {
  223. #trace(\%dependants);
  224. my $removed_key = 0;
  225. while (my ($param, $reqs) = each %dependants) {
  226. my $requirement_missing = 0;
  227. foreach my $requirement (@$reqs) {
  228. if (length($requirement)>0 && ! exists $handled{$requirement}) {
  229. $requirement_missing = 1;
  230. last;
  231. }
  232. }
  233. if (not $requirement_missing) {
  234. push @sorted_parameters, $param;
  235. $handled{$param} = 1;
  236. delete $dependants{$param};
  237. $removed_key = 1;
  238. $param_count--;
  239. last;
  240. }
  241. }
  242. #die "unresolvable requirements" if $removed_key == 0;
  243. }
  244. return \@sorted_parameters;
  245. }
  246. sub new($) {
  247. my $class = shift;
  248. my $self = {
  249. services => [],
  250. defaults => {},
  251. engines => {},
  252. responses => {},
  253. http_proxy => undef,
  254. encoder => new HTTP::Fuzzer::Encoder()
  255. };
  256. bless($self, $class);
  257. return $self;
  258. }
  259. sub setHttpProxy($$) {
  260. my $self = shift;
  261. $self->{http_proxy} = shift;
  262. }
  263. sub addService($%) {
  264. my $self = shift;
  265. my %args = @_;
  266. my $svc = {
  267. method => $args{method} || 'GET',
  268. host => $args{host},
  269. url => $self->mask_parameters($args{url}),
  270. content => $self->mask_parameters($args{content}),
  271. headers => $self->mask_parameters($args{headers}) || {},
  272. url_escape => \&url_escape,
  273. content_escape => \&escape_xml,
  274. handler => ($args{handler} or die "required argument missing: 'handler'"),
  275. filter_response_codes => $args{filter_response_codes},
  276. requirements => {}};
  277. push @{$self->{services}}, $svc;
  278. }
  279. sub addRequirement($$$) {
  280. my $self = shift;
  281. my $parameter = shift;
  282. my $requirement = shift;
  283. $self->{requirements}->{$parameter} = [] unless defined($self->{requirements}->{$parameter});
  284. push @{$self->{requirements}->{$parameter}}, $requirement;
  285. }
  286. sub addFuzzingValue($$$) {
  287. my $self = shift;
  288. my $parameter = shift;
  289. my $value = shift;
  290. $self->{values}->{$parameter} ||= [];
  291. push @{$self->{values}->{$parameter}}, $value;
  292. }
  293. sub run($;$) {
  294. my $self = shift;
  295. my $mode = shift || ONLINE;
  296. die "invalid mode: '$mode'"
  297. unless ($mode eq ONLINE || $mode eq OFFLINE);
  298. $self->{mode} = $mode;
  299. foreach my $svc (@{$self->{services}}) {
  300. my @params = $self->extract_params($svc->{url});
  301. if (defined($svc->{content})) {
  302. push @params, $self->extract_params($svc->{content});
  303. }
  304. while (my ($k, $v) = each %{$svc->{headers}}) {
  305. push @params, $self->extract_params($v);
  306. }
  307. my $sorted_parameters = $self->sort_parameters(\@params);
  308. $self->combine_values($svc, {}, $sorted_parameters);
  309. }
  310. }
  311. 1;