本帖最后由 faqiadegege 于 2024-8-21 19:19 编辑
faqia 这里浅浅分析 Zyxel ATP 系列命令注入漏洞
参考:https://vulncheck.com/blog/zyxel-cve-2023-33012#you-get-one-shot
首先,
在其httpd.conf中显示路径
...
LoadModule auth_zyxel_module modules/mod_auth_zyxel.so
...
AuthZyxelSkipPattern
/images/
/lib/
/mobile/
/weblogin.cgi
/admin.cgi
/login.cgi
/error.cgi
/redirect.cgi
/I18N.js/language
/logo/
/ext-js/web-pages/login/no_granted.html
/ssltun.jar/sslapp.jar
/VncViewer.jar
/Forwarder.jar
/eps.jar
/css/
/sdwan_intro.html/sdwan_intro_video.html
/videos/
/webauth_error.cgi
/webauth_relogin.cgi
/SSHTermApplet-jdk1.3.1-dependencies-signed.jar
/SSHTermApplet-jdkbug-workaround-signed.jar
/SSHTermApplet-signed.jar
/commons-logging.properties
/org.apache.commons.logging.LogFactory/fetch_ap_info.cgi
/agree.cgi
/walled_garden.cgi
/payment_transaction.cgi
/paypal_pdt.cgi
/redirect_pdt.cgi
/securepay.cgi
/authorize_dot_net.cgi
/payment_failed.cgi
/customize/
/multi-portal/
/free_time.cgi
/free_time_redirect.cgi
/free_time_transaction.cgi
/free_time_failed.cgi
/js/
/terms_of_service.html
/dynamic_script.cgi
/ext-js/ext/ext-all.js
/ext-js/ext/adapter/ext/ext-base.js
/ext-js/ext/resources/css/ext-all.css
/ext-js/app/common/zyFunction.js
/ext-js/app/common/zld_product_spec.js
/cf_hdf_blockpage.cgi
/2FA-access.cgi
/webauth_ga.cgi
/fbwifi_error.cgi
/fbwifi/
/ztp/cgi-bin/ztp_reg.py
/ztp/cgi-bin/checkdata.py
/ztp/cgi-bin/parse_config.py
/ztp/cgi-bin/checkconn.py
/ztp/cgi-bin/ztppolling.py
/ztp/cgi-bin/activate.py
/ztp/cgi-bin/conn_fail_checking.py
/ztp/cgi-bin/changeLEDst.py
/ztp/cgi-bin/postcertificate.py
/ztp/cgi-bin/serverinit.py
/ztp/cgi-bin/twoFApincode.py
/ztp/cgi-bin/twoFApolling.py
/ztp/cgi-bin/vpn_certificate.py
/ztp/cgi-bin/ztp_bg.py
/ztp/cgi-bin/dumpztplog.py
/ztp/activation_success.html
/ztp/activation_fail.html
/ztp/activationfail.html
/ztp/apply_fail.html
/ztp/twoFAapps.html
/ztp/twoFAsms.html
/ztp/verification_fail.html
/ztp/zld_enabled.html
/ztp/ztp_enabled.html
/ztp/ztp_reg.html
/ztp/css/ztp/images
/ztp/fonts
...
sdwan_interface 和sdwan_iface_ipc
进程间通信时,
可执行代码如下,向V31中组包了大量数据,其中的offset584 的数据被使用了 0x14长度
紧接着,使用pic_sdwan_send_config将组包数据 v31作为参数 发送给接口 sdwan_interface
操作 /var/log/ztplog ,追加数据
将 【IPC】写入到 ztplog 中
将 pic_sdwan_send_config (&v31) 写入到 ztplog中
在 sdwan_interface 接口中,offset_584被添加到system 中command 执行
这两项数据,尝试下载v37版本,但是解压时需要密码,并尝试使用二进制查找该两部分线索,没有搜到,这里无法做出判断
/ztp/cgi-bin/parse_config.py
可访问在其代码中有如下逻辑
[Python] 纯文本查看 复制代码 def main():
form = cgi.FieldStorage()
conf_str = form.getvalue("config")
#### skip ####
if conf_str is None:
conf_str = ""
else:
#### skip ####
if not os.path.exists(ztpinclude.SERVER_SOCK_FILE):
logging.error(
"Cannot find sdwan_interface socket [%s]!" % ztpinclude.SERVER_SOCK_FILE
)
print("ParseError: 0xC0DE0005")
else:
conf_str = urllib.unquote(conf_str)
try:
decoded_config = base64.b64decode(conf_str)
except:
logging.error("invalid base64 str %s" % conf_str)
print("ParseError: 0xC0DE0004")
return
#### skip ####
try:
fout = open(ztpinclude.ZTPFILEPATH + "ztpconf.conf", "w+")
if fout is not None:
fout.write(decoded_config)
ok = True
fout.close()
except Exception as e:
logging.debug("e=%s" % e)
print("ParseError: 0xC0DE0002")
return
#### skip ####
if ok:
ztp_soc.ztp_led_start()
(parse_result, ou, org, cn) = network_parse.parse_result(
ztpinclude.ZTPFILEPATH + "ztpconf.conf"
)
if parse_result == ztpinclude.APPLYSUCC:
csrmgr.new_csrcfg(ou, org, cn)
print(
"ou=%s,org=%s,cn=%s"
% (urllib.quote(ou), urllib.quote(org), urllib.quote(cn))
)
else:
print("ParseError")
else:
print("ParseError: 0xC0DE0006")
如上,未认证的用户可以直接提交config数据并写入文件ztpconf.conf中在上面代码中,最后被写入的config文件,调用parse_result解析逻辑如下
[Python] 纯文本查看 复制代码 def parse_result(filepath):
#### skip ####
if os.path.isfile(filepath):
parser = Parser()
config = parser.parse(filepath)
if check_model_id(config) != 0:
logging.info("Check model id with config fail!!")
return (ztpinclude.MODELIDERR, parm_ou, parm_o, parm_cn)
save_config_data(config)
with open(ztpinclude.ZTPFILEPATH + 'parsed_config', 'w+') as fout:
for configlist in config:
try:
if configlist['proto'] == "cellular":
#### skip ####
elif configlist['proto'] == "static":
#### skip ####
elif configlist['proto'] == "pppoe":
#### skip ####
elif configlist['proto'] == "deviceha":
#### skip ####
elif configlist['proto'] == "certificate":
#### skip ####
elif configlist['proto'] == "vti":
if not handle_vti(configlist, vti_cnt):
break
vti_cnt += 1
elif configlist['proto'] == "gre":
if not handle_gre(configlist, gre_cnt):
break
gre_cnt += 1
except Exception as e:
#### skip ####
return (applyresult, parm_ou, parm_o, parm_cn)
else:
#### skip ####
重点关注上面的解析的两个分支
configlist['proto'] == "vti":时
[Python] 纯文本查看 复制代码 def handle_vti(configlist, idx):
ok = False
qsrname = "/tmp/%s.qsr" % configlist["name"]//qsr文件
logging.info("setting up vti interface")
logging.info("; ".join(["=".join(_) for _ in configlist.items()]))
out = open(qsrname, "w+")
if out:
for k in configlist:
out.write("%s %sn" % (k, configlist[k]))//向qsr中写入
out.flush()
out.close()
else:
return False
# it's time to create vti interface
# sdwan_iface_ipc 7 vti0 192.168.100.1 24 192.168.100.2 qsr /tmp/qsr0.txt
params = [
"/usr/sbin/sdwan_iface_ipc",
"7",
configlist["name"],
configlist["ipaddr"],
configlist["netmask"],
]
if "gateway" in configlist:
params.append(configlist["gateway"])
else:
params.append("-")
params.append("qsr")
params.append(qsrname)
response = subprocess.call(params)//启动执行命令,包含了qsr内容
if response != (256 >> 8):
logging.info("Apply fail : %d %s" % (response, "".join(params)))
applyresult = ztpinclude.APPLYFAIL
ok = False
else:
f = open("/tmp/ignore-nccubs-vpn-reset", "a")
if f is not None:
f.write("%s," % configlist["name"])
f.close()
ok = True
return ok
configlist['proto'] == "gre" 时:
[Python] 纯文本查看 复制代码 def handle_gre(configlist, idx):
ok = False
logging.info("setting up gre interface")
logging.info("; ".join(["=".join(_) for _ in configlist.items()]))
# it's time to create gre interface
# sdwan_iface_ipc 8 gre1 192.168.100.1 24 192.168.100.2 if:eth0 61.220.240.159 key 190815111 nhrp nhrppsk ciscozyxel nhs 192.168.100.2
# sdwan_iface_ipc 8 gre1 192.168.100.1 24 192.168.100.2 61.220.240.160 61.220.240.159 key 190815111 nhrp nhrppsk ciscozyxel nhs 192.168.100.2
params = [
"/usr/sbin/sdwan_iface_ipc",
"8",
configlist["name"],
configlist["ipaddr"],
configlist["netmask"],
]
if "gateway" in configlist:
params.append(configlist["gateway"])
else:
params.append("-")
if "base" in configlist:
params.append("if:%s" % configlist["base"])
elif "localip" in configlist:
params.append(configlist["localip"])
else:
logging.info("Apply fail: neither base or localip is specificied")
return False
params.append(configlist["remoteip"])
if "key" in configlist://将配置内容追加到params中
params.append("key")
params.append(configlist["key"])
if "nhrp" in configlist and configlist["nhrp"] != "0":
params.append("nhrp")
if "nhrpsecret" in configlist:
params.append("nhrppsk")
params.append(configlist["nhrpsecret"])
if "nhs" in configlist:
params.append("nhs")
params.append(configlist["nhs"])
response = subprocess.call(params)//执行命令,内容为配置信息
if response != (256 >> 8):
logging.info("Apply fail: %d %s" % (response, " ".join(params)))
applyresult = ztpinclude.APPLYFAIL
ok = False
else:
ok = True
return ok
综上,漏洞过程为:使用 gre 分支 向config 传输base64编码的数据
或者
向 /tmp/*.qsr中写入数据
最后sdwan_interface 和 sdwan_iface_ipc 进程间通信中可执行代码
传入参数到 offset_584中
之后pic_sdwan_send_config 将这部分数据组包发给 sdwan_interface
在sdwan_interface 中,offset_584 被发送给 system 执行
其执行结果 + IPC 写入到 /var/log/ztplog 中
此时,只需要触发 IPC 即可
前面说到/ztp/cgi-bin/parse_config.py 可访问
而在其代码中 获取config数据
对数据解析后两个分支 vti和gre中
都使用了/usr/sbin/sdwan_iface_ipc而该服务的参数正好来自config
并且在vti分支,还可以写文件,传入命令
!!!此笔记仅作学习研究
|